@opensaas/stack-core 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +74 -0
- package/CLAUDE.md +18 -2
- package/dist/access/access-filter.d.ts +29 -0
- package/dist/access/access-filter.d.ts.map +1 -0
- package/dist/access/access-filter.js +68 -0
- package/dist/access/access-filter.js.map +1 -0
- package/dist/access/engine.d.ts +15 -48
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +14 -280
- package/dist/access/engine.js.map +1 -1
- package/dist/access/field-access.d.ts +44 -0
- package/dist/access/field-access.d.ts.map +1 -0
- package/dist/access/field-access.js +123 -0
- package/dist/access/field-access.js.map +1 -0
- package/dist/access/field-access.test.d.ts +2 -0
- package/dist/access/field-access.test.d.ts.map +1 -0
- package/dist/access/{engine.test.js → field-access.test.js} +2 -2
- package/dist/access/field-access.test.js.map +1 -0
- package/dist/access/field-visibility.d.ts +13 -0
- package/dist/access/field-visibility.d.ts.map +1 -0
- package/dist/access/field-visibility.js +155 -0
- package/dist/access/field-visibility.js.map +1 -0
- package/dist/access/index.d.ts +4 -1
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +8 -1
- package/dist/access/index.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/types.d.ts +45 -4
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/hook-pipeline.d.ts +49 -0
- package/dist/context/hook-pipeline.d.ts.map +1 -0
- package/dist/context/hook-pipeline.js +75 -0
- package/dist/context/hook-pipeline.js.map +1 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +30 -462
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +72 -68
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/write-pipeline.d.ts +158 -0
- package/dist/context/write-pipeline.d.ts.map +1 -0
- package/dist/context/write-pipeline.js +306 -0
- package/dist/context/write-pipeline.js.map +1 -0
- package/dist/extend.d.ts +3 -0
- package/dist/extend.d.ts.map +1 -0
- package/dist/extend.js +10 -0
- package/dist/extend.js.map +1 -0
- package/dist/fields/index.d.ts +1 -0
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +213 -2
- package/dist/fields/index.js.map +1 -1
- package/dist/hooks/index.d.ts +20 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +202 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +5 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -10
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +8 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +16 -0
- package/dist/internal.js.map +1 -0
- package/dist/validation/field-config.d.ts +55 -0
- package/dist/validation/field-config.d.ts.map +1 -0
- package/dist/validation/field-config.js +100 -0
- package/dist/validation/field-config.js.map +1 -0
- package/dist/validation/field-config.test.d.ts +2 -0
- package/dist/validation/field-config.test.d.ts.map +1 -0
- package/dist/validation/field-config.test.js +159 -0
- package/dist/validation/field-config.test.js.map +1 -0
- package/package.json +12 -4
- package/src/access/access-filter.ts +97 -0
- package/src/access/engine.ts +13 -396
- package/src/access/{engine.test.ts → field-access.test.ts} +1 -1
- package/src/access/field-access.ts +159 -0
- package/src/access/field-visibility.ts +247 -0
- package/src/access/index.ts +7 -4
- package/src/config/index.ts +1 -0
- package/src/config/types.ts +51 -4
- package/src/context/hook-pipeline.ts +160 -0
- package/src/context/index.ts +29 -667
- package/src/context/nested-operations.ts +142 -111
- package/src/context/write-pipeline.ts +543 -0
- package/src/extend.ts +14 -0
- package/src/fields/index.ts +310 -2
- package/src/hooks/index.ts +227 -0
- package/src/index.ts +27 -90
- package/src/internal.ts +49 -0
- package/src/validation/field-config.test.ts +199 -0
- package/src/validation/field-config.ts +145 -0
- package/tests/access-relationships.test.ts +4 -4
- package/tests/access.test.ts +1 -1
- package/tests/field-hooks.test.ts +410 -0
- package/tests/field-types.test.ts +1 -1
- package/tests/hook-pipeline.test.ts +233 -0
- package/tests/nested-operation-registry.test.ts +206 -0
- package/tests/write-pipeline.test.ts +588 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +43 -1
- package/dist/access/engine.test.d.ts +0 -2
- package/dist/access/engine.test.d.ts.map +0 -1
- package/dist/access/engine.test.js.map +0 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ListConfig } from '../config/types.js';
|
|
2
|
+
import type { AccessContext } from '../access/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Hook Pipeline — the single module that runs the transform+validate span of a
|
|
5
|
+
* write: list `resolveInput` → field `resolveInput` → list `validate` → field
|
|
6
|
+
* `validate` → built-in field rules (`validateFieldRules`). It owns the order of
|
|
7
|
+
* these phases and the threading of `resolvedData` through them, in one place.
|
|
8
|
+
*
|
|
9
|
+
* It is THE place where input is shaped and validated; it throws
|
|
10
|
+
* {@link ValidationError} on failure exactly as before (validate hooks via
|
|
11
|
+
* `addValidationError`, then `validateFieldRules`) — validation is never silent.
|
|
12
|
+
*
|
|
13
|
+
* Side-effect hooks (`beforeOperation`/`afterOperation`), operation-level access,
|
|
14
|
+
* writable-field filtering, nested operations, persistence and Field Visibility
|
|
15
|
+
* are deliberately OUT of this span — they stay in the Write Pipeline. See the
|
|
16
|
+
* "Hook Pipeline" and "Write Pipeline" glossary terms in CONTEXT.md.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Arguments for one transform+validate span. Only the create/update operations
|
|
20
|
+
* run this span (delete skips the input-shaping phases entirely).
|
|
21
|
+
*/
|
|
22
|
+
export interface HookPipelineArgs {
|
|
23
|
+
operation: 'create' | 'update';
|
|
24
|
+
listName: string;
|
|
25
|
+
listConfig: ListConfig<any>;
|
|
26
|
+
/** The original input data for the write. */
|
|
27
|
+
inputData: Record<string, unknown>;
|
|
28
|
+
/** The existing row for update; `undefined` for create. */
|
|
29
|
+
item: Record<string, unknown> | undefined;
|
|
30
|
+
context: AccessContext;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Result of a transform+validate span: the fully-resolved write data after the
|
|
34
|
+
* resolveInput hooks have run and all validation has passed.
|
|
35
|
+
*/
|
|
36
|
+
export interface HookPipelineResult {
|
|
37
|
+
resolvedData: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The transform+validate span, owning order + `resolvedData` threading.
|
|
41
|
+
*/
|
|
42
|
+
export interface HookPipeline {
|
|
43
|
+
run(args: HookPipelineArgs): Promise<HookPipelineResult>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The default Hook Pipeline instance used by the Write Pipeline.
|
|
47
|
+
*/
|
|
48
|
+
export declare const hookPipeline: HookPipeline;
|
|
49
|
+
//# sourceMappingURL=hook-pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-pipeline.d.ts","sourceRoot":"","sources":["../../src/context/hook-pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAUvD;;;;;;;;;;;;;;GAcG;AAEH;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAEhB,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,CAAA;IAC3B,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;IACzC,OAAO,EAAE,aAAa,CAAA;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACtC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;CACzD;AAkGD;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,YAE1B,CAAA"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { executeResolveInput, executeValidate, executeFieldResolveInputHooks, executeFieldValidateHooks, validateFieldRules, ValidationError, } from '../hooks/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run the transform+validate span once.
|
|
4
|
+
*
|
|
5
|
+
* Phase order (owned here, in one place):
|
|
6
|
+
* list `resolveInput`
|
|
7
|
+
* → field `resolveInput`
|
|
8
|
+
* → list `validate`
|
|
9
|
+
* → field `validate`
|
|
10
|
+
* → built-in field rules (`validateFieldRules`)
|
|
11
|
+
*
|
|
12
|
+
* Contract preserved exactly:
|
|
13
|
+
* - `resolvedData` starts as `inputData` and is threaded through each phase;
|
|
14
|
+
* - validate hooks report failures via `addValidationError` → THROW
|
|
15
|
+
* `ValidationError` (never silent);
|
|
16
|
+
* - built-in field rule failures THROW `ValidationError`;
|
|
17
|
+
* - on success returns the transformed `resolvedData`.
|
|
18
|
+
*/
|
|
19
|
+
async function runHookPipeline(args) {
|
|
20
|
+
const { operation, listName, listConfig, inputData, item, context } = args;
|
|
21
|
+
// ── Phase 1: list-level resolveInput ──────────────────────────────────────
|
|
22
|
+
let resolvedData = await executeResolveInput(listConfig.hooks, operation === 'create'
|
|
23
|
+
? {
|
|
24
|
+
listKey: listName,
|
|
25
|
+
operation: 'create',
|
|
26
|
+
inputData,
|
|
27
|
+
resolvedData: inputData,
|
|
28
|
+
item: undefined,
|
|
29
|
+
context,
|
|
30
|
+
}
|
|
31
|
+
: {
|
|
32
|
+
listKey: listName,
|
|
33
|
+
operation: 'update',
|
|
34
|
+
inputData,
|
|
35
|
+
resolvedData: inputData,
|
|
36
|
+
item,
|
|
37
|
+
context,
|
|
38
|
+
});
|
|
39
|
+
// ── Phase 1.5: field-level resolveInput (e.g. hash passwords) ──────────────
|
|
40
|
+
resolvedData = await executeFieldResolveInputHooks(inputData, resolvedData, listConfig.fields, operation, context, listName, item);
|
|
41
|
+
// ── Phase 2: list-level validate ──────────────────────────────────────────
|
|
42
|
+
await executeValidate(listConfig.hooks, operation === 'create'
|
|
43
|
+
? {
|
|
44
|
+
listKey: listName,
|
|
45
|
+
operation: 'create',
|
|
46
|
+
inputData,
|
|
47
|
+
resolvedData,
|
|
48
|
+
item: undefined,
|
|
49
|
+
context,
|
|
50
|
+
}
|
|
51
|
+
: {
|
|
52
|
+
listKey: listName,
|
|
53
|
+
operation: 'update',
|
|
54
|
+
inputData,
|
|
55
|
+
resolvedData,
|
|
56
|
+
item,
|
|
57
|
+
context,
|
|
58
|
+
});
|
|
59
|
+
// ── Phase 2.5: field-level validate ───────────────────────────────────────
|
|
60
|
+
await executeFieldValidateHooks(inputData, resolvedData, listConfig.fields, operation, context, listName, item);
|
|
61
|
+
// ── Phase 3: built-in field rules (isRequired, length, etc.) ──────────────
|
|
62
|
+
// Validation failures THROW (validation is not silent).
|
|
63
|
+
const validation = validateFieldRules(resolvedData, listConfig.fields, operation);
|
|
64
|
+
if (validation.errors.length > 0) {
|
|
65
|
+
throw new ValidationError(validation.errors, validation.fieldErrors);
|
|
66
|
+
}
|
|
67
|
+
return { resolvedData };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The default Hook Pipeline instance used by the Write Pipeline.
|
|
71
|
+
*/
|
|
72
|
+
export const hookPipeline = {
|
|
73
|
+
run: runHookPipeline,
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=hook-pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-pipeline.js","sourceRoot":"","sources":["../../src/context/hook-pipeline.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,6BAA6B,EAC7B,yBAAyB,EACzB,kBAAkB,EAClB,eAAe,GAChB,MAAM,mBAAmB,CAAA;AAiD1B;;;;;;;;;;;;;;;;GAgBG;AACH,KAAK,UAAU,eAAe,CAAC,IAAsB;IACnD,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;IAE1E,6EAA6E;IAC7E,IAAI,YAAY,GAAG,MAAM,mBAAmB,CAC1C,UAAU,CAAC,KAAK,EAChB,SAAS,KAAK,QAAQ;QACpB,CAAC,CAAC;YACE,OAAO,EAAE,QAAQ;YACjB,SAAS,EAAE,QAAQ;YACnB,SAAS;YACT,YAAY,EAAE,SAAS;YACvB,IAAI,EAAE,SAAS;YACf,OAAO;SACR;QACH,CAAC,CAAC;YACE,OAAO,EAAE,QAAQ;YACjB,SAAS,EAAE,QAAQ;YACnB,SAAS;YACT,YAAY,EAAE,SAAS;YACvB,IAAI;YACJ,OAAO;SACR,CACN,CAAA;IAED,8EAA8E;IAC9E,YAAY,GAAG,MAAM,6BAA6B,CAChD,SAAS,EACT,YAAY,EACZ,UAAU,CAAC,MAAM,EACjB,SAAS,EACT,OAAO,EACP,QAAQ,EACR,IAAI,CACL,CAAA;IAED,6EAA6E;IAC7E,MAAM,eAAe,CACnB,UAAU,CAAC,KAAK,EAChB,SAAS,KAAK,QAAQ;QACpB,CAAC,CAAC;YACE,OAAO,EAAE,QAAQ;YACjB,SAAS,EAAE,QAAQ;YACnB,SAAS;YACT,YAAY;YACZ,IAAI,EAAE,SAAS;YACf,OAAO;SACR;QACH,CAAC,CAAC;YACE,OAAO,EAAE,QAAQ;YACjB,SAAS,EAAE,QAAQ;YACnB,SAAS;YACT,YAAY;YACZ,IAAI;YACJ,OAAO;SACR,CACN,CAAA;IAED,6EAA6E;IAC7E,MAAM,yBAAyB,CAC7B,SAAS,EACT,YAAY,EACZ,UAAU,CAAC,MAAM,EACjB,SAAS,EACT,OAAO,EACP,QAAQ,EACR,IAAI,CACL,CAAA;IAED,6EAA6E;IAC7E,wDAAwD;IACxD,MAAM,UAAU,GAAG,kBAAkB,CAAC,YAAY,EAAE,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IACjF,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,eAAe,CAAC,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,CAAA;IACtE,CAAC;IAED,OAAO,EAAE,YAAY,EAAE,CAAA;AACzB,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC,GAAG,EAAE,eAAe;CACrB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/context/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,oBAAoB,CAAA;AACpE,OAAO,KAAK,EAAE,OAAO,EAAiB,kBAAkB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/context/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,oBAAoB,CAAA;AACpE,OAAO,KAAK,EAAE,OAAO,EAAiB,kBAAkB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AASlG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAS1D,MAAM,MAAM,iBAAiB,GACzB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GACpE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAChF;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAA;AA4GrD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,OAAO,SAAS,cAAc,EAC9B,OAAO,SAAS,gBAAgB,GAAG,gBAAgB,EAEnD,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,OAAO,GAAG,IAAI,EACvB,OAAO,CAAC,EAAE,YAAY,EACtB,OAAO,GAAE,OAAe,GACvB;IACD,EAAE,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAA;IAC/B,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,EAAE,YAAY,CAAA;IACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAChC,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5D,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,MAAM;QACV,EAAE,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAA;QAC/B,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;QACvB,MAAM,EAAE,OAAO,CAAA;QACf,OAAO,EAAE,YAAY,CAAA;QACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAChC,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;QAC5D,IAAI,EAAE,MAAM,OAAO,CAAA;QACnB,OAAO,EAAE,OAAO,CAAA;KACjB,CAAA;CACF,CAgMA"}
|
package/dist/context/index.js
CHANGED
|
@@ -1,211 +1,8 @@
|
|
|
1
|
-
import { checkAccess, mergeFilters, filterReadableFields,
|
|
2
|
-
import {
|
|
3
|
-
import { processNestedOperations } from './nested-operations.js';
|
|
1
|
+
import { checkAccess, mergeFilters, filterReadableFields, buildIncludeWithAccessControl, } from '../access/index.js';
|
|
2
|
+
import { ValidationError, DatabaseError } from '../hooks/index.js';
|
|
4
3
|
import { getDbKey } from '../lib/case-utils.js';
|
|
5
4
|
import { buildInclude, pickFields, isFragment } from '../query/index.js';
|
|
6
|
-
|
|
7
|
-
* Execute field-level resolveInput hooks
|
|
8
|
-
* Allows fields to transform their input values before database write
|
|
9
|
-
*/
|
|
10
|
-
async function executeFieldResolveInputHooks(
|
|
11
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
-
inputData,
|
|
13
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
-
resolvedData, fields, operation, context, listKey,
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
-
item) {
|
|
17
|
-
let result = { ...resolvedData };
|
|
18
|
-
console.log('Executing field resolveInput hooks for list:', listKey, 'operation:', operation, 'inputData:', inputData, 'resolvedData before field hooks:', resolvedData);
|
|
19
|
-
for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
|
|
20
|
-
// Skip if field not in data
|
|
21
|
-
if (!(fieldKey in result))
|
|
22
|
-
continue;
|
|
23
|
-
// Skip if no hooks defined
|
|
24
|
-
if (!fieldConfig.hooks?.resolveInput)
|
|
25
|
-
continue;
|
|
26
|
-
// Execute field hook
|
|
27
|
-
// Type assertion is safe here because hooks are typed correctly in field definitions
|
|
28
|
-
// and we're working with runtime values that match those types
|
|
29
|
-
const transformedValue = await fieldConfig.hooks.resolveInput({
|
|
30
|
-
listKey,
|
|
31
|
-
fieldKey,
|
|
32
|
-
operation,
|
|
33
|
-
inputData,
|
|
34
|
-
item,
|
|
35
|
-
resolvedData: { ...result }, // Pass a copy to avoid mutation affecting recorded args
|
|
36
|
-
context,
|
|
37
|
-
});
|
|
38
|
-
// Create new object with updated field to avoid mutating the passed reference
|
|
39
|
-
result = { ...result, [fieldKey]: transformedValue };
|
|
40
|
-
}
|
|
41
|
-
return result;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Execute field-level validate hooks
|
|
45
|
-
* Allows fields to perform custom validation after resolveInput but before database write
|
|
46
|
-
*/
|
|
47
|
-
async function executeFieldValidateHooks(
|
|
48
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
-
inputData,
|
|
50
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
-
resolvedData, fields, operation, context, listKey,
|
|
52
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
-
item) {
|
|
54
|
-
const errors = [];
|
|
55
|
-
const fieldErrors = {};
|
|
56
|
-
const addValidationError = (fieldKey) => (msg) => {
|
|
57
|
-
errors.push(msg);
|
|
58
|
-
fieldErrors[fieldKey] = msg;
|
|
59
|
-
};
|
|
60
|
-
for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
|
|
61
|
-
// Support both 'validate' (new) and 'validateInput' (deprecated) for backwards compatibility
|
|
62
|
-
const validateHook = fieldConfig.hooks?.validate ?? fieldConfig.hooks?.validateInput;
|
|
63
|
-
if (!validateHook)
|
|
64
|
-
continue;
|
|
65
|
-
// Execute field hook
|
|
66
|
-
// Type assertion is safe here because hooks are typed correctly in field definitions
|
|
67
|
-
if (operation === 'delete') {
|
|
68
|
-
await validateHook({
|
|
69
|
-
listKey,
|
|
70
|
-
fieldKey,
|
|
71
|
-
operation: 'delete',
|
|
72
|
-
item,
|
|
73
|
-
context,
|
|
74
|
-
addValidationError: addValidationError(fieldKey),
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
else if (operation === 'create') {
|
|
78
|
-
await validateHook({
|
|
79
|
-
listKey,
|
|
80
|
-
fieldKey,
|
|
81
|
-
operation: 'create',
|
|
82
|
-
inputData,
|
|
83
|
-
item: undefined,
|
|
84
|
-
resolvedData,
|
|
85
|
-
context,
|
|
86
|
-
addValidationError: addValidationError(fieldKey),
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
// operation === 'update'
|
|
91
|
-
await validateHook({
|
|
92
|
-
listKey,
|
|
93
|
-
fieldKey,
|
|
94
|
-
operation: 'update',
|
|
95
|
-
inputData,
|
|
96
|
-
item,
|
|
97
|
-
resolvedData,
|
|
98
|
-
context,
|
|
99
|
-
addValidationError: addValidationError(fieldKey),
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (errors.length > 0) {
|
|
104
|
-
throw new ValidationError(errors, fieldErrors);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Execute field-level beforeOperation hooks (side effects only)
|
|
109
|
-
* Allows fields to perform side effects before database write
|
|
110
|
-
*/
|
|
111
|
-
async function executeFieldBeforeOperationHooks(
|
|
112
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
-
inputData,
|
|
114
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
115
|
-
resolvedData, fields, operation, context, listKey,
|
|
116
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
117
|
-
item) {
|
|
118
|
-
for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
|
|
119
|
-
// Skip if no hooks defined
|
|
120
|
-
if (!fieldConfig.hooks?.beforeOperation)
|
|
121
|
-
continue;
|
|
122
|
-
// Skip if field not in data (for create/update)
|
|
123
|
-
if (operation !== 'delete' && !(fieldKey in resolvedData))
|
|
124
|
-
continue;
|
|
125
|
-
// Execute field hook (side effects only, no return value used)
|
|
126
|
-
// Type assertion is safe here because hooks are typed correctly in field definitions
|
|
127
|
-
if (operation === 'delete') {
|
|
128
|
-
await fieldConfig.hooks.beforeOperation({
|
|
129
|
-
listKey,
|
|
130
|
-
fieldKey,
|
|
131
|
-
operation: 'delete',
|
|
132
|
-
item,
|
|
133
|
-
context,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
else if (operation === 'create') {
|
|
137
|
-
await fieldConfig.hooks.beforeOperation({
|
|
138
|
-
listKey,
|
|
139
|
-
fieldKey,
|
|
140
|
-
operation: 'create',
|
|
141
|
-
inputData,
|
|
142
|
-
resolvedData,
|
|
143
|
-
context,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
// operation === 'update'
|
|
148
|
-
await fieldConfig.hooks.beforeOperation({
|
|
149
|
-
listKey,
|
|
150
|
-
fieldKey,
|
|
151
|
-
operation: 'update',
|
|
152
|
-
inputData,
|
|
153
|
-
item,
|
|
154
|
-
resolvedData,
|
|
155
|
-
context,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Execute field-level afterOperation hooks (side effects only)
|
|
162
|
-
* Allows fields to perform side effects after database operations
|
|
163
|
-
*/
|
|
164
|
-
async function executeFieldAfterOperationHooks(
|
|
165
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
|
-
item, inputData, resolvedData, fields, operation, context, listKey,
|
|
167
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
168
|
-
originalItem) {
|
|
169
|
-
for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
|
|
170
|
-
// Skip if no hooks defined
|
|
171
|
-
if (!fieldConfig.hooks?.afterOperation)
|
|
172
|
-
continue;
|
|
173
|
-
// Execute field hook (side effects only, no return value used)
|
|
174
|
-
if (operation === 'delete') {
|
|
175
|
-
await fieldConfig.hooks.afterOperation({
|
|
176
|
-
listKey,
|
|
177
|
-
fieldKey,
|
|
178
|
-
operation: 'delete',
|
|
179
|
-
originalItem,
|
|
180
|
-
context,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
else if (operation === 'create') {
|
|
184
|
-
await fieldConfig.hooks.afterOperation({
|
|
185
|
-
listKey,
|
|
186
|
-
fieldKey,
|
|
187
|
-
operation: 'create',
|
|
188
|
-
inputData,
|
|
189
|
-
item,
|
|
190
|
-
resolvedData,
|
|
191
|
-
context,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
// operation === 'update'
|
|
196
|
-
await fieldConfig.hooks.afterOperation({
|
|
197
|
-
listKey,
|
|
198
|
-
fieldKey,
|
|
199
|
-
operation: 'update',
|
|
200
|
-
inputData,
|
|
201
|
-
originalItem,
|
|
202
|
-
item,
|
|
203
|
-
resolvedData,
|
|
204
|
-
context,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
5
|
+
import { runWritePipeline, createWriteStrategy, updateWriteStrategy, deleteWriteStrategy, } from './write-pipeline.js';
|
|
209
6
|
/**
|
|
210
7
|
* Check if a list is configured as a singleton
|
|
211
8
|
*/
|
|
@@ -339,7 +136,7 @@ export function getContext(config, prisma, session, storage, _isSudo = false) {
|
|
|
339
136
|
findMany: findManyOp,
|
|
340
137
|
create: createOp,
|
|
341
138
|
update: updateOp,
|
|
342
|
-
delete: createDelete(listName, listConfig, prisma, context),
|
|
139
|
+
delete: createDelete(listName, listConfig, prisma, context, config),
|
|
343
140
|
count: createCount(listName, listConfig, prisma, context),
|
|
344
141
|
createMany: createCreateMany(listName, listConfig, prisma, context, config, createOp),
|
|
345
142
|
updateMany: createUpdateMany(listName, listConfig, prisma, context, config, findManyOp, updateOp),
|
|
@@ -591,100 +388,18 @@ listConfig, prisma, context, config) {
|
|
|
591
388
|
function createCreate(listName,
|
|
592
389
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
593
390
|
listConfig, prisma, context, config) {
|
|
391
|
+
// Thin adapter over the Write Pipeline: pick the create strategy, run the
|
|
392
|
+
// canonical secured write sequence, return its result.
|
|
594
393
|
return async (args) => {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const model = prisma[getDbKey(listName)];
|
|
600
|
-
const existingCount = await model.count();
|
|
601
|
-
if (existingCount > 0) {
|
|
602
|
-
throw new ValidationError([`Cannot create: ${listName} is a singleton list with an existing record`], {});
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
// 1. Check create access (skip if sudo mode)
|
|
606
|
-
if (!context._isSudo) {
|
|
607
|
-
const createAccess = listConfig.access?.operation?.create;
|
|
608
|
-
const accessResult = await checkAccess(createAccess, {
|
|
609
|
-
session: context.session,
|
|
610
|
-
context,
|
|
611
|
-
});
|
|
612
|
-
if (accessResult === false) {
|
|
613
|
-
return null;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
// 2. Execute list-level resolveInput hook
|
|
617
|
-
let resolvedData = await executeResolveInput(listConfig.hooks, {
|
|
618
|
-
listKey: listName,
|
|
619
|
-
operation: 'create',
|
|
620
|
-
inputData: args.data,
|
|
621
|
-
resolvedData: args.data,
|
|
622
|
-
item: undefined,
|
|
394
|
+
return runWritePipeline({
|
|
395
|
+
listName,
|
|
396
|
+
listConfig,
|
|
397
|
+
prisma,
|
|
623
398
|
context,
|
|
624
|
-
|
|
625
|
-
// 2.5. Execute field-level resolveInput hooks (e.g., hash passwords)
|
|
626
|
-
resolvedData = await executeFieldResolveInputHooks(args.data, resolvedData, listConfig.fields, 'create', context, listName);
|
|
627
|
-
// 3. Execute list-level validate hook
|
|
628
|
-
await executeValidate(listConfig.hooks, {
|
|
629
|
-
listKey: listName,
|
|
630
|
-
operation: 'create',
|
|
399
|
+
config,
|
|
631
400
|
inputData: args.data,
|
|
632
|
-
|
|
633
|
-
item: undefined,
|
|
634
|
-
context,
|
|
401
|
+
strategy: createWriteStrategy(listName, listConfig, context),
|
|
635
402
|
});
|
|
636
|
-
// 3.5. Execute field-level validate hooks
|
|
637
|
-
await executeFieldValidateHooks(args.data, resolvedData, listConfig.fields, 'create', context, listName);
|
|
638
|
-
// 4. Field validation (isRequired, length, etc.)
|
|
639
|
-
const validation = validateFieldRules(resolvedData, listConfig.fields, 'create');
|
|
640
|
-
if (validation.errors.length > 0) {
|
|
641
|
-
throw new ValidationError(validation.errors, validation.fieldErrors);
|
|
642
|
-
}
|
|
643
|
-
// 5. Filter writable fields (field-level access control, skip if sudo mode)
|
|
644
|
-
const filteredData = await filterWritableFields(resolvedData, listConfig.fields, 'create', {
|
|
645
|
-
session: context.session,
|
|
646
|
-
context: { ...context, _isSudo: context._isSudo },
|
|
647
|
-
inputData: args.data,
|
|
648
|
-
});
|
|
649
|
-
// 5.5. Process nested relationship operations
|
|
650
|
-
const data = await processNestedOperations(filteredData, listConfig.fields, config, { ...context, prisma }, 'create');
|
|
651
|
-
// 6. Execute field-level beforeOperation hooks (side effects only)
|
|
652
|
-
await executeFieldBeforeOperationHooks(args.data, resolvedData, listConfig.fields, 'create', context, listName);
|
|
653
|
-
// 7. Execute list-level beforeOperation hook
|
|
654
|
-
await executeBeforeOperation(listConfig.hooks, {
|
|
655
|
-
listKey: listName,
|
|
656
|
-
operation: 'create',
|
|
657
|
-
inputData: args.data,
|
|
658
|
-
resolvedData,
|
|
659
|
-
context,
|
|
660
|
-
});
|
|
661
|
-
// 8. Execute database create
|
|
662
|
-
// Access Prisma model dynamically - required because model names are generated at runtime
|
|
663
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
664
|
-
const model = prisma[getDbKey(listName)];
|
|
665
|
-
// Singleton lists use Int @id with value always 1 (matching Keystone 6 behaviour)
|
|
666
|
-
const createData = isSingletonList(listConfig) ? { id: 1, ...data } : data;
|
|
667
|
-
const item = await model.create({
|
|
668
|
-
data: createData,
|
|
669
|
-
});
|
|
670
|
-
// 9. Execute list-level afterOperation hook
|
|
671
|
-
await executeAfterOperation(listConfig.hooks, {
|
|
672
|
-
listKey: listName,
|
|
673
|
-
operation: 'create',
|
|
674
|
-
inputData: args.data,
|
|
675
|
-
item,
|
|
676
|
-
resolvedData,
|
|
677
|
-
context,
|
|
678
|
-
});
|
|
679
|
-
// 10. Execute field-level afterOperation hooks (side effects only)
|
|
680
|
-
await executeFieldAfterOperationHooks(item, args.data, resolvedData, listConfig.fields, 'create', context, listName, undefined);
|
|
681
|
-
// 11. Filter readable fields and apply resolveOutput hooks (including nested relationships)
|
|
682
|
-
// Pass sudo flag through context to skip field-level access checks
|
|
683
|
-
const filtered = await filterReadableFields(item, listConfig.fields, {
|
|
684
|
-
session: context.session,
|
|
685
|
-
context: { ...context, _isSudo: context._isSudo },
|
|
686
|
-
}, config, 0, listName);
|
|
687
|
-
return filtered;
|
|
688
403
|
};
|
|
689
404
|
}
|
|
690
405
|
/**
|
|
@@ -711,109 +426,18 @@ createFn) {
|
|
|
711
426
|
function createUpdate(listName,
|
|
712
427
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
713
428
|
listConfig, prisma, context, config) {
|
|
429
|
+
// Thin adapter over the Write Pipeline: pick the update strategy, run the
|
|
430
|
+
// canonical secured write sequence, return its result.
|
|
714
431
|
return async (args) => {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const item = await model.findUnique({
|
|
720
|
-
where: args.where,
|
|
721
|
-
});
|
|
722
|
-
if (!item) {
|
|
723
|
-
return null;
|
|
724
|
-
}
|
|
725
|
-
// 2. Check update access (skip if sudo mode)
|
|
726
|
-
if (!context._isSudo) {
|
|
727
|
-
const updateAccess = listConfig.access?.operation?.update;
|
|
728
|
-
const accessResult = await checkAccess(updateAccess, {
|
|
729
|
-
session: context.session,
|
|
730
|
-
item,
|
|
731
|
-
context,
|
|
732
|
-
});
|
|
733
|
-
if (accessResult === false) {
|
|
734
|
-
return null;
|
|
735
|
-
}
|
|
736
|
-
// If access returns a filter, check if item matches
|
|
737
|
-
if (typeof accessResult === 'object') {
|
|
738
|
-
const matchesFilter = await model.findFirst({
|
|
739
|
-
where: mergeFilters(args.where, accessResult),
|
|
740
|
-
});
|
|
741
|
-
if (!matchesFilter) {
|
|
742
|
-
return null;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
// 3. Execute list-level resolveInput hook
|
|
747
|
-
let resolvedData = await executeResolveInput(listConfig.hooks, {
|
|
748
|
-
listKey: listName,
|
|
749
|
-
operation: 'update',
|
|
750
|
-
inputData: args.data,
|
|
751
|
-
resolvedData: args.data,
|
|
752
|
-
item,
|
|
432
|
+
return runWritePipeline({
|
|
433
|
+
listName,
|
|
434
|
+
listConfig,
|
|
435
|
+
prisma,
|
|
753
436
|
context,
|
|
754
|
-
|
|
755
|
-
// 3.5. Execute field-level resolveInput hooks (e.g., hash passwords)
|
|
756
|
-
resolvedData = await executeFieldResolveInputHooks(args.data, resolvedData, listConfig.fields, 'update', context, listName, item);
|
|
757
|
-
// 4. Execute list-level validate hook
|
|
758
|
-
await executeValidate(listConfig.hooks, {
|
|
759
|
-
listKey: listName,
|
|
760
|
-
operation: 'update',
|
|
437
|
+
config,
|
|
761
438
|
inputData: args.data,
|
|
762
|
-
|
|
763
|
-
item,
|
|
764
|
-
context,
|
|
439
|
+
strategy: updateWriteStrategy(listConfig, context, args.where),
|
|
765
440
|
});
|
|
766
|
-
// 4.5. Execute field-level validate hooks
|
|
767
|
-
await executeFieldValidateHooks(args.data, resolvedData, listConfig.fields, 'update', context, listName, item);
|
|
768
|
-
// 5. Field validation (isRequired, length, etc.)
|
|
769
|
-
const validation = validateFieldRules(resolvedData, listConfig.fields, 'update');
|
|
770
|
-
if (validation.errors.length > 0) {
|
|
771
|
-
throw new ValidationError(validation.errors, validation.fieldErrors);
|
|
772
|
-
}
|
|
773
|
-
// 6. Filter writable fields (field-level access control, skip if sudo mode)
|
|
774
|
-
const filteredData = await filterWritableFields(resolvedData, listConfig.fields, 'update', {
|
|
775
|
-
session: context.session,
|
|
776
|
-
item,
|
|
777
|
-
context: { ...context, _isSudo: context._isSudo },
|
|
778
|
-
inputData: args.data,
|
|
779
|
-
});
|
|
780
|
-
// 6.5. Process nested relationship operations
|
|
781
|
-
const data = await processNestedOperations(filteredData, listConfig.fields, config, { ...context, prisma }, 'update');
|
|
782
|
-
// 7. Execute field-level beforeOperation hooks (side effects only)
|
|
783
|
-
await executeFieldBeforeOperationHooks(args.data, resolvedData, listConfig.fields, 'update', context, listName, item);
|
|
784
|
-
// 8. Execute list-level beforeOperation hook
|
|
785
|
-
await executeBeforeOperation(listConfig.hooks, {
|
|
786
|
-
listKey: listName,
|
|
787
|
-
operation: 'update',
|
|
788
|
-
inputData: args.data,
|
|
789
|
-
item,
|
|
790
|
-
resolvedData,
|
|
791
|
-
context,
|
|
792
|
-
});
|
|
793
|
-
// 9. Execute database update
|
|
794
|
-
const updated = await model.update({
|
|
795
|
-
where: args.where,
|
|
796
|
-
data,
|
|
797
|
-
});
|
|
798
|
-
// 10. Execute list-level afterOperation hook
|
|
799
|
-
await executeAfterOperation(listConfig.hooks, {
|
|
800
|
-
listKey: listName,
|
|
801
|
-
operation: 'update',
|
|
802
|
-
inputData: args.data,
|
|
803
|
-
originalItem: item, // item is the original item before the update
|
|
804
|
-
item: updated,
|
|
805
|
-
resolvedData,
|
|
806
|
-
context,
|
|
807
|
-
});
|
|
808
|
-
// 11. Execute field-level afterOperation hooks (side effects only)
|
|
809
|
-
await executeFieldAfterOperationHooks(updated, args.data, resolvedData, listConfig.fields, 'update', context, listName, item);
|
|
810
|
-
// 12. Filter readable fields and apply resolveOutput hooks (including nested relationships)
|
|
811
|
-
// Pass sudo flag through context to skip field-level access checks
|
|
812
|
-
const filtered = await filterReadableFields(updated, listConfig.fields, {
|
|
813
|
-
session: context.session,
|
|
814
|
-
context: { ...context, _isSudo: context._isSudo },
|
|
815
|
-
}, config, 0, listName);
|
|
816
|
-
return filtered;
|
|
817
441
|
};
|
|
818
442
|
}
|
|
819
443
|
/**
|
|
@@ -844,75 +468,19 @@ updateFn) {
|
|
|
844
468
|
*/
|
|
845
469
|
function createDelete(listName,
|
|
846
470
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
847
|
-
listConfig, prisma, context) {
|
|
471
|
+
listConfig, prisma, context, config) {
|
|
472
|
+
// Thin adapter over the Write Pipeline: pick the delete strategy, run the
|
|
473
|
+
// canonical secured write sequence, return its result.
|
|
848
474
|
return async (args) => {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
// 1. Fetch the item to pass to access control and hooks
|
|
854
|
-
// Access Prisma model dynamically - required because model names are generated at runtime
|
|
855
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
856
|
-
const model = prisma[getDbKey(listName)];
|
|
857
|
-
const item = await model.findUnique({
|
|
858
|
-
where: args.where,
|
|
859
|
-
});
|
|
860
|
-
if (!item) {
|
|
861
|
-
return null;
|
|
862
|
-
}
|
|
863
|
-
// 2. Check delete access (skip if sudo mode)
|
|
864
|
-
if (!context._isSudo) {
|
|
865
|
-
const deleteAccess = listConfig.access?.operation?.delete;
|
|
866
|
-
const accessResult = await checkAccess(deleteAccess, {
|
|
867
|
-
session: context.session,
|
|
868
|
-
item,
|
|
869
|
-
context,
|
|
870
|
-
});
|
|
871
|
-
if (accessResult === false) {
|
|
872
|
-
return null;
|
|
873
|
-
}
|
|
874
|
-
// If access returns a filter, check if item matches
|
|
875
|
-
if (typeof accessResult === 'object') {
|
|
876
|
-
const matchesFilter = await model.findFirst({
|
|
877
|
-
where: mergeFilters(args.where, accessResult),
|
|
878
|
-
});
|
|
879
|
-
if (!matchesFilter) {
|
|
880
|
-
return null;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
// 3. Execute list-level validate hook
|
|
885
|
-
await executeValidate(listConfig.hooks, {
|
|
886
|
-
listKey: listName,
|
|
887
|
-
operation: 'delete',
|
|
888
|
-
item,
|
|
889
|
-
context,
|
|
890
|
-
});
|
|
891
|
-
// 3.5. Execute field-level validate hooks
|
|
892
|
-
await executeFieldValidateHooks(undefined, undefined, listConfig.fields, 'delete', context, listName, item);
|
|
893
|
-
// 4. Execute field-level beforeOperation hooks (side effects only)
|
|
894
|
-
await executeFieldBeforeOperationHooks({}, {}, listConfig.fields, 'delete', context, listName, item);
|
|
895
|
-
// 5. Execute list-level beforeOperation hook
|
|
896
|
-
await executeBeforeOperation(listConfig.hooks, {
|
|
897
|
-
listKey: listName,
|
|
898
|
-
operation: 'delete',
|
|
899
|
-
item,
|
|
900
|
-
context,
|
|
901
|
-
});
|
|
902
|
-
// 6. Execute database delete
|
|
903
|
-
const deleted = await model.delete({
|
|
904
|
-
where: args.where,
|
|
905
|
-
});
|
|
906
|
-
// 7. Execute list-level afterOperation hook
|
|
907
|
-
await executeAfterOperation(listConfig.hooks, {
|
|
908
|
-
listKey: listName,
|
|
909
|
-
operation: 'delete',
|
|
910
|
-
originalItem: item, // item is the original item before deletion
|
|
475
|
+
return runWritePipeline({
|
|
476
|
+
listName,
|
|
477
|
+
listConfig,
|
|
478
|
+
prisma,
|
|
911
479
|
context,
|
|
480
|
+
config,
|
|
481
|
+
inputData: undefined,
|
|
482
|
+
strategy: deleteWriteStrategy(listName, listConfig, context, args.where),
|
|
912
483
|
});
|
|
913
|
-
// 8. Execute field-level afterOperation hooks (side effects only)
|
|
914
|
-
await executeFieldAfterOperationHooks(deleted, undefined, undefined, listConfig.fields, 'delete', context, listName, item);
|
|
915
|
-
return deleted;
|
|
916
484
|
};
|
|
917
485
|
}
|
|
918
486
|
/**
|