@opensaas/stack-core 0.20.1 → 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.
Files changed (105) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +72 -0
  3. package/CLAUDE.md +18 -2
  4. package/dist/access/access-filter.d.ts +29 -0
  5. package/dist/access/access-filter.d.ts.map +1 -0
  6. package/dist/access/access-filter.js +68 -0
  7. package/dist/access/access-filter.js.map +1 -0
  8. package/dist/access/engine.d.ts +15 -48
  9. package/dist/access/engine.d.ts.map +1 -1
  10. package/dist/access/engine.js +14 -280
  11. package/dist/access/engine.js.map +1 -1
  12. package/dist/access/field-access.d.ts +44 -0
  13. package/dist/access/field-access.d.ts.map +1 -0
  14. package/dist/access/field-access.js +123 -0
  15. package/dist/access/field-access.js.map +1 -0
  16. package/dist/access/field-access.test.d.ts +2 -0
  17. package/dist/access/field-access.test.d.ts.map +1 -0
  18. package/dist/access/{engine.test.js → field-access.test.js} +2 -2
  19. package/dist/access/field-access.test.js.map +1 -0
  20. package/dist/access/field-visibility.d.ts +13 -0
  21. package/dist/access/field-visibility.d.ts.map +1 -0
  22. package/dist/access/field-visibility.js +155 -0
  23. package/dist/access/field-visibility.js.map +1 -0
  24. package/dist/access/index.d.ts +4 -1
  25. package/dist/access/index.d.ts.map +1 -1
  26. package/dist/access/index.js +8 -1
  27. package/dist/access/index.js.map +1 -1
  28. package/dist/config/index.d.ts +1 -1
  29. package/dist/config/index.d.ts.map +1 -1
  30. package/dist/config/types.d.ts +45 -4
  31. package/dist/config/types.d.ts.map +1 -1
  32. package/dist/context/hook-pipeline.d.ts +49 -0
  33. package/dist/context/hook-pipeline.d.ts.map +1 -0
  34. package/dist/context/hook-pipeline.js +75 -0
  35. package/dist/context/hook-pipeline.js.map +1 -0
  36. package/dist/context/index.d.ts.map +1 -1
  37. package/dist/context/index.js +30 -462
  38. package/dist/context/index.js.map +1 -1
  39. package/dist/context/nested-operations.d.ts.map +1 -1
  40. package/dist/context/nested-operations.js +72 -68
  41. package/dist/context/nested-operations.js.map +1 -1
  42. package/dist/context/write-pipeline.d.ts +158 -0
  43. package/dist/context/write-pipeline.d.ts.map +1 -0
  44. package/dist/context/write-pipeline.js +306 -0
  45. package/dist/context/write-pipeline.js.map +1 -0
  46. package/dist/extend.d.ts +3 -0
  47. package/dist/extend.d.ts.map +1 -0
  48. package/dist/extend.js +10 -0
  49. package/dist/extend.js.map +1 -0
  50. package/dist/fields/index.d.ts +1 -0
  51. package/dist/fields/index.d.ts.map +1 -1
  52. package/dist/fields/index.js +213 -2
  53. package/dist/fields/index.js.map +1 -1
  54. package/dist/hooks/index.d.ts +20 -0
  55. package/dist/hooks/index.d.ts.map +1 -1
  56. package/dist/hooks/index.js +202 -0
  57. package/dist/hooks/index.js.map +1 -1
  58. package/dist/index.d.ts +5 -9
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +19 -10
  61. package/dist/index.js.map +1 -1
  62. package/dist/internal.d.ts +8 -0
  63. package/dist/internal.d.ts.map +1 -0
  64. package/dist/internal.js +16 -0
  65. package/dist/internal.js.map +1 -0
  66. package/dist/validation/field-config.d.ts +55 -0
  67. package/dist/validation/field-config.d.ts.map +1 -0
  68. package/dist/validation/field-config.js +100 -0
  69. package/dist/validation/field-config.js.map +1 -0
  70. package/dist/validation/field-config.test.d.ts +2 -0
  71. package/dist/validation/field-config.test.d.ts.map +1 -0
  72. package/dist/validation/field-config.test.js +159 -0
  73. package/dist/validation/field-config.test.js.map +1 -0
  74. package/package.json +11 -3
  75. package/src/access/access-filter.ts +97 -0
  76. package/src/access/engine.ts +13 -396
  77. package/src/access/{engine.test.ts → field-access.test.ts} +1 -1
  78. package/src/access/field-access.ts +159 -0
  79. package/src/access/field-visibility.ts +247 -0
  80. package/src/access/index.ts +7 -4
  81. package/src/config/index.ts +1 -0
  82. package/src/config/types.ts +51 -4
  83. package/src/context/hook-pipeline.ts +160 -0
  84. package/src/context/index.ts +29 -667
  85. package/src/context/nested-operations.ts +142 -111
  86. package/src/context/write-pipeline.ts +543 -0
  87. package/src/extend.ts +14 -0
  88. package/src/fields/index.ts +310 -2
  89. package/src/hooks/index.ts +227 -0
  90. package/src/index.ts +27 -90
  91. package/src/internal.ts +49 -0
  92. package/src/validation/field-config.test.ts +199 -0
  93. package/src/validation/field-config.ts +145 -0
  94. package/tests/access-relationships.test.ts +4 -4
  95. package/tests/access.test.ts +1 -1
  96. package/tests/field-hooks.test.ts +410 -0
  97. package/tests/field-types.test.ts +1 -1
  98. package/tests/hook-pipeline.test.ts +233 -0
  99. package/tests/nested-operation-registry.test.ts +206 -0
  100. package/tests/write-pipeline.test.ts +588 -0
  101. package/tsconfig.tsbuildinfo +1 -1
  102. package/vitest.config.ts +43 -1
  103. package/dist/access/engine.test.d.ts +0 -2
  104. package/dist/access/engine.test.d.ts.map +0 -1
  105. package/dist/access/engine.test.js.map +0 -1
@@ -0,0 +1,306 @@
1
+ import { checkAccess, mergeFilters, filterReadableFields, filterWritableFields, } from '../access/index.js';
2
+ import { executeValidate, executeBeforeOperation, executeAfterOperation, executeFieldValidateHooks, executeFieldBeforeOperationHooks, executeFieldAfterOperationHooks, ValidationError, } from '../hooks/index.js';
3
+ import { hookPipeline } from './hook-pipeline.js';
4
+ import { processNestedOperations } from './nested-operations.js';
5
+ import { getDbKey } from '../lib/case-utils.js';
6
+ /**
7
+ * Resolve the dynamic Prisma model for a list. Model names are generated at
8
+ * runtime from list keys, which is the one place a cast is unavoidable — it is
9
+ * kept localized here (mirroring the existing pattern in `context/index.ts`).
10
+ */
11
+ function getModel(prisma, listName) {
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- model names are generated at runtime
13
+ return prisma[getDbKey(listName)];
14
+ }
15
+ /**
16
+ * Check if a list is configured as a singleton.
17
+ */
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
19
+ function isSingletonList(listConfig) {
20
+ return !!listConfig.isSingleton;
21
+ }
22
+ /**
23
+ * Run the canonical secured write sequence once.
24
+ *
25
+ * Phase order (owned here, in one place):
26
+ * resolve target + operation-level access
27
+ * → list/field `resolveInput`
28
+ * → list/field `validate`
29
+ * → built-in field rules (`validateFieldRules`)
30
+ * → filter writable fields
31
+ * → nested operations
32
+ * → list/field `beforeOperation`
33
+ * → DB
34
+ * → list/field `afterOperation`
35
+ * → `filterReadableFields` (Field Visibility)
36
+ *
37
+ * Contract preserved exactly:
38
+ * - missing target / access denied / filter non-match → `null` (silent),
39
+ * BEFORE the DB call and BEFORE `beforeOperation`.
40
+ * - validation failure → THROW `ValidationError` (never silent).
41
+ * - sudo mode skips access checks and writable-field filtering (the strategy
42
+ * and `filterWritableFields` both honour `context._isSudo`).
43
+ * - `afterOperation` receives `originalItem` for update/delete (undefined for
44
+ * create).
45
+ * - delete returns the deleted row as-is (no Field Visibility pass), matching
46
+ * current behaviour.
47
+ */
48
+ export async function runWritePipeline(args) {
49
+ const { listName, listConfig, prisma, context, config, inputData, strategy } = args;
50
+ const { operation } = strategy;
51
+ const model = getModel(prisma, listName);
52
+ // ── Phase 1: resolve target + operation-level access ──────────────────────
53
+ // Short-circuits to `null` (silent failure) for missing target, denied
54
+ // access, or filter non-match — before any hook side effects or the DB call.
55
+ const resolution = await strategy.resolveTarget(model);
56
+ if (resolution.status === 'denied') {
57
+ return null;
58
+ }
59
+ const originalItem = resolution.originalItem;
60
+ // ── Delete path: skip input phases, run only validate/field-validate ────────
61
+ // (matches current delete behaviour exactly).
62
+ if (!strategy.runInputPhases) {
63
+ return runDeletePath({ listName, listConfig, context, originalItem, model, strategy });
64
+ }
65
+ // Only create/update reach here (delete short-circuited above). Narrow the
66
+ // operation so the field-hook helpers receive a 'create' | 'update' value.
67
+ const writeOp = operation === 'create' ? 'create' : 'update';
68
+ // `inputData` is always present for create/update (the operations that run
69
+ // input phases). Default to {} only as a defensive measure.
70
+ const input = inputData ?? {};
71
+ // ── Phases 2–4: transform + validate span (Hook Pipeline) ──────────────────
72
+ // The Hook Pipeline owns the list/field `resolveInput` → list/field `validate`
73
+ // → built-in field rules span and the `resolvedData` threading through it. It
74
+ // THROWS `ValidationError` on any validation failure (never silent).
75
+ const { resolvedData } = await hookPipeline.run({
76
+ operation: writeOp,
77
+ listName,
78
+ listConfig,
79
+ inputData: input,
80
+ item: originalItem,
81
+ context,
82
+ });
83
+ // ── Phase 5: filter writable fields (field-level access, skip if sudo) ──────
84
+ const filteredData = await filterWritableFields(resolvedData, listConfig.fields, writeOp, {
85
+ session: context.session,
86
+ item: originalItem,
87
+ context: { ...context, _isSudo: context._isSudo },
88
+ inputData: input,
89
+ });
90
+ // ── Phase 5.5: process nested relationship operations ───────────────────────
91
+ const data = await processNestedOperations(filteredData, listConfig.fields, config, { ...context, prisma }, writeOp);
92
+ // ── Phase 6: field-level beforeOperation (side effects only) ────────────────
93
+ await executeFieldBeforeOperationHooks(input, resolvedData, listConfig.fields, writeOp, context, listName, originalItem);
94
+ // ── Phase 7: list-level beforeOperation ─────────────────────────────────────
95
+ await executeBeforeOperation(listConfig.hooks, operation === 'create'
96
+ ? {
97
+ listKey: listName,
98
+ operation: 'create',
99
+ inputData: input,
100
+ resolvedData,
101
+ context,
102
+ }
103
+ : {
104
+ listKey: listName,
105
+ operation: 'update',
106
+ inputData: input,
107
+ item: originalItem,
108
+ resolvedData,
109
+ context,
110
+ });
111
+ // ── Phase 8: DB write ───────────────────────────────────────────────────────
112
+ const item = await strategy.persist(model, data);
113
+ // ── Phase 9: list-level afterOperation ──────────────────────────────────────
114
+ await executeAfterOperation(listConfig.hooks, operation === 'create'
115
+ ? {
116
+ listKey: listName,
117
+ operation: 'create',
118
+ inputData: input,
119
+ item,
120
+ resolvedData,
121
+ context,
122
+ }
123
+ : {
124
+ listKey: listName,
125
+ operation: 'update',
126
+ inputData: input,
127
+ // originalItem is the row before the update
128
+ originalItem: originalItem,
129
+ item,
130
+ resolvedData,
131
+ context,
132
+ });
133
+ // ── Phase 10: field-level afterOperation (side effects only) ────────────────
134
+ await executeFieldAfterOperationHooks(item, input, resolvedData, listConfig.fields, writeOp, context, listName, originalItem);
135
+ // ── Phase 11: Field Visibility (filter readable fields + resolveOutput) ─────
136
+ return filterReadableFields(item, listConfig.fields, {
137
+ session: context.session,
138
+ context: { ...context, _isSudo: context._isSudo },
139
+ }, config, 0, listName);
140
+ }
141
+ /**
142
+ * The delete tail of the pipeline: skips the input-shaping phases and runs only
143
+ * validate/field-validate before the DB delete, then the after-hooks. Returns
144
+ * the deleted row as-is (no Field Visibility pass) — matching current delete
145
+ * behaviour exactly.
146
+ */
147
+ async function runDeletePath(args) {
148
+ const { listName, listConfig, context, originalItem, model, strategy } = args;
149
+ const item = originalItem;
150
+ // ── Phase 3: list-level validate (delete) ──────────────────────────────────
151
+ await executeValidate(listConfig.hooks, {
152
+ listKey: listName,
153
+ operation: 'delete',
154
+ item,
155
+ context,
156
+ });
157
+ // ── Phase 3.5: field-level validate (delete) ────────────────────────────────
158
+ await executeFieldValidateHooks(undefined, undefined, listConfig.fields, 'delete', context, listName, item);
159
+ // ── Phase 6: field-level beforeOperation (delete) ───────────────────────────
160
+ await executeFieldBeforeOperationHooks({}, {}, listConfig.fields, 'delete', context, listName, item);
161
+ // ── Phase 7: list-level beforeOperation (delete) ────────────────────────────
162
+ await executeBeforeOperation(listConfig.hooks, {
163
+ listKey: listName,
164
+ operation: 'delete',
165
+ item,
166
+ context,
167
+ });
168
+ // ── Phase 8: DB delete ──────────────────────────────────────────────────────
169
+ const deleted = await strategy.persist(model, {});
170
+ // ── Phase 9: list-level afterOperation (delete) ─────────────────────────────
171
+ await executeAfterOperation(listConfig.hooks, {
172
+ listKey: listName,
173
+ operation: 'delete',
174
+ originalItem: item,
175
+ context,
176
+ });
177
+ // ── Phase 10: field-level afterOperation (delete) ───────────────────────────
178
+ await executeFieldAfterOperationHooks(deleted, undefined, undefined, listConfig.fields, 'delete', context, listName, item);
179
+ return deleted;
180
+ }
181
+ // ── Per-operation strategies ──────────────────────────────────────────────────
182
+ /**
183
+ * Create strategy.
184
+ *
185
+ * Axis 1: checks `create` access with NO existing row. Enforces the
186
+ * singleton-create constraint even under sudo. On create, an access result of
187
+ * `true` OR a filter object both proceed — there is no filter re-check.
188
+ * Axis 2: runs all input phases.
189
+ * Axis 3: `model.create({ data })`, prepending `id: 1` for singleton lists.
190
+ */
191
+ export function createWriteStrategy(listName,
192
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
193
+ listConfig, context) {
194
+ const singleton = isSingletonList(listConfig);
195
+ return {
196
+ operation: 'create',
197
+ runInputPhases: true,
198
+ async resolveTarget(model) {
199
+ // Singleton constraint is enforced even under sudo.
200
+ if (singleton) {
201
+ const existingCount = await model.count();
202
+ if (existingCount > 0) {
203
+ throw new ValidationError([`Cannot create: ${listName} is a singleton list with an existing record`], {});
204
+ }
205
+ }
206
+ if (!context._isSudo) {
207
+ const accessResult = await checkAccess(listConfig.access?.operation?.create, {
208
+ session: context.session,
209
+ context,
210
+ });
211
+ if (accessResult === false) {
212
+ return { status: 'denied' };
213
+ }
214
+ }
215
+ return { status: 'ok', originalItem: undefined };
216
+ },
217
+ async persist(model, data) {
218
+ // Singleton lists use Int @id with value always 1 (matching Keystone 6).
219
+ const createData = singleton ? { id: 1, ...data } : data;
220
+ return model.create({ data: createData });
221
+ },
222
+ };
223
+ }
224
+ /**
225
+ * Build the shared target resolution for update/delete: fetch the row (missing
226
+ * → denied), check operation-level access (false → denied), and if access
227
+ * returns a filter, re-check via `findFirst(mergeFilters(where, filter))`
228
+ * (no match → denied). An access result of `true` proceeds with no re-check.
229
+ */
230
+ function resolveExistingTarget(
231
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
232
+ listConfig, context, where, access) {
233
+ return async (model) => {
234
+ const item = await model.findUnique({ where });
235
+ if (!item) {
236
+ return { status: 'denied' };
237
+ }
238
+ if (!context._isSudo) {
239
+ const accessResult = await checkAccess(listConfig.access?.operation?.[access], {
240
+ session: context.session,
241
+ item,
242
+ context,
243
+ });
244
+ if (accessResult === false) {
245
+ return { status: 'denied' };
246
+ }
247
+ // A filter result must additionally match the target row.
248
+ if (typeof accessResult === 'object') {
249
+ const matchesFilter = await model.findFirst({
250
+ where: mergeFilters(where, accessResult) ?? {},
251
+ });
252
+ if (!matchesFilter) {
253
+ return { status: 'denied' };
254
+ }
255
+ }
256
+ }
257
+ return { status: 'ok', originalItem: item };
258
+ };
259
+ }
260
+ /**
261
+ * Update strategy.
262
+ *
263
+ * Axis 1: fetch row, check `update` access, re-check filter results.
264
+ * Axis 2: runs all input phases.
265
+ * Axis 3: `model.update({ where, data })`; afterOperation gets `originalItem`.
266
+ */
267
+ export function updateWriteStrategy(
268
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
269
+ listConfig, context, where) {
270
+ return {
271
+ operation: 'update',
272
+ runInputPhases: true,
273
+ resolveTarget: resolveExistingTarget(listConfig, context, where, 'update'),
274
+ async persist(model, data) {
275
+ return model.update({ where, data });
276
+ },
277
+ };
278
+ }
279
+ /**
280
+ * Delete strategy.
281
+ *
282
+ * Axis 1: enforce singleton constraint (even under sudo), fetch row, check
283
+ * `delete` access, re-check filter results.
284
+ * Axis 2: SKIPS input phases (runs only validate/field-validate).
285
+ * Axis 3: `model.delete({ where })`; afterOperation gets `originalItem`.
286
+ */
287
+ export function deleteWriteStrategy(listName,
288
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
289
+ listConfig, context, where) {
290
+ const resolveTarget = resolveExistingTarget(listConfig, context, where, 'delete');
291
+ return {
292
+ operation: 'delete',
293
+ runInputPhases: false,
294
+ async resolveTarget(model) {
295
+ // Singleton lists may not be deleted (enforced even under sudo).
296
+ if (isSingletonList(listConfig)) {
297
+ throw new ValidationError([`Cannot delete: ${listName} is a singleton list`], {});
298
+ }
299
+ return resolveTarget(model);
300
+ },
301
+ async persist(model) {
302
+ return model.delete({ where });
303
+ },
304
+ };
305
+ }
306
+ //# sourceMappingURL=write-pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-pipeline.js","sourceRoot":"","sources":["../../src/context/write-pipeline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export type { Plugin, PluginContext, GeneratedFiles } from './config/index.js';
2
+ export type { BaseFieldConfig, TypeInfo, TypeDescriptor } from './config/index.js';
3
+ //# sourceMappingURL=extend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extend.d.ts","sourceRoot":"","sources":["../src/extend.ts"],"names":[],"mappings":"AAUA,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAG9E,YAAY,EAAE,eAAe,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA"}
package/dist/extend.js ADDED
@@ -0,0 +1,10 @@
1
+ // ───────────────────────────────────────────────────────────────
2
+ // @opensaas/stack-core/extend
3
+ //
4
+ // Authoring contracts for extending the stack: implement these when
5
+ // you build a plugin or a third-party field package. Stable, public
6
+ // API — distinct from the everyday consumer surface on the root entry
7
+ // point and from the unstable plumbing on `/internal`.
8
+ // ───────────────────────────────────────────────────────────────
9
+ export {};
10
+ //# sourceMappingURL=extend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extend.js","sourceRoot":"","sources":["../src/extend.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,8BAA8B;AAC9B,EAAE;AACF,oEAAoE;AACpE,oEAAoE;AACpE,sEAAsE;AACtE,uDAAuD;AACvD,kEAAkE"}
@@ -1,4 +1,5 @@
1
1
  import type { TextField, IntegerField, DecimalField, CheckboxField, TimestampField, CalendarDayField, PasswordField, SelectField, RelationshipField, JsonField, VirtualField } from '../config/types.js';
2
+ export type { TextField, IntegerField, DecimalField, CheckboxField, TimestampField, CalendarDayField, PasswordField, SelectField, RelationshipField, JsonField, VirtualField, PrismaRelationResult, } from '../config/types.js';
2
3
  /**
3
4
  * Text field
4
5
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fields/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fields/index.ts"],"names":[],"mappings":"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"}
@@ -541,7 +541,7 @@ export function password(options) {
541
541
  type: 'password',
542
542
  ...options,
543
543
  resultExtension: {
544
- outputType: "import('@opensaas/stack-core').HashedPassword",
544
+ outputType: "import('@opensaas/stack-core/internal').HashedPassword",
545
545
  // No compute - delegates to resolveOutput hook
546
546
  },
547
547
  ui: {
@@ -733,6 +733,215 @@ export function select(options) {
733
733
  },
734
734
  };
735
735
  }
736
+ /**
737
+ * Parse a relationship ref into its target list and optional target field.
738
+ * Supports both 'ListName.fieldName' (bidirectional) and 'ListName' (list-only) formats.
739
+ */
740
+ function parseRelationshipRef(ref) {
741
+ const parts = ref.split('.');
742
+ if (parts.length === 1) {
743
+ const list = parts[0];
744
+ if (!list) {
745
+ throw new Error(`Invalid relationship ref: ${ref}`);
746
+ }
747
+ return { list };
748
+ }
749
+ else if (parts.length === 2) {
750
+ const [list, field] = parts;
751
+ if (!list || !field) {
752
+ throw new Error(`Invalid relationship ref: ${ref}`);
753
+ }
754
+ return { list, field };
755
+ }
756
+ else {
757
+ throw new Error(`Invalid relationship ref: ${ref}`);
758
+ }
759
+ }
760
+ /**
761
+ * Check if a relationship is one-to-one (bidirectional with both sides having many: false).
762
+ */
763
+ function isOneToOneRelationship(fieldName, field, config) {
764
+ const { list: targetList, field: targetField } = parseRelationshipRef(field.ref);
765
+ if (!targetField) {
766
+ return false;
767
+ }
768
+ if (field.many) {
769
+ return false;
770
+ }
771
+ const targetListConfig = config.lists[targetList];
772
+ if (!targetListConfig) {
773
+ throw new Error(`Referenced list "${targetList}" not found in config`);
774
+ }
775
+ const targetFieldConfig = targetListConfig.fields[targetField];
776
+ if (!targetFieldConfig) {
777
+ throw new Error(`Referenced field "${targetList}.${targetField}" not found. If you want a one-sided relationship, use ref: "${targetList}" instead of ref: "${targetList}.${targetField}"`);
778
+ }
779
+ if (targetFieldConfig.type !== 'relationship') {
780
+ throw new Error(`Referenced field "${targetList}.${targetField}" is not a relationship field`);
781
+ }
782
+ return !targetFieldConfig.many;
783
+ }
784
+ /**
785
+ * Determine if this side of a relationship should store the foreign key.
786
+ * For one-to-one relationships, only one side stores the foreign key.
787
+ */
788
+ function shouldHaveForeignKey(listKey, fieldName, field, config) {
789
+ const { list: targetList, field: targetField } = parseRelationshipRef(field.ref);
790
+ if (!targetField) {
791
+ return true;
792
+ }
793
+ if (field.many) {
794
+ return false;
795
+ }
796
+ const isOneToOne = isOneToOneRelationship(fieldName, field, config);
797
+ if (!isOneToOne) {
798
+ return true;
799
+ }
800
+ const targetListConfig = config.lists[targetList];
801
+ const targetFieldConfig = targetListConfig.fields[targetField];
802
+ const thisSideExplicit = field.db?.foreignKey;
803
+ const otherSideExplicit = targetFieldConfig.db?.foreignKey;
804
+ if (thisSideExplicit === true && otherSideExplicit === true) {
805
+ throw new Error(`Invalid one-to-one relationship: both "${listKey}.${fieldName}" and "${targetList}.${targetField}" have db.foreignKey set to true. Only one side can store the foreign key.`);
806
+ }
807
+ if (thisSideExplicit === true) {
808
+ return true;
809
+ }
810
+ if (otherSideExplicit === true) {
811
+ return false;
812
+ }
813
+ // Default: the alphabetically "smaller" list name gets the foreign key
814
+ const comparison = listKey.localeCompare(targetList);
815
+ if (comparison !== 0) {
816
+ return comparison < 0;
817
+ }
818
+ // Self-referential: use field name ordering
819
+ return fieldName.localeCompare(targetField) < 0;
820
+ }
821
+ /**
822
+ * Check whether a many relationship is a true many-to-many (both sides many).
823
+ */
824
+ function isManyToMany(fieldName, field, config) {
825
+ if (!field.many) {
826
+ return false;
827
+ }
828
+ const { list: targetList, field: targetField } = parseRelationshipRef(field.ref);
829
+ // List-only ref with many: true is implicitly many-to-many
830
+ if (!targetField) {
831
+ return true;
832
+ }
833
+ const targetFieldConfig = config.lists[targetList]?.fields[targetField];
834
+ if (!targetFieldConfig || targetFieldConfig.type !== 'relationship') {
835
+ return false;
836
+ }
837
+ return !!targetFieldConfig.many;
838
+ }
839
+ /**
840
+ * Compute the explicit relation name for a bidirectional many-to-many relationship,
841
+ * or `undefined` when Prisma's default naming should be used.
842
+ *
843
+ * Honours per-field `db.relationName` (which must match on both sides) and the
844
+ * global `db.joinTableNaming: 'keystone'` setting, picking a deterministic owner
845
+ * for bidirectional relationships so both sides resolve to the same name.
846
+ */
847
+ function computeManyToManyRelationName(listKey, fieldName, field, config) {
848
+ const { list: targetList, field: targetField } = parseRelationshipRef(field.ref);
849
+ const joinTableNaming = config.db.joinTableNaming || 'prisma';
850
+ const sourceRelationName = field.db?.relationName;
851
+ let targetRelationName;
852
+ if (targetField) {
853
+ const targetFieldConfig = config.lists[targetList]?.fields[targetField];
854
+ if (targetFieldConfig?.type === 'relationship') {
855
+ targetRelationName = targetFieldConfig.db?.relationName;
856
+ }
857
+ }
858
+ if (sourceRelationName && targetRelationName && sourceRelationName !== targetRelationName) {
859
+ throw new Error(`Relation name mismatch: ${listKey}.${fieldName} has relationName "${sourceRelationName}" but ${targetList}.${targetField} has "${targetRelationName}". Both sides must use the same relationName.`);
860
+ }
861
+ const explicitRelationName = sourceRelationName || targetRelationName;
862
+ if (explicitRelationName) {
863
+ return explicitRelationName;
864
+ }
865
+ if (joinTableNaming === 'keystone') {
866
+ if (targetField) {
867
+ // Pick a deterministic owner so both sides agree on the relation name
868
+ const sourceKey = `${listKey}.${fieldName}`;
869
+ const targetKey = `${targetList}.${targetField}`;
870
+ return sourceKey.localeCompare(targetKey) < 0
871
+ ? `${listKey}_${fieldName}`
872
+ : `${targetList}_${targetField}`;
873
+ }
874
+ return `${listKey}_${fieldName}`;
875
+ }
876
+ // Default Prisma naming - no explicit relation name needed
877
+ return undefined;
878
+ }
879
+ /**
880
+ * Build the Prisma schema contribution for a relationship field.
881
+ */
882
+ function getPrismaRelation(field, fieldName, listKey, config) {
883
+ const { list: targetList, field: targetField } = parseRelationshipRef(field.ref);
884
+ const paddedName = fieldName.padEnd(12);
885
+ // Synthetic back-relation for list-only refs (Prisma requires an opposite field)
886
+ let backRelation;
887
+ if (!targetField) {
888
+ const syntheticFieldName = `from_${listKey}_${fieldName}`;
889
+ const relationName = field.db?.relationName ?? `${listKey}_${fieldName}`;
890
+ backRelation = {
891
+ targetList,
892
+ line: ` ${syntheticFieldName.padEnd(12)} ${listKey}[] @relation("${relationName}")`,
893
+ };
894
+ }
895
+ if (field.many) {
896
+ let relationLine;
897
+ if (targetField) {
898
+ // Bidirectional many side: use explicit relation name only for true many-to-many
899
+ const m2mName = isManyToMany(fieldName, field, config)
900
+ ? computeManyToManyRelationName(listKey, fieldName, field, config)
901
+ : undefined;
902
+ relationLine = m2mName
903
+ ? ` ${paddedName} ${targetList}[] @relation("${m2mName}")`
904
+ : ` ${paddedName} ${targetList}[]`;
905
+ }
906
+ else {
907
+ // List-only ref many side: always a named relation paired with the synthetic field
908
+ const relationName = field.db?.relationName ?? `${listKey}_${fieldName}`;
909
+ relationLine = ` ${paddedName} ${targetList}[] @relation("${relationName}")`;
910
+ }
911
+ if (field.db?.extendPrismaSchema) {
912
+ relationLine = field.db.extendPrismaSchema({ relationLine }).relationLine;
913
+ }
914
+ return { modelLines: [relationLine], backRelation };
915
+ }
916
+ // Single relationship
917
+ if (shouldHaveForeignKey(listKey, fieldName, field, config)) {
918
+ const foreignKeyField = `${fieldName}Id`;
919
+ const fkPaddedName = foreignKeyField.padEnd(12);
920
+ const uniqueModifier = isOneToOneRelationship(fieldName, field, config) ? ' @unique' : '';
921
+ const mapModifier = typeof field.db?.foreignKey === 'object' && field.db.foreignKey.map
922
+ ? ` @map("${field.db.foreignKey.map}")`
923
+ : ` @map("${fieldName}")`;
924
+ let fkLine = ` ${fkPaddedName} String?${uniqueModifier}${mapModifier}`;
925
+ let relationLine = targetField
926
+ ? ` ${paddedName} ${targetList}? @relation(fields: [${foreignKeyField}], references: [id])`
927
+ : ` ${paddedName} ${targetList}? @relation("${listKey}_${fieldName}", fields: [${foreignKeyField}], references: [id])`;
928
+ if (field.db?.extendPrismaSchema) {
929
+ const extended = field.db.extendPrismaSchema({ fkLine, relationLine });
930
+ fkLine = extended.fkLine ?? fkLine;
931
+ relationLine = extended.relationLine;
932
+ }
933
+ // Default to indexing foreign keys (matching Keystone behaviour) unless disabled
934
+ const indexType = field.isIndexed ?? true;
935
+ const foreignKeyIndex = indexType !== false ? { foreignKeyField, indexType } : undefined;
936
+ return { modelLines: [fkLine, relationLine], foreignKeyIndex, backRelation };
937
+ }
938
+ // Non-FK side of a one-to-one relationship: just the relation field
939
+ let relationLine = ` ${paddedName} ${targetList}?`;
940
+ if (field.db?.extendPrismaSchema) {
941
+ relationLine = field.db.extendPrismaSchema({ relationLine }).relationLine;
942
+ }
943
+ return { modelLines: [relationLine], backRelation };
944
+ }
736
945
  /**
737
946
  * Relationship field
738
947
  */
@@ -758,10 +967,12 @@ export function relationship(options) {
758
967
  'List-only refs (ref: "ListName") always create foreign keys automatically.');
759
968
  }
760
969
  }
761
- return {
970
+ const field = {
762
971
  type: 'relationship',
763
972
  ...options,
764
973
  };
974
+ field.getPrismaRelation = (fieldName, _allFields, listKey, config) => getPrismaRelation(field, fieldName, listKey, config);
975
+ return field;
765
976
  }
766
977
  /**
767
978
  * JSON field for storing arbitrary JSON data