@cosmicdrift/kumiko-framework 0.365.1 → 0.367.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 (110) hide show
  1. package/AGENTS.md +2 -2
  2. package/agents/examples/access-control.md +32 -30
  3. package/agents/examples/anonymous-access-multitenant.md +14 -16
  4. package/agents/examples/anonymous-access.md +36 -43
  5. package/agents/examples/auth-foundation-providers.md +1 -1
  6. package/agents/examples/basic-entity.md +1 -1
  7. package/agents/examples/boot-check.md +3 -3
  8. package/agents/examples/chat-channels.md +1 -1
  9. package/agents/examples/cross-feature-events.md +7 -7
  10. package/agents/examples/currencies-global.md +3 -3
  11. package/agents/examples/currencies-per-tenant.md +2 -2
  12. package/agents/examples/custom-fields-basic.md +4 -3
  13. package/agents/examples/custom-handlers.md +40 -44
  14. package/agents/examples/dashboard-stats.md +6 -6
  15. package/agents/examples/delivery-notifications.md +9 -9
  16. package/agents/examples/designer-demo.md +2 -1
  17. package/agents/examples/embedded-entity-form.md +13 -13
  18. package/agents/examples/embedded.md +1 -1
  19. package/agents/examples/encrypted-tenant-config.md +1 -1
  20. package/agents/examples/error-contract.md +25 -25
  21. package/agents/examples/event-sourcing.md +96 -76
  22. package/agents/examples/feature-toggles.md +29 -21
  23. package/agents/examples/files-post-processing.md +22 -26
  24. package/agents/examples/idempotency.md +17 -19
  25. package/agents/examples/inbound-mail-triage.md +1 -1
  26. package/agents/examples/lane-routing.md +10 -12
  27. package/agents/examples/legal-pages.md +1 -1
  28. package/agents/examples/lifecycle-hooks.md +59 -39
  29. package/agents/examples/managed-config.md +9 -9
  30. package/agents/examples/managed-pages.md +1 -1
  31. package/agents/examples/notes-history-basic.md +3 -3
  32. package/agents/examples/pdf-audit-export.md +7 -7
  33. package/agents/examples/pipeline-basics.md +8 -8
  34. package/agents/examples/public-share-token.md +1 -1
  35. package/agents/examples/rate-limiting.md +7 -9
  36. package/agents/examples/record-detail-layout.md +26 -20
  37. package/agents/examples/reference-data.md +1 -1
  38. package/agents/examples/relations.md +2 -2
  39. package/agents/examples/screens-nav.md +2 -2
  40. package/agents/examples/search.md +17 -19
  41. package/agents/examples/security-baseline.md +1 -1
  42. package/agents/examples/state-machine.md +1 -1
  43. package/agents/examples/tags-basic.md +3 -3
  44. package/agents/examples/tenant-settings.md +2 -2
  45. package/agents/examples/timezones.md +12 -12
  46. package/agents/examples/user-data-rights.md +14 -29
  47. package/agents/examples/user-profile.md +1 -1
  48. package/agents/examples/webhook-step.md +1 -1
  49. package/agents/examples/writeform-section.md +21 -15
  50. package/agents/features.md +4 -4
  51. package/agents/widgets.md +5 -0
  52. package/dist/api/server.js +12 -4
  53. package/dist/bun-db/query.js +16 -1
  54. package/dist/db/entity-table-meta.js +4 -4
  55. package/dist/db/queries/seed-context.js +3 -3
  56. package/dist/engine/boot-validator/api-ext.js +2 -2
  57. package/dist/engine/boot-validator/config-deps.js +1 -1
  58. package/dist/engine/boot-validator/handler-field-references.js +3 -10
  59. package/dist/engine/boot-validator/screens.js +1 -1
  60. package/dist/engine/build-app-schema.js +3 -6
  61. package/dist/engine/define-feature.js +5 -5
  62. package/dist/engine/entity-handlers.d.ts +2 -2
  63. package/dist/engine/entity-handlers.js +5 -5
  64. package/dist/engine/factories.d.ts +11 -11
  65. package/dist/engine/factories.js +11 -11
  66. package/dist/engine/feature-ast/extractors/round2.js +16 -2
  67. package/dist/engine/feature-ast/render.js +4 -7
  68. package/dist/engine/feature-config-events-jobs.d.ts +6 -0
  69. package/dist/engine/feature-config-events-jobs.js +5 -0
  70. package/dist/engine/feature-entity-handlers.d.ts +4 -23
  71. package/dist/engine/feature-entity-handlers.js +77 -100
  72. package/dist/engine/feature-ui-extensions.d.ts +9 -14
  73. package/dist/engine/feature-ui-extensions.js +22 -14
  74. package/dist/engine/handler-field-references.d.ts +25 -0
  75. package/dist/engine/handler-field-references.js +80 -6
  76. package/dist/engine/index.d.ts +3 -3
  77. package/dist/engine/index.js +2 -2
  78. package/dist/engine/object-form.d.ts +2 -0
  79. package/dist/engine/object-form.js +13 -10
  80. package/dist/engine/registry-ingest.js +3 -0
  81. package/dist/engine/registry-state.js +2 -2
  82. package/dist/engine/registry-validate.d.ts +1 -0
  83. package/dist/engine/registry-validate.js +21 -2
  84. package/dist/engine/registry.js +2 -1
  85. package/dist/engine/strip-disabled-features-from-app-schema.d.ts +6 -0
  86. package/dist/engine/strip-disabled-features-from-app-schema.js +89 -0
  87. package/dist/engine/template-variables.d.ts +3 -0
  88. package/dist/engine/template-variables.js +3 -0
  89. package/dist/engine/types/index.d.ts +1 -1
  90. package/dist/files/create-file-ref-from-bytes.d.ts +1 -0
  91. package/dist/files/create-file-ref-from-bytes.js +7 -2
  92. package/dist/files/feature.js +1 -1
  93. package/dist/jobs/job-runner.js +16 -2
  94. package/dist/pipeline/dispatch-query.js +1 -1
  95. package/dist/pipeline/dispatch-shared.d.ts +2 -1
  96. package/dist/pipeline/dispatch-shared.js +3 -6
  97. package/dist/pipeline/tenant-timezone-config-key.d.ts +1 -0
  98. package/dist/pipeline/tenant-timezone-config-key.js +6 -0
  99. package/dist/testing/index.d.ts +1 -0
  100. package/dist/testing/index.js +1 -0
  101. package/dist/testing/manual-clock.d.ts +14 -0
  102. package/dist/testing/manual-clock.js +12 -0
  103. package/dist/time/index.d.ts +1 -1
  104. package/dist/time/tz-context.d.ts +1 -1
  105. package/dist/time/tz-context.js +9 -4
  106. package/dist/upgrade-cli.js +13 -2
  107. package/package.json +4 -4
  108. package/src/changes.json +52 -0
  109. package/src/scripts/codemod/README.md +23 -0
  110. package/src/scripts/codemod/object-form.ts +709 -0
@@ -0,0 +1,6 @@
1
+ // Framework/pipeline stays bundled-features-free, so this can't import the
2
+ // `tenant` feature — the literal below IS the coupling to its `timezone`
3
+ // config key. Renaming that key (or the "tenant" feature name) must update
4
+ // this constant too; tenant-timezone-boot.integration.test.ts boots the real
5
+ // createTenantFeature() and would catch a drift.
6
+ export const TENANT_TIMEZONE_CONFIG_KEY = "tenant:config:timezone";
@@ -13,6 +13,7 @@ export { describeFileProviderContract } from "./file-provider-contract.js";
13
13
  export { bridgeStub } from "./handler-context.js";
14
14
  export { getSetCookieRaw, getSetCookies, getSetCookieValue, type ParsedSetCookie, } from "./http-cookies.js";
15
15
  export { createLateBoundHolder, type LateBoundHolder } from "./late-bound.js";
16
+ export { createManualClock, type ManualClock } from "./manual-clock.js";
16
17
  export { buildMultipartBody, patchFileInstanceofForBunTest } from "./multipart-helper.js";
17
18
  export { createMutableMasterKeyProvider, createTestEnvelopeCipher, createTestMasterKeyProvider, type MutableMasterKeyProvider, } from "./mutable-master-key-provider.js";
18
19
  export { createRecordingProvider, type RecordingProvider, } from "./observability-recorder.js";
@@ -21,6 +21,7 @@ export { describeFileProviderContract } from "./file-provider-contract.js";
21
21
  export { bridgeStub } from "./handler-context.js";
22
22
  export { getSetCookieRaw, getSetCookies, getSetCookieValue, } from "./http-cookies.js";
23
23
  export { createLateBoundHolder } from "./late-bound.js";
24
+ export { createManualClock } from "./manual-clock.js";
24
25
  export { buildMultipartBody, patchFileInstanceofForBunTest } from "./multipart-helper.js";
25
26
  export { createMutableMasterKeyProvider, createTestEnvelopeCipher, createTestMasterKeyProvider, } from "./mutable-master-key-provider.js";
26
27
  export { createRecordingProvider, } from "./observability-recorder.js";
@@ -0,0 +1,14 @@
1
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
+ import type { Clock } from "@cosmicdrift/kumiko-types/tz-context";
3
+ type ClockStep = {
4
+ readonly hours?: number;
5
+ readonly minutes?: number;
6
+ readonly seconds?: number;
7
+ readonly milliseconds?: number;
8
+ };
9
+ export type ManualClock = Clock & {
10
+ set(instant: Temporal.Instant): void;
11
+ advance(step: ClockStep): void;
12
+ };
13
+ export declare function createManualClock(start: Temporal.Instant): ManualClock;
14
+ export {};
@@ -0,0 +1,12 @@
1
+ export function createManualClock(start) {
2
+ let current = start;
3
+ return {
4
+ now: () => current,
5
+ set: (instant) => {
6
+ current = instant;
7
+ },
8
+ advance: (step) => {
9
+ current = current.add(step);
10
+ },
11
+ };
12
+ }
@@ -3,4 +3,4 @@ export type { GeoAddress, GeoCoordinates, GeoTzProvider } from "./geo-tz.js";
3
3
  export { isValidIanaTimeZone } from "./iana.js";
4
4
  export { instantToLegacyDate, legacyDateToInstant } from "./legacy-date.js";
5
5
  export { ensureTemporalPolyfill, getTemporal } from "./polyfill.js";
6
- export { createTzContext, createTzContextAsync, type LocatedTimestampJson, type TzContext, type TzContextOptions, } from "./tz-context.js";
6
+ export { type Clock, createTzContext, createTzContextAsync, type LocatedTimestampJson, type TzContext, type TzContextOptions, } from "./tz-context.js";
@@ -1,5 +1,5 @@
1
1
  import type { TzContext, TzContextOptions } from "@cosmicdrift/kumiko-types/tz-context";
2
- export type { LocatedTimestampJson, TzContext, TzContextOptions, } from "@cosmicdrift/kumiko-types/tz-context";
2
+ export type { Clock, LocatedTimestampJson, TzContext, TzContextOptions, } from "@cosmicdrift/kumiko-types/tz-context";
3
3
  /**
4
4
  * Factory: creates a TzContext for the current request.
5
5
  * Uses the kumiko-types Temporal module export (not globalThis.Temporal) so
@@ -19,6 +19,9 @@
19
19
  // live in @cosmicdrift/kumiko-types/tz-context — only the factories are here.
20
20
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
21
21
  import { ensureTemporalPolyfill } from "./polyfill.js";
22
+ const SYSTEM_CLOCK = {
23
+ now: () => Temporal.Now.instant(), // @wrapper-known semantic-alias
24
+ };
22
25
  /**
23
26
  * Factory: creates a TzContext for the current request.
24
27
  * Uses the kumiko-types Temporal module export (not globalThis.Temporal) so
@@ -28,14 +31,16 @@ export function createTzContext(options = {}) {
28
31
  const tenant = options.tenant ?? "UTC";
29
32
  const user = options.user ?? tenant;
30
33
  const geoTz = options.geoTz;
34
+ const clock = options.clock ?? SYSTEM_CLOCK;
35
+ const nowIn = (tz) => clock.now().toZonedDateTimeISO(tz);
31
36
  return {
32
37
  tenant,
33
38
  user,
34
- now: () => Temporal.Now.instant(), // @wrapper-known semantic-alias
35
- nowIn: (tz) => Temporal.Now.zonedDateTimeISO(tz), // @wrapper-known semantic-alias
36
- today: (tz) => Temporal.Now.plainDateISO(tz), // @wrapper-known semantic-alias
39
+ now: () => clock.now(),
40
+ nowIn,
41
+ today: (tz) => nowIn(tz).toPlainDate(),
37
42
  todayRange: (tz) => {
38
- const today = Temporal.Now.plainDateISO(tz);
43
+ const today = nowIn(tz).toPlainDate();
39
44
  const startZdt = today.toZonedDateTime({ timeZone: tz });
40
45
  const endZdt = today.add({ days: 1 }).toZonedDateTime({ timeZone: tz });
41
46
  return { start: startZdt.toInstant(), end: endZdt.toInstant() };
@@ -297,6 +297,12 @@ export function manualEntryId(version, title) {
297
297
  const hash = createHash("sha256").update(title).digest("hex").slice(0, MANUAL_ID_HASH_LENGTH);
298
298
  return `${version}:${hash}`;
299
299
  }
300
+ // Breaking entries that need hand work carry the id `kumiko-upgrade --resolve` takes.
301
+ function manualIdOf(entry) {
302
+ return entry.type === "breaking" && needsManualMigration(entry)
303
+ ? manualEntryId(entry.version, entry.title)
304
+ : undefined;
305
+ }
300
306
  function toMarkerManual(entry) {
301
307
  return {
302
308
  id: manualEntryId(entry.version, entry.title),
@@ -739,7 +745,11 @@ export async function runUpgradeCli(argv, cwd, out, options) {
739
745
  }
740
746
  if (jsonMode) {
741
747
  const { pendingManual } = carryOf(markerRead);
742
- out.log(JSON.stringify({ currentVersion, installedVersion, pending, pendingManual }, null, 2));
748
+ const pendingWithManualId = pending.map((entry) => {
749
+ const manualId = manualIdOf(entry);
750
+ return manualId === undefined ? entry : { ...entry, manualId };
751
+ });
752
+ out.log(JSON.stringify({ currentVersion, installedVersion, pending: pendingWithManualId, pendingManual }, null, 2));
743
753
  return 0;
744
754
  }
745
755
  const breaking = pending.filter((e) => e.type === "breaking");
@@ -759,7 +769,8 @@ export async function runUpgradeCli(argv, cwd, out, options) {
759
769
  out.log(` ⚠ BREAKING (${breaking.length})`);
760
770
  out.log("");
761
771
  for (const e of breaking) {
762
- out.log(` ${e.version} · ${e.title}`);
772
+ const manualId = manualIdOf(e);
773
+ out.log(` ${e.version} · ${e.title}${manualId === undefined ? "" : ` [${manualId}]`}`);
763
774
  if (verbose && e.detail) {
764
775
  out.log(` ${e.detail}`);
765
776
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.365.1",
3
+ "version": "0.367.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>",
@@ -210,8 +210,8 @@
210
210
  "./package.json": "./package.json"
211
211
  },
212
212
  "dependencies": {
213
- "@cosmicdrift/kumiko-http": "0.365.1",
214
- "@cosmicdrift/kumiko-types": "0.365.1",
213
+ "@cosmicdrift/kumiko-http": "0.367.0",
214
+ "@cosmicdrift/kumiko-types": "0.367.0",
215
215
  "bullmq": "^5.76.7",
216
216
  "bun-types": "^1.3.13",
217
217
  "hono": "^4.13.1",
@@ -227,7 +227,7 @@
227
227
  "zod": "^4.4.3"
228
228
  },
229
229
  "devDependencies": {
230
- "@cosmicdrift/kumiko-dispatcher-live": "0.365.1",
230
+ "@cosmicdrift/kumiko-dispatcher-live": "0.367.0",
231
231
  "bun-types": "^1.3.13",
232
232
  "pino-pretty": "^13.1.3"
233
233
  },
package/src/changes.json CHANGED
@@ -1,4 +1,56 @@
1
1
  [
2
+ {
3
+ "version": "0.367.0",
4
+ "type": "fix",
5
+ "title": "Seed context finds template resources again",
6
+ "detail": "findTemplateResources in the seed context filtered on an is_deleted column that the template resource table does not have, so every seed that read template resources failed. The query now reads the live rows only."
7
+ },
8
+ {
9
+ "version": "0.367.0",
10
+ "type": "improvement",
11
+ "title": "Seeds can store files and see why a write failed",
12
+ "detail": "ProdSeedFn deps now carry createFileRef(tenantId, input), which stores bytes as a fileRef with the same validation and storage layout as POST /files, without a helper write handler. A seed write that fails with internal_error now names the handler and the cause message instead of a bare \"internal error\". The seed dispatcher gets the app's dispatcherOptions (and the job runner in all-in-one mode), like the HTTP dispatcher. The \"ctx.files is unavailable\" message of createFileRefFromBytes names the real cause: the dispatcher context has no file-provider resolver."
13
+ },
14
+ {
15
+ "version": "0.366.0",
16
+ "type": "breaking",
17
+ "title": "One injectable Clock for ctx.tz in handlers and jobs",
18
+ "detail": "New type Clock and TzContextOptions.clock. AppContext.clock feeds ctx.tz.now() in handlers, and JobContext.tz gives jobs the same ctx.tz API (tenant timezone from tenant config, read once per job run, UTC fallback). The testing helper createManualClock(start) returns a clock with set and advance. The tenant-lifecycle job now reads time through ctx.tz.now().",
19
+ "migration": "Code that builds a JobContext literal itself (boot seeds, test doubles) adds `tz: createTzContext()`."
20
+ },
21
+ {
22
+ "version": "0.366.0",
23
+ "type": "improvement",
24
+ "title": "Handler input references work on nested fields and record keys",
25
+ "detail": "The references meta on handler input schemas is now found on fields inside arrays, nested objects and records, and on record keys. The new readHandlerFieldReferences and handlerFieldReferences return each hit with a path such as \"channels[].channelKey\" or \"labels{key}\" plus typed path segments. Boot validation checks nested references too and names the path in its error message. readHandlerFieldReference and handlerFieldReference behave as before."
26
+ },
27
+ {
28
+ "version": "0.366.0",
29
+ "type": "breaking",
30
+ "title": "Registrar methods take a single object, positional calls throw at registration",
31
+ "detail": "r.writeHandler, r.queryHandler, r.entity, r.requires and r.hook now take one object. The positional overloads are gone from the types and throw when the feature is defined, with a message that points at the codemod. The throw is removed in the first release after 2026-10-29.\n\nwriteHandler (queryHandler is the same shape):\n before: r.writeHandler(\"order:archive\", schema, handler, { access });\n after: r.writeHandler({ name: \"order:archive\", schema, handler, access });\n\nhook (type picks the handler shape, target is a name, a list or { allOf }):\n before: r.hook(\"postSave\", \"order\", handler, { escapeHatch });\n after: r.hook({ type: \"postSave\", target: \"order\", handler, escapeHatch });\n\nentity and requires:\n before: r.entity(\"order\", definition, { table: ordersTable }); r.requires(\"auth\", \"tenant\");\n after: r.entity({ name: \"order\", ...definition, backingTable: ordersTable }); r.requires({ features: [\"auth\", \"tenant\"] });\n\nregisterEntityCrud is no longer exported from @cosmicdrift/kumiko-framework/engine; call r.crud(name, entity, options) instead. The entity option { table } is now backingTable on the r.entity object and lands in the feature's entityTables, not in its entities. HookDefinition and EntityObjectDefinition are exported from @cosmicdrift/kumiko-types. Hook definitions are checked per type: validation takes no escapeHatch, phase only exists on postSave and postDelete, and { allOf } is not accepted on preSave.",
32
+ "migration": "Run `bun kumiko-upgrade --apply`. It rewrites every positional r.writeHandler, r.queryHandler, r.entity, r.requires, r.hook and registerEntityCrud call to the object form. Calls it cannot resolve statically (a spread argument, a registrar that is not typed as FeatureRegistrar) are listed as manual work with file and line; rewrite those by hand.",
33
+ "codemod": "scripts/codemod/object-form.ts",
34
+ "manualAfterCodemod": true
35
+ },
36
+ {
37
+ "version": "0.366.0",
38
+ "type": "improvement",
39
+ "title": "The app schema no longer lists screens, navs and workspaces of features a tenant has switched off",
40
+ "detail": "A feature declared with r.toggleable() that is off for the caller's tenant (tier engine or feature toggles) used to stay in the nav while all its handlers answered 403 feature_disabled. GET /api/schema now drops its screens, navs, content collections and workspaces for that tenant, and every action, redirect, nav parent and workspace member that pointed at them is removed too. Entities and translations stay. The response is cached per role set and per set of disabled features, so tenants with the same set share one ETag. Without an effectiveFeatures resolver nothing changes."
41
+ },
42
+ {
43
+ "version": "0.366.0",
44
+ "type": "improvement",
45
+ "title": "selectMany supports column projection via SelectOptions.fields",
46
+ "detail": "SelectOptions gains fields, a list of entity field names. selectMany then selects only those columns instead of all of them. An empty list or a name that is not a column of the table throws."
47
+ },
48
+ {
49
+ "version": "0.366.0",
50
+ "type": "improvement",
51
+ "title": "kumiko-upgrade names each pending manual step by an id",
52
+ "detail": "Every pending manual step of kumiko-upgrade now has a stable id, derived from the release version and the step title. The text output shows it in brackets and the JSON output carries it as manualId, so a script or an agent can tell steps apart and record which ones it has done."
53
+ },
2
54
  {
3
55
  "version": "0.365.1",
4
56
  "type": "fix",
@@ -34,3 +34,26 @@ at — fix those by hand. The report also lists every field that newly
34
34
  gains `lookupable` (searchable-only fields resolving to `find: "fuzzy"`)
35
35
  — each of those needs a `_bidx` column migration, which this script does
36
36
  not write.
37
+
38
+ ## object-form.ts
39
+
40
+ Rewrites the positional registrar calls `r.writeHandler(name, schema, handler, options)`,
41
+ `r.queryHandler(…)`, `r.entity(name, definition, { table })`, `r.requires("a", "b")` and
42
+ `r.hook(type, target, fn, options?)` to their object form, and
43
+ `registerEntityCrud(r, name, entity, options?)` to `r.crud(name, entity, options?)`
44
+ (the now-unused import is removed). Idempotent — object-form calls are left alone.
45
+
46
+ A call is only touched when its receiver is provably a feature registrar: a parameter of
47
+ the `defineFeature(name, (r) => …)` callback, or a binding typed `FeatureRegistrar`.
48
+ Anything else (`sessions.requires("x")`, `foo.entity(…)`) stays untouched. A call on an
49
+ untyped `r`/`registrar` it cannot resolve, a spread argument, too few arguments or
50
+ `entity` options other than `{ table }` are reported as `manual` instead of guessed at.
51
+
52
+ ```bash
53
+ bun node_modules/@cosmicdrift/kumiko-framework/src/scripts/codemod/object-form.ts <dir> [--check | --dry-run | --write] [--no-format]
54
+ ```
55
+
56
+ Runs automatically via `bun kumiko-upgrade --apply`. Without a mode flag it writes, then
57
+ formats the changed files with the Biome binary found in `node_modules/.bin` (`--no-format`
58
+ skips that). `--dry-run` only reports; `--check` reports and exits 1 while any rewritable
59
+ or manual site is left.