@cosmicdrift/kumiko-framework 0.333.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/eagerload.js +2 -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/config-deps.js +13 -0
- 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 +44 -4
- 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 +138 -36
- 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 -19
- package/dist/engine/build-config-feature-schema.js +106 -22
- package/dist/engine/changeset-changes.js +24 -10
- package/dist/engine/config-helpers.d.ts +2 -2
- package/dist/engine/entity-handlers.js +7 -2
- 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/feature-entity-handlers.js +22 -0
- package/dist/engine/field-access.d.ts +2 -0
- package/dist/engine/field-access.js +40 -0
- package/dist/engine/index.d.ts +5 -4
- package/dist/engine/index.js +4 -3
- 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 +2 -0
- package/dist/engine/schema-builder.js +20 -3
- 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/types/index.d.ts +1 -1
- 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-ref-entity.d.ts +6 -0
- package/dist/files/file-ref-table.d.ts +6 -0
- 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 +28 -35
- package/dist/i18n/settings-hub-keys.js +21 -4
- 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-batch.js +8 -3
- 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 +27 -7
- package/dist/pipeline/dispatcher-utils.js +4 -1
- 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/pipeline/write-result-masking.d.ts +6 -0
- package/dist/pipeline/write-result-masking.js +36 -0
- 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/shared-entities.d.ts +10 -0
- package/dist/testing/wait-for.js +1 -0
- package/dist/ui-types/index.d.ts +2 -2
- 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 +218 -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
|
@@ -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() {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { WriteResult } from "../engine/types/index.js";
|
|
2
|
+
import type { BatchCommand, BatchResult, DispatchContext } from "./dispatch-shared.js";
|
|
3
|
+
type EntityLookup = Pick<DispatchContext["registry"], "getHandlerEntity" | "getEntity">;
|
|
4
|
+
export declare function maskWriteResultForClient(registry: EntityLookup, handlerType: string, result: WriteResult): WriteResult;
|
|
5
|
+
export declare function maskBatchResultForClient(registry: EntityLookup, commands: readonly BatchCommand[], batch: BatchResult): BatchResult;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { maskWriteOnlyFields } from "../engine/field-access.js";
|
|
2
|
+
import { isLifecycleResult } from "./dispatcher-utils.js";
|
|
3
|
+
function maskLifecycleOrRow(entity, data) {
|
|
4
|
+
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
5
|
+
return data;
|
|
6
|
+
if (!isLifecycleResult(data))
|
|
7
|
+
return maskWriteOnlyFields(entity, data); // @cast-boundary engine-payload
|
|
8
|
+
if (data.kind === "save") {
|
|
9
|
+
return {
|
|
10
|
+
...data,
|
|
11
|
+
data: maskWriteOnlyFields(entity, data.data),
|
|
12
|
+
changes: maskWriteOnlyFields(entity, data.changes),
|
|
13
|
+
previous: maskWriteOnlyFields(entity, data.previous),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return { ...data, data: maskWriteOnlyFields(entity, data.data) };
|
|
17
|
+
}
|
|
18
|
+
// Copy-on-mask: the original results still feed postSaveBatch hooks, which
|
|
19
|
+
// need the plaintext. Only what leaves the dispatcher is masked.
|
|
20
|
+
export function maskWriteResultForClient(registry, handlerType, result) {
|
|
21
|
+
if (!result.isSuccess)
|
|
22
|
+
return result;
|
|
23
|
+
const entityName = registry.getHandlerEntity(handlerType);
|
|
24
|
+
const entity = entityName ? registry.getEntity(entityName) : undefined;
|
|
25
|
+
if (!entity)
|
|
26
|
+
return result;
|
|
27
|
+
const data = maskLifecycleOrRow(entity, result.data);
|
|
28
|
+
return data === result.data ? result : { ...result, data };
|
|
29
|
+
}
|
|
30
|
+
export function maskBatchResultForClient(registry, commands, batch) {
|
|
31
|
+
const results = batch.results.map((result, index) => {
|
|
32
|
+
const command = commands[index];
|
|
33
|
+
return command ? maskWriteResultForClient(registry, command.type, result) : result;
|
|
34
|
+
});
|
|
35
|
+
return { ...batch, results };
|
|
36
|
+
}
|
|
@@ -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;
|
|
@@ -9,6 +9,7 @@ export declare const sharedWidgetEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
9
9
|
readonly filterable?: boolean;
|
|
10
10
|
readonly encrypted?: boolean;
|
|
11
11
|
readonly sensitive?: boolean;
|
|
12
|
+
readonly writeOnly?: boolean;
|
|
12
13
|
readonly readAsInstruction?: boolean;
|
|
13
14
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
14
15
|
readonly default?: string;
|
|
@@ -31,6 +32,7 @@ export declare const sharedWidgetTable: import("../db/table-builder.js").EntityT
|
|
|
31
32
|
readonly filterable?: boolean;
|
|
32
33
|
readonly encrypted?: boolean;
|
|
33
34
|
readonly sensitive?: boolean;
|
|
35
|
+
readonly writeOnly?: boolean;
|
|
34
36
|
readonly readAsInstruction?: boolean;
|
|
35
37
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
36
38
|
readonly default?: string;
|
|
@@ -53,6 +55,7 @@ export declare const sharedUserEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
53
55
|
readonly filterable?: boolean;
|
|
54
56
|
readonly encrypted?: boolean;
|
|
55
57
|
readonly sensitive?: boolean;
|
|
58
|
+
readonly writeOnly?: boolean;
|
|
56
59
|
readonly readAsInstruction?: boolean;
|
|
57
60
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
58
61
|
readonly default?: string;
|
|
@@ -73,6 +76,7 @@ export declare const sharedUserEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
73
76
|
readonly filterable?: boolean;
|
|
74
77
|
readonly encrypted?: boolean;
|
|
75
78
|
readonly sensitive?: boolean;
|
|
79
|
+
readonly writeOnly?: boolean;
|
|
76
80
|
readonly readAsInstruction?: boolean;
|
|
77
81
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
78
82
|
readonly default?: string;
|
|
@@ -93,6 +97,7 @@ export declare const sharedUserEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
93
97
|
readonly filterable?: boolean;
|
|
94
98
|
readonly encrypted?: boolean;
|
|
95
99
|
readonly sensitive?: boolean;
|
|
100
|
+
readonly writeOnly?: boolean;
|
|
96
101
|
readonly readAsInstruction?: boolean;
|
|
97
102
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
98
103
|
readonly default?: string;
|
|
@@ -118,6 +123,7 @@ export declare const sharedUserTable: import("../db/table-builder.js").EntityTab
|
|
|
118
123
|
readonly filterable?: boolean;
|
|
119
124
|
readonly encrypted?: boolean;
|
|
120
125
|
readonly sensitive?: boolean;
|
|
126
|
+
readonly writeOnly?: boolean;
|
|
121
127
|
readonly readAsInstruction?: boolean;
|
|
122
128
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
123
129
|
readonly default?: string;
|
|
@@ -138,6 +144,7 @@ export declare const sharedUserTable: import("../db/table-builder.js").EntityTab
|
|
|
138
144
|
readonly filterable?: boolean;
|
|
139
145
|
readonly encrypted?: boolean;
|
|
140
146
|
readonly sensitive?: boolean;
|
|
147
|
+
readonly writeOnly?: boolean;
|
|
141
148
|
readonly readAsInstruction?: boolean;
|
|
142
149
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
143
150
|
readonly default?: string;
|
|
@@ -158,6 +165,7 @@ export declare const sharedUserTable: import("../db/table-builder.js").EntityTab
|
|
|
158
165
|
readonly filterable?: boolean;
|
|
159
166
|
readonly encrypted?: boolean;
|
|
160
167
|
readonly sensitive?: boolean;
|
|
168
|
+
readonly writeOnly?: boolean;
|
|
161
169
|
readonly readAsInstruction?: boolean;
|
|
162
170
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
163
171
|
readonly default?: string;
|
|
@@ -183,6 +191,7 @@ export declare const sharedItemEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
183
191
|
readonly filterable?: boolean;
|
|
184
192
|
readonly encrypted?: boolean;
|
|
185
193
|
readonly sensitive?: boolean;
|
|
194
|
+
readonly writeOnly?: boolean;
|
|
186
195
|
readonly readAsInstruction?: boolean;
|
|
187
196
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
188
197
|
readonly default?: string;
|
|
@@ -224,6 +233,7 @@ export declare const sharedItemTable: import("../db/table-builder.js").EntityTab
|
|
|
224
233
|
readonly filterable?: boolean;
|
|
225
234
|
readonly encrypted?: boolean;
|
|
226
235
|
readonly sensitive?: boolean;
|
|
236
|
+
readonly writeOnly?: boolean;
|
|
227
237
|
readonly readAsInstruction?: boolean;
|
|
228
238
|
readonly format?: "email" | "url" | "phone" | "password";
|
|
229
239
|
readonly default?: string;
|
package/dist/testing/wait-for.js
CHANGED
package/dist/ui-types/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { ParsedRefTarget } from "../engine/parse-ref-target.js";
|
|
|
5
5
|
export { parseRefTarget } from "../engine/parse-ref-target.js";
|
|
6
6
|
export type { FieldsOrGroupsSection } from "../engine/screen-helpers.js";
|
|
7
7
|
export { evalFieldCondition, explicitListScreenId, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
|
|
8
|
-
export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefinition, FieldDefinition, FileFieldDef, FilesFieldDef, ImageFieldDef, ImagesFieldDef, NumberFieldDef, SelectFieldDef, SelectOptionTone, TextFieldDef, } from "../engine/types/fields.js";
|
|
8
|
+
export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefinition, FieldDefinition, FileFieldDef, FilesFieldDef, ImageFieldDef, ImagesFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, SelectFieldDef, SelectOptionTone, TextFieldDef, } from "../engine/types/fields.js";
|
|
9
9
|
export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, RoleAccessPersonalData, RoleAccessRule, } from "../engine/types/handlers.js";
|
|
10
10
|
export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
|
|
11
11
|
export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav.js";
|
|
@@ -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.335.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.335.0",
|
|
210
|
+
"@cosmicdrift/kumiko-types": "0.335.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.335.0",
|
|
227
227
|
"bun-types": "^1.3.13",
|
|
228
228
|
"pino-pretty": "^13.1.3"
|
|
229
229
|
},
|