@cosmicdrift/kumiko-framework 0.334.0 → 0.336.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/anonymous-live-entities.d.ts +2 -0
- package/dist/api/anonymous-live-entities.js +17 -0
- 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/index.d.ts +2 -1
- package/dist/api/index.js +1 -0
- 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 +20 -10
- package/dist/api/sse-broker.d.ts +3 -0
- package/dist/api/sse-broker.js +14 -0
- package/dist/api/sse-route.d.ts +5 -2
- package/dist/api/sse-route.js +71 -11
- 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 +32 -11
- 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 +45 -38
- 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 +53 -5
- package/dist/engine/boot-validator/screens.d.ts +2 -2
- package/dist/engine/boot-validator/screens.js +125 -46
- 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 +65 -20
- package/dist/engine/build-config-feature-schema.js +8 -1
- package/dist/engine/changeset-changes.js +24 -10
- package/dist/engine/create-app.js +10 -1
- package/dist/engine/extension-names.d.ts +4 -0
- package/dist/engine/extension-names.js +4 -0
- package/dist/engine/feature-ast/extractors/events.js +11 -8
- package/dist/engine/feature-ast/extractors/handlers.js +39 -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/feature-entity-handlers.d.ts +1 -0
- package/dist/engine/feature-entity-handlers.js +2 -0
- 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/registry-validate.d.ts +1 -0
- package/dist/engine/registry-validate.js +14 -0
- package/dist/engine/registry.js +2 -1
- 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 +34 -7
- package/dist/engine/steps/_read-source.js +8 -0
- package/dist/engine/system-user.d.ts +1 -0
- package/dist/engine/system-user.js +3 -0
- package/dist/engine/where-rule-lint.js +22 -4
- package/dist/env/dry-run.js +10 -3
- 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 +10 -2
- 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 +172 -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 +2 -1
- package/dist/observability/index.js +2 -1
- package/dist/observability/standard-metrics.d.ts +4 -0
- package/dist/observability/standard-metrics.js +16 -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-delivery.js +5 -0
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +30 -12
- 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 +19 -20
- package/dist/pipeline/tenant-timezone-cache.d.ts +2 -0
- package/dist/pipeline/tenant-timezone-cache.js +20 -6
- package/dist/pipeline/write-origin.js +13 -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-event-consumers.js +3 -0
- package/dist/stack/drain-jobs.d.ts +3 -1
- package/dist/stack/drain-jobs.js +55 -13
- 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 +264 -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 +111 -44
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +136 -42
- 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
|
@@ -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) {
|
|
@@ -17,6 +17,9 @@ export async function drainEventConsumers(stack, consumerNames, options) {
|
|
|
17
17
|
"multiStreamProjection is wired for this stack.");
|
|
18
18
|
}
|
|
19
19
|
const maxPasses = options?.maxPasses ?? DEFAULT_MAX_PASSES;
|
|
20
|
+
if (!Number.isInteger(maxPasses) || maxPasses < 1) {
|
|
21
|
+
throw new Error("drainEventConsumers: maxPasses must be a positive integer");
|
|
22
|
+
}
|
|
20
23
|
const target = await getEventsHighWaterMark(db);
|
|
21
24
|
for (let pass = 0; pass < maxPasses; pass++) {
|
|
22
25
|
await eventDispatcher.runOnce();
|
|
@@ -17,4 +17,6 @@ export type DrainJobsStack = {
|
|
|
17
17
|
readonly eventDispatcher?: EventDispatcher;
|
|
18
18
|
readonly jobRunner?: JobRunner;
|
|
19
19
|
};
|
|
20
|
-
export declare function drainJobs(stack: DrainJobsStack, tracker: JobFailureTracker
|
|
20
|
+
export declare function drainJobs(stack: DrainJobsStack, tracker: JobFailureTracker, options?: {
|
|
21
|
+
readonly timeoutMs?: number;
|
|
22
|
+
}): Promise<void>;
|
package/dist/stack/drain-jobs.js
CHANGED
|
@@ -12,6 +12,9 @@ const DRAIN_IDLE_POLL_MS = 25;
|
|
|
12
12
|
// enqueue that lands just after the first idle read still gets picked up
|
|
13
13
|
// by the second pass instead of racing straight past it.
|
|
14
14
|
const REQUIRED_IDLE_PASSES = 2;
|
|
15
|
+
// Below the 15s per-test budget so a stuck drain reports what is open rather
|
|
16
|
+
// than surfacing as a bare test timeout.
|
|
17
|
+
const DEFAULT_DRAIN_TIMEOUT_MS = 10_000;
|
|
15
18
|
function sleep(ms) {
|
|
16
19
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
20
|
}
|
|
@@ -47,6 +50,26 @@ export function createJobFailureTracker() {
|
|
|
47
50
|
},
|
|
48
51
|
};
|
|
49
52
|
}
|
|
53
|
+
// Diagnostic only: lets the deadline error name what was still open.
|
|
54
|
+
async function describeLaggingConsumers(db, consumerNames) {
|
|
55
|
+
// skip: no shared consumers wired — nothing to catch up on.
|
|
56
|
+
if (consumerNames.length === 0)
|
|
57
|
+
return [];
|
|
58
|
+
const target = await getEventsHighWaterMark(db);
|
|
59
|
+
const lagging = [];
|
|
60
|
+
for (const name of consumerNames) {
|
|
61
|
+
const state = await getConsumerState(db, name);
|
|
62
|
+
if (state && state.lastProcessedEventId < target) {
|
|
63
|
+
lagging.push(`${name} (status=${state.status}, lastProcessedEventId=${state.lastProcessedEventId}, target=${target})`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return lagging;
|
|
67
|
+
}
|
|
68
|
+
async function drainTimeoutError(db, consumerNames, timeoutMs, pending) {
|
|
69
|
+
const lagging = await describeLaggingConsumers(db, consumerNames);
|
|
70
|
+
return new Error(`drainJobs: not drained after ${timeoutMs}ms — pending jobs: ${pending}, ` +
|
|
71
|
+
`consumers behind: ${lagging.length === 0 ? "none" : lagging.join("; ")}`);
|
|
72
|
+
}
|
|
50
73
|
async function sharedConsumersCaughtUp(db, consumerNames) {
|
|
51
74
|
// skip: no shared consumers wired — nothing to catch up on.
|
|
52
75
|
if (consumerNames.length === 0)
|
|
@@ -66,8 +89,12 @@ async function sharedConsumersCaughtUp(db, consumerNames) {
|
|
|
66
89
|
}
|
|
67
90
|
return allCaughtUp;
|
|
68
91
|
}
|
|
69
|
-
|
|
92
|
+
// Fails a stuck drain with a diagnosis instead of a generic test-runner timeout.
|
|
93
|
+
// It only bounds the loop; it never replaces waiting for real work.
|
|
94
|
+
export async function drainJobs(stack, tracker, options) {
|
|
70
95
|
const { db, eventDispatcher, jobRunner } = stack;
|
|
96
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
97
|
+
const deadline = Date.now() + timeoutMs;
|
|
71
98
|
if (!jobRunner) {
|
|
72
99
|
throw new Error("drainJobs: stack.jobRunner is undefined — pass `jobs: {...}` to setupTestStack and " +
|
|
73
100
|
"register at least one r.job(...) so a runner is built.");
|
|
@@ -76,24 +103,39 @@ export async function drainJobs(stack, tracker) {
|
|
|
76
103
|
.filter((consumer) => (consumer.delivery ?? "shared") === "shared")
|
|
77
104
|
.map((consumer) => consumer.name);
|
|
78
105
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
106
|
+
try {
|
|
107
|
+
let consecutiveIdlePasses = 0;
|
|
108
|
+
while (consecutiveIdlePasses < REQUIRED_IDLE_PASSES) {
|
|
109
|
+
await eventDispatcher?.runOnce();
|
|
110
|
+
const pending = await jobRunner.countPendingJobs();
|
|
111
|
+
const caughtUp = await sharedConsumersCaughtUp(db, sharedConsumerNames);
|
|
112
|
+
consecutiveIdlePasses = pending === 0 && caughtUp ? consecutiveIdlePasses + 1 : 0;
|
|
113
|
+
if (consecutiveIdlePasses < REQUIRED_IDLE_PASSES) {
|
|
114
|
+
if (Date.now() > deadline) {
|
|
115
|
+
throw await drainTimeoutError(db, sharedConsumerNames, timeoutMs, pending);
|
|
116
|
+
}
|
|
117
|
+
await Promise.race([tracker.waitForActivity(), sleep(DRAIN_IDLE_POLL_MS)]);
|
|
118
|
+
}
|
|
87
119
|
}
|
|
88
120
|
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
// A failed job often kills its consumer, so the drain error is only the follow-up symptom.
|
|
123
|
+
if (tracker.failures.size === 0)
|
|
124
|
+
throw err;
|
|
125
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
126
|
+
throw new Error(`${message}; ${describeJobFailures(tracker)}`, { cause: err });
|
|
127
|
+
}
|
|
89
128
|
if (tracker.failures.size > 0) {
|
|
90
|
-
|
|
91
|
-
.map((failure) => `${failure.jobName}: ${failure.error}`)
|
|
92
|
-
.join("; ");
|
|
93
|
-
throw new Error(`drainJobs: job(s) failed: ${details}`);
|
|
129
|
+
throw new Error(`drainJobs: ${describeJobFailures(tracker)}`);
|
|
94
130
|
}
|
|
95
131
|
}
|
|
96
132
|
finally {
|
|
97
133
|
tracker.clear();
|
|
98
134
|
}
|
|
99
135
|
}
|
|
136
|
+
function describeJobFailures(tracker) {
|
|
137
|
+
const details = [...tracker.failures.values()]
|
|
138
|
+
.map((failure) => `${failure.jobName}: ${failure.error}`)
|
|
139
|
+
.join("; ");
|
|
140
|
+
return `job(s) failed: ${details}`;
|
|
141
|
+
}
|
|
@@ -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;
|
package/dist/upgrade-cli.js
CHANGED
|
@@ -56,10 +56,10 @@ function readChangelogFile(filePath) {
|
|
|
56
56
|
return [];
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
function
|
|
59
|
+
export function findFeatureChangelogFiles(featuresDir) {
|
|
60
60
|
if (!existsSync(featuresDir))
|
|
61
61
|
return [];
|
|
62
|
-
const
|
|
62
|
+
const files = [];
|
|
63
63
|
const features = readdirSync(featuresDir, { withFileTypes: true })
|
|
64
64
|
.filter((d) => d.isDirectory())
|
|
65
65
|
.map((d) => d.name);
|
|
@@ -71,9 +71,13 @@ function collectChangelogs(featuresDir) {
|
|
|
71
71
|
const srcLayout = join(featuresDir, name, "src", "changes.json");
|
|
72
72
|
const flatLayout = join(featuresDir, name, "changes.json");
|
|
73
73
|
const changelogPath = existsSync(srcLayout) ? srcLayout : flatLayout;
|
|
74
|
-
|
|
74
|
+
if (existsSync(changelogPath))
|
|
75
|
+
files.push(changelogPath);
|
|
75
76
|
}
|
|
76
|
-
return
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
79
|
+
function collectChangelogs(featuresDir) {
|
|
80
|
+
return findFeatureChangelogFiles(featuresDir).flatMap(readChangelogFile);
|
|
77
81
|
}
|
|
78
82
|
// bundled-features is excluded: findFeaturesDirs collects it per feature
|
|
79
83
|
// (src/<feature>/changes.json), every other package has one src/changes.json.
|
|
@@ -99,7 +103,8 @@ export function findPackageChangelogFiles(cwd) {
|
|
|
99
103
|
const scopeDir = join(dir, "node_modules/@cosmicdrift");
|
|
100
104
|
if (existsSync(scopeDir)) {
|
|
101
105
|
const pkgNames = readdirSync(scopeDir, { withFileTypes: true })
|
|
102
|
-
|
|
106
|
+
// Dirent has lstat semantics: workspace-linked packages are symlinks.
|
|
107
|
+
.filter((d) => (d.isDirectory() || d.isSymbolicLink()) && d.name !== "kumiko-bundled-features")
|
|
103
108
|
.map((d) => d.name);
|
|
104
109
|
for (const name of pkgNames) {
|
|
105
110
|
if (seenNames.has(name))
|
|
@@ -257,21 +262,21 @@ function writeUpgradeMarker(targetDir, marker) {
|
|
|
257
262
|
mkdirSync(dir, { recursive: true });
|
|
258
263
|
writeFileSync(join(dir, "upgrade-state.json"), `${JSON.stringify(marker, null, 2)}\n`, "utf-8");
|
|
259
264
|
}
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
265
|
+
// A missing marker is a bootstrap app that never ran `--apply` (fw#2299); an
|
|
266
|
+
// existing but unreadable one must not silently fall back to the installed
|
|
267
|
+
// version, or a bare `--apply` would skip every codemod in between.
|
|
263
268
|
function readMarkerVersion(targetDir) {
|
|
264
269
|
const markerPath = join(targetDir, ".kumiko", "upgrade-state.json");
|
|
265
270
|
if (!existsSync(markerPath))
|
|
266
|
-
return
|
|
271
|
+
return { kind: "missing" };
|
|
267
272
|
try {
|
|
268
273
|
const parsed = JSON.parse(readFileSync(markerPath, "utf-8"));
|
|
269
274
|
return typeof parsed.version === "string" && SEMVER_RE.test(parsed.version)
|
|
270
|
-
? parsed.version
|
|
271
|
-
:
|
|
275
|
+
? { kind: "ok", version: parsed.version }
|
|
276
|
+
: { kind: "invalid" };
|
|
272
277
|
}
|
|
273
278
|
catch {
|
|
274
|
-
return
|
|
279
|
+
return { kind: "invalid" };
|
|
275
280
|
}
|
|
276
281
|
}
|
|
277
282
|
/** Highest pending version strictly below the earliest open manual breaking
|
|
@@ -337,10 +342,12 @@ markerVersion) {
|
|
|
337
342
|
: " ✓ No breaking changes pending.");
|
|
338
343
|
if (!dryRun) {
|
|
339
344
|
const markerVer = markerVersionForPending(pending, manualEntries, markerVersion);
|
|
345
|
+
const pendingManual = manualEntries.map((e) => ({ version: e.version, title: e.title }));
|
|
340
346
|
writeUpgradeMarker(targetDir, {
|
|
341
347
|
version: markerVer,
|
|
342
348
|
appliedAt: Temporal.Now.instant().toString(),
|
|
343
349
|
codemods: [],
|
|
350
|
+
...(pendingManual.length > 0 && { pendingManual }),
|
|
344
351
|
});
|
|
345
352
|
out.log(` ✓ Applied 0 codemod(s). Wrote ${join(targetDir, ".kumiko/upgrade-state.json")}`);
|
|
346
353
|
logManualMarkerOutcome(out, manualEntries, markerVer);
|
|
@@ -431,7 +438,14 @@ export async function runUpgradeCli(argv, cwd, out, options) {
|
|
|
431
438
|
// when the marker says otherwise — a bare `--apply` would then always
|
|
432
439
|
// report "Nothing new" and bootstrap the marker onto the installed
|
|
433
440
|
// version, hiding breaking changes the marker never actually saw.
|
|
434
|
-
const
|
|
441
|
+
const marker = fromFlag === undefined ? readMarkerVersion(targetDir) : { kind: "missing" };
|
|
442
|
+
if (marker.kind === "invalid") {
|
|
443
|
+
out.err("");
|
|
444
|
+
out.err(" invalid .kumiko/upgrade-state.json, fix it or pass --from <version>");
|
|
445
|
+
out.err("");
|
|
446
|
+
return 1;
|
|
447
|
+
}
|
|
448
|
+
const currentVersion = fromFlag ?? (marker.kind === "ok" ? marker.version : undefined) ?? installedVersion;
|
|
435
449
|
if (!currentVersion) {
|
|
436
450
|
out.err("");
|
|
437
451
|
out.err(" Could not detect Kumiko version.");
|
|
@@ -465,10 +479,17 @@ export async function runUpgradeCli(argv, cwd, out, options) {
|
|
|
465
479
|
if (getFlag(args, "apply")) {
|
|
466
480
|
// Marker must reflect what is actually installed under the target (or
|
|
467
481
|
// cwd), not the filter baseline above — otherwise CI stays green forever.
|
|
468
|
-
const markerVersion =
|
|
482
|
+
const markerVersion = dirFlag ? readCurrentVersion(targetDir) : installedVersion;
|
|
469
483
|
const dryRun = getFlag(args, "dry-run");
|
|
484
|
+
if (markerVersion === null && !dryRun) {
|
|
485
|
+
out.err("");
|
|
486
|
+
out.err(` Could not detect the installed Kumiko version under ${targetDir}.`);
|
|
487
|
+
out.err(" Refusing to write an upgrade marker without it (--from is only a filter).");
|
|
488
|
+
out.err("");
|
|
489
|
+
return 1;
|
|
490
|
+
}
|
|
470
491
|
out.log("");
|
|
471
|
-
const code = await applyCodemods(out, pending, repoRoot, targetDir, dryRun, markerVersion);
|
|
492
|
+
const code = await applyCodemods(out, pending, repoRoot, targetDir, dryRun, markerVersion ?? currentVersion);
|
|
472
493
|
out.log("");
|
|
473
494
|
return code;
|
|
474
495
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.336.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -206,8 +206,8 @@
|
|
|
206
206
|
"./package.json": "./package.json"
|
|
207
207
|
},
|
|
208
208
|
"dependencies": {
|
|
209
|
-
"@cosmicdrift/kumiko-http": "0.
|
|
210
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
209
|
+
"@cosmicdrift/kumiko-http": "0.336.0",
|
|
210
|
+
"@cosmicdrift/kumiko-types": "0.336.0",
|
|
211
211
|
"bullmq": "^5.76.7",
|
|
212
212
|
"bun-types": "^1.3.13",
|
|
213
213
|
"hono": "^4.13.1",
|
|
@@ -223,7 +223,7 @@
|
|
|
223
223
|
"zod": "^4.4.3"
|
|
224
224
|
},
|
|
225
225
|
"devDependencies": {
|
|
226
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
226
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.336.0",
|
|
227
227
|
"bun-types": "^1.3.13",
|
|
228
228
|
"pino-pretty": "^13.1.3"
|
|
229
229
|
},
|