@zudojs/database 0.1.0 → 1.0.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 +21 -0
- package/README.md +223 -17
- package/dist/cache/cache.memory.d.ts +167 -0
- package/dist/cache/cache.memory.js +186 -40
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +2 -2
- package/dist/database/database.core.d.ts +91 -0
- package/dist/database/database.core.js +21 -4
- package/dist/databaseClient/databaseClient.core.d.ts +165 -14
- package/dist/databaseClient/databaseClient.core.js +304 -70
- package/dist/databaseClient/databaseClient.errors.d.ts +112 -0
- package/dist/databaseClient/databaseClient.errors.js +425 -0
- package/dist/databaseClient/index.d.ts +2 -1
- package/dist/databaseClient/index.js +2 -1
- package/dist/databaseConnection/databaseConnection.manager.d.ts +163 -0
- package/dist/databaseConnection/databaseConnection.manager.js +138 -76
- package/dist/databaseConnection/index.d.ts +1 -1
- package/dist/databaseType/databaseType.type.d.ts +224 -0
- package/dist/databaseType/databaseType.type.js +0 -35
- package/dist/health/health.check.d.ts +74 -0
- package/dist/health/health.check.js +78 -47
- package/dist/health/index.d.ts +1 -1
- package/dist/health/index.js +1 -1
- package/dist/index.d.ts +14 -14
- package/dist/index.js +11 -11
- package/dist/locks/index.d.ts +2 -2
- package/dist/locks/index.js +2 -2
- package/dist/locks/locks.core.d.ts +128 -0
- package/dist/locks/locks.core.js +144 -85
- package/dist/migration/index.d.ts +3 -1
- package/dist/migration/index.js +2 -0
- package/dist/migration/migration.dialect.d.ts +65 -0
- package/dist/migration/migration.dialect.js +63 -0
- package/dist/migration/migration.helpers.d.ts +65 -0
- package/dist/migration/migration.helpers.js +60 -18
- package/dist/migration/migration.runner.d.ts +75 -0
- package/dist/migration/migration.runner.js +195 -107
- package/dist/migration/migration.types.d.ts +85 -0
- package/dist/pagination/index.d.ts +2 -1
- package/dist/pagination/index.js +2 -1
- package/dist/pagination/pagination.core.d.ts +163 -0
- package/dist/pagination/pagination.core.js +78 -9
- package/dist/pagination/pagination.keyset.d.ts +55 -0
- package/dist/pagination/pagination.keyset.js +108 -0
- package/dist/queryBuilder/index.d.ts +4 -3
- package/dist/queryBuilder/index.js +3 -2
- package/dist/queryBuilder/queryBuilder.core.d.ts +178 -0
- package/dist/queryBuilder/queryBuilder.core.js +92 -14
- package/dist/queryBuilder/queryBuilder.factory.d.ts +6 -0
- package/dist/queryBuilder/queryBuilder.factory.js +0 -25
- package/dist/queryBuilder/queryBuilder.filter.d.ts +170 -0
- package/dist/queryBuilder/queryBuilder.filter.js +191 -30
- package/dist/queryBuilder/queryBuilder.prisma.d.ts +60 -0
- package/dist/queryBuilder/queryBuilder.prisma.js +233 -0
- package/dist/queryBuilder/queryBuilder.type.d.ts +48 -0
- package/dist/relations/index.d.ts +1 -1
- package/dist/relations/index.js +1 -1
- package/dist/relations/relations.definition.d.ts +184 -0
- package/dist/relations/relations.definition.js +186 -19
- package/dist/repository/index.d.ts +2 -1
- package/dist/repository/index.js +1 -0
- package/dist/repository/repository.base.d.ts +270 -0
- package/dist/repository/repository.base.js +350 -84
- package/dist/repository/repository.errors.d.ts +60 -0
- package/dist/repository/repository.errors.js +235 -0
- package/dist/seed/index.d.ts +2 -2
- package/dist/seed/index.js +1 -1
- package/dist/seed/seed.runner.d.ts +160 -0
- package/dist/seed/seed.runner.js +194 -225
- package/dist/transaction/index.d.ts +1 -1
- package/dist/transaction/index.js +1 -1
- package/dist/transaction/transaction.core.d.ts +117 -0
- package/dist/transaction/transaction.core.js +83 -56
- package/dist/unitOfWork/unitOfWork.core.d.ts +44 -0
- package/dist/unitOfWork/unitOfWork.core.js +9 -20
- package/package.json +40 -24
- package/dist/cache/cache.memory.d.ts.map +0 -1
- package/dist/cache/cache.memory.js.map +0 -1
- package/dist/cache/index.d.ts.map +0 -1
- package/dist/cache/index.js.map +0 -1
- package/dist/database/database.core.d.ts.map +0 -1
- package/dist/database/database.core.js.map +0 -1
- package/dist/database/index.d.ts.map +0 -1
- package/dist/database/index.js.map +0 -1
- package/dist/databaseClient/databaseClient.core.d.ts.map +0 -1
- package/dist/databaseClient/databaseClient.core.js.map +0 -1
- package/dist/databaseClient/databaseClient.logger.d.ts.map +0 -1
- package/dist/databaseClient/databaseClient.logger.js.map +0 -1
- package/dist/databaseClient/index.d.ts.map +0 -1
- package/dist/databaseClient/index.js.map +0 -1
- package/dist/databaseConnection/databaseConnection.manager.d.ts.map +0 -1
- package/dist/databaseConnection/databaseConnection.manager.js.map +0 -1
- package/dist/databaseConnection/index.d.ts.map +0 -1
- package/dist/databaseConnection/index.js.map +0 -1
- package/dist/databaseType/databaseType.type.d.ts.map +0 -1
- package/dist/databaseType/databaseType.type.js.map +0 -1
- package/dist/databaseType/index.d.ts.map +0 -1
- package/dist/databaseType/index.js.map +0 -1
- package/dist/health/health.check.d.ts.map +0 -1
- package/dist/health/health.check.js.map +0 -1
- package/dist/health/index.d.ts.map +0 -1
- package/dist/health/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/locks/index.d.ts.map +0 -1
- package/dist/locks/index.js.map +0 -1
- package/dist/locks/locks.core.d.ts.map +0 -1
- package/dist/locks/locks.core.js.map +0 -1
- package/dist/migration/index.d.ts.map +0 -1
- package/dist/migration/index.js.map +0 -1
- package/dist/migration/migration.helpers.d.ts.map +0 -1
- package/dist/migration/migration.helpers.js.map +0 -1
- package/dist/migration/migration.runner.d.ts.map +0 -1
- package/dist/migration/migration.runner.js.map +0 -1
- package/dist/migration/migration.types.d.ts.map +0 -1
- package/dist/migration/migration.types.js.map +0 -1
- package/dist/pagination/index.d.ts.map +0 -1
- package/dist/pagination/index.js.map +0 -1
- package/dist/pagination/pagination.core.d.ts.map +0 -1
- package/dist/pagination/pagination.core.js.map +0 -1
- package/dist/queryBuilder/index.d.ts.map +0 -1
- package/dist/queryBuilder/index.js.map +0 -1
- package/dist/queryBuilder/queryBuilder.core.d.ts.map +0 -1
- package/dist/queryBuilder/queryBuilder.core.js.map +0 -1
- package/dist/queryBuilder/queryBuilder.factory.d.ts.map +0 -1
- package/dist/queryBuilder/queryBuilder.factory.js.map +0 -1
- package/dist/queryBuilder/queryBuilder.filter.d.ts.map +0 -1
- package/dist/queryBuilder/queryBuilder.filter.js.map +0 -1
- package/dist/queryBuilder/queryBuilder.type.d.ts.map +0 -1
- package/dist/queryBuilder/queryBuilder.type.js.map +0 -1
- package/dist/relations/index.d.ts.map +0 -1
- package/dist/relations/index.js.map +0 -1
- package/dist/relations/relations.definition.d.ts.map +0 -1
- package/dist/relations/relations.definition.js.map +0 -1
- package/dist/repository/index.d.ts.map +0 -1
- package/dist/repository/index.js.map +0 -1
- package/dist/repository/repository.base.d.ts.map +0 -1
- package/dist/repository/repository.base.js.map +0 -1
- package/dist/seed/index.d.ts.map +0 -1
- package/dist/seed/index.js.map +0 -1
- package/dist/seed/seed.runner.d.ts.map +0 -1
- package/dist/seed/seed.runner.js.map +0 -1
- package/dist/transaction/index.d.ts.map +0 -1
- package/dist/transaction/index.js.map +0 -1
- package/dist/transaction/transaction.core.d.ts.map +0 -1
- package/dist/transaction/transaction.core.js.map +0 -1
- package/dist/unitOfWork/index.d.ts.map +0 -1
- package/dist/unitOfWork/index.js.map +0 -1
- package/dist/unitOfWork/unitOfWork.core.d.ts.map +0 -1
- package/dist/unitOfWork/unitOfWork.core.js.map +0 -1
package/dist/seed/seed.runner.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { DatabaseError } from "@zudojs/errors";
|
|
2
|
-
import {
|
|
1
|
+
import { DatabaseError, DatabaseOperation, } from "@zudojs/errors";
|
|
2
|
+
import { isDatabaseErrorLike } from "../databaseClient/databaseClient.errors.js";
|
|
3
|
+
import { getSqlDialect } from "../migration/migration.dialect.js";
|
|
4
|
+
import { hashLockKey, validateIdentifier, validateLockKey, } from "../migration/migration.helpers.js";
|
|
3
5
|
/**
|
|
4
6
|
* Default seed tracking table.
|
|
5
7
|
*/
|
|
@@ -10,22 +12,31 @@ export const DEFAULT_SEED_TABLE = "_seeds";
|
|
|
10
12
|
export const DEFAULT_SEED_LOCK = "database:seeds";
|
|
11
13
|
/**
|
|
12
14
|
* Runs and tracks database seeds.
|
|
15
|
+
*
|
|
16
|
+
* Every entry point re-reads the applied history *inside* the advisory
|
|
17
|
+
* lock before deciding what to execute, so two runners started together
|
|
18
|
+
* never apply or revert the same seed twice.
|
|
13
19
|
*/
|
|
14
20
|
export class SeedRunner {
|
|
15
21
|
client;
|
|
16
22
|
seeds;
|
|
17
23
|
tableName;
|
|
18
24
|
lockKey;
|
|
25
|
+
dialect;
|
|
26
|
+
transactionOptions;
|
|
27
|
+
perItemTransaction;
|
|
19
28
|
constructor(client, seeds, options = {}) {
|
|
20
|
-
if (!client)
|
|
29
|
+
if (!client)
|
|
21
30
|
throw new TypeError("A database client is required.");
|
|
22
|
-
}
|
|
23
31
|
this.client = client;
|
|
24
32
|
this.seeds = normalizeSeeds(seeds);
|
|
25
33
|
this.tableName = options.tableName ?? DEFAULT_SEED_TABLE;
|
|
26
34
|
this.lockKey = options.lockKey ?? DEFAULT_SEED_LOCK;
|
|
35
|
+
this.dialect = getSqlDialect(options.dialect);
|
|
36
|
+
this.transactionOptions = { ...options.transaction };
|
|
37
|
+
this.perItemTransaction = options.perItemTransaction ?? true;
|
|
27
38
|
validateIdentifier(this.tableName, "seed table name");
|
|
28
|
-
validateLockKey(this.lockKey);
|
|
39
|
+
validateLockKey(this.lockKey, "seed lock key");
|
|
29
40
|
}
|
|
30
41
|
/**
|
|
31
42
|
* Returns the seed runner status.
|
|
@@ -33,276 +44,275 @@ export class SeedRunner {
|
|
|
33
44
|
async status() {
|
|
34
45
|
await this.ensureSeedTable();
|
|
35
46
|
const applied = await this.getAppliedSeeds();
|
|
36
|
-
|
|
37
|
-
const pending = this.seeds.filter((seed) => !appliedNames.has(seed.name));
|
|
38
|
-
return {
|
|
39
|
-
pending,
|
|
40
|
-
applied,
|
|
41
|
-
};
|
|
47
|
+
return { pending: this.computePending(applied), applied };
|
|
42
48
|
}
|
|
43
49
|
/**
|
|
44
|
-
* Executes every pending seed.
|
|
50
|
+
* Executes every pending seed in order.
|
|
45
51
|
*/
|
|
46
52
|
async run() {
|
|
47
53
|
await this.ensureSeedTable();
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
applied: [],
|
|
54
|
-
skipped: applied,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
54
|
+
const before = await this.getAppliedSeeds();
|
|
55
|
+
const pending = this.computePending(before);
|
|
56
|
+
if (pending.length === 0)
|
|
57
|
+
return { applied: [], skipped: before };
|
|
57
58
|
const newlyApplied = [];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
const skipped = [...before];
|
|
60
|
+
if (!this.perItemTransaction) {
|
|
61
|
+
await this.client.transaction(async (transaction) => {
|
|
62
|
+
await this.acquireSeedLock(transaction);
|
|
63
|
+
const applied = await this.getAppliedSeeds(transaction);
|
|
64
|
+
skipped.push(...applied.filter((r) => !hasName(before, r.name)));
|
|
65
|
+
for (const seed of this.computePending(applied)) {
|
|
66
|
+
await this.executeSeed(transaction, seed);
|
|
67
|
+
newlyApplied.push(await this.recordSeed(transaction, seed));
|
|
68
|
+
}
|
|
69
|
+
}, this.buildTransactionOptions());
|
|
70
|
+
return { applied: newlyApplied, skipped };
|
|
71
|
+
}
|
|
72
|
+
for (const seed of pending) {
|
|
73
|
+
const outcome = await this.client.transaction(async (transaction) => {
|
|
74
|
+
await this.acquireSeedLock(transaction);
|
|
75
|
+
const applied = await this.getAppliedSeeds(transaction);
|
|
76
|
+
const existing = applied.find((r) => r.name === seed.name);
|
|
77
|
+
if (existing)
|
|
78
|
+
return { record: existing, skipped: true };
|
|
61
79
|
await this.executeSeed(transaction, seed);
|
|
62
80
|
const record = await this.recordSeed(transaction, seed);
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
81
|
+
return { record, skipped: false };
|
|
82
|
+
}, this.buildTransactionOptions());
|
|
83
|
+
if (outcome.skipped)
|
|
84
|
+
skipped.push(outcome.record);
|
|
85
|
+
else
|
|
86
|
+
newlyApplied.push(outcome.record);
|
|
87
|
+
}
|
|
88
|
+
return { applied: newlyApplied, skipped };
|
|
70
89
|
}
|
|
71
90
|
/**
|
|
72
|
-
* Executes one named seed.
|
|
91
|
+
* Executes one named seed. Returns the existing record when already
|
|
92
|
+
* applied.
|
|
73
93
|
*/
|
|
74
94
|
async runOne(name) {
|
|
75
95
|
validateSeedName(name);
|
|
76
96
|
await this.ensureSeedTable();
|
|
77
97
|
const seed = this.seeds.find((candidate) => candidate.name === name);
|
|
78
98
|
if (!seed) {
|
|
79
|
-
throw
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const existing = applied.find((record) => record.name === name);
|
|
83
|
-
if (existing) {
|
|
84
|
-
return existing;
|
|
99
|
+
throw this.seedError(`Seed "${name}" is not registered.`, {
|
|
100
|
+
metadata: { name },
|
|
101
|
+
});
|
|
85
102
|
}
|
|
86
|
-
|
|
87
|
-
await this.client.transaction(async (transaction) => {
|
|
103
|
+
return this.client.transaction(async (transaction) => {
|
|
88
104
|
await this.acquireSeedLock(transaction);
|
|
89
105
|
const current = await this.getAppliedSeeds(transaction);
|
|
90
106
|
const alreadyApplied = current.find((record) => record.name === name);
|
|
91
|
-
if (alreadyApplied)
|
|
92
|
-
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
107
|
+
if (alreadyApplied)
|
|
108
|
+
return alreadyApplied;
|
|
95
109
|
await this.executeSeed(transaction, seed);
|
|
96
|
-
|
|
97
|
-
});
|
|
98
|
-
if (!result) {
|
|
99
|
-
throw new DatabaseError(`Seed "${name}" did not produce an execution record.`);
|
|
100
|
-
}
|
|
101
|
-
return result;
|
|
110
|
+
return this.recordSeed(transaction, seed);
|
|
111
|
+
}, this.buildTransactionOptions());
|
|
102
112
|
}
|
|
103
113
|
/**
|
|
104
114
|
* Rolls back the most recently applied seed.
|
|
105
115
|
*/
|
|
106
116
|
async rollback() {
|
|
107
|
-
await this.
|
|
108
|
-
|
|
109
|
-
if (applied.length === 0) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
const latest = applied[applied.length - 1];
|
|
113
|
-
if (!latest) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
const seed = this.seeds.find((candidate) => candidate.name === latest.name);
|
|
117
|
-
if (!seed) {
|
|
118
|
-
throw new DatabaseError(`Seed "${latest.name}" is recorded as applied but is not registered.`);
|
|
119
|
-
}
|
|
120
|
-
if (!seed.rollback) {
|
|
121
|
-
throw new DatabaseError(`Seed "${seed.name}" does not define a rollback operation.`);
|
|
122
|
-
}
|
|
123
|
-
await this.client.transaction(async (transaction) => {
|
|
124
|
-
await this.acquireSeedLock(transaction);
|
|
125
|
-
await seed.rollback(transaction);
|
|
126
|
-
await this.deleteSeedRecord(transaction, seed.name);
|
|
127
|
-
});
|
|
128
|
-
return latest;
|
|
117
|
+
const [record] = await this.rollbackSteps(1);
|
|
118
|
+
return record ?? null;
|
|
129
119
|
}
|
|
130
120
|
/**
|
|
131
121
|
* Rolls back every applied seed in reverse execution order.
|
|
132
122
|
*/
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const applied = await this.getAppliedSeeds();
|
|
136
|
-
if (applied.length === 0) {
|
|
137
|
-
return [];
|
|
138
|
-
}
|
|
139
|
-
const rolledBack = [];
|
|
140
|
-
await this.client.transaction(async (transaction) => {
|
|
141
|
-
await this.acquireSeedLock(transaction);
|
|
142
|
-
for (let index = applied.length - 1; index >= 0; index -= 1) {
|
|
143
|
-
const record = applied[index];
|
|
144
|
-
if (!record) {
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
const seed = this.seeds.find((candidate) => candidate.name === record.name);
|
|
148
|
-
if (!seed) {
|
|
149
|
-
throw new DatabaseError(`Seed "${record.name}" is not registered.`);
|
|
150
|
-
}
|
|
151
|
-
if (!seed.rollback) {
|
|
152
|
-
throw new DatabaseError(`Seed "${seed.name}" does not define a rollback operation.`);
|
|
153
|
-
}
|
|
154
|
-
await seed.rollback(transaction);
|
|
155
|
-
await this.deleteSeedRecord(transaction, seed.name);
|
|
156
|
-
rolledBack.push(record);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
return rolledBack;
|
|
123
|
+
rollbackAll() {
|
|
124
|
+
return this.rollbackSteps(Number.POSITIVE_INFINITY);
|
|
160
125
|
}
|
|
161
126
|
/**
|
|
162
|
-
* Creates the seed tracking table if it does not exist
|
|
127
|
+
* Creates the seed tracking table if it does not exist, adding the
|
|
128
|
+
* `sequence` column to tables created by earlier versions.
|
|
163
129
|
*/
|
|
164
130
|
async ensureSeedTable() {
|
|
165
|
-
const
|
|
131
|
+
const q = (id) => this.dialect.quoteIdentifier(id);
|
|
132
|
+
const { varchar255, timestamp } = this.dialect.types;
|
|
133
|
+
const ddl = `CREATE TABLE IF NOT EXISTS ${q(this.tableName)} (` +
|
|
134
|
+
`${q("name")} ${varchar255} PRIMARY KEY, ` +
|
|
135
|
+
`${q("applied_at")} ${timestamp} NOT NULL DEFAULT ${this.dialect.currentTimestamp}, ` +
|
|
136
|
+
`${q("sequence")} BIGSERIAL NOT NULL)`;
|
|
137
|
+
const upgrade = `ALTER TABLE ${q(this.tableName)} ` +
|
|
138
|
+
`ADD COLUMN IF NOT EXISTS ${q("sequence")} BIGSERIAL NOT NULL`;
|
|
166
139
|
try {
|
|
167
|
-
await this.client.
|
|
168
|
-
|
|
169
|
-
"name" VARCHAR(255) PRIMARY KEY,
|
|
170
|
-
"applied_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
171
|
-
)
|
|
172
|
-
`);
|
|
140
|
+
await this.client.executeRawUnsafe(ddl);
|
|
141
|
+
await this.client.executeRawUnsafe(upgrade);
|
|
173
142
|
}
|
|
174
143
|
catch (error) {
|
|
175
|
-
throw
|
|
144
|
+
throw this.seedError("Failed to initialize the seed tracking table.", {
|
|
176
145
|
cause: error,
|
|
177
|
-
metadata: {
|
|
178
|
-
tableName: this.tableName,
|
|
179
|
-
},
|
|
146
|
+
metadata: { tableName: this.tableName },
|
|
180
147
|
});
|
|
181
148
|
}
|
|
182
149
|
}
|
|
183
150
|
/**
|
|
184
|
-
* Returns all applied seeds.
|
|
151
|
+
* Returns all applied seeds ordered by execution sequence.
|
|
185
152
|
*/
|
|
186
153
|
async getAppliedSeeds(transaction) {
|
|
187
|
-
const
|
|
154
|
+
const q = (id) => this.dialect.quoteIdentifier(id);
|
|
155
|
+
const sql = `SELECT ${q("name")}, ${q("applied_at")}, ${q("sequence")} ` +
|
|
156
|
+
`FROM ${q(this.tableName)} ORDER BY ${q("sequence")} ASC`;
|
|
188
157
|
try {
|
|
189
|
-
|
|
190
|
-
if (transaction) {
|
|
191
|
-
rows = await transaction.$queryRawUnsafe(`
|
|
192
|
-
SELECT
|
|
193
|
-
"name",
|
|
194
|
-
"applied_at"
|
|
195
|
-
FROM ${table}
|
|
196
|
-
ORDER BY "applied_at" ASC, "name" ASC
|
|
197
|
-
`);
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
rows = await this.client.queryRaw(Prisma.sql `
|
|
201
|
-
SELECT
|
|
202
|
-
"name",
|
|
203
|
-
"applied_at"
|
|
204
|
-
FROM ${table}
|
|
205
|
-
ORDER BY "applied_at" ASC, "name" ASC
|
|
206
|
-
`);
|
|
207
|
-
}
|
|
158
|
+
const rows = await this.executor(transaction).query(sql);
|
|
208
159
|
return rows.map((row) => ({
|
|
209
160
|
name: row.name,
|
|
210
161
|
appliedAt: new Date(row.applied_at),
|
|
162
|
+
sequence: Number(row.sequence),
|
|
211
163
|
}));
|
|
212
164
|
}
|
|
213
165
|
catch (error) {
|
|
214
|
-
throw
|
|
166
|
+
throw this.seedError("Failed to read seed execution history.", {
|
|
215
167
|
cause: error,
|
|
216
|
-
metadata: {
|
|
217
|
-
tableName: this.tableName,
|
|
218
|
-
},
|
|
168
|
+
metadata: { tableName: this.tableName },
|
|
219
169
|
});
|
|
220
170
|
}
|
|
221
171
|
}
|
|
222
172
|
/**
|
|
223
|
-
*
|
|
173
|
+
* Alias of {@link getAppliedSeeds}.
|
|
224
174
|
*/
|
|
175
|
+
getHistory() {
|
|
176
|
+
return this.getAppliedSeeds();
|
|
177
|
+
}
|
|
178
|
+
async rollbackSteps(limit) {
|
|
179
|
+
await this.ensureSeedTable();
|
|
180
|
+
const rolledBack = [];
|
|
181
|
+
if (!this.perItemTransaction) {
|
|
182
|
+
await this.client.transaction(async (transaction) => {
|
|
183
|
+
await this.acquireSeedLock(transaction);
|
|
184
|
+
const applied = [...(await this.getAppliedSeeds(transaction))];
|
|
185
|
+
while (applied.length > 0 && rolledBack.length < limit) {
|
|
186
|
+
const record = applied.pop();
|
|
187
|
+
await this.revertRecord(transaction, record);
|
|
188
|
+
rolledBack.push(record);
|
|
189
|
+
}
|
|
190
|
+
}, this.buildTransactionOptions());
|
|
191
|
+
return rolledBack;
|
|
192
|
+
}
|
|
193
|
+
while (rolledBack.length < limit) {
|
|
194
|
+
const record = await this.client.transaction(async (transaction) => {
|
|
195
|
+
await this.acquireSeedLock(transaction);
|
|
196
|
+
const applied = await this.getAppliedSeeds(transaction);
|
|
197
|
+
const latest = applied[applied.length - 1];
|
|
198
|
+
if (!latest)
|
|
199
|
+
return null;
|
|
200
|
+
await this.revertRecord(transaction, latest);
|
|
201
|
+
return latest;
|
|
202
|
+
}, this.buildTransactionOptions());
|
|
203
|
+
if (!record)
|
|
204
|
+
break;
|
|
205
|
+
rolledBack.push(record);
|
|
206
|
+
}
|
|
207
|
+
return rolledBack;
|
|
208
|
+
}
|
|
209
|
+
async revertRecord(transaction, record) {
|
|
210
|
+
const seed = this.seeds.find((candidate) => candidate.name === record.name);
|
|
211
|
+
if (!seed) {
|
|
212
|
+
throw this.seedError(`Seed "${record.name}" is recorded as applied but is not registered.`, { metadata: { name: record.name } });
|
|
213
|
+
}
|
|
214
|
+
if (!seed.rollback) {
|
|
215
|
+
throw this.seedError(`Seed "${seed.name}" does not define a rollback operation.`, { metadata: { name: seed.name } });
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
await seed.rollback(transaction);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
throw this.seedError(`Rollback of seed "${seed.name}" failed.`, {
|
|
222
|
+
cause: error,
|
|
223
|
+
metadata: { name: seed.name },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
await this.deleteSeedRecord(transaction, seed.name);
|
|
227
|
+
}
|
|
228
|
+
computePending(applied) {
|
|
229
|
+
const names = new Set(applied.map((record) => record.name));
|
|
230
|
+
return this.seeds.filter((seed) => !names.has(seed.name));
|
|
231
|
+
}
|
|
232
|
+
buildTransactionOptions() {
|
|
233
|
+
return { ...this.transactionOptions };
|
|
234
|
+
}
|
|
235
|
+
executor(transaction) {
|
|
236
|
+
if (transaction) {
|
|
237
|
+
return {
|
|
238
|
+
query: (sql, ...values) => transaction.$queryRawUnsafe(sql, ...values),
|
|
239
|
+
execute: (sql, ...values) => transaction.$executeRawUnsafe(sql, ...values),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
query: (sql, ...values) => this.client.queryRawUnsafe(sql, values),
|
|
244
|
+
execute: (sql, ...values) => this.client.executeRawUnsafe(sql, values),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
225
247
|
async executeSeed(transaction, seed) {
|
|
226
248
|
try {
|
|
227
249
|
await seed.run(transaction);
|
|
228
250
|
}
|
|
229
251
|
catch (error) {
|
|
230
|
-
throw
|
|
252
|
+
throw this.seedError(`Seed "${seed.name}" failed.`, {
|
|
231
253
|
cause: error,
|
|
232
|
-
metadata: {
|
|
233
|
-
name: seed.name,
|
|
234
|
-
order: seed.order ?? 0,
|
|
235
|
-
},
|
|
254
|
+
metadata: { name: seed.name, order: seed.order ?? 0 },
|
|
236
255
|
});
|
|
237
256
|
}
|
|
238
257
|
}
|
|
239
|
-
/**
|
|
240
|
-
* Records a successfully executed seed.
|
|
241
|
-
*/
|
|
242
258
|
async recordSeed(transaction, seed) {
|
|
243
|
-
const
|
|
259
|
+
const q = (id) => this.dialect.quoteIdentifier(id);
|
|
260
|
+
const sql = `INSERT INTO ${q(this.tableName)} (${q("name")}) ` +
|
|
261
|
+
`VALUES (${this.dialect.placeholder(1)}) ` +
|
|
262
|
+
`RETURNING ${q("name")}, ${q("applied_at")}, ${q("sequence")}`;
|
|
244
263
|
try {
|
|
245
|
-
await transaction
|
|
246
|
-
|
|
247
|
-
("name")
|
|
248
|
-
VALUES
|
|
249
|
-
($1)
|
|
250
|
-
`, seed.name);
|
|
264
|
+
const rows = await this.executor(transaction).query(sql, seed.name);
|
|
265
|
+
const row = rows[0];
|
|
251
266
|
return {
|
|
252
267
|
name: seed.name,
|
|
253
|
-
appliedAt: new Date(),
|
|
268
|
+
appliedAt: row ? new Date(row.applied_at) : new Date(),
|
|
269
|
+
sequence: row ? Number(row.sequence) : 0,
|
|
254
270
|
};
|
|
255
271
|
}
|
|
256
272
|
catch (error) {
|
|
257
|
-
throw
|
|
273
|
+
throw this.seedError(`Failed to record seed "${seed.name}".`, {
|
|
258
274
|
cause: error,
|
|
259
|
-
metadata: {
|
|
260
|
-
name: seed.name,
|
|
261
|
-
},
|
|
275
|
+
metadata: { name: seed.name },
|
|
262
276
|
});
|
|
263
277
|
}
|
|
264
278
|
}
|
|
265
|
-
/**
|
|
266
|
-
* Removes a seed execution record.
|
|
267
|
-
*/
|
|
268
279
|
async deleteSeedRecord(transaction, name) {
|
|
269
|
-
const
|
|
280
|
+
const q = (id) => this.dialect.quoteIdentifier(id);
|
|
281
|
+
const sql = `DELETE FROM ${q(this.tableName)} ` +
|
|
282
|
+
`WHERE ${q("name")} = ${this.dialect.placeholder(1)}`;
|
|
270
283
|
try {
|
|
271
|
-
await transaction
|
|
272
|
-
DELETE FROM ${table}
|
|
273
|
-
WHERE "name" = $1
|
|
274
|
-
`, name);
|
|
284
|
+
await this.executor(transaction).execute(sql, name);
|
|
275
285
|
}
|
|
276
286
|
catch (error) {
|
|
277
|
-
throw
|
|
287
|
+
throw this.seedError(`Failed to remove seed record "${name}".`, {
|
|
278
288
|
cause: error,
|
|
279
|
-
metadata: {
|
|
280
|
-
name,
|
|
281
|
-
},
|
|
289
|
+
metadata: { name },
|
|
282
290
|
});
|
|
283
291
|
}
|
|
284
292
|
}
|
|
285
|
-
/**
|
|
286
|
-
* Acquires the advisory lock used for seed execution.
|
|
287
|
-
*/
|
|
288
293
|
async acquireSeedLock(transaction) {
|
|
289
294
|
try {
|
|
290
|
-
|
|
291
|
-
await transaction.$executeRaw `
|
|
292
|
-
SELECT pg_advisory_xact_lock(
|
|
293
|
-
${lock}
|
|
294
|
-
)
|
|
295
|
-
`;
|
|
295
|
+
await this.executor(transaction).execute(this.dialect.advisoryTransactionLock(), hashLockKey(this.lockKey));
|
|
296
296
|
}
|
|
297
297
|
catch (error) {
|
|
298
|
-
throw
|
|
298
|
+
throw this.seedError("Failed to acquire the database seed lock.", {
|
|
299
299
|
cause: error,
|
|
300
|
-
metadata: {
|
|
301
|
-
lockKey: this.lockKey,
|
|
302
|
-
},
|
|
300
|
+
metadata: { lockKey: this.lockKey },
|
|
303
301
|
});
|
|
304
302
|
}
|
|
305
303
|
}
|
|
304
|
+
seedError(message, options = {}) {
|
|
305
|
+
if (isDatabaseErrorLike(options.cause) && !options.metadata) {
|
|
306
|
+
return options.cause;
|
|
307
|
+
}
|
|
308
|
+
return new DatabaseError(message, {
|
|
309
|
+
...options,
|
|
310
|
+
operation: DatabaseOperation.MIGRATION,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function hasName(records, name) {
|
|
315
|
+
return records.some((record) => record.name === name);
|
|
306
316
|
}
|
|
307
317
|
/**
|
|
308
318
|
* Creates a seed runner.
|
|
@@ -311,7 +321,7 @@ export function createSeedRunner(client, seeds, options) {
|
|
|
311
321
|
return new SeedRunner(client, seeds, options);
|
|
312
322
|
}
|
|
313
323
|
/**
|
|
314
|
-
* Validates and sorts seed definitions.
|
|
324
|
+
* Validates and sorts seed definitions (stable sort on `order`).
|
|
315
325
|
*/
|
|
316
326
|
export function normalizeSeeds(seeds) {
|
|
317
327
|
if (!Array.isArray(seeds)) {
|
|
@@ -320,9 +330,7 @@ export function normalizeSeeds(seeds) {
|
|
|
320
330
|
const normalized = seeds
|
|
321
331
|
.map((seed) => {
|
|
322
332
|
validateSeed(seed);
|
|
323
|
-
return Object.freeze({
|
|
324
|
-
...seed,
|
|
325
|
-
});
|
|
333
|
+
return Object.freeze({ ...seed });
|
|
326
334
|
})
|
|
327
335
|
.sort((first, second) => (first.order ?? 0) - (second.order ?? 0));
|
|
328
336
|
const names = new Set();
|
|
@@ -352,9 +360,6 @@ export function validateSeed(seed) {
|
|
|
352
360
|
throw new TypeError(`Seed "${seed.name}" has an invalid rollback function.`);
|
|
353
361
|
}
|
|
354
362
|
}
|
|
355
|
-
/**
|
|
356
|
-
* Validates a seed name.
|
|
357
|
-
*/
|
|
358
363
|
function validateSeedName(name) {
|
|
359
364
|
if (typeof name !== "string" || name.trim().length === 0) {
|
|
360
365
|
throw new TypeError("Seed name is required.");
|
|
@@ -363,40 +368,4 @@ function validateSeedName(name) {
|
|
|
363
368
|
throw new TypeError("Seed name cannot exceed 255 characters.");
|
|
364
369
|
}
|
|
365
370
|
}
|
|
366
|
-
/**
|
|
367
|
-
* Validates an SQL identifier.
|
|
368
|
-
*/
|
|
369
|
-
function validateIdentifier(identifier, name) {
|
|
370
|
-
if (typeof identifier !== "string" ||
|
|
371
|
-
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
372
|
-
throw new TypeError(`Invalid ${name}: "${identifier}".`);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Validates an advisory lock key.
|
|
377
|
-
*/
|
|
378
|
-
function validateLockKey(lockKey) {
|
|
379
|
-
if (typeof lockKey !== "string" || lockKey.trim().length === 0) {
|
|
380
|
-
throw new TypeError("A database seed lock key is required.");
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Quotes a validated SQL identifier.
|
|
385
|
-
*/
|
|
386
|
-
function quoteIdentifier(identifier) {
|
|
387
|
-
validateIdentifier(identifier, "identifier");
|
|
388
|
-
return `"${identifier}"`;
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Generates a deterministic signed 64-bit advisory lock key.
|
|
392
|
-
*/
|
|
393
|
-
function hashLockKey(value) {
|
|
394
|
-
let hash = 1469598103934665603n;
|
|
395
|
-
const bytes = new TextEncoder().encode(value);
|
|
396
|
-
for (const byte of bytes) {
|
|
397
|
-
hash ^= BigInt(byte);
|
|
398
|
-
hash = BigInt.asIntN(64, hash * 1099511628211n);
|
|
399
|
-
}
|
|
400
|
-
return BigInt.asIntN(64, hash);
|
|
401
|
-
}
|
|
402
371
|
//# sourceMappingURL=seed.runner.js.map
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Managed transaction execution with retry support.
|
|
5
5
|
*/
|
|
6
|
-
export { TransactionManager, createTransactionManager, withTransaction, withTransactionRetry, createTransactionContext, createTransactionId, isTransactionActive, isTransactionCommitted, isTransactionFailed, type TransactionStatus, type TransactionContext, type ManagedTransactionOptions, } from "./transaction.core.js";
|
|
6
|
+
export { TransactionManager, createTransactionManager, withTransaction, withTransactionRetry, createTransactionContext, createTransactionId, getTransactionContextFromError, isTransactionActive, isTransactionCommitted, isTransactionFailed, type TransactionStatus, type TransactionContext, type TransactionOutcome, type TransactionRetryOptions, type ManagedTransactionOptions, } from "./transaction.core.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Managed transaction execution with retry support.
|
|
5
5
|
*/
|
|
6
|
-
export { TransactionManager, createTransactionManager, withTransaction, withTransactionRetry, createTransactionContext, createTransactionId, isTransactionActive, isTransactionCommitted, isTransactionFailed, } from "./transaction.core.js";
|
|
6
|
+
export { TransactionManager, createTransactionManager, withTransaction, withTransactionRetry, createTransactionContext, createTransactionId, getTransactionContextFromError, isTransactionActive, isTransactionCommitted, isTransactionFailed, } from "./transaction.core.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|