@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
@@ -45,14 +45,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
45
45
  return (mod && mod.__esModule) ? mod : { "default": mod };
46
46
  };
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
- exports.migrationShape = migrationShape;
49
- exports.failedOnWhatWasThere = failedOnWhatWasThere;
50
48
  exports.planSchemaSync = planSchemaSync;
51
49
  exports.readMigrationHistory = readMigrationHistory;
52
- exports.schemaMissingIn = schemaMissingIn;
53
50
  exports.schemaSyncFailure = schemaSyncFailure;
54
51
  exports.syncDatabaseSchema = syncDatabaseSchema;
55
- exports.warnOfUnmigratedSchemaChanges = warnOfUnmigratedSchemaChanges;
56
52
  exports.runMigrations = runMigrations;
57
53
  const node_child_process_1 = require("node:child_process");
58
54
  const fs = __importStar(require("node:fs"));
@@ -61,134 +57,13 @@ const os = __importStar(require("node:os"));
61
57
  const logger_1 = __importDefault(require("../logger"));
62
58
  const config_1 = require("../config");
63
59
  const database_check_1 = require("./database-check");
64
- /** A script's statements, without comments, split where a `;` ends one outside quotes. */
65
- function sqlStatements(script) {
66
- const statements = [];
67
- let current = '';
68
- let i = 0;
69
- while (i < script.length) {
70
- const c = script[i];
71
- if (c === '-' && script[i + 1] === '-') {
72
- const end = script.indexOf('\n', i);
73
- i = end === -1 ? script.length : end;
74
- current += ' ';
75
- }
76
- else if (c === '/' && script[i + 1] === '*') {
77
- const end = script.indexOf('*/', i + 2);
78
- i = end === -1 ? script.length : end + 2;
79
- current += ' ';
80
- }
81
- else if (c === "'" || c === '"' || c === '`' || c === '[') {
82
- const close = c === '[' ? ']' : c;
83
- let j = i + 1;
84
- while (j < script.length) {
85
- if (script[j] === close && close !== ']' && script[j + 1] === close)
86
- j += 2;
87
- else if (script[j] === close)
88
- break;
89
- else
90
- j++;
91
- }
92
- current += script.slice(i, j + 1);
93
- i = j + 1;
94
- }
95
- else if (c === ';') {
96
- if (current.trim())
97
- statements.push(current.trim());
98
- current = '';
99
- i++;
100
- }
101
- else {
102
- current += c;
103
- i++;
104
- }
105
- }
106
- if (current.trim())
107
- statements.push(current.trim());
108
- return statements;
109
- }
110
- /** The part of a statement a table comparison sees: a table, a column, an index. */
111
- const SEEN_DDL = /^(CREATE (UNIQUE )?INDEX|CREATE TABLE|ALTER TABLE|DROP TABLE|DROP INDEX)\b/;
112
- /** Prisma's copy of a table it redefines: every row, into the table that replaces it. */
113
- const REDEFINE_COPY = /^INSERT INTO "new_([^"]+)" \([^)]*\) SELECT .* FROM "\1"$/;
114
- /** What a migration does that the tables can and can't show. */
115
- function migrationShape(script) {
116
- let statements = 0;
117
- const unseen = [];
118
- for (const statement of sqlStatements(script)) {
119
- const flat = statement.replace(/\s+/g, ' ');
120
- const upper = flat.toUpperCase();
121
- if (upper.startsWith('PRAGMA '))
122
- continue;
123
- statements++;
124
- const seen = (SEEN_DDL.test(upper) &&
125
- !/\bCOLLATE\b/.test(upper) &&
126
- !/^CREATE TABLE .* AS SELECT\b/.test(upper)) ||
127
- REDEFINE_COPY.test(flat);
128
- if (!seen)
129
- unseen.push(flat.length > 60 ? `${flat.slice(0, 57)}...` : flat);
130
- }
131
- return { statements, unseen };
132
- }
133
- /** Whether a migration that failed can be run again without doing any of it twice. */
134
- function rerunnable(shape) {
135
- // A failed statement changed nothing (SQLite undoes it), so one statement
136
- // did nothing. Several whose every effect shows: a part that ran is still
137
- // there, and running it again fails on it rather than doing it twice.
138
- return shape.statements <= 1 || shape.unseen.length === 0;
139
- }
140
- /** Whether a failed migration's error says what it makes was already there. */
141
- function failedOnWhatWasThere(logs) {
142
- return /already exists|duplicate column name/i.test(logs !== null && logs !== void 0 ? logs : '');
143
- }
144
- /** For a planner given no SQL: every migration changes only what the tables show. */
145
- const SHOWN = () => ({ statements: 1, unseen: [] });
146
60
  /**
147
- * The run of the migrations whose tables are these: the largest k for which
148
- * `tablesMatch(first k)` holds, then down while `tablesMatch(first k - 1)`
149
- * holds too. Two runs that make the same tables differ by migrations the
150
- * comparison can't see (a backfill), which must run rather than be recorded.
151
- * It never goes below the last migration the history records, and gives up
152
- * (null) if a comparison can't be made. Every migration comes first, as `db
153
- * push` leaves the tables, then the history's own, then the rest from the top.
61
+ * Which command brings a database up to date. `historyMatchesTables` is asked
62
+ * only about a database with migrations left to apply. Given the applied
63
+ * migrations, it says whether the tables are what they make, or null when it
64
+ * can't tell.
154
65
  */
155
- function rebaseline(history, localMigrations, tablesMatch, known) {
156
- const applied = new Set(history.applied);
157
- const lowest = localMigrations.reduce((last, name, i) => (applied.has(name) ? i + 1 : last), 0);
158
- const ask = (k) => {
159
- if (!known.has(k))
160
- known.set(k, tablesMatch(localMigrations.slice(0, k)));
161
- return known.get(k);
162
- };
163
- const order = [localMigrations.length, lowest];
164
- for (let k = localMigrations.length - 1; k > lowest; k--)
165
- order.push(k);
166
- for (let k of new Set(order)) {
167
- const matches = ask(k);
168
- if (matches === null)
169
- return null;
170
- if (matches === false)
171
- continue;
172
- while (k > lowest) {
173
- const below = ask(k - 1);
174
- if (below === null)
175
- return null;
176
- if (below === false)
177
- break;
178
- k--;
179
- }
180
- return k;
181
- }
182
- return null;
183
- }
184
- /**
185
- * Which command brings a database up to date, and what to record first.
186
- * `tablesMatch` is asked only about a database with migrations left to apply
187
- * or a failed one recorded. Given a run of migrations, it says whether the
188
- * tables are what they make, or null when it can't tell. `shapeOf` says what
189
- * a migration does that the tables can't show.
190
- */
191
- function planSchemaSync(history, localMigrations, tablesMatch, shapeOf = SHOWN) {
66
+ function planSchemaSync(history, localMigrations, historyMatchesTables) {
192
67
  if (!history) {
193
68
  return {
194
69
  command: 'db push',
@@ -198,138 +73,44 @@ function planSchemaSync(history, localMigrations, tablesMatch, shapeOf = SHOWN)
198
73
  failed: [],
199
74
  };
200
75
  }
201
- const applied = new Set(history.applied);
202
- const recorded = localMigrations.filter((name) => applied.has(name));
203
- const known = new Map();
204
- if (history.failed.length === 0) {
205
- const pending = localMigrations.filter((name) => !applied.has(name));
206
- if (pending.length === 0) {
207
- return {
208
- command: 'migrate deploy',
209
- acceptDataLoss: false,
210
- reason: 'its migration history has every migration',
211
- basis: 'complete',
212
- failed: [],
213
- };
214
- }
215
- const matches = tablesMatch(recorded);
216
- if (matches !== false) {
217
- return {
218
- command: 'migrate deploy',
219
- acceptDataLoss: false,
220
- reason: `its migration history is missing ${pending.length} migration(s)` +
221
- (matches === null ? ' (the tables could not be compared with the history)' : ''),
222
- basis: matches === null ? 'uncompared' : 'behind',
223
- failed: [],
224
- };
225
- }
226
- // The history's own run, already compared, when it is the first n.
227
- if (recorded.every((name, i) => localMigrations[i] === name)) {
228
- known.set(recorded.length, false);
229
- }
230
- }
231
- // A history from a later release, or another branch's: not this release's to rewrite or run.
232
- const local = new Set(localMigrations);
233
- const unknown = [...history.applied, ...history.failed].filter((name) => !local.has(name));
234
- if (unknown.length > 0) {
76
+ if (history.failed.length > 0) {
235
77
  return {
236
78
  command: 'db push',
237
79
  acceptDataLoss: false,
238
- reason: `its migration history names ${unknown.length} migration(s) this version doesn't have ` +
239
- `(${unknown.join(', ')}), so another release of Xenon brought it up to date: Xenon ` +
240
- "doesn't change its history or try this version's migrations on it",
241
- basis: 'other-release',
80
+ reason: `its migration history records a failed migration (${history.failed.join(', ')}), ` +
81
+ 'which migrate deploy will not get past',
82
+ basis: 'failed-migration',
242
83
  failed: history.failed,
243
- unknown,
244
84
  };
245
85
  }
246
- const failed = history.failed;
247
- const logsOf = (name) => { var _a, _b; return (_b = (_a = history.failureLogs) === null || _a === void 0 ? void 0 : _a[name]) !== null && _b !== void 0 ? _b : ''; };
248
- const unresolved = (migration, why, detail) => ({
249
- command: 'none',
250
- acceptDataLoss: false,
251
- reason: `its migration history records ${migration} as failed, and ${detail}: Xenon neither ` +
252
- 'runs it again nor records it as applied',
253
- basis: 'failed-unresolved',
254
- failed,
255
- unresolved: { migration, why, logs: logsOf(migration) },
256
- });
257
- const k = rebaseline(history, localMigrations, tablesMatch, known);
258
- if (k !== null) {
259
- const within = new Set(localMigrations.slice(0, k));
260
- const toRecord = localMigrations.slice(0, k).filter((name) => !applied.has(name));
261
- for (const name of failed) {
262
- const shape = shapeOf(name);
263
- if (within.has(name)) {
264
- // The tables have what it makes: recorded as applied only if it failed
265
- // because that was there already, and it does nothing they can't show.
266
- if (!failedOnWhatWasThere(logsOf(name))) {
267
- return unresolved(name, 'part-way', 'the tables have what it makes, though it failed on something other than that ' +
268
- 'being there already, so part of it may have run before it failed');
269
- }
270
- if (shape.unseen.length > 0) {
271
- return unresolved(name, 'unseen-part', 'what it makes was in the tables already when it ran, but it also changes what ' +
272
- `the tables can't show (${shape.unseen[0]}), which never ran`);
273
- }
274
- }
275
- else if (!rerunnable(shape)) {
276
- return unresolved(name, 'part-way', `its ${shape.statements} statements change what the tables can't show ` +
277
- `(${shape.unseen[0]}), so the tables can't tell how many of them ran before it failed`);
278
- }
279
- }
280
- const unseen = toRecord.filter((name) => !failed.includes(name) && shapeOf(name).unseen.length > 0);
281
- if (unseen.length > 0) {
282
- return {
283
- command: 'db push',
284
- acceptDataLoss: false,
285
- reason: `its tables are what its first ${k} migrations make, though its history records ` +
286
- `${recorded.length}, but ${unseen.join(', ')} ${unseen.length > 1 ? 'change' : 'changes'} ` +
287
- `what the tables can't show (${shapeOf(unseen[0]).unseen[0]}), so Xenon can't tell ` +
288
- 'whether that part ran, leaves the history as it is and uses db push, as before',
289
- basis: 'unrecordable',
290
- failed,
291
- unseen,
292
- };
293
- }
294
- const rolledBack = failed;
86
+ const applied = new Set(history.applied);
87
+ const pending = localMigrations.filter((name) => !applied.has(name));
88
+ if (pending.length === 0) {
295
89
  return {
296
90
  command: 'migrate deploy',
297
91
  acceptDataLoss: false,
298
- reason: `its tables are what its first ${k} migrations make, though its history records ` +
299
- `${recorded.length}` +
300
- (failed.length ? ` and a failed one (${failed.join(', ')})` : '') +
301
- ': Xenon ' +
302
- (rolledBack.length ? `marks ${rolledBack.join(', ')} rolled back, ` : '') +
303
- (toRecord.length ? `records ${toRecord.join(', ')} as applied, ` : '') +
304
- 'then runs migrate deploy',
305
- basis: 'rebaselined',
306
- failed,
307
- resolve: { rolledBack, applied: toRecord },
92
+ reason: 'its migration history has every migration',
93
+ basis: 'complete',
94
+ failed: [],
308
95
  };
309
96
  }
310
- if (failed.length > 0) {
311
- // The copy runs a failed migration again: only one that can't do half its work twice.
312
- const unsafe = failed.find((name) => !rerunnable(shapeOf(name)));
313
- if (unsafe) {
314
- const shape = shapeOf(unsafe);
315
- return unresolved(unsafe, 'part-way', `its ${shape.statements} statements change what the tables can't show ` +
316
- `(${shape.unseen[0]}), so the tables can't tell how many of them ran before it failed`);
317
- }
97
+ const matches = historyMatchesTables(localMigrations.filter((name) => applied.has(name)));
98
+ if (matches === false) {
318
99
  return {
319
100
  command: 'db push',
320
101
  acceptDataLoss: false,
321
- reason: `its migration history records a failed migration (${failed.join(', ')}), ` +
322
- 'which migrate deploy will not get past, and its tables match no run of the migrations',
323
- basis: 'failed-migration',
324
- failed,
102
+ reason: 'its tables differ from what its recorded migrations make (db push updated it, or it ' +
103
+ 'was changed by hand), so migrate deploy could fail on a change that is already there',
104
+ basis: 'tables-differ',
105
+ failed: [],
325
106
  };
326
107
  }
327
108
  return {
328
- command: 'db push',
109
+ command: 'migrate deploy',
329
110
  acceptDataLoss: false,
330
- reason: 'its tables differ from what its recorded migrations make, and from what any longer ' +
331
- 'run of the migrations makes (something was added to them by hand, say)',
332
- basis: 'tables-differ',
111
+ reason: `its migration history is missing ${pending.length} migration(s)` +
112
+ (matches === null ? ' (the tables could not be compared with the history)' : ''),
113
+ basis: matches === null ? 'uncompared' : 'behind',
333
114
  failed: [],
334
115
  };
335
116
  }
@@ -346,19 +127,6 @@ function sqliteFilePath(databaseUrl, baseDir) {
346
127
  return file;
347
128
  return baseDir ? path.resolve(baseDir, file) : null;
348
129
  }
349
- /** What Prisma wrote down when each failed migration failed; nothing when it can't be read. */
350
- function failureLogsIn(client) {
351
- return __awaiter(this, void 0, void 0, function* () {
352
- try {
353
- const rows = yield client.$queryRawUnsafe('SELECT migration_name, logs FROM "_prisma_migrations" ' +
354
- 'WHERE finished_at IS NULL AND rolled_back_at IS NULL');
355
- return Object.fromEntries(rows.map((r) => { var _a; return [r.migration_name, String((_a = r.logs) !== null && _a !== void 0 ? _a : '')]; }));
356
- }
357
- catch (_a) {
358
- return {};
359
- }
360
- });
361
- }
362
130
  /** The migration history in `_prisma_migrations`, or null when the database has none. */
363
131
  function readMigrationHistory(databaseUrl) {
364
132
  return __awaiter(this, void 0, void 0, function* () {
@@ -370,10 +138,10 @@ function readMigrationHistory(databaseUrl) {
370
138
  const client = new PrismaClient({ datasources: { db: { url: databaseUrl } } });
371
139
  try {
372
140
  const rows = yield client.$queryRawUnsafe('SELECT migration_name, finished_at, rolled_back_at FROM "_prisma_migrations"');
373
- const failed = rows
374
- .filter((r) => !r.finished_at && !r.rolled_back_at)
375
- .map((r) => r.migration_name);
376
- return Object.assign({ applied: rows.filter((r) => r.finished_at && !r.rolled_back_at).map((r) => r.migration_name), failed }, (failed.length ? { failureLogs: yield failureLogsIn(client) } : {}));
141
+ return {
142
+ applied: rows.filter((r) => r.finished_at && !r.rolled_back_at).map((r) => r.migration_name),
143
+ failed: rows.filter((r) => !r.finished_at && !r.rolled_back_at).map((r) => r.migration_name),
144
+ };
377
145
  }
378
146
  catch (err) {
379
147
  const message = String((_c = (_b = (_a = err === null || err === void 0 ? void 0 : err.meta) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : err);
@@ -393,32 +161,6 @@ function readMigrationHistory(databaseUrl) {
393
161
  }
394
162
  });
395
163
  }
396
- /**
397
- * A consistent copy of a SQLite file, made by SQLite itself (`VACUUM INTO`)
398
- * through a short-lived Prisma client, as the history is read. A copy of the
399
- * file's bytes alone would be wrong beside a hot journal or a WAL.
400
- */
401
- function copyWithVacuum(from, to) {
402
- return __awaiter(this, void 0, void 0, function* () {
403
- const { PrismaClient } = yield Promise.resolve().then(() => __importStar(require('../generated/client')));
404
- const client = new PrismaClient({ datasources: { db: { url: `file:${from}` } } });
405
- try {
406
- yield client.$executeRawUnsafe(`VACUUM INTO '${to.replace(/'/g, "''")}'`);
407
- }
408
- finally {
409
- yield client.$disconnect();
410
- }
411
- });
412
- }
413
- function freeBytesIn(dir) {
414
- try {
415
- const stats = fs.statfsSync(dir);
416
- return Number(stats.bavail) * Number(stats.bsize);
417
- }
418
- catch (_a) {
419
- return null;
420
- }
421
- }
422
164
  /** The migrations in prisma/migrations, in the order Prisma applies them. */
423
165
  function localMigrationsIn(migrationsDir) {
424
166
  return fs
@@ -433,275 +175,96 @@ function shellWord(word) {
433
175
  /**
434
176
  * The commands a failure's message gives, ready to paste. A copy is made with
435
177
  * SQLite's `VACUUM INTO` through `prisma db execute`, so it is consistent
436
- * whatever journal sits beside the file, and needs nothing but the CLI. It
437
- * goes into an empty file given the database's own mode first: `VACUUM INTO`
438
- * takes an empty file, and refuses one that isn't, so a backup never
439
- * overwrites another.
178
+ * whatever journal sits beside the file, and needs nothing but the CLI.
440
179
  */
441
180
  function pasteable(at) {
442
181
  const schema = shellWord(at.schema);
443
- const url = shellWord(at.databaseUrl);
182
+ const url = (file) => `file:${file}`;
444
183
  return {
445
- backup(target) {
184
+ copyTo(target) {
446
185
  const statement = `VACUUM INTO '${target.replace(/'/g, "''")}'`;
447
186
  const quoted = /["$`\\!]/.test(statement) ? shellWord(statement) : `"${statement}"`;
448
- const copy = `echo ${quoted} | ${at.prisma} db execute --url ${url} --stdin`;
449
- if (at.mode === undefined || at.mode === null)
450
- return copy;
451
- const word = shellWord(target);
452
- const mode = (at.mode & 0o777).toString(8).padStart(3, '0');
453
- return `touch ${word} && chmod ${mode} ${word} && ${copy}`;
187
+ return `echo ${quoted} | ${at.prisma} db execute --url ${shellWord(at.databaseUrl)} --stdin`;
188
+ },
189
+ deploy(file) {
190
+ return `DATABASE_URL=${shellWord(url(file))} ${at.prisma} migrate deploy --schema ${schema}`;
454
191
  },
455
192
  letGo() {
456
- return (`DATABASE_URL=${url} ${at.prisma} db push --skip-generate ` +
193
+ return (`DATABASE_URL=${shellWord(at.databaseUrl)} ${at.prisma} db push --skip-generate ` +
457
194
  `--accept-data-loss --schema ${schema}`);
458
195
  },
459
- resolve(flag, migration) {
460
- return (`DATABASE_URL=${url} ${at.prisma} migrate resolve ${flag} ${shellWord(migration)} ` +
461
- `--schema ${schema}`);
462
- },
463
196
  };
464
197
  }
465
- /** A backup's name: the file's, stamped with the time to the millisecond. */
198
+ /** A backup's name: the file's, stamped with the time, so one never overwrites another. */
466
199
  function backupOf(file, now) {
467
- return `${file}.backup-${now.toISOString().replace(/[-:.]/g, '')}`;
200
+ const stamp = now.toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '');
201
+ return `${file}.backup-${stamp}`;
468
202
  }
469
203
  /** The migration a P3018 names, if it does. */
470
204
  function failedMigrationIn(output) {
471
205
  const match = /Migration name:\s*(\S+)/.exec(output);
472
206
  return match ? match[1] : null;
473
207
  }
474
- const megabytes = (bytes) => Math.ceil(bytes / (1024 * 1024));
475
- /**
476
- * What a database lacks of what should be there (prisma/schema.prisma, or
477
- * what a run of migrations makes), from Prisma's summary of a diff from that
478
- * to the database: every line that removes or changes something (with its
479
- * table's line), or null when it only adds (what was added to the database
480
- * by hand) or finds nothing. A line it doesn't know counts as lacking.
481
- */
482
- function schemaMissingIn(summary) {
483
- const missing = [];
484
- let header = '';
485
- for (const line of summary.split('\n').map((l) => l.trimEnd())) {
486
- if (!line.trim() || /^No difference detected\.?$/.test(line.trim()))
487
- continue;
488
- if (!/^\s/.test(line)) {
489
- // A table's heading: added tables, or a table whose lines say what changed.
490
- header = line;
491
- if (!/^\[\+\]/.test(line) && !/^\[\*\] Changed the `[^`]+` table$/.test(line)) {
492
- missing.push(line);
493
- }
494
- continue;
495
- }
496
- const item = line.trim();
497
- const adds = /^\[\+\]/.test(item) || (/^- /.test(item) && /^\[\+\]/.test(header));
498
- if (adds)
499
- continue;
500
- if (!missing.includes(header))
501
- missing.push(header);
502
- missing.push(line);
503
- }
504
- return missing.length ? missing.join('\n') : null;
505
- }
506
- /** What the copy told about the database, for a refusal's first sentence and its way to keep. */
507
- function copyTrialText(trial, basis) {
508
- const thenTries = "and start again: Xenon then tries this version's migrations on a copy first, and uses " +
509
- 'them if they work there.';
510
- if (!trial || trial.outcome === 'worked')
511
- return { why: '', keep: null };
512
- if (trial.outcome === 'failed') {
513
- return {
514
- why: ", and this version's migrations failed on a copy of it" +
515
- (trial.migration ? ` (${trial.migration})` : ''),
516
- keep: null,
517
- };
518
- }
519
- if (trial.outcome === 'incomplete' && trial.stage === 'before') {
520
- return {
521
- why: (basis === 'tables-differ'
522
- ? ': they lack part of what those make'
523
- : ', and its tables lack part of what its recorded migrations make') +
524
- " (listed at the end), so Xenon does not try this version's migrations on a copy of it",
525
- keep: `To keep it, add what the tables lack to the database ${thenTries}`,
526
- };
527
- }
528
- if (trial.outcome === 'incomplete') {
529
- return {
530
- why: ", and after this version's migrations a copy of it still lacked part of this " +
531
- "version's schema (listed at the end), so Xenon doesn't run them on it",
532
- keep: 'To keep it, add what the copy lacked to the database (prisma/schema.prisma says what ' +
533
- `it is) ${thenTries}`,
534
- };
535
- }
536
- if (trial.why === 'space') {
537
- const free = trial.neededBytes
538
- ? `free ${megabytes(trial.neededBytes)} MB next to the file`
539
- : 'make room next to the file';
540
- return {
541
- why: `, and there was no room next to it for a copy to try this version's migrations on (${trial.detail})`,
542
- keep: `To keep it, ${free} ${thenTries}`,
543
- };
544
- }
545
- if (trial.why === 'locked') {
546
- return {
547
- why: `, and it was locked, so Xenon could not copy it to try this version's migrations on (${trial.detail})`,
548
- keep: `To keep it, stop whatever else is using the file (another server?) ${thenTries}`,
549
- };
550
- }
551
- return {
552
- why: `, and Xenon could not copy it to try this version's migrations on (${trial.detail})`,
553
- keep: `To keep it, fix what stopped the copy ${thenTries}`,
554
- };
555
- }
556
- /** Why `db push` was the command, for its refusal's first sentence. */
557
- function dbPushBecause(plan) {
558
- var _a, _b;
559
- switch (plan.basis) {
560
- case 'failed-migration':
561
- return {
562
- why: `its migration history records a failed migration (${plan.failed.join(', ')}), ` +
563
- "which prisma migrate deploy won't run past",
564
- keep: null,
565
- };
566
- case 'other-release':
567
- return {
568
- why: 'its migration history names migrations this version of Xenon does not have ' +
569
- `(${((_a = plan.unknown) !== null && _a !== void 0 ? _a : []).join(', ')}): another release brought it up to date`,
570
- keep: 'To keep it, start it with the release of Xenon that last brought it up to date.',
571
- };
572
- case 'unrecordable':
573
- return {
574
- why: 'its tables are ahead of its migration history, and the migrations they are ahead ' +
575
- `by include ${((_b = plan.unseen) !== null && _b !== void 0 ? _b : []).join(', ')}, which change(s) what the tables ` +
576
- "can't show, so Xenon can't record them as applied",
577
- keep: null,
578
- };
579
- default:
580
- return {
581
- why: 'its tables differ from what its recorded migrations make (something was added to ' +
582
- 'them by hand, say)',
583
- keep: null,
584
- };
585
- }
586
- }
587
- /** What Prisma wrote down when a migration failed, without the engine's stack trace. */
588
- function withoutStack(logs) {
589
- return logs.split(/\n\s+0: /)[0].trim();
590
- }
591
- /** What to do about a failed migration Xenon neither runs again nor records as applied. */
592
- function unresolvedAdvice(migration, why, where, at, now) {
593
- const run = pasteable(at);
594
- const sql = at.migrationsDir
595
- ? path.join(at.migrationsDir, migration, 'migration.sql')
596
- : `prisma/migrations/${migration}/migration.sql`;
597
- const reason = why === 'unseen-part'
598
- ? 'What it makes was already in the tables when it ran, but it also changes rows (or ' +
599
- "makes a trigger, a view or a collation), which the tables can't show, and that " +
600
- 'part never ran'
601
- : "SQLite keeps what a migration's statements did up to the one that failed, so part " +
602
- 'of it may be in the tables';
603
- return (`A migration, ${migration}, failed on the database at ${where}, and its migration ` +
604
- `history records it as failed (Prisma's reason is below). ${reason}: Xenon neither runs ` +
605
- 'it again nor records it as applied. ' +
606
- (at.file
607
- ? `Back the file up:\n ${run.backup(backupOf(at.file, now))}\n`
608
- : 'Back the database up. ') +
609
- `Then compare what it does (${sql}) with the tables and, once the rows it failed on are ` +
610
- "changed, either finish what it didn't do and record it as applied:\n" +
611
- ` ${run.resolve('--applied', migration)}\n` +
612
- 'or undo what it did and record it as rolled back, so that the next start runs it again:\n' +
613
- ` ${run.resolve('--rolled-back', migration)}\n` +
614
- 'Then start again.');
615
- }
616
208
  /**
617
209
  * What went wrong and what to do, then Prisma's own output. A refusal to
618
210
  * delete data gives the commands to run, with this server's paths, and never
619
- * sends `migrate deploy` to a database: it has been tried on a copy by then,
620
- * or the copy couldn't be made or wasn't tried, and it fails on one with a
621
- * failed migration recorded (P3009) or with tables ahead of its history
622
- * (P3018, which records the migration as failed). `step` names a failed call
623
- * other than the plan's command; `failed` describes the migration a P3018
624
- * names.
211
+ * sends `migrate deploy` to a database it would fail on: one with a failed
212
+ * migration recorded (P3009), or one whose tables are ahead of its history
213
+ * (P3018, which records the migration as failed).
625
214
  */
626
- function schemaSyncFailure(plan, output, at, { now = new Date(), step, failed, } = {}) {
627
- var _a, _b, _c, _d, _e, _f, _g;
215
+ function schemaSyncFailure(plan, output, at, now = new Date()) {
628
216
  const where = (0, database_check_1.describeDatabaseUrl)(at.databaseUrl);
629
217
  const run = pasteable(at);
630
218
  let sentence;
631
- let after = '';
632
- if (plan.basis === 'failed-unresolved' && plan.unresolved) {
633
- sentence = unresolvedAdvice(plan.unresolved.migration, plan.unresolved.why, where, at, now);
634
- }
635
- else if (step) {
636
- sentence =
637
- `prisma ${step} could not correct the migration history of the database at ${where}: ` +
638
- 'back the file up and fix what Prisma reports below, then start again.';
639
- }
640
- else if (/Added the required column/i.test(output)) {
219
+ if (/Added the required column/i.test(output)) {
641
220
  sentence =
642
221
  `This version adds a required column to a table that already has rows in ${where}, ` +
643
222
  'which prisma db push cannot do: back the file up, then migrate it by hand or start ' +
644
223
  'from a fresh database.';
645
224
  }
646
225
  else if (/accept-data-loss|There might be data loss/i.test(output)) {
647
- const because = dbPushBecause(plan);
648
- const trial = copyTrialText(plan.trial, plan.basis);
649
- if (plan.basis === 'tables-differ' &&
650
- ((_a = plan.trial) === null || _a === void 0 ? void 0 : _a.outcome) === 'incomplete' &&
651
- plan.trial.stage === 'before') {
652
- // Not (only) something added by hand: something missing, which the next words say.
653
- because.why = 'its tables differ from what its recorded migrations make';
654
- }
226
+ const why = plan.basis === 'failed-migration'
227
+ ? `its migration history records a failed migration (${plan.failed.join(', ')}), ` +
228
+ "which prisma migrate deploy won't run past"
229
+ : 'its tables differ from what its recorded migrations make (something was added to ' +
230
+ 'them by hand, or prisma db push moved them past the history)';
231
+ const backup = at.file
232
+ ? `Back the file up first:\n ${run.copyTo(backupOf(at.file, now))}\n`
233
+ : 'Back the database up first. ';
655
234
  sentence =
656
235
  'prisma db push stopped rather than delete what Prisma lists below from the database ' +
657
- `at ${where}. Xenon brings this database up to date with prisma db push because ` +
658
- `${because.why}${trial.why}. Without --accept-data-loss, db push deletes no table or ` +
659
- 'column that holds data. ' +
660
- (at.file
661
- ? 'To let what is listed go, back the file up and let it go with this (nothing goes ' +
662
- `unless the backup is made), then start again:\n ${run.backup(backupOf(at.file, now))}` +
663
- ` && ${run.letGo()}\n`
664
- : 'To let what is listed go, back the database up, then run this and start again:\n' +
665
- ` ${run.letGo()}\n`) +
666
- ((_c = (_b = trial.keep) !== null && _b !== void 0 ? _b : because.keep) !== null && _c !== void 0 ? _c : 'To keep any of it, copy it out of the database first.');
667
- if (((_d = plan.trial) === null || _d === void 0 ? void 0 : _d.outcome) === 'failed') {
668
- after = `\n\nOn the copy, prisma migrate deploy said:\n${plan.trial.output}`;
236
+ `at ${where}. Xenon brings this database up to date with prisma db push because ${why}, ` +
237
+ 'and without --accept-data-loss db push deletes no table or column that holds data. ' +
238
+ backup +
239
+ `To let what is listed go, run this and start again:\n ${run.letGo()}\n`;
240
+ if (plan.basis === 'failed-migration' || !at.file) {
241
+ sentence += 'To keep any of it, copy it out of the database first.';
669
242
  }
670
- else if (((_e = plan.trial) === null || _e === void 0 ? void 0 : _e.outcome) === 'incomplete' && plan.trial.stage === 'before') {
671
- after =
672
- '\n\nThe tables lack this, which their recorded migrations make (prisma migrate diff ' +
673
- `from the recorded migrations to the tables):\n${plan.trial.missing}`;
674
- }
675
- else if (((_f = plan.trial) === null || _f === void 0 ? void 0 : _f.outcome) === 'incomplete') {
676
- after =
677
- "\n\nAfter this version's migrations, the copy still lacked this (prisma migrate diff " +
678
- `from prisma/schema.prisma to the copy):\n${plan.trial.missing}`;
243
+ else {
244
+ // migrate deploy works on the file only if the tables are not ahead of
245
+ // its history, which nothing here can tell: a copy finds out.
246
+ const copy = `${at.file}.copy`;
247
+ sentence +=
248
+ "To keep it, try this version's migrations on a copy of the file instead:\n" +
249
+ ` ${run.copyTo(copy)}\n` +
250
+ ` ${run.deploy(copy)}\n` +
251
+ 'If that works, run them on the file, delete the copy and start again:\n' +
252
+ ` ${run.deploy(at.file)}\n` +
253
+ 'If it fails, they would fail on the file too (P3018 means the tables already have ' +
254
+ "a change the history doesn't record): delete the copy, copy out anything you need, " +
255
+ 'and let what is listed go as above.';
679
256
  }
680
257
  }
681
258
  else if (/P3018/.test(output)) {
682
- const name = (_g = failed === null || failed === void 0 ? void 0 : failed.migration) !== null && _g !== void 0 ? _g : failedMigrationIn(output);
683
- const failedOn = `A migration${name ? `, ${name},` : ''} failed on the database at ${where}`;
684
- if (failed && failed.wasThere && failed.shown) {
685
- sentence =
686
- `${failedOn}, because what it makes was already there (Prisma's reason is below), ` +
687
- 'and its migration history now records it as failed. Start again: the next start ' +
688
- 'records it as applied if the tables are what the migrations up to it make.';
689
- }
690
- else if (failed && !failed.wasThere && failed.leftNothing) {
691
- sentence =
692
- `${failedOn}, and its migration history now records it as failed. Check Prisma's ` +
693
- 'reason below. If it failed on the rows in the database (a unique index over ' +
694
- 'duplicate values, say), back the file up, change those rows, and start again: the ' +
695
- 'next start runs it again.';
696
- }
697
- else if (failed) {
698
- sentence = unresolvedAdvice(failed.migration, failed.wasThere ? 'unseen-part' : 'part-way', where, at, now);
699
- }
700
- else {
701
- sentence =
702
- `${failedOn}, and its migration history now records it as failed. Check Prisma's ` +
703
- 'reason below, back the file up and fix it, then start again.';
704
- }
259
+ const name = failedMigrationIn(output);
260
+ sentence =
261
+ `A migration${name ? `, ${name},` : ''} failed on the database at ${where}, and its ` +
262
+ "migration history now records it as failed. Check Prisma's reason below. If what the " +
263
+ 'migration makes is already there ("already exists", "duplicate column name"), start ' +
264
+ 'again: Xenon brings a database with a failed migration recorded up to date with ' +
265
+ 'prisma db push, which stops before deleting a table or column that holds data. If ' +
266
+ 'the migration failed on the rows in the database (a unique index over duplicate ' +
267
+ 'values, say), back the file up, change those rows, and start again.';
705
268
  }
706
269
  else {
707
270
  sentence =
@@ -709,7 +272,7 @@ function schemaSyncFailure(plan, output, at, { now = new Date(), step, failed, }
709
272
  'file up and fix what Prisma reports below, or, if its schema already matches this ' +
710
273
  'version, set XENON_AUTO_MIGRATE=false to start without changing it.';
711
274
  }
712
- return `[DBMigrate] Cannot start: ${sentence}\n\n${output}${after}`;
275
+ return `[DBMigrate] Cannot start: ${sentence}\n\n${output}`;
713
276
  }
714
277
  /** What a failed prisma call printed: its warnings (stdout) and its error (stderr). */
715
278
  function cliOutput(err) {
@@ -746,174 +309,29 @@ function resolvePrismaInvocation(rootDir) {
746
309
  return { cmd: localBin, prefix: [] };
747
310
  return { cmd: 'npx', prefix: ['prisma'] };
748
311
  }
749
- /** Where a start tries the migrations: next to the file, named for this process. */
750
- const TRIAL_INFIX = '.xenon-trial-';
751
- /** Beyond twice the file's size (the copy, and a migration that copies a table), for the copy. */
752
- const TRIAL_MARGIN_BYTES = 64 * 1024 * 1024;
753
- function removeTrialFiles(copy) {
754
- for (const suffix of ['', '-journal', '-wal', '-shm']) {
755
- fs.rmSync(`${copy}${suffix}`, { force: true });
756
- }
757
- }
758
- /** Whether a process is running. Signal 0 only asks; it sends nothing. */
759
- function processIsRunning(pid) {
760
- if (!Number.isSafeInteger(pid) || pid <= 1)
761
- return false;
762
- try {
763
- process.kill(pid, 0);
764
- return true;
765
- }
766
- catch (err) {
767
- return (err === null || err === void 0 ? void 0 : err.code) === 'EPERM';
768
- }
769
- }
770
- /** Removes the copies earlier starts left next to the file, unless their process still runs. */
771
- function sweepTrialCopies(file) {
772
- const dir = path.dirname(file);
773
- const prefix = `${path.basename(file)}${TRIAL_INFIX}`;
774
- let names;
775
- try {
776
- names = fs.readdirSync(dir);
777
- }
778
- catch (_a) {
779
- return;
780
- }
781
- for (const name of names) {
782
- if (!name.startsWith(prefix))
783
- continue;
784
- const match = /^(\d+)(-journal|-wal|-shm)?$/.exec(name.slice(prefix.length));
785
- if (!match)
786
- continue;
787
- const pid = Number(match[1]);
788
- if (pid !== process.pid && processIsRunning(pid))
789
- continue;
790
- try {
791
- fs.rmSync(path.join(dir, name), { force: true });
792
- logger_1.default.info(`[DBMigrate] Removed ${name}, a copy of the database an earlier start left.`);
793
- }
794
- catch (err) {
795
- logger_1.default.warn(`[DBMigrate] Could not remove ${name}, left by an earlier start: ${err === null || err === void 0 ? void 0 : err.message}`);
796
- }
797
- }
798
- }
799
- /**
800
- * `migrate deploy` on a copy of the file, next to it (not in os.tmpdir(), which
801
- * can be RAM-backed), with any failed migration rolled back there first, then
802
- * whether the copy has everything prisma/schema.prisma has (`lacks`): the
803
- * migrations make only what they make, so a copy they worked on can still lack
804
- * what the tables lacked before them (an index dropped by hand, a column an
805
- * older release's `db push` took away). The copy gets the file's own mode
806
- * before any data is in it. It is skipped when there isn't room for it or the
807
- * file can't be copied, and it is deleted whatever happens.
808
- */
809
- function tryMigrationsOnCopy(file, failed, migrate, lacks, options) {
810
- return __awaiter(this, void 0, void 0, function* () {
811
- var _a, _b, _c, _d;
812
- const copy = `${file}${TRIAL_INFIX}${process.pid}`;
813
- let stats;
814
- try {
815
- stats = fs.statSync(file);
816
- }
817
- catch (err) {
818
- return { outcome: 'skipped', why: 'copy', detail: String((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err) };
819
- }
820
- const neededBytes = stats.size * 2 + TRIAL_MARGIN_BYTES;
821
- const free = options.freeBytes(path.dirname(file));
822
- if (free === null) {
823
- logger_1.default.info('[DBMigrate] Xenon could not tell how much space is free next to the database, so it ' +
824
- `tries the copy anyway (it needs about ${megabytes(neededBytes)} MB).`);
825
- }
826
- else if (free < neededBytes) {
827
- return {
828
- outcome: 'skipped',
829
- why: 'space',
830
- neededBytes,
831
- detail: `the copy needs ${megabytes(neededBytes)} MB and ${megabytes(free)} MB is free`,
832
- };
833
- }
834
- try {
835
- removeTrialFiles(copy);
836
- try {
837
- // VACUUM INTO writes into an empty file, which keeps the mode it has.
838
- fs.writeFileSync(copy, '', { mode: stats.mode & 0o777 });
839
- fs.chmodSync(copy, stats.mode & 0o777);
840
- yield options.copyDatabase(file, copy);
841
- }
842
- catch (err) {
843
- const message = String((_d = (_c = (_b = err === null || err === void 0 ? void 0 : err.meta) === null || _b === void 0 ? void 0 : _b.message) !== null && _c !== void 0 ? _c : err === null || err === void 0 ? void 0 : err.message) !== null && _d !== void 0 ? _d : err).trim();
844
- if (/disk is full|SQLITE_FULL|no space left|ENOSPC/i.test(message)) {
845
- return { outcome: 'skipped', why: 'space', neededBytes, detail: message };
846
- }
847
- const locked = /database is locked|SQLITE_BUSY|busy/i.test(message);
848
- return { outcome: 'skipped', why: locked ? 'locked' : 'copy', detail: message };
849
- }
850
- try {
851
- for (const name of failed) {
852
- migrate(['migrate', 'resolve', '--rolled-back', name], `file:${copy}`);
853
- }
854
- migrate(['migrate', 'deploy'], `file:${copy}`);
855
- }
856
- catch (err) {
857
- const output = cliOutput(err);
858
- return { outcome: 'failed', output, migration: failedMigrationIn(output) };
859
- }
860
- const missing = lacks(`file:${copy}`);
861
- return missing === null
862
- ? { outcome: 'worked' }
863
- : { outcome: 'incomplete', stage: 'after', missing };
864
- }
865
- finally {
866
- removeTrialFiles(copy);
867
- }
868
- });
869
- }
870
- /** A migration's SQL, as `migrationShape` reads it; unreadable counts as all unseen. */
871
- function shapeIn(migrationsDir) {
872
- const shapes = new Map();
873
- return (name) => {
874
- var _a;
875
- let shape = shapes.get(name);
876
- if (!shape) {
877
- try {
878
- shape = migrationShape(fs.readFileSync(path.join(migrationsDir, name, 'migration.sql'), 'utf8'));
879
- }
880
- catch (err) {
881
- shape = { statements: 2, unseen: [`(its SQL could not be read: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err})`] };
882
- }
883
- shapes.set(name, shape);
884
- }
885
- return shape;
886
- };
887
- }
888
312
  /**
889
313
  * Bring the database at `config.databaseUrl` up to date with prisma/schema.prisma,
890
314
  * by the rule above. Throws an Error whose message says what to do when it can't.
891
315
  * `npm run db:migrate` runs this too, whatever XENON_AUTO_MIGRATE says.
892
316
  */
893
317
  function syncDatabaseSchema() {
894
- return __awaiter(this, arguments, void 0, function* (runSync = node_child_process_1.execFileSync, readHistory = readMigrationHistory, options = {}) {
895
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
896
- const rootDir = (_a = options.root) !== null && _a !== void 0 ? _a : resolvePluginRoot();
318
+ return __awaiter(this, arguments, void 0, function* (runSync = node_child_process_1.execFileSync, readHistory = readMigrationHistory) {
319
+ var _a;
320
+ const rootDir = resolvePluginRoot();
897
321
  const schemaPath = path.join(rootDir, 'prisma', 'schema.prisma');
898
322
  const migrationsDir = path.join(rootDir, 'prisma', 'migrations');
899
323
  if (!fs.existsSync(schemaPath) || !fs.existsSync(migrationsDir)) {
900
324
  logger_1.default.warn(`[DBMigrate] Skipping auto-migrate: schema or migrations missing at ${rootDir}`);
901
- return undefined;
325
+ return;
902
326
  }
903
327
  const databaseUrl = config_1.config.databaseUrl;
904
328
  const where = (0, database_check_1.describeDatabaseUrl)(databaseUrl);
905
329
  // The file the CLI will use: a relative path is read from prisma/, as Prisma does.
906
330
  const file = sqliteFilePath(databaseUrl, path.dirname(schemaPath));
907
331
  fs.mkdirSync(path.dirname(file !== null && file !== void 0 ? file : config_1.config.databasePath), { recursive: true });
908
- if (file)
909
- sweepTrialCopies(file);
910
332
  const { cmd, prefix } = resolvePrismaInvocation(rootDir);
911
- /** A prisma call against a database: this one unless another URL is given. */
912
- const prisma = (args, url = databaseUrl) => runSync(cmd, [...prefix, ...args, '--schema', schemaPath], {
913
- env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: url }),
914
- cwd: rootDir,
915
- stdio: 'pipe',
916
- });
333
+ const env = Object.assign(Object.assign({}, process.env), { DATABASE_URL: databaseUrl });
334
+ const opts = { env, cwd: rootDir, stdio: 'pipe' };
917
335
  let history;
918
336
  try {
919
337
  history = yield readHistory(file ? `file:${file}` : databaseUrl);
@@ -922,37 +340,22 @@ function syncDatabaseSchema() {
922
340
  throw new Error('[DBMigrate] Cannot start: Xenon could not read the migration history of the database ' +
923
341
  `at ${where}, so it can't tell how to bring it up to date: if another server is using ` +
924
342
  'the file, stop that one; otherwise back the file up, check that it is a Xenon SQLite ' +
925
- `database (Prisma's message is below), and start again.\n\n${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
343
+ `database (Prisma's message is below), and start again.\n\n${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
926
344
  }
927
- /**
928
- * `migrate diff` from a run of migrations (this release's copies of them, in
929
- * a scratch directory) to the tables, with `--exit-code` when asked.
930
- */
931
- const diffFromMigrations = (migrations, exitCode) => {
345
+ // Would the history's own migrations make exactly these tables? Built from
346
+ // this release's copies of the applied migrations, in a scratch directory.
347
+ const historyMatchesTables = (applied) => {
932
348
  const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'xenon-migration-history-'));
933
349
  try {
934
350
  const lock = path.join(migrationsDir, 'migration_lock.toml');
935
351
  if (fs.existsSync(lock))
936
352
  fs.copyFileSync(lock, path.join(scratch, 'migration_lock.toml'));
937
- for (const name of migrations) {
353
+ for (const name of applied) {
938
354
  fs.mkdirSync(path.join(scratch, name));
939
355
  fs.copyFileSync(path.join(migrationsDir, name, 'migration.sql'), path.join(scratch, name, 'migration.sql'));
940
356
  }
941
357
  const diff = ['migrate', 'diff', '--from-migrations', scratch, '--to-schema-datasource'];
942
- return runSync(cmd, [...prefix, ...diff, schemaPath, ...(exitCode ? ['--exit-code'] : [])], {
943
- env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: databaseUrl }),
944
- cwd: rootDir,
945
- stdio: 'pipe',
946
- });
947
- }
948
- finally {
949
- fs.rmSync(scratch, { recursive: true, force: true });
950
- }
951
- };
952
- // Would this run of migrations make exactly these tables?
953
- const tablesMatch = (migrations) => {
954
- try {
955
- diffFromMigrations(migrations, true);
358
+ runSync(cmd, [...prefix, ...diff, schemaPath, '--exit-code'], opts);
956
359
  return true;
957
360
  }
958
361
  catch (err) {
@@ -961,221 +364,61 @@ function syncDatabaseSchema() {
961
364
  logger_1.default.warn(`[DBMigrate] Could not compare the tables with the migration history: ${cliOutput(err)}`);
962
365
  return null;
963
366
  }
964
- };
965
- // What a run of migrations makes that the tables lack (what was added to
966
- // them by hand aside); null when they lack nothing.
967
- const tablesLack = (migrations) => {
968
- var _a;
969
- try {
970
- return schemaMissingIn(String((_a = diffFromMigrations(migrations, false)) !== null && _a !== void 0 ? _a : ''));
971
- }
972
- catch (err) {
973
- return `Xenon could not compare the tables with their migrations: ${cliOutput(err)}`;
974
- }
975
- };
976
- // What prisma/schema.prisma has that a database lacks; null when it lacks nothing.
977
- const lacksOfSchema = (url) => {
978
- try {
979
- const summary = runSync(cmd, [...prefix, 'migrate', 'diff', '--from-schema-datamodel', schemaPath, '--to-url', url], { env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: url }), cwd: rootDir, stdio: 'pipe' });
980
- return schemaMissingIn(String(summary !== null && summary !== void 0 ? summary : ''));
981
- }
982
- catch (err) {
983
- return `Xenon could not compare the copy with prisma/schema.prisma: ${cliOutput(err)}`;
367
+ finally {
368
+ fs.rmSync(scratch, { recursive: true, force: true });
984
369
  }
985
370
  };
986
- const localMigrations = localMigrationsIn(migrationsDir);
987
- const shapeOf = shapeIn(migrationsDir);
988
- const plan = planSchemaSync(history, localMigrations, tablesMatch, shapeOf);
989
- const tryCopy = Boolean(history &&
990
- file &&
991
- plan.command === 'db push' &&
992
- (plan.basis === 'tables-differ' || plan.basis === 'failed-migration'));
993
- const at = {
994
- prisma: [cmd, ...prefix].map(shellWord).join(' '),
995
- schema: schemaPath,
996
- databaseUrl: file ? `file:${file}` : databaseUrl,
997
- file,
998
- mode: file && fs.existsSync(file) ? fs.statSync(file).mode & 0o777 : null,
999
- migrationsDir,
1000
- };
1001
- if (plan.command === 'none') {
1002
- logger_1.default.warn(`[DBMigrate] Not changing the database at ${where}: ${plan.reason}.`);
1003
- throw new Error(schemaSyncFailure(plan, withoutStack((_d = (_c = plan.unresolved) === null || _c === void 0 ? void 0 : _c.logs) !== null && _d !== void 0 ? _d : ''), at));
1004
- }
1005
- const how = tryCopy ? 'migrate deploy on a copy first, else db push' : plan.command;
1006
- const announce = `[DBMigrate] Syncing database schema (${how}) at ${where}: ${plan.reason}.`;
1007
- // A database with a history that leaves plain migrate deploy is worth a look.
1008
- if (history && (plan.command === 'db push' || plan.basis === 'rebaselined'))
371
+ const plan = planSchemaSync(history, localMigrationsIn(migrationsDir), historyMatchesTables);
372
+ const announce = `[DBMigrate] Syncing database schema (${plan.command}) at ${where}: ${plan.reason}.`;
373
+ // A database with a history that leaves migrate deploy is worth a look.
374
+ if (history && plan.command === 'db push')
1009
375
  logger_1.default.warn(announce);
1010
376
  else
1011
377
  logger_1.default.info(announce);
1012
- /** What is known of the migration a P3018 names: whether running it again is safe. */
1013
- const factsOfFailed = (output) => {
1014
- const migration = failedMigrationIn(output);
1015
- const index = migration ? localMigrations.indexOf(migration) : -1;
1016
- if (!migration || index < 0)
1017
- return undefined;
1018
- const shape = shapeOf(migration);
1019
- const wasThere = failedOnWhatWasThere(output);
1020
- const shown = shape.unseen.length === 0;
1021
- // One statement that failed did nothing. Several that only change what the
1022
- // tables show did nothing if the tables are what the ones before it make.
1023
- const leftNothing = !wasThere &&
1024
- (shape.statements <= 1 || (shown && tablesMatch(localMigrations.slice(0, index)) === true));
1025
- return { migration, wasThere, shown, leftNothing };
1026
- };
1027
- let step;
1028
- let ran = plan;
378
+ const args = plan.command === 'db push'
379
+ ? [
380
+ ...prefix,
381
+ 'db',
382
+ 'push',
383
+ '--skip-generate',
384
+ ...(plan.acceptDataLoss ? ['--accept-data-loss'] : []),
385
+ '--schema',
386
+ schemaPath,
387
+ ]
388
+ : [...prefix, 'migrate', 'deploy', '--schema', schemaPath];
1029
389
  try {
1030
- if (plan.command === 'migrate deploy') {
1031
- for (const name of (_f = (_e = plan.resolve) === null || _e === void 0 ? void 0 : _e.rolledBack) !== null && _f !== void 0 ? _f : []) {
1032
- step = `migrate resolve --rolled-back ${name}`;
1033
- logger_1.default.info(`[DBMigrate] Marking ${name} rolled back in the migration history.`);
1034
- prisma(['migrate', 'resolve', '--rolled-back', name]);
1035
- }
1036
- for (const name of (_h = (_g = plan.resolve) === null || _g === void 0 ? void 0 : _g.applied) !== null && _h !== void 0 ? _h : []) {
1037
- step = `migrate resolve --applied ${name}`;
1038
- logger_1.default.info(`[DBMigrate] Recording ${name} as applied: the tables show all it does.`);
1039
- prisma(['migrate', 'resolve', '--applied', name]);
1040
- }
1041
- step = undefined;
1042
- prisma(['migrate', 'deploy']);
1043
- }
1044
- else {
1045
- if (tryCopy && file && history) {
1046
- // migrate deploy builds on what the recorded migrations made. On tables
1047
- // that lack part of it (an index dropped by hand, a column an older
1048
- // release's db push took away) it would leave the gap, or fill it
1049
- // wrongly: SQLite reads a missing "column" in a redefined table's copy
1050
- // as a string, so the column comes back holding its own name.
1051
- const applied = new Set(history.applied);
1052
- const lacking = tablesLack(localMigrations.filter((name) => applied.has(name)));
1053
- let trial;
1054
- if (lacking !== null) {
1055
- trial = { outcome: 'incomplete', stage: 'before', missing: lacking };
1056
- }
1057
- else {
1058
- let size = '';
1059
- try {
1060
- size = `, ${megabytes(fs.statSync(file).size)} MB to copy, which can take a while`;
1061
- }
1062
- catch (_l) {
1063
- /* the trial says why */
1064
- }
1065
- logger_1.default.info("[DBMigrate] Trying this version's migrations on a copy of the database first " +
1066
- `(${path.basename(file)}${TRIAL_INFIX}${process.pid}, next to it${size}).`);
1067
- trial = yield tryMigrationsOnCopy(file, plan.failed, prisma, lacksOfSchema, {
1068
- copyDatabase: (_j = options.copyDatabase) !== null && _j !== void 0 ? _j : copyWithVacuum,
1069
- freeBytes: (_k = options.freeBytes) !== null && _k !== void 0 ? _k : freeBytesIn,
1070
- });
1071
- }
1072
- if (trial.outcome === 'worked') {
1073
- logger_1.default.info("[DBMigrate] This version's migrations worked on the copy, and left it with " +
1074
- 'everything prisma/schema.prisma has, so Xenon runs prisma migrate deploy on the ' +
1075
- 'database.');
1076
- ran = Object.assign(Object.assign({}, plan), { command: 'migrate deploy', trial });
1077
- for (const name of plan.failed) {
1078
- step = `migrate resolve --rolled-back ${name}`;
1079
- prisma(['migrate', 'resolve', '--rolled-back', name]);
1080
- }
1081
- step = undefined;
1082
- prisma(['migrate', 'deploy']);
1083
- logger_1.default.info('[DBMigrate] Database schema in sync.');
1084
- return ran;
1085
- }
1086
- logger_1.default.warn(trial.outcome === 'failed'
1087
- ? "[DBMigrate] This version's migrations failed on the copy" +
1088
- (trial.migration ? ` (${trial.migration})` : '') +
1089
- ', so Xenon brings the database up to date with prisma db push.'
1090
- : trial.outcome === 'incomplete' && trial.stage === 'before'
1091
- ? '[DBMigrate] The tables lack part of what their recorded migrations make, so ' +
1092
- "Xenon doesn't try this version's migrations on them and brings them up to " +
1093
- `date with prisma db push:\n${trial.missing}`
1094
- : trial.outcome === 'incomplete'
1095
- ? "[DBMigrate] This version's migrations worked on the copy, but it still " +
1096
- "lacked part of this version's schema, so Xenon doesn't run them on the " +
1097
- `database and brings it up to date with prisma db push:\n${trial.missing}`
1098
- : `[DBMigrate] Xenon did not try the migrations on a copy: ${trial.detail}.`);
1099
- ran = Object.assign(Object.assign({}, plan), { trial });
1100
- }
1101
- prisma([
1102
- 'db',
1103
- 'push',
1104
- '--skip-generate',
1105
- ...(plan.acceptDataLoss ? ['--accept-data-loss'] : []),
1106
- ]);
1107
- }
390
+ runSync(cmd, args, opts);
1108
391
  logger_1.default.info('[DBMigrate] Database schema in sync.');
1109
- return ran;
1110
392
  }
1111
393
  catch (err) {
1112
394
  const msg = cliOutput(err);
1113
- logger_1.default.error(`[DBMigrate] prisma ${step !== null && step !== void 0 ? step : ran.command} failed: ${msg}`);
395
+ logger_1.default.error(`[DBMigrate] prisma ${plan.command} failed: ${msg}`);
1114
396
  // Boot must not continue. The schema is not what the code expects, so the
1115
397
  // next query fails somewhere unrelated — `prisma.user.count()` reporting a
1116
398
  // missing User table, say — and buries this, the actual cause. Stop here
1117
399
  // and hand back something the operator can act on.
1118
- const failed = !step && /P3018/.test(msg) ? factsOfFailed(msg) : undefined;
1119
- throw new Error(schemaSyncFailure(ran, msg, at, { step, failed }));
400
+ throw new Error(schemaSyncFailure(plan, msg, {
401
+ prisma: [cmd, ...prefix].map(shellWord).join(' '),
402
+ schema: schemaPath,
403
+ databaseUrl: file ? `file:${file}` : databaseUrl,
404
+ file,
405
+ }));
1120
406
  }
1121
407
  });
1122
408
  }
1123
- /**
1124
- * Whether prisma/schema.prisma has changes no migration in prisma/migrations
1125
- * makes, with a warning when it has; null when it can't tell. `npm run
1126
- * db:migrate` asks: a database with a migration history is updated with
1127
- * `migrate deploy`, which applies migrations only, so it never gets them (nor
1128
- * does any server this release reaches), while the log says it is in sync.
1129
- * `npm run db:generate` gives the developer's database a history.
1130
- */
1131
- function warnOfUnmigratedSchemaChanges(runSync = node_child_process_1.execFileSync, options = {}) {
1132
- var _a;
1133
- const rootDir = (_a = options.root) !== null && _a !== void 0 ? _a : resolvePluginRoot();
1134
- const schemaPath = path.join(rootDir, 'prisma', 'schema.prisma');
1135
- const migrationsDir = path.join(rootDir, 'prisma', 'migrations');
1136
- if (!fs.existsSync(schemaPath) || !fs.existsSync(migrationsDir))
1137
- return null;
1138
- const { cmd, prefix } = resolvePrismaInvocation(rootDir);
1139
- try {
1140
- runSync(cmd, [
1141
- ...prefix,
1142
- 'migrate',
1143
- 'diff',
1144
- '--from-migrations',
1145
- migrationsDir,
1146
- '--to-schema-datamodel',
1147
- schemaPath,
1148
- '--exit-code',
1149
- ], { env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: config_1.config.databaseUrl }), cwd: rootDir, stdio: 'pipe' });
1150
- return false;
1151
- }
1152
- catch (err) {
1153
- if ((err === null || err === void 0 ? void 0 : err.status) === 2) {
1154
- logger_1.default.warn('[DBMigrate] prisma/schema.prisma has changes that no migration in prisma/migrations ' +
1155
- 'makes. Add one with `npm run db:generate -- --name <change>`: a database with a ' +
1156
- 'migration history is brought up to date with prisma migrate deploy, which applies ' +
1157
- 'migrations only, so it never gets these changes, and nor does any server this ' +
1158
- 'release reaches, even where the log says the schema is in sync.');
1159
- return true;
1160
- }
1161
- logger_1.default.warn(`[DBMigrate] Could not compare prisma/schema.prisma with prisma/migrations: ${cliOutput(err)}`);
1162
- return null;
1163
- }
1164
- }
1165
409
  /** The startup step: `syncDatabaseSchema`, unless XENON_AUTO_MIGRATE=false. */
1166
410
  function runMigrations() {
1167
- return __awaiter(this, arguments, void 0, function* (runSync = node_child_process_1.execFileSync, readHistory = readMigrationHistory, options = {}) {
411
+ return __awaiter(this, arguments, void 0, function* (runSync = node_child_process_1.execFileSync, readHistory = readMigrationHistory) {
1168
412
  if (!config_1.config.autoMigrate) {
1169
413
  logger_1.default.info('[DBMigrate] Auto-migrate disabled by XENON_AUTO_MIGRATE=false. Bring the schema up ' +
1170
414
  'to date yourself before the server starts: `prisma migrate deploy` for a database ' +
1171
415
  'whose migration history (`_prisma_migrations`) matches its tables (`prisma migrate ' +
1172
416
  'diff --from-migrations <the recorded ones> --to-schema-datasource prisma/schema.prisma ' +
1173
417
  '--exit-code` says so), `prisma db push` for any other (a `migrate deploy` that stops ' +
1174
- 'with P3018 on a change already there means the database is one of those, until ' +
1175
- '`prisma migrate resolve --applied` records the migrations its tables already have), ' +
1176
- 'or `npm run db:migrate` from a source checkout, which chooses for you.');
418
+ 'with P3018 on a change already there means the database is one of those), or ' +
419
+ '`npm run db:migrate` from a source checkout, which chooses for you.');
1177
420
  return;
1178
421
  }
1179
- yield syncDatabaseSchema(runSync, readHistory, options);
422
+ yield syncDatabaseSchema(runSync, readHistory);
1180
423
  });
1181
424
  }