@xenon-device-management/xenon 2.16.0 → 2.17.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/README.md +2 -2
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-2W4JvEyZ.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-GmckX_7r.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DqODzkyo.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-7hETKEWb.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-GOgssFEP.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-Yvf6RGMd.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-KrPnfdsf.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-C6WjZFeI.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CSRIhZEh.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CB8ST3nE.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C9o90KId.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-CPm3w9l2.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-DwIgERRt.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-DrLbHifn.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-DU_Ls06P.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-CWYdEzJ_.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BqNVdln7.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BtB0_Jg_.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-C8NhdtyN.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-gMg0ffvv.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CiYZ3N4M.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-EaaqDmLN.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-DDau4Axv.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-DQ83B1ed.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-K1ovTQam.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-Biq00pMt.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CBv88cH7.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-Bmltailn.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-BPrld-3h.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-t0QB14JI.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BgGZ8AvV.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-BroHxNOh.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-Bjmf49i6.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-DeD__6wa.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/{index-B0gQkEg0.js → index-Dk5J6HB1.js} +2 -2
- package/lib/public/assets/{index-B0JeuZbg.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-72oF89LJ.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-DIc4MSC7.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-BcWm2vW8.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-B06IJJad.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-pVLvnN30.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-CN77vy--.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CSdzQSkY.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-BtrN5IkI.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CNsQfo6w.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-BSfA8ZxG.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-BfTebCK5.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-D8KUX6Qg.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-Cvc8xzic.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-C9Zj84UF.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-Bi27STgs.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-CPsOuwSu.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DEQ-DBOX.js → runbook-page-CWLkC1Fd.js} +1 -1
- package/lib/public/assets/{select-BWpnhWXj.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BefSCSpe.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-MSk96Zz7.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/{session-detail-page-DPyBNef_.js → session-detail-page-C0wYaXqy.js} +1 -1
- package/lib/public/assets/{settings-V6x-cXwy.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-Duex-GZD.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BBcoqBmL.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-ts8p5rEh.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-CZ2ew6AZ.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-Dv99y_le.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-DrYC8JCF.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C7QkBNkP.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-gJ5Y01yo.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D7tKMESH.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-CqZVuZGs.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-DI5DSW99.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-ClcHFxfo.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +1 -1
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/dashboard/event-manager.js +5 -8
- package/lib/src/device-utils.js +11 -2
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/plugin.js +4 -1
- package/lib/src/scripts/initialize-database.js +0 -6
- package/lib/src/scripts/run-migrations.js +123 -880
- package/lib/src/services/SessionLifecycleService.js +20 -5
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +199 -44
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/sessions/RemoteSession.js +11 -2
- package/lib/src/sessions/SessionManager.js +28 -1
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +15 -0
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- package/lib/test/unit/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/run-migrations-database.spec.js +57 -424
- package/lib/test/unit/run-migrations.test.js +74 -674
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +32 -94
- package/lib/test/unit/session-log-indexes.spec.js +15 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/schema.json +1 -1
|
@@ -19,7 +19,6 @@ const os_1 = __importDefault(require("os"));
|
|
|
19
19
|
const path_1 = __importDefault(require("path"));
|
|
20
20
|
const client_1 = require("../../src/generated/client");
|
|
21
21
|
const config_1 = require("../../src/config");
|
|
22
|
-
const sinon_1 = __importDefault(require("sinon"));
|
|
23
22
|
const run_migrations_1 = require("../../src/scripts/run-migrations");
|
|
24
23
|
// The startup schema step, run for real: the prisma CLI from node_modules
|
|
25
24
|
// against scratch SQLite files, never the server's own database.
|
|
@@ -52,8 +51,8 @@ function prismaCli(args, file, input) {
|
|
|
52
51
|
function sql(file, statements) {
|
|
53
52
|
prismaCli(['db', 'execute', '--url', `file:${file}`, '--stdin'], file, statements);
|
|
54
53
|
}
|
|
55
|
-
/** Whether the database's tables are exactly what
|
|
56
|
-
function matchesSchema(file
|
|
54
|
+
/** Whether the database's tables are exactly what prisma/schema.prisma says. */
|
|
55
|
+
function matchesSchema(file) {
|
|
57
56
|
try {
|
|
58
57
|
prismaCli([
|
|
59
58
|
'migrate',
|
|
@@ -61,7 +60,7 @@ function matchesSchema(file, schema = SCHEMA) {
|
|
|
61
60
|
'--from-url',
|
|
62
61
|
`file:${file}`,
|
|
63
62
|
'--to-schema-datamodel',
|
|
64
|
-
|
|
63
|
+
SCHEMA,
|
|
65
64
|
'--exit-code',
|
|
66
65
|
], file);
|
|
67
66
|
return true;
|
|
@@ -101,21 +100,18 @@ function recordedHistory(file) {
|
|
|
101
100
|
return rows;
|
|
102
101
|
});
|
|
103
102
|
}
|
|
104
|
-
// A selector fingerprint too, so a migration that changes LocatorEtalon meets a row.
|
|
105
103
|
const SEED = `
|
|
106
104
|
INSERT INTO "Team" ("id", "name", "createdAt") VALUES ('team-1', 'Payments', 1759600000000);
|
|
107
105
|
INSERT INTO "Build" ("id", "name", "createdAt", "updatedAt") VALUES ('build-1', 'nightly #42', 1759600000000, 1759600000000);
|
|
108
|
-
INSERT INTO "LocatorEtalon" ("id", "selector", "strategy", "attributes", "nodeName", "lastSeen", "createdAt", "updatedAt") VALUES ('et-1', '//b', 'xpath', '{}', 'Button', 1759600000000, 1759600000000, 1759600000000);
|
|
109
106
|
`;
|
|
110
107
|
function seededRows(file) {
|
|
111
108
|
return __awaiter(this, void 0, void 0, function* () {
|
|
112
109
|
const teams = yield query(file, 'SELECT id, name FROM "Team"');
|
|
113
110
|
const builds = yield query(file, 'SELECT id, name FROM "Build"');
|
|
114
|
-
|
|
115
|
-
return [...teams, ...builds, ...etalons].map((r) => `${r.id}=${r.name}`).sort();
|
|
111
|
+
return [...teams, ...builds].map((r) => `${r.id}=${r.name}`).sort();
|
|
116
112
|
});
|
|
117
113
|
}
|
|
118
|
-
const SEEDED = ['build-1=nightly #42', '
|
|
114
|
+
const SEEDED = ['build-1=nightly #42', 'team-1=Payments'];
|
|
119
115
|
describe('runMigrations against real databases', function () {
|
|
120
116
|
// Each start runs the prisma CLI two or three times.
|
|
121
117
|
this.timeout(120000);
|
|
@@ -233,53 +229,25 @@ describe('runMigrations against real databases', function () {
|
|
|
233
229
|
yield start(file, 'postgresql');
|
|
234
230
|
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
235
231
|
}));
|
|
236
|
-
|
|
237
|
-
function expectFullHistory(file_1) {
|
|
238
|
-
return __awaiter(this, arguments, void 0, function* (file, migrations = LOCAL_MIGRATIONS) {
|
|
239
|
-
const history = yield recordedHistory(file);
|
|
240
|
-
const applied = history.filter((r) => r.finished_at && !r.rolled_back_at);
|
|
241
|
-
(0, chai_1.expect)(applied.map((r) => r.migration_name).sort()).to.deep.equal(migrations);
|
|
242
|
-
(0, chai_1.expect)(history.filter((r) => !r.finished_at && !r.rolled_back_at)).to.deep.equal([]);
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
/** The copies a start tries the migrations on, which it must not leave behind. */
|
|
246
|
-
function trialCopiesOf(file) {
|
|
247
|
-
const prefix = `${path_1.default.basename(file)}.xenon-trial-`;
|
|
248
|
-
return fs_1.default.readdirSync(path_1.default.dirname(file)).filter((n) => n.startsWith(prefix));
|
|
249
|
-
}
|
|
250
|
-
// Re-baselining: db push writes no history, so these two stayed on db push
|
|
251
|
-
// for good, and refused at the first release whose migration drops a column
|
|
252
|
-
// or adds a unique index (see the next releases below). Now their history
|
|
253
|
-
// records what the tables have, and migrate deploy takes over.
|
|
254
|
-
it('records what the tables of a database ahead of its history have, and puts it back on migrate deploy', () => __awaiter(this, void 0, void 0, function* () {
|
|
232
|
+
it('starts a database whose tables are ahead of its history, without recording a failed migration', () => __awaiter(this, void 0, void 0, function* () {
|
|
255
233
|
const file = copyOf(historyBehind);
|
|
256
234
|
yield start(file, 'postgresql');
|
|
257
235
|
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
258
236
|
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
259
|
-
yield
|
|
260
|
-
(0, chai_1.expect)(
|
|
237
|
+
const history = yield recordedHistory(file);
|
|
238
|
+
(0, chai_1.expect)(history.filter((r) => !r.finished_at && !r.rolled_back_at)).to.deep.equal([]);
|
|
261
239
|
}));
|
|
262
|
-
it('starts a database whose history records a failed migration
|
|
240
|
+
it('starts a database whose history records a failed migration', () => __awaiter(this, void 0, void 0, function* () {
|
|
263
241
|
const file = copyOf(historyFailed);
|
|
264
242
|
yield start(file, 'postgresql');
|
|
265
243
|
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
266
244
|
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
267
|
-
yield expectFullHistory(file);
|
|
268
|
-
}));
|
|
269
|
-
it('records a migration that was rolled back when the tables have it', () => __awaiter(this, void 0, void 0, function* () {
|
|
270
|
-
const file = copyOf(historyFailed);
|
|
271
|
-
prismaCli(['migrate', 'resolve', '--rolled-back', LOCAL_MIGRATIONS[LOCAL_MIGRATIONS.length - 1]], file);
|
|
272
|
-
yield start(file, 'sqlite');
|
|
273
|
-
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
274
|
-
yield expectFullHistory(file);
|
|
275
245
|
}));
|
|
276
246
|
// A history database whose tables differ from its migrations for another
|
|
277
247
|
// reason than db push: a table with a row and an index added by hand. `db
|
|
278
248
|
// push --accept-data-loss` would drop them, and the default setting did.
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
// without the flag refuses to drop the table, and the start stops with
|
|
282
|
-
// commands to paste. The cases that stop follow them as an operator would.
|
|
249
|
+
// Without the flag, db push refuses to drop the table, and the start stops
|
|
250
|
+
// with commands to paste. Each case below follows them as an operator would.
|
|
283
251
|
const HAND = 'CREATE TABLE "LabNotes" ("id" INTEGER PRIMARY KEY, "note" TEXT); ' +
|
|
284
252
|
'INSERT INTO "LabNotes" ("note") VALUES (\'keep me\'); ' +
|
|
285
253
|
'CREATE INDEX "lab_team_created" ON "Team"("createdAt");';
|
|
@@ -298,7 +266,7 @@ describe('runMigrations against real databases', function () {
|
|
|
298
266
|
function commandsIn(message) {
|
|
299
267
|
return message
|
|
300
268
|
.split('\n')
|
|
301
|
-
.filter((line) => /^ {2}(DATABASE_URL=|echo
|
|
269
|
+
.filter((line) => /^ {2}(DATABASE_URL=|echo )/.test(line))
|
|
302
270
|
.map((line) => line.trim());
|
|
303
271
|
}
|
|
304
272
|
/** Runs a command from a message as an operator would paste it into a shell. */
|
|
@@ -328,13 +296,13 @@ describe('runMigrations against real databases', function () {
|
|
|
328
296
|
}
|
|
329
297
|
const KEPT = { notes: ['keep me'], index: true };
|
|
330
298
|
const GONE = { notes: [], index: false };
|
|
331
|
-
/** Whether the tables are
|
|
332
|
-
function matchesSchemaWithHandAdded(file
|
|
299
|
+
/** Whether the tables are this version's schema plus what was added by hand. */
|
|
300
|
+
function matchesSchemaWithHandAdded(file) {
|
|
333
301
|
const probe = `${file}.probe`;
|
|
334
302
|
fs_1.default.copyFileSync(file, probe);
|
|
335
303
|
try {
|
|
336
|
-
sql(probe, 'DROP TABLE
|
|
337
|
-
return matchesSchema(probe
|
|
304
|
+
sql(probe, 'DROP TABLE "LabNotes"; DROP INDEX "lab_team_created";');
|
|
305
|
+
return matchesSchema(probe);
|
|
338
306
|
}
|
|
339
307
|
finally {
|
|
340
308
|
fs_1.default.rmSync(probe, { force: true });
|
|
@@ -346,64 +314,63 @@ describe('runMigrations against real databases', function () {
|
|
|
346
314
|
return history.filter((r) => !r.finished_at && !r.rolled_back_at).map((r) => r.migration_name);
|
|
347
315
|
});
|
|
348
316
|
}
|
|
349
|
-
it('
|
|
317
|
+
it('drops nothing added by hand to a history database, and its copy-first advice keeps it', () => __awaiter(this, void 0, void 0, function* () {
|
|
350
318
|
const file = copyOf(withHistory);
|
|
351
319
|
sql(file, HAND);
|
|
320
|
+
const message = yield startStops(file);
|
|
321
|
+
(0, chai_1.expect)(message).to.match(/stopped rather than delete/);
|
|
322
|
+
(0, chai_1.expect)(message).to.include('LabNotes');
|
|
323
|
+
(0, chai_1.expect)(yield handAdded(file)).to.deep.equal(KEPT);
|
|
324
|
+
(0, chai_1.expect)(yield recordedHistory(file)).to.have.length(LOCAL_MIGRATIONS.length - 1);
|
|
325
|
+
// Back up, copy, deploy on the copy, then on the file, as it says.
|
|
326
|
+
const commands = commandsIn(message);
|
|
327
|
+
const backup = commands.find((c) => c.includes('.backup-'));
|
|
328
|
+
const copy = commands.find((c) => c.includes('VACUUM INTO') && c !== backup);
|
|
329
|
+
const copyUrl = `file:${vacuumTarget(copy)}`;
|
|
330
|
+
const onCopy = commands.find((c) => c.startsWith(`DATABASE_URL=${copyUrl} `));
|
|
331
|
+
const onFile = commands.find((c) => c.startsWith(`DATABASE_URL=file:${file} `) && c.includes('migrate deploy'));
|
|
332
|
+
(0, chai_1.expect)([backup, copy, onCopy, onFile].every(Boolean), message).to.equal(true);
|
|
333
|
+
paste(backup);
|
|
334
|
+
paste(copy);
|
|
335
|
+
paste(onCopy);
|
|
336
|
+
paste(onFile);
|
|
337
|
+
fs_1.default.rmSync(vacuumTarget(copy));
|
|
352
338
|
yield start(file, 'sqlite');
|
|
353
339
|
(0, chai_1.expect)(yield handAdded(file)).to.deep.equal(KEPT);
|
|
340
|
+
(0, chai_1.expect)(yield handAdded(vacuumTarget(backup))).to.deep.equal(KEPT);
|
|
354
341
|
(0, chai_1.expect)(matchesSchemaWithHandAdded(file)).to.equal(true);
|
|
355
342
|
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
356
|
-
yield
|
|
357
|
-
(0, chai_1.expect)(trialCopiesOf(file), 'the copy is gone').to.deep.equal([]);
|
|
358
|
-
yield start(file, 'sqlite');
|
|
359
|
-
(0, chai_1.expect)(yield handAdded(file)).to.deep.equal(KEPT);
|
|
360
|
-
}));
|
|
361
|
-
// db push drops an index without asking, with or without the flag.
|
|
362
|
-
it('keeps an index added by hand to a history database', () => __awaiter(this, void 0, void 0, function* () {
|
|
363
|
-
const file = copyOf(withHistory);
|
|
364
|
-
sql(file, 'CREATE INDEX "lab_team_created" ON "Team"("createdAt");');
|
|
365
|
-
yield start(file, 'sqlite');
|
|
366
|
-
(0, chai_1.expect)((yield handAdded(file)).index).to.equal(true);
|
|
367
|
-
yield expectFullHistory(file);
|
|
343
|
+
(0, chai_1.expect)(yield recordedHistory(file)).to.have.length(LOCAL_MIGRATIONS.length);
|
|
368
344
|
}));
|
|
369
|
-
// Tables ahead of the history and a table added by hand:
|
|
370
|
-
//
|
|
371
|
-
// the
|
|
345
|
+
// Tables ahead of the history and a table added by hand: migrate deploy
|
|
346
|
+
// fails on the change already there (P3018) and records it as failed, so
|
|
347
|
+
// the advice tries it on a copy, which fails there and leaves the file be.
|
|
372
348
|
it('never sends migrate deploy to a database whose tables are ahead of its history', () => __awaiter(this, void 0, void 0, function* () {
|
|
373
349
|
const file = copyOf(historyBehind);
|
|
374
350
|
sql(file, HAND);
|
|
375
|
-
// Not what the umask gives: the backup gets the file's own.
|
|
376
|
-
fs_1.default.chmodSync(file, 0o640);
|
|
377
351
|
const message = yield startStops(file);
|
|
378
352
|
(0, chai_1.expect)(message).to.match(/stopped rather than delete/);
|
|
379
|
-
(0, chai_1.expect)(message).to.match(/failed on a copy/);
|
|
380
353
|
(0, chai_1.expect)(message).to.include('LabNotes');
|
|
381
|
-
(0, chai_1.expect)(yield failedMigrations(file), 'the file records no failed migration').to.deep.equal([]);
|
|
382
|
-
(0, chai_1.expect)(yield handAdded(file)).to.deep.equal(KEPT);
|
|
383
|
-
(0, chai_1.expect)(trialCopiesOf(file)).to.deep.equal([]);
|
|
384
354
|
const commands = commandsIn(message);
|
|
385
|
-
|
|
386
|
-
|
|
355
|
+
const backup = commands.find((c) => c.includes('.backup-'));
|
|
356
|
+
const copy = commands.find((c) => c.includes('VACUUM INTO') && c !== backup);
|
|
357
|
+
const copyUrl = `file:${vacuumTarget(copy)}`;
|
|
358
|
+
const onCopy = commands.find((c) => c.startsWith(`DATABASE_URL=${copyUrl} `));
|
|
387
359
|
const letGo = commands.find((c) => c.includes('--accept-data-loss'));
|
|
388
|
-
(0, chai_1.expect)(letGo, message).to.
|
|
389
|
-
(0, chai_1.expect)(letGo).to.
|
|
390
|
-
(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
(0, chai_1.expect)(yield handAdded(file)).to.deep.equal(KEPT);
|
|
397
|
-
fs_1.default.rmSync(backup);
|
|
398
|
-
// Let it go, as it says, and start: the history then catches up.
|
|
360
|
+
(0, chai_1.expect)([backup, copy, onCopy, letGo].every(Boolean), message).to.equal(true);
|
|
361
|
+
(0, chai_1.expect)(letGo).to.equal(`DATABASE_URL=file:${file} ${PRISMA} db push --skip-generate --accept-data-loss --schema ${SCHEMA}`);
|
|
362
|
+
paste(backup);
|
|
363
|
+
paste(copy);
|
|
364
|
+
(0, chai_1.expect)(() => paste(onCopy)).to.throw(/P3018/);
|
|
365
|
+
fs_1.default.rmSync(vacuumTarget(copy));
|
|
366
|
+
(0, chai_1.expect)(yield failedMigrations(file), 'the file records no failed migration').to.deep.equal([]);
|
|
367
|
+
// Let it go, as the message says when the copy fails.
|
|
399
368
|
paste(letGo);
|
|
400
|
-
(0, chai_1.expect)(fs_1.default.statSync(backup).mode & 0o777).to.equal(0o640);
|
|
401
369
|
yield start(file, 'sqlite');
|
|
402
370
|
(0, chai_1.expect)(yield handAdded(file)).to.deep.equal(GONE);
|
|
403
|
-
(0, chai_1.expect)(yield handAdded(backup)).to.deep.equal(KEPT);
|
|
371
|
+
(0, chai_1.expect)(yield handAdded(vacuumTarget(backup))).to.deep.equal(KEPT);
|
|
404
372
|
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
405
373
|
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
406
|
-
yield expectFullHistory(file);
|
|
407
374
|
}));
|
|
408
375
|
// A failed migration recorded: migrate deploy stops at once (P3009).
|
|
409
376
|
it('never suggests migrate deploy for a history that records a failed migration', () => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -414,353 +381,19 @@ describe('runMigrations against real databases', function () {
|
|
|
414
381
|
(0, chai_1.expect)(message).to.match(/stopped rather than delete/);
|
|
415
382
|
(0, chai_1.expect)(message).to.include(newest);
|
|
416
383
|
(0, chai_1.expect)(message).to.include('LabNotes');
|
|
417
|
-
(0, chai_1.expect)(yield failedMigrations(file)).to.deep.equal([newest]);
|
|
418
384
|
const commands = commandsIn(message);
|
|
419
385
|
(0, chai_1.expect)(commands.filter((c) => c.includes('migrate deploy'))).to.deep.equal([]);
|
|
386
|
+
const backup = commands.find((c) => c.includes('.backup-'));
|
|
420
387
|
const letGo = commands.find((c) => c.includes('--accept-data-loss'));
|
|
421
|
-
(0, chai_1.expect)(letGo, message).to.
|
|
388
|
+
(0, chai_1.expect)([backup, letGo].every(Boolean), message).to.equal(true);
|
|
389
|
+
paste(backup);
|
|
422
390
|
paste(letGo);
|
|
423
391
|
yield start(file, 'sqlite');
|
|
424
392
|
(0, chai_1.expect)(yield handAdded(file)).to.deep.equal(GONE);
|
|
425
|
-
(0, chai_1.expect)(yield handAdded(vacuumTarget(
|
|
393
|
+
(0, chai_1.expect)(yield handAdded(vacuumTarget(backup))).to.deep.equal(KEPT);
|
|
426
394
|
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
427
395
|
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
428
|
-
yield expectFullHistory(file);
|
|
429
396
|
}));
|
|
430
|
-
// A start that died while it tried the migrations on a copy left the copy.
|
|
431
|
-
it('removes a copy a start that died left behind', () => __awaiter(this, void 0, void 0, function* () {
|
|
432
|
-
const file = copyOf(withHistory);
|
|
433
|
-
const leftover = `${file}.xenon-trial-999993`;
|
|
434
|
-
fs_1.default.copyFileSync(file, leftover);
|
|
435
|
-
fs_1.default.writeFileSync(`${leftover}-journal`, '');
|
|
436
|
-
// Signal 0 only asks whether a process is there; stubbed all the same.
|
|
437
|
-
const kill = sinon_1.default.stub(process, 'kill').callsFake((() => {
|
|
438
|
-
throw Object.assign(new Error('kill ESRCH'), { code: 'ESRCH' });
|
|
439
|
-
}));
|
|
440
|
-
try {
|
|
441
|
-
yield start(file, 'sqlite');
|
|
442
|
-
}
|
|
443
|
-
finally {
|
|
444
|
-
kill.restore();
|
|
445
|
-
}
|
|
446
|
-
(0, chai_1.expect)(trialCopiesOf(file)).to.deep.equal([]);
|
|
447
|
-
}));
|
|
448
|
-
// The two kinds of migration `db push` without --accept-data-loss refuses
|
|
449
|
-
// on a table with rows: one that drops a column, one that adds a unique
|
|
450
|
-
// index. Each is made as a release after this one would make it, with
|
|
451
|
-
// Prisma's own migration for the change.
|
|
452
|
-
describe('the next release', () => {
|
|
453
|
-
/**
|
|
454
|
-
* A plugin root for a release after this one: this schema, edited, and its
|
|
455
|
-
* migration. The migration is Prisma's own for the edit, unless `script`
|
|
456
|
-
* gives one written by hand (a backfill changes rows, and no table).
|
|
457
|
-
*/
|
|
458
|
-
function nextRelease(name, edit, script, migrationName = `29990101000000_${name}`) {
|
|
459
|
-
const root = path_1.default.join(dir, `next-${name}`);
|
|
460
|
-
fs_1.default.mkdirSync(path_1.default.join(root, 'prisma'), { recursive: true });
|
|
461
|
-
fs_1.default.cpSync(MIGRATIONS, path_1.default.join(root, 'prisma', 'migrations'), { recursive: true });
|
|
462
|
-
const before = fs_1.default.readFileSync(SCHEMA, 'utf8');
|
|
463
|
-
const after = edit(before);
|
|
464
|
-
if (script === undefined)
|
|
465
|
-
(0, chai_1.expect)(after, 'the edit changes the schema').to.not.equal(before);
|
|
466
|
-
const schema = path_1.default.join(root, 'prisma', 'schema.prisma');
|
|
467
|
-
fs_1.default.writeFileSync(schema, after);
|
|
468
|
-
script !== null && script !== void 0 ? script : (script = (0, child_process_1.execFileSync)(PRISMA, [
|
|
469
|
-
'migrate',
|
|
470
|
-
'diff',
|
|
471
|
-
'--from-schema-datamodel',
|
|
472
|
-
SCHEMA,
|
|
473
|
-
'--to-schema-datamodel',
|
|
474
|
-
schema,
|
|
475
|
-
'--script',
|
|
476
|
-
], { cwd: ROOT, env: Object.assign(Object.assign({}, process.env), { CHECKPOINT_DISABLE: '1' }), stdio: 'pipe' }).toString());
|
|
477
|
-
const migration = path_1.default.join(root, 'prisma', 'migrations', migrationName);
|
|
478
|
-
fs_1.default.mkdirSync(migration);
|
|
479
|
-
fs_1.default.writeFileSync(path_1.default.join(migration, 'migration.sql'), script);
|
|
480
|
-
fs_1.default.symlinkSync(path_1.default.join(ROOT, 'node_modules'), path_1.default.join(root, 'node_modules'));
|
|
481
|
-
return root;
|
|
482
|
-
}
|
|
483
|
-
let dropColumn;
|
|
484
|
-
let addUnique;
|
|
485
|
-
/** This release's database with its full history. */
|
|
486
|
-
let current;
|
|
487
|
-
/** This release's database with its full history, and a table and index added by hand. */
|
|
488
|
-
let currentWithHand;
|
|
489
|
-
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
490
|
-
dropColumn = nextRelease('drop_attributes', (schema) => schema.replace(/(model LocatorEtalon \{[^}]*?)\n {2}attributes +String\n/, '$1\n'));
|
|
491
|
-
addUnique = nextRelease('unique_strategy_node', (schema) => schema.replace(/(model LocatorEtalon \{[^}]*?)\n\}/, '$1\n\n @@unique([strategy, nodeName])\n}'));
|
|
492
|
-
current = path_1.default.join(dir, 'current.db');
|
|
493
|
-
fs_1.default.copyFileSync(withHistory, current);
|
|
494
|
-
prismaCli(['migrate', 'deploy', '--schema', SCHEMA], current);
|
|
495
|
-
currentWithHand = path_1.default.join(dir, 'current-with-hand.db');
|
|
496
|
-
fs_1.default.copyFileSync(current, currentWithHand);
|
|
497
|
-
sql(currentWithHand, HAND);
|
|
498
|
-
}));
|
|
499
|
-
function startNext(file, root) {
|
|
500
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
501
|
-
config_1.config.databaseUrl = `file:${file}`;
|
|
502
|
-
config_1.config.databaseProvider = 'sqlite';
|
|
503
|
-
yield (0, run_migrations_1.runMigrations)(undefined, undefined, { root });
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
const migrationsOf = (root) => fs_1.default
|
|
507
|
-
.readdirSync(path_1.default.join(root, 'prisma', 'migrations'))
|
|
508
|
-
.filter((n) => !n.endsWith('.toml'))
|
|
509
|
-
.sort();
|
|
510
|
-
for (const [label, rootOf] of [
|
|
511
|
-
['drops a column', () => dropColumn],
|
|
512
|
-
['adds a unique index', () => addUnique],
|
|
513
|
-
]) {
|
|
514
|
-
describe(`that ${label}`, () => {
|
|
515
|
-
it('updates a database ahead of its history with migrate deploy', () => __awaiter(this, void 0, void 0, function* () {
|
|
516
|
-
const root = rootOf();
|
|
517
|
-
const file = copyOf(historyBehind);
|
|
518
|
-
yield startNext(file, root);
|
|
519
|
-
(0, chai_1.expect)(matchesSchema(file, path_1.default.join(root, 'prisma', 'schema.prisma'))).to.equal(true);
|
|
520
|
-
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
521
|
-
yield expectFullHistory(file, migrationsOf(root));
|
|
522
|
-
}));
|
|
523
|
-
it('updates a database with a failed migration recorded with migrate deploy', () => __awaiter(this, void 0, void 0, function* () {
|
|
524
|
-
const root = rootOf();
|
|
525
|
-
const file = copyOf(historyFailed);
|
|
526
|
-
yield startNext(file, root);
|
|
527
|
-
(0, chai_1.expect)(matchesSchema(file, path_1.default.join(root, 'prisma', 'schema.prisma'))).to.equal(true);
|
|
528
|
-
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
529
|
-
yield expectFullHistory(file, migrationsOf(root));
|
|
530
|
-
}));
|
|
531
|
-
it('keeps what was added by hand to a database with its full history', () => __awaiter(this, void 0, void 0, function* () {
|
|
532
|
-
const root = rootOf();
|
|
533
|
-
const file = copyOf(currentWithHand);
|
|
534
|
-
yield startNext(file, root);
|
|
535
|
-
(0, chai_1.expect)(yield handAdded(file)).to.deep.equal(KEPT);
|
|
536
|
-
(0, chai_1.expect)(matchesSchemaWithHandAdded(file, path_1.default.join(root, 'prisma', 'schema.prisma'))).to.equal(true);
|
|
537
|
-
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
538
|
-
yield expectFullHistory(file, migrationsOf(root));
|
|
539
|
-
}));
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
function stopsNext(file, root) {
|
|
543
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
544
|
-
try {
|
|
545
|
-
yield startNext(file, root);
|
|
546
|
-
}
|
|
547
|
-
catch (e) {
|
|
548
|
-
return e.message;
|
|
549
|
-
}
|
|
550
|
-
throw new Error('the start was meant to stop');
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
function teamNames(file) {
|
|
554
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
555
|
-
return (yield query(file, 'SELECT name FROM "Team"')).map((r) => r.name);
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
/** What the history records as applied (sorted) and as failed. */
|
|
559
|
-
function recorded(file) {
|
|
560
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
561
|
-
const history = yield recordedHistory(file);
|
|
562
|
-
return {
|
|
563
|
-
applied: history
|
|
564
|
-
.filter((r) => r.finished_at && !r.rolled_back_at)
|
|
565
|
-
.map((r) => r.migration_name)
|
|
566
|
-
.sort(),
|
|
567
|
-
failed: yield failedMigrations(file),
|
|
568
|
-
};
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
const newestOf = (root) => migrationsOf(root)[migrationsOf(root).length - 1];
|
|
572
|
-
// Prisma's diff compares tables, so it can't see what a migration does to
|
|
573
|
-
// the rows (a backfill), nor a trigger, a view or an index's collation.
|
|
574
|
-
// Through #493, re-baselining recorded such a migration as applied when the
|
|
575
|
-
// tables matched it, so it never ran, and recorded a migration that had
|
|
576
|
-
// failed on the rows as applied at the start after.
|
|
577
|
-
describe('whose migration changes rows', () => {
|
|
578
|
-
let backfill;
|
|
579
|
-
let backfillFails;
|
|
580
|
-
let addThenFill;
|
|
581
|
-
before(() => {
|
|
582
|
-
backfill = nextRelease('backfill', (schema) => schema, 'UPDATE "Team" SET "name" = "name" || \' [backfilled]\';\n');
|
|
583
|
-
// One statement, failing on the team named Payments: SQLite undoes it whole.
|
|
584
|
-
backfillFails = nextRelease('backfill_fails', (schema) => schema, 'UPDATE "Team" SET "name" = CASE WHEN "name" = \'Payments\' THEN NULL ' +
|
|
585
|
-
'ELSE "name" || \' [backfilled]\' END;\n');
|
|
586
|
-
// A column, then a backfill whose second statement fails on that team.
|
|
587
|
-
// Prisma doesn't run a SQLite migration in a transaction, so the column
|
|
588
|
-
// and the first statement stay.
|
|
589
|
-
addThenFill = nextRelease('team_slug', (schema) => schema.replace(/(model Team \{\n)/, '$1 slug String?\n'), 'ALTER TABLE "Team" ADD COLUMN "slug" TEXT;\n' +
|
|
590
|
-
'UPDATE "Team" SET "slug" = lower("name");\n' +
|
|
591
|
-
'UPDATE "Team" SET "name" = NULL WHERE "slug" = \'payments\';\n');
|
|
592
|
-
});
|
|
593
|
-
it('runs a backfill on a database ahead of its history, rather than record it', () => __awaiter(this, void 0, void 0, function* () {
|
|
594
|
-
const file = copyOf(historyBehind);
|
|
595
|
-
yield startNext(file, backfill);
|
|
596
|
-
(0, chai_1.expect)(yield teamNames(file)).to.deep.equal(['Payments [backfilled]']);
|
|
597
|
-
yield expectFullHistory(file, migrationsOf(backfill));
|
|
598
|
-
}));
|
|
599
|
-
it('runs a backfill on a database whose history records a failed migration', () => __awaiter(this, void 0, void 0, function* () {
|
|
600
|
-
const file = copyOf(historyFailed);
|
|
601
|
-
yield startNext(file, backfill);
|
|
602
|
-
(0, chai_1.expect)(yield teamNames(file)).to.deep.equal(['Payments [backfilled]']);
|
|
603
|
-
yield expectFullHistory(file, migrationsOf(backfill));
|
|
604
|
-
}));
|
|
605
|
-
// db push made the tables of both migrations after the history, the
|
|
606
|
-
// backfill's and a column's: the tables match the column's run, and say
|
|
607
|
-
// nothing of whether the backfill ran (db push never runs one).
|
|
608
|
-
it('leaves the history alone when a backfill is among what db push already made', () => __awaiter(this, void 0, void 0, function* () {
|
|
609
|
-
const root = nextRelease('backfill_then_column', (schema) => schema.replace(/(model Team \{\n)/, '$1 slug String?\n'), 'ALTER TABLE "Team" ADD COLUMN "slug" TEXT;\n', '29990102000000_team_slug');
|
|
610
|
-
const backfillDir = path_1.default.join(root, 'prisma', 'migrations', newestOf(backfill));
|
|
611
|
-
fs_1.default.mkdirSync(backfillDir);
|
|
612
|
-
fs_1.default.copyFileSync(path_1.default.join(backfill, 'prisma', 'migrations', newestOf(backfill), 'migration.sql'), path_1.default.join(backfillDir, 'migration.sql'));
|
|
613
|
-
const file = copyOf(current);
|
|
614
|
-
prismaCli(['db', 'push', '--skip-generate', '--schema', path_1.default.join(root, 'prisma', 'schema.prisma')], file);
|
|
615
|
-
yield startNext(file, root);
|
|
616
|
-
(0, chai_1.expect)(yield recorded(file)).to.deep.equal({ applied: LOCAL_MIGRATIONS, failed: [] });
|
|
617
|
-
(0, chai_1.expect)(yield teamNames(file)).to.deep.equal(['Payments']);
|
|
618
|
-
(0, chai_1.expect)(matchesSchema(file, path_1.default.join(root, 'prisma', 'schema.prisma'))).to.equal(true);
|
|
619
|
-
}));
|
|
620
|
-
it('keeps a backfill that failed on the rows failed until they change, then runs it', () => __awaiter(this, void 0, void 0, function* () {
|
|
621
|
-
const newest = newestOf(backfillFails);
|
|
622
|
-
const file = copyOf(current);
|
|
623
|
-
for (const attempt of [1, 2]) {
|
|
624
|
-
const message = yield stopsNext(file, backfillFails);
|
|
625
|
-
(0, chai_1.expect)(message, `start ${attempt}`).to.include(newest);
|
|
626
|
-
(0, chai_1.expect)(message).to.include('NOT NULL constraint failed');
|
|
627
|
-
(0, chai_1.expect)(message).to.match(/change those rows, and start again: the next start runs it/);
|
|
628
|
-
(0, chai_1.expect)(yield recorded(file), `start ${attempt}`).to.deep.equal({
|
|
629
|
-
applied: LOCAL_MIGRATIONS,
|
|
630
|
-
failed: [newest],
|
|
631
|
-
});
|
|
632
|
-
(0, chai_1.expect)(yield teamNames(file)).to.deep.equal(['Payments']);
|
|
633
|
-
}
|
|
634
|
-
sql(file, 'UPDATE "Team" SET "name" = \'Payments team\';');
|
|
635
|
-
yield startNext(file, backfillFails);
|
|
636
|
-
(0, chai_1.expect)(yield teamNames(file)).to.deep.equal(['Payments team [backfilled]']);
|
|
637
|
-
yield expectFullHistory(file, migrationsOf(backfillFails));
|
|
638
|
-
}));
|
|
639
|
-
it('never records a migration that failed part-way as applied', () => __awaiter(this, void 0, void 0, function* () {
|
|
640
|
-
const newest = newestOf(addThenFill);
|
|
641
|
-
const file = copyOf(current);
|
|
642
|
-
let message = '';
|
|
643
|
-
for (const attempt of [1, 2]) {
|
|
644
|
-
message = yield stopsNext(file, addThenFill);
|
|
645
|
-
(0, chai_1.expect)(message, `start ${attempt}`).to.include(newest);
|
|
646
|
-
(0, chai_1.expect)(message).to.include('NOT NULL constraint failed');
|
|
647
|
-
(0, chai_1.expect)(message).to.match(/part of it/);
|
|
648
|
-
(0, chai_1.expect)(message).to.not.match(/change those rows, and start again/);
|
|
649
|
-
(0, chai_1.expect)(yield recorded(file), `start ${attempt}`).to.deep.equal({
|
|
650
|
-
applied: LOCAL_MIGRATIONS,
|
|
651
|
-
failed: [newest],
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
// As the message says: change the rows, finish what the migration
|
|
655
|
-
// didn't do, record it, and start.
|
|
656
|
-
const commands = commandsIn(message);
|
|
657
|
-
const applied = commands.find((c) => c.includes(`migrate resolve --applied ${newest}`));
|
|
658
|
-
const rolledBack = commands.find((c) => c.includes(`migrate resolve --rolled-back ${newest}`));
|
|
659
|
-
(0, chai_1.expect)([applied, rolledBack].every(Boolean), message).to.equal(true);
|
|
660
|
-
sql(file, 'UPDATE "Team" SET "name" = \'Payments team\', "slug" = \'payments team\';');
|
|
661
|
-
paste(applied);
|
|
662
|
-
yield startNext(file, addThenFill);
|
|
663
|
-
yield expectFullHistory(file, migrationsOf(addThenFill));
|
|
664
|
-
(0, chai_1.expect)(matchesSchema(file, path_1.default.join(addThenFill, 'prisma', 'schema.prisma'))).to.equal(true);
|
|
665
|
-
}));
|
|
666
|
-
});
|
|
667
|
-
/** The columns a table has. */
|
|
668
|
-
function columnsOf(file, table) {
|
|
669
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
670
|
-
const rows = yield query(file, `SELECT name FROM pragma_table_info('${table}')`);
|
|
671
|
-
return rows.map((r) => r.name);
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
// `migrate deploy` applies migrations and nothing else, so that they work
|
|
675
|
-
// on a copy says nothing of what the tables lacked before them. Through
|
|
676
|
-
// #493 a copy that worked wrote the full history over tables that lacked
|
|
677
|
-
// part of the schema, and every later start took the history's word. A
|
|
678
|
-
// migration that redefines a table lacking a column filled the column
|
|
679
|
-
// with its own name: SQLite reads an unknown "path" as the string 'path'.
|
|
680
|
-
for (const [label, rootOf] of [
|
|
681
|
-
['drops a column', () => dropColumn],
|
|
682
|
-
['adds a unique index', () => addUnique],
|
|
683
|
-
]) {
|
|
684
|
-
describe(`that ${label}, on tables that lack part of this release`, () => {
|
|
685
|
-
it('does not record it over an index dropped by hand', () => __awaiter(this, void 0, void 0, function* () {
|
|
686
|
-
const root = rootOf();
|
|
687
|
-
const file = copyOf(current);
|
|
688
|
-
sql(file, 'DROP INDEX "SessionLog_session_id_createdAt_idx";');
|
|
689
|
-
const message = yield stopsNext(file, root);
|
|
690
|
-
(0, chai_1.expect)(message).to.match(/differ from what its recorded migrations make: they lack part of what those make/);
|
|
691
|
-
(0, chai_1.expect)(message).to.include('session_id, createdAt');
|
|
692
|
-
(0, chai_1.expect)(yield recorded(file)).to.deep.equal({ applied: LOCAL_MIGRATIONS, failed: [] });
|
|
693
|
-
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
694
|
-
(0, chai_1.expect)(trialCopiesOf(file)).to.deep.equal([]);
|
|
695
|
-
}));
|
|
696
|
-
// An older release's db push --accept-data-loss (the default through
|
|
697
|
-
// 2.15.0) takes away the newest migration's column and leaves the history.
|
|
698
|
-
it('does not record it over a database an older release pushed back', () => __awaiter(this, void 0, void 0, function* () {
|
|
699
|
-
const root = rootOf();
|
|
700
|
-
const file = copyOf(current);
|
|
701
|
-
const older = path_1.default.join(dir, `older-${path_1.default.basename(root)}.prisma`);
|
|
702
|
-
fs_1.default.writeFileSync(older, fs_1.default
|
|
703
|
-
.readFileSync(SCHEMA, 'utf8')
|
|
704
|
-
.replace(/\n(\s*\/\/\/[^\n]*\n)*\s*path\s+String\?\n/, '\n'));
|
|
705
|
-
prismaCli(['db', 'push', '--skip-generate', '--accept-data-loss', '--schema', older], file);
|
|
706
|
-
(0, chai_1.expect)(matchesSchema(file, older), 'the older release pushed it').to.equal(true);
|
|
707
|
-
const message = yield stopsNext(file, root);
|
|
708
|
-
(0, chai_1.expect)(message).to.match(/differ from what its recorded migrations make: they lack part of what those make/);
|
|
709
|
-
(0, chai_1.expect)(message).to.include('LocatorEtalon');
|
|
710
|
-
(0, chai_1.expect)(yield recorded(file)).to.deep.equal({ applied: LOCAL_MIGRATIONS, failed: [] });
|
|
711
|
-
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
712
|
-
(0, chai_1.expect)(yield columnsOf(file, 'LocatorEtalon')).to.not.include('path');
|
|
713
|
-
(0, chai_1.expect)(yield columnsOf(file, 'LocatorEtalon')).to.include('attributes');
|
|
714
|
-
}));
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
// A schema with a change no migration makes (a build from a checkout
|
|
718
|
-
// someone edited, say): the migrations work on the copy, which still
|
|
719
|
-
// lacks the change, so the file gets neither them nor a full history.
|
|
720
|
-
it('does not record a release over tables that lack what its schema has beyond its migrations', () => __awaiter(this, void 0, void 0, function* () {
|
|
721
|
-
const dropScript = fs_1.default.readFileSync(path_1.default.join(dropColumn, 'prisma', 'migrations', newestOf(dropColumn), 'migration.sql'), 'utf8');
|
|
722
|
-
const unmigrated = nextRelease('drop_and_unmigrated_index', () => fs_1.default
|
|
723
|
-
.readFileSync(path_1.default.join(dropColumn, 'prisma', 'schema.prisma'), 'utf8')
|
|
724
|
-
.replace(/(model Team \{[^}]*?)\n\}/, '$1\n\n @@index([name, createdAt])\n}'), dropScript, newestOf(dropColumn));
|
|
725
|
-
const file = copyOf(currentWithHand);
|
|
726
|
-
const message = yield stopsNext(file, unmigrated);
|
|
727
|
-
(0, chai_1.expect)(message).to.match(/after this version's migrations a copy of it still lacked part/);
|
|
728
|
-
(0, chai_1.expect)(message).to.include('name, createdAt');
|
|
729
|
-
(0, chai_1.expect)(yield recorded(file)).to.deep.equal({ applied: LOCAL_MIGRATIONS, failed: [] });
|
|
730
|
-
(0, chai_1.expect)(yield handAdded(file)).to.deep.equal(KEPT);
|
|
731
|
-
(0, chai_1.expect)(yield columnsOf(file, 'LocatorEtalon')).to.include('attributes');
|
|
732
|
-
(0, chai_1.expect)(trialCopiesOf(file)).to.deep.equal([]);
|
|
733
|
-
}));
|
|
734
|
-
// A history that names a migration this release doesn't have (a later
|
|
735
|
-
// release's, or another branch's) belongs to another release. Through
|
|
736
|
-
// #493 only re-baselining left it alone: the copy ran this release's
|
|
737
|
-
// migrations on it and the file got them, dropping a column the other
|
|
738
|
-
// release still has.
|
|
739
|
-
it('leaves a database from another release alone', () => __awaiter(this, void 0, void 0, function* () {
|
|
740
|
-
const elsewhere = nextRelease('elsewhere', (schema) => schema.replace(/(model Team \{\n)/, '$1 elsewhere String?\n'), 'ALTER TABLE "Team" ADD COLUMN "elsewhere" TEXT;\n', '29980101000000_elsewhere');
|
|
741
|
-
const file = copyOf(current);
|
|
742
|
-
prismaCli(['migrate', 'deploy', '--schema', path_1.default.join(elsewhere, 'prisma', 'schema.prisma')], file);
|
|
743
|
-
sql(file, 'UPDATE "Team" SET "elsewhere" = \'other release\';');
|
|
744
|
-
const message = yield stopsNext(file, dropColumn);
|
|
745
|
-
(0, chai_1.expect)(message).to.match(/another release/);
|
|
746
|
-
(0, chai_1.expect)(message).to.include('29980101000000_elsewhere');
|
|
747
|
-
const { applied } = yield recorded(file);
|
|
748
|
-
(0, chai_1.expect)(applied).to.not.include(newestOf(dropColumn));
|
|
749
|
-
(0, chai_1.expect)(yield query(file, 'SELECT attributes FROM "LocatorEtalon"')).to.have.length(1);
|
|
750
|
-
(0, chai_1.expect)(trialCopiesOf(file)).to.deep.equal([]);
|
|
751
|
-
}));
|
|
752
|
-
// Minor: `npm run db:migrate` warns of a schema edit with no migration.
|
|
753
|
-
it('tells schema changes with no migration from a schema its migrations make', () => {
|
|
754
|
-
const unmigrated = path_1.default.join(dir, 'unmigrated');
|
|
755
|
-
fs_1.default.mkdirSync(path_1.default.join(unmigrated, 'prisma'), { recursive: true });
|
|
756
|
-
fs_1.default.cpSync(MIGRATIONS, path_1.default.join(unmigrated, 'prisma', 'migrations'), { recursive: true });
|
|
757
|
-
fs_1.default.copyFileSync(path_1.default.join(dropColumn, 'prisma', 'schema.prisma'), path_1.default.join(unmigrated, 'prisma', 'schema.prisma'));
|
|
758
|
-
fs_1.default.symlinkSync(path_1.default.join(ROOT, 'node_modules'), path_1.default.join(unmigrated, 'node_modules'));
|
|
759
|
-
(0, chai_1.expect)((0, run_migrations_1.warnOfUnmigratedSchemaChanges)(undefined, { root: ROOT })).to.equal(false);
|
|
760
|
-
(0, chai_1.expect)((0, run_migrations_1.warnOfUnmigratedSchemaChanges)(undefined, { root: dropColumn })).to.equal(false);
|
|
761
|
-
(0, chai_1.expect)((0, run_migrations_1.warnOfUnmigratedSchemaChanges)(undefined, { root: unmigrated })).to.equal(true);
|
|
762
|
-
});
|
|
763
|
-
});
|
|
764
397
|
// Prisma resolves a relative SQLite URL against the schema's directory,
|
|
765
398
|
// prisma/. Through the first cut of this change the history was read through
|
|
766
399
|
// a client that couldn't open a file in a directory that wasn't there yet,
|