@cosmicdrift/kumiko-framework 0.366.0 → 0.368.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 +1 -7
- package/README.md +1 -1
- package/agents/examples/access-control.md +2 -2
- package/agents/examples/anonymous-access-multitenant.md +2 -2
- package/agents/examples/anonymous-access.md +3 -3
- package/agents/examples/apex-landing.md +1 -1
- package/agents/examples/apex-surface-auth.md +6 -7
- package/agents/examples/api-worker-split.md +7 -8
- package/agents/examples/auth-claims.md +2 -1
- package/agents/examples/auth-foundation-providers.md +3 -3
- package/agents/examples/auth-mfa.md +3 -4
- package/agents/examples/basic-entity.md +3 -4
- package/agents/examples/chat-channels.md +4 -4
- package/agents/examples/compliance-profiles-demo.md +2 -3
- package/agents/examples/cross-feature-events.md +4 -4
- package/agents/examples/crypto-shredding-hr.md +14 -9
- package/agents/examples/currencies-global.md +3 -4
- package/agents/examples/currencies-per-tenant.md +3 -4
- package/agents/examples/custom-fields-basic.md +2 -3
- package/agents/examples/custom-handlers.md +3 -5
- package/agents/examples/dashboard-stats.md +2 -3
- package/agents/examples/delivery-notifications.md +5 -6
- package/agents/examples/e2e-generator.md +1 -1
- package/agents/examples/embedded-entity-form.md +2 -3
- package/agents/examples/embedded.md +2 -3
- package/agents/examples/encrypted-tenant-config.md +5 -6
- package/agents/examples/error-contract.md +3 -4
- package/agents/examples/event-sourcing.md +19 -23
- package/agents/examples/feature-toggles.md +4 -4
- package/agents/examples/field-access.md +3 -4
- package/agents/examples/files-post-processing.md +12 -13
- package/agents/examples/golden-path.md +208 -0
- package/agents/examples/idempotency.md +2 -2
- package/agents/examples/inbound-mail-triage.md +7 -7
- package/agents/examples/lane-routing.md +3 -3
- package/agents/examples/ledger-banking.md +2 -3
- package/agents/examples/ledger-invoicing.md +2 -3
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +3 -3
- package/agents/examples/managed-config.md +3 -4
- package/agents/examples/managed-pages.md +2 -2
- package/agents/examples/notes-history-basic.md +4 -5
- package/agents/examples/ownership.md +3 -5
- package/agents/examples/pdf-audit-export.md +2 -3
- package/agents/examples/pipeline-basics.md +2 -2
- package/agents/examples/public-share-token.md +3 -3
- package/agents/examples/rate-limiting.md +2 -1
- package/agents/examples/realtime-sse.md +2 -3
- package/agents/examples/record-detail-layout.md +1 -1
- package/agents/examples/reference-data.md +3 -3
- package/agents/examples/relations.md +2 -2
- package/agents/examples/screens-nav.md +1 -1
- package/agents/examples/search.md +2 -2
- package/agents/examples/secrets-demo.md +8 -8
- package/agents/examples/security-baseline.md +2 -2
- package/agents/examples/session-revocation.md +7 -8
- package/agents/examples/state-machine.md +181 -165
- package/agents/examples/tags-basic.md +2 -3
- package/agents/examples/tenant-isolation.md +2 -3
- package/agents/examples/tenant-settings.md +2 -2
- package/agents/examples/tier-admin.md +3 -4
- package/agents/examples/timezones.md +3 -3
- package/agents/examples/user-data-rights.md +22 -33
- package/agents/examples/user-profile.md +3 -3
- package/agents/examples/webhook-step.md +2 -3
- package/agents/examples/wizard-form.md +4 -5
- package/agents/examples/workflow-engine.md +7 -3
- package/agents/examples/writeform-section.md +1 -1
- package/agents/examples.md +1 -0
- package/agents/features.md +4 -4
- package/dist/consumer-cli.js +2 -3
- package/dist/crypto/index.d.ts +1 -1
- package/dist/crypto/index.js +1 -1
- package/dist/crypto/owner-transition-rekey.js +2 -0
- package/dist/crypto/pg-kms-adapter.js +1 -1
- package/dist/crypto/subject-resolver.d.ts +2 -0
- package/dist/crypto/subject-resolver.js +30 -15
- package/dist/db/bun-db-deprecated.d.ts +10 -0
- package/dist/{bun-db/index.js → db/bun-db-deprecated.js} +4 -3
- package/dist/db/config-seed.js +1 -1
- package/dist/db/connection.d.ts +23 -5
- package/dist/db/connection.js +10 -5
- package/dist/db/dialect.js +1 -1
- package/dist/db/entity-field-encryption.d.ts +5 -0
- package/dist/db/entity-field-encryption.js +29 -0
- package/dist/db/entity-table-meta.js +6 -5
- package/dist/db/event-store-executor-read.js +26 -5
- package/dist/db/index.d.ts +4 -4
- package/dist/db/index.js +1 -1
- package/dist/db/migrate-runner.js +1 -1
- package/dist/db/queries/backfill-pii.js +16 -9
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/event-store.d.ts +2 -0
- package/dist/db/queries/event-store.js +26 -0
- package/dist/db/queries/seed-context.js +3 -3
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/query.d.ts +105 -1
- package/dist/db/query.js +1450 -3
- package/dist/{stack/table-helpers.d.ts → db/table-push.d.ts} +5 -15
- package/dist/{stack/table-helpers.js → db/table-push.js} +6 -46
- package/dist/db/tenant-db.d.ts +11 -0
- package/dist/db/tenant-db.js +51 -12
- package/dist/derivatives/derivatives-context.d.ts +1 -1
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/gdpr-storage.js +3 -3
- package/dist/engine/boot-validator/pii-retention.js +3 -3
- package/dist/engine/boot-validator/record-owned.js +3 -3
- package/dist/engine/boot-validator/screens.js +8 -1
- package/dist/engine/bridge-stub.d.ts +4 -0
- package/dist/engine/bridge-stub.js +62 -0
- package/dist/engine/event-helpers.d.ts +2 -1
- package/dist/engine/event-helpers.js +1 -0
- package/dist/engine/feature-config-events-jobs.d.ts +4 -0
- package/dist/engine/feature-config-events-jobs.js +4 -2
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +1 -0
- package/dist/engine/registry-ingest.js +3 -0
- package/dist/engine/template-variables.d.ts +3 -0
- package/dist/engine/template-variables.js +3 -0
- package/dist/es-ops/operations-schema.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/archive.js +1 -1
- package/dist/event-store/errors.d.ts +2 -1
- package/dist/event-store/errors.js +6 -2
- package/dist/event-store/event-store.js +17 -9
- package/dist/event-store/events-schema.js +1 -1
- package/dist/event-store/rebuild-dead-letter.js +3 -3
- package/dist/event-store/snapshot.js +1 -1
- package/dist/event-store/upcaster-dead-letter.js +3 -3
- package/dist/files/create-file-ref-from-bytes.d.ts +1 -0
- package/dist/files/create-file-ref-from-bytes.js +8 -3
- package/dist/files/file-routes.js +1 -1
- package/dist/internal/stack-support.d.ts +12 -0
- package/dist/internal/stack-support.js +12 -0
- package/dist/internal/testing.d.ts +4 -0
- package/dist/internal/testing.js +5 -0
- package/dist/jobs/job-runner.js +23 -11
- package/dist/migrations/pending-rebuilds.js +1 -1
- package/dist/migrations/pii-event-backfill.js +2 -2
- package/dist/pipeline/dispatch-shared.js +43 -23
- package/dist/pipeline/dispatch-write.js +6 -2
- package/dist/pipeline/dispatcher-utils.js +6 -2
- package/dist/pipeline/event-consumer-state.js +1 -1
- package/dist/pipeline/projection-rebuild.d.ts +1 -0
- package/dist/pipeline/projection-rebuild.js +1 -1
- package/dist/pipeline/projection-state.js +1 -1
- package/dist/pipeline/system-hooks.js +1 -1
- package/dist/schema-cli.js +3 -3
- package/dist/search/purge-subject.js +20 -10
- package/dist/seeding/index.d.ts +1 -0
- package/dist/seeding/index.js +1 -0
- package/dist/stack/index.d.ts +10 -9
- package/dist/stack/index.js +11 -19
- package/dist/testing/e2e-generator.d.ts +5 -49
- package/dist/testing/e2e-generator.js +5 -382
- package/dist/testing/handler-context.d.ts +5 -4
- package/dist/testing/handler-context.js +5 -69
- package/dist/testing/index.d.ts +10 -26
- package/dist/testing/index.js +10 -34
- package/dist/testing/real-providers.d.ts +5 -3
- package/dist/testing/real-providers.js +5 -21
- package/package.json +37 -9
- package/src/changes.json +70 -0
- package/src/scripts/codemod/README.md +43 -0
- package/src/scripts/codemod/codemod-support.ts +276 -0
- package/src/scripts/codemod/db-factory.ts +810 -0
- package/src/scripts/codemod/feature-factory.ts +429 -0
- package/src/scripts/codemod/s3-imports.ts +274 -0
- package/src/scripts/codemod/tenant-db-options.ts +454 -0
- package/src/scripts/codemod/testing-move-targets.ts +161 -0
- package/src/scripts/codemod/testing-move.ts +520 -0
- package/dist/bun-db/connection.d.ts +0 -17
- package/dist/bun-db/connection.js +0 -40
- package/dist/bun-db/index.d.ts +0 -4
- package/dist/bun-db/query.d.ts +0 -106
- package/dist/bun-db/query.js +0 -1457
- package/dist/db/api.d.ts +0 -19
- package/dist/db/api.js +0 -23
- package/dist/db/bun-provider.d.ts +0 -2
- package/dist/db/bun-provider.js +0 -28
- package/dist/db/postgres-provider.d.ts +0 -2
- package/dist/db/postgres-provider.js +0 -21
- package/dist/db/query-api.d.ts +0 -2
- package/dist/db/query-api.js +0 -6
- package/dist/stack/db.d.ts +0 -22
- package/dist/stack/db.js +0 -67
- package/dist/stack/drain-event-consumers.d.ts +0 -4
- package/dist/stack/drain-event-consumers.js +0 -51
- package/dist/stack/drain-jobs.d.ts +0 -22
- package/dist/stack/drain-jobs.js +0 -141
- package/dist/stack/event-collector.d.ts +0 -9
- package/dist/stack/event-collector.js +0 -12
- package/dist/stack/push-entity-projection-tables.d.ts +0 -24
- package/dist/stack/push-entity-projection-tables.js +0 -74
- package/dist/stack/redis.d.ts +0 -17
- package/dist/stack/redis.js +0 -52
- package/dist/stack/request-helper.d.ts +0 -35
- package/dist/stack/request-helper.js +0 -144
- package/dist/stack/test-stack.d.ts +0 -192
- package/dist/stack/test-stack.js +0 -429
- package/dist/testing/access-assertions.d.ts +0 -2
- package/dist/testing/access-assertions.js +0 -13
- package/dist/testing/assertions.d.ts +0 -10
- package/dist/testing/assertions.js +0 -22
- package/dist/testing/boot-validator-fixture.d.ts +0 -3
- package/dist/testing/boot-validator-fixture.js +0 -101
- package/dist/testing/closed-connection-error.d.ts +0 -7
- package/dist/testing/closed-connection-error.js +0 -46
- package/dist/testing/db-cleanup.d.ts +0 -9
- package/dist/testing/db-cleanup.js +0 -31
- package/dist/testing/expect-error.d.ts +0 -2
- package/dist/testing/expect-error.js +0 -22
- package/dist/testing/file-provider-contract.d.ts +0 -2
- package/dist/testing/file-provider-contract.js +0 -102
- package/dist/testing/http-cookies.d.ts +0 -7
- package/dist/testing/http-cookies.js +0 -47
- package/dist/testing/late-bound.d.ts +0 -9
- package/dist/testing/late-bound.js +0 -31
- package/dist/testing/manual-clock.d.ts +0 -14
- package/dist/testing/manual-clock.js +0 -12
- package/dist/testing/multipart-helper.d.ts +0 -22
- package/dist/testing/multipart-helper.js +0 -77
- package/dist/testing/mutable-master-key-provider.d.ts +0 -7
- package/dist/testing/mutable-master-key-provider.js +0 -35
- package/dist/testing/observability-recorder.d.ts +0 -9
- package/dist/testing/observability-recorder.js +0 -29
- package/dist/testing/seed.d.ts +0 -9
- package/dist/testing/seed.js +0 -21
- package/dist/testing/shared-entities.d.ts +0 -286
- package/dist/testing/shared-entities.js +0 -56
- package/dist/testing/utils.d.ts +0 -1
- package/dist/testing/utils.js +0 -1
- package/dist/testing/wait-for.d.ts +0 -20
- package/dist/testing/wait-for.js +0 -47
- package/dist/testing/without-ambient-temporal.d.ts +0 -5
- package/dist/testing/without-ambient-temporal.js +0 -17
- package/dist/testing/zip-fixture.d.ts +0 -1
- package/dist/testing/zip-fixture.js +0 -33
- /package/dist/{stack/test-users.d.ts → seeding/seed-users.d.ts} +0 -0
- /package/dist/{stack/test-users.js → seeding/seed-users.js} +0 -0
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
import { isPersonalDataGated, UNATTRIBUTED_ORIGIN, } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
3
3
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
4
4
|
import { requestContext } from "../api/request-context.js";
|
|
5
|
-
import { instantToTimestamptzParam } from "../bun-db/query.js";
|
|
6
5
|
import { encryptEventPayloadPii } from "../crypto/event-pii.js";
|
|
7
6
|
import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
|
|
8
|
-
import { claimXactId, insertSubsequentEventRow, notifyPgChannel, runAtomicEventWrite, selectAggregateMaxVersion, selectEventsHighWaterMark, selectStreamMaxVersion, selectStreamMaxVersions, } from "../db/queries/event-store.js";
|
|
9
|
-
import {
|
|
7
|
+
import { claimXactId, insertFirstEventRow, insertSubsequentEventRow, notifyPgChannel, runAtomicEventWrite, selectAggregateMaxVersion, selectEventsHighWaterMark, selectStreamMaxVersion, selectStreamMaxVersions, } from "../db/queries/event-store.js";
|
|
8
|
+
import { instantToTimestamptzParam, selectMany } from "../db/query.js";
|
|
10
9
|
import { isStreamArchived } from "./archive.js";
|
|
11
10
|
import { IdempotentAppendConflictError, SeedTimeOrderError, VersionConflictError, } from "./errors.js";
|
|
12
11
|
import { eventsTable } from "./events-schema.js";
|
|
@@ -112,21 +111,21 @@ async function assertSeedTimeNotBeforePredecessor(db, event, seedCreatedAt) {
|
|
|
112
111
|
}
|
|
113
112
|
}
|
|
114
113
|
async function insertFirstEvent(db, event, newVersion, eventVersion, seedCreatedAt) {
|
|
115
|
-
const row = await
|
|
114
|
+
const row = await insertFirstEventRow(db, {
|
|
116
115
|
aggregateId: event.aggregateId,
|
|
117
116
|
aggregateType: event.aggregateType,
|
|
118
117
|
tenantId: event.tenantId,
|
|
119
|
-
|
|
118
|
+
newVersion,
|
|
120
119
|
type: event.type,
|
|
121
120
|
eventVersion,
|
|
122
121
|
payload: event.payload,
|
|
123
122
|
metadata: event.metadata,
|
|
124
123
|
createdBy: event.metadata.userId,
|
|
125
|
-
|
|
124
|
+
createdAt: seedCreatedAt && instantToTimestamptzParam(seedCreatedAt),
|
|
126
125
|
});
|
|
127
126
|
if (!row)
|
|
128
|
-
throw
|
|
129
|
-
return
|
|
127
|
+
throw await versionConflictWithCurrentVersion(db, event);
|
|
128
|
+
return insertReturnOf(row);
|
|
130
129
|
}
|
|
131
130
|
// Subsequent event — predecessor must exist AND belong to the same tenant.
|
|
132
131
|
// INSERT … SELECT … WHERE EXISTS is awkward in the typed builder, so this
|
|
@@ -147,7 +146,10 @@ async function insertSubsequentEvent(db, event, newVersion, eventVersion, seedCr
|
|
|
147
146
|
createdAt: seedCreatedAt && instantToTimestamptzParam(seedCreatedAt),
|
|
148
147
|
});
|
|
149
148
|
if (!row)
|
|
150
|
-
throw
|
|
149
|
+
throw await versionConflictWithCurrentVersion(db, event);
|
|
150
|
+
return insertReturnOf(row);
|
|
151
|
+
}
|
|
152
|
+
function insertReturnOf(row) {
|
|
151
153
|
const createdAt = row.created_at instanceof Date
|
|
152
154
|
? Temporal.Instant.fromEpochMilliseconds(row.created_at.getTime())
|
|
153
155
|
: Temporal.Instant.from(row.created_at);
|
|
@@ -156,6 +158,12 @@ async function insertSubsequentEvent(db, event, newVersion, eventVersion, seedCr
|
|
|
156
158
|
createdAt,
|
|
157
159
|
};
|
|
158
160
|
}
|
|
161
|
+
// Runs in the still-healthy write transaction: ON CONFLICT DO NOTHING left it
|
|
162
|
+
// unaborted, so the stream version is readable here.
|
|
163
|
+
async function versionConflictWithCurrentVersion(db, event) {
|
|
164
|
+
const currentVersion = await selectStreamMaxVersion(db, event.aggregateId, event.tenantId);
|
|
165
|
+
return new VersionConflictError(event.aggregateId, event.expectedVersion, currentVersion);
|
|
166
|
+
}
|
|
159
167
|
function buildStoredEvent(event, newVersion, eventVersion, row) {
|
|
160
168
|
return {
|
|
161
169
|
id: String(row.id),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { bigserial, index, instant, integer, jsonb, table as pgTable, sql, text, uniqueIndex, uuid, } from "../db/dialect.js";
|
|
3
3
|
import { tableExists } from "../db/index.js";
|
|
4
4
|
import { ensureIdempotencyKeyIndex, ensureMembershipPayloadTenantIndex, } from "../db/queries/event-store.js";
|
|
5
|
-
import { unsafePushTables } from "../
|
|
5
|
+
import { unsafePushTables } from "../db/table-push.js";
|
|
6
6
|
import { createArchivedStreamsTable } from "./archive.js";
|
|
7
7
|
import { createSnapshotsTable } from "./snapshot.js";
|
|
8
8
|
// Event-store schema as a Drizzle table. The typed select/insert path handles
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// is a separate ops step — same stance as the upcaster dead-letter.
|
|
15
15
|
import { bigint, index, jsonb, table as pgTable, text, timestamp, uuid } from "../db/dialect.js";
|
|
16
16
|
import { tableExists } from "../db/schema-inspection.js";
|
|
17
|
-
import { unsafePushTables } from "../
|
|
17
|
+
import { unsafePushTables } from "../db/table-push.js";
|
|
18
18
|
export const rebuildDeadLetterTable = pgTable("kumiko_rebuild_dead_letters", {
|
|
19
19
|
// Surrogate PK — the same event can land here across multiple rebuild
|
|
20
20
|
// attempts before the fix ships, without unique-violation noise.
|
|
@@ -47,7 +47,7 @@ export async function createRebuildDeadLetterTable(db) {
|
|
|
47
47
|
// tx). Unqualified insert — during the rebuild the search_path points at the
|
|
48
48
|
// shadow schema, and this table exists only in public, so it falls through.
|
|
49
49
|
export async function recordRebuildDeadLetters(db, projectionName, skipped) {
|
|
50
|
-
const { insertMany } = await import("../
|
|
50
|
+
const { insertMany } = await import("../db/query.js");
|
|
51
51
|
await insertMany(db, rebuildDeadLetterTable, skipped.map(({ event, error }) => ({
|
|
52
52
|
projectionName,
|
|
53
53
|
eventId: event.id,
|
|
@@ -61,7 +61,7 @@ export async function recordRebuildDeadLetters(db, projectionName, skipped) {
|
|
|
61
61
|
}
|
|
62
62
|
// Ops-side triage query, optionally scoped to one projection.
|
|
63
63
|
export async function listRebuildDeadLetters(db, options = {}) {
|
|
64
|
-
const { selectMany } = await import("../
|
|
64
|
+
const { selectMany } = await import("../db/query.js");
|
|
65
65
|
const limit = options.limit ?? 100;
|
|
66
66
|
const where = options.projectionName !== undefined ? { projectionName: options.projectionName } : undefined;
|
|
67
67
|
return selectMany(db, rebuildDeadLetterTable, where, {
|
|
@@ -4,7 +4,7 @@ import { index, instant, integer, jsonb, table as pgTable, primaryKey, sql, text
|
|
|
4
4
|
import { ensureSnapshotVersionColumn, upsertSnapshot } from "../db/queries/event-store.js";
|
|
5
5
|
import { selectMany } from "../db/query.js";
|
|
6
6
|
import { tableExists } from "../db/schema-inspection.js";
|
|
7
|
-
import { unsafePushTables } from "../
|
|
7
|
+
import { unsafePushTables } from "../db/table-push.js";
|
|
8
8
|
import { isStreamArchived } from "./archive.js";
|
|
9
9
|
import { loadEventsAfterVersion } from "./event-store.js";
|
|
10
10
|
// Marten-aligned snapshot store. A snapshot is a point-in-time materialised
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// separate CLI step — not implemented here, tracked as follow-up.
|
|
14
14
|
import { bigint, index, integer, jsonb, table as pgTable, text, timestamp, uuid, } from "../db/dialect.js";
|
|
15
15
|
import { tableExists } from "../db/schema-inspection.js";
|
|
16
|
-
import { unsafePushTables } from "../
|
|
16
|
+
import { unsafePushTables } from "../db/table-push.js";
|
|
17
17
|
export const upcasterDeadLetterTable = pgTable("kumiko_upcaster_dead_letters", {
|
|
18
18
|
// Surrogate PK. We don't reuse eventId so a single event can land
|
|
19
19
|
// here multiple times (retry attempts across deploys before the fix
|
|
@@ -49,7 +49,7 @@ export async function createUpcasterDeadLetterTable(db) {
|
|
|
49
49
|
// ops tooling uses it for correlate-and-replay flows.
|
|
50
50
|
export async function recordUpcasterDeadLetter(db, args) {
|
|
51
51
|
const message = args.error instanceof Error ? args.error.message : String(args.error);
|
|
52
|
-
const { insertOne } = await import("../
|
|
52
|
+
const { insertOne } = await import("../db/query.js");
|
|
53
53
|
await insertOne(db, upcasterDeadLetterTable, {
|
|
54
54
|
eventId: args.event.id,
|
|
55
55
|
tenantId: args.event.tenantId,
|
|
@@ -65,7 +65,7 @@ export async function recordUpcasterDeadLetter(db, args) {
|
|
|
65
65
|
// Ops-side query. Loads recent failures, optionally scoped by event-type
|
|
66
66
|
// to triage a single broken migration without pulling the full table.
|
|
67
67
|
export async function listDeadLetters(db, options = {}) {
|
|
68
|
-
const { selectMany } = await import("../
|
|
68
|
+
const { selectMany } = await import("../db/query.js");
|
|
69
69
|
const limit = options.limit ?? 100;
|
|
70
70
|
const where = options.eventType !== undefined ? { eventType: options.eventType } : undefined;
|
|
71
71
|
const rows = await selectMany(db, upcasterDeadLetterTable, where, {
|
|
@@ -19,6 +19,7 @@ export type CreateFileRefInput = {
|
|
|
19
19
|
readonly entityId?: string;
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
|
+
export declare function filesUnavailableMessage(caller: string): string;
|
|
22
23
|
export declare function createFileRefFromBytes(ctx: CreateFileRefContext, input: CreateFileRefInput): Promise<WriteResult<CreatedFileRef>>;
|
|
23
24
|
export type UploadFileBytesContext = Pick<CreateFileRefContext, "db" | "files" | "registry" | "user">;
|
|
24
25
|
export declare function uploadFileBytesBeforeTransaction(ctx: UploadFileBytesContext, input: CreateFileRefInput): Promise<BeforeTransactionResult<UploadedFileBytes>>;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { fetchOne } from "../
|
|
1
|
+
import { fetchOne } from "../db/query.js";
|
|
2
2
|
import { FrameworkReasons, InternalError, UnprocessableError, writeFailure, } from "../errors/index.js";
|
|
3
3
|
import { createFileRefRow, DEFAULT_MAX_UPLOAD_SIZE, storeFileAsFileRef, writeAcceptedFileBytes, } from "./create-file-ref.js";
|
|
4
4
|
import { fileRefsTable } from "./file-ref-table.js";
|
|
5
5
|
import { readFilesRouteOptions } from "./file-routes.js";
|
|
6
6
|
import { assertSafeStorageKey } from "./types.js";
|
|
7
|
+
// The handler context has no HTTP/job/seed marker to name, so the message states the actual
|
|
8
|
+
// cause: the dispatcher's AppContext carries no file-provider resolver.
|
|
9
|
+
export function filesUnavailableMessage(caller) {
|
|
10
|
+
return `${caller}: ctx.files is unavailable because no file-provider resolver is wired on this dispatcher context. Mount a file-provider-* feature and build the dispatcher context with withFileProviderResolver (HTTP, job and seed dispatchers each need it)`;
|
|
11
|
+
}
|
|
7
12
|
// Server-side counterpart of POST /files for handlers/jobs that produce bytes
|
|
8
13
|
// themselves (generated PDFs, exports). Same validation, storage layout and
|
|
9
14
|
// fileRef.created event as an upload; the tenant is always ctx.user.tenantId.
|
|
@@ -11,7 +16,7 @@ export async function createFileRefFromBytes(ctx, input) {
|
|
|
11
16
|
const { files } = ctx;
|
|
12
17
|
if (!files) {
|
|
13
18
|
return writeFailure(new InternalError({
|
|
14
|
-
message: "createFileRefFromBytes
|
|
19
|
+
message: filesUnavailableMessage("createFileRefFromBytes"),
|
|
15
20
|
}));
|
|
16
21
|
}
|
|
17
22
|
const outcome = await storeFileAsFileRef({
|
|
@@ -48,7 +53,7 @@ export async function uploadFileBytesBeforeTransaction(ctx, input) {
|
|
|
48
53
|
const { files } = ctx;
|
|
49
54
|
if (!files) {
|
|
50
55
|
return writeFailure(new InternalError({
|
|
51
|
-
message: "uploadFileBytesBeforeTransaction
|
|
56
|
+
message: filesUnavailableMessage("uploadFileBytesBeforeTransaction"),
|
|
52
57
|
}));
|
|
53
58
|
}
|
|
54
59
|
const outcome = await writeAcceptedFileBytes({
|
|
@@ -2,8 +2,8 @@ import { Hono } from "hono";
|
|
|
2
2
|
import { bodyLimit } from "hono/body-limit";
|
|
3
3
|
import { getUser } from "../api/auth-middleware.js";
|
|
4
4
|
import { requestContext } from "../api/request-context.js";
|
|
5
|
-
import { selectMany } from "../bun-db/index.js";
|
|
6
5
|
import { collectPiiSubjectFields, configuredPiiSubjectKms, decryptPiiFieldValues, isPiiCiphertext, PII_ERASED_SENTINEL, } from "../crypto/index.js";
|
|
6
|
+
import { selectMany } from "../db/query.js";
|
|
7
7
|
import { createProjectingTenantDb } from "../db/tenant-db.js";
|
|
8
8
|
import { createDerivativesContext, resolveFieldVariant, resolveRenderer, } from "../derivatives/index.js";
|
|
9
9
|
import { SYSTEM_USER_ID } from "../engine/system-user.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { createDbConnection } from "../db/connection.js";
|
|
2
|
+
export { createDatabase, databaseExists, dropDatabaseIfExists, truncateTablesRestartIdentity, } from "../db/queries/test-stack.js";
|
|
3
|
+
export { type AnyDb, isClosedConnectionError } from "../db/query.js";
|
|
4
|
+
export { columnNamesOf } from "../db/schema-inspection.js";
|
|
5
|
+
export { schemaTableName } from "../db/table-introspection.js";
|
|
6
|
+
export { addMissingColumns, addMissingIndexes, tableToMeta, unsafeCreateEntityTable, unsafeEnsureEntityTable, unsafePushTables, } from "../db/table-push.js";
|
|
7
|
+
export { SEARCHABLE_FALSE_WHITELIST } from "../engine/boot-validator/entity-list-screens.js";
|
|
8
|
+
export { validateNavBootSubset } from "../engine/boot-validator/index.js";
|
|
9
|
+
export { validateOwnershipBoot } from "../engine/boot-validator/ownership.js";
|
|
10
|
+
export { featureHasI18nSurface, requiredKeysFromFeature } from "../i18n/required-surface-keys.js";
|
|
11
|
+
export type { Lifecycle } from "../lifecycle/index.js";
|
|
12
|
+
export { createRateLimitResolver } from "../rate-limit/index.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @runtime dev
|
|
2
|
+
export { createDbConnection } from "../db/connection.js";
|
|
3
|
+
export { createDatabase, databaseExists, dropDatabaseIfExists, truncateTablesRestartIdentity, } from "../db/queries/test-stack.js";
|
|
4
|
+
export { isClosedConnectionError } from "../db/query.js";
|
|
5
|
+
export { columnNamesOf } from "../db/schema-inspection.js";
|
|
6
|
+
export { schemaTableName } from "../db/table-introspection.js";
|
|
7
|
+
export { addMissingColumns, addMissingIndexes, tableToMeta, unsafeCreateEntityTable, unsafeEnsureEntityTable, unsafePushTables, } from "../db/table-push.js";
|
|
8
|
+
export { SEARCHABLE_FALSE_WHITELIST } from "../engine/boot-validator/entity-list-screens.js";
|
|
9
|
+
export { validateNavBootSubset } from "../engine/boot-validator/index.js";
|
|
10
|
+
export { validateOwnershipBoot } from "../engine/boot-validator/ownership.js";
|
|
11
|
+
export { featureHasI18nSurface, requiredKeysFromFeature } from "../i18n/required-surface-keys.js";
|
|
12
|
+
export { createRateLimitResolver } from "../rate-limit/index.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { resetBlindIndexKeyForTests } from "../crypto/blind-index.js";
|
|
2
|
+
export { resetEventPiiCatalogForTests } from "../crypto/event-pii.js";
|
|
3
|
+
export { resetPiiSubjectKmsForTests } from "../crypto/pii-field-encryption.js";
|
|
4
|
+
export { resetEntityFieldEncryptionCacheForTests } from "../db/entity-field-encryption.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// @runtime test
|
|
2
|
+
export { resetBlindIndexKeyForTests } from "../crypto/blind-index.js";
|
|
3
|
+
export { resetEventPiiCatalogForTests } from "../crypto/event-pii.js";
|
|
4
|
+
export { resetPiiSubjectKmsForTests } from "../crypto/pii-field-encryption.js";
|
|
5
|
+
export { resetEntityFieldEncryptionCacheForTests } from "../db/entity-field-encryption.js";
|
package/dist/jobs/job-runner.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Redis } from "ioredis";
|
|
|
6
6
|
import { requestContext } from "../api/request-context.js";
|
|
7
7
|
import { createTenantDb, createUncheckedSystemDb } from "../db/tenant-db.js";
|
|
8
8
|
import { createDerivativesContext } from "../derivatives/derivatives-context.js";
|
|
9
|
+
import { bridgeStub } from "../engine/bridge-stub.js";
|
|
9
10
|
import { qnScope } from "../engine/qualified-name.js";
|
|
10
11
|
import { createSystemUser } from "../engine/system-user.js";
|
|
11
12
|
import { SYSTEM_TENANT_ID, } from "../engine/types/index.js";
|
|
@@ -19,7 +20,6 @@ import { RedisKeys } from "../pipeline/redis-keys.js";
|
|
|
19
20
|
import { createGatedIdentitySwitch } from "../pipeline/system-identity-switch.js";
|
|
20
21
|
import { TENANT_TIMEZONE_CONFIG_KEY } from "../pipeline/tenant-timezone-config-key.js";
|
|
21
22
|
import { buildPersonalDataGate, isPersonalDataGated, parseWriteOrigin, } from "../pipeline/write-origin.js";
|
|
22
|
-
import { bridgeStub } from "../testing/handler-context.js";
|
|
23
23
|
import { createTzContext, isValidIanaTimeZone } from "../time/index.js";
|
|
24
24
|
import { cronRepeatOptions } from "./cron-repeat-options.js";
|
|
25
25
|
import { resolveMaxPerTenant } from "./max-per-tenant.js";
|
|
@@ -429,7 +429,7 @@ export function createJobRunner(options) {
|
|
|
429
429
|
return undefined;
|
|
430
430
|
return async () => {
|
|
431
431
|
const systemUser = createSystemUser(SYSTEM_TENANT_ID);
|
|
432
|
-
const systemModeDb = createTenantDb(db, SYSTEM_TENANT_ID, "system");
|
|
432
|
+
const systemModeDb = createTenantDb({ db, tenantId: SYSTEM_TENANT_ID, mode: "system" });
|
|
433
433
|
const reportEscapeHatch = createEscapeHatchReporter({
|
|
434
434
|
handler: ACTIVE_TENANT_IDS_QUERY_NAME,
|
|
435
435
|
tenantId: SYSTEM_TENANT_ID,
|
|
@@ -922,9 +922,14 @@ export function createJobRunner(options) {
|
|
|
922
922
|
// unchecked cross-tenant escape hatch for such a job. Gated like jobDb so
|
|
923
923
|
// ctx.systemDb, built from it, is gated too.
|
|
924
924
|
const tenantScopedDb = configDb
|
|
925
|
-
? createTenantDb(
|
|
926
|
-
|
|
927
|
-
|
|
925
|
+
? createTenantDb({
|
|
926
|
+
db: configDb,
|
|
927
|
+
tenantId,
|
|
928
|
+
mode: "system",
|
|
929
|
+
grants: {
|
|
930
|
+
projectionRegistry: registry,
|
|
931
|
+
...(jobPersonalDataGate && { personalDataGate: jobPersonalDataGate }),
|
|
932
|
+
},
|
|
928
933
|
})
|
|
929
934
|
: undefined;
|
|
930
935
|
const isSystemJob = registry.isJobSystemScoped(jobName);
|
|
@@ -945,12 +950,19 @@ export function createJobRunner(options) {
|
|
|
945
950
|
? createUncheckedSystemDb(tenantScopedDb, undefined, reportEscapeHatch)
|
|
946
951
|
: undefined;
|
|
947
952
|
const jobDb = configDb
|
|
948
|
-
? createTenantDb(
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
953
|
+
? createTenantDb({
|
|
954
|
+
db: configDb,
|
|
955
|
+
tenantId,
|
|
956
|
+
mode: "tenant",
|
|
957
|
+
tracer: context.tracer,
|
|
958
|
+
meter: context.meter,
|
|
959
|
+
grants: {
|
|
960
|
+
unsafeRaw: escapeHatchFor(jobDef.escapeHatch, "unsafeRaw"),
|
|
961
|
+
globalWrites: escapeHatchFor(jobDef.escapeHatch, "globalWrites"),
|
|
962
|
+
report: reportEscapeHatch,
|
|
963
|
+
projectionRegistry: registry,
|
|
964
|
+
...(jobPersonalDataGate && { personalDataGate: jobPersonalDataGate }),
|
|
965
|
+
},
|
|
954
966
|
})
|
|
955
967
|
: undefined;
|
|
956
968
|
const crossTenantReadsReason = escapeHatchFor(jobDef.escapeHatch, "unsafeRaw")?.reason;
|
|
@@ -14,9 +14,9 @@ import { instant, table as pgTable, sql, text } from "../db/dialect.js";
|
|
|
14
14
|
import { deleteMany, selectMany, upsertOnConflict } from "../db/query.js";
|
|
15
15
|
import { readRebuildMarker } from "../db/rebuild-marker.js";
|
|
16
16
|
import { tableExists } from "../db/schema-inspection.js";
|
|
17
|
+
import { unsafePushTables } from "../db/table-push.js";
|
|
17
18
|
import { createFallbackLogger } from "../logging/utils.js";
|
|
18
19
|
import { rebuildMultiStreamProjection, rebuildProjection, } from "../pipeline/index.js";
|
|
19
|
-
import { unsafePushTables } from "../stack/index.js";
|
|
20
20
|
import { buildProjectionTableIndex } from "./projection-table-index.js";
|
|
21
21
|
export const pendingRebuildsTable = pgTable("kumiko_pending_rebuilds", {
|
|
22
22
|
tableName: text("table_name").primaryKey(),
|
|
@@ -18,8 +18,8 @@ import { alterTableAddColumn } from "../db/queries/ddl.js";
|
|
|
18
18
|
import { persistPiiBackfillState, readPiiBackfillState, } from "../db/queries/pii-backfill-state.js";
|
|
19
19
|
import { upsertOnConflict } from "../db/query.js";
|
|
20
20
|
import { tableExists } from "../db/schema-inspection.js";
|
|
21
|
+
import { unsafePushTables } from "../db/table-push.js";
|
|
21
22
|
import { createFallbackLogger } from "../logging/utils.js";
|
|
22
|
-
import { unsafePushTables } from "../stack/index.js";
|
|
23
23
|
import { clearPendingRebuilds, createPendingRebuildsTable, listPendingRebuildRows, pendingRebuildsTable, rebuildProjectionOrMultiStream, } from "./pending-rebuilds.js";
|
|
24
24
|
import { buildProjectionTableIndex } from "./projection-table-index.js";
|
|
25
25
|
const PII_EVENT_BACKFILL_LOCK_NAMESPACE = 0x70_69_69_62; // 'piib'
|
|
@@ -71,7 +71,7 @@ export function piiAnnotationEntries(registry) {
|
|
|
71
71
|
// Bump when the backfill gains a transform stage: completed states keyed by the
|
|
72
72
|
// old fingerprint must run one more full pass. Kept out of piiAnnotationEntries,
|
|
73
73
|
// whose emptiness gates no_pii.
|
|
74
|
-
const PII_BACKFILL_TRANSFORM_VERSION = "owner-rekey-
|
|
74
|
+
const PII_BACKFILL_TRANSFORM_VERSION = "owner-rekey-v2";
|
|
75
75
|
export function piiAnnotationFingerprint(registry) {
|
|
76
76
|
return createHash("sha256")
|
|
77
77
|
.update([...piiAnnotationEntries(registry), PII_BACKFILL_TRANSFORM_VERSION].join("\n"))
|
|
@@ -289,16 +289,24 @@ readOnlyForTenant = false) {
|
|
|
289
289
|
});
|
|
290
290
|
const identitySwitchGrantReason = identitySwitchEscapeHatch?.reason ??
|
|
291
291
|
(isSystem ? `r.systemScope() feature "${featureName ?? "unknown"}"` : undefined);
|
|
292
|
-
const buildTenantScopedDb = (source, signal, foreignTenantId, denyWrites = false) => createTenantDb(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
292
|
+
const buildTenantScopedDb = (source, signal, foreignTenantId, denyWrites = false) => createTenantDb({
|
|
293
|
+
db: source,
|
|
294
|
+
tenantId: foreignTenantId ?? user.tenantId,
|
|
295
|
+
mode: isSystem && foreignTenantId === undefined ? "system" : "tenant",
|
|
296
|
+
tracer: context.tracer,
|
|
297
|
+
meter: context.meter,
|
|
298
|
+
signal,
|
|
299
|
+
grants: {
|
|
300
|
+
...(foreignTenantId === undefined && {
|
|
301
|
+
globalWrites: escapeHatchFor(writeEscapeHatch, "globalWrites"),
|
|
302
|
+
unsafeRaw: escapeHatchFor(handlerEscapeHatch, "unsafeRaw"),
|
|
303
|
+
}),
|
|
304
|
+
report: reportEscapeHatch,
|
|
305
|
+
memberReadOnly: isMemberResolutionPrincipal(user),
|
|
306
|
+
personalDataGate: buildPersonalDataGate(registry, origin),
|
|
307
|
+
projectionRegistry: registry,
|
|
308
|
+
...(denyWrites && { writesDeniedReason: "without an open transaction" }),
|
|
309
|
+
},
|
|
302
310
|
});
|
|
303
311
|
// Propagate the request's AbortSignal so every TenantDb query throws when
|
|
304
312
|
// the client has disconnected — handlers with many sequential queries skip
|
|
@@ -410,7 +418,14 @@ readOnlyForTenant = false) {
|
|
|
410
418
|
const targetSystemUser = createSystemUser(targetTenantId);
|
|
411
419
|
return configFactory({
|
|
412
420
|
user: { id: targetSystemUser.id, tenantId: targetTenantId },
|
|
413
|
-
db: createTenantDb(
|
|
421
|
+
db: createTenantDb({
|
|
422
|
+
db: dbSource,
|
|
423
|
+
tenantId: targetTenantId,
|
|
424
|
+
mode: "tenant",
|
|
425
|
+
tracer: context.tracer,
|
|
426
|
+
meter: context.meter,
|
|
427
|
+
grants: { projectionRegistry: registry },
|
|
428
|
+
}),
|
|
414
429
|
secrets: context.secrets,
|
|
415
430
|
});
|
|
416
431
|
}
|
|
@@ -886,9 +901,7 @@ readOnlyForTenant = false) {
|
|
|
886
901
|
// Finger an typo-resistenten ctx.user-Patterns. Identisch zum
|
|
887
902
|
// event.user-Wert; Identity-Switches nutzen weiterhin queryAs/writeAs.
|
|
888
903
|
user,
|
|
889
|
-
_userId: user.id,
|
|
890
904
|
_tenantId: user.tenantId,
|
|
891
|
-
_handlerType: type,
|
|
892
905
|
...(hasLiveCommitSink && { scheduleAfterCommit }),
|
|
893
906
|
...(includeDeleted && { includeDeleted: true }),
|
|
894
907
|
...bridge,
|
|
@@ -1219,16 +1232,23 @@ function buildAuthClaimsContext(ctx, user) {
|
|
|
1219
1232
|
message: "dispatcher.resolveAuthClaims requires a database connection — none is configured.",
|
|
1220
1233
|
});
|
|
1221
1234
|
}
|
|
1222
|
-
const db = createTenantDb(
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1235
|
+
const db = createTenantDb({
|
|
1236
|
+
db: dbSource,
|
|
1237
|
+
tenantId: user.tenantId,
|
|
1238
|
+
mode: "tenant",
|
|
1239
|
+
tracer: context.tracer,
|
|
1240
|
+
meter: context.meter,
|
|
1241
|
+
grants: {
|
|
1242
|
+
report: createEscapeHatchReporter({
|
|
1243
|
+
handler: "r.authClaims hook",
|
|
1244
|
+
tenantId: user.tenantId,
|
|
1245
|
+
actor: user.id,
|
|
1246
|
+
sink: context._escapeHatchAuditSink,
|
|
1247
|
+
log: context.log ?? createFallbackLogger("dispatcher"),
|
|
1248
|
+
window: ctx.escapeHatchReportWindow,
|
|
1249
|
+
}),
|
|
1250
|
+
projectionRegistry: ctx.registry,
|
|
1251
|
+
},
|
|
1232
1252
|
});
|
|
1233
1253
|
const configAccessor = context._configAccessorFactory
|
|
1234
1254
|
? context._configAccessorFactory({
|
|
@@ -163,7 +163,7 @@ async function isParentRowHiddenFromCaller(ctx, parentEntityName, parentId, user
|
|
|
163
163
|
const source = resolveDbSource(ctx, tx);
|
|
164
164
|
if (!table || !source)
|
|
165
165
|
return true;
|
|
166
|
-
const callerDb = createTenantDb(source, user.tenantId, "tenant");
|
|
166
|
+
const callerDb = createTenantDb({ db: source, tenantId: user.tenantId, mode: "tenant" });
|
|
167
167
|
const visibleRow = await callerDb.fetchOne(table, { id: parentId });
|
|
168
168
|
if (visibleRow === undefined)
|
|
169
169
|
return true;
|
|
@@ -181,7 +181,11 @@ async function completeParentRow(ctx, parentEntityName, parentEntity, parentRow,
|
|
|
181
181
|
const source = resolveDbSource(ctx, tx);
|
|
182
182
|
if (!table || !source || typeof parentRow["id"] !== "string")
|
|
183
183
|
return parentRow;
|
|
184
|
-
const stored = await createTenantDb(
|
|
184
|
+
const stored = await createTenantDb({
|
|
185
|
+
db: source,
|
|
186
|
+
tenantId: user.tenantId,
|
|
187
|
+
mode: "tenant",
|
|
188
|
+
}).fetchOne(table, {
|
|
185
189
|
id: parentRow["id"],
|
|
186
190
|
});
|
|
187
191
|
return stored ? { ...stored, ...parentRow } : parentRow;
|
|
@@ -143,10 +143,14 @@ export function resolveType(type) {
|
|
|
143
143
|
// A custom write losing an append race (ctx.appendEvent, stream.append) is a
|
|
144
144
|
// retryable 409 like the CRUD executor's, not an internal error. Mapped here
|
|
145
145
|
// because errors/ is client code and must not import the event store.
|
|
146
|
-
// currentVersion -1 is the
|
|
146
|
+
// currentVersion -1 is the "not looked up" sentinel for a bare 23505 fallback.
|
|
147
147
|
export function wrapToKumiko(e) {
|
|
148
148
|
if (e instanceof EventStoreVersionConflictError) {
|
|
149
|
-
return new VersionConflictError({
|
|
149
|
+
return new VersionConflictError({
|
|
150
|
+
entityId: e.aggregateId,
|
|
151
|
+
expectedVersion: e.expectedVersion,
|
|
152
|
+
currentVersion: e.currentVersion ?? -1,
|
|
153
|
+
}, { cause: e });
|
|
150
154
|
}
|
|
151
155
|
return toKumikoError(e);
|
|
152
156
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { bigint, index, instant, integer, jsonb, table as pgTable, primaryKey, sql, text, } from "../db/dialect.js";
|
|
2
2
|
import { alterTableAddColumn } from "../db/queries/ddl.js";
|
|
3
3
|
import { tableExists } from "../db/schema-inspection.js";
|
|
4
|
-
import { unsafePushTables } from "../
|
|
4
|
+
import { unsafePushTables } from "../db/table-push.js";
|
|
5
5
|
// Reserved sentinel used in the instance_id column for consumers whose
|
|
6
6
|
// delivery is "shared" — i.e. one cursor across all dispatcher instances
|
|
7
7
|
// (the default, pre-Welle-2.7 behaviour). Per-instance consumers store
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { DbConnection } from "../db/connection.js";
|
|
3
3
|
import type { Registry } from "../engine/types/index.js";
|
|
4
4
|
import type { Meter } from "../observability/types/metric.js";
|
|
5
|
+
export declare const REBUILD_BATCH_SIZE = 1000;
|
|
5
6
|
export type RebuildResult = {
|
|
6
7
|
readonly projection: string;
|
|
7
8
|
readonly eventsProcessed: number;
|
|
@@ -8,7 +8,7 @@ import { projectionStateTable } from "./projection-state.js";
|
|
|
8
8
|
// Events replayed per catch-up batch. Each batch is a fresh READ COMMITTED
|
|
9
9
|
// SELECT, so a batch shorter than this means the currently-committed tail is
|
|
10
10
|
// drained.
|
|
11
|
-
const REBUILD_BATCH_SIZE = 1000;
|
|
11
|
+
export const REBUILD_BATCH_SIZE = 1000;
|
|
12
12
|
// Cap on UNLOCKED catch-up batches before forcing the cutover fence. Bounds the
|
|
13
13
|
// lock-free phase under sustained writes that never momentarily quiesce; the
|
|
14
14
|
// fenced final drain then always terminates (no new event can commit once the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bigint, index, instant, table as pgTable, sql, text } from "../db/dialect.js";
|
|
2
2
|
import { tableExists } from "../db/schema-inspection.js";
|
|
3
|
-
import { unsafePushTables } from "../
|
|
3
|
+
import { unsafePushTables } from "../db/table-push.js";
|
|
4
4
|
// Framework-level state for every registered projection. One row per qualified
|
|
5
5
|
// projection name. Written by the rebuild machinery; read by the CLI + any
|
|
6
6
|
// status dashboard. Lives alongside the events table as framework infra —
|
|
@@ -206,7 +206,7 @@ async function readProjectionRowForSearch(ctx, registry, entityName, entity, ten
|
|
|
206
206
|
if (entity.softDelete && table["isDeleted"]) {
|
|
207
207
|
where["isDeleted"] = false;
|
|
208
208
|
}
|
|
209
|
-
const row = await createTenantDb(baseDb, tenantId).fetchOne(table, where);
|
|
209
|
+
const row = await createTenantDb({ db: baseDb, tenantId }).fetchOne(table, where);
|
|
210
210
|
if (!row)
|
|
211
211
|
return undefined;
|
|
212
212
|
const state = rehydrateCompoundTypes(row, entity);
|
package/dist/schema-cli.js
CHANGED
|
@@ -303,7 +303,7 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
303
303
|
out.err(` ${migrationsDir} fehlt — erst schema generate <name>.`);
|
|
304
304
|
return 1;
|
|
305
305
|
}
|
|
306
|
-
const { db, close } = createDbConnection(dbUrl);
|
|
306
|
+
const { db, close } = createDbConnection({ url: dbUrl });
|
|
307
307
|
let wiring;
|
|
308
308
|
try {
|
|
309
309
|
const result = await runMigrationsFromDir(db, migrationsDir);
|
|
@@ -386,7 +386,7 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
386
386
|
out.err(` ${migrationsDir} fehlt — erst schema generate <name>.`);
|
|
387
387
|
return 1;
|
|
388
388
|
}
|
|
389
|
-
const { db, close } = createDbConnection(dbUrl);
|
|
389
|
+
const { db, close } = createDbConnection({ url: dbUrl });
|
|
390
390
|
try {
|
|
391
391
|
const result = await baselineMigrations(db, loadMigrationsFromDir(migrationsDir));
|
|
392
392
|
out.log("");
|
|
@@ -418,7 +418,7 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
418
418
|
return 0;
|
|
419
419
|
}
|
|
420
420
|
const local = loadMigrationsFromDir(migrationsDir);
|
|
421
|
-
const { db, close } = createDbConnection(dbUrl);
|
|
421
|
+
const { db, close } = createDbConnection({ url: dbUrl });
|
|
422
422
|
try {
|
|
423
423
|
// Frische DB ohne je gelaufenes `kumiko schema apply` → tracking-table
|
|
424
424
|
// fehlt = "nichts applied". Connection-/Permission-Fehler dagegen
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// that still carry the subject key in encrypted columns.
|
|
10
10
|
import { quoteIdent, subjectCiphertextLikePattern } from "../crypto/ciphertext-pattern.js";
|
|
11
11
|
import { isSelfPiiField } from "../crypto/is-self-pii-field.js";
|
|
12
|
-
import { collectSearchableSubjectFields } from "../crypto/subject-resolver.js";
|
|
12
|
+
import { collectSearchableSubjectFields, isRecordKeyedField } from "../crypto/subject-resolver.js";
|
|
13
13
|
import { resolveTableName } from "../db/entity-table-meta.js";
|
|
14
14
|
import { executeRawQueryRead } from "../db/queries/raw-sql.js";
|
|
15
15
|
import { columnNamesOf, tableExists } from "../db/schema-inspection.js";
|
|
@@ -63,20 +63,30 @@ function tenantOwnershipPredicates(entity, searchableFields, tenantId, nextParam
|
|
|
63
63
|
return finalizePredicateParts(parts, params);
|
|
64
64
|
}
|
|
65
65
|
function recordOwnershipPredicates(entity, searchableFields, entityName, subjectEntity, subjectId, nextParam) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
let recordIdN;
|
|
66
|
+
const ownerlessColumns = new Set();
|
|
67
|
+
let hasRecordOwnedField = false;
|
|
69
68
|
for (const fieldName of searchableFields) {
|
|
70
69
|
const field = entity.fields[fieldName];
|
|
71
|
-
if (!field)
|
|
70
|
+
if (!field || entityName !== subjectEntity || !isRecordKeyedField(field))
|
|
72
71
|
continue;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
if ("recordOwned" in field && field.recordOwned === true) {
|
|
73
|
+
hasRecordOwnedField = true;
|
|
74
|
+
}
|
|
75
|
+
else if ("userOwned" in field && field.userOwned !== undefined) {
|
|
76
|
+
ownerlessColumns.add(toSnakeCase(field.userOwned.ownerField));
|
|
78
77
|
}
|
|
79
78
|
}
|
|
79
|
+
if (!hasRecordOwnedField && ownerlessColumns.size === 0)
|
|
80
|
+
return null;
|
|
81
|
+
const idN = nextParam();
|
|
82
|
+
const parts = [];
|
|
83
|
+
if (hasRecordOwnedField)
|
|
84
|
+
parts.push(`${quoteIdent("id")} = $${idN}`);
|
|
85
|
+
// whenAbsent: "self" keys a row on itself only while its owner is empty
|
|
86
|
+
for (const col of ownerlessColumns) {
|
|
87
|
+
parts.push(`(${quoteIdent("id")} = $${idN} AND (${quoteIdent(col)} IS NULL OR ${quoteIdent(col)}::text = ''))`);
|
|
88
|
+
}
|
|
89
|
+
const params = [subjectId];
|
|
80
90
|
return finalizePredicateParts(parts, params);
|
|
81
91
|
}
|
|
82
92
|
/** Build OR predicates for rows owned by `subject` (id / ownerField / tenant_id). */
|
package/dist/seeding/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { type EventStoreSeedExisting, type RunEventStoreSeedOptions, runEventStoreSeed, } from "./entity-seed.js";
|
|
2
|
+
export { createTestUser, TestUsers, testTenantId, testUserId } from "./seed-users.js";
|
|
2
3
|
export { DEFAULT_SEED_IF_EXISTS, type SeedIfExists } from "./types.js";
|
package/dist/seeding/index.js
CHANGED