Skip to content

Commit

Permalink
Add beforeOperation.[create|update|delete] and `afterOperation.[cre…
Browse files Browse the repository at this point in the history
…ate|update|delete]` operation routing for list hooks (#8826)
  • Loading branch information
dcousens authored Sep 25, 2023
1 parent feb86cc commit dc26bdf
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 95 deletions.
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
111 changes: 89 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 | null;
description: string | null;
views: string | null;
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'
>;

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,16 @@ function getListsWithInitialisedFields(
update: f.graphql?.isNonNull?.update ?? false,
},
},
input: { ...f.input }, // copy
ui: {
...f.ui,
label: f.label ?? null,
description: f.ui?.description ?? null,
views: f.ui?.views ?? null,
createView: {
...f.ui?.createView,
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 +365,19 @@ function getListsWithInitialisedFields(
},

listView: {
...f.ui?.listView,
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
14 changes: 7 additions & 7 deletions packages/core/src/lib/create-admin-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,26 +194,26 @@ 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
field.ui.views === null
? null
: (assertValidView(field.views, `lists.${listKey}.fields.${fieldKey}.ui.views`),
getViewId(field.ui.views)),
fieldMeta: null,
listKey: listKey,
search: list.ui.searchableFields.get(fieldKey) ?? null,
createView: {
fieldMode: normalizeMaybeSessionFunction(field.ui?.createView.fieldMode),
fieldMode: normalizeMaybeSessionFunction(field.ui.createView.fieldMode),
},
itemView: {
fieldMode: field.ui?.itemView.fieldMode,
fieldPosition: field.ui?.itemView?.fieldPosition || 'form',
fieldMode: field.ui.itemView.fieldMode,
fieldPosition: field.ui.itemView.fieldPosition,
},
listView: {
fieldMode: normalizeMaybeSessionFunction(field.ui?.listView?.fieldMode),
fieldMode: normalizeMaybeSessionFunction(field.ui.listView.fieldMode),
},
isFilterable: normalizeIsOrderFilter(
field.input?.where ? field.graphql.isEnabled.filter : false,
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?
ui?: {
description?: string;
views?: string;
Expand Down
Loading

0 comments on commit dc26bdf

Please sign in to comment.