console: add insert handling for permissions

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7973
Co-authored-by: Julian@Hasura <118911427+julian-mayorga@users.noreply.github.com>
GitOrigin-RevId: 6367c6bbf66e58fe2e08caacdf3217125c29a4c7
This commit is contained in:
Erik Magnusson 2023-02-22 17:25:27 +02:00 committed by hasura-bot
parent d3287e5fb3
commit 3423e53480
11 changed files with 72 additions and 251 deletions

View File

@ -199,7 +199,6 @@ const Component = (props: ComponentProps) => {
tables={tables}
roles={roles}
/>
<div className="pt-2 flex gap-2">
<Button
type="submit"

View File

@ -47,7 +47,6 @@ const selectArgs: CreateInsertArgs = {
test('create select args object from form data', () => {
const result = createInsertArgs(selectArgs);
expect(result).toEqual([
{
args: {
@ -63,7 +62,7 @@ test('create select args object from form data', () => {
allow_aggregations: false,
columns: ['email', 'type'],
filter: {},
presets: [],
set: [],
},
role: 'user',
source: 'default',

View File

@ -3,14 +3,14 @@ import produce from 'immer';
import { allowedMetadataTypes } from '@/features/MetadataAPI';
import { AccessType } from '../../types';
import { PermissionsSchema, Presets } from '../../schema';
import { PermissionsSchema } from '../../schema';
import { areTablesEqual } from '@/features/hasura-metadata-api';
import { Table } from '@/features/hasura-metadata-types';
import { getTableDisplayName } from '@/features/DatabaseRelationships';
type SelectPermissionMetadata = {
columns: string[];
presets: Presets;
set: Record<string, any>;
filter: Record<string, any>;
allow_aggregations?: boolean;
limit?: number;
@ -46,7 +46,7 @@ const createSelectObject = (input: PermissionsSchema) => {
const permissionObject: SelectPermissionMetadata = {
columns,
filter,
presets: [],
set: [],
allow_aggregations: input.aggregationEnabled,
};
@ -68,6 +68,40 @@ const createSelectObject = (input: PermissionsSchema) => {
throw new Error('Case not handled');
};
type InsertPermissionMetadata = {
columns: string[];
check: Record<string, any>;
allow_upsert: boolean;
backend_only?: boolean;
set: Record<string, any>;
};
const createInsertObject = (input: PermissionsSchema) => {
if (input.queryType === 'insert') {
const columns = Object.entries(input.columns)
.filter(({ 1: value }) => value)
.map(([key]) => key);
const set =
input?.presets?.reduce((acc, preset) => {
if (preset.columnName === 'default') return acc;
return { ...acc, [preset.columnName]: preset.columnValue };
}, {}) ?? {};
const permissionObject: InsertPermissionMetadata = {
columns,
check: input.check,
allow_upsert: true,
set,
backend_only: input.backendOnly,
};
return permissionObject;
}
throw new Error('Case not handled');
};
/**
* creates the permissions object for the server
*/
@ -76,7 +110,7 @@ const createPermission = (formData: PermissionsSchema) => {
case 'select':
return createSelectObject(formData);
case 'insert':
throw new Error('Case not handled');
return createInsertObject(formData);
case 'update':
throw new Error('Case not handled');
case 'delete':
@ -169,7 +203,7 @@ export const createInsertArgs = ({
d => {
if (!areTablesEqual(clonedPermissionTable, table)) {
d.columns = [];
d.presets = [];
d.set = {};
}
return d;

View File

@ -3,6 +3,7 @@ import { useFormContext } from 'react-hook-form';
import { Collapse } from '@/new-components/deprecated';
import { QueryType } from '../../types';
import { Switch } from '../../../../new-components/Switch/Switch';
export interface BackEndOnlySectionProps {
queryType: QueryType;
@ -13,7 +14,7 @@ export const BackendOnlySection: React.FC<BackEndOnlySectionProps> = ({
queryType,
defaultOpen,
}) => {
const { register, watch } = useFormContext();
const { setValue, watch } = useFormContext();
const enabled = watch('backendOnly');
@ -29,12 +30,10 @@ export const BackendOnlySection: React.FC<BackEndOnlySectionProps> = ({
/>
<Collapse.Content>
<label className="flex items-center gap-4">
<input
type="checkbox"
className="rounded shadow-sm border border-gray-300 hover:border-gray-400 focus:ring-yellow-400 m-0"
{...register('backendOnly')}
<Switch
checked={enabled}
onCheckedChange={switched => setValue('backendOnly', switched)}
/>
<span>Allow from backends only</span>
</label>
</Collapse.Content>

View File

@ -78,12 +78,12 @@ const PresetsRow: React.FC<PresetsRowProps> = ({
<div>
<input
id="value"
id="columnValue"
type="text"
className={className}
placeholder="Column value"
disabled={disabled}
{...register(`presets.${id}.value`)}
{...register(`presets.${id}.columnValue`)}
/>
</div>

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import AceEditor from 'react-ace';
import { useFormContext } from 'react-hook-form';
import { Table } from '@/features/hasura-metadata-types';
@ -11,21 +11,13 @@ import { InputField } from '@/new-components/Form';
import { IconTooltip } from '@/new-components/Tooltip';
import { Collapse } from '@/new-components/deprecated';
import { getIngForm } from '../../../../components/Services/Data/utils';
import { RowPermissionBuilder } from './RowPermissionsBuilder';
import { QueryType } from '../../types';
import { ReturnValue } from '../hooks';
import { getAllowedFilterKeys } from '../../PermissionsTable/hooks';
const NoChecksLabel = () => (
<span data-test="without-checks">
Without any checks&nbsp;
{/* {filterQueries['{}'] && (
<i className={styles.add_mar_left_small}>
(Same as <b>{filterQueries['{}'].join(', ')}</b>)
</i>
)} */}
</span>
<span data-test="without-checks">Without any checks&nbsp;</span>
);
const CustomLabel = () => (
@ -256,7 +248,7 @@ export const RowPermissionsSection: React.FC<RowPermissionsProps> = ({
<div className="pt-4">
{!isLoading && tableName ? (
<RowPermissionBuilder
nesting={['filter']}
nesting={getAllowedFilterKeys(queryType)}
table={table}
dataSourceName={dataSourceName}
/>

View File

@ -1,202 +0,0 @@
import React from 'react';
import { z } from 'zod';
import { ComponentStory, Meta } from '@storybook/react';
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { SimpleForm } from '@/new-components/Form';
import { RowPermissionBuilder } from './RowPermissionBuilder';
import { createDefaultValues } from './utils';
import {
complicatedExample,
exampleWithBoolOperator,
exampleWithNotOperator,
exampleWithRelationship,
handlers,
schema,
simpleExample,
} from './mocks';
export default {
title: 'Features/Permissions/Form/Row Permissions Builder',
component: RowPermissionBuilder,
decorators: [ReactQueryDecorator()],
parameters: {
msw: handlers(),
},
} as Meta;
export const Primary: ComponentStory<typeof RowPermissionBuilder> = args => (
<RowPermissionBuilder {...args} />
);
Primary.args = {
tableName: 'user',
nesting: ['filter'],
};
Primary.decorators = [
Component => {
return (
<div style={{ width: 800 }}>
<SimpleForm schema={z.any()} onSubmit={console.log}>
<Component />
</SimpleForm>
</div>
);
},
];
export const WithDefaults: ComponentStory<
typeof RowPermissionBuilder
> = args => <RowPermissionBuilder {...args} />;
WithDefaults.args = {
tableName: 'Album',
nesting: ['filter'],
};
WithDefaults.decorators = [
Component => {
return (
<div style={{ width: 800 }}>
<SimpleForm
schema={z.any()}
options={{
defaultValues: createDefaultValues({
tableName: 'Album',
schema,
existingPermission: simpleExample,
tableConfig: {},
}),
}}
onSubmit={console.log}
>
<Component />
</SimpleForm>
</div>
);
},
];
export const WithDefaultsBool: ComponentStory<
typeof RowPermissionBuilder
> = args => <RowPermissionBuilder {...args} />;
WithDefaultsBool.args = {
tableName: 'user',
nesting: ['filter'],
};
WithDefaultsBool.decorators = [
Component => {
return (
<div style={{ width: 800 }}>
<SimpleForm
schema={z.any()}
options={{
defaultValues: createDefaultValues({
tableName: 'user',
schema,
existingPermission: exampleWithBoolOperator,
tableConfig: {},
}),
}}
onSubmit={console.log}
>
<Component />
</SimpleForm>
</div>
);
},
];
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} />;
WithDefaultsRelationship.args = {
tableName: 'user',
nesting: ['filter'],
};
WithDefaultsRelationship.decorators = [
Component => {
return (
<div style={{ width: 800 }}>
<SimpleForm
schema={z.any()}
options={{
defaultValues: createDefaultValues({
tableName: 'user',
schema,
existingPermission: exampleWithRelationship,
tableConfig: {},
}),
}}
onSubmit={console.log}
>
<Component />;
</SimpleForm>
</div>
);
},
];
export const WithPointlesslyComplicatedRelationship: ComponentStory<
typeof RowPermissionBuilder
> = args => <RowPermissionBuilder {...args} />;
WithPointlesslyComplicatedRelationship.args = {
tableName: 'user',
nesting: ['filter'],
};
WithPointlesslyComplicatedRelationship.decorators = [
Component => {
return (
<div style={{ width: 800 }}>
<SimpleForm
schema={z.any()}
options={{
defaultValues: createDefaultValues({
tableName: 'user',
schema,
existingPermission: complicatedExample,
tableConfig: {},
}),
}}
onSubmit={console.log}
>
<Component />
</SimpleForm>
</div>
);
},
];

View File

@ -67,6 +67,8 @@ export const ValueInput = ({
o => o.operator === comparatorName
);
const jsType = typeof graphQLTypeToJsType(value, comparator?.type);
const inputType =
jsType === 'boolean' ? 'checkbox' : jsType === 'string' ? 'text' : 'number';
return (
<>
@ -79,16 +81,15 @@ export const ValueInput = ({
value={value}
comparatorType={comparator?.type}
/>
{jsType === 'boolean' ||
(isComparator(comparatorName) && (
<Button
disabled={comparatorName === '_where' && isEmpty(table)}
onClick={() => setValue(path, 'X-Hasura-User-Id')}
mode="default"
>
[x-hasura-user-id]
</Button>
))}
{inputType === 'text' && isComparator(comparatorName) && (
<Button
disabled={comparatorName === '_where' && isEmpty(table)}
onClick={() => setValue(path, 'X-Hasura-User-Id')}
mode="default"
>
[x-hasura-user-id]
</Button>
)}
</>
);
};

View File

@ -66,13 +66,15 @@ export const getPresets = ({ currentQueryPermissions }: GetPresetArgs) => {
[string, string]
>;
return set.map(([columnName, value]) => {
return set.map(([columnName, columnValue]) => {
return {
columnName,
presetType: value.startsWith('x-hasura')
? 'from session variable'
: 'static',
value,
presetType:
typeof columnValue === 'string' &&
columnValue.toLowerCase().startsWith('x-hasura')
? 'from session variable'
: 'static',
columnValue,
};
});
};
@ -147,7 +149,7 @@ export const createPermission = {
permission: InsertPermissionDefinition,
tableColumns: TableColumn[]
) => {
const check = JSON.stringify(permission.check) || '';
const check = permission.check || {};
const checkType = getCheckType(permission.check);
const presets = getPresets({
currentQueryPermissions: permission,

View File

@ -4,17 +4,13 @@ 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;

View File

@ -120,7 +120,8 @@ export const PermissionsTable: React.FC<PermissionsTableProps> = ({
{permissionTypes.map(({ permissionType, access }) => {
// only select is possible on GDC as mutations are not available yet
const isEditable =
roleName !== 'admin' && permissionType === 'select';
(roleName !== 'admin' && permissionType === 'select') ||
(roleName !== 'admin' && permissionType === 'insert');
if (isNewRole) {
return (