@oneuptime/common 12.0.3 → 12.0.4
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/CephCluster.ts +14 -0
- package/Models/DatabaseModels/CodeRepository.ts +14 -6
- package/Models/DatabaseModels/DockerHost.ts +14 -0
- package/Models/DatabaseModels/GlobalOidcProject.ts +18 -0
- package/Models/DatabaseModels/GlobalSsoProject.ts +18 -0
- package/Models/DatabaseModels/IoTFleet.ts +14 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +14 -0
- package/Models/DatabaseModels/NetworkDevice.ts +14 -0
- package/Models/DatabaseModels/NetworkInterface.ts +18 -0
- package/Models/DatabaseModels/Project.ts +16 -2
- package/Models/DatabaseModels/ProxmoxCluster.ts +14 -0
- package/Models/DatabaseModels/Service.ts +30 -0
- package/Models/DatabaseModels/StatusPagePrivateUser.ts +30 -0
- package/Models/DatabaseModels/User.ts +30 -0
- package/Server/API/AIAgentDataAPI.ts +31 -0
- package/Server/API/DashboardAPI.ts +419 -12
- package/Server/API/GitHubAPI.ts +119 -283
- package/Server/API/UserAPI.ts +263 -1
- package/Server/EnvironmentConfig.ts +7 -3
- package/Server/Infrastructure/GlobalCache.ts +37 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.ts +21 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.ts +68 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +100 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +214 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.ts +69 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
- package/Server/Services/AnalyticsDatabaseService.ts +72 -1
- package/Server/Services/CodeRepositoryService.ts +105 -2
- package/Server/Services/DatabaseService.ts +185 -2
- package/Server/Services/OpenTelemetryIngestService.ts +230 -26
- package/Server/Services/UserService.ts +184 -0
- package/Server/Utils/AI/Toolbox/CodeTools.ts +45 -4
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +13 -0
- package/Server/Utils/CodeRepository/GitHub/GitHub.ts +157 -4
- package/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.ts +130 -0
- package/Server/Utils/PasswordHash.ts +306 -0
- package/Tests/App/Dashboard/OverviewCustomFields.test.tsx +333 -0
- package/Tests/App/Dashboard/UserCustomFields.test.tsx +392 -0
- package/Tests/Server/API/AIAgentDataRepositoryToken.test.ts +381 -0
- package/Tests/Server/API/DashboardPublicAttributeValuesAPI.test.ts +943 -0
- package/Tests/Server/API/DashboardPublicMetricsAggregateAPI.test.ts +1296 -0
- package/Tests/Server/API/DashboardPublicTemplatePayloads.test.ts +598 -0
- package/Tests/Server/API/GitHubAppInstallationBindingAPI.test.ts +539 -0
- package/Tests/Server/API/Helpers.ts +6 -1
- package/Tests/Server/API/UserProjectsAPI.test.ts +852 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +128 -0
- package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +215 -0
- package/Tests/Server/Services/AddPerUserPasswordSaltMigration.test.ts +178 -0
- package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +582 -0
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +5 -0
- package/Tests/Server/Services/CodeRepositoryInstallationBinding.test.ts +305 -0
- package/Tests/Server/Services/CodeRepositoryResolutionBinding.test.ts +147 -0
- package/Tests/Server/Services/DatabaseServicePerUserPasswordSalt.test.ts +870 -0
- package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +568 -0
- package/Tests/Server/Services/RestoreDroppedUniqueIndexesMigration.test.ts +386 -0
- package/Tests/Server/Services/RestoreServiceLowerNameIndexMigration.test.ts +240 -0
- package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +885 -0
- package/Tests/Server/Services/WidenHashedStringColumnsForScryptMigration.test.ts +220 -0
- package/Tests/Server/Utils/AI/CodeTools.test.ts +47 -1
- package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +10 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +120 -0
- package/Tests/Server/Utils/CodeRepository/GitHubInstallationBinding.test.ts +292 -0
- package/Tests/Server/Utils/GitHubInstallationOwnershipVerification.test.ts +340 -0
- package/Tests/Server/Utils/GitHubWebhookAndTreeCacheIsolation.test.ts +250 -0
- package/Tests/Server/Utils/PasswordHash.test.ts +555 -0
- package/Tests/Types/Database/ColumnLength.test.ts +7 -1
- package/Tests/Types/HashedStringPerUserSalt.test.ts +476 -0
- package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +293 -0
- package/Tests/Types/Monitor/HostAlertTemplates.test.ts +249 -0
- package/Tests/Types/Monitor/IotAlertTemplates.test.ts +320 -0
- package/Tests/Types/Monitor/PodmanAlertTemplates.test.ts +271 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +696 -0
- package/Tests/UI/Utils/UserProjectsModelAPI.test.ts +714 -0
- package/Tests/Utils/TeamMembersByProject.test.ts +655 -0
- package/Types/Database/ColumnLength.ts +9 -1
- package/Types/Database/TableColumn.ts +11 -0
- package/Types/Database/UnsynchronizedIndex.ts +48 -0
- package/Types/HashedString.ts +139 -4
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +85 -11
- package/UI/Components/Navbar/NavBar.tsx +12 -2
- package/UI/Components/Navbar/NavBarMenuModal.tsx +19 -7
- package/UI/Utils/ModelAPI/UserProjectsModelAPI.ts +257 -0
- package/Utils/TeamMembersByProject.ts +237 -0
- package/build/dist/Models/DatabaseModels/CephCluster.js +16 -1
- package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CodeRepository.js +14 -6
- package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DockerHost.js +16 -1
- package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +16 -1
- package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +16 -1
- package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IoTFleet.js +16 -1
- package/build/dist/Models/DatabaseModels/IoTFleet.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +16 -1
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +16 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkInterface.js +16 -1
- package/build/dist/Models/DatabaseModels/NetworkInterface.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +16 -2
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +16 -1
- package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Service.js +28 -1
- package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +32 -0
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
- package/build/dist/Models/DatabaseModels/User.js +32 -0
- package/build/dist/Models/DatabaseModels/User.js.map +1 -1
- package/build/dist/Server/API/AIAgentDataAPI.js +21 -2
- package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +304 -10
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/GitHubAPI.js +94 -167
- package/build/dist/Server/API/GitHubAPI.js.map +1 -1
- package/build/dist/Server/API/UserAPI.js +187 -2
- package/build/dist/Server/API/UserAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +7 -3
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +29 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js +59 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +93 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js +101 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js +66 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +57 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/CodeRepositoryService.js +90 -2
- package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +160 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/OpenTelemetryIngestService.js +155 -25
- package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +165 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +31 -1
- package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +13 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +126 -4
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js +136 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js.map +1 -0
- package/build/dist/Server/Utils/PasswordHash.js +237 -0
- package/build/dist/Server/Utils/PasswordHash.js.map +1 -0
- package/build/dist/Types/Database/ColumnLength.js +9 -1
- package/build/dist/Types/Database/ColumnLength.js.map +1 -1
- package/build/dist/Types/Database/TableColumn.js.map +1 -1
- package/build/dist/Types/Database/UnsynchronizedIndex.js +46 -0
- package/build/dist/Types/Database/UnsynchronizedIndex.js.map +1 -0
- package/build/dist/Types/HashedString.js +95 -5
- package/build/dist/Types/HashedString.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +51 -10
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +3 -1
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +19 -7
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js +156 -0
- package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js.map +1 -0
- package/build/dist/Utils/TeamMembersByProject.js +145 -0
- package/build/dist/Utils/TeamMembersByProject.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import CephCluster from "../../../Models/DatabaseModels/CephCluster";
|
|
2
|
+
import DockerHost from "../../../Models/DatabaseModels/DockerHost";
|
|
3
|
+
import GlobalOIDCProject from "../../../Models/DatabaseModels/GlobalOidcProject";
|
|
4
|
+
import GlobalSSOProject from "../../../Models/DatabaseModels/GlobalSsoProject";
|
|
5
|
+
import IoTFleet from "../../../Models/DatabaseModels/IoTFleet";
|
|
6
|
+
import KubernetesCluster from "../../../Models/DatabaseModels/KubernetesCluster";
|
|
7
|
+
import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
|
|
8
|
+
import NetworkInterface from "../../../Models/DatabaseModels/NetworkInterface";
|
|
9
|
+
import ProxmoxCluster from "../../../Models/DatabaseModels/ProxmoxCluster";
|
|
10
|
+
import { RestoreDroppedUniqueIndexes1786200000000 } from "../../../Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes";
|
|
11
|
+
import SchemaMigrations from "../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
|
|
12
|
+
import { QueryRunner, getMetadataArgsStorage } from "typeorm";
|
|
13
|
+
import type { IndexMetadataArgs } from "typeorm/metadata-args/IndexMetadataArgs";
|
|
14
|
+
import { describe, expect, test } from "@jest/globals";
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* Nine UNIQUE indexes were deleted from production by autogenerated migrations
|
|
18
|
+
* and never restored. They were found by auditing every hand-named index ever
|
|
19
|
+
* created in a migration up() against a database with all registered
|
|
20
|
+
* migrations applied: 308 exist, 302 are absent, and ten of the absent ones
|
|
21
|
+
* carried a UNIQUE/partial/expression definition TypeORM cannot express and so
|
|
22
|
+
* never re-created. (The tenth, the Service LOWER(name) index, is covered by
|
|
23
|
+
* RestoreServiceLowerNameIndexMigration.test.ts.)
|
|
24
|
+
*
|
|
25
|
+
* The cause is structural, so the fix has to be structural too:
|
|
26
|
+
* RdbmsSchemaBuilder.dropOldIndices matches database indexes to entity metadata
|
|
27
|
+
* BY NAME and drops every one it cannot find. An index that lives only in a
|
|
28
|
+
* migration is invisible to entity metadata and gets dropped again on the next
|
|
29
|
+
* `migration:generate`.
|
|
30
|
+
*
|
|
31
|
+
* Three things must therefore all hold, and a regression in ANY one puts the
|
|
32
|
+
* index back on death row:
|
|
33
|
+
*
|
|
34
|
+
* 1. Each entity declares its index BY NAME with `synchronize: false`.
|
|
35
|
+
* 2. The migration creates all nine, partial on "deletedAt" IS NULL, and
|
|
36
|
+
* repairs duplicates first so it cannot fail on real data.
|
|
37
|
+
* 3. The migration is registered — an unregistered migration never runs.
|
|
38
|
+
*
|
|
39
|
+
* Pure metadata/mocks — no Postgres connection anywhere.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
type ModelClass = { new (): unknown };
|
|
43
|
+
|
|
44
|
+
// [entity, index name, table name]
|
|
45
|
+
const PINNED_INDEXES: Array<[ModelClass, string, string]> = [
|
|
46
|
+
[CephCluster, "IDX_ceph_cluster_slug", "CephCluster"],
|
|
47
|
+
[DockerHost, "IDX_docker_host_slug", "DockerHost"],
|
|
48
|
+
[IoTFleet, "IDX_iot_fleet_slug", "IoTFleet"],
|
|
49
|
+
[KubernetesCluster, "IDX_kubernetes_cluster_slug", "KubernetesCluster"],
|
|
50
|
+
[NetworkDevice, "IDX_network_device_slug", "NetworkDevice"],
|
|
51
|
+
[ProxmoxCluster, "IDX_proxmox_cluster_slug", "ProxmoxCluster"],
|
|
52
|
+
[
|
|
53
|
+
GlobalOIDCProject,
|
|
54
|
+
"IDX_GlobalOIDCProject_globalOidcId_projectId",
|
|
55
|
+
"GlobalOIDCProject",
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
GlobalSSOProject,
|
|
59
|
+
"IDX_GlobalSSOProject_globalSsoId_projectId",
|
|
60
|
+
"GlobalSSOProject",
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
NetworkInterface,
|
|
64
|
+
"IDX_network_interface_device_ifindex",
|
|
65
|
+
"NetworkInterface",
|
|
66
|
+
],
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
type CapturedQueriesFunction = (
|
|
70
|
+
run: (queryRunner: QueryRunner) => Promise<void>,
|
|
71
|
+
) => Promise<Array<string>>;
|
|
72
|
+
|
|
73
|
+
const capturedQueries: CapturedQueriesFunction = async (
|
|
74
|
+
run: (queryRunner: QueryRunner) => Promise<void>,
|
|
75
|
+
): Promise<Array<string>> => {
|
|
76
|
+
const queries: Array<string> = [];
|
|
77
|
+
const queryRunner: QueryRunner = {
|
|
78
|
+
query: async (sql: string): Promise<void> => {
|
|
79
|
+
queries.push(sql);
|
|
80
|
+
},
|
|
81
|
+
} as unknown as QueryRunner;
|
|
82
|
+
|
|
83
|
+
await run(queryRunner);
|
|
84
|
+
return queries;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type UpQueriesFunction = () => Promise<Array<string>>;
|
|
88
|
+
|
|
89
|
+
const upQueries: UpQueriesFunction = (): Promise<Array<string>> => {
|
|
90
|
+
return capturedQueries(async (qr: QueryRunner): Promise<void> => {
|
|
91
|
+
await new RestoreDroppedUniqueIndexes1786200000000().up(qr);
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
describe("restored UNIQUE indexes", () => {
|
|
96
|
+
describe("1. the entity declarations that keep them alive", () => {
|
|
97
|
+
test.each(PINNED_INDEXES)(
|
|
98
|
+
"%p declares %s by name",
|
|
99
|
+
(model: ModelClass, indexName: string): void => {
|
|
100
|
+
const found: IndexMetadataArgs | undefined =
|
|
101
|
+
getMetadataArgsStorage().indices.find(
|
|
102
|
+
(index: IndexMetadataArgs): boolean => {
|
|
103
|
+
return index.target === model && index.name === indexName;
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(found).toBeDefined();
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
test.each(PINNED_INDEXES)(
|
|
112
|
+
"%p declares %s as UNIQUE and partial",
|
|
113
|
+
(model: ModelClass, indexName: string): void => {
|
|
114
|
+
/*
|
|
115
|
+
* THE assertion. These are declared NATIVELY, not pinned with
|
|
116
|
+
* synchronize:false, so the schema builder owns them and the drift
|
|
117
|
+
* check actively polices them. For that to work the declaration has to
|
|
118
|
+
* match the database: same name, same uniqueness, same columns.
|
|
119
|
+
* dropOldIndices compares exactly those, and drops on any mismatch.
|
|
120
|
+
*/
|
|
121
|
+
const found: IndexMetadataArgs | undefined =
|
|
122
|
+
getMetadataArgsStorage().indices.find(
|
|
123
|
+
(index: IndexMetadataArgs): boolean => {
|
|
124
|
+
return index.target === model && index.name === indexName;
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(found?.unique).toBe(true);
|
|
129
|
+
expect(found?.where).toBe('"deletedAt" IS NULL');
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
test.each(PINNED_INDEXES)(
|
|
134
|
+
"%p leaves %s under TypeORM's ownership",
|
|
135
|
+
(model: ModelClass, indexName: string): void => {
|
|
136
|
+
/*
|
|
137
|
+
* The opposite of the Service LOWER(name) index, which MUST opt out
|
|
138
|
+
* because @Index() cannot express an expression index. These nine can
|
|
139
|
+
* all be expressed, so opting them out would only hide them from the
|
|
140
|
+
* drift check for no benefit.
|
|
141
|
+
*/
|
|
142
|
+
const found: IndexMetadataArgs | undefined =
|
|
143
|
+
getMetadataArgsStorage().indices.find(
|
|
144
|
+
(index: IndexMetadataArgs): boolean => {
|
|
145
|
+
return index.target === model && index.name === indexName;
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(found?.synchronize).not.toBe(false);
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("2. the migration's SQL contract", () => {
|
|
155
|
+
test("creates all nine indexes and no others", async () => {
|
|
156
|
+
const creates: Array<string> = (await upQueries()).filter(
|
|
157
|
+
(sql: string): boolean => {
|
|
158
|
+
return sql.includes("CREATE UNIQUE INDEX");
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(creates).toHaveLength(PINNED_INDEXES.length);
|
|
163
|
+
|
|
164
|
+
for (const [, indexName] of PINNED_INDEXES) {
|
|
165
|
+
expect(
|
|
166
|
+
creates.some((sql: string): boolean => {
|
|
167
|
+
return sql.includes(`"${indexName}"`);
|
|
168
|
+
}),
|
|
169
|
+
).toBe(true);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("every index is UNIQUE — the lost guarantee is the point", async () => {
|
|
174
|
+
const creates: Array<string> = (await upQueries()).filter(
|
|
175
|
+
(sql: string): boolean => {
|
|
176
|
+
return sql.includes("CREATE UNIQUE INDEX");
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
for (const sql of creates) {
|
|
181
|
+
expect(sql).toContain("CREATE UNIQUE INDEX");
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("every index is partial on deletedAt IS NULL", async () => {
|
|
186
|
+
/*
|
|
187
|
+
* Matches how the app reads (DatabaseService adds deletedAt IS NULL to
|
|
188
|
+
* every non-withDeleted query) and stops a soft-deleted row holding a
|
|
189
|
+
* slug hostage forever.
|
|
190
|
+
*/
|
|
191
|
+
const creates: Array<string> = (await upQueries()).filter(
|
|
192
|
+
(sql: string): boolean => {
|
|
193
|
+
return sql.includes("CREATE UNIQUE INDEX");
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
for (const sql of creates) {
|
|
198
|
+
expect(sql).toContain('WHERE "deletedAt" IS NULL');
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("every index is idempotent", async () => {
|
|
203
|
+
/*
|
|
204
|
+
* An operator may build these by hand ahead of the deploy. IF NOT EXISTS
|
|
205
|
+
* makes that a no-op rather than a failed migration.
|
|
206
|
+
*/
|
|
207
|
+
const creates: Array<string> = (await upQueries()).filter(
|
|
208
|
+
(sql: string): boolean => {
|
|
209
|
+
return sql.includes("CREATE UNIQUE INDEX");
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
for (const sql of creates) {
|
|
214
|
+
expect(sql).toContain("IF NOT EXISTS");
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("no index is created CONCURRENTLY", async () => {
|
|
219
|
+
/*
|
|
220
|
+
* migrationsTransactionMode defaults to "all", and CREATE INDEX
|
|
221
|
+
* CONCURRENTLY cannot run inside a transaction block — it would abort
|
|
222
|
+
* the entire migration run on boot.
|
|
223
|
+
*/
|
|
224
|
+
for (const sql of await upQueries()) {
|
|
225
|
+
expect(sql).not.toContain("CONCURRENTLY");
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("every index is repaired BEFORE it is created", async () => {
|
|
230
|
+
/*
|
|
231
|
+
* A UNIQUE index cannot be built over existing duplicates. If a CREATE
|
|
232
|
+
* ever preceded its repair, the migration would fail on real data and
|
|
233
|
+
* block the deploy — which is the failure mode this ordering prevents.
|
|
234
|
+
*/
|
|
235
|
+
const queries: Array<string> = await upQueries();
|
|
236
|
+
|
|
237
|
+
for (const [, indexName, table] of PINNED_INDEXES) {
|
|
238
|
+
const createAt: number = queries.findIndex((sql: string): boolean => {
|
|
239
|
+
return sql.includes(`"${indexName}"`);
|
|
240
|
+
});
|
|
241
|
+
const repairAt: number = queries.findIndex((sql: string): boolean => {
|
|
242
|
+
return sql.includes(`UPDATE "${table}"`);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(repairAt).toBeGreaterThan(-1);
|
|
246
|
+
expect(createAt).toBeGreaterThan(repairAt);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("repairs are deterministic — oldest row always wins", async () => {
|
|
251
|
+
const repairs: Array<string> = (await upQueries()).filter(
|
|
252
|
+
(sql: string): boolean => {
|
|
253
|
+
return sql.includes("ROW_NUMBER()");
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
expect(repairs).toHaveLength(PINNED_INDEXES.length);
|
|
258
|
+
|
|
259
|
+
for (const sql of repairs) {
|
|
260
|
+
expect(sql).toContain('ORDER BY "createdAt" ASC, _id ASC');
|
|
261
|
+
// Only the losers are touched.
|
|
262
|
+
expect(sql).toContain("r.rn > 1");
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("slug repair RENAMES and never deletes", async () => {
|
|
267
|
+
/*
|
|
268
|
+
* A slug collision must not cost the user a row. The losing row keeps
|
|
269
|
+
* all its data and gets a discriminator appended.
|
|
270
|
+
*/
|
|
271
|
+
const slugRepairs: Array<string> = (await upQueries()).filter(
|
|
272
|
+
(sql: string): boolean => {
|
|
273
|
+
return sql.includes('SET "slug"');
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
expect(slugRepairs).toHaveLength(6);
|
|
278
|
+
|
|
279
|
+
for (const sql of slugRepairs) {
|
|
280
|
+
expect(sql).toContain("LEFT(t._id::text, 8)");
|
|
281
|
+
expect(sql).not.toContain("DELETE");
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("duplicate mapping rows are SOFT-deleted, never hard-deleted", async () => {
|
|
286
|
+
/*
|
|
287
|
+
* Soft-deleting takes the row out of a partial index without destroying
|
|
288
|
+
* it and without touching any foreign key, so no cascade can fire.
|
|
289
|
+
*/
|
|
290
|
+
const softDeletes: Array<string> = (await upQueries()).filter(
|
|
291
|
+
(sql: string): boolean => {
|
|
292
|
+
return sql.includes('SET "deletedAt" = CURRENT_TIMESTAMP');
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
expect(softDeletes).toHaveLength(3);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("the migration never hard-deletes anything", async () => {
|
|
300
|
+
for (const sql of await upQueries()) {
|
|
301
|
+
expect(sql).not.toContain("DELETE FROM");
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("repairs only consider live rows", async () => {
|
|
306
|
+
/*
|
|
307
|
+
* The index is partial, so a soft-deleted duplicate is already outside
|
|
308
|
+
* it and must not be "repaired".
|
|
309
|
+
*/
|
|
310
|
+
const repairs: Array<string> = (await upQueries()).filter(
|
|
311
|
+
(sql: string): boolean => {
|
|
312
|
+
return sql.includes("ROW_NUMBER()");
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
for (const sql of repairs) {
|
|
317
|
+
expect(sql).toContain('WHERE "deletedAt" IS NULL');
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("down() drops exactly the nine indexes up() created", async () => {
|
|
322
|
+
const queries: Array<string> = await capturedQueries(
|
|
323
|
+
async (qr: QueryRunner): Promise<void> => {
|
|
324
|
+
await new RestoreDroppedUniqueIndexes1786200000000().down(qr);
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
expect(queries).toHaveLength(PINNED_INDEXES.length);
|
|
329
|
+
|
|
330
|
+
for (const [, indexName] of PINNED_INDEXES) {
|
|
331
|
+
expect(
|
|
332
|
+
queries.some((sql: string): boolean => {
|
|
333
|
+
return sql.includes("DROP INDEX") && sql.includes(indexName);
|
|
334
|
+
}),
|
|
335
|
+
).toBe(true);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("down() does not attempt to un-repair data", async () => {
|
|
340
|
+
/*
|
|
341
|
+
* A renamed slug and a soft-deleted duplicate are indistinguishable
|
|
342
|
+
* afterwards from ones the application produced. Guessing would corrupt
|
|
343
|
+
* live data, so down() only reverses the schema.
|
|
344
|
+
*/
|
|
345
|
+
const queries: Array<string> = await capturedQueries(
|
|
346
|
+
async (qr: QueryRunner): Promise<void> => {
|
|
347
|
+
await new RestoreDroppedUniqueIndexes1786200000000().down(qr);
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
for (const sql of queries) {
|
|
352
|
+
expect(sql).not.toContain("UPDATE");
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe("3. registration", () => {
|
|
358
|
+
test("the migration is registered so it actually runs on boot", () => {
|
|
359
|
+
expect(SchemaMigrations).toContain(
|
|
360
|
+
RestoreDroppedUniqueIndexes1786200000000,
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("it runs after every migration that dropped one of these indexes", () => {
|
|
365
|
+
/*
|
|
366
|
+
* Restoring before the drop would leave the database with no index
|
|
367
|
+
* again. 1783937343400 is the migration that removed the NetworkDevice
|
|
368
|
+
* and NetworkInterface indexes.
|
|
369
|
+
*/
|
|
370
|
+
const names: Array<string> = SchemaMigrations.map(
|
|
371
|
+
(migration: unknown): string => {
|
|
372
|
+
return (migration as { name: string }).name;
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const restoreAt: number = names.indexOf(
|
|
377
|
+
"RestoreDroppedUniqueIndexes1786200000000",
|
|
378
|
+
);
|
|
379
|
+
const dropperAt: number = names.indexOf("MigrationName1783937343400");
|
|
380
|
+
|
|
381
|
+
expect(restoreAt).toBeGreaterThan(-1);
|
|
382
|
+
expect(dropperAt).toBeGreaterThan(-1);
|
|
383
|
+
expect(restoreAt).toBeGreaterThan(dropperAt);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import Service from "../../../Models/DatabaseModels/Service";
|
|
2
|
+
import { RestoreServiceLowerNameIndex1786100000000 } from "../../../Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex";
|
|
3
|
+
import SchemaMigrations from "../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
|
|
4
|
+
import { QueryRunner, getMetadataArgsStorage } from "typeorm";
|
|
5
|
+
import type { IndexMetadataArgs } from "typeorm/metadata-args/IndexMetadataArgs";
|
|
6
|
+
import { describe, expect, test } from "@jest/globals";
|
|
7
|
+
|
|
8
|
+
const INDEX_NAME: string = "IDX_SERVICE_PROJECT_LOWER_NAME";
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* The telemetry ingest path resolves `service.name` case-insensitively, so the
|
|
12
|
+
* predicate is `LOWER("Service"."name") = $2`. Only an EXPRESSION index can
|
|
13
|
+
* serve that; an index over the raw `name` column cannot, and the lookup
|
|
14
|
+
* degrades to O(services in project) heap fetches on the hottest read in the
|
|
15
|
+
* product. Production ran at 100% CPU on it.
|
|
16
|
+
*
|
|
17
|
+
* The index this migration creates has been deleted from production once
|
|
18
|
+
* already. It was created as IDX_SERVICE_PROJECT_NAME_UNIQUE by migration
|
|
19
|
+
* 1774559064921 and dropped by autogenerated migration 1775735059360, whose
|
|
20
|
+
* down() "restores" it WITHOUT the LOWER() — so even the round-trip loses it.
|
|
21
|
+
* The cause is structural: TypeORM's schema builder matches database indexes to
|
|
22
|
+
* entity metadata by NAME and drops every one it cannot find, and an expression
|
|
23
|
+
* index that exists only in a migration is invisible to entity metadata.
|
|
24
|
+
*
|
|
25
|
+
* Three things must therefore all hold, and a regression in ANY one silently
|
|
26
|
+
* reintroduces the incident:
|
|
27
|
+
*
|
|
28
|
+
* 1. The entity declares the index BY NAME with `synchronize: false`. This is
|
|
29
|
+
* what stops the next `migration:generate` emitting a DROP for it. Without
|
|
30
|
+
* it the migration below gets undone and nobody notices until CPU spikes.
|
|
31
|
+
* 2. The migration's SQL contract: up() creates an expression index on
|
|
32
|
+
* LOWER("name"), partial on "deletedAt" IS NULL, and down() drops it.
|
|
33
|
+
* 3. The migration is registered in SchemaMigrations/Index.ts. An
|
|
34
|
+
* unregistered migration is dead code that never runs on boot.
|
|
35
|
+
*
|
|
36
|
+
* Pure metadata/mocks — no Postgres connection anywhere.
|
|
37
|
+
*/
|
|
38
|
+
describe("Service LOWER(name) expression index", () => {
|
|
39
|
+
describe("1. the entity declaration that keeps it alive", () => {
|
|
40
|
+
type FindIndexFunction = () => IndexMetadataArgs | undefined;
|
|
41
|
+
|
|
42
|
+
const findIndex: FindIndexFunction = (): IndexMetadataArgs | undefined => {
|
|
43
|
+
return getMetadataArgsStorage().indices.find(
|
|
44
|
+
(index: IndexMetadataArgs): boolean => {
|
|
45
|
+
return index.target === Service && index.name === INDEX_NAME;
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
test("Service declares the index by name", () => {
|
|
51
|
+
/*
|
|
52
|
+
* Name matching is the whole mechanism — RdbmsSchemaBuilder looks up
|
|
53
|
+
* `metadata.indices.find((i) => i.name === tableIndex.name)` and drops
|
|
54
|
+
* anything unmatched. The name here must equal the one in the migration.
|
|
55
|
+
*/
|
|
56
|
+
expect(findIndex()).toBeDefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("the declaration opts OUT of schema synchronization", () => {
|
|
60
|
+
/*
|
|
61
|
+
* THE assertion. `synchronize: false` makes the builder skip the index in
|
|
62
|
+
* dropOldIndices AND in createNewIndices. Flip this to true (or drop the
|
|
63
|
+
* option) and TypeORM owns the index again — at which point it cannot
|
|
64
|
+
* express LOWER() and will replace it with a raw-column index, which is
|
|
65
|
+
* exactly the regression that caused the incident.
|
|
66
|
+
*/
|
|
67
|
+
expect(findIndex()?.synchronize).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("the pre-existing raw-name unique index is left untouched", () => {
|
|
71
|
+
/*
|
|
72
|
+
* The expression index is additive. The raw (projectId, name) unique
|
|
73
|
+
* index still enforces the create-race guard that the service layer's
|
|
74
|
+
* catch-and-refetch path depends on.
|
|
75
|
+
*/
|
|
76
|
+
const rawUnique: IndexMetadataArgs | undefined = getMetadataArgsStorage()
|
|
77
|
+
.indices.filter((index: IndexMetadataArgs): boolean => {
|
|
78
|
+
return index.target === Service;
|
|
79
|
+
})
|
|
80
|
+
.find((index: IndexMetadataArgs): boolean => {
|
|
81
|
+
return index.unique === true && index.name === undefined;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(rawUnique).toBeDefined();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("2. the migration's SQL contract", () => {
|
|
89
|
+
type CapturedQueriesFunction = (
|
|
90
|
+
run: (queryRunner: QueryRunner) => Promise<void>,
|
|
91
|
+
) => Promise<Array<string>>;
|
|
92
|
+
|
|
93
|
+
const capturedQueries: CapturedQueriesFunction = async (
|
|
94
|
+
run: (queryRunner: QueryRunner) => Promise<void>,
|
|
95
|
+
): Promise<Array<string>> => {
|
|
96
|
+
const queries: Array<string> = [];
|
|
97
|
+
const queryRunner: QueryRunner = {
|
|
98
|
+
query: async (sql: string): Promise<void> => {
|
|
99
|
+
queries.push(sql);
|
|
100
|
+
},
|
|
101
|
+
} as unknown as QueryRunner;
|
|
102
|
+
|
|
103
|
+
await run(queryRunner);
|
|
104
|
+
return queries;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
test("up() indexes the LOWER() EXPRESSION, not the raw column", () => {
|
|
108
|
+
/*
|
|
109
|
+
* An index on ("projectId", "name") is not a substitute and would leave
|
|
110
|
+
* the predicate non-sargable. This is the difference between O(log n) and
|
|
111
|
+
* O(services in project) per ingest resolution.
|
|
112
|
+
*/
|
|
113
|
+
return capturedQueries(async (qr: QueryRunner): Promise<void> => {
|
|
114
|
+
await new RestoreServiceLowerNameIndex1786100000000().up(qr);
|
|
115
|
+
}).then((queries: Array<string>): void => {
|
|
116
|
+
expect(queries).toHaveLength(1);
|
|
117
|
+
expect(queries[0]).toContain('LOWER("name")');
|
|
118
|
+
expect(queries[0]).toContain('ON "Service"');
|
|
119
|
+
expect(queries[0]).toContain(INDEX_NAME);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("up() matches the query's soft-delete predicate", async () => {
|
|
124
|
+
/*
|
|
125
|
+
* The lookup always carries `deletedAt IS NULL` (DatabaseService adds it
|
|
126
|
+
* for every non-withDeleted read). A partial index on the same predicate
|
|
127
|
+
* stays usable and stays small.
|
|
128
|
+
*/
|
|
129
|
+
const queries: Array<string> = await capturedQueries(
|
|
130
|
+
async (qr: QueryRunner): Promise<void> => {
|
|
131
|
+
await new RestoreServiceLowerNameIndex1786100000000().up(qr);
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(queries[0]).toContain('WHERE "deletedAt" IS NULL');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("up() carries createdAt so the ORDER BY needs no Sort node", async () => {
|
|
139
|
+
const queries: Array<string> = await capturedQueries(
|
|
140
|
+
async (qr: QueryRunner): Promise<void> => {
|
|
141
|
+
await new RestoreServiceLowerNameIndex1786100000000().up(qr);
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(queries[0]).toContain('"createdAt"');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("up() is idempotent so a hand-built index does not break the deploy", async () => {
|
|
149
|
+
/*
|
|
150
|
+
* The incident mitigation is to build this CONCURRENTLY on the live
|
|
151
|
+
* database under the same name before deploying. IF NOT EXISTS makes the
|
|
152
|
+
* migration a no-op in that case instead of a failed deploy.
|
|
153
|
+
*/
|
|
154
|
+
const queries: Array<string> = await capturedQueries(
|
|
155
|
+
async (qr: QueryRunner): Promise<void> => {
|
|
156
|
+
await new RestoreServiceLowerNameIndex1786100000000().up(qr);
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(queries[0]).toContain("IF NOT EXISTS");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("up() is NOT concurrent — migrations run inside one transaction", async () => {
|
|
164
|
+
/*
|
|
165
|
+
* TypeORM defaults migrationsTransactionMode to "all", and CREATE INDEX
|
|
166
|
+
* CONCURRENTLY cannot run inside a transaction block. Adding CONCURRENTLY
|
|
167
|
+
* here would abort the entire migration run on boot.
|
|
168
|
+
*/
|
|
169
|
+
const queries: Array<string> = await capturedQueries(
|
|
170
|
+
async (qr: QueryRunner): Promise<void> => {
|
|
171
|
+
await new RestoreServiceLowerNameIndex1786100000000().up(qr);
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(queries[0]).not.toContain("CONCURRENTLY");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("up() does NOT recreate the index as UNIQUE", async () => {
|
|
179
|
+
/*
|
|
180
|
+
* The dropped original was unique. Recreating it that way fails on any
|
|
181
|
+
* project already holding two services whose names differ only by case —
|
|
182
|
+
* which the surviving raw-name unique index permits. Restoring
|
|
183
|
+
* case-insensitive uniqueness needs a dedupe pass and its own migration.
|
|
184
|
+
*/
|
|
185
|
+
const queries: Array<string> = await capturedQueries(
|
|
186
|
+
async (qr: QueryRunner): Promise<void> => {
|
|
187
|
+
await new RestoreServiceLowerNameIndex1786100000000().up(qr);
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(queries[0]).not.toContain("UNIQUE");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("down() drops exactly what up() created", async () => {
|
|
195
|
+
const queries: Array<string> = await capturedQueries(
|
|
196
|
+
async (qr: QueryRunner): Promise<void> => {
|
|
197
|
+
await new RestoreServiceLowerNameIndex1786100000000().down(qr);
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(queries).toHaveLength(1);
|
|
202
|
+
expect(queries[0]).toContain("DROP INDEX");
|
|
203
|
+
expect(queries[0]).toContain(INDEX_NAME);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("3. registration", () => {
|
|
208
|
+
test("the migration is registered so it actually runs on boot", () => {
|
|
209
|
+
/*
|
|
210
|
+
* CI enforces this separately, but an unregistered migration is dead
|
|
211
|
+
* code — the index would never be created in any environment.
|
|
212
|
+
*/
|
|
213
|
+
expect(SchemaMigrations).toContain(
|
|
214
|
+
RestoreServiceLowerNameIndex1786100000000,
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("it is registered AFTER the migration that dropped the original", () => {
|
|
219
|
+
/*
|
|
220
|
+
* Ordering is the correctness condition: 1775735059360 drops
|
|
221
|
+
* IDX_SERVICE_PROJECT_NAME_UNIQUE. Running this restore before that drop
|
|
222
|
+
* would leave a database with no usable index again.
|
|
223
|
+
*/
|
|
224
|
+
const names: Array<string> = SchemaMigrations.map(
|
|
225
|
+
(migration: unknown): string => {
|
|
226
|
+
return (migration as { name: string }).name;
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const restoreAt: number = names.indexOf(
|
|
231
|
+
"RestoreServiceLowerNameIndex1786100000000",
|
|
232
|
+
);
|
|
233
|
+
const dropperAt: number = names.indexOf("MigrationName1775735059360");
|
|
234
|
+
|
|
235
|
+
expect(restoreAt).toBeGreaterThan(-1);
|
|
236
|
+
expect(dropperAt).toBeGreaterThan(-1);
|
|
237
|
+
expect(restoreAt).toBeGreaterThan(dropperAt);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|