@cosmicdrift/kumiko-framework 0.365.1 → 0.366.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/AGENTS.md +2 -2
- package/agents/examples/access-control.md +32 -30
- package/agents/examples/anonymous-access-multitenant.md +14 -16
- package/agents/examples/anonymous-access.md +36 -43
- package/agents/examples/auth-foundation-providers.md +1 -1
- package/agents/examples/basic-entity.md +1 -1
- package/agents/examples/boot-check.md +3 -3
- package/agents/examples/chat-channels.md +1 -1
- package/agents/examples/cross-feature-events.md +7 -7
- package/agents/examples/currencies-global.md +3 -3
- package/agents/examples/currencies-per-tenant.md +2 -2
- package/agents/examples/custom-fields-basic.md +4 -3
- package/agents/examples/custom-handlers.md +40 -44
- package/agents/examples/dashboard-stats.md +6 -6
- package/agents/examples/delivery-notifications.md +9 -9
- package/agents/examples/designer-demo.md +2 -1
- package/agents/examples/embedded-entity-form.md +13 -13
- package/agents/examples/embedded.md +1 -1
- package/agents/examples/encrypted-tenant-config.md +1 -1
- package/agents/examples/error-contract.md +25 -25
- package/agents/examples/event-sourcing.md +96 -76
- package/agents/examples/feature-toggles.md +29 -21
- package/agents/examples/files-post-processing.md +22 -26
- package/agents/examples/idempotency.md +17 -19
- package/agents/examples/inbound-mail-triage.md +1 -1
- package/agents/examples/lane-routing.md +10 -12
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +59 -39
- package/agents/examples/managed-config.md +9 -9
- package/agents/examples/managed-pages.md +1 -1
- package/agents/examples/notes-history-basic.md +3 -3
- package/agents/examples/pdf-audit-export.md +7 -7
- package/agents/examples/pipeline-basics.md +8 -8
- package/agents/examples/public-share-token.md +1 -1
- package/agents/examples/rate-limiting.md +7 -9
- package/agents/examples/record-detail-layout.md +26 -20
- package/agents/examples/reference-data.md +1 -1
- package/agents/examples/relations.md +2 -2
- package/agents/examples/screens-nav.md +2 -2
- package/agents/examples/search.md +17 -19
- package/agents/examples/security-baseline.md +1 -1
- package/agents/examples/state-machine.md +1 -1
- package/agents/examples/tags-basic.md +3 -3
- package/agents/examples/tenant-settings.md +2 -2
- package/agents/examples/timezones.md +12 -12
- package/agents/examples/user-data-rights.md +3 -3
- package/agents/examples/user-profile.md +1 -1
- package/agents/examples/webhook-step.md +1 -1
- package/agents/examples/writeform-section.md +21 -15
- package/agents/widgets.md +5 -0
- package/dist/api/server.js +12 -4
- package/dist/bun-db/query.js +16 -1
- package/dist/db/entity-table-meta.js +4 -4
- package/dist/engine/boot-validator/api-ext.js +2 -2
- package/dist/engine/boot-validator/config-deps.js +1 -1
- package/dist/engine/boot-validator/handler-field-references.js +3 -10
- package/dist/engine/boot-validator/screens.js +1 -1
- package/dist/engine/build-app-schema.js +3 -6
- package/dist/engine/define-feature.js +5 -5
- package/dist/engine/entity-handlers.d.ts +2 -2
- package/dist/engine/entity-handlers.js +5 -5
- package/dist/engine/factories.d.ts +11 -11
- package/dist/engine/factories.js +11 -11
- package/dist/engine/feature-ast/extractors/round2.js +16 -2
- package/dist/engine/feature-ast/render.js +4 -7
- package/dist/engine/feature-config-events-jobs.d.ts +2 -0
- package/dist/engine/feature-config-events-jobs.js +3 -0
- package/dist/engine/feature-entity-handlers.d.ts +4 -23
- package/dist/engine/feature-entity-handlers.js +77 -100
- package/dist/engine/feature-ui-extensions.d.ts +9 -14
- package/dist/engine/feature-ui-extensions.js +22 -14
- package/dist/engine/handler-field-references.d.ts +25 -0
- package/dist/engine/handler-field-references.js +80 -6
- package/dist/engine/index.d.ts +3 -3
- package/dist/engine/index.js +2 -2
- package/dist/engine/object-form.d.ts +2 -0
- package/dist/engine/object-form.js +13 -10
- package/dist/engine/registry-state.js +2 -2
- package/dist/engine/registry-validate.d.ts +1 -0
- package/dist/engine/registry-validate.js +21 -2
- package/dist/engine/registry.js +2 -1
- package/dist/engine/strip-disabled-features-from-app-schema.d.ts +6 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +89 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/files/feature.js +1 -1
- package/dist/jobs/job-runner.js +16 -2
- package/dist/pipeline/dispatch-query.js +1 -1
- package/dist/pipeline/dispatch-shared.d.ts +2 -1
- package/dist/pipeline/dispatch-shared.js +3 -6
- package/dist/pipeline/tenant-timezone-config-key.d.ts +1 -0
- package/dist/pipeline/tenant-timezone-config-key.js +6 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/manual-clock.d.ts +14 -0
- package/dist/testing/manual-clock.js +12 -0
- package/dist/time/index.d.ts +1 -1
- package/dist/time/tz-context.d.ts +1 -1
- package/dist/time/tz-context.js +9 -4
- package/dist/upgrade-cli.js +13 -2
- package/package.json +4 -4
- package/src/changes.json +40 -0
- package/src/scripts/codemod/README.md +23 -0
- package/src/scripts/codemod/object-form.ts +709 -0
|
@@ -2,3 +2,5 @@ export declare function splitNamedDefinition<T extends {
|
|
|
2
2
|
readonly name: string;
|
|
3
3
|
}>(definition: T): [string, Omit<T, "name">];
|
|
4
4
|
export declare function unwrapArrayForm<K extends string>(args: readonly [Record<K, readonly string[]>] | readonly string[], key: K): readonly string[];
|
|
5
|
+
export declare function isObjectArgument(value: unknown): boolean;
|
|
6
|
+
export declare function throwPositionalFormRemoved(featureName: string, method: string): never;
|
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
// Shared
|
|
2
|
-
// (`r.entity({ name: "item", ... })`)
|
|
3
|
-
//
|
|
4
|
-
// renderer emits for Designer/AI-generated code — a single object argument
|
|
5
|
-
// with named fields is easier to generate correctly than positional args
|
|
6
|
-
// whose count/order vary per method.
|
|
1
|
+
// Shared helpers for registrar methods that take a single object argument
|
|
2
|
+
// (`r.entity({ name: "item", ... })`). The positional overloads are gone; the
|
|
3
|
+
// runtime check below turns a leftover positional call into a pointer at the codemod.
|
|
7
4
|
export function splitNamedDefinition(definition) {
|
|
8
5
|
const { name, ...rest } = definition;
|
|
9
6
|
return [name, rest];
|
|
10
7
|
}
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// same string array under `key` (the feature-ast renderer's canonical
|
|
14
|
-
// shape for Designer/AI-generated code).
|
|
8
|
+
// `optionalRequires` and `readConfig` still accept variadic strings next to a
|
|
9
|
+
// single object wrapping the same string array under `key`.
|
|
15
10
|
export function unwrapArrayForm(args, key) {
|
|
16
11
|
const [first] = args;
|
|
17
12
|
if (typeof first === "object" && first !== null && key in first) {
|
|
@@ -19,3 +14,11 @@ export function unwrapArrayForm(args, key) {
|
|
|
19
14
|
}
|
|
20
15
|
return args;
|
|
21
16
|
}
|
|
17
|
+
export function isObjectArgument(value) {
|
|
18
|
+
return typeof value === "object" && value !== null;
|
|
19
|
+
}
|
|
20
|
+
// Removed in the first release after 2026-10-29; until then a positional call fails with
|
|
21
|
+
// a pointer at the codemod instead of an obscure "undefined is not a function".
|
|
22
|
+
export function throwPositionalFormRemoved(featureName, method) {
|
|
23
|
+
throw new Error(`[Feature ${featureName}] positional r.${method}(…) removed in 0.366, run \`bun kumiko-upgrade --apply\``);
|
|
24
|
+
}
|
|
@@ -12,7 +12,7 @@ const IMPLICIT_PROJECTION_SUFFIX = "-entity";
|
|
|
12
12
|
// durch implicit-projection-equivalence.integration.ts).
|
|
13
13
|
export function buildImplicitProjection(featureName, entityName, entity, qualify, backingTable, extensions = []) {
|
|
14
14
|
const name = qualify(featureName, "projection", `${entityName}${IMPLICIT_PROJECTION_SUFFIX}`);
|
|
15
|
-
// Backing table (r.entity(name, def,
|
|
15
|
+
// Backing table (r.entity({ name, ...def, backingTable })) is the one physical table
|
|
16
16
|
// object shared by executor-writes, rebuild-replay, test-push and
|
|
17
17
|
// collectTableMetas — restoring the #255 invariant (test-push == generate).
|
|
18
18
|
// Validated as a superset of the field-derived columns so a field/table
|
|
@@ -107,7 +107,7 @@ function collectExtensionSources(projectionName, entityName, extensions) {
|
|
|
107
107
|
function resolveBackingTable(entityName, entity, backingTable) {
|
|
108
108
|
const tableMeta = asEntityTableMeta(backingTable);
|
|
109
109
|
if (!tableMeta) {
|
|
110
|
-
throw new Error(`r.entity("${entityName}", …,
|
|
110
|
+
throw new Error(`r.entity({ name: "${entityName}", …, backingTable }): the backing table carries no ` +
|
|
111
111
|
"EntityTableMeta — build it via table() / buildEntityTable.");
|
|
112
112
|
}
|
|
113
113
|
assertBackingTableSuperset(entityName, deriveEntityTableMeta(entityName, entity), tableMeta);
|
|
@@ -19,6 +19,7 @@ export declare function buildEventUpcasterChains(state: RegistryState, features:
|
|
|
19
19
|
export declare function validateProjectionApplyKeys(state: RegistryState): void;
|
|
20
20
|
export declare function validateRequiredFeatures(state: RegistryState, features: readonly FeatureDefinition[]): void;
|
|
21
21
|
export declare function validateNotificationRecordMappings(state: RegistryState): void;
|
|
22
|
+
export declare function validateNotificationRenamedFrom(state: RegistryState): void;
|
|
22
23
|
export declare function resolveNotificationTriggersAndRegisterHooks(state: RegistryState): void;
|
|
23
24
|
export declare function validateLifecycleHookTargets(state: RegistryState): void;
|
|
24
25
|
export declare function validateEntityHookTargets(state: RegistryState, features: readonly FeatureDefinition[]): void;
|
|
@@ -462,7 +462,7 @@ export function validateProjectionApplyKeys(state) {
|
|
|
462
462
|
const validEventTypes = new Set();
|
|
463
463
|
// Two source-modes are legal:
|
|
464
464
|
//
|
|
465
|
-
// (a) Registered entity (r.entity(src, ...)) — the "normal" case:
|
|
465
|
+
// (a) Registered entity (r.entity({ name: src, ... })) — the "normal" case:
|
|
466
466
|
// auto-lifecycle events `<src>.created/.updated/.deleted/.restored`
|
|
467
467
|
// fire when the event-store-executor writes, and any domain-event
|
|
468
468
|
// (r.defineEvent) appended onto an aggregate of that type is
|
|
@@ -497,7 +497,7 @@ export function validateProjectionApplyKeys(state) {
|
|
|
497
497
|
throw new Error(`Projection "${projName}" declares source(s) [${unregistered.join(", ")}] that are not registered entities, ` +
|
|
498
498
|
`and has no domain-event apply-keys. This is either a typo or a missing r.defineEvent registration. ` +
|
|
499
499
|
`Events-only projections need at least one apply-key from r.defineEvent; ` +
|
|
500
|
-
`CRUD-style projections need r.entity("${unregistered[0]}", ...).`);
|
|
500
|
+
`CRUD-style projections need r.entity({ name: "${unregistered[0]}", ... }).`);
|
|
501
501
|
}
|
|
502
502
|
}
|
|
503
503
|
for (const applyKey of Object.keys(projDef.apply)) {
|
|
@@ -547,6 +547,25 @@ export function validateNotificationRecordMappings(state) {
|
|
|
547
547
|
}
|
|
548
548
|
}
|
|
549
549
|
}
|
|
550
|
+
export function validateNotificationRenamedFrom(state) {
|
|
551
|
+
const ownerByAlias = new Map();
|
|
552
|
+
for (const [qualifiedName, notifDef] of state.notificationMap) {
|
|
553
|
+
for (const alias of notifDef.renamedFrom ?? []) {
|
|
554
|
+
const where = `Notification "${qualifiedName}" renamedFrom "${alias}"`;
|
|
555
|
+
if (alias === "*") {
|
|
556
|
+
throw new Error(`${where}: "*" is the preference wildcard, not a notification name`);
|
|
557
|
+
}
|
|
558
|
+
if (state.notificationMap.has(alias)) {
|
|
559
|
+
throw new Error(`${where} is a current notification name; remove it from renamedFrom or rename the notification`);
|
|
560
|
+
}
|
|
561
|
+
const otherOwner = ownerByAlias.get(alias);
|
|
562
|
+
if (otherOwner !== undefined && otherOwner !== qualifiedName) {
|
|
563
|
+
throw new Error(`${where} is also declared by notification "${otherOwner}"; an alias can have only one owner, remove it from one of them`);
|
|
564
|
+
}
|
|
565
|
+
ownerByAlias.set(alias, qualifiedName);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
550
569
|
export function resolveNotificationTriggersAndRegisterHooks(state) {
|
|
551
570
|
// Resolve notification triggers and register postSave hooks
|
|
552
571
|
// Done after all features are registered so cross-feature triggers work
|
package/dist/engine/registry.js
CHANGED
|
@@ -3,7 +3,7 @@ import { dedupeFeatures } from "./dedupe-features.js";
|
|
|
3
3
|
import { buildRegistryFacade } from "./registry-facade.js";
|
|
4
4
|
import { populateClaimsAndAuth, populateConfigKeys, populateEvents, populateExtensionsAndSeeds, populateFeatureCore, populateHandlers, populateHooks, populateJobsAndNotifications, populateMetricsAndSecrets, populateProjectionsAndTables, populateScreensNavWorkspaces, populateSystemEvents, populateTranslations, } from "./registry-ingest.js";
|
|
5
5
|
import { createInitialState } from "./registry-state.js";
|
|
6
|
-
import { applyExtensionUsages, autoWireSoftDeleteJobs, buildEntityEventUpcasterChains, buildEventUpcasterChains, buildImplicitProjections, buildIncomingRelations, buildSearchableSortableCaches, computeHasRateLimitedHandler, finalizeWorkspaceNavMembership, populateHandlerEntityMappings, publishEventPiiCatalog, resolveJobTriggers, resolveNotificationTriggersAndRegisterHooks, validateBootGates, validateEntityHookTargets, validateEventMigrationVersions, validateExtensionSelectors, validateExtensionUsageTargets, validateFieldAccessHandlersAreEntityMapped, validateJobBackoff, validateLifecycleHookTargets, validateLiveEntities, validateMaxPerTenant, validateNotificationRecordMappings, validateProjectionApplyKeys, validateRelationTargetsExist, validateRequiredFeatures, } from "./registry-validate.js";
|
|
6
|
+
import { applyExtensionUsages, autoWireSoftDeleteJobs, buildEntityEventUpcasterChains, buildEventUpcasterChains, buildImplicitProjections, buildIncomingRelations, buildSearchableSortableCaches, computeHasRateLimitedHandler, finalizeWorkspaceNavMembership, populateHandlerEntityMappings, publishEventPiiCatalog, resolveJobTriggers, resolveNotificationTriggersAndRegisterHooks, validateBootGates, validateEntityHookTargets, validateEventMigrationVersions, validateExtensionSelectors, validateExtensionUsageTargets, validateFieldAccessHandlersAreEntityMapped, validateJobBackoff, validateLifecycleHookTargets, validateLiveEntities, validateMaxPerTenant, validateNotificationRecordMappings, validateNotificationRenamedFrom, validateProjectionApplyKeys, validateRelationTargetsExist, validateRequiredFeatures, } from "./registry-validate.js";
|
|
7
7
|
// This is where the magic happens. By "magic" I mean: precomputed maps.
|
|
8
8
|
// I build everything once at boot (hooks, relations, searchable fields, ...)
|
|
9
9
|
// so nothing has to iterate over objects at runtime. O(1) instead of O(n*m).
|
|
@@ -42,6 +42,7 @@ export function createRegistry(rawFeatures) {
|
|
|
42
42
|
validateRequiredFeatures(state, features);
|
|
43
43
|
resolveNotificationTriggersAndRegisterHooks(state);
|
|
44
44
|
validateNotificationRecordMappings(state);
|
|
45
|
+
validateNotificationRenamedFrom(state);
|
|
45
46
|
validateLifecycleHookTargets(state);
|
|
46
47
|
validateEntityHookTargets(state, features);
|
|
47
48
|
resolveJobTriggers(state);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AppSchema } from "../ui-types/app-schema.js";
|
|
2
|
+
import type { EffectiveFeaturesResolver } from "./tier-resolver-extension.js";
|
|
3
|
+
import type { Registry } from "./types/feature.js";
|
|
4
|
+
import type { TenantId } from "./types/identifiers.js";
|
|
5
|
+
export declare function resolveDisabledFeatureNames(registry: Registry, effectiveFeatures: EffectiveFeaturesResolver, tenantId: TenantId): Promise<readonly string[]>;
|
|
6
|
+
export declare function stripDisabledFeaturesFromAppSchema(schema: AppSchema, registry: Registry, disabledFeatureNames: readonly string[]): AppSchema;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Tenant-level counterpart to projectAppSchemaForRoles: removes the UI surface
|
|
2
|
+
// (screens, navs, content collections, workspaces) of features the tenant has
|
|
3
|
+
// switched off via r.toggleable(). Entities and translations stay — enabled
|
|
4
|
+
// features may still reference them. Run BEFORE the role projection: it keeps
|
|
5
|
+
// only targets that exist in the schema, so once the disabled screens/navs are
|
|
6
|
+
// gone it also drops every action, redirect, nav and workspace member that
|
|
7
|
+
// pointed at them. The one reference it does not resolve is a nav `parent`
|
|
8
|
+
// pointing at a missing nav; that is pruned here.
|
|
9
|
+
import { isToggleableFeature } from "./effective-features.js";
|
|
10
|
+
import { isValidQn, qualifyEntityName } from "./qualified-name.js";
|
|
11
|
+
// Sorted, so the result doubles as a stable cache key. The trial gate is
|
|
12
|
+
// consulted like the dispatcher's feature gate does, but only for toggleable
|
|
13
|
+
// features: a trial can only unlock tier (toggleable) features, so a
|
|
14
|
+
// cascade-disabled always-on feature needs no gate call.
|
|
15
|
+
export async function resolveDisabledFeatureNames(registry, effectiveFeatures, tenantId) {
|
|
16
|
+
const enabled = effectiveFeatures(tenantId);
|
|
17
|
+
const disabled = [];
|
|
18
|
+
for (const [featureName, feature] of registry.features) {
|
|
19
|
+
if (enabled.has(featureName))
|
|
20
|
+
continue;
|
|
21
|
+
if (isToggleableFeature(feature) &&
|
|
22
|
+
(await effectiveFeatures.trialGate?.(tenantId, featureName))) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
disabled.push(featureName);
|
|
26
|
+
}
|
|
27
|
+
return disabled.sort();
|
|
28
|
+
}
|
|
29
|
+
function navParentQn(featureName, parent) {
|
|
30
|
+
return isValidQn(parent) ? parent : qualifyEntityName(featureName, "nav", parent);
|
|
31
|
+
}
|
|
32
|
+
// Navs of enabled features whose parent chain ends in a removed nav.
|
|
33
|
+
function collectOrphanedNavQns(features, removedNavQns) {
|
|
34
|
+
const orphaned = new Set();
|
|
35
|
+
let changed = true;
|
|
36
|
+
while (changed) {
|
|
37
|
+
changed = false;
|
|
38
|
+
for (const feature of features) {
|
|
39
|
+
for (const nav of feature.navs ?? []) {
|
|
40
|
+
const qn = qualifyEntityName(feature.featureName, "nav", nav.id);
|
|
41
|
+
if (removedNavQns.has(qn) || nav.parent === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
if (removedNavQns.has(navParentQn(feature.featureName, nav.parent))) {
|
|
44
|
+
removedNavQns.add(qn);
|
|
45
|
+
orphaned.add(qn);
|
|
46
|
+
changed = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return orphaned;
|
|
52
|
+
}
|
|
53
|
+
export function stripDisabledFeaturesFromAppSchema(schema, registry, disabledFeatureNames) {
|
|
54
|
+
if (disabledFeatureNames.length === 0)
|
|
55
|
+
return schema;
|
|
56
|
+
const disabled = new Set(disabledFeatureNames);
|
|
57
|
+
const removedNavQns = new Set();
|
|
58
|
+
for (const feature of schema.features) {
|
|
59
|
+
if (!disabled.has(feature.featureName))
|
|
60
|
+
continue;
|
|
61
|
+
for (const nav of feature.navs ?? []) {
|
|
62
|
+
removedNavQns.add(qualifyEntityName(feature.featureName, "nav", nav.id));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const orphanedNavQns = collectOrphanedNavQns(schema.features, removedNavQns);
|
|
66
|
+
const features = schema.features.map((feature) => {
|
|
67
|
+
if (disabled.has(feature.featureName)) {
|
|
68
|
+
const { navs: _navs, contentCollections: _collections, workspaces: _ws, ...rest } = feature;
|
|
69
|
+
return { ...rest, screens: [] };
|
|
70
|
+
}
|
|
71
|
+
if (orphanedNavQns.size === 0 || feature.navs === undefined)
|
|
72
|
+
return feature;
|
|
73
|
+
const navs = feature.navs.filter((nav) => !orphanedNavQns.has(qualifyEntityName(feature.featureName, "nav", nav.id)));
|
|
74
|
+
if (navs.length === feature.navs.length)
|
|
75
|
+
return feature;
|
|
76
|
+
const { navs: _navs, ...rest } = feature;
|
|
77
|
+
return navs.length > 0 ? { ...rest, navs } : rest;
|
|
78
|
+
});
|
|
79
|
+
// Workspace definitions carry the short id the author declared; the owning
|
|
80
|
+
// feature is only known to the registry.
|
|
81
|
+
const disabledWorkspaceIds = new Set(disabledFeatureNames.flatMap((featureName) => Object.keys(registry.features.get(featureName)?.workspaces ?? {})));
|
|
82
|
+
const workspaces = schema.workspaces?.filter((ws) => !disabledWorkspaceIds.has(ws.definition.id));
|
|
83
|
+
const { workspaces: _original, ...restSchema } = schema;
|
|
84
|
+
return {
|
|
85
|
+
...restSchema,
|
|
86
|
+
features,
|
|
87
|
+
...(workspaces !== undefined && workspaces.length > 0 && { workspaces }),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -4,7 +4,7 @@ export type { QueryHandlerDefinition, StreamHandlerDefinition, WriteHandlerDefin
|
|
|
4
4
|
export type { EntityCrudRegistrar, EntityCrudVerb, EntityHandlerOptions, EntityQueryHandlerOptions, EntityWriteHandlerOptions, RegisterEntityCrudOptions, } from "@cosmicdrift/kumiko-types/entity-handlers";
|
|
5
5
|
export type { KumikoEntityTypeMap, KumikoEventTypeMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, } from "@cosmicdrift/kumiko-types/event-type-map";
|
|
6
6
|
export type { ExtensionOptionsArgs, ExtensionOptionsFor, KumikoExtensionOptionsMap, } from "@cosmicdrift/kumiko-types/extension-options-map";
|
|
7
|
-
export type { BootCheckContext, BootCheckFn, FeatureDefinition, FeatureMetricDef, FeatureMetricType, FeatureRegistrar, MetricOptions, Registry, SecretKeyDefinition, SecretKeyHandle, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretOptions, StoreTableDef, StoreTableEntry, StoreTableOptions, UiHintOption, UiHints, } from "@cosmicdrift/kumiko-types/feature";
|
|
7
|
+
export type { BootCheckContext, BootCheckFn, EntityObjectDefinition, FeatureDefinition, FeatureMetricDef, FeatureMetricType, FeatureRegistrar, HookDefinition, MetricOptions, Registry, SecretKeyDefinition, SecretKeyHandle, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretOptions, StoreTableDef, StoreTableEntry, StoreTableOptions, UiHintOption, UiHints, } from "@cosmicdrift/kumiko-types/feature";
|
|
8
8
|
export type { FieldIconKey } from "@cosmicdrift/kumiko-types/field-icon";
|
|
9
9
|
export type { AnyFileFieldDef, BigIntFieldDef, BooleanFieldDef, ClientDerivedFieldDef, DateFieldDef, DecimalFieldDef, DefaultCurrency, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EmbeddedDerivedCellDef, EmbeddedFieldDef, EmbeddedSubFieldDef, EntityDefinition, EntityEventMigration, EntityIndexDef, FieldAccess, FieldDefinition, FieldsMap, FileFieldDef, FilesFieldDef, Findability, ImageFieldDef, ImagesFieldDef, JsonbFieldDef, LocatedTimestampFieldDef, LongTextFieldDef, LongTextFindability, MoneyCurrencySource, MoneyFieldDef, MultiSelectFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, ReferenceFieldDef, ResolvedPiiFlags, RetentionDef, SelectFieldDef, SelectOptionTone, TextFieldDef, TimestampFieldDef, TransitionMap, TzFieldDef, } from "@cosmicdrift/kumiko-types/fields";
|
|
10
10
|
export type { AccessRule, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AggregateStreamHandle, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadBucketNormalization, PayloadRateLimitOption, PreparedWriteEvent, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TenantWriteTarget, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
|
package/dist/files/feature.js
CHANGED
|
@@ -22,7 +22,7 @@ export function createFilesFeature(options = {}) {
|
|
|
22
22
|
category: "storage",
|
|
23
23
|
recommended: false,
|
|
24
24
|
});
|
|
25
|
-
r.entity(FILE_REF_ENTITY_NAME, fileRefEntity);
|
|
25
|
+
r.entity({ name: FILE_REF_ENTITY_NAME, ...fileRefEntity });
|
|
26
26
|
r.writeHandler(bindReferencedFilesWrite);
|
|
27
27
|
return { routeOptions: options };
|
|
28
28
|
});
|
package/dist/jobs/job-runner.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
1
2
|
import { escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
3
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
4
|
import { DelayedError, Queue, UnrecoverableError, Worker } from "bullmq";
|
|
@@ -16,8 +17,10 @@ import { createEscapeHatchProcessDedup, createEscapeHatchReporter, createEscapeH
|
|
|
16
17
|
import { createDistributedLock } from "../pipeline/distributed-lock.js";
|
|
17
18
|
import { RedisKeys } from "../pipeline/redis-keys.js";
|
|
18
19
|
import { createGatedIdentitySwitch } from "../pipeline/system-identity-switch.js";
|
|
20
|
+
import { TENANT_TIMEZONE_CONFIG_KEY } from "../pipeline/tenant-timezone-config-key.js";
|
|
19
21
|
import { buildPersonalDataGate, isPersonalDataGated, parseWriteOrigin, } from "../pipeline/write-origin.js";
|
|
20
22
|
import { bridgeStub } from "../testing/handler-context.js";
|
|
23
|
+
import { createTzContext, isValidIanaTimeZone } from "../time/index.js";
|
|
21
24
|
import { cronRepeatOptions } from "./cron-repeat-options.js";
|
|
22
25
|
import { resolveMaxPerTenant } from "./max-per-tenant.js";
|
|
23
26
|
// Shape-checked after the BullMQ/Redis JSON round-trip. Unlike `_writeOrigin`
|
|
@@ -201,6 +204,14 @@ function jobSubjectKey(jobName, payload, fields, declaration) {
|
|
|
201
204
|
}
|
|
202
205
|
return serializeJobSubject(subject);
|
|
203
206
|
}
|
|
207
|
+
async function resolveJobTz(config, context) {
|
|
208
|
+
const tenantTz = config ? await config(TENANT_TIMEZONE_CONFIG_KEY) : undefined;
|
|
209
|
+
return createTzContext({
|
|
210
|
+
...(context.geoTzProvider !== undefined && { geoTz: context.geoTzProvider }),
|
|
211
|
+
...(context.clock !== undefined && { clock: context.clock }),
|
|
212
|
+
tenant: typeof tenantTz === "string" && isValidIanaTimeZone(tenantTz) ? tenantTz : "UTC",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
204
215
|
// Jobs that never touch ctx.db run fine on a runner built without one; a job
|
|
205
216
|
// that does gets a named error instead of a bare TypeError on undefined.
|
|
206
217
|
function missingDbProxy(jobName) {
|
|
@@ -984,7 +995,7 @@ export function createJobRunner(options) {
|
|
|
984
995
|
return dispatchWriteRef.write(user, qn, payload, jobOrigin);
|
|
985
996
|
},
|
|
986
997
|
}, { reason: identitySwitchGrantReason, report: reportEscapeHatch });
|
|
987
|
-
const
|
|
998
|
+
const jobContextWithoutTz = {
|
|
988
999
|
...context,
|
|
989
1000
|
db: jobDb ?? missingDbProxy(jobName),
|
|
990
1001
|
files,
|
|
@@ -1060,7 +1071,10 @@ export function createJobRunner(options) {
|
|
|
1060
1071
|
handler: jobName,
|
|
1061
1072
|
feature: registry.getJobFeature(jobName) ?? qnScope(jobName),
|
|
1062
1073
|
writeOrigin: jobOrigin,
|
|
1063
|
-
}, () => jobDef.handler(payload,
|
|
1074
|
+
}, async () => jobDef.handler(payload, {
|
|
1075
|
+
...jobContextWithoutTz,
|
|
1076
|
+
tz: await resolveJobTz(config, context),
|
|
1077
|
+
}));
|
|
1064
1078
|
// Stamped before the observer hook: a throwing onJobComplete must not
|
|
1065
1079
|
// make a run that actually succeeded look dead to the liveness alert.
|
|
1066
1080
|
if (context.meter) {
|
|
@@ -66,7 +66,7 @@ async function runQueryHandler(ctx, type, handler, payload, includeDeleted, user
|
|
|
66
66
|
// below only knows the entity's stammfields).
|
|
67
67
|
//
|
|
68
68
|
// Two firing-pfade kombiniert in dieser Reihenfolge:
|
|
69
|
-
// 1. Handler-keyed hooks via r.hook("postQuery", "ns:query:list",
|
|
69
|
+
// 1. Handler-keyed hooks via r.hook({ type: "postQuery", target: "ns:query:list", handler })
|
|
70
70
|
// — feuern nur für genau diesen handler
|
|
71
71
|
// 2. Entity-keyed hooks via r.entityHook("postQuery", "property", fn)
|
|
72
72
|
// — feuern für ALLE query-handlers des entity
|
|
@@ -12,9 +12,10 @@ import { type AfterCommitHook } from "./dispatcher-utils.js";
|
|
|
12
12
|
import type { IdempotencyGuard } from "./idempotency.js";
|
|
13
13
|
import type { LifecycleHooks } from "./lifecycle-pipeline.js";
|
|
14
14
|
import type { TenantTimezoneCache } from "./tenant-timezone-cache.js";
|
|
15
|
+
import { TENANT_TIMEZONE_CONFIG_KEY } from "./tenant-timezone-config-key.js";
|
|
15
16
|
import { type WriteOrigin } from "./write-origin.js";
|
|
16
17
|
export type { WriteOrigin } from "./write-origin.js";
|
|
17
|
-
export
|
|
18
|
+
export { TENANT_TIMEZONE_CONFIG_KEY };
|
|
18
19
|
export declare const CONFIG_WRITE_SET_TYPE = "config:write:set";
|
|
19
20
|
export declare const CONFIG_WRITE_RESET_TYPE = "config:write:reset";
|
|
20
21
|
export type BatchCommand = {
|
|
@@ -29,13 +29,9 @@ import { dispatcherSpanAttributes, isFailedWriteResult, NestedWriteRollback, } f
|
|
|
29
29
|
import { createMemberReaderFn } from "./member-reader.js";
|
|
30
30
|
import { createDeferringNotify } from "./notify-scope.js";
|
|
31
31
|
import { createGatedIdentitySwitch, createGatedMemberReader, createGatedProjectionReader, isSystemIdentity, systemIdentitySwitchDenied, } from "./system-identity-switch.js";
|
|
32
|
+
import { TENANT_TIMEZONE_CONFIG_KEY } from "./tenant-timezone-config-key.js";
|
|
32
33
|
import { buildPersonalDataGate, rootWriteOrigin } from "./write-origin.js";
|
|
33
|
-
|
|
34
|
-
// `tenant` feature — the literal below IS the coupling to its `timezone`
|
|
35
|
-
// config key. Renaming that key (or the "tenant" feature name) must update
|
|
36
|
-
// this constant too; tenant-timezone-boot.integration.test.ts boots the real
|
|
37
|
-
// createTenantFeature() and would catch a drift.
|
|
38
|
-
export const TENANT_TIMEZONE_CONFIG_KEY = "tenant:config:timezone";
|
|
34
|
+
export { TENANT_TIMEZONE_CONFIG_KEY };
|
|
39
35
|
// Same bundled-features-free constraint as above — these name the config
|
|
40
36
|
// feature's write handlers so dispatch-write.ts can invalidate
|
|
41
37
|
// TENANT_TIMEZONE_CONFIG_KEY's cache entry on a successful write without
|
|
@@ -826,6 +822,7 @@ readOnlyForTenant = false) {
|
|
|
826
822
|
const safeUserTz = user.timezone !== undefined && isValidIanaTimeZone(user.timezone) ? user.timezone : undefined;
|
|
827
823
|
const tz = createTzContext({
|
|
828
824
|
...(context.geoTzProvider !== undefined ? { geoTz: context.geoTzProvider } : {}),
|
|
825
|
+
...(context.clock !== undefined ? { clock: context.clock } : {}),
|
|
829
826
|
tenant: safeTenantTz,
|
|
830
827
|
...(safeUserTz !== undefined && { user: safeUserTz }),
|
|
831
828
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const TENANT_TIMEZONE_CONFIG_KEY = "tenant:config:timezone";
|
|
@@ -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";
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/testing/index.js
CHANGED
|
@@ -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 {};
|
package/dist/time/index.d.ts
CHANGED
|
@@ -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
|
package/dist/time/tz-context.js
CHANGED
|
@@ -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: () =>
|
|
35
|
-
nowIn
|
|
36
|
-
today: (tz) =>
|
|
39
|
+
now: () => clock.now(),
|
|
40
|
+
nowIn,
|
|
41
|
+
today: (tz) => nowIn(tz).toPlainDate(),
|
|
37
42
|
todayRange: (tz) => {
|
|
38
|
-
const today =
|
|
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() };
|
package/dist/upgrade-cli.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.366.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.
|
|
214
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
213
|
+
"@cosmicdrift/kumiko-http": "0.366.0",
|
|
214
|
+
"@cosmicdrift/kumiko-types": "0.366.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.
|
|
230
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.366.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,44 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.366.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "One injectable Clock for ctx.tz in handlers and jobs",
|
|
6
|
+
"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().",
|
|
7
|
+
"migration": "Code that builds a JobContext literal itself (boot seeds, test doubles) adds `tz: createTzContext()`."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.366.0",
|
|
11
|
+
"type": "improvement",
|
|
12
|
+
"title": "Handler input references work on nested fields and record keys",
|
|
13
|
+
"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."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "0.366.0",
|
|
17
|
+
"type": "breaking",
|
|
18
|
+
"title": "Registrar methods take a single object, positional calls throw at registration",
|
|
19
|
+
"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.",
|
|
20
|
+
"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.",
|
|
21
|
+
"codemod": "scripts/codemod/object-form.ts",
|
|
22
|
+
"manualAfterCodemod": true
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "0.366.0",
|
|
26
|
+
"type": "improvement",
|
|
27
|
+
"title": "The app schema no longer lists screens, navs and workspaces of features a tenant has switched off",
|
|
28
|
+
"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."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"version": "0.366.0",
|
|
32
|
+
"type": "improvement",
|
|
33
|
+
"title": "selectMany supports column projection via SelectOptions.fields",
|
|
34
|
+
"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."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"version": "0.366.0",
|
|
38
|
+
"type": "improvement",
|
|
39
|
+
"title": "kumiko-upgrade names each pending manual step by an id",
|
|
40
|
+
"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."
|
|
41
|
+
},
|
|
2
42
|
{
|
|
3
43
|
"version": "0.365.1",
|
|
4
44
|
"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.
|