@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.
@@ -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 type { AppSchema, EntityDefinition, FeatureSchema, WorkspaceSchema } from "../ui-types";
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: Object.values(feature.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>> {
@@ -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 { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "./screen-helpers";
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
- isExtensionEditSection,
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 (isExtensionEditSection(section)) continue;
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 (isExtensionEditSection(section)) continue;
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 (isExtensionEditSection(section)) continue;
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;
@@ -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,