mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
feature(console): Rebuild Permissions Picker
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7662 Co-authored-by: Julian <843342+okjulian@users.noreply.github.com> GitOrigin-RevId: c4d5a1d8d24124d435931c48389d6086410ed8ba
This commit is contained in:
parent
0166f1892b
commit
b20a712443
@ -75,7 +75,6 @@ export const useRows = ({
|
||||
columns,
|
||||
options,
|
||||
});
|
||||
console.log({ queryKey });
|
||||
|
||||
return useQuery({
|
||||
queryKey,
|
||||
|
@ -26,9 +26,14 @@ describe('getTableDisplayName', () => {
|
||||
});
|
||||
|
||||
describe('when table is object and includes "name"', () => {
|
||||
it('returns .name', () => {
|
||||
it('returns .name if object has a schema key (Postgres)', () => {
|
||||
expect(getTableDisplayName({ name: 'aName' })).toBe('aName');
|
||||
});
|
||||
it('returns name and other keys concatenated (non Postgres DBs)', () => {
|
||||
expect(getTableDisplayName({ name: 'aName', dataset: 'aDataset' })).toBe(
|
||||
'aDataset.aName'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when table is object without name', () => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
import isObject from 'lodash.isobject';
|
||||
import { isSchemaTable } from '../components/RelationshipForm/utils';
|
||||
|
||||
/*
|
||||
this function isn't entirely generic but it will hold for the current set of native DBs we have & GDC as well
|
||||
@ -17,8 +18,8 @@ export const getTableDisplayName = (table: Table): string => {
|
||||
return table;
|
||||
}
|
||||
|
||||
if (typeof table === 'object' && 'name' in table) {
|
||||
return (table as { name: string }).name;
|
||||
if (typeof table === 'object' && isSchemaTable(table)) {
|
||||
return table.name;
|
||||
}
|
||||
|
||||
if (isObject(table)) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { MetadataTable, Source } from '@/features/hasura-metadata-types';
|
||||
import { AllowedQueryOperation } from '../query';
|
||||
import { TableEntry } from '../../../metadata/types';
|
||||
|
||||
export const getTypeName = ({
|
||||
defaultQueryRoot,
|
||||
@ -8,7 +9,7 @@ export const getTypeName = ({
|
||||
sourceCustomization,
|
||||
}: {
|
||||
defaultQueryRoot: string | never[];
|
||||
configuration?: MetadataTable['configuration'];
|
||||
configuration?: TableEntry['configuration'] | MetadataTable['configuration'];
|
||||
operation: AllowedQueryOperation;
|
||||
sourceCustomization?: Source['customization'];
|
||||
defaultSchema?: string;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useConsoleForm } from '@/new-components/Form';
|
||||
import { Button } from '@/new-components/Button';
|
||||
import { IndicatorCard } from '@/new-components/IndicatorCard';
|
||||
@ -8,7 +8,6 @@ import {
|
||||
useRoles,
|
||||
useSupportedQueryTypes,
|
||||
} from '@/features/MetadataAPI';
|
||||
import { getTableDisplayName } from '@/features/DatabaseRelationships';
|
||||
|
||||
import { PermissionsSchema, schema } from './../schema';
|
||||
import { AccessType, QueryType } from '../types';
|
||||
@ -24,6 +23,8 @@ import {
|
||||
|
||||
import { useFormData, useUpdatePermissions } from './hooks';
|
||||
import ColumnRootFieldPermissions from './components/RootFieldPermissions/RootFieldPermissions';
|
||||
import { useListAllTableColumns } from '@/features/Data';
|
||||
import { useMetadataSource } from '@/features/MetadataAPI';
|
||||
|
||||
export interface ComponentProps {
|
||||
dataSourceName: string;
|
||||
@ -79,9 +80,8 @@ const Component = (props: ComponentProps) => {
|
||||
const rowPermissions = queryType === 'update' ? ['pre', 'post'] : [queryType];
|
||||
|
||||
const { formData, defaultValues } = data || {};
|
||||
|
||||
const {
|
||||
methods: { getValues },
|
||||
methods: { getValues, reset },
|
||||
Form,
|
||||
} = useConsoleForm({
|
||||
schema,
|
||||
@ -90,6 +90,13 @@ const Component = (props: ComponentProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Reset form when default values change
|
||||
// E.g. when switching tables
|
||||
useEffect(() => {
|
||||
const newValues = getValues();
|
||||
reset({ ...newValues, ...defaultValues });
|
||||
}, [roleName, defaultValues]);
|
||||
|
||||
// allRowChecks relates to other queries and is for duplicating from others
|
||||
const allRowChecks = defaultValues?.allRowChecks;
|
||||
|
||||
@ -134,6 +141,9 @@ const Component = (props: ComponentProps) => {
|
||||
}
|
||||
allRowChecks={allRowChecks || []}
|
||||
dataSourceName={dataSourceName}
|
||||
supportedOperators={
|
||||
data?.defaultValues?.supportedOperators ?? []
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
@ -220,11 +230,18 @@ export interface PermissionsFormProps {
|
||||
export const PermissionsForm = (props: PermissionsFormProps) => {
|
||||
const { dataSourceName, table, queryType, roleName } = props;
|
||||
|
||||
const { columns: tableColumns, isLoading: isLoadingTables } =
|
||||
useListAllTableColumns(dataSourceName, table);
|
||||
|
||||
const { data: metadataSource } = useMetadataSource(dataSourceName);
|
||||
|
||||
const { data, isError, isLoading } = useFormData({
|
||||
dataSourceName,
|
||||
table,
|
||||
queryType,
|
||||
roleName,
|
||||
tableColumns,
|
||||
metadataSource,
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
@ -233,7 +250,15 @@ export const PermissionsForm = (props: PermissionsFormProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading || !data) {
|
||||
if (
|
||||
isLoading ||
|
||||
!data ||
|
||||
isLoadingTables ||
|
||||
!tableColumns ||
|
||||
tableColumns?.length === 0 ||
|
||||
!metadataSource ||
|
||||
!data.defaultValues
|
||||
) {
|
||||
return <IndicatorCard status="info">Loading...</IndicatorCard>;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,163 @@
|
||||
import { TableColumn } from '@/features/DataSource';
|
||||
import { RelationshipType } from '../../RelationshipsTable/types';
|
||||
import { MetadataDataSource } from '../../../metadata/types';
|
||||
import { ManualObjectRelationship } from '@/features/hasura-metadata-types';
|
||||
|
||||
const boolOperators = ['_and', '_or', '_not'];
|
||||
export const getBoolOperators = () => {
|
||||
const boolMap = boolOperators.map(boolOperator => ({
|
||||
name: boolOperator,
|
||||
kind: 'boolOperator',
|
||||
meta: null,
|
||||
}));
|
||||
return boolMap;
|
||||
};
|
||||
|
||||
const getExistOperators = () => {
|
||||
return ['_exists'];
|
||||
};
|
||||
|
||||
export const formatTableColumns = (columns: TableColumn[]) => {
|
||||
if (!columns) return [];
|
||||
return columns?.map(column => {
|
||||
return {
|
||||
kind: 'column',
|
||||
name: column.name,
|
||||
meta: { name: column.name, type: column.consoleDataType },
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const formatTableRelationships = (
|
||||
metadataTables: MetadataDataSource['tables']
|
||||
) => {
|
||||
if (!metadataTables) return [];
|
||||
const met = metadataTables.reduce(
|
||||
(tally: RelationshipType[], curr: RelationshipType) => {
|
||||
const object_relationships = curr.object_relationships;
|
||||
if (!object_relationships) return tally;
|
||||
const relations = object_relationships
|
||||
.map(
|
||||
(relationship: {
|
||||
using: {
|
||||
manual_configuration: {
|
||||
remote_table: { dataset: string; name: string };
|
||||
};
|
||||
};
|
||||
name: string;
|
||||
}) => {
|
||||
if (!relationship?.using) return undefined;
|
||||
return {
|
||||
kind: 'relationship',
|
||||
name: relationship?.name,
|
||||
meta: {
|
||||
name: relationship?.name,
|
||||
type: `${relationship?.using?.manual_configuration?.remote_table?.dataset}_${relationship?.using?.manual_configuration?.remote_table?.name}`,
|
||||
isObject: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
)
|
||||
.filter(Boolean);
|
||||
return [...tally, ...relations];
|
||||
},
|
||||
[]
|
||||
);
|
||||
return met;
|
||||
};
|
||||
|
||||
export interface CreateOperatorsArgs {
|
||||
tableName: string;
|
||||
existingPermission?: Record<string, any>;
|
||||
tableColumns: TableColumn[];
|
||||
sourceMetadataTables: MetadataDataSource['tables'] | undefined;
|
||||
}
|
||||
|
||||
export const createOperatorsObject = ({
|
||||
tableName = '',
|
||||
existingPermission,
|
||||
tableColumns,
|
||||
sourceMetadataTables,
|
||||
}: CreateOperatorsArgs): Record<string, any> => {
|
||||
if (!existingPermission) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const data = {
|
||||
boolOperators: boolOperators,
|
||||
existOperators: getExistOperators(),
|
||||
columns: formatTableColumns(tableColumns),
|
||||
relationships: sourceMetadataTables
|
||||
? formatTableRelationships(sourceMetadataTables)
|
||||
: [],
|
||||
};
|
||||
|
||||
const colNames = data.columns.map(col => col.name);
|
||||
const relationships = data.relationships.map(
|
||||
(rel: ManualObjectRelationship) => rel.name
|
||||
);
|
||||
|
||||
const operators = Object.entries(existingPermission).reduce(
|
||||
(_acc, [key, value]) => {
|
||||
if (boolOperators.includes(key)) {
|
||||
return {
|
||||
name: key,
|
||||
typeName: key,
|
||||
type: 'boolOperator',
|
||||
[key]: Array.isArray(value)
|
||||
? value.map((each: Record<string, any>) =>
|
||||
createOperatorsObject({
|
||||
tableName,
|
||||
tableColumns,
|
||||
existingPermission: each,
|
||||
sourceMetadataTables,
|
||||
})
|
||||
)
|
||||
: createOperatorsObject({
|
||||
tableName,
|
||||
tableColumns,
|
||||
existingPermission: value,
|
||||
sourceMetadataTables,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (relationships.includes(key)) {
|
||||
const rel = data.relationships.find(
|
||||
(relationship: ManualObjectRelationship) => key === relationship.name
|
||||
);
|
||||
const typeName = rel?.meta?.type?.type;
|
||||
|
||||
return {
|
||||
name: key,
|
||||
typeName,
|
||||
type: 'relationship',
|
||||
[key]: createOperatorsObject({
|
||||
tableName,
|
||||
existingPermission: value,
|
||||
tableColumns,
|
||||
sourceMetadataTables,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (colNames.includes(key)) {
|
||||
return {
|
||||
name: key,
|
||||
typeName: key,
|
||||
type: 'column',
|
||||
columnOperator: createOperatorsObject({
|
||||
tableName,
|
||||
existingPermission: value,
|
||||
tableColumns,
|
||||
sourceMetadataTables,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return key;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return operators;
|
||||
};
|
@ -2,7 +2,7 @@ import { allowedMetadataTypes } from '@/features/MetadataAPI';
|
||||
|
||||
import { AccessType, QueryType } from '../../types';
|
||||
import { PermissionsSchema } from '../../schema';
|
||||
import { createInsertArgs } from './utils';
|
||||
import { createInsertArgs, ExistingPermission } from './utils';
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
|
||||
interface CreateBodyArgs {
|
||||
@ -104,7 +104,7 @@ interface CreateInsertBodyArgs extends CreateBodyArgs {
|
||||
queryType: QueryType;
|
||||
formData: PermissionsSchema;
|
||||
accessType: AccessType;
|
||||
existingPermissions: any;
|
||||
existingPermissions: ExistingPermission[];
|
||||
driver: string;
|
||||
tables: Table[];
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ type SelectPermissionMetadata = {
|
||||
filter: Record<string, any>;
|
||||
allow_aggregations?: boolean;
|
||||
limit?: number;
|
||||
query_root_fields?: any[];
|
||||
subscription_root_fields?: any[];
|
||||
query_root_fields?: string[];
|
||||
subscription_root_fields?: string[];
|
||||
};
|
||||
|
||||
const createSelectObject = (input: PermissionsSchema) => {
|
||||
@ -26,11 +26,14 @@ const createSelectObject = (input: PermissionsSchema) => {
|
||||
|
||||
// in row permissions builder an extra input is rendered automatically
|
||||
// this will always be empty and needs to be removed
|
||||
|
||||
const filter = Object.entries(input.filter).reduce<Record<string, any>>(
|
||||
(acc, [operator, value]) => {
|
||||
if (operator === '_and' || operator === '_or') {
|
||||
const newValue = (value as any[])?.slice(0, -1);
|
||||
acc[operator] = newValue;
|
||||
const filteredEmptyObjects = (value as any[]).filter(
|
||||
p => Object.keys(p).length !== 0
|
||||
);
|
||||
acc[operator] = filteredEmptyObjects;
|
||||
return acc;
|
||||
}
|
||||
|
||||
@ -95,10 +98,10 @@ export interface CreateInsertArgs {
|
||||
driver: string;
|
||||
}
|
||||
|
||||
interface ExistingPermission {
|
||||
export interface ExistingPermission {
|
||||
table: unknown;
|
||||
role: string;
|
||||
queryType: any;
|
||||
queryType: string;
|
||||
}
|
||||
/**
|
||||
* creates the insert arguments to update permissions
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
QueryRootPermissionType,
|
||||
} from './RootFieldPermissions/types';
|
||||
|
||||
const getAccessText = (queryType: any) => {
|
||||
const getAccessText = (queryType: string) => {
|
||||
if (queryType === 'insert') {
|
||||
return 'to set input for';
|
||||
}
|
||||
@ -94,6 +94,8 @@ export const ColumnPermissionsSection: React.FC<
|
||||
const [showConfirmation, setShowConfirmationModal] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const all = watch();
|
||||
|
||||
const [selectedColumns, queryRootFields, subscriptionRootFields] = watch([
|
||||
'columns',
|
||||
'query_root_fields',
|
||||
|
@ -4,7 +4,7 @@ import { useFormContext } from 'react-hook-form';
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
import { useHttpClient } from '@/features/Network';
|
||||
import { useQuery } from 'react-query';
|
||||
import { DataSource, exportMetadata } from '@/features/DataSource';
|
||||
import { DataSource, exportMetadata, Operator } from '@/features/DataSource';
|
||||
import { areTablesEqual } from '@/features/RelationshipsTable';
|
||||
import { getTypeName } from '@/features/GraphQLUtils';
|
||||
import { InputField } from '@/new-components/Form';
|
||||
@ -40,6 +40,7 @@ export interface RowPermissionsProps {
|
||||
subQueryType?: string;
|
||||
allRowChecks: Array<{ queryType: QueryType; value: string }>;
|
||||
dataSourceName: string;
|
||||
supportedOperators: Operator[];
|
||||
}
|
||||
|
||||
enum SelectedSection {
|
||||
@ -132,6 +133,7 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
subQueryType,
|
||||
allRowChecks,
|
||||
dataSourceName,
|
||||
supportedOperators,
|
||||
}) => {
|
||||
const { data: tableName, isLoading } = useTypeName({ table, dataSourceName });
|
||||
const { register, watch, setValue } = useFormContext();
|
||||
@ -245,7 +247,6 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
|
||||
<div className="pt-4">
|
||||
{!isLoading && tableName ? (
|
||||
<RowPermissionBuilder
|
||||
tableName={tableName}
|
||||
nesting={['filter']}
|
||||
table={table}
|
||||
dataSourceName={dataSourceName}
|
||||
|
@ -9,6 +9,7 @@ import { createDefaultValues } from './utils';
|
||||
import {
|
||||
complicatedExample,
|
||||
exampleWithBoolOperator,
|
||||
exampleWithNotOperator,
|
||||
exampleWithRelationship,
|
||||
handlers,
|
||||
schema,
|
||||
@ -105,6 +106,38 @@ WithDefaultsBool.decorators = [
|
||||
},
|
||||
];
|
||||
|
||||
export const WithDefaultsNot: ComponentStory<
|
||||
typeof RowPermissionBuilder
|
||||
> = args => <RowPermissionBuilder {...args} />;
|
||||
|
||||
WithDefaultsNot.args = {
|
||||
tableName: 'user',
|
||||
nesting: ['filter'],
|
||||
};
|
||||
|
||||
WithDefaultsNot.decorators = [
|
||||
Component => {
|
||||
return (
|
||||
<div style={{ width: 800 }}>
|
||||
<SimpleForm
|
||||
schema={z.any()}
|
||||
options={{
|
||||
defaultValues: createDefaultValues({
|
||||
tableName: 'user',
|
||||
schema,
|
||||
existingPermission: exampleWithNotOperator,
|
||||
tableConfig: {},
|
||||
}),
|
||||
}}
|
||||
onSubmit={console.log}
|
||||
>
|
||||
<Component />
|
||||
</SimpleForm>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
];
|
||||
|
||||
export const WithDefaultsRelationship: ComponentStory<
|
||||
typeof RowPermissionBuilder
|
||||
> = args => <RowPermissionBuilder {...args} />;
|
||||
|
@ -1,73 +1,49 @@
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
import React from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Builder, JsonItem } from './components';
|
||||
import { useIntrospectSchema } from './hooks';
|
||||
import { createDisplayJson } from './utils';
|
||||
import { RowPermissionsInput } from './components';
|
||||
import { usePermissionTables } from './hooks/usePermissionTables';
|
||||
import { usePermissionComparators } from './hooks/usePermissionComparators';
|
||||
import { useMetadataTable } from '../../../../hasura-metadata-api/metadataHooks';
|
||||
import { getMetadataTableCustomName } from './utils/getMetadataTableCustomName';
|
||||
|
||||
interface Props {
|
||||
tableName: string;
|
||||
/**
|
||||
* The builder is a recursive structure
|
||||
* the nesting describes the level of the structure
|
||||
* so react hook form can correctly register the fields
|
||||
* e.g. ['filter', 'Title', '_eq'] would be registered as 'filter.Title._eq'
|
||||
*/
|
||||
nesting: string[];
|
||||
table: Table;
|
||||
dataSourceName: string;
|
||||
}
|
||||
|
||||
export const RowPermissionBuilder = ({
|
||||
tableName,
|
||||
nesting,
|
||||
table,
|
||||
dataSourceName,
|
||||
}: Props) => {
|
||||
const { watch } = useFormContext();
|
||||
const { data: schema } = useIntrospectSchema();
|
||||
const { watch, setValue } = useFormContext();
|
||||
|
||||
// by watching the top level of nesting we can get the values for the whole builder
|
||||
// this value will always be 'filter' or 'check' depending on the query type
|
||||
const value = watch(nesting[0]);
|
||||
const json = createDisplayJson(value || {});
|
||||
const permissionsKey = nesting[0];
|
||||
|
||||
if (!schema) {
|
||||
return null;
|
||||
}
|
||||
const value = watch(permissionsKey);
|
||||
|
||||
const { data: tableConfig } = useMetadataTable(dataSourceName, table);
|
||||
const tables = usePermissionTables({
|
||||
dataSourceName,
|
||||
tableCustomName: getMetadataTableCustomName(tableConfig),
|
||||
});
|
||||
|
||||
const comparators = usePermissionComparators();
|
||||
|
||||
if (!tables) return <>Loading</>;
|
||||
return (
|
||||
<div key={tableName} className="flex flex-col space-y-4 w-full">
|
||||
<div className="p-6 rounded-lg bg-white border border-gray-200 min-h-32 w-full">
|
||||
<AceEditor
|
||||
mode="json"
|
||||
minLines={1}
|
||||
fontSize={14}
|
||||
height="18px"
|
||||
width="100%"
|
||||
theme="github"
|
||||
name={`${tableName}-json-editor`}
|
||||
value={json}
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
setOptions={{ useWorker: false }}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6 rounded-lg bg-white border border-gray-200w-full">
|
||||
<JsonItem text="{" />
|
||||
<div className="py-2">
|
||||
<Builder
|
||||
tableName={tableName}
|
||||
nesting={nesting}
|
||||
schema={schema}
|
||||
dataSourceName={dataSourceName}
|
||||
table={table}
|
||||
/>
|
||||
</div>
|
||||
<JsonItem text="}" />
|
||||
</div>
|
||||
</div>
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={permissions => {
|
||||
setValue(permissionsKey, permissions);
|
||||
}}
|
||||
table={table}
|
||||
tables={tables}
|
||||
permissions={value}
|
||||
comparators={comparators}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,232 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { GraphQLSchema } from 'graphql';
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
import { RenderFormElement } from './RenderFormElement';
|
||||
import { CustomField } from './Fields';
|
||||
import { JsonItem } from './Elements';
|
||||
import { getColumnOperators } from '../utils';
|
||||
|
||||
import { useData } from '../hooks';
|
||||
|
||||
const createKey = (inputs: string[]) => inputs.filter(Boolean).join('.');
|
||||
|
||||
interface Args {
|
||||
value: string;
|
||||
data: ReturnType<typeof useData>['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* return value to be set for dropdown state
|
||||
*/
|
||||
const getNewValues = ({ value, data }: Args) => {
|
||||
const allItemsArray = [
|
||||
...data.boolOperators,
|
||||
...data.columns,
|
||||
...data.relationships,
|
||||
];
|
||||
const selectedItem = allItemsArray.find(item => item.name === value);
|
||||
|
||||
switch (selectedItem?.kind) {
|
||||
case 'boolOperator':
|
||||
return {
|
||||
name: selectedItem.name,
|
||||
typeName: selectedItem.name,
|
||||
type: 'boolOperator',
|
||||
columnOperator: '_eq',
|
||||
};
|
||||
case 'column':
|
||||
return {
|
||||
name: selectedItem.name,
|
||||
typeName: selectedItem.name,
|
||||
type: 'column',
|
||||
columnOperator: '_eq',
|
||||
};
|
||||
case 'relationship':
|
||||
return {
|
||||
name: selectedItem.name,
|
||||
// for relationships the type name will be different from the name
|
||||
// for example if the relationship name is things the type name will be thing
|
||||
// therefore we need both to
|
||||
// 1. correctly display the relationship name in the permission i.e. things
|
||||
// 2. find the information needed about the type from the schema using the type name i.e. thing
|
||||
typeName: selectedItem?.meta?.type?.type,
|
||||
type: 'relationship',
|
||||
columnOperator: '_eq',
|
||||
};
|
||||
default:
|
||||
throw new Error('Case not handled');
|
||||
}
|
||||
};
|
||||
|
||||
interface RenderJsonDisplayProps {
|
||||
dropDownState: { name: string; type: string };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* needed to render the next level of the form differently depending on which item is selected
|
||||
*/
|
||||
const RenderJsonDisplay = (props: RenderJsonDisplayProps) => {
|
||||
const { dropDownState } = props;
|
||||
|
||||
const isObjectType =
|
||||
dropDownState?.type === 'column' ||
|
||||
dropDownState?.type === 'relationship' ||
|
||||
dropDownState?.name === '_not';
|
||||
|
||||
// if nothing is selected render a disabled input
|
||||
if (!dropDownState?.type) {
|
||||
return <CustomField.Input disabled />;
|
||||
}
|
||||
|
||||
if (isObjectType) {
|
||||
return (
|
||||
<>
|
||||
<JsonItem text="{" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (dropDownState?.name === '_and' || dropDownState?.name === '_or') {
|
||||
return (
|
||||
<>
|
||||
<JsonItem text="[" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
tableName: string;
|
||||
/**
|
||||
* The builder is a recursive structure
|
||||
* the nesting describes the level of the structure
|
||||
* so react hook form can correctly register the fields
|
||||
* e.g. ['filter', 'Title', '_eq'] would be registered as 'filter.Title._eq'
|
||||
*/
|
||||
nesting: string[];
|
||||
schema: GraphQLSchema;
|
||||
dataSourceName: string;
|
||||
table: Table;
|
||||
}
|
||||
|
||||
export const Builder = (props: Props) => {
|
||||
const { tableName, nesting, schema, dataSourceName, table } = props;
|
||||
|
||||
const { data, tableConfig } = useData({
|
||||
tableName,
|
||||
schema,
|
||||
// we have to pass in table like this because if it is a relationship if will
|
||||
// fetch the wrong table config otherwise
|
||||
table,
|
||||
dataSourceName,
|
||||
});
|
||||
const { unregister, setValue, getValues } = useFormContext();
|
||||
// the selections from the dropdowns are stored on the form state under the key "operators"
|
||||
// this will be removed for submitting the form
|
||||
// and is generated from the permissions object when rendering the form from existing data
|
||||
const operatorsKey = createKey(['operators', ...nesting]);
|
||||
const dropDownState = getValues(operatorsKey);
|
||||
|
||||
const permissionsKey = createKey([...nesting, dropDownState?.name]);
|
||||
const columnKey = createKey([
|
||||
...nesting,
|
||||
dropDownState?.name,
|
||||
dropDownState?.columnOperator || '_eq',
|
||||
]);
|
||||
|
||||
const columnOperators = React.useMemo(() => {
|
||||
if (dropDownState?.name && dropDownState?.type === 'column' && schema) {
|
||||
return getColumnOperators({
|
||||
tableName,
|
||||
columnName: dropDownState.name,
|
||||
schema,
|
||||
tableConfig,
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [tableName, dropDownState, schema, tableConfig]);
|
||||
|
||||
const handleDropdownChange: React.ChangeEventHandler<
|
||||
HTMLSelectElement
|
||||
> = e => {
|
||||
const value = e.target.value;
|
||||
|
||||
// as the form is populated a json object is built up
|
||||
// when the dropdown changes at a specific level
|
||||
// everything below that level needs to be removed
|
||||
// set value undefined is necessary to remove field arrays
|
||||
if (dropDownState?.name === '_and' || dropDownState?.name === '_or') {
|
||||
setValue(permissionsKey, undefined);
|
||||
}
|
||||
// when the dropdown changes both the permissions object
|
||||
// and operators object need to be unregistered below this level
|
||||
unregister(permissionsKey);
|
||||
unregister(operatorsKey);
|
||||
|
||||
const newValue = getNewValues({ value, data });
|
||||
return setValue(operatorsKey, newValue);
|
||||
};
|
||||
|
||||
const handleColumnChange: React.ChangeEventHandler<HTMLSelectElement> = e => {
|
||||
const target = e.target.value;
|
||||
|
||||
// when the dropdown value changes the previous field needs to be unregistered
|
||||
// so it is removed from the form state
|
||||
unregister(columnKey);
|
||||
setValue(operatorsKey, {
|
||||
...dropDownState,
|
||||
columnOperator: target,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inline-flex pl-6 flex-col w-full border-dashed border-l border-gray-200">
|
||||
<div className="flex items-center">
|
||||
<CustomField.Select
|
||||
title="Relationship"
|
||||
value={dropDownState?.name || '-'}
|
||||
onChange={handleDropdownChange}
|
||||
>
|
||||
<option key="-" value="-">
|
||||
-
|
||||
</option>
|
||||
{Object.entries(data).map(([section, list]) => {
|
||||
return (
|
||||
<optgroup label={section} key={section}>
|
||||
{list.map(item => (
|
||||
<option key={item.name} value={item.name}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
);
|
||||
})}
|
||||
</CustomField.Select>
|
||||
|
||||
<JsonItem text=":" className="mr-4" />
|
||||
|
||||
<RenderJsonDisplay dropDownState={dropDownState} />
|
||||
</div>
|
||||
|
||||
{/* depending on the selection from the drop down different form elements need to render */}
|
||||
{/* for example if "_and" is selected a field array needs to render */}
|
||||
<RenderFormElement
|
||||
key={permissionsKey}
|
||||
columnKey={columnKey}
|
||||
tableName={tableName}
|
||||
dropDownState={dropDownState}
|
||||
columnOperators={columnOperators}
|
||||
handleColumnChange={handleColumnChange}
|
||||
nesting={nesting}
|
||||
schema={schema}
|
||||
table={table}
|
||||
dataSourceName={dataSourceName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,48 @@
|
||||
import { useContext } from 'react';
|
||||
import { allOperators } from '@/components/Common/FilterQuery/utils';
|
||||
import { rowPermissionsContext } from './RowPermissionsProvider';
|
||||
import { tableContext } from './TableProvider';
|
||||
|
||||
const defaultOperators = allOperators.map(o => ({
|
||||
name: o.name,
|
||||
operator: o.alias,
|
||||
}));
|
||||
|
||||
export const Comparator = ({
|
||||
comparator,
|
||||
path,
|
||||
noValue,
|
||||
}: {
|
||||
comparator: string;
|
||||
path: string[];
|
||||
noValue?: boolean;
|
||||
}) => {
|
||||
const { setKey, comparators } = useContext(rowPermissionsContext);
|
||||
const comparatorLevelId = `${path?.join('.')}-select${
|
||||
noValue ? '-is-empty' : ''
|
||||
}`;
|
||||
const { columns } = useContext(tableContext);
|
||||
const columnName = path[path.length - 2];
|
||||
const column = columns.find(c => c.name === columnName);
|
||||
const operators =
|
||||
column?.type && comparators[column.type]?.operators
|
||||
? comparators[column.type].operators
|
||||
: defaultOperators;
|
||||
|
||||
return (
|
||||
<select
|
||||
data-testid={comparatorLevelId}
|
||||
className="border border-gray-200 rounded-md p-2"
|
||||
value={comparator}
|
||||
onChange={e => {
|
||||
setKey({ path, key: e.target.value, type: 'comparator' });
|
||||
}}
|
||||
>
|
||||
{operators.map((o, index) => (
|
||||
<option key={index} value={o.operator}>
|
||||
{o.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
@ -1,32 +0,0 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface Props extends React.ComponentProps<'div'> {
|
||||
text: JsonOptions;
|
||||
}
|
||||
|
||||
type JsonOptions = '{' | '}' | '[' | ']' | '},' | ',' | '"' | ':';
|
||||
|
||||
const chooseColor = (text: JsonOptions) => {
|
||||
switch (text) {
|
||||
case '{':
|
||||
case '}':
|
||||
case '},':
|
||||
return 'text-blue-800';
|
||||
case '[':
|
||||
case ']':
|
||||
return 'text-yellow-500';
|
||||
default:
|
||||
return 'text-black';
|
||||
}
|
||||
};
|
||||
|
||||
export const JsonItem = (props: Props) => {
|
||||
const color = chooseColor(props.text);
|
||||
|
||||
return (
|
||||
<span className={clsx('font-bold text-lg', color, props.className)}>
|
||||
{props.text}
|
||||
</span>
|
||||
);
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
import { Key } from './Key';
|
||||
import { ValueInput } from './ValueInput';
|
||||
|
||||
export const EmptyEntry = ({ path }: { path: string[] }) => {
|
||||
return (
|
||||
<div className="ml-6">
|
||||
<div className="p-2 flex gap-4">
|
||||
<span className="flex gap-4">
|
||||
<Key k={''} path={path} noValue />
|
||||
</span>
|
||||
<ValueInput value={''} path={path} noValue />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,94 @@
|
||||
import { Fragment, useContext, ReactNode } from 'react';
|
||||
import { get, isEmpty, isPlainObject } from 'lodash';
|
||||
import { isComparator, isPrimitive } from './utils/helpers';
|
||||
import { tableContext, TableProvider } from './TableProvider';
|
||||
import { typesContext } from './TypesProvider';
|
||||
import { Key } from './Key';
|
||||
import { Token } from './Token';
|
||||
import { PermissionsInput } from './PermissionsInput';
|
||||
import { EmptyEntry } from './EmptyEntry';
|
||||
|
||||
export const Entry = ({
|
||||
k,
|
||||
v,
|
||||
path,
|
||||
}: {
|
||||
k: string;
|
||||
v: any;
|
||||
path: string[];
|
||||
}) => {
|
||||
const { table } = useContext(tableContext);
|
||||
const isDisabled = k === '_where' && isEmpty(table);
|
||||
const { types } = useContext(typesContext);
|
||||
const { relationships } = useContext(tableContext);
|
||||
let Wrapper: any = Fragment;
|
||||
const type = get(types, path)?.type;
|
||||
|
||||
if (type === 'relationship') {
|
||||
const relationship = relationships.find(
|
||||
r => r.name === path[path.length - 1]
|
||||
);
|
||||
if (relationship) {
|
||||
const relationshipTable = relationship.table;
|
||||
Wrapper = ({ children }: { children?: ReactNode | undefined }) => (
|
||||
<TableProvider table={relationshipTable}>{children}</TableProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ marginLeft: 8 + (path.length + 1) * 4 + 'px' }}
|
||||
className={`my-2 ${isDisabled ? 'bg-gray-50' : ''}`}
|
||||
>
|
||||
<div className={`p-2 ${isPrimitive(v) ? ' flex gap-4' : ''}`}>
|
||||
<span className="flex gap-4">
|
||||
<Key k={k} path={path} />
|
||||
<span>: </span>
|
||||
{Array.isArray(v) ? (
|
||||
<Token token={'['} inline />
|
||||
) : isPrimitive(v) ? null : (
|
||||
<Token token={'{'} inline />
|
||||
)}
|
||||
</span>
|
||||
{k === '_exists' ? (
|
||||
<TableProvider>
|
||||
<Entry k="_where" v={v._where} path={[...path, '_where']} />
|
||||
<Entry k="_table" v={v._table} path={[...path, '_table']} />
|
||||
</TableProvider>
|
||||
) : (
|
||||
<Wrapper>
|
||||
<div
|
||||
className={
|
||||
!isComparator(k) ? `border-dashed border-l border-gray-200` : ''
|
||||
}
|
||||
>
|
||||
<PermissionsInput permissions={v} path={path} />
|
||||
{Array.isArray(v) && k !== '_table' ? (
|
||||
<div className="p-2 ml-6">
|
||||
<Token token={'{'} />
|
||||
<EmptyEntry path={[...path, `${v.length}`]} />
|
||||
<Token token={'}'} />
|
||||
</div>
|
||||
) : null}
|
||||
{isEmpty(v) && isPlainObject(v) && k !== '_table' ? (
|
||||
<EmptyEntry path={path} />
|
||||
) : null}
|
||||
</div>
|
||||
</Wrapper>
|
||||
)}
|
||||
{Array.isArray(v) ? (
|
||||
<div className="flex gap-2">
|
||||
<Token token={']'} inline />
|
||||
<Token token={','} inline />
|
||||
</div>
|
||||
) : isPrimitive(v) ? null : (
|
||||
<div className="flex gap-2">
|
||||
<Token token={'}'} inline />
|
||||
<Token token={','} inline />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,144 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GraphQLSchema } from 'graphql';
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
import { Builder } from './Builder';
|
||||
import { JsonItem } from './Elements';
|
||||
|
||||
interface FieldArrayElementProps {
|
||||
index: number;
|
||||
arrayKey: string;
|
||||
tableName: string;
|
||||
nesting: string[];
|
||||
field: Field;
|
||||
fields: Field[];
|
||||
append: ReturnType<typeof useFieldArray>['append'];
|
||||
schema: GraphQLSchema;
|
||||
dataSourceName: string;
|
||||
table: Table;
|
||||
// tableConfig: ReturnType<typeof useTableConfiguration>['data'];
|
||||
}
|
||||
|
||||
type Field = Record<'id', string>;
|
||||
|
||||
export const FieldArrayElement = (props: FieldArrayElementProps) => {
|
||||
const {
|
||||
index,
|
||||
arrayKey,
|
||||
tableName,
|
||||
field,
|
||||
nesting,
|
||||
fields,
|
||||
append,
|
||||
schema,
|
||||
table,
|
||||
dataSourceName,
|
||||
// tableConfig,
|
||||
} = props;
|
||||
const { watch } = useFormContext();
|
||||
|
||||
// from this we can determine if the dropdown has been selected
|
||||
// if it has and the element is the final field
|
||||
// another element needs to be appended
|
||||
const currentField = watch(`operators.${arrayKey}.${index}`);
|
||||
const isFinalField = fields.length - 1 === index;
|
||||
|
||||
if (currentField && isFinalField) {
|
||||
append({});
|
||||
}
|
||||
|
||||
if (isFinalField) {
|
||||
return (
|
||||
<>
|
||||
<div className="px-6">
|
||||
<JsonItem text="{" />
|
||||
<Builder
|
||||
key={field.id}
|
||||
tableName={tableName}
|
||||
nesting={[...nesting, index.toString()]}
|
||||
schema={schema}
|
||||
dataSourceName={dataSourceName}
|
||||
table={table}
|
||||
// tableConfig={tableConfig}
|
||||
/>
|
||||
<JsonItem text="}" />
|
||||
</div>
|
||||
<JsonItem text="]" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-6">
|
||||
<JsonItem text="{" />
|
||||
<Builder
|
||||
key={field.id}
|
||||
tableName={tableName}
|
||||
nesting={[...nesting, index.toString()]}
|
||||
schema={schema}
|
||||
dataSourceName={dataSourceName}
|
||||
table={table}
|
||||
// tableConfig={tableConfig}
|
||||
/>
|
||||
<JsonItem text="}," />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
tableName: string;
|
||||
nesting: string[];
|
||||
schema: GraphQLSchema;
|
||||
dataSourceName: string;
|
||||
table: Table;
|
||||
// tableConfig: ReturnType<typeof useTableConfiguration>['data'];
|
||||
}
|
||||
|
||||
export const FieldArray = (props: Props) => {
|
||||
const {
|
||||
tableName,
|
||||
nesting,
|
||||
schema,
|
||||
dataSourceName,
|
||||
table,
|
||||
// tableConfig
|
||||
} = props;
|
||||
const arrayKey = nesting.join('.');
|
||||
|
||||
const { fields, append } = useFieldArray({
|
||||
name: arrayKey,
|
||||
});
|
||||
|
||||
// automatically append a new field when the array is empty
|
||||
// necessary to render an element
|
||||
if (fields.length === 0) {
|
||||
append({});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={arrayKey}
|
||||
className="flex flex-col w-full border-dashed border-l border-gray-200"
|
||||
>
|
||||
<div className="flex flex-col py-2">
|
||||
{fields.map((field, index) => (
|
||||
<FieldArrayElement
|
||||
key={field.id}
|
||||
index={index}
|
||||
arrayKey={arrayKey}
|
||||
tableName={tableName}
|
||||
field={field}
|
||||
fields={fields}
|
||||
nesting={nesting}
|
||||
append={append}
|
||||
schema={schema}
|
||||
dataSourceName={dataSourceName}
|
||||
table={table}
|
||||
// tableConfig={tableConfig}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import { JsonItem } from './Elements';
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
(props, ref) => {
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
{props.type === 'text' && <JsonItem text={`"`} />}
|
||||
<label
|
||||
htmlFor={props.id}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
{/* <strong>{props.title}</strong> */}
|
||||
<input
|
||||
ref={ref}
|
||||
className="block w-36 h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus:outline-0 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
|
||||
{...props}
|
||||
/>
|
||||
</label>
|
||||
{props.type === 'text' && <JsonItem text={`"`} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const Select = (props: React.ComponentProps<'select'>) => {
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
<JsonItem text={`"`} />
|
||||
<label
|
||||
htmlFor={props.id}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
{/* <strong>{props.title}</strong> */}
|
||||
<select
|
||||
className="block w-32 h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus:outline-0 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
|
||||
{...props}
|
||||
/>
|
||||
</label>
|
||||
<JsonItem text={`"`} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomField = {
|
||||
Input,
|
||||
Select,
|
||||
};
|
@ -0,0 +1,35 @@
|
||||
import AceEditor from 'react-ace';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { getTableDisplayName } from '@/features/DatabaseRelationships';
|
||||
import { rowPermissionsContext } from './RowPermissionsProvider';
|
||||
|
||||
export const JsonEditor = () => {
|
||||
const { permissions, table, setPermissions } = useContext(
|
||||
rowPermissionsContext
|
||||
);
|
||||
return (
|
||||
<div className="p-6 rounded-lg bg-white border border-gray-200 min-h-32 w-full">
|
||||
<AceEditor
|
||||
mode="json"
|
||||
onChange={value => {
|
||||
try {
|
||||
// Only set new permissions on valid JSON
|
||||
setPermissions(JSON.parse(value));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
minLines={1}
|
||||
fontSize={14}
|
||||
height="18px"
|
||||
width="100%"
|
||||
theme="github"
|
||||
name={`${getTableDisplayName(table)}-json-editor`}
|
||||
value={JSON.stringify(permissions)}
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
setOptions={{ useWorker: false }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import { isComparator } from './utils';
|
||||
import { Operator } from './Operator';
|
||||
import { Comparator } from './Comparator';
|
||||
|
||||
export const Key = ({
|
||||
k,
|
||||
path,
|
||||
noValue,
|
||||
}: {
|
||||
k: string;
|
||||
path: string[];
|
||||
noValue?: boolean;
|
||||
}) => {
|
||||
if (k === '_where' || k === '_table') {
|
||||
return <span className="font-bold">{k}</span>;
|
||||
}
|
||||
|
||||
if (isComparator(k)) {
|
||||
return <Comparator noValue={noValue} comparator={k} path={path} />;
|
||||
}
|
||||
return <Operator noValue={noValue} operator={k} path={path} />;
|
||||
};
|
@ -0,0 +1,83 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useContext } from 'react';
|
||||
import { rowPermissionsContext } from './RowPermissionsProvider';
|
||||
import { tableContext } from './TableProvider';
|
||||
import { PermissionType } from './types';
|
||||
|
||||
export const Operator = ({
|
||||
operator,
|
||||
path,
|
||||
noValue,
|
||||
}: {
|
||||
operator: string;
|
||||
path: string[];
|
||||
noValue?: boolean;
|
||||
}) => {
|
||||
const { operators, setKey } = useContext(rowPermissionsContext);
|
||||
const { columns, table, relationships } = useContext(tableContext);
|
||||
const parent = path[path.length - 1];
|
||||
const operatorLevelId = `${path?.join('.')}-select${noValue ? '-empty' : ''}`;
|
||||
return (
|
||||
<select
|
||||
data-testid={operatorLevelId || 'root-operator-picker'}
|
||||
className="border border-gray-200 rounded-md p-2"
|
||||
value={operator}
|
||||
disabled={parent === '_where' && isEmpty(table)}
|
||||
onChange={e => {
|
||||
const type = e.target.selectedOptions[0].dataset.type as PermissionType;
|
||||
setKey({ path, key: e.target.value, type });
|
||||
}}
|
||||
>
|
||||
PermissionType
|
||||
<option value="">-</option>
|
||||
{operators.boolean?.items.length ? (
|
||||
<optgroup label="Bool operators">
|
||||
{operators.boolean.items.map((item, index) => (
|
||||
<option
|
||||
data-type="boolean"
|
||||
key={'boolean' + index}
|
||||
value={item.value}
|
||||
>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
) : null}
|
||||
{columns.length ? (
|
||||
<optgroup label="Columns">
|
||||
{columns.map((column, index) => (
|
||||
<option
|
||||
data-type="column"
|
||||
key={'column' + index}
|
||||
value={column.name}
|
||||
>
|
||||
{column.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
) : null}
|
||||
{operators.exist?.items.length ? (
|
||||
<optgroup label="Exist operators">
|
||||
{operators.exist.items.map((item, index) => (
|
||||
<option data-type="exist" key={'exist' + index} value={item.value}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
) : null}
|
||||
{relationships?.length ? (
|
||||
<optgroup label="Relationships">
|
||||
{relationships.map((item, index) => (
|
||||
<option
|
||||
data-type="relationship"
|
||||
key={'relationship' + index}
|
||||
value={item.name}
|
||||
>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
) : null}
|
||||
</select>
|
||||
);
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Entry } from './Entry';
|
||||
import { Operator } from './Operator';
|
||||
import { isPrimitive } from './utils';
|
||||
import { ValueInput } from './ValueInput';
|
||||
import { Permissions } from './types';
|
||||
|
||||
export const PermissionsInput = ({
|
||||
permissions,
|
||||
path,
|
||||
}: {
|
||||
permissions: Permissions;
|
||||
path: string[];
|
||||
}) => {
|
||||
const currentPath = path[path.length - 1];
|
||||
if (isEmpty(permissions) && path.length === 0) {
|
||||
return <Operator operator={'_eq'} path={[]} />;
|
||||
}
|
||||
if (isPrimitive(permissions) || currentPath === '_table') {
|
||||
return <ValueInput value={permissions} path={path} />;
|
||||
}
|
||||
if (Array.isArray(permissions)) {
|
||||
return (
|
||||
<>
|
||||
{permissions.map((v, i) => {
|
||||
const index = isEmpty(v) ? 0 : i;
|
||||
return (
|
||||
<PermissionsInput
|
||||
key={path.join('.') + '.' + index}
|
||||
permissions={v}
|
||||
path={[...path, `${index}`]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
const entries = Object.entries(permissions);
|
||||
return (
|
||||
<>
|
||||
{entries.map(([k, v]) => {
|
||||
const childPath = [...path, k];
|
||||
return <Entry key={k} path={childPath} k={k} v={v} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,166 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { GraphQLSchema } from 'graphql';
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
import { CustomField } from './Fields';
|
||||
import { FieldArray } from './FieldArray';
|
||||
import { Builder } from './Builder';
|
||||
import { JsonItem } from './Elements';
|
||||
import { findColumnOperator, getColumnOperators } from '../utils';
|
||||
|
||||
interface Props {
|
||||
columnKey: string;
|
||||
tableName: string;
|
||||
dropDownState: any;
|
||||
columnOperators: ReturnType<typeof getColumnOperators>;
|
||||
handleColumnChange: (i: any) => void;
|
||||
/**
|
||||
* The builder is a recursive structure
|
||||
* the nesting describes the level of the structure
|
||||
* so react hook form can correctly register the fields
|
||||
* e.g. ['filter', 'Title', '_eq'] would be registered as 'filter.Title._eq'
|
||||
*/
|
||||
nesting: string[];
|
||||
schema: GraphQLSchema;
|
||||
table: Table;
|
||||
dataSourceName: string;
|
||||
}
|
||||
|
||||
export const RenderFormElement = (props: Props) => {
|
||||
const {
|
||||
columnKey,
|
||||
tableName,
|
||||
dropDownState,
|
||||
columnOperators,
|
||||
handleColumnChange,
|
||||
nesting,
|
||||
schema,
|
||||
table,
|
||||
dataSourceName,
|
||||
// tableConfig,
|
||||
} = props;
|
||||
|
||||
const { register, setValue, watch } = useFormContext();
|
||||
|
||||
const selectedOperator = React.useMemo(
|
||||
() => findColumnOperator({ columnKey, columnOperators }),
|
||||
[columnOperators, columnKey]
|
||||
);
|
||||
const val = watch(columnKey);
|
||||
const selectedOperatorType = React.useMemo(() => {
|
||||
if (typeof val === 'string' && val.startsWith('X-Hasura-')) {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
return selectedOperator?.type?.type === 'String' ? 'text' : 'number';
|
||||
}, [selectedOperator, val]);
|
||||
|
||||
const setValueAs = (value: 'text' | 'number') => {
|
||||
const isNumber = !Number.isNaN(parseInt(value, 10));
|
||||
if (isNumber) {
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
switch (dropDownState?.type) {
|
||||
case 'column':
|
||||
if (!selectedOperator) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center px-6 border-dashed border-l border-gray-200 py-4">
|
||||
{!!columnOperators.length && (
|
||||
<CustomField.Select
|
||||
title="Column Operator"
|
||||
value={dropDownState.columnOperator}
|
||||
onChange={handleColumnChange}
|
||||
>
|
||||
{columnOperators.map(({ name }) => {
|
||||
return (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</CustomField.Select>
|
||||
)}
|
||||
|
||||
<JsonItem text=":" className="mr-4" />
|
||||
<div className="flex gap-2">
|
||||
<CustomField.Input
|
||||
key={`${columnKey}-${selectedOperatorType}`}
|
||||
title="Column value"
|
||||
type={selectedOperatorType}
|
||||
{...register(columnKey, {
|
||||
setValueAs,
|
||||
})}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="text-blue-800 font-bold text-sm"
|
||||
onClick={() => setValue(columnKey, 'X-Hasura-User-Id')}
|
||||
>
|
||||
[X-Hasura-User-Id]
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<JsonItem text="}" />
|
||||
</div>
|
||||
);
|
||||
// when rendering a relationship the name of the table needs to be updated
|
||||
// to look for values from the connected table
|
||||
case 'relationship':
|
||||
return (
|
||||
<div className="border-l-dashed border-gray-200">
|
||||
<div className="py-4">
|
||||
<Builder
|
||||
key={columnKey}
|
||||
tableName={dropDownState.typeName}
|
||||
nesting={[...nesting, dropDownState.name]}
|
||||
schema={schema}
|
||||
table={table}
|
||||
dataSourceName={dataSourceName}
|
||||
/>
|
||||
</div>
|
||||
<JsonItem text="}" />
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'boolOperator':
|
||||
return (
|
||||
<>
|
||||
{dropDownState.name === '_not' && (
|
||||
<div className="border-l-dashed border-gray-200">
|
||||
<div className="py-4">
|
||||
<Builder
|
||||
key={columnKey}
|
||||
tableName={tableName}
|
||||
nesting={[...nesting, dropDownState.name]}
|
||||
schema={schema}
|
||||
table={table}
|
||||
dataSourceName={dataSourceName}
|
||||
/>
|
||||
</div>
|
||||
<JsonItem text="}" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(dropDownState.name === '_and' || dropDownState.name === '_or') && (
|
||||
<FieldArray
|
||||
key={columnKey}
|
||||
tableName={tableName}
|
||||
nesting={[...nesting, dropDownState.name]}
|
||||
schema={schema}
|
||||
table={table}
|
||||
dataSourceName={dataSourceName}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
@ -0,0 +1,25 @@
|
||||
import clsx from 'clsx';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useContext } from 'react';
|
||||
import { PermissionsInput } from './PermissionsInput';
|
||||
import { rowPermissionsContext } from './RowPermissionsProvider';
|
||||
import { Token } from './Token';
|
||||
|
||||
export const RootInput = () => {
|
||||
const { permissions } = useContext(rowPermissionsContext);
|
||||
return (
|
||||
<div className="p-6 rounded-lg bg-white border border-gray-200w-full">
|
||||
<Token token={'{'} />
|
||||
<div
|
||||
className={clsx(
|
||||
`py-2 border-dashed border-l border-gray-200 `,
|
||||
isEmpty(permissions) && 'pl-6'
|
||||
)}
|
||||
id="permissions-form-builder"
|
||||
>
|
||||
<PermissionsInput permissions={permissions} path={[]} />
|
||||
</div>
|
||||
<Token token={'}'} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,438 @@
|
||||
import { ComponentStory, Meta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { RowPermissionsInput } from './RowPermissionsInput';
|
||||
import { within } from '@testing-library/dom';
|
||||
import { userEvent, waitFor } from '@storybook/testing-library';
|
||||
import { expect } from '@storybook/jest';
|
||||
import { tables } from './__tests__/fixtures/tables';
|
||||
import { comparators } from './__tests__/fixtures/comparators';
|
||||
|
||||
export default {
|
||||
title: 'Features/Permissions/Form/Row Permissions Input',
|
||||
component: RowPermissionsInput,
|
||||
} as Meta;
|
||||
|
||||
export const SetRootLevelPermission: ComponentStory<
|
||||
typeof RowPermissionsInput
|
||||
> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SetExistsPermission: ComponentStory<
|
||||
typeof RowPermissionsInput
|
||||
> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SetMultilevelExistsPermission: ComponentStory<
|
||||
typeof RowPermissionsInput
|
||||
> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SetAndPermission: ComponentStory<
|
||||
typeof RowPermissionsInput
|
||||
> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SetMultilevelAndPermission: ComponentStory<
|
||||
typeof RowPermissionsInput
|
||||
> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SetNotPermission: ComponentStory<
|
||||
typeof RowPermissionsInput
|
||||
> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SetOrPermission: ComponentStory<
|
||||
typeof RowPermissionsInput
|
||||
> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SetMultilevelOrPermission: ComponentStory<
|
||||
typeof RowPermissionsInput
|
||||
> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Empty: ComponentStory<typeof RowPermissionsInput> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Exists: ComponentStory<typeof RowPermissionsInput> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{
|
||||
_exists: {
|
||||
_table: {},
|
||||
_where: {},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SetDisabledExistsPermission: ComponentStory<
|
||||
typeof RowPermissionsInput
|
||||
> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{
|
||||
_exists: {
|
||||
_table: {},
|
||||
_where: {},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ExistsWhere: ComponentStory<typeof RowPermissionsInput> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{
|
||||
_exists: {
|
||||
_table: { dataset: 'bigquery_sample', name: 'sample_table' },
|
||||
_where: {
|
||||
_and: [
|
||||
{ STATUS: { _eq: 'X-Hasura-User-Id' } },
|
||||
{ Period: { _eq: 'Period' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const EmptyExists: ComponentStory<typeof RowPermissionsInput> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{
|
||||
_exists: {
|
||||
_table: {},
|
||||
_where: {},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const And: ComponentStory<typeof RowPermissionsInput> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{
|
||||
_and: [
|
||||
{ STATUS: { _eq: 'X-Hasura-User-Id' } },
|
||||
{ Period: { _eq: 'Period' } },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const EmptyAnd: ComponentStory<typeof RowPermissionsInput> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{
|
||||
_and: [{}],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Not: ComponentStory<typeof RowPermissionsInput> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{
|
||||
_not: { STATUS: { _eq: 'X-Hasura-User-Id' } },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const EmptyNot: ComponentStory<typeof RowPermissionsInput> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{
|
||||
_not: {},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Relationships: ComponentStory<
|
||||
typeof RowPermissionsInput
|
||||
> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={['Album']}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{ Author: { name: { _eq: '' } } }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const RelationshipsColumns: ComponentStory<
|
||||
typeof RowPermissionsInput
|
||||
> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={['Album']}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{ Label: { id: { _eq: '' } } }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ColumnTypes: ComponentStory<typeof RowPermissionsInput> = args => (
|
||||
<RowPermissionsInput
|
||||
onPermissionsChange={action('onPermissionsChange')}
|
||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
permissions={{ Series_reference: { _eq: '' } }}
|
||||
/>
|
||||
);
|
||||
|
||||
SetRootLevelPermission.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(canvas.getByTestId('-select'));
|
||||
await userEvent.selectOptions(canvas.getByTestId('-select'), 'Subject');
|
||||
};
|
||||
|
||||
SetExistsPermission.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.selectOptions(canvas.getByTestId('-select'), '_exists');
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_exists._table-select'),
|
||||
'Label'
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_exists._where-select-empty'),
|
||||
'id'
|
||||
);
|
||||
|
||||
await userEvent.type(
|
||||
canvas.getByTestId('_exists._where.id._eq-input'),
|
||||
'1337'
|
||||
);
|
||||
};
|
||||
|
||||
SetMultilevelExistsPermission.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.selectOptions(canvas.getByTestId('-select'), '_exists');
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_exists._table-select'),
|
||||
'Label'
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_exists._where-select-empty'),
|
||||
'_exists'
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_exists._where._exists._table-select'),
|
||||
'Label'
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_exists._where._exists._where-select-empty'),
|
||||
'id'
|
||||
);
|
||||
|
||||
await userEvent.type(
|
||||
canvas.getByTestId('_exists._where._exists._where.id._eq-input'),
|
||||
'1337'
|
||||
);
|
||||
};
|
||||
|
||||
SetAndPermission.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.selectOptions(canvas.getByTestId('-select'), '_and');
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_and.1-select-empty'),
|
||||
'Series_reference'
|
||||
);
|
||||
|
||||
await userEvent.type(
|
||||
canvas.getByTestId('_and.1.Series_reference._eq-input'),
|
||||
'1337',
|
||||
{ delay: 300 }
|
||||
);
|
||||
};
|
||||
|
||||
SetMultilevelAndPermission.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.selectOptions(canvas.getByTestId('-select'), '_and');
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_and.1-select-empty'),
|
||||
'Series_reference'
|
||||
);
|
||||
|
||||
await userEvent.type(
|
||||
canvas.getByTestId('_and.1.Series_reference._eq-input'),
|
||||
'1337',
|
||||
{ delay: 300 }
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_and.2-select-empty'),
|
||||
'STATUS'
|
||||
);
|
||||
await userEvent.type(canvas.getByTestId('_and.2.STATUS._eq-input'), '1338');
|
||||
};
|
||||
|
||||
SetNotPermission.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.selectOptions(canvas.getByTestId('-select'), '_not');
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_not-select-empty'),
|
||||
'Period'
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_not.Period._eq-select'),
|
||||
'_neq'
|
||||
);
|
||||
};
|
||||
|
||||
SetOrPermission.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.selectOptions(canvas.getByTestId('-select'), '_or');
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_or.1-select-empty'),
|
||||
'Period'
|
||||
);
|
||||
|
||||
await userEvent.type(canvas.getByTestId('_or.1.Period._eq-input'), '1337');
|
||||
};
|
||||
|
||||
SetMultilevelOrPermission.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.selectOptions(canvas.getByTestId('-select'), '_or');
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_or.1-select-empty'),
|
||||
'Series_reference'
|
||||
);
|
||||
|
||||
await userEvent.type(
|
||||
canvas.getByTestId('_or.1.Series_reference._eq-input'),
|
||||
'1337',
|
||||
{ delay: 300 }
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_or.2-select-empty'),
|
||||
'STATUS'
|
||||
);
|
||||
await userEvent.type(canvas.getByTestId('_or.2.STATUS._eq-input'), '1338', {
|
||||
delay: 300,
|
||||
});
|
||||
};
|
||||
|
||||
SetDisabledExistsPermission.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const existElement = canvas.getByTestId('_exists._where-input-no-value');
|
||||
expect(existElement).toHaveAttribute('disabled');
|
||||
|
||||
await userEvent.selectOptions(
|
||||
canvas.getByTestId('_exists._table-select'),
|
||||
'Label'
|
||||
);
|
||||
|
||||
expect(existElement).not.toHaveAttribute('disabled');
|
||||
};
|
@ -0,0 +1,55 @@
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
import { Tables, Operators, Permissions, Comparators } from './types';
|
||||
import { RowPermissionsProvider } from './RowPermissionsProvider';
|
||||
import { TypesProvider } from './TypesProvider';
|
||||
import { TableProvider } from './TableProvider';
|
||||
import { RootInput } from './RootInput';
|
||||
import { JsonEditor } from './JsonEditor';
|
||||
|
||||
export const RowPermissionsInput = ({
|
||||
permissions,
|
||||
tables,
|
||||
table,
|
||||
onPermissionsChange,
|
||||
comparators,
|
||||
}: {
|
||||
permissions: Permissions;
|
||||
tables: Tables;
|
||||
table: Table;
|
||||
onPermissionsChange?: (permissions: Permissions) => void;
|
||||
comparators: Comparators;
|
||||
}) => {
|
||||
const operators: Operators = {
|
||||
boolean: {
|
||||
label: 'Bool operators',
|
||||
items: [
|
||||
{ name: '_and', value: '_and' },
|
||||
{ name: '_not', value: '_not' },
|
||||
{ name: '_or', value: '_or' },
|
||||
],
|
||||
},
|
||||
exist: {
|
||||
label: 'Exist operators',
|
||||
items: [{ name: '_exists', value: '_exists' }],
|
||||
},
|
||||
};
|
||||
return (
|
||||
<RowPermissionsProvider
|
||||
operators={operators}
|
||||
permissions={permissions}
|
||||
table={table}
|
||||
tables={tables}
|
||||
comparators={comparators}
|
||||
onPermissionsChange={onPermissionsChange}
|
||||
>
|
||||
<TypesProvider>
|
||||
<TableProvider table={table}>
|
||||
<div className="flex flex-col space-y-4 w-full">
|
||||
<JsonEditor />
|
||||
<RootInput />
|
||||
</div>
|
||||
</TableProvider>
|
||||
</TypesProvider>
|
||||
</RowPermissionsProvider>
|
||||
);
|
||||
};
|
@ -0,0 +1,91 @@
|
||||
import { set } from 'lodash';
|
||||
import { useCallback, useEffect, useState, createContext } from 'react';
|
||||
import { Permissions, RowPermissionsState } from './types';
|
||||
import { updateKey } from './utils/helpers';
|
||||
|
||||
export const rowPermissionsContext = createContext<RowPermissionsState>({
|
||||
table: {},
|
||||
tables: [],
|
||||
comparators: {},
|
||||
operators: {},
|
||||
permissions: {},
|
||||
setValue: () => {},
|
||||
setKey: () => {},
|
||||
setPermissions: () => {},
|
||||
});
|
||||
|
||||
export const RowPermissionsProvider = ({
|
||||
children,
|
||||
operators,
|
||||
permissions,
|
||||
table,
|
||||
tables,
|
||||
comparators,
|
||||
onPermissionsChange,
|
||||
}: Pick<
|
||||
RowPermissionsState,
|
||||
'permissions' | 'operators' | 'table' | 'tables' | 'comparators'
|
||||
> & {
|
||||
children?: React.ReactNode | undefined;
|
||||
onPermissionsChange?: (permissions: Permissions) => void;
|
||||
}) => {
|
||||
const [permissionsState, setPermissionsState] = useState<
|
||||
Pick<RowPermissionsState, 'permissions' | 'operators'>
|
||||
>({ operators, permissions });
|
||||
const stringifiedPermissions = JSON.stringify(permissionsState.permissions);
|
||||
const stringifiedServerPermissionsValue = JSON.stringify(permissions);
|
||||
|
||||
const setValue = useCallback<RowPermissionsState['setValue']>(
|
||||
(path, value) => {
|
||||
const clone = { ...permissionsState };
|
||||
const newPermissions = set(clone, ['permissions', ...path], value);
|
||||
setPermissionsState(newPermissions);
|
||||
onPermissionsChange?.(newPermissions.permissions);
|
||||
},
|
||||
[permissionsState, setPermissionsState, onPermissionsChange]
|
||||
);
|
||||
const setKey = useCallback<RowPermissionsState['setKey']>(
|
||||
({ key, path, type }) => {
|
||||
const newPermissions = updateKey({
|
||||
permissionsState,
|
||||
newKey: key,
|
||||
keyPath: path,
|
||||
type,
|
||||
});
|
||||
setPermissionsState(newPermissions);
|
||||
onPermissionsChange?.(newPermissions.permissions);
|
||||
},
|
||||
[permissionsState, setPermissionsState, onPermissionsChange]
|
||||
);
|
||||
|
||||
const setPermissions = useCallback<RowPermissionsState['setPermissions']>(
|
||||
permissions => {
|
||||
setPermissionsState({ ...permissionsState, permissions });
|
||||
// Set outside permissions when internals change
|
||||
onPermissionsChange?.(permissions);
|
||||
},
|
||||
[permissionsState, setPermissionsState, onPermissionsChange]
|
||||
);
|
||||
|
||||
// Set internal state when outside permissions change
|
||||
useEffect(() => {
|
||||
if (JSON.stringify(permissions) !== stringifiedPermissions) {
|
||||
setPermissionsState({ ...permissionsState, permissions });
|
||||
}
|
||||
}, [stringifiedServerPermissionsValue, setPermissionsState]);
|
||||
return (
|
||||
<rowPermissionsContext.Provider
|
||||
value={{
|
||||
...permissionsState,
|
||||
setValue,
|
||||
setKey,
|
||||
table,
|
||||
setPermissions,
|
||||
tables,
|
||||
comparators,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</rowPermissionsContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,58 @@
|
||||
import { useState, useContext, useEffect, createContext } from 'react';
|
||||
import { areTablesEqual } from '@/features/hasura-metadata-api';
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
import { rowPermissionsContext } from './RowPermissionsProvider';
|
||||
import { Columns, Relationships, TableContext } from './types';
|
||||
import { getTableDisplayName } from '../../../../../DatabaseRelationships/utils/helpers';
|
||||
|
||||
export const tableContext = createContext<TableContext>({
|
||||
table: {},
|
||||
setTable: () => {},
|
||||
comparator: undefined,
|
||||
setComparator: () => {},
|
||||
columns: [],
|
||||
setColumns: () => {},
|
||||
relationships: [],
|
||||
setRelationships: () => {},
|
||||
});
|
||||
|
||||
export const TableProvider = ({
|
||||
children,
|
||||
table: defaultTable,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
table?: Table;
|
||||
}) => {
|
||||
const [table, setTableName] = useState<Table>(defaultTable || {});
|
||||
const [comparator, setComparator] = useState<string | undefined>();
|
||||
const [columns, setColumns] = useState<Columns>([]);
|
||||
const [relationships, setRelationships] = useState<Relationships>([]);
|
||||
const { tables } = useContext(rowPermissionsContext);
|
||||
// Stringify values to get a stable value for useEffect
|
||||
const stringifiedTable = JSON.stringify(table);
|
||||
const stringifiedTables = JSON.stringify(tables);
|
||||
useEffect(() => {
|
||||
const foundTable = tables.find(t => areTablesEqual(t.table, table));
|
||||
if (foundTable) {
|
||||
setColumns(foundTable.columns);
|
||||
setRelationships(foundTable.relationships);
|
||||
}
|
||||
}, [stringifiedTable, stringifiedTables, setColumns, setRelationships]);
|
||||
|
||||
return (
|
||||
<tableContext.Provider
|
||||
value={{
|
||||
columns,
|
||||
setColumns,
|
||||
table,
|
||||
setTable: setTableName,
|
||||
relationships,
|
||||
setRelationships,
|
||||
comparator,
|
||||
setComparator,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</tableContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
export const Token = ({ token, inline }: { token: any; inline?: boolean }) => {
|
||||
return (
|
||||
<div
|
||||
className={`font-bold text-lg text-black ${inline ? 'inline-block' : ''}`}
|
||||
>
|
||||
{token}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
import { TypesContext, PermissionType } from './types';
|
||||
import {
|
||||
useState,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { rowPermissionsContext } from './RowPermissionsProvider';
|
||||
import { set, unset } from 'lodash';
|
||||
import { getPermissionTypes } from './utils/typeProviderHelpers';
|
||||
|
||||
export const typesContext = createContext<TypesContext>({
|
||||
types: {},
|
||||
setType: () => {},
|
||||
});
|
||||
|
||||
// Provides types for permissions
|
||||
// This is used to determine if a permission is a column or relationship
|
||||
export const TypesProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [types, setTypes] = useState<Record<string, { type: PermissionType }>>(
|
||||
{}
|
||||
);
|
||||
const { permissions, tables, table } = useContext(rowPermissionsContext);
|
||||
const setType = useCallback(
|
||||
({
|
||||
type,
|
||||
path,
|
||||
value,
|
||||
}: {
|
||||
type: PermissionType;
|
||||
path: string[];
|
||||
value: any;
|
||||
}) => {
|
||||
setTypes(prev => {
|
||||
// Remove old path
|
||||
const newTypes = { ...prev };
|
||||
unset(newTypes, path.join('.'));
|
||||
// Set new type on new path
|
||||
set(newTypes, [...path.slice(0, -1), value].join('.'), { type });
|
||||
|
||||
return newTypes;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
// Stringify values to get a stable value for useEffect
|
||||
const jsonPermissions = JSON.stringify(permissions);
|
||||
const stringifiedTable = JSON.stringify(table);
|
||||
// Recursively set types
|
||||
useEffect(() => {
|
||||
const newTypes = getPermissionTypes(tables, table, permissions);
|
||||
|
||||
setTypes(newTypes);
|
||||
}, [jsonPermissions, setTypes, stringifiedTable]);
|
||||
|
||||
return (
|
||||
<typesContext.Provider value={{ types, setType }}>
|
||||
{children}
|
||||
</typesContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,106 @@
|
||||
import { areTablesEqual } from '@/features/hasura-metadata-api';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { rowPermissionsContext } from './RowPermissionsProvider';
|
||||
import { tableContext } from './TableProvider';
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
import { getTableDisplayName } from '@/features/DatabaseRelationships';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Button } from '@/new-components/Button';
|
||||
import { graphQLTypeToJsType, isComparator } from './utils';
|
||||
|
||||
export const ValueInput = ({
|
||||
value,
|
||||
path,
|
||||
noValue,
|
||||
}: {
|
||||
value: any;
|
||||
path: string[];
|
||||
noValue?: boolean;
|
||||
}) => {
|
||||
const { setValue, tables, comparators } = useContext(rowPermissionsContext);
|
||||
const { table, columns, setTable, setComparator } = useContext(tableContext);
|
||||
const comparatorName = path[path.length - 1];
|
||||
const componentLevelId = `${path.join('.')}-select${
|
||||
noValue ? '-no-value' : ''
|
||||
}`;
|
||||
const componentLevelInputId = `${path.join('.')}-input${
|
||||
noValue ? '-no-value' : ''
|
||||
}`;
|
||||
const stringifiedTable = JSON.stringify(table);
|
||||
// Sync table name with ColumnsContext table value
|
||||
useEffect(() => {
|
||||
if (comparatorName === '_table' && !areTablesEqual(value, table)) {
|
||||
setTable(value);
|
||||
}
|
||||
}, [comparatorName, stringifiedTable, value, setTable, setComparator]);
|
||||
|
||||
if (comparatorName === '_table') {
|
||||
// Select table
|
||||
return (
|
||||
<div className="ml-6">
|
||||
<div className="p-2 flex gap-4">
|
||||
<select
|
||||
data-testid={componentLevelId}
|
||||
className="border border-gray-200 rounded-md"
|
||||
value={JSON.stringify(value)}
|
||||
onChange={e => {
|
||||
setValue(path, JSON.parse(e.target.value) as Table);
|
||||
}}
|
||||
>
|
||||
<option value="">-</option>
|
||||
{tables.map(t => {
|
||||
const tableDisplayName = getTableDisplayName(t.table);
|
||||
return (
|
||||
// Call JSON.stringify becayse value cannot be array or object. Will be parsed in setValue
|
||||
<option key={tableDisplayName} value={JSON.stringify(t.table)}>
|
||||
{tableDisplayName}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const parent = path[path.length - 2];
|
||||
const column = columns.find(c => c.name === parent);
|
||||
const comparator = comparators[column?.type || '']?.operators.find(
|
||||
o => o.operator === comparatorName
|
||||
);
|
||||
const jsType = typeof graphQLTypeToJsType(value, comparator?.type);
|
||||
const inputType =
|
||||
jsType === 'boolean' ? 'checkbox' : jsType === 'string' ? 'text' : 'number';
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
data-testid={`${componentLevelInputId}`}
|
||||
disabled={comparatorName === '_where' && isEmpty(table)}
|
||||
className="border border-gray-200 rounded-md p-2 !mr-4"
|
||||
// Set checked attribute if jsType is boolean, value otherwise
|
||||
{...(jsType === 'boolean' ? { checked: value } : { value })}
|
||||
type={inputType}
|
||||
onChange={e => {
|
||||
if (jsType === 'boolean') {
|
||||
setValue(path, e.target.checked);
|
||||
} else {
|
||||
setValue(
|
||||
path,
|
||||
graphQLTypeToJsType(e.target.value, comparator?.type)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isComparator(comparatorName) && (
|
||||
<Button
|
||||
disabled={comparatorName === '_where' && isEmpty(table)}
|
||||
onClick={() => setValue(path, 'X-Hasura-User-Id')}
|
||||
mode="default"
|
||||
>
|
||||
[x-hasura-user-id]
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,475 @@
|
||||
import { Comparators } from '../../types';
|
||||
import {
|
||||
BooleanType,
|
||||
FloatType,
|
||||
IntType,
|
||||
StringType,
|
||||
stringType,
|
||||
} from './graphql';
|
||||
|
||||
export const comparators: Comparators = {
|
||||
number_SQLite_comparison_exp: {
|
||||
operators: [
|
||||
{
|
||||
name: 'equals',
|
||||
operator: '_eq',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: '>',
|
||||
operator: '_gt',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: '>=',
|
||||
operator: '_gte',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: 'in',
|
||||
operator: '_in',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: 'is null',
|
||||
operator: '_is_null',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: '<',
|
||||
operator: '_lt',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: '<=',
|
||||
operator: '_lte',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: 'not equals',
|
||||
operator: '_neq',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: 'not in',
|
||||
operator: '_nin',
|
||||
type: IntType,
|
||||
},
|
||||
],
|
||||
},
|
||||
string_SQLite_comparison_exp: {
|
||||
operators: [
|
||||
{
|
||||
name: 'equals',
|
||||
operator: '_eq',
|
||||
type: stringType,
|
||||
},
|
||||
{
|
||||
name: '>',
|
||||
operator: '_gt',
|
||||
type: stringType,
|
||||
},
|
||||
{
|
||||
name: '>=',
|
||||
operator: '_gte',
|
||||
type: stringType,
|
||||
},
|
||||
{
|
||||
name: 'in',
|
||||
operator: '_in',
|
||||
type: stringType,
|
||||
},
|
||||
{
|
||||
name: 'is null',
|
||||
operator: '_is_null',
|
||||
type: stringType,
|
||||
},
|
||||
{
|
||||
name: '<',
|
||||
operator: '_lt',
|
||||
type: stringType,
|
||||
},
|
||||
{
|
||||
name: '<=',
|
||||
operator: '_lte',
|
||||
type: stringType,
|
||||
},
|
||||
{
|
||||
name: 'not equals',
|
||||
operator: '_neq',
|
||||
type: stringType,
|
||||
},
|
||||
{
|
||||
name: 'not in',
|
||||
operator: '_nin',
|
||||
type: stringType,
|
||||
},
|
||||
],
|
||||
},
|
||||
String_comparison_exp: {
|
||||
operators: [
|
||||
{
|
||||
name: 'equals',
|
||||
operator: '_eq',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: '>',
|
||||
operator: '_gt',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: '>=',
|
||||
operator: '_gte',
|
||||
type: StringType,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'like (case-insensitive)',
|
||||
operator: '_ilike',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: 'in',
|
||||
operator: '_in',
|
||||
type: StringType,
|
||||
},
|
||||
|
||||
{
|
||||
name: '~*',
|
||||
operator: '_iregex',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: 'is null',
|
||||
operator: '_is_null',
|
||||
type: StringType,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'like',
|
||||
operator: '_like',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: '<',
|
||||
operator: '_lt',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: '<=',
|
||||
operator: '_lte',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: 'not equals',
|
||||
operator: '_neq',
|
||||
type: StringType,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'not like (case-insensitive)',
|
||||
operator: '_nilike',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: 'not in',
|
||||
operator: '_nin',
|
||||
type: StringType,
|
||||
},
|
||||
|
||||
{
|
||||
name: '!~*',
|
||||
operator: '_niregex',
|
||||
type: StringType,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'not like',
|
||||
operator: '_nlike',
|
||||
type: StringType,
|
||||
},
|
||||
|
||||
{
|
||||
name: '!~',
|
||||
operator: '_nregex',
|
||||
type: StringType,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'not similar',
|
||||
operator: '_nsimilar',
|
||||
type: StringType,
|
||||
},
|
||||
|
||||
{
|
||||
name: '~',
|
||||
operator: '_regex',
|
||||
type: StringType,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'similar',
|
||||
operator: '_similar',
|
||||
type: StringType,
|
||||
},
|
||||
],
|
||||
},
|
||||
Int_comparison_exp: {
|
||||
operators: [
|
||||
{
|
||||
name: 'equals',
|
||||
operator: '_eq',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: '>',
|
||||
operator: '_gt',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: '>=',
|
||||
operator: '_gte',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: 'in',
|
||||
operator: '_in',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: 'is null',
|
||||
operator: '_is_null',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: '<',
|
||||
operator: '_lt',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: '<=',
|
||||
operator: '_lte',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: 'not equals',
|
||||
operator: '_neq',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: 'not in',
|
||||
operator: '_nin',
|
||||
type: IntType,
|
||||
},
|
||||
],
|
||||
},
|
||||
String_BigQuery_comparison_exp: {
|
||||
operators: [
|
||||
{
|
||||
name: 'equals',
|
||||
operator: '_eq',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: '>',
|
||||
operator: '_gt',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: '>=',
|
||||
operator: '_gte',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: 'in',
|
||||
operator: '_in',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: 'is null',
|
||||
operator: '_is_null',
|
||||
type: StringType,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'like',
|
||||
operator: '_like',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: '<',
|
||||
operator: '_lt',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: '<=',
|
||||
operator: '_lte',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: 'not equals',
|
||||
operator: '_neq',
|
||||
type: StringType,
|
||||
},
|
||||
{
|
||||
name: 'not in',
|
||||
operator: '_nin',
|
||||
type: StringType,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'not like',
|
||||
operator: '_nlike',
|
||||
type: StringType,
|
||||
},
|
||||
],
|
||||
},
|
||||
Float_BigQuery_comparison_exp: {
|
||||
operators: [
|
||||
{
|
||||
name: 'equals',
|
||||
operator: '_eq',
|
||||
type: FloatType,
|
||||
},
|
||||
{
|
||||
name: '>',
|
||||
operator: '_gt',
|
||||
type: FloatType,
|
||||
},
|
||||
{
|
||||
name: '>=',
|
||||
operator: '_gte',
|
||||
type: FloatType,
|
||||
},
|
||||
{
|
||||
name: 'in',
|
||||
operator: '_in',
|
||||
type: FloatType,
|
||||
},
|
||||
{
|
||||
name: 'is null',
|
||||
operator: '_is_null',
|
||||
type: FloatType,
|
||||
},
|
||||
{
|
||||
name: '<',
|
||||
operator: '_lt',
|
||||
type: FloatType,
|
||||
},
|
||||
{
|
||||
name: '<=',
|
||||
operator: '_lte',
|
||||
type: FloatType,
|
||||
},
|
||||
{
|
||||
name: 'not equals',
|
||||
operator: '_neq',
|
||||
type: FloatType,
|
||||
},
|
||||
{
|
||||
name: 'not in',
|
||||
operator: '_nin',
|
||||
type: FloatType,
|
||||
},
|
||||
],
|
||||
},
|
||||
Boolean_BigQuery_comparison_exp: {
|
||||
operators: [
|
||||
{
|
||||
name: 'equals',
|
||||
operator: '_eq',
|
||||
type: BooleanType,
|
||||
},
|
||||
{
|
||||
name: '>',
|
||||
operator: '_gt',
|
||||
type: BooleanType,
|
||||
},
|
||||
{
|
||||
name: '>=',
|
||||
operator: '_gte',
|
||||
type: BooleanType,
|
||||
},
|
||||
{
|
||||
name: 'in',
|
||||
operator: '_in',
|
||||
type: BooleanType,
|
||||
},
|
||||
{
|
||||
name: 'is null',
|
||||
operator: '_is_null',
|
||||
type: BooleanType,
|
||||
},
|
||||
{
|
||||
name: '<',
|
||||
operator: '_lt',
|
||||
type: BooleanType,
|
||||
},
|
||||
{
|
||||
name: '<=',
|
||||
operator: '_lte',
|
||||
type: BooleanType,
|
||||
},
|
||||
{
|
||||
name: 'not equals',
|
||||
operator: '_neq',
|
||||
type: BooleanType,
|
||||
},
|
||||
{
|
||||
name: 'not in',
|
||||
operator: '_nin',
|
||||
type: BooleanType,
|
||||
},
|
||||
],
|
||||
},
|
||||
Int_BigQuery_comparison_exp: {
|
||||
operators: [
|
||||
{
|
||||
name: 'equals',
|
||||
operator: '_eq',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: '>',
|
||||
operator: '_gt',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: '>=',
|
||||
operator: '_gte',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: 'in',
|
||||
operator: '_in',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: 'is null',
|
||||
operator: '_is_null',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: '<',
|
||||
operator: '_lt',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: '<=',
|
||||
operator: '_lte',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: 'not equals',
|
||||
operator: '_neq',
|
||||
type: IntType,
|
||||
},
|
||||
{
|
||||
name: 'not in',
|
||||
operator: '_nin',
|
||||
type: IntType,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@ -0,0 +1,35 @@
|
||||
import {
|
||||
buildClientSchema,
|
||||
GraphQLSchema,
|
||||
IntrospectionQuery,
|
||||
NamedTypeNode,
|
||||
parseType,
|
||||
typeFromAST,
|
||||
} from 'graphql';
|
||||
import { introspection } from './introspection';
|
||||
|
||||
export const schema = new GraphQLSchema(
|
||||
buildClientSchema(
|
||||
introspection.data as unknown as IntrospectionQuery
|
||||
).toConfig()
|
||||
);
|
||||
|
||||
export function createType(typeName: string) {
|
||||
const type = typeFromAST(schema, parseType(typeName) as NamedTypeNode);
|
||||
if (!type) {
|
||||
throw new Error(`Type ${typeName} not found in schema`);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
export const StringType = createType('String');
|
||||
|
||||
export const BooleanType = createType('Boolean');
|
||||
|
||||
export const IntType = createType('Int');
|
||||
|
||||
export const FloatType = createType('Float');
|
||||
|
||||
export const numberType = createType('number');
|
||||
|
||||
export const stringType = createType('string');
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,65 @@
|
||||
import { Tables } from '../../types';
|
||||
|
||||
export const tables: Tables = [
|
||||
{
|
||||
table: { schema: 'public', name: 'Label' },
|
||||
columns: [
|
||||
{ name: 'id', type: 'Int_comparison_exp' },
|
||||
{ name: 'name', type: 'String_comparison_exp' },
|
||||
],
|
||||
relationships: [],
|
||||
},
|
||||
{
|
||||
table: ['Artist'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'number_SQLite_comparison_exp' },
|
||||
{ name: 'name', type: 'string_SQLite_comparison_exp' },
|
||||
{ name: 'surname', type: 'string_SQLite_comparison_exp' },
|
||||
],
|
||||
relationships: [],
|
||||
},
|
||||
{
|
||||
table: ['Album'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'number_SQLite_comparison_exp' },
|
||||
{ name: 'title', type: 'string_SQLite_comparison_exp' },
|
||||
],
|
||||
relationships: [
|
||||
{
|
||||
name: 'Author',
|
||||
table: { schema: 'public', name: 'Artist' },
|
||||
type: 'object',
|
||||
},
|
||||
{
|
||||
name: 'Label',
|
||||
table: { schema: 'public', name: 'Label' },
|
||||
type: 'object',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: ['Customer'],
|
||||
columns: [],
|
||||
relationships: [],
|
||||
},
|
||||
{
|
||||
table: { dataset: 'bigquery_sample', name: 'sample_table' },
|
||||
columns: [
|
||||
{ name: 'Series_reference', type: 'String_BigQuery_comparison_exp' },
|
||||
{ name: 'Period', type: 'Float_BigQuery_comparison_exp' },
|
||||
{ name: 'Data_value', type: 'Float_BigQuery_comparison_exp' },
|
||||
{ name: 'Suppressed', type: 'Boolean_BigQuery_comparison_exp' },
|
||||
{ name: 'STATUS', type: 'String_BigQuery_comparison_exp' },
|
||||
{ name: 'UNITS', type: 'String_BigQuery_comparison_exp' },
|
||||
{ name: 'Magnitude', type: 'Int_comparison_exp' },
|
||||
{ name: 'Subject', type: 'String_BigQuery_comparison_exp' },
|
||||
{ name: 'Group', type: 'String_BigQuery_comparison_exp' },
|
||||
{ name: 'Series_title_1', type: 'String_BigQuery_comparison_exp' },
|
||||
{ name: 'Series_title_2', type: 'String_BigQuery_comparison_exp' },
|
||||
{ name: 'Series_title_3', type: 'String_BigQuery_comparison_exp' },
|
||||
{ name: 'Series_title_4', type: 'String_BigQuery_comparison_exp' },
|
||||
{ name: 'Series_title_5', type: 'String_BigQuery_comparison_exp' },
|
||||
],
|
||||
relationships: [],
|
||||
},
|
||||
];
|
@ -1,3 +1,2 @@
|
||||
export * from './Builder';
|
||||
export * from './FieldArray';
|
||||
export * from './Elements';
|
||||
export * from './types';
|
||||
export * from './RowPermissionsInput';
|
||||
|
@ -0,0 +1,80 @@
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
import { GraphQLType } from 'graphql';
|
||||
|
||||
export type Operators = Record<
|
||||
string,
|
||||
{ label: string; items: Array<{ name: string; value: string }> }
|
||||
>;
|
||||
|
||||
export type Permissions = Record<string, any>;
|
||||
|
||||
export type Columns = Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
graphQLType: GraphQLType;
|
||||
}>;
|
||||
|
||||
export type Relationships = Array<{
|
||||
name: string;
|
||||
table: Table;
|
||||
type: 'object' | 'array';
|
||||
}>;
|
||||
|
||||
export type Tables = Array<{
|
||||
table: Table;
|
||||
columns: Columns;
|
||||
relationships: Relationships;
|
||||
}>;
|
||||
|
||||
export type Comparator = {
|
||||
operators: Array<{
|
||||
name: string;
|
||||
operator: string;
|
||||
defaultValue?: string;
|
||||
type: GraphQLType;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type Comparators = Record<string, Comparator>;
|
||||
|
||||
export type PermissionType =
|
||||
| 'column'
|
||||
| 'exist'
|
||||
| 'relationship'
|
||||
| 'value'
|
||||
| 'comparator';
|
||||
|
||||
export type RowPermissionsState = {
|
||||
operators: Operators;
|
||||
permissions: Permissions;
|
||||
comparators: Comparators;
|
||||
table: Table;
|
||||
tables: Tables;
|
||||
setValue: (path: string[], value: any) => void;
|
||||
setKey: (props: { path: string[]; key: any; type: PermissionType }) => void;
|
||||
setPermissions: (permissions: Permissions) => void;
|
||||
};
|
||||
|
||||
export type TypesContext = {
|
||||
types: Record<string, { type: PermissionType }>;
|
||||
setType: ({
|
||||
type,
|
||||
path,
|
||||
value,
|
||||
}: {
|
||||
type: PermissionType;
|
||||
path: string[];
|
||||
value: any;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export type TableContext = {
|
||||
table: Table;
|
||||
setTable: (table: Table) => void;
|
||||
comparator?: string | undefined;
|
||||
setComparator: (comparator: string | undefined) => void;
|
||||
columns: Columns;
|
||||
setColumns: (columns: Columns) => void;
|
||||
relationships: Relationships;
|
||||
setRelationships: (relationships: Relationships) => void;
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
import { columnsFromSchema } from './columnsFromSchema';
|
||||
import { schema, createType } from '../__tests__/fixtures/graphql';
|
||||
|
||||
describe('columnsFromSchema', () => {
|
||||
it('should return columns from schema', () => {
|
||||
const result = columnsFromSchema(schema);
|
||||
const columns = result['Artist'];
|
||||
expect(columns).toEqual([
|
||||
{
|
||||
name: 'ArtistId',
|
||||
type: 'number_SQLite_comparison_exp',
|
||||
graphQLType: createType('number_SQLite_comparison_exp'),
|
||||
},
|
||||
{
|
||||
name: 'Name',
|
||||
type: 'string_SQLite_comparison_exp',
|
||||
graphQLType: createType('string_SQLite_comparison_exp'),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -0,0 +1,32 @@
|
||||
import { GraphQLSchema, isInputObjectType } from 'graphql';
|
||||
import { Columns } from '../types';
|
||||
|
||||
export function columnsFromSchema(
|
||||
schema: GraphQLSchema | undefined
|
||||
): Record<string, Columns> {
|
||||
if (!schema) {
|
||||
return {};
|
||||
}
|
||||
// Get input types ending in `_bool_exp`. They represent tables
|
||||
// E.g: Artist_bool_exp
|
||||
// For each table input type, get the fields that end with `_comparison_exp`
|
||||
// Return a map of table name to columns
|
||||
const inputObjectTypes = Object.values(schema.getTypeMap()).filter(
|
||||
type => isInputObjectType(type) && type.name.endsWith('_bool_exp')
|
||||
);
|
||||
return inputObjectTypes.reduce((acc, inputType) => {
|
||||
if (!isInputObjectType(inputType)) {
|
||||
return acc;
|
||||
}
|
||||
const columns: Columns = Object.values(inputType.getFields())
|
||||
.filter(field => field.type.toString().endsWith('_comparison_exp'))
|
||||
.map(field => {
|
||||
return {
|
||||
name: field.name,
|
||||
type: field.type.toString(),
|
||||
graphQLType: field.type,
|
||||
};
|
||||
});
|
||||
return { ...acc, [inputType.name.replace('_bool_exp', '')]: columns };
|
||||
}, {} as Record<string, Columns>);
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
import { comparatorsFromSchema } from './comparatorsFromSchema';
|
||||
import { NamedTypeNode, parseType, typeFromAST } from 'graphql';
|
||||
import { schema } from '../__tests__/fixtures/graphql';
|
||||
|
||||
describe('comparatorsFromSchema', () => {
|
||||
it('should return comparators from schema', () => {
|
||||
const result = comparatorsFromSchema(schema);
|
||||
const operator = result['number_SQLite_comparison_exp'];
|
||||
expect(operator?.operators).toEqual([
|
||||
{
|
||||
name: 'equals',
|
||||
operator: '_eq',
|
||||
type: typeFromAST(schema, parseType('number') as NamedTypeNode),
|
||||
},
|
||||
{
|
||||
name: '>',
|
||||
operator: '_gt',
|
||||
type: typeFromAST(schema, parseType('number') as NamedTypeNode),
|
||||
},
|
||||
{
|
||||
name: '>=',
|
||||
operator: '_gte',
|
||||
type: typeFromAST(schema, parseType('number') as NamedTypeNode),
|
||||
},
|
||||
{
|
||||
name: 'in',
|
||||
operator: '_in',
|
||||
type: typeFromAST(schema, parseType('[number!]') as NamedTypeNode),
|
||||
},
|
||||
{
|
||||
name: 'is null',
|
||||
operator: '_is_null',
|
||||
type: typeFromAST(schema, parseType('Boolean') as NamedTypeNode),
|
||||
},
|
||||
{
|
||||
name: '<',
|
||||
operator: '_lt',
|
||||
type: typeFromAST(schema, parseType('number') as NamedTypeNode),
|
||||
},
|
||||
{
|
||||
name: '<=',
|
||||
operator: '_lte',
|
||||
type: typeFromAST(schema, parseType('number') as NamedTypeNode),
|
||||
},
|
||||
{
|
||||
name: 'not equals',
|
||||
operator: '_neq',
|
||||
type: typeFromAST(schema, parseType('number') as NamedTypeNode),
|
||||
},
|
||||
{
|
||||
name: 'not in',
|
||||
operator: '_nin',
|
||||
type: typeFromAST(schema, parseType('[number!]') as NamedTypeNode),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -0,0 +1,26 @@
|
||||
import { allOperators } from '@/components/Common/FilterQuery/utils';
|
||||
import { GraphQLSchema, isInputObjectType } from 'graphql';
|
||||
import { lowerCase } from 'lodash';
|
||||
import { Comparators } from '../types';
|
||||
|
||||
export function comparatorsFromSchema(schema: GraphQLSchema): Comparators {
|
||||
// Get input types ending in `_comparison_exp`
|
||||
// E.g: String_BigQuery_comparison_exp, string_SQLite_comparison_exp, String_comparison_exp, etc...
|
||||
const inputObjectTypes = Object.values(schema.getTypeMap()).filter(
|
||||
type => isInputObjectType(type) && type.name.endsWith('_comparison_exp')
|
||||
);
|
||||
return inputObjectTypes.reduce((acc, inputType) => {
|
||||
if (!isInputObjectType(inputType)) {
|
||||
return acc;
|
||||
}
|
||||
const operators = Object.values(inputType.getFields()).map(field => {
|
||||
const name = field.name;
|
||||
return {
|
||||
name: allOperators.find(o => o.alias === name)?.name ?? lowerCase(name),
|
||||
operator: name,
|
||||
type: field.type,
|
||||
};
|
||||
});
|
||||
return { ...acc, [inputType.name]: { operators } };
|
||||
}, {});
|
||||
}
|
@ -0,0 +1,273 @@
|
||||
import { PermissionType } from '../types';
|
||||
import { updateKey } from './helpers';
|
||||
|
||||
describe('RowPermissionInput -> updateKey should', () => {
|
||||
test('create root value object', () => {
|
||||
const createRootValueInput = {
|
||||
permissionsState: {
|
||||
operators: {
|
||||
boolean: {
|
||||
label: 'Bool operators',
|
||||
items: [
|
||||
{ name: '_and', value: '_and' },
|
||||
{ name: '_not', value: '_not' },
|
||||
{ name: '_or', value: '_or' },
|
||||
],
|
||||
},
|
||||
exist: {
|
||||
label: 'Exist operators',
|
||||
items: [{ name: '_exists', value: '_exists' }],
|
||||
},
|
||||
},
|
||||
permissions: { _exists: { _table: {}, _where: {} } },
|
||||
},
|
||||
newKey: 'Series_reference',
|
||||
keyPath: ['_exists'],
|
||||
type: 'column' as PermissionType,
|
||||
};
|
||||
|
||||
const result = updateKey(createRootValueInput);
|
||||
expect(result).toEqual({
|
||||
operators: {
|
||||
boolean: {
|
||||
items: [
|
||||
{ name: '_and', value: '_and' },
|
||||
{ name: '_not', value: '_not' },
|
||||
{ name: '_or', value: '_or' },
|
||||
],
|
||||
label: 'Bool operators',
|
||||
},
|
||||
exist: {
|
||||
items: [{ name: '_exists', value: '_exists' }],
|
||||
label: 'Exist operators',
|
||||
},
|
||||
},
|
||||
permissions: { Series_reference: { _eq: '' } },
|
||||
});
|
||||
});
|
||||
|
||||
test('remove previous key value if the new is as operator', () => {
|
||||
const removePreviousInput = {
|
||||
permissionsState: {
|
||||
operators: {
|
||||
boolean: {
|
||||
label: 'Bool operators',
|
||||
items: [
|
||||
{ name: '_and', value: '_and' },
|
||||
{ name: '_not', value: '_not' },
|
||||
{ name: '_or', value: '_or' },
|
||||
],
|
||||
},
|
||||
exist: {
|
||||
label: 'Exist operators',
|
||||
items: [{ name: '_exists', value: '_exists' }],
|
||||
},
|
||||
},
|
||||
permissions: { Series_reference: { _eq: '' } },
|
||||
},
|
||||
newKey: '_neq',
|
||||
keyPath: ['Series_reference', '_eq'],
|
||||
type: 'comparator' as PermissionType,
|
||||
};
|
||||
|
||||
const result = updateKey(removePreviousInput);
|
||||
expect(result).toEqual({
|
||||
operators: {
|
||||
boolean: {
|
||||
label: 'Bool operators',
|
||||
items: [
|
||||
{ name: '_and', value: '_and' },
|
||||
{ name: '_not', value: '_not' },
|
||||
{ name: '_or', value: '_or' },
|
||||
],
|
||||
},
|
||||
exist: {
|
||||
label: 'Exist operators',
|
||||
items: [{ name: '_exists', value: '_exists' }],
|
||||
},
|
||||
},
|
||||
permissions: { Series_reference: { _neq: '' } },
|
||||
});
|
||||
});
|
||||
|
||||
test('strip object from array when removing permission', () => {
|
||||
const stripArrayLevelEmptyValuesInput = {
|
||||
permissionsState: {
|
||||
operators: {
|
||||
boolean: {
|
||||
label: 'Bool operators',
|
||||
items: [
|
||||
{ name: '_and', value: '_and' },
|
||||
{ name: '_not', value: '_not' },
|
||||
{ name: '_or', value: '_or' },
|
||||
],
|
||||
},
|
||||
exist: {
|
||||
label: 'Exist operators',
|
||||
items: [{ name: '_exists', value: '_exists' }],
|
||||
},
|
||||
},
|
||||
permissions: { _and: [{}, { Data_value: { _eq: '' } }] },
|
||||
},
|
||||
newKey: '',
|
||||
keyPath: ['_and', '1', 'Data_value'],
|
||||
};
|
||||
const result = updateKey(stripArrayLevelEmptyValuesInput);
|
||||
|
||||
expect(result).toEqual({
|
||||
operators: {
|
||||
boolean: {
|
||||
label: 'Bool operators',
|
||||
items: [
|
||||
{ name: '_and', value: '_and' },
|
||||
{ name: '_not', value: '_not' },
|
||||
{ name: '_or', value: '_or' },
|
||||
],
|
||||
},
|
||||
exist: {
|
||||
label: 'Exist operators',
|
||||
items: [{ name: '_exists', value: '_exists' }],
|
||||
},
|
||||
},
|
||||
permissions: { _and: [{}] },
|
||||
});
|
||||
});
|
||||
|
||||
test('reset root level state when picking empty value', () => {
|
||||
const resetRootLevelWhenPickingEmptyValue = {
|
||||
permissionsState: {
|
||||
operators: {
|
||||
boolean: {
|
||||
label: 'Bool operators',
|
||||
items: [
|
||||
{ name: '_and', value: '_and' },
|
||||
{ name: '_not', value: '_not' },
|
||||
{ name: '_or', value: '_or' },
|
||||
],
|
||||
},
|
||||
exist: {
|
||||
label: 'Exist operators',
|
||||
items: [{ name: '_exists', value: '_exists' }],
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
_not: { Series_reference: { _eq: 'X-Hasura-User-Id' } },
|
||||
},
|
||||
},
|
||||
newKey: '',
|
||||
keyPath: ['_not'],
|
||||
};
|
||||
|
||||
const result = updateKey(resetRootLevelWhenPickingEmptyValue);
|
||||
|
||||
expect(result).toEqual({
|
||||
operators: {
|
||||
boolean: {
|
||||
label: 'Bool operators',
|
||||
items: [
|
||||
{ name: '_and', value: '_and' },
|
||||
{ name: '_not', value: '_not' },
|
||||
{ name: '_or', value: '_or' },
|
||||
],
|
||||
},
|
||||
exist: {
|
||||
label: 'Exist operators',
|
||||
items: [{ name: '_exists', value: '_exists' }],
|
||||
},
|
||||
},
|
||||
permissions: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('reset nested level state when picking empty value', () => {
|
||||
const resetNestedLevelWhenPickingEmptyValue = {
|
||||
permissionsState: {
|
||||
operators: {
|
||||
boolean: {
|
||||
label: 'Bool operators',
|
||||
items: [
|
||||
{ name: '_and', value: '_and' },
|
||||
{ name: '_not', value: '_not' },
|
||||
{ name: '_or', value: '_or' },
|
||||
],
|
||||
},
|
||||
exist: {
|
||||
label: 'Exist operators',
|
||||
items: [{ name: '_exists', value: '_exists' }],
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
_not: {
|
||||
_and: [{}, { Series_reference: { _eq: 'X-Hasura-User-Id' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
newKey: '',
|
||||
keyPath: ['_not', '_and'],
|
||||
};
|
||||
|
||||
const result = updateKey(resetNestedLevelWhenPickingEmptyValue);
|
||||
|
||||
expect(result).toEqual({
|
||||
operators: {
|
||||
boolean: {
|
||||
label: 'Bool operators',
|
||||
items: [
|
||||
{ name: '_and', value: '_and' },
|
||||
{ name: '_not', value: '_not' },
|
||||
{ name: '_or', value: '_or' },
|
||||
],
|
||||
},
|
||||
exist: {
|
||||
label: 'Exist operators',
|
||||
items: [{ name: '_exists', value: '_exists' }],
|
||||
},
|
||||
},
|
||||
permissions: { _not: {} },
|
||||
});
|
||||
});
|
||||
|
||||
test('switch from _not to _and', () => {
|
||||
const resetNestedLevelWhenPickingEmptyValue = {
|
||||
permissionsState: {
|
||||
operators: {
|
||||
boolean: {
|
||||
label: 'Bool operators',
|
||||
items: [
|
||||
{ name: '_and', value: '_and' },
|
||||
{ name: '_not', value: '_not' },
|
||||
{ name: '_or', value: '_or' },
|
||||
],
|
||||
},
|
||||
exist: {
|
||||
label: 'Exist operators',
|
||||
items: [{ name: '_exists', value: '_exists' }],
|
||||
},
|
||||
},
|
||||
permissions: { _not: {} },
|
||||
},
|
||||
newKey: '',
|
||||
keyPath: ['_not', '_and'],
|
||||
};
|
||||
|
||||
const result = updateKey(resetNestedLevelWhenPickingEmptyValue);
|
||||
|
||||
expect(result).toEqual({
|
||||
operators: {
|
||||
boolean: {
|
||||
label: 'Bool operators',
|
||||
items: [
|
||||
{ name: '_and', value: '_and' },
|
||||
{ name: '_not', value: '_not' },
|
||||
{ name: '_or', value: '_or' },
|
||||
],
|
||||
},
|
||||
exist: {
|
||||
label: 'Exist operators',
|
||||
items: [{ name: '_exists', value: '_exists' }],
|
||||
},
|
||||
},
|
||||
permissions: { _not: { _and: {} } },
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,117 @@
|
||||
import { get, isEmpty, set, unset, isObjectLike } from 'lodash';
|
||||
import { RowPermissionsState, PermissionType } from '../types';
|
||||
import { allOperators } from '@/components/Common/FilterQuery/utils';
|
||||
import { GraphQLType, isScalarType } from 'graphql';
|
||||
|
||||
const getKeyPath = ({
|
||||
keyPath,
|
||||
newKey,
|
||||
permissionsState,
|
||||
type,
|
||||
}: {
|
||||
permissionsState: Pick<RowPermissionsState, 'permissions' | 'operators'>;
|
||||
keyPath: string[];
|
||||
newKey: string;
|
||||
type?: PermissionType;
|
||||
}) => {
|
||||
// Store value before deleting key
|
||||
const value = get(permissionsState, ['permissions', ...keyPath]);
|
||||
let path = keyPath;
|
||||
|
||||
if (!isEmpty(value) || type === 'relationship') {
|
||||
unset(permissionsState, ['permissions', ...keyPath]);
|
||||
path = keyPath.slice(0, -1);
|
||||
}
|
||||
|
||||
if (isComparator(newKey) && path.length >= 1) {
|
||||
unset(permissionsState, ['permissions', ...keyPath]);
|
||||
path = keyPath.slice(0, -1);
|
||||
}
|
||||
|
||||
const previousKey = keyPath[keyPath.length - 1];
|
||||
if ((previousKey === '_not' && newKey === '_and') || newKey === '_or') {
|
||||
unset(permissionsState, ['permissions', ...keyPath]);
|
||||
path = keyPath.slice(0, -1);
|
||||
}
|
||||
|
||||
if (newKey === '') return ['permissions', ...path];
|
||||
return ['permissions', ...path, newKey];
|
||||
};
|
||||
|
||||
const getInitialValue = (key: string, type?: PermissionType) => {
|
||||
switch (key) {
|
||||
case '_and':
|
||||
return [{}];
|
||||
case '_or':
|
||||
return [{}];
|
||||
case '_not':
|
||||
return {};
|
||||
case '_exists':
|
||||
return {
|
||||
_where: {},
|
||||
_table: {},
|
||||
};
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'column':
|
||||
// Depends on column type
|
||||
return { _eq: '' };
|
||||
case 'comparator':
|
||||
// Depends on comparator type
|
||||
return '';
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const updateKey = ({
|
||||
permissionsState,
|
||||
newKey,
|
||||
keyPath,
|
||||
type,
|
||||
}: {
|
||||
permissionsState: Pick<RowPermissionsState, 'permissions' | 'operators'>;
|
||||
newKey: string; // New key to be set
|
||||
keyPath: string[]; // Path to the key to be deleted
|
||||
type?: PermissionType;
|
||||
}) => {
|
||||
const clone = { ...permissionsState };
|
||||
const path = getKeyPath({ permissionsState: clone, keyPath, newKey, type });
|
||||
const value = getInitialValue(newKey, type);
|
||||
|
||||
const parentKey = path[path.length - 1];
|
||||
const parentIsArray = parseInt(parentKey);
|
||||
if (parentIsArray) {
|
||||
const prevPath = path.slice(0, -1);
|
||||
const obj = get(clone, prevPath);
|
||||
const filtered = obj.filter((o: Record<string, string>) => !isEmpty(o));
|
||||
return set(clone, prevPath, filtered.length ? filtered : [{}]);
|
||||
}
|
||||
|
||||
return set(clone, path, value);
|
||||
};
|
||||
|
||||
export const isComparator = (k: string) => {
|
||||
return allOperators.find(o => o.alias === k);
|
||||
};
|
||||
|
||||
export const isPrimitive = (value: any) => {
|
||||
return !isObjectLike(value);
|
||||
};
|
||||
|
||||
export function graphQLTypeToJsType(
|
||||
value: string,
|
||||
type: GraphQLType | undefined
|
||||
): boolean | string | number {
|
||||
if (!isScalarType(type)) {
|
||||
return value;
|
||||
}
|
||||
if (type.name === 'Int' || type.name === 'ID' || type.name === 'Float') {
|
||||
return Number(value);
|
||||
} else if (type.name === 'Boolean') {
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
// Default to string on custom scalars since we have no way of knowing if they map to a number or boolean
|
||||
return value;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './helpers';
|
@ -0,0 +1,55 @@
|
||||
import { Tables } from '../types';
|
||||
import { getPermissionTypes } from './typeProviderHelpers';
|
||||
|
||||
describe('RowPermissionInput -> updateKey should', () => {
|
||||
test('create root value object', () => {
|
||||
const tables = [
|
||||
{
|
||||
table: {
|
||||
dataset: 'bigquery_sample',
|
||||
name: 'sample_table',
|
||||
},
|
||||
relationships: [
|
||||
{
|
||||
name: 'bq_test_relation',
|
||||
type: 'object',
|
||||
table: {
|
||||
dataset: 'bigquery_sample',
|
||||
name: 'sample_table',
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'test_2',
|
||||
|
||||
table: {
|
||||
dataset: 'bigquery_sample',
|
||||
name: 'sample_table',
|
||||
},
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
{ name: 'Series_reference', type: 'STRING' },
|
||||
{ name: 'Period', type: 'FLOAT64' },
|
||||
{ name: 'Data_value', type: 'FLOAT64' },
|
||||
{ name: 'Suppressed', type: 'BOOL' },
|
||||
{ name: 'STATUS', type: 'STRING' },
|
||||
{ name: 'UNITS', type: 'STRING' },
|
||||
{ name: 'Magnitude', type: 'INT64' },
|
||||
{ name: 'Subject', type: 'STRING' },
|
||||
{ name: 'Group', type: 'STRING' },
|
||||
{ name: 'Series_title_1', type: 'STRING' },
|
||||
{ name: 'Series_title_2', type: 'STRING' },
|
||||
{ name: 'Series_title_3', type: 'STRING' },
|
||||
{ name: 'Series_title_4', type: 'STRING' },
|
||||
{ name: 'Series_title_5', type: 'STRING' },
|
||||
],
|
||||
},
|
||||
] as Tables;
|
||||
const table = { dataset: 'bigquery_sample', name: 'sample_table' };
|
||||
const permissions = { Series_reference: { _eq: '' } };
|
||||
const result = getPermissionTypes(tables, table, permissions);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
@ -0,0 +1,33 @@
|
||||
import { areTablesEqual } from '@/features/hasura-metadata-api';
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
import { isObjectLike, set } from 'lodash';
|
||||
import { Permissions, Tables } from '../types';
|
||||
|
||||
export const getPermissionTypes = (
|
||||
tables: Tables,
|
||||
table: Table,
|
||||
permissions: Permissions
|
||||
) => {
|
||||
const newTypes = {};
|
||||
const setPermissionTypes = (value: any, path: string[]) => {
|
||||
const relationshipTable = tables.find(t => areTablesEqual(t.table, table));
|
||||
if (
|
||||
relationshipTable?.relationships.find(
|
||||
r => r.name === path[path.length - 1]
|
||||
)
|
||||
) {
|
||||
set(newTypes, path.join('.'), { type: 'relationship' });
|
||||
}
|
||||
if (isObjectLike(value)) {
|
||||
if (Array.isArray(value)) {
|
||||
setPermissionTypes(value[0], [...path, '0']);
|
||||
} else {
|
||||
Object.keys(value).forEach(key => {
|
||||
setPermissionTypes(value[key], [...path, key]);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
setPermissionTypes(permissions, []);
|
||||
return newTypes;
|
||||
};
|
@ -73,6 +73,7 @@ export const useData = ({ tableName, schema, table, dataSourceName }: Args) => {
|
||||
return {
|
||||
data: {
|
||||
boolOperators: [],
|
||||
existOperators: [],
|
||||
columns: [],
|
||||
relationships: [],
|
||||
},
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { useIntrospectSchema } from '.';
|
||||
import { comparatorsFromSchema } from '../components/utils/comparatorsFromSchema';
|
||||
|
||||
export function usePermissionComparators() {
|
||||
const { data: schema } = useIntrospectSchema();
|
||||
return schema ? comparatorsFromSchema(schema) : {};
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import { MetadataSelectors, useMetadata } from '@/features/hasura-metadata-api';
|
||||
import { Tables } from '../components';
|
||||
import { MetadataTable } from '@/features/hasura-metadata-types';
|
||||
import { columnsFromSchema } from '../components/utils/columnsFromSchema';
|
||||
import { getTableDisplayName } from '@/features/DatabaseRelationships';
|
||||
import { useIntrospectSchema } from '.';
|
||||
|
||||
export const usePermissionTables = ({
|
||||
dataSourceName,
|
||||
tableCustomName,
|
||||
}: {
|
||||
dataSourceName: string;
|
||||
tableCustomName: string | undefined;
|
||||
}) => {
|
||||
const { data: tables } = useMetadata(
|
||||
MetadataSelectors.getTables(dataSourceName)
|
||||
);
|
||||
const { data: schema } = useIntrospectSchema();
|
||||
if (!tables) return [];
|
||||
const allColumns = columnsFromSchema(schema);
|
||||
return tables.map(table => {
|
||||
// Table name. Replace . with _ because GraphQL doesn't allow . in field names
|
||||
const tableName = getTableDisplayName(table.table).replace(/\./g, '_');
|
||||
const customName = tableCustomName ?? tableName;
|
||||
|
||||
return {
|
||||
table: table.table,
|
||||
dataSource: dataSourceName,
|
||||
relationships: tableRelationships(table),
|
||||
columns: allColumns[customName] ?? [],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
type Relationships = Tables[number]['relationships'];
|
||||
|
||||
function tableRelationships(table: MetadataTable): Relationships {
|
||||
const relationships = [] as Relationships;
|
||||
if (table.array_relationships) {
|
||||
relationships.push(
|
||||
...table.array_relationships.map(r => {
|
||||
const relatedTable =
|
||||
'manual_configuration' in r.using
|
||||
? r.using.manual_configuration.remote_table
|
||||
: r.using.foreign_key_constraint_on.table;
|
||||
return {
|
||||
name: r.name,
|
||||
type: 'array' as const,
|
||||
table: relatedTable,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
if (table.object_relationships) {
|
||||
relationships.push(
|
||||
...table.object_relationships.map(r => {
|
||||
const relatedTable =
|
||||
'manual_configuration' in r.using
|
||||
? r.using.manual_configuration.remote_table
|
||||
: Array.isArray(r.using.foreign_key_constraint_on) ||
|
||||
typeof r.using.foreign_key_constraint_on === 'string'
|
||||
? r.using.foreign_key_constraint_on
|
||||
: r.using.foreign_key_constraint_on.table;
|
||||
return {
|
||||
name: r.name,
|
||||
type: 'object' as const,
|
||||
table: relatedTable,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
return relationships;
|
||||
}
|
@ -1,62 +1,80 @@
|
||||
export const simpleExample = {
|
||||
Title: {
|
||||
_eq: 'hello',
|
||||
import { TableColumn } from '@/features/DataSource';
|
||||
import { MetadataDataSource } from '@/metadata/types';
|
||||
export const tableColumns = [
|
||||
{
|
||||
name: 'Series_reference',
|
||||
dataType: 'STRING',
|
||||
consoleDataType: 'string',
|
||||
nullable: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const exampleWithBoolOperator = {
|
||||
_and: [
|
||||
{
|
||||
age: {
|
||||
_eq: 8,
|
||||
},
|
||||
},
|
||||
{
|
||||
email: {
|
||||
_eq: 'adsff',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const exampleWithRelationship = {
|
||||
things: {
|
||||
name: {
|
||||
_eq: 'asdas',
|
||||
},
|
||||
{
|
||||
name: 'Period',
|
||||
dataType: 'FLOAT64',
|
||||
consoleDataType: 'number',
|
||||
nullable: false,
|
||||
},
|
||||
};
|
||||
{
|
||||
name: 'Data_value',
|
||||
dataType: 'FLOAT64',
|
||||
consoleDataType: 'number',
|
||||
nullable: false,
|
||||
},
|
||||
] as TableColumn[];
|
||||
|
||||
export const complicatedExample = {
|
||||
_and: [
|
||||
export const sourceMetadata = {
|
||||
name: 'bigquery_test1',
|
||||
kind: 'bigquery',
|
||||
tables: [
|
||||
{
|
||||
things: {
|
||||
_and: [
|
||||
{
|
||||
user: {
|
||||
age: {
|
||||
_eq: 22,
|
||||
table: { dataset: 'bigquery_sample', name: 'sample_table' },
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'bq_test_relation',
|
||||
using: {
|
||||
manual_configuration: {
|
||||
column_mapping: { Period: 'STATUS' },
|
||||
remote_table: {
|
||||
dataset: 'bigquery_sample',
|
||||
name: 'sample_table',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
_or: [
|
||||
{
|
||||
fk_user_id: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'test_2',
|
||||
using: {
|
||||
manual_configuration: {
|
||||
column_mapping: { Period: 'Series_title_2' },
|
||||
insertion_order: null,
|
||||
remote_table: {
|
||||
dataset: 'bigquery_sample',
|
||||
name: 'sample_table',
|
||||
},
|
||||
{
|
||||
user: {
|
||||
age: {
|
||||
_gte: 44,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
select_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: ['Series_reference', 'Period', 'Data_value'],
|
||||
filter: {
|
||||
_and: [{ Series_reference: { _eq: 'X-Hasura-User-Id' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
configuration: {
|
||||
datasets: ['bigquery_sample'],
|
||||
global_select_limit: 1,
|
||||
project_id: 'sensei',
|
||||
service_account: {
|
||||
client_email: '@mail.com',
|
||||
private_key: '',
|
||||
project_id: 'sensei',
|
||||
},
|
||||
},
|
||||
} as MetadataDataSource;
|
||||
|
@ -1,37 +1,55 @@
|
||||
import {
|
||||
schema,
|
||||
simpleExample,
|
||||
exampleWithBoolOperator,
|
||||
exampleWithRelationship,
|
||||
complicatedExample,
|
||||
} from '../mocks';
|
||||
import { tableColumns, sourceMetadata } from '../mocks';
|
||||
import { createDefaultValues } from './createDefaultValues';
|
||||
|
||||
const tableName = 'bigquery_sample_sample_table';
|
||||
|
||||
type Expected = {
|
||||
operators: Record<string, any> | undefined;
|
||||
filter: Record<string, any> | undefined;
|
||||
};
|
||||
|
||||
test('renders basic permission', () => {
|
||||
test('renders root value permission', () => {
|
||||
const result = createDefaultValues({
|
||||
tableName: 'Album',
|
||||
schema,
|
||||
existingPermission: simpleExample,
|
||||
tableConfig: {},
|
||||
tableName,
|
||||
tableColumns,
|
||||
sourceMetadata,
|
||||
existingPermission: { Series_reference: { _eq: 'X-Hasura-User-Id' } },
|
||||
});
|
||||
|
||||
const expected: Expected = {
|
||||
filter: {
|
||||
Title: {
|
||||
_eq: 'hello',
|
||||
},
|
||||
},
|
||||
operators: {
|
||||
filter: {
|
||||
columnOperator: '_eq',
|
||||
name: 'Title',
|
||||
name: 'Series_reference',
|
||||
typeName: 'Series_reference',
|
||||
type: 'column',
|
||||
typeName: 'Title',
|
||||
columnOperator: '_eq',
|
||||
},
|
||||
},
|
||||
filter: { Series_reference: { _eq: 'X-Hasura-User-Id' } },
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('renders _exist permission', () => {
|
||||
const result = createDefaultValues({
|
||||
tableName,
|
||||
tableColumns,
|
||||
sourceMetadata,
|
||||
existingPermission: {
|
||||
_exists: {
|
||||
_table: { dataset: 'bigquery_sample', name: 'sample_table' },
|
||||
_where: { Data_value: { _eq: 'X-Hasura-User-Id' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const expected: Expected = {
|
||||
operators: { filter: '_exists' },
|
||||
filter: {
|
||||
_exists: {
|
||||
_table: { dataset: 'bigquery_sample', name: 'sample_table' },
|
||||
_where: { Data_value: { _eq: 'X-Hasura-User-Id' } },
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -39,164 +57,69 @@ test('renders basic permission', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('renders bool operator permission', () => {
|
||||
test('renders _and operator permission', () => {
|
||||
const result = createDefaultValues({
|
||||
tableName: 'Album',
|
||||
schema,
|
||||
existingPermission: exampleWithBoolOperator,
|
||||
tableConfig: {},
|
||||
tableName,
|
||||
tableColumns,
|
||||
sourceMetadata,
|
||||
existingPermission: {
|
||||
_and: [
|
||||
{ Data_value: { _eq: 'X-Hasura-User-Id' } },
|
||||
{ Group: { _eq: 'X-Hasura-User-Id' } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const expected = {
|
||||
filter: {
|
||||
_and: [
|
||||
{
|
||||
age: {
|
||||
_eq: 8,
|
||||
},
|
||||
},
|
||||
{
|
||||
email: {
|
||||
_eq: 'adsff',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
operators: {
|
||||
filter: {
|
||||
_and: ['age', 'email'],
|
||||
name: '_and',
|
||||
type: 'boolOperator',
|
||||
typeName: '_and',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('renders permission with relationship', () => {
|
||||
const result = createDefaultValues({
|
||||
tableName: 'Album',
|
||||
schema,
|
||||
existingPermission: exampleWithRelationship,
|
||||
tableConfig: {},
|
||||
});
|
||||
|
||||
const expected: Expected = {
|
||||
filter: {
|
||||
things: {
|
||||
name: {
|
||||
_eq: 'asdas',
|
||||
},
|
||||
},
|
||||
},
|
||||
operators: {
|
||||
filter: 'things',
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('renders complex permission', () => {
|
||||
const result = createDefaultValues({
|
||||
tableName: 'user',
|
||||
schema,
|
||||
existingPermission: complicatedExample,
|
||||
tableConfig: {},
|
||||
});
|
||||
|
||||
const expected: Expected = {
|
||||
filter: {
|
||||
_and: [
|
||||
{
|
||||
things: {
|
||||
_and: [
|
||||
{
|
||||
user: {
|
||||
age: {
|
||||
_eq: 22,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
_or: [
|
||||
{
|
||||
fk_user_id: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
{
|
||||
user: {
|
||||
age: {
|
||||
_gte: 44,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
operators: {
|
||||
filter: {
|
||||
type: 'boolOperator',
|
||||
_and: [
|
||||
{
|
||||
name: 'things',
|
||||
things: {
|
||||
_and: [
|
||||
{
|
||||
name: 'user',
|
||||
type: 'relationship',
|
||||
typeName: 'user',
|
||||
user: {
|
||||
columnOperator: '_eq',
|
||||
name: 'age',
|
||||
type: 'column',
|
||||
typeName: 'age',
|
||||
},
|
||||
},
|
||||
{
|
||||
_or: [
|
||||
{
|
||||
columnOperator: '_eq',
|
||||
name: 'fk_user_id',
|
||||
type: 'column',
|
||||
typeName: 'fk_user_id',
|
||||
},
|
||||
{
|
||||
name: 'user',
|
||||
type: 'relationship',
|
||||
typeName: 'user',
|
||||
user: {
|
||||
columnOperator: '_gte',
|
||||
name: 'age',
|
||||
type: 'column',
|
||||
typeName: 'age',
|
||||
},
|
||||
},
|
||||
],
|
||||
name: '_or',
|
||||
type: 'boolOperator',
|
||||
typeName: '_or',
|
||||
},
|
||||
],
|
||||
name: '_and',
|
||||
type: 'boolOperator',
|
||||
typeName: '_and',
|
||||
},
|
||||
type: 'relationship',
|
||||
typeName: 'thing',
|
||||
name: 'Data_value',
|
||||
typeName: 'Data_value',
|
||||
type: 'column',
|
||||
columnOperator: '_eq',
|
||||
},
|
||||
'Group',
|
||||
],
|
||||
name: '_and',
|
||||
type: 'boolOperator',
|
||||
typeName: '_and',
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
_and: [
|
||||
{ Data_value: { _eq: 'X-Hasura-User-Id' } },
|
||||
{ Group: { _eq: 'X-Hasura-User-Id' } },
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('renders _not operator with number value permission', () => {
|
||||
const result = createDefaultValues({
|
||||
tableName,
|
||||
tableColumns,
|
||||
sourceMetadata,
|
||||
existingPermission: { _not: { Data_value: { _eq: 1337 } } },
|
||||
});
|
||||
|
||||
const expected = {
|
||||
operators: {
|
||||
filter: {
|
||||
name: '_not',
|
||||
typeName: '_not',
|
||||
type: 'boolOperator',
|
||||
_not: {
|
||||
name: 'Data_value',
|
||||
typeName: 'Data_value',
|
||||
type: 'column',
|
||||
columnOperator: '_eq',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: { _not: { Data_value: { _eq: 1337 } } },
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
@ -1,111 +1,29 @@
|
||||
import { MetadataTable } from '@/features/hasura-metadata-types';
|
||||
import { GraphQLSchema } from 'graphql';
|
||||
import { getAllColumnsAndOperators } from '.';
|
||||
import { createOperatorsObject } from '../../../PermissionsForm.utils';
|
||||
import { TableColumn } from '@/features/DataSource';
|
||||
import { MetadataDataSource } from '../../../../../../metadata/types';
|
||||
|
||||
export interface CreateOperatorsArgs {
|
||||
tableName: string;
|
||||
schema?: GraphQLSchema;
|
||||
existingPermission?: Record<string, any>;
|
||||
tableConfig: MetadataTable['configuration'];
|
||||
}
|
||||
|
||||
export const createOperatorsObject = ({
|
||||
tableName,
|
||||
schema,
|
||||
existingPermission,
|
||||
tableConfig,
|
||||
}: CreateOperatorsArgs): Record<string, any> => {
|
||||
if (!existingPermission || !schema) {
|
||||
return {};
|
||||
}
|
||||
const data = getAllColumnsAndOperators({ tableName, schema, tableConfig });
|
||||
|
||||
const colNames = data.columns.map(col => col.name);
|
||||
const boolOperators = data.boolOperators.map(bo => bo.name);
|
||||
const relationships = data.relationships.map(rel => rel.name);
|
||||
|
||||
const operators = Object.entries(existingPermission).reduce(
|
||||
(_acc, [key, value]) => {
|
||||
if (boolOperators.includes(key)) {
|
||||
return {
|
||||
name: key,
|
||||
typeName: key,
|
||||
type: 'boolOperator',
|
||||
[key]: Array.isArray(value)
|
||||
? value.map((each: Record<string, any>) =>
|
||||
createOperatorsObject({
|
||||
tableName,
|
||||
schema,
|
||||
existingPermission: each,
|
||||
tableConfig,
|
||||
})
|
||||
)
|
||||
: createOperatorsObject({
|
||||
tableName,
|
||||
schema,
|
||||
existingPermission: value,
|
||||
tableConfig,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (relationships.includes(key)) {
|
||||
const rel = data.relationships.find(r => key === r.name);
|
||||
const typeName = rel?.meta?.type?.type;
|
||||
|
||||
return {
|
||||
name: key,
|
||||
typeName,
|
||||
type: 'relationship',
|
||||
[key]: createOperatorsObject({
|
||||
tableName: typeName || '',
|
||||
schema,
|
||||
existingPermission: value,
|
||||
tableConfig,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (colNames.includes(key)) {
|
||||
return {
|
||||
name: key,
|
||||
typeName: key,
|
||||
type: 'column',
|
||||
columnOperator: createOperatorsObject({
|
||||
tableName,
|
||||
schema,
|
||||
existingPermission: value,
|
||||
tableConfig,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return key;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return operators;
|
||||
};
|
||||
|
||||
export interface CreateDefaultsArgs {
|
||||
tableName: string;
|
||||
schema?: GraphQLSchema;
|
||||
existingPermission?: Record<string, any>;
|
||||
tableConfig: MetadataTable['configuration'];
|
||||
sourceMetadata: MetadataDataSource | undefined;
|
||||
tableColumns: TableColumn[];
|
||||
}
|
||||
|
||||
export const createDefaultValues = (props: CreateDefaultsArgs) => {
|
||||
const { tableName, schema, existingPermission, tableConfig } = props;
|
||||
const { tableName, existingPermission, tableColumns, sourceMetadata } = props;
|
||||
if (!existingPermission) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const operators = createOperatorsObject({
|
||||
tableName,
|
||||
schema,
|
||||
existingPermission,
|
||||
tableConfig,
|
||||
tableColumns,
|
||||
sourceMetadataTables: sourceMetadata?.tables,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { MetadataTable } from '@/features/hasura-metadata-types';
|
||||
|
||||
export const getMetadataTableCustomName = (
|
||||
metadataTable: MetadataTable | undefined
|
||||
) => {
|
||||
return metadataTable?.configuration?.custom_name;
|
||||
};
|
@ -1,6 +1,5 @@
|
||||
import {
|
||||
getAllColumnsAndOperators,
|
||||
findColumnOperator,
|
||||
getColumnOperators,
|
||||
} from './graphqlParsers';
|
||||
import { schema } from '../mocks';
|
||||
@ -93,21 +92,3 @@ test('correctly fetches operators for a given column', () => {
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('correctly fetches information about a column operator', () => {
|
||||
const columnOperators = getColumnOperators({
|
||||
tableName: 'user',
|
||||
schema,
|
||||
columnName: 'age',
|
||||
tableConfig: {},
|
||||
});
|
||||
const result = findColumnOperator({ columnKey: '_eq', columnOperators });
|
||||
const expected: ReturnType<typeof findColumnOperator> = {
|
||||
name: '_eq',
|
||||
type: {
|
||||
type: 'float8',
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Operator } from '@/features/DataSource';
|
||||
import { MetadataTable } from '@/features/hasura-metadata-types';
|
||||
import {
|
||||
GraphQLFieldMap,
|
||||
@ -52,6 +53,7 @@ export const getFields = (tableName: string, schema: GraphQLSchema) => {
|
||||
|
||||
if (isObjectType(type) || isInputObjectType(type)) {
|
||||
const fields = type.getFields();
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@ -140,24 +142,15 @@ export const getBoolOperators = () => {
|
||||
return ['_and', '_or', '_not'];
|
||||
};
|
||||
|
||||
const getExistOperators = () => {
|
||||
return ['_exists'];
|
||||
};
|
||||
|
||||
interface FindColumnArgs {
|
||||
columnKey: string;
|
||||
columnOperators: ReturnType<typeof getColumnOperators>;
|
||||
columnOperators: Operator[];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* finds the name and type of specified column
|
||||
*/
|
||||
export const findColumnOperator = ({
|
||||
columnKey,
|
||||
columnOperators,
|
||||
}: FindColumnArgs) => {
|
||||
const columnOperatorArray = columnKey.split('.');
|
||||
const value = columnOperatorArray[columnOperatorArray.length - 1];
|
||||
return columnOperators.find(({ name }) => name === value);
|
||||
};
|
||||
|
||||
interface Args {
|
||||
tableName: string;
|
||||
schema: GraphQLSchema;
|
||||
@ -188,6 +181,7 @@ export const getAllColumnsAndOperators = ({
|
||||
const metadataTableName = tableConfig?.custom_name ?? tableName;
|
||||
const fields = getFields(metadataTableName, schema);
|
||||
const boolOperators = getBoolOperators();
|
||||
const existOperators = getExistOperators();
|
||||
const columns = getColumns(fields);
|
||||
const relationships = getRelationships(fields);
|
||||
|
||||
@ -196,6 +190,11 @@ export const getAllColumnsAndOperators = ({
|
||||
kind: 'boolOperator',
|
||||
meta: null,
|
||||
}));
|
||||
const existMap = existOperators.map(existOperator => ({
|
||||
name: existOperator,
|
||||
kind: 'existOperator',
|
||||
meta: null,
|
||||
}));
|
||||
const colMap = columns.map(column => ({
|
||||
name: getOriginalTableNameFromCustomName(tableConfig, column.name),
|
||||
kind: 'column',
|
||||
@ -206,5 +205,10 @@ export const getAllColumnsAndOperators = ({
|
||||
kind: 'relationship',
|
||||
meta: relationship,
|
||||
}));
|
||||
return { boolOperators: boolMap, columns: colMap, relationships: relMap };
|
||||
return {
|
||||
boolOperators: boolMap,
|
||||
existOperators: existMap,
|
||||
columns: colMap,
|
||||
relationships: relMap,
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { GraphQLSchema } from 'graphql';
|
||||
|
||||
import isEqual from 'lodash.isequal';
|
||||
|
||||
import { TableColumn } from '@/features/DataSource';
|
||||
@ -10,26 +8,25 @@ import { Metadata } from '@/features/hasura-metadata-types';
|
||||
import { PermissionsSchema } from '../../../../../schema';
|
||||
|
||||
import type { QueryType } from '../../../../../types';
|
||||
import { SourceCustomization } from '../../../../../../hasura-metadata-types/source/source';
|
||||
import { Operator } from '../../../../../../DataSource/types';
|
||||
|
||||
import {
|
||||
MetadataDataSource,
|
||||
TableEntry,
|
||||
} from '../../../../../../../metadata/types';
|
||||
|
||||
import {
|
||||
createPermissionsObject,
|
||||
getRowPermissionsForAllOtherQueriesMatchingSelectedRole,
|
||||
} from './utils';
|
||||
|
||||
interface GetMetadataTableArgs {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
metadata: Metadata;
|
||||
trackedTables: TableEntry[] | undefined;
|
||||
}
|
||||
|
||||
const getMetadataTable = ({
|
||||
dataSourceName,
|
||||
table,
|
||||
metadata,
|
||||
}: GetMetadataTableArgs) => {
|
||||
const trackedTables = metadata.metadata?.sources?.find(
|
||||
source => source.name === dataSourceName
|
||||
)?.tables;
|
||||
|
||||
const getMetadataTable = ({ table, trackedTables }: GetMetadataTableArgs) => {
|
||||
// find selected table
|
||||
const currentTable = trackedTables?.find(trackedTable =>
|
||||
isEqual(trackedTable.table, table)
|
||||
@ -38,45 +35,36 @@ const getMetadataTable = ({
|
||||
return currentTable;
|
||||
};
|
||||
|
||||
interface Args {
|
||||
export interface CreateDefaultValuesArgs {
|
||||
queryType: QueryType;
|
||||
roleName: string;
|
||||
table: unknown;
|
||||
dataSourceName: string;
|
||||
metadata: Metadata;
|
||||
tableColumns: TableColumn[];
|
||||
schema: GraphQLSchema;
|
||||
defaultQueryRoot: string | never[];
|
||||
metadataSource: MetadataDataSource | undefined;
|
||||
supportedOperators: Operator[];
|
||||
}
|
||||
|
||||
export const createDefaultValues = ({
|
||||
queryType,
|
||||
roleName,
|
||||
table,
|
||||
dataSourceName,
|
||||
metadata,
|
||||
tableColumns,
|
||||
schema,
|
||||
defaultQueryRoot,
|
||||
}: Args) => {
|
||||
metadataSource,
|
||||
supportedOperators,
|
||||
}: CreateDefaultValuesArgs) => {
|
||||
const selectedTable = getMetadataTable({
|
||||
dataSourceName,
|
||||
table,
|
||||
metadata,
|
||||
trackedTables: metadataSource?.tables,
|
||||
});
|
||||
|
||||
const metadataSource = metadata.metadata.sources.find(
|
||||
s => s.name === dataSourceName
|
||||
);
|
||||
|
||||
/**
|
||||
* This is GDC specific, we have to move this to DAL later
|
||||
*/
|
||||
|
||||
const tableName = getTypeName({
|
||||
defaultQueryRoot,
|
||||
operation: 'select',
|
||||
sourceCustomization: metadataSource?.customization,
|
||||
sourceCustomization: metadataSource?.customization as SourceCustomization,
|
||||
configuration: selectedTable?.configuration,
|
||||
});
|
||||
|
||||
@ -91,15 +79,17 @@ export const createDefaultValues = ({
|
||||
filterType: 'none',
|
||||
columns: {},
|
||||
allRowChecks,
|
||||
supportedOperators,
|
||||
};
|
||||
|
||||
if (selectedTable) {
|
||||
const permissionsObject = createPermissionsObject({
|
||||
queryType,
|
||||
selectedTable,
|
||||
roleName,
|
||||
tableColumns,
|
||||
schema,
|
||||
tableName,
|
||||
metadataSource,
|
||||
});
|
||||
|
||||
return { ...baseDefaultValues, ...permissionsObject };
|
||||
|
@ -8,6 +8,7 @@ import type {
|
||||
MetadataTable,
|
||||
Permission,
|
||||
SelectPermissionDefinition,
|
||||
Source,
|
||||
UpdatePermissionDefinition,
|
||||
} from '@/features/hasura-metadata-types';
|
||||
|
||||
@ -19,6 +20,10 @@ import {
|
||||
import { createDefaultValues } from '../../../../components/RowPermissionsBuilder';
|
||||
|
||||
import type { QueryType } from '../../../../../types';
|
||||
import {
|
||||
MetadataDataSource,
|
||||
TableEntry,
|
||||
} from '../../../../../../../metadata/types';
|
||||
|
||||
export const getCheckType = (
|
||||
check?: Record<string, unknown> | null
|
||||
@ -111,7 +116,7 @@ export interface UseDefaultValuesArgs {
|
||||
export const getRowPermissionsForAllOtherQueriesMatchingSelectedRole = (
|
||||
selectedQuery: QueryType,
|
||||
selectedRole: string,
|
||||
table?: MetadataTable
|
||||
table?: TableEntry
|
||||
) => {
|
||||
const res = Object.entries(table || {}).reduce<
|
||||
Array<{ queryType: QueryType; value: any }>
|
||||
@ -163,20 +168,20 @@ export const createPermission = {
|
||||
select: (
|
||||
permission: SelectPermissionDefinition,
|
||||
tableColumns: TableColumn[],
|
||||
schema: GraphQLSchema,
|
||||
tableName: string,
|
||||
tableConfig: MetadataTable['configuration']
|
||||
metadataSource: MetadataDataSource | undefined
|
||||
) => {
|
||||
const { filter, operators } = createDefaultValues({
|
||||
const { filter, operators: ops } = createDefaultValues({
|
||||
tableName,
|
||||
existingPermission: permission.filter,
|
||||
schema,
|
||||
tableConfig,
|
||||
tableColumns,
|
||||
sourceMetadata: metadataSource,
|
||||
});
|
||||
|
||||
const filterType = getCheckType(permission?.filter);
|
||||
|
||||
const columns = getColumns(permission?.columns || [], tableColumns);
|
||||
|
||||
const rowCount = getRowCount({
|
||||
currentQueryPermissions: permission,
|
||||
});
|
||||
@ -190,7 +195,7 @@ export const createPermission = {
|
||||
columns,
|
||||
rowCount,
|
||||
aggregationEnabled,
|
||||
operators,
|
||||
operators: ops,
|
||||
query_root_fields: permission.query_root_fields || null,
|
||||
subscription_root_fields: permission.subscription_root_fields || null,
|
||||
};
|
||||
@ -252,7 +257,7 @@ export const createPermission = {
|
||||
};
|
||||
|
||||
interface GetCurrentPermissionArgs {
|
||||
table?: MetadataTable;
|
||||
table?: TableEntry;
|
||||
roleName: string;
|
||||
queryType: QueryType;
|
||||
}
|
||||
@ -284,11 +289,11 @@ export const getCurrentPermission = ({
|
||||
|
||||
interface ObjArgs {
|
||||
queryType: QueryType;
|
||||
selectedTable: MetadataTable;
|
||||
selectedTable: TableEntry;
|
||||
tableColumns: TableColumn[];
|
||||
roleName: string;
|
||||
schema: any;
|
||||
tableName: string;
|
||||
metadataSource: MetadataDataSource | undefined;
|
||||
}
|
||||
|
||||
export const createPermissionsObject = ({
|
||||
@ -296,8 +301,8 @@ export const createPermissionsObject = ({
|
||||
selectedTable,
|
||||
tableColumns,
|
||||
roleName,
|
||||
schema,
|
||||
tableName,
|
||||
metadataSource,
|
||||
}: ObjArgs) => {
|
||||
const selectedPermission = getCurrentPermission({
|
||||
table: selectedTable,
|
||||
@ -315,9 +320,9 @@ export const createPermissionsObject = ({
|
||||
return createPermission.select(
|
||||
selectedPermission.permission as SelectPermissionDefinition,
|
||||
tableColumns,
|
||||
schema,
|
||||
tableName,
|
||||
selectedTable.configuration
|
||||
// selectedTable.configuration,
|
||||
metadataSource
|
||||
);
|
||||
case 'update':
|
||||
return createPermission.update(
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { TableColumn } from '@/features/DataSource';
|
||||
|
||||
import { Metadata, MetadataTable } from '@/features/hasura-metadata-types';
|
||||
import { Metadata } from '@/features/hasura-metadata-types';
|
||||
import { isPermission } from '../../../../../utils';
|
||||
import {
|
||||
MetadataDataSource,
|
||||
TableEntry,
|
||||
} from '../../../../../../../metadata/types';
|
||||
|
||||
type Operation = 'insert' | 'select' | 'update' | 'delete';
|
||||
|
||||
@ -23,15 +26,11 @@ export const getAllowedFilterKeys = (
|
||||
type GetMetadataTableArgs = {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
metadata: Metadata;
|
||||
trackedTables: TableEntry[] | undefined;
|
||||
};
|
||||
|
||||
const getMetadataTable = (args: GetMetadataTableArgs) => {
|
||||
const { dataSourceName, table, metadata } = args;
|
||||
|
||||
const trackedTables = metadata.metadata?.sources?.find(
|
||||
source => source.name === dataSourceName
|
||||
)?.tables;
|
||||
const { table, trackedTables } = args;
|
||||
|
||||
const selectedTable = trackedTables?.find(
|
||||
trackedTable => JSON.stringify(trackedTable.table) === JSON.stringify(table)
|
||||
@ -46,7 +45,7 @@ const getMetadataTable = (args: GetMetadataTableArgs) => {
|
||||
};
|
||||
};
|
||||
|
||||
const getRoles = (metadataTables?: MetadataTable[]) => {
|
||||
const getRoles = (metadataTables?: TableEntry[]) => {
|
||||
// go through all tracked tables
|
||||
const res = metadataTables?.reduce<Set<string>>((acc, each) => {
|
||||
// go through all permissions
|
||||
@ -66,20 +65,22 @@ const getRoles = (metadataTables?: MetadataTable[]) => {
|
||||
return Array.from(res || []);
|
||||
};
|
||||
|
||||
interface Args {
|
||||
export interface CreateFormDataArgs {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
metadata: Metadata;
|
||||
tableColumns: TableColumn[];
|
||||
metadataSource: MetadataDataSource | undefined;
|
||||
trackedTables: TableEntry[] | undefined;
|
||||
}
|
||||
|
||||
export const createFormData = (props: Args) => {
|
||||
const { dataSourceName, table, metadata, tableColumns } = props;
|
||||
export const createFormData = (props: CreateFormDataArgs) => {
|
||||
const { dataSourceName, table, tableColumns, trackedTables } = props;
|
||||
// find the specific metadata table
|
||||
const metadataTable = getMetadataTable({
|
||||
dataSourceName,
|
||||
table,
|
||||
metadata,
|
||||
trackedTables: trackedTables,
|
||||
});
|
||||
|
||||
const roles = getRoles(metadataTable.tables);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,48 +1,87 @@
|
||||
import { createFormData } from './createFormData';
|
||||
import { createDefaultValues } from './createDefaultValues';
|
||||
import { defaultValuesInput, formDataInput } from './mock';
|
||||
import { useFormDataCreateDefaultValuesMock, createFormDataMock } from './mock';
|
||||
|
||||
const formDataMockResult: ReturnType<typeof createFormData> = {
|
||||
columns: ['ArtistId', 'Name'],
|
||||
roles: ['user'],
|
||||
const formDataMockResult = {
|
||||
roles: ['asdf', 'new', 'sdfsf', 'testrole', 'user'],
|
||||
supportedQueries: ['select'],
|
||||
tableNames: [['Album'], ['Artist']],
|
||||
tableNames: [{ dataset: 'bigquery_sample', name: 'sample_table' }],
|
||||
columns: [
|
||||
'Series_reference',
|
||||
'Period',
|
||||
'Data_value',
|
||||
'Suppressed',
|
||||
'STATUS',
|
||||
'UNITS',
|
||||
'Magnitude',
|
||||
'Subject',
|
||||
'Group',
|
||||
'Series_title_1',
|
||||
'Series_title_2',
|
||||
'Series_title_3',
|
||||
'Series_title_4',
|
||||
'Series_title_5',
|
||||
],
|
||||
};
|
||||
|
||||
test('returns correctly formatted formData', () => {
|
||||
const result = createFormData(formDataInput);
|
||||
const result = createFormData(createFormDataMock);
|
||||
expect(result).toEqual(formDataMockResult);
|
||||
});
|
||||
|
||||
const defaultValuesMockResult: ReturnType<typeof createDefaultValues> = {
|
||||
aggregationEnabled: true,
|
||||
allRowChecks: [],
|
||||
columns: {
|
||||
ArtistId: false,
|
||||
Name: true,
|
||||
},
|
||||
filter: {
|
||||
ArtistId: {
|
||||
_gt: 5,
|
||||
},
|
||||
},
|
||||
queryType: 'select',
|
||||
filterType: 'custom',
|
||||
columns: {
|
||||
Series_reference: false,
|
||||
Period: false,
|
||||
Data_value: false,
|
||||
Suppressed: false,
|
||||
STATUS: false,
|
||||
UNITS: false,
|
||||
Magnitude: false,
|
||||
Subject: false,
|
||||
Group: false,
|
||||
Series_title_1: false,
|
||||
Series_title_2: false,
|
||||
Series_title_3: false,
|
||||
Series_title_4: false,
|
||||
Series_title_5: false,
|
||||
},
|
||||
allRowChecks: [],
|
||||
supportedOperators: [
|
||||
{ name: 'equals', value: '_eq' },
|
||||
{ name: 'not equals', value: '_neq' },
|
||||
{ name: 'in', value: '_in', defaultValue: '[]' },
|
||||
{ name: 'nin', value: '_nin', defaultValue: '[]' },
|
||||
{ name: '>', value: '_gt' },
|
||||
{ name: '<', value: '_lt' },
|
||||
{ name: '>=', value: '_gte' },
|
||||
{ name: '<=', value: '_lte' },
|
||||
{ name: 'like', value: '_like', defaultValue: '%%' },
|
||||
{ name: 'not like', value: '_nlike', defaultValue: '%%' },
|
||||
],
|
||||
filter: { _not: { Data_value: { _eq: 1337 } } },
|
||||
rowCount: '0',
|
||||
aggregationEnabled: false,
|
||||
operators: {
|
||||
filter: {
|
||||
columnOperator: '_gt',
|
||||
name: 'ArtistId',
|
||||
type: 'column',
|
||||
typeName: 'ArtistId',
|
||||
name: '_not',
|
||||
typeName: '_not',
|
||||
type: 'boolOperator',
|
||||
_not: {
|
||||
name: 'Data_value',
|
||||
typeName: 'Data_value',
|
||||
type: 'column',
|
||||
columnOperator: '_eq',
|
||||
},
|
||||
},
|
||||
},
|
||||
query_root_fields: null,
|
||||
subscription_root_fields: ['select', 'select_by_pk'],
|
||||
queryType: 'select',
|
||||
rowCount: '3',
|
||||
subscription_root_fields: null,
|
||||
};
|
||||
|
||||
test('use default values returns values correctly', () => {
|
||||
const result = createDefaultValues(defaultValuesInput);
|
||||
|
||||
const result = createDefaultValues(useFormDataCreateDefaultValuesMock);
|
||||
expect(result).toEqual(defaultValuesMockResult);
|
||||
});
|
||||
|
@ -1,26 +1,33 @@
|
||||
import { useQuery } from 'react-query';
|
||||
import { buildClientSchema } from 'graphql';
|
||||
|
||||
import {
|
||||
DataSource,
|
||||
exportMetadata,
|
||||
Operator,
|
||||
runIntrospectionQuery,
|
||||
TableColumn,
|
||||
} from '@/features/DataSource';
|
||||
import { useHttpClient } from '@/features/Network';
|
||||
|
||||
import { createDefaultValues } from './createDefaultValues';
|
||||
import { createFormData } from './createFormData';
|
||||
import { Table } from '@/dataSources';
|
||||
import { Source } from '@/features/hasura-metadata-types';
|
||||
import { MetadataDataSource } from '@/metadata/types';
|
||||
import { Feature } from '../../../../../DataSource/index';
|
||||
|
||||
export type Args = {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
roleName: string;
|
||||
queryType: 'select' | 'insert' | 'update' | 'delete';
|
||||
tableColumns: TableColumn[];
|
||||
metadataSource: MetadataDataSource | undefined;
|
||||
};
|
||||
|
||||
type ReturnValue = {
|
||||
formData: ReturnType<typeof createFormData>;
|
||||
defaultValues: ReturnType<typeof createDefaultValues>;
|
||||
formData: ReturnType<typeof createFormData> | undefined;
|
||||
defaultValues: ReturnType<typeof createDefaultValues> | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -33,6 +40,8 @@ export const useFormData = ({
|
||||
table,
|
||||
roleName,
|
||||
queryType,
|
||||
tableColumns = [],
|
||||
metadataSource,
|
||||
}: Args) => {
|
||||
const httpClient = useHttpClient();
|
||||
return useQuery<ReturnValue, Error>({
|
||||
@ -41,18 +50,13 @@ export const useFormData = ({
|
||||
'permissionFormData',
|
||||
JSON.stringify(table),
|
||||
roleName,
|
||||
tableColumns,
|
||||
],
|
||||
queryFn: async () => {
|
||||
const introspectionResult = await runIntrospectionQuery({ httpClient });
|
||||
const schema = buildClientSchema(introspectionResult.data);
|
||||
if (tableColumns.length === 0)
|
||||
return { formData: undefined, defaultValues: undefined };
|
||||
const metadata = await exportMetadata({ httpClient });
|
||||
|
||||
// get table columns for metadata table from db introspection
|
||||
const tableColumns = await DataSource(httpClient).getTableColumns({
|
||||
dataSourceName,
|
||||
table,
|
||||
});
|
||||
|
||||
const defaultQueryRoot = await DataSource(httpClient).getDefaultQueryRoot(
|
||||
{
|
||||
dataSourceName,
|
||||
@ -60,6 +64,12 @@ export const useFormData = ({
|
||||
}
|
||||
);
|
||||
|
||||
const supportedOperators = (await DataSource(
|
||||
httpClient
|
||||
).getSupportedOperators({
|
||||
dataSourceName,
|
||||
})) as Operator[];
|
||||
|
||||
const defaultValues = createDefaultValues({
|
||||
queryType,
|
||||
roleName,
|
||||
@ -67,8 +77,9 @@ export const useFormData = ({
|
||||
metadata,
|
||||
table,
|
||||
tableColumns,
|
||||
schema,
|
||||
defaultQueryRoot,
|
||||
metadataSource,
|
||||
supportedOperators: supportedOperators ?? [],
|
||||
});
|
||||
|
||||
const formData = createFormData({
|
||||
@ -76,10 +87,13 @@ export const useFormData = ({
|
||||
table,
|
||||
metadata,
|
||||
tableColumns,
|
||||
trackedTables: metadataSource?.tables,
|
||||
metadataSource,
|
||||
});
|
||||
|
||||
return { formData, defaultValues };
|
||||
},
|
||||
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
};
|
||||
|
@ -101,9 +101,8 @@ export const useSubmitForm = (args: UseSubmitFormArgs) => {
|
||||
accessType,
|
||||
resourceVersion: resource_version,
|
||||
formData,
|
||||
existingPermissions,
|
||||
existingPermissions: existingPermissions ?? [],
|
||||
});
|
||||
|
||||
await mutate.mutateAsync(
|
||||
{
|
||||
query: body,
|
||||
@ -115,6 +114,9 @@ export const useSubmitForm = (args: UseSubmitFormArgs) => {
|
||||
title: 'Success!',
|
||||
message: 'Permissions saved successfully!',
|
||||
});
|
||||
exportMetadata({
|
||||
httpClient,
|
||||
});
|
||||
},
|
||||
onError: err => {
|
||||
fireNotification({
|
||||
|
@ -40,6 +40,7 @@ export const schema = z.discriminatedUnion('queryType', [
|
||||
columns,
|
||||
presets,
|
||||
backendOnly: z.boolean().optional(),
|
||||
supportedOperators: z.array(z.any()),
|
||||
clonePermissions: z.array(permission).optional(),
|
||||
}),
|
||||
z.object({
|
||||
@ -53,6 +54,7 @@ export const schema = z.discriminatedUnion('queryType', [
|
||||
clonePermissions: z.array(permission).optional(),
|
||||
query_root_fields: z.array(z.string()).nullable().optional(),
|
||||
subscription_root_fields: z.array(z.string()).nullable().optional(),
|
||||
supportedOperators: z.array(z.any()),
|
||||
}),
|
||||
z.object({
|
||||
queryType: z.literal('update'),
|
||||
@ -63,6 +65,7 @@ export const schema = z.discriminatedUnion('queryType', [
|
||||
check: z.any(),
|
||||
presets,
|
||||
backendOnly: z.boolean().optional(),
|
||||
supportedOperators: z.array(z.any()),
|
||||
clonePermissions: z.array(permission).optional(),
|
||||
}),
|
||||
z.object({
|
||||
@ -70,6 +73,7 @@ export const schema = z.discriminatedUnion('queryType', [
|
||||
filterType: z.string(),
|
||||
filter: z.any(),
|
||||
backendOnly: z.boolean().optional(),
|
||||
supportedOperators: z.array(z.any()),
|
||||
clonePermissions: z.array(permission).optional(),
|
||||
}),
|
||||
]);
|
||||
|
@ -142,7 +142,6 @@ export const metadataHandlers: Partial<
|
||||
},
|
||||
};
|
||||
}
|
||||
console.log({ query_name, queries: existingCollection.definition.queries });
|
||||
const existingQuery = existingCollection.definition.queries.find(
|
||||
q => q.name === query_name
|
||||
);
|
||||
|
@ -12,7 +12,7 @@ export const DEFAULT_STALE_TIME = 5 * 60000; // 5 minutes as default stale time
|
||||
Default stale time is 5 minutes, but can be adjusted using the staleTime arg
|
||||
*/
|
||||
|
||||
const METADATA_QUERY_KEY = 'export_metadata';
|
||||
export const METADATA_QUERY_KEY = 'export_metadata';
|
||||
|
||||
export const useInvalidateMetadata = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
22
frontend/package-lock.json
generated
22
frontend/package-lock.json
generated
@ -84,6 +84,7 @@
|
||||
"lodash.isobject": "^3.0.2",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.pickby": "^4.6.0",
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.uniqueid": "^4.0.1",
|
||||
"lru-memoize": "1.0.0",
|
||||
"luxon": "^1.17.2",
|
||||
@ -225,6 +226,7 @@
|
||||
"@types/lodash.isobject": "^3.0.7",
|
||||
"@types/lodash.merge": "^4.6.6",
|
||||
"@types/lodash.pickby": "^4.6.7",
|
||||
"@types/lodash.set": "^4.3.7",
|
||||
"@types/lodash.uniqueid": "^4.0.7",
|
||||
"@types/mini-css-extract-plugin": "0.9.1",
|
||||
"@types/node": "16.11.7",
|
||||
@ -278,7 +280,7 @@
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"css-loader": "3.5.3",
|
||||
"cypress": "^10.2.0",
|
||||
"cypress-plugin-snapshots": "^1.4.4",
|
||||
"cypress-plugin-snapshots": "1.4.4",
|
||||
"cypress-wait-until": "^1.7.2",
|
||||
"dedent": "0.7.0",
|
||||
"dotenv": "5.0.1",
|
||||
@ -27469,6 +27471,15 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash.set": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.7.tgz",
|
||||
"integrity": "sha512-bS5Wkg/nrT82YUfkNYPSccFrNZRL+irl7Yt4iM6OTSQ0VZJED2oUIVm15NkNtUAQ8SRhCe+axqERUV6MJgkeEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash.uniqueid": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.uniqueid/-/lodash.uniqueid-4.0.7.tgz",
|
||||
@ -84627,6 +84638,15 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash.set": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.7.tgz",
|
||||
"integrity": "sha512-bS5Wkg/nrT82YUfkNYPSccFrNZRL+irl7Yt4iM6OTSQ0VZJED2oUIVm15NkNtUAQ8SRhCe+axqERUV6MJgkeEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash.uniqueid": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.uniqueid/-/lodash.uniqueid-4.0.7.tgz",
|
||||
|
@ -119,6 +119,7 @@
|
||||
"lodash.isobject": "^3.0.2",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.pickby": "^4.6.0",
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.uniqueid": "^4.0.1",
|
||||
"lru-memoize": "1.0.0",
|
||||
"luxon": "^1.17.2",
|
||||
@ -260,6 +261,7 @@
|
||||
"@types/lodash.isobject": "^3.0.7",
|
||||
"@types/lodash.merge": "^4.6.6",
|
||||
"@types/lodash.pickby": "^4.6.7",
|
||||
"@types/lodash.set": "^4.3.7",
|
||||
"@types/lodash.uniqueid": "^4.0.7",
|
||||
"@types/mini-css-extract-plugin": "0.9.1",
|
||||
"@types/node": "16.11.7",
|
||||
|
Loading…
Reference in New Issue
Block a user