@opensaas/stack-core 0.37.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.
Files changed (226) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +191 -0
  3. package/CLAUDE.md +42 -3
  4. package/dist/access/access-filter.d.ts +70 -113
  5. package/dist/access/access-filter.d.ts.map +1 -1
  6. package/dist/access/access-filter.js +183 -210
  7. package/dist/access/access-filter.js.map +1 -1
  8. package/dist/access/access-filter.test.js +148 -188
  9. package/dist/access/access-filter.test.js.map +1 -1
  10. package/dist/access/declared-dependencies.d.ts +66 -26
  11. package/dist/access/declared-dependencies.d.ts.map +1 -1
  12. package/dist/access/declared-dependencies.js +62 -17
  13. package/dist/access/declared-dependencies.js.map +1 -1
  14. package/dist/access/declared-dependencies.test.d.ts +2 -0
  15. package/dist/access/declared-dependencies.test.d.ts.map +1 -0
  16. package/dist/access/declared-dependencies.test.js +226 -0
  17. package/dist/access/declared-dependencies.test.js.map +1 -0
  18. package/dist/access/depth-limits.d.ts +8 -7
  19. package/dist/access/depth-limits.d.ts.map +1 -1
  20. package/dist/access/depth-limits.js +8 -7
  21. package/dist/access/depth-limits.js.map +1 -1
  22. package/dist/access/engine.d.ts +0 -20
  23. package/dist/access/engine.d.ts.map +1 -1
  24. package/dist/access/engine.js +0 -25
  25. package/dist/access/engine.js.map +1 -1
  26. package/dist/access/errors.d.ts +61 -8
  27. package/dist/access/errors.d.ts.map +1 -1
  28. package/dist/access/errors.js +91 -12
  29. package/dist/access/errors.js.map +1 -1
  30. package/dist/access/field-access.d.ts +39 -1
  31. package/dist/access/field-access.d.ts.map +1 -1
  32. package/dist/access/field-access.js +108 -42
  33. package/dist/access/field-access.js.map +1 -1
  34. package/dist/access/field-access.test.js +229 -2
  35. package/dist/access/field-access.test.js.map +1 -1
  36. package/dist/access/field-transforms.d.ts +0 -27
  37. package/dist/access/field-transforms.d.ts.map +1 -1
  38. package/dist/access/field-visibility.d.ts +2 -5
  39. package/dist/access/field-visibility.d.ts.map +1 -1
  40. package/dist/access/field-visibility.js +89 -41
  41. package/dist/access/field-visibility.js.map +1 -1
  42. package/dist/access/index.d.ts +6 -3
  43. package/dist/access/index.d.ts.map +1 -1
  44. package/dist/access/index.js +12 -2
  45. package/dist/access/index.js.map +1 -1
  46. package/dist/access/query-validation.d.ts +121 -0
  47. package/dist/access/query-validation.d.ts.map +1 -0
  48. package/dist/access/query-validation.js +292 -0
  49. package/dist/access/query-validation.js.map +1 -0
  50. package/dist/access/relationship-count.d.ts +1 -1
  51. package/dist/access/relationship-count.d.ts.map +1 -1
  52. package/dist/access/relationship-count.js +2 -10
  53. package/dist/access/relationship-count.js.map +1 -1
  54. package/dist/access/relationship-label-filter.d.ts +23 -23
  55. package/dist/access/relationship-label-filter.d.ts.map +1 -1
  56. package/dist/access/relationship-label-filter.js +5 -76
  57. package/dist/access/relationship-label-filter.js.map +1 -1
  58. package/dist/access/relationship-label-filter.test.js +19 -56
  59. package/dist/access/relationship-label-filter.test.js.map +1 -1
  60. package/dist/access/transaction-registry.d.ts +45 -0
  61. package/dist/access/transaction-registry.d.ts.map +1 -0
  62. package/dist/access/transaction-registry.js +37 -0
  63. package/dist/access/transaction-registry.js.map +1 -0
  64. package/dist/access/types.d.ts +55 -59
  65. package/dist/access/types.d.ts.map +1 -1
  66. package/dist/config/index.d.ts +9 -7
  67. package/dist/config/index.d.ts.map +1 -1
  68. package/dist/config/index.js +10 -18
  69. package/dist/config/index.js.map +1 -1
  70. package/dist/config/label.d.ts +6 -13
  71. package/dist/config/label.d.ts.map +1 -1
  72. package/dist/config/label.js +6 -13
  73. package/dist/config/label.js.map +1 -1
  74. package/dist/config/nav-count.d.ts +11 -19
  75. package/dist/config/nav-count.d.ts.map +1 -1
  76. package/dist/config/nav-count.js +11 -19
  77. package/dist/config/nav-count.js.map +1 -1
  78. package/dist/config/plugin-engine.d.ts +0 -10
  79. package/dist/config/plugin-engine.d.ts.map +1 -1
  80. package/dist/config/plugin-engine.js +1 -37
  81. package/dist/config/plugin-engine.js.map +1 -1
  82. package/dist/config/types.d.ts +94 -109
  83. package/dist/config/types.d.ts.map +1 -1
  84. package/dist/context/apply-defaults.d.ts +8 -31
  85. package/dist/context/apply-defaults.d.ts.map +1 -1
  86. package/dist/context/apply-defaults.js +12 -41
  87. package/dist/context/apply-defaults.js.map +1 -1
  88. package/dist/context/hook-pipeline.d.ts +4 -29
  89. package/dist/context/hook-pipeline.d.ts.map +1 -1
  90. package/dist/context/hook-pipeline.js +8 -41
  91. package/dist/context/hook-pipeline.js.map +1 -1
  92. package/dist/context/index.d.ts +4 -15
  93. package/dist/context/index.d.ts.map +1 -1
  94. package/dist/context/index.js +223 -215
  95. package/dist/context/index.js.map +1 -1
  96. package/dist/context/nested-operations.d.ts +7 -26
  97. package/dist/context/nested-operations.d.ts.map +1 -1
  98. package/dist/context/nested-operations.js +73 -171
  99. package/dist/context/nested-operations.js.map +1 -1
  100. package/dist/context/transaction-boundary.d.ts +36 -47
  101. package/dist/context/transaction-boundary.d.ts.map +1 -1
  102. package/dist/context/transaction-boundary.js +92 -79
  103. package/dist/context/transaction-boundary.js.map +1 -1
  104. package/dist/context/write-pipeline.d.ts +25 -64
  105. package/dist/context/write-pipeline.d.ts.map +1 -1
  106. package/dist/context/write-pipeline.js +81 -111
  107. package/dist/context/write-pipeline.js.map +1 -1
  108. package/dist/fields/format-prisma-default.d.ts.map +1 -1
  109. package/dist/fields/format-prisma-default.js +4 -7
  110. package/dist/fields/format-prisma-default.js.map +1 -1
  111. package/dist/fields/index.d.ts +17 -2
  112. package/dist/fields/index.d.ts.map +1 -1
  113. package/dist/fields/index.js +136 -147
  114. package/dist/fields/index.js.map +1 -1
  115. package/dist/filter/collect.d.ts +26 -3
  116. package/dist/filter/collect.d.ts.map +1 -1
  117. package/dist/filter/collect.js +30 -12
  118. package/dist/filter/collect.js.map +1 -1
  119. package/dist/filter/filter.test.js +98 -15
  120. package/dist/filter/filter.test.js.map +1 -1
  121. package/dist/filter/index.d.ts +1 -0
  122. package/dist/filter/index.d.ts.map +1 -1
  123. package/dist/filter/index.js.map +1 -1
  124. package/dist/filter/map.d.ts.map +1 -1
  125. package/dist/filter/map.js +0 -5
  126. package/dist/filter/map.js.map +1 -1
  127. package/dist/filter/parse.d.ts.map +1 -1
  128. package/dist/filter/parse.js +2 -9
  129. package/dist/filter/parse.js.map +1 -1
  130. package/dist/filter/serialize.d.ts.map +1 -1
  131. package/dist/filter/serialize.js +2 -4
  132. package/dist/filter/serialize.js.map +1 -1
  133. package/dist/hooks/index.d.ts +4 -42
  134. package/dist/hooks/index.d.ts.map +1 -1
  135. package/dist/hooks/index.js +5 -59
  136. package/dist/hooks/index.js.map +1 -1
  137. package/dist/index.d.ts +3 -1
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +20 -4
  140. package/dist/index.js.map +1 -1
  141. package/dist/internal.d.ts +1 -0
  142. package/dist/internal.d.ts.map +1 -1
  143. package/dist/internal.js +4 -0
  144. package/dist/internal.js.map +1 -1
  145. package/dist/lib/case-utils.d.ts +0 -32
  146. package/dist/lib/case-utils.d.ts.map +1 -1
  147. package/dist/lib/case-utils.js +0 -32
  148. package/dist/lib/case-utils.js.map +1 -1
  149. package/dist/mcp/handler.d.ts +0 -4
  150. package/dist/mcp/handler.d.ts.map +1 -1
  151. package/dist/mcp/handler.js +12 -69
  152. package/dist/mcp/handler.js.map +1 -1
  153. package/dist/query/index.d.ts +55 -227
  154. package/dist/query/index.d.ts.map +1 -1
  155. package/dist/query/index.js +41 -117
  156. package/dist/query/index.js.map +1 -1
  157. package/dist/query/relationship-options.d.ts +1 -1
  158. package/dist/query/relationship-options.js +1 -1
  159. package/dist/utils/password.d.ts +0 -68
  160. package/dist/utils/password.d.ts.map +1 -1
  161. package/dist/utils/password.js +7 -76
  162. package/dist/utils/password.js.map +1 -1
  163. package/dist/validation/field-config.d.ts.map +1 -1
  164. package/dist/validation/field-config.js +0 -6
  165. package/dist/validation/field-config.js.map +1 -1
  166. package/dist/validation/schema.d.ts +0 -7
  167. package/dist/validation/schema.d.ts.map +1 -1
  168. package/dist/validation/schema.js +0 -10
  169. package/dist/validation/schema.js.map +1 -1
  170. package/package.json +1 -1
  171. package/src/access/access-filter.test.ts +205 -275
  172. package/src/access/access-filter.ts +236 -272
  173. package/src/access/declared-dependencies.test.ts +277 -0
  174. package/src/access/declared-dependencies.ts +117 -37
  175. package/src/access/depth-limits.ts +8 -7
  176. package/src/access/engine.ts +0 -25
  177. package/src/access/errors.ts +97 -12
  178. package/src/access/field-access.test.ts +312 -2
  179. package/src/access/field-access.ts +118 -42
  180. package/src/access/field-transforms.ts +8 -39
  181. package/src/access/field-visibility.ts +99 -45
  182. package/src/access/index.ts +17 -5
  183. package/src/access/query-validation.ts +359 -0
  184. package/src/access/relationship-count.ts +3 -11
  185. package/src/access/relationship-label-filter.test.ts +19 -68
  186. package/src/access/relationship-label-filter.ts +27 -104
  187. package/src/access/transaction-registry.ts +58 -0
  188. package/src/access/types.ts +83 -86
  189. package/src/config/index.ts +10 -22
  190. package/src/config/label.ts +6 -13
  191. package/src/config/nav-count.ts +11 -19
  192. package/src/config/plugin-engine.ts +1 -37
  193. package/src/config/types.ts +95 -110
  194. package/src/context/apply-defaults.ts +12 -44
  195. package/src/context/hook-pipeline.ts +12 -67
  196. package/src/context/index.ts +311 -276
  197. package/src/context/nested-operations.ts +94 -226
  198. package/src/context/transaction-boundary.ts +113 -109
  199. package/src/context/write-pipeline.ts +99 -155
  200. package/src/fields/format-prisma-default.ts +4 -7
  201. package/src/fields/index.ts +155 -147
  202. package/src/filter/collect.ts +48 -12
  203. package/src/filter/filter.test.ts +153 -15
  204. package/src/filter/index.ts +1 -0
  205. package/src/filter/map.ts +0 -5
  206. package/src/filter/parse.ts +2 -9
  207. package/src/filter/serialize.ts +2 -4
  208. package/src/hooks/index.ts +5 -59
  209. package/src/index.ts +23 -4
  210. package/src/internal.ts +5 -0
  211. package/src/lib/case-utils.ts +0 -32
  212. package/src/mcp/handler.ts +13 -71
  213. package/src/query/index.ts +82 -235
  214. package/src/query/relationship-options.ts +1 -1
  215. package/src/utils/password.ts +7 -76
  216. package/src/validation/field-config.ts +0 -6
  217. package/src/validation/schema.ts +0 -10
  218. package/tests/access-relationships.test.ts +18 -16
  219. package/tests/computed-field-selective-evaluation.test.ts +418 -0
  220. package/tests/context.test.ts +727 -1
  221. package/tests/field-types.test.ts +222 -0
  222. package/tests/mcp-handler.test.ts +34 -0
  223. package/tests/needs-declared-dependencies.test.ts +7 -4
  224. package/tests/resolve-chain.test.ts +11 -11
  225. package/tests/transaction-boundary-outermost.test.ts +530 -0
  226. package/tsconfig.tsbuildinfo +1 -1
@@ -1,63 +1,34 @@
1
1
  import type { FieldConfig } from '../config/types.js'
2
2
 
3
3
  /**
4
- * Apply field `defaultValue`s to omitted inputs on CREATE the runtime half of
5
- * the resolve-then-validate ordering (Keystone 6 parity, issue #615).
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
- * A field's `defaultValue` is otherwise only realised as a Prisma `@default(...)`
8
- * applied by the database at write time, which is AFTER the write pipeline's
9
- * validation phase has already run. That ordering means a required-with-default
10
- * field (e.g. `select({ validation: { isRequired: true }, defaultValue: 'X' })`)
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. An explicitly-provided value (including explicit
54
- // `null`) is preserved and must not be overwritten by the default.
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
- // The timestamp `{ kind: 'now' }` sentinel is a DB-level `@default(now())`
60
- // request, not a literal leave it for Prisma to apply at write time.
30
+ // `{ kind: 'now' }` requests the DB-level `@default(now())`, not a literal
31
+ // valueinjecting 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
- * Hook Pipeline the single module that runs the transform+validate span of a
16
- * write: list `resolveInput` field `resolveInput` list `validate` field
17
- * `validate` built-in field rules (`validateFieldRules`). It owns the order of
18
- * these phases and the threading of `resolvedData` through them, in one place.
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
- // ── Phase 1.75: apply field defaults to omitted inputs (CREATE only) ───────
120
- // Resolve-then-validate (Keystone parity, #615): a field declaring a
121
- // `defaultValue` is filled into `resolvedData` here AFTER resolveInput hooks
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
- // ── Phase 3: built-in field rules (isRequired, length, etc.) ──────────────
163
- // Validation failures THROW (validation is not silent).
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
- // ── Phase 4: split multi-column fields into physical columns ──────────────
170
- // Only reached once the logical value has passed validation (#789). Replaces
171
- // each multi-column field's logical key with its per-part columns, gated by
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
  }