@cosmicdrift/kumiko-framework 0.385.0 → 0.387.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/LICENSE +21 -57
- package/README.md +2 -2
- package/agents/widgets.md +39 -39
- package/dist/api/api-constants.js +2 -2
- package/dist/api/auth-middleware.js +7 -7
- package/dist/api/auth-rate-limiters.js +7 -3
- package/dist/api/auth-routes-mfa.js +1 -1
- package/dist/api/auth-routes-session.js +1 -1
- package/dist/api/auth-routes-shared.js +7 -3
- package/dist/api/auth-routes-signup.js +1 -1
- package/dist/api/client-ip.js +3 -1
- package/dist/api/extra-route.js +1 -1
- package/dist/api/pii-leak-guard.js +4 -2
- package/dist/api/readiness.js +5 -5
- package/dist/api/redis-sse-broker.js +8 -6
- package/dist/api/request-context.d.ts +2 -0
- package/dist/api/request-context.js +16 -7
- package/dist/api/routes.js +2 -2
- package/dist/api/server.js +34 -33
- package/dist/api/sse-broker.js +1 -1
- package/dist/api/sse-route.d.ts +13 -13
- package/dist/api/sse-route.js +15 -15
- package/dist/auth/index.js +4 -4
- package/dist/auth/roles.d.ts +4 -4
- package/dist/auth/roles.js +24 -24
- package/dist/compliance/duration-spec.js +18 -20
- package/dist/compliance/index.js +2 -3
- package/dist/compliance/override-schema.d.ts +4 -4
- package/dist/compliance/override-schema.js +19 -22
- package/dist/compliance/profiles.d.ts +26 -29
- package/dist/compliance/profiles.js +62 -69
- package/dist/compliance/sub-processors.d.ts +28 -30
- package/dist/compliance/sub-processors.js +17 -21
- package/dist/consumer-cli.js +4 -4
- package/dist/crypto/blind-index.js +18 -17
- package/dist/crypto/ciphertext-pattern.js +1 -1
- package/dist/crypto/event-pii.js +3 -3
- package/dist/crypto/kek-source.d.ts +1 -1
- package/dist/crypto/kek-source.js +3 -2
- package/dist/crypto/kms-adapter.d.ts +1 -1
- package/dist/crypto/kms-adapter.js +4 -6
- package/dist/crypto/kms-wiring.d.ts +1 -2
- package/dist/crypto/kms-wiring.js +2 -2
- package/dist/crypto/pg-kms-adapter.js +2 -6
- package/dist/crypto/pii-field-encryption.js +4 -4
- package/dist/crypto/subject-resolver.d.ts +1 -1
- package/dist/crypto/subject-resolver.js +2 -2
- package/dist/db/apply-entity-event.d.ts +6 -6
- package/dist/db/apply-entity-event.js +55 -55
- package/dist/db/blind-index-cleanup.js +7 -7
- package/dist/db/collect-table-metas.js +10 -11
- package/dist/db/compound-types.d.ts +1 -1
- package/dist/db/compound-types.js +12 -12
- package/dist/db/cross-tenant-reads.d.ts +8 -0
- package/dist/db/cross-tenant-reads.js +23 -0
- package/dist/db/cursor.js +3 -4
- package/dist/db/dialect.d.ts +2 -2
- package/dist/db/dialect.js +2 -2
- package/dist/db/eagerload.d.ts +3 -3
- package/dist/db/eagerload.js +21 -12
- package/dist/db/entity-table-meta.d.ts +2 -2
- package/dist/db/entity-table-meta.js +4 -4
- package/dist/db/event-store-executor-context.d.ts +1 -1
- package/dist/db/event-store-executor-context.js +13 -13
- package/dist/db/event-store-executor-read.js +27 -28
- package/dist/db/event-store-executor-write.js +65 -70
- package/dist/db/located-timestamp.d.ts +11 -11
- package/dist/db/located-timestamp.js +29 -30
- package/dist/db/migrate-generator.d.ts +1 -1
- package/dist/db/migrate-generator.js +26 -19
- package/dist/db/migrate-runner.js +25 -25
- package/dist/db/money.d.ts +6 -6
- package/dist/db/money.js +29 -27
- package/dist/db/queries/backfill-pii.js +5 -5
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/es-ops.d.ts +1 -1
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +3 -3
- package/dist/db/queries/event-store.js +4 -2
- package/dist/db/queries/projection-rebuild.js +3 -3
- package/dist/db/queries/raw-sql.d.ts +2 -2
- package/dist/db/queries/raw-sql.js +2 -2
- package/dist/db/queries/shadow-swap.js +19 -19
- package/dist/db/queries/test-stack.d.ts +4 -1
- package/dist/db/queries/test-stack.js +15 -2
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +6 -6
- package/dist/db/rebuild-marker.js +20 -21
- package/dist/db/replay-migration-sql.js +2 -2
- package/dist/db/schema-inspection.js +4 -5
- package/dist/db/table-builder.d.ts +1 -2
- package/dist/db/table-builder.js +82 -82
- package/dist/db/tenant-db.d.ts +2 -2
- package/dist/db/tenant-db.js +9 -2
- package/dist/derivatives/delete-stored-file.js +5 -2
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/access-roles.js +4 -3
- package/dist/engine/boot-validator/api-ext.js +14 -14
- package/dist/engine/boot-validator/config-deps.js +7 -6
- package/dist/engine/boot-validator/detail-screens.js +3 -3
- package/dist/engine/boot-validator/entity-handler.js +60 -61
- package/dist/engine/boot-validator/gdpr-storage.js +4 -4
- package/dist/engine/boot-validator/i18n-keys.js +1 -1
- package/dist/engine/boot-validator/index.d.ts +2 -2
- package/dist/engine/boot-validator/index.js +20 -20
- package/dist/engine/boot-validator/nav.js +5 -5
- package/dist/engine/boot-validator/ownership.js +12 -9
- package/dist/engine/boot-validator/parent-ref.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +50 -57
- package/dist/engine/boot-validator/projection-list-screens.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +1 -1
- package/dist/engine/boot-validator/screens-actions.js +15 -16
- package/dist/engine/boot-validator/screens-forms.js +10 -11
- package/dist/engine/boot-validator/screens-lists.js +14 -15
- package/dist/engine/boot-validator/screens.js +34 -40
- package/dist/engine/boot-validator/security-baseline.js +3 -2
- package/dist/engine/boot-validator/transfer-graph.js +2 -2
- package/dist/engine/boot-validator/workspaces.js +3 -3
- package/dist/engine/bridge-stub.js +3 -4
- package/dist/engine/build-app-schema.js +75 -75
- package/dist/engine/build-config-feature-schema.js +50 -50
- package/dist/engine/build-target.js +20 -23
- package/dist/engine/codemod/pipeline-codemod.js +20 -13
- package/dist/engine/config-helpers.js +4 -4
- package/dist/engine/constants.js +2 -2
- package/dist/engine/create-app.js +2 -2
- package/dist/engine/define-feature.js +12 -11
- package/dist/engine/define-handler.js +12 -11
- package/dist/engine/define-step.js +1 -1
- package/dist/engine/define-workflow.d.ts +5 -5
- package/dist/engine/define-workflow.js +6 -6
- package/dist/engine/entity-handlers.js +13 -14
- package/dist/engine/event-def.js +2 -2
- package/dist/engine/extension-names.d.ts +16 -17
- package/dist/engine/extension-names.js +37 -38
- package/dist/engine/extensions/escape-hatch-usage.js +1 -1
- package/dist/engine/extensions/tenant-data.js +2 -2
- package/dist/engine/extensions/user-data.d.ts +23 -24
- package/dist/engine/factories.d.ts +14 -13
- package/dist/engine/factories.js +38 -37
- package/dist/engine/feature-ast/entity-field-types.js +1 -1
- package/dist/engine/feature-ast/extractors/handlers.js +1 -1
- package/dist/engine/feature-ast/extractors/hooks.js +1 -1
- package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
- package/dist/engine/feature-ast/extractors/shared.js +2 -3
- package/dist/engine/feature-ast/parse.js +13 -13
- package/dist/engine/feature-ast/patch.js +1 -1
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +8 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -1
- package/dist/engine/feature-ast/patterns.d.ts +2 -2
- package/dist/engine/feature-ast/render.js +2 -2
- package/dist/engine/feature-config-events-jobs.js +15 -12
- package/dist/engine/feature-entity-handlers.js +9 -5
- package/dist/engine/feature-manifest.d.ts +9 -9
- package/dist/engine/feature-manifest.js +6 -7
- package/dist/engine/feature-ui-extensions.d.ts +3 -3
- package/dist/engine/feature-ui-extensions.js +30 -22
- package/dist/engine/field-access.js +5 -6
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +4 -4
- package/dist/engine/object-form.d.ts +1 -0
- package/dist/engine/object-form.js +14 -4
- package/dist/engine/ownership.js +3 -3
- package/dist/engine/parse-ref-target.js +5 -7
- package/dist/engine/pattern-library/index.js +2 -2
- package/dist/engine/pattern-library/library.js +2 -2
- package/dist/engine/pattern-library/types.js +3 -3
- package/dist/engine/pipeline.js +2 -2
- package/dist/engine/project-app-schema-for-roles.js +1 -1
- package/dist/engine/projection-helpers.js +9 -9
- package/dist/engine/qualified-name.js +2 -2
- package/dist/engine/registry-facade.js +1 -1
- package/dist/engine/registry-ingest.js +12 -12
- package/dist/engine/registry-state.js +17 -19
- package/dist/engine/registry-validate.js +12 -12
- package/dist/engine/run-in.js +2 -2
- package/dist/engine/run-pipeline.js +7 -0
- package/dist/engine/schema-builder.js +40 -38
- package/dist/engine/screen-filter-ops.js +17 -20
- package/dist/engine/screen-helpers.js +4 -3
- package/dist/engine/state-machine.d.ts +12 -12
- package/dist/engine/state-machine.js +7 -7
- package/dist/engine/steps/_resolver-utils.js +4 -3
- package/dist/engine/steps/for-each.js +4 -4
- package/dist/engine/steps/unsafe-projection-delete.js +1 -1
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
- package/dist/engine/steps/wait-for-event.js +4 -4
- package/dist/engine/steps/workflow-system-events.js +3 -3
- package/dist/engine/tier-resolver-extension.d.ts +29 -27
- package/dist/engine/tier-resolver-extension.js +26 -26
- package/dist/engine/transfer-adjacency.js +2 -2
- package/dist/engine/validate-projection-allowlist.js +7 -27
- package/dist/engine/walk-step-instances.d.ts +2 -0
- package/dist/engine/walk-step-instances.js +38 -0
- package/dist/engine/where-rule-lint.js +1 -1
- package/dist/engine/write-handler-qn-extract.js +1 -1
- package/dist/entrypoint/index.js +3 -3
- package/dist/env/index.d.ts +7 -6
- package/dist/env/index.js +10 -9
- package/dist/errors/transition-details.js +7 -7
- package/dist/errors/write-error-info.d.ts +6 -6
- package/dist/errors/write-error-info.js +6 -6
- package/dist/es-ops/context.d.ts +9 -9
- package/dist/es-ops/context.js +28 -28
- package/dist/es-ops/index.js +7 -9
- package/dist/es-ops/operations-schema.js +4 -10
- package/dist/es-ops/runner.d.ts +11 -11
- package/dist/es-ops/runner.js +49 -53
- package/dist/es-ops/types.d.ts +55 -55
- package/dist/es-ops/types.js +11 -11
- package/dist/event-store/admin-api.js +6 -7
- package/dist/event-store/errors.js +1 -1
- package/dist/event-store/event-store.js +2 -2
- package/dist/event-store/rebuild-dead-letter.js +1 -1
- package/dist/files/feature.js +9 -10
- package/dist/files/file-handle.js +5 -6
- package/dist/files/file-ref-entity.js +15 -15
- package/dist/files/file-ref-table.js +7 -8
- package/dist/files/file-routes.d.ts +7 -0
- package/dist/files/file-routes.js +11 -5
- package/dist/files/in-memory-provider.js +8 -9
- package/dist/files/local-provider.js +18 -25
- package/dist/files/provider-resolver.d.ts +7 -7
- package/dist/files/storage-tracking.js +2 -2
- package/dist/files/zip-stream.d.ts +12 -19
- package/dist/files/zip-stream.js +44 -63
- package/dist/i18n/required-surface-keys.d.ts +1 -1
- package/dist/i18n/required-surface-keys.js +8 -8
- package/dist/internal/stack-support.d.ts +1 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-helpers.d.ts +1 -2
- package/dist/jobs/job-runner-helpers.js +8 -18
- package/dist/jobs/job-runner-schedulers.d.ts +1 -1
- package/dist/jobs/job-runner-schedulers.js +3 -4
- package/dist/jobs/job-runner.js +17 -18
- package/dist/lifecycle/lifecycle.js +3 -5
- package/dist/lifecycle/signal-handlers.js +5 -4
- package/dist/logging/utils.d.ts +2 -1
- package/dist/logging/utils.js +38 -10
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/kumiko-drift.js +14 -13
- package/dist/migrations/pending-rebuilds.d.ts +21 -20
- package/dist/migrations/pending-rebuilds.js +27 -27
- package/dist/migrations/projection-table-index.d.ts +3 -3
- package/dist/migrations/projection-table-index.js +8 -8
- package/dist/observability/metric-validator.js +1 -1
- package/dist/observability/metrics-handle.js +3 -3
- package/dist/pipeline/append-event-core.js +11 -2
- package/dist/pipeline/cascade-handler.d.ts +1 -1
- package/dist/pipeline/dispatch-batch.js +2 -2
- package/dist/pipeline/dispatch-query.js +10 -10
- package/dist/pipeline/dispatch-shared.d.ts +1 -0
- package/dist/pipeline/dispatch-shared.js +42 -19
- package/dist/pipeline/dispatch-stream.js +1 -1
- package/dist/pipeline/dispatch-write.js +3 -3
- package/dist/pipeline/dispatcher-utils.d.ts +1 -1
- package/dist/pipeline/dispatcher-utils.js +1 -1
- package/dist/pipeline/event-consumer-state.js +2 -2
- package/dist/pipeline/event-dispatcher-delivery.js +1 -1
- package/dist/pipeline/event-dispatcher.js +17 -28
- package/dist/pipeline/irreversible-operation-gate.js +1 -1
- package/dist/pipeline/lifecycle-pipeline.js +9 -9
- package/dist/pipeline/member-read-only-transaction.js +1 -3
- package/dist/pipeline/msp-rebuild.js +1 -1
- package/dist/pipeline/projection-rebuild.js +13 -12
- package/dist/pipeline/system-hooks.js +39 -41
- package/dist/pipeline/system-identity-switch.js +1 -3
- package/dist/pipeline/tenant-timezone-cache.js +1 -1
- package/dist/random/generate.d.ts +29 -29
- package/dist/random/generate.js +30 -30
- package/dist/random/words.js +20 -20
- package/dist/rate-limit/middleware.js +9 -5
- package/dist/redis/client.js +2 -2
- package/dist/redis/pubsub-signal.js +14 -17
- package/dist/schema-cli.js +116 -22
- package/dist/search/index.js +4 -4
- package/dist/search/purge-subject.js +4 -5
- package/dist/search/reindex-entity.js +6 -9
- package/dist/time/boot-tz-warning.d.ts +2 -2
- package/dist/time/boot-tz-warning.js +8 -8
- package/dist/time/iana.js +10 -10
- package/dist/time/index.js +9 -9
- package/dist/time/legacy-date.d.ts +2 -2
- package/dist/time/legacy-date.js +6 -8
- package/dist/time/polyfill.js +1 -1
- package/dist/time/tz-context.d.ts +1 -1
- package/dist/time/tz-context.js +1 -1
- package/dist/ui-types/app-schema.js +7 -7
- package/dist/ui-types/list-row-meta.js +4 -4
- package/dist/upgrade-cli.js +10 -9
- package/dist/utils/compare.js +1 -1
- package/package.json +4 -4
- package/src/changes.json +54 -0
- package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
- package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
- package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
- package/src/scripts/codemod/pii-personal-migration.ts +17 -8
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
package/dist/db/table-builder.js
CHANGED
|
@@ -16,29 +16,28 @@ export function physicalColumnName(table, field) {
|
|
|
16
16
|
// Returns column(s) for a field. Most fields return a single entry,
|
|
17
17
|
// money returns two (amount + currency).
|
|
18
18
|
//
|
|
19
|
-
// `required: true`
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// die einzige Wahrheit.
|
|
19
|
+
// `required: true` on a FieldDefinition always maps to .notNull() in the
|
|
20
|
+
// database column. Previously this applied only to reference fields, while
|
|
21
|
+
// text/select/number/etc. remained silently nullable even when the API
|
|
22
|
+
// validator enforced required. That forced hand-written PgTable definitions
|
|
23
|
+
// to add .notNull(), causing duplicate definitions and schema drift. Now
|
|
24
|
+
// r.entity is the single source of truth.
|
|
26
25
|
function fieldToColumns(name, field, entity) {
|
|
27
26
|
const snakeName = toSnakeCase(name);
|
|
28
27
|
switch (field.type) {
|
|
29
28
|
case "text":
|
|
30
29
|
case "longText": {
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
30
|
+
// Both map to PG `text` (unbounded). The difference exists only in the
|
|
31
|
+
// type layer: longText has no sortable/searchable/filterable support.
|
|
32
|
+
// The order default() before notNull() matters: the column builder
|
|
33
|
+
// chains both; without default(), the generated SQL has no DEFAULT
|
|
34
|
+
// clause, which breaks ALTER TABLE ADD COLUMN on existing rows.
|
|
35
|
+
// longText currently has no default field in its type, but checking
|
|
36
|
+
// `field.default !== undefined` is defensive.
|
|
38
37
|
const base = text(snakeName);
|
|
39
38
|
const withDefault = "default" in field && field.default !== undefined ? base.default(field.default) : base;
|
|
40
39
|
const mainCol = { [name]: field.required ? withDefault.notNull() : withDefault };
|
|
41
|
-
// lookupable → nullable HMAC-
|
|
40
|
+
// lookupable → nullable HMAC blind-index column (kept in lockstep with
|
|
42
41
|
// fieldToColumnMeta in entity-table-meta.ts).
|
|
43
42
|
if (field.type === "text" && field.lookupable === true) {
|
|
44
43
|
return { ...mainCol, [`${name}Bidx`]: text(`${snakeName}_bidx`) };
|
|
@@ -54,23 +53,23 @@ function fieldToColumns(name, field, entity) {
|
|
|
54
53
|
: boolean(snakeName),
|
|
55
54
|
};
|
|
56
55
|
case "select": {
|
|
57
|
-
// default()
|
|
58
|
-
// defaults
|
|
59
|
-
//
|
|
60
|
-
//
|
|
56
|
+
// Pass default() through because entity-table-meta renders declared
|
|
57
|
+
// defaults. This builder must stay in lockstep; otherwise the metadata
|
|
58
|
+
// on the buildEntityTable object describes a different column than the
|
|
59
|
+
// migration.
|
|
61
60
|
const base = text(snakeName);
|
|
62
61
|
const col = field.default !== undefined ? base.default(field.default) : base;
|
|
63
62
|
return { [name]: field.required ? col.notNull() : col };
|
|
64
63
|
}
|
|
65
64
|
case "multiSelect":
|
|
66
|
-
// jsonb
|
|
65
|
+
// jsonb string array with default `[]`, always NOT NULL.
|
|
67
66
|
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// null
|
|
73
|
-
//
|
|
67
|
+
// The `required` flag on MultiSelectFieldDef is intentionally ignored:
|
|
68
|
+
// with default `[]`, the field is structurally never null (an insert
|
|
69
|
+
// without a value produces an empty array, not NULL). Read-side code
|
|
70
|
+
// therefore needs no null check; this is an API guarantee. Code that
|
|
71
|
+
// needs actual null (meaning the field has never been set) should use
|
|
72
|
+
// a separate status field or an optional-typed reference instead of a
|
|
74
73
|
// multi-select.
|
|
75
74
|
return { [name]: jsonb(snakeName).default([]).notNull() };
|
|
76
75
|
case "number": {
|
|
@@ -84,19 +83,18 @@ function fieldToColumns(name, field, entity) {
|
|
|
84
83
|
return { [name]: field.required ? col.notNull() : col };
|
|
85
84
|
}
|
|
86
85
|
case "bigInt": {
|
|
87
|
-
// 64-bit
|
|
88
|
-
// mode:"number"
|
|
89
|
-
// JS
|
|
90
|
-
//
|
|
91
|
-
// eigenem Codec.
|
|
86
|
+
// 64-bit integer for audit counters, byte sizes, and cumulative sums.
|
|
87
|
+
// mode:"number" produces a JS `number` (safe up to 2^53 ≈ 9 PB) rather
|
|
88
|
+
// than JS `bigint`, making it JSON-serializable and frontend-friendly.
|
|
89
|
+
// Values above 2^53 should use a text field with a custom codec.
|
|
92
90
|
const base = bigint(snakeName, { mode: "number" });
|
|
93
91
|
const col = field.default !== undefined ? base.default(field.default) : base;
|
|
94
92
|
return { [name]: field.required ? col.notNull() : col };
|
|
95
93
|
}
|
|
96
94
|
case "reference":
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
95
|
+
// FK-style UUID column. Multi-mode stores UUIDs as a jsonb string array;
|
|
96
|
+
// single-mode remains a conventional UUID column (NOT NULL only when
|
|
97
|
+
// required).
|
|
100
98
|
if (field.multiple === true) {
|
|
101
99
|
return { [name]: jsonb(snakeName).default([]).notNull() };
|
|
102
100
|
}
|
|
@@ -104,11 +102,12 @@ function fieldToColumns(name, field, entity) {
|
|
|
104
102
|
[name]: field.required ? uuid(snakeName).notNull() : uuid(snakeName),
|
|
105
103
|
};
|
|
106
104
|
case "money":
|
|
107
|
-
// BIGINT
|
|
108
|
-
// the currency column
|
|
109
|
-
// which is too
|
|
110
|
-
// aggregates. BIGINT handles up to
|
|
111
|
-
//
|
|
105
|
+
// BIGINT stores the integer minor unit (cents for EUR, yen for JPY —
|
|
106
|
+
// the currency column identifies which). INTEGER would cap at about
|
|
107
|
+
// 21 M EUR, which is too low for B2B invoices, property values, or
|
|
108
|
+
// balance aggregates. BIGINT safely handles up to about 90 trillion
|
|
109
|
+
// EUR in JS.
|
|
110
|
+
// Currency always has a default, so it is structurally .notNull().
|
|
112
111
|
return {
|
|
113
112
|
[name]: field.required ? moneyAmount(snakeName).notNull() : moneyAmount(snakeName),
|
|
114
113
|
[`${name}Currency`]: text(`${snakeName}_currency`)
|
|
@@ -116,47 +115,47 @@ function fieldToColumns(name, field, entity) {
|
|
|
116
115
|
.notNull(),
|
|
117
116
|
};
|
|
118
117
|
case "embedded":
|
|
119
|
-
// jsonb
|
|
120
|
-
// `required`
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
118
|
+
// jsonb with default `{}`, always NOT NULL, analogous to multiSelect.
|
|
119
|
+
// `required` is intentionally ignored because the default makes the
|
|
120
|
+
// field structurally never null. To make embedded data optional (so
|
|
121
|
+
// the field can be omitted entirely), model it with a wrapper field
|
|
122
|
+
// and boolean flag or a discriminated union.
|
|
124
123
|
// multiple → the value is a list of rows, so the empty default is `[]`.
|
|
125
124
|
return field.multiple === true
|
|
126
125
|
? { [name]: jsonb(snakeName).default([]).notNull() }
|
|
127
126
|
: { [name]: jsonb(snakeName).default({}).notNull() };
|
|
128
127
|
case "jsonb":
|
|
129
|
-
// Free-form jsonb — keys
|
|
130
|
-
// (
|
|
131
|
-
// `customFields
|
|
128
|
+
// Free-form jsonb — keys are not schema-validated. Default `{}`, NOT NULL
|
|
129
|
+
// (analogous to embedded). Use case: the host entity's custom-fields
|
|
130
|
+
// bundle in the `customFields` column, whose keys are tenant-defined.
|
|
132
131
|
return { [name]: jsonb(snakeName).default({}).notNull() };
|
|
133
132
|
case "date": {
|
|
134
133
|
// Real PG `date` — no time-of-day, no timezone. Temporal.PlainDate
|
|
135
134
|
// end-to-end (see plainDate() in dialect.ts). Was aliased on instant()
|
|
136
|
-
// = TIMESTAMPTZ
|
|
135
|
+
// = TIMESTAMPTZ (TZ-dependent round-trip).
|
|
137
136
|
const col = plainDate(snakeName);
|
|
138
137
|
return { [name]: field.required ? col.notNull() : col };
|
|
139
138
|
}
|
|
140
139
|
case "timestamp": {
|
|
141
|
-
// UTC
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
140
|
+
// UTC instant — stored as TIMESTAMPTZ in PG and read/written as
|
|
141
|
+
// Temporal.Instant through the instant() customType (see dialect.ts).
|
|
142
|
+
// Callers use only Temporal.Instant, never JS Date. Comparisons
|
|
143
|
+
// (lte/gt/orderBy) also accept Instants directly, so no .toString()
|
|
144
|
+
// cast is needed.
|
|
146
145
|
const col = instant(snakeName);
|
|
147
146
|
return { [name]: field.required ? col.notNull() : col };
|
|
148
147
|
}
|
|
149
148
|
case "tz": {
|
|
150
|
-
// IANA-
|
|
151
|
-
//
|
|
149
|
+
// IANA time-zone name as TEXT — validated through the Zod schema in the
|
|
150
|
+
// validator step. Snake-case convention: `pickup_tz`.
|
|
152
151
|
const col = text(snakeName);
|
|
153
152
|
return { [name]: field.required ? col.notNull() : col };
|
|
154
153
|
}
|
|
155
154
|
case "locatedTimestamp": {
|
|
156
|
-
//
|
|
157
|
-
// _utc
|
|
158
|
-
//
|
|
159
|
-
//
|
|
155
|
+
// Two columns form an atomic pair: <name>_utc TIMESTAMPTZ + <name>_tz TEXT.
|
|
156
|
+
// _utc is instant() (Temporal.Instant), and _tz is text (IANA name).
|
|
157
|
+
// The executor wires automatic conversion (at+tz → utc on insert;
|
|
158
|
+
// utc+tz → at on read). required propagates to both columns.
|
|
160
159
|
const utc = instant(`${snakeName}_utc`);
|
|
161
160
|
const tz = text(`${snakeName}_tz`);
|
|
162
161
|
return {
|
|
@@ -188,12 +187,12 @@ function fieldToColumns(name, field, entity) {
|
|
|
188
187
|
* Derives a table name from an entity name:
|
|
189
188
|
* 1. camelCase → snake_case (e.g. "memberTask" → "member_task")
|
|
190
189
|
* 2. Simple English pluralization (category→categories, status→statuses, task→tasks)
|
|
191
|
-
* 3. `read_` prefix —
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* (
|
|
195
|
-
*
|
|
196
|
-
*
|
|
190
|
+
* 3. `read_` prefix — marks the table as an event-sourced read model so
|
|
191
|
+
* it is immediately distinguishable in database tools and application
|
|
192
|
+
* code is not expected to write to it directly. The event store
|
|
193
|
+
* (`kumiko_events`) and framework state (`kumiko_*`) use their own
|
|
194
|
+
* prefixes; ordinary app-side tables without event-sourcing have none.
|
|
195
|
+
* The three categories are therefore distinguishable in the table browser.
|
|
197
196
|
*/
|
|
198
197
|
const ES_PLURAL_SUFFIXES = ["s", "sh", "ch", "x"];
|
|
199
198
|
export const READ_MODEL_PREFIX = "read_";
|
|
@@ -219,12 +218,13 @@ export function buildBaseColumns(softDelete, idType = "uuid") {
|
|
|
219
218
|
id: idColumn,
|
|
220
219
|
tenantId: uuid("tenant_id").notNull(),
|
|
221
220
|
version: integer("version").default(1).notNull(),
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
221
|
+
// Temporal.Instant is used throughout (see instant() in dialect.ts).
|
|
222
|
+
// Previously the default mode was "date", which was inconsistent with
|
|
223
|
+
// user-defined timestamp fields (mode "string"). A single mode is now
|
|
224
|
+
// used for all timestamps.
|
|
225
225
|
// customType doesn't expose Drizzle's `defaultNow()` shortcut — use raw
|
|
226
|
-
// SQL so PG sets the value on insert and
|
|
227
|
-
// Instant
|
|
226
|
+
// SQL so PG sets the value on insert and callers need not pass an
|
|
227
|
+
// Instant for every row creation.
|
|
228
228
|
insertedAt: instant("inserted_at").default(sql `now()`).notNull(),
|
|
229
229
|
modifiedAt: instant("modified_at"),
|
|
230
230
|
// User-IDs are stringified UUIDs post-ES migration. Text (not uuid) so the
|
|
@@ -295,9 +295,9 @@ export function buildEntityTable(entityName, entity, options) {
|
|
|
295
295
|
}
|
|
296
296
|
// Default table name derived from entityName (e.g. "memberTask" → "read_member_tasks")
|
|
297
297
|
const baseTableName = entity.table ?? toTableName(entityName);
|
|
298
|
-
// featureName
|
|
299
|
-
//
|
|
300
|
-
// featureName
|
|
298
|
+
// The featureName prefix is inserted between read_ and the base name so
|
|
299
|
+
// all read models consistently start with `read_`, whether or not
|
|
300
|
+
// featureName is set. Examples:
|
|
301
301
|
// featureName="shop", base="read_orders" → "read_shop_orders"
|
|
302
302
|
// featureName=undef, base="read_orders" → "read_orders"
|
|
303
303
|
// featureName="shop", base="orders" (no read_) → "shop_orders"
|
|
@@ -349,7 +349,7 @@ export function buildEntityTable(entityName, entity, options) {
|
|
|
349
349
|
indexes[`${tableName}_${toSnakeCase(fieldName)}_idx`] = index(`${tableName}_${toSnakeCase(fieldName)}_idx`).on(column);
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
|
-
// lookupable
|
|
352
|
+
// lookupable fields: index the bidx column (kept in lockstep with
|
|
353
353
|
// deriveEntityTableMeta).
|
|
354
354
|
const bidxFieldByField = new Map();
|
|
355
355
|
for (const [name, field] of Object.entries(entity.fields)) {
|
|
@@ -362,10 +362,10 @@ export function buildEntityTable(entityName, entity, options) {
|
|
|
362
362
|
indexes[idxName] = index(idxName).on(column);
|
|
363
363
|
}
|
|
364
364
|
}
|
|
365
|
-
// entity.indexes
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
365
|
+
// entity.indexes contains composite/unique indexes explicitly declared by
|
|
366
|
+
// the author. Columns are addressed by field name (camelCase), and the
|
|
367
|
+
// index name follows <table>_<col1>_<col2>_<unique|idx>; index.name can
|
|
368
|
+
// override the convention.
|
|
369
369
|
for (const def of entity.indexes ?? []) {
|
|
370
370
|
const cols = def.columns
|
|
371
371
|
.map((fieldName) => tHandle[fieldName])
|
|
@@ -388,12 +388,12 @@ export function buildEntityTable(entityName, entity, options) {
|
|
|
388
388
|
}
|
|
389
389
|
else if (def.unique === true && entity.softDelete === true) {
|
|
390
390
|
// Soft-deleted rows keep their value, so a full unique index would
|
|
391
|
-
// permanently block reuse; kept in lock-step with entity-table-meta.ts
|
|
391
|
+
// permanently block reuse; kept in lock-step with entity-table-meta.ts.
|
|
392
392
|
chain = chain.where(sql.raw(SOFT_DELETE_LIVE_ROW_PREDICATE));
|
|
393
393
|
}
|
|
394
394
|
indexes[indexName] = chain;
|
|
395
|
-
//
|
|
396
|
-
// (
|
|
395
|
+
// Partial bidx counterpart for unique indexes over lookupable fields
|
|
396
|
+
// (kept in lockstep with deriveEntityTableMeta).
|
|
397
397
|
if (def.unique === true && def.where === undefined) {
|
|
398
398
|
const bidxFieldNames = def.columns.map((c) => bidxFieldByField.get(c) ?? c);
|
|
399
399
|
if (bidxFieldNames.some((c, i) => c !== def.columns[i])) {
|
|
@@ -406,7 +406,7 @@ export function buildEntityTable(entityName, entity, options) {
|
|
|
406
406
|
.map((c) => `"${toSnakeCase(c)}" IS NOT NULL`);
|
|
407
407
|
// Soft-deleted rows keep their bidx hash — without this, a
|
|
408
408
|
// restored row can't reuse a value a soft-deleted sibling still
|
|
409
|
-
// holds
|
|
409
|
+
// holds.
|
|
410
410
|
const whereText = (entity.softDelete === true
|
|
411
411
|
? [...notNullParts, SOFT_DELETE_LIVE_ROW_PREDICATE]
|
|
412
412
|
: notNullParts).join(" AND ");
|
package/dist/db/tenant-db.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare function withExecutorManagedWrites(runner: DbRunner): ExecutorMan
|
|
|
16
16
|
export declare function withoutExecutorManagedWrites(runner: DbRunner): DbRunner;
|
|
17
17
|
export declare function assertPersonalDataWrite(db: TenantDb, tableName: string, keys: readonly string[], entity: EntityDefinition): void;
|
|
18
18
|
export declare function unsafeRawForDeclaredStep(holder: TenantDb | UncheckedSystemDb, reason: string): DbRunner;
|
|
19
|
-
export declare function withSystemDbUnsafeRawGrant(systemDb:
|
|
19
|
+
export declare function withSystemDbUnsafeRawGrant<T extends object>(systemDb: T, grant: EscapeHatchDeclaration | undefined, deniedCallerLabel: string): T | UncheckedSystemDb;
|
|
20
20
|
export declare function createUncheckedSystemDb(db: TenantDb, dbOutsideTransaction?: TenantDb, report?: EscapeHatchReporter): UncheckedSystemDb;
|
|
21
21
|
export declare function createSystemDbView(db: TenantDb, dbOutsideTransaction?: TenantDb, report?: EscapeHatchReporter): UncheckedSystemDb;
|
|
22
22
|
export declare function scopeTenantDbToTenant(db: TenantDb, tenantId: TenantId): TenantDb;
|
|
@@ -34,7 +34,7 @@ export type TenantDbGrants = {
|
|
|
34
34
|
readonly writesDeniedReason?: string;
|
|
35
35
|
};
|
|
36
36
|
export type PersonalDataGate = (tableName: string, keys: readonly string[], entity?: EntityDefinition) => void;
|
|
37
|
-
export declare function withUnsafeRawGrant(tenantDb:
|
|
37
|
+
export declare function withUnsafeRawGrant<T extends object>(tenantDb: T, grant: EscapeHatchDeclaration | undefined): T | TenantDb;
|
|
38
38
|
export declare function acknowledgeConventionCrossTenant(tenantDb: TenantDb, reason: string, options?: {
|
|
39
39
|
readonly deferReport?: boolean;
|
|
40
40
|
}): TenantDb;
|
package/dist/db/tenant-db.js
CHANGED
|
@@ -26,6 +26,9 @@ function boundRunner(runner, gate, projectionRegistry, executorManagedWrites) {
|
|
|
26
26
|
const proxy = new Proxy(runner, {
|
|
27
27
|
// Tagged-template calls need the real driver object as `this`.
|
|
28
28
|
apply(target, _thisArg, args) {
|
|
29
|
+
if (typeof target !== "function") {
|
|
30
|
+
throw new TypeError("tenant-db runner proxy applied on a non-callable runner");
|
|
31
|
+
}
|
|
29
32
|
return Reflect.apply(target, target, args);
|
|
30
33
|
},
|
|
31
34
|
get(target, prop, _receiver) {
|
|
@@ -51,7 +54,11 @@ function boundRunner(runner, gate, projectionRegistry, executorManagedWrites) {
|
|
|
51
54
|
}
|
|
52
55
|
const gatedArgs = [
|
|
53
56
|
...args.slice(0, -1),
|
|
54
|
-
(tx) =>
|
|
57
|
+
(tx) => {
|
|
58
|
+
// @cast-boundary db-runner — driver callback hands over the tx handle untyped
|
|
59
|
+
const txRunner = tx;
|
|
60
|
+
return callback(boundRunner(txRunner, gate, projectionRegistry, executorManagedWrites));
|
|
61
|
+
},
|
|
55
62
|
];
|
|
56
63
|
return Reflect.apply(value, target, gatedArgs);
|
|
57
64
|
};
|
|
@@ -179,7 +186,7 @@ function buildUncheckedSystemDb(db, dbOutsideTransaction, report, gate) {
|
|
|
179
186
|
// Reference rows (tenantId === SYSTEM_TENANT_ID) are allowed, mirroring
|
|
180
187
|
// "tenant"-mode readWhere's own [tenantId, SYSTEM_TENANT_ID] allowlist.
|
|
181
188
|
assertRowsTenant(rows, tenantField) {
|
|
182
|
-
const hasOffender = rows.some((row) => !allowedTenantIds.
|
|
189
|
+
const hasOffender = rows.some((row) => !allowedTenantIds.some((allowed) => allowed === row[tenantField]));
|
|
183
190
|
if (hasOffender) {
|
|
184
191
|
throw new AccessDeniedError({
|
|
185
192
|
message: `systemScope() row tenant self-check failed on field "${String(tenantField)}"`,
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// Used by both the user-data-rights-defaults forget hook and the
|
|
13
13
|
// data-retention hardDelete cleanup — moved here so neither copies the logic.
|
|
14
14
|
import { assertSafeStorageKey } from "../files/types.js";
|
|
15
|
+
import { createFallbackLogger } from "../logging/utils.js";
|
|
15
16
|
import { derivativeListPrefix, isDerivativeKeyOf } from "./variant-key.js";
|
|
16
17
|
// Wrap rather than let a raw provider error (e.g. S3 AccessDenied) surface
|
|
17
18
|
// unexplained — list() is a requirement on top of delete(); a bucket policy
|
|
@@ -62,8 +63,10 @@ export async function deleteStoredFileAndDerivatives(originalKey, store, errorCo
|
|
|
62
63
|
}
|
|
63
64
|
catch (err) {
|
|
64
65
|
failedKeys.push(key);
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
createFallbackLogger(errorContext).warn("storage delete failed", {
|
|
67
|
+
key,
|
|
68
|
+
error: err instanceof Error ? err.message : String(err),
|
|
69
|
+
});
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
return failedKeys;
|
|
@@ -116,7 +116,7 @@ async function resolveOverlaySpec(spec, row, registry, tenantId) {
|
|
|
116
116
|
// allowlisted before it becomes a response Content-Type or storage-provider
|
|
117
117
|
// metadata. Otherwise an app whose renderer wildcard is broad enough (e.g.
|
|
118
118
|
// `image/*`) would let a client-declared `image/svg+xml` ride straight
|
|
119
|
-
// through as an honestly-declared (non-sniffed) active-content type
|
|
119
|
+
// through as an honestly-declared (non-sniffed) active-content type.
|
|
120
120
|
const KNOWN_SAFE_VARIANT_MIME_TYPES = new Set([
|
|
121
121
|
"image/jpeg",
|
|
122
122
|
"image/png",
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createFallbackLogger } from "../../logging/utils.js";
|
|
1
2
|
import { normalizeAccessEntry } from "../ownership.js";
|
|
3
|
+
const log = createFallbackLogger("kumiko:boot");
|
|
2
4
|
const BUILTIN_ROLES = new Set(["all", "system"]);
|
|
3
5
|
function addRole(roleHandlers, role, identifier) {
|
|
4
6
|
let handlers = roleHandlers.get(role);
|
|
@@ -50,7 +52,7 @@ function addEntityFieldRoles(roleHandlers, f) {
|
|
|
50
52
|
// purpose) are the normal case, not a typo — and until every access
|
|
51
53
|
// surface is scanned, a role can look unique here while it's really used
|
|
52
54
|
// elsewhere (configKeys / entity+field access), a false positive in the
|
|
53
|
-
// other direction.
|
|
55
|
+
// other direction. This is why this stays opt-in rather than a
|
|
54
56
|
// default-on prod warning.
|
|
55
57
|
export function warnOnUniqueAccessRoles(features) {
|
|
56
58
|
// role → set of distinct identifiers using it, across every access
|
|
@@ -68,8 +70,7 @@ export function warnOnUniqueAccessRoles(features) {
|
|
|
68
70
|
if (handlers.size !== 1)
|
|
69
71
|
continue;
|
|
70
72
|
const [identifier] = handlers;
|
|
71
|
-
|
|
72
|
-
console.warn(`[kumiko:boot] Access role "${role}" is only used by one handler (${identifier}). ` +
|
|
73
|
+
log.warn(`Access role "${role}" is only used by one handler (${identifier}). ` +
|
|
73
74
|
`This is often a typo — the role is unknown to every other handler in the boot scan. ` +
|
|
74
75
|
`If this is intentional, ignore this warning.`);
|
|
75
76
|
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { createFallbackLogger } from "../../logging/utils.js";
|
|
2
|
+
const log = createFallbackLogger("kumiko:boot");
|
|
1
3
|
// --- Cross-feature API exposure / usage matching ---
|
|
2
4
|
//
|
|
3
5
|
// `r.exposesApi(name, impl)` registers a callable; `r.usesApi(name)`
|
|
4
|
-
// declares a caller. Boot
|
|
5
|
-
// 1.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// mit Warning markiert weil es typisch ein Refactor-Restbestand ist.
|
|
6
|
+
// declares a caller. Boot validation checks three invariants:
|
|
7
|
+
// 1. Every usesApi(name) finds an exposesApi(name) in some feature.
|
|
8
|
+
// 2. The exposing feature is listed in the caller's
|
|
9
|
+
// requires/optionalRequires so cross-feature call ordering works.
|
|
10
|
+
// 3. Self-exposure is allowed (a feature calling its own API), but is
|
|
11
|
+
// warned about because it is typically a refactoring leftover.
|
|
11
12
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
13
|
+
// apiNames must be globally unique across features; validateBoot() checks
|
|
14
|
+
// this before the per-feature walk.
|
|
14
15
|
export function validateApiExposureMatching(feature, allExposedApis, featureMap) {
|
|
15
16
|
for (const apiName of feature.usedApis) {
|
|
16
17
|
const providerFeature = allExposedApis.get(apiName);
|
|
@@ -19,17 +20,16 @@ export function validateApiExposureMatching(feature, allExposedApis, featureMap)
|
|
|
19
20
|
throw new Error(`[Feature ${feature.name}] r.usesApi("${apiName}") but no feature exposes that API. Known exposed APIs: ${known}`);
|
|
20
21
|
}
|
|
21
22
|
if (providerFeature === feature.name) {
|
|
22
|
-
|
|
23
|
-
console.warn(`[kumiko:boot] [Feature ${feature.name}] r.usesApi("${apiName}") on its own r.exposesApi — typically a refactor leftover. Call the impl directly instead.`);
|
|
23
|
+
log.warn(`[Feature ${feature.name}] r.usesApi("${apiName}") on its own r.exposesApi — typically a refactor leftover. Call the impl directly instead.`);
|
|
24
24
|
continue;
|
|
25
25
|
}
|
|
26
26
|
const allDeps = [...feature.requires, ...feature.optionalRequires];
|
|
27
27
|
if (!allDeps.includes(providerFeature)) {
|
|
28
28
|
throw new Error(`[Feature ${feature.name}] r.usesApi("${apiName}") is exposed by "${providerFeature}" but feature is not in requires/optionalRequires. Add r.requires({ features: ["${providerFeature}"] }).`);
|
|
29
29
|
}
|
|
30
|
-
// Sanity: provider feature
|
|
31
|
-
//
|
|
32
|
-
//
|
|
30
|
+
// Sanity: the provider feature must exist in this app's feature set.
|
|
31
|
+
// This should always hold when allExposedApis was built from `features`,
|
|
32
|
+
// but remains defensive for unclear constructor paths.
|
|
33
33
|
if (!featureMap.has(providerFeature)) {
|
|
34
34
|
throw new Error(`[Feature ${feature.name}] internal: r.usesApi("${apiName}") points to provider "${providerFeature}" which is not in feature map. Register the provider feature in the app's feature list.`);
|
|
35
35
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { optionsQueryFieldRefs } from "@cosmicdrift/kumiko-types/fields";
|
|
2
|
+
import { createFallbackLogger } from "../../logging/utils.js";
|
|
2
3
|
import { isEncryptedAtRest } from "../config-helpers.js";
|
|
4
|
+
const log = createFallbackLogger("kumiko:boot");
|
|
3
5
|
// --- Toggleable-dependency warnings ---
|
|
4
6
|
//
|
|
5
7
|
// When feature A declares r.requires({ features: ["B"] }) and B is toggleable with
|
|
@@ -15,8 +17,7 @@ export function warnOnToggleableDependencies(features, featureMap) {
|
|
|
15
17
|
if (!depFeature)
|
|
16
18
|
continue; // requires-target-missing is handled elsewhere
|
|
17
19
|
if (depFeature.toggleableDefault === false) {
|
|
18
|
-
|
|
19
|
-
console.warn(`[kumiko:boot] Feature "${f.name}" requires "${dep}", which is toggleable(default=false). ` +
|
|
20
|
+
log.warn(`Feature "${f.name}" requires "${dep}", which is toggleable(default=false). ` +
|
|
20
21
|
`"${f.name}" will be effectively disabled until "${dep}" is enabled globally via the feature-toggles feature. ` +
|
|
21
22
|
`If this is intentional, ignore this warning; otherwise consider r.optionalRequires() or default=true.`);
|
|
22
23
|
}
|
|
@@ -70,9 +71,9 @@ export function validateConfigKeyRequired(feature) {
|
|
|
70
71
|
for (const [keyName, keyDef] of Object.entries(feature.configKeys ?? {})) {
|
|
71
72
|
if (keyDef.required !== true)
|
|
72
73
|
continue;
|
|
73
|
-
// required
|
|
74
|
-
//
|
|
75
|
-
//
|
|
74
|
+
// `required` means the tenant must configure the key. A non-empty default
|
|
75
|
+
// or computed resolver means the key can never be unset, so readiness could
|
|
76
|
+
// never report the omission and the flag would be misleading.
|
|
76
77
|
if (keyDef.computed !== undefined) {
|
|
77
78
|
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" has required=true AND a computed resolver — a computed key can never be missing; drop one of the two`);
|
|
78
79
|
}
|
|
@@ -160,7 +161,7 @@ export function validateConfigKeyBacking(feature) {
|
|
|
160
161
|
// read/write without it throws loud at request time, not silently.
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
|
-
// --- Config key piiEncrypted compatibility
|
|
164
|
+
// --- Config key piiEncrypted compatibility ---
|
|
164
165
|
export function validateConfigKeyPiiEncrypted(feature) {
|
|
165
166
|
for (const [keyName, keyDef] of Object.entries(feature.configKeys)) {
|
|
166
167
|
if (!keyDef.piiEncrypted)
|
|
@@ -2,9 +2,9 @@ import { qualifyEntityName } from "../qualified-name.js";
|
|
|
2
2
|
import { findEntityFeature } from "./screens.js";
|
|
3
3
|
// Entity name → the one screen that declares detailFor: "<entity>". Built
|
|
4
4
|
// once, up front (before the per-feature validateScreens loop) so both this
|
|
5
|
-
// module's own uniqueness check AND rowAction entity-targets (
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// module's own uniqueness check AND rowAction entity-targets (in screens.ts)
|
|
6
|
+
// resolve against the same map instead of re-walking every feature's
|
|
7
|
+
// screens twice.
|
|
8
8
|
export function collectDetailForScreens(features, featureMap) {
|
|
9
9
|
const screenQnByEntity = new Map();
|
|
10
10
|
const result = new Map();
|