@cosmicdrift/kumiko-framework 0.240.0 → 0.241.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.240.0",
3
+ "version": "0.241.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -198,7 +198,7 @@
198
198
  "./package.json": "./package.json"
199
199
  },
200
200
  "dependencies": {
201
- "@cosmicdrift/kumiko-types": "0.240.0",
201
+ "@cosmicdrift/kumiko-types": "0.241.0",
202
202
  "bullmq": "^5.76.7",
203
203
  "bun-types": "^1.3.13",
204
204
  "hono": "^4.13.1",
@@ -214,7 +214,7 @@
214
214
  "zod": "^4.4.3"
215
215
  },
216
216
  "devDependencies": {
217
- "@cosmicdrift/kumiko-dispatcher-live": "0.240.0",
217
+ "@cosmicdrift/kumiko-dispatcher-live": "0.241.0",
218
218
  "bun-types": "^1.3.13",
219
219
  "pino-pretty": "^13.1.3"
220
220
  },
package/src/changes.json CHANGED
@@ -1,4 +1,28 @@
1
1
  [
2
+ {
3
+ "version": "0.241.0",
4
+ "type": "improvement",
5
+ "title": "ProjectionDetailScreenDefinition gains singleton: boolean for self-service screens bound to a caller-scoped query (fw#2312).",
6
+ "detail": "A `projectionDetail` screen previously always resolved its row from a path id — the only case `ProjectionDetailBody` handled — so binding one to a `me`-style query (e.g. `user:query:user:me`) rendered nothing but a \"missing id\" error banner. With `singleton: true`, the query is called without the `idParam` key (there is no id to send) and any path id is ignored rather than forwarded into the query or into extension sections' entity-id resolution — a singleton row is server-picked, so no client-supplied id can reach it. The boot validator rejects `idParam` or `detailFor` declared together with `singleton` (both are meaningless once the server owns row selection)."
7
+ },
8
+ {
9
+ "version": "0.241.0",
10
+ "type": "improvement",
11
+ "title": "DashboardStatPanel gains an optional params field for static query parameters (fw#2312).",
12
+ "detail": "`DashboardStatPanel.params?: Readonly<Record<string, unknown>>` (`@cosmicdrift/kumiko-types`) is merged under the panel's dynamic `filterParams` by `@cosmicdrift/kumiko-renderer-web`'s dashboard body — lets a panel pin a value (e.g. a status facet) the screen-wide filter doesn't cover, without needing its own query. Additive; existing `dashboard` screens are unaffected."
13
+ },
14
+ {
15
+ "version": "0.241.0",
16
+ "type": "improvement",
17
+ "title": "EditFieldSpec.renderer gains a json format for pretty-printed structured fields (fw#2312).",
18
+ "detail": "`FieldFormatRegistry.json: { indent?: number }` (`@cosmicdrift/kumiko-types`, resolved by `@cosmicdrift/kumiko-headless`) — set `renderer: { format: \"json\", indent: 2 }` on an `EditFieldSpec` to pretty-print a JSON-string field instead of showing the raw escaped string. Pairs with the new `JsonView` primitive below when `@cosmicdrift/kumiko-renderer-web` is in use."
19
+ },
20
+ {
21
+ "version": "0.241.0",
22
+ "type": "improvement",
23
+ "title": "Optional JsonView primitive for structured, syntax-highlighted JSON display (fw#2312).",
24
+ "detail": "`CorePrimitives.JsonView` (`@cosmicdrift/kumiko-renderer`, web impl `DefaultJsonView` in `@cosmicdrift/kumiko-renderer-web`) receives the raw value (not a pre-stringified string) and stringifies + tokenizes itself; never throws on circular references, `BigInt`, or other non-serializable input. Optional on `CorePrimitives`, so an existing partial `CorePrimitives` test mock/provider keeps compiling unchanged — every call site (renderer's `format: \"json\"` fields, the jsonb/embedded/files/images fallback banner, `EditorPanel`'s unresolved-target args display) falls back to the prior `<Text>`/`<pre>` rendering when no `JsonView` is registered. A custom `PrimitivesProvider` that wants the improved rendering needs to add a `JsonView` implementation; nothing breaks if it doesn't."
25
+ },
2
26
  {
3
27
  "version": "0.235.0",
4
28
  "type": "breaking",
@@ -0,0 +1,81 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { z } from "zod";
3
+ import { withBootValidatorFixture } from "../../testing/boot-validator-fixture";
4
+ import { validateBoot as validateBootRaw } from "../boot-validator";
5
+ import { defineFeature } from "../define-feature";
6
+ import { createEntity, createTextField } from "../factories";
7
+
8
+ function validateBoot(features: Parameters<typeof validateBootRaw>[0]): void {
9
+ validateBootRaw(withBootValidatorFixture(features));
10
+ }
11
+
12
+ // fw#2312: `singleton: true` marks a projectionDetail screen whose query
13
+ // determines the shown row from the caller's context (session), not a row
14
+ // id in the path — a self-service screen like "my profile". idParam names
15
+ // the query-payload key for a path row id, which a singleton screen never
16
+ // sends; detailFor drives the auto-generated "Edit" action's navigation via
17
+ // that same path id (fw#2166). Both combinations are rejected at boot
18
+ // instead of silently winning (idParam ignored / edit button opening a
19
+ // blank create form).
20
+ describe("validateBoot — projectionDetail singleton (fw#2312)", () => {
21
+ test("singleton + idParam throws", () => {
22
+ const feature = defineFeature("app", (r) => {
23
+ r.queryHandler("me", z.object({}), async () => ({ id: "u1" }), {
24
+ access: { openToAll: true },
25
+ });
26
+ r.screen({
27
+ id: "profile",
28
+ type: "projectionDetail",
29
+ query: "app:query:me",
30
+ singleton: true,
31
+ idParam: "userId",
32
+ layout: { sections: [{ title: "s", fields: ["id"] }] },
33
+ });
34
+ });
35
+ expect(() => validateBoot([feature])).toThrow(
36
+ /Screen "profile" \(projectionDetail\) sets both singleton: true and idParam/,
37
+ );
38
+ });
39
+
40
+ test("singleton + detailFor throws", () => {
41
+ const feature = defineFeature("app", (r) => {
42
+ r.entity("user", createEntity({ fields: { name: createTextField() } }));
43
+ r.queryHandler("me", z.object({}), async () => ({ id: "u1" }), {
44
+ access: { openToAll: true },
45
+ });
46
+ r.screen({
47
+ id: "profile",
48
+ type: "projectionDetail",
49
+ query: "app:query:me",
50
+ singleton: true,
51
+ detailFor: "user",
52
+ layout: { sections: [{ title: "s", fields: ["id"] }] },
53
+ });
54
+ r.screen({
55
+ id: "user-edit",
56
+ type: "entityEdit",
57
+ entity: "user",
58
+ layout: { sections: [{ columns: 1, fields: ["name"] }] },
59
+ });
60
+ });
61
+ expect(() => validateBoot([feature])).toThrow(
62
+ /Screen "profile" \(projectionDetail\) sets both singleton: true and detailFor/,
63
+ );
64
+ });
65
+
66
+ test("singleton without idParam/detailFor boots cleanly", () => {
67
+ const feature = defineFeature("app", (r) => {
68
+ r.queryHandler("me", z.object({}), async () => ({ id: "u1" }), {
69
+ access: { openToAll: true },
70
+ });
71
+ r.screen({
72
+ id: "profile",
73
+ type: "projectionDetail",
74
+ query: "app:query:me",
75
+ singleton: true,
76
+ layout: { sections: [{ title: "s", fields: ["id"] }] },
77
+ });
78
+ });
79
+ expect(() => validateBoot([feature])).not.toThrow();
80
+ });
81
+ });
@@ -622,6 +622,26 @@ export function validateScreens(
622
622
  `[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has empty or non-string query.`,
623
623
  );
624
624
  }
625
+ if (screen.singleton === true && screen.idParam !== undefined) {
626
+ throw new Error(
627
+ `[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets both singleton: true ` +
628
+ `and idParam — singleton means the server picks the row from the caller's context, so there ` +
629
+ `is no row id to send under idParam. Remove idParam.`,
630
+ );
631
+ }
632
+ // singleton has no row id in the path, but the auto-generated "Edit"
633
+ // action (fw#2166, defaultEditAction in kumiko-screen.tsx) navigates
634
+ // to the detailFor entity's entityEdit screen using that path id —
635
+ // under singleton it would silently open that screen in CREATE mode
636
+ // instead of editing the singleton row. Reject rather than let that
637
+ // win silently, same as the idParam check above.
638
+ if (screen.singleton === true && screen.detailFor !== undefined) {
639
+ throw new Error(
640
+ `[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets both singleton: true ` +
641
+ `and detailFor — the auto-generated "Edit" action navigates using the path's row id, which a ` +
642
+ `singleton screen never has. Remove detailFor, or declare an explicit "edit" action instead.`,
643
+ );
644
+ }
625
645
  if (screen.layout.sections.length === 0) {
626
646
  throw new Error(
627
647
  `[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has an empty sections list — ` +