turbine-orm 0.40.0 → 0.40.1
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/README.md +20 -16
- package/dist/cjs/cli/index.js +149 -36
- package/dist/cjs/cli/migrate.js +82 -26
- package/dist/cjs/schema-sql.js +12 -3
- package/dist/cjs/seed.js +104 -12
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +151 -38
- package/dist/cli/migrate.d.ts +40 -14
- package/dist/cli/migrate.js +80 -27
- package/dist/schema-sql.js +12 -3
- package/dist/seed.d.ts +11 -0
- package/dist/seed.js +105 -14
- package/package.json +1 -1
package/dist/cli/migrate.js
CHANGED
|
@@ -19,6 +19,24 @@ import { postgresql } from '../adapters/index.js';
|
|
|
19
19
|
import { postgresDialect } from '../dialect.js';
|
|
20
20
|
import { MigrationError } from '../errors.js';
|
|
21
21
|
import { DESTRUCTIVE_KIND_LABEL, scanDestructiveSql } from './destructive.js';
|
|
22
|
+
/** Extract the YYYYMMDDHHMMSS timestamp prefix from a migration name, or null. */
|
|
23
|
+
export function migrationTimestamp(name) {
|
|
24
|
+
const m = name.match(/^(\d{14})(?:_|$)/);
|
|
25
|
+
return m ? m[1] : null;
|
|
26
|
+
}
|
|
27
|
+
/** Scan a set of migration files' UP sections for data-destroying statements. */
|
|
28
|
+
export function collectUpDestructive(files) {
|
|
29
|
+
const offenders = [];
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
const { up } = parseMigrationSQL(file.path);
|
|
32
|
+
if (!up)
|
|
33
|
+
continue;
|
|
34
|
+
const hits = scanDestructiveSql(up);
|
|
35
|
+
if (hits.length > 0)
|
|
36
|
+
offenders.push({ file: file.filename, hits });
|
|
37
|
+
}
|
|
38
|
+
return offenders;
|
|
39
|
+
}
|
|
22
40
|
// ---------------------------------------------------------------------------
|
|
23
41
|
// Tracking table management
|
|
24
42
|
// ---------------------------------------------------------------------------
|
|
@@ -432,7 +450,7 @@ async function validateChecksums(client, migrationsDir, dialect = postgresDialec
|
|
|
432
450
|
}
|
|
433
451
|
return mismatches;
|
|
434
452
|
}
|
|
435
|
-
function formatChecksumMismatchError(mismatches) {
|
|
453
|
+
export function formatChecksumMismatchError(mismatches) {
|
|
436
454
|
const modified = mismatches.filter((m) => m.type === 'modified');
|
|
437
455
|
const missing = mismatches.filter((m) => m.type === 'missing');
|
|
438
456
|
const lines = [
|
|
@@ -450,7 +468,14 @@ function formatChecksumMismatchError(mismatches) {
|
|
|
450
468
|
lines.push('');
|
|
451
469
|
lines.push('Fix one of these:');
|
|
452
470
|
lines.push(' 1. Restore the file(s) to their original content, OR');
|
|
453
|
-
|
|
471
|
+
if (modified.length > 0) {
|
|
472
|
+
// `migrate down` needs the file on disk to read its DOWN section, so it is
|
|
473
|
+
// only a remedy for MODIFIED files, never for deleted ones.
|
|
474
|
+
lines.push(' 2. Roll back the affected migrations with `npx turbine migrate down` (modified files only), OR');
|
|
475
|
+
}
|
|
476
|
+
if (missing.length > 0) {
|
|
477
|
+
lines.push(' (deleted files cannot be rolled back: restore the file, then run `migrate down` if needed), OR');
|
|
478
|
+
}
|
|
454
479
|
lines.push(' 3. Pass `--allow-drift` to bypass this check (advanced — make sure you know what you are doing).');
|
|
455
480
|
return lines.join('\n');
|
|
456
481
|
}
|
|
@@ -559,42 +584,43 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
559
584
|
}
|
|
560
585
|
const applied = await getAppliedMigrations(client, dialect);
|
|
561
586
|
const appliedNames = new Set(applied.map((m) => m.name));
|
|
587
|
+
// Newest already-applied timestamp. Anything applied below this line is
|
|
588
|
+
// going in out of order (an older migration created/applied after a newer
|
|
589
|
+
// one). Used to surface a warning; never blocks.
|
|
590
|
+
const newestPrior = applied
|
|
591
|
+
.map((m) => ({ ts: migrationTimestamp(m.name), name: m.name }))
|
|
592
|
+
.filter((m) => m.ts !== null)
|
|
593
|
+
.sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0))[0];
|
|
562
594
|
const allFiles = listMigrationFiles(migrationsDir);
|
|
563
595
|
let pending = allFiles.filter((f) => !appliedNames.has(f.name));
|
|
564
596
|
if (options?.step != null && options.step > 0) {
|
|
565
597
|
pending = pending.slice(0, options.step);
|
|
566
598
|
}
|
|
599
|
+
// Destructive statements in the pending batch, computed once. Returned in
|
|
600
|
+
// the result regardless of the gate so `deploy` can print a notice even
|
|
601
|
+
// though it proceeds by design.
|
|
602
|
+
const destructive = collectUpDestructive(pending);
|
|
567
603
|
// Data-loss gate: refuse to run pending migrations containing destructive
|
|
568
604
|
// statements unless the caller has EXPLICITLY opted in. The CLI layers an
|
|
569
605
|
// interactive typed confirmation on top of this; programmatic callers must
|
|
570
606
|
// pass `allowDestructive: true`. Safe-by-default is the whole point — a
|
|
571
607
|
// DROP TABLE should never run just because a file exists.
|
|
572
|
-
if (!options?.allowDestructive) {
|
|
573
|
-
const
|
|
574
|
-
for (const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const hits = scanDestructiveSql(up);
|
|
579
|
-
if (hits.length > 0)
|
|
580
|
-
offenders.push({ file: file.filename, hits });
|
|
581
|
-
}
|
|
582
|
-
if (offenders.length > 0) {
|
|
583
|
-
const lines = ['[turbine] Refusing to apply migrations containing DESTRUCTIVE statements:', ''];
|
|
584
|
-
for (const o of offenders) {
|
|
585
|
-
lines.push(` ${o.file}`);
|
|
586
|
-
for (const h of o.hits) {
|
|
587
|
-
lines.push(` - [${h.kind}] ${h.target} — ${DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
588
|
-
}
|
|
608
|
+
if (!options?.allowDestructive && destructive.length > 0) {
|
|
609
|
+
const lines = ['[turbine] Refusing to apply migrations containing DESTRUCTIVE statements:', ''];
|
|
610
|
+
for (const o of destructive) {
|
|
611
|
+
lines.push(` ${o.file}`);
|
|
612
|
+
for (const h of o.hits) {
|
|
613
|
+
lines.push(` - [${h.kind}] ${h.target}: ${DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
589
614
|
}
|
|
590
|
-
lines.push('');
|
|
591
|
-
lines.push('Review the statements above. To proceed: run `npx turbine migrate up` interactively');
|
|
592
|
-
lines.push('and confirm, pass --allow-destructive, or set allowDestructive: true programmatically.');
|
|
593
|
-
throw new MigrationError(lines.join('\n'));
|
|
594
615
|
}
|
|
616
|
+
lines.push('');
|
|
617
|
+
lines.push('Review the statements above. To proceed: run `npx turbine migrate up` interactively');
|
|
618
|
+
lines.push('and confirm, pass --allow-destructive, or set allowDestructive: true programmatically.');
|
|
619
|
+
throw new MigrationError(lines.join('\n'));
|
|
595
620
|
}
|
|
596
621
|
const results = [];
|
|
597
622
|
const errors = [];
|
|
623
|
+
const outOfOrder = [];
|
|
598
624
|
for (const file of pending) {
|
|
599
625
|
const { up } = parseMigrationSQL(file.path);
|
|
600
626
|
if (!up) {
|
|
@@ -609,6 +635,11 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
609
635
|
await client.query(dialect.buildMigrationInsertApplied(quotedTrackingTable(dialect)), [file.name, hash]);
|
|
610
636
|
await client.query('COMMIT');
|
|
611
637
|
results.push(file);
|
|
638
|
+
// Flag an out-of-order apply: this file's timestamp is older than a
|
|
639
|
+
// migration that was already applied before this run started.
|
|
640
|
+
if (newestPrior && file.timestamp && file.timestamp < newestPrior.ts) {
|
|
641
|
+
outOfOrder.push({ applied: file.filename, newestPrior: `${newestPrior.name}.sql` });
|
|
642
|
+
}
|
|
612
643
|
}
|
|
613
644
|
catch (err) {
|
|
614
645
|
await client.query('ROLLBACK');
|
|
@@ -618,7 +649,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
618
649
|
break;
|
|
619
650
|
}
|
|
620
651
|
}
|
|
621
|
-
return { applied: results, errors };
|
|
652
|
+
return { applied: results, errors, destructive, outOfOrder };
|
|
622
653
|
}
|
|
623
654
|
finally {
|
|
624
655
|
await releaseLock(client, lockId, adapter);
|
|
@@ -634,7 +665,9 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
634
665
|
*/
|
|
635
666
|
export async function migrateDeploy(connectionString, migrationsDir, options) {
|
|
636
667
|
return migrateUp(connectionString, migrationsDir, {
|
|
637
|
-
|
|
668
|
+
// Honor `--allow-drift` on deploy exactly as `up` does: deploy's own drift
|
|
669
|
+
// error recommends this flag, so it must actually bypass the checksum block.
|
|
670
|
+
allowDrift: options?.allowDrift === true,
|
|
638
671
|
allowDestructive: true,
|
|
639
672
|
adapter: options?.adapter,
|
|
640
673
|
dialect: options?.dialect,
|
|
@@ -693,7 +726,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
693
726
|
for (const o of offenders) {
|
|
694
727
|
lines.push(` ${o.file}`);
|
|
695
728
|
for (const h of o.hits) {
|
|
696
|
-
lines.push(` - [${h.kind}] ${h.target}
|
|
729
|
+
lines.push(` - [${h.kind}] ${h.target}: ${DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
697
730
|
}
|
|
698
731
|
}
|
|
699
732
|
lines.push('');
|
|
@@ -755,7 +788,8 @@ export async function migrateStatus(connectionString, migrationsDir, options) {
|
|
|
755
788
|
const applied = await getAppliedMigrations(client, dialect);
|
|
756
789
|
const appliedMap = new Map(applied.map((m) => [m.name, m]));
|
|
757
790
|
const allFiles = listMigrationFiles(migrationsDir);
|
|
758
|
-
|
|
791
|
+
const fileNames = new Set(allFiles.map((f) => f.name));
|
|
792
|
+
const fromFiles = allFiles.map((file) => {
|
|
759
793
|
const record = appliedMap.get(file.name);
|
|
760
794
|
let checksumValid;
|
|
761
795
|
if (record) {
|
|
@@ -770,6 +804,25 @@ export async function migrateStatus(connectionString, migrationsDir, options) {
|
|
|
770
804
|
checksumValid,
|
|
771
805
|
};
|
|
772
806
|
});
|
|
807
|
+
// Applied migrations whose file was deleted from disk. up/deploy already
|
|
808
|
+
// catch this as drift; status must not silently drop them from history.
|
|
809
|
+
const missing = applied
|
|
810
|
+
.filter((m) => !fileNames.has(m.name))
|
|
811
|
+
.map((m) => ({
|
|
812
|
+
file: parseMigrationFilename(`${m.name}.sql`) ?? {
|
|
813
|
+
filename: `${m.name}.sql`,
|
|
814
|
+
path: '',
|
|
815
|
+
name: m.name,
|
|
816
|
+
timestamp: '',
|
|
817
|
+
},
|
|
818
|
+
applied: true,
|
|
819
|
+
appliedAt: m.applied_at,
|
|
820
|
+
checksumValid: false,
|
|
821
|
+
missingFile: true,
|
|
822
|
+
}));
|
|
823
|
+
// Keep the overall list in timestamp order so a deleted entry appears where
|
|
824
|
+
// it belongs in history, not tacked on at the end.
|
|
825
|
+
return [...fromFiles, ...missing].sort((a, b) => a.file.name < b.file.name ? -1 : a.file.name > b.file.name ? 1 : 0);
|
|
773
826
|
}
|
|
774
827
|
finally {
|
|
775
828
|
await client.end();
|
package/dist/schema-sql.js
CHANGED
|
@@ -821,14 +821,23 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
821
821
|
}
|
|
822
822
|
}
|
|
823
823
|
}
|
|
824
|
-
// Check for columns in DB that are not in schema
|
|
824
|
+
// Check for columns in DB that are not in schema.
|
|
825
|
+
// The DROP COLUMN statement stays IN `result.statements` so the destructive
|
|
826
|
+
// gate (findDestructivePushStatements / scanDestructiveSql, and the migrate
|
|
827
|
+
// up gate) can see it and refuse by default. Safety is enforced by that
|
|
828
|
+
// gate, not by hiding the statement: withholding it here previously made
|
|
829
|
+
// `push` report "already in sync" for a destructive-only diff and print a
|
|
830
|
+
// false "Altered" for a mixed one, since the 0.36 guard never received it.
|
|
831
|
+
// No reverse is emitted (a dropped column and its data cannot be recreated
|
|
832
|
+
// automatically), so diff-generated migrations fall back to the documented
|
|
833
|
+
// irreversible-DOWN placeholder for this change.
|
|
825
834
|
for (const dbColName of Object.keys(dbCols)) {
|
|
826
835
|
const hasField = Object.entries(tableDef.columns).some(([fieldName]) => camelToSnake(fieldName) === dbColName);
|
|
827
836
|
if (!hasField) {
|
|
828
837
|
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP COLUMN ${dialect.quoteIdentifier(dbColName)};`;
|
|
829
|
-
const reverseSql = `-- Cannot auto-reverse DROP COLUMN for "${dbColName}"
|
|
838
|
+
const reverseSql = `-- Cannot auto-reverse DROP COLUMN for "${dbColName}"; add it back manually`;
|
|
830
839
|
alterDef.columns.push({ column: dbColName, action: 'drop', sql, reverseSql });
|
|
831
|
-
|
|
840
|
+
result.statements.push(sql);
|
|
832
841
|
}
|
|
833
842
|
}
|
|
834
843
|
if (alterDef.columns.length > 0) {
|
package/dist/seed.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { TurbineClient } from './client.js';
|
|
2
2
|
export type SeedFunction = (db: TurbineClient) => Promise<void> | void;
|
|
3
3
|
export type DefinedSeed = () => Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* Extract the filesystem path from a single V8 stack-trace line, regardless of
|
|
6
|
+
* whether the frame is a `file://` URL (ESM), a bare absolute path (CJS / tsx),
|
|
7
|
+
* or a wrapped `(… )` location. The trailing `:line:col` (and any surrounding
|
|
8
|
+
* parens) are peeled from the END so a Windows drive colon or a URL scheme colon
|
|
9
|
+
* inside the path never confuses the match. Non-file frames (`node:internal/…`,
|
|
10
|
+
* `<anonymous>`) return null.
|
|
11
|
+
*
|
|
12
|
+
* Exported for unit testing the frame parser in isolation.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseStackFramePath(line: string): string | null;
|
|
4
15
|
export declare function defineSeed(fn: SeedFunction): DefinedSeed;
|
package/dist/seed.js
CHANGED
|
@@ -1,32 +1,104 @@
|
|
|
1
|
-
import { realpathSync } from 'node:fs';
|
|
1
|
+
import { realpathSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
-
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
4
|
import { TurbineClient } from './client.js';
|
|
5
5
|
import { ConnectionError } from './errors.js';
|
|
6
6
|
const emptySchema = { tables: {}, enums: {} };
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Extract the filesystem path from a single V8 stack-trace line, regardless of
|
|
9
|
+
* whether the frame is a `file://` URL (ESM), a bare absolute path (CJS / tsx),
|
|
10
|
+
* or a wrapped `(… )` location. The trailing `:line:col` (and any surrounding
|
|
11
|
+
* parens) are peeled from the END so a Windows drive colon or a URL scheme colon
|
|
12
|
+
* inside the path never confuses the match. Non-file frames (`node:internal/…`,
|
|
13
|
+
* `<anonymous>`) return null.
|
|
14
|
+
*
|
|
15
|
+
* Exported for unit testing the frame parser in isolation.
|
|
16
|
+
*/
|
|
17
|
+
export function parseStackFramePath(line) {
|
|
18
|
+
// Peel the trailing `:line:col` (with any closing paren) from the END so a
|
|
19
|
+
// Windows drive colon or a `file://` scheme colon earlier in the path is never
|
|
20
|
+
// mistaken for the location separator.
|
|
21
|
+
const loc = line.match(/:(\d+):(\d+)\)?\s*$/);
|
|
22
|
+
if (!loc || loc.index === undefined)
|
|
10
23
|
return null;
|
|
24
|
+
let head = line.slice(0, loc.index);
|
|
25
|
+
const paren = head.lastIndexOf('(');
|
|
26
|
+
if (paren !== -1) {
|
|
27
|
+
// `at fn (PATH:line:col)`: the path is whatever the last "(" wraps.
|
|
28
|
+
head = head.slice(paren + 1);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// `at PATH:line:col`: drop the leading " at " prefix.
|
|
32
|
+
head = head.replace(/^\s*at\s+/, '');
|
|
33
|
+
}
|
|
34
|
+
head = head.trim();
|
|
35
|
+
if (head.startsWith('file://')) {
|
|
36
|
+
try {
|
|
37
|
+
return fileURLToPath(head);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Accept only absolute filesystem paths (POSIX `/…` or Windows `C:\…` / `C:/…`).
|
|
44
|
+
if (/^(\/|[A-Za-z]:[\\/])/.test(head))
|
|
45
|
+
return head;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/** Best-effort canonicalization so two spellings of the same file compare equal. */
|
|
49
|
+
function canonicalPath(p) {
|
|
11
50
|
try {
|
|
12
|
-
return
|
|
51
|
+
return realpathSync(p);
|
|
13
52
|
}
|
|
14
53
|
catch {
|
|
15
|
-
return
|
|
54
|
+
return resolve(p);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The canonical path of THIS module's own file, captured once at load time from
|
|
59
|
+
* a fresh stack. It is used to skip the library's own frames when locating the
|
|
60
|
+
* caller. This is robust to the src (`seed.ts`) vs published dist (`seed.js`)
|
|
61
|
+
* basename difference AND to the fact that the user's own file is ALSO named
|
|
62
|
+
* `seed.ts`, which a basename skip-list would wrongly exclude. This fixes the
|
|
63
|
+
* silent no-op: previously the plain-path skip-list only knew `src/seed.ts`, so
|
|
64
|
+
* the library's own `dist/seed.js` frame (or a tsx plain-path frame) was
|
|
65
|
+
* mistaken for the caller and the entry===caller self-run check never passed.
|
|
66
|
+
*/
|
|
67
|
+
const SELF_PATH = (() => {
|
|
68
|
+
const stack = new Error().stack;
|
|
69
|
+
if (!stack)
|
|
70
|
+
return null;
|
|
71
|
+
// Frame [1] (after the "Error" header) is this IIFE, i.e. the current module.
|
|
72
|
+
for (const line of stack.split('\n').slice(1)) {
|
|
73
|
+
const p = parseStackFramePath(line);
|
|
74
|
+
if (p)
|
|
75
|
+
return canonicalPath(p);
|
|
16
76
|
}
|
|
77
|
+
return null;
|
|
78
|
+
})();
|
|
79
|
+
function entryUrl() {
|
|
80
|
+
const entry = process.argv[1];
|
|
81
|
+
if (!entry)
|
|
82
|
+
return null;
|
|
83
|
+
return pathToFileURL(canonicalPath(entry)).href;
|
|
17
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* The first stack frame that is NOT part of this module, expressed as a
|
|
87
|
+
* canonical `file://` URL. That frame is whoever invoked `defineSeed`: the
|
|
88
|
+
* user's seed module when the file is run directly.
|
|
89
|
+
*/
|
|
18
90
|
function callerUrl() {
|
|
19
91
|
const stack = new Error().stack;
|
|
20
92
|
if (!stack)
|
|
21
93
|
return null;
|
|
22
94
|
for (const line of stack.split('\n').slice(2)) {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
95
|
+
const p = parseStackFramePath(line);
|
|
96
|
+
if (!p)
|
|
97
|
+
continue;
|
|
98
|
+
const canonical = canonicalPath(p);
|
|
99
|
+
if (SELF_PATH && canonical === SELF_PATH)
|
|
100
|
+
continue; // skip the library's own frames
|
|
101
|
+
return pathToFileURL(canonical).href;
|
|
30
102
|
}
|
|
31
103
|
return null;
|
|
32
104
|
}
|
|
@@ -35,6 +107,24 @@ function isDirectSeedModule() {
|
|
|
35
107
|
const caller = callerUrl();
|
|
36
108
|
return process.env.NODE_TEST_CONTEXT === undefined && !!entry && !!caller && entry === caller;
|
|
37
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Signal, to a parent `turbine seed` process, that a defineSeed callback
|
|
112
|
+
* actually executed to completion. The CLI sets `TURBINE_SEED_SENTINEL` to a
|
|
113
|
+
* temp path before spawning the seed; if the file never appears the CLI knows
|
|
114
|
+
* the seed module loaded but no callback ran, and reports that as a failure
|
|
115
|
+
* instead of a false "Seed completed".
|
|
116
|
+
*/
|
|
117
|
+
function markSeedRan() {
|
|
118
|
+
const sentinel = process.env.TURBINE_SEED_SENTINEL;
|
|
119
|
+
if (!sentinel)
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
writeFileSync(sentinel, 'ran');
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Best-effort only: an unwritable sentinel must never fail a good seed run.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
38
128
|
async function runSeed(fn) {
|
|
39
129
|
const connectionString = process.env.DATABASE_URL;
|
|
40
130
|
if (!connectionString) {
|
|
@@ -43,6 +133,7 @@ async function runSeed(fn) {
|
|
|
43
133
|
const db = new TurbineClient({ connectionString }, emptySchema);
|
|
44
134
|
try {
|
|
45
135
|
await fn(db);
|
|
136
|
+
markSeedRan();
|
|
46
137
|
}
|
|
47
138
|
finally {
|
|
48
139
|
await db.disconnect();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.1",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|