@opensaas/stack-core 0.38.0 → 0.39.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 +123 -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/config/types.ts
CHANGED
|
@@ -2,9 +2,6 @@ import type { AccessControl, FieldAccess } from '../access/types.js'
|
|
|
2
2
|
import type { FilterSpec } from '../filter/types.js'
|
|
3
3
|
import type { z } from 'zod'
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Field configuration types
|
|
7
|
-
*/
|
|
8
5
|
export type FieldType =
|
|
9
6
|
'text' | 'integer' | 'checkbox' | 'timestamp' | 'password' | 'select' | 'relationship' | string // Allow custom field types from third-party packages
|
|
10
7
|
|
|
@@ -12,10 +9,7 @@ export type FieldType =
|
|
|
12
9
|
* Field-level hook argument types (exported for user annotations)
|
|
13
10
|
*/
|
|
14
11
|
|
|
15
|
-
/**
|
|
16
|
-
* Arguments for field-level resolveInput hook
|
|
17
|
-
* Used to transform field values before database write
|
|
18
|
-
*/
|
|
12
|
+
/** Arguments for {@link FieldHooks.resolveInput}. */
|
|
19
13
|
export type FieldResolveInputHookArgs<
|
|
20
14
|
TTypeInfo extends TypeInfo,
|
|
21
15
|
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
@@ -39,10 +33,7 @@ export type FieldResolveInputHookArgs<
|
|
|
39
33
|
context: import('../access/types.js').AccessContext
|
|
40
34
|
}
|
|
41
35
|
|
|
42
|
-
/**
|
|
43
|
-
* Arguments for field-level validate hook
|
|
44
|
-
* Used for custom validation logic
|
|
45
|
-
*/
|
|
36
|
+
/** Arguments for {@link FieldHooks.validate} (and its deprecated `validateInput` alias). */
|
|
46
37
|
export type FieldValidateHookArgs<
|
|
47
38
|
TTypeInfo extends TypeInfo,
|
|
48
39
|
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
@@ -76,10 +67,7 @@ export type FieldValidateHookArgs<
|
|
|
76
67
|
addValidationError: (msg: string) => void
|
|
77
68
|
}
|
|
78
69
|
|
|
79
|
-
/**
|
|
80
|
-
* Arguments for field-level beforeOperation hook
|
|
81
|
-
* Used for side effects before database write
|
|
82
|
-
*/
|
|
70
|
+
/** Arguments for {@link FieldHooks.beforeOperation}. */
|
|
83
71
|
export type FieldBeforeOperationHookArgs<
|
|
84
72
|
TTypeInfo extends TypeInfo,
|
|
85
73
|
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
@@ -109,10 +97,7 @@ export type FieldBeforeOperationHookArgs<
|
|
|
109
97
|
context: import('../access/types.js').AccessContext
|
|
110
98
|
}
|
|
111
99
|
|
|
112
|
-
/**
|
|
113
|
-
* Arguments for field-level afterOperation hook
|
|
114
|
-
* Used for side effects after database operation
|
|
115
|
-
*/
|
|
100
|
+
/** Arguments for {@link FieldHooks.afterOperation}. */
|
|
116
101
|
export type FieldAfterOperationHookArgs<
|
|
117
102
|
TTypeInfo extends TypeInfo,
|
|
118
103
|
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
@@ -263,10 +248,7 @@ export type FieldAfterTransactionHookArgs<
|
|
|
263
248
|
context: import('../access/types.js').AccessContext
|
|
264
249
|
}
|
|
265
250
|
|
|
266
|
-
/**
|
|
267
|
-
* Arguments for field-level resolveOutput hook
|
|
268
|
-
* Used to transform field values after database read
|
|
269
|
-
*/
|
|
251
|
+
/** Arguments for {@link FieldHooks.resolveOutput}. */
|
|
270
252
|
export type FieldResolveOutputHookArgs<
|
|
271
253
|
TTypeInfo extends TypeInfo,
|
|
272
254
|
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
@@ -507,10 +489,10 @@ export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
|
|
|
507
489
|
defaultValue?: unknown
|
|
508
490
|
hooks?: FieldHooks<TTypeInfo>
|
|
509
491
|
/**
|
|
510
|
-
* Marks this field as virtual
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
492
|
+
* Marks this field as virtual — not stored in database, computed via
|
|
493
|
+
* `resolveInput`/`resolveOutput` hooks, and excluded from the Prisma
|
|
494
|
+
* schema and input types. Computed whenever the read is going to return
|
|
495
|
+
* it (ADR-0027) — not gated behind an explicit `include`/selection.
|
|
514
496
|
*/
|
|
515
497
|
virtual?: boolean
|
|
516
498
|
/**
|
|
@@ -877,6 +859,25 @@ export type DecimalField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfi
|
|
|
877
859
|
isIndexed?: boolean | 'unique'
|
|
878
860
|
}
|
|
879
861
|
|
|
862
|
+
/**
|
|
863
|
+
* 64-bit integer field (Prisma `BigInt`, TypeScript `bigint`) — for values that
|
|
864
|
+
* overflow `integer()`'s 32-bit `Int` (e.g. a millisecond epoch).
|
|
865
|
+
*
|
|
866
|
+
* Wire representation (ADR-0029): `bigint` in application code, a decimal
|
|
867
|
+
* string over MCP — `bigint` is not JSON-serialisable, so the MCP handler
|
|
868
|
+
* renders it as a string rather than throwing.
|
|
869
|
+
*/
|
|
870
|
+
export type BigIntField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
|
|
871
|
+
type: 'bigInt'
|
|
872
|
+
defaultValue?: bigint | number | string
|
|
873
|
+
validation?: {
|
|
874
|
+
isRequired?: boolean
|
|
875
|
+
min?: bigint
|
|
876
|
+
max?: bigint
|
|
877
|
+
}
|
|
878
|
+
isIndexed?: boolean | 'unique'
|
|
879
|
+
}
|
|
880
|
+
|
|
880
881
|
export type CheckboxField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
|
|
881
882
|
type: 'checkbox'
|
|
882
883
|
}
|
|
@@ -1358,20 +1359,8 @@ export type VirtualField<TTypeInfo extends TypeInfo> = BaseFieldConfig<TTypeInfo
|
|
|
1358
1359
|
*/
|
|
1359
1360
|
export type FieldConfig = BaseFieldConfig<TypeInfo>
|
|
1360
1361
|
|
|
1361
|
-
/**
|
|
1362
|
-
* List configuration types
|
|
1363
|
-
*/
|
|
1364
|
-
|
|
1365
|
-
/**
|
|
1366
|
-
* Utility type to inject TypeInfo into a single field config
|
|
1367
|
-
* Extracts TInput and TOutput from BaseFieldConfig and reconstructs with new TypeInfo
|
|
1368
|
-
*/
|
|
1369
1362
|
type WithTypeInfo<TTypeInfo extends TypeInfo> = BaseFieldConfig<TTypeInfo>
|
|
1370
1363
|
|
|
1371
|
-
/**
|
|
1372
|
-
* Utility type to transform all fields in a record to inject TypeInfo
|
|
1373
|
-
* Maps over each field and applies WithTypeInfo transformation
|
|
1374
|
-
*/
|
|
1375
1364
|
export type FieldsWithTypeInfo<TTypeInfo extends TypeInfo> = {
|
|
1376
1365
|
[key: string]: WithTypeInfo<TTypeInfo>
|
|
1377
1366
|
}
|
|
@@ -1547,8 +1536,7 @@ export type ListAccessControl<T = any> =
|
|
|
1547
1536
|
}
|
|
1548
1537
|
|
|
1549
1538
|
/**
|
|
1550
|
-
* Hook arguments for resolveInput hook
|
|
1551
|
-
* Uses discriminated union to provide proper types based on operation
|
|
1539
|
+
* Hook arguments for the list-level `resolveInput` hook.
|
|
1552
1540
|
* - create: resolvedData is CreateInput, item is undefined
|
|
1553
1541
|
* - update: resolvedData is UpdateInput, item is the existing record
|
|
1554
1542
|
*/
|
|
@@ -1575,8 +1563,7 @@ export type ResolveInputHookArgs<
|
|
|
1575
1563
|
}
|
|
1576
1564
|
|
|
1577
1565
|
/**
|
|
1578
|
-
* Hook arguments for validate hook (renamed from validateInput for Keystone compatibility)
|
|
1579
|
-
* Uses discriminated union to provide proper types based on operation
|
|
1566
|
+
* Hook arguments for the list-level `validate` hook (renamed from `validateInput` for Keystone compatibility).
|
|
1580
1567
|
* - create: resolvedData is CreateInput, item is undefined
|
|
1581
1568
|
* - update: resolvedData is UpdateInput, item is the existing record
|
|
1582
1569
|
* - delete: item is the item being deleted
|
|
@@ -1613,8 +1600,7 @@ export type ValidateHookArgs<
|
|
|
1613
1600
|
}
|
|
1614
1601
|
|
|
1615
1602
|
/**
|
|
1616
|
-
* Hook arguments for beforeOperation hook
|
|
1617
|
-
* Uses discriminated union to provide proper types based on operation
|
|
1603
|
+
* Hook arguments for the list-level `beforeOperation` hook.
|
|
1618
1604
|
* - create: has inputData and resolvedData, no item
|
|
1619
1605
|
* - update: has inputData, resolvedData, and item
|
|
1620
1606
|
* - delete: has item only
|
|
@@ -1647,8 +1633,7 @@ export type BeforeOperationHookArgs<
|
|
|
1647
1633
|
}
|
|
1648
1634
|
|
|
1649
1635
|
/**
|
|
1650
|
-
* Hook arguments for afterOperation hook
|
|
1651
|
-
* Uses discriminated union to provide proper types based on operation
|
|
1636
|
+
* Hook arguments for the list-level `afterOperation` hook.
|
|
1652
1637
|
* - create: has item, inputData, and resolvedData, no originalItem
|
|
1653
1638
|
* - update: has item, originalItem, inputData, and resolvedData
|
|
1654
1639
|
* - delete: has originalItem only
|
|
@@ -1848,14 +1833,20 @@ export type ListIndexFieldRef =
|
|
|
1848
1833
|
}
|
|
1849
1834
|
|
|
1850
1835
|
/**
|
|
1851
|
-
* A model-level
|
|
1852
|
-
*
|
|
1853
|
-
*
|
|
1836
|
+
* A model-level `@@unique`/`@@index` constraint spanning one or more of a
|
|
1837
|
+
* list's own fields. See {@link ListConfig.db}'s `indexes` for the full
|
|
1838
|
+
* explanation and examples (#864, #918).
|
|
1854
1839
|
*/
|
|
1855
1840
|
export type ListIndex = {
|
|
1856
1841
|
/**
|
|
1857
1842
|
* The fields participating in this index/constraint, in declaration order.
|
|
1858
1843
|
* OpenSaaS field names, not raw database column names.
|
|
1844
|
+
*
|
|
1845
|
+
* One or more fields — a named single-column constraint is as legitimate
|
|
1846
|
+
* as a composite one, and is the form to reach for when a live table's
|
|
1847
|
+
* constraint carries a name Prisma wouldn't derive on its own. Field-level
|
|
1848
|
+
* `isIndexed` remains the sugar for the unnamed single-column case; this
|
|
1849
|
+
* is the full form when a name or a sort direction is needed.
|
|
1859
1850
|
*/
|
|
1860
1851
|
fields: ListIndexFieldRef[]
|
|
1861
1852
|
/**
|
|
@@ -1949,31 +1940,40 @@ export type ListConfig<TTypeInfo extends TypeInfo> = {
|
|
|
1949
1940
|
*/
|
|
1950
1941
|
timestamps?: boolean
|
|
1951
1942
|
/**
|
|
1952
|
-
* Model-level
|
|
1953
|
-
*
|
|
1943
|
+
* Model-level `@@unique`/`@@index` constraints spanning one or more of
|
|
1944
|
+
* this list's own fields (#864, #918). See the [reference
|
|
1945
|
+
* docs](https://stack.opensaas.au/docs/reference/config-api#dbindexes)
|
|
1946
|
+
* for the full explanation, arity, and error conditions.
|
|
1954
1947
|
*
|
|
1955
|
-
* Field-level `isIndexed` (on a scalar or relationship field)
|
|
1956
|
-
*
|
|
1957
|
-
*
|
|
1958
|
-
*
|
|
1959
|
-
*
|
|
1960
|
-
*
|
|
1961
|
-
*
|
|
1962
|
-
*
|
|
1963
|
-
*
|
|
1948
|
+
* Field-level `isIndexed` (on a scalar or relationship field) is the
|
|
1949
|
+
* sugar for the unnamed single-column case. `db.indexes` is the full
|
|
1950
|
+
* form — reach for it when a constraint needs a `name` (Prisma's `map:`,
|
|
1951
|
+
* for adopting an existing live constraint), a `sort` direction, or spans
|
|
1952
|
+
* more than one column. Arity is incidental: each entry names one or
|
|
1953
|
+
* more of the list's own OpenSaaS field names, not raw database column
|
|
1954
|
+
* names. The generator resolves each to its underlying Prisma column — a
|
|
1955
|
+
* scalar field's own name (its Prisma field name is unaffected by
|
|
1956
|
+
* `db.map`), or a relationship field's foreign key column (`<field>Id`)
|
|
1957
|
+
* when this side owns it.
|
|
1964
1958
|
*
|
|
1965
|
-
* A
|
|
1966
|
-
*
|
|
1967
|
-
*
|
|
1968
|
-
*
|
|
1969
|
-
*
|
|
1970
|
-
*
|
|
1971
|
-
*
|
|
1959
|
+
* A **unique** entry is the load-bearing case: it's the database-level
|
|
1960
|
+
* backstop for a business rule concurrent writes could otherwise both
|
|
1961
|
+
* slip past (e.g. "one booking per student per production", or a
|
|
1962
|
+
* single-column unique a live table already enforces under a name Prisma
|
|
1963
|
+
* wouldn't derive) — something a hook's existence check cannot close on
|
|
1964
|
+
* its own, and can't be retrofitted once duplicate rows exist. An
|
|
1965
|
+
* **index** (non-unique) entry is the equivalent performance-only case
|
|
1966
|
+
* (a hot lookup path).
|
|
1972
1967
|
*
|
|
1973
|
-
*
|
|
1974
|
-
*
|
|
1975
|
-
*
|
|
1976
|
-
*
|
|
1968
|
+
* Two conditions fail `pnpm generate` with an error naming the list and
|
|
1969
|
+
* the entry: an empty `fields` array, and a single-field entry that
|
|
1970
|
+
* indexes the exact column a field-level `isIndexed` on the same list
|
|
1971
|
+
* already indexes (the error also names that field and its `isIndexed`
|
|
1972
|
+
* value — one of the two should be removed). An entry naming a field the
|
|
1973
|
+
* list doesn't have, a virtual field, a to-many relationship, or the
|
|
1974
|
+
* non-FK side of a one-to-one relationship fails the same way, naming
|
|
1975
|
+
* the bad field too — no entry is ever silently dropped or emitted as
|
|
1976
|
+
* invalid Prisma.
|
|
1977
1977
|
*
|
|
1978
1978
|
* @example One audition per student per production (composite unique)
|
|
1979
1979
|
* ```typescript
|
|
@@ -2007,12 +2007,22 @@ export type ListConfig<TTypeInfo extends TypeInfo> = {
|
|
|
2007
2007
|
* })
|
|
2008
2008
|
* // Generates: @@index([identifier, createdAt(sort: Desc)], map: "AuthVerification_identifier_createdAt_idx")
|
|
2009
2009
|
* ```
|
|
2010
|
+
*
|
|
2011
|
+
* @example Naming a single-column unique constraint (adopting a live table's existing name)
|
|
2012
|
+
* ```typescript
|
|
2013
|
+
* RateLimit: list({
|
|
2014
|
+
* fields: { key: text() }, // no isIndexed here — db.indexes owns this column instead
|
|
2015
|
+
* db: {
|
|
2016
|
+
* indexes: [{ fields: ['key'], unique: true, name: 'RateLimit_key_key' }],
|
|
2017
|
+
* },
|
|
2018
|
+
* })
|
|
2019
|
+
* // Generates: @@unique([key], map: "RateLimit_key_key")
|
|
2020
|
+
* // Setting key's own isIndexed too would duplicate this constraint and
|
|
2021
|
+
* // fail generation — one of the two must own the column.
|
|
2022
|
+
* ```
|
|
2010
2023
|
*/
|
|
2011
2024
|
indexes?: ListIndex[]
|
|
2012
2025
|
}
|
|
2013
|
-
/**
|
|
2014
|
-
* MCP server configuration for this list
|
|
2015
|
-
*/
|
|
2016
2026
|
mcp?: ListMcpConfig
|
|
2017
2027
|
/**
|
|
2018
2028
|
* Restricts this list to a single record (singleton pattern)
|
|
@@ -2319,9 +2329,6 @@ export type ListConfigInput<TTypeInfo extends TypeInfo> = Omit<ListConfig<TTypeI
|
|
|
2319
2329
|
access?: ListAccessControl<TTypeInfo['item']>
|
|
2320
2330
|
}
|
|
2321
2331
|
|
|
2322
|
-
/**
|
|
2323
|
-
* Database configuration
|
|
2324
|
-
*/
|
|
2325
2332
|
export type DatabaseConfig = {
|
|
2326
2333
|
provider: 'postgresql' | 'mysql' | 'sqlite'
|
|
2327
2334
|
/**
|
|
@@ -2540,9 +2547,6 @@ export type DatabaseConfig = {
|
|
|
2540
2547
|
}
|
|
2541
2548
|
}
|
|
2542
2549
|
|
|
2543
|
-
/**
|
|
2544
|
-
* Session configuration
|
|
2545
|
-
*/
|
|
2546
2550
|
export type SessionConfig = {
|
|
2547
2551
|
// Uses `any` return type because session structure is user-defined and varies per application
|
|
2548
2552
|
// The stack doesn't enforce a specific session shape - users can use NextAuth, Clerk, etc.
|
|
@@ -2550,9 +2554,6 @@ export type SessionConfig = {
|
|
|
2550
2554
|
getSession: () => Promise<any>
|
|
2551
2555
|
}
|
|
2552
2556
|
|
|
2553
|
-
/**
|
|
2554
|
-
* Theme preset options
|
|
2555
|
-
*/
|
|
2556
2557
|
export type ThemePreset = 'modern' | 'classic' | 'neon'
|
|
2557
2558
|
|
|
2558
2559
|
/**
|
|
@@ -2648,14 +2649,8 @@ export type ThemeConfig = {
|
|
|
2648
2649
|
shadows?: ThemeShadows
|
|
2649
2650
|
}
|
|
2650
2651
|
|
|
2651
|
-
/**
|
|
2652
|
-
* UI configuration
|
|
2653
|
-
*/
|
|
2654
2652
|
export type UIConfig = {
|
|
2655
2653
|
basePath?: string
|
|
2656
|
-
/**
|
|
2657
|
-
* Theme configuration for the admin UI
|
|
2658
|
-
*/
|
|
2659
2654
|
theme?: ThemeConfig
|
|
2660
2655
|
}
|
|
2661
2656
|
|
|
@@ -2698,18 +2693,12 @@ export type McpCustomTool = {
|
|
|
2698
2693
|
* Unique name for the tool
|
|
2699
2694
|
*/
|
|
2700
2695
|
name: string
|
|
2701
|
-
/**
|
|
2702
|
-
* Description of what the tool does
|
|
2703
|
-
*/
|
|
2704
2696
|
description: string
|
|
2705
2697
|
/**
|
|
2706
2698
|
* Input schema (Zod schema)
|
|
2707
2699
|
*/
|
|
2708
2700
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2709
2701
|
inputSchema: any
|
|
2710
|
-
/**
|
|
2711
|
-
* Handler function that executes the tool
|
|
2712
|
-
*/
|
|
2713
2702
|
handler: (args: {
|
|
2714
2703
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2715
2704
|
input: any
|
|
@@ -2717,9 +2706,6 @@ export type McpCustomTool = {
|
|
|
2717
2706
|
}) => Promise<unknown>
|
|
2718
2707
|
}
|
|
2719
2708
|
|
|
2720
|
-
/**
|
|
2721
|
-
* List-level MCP configuration
|
|
2722
|
-
*/
|
|
2723
2709
|
export type ListMcpConfig = {
|
|
2724
2710
|
/**
|
|
2725
2711
|
* Enable MCP tools for this list
|
|
@@ -2790,9 +2776,6 @@ export type McpAuthConfig =
|
|
|
2790
2776
|
* Authentication type - custom auth provider
|
|
2791
2777
|
*/
|
|
2792
2778
|
type: string
|
|
2793
|
-
/**
|
|
2794
|
-
* Additional auth-specific configuration
|
|
2795
|
-
*/
|
|
2796
2779
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Allows custom auth provider configuration
|
|
2797
2780
|
[key: string]: any
|
|
2798
2781
|
}
|
|
@@ -2920,15 +2903,17 @@ export type PluginContext = {
|
|
|
2920
2903
|
|
|
2921
2904
|
/**
|
|
2922
2905
|
* Add a new list to the config
|
|
2923
|
-
* Throws error if list already exists
|
|
2906
|
+
* Throws error if list already exists
|
|
2924
2907
|
*/
|
|
2925
2908
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API must accept any list config
|
|
2926
2909
|
addList: (name: string, listConfig: ListConfig<any>) => void
|
|
2927
2910
|
|
|
2928
2911
|
/**
|
|
2929
|
-
* Extend an existing list with additional fields, hooks, or
|
|
2930
|
-
*
|
|
2931
|
-
*
|
|
2912
|
+
* Extend an existing list with additional fields, hooks, or MCP config.
|
|
2913
|
+
* Merges fields, hooks, and MCP config. Throws if the list doesn't exist,
|
|
2914
|
+
* or if the extension sets operation-level `access` — access control
|
|
2915
|
+
* belongs to whoever created the list, never a plugin extending it
|
|
2916
|
+
* (ADR-0013).
|
|
2932
2917
|
*/
|
|
2933
2918
|
extendList: (
|
|
2934
2919
|
name: string,
|
|
@@ -3047,10 +3032,6 @@ export type Plugin = {
|
|
|
3047
3032
|
}
|
|
3048
3033
|
}
|
|
3049
3034
|
|
|
3050
|
-
/**
|
|
3051
|
-
* Main configuration type
|
|
3052
|
-
* Using interface instead of type to allow module augmentation
|
|
3053
|
-
*/
|
|
3054
3035
|
/**
|
|
3055
3036
|
* Configurable generator output locations.
|
|
3056
3037
|
*
|
|
@@ -3112,6 +3093,10 @@ export interface OutputConfig {
|
|
|
3112
3093
|
buildTarget?: 'node'
|
|
3113
3094
|
}
|
|
3114
3095
|
|
|
3096
|
+
/**
|
|
3097
|
+
* Main configuration type.
|
|
3098
|
+
* Uses an interface, not a type alias, so it can be extended via module augmentation.
|
|
3099
|
+
*/
|
|
3115
3100
|
export interface OpenSaasConfig {
|
|
3116
3101
|
db: DatabaseConfig
|
|
3117
3102
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Config must accept any list configuration
|
|
@@ -1,63 +1,34 @@
|
|
|
1
1
|
import type { FieldConfig } from '../config/types.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Fills a field's `defaultValue` into `resolvedData` for CREATE when the field
|
|
5
|
+
* was omitted, run after resolveInput hooks and before validation. Shared by
|
|
6
|
+
* the top-level create path (Hook Pipeline) and the nested-relation create path.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* fails `isRequired` validation on an omitted input even though a default exists.
|
|
12
|
-
*
|
|
13
|
-
* This helper closes that gap: in the resolve phase (after `resolveInput` hooks,
|
|
14
|
-
* before validation) it fills `resolvedData[field]` with the field's
|
|
15
|
-
* `defaultValue` ONLY when the field was OMITTED (value is `undefined`). It is a
|
|
16
|
-
* SINGLE shared mechanism used by both the top-level create path (Hook Pipeline)
|
|
17
|
-
* and the nested-relation create path.
|
|
18
|
-
*
|
|
19
|
-
* Guard rails (each acceptance-criteria-driven):
|
|
20
|
-
* - CREATE only. Update never injects defaults for omitted fields (the caller
|
|
21
|
-
* only invokes this for `operation === 'create'`).
|
|
22
|
-
* - Explicitly-provided values are preserved. A key present in `resolvedData`
|
|
23
|
-
* — INCLUDING an explicit `null` — is left untouched; only `undefined`
|
|
24
|
-
* (omitted) keys are filled.
|
|
25
|
-
* - Virtual, system (`id`/`createdAt`/`updatedAt`) and relationship fields are
|
|
26
|
-
* skipped — they have no scalar `defaultValue` to inject and relationships
|
|
27
|
-
* carry connect/create payloads rather than literal defaults.
|
|
28
|
-
* - The timestamp `{ kind: 'now' }` sentinel is NOT injected: it is not a
|
|
29
|
-
* literal value but a request for the DB-level `@default(now())`, which still
|
|
30
|
-
* applies at write time. Injecting the sentinel object would corrupt the
|
|
31
|
-
* payload. (A concrete `Date` default is a real literal and IS injected.)
|
|
32
|
-
*
|
|
33
|
-
* The function mutates and returns `resolvedData` (consistent with the other
|
|
34
|
-
* resolve-phase helpers that thread `resolvedData` through the pipeline).
|
|
8
|
+
* Prisma only realises `defaultValue` as `@default(...)` at the database write,
|
|
9
|
+
* which is after this pipeline's validation phase — so without this, a
|
|
10
|
+
* required field with a default (e.g. `select({ validation: { isRequired: true },
|
|
11
|
+
* defaultValue: 'X' })`) fails `isRequired` on an omitted input. See issue #615.
|
|
35
12
|
*/
|
|
36
13
|
export function applyCreateDefaults(
|
|
37
14
|
resolvedData: Record<string, unknown>,
|
|
38
15
|
fieldConfigs: Record<string, FieldConfig>,
|
|
39
16
|
): Record<string, unknown> {
|
|
40
17
|
for (const [fieldKey, fieldConfig] of Object.entries(fieldConfigs)) {
|
|
41
|
-
// Skip virtual fields — not stored in the database.
|
|
42
18
|
if (fieldConfig.virtual) continue
|
|
43
|
-
|
|
44
|
-
// Skip system fields — always managed by the framework/DB.
|
|
45
19
|
if (fieldKey === 'id' || fieldKey === 'createdAt' || fieldKey === 'updatedAt') continue
|
|
46
|
-
|
|
47
|
-
// Skip relationships — they carry connect/create payloads, not literal defaults.
|
|
20
|
+
// Relationships carry connect/create payloads, not literal defaults.
|
|
48
21
|
if (fieldConfig.type === 'relationship') continue
|
|
49
|
-
|
|
50
|
-
// No declared default → nothing to inject.
|
|
51
22
|
if (!('defaultValue' in fieldConfig) || fieldConfig.defaultValue === undefined) continue
|
|
52
23
|
|
|
53
|
-
// Only fill OMITTED keys
|
|
54
|
-
// `null
|
|
24
|
+
// Only fill OMITTED (`undefined`) keys — an explicitly-provided value,
|
|
25
|
+
// including explicit `null`, must survive untouched.
|
|
55
26
|
if (resolvedData[fieldKey] !== undefined) continue
|
|
56
27
|
|
|
57
28
|
const defaultValue = fieldConfig.defaultValue
|
|
58
29
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
30
|
+
// `{ kind: 'now' }` requests the DB-level `@default(now())`, not a literal
|
|
31
|
+
// value — injecting the sentinel object itself would corrupt the payload.
|
|
61
32
|
if (isNowSentinel(defaultValue)) continue
|
|
62
33
|
|
|
63
34
|
resolvedData[fieldKey] = defaultValue
|
|
@@ -66,9 +37,6 @@ export function applyCreateDefaults(
|
|
|
66
37
|
return resolvedData
|
|
67
38
|
}
|
|
68
39
|
|
|
69
|
-
/**
|
|
70
|
-
* Detect the timestamp `{ kind: 'now' }` default sentinel.
|
|
71
|
-
*/
|
|
72
40
|
function isNowSentinel(value: unknown): boolean {
|
|
73
41
|
return (
|
|
74
42
|
typeof value === 'object' &&
|
|
@@ -12,78 +12,33 @@ import {
|
|
|
12
12
|
import { applyCreateDefaults } from './apply-defaults.js'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* It is THE place where input is shaped and validated; it throws
|
|
21
|
-
* {@link ValidationError} on failure exactly as before (validate hooks via
|
|
22
|
-
* `addValidationError`, then `validateFieldRules`) — validation is never silent.
|
|
23
|
-
*
|
|
24
|
-
* Side-effect hooks (`beforeOperation`/`afterOperation`), operation-level access,
|
|
25
|
-
* writable-field filtering, nested operations, persistence and Field Visibility
|
|
26
|
-
* are deliberately OUT of this span — they stay in the Write Pipeline. See the
|
|
27
|
-
* "Hook Pipeline" and "Write Pipeline" glossary terms in CONTEXT.md.
|
|
15
|
+
* The transform+validate span of a write. See the "Hook Pipeline" and "Write
|
|
16
|
+
* Pipeline" glossary terms in CONTEXT.md for the full phase order and the
|
|
17
|
+
* boundary with the Write Pipeline (which owns side-effect hooks, access,
|
|
18
|
+
* writable-field filtering, nested operations, persistence, Field Visibility).
|
|
28
19
|
*/
|
|
29
20
|
|
|
30
|
-
/**
|
|
31
|
-
* Arguments for one transform+validate span. Only the create/update operations
|
|
32
|
-
* run this span (delete skips the input-shaping phases entirely).
|
|
33
|
-
*/
|
|
34
21
|
export interface HookPipelineArgs {
|
|
35
22
|
operation: 'create' | 'update'
|
|
36
23
|
listName: string
|
|
37
24
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
38
25
|
listConfig: ListConfig<any>
|
|
39
|
-
/** The original input data for the write. */
|
|
40
26
|
inputData: Record<string, unknown>
|
|
41
|
-
/** The existing row for update; `undefined` for create. */
|
|
42
27
|
item: Record<string, unknown> | undefined
|
|
43
28
|
context: AccessContext
|
|
44
29
|
}
|
|
45
30
|
|
|
46
|
-
/**
|
|
47
|
-
* Result of a transform+validate span: the fully-resolved write data after the
|
|
48
|
-
* resolveInput hooks have run and all validation has passed.
|
|
49
|
-
*/
|
|
50
31
|
export interface HookPipelineResult {
|
|
51
32
|
resolvedData: Record<string, unknown>
|
|
52
33
|
}
|
|
53
34
|
|
|
54
|
-
/**
|
|
55
|
-
* The transform+validate span, owning order + `resolvedData` threading.
|
|
56
|
-
*/
|
|
57
35
|
export interface HookPipeline {
|
|
58
36
|
run(args: HookPipelineArgs): Promise<HookPipelineResult>
|
|
59
37
|
}
|
|
60
38
|
|
|
61
|
-
/**
|
|
62
|
-
* Run the transform+validate span once.
|
|
63
|
-
*
|
|
64
|
-
* Phase order (owned here, in one place):
|
|
65
|
-
* list `resolveInput`
|
|
66
|
-
* → field `resolveInput`
|
|
67
|
-
* → list `validate`
|
|
68
|
-
* → field `validate`
|
|
69
|
-
* → built-in field rules (`validateFieldRules`)
|
|
70
|
-
* → split multi-column fields (`splitMultiColumnFields`)
|
|
71
|
-
*
|
|
72
|
-
* Contract preserved exactly:
|
|
73
|
-
* - `resolvedData` starts as `inputData` and is threaded through each phase;
|
|
74
|
-
* - validate hooks report failures via `addValidationError` → THROW
|
|
75
|
-
* `ValidationError` (never silent);
|
|
76
|
-
* - built-in field rule failures THROW `ValidationError`;
|
|
77
|
-
* - a multi-column field (e.g. storage image()/file() in Keystone-parity
|
|
78
|
-
* mode) is validated under its LOGICAL key BEFORE it is split into
|
|
79
|
-
* physical columns (#789) — an unrecognised value throws instead of being
|
|
80
|
-
* silently split into null/undefined columns;
|
|
81
|
-
* - on success returns the transformed `resolvedData`.
|
|
82
|
-
*/
|
|
83
39
|
async function runHookPipeline(args: HookPipelineArgs): Promise<HookPipelineResult> {
|
|
84
40
|
const { operation, listName, listConfig, inputData, item, context } = args
|
|
85
41
|
|
|
86
|
-
// ── Phase 1: list-level resolveInput ──────────────────────────────────────
|
|
87
42
|
let resolvedData = await executeResolveInput(
|
|
88
43
|
listConfig.hooks,
|
|
89
44
|
operation === 'create'
|
|
@@ -105,7 +60,6 @@ async function runHookPipeline(args: HookPipelineArgs): Promise<HookPipelineResu
|
|
|
105
60
|
},
|
|
106
61
|
)
|
|
107
62
|
|
|
108
|
-
// ── Phase 1.5: field-level resolveInput (e.g. hash passwords) ──────────────
|
|
109
63
|
resolvedData = await executeFieldResolveInputHooks(
|
|
110
64
|
inputData,
|
|
111
65
|
resolvedData,
|
|
@@ -116,17 +70,13 @@ async function runHookPipeline(args: HookPipelineArgs): Promise<HookPipelineResu
|
|
|
116
70
|
item,
|
|
117
71
|
)
|
|
118
72
|
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
// and BEFORE validation — but only when the field was OMITTED, so a
|
|
123
|
-
// required-with-default field passes `isRequired` instead of failing it.
|
|
124
|
-
// Update is untouched (no default injection on update).
|
|
73
|
+
// Must run after resolveInput hooks and before validation, so a
|
|
74
|
+
// required-with-default field passes `isRequired` on an omitted input
|
|
75
|
+
// instead of failing it (see applyCreateDefaults, issue #615).
|
|
125
76
|
if (operation === 'create') {
|
|
126
77
|
resolvedData = applyCreateDefaults(resolvedData, listConfig.fields)
|
|
127
78
|
}
|
|
128
79
|
|
|
129
|
-
// ── Phase 2: list-level validate ──────────────────────────────────────────
|
|
130
80
|
await executeValidate(
|
|
131
81
|
listConfig.hooks,
|
|
132
82
|
operation === 'create'
|
|
@@ -148,7 +98,6 @@ async function runHookPipeline(args: HookPipelineArgs): Promise<HookPipelineResu
|
|
|
148
98
|
},
|
|
149
99
|
)
|
|
150
100
|
|
|
151
|
-
// ── Phase 2.5: field-level validate ───────────────────────────────────────
|
|
152
101
|
await executeFieldValidateHooks(
|
|
153
102
|
inputData,
|
|
154
103
|
resolvedData,
|
|
@@ -159,17 +108,16 @@ async function runHookPipeline(args: HookPipelineArgs): Promise<HookPipelineResu
|
|
|
159
108
|
item,
|
|
160
109
|
)
|
|
161
110
|
|
|
162
|
-
//
|
|
163
|
-
//
|
|
111
|
+
// Unlike an access-denied `context.db` operation, a validation failure
|
|
112
|
+
// throws rather than returning null/[] — validation is never silent.
|
|
164
113
|
const validation = validateFieldRules(resolvedData, listConfig.fields, operation)
|
|
165
114
|
if (validation.errors.length > 0) {
|
|
166
115
|
throw new ValidationError(validation.errors, validation.fieldErrors)
|
|
167
116
|
}
|
|
168
117
|
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
// the field's own write access (see `splitMultiColumnFields`).
|
|
118
|
+
// Runs only once the logical value has passed validation above (#789) — a
|
|
119
|
+
// multi-column field validated after splitting could see an unrecognised
|
|
120
|
+
// value silently become null/undefined columns instead of throwing.
|
|
173
121
|
resolvedData = await splitMultiColumnFields(
|
|
174
122
|
inputData,
|
|
175
123
|
resolvedData,
|
|
@@ -182,9 +130,6 @@ async function runHookPipeline(args: HookPipelineArgs): Promise<HookPipelineResu
|
|
|
182
130
|
return { resolvedData }
|
|
183
131
|
}
|
|
184
132
|
|
|
185
|
-
/**
|
|
186
|
-
* The default Hook Pipeline instance used by the Write Pipeline.
|
|
187
|
-
*/
|
|
188
133
|
export const hookPipeline: HookPipeline = {
|
|
189
134
|
run: runHookPipeline,
|
|
190
135
|
}
|