@xenon-device-management/xenon 2.15.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 +38 -26
- package/lib/package.json +2 -1
- package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/index-Dk5J6HB1.js +219 -0
- package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +11 -7
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/app/openapi/sessions.yaml +7 -5
- package/lib/src/app/routers/auth.js +2 -1
- package/lib/src/app/routers/users.js +2 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/event-manager.js +23 -154
- package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-utils.js +25 -8
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +13 -15
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/plugin.js +12 -4
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +29 -33
- package/lib/src/scripts/run-migrations.js +320 -25
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +6 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +6 -2
- package/lib/src/services/SessionLifecycleService.js +52 -23
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +361 -42
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/healing/HealingOrchestrator.js +3 -1
- package/lib/src/services/identity/identityChanges.js +41 -0
- 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 +279 -37
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +23 -4
- package/lib/src/sessions/SessionManager.js +33 -9
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/event-log-session-data.spec.js +11 -4
- package/lib/test/unit/failure-analysis.spec.js +22 -8
- package/lib/test/unit/failure-categories.spec.js +24 -22
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/healing-tier-gate.spec.js +3 -2
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
- 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/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -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/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/run-migrations-database.spec.js +440 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +264 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
- package/lib/test/unit/selector-verification-job.spec.js +15 -6
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +43 -96
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +16 -1
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/schema.json +11 -7
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/index-BOxe43p1.js +0 -219
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const chai_1 = require("chai");
|
|
16
|
+
const child_process_1 = require("child_process");
|
|
17
|
+
const fs_1 = __importDefault(require("fs"));
|
|
18
|
+
const os_1 = __importDefault(require("os"));
|
|
19
|
+
const path_1 = __importDefault(require("path"));
|
|
20
|
+
const client_1 = require("../../src/generated/client");
|
|
21
|
+
const config_1 = require("../../src/config");
|
|
22
|
+
const run_migrations_1 = require("../../src/scripts/run-migrations");
|
|
23
|
+
// The startup schema step, run for real: the prisma CLI from node_modules
|
|
24
|
+
// against scratch SQLite files, never the server's own database.
|
|
25
|
+
//
|
|
26
|
+
// Through 2.15.0 the step chose its command from `databaseProvider` alone:
|
|
27
|
+
// `migrate deploy` for postgresql, `db push` for anything else. A database
|
|
28
|
+
// Xenon made with the default setting (or `npm run db:migrate`) has tables and
|
|
29
|
+
// no migration history, and `migrate deploy` refuses one of those (P3005), so a
|
|
30
|
+
// Xenon Control profile set to postgresql stopped the server. The default
|
|
31
|
+
// setting, in turn, ran `db push` on a database that keeps a migration history,
|
|
32
|
+
// moving its tables past that history, after which `migrate deploy` failed on
|
|
33
|
+
// the migration it had already applied (P3018) and recorded it as failed.
|
|
34
|
+
// The database now decides.
|
|
35
|
+
const ROOT = path_1.default.resolve(__dirname, '../..');
|
|
36
|
+
const PRISMA = path_1.default.join(ROOT, 'node_modules', '.bin', 'prisma');
|
|
37
|
+
const SCHEMA = path_1.default.join(ROOT, 'prisma', 'schema.prisma');
|
|
38
|
+
const MIGRATIONS = path_1.default.join(ROOT, 'prisma', 'migrations');
|
|
39
|
+
const LOCAL_MIGRATIONS = fs_1.default
|
|
40
|
+
.readdirSync(MIGRATIONS)
|
|
41
|
+
.filter((name) => fs_1.default.existsSync(path_1.default.join(MIGRATIONS, name, 'migration.sql')))
|
|
42
|
+
.sort();
|
|
43
|
+
function prismaCli(args, file, input) {
|
|
44
|
+
return (0, child_process_1.execFileSync)(PRISMA, args, {
|
|
45
|
+
cwd: ROOT,
|
|
46
|
+
env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: `file:${file}`, CHECKPOINT_DISABLE: '1' }),
|
|
47
|
+
input,
|
|
48
|
+
stdio: 'pipe',
|
|
49
|
+
}).toString();
|
|
50
|
+
}
|
|
51
|
+
function sql(file, statements) {
|
|
52
|
+
prismaCli(['db', 'execute', '--url', `file:${file}`, '--stdin'], file, statements);
|
|
53
|
+
}
|
|
54
|
+
/** Whether the database's tables are exactly what prisma/schema.prisma says. */
|
|
55
|
+
function matchesSchema(file) {
|
|
56
|
+
try {
|
|
57
|
+
prismaCli([
|
|
58
|
+
'migrate',
|
|
59
|
+
'diff',
|
|
60
|
+
'--from-url',
|
|
61
|
+
`file:${file}`,
|
|
62
|
+
'--to-schema-datamodel',
|
|
63
|
+
SCHEMA,
|
|
64
|
+
'--exit-code',
|
|
65
|
+
], file);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
if ((err === null || err === void 0 ? void 0 : err.status) === 2)
|
|
70
|
+
return false;
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function query(file, statement) {
|
|
75
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
76
|
+
const client = new client_1.PrismaClient({ datasources: { db: { url: `file:${file}` } } });
|
|
77
|
+
try {
|
|
78
|
+
return (yield client.$queryRawUnsafe(statement));
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
yield client.$disconnect();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** `_prisma_migrations`, or null when the database has none. */
|
|
86
|
+
function historyOf(file) {
|
|
87
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
const [{ n }] = yield query(file, "SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = '_prisma_migrations'");
|
|
89
|
+
if (Number(n) === 0)
|
|
90
|
+
return null;
|
|
91
|
+
return query(file, 'SELECT migration_name, finished_at, rolled_back_at FROM "_prisma_migrations"');
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** `_prisma_migrations`, which the database must have. */
|
|
95
|
+
function recordedHistory(file) {
|
|
96
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
const rows = yield historyOf(file);
|
|
98
|
+
if (!rows)
|
|
99
|
+
throw new Error(`${file} has no migration history`);
|
|
100
|
+
return rows;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const SEED = `
|
|
104
|
+
INSERT INTO "Team" ("id", "name", "createdAt") VALUES ('team-1', 'Payments', 1759600000000);
|
|
105
|
+
INSERT INTO "Build" ("id", "name", "createdAt", "updatedAt") VALUES ('build-1', 'nightly #42', 1759600000000, 1759600000000);
|
|
106
|
+
`;
|
|
107
|
+
function seededRows(file) {
|
|
108
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
109
|
+
const teams = yield query(file, 'SELECT id, name FROM "Team"');
|
|
110
|
+
const builds = yield query(file, 'SELECT id, name FROM "Build"');
|
|
111
|
+
return [...teams, ...builds].map((r) => `${r.id}=${r.name}`).sort();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const SEEDED = ['build-1=nightly #42', 'team-1=Payments'];
|
|
115
|
+
describe('runMigrations against real databases', function () {
|
|
116
|
+
// Each start runs the prisma CLI two or three times.
|
|
117
|
+
this.timeout(120000);
|
|
118
|
+
let dir;
|
|
119
|
+
let saved;
|
|
120
|
+
/** The release before this one, with its migration history: what `migrate deploy` made. */
|
|
121
|
+
let withHistory;
|
|
122
|
+
/** The release before this one with no migration history: what `db push` made. */
|
|
123
|
+
let withoutHistory;
|
|
124
|
+
/** A history behind its tables: the default setting's `db push` ran on a `migrate deploy` database. */
|
|
125
|
+
let historyBehind;
|
|
126
|
+
/** A history that records a failed migration: `migrate deploy` ran on `historyBehind`. */
|
|
127
|
+
let historyFailed;
|
|
128
|
+
let copies = 0;
|
|
129
|
+
function copyOf(fixture) {
|
|
130
|
+
const file = path_1.default.join(dir, `start-${++copies}.db`);
|
|
131
|
+
if (fixture)
|
|
132
|
+
fs_1.default.copyFileSync(fixture, file);
|
|
133
|
+
return file;
|
|
134
|
+
}
|
|
135
|
+
function start(file, provider) {
|
|
136
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
137
|
+
config_1.config.databaseUrl = `file:${file}`;
|
|
138
|
+
config_1.config.databaseProvider = provider;
|
|
139
|
+
yield (0, run_migrations_1.runMigrations)();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
before(() => {
|
|
143
|
+
dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-run-migrations-'));
|
|
144
|
+
// The previous release: every migration but the newest, so a start has one to apply.
|
|
145
|
+
const previous = path_1.default.join(dir, 'previous');
|
|
146
|
+
fs_1.default.mkdirSync(path_1.default.join(previous, 'migrations'), { recursive: true });
|
|
147
|
+
fs_1.default.copyFileSync(SCHEMA, path_1.default.join(previous, 'schema.prisma'));
|
|
148
|
+
fs_1.default.copyFileSync(path_1.default.join(MIGRATIONS, 'migration_lock.toml'), path_1.default.join(previous, 'migrations', 'migration_lock.toml'));
|
|
149
|
+
for (const name of LOCAL_MIGRATIONS.slice(0, -1)) {
|
|
150
|
+
fs_1.default.mkdirSync(path_1.default.join(previous, 'migrations', name));
|
|
151
|
+
fs_1.default.copyFileSync(path_1.default.join(MIGRATIONS, name, 'migration.sql'), path_1.default.join(previous, 'migrations', name, 'migration.sql'));
|
|
152
|
+
}
|
|
153
|
+
withHistory = path_1.default.join(dir, 'with-history.db');
|
|
154
|
+
prismaCli(['migrate', 'deploy', '--schema', path_1.default.join(previous, 'schema.prisma')], withHistory);
|
|
155
|
+
withoutHistory = path_1.default.join(dir, 'without-history.db');
|
|
156
|
+
fs_1.default.copyFileSync(withHistory, withoutHistory);
|
|
157
|
+
sql(withoutHistory, 'DROP TABLE "_prisma_migrations";');
|
|
158
|
+
sql(withHistory, SEED);
|
|
159
|
+
sql(withoutHistory, SEED);
|
|
160
|
+
historyBehind = path_1.default.join(dir, 'history-behind.db');
|
|
161
|
+
fs_1.default.copyFileSync(withHistory, historyBehind);
|
|
162
|
+
prismaCli(['db', 'push', '--skip-generate', '--schema', SCHEMA], historyBehind);
|
|
163
|
+
historyFailed = path_1.default.join(dir, 'history-failed.db');
|
|
164
|
+
fs_1.default.copyFileSync(historyBehind, historyFailed);
|
|
165
|
+
try {
|
|
166
|
+
prismaCli(['migrate', 'deploy', '--schema', SCHEMA], historyFailed);
|
|
167
|
+
}
|
|
168
|
+
catch (_a) {
|
|
169
|
+
// P3018: the newest migration is already applied. That is the point.
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
after(() => {
|
|
173
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
174
|
+
});
|
|
175
|
+
beforeEach(() => {
|
|
176
|
+
saved = {
|
|
177
|
+
autoMigrate: config_1.config.autoMigrate,
|
|
178
|
+
databaseProvider: config_1.config.databaseProvider,
|
|
179
|
+
databaseUrl: config_1.config.databaseUrl,
|
|
180
|
+
};
|
|
181
|
+
config_1.config.autoMigrate = true;
|
|
182
|
+
});
|
|
183
|
+
afterEach(() => {
|
|
184
|
+
Object.assign(config_1.config, saved);
|
|
185
|
+
});
|
|
186
|
+
it('builds the fixtures it means to', () => __awaiter(this, void 0, void 0, function* () {
|
|
187
|
+
(0, chai_1.expect)(LOCAL_MIGRATIONS.length).to.be.greaterThan(1);
|
|
188
|
+
(0, chai_1.expect)(yield historyOf(withHistory)).to.have.length(LOCAL_MIGRATIONS.length - 1);
|
|
189
|
+
(0, chai_1.expect)(yield historyOf(withoutHistory)).to.equal(null);
|
|
190
|
+
(0, chai_1.expect)(matchesSchema(withoutHistory), 'one migration left to apply').to.equal(false);
|
|
191
|
+
(0, chai_1.expect)(matchesSchema(historyBehind)).to.equal(true);
|
|
192
|
+
const failed = (yield recordedHistory(historyFailed)).filter((r) => !r.finished_at);
|
|
193
|
+
(0, chai_1.expect)(failed.map((r) => r.migration_name)).to.deep.equal([
|
|
194
|
+
LOCAL_MIGRATIONS[LOCAL_MIGRATIONS.length - 1],
|
|
195
|
+
]);
|
|
196
|
+
}));
|
|
197
|
+
// Through 2.15.0 the setting chose the command, so these two run under both.
|
|
198
|
+
for (const provider of ['sqlite', 'postgresql']) {
|
|
199
|
+
describe(`with databaseProvider ${provider}`, () => {
|
|
200
|
+
it('brings a database kept with db push up to date, keeps its rows, and keeps it on db push', () => __awaiter(this, void 0, void 0, function* () {
|
|
201
|
+
const file = copyOf(withoutHistory);
|
|
202
|
+
yield start(file, provider);
|
|
203
|
+
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
204
|
+
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
205
|
+
(0, chai_1.expect)(yield historyOf(file), 'no migration history was made up').to.equal(null);
|
|
206
|
+
yield start(file, provider);
|
|
207
|
+
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
208
|
+
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
209
|
+
}));
|
|
210
|
+
it('applies the missing migration to a database with migration history, and records it', () => __awaiter(this, void 0, void 0, function* () {
|
|
211
|
+
const file = copyOf(withHistory);
|
|
212
|
+
yield start(file, provider);
|
|
213
|
+
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
214
|
+
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
215
|
+
const history = yield recordedHistory(file);
|
|
216
|
+
(0, chai_1.expect)(history.map((r) => r.migration_name).sort()).to.deep.equal(LOCAL_MIGRATIONS);
|
|
217
|
+
(0, chai_1.expect)(history.every((r) => r.finished_at && !r.rolled_back_at)).to.equal(true);
|
|
218
|
+
yield start(file, provider);
|
|
219
|
+
(0, chai_1.expect)(yield recordedHistory(file)).to.have.length(LOCAL_MIGRATIONS.length);
|
|
220
|
+
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
221
|
+
}));
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// The rest ran into trouble under postgresql, the setting that chose migrate deploy.
|
|
225
|
+
it('creates a new database', () => __awaiter(this, void 0, void 0, function* () {
|
|
226
|
+
const file = copyOf(null);
|
|
227
|
+
yield start(file, 'postgresql');
|
|
228
|
+
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
229
|
+
yield start(file, 'postgresql');
|
|
230
|
+
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
231
|
+
}));
|
|
232
|
+
it('starts a database whose tables are ahead of its history, without recording a failed migration', () => __awaiter(this, void 0, void 0, function* () {
|
|
233
|
+
const file = copyOf(historyBehind);
|
|
234
|
+
yield start(file, 'postgresql');
|
|
235
|
+
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
236
|
+
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
237
|
+
const history = yield recordedHistory(file);
|
|
238
|
+
(0, chai_1.expect)(history.filter((r) => !r.finished_at && !r.rolled_back_at)).to.deep.equal([]);
|
|
239
|
+
}));
|
|
240
|
+
it('starts a database whose history records a failed migration', () => __awaiter(this, void 0, void 0, function* () {
|
|
241
|
+
const file = copyOf(historyFailed);
|
|
242
|
+
yield start(file, 'postgresql');
|
|
243
|
+
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
244
|
+
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
245
|
+
}));
|
|
246
|
+
// A history database whose tables differ from its migrations for another
|
|
247
|
+
// reason than db push: a table with a row and an index added by hand. `db
|
|
248
|
+
// push --accept-data-loss` would drop them, and the default setting did.
|
|
249
|
+
// Without the flag, db push refuses to drop the table, and the start stops
|
|
250
|
+
// with commands to paste. Each case below follows them as an operator would.
|
|
251
|
+
const HAND = 'CREATE TABLE "LabNotes" ("id" INTEGER PRIMARY KEY, "note" TEXT); ' +
|
|
252
|
+
'INSERT INTO "LabNotes" ("note") VALUES (\'keep me\'); ' +
|
|
253
|
+
'CREATE INDEX "lab_team_created" ON "Team"("createdAt");';
|
|
254
|
+
function startStops(file) {
|
|
255
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
256
|
+
try {
|
|
257
|
+
yield start(file, 'sqlite');
|
|
258
|
+
}
|
|
259
|
+
catch (e) {
|
|
260
|
+
return e.message;
|
|
261
|
+
}
|
|
262
|
+
throw new Error('the start was meant to stop');
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/** The commands a failure message gives, one to an indented line, in its order. */
|
|
266
|
+
function commandsIn(message) {
|
|
267
|
+
return message
|
|
268
|
+
.split('\n')
|
|
269
|
+
.filter((line) => /^ {2}(DATABASE_URL=|echo )/.test(line))
|
|
270
|
+
.map((line) => line.trim());
|
|
271
|
+
}
|
|
272
|
+
/** Runs a command from a message as an operator would paste it into a shell. */
|
|
273
|
+
function paste(command) {
|
|
274
|
+
return (0, child_process_1.execSync)(command, {
|
|
275
|
+
shell: '/bin/sh',
|
|
276
|
+
stdio: 'pipe',
|
|
277
|
+
env: Object.assign(Object.assign({}, process.env), { CHECKPOINT_DISABLE: '1' }),
|
|
278
|
+
}).toString();
|
|
279
|
+
}
|
|
280
|
+
/** The file a `VACUUM INTO '<file>'` command writes. */
|
|
281
|
+
function vacuumTarget(command) {
|
|
282
|
+
const match = /VACUUM INTO '([^']+)'/.exec(command);
|
|
283
|
+
if (!match)
|
|
284
|
+
throw new Error(`not a copy: ${command}`);
|
|
285
|
+
return match[1];
|
|
286
|
+
}
|
|
287
|
+
function handAdded(file) {
|
|
288
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
289
|
+
const [{ n }] = yield query(file, "SELECT count(*) AS n FROM sqlite_master WHERE name = 'LabNotes'");
|
|
290
|
+
const notes = Number(n) === 0
|
|
291
|
+
? []
|
|
292
|
+
: (yield query(file, 'SELECT note FROM "LabNotes"')).map((r) => r.note);
|
|
293
|
+
const index = yield query(file, "SELECT name FROM sqlite_master WHERE name = 'lab_team_created'");
|
|
294
|
+
return { notes, index: index.length === 1 };
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const KEPT = { notes: ['keep me'], index: true };
|
|
298
|
+
const GONE = { notes: [], index: false };
|
|
299
|
+
/** Whether the tables are this version's schema plus what was added by hand. */
|
|
300
|
+
function matchesSchemaWithHandAdded(file) {
|
|
301
|
+
const probe = `${file}.probe`;
|
|
302
|
+
fs_1.default.copyFileSync(file, probe);
|
|
303
|
+
try {
|
|
304
|
+
sql(probe, 'DROP TABLE "LabNotes"; DROP INDEX "lab_team_created";');
|
|
305
|
+
return matchesSchema(probe);
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
fs_1.default.rmSync(probe, { force: true });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function failedMigrations(file) {
|
|
312
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
313
|
+
const history = yield recordedHistory(file);
|
|
314
|
+
return history.filter((r) => !r.finished_at && !r.rolled_back_at).map((r) => r.migration_name);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
it('drops nothing added by hand to a history database, and its copy-first advice keeps it', () => __awaiter(this, void 0, void 0, function* () {
|
|
318
|
+
const file = copyOf(withHistory);
|
|
319
|
+
sql(file, HAND);
|
|
320
|
+
const message = yield startStops(file);
|
|
321
|
+
(0, chai_1.expect)(message).to.match(/stopped rather than delete/);
|
|
322
|
+
(0, chai_1.expect)(message).to.include('LabNotes');
|
|
323
|
+
(0, chai_1.expect)(yield handAdded(file)).to.deep.equal(KEPT);
|
|
324
|
+
(0, chai_1.expect)(yield recordedHistory(file)).to.have.length(LOCAL_MIGRATIONS.length - 1);
|
|
325
|
+
// Back up, copy, deploy on the copy, then on the file, as it says.
|
|
326
|
+
const commands = commandsIn(message);
|
|
327
|
+
const backup = commands.find((c) => c.includes('.backup-'));
|
|
328
|
+
const copy = commands.find((c) => c.includes('VACUUM INTO') && c !== backup);
|
|
329
|
+
const copyUrl = `file:${vacuumTarget(copy)}`;
|
|
330
|
+
const onCopy = commands.find((c) => c.startsWith(`DATABASE_URL=${copyUrl} `));
|
|
331
|
+
const onFile = commands.find((c) => c.startsWith(`DATABASE_URL=file:${file} `) && c.includes('migrate deploy'));
|
|
332
|
+
(0, chai_1.expect)([backup, copy, onCopy, onFile].every(Boolean), message).to.equal(true);
|
|
333
|
+
paste(backup);
|
|
334
|
+
paste(copy);
|
|
335
|
+
paste(onCopy);
|
|
336
|
+
paste(onFile);
|
|
337
|
+
fs_1.default.rmSync(vacuumTarget(copy));
|
|
338
|
+
yield start(file, 'sqlite');
|
|
339
|
+
(0, chai_1.expect)(yield handAdded(file)).to.deep.equal(KEPT);
|
|
340
|
+
(0, chai_1.expect)(yield handAdded(vacuumTarget(backup))).to.deep.equal(KEPT);
|
|
341
|
+
(0, chai_1.expect)(matchesSchemaWithHandAdded(file)).to.equal(true);
|
|
342
|
+
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
343
|
+
(0, chai_1.expect)(yield recordedHistory(file)).to.have.length(LOCAL_MIGRATIONS.length);
|
|
344
|
+
}));
|
|
345
|
+
// Tables ahead of the history and a table added by hand: migrate deploy
|
|
346
|
+
// fails on the change already there (P3018) and records it as failed, so
|
|
347
|
+
// the advice tries it on a copy, which fails there and leaves the file be.
|
|
348
|
+
it('never sends migrate deploy to a database whose tables are ahead of its history', () => __awaiter(this, void 0, void 0, function* () {
|
|
349
|
+
const file = copyOf(historyBehind);
|
|
350
|
+
sql(file, HAND);
|
|
351
|
+
const message = yield startStops(file);
|
|
352
|
+
(0, chai_1.expect)(message).to.match(/stopped rather than delete/);
|
|
353
|
+
(0, chai_1.expect)(message).to.include('LabNotes');
|
|
354
|
+
const commands = commandsIn(message);
|
|
355
|
+
const backup = commands.find((c) => c.includes('.backup-'));
|
|
356
|
+
const copy = commands.find((c) => c.includes('VACUUM INTO') && c !== backup);
|
|
357
|
+
const copyUrl = `file:${vacuumTarget(copy)}`;
|
|
358
|
+
const onCopy = commands.find((c) => c.startsWith(`DATABASE_URL=${copyUrl} `));
|
|
359
|
+
const letGo = commands.find((c) => c.includes('--accept-data-loss'));
|
|
360
|
+
(0, chai_1.expect)([backup, copy, onCopy, letGo].every(Boolean), message).to.equal(true);
|
|
361
|
+
(0, chai_1.expect)(letGo).to.equal(`DATABASE_URL=file:${file} ${PRISMA} db push --skip-generate --accept-data-loss --schema ${SCHEMA}`);
|
|
362
|
+
paste(backup);
|
|
363
|
+
paste(copy);
|
|
364
|
+
(0, chai_1.expect)(() => paste(onCopy)).to.throw(/P3018/);
|
|
365
|
+
fs_1.default.rmSync(vacuumTarget(copy));
|
|
366
|
+
(0, chai_1.expect)(yield failedMigrations(file), 'the file records no failed migration').to.deep.equal([]);
|
|
367
|
+
// Let it go, as the message says when the copy fails.
|
|
368
|
+
paste(letGo);
|
|
369
|
+
yield start(file, 'sqlite');
|
|
370
|
+
(0, chai_1.expect)(yield handAdded(file)).to.deep.equal(GONE);
|
|
371
|
+
(0, chai_1.expect)(yield handAdded(vacuumTarget(backup))).to.deep.equal(KEPT);
|
|
372
|
+
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
373
|
+
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
374
|
+
}));
|
|
375
|
+
// A failed migration recorded: migrate deploy stops at once (P3009).
|
|
376
|
+
it('never suggests migrate deploy for a history that records a failed migration', () => __awaiter(this, void 0, void 0, function* () {
|
|
377
|
+
const file = copyOf(historyFailed);
|
|
378
|
+
sql(file, HAND);
|
|
379
|
+
const newest = LOCAL_MIGRATIONS[LOCAL_MIGRATIONS.length - 1];
|
|
380
|
+
const message = yield startStops(file);
|
|
381
|
+
(0, chai_1.expect)(message).to.match(/stopped rather than delete/);
|
|
382
|
+
(0, chai_1.expect)(message).to.include(newest);
|
|
383
|
+
(0, chai_1.expect)(message).to.include('LabNotes');
|
|
384
|
+
const commands = commandsIn(message);
|
|
385
|
+
(0, chai_1.expect)(commands.filter((c) => c.includes('migrate deploy'))).to.deep.equal([]);
|
|
386
|
+
const backup = commands.find((c) => c.includes('.backup-'));
|
|
387
|
+
const letGo = commands.find((c) => c.includes('--accept-data-loss'));
|
|
388
|
+
(0, chai_1.expect)([backup, letGo].every(Boolean), message).to.equal(true);
|
|
389
|
+
paste(backup);
|
|
390
|
+
paste(letGo);
|
|
391
|
+
yield start(file, 'sqlite');
|
|
392
|
+
(0, chai_1.expect)(yield handAdded(file)).to.deep.equal(GONE);
|
|
393
|
+
(0, chai_1.expect)(yield handAdded(vacuumTarget(backup))).to.deep.equal(KEPT);
|
|
394
|
+
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
395
|
+
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
396
|
+
}));
|
|
397
|
+
// Prisma resolves a relative SQLite URL against the schema's directory,
|
|
398
|
+
// prisma/. Through the first cut of this change the history was read through
|
|
399
|
+
// a client that couldn't open a file in a directory that wasn't there yet,
|
|
400
|
+
// where the CLI makes it.
|
|
401
|
+
it('creates a new database at a relative file: URL in a directory that is not there yet', () => __awaiter(this, void 0, void 0, function* () {
|
|
402
|
+
const target = path_1.default.join(dir, 'relative', 'nested', 'xenon.db');
|
|
403
|
+
const relative = path_1.default.relative(path_1.default.dirname(SCHEMA), target);
|
|
404
|
+
(0, chai_1.expect)(path_1.default.isAbsolute(relative)).to.equal(false);
|
|
405
|
+
config_1.config.databaseUrl = `file:${relative}`;
|
|
406
|
+
config_1.config.databaseProvider = 'sqlite';
|
|
407
|
+
yield (0, run_migrations_1.runMigrations)();
|
|
408
|
+
(0, chai_1.expect)(fs_1.default.existsSync(target), 'the database is where the URL points').to.equal(true);
|
|
409
|
+
(0, chai_1.expect)(matchesSchema(target)).to.equal(true);
|
|
410
|
+
}));
|
|
411
|
+
// A `_prisma_migrations` table that isn't Prisma's (other columns) is no
|
|
412
|
+
// migration history. The default setting started on such a database.
|
|
413
|
+
it("starts a database whose _prisma_migrations table isn't Prisma's", () => __awaiter(this, void 0, void 0, function* () {
|
|
414
|
+
const file = copyOf(withoutHistory);
|
|
415
|
+
sql(file, 'CREATE TABLE "_prisma_migrations" ("id" TEXT PRIMARY KEY, "migration_name" TEXT);');
|
|
416
|
+
yield start(file, 'postgresql');
|
|
417
|
+
(0, chai_1.expect)(matchesSchema(file)).to.equal(true);
|
|
418
|
+
(0, chai_1.expect)(yield seededRows(file)).to.deep.equal(SEEDED);
|
|
419
|
+
}));
|
|
420
|
+
// `npm run db:migrate` checks the URL as a start does, before reading anything.
|
|
421
|
+
it('stops npm run db:migrate on a PostgreSQL URL with the database check, not a read error', () => {
|
|
422
|
+
var _a, _b;
|
|
423
|
+
let output = '';
|
|
424
|
+
let status = 0;
|
|
425
|
+
try {
|
|
426
|
+
output = (0, child_process_1.execFileSync)(path_1.default.join(ROOT, 'node_modules', '.bin', 'ts-node'), ['-T', path_1.default.join(ROOT, 'src', 'scripts', 'initialize-database.ts')], {
|
|
427
|
+
cwd: ROOT,
|
|
428
|
+
env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: 'postgresql://xenon:pw@127.0.0.1:1/xenon', XENON_DB_PROVIDER: '' }),
|
|
429
|
+
stdio: 'pipe',
|
|
430
|
+
}).toString();
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
status = err.status;
|
|
434
|
+
output = `${(_a = err.stdout) !== null && _a !== void 0 ? _a : ''}${(_b = err.stderr) !== null && _b !== void 0 ? _b : ''}`;
|
|
435
|
+
}
|
|
436
|
+
(0, chai_1.expect)(status, output).to.not.equal(0);
|
|
437
|
+
(0, chai_1.expect)(output).to.match(/built for SQLite/);
|
|
438
|
+
(0, chai_1.expect)(output).to.not.match(/could not read/);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
@@ -8,8 +8,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
15
|
const chai_1 = require("chai");
|
|
16
|
+
const os_1 = __importDefault(require("os"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
13
18
|
const run_migrations_1 = require("../../src/scripts/run-migrations");
|
|
14
19
|
const config_1 = require("../../src/config");
|
|
15
20
|
/**
|
|
@@ -22,51 +27,60 @@ const config_1 = require("../../src/config");
|
|
|
22
27
|
* runMigrations must fail fast, and say what actually happened.
|
|
23
28
|
*/
|
|
24
29
|
describe('runMigrations failure handling', () => {
|
|
30
|
+
// runMigrations reads the database before it chooses a command. A file that
|
|
31
|
+
// isn't there has no history and is never opened, so nothing here reaches
|
|
32
|
+
// the server's own database.
|
|
33
|
+
const scratchFile = path_1.default.join(os_1.default.tmpdir(), `xenon-fail-fast-${process.pid}-absent.db`);
|
|
25
34
|
let originalAutoMigrate;
|
|
26
|
-
let
|
|
35
|
+
let originalUrl;
|
|
27
36
|
beforeEach(() => {
|
|
28
37
|
originalAutoMigrate = config_1.config.autoMigrate;
|
|
29
|
-
|
|
38
|
+
originalUrl = config_1.config.databaseUrl;
|
|
30
39
|
config_1.config.autoMigrate = true;
|
|
31
|
-
config_1.config.
|
|
40
|
+
config_1.config.databaseUrl = `file:${scratchFile}`;
|
|
32
41
|
});
|
|
33
42
|
afterEach(() => {
|
|
34
43
|
config_1.config.autoMigrate = originalAutoMigrate;
|
|
35
|
-
config_1.config.
|
|
44
|
+
config_1.config.databaseUrl = originalUrl;
|
|
36
45
|
});
|
|
46
|
+
function failureOf(stderr) {
|
|
47
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
48
|
+
const failingRunner = () => {
|
|
49
|
+
throw Object.assign(new Error('Command failed'), { stderr: Buffer.from(stderr) });
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
yield (0, run_migrations_1.runMigrations)(failingRunner);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
return e;
|
|
56
|
+
}
|
|
57
|
+
throw new Error('runMigrations must not swallow the failure');
|
|
58
|
+
});
|
|
59
|
+
}
|
|
37
60
|
it('throws instead of swallowing when the schema sync fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
38
|
-
const
|
|
39
|
-
prismaErr.stderr = Buffer.from('We found changes that cannot be executed:\n' +
|
|
61
|
+
const thrown = yield failureOf('We found changes that cannot be executed:\n' +
|
|
40
62
|
' - Added the required column `userId` to the `ApiKey` table without a default value.');
|
|
41
|
-
|
|
42
|
-
throw prismaErr;
|
|
43
|
-
};
|
|
44
|
-
let thrown;
|
|
45
|
-
try {
|
|
46
|
-
yield (0, run_migrations_1.runMigrations)(failingRunner);
|
|
47
|
-
}
|
|
48
|
-
catch (e) {
|
|
49
|
-
thrown = e;
|
|
50
|
-
}
|
|
51
|
-
(0, chai_1.expect)(thrown, 'runMigrations must not swallow the failure').to.be.an('Error');
|
|
63
|
+
(0, chai_1.expect)(thrown).to.be.an('Error');
|
|
52
64
|
// The message has to carry the real cause, not just "sync failed".
|
|
53
65
|
(0, chai_1.expect)(thrown.message).to.match(/userId/);
|
|
54
66
|
(0, chai_1.expect)(thrown.message).to.match(/ApiKey/);
|
|
67
|
+
(0, chai_1.expect)(thrown.message).to.match(/adds a required column to a table that already has rows/);
|
|
55
68
|
}));
|
|
56
69
|
it('names the database so the operator knows what to back up', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
|
-
const
|
|
58
|
-
throw Object.assign(new Error('Command failed'), { stderr: Buffer.from('boom') });
|
|
59
|
-
};
|
|
60
|
-
let thrown;
|
|
61
|
-
try {
|
|
62
|
-
yield (0, run_migrations_1.runMigrations)(failingRunner);
|
|
63
|
-
}
|
|
64
|
-
catch (e) {
|
|
65
|
-
thrown = e;
|
|
66
|
-
}
|
|
70
|
+
const thrown = yield failureOf('boom');
|
|
67
71
|
(0, chai_1.expect)(thrown).to.be.an('Error');
|
|
72
|
+
(0, chai_1.expect)(thrown.message).to.include(scratchFile);
|
|
68
73
|
(0, chai_1.expect)(thrown.message).to.match(/XENON_AUTO_MIGRATE/);
|
|
69
74
|
}));
|
|
75
|
+
// Through 2.15.0 every failure was put down to a required column, P3005
|
|
76
|
+
// ("The database schema is not empty") included.
|
|
77
|
+
it("doesn't blame a required column when that isn't the cause", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
const thrown = yield failureOf('Error: P3005\n\nThe database schema is not empty. Read more about how to baseline an ' +
|
|
79
|
+
'existing production database: https://pris.ly/d/migrate-baseline');
|
|
80
|
+
(0, chai_1.expect)(thrown.message).to.not.match(/required column/i);
|
|
81
|
+
(0, chai_1.expect)(thrown.message).to.match(/P3005/);
|
|
82
|
+
(0, chai_1.expect)(thrown.message).to.match(/could not bring the database at .* up to date/);
|
|
83
|
+
}));
|
|
70
84
|
it('resolves normally when the schema sync succeeds', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
71
85
|
let called = false;
|
|
72
86
|
yield (0, run_migrations_1.runMigrations)(() => {
|