@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
@@ -1,7 +1,8 @@
1
1
  import * as z from "zod";
2
- import { zodDef, zodMeta, zodShapeField } from "../env/_zod-introspect.js";
2
+ import { zodDef, zodMeta, zodShape, zodShapeField } from "../env/_zod-introspect.js";
3
3
  export const HANDLER_FIELD_REFERENCES_META_KEY = "references";
4
4
  const MAX_WRAPPER_DEPTH = 8;
5
+ const MAX_NESTING_DEPTH = 8;
5
6
  const referenceMetaSchema = z.union([
6
7
  z.string().min(1),
7
8
  z.object({ entity: z.string().min(1), by: z.string().min(1).optional() }),
@@ -42,11 +43,7 @@ function isArrayField(field) {
42
43
  }
43
44
  return false;
44
45
  }
45
- export function readHandlerFieldReference(schema, field) {
46
- const objectSchema = unwrapObjectSchema(schema);
47
- const fieldSchema = objectSchema && zodShapeField(objectSchema, field);
48
- if (fieldSchema === undefined)
49
- return { kind: "none" };
46
+ function readFieldReference(fieldSchema) {
50
47
  const raw = findRawReferenceMeta(fieldSchema);
51
48
  if (raw === undefined)
52
49
  return { kind: "none" };
@@ -63,6 +60,83 @@ export function readHandlerFieldReference(schema, field) {
63
60
  },
64
61
  };
65
62
  }
63
+ export function readHandlerFieldReference(schema, field) {
64
+ const objectSchema = unwrapObjectSchema(schema);
65
+ const fieldSchema = objectSchema && zodShapeField(objectSchema, field);
66
+ if (fieldSchema === undefined)
67
+ return { kind: "none" };
68
+ return readFieldReference(fieldSchema);
69
+ }
70
+ function pathOf(segments) {
71
+ let path = "";
72
+ for (const segment of segments) {
73
+ if (segment.kind === "field")
74
+ path += path === "" ? segment.name : `.${segment.name}`;
75
+ else if (segment.kind === "element")
76
+ path += "[]";
77
+ else if (segment.kind === "recordKey")
78
+ path += "{key}";
79
+ else
80
+ path += "{}";
81
+ }
82
+ return path;
83
+ }
84
+ function collectAt(schema, segments, out) {
85
+ const reading = readFieldReference(schema);
86
+ if (reading.kind !== "none")
87
+ out.push({ path: pathOf(segments), segments, reading });
88
+ }
89
+ function walkNested(schema, segments, depth, out) {
90
+ // skip: schemas nested deeper than MAX_NESTING_DEPTH are not inspected
91
+ if (depth > MAX_NESTING_DEPTH)
92
+ return;
93
+ let current = schema;
94
+ let currentSegments = segments;
95
+ for (let layer = 0; layer < MAX_WRAPPER_DEPTH && current !== undefined; layer++) {
96
+ if (current instanceof z.ZodObject) {
97
+ walkObject(current, currentSegments, depth, out);
98
+ break;
99
+ }
100
+ if (current instanceof z.ZodRecord) {
101
+ const { keyType, valueType } = current;
102
+ if (keyType instanceof z.ZodType) {
103
+ collectAt(keyType, [...currentSegments, { kind: "recordKey" }], out);
104
+ }
105
+ if (valueType instanceof z.ZodType) {
106
+ const valueSegments = [...currentSegments, { kind: "recordValue" }];
107
+ collectAt(valueType, valueSegments, out);
108
+ walkNested(valueType, valueSegments, depth + 1, out);
109
+ }
110
+ break;
111
+ }
112
+ if (current instanceof z.ZodArray) {
113
+ currentSegments = [...currentSegments, { kind: "element" }];
114
+ current = zodDef(current)?.element;
115
+ continue;
116
+ }
117
+ current = zodDef(current)?.innerType;
118
+ }
119
+ }
120
+ function walkObject(objectSchema, segments, depth, out) {
121
+ for (const [name, fieldSchema] of Object.entries(zodShape(objectSchema))) {
122
+ const fieldSegments = [...segments, { kind: "field", name }];
123
+ collectAt(fieldSchema, fieldSegments, out);
124
+ walkNested(fieldSchema, fieldSegments, depth + 1, out);
125
+ }
126
+ }
127
+ /** All reference metas of a handler input schema, including fields nested in arrays,
128
+ * objects and records. `z.lazy()` is not followed. */
129
+ export function readHandlerFieldReferences(schema) {
130
+ const objectSchema = unwrapObjectSchema(schema);
131
+ if (objectSchema === undefined)
132
+ return [];
133
+ const out = [];
134
+ walkObject(objectSchema, [], 0, out);
135
+ return out;
136
+ }
137
+ export function handlerFieldReferences(schema) {
138
+ return readHandlerFieldReferences(schema).flatMap(({ path, segments, reading }) => reading.kind === "ok" ? [{ path, segments, reference: reading.reference }] : []);
139
+ }
66
140
  export function handlerFieldReference(schema, field) {
67
141
  const reading = readHandlerFieldReference(schema, field);
68
142
  return reading.kind === "ok" ? reading.reference : undefined;
@@ -25,7 +25,7 @@ export type { WorkflowDefinition, WorkflowInput, WorkflowTrigger } from "./defin
25
25
  export { computeDefinitionFingerprint, defineWorkflow } from "./define-workflow.js";
26
26
  export type { ToggleReader } from "./effective-features.js";
27
27
  export { computeEffectiveFeatures, isToggleableFeature } from "./effective-features.js";
28
- export { createEntityExecutor, defineEntityCreateHandler, defineEntityDeleteHandler, defineEntityDetailHandler, defineEntityListHandler, defineEntityQueryHandler, defineEntityRestoreHandler, defineEntityUpdateHandler, defineEntityWriteHandler, defineProjectionQueryHandler, type EntityCrudRegistrar, entityListSchema, MAX_LIST_LIMIT, type RegisterEntityCrudOptions, registerEntityCrud, } from "./entity-handlers.js";
28
+ export { createEntityExecutor, defineEntityCreateHandler, defineEntityDeleteHandler, defineEntityDetailHandler, defineEntityListHandler, defineEntityQueryHandler, defineEntityRestoreHandler, defineEntityUpdateHandler, defineEntityWriteHandler, defineProjectionQueryHandler, type EntityCrudRegistrar, entityListSchema, MAX_LIST_LIMIT, type RegisterEntityCrudOptions, } from "./entity-handlers.js";
29
29
  export { declareEscapeHatch } from "./escape-hatch-declaration.js";
30
30
  export type { EmitCtx } from "./event-helpers.js";
31
31
  export { emitEvent, typedPayload } from "./event-helpers.js";
@@ -43,7 +43,7 @@ export { addPattern, applyChanges, createFeaturePatcher, getPatternSchema, group
43
43
  export { type ChangelogEntry, type ChangelogType, compareVersions, type FeatureChangelog, filterEntriesAfter, parseFeatureChangelog, sortEntries, validateChangelog, } from "./feature-changelog.js";
44
44
  export { type BuildManifestOptions, buildManifestFromRegistry, type FeatureManifest, type ManifestConfigKey, type ManifestExtension, type ManifestFeature, type ManifestSecret, type ManifestSystemEvent, serializeManifest, } from "./feature-manifest.js";
45
45
  export { checkWriteFieldOwnership, checkWriteFieldRoles, filterReadFields, maskWriteOnlyFields, } from "./field-access.js";
46
- export { HANDLER_FIELD_REFERENCES_META_KEY, type HandlerFieldReference, type HandlerFieldReferenceMeta, handlerFieldReference, } from "./handler-field-references.js";
46
+ export { HANDLER_FIELD_REFERENCES_META_KEY, type HandlerFieldPathSegment, type HandlerFieldReference, type HandlerFieldReferenceAtPath, type HandlerFieldReferenceMeta, handlerFieldReference, handlerFieldReferences, readHandlerFieldReferences, } from "./handler-field-references.js";
47
47
  export { resolveName, withResponseData } from "./handler-helpers.js";
48
48
  export { i18nKey } from "./i18n-key.js";
49
49
  export { buildSessionRoles, findForbiddenMembershipRole, isForbiddenMembershipRole, stripForbiddenMembershipRoles, } from "./membership-roles.js";
@@ -81,7 +81,7 @@ export { assertNotificationVariables, templateVariable, templateVariablesToZod,
81
81
  export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, requireTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
82
82
  export { findOverDeepTransferChain, resolveTransferAdjacency as resolveEntityTransferAdjacency, type TransferAdjacency, type TransferEdge, type TransferEdgeLink, } from "./transfer-adjacency.js";
83
83
  export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
84
- export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MaxPerTenantOptions, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreparedWriteEvent, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
84
+ export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityObjectDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookDefinition, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MaxPerTenantOptions, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreparedWriteEvent, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
85
85
  export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
86
86
  export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.js";
87
87
  export { runValidation } from "./validation.js";
@@ -25,7 +25,7 @@ export { createEntityExecutor, defineEntityCreateHandler, defineEntityDeleteHand
25
25
  // Apps nehmen die verb-spezifischen Wrapper oben. Existierende
26
26
  // Caller (Integration-Tests, alte bundled-features) bleiben so
27
27
  // unverändert lauffähig.
28
- defineEntityQueryHandler, defineEntityRestoreHandler, defineEntityUpdateHandler, defineEntityWriteHandler, defineProjectionQueryHandler, entityListSchema, MAX_LIST_LIMIT, registerEntityCrud, } from "./entity-handlers.js";
28
+ defineEntityQueryHandler, defineEntityRestoreHandler, defineEntityUpdateHandler, defineEntityWriteHandler, defineProjectionQueryHandler, entityListSchema, MAX_LIST_LIMIT, } from "./entity-handlers.js";
29
29
  export { declareEscapeHatch } from "./escape-hatch-declaration.js";
30
30
  export { emitEvent, typedPayload } from "./event-helpers.js";
31
31
  export { EXT_ASSIGNABLE_ROLE, EXT_DERIVATIVE_OVERLAY_RESOLVER, EXT_DERIVATIVE_PUBLIC_PREDICATE, EXT_DERIVATIVE_RENDERER, EXT_EXTERNAL_RESOURCE, EXT_FILE_PROVIDER, EXT_INFRA_RESOURCE, EXT_PRINCIPAL_STATUS, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, EXT_TENANT_LIFECYCLE_STATUS, EXT_USER_DATA, EXT_USER_DATA_ORDER, FILE_PROVIDER_CONFIG_KEY, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, TENANT_MEMBERSHIPS_QUERY, } from "./extension-names.js";
@@ -38,7 +38,7 @@ export { addPattern, applyChanges, createFeaturePatcher, getPatternSchema, group
38
38
  export { compareVersions, filterEntriesAfter, parseFeatureChangelog, sortEntries, validateChangelog, } from "./feature-changelog.js";
39
39
  export { buildManifestFromRegistry, serializeManifest, } from "./feature-manifest.js";
40
40
  export { checkWriteFieldOwnership, checkWriteFieldRoles, filterReadFields, maskWriteOnlyFields, } from "./field-access.js";
41
- export { HANDLER_FIELD_REFERENCES_META_KEY, handlerFieldReference, } from "./handler-field-references.js";
41
+ export { HANDLER_FIELD_REFERENCES_META_KEY, handlerFieldReference, handlerFieldReferences, readHandlerFieldReferences, } from "./handler-field-references.js";
42
42
  export { resolveName, withResponseData } from "./handler-helpers.js";
43
43
  export { i18nKey } from "./i18n-key.js";
44
44
  // findForbiddenMembershipRole/isForbiddenMembershipRole/
@@ -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 helper for registrar methods that accept an Object-Form call
2
- // (`r.entity({ name: "item", ... })`) alongside the classic positional form
3
- // (`r.entity("item", { ... })`). Object-Form is the shape the feature-ast
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
- // Shared helper for registrar methods that accept either variadic strings
12
- // (hand-written call sites) or a single Object-Form object wrapping the
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
+ }
@@ -108,6 +108,9 @@ const RESERVED_TEMPLATE_VARIABLE_NAMES = new Set([
108
108
  "link",
109
109
  "money",
110
110
  "date",
111
+ "richText",
112
+ "stepIs",
113
+ "percent",
111
114
  ]);
112
115
  const KEBAB_NOTIFICATION_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
113
116
  function isTemplateVariableName(value) {
@@ -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, { table })) is the one physical table
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}", …, { table }): the backing table carries no ` +
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
@@ -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
+ }
@@ -23,6 +23,9 @@ export declare const templateVariable: {
23
23
  readonly date: (opts?: CommonOptions & {
24
24
  readonly example?: string;
25
25
  }) => TemplateScalarVariableDef;
26
+ readonly percent: (opts?: CommonOptions & {
27
+ readonly example?: number;
28
+ }) => TemplateScalarVariableDef;
26
29
  readonly list: (item: Readonly<Record<string, TemplateScalarVariableDef>>, opts?: CommonOptions & {
27
30
  readonly example?: readonly Readonly<Record<string, unknown>>[];
28
31
  }) => TemplateListVariableDef;
@@ -16,6 +16,7 @@ export const templateVariable = {
16
16
  type: "date",
17
17
  ...opts,
18
18
  }),
19
+ percent: (opts = {}) => ({ type: "percent", ...opts }),
19
20
  list: (item, opts = {}) => ({ type: "list", item, ...opts }),
20
21
  };
21
22
  const LINK_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
@@ -48,6 +49,8 @@ function scalarToZod(def) {
48
49
  return moneySchema;
49
50
  case "date":
50
51
  return dateSchema;
52
+ case "percent":
53
+ return z.number().finite();
51
54
  }
52
55
  }
53
56
  function variableToZod(def) {
@@ -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";
@@ -19,6 +19,7 @@ export type CreateFileRefInput = {
19
19
  readonly entityId?: string;
20
20
  };
21
21
  };
22
+ export declare function filesUnavailableMessage(caller: string): string;
22
23
  export declare function createFileRefFromBytes(ctx: CreateFileRefContext, input: CreateFileRefInput): Promise<WriteResult<CreatedFileRef>>;
23
24
  export type UploadFileBytesContext = Pick<CreateFileRefContext, "db" | "files" | "registry" | "user">;
24
25
  export declare function uploadFileBytesBeforeTransaction(ctx: UploadFileBytesContext, input: CreateFileRefInput): Promise<BeforeTransactionResult<UploadedFileBytes>>;
@@ -4,6 +4,11 @@ import { createFileRefRow, DEFAULT_MAX_UPLOAD_SIZE, storeFileAsFileRef, writeAcc
4
4
  import { fileRefsTable } from "./file-ref-table.js";
5
5
  import { readFilesRouteOptions } from "./file-routes.js";
6
6
  import { assertSafeStorageKey } from "./types.js";
7
+ // The handler context has no HTTP/job/seed marker to name, so the message states the actual
8
+ // cause: the dispatcher's AppContext carries no file-provider resolver.
9
+ export function filesUnavailableMessage(caller) {
10
+ return `${caller}: ctx.files is unavailable because no file-provider resolver is wired on this dispatcher context. Mount a file-provider-* feature and build the dispatcher context with withFileProviderResolver (HTTP, job and seed dispatchers each need it)`;
11
+ }
7
12
  // Server-side counterpart of POST /files for handlers/jobs that produce bytes
8
13
  // themselves (generated PDFs, exports). Same validation, storage layout and
9
14
  // fileRef.created event as an upload; the tenant is always ctx.user.tenantId.
@@ -11,7 +16,7 @@ export async function createFileRefFromBytes(ctx, input) {
11
16
  const { files } = ctx;
12
17
  if (!files) {
13
18
  return writeFailure(new InternalError({
14
- message: "createFileRefFromBytes: ctx.files is unavailable — mount a file-provider feature to store files",
19
+ message: filesUnavailableMessage("createFileRefFromBytes"),
15
20
  }));
16
21
  }
17
22
  const outcome = await storeFileAsFileRef({
@@ -48,7 +53,7 @@ export async function uploadFileBytesBeforeTransaction(ctx, input) {
48
53
  const { files } = ctx;
49
54
  if (!files) {
50
55
  return writeFailure(new InternalError({
51
- message: "uploadFileBytesBeforeTransaction: ctx.files is unavailable — mount a file-provider feature to store files",
56
+ message: filesUnavailableMessage("uploadFileBytesBeforeTransaction"),
52
57
  }));
53
58
  }
54
59
  const outcome = await writeAcceptedFileBytes({
@@ -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
  });
@@ -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 jobContext = {
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, jobContext));
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", fn)
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 declare const TENANT_TIMEZONE_CONFIG_KEY = "tenant:config:timezone";
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
- // Framework/pipeline stays bundled-features-free, so this can't import the
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";