@syntax-syllogism/warden-core 0.2.0 → 0.3.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/README.md +26 -4
- package/conformance/README.md +21 -0
- package/conformance/fixtures/01-single-persona.json +34 -0
- package/conformance/fixtures/02-multi-persona-union.json +32 -0
- package/conformance/fixtures/03-duplicate-grant-dedup.json +24 -0
- package/conformance/fixtures/04-already-present-noop.json +15 -0
- package/conformance/fixtures/05-unresolved-reference.json +17 -0
- package/conformance/fixtures/06-multi-user.json +28 -0
- package/conformance/fixtures/07-deterministic-ordering.json +39 -0
- package/conformance/fixtures/08-category-specific-reference-keys.json +42 -0
- package/lib/commandOptions.d.ts +84 -0
- package/lib/commandOptions.js +71 -0
- package/lib/commandOptions.js.map +1 -0
- package/lib/index.d.ts +10 -3
- package/lib/index.js +6 -3
- package/lib/index.js.map +1 -1
- package/lib/lifecycle/errors.d.ts +1 -1
- package/lib/lifecycle/errors.js.map +1 -1
- package/lib/lifecycle/freezeState.d.ts +0 -16
- package/lib/lifecycle/freezeState.js +0 -73
- package/lib/lifecycle/freezeState.js.map +1 -1
- package/lib/lifecycle/stripPlan.d.ts +2 -13
- package/lib/lifecycle/stripPlan.js +2 -53
- package/lib/lifecycle/stripPlan.js.map +1 -1
- package/lib/matching/index.js +8 -8
- package/lib/matching/index.js.map +1 -1
- package/lib/provisioning/conformancePlan.d.ts +7 -0
- package/lib/provisioning/conformancePlan.js +135 -0
- package/lib/provisioning/conformancePlan.js.map +1 -0
- package/lib/provisioning/provisionUserUseCase.d.ts +0 -1
- package/lib/provisioning/provisionUserUseCase.js +0 -5
- package/lib/provisioning/provisionUserUseCase.js.map +1 -1
- package/lib/provisioning/userPlan.js +2 -2
- package/lib/provisioning/userPlan.js.map +1 -1
- package/lib/provisioning/userSave.d.ts +2 -1
- package/lib/provisioning/userSave.js +10 -6
- package/lib/provisioning/userSave.js.map +1 -1
- package/lib/relatedRecords/apply.d.ts +1 -1
- package/lib/relatedRecords/apply.js +5 -4
- package/lib/relatedRecords/apply.js.map +1 -1
- package/lib/relatedRecords/preflight.d.ts +2 -4
- package/lib/relatedRecords/preflight.js +2 -4
- package/lib/relatedRecords/preflight.js.map +1 -1
- package/lib/spec/conformanceFixture.d.ts +209 -0
- package/lib/spec/conformanceFixture.js +77 -0
- package/lib/spec/conformanceFixture.js.map +1 -0
- package/lib/spec/index.d.ts +3 -1
- package/lib/spec/index.js +2 -1
- package/lib/spec/index.js.map +1 -1
- package/lib/spec/parse.d.ts +4 -0
- package/lib/spec/parse.js +4 -0
- package/lib/spec/parse.js.map +1 -1
- package/lib/useCase.d.ts +45 -0
- package/lib/useCase.js +31 -0
- package/lib/useCase.js.map +1 -0
- package/lib/useCases.d.ts +152 -0
- package/lib/useCases.js +698 -0
- package/lib/useCases.js.map +1 -0
- package/package.json +1 -1
- package/schemas/conformance-fixture.schema.json +444 -0
- package/lib/shared/prompt.d.ts +0 -4
- package/lib/shared/prompt.js +0 -15
- package/lib/shared/prompt.js.map +0 -1
package/lib/useCases.js
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import { getResolver } from './access/resolvers/index.js';
|
|
2
|
+
import { resolveReverseAccess } from './access/reverse.js';
|
|
3
|
+
import { describeUserFields } from './shared/userFields.js';
|
|
4
|
+
import { parseUserFlag, resolveTargetField, resolveTargets, extractDefTargets } from './lifecycle/targeting.js';
|
|
5
|
+
import { lifecycleMessage } from './lifecycle/messages.js';
|
|
6
|
+
import { LifecycleError } from './lifecycle/errors.js';
|
|
7
|
+
import { failedResult, makeNotice, resolvedTargetResult, summarizeLifecycle } from './lifecycle/output.js';
|
|
8
|
+
import { loadAssignmentState } from './lifecycle/assignmentState.js';
|
|
9
|
+
import { runAssignmentCreates, runRecordUpdate } from './lifecycle/dmlRunner.js';
|
|
10
|
+
import { buildSnapshotFile } from './lifecycle/snapshotState.js';
|
|
11
|
+
import { parseSnapshot } from './spec/parse.js';
|
|
12
|
+
import { buildTargetState } from './lifecycle/stripPlan.js';
|
|
13
|
+
import { applyStripState } from './lifecycle/stripApply.js';
|
|
14
|
+
import { FREEZE, UNFREEZE } from './lifecycle/freezeState.js';
|
|
15
|
+
import { provisionOptionsSchema, freezeOptionsSchema, unfreezeOptionsSchema, stripOptionsSchema, diffOptionsSchema, accessOptionsSchema, snapshotOptionsSchema, restoreOptionsSchema, } from './commandOptions.js';
|
|
16
|
+
import { checkCancelled, endProgress, startProgress, } from './useCase.js';
|
|
17
|
+
import { resolveDefinitions } from './provisioning/definitionReader.js';
|
|
18
|
+
import { buildUserPlans } from './provisioning/userPlan.js';
|
|
19
|
+
import { resolveReferences } from './provisioning/referenceResolution.js';
|
|
20
|
+
import { validateAndCanonicalizeUsers, validateExternalIdFieldForFlag, validatePersonaModes, } from './provisioning/planner.js';
|
|
21
|
+
import { getExistingUsers } from './provisioning/provisionUserUseCase.js';
|
|
22
|
+
import { calculateUserLicenseUsage } from './provisioning/licenseUsage.js';
|
|
23
|
+
import { applySavedPlan, executeBulkUserSaves, planDryRunResult, markRelatedUnapplied, } from './provisioning/userSave.js';
|
|
24
|
+
import { runRelatedPreflight, emptyPreflightResult } from './relatedRecords/preflight.js';
|
|
25
|
+
import { applyRelatedPhase } from './relatedRecords/apply.js';
|
|
26
|
+
import { buildRelatedPlans } from './relatedRecords/plan.js';
|
|
27
|
+
import { assertValidRelatedCatalog } from './relatedRecords/catalog.js';
|
|
28
|
+
import { deriveMyDomain } from './provisioning/planner.js';
|
|
29
|
+
import { provisioningMessage } from './provisioning/messages.js';
|
|
30
|
+
import { summarize, toUserResult } from './provisioning/userPlan.js';
|
|
31
|
+
import { soqlIn } from './shared/sfUtils.js';
|
|
32
|
+
const descriptor = (id, title, destructive, optionsSchema) => ({
|
|
33
|
+
id,
|
|
34
|
+
title,
|
|
35
|
+
group: 'User Lifecycle',
|
|
36
|
+
destructive,
|
|
37
|
+
optionsSchema,
|
|
38
|
+
});
|
|
39
|
+
const runRead = async (ctx, work) => {
|
|
40
|
+
startProgress(ctx);
|
|
41
|
+
const result = await work();
|
|
42
|
+
endProgress(ctx);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
const conformanceMessage = (key, args = []) => {
|
|
46
|
+
const templates = {
|
|
47
|
+
'verify.violation.notFound': 'user not found',
|
|
48
|
+
'verify.violation.error': 'error: %s',
|
|
49
|
+
'verify.violation.missing': '%s missing: %s',
|
|
50
|
+
'verify.violation.extra': '%s extra (sync): %s',
|
|
51
|
+
'verify.violation.profile': 'profile mismatch: %s -> %s',
|
|
52
|
+
'verify.violation.role': 'role mismatch: %s -> %s',
|
|
53
|
+
};
|
|
54
|
+
return (templates[key] ?? key).replaceAll('%s', () => args.shift() ?? '');
|
|
55
|
+
};
|
|
56
|
+
const targetRequests = async (conn, options, fieldMap) => {
|
|
57
|
+
if (options.user) {
|
|
58
|
+
const parsed = parseUserFlag(options.user);
|
|
59
|
+
const field = resolveTargetField(parsed.field, fieldMap);
|
|
60
|
+
if (!field)
|
|
61
|
+
throw new Error(lifecycleMessage('errorInvalidUserMatchField', [parsed.field]));
|
|
62
|
+
return { requests: [{ key: `${field}:${parsed.value}`, field, value: parsed.value, order: 0 }], errors: [] };
|
|
63
|
+
}
|
|
64
|
+
if (options.usersDoc)
|
|
65
|
+
return extractDefTargets(options.usersDoc, options.externalId, fieldMap);
|
|
66
|
+
if (!options.usersPath)
|
|
67
|
+
throw new Error('A user or users definition is required.');
|
|
68
|
+
const { buildTargetRequests } = await import('./lifecycle/targeting.js');
|
|
69
|
+
return buildTargetRequests({
|
|
70
|
+
user: undefined,
|
|
71
|
+
'users-def': options.usersPath,
|
|
72
|
+
'external-id': options.externalId,
|
|
73
|
+
'input-format': options.inputFormat,
|
|
74
|
+
'csv-list-delimiter': options.csvListDelimiter,
|
|
75
|
+
}, fieldMap);
|
|
76
|
+
};
|
|
77
|
+
const resolveTargetSelection = async (conn, options, fieldMap) => {
|
|
78
|
+
const requested = await targetRequests(conn, options, fieldMap);
|
|
79
|
+
const resolved = await resolveTargets(conn, requested.requests, fieldMap);
|
|
80
|
+
return { targets: resolved.targets, errors: requested.errors.concat(resolved.errors) };
|
|
81
|
+
};
|
|
82
|
+
const freezePlan = async (conn, options, direction, ctx) => {
|
|
83
|
+
checkCancelled(ctx);
|
|
84
|
+
const fieldMap = await describeUserFields(conn);
|
|
85
|
+
const selection = await resolveTargetSelection(conn, options, fieldMap);
|
|
86
|
+
const targets = selection.targets;
|
|
87
|
+
const rows = targets.length
|
|
88
|
+
? (await conn.query(`SELECT Id, UserId, IsFrozen FROM UserLogin WHERE UserId IN (${soqlIn(targets.map((target) => target.Id))})`)).records
|
|
89
|
+
: [];
|
|
90
|
+
const byUserId = new Map(rows.map((row) => [row.UserId, row]));
|
|
91
|
+
const users = selection.errors.map(failedResult);
|
|
92
|
+
const updates = [];
|
|
93
|
+
for (const target of targets) {
|
|
94
|
+
checkCancelled(ctx);
|
|
95
|
+
const login = byUserId.get(target.Id);
|
|
96
|
+
const result = resolvedTargetResult(target, login);
|
|
97
|
+
if (!login)
|
|
98
|
+
result.warnings.push(lifecycleMessage('warningMissingUserLogin'));
|
|
99
|
+
else if (direction.isAlreadyInState(login))
|
|
100
|
+
result.actions.push(makeNotice(direction.alreadyKey));
|
|
101
|
+
else {
|
|
102
|
+
result.status = 'planned';
|
|
103
|
+
result.actions.push(makeNotice(direction.wouldKey));
|
|
104
|
+
updates.push({
|
|
105
|
+
resultIndex: users.length,
|
|
106
|
+
row: { Id: login.Id, IsFrozen: direction.targetState },
|
|
107
|
+
actionKey: direction.actionKey,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
users.push(result);
|
|
111
|
+
}
|
|
112
|
+
return { direction: direction.targetState ? 'freeze' : 'unfreeze', users, updates, warnings: [] };
|
|
113
|
+
};
|
|
114
|
+
const applyFreezePlan = async (conn, plan, ctx) => {
|
|
115
|
+
startProgress(ctx, 'apply');
|
|
116
|
+
const users = plan.users.map((user) => ({
|
|
117
|
+
...user,
|
|
118
|
+
status: user.status === 'planned' ? 'unchanged' : user.status,
|
|
119
|
+
actions: user.actions.filter((action) => action.key !== 'wouldFreeze' && action.key !== 'wouldUnfreeze'),
|
|
120
|
+
errors: [...user.errors],
|
|
121
|
+
warnings: [...user.warnings],
|
|
122
|
+
}));
|
|
123
|
+
if (plan.updates.length > 0) {
|
|
124
|
+
const saved = await conn.sobject('UserLogin').update(plan.updates.map((update) => update.row), { allOrNone: false });
|
|
125
|
+
const results = Array.isArray(saved) ? saved : [saved];
|
|
126
|
+
results.forEach((saveResult, index) => {
|
|
127
|
+
const user = users[plan.updates[index].resultIndex];
|
|
128
|
+
if (saveResult.success) {
|
|
129
|
+
user.status = 'changed';
|
|
130
|
+
user.actions.push(makeNotice(plan.updates[index].actionKey));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
user.status = 'failed';
|
|
134
|
+
user.errors.push(...(saveResult.errors ?? []).map((error) => error.message ?? String(error)));
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
endProgress(ctx);
|
|
139
|
+
return { summary: summarizeLifecycle(users), users };
|
|
140
|
+
};
|
|
141
|
+
export const freeze = {
|
|
142
|
+
kind: 'write',
|
|
143
|
+
descriptor: descriptor('freeze', 'Freeze', false, freezeOptionsSchema),
|
|
144
|
+
plan: (conn, options, ctx) => runRead(ctx, () => freezePlan(conn, options, FREEZE, ctx)),
|
|
145
|
+
apply: applyFreezePlan,
|
|
146
|
+
};
|
|
147
|
+
export const unfreeze = {
|
|
148
|
+
kind: 'write',
|
|
149
|
+
descriptor: descriptor('unfreeze', 'Unfreeze', false, unfreezeOptionsSchema),
|
|
150
|
+
plan: (conn, options, ctx) => runRead(ctx, () => freezePlan(conn, options, UNFREEZE, ctx)),
|
|
151
|
+
apply: applyFreezePlan,
|
|
152
|
+
};
|
|
153
|
+
const stripFlags = (options) => ({
|
|
154
|
+
user: options.user,
|
|
155
|
+
'users-def': options.usersPath,
|
|
156
|
+
'external-id': options.externalId,
|
|
157
|
+
'input-format': options.inputFormat,
|
|
158
|
+
'csv-list-delimiter': options.csvListDelimiter,
|
|
159
|
+
'no-freeze': options.noFreeze,
|
|
160
|
+
'no-deactivate': options.noDeactivate,
|
|
161
|
+
'keep-permsets': options.keepPermsets,
|
|
162
|
+
'keep-permset-groups': options.keepPermsetGroups,
|
|
163
|
+
'keep-public-groups': options.keepPublicGroups,
|
|
164
|
+
'keep-queues': options.keepQueues,
|
|
165
|
+
'keep-licenses': options.keepLicenses,
|
|
166
|
+
});
|
|
167
|
+
const makeStripPlan = async (conn, options, ctx) => {
|
|
168
|
+
checkCancelled(ctx);
|
|
169
|
+
const fieldMap = await describeUserFields(conn);
|
|
170
|
+
const selection = await resolveTargetSelection(conn, options, fieldMap);
|
|
171
|
+
const stateMaps = await loadAssignmentState(conn, selection.targets.map((target) => target.Id));
|
|
172
|
+
const flags = stripFlags(options);
|
|
173
|
+
const states = selection.targets.map((target) => buildTargetState({
|
|
174
|
+
target,
|
|
175
|
+
loginRows: stateMaps.userLoginByUserId.get(target.Id) ?? [],
|
|
176
|
+
rows: {
|
|
177
|
+
psa: stateMaps.psaByUserId.get(target.Id) ?? [],
|
|
178
|
+
group: stateMaps.groupByUserId.get(target.Id) ?? [],
|
|
179
|
+
psl: stateMaps.pslByUserId.get(target.Id) ?? [],
|
|
180
|
+
},
|
|
181
|
+
flags,
|
|
182
|
+
}));
|
|
183
|
+
const previewStates = selection.targets.map((target) => buildTargetState({
|
|
184
|
+
target,
|
|
185
|
+
loginRows: stateMaps.userLoginByUserId.get(target.Id) ?? [],
|
|
186
|
+
rows: {
|
|
187
|
+
psa: stateMaps.psaByUserId.get(target.Id) ?? [],
|
|
188
|
+
group: stateMaps.groupByUserId.get(target.Id) ?? [],
|
|
189
|
+
psl: stateMaps.pslByUserId.get(target.Id) ?? [],
|
|
190
|
+
},
|
|
191
|
+
flags: { ...flags, 'dry-run': true },
|
|
192
|
+
}));
|
|
193
|
+
const previewUsers = selection.errors.map(failedResult).concat(previewStates.map((state) => state.result));
|
|
194
|
+
return {
|
|
195
|
+
states,
|
|
196
|
+
errors: selection.errors,
|
|
197
|
+
preview: { summary: summarizeLifecycle(previewUsers), users: previewUsers },
|
|
198
|
+
warnings: [],
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
export const strip = {
|
|
202
|
+
kind: 'write',
|
|
203
|
+
descriptor: descriptor('strip', 'Strip', true, stripOptionsSchema),
|
|
204
|
+
plan: (conn, options, ctx) => runRead(ctx, () => makeStripPlan(conn, options, ctx)),
|
|
205
|
+
apply: async (conn, plan, ctx) => {
|
|
206
|
+
startProgress(ctx, 'apply');
|
|
207
|
+
for (const state of plan.states) {
|
|
208
|
+
checkCancelled(ctx);
|
|
209
|
+
// eslint-disable-next-line no-await-in-loop
|
|
210
|
+
await applyStripState(conn, state);
|
|
211
|
+
}
|
|
212
|
+
const users = plan.errors.map(failedResult).concat(plan.states.map((state) => state.result));
|
|
213
|
+
endProgress(ctx);
|
|
214
|
+
return { summary: summarizeLifecycle(users), users };
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
export const diff = {
|
|
218
|
+
kind: 'read',
|
|
219
|
+
descriptor: descriptor('diff', 'Diff', false, diffOptionsSchema),
|
|
220
|
+
run: (conn, options, ctx) => runRead(ctx, async () => {
|
|
221
|
+
if (options.mode === 'user') {
|
|
222
|
+
if (!options.user || !options.against)
|
|
223
|
+
throw new Error('Both user and against are required in user mode.');
|
|
224
|
+
if (options.verify)
|
|
225
|
+
throw new LifecycleError('errorVerifyUserMode', 'Verification is only available in persona mode.');
|
|
226
|
+
const { executeUserToUserDiff } = await import('./lifecycle/userDiff.js');
|
|
227
|
+
return executeUserToUserDiff({ connection: conn, user: options.user, against: options.against });
|
|
228
|
+
}
|
|
229
|
+
const { executePersonaDiff } = await import('./lifecycle/userDiff.js');
|
|
230
|
+
const definitions = options.usersDoc
|
|
231
|
+
? {
|
|
232
|
+
usersDoc: options.usersDoc,
|
|
233
|
+
personasDoc: options.personasDoc,
|
|
234
|
+
personasSupplied: options.personasDoc !== undefined,
|
|
235
|
+
}
|
|
236
|
+
: { usersPath: options.usersPath, personasPath: options.personasPath };
|
|
237
|
+
const result = await executePersonaDiff({
|
|
238
|
+
connection: conn,
|
|
239
|
+
...definitions,
|
|
240
|
+
inputFormat: options.inputFormat,
|
|
241
|
+
csvListDelimiter: options.csvListDelimiter,
|
|
242
|
+
externalId: options.externalId,
|
|
243
|
+
});
|
|
244
|
+
if (!options.verify)
|
|
245
|
+
return result;
|
|
246
|
+
const { verifyUserDiff } = await import('./lifecycle/conformance.js');
|
|
247
|
+
return verifyUserDiff(result, conformanceMessage);
|
|
248
|
+
}),
|
|
249
|
+
};
|
|
250
|
+
export const access = {
|
|
251
|
+
kind: 'read',
|
|
252
|
+
descriptor: descriptor('access', 'Access', false, accessOptionsSchema),
|
|
253
|
+
run: (conn, options, ctx) => runRead(ctx, async () => {
|
|
254
|
+
const resolver = getResolver(options.type);
|
|
255
|
+
const hasTarget = Boolean(options.target);
|
|
256
|
+
const hasUser = Boolean(options.user);
|
|
257
|
+
if (options.target && options.sobject)
|
|
258
|
+
throw new LifecycleError('errorAccessScopesMutuallyExclusive', 'target and sobject are mutually exclusive.');
|
|
259
|
+
if (!hasTarget && !hasUser)
|
|
260
|
+
throw new Error('An access target or user is required.');
|
|
261
|
+
let target;
|
|
262
|
+
if (options.target)
|
|
263
|
+
target = await resolver.validateTarget(conn, options.target);
|
|
264
|
+
else {
|
|
265
|
+
if (!options.sobject || (options.type !== 'field' && options.type !== 'object'))
|
|
266
|
+
throw new Error('SObject scope is only valid for field and object access.');
|
|
267
|
+
const objectTarget = await getResolver('object').validateTarget(conn, options.sobject);
|
|
268
|
+
target = { ...objectTarget, type: options.type };
|
|
269
|
+
}
|
|
270
|
+
if (!options.user)
|
|
271
|
+
return resolver.resolve(conn, target);
|
|
272
|
+
const fieldMap = await describeUserFields(conn);
|
|
273
|
+
const parsed = parseUserFlag(options.user);
|
|
274
|
+
const field = resolveTargetField(parsed.field, fieldMap);
|
|
275
|
+
if (!field)
|
|
276
|
+
throw new Error(lifecycleMessage('errorInvalidUserMatchField', [parsed.field]));
|
|
277
|
+
const resolved = await resolveTargets(conn, [{ key: `${field}:${parsed.value}`, field, value: parsed.value, order: 0 }], fieldMap);
|
|
278
|
+
if (resolved.errors.length > 0 || !resolved.targets[0])
|
|
279
|
+
throw new Error(resolved.errors[0]?.message ?? 'User resolution failed.');
|
|
280
|
+
const user = resolved.targets[0];
|
|
281
|
+
return resolveReverseAccess(conn, { Id: user.Id, name: user.name ?? user.Id, username: user.username ?? '' }, target);
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
export const snapshot = {
|
|
285
|
+
kind: 'read',
|
|
286
|
+
descriptor: descriptor('snapshot', 'Snapshot', false, snapshotOptionsSchema),
|
|
287
|
+
run: (conn, options, ctx) => runRead(ctx, async () => {
|
|
288
|
+
const fieldMap = await describeUserFields(conn);
|
|
289
|
+
const selection = await resolveTargetSelection(conn, options, fieldMap);
|
|
290
|
+
if (selection.errors.length > 0)
|
|
291
|
+
throw new Error(selection.errors.map((error) => error.message).join('; '));
|
|
292
|
+
const state = await loadAssignmentState(conn, selection.targets.map((target) => target.Id));
|
|
293
|
+
return buildSnapshotFile(conn, selection.targets, state, options.org);
|
|
294
|
+
}),
|
|
295
|
+
};
|
|
296
|
+
const publicProvisionResult = (result) => ({
|
|
297
|
+
key: result.key,
|
|
298
|
+
...(result.id === undefined ? {} : { id: result.id }),
|
|
299
|
+
...(result.userName === undefined ? {} : { userName: result.userName }),
|
|
300
|
+
...(result.username === undefined ? {} : { username: result.username }),
|
|
301
|
+
personas: result.personas,
|
|
302
|
+
matchedBy: result.matchedBy,
|
|
303
|
+
matchValue: result.matchValue,
|
|
304
|
+
matched: result.matched,
|
|
305
|
+
status: result.status,
|
|
306
|
+
actions: result.actions,
|
|
307
|
+
errors: result.errors,
|
|
308
|
+
...(result.relatedRecords ? { relatedRecords: result.relatedRecords } : {}),
|
|
309
|
+
});
|
|
310
|
+
const serializeRefs = (refs) => ({
|
|
311
|
+
profilesByRef: Object.fromEntries(refs.profilesByRef),
|
|
312
|
+
rolesByRef: Object.fromEntries(refs.rolesByRef),
|
|
313
|
+
permissionSetIdsByRef: Object.fromEntries(refs.permissionSetIdsByRef),
|
|
314
|
+
permissionSetGroupIdsByRef: Object.fromEntries(refs.permissionSetGroupIdsByRef),
|
|
315
|
+
publicGroupIdsByRef: Object.fromEntries(refs.publicGroupIdsByRef),
|
|
316
|
+
queueIdsByRef: Object.fromEntries(refs.queueIdsByRef),
|
|
317
|
+
labels: refs.labels,
|
|
318
|
+
warnings: refs.warnings,
|
|
319
|
+
});
|
|
320
|
+
const deserializeRefs = (refs) => ({
|
|
321
|
+
profilesByRef: new Map(Object.entries(refs.profilesByRef)),
|
|
322
|
+
rolesByRef: new Map(Object.entries(refs.rolesByRef)),
|
|
323
|
+
permissionSetIdsByRef: new Map(Object.entries(refs.permissionSetIdsByRef)),
|
|
324
|
+
permissionSetGroupIdsByRef: new Map(Object.entries(refs.permissionSetGroupIdsByRef)),
|
|
325
|
+
publicGroupIdsByRef: new Map(Object.entries(refs.publicGroupIdsByRef)),
|
|
326
|
+
queueIdsByRef: new Map(Object.entries(refs.queueIdsByRef)),
|
|
327
|
+
labels: refs.labels,
|
|
328
|
+
warnings: refs.warnings,
|
|
329
|
+
});
|
|
330
|
+
const provisionPlan = async (conn, options, ctx) => {
|
|
331
|
+
checkCancelled(ctx);
|
|
332
|
+
const input = {
|
|
333
|
+
connection: conn,
|
|
334
|
+
usersDoc: options.usersDoc,
|
|
335
|
+
personasDoc: options.personasDoc,
|
|
336
|
+
relatedDoc: options.relatedDoc,
|
|
337
|
+
usersPath: options.usersPath,
|
|
338
|
+
personasPath: options.personasPath,
|
|
339
|
+
relatedPath: options.relatedPath,
|
|
340
|
+
inputFormat: options.inputFormat,
|
|
341
|
+
csvListDelimiter: options.csvListDelimiter,
|
|
342
|
+
externalId: options.externalId,
|
|
343
|
+
fuzzyUsername: options.fuzzyUsername,
|
|
344
|
+
dryRun: false,
|
|
345
|
+
};
|
|
346
|
+
const fieldMap = await describeUserFields(conn);
|
|
347
|
+
const definitions = await resolveDefinitions(input, fieldMap);
|
|
348
|
+
const personas = definitions.personasDoc.personas;
|
|
349
|
+
validatePersonaModes(personas);
|
|
350
|
+
validateExternalIdFieldForFlag(options.externalId, fieldMap);
|
|
351
|
+
const catalog = definitions.relatedDoc ? assertValidRelatedCatalog(definitions.relatedDoc, fieldMap) : undefined;
|
|
352
|
+
const users = validateAndCanonicalizeUsers(definitions.usersDoc.users, personas, fieldMap, definitions.personasSupplied, {
|
|
353
|
+
catalogSupplied: Boolean(catalog),
|
|
354
|
+
names: new Set(Object.keys(catalog?.relationships ?? {})),
|
|
355
|
+
});
|
|
356
|
+
const entries = users.map((user, order) => ({ user, order }));
|
|
357
|
+
const validUsers = entries.filter(({ user }) => !user.validationErrors?.length);
|
|
358
|
+
const refs = await resolveReferences(conn, personas, validUsers.map(({ user }) => user));
|
|
359
|
+
const existing = await getExistingUsers(conn, validUsers.map(({ user }) => user), {
|
|
360
|
+
defaultExternalIdField: options.externalId ? fieldMap.get(options.externalId.toLowerCase())?.name : undefined,
|
|
361
|
+
defaultFuzzyUsername: options.fuzzyUsername,
|
|
362
|
+
fieldMap,
|
|
363
|
+
});
|
|
364
|
+
const preflight = catalog
|
|
365
|
+
? await runRelatedPreflight({
|
|
366
|
+
conn,
|
|
367
|
+
catalog,
|
|
368
|
+
selected: [...new Set(validUsers.flatMap(({ user }) => user.related ?? []))],
|
|
369
|
+
cache: new Map(),
|
|
370
|
+
})
|
|
371
|
+
: emptyPreflightResult();
|
|
372
|
+
refs.warnings.push(...preflight.warnings);
|
|
373
|
+
const relatedPlansByOrder = catalog
|
|
374
|
+
? await buildRelatedPlans({
|
|
375
|
+
conn,
|
|
376
|
+
users: validUsers,
|
|
377
|
+
catalog,
|
|
378
|
+
preflight,
|
|
379
|
+
userFieldMap: fieldMap,
|
|
380
|
+
})
|
|
381
|
+
: undefined;
|
|
382
|
+
const plans = buildUserPlans({
|
|
383
|
+
validUsers,
|
|
384
|
+
refs,
|
|
385
|
+
existing,
|
|
386
|
+
fieldMap,
|
|
387
|
+
defaultExternalIdField: options.externalId ? fieldMap.get(options.externalId.toLowerCase())?.name : undefined,
|
|
388
|
+
myDomain: deriveMyDomain(conn.instanceUrl),
|
|
389
|
+
dryRun: false,
|
|
390
|
+
relatedPlansByOrder,
|
|
391
|
+
});
|
|
392
|
+
const previewPlans = buildUserPlans({
|
|
393
|
+
validUsers,
|
|
394
|
+
refs,
|
|
395
|
+
existing,
|
|
396
|
+
fieldMap,
|
|
397
|
+
defaultExternalIdField: options.externalId ? fieldMap.get(options.externalId.toLowerCase())?.name : undefined,
|
|
398
|
+
myDomain: deriveMyDomain(conn.instanceUrl),
|
|
399
|
+
dryRun: true,
|
|
400
|
+
relatedPlansByOrder,
|
|
401
|
+
});
|
|
402
|
+
const licenses = await calculateUserLicenseUsage(conn, plans);
|
|
403
|
+
const validationResults = entries
|
|
404
|
+
.filter(({ user }) => user.validationErrors?.length)
|
|
405
|
+
.map(({ user, order }) => ({
|
|
406
|
+
planId: `${order}:${user.inputKey}:${user.personas.join('+')}:validation`,
|
|
407
|
+
order,
|
|
408
|
+
key: user.inputKey,
|
|
409
|
+
personas: user.personas,
|
|
410
|
+
matchedBy: user.matchField ?? null,
|
|
411
|
+
matchValue: null,
|
|
412
|
+
matched: false,
|
|
413
|
+
status: 'failed',
|
|
414
|
+
actions: [],
|
|
415
|
+
errors: user.validationErrors?.map((error) => lifecycleMessage(error.code, error.messageArgs)) ?? [],
|
|
416
|
+
}));
|
|
417
|
+
const planned = await Promise.all(previewPlans.map((plan) => planDryRunResult({ conn, plan, refs })));
|
|
418
|
+
const allPreviewResults = validationResults.concat(planned).sort((left, right) => left.order - right.order);
|
|
419
|
+
const preview = {
|
|
420
|
+
summary: summarize(allPreviewResults, refs.warnings.length + licenses.filter((license) => license.shortfall > 0).length),
|
|
421
|
+
users: allPreviewResults.map(publicProvisionResult),
|
|
422
|
+
licenses,
|
|
423
|
+
permissionSetLicenses: { evaluated: false, note: 'not evaluated' },
|
|
424
|
+
};
|
|
425
|
+
return { plans, refs: serializeRefs(refs), validationResults, licenses, preview, warnings: refs.warnings };
|
|
426
|
+
};
|
|
427
|
+
const applyProvisionPlan = async (conn, plan, ctx) => {
|
|
428
|
+
startProgress(ctx, 'apply');
|
|
429
|
+
const refs = deserializeRefs(plan.refs);
|
|
430
|
+
checkCancelled(ctx);
|
|
431
|
+
const outcomes = await executeBulkUserSaves(conn, plan.plans, () => checkCancelled(ctx));
|
|
432
|
+
const invalid = plan.plans
|
|
433
|
+
.filter((item) => item.errors.length > 0)
|
|
434
|
+
.map((item) => toUserResult(markRelatedUnapplied(item), 'failed', { includeExistingId: false }));
|
|
435
|
+
const failures = outcomes
|
|
436
|
+
.filter((outcome) => !outcome.success)
|
|
437
|
+
.map((outcome) => toUserResult(markRelatedUnapplied(outcome.plan), 'failed', { errors: outcome.errors, includeExistingId: false }));
|
|
438
|
+
const saved = outcomes.filter((outcome) => outcome.success);
|
|
439
|
+
checkCancelled(ctx);
|
|
440
|
+
const related = await applyRelatedPhase(conn, saved
|
|
441
|
+
.filter((outcome) => outcome.plan.relatedPlans && outcome.id)
|
|
442
|
+
.map((outcome) => ({
|
|
443
|
+
planId: outcome.plan.planId,
|
|
444
|
+
relatedPlans: outcome.plan.relatedPlans,
|
|
445
|
+
savedUserId: outcome.id,
|
|
446
|
+
})), 'after', () => checkCancelled(ctx));
|
|
447
|
+
for (const outcome of saved) {
|
|
448
|
+
const results = related.get(outcome.plan.planId);
|
|
449
|
+
if (results)
|
|
450
|
+
outcome.plan.relatedResults = results;
|
|
451
|
+
}
|
|
452
|
+
const applied = [];
|
|
453
|
+
for (const outcome of saved) {
|
|
454
|
+
checkCancelled(ctx);
|
|
455
|
+
// eslint-disable-next-line no-await-in-loop
|
|
456
|
+
const outcomeResult = await applySavedPlan({
|
|
457
|
+
conn,
|
|
458
|
+
outcome,
|
|
459
|
+
refs,
|
|
460
|
+
message: (key) => key,
|
|
461
|
+
beforeWrite: () => checkCancelled(ctx),
|
|
462
|
+
});
|
|
463
|
+
applied.push(outcomeResult);
|
|
464
|
+
}
|
|
465
|
+
const users = plan.validationResults
|
|
466
|
+
.concat(invalid, failures, applied)
|
|
467
|
+
.sort((left, right) => left.order - right.order)
|
|
468
|
+
.map(publicProvisionResult);
|
|
469
|
+
endProgress(ctx);
|
|
470
|
+
return { summary: summarize(users, plan.warnings.length), users };
|
|
471
|
+
};
|
|
472
|
+
export const provision = {
|
|
473
|
+
kind: 'write',
|
|
474
|
+
descriptor: descriptor('provision', 'Provision', false, provisionOptionsSchema),
|
|
475
|
+
plan: (conn, options, ctx) => runRead(ctx, () => provisionPlan(conn, options, ctx)),
|
|
476
|
+
apply: applyProvisionPlan,
|
|
477
|
+
};
|
|
478
|
+
const identityReview = (snapshotUser, target) => {
|
|
479
|
+
const snapshotIdentity = {
|
|
480
|
+
name: snapshotUser.name,
|
|
481
|
+
username: snapshotUser.username,
|
|
482
|
+
email: snapshotUser.email,
|
|
483
|
+
profile: snapshotUser.profile,
|
|
484
|
+
role: snapshotUser.role,
|
|
485
|
+
};
|
|
486
|
+
if (!Object.values(snapshotIdentity).some((value) => value !== undefined))
|
|
487
|
+
return undefined;
|
|
488
|
+
return {
|
|
489
|
+
snapshot: Object.fromEntries(Object.entries(snapshotIdentity).filter(([, value]) => value !== undefined)),
|
|
490
|
+
org: {
|
|
491
|
+
...(target.name !== undefined ? { name: target.name } : {}),
|
|
492
|
+
...(target.username !== undefined ? { username: target.username } : {}),
|
|
493
|
+
...(target.email !== undefined ? { email: target.email } : {}),
|
|
494
|
+
...(target.profile !== undefined ? { profile: target.profile } : {}),
|
|
495
|
+
...(target.role !== undefined ? { role: target.role } : {}),
|
|
496
|
+
userId: target.Id,
|
|
497
|
+
},
|
|
498
|
+
match: { field: target.field, value: target.value },
|
|
499
|
+
};
|
|
500
|
+
};
|
|
501
|
+
const identityMismatches = (review) => {
|
|
502
|
+
const fields = ['name', 'username', 'email', 'profile', 'role'];
|
|
503
|
+
return fields.flatMap((field) => {
|
|
504
|
+
const expected = review.snapshot[field];
|
|
505
|
+
const actual = review.org[field];
|
|
506
|
+
return expected !== undefined && expected !== actual
|
|
507
|
+
? [`Snapshot ${field} "${expected}" differs from org "${actual ?? '(missing)'}".`]
|
|
508
|
+
: [];
|
|
509
|
+
});
|
|
510
|
+
};
|
|
511
|
+
const resolveRestoreRefs = async (conn, users) => {
|
|
512
|
+
const query = async (table, values, name, where = '') => {
|
|
513
|
+
const unique = [...new Set(values)];
|
|
514
|
+
if (unique.length === 0)
|
|
515
|
+
return { resolved: {}, missing: [] };
|
|
516
|
+
const rows = (await conn.query(`SELECT Id, ${name} FROM ${table} WHERE ${name} IN (${soqlIn(unique)})${where ? ` AND ${where}` : ''}`)).records;
|
|
517
|
+
const resolved = Object.fromEntries(rows.map((row) => [row[name], row.Id]));
|
|
518
|
+
return { resolved, missing: unique.filter((value) => !resolved[value]) };
|
|
519
|
+
};
|
|
520
|
+
return {
|
|
521
|
+
permissionSets: await query('PermissionSet', users.flatMap((user) => user.permissionSets), 'Name'),
|
|
522
|
+
permissionSetGroups: await query('PermissionSetGroup', users.flatMap((user) => user.permissionSetGroups), 'DeveloperName'),
|
|
523
|
+
publicGroups: await query('Group', users.flatMap((user) => user.publicGroups), 'DeveloperName', "Type = 'Regular'"),
|
|
524
|
+
queues: await query('Group', users.flatMap((user) => user.queues), 'DeveloperName', "Type = 'Queue'"),
|
|
525
|
+
licenses: await query('PermissionSetLicense', users.flatMap((user) => user.permissionSetLicenses), 'DeveloperName'),
|
|
526
|
+
};
|
|
527
|
+
};
|
|
528
|
+
const ids = (names, refs) => names.map((name) => refs.resolved[name]).filter((id) => Boolean(id));
|
|
529
|
+
const missingIds = (wanted, current) => {
|
|
530
|
+
const currentIds = new Set(current.filter((id) => Boolean(id)));
|
|
531
|
+
return wanted.filter((id) => !currentIds.has(id));
|
|
532
|
+
};
|
|
533
|
+
const makeRestoreUserPlan = (state, refs, snapshotUser, target) => {
|
|
534
|
+
const result = {
|
|
535
|
+
key: target.key,
|
|
536
|
+
id: target.Id,
|
|
537
|
+
name: target.name,
|
|
538
|
+
username: target.username,
|
|
539
|
+
isActive: target.IsActive,
|
|
540
|
+
...(state.userLoginByUserId.get(target.Id)?.[0]?.IsFrozen === undefined
|
|
541
|
+
? {}
|
|
542
|
+
: { isFrozen: state.userLoginByUserId.get(target.Id)?.[0]?.IsFrozen }),
|
|
543
|
+
status: 'unchanged',
|
|
544
|
+
actions: [],
|
|
545
|
+
skipped: [],
|
|
546
|
+
warnings: [],
|
|
547
|
+
errors: [],
|
|
548
|
+
};
|
|
549
|
+
const review = identityReview(snapshotUser, target);
|
|
550
|
+
if (review) {
|
|
551
|
+
result.identityReview = review;
|
|
552
|
+
result.warnings.push(...identityMismatches(review));
|
|
553
|
+
}
|
|
554
|
+
const psa = state.psaByUserId.get(target.Id) ?? [];
|
|
555
|
+
const groups = state.groupByUserId.get(target.Id) ?? [];
|
|
556
|
+
const psl = state.pslByUserId.get(target.Id) ?? [];
|
|
557
|
+
const permissionSetAdds = missingIds(ids(snapshotUser.permissionSets, refs.permissionSets), psa.map((row) => row.PermissionSetId));
|
|
558
|
+
const permissionSetGroupAdds = missingIds(ids(snapshotUser.permissionSetGroups, refs.permissionSetGroups), psa.map((row) => row.PermissionSetGroupId));
|
|
559
|
+
const publicGroupAdds = missingIds(ids(snapshotUser.publicGroups, refs.publicGroups), groups.filter((row) => row.Group?.Type === 'Regular').map((row) => row.GroupId));
|
|
560
|
+
const queueAdds = missingIds(ids(snapshotUser.queues, refs.queues), groups.filter((row) => row.Group?.Type === 'Queue').map((row) => row.GroupId));
|
|
561
|
+
const licenseAdds = missingIds(ids(snapshotUser.permissionSetLicenses, refs.licenses), psl.map((row) => row.PermissionSetLicenseId));
|
|
562
|
+
const items = (names, resolution, type, wanted) => names.flatMap((name) => resolution.resolved[name] && wanted.includes(resolution.resolved[name])
|
|
563
|
+
? [{ id: resolution.resolved[name], apiName: name, type }]
|
|
564
|
+
: []);
|
|
565
|
+
const loginUpdates = (state.userLoginByUserId.get(target.Id) ?? [])
|
|
566
|
+
.filter((row) => row.IsFrozen)
|
|
567
|
+
.map((row) => ({ Id: row.Id, IsFrozen: false }));
|
|
568
|
+
const userUpdate = target.IsActive ? undefined : { Id: target.Id, IsActive: true };
|
|
569
|
+
const warnMissing = (table, names, resolution) => {
|
|
570
|
+
for (const name of names) {
|
|
571
|
+
if (resolution.missing.includes(name)) {
|
|
572
|
+
result.warnings.push(provisioningMessage('warningReferenceMissing', [table, name]));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
warnMissing('PermissionSet', snapshotUser.permissionSets, refs.permissionSets);
|
|
577
|
+
warnMissing('PermissionSetGroup', snapshotUser.permissionSetGroups, refs.permissionSetGroups);
|
|
578
|
+
warnMissing('Group', snapshotUser.publicGroups, refs.publicGroups);
|
|
579
|
+
warnMissing('Group', snapshotUser.queues, refs.queues);
|
|
580
|
+
warnMissing('PermissionSetLicense', snapshotUser.permissionSetLicenses, refs.licenses);
|
|
581
|
+
const pendingDml = Boolean(userUpdate) || loginUpdates.length > 0;
|
|
582
|
+
const preview = { ...result, status: 'unchanged', actions: [] };
|
|
583
|
+
if (userUpdate)
|
|
584
|
+
preview.actions.push(makeNotice('wouldActivate'));
|
|
585
|
+
if (loginUpdates.length)
|
|
586
|
+
preview.actions.push(makeNotice('wouldUnfreeze'));
|
|
587
|
+
const action = (key, values, entries) => {
|
|
588
|
+
if (values.length) {
|
|
589
|
+
preview.status = 'planned';
|
|
590
|
+
preview.actions.push(makeNotice(key, values.length, entries));
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
if (pendingDml)
|
|
594
|
+
preview.status = 'planned';
|
|
595
|
+
action('wouldAssignPermissionSet', permissionSetAdds, items(snapshotUser.permissionSets, refs.permissionSets, 'PermissionSet', permissionSetAdds));
|
|
596
|
+
action('wouldAssignPermissionSetGroup', permissionSetGroupAdds, items(snapshotUser.permissionSetGroups, refs.permissionSetGroups, 'PermissionSetGroup', permissionSetGroupAdds));
|
|
597
|
+
action('wouldAddPublicGroupMember', publicGroupAdds, items(snapshotUser.publicGroups, refs.publicGroups, 'PublicGroup', publicGroupAdds));
|
|
598
|
+
action('wouldAddQueueMember', queueAdds, items(snapshotUser.queues, refs.queues, 'Queue', queueAdds));
|
|
599
|
+
action('wouldAssignPermissionSetLicense', licenseAdds, items(snapshotUser.permissionSetLicenses, refs.licenses, 'PermissionSetLicense', licenseAdds));
|
|
600
|
+
return {
|
|
601
|
+
userId: target.Id,
|
|
602
|
+
result,
|
|
603
|
+
preview,
|
|
604
|
+
...(userUpdate ? { userUpdate } : {}),
|
|
605
|
+
loginUpdates,
|
|
606
|
+
permissionSetAdds,
|
|
607
|
+
permissionSetGroupAdds,
|
|
608
|
+
publicGroupAdds,
|
|
609
|
+
queueAdds,
|
|
610
|
+
licenseAdds,
|
|
611
|
+
permissionSetItems: items(snapshotUser.permissionSets, refs.permissionSets, 'PermissionSet', permissionSetAdds),
|
|
612
|
+
permissionSetGroupItems: items(snapshotUser.permissionSetGroups, refs.permissionSetGroups, 'PermissionSetGroup', permissionSetGroupAdds),
|
|
613
|
+
publicGroupItems: items(snapshotUser.publicGroups, refs.publicGroups, 'PublicGroup', publicGroupAdds),
|
|
614
|
+
queueItems: items(snapshotUser.queues, refs.queues, 'Queue', queueAdds),
|
|
615
|
+
licenseItems: items(snapshotUser.permissionSetLicenses, refs.licenses, 'PermissionSetLicense', licenseAdds),
|
|
616
|
+
};
|
|
617
|
+
};
|
|
618
|
+
const applyRestoreUserPlan = async (conn, plan) => {
|
|
619
|
+
const result = plan.result;
|
|
620
|
+
if (plan.userUpdate)
|
|
621
|
+
await runRecordUpdate({ conn, result, sobject: 'User', rows: [plan.userUpdate], actionKey: 'activated' });
|
|
622
|
+
await runRecordUpdate({ conn, result, sobject: 'UserLogin', rows: plan.loginUpdates, actionKey: 'unfrozen' });
|
|
623
|
+
const create = async (sobject, rows, actionKey, items) => runAssignmentCreates({ conn, result, sobject, rows, actionKey, items });
|
|
624
|
+
await create('PermissionSetAssignment', plan.permissionSetAdds.map((id) => ({ AssigneeId: plan.userId, PermissionSetId: id })), 'assignedPermissionSet', plan.permissionSetItems);
|
|
625
|
+
await create('PermissionSetAssignment', plan.permissionSetGroupAdds.map((id) => ({ AssigneeId: plan.userId, PermissionSetGroupId: id })), 'assignedPermissionSetGroup', plan.permissionSetGroupItems);
|
|
626
|
+
await create('GroupMember', plan.publicGroupAdds.map((id) => ({ GroupId: id, UserOrGroupId: plan.userId })), 'addedPublicGroupMember', plan.publicGroupItems);
|
|
627
|
+
await create('GroupMember', plan.queueAdds.map((id) => ({ GroupId: id, UserOrGroupId: plan.userId })), 'addedQueueMember', plan.queueItems);
|
|
628
|
+
await create('PermissionSetLicenseAssign', plan.licenseAdds.map((id) => ({ AssigneeId: plan.userId, PermissionSetLicenseId: id })), 'assignedPermissionSetLicense', plan.licenseItems);
|
|
629
|
+
};
|
|
630
|
+
const restorePlan = async (conn, options, ctx) => {
|
|
631
|
+
checkCancelled(ctx);
|
|
632
|
+
const snapshotFile = options.snapshotDoc
|
|
633
|
+
? parseSnapshot(options.snapshotDoc)
|
|
634
|
+
: options.snapshotPath
|
|
635
|
+
? await (await import('./lifecycle/snapshotState.js')).readSnapshotFile(options.snapshotPath)
|
|
636
|
+
: undefined;
|
|
637
|
+
if (!snapshotFile)
|
|
638
|
+
throw new Error('A snapshot document or path is required.');
|
|
639
|
+
const fieldMap = await describeUserFields(conn);
|
|
640
|
+
const requests = [];
|
|
641
|
+
const errors = [];
|
|
642
|
+
for (const [order, user] of snapshotFile.users.entries()) {
|
|
643
|
+
const field = resolveTargetField(user.match, fieldMap);
|
|
644
|
+
if (!field)
|
|
645
|
+
errors.push({
|
|
646
|
+
key: `${user.match}:${user.matchValue}`,
|
|
647
|
+
status: 'failed',
|
|
648
|
+
actions: [],
|
|
649
|
+
skipped: [],
|
|
650
|
+
warnings: [],
|
|
651
|
+
errors: [lifecycleMessage('errorInvalidUserMatchField', [user.match])],
|
|
652
|
+
});
|
|
653
|
+
else
|
|
654
|
+
requests.push({ key: `${field}:${user.matchValue}`, field, value: user.matchValue, order });
|
|
655
|
+
}
|
|
656
|
+
const resolved = await resolveTargets(conn, requests, fieldMap);
|
|
657
|
+
errors.push(...resolved.errors.map(failedResult));
|
|
658
|
+
const [state, refs] = await Promise.all([
|
|
659
|
+
loadAssignmentState(conn, resolved.targets.map((target) => target.Id)),
|
|
660
|
+
resolveRestoreRefs(conn, snapshotFile.users),
|
|
661
|
+
]);
|
|
662
|
+
const plans = resolved.targets.map((target) => makeRestoreUserPlan(state, refs, snapshotFile.users[target.order], target));
|
|
663
|
+
const warnings = plans.flatMap((plan) => plan.preview.warnings);
|
|
664
|
+
const previewUsers = errors.concat(plans.map((plan) => plan.preview));
|
|
665
|
+
return {
|
|
666
|
+
users: plans,
|
|
667
|
+
errors,
|
|
668
|
+
warnings,
|
|
669
|
+
preview: { summary: summarizeLifecycle(previewUsers), users: previewUsers },
|
|
670
|
+
};
|
|
671
|
+
};
|
|
672
|
+
export const restore = {
|
|
673
|
+
kind: 'write',
|
|
674
|
+
descriptor: descriptor('restore', 'Restore', false, restoreOptionsSchema),
|
|
675
|
+
plan: (conn, options, ctx) => runRead(ctx, () => restorePlan(conn, options, ctx)),
|
|
676
|
+
apply: async (conn, plan, ctx) => {
|
|
677
|
+
startProgress(ctx, 'apply');
|
|
678
|
+
for (const user of plan.users) {
|
|
679
|
+
checkCancelled(ctx);
|
|
680
|
+
// eslint-disable-next-line no-await-in-loop
|
|
681
|
+
await applyRestoreUserPlan(conn, user);
|
|
682
|
+
}
|
|
683
|
+
const users = plan.errors.concat(plan.users.map((user) => user.result));
|
|
684
|
+
endProgress(ctx);
|
|
685
|
+
return { summary: summarizeLifecycle(users), users };
|
|
686
|
+
},
|
|
687
|
+
};
|
|
688
|
+
export const commandDescriptors = [
|
|
689
|
+
provision.descriptor,
|
|
690
|
+
access.descriptor,
|
|
691
|
+
diff.descriptor,
|
|
692
|
+
freeze.descriptor,
|
|
693
|
+
unfreeze.descriptor,
|
|
694
|
+
strip.descriptor,
|
|
695
|
+
snapshot.descriptor,
|
|
696
|
+
restore.descriptor,
|
|
697
|
+
];
|
|
698
|
+
//# sourceMappingURL=useCases.js.map
|