@cosmicdrift/kumiko-framework 0.334.0 → 0.335.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/auth-routes.js +19 -4
- package/dist/api/client-ip.js +3 -6
- package/dist/api/extra-route.js +3 -2
- package/dist/api/pii-leak-guard.js +7 -4
- package/dist/api/redis-sse-broker.d.ts +2 -0
- package/dist/api/redis-sse-broker.js +6 -7
- package/dist/api/routes.js +16 -17
- package/dist/api/server.js +14 -7
- package/dist/api/sse-broker.d.ts +3 -0
- package/dist/api/sse-broker.js +14 -0
- package/dist/api/sse-route.d.ts +1 -1
- package/dist/api/sse-route.js +39 -6
- package/dist/bun-db/connection.js +0 -2
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +1 -0
- package/dist/bun-db/query.js +15 -4
- package/dist/crypto/kek-source.js +40 -36
- package/dist/crypto/kms-adapter.js +2 -2
- package/dist/db/apply-entity-event.js +8 -2
- package/dist/db/blind-index-cleanup.js +11 -1
- package/dist/db/entity-table-meta.js +5 -3
- package/dist/db/event-store-executor-context.js +13 -19
- package/dist/db/event-store-executor-read.js +39 -36
- package/dist/db/event-store-executor-write.js +15 -17
- package/dist/db/migrate-generator.js +27 -28
- package/dist/db/pg-error.js +9 -5
- package/dist/db/queries/backfill-pii.js +15 -5
- package/dist/db/queries/shadow-swap.js +5 -4
- package/dist/db/table-builder.d.ts +1 -0
- package/dist/db/table-builder.js +7 -15
- package/dist/db/tenant-db.js +18 -2
- package/dist/derivatives/variant-key.js +3 -1
- package/dist/engine/boot-validator/access-declarations.js +12 -3
- package/dist/engine/boot-validator/entity-handler.js +13 -1
- package/dist/engine/boot-validator/entity-list-screens.d.ts +1 -0
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- package/dist/engine/boot-validator/index.js +6 -6
- package/dist/engine/boot-validator/nav.d.ts +2 -1
- package/dist/engine/boot-validator/nav.js +10 -20
- package/dist/engine/boot-validator/ownership.d.ts +1 -1
- package/dist/engine/boot-validator/ownership.js +2 -1
- package/dist/engine/boot-validator/pii-retention.d.ts +2 -0
- package/dist/engine/boot-validator/pii-retention.js +5 -2
- package/dist/engine/boot-validator/projection-list-screens.js +8 -0
- package/dist/engine/boot-validator/query-output-columns.js +41 -5
- package/dist/engine/boot-validator/screens.d.ts +2 -2
- package/dist/engine/boot-validator/screens.js +103 -33
- package/dist/engine/boot-validator/transfer-graph.js +9 -6
- package/dist/engine/boot-validator/url-prefill-fields.js +8 -6
- package/dist/engine/boot-validator/zod-shape.js +17 -4
- package/dist/engine/build-app-schema.js +62 -19
- package/dist/engine/build-config-feature-schema.js +8 -1
- package/dist/engine/changeset-changes.js +24 -10
- package/dist/engine/feature-ast/extractors/events.js +11 -8
- package/dist/engine/feature-ast/extractors/handlers.js +38 -16
- package/dist/engine/feature-ast/extractors/resolve-import.d.ts +7 -2
- package/dist/engine/feature-ast/extractors/resolve-import.js +42 -15
- package/dist/engine/feature-ast/parse.d.ts +1 -0
- package/dist/engine/feature-ast/parse.js +18 -3
- package/dist/engine/feature-ast/patch.js +80 -61
- package/dist/engine/feature-ast/patcher.d.ts +1 -0
- package/dist/engine/feature-ast/patcher.js +2 -1
- package/dist/engine/feature-ast/render.js +1 -1
- package/dist/engine/index.d.ts +4 -3
- package/dist/engine/index.js +3 -2
- package/dist/engine/ownership.d.ts +1 -0
- package/dist/engine/ownership.js +15 -2
- package/dist/engine/pattern-library/mixed-schemas.js +15 -29
- package/dist/engine/pattern-library/shared-fields.d.ts +2 -0
- package/dist/engine/pattern-library/shared-fields.js +17 -0
- package/dist/engine/registry-facade.js +3 -0
- package/dist/engine/role-assignment.d.ts +7 -1
- package/dist/engine/role-assignment.js +27 -7
- package/dist/engine/schema-builder.d.ts +1 -0
- package/dist/engine/schema-builder.js +9 -2
- package/dist/engine/screen-helpers.js +15 -4
- package/dist/engine/steps/_event-match.js +22 -3
- package/dist/engine/steps/_read-source.js +8 -0
- package/dist/engine/where-rule-lint.js +22 -4
- package/dist/env/index.js +21 -1
- package/dist/errors/reasons.d.ts +0 -1
- package/dist/errors/reasons.js +2 -5
- package/dist/event-store/event-store.js +6 -1
- package/dist/event-store/provenance-append.js +4 -1
- package/dist/files/file-routes.d.ts +0 -1
- package/dist/files/file-routes.js +22 -7
- package/dist/files/local-provider.js +13 -10
- package/dist/files/provider-resolver.js +20 -1
- package/dist/files/types.js +16 -1
- package/dist/i18n/request-locale.d.ts +3 -2
- package/dist/i18n/request-locale.js +16 -6
- package/dist/i18n/required-surface-keys.js +23 -35
- package/dist/i18n/settings-hub-keys.js +3 -0
- package/dist/jobs/index.d.ts +2 -2
- package/dist/jobs/index.js +1 -1
- package/dist/jobs/job-runner.d.ts +2 -0
- package/dist/jobs/job-runner.js +150 -59
- package/dist/migrations/index.d.ts +1 -1
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/pending-rebuilds.d.ts +4 -0
- package/dist/migrations/pending-rebuilds.js +23 -1
- package/dist/observability/escape-hatch-report.d.ts +1 -0
- package/dist/observability/escape-hatch-report.js +3 -0
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/pipeline/dispatch-query.d.ts +1 -1
- package/dist/pipeline/dispatch-query.js +2 -2
- package/dist/pipeline/dispatch-shared.d.ts +2 -3
- package/dist/pipeline/dispatch-shared.js +14 -9
- package/dist/pipeline/dispatch-stream.js +2 -5
- package/dist/pipeline/dispatch-write.js +22 -5
- package/dist/pipeline/dispatcher.js +1 -2
- package/dist/pipeline/event-dispatcher-admin.js +12 -2
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +26 -11
- package/dist/pipeline/index.d.ts +1 -1
- package/dist/pipeline/index.js +1 -1
- package/dist/pipeline/member-read-only-transaction.js +1 -14
- package/dist/pipeline/msp-rebuild.d.ts +1 -0
- package/dist/pipeline/msp-rebuild.js +1 -0
- package/dist/pipeline/system-hooks.d.ts +1 -1
- package/dist/pipeline/system-hooks.js +9 -7
- package/dist/pipeline/tenant-timezone-cache.d.ts +2 -0
- package/dist/pipeline/tenant-timezone-cache.js +20 -6
- package/dist/redis/pubsub-signal.d.ts +2 -0
- package/dist/redis/pubsub-signal.js +2 -2
- package/dist/schema-cli.js +4 -14
- package/dist/search/meilisearch-adapter.js +17 -6
- package/dist/search/purge-subject.js +8 -3
- package/dist/stack/drain-jobs.js +24 -12
- package/dist/stack/table-helpers.js +10 -8
- package/dist/testing/closed-connection-error.js +2 -3
- package/dist/testing/wait-for.js +1 -0
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.d.ts +1 -0
- package/dist/upgrade-cli.js +36 -15
- package/package.json +4 -4
- package/src/changes.json +206 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +14 -2
- package/src/scripts/codemod/migrate-db-raw.ts +16 -8
- package/src/scripts/codemod/migrate-open-to-all.ts +7 -1
- package/src/scripts/codemod/pii-personal-migration.ts +56 -19
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +83 -26
- package/dist/scripts/codemod/crypto-shredding-testing-move.d.ts +0 -2
- package/dist/scripts/codemod/crypto-shredding-testing-move.js +0 -109
- package/dist/scripts/codemod/migrate-cross-tenant.d.ts +0 -19
- package/dist/scripts/codemod/migrate-cross-tenant.js +0 -266
- package/dist/scripts/codemod/migrate-db-raw.d.ts +0 -23
- package/dist/scripts/codemod/migrate-db-raw.js +0 -345
- package/dist/scripts/codemod/migrate-open-to-all.d.ts +0 -22
- package/dist/scripts/codemod/migrate-open-to-all.js +0 -194
- package/dist/scripts/codemod/pii-personal-migration.d.ts +0 -11
- package/dist/scripts/codemod/pii-personal-migration.js +0 -615
- package/dist/scripts/codemod/unprocessable-error-details-reason.d.ts +0 -2
- package/dist/scripts/codemod/unprocessable-error-details-reason.js +0 -235
|
@@ -97,7 +97,7 @@ async function runLifecycle(ctx, type, data, handlerContext, user, origin, after
|
|
|
97
97
|
if (result.kind === "save") {
|
|
98
98
|
await lifecycle.runPostSave(type, result, handlerContext, HookPhases.inTransaction);
|
|
99
99
|
afterCommitHooks.push(async () => {
|
|
100
|
-
const afterCommitContext = await buildHandlerContext(ctx, type, user, origin, undefined,
|
|
100
|
+
const afterCommitContext = await buildHandlerContext(ctx, type, user, origin, undefined, undefined, undefined, true);
|
|
101
101
|
await lifecycle.runPostSave(type, result, afterCommitContext, HookPhases.afterCommit);
|
|
102
102
|
});
|
|
103
103
|
}
|
|
@@ -105,7 +105,7 @@ async function runLifecycle(ctx, type, data, handlerContext, user, origin, after
|
|
|
105
105
|
await lifecycle.runPreDelete(type, result, handlerContext);
|
|
106
106
|
await lifecycle.runPostDelete(type, result, handlerContext, HookPhases.inTransaction);
|
|
107
107
|
afterCommitHooks.push(async () => {
|
|
108
|
-
const afterCommitContext = await buildHandlerContext(ctx, type, user, origin, undefined,
|
|
108
|
+
const afterCommitContext = await buildHandlerContext(ctx, type, user, origin, undefined, undefined, undefined, true);
|
|
109
109
|
await lifecycle.runPostDelete(type, result, afterCommitContext, HookPhases.afterCommit);
|
|
110
110
|
});
|
|
111
111
|
}
|
|
@@ -115,8 +115,7 @@ async function runLifecycle(ctx, type, data, handlerContext, user, origin, after
|
|
|
115
115
|
//
|
|
116
116
|
// Contract:
|
|
117
117
|
// - `tx` is the active Drizzle transaction handle, or undefined for "no
|
|
118
|
-
// surrounding transaction" —
|
|
119
|
-
// transaction already committed (afterCommit phase).
|
|
118
|
+
// surrounding transaction" — no Postgres is configured.
|
|
120
119
|
// - `afterCommitHooks` collects deferred side-effects that must only fire
|
|
121
120
|
// after the transaction commits. The caller flushes them on commit, drops
|
|
122
121
|
// them on rollback. executeWrite never fires them directly.
|
|
@@ -145,6 +144,23 @@ async function isParentRowHiddenFromCaller(ctx, parentEntityName, parentId, user
|
|
|
145
144
|
return true;
|
|
146
145
|
return hasTenantColumn(table) && visibleRow["tenantId"] !== user.tenantId;
|
|
147
146
|
}
|
|
147
|
+
// A custom create handler may return a partial row. The ownership check only
|
|
148
|
+
// inspects the keys it is given, so a row missing the ownership-bound field
|
|
149
|
+
// would be denied (rule evaluated against undefined) or skipped entirely.
|
|
150
|
+
// Complete it from the stored row, read through the caller's tenant filter.
|
|
151
|
+
async function completeParentRow(ctx, parentEntityName, parentEntity, parentRow, user, tx) {
|
|
152
|
+
if (Object.keys(parentEntity.fields).every((fieldName) => fieldName in parentRow)) {
|
|
153
|
+
return parentRow;
|
|
154
|
+
}
|
|
155
|
+
const table = getTable(ctx, parentEntityName);
|
|
156
|
+
const source = resolveDbSource(ctx, tx);
|
|
157
|
+
if (!table || !source || typeof parentRow["id"] !== "string")
|
|
158
|
+
return parentRow;
|
|
159
|
+
const stored = await createTenantDb(source, user.tenantId, "tenant").fetchOne(table, {
|
|
160
|
+
id: parentRow["id"],
|
|
161
|
+
});
|
|
162
|
+
return stored ? { ...stored, ...parentRow } : parentRow;
|
|
163
|
+
}
|
|
148
164
|
// Nested-write orchestration (v1: depth=1, create-only, hasMany-only).
|
|
149
165
|
//
|
|
150
166
|
// When a parent `:create` handler's payload carries values under keys
|
|
@@ -233,7 +249,8 @@ export async function executeNestedWrite(ctx, type, payload, user, origin, tx, a
|
|
|
233
249
|
if (parentEntityName) {
|
|
234
250
|
const parentEntity = registry.getEntity(parentEntityName);
|
|
235
251
|
if (parentEntity) {
|
|
236
|
-
const
|
|
252
|
+
const ownershipRow = await completeParentRow(ctx, parentEntityName, parentEntity, parentRow, user, tx);
|
|
253
|
+
const deniedField = checkWriteFieldOwnership(parentEntity, ownershipRow, user);
|
|
237
254
|
if (deniedField) {
|
|
238
255
|
return writeFailure(new AccessDeniedError({
|
|
239
256
|
message: `nested-write: parent row ownership check failed on field "${deniedField}" of "${type}" — refusing to attach children to a parent row the caller does not own`,
|
|
@@ -2,8 +2,7 @@ import { runWithWriteOrigin } from "../api/request-context.js";
|
|
|
2
2
|
import { DEPRECATED_CROSS_TENANT_SIGNAL, isDeprecatedCrossTenantHandler, } from "../engine/entity-handlers.js";
|
|
3
3
|
import { TENANT_MEMBERSHIPS_QUERY } from "../engine/extension-names.js";
|
|
4
4
|
import { InternalError, reraiseAsKumikoError } from "../errors/index.js";
|
|
5
|
-
import { createEscapeHatchReportWindow } from "../observability/
|
|
6
|
-
import { getFallbackMeter, getFallbackTracer, registerStandardMetrics, } from "../observability/index.js";
|
|
5
|
+
import { createEscapeHatchReportWindow, getFallbackMeter, getFallbackTracer, registerStandardMetrics, } from "../observability/index.js";
|
|
7
6
|
import { INTERACTIVE_SIGN_IN_POLICY, resolveActiveMembershipFn } from "./active-membership.js";
|
|
8
7
|
import { runBatch, unwrapSingle } from "./dispatch-batch.js";
|
|
9
8
|
import { executeQuery } from "./dispatch-query.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { advanceConsumerPastEventReturning, removePendingGapReturning, selectSnapshotXmax, updateConsumerStatusReturning, } from "../db/queries/event-consumer.js";
|
|
1
|
+
import { advanceConsumerPastEventReturning, removePendingGapReturning, selectConsumerForUpdate, selectSnapshotXmax, updateConsumerStatusReturning, } from "../db/queries/event-consumer.js";
|
|
2
2
|
import { selectNextEventIdAfter, selectSmallestVisibleIdInRanges, } from "../db/queries/event-store.js";
|
|
3
3
|
import { coerceRow, extractTableInfo, selectMany } from "../db/query.js";
|
|
4
4
|
import { getEventsHighWaterMark } from "../event-store/index.js";
|
|
@@ -50,6 +50,14 @@ async function requireConsumerRow(db, name, instanceId) {
|
|
|
50
50
|
}
|
|
51
51
|
return row;
|
|
52
52
|
}
|
|
53
|
+
async function requireConsumerRowForUpdate(tx, name, instanceId) {
|
|
54
|
+
const raw = await selectConsumerForUpdate(tx, name, instanceId);
|
|
55
|
+
if (!raw) {
|
|
56
|
+
throw new Error(`Consumer "${name}" (instance_id="${instanceId}") has no state row — it hasn't run yet, the name is misspelled, or the instance is misspelled. ` +
|
|
57
|
+
`For per-instance consumers pass the instance_id explicitly; shared consumers use the default.`);
|
|
58
|
+
}
|
|
59
|
+
return coerceRow(raw, extractTableInfo(eventConsumerStateTable));
|
|
60
|
+
}
|
|
53
61
|
async function applyConsumerStatusTransition(db, name, instanceId, targetStatus) {
|
|
54
62
|
const raw = await updateConsumerStatusReturning(db, name, instanceId, targetStatus);
|
|
55
63
|
const updated = raw && coerceRow(raw, extractTableInfo(eventConsumerStateTable));
|
|
@@ -84,8 +92,10 @@ export async function enableConsumer(db, name, instanceId = SHARED_INSTANCE_SENT
|
|
|
84
92
|
// event after the cursor. Single TX so concurrent dispatcher passes can't
|
|
85
93
|
// double-advance. Nothing to skip → idempotent no-op.
|
|
86
94
|
export async function skipPoisonEvent(db, name, instanceId = SHARED_INSTANCE_SENTINEL) {
|
|
87
|
-
const before = await requireConsumerRow(db, name, instanceId);
|
|
88
95
|
return db.begin(async (tx) => {
|
|
96
|
+
// Locked read: a dispatcher turn committing new gaps between an unlocked
|
|
97
|
+
// read and the pending_gaps overwrite below would have them dropped.
|
|
98
|
+
const before = await requireConsumerRowForUpdate(tx, name, instanceId);
|
|
89
99
|
const pendingGaps = before.pendingGaps;
|
|
90
100
|
const failedEventId = before.lastFailedEventId;
|
|
91
101
|
const failedIsPending = failedEventId !== null && pendingGaps.some((gap) => rangeContainsId(gap, failedEventId));
|
|
@@ -31,6 +31,7 @@ export type EventDispatcher = {
|
|
|
31
31
|
runOnce(): Promise<DispatcherPassResult>;
|
|
32
32
|
ensureRegistered(): Promise<void>;
|
|
33
33
|
drain(): Promise<void>;
|
|
34
|
+
withBackgroundPassesPaused<T>(fn: () => Promise<T>): Promise<T>;
|
|
34
35
|
readonly consumers: readonly EventConsumer[];
|
|
35
36
|
};
|
|
36
37
|
export type EventDispatcherOptions = {
|
|
@@ -170,6 +170,15 @@ export function createEventDispatcher(options) {
|
|
|
170
170
|
// skip: errors already recorded per-consumer inside the pass
|
|
171
171
|
})));
|
|
172
172
|
}
|
|
173
|
+
let backgroundPassPauseDepth = 0;
|
|
174
|
+
function runBackgroundPass() {
|
|
175
|
+
// skip: background passes are paused
|
|
176
|
+
if (backgroundPassPauseDepth > 0)
|
|
177
|
+
return;
|
|
178
|
+
void runOnce().catch(() => {
|
|
179
|
+
// skip: per-consumer errors already recorded in the state row
|
|
180
|
+
});
|
|
181
|
+
}
|
|
173
182
|
async function runOnce() {
|
|
174
183
|
if (!preRegistered) {
|
|
175
184
|
throw new Error("EventDispatcher.runOnce() called before start() — consumer state rows are not registered. Call start() first (production) or ensureRegistered() (tests after truncating kumiko_event_consumers).");
|
|
@@ -445,11 +454,7 @@ export function createEventDispatcher(options) {
|
|
|
445
454
|
// any consumer that exists, including freshly-deployed ones.
|
|
446
455
|
await preRegisterConsumers(db, consumers, options.instanceId);
|
|
447
456
|
preRegistered = true;
|
|
448
|
-
timer = setInterval(
|
|
449
|
-
void runOnce().catch(() => {
|
|
450
|
-
// skip: per-consumer errors already recorded in the state row
|
|
451
|
-
});
|
|
452
|
-
}, pollIntervalMs);
|
|
457
|
+
timer = setInterval(runBackgroundPass, pollIntervalMs);
|
|
453
458
|
// NOTIFY-based wake-up: subscribe on the same channel that
|
|
454
459
|
// event-store.append fires on commit. Fires runOnce directly, no
|
|
455
460
|
// polling round-trip. The timer stays on as a belt-and-braces
|
|
@@ -464,11 +469,7 @@ export function createEventDispatcher(options) {
|
|
|
464
469
|
emitEventDispatcherListenConnected(meter, false);
|
|
465
470
|
if (options.pgClient) {
|
|
466
471
|
try {
|
|
467
|
-
const sub = await options.pgClient.listen(EVENTS_PUBSUB_CHANNEL, () => {
|
|
468
|
-
void runOnce().catch(() => {
|
|
469
|
-
// skip: per-consumer errors already recorded in the state row
|
|
470
|
-
});
|
|
471
|
-
}, () => {
|
|
472
|
+
const sub = await options.pgClient.listen(EVENTS_PUBSUB_CHANNEL, runBackgroundPass, () => {
|
|
472
473
|
// Fires on initial connect and again when postgres.js's own
|
|
473
474
|
// re-LISTEN lands after a drop; during an outage that re-LISTEN
|
|
474
475
|
// waits in its connection backoff (measured 10-30s after the DB
|
|
@@ -517,7 +518,21 @@ export function createEventDispatcher(options) {
|
|
|
517
518
|
await preRegisterConsumers(db, consumers, options.instanceId);
|
|
518
519
|
preRegistered = true;
|
|
519
520
|
},
|
|
520
|
-
drain
|
|
521
|
+
async drain() {
|
|
522
|
+
await drainInFlightPasses();
|
|
523
|
+
await drainInFlightTurns();
|
|
524
|
+
},
|
|
525
|
+
async withBackgroundPassesPaused(fn) {
|
|
526
|
+
backgroundPassPauseDepth++;
|
|
527
|
+
try {
|
|
528
|
+
await drainInFlightPasses();
|
|
529
|
+
await drainInFlightTurns();
|
|
530
|
+
return await fn();
|
|
531
|
+
}
|
|
532
|
+
finally {
|
|
533
|
+
backgroundPassPauseDepth--;
|
|
534
|
+
}
|
|
535
|
+
},
|
|
521
536
|
runOnce,
|
|
522
537
|
};
|
|
523
538
|
}
|
package/dist/pipeline/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { EscapeHatchReportWindow } from "../observability/escape-hatch-report.js";
|
|
2
|
-
export { createEscapeHatchReporter, createEscapeHatchReportWindow, ESCAPE_HATCH_USED_SIGNAL, fallbackEscapeHatchReporter, reportEscapeHatchUse, } from "../observability/escape-hatch-report.js";
|
|
2
|
+
export { createEscapeHatchReporter, createEscapeHatchReportWindow, ESCAPE_HATCH_USED_SIGNAL, fallbackEscapeHatchReporter, reportEscapeHatchUse, UNATTRIBUTED_ACTOR, } from "../observability/escape-hatch-report.js";
|
|
3
3
|
export type { ResolveAuthClaimsArgs } from "./auth-claims-resolver.js";
|
|
4
4
|
export { resolveAuthClaims } from "./auth-claims-resolver.js";
|
|
5
5
|
export { createCascadeDeleteHook } from "./cascade-handler.js";
|
package/dist/pipeline/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createEscapeHatchReporter, createEscapeHatchReportWindow, ESCAPE_HATCH_USED_SIGNAL, fallbackEscapeHatchReporter, reportEscapeHatchUse, } from "../observability/escape-hatch-report.js";
|
|
1
|
+
export { createEscapeHatchReporter, createEscapeHatchReportWindow, ESCAPE_HATCH_USED_SIGNAL, fallbackEscapeHatchReporter, reportEscapeHatchUse, UNATTRIBUTED_ACTOR, } from "../observability/escape-hatch-report.js";
|
|
2
2
|
export { resolveAuthClaims } from "./auth-claims-resolver.js";
|
|
3
3
|
export { createCascadeDeleteHook } from "./cascade-handler.js";
|
|
4
4
|
export { createDispatcher, dispatcherToWriteRef } from "./dispatcher.js";
|
|
@@ -3,21 +3,8 @@ import { asRawClient, runInSavepoint, transaction } from "../db/query.js";
|
|
|
3
3
|
import { AccessDeniedError, InternalError, memberResolutionReadOnlyDenied, } from "../errors/index.js";
|
|
4
4
|
import { resolveDbSource } from "./dispatch-shared.js";
|
|
5
5
|
const PG_READ_ONLY_SQLSTATE = "25006";
|
|
6
|
-
// Bun.SQL surfaces the PG SQLSTATE as `errno` (code stays "ERR_POSTGRES_SERVER_ERROR");
|
|
7
|
-
// postgres.js surfaces it as `code`, possibly one `.cause` layer down (DrizzleQueryError-style wrapping).
|
|
8
6
|
function isReadOnlyTransactionViolation(e) {
|
|
9
|
-
|
|
10
|
-
return true;
|
|
11
|
-
if (typeof e !== "object" || e === null)
|
|
12
|
-
return false;
|
|
13
|
-
// @cast-boundary error-details — Bun.SQL error shape (errno)
|
|
14
|
-
if (e.errno === PG_READ_ONLY_SQLSTATE)
|
|
15
|
-
return true;
|
|
16
|
-
// @cast-boundary error-details — Bun.SQL error shape (errno) under .cause
|
|
17
|
-
const cause = e.cause;
|
|
18
|
-
if (typeof cause !== "object" || cause === null)
|
|
19
|
-
return false;
|
|
20
|
-
return cause.errno === PG_READ_ONLY_SQLSTATE;
|
|
7
|
+
return extractPgError(e)?.code === PG_READ_ONLY_SQLSTATE;
|
|
21
8
|
}
|
|
22
9
|
async function runInDiscardedReadOnlySavepoint(tx, fn) {
|
|
23
10
|
const discardMarker = {};
|
|
@@ -8,5 +8,6 @@ export type MspRebuildDeps = {
|
|
|
8
8
|
readonly meter?: Meter;
|
|
9
9
|
readonly onMetrics?: (result: RebuildResult) => void;
|
|
10
10
|
readonly markDeadOnFailure?: boolean;
|
|
11
|
+
readonly signal?: AbortSignal;
|
|
11
12
|
};
|
|
12
13
|
export declare function rebuildMultiStreamProjection(mspName: string, deps: MspRebuildDeps): Promise<RebuildResult>;
|
|
@@ -71,6 +71,7 @@ export async function rebuildMultiStreamProjection(mspName, deps) {
|
|
|
71
71
|
const events = await selectMany(tx, eventsTable, { type: [...subscribedTypes], id: { lte: horizon } }, { orderBy: { col: "id", direction: "asc" } });
|
|
72
72
|
const upcasters = registry.getEventUpcasters();
|
|
73
73
|
for (const row of events) {
|
|
74
|
+
deps.signal?.throwIfAborted();
|
|
74
75
|
const raw = {
|
|
75
76
|
id: String(row.id),
|
|
76
77
|
aggregateId: row.aggregateId,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SseBroker } from "../api/sse-broker.js";
|
|
2
2
|
import type { EntityId, JobRunnerRef, Registry, TenantId } from "../engine/types/index.js";
|
|
3
3
|
import type { SearchAdapter, SearchDocument } from "../search/types.js";
|
|
4
4
|
import type { EventConsumer } from "./event-dispatcher.js";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { readNonEmptyStringArray, } from "../api/sse-broker.js";
|
|
1
2
|
import { collectSearchableSubjectFields, configuredPiiSubjectKms, decryptPiiFieldValues, isPiiCiphertext, PII_ERASED_SENTINEL, } from "../crypto/index.js";
|
|
2
3
|
import { createTenantDb, entityTableFromRegistry, rehydrateCompoundTypes } from "../db/index.js";
|
|
3
4
|
import { tenantChannel } from "../engine/constants.js";
|
|
5
|
+
import { InternalError } from "../errors/index.js";
|
|
4
6
|
// --- Search Index Consumer (async, via event-dispatcher) ---
|
|
5
7
|
//
|
|
6
8
|
// Search-Indexierung läuft seit D.4 als async EventConsumer über den event-
|
|
@@ -187,8 +189,13 @@ async function readProjectionRowForSearch(ctx, registry, entityName, entity, ten
|
|
|
187
189
|
// DbConnection: event-dispatcher creation is gated on it being set
|
|
188
190
|
// (api/server.ts), never a HandlerContext's already-tenant-scoped TenantDb.
|
|
189
191
|
const baseDb = ctx.db;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
+
// undefined must keep meaning "no live row" (the caller removes the index
|
|
193
|
+
// entry), so a missing db fails the event for retry instead.
|
|
194
|
+
if (!baseDb) {
|
|
195
|
+
throw new InternalError({
|
|
196
|
+
message: `search consumer: no db on the consumer context — cannot read the live row of "${entityName}" ${entityId}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
192
199
|
const table = entityTableFromRegistry(registry, entityName, entity);
|
|
193
200
|
const where = { id: entityId };
|
|
194
201
|
if (entity.softDelete && table["isDeleted"]) {
|
|
@@ -511,11 +518,6 @@ function readUserIdFromPreviousSnapshot(payload) {
|
|
|
511
518
|
const userId = previous["userId"];
|
|
512
519
|
return typeof userId === "string" && userId.length > 0 ? userId : undefined;
|
|
513
520
|
}
|
|
514
|
-
function readNonEmptyStringArray(value) {
|
|
515
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
516
|
-
return undefined;
|
|
517
|
-
return value.every((v) => typeof v === "string") ? value : undefined;
|
|
518
|
-
}
|
|
519
521
|
// session-revoked's scope resolution, in order:
|
|
520
522
|
// 1. streamScope === "revoked-sessions" with a non-empty sessionIds array
|
|
521
523
|
// (only the raw sessionRevoker callback sets this) → narrow "sessions"
|
|
@@ -10,6 +10,8 @@ export type TenantTimezoneCacheEntry = {
|
|
|
10
10
|
export type TenantTimezoneCache = {
|
|
11
11
|
get(tenantId: TenantId): TenantTimezoneCacheEntry | undefined;
|
|
12
12
|
set(tenantId: TenantId, value: string | undefined): void;
|
|
13
|
+
generation(): number;
|
|
14
|
+
setIfFresh(tenantId: TenantId, value: string | undefined, generationAtRead: number): void;
|
|
13
15
|
invalidate(tenantId: TenantId): void;
|
|
14
16
|
clear(): void;
|
|
15
17
|
size(): number;
|
|
@@ -10,20 +10,27 @@ const DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
|
10
10
|
const DEFAULT_MAX_ENTRIES = 1000;
|
|
11
11
|
export function createTenantTimezoneCache(opts = {}) {
|
|
12
12
|
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
13
|
-
|
|
13
|
+
// 0 or negative would never evict (size < max is never true) and grow unbounded.
|
|
14
|
+
const maxEntries = Math.max(1, opts.maxEntries ?? DEFAULT_MAX_ENTRIES);
|
|
14
15
|
const now = opts.now ?? (() => Date.now());
|
|
15
16
|
// Map insertion order doubles as LRU order: touch = delete+re-insert.
|
|
16
17
|
const entries = new Map();
|
|
18
|
+
let generation = 0;
|
|
17
19
|
function evictOldestIfFull() {
|
|
18
20
|
// skip: cache has room, nothing to evict
|
|
19
21
|
if (entries.size < maxEntries)
|
|
20
22
|
return;
|
|
21
23
|
const oldestKey = entries.keys().next().value;
|
|
22
|
-
// skip: defensive —
|
|
24
|
+
// skip: defensive — maxEntries >= 1 guarantees a non-empty map here
|
|
23
25
|
if (oldestKey === undefined)
|
|
24
26
|
return;
|
|
25
27
|
entries.delete(oldestKey);
|
|
26
28
|
}
|
|
29
|
+
function set(tenantId, value) {
|
|
30
|
+
entries.delete(tenantId);
|
|
31
|
+
evictOldestIfFull();
|
|
32
|
+
entries.set(tenantId, { value, expiresAt: now() + ttlMs });
|
|
33
|
+
}
|
|
27
34
|
return {
|
|
28
35
|
get(tenantId) {
|
|
29
36
|
const hit = entries.get(tenantId);
|
|
@@ -37,15 +44,22 @@ export function createTenantTimezoneCache(opts = {}) {
|
|
|
37
44
|
entries.set(tenantId, hit);
|
|
38
45
|
return { value: hit.value };
|
|
39
46
|
},
|
|
40
|
-
set
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
set,
|
|
48
|
+
generation() {
|
|
49
|
+
return generation;
|
|
50
|
+
},
|
|
51
|
+
setIfFresh(tenantId, value, generationAtRead) {
|
|
52
|
+
// skip: an invalidation ran since the read started, the value may be pre-write
|
|
53
|
+
if (generationAtRead !== generation)
|
|
54
|
+
return;
|
|
55
|
+
set(tenantId, value);
|
|
44
56
|
},
|
|
45
57
|
invalidate(tenantId) {
|
|
58
|
+
generation++;
|
|
46
59
|
entries.delete(tenantId);
|
|
47
60
|
},
|
|
48
61
|
clear() {
|
|
62
|
+
generation++;
|
|
49
63
|
entries.clear();
|
|
50
64
|
},
|
|
51
65
|
size() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RedisClientOptions } from "./client.js";
|
|
1
2
|
export type PubSubSignal = {
|
|
2
3
|
publish(channel: string, payload: unknown): void;
|
|
3
4
|
onMessage(listener: (channel: string, payload: unknown) => void): void;
|
|
@@ -7,5 +8,6 @@ export type PubSubSignalOptions = {
|
|
|
7
8
|
readonly redisUrl: string;
|
|
8
9
|
readonly channelPattern: string;
|
|
9
10
|
readonly label: string;
|
|
11
|
+
readonly clientOptions?: RedisClientOptions;
|
|
10
12
|
};
|
|
11
13
|
export declare function createRedisPubSubSignal(opts: PubSubSignalOptions): PubSubSignal;
|
|
@@ -9,8 +9,8 @@ function logConnectionError(label, role) {
|
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
export function createRedisPubSubSignal(opts) {
|
|
12
|
-
const publisher = createRedisClient(opts.redisUrl);
|
|
13
|
-
const subscriber = createRedisClient(opts.redisUrl);
|
|
12
|
+
const publisher = createRedisClient(opts.redisUrl, opts.clientOptions);
|
|
13
|
+
const subscriber = createRedisClient(opts.redisUrl, opts.clientOptions);
|
|
14
14
|
publisher.on("error", logConnectionError(opts.label, "publisher"));
|
|
15
15
|
subscriber.on("error", logConnectionError(opts.label, "subscriber"));
|
|
16
16
|
const listeners = new Set();
|
package/dist/schema-cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { assertValidMigrationName, baselineMigrations, createDbConnection, diffR
|
|
|
14
14
|
import { validateBoot } from "./engine/boot-validator.js";
|
|
15
15
|
import { createRegistry } from "./engine/registry.js";
|
|
16
16
|
import { createEventsTable } from "./event-store/index.js";
|
|
17
|
-
import { queueRebuildsFromMarkers, runPendingRebuilds } from "./migrations/index.js";
|
|
17
|
+
import { queueRebuildsFromMarkers, reportPendingRebuildRun, runPendingRebuilds, } from "./migrations/index.js";
|
|
18
18
|
import { createEventConsumerStateTable, createProjectionStateTable } from "./pipeline/index.js";
|
|
19
19
|
import { ensureTemporalPolyfill } from "./time/index.js";
|
|
20
20
|
const SNAPSHOT_FILENAME = ".snapshot.json";
|
|
@@ -49,6 +49,7 @@ function nextSequenceNumber(migrationsDir) {
|
|
|
49
49
|
}
|
|
50
50
|
return max + 1;
|
|
51
51
|
}
|
|
52
|
+
const SCHEMA_SUBCOMMANDS = ["generate", "validate", "apply", "baseline", "status"];
|
|
52
53
|
/**
|
|
53
54
|
* Runs a schema-CLI subcommand. `appCwd` is the app workspace root (where
|
|
54
55
|
* `kumiko/schema.ts` + `kumiko/migrations/` live). Returns a process exit code.
|
|
@@ -319,18 +320,7 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
319
320
|
});
|
|
320
321
|
const registry = createRegistry(options.features);
|
|
321
322
|
const rebuildRun = await runPendingRebuilds(db, registry, { thisRunTables });
|
|
322
|
-
|
|
323
|
-
out.log(` Rebuild ${rebuildRun.rebuilt.length} Projection(s)…`);
|
|
324
|
-
for (const r of rebuildRun.rebuilt) {
|
|
325
|
-
out.log(` ↻ ${r.projection} (${r.eventsProcessed} events)`);
|
|
326
|
-
}
|
|
327
|
-
out.log("");
|
|
328
|
-
}
|
|
329
|
-
if (rebuildRun.failed.length > 0) {
|
|
330
|
-
throw new Error(`Projection rebuild failed for: ${rebuildRun.failed
|
|
331
|
-
.map((f) => `${f.projection} (${f.error})`)
|
|
332
|
-
.join("; ")}. Table(s) stay queued in kumiko_pending_rebuilds — retried on the next apply.`);
|
|
333
|
-
}
|
|
323
|
+
reportPendingRebuildRun(rebuildRun, out.log);
|
|
334
324
|
}
|
|
335
325
|
return 0;
|
|
336
326
|
}
|
|
@@ -421,7 +411,7 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
421
411
|
// move on without ever running a migration.
|
|
422
412
|
if (sub !== undefined) {
|
|
423
413
|
out.err(` Unbekanntes Subcommand: "${sub}"`);
|
|
424
|
-
out.err(
|
|
414
|
+
out.err(` Erlaubt: ${SCHEMA_SUBCOMMANDS.join(" | ")}`);
|
|
425
415
|
return 1;
|
|
426
416
|
}
|
|
427
417
|
out.log("");
|
|
@@ -39,6 +39,12 @@ export function createMeilisearchAdapter(options) {
|
|
|
39
39
|
// always wins and is never overwritten by the lazy default — its promise
|
|
40
40
|
// sits in the map exactly like a lazily-triggered one.
|
|
41
41
|
const configuredTenants = new Map();
|
|
42
|
+
// Exact inverse of meilisearchTenantIndex. A bare startsWith(prefix) would also
|
|
43
|
+
// match a longer sibling prefix (`kumiko_` vs `kumiko_staging_`) sharing the
|
|
44
|
+
// instance; tenant ids never contain `_`, which excludes that sibling.
|
|
45
|
+
function isOwnTenantIndex(uid) {
|
|
46
|
+
return uid.startsWith(prefix) && /^t[0-9A-Za-z-]+$/.test(uid.slice(prefix.length));
|
|
47
|
+
}
|
|
42
48
|
async function applyConfig(tenantId, config) {
|
|
43
49
|
const index = client.index(meilisearchTenantIndex(prefix, tenantId));
|
|
44
50
|
const fields = config.rankingFields ?? config.searchableFields;
|
|
@@ -124,18 +130,23 @@ export function createMeilisearchAdapter(options) {
|
|
|
124
130
|
for (let offset = 0;; offset += INDEX_LIST_PAGE_SIZE) {
|
|
125
131
|
const page = await client.getIndexes({ limit: INDEX_LIST_PAGE_SIZE, offset });
|
|
126
132
|
for (const index of page.results) {
|
|
127
|
-
if (index.uid
|
|
133
|
+
if (isOwnTenantIndex(index.uid))
|
|
128
134
|
uids.push(index.uid);
|
|
129
135
|
}
|
|
130
136
|
if (page.results.length === 0 || offset + page.results.length >= page.total)
|
|
131
137
|
break;
|
|
132
138
|
}
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
// Cleared even when a delete throws midway: otherwise ensureConfigured treats the
|
|
140
|
+
// already-dropped tenants as configured and the next index() recreates an
|
|
141
|
+
// unconfigured index ("not filterable" on filtered search).
|
|
142
|
+
try {
|
|
143
|
+
for (const uid of uids) {
|
|
144
|
+
await awaitSucceededTask(client.deleteIndex(uid), REMOVE_TOLERATED_ERROR_CODES);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
configuredTenants.clear();
|
|
135
149
|
}
|
|
136
|
-
// Otherwise ensureConfigured treats the dropped tenants as configured and the
|
|
137
|
-
// next index() recreates an unconfigured index ("not filterable" on filtered search).
|
|
138
|
-
configuredTenants.clear();
|
|
139
150
|
return uids.length;
|
|
140
151
|
},
|
|
141
152
|
async removeBatch(tenantId, items) {
|
|
@@ -12,7 +12,7 @@ import { isSelfPiiField } from "../crypto/is-self-pii-field.js";
|
|
|
12
12
|
import { collectSearchableSubjectFields } 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
|
-
import { tableExists } from "../db/schema-inspection.js";
|
|
15
|
+
import { columnNamesOf, tableExists } from "../db/schema-inspection.js";
|
|
16
16
|
import { toSnakeCase } from "../utils/case.js";
|
|
17
17
|
function finalizePredicateParts(parts, params) {
|
|
18
18
|
if (parts.length === 0)
|
|
@@ -140,8 +140,8 @@ subject) {
|
|
|
140
140
|
const seen = new Set();
|
|
141
141
|
for (const feature of features.values()) {
|
|
142
142
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
143
|
+
const declaredFields = collectSearchableSubjectFields(entity);
|
|
144
|
+
if (declaredFields.length === 0)
|
|
145
145
|
continue;
|
|
146
146
|
const tableName = resolveTableName(entityName, entity, undefined);
|
|
147
147
|
// Same post-eraseKey hazard as nullBlindIndexesForSubject (fw#2550):
|
|
@@ -149,6 +149,11 @@ subject) {
|
|
|
149
149
|
// (and the audit event after it).
|
|
150
150
|
if (!(await tableExists(db, tableName)))
|
|
151
151
|
continue;
|
|
152
|
+
// A migrated table can still lack a later-added searchable column.
|
|
153
|
+
const columns = await columnNamesOf(db, tableName);
|
|
154
|
+
const fields = declaredFields.filter((f) => columns.has(toSnakeCase(f)));
|
|
155
|
+
if (fields.length === 0)
|
|
156
|
+
continue;
|
|
152
157
|
const predicate = buildSubjectPredicate(entity, fields, likePattern, entityName, subject);
|
|
153
158
|
const rows = await collectMatchingRowsForEntity(db, tableName, predicate.sql, predicate.params);
|
|
154
159
|
for (const row of rows) {
|
package/dist/stack/drain-jobs.js
CHANGED
|
@@ -76,24 +76,36 @@ export async function drainJobs(stack, tracker) {
|
|
|
76
76
|
.filter((consumer) => (consumer.delivery ?? "shared") === "shared")
|
|
77
77
|
.map((consumer) => consumer.name);
|
|
78
78
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
try {
|
|
80
|
+
let consecutiveIdlePasses = 0;
|
|
81
|
+
while (consecutiveIdlePasses < REQUIRED_IDLE_PASSES) {
|
|
82
|
+
await eventDispatcher?.runOnce();
|
|
83
|
+
const pending = await jobRunner.countPendingJobs();
|
|
84
|
+
const caughtUp = await sharedConsumersCaughtUp(db, sharedConsumerNames);
|
|
85
|
+
consecutiveIdlePasses = pending === 0 && caughtUp ? consecutiveIdlePasses + 1 : 0;
|
|
86
|
+
if (consecutiveIdlePasses < REQUIRED_IDLE_PASSES) {
|
|
87
|
+
await Promise.race([tracker.waitForActivity(), sleep(DRAIN_IDLE_POLL_MS)]);
|
|
88
|
+
}
|
|
87
89
|
}
|
|
88
90
|
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
// A failed job often kills its consumer, so the drain error is only the follow-up symptom.
|
|
93
|
+
if (tracker.failures.size === 0)
|
|
94
|
+
throw err;
|
|
95
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
96
|
+
throw new Error(`${message}; ${describeJobFailures(tracker)}`, { cause: err });
|
|
97
|
+
}
|
|
89
98
|
if (tracker.failures.size > 0) {
|
|
90
|
-
|
|
91
|
-
.map((failure) => `${failure.jobName}: ${failure.error}`)
|
|
92
|
-
.join("; ");
|
|
93
|
-
throw new Error(`drainJobs: job(s) failed: ${details}`);
|
|
99
|
+
throw new Error(`drainJobs: ${describeJobFailures(tracker)}`);
|
|
94
100
|
}
|
|
95
101
|
}
|
|
96
102
|
finally {
|
|
97
103
|
tracker.clear();
|
|
98
104
|
}
|
|
99
105
|
}
|
|
106
|
+
function describeJobFailures(tracker) {
|
|
107
|
+
const details = [...tracker.failures.values()]
|
|
108
|
+
.map((failure) => `${failure.jobName}: ${failure.error}`)
|
|
109
|
+
.join("; ");
|
|
110
|
+
return `job(s) failed: ${details}`;
|
|
111
|
+
}
|
|
@@ -117,12 +117,6 @@ function renderColumnType(col) {
|
|
|
117
117
|
* rebuilding it.
|
|
118
118
|
*/
|
|
119
119
|
export async function resetEventStore(stack, extraTables = []) {
|
|
120
|
-
// A pass the dispatcher's own timer/LISTEN already started (from a
|
|
121
|
-
// preceding test) may still be writing into the tables truncated below —
|
|
122
|
-
// drain it first so the TRUNCATE never races a write it didn't cause.
|
|
123
|
-
if (stack.eventDispatcher) {
|
|
124
|
-
await stack.eventDispatcher.drain();
|
|
125
|
-
}
|
|
126
120
|
const frameworkTables = [
|
|
127
121
|
"kumiko_events",
|
|
128
122
|
"kumiko_event_consumers",
|
|
@@ -131,8 +125,16 @@ export async function resetEventStore(stack, extraTables = []) {
|
|
|
131
125
|
"kumiko_projections",
|
|
132
126
|
];
|
|
133
127
|
const extraNames = extraTables.map((t) => (typeof t === "string" ? t : tableNameOf(t)));
|
|
134
|
-
|
|
128
|
+
const truncateAndReregister = async () => {
|
|
129
|
+
await truncateTablesRestartIdentity(stack.db, [...frameworkTables, ...extraNames]);
|
|
130
|
+
await stack.eventDispatcher?.ensureRegistered();
|
|
131
|
+
};
|
|
132
|
+
// The dispatcher's own timer/LISTEN must not start a pass between the
|
|
133
|
+
// TRUNCATE and the consumer re-registration, so they are paused for the whole reset.
|
|
135
134
|
if (stack.eventDispatcher) {
|
|
136
|
-
await stack.eventDispatcher.
|
|
135
|
+
await stack.eventDispatcher.withBackgroundPassesPaused(truncateAndReregister);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
await truncateAndReregister();
|
|
137
139
|
}
|
|
138
140
|
}
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
// the production matcher checks driver-specific codes a fake can't reproduce.
|
|
3
3
|
import postgres from "postgres";
|
|
4
4
|
import { isClosedConnectionError } from "../bun-db/query.js";
|
|
5
|
+
import { requireEnv } from "../stack/db.js";
|
|
5
6
|
export function testDatabaseUrl() {
|
|
6
|
-
return (
|
|
7
|
-
process.env["DATABASE_URL"] ??
|
|
8
|
-
"postgresql://kumiko:kumiko@localhost:15432/kumiko_test");
|
|
7
|
+
return requireEnv("TEST_DATABASE_URL");
|
|
9
8
|
}
|
|
10
9
|
const MAX_WARMUP_ROUNDS = 10;
|
|
11
10
|
const READS_PER_ROUND = 5;
|
package/dist/testing/wait-for.js
CHANGED
package/dist/ui-types/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { metricField } from "../engine/types/screen.js";
|
|
|
14
14
|
export type { TargetRef } from "../engine/types/target-ref.js";
|
|
15
15
|
export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node.js";
|
|
16
16
|
export type { WorkspaceDefinition } from "../engine/types/workspace.js";
|
|
17
|
-
export { PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
|
|
17
|
+
export { ACTION_FORM_ENTITY, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
|
|
18
18
|
export type { AppSchema, FeatureSchema, QualifiedContentCollection, WorkspaceSchema, } from "./app-schema.js";
|
|
19
19
|
export { type Formality, formalLocaleTag } from "./formality.js";
|
|
20
20
|
export type { ListRowMetaColumnType, ListRowMetaReference, ReferenceLookupSource, SystemReferenceLabel, } from "./list-row-meta.js";
|
package/dist/ui-types/index.js
CHANGED
|
@@ -25,7 +25,7 @@ export { parseRefTarget } from "../engine/parse-ref-target.js";
|
|
|
25
25
|
export { evalFieldCondition, explicitListScreenId, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
|
|
26
26
|
export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
|
|
27
27
|
export { metricField } from "../engine/types/screen.js";
|
|
28
|
-
export { PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
|
|
28
|
+
export { ACTION_FORM_ENTITY, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
|
|
29
29
|
export { formalLocaleTag } from "./formality.js";
|
|
30
30
|
export { LIST_ROW_META_COLUMNS, LIST_ROW_META_REFERENCES, REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "./list-row-meta.js";
|
|
31
31
|
export { resolveTranslationValue, translationValueOtherText, } from "./plural.js";
|
package/dist/upgrade-cli.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type UpgradeCliOut = {
|
|
|
2
2
|
readonly log: (line: string) => void;
|
|
3
3
|
readonly err: (line: string) => void;
|
|
4
4
|
};
|
|
5
|
+
export declare function findFeatureChangelogFiles(featuresDir: string): string[];
|
|
5
6
|
export declare function findPackageChangelogFiles(cwd: string): string[];
|
|
6
7
|
export declare function findFeaturesDirs(cwd: string): string[];
|
|
7
8
|
export declare function findCodemodScriptsRoot(repoRoot: string): string | null;
|