@pikku/cli 0.12.89 → 0.12.90
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/console-app/assets/{index-BaOGIr2O.js → index-C5Bd44e4.js} +149 -149
- package/console-app/index.html +1 -1
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +21 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.json +6 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +30 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +64 -4
- package/dist/.pikku/function/pikku-functions.gen.js +9 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +4 -0
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +9 -1
- package/dist/.pikku/schemas/schemas/DbBaselineInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/DbCheckInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/DbCodegenInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/DbExportInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricValidateInput.schema.json +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
- package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/cli.wiring.js +20 -0
- package/dist/src/fabric/fabric-commands.d.ts +9 -3
- package/dist/src/fabric/fabric-commands.js +6 -0
- package/dist/src/fabric/functions/validate.function.d.ts +15 -5
- package/dist/src/fabric/functions/validate.function.js +69 -16
- package/dist/src/fabric/lib/config.d.ts +11 -1
- package/dist/src/fabric/lib/config.js +39 -1
- package/dist/src/fabric/lib/frontend-typecheck.d.ts +46 -0
- package/dist/src/fabric/lib/frontend-typecheck.js +139 -0
- package/dist/src/functions/commands/db-baseline.d.ts +13 -0
- package/dist/src/functions/commands/db-baseline.js +48 -0
- package/dist/src/functions/commands/db-check.d.ts +12 -0
- package/dist/src/functions/commands/db-check.js +64 -0
- package/dist/src/functions/commands/db-codegen.d.ts +11 -0
- package/dist/src/functions/commands/db-codegen.js +39 -0
- package/dist/src/functions/commands/db-export.d.ts +12 -0
- package/dist/src/functions/commands/db-export.js +31 -0
- package/dist/src/functions/commands/db-generate.js +19 -26
- package/dist/src/functions/db/db-migrator.d.ts +35 -4
- package/dist/src/functions/db/db-migrator.js +67 -10
- package/dist/src/functions/db/local-db.d.ts +235 -13
- package/dist/src/functions/db/local-db.js +430 -45
- package/dist/src/functions/db/postgres/postgres-introspector.js +19 -1
- package/dist/src/functions/db/postgres/postgres-migrator.d.ts +1 -0
- package/dist/src/functions/db/postgres/postgres-migrator.js +4 -1
- package/dist/src/functions/db/sqlite/sqlite-introspector.js +2 -1
- package/dist/src/functions/db/sqlite/sqlite-migrator.d.ts +1 -0
- package/dist/src/functions/db/sqlite/sqlite-migrator.js +6 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/skills/pikku-i18n/SKILL.md +53 -6
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { pikkuSessionlessFunc } from '#pikku';
|
|
2
|
+
import { resolveDb, baseline } from '../db/local-db.js';
|
|
3
|
+
import { loadUserConfigForDb } from './db-shared.js';
|
|
4
|
+
/**
|
|
5
|
+
* Record the pending migrations as applied, without running them.
|
|
6
|
+
*
|
|
7
|
+
* For a database that already contains what they describe. The case it exists
|
|
8
|
+
* for is a schema that was created at boot and written down afterwards: every
|
|
9
|
+
* deployment already has those tables, so applying the migration fails, and
|
|
10
|
+
* leaving it pending forever means the history never catches up with reality.
|
|
11
|
+
*
|
|
12
|
+
* Refuses unless the database really is up to date, because that is the entire
|
|
13
|
+
* premise. A database that is behind gets the same report `db check` gives —
|
|
14
|
+
* baselining it would bury a real gap under a history claiming otherwise.
|
|
15
|
+
*/
|
|
16
|
+
export const dbBaseline = pikkuSessionlessFunc({
|
|
17
|
+
remote: true,
|
|
18
|
+
func: async ({ logger, config }) => {
|
|
19
|
+
const userConfig = await loadUserConfigForDb({ config, logger });
|
|
20
|
+
if (!userConfig)
|
|
21
|
+
return;
|
|
22
|
+
const resolved = resolveDb(userConfig, config.rootDir, config.outDir, config.runtimeDir);
|
|
23
|
+
if (!resolved) {
|
|
24
|
+
logger.error('pikku db baseline: no database configured — set sqliteDb or postgresUrl in your createConfig.');
|
|
25
|
+
throw new Error('no database configured');
|
|
26
|
+
}
|
|
27
|
+
const result = await baseline(resolved, config.rootDir, config.srcDirectories, logger);
|
|
28
|
+
if (result.status === 'behind') {
|
|
29
|
+
logger.error('db baseline: refused — the database does not yet contain everything the migrations describe:');
|
|
30
|
+
if (result.drift.missingTables.length) {
|
|
31
|
+
logger.error(` missing tables: ${result.drift.missingTables.join(', ')}`);
|
|
32
|
+
}
|
|
33
|
+
for (const { table, columns } of result.drift.missingColumns) {
|
|
34
|
+
logger.error(` ${table} missing columns: ${columns.join(', ')}`);
|
|
35
|
+
}
|
|
36
|
+
logger.error(' Baselining now would record migrations that never ran. Run `pikku db migrate` instead.');
|
|
37
|
+
throw new Error('database is behind its migrations');
|
|
38
|
+
}
|
|
39
|
+
if (result.recorded.length === 0) {
|
|
40
|
+
logger.info('db baseline: nothing pending — every migration is recorded');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
logger.info(`db baseline: recorded ${result.recorded.length} migration(s) as applied without running them:`);
|
|
44
|
+
for (const name of result.recorded) {
|
|
45
|
+
logger.info(` ${name}`);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report how the configured database differs from the schema its migrations
|
|
3
|
+
* define.
|
|
4
|
+
*
|
|
5
|
+
* Answers the question nobody can otherwise answer without going and looking by
|
|
6
|
+
* hand: does this database still match what we wrote down? Missing tables and
|
|
7
|
+
* columns fail the command — the database is behind and a migration has not
|
|
8
|
+
* been applied. Tables the migrations never mention are reported but never
|
|
9
|
+
* fail: something created them outside the migration history, which is worth
|
|
10
|
+
* seeing but is not this command's to fix.
|
|
11
|
+
*/
|
|
12
|
+
export declare const dbCheck: import("#pikku").PikkuFunctionConfig<{}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{}, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{}, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { pikkuSessionlessFunc } from '#pikku';
|
|
2
|
+
import { resolveDb, computeSchemaDrift } from '../db/local-db.js';
|
|
3
|
+
import { loadUserConfigForDb } from './db-shared.js';
|
|
4
|
+
/**
|
|
5
|
+
* Report how the configured database differs from the schema its migrations
|
|
6
|
+
* define.
|
|
7
|
+
*
|
|
8
|
+
* Answers the question nobody can otherwise answer without going and looking by
|
|
9
|
+
* hand: does this database still match what we wrote down? Missing tables and
|
|
10
|
+
* columns fail the command — the database is behind and a migration has not
|
|
11
|
+
* been applied. Tables the migrations never mention are reported but never
|
|
12
|
+
* fail: something created them outside the migration history, which is worth
|
|
13
|
+
* seeing but is not this command's to fix.
|
|
14
|
+
*/
|
|
15
|
+
export const dbCheck = pikkuSessionlessFunc({
|
|
16
|
+
remote: true,
|
|
17
|
+
func: async ({ logger, config }) => {
|
|
18
|
+
const userConfig = await loadUserConfigForDb({ config, logger });
|
|
19
|
+
if (!userConfig)
|
|
20
|
+
return;
|
|
21
|
+
const resolved = resolveDb(userConfig, config.rootDir, config.outDir, config.runtimeDir);
|
|
22
|
+
if (!resolved) {
|
|
23
|
+
logger.error('pikku db check: no database configured — set sqliteDb or postgresUrl in your createConfig.');
|
|
24
|
+
throw new Error('no database configured');
|
|
25
|
+
}
|
|
26
|
+
const drift = await computeSchemaDrift(resolved, config.rootDir, config.srcDirectories, logger);
|
|
27
|
+
// Table names elide the `public` schema, which reads as ambiguous in a
|
|
28
|
+
// report whose whole point can be a second copy of `app.orders` sitting in
|
|
29
|
+
// `public`. Put the schema back for display only.
|
|
30
|
+
const qualify = (table) => resolved.dialect === 'postgres' && !table.includes('.')
|
|
31
|
+
? `public.${table}`
|
|
32
|
+
: table;
|
|
33
|
+
if (drift.runtimeTables.length) {
|
|
34
|
+
logger.info(`db check: ${drift.runtimeTables.length} table(s) the pikku runtime created at boot, which no migration records:`);
|
|
35
|
+
for (const table of drift.runtimeTables) {
|
|
36
|
+
logger.info(` ${qualify(table)}`);
|
|
37
|
+
}
|
|
38
|
+
logger.info(' Run `pikku db generate` to write them down, so the schema stops depending on which services happened to start.');
|
|
39
|
+
}
|
|
40
|
+
if (drift.extraTables.length) {
|
|
41
|
+
logger.info(`db check: ${drift.extraTables.length} table(s) in the database that no migration creates:`);
|
|
42
|
+
for (const table of drift.extraTables) {
|
|
43
|
+
logger.info(` ${qualify(table)}`);
|
|
44
|
+
}
|
|
45
|
+
logger.info(' Left alone — a migration cannot know whether these hold data worth keeping.');
|
|
46
|
+
for (const { schema, requires, owner } of drift.skippedRuntimeSchemas) {
|
|
47
|
+
logger.info(` Note: the runtime's '${schema}' tables could not be recognised — it needs '${requires}', which this project does not create. ${owner} owns it.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (drift.inSync) {
|
|
51
|
+
logger.info('db check: the database matches its migrations');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
logger.error('db check: the database is behind its migrations:');
|
|
55
|
+
if (drift.missingTables.length) {
|
|
56
|
+
logger.error(` missing tables: ${drift.missingTables.map(qualify).join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
for (const { table, columns } of drift.missingColumns) {
|
|
59
|
+
logger.error(` ${qualify(table)} missing columns: ${columns.join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
logger.error(' Run `pikku db migrate` to apply them.');
|
|
62
|
+
throw new Error('database schema is behind its migrations');
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regenerate the database types from the migration files alone, without
|
|
3
|
+
* connecting to the configured database.
|
|
4
|
+
*
|
|
5
|
+
* `db migrate` can only emit types after it has migrated the real database,
|
|
6
|
+
* which forces codegen to run late — after a deploy has already moved the
|
|
7
|
+
* schema. This command applies the same migrations to a throwaway database and
|
|
8
|
+
* introspects that, so `pikku all` can be given an accurate table zod on a
|
|
9
|
+
* machine with no database reachable at all.
|
|
10
|
+
*/
|
|
11
|
+
export declare const dbCodegen: import("#pikku").PikkuFunctionConfig<{}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{}, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{}, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { pikkuSessionlessFunc } from '#pikku';
|
|
2
|
+
import { resolveDb, migrateAndCodegen } from '../db/local-db.js';
|
|
3
|
+
import { loadUserConfigForDb } from './db-shared.js';
|
|
4
|
+
/**
|
|
5
|
+
* Regenerate the database types from the migration files alone, without
|
|
6
|
+
* connecting to the configured database.
|
|
7
|
+
*
|
|
8
|
+
* `db migrate` can only emit types after it has migrated the real database,
|
|
9
|
+
* which forces codegen to run late — after a deploy has already moved the
|
|
10
|
+
* schema. This command applies the same migrations to a throwaway database and
|
|
11
|
+
* introspects that, so `pikku all` can be given an accurate table zod on a
|
|
12
|
+
* machine with no database reachable at all.
|
|
13
|
+
*/
|
|
14
|
+
export const dbCodegen = pikkuSessionlessFunc({
|
|
15
|
+
remote: true,
|
|
16
|
+
func: async ({ logger, config }) => {
|
|
17
|
+
const userConfig = await loadUserConfigForDb({ config, logger });
|
|
18
|
+
if (!userConfig)
|
|
19
|
+
return;
|
|
20
|
+
const resolved = resolveDb(userConfig, config.rootDir, config.outDir, config.runtimeDir);
|
|
21
|
+
if (!resolved) {
|
|
22
|
+
logger.error('pikku db codegen: no database configured — set sqliteDb or postgresUrl in your createConfig.');
|
|
23
|
+
throw new Error('no database configured');
|
|
24
|
+
}
|
|
25
|
+
const { migrate, codegen, zod } = await migrateAndCodegen(resolved, {
|
|
26
|
+
scratch: true,
|
|
27
|
+
});
|
|
28
|
+
for (const warning of codegen.warnings) {
|
|
29
|
+
logger.diagnostic(warning);
|
|
30
|
+
}
|
|
31
|
+
logger.info(`db codegen: applied ${migrate.applied.length} migration(s) to a scratch ${resolved.dialect} database`);
|
|
32
|
+
logger.info(codegen.written
|
|
33
|
+
? `db codegen: regenerated ${codegen.outFile} (${codegen.tables.length} tables)`
|
|
34
|
+
: `db codegen: ${codegen.outFile} unchanged`);
|
|
35
|
+
logger.info(zod.written
|
|
36
|
+
? `db codegen: regenerated ${zod.outFile} (${zod.tables.length} tables)`
|
|
37
|
+
: `db codegen: ${zod.outFile} unchanged`);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publish this package's schema so a project consuming it as an addon can
|
|
3
|
+
* create its tables.
|
|
4
|
+
*
|
|
5
|
+
* An addon has no database of its own — it runs inside the consumer, against
|
|
6
|
+
* the consumer's — so it must never create tables at boot. It says what it
|
|
7
|
+
* needs, and the consumer's `db generate` folds that into its own migration
|
|
8
|
+
* history, where the project can review it like any other migration.
|
|
9
|
+
*
|
|
10
|
+
* Run this from the addon's build, beside the rest of its codegen.
|
|
11
|
+
*/
|
|
12
|
+
export declare const dbExport: import("#pikku").PikkuFunctionConfig<{}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{}, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{}, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { pikkuSessionlessFunc } from '#pikku';
|
|
4
|
+
import { exportSchema } from '../db/local-db.js';
|
|
5
|
+
/**
|
|
6
|
+
* Publish this package's schema so a project consuming it as an addon can
|
|
7
|
+
* create its tables.
|
|
8
|
+
*
|
|
9
|
+
* An addon has no database of its own — it runs inside the consumer, against
|
|
10
|
+
* the consumer's — so it must never create tables at boot. It says what it
|
|
11
|
+
* needs, and the consumer's `db generate` folds that into its own migration
|
|
12
|
+
* history, where the project can review it like any other migration.
|
|
13
|
+
*
|
|
14
|
+
* Run this from the addon's build, beside the rest of its codegen.
|
|
15
|
+
*/
|
|
16
|
+
export const dbExport = pikkuSessionlessFunc({
|
|
17
|
+
remote: true,
|
|
18
|
+
func: async ({ logger, config }) => {
|
|
19
|
+
const artifact = await exportSchema(config.rootDir);
|
|
20
|
+
const dialects = Object.keys(artifact);
|
|
21
|
+
if (dialects.length === 0) {
|
|
22
|
+
logger.info('db export: no db/sqlite or db/postgres migrations — nothing to publish');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const file = join(config.outDir, 'db', 'pikku-db-meta.gen.json');
|
|
26
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
27
|
+
writeFileSync(file, `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
|
|
28
|
+
logger.info(`db export: wrote ${file} for ${dialects.join(', ')}`);
|
|
29
|
+
logger.info(' Ship it with the package — a consumer resolves it through the package name.');
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { pikkuSessionlessFunc } from '#pikku';
|
|
2
|
-
import { resolveDb,
|
|
2
|
+
import { resolveDb, generateMigrations } from '../db/local-db.js';
|
|
3
3
|
import { loadUserConfigForDb } from './db-shared.js';
|
|
4
4
|
export const dbGenerate = pikkuSessionlessFunc({
|
|
5
5
|
remote: true,
|
|
6
|
-
func: async ({ logger, config }) => {
|
|
6
|
+
func: async ({ logger, config, getInspectorState }) => {
|
|
7
7
|
const userConfig = await loadUserConfigForDb({ config, logger });
|
|
8
8
|
if (!userConfig)
|
|
9
9
|
return;
|
|
@@ -12,31 +12,24 @@ export const dbGenerate = pikkuSessionlessFunc({
|
|
|
12
12
|
logger.error('pikku db generate: no database configured — set sqliteDb or postgresUrl in your createConfig.');
|
|
13
13
|
throw new Error('no database configured');
|
|
14
14
|
}
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
if (cols)
|
|
32
|
-
logger.error(` missing columns: ${cols}`);
|
|
33
|
-
logger.error(' Write a forward migration adding these by hand (incremental auto-generation is not yet supported).');
|
|
34
|
-
throw new Error('incremental auth schema change requires a manual migration');
|
|
15
|
+
const state = await getInspectorState();
|
|
16
|
+
const addons = [...state.rpc.wireAddonDeclarations.values()].map(({ package: pkg, remote }) => ({ package: pkg, remote }));
|
|
17
|
+
const { upToDate, written } = await generateMigrations(resolved, config.rootDir, config.srcDirectories, logger, addons);
|
|
18
|
+
for (const source of upToDate) {
|
|
19
|
+
logger.info(`db generate: ${source} is already covered by existing migrations`);
|
|
20
|
+
}
|
|
21
|
+
if (written.length === 0) {
|
|
22
|
+
if (upToDate.length === 0) {
|
|
23
|
+
logger.info('db generate: nothing declares tables — nothing to generate');
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const { source, file, needsBackfill } of written) {
|
|
28
|
+
logger.info(`db generate: wrote ${file} for ${source}`);
|
|
29
|
+
for (const column of needsBackfill) {
|
|
30
|
+
logger.warn(` ${column} is NOT NULL with no default — decide what existing rows get before applying.`);
|
|
35
31
|
}
|
|
36
|
-
case 'written':
|
|
37
|
-
logger.info(`db generate: wrote ${result.file}`);
|
|
38
|
-
logger.info(' Review it, then run `pikku db migrate` to apply.');
|
|
39
|
-
return;
|
|
40
32
|
}
|
|
33
|
+
logger.info(' Review them, then run `pikku db migrate` to apply.');
|
|
41
34
|
},
|
|
42
35
|
});
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The migrator's own bookkeeping table, which belongs to no dialect and to no
|
|
3
|
+
* project's schema.
|
|
4
|
+
*
|
|
5
|
+
* Every introspector hides it for that reason. Leaving it visible on one
|
|
6
|
+
* dialect and not the other is not cosmetic: a schema source exported from a
|
|
7
|
+
* database that has been migrated would publish `sql_migrations` as one of its
|
|
8
|
+
* own tables, and the consumer — which also has it — would then read the source
|
|
9
|
+
* as partially covered and emit column deltas instead of the source's own SQL,
|
|
10
|
+
* silently dropping its primary keys, indexes and constraints.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MIGRATION_TRACKING_TABLE = "sql_migrations";
|
|
1
13
|
export declare class MigrationDriftError extends Error {
|
|
2
14
|
readonly file: string;
|
|
3
15
|
readonly recordedHash: string;
|
|
@@ -23,10 +35,29 @@ export interface MigrationExecutor {
|
|
|
23
35
|
ensureTrackingTable(): Promise<void>;
|
|
24
36
|
getApplied(): Promise<AppliedMigration[]>;
|
|
25
37
|
runMigration(sql: string, name: string, hash: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Record a migration as applied without running its SQL.
|
|
40
|
+
*
|
|
41
|
+
* For a database that already contains what the migration describes, because
|
|
42
|
+
* something created those tables before anyone wrote them down. Only ever
|
|
43
|
+
* called once the caller has confirmed that is actually true — recording a
|
|
44
|
+
* migration whose tables are absent leaves a database permanently behind with
|
|
45
|
+
* no pending migration to reveal it.
|
|
46
|
+
*/
|
|
47
|
+
recordMigration(name: string, hash: string): Promise<void>;
|
|
26
48
|
}
|
|
49
|
+
export declare function migrate(executor: MigrationExecutor, migrationsDir: string): Promise<MigrateResult>;
|
|
27
50
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
51
|
+
* Record every pending migration as applied, without running any of it.
|
|
52
|
+
*
|
|
53
|
+
* The escape hatch for a database that already has the tables a migration
|
|
54
|
+
* creates — the shape you get when a runtime bootstrapped its own schema at
|
|
55
|
+
* boot and the migration writing it down was authored afterwards. Running that
|
|
56
|
+
* migration would fail on every existing deployment; skipping it forever would
|
|
57
|
+
* leave the history lying. Recording it says what is true.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately unconditional here. Whether the database really does match is a
|
|
60
|
+
* question about schemas, not migration files, so the caller answers it first
|
|
61
|
+
* and this only runs once it has.
|
|
31
62
|
*/
|
|
32
|
-
export declare function
|
|
63
|
+
export declare function baselineMigrations(executor: MigrationExecutor, migrationsDir: string): Promise<string[]>;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* The migrator's own bookkeeping table, which belongs to no dialect and to no
|
|
6
|
+
* project's schema.
|
|
7
|
+
*
|
|
8
|
+
* Every introspector hides it for that reason. Leaving it visible on one
|
|
9
|
+
* dialect and not the other is not cosmetic: a schema source exported from a
|
|
10
|
+
* database that has been migrated would publish `sql_migrations` as one of its
|
|
11
|
+
* own tables, and the consumer — which also has it — would then read the source
|
|
12
|
+
* as partially covered and emit column deltas instead of the source's own SQL,
|
|
13
|
+
* silently dropping its primary keys, indexes and constraints.
|
|
14
|
+
*/
|
|
15
|
+
export const MIGRATION_TRACKING_TABLE = 'sql_migrations';
|
|
4
16
|
export class MigrationDriftError extends Error {
|
|
5
17
|
file;
|
|
6
18
|
recordedHash;
|
|
@@ -31,9 +43,25 @@ function sha256(bytes) {
|
|
|
31
43
|
* executor. Hashes raw file bytes on apply; subsequent runs re-hash and bail
|
|
32
44
|
* with `MigrationDriftError` if any applied file has changed on disk.
|
|
33
45
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The migrations on disk, or none.
|
|
48
|
+
*
|
|
49
|
+
* A project that has never generated a migration has no directory to read, and
|
|
50
|
+
* that is the ordinary first-run state rather than a failure — it is precisely
|
|
51
|
+
* the project `db generate` exists to serve.
|
|
52
|
+
*/
|
|
53
|
+
const migrationFiles = (migrationsDir) => existsSync(migrationsDir)
|
|
54
|
+
? readdirSync(migrationsDir)
|
|
55
|
+
.filter((f) => f.endsWith('.sql'))
|
|
56
|
+
.sort()
|
|
57
|
+
: [];
|
|
58
|
+
/**
|
|
59
|
+
* Re-hash every applied migration and bail if one has changed on disk.
|
|
60
|
+
*
|
|
61
|
+
* Applies to baselining as much as to migrating: recording a file as applied
|
|
62
|
+
* only means anything if the file is still the one that was applied.
|
|
63
|
+
*/
|
|
64
|
+
function assertNoDrift(applied, migrationsDir) {
|
|
37
65
|
for (const row of applied) {
|
|
38
66
|
let currentHash = null;
|
|
39
67
|
try {
|
|
@@ -46,13 +74,15 @@ export async function migrate(executor, migrationsDir) {
|
|
|
46
74
|
throw new MigrationDriftError(row.name, row.hash, currentHash, row.applied_at, migrationsDir);
|
|
47
75
|
}
|
|
48
76
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
77
|
+
}
|
|
78
|
+
export async function migrate(executor, migrationsDir) {
|
|
79
|
+
await executor.ensureTrackingTable();
|
|
80
|
+
const applied = await executor.getApplied();
|
|
81
|
+
assertNoDrift(applied, migrationsDir);
|
|
82
|
+
const appliedNames = new Set(applied.map((r) => r.name));
|
|
53
83
|
const result = { applied: [], skipped: [] };
|
|
54
|
-
for (const name of
|
|
55
|
-
if (
|
|
84
|
+
for (const name of migrationFiles(migrationsDir)) {
|
|
85
|
+
if (appliedNames.has(name)) {
|
|
56
86
|
result.skipped.push(name);
|
|
57
87
|
continue;
|
|
58
88
|
}
|
|
@@ -63,3 +93,30 @@ export async function migrate(executor, migrationsDir) {
|
|
|
63
93
|
}
|
|
64
94
|
return result;
|
|
65
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Record every pending migration as applied, without running any of it.
|
|
98
|
+
*
|
|
99
|
+
* The escape hatch for a database that already has the tables a migration
|
|
100
|
+
* creates — the shape you get when a runtime bootstrapped its own schema at
|
|
101
|
+
* boot and the migration writing it down was authored afterwards. Running that
|
|
102
|
+
* migration would fail on every existing deployment; skipping it forever would
|
|
103
|
+
* leave the history lying. Recording it says what is true.
|
|
104
|
+
*
|
|
105
|
+
* Deliberately unconditional here. Whether the database really does match is a
|
|
106
|
+
* question about schemas, not migration files, so the caller answers it first
|
|
107
|
+
* and this only runs once it has.
|
|
108
|
+
*/
|
|
109
|
+
export async function baselineMigrations(executor, migrationsDir) {
|
|
110
|
+
await executor.ensureTrackingTable();
|
|
111
|
+
const applied = await executor.getApplied();
|
|
112
|
+
assertNoDrift(applied, migrationsDir);
|
|
113
|
+
const appliedNames = new Set(applied.map((r) => r.name));
|
|
114
|
+
const recorded = [];
|
|
115
|
+
for (const name of migrationFiles(migrationsDir)) {
|
|
116
|
+
if (appliedNames.has(name))
|
|
117
|
+
continue;
|
|
118
|
+
await executor.recordMigration(name, sha256(readFileSync(join(migrationsDir, name))));
|
|
119
|
+
recorded.push(name);
|
|
120
|
+
}
|
|
121
|
+
return recorded;
|
|
122
|
+
}
|