@pikku/cli 0.12.90 → 0.12.92
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/LICENSE +106 -0
- package/README.md +25 -2
- package/console-app/assets/{index-CSzCJzBb.css → index-D0HG8q0B.css} +1 -1
- package/console-app/assets/{index-C5Bd44e4.js → index-DDpIMCpy.js} +149 -149
- package/console-app/index.html +2 -2
- 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 +6 -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 +15 -0
- 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 +43 -0
- 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 +4 -4
- 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 +39 -44
- package/dist/.pikku/function/pikku-functions.gen.js +1 -3
- 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-scenarios.gen.d.ts +9 -0
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +9 -0
- 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 +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.d.ts +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +10 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.json +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +4 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions.gen.js +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.d.ts +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +10 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +4 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.js +1 -0
- package/dist/.pikku/scenarios/schemas/register.gen.d.ts +4 -0
- package/dist/.pikku/scenarios/schemas/register.gen.js +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 +5 -3
- package/dist/.pikku/schemas/schemas/FabricSecretsListOutput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/FabricSecretsRotateInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricSecretsRotateOutput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricSecretsSetOutput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/ScenarioRunInput.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/meta/allWorkflow.gen.json +2 -8
- package/dist/.pikku/workflow/pikku-scenario-actors.gen.d.ts +19 -0
- package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +17 -0
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +152 -3
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +39 -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 +28 -0
- package/dist/src/deploy/analyzer/analyzer.js +22 -5
- package/dist/src/deploy/build-pipeline.js +5 -1
- package/dist/src/fabric/fabric-commands.d.ts +37 -9
- package/dist/src/fabric/fabric-commands.js +12 -0
- package/dist/src/fabric/functions/domains-add.function.d.ts +4 -4
- package/dist/src/fabric/functions/secrets-list.function.d.ts +16 -4
- package/dist/src/fabric/functions/secrets-list.function.js +12 -10
- package/dist/src/fabric/functions/secrets-rotate.function.d.ts +24 -0
- package/dist/src/fabric/functions/secrets-rotate.function.js +36 -0
- package/dist/src/fabric/functions/secrets-set.function.d.ts +8 -4
- package/dist/src/fabric/functions/secrets-set.function.js +18 -7
- package/dist/src/fabric/lib/http.d.ts +7 -1
- package/dist/src/fabric/lib/sealed-box.d.ts +16 -0
- package/dist/src/fabric/lib/sealed-box.js +72 -0
- package/dist/src/functions/commands/dev.js +11 -0
- package/dist/src/functions/commands/load-user-project.d.ts +7 -0
- package/dist/src/functions/commands/load-user-project.js +21 -0
- package/dist/src/functions/commands/pikku-command-bootstrap.js +13 -0
- package/dist/src/functions/commands/scenario-browser.d.ts +84 -0
- package/dist/src/functions/commands/scenario-browser.js +68 -0
- package/dist/src/functions/commands/scenario-environment.d.ts +32 -0
- package/dist/src/functions/commands/scenario-environment.js +61 -0
- package/dist/src/functions/commands/scenario-formatter.d.ts +81 -0
- package/dist/src/functions/commands/scenario-formatter.js +120 -0
- package/dist/src/functions/commands/scenario-ladder.d.ts +66 -0
- package/dist/src/functions/commands/scenario-ladder.js +132 -0
- package/dist/src/functions/commands/scenario-plan.d.ts +61 -0
- package/dist/src/functions/commands/scenario-plan.js +106 -0
- package/dist/src/functions/commands/scenario.d.ts +24 -0
- package/dist/src/functions/commands/scenario.js +322 -50
- package/dist/src/functions/commands/serve.js +2 -0
- package/dist/src/functions/commands/skills.js +27 -61
- package/dist/src/functions/db/db-codegen.d.ts +6 -0
- package/dist/src/functions/db/db-codegen.js +9 -0
- package/dist/src/functions/db/db-migrator.js +14 -0
- package/dist/src/functions/db/local-db.d.ts +8 -0
- package/dist/src/functions/db/local-db.js +24 -2
- package/dist/src/functions/db/migration-identifiers.d.ts +58 -0
- package/dist/src/functions/db/migration-identifiers.js +262 -0
- package/dist/src/functions/db/migration-provenance.d.ts +35 -0
- package/dist/src/functions/db/migration-provenance.js +80 -0
- package/dist/src/functions/db/schema-sql.d.ts +43 -0
- package/dist/src/functions/db/schema-sql.js +135 -0
- package/dist/src/functions/db/sqlite/sqlite-kysely.js +22 -2
- package/dist/src/functions/wirings/functions/pikku-command-functions.js +26 -8
- package/dist/src/functions/wirings/functions/schemas.js +13 -1
- package/dist/src/functions/wirings/functions/serialize-function-types.js +3 -3
- package/dist/src/functions/wirings/rpc/pikku-command-rpc.js +6 -1
- package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.d.ts +10 -0
- package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.js +102 -0
- package/dist/src/functions/wirings/scenarios/scenario-partition.d.ts +42 -0
- package/dist/src/functions/wirings/scenarios/scenario-partition.js +71 -0
- package/dist/src/functions/wirings/scenarios/scenario-schema-partition.d.ts +33 -0
- package/dist/src/functions/wirings/scenarios/scenario-schema-partition.js +49 -0
- package/dist/src/functions/wirings/scenarios/serialize-feature-meta.d.ts +14 -0
- package/dist/src/functions/wirings/scenarios/serialize-feature-meta.js +29 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.d.ts +12 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.js +56 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.d.ts +7 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.js +40 -0
- package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +43 -8
- package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +1 -1
- package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +13 -2
- package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.d.ts +4 -0
- package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.js +62 -0
- package/dist/src/functions/wirings/workflow/serialize-workflow-bootstrap-map.js +10 -3
- package/dist/src/functions/wirings/workflow/serialize-workflow-map.js +10 -3
- package/dist/src/functions/wirings/workflow/serialize-workflow-meta.js +4 -4
- package/dist/src/functions/wirings/workflow/serialize-workflow-types.d.ts +1 -1
- package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +232 -3
- package/dist/src/functions/workflows/all.workflow.js +2 -7
- package/dist/src/server/server-ready.d.ts +12 -0
- package/dist/src/server/server-ready.js +12 -0
- package/dist/src/server/spawn-dev-server.d.ts +50 -0
- package/dist/src/server/spawn-dev-server.js +112 -0
- package/dist/src/services.js +12 -1
- package/dist/src/utils/file-writer.js +14 -2
- package/dist/src/utils/meta-diff.js +5 -2
- package/dist/src/utils/pikku-cli-config.d.ts +14 -0
- package/dist/src/utils/pikku-cli-config.js +60 -0
- package/dist/src/utils/remove-legacy-scaffold-file.d.ts +14 -0
- package/dist/src/utils/remove-legacy-scaffold-file.js +23 -0
- package/dist/src/utils/resolve-scenario-actors.d.ts +23 -0
- package/dist/src/utils/resolve-scenario-actors.js +74 -0
- package/dist/src/utils/serialize-schemas.d.ts +3 -1
- package/dist/src/utils/serialize-schemas.js +39 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -8
- package/dist/.pikku/schemas/schemas/PikkuScenarioFunctionsOutput.schema.json +0 -1
- package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.d.ts +0 -1
- package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.js +0 -31
- package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.d.ts +0 -10
- package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.js +0 -104
- package/skills/pikku-addon/SKILL.md +0 -243
- package/skills/pikku-addon/references/addon-package-manifest.md +0 -63
- package/skills/pikku-ai-agent/SKILL.md +0 -231
- package/skills/pikku-ai-vercel/SKILL.md +0 -82
- package/skills/pikku-ai-voice/SKILL.md +0 -88
- package/skills/pikku-audit/SKILL.md +0 -175
- package/skills/pikku-aws/SKILL.md +0 -111
- package/skills/pikku-backblaze/SKILL.md +0 -71
- package/skills/pikku-better-auth/SKILL.md +0 -298
- package/skills/pikku-cli/SKILL.md +0 -198
- package/skills/pikku-cli/references/complete-example.md +0 -82
- package/skills/pikku-concepts/SKILL.md +0 -250
- package/skills/pikku-concepts/references/concept-mapping.md +0 -556
- package/skills/pikku-concepts/references/packages.md +0 -29
- package/skills/pikku-config/SKILL.md +0 -212
- package/skills/pikku-cron/SKILL.md +0 -214
- package/skills/pikku-deploy-azure/SKILL.md +0 -71
- package/skills/pikku-deploy-cloudflare/SKILL.md +0 -82
- package/skills/pikku-deploy-express/SKILL.md +0 -86
- package/skills/pikku-deploy-fastify/SKILL.md +0 -73
- package/skills/pikku-deploy-lambda/SKILL.md +0 -110
- package/skills/pikku-deploy-nextjs/SKILL.md +0 -78
- package/skills/pikku-deploy-uws/SKILL.md +0 -88
- package/skills/pikku-deps/SKILL.md +0 -98
- package/skills/pikku-emails/SKILL.md +0 -163
- package/skills/pikku-fabric/SKILL.md +0 -318
- package/skills/pikku-fabric-debug/SKILL.md +0 -112
- package/skills/pikku-feature/SKILL.md +0 -258
- package/skills/pikku-gateway-slack/SKILL.md +0 -115
- package/skills/pikku-http/SKILL.md +0 -220
- package/skills/pikku-http/references/http-options.md +0 -55
- package/skills/pikku-i18n/SKILL.md +0 -137
- package/skills/pikku-info/SKILL.md +0 -100
- package/skills/pikku-jose/SKILL.md +0 -105
- package/skills/pikku-kysely/SKILL.md +0 -219
- package/skills/pikku-machine-auth/SKILL.md +0 -183
- package/skills/pikku-mcp/SKILL.md +0 -241
- package/skills/pikku-middleware/SKILL.md +0 -231
- package/skills/pikku-middleware/references/middleware-patterns.md +0 -61
- package/skills/pikku-mongodb/SKILL.md +0 -105
- package/skills/pikku-n8n-import/SKILL.md +0 -109
- package/skills/pikku-n8n-import/SPEC.md +0 -84
- package/skills/pikku-n8n-import/references/addon-mapping.md +0 -121
- package/skills/pikku-n8n-import/references/code-translation.md +0 -121
- package/skills/pikku-n8n-import/references/loops-and-control.md +0 -87
- package/skills/pikku-paraglide/SKILL.md +0 -117
- package/skills/pikku-permissions/SKILL.md +0 -192
- package/skills/pikku-pino/SKILL.md +0 -79
- package/skills/pikku-product-second-opinion/README.md +0 -43
- package/skills/pikku-product-second-opinion/SKILL.md +0 -158
- package/skills/pikku-product-second-opinion/example/sample-report.md +0 -90
- package/skills/pikku-product-second-opinion/references/report-template.md +0 -73
- package/skills/pikku-queue/SKILL.md +0 -240
- package/skills/pikku-react/SKILL.md +0 -212
- package/skills/pikku-react-query/SKILL.md +0 -242
- package/skills/pikku-realtime/SKILL.md +0 -236
- package/skills/pikku-realtime/references/other-routes.md +0 -23
- package/skills/pikku-redis/SKILL.md +0 -90
- package/skills/pikku-rpc/SKILL.md +0 -171
- package/skills/pikku-rtl/SKILL.md +0 -219
- package/skills/pikku-scenario/SKILL.md +0 -215
- package/skills/pikku-schedule/SKILL.md +0 -57
- package/skills/pikku-schema-ajv/SKILL.md +0 -62
- package/skills/pikku-schema-cfworker/SKILL.md +0 -63
- package/skills/pikku-security/SKILL.md +0 -108
- package/skills/pikku-services/SKILL.md +0 -248
- package/skills/pikku-services/references/audit-wire-service.md +0 -34
- package/skills/pikku-software-archaeology/README.md +0 -70
- package/skills/pikku-software-archaeology/SKILL.md +0 -186
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +0 -625
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +0 -49
- package/skills/pikku-software-archaeology/scripts/validate.mjs +0 -173
- package/skills/pikku-tag-middleware/SKILL.md +0 -13
- package/skills/pikku-template-clone/SKILL.md +0 -40
- package/skills/pikku-trigger/SKILL.md +0 -181
- package/skills/pikku-versioning/SKILL.md +0 -173
- package/skills/pikku-websocket/SKILL.md +0 -243
- package/skills/pikku-workflow/SKILL.md +0 -172
- package/skills/pikku-workflow/references/workflow-reference.md +0 -63
- package/skills/pikku-workflows-client/SKILL.md +0 -150
- package/skills/pikku-ws/SKILL.md +0 -47
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { loadAnnotations, nameSuggestsKind, } from './annotation-parser.js';
|
|
4
4
|
import { ZOD_FORMATS } from './zod-codegen.js';
|
|
5
|
+
import { bareUnquotedTableName, readMigrationProvenance, } from './migration-provenance.js';
|
|
5
6
|
import { ErrorCode } from '@pikku/inspector';
|
|
6
7
|
/**
|
|
7
8
|
* The column kind implied by the *real* DB type, dialect-aware. Postgres has
|
|
@@ -504,8 +505,16 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
504
505
|
let schemaJsonBody = null;
|
|
505
506
|
if (options.schemaJsonFile) {
|
|
506
507
|
const fkMap = await introspector.getAllForeignKeys();
|
|
508
|
+
// Attribution comes from the migrations rather than from `schemaSources()`:
|
|
509
|
+
// the JSON is written by `db migrate`, which has no reason to load the
|
|
510
|
+
// project's Better Auth config, and the generated migrations already record
|
|
511
|
+
// both the source and its prose. See `readMigrationProvenance`.
|
|
512
|
+
const provenance = options.migrationsDir
|
|
513
|
+
? readMigrationProvenance(options.migrationsDir)
|
|
514
|
+
: new Map();
|
|
507
515
|
const jsonTables = tables.map((t) => ({
|
|
508
516
|
name: t.name,
|
|
517
|
+
...(provenance.get(bareUnquotedTableName(t.name)) ?? { source: 'app' }),
|
|
509
518
|
columns: t.columns.map((c) => {
|
|
510
519
|
const fk = fkMap.get(t.name)?.find((f) => f.column === c.name);
|
|
511
520
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { assertSnakeCaseIdentifiers } from './migration-identifiers.js';
|
|
4
5
|
/**
|
|
5
6
|
* The migrator's own bookkeeping table, which belongs to no dialect and to no
|
|
6
7
|
* project's schema.
|
|
@@ -75,7 +76,20 @@ function assertNoDrift(applied, migrationsDir) {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Every migration on disk, read once.
|
|
81
|
+
*
|
|
82
|
+
* The identifier check reads all of them and not just the pending ones, so that
|
|
83
|
+
* whether a migration is rejected depends on the files alone. A camelCase
|
|
84
|
+
* column that had already been applied somewhere would otherwise pass on that
|
|
85
|
+
* machine and fail on a fresh checkout, which is the opposite of deterministic.
|
|
86
|
+
*/
|
|
87
|
+
const readMigrations = (migrationsDir) => migrationFiles(migrationsDir).map((name) => ({
|
|
88
|
+
name,
|
|
89
|
+
sql: readFileSync(join(migrationsDir, name), 'utf8'),
|
|
90
|
+
}));
|
|
78
91
|
export async function migrate(executor, migrationsDir) {
|
|
92
|
+
assertSnakeCaseIdentifiers(readMigrations(migrationsDir));
|
|
79
93
|
await executor.ensureTrackingTable();
|
|
80
94
|
const applied = await executor.getApplied();
|
|
81
95
|
assertNoDrift(applied, migrationsDir);
|
|
@@ -307,6 +307,14 @@ export interface GenerateResult {
|
|
|
307
307
|
* Partially covered writes the delta, because re-emitting the whole schema
|
|
308
308
|
* would fail on the tables that already exist.
|
|
309
309
|
*
|
|
310
|
+
* The delta is itself two different things, and only one of them is a diff. A
|
|
311
|
+
* table the migrations already have but a column short is a genuine alteration,
|
|
312
|
+
* so it becomes `ALTER TABLE … ADD COLUMN`. A table they do not have at all —
|
|
313
|
+
* what enabling a Better Auth plugin or upgrading an addon produces — has
|
|
314
|
+
* nothing to diff against, so it is lifted verbatim out of the source's own SQL
|
|
315
|
+
* for the same reason the first-time case is: the column map it would otherwise
|
|
316
|
+
* be rendered from knows nothing about keys, constraints or indexes.
|
|
317
|
+
*
|
|
310
318
|
* Migrations are written one file per source, numbered in dependency order, so
|
|
311
319
|
* a project can review and apply them independently.
|
|
312
320
|
*/
|
|
@@ -10,6 +10,7 @@ import { loadAuthOptions, getAuthMigrations } from './better-auth-schema.js';
|
|
|
10
10
|
import { generateSchemaTypes } from './db-codegen.js';
|
|
11
11
|
import { generateZodTypes } from './zod-codegen.js';
|
|
12
12
|
import { createCoercionPlugin } from './coercion-plugin.js';
|
|
13
|
+
import { tableCreationSql } from './schema-sql.js';
|
|
13
14
|
import { SqliteMigrationExecutor } from './sqlite/sqlite-migrator.js';
|
|
14
15
|
import { SqliteIntrospector } from './sqlite/sqlite-introspector.js';
|
|
15
16
|
import { createSqliteKysely } from './sqlite/sqlite-kysely.js';
|
|
@@ -190,6 +191,7 @@ export async function migrateAndCodegen(resolved, options = {}) {
|
|
|
190
191
|
camelCase: resolved.camelCase,
|
|
191
192
|
rootDir: resolved.rootDir,
|
|
192
193
|
dialect: 'sqlite',
|
|
194
|
+
migrationsDir: resolved.migrationsDir,
|
|
193
195
|
});
|
|
194
196
|
}
|
|
195
197
|
finally {
|
|
@@ -216,6 +218,7 @@ export async function migrateAndCodegen(resolved, options = {}) {
|
|
|
216
218
|
camelCase: resolved.camelCase,
|
|
217
219
|
rootDir: resolved.rootDir,
|
|
218
220
|
dialect: 'postgres',
|
|
221
|
+
migrationsDir: resolved.migrationsDir,
|
|
219
222
|
});
|
|
220
223
|
}
|
|
221
224
|
finally {
|
|
@@ -983,6 +986,14 @@ function addColumnStatements(table, columns) {
|
|
|
983
986
|
* Partially covered writes the delta, because re-emitting the whole schema
|
|
984
987
|
* would fail on the tables that already exist.
|
|
985
988
|
*
|
|
989
|
+
* The delta is itself two different things, and only one of them is a diff. A
|
|
990
|
+
* table the migrations already have but a column short is a genuine alteration,
|
|
991
|
+
* so it becomes `ALTER TABLE … ADD COLUMN`. A table they do not have at all —
|
|
992
|
+
* what enabling a Better Auth plugin or upgrading an addon produces — has
|
|
993
|
+
* nothing to diff against, so it is lifted verbatim out of the source's own SQL
|
|
994
|
+
* for the same reason the first-time case is: the column map it would otherwise
|
|
995
|
+
* be rendered from knows nothing about keys, constraints or indexes.
|
|
996
|
+
*
|
|
986
997
|
* Migrations are written one file per source, numbered in dependency order, so
|
|
987
998
|
* a project can review and apply them independently.
|
|
988
999
|
*/
|
|
@@ -1009,9 +1020,20 @@ export async function generateMigrations(resolved, rootDir, srcDirectories, logg
|
|
|
1009
1020
|
else {
|
|
1010
1021
|
const statements = [];
|
|
1011
1022
|
for (const table of missingTables) {
|
|
1023
|
+
// A wholly new table is the first-time case in miniature: nothing to
|
|
1024
|
+
// diff against, and the source's own SQL already says exactly how to
|
|
1025
|
+
// build it. Rendering the column map instead would drop the primary
|
|
1026
|
+
// key, the foreign keys and the indexes — a table that applies cleanly
|
|
1027
|
+
// and is permanently wrong.
|
|
1028
|
+
const own = tableCreationSql(source.desired.sql, table);
|
|
1029
|
+
if (own.length > 0) {
|
|
1030
|
+
statements.push(own.join('\n\n'));
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1012
1033
|
const columns = source.desired.tables.get(table);
|
|
1013
|
-
statements.push(`-- REVIEW: ${table} is new
|
|
1014
|
-
`--
|
|
1034
|
+
statements.push(`-- REVIEW: ${table} is new, and its CREATE TABLE could not be found in the\n` +
|
|
1035
|
+
`-- source's own SQL. The column list below carries no indexes, constraints\n` +
|
|
1036
|
+
`-- or foreign keys — copy the real statement over it before applying.\n` +
|
|
1015
1037
|
`CREATE TABLE ${table} (\n` +
|
|
1016
1038
|
[...(columns?.values() ?? [])]
|
|
1017
1039
|
.map((c) => ` ${c.name} ${c.type}${c.notNull ? ' NOT NULL' : ''}`)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catching camelCase columns at migrate time, before they can half-work.
|
|
3
|
+
*
|
|
4
|
+
* Pikku's Kysely runs with `CamelCasePlugin`, so `priceCents` in TypeScript is
|
|
5
|
+
* `price_cents` in SQL and nothing else. A migration that declares the column
|
|
6
|
+
* *as* `priceCents` is not merely unconventional — it is broken in the one way
|
|
7
|
+
* that hides itself: `.selectAll()` compiles to `SELECT *` and never names an
|
|
8
|
+
* identifier, so the table reads back perfectly, while the first query that
|
|
9
|
+
* names the column (`.select(['animal.priceCents'])`) asks for `price_cents`
|
|
10
|
+
* and gets `no such column`. The usual conclusion is that the plugin is broken,
|
|
11
|
+
* and the usual response is a raw `sql` template or a retreat to `.selectAll()`
|
|
12
|
+
* — both of which keep the real cause alive.
|
|
13
|
+
*
|
|
14
|
+
* There is no exception to escape. Even the Better Auth schema `db generate`
|
|
15
|
+
* writes is snake_case (`email_verified`, `user_id`), because Better Auth is
|
|
16
|
+
* handed the app's own Kysely and its camelCase field names compile the same
|
|
17
|
+
* way everyone else's do.
|
|
18
|
+
*/
|
|
19
|
+
/** A camelCase identifier a migration declares, and what it should have said. */
|
|
20
|
+
export interface CamelCaseIdentifier {
|
|
21
|
+
file: string;
|
|
22
|
+
table: string;
|
|
23
|
+
/** `null` when the table name itself is the offender. */
|
|
24
|
+
column: string | null;
|
|
25
|
+
suggestion: string;
|
|
26
|
+
}
|
|
27
|
+
export declare class CamelCaseIdentifierError extends Error {
|
|
28
|
+
readonly offenders: CamelCaseIdentifier[];
|
|
29
|
+
constructor(offenders: CamelCaseIdentifier[]);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Strip comments so a word in prose can never be read as an identifier.
|
|
33
|
+
*
|
|
34
|
+
* `splitStatements` already skips over comments when it looks for a boundary,
|
|
35
|
+
* but it hands back the statement with them still in it, and `-- the userId
|
|
36
|
+
* column` would otherwise be flagged. Quoting rules are honoured for the same
|
|
37
|
+
* reason they are there: `'a -- b'` is a string, not a comment.
|
|
38
|
+
*/
|
|
39
|
+
export declare function stripSqlComments(sql: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Every camelCase identifier one migration file declares.
|
|
42
|
+
*
|
|
43
|
+
* Only declarations are read — a `CREATE TABLE` column list and an `ALTER TABLE
|
|
44
|
+
* … ADD COLUMN`. Everywhere else an identifier merely refers to something that
|
|
45
|
+
* was declared somewhere, and reporting those would name the same mistake once
|
|
46
|
+
* per reference while adding nothing to the fix.
|
|
47
|
+
*/
|
|
48
|
+
export declare function findCamelCaseIdentifiers(file: string, sql: string): CamelCaseIdentifier[];
|
|
49
|
+
/**
|
|
50
|
+
* Bail unless every migration on disk is snake_case throughout.
|
|
51
|
+
*
|
|
52
|
+
* Reports all of them at once: a camelCase column is rarely alone, and a
|
|
53
|
+
* one-at-a-time failure turns a single edit into one migrate run per column.
|
|
54
|
+
*/
|
|
55
|
+
export declare function assertSnakeCaseIdentifiers(migrations: Array<{
|
|
56
|
+
name: string;
|
|
57
|
+
sql: string;
|
|
58
|
+
}>): void;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catching camelCase columns at migrate time, before they can half-work.
|
|
3
|
+
*
|
|
4
|
+
* Pikku's Kysely runs with `CamelCasePlugin`, so `priceCents` in TypeScript is
|
|
5
|
+
* `price_cents` in SQL and nothing else. A migration that declares the column
|
|
6
|
+
* *as* `priceCents` is not merely unconventional — it is broken in the one way
|
|
7
|
+
* that hides itself: `.selectAll()` compiles to `SELECT *` and never names an
|
|
8
|
+
* identifier, so the table reads back perfectly, while the first query that
|
|
9
|
+
* names the column (`.select(['animal.priceCents'])`) asks for `price_cents`
|
|
10
|
+
* and gets `no such column`. The usual conclusion is that the plugin is broken,
|
|
11
|
+
* and the usual response is a raw `sql` template or a retreat to `.selectAll()`
|
|
12
|
+
* — both of which keep the real cause alive.
|
|
13
|
+
*
|
|
14
|
+
* There is no exception to escape. Even the Better Auth schema `db generate`
|
|
15
|
+
* writes is snake_case (`email_verified`, `user_id`), because Better Auth is
|
|
16
|
+
* handed the app's own Kysely and its camelCase field names compile the same
|
|
17
|
+
* way everyone else's do.
|
|
18
|
+
*/
|
|
19
|
+
import { splitStatements } from './schema-sql.js';
|
|
20
|
+
export class CamelCaseIdentifierError extends Error {
|
|
21
|
+
offenders;
|
|
22
|
+
constructor(offenders) {
|
|
23
|
+
const lines = offenders.map((o) => ` ${o.file} ${o.column === null ? o.table : `${o.table}.${o.column}`} → ${o.suggestion}`);
|
|
24
|
+
super(`[PKU-DB-CAMEL] Migrations declare camelCase identifiers.\n\n` +
|
|
25
|
+
`Pikku's Kysely runs with CamelCasePlugin, which maps camelCase in TypeScript\n` +
|
|
26
|
+
`to snake_case in SQL. A camelCase column half-works: \`.selectAll()\` emits\n` +
|
|
27
|
+
`\`SELECT *\` so the table reads fine, but naming the column compiles it to\n` +
|
|
28
|
+
`snake_case and the database answers \`no such column\`.\n\n` +
|
|
29
|
+
`${lines.join('\n')}\n\n` +
|
|
30
|
+
`Fix the column definition in the migration file itself and replay your dev\n` +
|
|
31
|
+
`database from scratch. Do not write a RENAME COLUMN migration — that leaves\n` +
|
|
32
|
+
`the banned identifier in a SQL file forever.`);
|
|
33
|
+
this.offenders = offenders;
|
|
34
|
+
this.name = 'CamelCaseIdentifierError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Strip comments so a word in prose can never be read as an identifier.
|
|
39
|
+
*
|
|
40
|
+
* `splitStatements` already skips over comments when it looks for a boundary,
|
|
41
|
+
* but it hands back the statement with them still in it, and `-- the userId
|
|
42
|
+
* column` would otherwise be flagged. Quoting rules are honoured for the same
|
|
43
|
+
* reason they are there: `'a -- b'` is a string, not a comment.
|
|
44
|
+
*/
|
|
45
|
+
export function stripSqlComments(sql) {
|
|
46
|
+
let out = '';
|
|
47
|
+
let i = 0;
|
|
48
|
+
while (i < sql.length) {
|
|
49
|
+
const ch = sql[i];
|
|
50
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
51
|
+
const start = i;
|
|
52
|
+
i++;
|
|
53
|
+
while (i < sql.length) {
|
|
54
|
+
if (sql[i] === '\\' && ch === "'") {
|
|
55
|
+
i += 2;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (sql[i] === ch) {
|
|
59
|
+
if (sql[i + 1] === ch) {
|
|
60
|
+
i += 2;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
i++;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
out += sql.slice(start, i);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (ch === '-' && sql[i + 1] === '-') {
|
|
72
|
+
const end = sql.indexOf('\n', i);
|
|
73
|
+
i = end === -1 ? sql.length : end;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (ch === '/' && sql[i + 1] === '*') {
|
|
77
|
+
const end = sql.indexOf('*/', i + 2);
|
|
78
|
+
i = end === -1 ? sql.length : end + 2;
|
|
79
|
+
// A block comment can span lines, and removing it outright would join two
|
|
80
|
+
// statements onto one line. A space keeps the tokens either side apart.
|
|
81
|
+
out += ' ';
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
out += ch;
|
|
85
|
+
i++;
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
const IDENTIFIER = String.raw `"[^"]*"|\`[^\`]*\`|\[[^\]]*\]|[A-Za-z_][A-Za-z_0-9$]*`;
|
|
90
|
+
const CREATE_TABLE = new RegExp(String.raw `^CREATE\s+(?:TEMP(?:ORARY)?\s+|UNLOGGED\s+)*TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(\S+?)\s*\(`, 'i');
|
|
91
|
+
const ALTER_TABLE = new RegExp(String.raw `^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?(\S+)`, 'i');
|
|
92
|
+
const ADD_COLUMN = new RegExp(String.raw `^\s*ADD\s+(?:COLUMN\s+)?(?:IF\s+NOT\s+EXISTS\s+)?(${IDENTIFIER})`, 'i');
|
|
93
|
+
const COLUMN_NAME = new RegExp(String.raw `^\s*(${IDENTIFIER})`);
|
|
94
|
+
/**
|
|
95
|
+
* The words that open a table-level constraint rather than a column.
|
|
96
|
+
*
|
|
97
|
+
* Only consulted for an unquoted first token: `"check"` in quotes is a column
|
|
98
|
+
* named check, however unwise, and skipping it would let `"checkedAt"`'s
|
|
99
|
+
* neighbours hide behind it.
|
|
100
|
+
*/
|
|
101
|
+
const CONSTRAINT_KEYWORDS = new Set([
|
|
102
|
+
'constraint',
|
|
103
|
+
'primary',
|
|
104
|
+
'foreign',
|
|
105
|
+
'unique',
|
|
106
|
+
'check',
|
|
107
|
+
'exclude',
|
|
108
|
+
'index',
|
|
109
|
+
'key',
|
|
110
|
+
'like',
|
|
111
|
+
'period',
|
|
112
|
+
'fulltext',
|
|
113
|
+
'spatial',
|
|
114
|
+
]);
|
|
115
|
+
/** Drop the quoting a dialect happens to use, leaving the identifier itself. */
|
|
116
|
+
const unquote = (name) => /^["`[]/.test(name) ? name.slice(1, -1) : name;
|
|
117
|
+
const isCamelCase = (name) => /[a-z][A-Z]/.test(name);
|
|
118
|
+
const toSnakeCase = (name) => name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
119
|
+
/**
|
|
120
|
+
* Walk `sql` from `from` and return the offsets just inside and just outside
|
|
121
|
+
* the parenthesised group that starts there, or `null` if it never closes.
|
|
122
|
+
*
|
|
123
|
+
* Quote-aware, because a `)` inside `DEFAULT ')'` closes nothing.
|
|
124
|
+
*/
|
|
125
|
+
function parenSpan(sql, from) {
|
|
126
|
+
let depth = 0;
|
|
127
|
+
let i = from;
|
|
128
|
+
while (i < sql.length) {
|
|
129
|
+
const ch = sql[i];
|
|
130
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
131
|
+
i++;
|
|
132
|
+
while (i < sql.length && sql[i] !== ch)
|
|
133
|
+
i++;
|
|
134
|
+
i++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (ch === '(') {
|
|
138
|
+
depth++;
|
|
139
|
+
if (depth === 1)
|
|
140
|
+
from = i + 1;
|
|
141
|
+
}
|
|
142
|
+
else if (ch === ')') {
|
|
143
|
+
depth--;
|
|
144
|
+
if (depth === 0)
|
|
145
|
+
return { start: from, end: i };
|
|
146
|
+
}
|
|
147
|
+
i++;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Split a definition list on the commas that separate its items.
|
|
153
|
+
*
|
|
154
|
+
* The commas inside `NUMERIC(10,2)`, a `CHECK (x IN ('a','b'))` or a compound
|
|
155
|
+
* `PRIMARY KEY (a, b)` belong to those and not to the list, so only depth zero
|
|
156
|
+
* counts.
|
|
157
|
+
*/
|
|
158
|
+
function splitTopLevel(list) {
|
|
159
|
+
const items = [];
|
|
160
|
+
let depth = 0;
|
|
161
|
+
let start = 0;
|
|
162
|
+
let i = 0;
|
|
163
|
+
while (i < list.length) {
|
|
164
|
+
const ch = list[i];
|
|
165
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
166
|
+
i++;
|
|
167
|
+
while (i < list.length && list[i] !== ch)
|
|
168
|
+
i++;
|
|
169
|
+
i++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (ch === '(')
|
|
173
|
+
depth++;
|
|
174
|
+
else if (ch === ')')
|
|
175
|
+
depth--;
|
|
176
|
+
else if (ch === ',' && depth === 0) {
|
|
177
|
+
items.push(list.slice(start, i));
|
|
178
|
+
start = i + 1;
|
|
179
|
+
}
|
|
180
|
+
i++;
|
|
181
|
+
}
|
|
182
|
+
items.push(list.slice(start));
|
|
183
|
+
return items.filter((item) => item.trim().length > 0);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Every camelCase identifier one migration file declares.
|
|
187
|
+
*
|
|
188
|
+
* Only declarations are read — a `CREATE TABLE` column list and an `ALTER TABLE
|
|
189
|
+
* … ADD COLUMN`. Everywhere else an identifier merely refers to something that
|
|
190
|
+
* was declared somewhere, and reporting those would name the same mistake once
|
|
191
|
+
* per reference while adding nothing to the fix.
|
|
192
|
+
*/
|
|
193
|
+
export function findCamelCaseIdentifiers(file, sql) {
|
|
194
|
+
const offenders = [];
|
|
195
|
+
for (const statement of splitStatements(stripSqlComments(sql))) {
|
|
196
|
+
const create = CREATE_TABLE.exec(statement);
|
|
197
|
+
if (create) {
|
|
198
|
+
const table = unquote(create[1].split('.').pop());
|
|
199
|
+
if (isCamelCase(table)) {
|
|
200
|
+
offenders.push({
|
|
201
|
+
file,
|
|
202
|
+
table,
|
|
203
|
+
column: null,
|
|
204
|
+
suggestion: toSnakeCase(table),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const span = parenSpan(statement, create.index + create[0].length - 1);
|
|
208
|
+
if (!span)
|
|
209
|
+
continue;
|
|
210
|
+
for (const item of splitTopLevel(statement.slice(span.start, span.end))) {
|
|
211
|
+
const name = COLUMN_NAME.exec(item)?.[1];
|
|
212
|
+
if (!name)
|
|
213
|
+
continue;
|
|
214
|
+
if (CONSTRAINT_KEYWORDS.has(name.toLowerCase()))
|
|
215
|
+
continue;
|
|
216
|
+
const column = unquote(name);
|
|
217
|
+
if (isCamelCase(column)) {
|
|
218
|
+
offenders.push({
|
|
219
|
+
file,
|
|
220
|
+
table,
|
|
221
|
+
column,
|
|
222
|
+
suggestion: toSnakeCase(column),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const alter = ALTER_TABLE.exec(statement);
|
|
229
|
+
if (!alter)
|
|
230
|
+
continue;
|
|
231
|
+
const table = unquote(alter[1].split('.').pop());
|
|
232
|
+
// Postgres lets one ALTER carry several actions, and only the ADDs declare.
|
|
233
|
+
for (const action of splitTopLevel(statement.slice(alter[0].length).replace(/;\s*$/, ''))) {
|
|
234
|
+
const name = ADD_COLUMN.exec(action)?.[1];
|
|
235
|
+
if (!name)
|
|
236
|
+
continue;
|
|
237
|
+
if (CONSTRAINT_KEYWORDS.has(name.toLowerCase()))
|
|
238
|
+
continue;
|
|
239
|
+
const column = unquote(name);
|
|
240
|
+
if (isCamelCase(column)) {
|
|
241
|
+
offenders.push({
|
|
242
|
+
file,
|
|
243
|
+
table,
|
|
244
|
+
column,
|
|
245
|
+
suggestion: toSnakeCase(column),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return offenders;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Bail unless every migration on disk is snake_case throughout.
|
|
254
|
+
*
|
|
255
|
+
* Reports all of them at once: a camelCase column is rarely alone, and a
|
|
256
|
+
* one-at-a-time failure turns a single edit into one migrate run per column.
|
|
257
|
+
*/
|
|
258
|
+
export function assertSnakeCaseIdentifiers(migrations) {
|
|
259
|
+
const offenders = migrations.flatMap(({ name, sql }) => findCamelCaseIdentifiers(name, sql));
|
|
260
|
+
if (offenders.length > 0)
|
|
261
|
+
throw new CamelCaseIdentifierError(offenders);
|
|
262
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who declared a table, and where the declaration came from.
|
|
3
|
+
*
|
|
4
|
+
* `source` is the stable identifier a consumer filters on — the same label
|
|
5
|
+
* `db generate` gives the migration it writes (`better-auth`, `pikku-runtime`,
|
|
6
|
+
* an addon's flattened package name). `origin` is the prose from the migration
|
|
7
|
+
* header, written for a human reading a tooltip.
|
|
8
|
+
*/
|
|
9
|
+
export interface TableProvenance {
|
|
10
|
+
source: string;
|
|
11
|
+
origin: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Drop the schema qualifier and any quoting, so `"app"."user"` and `user` are
|
|
15
|
+
* the same key.
|
|
16
|
+
*
|
|
17
|
+
* Introspection reports a bare name on SQLite and a qualified one on Postgres,
|
|
18
|
+
* while a migration may write either — comparing on the bare, unquoted name is
|
|
19
|
+
* the only form both sides agree on.
|
|
20
|
+
*/
|
|
21
|
+
export declare function bareUnquotedTableName(name: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Read the table→source map out of a project's migration directory.
|
|
24
|
+
*
|
|
25
|
+
* Provenance is decided at `db generate` time, but the schema JSON is written
|
|
26
|
+
* at `db migrate` time, and the two commands do not have to run together. The
|
|
27
|
+
* migration files are the durable record that survives between them: each
|
|
28
|
+
* generated one carries its origin in a header and its source in its filename,
|
|
29
|
+
* so re-reading them costs a directory scan and needs neither a Better Auth
|
|
30
|
+
* config load nor a live database.
|
|
31
|
+
*
|
|
32
|
+
* Tables absent from the result are app-owned — including every table when the
|
|
33
|
+
* project has no generated migrations at all.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readMigrationProvenance(migrationsDir: string): Map<string, TableProvenance>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* The header `db generate` stamps on every migration it writes.
|
|
5
|
+
*
|
|
6
|
+
* Matching it is what separates a generated migration from one the project
|
|
7
|
+
* authored: a file without this line declares the project's own tables, and
|
|
8
|
+
* anything it creates is app-owned by definition.
|
|
9
|
+
*/
|
|
10
|
+
const GENERATED_HEADER = /^-- Generated by `pikku db generate` from (.+)\.\s*$/m;
|
|
11
|
+
/** `0002-pikku-runtime.sql` → `pikku-runtime`. */
|
|
12
|
+
const MIGRATION_LABEL = /^\d+-(.+)\.sql$/;
|
|
13
|
+
/**
|
|
14
|
+
* Strip `--` line comments so a `CREATE TABLE` quoted inside a REVIEW note is
|
|
15
|
+
* not mistaken for one the migration actually runs.
|
|
16
|
+
*/
|
|
17
|
+
const stripComments = (sql) => sql.replace(/--[^\n]*/g, '');
|
|
18
|
+
const CREATE_TABLE = /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([^\s(;]+)/gi;
|
|
19
|
+
/**
|
|
20
|
+
* Drop the schema qualifier and any quoting, so `"app"."user"` and `user` are
|
|
21
|
+
* the same key.
|
|
22
|
+
*
|
|
23
|
+
* Introspection reports a bare name on SQLite and a qualified one on Postgres,
|
|
24
|
+
* while a migration may write either — comparing on the bare, unquoted name is
|
|
25
|
+
* the only form both sides agree on.
|
|
26
|
+
*/
|
|
27
|
+
export function bareUnquotedTableName(name) {
|
|
28
|
+
const last = name.split('.').pop() ?? name;
|
|
29
|
+
return last.replace(/^["'`[]|["'`\]]$/g, '');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read the table→source map out of a project's migration directory.
|
|
33
|
+
*
|
|
34
|
+
* Provenance is decided at `db generate` time, but the schema JSON is written
|
|
35
|
+
* at `db migrate` time, and the two commands do not have to run together. The
|
|
36
|
+
* migration files are the durable record that survives between them: each
|
|
37
|
+
* generated one carries its origin in a header and its source in its filename,
|
|
38
|
+
* so re-reading them costs a directory scan and needs neither a Better Auth
|
|
39
|
+
* config load nor a live database.
|
|
40
|
+
*
|
|
41
|
+
* Tables absent from the result are app-owned — including every table when the
|
|
42
|
+
* project has no generated migrations at all.
|
|
43
|
+
*/
|
|
44
|
+
export function readMigrationProvenance(migrationsDir) {
|
|
45
|
+
const provenance = new Map();
|
|
46
|
+
let files;
|
|
47
|
+
try {
|
|
48
|
+
files = readdirSync(migrationsDir);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// No migrations directory is the ordinary case for a project that has not
|
|
52
|
+
// run `db generate` yet — every table is app-owned.
|
|
53
|
+
return provenance;
|
|
54
|
+
}
|
|
55
|
+
for (const file of files.filter((f) => f.endsWith('.sql')).sort()) {
|
|
56
|
+
let sql;
|
|
57
|
+
try {
|
|
58
|
+
sql = readFileSync(join(migrationsDir, file), 'utf8');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const header = GENERATED_HEADER.exec(sql);
|
|
64
|
+
if (!header)
|
|
65
|
+
continue;
|
|
66
|
+
const label = MIGRATION_LABEL.exec(file);
|
|
67
|
+
if (!label)
|
|
68
|
+
continue;
|
|
69
|
+
const source = label[1];
|
|
70
|
+
const origin = header[1];
|
|
71
|
+
for (const match of stripComments(sql).matchAll(CREATE_TABLE)) {
|
|
72
|
+
const table = bareUnquotedTableName(match[1]);
|
|
73
|
+
// First writer wins: a later migration that recreates a table is fixing
|
|
74
|
+
// it, not claiming it.
|
|
75
|
+
if (!provenance.has(table))
|
|
76
|
+
provenance.set(table, { source, origin });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return provenance;
|
|
80
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a schema source's own SQL back out, one table at a time.
|
|
3
|
+
*
|
|
4
|
+
* A source hands `db generate` both what must exist (`tables`) and what creates
|
|
5
|
+
* it (`sql`). The column map is a comparison surface — it answers "is this
|
|
6
|
+
* table there, and does it have these columns" — and it is deliberately lossy:
|
|
7
|
+
* primary keys, foreign keys, uniqueness, check constraints and indexes are all
|
|
8
|
+
* absent from it. So the moment the generator has to *create* something, the
|
|
9
|
+
* only honest source is the SQL, and this is what pulls the relevant part of it
|
|
10
|
+
* out.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Split a SQL script into its top-level statements, semicolons included.
|
|
14
|
+
*
|
|
15
|
+
* A naive `split(';')` is wrong in a way that only shows up later: a semicolon
|
|
16
|
+
* inside a string literal, a quoted identifier or a comment is not a statement
|
|
17
|
+
* boundary, and cutting there yields two fragments that are each valid-looking
|
|
18
|
+
* and neither of which does what the original did.
|
|
19
|
+
*/
|
|
20
|
+
export declare function splitStatements(sql: string): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Reduce a written table name to the form two sources can be compared on.
|
|
23
|
+
*
|
|
24
|
+
* The same table is `two_factor` to one writer, `"two_factor"` to Kysely and
|
|
25
|
+
* `public.two_factor` to Postgres introspection. Dropping the schema qualifier,
|
|
26
|
+
* the quoting and the case is the only shape all three agree on.
|
|
27
|
+
*/
|
|
28
|
+
export declare function bareTableName(name: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Every statement in `sql` that builds `table`, in the order the source wrote
|
|
31
|
+
* them.
|
|
32
|
+
*
|
|
33
|
+
* The `CREATE TABLE` and its indexes, plus any `ALTER TABLE` the source uses to
|
|
34
|
+
* hang a constraint on it afterwards — which together are what the column list
|
|
35
|
+
* cannot express. Source order is preserved because it is load-bearing: an
|
|
36
|
+
* index cannot precede its table, and a table cannot precede one it references.
|
|
37
|
+
*
|
|
38
|
+
* An empty result means the source's SQL does not visibly create the table.
|
|
39
|
+
* That is a real answer rather than a failure — a source may create a table
|
|
40
|
+
* from something other than a literal `CREATE TABLE` — and the caller is
|
|
41
|
+
* expected to fall back rather than emit nothing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function tableCreationSql(sql: string, table: string): string[];
|