@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.
Files changed (100) hide show
  1. package/console-app/assets/{index-BaOGIr2O.js → index-C5Bd44e4.js} +149 -149
  2. package/console-app/index.html +1 -1
  3. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  4. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  6. package/dist/.pikku/cli/pikku-cli-channel.js +21 -1
  7. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  8. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  9. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  10. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  11. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.json +6 -1
  12. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  13. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  15. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +30 -1
  16. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  17. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  18. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  19. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  20. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  21. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  22. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  23. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  24. package/dist/.pikku/function/pikku-functions-meta.gen.json +64 -4
  25. package/dist/.pikku/function/pikku-functions.gen.js +9 -1
  26. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  27. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  28. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  29. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  30. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  31. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  32. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  33. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  34. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  35. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-types.gen.js +1 -1
  37. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  38. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  39. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  40. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  41. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  42. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  43. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +4 -0
  44. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  45. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  46. package/dist/.pikku/schemas/register.gen.js +9 -1
  47. package/dist/.pikku/schemas/schemas/DbBaselineInput.schema.json +1 -0
  48. package/dist/.pikku/schemas/schemas/DbCheckInput.schema.json +1 -0
  49. package/dist/.pikku/schemas/schemas/DbCodegenInput.schema.json +1 -0
  50. package/dist/.pikku/schemas/schemas/DbExportInput.schema.json +1 -0
  51. package/dist/.pikku/schemas/schemas/FabricValidateInput.schema.json +1 -1
  52. package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
  53. package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
  54. package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
  55. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  56. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  57. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  58. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  59. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  60. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  61. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  62. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  63. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  64. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  65. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  66. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  67. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  68. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  69. package/dist/bin/pikku-bin.mjs +2 -2
  70. package/dist/src/cli.wiring.js +20 -0
  71. package/dist/src/fabric/fabric-commands.d.ts +9 -3
  72. package/dist/src/fabric/fabric-commands.js +6 -0
  73. package/dist/src/fabric/functions/validate.function.d.ts +15 -5
  74. package/dist/src/fabric/functions/validate.function.js +69 -16
  75. package/dist/src/fabric/lib/config.d.ts +11 -1
  76. package/dist/src/fabric/lib/config.js +39 -1
  77. package/dist/src/fabric/lib/frontend-typecheck.d.ts +46 -0
  78. package/dist/src/fabric/lib/frontend-typecheck.js +139 -0
  79. package/dist/src/functions/commands/db-baseline.d.ts +13 -0
  80. package/dist/src/functions/commands/db-baseline.js +48 -0
  81. package/dist/src/functions/commands/db-check.d.ts +12 -0
  82. package/dist/src/functions/commands/db-check.js +64 -0
  83. package/dist/src/functions/commands/db-codegen.d.ts +11 -0
  84. package/dist/src/functions/commands/db-codegen.js +39 -0
  85. package/dist/src/functions/commands/db-export.d.ts +12 -0
  86. package/dist/src/functions/commands/db-export.js +31 -0
  87. package/dist/src/functions/commands/db-generate.js +19 -26
  88. package/dist/src/functions/db/db-migrator.d.ts +35 -4
  89. package/dist/src/functions/db/db-migrator.js +67 -10
  90. package/dist/src/functions/db/local-db.d.ts +235 -13
  91. package/dist/src/functions/db/local-db.js +430 -45
  92. package/dist/src/functions/db/postgres/postgres-introspector.js +19 -1
  93. package/dist/src/functions/db/postgres/postgres-migrator.d.ts +1 -0
  94. package/dist/src/functions/db/postgres/postgres-migrator.js +4 -1
  95. package/dist/src/functions/db/sqlite/sqlite-introspector.js +2 -1
  96. package/dist/src/functions/db/sqlite/sqlite-migrator.d.ts +1 -0
  97. package/dist/src/functions/db/sqlite/sqlite-migrator.js +6 -1
  98. package/dist/tsconfig.tsbuildinfo +1 -1
  99. package/package.json +3 -3
  100. 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, generateAuthMigration } from '../db/local-db.js';
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 result = await generateAuthMigration(resolved, config.rootDir, config.srcDirectories, logger);
16
- switch (result.status) {
17
- case 'no-auth':
18
- logger.info('db generate: no pikkuBetterAuth found — nothing to generate');
19
- return;
20
- case 'up-to-date':
21
- logger.info('db generate: Better Auth schema already covered by existing migrations — nothing to generate');
22
- return;
23
- case 'incremental-unsupported': {
24
- const cols = (result.missingColumns ?? [])
25
- .map((m) => `${m.table}(${m.columns.join(', ')})`)
26
- .join('; ');
27
- logger.error('db generate: the Better Auth config requires schema changes on top of an existing auth schema:');
28
- if (result.missingTables?.length) {
29
- logger.error(` missing tables: ${result.missingTables.join(', ')}`);
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
- * Apply pending migrations from `migrationsDir/*.sql` using the supplied
29
- * executor. Hashes raw file bytes on apply; subsequent runs re-hash and bail
30
- * with `MigrationDriftError` if any applied file has changed on disk.
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 migrate(executor: MigrationExecutor, migrationsDir: string): Promise<MigrateResult>;
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
- export async function migrate(executor, migrationsDir) {
35
- await executor.ensureTrackingTable();
36
- const applied = await executor.getApplied();
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
- const appliedSet = new Set(applied.map((r) => r.name));
50
- const files = readdirSync(migrationsDir)
51
- .filter((f) => f.endsWith('.sql'))
52
- .sort();
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 files) {
55
- if (appliedSet.has(name)) {
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
+ }