@xenon-device-management/xenon 2.15.0 → 2.16.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 (183) hide show
  1. package/README.md +38 -26
  2. package/lib/package.json +2 -1
  3. package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-GmckX_7r.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DqODzkyo.js} +1 -1
  6. package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-7hETKEWb.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-GOgssFEP.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-Yvf6RGMd.js} +1 -1
  9. package/lib/public/assets/{Menu-CtdCybzA.js → Menu-KrPnfdsf.js} +1 -1
  10. package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-C6WjZFeI.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CSRIhZEh.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-CB8ST3nE.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-C9o90KId.js} +1 -1
  14. package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-CPm3w9l2.js} +1 -1
  15. package/lib/public/assets/{Table-zNTX92Pn.js → Table-DwIgERRt.js} +1 -1
  16. package/lib/public/assets/{activity-CJ8LwZq9.js → activity-DrLbHifn.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-DU_Ls06P.js} +1 -1
  18. package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CWYdEzJ_.js} +1 -1
  19. package/lib/public/assets/{apps-BjGymyf3.js → apps-BqNVdln7.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-BtB0_Jg_.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-C8NhdtyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-gMg0ffvv.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-CiYZ3N4M.js} +2 -2
  24. package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-EaaqDmLN.js} +1 -1
  25. package/lib/public/assets/{button-Cg08-EMT.js → button-DDau4Axv.js} +1 -1
  26. package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-DQ83B1ed.js} +1 -1
  27. package/lib/public/assets/{check-BMTaH72W.js → check-K1ovTQam.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Biq00pMt.js} +1 -1
  29. package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-CBv88cH7.js} +1 -1
  30. package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-Bmltailn.js} +1 -1
  31. package/lib/public/assets/{clock-C8_HViQP.js → clock-BPrld-3h.js} +1 -1
  32. package/lib/public/assets/{copy-Be7LNMXV.js → copy-t0QB14JI.js} +1 -1
  33. package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-BgGZ8AvV.js} +1 -1
  34. package/lib/public/assets/{download-B56lwxWP.js → download-BroHxNOh.js} +1 -1
  35. package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-Bjmf49i6.js} +1 -1
  36. package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-DeD__6wa.js} +1 -1
  37. package/lib/public/assets/{index-CfxsUslp.js → index-72oF89LJ.js} +1 -1
  38. package/lib/public/assets/{index-B9KQ6LsY.js → index-B0JeuZbg.js} +1 -1
  39. package/lib/public/assets/index-B0gQkEg0.js +219 -0
  40. package/lib/public/assets/{input-CZHFjbSg.js → input-DIc4MSC7.js} +1 -1
  41. package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-BcWm2vW8.js} +1 -1
  42. package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-B06IJJad.js} +1 -1
  43. package/lib/public/assets/{lock-CkOwgDcx.js → lock-pVLvnN30.js} +1 -1
  44. package/lib/public/assets/{login-D5cc4w7M.js → login-CN77vy--.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CSdzQSkY.js} +1 -1
  46. package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BtrN5IkI.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
  48. package/lib/public/assets/{network-Dii9qdvk.js → network-BSfA8ZxG.js} +1 -1
  49. package/lib/public/assets/{overview-rolAACrO.js → overview-BfTebCK5.js} +1 -1
  50. package/lib/public/assets/{page-header-COQZxJfR.js → page-header-D8KUX6Qg.js} +1 -1
  51. package/lib/public/assets/{play-DrN1Se_R.js → play-Cvc8xzic.js} +1 -1
  52. package/lib/public/assets/{plus-5aq-9uRx.js → plus-C9Zj84UF.js} +1 -1
  53. package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Bi27STgs.js} +1 -1
  54. package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-CPsOuwSu.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-DEQ-DBOX.js} +153 -102
  56. package/lib/public/assets/{select-Bpyn7rWO.js → select-BWpnhWXj.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-BefSCSpe.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-MSk96Zz7.js} +1 -1
  59. package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
  60. package/lib/public/assets/{settings-C3uyGhyc.js → settings-V6x-cXwy.js} +1 -1
  61. package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-Duex-GZD.js} +1 -1
  62. package/lib/public/assets/{tablet-BxUScAeO.js → tablet-BBcoqBmL.js} +1 -1
  63. package/lib/public/assets/{teams-B5ljGkY2.js → teams-ts8p5rEh.js} +1 -1
  64. package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-CZ2ew6AZ.js} +1 -1
  65. package/lib/public/assets/{upload-C2gr44RS.js → upload-Dv99y_le.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-DrYC8JCF.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
  68. package/lib/public/assets/{users-D_vv_yz4.js → users-D7tKMESH.js} +1 -1
  69. package/lib/public/assets/{users-MY6eNe6s.js → users-gJ5Y01yo.js} +1 -1
  70. package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-CqZVuZGs.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-DI5DSW99.js} +1 -1
  72. package/lib/public/assets/{zap-DO9ry1iz.js → zap-ClcHFxfo.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +11 -7
  75. package/lib/src/InternalHttpClient.js +27 -5
  76. package/lib/src/XenonCapabilityManager.js +10 -3
  77. package/lib/src/app/openapi/sessions.yaml +7 -5
  78. package/lib/src/app/routers/auth.js +2 -1
  79. package/lib/src/app/routers/users.js +2 -1
  80. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  81. package/lib/src/dashboard/commandLogFields.js +42 -0
  82. package/lib/src/dashboard/event-manager.js +26 -154
  83. package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
  84. package/lib/src/dashboard/services/failureCategories.js +264 -0
  85. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  86. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  87. package/lib/src/device-utils.js +14 -6
  88. package/lib/src/gateway/forwardToNode.js +4 -11
  89. package/lib/src/gateway/hubSessionToken.js +27 -4
  90. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  91. package/lib/src/helpers/outboundProxy.js +219 -0
  92. package/lib/src/index.js +21 -0
  93. package/lib/src/interceptors/CommandInterceptor.js +13 -15
  94. package/lib/src/interceptors/xenonScripts.js +28 -7
  95. package/lib/src/logger.js +3 -1
  96. package/lib/src/plugin.js +8 -3
  97. package/lib/src/scripts/database-check.js +3 -3
  98. package/lib/src/scripts/initialize-database.js +35 -33
  99. package/lib/src/scripts/run-migrations.js +1079 -27
  100. package/lib/src/services/ApiKeyService.js +16 -5
  101. package/lib/src/services/InterceptorService.js +6 -2
  102. package/lib/src/services/OrphanSweeper.js +6 -0
  103. package/lib/src/services/SelectorStateService.js +11 -7
  104. package/lib/src/services/SelectorVerificationJob.js +6 -2
  105. package/lib/src/services/SessionLifecycleService.js +32 -18
  106. package/lib/src/services/SocketClient.js +11 -0
  107. package/lib/src/services/SocketServer.js +361 -42
  108. package/lib/src/services/TeamService.js +5 -1
  109. package/lib/src/services/TracingService.js +171 -60
  110. package/lib/src/services/UserService.js +14 -3
  111. package/lib/src/services/UserSessionService.js +16 -4
  112. package/lib/src/services/healing/HealingOrchestrator.js +3 -1
  113. package/lib/src/services/identity/identityChanges.js +41 -0
  114. package/lib/src/services/logcat/SessionDeviceLogs.js +97 -10
  115. package/lib/src/services/logcat/deviceLogBook.js +5 -4
  116. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  117. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  118. package/lib/src/services/token/JwtKeyService.js +8 -2
  119. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  120. package/lib/src/sessions/LocalSession.js +29 -1
  121. package/lib/src/sessions/RemoteSession.js +12 -2
  122. package/lib/src/sessions/SessionManager.js +5 -8
  123. package/lib/test/helpers/driver-log.js +38 -0
  124. package/lib/test/helpers/fake-http-proxy.js +106 -0
  125. package/lib/test/helpers/otlp-probe.js +107 -0
  126. package/lib/test/integration/androidDevices.spec.js +1 -1
  127. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  128. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  129. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  130. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  131. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  132. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  133. package/lib/test/unit/dashboard-events-device-logs.spec.js +59 -11
  134. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  135. package/lib/test/unit/database-check.spec.js +1 -0
  136. package/lib/test/unit/device-utils.spec.js +13 -4
  137. package/lib/test/unit/event-log-session-data.spec.js +11 -4
  138. package/lib/test/unit/failure-analysis.spec.js +22 -8
  139. package/lib/test/unit/failure-categories.spec.js +24 -22
  140. package/lib/test/unit/failure-categorization.spec.js +393 -0
  141. package/lib/test/unit/healing-tier-gate.spec.js +3 -2
  142. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
  143. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  144. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  145. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  146. package/lib/test/unit/logcat-ws.spec.js +78 -37
  147. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  148. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  149. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  150. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  151. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  152. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  153. package/lib/test/unit/proxy-option.spec.js +358 -0
  154. package/lib/test/unit/run-migrations-database.spec.js +807 -0
  155. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  156. package/lib/test/unit/run-migrations.test.js +864 -0
  157. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  158. package/lib/test/unit/selector-state-service.spec.js +72 -23
  159. package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
  160. package/lib/test/unit/selector-verification-job.spec.js +15 -6
  161. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  162. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  163. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  164. package/lib/test/unit/session-device-logs.spec.js +11 -2
  165. package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
  166. package/lib/test/unit/session-log-indexes.spec.js +1 -1
  167. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  168. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  169. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  170. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  171. package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
  172. package/lib/tsconfig.tsbuildinfo +1 -1
  173. package/package.json +2 -1
  174. package/schema.json +11 -7
  175. package/scripts/dev/readme-screenshots.js +276 -23
  176. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  177. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  178. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  179. package/scripts/export-openapi.js +32 -0
  180. package/scripts/lib/openapi-for-site.js +79 -0
  181. package/lib/public/assets/index-BOxe43p1.js +0 -219
  182. package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
  183. package/lib/src/scripts/prepare-prisma.js +0 -89
@@ -45,6 +45,14 @@ 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
+ exports.planSchemaSync = planSchemaSync;
51
+ exports.readMigrationHistory = readMigrationHistory;
52
+ exports.schemaMissingIn = schemaMissingIn;
53
+ exports.schemaSyncFailure = schemaSyncFailure;
54
+ exports.syncDatabaseSchema = syncDatabaseSchema;
55
+ exports.warnOfUnmigratedSchemaChanges = warnOfUnmigratedSchemaChanges;
48
56
  exports.runMigrations = runMigrations;
49
57
  const node_child_process_1 = require("node:child_process");
50
58
  const fs = __importStar(require("node:fs"));
@@ -52,6 +60,663 @@ const path = __importStar(require("node:path"));
52
60
  const os = __importStar(require("node:os"));
53
61
  const logger_1 = __importDefault(require("../logger"));
54
62
  const config_1 = require("../config");
63
+ 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
+ /**
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.
154
+ */
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) {
192
+ if (!history) {
193
+ return {
194
+ command: 'db push',
195
+ acceptDataLoss: true,
196
+ reason: 'it has no migration history (a new database, or one kept up to date with db push)',
197
+ basis: 'no-history',
198
+ failed: [],
199
+ };
200
+ }
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) {
235
+ return {
236
+ command: 'db push',
237
+ 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',
242
+ failed: history.failed,
243
+ unknown,
244
+ };
245
+ }
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;
295
+ return {
296
+ command: 'migrate deploy',
297
+ 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 },
308
+ };
309
+ }
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
+ }
318
+ return {
319
+ command: 'db push',
320
+ 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,
325
+ };
326
+ }
327
+ return {
328
+ command: 'db push',
329
+ 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',
333
+ failed: [],
334
+ };
335
+ }
336
+ /**
337
+ * The file a SQLite `file:` URL names, or null for anything else. Prisma reads
338
+ * a relative path from the schema's directory, so that is `baseDir`; without
339
+ * one, a relative path gives null.
340
+ */
341
+ function sqliteFilePath(databaseUrl, baseDir) {
342
+ if (!databaseUrl.startsWith('file:'))
343
+ return null;
344
+ const file = databaseUrl.slice('file:'.length).split('?')[0];
345
+ if (path.isAbsolute(file))
346
+ return file;
347
+ return baseDir ? path.resolve(baseDir, file) : null;
348
+ }
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
+ /** The migration history in `_prisma_migrations`, or null when the database has none. */
363
+ function readMigrationHistory(databaseUrl) {
364
+ return __awaiter(this, void 0, void 0, function* () {
365
+ var _a, _b, _c;
366
+ const file = sqliteFilePath(databaseUrl);
367
+ if (file && !fs.existsSync(file))
368
+ return null;
369
+ const { PrismaClient } = yield Promise.resolve().then(() => __importStar(require('../generated/client')));
370
+ const client = new PrismaClient({ datasources: { db: { url: databaseUrl } } });
371
+ try {
372
+ 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) } : {}));
377
+ }
378
+ catch (err) {
379
+ 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);
380
+ if (/no such table|relation .* does not exist/i.test(message))
381
+ return null;
382
+ if (/no such column|column .* does not exist/i.test(message)) {
383
+ // A table of that name that isn't Prisma's. The default setting always
384
+ // started on such a database, with db push, which leaves the table alone.
385
+ logger_1.default.warn("[DBMigrate] The database's _prisma_migrations table is not Prisma's migration " +
386
+ `history (${message}), so Xenon treats the database as having none.`);
387
+ return null;
388
+ }
389
+ throw err;
390
+ }
391
+ finally {
392
+ yield client.$disconnect();
393
+ }
394
+ });
395
+ }
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
+ /** The migrations in prisma/migrations, in the order Prisma applies them. */
423
+ function localMigrationsIn(migrationsDir) {
424
+ return fs
425
+ .readdirSync(migrationsDir)
426
+ .filter((name) => fs.existsSync(path.join(migrationsDir, name, 'migration.sql')))
427
+ .sort();
428
+ }
429
+ /** A word a POSIX shell reads as itself: quoted only when it has to be. */
430
+ function shellWord(word) {
431
+ return /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, "'\\''")}'`;
432
+ }
433
+ /**
434
+ * The commands a failure's message gives, ready to paste. A copy is made with
435
+ * 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.
440
+ */
441
+ function pasteable(at) {
442
+ const schema = shellWord(at.schema);
443
+ const url = shellWord(at.databaseUrl);
444
+ return {
445
+ backup(target) {
446
+ const statement = `VACUUM INTO '${target.replace(/'/g, "''")}'`;
447
+ 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}`;
454
+ },
455
+ letGo() {
456
+ return (`DATABASE_URL=${url} ${at.prisma} db push --skip-generate ` +
457
+ `--accept-data-loss --schema ${schema}`);
458
+ },
459
+ resolve(flag, migration) {
460
+ return (`DATABASE_URL=${url} ${at.prisma} migrate resolve ${flag} ${shellWord(migration)} ` +
461
+ `--schema ${schema}`);
462
+ },
463
+ };
464
+ }
465
+ /** A backup's name: the file's, stamped with the time to the millisecond. */
466
+ function backupOf(file, now) {
467
+ return `${file}.backup-${now.toISOString().replace(/[-:.]/g, '')}`;
468
+ }
469
+ /** The migration a P3018 names, if it does. */
470
+ function failedMigrationIn(output) {
471
+ const match = /Migration name:\s*(\S+)/.exec(output);
472
+ return match ? match[1] : null;
473
+ }
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
+ /**
617
+ * What went wrong and what to do, then Prisma's own output. A refusal to
618
+ * 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.
625
+ */
626
+ function schemaSyncFailure(plan, output, at, { now = new Date(), step, failed, } = {}) {
627
+ var _a, _b, _c, _d, _e, _f, _g;
628
+ const where = (0, database_check_1.describeDatabaseUrl)(at.databaseUrl);
629
+ const run = pasteable(at);
630
+ 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)) {
641
+ sentence =
642
+ `This version adds a required column to a table that already has rows in ${where}, ` +
643
+ 'which prisma db push cannot do: back the file up, then migrate it by hand or start ' +
644
+ 'from a fresh database.';
645
+ }
646
+ 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
+ }
655
+ sentence =
656
+ '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}`;
669
+ }
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}`;
679
+ }
680
+ }
681
+ 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
+ }
705
+ }
706
+ else {
707
+ sentence =
708
+ `prisma ${plan.command} could not bring the database at ${where} up to date: back the ` +
709
+ 'file up and fix what Prisma reports below, or, if its schema already matches this ' +
710
+ 'version, set XENON_AUTO_MIGRATE=false to start without changing it.';
711
+ }
712
+ return `[DBMigrate] Cannot start: ${sentence}\n\n${output}${after}`;
713
+ }
714
+ /** What a failed prisma call printed: its warnings (stdout) and its error (stderr). */
715
+ function cliOutput(err) {
716
+ var _a;
717
+ const parts = [err === null || err === void 0 ? void 0 : err.stdout, err === null || err === void 0 ? void 0 : err.stderr].map((b) => (b ? b.toString().trim() : ''));
718
+ return parts.filter(Boolean).join('\n') || String((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err).trim();
719
+ }
55
720
  function resolvePluginRoot() {
56
721
  let dir = __dirname;
57
722
  for (let i = 0; i < 8; i++) {
@@ -81,49 +746,436 @@ function resolvePrismaInvocation(rootDir) {
81
746
  return { cmd: localBin, prefix: [] };
82
747
  return { cmd: 'npx', prefix: ['prisma'] };
83
748
  }
84
- function runMigrations() {
85
- return __awaiter(this, arguments, void 0, function* (runSync = node_child_process_1.execFileSync) {
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) => {
86
874
  var _a;
87
- if (!config_1.config.autoMigrate) {
88
- logger_1.default.info('[DBMigrate] Auto-migrate disabled by XENON_AUTO_MIGRATE=false. ' +
89
- 'Apply schema changes externally (`prisma migrate deploy` for ' +
90
- 'PostgreSQL, `prisma db push` for SQLite) before the hub boots.');
91
- return;
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);
92
884
  }
93
- const rootDir = resolvePluginRoot();
885
+ return shape;
886
+ };
887
+ }
888
+ /**
889
+ * Bring the database at `config.databaseUrl` up to date with prisma/schema.prisma,
890
+ * by the rule above. Throws an Error whose message says what to do when it can't.
891
+ * `npm run db:migrate` runs this too, whatever XENON_AUTO_MIGRATE says.
892
+ */
893
+ 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();
94
897
  const schemaPath = path.join(rootDir, 'prisma', 'schema.prisma');
95
898
  const migrationsDir = path.join(rootDir, 'prisma', 'migrations');
96
899
  if (!fs.existsSync(schemaPath) || !fs.existsSync(migrationsDir)) {
97
900
  logger_1.default.warn(`[DBMigrate] Skipping auto-migrate: schema or migrations missing at ${rootDir}`);
98
- return;
901
+ return undefined;
99
902
  }
100
- const dbDir = path.dirname(config_1.config.databasePath || path.join(os.homedir(), '.cache', 'xenon', 'xenon.db'));
101
- fs.mkdirSync(dbDir, { recursive: true });
903
+ const databaseUrl = config_1.config.databaseUrl;
904
+ const where = (0, database_check_1.describeDatabaseUrl)(databaseUrl);
905
+ // The file the CLI will use: a relative path is read from prisma/, as Prisma does.
906
+ const file = sqliteFilePath(databaseUrl, path.dirname(schemaPath));
907
+ fs.mkdirSync(path.dirname(file !== null && file !== void 0 ? file : config_1.config.databasePath), { recursive: true });
908
+ if (file)
909
+ sweepTrialCopies(file);
102
910
  const { cmd, prefix } = resolvePrismaInvocation(rootDir);
103
- const env = Object.assign(Object.assign({}, process.env), { DATABASE_URL: config_1.config.databaseUrl });
104
- const isSqlite = config_1.config.databaseProvider === 'sqlite';
105
- const args = isSqlite
106
- ? [...prefix, 'db', 'push', '--skip-generate', '--accept-data-loss', '--schema', schemaPath]
107
- : [...prefix, 'migrate', 'deploy', '--schema', schemaPath];
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
+ });
917
+ let history;
108
918
  try {
109
- logger_1.default.info(`[DBMigrate] Syncing database schema (${config_1.config.databaseProvider}, ${isSqlite ? 'db push' : 'migrate deploy'})...`);
110
- runSync(cmd, args, { env, cwd: rootDir, stdio: 'pipe' });
919
+ history = yield readHistory(file ? `file:${file}` : databaseUrl);
920
+ }
921
+ catch (err) {
922
+ throw new Error('[DBMigrate] Cannot start: Xenon could not read the migration history of the database ' +
923
+ `at ${where}, so it can't tell how to bring it up to date: if another server is using ` +
924
+ '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}`);
926
+ }
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) => {
932
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'xenon-migration-history-'));
933
+ try {
934
+ const lock = path.join(migrationsDir, 'migration_lock.toml');
935
+ if (fs.existsSync(lock))
936
+ fs.copyFileSync(lock, path.join(scratch, 'migration_lock.toml'));
937
+ for (const name of migrations) {
938
+ fs.mkdirSync(path.join(scratch, name));
939
+ fs.copyFileSync(path.join(migrationsDir, name, 'migration.sql'), path.join(scratch, name, 'migration.sql'));
940
+ }
941
+ 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);
956
+ return true;
957
+ }
958
+ catch (err) {
959
+ if ((err === null || err === void 0 ? void 0 : err.status) === 2)
960
+ return false;
961
+ logger_1.default.warn(`[DBMigrate] Could not compare the tables with the migration history: ${cliOutput(err)}`);
962
+ return null;
963
+ }
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)}`;
984
+ }
985
+ };
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'))
1009
+ logger_1.default.warn(announce);
1010
+ else
1011
+ 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;
1029
+ 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
+ }
111
1108
  logger_1.default.info('[DBMigrate] Database schema in sync.');
1109
+ return ran;
112
1110
  }
113
1111
  catch (err) {
114
- const msg = (((_a = err === null || err === void 0 ? void 0 : err.stderr) === null || _a === void 0 ? void 0 : _a.toString()) || (err === null || err === void 0 ? void 0 : err.message) || String(err)).trim();
115
- logger_1.default.error(`[DBMigrate] Auto-sync failed: ${msg}`);
1112
+ const msg = cliOutput(err);
1113
+ logger_1.default.error(`[DBMigrate] prisma ${step !== null && step !== void 0 ? step : ran.command} failed: ${msg}`);
116
1114
  // Boot must not continue. The schema is not what the code expects, so the
117
1115
  // next query fails somewhere unrelated — `prisma.user.count()` reporting a
118
1116
  // missing User table, say — and buries this, the actual cause. Stop here
119
1117
  // and hand back something the operator can act on.
120
- throw new Error(`[DBMigrate] Cannot start: database schema sync failed for ${config_1.config.databaseProvider}.\n` +
121
- `${msg}\n\n` +
122
- `Database: ${config_1.config.databaseUrl}\n` +
123
- `A required column cannot be added to a table that already has rows. ` +
124
- `Back up the database above, then either migrate it by hand or start ` +
125
- `from a fresh one. To boot without touching the schema (the schema must ` +
126
- `already match), set XENON_AUTO_MIGRATE=false.`);
1118
+ const failed = !step && /P3018/.test(msg) ? factsOfFailed(msg) : undefined;
1119
+ throw new Error(schemaSyncFailure(ran, msg, at, { step, failed }));
1120
+ }
1121
+ });
1122
+ }
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
+ /** The startup step: `syncDatabaseSchema`, unless XENON_AUTO_MIGRATE=false. */
1166
+ function runMigrations() {
1167
+ return __awaiter(this, arguments, void 0, function* (runSync = node_child_process_1.execFileSync, readHistory = readMigrationHistory, options = {}) {
1168
+ if (!config_1.config.autoMigrate) {
1169
+ logger_1.default.info('[DBMigrate] Auto-migrate disabled by XENON_AUTO_MIGRATE=false. Bring the schema up ' +
1170
+ 'to date yourself before the server starts: `prisma migrate deploy` for a database ' +
1171
+ 'whose migration history (`_prisma_migrations`) matches its tables (`prisma migrate ' +
1172
+ 'diff --from-migrations <the recorded ones> --to-schema-datasource prisma/schema.prisma ' +
1173
+ '--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.');
1177
+ return;
127
1178
  }
1179
+ yield syncDatabaseSchema(runSync, readHistory, options);
128
1180
  });
129
1181
  }