@cosmicdrift/kumiko-framework 0.373.0 → 0.377.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.
Files changed (76) hide show
  1. package/AGENTS.md +2 -2
  2. package/agents/examples/apex-landing.md +1 -1
  3. package/agents/examples/apex-surface-auth.md +3 -3
  4. package/agents/examples/compose-stacks.md +15 -8
  5. package/agents/examples/embedded-entity-form.md +1 -1
  6. package/agents/examples/notes-history-basic.md +1 -1
  7. package/agents/examples/tags-basic.md +1 -1
  8. package/agents/examples/user-profile.md +1 -1
  9. package/agents/examples/wizard-form.md +4 -4
  10. package/agents/widgets.md +4 -3
  11. package/dist/api/index.d.ts +1 -1
  12. package/dist/api/index.js +1 -1
  13. package/dist/api/routes.js +2 -2
  14. package/dist/api/server.js +2 -1
  15. package/dist/config/index.d.ts +4 -0
  16. package/dist/db/index.d.ts +2 -2
  17. package/dist/db/index.js +1 -1
  18. package/dist/db/migrate-runner.js +12 -4
  19. package/dist/db/money.js +1 -1
  20. package/dist/db/query.js +14 -14
  21. package/dist/db/replay-migration-sql.d.ts +1 -0
  22. package/dist/db/replay-migration-sql.js +115 -11
  23. package/dist/db/tenant-db.d.ts +7 -0
  24. package/dist/db/tenant-db.js +17 -5
  25. package/dist/engine/boot-validator/projection-list-screens.js +28 -37
  26. package/dist/engine/boot-validator/query-refs.js +26 -8
  27. package/dist/engine/boot-validator/screens.d.ts +1 -1
  28. package/dist/engine/boot-validator/screens.js +334 -42
  29. package/dist/engine/boot-validator/url-prefill-fields.js +3 -0
  30. package/dist/engine/build-app-schema.js +7 -3
  31. package/dist/engine/entity-list-searchability.d.ts +5 -0
  32. package/dist/engine/entity-list-searchability.js +35 -0
  33. package/dist/engine/feature-ui-extensions.js +5 -2
  34. package/dist/engine/index.d.ts +3 -1
  35. package/dist/engine/index.js +2 -0
  36. package/dist/engine/paged-list.d.ts +29 -0
  37. package/dist/engine/paged-list.js +56 -0
  38. package/dist/engine/parse-ref-target.js +1 -1
  39. package/dist/engine/project-app-schema-for-roles.js +37 -6
  40. package/dist/engine/provider-plugin-context.d.ts +3 -0
  41. package/dist/engine/provider-plugin-context.js +28 -0
  42. package/dist/engine/schema-builder.js +7 -0
  43. package/dist/engine/types/index.d.ts +2 -2
  44. package/dist/env/index.js +1 -1
  45. package/dist/errors/zod-bridge.js +1 -1
  46. package/dist/files/file-ref-entity.d.ts +6 -6
  47. package/dist/files/file-ref-table.d.ts +6 -6
  48. package/dist/files/index.d.ts +1 -1
  49. package/dist/files/index.js +1 -1
  50. package/dist/files/provider-resolver.d.ts +17 -17
  51. package/dist/files/provider-resolver.js +1 -1
  52. package/dist/http/egress.d.ts +14 -0
  53. package/dist/http/egress.js +144 -0
  54. package/dist/http/index.d.ts +3 -2
  55. package/dist/http/index.js +2 -1
  56. package/dist/http/policy.d.ts +30 -0
  57. package/dist/http/policy.js +299 -0
  58. package/dist/i18n/required-surface-keys.js +52 -2
  59. package/dist/jobs/job-runner.js +4 -2
  60. package/dist/pipeline/config-accessor-factory.d.ts +3 -0
  61. package/dist/pipeline/config-accessor-factory.js +3 -0
  62. package/dist/pipeline/dispatch-shared.js +7 -5
  63. package/dist/pipeline/index.d.ts +1 -0
  64. package/dist/pipeline/index.js +1 -0
  65. package/dist/ui-types/index.d.ts +1 -1
  66. package/dist/upgrade-changelog.d.ts +25 -0
  67. package/dist/upgrade-changelog.js +48 -0
  68. package/dist/upgrade-cli.js +78 -13
  69. package/package.json +3 -5
  70. package/presets/biome-app.json +8 -0
  71. package/src/changes.json +132 -0
  72. package/src/scripts/codemod/README.md +25 -0
  73. package/src/scripts/codemod/codemod-support.ts +64 -6
  74. package/src/scripts/codemod/package-merge.ts +260 -0
  75. package/src/scripts/codemod/s3-imports.ts +4 -42
  76. package/src/scripts/codemod/testing-injection.ts +135 -0
@@ -36,6 +36,13 @@ export declare function acknowledgeConventionCrossTenant(tenantDb: TenantDb, rea
36
36
  readonly deferReport?: boolean;
37
37
  }): TenantDb;
38
38
  export declare function reportConventionCrossTenantUse(acknowledged: TenantDb, target?: EscapeHatchTarget): void;
39
+ export type OutsideTransactionContext = {
40
+ readonly dbOutsideTransaction: TenantDb | undefined;
41
+ };
42
+ export declare function hasOutsideTransactionDb(ctx: OutsideTransactionContext): ctx is OutsideTransactionContext & {
43
+ readonly dbOutsideTransaction: TenantDb;
44
+ };
45
+ export declare function requireOutsideTransactionDb(ctx: OutsideTransactionContext): TenantDb;
39
46
  export declare function runInOwnTransaction<T>(tenantDb: TenantDb, fn: (txDb: TenantDb) => Promise<T>): Promise<T>;
40
47
  export type CreateTenantDbOptions = {
41
48
  readonly db: DbRunner;
@@ -103,7 +103,7 @@ function buildUncheckedSystemDb(db, dbOutsideTransaction, report, gate) {
103
103
  // AccessDeniedError) because this is a dispatch wiring fault, not a
104
104
  // tenant-access denial — mirrors the "no database connection configured"
105
105
  // case in dispatch-shared.ts's appendDomainEvent.
106
- function requireOutsideTransactionDb() {
106
+ function requireSystemOutsideTransactionDb() {
107
107
  if (!dbOutsideTransaction) {
108
108
  throw new InternalError({
109
109
  message: "systemScope() outsideTransaction check failed: no outside-transaction database " +
@@ -173,13 +173,13 @@ function buildUncheckedSystemDb(db, dbOutsideTransaction, report, gate) {
173
173
  message: `systemScope() outsideTransaction tenant self-check failed: expected "${db.tenantId}", got "${tenantId}"`,
174
174
  });
175
175
  }
176
- return requireOutsideTransactionDb();
176
+ return requireSystemOutsideTransactionDb();
177
177
  },
178
178
  acknowledgeCrossTenant(reason) {
179
179
  if (reason.trim().length === 0) {
180
180
  throw new Error("acknowledgeCrossTenant requires a non-empty reason");
181
181
  }
182
- const result = requireOutsideTransactionDb();
182
+ const result = requireSystemOutsideTransactionDb();
183
183
  report("acknowledge-cross-tenant", reason);
184
184
  return result;
185
185
  },
@@ -301,12 +301,24 @@ export function reportConventionCrossTenantUse(acknowledged, target) {
301
301
  reporter(target);
302
302
  }
303
303
  const ownTransactionRebinders = new WeakMap();
304
+ export function hasOutsideTransactionDb(ctx) {
305
+ return ctx.dbOutsideTransaction !== undefined;
306
+ }
307
+ export function requireOutsideTransactionDb(ctx) {
308
+ if (!hasOutsideTransactionDb(ctx)) {
309
+ throw new InternalError({
310
+ message: "ctx.dbOutsideTransaction is not available in this dispatch phase (member-resolution, before-transaction denial) or metering tenant mode.",
311
+ });
312
+ }
313
+ return ctx.dbOutsideTransaction;
314
+ }
304
315
  export async function runInOwnTransaction(tenantDb, fn) {
305
316
  const rebind = ownTransactionRebinders.get(tenantDb);
306
317
  if (!rebind) {
307
318
  throw new InternalError({
308
- message: "runInOwnTransaction received a TenantDb not built by createTenantDb — " +
309
- "no transaction rebinder bound.",
319
+ message: "runInOwnTransaction received a TenantDb without a transaction rebinder: either it was not " +
320
+ "built by createTenantDb, or it is the fail-closed ctx.db/ctx.dbOutsideTransaction of an " +
321
+ "r.systemScope() handler (use ctx.systemDb.outsideTransaction there).",
310
322
  });
311
323
  }
312
324
  return rebind(fn);
@@ -1,5 +1,5 @@
1
- import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
2
- import { parseQn, QnTypes, qualifyEntityName } from "../qualified-name.js";
1
+ import { isListQueryWithoutSearchableFields, resolveEntityBehindListQuery, } from "../entity-list-searchability.js";
2
+ import { QnTypes, qualifyEntityName } from "../qualified-name.js";
3
3
  import { isEditLayoutScreen } from "../screen-helpers.js";
4
4
  import { relatedListGroupHeaderLabel, relatedListGroupKey, } from "../types/index.js";
5
5
  import { SEARCHABLE_FALSE_WHITELIST } from "./entity-list-screens.js";
@@ -28,28 +28,38 @@ function schemaAccepts(schema, key) {
28
28
  const shape = getZodObjectShape(schema);
29
29
  return shape !== undefined && key in shape;
30
30
  }
31
- function validateOneProjectionListScreen(feature, screen, queryHandlers) {
31
+ // sortable/paginated are derived by buildAppSchema from the query's Zod
32
+ // schema (fw#2165) — there is no separate wire type from the author-facing
33
+ // ProjectionListScreenDefinition, so hand-authoring them would otherwise be
34
+ // silently overwritten with no signal to the author. Reject outright.
35
+ function rejectHandAuthoredDerivedFlags(prefix, screen) {
36
+ if (screen.sortable !== undefined) {
37
+ throw new Error(`${prefix}: sortable is derived from the query's Zod schema, don't set it`);
38
+ }
39
+ if (screen.paginated !== undefined) {
40
+ throw new Error(`${prefix}: paginated is derived from the query's Zod schema, don't set it`);
41
+ }
42
+ }
43
+ function validateOneProjectionListScreen(feature, screen, queryHandlers, featureMap) {
32
44
  const prefix = `[projectionList] Feature "${feature.name}" screen "${screen.id}"`;
33
- const schema = queryHandlers.get(screen.query)?.schema;
45
+ const handler = queryHandlers.get(screen.query);
46
+ const schema = handler?.schema;
47
+ // buildAppSchema derives searchable: false for these, so no whitelist entry is needed.
48
+ const searchDerivedOff = isListQueryWithoutSearchableFields(screen.query, handler, (name) => findEntity(featureMap, name));
34
49
  if (screen.searchable === true && !schemaAccepts(schema, "search")) {
35
50
  throw new Error(`${prefix}: searchable: true but query "${screen.query}" has no "search" parameter in its Zod schema. Add a "search" field to the query schema or remove searchable: true.`);
36
51
  }
52
+ if (screen.searchable === true && searchDerivedOff) {
53
+ throw new Error(`${prefix}: searchable: true but query "${screen.query}" lists an entity with no searchable field, so a search would never match. Mark a field searchable: true on the entity or remove searchable: true.`);
54
+ }
37
55
  if (screen.searchable === false &&
38
56
  schemaAccepts(schema, "search") &&
57
+ !searchDerivedOff &&
39
58
  !SEARCHABLE_FALSE_WHITELIST.has(screen.id)) {
40
59
  throw new Error(`${prefix}: query "${screen.query}" accepts "search" but searchable: false disables it — remove searchable: false or add "${screen.id}" to SEARCHABLE_FALSE_WHITELIST`);
41
60
  }
42
- // sortable/paginated are derived by buildAppSchema from the query's Zod
43
- // schema (fw#2165) — there is no separate wire type from the author-facing
44
- // ProjectionListScreenDefinition, so hand-authoring them would otherwise be
45
- // silently overwritten with no signal to the author. Reject outright.
46
- if (screen.sortable !== undefined) {
47
- throw new Error(`${prefix}: sortable is derived from the query's Zod schema, don't set it`);
48
- }
49
- if (screen.paginated !== undefined) {
50
- throw new Error(`${prefix}: paginated is derived from the query's Zod schema, don't set it`);
51
- }
52
- const searchActive = screen.searchable !== false && schemaAccepts(schema, "search");
61
+ rejectHandAuthoredDerivedFlags(prefix, screen);
62
+ const searchActive = screen.searchable !== false && !searchDerivedOff && schemaAccepts(schema, "search");
53
63
  const sortActive = schemaAccepts(schema, "sort");
54
64
  if ((searchActive || sortActive) && screen.defaultSort === undefined) {
55
65
  throw new Error(`${prefix}: defaultSort required when search or sort is active. Set defaultSort: { field, direction }.`);
@@ -132,11 +142,12 @@ function validateProjectionListFacetsSchemaAcceptance(prefix, screen, schema) {
132
142
  }
133
143
  export function validateProjectionListScreens(features) {
134
144
  const queryHandlers = buildQueryHandlerMap(features);
145
+ const featureMap = new Map(features.map((f) => [f.name, f]));
135
146
  for (const feature of features) {
136
147
  for (const screen of Object.values(feature.screens)) {
137
148
  if (screen.type !== "projectionList")
138
149
  continue;
139
- validateOneProjectionListScreen(feature, screen, queryHandlers);
150
+ validateOneProjectionListScreen(feature, screen, queryHandlers, featureMap);
140
151
  }
141
152
  }
142
153
  }
@@ -155,26 +166,6 @@ function validateRelatedListFacets(prefix, section, schema) {
155
166
  return;
156
167
  throw new Error(`${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.`);
157
168
  }
158
- // parentFilter resolves an entity field map to
159
- // check `field` against ONLY when the query is one of the entity-convention
160
- // factories (defineEntityListHandler et al., branded by
161
- // ENTITY_CONVENTION_QUERY_BRAND) — a hand-written handler carries no entity,
162
- // same "capability absent, no throw" policy as schemaAccepts above. The
163
- // entity name comes from the handler's own `<entityName>:list` naming
164
- // convention (parseHandlerName in entity-handlers.ts), recovered here from
165
- // the query's fully qualified name.
166
- function resolveEntityBehindQuery(queryQn, handler, featureMap) {
167
- if (handler === undefined || handler[ENTITY_CONVENTION_QUERY_BRAND] !== true)
168
- return undefined;
169
- let entityName;
170
- try {
171
- entityName = parseQn(queryQn).name.split(":")[0] ?? "";
172
- }
173
- catch {
174
- return undefined;
175
- }
176
- return entityName === "" ? undefined : findEntity(featureMap, entityName);
177
- }
178
169
  // parentFilter lets a relatedList reuse the
179
170
  // generic `<entity>:list` query instead of a bespoke child-rows handler —
180
171
  // same 422 footgun as facets/filter above, plus its own mutual-exclusivity
@@ -193,7 +184,7 @@ function validateRelatedListParentFilter(prefix, section, schema, handler, featu
193
184
  `Zod schema — add filter: z.object({ field: z.string(), op: z.enum(["eq","ne","lt","gt","lte","gte","in"]), ` +
194
185
  `value: z.unknown() }).optional() (or reuse entityListSchema's shape) to the handler's schema.`);
195
186
  }
196
- const entity = resolveEntityBehindQuery(section.query, handler, featureMap);
187
+ const entity = resolveEntityBehindListQuery(section.query, handler, (name) => findEntity(featureMap, name));
197
188
  if (entity !== undefined && entity.fields[section.parentFilter.field] === undefined) {
198
189
  throw new Error(`${prefix}: parentFilter.field "${section.parentFilter.field}" is not a declared field on the ` +
199
190
  `entity behind query "${section.query}". Known fields: ${Object.keys(entity.fields).sort().join(", ")}.`);
@@ -1,3 +1,4 @@
1
+ import { isFieldsEditSection, normalizeEditField, sectionFieldSpecs } from "../screen-helpers.js";
1
2
  import { buildQueryHandlerMap } from "./projection-list-screens.js";
2
3
  const NOT_REGISTERED_SUFFIX = "is not a registered query-handler. Check the QN spelling (expected " +
3
4
  '"<feature>:query:<short>") and that the handler is declared via r.queryHandler(...).';
@@ -29,19 +30,30 @@ function checkVisibleWhenQueryRef(queryHandlers, query, buildPrefix) {
29
30
  // types) keeps this collector correct if that restriction is ever lifted.
30
31
  function checkEditLayoutQueryRefs(queryHandlers, featureName, screenId, screenType, layout) {
31
32
  for (const section of layout.sections) {
33
+ if (isFieldsEditSection(section)) {
34
+ for (const fieldSpec of sectionFieldSpecs(section)) {
35
+ const { field, fillAction } = normalizeEditField(fieldSpec);
36
+ if (fillAction === undefined)
37
+ continue;
38
+ checkQueryRef(queryHandlers, fillAction.query, () => `[Feature ${featureName}] Screen "${screenId}" (${screenType}) field "${field}" fillAction`);
39
+ }
40
+ }
32
41
  if (section.kind !== "relatedList")
33
42
  continue;
34
43
  checkQueryRef(queryHandlers, section.query, () => `[Feature ${featureName}] Screen "${screenId}" (${screenType}) relatedList section "${section.title}"`);
35
44
  }
36
45
  }
37
46
  function checkDashboardQueryRefs(queryHandlers, featureName, screenId, screen) {
47
+ if (screen.record?.query !== undefined) {
48
+ checkQueryRef(queryHandlers, screen.record.query, () => `[Feature ${featureName}] Screen "${screenId}" (dashboard) record`);
49
+ }
38
50
  for (const panel of screen.panels) {
39
51
  if (panel.kind === "custom")
40
52
  continue;
41
53
  if (panel.visibleWhen !== undefined) {
42
54
  checkVisibleWhenQueryRef(queryHandlers, panel.visibleWhen.query, () => `[Feature ${featureName}] Screen "${screenId}" (dashboard) ${panel.kind === "screen" ? "screen-panel" : "panel"} "${panel.id}" visibleWhen`);
43
55
  }
44
- if (panel.kind === "screen")
56
+ if (panel.kind === "screen" || panel.kind === "text")
45
57
  continue;
46
58
  if (panel.kind === "stat-group") {
47
59
  for (const stat of panel.stats) {
@@ -70,6 +82,18 @@ function checkFormFieldQueryRefs(queryHandlers, featureName, screenId, screenTyp
70
82
  }
71
83
  }
72
84
  }
85
+ function checkEditFormScreenQueryRefs(queryHandlers, featureName, screenId, screen) {
86
+ checkEditLayoutQueryRefs(queryHandlers, featureName, screenId, screen.type, screen.layout);
87
+ if (screen.type === "actionForm" && screen.load !== undefined) {
88
+ checkQueryRef(queryHandlers, screen.load.query, () => `[Feature ${featureName}] Screen "${screenId}" (actionForm) load`);
89
+ }
90
+ if (screen.type !== "entityEdit") {
91
+ checkFormFieldQueryRefs(queryHandlers, featureName, screenId, screen.type, screen.fields);
92
+ }
93
+ if (screen.type === "secretMint" && screen.confirm !== undefined) {
94
+ checkFormFieldQueryRefs(queryHandlers, featureName, screenId, "secretMint confirm", screen.confirm.fields);
95
+ }
96
+ }
73
97
  function checkScreenQueryRefs(queryHandlers, featureName, screenId, screen) {
74
98
  if (screen.visibleWhen !== undefined) {
75
99
  checkVisibleWhenQueryRef(queryHandlers, screen.visibleWhen.query, () => `[Feature ${featureName}] Screen "${screenId}" (${screen.type}) visibleWhen`);
@@ -90,13 +114,7 @@ function checkScreenQueryRefs(queryHandlers, featureName, screenId, screen) {
90
114
  screen.type === "actionForm" ||
91
115
  screen.type === "configEdit" ||
92
116
  screen.type === "secretMint") {
93
- checkEditLayoutQueryRefs(queryHandlers, featureName, screenId, screen.type, screen.layout);
94
- if (screen.type !== "entityEdit") {
95
- checkFormFieldQueryRefs(queryHandlers, featureName, screenId, screen.type, screen.fields);
96
- }
97
- if (screen.type === "secretMint" && screen.confirm !== undefined) {
98
- checkFormFieldQueryRefs(queryHandlers, featureName, screenId, "secretMint confirm", screen.confirm.fields);
99
- }
117
+ checkEditFormScreenQueryRefs(queryHandlers, featureName, screenId, screen);
100
118
  }
101
119
  else if (screen.type === "dashboard") {
102
120
  checkDashboardQueryRefs(queryHandlers, featureName, screenId, screen);
@@ -3,7 +3,7 @@ import type { ConfigKeyDefinition, FeatureDefinition } from "../types/index.ts";
3
3
  import type { ActionFormScreenDefinition, DashboardCustomPanel, DashboardScreenPanel, RelatedListToolbarAction, RowAction, RowFieldExtractor, ScreenDefinition, SecretMintScreenDefinition, ToolbarAction } from "../types/screen.ts";
4
4
  export declare function rowFieldExtractorKeys(params: RowFieldExtractor): readonly string[];
5
5
  export declare function readsNavigateParamsAsFormPrefill(target: ScreenDefinition, explicitEntityId: string | undefined, sourceScreenEntity: string | undefined): boolean;
6
- export declare function validateActionHasIcon(featureName: string, screenId: string, screenType: "projectionDetail" | "entityEdit" | "entityList" | "projectionList", context: string, action: {
6
+ export declare function validateActionHasIcon(featureName: string, screenId: string, screenType: "projectionDetail" | "entityEdit" | "entityList" | "projectionList" | "dashboard", context: string, action: {
7
7
  readonly id: string;
8
8
  readonly icon?: IconKey;
9
9
  }): void;