@oneuptime/common 14.0.0 → 14.0.1
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/Index.ts +4 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
- package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/DatabaseService.ts +10 -0
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/MonitorService.ts +3 -2
- package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
- package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
- package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
- package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
- package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
- package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
- package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
- package/Server/Utils/Monitor/AffectedResourceList.ts +31 -73
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +181 -136
- package/Server/Utils/Monitor/RootCauseList.ts +139 -0
- package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
- package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
- package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
- package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
- package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
- package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
- package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
- package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
- package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
- package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
- package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
- package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
- package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
- package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
- package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
- package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
- package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
- package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
- package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
- package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
- package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
- package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
- package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
- package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
- package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
- package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
- package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
- package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
- package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
- package/Tests/Server/Types/Markdown.test.ts +2 -1
- package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
- package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
- package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +405 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
- package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
- package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
- package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
- package/Tests/Types/API/Route.test.ts +56 -0
- package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
- package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
- package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
- package/Tests/UI/Components/Icon.test.tsx +26 -0
- package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
- package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
- package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
- package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
- package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
- package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
- package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
- package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
- package/Types/API/Route.ts +19 -2
- package/Types/API/URL.ts +26 -2
- package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
- package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
- package/Types/Permission.ts +90 -0
- package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
- package/Types/Rules/RuleRun.ts +4 -0
- package/UI/Components/Feed/FeedOptions.ts +427 -0
- package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
- package/UI/Components/Feed/useFeedItems.ts +28 -9
- package/UI/Components/Feed/useFeedOptions.ts +225 -0
- package/UI/Components/Icon/Icon.tsx +1 -1
- package/Utils/LabelRuleImportExport.ts +27 -9
- package/Utils/Monitor/MetricValueFormatter.ts +2 -2
- package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
- package/Utils/Slo/SloDuration.ts +22 -6
- package/Utils/Slo/SloHealth.ts +8 -7
- package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
- package/Utils/Slo/SloOverviewText.ts +33 -0
- package/Utils/Slo/SloProjection.ts +4 -3
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.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/DatabaseService.js +7 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +3 -2
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
- package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +22 -51
- package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +139 -109
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
- package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
- package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
- package/build/dist/Types/API/Route.js +18 -2
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/API/URL.js +23 -2
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Permission.js +84 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
- package/build/dist/Types/Rules/RuleRun.js +4 -0
- package/build/dist/Types/Rules/RuleRun.js.map +1 -1
- package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
- package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
- package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
- package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
- package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
- package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
- package/build/dist/UI/Components/Icon/Icon.js +1 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/Utils/LabelRuleImportExport.js +22 -6
- package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
- package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
- package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
- package/build/dist/Utils/Slo/SloDuration.js +13 -5
- package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
- package/build/dist/Utils/Slo/SloHealth.js +8 -7
- package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
- package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
- package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
- package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
- package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
- package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { AddSloLabelAndOwnerRules1794100000000 } from "../../../../Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules";
|
|
2
|
+
import SchemaMigrations from "../../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
|
|
3
|
+
import ServiceLevelObjectiveLabelRule from "../../../../Models/DatabaseModels/ServiceLevelObjectiveLabelRule";
|
|
4
|
+
import ServiceLevelObjectiveOwnerRule from "../../../../Models/DatabaseModels/ServiceLevelObjectiveOwnerRule";
|
|
5
|
+
import { describe, expect, test } from "@jest/globals";
|
|
6
|
+
import {
|
|
7
|
+
DefaultNamingStrategy,
|
|
8
|
+
QueryRunner,
|
|
9
|
+
getMetadataArgsStorage,
|
|
10
|
+
} from "typeorm";
|
|
11
|
+
import type { ColumnMetadataArgs } from "typeorm/metadata-args/ColumnMetadataArgs";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* The schema half of SLO label and owner rules.
|
|
15
|
+
*
|
|
16
|
+
* The migration was generated against the models, so its names are TypeORM's
|
|
17
|
+
* by construction. What this pins is that it STAYS the migration for these
|
|
18
|
+
* models: every table and join table is created with every column the models
|
|
19
|
+
* persist (criteria included, which 1792500000000 never added to these new
|
|
20
|
+
* tables), every constraint carries TypeORM's own name - a hand edit that
|
|
21
|
+
* renamed one would pass review and then fail the Schema Drift job - rules go
|
|
22
|
+
* with their project and a label, user or team leaves every rule that named
|
|
23
|
+
* it, nothing unrelated rode along, and down() undoes up() in reverse.
|
|
24
|
+
*
|
|
25
|
+
* Fake QueryRunner only. Applying it to Postgres 15, re-generating to "No
|
|
26
|
+
* changes in database schema were found", reverting and applying it again is
|
|
27
|
+
* how it was verified when written.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const namingStrategy: DefaultNamingStrategy = new DefaultNamingStrategy();
|
|
31
|
+
|
|
32
|
+
type RecordQueriesFunction = (
|
|
33
|
+
direction: "up" | "down",
|
|
34
|
+
) => Promise<Array<string>>;
|
|
35
|
+
|
|
36
|
+
const recordQueries: RecordQueriesFunction = async (
|
|
37
|
+
direction: "up" | "down",
|
|
38
|
+
): Promise<Array<string>> => {
|
|
39
|
+
const statements: Array<string> = [];
|
|
40
|
+
|
|
41
|
+
const queryRunner: QueryRunner = {
|
|
42
|
+
query: async (statement: string): Promise<void> => {
|
|
43
|
+
statements.push(statement);
|
|
44
|
+
},
|
|
45
|
+
} as unknown as QueryRunner;
|
|
46
|
+
|
|
47
|
+
await new AddSloLabelAndOwnerRules1794100000000()[direction](queryRunner);
|
|
48
|
+
|
|
49
|
+
return statements;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Columns a model persists, inherited ones (_id, version, criteria) included.
|
|
53
|
+
function persistedColumns(modelType: unknown): Array<string> {
|
|
54
|
+
const names: Array<string> = [];
|
|
55
|
+
let current: unknown = modelType;
|
|
56
|
+
|
|
57
|
+
while (typeof current === "function" && current !== Function.prototype) {
|
|
58
|
+
for (const column of getMetadataArgsStorage().columns) {
|
|
59
|
+
if (column.target === current) {
|
|
60
|
+
const args: ColumnMetadataArgs = column;
|
|
61
|
+
names.push(args.options.name || args.propertyName);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
current = Object.getPrototypeOf(current);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return names;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createTableStatement(
|
|
72
|
+
statements: Array<string>,
|
|
73
|
+
tableName: string,
|
|
74
|
+
): string {
|
|
75
|
+
const matches: Array<string> = statements.filter(
|
|
76
|
+
(statement: string): boolean => {
|
|
77
|
+
return statement.startsWith(`CREATE TABLE "${tableName}" (`);
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect({ tableName, found: matches.length }).toEqual({
|
|
82
|
+
tableName,
|
|
83
|
+
found: 1,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return matches[0]!;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function foreignKeyStatement(
|
|
90
|
+
statements: Array<string>,
|
|
91
|
+
tableName: string,
|
|
92
|
+
columnName: string,
|
|
93
|
+
): string {
|
|
94
|
+
const name: string = namingStrategy.foreignKeyName(tableName, [columnName]);
|
|
95
|
+
const matches: Array<string> = statements.filter(
|
|
96
|
+
(statement: string): boolean => {
|
|
97
|
+
return statement.startsWith(
|
|
98
|
+
`ALTER TABLE "${tableName}" ADD CONSTRAINT "${name}" FOREIGN KEY ("${columnName}")`,
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect({ tableName, columnName, found: matches.length }).toEqual({
|
|
104
|
+
tableName,
|
|
105
|
+
columnName,
|
|
106
|
+
found: 1,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return matches[0]!;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const RULE_TABLES: Array<[string, unknown]> = [
|
|
113
|
+
["ServiceLevelObjectiveLabelRule", ServiceLevelObjectiveLabelRule],
|
|
114
|
+
["ServiceLevelObjectiveOwnerRule", ServiceLevelObjectiveOwnerRule],
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
interface JoinTableSpec {
|
|
118
|
+
tableName: string;
|
|
119
|
+
ruleColumn: string;
|
|
120
|
+
ruleTable: string;
|
|
121
|
+
relatedColumn: string;
|
|
122
|
+
relatedTable: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const JOIN_TABLES: Array<JoinTableSpec> = [
|
|
126
|
+
{
|
|
127
|
+
tableName: "ServiceLevelObjectiveLabelRuleServiceLevelObjectiveLabel",
|
|
128
|
+
ruleColumn: "serviceLevelObjectiveLabelRuleId",
|
|
129
|
+
ruleTable: "ServiceLevelObjectiveLabelRule",
|
|
130
|
+
relatedColumn: "labelId",
|
|
131
|
+
relatedTable: "Label",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
tableName: "ServiceLevelObjectiveLabelRuleLabelToAdd",
|
|
135
|
+
ruleColumn: "serviceLevelObjectiveLabelRuleId",
|
|
136
|
+
ruleTable: "ServiceLevelObjectiveLabelRule",
|
|
137
|
+
relatedColumn: "labelId",
|
|
138
|
+
relatedTable: "Label",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
tableName: "ServiceLevelObjectiveOwnerRuleServiceLevelObjectiveLabel",
|
|
142
|
+
ruleColumn: "serviceLevelObjectiveOwnerRuleId",
|
|
143
|
+
ruleTable: "ServiceLevelObjectiveOwnerRule",
|
|
144
|
+
relatedColumn: "labelId",
|
|
145
|
+
relatedTable: "Label",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
tableName: "ServiceLevelObjectiveOwnerRuleOwnerUser",
|
|
149
|
+
ruleColumn: "serviceLevelObjectiveOwnerRuleId",
|
|
150
|
+
ruleTable: "ServiceLevelObjectiveOwnerRule",
|
|
151
|
+
relatedColumn: "userId",
|
|
152
|
+
relatedTable: "User",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
tableName: "ServiceLevelObjectiveOwnerRuleOwnerTeam",
|
|
156
|
+
ruleColumn: "serviceLevelObjectiveOwnerRuleId",
|
|
157
|
+
ruleTable: "ServiceLevelObjectiveOwnerRule",
|
|
158
|
+
relatedColumn: "teamId",
|
|
159
|
+
relatedTable: "Team",
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
const ALL_TABLES: Array<string> = [
|
|
164
|
+
...RULE_TABLES.map(([tableName]: [string, unknown]): string => {
|
|
165
|
+
return tableName;
|
|
166
|
+
}),
|
|
167
|
+
...JOIN_TABLES.map((spec: JoinTableSpec): string => {
|
|
168
|
+
return spec.tableName;
|
|
169
|
+
}),
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
describe("AddSloLabelAndOwnerRules1794100000000", () => {
|
|
173
|
+
test("is registered under the name its class carries", () => {
|
|
174
|
+
const migration: AddSloLabelAndOwnerRules1794100000000 =
|
|
175
|
+
new AddSloLabelAndOwnerRules1794100000000();
|
|
176
|
+
|
|
177
|
+
expect(migration.name).toBe("AddSloLabelAndOwnerRules1794100000000");
|
|
178
|
+
expect(SchemaMigrations).toContain(AddSloLabelAndOwnerRules1794100000000);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
* Not "is registered last": the next migration to land falsifies that
|
|
183
|
+
* without going anywhere near these tables. What this migration must not do
|
|
184
|
+
* is jump the queue of those registered before it; the registry-wide guard
|
|
185
|
+
* on the newest entry lives in SchemaMigrationsOrdering.
|
|
186
|
+
*/
|
|
187
|
+
test("its timestamp keeps it behind every migration registered before it", () => {
|
|
188
|
+
const timestampOf: (className: string) => number | null = (
|
|
189
|
+
className: string,
|
|
190
|
+
): number | null => {
|
|
191
|
+
const match: RegExpMatchArray | null = className.match(/(\d{13})$/);
|
|
192
|
+
return match ? Number(match[1]) : null;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const names: Array<string> = (
|
|
196
|
+
SchemaMigrations as unknown as Array<{ name: string }>
|
|
197
|
+
).map((registered: { name: string }): string => {
|
|
198
|
+
return registered.name;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const ownIndex: number = names.indexOf(
|
|
202
|
+
"AddSloLabelAndOwnerRules1794100000000",
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// indexOf -1 would make the slice below empty and this test vacuous.
|
|
206
|
+
expect(ownIndex).toBeGreaterThan(0);
|
|
207
|
+
|
|
208
|
+
const notBehind: Array<string> = names
|
|
209
|
+
.slice(0, ownIndex)
|
|
210
|
+
.filter((className: string): boolean => {
|
|
211
|
+
const timestamp: number | null = timestampOf(className);
|
|
212
|
+
return timestamp !== null && timestamp >= 1794100000000;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(notBehind).toEqual([]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("touches only the seven new tables", async () => {
|
|
219
|
+
const statements: Array<string> = await recordQueries("up");
|
|
220
|
+
|
|
221
|
+
for (const statement of statements) {
|
|
222
|
+
const table: RegExpMatchArray | null = statement.match(
|
|
223
|
+
/^(?:CREATE TABLE|ALTER TABLE|CREATE INDEX "[^"]+" ON) "([^"]+)"/,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect({ statement, table: table?.[1] }).toEqual({
|
|
227
|
+
statement,
|
|
228
|
+
table: expect.stringMatching(new RegExp(`^(${ALL_TABLES.join("|")})$`)),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
expect(
|
|
233
|
+
statements.filter((statement: string): boolean => {
|
|
234
|
+
return statement.startsWith("CREATE TABLE");
|
|
235
|
+
}),
|
|
236
|
+
).toHaveLength(ALL_TABLES.length);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test.each(RULE_TABLES)(
|
|
240
|
+
"creates %s with every column the model persists",
|
|
241
|
+
async (tableName: string, modelType: unknown) => {
|
|
242
|
+
const statement: string = createTableStatement(
|
|
243
|
+
await recordQueries("up"),
|
|
244
|
+
tableName,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
for (const column of persistedColumns(modelType)) {
|
|
248
|
+
expect({
|
|
249
|
+
tableName,
|
|
250
|
+
column,
|
|
251
|
+
created: statement.includes(`"${column}"`),
|
|
252
|
+
}).toEqual({ tableName, column, created: true });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
expect(statement).toContain('"criteria" jsonb');
|
|
256
|
+
expect(statement).toContain('"isEnabled" boolean NOT NULL DEFAULT true');
|
|
257
|
+
expect(statement).toContain('"projectId" uuid NOT NULL');
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
test("defaults an owner rule to notifying the owners it adds", async () => {
|
|
262
|
+
expect(
|
|
263
|
+
createTableStatement(
|
|
264
|
+
await recordQueries("up"),
|
|
265
|
+
"ServiceLevelObjectiveOwnerRule",
|
|
266
|
+
),
|
|
267
|
+
).toContain('"notifyOwners" boolean NOT NULL DEFAULT true');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test.each(RULE_TABLES)(
|
|
271
|
+
"deletes %s with its project, and keeps it when its author is deleted",
|
|
272
|
+
async (tableName: string) => {
|
|
273
|
+
const statements: Array<string> = await recordQueries("up");
|
|
274
|
+
|
|
275
|
+
expect(foreignKeyStatement(statements, tableName, "projectId")).toContain(
|
|
276
|
+
'REFERENCES "Project"("_id") ON DELETE CASCADE',
|
|
277
|
+
);
|
|
278
|
+
expect(
|
|
279
|
+
foreignKeyStatement(statements, tableName, "createdByUserId"),
|
|
280
|
+
).toContain('REFERENCES "User"("_id") ON DELETE SET NULL');
|
|
281
|
+
expect(
|
|
282
|
+
foreignKeyStatement(statements, tableName, "deletedByUserId"),
|
|
283
|
+
).toContain('REFERENCES "User"("_id") ON DELETE SET NULL');
|
|
284
|
+
},
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
test.each(JOIN_TABLES)(
|
|
288
|
+
"creates $tableName, keyed by the rule and the $relatedTable",
|
|
289
|
+
async (spec: JoinTableSpec) => {
|
|
290
|
+
const statements: Array<string> = await recordQueries("up");
|
|
291
|
+
const statement: string = createTableStatement(
|
|
292
|
+
statements,
|
|
293
|
+
spec.tableName,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
expect(statement).toContain(
|
|
297
|
+
`PRIMARY KEY ("${spec.ruleColumn}", "${spec.relatedColumn}")`,
|
|
298
|
+
);
|
|
299
|
+
expect(spec.tableName.length).toBeLessThanOrEqual(63);
|
|
300
|
+
|
|
301
|
+
// Both sides clean up after themselves.
|
|
302
|
+
expect(
|
|
303
|
+
foreignKeyStatement(statements, spec.tableName, spec.ruleColumn),
|
|
304
|
+
).toContain(`REFERENCES "${spec.ruleTable}"("_id") ON DELETE CASCADE`);
|
|
305
|
+
expect(
|
|
306
|
+
foreignKeyStatement(statements, spec.tableName, spec.relatedColumn),
|
|
307
|
+
).toContain(`REFERENCES "${spec.relatedTable}"("_id") ON DELETE CASCADE`);
|
|
308
|
+
|
|
309
|
+
for (const column of [spec.ruleColumn, spec.relatedColumn]) {
|
|
310
|
+
const indexName: string = namingStrategy.indexName(spec.tableName, [
|
|
311
|
+
column,
|
|
312
|
+
]);
|
|
313
|
+
|
|
314
|
+
expect(statements).toContain(
|
|
315
|
+
`CREATE INDEX "${indexName}" ON "${spec.tableName}" ("${column}") `,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
test("indexes the columns the rule engines and tables filter on", async () => {
|
|
322
|
+
const statements: Array<string> = await recordQueries("up");
|
|
323
|
+
|
|
324
|
+
for (const [tableName] of RULE_TABLES) {
|
|
325
|
+
for (const column of ["projectId", "name", "isEnabled"]) {
|
|
326
|
+
const indexName: string = namingStrategy.indexName(tableName, [column]);
|
|
327
|
+
|
|
328
|
+
expect(statements).toContain(
|
|
329
|
+
`CREATE INDEX "${indexName}" ON "${tableName}" ("${column}") `,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("down() drops every table up() created, join tables first", async () => {
|
|
336
|
+
const down: Array<string> = await recordQueries("down");
|
|
337
|
+
const dropped: Array<string> = down
|
|
338
|
+
.map((statement: string): string | null => {
|
|
339
|
+
const match: RegExpMatchArray | null = statement.match(
|
|
340
|
+
/^DROP TABLE "([^"]+)"$/,
|
|
341
|
+
);
|
|
342
|
+
return match ? match[1]! : null;
|
|
343
|
+
})
|
|
344
|
+
.filter((tableName: string | null): tableName is string => {
|
|
345
|
+
return tableName !== null;
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect([...dropped].sort()).toEqual([...ALL_TABLES].sort());
|
|
349
|
+
|
|
350
|
+
for (const spec of JOIN_TABLES) {
|
|
351
|
+
expect(dropped.indexOf(spec.tableName)).toBeLessThan(
|
|
352
|
+
dropped.indexOf(spec.ruleTable),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("down() removes every constraint and index up() added, in reverse", async () => {
|
|
358
|
+
const up: Array<string> = await recordQueries("up");
|
|
359
|
+
const down: Array<string> = await recordQueries("down");
|
|
360
|
+
|
|
361
|
+
const addedConstraints: Array<string> = up
|
|
362
|
+
.map((statement: string): string | null => {
|
|
363
|
+
const match: RegExpMatchArray | null = statement.match(
|
|
364
|
+
/ADD CONSTRAINT "([^"]+)"/,
|
|
365
|
+
);
|
|
366
|
+
return match ? match[1]! : null;
|
|
367
|
+
})
|
|
368
|
+
.filter((name: string | null): name is string => {
|
|
369
|
+
return name !== null;
|
|
370
|
+
});
|
|
371
|
+
const droppedConstraints: Array<string> = down
|
|
372
|
+
.map((statement: string): string | null => {
|
|
373
|
+
const match: RegExpMatchArray | null = statement.match(
|
|
374
|
+
/DROP CONSTRAINT "([^"]+)"/,
|
|
375
|
+
);
|
|
376
|
+
return match ? match[1]! : null;
|
|
377
|
+
})
|
|
378
|
+
.filter((name: string | null): name is string => {
|
|
379
|
+
return name !== null;
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
expect(droppedConstraints).toEqual([...addedConstraints].reverse());
|
|
383
|
+
|
|
384
|
+
const addedIndexes: Array<string> = up
|
|
385
|
+
.map((statement: string): string | null => {
|
|
386
|
+
const match: RegExpMatchArray | null = statement.match(
|
|
387
|
+
/^CREATE INDEX "([^"]+)"/,
|
|
388
|
+
);
|
|
389
|
+
return match ? match[1]! : null;
|
|
390
|
+
})
|
|
391
|
+
.filter((name: string | null): name is string => {
|
|
392
|
+
return name !== null;
|
|
393
|
+
});
|
|
394
|
+
const droppedIndexes: Array<string> = down
|
|
395
|
+
.map((statement: string): string | null => {
|
|
396
|
+
const match: RegExpMatchArray | null = statement.match(
|
|
397
|
+
/^DROP INDEX "public"\."([^"]+)"$/,
|
|
398
|
+
);
|
|
399
|
+
return match ? match[1]! : null;
|
|
400
|
+
})
|
|
401
|
+
.filter((name: string | null): name is string => {
|
|
402
|
+
return name !== null;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
expect([...droppedIndexes].sort()).toEqual([...addedIndexes].sort());
|
|
406
|
+
});
|
|
407
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { AddSloMonitorRuleMonitorType1794200000000 } from "../../../../Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType";
|
|
2
|
+
import SchemaMigrations from "../../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
|
|
3
|
+
import ServiceLevelObjectiveMonitorRule from "../../../../Models/DatabaseModels/ServiceLevelObjectiveMonitorRule";
|
|
4
|
+
import ColumnLength from "../../../../Types/Database/ColumnLength";
|
|
5
|
+
import ColumnType from "../../../../Types/Database/ColumnType";
|
|
6
|
+
import { describe, expect, test } from "@jest/globals";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { QueryRunner, getMetadataArgsStorage } from "typeorm";
|
|
10
|
+
import type { ColumnMetadataArgs } from "typeorm/metadata-args/ColumnMetadataArgs";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* The schema half of "monitor type" as an SLO monitor rule criterion: one
|
|
14
|
+
* nullable column on a table 1793100000000-SloProductOverhaul created.
|
|
15
|
+
*
|
|
16
|
+
* SloProductOverhaul has shipped, so its CREATE TABLE is never edited to carry
|
|
17
|
+
* the column; that migration's own test accepts a column a later registered
|
|
18
|
+
* migration adds, and this is the later migration's test. It pins that the
|
|
19
|
+
* column is added exactly as the model declares it (a mismatch is a green
|
|
20
|
+
* deploy followed by a red Schema Drift job), that existing rules stay valid
|
|
21
|
+
* without a backfill, and that down() undoes up().
|
|
22
|
+
*
|
|
23
|
+
* Fake QueryRunner only. Applying it to Postgres and re-running the Schema
|
|
24
|
+
* Drift check to "No schema drift." is how it was verified.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const OWN_CLASS_NAME: string = "AddSloMonitorRuleMonitorType1794200000000";
|
|
28
|
+
|
|
29
|
+
const TABLE_CREATED_BY: string = "SloProductOverhaul1793100000000";
|
|
30
|
+
|
|
31
|
+
const MIGRATION_PATH: string = path.join(
|
|
32
|
+
__dirname,
|
|
33
|
+
"..",
|
|
34
|
+
"..",
|
|
35
|
+
"..",
|
|
36
|
+
"..",
|
|
37
|
+
"Server",
|
|
38
|
+
"Infrastructure",
|
|
39
|
+
"Postgres",
|
|
40
|
+
"SchemaMigrations",
|
|
41
|
+
"1794200000000-AddSloMonitorRuleMonitorType.ts",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
type TimestampOfClassNameFunction = (className: string) => number | null;
|
|
45
|
+
|
|
46
|
+
// The timestamp TypeORM sorts by and records: the one in the CLASS name.
|
|
47
|
+
const timestampOfClassName: TimestampOfClassNameFunction = (
|
|
48
|
+
className: string,
|
|
49
|
+
): number | null => {
|
|
50
|
+
const match: RegExpMatchArray | null = className.match(/(\d{13})$/);
|
|
51
|
+
return match ? Number(match[1]) : null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const registeredNames: Array<string> = (
|
|
55
|
+
SchemaMigrations as unknown as Array<{ name: string }>
|
|
56
|
+
).map((migration: { name: string }): string => {
|
|
57
|
+
return migration.name;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
type RecordQueriesFunction = (
|
|
61
|
+
direction: "up" | "down",
|
|
62
|
+
) => Promise<Array<string>>;
|
|
63
|
+
|
|
64
|
+
const recordQueries: RecordQueriesFunction = async (
|
|
65
|
+
direction: "up" | "down",
|
|
66
|
+
): Promise<Array<string>> => {
|
|
67
|
+
const statements: Array<string> = [];
|
|
68
|
+
|
|
69
|
+
const queryRunner: QueryRunner = {
|
|
70
|
+
query: async (statement: string): Promise<void> => {
|
|
71
|
+
statements.push(statement);
|
|
72
|
+
},
|
|
73
|
+
} as unknown as QueryRunner;
|
|
74
|
+
|
|
75
|
+
await new AddSloMonitorRuleMonitorType1794200000000()[direction](queryRunner);
|
|
76
|
+
|
|
77
|
+
return statements;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
describe("AddSloMonitorRuleMonitorType migration - identity and registration", () => {
|
|
81
|
+
test("lives at its round stamp, with a class and name that carry it", () => {
|
|
82
|
+
const source: string = fs.readFileSync(MIGRATION_PATH, "utf8");
|
|
83
|
+
|
|
84
|
+
expect(source).toContain(`export class ${OWN_CLASS_NAME}`);
|
|
85
|
+
expect(source).toContain(`public name: string = "${OWN_CLASS_NAME}";`);
|
|
86
|
+
expect(new AddSloMonitorRuleMonitorType1794200000000().name).toBe(
|
|
87
|
+
OWN_CLASS_NAME,
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("is registered, so the column actually reaches every database", () => {
|
|
92
|
+
expect(registeredNames).toContain(OWN_CLASS_NAME);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* Not "is registered last": that claim is falsified by whichever migration
|
|
97
|
+
* lands next, without it going anywhere near this column. What matters is
|
|
98
|
+
* the queue this one must not jump - see SchemaMigrationsOrdering for the
|
|
99
|
+
* registry-wide guard on the newest entry.
|
|
100
|
+
*/
|
|
101
|
+
test("its timestamp keeps it behind every migration registered before it", () => {
|
|
102
|
+
const ownIndex: number = registeredNames.indexOf(OWN_CLASS_NAME);
|
|
103
|
+
|
|
104
|
+
// indexOf -1 would make the slice below empty and this test vacuous.
|
|
105
|
+
expect(ownIndex).toBeGreaterThan(0);
|
|
106
|
+
|
|
107
|
+
const ownTimestamp: number = timestampOfClassName(OWN_CLASS_NAME)!;
|
|
108
|
+
|
|
109
|
+
const notBehind: Array<string> = registeredNames
|
|
110
|
+
.slice(0, ownIndex)
|
|
111
|
+
.filter((className: string): boolean => {
|
|
112
|
+
const timestamp: number | null = timestampOfClassName(className);
|
|
113
|
+
return timestamp !== null && timestamp >= ownTimestamp;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(notBehind).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("runs after the migration that creates the table it alters", () => {
|
|
120
|
+
expect(registeredNames.indexOf(TABLE_CREATED_BY)).toBeGreaterThan(-1);
|
|
121
|
+
expect(registeredNames.indexOf(TABLE_CREATED_BY)).toBeLessThan(
|
|
122
|
+
registeredNames.indexOf(OWN_CLASS_NAME),
|
|
123
|
+
);
|
|
124
|
+
expect(timestampOfClassName(TABLE_CREATED_BY)!).toBeLessThan(
|
|
125
|
+
timestampOfClassName(OWN_CLASS_NAME)!,
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("AddSloMonitorRuleMonitorType migration - up()", () => {
|
|
131
|
+
/*
|
|
132
|
+
* Nullable with no default and no backfill: every rule saved before this
|
|
133
|
+
* column existed reads as "no type filter", which is what it meant.
|
|
134
|
+
*/
|
|
135
|
+
test("adds exactly the nullable monitorType column and nothing else", async () => {
|
|
136
|
+
expect(await recordQueries("up")).toEqual([
|
|
137
|
+
'ALTER TABLE "ServiceLevelObjectiveMonitorRule" ADD "monitorType" character varying(100)',
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("matches the column the model declares, so the Schema Drift job stays green", () => {
|
|
142
|
+
const declared: ColumnMetadataArgs | undefined =
|
|
143
|
+
getMetadataArgsStorage().columns.find(
|
|
144
|
+
(column: ColumnMetadataArgs): boolean => {
|
|
145
|
+
return (
|
|
146
|
+
column.target === ServiceLevelObjectiveMonitorRule &&
|
|
147
|
+
column.propertyName === "monitorType"
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(declared).toBeDefined();
|
|
153
|
+
expect(declared!.options.name).toBeUndefined();
|
|
154
|
+
expect(declared!.options.type).toBe(ColumnType.ShortText);
|
|
155
|
+
expect(declared!.options.length).toBe(ColumnLength.ShortText);
|
|
156
|
+
expect(declared!.options.nullable).toBe(true);
|
|
157
|
+
expect(declared!.options.default).toBeUndefined();
|
|
158
|
+
|
|
159
|
+
// ColumnType.ShortText is varchar, which Postgres spells character varying.
|
|
160
|
+
expect(ColumnType.ShortText).toBe("varchar");
|
|
161
|
+
expect(ColumnLength.ShortText).toBe(100);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("AddSloMonitorRuleMonitorType migration - down()", () => {
|
|
166
|
+
test("drops exactly the column up() added", async () => {
|
|
167
|
+
expect(await recordQueries("down")).toEqual([
|
|
168
|
+
'ALTER TABLE "ServiceLevelObjectiveMonitorRule" DROP COLUMN "monitorType"',
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -100,6 +100,76 @@ const persistedColumns: PersistedColumnsFunction = (
|
|
|
100
100
|
return names;
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
+
const MIGRATION_TIMESTAMP: number = 1793100000000;
|
|
104
|
+
|
|
105
|
+
const MIGRATIONS_DIRECTORY: string = path.dirname(MIGRATION_PATH);
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
* Columns that a migration registered AFTER this one adds to `tableName`.
|
|
109
|
+
*
|
|
110
|
+
* This migration has shipped, so a column its models gain later arrives in a
|
|
111
|
+
* later migration, never in an edit to the CREATE TABLE here: editing it would
|
|
112
|
+
* do nothing for the databases that already ran it while putting them out of
|
|
113
|
+
* step with fresh ones. So the invariant the column checks below pin is "every
|
|
114
|
+
* column the model persists is created here or added by a later registered
|
|
115
|
+
* migration", which still fails a column that no migration creates at all. The
|
|
116
|
+
* later migration's own test pins that column's definition, and the Schema
|
|
117
|
+
* Drift job owns the end-state schema.
|
|
118
|
+
*
|
|
119
|
+
* "Later" is by the timestamp in the class name, which is what TypeORM sorts
|
|
120
|
+
* by, and only registered migrations count - a file nobody registered never
|
|
121
|
+
* runs. Their source is read rather than run against the fake QueryRunner, so
|
|
122
|
+
* a later data migration that reads rows back cannot break this suite.
|
|
123
|
+
*/
|
|
124
|
+
type ColumnsAddedLaterFunction = (tableName: string) => Set<string>;
|
|
125
|
+
|
|
126
|
+
const columnsAddedByLaterMigrations: ColumnsAddedLaterFunction = (
|
|
127
|
+
tableName: string,
|
|
128
|
+
): Set<string> => {
|
|
129
|
+
const laterTimestamps: Set<string> = new Set<string>();
|
|
130
|
+
|
|
131
|
+
for (const migration of SchemaMigrations as unknown as Array<{
|
|
132
|
+
name: string;
|
|
133
|
+
}>) {
|
|
134
|
+
const timestamp: string | undefined =
|
|
135
|
+
migration.name.match(/(\d{13})$/)?.[1];
|
|
136
|
+
|
|
137
|
+
if (timestamp && Number(timestamp) > MIGRATION_TIMESTAMP) {
|
|
138
|
+
laterTimestamps.add(timestamp);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const addColumn: RegExp = new RegExp(
|
|
143
|
+
`ALTER TABLE "${tableName}" ADD (?:COLUMN )?"([^"]+)"`,
|
|
144
|
+
"g",
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const added: Set<string> = new Set<string>();
|
|
148
|
+
|
|
149
|
+
for (const fileName of fs.readdirSync(MIGRATIONS_DIRECTORY)) {
|
|
150
|
+
if (
|
|
151
|
+
!fileName.endsWith(".ts") ||
|
|
152
|
+
!laterTimestamps.has(fileName.split("-")[0]!)
|
|
153
|
+
) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const source: string = fs.readFileSync(
|
|
158
|
+
path.join(MIGRATIONS_DIRECTORY, fileName),
|
|
159
|
+
"utf8",
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
let match: RegExpExecArray | null = addColumn.exec(source);
|
|
163
|
+
|
|
164
|
+
while (match) {
|
|
165
|
+
added.add(match[1]!);
|
|
166
|
+
match = addColumn.exec(source);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return added;
|
|
171
|
+
};
|
|
172
|
+
|
|
103
173
|
type CreateTableStatementFunction = (
|
|
104
174
|
statements: Array<string>,
|
|
105
175
|
tableName: string,
|
|
@@ -391,7 +461,7 @@ describe("SloProductOverhaul migration - scope", () => {
|
|
|
391
461
|
});
|
|
392
462
|
|
|
393
463
|
describe("SloProductOverhaul migration - ServiceLevelObjectiveMonitorRule", () => {
|
|
394
|
-
test("creates every column the model persists, criteria included", async () => {
|
|
464
|
+
test("creates every column the model persists, criteria included, bar those a later migration adds", async () => {
|
|
395
465
|
const statement: string = createTableStatement(
|
|
396
466
|
await recordQueries("up"),
|
|
397
467
|
"ServiceLevelObjectiveMonitorRule",
|
|
@@ -404,8 +474,15 @@ describe("SloProductOverhaul migration - ServiceLevelObjectiveMonitorRule", () =
|
|
|
404
474
|
expect(columns).toContain("criteria");
|
|
405
475
|
expect(columns).toContain("monitorNamePattern");
|
|
406
476
|
|
|
477
|
+
const addedLater: Set<string> = columnsAddedByLaterMigrations(
|
|
478
|
+
"ServiceLevelObjectiveMonitorRule",
|
|
479
|
+
);
|
|
480
|
+
|
|
407
481
|
for (const column of columns) {
|
|
408
|
-
expect({
|
|
482
|
+
expect({
|
|
483
|
+
column,
|
|
484
|
+
created: statement.includes(`"${column}"`) || addedLater.has(column),
|
|
485
|
+
}).toEqual({
|
|
409
486
|
column,
|
|
410
487
|
created: true,
|
|
411
488
|
});
|
|
@@ -460,14 +537,21 @@ describe("SloProductOverhaul migration - ServiceLevelObjectiveMonitorRule", () =
|
|
|
460
537
|
});
|
|
461
538
|
|
|
462
539
|
describe("SloProductOverhaul migration - ServiceLevelObjectiveFeed", () => {
|
|
463
|
-
test("creates every column the model persists", async () => {
|
|
540
|
+
test("creates every column the model persists, bar those a later migration adds", async () => {
|
|
464
541
|
const statement: string = createTableStatement(
|
|
465
542
|
await recordQueries("up"),
|
|
466
543
|
"ServiceLevelObjectiveFeed",
|
|
467
544
|
);
|
|
468
545
|
|
|
546
|
+
const addedLater: Set<string> = columnsAddedByLaterMigrations(
|
|
547
|
+
"ServiceLevelObjectiveFeed",
|
|
548
|
+
);
|
|
549
|
+
|
|
469
550
|
for (const column of persistedColumns(ServiceLevelObjectiveFeed)) {
|
|
470
|
-
expect({
|
|
551
|
+
expect({
|
|
552
|
+
column,
|
|
553
|
+
created: statement.includes(`"${column}"`) || addedLater.has(column),
|
|
554
|
+
}).toEqual({
|
|
471
555
|
column,
|
|
472
556
|
created: true,
|
|
473
557
|
});
|