@opensaas/stack-core 0.38.0 → 0.39.1
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 +125 -0
- package/CLAUDE.md +21 -0
- package/dist/access/access-filter.d.ts +46 -1
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +113 -4
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/declared-dependencies.d.ts.map +1 -1
- package/dist/access/declared-dependencies.js +4 -9
- package/dist/access/declared-dependencies.js.map +1 -1
- package/dist/access/engine.d.ts +0 -20
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +0 -25
- package/dist/access/engine.js.map +1 -1
- package/dist/access/errors.d.ts +49 -0
- package/dist/access/errors.d.ts.map +1 -1
- package/dist/access/errors.js +75 -0
- package/dist/access/errors.js.map +1 -1
- package/dist/access/field-access.d.ts +39 -1
- package/dist/access/field-access.d.ts.map +1 -1
- package/dist/access/field-access.js +108 -42
- package/dist/access/field-access.js.map +1 -1
- package/dist/access/field-access.test.js +229 -2
- package/dist/access/field-access.test.js.map +1 -1
- package/dist/access/field-transforms.d.ts +0 -27
- package/dist/access/field-transforms.d.ts.map +1 -1
- package/dist/access/field-visibility.d.ts +0 -4
- package/dist/access/field-visibility.d.ts.map +1 -1
- package/dist/access/field-visibility.js +13 -39
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +6 -2
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +12 -2
- package/dist/access/index.js.map +1 -1
- package/dist/access/query-validation.d.ts +121 -0
- package/dist/access/query-validation.d.ts.map +1 -0
- package/dist/access/query-validation.js +292 -0
- package/dist/access/query-validation.js.map +1 -0
- package/dist/access/relationship-count.d.ts.map +1 -1
- package/dist/access/relationship-count.js +2 -10
- package/dist/access/relationship-count.js.map +1 -1
- package/dist/access/relationship-label-filter.d.ts +23 -23
- package/dist/access/relationship-label-filter.d.ts.map +1 -1
- package/dist/access/relationship-label-filter.js +5 -76
- package/dist/access/relationship-label-filter.js.map +1 -1
- package/dist/access/relationship-label-filter.test.js +19 -56
- package/dist/access/relationship-label-filter.test.js.map +1 -1
- package/dist/access/transaction-registry.d.ts +45 -0
- package/dist/access/transaction-registry.d.ts.map +1 -0
- package/dist/access/transaction-registry.js +37 -0
- package/dist/access/transaction-registry.js.map +1 -0
- package/dist/access/types.d.ts +55 -59
- package/dist/access/types.d.ts.map +1 -1
- package/dist/config/index.d.ts +9 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +10 -18
- package/dist/config/index.js.map +1 -1
- package/dist/config/label.d.ts +6 -13
- package/dist/config/label.d.ts.map +1 -1
- package/dist/config/label.js +6 -13
- package/dist/config/label.js.map +1 -1
- package/dist/config/nav-count.d.ts +11 -19
- package/dist/config/nav-count.d.ts.map +1 -1
- package/dist/config/nav-count.js +11 -19
- package/dist/config/nav-count.js.map +1 -1
- package/dist/config/plugin-engine.d.ts +0 -10
- package/dist/config/plugin-engine.d.ts.map +1 -1
- package/dist/config/plugin-engine.js +1 -37
- package/dist/config/plugin-engine.js.map +1 -1
- package/dist/config/types.d.ts +94 -109
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/apply-defaults.d.ts +8 -31
- package/dist/context/apply-defaults.d.ts.map +1 -1
- package/dist/context/apply-defaults.js +12 -41
- package/dist/context/apply-defaults.js.map +1 -1
- package/dist/context/hook-pipeline.d.ts +4 -29
- package/dist/context/hook-pipeline.d.ts.map +1 -1
- package/dist/context/hook-pipeline.js +8 -41
- package/dist/context/hook-pipeline.js.map +1 -1
- package/dist/context/index.d.ts +4 -15
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +190 -193
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts +7 -26
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +73 -171
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/transaction-boundary.d.ts +36 -47
- package/dist/context/transaction-boundary.d.ts.map +1 -1
- package/dist/context/transaction-boundary.js +92 -79
- package/dist/context/transaction-boundary.js.map +1 -1
- package/dist/context/write-pipeline.d.ts +25 -64
- package/dist/context/write-pipeline.d.ts.map +1 -1
- package/dist/context/write-pipeline.js +81 -111
- package/dist/context/write-pipeline.js.map +1 -1
- package/dist/fields/format-prisma-default.d.ts.map +1 -1
- package/dist/fields/format-prisma-default.js +4 -7
- package/dist/fields/format-prisma-default.js.map +1 -1
- package/dist/fields/index.d.ts +17 -2
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +136 -147
- package/dist/fields/index.js.map +1 -1
- package/dist/filter/collect.d.ts +26 -3
- package/dist/filter/collect.d.ts.map +1 -1
- package/dist/filter/collect.js +30 -12
- package/dist/filter/collect.js.map +1 -1
- package/dist/filter/filter.test.js +98 -15
- package/dist/filter/filter.test.js.map +1 -1
- package/dist/filter/index.d.ts +1 -0
- package/dist/filter/index.d.ts.map +1 -1
- package/dist/filter/index.js.map +1 -1
- package/dist/filter/map.d.ts.map +1 -1
- package/dist/filter/map.js +0 -5
- package/dist/filter/map.js.map +1 -1
- package/dist/filter/parse.d.ts.map +1 -1
- package/dist/filter/parse.js +2 -9
- package/dist/filter/parse.js.map +1 -1
- package/dist/filter/serialize.d.ts.map +1 -1
- package/dist/filter/serialize.js +2 -4
- package/dist/filter/serialize.js.map +1 -1
- package/dist/hooks/index.d.ts +4 -42
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +5 -59
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -4
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -0
- package/dist/internal.js.map +1 -1
- package/dist/lib/case-utils.d.ts +0 -32
- package/dist/lib/case-utils.d.ts.map +1 -1
- package/dist/lib/case-utils.js +0 -32
- package/dist/lib/case-utils.js.map +1 -1
- package/dist/mcp/handler.d.ts +0 -4
- package/dist/mcp/handler.d.ts.map +1 -1
- package/dist/mcp/handler.js +12 -69
- package/dist/mcp/handler.js.map +1 -1
- package/dist/query/index.d.ts +26 -227
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +14 -117
- package/dist/query/index.js.map +1 -1
- package/dist/utils/password.d.ts +0 -68
- package/dist/utils/password.d.ts.map +1 -1
- package/dist/utils/password.js +7 -76
- package/dist/utils/password.js.map +1 -1
- package/dist/validation/field-config.d.ts.map +1 -1
- package/dist/validation/field-config.js +0 -6
- package/dist/validation/field-config.js.map +1 -1
- package/dist/validation/schema.d.ts +0 -7
- package/dist/validation/schema.d.ts.map +1 -1
- package/dist/validation/schema.js +0 -10
- package/dist/validation/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/access/access-filter.ts +152 -5
- package/src/access/declared-dependencies.ts +4 -9
- package/src/access/engine.ts +0 -25
- package/src/access/errors.ts +81 -0
- package/src/access/field-access.test.ts +312 -2
- package/src/access/field-access.ts +118 -42
- package/src/access/field-transforms.ts +8 -39
- package/src/access/field-visibility.ts +16 -47
- package/src/access/index.ts +20 -2
- package/src/access/query-validation.ts +359 -0
- package/src/access/relationship-count.ts +2 -10
- package/src/access/relationship-label-filter.test.ts +19 -68
- package/src/access/relationship-label-filter.ts +27 -104
- package/src/access/transaction-registry.ts +58 -0
- package/src/access/types.ts +83 -86
- package/src/config/index.ts +10 -22
- package/src/config/label.ts +6 -13
- package/src/config/nav-count.ts +11 -19
- package/src/config/plugin-engine.ts +1 -37
- package/src/config/types.ts +95 -110
- package/src/context/apply-defaults.ts +12 -44
- package/src/context/hook-pipeline.ts +12 -67
- package/src/context/index.ts +263 -247
- package/src/context/nested-operations.ts +94 -226
- package/src/context/transaction-boundary.ts +113 -109
- package/src/context/write-pipeline.ts +99 -155
- package/src/fields/format-prisma-default.ts +4 -7
- package/src/fields/index.ts +155 -147
- package/src/filter/collect.ts +48 -12
- package/src/filter/filter.test.ts +153 -15
- package/src/filter/index.ts +1 -0
- package/src/filter/map.ts +0 -5
- package/src/filter/parse.ts +2 -9
- package/src/filter/serialize.ts +2 -4
- package/src/hooks/index.ts +5 -59
- package/src/index.ts +23 -4
- package/src/internal.ts +5 -0
- package/src/lib/case-utils.ts +0 -32
- package/src/mcp/handler.ts +13 -71
- package/src/query/index.ts +29 -235
- package/src/utils/password.ts +7 -76
- package/src/validation/field-config.ts +0 -6
- package/src/validation/schema.ts +0 -10
- package/tests/context.test.ts +700 -1
- package/tests/field-types.test.ts +222 -0
- package/tests/mcp-handler.test.ts +34 -0
- package/tests/transaction-boundary-outermost.test.ts +530 -0
- package/tsconfig.tsbuildinfo +1 -1
package/dist/fields/index.js
CHANGED
|
@@ -3,18 +3,11 @@ import { hashPassword, isHashedPassword, HashedPassword } from '../utils/passwor
|
|
|
3
3
|
import { formatPrismaDefault } from './format-prisma-default.js';
|
|
4
4
|
import { getLabelFieldName } from '../config/label.js';
|
|
5
5
|
import { RELATIONSHIP_COUNT_FILTER_KEY } from '../filter/types.js';
|
|
6
|
-
/**
|
|
7
|
-
* Operators shared by numeric/date fields: plain equality plus the four
|
|
8
|
-
* comparisons. `eq` maps to Prisma's `equals`; the comparisons pass through.
|
|
9
|
-
*/
|
|
6
|
+
/** Operators shared by numeric/date fields' `getFilterSpec`. */
|
|
10
7
|
const COMPARISON_OPERATORS = ['eq', 'gt', 'gte', 'lt', 'lte'];
|
|
11
|
-
/** Map a Filter operator to its Prisma condition key (`eq` → `equals`). */
|
|
12
8
|
function prismaComparisonKey(operator) {
|
|
13
9
|
return operator === 'eq' ? 'equals' : operator;
|
|
14
10
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Format field name for display in error messages
|
|
17
|
-
*/
|
|
18
11
|
function formatFieldName(fieldName) {
|
|
19
12
|
return fieldName
|
|
20
13
|
.replace(/([A-Z])/g, ' $1')
|
|
@@ -59,11 +52,9 @@ export function text(options) {
|
|
|
59
52
|
const isRequired = validation?.isRequired;
|
|
60
53
|
const isNullable = db?.isNullable ?? !isRequired;
|
|
61
54
|
let modifiers = '';
|
|
62
|
-
// Optional modifier
|
|
63
55
|
if (isNullable) {
|
|
64
56
|
modifiers += '?';
|
|
65
57
|
}
|
|
66
|
-
// Native type modifier (e.g., @db.Text)
|
|
67
58
|
if (db?.nativeType) {
|
|
68
59
|
modifiers += ` @db.${db.nativeType}`;
|
|
69
60
|
}
|
|
@@ -88,7 +79,6 @@ export function text(options) {
|
|
|
88
79
|
if (options?.isIndexed === 'unique') {
|
|
89
80
|
modifiers += ' @unique';
|
|
90
81
|
}
|
|
91
|
-
// Map modifier
|
|
92
82
|
if (db?.map) {
|
|
93
83
|
modifiers += ` @map("${db.map}")`;
|
|
94
84
|
}
|
|
@@ -106,9 +96,6 @@ export function text(options) {
|
|
|
106
96
|
optional: !isRequired,
|
|
107
97
|
};
|
|
108
98
|
},
|
|
109
|
-
// Text fields drive free-text search: a bare word (or a `field:value`) maps
|
|
110
|
-
// to a case-preserving `contains`. This is what replaces the old hard-coded
|
|
111
|
-
// `type === 'text'` search in the admin list view.
|
|
112
99
|
getFilterSpec: (fieldName) => ({
|
|
113
100
|
operators: ['eq'],
|
|
114
101
|
freeText: true,
|
|
@@ -148,21 +135,16 @@ export function integer(options) {
|
|
|
148
135
|
const isRequired = validation?.isRequired;
|
|
149
136
|
const isNullable = db?.isNullable ?? !isRequired;
|
|
150
137
|
let modifiers = '';
|
|
151
|
-
// Optional modifier
|
|
152
138
|
if (isNullable) {
|
|
153
139
|
modifiers += '?';
|
|
154
140
|
}
|
|
155
|
-
// Native type modifier (e.g., @db.SmallInt, @db.BigInt)
|
|
156
141
|
if (db?.nativeType) {
|
|
157
142
|
modifiers += ` @db.${db.nativeType}`;
|
|
158
143
|
}
|
|
159
|
-
// Default value if provided (bare numeric literal). Independent of the
|
|
160
|
-
// nullable `?` modifier above — the default never overwrites nullability.
|
|
161
144
|
const defaultLiteral = formatPrismaDefault(options?.defaultValue, 'integer');
|
|
162
145
|
if (defaultLiteral !== undefined) {
|
|
163
146
|
modifiers += ` @default(${defaultLiteral})`;
|
|
164
147
|
}
|
|
165
|
-
// Map modifier
|
|
166
148
|
if (db?.map) {
|
|
167
149
|
modifiers += ` @map("${db.map}")`;
|
|
168
150
|
}
|
|
@@ -178,8 +160,7 @@ export function integer(options) {
|
|
|
178
160
|
optional: !isRequired,
|
|
179
161
|
};
|
|
180
162
|
},
|
|
181
|
-
//
|
|
182
|
-
// value can't be interpreted, so its token degrades to free text.
|
|
163
|
+
// A non-integer token can't be interpreted, so it degrades to free text.
|
|
183
164
|
getFilterSpec: (fieldName) => ({
|
|
184
165
|
operators: COMPARISON_OPERATORS,
|
|
185
166
|
toCondition: (operator, value) => {
|
|
@@ -244,8 +225,6 @@ export function decimal(options) {
|
|
|
244
225
|
scale,
|
|
245
226
|
...options,
|
|
246
227
|
getZodSchema: (fieldName, operation) => {
|
|
247
|
-
// Decimal values can be provided as strings or numbers
|
|
248
|
-
// Prisma will convert them to Decimal instances
|
|
249
228
|
const baseSchema = z.union([
|
|
250
229
|
z.string({
|
|
251
230
|
message: `${formatFieldName(fieldName)} must be a decimal value (string or number)`,
|
|
@@ -257,7 +236,6 @@ export function decimal(options) {
|
|
|
257
236
|
message: `${formatFieldName(fieldName)} must be a decimal value`,
|
|
258
237
|
});
|
|
259
238
|
let schema = baseSchema;
|
|
260
|
-
// Add min validation if specified
|
|
261
239
|
if (options?.validation?.min !== undefined) {
|
|
262
240
|
const minValue = parseFloat(options.validation.min);
|
|
263
241
|
schema = schema.refine((val) => {
|
|
@@ -267,7 +245,6 @@ export function decimal(options) {
|
|
|
267
245
|
message: `${formatFieldName(fieldName)} must be at least ${options.validation.min}`,
|
|
268
246
|
});
|
|
269
247
|
}
|
|
270
|
-
// Add max validation if specified
|
|
271
248
|
if (options?.validation?.max !== undefined) {
|
|
272
249
|
const maxValue = parseFloat(options.validation.max);
|
|
273
250
|
schema = schema.refine((val) => {
|
|
@@ -287,23 +264,18 @@ export function decimal(options) {
|
|
|
287
264
|
const isRequired = validation?.isRequired;
|
|
288
265
|
const isNullable = db?.isNullable ?? !isRequired;
|
|
289
266
|
let modifiers = '';
|
|
290
|
-
// Optional modifier
|
|
291
267
|
if (isNullable) {
|
|
292
268
|
modifiers += '?';
|
|
293
269
|
}
|
|
294
|
-
// Precision and scale
|
|
295
270
|
modifiers += ` @db.Decimal(${precision}, ${scale})`;
|
|
296
|
-
// Default value if provided
|
|
297
271
|
if (options?.defaultValue !== undefined) {
|
|
298
272
|
modifiers += ` @default(${options.defaultValue})`;
|
|
299
273
|
}
|
|
300
|
-
// Database mapping
|
|
301
274
|
if (db?.map) {
|
|
302
275
|
modifiers += ` @map("${db.map}")`;
|
|
303
276
|
}
|
|
304
|
-
// Unique modifier
|
|
305
|
-
//
|
|
306
|
-
// generator as `@@index([...])` on the model.
|
|
277
|
+
// Unique modifier — non-unique index routes through `index` below,
|
|
278
|
+
// same as `text()`'s getPrismaType.
|
|
307
279
|
if (options?.isIndexed === 'unique') {
|
|
308
280
|
modifiers += ' @unique';
|
|
309
281
|
}
|
|
@@ -346,6 +318,116 @@ export function decimal(options) {
|
|
|
346
318
|
}),
|
|
347
319
|
};
|
|
348
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* 64-bit integer field for values that overflow `integer()`'s 32-bit `Int`
|
|
323
|
+
* (e.g. a millisecond epoch). Prisma `BigInt`, TypeScript `bigint`.
|
|
324
|
+
*
|
|
325
|
+
* Accepts `bigint`, an integer `number`, or a numeric `string` on write and
|
|
326
|
+
* always coerces to `bigint`. A `number` above `Number.MAX_SAFE_INTEGER` is
|
|
327
|
+
* rejected rather than coerced — it has already lost precision before this
|
|
328
|
+
* field sees it, so accepting it would reintroduce the exact defect this
|
|
329
|
+
* field exists to prevent. Pass a `bigint` or a string for values beyond that
|
|
330
|
+
* range.
|
|
331
|
+
*
|
|
332
|
+
* Wire representation (ADR-0029): `bigint` in application code, a decimal
|
|
333
|
+
* string over MCP.
|
|
334
|
+
*/
|
|
335
|
+
export function bigInt(options) {
|
|
336
|
+
return {
|
|
337
|
+
type: 'bigInt',
|
|
338
|
+
...options,
|
|
339
|
+
getZodSchema: (fieldName, operation) => {
|
|
340
|
+
const validation = options?.validation;
|
|
341
|
+
const isRequired = validation?.isRequired;
|
|
342
|
+
const coerced = z
|
|
343
|
+
.union([z.bigint(), z.number(), z.string()], {
|
|
344
|
+
message: `${formatFieldName(fieldName)} must be a bigint, an integer number, or a numeric string`,
|
|
345
|
+
})
|
|
346
|
+
.transform((val, ctx) => {
|
|
347
|
+
if (typeof val === 'bigint')
|
|
348
|
+
return val;
|
|
349
|
+
if (typeof val === 'number') {
|
|
350
|
+
if (!Number.isInteger(val)) {
|
|
351
|
+
ctx.addIssue(`${formatFieldName(fieldName)} must be an integer`);
|
|
352
|
+
return z.NEVER;
|
|
353
|
+
}
|
|
354
|
+
if (!Number.isSafeInteger(val)) {
|
|
355
|
+
ctx.addIssue(`${formatFieldName(fieldName)} exceeds Number.MAX_SAFE_INTEGER — pass a bigint or a numeric string instead`);
|
|
356
|
+
return z.NEVER;
|
|
357
|
+
}
|
|
358
|
+
return BigInt(val);
|
|
359
|
+
}
|
|
360
|
+
const trimmed = val.trim();
|
|
361
|
+
if (!/^-?\d+$/.test(trimmed)) {
|
|
362
|
+
ctx.addIssue(`${formatFieldName(fieldName)} must be an integer`);
|
|
363
|
+
return z.NEVER;
|
|
364
|
+
}
|
|
365
|
+
return BigInt(trimmed);
|
|
366
|
+
});
|
|
367
|
+
let schema = coerced;
|
|
368
|
+
if (validation?.min !== undefined) {
|
|
369
|
+
const min = validation.min;
|
|
370
|
+
schema = schema.refine((val) => val >= min, {
|
|
371
|
+
message: `${formatFieldName(fieldName)} must be at least ${min}`,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (validation?.max !== undefined) {
|
|
375
|
+
const max = validation.max;
|
|
376
|
+
schema = schema.refine((val) => val <= max, {
|
|
377
|
+
message: `${formatFieldName(fieldName)} must be at most ${max}`,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return !isRequired || operation === 'update' ? schema.optional().nullable() : schema;
|
|
381
|
+
},
|
|
382
|
+
getPrismaType: (_fieldName) => {
|
|
383
|
+
const validation = options?.validation;
|
|
384
|
+
const db = options?.db;
|
|
385
|
+
const isRequired = validation?.isRequired;
|
|
386
|
+
const isNullable = db?.isNullable ?? !isRequired;
|
|
387
|
+
let modifiers = '';
|
|
388
|
+
if (isNullable) {
|
|
389
|
+
modifiers += '?';
|
|
390
|
+
}
|
|
391
|
+
if (db?.nativeType) {
|
|
392
|
+
modifiers += ` @db.${db.nativeType}`;
|
|
393
|
+
}
|
|
394
|
+
if (options?.defaultValue !== undefined) {
|
|
395
|
+
modifiers += ` @default(${options.defaultValue})`;
|
|
396
|
+
}
|
|
397
|
+
if (db?.map) {
|
|
398
|
+
modifiers += ` @map("${db.map}")`;
|
|
399
|
+
}
|
|
400
|
+
// Unique modifier — non-unique index routes through `index` below,
|
|
401
|
+
// same as `text()`'s getPrismaType.
|
|
402
|
+
if (options?.isIndexed === 'unique') {
|
|
403
|
+
modifiers += ' @unique';
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
type: 'BigInt',
|
|
407
|
+
modifiers: modifiers.trimStart() || undefined,
|
|
408
|
+
index: options?.isIndexed === true ? true : undefined,
|
|
409
|
+
};
|
|
410
|
+
},
|
|
411
|
+
getTypeScriptType: () => {
|
|
412
|
+
const isRequired = options?.validation?.isRequired;
|
|
413
|
+
return {
|
|
414
|
+
type: 'bigint',
|
|
415
|
+
optional: !isRequired,
|
|
416
|
+
};
|
|
417
|
+
},
|
|
418
|
+
// A non-integer token degrades to free text.
|
|
419
|
+
getFilterSpec: (fieldName) => ({
|
|
420
|
+
operators: COMPARISON_OPERATORS,
|
|
421
|
+
toCondition: (operator, value) => {
|
|
422
|
+
const trimmed = value.trim();
|
|
423
|
+
if (!/^-?\d+$/.test(trimmed))
|
|
424
|
+
return null;
|
|
425
|
+
return { [fieldName]: { [prismaComparisonKey(operator)]: BigInt(trimmed) } };
|
|
426
|
+
},
|
|
427
|
+
suggestions: { valueSource: { kind: 'none' } },
|
|
428
|
+
}),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
349
431
|
/**
|
|
350
432
|
* Checkbox (boolean) field
|
|
351
433
|
*/
|
|
@@ -360,15 +442,15 @@ export function checkbox(options) {
|
|
|
360
442
|
const db = options?.db;
|
|
361
443
|
const hasDefault = options?.defaultValue !== undefined;
|
|
362
444
|
let modifiers = '';
|
|
363
|
-
//
|
|
364
|
-
//
|
|
445
|
+
// Checkboxes are non-nullable by default (must be true or false), unlike
|
|
446
|
+
// the other scalar fields' nullable-unless-required default — set
|
|
447
|
+
// db.isNullable: true to allow NULL.
|
|
365
448
|
if (db?.isNullable === true) {
|
|
366
449
|
modifiers += '?';
|
|
367
450
|
}
|
|
368
451
|
if (hasDefault) {
|
|
369
452
|
modifiers += ` @default(${options.defaultValue})`;
|
|
370
453
|
}
|
|
371
|
-
// Map modifier
|
|
372
454
|
if (db?.map) {
|
|
373
455
|
modifiers += ` @map("${db.map}")`;
|
|
374
456
|
}
|
|
@@ -383,8 +465,7 @@ export function checkbox(options) {
|
|
|
383
465
|
optional: options?.defaultValue === undefined,
|
|
384
466
|
};
|
|
385
467
|
},
|
|
386
|
-
//
|
|
387
|
-
// other than true/false can't be interpreted and degrades to free text.
|
|
468
|
+
// Anything other than true/false degrades to free text.
|
|
388
469
|
getFilterSpec: (fieldName) => ({
|
|
389
470
|
operators: ['eq'],
|
|
390
471
|
toCondition: (operator, value) => {
|
|
@@ -425,22 +506,17 @@ export function timestamp(options) {
|
|
|
425
506
|
typeof options.defaultValue === 'object' &&
|
|
426
507
|
'kind' in options.defaultValue &&
|
|
427
508
|
options.defaultValue.kind === 'now';
|
|
428
|
-
// Nullability: explicit db.isNullable overrides the default (nullable unless @default(now()))
|
|
429
509
|
const isNullable = db?.isNullable ?? !hasDefaultNow;
|
|
430
510
|
let modifiers = '';
|
|
431
|
-
// Optional modifier
|
|
432
511
|
if (isNullable) {
|
|
433
512
|
modifiers += '?';
|
|
434
513
|
}
|
|
435
|
-
// Default value
|
|
436
514
|
if (hasDefaultNow) {
|
|
437
515
|
modifiers += ' @default(now())';
|
|
438
516
|
}
|
|
439
|
-
// Native type modifier (e.g., @db.Timestamptz for PostgreSQL)
|
|
440
517
|
if (db?.nativeType) {
|
|
441
518
|
modifiers += ` @db.${db.nativeType}`;
|
|
442
519
|
}
|
|
443
|
-
// Map modifier
|
|
444
520
|
if (db?.map) {
|
|
445
521
|
modifiers += ` @map("${db.map}")`;
|
|
446
522
|
}
|
|
@@ -459,8 +535,7 @@ export function timestamp(options) {
|
|
|
459
535
|
optional: !hasDefault,
|
|
460
536
|
};
|
|
461
537
|
},
|
|
462
|
-
//
|
|
463
|
-
// unparseable date degrades to free text.
|
|
538
|
+
// An unparseable date degrades to free text.
|
|
464
539
|
getFilterSpec: (fieldName) => ({
|
|
465
540
|
operators: COMPARISON_OPERATORS,
|
|
466
541
|
toCondition: (operator, value) => {
|
|
@@ -537,23 +612,11 @@ export function calendarDay(options) {
|
|
|
537
612
|
return {
|
|
538
613
|
type: 'calendarDay',
|
|
539
614
|
...options,
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
// UTC-midnight Date; leave anything else (a Date already, null/undefined,
|
|
546
|
-
// or a malformed string) untouched so the zod schema below still rejects
|
|
547
|
-
// malformed input with a clear message. Reads resolvedData[fieldKey]
|
|
548
|
-
// (not raw inputData) so a list-level resolveInput that injects a default
|
|
549
|
-
// for an omitted key is still coerced instead of being overwritten.
|
|
550
|
-
//
|
|
551
|
-
// Reads: the underlying @db.Date column hands Prisma a Date (or a TEXT
|
|
552
|
-
// string under the SQLite fallback). Normalise to a YYYY-MM-DD string so the
|
|
553
|
-
// runtime value matches the declared `string` type. UTC components are used
|
|
554
|
-
// so the formatting never drifts a day in non-UTC timezones.
|
|
555
|
-
// Cast hooks to any since field builders are generic and can't know the
|
|
556
|
-
// specific TFieldKey (same pattern as password()).
|
|
615
|
+
// Hook Pipeline runs field resolveInput before zod validation — the only
|
|
616
|
+
// point a YYYY-MM-DD string can be turned into what Prisma's `@db.Date`
|
|
617
|
+
// write validator accepts (#621). Reads resolvedData[fieldKey], not raw
|
|
618
|
+
// inputData, so a list-level resolveInput's injected default for an
|
|
619
|
+
// omitted key is still coerced rather than overwritten.
|
|
557
620
|
hooks: {
|
|
558
621
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Field builder hooks must be generic
|
|
559
622
|
resolveInput: ({ resolvedData, fieldKey }) => {
|
|
@@ -567,17 +630,15 @@ export function calendarDay(options) {
|
|
|
567
630
|
},
|
|
568
631
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Field builder hooks must be generic
|
|
569
632
|
resolveOutput: ({ value }) => formatCalendarDay(value),
|
|
570
|
-
// Merge with user-provided hooks if any
|
|
571
633
|
...options?.hooks,
|
|
572
634
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Hook object needs type assertion for field builder
|
|
573
635
|
},
|
|
574
636
|
getZodSchema: (fieldName, operation) => {
|
|
575
637
|
const validation = options?.validation;
|
|
576
638
|
const isRequired = validation?.isRequired;
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
// untouched and still fail the regex here with a clear message.
|
|
639
|
+
// Accepts a `Date` because resolveInput above already converted a valid
|
|
640
|
+
// string to one before this schema runs; a malformed string falls
|
|
641
|
+
// through resolveInput untouched and fails the regex here instead.
|
|
581
642
|
const stringSchema = z
|
|
582
643
|
.string({
|
|
583
644
|
message: `${formatFieldName(fieldName)} must be a valid date in ISO8601 format (YYYY-MM-DD)`,
|
|
@@ -590,7 +651,6 @@ export function calendarDay(options) {
|
|
|
590
651
|
return dateSchema;
|
|
591
652
|
}
|
|
592
653
|
else if (isRequired && operation === 'update') {
|
|
593
|
-
// Required in update mode: omitted keys pass; present values must be valid
|
|
594
654
|
return dateSchema.optional();
|
|
595
655
|
}
|
|
596
656
|
else {
|
|
@@ -603,27 +663,22 @@ export function calendarDay(options) {
|
|
|
603
663
|
const isRequired = validation?.isRequired;
|
|
604
664
|
const isNullable = db?.isNullable ?? !isRequired;
|
|
605
665
|
let modifiers = '';
|
|
606
|
-
// Optional modifier
|
|
607
666
|
if (isNullable) {
|
|
608
667
|
modifiers += '?';
|
|
609
668
|
}
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
// SQLite will use TEXT for DateTime fields
|
|
669
|
+
// SQLite has no native DATE type and falls back to TEXT for DateTime
|
|
670
|
+
// columns, so @db.Date only applies on PostgreSQL/MySQL.
|
|
613
671
|
if (provider && provider.toLowerCase() !== 'sqlite') {
|
|
614
672
|
modifiers += ' @db.Date';
|
|
615
673
|
}
|
|
616
|
-
// Default value if provided
|
|
617
674
|
if (options?.defaultValue !== undefined) {
|
|
618
675
|
modifiers += ` @default("${options.defaultValue}")`;
|
|
619
676
|
}
|
|
620
|
-
// Database mapping
|
|
621
677
|
if (db?.map) {
|
|
622
678
|
modifiers += ` @map("${db.map}")`;
|
|
623
679
|
}
|
|
624
|
-
// Unique modifier
|
|
625
|
-
//
|
|
626
|
-
// generator as `@@index([...])` on the model.
|
|
680
|
+
// Unique modifier — non-unique index routes through `index` below,
|
|
681
|
+
// same as `text()`'s getPrismaType.
|
|
627
682
|
if (options?.isIndexed === 'unique') {
|
|
628
683
|
modifiers += ' @unique';
|
|
629
684
|
}
|
|
@@ -638,12 +693,6 @@ export function calendarDay(options) {
|
|
|
638
693
|
const db = options?.db;
|
|
639
694
|
const isRequired = validation?.isRequired;
|
|
640
695
|
const isNullable = db?.isNullable ?? !isRequired;
|
|
641
|
-
// calendarDay is a YYYY-MM-DD string end-to-end (Keystone's CalendarDay
|
|
642
|
-
// scalar). Returning 'string' here makes the entity/read type and the
|
|
643
|
-
// standalone generated CreateInput/UpdateInput types `string`. At the
|
|
644
|
-
// context.db write path a Date is still rejected at runtime by validation
|
|
645
|
-
// (the generated db method `data` type derives from Prisma's `Date | string`
|
|
646
|
-
// input — making it a compile-time error is tracked in #599).
|
|
647
696
|
return {
|
|
648
697
|
type: 'string',
|
|
649
698
|
optional: isNullable,
|
|
@@ -749,43 +798,34 @@ export function password(options) {
|
|
|
749
798
|
...options,
|
|
750
799
|
resultExtension: {
|
|
751
800
|
outputType: "import('@opensaas/stack-core/internal').HashedPassword",
|
|
752
|
-
// No compute - delegates to resolveOutput hook
|
|
753
801
|
},
|
|
754
802
|
ui: {
|
|
755
803
|
...options?.ui,
|
|
756
804
|
valueForClientSerialization: ({ value }) => ({ isSet: !!value }),
|
|
757
805
|
},
|
|
758
|
-
// Cast hooks to any since field builders are generic and can't know the specific TFieldKey
|
|
759
806
|
hooks: {
|
|
760
|
-
// Hash password before writing to database
|
|
761
807
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Field builder hooks must be generic
|
|
762
808
|
resolveInput: async ({ inputData, fieldKey }) => {
|
|
763
|
-
// Skip if undefined or null (allows partial updates)
|
|
764
809
|
const inputValue = inputData[fieldKey];
|
|
765
810
|
if (inputValue === undefined || inputValue === null) {
|
|
766
811
|
return inputValue;
|
|
767
812
|
}
|
|
768
|
-
// Skip if not a string
|
|
769
813
|
if (typeof inputValue !== 'string' || inputValue.length === 0) {
|
|
770
814
|
return inputValue;
|
|
771
815
|
}
|
|
772
|
-
//
|
|
816
|
+
// Idempotent: skip re-hashing a value that's already a hash.
|
|
773
817
|
if (isHashedPassword(inputValue)) {
|
|
774
818
|
return inputValue;
|
|
775
819
|
}
|
|
776
|
-
// Hash the password
|
|
777
820
|
return (await hashPassword(inputValue)).toString();
|
|
778
821
|
},
|
|
779
|
-
// Wrap password with HashedPassword class after reading from database
|
|
780
822
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Field builder hooks must be generic
|
|
781
823
|
resolveOutput: ({ value }) => {
|
|
782
|
-
// Only wrap string values (hashed passwords)
|
|
783
824
|
if (typeof value === 'string' && value.length > 0) {
|
|
784
825
|
return new HashedPassword(value);
|
|
785
826
|
}
|
|
786
827
|
return undefined;
|
|
787
828
|
},
|
|
788
|
-
// Merge with user-provided hooks if any
|
|
789
829
|
...options?.hooks,
|
|
790
830
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Hook object needs type assertion for field builder
|
|
791
831
|
},
|
|
@@ -793,7 +833,6 @@ export function password(options) {
|
|
|
793
833
|
const validation = options?.validation;
|
|
794
834
|
const isRequired = validation?.isRequired;
|
|
795
835
|
if (isRequired && operation === 'create') {
|
|
796
|
-
// Required in create mode: reject undefined and empty strings
|
|
797
836
|
return z
|
|
798
837
|
.string({
|
|
799
838
|
message: `${formatFieldName(fieldName)} must be text`,
|
|
@@ -803,7 +842,6 @@ export function password(options) {
|
|
|
803
842
|
});
|
|
804
843
|
}
|
|
805
844
|
else if (isRequired && operation === 'update') {
|
|
806
|
-
// Required in update mode: omitted keys pass; if provided, reject empty strings
|
|
807
845
|
return z
|
|
808
846
|
.string()
|
|
809
847
|
.min(1, {
|
|
@@ -812,7 +850,6 @@ export function password(options) {
|
|
|
812
850
|
.optional();
|
|
813
851
|
}
|
|
814
852
|
else {
|
|
815
|
-
// Not required: can be undefined or any string
|
|
816
853
|
return z
|
|
817
854
|
.string({
|
|
818
855
|
message: `${formatFieldName(fieldName)} must be text`,
|
|
@@ -827,15 +864,12 @@ export function password(options) {
|
|
|
827
864
|
const isRequired = validation?.isRequired;
|
|
828
865
|
const isNullable = db?.isNullable ?? !isRequired;
|
|
829
866
|
let modifiers = '';
|
|
830
|
-
// Optional modifier
|
|
831
867
|
if (isNullable) {
|
|
832
868
|
modifiers += '?';
|
|
833
869
|
}
|
|
834
|
-
// Native type modifier (e.g., @db.Text)
|
|
835
870
|
if (db?.nativeType) {
|
|
836
871
|
modifiers += ` @db.${db.nativeType}`;
|
|
837
872
|
}
|
|
838
|
-
// Map modifier
|
|
839
873
|
if (db?.map) {
|
|
840
874
|
modifiers += ` @map("${db.map}")`;
|
|
841
875
|
}
|
|
@@ -900,7 +934,6 @@ export function select(options) {
|
|
|
900
934
|
// (mirroring text/integer) so the default never overwrites the `?`.
|
|
901
935
|
const isNullable = options.db?.isNullable ?? (!isRequired && !hasDefault);
|
|
902
936
|
let modifiers = '';
|
|
903
|
-
// Optional modifier
|
|
904
937
|
if (isNullable) {
|
|
905
938
|
modifiers += '?';
|
|
906
939
|
}
|
|
@@ -911,11 +944,10 @@ export function select(options) {
|
|
|
911
944
|
const capitalizedField = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
|
|
912
945
|
const derivedEnumName = listName ? `${listName}${capitalizedField}` : capitalizedField;
|
|
913
946
|
const enumName = options.db?.enumName ?? derivedEnumName;
|
|
914
|
-
//
|
|
947
|
+
// No quotes for enum default values (unlike the string branch below).
|
|
915
948
|
if (hasDefault) {
|
|
916
949
|
modifiers += ` @default(${options.defaultValue})`;
|
|
917
950
|
}
|
|
918
|
-
// Map modifier
|
|
919
951
|
if (options.db?.map) {
|
|
920
952
|
modifiers += ` @map("${options.db.map}")`;
|
|
921
953
|
}
|
|
@@ -926,11 +958,9 @@ export function select(options) {
|
|
|
926
958
|
};
|
|
927
959
|
}
|
|
928
960
|
// String type (default)
|
|
929
|
-
// Add default value if provided
|
|
930
961
|
if (hasDefault) {
|
|
931
962
|
modifiers += ` @default("${options.defaultValue}")`;
|
|
932
963
|
}
|
|
933
|
-
// Map modifier
|
|
934
964
|
if (options.db?.map) {
|
|
935
965
|
modifiers += ` @map("${options.db.map}")`;
|
|
936
966
|
}
|
|
@@ -995,9 +1025,6 @@ function parseRelationshipRef(ref) {
|
|
|
995
1025
|
throw new Error(`Invalid relationship ref: ${ref}`);
|
|
996
1026
|
}
|
|
997
1027
|
}
|
|
998
|
-
/**
|
|
999
|
-
* Check if a relationship is one-to-one (bidirectional with both sides having many: false).
|
|
1000
|
-
*/
|
|
1001
1028
|
function isOneToOneRelationship(fieldName, field, config) {
|
|
1002
1029
|
const { list: targetList, field: targetField } = parseRelationshipRef(field.ref);
|
|
1003
1030
|
if (!targetField) {
|
|
@@ -1019,10 +1046,6 @@ function isOneToOneRelationship(fieldName, field, config) {
|
|
|
1019
1046
|
}
|
|
1020
1047
|
return !targetFieldConfig.many;
|
|
1021
1048
|
}
|
|
1022
|
-
/**
|
|
1023
|
-
* Determine if this side of a relationship should store the foreign key.
|
|
1024
|
-
* For one-to-one relationships, only one side stores the foreign key.
|
|
1025
|
-
*/
|
|
1026
1049
|
function shouldHaveForeignKey(listKey, fieldName, field, config) {
|
|
1027
1050
|
const { list: targetList, field: targetField } = parseRelationshipRef(field.ref);
|
|
1028
1051
|
if (!targetField) {
|
|
@@ -1056,9 +1079,6 @@ function shouldHaveForeignKey(listKey, fieldName, field, config) {
|
|
|
1056
1079
|
// Self-referential: use field name ordering
|
|
1057
1080
|
return fieldName.localeCompare(targetField) < 0;
|
|
1058
1081
|
}
|
|
1059
|
-
/**
|
|
1060
|
-
* Check whether a many relationship is a true many-to-many (both sides many).
|
|
1061
|
-
*/
|
|
1062
1082
|
function isManyToMany(fieldName, field, config) {
|
|
1063
1083
|
if (!field.many) {
|
|
1064
1084
|
return false;
|
|
@@ -1111,12 +1131,8 @@ function computeManyToManyRelationName(listKey, fieldName, field, config) {
|
|
|
1111
1131
|
}
|
|
1112
1132
|
return `${listKey}_${fieldName}`;
|
|
1113
1133
|
}
|
|
1114
|
-
// Default Prisma naming - no explicit relation name needed
|
|
1115
1134
|
return undefined;
|
|
1116
1135
|
}
|
|
1117
|
-
/**
|
|
1118
|
-
* Build the Prisma schema contribution for a relationship field.
|
|
1119
|
-
*/
|
|
1120
1136
|
function getPrismaRelation(field, fieldName, listKey, config) {
|
|
1121
1137
|
const { list: targetList, field: targetField } = parseRelationshipRef(field.ref);
|
|
1122
1138
|
const paddedName = fieldName.padEnd(12);
|
|
@@ -1203,27 +1219,20 @@ export function relationship(options) {
|
|
|
1203
1219
|
if (!options.ref) {
|
|
1204
1220
|
throw new Error('Relationship field must have a ref');
|
|
1205
1221
|
}
|
|
1206
|
-
// Validate ref format: 'ListName.fieldName' or 'ListName'
|
|
1207
1222
|
const refParts = options.ref.split('.');
|
|
1208
1223
|
if (refParts.length !== 1 && refParts.length !== 2) {
|
|
1209
1224
|
throw new Error(`Invalid relationship ref format: "${options.ref}". Expected format: "ListName.fieldName" or "ListName"`);
|
|
1210
1225
|
}
|
|
1211
|
-
// Validate db.foreignKey usage
|
|
1212
1226
|
if (options.db?.foreignKey !== undefined) {
|
|
1213
|
-
// Can only be used on single relationships (not many)
|
|
1214
1227
|
if (options.many) {
|
|
1215
1228
|
throw new Error('db.foreignKey can only be used on single relationships (many: false or undefined). ' +
|
|
1216
1229
|
'Many-side of a relationship never stores the foreign key.');
|
|
1217
1230
|
}
|
|
1218
|
-
// Can only be used on bidirectional relationships (with target field)
|
|
1219
1231
|
if (refParts.length === 1) {
|
|
1220
1232
|
throw new Error('db.foreignKey can only be used on bidirectional relationships (ref: "ListName.fieldName"). ' +
|
|
1221
1233
|
'List-only refs (ref: "ListName") always create foreign keys automatically.');
|
|
1222
1234
|
}
|
|
1223
1235
|
}
|
|
1224
|
-
// Validate db.isNullable usage: only the FK-owning (single) side of a
|
|
1225
|
-
// relationship has a column to make non-nullable — the many side always
|
|
1226
|
-
// generates an array field with no nullability of its own.
|
|
1227
1236
|
if (options.db?.isNullable !== undefined && options.many) {
|
|
1228
1237
|
throw new Error('db.isNullable can only be used on single relationships (many: false or undefined). ' +
|
|
1229
1238
|
'Many-side of a relationship has no foreign key column to make non-nullable.');
|
|
@@ -1333,7 +1342,6 @@ export function json(options) {
|
|
|
1333
1342
|
getZodSchema: (fieldName, operation) => {
|
|
1334
1343
|
const validation = options?.validation;
|
|
1335
1344
|
const isRequired = validation?.isRequired;
|
|
1336
|
-
// Accept any valid JSON value
|
|
1337
1345
|
const baseSchema = z.unknown();
|
|
1338
1346
|
if (isRequired && operation === 'create') {
|
|
1339
1347
|
// Required in create mode: a value must be provided and it must be
|
|
@@ -1360,7 +1368,6 @@ export function json(options) {
|
|
|
1360
1368
|
.optional();
|
|
1361
1369
|
}
|
|
1362
1370
|
else {
|
|
1363
|
-
// Not required: can be undefined or null
|
|
1364
1371
|
return baseSchema.optional().nullable();
|
|
1365
1372
|
}
|
|
1366
1373
|
},
|
|
@@ -1370,22 +1377,16 @@ export function json(options) {
|
|
|
1370
1377
|
const isRequired = validation?.isRequired;
|
|
1371
1378
|
const isNullable = db?.isNullable ?? !isRequired;
|
|
1372
1379
|
let modifiers = '';
|
|
1373
|
-
// Optional modifier
|
|
1374
1380
|
if (isNullable) {
|
|
1375
1381
|
modifiers += '?';
|
|
1376
1382
|
}
|
|
1377
|
-
// Native type modifier
|
|
1378
1383
|
if (db?.nativeType) {
|
|
1379
1384
|
modifiers += ` @db.${db.nativeType}`;
|
|
1380
1385
|
}
|
|
1381
|
-
// Default value if provided. Uses Keystone's JSON-literal form: canonical
|
|
1382
|
-
// (space-free) JSON wrapped in escaped double quotes. Independent of the
|
|
1383
|
-
// nullable `?` modifier above — the default never overwrites nullability.
|
|
1384
1386
|
const defaultLiteral = formatPrismaDefault(options?.defaultValue, 'json');
|
|
1385
1387
|
if (defaultLiteral !== undefined) {
|
|
1386
1388
|
modifiers += ` @default(${defaultLiteral})`;
|
|
1387
1389
|
}
|
|
1388
|
-
// Map modifier
|
|
1389
1390
|
if (db?.map) {
|
|
1390
1391
|
modifiers += ` @map("${db.map}")`;
|
|
1391
1392
|
}
|
|
@@ -1414,17 +1415,10 @@ function typeDescriptorToString(descriptor) {
|
|
|
1414
1415
|
if (typeof descriptor === 'string') {
|
|
1415
1416
|
return descriptor;
|
|
1416
1417
|
}
|
|
1417
|
-
// Extract type name from constructor or use provided name
|
|
1418
1418
|
const typeName = descriptor.name || descriptor.value.name;
|
|
1419
|
-
// Generate import string
|
|
1420
1419
|
return `import('${descriptor.from}').${typeName}`;
|
|
1421
1420
|
}
|
|
1422
|
-
/**
|
|
1423
|
-
* Extract TypeScript imports from a TypeDescriptor
|
|
1424
|
-
* Returns array of import statements needed for type generation
|
|
1425
|
-
*/
|
|
1426
1421
|
function typeDescriptorToImports(descriptor) {
|
|
1427
|
-
// If it's a string, check if it's an import string
|
|
1428
1422
|
if (typeof descriptor === 'string') {
|
|
1429
1423
|
const importMatch = descriptor.match(/import\('([^']+)'\)\.(\w+)/);
|
|
1430
1424
|
if (importMatch) {
|
|
@@ -1519,12 +1513,10 @@ function typeDescriptorToImports(descriptor) {
|
|
|
1519
1513
|
* @returns Virtual field configuration
|
|
1520
1514
|
*/
|
|
1521
1515
|
export function virtual(options) {
|
|
1522
|
-
// Validate that resolveOutput is provided
|
|
1523
1516
|
if (!options.hooks?.resolveOutput) {
|
|
1524
1517
|
throw new Error('Virtual fields must provide a resolveOutput hook to compute their value. ' +
|
|
1525
1518
|
'Example: hooks: { resolveOutput: ({ item }) => computeValue(item) }');
|
|
1526
1519
|
}
|
|
1527
|
-
// Convert type descriptor to string
|
|
1528
1520
|
const outputType = typeDescriptorToString(options.type);
|
|
1529
1521
|
const imports = typeDescriptorToImports(options.type);
|
|
1530
1522
|
const { type: _, ...rest } = options;
|
|
@@ -1533,19 +1525,16 @@ export function virtual(options) {
|
|
|
1533
1525
|
virtual: true,
|
|
1534
1526
|
outputType,
|
|
1535
1527
|
...rest,
|
|
1536
|
-
//
|
|
1537
|
-
// Return undefined to signal generator to skip this field
|
|
1528
|
+
// undefined signals the generator to skip creating a database column.
|
|
1538
1529
|
getPrismaType: undefined,
|
|
1539
|
-
// Virtual fields appear in output types with their specified type
|
|
1540
1530
|
getTypeScriptType: () => {
|
|
1541
1531
|
return {
|
|
1542
1532
|
type: outputType,
|
|
1543
|
-
optional: false, //
|
|
1533
|
+
optional: false, // A virtual field always computes a value.
|
|
1544
1534
|
};
|
|
1545
1535
|
},
|
|
1546
|
-
// Add import statements if needed
|
|
1547
1536
|
getTypeScriptImports: imports.length > 0 ? () => imports : undefined,
|
|
1548
|
-
// Virtual fields
|
|
1537
|
+
// Virtual fields don't accept database input, so validation always fails.
|
|
1549
1538
|
getZodSchema: () => {
|
|
1550
1539
|
return z.never();
|
|
1551
1540
|
},
|