Matt/console/permissions form

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2893
Co-authored-by: Ikechukwu Eze <22247592+iykekings@users.noreply.github.com>
GitOrigin-RevId: e6c8dedf8369df887fdc376c606b9b2e9b3d5ddd
This commit is contained in:
Matt Hardman 2022-02-14 09:35:44 -06:00 committed by hasura-bot
parent 476eb5fe2b
commit d2724878d3
38 changed files with 9459 additions and 3379 deletions

View File

@ -187,7 +187,8 @@
"**/*.spec.tsx",
"**/*.stories.tsx",
"**/*.stories.mdx",
"**/*.mock.tsx"
"**/*.mock.tsx",
"**/*.mock.ts"
]
}
],

10945
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -78,6 +78,7 @@
"graphql-voyager": "1.0.0-rc.29",
"highlight.js": "9.15.8",
"history": "3.3.0",
"immer": "9.0.12",
"inflection": "1.12.0",
"isomorphic-fetch": "2.2.1",
"jsonwebtoken": "8.5.1",
@ -241,7 +242,7 @@
"lint-staged": "10.2.2",
"mini-css-extract-plugin": "0.4.5",
"msw": "0.29.0",
"msw-storybook-addon": "1.3.0",
"msw-storybook-addon": "1.6.0",
"node-sass": "4.14.1",
"nyc": "15.0.1",
"optimize-css-assets-webpack-plugin": "5.0.3",
@ -275,5 +276,5 @@
},
"msw": {
"workerDirectory": "static"
}
}
}

View File

@ -108,11 +108,11 @@ export const getTableColumn = (table: Table, columnName: string) => {
};
export const getTableRelationshipNames = (table: Table) => {
return table.relationships.map(r => r.rel_name);
return table?.relationships?.map(r => r.rel_name);
};
export function getTableRelationship(table: Table, relationshipName: string) {
return table.relationships.find(
return table?.relationships?.find(
relationship => relationship.rel_name === relationshipName
);
}

View File

@ -227,7 +227,7 @@ export const getGroupedTableComputedFields = (
table: ComputedField[];
} = { scalar: [], table: [] };
computed_fields.forEach(computedField => {
computed_fields?.forEach(computedField => {
const computedFieldFnDef = computedField.definition.function;
const computedFieldFn = findFunction(
allFunctions,

View File

@ -126,7 +126,7 @@ class PermissionBuilder extends React.Component<PermissionBuilderProps> {
const allSchemaNames = allTableSchemas.map(t => t.table_schema);
if (!allSchemaNames.includes(existTableSchema)) {
if (!allSchemaNames?.includes(existTableSchema)) {
missingSchemas.push(existTableSchema);
}
}
@ -148,7 +148,7 @@ class PermissionBuilder extends React.Component<PermissionBuilderProps> {
tableRelationshipNames = getTableRelationshipNames(tableSchema);
}
if (tableRelationshipNames.includes(operator)) {
if (tableRelationshipNames?.includes(operator)) {
const relationship = getTableRelationship(tableSchema!, operator);
const refTable = getRelationshipRefTable(tableSchema!, relationship!);
@ -884,7 +884,7 @@ class PermissionBuilder extends React.Component<PermissionBuilderProps> {
}
let columnExp = null;
if (tableRelationshipNames.includes(columnName)) {
if (tableRelationshipNames?.includes(columnName)) {
const relationship = getTableRelationship(tableSchema!, columnName);
const refTable = getRelationshipRefTable(tableSchema!, relationship!);

View File

@ -0,0 +1,114 @@
import React from 'react';
import { Story, Meta } from '@storybook/react';
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { PermissionsForm, PermissionsFormProps } from './PermissionsForm';
import { handlers } from './mocks/handlers.mock';
export default {
title: 'Permissions Form/Form',
component: PermissionsForm,
decorators: [ReactQueryDecorator()],
parameters: {
msw: handlers,
},
} as Meta;
const schemaName = 'public';
const tableName = 'users';
const roleName = 'user';
export const Showcase: Story<PermissionsFormProps> = () => {
return (
<>
<p className="font-bold py-4">Query Type: Insert</p>
<PermissionsForm
schemaName={schemaName}
tableName={tableName}
roleName={roleName}
accessType="partialAccess"
queryType="insert"
handleClose={() => {}}
/>
<p className="font-bold py-4">Query Type: Select</p>
<PermissionsForm
schemaName={schemaName}
tableName={tableName}
roleName={roleName}
accessType="partialAccess"
queryType="select"
handleClose={() => {}}
/>
<p className="font-bold py-4">Query Type: Update</p>
<PermissionsForm
schemaName={schemaName}
tableName={tableName}
roleName={roleName}
accessType="noAccess"
queryType="update"
handleClose={() => {}}
/>
<p className="font-bold py-4">Query Type: Delete</p>
<PermissionsForm
schemaName={schemaName}
tableName={tableName}
roleName={roleName}
accessType="noAccess"
queryType="delete"
handleClose={() => {}}
/>
</>
);
};
export const Insert: Story<PermissionsFormProps> = args => (
<PermissionsForm {...args} />
);
Insert.args = {
schemaName,
tableName,
roleName,
accessType: 'partialAccess',
queryType: 'insert',
handleClose: () => {},
};
Insert.parameters = {
// Disable storybook for Insert stories
chromatic: { disableSnapshot: true },
};
export const Select: Story<PermissionsFormProps> = args => (
<PermissionsForm {...args} />
);
Select.args = {
...Insert.args,
queryType: 'select',
};
Select.parameters = Insert.parameters;
export const Update: Story<PermissionsFormProps> = args => (
<PermissionsForm {...args} />
);
Update.args = {
...Insert.args,
queryType: 'update',
accessType: 'noAccess',
};
Update.parameters = Insert.parameters;
export const Delete: Story<PermissionsFormProps> = args => (
<PermissionsForm {...args} />
);
Delete.args = {
...Insert.args,
queryType: 'delete',
accessType: 'noAccess',
};
Delete.parameters = Insert.parameters;

View File

@ -0,0 +1,246 @@
import React from 'react';
import { useFormContext } from 'react-hook-form';
import { Form } from '@/new-components/Form';
import { Button } from '@/new-components/Button';
import {
RowPermissionsSectionWrapper,
RowPermissionsSection,
ColumnPermissionsSection,
ColumnPresetsSection,
AggregationSection,
BackendOnlySection,
ClonePermissionsSection,
} from './components';
import { useDefaultValues, useFormData, useUpdatePermissions } from './hooks';
import { schema } from './utils/formSchema';
import { AccessType, FormOutput, QueryType } from './types';
export interface PermissionsFormProps {
schemaName: string;
tableName: string;
queryType: QueryType;
roleName: string;
accessType: AccessType;
handleClose: () => void;
}
interface ResetterProps {
defaultValues: FormOutput;
}
// required to update the default values when the form switches between query types
// for example from update to select
const Resetter: React.FC<ResetterProps> = ({ defaultValues }) => {
const { reset } = useFormContext();
const initialRender = React.useRef(true);
React.useEffect(() => {
if (initialRender.current) {
initialRender.current = false;
} else {
reset(defaultValues);
}
}, [defaultValues, reset]);
return null;
};
export const PermissionsForm: React.FC<PermissionsFormProps> = ({
schemaName,
tableName,
queryType,
roleName,
accessType,
handleClose,
}) => {
const {
data,
isLoading: loadingFormData,
isError: formDataError,
} = useFormData({
schemaName,
tableName,
queryType,
roleName,
});
const {
data: defaults,
isLoading: defaultValuesLoading,
isError: defaultValuesError,
} = useDefaultValues({
schemaName,
tableName,
roleName,
queryType,
});
const { updatePermissions, deletePermissions } = useUpdatePermissions({
schemaName,
tableName,
queryType,
roleName,
accessType,
});
const handleSubmit = async (formData: FormOutput) => {
updatePermissions.submit(formData);
handleClose();
};
const handleDelete = async () => {
await deletePermissions.submit([queryType]);
handleClose();
};
const isError = formDataError || defaultValuesError;
// these will be replaced by components once spec is decided
if (isError) {
return <div>Error loading form data</div>;
}
const isSubmittingError =
updatePermissions.isError || deletePermissions.isError;
// these will be replaced by components once spec is decided
if (isSubmittingError) {
return <div>Error submitting form data</div>;
}
const isLoading = loadingFormData || defaultValuesLoading;
// these will be replaced by components once spec is decided
if (isLoading) {
return <div>Loading...</div>;
}
const {
// i.e. tables that are not the currently open table (this is for cloning permissions)
otherTableNames,
allFunctions,
roles,
tables,
supportedQueries,
columns,
} = data;
// allRowChecks relates to other queries and is for duplicating from others
// therefore it shouldn't be passed to the form as a default value
const { allRowChecks, ...defaultValues } = defaults;
// for update it is possible to set pre update and post update row checks
const rowPermissions = queryType === 'update' ? ['pre', 'post'] : [queryType];
return (
<Form onSubmit={handleSubmit} schema={schema} options={{ defaultValues }}>
{() => (
<div className="bg-white rounded p-md border border-gray-300">
<div className="pb-4 flex items-center gap-4">
<Button type="button" onClick={handleClose}>
Close
</Button>
<h3 data-testid="form-title">
<strong>Role:</strong> {roleName} <strong>Action:</strong>{' '}
{queryType}
</h3>
</div>
<Resetter defaultValues={defaultValues} />
<RowPermissionsSectionWrapper
roleName={roleName}
queryType={queryType}
defaultOpen
>
{rowPermissions.map(permissionName => (
<React.Fragment key={permissionName}>
{/* if queryType is update 2 row permissions sections are rendered (pre and post) */}
{/* therefore they need titles */}
{queryType === 'update' && (
<p className="my-2">
<strong>
{permissionName === 'pre' ? 'Pre-update' : 'Post-update'}
&nbsp; check
</strong>
&nbsp;
{permissionName === 'Post-update' && '(optional)'}
</p>
)}
<RowPermissionsSection
queryType={queryType}
subQueryType={
queryType === 'update' ? permissionName : undefined
}
schemaName={schemaName}
tableName={tableName}
allRowChecks={allRowChecks}
allSchemas={tables}
allFunctions={allFunctions}
/>
</React.Fragment>
))}
</RowPermissionsSectionWrapper>
{queryType !== 'delete' && (
<ColumnPermissionsSection
roleName={roleName}
queryType={queryType}
columns={columns!}
/>
)}
{['insert', 'update'].includes(queryType) && (
<ColumnPresetsSection queryType={queryType} columns={columns!} />
)}
{queryType === 'select' && (
<AggregationSection queryType={queryType} roleName={roleName} />
)}
{queryType === 'insert' && (
<BackendOnlySection queryType={queryType} />
)}
<hr className="my-4" />
{otherTableNames && roles && (
<ClonePermissionsSection
queryType={queryType}
tables={otherTableNames}
supportedQueryTypes={supportedQueries}
roles={roles}
/>
)}
<hr className="my-4" />
<div className="pt-2 flex gap-2">
<Button
type="submit"
mode="primary"
isLoading={updatePermissions.isLoading}
>
Save Permissions
</Button>
<Button
type="button"
disabled={accessType === 'noAccess'}
mode="destructive"
isLoading={deletePermissions.isLoading}
onClick={handleDelete}
>
Delete Permissions
</Button>
</div>
</div>
)}
</Form>
);
};
export default PermissionsForm;

View File

@ -0,0 +1,99 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`update metadata in cache 1`] = `
Object {
"metadata": Object {
"sources": Array [
Object {
"configuration": Object {
"connection_info": Object {
"database_url": Object {
"from_env": "HASURA_GRAPHQL_DATABASE_URL",
},
"isolation_level": "read-committed",
"pool_settings": Object {
"connection_lifetime": 600,
"idle_timeout": 180,
"max_connections": 50,
"retries": 1,
},
"use_prepared_statements": true,
},
},
"functions": Array [
Object {
"function": Object {
"name": "search_user2",
"schema": "public",
},
},
],
"kind": "postgres",
"name": "default",
"tables": Array [
Object {
"table": Object {
"name": "a_table",
"schema": "public",
},
},
Object {
"insert_permissions": Array [
Object {
"permission": Object {
"allow_aggregations": false,
"backend_only": false,
"check": Object {
"id": Object {
"_eq": 1,
},
},
"columns": Array [
"email",
"type",
],
"computed_fields": Array [],
"filter": Object {},
"limit": 0,
"presets": Object {},
},
"role": "user",
"source": "default",
"table": Object {
"name": "users",
"schema": "public",
},
},
],
"select_permissions": Array [
Object {
"permission": Object {
"allow_aggregations": true,
"columns": Array [
"email",
"id",
"type",
],
"filter": Object {
"id": Object {
"_eq": 1,
},
},
"limit": 5,
},
"role": "user",
},
],
"table": Object {
"name": "users",
"schema": "public",
},
},
],
},
],
"version": 3,
},
"resource_version": 30,
}
`;

View File

@ -0,0 +1,47 @@
import { updateTablePermission } from '../cache';
import { metadata } from '../../mocks/dataStubs';
const data = {
type: 'bulk',
source: 'default',
resource_version: 30,
args: [
{
type: 'pg_create_insert_permission',
args: {
table: {
name: 'users',
schema: 'public',
},
role: 'user',
permission: {
columns: ['email', 'type'],
presets: {},
computed_fields: [],
backend_only: false,
limit: 0,
allow_aggregations: false,
check: {
id: {
_eq: 1,
},
},
filter: {},
},
source: 'default',
},
},
],
};
test('update metadata in cache', () => {
const result = updateTablePermission({
key: 'insert_permissions',
tableName: 'users',
roleName: 'user',
metadata,
data,
});
expect(result).toMatchSnapshot();
});

View File

@ -0,0 +1,142 @@
import { Api } from '@/hooks/apiUtils';
import Endpoints from '@/Endpoints';
import { QualifiedTable } from '@/metadata/types';
import { AccessType, FormOutput, QueryType } from '../types';
interface CreateBodyArgs {
dataSource: string;
qualifiedTable: QualifiedTable;
roleName: string;
resourceVersion: number;
}
interface CreateDeleteBodyArgs extends CreateBodyArgs {
queries: QueryType[];
}
const createDeleteBody = ({
dataSource,
qualifiedTable,
roleName,
resourceVersion,
queries,
}: CreateDeleteBodyArgs) => {
const args = queries.map(queryType => ({
type: `${dataSource}_drop_${queryType}_permission`,
args: {
table: qualifiedTable,
role: roleName,
source: 'default',
},
}));
const body = {
type: 'bulk',
source: 'default',
resource_version: resourceVersion,
args,
};
return body;
};
interface CreateInsertBodyArgs extends CreateBodyArgs {
queryType: QueryType;
formData: FormOutput;
accessType: AccessType;
}
const createInsertBody = ({
dataSource,
qualifiedTable,
queryType,
roleName,
formData,
accessType,
resourceVersion,
}: CreateInsertBodyArgs) => {
const presets = formData.presets?.reduce((acc, preset) => {
if (preset.columnValue) {
acc[preset.columnName] = preset.columnValue;
}
return acc;
}, {} as Record<string, string | number>);
const columns = Object.entries(formData.columns)
.filter(({ 1: value }) => value)
.map(([key]) => key);
const permission = {
columns,
presets,
computed_fields: [],
backend_only: formData.backendOnly,
limit: parseInt(formData.rowCount, 10),
allow_aggregations: formData.aggregationEnabled,
check: JSON.parse(formData.check || '{}'),
filter: JSON.parse(formData.filter || '{}'),
};
const args = [
{
type: `${dataSource}_create_${queryType}_permission`,
args: {
table: qualifiedTable,
role: roleName,
permission,
source: 'default',
},
},
];
if (accessType !== 'noAccess') {
args.unshift({
type: `${dataSource}_drop_${queryType}_permission`,
args: {
table: qualifiedTable,
role: roleName,
source: 'default',
},
} as typeof args[0]);
}
const formBody = {
type: 'bulk',
source: 'default',
resource_version: resourceVersion,
args,
};
return formBody;
};
interface CreatePermissionsArgs {
headers: any;
body: ReturnType<typeof createInsertBody>;
}
const createPermissions = ({ headers, body }: CreatePermissionsArgs) =>
Api.post<string[]>(
{ headers, body, url: Endpoints.metadata },
result => result
);
interface DeletePermissionsArgs {
headers: any;
body: ReturnType<typeof createDeleteBody>;
}
const deletePermissions = ({ headers, body }: DeletePermissionsArgs) =>
Api.post<string[]>(
{ headers, body, url: Endpoints.metadata },
result => result
);
export const api = {
createPermissions,
deletePermissions,
createInsertBody,
createDeleteBody,
};

View File

@ -0,0 +1,118 @@
import { useQueryClient } from 'react-query';
import produce from 'immer';
import {
DeletePermissionEntry,
InsertPermissionEntry,
SelectPermissionEntry,
UpdatePermissionEntry,
} from '@/metadata/types';
import { MetadataResponse } from '../../MetadataAPI';
import { api } from './api';
import { AccessType, QueryType } from '../types';
type PermissionEntry =
| InsertPermissionEntry
| SelectPermissionEntry
| UpdatePermissionEntry
| DeletePermissionEntry;
type MetadataKeys =
| 'insert_permissions'
| 'select_permissions'
| 'update_permissions'
| 'delete_permissions';
interface UpdateTablePermissionArgs {
key: MetadataKeys;
tableName: string;
roleName: string;
metadata: MetadataResponse;
data: ReturnType<typeof api.createInsertBody>;
}
export const updateTablePermission = ({
key,
tableName,
roleName,
metadata,
data,
}: UpdateTablePermissionArgs) => {
// find the arg
const newMetadataItem = data.args.find(arg => arg.type.includes('create'));
// find and update the relevant piece of metadata that needs updating
const nextState = produce(metadata, draft => {
// find the table that is being edited
const selectedTable = draft.metadata.sources[0].tables.find(
({ table }) => table.name === tableName
);
// find the queryType that is being edited
const selectedPermission = selectedTable?.[key];
// find the index of the role that is being edited
const selectedRolePermissionIndex = selectedPermission?.findIndex(
(permission: PermissionEntry) => permission.role === roleName
);
// if the selected permission already exists replace it
if (
selectedRolePermissionIndex !== undefined &&
selectedPermission &&
newMetadataItem
) {
selectedPermission[selectedRolePermissionIndex] = newMetadataItem?.args;
} else if (newMetadataItem) {
selectedPermission?.push(newMetadataItem.args);
}
});
return nextState;
};
interface HandleUpdateArgs {
args: {
tableName: string;
schemaName: string;
roleName: string;
queryType: QueryType;
accessType: AccessType;
};
response: {
headers: any;
body: ReturnType<typeof api.createInsertBody>;
};
}
const useUpdateTablePermissionCache = () => {
const client = useQueryClient();
const handleUpdate = ({ args, response }: HandleUpdateArgs) => {
const metadata = client.getQueryData<MetadataResponse>(['metadata']);
const { tableName, roleName, queryType } = args;
if (metadata) {
// update cache
const result = updateTablePermission({
key: `${queryType}_permissions`,
tableName,
roleName,
metadata,
data: response.body,
});
client.setQueryData('metadata', result);
}
return { metadata };
};
return { handleUpdate };
};
export const cache = {
useUpdateTablePermissionCache,
};

View File

@ -0,0 +1,2 @@
export * from './api';
export * from './cache';

View File

@ -23,7 +23,7 @@ export const AggregationSection: React.FC<AggregationProps> = ({
// if no row permissions are selected selection should be disabled
const disabled = useIsDisabled(queryType);
const enabled = watch('enableAggregation');
const enabled = watch('aggregationEnabled');
if (!isFeatureSupported('tables.permissions.aggregation')) {
return null;
@ -46,7 +46,7 @@ export const AggregationSection: React.FC<AggregationProps> = ({
type="checkbox"
title={disabled ? 'Set row permissions first' : ''}
disabled={disabled}
{...register('enableAggregation')}
{...register('aggregationEnabled')}
/>
<p>
Allow role <strong>{roleName}</strong> to make aggregation queries

View File

@ -42,11 +42,11 @@ const useStatus = (disabled: boolean) => {
return { data: 'Disabled: Set row permissions first', isError: false };
}
if (selectedColumns.length === 0) {
if (selectedColumns?.length === 0) {
return { data: 'No columns', isError: false };
}
if (selectedColumns.length === columnValues.length) {
if (selectedColumns?.length === columnValues?.length) {
return { data: 'All columns', isError: false };
}
@ -104,7 +104,7 @@ export const ColumnPermissionsSection: React.FC<ColumnPermissionsSectionProps> =
</div>
<fieldset className="flex gap-4">
{columns.map(fieldName => (
{columns?.map(fieldName => (
<label key={fieldName} className="flex gap-2 items-center">
<input
type="checkbox"

View File

@ -83,7 +83,7 @@ const PresetsRow: React.FC<PresetsRowProps> = ({
className={className}
placeholder="Column value"
disabled={disabled}
{...register(`presets.${id}.columnValue`)}
{...register(`presets.${id}.value`)}
/>
</div>
@ -131,7 +131,7 @@ const useStatus = (disabled: boolean) => {
.map(({ columnName }) => columnName)
.filter(columnName => columnName !== 'default');
if (!columnNames.length) {
if (!columnNames?.length) {
return 'No Presets';
}
@ -164,8 +164,8 @@ export const ColumnPresetsSection: React.FC<ColumnPresetsSectionProps> = ({
React.useEffect(() => {
const finalRowIsNotDefault =
controlledFields[controlledFields.length - 1]?.columnName !== 'default';
const allColumnsSet = controlledFields.length === columns.length;
controlledFields[controlledFields?.length - 1]?.columnName !== 'default';
const allColumnsSet = controlledFields?.length === columns?.length;
if (finalRowIsNotDefault && !allColumnsSet) {
append({
@ -174,7 +174,7 @@ export const ColumnPresetsSection: React.FC<ColumnPresetsSectionProps> = ({
columnValue: '',
});
}
}, [controlledFields, columns.length, append]);
}, [controlledFields, columns?.length, append]);
return (
<Collapse defaultOpen={defaultOpen}>

View File

@ -10,7 +10,7 @@ import {
RowPermissionsWrapperProps,
} from './RowPermissions';
import { currentSchema, allSchemas, allFunctions } from '../mocks/mockData';
import { allSchemas, allFunctions } from '../mocks/mockData';
import { QueryType } from '../types';
export default {
@ -29,23 +29,16 @@ export default {
const roleName = 'two';
// this will be moved into a utils folder
const allRowChecks = ({ role, query }: { role: string; query: string }) => {
const currentRole = currentSchema.permissions.find(
({ role_name }) => role === role_name
);
if (currentRole) {
const { permissions } = currentRole;
return Object.entries(permissions)
.filter(([name, info]) => name !== query && info.filter)
.map(([name, info]) => ({
queryType: name as QueryType,
filter: JSON.stringify(info.filter),
}));
}
return [];
};
const allRowChecks = [
{
queryType: 'insert' as QueryType,
value: '{"id":{"_eq":1}}',
},
{
queryType: 'select' as QueryType,
value: '{"id":{"_eq":1}}',
},
];
interface Props {
wrapper: RowPermissionsWrapperProps;
@ -63,7 +56,7 @@ Insert.args = {
schemaName: 'public',
tableName: 'users',
queryType: 'delete',
allRowChecks: allRowChecks({ role: roleName, query: 'insert' }),
allRowChecks,
allSchemas,
allFunctions,
},
@ -77,9 +70,9 @@ export const Select: Story<Props> = args => (
Select.args = {
wrapper: { roleName, queryType: 'select', defaultOpen: true },
section: {
...Insert.args.section!,
...Insert!.args!.section!,
queryType: 'select',
allRowChecks: allRowChecks({ role: roleName, query: 'select' }),
allRowChecks,
},
};
@ -93,7 +86,7 @@ Update.args = {
section: {
...Insert.args.section!,
queryType: 'update',
allRowChecks: allRowChecks({ role: roleName, query: 'update' }),
allRowChecks,
},
};
@ -107,7 +100,7 @@ Delete.args = {
section: {
...Insert.args.section!,
queryType: 'delete',
allRowChecks: allRowChecks({ role: roleName, query: 'delete' }),
allRowChecks,
},
};

View File

@ -4,7 +4,7 @@ import { useFormContext, Controller } from 'react-hook-form';
import 'brace/mode/json';
import 'brace/theme/github';
import { Table } from '@/dataSources/types';
import { NormalizedTable, Table } from '@/dataSources/types';
import { PGFunction } from '@/dataSources/services/postgresql/types';
import { generateTableDef } from '@/dataSources';
import { InputField } from '@/new-components/Form';
@ -40,9 +40,9 @@ export interface RowPermissionsProps {
subQueryType?: string;
schemaName: string;
tableName: string;
allRowChecks: Array<{ queryType: QueryType; filter: string }>;
allSchemas: Table[];
allFunctions: PGFunction[];
allRowChecks: Array<{ queryType: QueryType; value: string }>;
allSchemas?: NormalizedTable[];
allFunctions?: PGFunction[];
}
enum SelectedSection {
@ -51,7 +51,7 @@ enum SelectedSection {
NoneSelected = 'none',
}
const getRowPermission = (queryType: string, subQueryType?: string) => {
const getRowPermission = (queryType: QueryType, subQueryType?: string) => {
if (queryType === 'insert') {
return 'check';
}
@ -68,7 +68,7 @@ const getRowPermission = (queryType: string, subQueryType?: string) => {
};
const getRowPermissionCheckType = (
queryType: string,
queryType: QueryType,
subQueryType?: string
) => {
if (queryType === 'insert') {
@ -96,7 +96,6 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
allFunctions,
}) => {
const { control, register, watch, setValue } = useFormContext();
// determines whether the inputs should be pointed at `check` or `filter`
const rowPermissions = getRowPermission(queryType, subQueryType);
// determines whether the check type should be pointer at `checkType` or `filterType`
@ -110,7 +109,7 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
queryType === 'update' && subQueryType === 'post' && !watch('check');
const schemaList = React.useMemo(
() => allSchemas.map(({ table_schema }) => table_schema),
() => allSchemas?.map(({ table_schema }) => table_schema),
[allSchemas]
);
@ -147,7 +146,7 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
)}
</div>
{allRowChecks.map(({ queryType: query, filter }) => (
{allRowChecks?.map(({ queryType: query, value }) => (
<div key={query}>
<label className="flex items-center gap-2">
<input
@ -157,7 +156,7 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
disabled={disabled}
onClick={() => {
setValue(rowPermissionsCheckType, query);
setValue(rowPermissions, filter);
setValue(rowPermissions, value);
}}
{...register(rowPermissionsCheckType)}
/>
@ -169,7 +168,7 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
{selectedSection === query && (
<div className="pt-4">
<JSONEditor
data={filter}
data={value}
onChange={output => {
setValue(rowPermissionsCheckType, SelectedSection.Custom);
setValue(rowPermissions, output);
@ -208,18 +207,20 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
initData="{}"
/>
<PermissionBuilder
dispatchFuncSetFilter={output => {
onChange(output);
}}
loadSchemasFunc={() => {}}
tableDef={generateTableDef(tableName, schemaName)}
allTableSchemas={allSchemas}
allFunctions={allFunctions}
schemaList={schemaList}
filter={value || '{}'}
dispatch={() => console.log('output')}
/>
{allSchemas && allFunctions && schemaList && (
<PermissionBuilder
dispatchFuncSetFilter={output => {
onChange(output);
}}
loadSchemasFunc={() => {}}
tableDef={generateTableDef(tableName, schemaName)}
allTableSchemas={allSchemas as Table[]}
allFunctions={allFunctions}
schemaList={schemaList}
filter={value || '{}'}
dispatch={() => console.log('output')}
/>
)}
</>
)}
/>

View File

@ -0,0 +1,2 @@
export * from './useDefaultValues';
export * from './useFormData';

View File

@ -0,0 +1,70 @@
import React from 'react';
import ReactJson from 'react-json-view';
import { Story, Meta } from '@storybook/react';
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { useDefaultValues, UseDefaultValuesArgs } from './useDefaultValues';
import { handlers } from '../../mocks/handlers.mock';
const UseDefaultValuesComponent = ({
schemaName,
tableName,
roleName,
queryType,
}: UseDefaultValuesArgs) => {
const results = useDefaultValues({
schemaName,
tableName,
roleName,
queryType,
});
return <ReactJson src={results} />;
};
export default {
title: 'Permissions Form/hooks/useDefaultValues',
component: UseDefaultValuesComponent,
decorators: [ReactQueryDecorator()],
parameters: {
msw: handlers,
chromatic: { disableSnapshot: true },
},
} as Meta;
const schemaName = 'public';
const tableName = 'users';
const roleName = 'user';
export const Insert: Story<UseDefaultValuesArgs> = args => (
<UseDefaultValuesComponent {...args} />
);
Insert.args = {
schemaName,
tableName,
roleName,
queryType: 'insert',
};
export const Select: Story<UseDefaultValuesArgs> = args => (
<UseDefaultValuesComponent {...args} />
);
Select.args = {
...Insert.args,
queryType: 'select',
};
export const Update: Story<UseDefaultValuesArgs> = args => (
<UseDefaultValuesComponent {...args} />
);
Update.args = {
...Insert.args,
queryType: 'update',
};
export const Delete: Story<UseDefaultValuesArgs> = args => (
<UseDefaultValuesComponent {...args} />
);
Delete.args = {
...Insert.args,
queryType: 'delete',
};

View File

@ -0,0 +1,203 @@
import { NormalizedTable } from '@/dataSources/types';
import { useMetadataTablePermissions } from '@/features/MetadataAPI';
import { useSingleTable } from '@/hooks';
import { useAppSelector } from '@/store';
import { currentDriver } from '@/dataSources';
import { getCurrentRole } from '../../utils';
import { QueryType } from '../../types';
namespace Format {
export const getCheckType = (check?: string | null) => {
if (!check) {
return 'none';
}
if (check === '{}') {
return 'no_checks';
}
return 'custom';
};
interface GetRowCountArgs {
currentQueryPermissions?: Record<string, any>;
}
export const getRowCount = ({ currentQueryPermissions }: GetRowCountArgs) => {
return `${currentQueryPermissions?.limit ?? 0}`;
};
interface GetCheckArgs {
currentQueryPermissions?: Record<string, any>;
type: 'check' | 'filter';
}
export const getCheck = ({ currentQueryPermissions, type }: GetCheckArgs) => {
const check = currentQueryPermissions?.[type];
return check ? JSON.stringify(check) : '';
};
interface FormatColumnsArgs {
table?: NormalizedTable | null;
currentQueryPermissions?: Record<string, any>;
}
export const formatColumns = ({
table,
currentQueryPermissions,
}: FormatColumnsArgs) => {
const allColumns = table?.columns?.map(({ column_name }) => column_name);
const selectedColumns = currentQueryPermissions?.columns;
if (!allColumns || !selectedColumns) {
return {};
}
return allColumns?.reduce((acc, column) => {
const selected = selectedColumns?.includes(column);
acc[column] = selected;
return acc;
}, {} as Record<typeof selectedColumns, boolean>);
};
interface GetPresetArgs {
currentQueryPermissions?: Record<string, any>;
}
export const getPresets = ({ currentQueryPermissions }: GetPresetArgs) => {
const set = Object.entries(currentQueryPermissions?.set || {}) as Array<
[string, string]
>;
return set.map(([columnName, value]) => {
return {
columnName,
presetType: value.startsWith('x-hasura')
? 'from session variable'
: 'static',
value,
};
});
};
export const getAllRowChecks = (
currentQuery: QueryType,
permissions?: Record<string, any>
) => {
const allChecks = Object.entries(permissions || {}) as [QueryType, any];
return allChecks
.filter(([queryType]) => queryType !== currentQuery)
.map(([queryType, permission]) => {
if (['insert', 'update'].includes(queryType)) {
return { queryType, value: JSON.stringify(permission.check || {}) };
}
return {
queryType,
value: JSON.stringify(permission.filter || {}),
};
});
};
}
export interface UseDefaultValuesArgs {
schemaName: string;
tableName: string;
roleName: string;
queryType: QueryType;
}
const useLoadPermissions = ({
schemaName,
tableName,
roleName,
queryType,
}: UseDefaultValuesArgs) => {
const dataSource: string =
useAppSelector(state => state.tables.currentDataSource) || 'default';
const {
data: table,
isLoading: tableLoading,
isError: tableError,
} = useSingleTable({
source: dataSource,
driver: currentDriver,
table: { name: tableName, schema: schemaName },
});
const {
data: permissions,
isLoading: permissionsLoading,
isError: permissionsError,
} = useMetadataTablePermissions(
{
schema: schemaName,
name: tableName,
},
dataSource
);
const currentRolePermissions = getCurrentRole({ permissions, roleName });
const currentQueryPermissions =
currentRolePermissions?.permissions?.[queryType];
const allRowChecks = Format.getAllRowChecks(
queryType,
currentRolePermissions?.permissions
);
const isLoading = tableLoading || permissionsLoading;
const isError = tableError || permissionsError;
return {
data: {
currentQueryPermissions,
allRowChecks,
table,
},
isLoading,
isError,
};
};
export const useDefaultValues = (args: UseDefaultValuesArgs) => {
const {
data: { currentQueryPermissions, table, allRowChecks },
isLoading,
isError,
} = useLoadPermissions(args);
const check = Format.getCheck({ currentQueryPermissions, type: 'check' });
const filter = Format.getCheck({
currentQueryPermissions,
type: 'filter',
});
const checkType = Format.getCheckType(check);
const filterType = Format.getCheckType(filter);
const rowCount = Format.getRowCount({ currentQueryPermissions });
const columns = Format.formatColumns({ table, currentQueryPermissions });
const presets = Format.getPresets({ currentQueryPermissions });
return {
data: {
checkType,
filterType,
check,
filter,
rowCount,
columns,
presets,
backendOnly: currentQueryPermissions?.backend_only || false,
aggregationEnabled: currentQueryPermissions?.allow_aggregations || false,
clonePermissions: [],
allRowChecks,
},
isLoading,
isError,
};
};

View File

@ -0,0 +1,40 @@
import React from 'react';
import ReactJson from 'react-json-view';
import { Story, Meta } from '@storybook/react';
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { useFormData, UseFormDataArgs } from './useFormData';
import { handlers } from '../../mocks/handlers.mock';
const UseFormDataComponent = ({
schemaName,
tableName,
roleName,
queryType,
}: UseFormDataArgs) => {
const results = useFormData({ schemaName, tableName, roleName, queryType });
return <ReactJson src={results} />;
};
export default {
title: 'Permissions Form/hooks/useFormData',
component: UseFormDataComponent,
decorators: [ReactQueryDecorator()],
parameters: {
msw: handlers,
},
} as Meta;
const schemaName = 'public';
const tableName = 'users';
const roleName = 'two';
export const Primary: Story<UseFormDataArgs> = args => (
<UseFormDataComponent {...args} />
);
Primary.args = {
schemaName,
tableName,
roleName,
queryType: 'insert',
};

View File

@ -0,0 +1,121 @@
import React from 'react';
import {
useTrackableFunctions,
useDataSourceTables,
useSingleTable,
} from '@/hooks';
import { useAppSelector } from '@/store';
import { currentDriver, dataSource } from '@/dataSources';
import { useRoles } from '../../../MetadataAPI';
import { QueryType } from '../../types';
export interface UseFormDataArgs {
schemaName: string;
tableName: string;
roleName: string;
queryType: QueryType;
}
const useLoadSchemas = ({ schemaName, tableName }: UseFormDataArgs) => {
const source: string = useAppSelector(
state => state.tables.currentDataSource
);
const {
data: tables,
isLoading: tablesLoading,
isError: tablesError,
} = useDataSourceTables(
{ schemas: [schemaName], source, driver: currentDriver },
{
enabled: !!schemaName,
retry: 0,
}
);
const {
data: table,
isLoading: tableLoading,
isError: tableError,
} = useSingleTable({
source,
driver: currentDriver,
table: { name: tableName, schema: schemaName },
});
const { data: roles } = useRoles();
const { data: allFunctions } = useTrackableFunctions(
{ schemas: [schemaName], source, driver: currentDriver },
{
enabled: !!schemaName,
retry: 0,
}
);
const isError = tablesError || tableError;
const isLoading = tablesLoading || tableLoading;
if (isError) {
return {
data: {},
isLoading: false,
isError: true,
};
}
if (isLoading) {
return {
data: {},
isLoading: true,
isError: false,
};
}
return {
data: { tables, table, roles, allFunctions },
isLoading: false,
isError: false,
};
};
export const useFormData = (props: UseFormDataArgs) => {
const {
data: { table, tables, roles, allFunctions },
isLoading,
isError,
} = useLoadSchemas(props);
const otherTableNames = React.useMemo(
() =>
tables
?.filter(({ table_name }) => table_name !== table?.table_name)
.map(({ table_name }) => table_name),
[tables, table]
);
const columns = React.useMemo(
() => table?.columns.map(({ column_name }) => column_name),
[table]
);
let supportedQueries: string[] = [];
if (table) {
// supportedQueries = ['insert', 'select', 'update', 'delete'];
supportedQueries = dataSource.getTableSupportedQueries(table);
}
return {
data: {
table,
tables,
otherTableNames,
columns,
allFunctions,
roles,
supportedQueries,
},
isLoading,
isError,
};
};

View File

@ -0,0 +1,3 @@
export * from './dataFetchingHooks';
export * from './submitHooks';
export * from './useIsDisabled';

View File

@ -0,0 +1 @@
export * from './useUpdatePermissions';

View File

@ -0,0 +1,55 @@
import { useMutation, useQueryClient } from 'react-query';
import { useMetadataVersion } from '@/features/MetadataAPI';
import { useAppSelector } from '@/store';
import { QueryType } from '../../types';
import { api } from '../../api';
export interface UseDeletePermissionArgs {
tableName: string;
schemaName: string;
roleName: string;
}
export const useDeletePermission = ({
tableName,
schemaName,
roleName,
}: UseDeletePermissionArgs) => {
const client = useQueryClient();
const headers = useAppSelector(state => state.tables.dataHeaders);
const { data: resourceVersion } = useMetadataVersion();
const { mutateAsync, ...rest } = useMutation(api.deletePermissions, {
onError: (_err, _, context: any) => {
// if there is an error set the metadata query to the original value
client.setQueryData('metadata', context.metadata);
},
onSettled: () => {
// once the mutation is complete invalidate the query
// ensure the client state is upto date with the server state
client.invalidateQueries('metadata');
},
});
const submit = async (queries: QueryType[]) => {
if (!resourceVersion) {
throw new Error('No resource version provided');
}
const body = api.createDeleteBody({
dataSource: 'pg',
qualifiedTable: { name: tableName, schema: schemaName },
roleName,
resourceVersion,
queries,
});
await mutateAsync({ headers, body });
};
return { submit, ...rest };
};

View File

@ -0,0 +1,58 @@
import { useMutation, useQueryClient } from 'react-query';
import { useAppSelector } from '@/store';
import { useMetadataVersion } from '../../../MetadataAPI';
import { AccessType, FormOutput, QueryType } from '../../types';
import { api, cache } from '../../api';
export interface UseSubmitFormArgs {
tableName: string;
schemaName: string;
roleName: string;
queryType: QueryType;
accessType: AccessType;
}
export const useSubmitForm = (args: UseSubmitFormArgs) => {
const client = useQueryClient();
const headers = useAppSelector(state => state.tables.dataHeaders);
const { data: resourceVersion } = useMetadataVersion();
const { handleUpdate } = cache.useUpdateTablePermissionCache();
const { mutateAsync, ...rest } = useMutation(api.createPermissions, {
mutationKey: 'permissionsUpdate',
// this performs an optimistic cache update
// once the mutation has resolved the cache will be updated if it failed
onMutate: response => handleUpdate({ args, response }),
onError: (_err, _, context: any) => {
// if there is an error set the metadata query to the original value
client.setQueryData('metadata', context.metadata);
},
onSettled: () => {
// once the mutation is complete invalidate the query
// ensure the client state is upto date with the server state
client.invalidateQueries('metadata');
},
});
const submit = async (formData: FormOutput) => {
if (!resourceVersion) {
throw new Error('No resource version provided');
}
const { tableName, schemaName, roleName, queryType, accessType } = args;
const body = api.createInsertBody({
dataSource: 'pg',
qualifiedTable: { name: tableName, schema: schemaName },
roleName,
queryType,
accessType,
resourceVersion,
formData,
});
await mutateAsync({ headers, body });
};
return { submit, ...rest };
};

View File

@ -0,0 +1,92 @@
import React from 'react';
import ReactJson from 'react-json-view';
import { Story, Meta } from '@storybook/react';
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { Button } from '@/new-components/Button';
import { handlers } from '../../mocks/handlers.mock';
import {
useUpdatePermissions,
UseUpdatePermissionsArgs,
} from './useUpdatePermissions';
import { useDefaultValues } from '..';
const UseUpdatePermissionsComponent = ({
schemaName,
tableName,
roleName,
queryType,
accessType,
}: UseUpdatePermissionsArgs) => {
const { data } = useDefaultValues({
schemaName,
tableName,
roleName,
queryType,
});
const { updatePermissions, deletePermissions } = useUpdatePermissions({
schemaName,
tableName,
roleName,
queryType,
accessType,
});
return (
<div>
<p>
<strong>Data to submit</strong>
</p>
{!!data && <ReactJson src={data} />}
<div className="flex gap-2 mt-2">
<Button
mode="primary"
isLoading={updatePermissions.isLoading}
onClick={() => updatePermissions.submit(data!)}
>
Submit
</Button>
<Button
mode="destructive"
isLoading={deletePermissions.isLoading}
onClick={() => deletePermissions.submit(['insert'])}
>
Delete
</Button>
</div>
<div className="mt-2">
{!!updatePermissions.data && <ReactJson src={updatePermissions.data} />}
{!!deletePermissions.data && <ReactJson src={deletePermissions.data} />}
</div>
</div>
);
};
export default {
title: 'Permissions Form/hooks/useUpdatePermissions',
component: UseUpdatePermissionsComponent,
decorators: [ReactQueryDecorator()],
parameters: {
msw: handlers,
chromatic: { disableSnapshot: true },
},
} as Meta;
const schemaName = 'public';
const tableName = 'users';
const roleName = 'user';
export const Primary: Story<UseUpdatePermissionsArgs> = args => (
<UseUpdatePermissionsComponent {...args} />
);
Primary.args = {
schemaName,
tableName,
roleName,
queryType: 'insert',
accessType: 'fullAccess',
};

View File

@ -0,0 +1,36 @@
import { useSubmitForm } from './useSubmitForm';
import { useDeletePermission } from './useDeletePermission';
import { AccessType, QueryType } from '../../types';
export interface UseUpdatePermissionsArgs {
tableName: string;
schemaName: string;
roleName: string;
queryType: QueryType;
accessType: AccessType;
}
export const useUpdatePermissions = ({
tableName,
schemaName,
roleName,
queryType,
accessType,
}: UseUpdatePermissionsArgs) => {
const updatePermissions = useSubmitForm({
tableName,
schemaName,
roleName,
queryType,
accessType,
});
const deletePermissions = useDeletePermission({
tableName,
schemaName,
roleName,
});
return { updatePermissions, deletePermissions };
};

View File

@ -0,0 +1 @@
export { PermissionsForm } from './PermissionsForm';

View File

@ -0,0 +1,71 @@
import { HasuraMetadataV3 } from '@/metadata/types';
export const schemaList = {
result_type: 'TuplesOk',
result: [['schema_name'], ['public'], ['default']],
};
export const query = {
result_type: 'TuplesOk',
result: [
['tables'],
[
'[{"table_schema":"public","table_name":"another_table","table_type":"TABLE","comment":"comment","columns":[{"comment": null, "data_type": "integer", "table_name": "another_table", "column_name": "id", "is_nullable": "NO", "table_schema": "public", "column_default": "nextval(\'another_table_id_seq\'::regclass)", "data_type_name": "int4", "ordinal_position": 1}, {"comment": "column comment", "data_type": "text", "table_name": "another_table", "column_name": "name", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 2}],"triggers":[],"view_info":null}, {"table_schema":"public","table_name":"users","table_type":"TABLE","comment":null,"columns":[{"comment": null, "data_type": "integer", "table_name": "users", "column_name": "id", "is_nullable": "NO", "table_schema": "public", "column_default": "nextval(\'users_id_seq\'::regclass)", "data_type_name": "int4", "ordinal_position": 1}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "email", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 3}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "name", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 2}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "type", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 5}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "username", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 4}],"triggers":[],"view_info":null}]',
],
],
};
export const metadata = {
resource_version: 30,
metadata: {
version: 3,
sources: [
{
name: 'default',
kind: 'postgres',
tables: [
{ table: { schema: 'public', name: 'a_table' } },
{
table: { schema: 'public', name: 'users' },
insert_permissions: [
{
role: 'user',
permission: {
check: { id: { _eq: 1 } },
set: { id: 'x-hasura-as' },
columns: ['email', 'type'],
backend_only: false,
},
},
],
select_permissions: [
{
role: 'user',
permission: {
columns: ['email', 'id', 'type'],
filter: { id: { _eq: 1 } },
limit: 5,
allow_aggregations: true,
},
},
],
},
],
functions: [{ function: { schema: 'public', name: 'search_user2' } }],
configuration: {
connection_info: {
use_prepared_statements: true,
database_url: { from_env: 'HASURA_GRAPHQL_DATABASE_URL' },
isolation_level: 'read-committed',
pool_settings: {
connection_lifetime: 600,
retries: 1,
idle_timeout: 180,
max_connections: 50,
},
},
},
},
],
} as HasuraMetadataV3,
};

View File

@ -0,0 +1,27 @@
import { rest } from 'msw';
import { metadata, schemaList, query } from './dataStubs';
const url = 'http://localhost:8080';
export const handlers = [
rest.post(`${url}/v2/query`, (req, res, ctx) => {
const body = req.body as Record<string, any>;
const isUseSchemaList = body?.args?.sql?.includes('SELECT schema_name');
if (isUseSchemaList) {
return res(ctx.json(schemaList));
}
return res(ctx.json(query));
}),
rest.post(`${url}/v1/metadata`, (req, res, ctx) => {
const body = req.body as Record<string, any>;
if (body.type === 'export_metadata') {
return res(ctx.json(metadata));
}
return res(ctx.json([{ message: 'success' }]));
}),
];

View File

@ -1,106 +1,9 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { ArgType, PGFunction } from '@/dataSources/services/postgresql/types';
import { Table } from '@/dataSources/types';
import { NormalizedTable } from '@/dataSources/types';
export const currentSchema: Table = {
table_schema: 'public',
table_name: 'users',
table_type: 'TABLE',
is_table_tracked: true,
columns: [
{
comment: null,
data_type: 'integer',
table_name: 'users',
column_name: 'id',
is_nullable: 'NO',
table_schema: 'public',
column_default: null,
data_type_name: 'int4',
ordinal_position: 1,
},
{
comment: null,
data_type: 'text',
table_name: 'users',
column_name: 'name',
is_nullable: 'YES',
table_schema: 'public',
column_default: null,
data_type_name: 'text',
ordinal_position: 2,
},
{
comment: null,
data_type: 'text',
table_name: 'users',
column_name: 'location',
is_nullable: 'YES',
table_schema: 'public',
column_default: null,
data_type_name: 'text',
ordinal_position: 3,
},
],
// comment: null,
// triggers: [],
primary_key: {
table_schema: 'public',
table_name: 'users',
constraint_name: 'users_pkey',
columns: ['id'],
},
relationships: [],
permissions: [
{
role_name: 'one',
permissions: {
insert: {
check: {},
columns: [],
backend_only: false,
},
},
table_name: 'users',
table_schema: 'public',
},
{
role_name: 'two',
permissions: {
insert: {
check: {},
set: {
name: 'moo',
},
columns: [],
backend_only: false,
},
select: {
columns: ['name'],
filter: {
name: {
_eq: 'moo',
},
},
},
},
table_name: 'users',
table_schema: 'public',
},
],
unique_constraints: [],
check_constraints: [],
foreign_key_constraints: [],
opp_foreign_key_constraints: [],
view_info: null,
remote_relationships: [],
is_enum: false,
// configuration: {},
computed_fields: [],
};
export const allSchemas: Table[] = [
export const allSchemas: NormalizedTable[] = [
{
table_schema: 'public',
table_name: 'users',

View File

@ -1 +1,11 @@
import * as z from 'zod';
import { schema } from '../utils/formSchema';
export type QueryType = 'insert' | 'select' | 'update' | 'delete';
export type AccessType =
| 'fullAccess'
| 'noAccess'
| 'partialAccess'
| 'partialAccessWarning';
export type FormOutput = z.infer<typeof schema>;

View File

@ -0,0 +1,30 @@
import * as z from 'zod';
export const schema = z.object({
checkType: z.string(),
filterType: z.string(),
check: z.string(),
filter: z.string(),
rowCount: z.string(),
columns: z.record(z.optional(z.boolean())),
presets: z.optional(
z.array(
z.object({
columnName: z.string(),
presetType: z.optional(z.string()),
columnValue: z.optional(z.union([z.string(), z.number()])),
})
)
),
aggregationEnabled: z.boolean(),
backendOnly: z.boolean(),
clonePermissions: z.optional(
z.array(
z.object({
tableName: z.optional(z.string()),
queryType: z.optional(z.string()),
roleName: z.optional(z.string()),
})
)
),
});

View File

@ -0,0 +1,14 @@
import { Permission } from '@/dataSources/types';
interface Args {
permissions?: Permission[];
roleName: string;
}
export const getCurrentRole = ({ permissions, roleName }: Args) => {
const rolePermissions = permissions?.find(
({ role_name }) => role_name === roleName
);
return rolePermissions;
};

View File

@ -0,0 +1,2 @@
export * from './functions';
export * from './formSchema';

View File

@ -1,6 +1,6 @@
import { rest } from 'msw';
const url = 'http://localhost:6006';
const url = 'http://localhost:8080';
export const handlers = [
rest.post(`${url}/v2/query`, (req, res, ctx) => {
@ -18,29 +18,15 @@ export const handlers = [
}
return res(
ctx.json([
{
result_type: 'TuplesOk',
result: [
['tables'],
[
'[{"table_schema":"public","table_name":"users","table_type":"TABLE","comment":"hias","columns":[{"comment": null, "data_type": "integer", "table_name": "users", "column_name": "id", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "int4", "ordinal_position": 1}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "description", "is_nullable": "YES", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 3}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "name", "is_nullable": "YES", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 2}],"triggers":[],"view_info":null}, {"table_schema":"default","table_name":"users","table_type":"TABLE","comment":null,"columns":[{"comment": null, "data_type": "integer", "table_name": "users", "column_name": "id", "is_nullable": "NO", "table_schema": "default", "column_default": "nextval(\'\\"default\\".users_id_seq\'::regclass)", "data_type_name": "int4", "ordinal_position": 1}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "name", "is_nullable": "NO", "table_schema": "default", "column_default": null, "data_type_name": "text", "ordinal_position": 2}],"triggers":[],"view_info":null}]',
],
ctx.json({
result_type: 'TuplesOk',
result: [
['tables'],
[
'[{"table_schema":"public","table_name":"another_table","table_type":"TABLE","comment":"comment","columns":[{"comment": null, "data_type": "integer", "table_name": "another_table", "column_name": "id", "is_nullable": "NO", "table_schema": "public", "column_default": "nextval(\'another_table_id_seq\'::regclass)", "data_type_name": "int4", "ordinal_position": 1}, {"comment": "column comment", "data_type": "text", "table_name": "another_table", "column_name": "name", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 2}],"triggers":[],"view_info":null}, {"table_schema":"public","table_name":"users","table_type":"TABLE","comment":null,"columns":[{"comment": null, "data_type": "integer", "table_name": "users", "column_name": "id", "is_nullable": "NO", "table_schema": "public", "column_default": "nextval(\'users_id_seq\'::regclass)", "data_type_name": "int4", "ordinal_position": 1}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "email", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 3}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "name", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 2}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "type", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 5}, {"comment": null, "data_type": "text", "table_name": "users", "column_name": "username", "is_nullable": "NO", "table_schema": "public", "column_default": null, "data_type_name": "text", "ordinal_position": 4}],"triggers":[],"view_info":null}]',
],
},
{ result_type: 'TuplesOk', result: [['coalesce'], ['[]']] },
{
result_type: 'TuplesOk',
result: [
['coalesce'],
[
'[{"table_schema":"default","table_name":"users","constraint_name":"users_pkey","columns":["id"]}, {"table_schema":"public","table_name":"users","constraint_name":"users_pkey","columns":["id"]}]',
],
],
},
{ result_type: 'TuplesOk', result: [['coalesce'], ['[]']] },
{ result_type: 'TuplesOk', result: [['coalesce'], ['[]']] },
])
],
})
);
}),
@ -49,7 +35,7 @@ export const handlers = [
if (body.type === 'export_metadata') {
return res(
ctx.json({
resource_version: 380,
resource_version: 30,
metadata: {
version: 3,
sources: [
@ -57,14 +43,16 @@ export const handlers = [
name: 'default',
kind: 'postgres',
tables: [
{ table: { schema: 'public', name: 'a_table' } },
{
table: { schema: 'public', name: 'users' },
insert_permissions: [
{
role: 'user',
permission: {
check: {},
columns: ['id', 'name'],
check: { id: { _eq: 1 } },
set: { id: 'x-hasura-as' },
columns: ['email', 'type'],
backend_only: false,
},
},
@ -73,21 +61,29 @@ export const handlers = [
{
role: 'user',
permission: {
columns: ['id', 'description', 'name'],
filter: {},
limit: 0,
columns: ['email', 'id', 'type'],
filter: { id: { _eq: 1 } },
limit: 5,
allow_aggregations: true,
},
},
],
},
],
functions: [{ function: { schema: 'public', name: 'me' } }],
functions: [
{ function: { schema: 'public', name: 'search_user2' } },
],
configuration: {
connection_info: {
use_prepared_statements: false,
database_url:
'postgres://postgres:postgrespassword@postgres:5432/postgres',
use_prepared_statements: true,
database_url: { from_env: 'HASURA_GRAPHQL_DATABASE_URL' },
isolation_level: 'read-committed',
pool_settings: {
connection_lifetime: 600,
retries: 1,
idle_timeout: 180,
max_connections: 50,
},
},
},
},