mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
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:
parent
d3287e5fb3
commit
3423e53480
@ -199,7 +199,6 @@ const Component = (props: ComponentProps) => {
|
||||
tables={tables}
|
||||
roles={roles}
|
||||
/>
|
||||
|
||||
<div className="pt-2 flex gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
{/* {filterQueries['{}'] && (
|
||||
<i className={styles.add_mar_left_small}>
|
||||
(Same as <b>{filterQueries['{}'].join(', ')}</b>)
|
||||
</i>
|
||||
)} */}
|
||||
</span>
|
||||
<span data-test="without-checks">Without any checks </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}
|
||||
/>
|
||||
|
@ -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>
|
||||
);
|
||||
},
|
||||
];
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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 (
|
||||
|
Loading…
Reference in New Issue
Block a user