@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.
- package/package.json +3 -3
- package/src/__tests__/anonymous-access.integration.test.ts +3 -3
- package/src/__tests__/derived-fields.integration.test.ts +1 -1
- package/src/__tests__/entity-list-limits.integration.test.ts +1 -1
- package/src/__tests__/error-contract.integration.test.ts +1 -1
- package/src/__tests__/ownership-where-write-path.integration.test.ts +1 -1
- package/src/__tests__/ownership.integration.test.ts +7 -3
- package/src/__tests__/schema-cli-rebuild.integration.test.ts +2 -2
- package/src/__tests__/transition-guard.integration.test.ts +3 -3
- package/src/api/__tests__/api.test.ts +21 -3
- package/src/api/__tests__/batch.integration.test.ts +3 -3
- package/src/api/__tests__/body-limit.test.ts +7 -1
- package/src/api/__tests__/dispatcher-live.integration.test.ts +1 -1
- package/src/api/__tests__/http-route-rate-limit.integration.test.ts +7 -1
- package/src/api/__tests__/metrics-endpoint.test.ts +7 -1
- package/src/api/__tests__/nested-write.integration.test.ts +3 -3
- package/src/api/__tests__/server-boot-guards.test.ts +11 -4
- package/src/bun-db/__tests__/write-brand.test.ts +4 -1
- package/src/db/__tests__/apply-entity-event-tenant.integration.test.ts +1 -1
- package/src/db/__tests__/assert-exists-in.integration.test.ts +1 -1
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +2 -2
- package/src/db/__tests__/collect-table-metas.test.ts +9 -5
- package/src/db/__tests__/compound-types.test.ts +1 -1
- package/src/db/__tests__/config-seed.integration.test.ts +3 -3
- package/src/db/__tests__/eagerload.integration.test.ts +7 -7
- package/src/db/__tests__/entity-table-from-registry.test.ts +8 -2
- package/src/db/__tests__/entity-table-meta-source.test.ts +7 -3
- package/src/db/__tests__/entity-table-types.test.ts +4 -4
- package/src/db/__tests__/event-store-executor-list.integration.test.ts +9 -2
- package/src/db/__tests__/event-store-executor-reference-search.integration.test.ts +4 -4
- package/src/db/__tests__/event-store-executor-reference-sort.integration.test.ts +3 -3
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +4 -4
- package/src/db/__tests__/event-store-executor.integration.test.ts +11 -6
- package/src/db/__tests__/event-store-system-stream.integration.test.ts +2 -2
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +6 -4
- package/src/db/__tests__/located-timestamp.test.ts +1 -1
- package/src/db/__tests__/money.test.ts +1 -1
- package/src/db/__tests__/multi-row-insert.integration.test.ts +3 -3
- package/src/db/__tests__/parent-ref-clause.test.ts +213 -0
- package/src/db/__tests__/schema-migration.integration.test.ts +30 -14
- package/src/db/__tests__/source-shadow-create.integration.test.ts +1 -1
- package/src/db/__tests__/table-builder-indexes.test.ts +14 -12
- package/src/db/__tests__/table-builder-required.test.ts +2 -2
- package/src/db/__tests__/tenant-db-where-merge.integration.test.ts +1 -1
- package/src/db/__tests__/tenant-db-where-merge.test.ts +5 -2
- package/src/db/__tests__/tenant-db.integration.test.ts +2 -2
- package/src/db/__tests__/unique-violation-mapping.integration.test.ts +2 -2
- package/src/db/event-store-executor-read.ts +97 -19
- package/src/db/parent-ref-clause.ts +182 -0
- package/src/db/table-builder.ts +7 -0
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +50 -8
- package/src/engine/__tests__/boot-validator-detail-for.test.ts +28 -4
- package/src/engine/__tests__/boot-validator-entity-list.test.ts +20 -4
- package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +9 -3
- package/src/engine/__tests__/boot-validator.test.ts +716 -91
- package/src/engine/__tests__/define-feature-entity-mapping.test.ts +2 -2
- package/src/engine/__tests__/engine.test.ts +128 -37
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/entity-presave-wiring.integration.test.ts +4 -4
- package/src/engine/__tests__/entity-restore-tenant-isolation.integration.test.ts +1 -1
- package/src/engine/__tests__/entity-write-crosstenant.integration.test.ts +1 -1
- package/src/engine/__tests__/extend-entity-projection.test.ts +1 -1
- package/src/engine/__tests__/extends-registrar.test.ts +13 -3
- package/src/engine/__tests__/factories-long-text.test.ts +10 -6
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +1 -1
- package/src/engine/__tests__/feature-crud-verb-access.integration.test.ts +1 -1
- package/src/engine/__tests__/multiselect-filter.integration.test.ts +1 -1
- package/src/engine/__tests__/nav.test.ts +1 -1
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +1 -1
- package/src/engine/__tests__/pipeline-observability.integration.test.ts +1 -1
- package/src/engine/__tests__/pipeline-performance.integration.test.ts +1 -1
- package/src/engine/__tests__/projection-detail-actions.test.ts +6 -1
- package/src/engine/__tests__/projection-detail-singleton.test.ts +6 -1
- package/src/engine/__tests__/projection-detail-tabs.test.ts +6 -1
- package/src/engine/__tests__/projection-list-filter.integration.test.ts +7 -2
- package/src/engine/__tests__/projection.test.ts +1 -1
- package/src/engine/__tests__/registry.test.ts +13 -3
- package/src/engine/__tests__/schema-builder.test.ts +39 -11
- package/src/engine/__tests__/screen.test.ts +21 -15
- package/src/engine/__tests__/search-payload-extension.test.ts +15 -3
- package/src/engine/__tests__/store-table.test.ts +6 -3
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +12 -4
- package/src/engine/__tests__/validation-hooks.test.ts +7 -1
- package/src/engine/boot-validator/__tests__/ownership.test.ts +2 -2
- package/src/engine/boot-validator/__tests__/parent-ref.test.ts +149 -0
- package/src/engine/boot-validator/action-wiring.ts +7 -7
- package/src/engine/boot-validator/index.ts +2 -0
- package/src/engine/boot-validator/parent-ref.ts +96 -0
- package/src/engine/boot-validator/projection-list-screens.ts +36 -23
- package/src/engine/boot-validator/query-output-columns.ts +2 -1
- package/src/engine/boot-validator/query-refs.ts +2 -1
- package/src/engine/boot-validator/screens.ts +322 -131
- package/src/engine/build-config-feature-schema.ts +2 -2
- package/src/engine/define-handler.ts +6 -0
- package/src/engine/entity-handlers.ts +6 -1
- package/src/engine/factories.ts +2 -0
- package/src/engine/feature-entity-handlers.ts +5 -0
- package/src/engine/index.ts +6 -0
- package/src/engine/ownership.ts +15 -0
- package/src/engine/pattern-library/mixed-schemas.ts +1 -0
- package/src/engine/screen-helpers.ts +25 -0
- package/src/engine/types/index.ts +5 -0
- package/src/es-ops/__tests__/runner.integration.test.ts +1 -1
- package/src/event-store/__tests__/upcaster.integration.test.ts +1 -1
- package/src/files/__tests__/file-field-pipeline.integration.test.ts +1 -1
- package/src/files/__tests__/files.integration.test.ts +1 -1
- package/src/files/file-ref-entity.ts +9 -9
- package/src/i18n/__tests__/i18n.test.ts +7 -1
- package/src/i18n/required-surface-keys.ts +48 -0
- package/src/migrations/__tests__/kumiko-drift.integration.test.ts +3 -1
- package/src/migrations/__tests__/pending-rebuilds.integration.test.ts +2 -2
- package/src/pipeline/__tests__/archive-stream.integration.test.ts +1 -1
- package/src/pipeline/__tests__/cascade-handler.integration.test.ts +23 -8
- package/src/pipeline/__tests__/cascade-handler.test.ts +18 -3
- package/src/pipeline/__tests__/causation-chain.integration.test.ts +1 -1
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +5 -3
- package/src/pipeline/__tests__/dispatcher.test.ts +42 -6
- package/src/pipeline/__tests__/domain-events-projections.integration.test.ts +2 -2
- package/src/pipeline/__tests__/fetch-for-writing.integration.test.ts +1 -1
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +42 -6
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +2 -2
- package/src/pipeline/__tests__/msp-multi-hop.integration.test.ts +1 -1
- package/src/pipeline/__tests__/msp-rebuild.integration.test.ts +6 -2
- package/src/pipeline/__tests__/multi-stream-projection.integration.test.ts +6 -2
- package/src/pipeline/__tests__/perf-rebuild.integration.test.ts +1 -1
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +3 -3
- package/src/pipeline/__tests__/query-projection.integration.test.ts +1 -1
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +2 -2
- package/src/pipeline/__tests__/search-consumer-named-events.integration.test.ts +13 -2
- package/src/pipeline/__tests__/try-append-event.integration.test.ts +1 -1
- package/src/search/__tests__/reindex-entity.integration.test.ts +6 -1
- package/src/search/__tests__/search-pii-derived-index.integration.test.ts +7 -1
- package/src/stack/__tests__/ownership-boot-guard.integration.test.ts +1 -1
- package/src/testing/__tests__/e2e-generator.test.ts +15 -5
- package/src/testing/e2e-generator.ts +2 -0
- package/src/testing/shared-entities.ts +4 -2
- 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
|
|
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
|
-
|
|
149
|
-
|
|
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") {
|