@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
|
@@ -37,7 +37,12 @@ export async function seedReferenceData(defs, registry, db) {
|
|
|
37
37
|
const keyValue = row[upsertKey];
|
|
38
38
|
if (keyValue === undefined)
|
|
39
39
|
continue;
|
|
40
|
-
|
|
40
|
+
// Soft-deleted rows are invisible to executor.update, so treat them as missing and re-create.
|
|
41
|
+
const existing = await fetchOne(db, table, {
|
|
42
|
+
[upsertKey]: keyValue,
|
|
43
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
44
|
+
...(entity.softDelete && { isDeleted: false }),
|
|
45
|
+
});
|
|
41
46
|
if (existing) {
|
|
42
47
|
const changes = {};
|
|
43
48
|
for (const [field, value] of Object.entries(row)) {
|
package/dist/db/table-builder.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { KUMIKO_META_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
2
2
|
import { toSnakeCase } from "../utils/case.js";
|
|
3
|
-
import { assertUnreachable } from "../utils/index.js";
|
|
4
3
|
import { markExecutorManagedTable } from "./executor-managed-tables.js";
|
|
5
4
|
import { columnHandleName } from "./table-introspection.js";
|
|
6
5
|
export { toSnakeCase } from "../utils/case.js";
|
|
@@ -13,172 +12,120 @@ export const SOFT_DELETE_LIVE_ROW_PREDICATE = `"is_deleted" = false`;
|
|
|
13
12
|
export function physicalColumnName(table, field) {
|
|
14
13
|
return columnHandleName(table, field) ?? toSnakeCase(field);
|
|
15
14
|
}
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// longText currently has no default field in its type, but checking
|
|
36
|
-
// `field.default !== undefined` is defensive.
|
|
37
|
-
const base = text(snakeName);
|
|
38
|
-
const withDefault = "default" in field && field.default !== undefined ? base.default(field.default) : base;
|
|
39
|
-
const mainCol = { [name]: field.required ? withDefault.notNull() : withDefault };
|
|
40
|
-
// lookupable → nullable HMAC blind-index column (kept in lockstep with
|
|
41
|
-
// fieldToColumnMeta in entity-table-meta.ts).
|
|
42
|
-
if (field.type === "text" && field.lookupable === true) {
|
|
43
|
-
return { ...mainCol, [`${name}Bidx`]: text(`${snakeName}_bidx`) };
|
|
44
|
-
}
|
|
45
|
-
return mainCol;
|
|
46
|
-
}
|
|
47
|
-
case "boolean":
|
|
48
|
-
return {
|
|
49
|
-
[name]: field.default !== undefined
|
|
50
|
-
? boolean(snakeName).default(field.default).notNull()
|
|
51
|
-
: field.required
|
|
52
|
-
? boolean(snakeName).notNull()
|
|
53
|
-
: boolean(snakeName),
|
|
54
|
-
};
|
|
55
|
-
case "select": {
|
|
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.
|
|
60
|
-
const base = text(snakeName);
|
|
61
|
-
const col = field.default !== undefined ? base.default(field.default) : base;
|
|
62
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
63
|
-
}
|
|
64
|
-
case "multiSelect":
|
|
65
|
-
// jsonb string array with default `[]`, always NOT NULL.
|
|
66
|
-
//
|
|
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
|
|
73
|
-
// multi-select.
|
|
74
|
-
return { [name]: jsonb(snakeName).default([]).notNull() };
|
|
75
|
-
case "number": {
|
|
76
|
-
const base = field.integer === true ? integer(snakeName) : doublePrecision(snakeName);
|
|
77
|
-
const col = field.default !== undefined ? base.default(field.default) : base;
|
|
78
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
79
|
-
}
|
|
80
|
-
case "decimal": {
|
|
81
|
-
const base = decimalColumn(snakeName, field.precision, field.scale);
|
|
82
|
-
const col = field.default !== undefined ? base.default(field.default) : base;
|
|
83
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
84
|
-
}
|
|
85
|
-
case "bigInt": {
|
|
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.
|
|
90
|
-
const base = bigint(snakeName, { mode: "number" });
|
|
91
|
-
const col = field.default !== undefined ? base.default(field.default) : base;
|
|
92
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
93
|
-
}
|
|
94
|
-
case "reference":
|
|
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).
|
|
98
|
-
if (field.multiple === true) {
|
|
99
|
-
return { [name]: jsonb(snakeName).default([]).notNull() };
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
[name]: field.required ? uuid(snakeName).notNull() : uuid(snakeName),
|
|
103
|
-
};
|
|
104
|
-
case "money":
|
|
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().
|
|
111
|
-
return {
|
|
112
|
-
[name]: field.required ? moneyAmount(snakeName).notNull() : moneyAmount(snakeName),
|
|
113
|
-
[`${name}Currency`]: text(`${snakeName}_currency`)
|
|
114
|
-
.default(entity.defaultCurrency ?? "EUR")
|
|
115
|
-
.notNull(),
|
|
116
|
-
};
|
|
117
|
-
case "embedded":
|
|
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.
|
|
123
|
-
// multiple → the value is a list of rows, so the empty default is `[]`.
|
|
124
|
-
return field.multiple === true
|
|
125
|
-
? { [name]: jsonb(snakeName).default([]).notNull() }
|
|
126
|
-
: { [name]: jsonb(snakeName).default({}).notNull() };
|
|
127
|
-
case "jsonb":
|
|
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.
|
|
131
|
-
return { [name]: jsonb(snakeName).default({}).notNull() };
|
|
132
|
-
case "date": {
|
|
133
|
-
// Real PG `date` — no time-of-day, no timezone. Temporal.PlainDate
|
|
134
|
-
// end-to-end (see plainDate() in dialect.ts). Was aliased on instant()
|
|
135
|
-
// = TIMESTAMPTZ (TZ-dependent round-trip).
|
|
136
|
-
const col = plainDate(snakeName);
|
|
137
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
138
|
-
}
|
|
139
|
-
case "timestamp": {
|
|
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.
|
|
145
|
-
const col = instant(snakeName);
|
|
146
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
147
|
-
}
|
|
148
|
-
case "tz": {
|
|
149
|
-
// IANA time-zone name as TEXT — validated through the Zod schema in the
|
|
150
|
-
// validator step. Snake-case convention: `pickup_tz`.
|
|
151
|
-
const col = text(snakeName);
|
|
152
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
153
|
-
}
|
|
154
|
-
case "locatedTimestamp": {
|
|
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.
|
|
159
|
-
const utc = instant(`${snakeName}_utc`);
|
|
160
|
-
const tz = text(`${snakeName}_tz`);
|
|
161
|
-
return {
|
|
162
|
-
[`${name}Utc`]: field.required ? utc.notNull() : utc,
|
|
163
|
-
[`${name}Tz`]: field.required ? tz.notNull() : tz,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
case "file":
|
|
167
|
-
case "image": {
|
|
168
|
-
// Single file: stores fileRefId as UUID — must match fileRefsTable.id
|
|
169
|
-
// (uuid column). Anything narrower (integer, text length-limited) would
|
|
170
|
-
// silently truncate or type-coerce at INSERT time and the FK reference
|
|
171
|
-
// would be unusable.
|
|
172
|
-
const col = uuid(snakeName);
|
|
173
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
174
|
-
}
|
|
175
|
-
case "files":
|
|
176
|
-
case "images":
|
|
177
|
-
// Ordered fileRef UUID array, same shape as multiSelect: never null.
|
|
178
|
-
return { [name]: jsonb(snakeName).default([]).notNull() };
|
|
179
|
-
default:
|
|
180
|
-
assertUnreachable(field, "field type");
|
|
15
|
+
// `required: true` maps to NOT NULL for every field type, so r.entity stays the
|
|
16
|
+
// single source of truth and hand-written tables never add .notNull() on top.
|
|
17
|
+
function notNullWhenRequired(column, required) {
|
|
18
|
+
return required ? column.notNull() : column;
|
|
19
|
+
}
|
|
20
|
+
// Without a DEFAULT clause ALTER TABLE ADD COLUMN breaks on existing rows, and
|
|
21
|
+
// entity-table-meta renders declared defaults too, so both builders stay in lockstep.
|
|
22
|
+
function withDeclaredDefault(column, value) {
|
|
23
|
+
return value !== undefined ? column.default(value) : column;
|
|
24
|
+
}
|
|
25
|
+
// longText differs from text only in the type layer (no sortable/searchable/filterable).
|
|
26
|
+
function textColumns(name, snakeName, field) {
|
|
27
|
+
const declaredDefault = "default" in field ? field.default : undefined;
|
|
28
|
+
const mainCol = {
|
|
29
|
+
[name]: notNullWhenRequired(withDeclaredDefault(text(snakeName), declaredDefault), field.required),
|
|
30
|
+
};
|
|
31
|
+
// Nullable HMAC blind-index column, in lockstep with fieldToColumnMeta in entity-table-meta.ts.
|
|
32
|
+
if (field.type === "text" && field.lookupable === true) {
|
|
33
|
+
return { ...mainCol, [`${name}Bidx`]: text(`${snakeName}_bidx`) };
|
|
181
34
|
}
|
|
35
|
+
return mainCol;
|
|
36
|
+
}
|
|
37
|
+
function booleanColumns(name, snakeName, field) {
|
|
38
|
+
if (field.default !== undefined) {
|
|
39
|
+
return { [name]: boolean(snakeName).default(field.default).notNull() };
|
|
40
|
+
}
|
|
41
|
+
return { [name]: notNullWhenRequired(boolean(snakeName), field.required) };
|
|
42
|
+
}
|
|
43
|
+
function numberColumns(name, snakeName, field) {
|
|
44
|
+
const base = field.integer === true ? integer(snakeName) : doublePrecision(snakeName);
|
|
45
|
+
return { [name]: notNullWhenRequired(withDeclaredDefault(base, field.default), field.required) };
|
|
46
|
+
}
|
|
47
|
+
function referenceColumns(name, snakeName, field) {
|
|
48
|
+
if (field.multiple === true)
|
|
49
|
+
return { [name]: emptyJsonbArray(snakeName) };
|
|
50
|
+
return { [name]: notNullWhenRequired(uuid(snakeName), field.required) };
|
|
51
|
+
}
|
|
52
|
+
// BIGINT minor units: INTEGER would cap at about 21 M EUR. The currency column
|
|
53
|
+
// always has a default, so it is structurally NOT NULL.
|
|
54
|
+
function moneyColumns(name, snakeName, field, entity) {
|
|
55
|
+
return {
|
|
56
|
+
[name]: notNullWhenRequired(moneyAmount(snakeName), field.required),
|
|
57
|
+
[`${name}Currency`]: text(`${snakeName}_currency`)
|
|
58
|
+
.default(entity.defaultCurrency ?? "EUR")
|
|
59
|
+
.notNull(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// <name>_utc + <name>_tz form an atomic pair the executor converts on write and
|
|
63
|
+
// read, so `required` propagates to both columns.
|
|
64
|
+
function locatedTimestampColumns(name, snakeName, field) {
|
|
65
|
+
return {
|
|
66
|
+
[`${name}Utc`]: notNullWhenRequired(instant(`${snakeName}_utc`), field.required),
|
|
67
|
+
[`${name}Tz`]: notNullWhenRequired(text(`${snakeName}_tz`), field.required),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// The `[]` default makes the column structurally never null, so `required` is
|
|
71
|
+
// intentionally ignored and read-side code needs no null check.
|
|
72
|
+
function emptyJsonbArray(snakeName) {
|
|
73
|
+
return jsonb(snakeName).default([]).notNull();
|
|
74
|
+
}
|
|
75
|
+
// Same reasoning as emptyJsonbArray; optional embedded data needs a wrapper field.
|
|
76
|
+
function emptyJsonbObject(snakeName) {
|
|
77
|
+
return jsonb(snakeName).default({}).notNull();
|
|
78
|
+
}
|
|
79
|
+
function singleColumnNotNullWhenRequired(column) {
|
|
80
|
+
return (name, snakeName, field) => ({
|
|
81
|
+
[name]: notNullWhenRequired(column(snakeName), field.required),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Each entry has a type-level twin in ColumnsForField and a meta-level twin in
|
|
85
|
+
// fieldToColumnMeta (entity-table-meta.ts).
|
|
86
|
+
const COLUMN_EMITTERS = {
|
|
87
|
+
text: textColumns,
|
|
88
|
+
longText: textColumns,
|
|
89
|
+
boolean: booleanColumns,
|
|
90
|
+
select: (name, snakeName, field) => ({
|
|
91
|
+
[name]: notNullWhenRequired(withDeclaredDefault(text(snakeName), field.default), field.required),
|
|
92
|
+
}),
|
|
93
|
+
multiSelect: (name, snakeName) => ({ [name]: emptyJsonbArray(snakeName) }),
|
|
94
|
+
number: numberColumns,
|
|
95
|
+
decimal: (name, snakeName, field) => ({
|
|
96
|
+
[name]: notNullWhenRequired(withDeclaredDefault(decimalColumn(snakeName, field.precision, field.scale), field.default), field.required),
|
|
97
|
+
}),
|
|
98
|
+
// mode "number" keeps the value JSON-serializable (safe up to 2^53).
|
|
99
|
+
bigInt: (name, snakeName, field) => ({
|
|
100
|
+
[name]: notNullWhenRequired(withDeclaredDefault(bigint(snakeName, { mode: "number" }), field.default), field.required),
|
|
101
|
+
}),
|
|
102
|
+
reference: referenceColumns,
|
|
103
|
+
money: moneyColumns,
|
|
104
|
+
embedded: (name, snakeName, field) => ({
|
|
105
|
+
[name]: field.multiple === true ? emptyJsonbArray(snakeName) : emptyJsonbObject(snakeName),
|
|
106
|
+
}),
|
|
107
|
+
jsonb: (name, snakeName) => ({ [name]: emptyJsonbObject(snakeName) }),
|
|
108
|
+
date: singleColumnNotNullWhenRequired(plainDate),
|
|
109
|
+
timestamp: singleColumnNotNullWhenRequired(instant),
|
|
110
|
+
tz: singleColumnNotNullWhenRequired(text),
|
|
111
|
+
locatedTimestamp: locatedTimestampColumns,
|
|
112
|
+
// Must match fileRefsTable.id (uuid); anything narrower breaks the reference.
|
|
113
|
+
file: singleColumnNotNullWhenRequired(uuid),
|
|
114
|
+
image: singleColumnNotNullWhenRequired(uuid),
|
|
115
|
+
files: (name, snakeName) => ({ [name]: emptyJsonbArray(snakeName) }),
|
|
116
|
+
images: (name, snakeName) => ({ [name]: emptyJsonbArray(snakeName) }),
|
|
117
|
+
};
|
|
118
|
+
function emitFieldColumns(type, name, field, entity) {
|
|
119
|
+
// A type from untyped input must not reach an Object.prototype member.
|
|
120
|
+
if (!Object.hasOwn(COLUMN_EMITTERS, type)) {
|
|
121
|
+
throw new Error(`[Kumiko] unhandled field type: ${String(field)}`);
|
|
122
|
+
}
|
|
123
|
+
const emit = COLUMN_EMITTERS[type];
|
|
124
|
+
return emit(name, toSnakeCase(name), field, entity);
|
|
125
|
+
}
|
|
126
|
+
// Most fields return one column; money and locatedTimestamp return two.
|
|
127
|
+
function fieldToColumns(name, field, entity) {
|
|
128
|
+
return emitFieldColumns(field.type, name, field, entity);
|
|
182
129
|
}
|
|
183
130
|
// Accepts both camelCase (`tenantMembership`) and kebab-case (`tenant-membership`)
|
|
184
131
|
// entity / field names. Implementation lives in utils/case — re-exported here
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TableLike } from "./query.ts";
|
|
2
|
+
export type TableInfo = {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly columnOf: (field: string) => string;
|
|
5
|
+
readonly pgTypeOf: (column: string) => string | undefined;
|
|
6
|
+
readonly bigintJsModeOf: (column: string) => "number" | "bigint" | undefined;
|
|
7
|
+
readonly fieldOf: (column: string) => string;
|
|
8
|
+
readonly hasColumn: (fieldOrColumn: string) => boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function extractTableInfo(table: TableLike): TableInfo;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { camelCase as envCamelCase } from "../env/index.js";
|
|
2
|
+
import { toSnakeCase } from "./table-builder.js";
|
|
3
|
+
import { asEntityTableMeta } from "./table-introspection.js";
|
|
4
|
+
// Idempotent snake_case → camelCase. `env.camelCase` always lowercases first
|
|
5
|
+
// (designed for SHOUT_CASE input) — for already-camelCase keys (mock rows
|
|
6
|
+
// in tests, projection-aliased columns) it would silently produce "tenantid"
|
|
7
|
+
// instead of leaving "tenantId" alone. Guard with an underscore check so
|
|
8
|
+
// the conversion only fires when the key is actually snake-shaped.
|
|
9
|
+
function snakeToCamel(key) {
|
|
10
|
+
if (!key.includes("_"))
|
|
11
|
+
return key;
|
|
12
|
+
return envCamelCase(key);
|
|
13
|
+
}
|
|
14
|
+
export function extractTableInfo(table) {
|
|
15
|
+
const meta = asEntityTableMeta(table);
|
|
16
|
+
if (!meta) {
|
|
17
|
+
throw new Error("extractTableInfo: table is not a kumiko EntityTableMeta — " +
|
|
18
|
+
"build it via buildEntityTable / deriveEntityTableMeta / table().");
|
|
19
|
+
}
|
|
20
|
+
const colByField = new Map();
|
|
21
|
+
const fieldByCol = new Map();
|
|
22
|
+
const typeByCol = new Map();
|
|
23
|
+
const bigintModeByCol = new Map();
|
|
24
|
+
for (const c of meta.columns) {
|
|
25
|
+
typeByCol.set(c.name, c.pgType);
|
|
26
|
+
if (c.bigintJsMode !== undefined)
|
|
27
|
+
bigintModeByCol.set(c.name, c.bigintJsMode);
|
|
28
|
+
// EntityTableMeta column names are snake_case. Map snake → snake AND
|
|
29
|
+
// derive a camelCase JS field-name so result rows can be renamed back
|
|
30
|
+
// to the API shape (`aggregate_id` → `aggregateId`).
|
|
31
|
+
colByField.set(c.name, c.name);
|
|
32
|
+
const camel = snakeToCamel(c.name);
|
|
33
|
+
if (camel !== c.name)
|
|
34
|
+
colByField.set(camel, c.name);
|
|
35
|
+
fieldByCol.set(c.name, camel === c.name ? c.name : camel);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
name: meta.tableName,
|
|
39
|
+
columnOf: (field) => colByField.get(field) ?? toSnakeCase(field),
|
|
40
|
+
pgTypeOf: (col) => typeByCol.get(col),
|
|
41
|
+
bigintJsModeOf: (col) => bigintModeByCol.get(col),
|
|
42
|
+
fieldOf: (col) => fieldByCol.get(col) ?? snakeToCamel(col),
|
|
43
|
+
hasColumn: (fieldOrColumn) => colByField.has(fieldOrColumn) || fieldByCol.has(fieldOrColumn),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
2
|
+
export declare function isTemporalInstant(v: unknown): v is Temporal.Instant;
|
|
3
|
+
export declare function isTemporalPlainDate(v: unknown): v is Temporal.PlainDate;
|
|
4
|
+
export declare function isTemporalPlainDateTimeLike(v: unknown): v is {
|
|
5
|
+
toPlainDate(): Temporal.PlainDate;
|
|
6
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
export function isTemporalInstant(v) {
|
|
3
|
+
return propertyOf(v, Symbol.toStringTag) === "Temporal.Instant";
|
|
4
|
+
}
|
|
5
|
+
export function isTemporalPlainDate(v) {
|
|
6
|
+
return propertyOf(v, Symbol.toStringTag) === "Temporal.PlainDate";
|
|
7
|
+
}
|
|
8
|
+
export function isTemporalPlainDateTimeLike(v) {
|
|
9
|
+
const tag = propertyOf(v, Symbol.toStringTag);
|
|
10
|
+
return ((tag === "Temporal.PlainDateTime" || tag === "Temporal.ZonedDateTime") &&
|
|
11
|
+
typeof v === "object" &&
|
|
12
|
+
v !== null &&
|
|
13
|
+
"toPlainDate" in v &&
|
|
14
|
+
typeof v.toPlainDate === "function");
|
|
15
|
+
}
|
|
@@ -6,5 +6,6 @@ export declare function isDbRunner(value: unknown): value is DbRunner;
|
|
|
6
6
|
export declare function bindTenantDbRunner(tenantDb: TenantDb, runner: DbRunner): void;
|
|
7
7
|
export declare function tenantDbRunner(tenantDb: TenantDb): DbRunner;
|
|
8
8
|
export declare function denyTenantDbWrites(tenantDb: TenantDb, reason: string): void;
|
|
9
|
+
export declare function tenantDbWritesDeniedReason(tenantDb: TenantDb): string | undefined;
|
|
9
10
|
export declare function tenantDbWritesDeniedError(operation: string, reason: string): InternalError;
|
|
10
11
|
export declare function tenantDbWriteRunner(tenantDb: TenantDb, operation: string): DbRunner;
|
|
@@ -26,6 +26,9 @@ const writesDeniedReasons = new WeakMap();
|
|
|
26
26
|
export function denyTenantDbWrites(tenantDb, reason) {
|
|
27
27
|
writesDeniedReasons.set(tenantDb, reason);
|
|
28
28
|
}
|
|
29
|
+
export function tenantDbWritesDeniedReason(tenantDb) {
|
|
30
|
+
return writesDeniedReasons.get(tenantDb);
|
|
31
|
+
}
|
|
29
32
|
export function tenantDbWritesDeniedError(operation, reason) {
|
|
30
33
|
return new InternalError({
|
|
31
34
|
message: `${operation}: writes are not available ${reason}; run the write through ctx.write so it gets its own transaction`,
|
package/dist/db/tenant-db.js
CHANGED
|
@@ -10,7 +10,7 @@ import { bindProjectionRegistry, projectionRegistryOf } from "./projection-regis
|
|
|
10
10
|
import { acquireNamespacedAdvisoryLock } from "./queries/advisory-lock.js";
|
|
11
11
|
import { queryDatabaseNow } from "./queries/database-now.js";
|
|
12
12
|
import { hasFieldProperty, tableNameOf } from "./table-introspection.js";
|
|
13
|
-
import { bindTenantDbRunner, denyTenantDbWrites, tenantDbRunner, tenantDbWritesDeniedError, } from "./tenant-db-runner.js";
|
|
13
|
+
import { bindTenantDbRunner, denyTenantDbWrites, tenantDbRunner, tenantDbWritesDeniedError, tenantDbWritesDeniedReason, } from "./tenant-db-runner.js";
|
|
14
14
|
export { SYSTEM_SCOPE_CHECK_BRAND, } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
15
15
|
const declaredUnsafeRawRunners = new WeakMap();
|
|
16
16
|
// The CRUD executor writes through tenantDbRunner, not insertOne, so it asks the
|
|
@@ -105,9 +105,7 @@ export function assertPersonalDataWrite(db, tableName, keys, entity) {
|
|
|
105
105
|
}
|
|
106
106
|
// Framework-private (not re-exported from db/index.ts): same grant check + audit as unsafeRaw, for engine forwarding.
|
|
107
107
|
export function unsafeRawForDeclaredStep(holder, reason) {
|
|
108
|
-
|
|
109
|
-
throw new Error("unsafeRawForDeclaredStep requires a non-empty reason");
|
|
110
|
-
}
|
|
108
|
+
assertReason("unsafeRawForDeclaredStep", reason);
|
|
111
109
|
const runner = declaredUnsafeRawRunners.get(holder);
|
|
112
110
|
if (!runner) {
|
|
113
111
|
throw new InternalError({
|
|
@@ -257,7 +255,13 @@ export function createSystemDbView(db, dbOutsideTransaction, report = fallbackEs
|
|
|
257
255
|
// same connection/transaction. For code that must run caller-supplied callbacks (e.g. config
|
|
258
256
|
// validators) without handing them a cross-tenant view.
|
|
259
257
|
export function scopeTenantDbToTenant(db, tenantId) {
|
|
260
|
-
|
|
258
|
+
const writesDeniedReason = tenantDbWritesDeniedReason(db);
|
|
259
|
+
return createTenantDb({
|
|
260
|
+
db: tenantDbRunner(db),
|
|
261
|
+
tenantId,
|
|
262
|
+
mode: "tenant",
|
|
263
|
+
...(writesDeniedReason !== undefined && { grants: { writesDeniedReason } }),
|
|
264
|
+
});
|
|
261
265
|
}
|
|
262
266
|
// @cast-boundary tenant-db-row
|
|
263
267
|
export function castTenantRows(rows) {
|
|
@@ -268,6 +272,18 @@ export { tableNameOf };
|
|
|
268
272
|
function hasGrant(decl) {
|
|
269
273
|
return decl !== undefined && decl.reason.trim().length > 0;
|
|
270
274
|
}
|
|
275
|
+
// Tenant ids left over when the caller excludes some via ne/notIn; undefined for other operator forms.
|
|
276
|
+
function tenantIdsAfterExclusion(requested, allowed) {
|
|
277
|
+
if (!isPlainObject(requested))
|
|
278
|
+
return undefined;
|
|
279
|
+
if ("ne" in requested)
|
|
280
|
+
return allowed.filter((t) => t !== requested["ne"]);
|
|
281
|
+
if (Array.isArray(requested["notIn"])) {
|
|
282
|
+
const excluded = requested["notIn"];
|
|
283
|
+
return allowed.filter((t) => !excluded.includes(t));
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
271
287
|
// Tenant ids a where.tenantId filter asks for; undefined for operator forms
|
|
272
288
|
// that cannot narrow to a tenant list (ne, like, ...).
|
|
273
289
|
export function requestedTenantIds(requested) {
|
|
@@ -429,7 +445,13 @@ function buildTenantDb(options) {
|
|
|
429
445
|
const allowed = [tenantId, SYSTEM_TENANT_ID];
|
|
430
446
|
const requested = where?.["tenantId"];
|
|
431
447
|
if (requested !== undefined) {
|
|
432
|
-
const requestedList = requestedTenantIds(requested)
|
|
448
|
+
const requestedList = requestedTenantIds(requested);
|
|
449
|
+
// An explicit exclusion or empty list must keep excluding, never widen to the full scope.
|
|
450
|
+
if (requestedList === undefined) {
|
|
451
|
+
return { ...where, tenantId: tenantIdsAfterExclusion(requested, allowed) ?? allowed };
|
|
452
|
+
}
|
|
453
|
+
if (requestedList.length === 0)
|
|
454
|
+
return { ...where, tenantId: [] };
|
|
433
455
|
const narrowed = requestedList.filter((t) => typeof t === "string" && allowed.includes(t));
|
|
434
456
|
return { ...where, tenantId: narrowed.length > 0 ? narrowed : allowed };
|
|
435
457
|
}
|
|
@@ -464,6 +486,14 @@ function buildTenantDb(options) {
|
|
|
464
486
|
function globalWriteReason() {
|
|
465
487
|
return grants?.globalWrites?.reason ?? "";
|
|
466
488
|
}
|
|
489
|
+
// The table is the audit target: process-level dedup would otherwise collapse a cron's
|
|
490
|
+
// writes to several global tables into one audit entry.
|
|
491
|
+
function reportGlobalWrite(table) {
|
|
492
|
+
report("global-write", globalWriteReason(), {
|
|
493
|
+
id: tableNameOf(table),
|
|
494
|
+
tenantId: tenantId ?? SYSTEM_TENANT_ID,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
467
497
|
function personalDataDenied(table, keys) {
|
|
468
498
|
if (!personalDataGate)
|
|
469
499
|
return undefined;
|
|
@@ -535,7 +565,7 @@ function buildTenantDb(options) {
|
|
|
535
565
|
personalDataDenied(table, Object.keys(values));
|
|
536
566
|
if (denied)
|
|
537
567
|
return Promise.reject(denied);
|
|
538
|
-
|
|
568
|
+
reportGlobalWrite(table);
|
|
539
569
|
return withDbSpan("insert", table, async () => insertRow(table, values));
|
|
540
570
|
},
|
|
541
571
|
updateMany(set, where) {
|
|
@@ -549,7 +579,7 @@ function buildTenantDb(options) {
|
|
|
549
579
|
if (!where || Object.keys(where).length === 0) {
|
|
550
580
|
return Promise.reject(new Error("db.global().updateMany without where would mass-update every tenant's rows. Pass at least one where condition."));
|
|
551
581
|
}
|
|
552
|
-
|
|
582
|
+
reportGlobalWrite(table);
|
|
553
583
|
return withDbSpan("update", table, async () => updateRows(table, set, where));
|
|
554
584
|
},
|
|
555
585
|
deleteMany(where) {
|
|
@@ -561,7 +591,7 @@ function buildTenantDb(options) {
|
|
|
561
591
|
if (!where || Object.keys(where).length === 0) {
|
|
562
592
|
return Promise.reject(new Error("db.global().deleteMany without where would mass-delete every tenant's rows. Pass at least one where condition."));
|
|
563
593
|
}
|
|
564
|
-
|
|
594
|
+
reportGlobalWrite(table);
|
|
565
595
|
return withDbSpan("delete", table, async () => {
|
|
566
596
|
await deleteRows(table, where);
|
|
567
597
|
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { WhereObject } from "@cosmicdrift/kumiko-types/where-clause-types";
|
|
2
|
+
import type { TableInfo } from "./table-info.ts";
|
|
3
|
+
export declare function buildWhereClause(info: TableInfo, where: WhereObject, startIndex: number, qualifier?: string): {
|
|
4
|
+
sqlText: string;
|
|
5
|
+
values: unknown[];
|
|
6
|
+
};
|