@cosmicdrift/kumiko-framework 0.387.0 → 0.388.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -1
- package/agents/examples/anonymous-access.md +1 -1
- package/agents/examples/apex-landing.md +204 -0
- package/agents/examples/api-worker-split.md +64 -0
- package/agents/examples/app-config.md +14 -0
- package/agents/examples/auth-mfa.md +115 -0
- package/agents/examples/env-composition.md +28 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/prod-boot-options.md +16 -0
- package/agents/examples/rate-limiting.md +48 -0
- package/agents/examples/record-detail-layout.md +692 -0
- package/agents/examples/seed-migration.md +212 -0
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +1 -0
- package/dist/api/client-error-route.js +8 -2
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/server-boot-checks.d.ts +10 -0
- package/dist/api/server-boot-checks.js +157 -0
- package/dist/api/server-event-dispatcher.d.ts +26 -0
- package/dist/api/server-event-dispatcher.js +208 -0
- package/dist/api/server-extra-routes.d.ts +11 -0
- package/dist/api/server-extra-routes.js +203 -0
- package/dist/api/server-middleware.d.ts +20 -0
- package/dist/api/server-middleware.js +211 -0
- package/dist/api/server-routes.d.ts +19 -0
- package/dist/api/server-routes.js +293 -0
- package/dist/api/server-websocket-routes.d.ts +4 -0
- package/dist/api/server-websocket-routes.js +108 -0
- package/dist/api/server-wiring.d.ts +32 -0
- package/dist/api/server-wiring.js +236 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1251
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-route.js +16 -12
- package/dist/crypto/owner-transition-rekey.d.ts +5 -1
- package/dist/crypto/owner-transition-rekey.js +1 -1
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +3 -1
- package/dist/db/aggregate-sql.d.ts +21 -0
- package/dist/db/aggregate-sql.js +155 -0
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +95 -0
- package/dist/db/dialect.js +1 -1
- package/dist/db/distinct-on.d.ts +1 -1
- package/dist/db/driver-row-coercion.d.ts +2 -0
- package/dist/db/driver-row-coercion.js +186 -0
- package/dist/db/driver-value-preparation.d.ts +16 -0
- package/dist/db/driver-value-preparation.js +99 -0
- package/dist/db/entity-field-encryption.d.ts +1 -0
- package/dist/db/entity-field-encryption.js +1 -1
- package/dist/db/entity-table-meta.js +103 -110
- package/dist/db/event-store-executor-context.d.ts +2 -1
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +112 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +203 -0
- package/dist/db/event-store-executor-read-paging.d.ts +14 -0
- package/dist/db/event-store-executor-read-paging.js +99 -0
- package/dist/db/event-store-executor-read-rows.d.ts +6 -0
- package/dist/db/event-store-executor-read-rows.js +32 -0
- package/dist/db/event-store-executor-read-search.d.ts +28 -0
- package/dist/db/event-store-executor-read-search.js +221 -0
- package/dist/db/event-store-executor-read-sort.d.ts +41 -0
- package/dist/db/event-store-executor-read-sort.js +130 -0
- package/dist/db/event-store-executor-read-where.d.ts +24 -0
- package/dist/db/event-store-executor-read-where.js +81 -0
- package/dist/db/event-store-executor-read.d.ts +2 -9
- package/dist/db/event-store-executor-read.js +48 -701
- package/dist/db/event-store-executor-write-steps.d.ts +20 -0
- package/dist/db/event-store-executor-write-steps.js +100 -0
- package/dist/db/event-store-executor-write.js +395 -499
- package/dist/db/index.d.ts +6 -2
- package/dist/db/index.js +5 -1
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/migrate-generator.js +71 -86
- package/dist/db/migrate-runner.js +4 -1
- package/dist/db/query.d.ts +2 -15
- package/dist/db/query.js +8 -761
- package/dist/db/reference-data.js +6 -1
- package/dist/db/table-builder.js +113 -166
- package/dist/db/table-info.d.ts +10 -0
- package/dist/db/table-info.js +45 -0
- package/dist/db/temporal-value-guards.d.ts +6 -0
- package/dist/db/temporal-value-guards.js +15 -0
- package/dist/db/tenant-db-runner.d.ts +1 -0
- package/dist/db/tenant-db-runner.js +3 -0
- package/dist/db/tenant-db.js +39 -9
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +2 -1
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/detail-screens.js +2 -2
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +17 -0
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/i18n-keys.js +5 -3
- package/dist/engine/boot-validator/index.js +2 -0
- package/dist/engine/boot-validator/pii-retention.js +233 -155
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +8 -1
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +2 -6
- package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
- package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
- package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
- package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
- package/dist/engine/boot-validator/screens-lists.js +12 -9
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +99 -860
- package/dist/engine/build-app-schema.js +56 -96
- package/dist/engine/define-feature.js +9 -2
- package/dist/engine/define-handler.js +1 -0
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +10 -21
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/feature-ast/extractors/handlers.js +1 -0
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/patch.js +4 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
- package/dist/engine/feature-ast/patterns.d.ts +9 -0
- package/dist/engine/feature-ast/patterns.js +43 -51
- package/dist/engine/feature-ast/render.js +48 -87
- package/dist/engine/feature-entity-handlers.js +3 -0
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +6 -4
- package/dist/engine/registry-ingest.js +1 -1
- package/dist/engine/schema-builder.js +228 -212
- package/dist/engine/screen-helpers.js +1 -1
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
- package/dist/engine/validate-projection-allowlist.js +2 -1
- package/dist/entrypoint/index.d.ts +4 -1
- package/dist/entrypoint/index.js +7 -1
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/es-ops/context.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/event-store.js +10 -4
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +4 -1
- package/dist/files/file-handle.js +2 -3
- package/dist/files/file-routes.js +11 -8
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/upload-ledger.js +7 -0
- package/dist/i18n/required-surface-keys.js +182 -206
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-context.d.ts +32 -0
- package/dist/jobs/job-runner-context.js +305 -0
- package/dist/jobs/job-runner-execution.d.ts +7 -0
- package/dist/jobs/job-runner-execution.js +160 -0
- package/dist/jobs/job-runner-gates.d.ts +19 -0
- package/dist/jobs/job-runner-gates.js +198 -0
- package/dist/jobs/job-runner-helpers.d.ts +17 -3
- package/dist/jobs/job-runner-helpers.js +61 -2
- package/dist/jobs/job-runner-scope.d.ts +44 -0
- package/dist/jobs/job-runner-scope.js +102 -0
- package/dist/jobs/job-runner-types.d.ts +12 -0
- package/dist/jobs/job-runner.d.ts +0 -1
- package/dist/jobs/job-runner.js +47 -657
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +12 -1
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-query.js +10 -3
- package/dist/pipeline/dispatch-shared.d.ts +13 -3
- package/dist/pipeline/dispatch-shared.js +58 -710
- package/dist/pipeline/dispatch-write.js +296 -189
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +3 -5
- package/dist/pipeline/handler-context-auth.d.ts +9 -0
- package/dist/pipeline/handler-context-auth.js +54 -0
- package/dist/pipeline/handler-context-config.d.ts +14 -0
- package/dist/pipeline/handler-context-config.js +113 -0
- package/dist/pipeline/handler-context-db.d.ts +24 -0
- package/dist/pipeline/handler-context-db.js +149 -0
- package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
- package/dist/pipeline/handler-context-dispatch.js +89 -0
- package/dist/pipeline/handler-context-events.d.ts +43 -0
- package/dist/pipeline/handler-context-events.js +237 -0
- package/dist/pipeline/handler-context-notify.d.ts +12 -0
- package/dist/pipeline/handler-context-notify.js +55 -0
- package/dist/pipeline/handler-context-observability.d.ts +7 -0
- package/dist/pipeline/handler-context-observability.js +21 -0
- package/dist/pipeline/handler-context-scope.d.ts +48 -0
- package/dist/pipeline/handler-context-scope.js +88 -0
- package/dist/pipeline/notify-scope.js +12 -1
- package/dist/pipeline/pre-handler-gates.js +20 -4
- package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
- package/dist/pipeline/pre-transaction-reservations.js +9 -3
- package/dist/pipeline/projection-rebuild.js +2 -1
- package/dist/schema-cli/apply-command.d.ts +2 -0
- package/dist/schema-cli/apply-command.js +97 -0
- package/dist/schema-cli/baseline-command.d.ts +2 -0
- package/dist/schema-cli/baseline-command.js +33 -0
- package/dist/schema-cli/generate-command.d.ts +2 -0
- package/dist/schema-cli/generate-command.js +90 -0
- package/dist/schema-cli/shared.d.ts +39 -0
- package/dist/schema-cli/shared.js +95 -0
- package/dist/schema-cli/status-command.d.ts +2 -0
- package/dist/schema-cli/status-command.js +73 -0
- package/dist/schema-cli/validate-boot-input.d.ts +5 -0
- package/dist/schema-cli/validate-boot-input.js +130 -0
- package/dist/schema-cli/validate-command.d.ts +2 -0
- package/dist/schema-cli/validate-command.js +130 -0
- package/dist/schema-cli.d.ts +2 -18
- package/dist/schema-cli.js +47 -604
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/upgrade-cli.js +149 -121
- package/package.json +4 -4
- package/src/changes.json +310 -0
- package/src/scripts/codemod/feature-layout.ts +10 -2
- package/src/scripts/codemod/object-form.ts +41 -4
- package/src/scripts/codemod/package-merge.ts +58 -8
- package/src/scripts/codemod/pii-personal-migration.ts +175 -145
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
|
@@ -77,139 +77,230 @@ function embeddedSubFieldToZod(subField) {
|
|
|
77
77
|
assertUnreachable(subField, "embedded sub-field type");
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
function textFieldToZod(field, { applyDefaults, isUpdate }) {
|
|
81
|
+
let schema = z.string();
|
|
82
|
+
if (field.maxLength)
|
|
83
|
+
schema = schema.max(field.maxLength);
|
|
84
|
+
if (field.minLength) {
|
|
85
|
+
// "" must stay valid for optional fields and writeOnly updates (""
|
|
86
|
+
// = unchanged); `required` rejects it separately via min(1) below.
|
|
87
|
+
const minLength = field.minLength;
|
|
88
|
+
schema = schema.refine((value) => value.length === 0 || value.length >= minLength, {
|
|
89
|
+
message: `at least ${minLength} characters`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (field.writeOnly === true) {
|
|
93
|
+
// Update: "" = unchanged (dropped before the executor), null = clear,
|
|
94
|
+
// unless required. Create: "" would leave a required field unset.
|
|
95
|
+
if (!isUpdate)
|
|
96
|
+
return field.required ? schema.min(1) : schema;
|
|
97
|
+
return field.required ? schema : schema.nullable();
|
|
98
|
+
}
|
|
99
|
+
if (field.format === "email")
|
|
100
|
+
schema = schema.email();
|
|
101
|
+
if (field.format === "url")
|
|
102
|
+
schema = schema.url();
|
|
103
|
+
if (field.format === "iban") {
|
|
104
|
+
// "" stays valid for an optional field; `required` rejects it via min(1) below.
|
|
105
|
+
schema = schema.superRefine((value, ctx) => {
|
|
106
|
+
if (value !== "" && !isValidIban(value)) {
|
|
107
|
+
ctx.addIssue({ code: "invalid_format", format: "iban", input: value });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (field.format === "month") {
|
|
112
|
+
// "" stays valid for an optional field; `required` rejects it via min(1) below.
|
|
113
|
+
schema = schema.refine((value) => value === "" || MONTH_PATTERN.test(value), {
|
|
114
|
+
message: "expected a month as YYYY-MM",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (field.required)
|
|
118
|
+
schema = schema.min(1);
|
|
119
|
+
return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
|
|
120
|
+
}
|
|
121
|
+
function longTextFieldToZod(field, applyDefaults) {
|
|
122
|
+
// longText has no `format` variant (by type design). Only optional
|
|
123
|
+
// maxLength and required are supported; otherwise it is an unrestricted
|
|
124
|
+
// z.string().
|
|
125
|
+
let schema = z.string();
|
|
126
|
+
if (field.maxLength)
|
|
127
|
+
schema = schema.max(field.maxLength);
|
|
128
|
+
if (field.required)
|
|
129
|
+
schema = schema.min(1);
|
|
130
|
+
return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
|
|
131
|
+
}
|
|
132
|
+
function selectFieldToZod(field, applyDefaults) {
|
|
133
|
+
const [first, ...rest] = field.options;
|
|
134
|
+
if (!first)
|
|
135
|
+
return z.string();
|
|
136
|
+
const enumSchema = z.enum([first, ...rest]);
|
|
137
|
+
if (field.default !== undefined) {
|
|
138
|
+
// Untouched <select> sends "" too; with a default that maps to the
|
|
139
|
+
// default (same semantics as undefined) instead of the invalid-value
|
|
140
|
+
// rejection. A field with a default is never "unset" —
|
|
141
|
+
// true on both insert AND update, so this branch (and its "" → default
|
|
142
|
+
// mapping) fires regardless of applyDefaults; only the `.default(...)`
|
|
143
|
+
// schema-level fallback for OMITTED input is update-gated below.
|
|
144
|
+
// `null` maps the same way: the no-default branch below normalizes
|
|
145
|
+
// an untouched select to null, and a client that reuses that value
|
|
146
|
+
// against a defaulted field must not get rejected either.
|
|
147
|
+
const mapped = z.preprocess((value) => (value === "" || value === null ? field.default : value), enumSchema);
|
|
148
|
+
return applyDefaults ? mapped.default(field.default) : mapped;
|
|
149
|
+
}
|
|
150
|
+
if (field.required)
|
|
151
|
+
return enumSchema;
|
|
152
|
+
// Optional select without a default: an untouched HTML <select> submits
|
|
153
|
+
// "" for its placeholder option. Treat that as "unset" (null) instead of
|
|
154
|
+
// an invalid enum value — null (not undefined) so the value survives the
|
|
155
|
+
// JSON-serialized event payload and an update can actually clear a
|
|
156
|
+
// previously-set select back to unset, not just skip validation.
|
|
157
|
+
return z.preprocess((value) => (value === "" ? null : value), enumSchema.nullable());
|
|
158
|
+
}
|
|
159
|
+
function multiSelectFieldToZod(field, applyDefaults) {
|
|
160
|
+
const [first, ...rest] = field.options;
|
|
161
|
+
if (!first)
|
|
162
|
+
return z.array(z.string());
|
|
163
|
+
// `required: true` means non-empty, as with a `text` field.
|
|
164
|
+
// An empty array is rejected; the global `.optional()` wrapping
|
|
165
|
+
// in buildInsertSchema determines whether the field may be omitted.
|
|
166
|
+
let schema = z.array(z.enum([first, ...rest]));
|
|
167
|
+
if (field.required)
|
|
168
|
+
schema = schema.min(1);
|
|
169
|
+
return field.default !== undefined && applyDefaults ? schema.default([...field.default]) : schema;
|
|
170
|
+
}
|
|
171
|
+
function numberFieldToZod(field, applyDefaults) {
|
|
172
|
+
let schema = z.number();
|
|
173
|
+
// `integer: true` maps to a Postgres int4 column (entity-table-meta.ts)
|
|
174
|
+
// — bound it here so an out-of-range write fails loud (400) at the
|
|
175
|
+
// schema boundary instead of dying in Postgres (22003 → 500).
|
|
176
|
+
if (field.integer)
|
|
177
|
+
schema = schema.int().min(-2147483648).max(2147483647);
|
|
178
|
+
if (field.min !== undefined)
|
|
179
|
+
schema = schema.min(field.min);
|
|
180
|
+
if (field.max !== undefined)
|
|
181
|
+
schema = schema.max(field.max);
|
|
182
|
+
return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
|
|
183
|
+
}
|
|
184
|
+
function decimalFieldToZod(field, applyDefaults) {
|
|
185
|
+
// Stored as numeric(precision, scale), surfaced as JS number. Bound the
|
|
186
|
+
// value at the write boundary so an over-range or over-scale input fails
|
|
187
|
+
// loud here instead of being silently rounded/rejected by Postgres.
|
|
188
|
+
const limit = 10 ** (field.precision - field.scale);
|
|
189
|
+
const schema = z
|
|
190
|
+
.number()
|
|
191
|
+
.gt(-limit)
|
|
192
|
+
.lt(limit)
|
|
193
|
+
.refine((n) => isRepresentableAtScale(n, field.scale), {
|
|
194
|
+
message: `at most ${field.scale} decimal places`,
|
|
195
|
+
});
|
|
196
|
+
return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
|
|
197
|
+
}
|
|
198
|
+
function moneyFieldToZod(currencies) {
|
|
199
|
+
const [first, ...rest] = currencies;
|
|
200
|
+
if (!first)
|
|
201
|
+
throw new Error("No currencies configured");
|
|
202
|
+
return z.object({
|
|
203
|
+
amount: z.number(),
|
|
204
|
+
currency: z.enum([first, ...rest]),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function embeddedFieldToZod(field) {
|
|
208
|
+
const shape = {};
|
|
209
|
+
for (const [subName, subField] of Object.entries(field.schema)) {
|
|
210
|
+
const zodSub = embeddedSubFieldToZod(subField);
|
|
211
|
+
shape[subName] = subField.required ? zodSub : zodSub.optional();
|
|
212
|
+
}
|
|
213
|
+
const baseRow = z.object(shape);
|
|
214
|
+
const derived = field.derived;
|
|
215
|
+
// The server is the authority for derived cells: a row is recomputed
|
|
216
|
+
// here, overwriting whatever the client sent, instead of merely
|
|
217
|
+
// being checked against it.
|
|
218
|
+
const row = derived === undefined
|
|
219
|
+
? baseRow
|
|
220
|
+
: z.preprocess((value) => withDerivedCells(value, derived, field.schema), baseRow);
|
|
221
|
+
if (field.multiple !== true)
|
|
222
|
+
return row;
|
|
223
|
+
// `required: true` means non-empty, same reading as multiSelect —
|
|
224
|
+
// whether the key may be omitted at all is decided by buildInsertSchema
|
|
225
|
+
// off the same flag. `minItems` overrides that default when set.
|
|
226
|
+
let list = z.array(row);
|
|
227
|
+
if (field.maxItems !== undefined)
|
|
228
|
+
list = list.max(field.maxItems);
|
|
229
|
+
const min = field.minItems ?? (field.required === true ? 1 : undefined);
|
|
230
|
+
if (min === undefined)
|
|
231
|
+
return list;
|
|
232
|
+
// A custom issue rather than min(): other arrays (multiSelect) share zod's
|
|
233
|
+
// too_small and must not read "rows"; the bridge honours params.i18nKey.
|
|
234
|
+
// The refinement is invisible to z.toJSONSchema; the meta keeps minItems for agent manifests.
|
|
235
|
+
return list.meta({ minItems: min }).superRefine((rows, ctx) => {
|
|
236
|
+
// skip: the minimum is met, nothing to report
|
|
237
|
+
if (rows.length >= min)
|
|
238
|
+
return;
|
|
239
|
+
ctx.addIssue({
|
|
240
|
+
code: "custom",
|
|
241
|
+
input: rows,
|
|
242
|
+
params: {
|
|
243
|
+
i18nKey: `errors.validation.list_min_rows.${min === 1 ? "one" : "other"}`,
|
|
244
|
+
minimum: min,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
function timestampFieldToZod(field) {
|
|
250
|
+
// When locatedBy is set: wall-clock time without an offset (ISO datetime
|
|
251
|
+
// without `Z`). Otherwise: ISO UTC datetime (with `Z`). Both are validated
|
|
252
|
+
// against the ISO-8601 schema through z.iso.datetime; precision (with or
|
|
253
|
+
// without an offset) depends on locatedBy.
|
|
254
|
+
const schema = field.locatedBy !== undefined ? z.iso.datetime({ local: true }) : z.iso.datetime();
|
|
255
|
+
return withDateBounds(schema, field.min, field.max);
|
|
256
|
+
}
|
|
257
|
+
function locatedTimestampToZod() {
|
|
258
|
+
// Combined wall-clock and time-zone object. On write, either
|
|
259
|
+
// { at, tz } (typical for a UI form, with utc calculated) or { utc, tz }
|
|
260
|
+
// (typical for server-to-server communication, with at calculated) is
|
|
261
|
+
// accepted. On read, the read wrapper provides all three fields.
|
|
262
|
+
//
|
|
263
|
+
// The schema guarantees only tz plus at least one of at or utc.
|
|
264
|
+
const at = z.iso.datetime({ local: true });
|
|
265
|
+
const tz = z.string().refine(isValidIanaTimeZone, { message: "invalid IANA time zone" });
|
|
266
|
+
const utc = z.iso.datetime();
|
|
267
|
+
return z.union([
|
|
268
|
+
z.object({ at, tz, utc: utc.optional() }),
|
|
269
|
+
z.object({ utc, tz, at: at.optional() }),
|
|
270
|
+
]);
|
|
271
|
+
}
|
|
272
|
+
function fileListFieldToZod(field) {
|
|
273
|
+
// Multi file: array of fileRef UUIDs. Same story as the singular
|
|
274
|
+
// variant — the element type has to match the UUID column on
|
|
275
|
+
// fileRefsTable.id.
|
|
276
|
+
const ids = z.array(z.uuid());
|
|
277
|
+
return field.maxCount === undefined ? ids : ids.max(field.maxCount);
|
|
278
|
+
}
|
|
81
279
|
export function fieldToZod(field, currencies, opts = {}) {
|
|
82
280
|
// Insert callers want `.default(...)` applied so an omitted field falls
|
|
83
281
|
// back to it; buildUpdateSchema passes applyDefaults: false so an omitted
|
|
84
282
|
// field on update stays omitted (a `{ title }` patch must not clobber
|
|
85
283
|
// other columns with their defaults) while a field's own default value is
|
|
86
|
-
// still known here for "" → default mapping (
|
|
284
|
+
// still known here for "" → default mapping (selectFieldToZod).
|
|
87
285
|
const applyDefaults = opts.applyDefaults ?? true;
|
|
286
|
+
const ctx = { applyDefaults, isUpdate: opts.isUpdate === true };
|
|
88
287
|
switch (field.type) {
|
|
89
|
-
case "text":
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (field.minLength) {
|
|
94
|
-
// "" must stay valid for optional fields and writeOnly updates (""
|
|
95
|
-
// = unchanged); `required` rejects it separately via min(1) below.
|
|
96
|
-
const minLength = field.minLength;
|
|
97
|
-
schema = schema.refine((value) => value.length === 0 || value.length >= minLength, {
|
|
98
|
-
message: `at least ${minLength} characters`,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
if (field.writeOnly === true) {
|
|
102
|
-
// Update: "" = unchanged (dropped before the executor), null = clear,
|
|
103
|
-
// unless required. Create: "" would leave a required field unset.
|
|
104
|
-
if (opts.isUpdate !== true)
|
|
105
|
-
return field.required ? schema.min(1) : schema;
|
|
106
|
-
return field.required ? schema : schema.nullable();
|
|
107
|
-
}
|
|
108
|
-
if (field.format === "email")
|
|
109
|
-
schema = schema.email();
|
|
110
|
-
if (field.format === "url")
|
|
111
|
-
schema = schema.url();
|
|
112
|
-
if (field.format === "iban") {
|
|
113
|
-
// "" stays valid for an optional field; `required` rejects it via min(1) below.
|
|
114
|
-
schema = schema.superRefine((value, ctx) => {
|
|
115
|
-
if (value !== "" && !isValidIban(value)) {
|
|
116
|
-
ctx.addIssue({ code: "invalid_format", format: "iban", input: value });
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
if (field.format === "month") {
|
|
121
|
-
// "" stays valid for an optional field; `required` rejects it via min(1) below.
|
|
122
|
-
schema = schema.refine((value) => value === "" || MONTH_PATTERN.test(value), {
|
|
123
|
-
message: "expected a month as YYYY-MM",
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
if (field.required)
|
|
127
|
-
schema = schema.min(1);
|
|
128
|
-
return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
|
|
129
|
-
}
|
|
130
|
-
case "longText": {
|
|
131
|
-
// longText has no `format` variant (by type design). Only optional
|
|
132
|
-
// maxLength and required are supported; otherwise it is an unrestricted
|
|
133
|
-
// z.string().
|
|
134
|
-
let schema = z.string();
|
|
135
|
-
if (field.maxLength)
|
|
136
|
-
schema = schema.max(field.maxLength);
|
|
137
|
-
if (field.required)
|
|
138
|
-
schema = schema.min(1);
|
|
139
|
-
return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
|
|
140
|
-
}
|
|
288
|
+
case "text":
|
|
289
|
+
return textFieldToZod(field, ctx);
|
|
290
|
+
case "longText":
|
|
291
|
+
return longTextFieldToZod(field, applyDefaults);
|
|
141
292
|
case "boolean": {
|
|
142
293
|
const schema = z.boolean();
|
|
143
294
|
return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
|
|
144
295
|
}
|
|
145
|
-
case "select":
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// rejection. A field with a default is never "unset" —
|
|
154
|
-
// true on both insert AND update, so this branch (and its "" → default
|
|
155
|
-
// mapping) fires regardless of applyDefaults; only the `.default(...)`
|
|
156
|
-
// schema-level fallback for OMITTED input is update-gated below.
|
|
157
|
-
// `null` maps the same way: the no-default branch below normalizes
|
|
158
|
-
// an untouched select to null, and a client that reuses that value
|
|
159
|
-
// against a defaulted field must not get rejected either.
|
|
160
|
-
const mapped = z.preprocess((value) => (value === "" || value === null ? field.default : value), enumSchema);
|
|
161
|
-
return applyDefaults ? mapped.default(field.default) : mapped;
|
|
162
|
-
}
|
|
163
|
-
if (field.required)
|
|
164
|
-
return enumSchema;
|
|
165
|
-
// Optional select without a default: an untouched HTML <select> submits
|
|
166
|
-
// "" for its placeholder option. Treat that as "unset" (null) instead of
|
|
167
|
-
// an invalid enum value — null (not undefined) so the value survives the
|
|
168
|
-
// JSON-serialized event payload and an update can actually clear a
|
|
169
|
-
// previously-set select back to unset, not just skip validation.
|
|
170
|
-
return z.preprocess((value) => (value === "" ? null : value), enumSchema.nullable());
|
|
171
|
-
}
|
|
172
|
-
case "multiSelect": {
|
|
173
|
-
const [first, ...rest] = field.options;
|
|
174
|
-
if (!first)
|
|
175
|
-
return z.array(z.string());
|
|
176
|
-
// `required: true` means non-empty, as with a `text` field.
|
|
177
|
-
// An empty array is rejected; the global `.optional()` wrapping
|
|
178
|
-
// in buildInsertSchema determines whether the field may be omitted.
|
|
179
|
-
let schema = z.array(z.enum([first, ...rest]));
|
|
180
|
-
if (field.required)
|
|
181
|
-
schema = schema.min(1);
|
|
182
|
-
return field.default !== undefined && applyDefaults
|
|
183
|
-
? schema.default([...field.default])
|
|
184
|
-
: schema;
|
|
185
|
-
}
|
|
186
|
-
case "number": {
|
|
187
|
-
let schema = z.number();
|
|
188
|
-
// `integer: true` maps to a Postgres int4 column (entity-table-meta.ts)
|
|
189
|
-
// — bound it here so an out-of-range write fails loud (400) at the
|
|
190
|
-
// schema boundary instead of dying in Postgres (22003 → 500).
|
|
191
|
-
if (field.integer)
|
|
192
|
-
schema = schema.int().min(-2147483648).max(2147483647);
|
|
193
|
-
if (field.min !== undefined)
|
|
194
|
-
schema = schema.min(field.min);
|
|
195
|
-
if (field.max !== undefined)
|
|
196
|
-
schema = schema.max(field.max);
|
|
197
|
-
return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
|
|
198
|
-
}
|
|
199
|
-
case "decimal": {
|
|
200
|
-
// Stored as numeric(precision, scale), surfaced as JS number. Bound the
|
|
201
|
-
// value at the write boundary so an over-range or over-scale input fails
|
|
202
|
-
// loud here instead of being silently rounded/rejected by Postgres.
|
|
203
|
-
const limit = 10 ** (field.precision - field.scale);
|
|
204
|
-
const schema = z
|
|
205
|
-
.number()
|
|
206
|
-
.gt(-limit)
|
|
207
|
-
.lt(limit)
|
|
208
|
-
.refine((n) => isRepresentableAtScale(n, field.scale), {
|
|
209
|
-
message: `at most ${field.scale} decimal places`,
|
|
210
|
-
});
|
|
211
|
-
return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
|
|
212
|
-
}
|
|
296
|
+
case "select":
|
|
297
|
+
return selectFieldToZod(field, applyDefaults);
|
|
298
|
+
case "multiSelect":
|
|
299
|
+
return multiSelectFieldToZod(field, applyDefaults);
|
|
300
|
+
case "number":
|
|
301
|
+
return numberFieldToZod(field, applyDefaults);
|
|
302
|
+
case "decimal":
|
|
303
|
+
return decimalFieldToZod(field, applyDefaults);
|
|
213
304
|
case "bigInt": {
|
|
214
305
|
// JS-`number` round-trip via mode:"number"; exact up to 2^53.
|
|
215
306
|
// The safe-integer cap ensures that a caller passing a float (for example,
|
|
@@ -218,111 +309,36 @@ export function fieldToZod(field, currencies, opts = {}) {
|
|
|
218
309
|
const schema = z.number().int().safe();
|
|
219
310
|
return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
|
|
220
311
|
}
|
|
221
|
-
case "money":
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
amount: z.number(),
|
|
227
|
-
currency: z.enum([first, ...rest]),
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
case "embedded": {
|
|
231
|
-
const shape = {};
|
|
232
|
-
for (const [subName, subField] of Object.entries(field.schema)) {
|
|
233
|
-
const zodSub = embeddedSubFieldToZod(subField);
|
|
234
|
-
shape[subName] = subField.required ? zodSub : zodSub.optional();
|
|
235
|
-
}
|
|
236
|
-
const baseRow = z.object(shape);
|
|
237
|
-
const derived = field.derived;
|
|
238
|
-
// The server is the authority for derived cells: a row is recomputed
|
|
239
|
-
// here, overwriting whatever the client sent, instead of merely
|
|
240
|
-
// being checked against it.
|
|
241
|
-
const row = derived === undefined
|
|
242
|
-
? baseRow
|
|
243
|
-
: z.preprocess((value) => withDerivedCells(value, derived, field.schema), baseRow);
|
|
244
|
-
if (field.multiple !== true)
|
|
245
|
-
return row;
|
|
246
|
-
// `required: true` means non-empty, same reading as multiSelect —
|
|
247
|
-
// whether the key may be omitted at all is decided by buildInsertSchema
|
|
248
|
-
// off the same flag. `minItems` overrides that default when set.
|
|
249
|
-
let list = z.array(row);
|
|
250
|
-
if (field.maxItems !== undefined)
|
|
251
|
-
list = list.max(field.maxItems);
|
|
252
|
-
const min = field.minItems ?? (field.required === true ? 1 : undefined);
|
|
253
|
-
if (min === undefined)
|
|
254
|
-
return list;
|
|
255
|
-
// A custom issue rather than min(): other arrays (multiSelect) share zod's
|
|
256
|
-
// too_small and must not read "rows"; the bridge honours params.i18nKey.
|
|
257
|
-
return list.superRefine((rows, ctx) => {
|
|
258
|
-
// skip: the minimum is met, nothing to report
|
|
259
|
-
if (rows.length >= min)
|
|
260
|
-
return;
|
|
261
|
-
ctx.addIssue({
|
|
262
|
-
code: "custom",
|
|
263
|
-
input: rows,
|
|
264
|
-
params: {
|
|
265
|
-
i18nKey: `errors.validation.list_min_rows.${min === 1 ? "one" : "other"}`,
|
|
266
|
-
minimum: min,
|
|
267
|
-
},
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
case "jsonb": {
|
|
312
|
+
case "money":
|
|
313
|
+
return moneyFieldToZod(currencies);
|
|
314
|
+
case "embedded":
|
|
315
|
+
return embeddedFieldToZod(field);
|
|
316
|
+
case "jsonb":
|
|
272
317
|
// Free-form jsonb — keys are tenant-/runtime-defined. Validation
|
|
273
318
|
// passes through any plain object.
|
|
274
319
|
return z.record(z.string(), z.unknown());
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return
|
|
279
|
-
|
|
280
|
-
case "timestamp": {
|
|
281
|
-
// When locatedBy is set: wall-clock time without an offset (ISO datetime
|
|
282
|
-
// without `Z`). Otherwise: ISO UTC datetime (with `Z`). Both are validated
|
|
283
|
-
// against the ISO-8601 schema through z.iso.datetime; precision (with or
|
|
284
|
-
// without an offset) depends on locatedBy.
|
|
285
|
-
const schema = field.locatedBy !== undefined ? z.iso.datetime({ local: true }) : z.iso.datetime();
|
|
286
|
-
return withDateBounds(schema, field.min, field.max);
|
|
287
|
-
}
|
|
288
|
-
case "tz": {
|
|
320
|
+
case "date":
|
|
321
|
+
return withDateBounds(z.string().date(), field.min, field.max);
|
|
322
|
+
case "timestamp":
|
|
323
|
+
return timestampFieldToZod(field);
|
|
324
|
+
case "tz":
|
|
289
325
|
// IANA time-zone name, validated against the runtime's zone list
|
|
290
326
|
// (isValidIanaTimeZone). An invalid name fails at the write boundary
|
|
291
327
|
// instead of later in ctx.tz.parse / Temporal.
|
|
292
328
|
return z.string().refine(isValidIanaTimeZone, { message: "invalid IANA time zone" });
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
// Combined wall-clock and time-zone object. On write, either
|
|
296
|
-
// { at, tz } (typical for a UI form, with utc calculated) or { utc, tz }
|
|
297
|
-
// (typical for server-to-server communication, with at calculated) is
|
|
298
|
-
// accepted. On read, the read wrapper provides all three fields.
|
|
299
|
-
//
|
|
300
|
-
// The schema guarantees only tz plus at least one of at or utc.
|
|
301
|
-
const at = z.iso.datetime({ local: true });
|
|
302
|
-
const tz = z.string().refine(isValidIanaTimeZone, { message: "invalid IANA time zone" });
|
|
303
|
-
const utc = z.iso.datetime();
|
|
304
|
-
return z.union([
|
|
305
|
-
z.object({ at, tz, utc: utc.optional() }),
|
|
306
|
-
z.object({ utc, tz, at: at.optional() }),
|
|
307
|
-
]);
|
|
308
|
-
}
|
|
329
|
+
case "locatedTimestamp":
|
|
330
|
+
return locatedTimestampToZod();
|
|
309
331
|
case "file":
|
|
310
|
-
case "image":
|
|
332
|
+
case "image":
|
|
311
333
|
// Single file: stores a fileRef UUID — must match fileRefsTable.id
|
|
312
334
|
// (uuid column). Pre-fix this was z.number() from an era when the
|
|
313
335
|
// column was (wrongly) integer; the table-builder fix to uuid needs
|
|
314
336
|
// a matching validation-layer fix here or the CRUD pipeline rejects
|
|
315
337
|
// every valid UUID with "expected number".
|
|
316
338
|
return z.uuid();
|
|
317
|
-
}
|
|
318
339
|
case "files":
|
|
319
|
-
case "images":
|
|
320
|
-
|
|
321
|
-
// variant — the element type has to match the UUID column on
|
|
322
|
-
// fileRefsTable.id.
|
|
323
|
-
const ids = z.array(z.uuid());
|
|
324
|
-
return field.maxCount === undefined ? ids : ids.max(field.maxCount);
|
|
325
|
-
}
|
|
340
|
+
case "images":
|
|
341
|
+
return fileListFieldToZod(field);
|
|
326
342
|
case "reference":
|
|
327
343
|
// Validates the UUID shape. Checking that the reference exists
|
|
328
344
|
// (the row exists in the referenced table and belongs to the tenant) is
|
|
@@ -92,7 +92,7 @@ export function normalizeListColumn(c) {
|
|
|
92
92
|
process.env.NODE_ENV !== "production" &&
|
|
93
93
|
col.renderer !== undefined &&
|
|
94
94
|
typeof col.renderer === "function") {
|
|
95
|
-
log.warn(`normalizeListColumn:
|
|
95
|
+
log.warn(`normalizeListColumn: field "${col.field}" has a function renderer, which JSON.stringify drops. Migrate it to a FormatSpec ({ format: "..." }).`);
|
|
96
96
|
}
|
|
97
97
|
return col;
|
|
98
98
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -11,42 +11,79 @@
|
|
|
11
11
|
// gone it also drops every action, redirect, nav and workspace member that
|
|
12
12
|
// pointed at them. The one reference it does not resolve is a nav `parent`
|
|
13
13
|
// pointing at a missing nav; that is pruned here.
|
|
14
|
+
import { createFallbackLogger, scrubLogText } from "../logging/index.js";
|
|
14
15
|
import { resolveScreenTargetQn } from "./boot-validator/screens.js";
|
|
15
16
|
import { isToggleableFeature } from "./effective-features.js";
|
|
17
|
+
import { isPureGroupingNav } from "./project-app-schema-for-roles.js";
|
|
16
18
|
import { isValidQn, qualifyEntityName } from "./qualified-name.js";
|
|
19
|
+
const schemaLogger = createFallbackLogger("app-schema");
|
|
17
20
|
// Sorted, so the result doubles as a stable cache key. The trial gate is
|
|
18
21
|
// consulted like the dispatcher's feature gate does, but only for toggleable
|
|
19
22
|
// features: a trial can only unlock tier (toggleable) features, so a
|
|
20
|
-
// cascade-disabled always-on feature needs no gate call.
|
|
23
|
+
// cascade-disabled always-on feature needs no gate call. A failing gate counts
|
|
24
|
+
// as "not unlocked" (the handlers answer 403 then anyway), so a gate outage
|
|
25
|
+
// hides the add-on instead of taking the whole schema endpoint down.
|
|
21
26
|
export async function resolveDisabledFeatureNames(registry, effectiveFeatures, tenantId) {
|
|
22
27
|
const enabled = effectiveFeatures(tenantId);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const trialGate = effectiveFeatures.trialGate;
|
|
29
|
+
const disabled = [...registry.features].filter(([featureName]) => !enabled.has(featureName));
|
|
30
|
+
const unlockedByTrial = await Promise.all(disabled.map(async ([featureName, feature]) => {
|
|
31
|
+
if (trialGate === undefined || !isToggleableFeature(feature))
|
|
32
|
+
return false;
|
|
33
|
+
try {
|
|
34
|
+
return await trialGate(tenantId, featureName);
|
|
30
35
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
catch (error) {
|
|
37
|
+
schemaLogger.warn("trial gate failed, treating feature as not unlocked", {
|
|
38
|
+
featureName,
|
|
39
|
+
error: scrubLogText(error instanceof Error ? error.message : String(error)),
|
|
40
|
+
});
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
return disabled
|
|
45
|
+
.filter((_, index) => !unlockedByTrial[index])
|
|
46
|
+
.map(([featureName]) => featureName)
|
|
47
|
+
.sort();
|
|
34
48
|
}
|
|
35
49
|
function navParentQn(featureName, parent) {
|
|
36
50
|
return isValidQn(parent) ? parent : qualifyEntityName(featureName, "nav", parent);
|
|
37
51
|
}
|
|
38
|
-
|
|
52
|
+
function navQnsByParent(features) {
|
|
53
|
+
const childrenByParent = new Map();
|
|
54
|
+
for (const feature of features) {
|
|
55
|
+
for (const nav of feature.navs ?? []) {
|
|
56
|
+
if (nav.parent === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
const parentQn = navParentQn(feature.featureName, nav.parent);
|
|
59
|
+
const children = childrenByParent.get(parentQn) ?? [];
|
|
60
|
+
children.push(qualifyEntityName(feature.featureName, "nav", nav.id));
|
|
61
|
+
childrenByParent.set(parentQn, children);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return childrenByParent;
|
|
65
|
+
}
|
|
66
|
+
// Navs of enabled features whose parent chain ends in a removed nav, plus pure grouping
|
|
67
|
+
// navs whose children are all removed (an empty section the role projection cannot drop,
|
|
68
|
+
// because it only drops groups that still have children).
|
|
39
69
|
function collectOrphanedNavQns(features, removedNavQns) {
|
|
40
70
|
const orphaned = new Set();
|
|
71
|
+
const childrenByParent = navQnsByParent(features);
|
|
41
72
|
let changed = true;
|
|
42
73
|
while (changed) {
|
|
43
74
|
changed = false;
|
|
44
75
|
for (const feature of features) {
|
|
45
76
|
for (const nav of feature.navs ?? []) {
|
|
46
77
|
const qn = qualifyEntityName(feature.featureName, "nav", nav.id);
|
|
47
|
-
if (removedNavQns.has(qn)
|
|
78
|
+
if (removedNavQns.has(qn))
|
|
48
79
|
continue;
|
|
49
|
-
|
|
80
|
+
const parentRemoved = nav.parent !== undefined &&
|
|
81
|
+
removedNavQns.has(navParentQn(feature.featureName, nav.parent));
|
|
82
|
+
const children = childrenByParent.get(qn) ?? [];
|
|
83
|
+
const emptiedGroup = isPureGroupingNav(nav) &&
|
|
84
|
+
children.length > 0 &&
|
|
85
|
+
children.every((childQn) => removedNavQns.has(childQn));
|
|
86
|
+
if (parentRemoved || emptiedGroup) {
|
|
50
87
|
removedNavQns.add(qn);
|
|
51
88
|
orphaned.add(qn);
|
|
52
89
|
changed = true;
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// statically; today it lives in this comment + the StepBuilder doc.
|
|
25
25
|
import { KUMIKO_META_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
26
26
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
27
|
+
import { resolveTableName } from "../db/entity-table-meta.js";
|
|
27
28
|
import { schemaTableName } from "../db/table-introspection.js";
|
|
28
29
|
import { getStep } from "./define-step.js";
|
|
29
30
|
import { buildPipelineSteps } from "./pipeline.js";
|
|
@@ -79,7 +80,7 @@ export function validateProjectionAllowlist(features) {
|
|
|
79
80
|
const aggregateTables = new Map();
|
|
80
81
|
for (const f of features) {
|
|
81
82
|
for (const [entityName, entity] of Object.entries(f.entities ?? {})) {
|
|
82
|
-
const tableName = entity
|
|
83
|
+
const tableName = resolveTableName(entityName, entity, undefined);
|
|
83
84
|
const existing = aggregateTables.get(tableName);
|
|
84
85
|
if (existing && existing !== f.name) {
|
|
85
86
|
throw new Error(`Aggregate-table "${tableName}" is registered by both feature "${existing}" and feature "${f.name}" via r.entity. ` +
|
|
@@ -10,7 +10,7 @@ import type { Lifecycle } from "../lifecycle/index.ts";
|
|
|
10
10
|
import type { ObservabilityProvider } from "../observability/index.ts";
|
|
11
11
|
import type { Dispatcher, DispatcherOptions } from "../pipeline/dispatcher.ts";
|
|
12
12
|
import type { EventDedup, EventDispatcher } from "../pipeline/index.ts";
|
|
13
|
-
import type { SystemHooks } from "../pipeline/lifecycle-pipeline.ts";
|
|
13
|
+
import type { LifecycleHooks, SystemHooks } from "../pipeline/lifecycle-pipeline.ts";
|
|
14
14
|
export type BaseEntrypointOptions = {
|
|
15
15
|
readonly registry: Registry;
|
|
16
16
|
readonly context: AppContext;
|
|
@@ -21,6 +21,7 @@ export type BaseEntrypointOptions = {
|
|
|
21
21
|
readonly systemHooks?: SystemHooks;
|
|
22
22
|
readonly eventDedup?: EventDedup;
|
|
23
23
|
readonly lifecycle?: Lifecycle;
|
|
24
|
+
readonly instanceId?: ServerOptions["instanceId"];
|
|
24
25
|
};
|
|
25
26
|
type JobsBlock = {
|
|
26
27
|
readonly redisUrl: string;
|
|
@@ -49,6 +50,7 @@ export type ApiEntrypointOptions = BaseEntrypointOptions & {
|
|
|
49
50
|
readonly runLocal?: boolean;
|
|
50
51
|
};
|
|
51
52
|
readonly trustedProxyHops?: ServerOptions["trustedProxyHops"];
|
|
53
|
+
readonly errorDocs?: ServerOptions["errorDocs"];
|
|
52
54
|
};
|
|
53
55
|
export type WorkerEntrypointOptions = BaseEntrypointOptions & JobsBlock & {
|
|
54
56
|
readonly eventDispatcher?: ServerOptions["eventDispatcher"];
|
|
@@ -61,6 +63,7 @@ export type ApiEntrypoint = {
|
|
|
61
63
|
readonly lifecycle: Lifecycle;
|
|
62
64
|
readonly observability: ObservabilityProvider;
|
|
63
65
|
readonly dispatcher: Dispatcher;
|
|
66
|
+
readonly lifecycleHooks: LifecycleHooks;
|
|
64
67
|
readonly eventDispatcher?: EventDispatcher;
|
|
65
68
|
readonly mode: "api";
|
|
66
69
|
start(): Promise<void>;
|