@cosmicdrift/kumiko-framework 0.257.0 → 0.258.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.
Files changed (138) hide show
  1. package/package.json +3 -3
  2. package/src/__tests__/anonymous-access.integration.test.ts +3 -3
  3. package/src/__tests__/derived-fields.integration.test.ts +1 -1
  4. package/src/__tests__/entity-list-limits.integration.test.ts +1 -1
  5. package/src/__tests__/error-contract.integration.test.ts +1 -1
  6. package/src/__tests__/ownership-where-write-path.integration.test.ts +1 -1
  7. package/src/__tests__/ownership.integration.test.ts +7 -3
  8. package/src/__tests__/schema-cli-rebuild.integration.test.ts +2 -2
  9. package/src/__tests__/transition-guard.integration.test.ts +3 -3
  10. package/src/api/__tests__/api.test.ts +21 -3
  11. package/src/api/__tests__/batch.integration.test.ts +3 -3
  12. package/src/api/__tests__/body-limit.test.ts +7 -1
  13. package/src/api/__tests__/dispatcher-live.integration.test.ts +1 -1
  14. package/src/api/__tests__/http-route-rate-limit.integration.test.ts +7 -1
  15. package/src/api/__tests__/metrics-endpoint.test.ts +7 -1
  16. package/src/api/__tests__/nested-write.integration.test.ts +3 -3
  17. package/src/api/__tests__/server-boot-guards.test.ts +11 -4
  18. package/src/bun-db/__tests__/write-brand.test.ts +4 -1
  19. package/src/db/__tests__/apply-entity-event-tenant.integration.test.ts +1 -1
  20. package/src/db/__tests__/assert-exists-in.integration.test.ts +1 -1
  21. package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +2 -2
  22. package/src/db/__tests__/collect-table-metas.test.ts +9 -5
  23. package/src/db/__tests__/compound-types.test.ts +1 -1
  24. package/src/db/__tests__/config-seed.integration.test.ts +3 -3
  25. package/src/db/__tests__/eagerload.integration.test.ts +7 -7
  26. package/src/db/__tests__/entity-table-from-registry.test.ts +8 -2
  27. package/src/db/__tests__/entity-table-meta-source.test.ts +7 -3
  28. package/src/db/__tests__/entity-table-types.test.ts +4 -4
  29. package/src/db/__tests__/event-store-executor-list.integration.test.ts +9 -2
  30. package/src/db/__tests__/event-store-executor-reference-search.integration.test.ts +4 -4
  31. package/src/db/__tests__/event-store-executor-reference-sort.integration.test.ts +3 -3
  32. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +4 -4
  33. package/src/db/__tests__/event-store-executor.integration.test.ts +11 -6
  34. package/src/db/__tests__/event-store-system-stream.integration.test.ts +2 -2
  35. package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +6 -4
  36. package/src/db/__tests__/located-timestamp.test.ts +1 -1
  37. package/src/db/__tests__/money.test.ts +1 -1
  38. package/src/db/__tests__/multi-row-insert.integration.test.ts +3 -3
  39. package/src/db/__tests__/parent-ref-clause.test.ts +213 -0
  40. package/src/db/__tests__/schema-migration.integration.test.ts +30 -14
  41. package/src/db/__tests__/source-shadow-create.integration.test.ts +1 -1
  42. package/src/db/__tests__/table-builder-indexes.test.ts +14 -12
  43. package/src/db/__tests__/table-builder-required.test.ts +2 -2
  44. package/src/db/__tests__/tenant-db-where-merge.integration.test.ts +1 -1
  45. package/src/db/__tests__/tenant-db-where-merge.test.ts +5 -2
  46. package/src/db/__tests__/tenant-db.integration.test.ts +2 -2
  47. package/src/db/__tests__/unique-violation-mapping.integration.test.ts +2 -2
  48. package/src/db/event-store-executor-read.ts +97 -19
  49. package/src/db/parent-ref-clause.ts +182 -0
  50. package/src/db/table-builder.ts +7 -0
  51. package/src/engine/__tests__/boot-validator-action-wiring.test.ts +50 -8
  52. package/src/engine/__tests__/boot-validator-detail-for.test.ts +28 -4
  53. package/src/engine/__tests__/boot-validator-entity-list.test.ts +20 -4
  54. package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +9 -3
  55. package/src/engine/__tests__/boot-validator.test.ts +716 -91
  56. package/src/engine/__tests__/define-feature-entity-mapping.test.ts +2 -2
  57. package/src/engine/__tests__/engine.test.ts +128 -37
  58. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  59. package/src/engine/__tests__/entity-presave-wiring.integration.test.ts +4 -4
  60. package/src/engine/__tests__/entity-restore-tenant-isolation.integration.test.ts +1 -1
  61. package/src/engine/__tests__/entity-write-crosstenant.integration.test.ts +1 -1
  62. package/src/engine/__tests__/extend-entity-projection.test.ts +1 -1
  63. package/src/engine/__tests__/extends-registrar.test.ts +13 -3
  64. package/src/engine/__tests__/factories-long-text.test.ts +10 -6
  65. package/src/engine/__tests__/feature-crud-shorthand.test.ts +1 -1
  66. package/src/engine/__tests__/feature-crud-verb-access.integration.test.ts +1 -1
  67. package/src/engine/__tests__/multiselect-filter.integration.test.ts +1 -1
  68. package/src/engine/__tests__/nav.test.ts +1 -1
  69. package/src/engine/__tests__/pipeline-handler.integration.test.ts +1 -1
  70. package/src/engine/__tests__/pipeline-observability.integration.test.ts +1 -1
  71. package/src/engine/__tests__/pipeline-performance.integration.test.ts +1 -1
  72. package/src/engine/__tests__/projection-detail-actions.test.ts +6 -1
  73. package/src/engine/__tests__/projection-detail-singleton.test.ts +6 -1
  74. package/src/engine/__tests__/projection-detail-tabs.test.ts +6 -1
  75. package/src/engine/__tests__/projection-list-filter.integration.test.ts +7 -2
  76. package/src/engine/__tests__/projection.test.ts +1 -1
  77. package/src/engine/__tests__/registry.test.ts +13 -3
  78. package/src/engine/__tests__/schema-builder.test.ts +39 -11
  79. package/src/engine/__tests__/screen.test.ts +21 -15
  80. package/src/engine/__tests__/search-payload-extension.test.ts +15 -3
  81. package/src/engine/__tests__/store-table.test.ts +6 -3
  82. package/src/engine/__tests__/validate-projection-allowlist.test.ts +12 -4
  83. package/src/engine/__tests__/validation-hooks.test.ts +7 -1
  84. package/src/engine/boot-validator/__tests__/ownership.test.ts +2 -2
  85. package/src/engine/boot-validator/__tests__/parent-ref.test.ts +149 -0
  86. package/src/engine/boot-validator/action-wiring.ts +7 -7
  87. package/src/engine/boot-validator/index.ts +2 -0
  88. package/src/engine/boot-validator/parent-ref.ts +96 -0
  89. package/src/engine/boot-validator/projection-list-screens.ts +36 -23
  90. package/src/engine/boot-validator/query-output-columns.ts +2 -1
  91. package/src/engine/boot-validator/query-refs.ts +2 -1
  92. package/src/engine/boot-validator/screens.ts +322 -131
  93. package/src/engine/build-config-feature-schema.ts +2 -2
  94. package/src/engine/define-handler.ts +6 -0
  95. package/src/engine/entity-handlers.ts +6 -1
  96. package/src/engine/factories.ts +2 -0
  97. package/src/engine/feature-entity-handlers.ts +5 -0
  98. package/src/engine/index.ts +6 -0
  99. package/src/engine/ownership.ts +15 -0
  100. package/src/engine/pattern-library/mixed-schemas.ts +1 -0
  101. package/src/engine/screen-helpers.ts +25 -0
  102. package/src/engine/types/index.ts +5 -0
  103. package/src/es-ops/__tests__/runner.integration.test.ts +1 -1
  104. package/src/event-store/__tests__/upcaster.integration.test.ts +1 -1
  105. package/src/files/__tests__/file-field-pipeline.integration.test.ts +1 -1
  106. package/src/files/__tests__/files.integration.test.ts +1 -1
  107. package/src/files/file-ref-entity.ts +9 -9
  108. package/src/i18n/__tests__/i18n.test.ts +7 -1
  109. package/src/i18n/required-surface-keys.ts +48 -0
  110. package/src/migrations/__tests__/kumiko-drift.integration.test.ts +3 -1
  111. package/src/migrations/__tests__/pending-rebuilds.integration.test.ts +2 -2
  112. package/src/pipeline/__tests__/archive-stream.integration.test.ts +1 -1
  113. package/src/pipeline/__tests__/cascade-handler.integration.test.ts +23 -8
  114. package/src/pipeline/__tests__/cascade-handler.test.ts +18 -3
  115. package/src/pipeline/__tests__/causation-chain.integration.test.ts +1 -1
  116. package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +5 -3
  117. package/src/pipeline/__tests__/dispatcher.test.ts +42 -6
  118. package/src/pipeline/__tests__/domain-events-projections.integration.test.ts +2 -2
  119. package/src/pipeline/__tests__/fetch-for-writing.integration.test.ts +1 -1
  120. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +42 -6
  121. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +2 -2
  122. package/src/pipeline/__tests__/msp-multi-hop.integration.test.ts +1 -1
  123. package/src/pipeline/__tests__/msp-rebuild.integration.test.ts +6 -2
  124. package/src/pipeline/__tests__/multi-stream-projection.integration.test.ts +6 -2
  125. package/src/pipeline/__tests__/perf-rebuild.integration.test.ts +1 -1
  126. package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
  127. package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +3 -3
  128. package/src/pipeline/__tests__/query-projection.integration.test.ts +1 -1
  129. package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +2 -2
  130. package/src/pipeline/__tests__/search-consumer-named-events.integration.test.ts +13 -2
  131. package/src/pipeline/__tests__/try-append-event.integration.test.ts +1 -1
  132. package/src/search/__tests__/reindex-entity.integration.test.ts +6 -1
  133. package/src/search/__tests__/search-pii-derived-index.integration.test.ts +7 -1
  134. package/src/stack/__tests__/ownership-boot-guard.integration.test.ts +1 -1
  135. package/src/testing/__tests__/e2e-generator.test.ts +15 -5
  136. package/src/testing/e2e-generator.ts +2 -0
  137. package/src/testing/shared-entities.ts +4 -2
  138. package/src/ui-types/index.ts +4 -0
@@ -0,0 +1,96 @@
1
+ import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
2
+ import type { EntityDefinition, FeatureDefinition, QueryHandlerDef } from "../types";
3
+
4
+ // --- Parent-ref boot validation (fw#2766) ---
5
+ //
6
+ // Join-row entities (EntityDefinition.parentRef) derive both the write-path
7
+ // deny and the read-path SQL gate from one declaration. This is the boot-time
8
+ // half of the fail-closed guarantee: the read gate itself (db/parent-ref-
9
+ // clause.ts) passes an ungated query through when no ParentVisibilityOption
10
+ // was supplied — throwing there would 500 every custom handler and
11
+ // framework-internal detail() call that doesn't wire it, so the enforcement
12
+ // has to happen loudly here instead, once, at boot.
13
+
14
+ function findEntity(
15
+ featureMap: ReadonlyMap<string, FeatureDefinition>,
16
+ entityName: string,
17
+ ): EntityDefinition | undefined {
18
+ for (const f of featureMap.values()) {
19
+ const entity = f.entities?.[entityName];
20
+ if (entity) return entity;
21
+ }
22
+ return undefined;
23
+ }
24
+
25
+ function findQueryHandler(
26
+ featureMap: ReadonlyMap<string, FeatureDefinition>,
27
+ handlerName: string,
28
+ ): QueryHandlerDef | undefined {
29
+ for (const f of featureMap.values()) {
30
+ const handler = f.queryHandlers[handlerName];
31
+ if (handler) return handler;
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ export function validateParentRefs(
37
+ feature: FeatureDefinition,
38
+ featureMap: ReadonlyMap<string, FeatureDefinition>,
39
+ ): void {
40
+ for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
41
+ const parentRef = entity.parentRef;
42
+ if (parentRef === undefined) continue;
43
+
44
+ if (entity.fields[parentRef.entityTypeField] === undefined) {
45
+ throw new Error(
46
+ `[Kumiko ParentRef] entity "${entityName}".parentRef.entityTypeField references ` +
47
+ `"${parentRef.entityTypeField}" — which is not a declared field on "${entityName}" ` +
48
+ `(feature: "${feature.name}"). Fix: point entityTypeField at a real field on the entity.`,
49
+ );
50
+ }
51
+ if (entity.fields[parentRef.entityIdField] === undefined) {
52
+ throw new Error(
53
+ `[Kumiko ParentRef] entity "${entityName}".parentRef.entityIdField references ` +
54
+ `"${parentRef.entityIdField}" — which is not a declared field on "${entityName}" ` +
55
+ `(feature: "${feature.name}"). Fix: point entityIdField at a real field on the entity.`,
56
+ );
57
+ }
58
+
59
+ for (const hostName of parentRef.allowedTypes ?? []) {
60
+ const host = findEntity(featureMap, hostName);
61
+ if (host === undefined) {
62
+ throw new Error(
63
+ `[Kumiko ParentRef] entity "${entityName}".parentRef.allowedTypes names "${hostName}" ` +
64
+ `— no entity with that name is registered in this feature set (feature: ` +
65
+ `"${feature.name}"). Fix: remove "${hostName}" from allowedTypes, or register it.`,
66
+ );
67
+ }
68
+ if (host.parentRef !== undefined) {
69
+ throw new Error(
70
+ `[Kumiko ParentRef] entity "${entityName}".parentRef.allowedTypes names "${hostName}" ` +
71
+ `— which itself declares a parentRef (feature: "${feature.name}"). A join-row ` +
72
+ `entity can't act as a host for another join-row entity (no recursive gating). ` +
73
+ `Fix: remove "${hostName}" from allowedTypes, or drop its own parentRef.`,
74
+ );
75
+ }
76
+ }
77
+
78
+ for (const verb of ["list", "detail"] as const) {
79
+ const handlerName = `${entityName}:${verb}`;
80
+ const handler = findQueryHandler(featureMap, handlerName);
81
+ // skip: no handler registered for this verb — nothing to brand-check
82
+ if (handler === undefined) continue;
83
+ if (handler[ENTITY_CONVENTION_QUERY_BRAND] !== true) {
84
+ throw new Error(
85
+ `[Kumiko ParentRef] query handler "${handlerName}" does not carry ` +
86
+ `ENTITY_CONVENTION_QUERY_BRAND, but entity "${entityName}" declares a parentRef ` +
87
+ `(feature: "${feature.name}"). A hand-written handler bypasses the parent-ref ` +
88
+ `read-gate unless it explicitly wires parentVisibility. Fix: register "${handlerName}" ` +
89
+ `via defineEntityListHandler/defineEntityDetailHandler (or defineEntityQueryHandler), ` +
90
+ `or pass { parentVisibility: { entities: ctx.registry.getAllEntities() } } to ` +
91
+ `executor.${verb} yourself.`,
92
+ );
93
+ }
94
+ }
95
+ }
96
+ }
@@ -1,5 +1,11 @@
1
1
  import { QnTypes, qualifyEntityName } from "../qualified-name";
2
- import type { FeatureDefinition, ProjectionListScreenDefinition, QueryHandlerDef } from "../types";
2
+ import { isEditLayoutScreen } from "../screen-helpers";
3
+ import type {
4
+ EditRelatedListSection,
5
+ FeatureDefinition,
6
+ ProjectionListScreenDefinition,
7
+ QueryHandlerDef,
8
+ } from "../types";
3
9
  import { SEARCHABLE_FALSE_WHITELIST } from "./entity-list-screens";
4
10
  import { getZodObjectShape } from "./zod-shape";
5
11
 
@@ -122,6 +128,32 @@ export function validateProjectionListScreens(features: readonly FeatureDefiniti
122
128
  }
123
129
  }
124
130
 
131
+ function validateRelatedListSearchable(
132
+ prefix: string,
133
+ section: EditRelatedListSection,
134
+ schema: QueryHandlerDef["schema"] | undefined,
135
+ ): void {
136
+ // skip: not searchable, or the schema already accepts search — nothing to reject.
137
+ if (section.searchable !== true || schemaAccepts(schema, "search")) return;
138
+ throw new Error(
139
+ `${prefix}: searchable: true but query "${section.query}" has no "search" parameter in its Zod schema`,
140
+ );
141
+ }
142
+
143
+ function validateRelatedListFacets(
144
+ prefix: string,
145
+ section: EditRelatedListSection,
146
+ schema: QueryHandlerDef["schema"] | undefined,
147
+ ): void {
148
+ // skip: no facets declared — nothing to reject.
149
+ if (section.facets === undefined || section.facets.length === 0) return;
150
+ // skip: the schema already accepts filters — nothing to reject.
151
+ if (schemaAccepts(schema, "filters")) return;
152
+ throw new Error(
153
+ `${prefix}: declares facets but query "${section.query}" has no "filters" parameter in its Zod schema — add filters: z.array(z.object({ field: z.string(), op: z.literal("in"), value: z.unknown() })).optional() (or reuse entityListSchema's shape) to the handler's schema.`,
154
+ );
155
+ }
156
+
125
157
  // relatedList sections declare search/facets the same way a projectionList
126
158
  // screen does, and hit the same 422 footgun: definePagedQueryHandler doesn't
127
159
  // auto-merge params into the handler's own Zod schema (fw#2165/#2224), so an
@@ -133,32 +165,13 @@ export function validateRelatedListSectionQueries(features: readonly FeatureDefi
133
165
  const queryHandlers = buildQueryHandlerMap(features);
134
166
  for (const feature of features) {
135
167
  for (const screen of Object.values(feature.screens)) {
136
- if (
137
- screen.type !== "projectionDetail" &&
138
- screen.type !== "entityEdit" &&
139
- screen.type !== "actionForm" &&
140
- screen.type !== "configEdit"
141
- ) {
142
- continue;
143
- }
168
+ if (!isEditLayoutScreen(screen)) continue;
144
169
  for (const section of screen.layout.sections) {
145
170
  if (section.kind !== "relatedList") continue;
146
171
  const prefix = `[Feature ${feature.name}] Screen "${screen.id}" (${screen.type}) relatedList section "${section.title}"`;
147
172
  const schema = queryHandlers.get(section.query)?.schema;
148
- if (section.searchable === true && !schemaAccepts(schema, "search")) {
149
- throw new Error(
150
- `${prefix}: searchable: true but query "${section.query}" has no "search" parameter in its Zod schema`,
151
- );
152
- }
153
- if (
154
- section.facets !== undefined &&
155
- section.facets.length > 0 &&
156
- !schemaAccepts(schema, "filters")
157
- ) {
158
- throw new Error(
159
- `${prefix}: declares facets but query "${section.query}" has no "filters" parameter in its Zod schema — add filters: z.array(z.object({ field: z.string(), op: z.literal("in"), value: z.unknown() })).optional() (or reuse entityListSchema's shape) to the handler's schema.`,
160
- );
161
- }
173
+ validateRelatedListSearchable(prefix, section, schema);
174
+ validateRelatedListFacets(prefix, section, schema);
162
175
  }
163
176
  }
164
177
  }
@@ -219,7 +219,8 @@ function checkScreenOutputColumns(
219
219
  } else if (
220
220
  screen.type === "entityEdit" ||
221
221
  screen.type === "actionForm" ||
222
- screen.type === "configEdit"
222
+ screen.type === "configEdit" ||
223
+ screen.type === "secretMint"
223
224
  ) {
224
225
  checkEditLayoutOutputColumns(queryHandlers, featureName, screenId, screen.type, screen.layout);
225
226
  } else if (screen.type === "dashboard") {
@@ -111,7 +111,8 @@ function checkScreenQueryRefs(
111
111
  } else if (
112
112
  screen.type === "entityEdit" ||
113
113
  screen.type === "actionForm" ||
114
- screen.type === "configEdit"
114
+ screen.type === "configEdit" ||
115
+ screen.type === "secretMint"
115
116
  ) {
116
117
  checkEditLayoutQueryRefs(queryHandlers, featureName, screenId, screen.type, screen.layout);
117
118
  } else if (screen.type === "dashboard") {