@bockit/sequel 0.0.0-stage → 0.1.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.
- package/LICENSE +202 -0
- package/README.md +191 -2
- package/THIRD_PARTY_NOTICES.md +268 -0
- package/binaries.json +106 -0
- package/dist/binaries/cache.d.ts +6 -0
- package/dist/binaries/cache.js +36 -0
- package/dist/binaries/ensure-binary.d.ts +10 -0
- package/dist/binaries/ensure-binary.js +85 -0
- package/dist/binaries/manifest.d.ts +30 -0
- package/dist/binaries/manifest.js +112 -0
- package/dist/binaries/platform.d.ts +3 -0
- package/dist/binaries/platform.js +10 -0
- package/dist/binaries/unpack.d.ts +2 -0
- package/dist/binaries/unpack.js +35 -0
- package/dist/binaries/verify.d.ts +2 -0
- package/dist/binaries/verify.js +16 -0
- package/dist/check.d.ts +9 -0
- package/dist/check.js +166 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +324 -0
- package/dist/config/define-config.d.ts +23 -0
- package/dist/config/define-config.js +4 -0
- package/dist/config/load-config.d.ts +6 -0
- package/dist/config/load-config.js +34 -0
- package/dist/config/resolve-config.d.ts +34 -0
- package/dist/config/resolve-config.js +144 -0
- package/dist/db/connection.d.ts +6 -0
- package/dist/db/connection.js +38 -0
- package/dist/db/shadow.d.ts +11 -0
- package/dist/db/shadow.js +68 -0
- package/dist/engine/client.d.ts +24 -0
- package/dist/engine/client.js +123 -0
- package/dist/engine/diff.d.ts +12 -0
- package/dist/engine/diff.js +51 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +5 -0
- package/dist/extras/snapshot.d.ts +3 -0
- package/dist/extras/snapshot.js +99 -0
- package/dist/files.d.ts +1 -0
- package/dist/files.js +15 -0
- package/dist/generate/generate-modules.d.ts +17 -0
- package/dist/generate/generate-modules.js +97 -0
- package/dist/generate/modules.d.ts +16 -0
- package/dist/generate/modules.js +118 -0
- package/dist/generate/pg-dump.d.ts +8 -0
- package/dist/generate/pg-dump.js +66 -0
- package/dist/generate/plugin-wrapper.d.ts +3 -0
- package/dist/generate/plugin-wrapper.js +20 -0
- package/dist/generate/sqlc.d.ts +27 -0
- package/dist/generate/sqlc.js +113 -0
- package/dist/generate/watch.d.ts +16 -0
- package/dist/generate/watch.js +73 -0
- package/dist/generator/generate.d.ts +21 -0
- package/dist/generator/generate.js +440 -0
- package/dist/generator/keys.d.ts +4 -0
- package/dist/generator/keys.js +24 -0
- package/dist/generator/lexer.d.ts +12 -0
- package/dist/generator/lexer.js +210 -0
- package/dist/generator/main.d.ts +1 -0
- package/dist/generator/main.js +12 -0
- package/dist/generator/pg-array-helper.d.ts +2 -0
- package/dist/generator/pg-array-helper.js +53 -0
- package/dist/generator/plugin.d.ts +6 -0
- package/dist/generator/plugin.js +26 -0
- package/dist/generator/request.d.ts +57 -0
- package/dist/generator/request.js +96 -0
- package/dist/generator/select-list.d.ts +27 -0
- package/dist/generator/select-list.js +580 -0
- package/dist/generator/strings.d.ts +2 -0
- package/dist/generator/strings.js +34 -0
- package/dist/generator/types.d.ts +23 -0
- package/dist/generator/types.js +216 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/migrate/adopt-prisma.d.ts +23 -0
- package/dist/migrate/adopt-prisma.js +119 -0
- package/dist/migrate/deploy.d.ts +7 -0
- package/dist/migrate/deploy.js +23 -0
- package/dist/migrate/drift.d.ts +10 -0
- package/dist/migrate/drift.js +36 -0
- package/dist/migrate/files.d.ts +19 -0
- package/dist/migrate/files.js +97 -0
- package/dist/migrate/history.d.ts +20 -0
- package/dist/migrate/history.js +54 -0
- package/dist/migrate/lint.d.ts +21 -0
- package/dist/migrate/lint.js +153 -0
- package/dist/migrate/new.d.ts +13 -0
- package/dist/migrate/new.js +41 -0
- package/dist/migrate/runner.d.ts +28 -0
- package/dist/migrate/runner.js +124 -0
- package/dist/migrate/sql-text.d.ts +33 -0
- package/dist/migrate/sql-text.js +233 -0
- package/dist/schema/schema.d.ts +16 -0
- package/dist/schema/schema.js +111 -0
- package/dist/schema/unsupported.d.ts +3 -0
- package/dist/schema/unsupported.js +36 -0
- package/dist/schema/wasm.d.ts +8 -0
- package/dist/schema/wasm.js +32 -0
- package/package.json +50 -4
- package/vendor/prisma-schema-wasm/LICENSE +201 -0
- package/vendor/prisma-schema-wasm/README.md +32 -0
- package/vendor/prisma-schema-wasm/package.json +3 -0
- package/vendor/prisma-schema-wasm/prisma_schema_build.js +539 -0
- package/vendor/prisma-schema-wasm/prisma_schema_build_bg.wasm +0 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
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 load(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 load(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 load(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 load(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 load(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 load(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
|
+
await deployTo(config, url, { log: out, quietWhenCurrent: true });
|
|
127
|
+
const folders = await findQueryFolders(config.queries);
|
|
128
|
+
const result = await generateModules({
|
|
129
|
+
sqlc: await ensureBinary('sqlc'),
|
|
130
|
+
databaseUrl: url,
|
|
131
|
+
folders,
|
|
132
|
+
bigint: config.types.bigint,
|
|
133
|
+
root: config.root,
|
|
134
|
+
});
|
|
135
|
+
for (const file of result.written)
|
|
136
|
+
out(`Wrote ${shown(config, file)}`);
|
|
137
|
+
for (const file of result.deleted)
|
|
138
|
+
out(`Deleted ${shown(config, file)}: its query file is gone`);
|
|
139
|
+
if (result.written.length === 0 && result.deleted.length === 0) {
|
|
140
|
+
out(`The generated modules are up to date (${plural(result.unchanged.length, 'module')}).`);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
if (!context.flags.watch) {
|
|
144
|
+
await once();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const stop = new AbortController();
|
|
148
|
+
process.once('SIGINT', () => stop.abort());
|
|
149
|
+
process.once('SIGTERM', () => stop.abort());
|
|
150
|
+
const folders = [...(await findQueryFolders(config.queries)), config.migrations];
|
|
151
|
+
await watchAndRerun({
|
|
152
|
+
folders,
|
|
153
|
+
run: once,
|
|
154
|
+
onError: (error) => process.stderr.write(`${describeError(error)}\n`),
|
|
155
|
+
signal: stop.signal,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async function extras(context) {
|
|
159
|
+
const config = await load(context);
|
|
160
|
+
const url = requireDatabase(config, 'test');
|
|
161
|
+
await deployTo(config, url, { log: out, quietWhenCurrent: true });
|
|
162
|
+
const next = await readExtrasSnapshot(url);
|
|
163
|
+
const current = await readFile(config.snapshot, 'utf8').catch(() => undefined);
|
|
164
|
+
if (current === next) {
|
|
165
|
+
out(`The extras snapshot is up to date: ${shown(config, config.snapshot)}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
await mkdir(path.dirname(config.snapshot), { recursive: true });
|
|
169
|
+
await writeAtomically(config.snapshot, next);
|
|
170
|
+
out(`Wrote ${shown(config, config.snapshot)}`);
|
|
171
|
+
}
|
|
172
|
+
async function check(context) {
|
|
173
|
+
const config = await load(context);
|
|
174
|
+
const failures = await runChecks(config, out);
|
|
175
|
+
if (failures.length > 0)
|
|
176
|
+
throw new SequelError(checkFailureMessage(failures));
|
|
177
|
+
out('All checks passed.');
|
|
178
|
+
}
|
|
179
|
+
function load(context) {
|
|
180
|
+
return loadConfig({ config: context.config });
|
|
181
|
+
}
|
|
182
|
+
function shown(config, file) {
|
|
183
|
+
return path.relative(config.root, file) || '.';
|
|
184
|
+
}
|
|
185
|
+
function plural(count, noun) {
|
|
186
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
187
|
+
}
|
|
188
|
+
function out(message) {
|
|
189
|
+
process.stdout.write(`${message}\n`);
|
|
190
|
+
}
|
|
191
|
+
const COMMANDS = {
|
|
192
|
+
validate: {
|
|
193
|
+
usage: 'validate',
|
|
194
|
+
summary: 'Check the schema folder, printing each error with its file and line.',
|
|
195
|
+
flags: [],
|
|
196
|
+
args: { min: 0, max: 0 },
|
|
197
|
+
run: validate,
|
|
198
|
+
},
|
|
199
|
+
format: {
|
|
200
|
+
usage: 'format',
|
|
201
|
+
summary: 'Format every file in the schema folder in place.',
|
|
202
|
+
flags: [],
|
|
203
|
+
args: { min: 0, max: 0 },
|
|
204
|
+
run: format,
|
|
205
|
+
},
|
|
206
|
+
'migrate new': {
|
|
207
|
+
usage: 'migrate new <name>',
|
|
208
|
+
summary: 'Diff the migration history against the schema and write a new migration.',
|
|
209
|
+
flags: [],
|
|
210
|
+
args: { min: 1, max: 1 },
|
|
211
|
+
run: migrateNew,
|
|
212
|
+
},
|
|
213
|
+
'migrate lint': {
|
|
214
|
+
usage: 'migrate lint [file…]',
|
|
215
|
+
summary: 'Lint the files given, or every migration after the lint cut-over.',
|
|
216
|
+
flags: [],
|
|
217
|
+
args: { min: 0, max: Infinity },
|
|
218
|
+
run: migrateLint,
|
|
219
|
+
},
|
|
220
|
+
'migrate adopt-prisma': {
|
|
221
|
+
usage: 'migrate adopt-prisma',
|
|
222
|
+
summary: "Move from Prisma's migration files and history to sequel's.",
|
|
223
|
+
flags: [],
|
|
224
|
+
args: { min: 0, max: 0 },
|
|
225
|
+
run: migrateAdoptPrisma,
|
|
226
|
+
},
|
|
227
|
+
'migrate deploy': {
|
|
228
|
+
usage: 'migrate deploy [--allow-out-of-order]',
|
|
229
|
+
summary: 'Apply pending migrations to the target database.',
|
|
230
|
+
flags: ['allow-out-of-order'],
|
|
231
|
+
args: { min: 0, max: 0 },
|
|
232
|
+
run: migrateDeploy,
|
|
233
|
+
},
|
|
234
|
+
generate: {
|
|
235
|
+
usage: 'generate [--watch]',
|
|
236
|
+
summary: 'Migrate the test database, then write a typed module beside each query file.',
|
|
237
|
+
flags: ['watch'],
|
|
238
|
+
args: { min: 0, max: 0 },
|
|
239
|
+
run: generate,
|
|
240
|
+
},
|
|
241
|
+
extras: {
|
|
242
|
+
usage: 'extras',
|
|
243
|
+
summary: 'Migrate the test database, then rewrite the extras snapshot.',
|
|
244
|
+
flags: [],
|
|
245
|
+
args: { min: 0, max: 0 },
|
|
246
|
+
run: extras,
|
|
247
|
+
},
|
|
248
|
+
check: {
|
|
249
|
+
usage: 'check',
|
|
250
|
+
summary: 'Fail on stale generated code, drift, lint findings or a stale snapshot.',
|
|
251
|
+
flags: [],
|
|
252
|
+
args: { min: 0, max: 0 },
|
|
253
|
+
run: check,
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
function help() {
|
|
257
|
+
const width = Math.max(...Object.values(COMMANDS).map((command) => command.usage.length));
|
|
258
|
+
const lines = Object.values(COMMANDS).map((command) => ` ${command.usage.padEnd(width)} ${command.summary}`);
|
|
259
|
+
return [
|
|
260
|
+
'Usage: sequel <command> [options]',
|
|
261
|
+
'',
|
|
262
|
+
'Commands:',
|
|
263
|
+
...lines,
|
|
264
|
+
'',
|
|
265
|
+
'Options:',
|
|
266
|
+
` ${'-c, --config <file>'.padEnd(width)} The config file. Default: sequel.config.ts`,
|
|
267
|
+
` ${'-h, --help'.padEnd(width)} Show this help.`,
|
|
268
|
+
'',
|
|
269
|
+
].join('\n');
|
|
270
|
+
}
|
|
271
|
+
async function main(argv) {
|
|
272
|
+
let parsed;
|
|
273
|
+
try {
|
|
274
|
+
parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
// parseArgs reports unknown and malformed options as TypeErrors with a clear message.
|
|
278
|
+
throw new SequelError(`${error.message}\nRun \`sequel --help\` for usage.`);
|
|
279
|
+
}
|
|
280
|
+
const { values, positionals } = parsed;
|
|
281
|
+
const [first, second] = positionals;
|
|
282
|
+
const name = first === 'migrate' && second !== undefined ? `migrate ${second}` : first;
|
|
283
|
+
if (values.help || name === undefined) {
|
|
284
|
+
process.stdout.write(help());
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const command = COMMANDS[name];
|
|
288
|
+
if (command === undefined) {
|
|
289
|
+
const hint = first === 'migrate'
|
|
290
|
+
? 'migrate needs a subcommand: new, lint, adopt-prisma or deploy.'
|
|
291
|
+
: `Unknown command \`${first}\`.`;
|
|
292
|
+
throw new SequelError(`${hint}\nRun \`sequel --help\` for usage.`);
|
|
293
|
+
}
|
|
294
|
+
for (const flag of ['watch', 'allow-out-of-order']) {
|
|
295
|
+
if (values[flag] && !command.flags.includes(flag)) {
|
|
296
|
+
throw new SequelError(`\`sequel ${name}\` has no --${flag} option. Usage: sequel ${command.usage}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const args = positionals.slice(name.split(' ').length);
|
|
300
|
+
if (args.length < command.args.min || args.length > command.args.max) {
|
|
301
|
+
throw new SequelError(`Usage: sequel ${command.usage}`);
|
|
302
|
+
}
|
|
303
|
+
await command.run({
|
|
304
|
+
config: values.config,
|
|
305
|
+
positionals: args,
|
|
306
|
+
flags: {
|
|
307
|
+
watch: values.watch ?? false,
|
|
308
|
+
'allow-out-of-order': values['allow-out-of-order'] ?? false,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
// An expected failure is its message alone; anything else keeps its stack.
|
|
313
|
+
function describeError(error) {
|
|
314
|
+
if (error instanceof SequelError)
|
|
315
|
+
return error.message;
|
|
316
|
+
return error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
await main(process.argv.slice(2));
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
process.stderr.write(`${describeError(error)}\n`);
|
|
323
|
+
process.exitCode = 1;
|
|
324
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
};
|
|
22
|
+
export type BigintMode = 'string' | 'number' | 'bigint';
|
|
23
|
+
export declare function defineConfig(config: SequelConfig): SequelConfig;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import { SequelError } from '../errors.js';
|
|
13
|
+
import { resolveConfig } from './resolve-config.js';
|
|
14
|
+
export const CONFIG_FILE = 'sequel.config.ts';
|
|
15
|
+
// Loads `sequel.config.ts` from `cwd`, or the file `--config` names, by dynamic import under
|
|
16
|
+
// Node's type stripping, then validates it and resolves its paths.
|
|
17
|
+
export async function loadConfig(options = {}) {
|
|
18
|
+
const cwd = options.cwd ?? process.cwd();
|
|
19
|
+
const file = path.resolve(cwd, options.config ?? CONFIG_FILE);
|
|
20
|
+
if (!existsSync(file)) {
|
|
21
|
+
throw new SequelError(options.config === undefined
|
|
22
|
+
? `No ${CONFIG_FILE} in ${cwd}. Create one that default-exports \`defineConfig({ … })\`, or pass --config <file>.`
|
|
23
|
+
: `The config file ${file}, named by --config, does not exist.`);
|
|
24
|
+
}
|
|
25
|
+
let module;
|
|
26
|
+
try {
|
|
27
|
+
module = await import(__rewriteRelativeImportExtension(pathToFileURL(file).href));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
31
|
+
throw new SequelError(`${file} failed to load: ${reason}\nIt runs under Node's type stripping, so it may use only erasable TypeScript syntax.`);
|
|
32
|
+
}
|
|
33
|
+
return resolveConfig(module.default, file);
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { BigintMode } from './define-config.ts';
|
|
2
|
+
export type DatabaseRole = 'target' | 'test' | 'shadow';
|
|
3
|
+
export type ResolvedConfig = {
|
|
4
|
+
configFile: string;
|
|
5
|
+
root: string;
|
|
6
|
+
schema: string;
|
|
7
|
+
migrations: string;
|
|
8
|
+
queries: string[];
|
|
9
|
+
snapshot: string;
|
|
10
|
+
databases: Record<DatabaseRole, string | undefined>;
|
|
11
|
+
timeouts: {
|
|
12
|
+
lockMs: number;
|
|
13
|
+
statementMs: number;
|
|
14
|
+
};
|
|
15
|
+
lint: {
|
|
16
|
+
cutover: string | undefined;
|
|
17
|
+
};
|
|
18
|
+
types: {
|
|
19
|
+
bigint: BigintMode;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export declare const DEFAULTS: {
|
|
23
|
+
readonly schema: 'prisma';
|
|
24
|
+
readonly migrations: 'prisma/migrations';
|
|
25
|
+
readonly queries: readonly ['src/**/queries'];
|
|
26
|
+
readonly snapshotFile: 'snapshot.sql';
|
|
27
|
+
readonly timeouts: {
|
|
28
|
+
readonly lockMs: 5000;
|
|
29
|
+
readonly statementMs: 60000;
|
|
30
|
+
};
|
|
31
|
+
readonly bigint: 'string';
|
|
32
|
+
};
|
|
33
|
+
export declare function resolveConfig(value: unknown, configFile: string): ResolvedConfig;
|
|
34
|
+
export declare function requireDatabase(config: ResolvedConfig, role: DatabaseRole): string;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { SequelError } from '../errors.js';
|
|
3
|
+
export const DEFAULTS = {
|
|
4
|
+
schema: 'prisma',
|
|
5
|
+
migrations: 'prisma/migrations',
|
|
6
|
+
queries: ['src/**/queries'],
|
|
7
|
+
snapshotFile: 'snapshot.sql',
|
|
8
|
+
timeouts: { lockMs: 5_000, statementMs: 60_000 },
|
|
9
|
+
bigint: 'string',
|
|
10
|
+
};
|
|
11
|
+
const KEYS = {
|
|
12
|
+
'': ['schema', 'migrations', 'queries', 'snapshot', 'databases', 'timeouts', 'lint', 'types'],
|
|
13
|
+
databases: ['target', 'test', 'shadow'],
|
|
14
|
+
timeouts: ['lockMs', 'statementMs'],
|
|
15
|
+
lint: ['cutover'],
|
|
16
|
+
types: ['bigint'],
|
|
17
|
+
};
|
|
18
|
+
const BIGINT_MODES = ['string', 'number', 'bigint'];
|
|
19
|
+
const IDENTITY = /^\d{14}_[a-z0-9_]+$/;
|
|
20
|
+
// Validates a loaded config and applies the defaults. `configFile` is absolute, and every
|
|
21
|
+
// path in the config resolves against its folder.
|
|
22
|
+
export function resolveConfig(value, configFile) {
|
|
23
|
+
const file = path.basename(configFile);
|
|
24
|
+
const fail = (key, problem) => {
|
|
25
|
+
throw new SequelError(`${file}: \`${key}\` ${problem}`);
|
|
26
|
+
};
|
|
27
|
+
const config = section(value, '', file, fail);
|
|
28
|
+
const databases = section(config.databases, 'databases', file, fail);
|
|
29
|
+
const timeouts = section(config.timeouts, 'timeouts', file, fail);
|
|
30
|
+
const lint = section(config.lint, 'lint', file, fail);
|
|
31
|
+
const types = section(config.types, 'types', file, fail);
|
|
32
|
+
const root = path.dirname(configFile);
|
|
33
|
+
const resolve = (relative) => path.resolve(root, relative);
|
|
34
|
+
const schema = resolve(pathValue(config.schema, 'schema', fail) ?? DEFAULTS.schema);
|
|
35
|
+
const migrations = resolve(pathValue(config.migrations, 'migrations', fail) ?? DEFAULTS.migrations);
|
|
36
|
+
const snapshotPath = pathValue(config.snapshot, 'snapshot', fail);
|
|
37
|
+
const snapshot = snapshotPath ? resolve(snapshotPath) : path.join(schema, DEFAULTS.snapshotFile);
|
|
38
|
+
const queryGlobs = config.queries === undefined ? DEFAULTS.queries : config.queries;
|
|
39
|
+
const queryList = typeof queryGlobs === 'string' ? [queryGlobs] : queryGlobs;
|
|
40
|
+
if (!Array.isArray(queryList) || queryList.length === 0) {
|
|
41
|
+
fail('queries', 'must be a glob or a non-empty list of globs, such as `src/**/queries`.');
|
|
42
|
+
}
|
|
43
|
+
const queries = queryList.map((glob, i) => resolve(pathValue(glob, `queries[${i}]`, fail)));
|
|
44
|
+
const resolvedDatabases = {
|
|
45
|
+
target: connection(databases.target, 'target', fail),
|
|
46
|
+
test: connection(databases.test, 'test', fail),
|
|
47
|
+
shadow: connection(databases.shadow, 'shadow', fail),
|
|
48
|
+
};
|
|
49
|
+
if (resolvedDatabases.shadow !== undefined) {
|
|
50
|
+
const name = databaseName(resolvedDatabases.shadow);
|
|
51
|
+
if (!name.endsWith('_shadow')) {
|
|
52
|
+
fail('databases.shadow', `names the database \`${name}\`, but a shadow database's name must end in \`_shadow\`, because sequel wipes it at each use. Point it at a database such as \`${name || 'app'}_shadow\`.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const cutover = lint.cutover;
|
|
56
|
+
if (cutover !== undefined && (typeof cutover !== 'string' || !IDENTITY.test(cutover))) {
|
|
57
|
+
fail('lint.cutover', 'must be a migration identity, `<YYYYMMDDHHMMSS>_<name>`: the file name of the newest migration not to lint, without `.sql`.');
|
|
58
|
+
}
|
|
59
|
+
const bigint = types.bigint ?? DEFAULTS.bigint;
|
|
60
|
+
if (!BIGINT_MODES.includes(bigint)) {
|
|
61
|
+
fail('types.bigint', `must be one of ${BIGINT_MODES.map((m) => `'${m}'`).join(', ')}.`);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
configFile,
|
|
65
|
+
root,
|
|
66
|
+
schema,
|
|
67
|
+
migrations,
|
|
68
|
+
queries,
|
|
69
|
+
snapshot,
|
|
70
|
+
databases: resolvedDatabases,
|
|
71
|
+
timeouts: {
|
|
72
|
+
lockMs: milliseconds(timeouts.lockMs, 'timeouts.lockMs', fail) ?? DEFAULTS.timeouts.lockMs,
|
|
73
|
+
statementMs: milliseconds(timeouts.statementMs, 'timeouts.statementMs', fail) ??
|
|
74
|
+
DEFAULTS.timeouts.statementMs,
|
|
75
|
+
},
|
|
76
|
+
lint: { cutover: cutover },
|
|
77
|
+
types: { bigint: bigint },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Returns the connection string for a database a command needs, or explains how to set it.
|
|
81
|
+
export function requireDatabase(config, role) {
|
|
82
|
+
const url = config.databases[role];
|
|
83
|
+
if (url === undefined) {
|
|
84
|
+
throw new SequelError(`${path.basename(config.configFile)}: \`databases.${role}\` is not set. Set the environment variable the config reads it from.`);
|
|
85
|
+
}
|
|
86
|
+
return url;
|
|
87
|
+
}
|
|
88
|
+
function section(value, key, file, fail) {
|
|
89
|
+
if (value === undefined && key !== '')
|
|
90
|
+
return {};
|
|
91
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
92
|
+
if (key === '') {
|
|
93
|
+
throw new SequelError(`${file} must default-export its config: \`export default defineConfig({ … })\`.`);
|
|
94
|
+
}
|
|
95
|
+
fail(key, 'must be an object.');
|
|
96
|
+
}
|
|
97
|
+
const record = value;
|
|
98
|
+
const known = KEYS[key] ?? [];
|
|
99
|
+
for (const name of Object.keys(record).sort()) {
|
|
100
|
+
if (!known.includes(name)) {
|
|
101
|
+
const full = key ? `${key}.${name}` : name;
|
|
102
|
+
fail(full, `is not a config key. Known keys${key ? ` of \`${key}\`` : ''}: ${known.join(', ')}.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return record;
|
|
106
|
+
}
|
|
107
|
+
function pathValue(value, key, fail) {
|
|
108
|
+
if (value === undefined)
|
|
109
|
+
return undefined;
|
|
110
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
111
|
+
fail(key, 'must be a non-empty path.');
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
function connection(value, role, fail) {
|
|
115
|
+
// An unset environment variable reads as undefined, or as '' in some shells and CI systems.
|
|
116
|
+
if (value === undefined || value === '')
|
|
117
|
+
return undefined;
|
|
118
|
+
const key = `databases.${role}`;
|
|
119
|
+
if (typeof value !== 'string')
|
|
120
|
+
fail(key, 'must be a connection string.');
|
|
121
|
+
const url = value;
|
|
122
|
+
let protocol;
|
|
123
|
+
try {
|
|
124
|
+
protocol = new URL(url).protocol;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return fail(key, 'is not a valid connection URL, such as `postgres://user@host:5432/db`.');
|
|
128
|
+
}
|
|
129
|
+
if (protocol !== 'postgres:' && protocol !== 'postgresql:') {
|
|
130
|
+
fail(key, 'must be a `postgres://` or `postgresql://` URL.');
|
|
131
|
+
}
|
|
132
|
+
return url;
|
|
133
|
+
}
|
|
134
|
+
function databaseName(url) {
|
|
135
|
+
return decodeURIComponent(new URL(url).pathname.replace(/^\//, ''));
|
|
136
|
+
}
|
|
137
|
+
function milliseconds(value, key, fail) {
|
|
138
|
+
if (value === undefined)
|
|
139
|
+
return undefined;
|
|
140
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
|
|
141
|
+
fail(key, 'must be a positive whole number of milliseconds.');
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
export declare function withClient<T>(url: string, fn: (client: pg.Client) => Promise<T>): Promise<T>;
|
|
3
|
+
export declare function databaseName(url: string): string;
|
|
4
|
+
export declare function withDatabaseName(url: string, name: string): string;
|
|
5
|
+
export declare function quoteIdent(name: string): string;
|
|
6
|
+
export declare function redactUrl(url: string): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
// Runs `fn` with one connection to `url`, and always closes it.
|
|
3
|
+
export async function withClient(url, fn) {
|
|
4
|
+
const client = new pg.Client({ connectionString: url });
|
|
5
|
+
await client.connect();
|
|
6
|
+
try {
|
|
7
|
+
return await fn(client);
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
await client.end().catch(() => { });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// The database a connection URL names, such as `app_test` in `postgres://host/app_test`.
|
|
14
|
+
export function databaseName(url) {
|
|
15
|
+
return decodeURIComponent(new URL(url).pathname.replace(/^\//, ''));
|
|
16
|
+
}
|
|
17
|
+
// The same server and credentials, connecting to another database.
|
|
18
|
+
export function withDatabaseName(url, name) {
|
|
19
|
+
const parsed = new URL(url);
|
|
20
|
+
parsed.pathname = `/${encodeURIComponent(name)}`;
|
|
21
|
+
return parsed.toString();
|
|
22
|
+
}
|
|
23
|
+
// A Postgres identifier, quoted, for the statements that can't take parameters.
|
|
24
|
+
export function quoteIdent(name) {
|
|
25
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
26
|
+
}
|
|
27
|
+
// The URL with any password replaced, for messages.
|
|
28
|
+
export function redactUrl(url) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(url);
|
|
31
|
+
if (parsed.password !== '')
|
|
32
|
+
parsed.password = '***';
|
|
33
|
+
return parsed.toString();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return '<invalid URL>';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ResolvedConfig } from '../config/resolve-config.ts';
|
|
2
|
+
export declare function assertShadowName(name: string): void;
|
|
3
|
+
export type ShadowOptions = {
|
|
4
|
+
fixedUrl: string;
|
|
5
|
+
} | {
|
|
6
|
+
testUrl: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function shadowOptions(config: ResolvedConfig): ShadowOptions;
|
|
9
|
+
export declare function withShadowDatabase<T>(options: ShadowOptions, fn: (url: string) => Promise<T>): Promise<T>;
|
|
10
|
+
export declare function dropShadowDatabase(serverUrl: string, name: string): Promise<void>;
|
|
11
|
+
export declare function wipeShadowDatabase(url: string): Promise<void>;
|