Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add beforeOperation.[create|update|delete] and afterOperation.[create|update|delete] operation routing for list hooks #8826

Merged
merged 8 commits into from
Sep 25, 2023
2 changes: 1 addition & 1 deletion .changeset/odd-lemons-hide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@keystone-6/core': patch
---

Fixes hooks.validateInput argument types for update operations
Fixes `hooks.validateInput` argument types for update operations
5 changes: 5 additions & 0 deletions .changeset/refine-hook-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@keystone-6/core": minor
---

Add `beforeOperation.[create|update|delete]` and `afterOperation.[create|update|delete]` operation routing for list hooks
20 changes: 16 additions & 4 deletions examples/hooks/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,23 @@ export const lists: Lists = {
// an example of a content filter, the prevents the title or content containing the word "Profanity"
if (preventDelete) return addValidationError('Cannot delete Post, preventDelete is true');
},
beforeOperation: ({ resolvedData, operation }) => {
console.log(`Post ${operation}`, resolvedData);

beforeOperation: ({ item, resolvedData, operation }) => {
console.log(`Post beforeOperation.${operation}`, resolvedData);
},
afterOperation: ({ resolvedData, operation }) => {
console.log(`Post ${operation}`, resolvedData);

afterOperation: {
create: ({ inputData, item }) => {
console.log(`Post afterOperation.create`, inputData, '->', item);
},

update: ({ originalItem, item }) => {
console.log(`Post afterOperation.update`, originalItem, '->', item);
},

delete: ({ originalItem }) => {
console.log(`Post afterOperation.delete`, originalItem, '-> deleted');
},
},
},
}),
Expand Down
113 changes: 91 additions & 22 deletions packages/core/src/lib/core/initialise-lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { ResolvedDBField, resolveRelationships } from './resolve-relationships';
import { outputTypeField } from './queries/output-field';
import { assertFieldsValid } from './field-assertions';

export type InitialisedField = Omit<NextFieldType, 'dbField' | 'access' | 'graphql'> & {
export type InitialisedField = {
access: ResolvedFieldAccessControl;
dbField: ResolvedDBField;
hooks: ResolvedFieldHooks<BaseListTypeInfo>;
Expand All @@ -48,17 +48,30 @@ export type InitialisedField = Omit<NextFieldType, 'dbField' | 'access' | 'graph
cacheHint: CacheHint | undefined;
};
ui: {
label: string;
description: string;
views: string;
createView: {
fieldMode: MaybeSessionFunction<'edit' | 'hidden', any>;
};
itemView: {
fieldMode: MaybeItemFunction<'read' | 'edit' | 'hidden', any>;
fieldPosition: MaybeItemFunction<'form' | 'sidebar', any>;
};
listView: {
fieldMode: MaybeSessionFunction<'read' | 'hidden', any>;
};
};
};
} & Pick<
NextFieldType,
| 'input'
| 'output'
| 'getAdminMeta'
| 'views'
| '__ksTelemetryFieldTypeName'
| 'extraOutputFields'
| 'unreferencedConcreteInterfaceImplementations'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will minimize the number of breaking changes moving forward and keeps us keenly aware of what else is being copied to this type.

>;

export type InitialisedList = {
access: ResolvedListAccessControl;
Expand Down Expand Up @@ -163,15 +176,6 @@ function defaultOperationHook() {}
function defaultListHooksResolveInput({ resolvedData }: { resolvedData: any }) {
return resolvedData;
}
function defaultFieldHooksResolveInput({
resolvedData,
fieldKey,
}: {
resolvedData: any;
fieldKey: string;
}) {
return resolvedData[fieldKey];
}

function parseListHooksResolveInput(f: ListHooks<BaseListTypeInfo>['resolveInput']) {
if (typeof f === 'function') {
Expand All @@ -185,25 +189,80 @@ function parseListHooksResolveInput(f: ListHooks<BaseListTypeInfo>['resolveInput
return { create, update };
}

function parseListHooksBeforeOperation(f: ListHooks<BaseListTypeInfo>['beforeOperation']) {
if (typeof f === 'function') {
return {
create: f,
update: f,
delete: f,
};
}

const {
create = defaultOperationHook,
update = defaultOperationHook,
delete: _delete = defaultOperationHook,
} = f ?? {};
return { create, update, delete: _delete };
}

function parseListHooksAfterOperation(f: ListHooks<BaseListTypeInfo>['afterOperation']) {
if (typeof f === 'function') {
return {
create: f,
update: f,
delete: f,
};
}

const {
create = defaultOperationHook,
update = defaultOperationHook,
delete: _delete = defaultOperationHook,
} = f ?? {};
return { create, update, delete: _delete };
}

function defaultFieldHooksResolveInput({
resolvedData,
fieldKey,
}: {
resolvedData: any;
fieldKey: string;
}) {
return resolvedData[fieldKey];
}

function parseListHooks(hooks: ListHooks<BaseListTypeInfo>): ResolvedListHooks<BaseListTypeInfo> {
return {
resolveInput: parseListHooksResolveInput(hooks.resolveInput),
validateInput: hooks.validateInput ?? defaultOperationHook,
validateDelete: hooks.validateDelete ?? defaultOperationHook,
beforeOperation: hooks.beforeOperation ?? defaultOperationHook,
afterOperation: hooks.afterOperation ?? defaultOperationHook,
beforeOperation: parseListHooksBeforeOperation(hooks.beforeOperation),
afterOperation: parseListHooksAfterOperation(hooks.afterOperation),
};
}

function parseFieldHooks(
hooks: FieldHooks<BaseListTypeInfo>
): ResolvedFieldHooks<BaseListTypeInfo> {
return {
resolveInput: hooks.resolveInput ?? defaultFieldHooksResolveInput,
resolveInput: {
create: hooks.resolveInput ?? defaultFieldHooksResolveInput,
update: hooks.resolveInput ?? defaultFieldHooksResolveInput,
},
validateInput: hooks.validateInput ?? defaultOperationHook,
validateDelete: hooks.validateDelete ?? defaultOperationHook,
beforeOperation: hooks.beforeOperation ?? defaultOperationHook,
afterOperation: hooks.afterOperation ?? defaultOperationHook,
beforeOperation: {
create: hooks.beforeOperation ?? defaultOperationHook,
update: hooks.beforeOperation ?? defaultOperationHook,
delete: hooks.beforeOperation ?? defaultOperationHook,
},
afterOperation: {
create: hooks.afterOperation ?? defaultOperationHook,
update: hooks.afterOperation ?? defaultOperationHook,
delete: hooks.afterOperation ?? defaultOperationHook,
},
};
}

Expand Down Expand Up @@ -276,7 +335,6 @@ function getListsWithInitialisedFields(
};

resultFields[fieldKey] = {
...f,
dbField: f.dbField as ResolvedDBField,
access: parseFieldAccessControl(f.access),
hooks: parseFieldHooks(f.hooks ?? {}),
Expand All @@ -289,16 +347,17 @@ function getListsWithInitialisedFields(
update: f.graphql?.isNonNull?.update ?? false,
},
},
input: { ...f.input }, // copy
ui: {
...f.ui,
label: f.label ?? '',
description: f.ui?.description ?? '',
views: f.ui?.views ?? '',
createView: {
...f.ui?.createView,
...f.ui?.createView, // copy
fieldMode: _isEnabled.create ? fieldModes.create : 'hidden',
},

itemView: {
...f.ui?.itemView,
fieldPosition: f.ui?.itemView?.fieldPosition ?? 'form',
fieldMode: _isEnabled.update
? fieldModes.item
: _isEnabled.read && fieldModes.item !== 'hidden'
Expand All @@ -307,10 +366,20 @@ function getListsWithInitialisedFields(
},

listView: {
...f.ui?.listView,
...f.ui?.listView, // copy
fieldMode: _isEnabled.read ? fieldModes.list : 'hidden',
},
},

// copy
__ksTelemetryFieldTypeName: f.__ksTelemetryFieldTypeName,
extraOutputFields: f.extraOutputFields,
getAdminMeta: f.getAdminMeta,
input: { ...f.input },
output: { ...f.output },
unreferencedConcreteInterfaceImplementations:
f.unreferencedConcreteInterfaceImplementations,
views: f.views,
};
}

Expand Down
16 changes: 11 additions & 5 deletions packages/core/src/lib/core/mutations/create-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,11 +302,17 @@ async function getResolvedData(
try {
return [
fieldKey,
await field.hooks.resolveInput({
...hookArgs,
resolvedData,
fieldKey,
}),
operation === 'create'
? await field.hooks.resolveInput.create({
...hookArgs,
resolvedData,
fieldKey,
})
: await field.hooks.resolveInput.update({
...hookArgs,
resolvedData,
fieldKey,
}),
];
} catch (error: any) {
fieldsErrors.push({
Expand Down
12 changes: 8 additions & 4 deletions packages/core/src/lib/core/mutations/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import type { InitialisedList } from '../initialise-lists';

export async function runSideEffectOnlyHook<
HookName extends 'beforeOperation' | 'afterOperation',
Args extends Parameters<NonNullable<InitialisedList['hooks'][HookName]>>[0]
Args extends Parameters<
NonNullable<InitialisedList['hooks'][HookName]['create' | 'update' | 'delete']>
>[0]
>(list: InitialisedList, hookName: HookName, args: Args) {
const { operation } = args;

let shouldRunFieldLevelHook: (fieldKey: string) => boolean;
if (args.operation === 'delete') {
if (operation === 'delete') {
// always run field hooks for delete operations
shouldRunFieldLevelHook = () => true;
} else {
Expand All @@ -22,7 +26,7 @@ export async function runSideEffectOnlyHook<
Object.entries(list.fields).map(async ([fieldKey, field]) => {
if (shouldRunFieldLevelHook(fieldKey)) {
try {
await field.hooks[hookName]({ fieldKey, ...args } as any); // TODO: FIXME any
await field.hooks[hookName][operation]({ fieldKey, ...args } as any); // TODO: FIXME any
} catch (error: any) {
fieldsErrors.push({ error, tag: `${list.listKey}.${fieldKey}.hooks.${hookName}` });
}
Expand All @@ -36,7 +40,7 @@ export async function runSideEffectOnlyHook<

// list hooks
try {
await list.hooks[hookName](args as any); // TODO: FIXME any
await list.hooks[hookName][operation](args as any); // TODO: FIXME any
} catch (error: any) {
throw extensionError(hookName, [{ error, tag: `${list.listKey}.hooks.${hookName}` }]);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/lib/create-admin-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@ export function createAdminMeta(

const fieldMeta = {
key: fieldKey,
label: field.label ?? humanize(fieldKey),
description: field.ui?.description ?? null,
label: field.ui.label ?? humanize(fieldKey),
description: field.ui.description ?? null,
viewsIndex: getViewId(field.views),
customViewsIndex:
field.ui?.views === undefined
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types/config/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type FilterOrderArgs<ListTypeInfo extends BaseListTypeInfo> = {
export type CommonFieldConfig<ListTypeInfo extends BaseListTypeInfo> = {
access?: FieldAccessControl<ListTypeInfo>;
hooks?: FieldHooks<ListTypeInfo, ListTypeInfo['fields']>;
label?: string;
label?: string; // TODO: move to ui?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently text({ label: 'a text field' }) is what we use for fields, but not for lists.
Strange

ui?: {
description?: string;
views?: string;
Expand Down
Loading