@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.
Files changed (114) hide show
  1. package/README.md +2 -2
  2. package/lib/package.json +1 -1
  3. package/lib/public/assets/{AnnotationOverlay-2W4JvEyZ.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-GmckX_7r.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DqODzkyo.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
  6. package/lib/public/assets/{EmptyState-7hETKEWb.js → EmptyState-DcEG-oAi.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-GOgssFEP.js → FieldGroup-CtnCDshE.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-Yvf6RGMd.js → FilterMenu-BK0dE9cc.js} +1 -1
  9. package/lib/public/assets/{Menu-KrPnfdsf.js → Menu-eKsmLpKS.js} +1 -1
  10. package/lib/public/assets/{Modal-C6WjZFeI.js → Modal-DPwop3Lz.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CSRIhZEh.js → RecordingPage-CfgSkZkB.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-CB8ST3nE.js → RecordingsPage-Bzl9pTUY.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C9o90KId.js → SegmentedControl-B_TZTrPm.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CPm3w9l2.js → SettingCard-DlmvZJPM.js} +1 -1
  15. package/lib/public/assets/{Table-DwIgERRt.js → Table-BigPWKXi.js} +1 -1
  16. package/lib/public/assets/{activity-DrLbHifn.js → activity-CvuZdQYu.js} +1 -1
  17. package/lib/public/assets/{ai-settings-DU_Ls06P.js → ai-settings-lI2UJ2n7.js} +1 -1
  18. package/lib/public/assets/{api-keys-CWYdEzJ_.js → api-keys-CPgf9XJM.js} +1 -1
  19. package/lib/public/assets/{apps-BqNVdln7.js → apps-DIzOvtUp.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BtB0_Jg_.js → arrow-left-DiP5ywBT.js} +1 -1
  21. package/lib/public/assets/{arrow-right-C8NhdtyN.js → arrow-right-DtW_8hm5.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-gMg0ffvv.js → arrow-up-right-C89FbNU0.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CiYZ3N4M.js → auth-shell-B7sUDb--.js} +2 -2
  24. package/lib/public/assets/{builds-page-EaaqDmLN.js → builds-page-BBhvrMfA.js} +1 -1
  25. package/lib/public/assets/{button-DDau4Axv.js → button-DugsdUw1.js} +1 -1
  26. package/lib/public/assets/{calendar-DQ83B1ed.js → calendar-grKd08ZH.js} +1 -1
  27. package/lib/public/assets/{check-K1ovTQam.js → check-ORjyNfnX.js} +1 -1
  28. package/lib/public/assets/{chevron-right-Biq00pMt.js → chevron-right-Ei2FxUXq.js} +1 -1
  29. package/lib/public/assets/{circle-check-CBv88cH7.js → circle-check-Day0HjW4.js} +1 -1
  30. package/lib/public/assets/{circle-x-Bmltailn.js → circle-x-CHxSRLHp.js} +1 -1
  31. package/lib/public/assets/{clock-BPrld-3h.js → clock-BDHewWTx.js} +1 -1
  32. package/lib/public/assets/{copy-t0QB14JI.js → copy-CTwT4I3D.js} +1 -1
  33. package/lib/public/assets/{device-explorer-BgGZ8AvV.js → device-explorer-DlhrKJ-g.js} +1 -1
  34. package/lib/public/assets/{download-BroHxNOh.js → download-D3bjz_vr.js} +1 -1
  35. package/lib/public/assets/{forgot-password-Bjmf49i6.js → forgot-password-BNBLi023.js} +1 -1
  36. package/lib/public/assets/{h264Stream-DeD__6wa.js → h264Stream-C51Au2i_.js} +1 -1
  37. package/lib/public/assets/{index-B0gQkEg0.js → index-Dk5J6HB1.js} +2 -2
  38. package/lib/public/assets/{index-B0JeuZbg.js → index-DncSz5LG.js} +1 -1
  39. package/lib/public/assets/{index-72oF89LJ.js → index-Z7_BY3pJ.js} +1 -1
  40. package/lib/public/assets/{input-DIc4MSC7.js → input-bTXM4l09.js} +1 -1
  41. package/lib/public/assets/{line-chart-BcWm2vW8.js → line-chart-B3YmVYyr.js} +1 -1
  42. package/lib/public/assets/{list-checks-B06IJJad.js → list-checks-BClKEj1-.js} +1 -1
  43. package/lib/public/assets/{lock-pVLvnN30.js → lock-CZsenX3D.js} +1 -1
  44. package/lib/public/assets/{login-CN77vy--.js → login-B6A6tOp9.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-CSdzQSkY.js → maintenance-settings-CmCJGy7O.js} +1 -1
  46. package/lib/public/assets/{monitor-BtrN5IkI.js → monitor-BdTNEglU.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CNsQfo6w.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
  48. package/lib/public/assets/{network-BSfA8ZxG.js → network-BS6vUc1x.js} +1 -1
  49. package/lib/public/assets/{overview-BfTebCK5.js → overview-Yfk_Y7g3.js} +1 -1
  50. package/lib/public/assets/{page-header-D8KUX6Qg.js → page-header-DhXCwpml.js} +1 -1
  51. package/lib/public/assets/{play-Cvc8xzic.js → play-D08gP1td.js} +1 -1
  52. package/lib/public/assets/{plus-C9Zj84UF.js → plus-BKboorfa.js} +1 -1
  53. package/lib/public/assets/{profile-page-Bi27STgs.js → profile-page-Cd9NodrA.js} +1 -1
  54. package/lib/public/assets/{reset-password-CPsOuwSu.js → reset-password-acLDyyDW.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DEQ-DBOX.js → runbook-page-CWLkC1Fd.js} +1 -1
  56. package/lib/public/assets/{select-BWpnhWXj.js → select-DZoRKNti.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BefSCSpe.js → selector-detail-redirect-DS4qrt84.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-MSk96Zz7.js → selector-health-page-TpmN61G3.js} +1 -1
  59. package/lib/public/assets/{session-detail-page-DPyBNef_.js → session-detail-page-C0wYaXqy.js} +1 -1
  60. package/lib/public/assets/{settings-V6x-cXwy.js → settings-CxPGEY2j.js} +1 -1
  61. package/lib/public/assets/{stat-tile-Duex-GZD.js → stat-tile-DUpyYgl-.js} +1 -1
  62. package/lib/public/assets/{tablet-BBcoqBmL.js → tablet-SA02BJNQ.js} +1 -1
  63. package/lib/public/assets/{teams-ts8p5rEh.js → teams-jamD0Kxr.js} +1 -1
  64. package/lib/public/assets/{trash-2-CZ2ew6AZ.js → trash-2-DO8fv6w4.js} +1 -1
  65. package/lib/public/assets/{upload-Dv99y_le.js → upload-CaiGCStb.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-DrYC8JCF.js → use-builds-data-jS-fcvVf.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-C7QkBNkP.js → use-password-reset-mode-BzoenegM.js} +1 -1
  68. package/lib/public/assets/{users-gJ5Y01yo.js → users-Bnw7Vs_B.js} +1 -1
  69. package/lib/public/assets/{users-D7tKMESH.js → users-DHvDrriG.js} +1 -1
  70. package/lib/public/assets/{video-off-CqZVuZGs.js → video-off-FEvy0w5U.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-DI5DSW99.js → webhook-settings-CBVB5lxi.js} +1 -1
  72. package/lib/public/assets/{zap-ClcHFxfo.js → zap-BMrTJO6M.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +1 -1
  75. package/lib/src/app/index.js +2 -0
  76. package/lib/src/app/openapi/grid.yaml +102 -0
  77. package/lib/src/dashboard/event-manager.js +5 -8
  78. package/lib/src/device-utils.js +11 -2
  79. package/lib/src/gateway/nodeAsk.js +57 -0
  80. package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
  81. package/lib/src/plugin.js +4 -1
  82. package/lib/src/scripts/initialize-database.js +0 -6
  83. package/lib/src/scripts/run-migrations.js +123 -880
  84. package/lib/src/services/SessionLifecycleService.js +20 -5
  85. package/lib/src/services/interceptor/CertManager.js +50 -16
  86. package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
  87. package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
  88. package/lib/src/services/logcat/SessionDeviceLogs.js +199 -44
  89. package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
  90. package/lib/src/services/metrics/nodeMetrics.js +20 -49
  91. package/lib/src/sessions/RemoteSession.js +11 -2
  92. package/lib/src/sessions/SessionManager.js +28 -1
  93. package/lib/test/helpers/fake-logcat.js +80 -0
  94. package/lib/test/helpers/hub-node-pair.js +227 -0
  95. package/lib/test/unit/dashboard-events-device-logs.spec.js +15 -0
  96. package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
  97. package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
  98. package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
  99. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
  100. package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
  101. package/lib/test/unit/node-device-log-store.spec.js +102 -0
  102. package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
  103. package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
  104. package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
  105. package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
  106. package/lib/test/unit/run-migrations-database.spec.js +57 -424
  107. package/lib/test/unit/run-migrations.test.js +74 -674
  108. package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
  109. package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
  110. package/lib/test/unit/session-device-logs.spec.js +32 -94
  111. package/lib/test/unit/session-log-indexes.spec.js +15 -0
  112. package/lib/tsconfig.tsbuildinfo +1 -1
  113. package/package.json +1 -1
  114. 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 a schema (prisma/schema.prisma) says. */
56
- function matchesSchema(file, schema = SCHEMA) {
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
- schema,
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
- const etalons = yield query(file, 'SELECT id, selector AS name FROM "LocatorEtalon"');
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', 'et-1=//b', 'team-1=Payments'];
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
- /** Every local migration recorded as applied, none failed: what migrate deploy leaves. */
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 expectFullHistory(file);
260
- (0, chai_1.expect)(trialCopiesOf(file)).to.deep.equal([]);
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, and clears it', () => __awaiter(this, void 0, void 0, function* () {
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
- // `migrate deploy`, the old `postgresql` path, kept them; a start now tries
280
- // it on a copy and uses it when it works there. When it doesn't, db push
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 |touch )/.test(line))
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 a schema's plus what was added by hand. */
332
- function matchesSchemaWithHandAdded(file, schema = SCHEMA) {
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 IF EXISTS "LabNotes"; DROP INDEX IF EXISTS "lab_team_created";');
337
- return matchesSchema(probe, schema);
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('keeps what was added by hand to a history database: migrate deploy worked on a copy', () => __awaiter(this, void 0, void 0, function* () {
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 expectFullHistory(file);
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: no run of the
370
- // migrations matches the tables, and migrate deploy fails on the copy on
371
- // the change already there (P3018). The file never gets migrate deploy.
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
- (0, chai_1.expect)(commands.filter((c) => c.includes('migrate deploy'))).to.deep.equal([]);
386
- // One command: the backup, and only once it is made, the data let go.
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.be.a('string');
389
- (0, chai_1.expect)(letGo).to.match(/^touch \S+\.backup-\S+ && chmod 640 \S+ && echo .* \| .* db execute /);
390
- (0, chai_1.expect)(letGo.endsWith(` && DATABASE_URL=file:${file} ${PRISMA} db push --skip-generate --accept-data-loss ` +
391
- `--schema ${SCHEMA}`), letGo).to.equal(true);
392
- // A backup that can't be made (its name is taken) lets nothing go.
393
- const backup = vacuumTarget(letGo);
394
- fs_1.default.writeFileSync(backup, 'taken');
395
- (0, chai_1.expect)(() => paste(letGo)).to.throw();
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.include('.backup-');
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(letGo))).to.deep.equal(KEPT);
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,