@bockit/sequel 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +227 -2
  3. package/THIRD_PARTY_NOTICES.md +268 -0
  4. package/binaries.json +106 -0
  5. package/dist/binaries/cache.d.ts +6 -0
  6. package/dist/binaries/cache.js +36 -0
  7. package/dist/binaries/ensure-binary.d.ts +10 -0
  8. package/dist/binaries/ensure-binary.js +85 -0
  9. package/dist/binaries/manifest.d.ts +30 -0
  10. package/dist/binaries/manifest.js +112 -0
  11. package/dist/binaries/platform.d.ts +3 -0
  12. package/dist/binaries/platform.js +10 -0
  13. package/dist/binaries/unpack.d.ts +2 -0
  14. package/dist/binaries/unpack.js +35 -0
  15. package/dist/binaries/verify.d.ts +2 -0
  16. package/dist/binaries/verify.js +16 -0
  17. package/dist/check.d.ts +9 -0
  18. package/dist/check.js +178 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +339 -0
  21. package/dist/config/define-config.d.ts +24 -0
  22. package/dist/config/define-config.js +4 -0
  23. package/dist/config/load-config.d.ts +6 -0
  24. package/dist/config/load-config.js +34 -0
  25. package/dist/config/resolve-config.d.ts +35 -0
  26. package/dist/config/resolve-config.js +159 -0
  27. package/dist/db/connection.d.ts +6 -0
  28. package/dist/db/connection.js +38 -0
  29. package/dist/db/shadow.d.ts +11 -0
  30. package/dist/db/shadow.js +68 -0
  31. package/dist/engine/client.d.ts +24 -0
  32. package/dist/engine/client.js +123 -0
  33. package/dist/engine/diff.d.ts +12 -0
  34. package/dist/engine/diff.js +51 -0
  35. package/dist/errors.d.ts +3 -0
  36. package/dist/errors.js +5 -0
  37. package/dist/extras/snapshot.d.ts +3 -0
  38. package/dist/extras/snapshot.js +99 -0
  39. package/dist/files.d.ts +1 -0
  40. package/dist/files.js +15 -0
  41. package/dist/generate/generate-modules.d.ts +17 -0
  42. package/dist/generate/generate-modules.js +97 -0
  43. package/dist/generate/modules.d.ts +16 -0
  44. package/dist/generate/modules.js +118 -0
  45. package/dist/generate/pg-dump.d.ts +8 -0
  46. package/dist/generate/pg-dump.js +66 -0
  47. package/dist/generate/plugin-wrapper.d.ts +3 -0
  48. package/dist/generate/plugin-wrapper.js +20 -0
  49. package/dist/generate/sqlc.d.ts +27 -0
  50. package/dist/generate/sqlc.js +113 -0
  51. package/dist/generate/watch.d.ts +16 -0
  52. package/dist/generate/watch.js +73 -0
  53. package/dist/generator/generate.d.ts +21 -0
  54. package/dist/generator/generate.js +440 -0
  55. package/dist/generator/keys.d.ts +4 -0
  56. package/dist/generator/keys.js +24 -0
  57. package/dist/generator/lexer.d.ts +12 -0
  58. package/dist/generator/lexer.js +210 -0
  59. package/dist/generator/main.d.ts +1 -0
  60. package/dist/generator/main.js +12 -0
  61. package/dist/generator/pg-array-helper.d.ts +2 -0
  62. package/dist/generator/pg-array-helper.js +53 -0
  63. package/dist/generator/plugin.d.ts +6 -0
  64. package/dist/generator/plugin.js +26 -0
  65. package/dist/generator/request.d.ts +57 -0
  66. package/dist/generator/request.js +96 -0
  67. package/dist/generator/select-list.d.ts +27 -0
  68. package/dist/generator/select-list.js +580 -0
  69. package/dist/generator/strings.d.ts +2 -0
  70. package/dist/generator/strings.js +34 -0
  71. package/dist/generator/types.d.ts +23 -0
  72. package/dist/generator/types.js +216 -0
  73. package/dist/index.d.ts +2 -0
  74. package/dist/index.js +2 -0
  75. package/dist/migrate/adopt-prisma.d.ts +23 -0
  76. package/dist/migrate/adopt-prisma.js +119 -0
  77. package/dist/migrate/deploy.d.ts +7 -0
  78. package/dist/migrate/deploy.js +23 -0
  79. package/dist/migrate/drift.d.ts +10 -0
  80. package/dist/migrate/drift.js +36 -0
  81. package/dist/migrate/files.d.ts +19 -0
  82. package/dist/migrate/files.js +97 -0
  83. package/dist/migrate/history.d.ts +20 -0
  84. package/dist/migrate/history.js +54 -0
  85. package/dist/migrate/lint.d.ts +21 -0
  86. package/dist/migrate/lint.js +153 -0
  87. package/dist/migrate/new.d.ts +13 -0
  88. package/dist/migrate/new.js +41 -0
  89. package/dist/migrate/runner.d.ts +28 -0
  90. package/dist/migrate/runner.js +124 -0
  91. package/dist/migrate/sql-text.d.ts +33 -0
  92. package/dist/migrate/sql-text.js +233 -0
  93. package/dist/schema/schema.d.ts +16 -0
  94. package/dist/schema/schema.js +111 -0
  95. package/dist/schema/unsupported.d.ts +3 -0
  96. package/dist/schema/unsupported.js +36 -0
  97. package/dist/schema/wasm.d.ts +8 -0
  98. package/dist/schema/wasm.js +32 -0
  99. package/package.json +50 -4
  100. package/vendor/prisma-schema-wasm/LICENSE +201 -0
  101. package/vendor/prisma-schema-wasm/README.md +32 -0
  102. package/vendor/prisma-schema-wasm/package.json +3 -0
  103. package/vendor/prisma-schema-wasm/prisma_schema_build.js +539 -0
  104. package/vendor/prisma-schema-wasm/prisma_schema_build_bg.wasm +0 -0
@@ -0,0 +1,3 @@
1
+ export declare const PLATFORMS: readonly ['darwin-arm64', 'linux-x64', 'linux-arm64'];
2
+ export type Platform = (typeof PLATFORMS)[number];
3
+ export declare function currentPlatform(os?: string, arch?: string): Platform;
@@ -0,0 +1,10 @@
1
+ import { SequelError } from '../errors.js';
2
+ export const PLATFORMS = ['darwin-arm64', 'linux-x64', 'linux-arm64'];
3
+ // sequel's name for the platform it runs on. Each manifest entry maps it to the tool's own name.
4
+ export function currentPlatform(os = process.platform, arch = process.arch) {
5
+ const platform = `${os}-${arch}`;
6
+ if (!PLATFORMS.includes(platform)) {
7
+ throw new SequelError(`sequel does not support ${platform}. Its pinned tools are built for ${PLATFORMS.join(', ')}.`);
8
+ }
9
+ return platform;
10
+ }
@@ -0,0 +1,2 @@
1
+ import type { PlatformEntry } from './manifest.ts';
2
+ export declare function unpack(asset: string, entry: PlatformEntry, workDir: string): Promise<string>;
@@ -0,0 +1,35 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createReadStream, createWriteStream } from 'node:fs';
3
+ import { mkdir } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import { promisify } from 'node:util';
7
+ import { createGunzip } from 'node:zlib';
8
+ import { SequelError } from '../errors.js';
9
+ const run = promisify(execFile);
10
+ // Extracts the executable from a verified asset into `workDir`, and returns its path.
11
+ // Only call this after the asset's checksum has passed.
12
+ export async function unpack(asset, entry, workDir) {
13
+ const output = path.join(workDir, 'executable');
14
+ switch (entry.archive) {
15
+ case 'none':
16
+ return asset;
17
+ case 'gz':
18
+ await pipeline(createReadStream(asset), createGunzip(), createWriteStream(output));
19
+ return output;
20
+ case 'tar.gz': {
21
+ const member = entry.member;
22
+ const dir = path.join(workDir, 'unpacked');
23
+ await mkdir(dir);
24
+ try {
25
+ // The system tar, present on every supported platform. Only the named member is
26
+ // extracted, and both GNU tar and bsdtar refuse members that escape `dir`.
27
+ await run('tar', ['-xzf', asset, '-C', dir, member]);
28
+ }
29
+ catch (error) {
30
+ throw new SequelError(`Could not extract ${member} from ${entry.asset} with tar: ${error.message.trim()}`);
31
+ }
32
+ return path.join(dir, member);
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,2 @@
1
+ export declare function sha256File(file: string): Promise<string>;
2
+ export declare function verifySha256(file: string, expected: string, what: string): Promise<void>;
@@ -0,0 +1,16 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { SequelError } from '../errors.js';
4
+ export async function sha256File(file) {
5
+ const hash = createHash('sha256');
6
+ for await (const chunk of createReadStream(file))
7
+ hash.update(chunk);
8
+ return hash.digest('hex');
9
+ }
10
+ // Refuses a file whose sha256 isn't the one recorded in the manifest. `what` names it.
11
+ export async function verifySha256(file, expected, what) {
12
+ const actual = await sha256File(file);
13
+ if (actual !== expected) {
14
+ throw new SequelError(`${what} failed its checksum: expected sha256 ${expected}, got ${actual}. It was not used.`);
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ import { type ResolvedConfig } from './config/resolve-config.ts';
2
+ export type CheckFailure = {
3
+ problem: string;
4
+ details: string[];
5
+ fix: string;
6
+ };
7
+ export declare function runChecks(config: ResolvedConfig, log: (message: string) => void): Promise<CheckFailure[]>;
8
+ export declare function lineChanges(current: string, next: string): string[];
9
+ export declare function checkFailureMessage(failures: CheckFailure[]): string;
package/dist/check.js ADDED
@@ -0,0 +1,178 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { ensureBinary } from './binaries/ensure-binary.js';
4
+ import { requireDatabase } from './config/resolve-config.js';
5
+ import { SequelError } from './errors.js';
6
+ import { readExtrasSnapshot } from './extras/snapshot.js';
7
+ import { checkModules } from './generate/generate-modules.js';
8
+ import { findQueryFolders } from './generate/modules.js';
9
+ import { deployTo } from './migrate/deploy.js';
10
+ import { diffMigrationsToSchema } from './migrate/drift.js';
11
+ import { findMigrations } from './migrate/files.js';
12
+ import { formatFinding, lintMigrations, migrationsToLint } from './migrate/lint.js';
13
+ import { readSchemaFiles, validateSchema } from './schema/schema.js';
14
+ import { unsupportedColumns } from './schema/unsupported.js';
15
+ // SPEC §3's `check`: brings the test database up to the latest migration, then runs every check
16
+ // and returns every failure, in SPEC's order. It writes no files. A check that can't run because
17
+ // an earlier one failed (drift needs a valid schema, say) is skipped, and the earlier failure
18
+ // says why. With external migrations it checks only the generated modules (SPEC §2).
19
+ export async function runChecks(config, log) {
20
+ const testUrl = requireDatabase(config, 'test');
21
+ const failures = [];
22
+ const fail = (order, failure) => failures.push({ order, failure });
23
+ const expected = async (order, problem, fix, body) => {
24
+ try {
25
+ return await body();
26
+ }
27
+ catch (error) {
28
+ if (!(error instanceof SequelError))
29
+ throw error;
30
+ fail(order, { problem, details: [error.message], fix });
31
+ return undefined;
32
+ }
33
+ };
34
+ // With external migrations, the test database is as the other tool left it, and only the
35
+ // generated modules are checked. The skip is said, never silent.
36
+ if (config.externalMigrations) {
37
+ log(`Skipped drift, lint, the extras snapshot and schema validation: this app's migrations are managed elsewhere (\`externalMigrations\` in ${path.basename(config.configFile)}).`);
38
+ await checkGeneratedModules(config, testUrl, expected, (failure) => fail(1, failure));
39
+ return failures.map((entry) => entry.failure);
40
+ }
41
+ const shown = (file) => path.relative(config.root, file);
42
+ // The schema folder.
43
+ const files = await expected(5, "The schema folder can't be read", "Fix the config's `schema`, then run `sequel check` again.", () => readSchemaFiles(config.schema, config.root));
44
+ let schemaValid = false;
45
+ if (files !== undefined) {
46
+ const message = validateSchema(files, config.root);
47
+ if (message === undefined)
48
+ schemaValid = true;
49
+ else {
50
+ fail(5, {
51
+ problem: "The schema folder doesn't validate",
52
+ details: [message],
53
+ fix: 'Fix the schema. `sequel validate` shows the same errors.',
54
+ });
55
+ }
56
+ }
57
+ // The migrations, and the test database.
58
+ const migrations = await expected(2, "The migrations can't be read", 'Fix the files named, then run `sequel check` again.', () => findMigrations(config.migrations, { root: config.root }));
59
+ const deployed = migrations !== undefined &&
60
+ (await expected(1, "The test database can't be brought up to the latest migration", 'Fix the migration named, then run `sequel check` again.', async () => {
61
+ await deployTo(config, testUrl, { log, quietWhenCurrent: true });
62
+ return true;
63
+ })) === true;
64
+ // 1. Generated modules.
65
+ if (deployed)
66
+ await checkGeneratedModules(config, testUrl, expected, (failure) => fail(1, failure));
67
+ // 2. Drift.
68
+ if (migrations !== undefined && files !== undefined && schemaValid) {
69
+ const diff = await expected(2, "Drift between the migrations and the schema folder can't be checked", 'Fix the problem named, then run `sequel check` again.', () => diffMigrationsToSchema({ config, migrations, files }));
70
+ if (diff?.changed) {
71
+ fail(2, {
72
+ problem: 'The migrations and the schema folder differ. Replayed, the migrations need this to match the schema',
73
+ details: [diff.sql.trimEnd()],
74
+ fix: 'Run `sequel migrate new <name>` to write a migration for the difference, or change the schema to match the migrations.',
75
+ });
76
+ }
77
+ }
78
+ // 3. Lint after the cut-over.
79
+ if (migrations !== undefined) {
80
+ const findings = await expected(3, "The migrations can't be linted", 'Fix the problem named, then run `sequel check` again.', async () => lintMigrations({
81
+ squawk: await ensureBinary('squawk'),
82
+ migrations: migrationsToLint(migrations, config.lint.cutover),
83
+ root: config.root,
84
+ ...(files === undefined ? {} : { unsupported: unsupportedColumns(files) }),
85
+ }));
86
+ if (findings !== undefined && findings.length > 0) {
87
+ fail(3, {
88
+ problem: `Migrations after the lint cut-over have ${findings.length} lint finding${findings.length === 1 ? '' : 's'}`,
89
+ details: findings.map(formatFinding),
90
+ fix: 'Fix each, or suppress a Squawk finding with `-- squawk-ignore <rule> -- <reason>`. `sequel migrate lint` lists them.',
91
+ });
92
+ }
93
+ }
94
+ // 4. The extras snapshot.
95
+ if (deployed) {
96
+ const next = await readExtrasSnapshot(testUrl);
97
+ const current = await readFile(config.snapshot, 'utf8').catch(() => undefined);
98
+ if (current !== next) {
99
+ fail(4, {
100
+ problem: current === undefined
101
+ ? `The extras snapshot ${shown(config.snapshot)} is missing`
102
+ : `The extras snapshot ${shown(config.snapshot)} is stale`,
103
+ details: current === undefined ? [] : lineChanges(current, next),
104
+ fix: 'Run `sequel extras`, and commit the snapshot.',
105
+ });
106
+ }
107
+ }
108
+ return failures.sort((a, b) => a.order - b.order).map((entry) => entry.failure);
109
+ }
110
+ // Generates the modules as `generate` would, against the test database as it stands, and
111
+ // compares them with the modules in place: stale, missing or orphaned.
112
+ async function checkGeneratedModules(config, testUrl, expected, fail) {
113
+ const shown = (file) => path.relative(config.root, file);
114
+ const modules = await expected(1, "The query modules can't be generated", 'Fix the queries named. `sequel generate` reports the same errors.', async () => checkModules({
115
+ sqlc: await ensureBinary('sqlc'),
116
+ databaseUrl: testUrl,
117
+ folders: await findQueryFolders(config.queries),
118
+ bigint: config.types.bigint,
119
+ root: config.root,
120
+ }));
121
+ if (modules === undefined)
122
+ return;
123
+ const details = [
124
+ ...modules.stale.map((file) => `stale: ${shown(file)}`),
125
+ ...modules.missing.map((file) => `missing: ${shown(file)}`),
126
+ ...modules.orphaned.map((file) => `orphaned: ${shown(file)} (its query file is gone)`),
127
+ ];
128
+ if (details.length > 0) {
129
+ fail({
130
+ problem: 'Generated modules are out of date',
131
+ details,
132
+ fix: 'Run `sequel generate`, and commit the result.',
133
+ });
134
+ }
135
+ }
136
+ // The lines that differ, from a longest-common-subsequence diff: `-` for the snapshot as
137
+ // committed, `+` for the database. Snapshots are small, so the quadratic table is fine.
138
+ export function lineChanges(current, next) {
139
+ const a = current.split('\n');
140
+ const b = next.split('\n');
141
+ const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
142
+ for (let i = a.length - 1; i >= 0; i--) {
143
+ for (let j = b.length - 1; j >= 0; j--) {
144
+ const row = lcs[i];
145
+ row[j] =
146
+ a[i] === b[j]
147
+ ? lcs[i + 1][j + 1] + 1
148
+ : Math.max(lcs[i + 1][j], row[j + 1]);
149
+ }
150
+ }
151
+ const changes = [];
152
+ let i = 0;
153
+ let j = 0;
154
+ while (i < a.length || j < b.length) {
155
+ if (i < a.length && j < b.length && a[i] === b[j]) {
156
+ i++;
157
+ j++;
158
+ }
159
+ else if (i < a.length &&
160
+ (j === b.length ||
161
+ lcs[i + 1][j] >= lcs[i][j + 1])) {
162
+ changes.push(`- ${a[i++]}`);
163
+ }
164
+ else {
165
+ changes.push(`+ ${b[j++]}`);
166
+ }
167
+ }
168
+ return changes;
169
+ }
170
+ export function checkFailureMessage(failures) {
171
+ const blocks = failures.map((failure, i) => {
172
+ const details = failure.details
173
+ .flatMap((detail) => detail.split('\n'))
174
+ .map((line) => ` ${line}`);
175
+ return [`${i + 1}. ${failure.problem}:`, ...details, ` Fix: ${failure.fix}`].join('\n');
176
+ });
177
+ return `sequel check found ${failures.length} problem${failures.length === 1 ? '' : 's'}:\n\n${blocks.join('\n\n')}`;
178
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, readFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { parseArgs } from 'node:util';
6
+ import { ensureBinary } from './binaries/ensure-binary.js';
7
+ import { checkFailureMessage, runChecks } from './check.js';
8
+ import { loadConfig } from './config/load-config.js';
9
+ import { requireDatabase } from './config/resolve-config.js';
10
+ import { SequelError } from './errors.js';
11
+ import { readExtrasSnapshot } from './extras/snapshot.js';
12
+ import { writeAtomically } from './files.js';
13
+ import { generateModules } from './generate/generate-modules.js';
14
+ import { findQueryFolders } from './generate/modules.js';
15
+ import { watchAndRerun } from './generate/watch.js';
16
+ import { databaseName } from './db/connection.js';
17
+ import { adoptPrismaFiles, adoptPrismaHistory, PRISMA_LOCK_FILE } from './migrate/adopt-prisma.js';
18
+ import { deployTo } from './migrate/deploy.js';
19
+ import { findMigrations, readMigrationFile } from './migrate/files.js';
20
+ import { lintFailureMessage, lintMigrations, migrationsToLint } from './migrate/lint.js';
21
+ import { newMigration } from './migrate/new.js';
22
+ import { assertValidSchema, formatSchemaFiles, readSchemaFiles, schemaWarnings, } from './schema/schema.js';
23
+ import { unsupportedColumns } from './schema/unsupported.js';
24
+ const OPTIONS = {
25
+ help: { type: 'boolean', short: 'h' },
26
+ config: { type: 'string', short: 'c' },
27
+ watch: { type: 'boolean' },
28
+ 'allow-out-of-order': { type: 'boolean' },
29
+ };
30
+ async function validate(context) {
31
+ const config = await loadManaged(context);
32
+ const files = await readSchemaFiles(config.schema, config.root);
33
+ assertValidSchema(files, config.root);
34
+ for (const warning of schemaWarnings(files, config.root)) {
35
+ out(`${warning.shown}:${warning.line}: warning: ${warning.message}`);
36
+ }
37
+ out(`The schema is valid: ${plural(files.length, 'file')} in ${shown(config, config.schema)}.`);
38
+ }
39
+ async function format(context) {
40
+ const config = await loadManaged(context);
41
+ const changed = await formatSchemaFiles(await readSchemaFiles(config.schema, config.root), config.root);
42
+ for (const file of changed)
43
+ out(`Formatted ${shown(config, file)}`);
44
+ if (changed.length === 0)
45
+ out(`The schema is already formatted.`);
46
+ }
47
+ async function migrateNew(context) {
48
+ const config = await loadManaged(context);
49
+ const { file, findings } = await newMigration({ config, name: context.positionals[0] });
50
+ if (file === undefined) {
51
+ out('The migrations already match the schema: no migration written.');
52
+ return;
53
+ }
54
+ out(`Wrote ${shown(config, file)}`);
55
+ if (findings.length > 0) {
56
+ throw new SequelError(`${lintFailureMessage(findings)}\n${shown(config, file)} stays written: fix it before committing.`);
57
+ }
58
+ out('Lint: no findings.');
59
+ }
60
+ async function migrateLint(context) {
61
+ const config = await loadManaged(context);
62
+ const migrations = context.positionals.length > 0
63
+ ? await Promise.all(context.positionals.map(async (given) => {
64
+ const file = path.resolve(given);
65
+ if (!existsSync(file))
66
+ throw new SequelError(`${given} does not exist.`);
67
+ return readMigrationFile(file, config.root);
68
+ }))
69
+ : migrationsToLint(await findMigrations(config.migrations, { root: config.root }), config.lint.cutover);
70
+ // The index rule reads the schema folder; without one, it is skipped.
71
+ const schema = await readSchemaFiles(config.schema, config.root).catch((error) => {
72
+ if (error instanceof SequelError)
73
+ return undefined;
74
+ throw error;
75
+ });
76
+ const findings = await lintMigrations({
77
+ squawk: await ensureBinary('squawk'),
78
+ migrations,
79
+ root: config.root,
80
+ ...(schema === undefined ? {} : { unsupported: unsupportedColumns(schema) }),
81
+ });
82
+ if (findings.length > 0)
83
+ throw new SequelError(lintFailureMessage(findings));
84
+ out(`${plural(migrations.length, 'migration file')} linted: no findings.`);
85
+ }
86
+ async function migrateAdoptPrisma(context) {
87
+ const config = await loadManaged(context);
88
+ const url = requireDatabase(config, 'target');
89
+ const files = await adoptPrismaFiles(config.migrations, config.root);
90
+ for (const { from, to } of files.renamed) {
91
+ out(`Renamed ${shown(config, from)} to ${shown(config, to)}`);
92
+ }
93
+ if (files.lockRemoved) {
94
+ out(`Removed ${shown(config, path.join(config.migrations, PRISMA_LOCK_FILE))}`);
95
+ }
96
+ if (files.renamed.length > 0 || files.lockRemoved) {
97
+ out('Commit the renamed files. Their contents are unchanged, so their checksums still match.');
98
+ }
99
+ else {
100
+ out("Files: none in Prisma's layout, so nothing to rename.");
101
+ }
102
+ const migrations = await findMigrations(config.migrations, { root: config.root });
103
+ const history = await adoptPrismaHistory({ url, migrations });
104
+ const database = databaseName(url);
105
+ if (history.status === 'adopted') {
106
+ out(`History: created sequel_migrations on ${database} from ${plural(history.rows, 'applied Prisma migration')}. _prisma_migrations is left as it was.`);
107
+ }
108
+ else if (history.status === 'already-adopted') {
109
+ out(`History: ${database} already has sequel_migrations, so nothing to adopt.`);
110
+ }
111
+ else {
112
+ out(`History: ${database} has no Prisma history (_prisma_migrations), so nothing to adopt. \`sequel migrate deploy\` creates sequel's history.`);
113
+ }
114
+ }
115
+ async function migrateDeploy(context) {
116
+ const config = await loadManaged(context);
117
+ await deployTo(config, requireDatabase(config, 'target'), {
118
+ allowOutOfOrder: context.flags['allow-out-of-order'],
119
+ log: out,
120
+ });
121
+ }
122
+ async function generate(context) {
123
+ const config = await load(context);
124
+ const url = requireDatabase(config, 'test');
125
+ const once = async () => {
126
+ // With external migrations, the test database is typed as the other tool left it.
127
+ if (!config.externalMigrations) {
128
+ await deployTo(config, url, { log: out, quietWhenCurrent: true });
129
+ }
130
+ const folders = await findQueryFolders(config.queries);
131
+ const result = await generateModules({
132
+ sqlc: await ensureBinary('sqlc'),
133
+ databaseUrl: url,
134
+ folders,
135
+ bigint: config.types.bigint,
136
+ root: config.root,
137
+ });
138
+ for (const file of result.written)
139
+ out(`Wrote ${shown(config, file)}`);
140
+ for (const file of result.deleted)
141
+ out(`Deleted ${shown(config, file)}: its query file is gone`);
142
+ if (result.written.length === 0 && result.deleted.length === 0) {
143
+ out(`The generated modules are up to date (${plural(result.unchanged.length, 'module')}).`);
144
+ }
145
+ };
146
+ if (!context.flags.watch) {
147
+ await once();
148
+ return;
149
+ }
150
+ const stop = new AbortController();
151
+ process.once('SIGINT', () => stop.abort());
152
+ process.once('SIGTERM', () => stop.abort());
153
+ const folders = await findQueryFolders(config.queries);
154
+ if (!config.externalMigrations)
155
+ folders.push(config.migrations);
156
+ await watchAndRerun({
157
+ folders,
158
+ run: once,
159
+ onError: (error) => process.stderr.write(`${describeError(error)}\n`),
160
+ signal: stop.signal,
161
+ });
162
+ }
163
+ async function extras(context) {
164
+ const config = await loadManaged(context);
165
+ const url = requireDatabase(config, 'test');
166
+ await deployTo(config, url, { log: out, quietWhenCurrent: true });
167
+ const next = await readExtrasSnapshot(url);
168
+ const current = await readFile(config.snapshot, 'utf8').catch(() => undefined);
169
+ if (current === next) {
170
+ out(`The extras snapshot is up to date: ${shown(config, config.snapshot)}`);
171
+ return;
172
+ }
173
+ await mkdir(path.dirname(config.snapshot), { recursive: true });
174
+ await writeAtomically(config.snapshot, next);
175
+ out(`Wrote ${shown(config, config.snapshot)}`);
176
+ }
177
+ async function check(context) {
178
+ const config = await load(context);
179
+ const failures = await runChecks(config, out);
180
+ if (failures.length > 0)
181
+ throw new SequelError(checkFailureMessage(failures));
182
+ out('All checks passed.');
183
+ }
184
+ function load(context) {
185
+ return loadConfig({ config: context.config });
186
+ }
187
+ // For the commands that act on the schema, the migrations or the extras snapshot: they refuse
188
+ // when another tool manages them, before connecting, reading a file or fetching a binary.
189
+ async function loadManaged(context) {
190
+ const config = await load(context);
191
+ if (config.externalMigrations) {
192
+ throw new SequelError(`\`sequel ${context.name}\` is off: ${path.basename(config.configFile)} sets \`externalMigrations\`, so another tool manages this app's schema and migrations, and sequel only types its queries. To have sequel manage them, remove \`externalMigrations\`.`);
193
+ }
194
+ return config;
195
+ }
196
+ function shown(config, file) {
197
+ return path.relative(config.root, file) || '.';
198
+ }
199
+ function plural(count, noun) {
200
+ return `${count} ${noun}${count === 1 ? '' : 's'}`;
201
+ }
202
+ function out(message) {
203
+ process.stdout.write(`${message}\n`);
204
+ }
205
+ const COMMANDS = {
206
+ validate: {
207
+ usage: 'validate',
208
+ summary: 'Check the schema folder, printing each error with its file and line.',
209
+ flags: [],
210
+ args: { min: 0, max: 0 },
211
+ run: validate,
212
+ },
213
+ format: {
214
+ usage: 'format',
215
+ summary: 'Format every file in the schema folder in place.',
216
+ flags: [],
217
+ args: { min: 0, max: 0 },
218
+ run: format,
219
+ },
220
+ 'migrate new': {
221
+ usage: 'migrate new <name>',
222
+ summary: 'Diff the migration history against the schema and write a new migration.',
223
+ flags: [],
224
+ args: { min: 1, max: 1 },
225
+ run: migrateNew,
226
+ },
227
+ 'migrate lint': {
228
+ usage: 'migrate lint [file…]',
229
+ summary: 'Lint the files given, or every migration after the lint cut-over.',
230
+ flags: [],
231
+ args: { min: 0, max: Infinity },
232
+ run: migrateLint,
233
+ },
234
+ 'migrate adopt-prisma': {
235
+ usage: 'migrate adopt-prisma',
236
+ summary: "Move from Prisma's migration files and history to sequel's.",
237
+ flags: [],
238
+ args: { min: 0, max: 0 },
239
+ run: migrateAdoptPrisma,
240
+ },
241
+ 'migrate deploy': {
242
+ usage: 'migrate deploy [--allow-out-of-order]',
243
+ summary: 'Apply pending migrations to the target database.',
244
+ flags: ['allow-out-of-order'],
245
+ args: { min: 0, max: 0 },
246
+ run: migrateDeploy,
247
+ },
248
+ generate: {
249
+ usage: 'generate [--watch]',
250
+ summary: 'Migrate the test database, then write a typed module beside each query file.',
251
+ flags: ['watch'],
252
+ args: { min: 0, max: 0 },
253
+ run: generate,
254
+ },
255
+ extras: {
256
+ usage: 'extras',
257
+ summary: 'Migrate the test database, then rewrite the extras snapshot.',
258
+ flags: [],
259
+ args: { min: 0, max: 0 },
260
+ run: extras,
261
+ },
262
+ check: {
263
+ usage: 'check',
264
+ summary: 'Fail on stale generated code, drift, lint findings or a stale snapshot.',
265
+ flags: [],
266
+ args: { min: 0, max: 0 },
267
+ run: check,
268
+ },
269
+ };
270
+ function help() {
271
+ const width = Math.max(...Object.values(COMMANDS).map((command) => command.usage.length));
272
+ const lines = Object.values(COMMANDS).map((command) => ` ${command.usage.padEnd(width)} ${command.summary}`);
273
+ return [
274
+ 'Usage: sequel <command> [options]',
275
+ '',
276
+ 'Commands:',
277
+ ...lines,
278
+ '',
279
+ 'Options:',
280
+ ` ${'-c, --config <file>'.padEnd(width)} The config file. Default: sequel.config.ts`,
281
+ ` ${'-h, --help'.padEnd(width)} Show this help.`,
282
+ '',
283
+ ].join('\n');
284
+ }
285
+ async function main(argv) {
286
+ let parsed;
287
+ try {
288
+ parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
289
+ }
290
+ catch (error) {
291
+ // parseArgs reports unknown and malformed options as TypeErrors with a clear message.
292
+ throw new SequelError(`${error.message}\nRun \`sequel --help\` for usage.`);
293
+ }
294
+ const { values, positionals } = parsed;
295
+ const [first, second] = positionals;
296
+ const name = first === 'migrate' && second !== undefined ? `migrate ${second}` : first;
297
+ if (values.help || name === undefined) {
298
+ process.stdout.write(help());
299
+ return;
300
+ }
301
+ const command = COMMANDS[name];
302
+ if (command === undefined) {
303
+ const hint = first === 'migrate'
304
+ ? 'migrate needs a subcommand: new, lint, adopt-prisma or deploy.'
305
+ : `Unknown command \`${first}\`.`;
306
+ throw new SequelError(`${hint}\nRun \`sequel --help\` for usage.`);
307
+ }
308
+ for (const flag of ['watch', 'allow-out-of-order']) {
309
+ if (values[flag] && !command.flags.includes(flag)) {
310
+ throw new SequelError(`\`sequel ${name}\` has no --${flag} option. Usage: sequel ${command.usage}`);
311
+ }
312
+ }
313
+ const args = positionals.slice(name.split(' ').length);
314
+ if (args.length < command.args.min || args.length > command.args.max) {
315
+ throw new SequelError(`Usage: sequel ${command.usage}`);
316
+ }
317
+ await command.run({
318
+ name,
319
+ config: values.config,
320
+ positionals: args,
321
+ flags: {
322
+ watch: values.watch ?? false,
323
+ 'allow-out-of-order': values['allow-out-of-order'] ?? false,
324
+ },
325
+ });
326
+ }
327
+ // An expected failure is its message alone; anything else keeps its stack.
328
+ function describeError(error) {
329
+ if (error instanceof SequelError)
330
+ return error.message;
331
+ return error instanceof Error ? (error.stack ?? error.message) : String(error);
332
+ }
333
+ try {
334
+ await main(process.argv.slice(2));
335
+ }
336
+ catch (error) {
337
+ process.stderr.write(`${describeError(error)}\n`);
338
+ process.exitCode = 1;
339
+ }
@@ -0,0 +1,24 @@
1
+ export type SequelConfig = {
2
+ schema?: string;
3
+ migrations?: string;
4
+ queries?: string | string[];
5
+ snapshot?: string;
6
+ databases?: {
7
+ target?: string | undefined;
8
+ test?: string | undefined;
9
+ shadow?: string | undefined;
10
+ };
11
+ timeouts?: {
12
+ lockMs?: number;
13
+ statementMs?: number;
14
+ };
15
+ lint?: {
16
+ cutover?: string;
17
+ };
18
+ types?: {
19
+ bigint?: BigintMode;
20
+ };
21
+ externalMigrations?: boolean;
22
+ };
23
+ export type BigintMode = 'string' | 'number' | 'bigint';
24
+ export declare function defineConfig(config: SequelConfig): SequelConfig;
@@ -0,0 +1,4 @@
1
+ // An identity function that exists for its types: `export default defineConfig({ … })`.
2
+ export function defineConfig(config) {
3
+ return config;
4
+ }
@@ -0,0 +1,6 @@
1
+ import { type ResolvedConfig } from './resolve-config.ts';
2
+ export declare const CONFIG_FILE = "sequel.config.ts";
3
+ export declare function loadConfig(options?: {
4
+ config?: string;
5
+ cwd?: string;
6
+ }): Promise<ResolvedConfig>;