@opensaas/stack-core 0.20.1 → 0.22.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 +334 -0
- package/CLAUDE.md +29 -11
- 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 +178 -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/access/multi-column-read-write.test.d.ts +2 -0
- package/dist/access/multi-column-read-write.test.d.ts.map +1 -0
- package/dist/access/multi-column-read-write.test.js +149 -0
- package/dist/access/multi-column-read-write.test.js.map +1 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/types.d.ts +334 -5
- 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/format-prisma-default.d.ts +35 -0
- package/dist/fields/format-prisma-default.d.ts.map +1 -0
- package/dist/fields/format-prisma-default.js +52 -0
- package/dist/fields/format-prisma-default.js.map +1 -0
- package/dist/fields/format-prisma-default.test.d.ts +2 -0
- package/dist/fields/format-prisma-default.test.d.ts.map +1 -0
- package/dist/fields/format-prisma-default.test.js +54 -0
- package/dist/fields/format-prisma-default.test.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 +267 -18
- package/dist/fields/index.js.map +1 -1
- package/dist/fields/select.test.js +85 -0
- package/dist/fields/select.test.js.map +1 -1
- package/dist/fields/text-keystone-compat.test.d.ts +2 -0
- package/dist/fields/text-keystone-compat.test.d.ts.map +1 -0
- package/dist/fields/text-keystone-compat.test.js +93 -0
- package/dist/fields/text-keystone-compat.test.js.map +1 -0
- package/dist/hooks/index.d.ts +20 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +246 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -9
- package/dist/index.js.map +1 -1
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +33 -0
- package/dist/index.test.js.map +1 -0
- 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/mcp/handler.js +0 -1
- package/dist/mcp/handler.js.map +1 -1
- 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 +11 -3
- 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 +269 -0
- package/src/access/index.ts +7 -4
- package/src/access/multi-column-read-write.test.ts +255 -0
- package/src/config/index.ts +3 -0
- package/src/config/types.ts +342 -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 +19 -0
- package/src/fields/format-prisma-default.test.ts +64 -0
- package/src/fields/format-prisma-default.ts +67 -0
- package/src/fields/index.ts +375 -20
- package/src/fields/select.test.ts +99 -0
- package/src/fields/text-keystone-compat.test.ts +126 -0
- package/src/hooks/index.ts +270 -0
- package/src/index.test.ts +50 -0
- package/src/index.ts +35 -82
- package/src/internal.ts +49 -0
- package/src/mcp/handler.ts +0 -2
- 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
|
@@ -4,54 +4,12 @@ import { checkAccess, filterWritableFields, getRelatedListConfig } from '../acce
|
|
|
4
4
|
import {
|
|
5
5
|
executeResolveInput,
|
|
6
6
|
executeValidate,
|
|
7
|
+
executeFieldResolveInputHooks,
|
|
7
8
|
validateFieldRules,
|
|
8
9
|
ValidationError,
|
|
9
10
|
} from '../hooks/index.js'
|
|
10
11
|
import { getDbKey } from '../lib/case-utils.js'
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
-
* Execute field-level resolveInput hooks
|
|
14
|
-
* Allows fields to transform their input values before database write
|
|
15
|
-
*/
|
|
16
|
-
async function executeFieldResolveInputHooks(
|
|
17
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
-
inputData: Record<string, any>,
|
|
19
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
-
resolvedData: Record<string, any>,
|
|
21
|
-
fields: Record<string, FieldConfig>,
|
|
22
|
-
operation: 'create' | 'update',
|
|
23
|
-
context: AccessContext,
|
|
24
|
-
listKey: string,
|
|
25
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
-
item?: any,
|
|
27
|
-
): Promise<Record<string, unknown>> {
|
|
28
|
-
let result = { ...resolvedData }
|
|
29
|
-
|
|
30
|
-
for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
|
|
31
|
-
// Skip if field not in data
|
|
32
|
-
if (!(fieldKey in result)) continue
|
|
33
|
-
|
|
34
|
-
// Skip if no hooks defined
|
|
35
|
-
if (!fieldConfig.hooks?.resolveInput) continue
|
|
36
|
-
|
|
37
|
-
// Execute field hook
|
|
38
|
-
const transformedValue = await fieldConfig.hooks.resolveInput({
|
|
39
|
-
listKey,
|
|
40
|
-
fieldKey,
|
|
41
|
-
operation,
|
|
42
|
-
inputData,
|
|
43
|
-
item,
|
|
44
|
-
resolvedData: { ...result }, // Pass a copy to avoid mutation affecting recorded args
|
|
45
|
-
context,
|
|
46
|
-
} as Parameters<typeof fieldConfig.hooks.resolveInput>[0])
|
|
47
|
-
|
|
48
|
-
// Create new object with updated field to avoid mutating the passed reference
|
|
49
|
-
result = { ...result, [fieldKey]: transformedValue }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return result
|
|
53
|
-
}
|
|
54
|
-
|
|
55
13
|
/**
|
|
56
14
|
* Check if a field config is a relationship field
|
|
57
15
|
*/
|
|
@@ -396,6 +354,139 @@ async function processNestedConnectOrCreate(
|
|
|
396
354
|
return Array.isArray(operations) ? processedOps : processedOps[0]
|
|
397
355
|
}
|
|
398
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Arguments passed to every nested-operation handler.
|
|
359
|
+
*
|
|
360
|
+
* A handler receives the raw value supplied for a single nested-op kind
|
|
361
|
+
* (e.g. the contents of `value.create`) alongside everything it needs to apply
|
|
362
|
+
* hooks, access control, and recursion.
|
|
363
|
+
*/
|
|
364
|
+
interface NestedOpHandlerArgs {
|
|
365
|
+
/** Raw payload supplied for this nested-op kind (e.g. the value of `value.create`). */
|
|
366
|
+
value: unknown
|
|
367
|
+
/** The list name of the related model (e.g. `'User'`). */
|
|
368
|
+
relatedListName: string
|
|
369
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
370
|
+
relatedListConfig: ListConfig<any>
|
|
371
|
+
context: AccessContext
|
|
372
|
+
config: OpenSaasConfig
|
|
373
|
+
/** Prisma client used for dynamic model access during access checks. */
|
|
374
|
+
prisma: unknown
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* A nested-operation handler describes how a single nested-op kind
|
|
379
|
+
* (`create`, `connect`, …) is processed before it reaches Prisma.
|
|
380
|
+
*
|
|
381
|
+
* Adding support for a new nested-op kind means registering a new entry in
|
|
382
|
+
* {@link nestedOpRegistry}, not editing the dispatch loop.
|
|
383
|
+
*/
|
|
384
|
+
interface NestedOpHandler {
|
|
385
|
+
/** Produce the processed payload for this nested-op kind. */
|
|
386
|
+
execute(args: NestedOpHandlerArgs): Promise<unknown>
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Registry of nested-operation handlers keyed by nested-op kind.
|
|
391
|
+
*
|
|
392
|
+
* The dispatch loop in {@link processNestedOperations} looks handlers up here
|
|
393
|
+
* instead of branching on each kind. Kinds that require hooks/access control
|
|
394
|
+
* (`create`, `connect`, `connectOrCreate`, `update`) provide an `execute` that
|
|
395
|
+
* applies them; pass-through kinds (`disconnect`, `delete`, `deleteMany`,
|
|
396
|
+
* `set`, `updateMany`) return their value unchanged so Prisma's own
|
|
397
|
+
* constraints apply.
|
|
398
|
+
*/
|
|
399
|
+
const nestedOpRegistry: Record<string, NestedOpHandler> = {
|
|
400
|
+
create: {
|
|
401
|
+
execute: ({ value, relatedListConfig, context, config }) =>
|
|
402
|
+
processNestedCreate(
|
|
403
|
+
value as Record<string, unknown> | Array<Record<string, unknown>>,
|
|
404
|
+
relatedListConfig,
|
|
405
|
+
context,
|
|
406
|
+
config,
|
|
407
|
+
),
|
|
408
|
+
},
|
|
409
|
+
connect: {
|
|
410
|
+
execute: ({ value, relatedListName, relatedListConfig, context, prisma }) =>
|
|
411
|
+
processNestedConnect(
|
|
412
|
+
value as Record<string, unknown> | Array<Record<string, unknown>>,
|
|
413
|
+
relatedListName,
|
|
414
|
+
relatedListConfig,
|
|
415
|
+
context,
|
|
416
|
+
prisma,
|
|
417
|
+
),
|
|
418
|
+
},
|
|
419
|
+
connectOrCreate: {
|
|
420
|
+
execute: ({ value, relatedListName, relatedListConfig, context, config, prisma }) =>
|
|
421
|
+
processNestedConnectOrCreate(
|
|
422
|
+
value as Record<string, unknown> | Array<Record<string, unknown>>,
|
|
423
|
+
relatedListName,
|
|
424
|
+
relatedListConfig,
|
|
425
|
+
context,
|
|
426
|
+
config,
|
|
427
|
+
prisma,
|
|
428
|
+
),
|
|
429
|
+
},
|
|
430
|
+
update: {
|
|
431
|
+
execute: ({ value, relatedListName, relatedListConfig, context, config, prisma }) =>
|
|
432
|
+
processNestedUpdate(
|
|
433
|
+
value as Record<string, unknown> | Array<Record<string, unknown>>,
|
|
434
|
+
relatedListName,
|
|
435
|
+
relatedListConfig,
|
|
436
|
+
context,
|
|
437
|
+
config,
|
|
438
|
+
prisma,
|
|
439
|
+
),
|
|
440
|
+
},
|
|
441
|
+
// Pass-through kinds: no hooks/access control, left to Prisma's own constraints.
|
|
442
|
+
disconnect: { execute: ({ value }) => Promise.resolve(value) },
|
|
443
|
+
delete: { execute: ({ value }) => Promise.resolve(value) },
|
|
444
|
+
deleteMany: { execute: ({ value }) => Promise.resolve(value) },
|
|
445
|
+
set: { execute: ({ value }) => Promise.resolve(value) },
|
|
446
|
+
updateMany: { execute: ({ value }) => Promise.resolve(value) },
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Order in which nested-op kinds are processed for a single relationship field.
|
|
451
|
+
*
|
|
452
|
+
* Mirrors the historical in-place dispatch order so behaviour is preserved.
|
|
453
|
+
*/
|
|
454
|
+
const nestedOpOrder = [
|
|
455
|
+
'create',
|
|
456
|
+
'connect',
|
|
457
|
+
'connectOrCreate',
|
|
458
|
+
'update',
|
|
459
|
+
'disconnect',
|
|
460
|
+
'delete',
|
|
461
|
+
'deleteMany',
|
|
462
|
+
'set',
|
|
463
|
+
'updateMany',
|
|
464
|
+
] as const
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Process the nested relationship operations supplied for a single
|
|
468
|
+
* relationship field's value, dispatching each present nested-op kind through
|
|
469
|
+
* the {@link nestedOpRegistry}.
|
|
470
|
+
*/
|
|
471
|
+
async function processFieldNestedOps(
|
|
472
|
+
valueRecord: Record<string, unknown>,
|
|
473
|
+
args: Omit<NestedOpHandlerArgs, 'value'>,
|
|
474
|
+
): Promise<Record<string, unknown>> {
|
|
475
|
+
const nestedOp: Record<string, unknown> = {}
|
|
476
|
+
|
|
477
|
+
for (const kind of nestedOpOrder) {
|
|
478
|
+
const value = valueRecord[kind]
|
|
479
|
+
if (value === undefined) {
|
|
480
|
+
continue
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const handler = nestedOpRegistry[kind]
|
|
484
|
+
nestedOp[kind] = await handler.execute({ ...args, value })
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return nestedOp
|
|
488
|
+
}
|
|
489
|
+
|
|
399
490
|
/**
|
|
400
491
|
* Process all nested operations in a data payload
|
|
401
492
|
* Recursively handles relationship fields with nested writes
|
|
@@ -435,74 +526,14 @@ export async function processNestedOperations(
|
|
|
435
526
|
|
|
436
527
|
const { listName: relatedListName, listConfig: relatedListConfig } = relatedConfig
|
|
437
528
|
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
context,
|
|
447
|
-
config,
|
|
448
|
-
)
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (valueRecord.connect !== undefined) {
|
|
452
|
-
nestedOp.connect = await processNestedConnect(
|
|
453
|
-
valueRecord.connect as Record<string, unknown> | Array<Record<string, unknown>>,
|
|
454
|
-
relatedListName,
|
|
455
|
-
relatedListConfig,
|
|
456
|
-
context,
|
|
457
|
-
context.prisma,
|
|
458
|
-
)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (valueRecord.connectOrCreate !== undefined) {
|
|
462
|
-
nestedOp.connectOrCreate = await processNestedConnectOrCreate(
|
|
463
|
-
valueRecord.connectOrCreate as Record<string, unknown> | Array<Record<string, unknown>>,
|
|
464
|
-
relatedListName,
|
|
465
|
-
relatedListConfig,
|
|
466
|
-
context,
|
|
467
|
-
config,
|
|
468
|
-
context.prisma,
|
|
469
|
-
)
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (valueRecord.update !== undefined) {
|
|
473
|
-
nestedOp.update = await processNestedUpdate(
|
|
474
|
-
valueRecord.update as Record<string, unknown> | Array<Record<string, unknown>>,
|
|
475
|
-
relatedListName,
|
|
476
|
-
relatedListConfig,
|
|
477
|
-
context,
|
|
478
|
-
config,
|
|
479
|
-
context.prisma,
|
|
480
|
-
)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// For other operations, pass through (disconnect, delete, set, etc.)
|
|
484
|
-
// These will be subject to Prisma's own constraints
|
|
485
|
-
if (valueRecord.disconnect !== undefined) {
|
|
486
|
-
nestedOp.disconnect = valueRecord.disconnect
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
if (valueRecord.delete !== undefined) {
|
|
490
|
-
nestedOp.delete = valueRecord.delete
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (valueRecord.deleteMany !== undefined) {
|
|
494
|
-
nestedOp.deleteMany = valueRecord.deleteMany
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (valueRecord.set !== undefined) {
|
|
498
|
-
nestedOp.set = valueRecord.set
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (valueRecord.updateMany !== undefined) {
|
|
502
|
-
nestedOp.updateMany = valueRecord.updateMany
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
processed[fieldName] = nestedOp
|
|
529
|
+
// Dispatch each present nested-op kind through the handler registry.
|
|
530
|
+
processed[fieldName] = await processFieldNestedOps(value as Record<string, unknown>, {
|
|
531
|
+
relatedListName,
|
|
532
|
+
relatedListConfig,
|
|
533
|
+
context,
|
|
534
|
+
config,
|
|
535
|
+
prisma: context.prisma,
|
|
536
|
+
})
|
|
506
537
|
}
|
|
507
538
|
|
|
508
539
|
return processed
|