@cosmicdrift/kumiko-framework 0.362.0 → 0.364.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 +307 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +443 -0
- package/agents/examples/anonymous-access.md +300 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +323 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +314 -0
- package/agents/examples/custom-handlers.md +397 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +405 -0
- package/agents/examples/error-contract.md +539 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +263 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +214 -0
- package/agents/examples/ledger-invoicing.md +235 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +345 -0
- package/agents/examples/ownership.md +396 -0
- package/agents/examples/pdf-audit-export.md +270 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +259 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +410 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +74 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +14 -5
- package/dist/api/auth-routes.js +5 -3
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/redis-sse-broker.js +10 -8
- package/dist/api/routes.js +48 -19
- package/dist/api/server.d.ts +2 -1
- package/dist/api/server.js +18 -6
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +12 -5
- package/dist/bun-db/query.js +88 -94
- 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/collect-table-metas.js +11 -7
- 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/eagerload.js +2 -2
- 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.d.ts +5 -6
- package/dist/db/event-store-executor-context.js +29 -27
- package/dist/db/event-store-executor-read.js +40 -25
- package/dist/db/event-store-executor-write.js +18 -20
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +6 -4
- package/dist/db/migrate-runner.js +32 -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.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.js +2 -2
- package/dist/db/queries/pii-backfill-state.js +2 -2
- 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-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +5 -2
- package/dist/db/tenant-db.js +34 -14
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/variant-key.js +2 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +84 -35
- package/dist/engine/boot-validator/entity-list-screens.js +3 -3
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +9 -9
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +155 -96
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/changeset-changes.js +3 -3
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +11 -7
- 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/extractors/handlers.js +2 -0
- 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 +10 -8
- package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
- package/dist/engine/feature-ast/patterns.d.ts +3 -3
- package/dist/engine/feature-ast/render.js +16 -5
- 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-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/feature-ui-extensions.js +2 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +3 -2
- 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 +3 -4
- package/dist/engine/soft-delete-cleanup.js +12 -4
- package/dist/engine/steps/call-feature.js +1 -3
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
- package/dist/engine/template-variables.d.ts +32 -0
- package/dist/engine/template-variables.js +80 -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/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +22 -0
- package/dist/files/create-file-ref.js +53 -25
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +8 -0
- package/dist/files/file-routes.js +2 -2
- package/dist/files/index.d.ts +2 -2
- package/dist/files/index.js +1 -1
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/local-provider.js +2 -1
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +45 -19
- 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/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +4 -1
- package/dist/jobs/job-runner.js +13 -9
- package/dist/observability/escape-hatch-report.d.ts +1 -1
- package/dist/observability/escape-hatch-report.js +17 -5
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.js +2 -3
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +142 -34
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/event-dispatcher-delivery.js +2 -3
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +2 -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/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +28 -9
- package/dist/pipeline/system-hooks.js +2 -1
- package/dist/pipeline/system-identity-switch.js +2 -2
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/search/meilisearch-adapter.js +39 -13
- 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 +7 -5
- package/src/changes.json +251 -8
package/dist/api/sse-route.js
CHANGED
|
@@ -3,7 +3,8 @@ import { streamSSE } from "hono/streaming";
|
|
|
3
3
|
import { tenantChannel } from "../engine/constants.js";
|
|
4
4
|
import { isAnonymousSessionUser } from "../engine/system-user.js";
|
|
5
5
|
import { Routes } from "./api-constants.js";
|
|
6
|
-
import { getAuthTokenExpiry, getUser } from "./auth-middleware.js";
|
|
6
|
+
import { getAuthTokenExpiry, getStaleCredentialCode, getUser } from "./auth-middleware.js";
|
|
7
|
+
import { clearSessionCookies, getSessionCookies } from "./session-cookies.js";
|
|
7
8
|
import { accessInvalidationCredentialFor } from "./sse-broker.js";
|
|
8
9
|
import { scheduleTokenExpiry } from "./token-expiry-timer.js";
|
|
9
10
|
/**
|
|
@@ -35,6 +36,10 @@ export const SSE_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
|
35
36
|
function isEntityEventData(data) {
|
|
36
37
|
return typeof data["aggregateType"] === "string";
|
|
37
38
|
}
|
|
39
|
+
// First frame on a stream that was downgraded to anonymous because the browser's auth
|
|
40
|
+
// cookie is stale. A downgraded stream answers 200, so EventSource never reports the
|
|
41
|
+
// session end by itself.
|
|
42
|
+
const SESSION_ENDED_SSE_EVENT = "session-ended";
|
|
38
43
|
// Whitelist, never a spread: the wire frame is a signal without field
|
|
39
44
|
// values even if a broker or pusher put a payload into event.data.
|
|
40
45
|
function toEntitySignalWire(event, data) {
|
|
@@ -92,7 +97,17 @@ export function createSseRoute(broker, options) {
|
|
|
92
97
|
// JWT expiry raises no access-invalidation event, so the stream must end
|
|
93
98
|
// itself when the token it was opened with expires.
|
|
94
99
|
const tokenExpiresAtSec = getAuthTokenExpiry(c);
|
|
100
|
+
const staleCredentialCode = getStaleCredentialCode(c);
|
|
101
|
+
// JS cannot remove the HttpOnly auth cookie, so the response does.
|
|
102
|
+
if (staleCredentialCode !== undefined)
|
|
103
|
+
clearSessionCookies(c, getSessionCookies(c));
|
|
95
104
|
return streamSSE(c, async (stream) => {
|
|
105
|
+
if (staleCredentialCode !== undefined) {
|
|
106
|
+
await stream.writeSSE({
|
|
107
|
+
event: SESSION_ENDED_SSE_EVENT,
|
|
108
|
+
data: JSON.stringify({ code: staleCredentialCode }),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
96
111
|
let resolveEnded;
|
|
97
112
|
const ended = new Promise((resolve) => {
|
|
98
113
|
resolveEnded = resolve;
|
package/dist/bun-db/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { BunDbConnection, BunDbConnectionOptions, BunDbRunner, BunDbTx, PgListenClient, } from "./connection.js";
|
|
2
2
|
export { bunDbConnectionOptionsFromEnv, createBunDbConnection } from "./connection.js";
|
|
3
3
|
export type { InnerJoinRow, InnerJoinSpec, InsertOnConflictDoNothingOptions, JoinColumnPair, JsonTextMatch, SelectOptions, TableInfo, WhereObject, WhereOperator, WhereValue, } from "./query.js";
|
|
4
|
-
export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, type DeleteManyBatchedOptions, type DeleteManyBatchedResult, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, type IncrementCounterOptions, incrementCounter, insertMany, insertOnConflictDoNothing, insertOne, isTimestamptzType, requireEntityTableMeta, runInSavepoint, runInSavepointIfSupported, selectInnerJoin, selectMany, transaction, type UpsertOnConflictOptions, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
|
|
4
|
+
export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, type DeleteManyBatchedOptions, type DeleteManyBatchedResult, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, type IncrementCounterOptions, incrementCounter, insertMany, insertOnConflictDoNothing, insertOne, isTimestamptzType, requireEntityTableMeta, runInSavepoint, runInSavepointIfSupported, runnerCanBeginTransaction, selectInnerJoin, selectMany, transaction, type UpsertOnConflictOptions, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
|
package/dist/bun-db/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// bun-db: Bun.sql-basierte DB-API ohne drizzle.
|
|
2
2
|
// Production-Pfad nach drizzle-removal.
|
|
3
3
|
export { bunDbConnectionOptionsFromEnv, createBunDbConnection } from "./connection.js";
|
|
4
|
-
export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, incrementCounter, insertMany, insertOnConflictDoNothing, insertOne, isTimestamptzType, requireEntityTableMeta, runInSavepoint, runInSavepointIfSupported, selectInnerJoin, selectMany, transaction, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
|
|
4
|
+
export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, incrementCounter, insertMany, insertOnConflictDoNothing, insertOne, isTimestamptzType, requireEntityTableMeta, runInSavepoint, runInSavepointIfSupported, runnerCanBeginTransaction, selectInnerJoin, selectMany, transaction, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
|
package/dist/bun-db/query.d.ts
CHANGED
|
@@ -2,15 +2,22 @@ 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
|
|
9
|
-
type RawClient = {
|
|
8
|
+
export { asEntityTableMeta, requireEntityTableMeta };
|
|
9
|
+
export type RawClient = {
|
|
10
10
|
unsafe: <TRow = unknown>(sql: string, params?: readonly unknown[]) => Promise<readonly TRow[]>;
|
|
11
|
-
begin
|
|
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
|
};
|
|
15
|
+
export type RawClientWithBegin = RawClient & {
|
|
16
|
+
begin: NonNullable<RawClient["begin"]>;
|
|
17
|
+
};
|
|
18
|
+
export declare function hasBegin(client: RawClient): client is RawClientWithBegin;
|
|
13
19
|
export declare function asRawClient(db: unknown): RawClient;
|
|
20
|
+
export declare function runnerCanBeginTransaction(db: unknown): boolean;
|
|
14
21
|
export declare function runInSavepoint<T>(tx: unknown, fn: (sp: unknown) => Promise<T>): Promise<T>;
|
|
15
22
|
export declare function runInSavepointIfSupported<T>(db: unknown, fn: (sp: DbRunner) => Promise<T>): Promise<T>;
|
|
16
23
|
export type AnyDb = BunDbRunner | unknown;
|
|
@@ -26,7 +33,7 @@ export type TableInfo = {
|
|
|
26
33
|
readonly hasColumn: (fieldOrColumn: string) => boolean;
|
|
27
34
|
};
|
|
28
35
|
export declare function extractTableInfo(table: TableLike): TableInfo;
|
|
29
|
-
export declare function coerceRow<
|
|
36
|
+
export declare function coerceRow<TOut = Record<string, unknown>>(row: Record<string, unknown>, info: TableInfo): TOut;
|
|
30
37
|
export declare function isClosedConnectionError(err: unknown): boolean;
|
|
31
38
|
export declare function describeClosedConnectionFailure(raw: unknown, err: unknown, attempts: number): Record<string, unknown> | undefined;
|
|
32
39
|
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, 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,20 @@ 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;
|
|
52
|
+
export { asEntityTableMeta, requireEntityTableMeta };
|
|
53
|
+
export function hasBegin(client) {
|
|
54
|
+
return typeof client.begin === "function";
|
|
71
55
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!meta) {
|
|
75
|
-
throw new Error(`${tableName}: table carries no EntityTableMeta — built via table()?`);
|
|
76
|
-
}
|
|
77
|
-
return meta;
|
|
56
|
+
function isRawClient(value) {
|
|
57
|
+
return typeof propertyOf(value, "unsafe") === "function";
|
|
78
58
|
}
|
|
79
59
|
export function asRawClient(db) {
|
|
80
|
-
const dbAny = db;
|
|
81
60
|
// Direct: Bun.SQL / postgres-js Sql / postgres-js TransactionSql. All three
|
|
82
61
|
// expose `.unsafe`; only Sql/Bun.SQL have `.begin` — TransactionSql uses
|
|
83
62
|
// `.savepoint` for nested-tx. We only require `.unsafe` here; callers that
|
|
84
63
|
// need `.begin` (e.g. `transaction()`) verify it themselves.
|
|
85
|
-
if (
|
|
86
|
-
return
|
|
87
|
-
}
|
|
64
|
+
if (isRawClient(db))
|
|
65
|
+
return db;
|
|
88
66
|
// TenantDb (ctx.db) has no unfiltered escape via asRawClient anymore — fail
|
|
89
67
|
// closed instead of silently unwrapping a tenant-scoped handle.
|
|
90
68
|
if (isTenantDbShape(db)) {
|
|
@@ -93,18 +71,18 @@ export function asRawClient(db) {
|
|
|
93
71
|
"{ reason } on the handler/hook (r.systemScope() features: ctx.systemDb.unsafeRaw(reason)).");
|
|
94
72
|
}
|
|
95
73
|
// Drizzle DbConnection (legacy compat): $client = postgres-js Sql.
|
|
96
|
-
const $client =
|
|
97
|
-
if ($client
|
|
74
|
+
const $client = propertyOf(db, "$client");
|
|
75
|
+
if (isRawClient($client))
|
|
98
76
|
return $client;
|
|
99
|
-
}
|
|
100
77
|
// Drizzle pg-transaction (legacy compat): session.client = postgres-js Sql.
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
if (sessionClient && typeof sessionClient["unsafe"] === "function") {
|
|
78
|
+
const sessionClient = propertyOf(propertyOf(db, "session"), "client");
|
|
79
|
+
if (isRawClient(sessionClient))
|
|
104
80
|
return sessionClient;
|
|
105
|
-
}
|
|
106
81
|
throw new Error("bun-db: db argument has no .unsafe() — pass Bun.SQL, postgres-js Sql, or a transaction handle.");
|
|
107
82
|
}
|
|
83
|
+
export function runnerCanBeginTransaction(db) {
|
|
84
|
+
return hasBegin(asRawClient(db));
|
|
85
|
+
}
|
|
108
86
|
// postgres.js / Bun.SQL reject the WHOLE begin() when any statement inside
|
|
109
87
|
// it errored — even if the JS error was caught (the driver tracks per-block
|
|
110
88
|
// query failures). Error confinement therefore must go through the driver's
|
|
@@ -131,7 +109,7 @@ export async function runInSavepoint(tx, fn) {
|
|
|
131
109
|
// to paper over by re-running `fn` without the savepoint's isolation.
|
|
132
110
|
export async function runInSavepointIfSupported(db, fn) {
|
|
133
111
|
const raw = asRawClient(db);
|
|
134
|
-
// @cast-boundary
|
|
112
|
+
// @cast-boundary db-runner savepoint handle → DbRunner at the single seam
|
|
135
113
|
const asRunner = (sp) => sp;
|
|
136
114
|
if (typeof raw.savepoint !== "function")
|
|
137
115
|
return fn(asRunner(db));
|
|
@@ -156,15 +134,14 @@ export async function runInSavepointIfSupported(db, fn) {
|
|
|
156
134
|
function isTenantDbShape(db) {
|
|
157
135
|
if (typeof db !== "object" || db === null)
|
|
158
136
|
return false;
|
|
159
|
-
|
|
160
|
-
if (typeof d["unsafe"] === "function")
|
|
137
|
+
if (typeof propertyOf(db, "unsafe") === "function")
|
|
161
138
|
return false;
|
|
162
|
-
return (typeof
|
|
163
|
-
typeof
|
|
164
|
-
typeof
|
|
165
|
-
typeof
|
|
166
|
-
typeof
|
|
167
|
-
"tenantId" in
|
|
139
|
+
return (typeof propertyOf(db, "selectMany") === "function" &&
|
|
140
|
+
typeof propertyOf(db, "fetchOne") === "function" &&
|
|
141
|
+
typeof propertyOf(db, "insertOne") === "function" &&
|
|
142
|
+
typeof propertyOf(db, "updateMany") === "function" &&
|
|
143
|
+
typeof propertyOf(db, "deleteMany") === "function" &&
|
|
144
|
+
"tenantId" in db);
|
|
168
145
|
}
|
|
169
146
|
function tenantDbDelegate(db) {
|
|
170
147
|
return isTenantDbShape(db) ? db : undefined;
|
|
@@ -182,6 +159,9 @@ function assertNotTenantScoped(db, fnName) {
|
|
|
182
159
|
const WHERE_OPERATOR_KEYS = ["gt", "gte", "lt", "lte", "ne", "in", "like", "jsonText"];
|
|
183
160
|
const _whereOperatorKeysExhaustive = true;
|
|
184
161
|
void _whereOperatorKeysExhaustive;
|
|
162
|
+
function isJsonTextMatchList(value) {
|
|
163
|
+
return Array.isArray(value);
|
|
164
|
+
}
|
|
185
165
|
function isWhereOperator(v) {
|
|
186
166
|
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
187
167
|
return false;
|
|
@@ -190,7 +170,7 @@ function isWhereOperator(v) {
|
|
|
190
170
|
const keys = Object.keys(v);
|
|
191
171
|
if (keys.length === 0)
|
|
192
172
|
return false;
|
|
193
|
-
return keys.every((k) => WHERE_OPERATOR_KEYS
|
|
173
|
+
return keys.every((k) => isOneOf(WHERE_OPERATOR_KEYS, k));
|
|
194
174
|
}
|
|
195
175
|
export function extractTableInfo(table) {
|
|
196
176
|
const meta = asEntityTableMeta(table);
|
|
@@ -236,10 +216,13 @@ function quoteIdent(name) {
|
|
|
236
216
|
//
|
|
237
217
|
// Symmetric on write: Temporal.Instant doesn't bind directly into postgres-js
|
|
238
218
|
// params — convert to ISO string when the column pgType is timestamptz.
|
|
219
|
+
function toStringTagOf(v) {
|
|
220
|
+
return typeof v === "object" && v !== null && Symbol.toStringTag in v
|
|
221
|
+
? v[Symbol.toStringTag]
|
|
222
|
+
: undefined;
|
|
223
|
+
}
|
|
239
224
|
function isTemporalInstant(v) {
|
|
240
|
-
return (
|
|
241
|
-
v !== null &&
|
|
242
|
-
v[Symbol.toStringTag] === "Temporal.Instant");
|
|
225
|
+
return toStringTagOf(v) === "Temporal.Instant";
|
|
243
226
|
}
|
|
244
227
|
// Postgres bigint → JS: safe integers become `number` (matches createBigIntField /
|
|
245
228
|
// drizzle mode:"number"); values outside ±2^53 stay `bigint` (money cents, raw
|
|
@@ -289,15 +272,14 @@ function instantFromDriver(value) {
|
|
|
289
272
|
return null;
|
|
290
273
|
}
|
|
291
274
|
function isTemporalPlainDate(v) {
|
|
292
|
-
return (
|
|
293
|
-
v !== null &&
|
|
294
|
-
v[Symbol.toStringTag] === "Temporal.PlainDate");
|
|
275
|
+
return toStringTagOf(v) === "Temporal.PlainDate";
|
|
295
276
|
}
|
|
296
277
|
function isTemporalPlainDateTimeLike(v) {
|
|
297
|
-
|
|
278
|
+
const tag = toStringTagOf(v);
|
|
279
|
+
return ((tag === "Temporal.PlainDateTime" || tag === "Temporal.ZonedDateTime") &&
|
|
280
|
+
typeof v === "object" &&
|
|
298
281
|
v !== null &&
|
|
299
|
-
|
|
300
|
-
v[Symbol.toStringTag] === "Temporal.ZonedDateTime") &&
|
|
282
|
+
"toPlainDate" in v &&
|
|
301
283
|
typeof v.toPlainDate === "function");
|
|
302
284
|
}
|
|
303
285
|
// Verified empirically against Bun.SQL (kumiko-framework#1924): a `date`
|
|
@@ -353,6 +335,10 @@ function plainDateFromDriver(value, column) {
|
|
|
353
335
|
//
|
|
354
336
|
// Unknown columns (computed/aliased) pass through unchanged.
|
|
355
337
|
export function coerceRow(row, info) {
|
|
338
|
+
// @cast-boundary db-row — the driver row (raw column names/types) becomes the caller's entity row shape here, and only here
|
|
339
|
+
return coerceDriverRow(row, info);
|
|
340
|
+
}
|
|
341
|
+
function coerceDriverRow(row, info) {
|
|
356
342
|
const out = {};
|
|
357
343
|
let changed = false;
|
|
358
344
|
for (const key of Object.keys(row)) {
|
|
@@ -422,7 +408,7 @@ export function coerceRow(row, info) {
|
|
|
422
408
|
changed = true;
|
|
423
409
|
out[fieldName] = coerced;
|
|
424
410
|
}
|
|
425
|
-
return
|
|
411
|
+
return changed ? out : row;
|
|
426
412
|
}
|
|
427
413
|
function coerceRows(rows, info) {
|
|
428
414
|
return rows.map((r) => coerceRow(r, info));
|
|
@@ -568,9 +554,15 @@ function buildWhereClause(info, where, startIndex, qualifier) {
|
|
|
568
554
|
if (pgType !== "jsonb") {
|
|
569
555
|
throw new Error(`where: jsonText requires a jsonb column, "${col}" is ${pgType}`);
|
|
570
556
|
}
|
|
571
|
-
const matches =
|
|
557
|
+
const matches = isJsonTextMatchList(value.jsonText) ? value.jsonText : [value.jsonText];
|
|
558
|
+
if (matches.length === 0) {
|
|
559
|
+
conditions.push("FALSE");
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
572
562
|
for (const match of matches) {
|
|
573
|
-
if (
|
|
563
|
+
if (match === null ||
|
|
564
|
+
typeof match !== "object" ||
|
|
565
|
+
!Array.isArray(match.keys) ||
|
|
574
566
|
match.keys.length === 0 ||
|
|
575
567
|
!match.keys.every((k) => typeof k === "string") ||
|
|
576
568
|
typeof match.eq !== "string") {
|
|
@@ -599,7 +591,7 @@ function buildWhereClause(info, where, startIndex, qualifier) {
|
|
|
599
591
|
like: "LIKE",
|
|
600
592
|
};
|
|
601
593
|
for (const [opKey, opSym] of Object.entries(opMap)) {
|
|
602
|
-
const opVal = value
|
|
594
|
+
const opVal = Reflect.get(value, opKey);
|
|
603
595
|
if (opVal === undefined)
|
|
604
596
|
continue;
|
|
605
597
|
// Mirrors applyMultiSelectFilter: ordering/pattern ops have no meaning on a jsonb
|
|
@@ -638,7 +630,7 @@ function buildWhereClause(info, where, startIndex, qualifier) {
|
|
|
638
630
|
values.push(p.bound);
|
|
639
631
|
}
|
|
640
632
|
}
|
|
641
|
-
const inVal = value
|
|
633
|
+
const inVal = value.in;
|
|
642
634
|
if (Array.isArray(inVal)) {
|
|
643
635
|
if (pgType === "jsonb") {
|
|
644
636
|
conditions.push(jsonbContainsAny(col, inVal));
|
|
@@ -711,11 +703,9 @@ export function isClosedConnectionError(err) {
|
|
|
711
703
|
function isPooledClient(raw) {
|
|
712
704
|
if (raw === null || (typeof raw !== "object" && typeof raw !== "function"))
|
|
713
705
|
return false;
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
typeof r.savepoint !== "function" &&
|
|
718
|
-
typeof r.release !== "function");
|
|
706
|
+
return (typeof propertyOf(raw, "begin") === "function" &&
|
|
707
|
+
typeof propertyOf(raw, "savepoint") !== "function" &&
|
|
708
|
+
typeof propertyOf(raw, "release") !== "function");
|
|
719
709
|
}
|
|
720
710
|
const closedConnectionLog = createFallbackLogger("bun-db");
|
|
721
711
|
// The driver rejects in-flight queries with this AbortError (no code) when the
|
|
@@ -803,8 +793,7 @@ async function unsafeLoggingClosedConnection(db, sqlText, params, context) {
|
|
|
803
793
|
}
|
|
804
794
|
const DEFAULT_POOL_MAX = 10;
|
|
805
795
|
function poolMaxOf(raw) {
|
|
806
|
-
|
|
807
|
-
const max = raw.options?.max;
|
|
796
|
+
const max = propertyOf(propertyOf(raw, "options"), "max");
|
|
808
797
|
return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : DEFAULT_POOL_MAX;
|
|
809
798
|
}
|
|
810
799
|
// Exported so raw-SQL query modules outside bun-db (e.g. bundled-features'
|
|
@@ -814,7 +803,7 @@ function poolMaxOf(raw) {
|
|
|
814
803
|
export async function unsafeReadRetrying(db, sqlText, params) {
|
|
815
804
|
const raw = asRawClient(db);
|
|
816
805
|
try {
|
|
817
|
-
return
|
|
806
|
+
return await raw.unsafe(sqlText, params);
|
|
818
807
|
}
|
|
819
808
|
catch (err) {
|
|
820
809
|
if (!isPooledClient(raw) || !isClosedConnectionError(err)) {
|
|
@@ -826,7 +815,7 @@ export async function unsafeReadRetrying(db, sqlText, params) {
|
|
|
826
815
|
let lastErr = err;
|
|
827
816
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
828
817
|
try {
|
|
829
|
-
return
|
|
818
|
+
return await raw.unsafe(sqlText, params);
|
|
830
819
|
}
|
|
831
820
|
catch (retryErr) {
|
|
832
821
|
lastErr = retryErr;
|
|
@@ -872,7 +861,7 @@ export async function selectMany(db, table, where, options) {
|
|
|
872
861
|
}
|
|
873
862
|
sqlText += ` LIMIT ${options.limit}`;
|
|
874
863
|
}
|
|
875
|
-
const raw =
|
|
864
|
+
const raw = await unsafeReadRetrying(db, sqlText, values);
|
|
876
865
|
return coerceRows(raw, info);
|
|
877
866
|
}
|
|
878
867
|
// biome-ignore lint/suspicious/noExplicitAny: see selectMany default
|
|
@@ -937,7 +926,7 @@ export async function selectInnerJoin(db, spec) {
|
|
|
937
926
|
sqlText += ` WHERE ${conditions.join(" AND ")}`;
|
|
938
927
|
if (spec.limit !== undefined)
|
|
939
928
|
sqlText += ` LIMIT ${spec.limit}`;
|
|
940
|
-
const raw =
|
|
929
|
+
const raw = await unsafeReadRetrying(db, sqlText, values);
|
|
941
930
|
return raw.map((row) => {
|
|
942
931
|
const leftRow = {};
|
|
943
932
|
const rightRow = {};
|
|
@@ -987,10 +976,10 @@ export async function insertMany(db, table, rows) {
|
|
|
987
976
|
valuesClauses.push(`(${placeholders.join(", ")})`);
|
|
988
977
|
}
|
|
989
978
|
const sqlText = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES ${valuesClauses.join(", ")} RETURNING *`;
|
|
990
|
-
const raw =
|
|
979
|
+
const raw = await unsafeLoggingClosedConnection(db, sqlText, params, {
|
|
991
980
|
operation: "insertMany",
|
|
992
981
|
table: info.name,
|
|
993
|
-
})
|
|
982
|
+
});
|
|
994
983
|
return coerceRows(raw, info);
|
|
995
984
|
}
|
|
996
985
|
// biome-ignore lint/suspicious/noExplicitAny: see selectMany default
|
|
@@ -1020,10 +1009,10 @@ export async function insertOne(db, table, values) {
|
|
|
1020
1009
|
})
|
|
1021
1010
|
.join(", ");
|
|
1022
1011
|
const sqlText = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES (${placeholders}) RETURNING *`;
|
|
1023
|
-
const rows =
|
|
1012
|
+
const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
|
|
1024
1013
|
operation: "insertOne",
|
|
1025
1014
|
table: info.name,
|
|
1026
|
-
})
|
|
1015
|
+
});
|
|
1027
1016
|
const first = rows[0];
|
|
1028
1017
|
if (!first)
|
|
1029
1018
|
return undefined;
|
|
@@ -1063,10 +1052,10 @@ export async function updateMany(db, table, set, where) {
|
|
|
1063
1052
|
values.push(v);
|
|
1064
1053
|
}
|
|
1065
1054
|
sqlText += " RETURNING *";
|
|
1066
|
-
const raw =
|
|
1055
|
+
const raw = await unsafeLoggingClosedConnection(db, sqlText, values, {
|
|
1067
1056
|
operation: "updateMany",
|
|
1068
1057
|
table: info.name,
|
|
1069
|
-
})
|
|
1058
|
+
});
|
|
1070
1059
|
return coerceRows(raw, info);
|
|
1071
1060
|
}
|
|
1072
1061
|
export async function deleteMany(db, table, where) {
|
|
@@ -1135,7 +1124,7 @@ export async function countWhere(db, table, where = {}) {
|
|
|
1135
1124
|
sqlText += ` WHERE ${w.sqlText}`;
|
|
1136
1125
|
values = w.values;
|
|
1137
1126
|
}
|
|
1138
|
-
const rows =
|
|
1127
|
+
const rows = await unsafeReadRetrying(db, sqlText, values);
|
|
1139
1128
|
return rows[0]?.count ?? 0;
|
|
1140
1129
|
}
|
|
1141
1130
|
const AGGREGATE_BUCKET_UNITS = ["hour", "day"];
|
|
@@ -1171,7 +1160,7 @@ function aggregateMeasureSql(info, measure) {
|
|
|
1171
1160
|
case "avg":
|
|
1172
1161
|
return `AVG(${requireAggregateColumn(info, measure.field, "measure field")})`;
|
|
1173
1162
|
default:
|
|
1174
|
-
throw new Error(`aggregateWhere: unknown measure fn "${String(measure
|
|
1163
|
+
throw new Error(`aggregateWhere: unknown measure fn "${String(Reflect.get(measure, "fn"))}"`);
|
|
1175
1164
|
}
|
|
1176
1165
|
}
|
|
1177
1166
|
function aggregateDimensionSql(info, dimension, index, params) {
|
|
@@ -1233,7 +1222,7 @@ export async function aggregateWhere(db, table, spec, where = {}) {
|
|
|
1233
1222
|
params.push(...w.values);
|
|
1234
1223
|
}
|
|
1235
1224
|
sqlText += aggregateGroupOrderLimitSql(spec, dimensions.length);
|
|
1236
|
-
const raw =
|
|
1225
|
+
const raw = await unsafeReadRetrying(db, sqlText, params);
|
|
1237
1226
|
return raw.map((row) => {
|
|
1238
1227
|
const value = row["value"];
|
|
1239
1228
|
return {
|
|
@@ -1277,10 +1266,10 @@ export async function upsertOnConflict(db, table, values, options) {
|
|
|
1277
1266
|
}
|
|
1278
1267
|
const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
|
|
1279
1268
|
const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO UPDATE SET ${updateParts.join(", ")} RETURNING *`;
|
|
1280
|
-
const rows =
|
|
1269
|
+
const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
|
|
1281
1270
|
operation: "upsertOnConflict",
|
|
1282
1271
|
table: info.name,
|
|
1283
|
-
})
|
|
1272
|
+
});
|
|
1284
1273
|
const first = rows[0];
|
|
1285
1274
|
if (!first)
|
|
1286
1275
|
return undefined;
|
|
@@ -1305,10 +1294,10 @@ export async function insertOnConflictDoNothing(db, table, values, options) {
|
|
|
1305
1294
|
const { sqlPrefix, params } = buildInsertSql(info, entries);
|
|
1306
1295
|
const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
|
|
1307
1296
|
const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO NOTHING RETURNING *`;
|
|
1308
|
-
const rows =
|
|
1297
|
+
const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
|
|
1309
1298
|
operation: "insertOnConflictDoNothing",
|
|
1310
1299
|
table: info.name,
|
|
1311
|
-
})
|
|
1300
|
+
});
|
|
1312
1301
|
const first = rows[0];
|
|
1313
1302
|
if (!first)
|
|
1314
1303
|
return undefined;
|
|
@@ -1357,10 +1346,10 @@ export async function incrementCounter(db, table, values, increments, options =
|
|
|
1357
1346
|
}
|
|
1358
1347
|
const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
|
|
1359
1348
|
const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO UPDATE SET ${updateParts.join(", ")} RETURNING *`;
|
|
1360
|
-
const rows =
|
|
1349
|
+
const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
|
|
1361
1350
|
operation: "incrementCounter",
|
|
1362
1351
|
table: info.name,
|
|
1363
|
-
})
|
|
1352
|
+
});
|
|
1364
1353
|
const first = rows[0];
|
|
1365
1354
|
if (!first)
|
|
1366
1355
|
return undefined;
|
|
@@ -1389,10 +1378,10 @@ export async function deleteManyBatched(db, table, where, options) {
|
|
|
1389
1378
|
const sqlText = `DELETE FROM ${tableQ} WHERE ${idQ} IN (
|
|
1390
1379
|
SELECT ${idQ} FROM ${tableQ} WHERE ${w.sqlText} LIMIT $${limitIdx}
|
|
1391
1380
|
) RETURNING ${idQ}`;
|
|
1392
|
-
const rows =
|
|
1381
|
+
const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
|
|
1393
1382
|
operation: "deleteManyBatched",
|
|
1394
1383
|
table: info.name,
|
|
1395
|
-
})
|
|
1384
|
+
});
|
|
1396
1385
|
batches++;
|
|
1397
1386
|
deleted += rows.length;
|
|
1398
1387
|
if (rows.length < limit)
|
|
@@ -1401,16 +1390,21 @@ export async function deleteManyBatched(db, table, where, options) {
|
|
|
1401
1390
|
return { deleted, batches };
|
|
1402
1391
|
}
|
|
1403
1392
|
export async function transaction(db, fn) {
|
|
1404
|
-
|
|
1393
|
+
const raw = asRawClient(db);
|
|
1394
|
+
if (!hasBegin(raw)) {
|
|
1395
|
+
throw new InternalError({
|
|
1396
|
+
message: "transaction: runner has no begin() — already inside a transaction or savepoint.",
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
return raw.begin(fn);
|
|
1405
1400
|
}
|
|
1406
1401
|
// A tx/savepoint handle has no begin(); fail closed instead of nesting silently or TypeError-ing.
|
|
1407
1402
|
export async function runInNewTransaction(db, fn) {
|
|
1408
1403
|
const raw = asRawClient(db);
|
|
1409
|
-
if (
|
|
1404
|
+
if (!hasBegin(raw)) {
|
|
1410
1405
|
throw new InternalError({
|
|
1411
1406
|
message: "runInNewTransaction: runner has no begin() — already inside a transaction or savepoint.",
|
|
1412
1407
|
});
|
|
1413
1408
|
}
|
|
1414
|
-
|
|
1415
|
-
return raw.begin((tx) => fn(tx));
|
|
1409
|
+
return raw.begin(fn);
|
|
1416
1410
|
}
|
|
@@ -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
|
|
@@ -15,6 +15,15 @@ function canonicalColumnsKey(meta) {
|
|
|
15
15
|
.map((c) => `${c.name}|${c.pgType}|${c.notNull}|${c.defaultSql ?? ""}|${c.primaryKey ?? false}|${c.identity ?? false}|${c.bigintJsMode ?? ""}`)
|
|
16
16
|
.join("\n");
|
|
17
17
|
}
|
|
18
|
+
function registerPass1Meta(metas, byName, meta, origin) {
|
|
19
|
+
const existing = byName.get(meta.tableName);
|
|
20
|
+
if (existing) {
|
|
21
|
+
throw new Error(`collectTableMetas: table "${meta.tableName}" is registered by ${origin} and ${existing.origin}. ` +
|
|
22
|
+
"Give one of them a different table name.");
|
|
23
|
+
}
|
|
24
|
+
metas.push(meta);
|
|
25
|
+
byName.set(meta.tableName, { meta, origin });
|
|
26
|
+
}
|
|
18
27
|
export function collectTableMetas(features) {
|
|
19
28
|
const metas = [];
|
|
20
29
|
const byName = new Map();
|
|
@@ -37,15 +46,10 @@ export function collectTableMetas(features) {
|
|
|
37
46
|
assertBackingTableSuperset(name, fieldMeta, tableMeta);
|
|
38
47
|
meta = tableMeta;
|
|
39
48
|
}
|
|
40
|
-
metas.
|
|
41
|
-
byName.set(meta.tableName, { meta, origin: `entity "${name}" (${feature.name})` });
|
|
49
|
+
registerPass1Meta(metas, byName, meta, `entity "${name}" (${feature.name})`);
|
|
42
50
|
}
|
|
43
51
|
for (const entry of Object.values(feature.storeTables)) {
|
|
44
|
-
metas.
|
|
45
|
-
byName.set(entry.meta.tableName, {
|
|
46
|
-
meta: entry.meta,
|
|
47
|
-
origin: `storeTable "${entry.name}" (${feature.name})`,
|
|
48
|
-
});
|
|
52
|
+
registerPass1Meta(metas, byName, entry.meta, `storeTable "${entry.name}" (${feature.name})`);
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
// Pass 2: table-tragende Registrierungen. Zwei Pässe, damit eine Entity-
|
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 === "")
|