@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
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import { computeBlindIndex, configuredBlindIndexKey } from "../crypto/blind-index.js";
|
|
3
|
+
import { quoteIdent } from "../crypto/ciphertext-pattern.js";
|
|
4
|
+
import { prepareJsonbValue, prepareValue } from "./driver-value-preparation.js";
|
|
5
|
+
function isJsonTextMatchList(value) {
|
|
6
|
+
return Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function isWhereOperator(v) {
|
|
9
|
+
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
10
|
+
return false;
|
|
11
|
+
// Don't false-positive on Date / Temporal / other domain-objects.
|
|
12
|
+
// WhereOperator is plain object literal with at most these keys.
|
|
13
|
+
const keys = Object.keys(v);
|
|
14
|
+
if (keys.length === 0)
|
|
15
|
+
return false;
|
|
16
|
+
return keys.every((key) => Object.hasOwn(OPERATOR_EMITTERS, key));
|
|
17
|
+
}
|
|
18
|
+
function isJsonbScalar(value) {
|
|
19
|
+
return value !== null && typeof value !== "object";
|
|
20
|
+
}
|
|
21
|
+
function valueList(target, values) {
|
|
22
|
+
return values
|
|
23
|
+
.map((v) => {
|
|
24
|
+
const p = prepareValue(v, target.pgType);
|
|
25
|
+
return p.kind === "literal" ? p.literal : `${target.bind(p.bound)}${p.sql}`;
|
|
26
|
+
})
|
|
27
|
+
.join(", ");
|
|
28
|
+
}
|
|
29
|
+
// multiSelect (and any other jsonb-array-of-scalars column) stores an
|
|
30
|
+
// array — a filter value is one option, not the whole array, so eq/ne/in
|
|
31
|
+
// must check array containment (`@>`) instead of scalar `=`/`<>`/`IN`
|
|
32
|
+
// against the jsonb column, which Postgres rejects outright.
|
|
33
|
+
// Mirrors applyMultiSelectFilter in event-store-executor-read-filter.ts — keep in lock-step.
|
|
34
|
+
function jsonbContainsAny(target, candidates) {
|
|
35
|
+
const { column, ref, bind } = target;
|
|
36
|
+
if (candidates.length === 0)
|
|
37
|
+
return "FALSE";
|
|
38
|
+
const parts = [];
|
|
39
|
+
for (const v of candidates) {
|
|
40
|
+
const p = prepareJsonbValue([v]);
|
|
41
|
+
// prepareJsonbValue never returns kind:"literal" (only prepareValue's
|
|
42
|
+
// isSqlExpression branch does) — narrow anyway, PreparedValue is a union.
|
|
43
|
+
if (p && p.kind === "param") {
|
|
44
|
+
parts.push(`${ref(column)} @> ${bind(p.bound)}${p.sql}`);
|
|
45
|
+
}
|
|
46
|
+
// A jsonb column holding the scalar itself is never `@>` an array of it.
|
|
47
|
+
if (isJsonbScalar(v)) {
|
|
48
|
+
parts.push(`${ref(column)} = ${bind(JSON.stringify(v))}::text::jsonb`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return parts.length > 0 ? `(${parts.join(" OR ")})` : "FALSE";
|
|
52
|
+
}
|
|
53
|
+
function isValidJsonTextMatch(match) {
|
|
54
|
+
if (match === null || typeof match !== "object")
|
|
55
|
+
return false;
|
|
56
|
+
const keys = propertyOf(match, "keys");
|
|
57
|
+
return (Array.isArray(keys) &&
|
|
58
|
+
keys.length > 0 &&
|
|
59
|
+
keys.every((k) => typeof k === "string") &&
|
|
60
|
+
typeof propertyOf(match, "eq") === "string");
|
|
61
|
+
}
|
|
62
|
+
function jsonTextMatchCondition(target, match) {
|
|
63
|
+
const { column, ref, bind } = target;
|
|
64
|
+
if (!isValidJsonTextMatch(match)) {
|
|
65
|
+
throw new Error(`where: jsonText on column "${column}" needs non-empty string keys and a string eq`);
|
|
66
|
+
}
|
|
67
|
+
const lookups = match.keys.map((key) => `${ref(column)}->>${bind(key)}::text`);
|
|
68
|
+
const lhs = lookups.length === 1 ? lookups[0] : `COALESCE(${lookups.join(", ")})`;
|
|
69
|
+
return `(${lhs} = ${bind(match.eq)})`;
|
|
70
|
+
}
|
|
71
|
+
function emitJsonText(target, operator) {
|
|
72
|
+
const { column, pgType } = target;
|
|
73
|
+
const operand = operator.jsonText;
|
|
74
|
+
if (operand === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
if (Object.keys(operator).length > 1) {
|
|
77
|
+
throw new Error(`where: jsonText on column "${column}" cannot be combined with other operators`);
|
|
78
|
+
}
|
|
79
|
+
if (pgType !== "jsonb") {
|
|
80
|
+
throw new Error(`where: jsonText requires a jsonb column, "${column}" is ${pgType}`);
|
|
81
|
+
}
|
|
82
|
+
const matches = isJsonTextMatchList(operand) ? operand : [operand];
|
|
83
|
+
if (matches.length === 0)
|
|
84
|
+
return "FALSE";
|
|
85
|
+
return matches.map((match) => jsonTextMatchCondition(target, match)).join(" AND ");
|
|
86
|
+
}
|
|
87
|
+
function comparisonCondition(target, symbol, operand) {
|
|
88
|
+
const { column, pgType, ref, bind } = target;
|
|
89
|
+
const p = prepareValue(operand, pgType);
|
|
90
|
+
return p.kind === "literal"
|
|
91
|
+
? `${ref(column)} ${symbol} ${p.literal}`
|
|
92
|
+
: `${ref(column)} ${symbol} ${bind(p.bound)}${p.sql}`;
|
|
93
|
+
}
|
|
94
|
+
function comparisonEmitter(key, symbol) {
|
|
95
|
+
return (target, operator) => {
|
|
96
|
+
const operand = operator[key];
|
|
97
|
+
if (operand === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
// Mirrors applyMultiSelectFilter: ordering/pattern ops have no meaning on a jsonb
|
|
100
|
+
// option array (`like` would crash as `jsonb ~~ jsonb`), so match nothing.
|
|
101
|
+
if (target.pgType === "jsonb")
|
|
102
|
+
return "FALSE";
|
|
103
|
+
return comparisonCondition(target, symbol, operand);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Mirrors applyMultiSelectFilter: an array `ne` means "does not contain all of these".
|
|
107
|
+
function jsonbNotEqualCondition(target, operand) {
|
|
108
|
+
const { column, ref, bind } = target;
|
|
109
|
+
if (Array.isArray(operand)) {
|
|
110
|
+
if (operand.length === 0)
|
|
111
|
+
return "FALSE";
|
|
112
|
+
const p = prepareJsonbValue(operand);
|
|
113
|
+
if (p && p.kind === "param") {
|
|
114
|
+
return `(${ref(column)} IS NULL OR NOT (${ref(column)} @> ${bind(p.bound)}${p.sql}))`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (isJsonbScalar(operand)) {
|
|
118
|
+
const p = prepareJsonbValue([operand]);
|
|
119
|
+
if (p && p.kind === "param") {
|
|
120
|
+
return `(${ref(column)} IS NULL OR NOT (${ref(column)} @> ${bind(p.bound)}${p.sql} OR ${ref(column)} = ${bind(JSON.stringify(operand))}::text::jsonb))`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
function emitNotEqual(target, operator) {
|
|
126
|
+
const operand = operator.ne;
|
|
127
|
+
if (operand === undefined)
|
|
128
|
+
return undefined;
|
|
129
|
+
if (operand === null && Object.keys(operator).length === 1) {
|
|
130
|
+
return `${target.ref(target.column)} IS NOT NULL`;
|
|
131
|
+
}
|
|
132
|
+
if (target.pgType === "jsonb") {
|
|
133
|
+
const jsonbCondition = jsonbNotEqualCondition(target, operand);
|
|
134
|
+
if (jsonbCondition !== undefined)
|
|
135
|
+
return jsonbCondition;
|
|
136
|
+
}
|
|
137
|
+
return comparisonCondition(target, "<>", operand);
|
|
138
|
+
}
|
|
139
|
+
function emitIn(target, operator) {
|
|
140
|
+
const operand = operator.in;
|
|
141
|
+
if (!Array.isArray(operand))
|
|
142
|
+
return undefined;
|
|
143
|
+
if (target.pgType === "jsonb")
|
|
144
|
+
return jsonbContainsAny(target, operand);
|
|
145
|
+
if (operand.length === 0)
|
|
146
|
+
return "FALSE";
|
|
147
|
+
return `${target.ref(target.column)} IN (${valueList(target, operand)})`;
|
|
148
|
+
}
|
|
149
|
+
function emitNotIn(target, operator) {
|
|
150
|
+
const operand = operator.notIn;
|
|
151
|
+
if (!Array.isArray(operand))
|
|
152
|
+
return undefined;
|
|
153
|
+
if (target.pgType === "jsonb") {
|
|
154
|
+
throw new Error(`where: notIn is not supported on jsonb column "${target.column}"`);
|
|
155
|
+
}
|
|
156
|
+
if (operand.length === 0)
|
|
157
|
+
return "TRUE";
|
|
158
|
+
return `${target.ref(target.column)} NOT IN (${valueList(target, operand)})`;
|
|
159
|
+
}
|
|
160
|
+
// Insertion order is the SQL emission order. jsonText comes first so its "cannot be combined" error wins over any other
|
|
161
|
+
// operator's error; the rest keep the historical order gt, gte, lt, lte, ne, like, in, notIn.
|
|
162
|
+
const OPERATOR_EMITTERS = {
|
|
163
|
+
jsonText: emitJsonText,
|
|
164
|
+
gt: comparisonEmitter("gt", ">"),
|
|
165
|
+
gte: comparisonEmitter("gte", ">="),
|
|
166
|
+
lt: comparisonEmitter("lt", "<"),
|
|
167
|
+
lte: comparisonEmitter("lte", "<="),
|
|
168
|
+
ne: emitNotEqual,
|
|
169
|
+
like: comparisonEmitter("like", "LIKE"),
|
|
170
|
+
in: emitIn,
|
|
171
|
+
notIn: emitNotIn,
|
|
172
|
+
};
|
|
173
|
+
function operatorConditions(target, operator) {
|
|
174
|
+
const conditions = [];
|
|
175
|
+
for (const emit of Object.values(OPERATOR_EMITTERS)) {
|
|
176
|
+
const condition = emit(target, operator);
|
|
177
|
+
if (condition !== undefined)
|
|
178
|
+
conditions.push(condition);
|
|
179
|
+
}
|
|
180
|
+
return conditions;
|
|
181
|
+
}
|
|
182
|
+
// Blind-index OR rewrite: if the column has a bidx
|
|
183
|
+
// counterpart (suffix convention `<col>_bidx`, framework-reserved)
|
|
184
|
+
// and a key is configured, equality matches both arms —
|
|
185
|
+
// plaintext legacy rows via the plaintext arm, encrypted rows
|
|
186
|
+
// via the HMAC. The `kumiko-pii:` prefix on ciphertexts makes
|
|
187
|
+
// a random match on the plaintext arm practically impossible.
|
|
188
|
+
function equalityCondition(target, value) {
|
|
189
|
+
const { info, column, pgType, ref, bind } = target;
|
|
190
|
+
const p = prepareValue(value, pgType);
|
|
191
|
+
if (p.kind === "literal")
|
|
192
|
+
return `${ref(column)} = ${p.literal}`;
|
|
193
|
+
const bidxKey = configuredBlindIndexKey();
|
|
194
|
+
const bidxCol = `${column}_bidx`;
|
|
195
|
+
if (bidxKey !== undefined &&
|
|
196
|
+
typeof value === "string" &&
|
|
197
|
+
!column.endsWith("_bidx") &&
|
|
198
|
+
info.hasColumn(bidxCol)) {
|
|
199
|
+
return `(${ref(column)} = ${bind(p.bound)} OR ${ref(bidxCol)} = ${bind(computeBlindIndex(bidxKey, value))})`;
|
|
200
|
+
}
|
|
201
|
+
return `${ref(column)} = ${bind(p.bound)}${p.sql}`;
|
|
202
|
+
}
|
|
203
|
+
function arrayCondition(target, values) {
|
|
204
|
+
if (values.length === 0)
|
|
205
|
+
return "FALSE";
|
|
206
|
+
if (target.pgType === "jsonb")
|
|
207
|
+
return jsonbContainsAny(target, values);
|
|
208
|
+
return `${target.ref(target.column)} IN (${valueList(target, values)})`;
|
|
209
|
+
}
|
|
210
|
+
function conditionsForValue(target, value) {
|
|
211
|
+
if (value === null)
|
|
212
|
+
return [`${target.ref(target.column)} IS NULL`];
|
|
213
|
+
if (Array.isArray(value))
|
|
214
|
+
return [arrayCondition(target, value)];
|
|
215
|
+
if (isWhereOperator(value))
|
|
216
|
+
return operatorConditions(target, value);
|
|
217
|
+
if (target.pgType === "jsonb" && isJsonbScalar(value)) {
|
|
218
|
+
return [jsonbContainsAny(target, [value])];
|
|
219
|
+
}
|
|
220
|
+
return [equalityCondition(target, value)];
|
|
221
|
+
}
|
|
222
|
+
export function buildWhereClause(info, where, startIndex, qualifier) {
|
|
223
|
+
const values = [];
|
|
224
|
+
// Single place that renders a column reference, so a join qualifier reaches
|
|
225
|
+
// every arm (blind index, jsonb containment, IS NULL, jsonText) alike.
|
|
226
|
+
const ref = (column) => qualifier === undefined ? quoteIdent(column) : `${quoteIdent(qualifier)}.${quoteIdent(column)}`;
|
|
227
|
+
const bind = (value) => {
|
|
228
|
+
values.push(value);
|
|
229
|
+
return `$${startIndex + values.length - 1}`;
|
|
230
|
+
};
|
|
231
|
+
const conditions = Object.entries(where).flatMap(([field, value]) => {
|
|
232
|
+
const column = info.columnOf(field);
|
|
233
|
+
return conditionsForValue({ info, column, pgType: info.pgTypeOf(column), ref, bind }, value);
|
|
234
|
+
});
|
|
235
|
+
return { sqlText: conditions.join(" AND "), values };
|
|
236
|
+
}
|
|
@@ -56,6 +56,7 @@ export async function deleteStoredFileAndDerivatives(originalKey, store, errorCo
|
|
|
56
56
|
...(await listDerivativeKeys(originalKey, store, errorContext)),
|
|
57
57
|
];
|
|
58
58
|
const failedKeys = [];
|
|
59
|
+
const log = createFallbackLogger(errorContext);
|
|
59
60
|
for (const key of keysToDelete) {
|
|
60
61
|
try {
|
|
61
62
|
assertSafeStorageKey(key);
|
|
@@ -63,7 +64,7 @@ export async function deleteStoredFileAndDerivatives(originalKey, store, errorCo
|
|
|
63
64
|
}
|
|
64
65
|
catch (err) {
|
|
65
66
|
failedKeys.push(key);
|
|
66
|
-
|
|
67
|
+
log.warn("storage delete failed", {
|
|
67
68
|
key,
|
|
68
69
|
error: err instanceof Error ? err.message : String(err),
|
|
69
70
|
});
|
|
@@ -13,7 +13,7 @@ export function validateFeatureBootChecks(features) {
|
|
|
13
13
|
}
|
|
14
14
|
catch (err) {
|
|
15
15
|
const message = err instanceof Error ? err.message : String(err);
|
|
16
|
-
throw new Error(`[Feature ${feature.name}] r.bootCheck failed: ${message}
|
|
16
|
+
throw new Error(`[Feature ${feature.name}] r.bootCheck failed: ${message}`);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { qualifyEntityName } from "../qualified-name.js";
|
|
2
|
-
import { findEntityFeature } from "./screens.js";
|
|
2
|
+
import { findEntityFeature } from "./screens-kind-shared.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 (in screens.ts)
|
|
5
|
+
// module's own uniqueness check AND rowAction entity-targets (in screens-actions.ts)
|
|
6
6
|
// resolve against the same map instead of re-walking every feature's
|
|
7
7
|
// screens twice.
|
|
8
8
|
export function collectDetailForScreens(features, featureMap) {
|
|
@@ -9,6 +9,7 @@ export declare function validateHandlerAccess(feature: FeatureDefinition): void;
|
|
|
9
9
|
export declare function validateAdditionalRateLimits(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
|
|
10
10
|
export declare function validatePerCommandTransaction(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
|
|
11
11
|
export declare function validateTenantlessAnonymous(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
|
|
12
|
+
export declare function validateRateLimitCost(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
|
|
12
13
|
export declare function validateRateLimitDisabledReason(featureName: string, kind: "write" | "query" | "stream", handlerName: string, rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"]): void;
|
|
13
14
|
export declare function validateRateLimitBucketList(featureName: string, kind: "write" | "query" | "stream", handlerName: string, rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"]): void;
|
|
14
15
|
export declare function validateNoAllRoleInHandlerAccess(featureName: string, kind: "write" | "query" | "stream", handlerName: string, access: NonNullable<FeatureDefinition["writeHandlers"][string]["access"]>): void;
|
|
@@ -139,6 +139,7 @@ export function validateHandlerAccess(feature) {
|
|
|
139
139
|
for (const [name, handler] of Object.entries(feature.writeHandlers)) {
|
|
140
140
|
validateTenantlessAnonymous(feature.name, name, handler);
|
|
141
141
|
validateAdditionalRateLimits(feature.name, name, handler);
|
|
142
|
+
validateRateLimitCost(feature.name, name, handler);
|
|
142
143
|
validatePerCommandTransaction(feature.name, name, handler);
|
|
143
144
|
}
|
|
144
145
|
}
|
|
@@ -235,6 +236,22 @@ export function validateTenantlessAnonymous(featureName, handlerName, handler) {
|
|
|
235
236
|
`tenant/user bucket, so one caller could exhaust it for all. Use ${IP_SCOPED_RATE_LIMIT_PER.map((allowed) => `"${allowed}"`).join(" or ")}.`);
|
|
236
237
|
}
|
|
237
238
|
}
|
|
239
|
+
export function validateRateLimitCost(featureName, handlerName, handler) {
|
|
240
|
+
// skip: no payload-derived cost declared
|
|
241
|
+
if (!handler.rateLimitCost)
|
|
242
|
+
return;
|
|
243
|
+
if (handler.rateLimit === undefined || isRateLimitDisabled(handler.rateLimit)) {
|
|
244
|
+
throw new Error(`write handler "${featureName}:write:${handlerName}" declares rateLimitCost without an active rateLimit — ` +
|
|
245
|
+
`the cost is booked on the rateLimit buckets, so declare rateLimit or drop rateLimitCost.`);
|
|
246
|
+
}
|
|
247
|
+
const isAnonymous = "roles" in handler.access && handler.access.roles.includes("anonymous");
|
|
248
|
+
const hasIpBucket = declaredRateLimitOptions(handler.rateLimit).some(({ per }) => IP_SCOPED_RATE_LIMIT_PER.includes(per));
|
|
249
|
+
if (isAnonymous || hasIpBucket) {
|
|
250
|
+
throw new Error(`write handler "${featureName}:write:${handlerName}" declares rateLimitCost on an anonymous or ip-limited handler — ` +
|
|
251
|
+
`rateLimitCost books after the parse, so such a handler would lose its pre-parse protection against junk floods. ` +
|
|
252
|
+
`Bulk handlers are authenticated: use a user/tenant bucket and drop "anonymous", or drop rateLimitCost.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
238
255
|
export function validateRateLimitDisabledReason(featureName, kind, handlerName, rateLimit) {
|
|
239
256
|
// skip: rate limit isn't disabled, so no reason is required
|
|
240
257
|
if (!isRateLimitDisabled(rateLimit))
|
|
@@ -3,15 +3,46 @@ import { parseQn } from "../qualified-name.js";
|
|
|
3
3
|
import { formDerivationsOfSchema } from "../with-form-derivations.js";
|
|
4
4
|
const NUMERIC_FORM_FIELD_TYPES = ["money", "number", "decimal"];
|
|
5
5
|
const NUMERIC_SUB_FIELD_TYPES = ["money", "number", "decimal"];
|
|
6
|
+
function isMoneyType(type) {
|
|
7
|
+
return type === "money";
|
|
8
|
+
}
|
|
9
|
+
// Money operands are computed in minor units, number/decimal in major units: mixing them
|
|
10
|
+
// silently scales the result by 100 or 10000.
|
|
11
|
+
function validateUnitCompatibility({ prefix, screen, target }, derivation) {
|
|
12
|
+
const targetIsMoney = isMoneyType(screen.fields[target]?.type);
|
|
13
|
+
const moneyFlags = derivation.from.map((source) => isMoneyType(screen.fields[source]?.type));
|
|
14
|
+
const moneyCount = moneyFlags.filter(Boolean).length;
|
|
15
|
+
if (derivation.op === "sum" || derivation.op === "subtract") {
|
|
16
|
+
if (moneyCount !== 0 && moneyCount !== moneyFlags.length) {
|
|
17
|
+
throw new Error(`${prefix}.${target}: "${derivation.op}" sources must be all money or all number/decimal.`);
|
|
18
|
+
}
|
|
19
|
+
if (moneyCount > 0 !== targetIsMoney) {
|
|
20
|
+
throw new Error(`${prefix}.${target}: a "${derivation.op}" over ${moneyCount > 0 ? "money" : "number/decimal"} sources needs a ${moneyCount > 0 ? "money" : "number/decimal"} target.`);
|
|
21
|
+
}
|
|
22
|
+
// skip: sum/subtract fully validated above.
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (moneyCount > 1 || (derivation.op === "percent" && moneyFlags[1] === true)) {
|
|
26
|
+
throw new Error(`${prefix}.${target}: "${derivation.op}" takes at most one money source${derivation.op === "percent" ? " (the base, not the rate)" : ""}.`);
|
|
27
|
+
}
|
|
28
|
+
if ((moneyCount === 1) !== targetIsMoney) {
|
|
29
|
+
throw new Error(`${prefix}.${target}: the target must be money exactly when a "${derivation.op}" source is money.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
6
32
|
function validateFromList({ prefix, screen, target }, derivation) {
|
|
7
33
|
if (derivation.op === "percent" && derivation.from.length !== 2) {
|
|
8
34
|
throw new Error(`${prefix}.${target}: "percent" needs exactly two sources (base, rate).`);
|
|
9
35
|
}
|
|
10
36
|
for (const source of derivation.from) {
|
|
11
|
-
|
|
37
|
+
const sourceField = screen.fields[source];
|
|
38
|
+
if (sourceField === undefined) {
|
|
12
39
|
throw new Error(`${prefix}.${target}: unknown source field "${source}". Name a field of this screen.`);
|
|
13
40
|
}
|
|
41
|
+
if (!NUMERIC_FORM_FIELD_TYPES.includes(sourceField.type)) {
|
|
42
|
+
throw new Error(`${prefix}.${target}: source "${source}" must be money, number or decimal, not "${sourceField.type}".`);
|
|
43
|
+
}
|
|
14
44
|
}
|
|
45
|
+
validateUnitCompatibility({ prefix, screen, target }, derivation);
|
|
15
46
|
}
|
|
16
47
|
function validateListSum(check, derivation) {
|
|
17
48
|
const { prefix, screen, target } = check;
|
|
@@ -26,6 +57,9 @@ function validateListSum(check, derivation) {
|
|
|
26
57
|
if (cellType === undefined || !NUMERIC_SUB_FIELD_TYPES.includes(cellType)) {
|
|
27
58
|
throw new Error(`${prefix}.${target}: "${derivation.cell}" must be a money, number or decimal sub-field of "${derivation.list}".`);
|
|
28
59
|
}
|
|
60
|
+
if (isMoneyType(cellType) !== isMoneyType(screen.fields[target]?.type)) {
|
|
61
|
+
throw new Error(`${prefix}.${target}: a listSum of a ${isMoneyType(cellType) ? "money" : "number/decimal"} cell needs a ${isMoneyType(cellType) ? "money" : "number/decimal"} target.`);
|
|
62
|
+
}
|
|
29
63
|
const { whenEmpty } = derivation;
|
|
30
64
|
// skip: whenEmpty is optional
|
|
31
65
|
if (whenEmpty === undefined)
|
|
@@ -94,9 +94,11 @@ export function validateTranslationLocaleConsistency(features) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
export function validateRequiredLocales(features, requiredLocales) {
|
|
97
|
-
const gaps =
|
|
98
|
-
|
|
97
|
+
const gaps = requiredLocales === undefined || requiredLocales.length === 0
|
|
98
|
+
? []
|
|
99
|
+
: findRequiredLocaleGaps(features, requiredLocales);
|
|
100
|
+
if (requiredLocales !== undefined && gaps.length > 0) {
|
|
99
101
|
const lines = gaps.map((gap) => `[i18n] Feature "${gap.featureName}": translation key "${gap.key}" is missing required locale(s) ${gap.missingLocales.join(", ")}.`);
|
|
100
|
-
throw new Error(`${lines.join("\n")}\nThe app requires locales [${
|
|
102
|
+
throw new Error(`${lines.join("\n")}\nThe app requires locales [${requiredLocales.join(", ")}] for every translation key; load the matching locale package or add the missing translations.`);
|
|
101
103
|
}
|
|
102
104
|
}
|
|
@@ -2,6 +2,7 @@ import { validateEntityFieldEncryptionAvailable } from "../../db/entity-field-en
|
|
|
2
2
|
import { dedupeFeatures } from "../dedupe-features.js";
|
|
3
3
|
import { FILE_STORAGE_PROVIDER_ENV } from "../extension-names.js";
|
|
4
4
|
import { QnTypes, qualifyEntityName } from "../qualified-name.js";
|
|
5
|
+
import { validateProjectionAllowlist } from "../validate-projection-allowlist.js";
|
|
5
6
|
import { validateAccessDeclarations } from "./access-declarations.js";
|
|
6
7
|
import { warnOnUniqueAccessRoles } from "./access-roles.js";
|
|
7
8
|
import { validateActionWiring, validateFieldWiring } from "./action-wiring.js";
|
|
@@ -238,6 +239,7 @@ export function validateBoot(rawFeatures, options) {
|
|
|
238
239
|
validateGdprStoragePersistence(features);
|
|
239
240
|
validateGlobalTenancyEntities(features);
|
|
240
241
|
validateFeatureBootChecks(features);
|
|
242
|
+
validateProjectionAllowlist(features);
|
|
241
243
|
if (hasEncryptedFields) {
|
|
242
244
|
// Availability check, not env-presence: eagerly building the keyring
|
|
243
245
|
// catches malformed keys (wrong length, bad base64) at boot instead of
|