@oneuptime/common 14.0.6 → 14.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/Models/DatabaseModels/InventoryItem.ts +23 -12
  2. package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  4. package/Server/Services/AlertService.ts +32 -36
  5. package/Server/Services/IncidentAlertService.ts +609 -2
  6. package/Server/Services/IncidentService.ts +134 -44
  7. package/Server/Services/InventoryItemService.ts +38 -0
  8. package/Server/Services/IoTDeviceCredentialService.ts +13 -4
  9. package/Server/Services/ScheduledMaintenanceService.ts +50 -46
  10. package/Server/Services/UserNotificationRuleService.ts +55 -53
  11. package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
  12. package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
  13. package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
  14. package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
  15. package/Server/Utils/Response.ts +31 -0
  16. package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
  17. package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
  18. package/Server/Utils/Topology/TopologyRequest.ts +463 -0
  19. package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
  20. package/Server/Utils/Topology/TopologySql.ts +1024 -0
  21. package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
  22. package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
  23. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
  24. package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
  25. package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
  26. package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
  27. package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
  28. package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
  29. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
  30. package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
  31. package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
  32. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +292 -1
  33. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
  34. package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
  35. package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
  36. package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
  37. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
  38. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
  39. package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
  40. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
  41. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
  42. package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
  43. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
  44. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
  45. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
  46. package/Tests/Server/Services/DomainService.test.ts +525 -0
  47. package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
  48. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
  49. package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
  50. package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
  51. package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
  52. package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
  53. package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
  54. package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
  55. package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
  56. package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
  57. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
  58. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
  59. package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
  60. package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
  61. package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
  62. package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
  63. package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
  64. package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
  65. package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
  66. package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
  67. package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
  68. package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
  69. package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
  70. package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
  71. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
  72. package/Types/Incident/IncidentAlertLink.ts +19 -0
  73. package/Types/Telemetry/InventoryLiveness.ts +26 -0
  74. package/Types/Topology/TopologyApi.ts +428 -0
  75. package/Types/Topology/TopologyTypeRules.ts +191 -0
  76. package/Utils/Incident/IncidentFromAlerts.ts +78 -0
  77. package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
  78. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  79. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
  80. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
  81. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  82. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  83. package/build/dist/Server/Services/AlertService.js +21 -33
  84. package/build/dist/Server/Services/AlertService.js.map +1 -1
  85. package/build/dist/Server/Services/IncidentAlertService.js +457 -3
  86. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
  87. package/build/dist/Server/Services/IncidentService.js +96 -42
  88. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  89. package/build/dist/Server/Services/InventoryItemService.js +31 -0
  90. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  91. package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
  92. package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
  93. package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -40
  94. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  95. package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
  96. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  97. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
  98. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
  99. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
  100. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
  101. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
  102. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
  103. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
  104. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
  105. package/build/dist/Server/Utils/Response.js +30 -0
  106. package/build/dist/Server/Utils/Response.js.map +1 -1
  107. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
  108. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
  109. package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
  110. package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
  111. package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
  112. package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
  113. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
  114. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
  115. package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
  116. package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
  117. package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
  118. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
  119. package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
  120. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
  121. package/build/dist/Types/Topology/TopologyApi.js +55 -0
  122. package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
  123. package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
  124. package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
  125. package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
  126. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
  127. package/package.json +1 -1
@@ -0,0 +1,386 @@
1
+ import InventoryItem from "../../../../Models/DatabaseModels/InventoryItem";
2
+ import {
3
+ AddInventoryItemProjectEntityKeyIndex1795200000000,
4
+ INVENTORY_ITEM_INLINE_INDEX_BUILD_MAX_ROWS,
5
+ INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX,
6
+ INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX_RUNBOOK,
7
+ } from "../../../../Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex";
8
+ import SchemaMigrations from "../../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
9
+ import logger from "../../../../Server/Utils/Logger";
10
+ import { QueryRunner, getMetadataArgsStorage } from "typeorm";
11
+ import type { IndexMetadataArgs } from "typeorm/metadata-args/IndexMetadataArgs";
12
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
13
+
14
+ /*
15
+ * The ("projectId", "entityKey") index behind the Topology API's key
16
+ * lookups. What must hold together:
17
+ *
18
+ * 1. The entity declares it BY NAME with synchronize: false, or the next
19
+ * generated migration drops it (the schema builder drops every index it
20
+ * cannot match by name) or tries to create a second copy.
21
+ * 2. The migration can never outlive the client's query timeout: it leaves
22
+ * statement_timeout at the connection's value (a raised one keeps an
23
+ * abandoned build holding its SHARE lock on the server), bounds its lock
24
+ * wait, and does not build at all on a table too large to index inside
25
+ * that window — it logs the CONCURRENTLY runbook instead.
26
+ * 3. It clears an INVALID leftover of a failed CONCURRENTLY pre-build, and
27
+ * an operator's valid pre-built index makes it a no-op.
28
+ * 4. It is registered in order, so it actually runs.
29
+ *
30
+ * Pure metadata and a scripted query runner — no database. The same
31
+ * branches run against a real Postgres in
32
+ * InventoryItemEntityKeyIndexMigrationPostgres.test.ts.
33
+ */
34
+
35
+ type Recorded = { sql: string; params: Array<unknown> | undefined };
36
+
37
+ interface Catalog {
38
+ /* The index as pg_index has it: absent, valid or invalid. */
39
+ index: "absent" | "valid" | "invalid";
40
+ reltuples: number;
41
+ relpages: number;
42
+ pages: number;
43
+ /* What the bounded count answers when the statistics are unknown. */
44
+ counted?: number;
45
+ /* An online (CONCURRENTLY) build of the index is running right now. */
46
+ building?: boolean;
47
+ }
48
+
49
+ const SMALL: Catalog = {
50
+ index: "absent",
51
+ reltuples: 5_000,
52
+ relpages: 100,
53
+ pages: 100,
54
+ };
55
+
56
+ async function record(
57
+ catalog: Catalog,
58
+ run: (queryRunner: QueryRunner) => Promise<void>,
59
+ ): Promise<Array<Recorded>> {
60
+ const recorded: Array<Recorded> = [];
61
+ const queryRunner: QueryRunner = {
62
+ query: async (sql: string, params?: Array<unknown>): Promise<unknown> => {
63
+ recorded.push({ sql, params });
64
+ if (sql.includes("pg_stat_progress_create_index")) {
65
+ return catalog.building ? [{ "?column?": 1 }] : [];
66
+ }
67
+ if (sql.includes("pg_index")) {
68
+ return catalog.index === "absent"
69
+ ? []
70
+ : [{ isValid: catalog.index === "valid" }];
71
+ }
72
+ if (sql.includes("reltuples")) {
73
+ return [
74
+ {
75
+ reltuples: catalog.reltuples,
76
+ relpages: catalog.relpages,
77
+ pages: catalog.pages,
78
+ },
79
+ ];
80
+ }
81
+ if (sql.includes("COUNT(*)")) {
82
+ return [{ count: catalog.counted ?? 0 }];
83
+ }
84
+ return [];
85
+ },
86
+ } as unknown as QueryRunner;
87
+ await run(queryRunner);
88
+ return recorded;
89
+ }
90
+
91
+ async function up(
92
+ catalog: Catalog,
93
+ maxRows?: number,
94
+ ): Promise<Array<Recorded>> {
95
+ return await record(
96
+ catalog,
97
+ async (queryRunner: QueryRunner): Promise<void> => {
98
+ await new AddInventoryItemProjectEntityKeyIndex1795200000000(maxRows).up(
99
+ queryRunner,
100
+ );
101
+ },
102
+ );
103
+ }
104
+
105
+ function statementsOf(recorded: Array<Recorded>): Array<string> {
106
+ return recorded.map((entry: Recorded): string => {
107
+ return entry.sql.replace(/\s+/g, " ").trim();
108
+ });
109
+ }
110
+
111
+ /* Structurally typed: @jest/globals and @types/jest disagree on spy types. */
112
+ type WarnSpy = { mock: { calls: Array<Array<unknown>> } };
113
+
114
+ function silenceWarnings(): WarnSpy {
115
+ return jest.spyOn(logger, "warn").mockImplementation((): void => {
116
+ return undefined;
117
+ }) as unknown as WarnSpy;
118
+ }
119
+
120
+ const CREATE: string = `CREATE INDEX IF NOT EXISTS "IDX_INVENTORY_ITEM_PROJECT_ENTITY_KEY" ON "InventoryItem" ("projectId", "entityKey")`;
121
+ const DROP: string = `DROP INDEX IF EXISTS "IDX_INVENTORY_ITEM_PROJECT_ENTITY_KEY"`;
122
+
123
+ describe("InventoryItem (projectId, entityKey) index", () => {
124
+ afterEach(() => {
125
+ jest.restoreAllMocks();
126
+ });
127
+
128
+ test("the entity declares it by name, outside schema synchronization", () => {
129
+ const declared: IndexMetadataArgs | undefined =
130
+ getMetadataArgsStorage().indices.find(
131
+ (index: IndexMetadataArgs): boolean => {
132
+ return (
133
+ index.target === InventoryItem &&
134
+ index.name === INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX
135
+ );
136
+ },
137
+ );
138
+ expect(declared).toBeDefined();
139
+ expect(declared?.synchronize).toBe(false);
140
+ expect(declared?.columns).toEqual(["projectId", "entityKey"]);
141
+ expect(declared?.unique).toBeFalsy();
142
+ });
143
+
144
+ test("the inline build is bounded at a million rows by default", () => {
145
+ expect(INVENTORY_ITEM_INLINE_INDEX_BUILD_MAX_ROWS).toBe(1_000_000);
146
+ expect(INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX_RUNBOOK).toBe(
147
+ `CREATE INDEX CONCURRENTLY IF NOT EXISTS "IDX_INVENTORY_ITEM_PROJECT_ENTITY_KEY" ON "InventoryItem" ("projectId", "entityKey")`,
148
+ );
149
+ });
150
+
151
+ test("up() on a small table: the connection's timeouts, then the plain, idempotent, non-unique build", async () => {
152
+ const recorded: Array<Recorded> = await up(SMALL);
153
+ const statements: Array<string> = statementsOf(recorded);
154
+ expect(statements).toHaveLength(5);
155
+ /*
156
+ * Never a raised statement_timeout: the client's query_timeout (35 s)
157
+ * cannot be raised, and a build the client abandons keeps running on
158
+ * the server with its SHARE lock for as long as the server allows.
159
+ */
160
+ expect(statements[0]).toBe(`SET LOCAL statement_timeout = DEFAULT`);
161
+ expect(statements[1]).toBe(`SET LOCAL lock_timeout = '5s'`);
162
+ expect(statements[2]).toContain("x.indisvalid");
163
+ expect(statements[2]).toContain("n.nspname = current_schema()");
164
+ expect(recorded[2]!.params).toEqual([
165
+ INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX,
166
+ ]);
167
+ expect(statements[3]).toContain("c.reltuples");
168
+ expect(statements[3]).toContain("pg_relation_size(c.oid)");
169
+ expect(statements[4]).toBe(CREATE);
170
+ // Migrations run inside a transaction: CONCURRENTLY would abort the run.
171
+ expect(statements.join(" ")).not.toContain("CONCURRENTLY");
172
+ expect(statements.join(" ")).not.toContain("UNIQUE");
173
+ expect(statements.join(" ")).not.toMatch(/statement_timeout = '\d+/);
174
+ });
175
+
176
+ test("up(): a valid index already there (an operator's pre-build) is left alone", async () => {
177
+ const statements: Array<string> = statementsOf(
178
+ await up({ ...SMALL, index: "valid" }),
179
+ );
180
+ expect(statements).toEqual([
181
+ `SET LOCAL statement_timeout = DEFAULT`,
182
+ `SET LOCAL lock_timeout = '5s'`,
183
+ expect.stringContaining("pg_index"),
184
+ ]);
185
+ });
186
+
187
+ test("up(): an INVALID leftover of a failed online build is dropped, then rebuilt", async () => {
188
+ const statements: Array<string> = statementsOf(
189
+ await up({ ...SMALL, index: "invalid" }),
190
+ );
191
+ expect(statements).toEqual([
192
+ `SET LOCAL statement_timeout = DEFAULT`,
193
+ `SET LOCAL lock_timeout = '5s'`,
194
+ expect.stringContaining("pg_index"),
195
+ expect.stringContaining("pg_stat_progress_create_index"),
196
+ DROP,
197
+ expect.stringContaining("reltuples"),
198
+ CREATE,
199
+ ]);
200
+ });
201
+
202
+ /*
203
+ * A CONCURRENTLY build that is still running is INVALID until it finishes.
204
+ * Dropping it would queue for ACCESS EXCLUSIVE behind the build and stall
205
+ * every reader and writer of InventoryItem, so it is left alone.
206
+ */
207
+ test("up(): an online build still in progress is neither dropped nor raced", async () => {
208
+ const warn: WarnSpy = silenceWarnings();
209
+ const statements: Array<string> = statementsOf(
210
+ await up({ ...SMALL, index: "invalid", building: true }),
211
+ );
212
+ expect(statements).not.toContain(DROP);
213
+ expect(statements.join(" ")).not.toContain("CREATE INDEX");
214
+ expect(warn).toHaveBeenCalledTimes(1);
215
+ expect(String(warn.mock.calls[0]![0])).toContain("in progress");
216
+ });
217
+
218
+ test("up() on a table too large to index inline: no build, a warning with the runbook", async () => {
219
+ const warn: WarnSpy = silenceWarnings();
220
+ const statements: Array<string> = statementsOf(
221
+ await up({
222
+ index: "absent",
223
+ reltuples: 2_500_000,
224
+ relpages: 50_000,
225
+ pages: 50_000,
226
+ }),
227
+ );
228
+ expect(statements).not.toContain(CREATE);
229
+ expect(statements.join(" ")).not.toContain("CREATE INDEX");
230
+ expect(statements.join(" ")).not.toContain("COUNT(*)");
231
+ expect(warn).toHaveBeenCalledTimes(1);
232
+ const message: string = String(warn.mock.calls[0]![0]);
233
+ expect(message).toContain(INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX_RUNBOOK);
234
+ expect(message).toContain("2500000");
235
+ expect(message).toContain("NOT created");
236
+ });
237
+
238
+ test("up(): an INVALID leftover on a large table is still dropped, so the runbook works as written", async () => {
239
+ silenceWarnings();
240
+ const statements: Array<string> = statementsOf(
241
+ await up({
242
+ index: "invalid",
243
+ reltuples: 3_000_000,
244
+ relpages: 60_000,
245
+ pages: 60_000,
246
+ }),
247
+ );
248
+ expect(statements).toContain(DROP);
249
+ expect(statements).not.toContain(CREATE);
250
+ });
251
+
252
+ test("the estimate counts growth since the last ANALYZE (reltuples scaled to the current pages)", async () => {
253
+ silenceWarnings();
254
+ // Analyzed at 600k rows over 10k pages; the table has tripled since.
255
+ const statements: Array<string> = statementsOf(
256
+ await up({
257
+ index: "absent",
258
+ reltuples: 600_000,
259
+ relpages: 10_000,
260
+ pages: 30_000,
261
+ }),
262
+ );
263
+ expect(statements).not.toContain(CREATE);
264
+ });
265
+
266
+ test.each([
267
+ ["never analyzed (reltuples -1)", { reltuples: -1, relpages: 0 }],
268
+ [
269
+ "analyzed while empty, has pages now",
270
+ { reltuples: 0, relpages: 0, pages: 20 },
271
+ ],
272
+ ])(
273
+ "unknown statistics (%s): a count bounded just past the threshold decides",
274
+ async (_label: string, statistics: Partial<Catalog>) => {
275
+ silenceWarnings();
276
+ const base: Catalog = {
277
+ index: "absent",
278
+ reltuples: -1,
279
+ relpages: 0,
280
+ pages: 20,
281
+ ...statistics,
282
+ };
283
+
284
+ const small: Array<Recorded> = await up({ ...base, counted: 10 }, 10);
285
+ const count: Recorded | undefined = small.find((entry: Recorded) => {
286
+ return entry.sql.includes("COUNT(*)");
287
+ });
288
+ expect(count?.sql.replace(/\s+/g, " ")).toBe(
289
+ `SELECT COUNT(*)::int AS "count" FROM (SELECT 1 FROM "InventoryItem" LIMIT $1) bounded`,
290
+ );
291
+ expect(count?.params).toEqual([11]);
292
+ expect(statementsOf(small)).toContain(CREATE);
293
+
294
+ const large: Array<Recorded> = await up({ ...base, counted: 11 }, 10);
295
+ expect(statementsOf(large)).not.toContain(CREATE);
296
+ },
297
+ );
298
+
299
+ test("the default bound is used when the migration is constructed as TypeORM does", async () => {
300
+ const recorded: Array<Recorded> = await up({
301
+ index: "absent",
302
+ reltuples: -1,
303
+ relpages: 0,
304
+ pages: 0,
305
+ counted: 7,
306
+ });
307
+ const count: Recorded | undefined = recorded.find((entry: Recorded) => {
308
+ return entry.sql.includes("COUNT(*)");
309
+ });
310
+ expect(count?.params).toEqual([
311
+ INVENTORY_ITEM_INLINE_INDEX_BUILD_MAX_ROWS + 1,
312
+ ]);
313
+ expect(statementsOf(recorded)).toContain(CREATE);
314
+ });
315
+
316
+ test("down() drops exactly that index, without waiting long for its lock", async () => {
317
+ const recorded: Array<Recorded> = await record(
318
+ SMALL,
319
+ async (queryRunner: QueryRunner): Promise<void> => {
320
+ await new AddInventoryItemProjectEntityKeyIndex1795200000000().down(
321
+ queryRunner,
322
+ );
323
+ },
324
+ );
325
+ expect(statementsOf(recorded)).toEqual([
326
+ `SET LOCAL statement_timeout = DEFAULT`,
327
+ `SET LOCAL lock_timeout = '5s'`,
328
+ DROP,
329
+ ]);
330
+ });
331
+
332
+ describe("registration", () => {
333
+ const MIGRATION_NAME: string =
334
+ "AddInventoryItemProjectEntityKeyIndex1795200000000";
335
+ const MIGRATION_TIMESTAMP: number = 1795200000000;
336
+
337
+ function registeredNames(): Array<string> {
338
+ return (SchemaMigrations as unknown as Array<{ name: string }>).map(
339
+ (registered: { name: string }): string => {
340
+ return registered.name;
341
+ },
342
+ );
343
+ }
344
+
345
+ function timestampOf(className: string): number | null {
346
+ const match: RegExpMatchArray | null = className.match(/(\d{13})$/);
347
+ return match ? Number(match[1]) : null;
348
+ }
349
+
350
+ test("is registered under the name its class carries", () => {
351
+ expect(
352
+ new AddInventoryItemProjectEntityKeyIndex1795200000000().name,
353
+ ).toBe(MIGRATION_NAME);
354
+ expect(SchemaMigrations).toContain(
355
+ AddInventoryItemProjectEntityKeyIndex1795200000000,
356
+ );
357
+ });
358
+
359
+ /*
360
+ * Placement relative to its neighbours only, so a later migration
361
+ * registered after this one never breaks this suite.
362
+ */
363
+ test("nothing registered before it is newer than it", () => {
364
+ const names: Array<string> = registeredNames();
365
+ const ownIndex: number = names.indexOf(MIGRATION_NAME);
366
+ expect(ownIndex).toBeGreaterThan(0);
367
+ expect(
368
+ names.slice(0, ownIndex).filter((className: string): boolean => {
369
+ const timestamp: number | null = timestampOf(className);
370
+ return timestamp !== null && timestamp >= MIGRATION_TIMESTAMP;
371
+ }),
372
+ ).toEqual([]);
373
+ });
374
+
375
+ test("nothing registered after it is older than it", () => {
376
+ const names: Array<string> = registeredNames();
377
+ const ownIndex: number = names.indexOf(MIGRATION_NAME);
378
+ expect(
379
+ names.slice(ownIndex + 1).filter((className: string): boolean => {
380
+ const timestamp: number | null = timestampOf(className);
381
+ return timestamp === null || timestamp <= MIGRATION_TIMESTAMP;
382
+ }),
383
+ ).toEqual([]);
384
+ });
385
+ });
386
+ });