@deterministic-code/migraters 0.0.1 → 0.0.2

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 (55) hide show
  1. package/README.md +1 -0
  2. package/cli.yaml +264 -0
  3. package/dist/bin/migrate-create.js +0 -0
  4. package/dist/bin/migrate-down.js +0 -0
  5. package/dist/bin/migrate-init.js +0 -0
  6. package/dist/bin/migrate-setup.js +0 -0
  7. package/dist/bin/migrate-up.js +0 -0
  8. package/package.json +7 -6
  9. package/src/abstractions/migrater.ts +0 -13
  10. package/src/abstractions/sql-dialect-base.ts +0 -36
  11. package/src/abstractions/sql-dialect-factory.ts +0 -29
  12. package/src/abstractions/sql-dialect.ts +0 -17
  13. package/src/bin/migrate-create.ts +0 -80
  14. package/src/bin/migrate-down.ts +0 -163
  15. package/src/bin/migrate-init.ts +0 -88
  16. package/src/bin/migrate-setup.ts +0 -98
  17. package/src/bin/migrate-up.ts +0 -111
  18. package/src/cli-contract.ts +0 -25
  19. package/src/cli-spec.ts +0 -64
  20. package/src/cli.ts +0 -52
  21. package/src/create-sequence.ts +0 -17
  22. package/src/datasource-migrate.ts +0 -314
  23. package/src/dialects/mysql-dialect.test.ts +0 -93
  24. package/src/dialects/mysql-dialect.ts +0 -67
  25. package/src/dialects/oracle-dialect.test.ts +0 -118
  26. package/src/dialects/oracle-dialect.ts +0 -91
  27. package/src/dialects/postgres-dialect.test.ts +0 -80
  28. package/src/dialects/postgres-dialect.ts +0 -65
  29. package/src/dialects/sqlite-dialect.rollback.test.ts +0 -30
  30. package/src/dialects/sqlite-dialect.test.ts +0 -77
  31. package/src/dialects/sqlite-dialect.ts +0 -96
  32. package/src/dialects/sqlserver-dialect.test.ts +0 -99
  33. package/src/dialects/sqlserver-dialect.ts +0 -87
  34. package/src/generate-help.test.ts +0 -33
  35. package/src/generate-help.ts +0 -173
  36. package/src/help.test.ts +0 -48
  37. package/src/help.ts +0 -12
  38. package/src/index.ts +0 -23
  39. package/src/infrastructure/connection-resolver.ts +0 -18
  40. package/src/infrastructure/default-factory.test.ts +0 -18
  41. package/src/infrastructure/default-factory.ts +0 -17
  42. package/src/infrastructure/interop-default.test.ts +0 -9
  43. package/src/infrastructure/interop-default.ts +0 -2
  44. package/src/infrastructure/message-templates.ts +0 -30
  45. package/src/infrastructure/split-on-go.ts +0 -19
  46. package/src/infrastructure/sql-templates.ts +0 -10
  47. package/src/infrastructure/templates-root.ts +0 -13
  48. package/src/init-select.test.ts +0 -26
  49. package/src/init-select.ts +0 -73
  50. package/src/migrater.ts +0 -92
  51. package/src/migration-client.ts +0 -21
  52. package/src/path-exists.ts +0 -10
  53. package/src/setup-sql.test.ts +0 -16
  54. package/src/sql.ts +0 -17
  55. package/tsconfig.build.json +0 -19
@@ -1,80 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const pg = vi.hoisted(() => {
4
- const state = {
5
- failRollback: false,
6
- ended: false,
7
- queries: [] as { sql: string; params?: unknown[] }[],
8
- };
9
- class Client {
10
- constructor(public readonly opts: { connectionString: string }) {}
11
- async connect() {}
12
- async query(sql: string, params?: unknown[]) {
13
- state.queries.push({ sql, params });
14
- if (sql === 'ROLLBACK' && state.failRollback) throw new Error('rollback failed');
15
- return { rows: [{ n: 1 }] };
16
- }
17
- async end() {
18
- state.ended = true;
19
- }
20
- }
21
- return { Client, state };
22
- });
23
-
24
- vi.mock('pg', () => ({ default: { Client: pg.Client } }));
25
-
26
- import { PostgresDialect } from './postgres-dialect.ts';
27
-
28
- const dialect = new PostgresDialect();
29
-
30
- describe('PostgresDialect', () => {
31
- afterEach(() => {
32
- pg.state.failRollback = false;
33
- pg.state.ended = false;
34
- pg.state.queries = [];
35
- });
36
-
37
- it('registers as postgres with NOW() and $n placeholders', async () => {
38
- expect(dialect.name).toBe('postgres');
39
- expect([...dialect.connectionEnvironmentVariables]).toEqual([
40
- 'PG_CONNECTION_STRING',
41
- 'DATABASE_URL',
42
- ]);
43
- expect(dialect.usesLastInsertRowid).toBe(false);
44
- expect(dialect.nowExpr()).toBe('NOW()');
45
- expect(dialect.translatePlaceholders('WHERE a = ? AND b = ?')).toBe('WHERE a = $1 AND b = $2');
46
- expect(dialect.migratesDdl).toMatch(/migrates/i);
47
- expect(dialect.migrateLogsDdl).toMatch(/migrate_logs/i);
48
- expect(await dialect.prerequisiteError('pg://x')).toBeNull();
49
- await dialect.prepareSetup('pg://x');
50
- });
51
-
52
- it('createClient talks through pg.Client with translated placeholders', async () => {
53
- const client = await dialect.createClient('postgresql://localhost/db');
54
- expect(client.dialect).toBe('postgres');
55
- await client.exec('SELECT 1');
56
- expect(await client.query('SELECT ?', [1])).toEqual([{ n: 1 }]);
57
- expect(pg.state.queries.some((q) => q.sql === 'SELECT $1' && q.params?.[0] === 1)).toBe(true);
58
- await client.run('UPDATE t SET x = ?', [2]);
59
- expect(await client.transaction(async () => 'ok')).toBe('ok');
60
- expect(pg.state.queries.map((q) => q.sql)).toContain('BEGIN');
61
- expect(pg.state.queries.map((q) => q.sql)).toContain('COMMIT');
62
- await expect(
63
- client.transaction(async () => {
64
- throw new Error('tx');
65
- }),
66
- ).rejects.toThrow('tx');
67
- expect(pg.state.queries.map((q) => q.sql)).toContain('ROLLBACK');
68
- pg.state.failRollback = true;
69
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
70
- await expect(
71
- client.transaction(async () => {
72
- throw new Error('tx2');
73
- }),
74
- ).rejects.toThrow('tx2');
75
- expect(warn).toHaveBeenCalled();
76
- warn.mockRestore();
77
- await client.close();
78
- expect(pg.state.ended).toBe(true);
79
- });
80
- });
@@ -1,65 +0,0 @@
1
- import type { MigrationClient } from '../migration-client.ts';
2
- import { readSqlTemplate } from '../infrastructure/sql-templates.ts';
3
- import { SqlDialectBase } from '../abstractions/sql-dialect-base.ts';
4
-
5
- const [migratesDdl, migrateLogsDdl] = await Promise.all([
6
- readSqlTemplate('postgres', 'migrates'),
7
- readSqlTemplate('postgres', 'migrate_logs'),
8
- ]);
9
-
10
- export class PostgresDialect extends SqlDialectBase {
11
- readonly name = 'postgres' as const;
12
- readonly connectionEnvironmentVariables = ['PG_CONNECTION_STRING', 'DATABASE_URL'] as const;
13
- readonly migratesDdl = migratesDdl;
14
- readonly migrateLogsDdl = migrateLogsDdl;
15
-
16
- nowExpr(): string {
17
- return 'NOW()';
18
- }
19
-
20
- translatePlaceholders(sql: string): string {
21
- let n = 0;
22
- return sql.replace(/\?/g, () => `$${++n}`);
23
- }
24
-
25
- async createClient(connection: string): Promise<MigrationClient> {
26
- const { default: pg } = await import('pg');
27
- const client = new pg.Client({ connectionString: connection });
28
- await client.connect();
29
- const translate = (sql: string) => this.translatePlaceholders(sql);
30
- return {
31
- dialect: 'postgres',
32
- async exec(sql) {
33
- await client.query(sql);
34
- },
35
- async query(sql, params = []) {
36
- const r = await client.query(translate(sql), params);
37
- return r.rows;
38
- },
39
- async run(sql, params = []) {
40
- await client.query(translate(sql), params);
41
- },
42
- async transaction(fn) {
43
- await client.query('BEGIN');
44
- try {
45
- const r = await fn();
46
- await client.query('COMMIT');
47
- return r;
48
- } catch (e) {
49
- try {
50
- await client.query('ROLLBACK');
51
- } catch (rollbackErr) {
52
- console.warn(
53
- 'postgres ROLLBACK failed after transaction error; original error rethrown',
54
- rollbackErr,
55
- );
56
- }
57
- throw e;
58
- }
59
- },
60
- async close() {
61
- await client.end();
62
- },
63
- };
64
- }
65
- }
@@ -1,30 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
-
3
- vi.mock('better-sqlite3', () => ({
4
- default: class {
5
- exec(sql: string) {
6
- if (sql === 'ROLLBACK') throw new Error('rollback failed');
7
- }
8
- prepare() {
9
- return { all() { return []; }, run() {} };
10
- }
11
- close() {}
12
- },
13
- }));
14
-
15
- import { SqliteDialect } from './sqlite-dialect.ts';
16
-
17
- describe('SqliteDialect rollback failure', () => {
18
- it('warns when ROLLBACK itself fails and still rethrows the original error', async () => {
19
- const client = await new SqliteDialect().createClient(':memory:');
20
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
21
- await expect(
22
- client.transaction(async () => {
23
- throw new Error('tx');
24
- }),
25
- ).rejects.toThrow('tx');
26
- expect(warn).toHaveBeenCalled();
27
- warn.mockRestore();
28
- await client.close();
29
- });
30
- });
@@ -1,77 +0,0 @@
1
- import { mkdtemp, writeFile } from 'node:fs/promises';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { describe, expect, it } from 'vitest';
5
- import { SqliteDialect, sqliteFilesystemPath } from './sqlite-dialect.ts';
6
-
7
- const dialect = new SqliteDialect();
8
-
9
- describe('SqliteDialect', () => {
10
- it('registers as sqlite with SQLITE_PATH / DB_PATH and last_insert_rowid', () => {
11
- expect(dialect.name).toBe('sqlite');
12
- expect([...dialect.connectionEnvironmentVariables]).toEqual(['SQLITE_PATH', 'DB_PATH']);
13
- expect(dialect.usesLastInsertRowid).toBe(true);
14
- expect(dialect.migratesDdl).toMatch(/migrates/i);
15
- expect(dialect.migrateLogsDdl).toMatch(/migrate_logs/i);
16
- expect(dialect.quoteIdent('t')).toBe('"t"');
17
- expect(dialect.nowExpr()).toBe('CURRENT_TIMESTAMP');
18
- expect(dialect.limitClause(3)).toBe('LIMIT 3');
19
- expect(dialect.translatePlaceholders('? ?')).toBe('? ?');
20
- });
21
-
22
- it('parses filesystem paths from every supported connection form', () => {
23
- expect(sqliteFilesystemPath(':memory:')).toBeNull();
24
- expect(sqliteFilesystemPath('')).toBeNull();
25
- expect(sqliteFilesystemPath(' :memory: ')).toBeNull();
26
- expect(sqliteFilesystemPath('./app.sqlite')).toBe('./app.sqlite');
27
- expect(sqliteFilesystemPath('sqlite:///abs/app.sqlite')).toBe('/abs/app.sqlite');
28
- expect(sqliteFilesystemPath('sqlite://rel.sqlite')).toBe('rel.sqlite');
29
- expect(sqliteFilesystemPath('sqlite:foo.db')).toBe('foo.db');
30
- expect(sqliteFilesystemPath('file:bar.db')).toBe('bar.db');
31
- expect(sqliteFilesystemPath('Data Source=./from-ado.sqlite')).toBe('./from-ado.sqlite');
32
- expect(sqliteFilesystemPath('data source = /tmp/x.sqlite')).toBe('/tmp/x.sqlite');
33
- expect(sqliteFilesystemPath('sqlite://')).toBeNull();
34
- expect(sqliteFilesystemPath('file:')).toBeNull();
35
- expect(sqliteFilesystemPath('other=value.sqlite')).toBe('other=value.sqlite');
36
- });
37
-
38
- it('prerequisiteError is null for memory, existing files, and missing-path-null', async () => {
39
- expect(await dialect.prerequisiteError(':memory:')).toBeNull();
40
- const dir = await mkdtemp(join(tmpdir(), 'sqlite-dialect-'));
41
- const file = join(dir, 'exists.sqlite');
42
- await writeFile(file, '');
43
- expect(await dialect.prerequisiteError(file)).toBeNull();
44
- const missing = join(dir, 'missing.sqlite');
45
- const err = await dialect.prerequisiteError(missing);
46
- expect(err).toContain(missing);
47
- expect(err).toMatch(/migrate-setup --provider sqlite/);
48
- });
49
-
50
- it('prepareSetup creates parent directories for file connections and no-ops memory', async () => {
51
- const dir = await mkdtemp(join(tmpdir(), 'sqlite-setup-'));
52
- const file = join(dir, 'nested', 'db', 'app.sqlite');
53
- await dialect.prepareSetup(file);
54
- const { pathExists } = await import('../path-exists.ts');
55
- expect(await pathExists(join(dir, 'nested', 'db'))).toBe(true);
56
- await dialect.prepareSetup(':memory:');
57
- });
58
-
59
- it('createClient rejects an empty path and runs exec/query/run/transaction on :memory:', async () => {
60
- await expect(dialect.createClient('')).rejects.toThrow(/sqlite requires a database file path/);
61
- const client = await dialect.createClient(':memory:');
62
- expect(client.dialect).toBe('sqlite');
63
- await client.exec('CREATE TABLE t (id INTEGER)');
64
- await client.run('INSERT INTO t (id) VALUES (?)', [1]);
65
- const viaQueryWrite = await client.query('INSERT INTO t (id) VALUES (?)', [2]);
66
- expect(viaQueryWrite).toEqual([]);
67
- expect(await client.query('SELECT id FROM t ORDER BY id')).toEqual([{ id: 1 }, { id: 2 }]);
68
- const tx = await client.transaction(async () => 7);
69
- expect(tx).toBe(7);
70
- await expect(
71
- client.transaction(async () => {
72
- throw new Error('boom');
73
- }),
74
- ).rejects.toThrow('boom');
75
- await client.close();
76
- });
77
- });
@@ -1,96 +0,0 @@
1
- import { mkdir } from 'node:fs/promises';
2
- import { dirname } from 'node:path';
3
- import type { MigrationClient, MigrationRow, MigrationSqlParam } from '../migration-client.ts';
4
- import { pathExists } from '../path-exists.ts';
5
- import { readSqlTemplate } from '../infrastructure/sql-templates.ts';
6
- import { fillTemplate, loadMessage } from '../infrastructure/message-templates.ts';
7
- import { SqlDialectBase } from '../abstractions/sql-dialect-base.ts';
8
-
9
- const [migratesDdl, migrateLogsDdl, sqlitePrerequisiteTpl] = await Promise.all([
10
- readSqlTemplate('sqlite', 'migrates'),
11
- readSqlTemplate('sqlite', 'migrate_logs'),
12
- loadMessage('errors/sqlite-prerequisite'),
13
- ]);
14
-
15
- export const sqliteFilesystemPath = (connection: string): string | null => {
16
- let s = connection.trim();
17
- const eq = s.indexOf('=');
18
- if (eq >= 0 && /data\s*source/i.test(s.slice(0, eq))) {
19
- s = s.slice(eq + 1).trim();
20
- } else if (/^sqlite:\/\//i.test(s)) {
21
- s = s.slice('sqlite://'.length);
22
- } else if (/^sqlite:/i.test(s)) {
23
- s = s.slice('sqlite:'.length);
24
- } else if (/^file:/i.test(s)) {
25
- s = s.slice('file:'.length);
26
- }
27
- if (s === ':memory:' || s === '') return null;
28
- return s;
29
- };
30
-
31
- export class SqliteDialect extends SqlDialectBase {
32
- readonly name = 'sqlite' as const;
33
- readonly connectionEnvironmentVariables = ['SQLITE_PATH', 'DB_PATH'] as const;
34
- readonly migratesDdl = migratesDdl;
35
- readonly migrateLogsDdl = migrateLogsDdl;
36
- override readonly usesLastInsertRowid = true;
37
-
38
- async prerequisiteError(connection: string): Promise<string | null> {
39
- const path = sqliteFilesystemPath(connection);
40
- if (path === null) return null;
41
- if (await pathExists(path)) return null;
42
- return fillTemplate(sqlitePrerequisiteTpl, { path });
43
- }
44
-
45
- async prepareSetup(connection: string): Promise<void> {
46
- const filePath = sqliteFilesystemPath(connection);
47
- if (filePath !== null) {
48
- await mkdir(dirname(filePath), { recursive: true });
49
- }
50
- }
51
-
52
- async createClient(connection: string): Promise<MigrationClient> {
53
- if (!connection) throw new Error('sqlite requires a database file path');
54
- const { default: Database } = await import('better-sqlite3');
55
- const db = new Database(connection);
56
- return {
57
- dialect: 'sqlite',
58
- async exec(sql) {
59
- db.exec(sql);
60
- },
61
- async query(sql, params = []) {
62
- const stmt = db.prepare<MigrationSqlParam[], MigrationRow>(sql);
63
- try {
64
- return stmt.all(...params);
65
- } catch {
66
- stmt.run(...params);
67
- return [];
68
- }
69
- },
70
- async run(sql, params = []) {
71
- db.prepare(sql).run(...params);
72
- },
73
- async transaction(fn) {
74
- db.exec('BEGIN');
75
- try {
76
- const r = await fn();
77
- db.exec('COMMIT');
78
- return r;
79
- } catch (e) {
80
- try {
81
- db.exec('ROLLBACK');
82
- } catch (rollbackErr) {
83
- console.warn(
84
- 'sqlite ROLLBACK failed after transaction error; original error rethrown',
85
- rollbackErr,
86
- );
87
- }
88
- throw e;
89
- }
90
- },
91
- async close() {
92
- db.close();
93
- },
94
- };
95
- }
96
- }
@@ -1,99 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const mssql = vi.hoisted(() => {
4
- const state = {
5
- failRollback: false,
6
- closed: false,
7
- batches: [] as string[],
8
- queries: [] as string[],
9
- inputs: [] as { name: string; value: unknown }[],
10
- };
11
- const request = () => ({
12
- input(name: string, value: unknown) {
13
- state.inputs.push({ name, value });
14
- },
15
- async query(sql: string) {
16
- state.queries.push(sql);
17
- if (sql.includes('empty')) return {};
18
- return { recordset: [{ n: 1 }] };
19
- },
20
- async batch(sql: string) {
21
- state.batches.push(sql);
22
- },
23
- });
24
- const transaction = () => ({
25
- async begin() {},
26
- async commit() {},
27
- async rollback() {
28
- if (state.failRollback) throw new Error('rollback failed');
29
- },
30
- });
31
- const pool = {
32
- request,
33
- transaction,
34
- async close() {
35
- state.closed = true;
36
- },
37
- };
38
- return {
39
- state,
40
- connect: async () => pool,
41
- };
42
- });
43
-
44
- vi.mock('mssql', () => ({ default: { connect: mssql.connect } }));
45
-
46
- import { SqlServerDialect } from './sqlserver-dialect.ts';
47
-
48
- const dialect = new SqlServerDialect();
49
-
50
- describe('SqlServerDialect', () => {
51
- afterEach(() => {
52
- mssql.state.failRollback = false;
53
- mssql.state.closed = false;
54
- mssql.state.batches = [];
55
- mssql.state.queries = [];
56
- mssql.state.inputs = [];
57
- });
58
-
59
- it('registers as sqlserver with bracket quotes, SYSUTCDATETIME, and OFFSET/FETCH', async () => {
60
- expect(dialect.name).toBe('sqlserver');
61
- expect([...dialect.connectionEnvironmentVariables]).toEqual(['MSSQL_URL', 'DATABASE_URL']);
62
- expect(dialect.quoteIdent('t')).toBe('[t]');
63
- expect(dialect.nowExpr()).toBe('SYSUTCDATETIME()');
64
- expect(dialect.limitClause(4)).toBe('OFFSET 0 ROWS FETCH NEXT 4 ROWS ONLY');
65
- expect(dialect.migratesDdl).toMatch(/migrates/i);
66
- expect(dialect.migrateLogsDdl).toMatch(/migrate_logs/i);
67
- expect(await dialect.prerequisiteError('mssql://x')).toBeNull();
68
- await dialect.prepareSetup('mssql://x');
69
- });
70
-
71
- it('createClient binds ? as @pN and handles empty recordsets', async () => {
72
- const client = await dialect.createClient('Server=localhost');
73
- expect(client.dialect).toBe('sqlserver');
74
- await client.exec('CREATE TABLE t (id INT)');
75
- expect(mssql.state.batches).toContain('CREATE TABLE t (id INT)');
76
- expect(await client.query('SELECT ?', [7])).toEqual([{ n: 1 }]);
77
- expect(mssql.state.queries).toContain('SELECT @p0');
78
- expect(mssql.state.inputs).toContainEqual({ name: 'p0', value: 7 });
79
- expect(await client.query('empty')).toEqual([]);
80
- await client.run('UPDATE t SET x = ?', [8]);
81
- expect(await client.transaction(async () => 'ok')).toBe('ok');
82
- await expect(
83
- client.transaction(async () => {
84
- throw new Error('tx');
85
- }),
86
- ).rejects.toThrow('tx');
87
- mssql.state.failRollback = true;
88
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
89
- await expect(
90
- client.transaction(async () => {
91
- throw new Error('tx2');
92
- }),
93
- ).rejects.toThrow('tx2');
94
- expect(warn).toHaveBeenCalled();
95
- warn.mockRestore();
96
- await client.close();
97
- expect(mssql.state.closed).toBe(true);
98
- });
99
- });
@@ -1,87 +0,0 @@
1
- import type { Request as MssqlRequest } from 'mssql';
2
- import type { MigrationClient, MigrationSqlParam } from '../migration-client.ts';
3
- import { interopDefault } from '../infrastructure/interop-default.ts';
4
- import { readSqlTemplate } from '../infrastructure/sql-templates.ts';
5
- import { SqlDialectBase } from '../abstractions/sql-dialect-base.ts';
6
-
7
- const [migratesDdl, migrateLogsDdl] = await Promise.all([
8
- readSqlTemplate('sqlserver', 'migrates'),
9
- readSqlTemplate('sqlserver', 'migrate_logs'),
10
- ]);
11
-
12
- const bindMssql = (
13
- req: Pick<MssqlRequest, 'input'>,
14
- sql: string,
15
- params: MigrationSqlParam[],
16
- ): string => {
17
- let n = 0;
18
- return sql.replace(/\?/g, () => {
19
- const name = `p${n}`;
20
- req.input(name, params[n]);
21
- n++;
22
- return `@${name}`;
23
- });
24
- };
25
-
26
- export class SqlServerDialect extends SqlDialectBase {
27
- readonly name = 'sqlserver' as const;
28
- readonly connectionEnvironmentVariables = ['MSSQL_URL', 'DATABASE_URL'] as const;
29
- readonly migratesDdl = migratesDdl;
30
- readonly migrateLogsDdl = migrateLogsDdl;
31
-
32
- quoteIdent(ident: string): string {
33
- return `[${ident}]`;
34
- }
35
-
36
- nowExpr(): string {
37
- return 'SYSUTCDATETIME()';
38
- }
39
-
40
- limitClause(n: number): string {
41
- return `OFFSET 0 ROWS FETCH NEXT ${n} ROWS ONLY`;
42
- }
43
-
44
- async createClient(connection: string): Promise<MigrationClient> {
45
- const mssql = interopDefault(await import('mssql'));
46
- const pool = await mssql.connect(connection);
47
- return {
48
- dialect: 'sqlserver',
49
- async exec(sql) {
50
- await pool.request().batch(sql);
51
- },
52
- async query(sql, params = []) {
53
- const req = pool.request();
54
- const translated = bindMssql(req, sql, params);
55
- const r = await req.query(translated);
56
- return r.recordset ?? [];
57
- },
58
- async run(sql, params = []) {
59
- const req = pool.request();
60
- const translated = bindMssql(req, sql, params);
61
- await req.query(translated);
62
- },
63
- async transaction(fn) {
64
- const tx = pool.transaction();
65
- await tx.begin();
66
- try {
67
- const r = await fn();
68
- await tx.commit();
69
- return r;
70
- } catch (e) {
71
- try {
72
- await tx.rollback();
73
- } catch (rollbackErr) {
74
- console.warn(
75
- 'sqlserver rollback failed after transaction error; original error rethrown',
76
- rollbackErr,
77
- );
78
- }
79
- throw e;
80
- }
81
- },
82
- async close() {
83
- await pool.close();
84
- },
85
- };
86
- }
87
- }
@@ -1,33 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { dirname, join } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { parse } from 'yaml';
5
- import { describe, expect, it } from 'vitest';
6
- import { generateHelpFiles, renderHelpTemplates } from './generate-help.ts';
7
-
8
- const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
9
- const CLI_YAML = join(REPO_ROOT, 'cli.yaml');
10
- const HELP_DIR = join(REPO_ROOT, 'shared', 'templates', 'help');
11
-
12
- describe('generate-help', () => {
13
- it('shared/templates/help matches cli.yaml', async () => {
14
- const generated = renderHelpTemplates(parse(await readFile(CLI_YAML, 'utf8')));
15
- for (const [verb, text] of Object.entries(generated)) {
16
- const onDisk = await readFile(join(HELP_DIR, `${verb}.txt`), 'utf8');
17
- expect(onDisk, verb).toBe(text);
18
- }
19
- });
20
-
21
- it('writes the verb templates', async () => {
22
- const written = await generateHelpFiles();
23
- expect(written.map((p) => p.slice(p.lastIndexOf('/') + 1)).sort()).toEqual([
24
- 'bundle.yaml',
25
- 'cli.yaml',
26
- 'create.txt',
27
- 'down.txt',
28
- 'init.txt',
29
- 'setup.txt',
30
- 'up.txt',
31
- ]);
32
- });
33
- });