@cosmicdrift/kumiko-framework 0.390.0 → 0.390.1
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/db/driver-value-preparation.js +23 -2
- package/dist/db/queries/test-stack.js +2 -1
- package/dist/db/row-level-security-boot-check.d.ts +2 -1
- package/dist/db/row-level-security-boot-check.js +7 -6
- package/dist/schema-cli/shared.d.ts +8 -2
- package/dist/schema-cli/shared.js +6 -3
- package/dist/schema-cli/validate-command.js +9 -13
- package/dist/upgrade-cli.d.ts +1 -0
- package/dist/upgrade-cli.js +19 -7
- package/package.json +4 -4
- package/src/changes.json +30 -0
|
@@ -65,9 +65,30 @@ const INTEGER_PG_TYPES = new Set([
|
|
|
65
65
|
export function instantToTimestamptzParam(instant) {
|
|
66
66
|
return Temporal.Instant.fromEpochMilliseconds(instant.epochMilliseconds).toString();
|
|
67
67
|
}
|
|
68
|
+
function instantOfIsoString(value) {
|
|
69
|
+
try {
|
|
70
|
+
return Temporal.Instant.from(value);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// skip: not an ISO instant, Postgres parses (or rejects) the text itself
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Event payloads carry instants as ISO strings with microseconds; a projection rebuild binds them
|
|
78
|
+
// as-is, so without the same truncation the replayed row differs from the live one.
|
|
79
|
+
function timestamptzInstantOf(value) {
|
|
80
|
+
if (isTemporalInstant(value))
|
|
81
|
+
return value;
|
|
82
|
+
if (typeof value === "string")
|
|
83
|
+
return instantOfIsoString(value);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
68
86
|
function prepareDriverNeutralScalar(value, pgType) {
|
|
69
|
-
if (
|
|
70
|
-
|
|
87
|
+
if (pgType === "timestamptz" || pgType === "timestamptz(3)") {
|
|
88
|
+
const instant = timestamptzInstantOf(value);
|
|
89
|
+
if (instant !== undefined) {
|
|
90
|
+
return { kind: "param", sql: "", bound: instantToTimestamptzParam(instant) };
|
|
91
|
+
}
|
|
71
92
|
}
|
|
72
93
|
// Sent as text, so Postgres rejects 3.5 for an integer column instead of rounding it.
|
|
73
94
|
if (typeof value === "number" && pgType !== undefined && INTEGER_PG_TYPES.has(pgType)) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { extractPgError } from "../pg-error.js";
|
|
2
2
|
import { asRawClient } from "../query.js";
|
|
3
|
+
import { toPostgresArrayLiteral } from "./postgres-array-literal.js";
|
|
3
4
|
import { PROJECTION_REBUILD_SCHEMA } from "./shadow-swap.js";
|
|
4
5
|
import { quoteTableIdent } from "./table-ops.js";
|
|
5
6
|
// Re-exported for back-compat — the generic DDL helpers moved to ./ddl so
|
|
@@ -89,7 +90,7 @@ export async function transferPublicSchemaOwnership(db, owner) {
|
|
|
89
90
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
90
91
|
JOIN pg_roles r ON r.oid = c.relowner
|
|
91
92
|
WHERE n.nspname = ANY($2::text[]) AND c.relkind IN ('r', 'p', 'S', 'v', 'm') AND r.rolname <> $1
|
|
92
|
-
AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = c.oid AND d.deptype IN ('a', 'i'))`, [owner, ownedSchemas]);
|
|
93
|
+
AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = c.oid AND d.deptype IN ('a', 'i'))`, [owner, toPostgresArrayLiteral(ownedSchemas)]);
|
|
93
94
|
for (const { nspname, relname, relkind } of relations) {
|
|
94
95
|
const kind = OWNED_RELATION_KINDS[relkind];
|
|
95
96
|
if (kind === undefined)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type KnownTables } from "./known-table-names.ts";
|
|
2
2
|
import type { AnyDb } from "./query.ts";
|
|
3
3
|
import type { RowLevelSecurityMode } from "./render-rls-ddl.ts";
|
|
4
|
-
export
|
|
4
|
+
export type RowLevelSecurityCheckScope = "database" | "declared tables";
|
|
5
|
+
export declare function assertRowLevelSecurityTableState(db: AnyDb, mode: RowLevelSecurityMode, knownTables: KnownTables, scope?: RowLevelSecurityCheckScope): Promise<void>;
|
|
5
6
|
export declare function assertRowLevelSecurityBootState(db: AnyDb, mode: RowLevelSecurityMode, knownTables: KnownTables): Promise<void>;
|
|
@@ -28,8 +28,9 @@ async function assertEnforcingRole(db) {
|
|
|
28
28
|
function listingOf(entries) {
|
|
29
29
|
return entries.map((entry) => `${entry.table} (${entry.missing.join(", ")})`).join("; ");
|
|
30
30
|
}
|
|
31
|
-
async function assertAllTenantTablesProtected(db, knownTables) {
|
|
31
|
+
async function assertAllTenantTablesProtected(db, knownTables, scope) {
|
|
32
32
|
const incomplete = (await readTenantTablesRowLevelSecurity(db))
|
|
33
|
+
.filter((table) => scope === "database" || knownTables.has(table.tableName))
|
|
33
34
|
.map((table) => ({ table: table.tableName, missing: missingPartsOf(table) }))
|
|
34
35
|
.filter((entry) => entry.missing.length > 0);
|
|
35
36
|
// skip: every tenant table is protected
|
|
@@ -59,8 +60,8 @@ async function assertAllTenantTablesProtected(db, knownTables) {
|
|
|
59
60
|
message: `rowLevelSecurity is "on" but tenant tables lack row-level security. ${sections.join(" ")}`,
|
|
60
61
|
});
|
|
61
62
|
}
|
|
62
|
-
async function assertNoLiveRowLevelSecurity(db) {
|
|
63
|
-
const tables = await readTablesWithRowLevelSecurityEnabled(db);
|
|
63
|
+
async function assertNoLiveRowLevelSecurity(db, knownTables, scope) {
|
|
64
|
+
const tables = (await readTablesWithRowLevelSecurityEnabled(db)).filter((table) => scope === "database" || knownTables.has(table));
|
|
64
65
|
// skip: no live row-level security to disable
|
|
65
66
|
if (tables.length === 0)
|
|
66
67
|
return;
|
|
@@ -70,13 +71,13 @@ async function assertNoLiveRowLevelSecurity(db) {
|
|
|
70
71
|
}
|
|
71
72
|
// Table state only, no role check: the role is a runtime property of the boot connection, so tools
|
|
72
73
|
// that connect with another role (CI, `schema validate`) use this one.
|
|
73
|
-
export async function assertRowLevelSecurityTableState(db, mode, knownTables) {
|
|
74
|
+
export async function assertRowLevelSecurityTableState(db, mode, knownTables, scope = "database") {
|
|
74
75
|
if (mode === "off") {
|
|
75
|
-
await assertNoLiveRowLevelSecurity(db);
|
|
76
|
+
await assertNoLiveRowLevelSecurity(db, knownTables, scope);
|
|
76
77
|
// skip: off-mode check done
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
79
|
-
await assertAllTenantTablesProtected(db, knownTables);
|
|
80
|
+
await assertAllTenantTablesProtected(db, knownTables, scope);
|
|
80
81
|
}
|
|
81
82
|
export async function assertRowLevelSecurityBootState(db, mode, knownTables) {
|
|
82
83
|
if (mode === "on")
|
|
@@ -5,7 +5,12 @@ export type SchemaCliOut = {
|
|
|
5
5
|
readonly log: (line: string) => void;
|
|
6
6
|
readonly err: (line: string) => void;
|
|
7
7
|
};
|
|
8
|
-
|
|
8
|
+
type RunSchemaCliRowLevelSecurityOption = {
|
|
9
|
+
/** The mode the app's runtime boots with (`AppConfig.rowLevelSecurity`). Wins over the schema
|
|
10
|
+
* file, so `apply` heals the same mode the boot check enforces. */
|
|
11
|
+
readonly rowLevelSecurity?: RowLevelSecurityMode;
|
|
12
|
+
};
|
|
13
|
+
export type RunSchemaCliOptions = RunSchemaCliRowLevelSecurityOption & ({
|
|
9
14
|
readonly features?: undefined;
|
|
10
15
|
readonly kmsSlots?: undefined;
|
|
11
16
|
} | {
|
|
@@ -17,7 +22,7 @@ export type RunSchemaCliOptions = {
|
|
|
17
22
|
* `[]` when the app wires none. This CLI parses no env schema, so the
|
|
18
23
|
* rebuild's KMS wiring cannot derive them itself. */
|
|
19
24
|
readonly kmsSlots: readonly string[];
|
|
20
|
-
};
|
|
25
|
+
});
|
|
21
26
|
export type SchemaCliContext = {
|
|
22
27
|
readonly argv: readonly string[];
|
|
23
28
|
readonly out: SchemaCliOut;
|
|
@@ -41,3 +46,4 @@ export declare function loadSchemaModOrReport(ctx: SchemaCliContext): Promise<Sc
|
|
|
41
46
|
export declare function loadRowLevelSecurityModeOrReport(ctx: SchemaCliContext): Promise<RowLevelSecurityMode | undefined>;
|
|
42
47
|
export declare function readDatabaseUrlOrReport(out: SchemaCliOut): string | undefined;
|
|
43
48
|
export declare function migrationsDirExistsOrReport(ctx: SchemaCliContext): boolean;
|
|
49
|
+
export {};
|
|
@@ -91,11 +91,14 @@ export async function loadSchemaModOrReport(ctx) {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
// `apply` needs only the mode: migrations and rebuilds on a role that RLS binds see nothing unless
|
|
94
|
-
// the connection runs in system scope, which only an "on" connection sets.
|
|
95
|
-
//
|
|
94
|
+
// the connection runs in system scope, which only an "on" connection sets. The prod image ships no
|
|
95
|
+
// kumiko/schema.ts, so without one apply falls back to the runtime's default instead of "off":
|
|
96
|
+
// otherwise the framework tables stay unhealed and the boot check of an "on" app fails.
|
|
96
97
|
export async function loadRowLevelSecurityModeOrReport(ctx) {
|
|
98
|
+
if (ctx.options.rowLevelSecurity !== undefined)
|
|
99
|
+
return ctx.options.rowLevelSecurity;
|
|
97
100
|
if (!existsSync(ctx.schemaFile))
|
|
98
|
-
return
|
|
101
|
+
return DEFAULT_ROW_LEVEL_SECURITY_MODE;
|
|
99
102
|
return (await loadSchemaModOrReport(ctx))?.rowLevelSecurity;
|
|
100
103
|
}
|
|
101
104
|
export function readDatabaseUrlOrReport(out) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { assertRowLevelSecurityTableState, createDbConnection, generateMigration, } from "../db/index.js";
|
|
4
|
-
import {
|
|
4
|
+
import { isLiveFrameworkTable } from "../db/known-table-names.js";
|
|
5
5
|
import { readMigrationSqlTexts } from "../db/migrate-runner.js";
|
|
6
6
|
import { diffReplayAgainstSnapshot, replayMigrationsDir } from "../db/replay-migration-sql.js";
|
|
7
7
|
import { validateBoot } from "../engine/boot-validator.js";
|
|
@@ -62,31 +62,27 @@ function checkBootValidity(out, schemaMod) {
|
|
|
62
62
|
return false;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
function declaredTablesOf(schemaMod) {
|
|
66
|
+
const names = new Set(schemaMod.entityMetas.map((meta) => meta.tableName));
|
|
67
|
+
return { has: (tableName) => names.has(tableName) || isLiveFrameworkTable(tableName) };
|
|
68
|
+
}
|
|
65
69
|
// 4. Live row-level security table state — the table half of the boot check. Only with a DATABASE_URL;
|
|
66
|
-
// without one the command stays database-free.
|
|
70
|
+
// without one the command stays database-free. Scoped to the tables kumiko/schema.ts declares plus the
|
|
71
|
+
// framework tables, because a local DATABASE_URL may point at a dev database several apps share.
|
|
67
72
|
async function checkLiveRowLevelSecurity(out, schemaMod) {
|
|
68
73
|
const dbUrl = process.env["DATABASE_URL"];
|
|
69
74
|
if (!dbUrl) {
|
|
70
75
|
out.log(" · row-level security: skipped — set DATABASE_URL to check the live database.");
|
|
71
76
|
return true;
|
|
72
77
|
}
|
|
73
|
-
let features;
|
|
74
|
-
try {
|
|
75
|
-
features = featureDefinitionsOf(schemaMod.features);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// skip: the boot check already reported the unreadable FEATURES export
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
const knownTables = features === undefined ? { has: () => true } : knownTablesOf(features);
|
|
82
78
|
const { db, close } = createDbConnection({
|
|
83
79
|
url: dbUrl,
|
|
84
80
|
maxConnections: 1,
|
|
85
81
|
rowLevelSecurity: schemaMod.rowLevelSecurity,
|
|
86
82
|
});
|
|
87
83
|
try {
|
|
88
|
-
await assertRowLevelSecurityTableState(db, schemaMod.rowLevelSecurity,
|
|
89
|
-
out.log(" ✓ row-level security: live database matches the configured mode");
|
|
84
|
+
await assertRowLevelSecurityTableState(db, schemaMod.rowLevelSecurity, declaredTablesOf(schemaMod), "declared tables");
|
|
85
|
+
out.log(" ✓ row-level security: live database matches the configured mode for this app's tables and the framework tables (tables of other apps sharing the database are not checked)");
|
|
90
86
|
return true;
|
|
91
87
|
}
|
|
92
88
|
catch (e) {
|
package/dist/upgrade-cli.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export declare function findFeaturesDirs(cwd: string): string[];
|
|
|
4
4
|
export declare function resolveCodemodScript(repoRoot: string, codemodField: string | undefined): string | null;
|
|
5
5
|
export declare function runUpgradeCli(argv: readonly string[], cwd: string, out: UpgradeCliOut, options?: {
|
|
6
6
|
readonly repoRoot?: string;
|
|
7
|
+
readonly codemodResolveFrom?: string;
|
|
7
8
|
}): Promise<number>;
|
package/dist/upgrade-cli.js
CHANGED
|
@@ -213,6 +213,10 @@ function resolveCodemodScriptAt(codemodScriptsRoot, codemodField) {
|
|
|
213
213
|
}
|
|
214
214
|
return resolved;
|
|
215
215
|
}
|
|
216
|
+
function findCodemodScriptsRootPreferring(resolveFrom, repoRoot) {
|
|
217
|
+
const own = resolveFrom === undefined ? null : findCodemodScriptsRoot(resolveFrom);
|
|
218
|
+
return own ?? findCodemodScriptsRoot(repoRoot);
|
|
219
|
+
}
|
|
216
220
|
export function resolveCodemodScript(repoRoot, codemodField) {
|
|
217
221
|
const scriptsRoot = findCodemodScriptsRoot(repoRoot);
|
|
218
222
|
if (!scriptsRoot)
|
|
@@ -388,7 +392,7 @@ function writePartialMarker(out, targetDir, pending, manualEntries, failedVersio
|
|
|
388
392
|
// even with zero pending entries, so an already-current app still gets a
|
|
389
393
|
// bootstrap marker recording its installed version.
|
|
390
394
|
// kumiko-lint-ignore complexity-budget sequential codemod runner with zero-pending bootstrap marker
|
|
391
|
-
async function applyCodemods(out, pending,
|
|
395
|
+
async function applyCodemods(out, pending, codemodLocation, targetDir, dryRun,
|
|
392
396
|
// Installed version for the zero-pending bootstrap marker — never a
|
|
393
397
|
// `--from` filter override (that would permanently skip real pending
|
|
394
398
|
// codemods once the CI guard compares against the fake marker).
|
|
@@ -443,14 +447,16 @@ markerVersion, previous, fromVersion, manualDetails) {
|
|
|
443
447
|
}
|
|
444
448
|
return 0;
|
|
445
449
|
}
|
|
446
|
-
const
|
|
450
|
+
const { repoRoot, resolveFrom: codemodResolveFrom } = codemodLocation;
|
|
451
|
+
const codemodScriptsRoot = findCodemodScriptsRootPreferring(codemodResolveFrom, repoRoot);
|
|
447
452
|
if (codemodScriptsRoot === null) {
|
|
448
|
-
|
|
453
|
+
const searched = codemodResolveFrom === undefined ? repoRoot : `${codemodResolveFrom} and ${repoRoot}`;
|
|
454
|
+
out.err(` ✗ could not locate @cosmicdrift/kumiko-feature-ast/src/scripts/codemod — is @cosmicdrift/kumiko-cli or @cosmicdrift/kumiko-dev-server installed? searched from ${searched} upward`);
|
|
449
455
|
return 1;
|
|
450
456
|
}
|
|
451
457
|
const ran = [];
|
|
452
458
|
for (const e of codemodEntries) {
|
|
453
|
-
const scriptPath =
|
|
459
|
+
const scriptPath = resolveCodemodScriptAt(codemodScriptsRoot, e.codemod);
|
|
454
460
|
if (!scriptPath) {
|
|
455
461
|
out.err(` ✗ ${e.version} · ${e.title} — invalid codemod path "${e.codemod}"`);
|
|
456
462
|
if (!dryRun)
|
|
@@ -681,7 +687,7 @@ function planUpgrade(args, cwd, targetDir) {
|
|
|
681
687
|
value: { ...baseline.value, targetInstalledVersion, allEntries, pending },
|
|
682
688
|
};
|
|
683
689
|
}
|
|
684
|
-
async function applyPlannedUpgrade(out, args, plan,
|
|
690
|
+
async function applyPlannedUpgrade(out, args, plan, codemodLocation, targetDir) {
|
|
685
691
|
// Marker must reflect what is actually installed under the target (or
|
|
686
692
|
// cwd), not the filter baseline above — otherwise CI stays green forever.
|
|
687
693
|
const markerVersion = plan.targetInstalledVersion;
|
|
@@ -690,7 +696,7 @@ async function applyPlannedUpgrade(out, args, plan, repoRoot, targetDir) {
|
|
|
690
696
|
return failUpgrade(out, `Could not detect the installed Kumiko version under ${targetDir}.`, "Refusing to write an upgrade marker without it (--from is only a filter).");
|
|
691
697
|
}
|
|
692
698
|
out.log("");
|
|
693
|
-
const code = await applyCodemods(out, plan.pending,
|
|
699
|
+
const code = await applyCodemods(out, plan.pending, codemodLocation, targetDir, dryRun, markerVersion ?? plan.currentVersion, carryOf(plan.markerRead), plan.currentVersion, getFlag(args, "verbose") ? manualDetailsOf(plan.allEntries) : undefined);
|
|
694
700
|
out.log("");
|
|
695
701
|
return code;
|
|
696
702
|
}
|
|
@@ -802,7 +808,13 @@ export async function runUpgradeCli(argv, cwd, out, options) {
|
|
|
802
808
|
if (planned.kind === "error")
|
|
803
809
|
return failUpgrade(out, ...planned.lines);
|
|
804
810
|
if (getFlag(args, "apply")) {
|
|
805
|
-
|
|
811
|
+
const codemodLocation = {
|
|
812
|
+
repoRoot: options?.repoRoot ?? cwd,
|
|
813
|
+
...(options?.codemodResolveFrom !== undefined && {
|
|
814
|
+
resolveFrom: options.codemodResolveFrom,
|
|
815
|
+
}),
|
|
816
|
+
};
|
|
817
|
+
return applyPlannedUpgrade(out, args, planned.value, codemodLocation, targetDir);
|
|
806
818
|
}
|
|
807
819
|
if (getFlag(args, "json"))
|
|
808
820
|
return reportUpgradeJson(out, planned.value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.390.
|
|
3
|
+
"version": "0.390.1",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
"./package.json": "./package.json"
|
|
200
200
|
},
|
|
201
201
|
"dependencies": {
|
|
202
|
-
"@cosmicdrift/kumiko-types": "0.390.
|
|
202
|
+
"@cosmicdrift/kumiko-types": "0.390.1",
|
|
203
203
|
"bullmq": "^5.76.7",
|
|
204
204
|
"hono": "^4.13.1",
|
|
205
205
|
"i18next": "^26.1.0",
|
|
@@ -209,7 +209,7 @@
|
|
|
209
209
|
"temporal-polyfill": "^0.3.2",
|
|
210
210
|
"uuid": "^14.0.0",
|
|
211
211
|
"write-excel-file": "^4.1.1",
|
|
212
|
-
"zod": "^4.
|
|
212
|
+
"zod": "^4.6.7"
|
|
213
213
|
},
|
|
214
214
|
"peerDependencies": {
|
|
215
215
|
"bun-types": "^1.4.4",
|
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
}
|
|
222
222
|
},
|
|
223
223
|
"devDependencies": {
|
|
224
|
-
"@cosmicdrift/kumiko-testing": "0.390.
|
|
224
|
+
"@cosmicdrift/kumiko-testing": "0.390.1",
|
|
225
225
|
"bun-types": "^1.4.4",
|
|
226
226
|
"fflate": "^0.8.2",
|
|
227
227
|
"meilisearch": "^0.58.0",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.390.1",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "schema apply in the production image uses the runtime's row-level security mode",
|
|
6
|
+
"detail": "The app image ships only kumiko/migrations, not kumiko/schema.ts. Without the schema file `schema apply` ran with row-level security \"off\", so kumiko_events, kumiko_snapshots and kumiko_archived_streams were not healed and the boot of an app in \"on\" mode (the default) failed with \"framework tables without it\". Without a schema file apply now uses the same default as runProdApp. RunSchemaCliOptions takes an optional rowLevelSecurity, which wins over the schema file; the scaffolded bin/kumiko.ts passes appConfig.rowLevelSecurity, like bin/main.ts does for runProdApp. Apps that do not set rowLevelSecurity need no change. An app that sets rowLevelSecurity \"off\" and keeps a hand-written bin/kumiko.ts must pass rowLevelSecurity: \"off\" to runSchemaCli, otherwise apply enables row-level security on the framework tables and the app's \"off\" boot check fails."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.390.1",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Projection rebuilds no longer report drift on timestamptz columns",
|
|
12
|
+
"detail": "Event payloads keep the microseconds of an instant, the live write stores milliseconds. A rebuild bound the payload's ISO string unchanged and wrote a different value, which the shadow diff reported as column drift. ISO instant strings bound to a timestamptz column are now truncated to milliseconds like Temporal.Instant values."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.390.1",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "The live row-level security check of `kumiko check` covers only the app's own tables",
|
|
18
|
+
"detail": "`schema validate` (the boot step of `kumiko check`) checks the live database when DATABASE_URL is set. On a dev database several apps share it failed on the other apps' tables. It now checks the tables kumiko/schema.ts declares plus the framework tables; tables no feature declares any more are still reported by the boot check, which runs against the app's own database. An unreachable database still fails the check."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.390.1",
|
|
22
|
+
"type": "fix",
|
|
23
|
+
"title": "kumiko-upgrade runs the codemods of its own feature-ast install",
|
|
24
|
+
"detail": "The kumiko-upgrade bins of kumiko-cli and kumiko-dev-server now look for the codemod scripts from their own package first, so a feature-ast copy hoisted above the app (for example in a workspace parent) can no longer supply codemods of another version. runUpgradeCli takes an optional codemodResolveFrom for that; without it the search still starts at the repo root."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.390.1",
|
|
28
|
+
"type": "fix",
|
|
29
|
+
"title": "zod range moved to ^4.6.7",
|
|
30
|
+
"detail": "All framework packages and the scaffold template require zod ^4.6.7, which fixes GHSA-MRHX-C7XC-M3W5."
|
|
31
|
+
},
|
|
2
32
|
{
|
|
3
33
|
"version": "0.390.0",
|
|
4
34
|
"type": "fix",
|