@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,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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"}
|
package/dist/extend.d.ts
ADDED
|
@@ -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"}
|
package/dist/fields/index.d.ts
CHANGED
|
@@ -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":"AACA,OAAO,KAAK,EACV,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,YAAY,EACb,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fields/index.ts"],"names":[],"mappings":"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"}
|
package/dist/fields/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|