@revoengine/cli 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +464 -8
- package/dist/src/cli.js +65 -6
- package/dist/src/client.d.ts +366 -5
- package/dist/src/client.js +954 -13
- package/dist/src/commands/auth.js +4 -2
- package/dist/src/commands/component.js +839 -412
- package/dist/src/commands/database-schemas.d.ts +2 -0
- package/dist/src/commands/database-schemas.js +188 -0
- package/dist/src/commands/database-views.d.ts +2 -0
- package/dist/src/commands/database-views.js +123 -0
- package/dist/src/commands/endpoints.js +114 -0
- package/dist/src/commands/env.d.ts +2 -0
- package/dist/src/commands/env.js +380 -0
- package/dist/src/commands/events.d.ts +2 -0
- package/dist/src/commands/events.js +146 -0
- package/dist/src/commands/groups.d.ts +2 -0
- package/dist/src/commands/groups.js +169 -0
- package/dist/src/commands/index.d.ts +10 -0
- package/dist/src/commands/index.js +10 -0
- package/dist/src/commands/job-templates.d.ts +2 -0
- package/dist/src/commands/job-templates.js +101 -0
- package/dist/src/commands/metadata.d.ts +2 -0
- package/dist/src/commands/metadata.js +159 -0
- package/dist/src/commands/project.js +82 -1
- package/dist/src/commands/role-groups.d.ts +2 -0
- package/dist/src/commands/role-groups.js +152 -0
- package/dist/src/commands/schedules.d.ts +2 -0
- package/dist/src/commands/schedules.js +141 -0
- package/dist/src/commands/terminal-service.d.ts +38 -0
- package/dist/src/commands/terminal-service.js +210 -0
- package/dist/src/commands/terminal.d.ts +22 -0
- package/dist/src/commands/terminal.js +511 -0
- package/dist/src/component-lock.d.ts +126 -2
- package/dist/src/component-lock.js +378 -15
- package/dist/src/config.d.ts +20 -0
- package/dist/src/config.js +121 -6
- package/dist/src/database-schema-artifacts.d.ts +7 -0
- package/dist/src/database-schema-artifacts.js +8 -0
- package/dist/src/env-sync.d.ts +83 -0
- package/dist/src/env-sync.js +315 -0
- package/dist/src/metadata-backfill.d.ts +56 -0
- package/dist/src/metadata-backfill.js +1176 -0
- package/dist/src/project.d.ts +17 -9
- package/dist/src/project.js +87 -11
- package/dist/src/prompt.js +10 -18
- package/dist/src/resource-metadata.d.ts +25 -0
- package/dist/src/resource-metadata.js +132 -0
- package/dist/src/resource-syncs/database-schema-sync.d.ts +117 -0
- package/dist/src/resource-syncs/database-schema-sync.js +2289 -0
- package/dist/src/resource-syncs/database-view-sync.d.ts +124 -0
- package/dist/src/resource-syncs/database-view-sync.js +1317 -0
- package/dist/src/resource-syncs/endpoint-sync.d.ts +96 -0
- package/dist/src/resource-syncs/endpoint-sync.js +1283 -0
- package/dist/src/resource-syncs/event-sync.d.ts +99 -0
- package/dist/src/resource-syncs/event-sync.js +949 -0
- package/dist/src/resource-syncs/group-sync.d.ts +86 -0
- package/dist/src/resource-syncs/group-sync.js +882 -0
- package/dist/src/resource-syncs/job-template-sync.d.ts +85 -0
- package/dist/src/resource-syncs/job-template-sync.js +782 -0
- package/dist/src/resource-syncs/role-group-sync.d.ts +83 -0
- package/dist/src/resource-syncs/role-group-sync.js +597 -0
- package/dist/src/resource-syncs/schedule-sync.d.ts +111 -0
- package/dist/src/resource-syncs/schedule-sync.js +1302 -0
- package/dist/src/resource-syncs/util.d.ts +19 -0
- package/dist/src/resource-syncs/util.js +116 -0
- package/dist/src/runtime-view.d.ts +1 -0
- package/dist/src/runtime-view.js +6 -1
- package/dist/src/sync-output.d.ts +38 -0
- package/dist/src/sync-output.js +131 -0
- package/dist/src/tracked-resources.d.ts +7 -0
- package/dist/src/tracked-resources.js +61 -0
- package/dist/src/types.d.ts +227 -0
- package/dist/src/ui.d.ts +3 -0
- package/dist/src/ui.js +68 -10
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +64 -0
- package/dist/src/workspace-component.d.ts +2 -0
- package/dist/src/workspace-component.js +34 -0
- package/dist/src/workspace-resource.d.ts +2 -0
- package/dist/src/workspace-resource.js +52 -0
- package/package.json +8 -3
|
@@ -0,0 +1,1176 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { isDatabaseBackupArtifact } from "./database-schema-artifacts.js";
|
|
3
|
+
import { buildStableKey, buildResourceIdentityStableKeyValue, normalizeStableKeyValue, readConfiguredResourceMetadataStableKey, sanitizeUserMetadata, } from "./resource-metadata.js";
|
|
4
|
+
import { shouldTrackResource } from "./tracked-resources.js";
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function unwrapResource(value) {
|
|
9
|
+
if (isRecord(value) && isRecord(value.data)) {
|
|
10
|
+
return value.data;
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function resourceIdOf(resource) {
|
|
15
|
+
return String(('databaseId' in resource && resource.databaseId)
|
|
16
|
+
|| ('databaseViewId' in resource && resource.databaseViewId)
|
|
17
|
+
|| ('jobTemplateId' in resource && resource.jobTemplateId)
|
|
18
|
+
|| ('endpointId' in resource && resource.endpointId)
|
|
19
|
+
|| ('scheduleId' in resource && resource.scheduleId)
|
|
20
|
+
|| ('jobEventId' in resource && resource.jobEventId)
|
|
21
|
+
|| ('eventId' in resource && resource.eventId)
|
|
22
|
+
|| ('groupId' in resource && resource.groupId)
|
|
23
|
+
|| resource.componentId
|
|
24
|
+
|| resource.roleGroupId
|
|
25
|
+
|| resource.id
|
|
26
|
+
|| '');
|
|
27
|
+
}
|
|
28
|
+
function normalizeValue(value, fallback = null) {
|
|
29
|
+
return value ?? fallback;
|
|
30
|
+
}
|
|
31
|
+
function rawConfiguredStableKey(resource, stableKeyName) {
|
|
32
|
+
for (const metadata of [resource.metadata, resource.metaData, resource.resourceMetadata]) {
|
|
33
|
+
if (isRecord(metadata) && Object.prototype.hasOwnProperty.call(metadata, stableKeyName)) {
|
|
34
|
+
return { present: true, value: metadata[stableKeyName] };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { present: false, value: undefined };
|
|
38
|
+
}
|
|
39
|
+
function metadataStableKeyIdentity(resourceType, stableKey) {
|
|
40
|
+
// TODO(APP-918): Align Role Groups with the exact Stable Key contract used by
|
|
41
|
+
// Components and Job Templates. Update role-group pull, plan, push and
|
|
42
|
+
// filesystem-path collision handling together before changing this branch.
|
|
43
|
+
const identityValue = resourceType === 'role-group'
|
|
44
|
+
? normalizeStableKeyValue(stableKey)
|
|
45
|
+
: stableKey;
|
|
46
|
+
return `${resourceType}:${identityValue}`;
|
|
47
|
+
}
|
|
48
|
+
export function generateStableKeySuffix(bytes = 4) {
|
|
49
|
+
return randomBytes(bytes).toString('hex');
|
|
50
|
+
}
|
|
51
|
+
export function buildGeneratedStableKey(resource) {
|
|
52
|
+
return `${buildResourceIdentityStableKeyValue(resource)}#${generateStableKeySuffix()}`;
|
|
53
|
+
}
|
|
54
|
+
function buildNamedResourceGeneratedStableKey(resource) {
|
|
55
|
+
const category = typeof resource.category === 'string' && resource.category.trim()
|
|
56
|
+
? resource.category.trim()
|
|
57
|
+
: 'Uncategorized';
|
|
58
|
+
const name = typeof resource.name === 'string' ? resource.name.trim() : '';
|
|
59
|
+
return `${category}/${name}#${generateStableKeySuffix()}`;
|
|
60
|
+
}
|
|
61
|
+
function isDatabaseSchemaPartition(database) {
|
|
62
|
+
return Boolean(database.parentId || database.parent);
|
|
63
|
+
}
|
|
64
|
+
function databaseDefinitions(database) {
|
|
65
|
+
return Array.isArray(database.definition)
|
|
66
|
+
? database.definition.filter(isRecord)
|
|
67
|
+
: [];
|
|
68
|
+
}
|
|
69
|
+
function columnStableKeyValue(definition, stableKeyName) {
|
|
70
|
+
const metadata = definition.metadata;
|
|
71
|
+
if (!isRecord(metadata) || !Object.hasOwn(metadata, stableKeyName)) {
|
|
72
|
+
return { present: false, value: undefined };
|
|
73
|
+
}
|
|
74
|
+
return { present: true, value: metadata[stableKeyName] };
|
|
75
|
+
}
|
|
76
|
+
function buildGeneratedDatabaseColumnStableKey(database, columnName) {
|
|
77
|
+
const category = typeof database.category === 'string' && database.category.trim()
|
|
78
|
+
? database.category.trim()
|
|
79
|
+
: 'Uncategorized';
|
|
80
|
+
const databaseName = typeof database.name === 'string' ? database.name.trim() : '';
|
|
81
|
+
return `${category}/${databaseName}/${columnName}#${generateStableKeySuffix()}`;
|
|
82
|
+
}
|
|
83
|
+
function buildColumnStableKeyMetadata(definition, stableKeyName, stableKey) {
|
|
84
|
+
return {
|
|
85
|
+
...sanitizeUserMetadata(definition.metadata),
|
|
86
|
+
[stableKeyName]: stableKey,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function buildDatabaseColumnMetadataUpdatePayload(input) {
|
|
90
|
+
if (!Number.isInteger(input.database.version)) {
|
|
91
|
+
throw new Error('database schema version is required to update column metadata');
|
|
92
|
+
}
|
|
93
|
+
if (input.assignments.length === 0) {
|
|
94
|
+
throw new Error('at least one database schema column metadata assignment is required');
|
|
95
|
+
}
|
|
96
|
+
const definitions = databaseDefinitions(input.database);
|
|
97
|
+
const assignmentsByDefinition = new Map(input.assignments.map((assignment) => [
|
|
98
|
+
assignment.definition,
|
|
99
|
+
assignment.stableKey,
|
|
100
|
+
]));
|
|
101
|
+
for (const assignment of input.assignments) {
|
|
102
|
+
const name = typeof assignment.definition.name === 'string' ? assignment.definition.name : 'unknown';
|
|
103
|
+
if (!definitions.includes(assignment.definition)) {
|
|
104
|
+
throw new Error(`database schema column "${name}" no longer exists`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const definition of definitions) {
|
|
108
|
+
const name = typeof definition.name === 'string' ? definition.name : 'unknown';
|
|
109
|
+
if (typeof definition.databaseDefinitionId !== 'string' || !definition.databaseDefinitionId) {
|
|
110
|
+
throw new Error(`database schema column "${name}" is missing databaseDefinitionId; refusing to rewrite the full definition for metadata backfill`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
version: input.database.version,
|
|
115
|
+
definition: definitions.map((definition) => {
|
|
116
|
+
const { metadata: _metadata, ...withoutMetadata } = definition;
|
|
117
|
+
const stableKey = assignmentsByDefinition.get(definition);
|
|
118
|
+
if (!stableKey) {
|
|
119
|
+
const metadata = sanitizeUserMetadata(definition.metadata);
|
|
120
|
+
return Object.keys(metadata).length > 0
|
|
121
|
+
? { ...withoutMetadata, metadata }
|
|
122
|
+
: withoutMetadata;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
...withoutMetadata,
|
|
126
|
+
metadata: buildColumnStableKeyMetadata(definition, input.stableKeyName, stableKey),
|
|
127
|
+
};
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function groupMetadataBackfillReason(group, existingStableKey, force) {
|
|
132
|
+
const baseReason = existingStableKey
|
|
133
|
+
? (force ? 'stable key will be regenerated' : 'stable key already exists')
|
|
134
|
+
: 'stable key missing';
|
|
135
|
+
const reservedKeys = new Set();
|
|
136
|
+
for (const metadata of [group.resourceMetadata, group.metaData, group.metadata]) {
|
|
137
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
for (const key of Object.keys(metadata)) {
|
|
141
|
+
if (key.startsWith('__')) {
|
|
142
|
+
reservedKeys.add(key);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (reservedKeys.size === 0) {
|
|
147
|
+
return baseReason;
|
|
148
|
+
}
|
|
149
|
+
return `${baseReason}; backend-reserved metadata keys excluded: ${[...reservedKeys].sort((left, right) => left.localeCompare(right)).join(', ')}`;
|
|
150
|
+
}
|
|
151
|
+
function waitForMutationSlot() {
|
|
152
|
+
return new Promise((resolve) => setTimeout(resolve, 100));
|
|
153
|
+
}
|
|
154
|
+
function scheduleExclusionReason(schedule) {
|
|
155
|
+
if (schedule.systemManaged === true) {
|
|
156
|
+
return 'system-managed schedule is excluded';
|
|
157
|
+
}
|
|
158
|
+
const materializedViewRefresh = schedule.targetType === 'PLATFORM_OPERATION'
|
|
159
|
+
&& isRecord(schedule.targetConfig)
|
|
160
|
+
&& schedule.targetConfig.kind === 'refresh_materialized_view';
|
|
161
|
+
if (schedule.targetType !== 'JOB_TEMPLATE' && !materializedViewRefresh) {
|
|
162
|
+
return `target type ${String(schedule.targetType || 'unknown')} is outside Schedule migration scope`;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
function eventExclusionReason(event) {
|
|
167
|
+
if (event.deletedAt || event.deletedBy) {
|
|
168
|
+
return 'deleted event is excluded';
|
|
169
|
+
}
|
|
170
|
+
if (event.targetType !== 'JOB_TEMPLATE') {
|
|
171
|
+
return `target type ${String(event.targetType || 'unknown')} is outside Event migration scope`;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function endpointExclusionReason(endpoint) {
|
|
176
|
+
return endpoint.deletedAt || endpoint.deletedBy ? 'deleted endpoint is excluded' : null;
|
|
177
|
+
}
|
|
178
|
+
function buildMetadataUpdatePayload(resource, stableKeyName, stableKey, resourceType) {
|
|
179
|
+
if (!Number.isInteger(resource.version)) {
|
|
180
|
+
throw new Error(`${resourceType} version is required to update metadata`);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
version: resource.version,
|
|
184
|
+
metadata: {
|
|
185
|
+
...sanitizeUserMetadata(resource.resourceMetadata),
|
|
186
|
+
...sanitizeUserMetadata(resource.metaData),
|
|
187
|
+
...sanitizeUserMetadata(resource.metadata),
|
|
188
|
+
[stableKeyName]: stableKey,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function databaseViewPrincipalIds(entries, property) {
|
|
193
|
+
if (!Array.isArray(entries)) {
|
|
194
|
+
throw new Error(`database view ${property === 'userId' ? 'users' : 'groups'} are required to preserve its access policy`);
|
|
195
|
+
}
|
|
196
|
+
return entries.map((entry) => {
|
|
197
|
+
const value = typeof entry === 'string'
|
|
198
|
+
? entry
|
|
199
|
+
: isRecord(entry) && typeof entry[property] === 'string'
|
|
200
|
+
? entry[property]
|
|
201
|
+
: undefined;
|
|
202
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
203
|
+
throw new Error(`database view ${property === 'userId' ? 'user' : 'group'} access policy contains an invalid principal`);
|
|
204
|
+
}
|
|
205
|
+
return value;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function databaseViewIndexesForUpdate(view) {
|
|
209
|
+
if (view.indexes === undefined) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
if (!Array.isArray(view.indexes)) {
|
|
213
|
+
throw new Error('materialized database view indexes are invalid and cannot be preserved safely');
|
|
214
|
+
}
|
|
215
|
+
return JSON.parse(JSON.stringify(view.indexes));
|
|
216
|
+
}
|
|
217
|
+
function buildDatabaseViewMetadataUpdatePayload(view, stableKeyName, stableKey) {
|
|
218
|
+
if (typeof view.name !== 'string' || !view.name.trim()) {
|
|
219
|
+
throw new Error('database view name is required for a full update');
|
|
220
|
+
}
|
|
221
|
+
if (view.type !== 'VIEW' && view.type !== 'MATERIALIZED_VIEW') {
|
|
222
|
+
throw new Error(`database view type ${String(view.type || 'unknown')} cannot be updated`);
|
|
223
|
+
}
|
|
224
|
+
if (view.definitionMode !== 'STRUCTURED' && view.definitionMode !== 'RAW_SQL') {
|
|
225
|
+
throw new Error(`database view definitionMode ${String(view.definitionMode || 'unknown')} cannot be updated`);
|
|
226
|
+
}
|
|
227
|
+
if (typeof view.restricted !== 'boolean') {
|
|
228
|
+
throw new Error('database view restricted access policy is required to preserve it');
|
|
229
|
+
}
|
|
230
|
+
if (!Number.isInteger(view.version) || view.version < 0) {
|
|
231
|
+
throw new Error('database view version is required for a full update');
|
|
232
|
+
}
|
|
233
|
+
if (view.type === 'MATERIALIZED_VIEW' && !Array.isArray(view.uniqueKey)) {
|
|
234
|
+
throw new Error('materialized database view uniqueKey is required to preserve its definition');
|
|
235
|
+
}
|
|
236
|
+
const payload = {
|
|
237
|
+
name: view.name,
|
|
238
|
+
...(typeof view.category === 'string' ? { category: view.category } : {}),
|
|
239
|
+
...(typeof view.desc === 'string'
|
|
240
|
+
? { desc: view.desc }
|
|
241
|
+
: typeof view.description === 'string'
|
|
242
|
+
? { desc: view.description }
|
|
243
|
+
: {}),
|
|
244
|
+
...buildMetadataUpdatePayload(view, stableKeyName, stableKey, 'database-view'),
|
|
245
|
+
type: view.type,
|
|
246
|
+
definitionMode: view.definitionMode,
|
|
247
|
+
restricted: view.restricted,
|
|
248
|
+
users: databaseViewPrincipalIds(view.users, 'userId'),
|
|
249
|
+
groups: databaseViewPrincipalIds(view.groups, 'groupId'),
|
|
250
|
+
};
|
|
251
|
+
const indexes = view.type === 'MATERIALIZED_VIEW'
|
|
252
|
+
? databaseViewIndexesForUpdate(view)
|
|
253
|
+
: undefined;
|
|
254
|
+
const materialized = view.type === 'MATERIALIZED_VIEW'
|
|
255
|
+
? {
|
|
256
|
+
uniqueKey: view.uniqueKey,
|
|
257
|
+
...(indexes === undefined ? {} : { indexes }),
|
|
258
|
+
}
|
|
259
|
+
: {};
|
|
260
|
+
if (view.definitionMode === 'STRUCTURED') {
|
|
261
|
+
if (!isRecord(view.request)) {
|
|
262
|
+
throw new Error('structured database view request is required for a full update');
|
|
263
|
+
}
|
|
264
|
+
return { ...payload, ...materialized, request: view.request };
|
|
265
|
+
}
|
|
266
|
+
if (typeof view.rawQuery !== 'string' || !view.rawQuery.trim()) {
|
|
267
|
+
throw new Error('raw SQL database view query is required for a full update');
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
...payload,
|
|
271
|
+
...materialized,
|
|
272
|
+
rawQuery: view.rawQuery,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function buildJobTemplateMetadataUpdatePayload(jobTemplate, stableKeyName, stableKey) {
|
|
276
|
+
if (!Object.hasOwn(jobTemplate, 'options') || jobTemplate.options === undefined || jobTemplate.options === null) {
|
|
277
|
+
throw new Error(`Job-template ${resourceIdOf(jobTemplate) || 'unknown'} is missing options in the API response; refusing metadata update to preserve its configuration.`);
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
...buildMetadataUpdatePayload(jobTemplate, stableKeyName, stableKey, 'job-template'),
|
|
281
|
+
options: jobTemplate.options,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
export async function buildMetadataBackfillPlan(input) {
|
|
285
|
+
const { client, envName, stableKeyName, trackedResources, force = false, skipPartitions = false } = input;
|
|
286
|
+
const items = [];
|
|
287
|
+
if (shouldTrackResource(trackedResources, 'component')) {
|
|
288
|
+
const { components } = await client.listAllComponents();
|
|
289
|
+
for (const summary of components) {
|
|
290
|
+
const summaryId = resourceIdOf(summary);
|
|
291
|
+
if (!summaryId) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const component = unwrapResource(await client.getComponent(summaryId));
|
|
295
|
+
const resourceId = resourceIdOf(component) || summaryId;
|
|
296
|
+
const existingStableKey = readConfiguredResourceMetadataStableKey(component, stableKeyName);
|
|
297
|
+
const stableKey = existingStableKey && !force
|
|
298
|
+
? undefined
|
|
299
|
+
: buildGeneratedStableKey(component);
|
|
300
|
+
const plannedStableKey = stableKey || existingStableKey || undefined;
|
|
301
|
+
items.push({
|
|
302
|
+
resourceType: 'component',
|
|
303
|
+
resourceId,
|
|
304
|
+
name: String(component.name || ''),
|
|
305
|
+
category: normalizeValue(component.category),
|
|
306
|
+
action: existingStableKey && !force ? 'skip' : 'update',
|
|
307
|
+
metadataField: 'metadata',
|
|
308
|
+
stableKeyName,
|
|
309
|
+
...(plannedStableKey ? { stableKey: plannedStableKey } : {}),
|
|
310
|
+
reason: existingStableKey
|
|
311
|
+
? (force ? 'stable key will be regenerated' : 'stable key already exists')
|
|
312
|
+
: 'stable key missing',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (shouldTrackResource(trackedResources, 'database-schema') && typeof client.listAllDatabaseSchemas === 'function') {
|
|
317
|
+
const { databases } = await client.listAllDatabaseSchemas({ projection: 'pull', skipPartitions });
|
|
318
|
+
for (const database of databases) {
|
|
319
|
+
if (isDatabaseBackupArtifact(database)) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const resourceId = resourceIdOf(database);
|
|
323
|
+
if (!resourceId) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const category = normalizeValue(database.category);
|
|
327
|
+
const name = String(database.name || '');
|
|
328
|
+
const rawStableKey = rawConfiguredStableKey(database, stableKeyName);
|
|
329
|
+
const invalidDatabaseStableKey = rawStableKey.present && (typeof rawStableKey.value !== 'string' || !rawStableKey.value.trim());
|
|
330
|
+
if (invalidDatabaseStableKey) {
|
|
331
|
+
const reason = `Invalid database-schema stableKey on ${resourceId}: metadata.${stableKeyName} must be a non-empty string.`;
|
|
332
|
+
items.push({
|
|
333
|
+
resourceType: 'database-schema', resourceId, name, category, action: 'blocked',
|
|
334
|
+
metadataField: 'metadata', stableKeyName, reason,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const existingStableKey = readConfiguredResourceMetadataStableKey(database, stableKeyName);
|
|
339
|
+
const stableKey = existingStableKey || buildNamedResourceGeneratedStableKey(database);
|
|
340
|
+
items.push({
|
|
341
|
+
resourceType: 'database-schema', resourceId, name, category,
|
|
342
|
+
action: existingStableKey ? 'skip' : 'update', metadataField: 'metadata', stableKeyName,
|
|
343
|
+
stableKey,
|
|
344
|
+
reason: existingStableKey ? 'stable key already exists' : 'assign-stable-key',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (isDatabaseSchemaPartition(database)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const definitions = databaseDefinitions(database);
|
|
351
|
+
const columnsByStableKey = new Map();
|
|
352
|
+
const columnsByName = new Map();
|
|
353
|
+
const columnsByDefinitionId = new Map();
|
|
354
|
+
for (const definition of definitions) {
|
|
355
|
+
const columnName = typeof definition.name === 'string' ? definition.name.trim() : '';
|
|
356
|
+
if (columnName) {
|
|
357
|
+
columnsByName.set(columnName, [...(columnsByName.get(columnName) || []), definition]);
|
|
358
|
+
}
|
|
359
|
+
const columnDefinitionId = typeof definition.databaseDefinitionId === 'string'
|
|
360
|
+
? definition.databaseDefinitionId.trim()
|
|
361
|
+
: '';
|
|
362
|
+
if (columnDefinitionId) {
|
|
363
|
+
columnsByDefinitionId.set(columnDefinitionId, [
|
|
364
|
+
...(columnsByDefinitionId.get(columnDefinitionId) || []),
|
|
365
|
+
definition,
|
|
366
|
+
]);
|
|
367
|
+
}
|
|
368
|
+
const rawColumnStableKey = columnStableKeyValue(definition, stableKeyName);
|
|
369
|
+
if (typeof rawColumnStableKey.value === 'string' && rawColumnStableKey.value.trim()) {
|
|
370
|
+
columnsByStableKey.set(rawColumnStableKey.value, [
|
|
371
|
+
...(columnsByStableKey.get(rawColumnStableKey.value) || []),
|
|
372
|
+
{ definition, name: columnName },
|
|
373
|
+
]);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const duplicateColumnStableKeys = new Set([...columnsByStableKey]
|
|
377
|
+
.filter(([, columns]) => columns.length > 1)
|
|
378
|
+
.map(([stableKeyValue]) => stableKeyValue));
|
|
379
|
+
const duplicateColumnNames = new Set([...columnsByName]
|
|
380
|
+
.filter(([, columns]) => columns.length > 1)
|
|
381
|
+
.map(([columnName]) => columnName));
|
|
382
|
+
const duplicateColumnDefinitionIds = new Set([...columnsByDefinitionId]
|
|
383
|
+
.filter(([, columns]) => columns.length > 1)
|
|
384
|
+
.map(([columnDefinitionId]) => columnDefinitionId));
|
|
385
|
+
for (const definition of definitions) {
|
|
386
|
+
const columnName = typeof definition.name === 'string' ? definition.name.trim() : '';
|
|
387
|
+
const columnDefinitionId = typeof definition.databaseDefinitionId === 'string'
|
|
388
|
+
? definition.databaseDefinitionId.trim()
|
|
389
|
+
: '';
|
|
390
|
+
const rawColumnStableKey = columnStableKeyValue(definition, stableKeyName);
|
|
391
|
+
const existingColumnStableKey = typeof rawColumnStableKey.value === 'string' && rawColumnStableKey.value.trim()
|
|
392
|
+
? rawColumnStableKey.value
|
|
393
|
+
: undefined;
|
|
394
|
+
const duplicate = existingColumnStableKey && duplicateColumnStableKeys.has(existingColumnStableKey);
|
|
395
|
+
const duplicateName = columnName && duplicateColumnNames.has(columnName);
|
|
396
|
+
const duplicateDefinitionId = columnDefinitionId && duplicateColumnDefinitionIds.has(columnDefinitionId);
|
|
397
|
+
const invalid = rawColumnStableKey.present && !existingColumnStableKey;
|
|
398
|
+
const columnReason = !columnName
|
|
399
|
+
? `Database column on ${resourceId} is missing a non-empty name; refusing to generate a Stable Key.`
|
|
400
|
+
: !columnDefinitionId
|
|
401
|
+
? `Database column "${columnName}" on ${resourceId} is missing databaseDefinitionId; refusing to rewrite the full definition.`
|
|
402
|
+
: duplicateName
|
|
403
|
+
? `Duplicate database column name "${columnName}" on root database schema ${resourceId}.`
|
|
404
|
+
: duplicateDefinitionId
|
|
405
|
+
? `Duplicate database column databaseDefinitionId "${columnDefinitionId}" on root database schema ${resourceId}.`
|
|
406
|
+
: invalid
|
|
407
|
+
? `Invalid database column stableKey on ${resourceId}.${columnName}: definition[].metadata.${stableKeyName} must be a non-empty string.`
|
|
408
|
+
: duplicate
|
|
409
|
+
? `Duplicate database column stableKey "${existingColumnStableKey}" on root database schema ${resourceId}.`
|
|
410
|
+
: existingColumnStableKey
|
|
411
|
+
? 'column stable key already exists'
|
|
412
|
+
: 'assign-column-stable-key';
|
|
413
|
+
items.push({
|
|
414
|
+
resourceType: 'database-schema',
|
|
415
|
+
resourceId,
|
|
416
|
+
name: `${name}.${columnName || '<unnamed>'}`,
|
|
417
|
+
category,
|
|
418
|
+
action: !columnName || !columnDefinitionId || duplicateName || duplicateDefinitionId || invalid || duplicate
|
|
419
|
+
? 'blocked'
|
|
420
|
+
: existingColumnStableKey ? 'skip' : 'update',
|
|
421
|
+
metadataField: 'definition[].metadata',
|
|
422
|
+
stableKeyName,
|
|
423
|
+
...(columnName && existingColumnStableKey ? { stableKey: existingColumnStableKey } : columnName ? {
|
|
424
|
+
stableKey: buildGeneratedDatabaseColumnStableKey(database, columnName),
|
|
425
|
+
} : {}),
|
|
426
|
+
...(columnName ? { columnName } : {}),
|
|
427
|
+
...(columnDefinitionId ? { columnDefinitionId } : {}),
|
|
428
|
+
reason: columnReason,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (shouldTrackResource(trackedResources, 'database-view') && typeof client.listAllDatabaseViewsForSync === 'function') {
|
|
434
|
+
const views = await client.listAllDatabaseViewsForSync();
|
|
435
|
+
for (const view of views) {
|
|
436
|
+
const resourceId = resourceIdOf(view);
|
|
437
|
+
if (!resourceId) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const category = normalizeValue(view.category);
|
|
441
|
+
const name = String(view.name || '');
|
|
442
|
+
const rawStableKey = rawConfiguredStableKey(view, stableKeyName);
|
|
443
|
+
if (rawStableKey.present && (typeof rawStableKey.value !== 'string' || !rawStableKey.value.trim())) {
|
|
444
|
+
const reason = `Invalid database-view stableKey on ${resourceId}: metadata.${stableKeyName} must be a non-empty string.`;
|
|
445
|
+
items.push({ resourceType: 'database-view', resourceId, name, category, action: 'blocked', metadataField: 'metadata', stableKeyName, reason });
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const existingStableKey = readConfiguredResourceMetadataStableKey(view, stableKeyName);
|
|
449
|
+
const stableKey = existingStableKey || buildNamedResourceGeneratedStableKey(view);
|
|
450
|
+
items.push({
|
|
451
|
+
resourceType: 'database-view',
|
|
452
|
+
resourceId,
|
|
453
|
+
name,
|
|
454
|
+
category,
|
|
455
|
+
action: existingStableKey ? 'skip' : 'update',
|
|
456
|
+
metadataField: 'metadata',
|
|
457
|
+
stableKeyName,
|
|
458
|
+
stableKey,
|
|
459
|
+
reason: existingStableKey ? 'stable key already exists' : 'assign-stable-key',
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (shouldTrackResource(trackedResources, 'endpoint') && typeof client.listAllEndpoints === 'function') {
|
|
464
|
+
const { endpoints } = await client.listAllEndpoints();
|
|
465
|
+
for (const summary of endpoints) {
|
|
466
|
+
const summaryId = resourceIdOf(summary);
|
|
467
|
+
if (!summaryId) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const endpoint = unwrapResource(await client.getEndpoint(summaryId));
|
|
471
|
+
const resourceId = resourceIdOf(endpoint) || summaryId;
|
|
472
|
+
const category = normalizeValue(endpoint.category);
|
|
473
|
+
const name = String(endpoint.name || '');
|
|
474
|
+
const exclusionReason = endpointExclusionReason(endpoint);
|
|
475
|
+
if (exclusionReason) {
|
|
476
|
+
items.push({
|
|
477
|
+
resourceType: 'endpoint', resourceId, name, category, action: 'skip', metadataField: 'metadata', stableKeyName,
|
|
478
|
+
reason: exclusionReason,
|
|
479
|
+
});
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const rawStableKey = rawConfiguredStableKey(endpoint, stableKeyName);
|
|
483
|
+
if (rawStableKey.present && (typeof rawStableKey.value !== 'string' || !rawStableKey.value.trim())) {
|
|
484
|
+
const reason = `Invalid endpoint stableKey on ${resourceId}: metadata.${stableKeyName} must be a non-empty string.`;
|
|
485
|
+
items.push({ resourceType: 'endpoint', resourceId, name, category, action: 'blocked', metadataField: 'metadata', stableKeyName, reason });
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const existingStableKey = readConfiguredResourceMetadataStableKey(endpoint, stableKeyName);
|
|
489
|
+
const stableKey = existingStableKey && !force ? undefined : buildNamedResourceGeneratedStableKey(endpoint);
|
|
490
|
+
const plannedStableKey = stableKey || existingStableKey || undefined;
|
|
491
|
+
items.push({
|
|
492
|
+
resourceType: 'endpoint', resourceId, name, category,
|
|
493
|
+
action: existingStableKey && !force ? 'skip' : 'update', metadataField: 'metadata', stableKeyName,
|
|
494
|
+
...(plannedStableKey ? { stableKey: plannedStableKey } : {}),
|
|
495
|
+
reason: existingStableKey
|
|
496
|
+
? (force ? 'stable key will be regenerated' : 'stable key already exists')
|
|
497
|
+
: 'stable key missing',
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (shouldTrackResource(trackedResources, 'role-group') && typeof client.listAllRoleGroups === 'function') {
|
|
502
|
+
const { roleGroups } = await client.listAllRoleGroups();
|
|
503
|
+
for (const summary of roleGroups) {
|
|
504
|
+
const summaryId = resourceIdOf(summary);
|
|
505
|
+
if (!summaryId) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const roleGroup = unwrapResource(await client.getRoleGroup(summaryId));
|
|
509
|
+
const resourceId = resourceIdOf(roleGroup) || summaryId;
|
|
510
|
+
const existingStableKey = readConfiguredResourceMetadataStableKey(roleGroup, stableKeyName);
|
|
511
|
+
const stableKey = existingStableKey && !force
|
|
512
|
+
? undefined
|
|
513
|
+
: buildGeneratedStableKey(roleGroup);
|
|
514
|
+
const plannedStableKey = stableKey || existingStableKey || undefined;
|
|
515
|
+
items.push({
|
|
516
|
+
resourceType: 'role-group',
|
|
517
|
+
resourceId,
|
|
518
|
+
name: String(roleGroup.name || ''),
|
|
519
|
+
category: normalizeValue(roleGroup.category),
|
|
520
|
+
action: existingStableKey && !force ? 'skip' : 'update',
|
|
521
|
+
metadataField: 'metadata',
|
|
522
|
+
stableKeyName,
|
|
523
|
+
...(plannedStableKey ? { stableKey: plannedStableKey } : {}),
|
|
524
|
+
reason: existingStableKey
|
|
525
|
+
? (force ? 'stable key will be regenerated' : 'stable key already exists')
|
|
526
|
+
: 'stable key missing',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (shouldTrackResource(trackedResources, 'group') && typeof client.listAllGroups === 'function') {
|
|
531
|
+
const { groups } = await client.listAllGroups();
|
|
532
|
+
for (const summary of groups) {
|
|
533
|
+
const summaryId = resourceIdOf(summary);
|
|
534
|
+
if (!summaryId) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const group = unwrapResource(await client.getGroup(summaryId));
|
|
538
|
+
const resourceId = resourceIdOf(group) || summaryId;
|
|
539
|
+
const existingStableKey = readConfiguredResourceMetadataStableKey(group, stableKeyName);
|
|
540
|
+
const stableKey = existingStableKey && !force
|
|
541
|
+
? undefined
|
|
542
|
+
: buildNamedResourceGeneratedStableKey(group);
|
|
543
|
+
const plannedStableKey = stableKey || existingStableKey || undefined;
|
|
544
|
+
items.push({
|
|
545
|
+
resourceType: 'group',
|
|
546
|
+
resourceId,
|
|
547
|
+
name: String(group.name || ''),
|
|
548
|
+
category: normalizeValue(group.category),
|
|
549
|
+
action: existingStableKey && !force ? 'skip' : 'update',
|
|
550
|
+
metadataField: 'metadata',
|
|
551
|
+
stableKeyName,
|
|
552
|
+
...(plannedStableKey ? { stableKey: plannedStableKey } : {}),
|
|
553
|
+
reason: groupMetadataBackfillReason(group, existingStableKey, force),
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (shouldTrackResource(trackedResources, 'job-template') && typeof client.listAllJobTemplates === 'function') {
|
|
558
|
+
const { jobTemplates } = await client.listAllJobTemplates();
|
|
559
|
+
for (const summary of jobTemplates) {
|
|
560
|
+
const summaryId = resourceIdOf(summary);
|
|
561
|
+
if (!summaryId)
|
|
562
|
+
continue;
|
|
563
|
+
const jobTemplate = unwrapResource(await client.getJobTemplate(summaryId));
|
|
564
|
+
const resourceId = resourceIdOf(jobTemplate) || summaryId;
|
|
565
|
+
const rawStableKey = rawConfiguredStableKey(jobTemplate, stableKeyName);
|
|
566
|
+
if (rawStableKey.present && (typeof rawStableKey.value !== 'string' || !rawStableKey.value.trim())) {
|
|
567
|
+
const reason = `Invalid job-template stableKey on ${resourceId}: metadata.${stableKeyName} must be a non-empty string.`;
|
|
568
|
+
items.push({
|
|
569
|
+
resourceType: 'job-template', resourceId, name: String(jobTemplate.name || ''), category: normalizeValue(jobTemplate.category),
|
|
570
|
+
action: 'blocked', metadataField: 'metadata', stableKeyName, reason,
|
|
571
|
+
});
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const existingStableKey = readConfiguredResourceMetadataStableKey(jobTemplate, stableKeyName);
|
|
575
|
+
const stableKey = existingStableKey && !force ? undefined : buildNamedResourceGeneratedStableKey(jobTemplate);
|
|
576
|
+
const plannedStableKey = stableKey || existingStableKey || undefined;
|
|
577
|
+
items.push({
|
|
578
|
+
resourceType: 'job-template',
|
|
579
|
+
resourceId,
|
|
580
|
+
name: String(jobTemplate.name || ''),
|
|
581
|
+
category: normalizeValue(jobTemplate.category),
|
|
582
|
+
action: existingStableKey && !force ? 'skip' : 'update',
|
|
583
|
+
metadataField: 'metadata',
|
|
584
|
+
stableKeyName,
|
|
585
|
+
...(plannedStableKey ? { stableKey: plannedStableKey } : {}),
|
|
586
|
+
reason: existingStableKey
|
|
587
|
+
? (force ? 'stable key will be regenerated' : 'stable key already exists')
|
|
588
|
+
: 'stable key missing',
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (shouldTrackResource(trackedResources, 'schedule') && typeof client.listAllSchedules === 'function') {
|
|
593
|
+
const { schedules } = await client.listAllSchedules();
|
|
594
|
+
for (const summary of schedules) {
|
|
595
|
+
const summaryId = resourceIdOf(summary);
|
|
596
|
+
if (!summaryId) {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const schedule = unwrapResource(await client.getSchedule(summaryId));
|
|
600
|
+
const resourceId = resourceIdOf(schedule) || summaryId;
|
|
601
|
+
const category = normalizeValue(schedule.category);
|
|
602
|
+
const name = String(schedule.name || '');
|
|
603
|
+
const exclusionReason = scheduleExclusionReason(schedule);
|
|
604
|
+
if (exclusionReason) {
|
|
605
|
+
items.push({
|
|
606
|
+
resourceType: 'schedule', resourceId, name, category, action: 'skip', metadataField: 'metadata', stableKeyName,
|
|
607
|
+
reason: exclusionReason,
|
|
608
|
+
});
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
const rawStableKey = rawConfiguredStableKey(schedule, stableKeyName);
|
|
612
|
+
if (rawStableKey.present && (typeof rawStableKey.value !== 'string' || !rawStableKey.value.trim())) {
|
|
613
|
+
const reason = `Invalid schedule stableKey on ${resourceId}: metadata.${stableKeyName} must be a non-empty string.`;
|
|
614
|
+
items.push({ resourceType: 'schedule', resourceId, name, category, action: 'blocked', metadataField: 'metadata', stableKeyName, reason });
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const existingStableKey = readConfiguredResourceMetadataStableKey(schedule, stableKeyName);
|
|
618
|
+
const stableKey = existingStableKey && !force ? undefined : buildNamedResourceGeneratedStableKey(schedule);
|
|
619
|
+
const plannedStableKey = stableKey || existingStableKey || undefined;
|
|
620
|
+
items.push({
|
|
621
|
+
resourceType: 'schedule', resourceId, name, category,
|
|
622
|
+
action: existingStableKey && !force ? 'skip' : 'update', metadataField: 'metadata', stableKeyName,
|
|
623
|
+
...(plannedStableKey ? { stableKey: plannedStableKey } : {}),
|
|
624
|
+
reason: existingStableKey
|
|
625
|
+
? (force ? 'stable key will be regenerated' : 'stable key already exists')
|
|
626
|
+
: 'stable key missing',
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (shouldTrackResource(trackedResources, 'event') && typeof client.listAllEvents === 'function') {
|
|
631
|
+
const { events } = await client.listAllEvents({ includeDeleted: true });
|
|
632
|
+
for (const summary of events) {
|
|
633
|
+
const summaryId = resourceIdOf(summary);
|
|
634
|
+
if (!summaryId) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
const event = unwrapResource(await client.getEvent(summaryId));
|
|
638
|
+
const resourceId = resourceIdOf(event) || summaryId;
|
|
639
|
+
const category = normalizeValue(event.category);
|
|
640
|
+
const name = String(event.name || '');
|
|
641
|
+
const exclusionReason = eventExclusionReason(event);
|
|
642
|
+
if (exclusionReason) {
|
|
643
|
+
items.push({
|
|
644
|
+
resourceType: 'event', resourceId, name, category, action: 'skip', metadataField: 'metadata', stableKeyName,
|
|
645
|
+
reason: exclusionReason,
|
|
646
|
+
});
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const rawStableKey = rawConfiguredStableKey(event, stableKeyName);
|
|
650
|
+
if (rawStableKey.present && (typeof rawStableKey.value !== 'string' || !rawStableKey.value.trim())) {
|
|
651
|
+
const reason = `Invalid event stableKey on ${resourceId}: metadata.${stableKeyName} must be a non-empty string.`;
|
|
652
|
+
items.push({ resourceType: 'event', resourceId, name, category, action: 'blocked', metadataField: 'metadata', stableKeyName, reason });
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const existingStableKey = readConfiguredResourceMetadataStableKey(event, stableKeyName);
|
|
656
|
+
const stableKey = existingStableKey && !force ? undefined : buildNamedResourceGeneratedStableKey(event);
|
|
657
|
+
const plannedStableKey = stableKey || existingStableKey || undefined;
|
|
658
|
+
items.push({
|
|
659
|
+
resourceType: 'event', resourceId, name, category,
|
|
660
|
+
action: existingStableKey && !force ? 'skip' : 'update', metadataField: 'metadata', stableKeyName,
|
|
661
|
+
...(plannedStableKey ? { stableKey: plannedStableKey } : {}),
|
|
662
|
+
reason: existingStableKey
|
|
663
|
+
? (force ? 'stable key will be regenerated' : 'stable key already exists')
|
|
664
|
+
: 'stable key missing',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const blockers = items.filter((item) => item.action === 'blocked').map((item) => item.reason);
|
|
669
|
+
const itemsByStableKey = new Map();
|
|
670
|
+
for (const item of items) {
|
|
671
|
+
if (item.metadataField !== 'metadata'
|
|
672
|
+
|| !item.stableKey
|
|
673
|
+
|| ((item.resourceType === 'schedule' || item.resourceType === 'event') && item.action === 'skip' && item.reason !== 'stable key already exists')) {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const identity = metadataStableKeyIdentity(item.resourceType, item.stableKey);
|
|
677
|
+
itemsByStableKey.set(identity, [...(itemsByStableKey.get(identity) || []), item]);
|
|
678
|
+
}
|
|
679
|
+
for (const duplicates of itemsByStableKey.values()) {
|
|
680
|
+
if (duplicates.length < 2) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
const stableKey = duplicates[0].stableKey;
|
|
684
|
+
const resourceType = duplicates[0].resourceType;
|
|
685
|
+
const reason = `Duplicate ${resourceType} stableKey "${stableKey}" on ${envName}.`;
|
|
686
|
+
blockers.push(reason);
|
|
687
|
+
for (const item of duplicates) {
|
|
688
|
+
item.action = 'blocked';
|
|
689
|
+
item.reason = reason;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
items.sort((left, right) => left.resourceType.localeCompare(right.resourceType)
|
|
693
|
+
|| buildStableKey({ name: left.name, category: left.category }).localeCompare(buildStableKey({ name: right.name, category: right.category }))
|
|
694
|
+
|| left.resourceId.localeCompare(right.resourceId));
|
|
695
|
+
const counts = {
|
|
696
|
+
update: items.filter((item) => item.action === 'update').length,
|
|
697
|
+
skip: items.filter((item) => item.action === 'skip').length,
|
|
698
|
+
blocked: items.filter((item) => item.action === 'blocked').length,
|
|
699
|
+
};
|
|
700
|
+
return {
|
|
701
|
+
schemaVersion: 1,
|
|
702
|
+
generatedAt: new Date().toISOString(),
|
|
703
|
+
env: envName,
|
|
704
|
+
baseUrl: client.baseUrl,
|
|
705
|
+
instance: client.instance,
|
|
706
|
+
stableKeyName,
|
|
707
|
+
trackedResources,
|
|
708
|
+
force,
|
|
709
|
+
...(skipPartitions ? { skipPartitions: true } : {}),
|
|
710
|
+
counts,
|
|
711
|
+
blockers: [...new Set(blockers)].sort((left, right) => left.localeCompare(right)),
|
|
712
|
+
items,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
export async function applyMetadataBackfillPlan(input) {
|
|
716
|
+
const { client, plan } = input;
|
|
717
|
+
if (client.baseUrl !== plan.baseUrl || client.instance !== plan.instance) {
|
|
718
|
+
throw new Error(`Metadata plan target ${plan.baseUrl} (${plan.instance}) does not match selected target ${client.baseUrl} (${client.instance || 'unknown'}).`);
|
|
719
|
+
}
|
|
720
|
+
if (plan.blockers.length > 0) {
|
|
721
|
+
throw new Error(`Metadata plan has ${plan.blockers.length} blocking validation error(s): ${plan.blockers.join(' ')}`);
|
|
722
|
+
}
|
|
723
|
+
if (plan.items.some((item) => item.action === 'blocked')) {
|
|
724
|
+
throw new Error('Metadata plan contains blocked items and cannot be applied.');
|
|
725
|
+
}
|
|
726
|
+
const currentResources = new Map();
|
|
727
|
+
const resourceTypes = new Set(plan.items.map((item) => item.resourceType));
|
|
728
|
+
if (resourceTypes.has('component')) {
|
|
729
|
+
const { components } = await client.listAllComponents();
|
|
730
|
+
for (const summary of components) {
|
|
731
|
+
const id = resourceIdOf(summary);
|
|
732
|
+
if (id)
|
|
733
|
+
currentResources.set(`component:${id}`, unwrapResource(await client.getComponent(id)));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (resourceTypes.has('database-schema')) {
|
|
737
|
+
const { databases } = await client.listAllDatabaseSchemas({
|
|
738
|
+
projection: 'pull',
|
|
739
|
+
skipPartitions: plan.skipPartitions,
|
|
740
|
+
});
|
|
741
|
+
for (const database of databases) {
|
|
742
|
+
if (isDatabaseBackupArtifact(database)) {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const id = resourceIdOf(database);
|
|
746
|
+
if (id) {
|
|
747
|
+
currentResources.set(`database-schema:${id}`, database);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (resourceTypes.has('database-view')) {
|
|
752
|
+
const views = await client.listAllDatabaseViewsForSync();
|
|
753
|
+
for (const view of views) {
|
|
754
|
+
const id = resourceIdOf(view);
|
|
755
|
+
if (id) {
|
|
756
|
+
currentResources.set(`database-view:${id}`, view);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (resourceTypes.has('endpoint')) {
|
|
761
|
+
const { endpoints } = await client.listAllEndpoints();
|
|
762
|
+
for (const summary of endpoints) {
|
|
763
|
+
const id = resourceIdOf(summary);
|
|
764
|
+
if (id) {
|
|
765
|
+
currentResources.set(`endpoint:${id}`, unwrapResource(await client.getEndpoint(id)));
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (resourceTypes.has('role-group')) {
|
|
770
|
+
const { roleGroups } = await client.listAllRoleGroups();
|
|
771
|
+
for (const summary of roleGroups) {
|
|
772
|
+
const id = resourceIdOf(summary);
|
|
773
|
+
if (id)
|
|
774
|
+
currentResources.set(`role-group:${id}`, unwrapResource(await client.getRoleGroup(id)));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (resourceTypes.has('group')) {
|
|
778
|
+
const { groups } = await client.listAllGroups();
|
|
779
|
+
for (const summary of groups) {
|
|
780
|
+
const id = resourceIdOf(summary);
|
|
781
|
+
if (id) {
|
|
782
|
+
currentResources.set(`group:${id}`, unwrapResource(await client.getGroup(id)));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (resourceTypes.has('job-template')) {
|
|
787
|
+
const { jobTemplates } = await client.listAllJobTemplates();
|
|
788
|
+
for (const summary of jobTemplates) {
|
|
789
|
+
const id = resourceIdOf(summary);
|
|
790
|
+
if (id)
|
|
791
|
+
currentResources.set(`job-template:${id}`, unwrapResource(await client.getJobTemplate(id)));
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (resourceTypes.has('schedule')) {
|
|
795
|
+
const { schedules } = await client.listAllSchedules();
|
|
796
|
+
for (const summary of schedules) {
|
|
797
|
+
const id = resourceIdOf(summary);
|
|
798
|
+
if (id) {
|
|
799
|
+
currentResources.set(`schedule:${id}`, unwrapResource(await client.getSchedule(id)));
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (resourceTypes.has('event')) {
|
|
804
|
+
const { events } = await client.listAllEvents({ includeDeleted: true });
|
|
805
|
+
for (const summary of events) {
|
|
806
|
+
const id = resourceIdOf(summary);
|
|
807
|
+
if (id) {
|
|
808
|
+
currentResources.set(`event:${id}`, unwrapResource(await client.getEvent(id)));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const stableKeyOwners = new Map();
|
|
813
|
+
for (const [identity, resource] of currentResources) {
|
|
814
|
+
const resourceType = identity.slice(0, identity.indexOf(':'));
|
|
815
|
+
if (resourceType === 'schedule' && scheduleExclusionReason(resource)) {
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
if (resourceType === 'event' && eventExclusionReason(resource)) {
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (resourceType === 'endpoint' && endpointExclusionReason(resource)) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
const stableKey = readConfiguredResourceMetadataStableKey(resource, plan.stableKeyName);
|
|
825
|
+
if (!stableKey)
|
|
826
|
+
continue;
|
|
827
|
+
const key = metadataStableKeyIdentity(resourceType, stableKey);
|
|
828
|
+
if (stableKeyOwners.has(key))
|
|
829
|
+
throw new Error(`Duplicate ${resourceType} stableKey "${stableKey}" detected before apply.`);
|
|
830
|
+
stableKeyOwners.set(key, identity);
|
|
831
|
+
}
|
|
832
|
+
for (const item of plan.items.filter((candidate) => candidate.action === 'update' && candidate.stableKey)) {
|
|
833
|
+
if (item.metadataField !== 'metadata') {
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
const owner = stableKeyOwners.get(metadataStableKeyIdentity(item.resourceType, item.stableKey));
|
|
837
|
+
if (owner && owner !== `${item.resourceType}:${item.resourceId}`) {
|
|
838
|
+
throw new Error(`Planned ${item.resourceType} stableKey "${item.stableKey}" is already assigned to another resource.`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (!plan.force) {
|
|
842
|
+
for (const item of plan.items.filter((candidate) => (candidate.resourceType === 'endpoint'
|
|
843
|
+
|| candidate.resourceType === 'job-template'
|
|
844
|
+
|| candidate.resourceType === 'schedule'
|
|
845
|
+
|| candidate.resourceType === 'event') && candidate.action === 'update')) {
|
|
846
|
+
const resource = currentResources.get(`${item.resourceType}:${item.resourceId}`);
|
|
847
|
+
if (!resource) {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
const rawStableKey = rawConfiguredStableKey(resource, plan.stableKeyName);
|
|
851
|
+
if (rawStableKey.present && (typeof rawStableKey.value !== 'string' || !rawStableKey.value.trim())) {
|
|
852
|
+
throw new Error(`Invalid ${item.resourceType} stableKey appeared before apply on ${item.resourceId}; refusing to overwrite it without --force.`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
const results = [];
|
|
857
|
+
let databaseMutationStarted = false;
|
|
858
|
+
const columnUpdatesByDatabase = new Map();
|
|
859
|
+
for (const item of plan.items) {
|
|
860
|
+
if (item.resourceType === 'database-schema' && item.metadataField === 'definition[].metadata' && item.action === 'update') {
|
|
861
|
+
columnUpdatesByDatabase.set(item.resourceId, [
|
|
862
|
+
...(columnUpdatesByDatabase.get(item.resourceId) || []),
|
|
863
|
+
item,
|
|
864
|
+
]);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const processedColumnUpdateDatabases = new Set();
|
|
868
|
+
for (const item of plan.items) {
|
|
869
|
+
if (item.action === 'skip') {
|
|
870
|
+
results.push({
|
|
871
|
+
resourceType: item.resourceType,
|
|
872
|
+
resourceId: item.resourceId,
|
|
873
|
+
...(item.columnName ? { columnName: item.columnName } : {}),
|
|
874
|
+
...(item.stableKey ? { stableKey: item.stableKey } : {}),
|
|
875
|
+
action: 'skip',
|
|
876
|
+
});
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (item.action !== 'update') {
|
|
880
|
+
throw new Error(`Unsupported metadata plan action: ${item.action}.`);
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
if (item.resourceType === 'component') {
|
|
884
|
+
const component = currentResources.get(`component:${item.resourceId}`);
|
|
885
|
+
if (!component)
|
|
886
|
+
throw new Error('component no longer exists');
|
|
887
|
+
const currentStableKey = readConfiguredResourceMetadataStableKey(component, plan.stableKeyName);
|
|
888
|
+
if (currentStableKey && !plan.force) {
|
|
889
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey: currentStableKey, action: 'skip' });
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
const stableKey = item.stableKey || buildGeneratedStableKey(component);
|
|
893
|
+
await client.updateComponent(item.resourceId, buildMetadataUpdatePayload(component, plan.stableKeyName, stableKey, item.resourceType));
|
|
894
|
+
results.push({
|
|
895
|
+
resourceType: item.resourceType,
|
|
896
|
+
resourceId: item.resourceId,
|
|
897
|
+
stableKey,
|
|
898
|
+
action: 'update',
|
|
899
|
+
});
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (item.resourceType === 'database-schema') {
|
|
903
|
+
const listed = currentResources.get(`database-schema:${item.resourceId}`);
|
|
904
|
+
if (!listed) {
|
|
905
|
+
throw new Error('database schema no longer exists');
|
|
906
|
+
}
|
|
907
|
+
if (item.metadataField === 'definition[].metadata' && processedColumnUpdateDatabases.has(item.resourceId)) {
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const live = unwrapResource(await client.getDatabaseSchema(item.resourceId));
|
|
911
|
+
if (item.metadataField === 'definition[].metadata') {
|
|
912
|
+
if (isDatabaseSchemaPartition(live)) {
|
|
913
|
+
throw new Error('database schema became a partition; column stable keys are inherited from the root');
|
|
914
|
+
}
|
|
915
|
+
const assignments = [];
|
|
916
|
+
for (const columnItem of columnUpdatesByDatabase.get(item.resourceId) || []) {
|
|
917
|
+
const columnName = columnItem.columnName;
|
|
918
|
+
const columnDefinitionId = columnItem.columnDefinitionId;
|
|
919
|
+
if (!columnName || !columnDefinitionId) {
|
|
920
|
+
throw new Error(`database schema column metadata plan for ${item.resourceId} is missing its live identity`);
|
|
921
|
+
}
|
|
922
|
+
const definitions = databaseDefinitions(live).filter((candidate) => candidate.databaseDefinitionId === columnDefinitionId);
|
|
923
|
+
if (definitions.length !== 1 || definitions[0].name !== columnName) {
|
|
924
|
+
throw new Error(`database schema column "${columnName}" no longer matches databaseDefinitionId "${columnDefinitionId}"`);
|
|
925
|
+
}
|
|
926
|
+
const definition = definitions[0];
|
|
927
|
+
const rawColumnStableKey = columnStableKeyValue(definition, plan.stableKeyName);
|
|
928
|
+
if (typeof rawColumnStableKey.value === 'string' && rawColumnStableKey.value.trim()) {
|
|
929
|
+
results.push({
|
|
930
|
+
resourceType: columnItem.resourceType,
|
|
931
|
+
resourceId: columnItem.resourceId,
|
|
932
|
+
columnName,
|
|
933
|
+
stableKey: rawColumnStableKey.value,
|
|
934
|
+
action: 'skip',
|
|
935
|
+
});
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (rawColumnStableKey.present) {
|
|
939
|
+
throw new Error(`Invalid database column stableKey appeared before apply on ${item.resourceId}.${columnName}; refusing to overwrite it.`);
|
|
940
|
+
}
|
|
941
|
+
assignments.push({
|
|
942
|
+
item: columnItem,
|
|
943
|
+
definition,
|
|
944
|
+
stableKey: columnItem.stableKey || buildGeneratedDatabaseColumnStableKey(live, columnName),
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
processedColumnUpdateDatabases.add(item.resourceId);
|
|
948
|
+
if (assignments.length === 0) {
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
if (databaseMutationStarted) {
|
|
952
|
+
await waitForMutationSlot();
|
|
953
|
+
}
|
|
954
|
+
databaseMutationStarted = true;
|
|
955
|
+
await client.updateDatabaseSchema(item.resourceId, buildDatabaseColumnMetadataUpdatePayload({
|
|
956
|
+
database: live,
|
|
957
|
+
assignments,
|
|
958
|
+
stableKeyName: plan.stableKeyName,
|
|
959
|
+
}));
|
|
960
|
+
for (const assignment of assignments) {
|
|
961
|
+
results.push({
|
|
962
|
+
resourceType: assignment.item.resourceType,
|
|
963
|
+
resourceId: assignment.item.resourceId,
|
|
964
|
+
columnName: assignment.item.columnName,
|
|
965
|
+
stableKey: assignment.stableKey,
|
|
966
|
+
action: 'update',
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
const currentStableKey = readConfiguredResourceMetadataStableKey(live, plan.stableKeyName);
|
|
972
|
+
if (currentStableKey) {
|
|
973
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey: currentStableKey, action: 'skip' });
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const rawStableKey = rawConfiguredStableKey(live, plan.stableKeyName);
|
|
977
|
+
if (rawStableKey.present) {
|
|
978
|
+
throw new Error(`Invalid database-schema stableKey appeared before apply on ${item.resourceId}; refusing to overwrite it.`);
|
|
979
|
+
}
|
|
980
|
+
if (databaseMutationStarted) {
|
|
981
|
+
await waitForMutationSlot();
|
|
982
|
+
}
|
|
983
|
+
databaseMutationStarted = true;
|
|
984
|
+
const stableKey = item.stableKey || buildNamedResourceGeneratedStableKey(live);
|
|
985
|
+
await client.updateDatabaseSchemaMetadata(item.resourceId, buildMetadataUpdatePayload(live, plan.stableKeyName, stableKey, item.resourceType));
|
|
986
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey, action: 'update' });
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if (item.resourceType === 'database-view') {
|
|
990
|
+
const view = currentResources.get(`database-view:${item.resourceId}`);
|
|
991
|
+
if (!view) {
|
|
992
|
+
throw new Error('database view no longer exists');
|
|
993
|
+
}
|
|
994
|
+
const currentStableKey = readConfiguredResourceMetadataStableKey(view, plan.stableKeyName);
|
|
995
|
+
if (currentStableKey) {
|
|
996
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey: currentStableKey, action: 'skip' });
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
const rawStableKey = rawConfiguredStableKey(view, plan.stableKeyName);
|
|
1000
|
+
if (rawStableKey.present) {
|
|
1001
|
+
throw new Error(`Invalid database-view stableKey appeared before apply on ${item.resourceId}; refusing to overwrite it.`);
|
|
1002
|
+
}
|
|
1003
|
+
const stableKey = item.stableKey || buildNamedResourceGeneratedStableKey(view);
|
|
1004
|
+
await client.updateDatabaseView(item.resourceId, buildDatabaseViewMetadataUpdatePayload(view, plan.stableKeyName, stableKey));
|
|
1005
|
+
const verified = unwrapResource(await client.getDatabaseView(item.resourceId));
|
|
1006
|
+
const verifiedStableKey = readConfiguredResourceMetadataStableKey(verified, plan.stableKeyName);
|
|
1007
|
+
if (verifiedStableKey !== stableKey) {
|
|
1008
|
+
throw new Error('updated database view did not retain the planned stable key');
|
|
1009
|
+
}
|
|
1010
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey, action: 'update' });
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
if (item.resourceType === 'endpoint') {
|
|
1014
|
+
const endpoint = currentResources.get(`endpoint:${item.resourceId}`);
|
|
1015
|
+
if (!endpoint) {
|
|
1016
|
+
throw new Error('endpoint no longer exists');
|
|
1017
|
+
}
|
|
1018
|
+
const exclusionReason = endpointExclusionReason(endpoint);
|
|
1019
|
+
if (exclusionReason) {
|
|
1020
|
+
throw new Error(`endpoint moved outside migration scope: ${exclusionReason}`);
|
|
1021
|
+
}
|
|
1022
|
+
const currentStableKey = readConfiguredResourceMetadataStableKey(endpoint, plan.stableKeyName);
|
|
1023
|
+
if (currentStableKey && !plan.force) {
|
|
1024
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey: currentStableKey, action: 'skip' });
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
const stableKey = item.stableKey || buildNamedResourceGeneratedStableKey(endpoint);
|
|
1028
|
+
await client.updateEndpoint(item.resourceId, buildMetadataUpdatePayload(endpoint, plan.stableKeyName, stableKey, item.resourceType));
|
|
1029
|
+
const verifiedEndpoint = unwrapResource(await client.getEndpoint(item.resourceId));
|
|
1030
|
+
const verifiedStableKey = readConfiguredResourceMetadataStableKey(verifiedEndpoint, plan.stableKeyName);
|
|
1031
|
+
if (verifiedStableKey !== stableKey) {
|
|
1032
|
+
throw new Error('updated endpoint did not retain the planned stable key');
|
|
1033
|
+
}
|
|
1034
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey, action: 'update' });
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (item.resourceType === 'role-group') {
|
|
1038
|
+
const roleGroup = currentResources.get(`role-group:${item.resourceId}`);
|
|
1039
|
+
if (!roleGroup)
|
|
1040
|
+
throw new Error('role-group no longer exists');
|
|
1041
|
+
const currentStableKey = readConfiguredResourceMetadataStableKey(roleGroup, plan.stableKeyName);
|
|
1042
|
+
if (currentStableKey && !plan.force) {
|
|
1043
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey: currentStableKey, action: 'skip' });
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
const stableKey = item.stableKey || buildGeneratedStableKey(roleGroup);
|
|
1047
|
+
await client.updateRoleGroup(item.resourceId, buildMetadataUpdatePayload(roleGroup, plan.stableKeyName, stableKey, item.resourceType));
|
|
1048
|
+
results.push({
|
|
1049
|
+
resourceType: item.resourceType,
|
|
1050
|
+
resourceId: item.resourceId,
|
|
1051
|
+
stableKey,
|
|
1052
|
+
action: 'update',
|
|
1053
|
+
});
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
if (item.resourceType === 'group') {
|
|
1057
|
+
const group = currentResources.get(`group:${item.resourceId}`);
|
|
1058
|
+
if (!group) {
|
|
1059
|
+
throw new Error('group no longer exists');
|
|
1060
|
+
}
|
|
1061
|
+
const currentStableKey = readConfiguredResourceMetadataStableKey(group, plan.stableKeyName);
|
|
1062
|
+
if (currentStableKey && !plan.force) {
|
|
1063
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey: currentStableKey, action: 'skip' });
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
const stableKey = item.stableKey || buildNamedResourceGeneratedStableKey(group);
|
|
1067
|
+
await client.updateGroup(item.resourceId, buildMetadataUpdatePayload(group, plan.stableKeyName, stableKey, item.resourceType));
|
|
1068
|
+
results.push({
|
|
1069
|
+
resourceType: item.resourceType,
|
|
1070
|
+
resourceId: item.resourceId,
|
|
1071
|
+
stableKey,
|
|
1072
|
+
action: 'update',
|
|
1073
|
+
});
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (item.resourceType === 'job-template') {
|
|
1077
|
+
const jobTemplate = currentResources.get(`job-template:${item.resourceId}`);
|
|
1078
|
+
if (!jobTemplate)
|
|
1079
|
+
throw new Error('job-template no longer exists');
|
|
1080
|
+
const currentStableKey = readConfiguredResourceMetadataStableKey(jobTemplate, plan.stableKeyName);
|
|
1081
|
+
if (currentStableKey && !plan.force) {
|
|
1082
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey: currentStableKey, action: 'skip' });
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
const stableKey = item.stableKey || buildNamedResourceGeneratedStableKey(jobTemplate);
|
|
1086
|
+
await client.updateJobTemplate(item.resourceId, buildJobTemplateMetadataUpdatePayload(jobTemplate, plan.stableKeyName, stableKey));
|
|
1087
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey, action: 'update' });
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
if (item.resourceType === 'schedule') {
|
|
1091
|
+
const schedule = currentResources.get(`schedule:${item.resourceId}`);
|
|
1092
|
+
if (!schedule) {
|
|
1093
|
+
throw new Error('schedule no longer exists');
|
|
1094
|
+
}
|
|
1095
|
+
const exclusionReason = scheduleExclusionReason(schedule);
|
|
1096
|
+
if (exclusionReason) {
|
|
1097
|
+
throw new Error(`schedule moved outside migration scope: ${exclusionReason}`);
|
|
1098
|
+
}
|
|
1099
|
+
const currentStableKey = readConfiguredResourceMetadataStableKey(schedule, plan.stableKeyName);
|
|
1100
|
+
if (currentStableKey && !plan.force) {
|
|
1101
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey: currentStableKey, action: 'skip' });
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
const stableKey = item.stableKey || buildNamedResourceGeneratedStableKey(schedule);
|
|
1105
|
+
await client.updateSchedule(item.resourceId, buildMetadataUpdatePayload(schedule, plan.stableKeyName, stableKey, item.resourceType));
|
|
1106
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey, action: 'update' });
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (item.resourceType === 'event') {
|
|
1110
|
+
const event = currentResources.get(`event:${item.resourceId}`);
|
|
1111
|
+
if (!event) {
|
|
1112
|
+
throw new Error('event no longer exists');
|
|
1113
|
+
}
|
|
1114
|
+
const exclusionReason = eventExclusionReason(event);
|
|
1115
|
+
if (exclusionReason) {
|
|
1116
|
+
throw new Error(`event moved outside migration scope: ${exclusionReason}`);
|
|
1117
|
+
}
|
|
1118
|
+
const currentStableKey = readConfiguredResourceMetadataStableKey(event, plan.stableKeyName);
|
|
1119
|
+
if (currentStableKey && !plan.force) {
|
|
1120
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey: currentStableKey, action: 'skip' });
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
const stableKey = item.stableKey || buildNamedResourceGeneratedStableKey(event);
|
|
1124
|
+
await client.updateEvent(item.resourceId, buildMetadataUpdatePayload(event, plan.stableKeyName, stableKey, item.resourceType));
|
|
1125
|
+
results.push({ resourceType: item.resourceType, resourceId: item.resourceId, stableKey, action: 'update' });
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
throw new Error(`unsupported resource type: ${item.resourceType}`);
|
|
1129
|
+
}
|
|
1130
|
+
catch (error) {
|
|
1131
|
+
results.push({
|
|
1132
|
+
resourceType: item.resourceType,
|
|
1133
|
+
resourceId: item.resourceId,
|
|
1134
|
+
stableKey: item.stableKey,
|
|
1135
|
+
...(item.columnName ? { columnName: item.columnName } : {}),
|
|
1136
|
+
action: 'failed',
|
|
1137
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (resourceTypes.has('database-schema')) {
|
|
1142
|
+
try {
|
|
1143
|
+
const { databases } = await client.listAllDatabaseSchemas({
|
|
1144
|
+
projection: 'pull',
|
|
1145
|
+
skipPartitions: plan.skipPartitions,
|
|
1146
|
+
});
|
|
1147
|
+
const verified = new Map(databases
|
|
1148
|
+
.filter((database) => !isDatabaseBackupArtifact(database))
|
|
1149
|
+
.map((database) => [resourceIdOf(database), database]));
|
|
1150
|
+
for (const result of results.filter((candidate) => candidate.resourceType === 'database-schema' && candidate.action === 'update')) {
|
|
1151
|
+
const database = verified.get(result.resourceId);
|
|
1152
|
+
const definition = result.columnName && database
|
|
1153
|
+
? databaseDefinitions(database).find((candidate) => candidate.name === result.columnName)
|
|
1154
|
+
: undefined;
|
|
1155
|
+
const stableKey = result.columnName
|
|
1156
|
+
? (definition
|
|
1157
|
+
? columnStableKeyValue(definition, plan.stableKeyName).value
|
|
1158
|
+
: null)
|
|
1159
|
+
: (database
|
|
1160
|
+
? readConfiguredResourceMetadataStableKey(database, plan.stableKeyName)
|
|
1161
|
+
: null);
|
|
1162
|
+
if (stableKey !== result.stableKey) {
|
|
1163
|
+
result.action = 'failed';
|
|
1164
|
+
result.error = 'final verification did not find the assigned stable key';
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
catch (error) {
|
|
1169
|
+
for (const result of results.filter((candidate) => candidate.resourceType === 'database-schema' && candidate.action === 'update')) {
|
|
1170
|
+
result.action = 'failed';
|
|
1171
|
+
result.error = `final verification failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return results;
|
|
1176
|
+
}
|