@cosmicdrift/kumiko-framework 0.362.0 → 0.363.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/dist/api/auth-middleware.js +7 -5
- package/dist/api/redis-sse-broker.js +10 -8
- package/dist/api/routes.js +2 -2
- package/dist/bun-db/query.d.ts +6 -4
- package/dist/bun-db/query.js +66 -92
- package/dist/crypto/kek-source.js +5 -3
- package/dist/crypto/pii-field-encryption.d.ts +4 -1
- package/dist/crypto/pii-field-encryption.js +2 -2
- package/dist/db/apply-entity-event.js +2 -2
- package/dist/db/cursor.js +2 -1
- package/dist/db/dialect.d.ts +1 -0
- package/dist/db/dialect.js +28 -13
- package/dist/db/entity-table-from-registry.js +1 -1
- package/dist/db/entity-table-meta.js +5 -13
- package/dist/db/event-store-executor-context.js +6 -6
- package/dist/db/event-store-executor-read.js +9 -9
- package/dist/db/migrate-generator.js +5 -3
- package/dist/db/migrate-runner.js +31 -35
- package/dist/db/parent-ref-clause.js +2 -2
- package/dist/db/queries/backfill-pii.js +11 -9
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.d.ts +6 -6
- package/dist/db/queries/event-consumer.js +17 -17
- package/dist/db/queries/event-store-admin.js +6 -6
- package/dist/db/queries/event-store-transfer.js +2 -2
- package/dist/db/queries/event-store.d.ts +1 -1
- package/dist/db/queries/event-store.js +2 -2
- package/dist/db/queries/pii-backfill-state.js +2 -2
- package/dist/db/queries/projection-rebuild.d.ts +1 -1
- package/dist/db/queries/raw-sql.js +1 -1
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/reference-data.js +3 -5
- package/dist/db/table-builder.js +6 -4
- package/dist/db/table-introspection.d.ts +16 -0
- package/dist/db/table-introspection.js +76 -0
- package/dist/db/tenant-db.d.ts +3 -2
- package/dist/db/tenant-db.js +4 -10
- package/dist/derivatives/variant-key.js +2 -1
- package/dist/engine/boot-validator/entity-list-screens.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +1 -1
- package/dist/engine/boot-validator/screens.js +2 -1
- package/dist/engine/changeset-changes.js +3 -3
- package/dist/engine/define-handler.js +3 -6
- package/dist/engine/embedded-derived.js +10 -6
- package/dist/engine/extensions/escape-hatch-usage.js +2 -1
- package/dist/engine/factories.js +5 -6
- package/dist/engine/feature-ast/extractors/events.js +43 -33
- package/dist/engine/feature-ast/patcher.d.ts +3 -3
- package/dist/engine/feature-ast/patcher.js +3 -3
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +8 -8
- package/dist/engine/feature-ast/pattern-change-schema.js +3 -3
- package/dist/engine/feature-ast/patterns.d.ts +3 -3
- package/dist/engine/feature-ast/render.js +6 -3
- package/dist/engine/feature-changelog.d.ts +2 -1
- package/dist/engine/feature-changelog.js +8 -6
- package/dist/engine/feature-config-events-jobs.d.ts +17 -7
- package/dist/engine/feature-config-events-jobs.js +20 -5
- package/dist/engine/feature-ui-extensions.js +2 -3
- package/dist/engine/index.d.ts +2 -1
- package/dist/engine/index.js +1 -0
- package/dist/engine/ownership.js +7 -20
- package/dist/engine/pattern-library/mixed-schemas.js +0 -1
- package/dist/engine/registry-ingest.d.ts +2 -1
- package/dist/engine/registry-ingest.js +85 -2
- package/dist/engine/registry-validate.d.ts +1 -0
- package/dist/engine/registry-validate.js +38 -8
- package/dist/engine/registry.js +2 -1
- package/dist/engine/schema-builder.js +1 -3
- package/dist/engine/soft-delete-cleanup.js +2 -1
- package/dist/engine/steps/call-feature.js +1 -3
- package/dist/engine/steps/unsafe-projection-upsert.js +2 -13
- package/dist/engine/template-variables.d.ts +29 -0
- package/dist/engine/template-variables.js +77 -0
- package/dist/engine/tier-resolver-extension.js +2 -3
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/validate-projection-allowlist.js +12 -13
- package/dist/engine/where-rule-lint.js +8 -7
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/files/file-routes.js +1 -1
- package/dist/files/local-provider.js +2 -1
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/mail-registry.d.ts +2 -0
- package/dist/i18n/mail-registry.js +7 -0
- package/dist/jobs/job-runner.js +5 -5
- package/dist/pipeline/dispatch-query.js +2 -3
- package/dist/pipeline/dispatch-shared.js +45 -14
- package/dist/pipeline/event-dispatcher-delivery.js +2 -3
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/member-read-only-transaction.js +1 -1
- package/dist/pipeline/notify-scope.d.ts +13 -0
- package/dist/pipeline/notify-scope.js +32 -0
- package/dist/pipeline/system-hooks.js +2 -1
- package/dist/pipeline/system-identity-switch.js +2 -2
- package/dist/search/meilisearch-adapter.js +3 -4
- package/dist/secrets/stored-envelope.js +7 -7
- package/dist/stack/push-entity-projection-tables.js +2 -3
- package/dist/stack/table-helpers.js +10 -26
- package/dist/stack/test-stack.js +2 -5
- package/dist/testing/multipart-helper.d.ts +0 -6
- package/dist/testing/multipart-helper.js +5 -5
- package/dist/testing/without-ambient-temporal.js +4 -4
- package/dist/utils/is-plain-object.d.ts +1 -2
- package/dist/utils/is-plain-object.js +1 -4
- package/package.json +4 -4
- package/src/changes.json +44 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
2
|
import { getCookie } from "hono/cookie";
|
|
2
3
|
import { TENANT_TEARDOWN_STATUSES } from "../engine/active-membership.js";
|
|
3
4
|
import { createAnonymousUser } from "../engine/system-user.js";
|
|
@@ -482,17 +483,18 @@ async function requestsCancelDestruction(c) {
|
|
|
482
483
|
return false;
|
|
483
484
|
try {
|
|
484
485
|
const path = c.req.path;
|
|
485
|
-
const body =
|
|
486
|
+
const body = await c.req.raw.clone().json();
|
|
486
487
|
if (path === "/api/write") {
|
|
487
|
-
return body
|
|
488
|
+
return propertyOf(body, "type") === TENANT_LIFECYCLE_CANCEL_QN;
|
|
488
489
|
}
|
|
489
490
|
if (path === "/api/batch") {
|
|
491
|
+
const commands = propertyOf(body, "commands");
|
|
490
492
|
// every(), not some(): a batch mixing the cancel command with other
|
|
491
493
|
// writes must NOT wave the whole batch through the teardown gate —
|
|
492
494
|
// only a batch consisting solely of cancel-destruction is exempt.
|
|
493
|
-
return (Array.isArray(
|
|
494
|
-
|
|
495
|
-
|
|
495
|
+
return (Array.isArray(commands) &&
|
|
496
|
+
commands.length > 0 &&
|
|
497
|
+
commands.every((command) => propertyOf(command, "type") === TENANT_LIFECYCLE_CANCEL_QN));
|
|
496
498
|
}
|
|
497
499
|
}
|
|
498
500
|
catch {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
2
|
import { redisChannelPrefixFromEnv, redisClientOptionsFromEnv, } from "../redis/client.js";
|
|
2
3
|
import { createRedisPubSubSignal } from "../redis/pubsub-signal.js";
|
|
3
4
|
import { createSseBroker, readNonEmptyStringArray, } from "./sse-broker.js";
|
|
@@ -37,7 +38,9 @@ export function createDefaultSseBroker(env = process.env) {
|
|
|
37
38
|
function isSseEvent(value) {
|
|
38
39
|
return (typeof value === "object" &&
|
|
39
40
|
value !== null &&
|
|
41
|
+
"type" in value &&
|
|
40
42
|
typeof value.type === "string" &&
|
|
43
|
+
"data" in value &&
|
|
41
44
|
typeof value.data === "object" &&
|
|
42
45
|
value.data !== null);
|
|
43
46
|
}
|
|
@@ -50,25 +53,24 @@ function isSseEvent(value) {
|
|
|
50
53
|
// sparing something it doesn't understand: fail-closed on a mixed rolling
|
|
51
54
|
// deploy. Anything malformed also falls back to userwide.
|
|
52
55
|
function parseInvalidationScope(payload) {
|
|
53
|
-
if (
|
|
56
|
+
if (!isPlainObject(payload))
|
|
54
57
|
return { kind: "user" };
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const { scope } = record;
|
|
58
|
+
if ("scope" in payload) {
|
|
59
|
+
const { scope } = payload;
|
|
58
60
|
if (scope === "sessions") {
|
|
59
|
-
const sessionIds = readNonEmptyStringArray(
|
|
61
|
+
const sessionIds = readNonEmptyStringArray(payload["sessionIds"]);
|
|
60
62
|
if (sessionIds)
|
|
61
63
|
return { kind: "sessions", sessionIds };
|
|
62
64
|
}
|
|
63
65
|
if (scope === "pat-tokens") {
|
|
64
|
-
const tokenIds = readNonEmptyStringArray(
|
|
66
|
+
const tokenIds = readNonEmptyStringArray(payload["tokenIds"]);
|
|
65
67
|
if (tokenIds)
|
|
66
68
|
return { kind: "pat-tokens", tokenIds };
|
|
67
69
|
}
|
|
68
70
|
return { kind: "user" };
|
|
69
71
|
}
|
|
70
|
-
if ("keptSessionId" in
|
|
71
|
-
const { keptSessionId } =
|
|
72
|
+
if ("keptSessionId" in payload) {
|
|
73
|
+
const { keptSessionId } = payload;
|
|
72
74
|
if (typeof keptSessionId === "string" && keptSessionId.length > 0) {
|
|
73
75
|
return { kind: "all-except-session", keptSessionId };
|
|
74
76
|
}
|
package/dist/api/routes.js
CHANGED
|
@@ -172,7 +172,7 @@ export function createApiRoutes(dispatcher, options = {}) {
|
|
|
172
172
|
// in-flight .next(), so awaiting here would block the response until
|
|
173
173
|
// that pending pull resolves (which may be never for an idle stream).
|
|
174
174
|
void generator.return(undefined).catch(() => { });
|
|
175
|
-
return c.body(null, CLIENT_CLOSED_REQUEST_STATUS); // @cast-boundary non-standard client-closed-request status, Hono's union doesn't include it
|
|
175
|
+
return c.body(null, CLIENT_CLOSED_REQUEST_STATUS); // @cast-boundary third-party-type-gap — non-standard client-closed-request status, Hono's union doesn't include it
|
|
176
176
|
}
|
|
177
177
|
return streamSSE(c, async (stream) => {
|
|
178
178
|
stream.onAbort(() => {
|
|
@@ -394,7 +394,7 @@ function clientAbortResponse(c, requestId, type) {
|
|
|
394
394
|
logClientWarn("request aborted by client", requestId, type, {
|
|
395
395
|
status: CLIENT_CLOSED_REQUEST_STATUS,
|
|
396
396
|
});
|
|
397
|
-
return c.body(null, CLIENT_CLOSED_REQUEST_STATUS); // @cast-boundary non-standard client-closed-request status, Hono's union doesn't include it
|
|
397
|
+
return c.body(null, CLIENT_CLOSED_REQUEST_STATUS); // @cast-boundary third-party-type-gap — non-standard client-closed-request status, Hono's union doesn't include it
|
|
398
398
|
}
|
|
399
399
|
// For /query + /command: no isSuccess on success (just { data } / {ok}), so we
|
|
400
400
|
// keep the same lean shape on failure — only the `error` key.
|
package/dist/bun-db/query.d.ts
CHANGED
|
@@ -2,13 +2,15 @@ import type { AggregateRow, AggregateSpec } from "@cosmicdrift/kumiko-types/aggr
|
|
|
2
2
|
import type { SelectOptions, WhereObject } from "@cosmicdrift/kumiko-types/where-clause-types";
|
|
3
3
|
import type { EntityTableMeta } from "../db/entity-table-meta.js";
|
|
4
4
|
import { type NotExecutorOnly } from "../db/table-builder.js";
|
|
5
|
+
import { asEntityTableMeta, requireEntityTableMeta } from "../db/table-introspection.js";
|
|
5
6
|
import type { DbRunner } from "@cosmicdrift/kumiko-types/db-connection";
|
|
6
7
|
import type { BunDbRunner } from "./connection.js";
|
|
7
|
-
export
|
|
8
|
-
export declare function requireEntityTableMeta(table: unknown, tableName: string): EntityTableMeta;
|
|
8
|
+
export { asEntityTableMeta, requireEntityTableMeta };
|
|
9
9
|
type RawClient = {
|
|
10
10
|
unsafe: <TRow = unknown>(sql: string, params?: readonly unknown[]) => Promise<readonly TRow[]>;
|
|
11
|
-
begin: <T>(fn: (tx:
|
|
11
|
+
begin: <T>(fn: (tx: BunDbRunner) => Promise<T>) => Promise<T>;
|
|
12
|
+
savepoint?: <T>(fn: (sp: unknown) => Promise<T>) => Promise<T>;
|
|
13
|
+
release?: unknown;
|
|
12
14
|
};
|
|
13
15
|
export declare function asRawClient(db: unknown): RawClient;
|
|
14
16
|
export declare function runInSavepoint<T>(tx: unknown, fn: (sp: unknown) => Promise<T>): Promise<T>;
|
|
@@ -26,7 +28,7 @@ export type TableInfo = {
|
|
|
26
28
|
readonly hasColumn: (fieldOrColumn: string) => boolean;
|
|
27
29
|
};
|
|
28
30
|
export declare function extractTableInfo(table: TableLike): TableInfo;
|
|
29
|
-
export declare function coerceRow<T
|
|
31
|
+
export declare function coerceRow<T>(row: T, info: TableInfo): T;
|
|
30
32
|
export declare function isClosedConnectionError(err: unknown): boolean;
|
|
31
33
|
export declare function describeClosedConnectionFailure(raw: unknown, err: unknown, attempts: number): Record<string, unknown> | undefined;
|
|
32
34
|
export declare function unsafeReadRetrying<TRow>(db: AnyDb, sqlText: string, params: readonly unknown[]): Promise<readonly TRow[]>;
|
package/dist/bun-db/query.js
CHANGED
|
@@ -18,16 +18,17 @@
|
|
|
18
18
|
// - drizzle pgTable (legacy, has Symbol-based tableName) — extracted via
|
|
19
19
|
// drizzle's getTableName + getTableColumns (drizzle kept only as a type
|
|
20
20
|
// reference, never as a runtime API call)
|
|
21
|
-
import { KUMIKO_META_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
22
21
|
// Static import (not getTemporal()): coercion works without an installed
|
|
23
22
|
// global and returns the same Temporal classes as the rest of the repo (#1480).
|
|
24
23
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
24
|
+
import { isOneOf, isPlainObject, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
25
25
|
import { requestContext } from "../api/request-context.js";
|
|
26
26
|
import { computeBlindIndex, configuredBlindIndexKey } from "../crypto/blind-index.js";
|
|
27
27
|
import { isPiiCiphertext } from "../crypto/pii-ciphertext-format.js";
|
|
28
28
|
import { SQL_EXPR_BRAND } from "../db/dialect.js";
|
|
29
29
|
import { extractPgError } from "../db/pg-error.js";
|
|
30
30
|
import { toSnakeCase } from "../db/table-builder.js";
|
|
31
|
+
import { asEntityTableMeta, requireEntityTableMeta } from "../db/table-introspection.js";
|
|
31
32
|
import { camelCase as envCamelCase } from "../env/index.js";
|
|
32
33
|
import { InternalError } from "../errors/index.js";
|
|
33
34
|
import { createFallbackLogger } from "../logging/index.js";
|
|
@@ -48,43 +49,17 @@ function snakeToCamel(key) {
|
|
|
48
49
|
// enumerable props, so an entity field named `source`/`columns`/`tableName`/…
|
|
49
50
|
// would overwrite the matching meta key — reading the meta from the symbol is
|
|
50
51
|
// the only collision-safe path.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
typeof v.tableName === "string" &&
|
|
55
|
-
Array.isArray(v.columns) &&
|
|
56
|
-
Array.isArray(v.indexes) &&
|
|
57
|
-
(v.source === "managed" || v.source === "unmanaged"));
|
|
58
|
-
}
|
|
59
|
-
// Resolve any framework table input to its canonical EntityTableMeta:
|
|
60
|
-
// - table()/buildEntityTable outputs carry it under KUMIKO_META_SYMBOL, immune
|
|
61
|
-
// to a column-handle shadowing a meta key.
|
|
62
|
-
// - deriveEntityTableMeta / defineUnmanagedTable return a plain meta with no
|
|
63
|
-
// handle-spread, so its structural shape is itself unshadowable.
|
|
64
|
-
export function asEntityTableMeta(table) {
|
|
65
|
-
if (table === null || typeof table !== "object")
|
|
66
|
-
return undefined;
|
|
67
|
-
const fromSymbol = table[KUMIKO_META_SYMBOL];
|
|
68
|
-
if (isEntityTableMeta(fromSymbol))
|
|
69
|
-
return fromSymbol;
|
|
70
|
-
return isEntityTableMeta(table) ? table : undefined;
|
|
71
|
-
}
|
|
72
|
-
export function requireEntityTableMeta(table, tableName) {
|
|
73
|
-
const meta = asEntityTableMeta(table);
|
|
74
|
-
if (!meta) {
|
|
75
|
-
throw new Error(`${tableName}: table carries no EntityTableMeta — built via table()?`);
|
|
76
|
-
}
|
|
77
|
-
return meta;
|
|
52
|
+
export { asEntityTableMeta, requireEntityTableMeta };
|
|
53
|
+
function isRawClient(value) {
|
|
54
|
+
return typeof propertyOf(value, "unsafe") === "function";
|
|
78
55
|
}
|
|
79
56
|
export function asRawClient(db) {
|
|
80
|
-
const dbAny = db;
|
|
81
57
|
// Direct: Bun.SQL / postgres-js Sql / postgres-js TransactionSql. All three
|
|
82
58
|
// expose `.unsafe`; only Sql/Bun.SQL have `.begin` — TransactionSql uses
|
|
83
59
|
// `.savepoint` for nested-tx. We only require `.unsafe` here; callers that
|
|
84
60
|
// need `.begin` (e.g. `transaction()`) verify it themselves.
|
|
85
|
-
if (
|
|
86
|
-
return
|
|
87
|
-
}
|
|
61
|
+
if (isRawClient(db))
|
|
62
|
+
return db;
|
|
88
63
|
// TenantDb (ctx.db) has no unfiltered escape via asRawClient anymore — fail
|
|
89
64
|
// closed instead of silently unwrapping a tenant-scoped handle.
|
|
90
65
|
if (isTenantDbShape(db)) {
|
|
@@ -93,16 +68,13 @@ export function asRawClient(db) {
|
|
|
93
68
|
"{ reason } on the handler/hook (r.systemScope() features: ctx.systemDb.unsafeRaw(reason)).");
|
|
94
69
|
}
|
|
95
70
|
// Drizzle DbConnection (legacy compat): $client = postgres-js Sql.
|
|
96
|
-
const $client =
|
|
97
|
-
if ($client
|
|
71
|
+
const $client = propertyOf(db, "$client");
|
|
72
|
+
if (isRawClient($client))
|
|
98
73
|
return $client;
|
|
99
|
-
}
|
|
100
74
|
// Drizzle pg-transaction (legacy compat): session.client = postgres-js Sql.
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
if (sessionClient && typeof sessionClient["unsafe"] === "function") {
|
|
75
|
+
const sessionClient = propertyOf(propertyOf(db, "session"), "client");
|
|
76
|
+
if (isRawClient(sessionClient))
|
|
104
77
|
return sessionClient;
|
|
105
|
-
}
|
|
106
78
|
throw new Error("bun-db: db argument has no .unsafe() — pass Bun.SQL, postgres-js Sql, or a transaction handle.");
|
|
107
79
|
}
|
|
108
80
|
// postgres.js / Bun.SQL reject the WHOLE begin() when any statement inside
|
|
@@ -131,7 +103,7 @@ export async function runInSavepoint(tx, fn) {
|
|
|
131
103
|
// to paper over by re-running `fn` without the savepoint's isolation.
|
|
132
104
|
export async function runInSavepointIfSupported(db, fn) {
|
|
133
105
|
const raw = asRawClient(db);
|
|
134
|
-
// @cast-boundary
|
|
106
|
+
// @cast-boundary db-runner savepoint handle → DbRunner at the single seam
|
|
135
107
|
const asRunner = (sp) => sp;
|
|
136
108
|
if (typeof raw.savepoint !== "function")
|
|
137
109
|
return fn(asRunner(db));
|
|
@@ -156,15 +128,14 @@ export async function runInSavepointIfSupported(db, fn) {
|
|
|
156
128
|
function isTenantDbShape(db) {
|
|
157
129
|
if (typeof db !== "object" || db === null)
|
|
158
130
|
return false;
|
|
159
|
-
|
|
160
|
-
if (typeof d["unsafe"] === "function")
|
|
131
|
+
if (typeof propertyOf(db, "unsafe") === "function")
|
|
161
132
|
return false;
|
|
162
|
-
return (typeof
|
|
163
|
-
typeof
|
|
164
|
-
typeof
|
|
165
|
-
typeof
|
|
166
|
-
typeof
|
|
167
|
-
"tenantId" in
|
|
133
|
+
return (typeof propertyOf(db, "selectMany") === "function" &&
|
|
134
|
+
typeof propertyOf(db, "fetchOne") === "function" &&
|
|
135
|
+
typeof propertyOf(db, "insertOne") === "function" &&
|
|
136
|
+
typeof propertyOf(db, "updateMany") === "function" &&
|
|
137
|
+
typeof propertyOf(db, "deleteMany") === "function" &&
|
|
138
|
+
"tenantId" in db);
|
|
168
139
|
}
|
|
169
140
|
function tenantDbDelegate(db) {
|
|
170
141
|
return isTenantDbShape(db) ? db : undefined;
|
|
@@ -182,6 +153,9 @@ function assertNotTenantScoped(db, fnName) {
|
|
|
182
153
|
const WHERE_OPERATOR_KEYS = ["gt", "gte", "lt", "lte", "ne", "in", "like", "jsonText"];
|
|
183
154
|
const _whereOperatorKeysExhaustive = true;
|
|
184
155
|
void _whereOperatorKeysExhaustive;
|
|
156
|
+
function isJsonTextMatchList(value) {
|
|
157
|
+
return Array.isArray(value);
|
|
158
|
+
}
|
|
185
159
|
function isWhereOperator(v) {
|
|
186
160
|
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
187
161
|
return false;
|
|
@@ -190,7 +164,7 @@ function isWhereOperator(v) {
|
|
|
190
164
|
const keys = Object.keys(v);
|
|
191
165
|
if (keys.length === 0)
|
|
192
166
|
return false;
|
|
193
|
-
return keys.every((k) => WHERE_OPERATOR_KEYS
|
|
167
|
+
return keys.every((k) => isOneOf(WHERE_OPERATOR_KEYS, k));
|
|
194
168
|
}
|
|
195
169
|
export function extractTableInfo(table) {
|
|
196
170
|
const meta = asEntityTableMeta(table);
|
|
@@ -236,10 +210,13 @@ function quoteIdent(name) {
|
|
|
236
210
|
//
|
|
237
211
|
// Symmetric on write: Temporal.Instant doesn't bind directly into postgres-js
|
|
238
212
|
// params — convert to ISO string when the column pgType is timestamptz.
|
|
213
|
+
function toStringTagOf(v) {
|
|
214
|
+
return typeof v === "object" && v !== null && Symbol.toStringTag in v
|
|
215
|
+
? v[Symbol.toStringTag]
|
|
216
|
+
: undefined;
|
|
217
|
+
}
|
|
239
218
|
function isTemporalInstant(v) {
|
|
240
|
-
return (
|
|
241
|
-
v !== null &&
|
|
242
|
-
v[Symbol.toStringTag] === "Temporal.Instant");
|
|
219
|
+
return toStringTagOf(v) === "Temporal.Instant";
|
|
243
220
|
}
|
|
244
221
|
// Postgres bigint → JS: safe integers become `number` (matches createBigIntField /
|
|
245
222
|
// drizzle mode:"number"); values outside ±2^53 stay `bigint` (money cents, raw
|
|
@@ -289,15 +266,14 @@ function instantFromDriver(value) {
|
|
|
289
266
|
return null;
|
|
290
267
|
}
|
|
291
268
|
function isTemporalPlainDate(v) {
|
|
292
|
-
return (
|
|
293
|
-
v !== null &&
|
|
294
|
-
v[Symbol.toStringTag] === "Temporal.PlainDate");
|
|
269
|
+
return toStringTagOf(v) === "Temporal.PlainDate";
|
|
295
270
|
}
|
|
296
271
|
function isTemporalPlainDateTimeLike(v) {
|
|
297
|
-
|
|
272
|
+
const tag = toStringTagOf(v);
|
|
273
|
+
return ((tag === "Temporal.PlainDateTime" || tag === "Temporal.ZonedDateTime") &&
|
|
274
|
+
typeof v === "object" &&
|
|
298
275
|
v !== null &&
|
|
299
|
-
|
|
300
|
-
v[Symbol.toStringTag] === "Temporal.ZonedDateTime") &&
|
|
276
|
+
"toPlainDate" in v &&
|
|
301
277
|
typeof v.toPlainDate === "function");
|
|
302
278
|
}
|
|
303
279
|
// Verified empirically against Bun.SQL (kumiko-framework#1924): a `date`
|
|
@@ -353,6 +329,8 @@ function plainDateFromDriver(value, column) {
|
|
|
353
329
|
//
|
|
354
330
|
// Unknown columns (computed/aliased) pass through unchanged.
|
|
355
331
|
export function coerceRow(row, info) {
|
|
332
|
+
if (!isPlainObject(row))
|
|
333
|
+
return row;
|
|
356
334
|
const out = {};
|
|
357
335
|
let changed = false;
|
|
358
336
|
for (const key of Object.keys(row)) {
|
|
@@ -568,7 +546,7 @@ function buildWhereClause(info, where, startIndex, qualifier) {
|
|
|
568
546
|
if (pgType !== "jsonb") {
|
|
569
547
|
throw new Error(`where: jsonText requires a jsonb column, "${col}" is ${pgType}`);
|
|
570
548
|
}
|
|
571
|
-
const matches =
|
|
549
|
+
const matches = isJsonTextMatchList(value.jsonText) ? value.jsonText : [value.jsonText];
|
|
572
550
|
for (const match of matches) {
|
|
573
551
|
if (!Array.isArray(match.keys) ||
|
|
574
552
|
match.keys.length === 0 ||
|
|
@@ -599,7 +577,7 @@ function buildWhereClause(info, where, startIndex, qualifier) {
|
|
|
599
577
|
like: "LIKE",
|
|
600
578
|
};
|
|
601
579
|
for (const [opKey, opSym] of Object.entries(opMap)) {
|
|
602
|
-
const opVal = value
|
|
580
|
+
const opVal = Reflect.get(value, opKey);
|
|
603
581
|
if (opVal === undefined)
|
|
604
582
|
continue;
|
|
605
583
|
// Mirrors applyMultiSelectFilter: ordering/pattern ops have no meaning on a jsonb
|
|
@@ -638,7 +616,7 @@ function buildWhereClause(info, where, startIndex, qualifier) {
|
|
|
638
616
|
values.push(p.bound);
|
|
639
617
|
}
|
|
640
618
|
}
|
|
641
|
-
const inVal = value
|
|
619
|
+
const inVal = value.in;
|
|
642
620
|
if (Array.isArray(inVal)) {
|
|
643
621
|
if (pgType === "jsonb") {
|
|
644
622
|
conditions.push(jsonbContainsAny(col, inVal));
|
|
@@ -711,11 +689,9 @@ export function isClosedConnectionError(err) {
|
|
|
711
689
|
function isPooledClient(raw) {
|
|
712
690
|
if (raw === null || (typeof raw !== "object" && typeof raw !== "function"))
|
|
713
691
|
return false;
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
typeof r.savepoint !== "function" &&
|
|
718
|
-
typeof r.release !== "function");
|
|
692
|
+
return (typeof propertyOf(raw, "begin") === "function" &&
|
|
693
|
+
typeof propertyOf(raw, "savepoint") !== "function" &&
|
|
694
|
+
typeof propertyOf(raw, "release") !== "function");
|
|
719
695
|
}
|
|
720
696
|
const closedConnectionLog = createFallbackLogger("bun-db");
|
|
721
697
|
// The driver rejects in-flight queries with this AbortError (no code) when the
|
|
@@ -803,8 +779,7 @@ async function unsafeLoggingClosedConnection(db, sqlText, params, context) {
|
|
|
803
779
|
}
|
|
804
780
|
const DEFAULT_POOL_MAX = 10;
|
|
805
781
|
function poolMaxOf(raw) {
|
|
806
|
-
|
|
807
|
-
const max = raw.options?.max;
|
|
782
|
+
const max = propertyOf(propertyOf(raw, "options"), "max");
|
|
808
783
|
return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : DEFAULT_POOL_MAX;
|
|
809
784
|
}
|
|
810
785
|
// Exported so raw-SQL query modules outside bun-db (e.g. bundled-features'
|
|
@@ -814,7 +789,7 @@ function poolMaxOf(raw) {
|
|
|
814
789
|
export async function unsafeReadRetrying(db, sqlText, params) {
|
|
815
790
|
const raw = asRawClient(db);
|
|
816
791
|
try {
|
|
817
|
-
return
|
|
792
|
+
return await raw.unsafe(sqlText, params);
|
|
818
793
|
}
|
|
819
794
|
catch (err) {
|
|
820
795
|
if (!isPooledClient(raw) || !isClosedConnectionError(err)) {
|
|
@@ -826,7 +801,7 @@ export async function unsafeReadRetrying(db, sqlText, params) {
|
|
|
826
801
|
let lastErr = err;
|
|
827
802
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
828
803
|
try {
|
|
829
|
-
return
|
|
804
|
+
return await raw.unsafe(sqlText, params);
|
|
830
805
|
}
|
|
831
806
|
catch (retryErr) {
|
|
832
807
|
lastErr = retryErr;
|
|
@@ -872,7 +847,7 @@ export async function selectMany(db, table, where, options) {
|
|
|
872
847
|
}
|
|
873
848
|
sqlText += ` LIMIT ${options.limit}`;
|
|
874
849
|
}
|
|
875
|
-
const raw =
|
|
850
|
+
const raw = await unsafeReadRetrying(db, sqlText, values);
|
|
876
851
|
return coerceRows(raw, info);
|
|
877
852
|
}
|
|
878
853
|
// biome-ignore lint/suspicious/noExplicitAny: see selectMany default
|
|
@@ -937,7 +912,7 @@ export async function selectInnerJoin(db, spec) {
|
|
|
937
912
|
sqlText += ` WHERE ${conditions.join(" AND ")}`;
|
|
938
913
|
if (spec.limit !== undefined)
|
|
939
914
|
sqlText += ` LIMIT ${spec.limit}`;
|
|
940
|
-
const raw =
|
|
915
|
+
const raw = await unsafeReadRetrying(db, sqlText, values);
|
|
941
916
|
return raw.map((row) => {
|
|
942
917
|
const leftRow = {};
|
|
943
918
|
const rightRow = {};
|
|
@@ -987,10 +962,10 @@ export async function insertMany(db, table, rows) {
|
|
|
987
962
|
valuesClauses.push(`(${placeholders.join(", ")})`);
|
|
988
963
|
}
|
|
989
964
|
const sqlText = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES ${valuesClauses.join(", ")} RETURNING *`;
|
|
990
|
-
const raw =
|
|
965
|
+
const raw = await unsafeLoggingClosedConnection(db, sqlText, params, {
|
|
991
966
|
operation: "insertMany",
|
|
992
967
|
table: info.name,
|
|
993
|
-
})
|
|
968
|
+
});
|
|
994
969
|
return coerceRows(raw, info);
|
|
995
970
|
}
|
|
996
971
|
// biome-ignore lint/suspicious/noExplicitAny: see selectMany default
|
|
@@ -1020,10 +995,10 @@ export async function insertOne(db, table, values) {
|
|
|
1020
995
|
})
|
|
1021
996
|
.join(", ");
|
|
1022
997
|
const sqlText = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES (${placeholders}) RETURNING *`;
|
|
1023
|
-
const rows =
|
|
998
|
+
const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
|
|
1024
999
|
operation: "insertOne",
|
|
1025
1000
|
table: info.name,
|
|
1026
|
-
})
|
|
1001
|
+
});
|
|
1027
1002
|
const first = rows[0];
|
|
1028
1003
|
if (!first)
|
|
1029
1004
|
return undefined;
|
|
@@ -1063,10 +1038,10 @@ export async function updateMany(db, table, set, where) {
|
|
|
1063
1038
|
values.push(v);
|
|
1064
1039
|
}
|
|
1065
1040
|
sqlText += " RETURNING *";
|
|
1066
|
-
const raw =
|
|
1041
|
+
const raw = await unsafeLoggingClosedConnection(db, sqlText, values, {
|
|
1067
1042
|
operation: "updateMany",
|
|
1068
1043
|
table: info.name,
|
|
1069
|
-
})
|
|
1044
|
+
});
|
|
1070
1045
|
return coerceRows(raw, info);
|
|
1071
1046
|
}
|
|
1072
1047
|
export async function deleteMany(db, table, where) {
|
|
@@ -1135,7 +1110,7 @@ export async function countWhere(db, table, where = {}) {
|
|
|
1135
1110
|
sqlText += ` WHERE ${w.sqlText}`;
|
|
1136
1111
|
values = w.values;
|
|
1137
1112
|
}
|
|
1138
|
-
const rows =
|
|
1113
|
+
const rows = await unsafeReadRetrying(db, sqlText, values);
|
|
1139
1114
|
return rows[0]?.count ?? 0;
|
|
1140
1115
|
}
|
|
1141
1116
|
const AGGREGATE_BUCKET_UNITS = ["hour", "day"];
|
|
@@ -1171,7 +1146,7 @@ function aggregateMeasureSql(info, measure) {
|
|
|
1171
1146
|
case "avg":
|
|
1172
1147
|
return `AVG(${requireAggregateColumn(info, measure.field, "measure field")})`;
|
|
1173
1148
|
default:
|
|
1174
|
-
throw new Error(`aggregateWhere: unknown measure fn "${String(measure
|
|
1149
|
+
throw new Error(`aggregateWhere: unknown measure fn "${String(Reflect.get(measure, "fn"))}"`);
|
|
1175
1150
|
}
|
|
1176
1151
|
}
|
|
1177
1152
|
function aggregateDimensionSql(info, dimension, index, params) {
|
|
@@ -1233,7 +1208,7 @@ export async function aggregateWhere(db, table, spec, where = {}) {
|
|
|
1233
1208
|
params.push(...w.values);
|
|
1234
1209
|
}
|
|
1235
1210
|
sqlText += aggregateGroupOrderLimitSql(spec, dimensions.length);
|
|
1236
|
-
const raw =
|
|
1211
|
+
const raw = await unsafeReadRetrying(db, sqlText, params);
|
|
1237
1212
|
return raw.map((row) => {
|
|
1238
1213
|
const value = row["value"];
|
|
1239
1214
|
return {
|
|
@@ -1277,10 +1252,10 @@ export async function upsertOnConflict(db, table, values, options) {
|
|
|
1277
1252
|
}
|
|
1278
1253
|
const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
|
|
1279
1254
|
const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO UPDATE SET ${updateParts.join(", ")} RETURNING *`;
|
|
1280
|
-
const rows =
|
|
1255
|
+
const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
|
|
1281
1256
|
operation: "upsertOnConflict",
|
|
1282
1257
|
table: info.name,
|
|
1283
|
-
})
|
|
1258
|
+
});
|
|
1284
1259
|
const first = rows[0];
|
|
1285
1260
|
if (!first)
|
|
1286
1261
|
return undefined;
|
|
@@ -1305,10 +1280,10 @@ export async function insertOnConflictDoNothing(db, table, values, options) {
|
|
|
1305
1280
|
const { sqlPrefix, params } = buildInsertSql(info, entries);
|
|
1306
1281
|
const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
|
|
1307
1282
|
const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO NOTHING RETURNING *`;
|
|
1308
|
-
const rows =
|
|
1283
|
+
const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
|
|
1309
1284
|
operation: "insertOnConflictDoNothing",
|
|
1310
1285
|
table: info.name,
|
|
1311
|
-
})
|
|
1286
|
+
});
|
|
1312
1287
|
const first = rows[0];
|
|
1313
1288
|
if (!first)
|
|
1314
1289
|
return undefined;
|
|
@@ -1357,10 +1332,10 @@ export async function incrementCounter(db, table, values, increments, options =
|
|
|
1357
1332
|
}
|
|
1358
1333
|
const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
|
|
1359
1334
|
const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO UPDATE SET ${updateParts.join(", ")} RETURNING *`;
|
|
1360
|
-
const rows =
|
|
1335
|
+
const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
|
|
1361
1336
|
operation: "incrementCounter",
|
|
1362
1337
|
table: info.name,
|
|
1363
|
-
})
|
|
1338
|
+
});
|
|
1364
1339
|
const first = rows[0];
|
|
1365
1340
|
if (!first)
|
|
1366
1341
|
return undefined;
|
|
@@ -1389,10 +1364,10 @@ export async function deleteManyBatched(db, table, where, options) {
|
|
|
1389
1364
|
const sqlText = `DELETE FROM ${tableQ} WHERE ${idQ} IN (
|
|
1390
1365
|
SELECT ${idQ} FROM ${tableQ} WHERE ${w.sqlText} LIMIT $${limitIdx}
|
|
1391
1366
|
) RETURNING ${idQ}`;
|
|
1392
|
-
const rows =
|
|
1367
|
+
const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
|
|
1393
1368
|
operation: "deleteManyBatched",
|
|
1394
1369
|
table: info.name,
|
|
1395
|
-
})
|
|
1370
|
+
});
|
|
1396
1371
|
batches++;
|
|
1397
1372
|
deleted += rows.length;
|
|
1398
1373
|
if (rows.length < limit)
|
|
@@ -1401,7 +1376,7 @@ export async function deleteManyBatched(db, table, where, options) {
|
|
|
1401
1376
|
return { deleted, batches };
|
|
1402
1377
|
}
|
|
1403
1378
|
export async function transaction(db, fn) {
|
|
1404
|
-
return
|
|
1379
|
+
return asRawClient(db).begin(fn);
|
|
1405
1380
|
}
|
|
1406
1381
|
// A tx/savepoint handle has no begin(); fail closed instead of nesting silently or TypeError-ing.
|
|
1407
1382
|
export async function runInNewTransaction(db, fn) {
|
|
@@ -1411,6 +1386,5 @@ export async function runInNewTransaction(db, fn) {
|
|
|
1411
1386
|
message: "runInNewTransaction: runner has no begin() — already inside a transaction or savepoint.",
|
|
1412
1387
|
});
|
|
1413
1388
|
}
|
|
1414
|
-
|
|
1415
|
-
return raw.begin((tx) => fn(tx));
|
|
1389
|
+
return raw.begin(fn);
|
|
1416
1390
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// A slot's plaintext always wins over its ciphertext sibling, with no request
|
|
4
4
|
// made at all. Any `*_CIPHERTEXT` in the env that names no requested slot is
|
|
5
5
|
// ignored — a foreign ciphertext must never fail boot.
|
|
6
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
6
7
|
const SCALEWAY_KEY_MANAGER_API_VERSION = "v1alpha1";
|
|
7
8
|
const DEFAULT_REGION = "fr-par";
|
|
8
9
|
const DECRYPT_TIMEOUT_MS = 5_000;
|
|
@@ -48,15 +49,16 @@ async function decryptCiphertext(ciphertext, keyId, token, region, fetchImpl, lo
|
|
|
48
49
|
}
|
|
49
50
|
let body;
|
|
50
51
|
try {
|
|
51
|
-
body =
|
|
52
|
+
body = await response.json();
|
|
52
53
|
}
|
|
53
54
|
catch {
|
|
54
55
|
throw new Error(`${prefix}Key Manager decrypt for key ${keyId} returned an unparseable response`);
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
+
const plaintext = propertyOf(body, "plaintext");
|
|
58
|
+
if (typeof plaintext !== "string") {
|
|
57
59
|
throw new Error(`${prefix}Key Manager decrypt for key ${keyId} returned no plaintext field`);
|
|
58
60
|
}
|
|
59
|
-
return
|
|
61
|
+
return plaintext;
|
|
60
62
|
}
|
|
61
63
|
throw new Error(`${prefix}Key Manager decrypt failed for key ${keyId}: retries exhausted`);
|
|
62
64
|
}
|
|
@@ -14,7 +14,10 @@ export interface EncryptPiiOptions {
|
|
|
14
14
|
readonly entityName: string;
|
|
15
15
|
readonly ownerFallback?: Record<string, unknown>;
|
|
16
16
|
}
|
|
17
|
-
export
|
|
17
|
+
export type PiiEncryptedRow<TRow extends Record<string, unknown>> = {
|
|
18
|
+
[K in keyof TRow]: TRow[K] extends string ? string : TRow[K] | string;
|
|
19
|
+
};
|
|
20
|
+
export declare function encryptPiiFieldValues<TRow extends Record<string, unknown>>(row: TRow, entity: EntityDefinition, piiFields: readonly string[], kms: LocalKeyKmsAdapter, kmsCtx: KmsContext, opts: EncryptPiiOptions): Promise<PiiEncryptedRow<TRow>>;
|
|
18
21
|
export declare function serializeJsonbValue(field: string, value: unknown): string;
|
|
19
22
|
export declare function decryptPiiFieldValues(row: Record<string, unknown>, piiFields: readonly string[], kms: LocalKeyKmsAdapter, kmsCtx: KmsContext): Promise<Record<string, unknown>>;
|
|
20
23
|
export declare function configurePiiSubjectKms(adapter: KmsAdapter | undefined): void;
|
|
@@ -187,8 +187,8 @@ export async function decryptPiiFieldValues(row, piiFields, kms, kmsCtx) {
|
|
|
187
187
|
try {
|
|
188
188
|
const key = await kms.getKey(subject, kmsCtx);
|
|
189
189
|
const plain = decryptValue(key, blob, subject, name, hasAad, isJson);
|
|
190
|
-
|
|
191
|
-
out[name] =
|
|
190
|
+
const decoded = isJson ? JSON.parse(plain) : plain;
|
|
191
|
+
out[name] = decoded;
|
|
192
192
|
}
|
|
193
193
|
catch (e) {
|
|
194
194
|
// KeyNotFound deliberately propagates: ciphertext without a key row
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
// `row` null (DELETE-Statements geben keine returning-Row her).
|
|
44
44
|
// - "skipped" → Event ist kein Auto-Verb (Domain-Event auf demselben
|
|
45
45
|
// Aggregate). Caller no-op.
|
|
46
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
46
47
|
import { collectLookupableFields, computeBlindIndexValues } from "../crypto/blind-index.js";
|
|
47
48
|
import { deleteMany, insertOne, updateMany } from "../db/query.js";
|
|
48
49
|
import { InternalError } from "../errors/index.js";
|
|
@@ -54,8 +55,7 @@ export function parseAutoVerb(event) {
|
|
|
54
55
|
if (!event.type.startsWith(prefix))
|
|
55
56
|
return null;
|
|
56
57
|
const verb = event.type.slice(prefix.length);
|
|
57
|
-
|
|
58
|
-
return AUTO_VERBS.includes(verb) ? verb : null;
|
|
58
|
+
return isOneOf(AUTO_VERBS, verb) ? verb : null;
|
|
59
59
|
}
|
|
60
60
|
/** Idempotente Anwendung eines Auto-Events auf die Entity-Tabelle.
|
|
61
61
|
* Wird sowohl beim Live-Append (innerhalb der Write-TX) als auch beim
|
package/dist/db/cursor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
2
|
// String-basiert damit UUIDs (Default seit Sprint F) + Integer-Auto-Increment
|
|
2
3
|
// durch denselben Cursor-Pfad laufen. UUIDv7 erfüllt die lex-Monotonie
|
|
3
4
|
// (time-ordered Prefix); UUIDv4 nicht — wer den nutzt, kriegt inkorrekte
|
|
@@ -28,7 +29,7 @@ function parseKeysetPayload(decoded) {
|
|
|
28
29
|
catch {
|
|
29
30
|
return null;
|
|
30
31
|
}
|
|
31
|
-
if (
|
|
32
|
+
if (!isPlainObject(raw))
|
|
32
33
|
return null;
|
|
33
34
|
const { v, i } = raw;
|
|
34
35
|
if (typeof i !== "string" || i === "")
|
package/dist/db/dialect.d.ts
CHANGED
|
@@ -85,6 +85,7 @@ export type SqlExpression = {
|
|
|
85
85
|
readonly params: readonly unknown[];
|
|
86
86
|
readonly [SQL_EXPR_BRAND]: true;
|
|
87
87
|
};
|
|
88
|
+
export declare function isSqlExpression(value: unknown): value is SqlExpression;
|
|
88
89
|
export declare function sql(strings: TemplateStringsArray, ...values: readonly unknown[]): SqlExpression;
|
|
89
90
|
export declare namespace sql {
|
|
90
91
|
var raw: (text: string) => SqlExpression;
|