@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/src/fields/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
TextField,
|
|
4
4
|
IntegerField,
|
|
5
5
|
DecimalField,
|
|
6
|
+
BigIntField,
|
|
6
7
|
CheckboxField,
|
|
7
8
|
TimestampField,
|
|
8
9
|
CalendarDayField,
|
|
@@ -21,13 +22,9 @@ import { getLabelFieldName } from '../config/label.js'
|
|
|
21
22
|
import type { FilterOperator, FilterSpec } from '../filter/types.js'
|
|
22
23
|
import { RELATIONSHIP_COUNT_FILTER_KEY } from '../filter/types.js'
|
|
23
24
|
|
|
24
|
-
/**
|
|
25
|
-
* Operators shared by numeric/date fields: plain equality plus the four
|
|
26
|
-
* comparisons. `eq` maps to Prisma's `equals`; the comparisons pass through.
|
|
27
|
-
*/
|
|
25
|
+
/** Operators shared by numeric/date fields' `getFilterSpec`. */
|
|
28
26
|
const COMPARISON_OPERATORS: FilterOperator[] = ['eq', 'gt', 'gte', 'lt', 'lte']
|
|
29
27
|
|
|
30
|
-
/** Map a Filter operator to its Prisma condition key (`eq` → `equals`). */
|
|
31
28
|
function prismaComparisonKey(operator: FilterOperator): string {
|
|
32
29
|
return operator === 'eq' ? 'equals' : operator
|
|
33
30
|
}
|
|
@@ -39,6 +36,7 @@ export type {
|
|
|
39
36
|
TextField,
|
|
40
37
|
IntegerField,
|
|
41
38
|
DecimalField,
|
|
39
|
+
BigIntField,
|
|
42
40
|
CheckboxField,
|
|
43
41
|
TimestampField,
|
|
44
42
|
CalendarDayField,
|
|
@@ -53,9 +51,6 @@ export type {
|
|
|
53
51
|
MultiColumnPrismaResult,
|
|
54
52
|
} from '../config/types.js'
|
|
55
53
|
|
|
56
|
-
/**
|
|
57
|
-
* Format field name for display in error messages
|
|
58
|
-
*/
|
|
59
54
|
function formatFieldName(fieldName: string): string {
|
|
60
55
|
return fieldName
|
|
61
56
|
.replace(/([A-Z])/g, ' $1')
|
|
@@ -117,12 +112,10 @@ export function text<
|
|
|
117
112
|
const isNullable = db?.isNullable ?? !isRequired
|
|
118
113
|
let modifiers = ''
|
|
119
114
|
|
|
120
|
-
// Optional modifier
|
|
121
115
|
if (isNullable) {
|
|
122
116
|
modifiers += '?'
|
|
123
117
|
}
|
|
124
118
|
|
|
125
|
-
// Native type modifier (e.g., @db.Text)
|
|
126
119
|
if (db?.nativeType) {
|
|
127
120
|
modifiers += ` @db.${db.nativeType}`
|
|
128
121
|
}
|
|
@@ -151,7 +144,6 @@ export function text<
|
|
|
151
144
|
modifiers += ' @unique'
|
|
152
145
|
}
|
|
153
146
|
|
|
154
|
-
// Map modifier
|
|
155
147
|
if (db?.map) {
|
|
156
148
|
modifiers += ` @map("${db.map}")`
|
|
157
149
|
}
|
|
@@ -171,9 +163,6 @@ export function text<
|
|
|
171
163
|
optional: !isRequired,
|
|
172
164
|
}
|
|
173
165
|
},
|
|
174
|
-
// Text fields drive free-text search: a bare word (or a `field:value`) maps
|
|
175
|
-
// to a case-preserving `contains`. This is what replaces the old hard-coded
|
|
176
|
-
// `type === 'text'` search in the admin list view.
|
|
177
166
|
getFilterSpec: (fieldName: string): FilterSpec => ({
|
|
178
167
|
operators: ['eq'],
|
|
179
168
|
freeText: true,
|
|
@@ -223,24 +212,19 @@ export function integer<
|
|
|
223
212
|
const isNullable = db?.isNullable ?? !isRequired
|
|
224
213
|
let modifiers = ''
|
|
225
214
|
|
|
226
|
-
// Optional modifier
|
|
227
215
|
if (isNullable) {
|
|
228
216
|
modifiers += '?'
|
|
229
217
|
}
|
|
230
218
|
|
|
231
|
-
// Native type modifier (e.g., @db.SmallInt, @db.BigInt)
|
|
232
219
|
if (db?.nativeType) {
|
|
233
220
|
modifiers += ` @db.${db.nativeType}`
|
|
234
221
|
}
|
|
235
222
|
|
|
236
|
-
// Default value if provided (bare numeric literal). Independent of the
|
|
237
|
-
// nullable `?` modifier above — the default never overwrites nullability.
|
|
238
223
|
const defaultLiteral = formatPrismaDefault(options?.defaultValue, 'integer')
|
|
239
224
|
if (defaultLiteral !== undefined) {
|
|
240
225
|
modifiers += ` @default(${defaultLiteral})`
|
|
241
226
|
}
|
|
242
227
|
|
|
243
|
-
// Map modifier
|
|
244
228
|
if (db?.map) {
|
|
245
229
|
modifiers += ` @map("${db.map}")`
|
|
246
230
|
}
|
|
@@ -258,8 +242,7 @@ export function integer<
|
|
|
258
242
|
optional: !isRequired,
|
|
259
243
|
}
|
|
260
244
|
},
|
|
261
|
-
//
|
|
262
|
-
// value can't be interpreted, so its token degrades to free text.
|
|
245
|
+
// A non-integer token can't be interpreted, so it degrades to free text.
|
|
263
246
|
getFilterSpec: (fieldName: string): FilterSpec => ({
|
|
264
247
|
operators: COMPARISON_OPERATORS,
|
|
265
248
|
toCondition: (operator, value) => {
|
|
@@ -327,8 +310,6 @@ export function decimal<
|
|
|
327
310
|
scale,
|
|
328
311
|
...options,
|
|
329
312
|
getZodSchema: (fieldName: string, operation: 'create' | 'update') => {
|
|
330
|
-
// Decimal values can be provided as strings or numbers
|
|
331
|
-
// Prisma will convert them to Decimal instances
|
|
332
313
|
const baseSchema = z.union(
|
|
333
314
|
[
|
|
334
315
|
z.string({
|
|
@@ -345,7 +326,6 @@ export function decimal<
|
|
|
345
326
|
|
|
346
327
|
let schema = baseSchema
|
|
347
328
|
|
|
348
|
-
// Add min validation if specified
|
|
349
329
|
if (options?.validation?.min !== undefined) {
|
|
350
330
|
const minValue = parseFloat(options.validation.min)
|
|
351
331
|
schema = schema.refine(
|
|
@@ -359,7 +339,6 @@ export function decimal<
|
|
|
359
339
|
)
|
|
360
340
|
}
|
|
361
341
|
|
|
362
|
-
// Add max validation if specified
|
|
363
342
|
if (options?.validation?.max !== undefined) {
|
|
364
343
|
const maxValue = parseFloat(options.validation.max)
|
|
365
344
|
schema = schema.refine(
|
|
@@ -385,27 +364,22 @@ export function decimal<
|
|
|
385
364
|
|
|
386
365
|
let modifiers = ''
|
|
387
366
|
|
|
388
|
-
// Optional modifier
|
|
389
367
|
if (isNullable) {
|
|
390
368
|
modifiers += '?'
|
|
391
369
|
}
|
|
392
370
|
|
|
393
|
-
// Precision and scale
|
|
394
371
|
modifiers += ` @db.Decimal(${precision}, ${scale})`
|
|
395
372
|
|
|
396
|
-
// Default value if provided
|
|
397
373
|
if (options?.defaultValue !== undefined) {
|
|
398
374
|
modifiers += ` @default(${options.defaultValue})`
|
|
399
375
|
}
|
|
400
376
|
|
|
401
|
-
// Database mapping
|
|
402
377
|
if (db?.map) {
|
|
403
378
|
modifiers += ` @map("${db.map}")`
|
|
404
379
|
}
|
|
405
380
|
|
|
406
|
-
// Unique modifier
|
|
407
|
-
//
|
|
408
|
-
// generator as `@@index([...])` on the model.
|
|
381
|
+
// Unique modifier — non-unique index routes through `index` below,
|
|
382
|
+
// same as `text()`'s getPrismaType.
|
|
409
383
|
if (options?.isIndexed === 'unique') {
|
|
410
384
|
modifiers += ' @unique'
|
|
411
385
|
}
|
|
@@ -450,6 +424,133 @@ export function decimal<
|
|
|
450
424
|
}
|
|
451
425
|
}
|
|
452
426
|
|
|
427
|
+
/**
|
|
428
|
+
* 64-bit integer field for values that overflow `integer()`'s 32-bit `Int`
|
|
429
|
+
* (e.g. a millisecond epoch). Prisma `BigInt`, TypeScript `bigint`.
|
|
430
|
+
*
|
|
431
|
+
* Accepts `bigint`, an integer `number`, or a numeric `string` on write and
|
|
432
|
+
* always coerces to `bigint`. A `number` above `Number.MAX_SAFE_INTEGER` is
|
|
433
|
+
* rejected rather than coerced — it has already lost precision before this
|
|
434
|
+
* field sees it, so accepting it would reintroduce the exact defect this
|
|
435
|
+
* field exists to prevent. Pass a `bigint` or a string for values beyond that
|
|
436
|
+
* range.
|
|
437
|
+
*
|
|
438
|
+
* Wire representation (ADR-0029): `bigint` in application code, a decimal
|
|
439
|
+
* string over MCP.
|
|
440
|
+
*/
|
|
441
|
+
export function bigInt<
|
|
442
|
+
TTypeInfo extends import('../config/types.js').TypeInfo = import('../config/types.js').TypeInfo,
|
|
443
|
+
>(options?: Omit<BigIntField<TTypeInfo>, 'type'>): BigIntField<TTypeInfo> {
|
|
444
|
+
return {
|
|
445
|
+
type: 'bigInt',
|
|
446
|
+
...options,
|
|
447
|
+
getZodSchema: (fieldName: string, operation: 'create' | 'update') => {
|
|
448
|
+
const validation = options?.validation
|
|
449
|
+
const isRequired = validation?.isRequired
|
|
450
|
+
|
|
451
|
+
const coerced = z
|
|
452
|
+
.union([z.bigint(), z.number(), z.string()], {
|
|
453
|
+
message: `${formatFieldName(fieldName)} must be a bigint, an integer number, or a numeric string`,
|
|
454
|
+
})
|
|
455
|
+
.transform((val, ctx) => {
|
|
456
|
+
if (typeof val === 'bigint') return val
|
|
457
|
+
|
|
458
|
+
if (typeof val === 'number') {
|
|
459
|
+
if (!Number.isInteger(val)) {
|
|
460
|
+
ctx.addIssue(`${formatFieldName(fieldName)} must be an integer`)
|
|
461
|
+
return z.NEVER
|
|
462
|
+
}
|
|
463
|
+
if (!Number.isSafeInteger(val)) {
|
|
464
|
+
ctx.addIssue(
|
|
465
|
+
`${formatFieldName(fieldName)} exceeds Number.MAX_SAFE_INTEGER — pass a bigint or a numeric string instead`,
|
|
466
|
+
)
|
|
467
|
+
return z.NEVER
|
|
468
|
+
}
|
|
469
|
+
return BigInt(val)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const trimmed = val.trim()
|
|
473
|
+
if (!/^-?\d+$/.test(trimmed)) {
|
|
474
|
+
ctx.addIssue(`${formatFieldName(fieldName)} must be an integer`)
|
|
475
|
+
return z.NEVER
|
|
476
|
+
}
|
|
477
|
+
return BigInt(trimmed)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
let schema = coerced
|
|
481
|
+
|
|
482
|
+
if (validation?.min !== undefined) {
|
|
483
|
+
const min = validation.min
|
|
484
|
+
schema = schema.refine((val) => val >= min, {
|
|
485
|
+
message: `${formatFieldName(fieldName)} must be at least ${min}`,
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (validation?.max !== undefined) {
|
|
490
|
+
const max = validation.max
|
|
491
|
+
schema = schema.refine((val) => val <= max, {
|
|
492
|
+
message: `${formatFieldName(fieldName)} must be at most ${max}`,
|
|
493
|
+
})
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return !isRequired || operation === 'update' ? schema.optional().nullable() : schema
|
|
497
|
+
},
|
|
498
|
+
getPrismaType: (_fieldName: string) => {
|
|
499
|
+
const validation = options?.validation
|
|
500
|
+
const db = options?.db
|
|
501
|
+
const isRequired = validation?.isRequired
|
|
502
|
+
const isNullable = db?.isNullable ?? !isRequired
|
|
503
|
+
let modifiers = ''
|
|
504
|
+
|
|
505
|
+
if (isNullable) {
|
|
506
|
+
modifiers += '?'
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (db?.nativeType) {
|
|
510
|
+
modifiers += ` @db.${db.nativeType}`
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (options?.defaultValue !== undefined) {
|
|
514
|
+
modifiers += ` @default(${options.defaultValue})`
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (db?.map) {
|
|
518
|
+
modifiers += ` @map("${db.map}")`
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Unique modifier — non-unique index routes through `index` below,
|
|
522
|
+
// same as `text()`'s getPrismaType.
|
|
523
|
+
if (options?.isIndexed === 'unique') {
|
|
524
|
+
modifiers += ' @unique'
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
type: 'BigInt',
|
|
529
|
+
modifiers: modifiers.trimStart() || undefined,
|
|
530
|
+
index: options?.isIndexed === true ? true : undefined,
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
getTypeScriptType: () => {
|
|
534
|
+
const isRequired = options?.validation?.isRequired
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
type: 'bigint',
|
|
538
|
+
optional: !isRequired,
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
// A non-integer token degrades to free text.
|
|
542
|
+
getFilterSpec: (fieldName: string): FilterSpec => ({
|
|
543
|
+
operators: COMPARISON_OPERATORS,
|
|
544
|
+
toCondition: (operator, value) => {
|
|
545
|
+
const trimmed = value.trim()
|
|
546
|
+
if (!/^-?\d+$/.test(trimmed)) return null
|
|
547
|
+
return { [fieldName]: { [prismaComparisonKey(operator)]: BigInt(trimmed) } }
|
|
548
|
+
},
|
|
549
|
+
suggestions: { valueSource: { kind: 'none' } },
|
|
550
|
+
}),
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
453
554
|
/**
|
|
454
555
|
* Checkbox (boolean) field
|
|
455
556
|
*/
|
|
@@ -467,8 +568,9 @@ export function checkbox<
|
|
|
467
568
|
const hasDefault = options?.defaultValue !== undefined
|
|
468
569
|
let modifiers = ''
|
|
469
570
|
|
|
470
|
-
//
|
|
471
|
-
//
|
|
571
|
+
// Checkboxes are non-nullable by default (must be true or false), unlike
|
|
572
|
+
// the other scalar fields' nullable-unless-required default — set
|
|
573
|
+
// db.isNullable: true to allow NULL.
|
|
472
574
|
if (db?.isNullable === true) {
|
|
473
575
|
modifiers += '?'
|
|
474
576
|
}
|
|
@@ -477,7 +579,6 @@ export function checkbox<
|
|
|
477
579
|
modifiers += ` @default(${options.defaultValue})`
|
|
478
580
|
}
|
|
479
581
|
|
|
480
|
-
// Map modifier
|
|
481
582
|
if (db?.map) {
|
|
482
583
|
modifiers += ` @map("${db.map}")`
|
|
483
584
|
}
|
|
@@ -493,8 +594,7 @@ export function checkbox<
|
|
|
493
594
|
optional: options?.defaultValue === undefined,
|
|
494
595
|
}
|
|
495
596
|
},
|
|
496
|
-
//
|
|
497
|
-
// other than true/false can't be interpreted and degrades to free text.
|
|
597
|
+
// Anything other than true/false degrades to free text.
|
|
498
598
|
getFilterSpec: (fieldName: string): FilterSpec => ({
|
|
499
599
|
operators: ['eq'],
|
|
500
600
|
toCondition: (operator, value) => {
|
|
@@ -537,27 +637,22 @@ export function timestamp<
|
|
|
537
637
|
'kind' in options.defaultValue &&
|
|
538
638
|
options.defaultValue.kind === 'now'
|
|
539
639
|
|
|
540
|
-
// Nullability: explicit db.isNullable overrides the default (nullable unless @default(now()))
|
|
541
640
|
const isNullable = db?.isNullable ?? !hasDefaultNow
|
|
542
641
|
|
|
543
642
|
let modifiers = ''
|
|
544
643
|
|
|
545
|
-
// Optional modifier
|
|
546
644
|
if (isNullable) {
|
|
547
645
|
modifiers += '?'
|
|
548
646
|
}
|
|
549
647
|
|
|
550
|
-
// Default value
|
|
551
648
|
if (hasDefaultNow) {
|
|
552
649
|
modifiers += ' @default(now())'
|
|
553
650
|
}
|
|
554
651
|
|
|
555
|
-
// Native type modifier (e.g., @db.Timestamptz for PostgreSQL)
|
|
556
652
|
if (db?.nativeType) {
|
|
557
653
|
modifiers += ` @db.${db.nativeType}`
|
|
558
654
|
}
|
|
559
655
|
|
|
560
|
-
// Map modifier
|
|
561
656
|
if (db?.map) {
|
|
562
657
|
modifiers += ` @map("${db.map}")`
|
|
563
658
|
}
|
|
@@ -579,8 +674,7 @@ export function timestamp<
|
|
|
579
674
|
optional: !hasDefault,
|
|
580
675
|
}
|
|
581
676
|
},
|
|
582
|
-
//
|
|
583
|
-
// unparseable date degrades to free text.
|
|
677
|
+
// An unparseable date degrades to free text.
|
|
584
678
|
getFilterSpec: (fieldName: string): FilterSpec => ({
|
|
585
679
|
operators: COMPARISON_OPERATORS,
|
|
586
680
|
toCondition: (operator, value) => {
|
|
@@ -659,23 +753,11 @@ export function calendarDay<
|
|
|
659
753
|
return {
|
|
660
754
|
type: 'calendarDay',
|
|
661
755
|
...options,
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
// UTC-midnight Date; leave anything else (a Date already, null/undefined,
|
|
668
|
-
// or a malformed string) untouched so the zod schema below still rejects
|
|
669
|
-
// malformed input with a clear message. Reads resolvedData[fieldKey]
|
|
670
|
-
// (not raw inputData) so a list-level resolveInput that injects a default
|
|
671
|
-
// for an omitted key is still coerced instead of being overwritten.
|
|
672
|
-
//
|
|
673
|
-
// Reads: the underlying @db.Date column hands Prisma a Date (or a TEXT
|
|
674
|
-
// string under the SQLite fallback). Normalise to a YYYY-MM-DD string so the
|
|
675
|
-
// runtime value matches the declared `string` type. UTC components are used
|
|
676
|
-
// so the formatting never drifts a day in non-UTC timezones.
|
|
677
|
-
// Cast hooks to any since field builders are generic and can't know the
|
|
678
|
-
// specific TFieldKey (same pattern as password()).
|
|
756
|
+
// Hook Pipeline runs field resolveInput before zod validation — the only
|
|
757
|
+
// point a YYYY-MM-DD string can be turned into what Prisma's `@db.Date`
|
|
758
|
+
// write validator accepts (#621). Reads resolvedData[fieldKey], not raw
|
|
759
|
+
// inputData, so a list-level resolveInput's injected default for an
|
|
760
|
+
// omitted key is still coerced rather than overwritten.
|
|
679
761
|
hooks: {
|
|
680
762
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Field builder hooks must be generic
|
|
681
763
|
resolveInput: ({ resolvedData, fieldKey }: { resolvedData: any; fieldKey: string }) => {
|
|
@@ -688,7 +770,6 @@ export function calendarDay<
|
|
|
688
770
|
},
|
|
689
771
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Field builder hooks must be generic
|
|
690
772
|
resolveOutput: ({ value }: { value: any }) => formatCalendarDay(value),
|
|
691
|
-
// Merge with user-provided hooks if any
|
|
692
773
|
...options?.hooks,
|
|
693
774
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Hook object needs type assertion for field builder
|
|
694
775
|
} as any,
|
|
@@ -696,10 +777,9 @@ export function calendarDay<
|
|
|
696
777
|
const validation = options?.validation
|
|
697
778
|
const isRequired = validation?.isRequired
|
|
698
779
|
|
|
699
|
-
//
|
|
700
|
-
//
|
|
701
|
-
//
|
|
702
|
-
// untouched and still fail the regex here with a clear message.
|
|
780
|
+
// Accepts a `Date` because resolveInput above already converted a valid
|
|
781
|
+
// string to one before this schema runs; a malformed string falls
|
|
782
|
+
// through resolveInput untouched and fails the regex here instead.
|
|
703
783
|
const stringSchema = z
|
|
704
784
|
.string({
|
|
705
785
|
message: `${formatFieldName(fieldName)} must be a valid date in ISO8601 format (YYYY-MM-DD)`,
|
|
@@ -713,7 +793,6 @@ export function calendarDay<
|
|
|
713
793
|
if (isRequired && operation === 'create') {
|
|
714
794
|
return dateSchema
|
|
715
795
|
} else if (isRequired && operation === 'update') {
|
|
716
|
-
// Required in update mode: omitted keys pass; present values must be valid
|
|
717
796
|
return dateSchema.optional()
|
|
718
797
|
} else {
|
|
719
798
|
return dateSchema.optional().nullable()
|
|
@@ -727,31 +806,26 @@ export function calendarDay<
|
|
|
727
806
|
|
|
728
807
|
let modifiers = ''
|
|
729
808
|
|
|
730
|
-
// Optional modifier
|
|
731
809
|
if (isNullable) {
|
|
732
810
|
modifiers += '?'
|
|
733
811
|
}
|
|
734
812
|
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
// SQLite will use TEXT for DateTime fields
|
|
813
|
+
// SQLite has no native DATE type and falls back to TEXT for DateTime
|
|
814
|
+
// columns, so @db.Date only applies on PostgreSQL/MySQL.
|
|
738
815
|
if (provider && provider.toLowerCase() !== 'sqlite') {
|
|
739
816
|
modifiers += ' @db.Date'
|
|
740
817
|
}
|
|
741
818
|
|
|
742
|
-
// Default value if provided
|
|
743
819
|
if (options?.defaultValue !== undefined) {
|
|
744
820
|
modifiers += ` @default("${options.defaultValue}")`
|
|
745
821
|
}
|
|
746
822
|
|
|
747
|
-
// Database mapping
|
|
748
823
|
if (db?.map) {
|
|
749
824
|
modifiers += ` @map("${db.map}")`
|
|
750
825
|
}
|
|
751
826
|
|
|
752
|
-
// Unique modifier
|
|
753
|
-
//
|
|
754
|
-
// generator as `@@index([...])` on the model.
|
|
827
|
+
// Unique modifier — non-unique index routes through `index` below,
|
|
828
|
+
// same as `text()`'s getPrismaType.
|
|
755
829
|
if (options?.isIndexed === 'unique') {
|
|
756
830
|
modifiers += ' @unique'
|
|
757
831
|
}
|
|
@@ -768,12 +842,6 @@ export function calendarDay<
|
|
|
768
842
|
const isRequired = validation?.isRequired
|
|
769
843
|
const isNullable = db?.isNullable ?? !isRequired
|
|
770
844
|
|
|
771
|
-
// calendarDay is a YYYY-MM-DD string end-to-end (Keystone's CalendarDay
|
|
772
|
-
// scalar). Returning 'string' here makes the entity/read type and the
|
|
773
|
-
// standalone generated CreateInput/UpdateInput types `string`. At the
|
|
774
|
-
// context.db write path a Date is still rejected at runtime by validation
|
|
775
|
-
// (the generated db method `data` type derives from Prisma's `Date | string`
|
|
776
|
-
// input — making it a compile-time error is tracked in #599).
|
|
777
845
|
return {
|
|
778
846
|
type: 'string',
|
|
779
847
|
optional: isNullable,
|
|
@@ -884,46 +952,37 @@ export function password<TTypeInfo extends import('../config/types.js').TypeInfo
|
|
|
884
952
|
...options,
|
|
885
953
|
resultExtension: {
|
|
886
954
|
outputType: "import('@opensaas/stack-core/internal').HashedPassword",
|
|
887
|
-
// No compute - delegates to resolveOutput hook
|
|
888
955
|
},
|
|
889
956
|
ui: {
|
|
890
957
|
...options?.ui,
|
|
891
958
|
valueForClientSerialization: ({ value }) => ({ isSet: !!value }),
|
|
892
959
|
},
|
|
893
|
-
// Cast hooks to any since field builders are generic and can't know the specific TFieldKey
|
|
894
960
|
hooks: {
|
|
895
|
-
// Hash password before writing to database
|
|
896
961
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Field builder hooks must be generic
|
|
897
962
|
resolveInput: async ({ inputData, fieldKey }: { inputData: any; fieldKey: string }) => {
|
|
898
|
-
// Skip if undefined or null (allows partial updates)
|
|
899
963
|
const inputValue = inputData[fieldKey]
|
|
900
964
|
if (inputValue === undefined || inputValue === null) {
|
|
901
965
|
return inputValue
|
|
902
966
|
}
|
|
903
967
|
|
|
904
|
-
// Skip if not a string
|
|
905
968
|
if (typeof inputValue !== 'string' || inputValue.length === 0) {
|
|
906
969
|
return inputValue
|
|
907
970
|
}
|
|
908
971
|
|
|
909
|
-
//
|
|
972
|
+
// Idempotent: skip re-hashing a value that's already a hash.
|
|
910
973
|
if (isHashedPassword(inputValue)) {
|
|
911
974
|
return inputValue
|
|
912
975
|
}
|
|
913
976
|
|
|
914
|
-
// Hash the password
|
|
915
977
|
return (await hashPassword(inputValue)).toString()
|
|
916
978
|
},
|
|
917
|
-
// Wrap password with HashedPassword class after reading from database
|
|
918
979
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Field builder hooks must be generic
|
|
919
980
|
resolveOutput: ({ value }: { value: any }) => {
|
|
920
|
-
// Only wrap string values (hashed passwords)
|
|
921
981
|
if (typeof value === 'string' && value.length > 0) {
|
|
922
982
|
return new HashedPassword(value)
|
|
923
983
|
}
|
|
924
984
|
return undefined
|
|
925
985
|
},
|
|
926
|
-
// Merge with user-provided hooks if any
|
|
927
986
|
...options?.hooks,
|
|
928
987
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Hook object needs type assertion for field builder
|
|
929
988
|
} as any,
|
|
@@ -932,7 +991,6 @@ export function password<TTypeInfo extends import('../config/types.js').TypeInfo
|
|
|
932
991
|
const isRequired = validation?.isRequired
|
|
933
992
|
|
|
934
993
|
if (isRequired && operation === 'create') {
|
|
935
|
-
// Required in create mode: reject undefined and empty strings
|
|
936
994
|
return z
|
|
937
995
|
.string({
|
|
938
996
|
message: `${formatFieldName(fieldName)} must be text`,
|
|
@@ -941,7 +999,6 @@ export function password<TTypeInfo extends import('../config/types.js').TypeInfo
|
|
|
941
999
|
message: `${formatFieldName(fieldName)} is required`,
|
|
942
1000
|
})
|
|
943
1001
|
} else if (isRequired && operation === 'update') {
|
|
944
|
-
// Required in update mode: omitted keys pass; if provided, reject empty strings
|
|
945
1002
|
return z
|
|
946
1003
|
.string()
|
|
947
1004
|
.min(1, {
|
|
@@ -949,7 +1006,6 @@ export function password<TTypeInfo extends import('../config/types.js').TypeInfo
|
|
|
949
1006
|
})
|
|
950
1007
|
.optional()
|
|
951
1008
|
} else {
|
|
952
|
-
// Not required: can be undefined or any string
|
|
953
1009
|
return z
|
|
954
1010
|
.string({
|
|
955
1011
|
message: `${formatFieldName(fieldName)} must be text`,
|
|
@@ -965,17 +1021,14 @@ export function password<TTypeInfo extends import('../config/types.js').TypeInfo
|
|
|
965
1021
|
const isNullable = db?.isNullable ?? !isRequired
|
|
966
1022
|
let modifiers = ''
|
|
967
1023
|
|
|
968
|
-
// Optional modifier
|
|
969
1024
|
if (isNullable) {
|
|
970
1025
|
modifiers += '?'
|
|
971
1026
|
}
|
|
972
1027
|
|
|
973
|
-
// Native type modifier (e.g., @db.Text)
|
|
974
1028
|
if (db?.nativeType) {
|
|
975
1029
|
modifiers += ` @db.${db.nativeType}`
|
|
976
1030
|
}
|
|
977
1031
|
|
|
978
|
-
// Map modifier
|
|
979
1032
|
if (db?.map) {
|
|
980
1033
|
modifiers += ` @map("${db.map}")`
|
|
981
1034
|
}
|
|
@@ -1055,7 +1108,6 @@ export function select<
|
|
|
1055
1108
|
const isNullable = options.db?.isNullable ?? (!isRequired && !hasDefault)
|
|
1056
1109
|
let modifiers = ''
|
|
1057
1110
|
|
|
1058
|
-
// Optional modifier
|
|
1059
1111
|
if (isNullable) {
|
|
1060
1112
|
modifiers += '?'
|
|
1061
1113
|
}
|
|
@@ -1068,12 +1120,11 @@ export function select<
|
|
|
1068
1120
|
const derivedEnumName = listName ? `${listName}${capitalizedField}` : capitalizedField
|
|
1069
1121
|
const enumName = options.db?.enumName ?? derivedEnumName
|
|
1070
1122
|
|
|
1071
|
-
//
|
|
1123
|
+
// No quotes for enum default values (unlike the string branch below).
|
|
1072
1124
|
if (hasDefault) {
|
|
1073
1125
|
modifiers += ` @default(${options.defaultValue})`
|
|
1074
1126
|
}
|
|
1075
1127
|
|
|
1076
|
-
// Map modifier
|
|
1077
1128
|
if (options.db?.map) {
|
|
1078
1129
|
modifiers += ` @map("${options.db.map}")`
|
|
1079
1130
|
}
|
|
@@ -1087,12 +1138,10 @@ export function select<
|
|
|
1087
1138
|
|
|
1088
1139
|
// String type (default)
|
|
1089
1140
|
|
|
1090
|
-
// Add default value if provided
|
|
1091
1141
|
if (hasDefault) {
|
|
1092
1142
|
modifiers += ` @default("${options.defaultValue}")`
|
|
1093
1143
|
}
|
|
1094
1144
|
|
|
1095
|
-
// Map modifier
|
|
1096
1145
|
if (options.db?.map) {
|
|
1097
1146
|
modifiers += ` @map("${options.db.map}")`
|
|
1098
1147
|
}
|
|
@@ -1159,9 +1208,6 @@ function parseRelationshipRef(ref: string): { list: string; field?: string } {
|
|
|
1159
1208
|
}
|
|
1160
1209
|
}
|
|
1161
1210
|
|
|
1162
|
-
/**
|
|
1163
|
-
* Check if a relationship is one-to-one (bidirectional with both sides having many: false).
|
|
1164
|
-
*/
|
|
1165
1211
|
function isOneToOneRelationship(
|
|
1166
1212
|
fieldName: string,
|
|
1167
1213
|
field: RelationshipField,
|
|
@@ -1194,10 +1240,6 @@ function isOneToOneRelationship(
|
|
|
1194
1240
|
return !(targetFieldConfig as RelationshipField).many
|
|
1195
1241
|
}
|
|
1196
1242
|
|
|
1197
|
-
/**
|
|
1198
|
-
* Determine if this side of a relationship should store the foreign key.
|
|
1199
|
-
* For one-to-one relationships, only one side stores the foreign key.
|
|
1200
|
-
*/
|
|
1201
1243
|
function shouldHaveForeignKey(
|
|
1202
1244
|
listKey: string,
|
|
1203
1245
|
fieldName: string,
|
|
@@ -1248,9 +1290,6 @@ function shouldHaveForeignKey(
|
|
|
1248
1290
|
return fieldName.localeCompare(targetField) < 0
|
|
1249
1291
|
}
|
|
1250
1292
|
|
|
1251
|
-
/**
|
|
1252
|
-
* Check whether a many relationship is a true many-to-many (both sides many).
|
|
1253
|
-
*/
|
|
1254
1293
|
function isManyToMany(
|
|
1255
1294
|
fieldName: string,
|
|
1256
1295
|
field: RelationshipField,
|
|
@@ -1324,13 +1363,9 @@ function computeManyToManyRelationName(
|
|
|
1324
1363
|
return `${listKey}_${fieldName}`
|
|
1325
1364
|
}
|
|
1326
1365
|
|
|
1327
|
-
// Default Prisma naming - no explicit relation name needed
|
|
1328
1366
|
return undefined
|
|
1329
1367
|
}
|
|
1330
1368
|
|
|
1331
|
-
/**
|
|
1332
|
-
* Build the Prisma schema contribution for a relationship field.
|
|
1333
|
-
*/
|
|
1334
1369
|
function getPrismaRelation(
|
|
1335
1370
|
field: RelationshipField,
|
|
1336
1371
|
fieldName: string,
|
|
@@ -1444,7 +1479,6 @@ export function relationship<
|
|
|
1444
1479
|
throw new Error('Relationship field must have a ref')
|
|
1445
1480
|
}
|
|
1446
1481
|
|
|
1447
|
-
// Validate ref format: 'ListName.fieldName' or 'ListName'
|
|
1448
1482
|
const refParts = options.ref.split('.')
|
|
1449
1483
|
if (refParts.length !== 1 && refParts.length !== 2) {
|
|
1450
1484
|
throw new Error(
|
|
@@ -1452,9 +1486,7 @@ export function relationship<
|
|
|
1452
1486
|
)
|
|
1453
1487
|
}
|
|
1454
1488
|
|
|
1455
|
-
// Validate db.foreignKey usage
|
|
1456
1489
|
if (options.db?.foreignKey !== undefined) {
|
|
1457
|
-
// Can only be used on single relationships (not many)
|
|
1458
1490
|
if (options.many) {
|
|
1459
1491
|
throw new Error(
|
|
1460
1492
|
'db.foreignKey can only be used on single relationships (many: false or undefined). ' +
|
|
@@ -1462,7 +1494,6 @@ export function relationship<
|
|
|
1462
1494
|
)
|
|
1463
1495
|
}
|
|
1464
1496
|
|
|
1465
|
-
// Can only be used on bidirectional relationships (with target field)
|
|
1466
1497
|
if (refParts.length === 1) {
|
|
1467
1498
|
throw new Error(
|
|
1468
1499
|
'db.foreignKey can only be used on bidirectional relationships (ref: "ListName.fieldName"). ' +
|
|
@@ -1471,9 +1502,6 @@ export function relationship<
|
|
|
1471
1502
|
}
|
|
1472
1503
|
}
|
|
1473
1504
|
|
|
1474
|
-
// Validate db.isNullable usage: only the FK-owning (single) side of a
|
|
1475
|
-
// relationship has a column to make non-nullable — the many side always
|
|
1476
|
-
// generates an array field with no nullability of its own.
|
|
1477
1505
|
if (options.db?.isNullable !== undefined && options.many) {
|
|
1478
1506
|
throw new Error(
|
|
1479
1507
|
'db.isNullable can only be used on single relationships (many: false or undefined). ' +
|
|
@@ -1601,7 +1629,6 @@ export function json<
|
|
|
1601
1629
|
const validation = options?.validation
|
|
1602
1630
|
const isRequired = validation?.isRequired
|
|
1603
1631
|
|
|
1604
|
-
// Accept any valid JSON value
|
|
1605
1632
|
const baseSchema = z.unknown()
|
|
1606
1633
|
|
|
1607
1634
|
if (isRequired && operation === 'create') {
|
|
@@ -1627,7 +1654,6 @@ export function json<
|
|
|
1627
1654
|
})
|
|
1628
1655
|
.optional()
|
|
1629
1656
|
} else {
|
|
1630
|
-
// Not required: can be undefined or null
|
|
1631
1657
|
return baseSchema.optional().nullable()
|
|
1632
1658
|
}
|
|
1633
1659
|
},
|
|
@@ -1638,25 +1664,19 @@ export function json<
|
|
|
1638
1664
|
const isNullable = db?.isNullable ?? !isRequired
|
|
1639
1665
|
let modifiers = ''
|
|
1640
1666
|
|
|
1641
|
-
// Optional modifier
|
|
1642
1667
|
if (isNullable) {
|
|
1643
1668
|
modifiers += '?'
|
|
1644
1669
|
}
|
|
1645
1670
|
|
|
1646
|
-
// Native type modifier
|
|
1647
1671
|
if (db?.nativeType) {
|
|
1648
1672
|
modifiers += ` @db.${db.nativeType}`
|
|
1649
1673
|
}
|
|
1650
1674
|
|
|
1651
|
-
// Default value if provided. Uses Keystone's JSON-literal form: canonical
|
|
1652
|
-
// (space-free) JSON wrapped in escaped double quotes. Independent of the
|
|
1653
|
-
// nullable `?` modifier above — the default never overwrites nullability.
|
|
1654
1675
|
const defaultLiteral = formatPrismaDefault(options?.defaultValue, 'json')
|
|
1655
1676
|
if (defaultLiteral !== undefined) {
|
|
1656
1677
|
modifiers += ` @default(${defaultLiteral})`
|
|
1657
1678
|
}
|
|
1658
1679
|
|
|
1659
|
-
// Map modifier
|
|
1660
1680
|
if (db?.map) {
|
|
1661
1681
|
modifiers += ` @map("${db.map}")`
|
|
1662
1682
|
}
|
|
@@ -1689,21 +1709,14 @@ function typeDescriptorToString(descriptor: import('../config/types.js').TypeDes
|
|
|
1689
1709
|
return descriptor
|
|
1690
1710
|
}
|
|
1691
1711
|
|
|
1692
|
-
// Extract type name from constructor or use provided name
|
|
1693
1712
|
const typeName = descriptor.name || descriptor.value.name
|
|
1694
1713
|
|
|
1695
|
-
// Generate import string
|
|
1696
1714
|
return `import('${descriptor.from}').${typeName}`
|
|
1697
1715
|
}
|
|
1698
1716
|
|
|
1699
|
-
/**
|
|
1700
|
-
* Extract TypeScript imports from a TypeDescriptor
|
|
1701
|
-
* Returns array of import statements needed for type generation
|
|
1702
|
-
*/
|
|
1703
1717
|
function typeDescriptorToImports(
|
|
1704
1718
|
descriptor: import('../config/types.js').TypeDescriptor,
|
|
1705
1719
|
): Array<{ names: string[]; from: string; typeOnly?: boolean }> {
|
|
1706
|
-
// If it's a string, check if it's an import string
|
|
1707
1720
|
if (typeof descriptor === 'string') {
|
|
1708
1721
|
const importMatch = descriptor.match(/import\('([^']+)'\)\.(\w+)/)
|
|
1709
1722
|
if (importMatch) {
|
|
@@ -1804,7 +1817,6 @@ export function virtual<TTypeInfo extends import('../config/types.js').TypeInfo>
|
|
|
1804
1817
|
type: import('../config/types.js').TypeDescriptor
|
|
1805
1818
|
},
|
|
1806
1819
|
): VirtualField<TTypeInfo> {
|
|
1807
|
-
// Validate that resolveOutput is provided
|
|
1808
1820
|
if (!options.hooks?.resolveOutput) {
|
|
1809
1821
|
throw new Error(
|
|
1810
1822
|
'Virtual fields must provide a resolveOutput hook to compute their value. ' +
|
|
@@ -1812,7 +1824,6 @@ export function virtual<TTypeInfo extends import('../config/types.js').TypeInfo>
|
|
|
1812
1824
|
)
|
|
1813
1825
|
}
|
|
1814
1826
|
|
|
1815
|
-
// Convert type descriptor to string
|
|
1816
1827
|
const outputType = typeDescriptorToString(options.type)
|
|
1817
1828
|
const imports = typeDescriptorToImports(options.type)
|
|
1818
1829
|
|
|
@@ -1823,19 +1834,16 @@ export function virtual<TTypeInfo extends import('../config/types.js').TypeInfo>
|
|
|
1823
1834
|
virtual: true,
|
|
1824
1835
|
outputType,
|
|
1825
1836
|
...rest,
|
|
1826
|
-
//
|
|
1827
|
-
// Return undefined to signal generator to skip this field
|
|
1837
|
+
// undefined signals the generator to skip creating a database column.
|
|
1828
1838
|
getPrismaType: undefined,
|
|
1829
|
-
// Virtual fields appear in output types with their specified type
|
|
1830
1839
|
getTypeScriptType: () => {
|
|
1831
1840
|
return {
|
|
1832
1841
|
type: outputType,
|
|
1833
|
-
optional: false, //
|
|
1842
|
+
optional: false, // A virtual field always computes a value.
|
|
1834
1843
|
}
|
|
1835
1844
|
},
|
|
1836
|
-
// Add import statements if needed
|
|
1837
1845
|
getTypeScriptImports: imports.length > 0 ? () => imports : undefined,
|
|
1838
|
-
// Virtual fields
|
|
1846
|
+
// Virtual fields don't accept database input, so validation always fails.
|
|
1839
1847
|
getZodSchema: () => {
|
|
1840
1848
|
return z.never()
|
|
1841
1849
|
},
|