@oneuptime/common 14.0.2 → 14.0.3
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/Models/DatabaseModels/AlertFeed.ts +2 -0
- package/Models/DatabaseModels/IncidentAlert.ts +540 -0
- package/Models/DatabaseModels/IncidentFeed.ts +2 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/Project.ts +60 -0
- package/Models/DatabaseModels/RumApplication.ts +52 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/IncidentAlertService.ts +1811 -0
- package/Server/Services/IncidentService.ts +210 -10
- package/Server/Services/IncidentStateTimelineService.ts +24 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
- package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
- package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
- package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
- package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
- package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
- package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
- package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
- package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
- package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
- package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
- package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
- package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
- package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
- package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
- package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
- package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
- package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
- package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
- package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
- package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
- package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
- package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
- package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
- package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
- package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
- package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
- package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
- package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
- package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
- package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
- package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
- package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
- package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
- package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
- package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
- package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
- package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
- package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
- package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
- package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
- package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
- package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
- package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
- package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
- package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
- package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
- package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
- package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
- package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
- package/Tests/Types/Rum/WebVitals.test.ts +173 -0
- package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
- package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
- package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
- package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
- package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
- package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
- package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
- package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
- package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
- package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
- package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
- package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
- package/Types/Incident/IncidentAlertLink.ts +38 -0
- package/Types/Permission.ts +48 -0
- package/Types/Rum/SessionReplay.ts +25 -6
- package/Types/Rum/WebVitals.ts +52 -0
- package/UI/Components/Detail/FieldLabel.tsx +8 -0
- package/UI/Components/InfoCard/InfoCard.tsx +47 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +95 -17
- package/UI/Components/ModelTable/Column.ts +5 -0
- package/UI/Components/Table/TableHeader.tsx +47 -2
- package/UI/Components/Table/Types/Column.ts +7 -0
- package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
- package/UI/Utils/PermissionGate.ts +11 -2
- package/Utils/Incident/IncidentFromAlerts.ts +516 -0
- package/Utils/Rum/SessionTraceState.ts +212 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
- package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +62 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +164 -12
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +25 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
- package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +66 -14
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +18 -2
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
- package/build/dist/UI/Utils/PermissionGate.js +4 -2
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
- package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
- package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ import RumSessionReplayView from "../../../Models/DatabaseModels/RumSessionRepla
|
|
|
13
13
|
import { ColumnAccessControl } from "../../../Types/BaseDatabase/AccessControl";
|
|
14
14
|
import Dictionary from "../../../Types/Dictionary";
|
|
15
15
|
import { TableColumnMetadata } from "../../../Types/Database/TableColumn";
|
|
16
|
+
import TableColumnType from "../../../Types/Database/TableColumnType";
|
|
16
17
|
import Permission from "../../../Types/Permission";
|
|
17
18
|
import SessionReplayCaptureTrigger from "../../../Types/Rum/SessionReplayCaptureTrigger";
|
|
18
19
|
import SessionReplayConsentMode from "../../../Types/Rum/SessionReplayConsentMode";
|
|
@@ -159,6 +160,8 @@ describe("RumApplication session replay configuration", () => {
|
|
|
159
160
|
"sessionReplayMonthlyBudgetInGB",
|
|
160
161
|
"sessionReplayLastChunkReceivedAt",
|
|
161
162
|
"sessionReplayBudgetExceededAt",
|
|
163
|
+
"sessionReplayTracePropagationOrigins",
|
|
164
|
+
"sessionReplaySameOriginTracePropagation",
|
|
162
165
|
];
|
|
163
166
|
|
|
164
167
|
for (const columnName of expectedColumns) {
|
|
@@ -266,6 +269,120 @@ describe("RumApplication session replay configuration", () => {
|
|
|
266
269
|
);
|
|
267
270
|
});
|
|
268
271
|
|
|
272
|
+
/*
|
|
273
|
+
* Same-origin trace propagation: the recorder adds traceparent and a
|
|
274
|
+
* tracestate member carrying the session id to requests to the page's own
|
|
275
|
+
* origin. On by default (for existing applications too, via the column
|
|
276
|
+
* default), so the switch is the way back without a customer redeploy.
|
|
277
|
+
* Turning it on links recordings to backend telemetry that may name the
|
|
278
|
+
* user and sends the session id past the customer's backend, which is the
|
|
279
|
+
* identity switch's reason for its narrower ACL - so it gets that ACL.
|
|
280
|
+
*/
|
|
281
|
+
it("ships same-origin trace propagation on by default, as a required boolean", () => {
|
|
282
|
+
const column: TableColumnMetadata = getColumn(
|
|
283
|
+
"sessionReplaySameOriginTracePropagation",
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
expect(column.defaultValue).toBe(true);
|
|
287
|
+
expect(column.required).toBe(true);
|
|
288
|
+
expect(column.isDefaultValueColumn).toBe(true);
|
|
289
|
+
expect(column.type).toBe(TableColumnType.Boolean);
|
|
290
|
+
expect(column.title).toBe("Same-origin trace propagation");
|
|
291
|
+
|
|
292
|
+
/* The description names what it sends, where, and what that costs. */
|
|
293
|
+
for (const phrase of [
|
|
294
|
+
"traceparent",
|
|
295
|
+
"tracestate",
|
|
296
|
+
"session id",
|
|
297
|
+
"own origin",
|
|
298
|
+
"sampled",
|
|
299
|
+
"third parties",
|
|
300
|
+
]) {
|
|
301
|
+
expect({ phrase, found: column.description?.includes(phrase) }).toEqual({
|
|
302
|
+
phrase,
|
|
303
|
+
found: true,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
/*
|
|
309
|
+
* TraceIdRatioBased hashes the trace id differently in each language SDK
|
|
310
|
+
* (and the OTel spec recommends it for root spans only), so a ratio
|
|
311
|
+
* delegate for sampled remote parents on every service of a polyglot
|
|
312
|
+
* backend drops spans from traces the first hop kept. The description
|
|
313
|
+
* used to recommend the delegate without saying where it goes.
|
|
314
|
+
*/
|
|
315
|
+
it("scopes the remote-parent ratio delegate in the description to the first hop", () => {
|
|
316
|
+
const description: string =
|
|
317
|
+
getColumn("sessionReplaySameOriginTracePropagation").description || "";
|
|
318
|
+
|
|
319
|
+
for (const phrase of [
|
|
320
|
+
"remoteParentSampled ratio delegate only on the service(s) your pages call directly",
|
|
321
|
+
"never on the services they call",
|
|
322
|
+
"ratio decisions differ between language SDKs",
|
|
323
|
+
"tail sampling in an OpenTelemetry Collector",
|
|
324
|
+
]) {
|
|
325
|
+
expect({ phrase, found: description.includes(phrase) }).toEqual({
|
|
326
|
+
phrase,
|
|
327
|
+
found: true,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
expect(description).not.toContain(
|
|
332
|
+
"(set a remoteParentSampled delegate to keep ratio sampling)",
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("gives same-origin trace propagation the identity switch's narrower create and update ACL", () => {
|
|
337
|
+
const accessControl: Dictionary<ColumnAccessControl> =
|
|
338
|
+
model.getColumnAccessControlForAllColumns();
|
|
339
|
+
const sameOrigin: ColumnAccessControl | undefined =
|
|
340
|
+
accessControl["sessionReplaySameOriginTracePropagation"];
|
|
341
|
+
const identity: ColumnAccessControl | undefined =
|
|
342
|
+
accessControl["sessionReplayCaptureUserIdentity"];
|
|
343
|
+
|
|
344
|
+
expect(sameOrigin).toBeDefined();
|
|
345
|
+
expect([...(sameOrigin?.create || [])].sort()).toEqual(
|
|
346
|
+
[...(identity?.create || [])].sort(),
|
|
347
|
+
);
|
|
348
|
+
expect([...(sameOrigin?.update || [])].sort()).toEqual(
|
|
349
|
+
[...(identity?.update || [])].sort(),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
for (const permission of [
|
|
353
|
+
Permission.ProjectMember,
|
|
354
|
+
Permission.SettingsMember,
|
|
355
|
+
]) {
|
|
356
|
+
expect(sameOrigin?.create || []).not.toContain(permission);
|
|
357
|
+
expect(sameOrigin?.update || []).not.toContain(permission);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
expect(sameOrigin?.update || []).toContain(Permission.ProjectAdmin);
|
|
361
|
+
expect(sameOrigin?.update || []).toContain(Permission.EditRumApplication);
|
|
362
|
+
|
|
363
|
+
/* Readable by everyone who can read the rest of the replay policy. */
|
|
364
|
+
expect([...(sameOrigin?.read || [])].sort()).toEqual(
|
|
365
|
+
[
|
|
366
|
+
...(accessControl["sessionReplayTracePropagationOrigins"]?.read || []),
|
|
367
|
+
].sort(),
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
/*
|
|
372
|
+
* The origins list used to promise "Empty means never inject". With
|
|
373
|
+
* same-origin propagation on by default that is false for the page's own
|
|
374
|
+
* origin, so the list is described as the CROSS-origin one.
|
|
375
|
+
*/
|
|
376
|
+
it("describes the trace propagation origins as the cross-origin list", () => {
|
|
377
|
+
const description: string =
|
|
378
|
+
getColumn("sessionReplayTracePropagationOrigins").description || "";
|
|
379
|
+
|
|
380
|
+
expect(description).not.toContain("Empty means never inject");
|
|
381
|
+
expect(description).toContain("OTHER origins");
|
|
382
|
+
expect(description).toContain("Same-origin trace propagation");
|
|
383
|
+
expect(description).toContain("never the session id");
|
|
384
|
+
});
|
|
385
|
+
|
|
269
386
|
it("never exposes the ingest diagnostics as writable", () => {
|
|
270
387
|
const accessControl: Dictionary<ColumnAccessControl> =
|
|
271
388
|
model.getColumnAccessControlForAllColumns();
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { AddIncidentAlert1794900000000 } from "../../../../Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert";
|
|
2
|
+
import SchemaMigrations from "../../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
|
|
3
|
+
import IncidentAlert from "../../../../Models/DatabaseModels/IncidentAlert";
|
|
4
|
+
import { describe, expect, test } from "@jest/globals";
|
|
5
|
+
import {
|
|
6
|
+
DefaultNamingStrategy,
|
|
7
|
+
QueryRunner,
|
|
8
|
+
getMetadataArgsStorage,
|
|
9
|
+
} from "typeorm";
|
|
10
|
+
import type { ColumnMetadataArgs } from "typeorm/metadata-args/ColumnMetadataArgs";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* The schema half of linking alerts to incidents.
|
|
14
|
+
*
|
|
15
|
+
* The migration was generated against the models, so its names are TypeORM's
|
|
16
|
+
* by construction. What this pins is that it STAYS the migration for them:
|
|
17
|
+
* the link table is created with every column the model persists, a pair is
|
|
18
|
+
* unique at the database level (the service's @UniqueColumnsTogether check is
|
|
19
|
+
* not race free on its own), a link goes with its project, incident or alert,
|
|
20
|
+
* outlives the user who made it, every constraint and index carries
|
|
21
|
+
* TypeORM's own name - a hand edit that renamed one would pass review and
|
|
22
|
+
* then fail the Schema Drift job - the two project switches are off for every
|
|
23
|
+
* existing project, and down() undoes up() in reverse.
|
|
24
|
+
*
|
|
25
|
+
* Fake QueryRunner only; IncidentAlertPostgres.test.ts runs the same
|
|
26
|
+
* guarantees against a migrated database.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const TABLE: string = "IncidentAlert";
|
|
30
|
+
const UNIQUE_COLUMNS: Array<string> = ["incidentId", "alertId", "projectId"];
|
|
31
|
+
const PROJECT_SWITCHES: Array<string> = [
|
|
32
|
+
"acknowledgeLinkedAlertsWhenIncidentAcknowledged",
|
|
33
|
+
"resolveLinkedAlertsWhenIncidentResolved",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const namingStrategy: DefaultNamingStrategy = new DefaultNamingStrategy();
|
|
37
|
+
|
|
38
|
+
type RecordQueriesFunction = (
|
|
39
|
+
direction: "up" | "down",
|
|
40
|
+
) => Promise<Array<string>>;
|
|
41
|
+
|
|
42
|
+
const recordQueries: RecordQueriesFunction = async (
|
|
43
|
+
direction: "up" | "down",
|
|
44
|
+
): Promise<Array<string>> => {
|
|
45
|
+
const statements: Array<string> = [];
|
|
46
|
+
|
|
47
|
+
const queryRunner: QueryRunner = {
|
|
48
|
+
query: async (statement: string): Promise<void> => {
|
|
49
|
+
statements.push(statement);
|
|
50
|
+
},
|
|
51
|
+
} as unknown as QueryRunner;
|
|
52
|
+
|
|
53
|
+
await new AddIncidentAlert1794900000000()[direction](queryRunner);
|
|
54
|
+
|
|
55
|
+
return statements;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Columns a model persists, inherited ones (_id, version, ...) included.
|
|
59
|
+
function persistedColumns(modelType: unknown): Array<string> {
|
|
60
|
+
const names: Array<string> = [];
|
|
61
|
+
let current: unknown = modelType;
|
|
62
|
+
|
|
63
|
+
while (typeof current === "function" && current !== Function.prototype) {
|
|
64
|
+
for (const column of getMetadataArgsStorage().columns) {
|
|
65
|
+
if (column.target === current) {
|
|
66
|
+
const args: ColumnMetadataArgs = column;
|
|
67
|
+
names.push(args.options.name || args.propertyName);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
current = Object.getPrototypeOf(current);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return names;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createTableStatement(statements: Array<string>): string {
|
|
78
|
+
const matches: Array<string> = statements.filter(
|
|
79
|
+
(statement: string): boolean => {
|
|
80
|
+
return statement.startsWith(`CREATE TABLE "${TABLE}" (`);
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(matches).toHaveLength(1);
|
|
85
|
+
|
|
86
|
+
return matches[0]!;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function foreignKeyStatement(
|
|
90
|
+
statements: Array<string>,
|
|
91
|
+
columnName: string,
|
|
92
|
+
): string {
|
|
93
|
+
const name: string = namingStrategy.foreignKeyName(TABLE, [columnName]);
|
|
94
|
+
const matches: Array<string> = statements.filter(
|
|
95
|
+
(statement: string): boolean => {
|
|
96
|
+
return statement.startsWith(
|
|
97
|
+
`ALTER TABLE "${TABLE}" ADD CONSTRAINT "${name}" FOREIGN KEY ("${columnName}")`,
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect({ columnName, found: matches.length }).toEqual({
|
|
103
|
+
columnName,
|
|
104
|
+
found: 1,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return matches[0]!;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function captured(statements: Array<string>, pattern: RegExp): Array<string> {
|
|
111
|
+
return statements
|
|
112
|
+
.map((statement: string): string | null => {
|
|
113
|
+
const match: RegExpMatchArray | null = statement.match(pattern);
|
|
114
|
+
return match ? match[1]! : null;
|
|
115
|
+
})
|
|
116
|
+
.filter((value: string | null): value is string => {
|
|
117
|
+
return value !== null;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
describe("AddIncidentAlert1794900000000", () => {
|
|
122
|
+
test("is registered under the name its class carries", () => {
|
|
123
|
+
const migration: AddIncidentAlert1794900000000 =
|
|
124
|
+
new AddIncidentAlert1794900000000();
|
|
125
|
+
|
|
126
|
+
expect(migration.name).toBe("AddIncidentAlert1794900000000");
|
|
127
|
+
expect(SchemaMigrations).toContain(AddIncidentAlert1794900000000);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("its timestamp keeps it behind every migration registered before it", () => {
|
|
131
|
+
const names: Array<string> = (
|
|
132
|
+
SchemaMigrations as unknown as Array<{ name: string }>
|
|
133
|
+
).map((registered: { name: string }): string => {
|
|
134
|
+
return registered.name;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const ownIndex: number = names.indexOf("AddIncidentAlert1794900000000");
|
|
138
|
+
|
|
139
|
+
expect(ownIndex).toBeGreaterThan(0);
|
|
140
|
+
|
|
141
|
+
const notBehind: Array<string> = names
|
|
142
|
+
.slice(0, ownIndex)
|
|
143
|
+
.filter((className: string): boolean => {
|
|
144
|
+
const match: RegExpMatchArray | null = className.match(/(\d{13})$/);
|
|
145
|
+
return match !== null && Number(match[1]) >= 1794900000000;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(notBehind).toEqual([]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/*
|
|
152
|
+
* It was registered last. Pinned as "nothing registered after it is older"
|
|
153
|
+
* rather than "it is last", which the next migration would falsify without
|
|
154
|
+
* going anywhere near this table.
|
|
155
|
+
*/
|
|
156
|
+
test("nothing registered after it is older than it", () => {
|
|
157
|
+
const names: Array<string> = (
|
|
158
|
+
SchemaMigrations as unknown as Array<{ name: string }>
|
|
159
|
+
).map((registered: { name: string }): string => {
|
|
160
|
+
return registered.name;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const ownIndex: number = names.indexOf("AddIncidentAlert1794900000000");
|
|
164
|
+
|
|
165
|
+
expect(ownIndex).toBeGreaterThan(0);
|
|
166
|
+
|
|
167
|
+
const olderAfter: Array<string> = names
|
|
168
|
+
.slice(ownIndex + 1)
|
|
169
|
+
.filter((className: string): boolean => {
|
|
170
|
+
const match: RegExpMatchArray | null = className.match(/(\d{13})$/);
|
|
171
|
+
return match === null || Number(match[1]) <= 1794900000000;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(olderAfter).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("touches only the IncidentAlert table and the Project switches", async () => {
|
|
178
|
+
const statements: Array<string> = await recordQueries("up");
|
|
179
|
+
|
|
180
|
+
for (const statement of statements) {
|
|
181
|
+
const table: RegExpMatchArray | null = statement.match(
|
|
182
|
+
/^(?:CREATE TABLE|ALTER TABLE|CREATE (?:UNIQUE )?INDEX "[^"]+" ON) "([^"]+)"/,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect({ statement, table: table?.[1] }).toEqual({
|
|
186
|
+
statement,
|
|
187
|
+
table: expect.stringMatching(/^(IncidentAlert|Project)$/),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const projectStatements: Array<string> = statements.filter(
|
|
192
|
+
(statement: string): boolean => {
|
|
193
|
+
return statement.startsWith('ALTER TABLE "Project"');
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Only the two switches are added to Project - nothing dropped, nothing retyped.
|
|
198
|
+
expect(projectStatements).toHaveLength(PROJECT_SWITCHES.length);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("creates IncidentAlert with every column the model persists", async () => {
|
|
202
|
+
const statement: string = createTableStatement(await recordQueries("up"));
|
|
203
|
+
const columns: Array<string> = persistedColumns(IncidentAlert);
|
|
204
|
+
|
|
205
|
+
expect(columns).toEqual(
|
|
206
|
+
expect.arrayContaining([
|
|
207
|
+
"_id",
|
|
208
|
+
"projectId",
|
|
209
|
+
"incidentId",
|
|
210
|
+
"alertId",
|
|
211
|
+
"createdByUserId",
|
|
212
|
+
"deletedByUserId",
|
|
213
|
+
]),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
for (const column of columns) {
|
|
217
|
+
expect({ column, created: statement.includes(`"${column}"`) }).toEqual({
|
|
218
|
+
column,
|
|
219
|
+
created: true,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
expect(statement).toContain('"projectId" uuid NOT NULL');
|
|
224
|
+
expect(statement).toContain('"incidentId" uuid NOT NULL');
|
|
225
|
+
expect(statement).toContain('"alertId" uuid NOT NULL');
|
|
226
|
+
expect(statement).toContain('"createdByUserId" uuid,');
|
|
227
|
+
expect(statement).toContain('"deletedByUserId" uuid,');
|
|
228
|
+
expect(statement).toContain(
|
|
229
|
+
`CONSTRAINT "${namingStrategy.primaryKeyName(TABLE, ["_id"])}" PRIMARY KEY ("_id")`,
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("a pair can only be linked once: a unique index on (incident, alert, project)", async () => {
|
|
234
|
+
const statements: Array<string> = await recordQueries("up");
|
|
235
|
+
const name: string = namingStrategy.indexName(TABLE, UNIQUE_COLUMNS);
|
|
236
|
+
|
|
237
|
+
expect(statements).toContain(
|
|
238
|
+
`CREATE UNIQUE INDEX "${name}" ON "${TABLE}" (${UNIQUE_COLUMNS.map(
|
|
239
|
+
(column: string): string => {
|
|
240
|
+
return `"${column}"`;
|
|
241
|
+
},
|
|
242
|
+
).join(", ")}) `,
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test.each(["projectId", "incidentId", "alertId"])(
|
|
247
|
+
"indexes %s, which the link lists filter on",
|
|
248
|
+
async (column: string) => {
|
|
249
|
+
const statements: Array<string> = await recordQueries("up");
|
|
250
|
+
const name: string = namingStrategy.indexName(TABLE, [column]);
|
|
251
|
+
|
|
252
|
+
expect(statements).toContain(
|
|
253
|
+
`CREATE INDEX "${name}" ON "${TABLE}" ("${column}") `,
|
|
254
|
+
);
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
test.each([
|
|
259
|
+
["projectId", "Project", "CASCADE"],
|
|
260
|
+
["incidentId", "Incident", "CASCADE"],
|
|
261
|
+
["alertId", "Alert", "CASCADE"],
|
|
262
|
+
["createdByUserId", "User", "SET NULL"],
|
|
263
|
+
["deletedByUserId", "User", "SET NULL"],
|
|
264
|
+
])(
|
|
265
|
+
"%s references %s and is %s when it is deleted",
|
|
266
|
+
async (column: string, referenced: string, onDelete: string) => {
|
|
267
|
+
expect(foreignKeyStatement(await recordQueries("up"), column)).toContain(
|
|
268
|
+
`REFERENCES "${referenced}"("_id") ON DELETE ${onDelete} ON UPDATE NO ACTION`,
|
|
269
|
+
);
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
test.each(PROJECT_SWITCHES)(
|
|
274
|
+
"adds Project.%s, off for every existing project",
|
|
275
|
+
async (column: string) => {
|
|
276
|
+
expect(await recordQueries("up")).toContain(
|
|
277
|
+
`ALTER TABLE "Project" ADD "${column}" boolean NOT NULL DEFAULT false`,
|
|
278
|
+
);
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
test("down() removes every constraint up() added, in reverse", async () => {
|
|
283
|
+
const up: Array<string> = await recordQueries("up");
|
|
284
|
+
const down: Array<string> = await recordQueries("down");
|
|
285
|
+
|
|
286
|
+
const added: Array<string> = captured(up, /ADD CONSTRAINT "([^"]+)"/);
|
|
287
|
+
const dropped: Array<string> = captured(down, /DROP CONSTRAINT "([^"]+)"/);
|
|
288
|
+
|
|
289
|
+
expect(added).toHaveLength(5);
|
|
290
|
+
expect(dropped).toEqual([...added].reverse());
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("down() drops every index up() created", async () => {
|
|
294
|
+
const up: Array<string> = await recordQueries("up");
|
|
295
|
+
const down: Array<string> = await recordQueries("down");
|
|
296
|
+
|
|
297
|
+
const created: Array<string> = captured(
|
|
298
|
+
up,
|
|
299
|
+
/^CREATE (?:UNIQUE )?INDEX "([^"]+)"/,
|
|
300
|
+
);
|
|
301
|
+
const dropped: Array<string> = captured(
|
|
302
|
+
down,
|
|
303
|
+
/^DROP INDEX "public"\."([^"]+)"$/,
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
expect(created).toHaveLength(4);
|
|
307
|
+
expect([...dropped].sort()).toEqual([...created].sort());
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("down() drops the Project switches and then the table, last", async () => {
|
|
311
|
+
const down: Array<string> = await recordQueries("down");
|
|
312
|
+
|
|
313
|
+
for (const column of PROJECT_SWITCHES) {
|
|
314
|
+
expect(down).toContain(`ALTER TABLE "Project" DROP COLUMN "${column}"`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
expect(down[down.length - 1]).toBe(`DROP TABLE "${TABLE}"`);
|
|
318
|
+
expect(
|
|
319
|
+
down.filter((statement: string): boolean => {
|
|
320
|
+
return statement.startsWith("DROP TABLE");
|
|
321
|
+
}),
|
|
322
|
+
).toEqual([`DROP TABLE "${TABLE}"`]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("down() mirrors up(): every statement kind up() ran has its undo", async () => {
|
|
326
|
+
const up: Array<string> = await recordQueries("up");
|
|
327
|
+
const down: Array<string> = await recordQueries("down");
|
|
328
|
+
|
|
329
|
+
const addedColumns: Array<string> = captured(
|
|
330
|
+
up,
|
|
331
|
+
/^ALTER TABLE "Project" ADD "([^"]+)"/,
|
|
332
|
+
);
|
|
333
|
+
const droppedColumns: Array<string> = captured(
|
|
334
|
+
down,
|
|
335
|
+
/^ALTER TABLE "Project" DROP COLUMN "([^"]+)"$/,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
expect(droppedColumns).toEqual([...addedColumns].reverse());
|
|
339
|
+
expect(down).toHaveLength(up.length);
|
|
340
|
+
});
|
|
341
|
+
});
|