@cosmicdrift/kumiko-framework 0.202.0 → 0.204.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/package.json +3 -3
- package/src/bun-db/__tests__/sql-expr-brand.test.ts +33 -1
- package/src/db/__tests__/list-pagination.test.ts +28 -0
- package/src/db/dialect.ts +7 -8
- package/src/db/entity-table-meta.ts +4 -3
- package/src/db/event-store-executor-read.ts +26 -2
- package/src/db/sql-inventory.ts +16 -1
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +46 -0
- package/src/engine/__tests__/boot-validator-projection-list.test.ts +191 -0
- package/src/engine/__tests__/build-app-schema.test.ts +94 -0
- package/src/engine/__tests__/projection-detail-actions.test.ts +137 -0
- package/src/engine/__tests__/screen.test.ts +192 -0
- package/src/engine/boot-validator/action-wiring.ts +15 -3
- package/src/engine/boot-validator/index.ts +2 -0
- package/src/engine/boot-validator/projection-list-screens.ts +82 -0
- package/src/engine/boot-validator/screens.ts +92 -1
- package/src/engine/build-app-schema.ts +55 -2
- package/src/engine/index.ts +6 -1
- package/src/engine/screen-helpers.ts +5 -0
- package/src/i18n/required-surface-keys.ts +11 -0
- package/src/testing/e2e-generator.ts +4 -4
- package/src/ui-types/index.ts +2 -0
|
@@ -27,7 +27,15 @@
|
|
|
27
27
|
// Kontext um sie zu lesen. TODO wenn das ein realer Use-Case wird:
|
|
28
28
|
// `effectiveFeatures` Argument annehmen und über alle iterations filtern.
|
|
29
29
|
|
|
30
|
-
import
|
|
30
|
+
import { ZodObject, type ZodType } from "zod";
|
|
31
|
+
import type {
|
|
32
|
+
AppSchema,
|
|
33
|
+
EntityDefinition,
|
|
34
|
+
FeatureSchema,
|
|
35
|
+
ProjectionListScreenDefinition,
|
|
36
|
+
ScreenDefinition,
|
|
37
|
+
WorkspaceSchema,
|
|
38
|
+
} from "../ui-types";
|
|
31
39
|
import {
|
|
32
40
|
buildConfigFeatureSchema,
|
|
33
41
|
type ConfigFeatureSchema,
|
|
@@ -63,7 +71,7 @@ export function buildAppSchema(registry: Registry, options: BuildAppSchemaOption
|
|
|
63
71
|
const featureSchema: FeatureSchema = {
|
|
64
72
|
featureName,
|
|
65
73
|
entities: projectEntities(feature.entities ?? {}),
|
|
66
|
-
screens:
|
|
74
|
+
screens: projectScreens(feature.screens, registry),
|
|
67
75
|
...(navs.length > 0 && { navs }),
|
|
68
76
|
...(contentCollections.length > 0 && { contentCollections }),
|
|
69
77
|
// #1059: verbatim r.translations({keys}) — see FeatureSchema.translations
|
|
@@ -289,6 +297,51 @@ export function findNonJsonSafePath(value: unknown, path: string): string | null
|
|
|
289
297
|
return path;
|
|
290
298
|
}
|
|
291
299
|
|
|
300
|
+
// projectionList screens don't declare searchable/sortable/paginated as
|
|
301
|
+
// authoring intent — they're derived here from the bound query handler's
|
|
302
|
+
// Zod schema, the source of truth for what parameters it actually accepts
|
|
303
|
+
// (fw#2165). A hand-written `searchable: true` still wins over the derived
|
|
304
|
+
// default (screen.searchable ?? derived); the boot-validator (3a) rejects
|
|
305
|
+
// one that contradicts the schema.
|
|
306
|
+
function projectScreens(
|
|
307
|
+
screens: Readonly<Record<string, ScreenDefinition>>,
|
|
308
|
+
registry: Registry,
|
|
309
|
+
): ScreenDefinition[] {
|
|
310
|
+
return Object.values(screens).map((screen) =>
|
|
311
|
+
screen.type === "projectionList" ? projectProjectionListScreen(screen, registry) : screen,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function projectProjectionListScreen(
|
|
316
|
+
screen: ProjectionListScreenDefinition,
|
|
317
|
+
registry: Registry,
|
|
318
|
+
): ProjectionListScreenDefinition {
|
|
319
|
+
const schema = registry.getQueryHandler(screen.query)?.schema;
|
|
320
|
+
const capabilities = deriveProjectionListCapabilities(schema);
|
|
321
|
+
return {
|
|
322
|
+
...screen,
|
|
323
|
+
searchable: screen.searchable ?? capabilities.searchable,
|
|
324
|
+
sortable: capabilities.sortable,
|
|
325
|
+
paginated: capabilities.paginated,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Zod v4: a ZodObject's param names live on `.shape`. A schema that isn't a
|
|
330
|
+
// ZodObject (e.g. a z.union across payload shapes) yields no capability
|
|
331
|
+
// instead of throwing — same as a missing/unresolved query handler.
|
|
332
|
+
function deriveProjectionListCapabilities(schema: ZodType | undefined): {
|
|
333
|
+
searchable: boolean;
|
|
334
|
+
sortable: boolean;
|
|
335
|
+
paginated: boolean;
|
|
336
|
+
} {
|
|
337
|
+
const shape = schema instanceof ZodObject ? schema.shape : undefined;
|
|
338
|
+
return {
|
|
339
|
+
searchable: shape !== undefined && "search" in shape,
|
|
340
|
+
sortable: shape !== undefined && "sort" in shape,
|
|
341
|
+
paginated: shape !== undefined && ("cursor" in shape || "offset" in shape),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
292
345
|
function projectEntities(
|
|
293
346
|
entities: Readonly<Record<string, EntityDefinition>>,
|
|
294
347
|
): Readonly<Record<string, EntityDefinition>> {
|
package/src/engine/index.ts
CHANGED
|
@@ -227,7 +227,12 @@ export { runsInLane } from "./run-in";
|
|
|
227
227
|
export type { StepListOutcome } from "./run-pipeline";
|
|
228
228
|
export { runPipeline, runStepList } from "./run-pipeline";
|
|
229
229
|
export { buildInsertSchema, buildUpdateSchema, fieldToZod } from "./schema-builder";
|
|
230
|
-
export {
|
|
230
|
+
export {
|
|
231
|
+
isExtensionEditSection,
|
|
232
|
+
isFieldsEditSection,
|
|
233
|
+
normalizeEditField,
|
|
234
|
+
normalizeListColumn,
|
|
235
|
+
} from "./screen-helpers";
|
|
231
236
|
export type { TransitionGraph } from "./state-machine";
|
|
232
237
|
export { defineTransitions, guardTransition } from "./state-machine";
|
|
233
238
|
export {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
EditExtensionSection,
|
|
3
3
|
EditFieldSpec,
|
|
4
|
+
EditFieldsSection,
|
|
4
5
|
EditSectionSpec,
|
|
5
6
|
FieldCondition,
|
|
6
7
|
FormatSpec,
|
|
@@ -11,6 +12,10 @@ export function isExtensionEditSection(section: EditSectionSpec): section is Edi
|
|
|
11
12
|
return section.kind === "extension";
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export function isFieldsEditSection(section: EditSectionSpec): section is EditFieldsSection {
|
|
16
|
+
return section.kind === undefined || section.kind === "fields";
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
// Type guard — narrows FieldRenderer to FormatSpec. Useful for renderer
|
|
15
20
|
// authors who branch on the three FieldRenderer variants without manual
|
|
16
21
|
// "format" in renderer checks.
|
|
@@ -153,6 +153,7 @@ export function requiredKeysFromScreen(
|
|
|
153
153
|
pushKey(out, section.title);
|
|
154
154
|
continue;
|
|
155
155
|
}
|
|
156
|
+
if (section.kind === "relatedList") continue; // rejected at boot, unreachable here
|
|
156
157
|
pushKey(out, section.title);
|
|
157
158
|
for (const f of section.fields) {
|
|
158
159
|
const fieldName = editFieldName(f);
|
|
@@ -174,6 +175,7 @@ export function requiredKeysFromScreen(
|
|
|
174
175
|
pushKey(out, section.title);
|
|
175
176
|
continue;
|
|
176
177
|
}
|
|
178
|
+
if (section.kind === "relatedList") continue; // rejected at boot, unreachable here
|
|
177
179
|
pushKey(out, section.title);
|
|
178
180
|
for (const f of section.fields) {
|
|
179
181
|
const fieldName = editFieldName(f);
|
|
@@ -195,6 +197,7 @@ export function requiredKeysFromScreen(
|
|
|
195
197
|
pushKey(out, section.title);
|
|
196
198
|
continue;
|
|
197
199
|
}
|
|
200
|
+
if (section.kind === "relatedList") continue; // rejected at boot, unreachable here
|
|
198
201
|
pushKey(out, section.title);
|
|
199
202
|
for (const f of section.fields) {
|
|
200
203
|
const fieldName = editFieldName(f);
|
|
@@ -209,6 +212,14 @@ export function requiredKeysFromScreen(
|
|
|
209
212
|
const detail = screen as ProjectionDetailScreenDefinition;
|
|
210
213
|
for (const section of detail.layout.sections) {
|
|
211
214
|
if (isExtensionEditSection(section)) continue; // rejected at boot, unreachable here
|
|
215
|
+
if (section.kind === "relatedList") {
|
|
216
|
+
pushKey(out, section.title);
|
|
217
|
+
for (const col of section.columns) {
|
|
218
|
+
const normalized = normalizeListColumn(col);
|
|
219
|
+
if (normalized.label !== undefined) pushKey(out, normalized.label);
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
212
223
|
pushKey(out, section.title);
|
|
213
224
|
for (const f of section.fields) {
|
|
214
225
|
const fieldName = editFieldName(f);
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
type EntityEditScreenDefinition,
|
|
31
31
|
type EntityListScreenDefinition,
|
|
32
32
|
type FieldDefinition,
|
|
33
|
-
|
|
33
|
+
isFieldsEditSection,
|
|
34
34
|
normalizeEditField,
|
|
35
35
|
normalizeListColumn,
|
|
36
36
|
parseQn,
|
|
@@ -243,7 +243,7 @@ function collectRequiredEditFields(
|
|
|
243
243
|
): string[] {
|
|
244
244
|
const out: string[] = [];
|
|
245
245
|
for (const section of screen.layout.sections) {
|
|
246
|
-
if (
|
|
246
|
+
if (!isFieldsEditSection(section)) continue;
|
|
247
247
|
for (const rawField of section.fields) {
|
|
248
248
|
const { field } = normalizeEditField(rawField);
|
|
249
249
|
const def = entity.fields[field];
|
|
@@ -281,7 +281,7 @@ function buildEditFillOps(
|
|
|
281
281
|
// form; Felder ohne Fixture-Wert (file/image/…) werden übersprungen.
|
|
282
282
|
const ops: EditFillOp[] = [];
|
|
283
283
|
for (const section of screen.layout.sections) {
|
|
284
|
-
if (
|
|
284
|
+
if (!isFieldsEditSection(section)) continue;
|
|
285
285
|
for (const raw of section.fields) {
|
|
286
286
|
const { field } = normalizeEditField(raw);
|
|
287
287
|
const def = entity.fields[field];
|
|
@@ -329,7 +329,7 @@ function pickIdentifyingForEdit(
|
|
|
329
329
|
: undefined;
|
|
330
330
|
|
|
331
331
|
for (const section of editScreen.layout.sections) {
|
|
332
|
-
if (
|
|
332
|
+
if (!isFieldsEditSection(section)) continue;
|
|
333
333
|
for (const rawField of section.fields) {
|
|
334
334
|
const { field } = normalizeEditField(rawField);
|
|
335
335
|
if (entity.fields[field]?.type !== "text") continue;
|
package/src/ui-types/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export { parseRefTarget } from "../engine/parse-ref-target";
|
|
|
28
28
|
export {
|
|
29
29
|
evalFieldCondition,
|
|
30
30
|
isExtensionEditSection,
|
|
31
|
+
isFieldsEditSection,
|
|
31
32
|
isFormatSpec,
|
|
32
33
|
normalizeEditField,
|
|
33
34
|
normalizeListColumn,
|
|
@@ -71,6 +72,7 @@ export type {
|
|
|
71
72
|
EditFieldSpec,
|
|
72
73
|
EditFieldsSection,
|
|
73
74
|
EditLayout,
|
|
75
|
+
EditRelatedListSection,
|
|
74
76
|
EditSectionSpec,
|
|
75
77
|
EntityEditScreenDefinition,
|
|
76
78
|
EntityListScreenDefinition,
|