@xenon-device-management/xenon 2.16.0 → 2.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-2W4JvEyZ.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-GmckX_7r.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DqODzkyo.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-7hETKEWb.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-GOgssFEP.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-Yvf6RGMd.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-KrPnfdsf.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-C6WjZFeI.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CSRIhZEh.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CB8ST3nE.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C9o90KId.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-CPm3w9l2.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-DwIgERRt.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-DrLbHifn.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-DU_Ls06P.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-CWYdEzJ_.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BqNVdln7.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BtB0_Jg_.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-C8NhdtyN.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-gMg0ffvv.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CiYZ3N4M.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-EaaqDmLN.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-DDau4Axv.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-DQ83B1ed.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-K1ovTQam.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-Biq00pMt.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CBv88cH7.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-Bmltailn.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-BPrld-3h.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-t0QB14JI.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BgGZ8AvV.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-BroHxNOh.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-Bjmf49i6.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-DeD__6wa.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/{index-B0gQkEg0.js → index-Dk5J6HB1.js} +2 -2
- package/lib/public/assets/{index-B0JeuZbg.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-72oF89LJ.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-DIc4MSC7.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-BcWm2vW8.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-B06IJJad.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-pVLvnN30.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-CN77vy--.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CSdzQSkY.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-BtrN5IkI.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CNsQfo6w.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-BSfA8ZxG.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-BfTebCK5.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-D8KUX6Qg.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-Cvc8xzic.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-C9Zj84UF.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-Bi27STgs.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-CPsOuwSu.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DEQ-DBOX.js → runbook-page-CWLkC1Fd.js} +1 -1
- package/lib/public/assets/{select-BWpnhWXj.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BefSCSpe.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-MSk96Zz7.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/{session-detail-page-DPyBNef_.js → session-detail-page-C0wYaXqy.js} +1 -1
- package/lib/public/assets/{settings-V6x-cXwy.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-Duex-GZD.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BBcoqBmL.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-ts8p5rEh.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-CZ2ew6AZ.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-Dv99y_le.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-DrYC8JCF.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C7QkBNkP.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-gJ5Y01yo.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D7tKMESH.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-CqZVuZGs.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-DI5DSW99.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-ClcHFxfo.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +1 -1
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/dashboard/event-manager.js +5 -8
- package/lib/src/device-utils.js +11 -2
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/plugin.js +4 -1
- package/lib/src/scripts/initialize-database.js +0 -6
- package/lib/src/scripts/run-migrations.js +123 -880
- package/lib/src/services/SessionLifecycleService.js +20 -5
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +199 -44
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/sessions/RemoteSession.js +11 -2
- package/lib/src/sessions/SessionManager.js +28 -1
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +15 -0
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- package/lib/test/unit/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/run-migrations-database.spec.js +57 -424
- package/lib/test/unit/run-migrations.test.js +74 -674
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +32 -94
- package/lib/test/unit/session-log-indexes.spec.js +15 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/schema.json +1 -1
|
@@ -68,7 +68,7 @@ describe('planSchemaSync', () => {
|
|
|
68
68
|
(0, chai_1.expect)(plan.command).to.equal('migrate deploy');
|
|
69
69
|
(0, chai_1.expect)(compared).to.deep.equal([LOCAL.slice(0, 2)]);
|
|
70
70
|
});
|
|
71
|
-
it('pushes when the tables
|
|
71
|
+
it('pushes when the tables are ahead of the history, so deploy would redo a change', () => {
|
|
72
72
|
const history = { applied: LOCAL.slice(0, 1), failed: [] };
|
|
73
73
|
const plan = (0, run_migrations_1.planSchemaSync)(history, LOCAL, () => false);
|
|
74
74
|
(0, chai_1.expect)(plan.command).to.equal('db push');
|
|
@@ -76,7 +76,6 @@ describe('planSchemaSync', () => {
|
|
|
76
76
|
(0, chai_1.expect)(plan.reason).to.match(/differ from what its recorded migrations make/);
|
|
77
77
|
// The failure message depends on why: see schemaSyncFailure.
|
|
78
78
|
(0, chai_1.expect)(plan.basis).to.equal('tables-differ');
|
|
79
|
-
(0, chai_1.expect)(plan.resolve).to.equal(undefined);
|
|
80
79
|
});
|
|
81
80
|
it('deploys, as the history says, when the tables could not be compared', () => {
|
|
82
81
|
const history = { applied: LOCAL.slice(0, 1), failed: [] };
|
|
@@ -84,234 +83,15 @@ describe('planSchemaSync', () => {
|
|
|
84
83
|
(0, chai_1.expect)(plan.command).to.equal('migrate deploy');
|
|
85
84
|
(0, chai_1.expect)(plan.reason).to.match(/could not be compared/);
|
|
86
85
|
});
|
|
87
|
-
it('pushes a database whose history records a failed migration
|
|
86
|
+
it('pushes a database whose history records a failed migration', () => {
|
|
88
87
|
const history = { applied: LOCAL.slice(0, 2), failed: [LOCAL[2]] };
|
|
89
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, LOCAL,
|
|
88
|
+
const plan = (0, run_migrations_1.planSchemaSync)(history, LOCAL, unasked);
|
|
90
89
|
(0, chai_1.expect)(plan.command).to.equal('db push');
|
|
91
90
|
(0, chai_1.expect)(plan.acceptDataLoss).to.equal(false);
|
|
92
91
|
(0, chai_1.expect)(plan.reason).to.include(LOCAL[2]);
|
|
93
92
|
(0, chai_1.expect)(plan.basis).to.equal('failed-migration');
|
|
94
93
|
(0, chai_1.expect)(plan.failed).to.deep.equal([LOCAL[2]]);
|
|
95
94
|
});
|
|
96
|
-
// Re-baselining. db push writes no history, so a history behind its tables,
|
|
97
|
-
// or one with a failed migration, stayed on db push for good: at the first
|
|
98
|
-
// release whose migration drops a column or adds a unique index, db push
|
|
99
|
-
// without --accept-data-loss refused it. When the tables are exactly what
|
|
100
|
-
// the first k migrations make, the history can say so.
|
|
101
|
-
describe('re-baselining', () => {
|
|
102
|
-
const FOUR = [...LOCAL, '20250104_d'];
|
|
103
|
-
it('records the migrations the tables already have, then deploys the rest', () => {
|
|
104
|
-
const history = { applied: FOUR.slice(0, 1), failed: [] };
|
|
105
|
-
const compared = [];
|
|
106
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => {
|
|
107
|
-
compared.push(migrations.length);
|
|
108
|
-
return migrations.length === 3;
|
|
109
|
-
});
|
|
110
|
-
(0, chai_1.expect)(plan.command).to.equal('migrate deploy');
|
|
111
|
-
(0, chai_1.expect)(plan.basis).to.equal('rebaselined');
|
|
112
|
-
(0, chai_1.expect)(plan.resolve).to.deep.equal({ rolledBack: [], applied: FOUR.slice(1, 3) });
|
|
113
|
-
(0, chai_1.expect)(plan.reason).to.match(/first 3 migrations/);
|
|
114
|
-
// The history first; then every migration (what db push leaves); then
|
|
115
|
-
// down; then one below the match, which must not match too.
|
|
116
|
-
(0, chai_1.expect)(compared).to.deep.equal([1, 4, 3, 2]);
|
|
117
|
-
});
|
|
118
|
-
// Prisma's comparison sees tables, never rows: a backfill makes the same
|
|
119
|
-
// tables as the migrations before it. Through #493 the largest k that
|
|
120
|
-
// matched won, so the backfill was recorded as applied and never ran.
|
|
121
|
-
it('runs a migration the comparison cannot see rather than record it', () => {
|
|
122
|
-
const history = { applied: FOUR.slice(0, 2), failed: [] };
|
|
123
|
-
const compared = [];
|
|
124
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => {
|
|
125
|
-
compared.push(migrations.length);
|
|
126
|
-
return migrations.length >= 3;
|
|
127
|
-
});
|
|
128
|
-
(0, chai_1.expect)(plan.basis).to.equal('rebaselined');
|
|
129
|
-
(0, chai_1.expect)(plan.resolve).to.deep.equal({ rolledBack: [], applied: [FOUR[2]] });
|
|
130
|
-
(0, chai_1.expect)(plan.reason).to.match(/first 3 migrations/);
|
|
131
|
-
(0, chai_1.expect)(compared).to.deep.equal([2, 4, 3]);
|
|
132
|
-
});
|
|
133
|
-
const dup = 'Database error code: 1\n\nDatabase error:\nduplicate column name: path';
|
|
134
|
-
const notNull = 'Database error code: 1299\n\nDatabase error:\nNOT NULL constraint failed: Team.name';
|
|
135
|
-
it('rolls a failed migration back first, and records it as applied when the tables have it', () => {
|
|
136
|
-
const history = {
|
|
137
|
-
applied: FOUR.slice(0, 2),
|
|
138
|
-
failed: [FOUR[2]],
|
|
139
|
-
failureLogs: { [FOUR[2]]: dup },
|
|
140
|
-
};
|
|
141
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => migrations.length === 4);
|
|
142
|
-
(0, chai_1.expect)(plan.command).to.equal('migrate deploy');
|
|
143
|
-
(0, chai_1.expect)(plan.basis).to.equal('rebaselined');
|
|
144
|
-
(0, chai_1.expect)(plan.resolve).to.deep.equal({ rolledBack: [FOUR[2]], applied: FOUR.slice(2, 4) });
|
|
145
|
-
});
|
|
146
|
-
// The rows a migration failed on stay as they were until someone changes
|
|
147
|
-
// them, and a migration can fail after its first statements ran. Through
|
|
148
|
-
// #493 the start after such a failure recorded it as applied.
|
|
149
|
-
it('leaves a failed migration alone when the tables have what it makes but it failed on the rows', () => {
|
|
150
|
-
var _a;
|
|
151
|
-
const history = {
|
|
152
|
-
applied: FOUR.slice(0, 3),
|
|
153
|
-
failed: [FOUR[3]],
|
|
154
|
-
failureLogs: { [FOUR[3]]: notNull },
|
|
155
|
-
};
|
|
156
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => migrations.length === 4);
|
|
157
|
-
(0, chai_1.expect)(plan.command).to.equal('none');
|
|
158
|
-
(0, chai_1.expect)(plan.basis).to.equal('failed-unresolved');
|
|
159
|
-
(0, chai_1.expect)(plan.resolve).to.equal(undefined);
|
|
160
|
-
(0, chai_1.expect)(plan.unresolved).to.deep.include({ migration: FOUR[3], why: 'part-way' });
|
|
161
|
-
(0, chai_1.expect)((_a = plan.unresolved) === null || _a === void 0 ? void 0 : _a.logs).to.include('NOT NULL');
|
|
162
|
-
});
|
|
163
|
-
it('leaves a failed migration alone when its record says nothing of why it failed', () => {
|
|
164
|
-
const history = { applied: FOUR.slice(0, 3), failed: [FOUR[3]] };
|
|
165
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => migrations.length === 4);
|
|
166
|
-
(0, chai_1.expect)(plan.basis).to.equal('failed-unresolved');
|
|
167
|
-
});
|
|
168
|
-
const backfill = { statements: 1, unseen: ['UPDATE "Team" SET "name" = ...'] };
|
|
169
|
-
const twoBackfills = {
|
|
170
|
-
statements: 2,
|
|
171
|
-
unseen: ['UPDATE "Team" SET "a" = 1', 'UPDATE "Team" SET "b" = 2'],
|
|
172
|
-
};
|
|
173
|
-
const shapes = (special) => (name) => { var _a; return (_a = special[name]) !== null && _a !== void 0 ? _a : { statements: 1, unseen: [] }; };
|
|
174
|
-
it('rolls back a one-statement backfill that failed, so deploy runs it again', () => {
|
|
175
|
-
const history = {
|
|
176
|
-
applied: FOUR.slice(0, 3),
|
|
177
|
-
failed: [FOUR[3]],
|
|
178
|
-
failureLogs: { [FOUR[3]]: notNull },
|
|
179
|
-
};
|
|
180
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => migrations.length >= 3, shapes({ [FOUR[3]]: backfill }));
|
|
181
|
-
(0, chai_1.expect)(plan.basis).to.equal('rebaselined');
|
|
182
|
-
(0, chai_1.expect)(plan.resolve).to.deep.equal({ rolledBack: [FOUR[3]], applied: [] });
|
|
183
|
-
});
|
|
184
|
-
it("leaves a failed backfill of several statements alone: the tables can't show how far it got", () => {
|
|
185
|
-
const history = {
|
|
186
|
-
applied: FOUR.slice(0, 3),
|
|
187
|
-
failed: [FOUR[3]],
|
|
188
|
-
failureLogs: { [FOUR[3]]: notNull },
|
|
189
|
-
};
|
|
190
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => migrations.length >= 3, shapes({ [FOUR[3]]: twoBackfills }));
|
|
191
|
-
(0, chai_1.expect)(plan.command).to.equal('none');
|
|
192
|
-
(0, chai_1.expect)(plan.unresolved).to.deep.include({ migration: FOUR[3], why: 'part-way' });
|
|
193
|
-
});
|
|
194
|
-
it("never records a failed migration as applied when part of it is what the tables can't show", () => {
|
|
195
|
-
const history = {
|
|
196
|
-
applied: FOUR.slice(0, 3),
|
|
197
|
-
failed: [FOUR[3]],
|
|
198
|
-
failureLogs: { [FOUR[3]]: dup },
|
|
199
|
-
};
|
|
200
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => migrations.length === 4, shapes({ [FOUR[3]]: { statements: 2, unseen: ['UPDATE "LocatorEtalon" ...'] } }));
|
|
201
|
-
(0, chai_1.expect)(plan.command).to.equal('none');
|
|
202
|
-
(0, chai_1.expect)(plan.unresolved).to.deep.include({ migration: FOUR[3], why: 'unseen-part' });
|
|
203
|
-
});
|
|
204
|
-
it("keeps a database on db push when a migration it would record changes what the tables can't show", () => {
|
|
205
|
-
const history = { applied: FOUR.slice(0, 1), failed: [] };
|
|
206
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => migrations.length === 3, shapes({ [FOUR[1]]: backfill }));
|
|
207
|
-
(0, chai_1.expect)(plan.command).to.equal('db push');
|
|
208
|
-
(0, chai_1.expect)(plan.acceptDataLoss).to.equal(false);
|
|
209
|
-
(0, chai_1.expect)(plan.basis).to.equal('unrecordable');
|
|
210
|
-
(0, chai_1.expect)(plan.unseen).to.deep.equal([FOUR[1]]);
|
|
211
|
-
(0, chai_1.expect)(plan.resolve).to.equal(undefined);
|
|
212
|
-
});
|
|
213
|
-
it('leaves a failed migration of several statements alone when the tables match no run', () => {
|
|
214
|
-
const history = { applied: FOUR.slice(0, 3), failed: [FOUR[3]] };
|
|
215
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, () => false, shapes({ [FOUR[3]]: twoBackfills }));
|
|
216
|
-
(0, chai_1.expect)(plan.command).to.equal('none');
|
|
217
|
-
(0, chai_1.expect)(plan.basis).to.equal('failed-unresolved');
|
|
218
|
-
});
|
|
219
|
-
it('rolls back a failed migration that left the tables as they were, so deploy runs it again', () => {
|
|
220
|
-
const history = { applied: FOUR.slice(0, 2), failed: [FOUR[2]] };
|
|
221
|
-
const compared = [];
|
|
222
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => {
|
|
223
|
-
compared.push(migrations.length);
|
|
224
|
-
return migrations.length === 2;
|
|
225
|
-
});
|
|
226
|
-
(0, chai_1.expect)(plan.command).to.equal('migrate deploy');
|
|
227
|
-
(0, chai_1.expect)(plan.resolve).to.deep.equal({ rolledBack: [FOUR[2]], applied: [] });
|
|
228
|
-
// Every migration first, then the history's own, before the rest.
|
|
229
|
-
(0, chai_1.expect)(compared).to.deep.equal([4, 2]);
|
|
230
|
-
});
|
|
231
|
-
it('never records fewer migrations than the history has', () => {
|
|
232
|
-
const history = { applied: FOUR.slice(0, 3), failed: [] };
|
|
233
|
-
const compared = [];
|
|
234
|
-
(0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => {
|
|
235
|
-
compared.push(migrations.length);
|
|
236
|
-
return false;
|
|
237
|
-
});
|
|
238
|
-
(0, chai_1.expect)(Math.min(...compared)).to.equal(3);
|
|
239
|
-
});
|
|
240
|
-
it('stops looking when the tables cannot be compared', () => {
|
|
241
|
-
const history = { applied: FOUR.slice(0, 1), failed: [] };
|
|
242
|
-
const compared = [];
|
|
243
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => {
|
|
244
|
-
compared.push(migrations.length);
|
|
245
|
-
return migrations.length === 1 ? false : null;
|
|
246
|
-
});
|
|
247
|
-
(0, chai_1.expect)(plan.command).to.equal('db push');
|
|
248
|
-
(0, chai_1.expect)(plan.basis).to.equal('tables-differ');
|
|
249
|
-
(0, chai_1.expect)(compared).to.deep.equal([1, 4]);
|
|
250
|
-
});
|
|
251
|
-
it('records a migration missing from the middle of the history when the tables have it', () => {
|
|
252
|
-
const history = { applied: [FOUR[0], FOUR[2]], failed: [] };
|
|
253
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => migrations.length === 3);
|
|
254
|
-
(0, chai_1.expect)(plan.command).to.equal('migrate deploy');
|
|
255
|
-
(0, chai_1.expect)(plan.resolve).to.deep.equal({ rolledBack: [], applied: [FOUR[1]] });
|
|
256
|
-
});
|
|
257
|
-
// A history that names a migration this release doesn't have (another
|
|
258
|
-
// branch's, or a later release's) is not this release's to rewrite.
|
|
259
|
-
it('leaves a history that names a migration this release lacks as it is', () => {
|
|
260
|
-
const history = { applied: [...LOCAL, '20990101_elsewhere'], failed: [] };
|
|
261
|
-
const compared = [];
|
|
262
|
-
const plan = (0, run_migrations_1.planSchemaSync)(history, FOUR, (migrations) => {
|
|
263
|
-
compared.push(migrations.length);
|
|
264
|
-
return false;
|
|
265
|
-
});
|
|
266
|
-
(0, chai_1.expect)(plan.command).to.equal('db push');
|
|
267
|
-
(0, chai_1.expect)(plan.basis).to.equal('other-release');
|
|
268
|
-
(0, chai_1.expect)(plan.unknown).to.deep.equal(['20990101_elsewhere']);
|
|
269
|
-
(0, chai_1.expect)(plan.resolve).to.equal(undefined);
|
|
270
|
-
(0, chai_1.expect)(compared, 'only the history itself').to.deep.equal([3]);
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
describe('migrationShape', () => {
|
|
275
|
-
it('counts what the tables show: tables, columns, indexes and the copy of a redefined table', () => {
|
|
276
|
-
const shape = (0, run_migrations_1.migrationShape)('-- RedefineTables\nPRAGMA defer_foreign_keys=ON;\nPRAGMA foreign_keys=OFF;\n' +
|
|
277
|
-
'CREATE TABLE "new_Team" ("id" TEXT NOT NULL PRIMARY KEY, "name" TEXT NOT NULL);\n' +
|
|
278
|
-
'INSERT INTO "new_Team" ("id", "name") SELECT "id", "name" FROM "Team";\n' +
|
|
279
|
-
'DROP TABLE "Team";\nALTER TABLE "new_Team" RENAME TO "Team";\n' +
|
|
280
|
-
'CREATE UNIQUE INDEX "Team_name_key" ON "Team"("name");\n' +
|
|
281
|
-
'CREATE INDEX "x" ON "Team"("name") WHERE "name" IS NOT NULL;\n' +
|
|
282
|
-
'DROP INDEX "y";\nPRAGMA foreign_keys=ON;\n');
|
|
283
|
-
(0, chai_1.expect)(shape).to.deep.equal({ statements: 7, unseen: [] });
|
|
284
|
-
});
|
|
285
|
-
it("finds what the tables can't show: rows, triggers, views, collations", () => {
|
|
286
|
-
const shape = (0, run_migrations_1.migrationShape)('UPDATE "Team" SET "name" = \'a; b\' || "name"; -- a comment; with a semicolon\n' +
|
|
287
|
-
'INSERT INTO "TeamMember" ("teamId") SELECT "id" FROM "Team";\n' +
|
|
288
|
-
'DELETE FROM "Log";\n' +
|
|
289
|
-
'CREATE VIEW "v" AS SELECT 1;\n' +
|
|
290
|
-
'CREATE TRIGGER "t" AFTER UPDATE ON "Team" BEGIN SELECT 1; END;\n' +
|
|
291
|
-
'CREATE INDEX "c" ON "Team"("name" COLLATE NOCASE);\n' +
|
|
292
|
-
'/* a block; comment */ INSERT INTO "new_Team" ("id") SELECT "id" FROM "Other";\n');
|
|
293
|
-
(0, chai_1.expect)(shape.unseen).to.have.length(8);
|
|
294
|
-
(0, chai_1.expect)(shape.unseen[0]).to.match(/^UPDATE "Team" SET "name" = 'a; b'/);
|
|
295
|
-
(0, chai_1.expect)(shape.statements).to.equal(8);
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
describe('schemaMissingIn', () => {
|
|
299
|
-
it('finds nothing missing where the database only has more', () => {
|
|
300
|
-
(0, chai_1.expect)((0, run_migrations_1.schemaMissingIn)('No difference detected.\n')).to.equal(null);
|
|
301
|
-
(0, chai_1.expect)((0, run_migrations_1.schemaMissingIn)('\n[+] Added tables\n - LabNotes\n\n[*] Changed the `Team` table\n' +
|
|
302
|
-
' [+] Added column `labCol`\n [+] Added index on columns (createdAt)\n')).to.equal(null);
|
|
303
|
-
});
|
|
304
|
-
it('lists what the database lacks, with its table', () => {
|
|
305
|
-
const missing = (0, run_migrations_1.schemaMissingIn)('\n[+] Added tables\n - LabNotes\n\n[*] Redefined table `LocatorEtalon`\n\n' +
|
|
306
|
-
'[*] Changed the `SessionLog` table\n [-] Removed index on columns (session_id, createdAt)\n' +
|
|
307
|
-
'\n[*] Changed the `Team` table\n [+] Added column `labCol`\n');
|
|
308
|
-
(0, chai_1.expect)(missing).to.equal('[*] Redefined table `LocatorEtalon`\n[*] Changed the `SessionLog` table\n' +
|
|
309
|
-
' [-] Removed index on columns (session_id, createdAt)');
|
|
310
|
-
});
|
|
311
|
-
it("counts a line it doesn't know as missing", () => {
|
|
312
|
-
(0, chai_1.expect)((0, run_migrations_1.schemaMissingIn)('Something new from Prisma')).to.equal('Something new from Prisma');
|
|
313
|
-
(0, chai_1.expect)((0, run_migrations_1.schemaMissingIn)('[-] Removed tables\n - Session')).to.equal('[-] Removed tables\n - Session');
|
|
314
|
-
});
|
|
315
95
|
});
|
|
316
96
|
describe('runMigrations chooses by the database, not by databaseProvider', () => {
|
|
317
97
|
const scratchUrl = `file:${path_1.default.join(os_1.default.tmpdir(), `xenon-choose-${process.pid}-absent.db`)}`;
|
|
@@ -374,38 +154,20 @@ describe('runMigrations: what each plan runs', () => {
|
|
|
374
154
|
Object.assign(config_1.config, saved);
|
|
375
155
|
sinon_1.default.restore();
|
|
376
156
|
});
|
|
377
|
-
/**
|
|
378
|
-
* Every prisma call's arguments and database URL, answered by the given
|
|
379
|
-
* steps: `diff` compares the tables with a run of migrations (`--exit-code`),
|
|
380
|
-
* `lack` lists what the tables lack of the recorded migrations before a
|
|
381
|
-
* copy, `check` what the copy lacks of the schema after the migrations,
|
|
382
|
-
* `resolve` writes the history, and `update` is the rest. A summary step
|
|
383
|
-
* left out finds nothing missing.
|
|
384
|
-
*/
|
|
157
|
+
/** Every prisma call's arguments, with `diff` and the update answered by the given steps. */
|
|
385
158
|
function run(history_1) {
|
|
386
|
-
return __awaiter(this, arguments, void 0, function* (history, steps = {}
|
|
159
|
+
return __awaiter(this, arguments, void 0, function* (history, steps = {}) {
|
|
387
160
|
const calls = [];
|
|
388
|
-
const urls = [];
|
|
389
161
|
try {
|
|
390
|
-
yield (0, run_migrations_1.runMigrations)((_cmd, args
|
|
391
|
-
const url = String(opts.env.DATABASE_URL);
|
|
162
|
+
yield (0, run_migrations_1.runMigrations)((_cmd, args) => {
|
|
392
163
|
calls.push(args);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
const step = args.includes('diff')
|
|
399
|
-
? steps.diff
|
|
400
|
-
: args.includes('resolve')
|
|
401
|
-
? steps.resolve
|
|
402
|
-
: steps.update;
|
|
403
|
-
return step ? step(args, url) : Buffer.from('ok');
|
|
404
|
-
}, () => __awaiter(this, void 0, void 0, function* () { return history; }), options);
|
|
405
|
-
return { calls, urls };
|
|
164
|
+
const step = args.includes('diff') ? steps.diff : steps.update;
|
|
165
|
+
return step ? step(args) : Buffer.from('ok');
|
|
166
|
+
}, () => __awaiter(this, void 0, void 0, function* () { return history; }));
|
|
167
|
+
return { calls };
|
|
406
168
|
}
|
|
407
169
|
catch (error) {
|
|
408
|
-
return { calls,
|
|
170
|
+
return { calls, error };
|
|
409
171
|
}
|
|
410
172
|
});
|
|
411
173
|
}
|
|
@@ -417,47 +179,19 @@ describe('runMigrations: what each plan runs', () => {
|
|
|
417
179
|
});
|
|
418
180
|
};
|
|
419
181
|
const update = (calls) => calls[calls.length - 1];
|
|
420
|
-
/** The number of migrations a `migrate diff --from-migrations <dir>` call builds. */
|
|
421
|
-
const migrationsIn = (args) => fs_1.default.readdirSync(args[args.indexOf('--from-migrations') + 1]).filter((n) => !n.endsWith('.toml'))
|
|
422
|
-
.length;
|
|
423
|
-
/** A diff that finds the tables equal to the first `k` migrations, and only those. */
|
|
424
|
-
const tablesAreFirst = (k) => (args) => migrationsIn(args) === k ? Buffer.from('') : fail(2, '')();
|
|
425
|
-
/** The prisma subcommands called, as words: `migrate resolve --applied x`. */
|
|
426
|
-
const words = (calls) => calls.map((args) => args
|
|
427
|
-
.filter((a) => !a.includes(path_1.default.sep) && a !== '--schema' && a !== '--exit-code')
|
|
428
|
-
.join(' '));
|
|
429
|
-
// What `db push` without --accept-data-loss prints when it would drop a table
|
|
430
|
-
// that holds rows: the list on stdout, the refusal on stderr.
|
|
431
|
-
const refusesDataLoss = fail(1, 'Error: Use the --accept-data-loss flag to ignore the data loss warnings like prisma db push --accept-data-loss', '⚠️ There might be data loss when applying the changes:\n\n' +
|
|
432
|
-
' • You are about to drop the `LabNotes` table, which is not empty (1 rows).');
|
|
433
|
-
const root = path_1.default.resolve(__dirname, '../..');
|
|
434
|
-
const prisma = path_1.default.join(root, 'node_modules', '.bin', 'prisma');
|
|
435
|
-
const schema = path_1.default.join(root, 'prisma', 'schema.prisma');
|
|
436
|
-
const scratchFile = scratchUrl.slice('file:'.length);
|
|
437
|
-
/** The commands a failure message gives, one to an indented line, in its order. */
|
|
438
|
-
const commandsIn = (message) => message
|
|
439
|
-
.split('\n')
|
|
440
|
-
.filter((line) => /^ {2}(DATABASE_URL=|echo |touch )/.test(line))
|
|
441
|
-
.map((line) => line.trim());
|
|
442
|
-
const letGo = `DATABASE_URL=file:${scratchFile} ${prisma} db push --skip-generate --accept-data-loss ` +
|
|
443
|
-
`--schema ${schema}`;
|
|
444
|
-
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
445
|
-
/** One command: a backup, and once it is made, `letGo`. */
|
|
446
|
-
const letGoAfterBackup = new RegExp(`^(touch \\S+ && chmod [0-7]{3} \\S+ && )?echo "VACUUM INTO '${escape(scratchFile)}\\.backup-` +
|
|
447
|
-
`\\d{8}T\\d{9}Z'" \\| \\S+ db execute --url \\S+ --stdin && ${escape(letGo)}$`);
|
|
448
182
|
it('accepts data loss only for a database with no history, as the default always did', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
449
183
|
const { calls } = yield run(null);
|
|
450
184
|
(0, chai_1.expect)(update(calls)).to.include.members(['db', 'push', '--accept-data-loss']);
|
|
451
185
|
}));
|
|
452
|
-
it('pushes without accepting data loss when the tables
|
|
186
|
+
it('pushes without accepting data loss when the tables differ from the history', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
453
187
|
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
454
188
|
const { calls } = yield run(history, { diff: fail(2, '') });
|
|
455
189
|
(0, chai_1.expect)(update(calls)).to.include.members(['db', 'push']);
|
|
456
190
|
(0, chai_1.expect)(update(calls)).to.not.include('--accept-data-loss');
|
|
457
191
|
}));
|
|
458
|
-
it('pushes without accepting data loss when the history records a failed migration
|
|
192
|
+
it('pushes without accepting data loss when the history records a failed migration', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
459
193
|
const history = { applied: everyMigration.slice(0, -1), failed: everyMigration.slice(-1) };
|
|
460
|
-
const { calls } = yield run(history
|
|
194
|
+
const { calls } = yield run(history);
|
|
461
195
|
(0, chai_1.expect)(update(calls)).to.include.members(['db', 'push']);
|
|
462
196
|
(0, chai_1.expect)(update(calls)).to.not.include('--accept-data-loss');
|
|
463
197
|
}));
|
|
@@ -466,381 +200,91 @@ describe('runMigrations: what each plan runs', () => {
|
|
|
466
200
|
const { calls } = yield run(history);
|
|
467
201
|
(0, chai_1.expect)(update(calls)).to.include.members(['migrate', 'deploy']);
|
|
468
202
|
}));
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
(0, chai_1.expect)(error).to.equal(undefined);
|
|
490
|
-
(0, chai_1.expect)(words(calls).filter((w) => !w.startsWith('migrate diff'))).to.deep.equal([
|
|
491
|
-
`migrate resolve --rolled-back ${newest}`,
|
|
492
|
-
`migrate resolve --applied ${newest}`,
|
|
493
|
-
'migrate deploy',
|
|
494
|
-
]);
|
|
495
|
-
}));
|
|
496
|
-
// The start after a migration failed on the rows: the history stays as it
|
|
497
|
-
// is, nothing runs, and the message says how to finish or undo it.
|
|
498
|
-
it('changes nothing, and says how to finish or undo it, for a failed migration it cannot resolve', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
203
|
+
// What `db push` without --accept-data-loss prints when it would drop a table
|
|
204
|
+
// that holds rows: the list on stdout, the refusal on stderr.
|
|
205
|
+
const refusesDataLoss = fail(1, 'Error: Use the --accept-data-loss flag to ignore the data loss warnings like prisma db push --accept-data-loss', '⚠️ There might be data loss when applying the changes:\n\n' +
|
|
206
|
+
' • You are about to drop the `LabNotes` table, which is not empty (1 rows).');
|
|
207
|
+
const root = path_1.default.resolve(__dirname, '../..');
|
|
208
|
+
const prisma = path_1.default.join(root, 'node_modules', '.bin', 'prisma');
|
|
209
|
+
const schema = path_1.default.join(root, 'prisma', 'schema.prisma');
|
|
210
|
+
const scratchFile = scratchUrl.slice('file:'.length);
|
|
211
|
+
/** The commands a failure message gives, one to an indented line, in its order. */
|
|
212
|
+
const commandsIn = (message) => message
|
|
213
|
+
.split('\n')
|
|
214
|
+
.filter((line) => /^ {2}(DATABASE_URL=|echo )/.test(line))
|
|
215
|
+
.map((line) => line.trim());
|
|
216
|
+
const letGo = `DATABASE_URL=file:${scratchFile} ${prisma} db push --skip-generate --accept-data-loss ` +
|
|
217
|
+
`--schema ${schema}`;
|
|
218
|
+
// A history database whose tables differ from its migrations: `migrate
|
|
219
|
+
// deploy` works on it when something was added by hand, and fails (P3018)
|
|
220
|
+
// when db push moved the tables past the history. The message can't tell
|
|
221
|
+
// which, so it never sends migrate deploy to the file before a copy.
|
|
222
|
+
it('says how to let go of what db push would delete, or keep it by trying migrate deploy on a copy first', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
499
223
|
var _a;
|
|
500
|
-
const
|
|
501
|
-
const
|
|
502
|
-
applied: everyMigration.slice(0, -1),
|
|
503
|
-
failed: [newest],
|
|
504
|
-
failureLogs: {
|
|
505
|
-
[newest]: 'Database error:\nNOT NULL constraint failed: Team.name\n\n 0: sql_schema_connector::apply',
|
|
506
|
-
},
|
|
507
|
-
};
|
|
508
|
-
const { calls, error } = yield run(history, { diff: tablesAreFirst(everyMigration.length) });
|
|
224
|
+
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
225
|
+
const { error } = yield run(history, { diff: fail(2, ''), update: refusesDataLoss });
|
|
509
226
|
(0, chai_1.expect)(error, 'the start stops').to.be.an('Error');
|
|
510
|
-
(0, chai_1.expect)(words(calls).filter((w) => !w.startsWith('migrate diff'))).to.deep.equal([]);
|
|
511
227
|
const message = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '';
|
|
512
|
-
(0, chai_1.expect)(message).to.
|
|
513
|
-
(0, chai_1.expect)(message).to.match(/
|
|
514
|
-
|
|
515
|
-
(0, chai_1.expect)(message
|
|
516
|
-
(0, chai_1.expect)(message).to.
|
|
228
|
+
(0, chai_1.expect)(message).to.match(/stopped rather than delete/);
|
|
229
|
+
(0, chai_1.expect)(message).to.match(/differ from what its recorded migrations make/);
|
|
230
|
+
// Prisma's own list of what it would drop, which is only on stdout.
|
|
231
|
+
(0, chai_1.expect)(message).to.include('LabNotes');
|
|
232
|
+
(0, chai_1.expect)(message).to.not.match(/required column/);
|
|
233
|
+
(0, chai_1.expect)(message, 'no plain advice to migrate deploy the file').to.not.match(/by hand with prisma migrate deploy/);
|
|
517
234
|
const commands = commandsIn(message);
|
|
518
|
-
|
|
519
|
-
|
|
235
|
+
const backup = commands.findIndex((c) => c.includes(`VACUUM INTO '${scratchFile}.backup-`) &&
|
|
236
|
+
c.endsWith(`| ${prisma} db execute --url file:${scratchFile} --stdin`));
|
|
237
|
+
(0, chai_1.expect)(backup, `a backup first:\n${message}`).to.equal(0);
|
|
238
|
+
(0, chai_1.expect)(commands).to.include(letGo);
|
|
239
|
+
const onCopy = commands.findIndex((c) => c.startsWith(`DATABASE_URL=file:${scratchFile}.copy ${prisma} migrate deploy --schema ${schema}`));
|
|
240
|
+
const copied = commands.findIndex((c) => c.includes(`VACUUM INTO '${scratchFile}.copy'`));
|
|
241
|
+
const onFile = commands.indexOf(`DATABASE_URL=file:${scratchFile} ${prisma} migrate deploy --schema ${schema}`);
|
|
242
|
+
(0, chai_1.expect)(copied, 'the copy is made').to.be.greaterThan(backup);
|
|
243
|
+
(0, chai_1.expect)(onCopy, 'migrate deploy runs on the copy').to.be.greaterThan(copied);
|
|
244
|
+
(0, chai_1.expect)(onFile, 'and on the file only after the copy').to.be.greaterThan(onCopy);
|
|
520
245
|
}));
|
|
521
|
-
describe('the copy', () => {
|
|
522
|
-
const trialPrefix = `${path_1.default.basename(scratchFile)}.xenon-trial-`;
|
|
523
|
-
const trialFiles = () => fs_1.default.readdirSync(path_1.default.dirname(scratchFile)).filter((n) => n.startsWith(trialPrefix));
|
|
524
|
-
beforeEach(() => {
|
|
525
|
-
fs_1.default.writeFileSync(scratchFile, '');
|
|
526
|
-
});
|
|
527
|
-
afterEach(() => {
|
|
528
|
-
fs_1.default.rmSync(scratchFile, { force: true });
|
|
529
|
-
for (const name of trialFiles())
|
|
530
|
-
fs_1.default.rmSync(path_1.default.join(path_1.default.dirname(scratchFile), name));
|
|
531
|
-
});
|
|
532
|
-
/** A copier that makes the file it is asked for, and notes where. */
|
|
533
|
-
function copier(made) {
|
|
534
|
-
return (_from, to) => __awaiter(this, void 0, void 0, function* () {
|
|
535
|
-
made.push(to);
|
|
536
|
-
fs_1.default.writeFileSync(to, 'copy');
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
const roomy = () => 1e12;
|
|
540
|
-
it('runs migrate deploy on a copy next to the database first, then on the database', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
541
|
-
const made = [];
|
|
542
|
-
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
543
|
-
const { calls, urls, error } = yield run(history, { diff: fail(2, '') }, { copyDatabase: copier(made), freeBytes: roomy });
|
|
544
|
-
(0, chai_1.expect)(error).to.equal(undefined);
|
|
545
|
-
(0, chai_1.expect)(made).to.have.length(1);
|
|
546
|
-
(0, chai_1.expect)(path_1.default.dirname(made[0])).to.equal(path_1.default.dirname(scratchFile));
|
|
547
|
-
(0, chai_1.expect)(path_1.default.basename(made[0])).to.equal(`${trialPrefix}${process.pid}`);
|
|
548
|
-
const deploys = urls.filter((_, i) => calls[i].includes('deploy'));
|
|
549
|
-
(0, chai_1.expect)(deploys).to.deep.equal([`file:${made[0]}`, scratchUrl]);
|
|
550
|
-
(0, chai_1.expect)(calls.some((args) => args.includes('push')), 'no db push').to.equal(false);
|
|
551
|
-
(0, chai_1.expect)(trialFiles(), 'the copy is deleted').to.deep.equal([]);
|
|
552
|
-
}));
|
|
553
|
-
it('rolls a failed migration back on the copy first, and on the database only when the copy took the rest', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
554
|
-
const made = [];
|
|
555
|
-
const newest = everyMigration[everyMigration.length - 1];
|
|
556
|
-
const history = { applied: everyMigration.slice(0, -1), failed: [newest] };
|
|
557
|
-
const { calls, urls, error } = yield run(history, { diff: fail(2, '') }, { copyDatabase: copier(made), freeBytes: roomy });
|
|
558
|
-
(0, chai_1.expect)(error).to.equal(undefined);
|
|
559
|
-
const steps = calls
|
|
560
|
-
.map((args, i) => ({ word: words([args])[0], url: urls[i] }))
|
|
561
|
-
.filter((c) => !c.word.startsWith('migrate diff'));
|
|
562
|
-
(0, chai_1.expect)(steps).to.deep.equal([
|
|
563
|
-
{ word: `migrate resolve --rolled-back ${newest}`, url: `file:${made[0]}` },
|
|
564
|
-
{ word: 'migrate deploy', url: `file:${made[0]}` },
|
|
565
|
-
{ word: `migrate resolve --rolled-back ${newest}`, url: scratchUrl },
|
|
566
|
-
{ word: 'migrate deploy', url: scratchUrl },
|
|
567
|
-
]);
|
|
568
|
-
}));
|
|
569
|
-
it('falls back to db push without the flag when the copy fails, and never deploys to the database', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
570
|
-
var _a;
|
|
571
|
-
const made = [];
|
|
572
|
-
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
573
|
-
const { calls, urls, error } = yield run(history, {
|
|
574
|
-
diff: fail(2, ''),
|
|
575
|
-
update: (args, url) => {
|
|
576
|
-
if (args.includes('deploy')) {
|
|
577
|
-
return fail(1, 'Error: P3018\n\nA migration failed to apply.\n\nMigration name: ' +
|
|
578
|
-
`${everyMigration[everyMigration.length - 1]}\n\nDatabase error:\nduplicate column name: path`)();
|
|
579
|
-
}
|
|
580
|
-
(0, chai_1.expect)(url).to.equal(scratchUrl);
|
|
581
|
-
return refusesDataLoss();
|
|
582
|
-
},
|
|
583
|
-
}, { copyDatabase: copier(made), freeBytes: roomy });
|
|
584
|
-
const deploys = urls.filter((_, i) => calls[i].includes('deploy'));
|
|
585
|
-
(0, chai_1.expect)(deploys).to.deep.equal([`file:${made[0]}`]);
|
|
586
|
-
(0, chai_1.expect)(update(calls)).to.include.members(['db', 'push']);
|
|
587
|
-
(0, chai_1.expect)(update(calls)).to.not.include('--accept-data-loss');
|
|
588
|
-
(0, chai_1.expect)(trialFiles()).to.deep.equal([]);
|
|
589
|
-
const message = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '';
|
|
590
|
-
(0, chai_1.expect)(message).to.match(/stopped rather than delete/);
|
|
591
|
-
(0, chai_1.expect)(message).to.match(/failed on a copy/);
|
|
592
|
-
(0, chai_1.expect)(message, "the copy's own reason").to.include('duplicate column name: path');
|
|
593
|
-
(0, chai_1.expect)(message).to.include('LabNotes');
|
|
594
|
-
const commands = commandsIn(message);
|
|
595
|
-
(0, chai_1.expect)(commands[0]).to.include(`VACUUM INTO '${scratchFile}.backup-`);
|
|
596
|
-
(0, chai_1.expect)(commands[0], 'let go only once backed up').to.match(letGoAfterBackup);
|
|
597
|
-
(0, chai_1.expect)(commands.filter((c) => c.includes('migrate deploy'))).to.deep.equal([]);
|
|
598
|
-
}));
|
|
599
|
-
it("skips the copy when there isn't room for it next to the database", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
600
|
-
var _a;
|
|
601
|
-
const made = [];
|
|
602
|
-
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
603
|
-
fs_1.default.writeFileSync(scratchFile, Buffer.alloc(1024 * 1024));
|
|
604
|
-
const { calls, error } = yield run(history, { diff: fail(2, ''), update: refusesDataLoss }, { copyDatabase: copier(made), freeBytes: () => 1024 * 1024 });
|
|
605
|
-
(0, chai_1.expect)(made, 'no copy').to.deep.equal([]);
|
|
606
|
-
(0, chai_1.expect)(calls.some((args) => args.includes('deploy'))).to.equal(false);
|
|
607
|
-
(0, chai_1.expect)(update(calls)).to.include.members(['db', 'push']);
|
|
608
|
-
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.match(/room/);
|
|
609
|
-
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.match(/MB/);
|
|
610
|
-
(0, chai_1.expect)(commandsIn((_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '')[0]).to.match(letGoAfterBackup);
|
|
611
|
-
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.include(`free ${Math.ceil((2 * 1024 * 1024 + 64 * 1024 * 1024) / (1024 * 1024))} MB`);
|
|
612
|
-
}));
|
|
613
|
-
it('skips the copy when the database is locked', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
614
|
-
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
615
|
-
const { calls, error } = yield run(history, { diff: fail(2, ''), update: refusesDataLoss }, {
|
|
616
|
-
copyDatabase: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
617
|
-
throw new Error('Raw query failed. Code: `5`. Message: `database is locked`');
|
|
618
|
-
}),
|
|
619
|
-
freeBytes: roomy,
|
|
620
|
-
});
|
|
621
|
-
(0, chai_1.expect)(calls.some((args) => args.includes('deploy'))).to.equal(false);
|
|
622
|
-
(0, chai_1.expect)(update(calls)).to.include.members(['db', 'push']);
|
|
623
|
-
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.match(/locked/);
|
|
624
|
-
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.match(/another server/);
|
|
625
|
-
(0, chai_1.expect)(trialFiles()).to.deep.equal([]);
|
|
626
|
-
}));
|
|
627
|
-
// migrate deploy builds on what the recorded migrations made. Through #493
|
|
628
|
-
// a copy it worked on stood for the file, even where the tables lacked
|
|
629
|
-
// part of that (an index dropped by hand, a column an older release's db
|
|
630
|
-
// push took away), and the file then got a full history over the gap.
|
|
631
|
-
it("doesn't try the copy when the tables lack part of what their recorded migrations make", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
632
|
-
var _a;
|
|
633
|
-
const made = [];
|
|
634
|
-
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
635
|
-
const lacking = '[*] Changed the `SessionLog` table\n [-] Removed index on columns (session_id, createdAt)';
|
|
636
|
-
const { calls, error } = yield run(history, {
|
|
637
|
-
diff: fail(2, ''),
|
|
638
|
-
lack: () => Buffer.from(`\n[+] Added tables\n - LabNotes\n\n${lacking}\n`),
|
|
639
|
-
update: refusesDataLoss,
|
|
640
|
-
}, { copyDatabase: copier(made), freeBytes: roomy });
|
|
641
|
-
(0, chai_1.expect)(made, 'no copy').to.deep.equal([]);
|
|
642
|
-
(0, chai_1.expect)(calls.some((args) => args.includes('deploy'))).to.equal(false);
|
|
643
|
-
(0, chai_1.expect)(update(calls)).to.include.members(['db', 'push']);
|
|
644
|
-
(0, chai_1.expect)(update(calls)).to.not.include('--accept-data-loss');
|
|
645
|
-
const message = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '';
|
|
646
|
-
(0, chai_1.expect)(message).to.match(/differ from what its recorded migrations make: they lack part of what those make/);
|
|
647
|
-
(0, chai_1.expect)(message).to.include(lacking);
|
|
648
|
-
(0, chai_1.expect)(message).to.not.include('LabNotes\n - ');
|
|
649
|
-
}));
|
|
650
|
-
it('says both reasons when a database with a failed migration also lacks part of its migrations', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
651
|
-
var _a;
|
|
652
|
-
const made = [];
|
|
653
|
-
const newest = everyMigration[everyMigration.length - 1];
|
|
654
|
-
const history = { applied: everyMigration.slice(0, -1), failed: [newest] };
|
|
655
|
-
const { error } = yield run(history, {
|
|
656
|
-
diff: fail(2, ''),
|
|
657
|
-
lack: () => Buffer.from('[*] Redefined table `LocatorEtalon`\n'),
|
|
658
|
-
update: refusesDataLoss,
|
|
659
|
-
}, { copyDatabase: copier(made), freeBytes: roomy });
|
|
660
|
-
(0, chai_1.expect)(made, 'no copy').to.deep.equal([]);
|
|
661
|
-
const message = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '';
|
|
662
|
-
(0, chai_1.expect)(message).to.match(new RegExp(`records a failed migration \\(${newest}\\), which prisma migrate deploy won't run ` +
|
|
663
|
-
'past, and its tables lack part of what its recorded migrations make'));
|
|
664
|
-
}));
|
|
665
|
-
it("doesn't run the migrations on the database when the copy still lacks part of the schema", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
666
|
-
var _a;
|
|
667
|
-
const made = [];
|
|
668
|
-
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
669
|
-
const { calls, urls, error } = yield run(history, {
|
|
670
|
-
diff: fail(2, ''),
|
|
671
|
-
check: (args) => {
|
|
672
|
-
(0, chai_1.expect)(args).to.include('--from-schema-datamodel');
|
|
673
|
-
return Buffer.from('\n[*] Redefined table `LocatorEtalon`\n');
|
|
674
|
-
},
|
|
675
|
-
update: (args) => (args.includes('deploy') ? Buffer.from('ok') : refusesDataLoss()),
|
|
676
|
-
}, { copyDatabase: copier(made), freeBytes: roomy });
|
|
677
|
-
const deploys = urls.filter((_, i) => calls[i].includes('deploy'));
|
|
678
|
-
(0, chai_1.expect)(deploys, 'the copy only').to.deep.equal([`file:${made[0]}`]);
|
|
679
|
-
(0, chai_1.expect)(update(calls)).to.include.members(['db', 'push']);
|
|
680
|
-
(0, chai_1.expect)(update(calls)).to.not.include('--accept-data-loss');
|
|
681
|
-
const message = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '';
|
|
682
|
-
(0, chai_1.expect)(message).to.match(/a copy of it still lacked part of this version's schema/);
|
|
683
|
-
(0, chai_1.expect)(message).to.include('[*] Redefined table `LocatorEtalon`');
|
|
684
|
-
(0, chai_1.expect)(trialFiles()).to.deep.equal([]);
|
|
685
|
-
}));
|
|
686
|
-
it("tries the copy when it can't tell how much space is free", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
687
|
-
const made = [];
|
|
688
|
-
const info = sinon_1.default.spy(logger_1.default, 'info');
|
|
689
|
-
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
690
|
-
const { error } = yield run(history, { diff: fail(2, '') }, { copyDatabase: copier(made), freeBytes: () => null });
|
|
691
|
-
(0, chai_1.expect)(error).to.equal(undefined);
|
|
692
|
-
(0, chai_1.expect)(made).to.have.length(1);
|
|
693
|
-
const lines = info.getCalls().map((c) => c.args.join(' '));
|
|
694
|
-
(0, chai_1.expect)(lines.some((l) => /could not tell how much space is free/.test(l))).to.equal(true);
|
|
695
|
-
}));
|
|
696
|
-
it('says what is copied before it copies', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
697
|
-
const info = sinon_1.default.spy(logger_1.default, 'info');
|
|
698
|
-
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
699
|
-
fs_1.default.writeFileSync(scratchFile, Buffer.alloc(3 * 1024 * 1024));
|
|
700
|
-
let before = -1;
|
|
701
|
-
yield run(history, { diff: fail(2, '') }, {
|
|
702
|
-
copyDatabase: (_from, to) => __awaiter(void 0, void 0, void 0, function* () {
|
|
703
|
-
before = info.callCount;
|
|
704
|
-
fs_1.default.writeFileSync(to, 'copy');
|
|
705
|
-
}),
|
|
706
|
-
freeBytes: roomy,
|
|
707
|
-
});
|
|
708
|
-
const lines = info
|
|
709
|
-
.getCalls()
|
|
710
|
-
.slice(0, before)
|
|
711
|
-
.map((c) => c.args.join(' '));
|
|
712
|
-
(0, chai_1.expect)(lines.some((l) => /3 MB to copy, which can take a while/.test(l))).to.equal(true);
|
|
713
|
-
}));
|
|
714
|
-
it("gives the copy the database's own mode before any data is in it", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
715
|
-
fs_1.default.chmodSync(scratchFile, 0o640);
|
|
716
|
-
let mode = -1;
|
|
717
|
-
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
718
|
-
yield run(history, { diff: fail(2, '') }, {
|
|
719
|
-
copyDatabase: (_from, to) => __awaiter(void 0, void 0, void 0, function* () {
|
|
720
|
-
mode = fs_1.default.statSync(to).mode & 0o777;
|
|
721
|
-
fs_1.default.writeFileSync(to, 'copy');
|
|
722
|
-
}),
|
|
723
|
-
freeBytes: roomy,
|
|
724
|
-
});
|
|
725
|
-
(0, chai_1.expect)(mode.toString(8)).to.equal('640');
|
|
726
|
-
}));
|
|
727
|
-
it('says there was no room when the copy runs out of space', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
728
|
-
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
729
|
-
const { error } = yield run(history, { diff: fail(2, ''), update: refusesDataLoss }, {
|
|
730
|
-
copyDatabase: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
731
|
-
throw new Error('Raw query failed. Code: `13`. Message: `database or disk is full`');
|
|
732
|
-
}),
|
|
733
|
-
freeBytes: () => null,
|
|
734
|
-
});
|
|
735
|
-
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.match(/no room next to it for a copy/);
|
|
736
|
-
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.match(/free \d+ MB next to the file/);
|
|
737
|
-
(0, chai_1.expect)(trialFiles()).to.deep.equal([]);
|
|
738
|
-
}));
|
|
739
|
-
// A history that names a migration this release lacks is another
|
|
740
|
-
// release's. Through #493 the copy ran this release's migrations on it,
|
|
741
|
-
// and then the file did.
|
|
742
|
-
it("doesn't try this release's migrations on a database from another release", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
743
|
-
const made = [];
|
|
744
|
-
const history = {
|
|
745
|
-
applied: [...everyMigration.slice(0, -1), '29980101000000_elsewhere'],
|
|
746
|
-
failed: [],
|
|
747
|
-
};
|
|
748
|
-
const { calls, error } = yield run(history, { diff: fail(2, ''), update: refusesDataLoss }, { copyDatabase: copier(made), freeBytes: roomy });
|
|
749
|
-
(0, chai_1.expect)(made, 'no copy').to.deep.equal([]);
|
|
750
|
-
(0, chai_1.expect)(calls.some((args) => args.includes('deploy') || args.includes('resolve'))).to.equal(false);
|
|
751
|
-
(0, chai_1.expect)(update(calls)).to.include.members(['db', 'push']);
|
|
752
|
-
(0, chai_1.expect)(update(calls)).to.not.include('--accept-data-loss');
|
|
753
|
-
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.match(/another release brought it up to date/);
|
|
754
|
-
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.include('29980101000000_elsewhere');
|
|
755
|
-
}));
|
|
756
|
-
// A start that died mid-trial leaves its copy. The next start removes it,
|
|
757
|
-
// unless the process that made it is still running.
|
|
758
|
-
it('removes a copy a previous start left, but not one a running start is using', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
759
|
-
const dir = path_1.default.dirname(scratchFile);
|
|
760
|
-
const dead = path_1.default.join(dir, `${trialPrefix}999991`);
|
|
761
|
-
const alive = path_1.default.join(dir, `${trialPrefix}999992`);
|
|
762
|
-
const other = `${scratchFile}.backup-20261005T000000Z`;
|
|
763
|
-
for (const f of [dead, `${dead}-journal`, alive, other])
|
|
764
|
-
fs_1.default.writeFileSync(f, 'x');
|
|
765
|
-
// Signal 0 only asks whether a process is there; stubbed all the same.
|
|
766
|
-
sinon_1.default.stub(process, 'kill').callsFake(((pid) => {
|
|
767
|
-
if (pid === 999992)
|
|
768
|
-
return true;
|
|
769
|
-
throw Object.assign(new Error('kill ESRCH'), { code: 'ESRCH' });
|
|
770
|
-
}));
|
|
771
|
-
try {
|
|
772
|
-
yield run({ applied: [...everyMigration], failed: [] });
|
|
773
|
-
(0, chai_1.expect)(trialFiles().sort()).to.deep.equal([path_1.default.basename(alive)]);
|
|
774
|
-
(0, chai_1.expect)(fs_1.default.existsSync(other)).to.equal(true);
|
|
775
|
-
}
|
|
776
|
-
finally {
|
|
777
|
-
fs_1.default.rmSync(other, { force: true });
|
|
778
|
-
}
|
|
779
|
-
}));
|
|
780
|
-
});
|
|
781
246
|
// With a failed migration recorded, `migrate deploy` stops at once (P3009),
|
|
782
247
|
// whatever the tables hold, so the message never suggests it.
|
|
783
248
|
it('names the failed migration and never suggests migrate deploy when the history records one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
784
249
|
var _a;
|
|
785
250
|
const failed = everyMigration[everyMigration.length - 1];
|
|
786
251
|
const history = { applied: everyMigration.slice(0, -1), failed: [failed] };
|
|
787
|
-
const { error } = yield run(history, {
|
|
252
|
+
const { error } = yield run(history, { update: refusesDataLoss });
|
|
788
253
|
(0, chai_1.expect)(error, 'the start stops').to.be.an('Error');
|
|
789
254
|
const message = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '';
|
|
790
255
|
(0, chai_1.expect)(message).to.match(/stopped rather than delete/);
|
|
791
256
|
(0, chai_1.expect)(message).to.include(failed);
|
|
792
257
|
(0, chai_1.expect)(message).to.match(/records a failed migration/);
|
|
258
|
+
(0, chai_1.expect)(message).to.not.match(/differ from what its recorded migrations make/);
|
|
793
259
|
(0, chai_1.expect)(message).to.include('LabNotes');
|
|
794
260
|
const commands = commandsIn(message);
|
|
795
261
|
(0, chai_1.expect)(commands[0]).to.include(`VACUUM INTO '${scratchFile}.backup-`);
|
|
796
|
-
(0, chai_1.expect)(commands
|
|
262
|
+
(0, chai_1.expect)(commands).to.include(letGo);
|
|
797
263
|
(0, chai_1.expect)(commands.filter((c) => c.includes('migrate deploy'))).to.deep.equal([]);
|
|
798
264
|
(0, chai_1.expect)(message).to.not.match(/by hand with prisma migrate deploy/);
|
|
799
265
|
}));
|
|
800
266
|
// A migration can fail on what is already in the tables, which a later
|
|
801
267
|
// start deals with, or on the rows (a unique index over duplicate values),
|
|
802
|
-
// which no start gets past until someone changes them.
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const newest = everyMigration[everyMigration.length - 1];
|
|
806
|
-
const failure = (name, error) => fail(1, `Error: P3018\n\nA migration failed to apply.\n\nMigration name: ${name}\n\n` +
|
|
807
|
-
`Database error:\n${error}`);
|
|
268
|
+
// which no start gets past until someone changes them.
|
|
269
|
+
it('says what to check when migrate deploy fails on a migration (P3018), without promising the next start recovers', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
270
|
+
var _a;
|
|
808
271
|
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
diff: fail(1, 'simulated diff failure'),
|
|
826
|
-
update: failure(newest, 'duplicate column name: path'),
|
|
827
|
-
});
|
|
828
|
-
const message = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '';
|
|
829
|
-
(0, chai_1.expect)(message).to.match(/because what it makes was already there/);
|
|
830
|
-
(0, chai_1.expect)(message).to.match(/records it as applied if the tables are what/);
|
|
831
|
-
(0, chai_1.expect)(message).to.not.match(/change those rows/);
|
|
832
|
-
}));
|
|
833
|
-
it("promises nothing for a migration this release doesn't have", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
834
|
-
var _a;
|
|
835
|
-
const { error } = yield run(history, {
|
|
836
|
-
diff: fail(1, 'simulated diff failure'),
|
|
837
|
-
update: failure('20261101000000_uniq', 'UNIQUE constraint failed'),
|
|
838
|
-
});
|
|
839
|
-
const message = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '';
|
|
840
|
-
(0, chai_1.expect)(message).to.include('20261101000000_uniq');
|
|
841
|
-
(0, chai_1.expect)(message).to.not.match(/next start/);
|
|
842
|
-
}));
|
|
843
|
-
});
|
|
272
|
+
const { error } = yield run(history, {
|
|
273
|
+
diff: fail(1, 'simulated diff failure'),
|
|
274
|
+
update: fail(1, 'Error: P3018\n\nA migration failed to apply.\n\nMigration name: 20261101000000_uniq\n\n' +
|
|
275
|
+
'Database error:\nUNIQUE constraint failed: LocatorEtalon.strategy, LocatorEtalon.nodeName'),
|
|
276
|
+
});
|
|
277
|
+
(0, chai_1.expect)(error, 'the start stops').to.be.an('Error');
|
|
278
|
+
const message = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '';
|
|
279
|
+
(0, chai_1.expect)(message).to.include('20261101000000_uniq');
|
|
280
|
+
(0, chai_1.expect)(message).to.include('UNIQUE constraint failed');
|
|
281
|
+
(0, chai_1.expect)(message).to.not.match(/next start brings the database up to date/);
|
|
282
|
+
(0, chai_1.expect)(message).to.not.match(/start again, or first/);
|
|
283
|
+
// Both things to check: a change already there, and rows the migration can't take.
|
|
284
|
+
(0, chai_1.expect)(message).to.match(/already there/);
|
|
285
|
+
(0, chai_1.expect)(message).to.match(/duplicate values/);
|
|
286
|
+
(0, chai_1.expect)(message).to.match(/change those rows/);
|
|
287
|
+
}));
|
|
844
288
|
it("says it couldn't read the history without blaming the file outright", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
845
289
|
let error;
|
|
846
290
|
try {
|
|
@@ -856,47 +300,3 @@ describe('runMigrations: what each plan runs', () => {
|
|
|
856
300
|
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.include('database is locked');
|
|
857
301
|
}));
|
|
858
302
|
});
|
|
859
|
-
// `npm run db:generate` runs `prisma migrate dev` on the developer's database,
|
|
860
|
-
// which gives it a migration history. From then on `npm run db:migrate` updates
|
|
861
|
-
// it with `migrate deploy`, so a schema.prisma edit with no migration never
|
|
862
|
-
// reaches it (or any server the release reaches), while the log said "in sync".
|
|
863
|
-
describe('npm run db:migrate: schema changes without a migration', () => {
|
|
864
|
-
afterEach(() => sinon_1.default.restore());
|
|
865
|
-
const failing = (status) => () => {
|
|
866
|
-
throw Object.assign(new Error('Command failed'), {
|
|
867
|
-
status,
|
|
868
|
-
stderr: Buffer.from('boom'),
|
|
869
|
-
stdout: Buffer.from(''),
|
|
870
|
-
});
|
|
871
|
-
};
|
|
872
|
-
it('warns to run npm run db:generate when prisma/schema.prisma has changes no migration makes', () => {
|
|
873
|
-
const warn = sinon_1.default.stub(logger_1.default, 'warn');
|
|
874
|
-
const calls = [];
|
|
875
|
-
const found = (0, run_migrations_1.warnOfUnmigratedSchemaChanges)((_cmd, args) => {
|
|
876
|
-
calls.push(args);
|
|
877
|
-
return failing(2)();
|
|
878
|
-
});
|
|
879
|
-
(0, chai_1.expect)(found).to.equal(true);
|
|
880
|
-
(0, chai_1.expect)(calls).to.have.length(1);
|
|
881
|
-
(0, chai_1.expect)(calls[0]).to.include.members([
|
|
882
|
-
'migrate',
|
|
883
|
-
'diff',
|
|
884
|
-
'--from-migrations',
|
|
885
|
-
'--to-schema-datamodel',
|
|
886
|
-
'--exit-code',
|
|
887
|
-
]);
|
|
888
|
-
const lines = warn.getCalls().map((c) => c.args.join(' '));
|
|
889
|
-
(0, chai_1.expect)(lines).to.have.length(1);
|
|
890
|
-
(0, chai_1.expect)(lines[0]).to.match(/npm run db:generate/);
|
|
891
|
-
(0, chai_1.expect)(lines[0]).to.match(/migrate deploy/);
|
|
892
|
-
});
|
|
893
|
-
it('says nothing when the migrations make the schema', () => {
|
|
894
|
-
const warn = sinon_1.default.stub(logger_1.default, 'warn');
|
|
895
|
-
(0, chai_1.expect)((0, run_migrations_1.warnOfUnmigratedSchemaChanges)(() => Buffer.from(''))).to.equal(false);
|
|
896
|
-
(0, chai_1.expect)(warn.called).to.equal(false);
|
|
897
|
-
});
|
|
898
|
-
it("doesn't stop when it can't tell", () => {
|
|
899
|
-
sinon_1.default.stub(logger_1.default, 'warn');
|
|
900
|
-
(0, chai_1.expect)((0, run_migrations_1.warnOfUnmigratedSchemaChanges)(failing(1))).to.equal(null);
|
|
901
|
-
});
|
|
902
|
-
});
|