@uipath/vertical-solutions-tool 1.204.0 → 1.205.0-preview.258
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 +80 -0
- package/dist/THIRD-PARTY-NOTICES.md +1617 -369
- package/dist/createSdk-2f9x47em.js +12 -0
- package/dist/deploy-jobs-3k87fkwz.js +23 -0
- package/dist/deploy-service-qaykt7er.js +1779 -0
- package/dist/deployment-search-arbm2z06.js +2649 -0
- package/dist/embedded-file-reader-r66242ey.js +114 -0
- package/dist/first-party-service-srxaftkn.js +27 -0
- package/dist/index-7a3eqtxv.js +3174 -0
- package/dist/index-8s9dxpj4.js +18812 -0
- package/dist/index-fyc0k5dd.js +51405 -0
- package/dist/index-h43sj8at.js +23075 -0
- package/dist/index-kmp9fmhb.js +25 -0
- package/dist/index-ngs0a2he.js +15 -0
- package/dist/index.js +14 -3
- package/dist/index.umd.min-fqefd9kn.js +7814 -0
- package/dist/job-commands-azgbrbdb.js +23 -0
- package/dist/job-output-t43hzj1f.js +36 -0
- package/dist/prompts-dtgt183n.js +260 -0
- package/dist/resume-service-mqfnnzb5.js +344 -0
- package/dist/seed-service-2d1f7zaz.js +2014 -0
- package/dist/seed-templates-sqqqratr.js +14 -0
- package/dist/service-errors-jzbq3z7p.js +22 -0
- package/dist/solution-auth-zq6gyxsv.js +11 -0
- package/dist/solution-deployments-m44fyb88.js +33 -0
- package/dist/templates-85yrztpk.js +15 -0
- package/dist/tool-342sz3hm.js +439 -0
- package/dist/tool-38ad77wd.js +1472 -0
- package/dist/tool-4z1dy1rn.js +260 -0
- package/dist/tool-53w8skv5.js +262 -0
- package/dist/tool-67587985.js +554 -0
- package/dist/tool-87e2ps7f.js +13 -0
- package/dist/tool-cag2z2d6.js +30 -0
- package/dist/tool-e4qgwf9h.js +2281 -0
- package/dist/tool-gdtpsdn7.js +342 -0
- package/dist/tool-gvmshnq9.js +555 -0
- package/dist/tool-h4f967yd.js +73 -0
- package/dist/tool-hdxtgckw.js +249 -0
- package/dist/tool-jjxbj28g.js +17067 -0
- package/dist/tool-keqjsxdx.js +28 -0
- package/dist/tool-maw0g02p.js +24 -0
- package/dist/tool-mn1wkbhw.js +113 -0
- package/dist/tool-n35nrs32.js +4949 -0
- package/dist/tool-p589v2r5.js +114 -0
- package/dist/tool-pz9qm3dv.js +6723 -0
- package/dist/tool-r1ewa0tf.js +5645 -0
- package/dist/tool-r2epw08s.js +164 -0
- package/dist/tool-rr9fensp.js +12 -0
- package/dist/tool-s04z9t80.js +19 -0
- package/dist/tool-s9qbxa2g.js +638 -0
- package/dist/tool-tjhwq0ez.js +261 -0
- package/dist/tool-v15vxc4x.js +183 -0
- package/dist/tool-vf2mgth0.js +14286 -0
- package/dist/tool-vp7rjr2q.js +22083 -0
- package/dist/tool-xd2ejt3h.js +103 -0
- package/dist/tool-yx29vb63.js +672 -0
- package/dist/tool.js +12 -2
- package/dist/watch-job-wh6m7xzq.js +31 -0
- package/package.json +3 -3
- package/dist/tool-xymb3dsq.js +0 -45085
|
@@ -0,0 +1,2014 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listDeploymentsOfSolution,
|
|
3
|
+
resolveInstalledFolder,
|
|
4
|
+
solutionNameFromManifest
|
|
5
|
+
} from "./tool-tjhwq0ez.js";
|
|
6
|
+
import {
|
|
7
|
+
BatchResponseSchema,
|
|
8
|
+
FailureRecordSchema,
|
|
9
|
+
WrittenRecordSchema,
|
|
10
|
+
buildEntityPlan,
|
|
11
|
+
fieldMap,
|
|
12
|
+
findFolderPath,
|
|
13
|
+
getManifest,
|
|
14
|
+
journalEntity,
|
|
15
|
+
listAllFolders,
|
|
16
|
+
listFolderDtos,
|
|
17
|
+
loadChoiceSetValues,
|
|
18
|
+
loadLiveEntities,
|
|
19
|
+
loadLiveRecords,
|
|
20
|
+
needsExpandedRecords,
|
|
21
|
+
orchestratorApi,
|
|
22
|
+
putOwn,
|
|
23
|
+
readAllPages,
|
|
24
|
+
readSeedContext,
|
|
25
|
+
readSeedFile,
|
|
26
|
+
readStrategy,
|
|
27
|
+
removeSeedContext,
|
|
28
|
+
withOptionalClears,
|
|
29
|
+
writeSeedContext
|
|
30
|
+
} from "./tool-e4qgwf9h.js";
|
|
31
|
+
import {
|
|
32
|
+
downloadSeedTemplates
|
|
33
|
+
} from "./tool-maw0g02p.js";
|
|
34
|
+
import {
|
|
35
|
+
isDotSegment,
|
|
36
|
+
isUnsafeSegment,
|
|
37
|
+
refreshSeedTemplates,
|
|
38
|
+
segmentNameProblem,
|
|
39
|
+
withoutControlBytes
|
|
40
|
+
} from "./tool-yx29vb63.js";
|
|
41
|
+
import"./tool-67587985.js";
|
|
42
|
+
import {
|
|
43
|
+
acquireSeedWorkspaceLock,
|
|
44
|
+
entityNameProblem,
|
|
45
|
+
insideSeedRoot,
|
|
46
|
+
namesSeedRoot,
|
|
47
|
+
packageDir,
|
|
48
|
+
packageNameProblem,
|
|
49
|
+
readJournal,
|
|
50
|
+
resolveSeedDir,
|
|
51
|
+
writeJournal
|
|
52
|
+
} from "./tool-s9qbxa2g.js";
|
|
53
|
+
import {
|
|
54
|
+
readServiceUrl,
|
|
55
|
+
rejectBlankOptions
|
|
56
|
+
} from "./tool-p589v2r5.js";
|
|
57
|
+
import {
|
|
58
|
+
collapseRowProblems,
|
|
59
|
+
writeProgress,
|
|
60
|
+
writeWarning
|
|
61
|
+
} from "./tool-v15vxc4x.js";
|
|
62
|
+
import {
|
|
63
|
+
createVssConfig,
|
|
64
|
+
isLoopbackHost
|
|
65
|
+
} from "./tool-xd2ejt3h.js";
|
|
66
|
+
import {
|
|
67
|
+
toDeployFailure
|
|
68
|
+
} from "./tool-r2epw08s.js";
|
|
69
|
+
import"./tool-4z1dy1rn.js";
|
|
70
|
+
import"./tool-87e2ps7f.js";
|
|
71
|
+
import"./tool-38ad77wd.js";
|
|
72
|
+
import {
|
|
73
|
+
routingFlags,
|
|
74
|
+
shellQuote
|
|
75
|
+
} from "./tool-cag2z2d6.js";
|
|
76
|
+
import {
|
|
77
|
+
failure,
|
|
78
|
+
invalidOption
|
|
79
|
+
} from "./tool-h4f967yd.js";
|
|
80
|
+
import"./tool-rr9fensp.js";
|
|
81
|
+
import"./tool-s04z9t80.js";
|
|
82
|
+
import"./tool-vf2mgth0.js";
|
|
83
|
+
import {
|
|
84
|
+
RESULTS,
|
|
85
|
+
canPrompt,
|
|
86
|
+
catchError,
|
|
87
|
+
extractErrorDetails,
|
|
88
|
+
extractErrorMessage
|
|
89
|
+
} from "./tool-pz9qm3dv.js";
|
|
90
|
+
import {
|
|
91
|
+
getFileSystem
|
|
92
|
+
} from "./tool-hdxtgckw.js";
|
|
93
|
+
import {
|
|
94
|
+
__require
|
|
95
|
+
} from "./tool-1yh8g550.js";
|
|
96
|
+
|
|
97
|
+
// src/services/seed-run/context-checks.ts
|
|
98
|
+
function contextForPackage(context, packageName) {
|
|
99
|
+
const wrote = context?.packageName.trim().toLowerCase();
|
|
100
|
+
if (!context || !wrote || wrote === packageName.trim().toLowerCase()) {
|
|
101
|
+
return context;
|
|
102
|
+
}
|
|
103
|
+
writeWarning(`This workspace's seed context was recorded for ${context.packageName}, not ` + `${packageName} — its version, folder, deployment name and service URL are ignored. ` + `Nothing then vouches for the files in it as ${packageName}'s, so the run stops ` + "unless --refresh replaces them; the scope is resolved for " + `${packageName} on its own.`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
function checkContextTenant(context, config, options) {
|
|
107
|
+
if (!context || options.folderKey !== undefined || options.folderPath !== undefined) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const sameOrg = sameId(context.organizationId, config.organizationId);
|
|
111
|
+
const sameOrgName = sameName(context.organizationName, config.organizationName);
|
|
112
|
+
const sameTenant = sameId(context.tenantId, config.tenantId);
|
|
113
|
+
const sameTenantName = sameName(context.tenantName, config.tenantName);
|
|
114
|
+
if (sameOrg && sameOrgName && sameTenant && sameTenantName) {
|
|
115
|
+
if (!comparable(context.organizationId, config.organizationId) && !comparable(context.organizationName, config.organizationName) && !comparable(context.tenantId, config.tenantId) && !comparable(context.tenantName, config.tenantName)) {
|
|
116
|
+
writeWarning("This seed workspace's context records no organization or tenant to check " + `against the one you are logged in to (${config.organizationName ?? "?"} / ` + `${config.tenantName ?? "?"}), so nothing here can tell whether it was ` + "written for this tenant. Pass --folder-key <guid> or --folder-path <path> " + "if you mean to seed a workspace from somewhere else.");
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
return failure("invalid-option", "This seed workspace was written by a deploy into a different organization or tenant " + `than the one you are logged in to (${config.organizationName ?? "?"} / ${config.tenantName ?? "?"}).`, "Log in to the tenant the deploy landed in (uip login, or uip login tenant set), or pass " + "--folder-key <guid> or --folder-path <path> to seed this tenant deliberately.", { result: RESULTS.ValidationError });
|
|
121
|
+
}
|
|
122
|
+
function sameId(recorded, live) {
|
|
123
|
+
return !recorded || !live || recorded === live;
|
|
124
|
+
}
|
|
125
|
+
function sameName(recorded, live) {
|
|
126
|
+
return !recorded || !live || recorded.trim().toLowerCase() === live.trim().toLowerCase();
|
|
127
|
+
}
|
|
128
|
+
function comparable(recorded, live) {
|
|
129
|
+
return Boolean(recorded) && Boolean(live);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/services/seed-run/folder-scope.ts
|
|
133
|
+
async function resolveFolderKey(config, manifest, context, options, explicitFolder) {
|
|
134
|
+
if (explicitFolder !== undefined) {
|
|
135
|
+
return { ok: true, folderKey: explicitFolder };
|
|
136
|
+
}
|
|
137
|
+
if (manifest.dataFabric?.schemaFilename) {
|
|
138
|
+
return { ok: true, folderKey: undefined };
|
|
139
|
+
}
|
|
140
|
+
const deployed = context?.packageVersion === options.packageVersion ? context : undefined;
|
|
141
|
+
if (deployed?.folderKey) {
|
|
142
|
+
writeProgress("Scoping Data Fabric calls to the solution folder recorded at deploy time (from the seed context).");
|
|
143
|
+
return { ok: true, folderKey: deployed.folderKey };
|
|
144
|
+
}
|
|
145
|
+
return resolveInstalledFolderKey(config, manifest, deployed, options);
|
|
146
|
+
}
|
|
147
|
+
async function resolveInstalledFolderKey(config, manifest, context, options) {
|
|
148
|
+
const solutionName = solutionNameFromManifest(manifest);
|
|
149
|
+
if (!solutionName) {
|
|
150
|
+
return needsFolderKey("The seed context records no folder and the manifest names no solution to look one up by.");
|
|
151
|
+
}
|
|
152
|
+
const [error, deployments] = await catchError(listDeploymentsOfSolution(config, solutionName, options.signal, context?.deploymentName));
|
|
153
|
+
if (error || !deployments) {
|
|
154
|
+
return failure("service-error", `Could not list deployments to resolve the folder scope: ${error?.message ?? "unknown error"}.`, "Fix the lookup problem and re-run, or pass --folder-key <guid> to scope the run yourself.");
|
|
155
|
+
}
|
|
156
|
+
const resolved = resolveInstalledFolder({
|
|
157
|
+
deployments,
|
|
158
|
+
deploymentName: context?.deploymentName,
|
|
159
|
+
solutionName,
|
|
160
|
+
packageVersion: options.packageVersion
|
|
161
|
+
});
|
|
162
|
+
if (resolved.kind === "resolved") {
|
|
163
|
+
const path = await findFolderPath(config, resolved.folderKey, options.signal);
|
|
164
|
+
writeProgress(`Scoping Data Fabric calls to folder ${path ?? resolved.folderKey} ` + `(installed deployment "${resolved.deployment.name}").`);
|
|
165
|
+
return { ok: true, folderKey: resolved.folderKey };
|
|
166
|
+
}
|
|
167
|
+
if (resolved.kind === "ambiguous") {
|
|
168
|
+
const installs = resolved.candidates.map((candidate) => {
|
|
169
|
+
const target = candidate.folderPath ? `, deployed into ${candidate.folderPath}` : "";
|
|
170
|
+
return `${candidate.installedRootFolderKey} (v${candidate.packageVersion}${target})`;
|
|
171
|
+
}).sort((a, b) => a.localeCompare(b)).join(", ");
|
|
172
|
+
return needsFolderKey(`Solution "${solutionName}" is installed in several folders (${installs}).`);
|
|
173
|
+
}
|
|
174
|
+
return needsFolderKey(`No installed deployment was found for solution "${solutionName}".`);
|
|
175
|
+
}
|
|
176
|
+
function rejectFolderPathForTenantLevel(manifest, options, packageName) {
|
|
177
|
+
if (options.folderPath === undefined || !manifest.dataFabric?.schemaFilename) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
return invalidSeedOption(`--folder-path cannot be used with ${packageName}: its ` + "Data Fabric entities are tenant-level (the manifest declares dataFabric.schemaFilename), " + "not inside a solution folder.", "Drop --folder-path to seed the tenant-level entities. To scope the run into a folder " + "on purpose, pass that folder's key with --folder-key <guid>.");
|
|
181
|
+
}
|
|
182
|
+
async function explicitFolderKey(config, options) {
|
|
183
|
+
if (options.folderPath !== undefined) {
|
|
184
|
+
return folderKeyForPath(config, options);
|
|
185
|
+
}
|
|
186
|
+
return { ok: true, folderKey: options.folderKey };
|
|
187
|
+
}
|
|
188
|
+
async function folderKeyForPath(config, options) {
|
|
189
|
+
const wanted = (options.folderPath ?? "").trim();
|
|
190
|
+
const [error, folders] = await catchError(listAllFolders(config, options.signal));
|
|
191
|
+
if (error || !folders) {
|
|
192
|
+
return failure("service-error", `Could not read the Orchestrator folders to resolve --folder-path ${wanted}: ` + `${error?.message ?? "unknown error"}`, "Pass the folder's key with --folder-key <guid> instead, or give this login the " + "right to read Orchestrator folders.", { errorCode: "server_error", retry: "RetryLater" });
|
|
193
|
+
}
|
|
194
|
+
const lower = wanted.toLowerCase();
|
|
195
|
+
const byPath = folders.filter((folder) => folder.path.toLowerCase() === lower);
|
|
196
|
+
const matches = byPath.length > 0 ? byPath : folders.filter((folder) => folder.displayName.toLowerCase() === lower);
|
|
197
|
+
const [match, ...others] = matches;
|
|
198
|
+
if (match === undefined) {
|
|
199
|
+
return invalidSeedOption(`--folder-path ${wanted} matches no Orchestrator folder in this tenant.`, "Pass the full path as Orchestrator shows it (for example " + "Shared/Lending/LoanSetup), or the folder's key with --folder-key <guid>.");
|
|
200
|
+
}
|
|
201
|
+
if (others.length > 0) {
|
|
202
|
+
return invalidSeedOption(`--folder-path ${wanted} matches ${matches.length} folders: ` + `${matches.map((folder) => folder.path).join(", ")}.`, "Pass the full path of the one you mean, or --folder-key <guid>.");
|
|
203
|
+
}
|
|
204
|
+
if (options.folderKey && match.key.toLowerCase() !== options.folderKey.toLowerCase()) {
|
|
205
|
+
return invalidSeedOption(`--folder-path ${wanted} (${match.key}) and --folder-key ` + `${options.folderKey} name different folders.`, "Drop one of the two, or correct it so both point at the same folder.");
|
|
206
|
+
}
|
|
207
|
+
writeProgress(`Scoping Data Fabric calls to folder ${match.path} (--folder-path).`);
|
|
208
|
+
return { ok: true, folderKey: match.key };
|
|
209
|
+
}
|
|
210
|
+
function invalidSeedOption(message, instructions) {
|
|
211
|
+
return failure("invalid-option", message, instructions);
|
|
212
|
+
}
|
|
213
|
+
function needsFolderKey(why) {
|
|
214
|
+
return failure("missing-option", `The folder to seed into could not be determined. ${why}`, "Pass --folder-key <guid> with the key of the folder the solution installed into (the " + "Solution folder under the deploy target in Orchestrator), or re-deploy the package — a " + "deploy records the folder in the seed context.", { result: RESULTS.ValidationError, errorCode: "invalid_argument" });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/services/seed-run/retry-command.ts
|
|
218
|
+
function seedRetryCommand(options) {
|
|
219
|
+
const flag = (name, value) => value === undefined ? "" : ` ${name} ${shellQuote(value)}`;
|
|
220
|
+
return "uip vss deploy seed" + flag("--dir", options.dir) + flag("--package-name", options.packageName) + flag("--package-version", options.packageVersion) + flag("--folder-key", options.folderKey) + flag("--folder-path", options.folderPath) + routingFlags(options) + (options.yes === true ? " --yes" : "");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/services/seed-run/refresh.ts
|
|
224
|
+
async function refreshAndScope(input) {
|
|
225
|
+
const { config, context, packageName, packageVersion, options } = input;
|
|
226
|
+
const tenantLevel = rejectFolderPathForTenantLevel(input.manifest, options, packageName);
|
|
227
|
+
if (tenantLevel)
|
|
228
|
+
return tenantLevel;
|
|
229
|
+
const explicit = await explicitFolderKey(config, {
|
|
230
|
+
...options,
|
|
231
|
+
packageVersion
|
|
232
|
+
});
|
|
233
|
+
if (!explicit.ok)
|
|
234
|
+
return explicit;
|
|
235
|
+
const explicitFolder = explicit.folderKey;
|
|
236
|
+
const refreshed = await refreshIfAsked({ ...input, explicitFolder });
|
|
237
|
+
if (!refreshed.ok)
|
|
238
|
+
return refreshed;
|
|
239
|
+
const scope = await resolveFolderKey(config, input.manifest, context, { ...options, packageVersion }, explicitFolder);
|
|
240
|
+
const folderKey = scope.ok ? scope.folderKey : undefined;
|
|
241
|
+
if (!refreshed.refreshed && folderKey !== undefined) {
|
|
242
|
+
warnIfFilledForAnotherFolder(context?.folderKey, folderKey);
|
|
243
|
+
}
|
|
244
|
+
const scopedElsewhere = folderKey !== undefined && context?.folderKey?.toLowerCase() !== folderKey.toLowerCase();
|
|
245
|
+
const servedElsewhere = input.serviceUrl !== undefined && context?.serviceUrl !== input.serviceUrl;
|
|
246
|
+
const sameInstall = context?.packageVersion === packageVersion && !scopedElsewhere;
|
|
247
|
+
if (refreshed.refreshed && (!sameInstall || servedElsewhere)) {
|
|
248
|
+
await recordRefreshedContext(input.workspaceDir, options, {
|
|
249
|
+
packageName,
|
|
250
|
+
packageVersion,
|
|
251
|
+
serviceUrl: input.serviceUrl,
|
|
252
|
+
organizationId: config.organizationId,
|
|
253
|
+
organizationName: config.organizationName,
|
|
254
|
+
tenantId: config.tenantId,
|
|
255
|
+
tenantName: config.tenantName,
|
|
256
|
+
folderKey: sameInstall ? folderKey ?? context?.folderKey : folderKey,
|
|
257
|
+
...sameInstall && context?.deploymentName !== undefined ? { deploymentName: context.deploymentName } : {}
|
|
258
|
+
}, Boolean(input.manifest.dataFabric?.schemaFilename));
|
|
259
|
+
}
|
|
260
|
+
return scope;
|
|
261
|
+
}
|
|
262
|
+
function warnIfFilledForAnotherFolder(recordedFolder, folderKey) {
|
|
263
|
+
if (recordedFolder === undefined) {
|
|
264
|
+
writeWarning("The seed workspace records no folder, so nothing says the files in it " + `were filled in for ${folderKey}. Those rows are about to be written ` + "there. Pass --refresh to replace them with this version's templates " + "first if they belong somewhere else.");
|
|
265
|
+
} else if (recordedFolder.toLowerCase() !== folderKey.toLowerCase()) {
|
|
266
|
+
writeWarning(`The seed workspace holds the files filled in for folder ${recordedFolder}, ` + `and this run is scoped to ${folderKey}. Those rows are about to be written ` + "there. Pass --refresh to replace them with this version's templates first, " + "or drop the folder option to seed the folder they belong to.");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function refreshIfAsked(input) {
|
|
270
|
+
const { context, packageName, packageVersion, options } = input;
|
|
271
|
+
const recorded = context?.packageVersion;
|
|
272
|
+
const recordedOther = recorded !== undefined && recorded !== packageVersion;
|
|
273
|
+
const unknownVersion = recorded === undefined && !input.creating && options.templatesAreCurrent !== true;
|
|
274
|
+
const scopedElsewhere = context?.folderKey !== undefined && input.explicitFolder !== undefined && input.explicitFolder.toLowerCase() !== context.folderKey.toLowerCase();
|
|
275
|
+
if (options.refresh !== true && !input.creating) {
|
|
276
|
+
if (recordedOther) {
|
|
277
|
+
return oldTemplatesKept(packageName, packageVersion, context);
|
|
278
|
+
}
|
|
279
|
+
return unknownVersion ? unknownTemplatesKept(options, packageName, packageVersion, false) : { ok: true, refreshed: false };
|
|
280
|
+
}
|
|
281
|
+
const fallsBack = !input.creating && !recordedOther && !unknownVersion && !scopedElsewhere;
|
|
282
|
+
const refreshed = await refreshTemplates(input, fallsBack ? " Seeding the files already in the workspace." : "");
|
|
283
|
+
if (refreshed || fallsBack)
|
|
284
|
+
return { ok: true, refreshed };
|
|
285
|
+
if (input.creating)
|
|
286
|
+
return noWorkspaceCreated(packageName, packageVersion);
|
|
287
|
+
if (recordedOther) {
|
|
288
|
+
return oldTemplatesKept(packageName, packageVersion, context);
|
|
289
|
+
}
|
|
290
|
+
if (scopedElsewhere) {
|
|
291
|
+
return otherFolderTemplatesKept(packageName, packageVersion, context);
|
|
292
|
+
}
|
|
293
|
+
return unknownTemplatesKept(options, packageName, packageVersion, true);
|
|
294
|
+
}
|
|
295
|
+
function otherFolderTemplatesKept(packageName, packageVersion, context) {
|
|
296
|
+
return failure("service-error", `Could not refresh the seed templates of ${packageName} ${packageVersion}: the ` + `workspace still holds the files filled in for folder ${String(context?.folderKey)}, ` + "and this run is scoped to another one, so nothing was seeded.", "Fix the problem reported above and run the same command again. To seed the folder " + "the files were filled in for, drop the folder option instead.", { errorCode: "server_error", retry: "RetryLater" });
|
|
297
|
+
}
|
|
298
|
+
function unknownTemplatesKept(options, packageName, packageVersion, refreshAttempted) {
|
|
299
|
+
const message = `The seed workspace of ${packageName} records no version, so the files in ` + `it cannot be shown to be ${packageVersion}'s; nothing was seeded.`;
|
|
300
|
+
return failure("service-error", message, refreshAttempted ? RETRY_AFTER_FIXING : `Refresh them with: ${seedRetryCommand(options)} --refresh`, { errorCode: "server_error", retry: "RetryLater" });
|
|
301
|
+
}
|
|
302
|
+
var RETRY_AFTER_FIXING = "Fix the problem reported above and run the same command again.";
|
|
303
|
+
function oldTemplatesKept(packageName, packageVersion, context) {
|
|
304
|
+
return failure("service-error", `Could not refresh the seed templates of ${packageName} to ${packageVersion}: ` + `the workspace still holds the ${String(context?.packageVersion)} templates, ` + "so nothing was seeded.", RETRY_AFTER_FIXING, { errorCode: "server_error", retry: "RetryLater" });
|
|
305
|
+
}
|
|
306
|
+
function noWorkspaceCreated(packageName, packageVersion) {
|
|
307
|
+
return failure("service-error", `Could not create the seed workspace for ${packageName} ${packageVersion}: ` + "the seed templates could not be downloaded or written.", RETRY_AFTER_FIXING, { errorCode: "server_error", retry: "RetryLater" });
|
|
308
|
+
}
|
|
309
|
+
async function recordRefreshedContext(workspaceDir, options, context, tenantLevel) {
|
|
310
|
+
const [error] = await catchError(writeSeedContext(context, workspaceDir));
|
|
311
|
+
if (!error) {
|
|
312
|
+
writeProgress(`Recorded ${context.packageName} ${context.packageVersion} in .seed-context.json.`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const [removeError] = await catchError(removeSeedContext(workspaceDir));
|
|
316
|
+
const stale = removeError ? ` The previous .seed-context.json could not be removed either (${removeError.message}), ` + "so do not run 'uip vss deploy seed' without arguments." : "";
|
|
317
|
+
const again = `uip vss deploy seed --package-name ${shellQuote(context.packageName)}` + ` --package-version ${shellQuote(context.packageVersion)}` + (isDefaultWorkspace(workspaceDir, context.packageName) ? "" : ` --dir ${shellQuote(workspaceDir)}`) + folderAdvice(context.folderKey, tenantLevel) + routingFlags(options) + (options.yes === true ? " --yes" : "") + " --refresh";
|
|
318
|
+
writeWarning(`Could not update .seed-context.json: ${error.message}.${stale} The workspace holds ` + `${context.packageName} ${context.packageVersion}'s templates, not yet filled in. ` + `Fix that, then run: ${again} — before you fill the CSVs in, since --refresh ` + "replaces them. A run without it refuses these files as an unknown version.");
|
|
319
|
+
}
|
|
320
|
+
function isDefaultWorkspace(workspaceDir, packageName) {
|
|
321
|
+
const fs = getFileSystem();
|
|
322
|
+
if (packageNameProblem(packageName))
|
|
323
|
+
return false;
|
|
324
|
+
return fs.path.resolve(workspaceDir) === fs.path.resolve(packageDir(packageName));
|
|
325
|
+
}
|
|
326
|
+
function folderAdvice(folderKey, tenantLevel) {
|
|
327
|
+
if (folderKey)
|
|
328
|
+
return ` --folder-key ${shellQuote(folderKey)}`;
|
|
329
|
+
return tenantLevel ? "" : " --folder-key <key>";
|
|
330
|
+
}
|
|
331
|
+
async function refreshTemplates(input, fallback) {
|
|
332
|
+
const [error, download] = await catchError(downloadSeedTemplates(input.config, input.packageName, input.packageVersion, input.options.signal));
|
|
333
|
+
if (error || !download) {
|
|
334
|
+
input.options.signal?.throwIfAborted();
|
|
335
|
+
writeWarning(`Could not refresh the seed templates: ${error?.message ?? "unknown error"}.${fallback}`);
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
const [extractError, result] = await catchError(refreshSeedTemplates({
|
|
339
|
+
dir: input.workspaceDir,
|
|
340
|
+
zip: download.zip,
|
|
341
|
+
lockHeld: input.lockHeld ?? false
|
|
342
|
+
}));
|
|
343
|
+
if (extractError || !result) {
|
|
344
|
+
writeWarning(`Could not extract the seed templates: ${extractError?.message ?? "unknown error"}.${fallback}`);
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
for (const warning of result.warnings)
|
|
348
|
+
writeWarning(warning);
|
|
349
|
+
writeProgress(`Refreshed ${result.written.length} seed ${result.written.length === 1 ? "file" : "files"} in ${result.dir}`);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/api/solution-resources.ts
|
|
354
|
+
var SOLUTION_COLLECTIONS = [
|
|
355
|
+
"Processes",
|
|
356
|
+
"Assets",
|
|
357
|
+
"StorageBuckets",
|
|
358
|
+
"Connections"
|
|
359
|
+
];
|
|
360
|
+
async function readSolutionResources(config, folderKey, wanted, signal) {
|
|
361
|
+
const resources = {
|
|
362
|
+
processes: [],
|
|
363
|
+
assets: [],
|
|
364
|
+
buckets: [],
|
|
365
|
+
connections: [],
|
|
366
|
+
warnings: []
|
|
367
|
+
};
|
|
368
|
+
const [folderError, folder] = await catchError(readFolder(config, folderKey, signal));
|
|
369
|
+
if (folderError || !folder) {
|
|
370
|
+
const why = folderError ? await extractErrorMessage(folderError) : "no such folder";
|
|
371
|
+
resources.warnings.push(`Could not read the solution folder: ${why}`);
|
|
372
|
+
return resources;
|
|
373
|
+
}
|
|
374
|
+
const collect = async (what, into, read) => {
|
|
375
|
+
const [error, rows] = await catchError(read());
|
|
376
|
+
if (error) {
|
|
377
|
+
resources.warnings.push(`Could not read the solution's ${what}: ${await extractErrorMessage(error)}`);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
into.push(...rows);
|
|
381
|
+
};
|
|
382
|
+
if (wanted.has("Processes")) {
|
|
383
|
+
await collect("processes", resources.processes, () => readProcesses(config, folderKey, signal));
|
|
384
|
+
}
|
|
385
|
+
if (wanted.has("Assets")) {
|
|
386
|
+
await collect("assets", resources.assets, () => readAssets(config, folderKey, signal));
|
|
387
|
+
}
|
|
388
|
+
if (wanted.has("StorageBuckets")) {
|
|
389
|
+
await collect("storage buckets", resources.buckets, () => readBucketsWithChildren(config, folderKey, resources.warnings, signal));
|
|
390
|
+
}
|
|
391
|
+
if (wanted.has("Connections")) {
|
|
392
|
+
await collect("connections", resources.connections, () => readConnections(config, folderKey, signal));
|
|
393
|
+
}
|
|
394
|
+
return { ...resources, folder };
|
|
395
|
+
}
|
|
396
|
+
async function readFolder(config, folderKey, signal) {
|
|
397
|
+
const api = await orchestratorApi(config, (sdk) => sdk.FoldersApi);
|
|
398
|
+
const folder = await api.foldersGetByKeyByIdentifier({ identifier: folderKey }, { signal });
|
|
399
|
+
return {
|
|
400
|
+
id: folder.id,
|
|
401
|
+
key: folder.key ?? folderKey,
|
|
402
|
+
path: folder.fullyQualifiedName ?? undefined,
|
|
403
|
+
parentId: folder.parentId ?? undefined,
|
|
404
|
+
parentKey: folder.parentKey ?? undefined
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
async function readProcesses(config, folderKey, signal) {
|
|
408
|
+
const api = await orchestratorApi(config, (sdk) => sdk.ReleasesApi, folderKey);
|
|
409
|
+
const releases = await readAllPages("processes", (page) => api.releasesGet(page, { signal }));
|
|
410
|
+
return releases.map((release) => ({
|
|
411
|
+
id: release.id,
|
|
412
|
+
key: release.key,
|
|
413
|
+
name: release.name,
|
|
414
|
+
processKey: release.processKey,
|
|
415
|
+
processVersion: release.processVersion,
|
|
416
|
+
processType: release.processType
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
async function readAssets(config, folderKey, signal) {
|
|
420
|
+
const api = await orchestratorApi(config, (sdk) => sdk.AssetsApi, folderKey);
|
|
421
|
+
const assets = await readAllPages("assets", (page) => api.assetsGetFiltered(page, { signal }));
|
|
422
|
+
return assets.map((asset) => ({
|
|
423
|
+
id: asset.id,
|
|
424
|
+
name: asset.name,
|
|
425
|
+
value: asset.value ?? undefined,
|
|
426
|
+
valueType: asset.valueType
|
|
427
|
+
}));
|
|
428
|
+
}
|
|
429
|
+
async function readBuckets(config, folderKey, signal) {
|
|
430
|
+
const api = await orchestratorApi(config, (sdk) => sdk.BucketsApi, folderKey);
|
|
431
|
+
const buckets = await readAllPages("storage buckets", (page) => api.bucketsGet(page, { signal }));
|
|
432
|
+
return buckets.map((bucket) => ({
|
|
433
|
+
id: bucket.id,
|
|
434
|
+
name: bucket.name,
|
|
435
|
+
identifier: bucket.identifier
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
async function readBucketsWithChildren(config, folderKey, warnings, signal) {
|
|
439
|
+
const buckets = await readBuckets(config, folderKey, signal);
|
|
440
|
+
const [listError, folders] = await catchError(listFolderDtos(config, signal));
|
|
441
|
+
if (listError || !folders) {
|
|
442
|
+
warnings.push("Could not list the Solution folders under the installed folder, so their storage " + `buckets were not read: ${await extractErrorMessage(listError)}`);
|
|
443
|
+
return buckets;
|
|
444
|
+
}
|
|
445
|
+
const wanted = folderKey.toLowerCase();
|
|
446
|
+
const children = folders.filter((folder) => folder.key && folder.folderType === "Solution" && folder.parentKey?.toLowerCase() === wanted);
|
|
447
|
+
for (const child of children) {
|
|
448
|
+
if (!child.key)
|
|
449
|
+
continue;
|
|
450
|
+
if (signal?.aborted)
|
|
451
|
+
break;
|
|
452
|
+
const [readError, childBuckets] = await catchError(readBuckets(config, child.key, signal));
|
|
453
|
+
if (readError || !childBuckets) {
|
|
454
|
+
warnings.push(`Could not read the storage buckets of the Solution folder ${child.displayName || child.key}: ` + await extractErrorMessage(readError));
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
buckets.push(...childBuckets);
|
|
458
|
+
}
|
|
459
|
+
return buckets;
|
|
460
|
+
}
|
|
461
|
+
var CONNECTION_PAGE_SIZE = 200;
|
|
462
|
+
var MAX_CONNECTION_PAGES = 50;
|
|
463
|
+
async function readConnections(config, folderKey, signal) {
|
|
464
|
+
const {
|
|
465
|
+
ConnectionsApi,
|
|
466
|
+
createConnectionsConfig,
|
|
467
|
+
folderOverride,
|
|
468
|
+
normalizeConnectionStatus
|
|
469
|
+
} = await import("./index-h43sj8at.js");
|
|
470
|
+
const api = new ConnectionsApi(await createConnectionsConfig({ tenant: config.tenantName }));
|
|
471
|
+
const scoped = folderOverride(folderKey);
|
|
472
|
+
const connections = [];
|
|
473
|
+
let previousFirst;
|
|
474
|
+
for (let pageIndex = 0;pageIndex < MAX_CONNECTION_PAGES; pageIndex += 1) {
|
|
475
|
+
const page = await api.apiV1ConnectionsGet({ pageIndex, pageSize: CONNECTION_PAGE_SIZE }, async (context) => ({
|
|
476
|
+
...scoped ? await scoped(context) : context.init,
|
|
477
|
+
signal
|
|
478
|
+
}));
|
|
479
|
+
const first = page[0]?.id;
|
|
480
|
+
if (page.length === 0 || first !== undefined && first === previousFirst) {
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
previousFirst = first;
|
|
484
|
+
for (const connection of page) {
|
|
485
|
+
connections.push({
|
|
486
|
+
id: connection.id,
|
|
487
|
+
name: connection.name ?? undefined,
|
|
488
|
+
state: connection.state === undefined ? undefined : normalizeConnectionStatus(connection.state)
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
if (page.length < CONNECTION_PAGE_SIZE)
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
return connections;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/services/seed/order.ts
|
|
498
|
+
class SeedCycleError extends Error {
|
|
499
|
+
entities;
|
|
500
|
+
result = RESULTS.ValidationError;
|
|
501
|
+
errorCode = "invalid_argument";
|
|
502
|
+
retry = "RetryWillNotFix";
|
|
503
|
+
instructions;
|
|
504
|
+
constructor(entities) {
|
|
505
|
+
super(`These entities reference each other in a cycle, so none of them can be ` + `seeded before the others: ${entities.join(", ")}.`);
|
|
506
|
+
this.entities = entities;
|
|
507
|
+
this.name = "SeedCycleError";
|
|
508
|
+
this.instructions = "Seed them in separate runs: move one entity's CSV out of the seeding " + "folder, seed, then put it back and seed again so its relationship " + "columns can resolve. A relationship cell holding a record id instead " + "of a local Id does not need the other entity seeded first.";
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function orderEntities(names, lookup) {
|
|
512
|
+
const dependsOn = dependencies(names, lookup);
|
|
513
|
+
const ordered = [];
|
|
514
|
+
const remaining = [...names];
|
|
515
|
+
while (remaining.length > 0) {
|
|
516
|
+
const index = remaining.findIndex((name) => (dependsOn.get(name)?.size ?? 0) === 0);
|
|
517
|
+
if (index === -1) {
|
|
518
|
+
throw new SeedCycleError(inCycle(remaining, dependsOn));
|
|
519
|
+
}
|
|
520
|
+
const [next] = remaining.splice(index, 1);
|
|
521
|
+
ordered.push(next);
|
|
522
|
+
for (const deps of dependsOn.values())
|
|
523
|
+
deps.delete(next.toLowerCase());
|
|
524
|
+
}
|
|
525
|
+
return ordered;
|
|
526
|
+
}
|
|
527
|
+
function dependencies(names, lookup) {
|
|
528
|
+
const wanted = new Set(names.map((name) => name.toLowerCase()));
|
|
529
|
+
const dependsOn = new Map;
|
|
530
|
+
for (const name of names) {
|
|
531
|
+
const deps = new Set;
|
|
532
|
+
const entity = lookup.byName.get(name.toLowerCase());
|
|
533
|
+
for (const field of entity?.fields.values() ?? []) {
|
|
534
|
+
const target = field.referenceEntityName?.toLowerCase();
|
|
535
|
+
if (field.isForeignKey && target && target !== name.toLowerCase() && wanted.has(target)) {
|
|
536
|
+
deps.add(target);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
dependsOn.set(name, deps);
|
|
540
|
+
}
|
|
541
|
+
return dependsOn;
|
|
542
|
+
}
|
|
543
|
+
function inCycle(remaining, dependsOn) {
|
|
544
|
+
let members = [...remaining];
|
|
545
|
+
for (;; ) {
|
|
546
|
+
const pointedAt = new Set(members.flatMap((name) => [...dependsOn.get(name) ?? []]));
|
|
547
|
+
const kept = members.filter((name) => pointedAt.has(name.toLowerCase()));
|
|
548
|
+
if (kept.length === members.length)
|
|
549
|
+
return members;
|
|
550
|
+
members = kept;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/services/seed-run/report.ts
|
|
555
|
+
function describePlans(plans) {
|
|
556
|
+
let inserts = 0;
|
|
557
|
+
let updates = 0;
|
|
558
|
+
let unchanged = 0;
|
|
559
|
+
for (const plan of plans) {
|
|
560
|
+
inserts += plan.toInsert.length;
|
|
561
|
+
updates += plan.toUpdate.length;
|
|
562
|
+
unchanged += plan.unchanged;
|
|
563
|
+
const parts = [];
|
|
564
|
+
if (plan.toInsert.length > 0)
|
|
565
|
+
parts.push(`+${plan.toInsert.length} insert`);
|
|
566
|
+
if (plan.toUpdate.length > 0)
|
|
567
|
+
parts.push(`~${plan.toUpdate.length} update`);
|
|
568
|
+
if (plan.toRepairAttachments.length > 0)
|
|
569
|
+
parts.push(`${plan.toRepairAttachments.length} attachment retry`);
|
|
570
|
+
if (plan.ignored > 0)
|
|
571
|
+
parts.push(`${plan.ignored} changed (ignored, insert strategy)`);
|
|
572
|
+
if (plan.unchanged > 0)
|
|
573
|
+
parts.push(`${plan.unchanged} unchanged`);
|
|
574
|
+
writeProgress(` ${plan.entityName}: ${parts.length > 0 ? parts.join(", ") : "nothing to do"}`);
|
|
575
|
+
}
|
|
576
|
+
if (plans.length > 0) {
|
|
577
|
+
writeProgress(`Total: +${inserts} insert, ~${updates} update, ${unchanged} unchanged`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function nothingToSeed(plans) {
|
|
581
|
+
const ignored = plans.reduce((total, plan) => total + plan.ignored, 0);
|
|
582
|
+
if (ignored === 0)
|
|
583
|
+
return "All records are up to date. Nothing to seed.";
|
|
584
|
+
return `Nothing to seed: ${ignored} ${ignored === 1 ? "row differs" : "rows differ"} from the seed ` + "files but the entity's strategy is insert, so existing records are left alone. " + "Every other record is up to date.";
|
|
585
|
+
}
|
|
586
|
+
function hasEntityWork(plan) {
|
|
587
|
+
return plan.toInsert.length > 0 || plan.toUpdate.length > 0 || plan.toRepairAttachments.length > 0;
|
|
588
|
+
}
|
|
589
|
+
function hasWork(plans) {
|
|
590
|
+
return plans.some(hasEntityWork);
|
|
591
|
+
}
|
|
592
|
+
function describeResult(result) {
|
|
593
|
+
const parts = [];
|
|
594
|
+
if (result.inserted > 0)
|
|
595
|
+
parts.push(`+${result.inserted} inserted`);
|
|
596
|
+
if (result.updated > 0)
|
|
597
|
+
parts.push(`~${result.updated} updated`);
|
|
598
|
+
if (result.attachmentsUploaded > 0)
|
|
599
|
+
parts.push(`${result.attachmentsUploaded} attachment(s) uploaded`);
|
|
600
|
+
if (result.attachmentsCleared > 0)
|
|
601
|
+
parts.push(`${result.attachmentsCleared} attachment(s) removed`);
|
|
602
|
+
if (result.failed > 0)
|
|
603
|
+
parts.push(`!${result.failed} failed`);
|
|
604
|
+
const attachmentFailures = result.errors.filter((error) => /attachment|uploading/.test(error)).length;
|
|
605
|
+
if (attachmentFailures > 0)
|
|
606
|
+
parts.push(`!${attachmentFailures} attachment problem(s)`);
|
|
607
|
+
return parts.length > 0 ? parts.join(", ") : "nothing written";
|
|
608
|
+
}
|
|
609
|
+
function finish(workspaceDir, summaries, errors, retry) {
|
|
610
|
+
const executed = summaries.filter((summary) => summary.inserted > 0 || summary.updated > 0);
|
|
611
|
+
const unexplained = summaries.reduce((total, summary) => total + summary.failed, 0);
|
|
612
|
+
if (errors.length === 0 && unexplained > 0) {
|
|
613
|
+
errors.push(`${unexplained} ${unexplained === 1 ? "row was" : "rows were"} not written and the ` + "service gave no reason.");
|
|
614
|
+
}
|
|
615
|
+
if (errors.length === 0) {
|
|
616
|
+
if (executed.length > 0) {
|
|
617
|
+
writeProgress("Seeding completed successfully.");
|
|
618
|
+
}
|
|
619
|
+
return { ok: true, workspaceDir, summaries };
|
|
620
|
+
}
|
|
621
|
+
const problems = collapseRowProblems(errors);
|
|
622
|
+
const noun = problems.length === 1 ? "problem" : "problems";
|
|
623
|
+
const list = problems.map((problem) => ` - ${problem}`).join(`
|
|
624
|
+
`);
|
|
625
|
+
writeWarning(`Seeding finished with ${problems.length} ${noun}:
|
|
626
|
+
${list}`);
|
|
627
|
+
const message = `Seeding finished with ${problems.length} ${noun}: ${problems.map((problem) => problem.replace(/\s*\n\s*/g, " ")).join("; ")}`;
|
|
628
|
+
return {
|
|
629
|
+
...failure("service-error", message, `Fix the seed files or the scope, then retry with: ${retry} — rows already seeded are ` + "recorded in the seed journal and will not be inserted twice."),
|
|
630
|
+
workspaceDir,
|
|
631
|
+
summaries
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function finishIdle(run, plans) {
|
|
635
|
+
return finish(run.input.workspaceDir, [...run.summaries, ...plans.map(nothingDone)], run.errors, run.retry);
|
|
636
|
+
}
|
|
637
|
+
function nothingDone(plan) {
|
|
638
|
+
return {
|
|
639
|
+
entityName: plan.entityName,
|
|
640
|
+
inserted: 0,
|
|
641
|
+
updated: 0,
|
|
642
|
+
unchanged: plan.unchanged,
|
|
643
|
+
ignored: plan.ignored,
|
|
644
|
+
failed: 0,
|
|
645
|
+
attachmentsUploaded: 0,
|
|
646
|
+
skipped: false
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function emptySummary(entityName, skipReason) {
|
|
650
|
+
return {
|
|
651
|
+
entityName,
|
|
652
|
+
inserted: 0,
|
|
653
|
+
updated: 0,
|
|
654
|
+
unchanged: 0,
|
|
655
|
+
ignored: 0,
|
|
656
|
+
failed: 0,
|
|
657
|
+
attachmentsUploaded: 0,
|
|
658
|
+
skipped: skipReason !== undefined,
|
|
659
|
+
...skipReason ? { skipReason } : {}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/services/seed-run/solution-namespace.ts
|
|
664
|
+
function solutionCollections(manifest, resources) {
|
|
665
|
+
const declared = manifest.solution;
|
|
666
|
+
const collections = new Map;
|
|
667
|
+
const processes = new Map;
|
|
668
|
+
for (const name of declared?.processes ?? []) {
|
|
669
|
+
const release = findByName(resources.processes, name);
|
|
670
|
+
if (!release)
|
|
671
|
+
continue;
|
|
672
|
+
processes.set(name, properties({
|
|
673
|
+
Id: release.id,
|
|
674
|
+
Key: release.key,
|
|
675
|
+
Name: release.name,
|
|
676
|
+
ProcessKey: release.processKey,
|
|
677
|
+
Version: release.processVersion,
|
|
678
|
+
Type: release.processType
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
collections.set("Processes", processes);
|
|
682
|
+
const assets = new Map;
|
|
683
|
+
for (const name of declared?.assets ?? []) {
|
|
684
|
+
const asset = findByName(resources.assets, name);
|
|
685
|
+
if (!asset)
|
|
686
|
+
continue;
|
|
687
|
+
assets.set(name, properties({
|
|
688
|
+
Id: asset.id,
|
|
689
|
+
Name: asset.name,
|
|
690
|
+
Value: asset.value,
|
|
691
|
+
Type: asset.valueType
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
collections.set("Assets", assets);
|
|
695
|
+
const buckets = new Map;
|
|
696
|
+
for (const bucket of resources.buckets) {
|
|
697
|
+
buckets.set(bucket.name, properties({
|
|
698
|
+
Id: bucket.id,
|
|
699
|
+
Name: bucket.name,
|
|
700
|
+
Identifier: bucket.identifier
|
|
701
|
+
}));
|
|
702
|
+
}
|
|
703
|
+
collections.set("StorageBuckets", buckets);
|
|
704
|
+
const connections = new Map;
|
|
705
|
+
for (const [name, connectorKey] of declared?.connections ?? []) {
|
|
706
|
+
const connection = resources.connections.find((candidate) => candidate.name?.toLowerCase() === name.toLowerCase());
|
|
707
|
+
if (!connection)
|
|
708
|
+
continue;
|
|
709
|
+
connections.set(name, properties({
|
|
710
|
+
Id: connection.id,
|
|
711
|
+
Name: name,
|
|
712
|
+
ConnectorKey: connectorKey,
|
|
713
|
+
State: connection.state
|
|
714
|
+
}));
|
|
715
|
+
}
|
|
716
|
+
collections.set("Connections", connections);
|
|
717
|
+
return collections;
|
|
718
|
+
}
|
|
719
|
+
function findByName(resources, name) {
|
|
720
|
+
const wanted = name.toLowerCase();
|
|
721
|
+
return resources.find((resource) => resource.name.toLowerCase() === wanted);
|
|
722
|
+
}
|
|
723
|
+
function properties(values) {
|
|
724
|
+
const out = new Map;
|
|
725
|
+
for (const [key, value] of Object.entries(values)) {
|
|
726
|
+
if (value !== undefined)
|
|
727
|
+
out.set(key, String(value));
|
|
728
|
+
}
|
|
729
|
+
return out;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/services/seed-run/plan-all.ts
|
|
733
|
+
async function planAll(run, lookup, present, expressionContext) {
|
|
734
|
+
const { input } = run;
|
|
735
|
+
writeProgress(`
|
|
736
|
+
Comparing with live data...`);
|
|
737
|
+
const bundledFiles = await listBundledFiles(input.workspaceDir, present);
|
|
738
|
+
const ordered = orderEntities(present.map((file) => file.entityName), lookup);
|
|
739
|
+
const plans = [];
|
|
740
|
+
let journalChanged = false;
|
|
741
|
+
for (const entityName of ordered) {
|
|
742
|
+
const file = present.find((f) => f.entityName === entityName);
|
|
743
|
+
if (file === undefined)
|
|
744
|
+
continue;
|
|
745
|
+
const planned = await planEntity({
|
|
746
|
+
input,
|
|
747
|
+
sdk: run.sdk,
|
|
748
|
+
file,
|
|
749
|
+
journal: run.journal,
|
|
750
|
+
expressionContext,
|
|
751
|
+
bundledFiles,
|
|
752
|
+
idMaps: run.idMaps
|
|
753
|
+
});
|
|
754
|
+
if (planned.ok)
|
|
755
|
+
journalChanged ||= planned.plan.journalChanged;
|
|
756
|
+
const plan = acceptPlan(run, entityName, planned);
|
|
757
|
+
if (plan !== undefined)
|
|
758
|
+
plans.push(plan);
|
|
759
|
+
}
|
|
760
|
+
holdBackDependents(plans, run.summaries, run.errors, run.idMaps);
|
|
761
|
+
if (journalChanged) {
|
|
762
|
+
await saveJournal(input.workspaceDir, run.journalScope, run.journal);
|
|
763
|
+
}
|
|
764
|
+
return plans;
|
|
765
|
+
}
|
|
766
|
+
function acceptPlan(run, entityName, planned) {
|
|
767
|
+
if (!planned.ok) {
|
|
768
|
+
run.errors.push(...planned.errors);
|
|
769
|
+
run.summaries.push(emptySummary(entityName, "invalid"));
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const plan = planned.plan;
|
|
773
|
+
for (const warning of plan.warnings)
|
|
774
|
+
writeWarning(warning);
|
|
775
|
+
if (plan.errors.length > 0) {
|
|
776
|
+
run.errors.push(...plan.errors);
|
|
777
|
+
run.summaries.push(emptySummary(entityName, "invalid"));
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
run.idMaps.set(entityName.toLowerCase(), new Map(plan.idMap));
|
|
781
|
+
return plan;
|
|
782
|
+
}
|
|
783
|
+
function reportPlanning(run, plans, missing) {
|
|
784
|
+
if (run.errors.length === 0 && missing.length === 0) {
|
|
785
|
+
writeProgress("Validation passed.");
|
|
786
|
+
}
|
|
787
|
+
describePlans(plans);
|
|
788
|
+
if (missing.length > 0 || run.errors.length > 0) {
|
|
789
|
+
const named = [
|
|
790
|
+
...missing,
|
|
791
|
+
...run.summaries.filter((s) => s.skipped).map((s) => s.entityName)
|
|
792
|
+
];
|
|
793
|
+
writeWarning(`Planning failed for: ${[...new Set(named)].join(", ")} — those entities will not be seeded.`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async function planEntity(args) {
|
|
797
|
+
const { input, sdk, file } = args;
|
|
798
|
+
const { entity } = file;
|
|
799
|
+
const [recordsError, liveRecords] = await catchError(loadLiveRecords(sdk, entity.id, input.folderKey, needsExpandedRecords(entity)));
|
|
800
|
+
if (recordsError) {
|
|
801
|
+
return {
|
|
802
|
+
ok: false,
|
|
803
|
+
errors: [
|
|
804
|
+
`${file.entityName}: could not read the existing records: ${recordsError.message}`
|
|
805
|
+
]
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
const dropped = await dropStaleJournalEntries(sdk, entity, file.entityName, args.journal, liveRecords ?? [], input.folderKey);
|
|
809
|
+
const plan = buildEntityPlan({
|
|
810
|
+
entityName: file.entityName,
|
|
811
|
+
entity,
|
|
812
|
+
csv: file.csv,
|
|
813
|
+
strategy: file.strategy,
|
|
814
|
+
liveRecords: liveRecords ?? [],
|
|
815
|
+
journal: args.journal,
|
|
816
|
+
expressionContext: args.expressionContext,
|
|
817
|
+
choiceValues: await loadChoiceValues(sdk, entity, input.folderKey),
|
|
818
|
+
bundledFiles: args.bundledFiles,
|
|
819
|
+
idMaps: args.idMaps
|
|
820
|
+
});
|
|
821
|
+
return {
|
|
822
|
+
ok: true,
|
|
823
|
+
plan: dropped ? { ...plan, journalChanged: true } : plan
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
async function dropStaleJournalEntries(sdk, entity, entityName, journal, liveRecords, folderKey) {
|
|
827
|
+
const entries = journal[entityName];
|
|
828
|
+
if (!entries)
|
|
829
|
+
return false;
|
|
830
|
+
const liveIds = new Set(liveRecords.flatMap((record) => typeof record.Id === "string" ? [record.Id.toLowerCase()] : []));
|
|
831
|
+
let changed = false;
|
|
832
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
833
|
+
if (liveIds.has(entry.recordId.toLowerCase()))
|
|
834
|
+
continue;
|
|
835
|
+
const [error] = await catchError(sdk.entities.getRecordById(entity.id, entry.recordId, folderKey === undefined ? undefined : { folderKey }));
|
|
836
|
+
if (!error)
|
|
837
|
+
continue;
|
|
838
|
+
const { errorCode } = await extractErrorDetails(error);
|
|
839
|
+
if (errorCode === "not_found") {
|
|
840
|
+
delete entries[key];
|
|
841
|
+
changed = true;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return changed;
|
|
845
|
+
}
|
|
846
|
+
async function listBundledFiles(workspaceDir, files) {
|
|
847
|
+
const fs = getFileSystem();
|
|
848
|
+
const bundled = new Map;
|
|
849
|
+
for (const file of files) {
|
|
850
|
+
const dir = fs.path.join(workspaceDir, file.entityName);
|
|
851
|
+
const [listError, entries] = await catchError(fs.readdir(dir));
|
|
852
|
+
if (listError || !entries)
|
|
853
|
+
continue;
|
|
854
|
+
for (const entry of entries) {
|
|
855
|
+
const [, stats] = await catchError(fs.stat(fs.path.join(dir, entry)));
|
|
856
|
+
if (stats?.isFile()) {
|
|
857
|
+
bundled.set(`${file.entityName}/${entry}`, stats.size);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return bundled;
|
|
862
|
+
}
|
|
863
|
+
function holdBackDependents(plans, summaries, errors, idMaps) {
|
|
864
|
+
let held = true;
|
|
865
|
+
while (held) {
|
|
866
|
+
held = false;
|
|
867
|
+
const unavailable = skippedEntities(summaries);
|
|
868
|
+
for (const plan of plans.slice()) {
|
|
869
|
+
const dependency = dependsOn(plan, unavailable);
|
|
870
|
+
if (dependency === undefined)
|
|
871
|
+
continue;
|
|
872
|
+
errors.push(`${plan.entityName}: rows reference ${dependency} by local Id, and ${dependency} ` + `could not be planned — nothing written for ${plan.entityName}.`);
|
|
873
|
+
summaries.push(emptySummary(plan.entityName, "depends-on-skipped"));
|
|
874
|
+
plans.splice(plans.indexOf(plan), 1);
|
|
875
|
+
idMaps.delete(plan.entityName.toLowerCase());
|
|
876
|
+
held = true;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function skippedEntities(summaries) {
|
|
881
|
+
return new Map(summaries.filter((summary) => summary.skipped).map((summary) => [
|
|
882
|
+
summary.entityName.toLowerCase(),
|
|
883
|
+
summary.entityName
|
|
884
|
+
]));
|
|
885
|
+
}
|
|
886
|
+
function dependsOn(plan, unavailable) {
|
|
887
|
+
for (const row of [...plan.toInsert, ...plan.toUpdate]) {
|
|
888
|
+
for (const reference of Object.values(row.foreignKeys)) {
|
|
889
|
+
const name = unavailable.get(reference.entity);
|
|
890
|
+
if (name !== undefined)
|
|
891
|
+
return name;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
async function saveJournal(workspaceDir, scope, journal) {
|
|
897
|
+
const [error] = await catchError(writeJournal(workspaceDir, scope, journal, true));
|
|
898
|
+
if (error) {
|
|
899
|
+
writeWarning(`Could not write the seed journal (.seed-ids.json): ${error.message}. ` + "Seeding continues, but retry protection degrades to live matching.");
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function buildExpressionContext(input, lookup, solution) {
|
|
903
|
+
const entityIds = new Map;
|
|
904
|
+
const entityNames = new Map;
|
|
905
|
+
for (const [name, entity] of lookup.byName) {
|
|
906
|
+
entityIds.set(name, entity.id);
|
|
907
|
+
entityNames.set(name, entity.name);
|
|
908
|
+
}
|
|
909
|
+
const folder = solution.resources?.folder;
|
|
910
|
+
return {
|
|
911
|
+
baseUrl: input.config.baseUrl,
|
|
912
|
+
organizationId: input.config.organizationId,
|
|
913
|
+
organizationName: input.config.organizationName,
|
|
914
|
+
tenantId: input.config.tenantId,
|
|
915
|
+
tenantName: input.config.tenantName,
|
|
916
|
+
folderKey: input.folderKey,
|
|
917
|
+
solutionFolderKey: input.folderKey,
|
|
918
|
+
solutionFolderPath: solution.folderPath,
|
|
919
|
+
solutionFolderId: textOf(folder?.id),
|
|
920
|
+
solutionParentFolderId: textOf(folder?.parentId),
|
|
921
|
+
solutionParentFolderKey: folder?.parentKey,
|
|
922
|
+
solutionResources: solution.resources ? solutionCollections(input.manifest, solution.resources) : undefined,
|
|
923
|
+
entityIds,
|
|
924
|
+
entityNames
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
function textOf(value) {
|
|
928
|
+
return value === undefined ? undefined : String(value);
|
|
929
|
+
}
|
|
930
|
+
async function loadChoiceValues(sdk, entity, folderKey) {
|
|
931
|
+
const byField = new Map;
|
|
932
|
+
for (const field of entity.fields.values()) {
|
|
933
|
+
if (!field.choiceSetId)
|
|
934
|
+
continue;
|
|
935
|
+
const values = await loadChoiceSetValues(sdk, field.choiceSetId, folderKey);
|
|
936
|
+
if (values)
|
|
937
|
+
byField.set(field.name.toLowerCase(), values);
|
|
938
|
+
}
|
|
939
|
+
return byField;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/services/seed/execute-context.ts
|
|
943
|
+
var BATCH_SIZE = 1000;
|
|
944
|
+
function plannedLocalIds(plan) {
|
|
945
|
+
return new Set([...plan.toInsert, ...plan.toUpdate].map((row) => row.localId).filter((localId) => Boolean(localId)));
|
|
946
|
+
}
|
|
947
|
+
function recordJournal(journal, entityName, written) {
|
|
948
|
+
if (written.length === 0)
|
|
949
|
+
return;
|
|
950
|
+
const entity = journalEntity(journal, entityName);
|
|
951
|
+
for (const [row, id] of written) {
|
|
952
|
+
const pendingFiles = Object.keys(row.attachments);
|
|
953
|
+
const entry = pendingFiles.length > 0 ? { recordId: id, pendingFiles } : { recordId: id };
|
|
954
|
+
putOwn(entity, row.key, entry);
|
|
955
|
+
}
|
|
956
|
+
putOwn(journal, entityName, entity);
|
|
957
|
+
}
|
|
958
|
+
async function saveJournal2(options, warnings) {
|
|
959
|
+
const [error] = await catchError(options.journalLockHeld ? writeJournal(options.workspaceDir, options.journalScope, options.journal, true) : writeJournal(options.workspaceDir, options.journalScope, options.journal));
|
|
960
|
+
if (error && !warnings.some((warning) => warning.includes(".seed-ids.json"))) {
|
|
961
|
+
warnings.push(`Could not write the seed journal (.seed-ids.json): ${error.message}. ` + "Seeding continues, but retry protection for this run's rows is lost.");
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function readBatchOutcome(response) {
|
|
965
|
+
const parsed = BatchResponseSchema.safeParse(response);
|
|
966
|
+
if (!parsed.success)
|
|
967
|
+
return { ids: [], failures: [] };
|
|
968
|
+
const container = parsed.data;
|
|
969
|
+
const successes = [
|
|
970
|
+
container.successRecords,
|
|
971
|
+
container.records,
|
|
972
|
+
container.items,
|
|
973
|
+
container.value
|
|
974
|
+
].find((candidate) => candidate !== undefined);
|
|
975
|
+
const ids = (successes ?? []).flatMap((item) => {
|
|
976
|
+
const record = WrittenRecordSchema.safeParse(item);
|
|
977
|
+
if (!record.success)
|
|
978
|
+
return [];
|
|
979
|
+
const id = record.data.Id ?? record.data.id;
|
|
980
|
+
return typeof id === "string" ? [id] : [];
|
|
981
|
+
});
|
|
982
|
+
const failures = (container.failureRecords ?? []).map((failure2) => {
|
|
983
|
+
const record = FailureRecordSchema.safeParse(failure2);
|
|
984
|
+
return record.success && record.data.error ? record.data.error : "the service rejected the record without a reason";
|
|
985
|
+
});
|
|
986
|
+
return { ids, failures };
|
|
987
|
+
}
|
|
988
|
+
function chunk(items, size) {
|
|
989
|
+
const batches = [];
|
|
990
|
+
for (let index = 0;index < items.length; index += size) {
|
|
991
|
+
batches.push(items.slice(index, index + size));
|
|
992
|
+
}
|
|
993
|
+
return batches;
|
|
994
|
+
}
|
|
995
|
+
function rowRange(batch) {
|
|
996
|
+
return `rows ${batch[0].line}-${batch[batch.length - 1].line}`;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// src/services/seed/execute-attachments.ts
|
|
1000
|
+
async function syncAttachments(options, scope, rows, errors) {
|
|
1001
|
+
let uploaded = 0;
|
|
1002
|
+
let cleared = 0;
|
|
1003
|
+
for (const [row, recordId] of rows) {
|
|
1004
|
+
if (options.signal?.aborted) {
|
|
1005
|
+
await saveJournal2(options, []);
|
|
1006
|
+
options.signal.throwIfAborted();
|
|
1007
|
+
}
|
|
1008
|
+
const failed = [];
|
|
1009
|
+
for (const [fieldName, relativePath] of attachmentsToSync(row)) {
|
|
1010
|
+
const ok = await uploadAttachmentField(options, scope, row, recordId, fieldName, relativePath, errors);
|
|
1011
|
+
if (ok) {
|
|
1012
|
+
uploaded += 1;
|
|
1013
|
+
} else {
|
|
1014
|
+
failed.push(fieldName);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
for (const fieldName of row.attachmentClears ?? []) {
|
|
1018
|
+
const [clearError] = await catchError(options.sdk.entities.deleteAttachment({ id: options.plan.entityId }, recordId, fieldName, scope));
|
|
1019
|
+
if (clearError) {
|
|
1020
|
+
errors.push(`${options.plan.entityName} row ${row.line}: removing the file from ` + `${fieldName} failed: ${clearError.message}`);
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
cleared += 1;
|
|
1024
|
+
}
|
|
1025
|
+
setPendingFiles(options.journal, options.plan.entityName, row.key, recordId, failed);
|
|
1026
|
+
}
|
|
1027
|
+
return { uploaded, cleared };
|
|
1028
|
+
}
|
|
1029
|
+
function attachmentsToSync(row) {
|
|
1030
|
+
return row.pendingFiles ? Object.entries(row.attachments).filter(([field]) => row.pendingFiles?.includes(field)) : Object.entries(row.attachments);
|
|
1031
|
+
}
|
|
1032
|
+
async function uploadAttachmentField(options, scope, row, recordId, fieldName, relativePath, errors) {
|
|
1033
|
+
const lexical = resolveWorkspacePath(options.workspaceDir, relativePath);
|
|
1034
|
+
const resolved = lexical !== undefined && await staysInWorkspace(options.workspaceDir, lexical) ? lexical : undefined;
|
|
1035
|
+
if (resolved === undefined) {
|
|
1036
|
+
const nameProblem = attachmentNameProblem(relativePath);
|
|
1037
|
+
const named = withoutControlBytes(relativePath);
|
|
1038
|
+
errors.push(`${options.plan.entityName} row ${row.line}: attachment path ` + (nameProblem === undefined ? `"${named}" points outside the seed workspace.` : `"${named}" is not a usable file name (${nameProblem}).`));
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
const [readError, bytes] = await catchError(getFileSystem().readFile(resolved));
|
|
1042
|
+
if (readError || bytes === null) {
|
|
1043
|
+
errors.push(`${options.plan.entityName} row ${row.line}: attachment ` + `"${relativePath}" could not be read: ${readError?.message ?? "the file is empty"}`);
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
const filename = relativePath.slice(relativePath.lastIndexOf("/") + 1);
|
|
1047
|
+
const [uploadError] = await catchError(options.sdk.entities.uploadAttachment({ id: options.plan.entityId }, recordId, fieldName, new File([new Uint8Array(bytes)], filename, {
|
|
1048
|
+
type: mimeTypeFor(filename)
|
|
1049
|
+
}), scope));
|
|
1050
|
+
if (uploadError) {
|
|
1051
|
+
errors.push(`${options.plan.entityName} row ${row.line}: uploading ` + `"${relativePath}" to ${fieldName} failed: ${uploadError.message}`);
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
var MIME_TYPES = {
|
|
1057
|
+
csv: "text/csv",
|
|
1058
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1059
|
+
gif: "image/gif",
|
|
1060
|
+
htm: "text/html",
|
|
1061
|
+
html: "text/html",
|
|
1062
|
+
jpeg: "image/jpeg",
|
|
1063
|
+
jpg: "image/jpeg",
|
|
1064
|
+
json: "application/json",
|
|
1065
|
+
md: "text/markdown",
|
|
1066
|
+
pdf: "application/pdf",
|
|
1067
|
+
png: "image/png",
|
|
1068
|
+
svg: "image/svg+xml",
|
|
1069
|
+
txt: "text/plain",
|
|
1070
|
+
webp: "image/webp",
|
|
1071
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1072
|
+
xml: "application/xml",
|
|
1073
|
+
yaml: "application/yaml",
|
|
1074
|
+
yml: "application/yaml",
|
|
1075
|
+
zip: "application/zip"
|
|
1076
|
+
};
|
|
1077
|
+
function mimeTypeFor(filename) {
|
|
1078
|
+
const dot = filename.lastIndexOf(".");
|
|
1079
|
+
const extension = dot === -1 ? "" : filename.slice(dot + 1).toLowerCase();
|
|
1080
|
+
return MIME_TYPES[extension] ?? "application/octet-stream";
|
|
1081
|
+
}
|
|
1082
|
+
function setPendingFiles(journal, entityName, rowKey, recordId, failed) {
|
|
1083
|
+
const entity = journalEntity(journal, entityName);
|
|
1084
|
+
if (!Object.hasOwn(entity, rowKey))
|
|
1085
|
+
return;
|
|
1086
|
+
putOwn(entity, rowKey, failed.length > 0 ? { recordId, pendingFiles: failed } : { recordId });
|
|
1087
|
+
putOwn(journal, entityName, entity);
|
|
1088
|
+
}
|
|
1089
|
+
function resolveWorkspacePath(workspaceDir, relativePath) {
|
|
1090
|
+
const segments = relativeSegments(relativePath);
|
|
1091
|
+
if (segments === undefined || segments.length === 0)
|
|
1092
|
+
return;
|
|
1093
|
+
if (segments.some(isUnsafeSegment))
|
|
1094
|
+
return;
|
|
1095
|
+
return getFileSystem().path.join(workspaceDir, ...segments);
|
|
1096
|
+
}
|
|
1097
|
+
function relativeSegments(relativePath) {
|
|
1098
|
+
const normalized = relativePath.replaceAll("\\", "/");
|
|
1099
|
+
if (normalized.startsWith("/") || /^[a-zA-Z]:/.test(normalized)) {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
return normalized.split("/").filter((part) => part !== "");
|
|
1103
|
+
}
|
|
1104
|
+
function attachmentNameProblem(relativePath) {
|
|
1105
|
+
const segments = relativeSegments(relativePath);
|
|
1106
|
+
if (segments === undefined || segments.some(isDotSegment))
|
|
1107
|
+
return;
|
|
1108
|
+
for (const segment of segments) {
|
|
1109
|
+
const problem = segmentNameProblem(segment);
|
|
1110
|
+
if (problem !== undefined)
|
|
1111
|
+
return problem;
|
|
1112
|
+
}
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
async function staysInWorkspace(workspaceDir, path) {
|
|
1116
|
+
const fs = getFileSystem();
|
|
1117
|
+
const [error, canonical] = await catchError(Promise.all([fs.realpath(workspaceDir), fs.realpath(path)]));
|
|
1118
|
+
if (error)
|
|
1119
|
+
return false;
|
|
1120
|
+
const [realRoot, realPath] = canonical;
|
|
1121
|
+
return isUnder(fs.path, realRoot, realPath) || isUnder(fs.path, workspaceDir, realPath);
|
|
1122
|
+
}
|
|
1123
|
+
function climbsOut(relative) {
|
|
1124
|
+
return relative.split(/[\\/]/)[0] === "..";
|
|
1125
|
+
}
|
|
1126
|
+
function isUnder(paths, root, path) {
|
|
1127
|
+
const relative = paths.relative(root, path);
|
|
1128
|
+
return relative === "" || !climbsOut(relative) && !paths.isAbsolute(relative);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/services/seed/execute-insert.ts
|
|
1132
|
+
async function insertRows(rows, options, scope, errors, warnings) {
|
|
1133
|
+
const written = [];
|
|
1134
|
+
const claimed = new Set(options.plan.matched.map((row) => row.recordId.toLowerCase()));
|
|
1135
|
+
const recovery = { blocked: false };
|
|
1136
|
+
for (const batch of chunk(rows, BATCH_SIZE)) {
|
|
1137
|
+
options.signal?.throwIfAborted();
|
|
1138
|
+
const batchWritten = await insertBatch(batch, options, scope, errors, warnings, claimed, recovery);
|
|
1139
|
+
if (batchWritten.length > 0) {
|
|
1140
|
+
recordJournal(options.journal, options.plan.entityName, batchWritten);
|
|
1141
|
+
await saveJournal2(options, warnings);
|
|
1142
|
+
}
|
|
1143
|
+
written.push(...batchWritten);
|
|
1144
|
+
}
|
|
1145
|
+
return written;
|
|
1146
|
+
}
|
|
1147
|
+
async function insertBatch(batch, options, scope, errors, warnings, claimed, recovery) {
|
|
1148
|
+
const { plan, sdk } = options;
|
|
1149
|
+
options.onProgress?.(` ${plan.entityName}: inserting ${batch.length} ${batch.length === 1 ? "record" : "records"}`);
|
|
1150
|
+
const [error, response] = await catchError(sdk.entities.insertRecordsById(plan.entityId, batch.map((row) => row.values), scope));
|
|
1151
|
+
if (error) {
|
|
1152
|
+
return recoverRejectedBatch(batch, options, scope, {
|
|
1153
|
+
errors,
|
|
1154
|
+
warnings,
|
|
1155
|
+
claimed,
|
|
1156
|
+
recovery,
|
|
1157
|
+
cause: error.message
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
const outcome = readBatchOutcome(response);
|
|
1161
|
+
for (const failure2 of outcome.failures) {
|
|
1162
|
+
errors.push(`${plan.entityName}: ${failure2}`);
|
|
1163
|
+
}
|
|
1164
|
+
if (outcome.ids.length === 0 && outcome.failures.length < batch.length) {
|
|
1165
|
+
return recoverWrittenRows(batch, options, scope, errors, warnings, claimed, recovery);
|
|
1166
|
+
}
|
|
1167
|
+
if (outcome.ids.length === batch.length) {
|
|
1168
|
+
const written = [];
|
|
1169
|
+
for (const [index, row] of batch.entries()) {
|
|
1170
|
+
written.push([row, outcome.ids[index]]);
|
|
1171
|
+
claimed.add(outcome.ids[index].toLowerCase());
|
|
1172
|
+
}
|
|
1173
|
+
return written;
|
|
1174
|
+
}
|
|
1175
|
+
if (outcome.ids.length > 0) {
|
|
1176
|
+
for (const id of outcome.ids)
|
|
1177
|
+
claimed.add(id.toLowerCase());
|
|
1178
|
+
errors.push(`${plan.entityName}: the service accepted ${outcome.ids.length} of ${batch.length} rows ` + "but did not say which, so none were journaled. Re-running would insert those rows " + "again — fix the rejected rows and delete the accepted ones first, or seed this " + "entity into a clean folder.");
|
|
1179
|
+
if (outcome.ids.length + outcome.failures.length < batch.length) {
|
|
1180
|
+
recovery.blocked = true;
|
|
1181
|
+
errors.push(`${plan.entityName}: the answer accounted for ${outcome.ids.length + outcome.failures.length} of ${batch.length} rows, so records of this batch may be in the entity ` + "unrecorded. Later batches were not matched against Data Fabric either.");
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return [];
|
|
1185
|
+
}
|
|
1186
|
+
async function recoverWrittenRows(batch, options, scope, errors, warnings, claimed, recovery) {
|
|
1187
|
+
const { plan } = options;
|
|
1188
|
+
const range = rowRange(batch);
|
|
1189
|
+
if (recovery.blocked) {
|
|
1190
|
+
errors.push(unreconciledEarlier(plan.entityName, range));
|
|
1191
|
+
return [];
|
|
1192
|
+
}
|
|
1193
|
+
const [error, live] = await catchError(readBack(options, scope));
|
|
1194
|
+
if (error) {
|
|
1195
|
+
recovery.blocked = true;
|
|
1196
|
+
errors.push(`${plan.entityName}: the service returned no result for ${range}, and reading ` + `the entity back failed: ${error.message}. Check the entity in Data Fabric ` + "before re-running — the rows may have been written.");
|
|
1197
|
+
return [];
|
|
1198
|
+
}
|
|
1199
|
+
const { written, missing } = pairWithLive(batch, live, plan, claimed);
|
|
1200
|
+
if (written.length > 0) {
|
|
1201
|
+
warnings.push(`${plan.entityName}: the service returned no record ids for ${range}; ` + `${written.length} of ${batch.length} rows were found in Data Fabric and recorded.`);
|
|
1202
|
+
}
|
|
1203
|
+
reportMissingRows(plan.entityName, range, batch.length, missing, errors);
|
|
1204
|
+
if (missing.length > 0)
|
|
1205
|
+
recovery.blocked = true;
|
|
1206
|
+
return written;
|
|
1207
|
+
}
|
|
1208
|
+
async function recoverRejectedBatch(batch, options, scope, report) {
|
|
1209
|
+
const { plan } = options;
|
|
1210
|
+
const range = rowRange(batch);
|
|
1211
|
+
const failure2 = `${plan.entityName}: inserting ${range} failed: ${report.cause}`;
|
|
1212
|
+
if (report.recovery.blocked) {
|
|
1213
|
+
report.errors.push(failure2);
|
|
1214
|
+
report.errors.push(unreconciledEarlier(plan.entityName, range));
|
|
1215
|
+
return [];
|
|
1216
|
+
}
|
|
1217
|
+
const [error, live] = await catchError(readBack(options, scope));
|
|
1218
|
+
if (error) {
|
|
1219
|
+
report.recovery.blocked = true;
|
|
1220
|
+
report.errors.push(`${failure2}. Reading the entity back to see what was written failed as well: ` + `${error.message}. Check the entity in Data Fabric before re-running — ` + "some of these rows may already be there.");
|
|
1221
|
+
return [];
|
|
1222
|
+
}
|
|
1223
|
+
const { written, missing } = pairWithLive(batch, live, plan, report.claimed);
|
|
1224
|
+
if (written.length > 0) {
|
|
1225
|
+
report.warnings.push(`${plan.entityName}: inserting ${range} reported an error (${report.cause}), ` + `but ${written.length} of ${batch.length} rows were found in Data Fabric ` + "afterwards and recorded.");
|
|
1226
|
+
}
|
|
1227
|
+
if (missing.length === batch.length) {
|
|
1228
|
+
report.errors.push(failure2);
|
|
1229
|
+
} else if (missing.length > 0) {
|
|
1230
|
+
report.errors.push(`${failure2} (${missing.length} of ${batch.length} rows not found afterwards: ` + `${nameRows(missing)})`);
|
|
1231
|
+
}
|
|
1232
|
+
if (missing.length > 0)
|
|
1233
|
+
report.recovery.blocked = true;
|
|
1234
|
+
return written;
|
|
1235
|
+
}
|
|
1236
|
+
function unreconciledEarlier(entityName, range) {
|
|
1237
|
+
return `${entityName}: not matching ${range} against Data Fabric, because an ` + "earlier batch of this entity could not be reconciled and its rows may " + "be there unrecorded. Check the entity, then re-run to pick up what is " + "missing.";
|
|
1238
|
+
}
|
|
1239
|
+
function readBack(options, scope) {
|
|
1240
|
+
const { plan, sdk } = options;
|
|
1241
|
+
return loadLiveRecords(sdk, plan.entityId, scope?.folderKey, plan.expandRecords);
|
|
1242
|
+
}
|
|
1243
|
+
function pairWithLive(batch, live, plan, claimed) {
|
|
1244
|
+
const unclaimed = live.filter((record) => typeof record.Id !== "string" || !claimed.has(record.Id.toLowerCase()));
|
|
1245
|
+
const ids = plan.locate(batch, unclaimed);
|
|
1246
|
+
const written = [];
|
|
1247
|
+
const missing = [];
|
|
1248
|
+
for (const [index, row] of batch.entries()) {
|
|
1249
|
+
const id = ids[index];
|
|
1250
|
+
if (id === undefined) {
|
|
1251
|
+
missing.push(row);
|
|
1252
|
+
} else {
|
|
1253
|
+
written.push([row, id]);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
for (const record of live) {
|
|
1257
|
+
if (typeof record.Id === "string")
|
|
1258
|
+
claimed.add(record.Id.toLowerCase());
|
|
1259
|
+
}
|
|
1260
|
+
return { written, missing };
|
|
1261
|
+
}
|
|
1262
|
+
function reportMissingRows(entityName, range, batchSize, missing, errors) {
|
|
1263
|
+
const blank = missing.filter((row) => Object.keys(row.values).length === 0);
|
|
1264
|
+
const other = missing.filter((row) => !blank.includes(row));
|
|
1265
|
+
if (other.length > 0) {
|
|
1266
|
+
errors.push(`${entityName}: the service returned no result for ${range} and ` + `${other.length} of ${batchSize} rows could not be found afterwards ` + `(${nameRows(other)}). Check the entity in Data Fabric ` + "before re-running — a row that was written would be inserted again.");
|
|
1267
|
+
}
|
|
1268
|
+
if (blank.length > 0) {
|
|
1269
|
+
errors.push(`${entityName}: the service returned no result for ${range}, and ` + `${blank.length} of ${batchSize} rows (${nameRows(blank)}) hold only files or ` + `links to other ${entityName} rows, which are set after the insert, so there ` + "is nothing to find them by. They were not journaled and their files were not " + "uploaded. Check the entity in Data Fabric before re-running — a row that was " + "written would be inserted again.");
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
function nameRows(rows) {
|
|
1273
|
+
const named = rows.slice(0, 5).map((row) => row.label).join(", ");
|
|
1274
|
+
return `${named}${rows.length > 5 ? ", …" : ""}`;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/services/seed/execute-update.ts
|
|
1278
|
+
async function updateRows(rows, options, scope, errors) {
|
|
1279
|
+
const written = [];
|
|
1280
|
+
for (const batch of chunk(rows, BATCH_SIZE)) {
|
|
1281
|
+
options.signal?.throwIfAborted();
|
|
1282
|
+
written.push(...await updateBatch(batch, options, scope, errors));
|
|
1283
|
+
}
|
|
1284
|
+
return written;
|
|
1285
|
+
}
|
|
1286
|
+
async function updateBatch(batch, options, scope, errors) {
|
|
1287
|
+
const { plan, sdk } = options;
|
|
1288
|
+
const withRecordId = (row) => [
|
|
1289
|
+
row,
|
|
1290
|
+
row.recordId
|
|
1291
|
+
];
|
|
1292
|
+
const withFields = batch.filter((row) => Object.keys(row.values).length > 0);
|
|
1293
|
+
const attachmentOnly = batch.filter((row) => Object.keys(row.values).length === 0).map(withRecordId);
|
|
1294
|
+
if (withFields.length === 0)
|
|
1295
|
+
return attachmentOnly;
|
|
1296
|
+
options.onProgress?.(` ${plan.entityName}: updating ${withFields.length} ${withFields.length === 1 ? "record" : "records"}`);
|
|
1297
|
+
const [error, response] = await catchError(sdk.entities.updateRecordsById(plan.entityId, withFields.map((row) => ({
|
|
1298
|
+
Id: row.recordId,
|
|
1299
|
+
...row.values
|
|
1300
|
+
})), scope));
|
|
1301
|
+
if (error) {
|
|
1302
|
+
errors.push(`${plan.entityName}: updating ${rowRange(batch)} failed: ${error.message}`);
|
|
1303
|
+
return attachmentOnly;
|
|
1304
|
+
}
|
|
1305
|
+
const outcome = readBatchOutcome(response);
|
|
1306
|
+
for (const failure2 of outcome.failures) {
|
|
1307
|
+
errors.push(`${plan.entityName}: ${failure2}`);
|
|
1308
|
+
}
|
|
1309
|
+
if (outcome.failures.length === 0)
|
|
1310
|
+
return batch.map(withRecordId);
|
|
1311
|
+
if (outcome.failures.length < withFields.length) {
|
|
1312
|
+
errors.push(`${plan.entityName}: the service accepted ${withFields.length - outcome.failures.length} of ${withFields.length} updates but did not say which, so none were ` + "counted or followed up. Fix the rejected rows and re-run; rows already " + "updated compare as unchanged.");
|
|
1313
|
+
}
|
|
1314
|
+
return attachmentOnly;
|
|
1315
|
+
}
|
|
1316
|
+
async function resolveSelfReferences(rows, options, scope, ownIds, errors, warnings) {
|
|
1317
|
+
const followUps = [];
|
|
1318
|
+
let rejected = 0;
|
|
1319
|
+
const planned = plannedLocalIds(options.plan);
|
|
1320
|
+
for (const [row, recordId] of rows) {
|
|
1321
|
+
const fields = selfReferenceFields(row, options, ownIds, planned, {
|
|
1322
|
+
errors,
|
|
1323
|
+
warnings
|
|
1324
|
+
});
|
|
1325
|
+
if (Object.keys(fields).length > 0) {
|
|
1326
|
+
followUps.push({ Id: recordId, ...fields });
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
for (const batch of chunk(followUps, BATCH_SIZE)) {
|
|
1330
|
+
options.signal?.throwIfAborted();
|
|
1331
|
+
const [error, response] = await catchError(options.sdk.entities.updateRecordsById(options.plan.entityId, batch, scope));
|
|
1332
|
+
if (error) {
|
|
1333
|
+
errors.push(`${options.plan.entityName}: linking ${batch.length} self-referencing ${batch.length === 1 ? "row" : "rows"} failed: ${error.message}`);
|
|
1334
|
+
rejected += batch.length;
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
const { failures } = readBatchOutcome(response);
|
|
1338
|
+
for (const failure2 of failures) {
|
|
1339
|
+
errors.push(`${options.plan.entityName}: ${failure2}`);
|
|
1340
|
+
}
|
|
1341
|
+
rejected += failures.length;
|
|
1342
|
+
}
|
|
1343
|
+
return rejected;
|
|
1344
|
+
}
|
|
1345
|
+
function selfReferenceFields(row, options, ownIds, planned, report) {
|
|
1346
|
+
const fields = fieldMap();
|
|
1347
|
+
for (const [field, ref] of Object.entries(row.foreignKeys)) {
|
|
1348
|
+
const id = ownIds.get(ref.localId);
|
|
1349
|
+
if (id === undefined && planned.has(ref.localId)) {
|
|
1350
|
+
report.errors.push(`${options.plan.entityName} row ${row.line}: "${ref.localId}" in ${field} ` + `was not linked because that ${options.plan.entityName} row failed — ` + "this row was written without it. Fix the failed row and re-run to link it.");
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
if (id === undefined) {
|
|
1354
|
+
const unresolved = `${options.plan.entityName} row ${row.line}: "${ref.localId}" in ${field} ` + `does not match the Id of any row in ${options.plan.entityName}.csv`;
|
|
1355
|
+
if (options.plan.requiredRelationships.has(field)) {
|
|
1356
|
+
report.errors.push(`${unresolved}, and ${field} is required — the row was ` + "written without it. Add the missing row and re-run to link it.");
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
report.warnings.push(`${unresolved} — the relationship was left empty.`);
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
fields[field] = { Id: id };
|
|
1363
|
+
}
|
|
1364
|
+
return fields;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/services/seed/execute.ts
|
|
1368
|
+
async function executeEntityPlan(options) {
|
|
1369
|
+
const { plan } = options;
|
|
1370
|
+
const errors = [];
|
|
1371
|
+
const warnings = [];
|
|
1372
|
+
const idMaps = options.idMaps ?? new Map;
|
|
1373
|
+
const scope = options.folderKey === undefined ? undefined : { folderKey: options.folderKey };
|
|
1374
|
+
const selfName = plan.entityName.toLowerCase();
|
|
1375
|
+
const ownIds = idMaps.get(selfName) ?? new Map;
|
|
1376
|
+
idMaps.set(selfName, ownIds);
|
|
1377
|
+
for (const [localId, recordId] of plan.idMap)
|
|
1378
|
+
ownIds.set(localId, recordId);
|
|
1379
|
+
const insertable = resolveReferences(plan.toInsert, options, idMaps, {
|
|
1380
|
+
warnings,
|
|
1381
|
+
errors
|
|
1382
|
+
});
|
|
1383
|
+
const updatable = resolveReferences(plan.toUpdate, options, idMaps, {
|
|
1384
|
+
warnings,
|
|
1385
|
+
errors
|
|
1386
|
+
});
|
|
1387
|
+
const inserts = insertable.rows;
|
|
1388
|
+
const insertedIds = await insertRows(inserts, options, scope, errors, warnings);
|
|
1389
|
+
let failed = inserts.length - insertedIds.length + insertable.heldBack + updatable.heldBack;
|
|
1390
|
+
for (const [row, id] of insertedIds) {
|
|
1391
|
+
if (row.localId)
|
|
1392
|
+
ownIds.set(row.localId, id);
|
|
1393
|
+
}
|
|
1394
|
+
const settled = settleSelfReferenceUpdates(updatable.rows.filter(hasUpdateWork), options, ownIds, { errors, warnings });
|
|
1395
|
+
const updates = settled.rows;
|
|
1396
|
+
failed += settled.heldBack;
|
|
1397
|
+
const updatedIds = await updateRows(updates, options, scope, errors);
|
|
1398
|
+
failed += updates.length - updatedIds.length;
|
|
1399
|
+
failed += await resolveSelfReferences([...insertedIds, ...updatedIds], options, scope, ownIds, errors, warnings);
|
|
1400
|
+
const withIds = [
|
|
1401
|
+
...insertedIds,
|
|
1402
|
+
...updatedIds,
|
|
1403
|
+
...plan.toRepairAttachments.map((row) => [
|
|
1404
|
+
row,
|
|
1405
|
+
row.recordId
|
|
1406
|
+
])
|
|
1407
|
+
];
|
|
1408
|
+
const attachments = await syncAttachments(options, scope, withIds, errors);
|
|
1409
|
+
await saveJournal2(options, warnings);
|
|
1410
|
+
return {
|
|
1411
|
+
inserted: insertedIds.length,
|
|
1412
|
+
updated: updatedIds.length,
|
|
1413
|
+
failed,
|
|
1414
|
+
attachmentsUploaded: attachments.uploaded,
|
|
1415
|
+
attachmentsCleared: attachments.cleared,
|
|
1416
|
+
errors,
|
|
1417
|
+
warnings
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
function resolveReferences(rows, options, idMaps, report) {
|
|
1421
|
+
const selfName = options.plan.entityName.toLowerCase();
|
|
1422
|
+
const resolved = [];
|
|
1423
|
+
let heldBack = 0;
|
|
1424
|
+
for (const row of rows) {
|
|
1425
|
+
const next = resolveRowReferences(row, options, idMaps, selfName, report);
|
|
1426
|
+
if (next === undefined) {
|
|
1427
|
+
heldBack += 1;
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
resolved.push(next);
|
|
1431
|
+
}
|
|
1432
|
+
return { rows: resolved, heldBack };
|
|
1433
|
+
}
|
|
1434
|
+
function hasUpdateWork(row) {
|
|
1435
|
+
return hasFieldOrFileWork(row) || Object.keys(row.foreignKeys).length > 0;
|
|
1436
|
+
}
|
|
1437
|
+
function hasFieldOrFileWork(row) {
|
|
1438
|
+
return Object.keys(row.values).length > 0 || Object.keys(row.attachments).length > 0 || (row.attachmentClears?.length ?? 0) > 0;
|
|
1439
|
+
}
|
|
1440
|
+
function settleSelfReferenceUpdates(rows, options, ownIds, report) {
|
|
1441
|
+
const planned = plannedLocalIds(options.plan);
|
|
1442
|
+
const kept = [];
|
|
1443
|
+
let heldBack = 0;
|
|
1444
|
+
for (const row of rows) {
|
|
1445
|
+
const refs = Object.entries(row.foreignKeys);
|
|
1446
|
+
if (hasFieldOrFileWork(row) || refs.some(([, ref]) => ownIds.has(ref.localId))) {
|
|
1447
|
+
kept.push(row);
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
const failedRef = refs.find(([, ref]) => planned.has(ref.localId));
|
|
1451
|
+
if (failedRef) {
|
|
1452
|
+
const [field, ref] = failedRef;
|
|
1453
|
+
report.errors.push(`${options.plan.entityName} row ${row.line}: "${ref.localId}" in ${field} ` + `was not written because that ${options.plan.entityName} row failed — ` + "this row was held back rather than written without it.");
|
|
1454
|
+
heldBack += 1;
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
const requiredRef = refs.find(([field]) => options.plan.requiredRelationships.has(field));
|
|
1458
|
+
if (requiredRef) {
|
|
1459
|
+
const [field, ref] = requiredRef;
|
|
1460
|
+
report.errors.push(`${options.plan.entityName} row ${row.line}: "${ref.localId}" in ${field} ` + `matches no row in ${options.plan.entityName}.csv and no record id, and ` + `${field} is required — this row was held back rather than updated ` + "without it.");
|
|
1461
|
+
heldBack += 1;
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
for (const [field, ref] of refs) {
|
|
1465
|
+
report.warnings.push(`${options.plan.entityName} row ${row.line}: "${ref.localId}" in ${field} ` + `does not match the Id of any row in ${options.plan.entityName}.csv — ` + "the relationship was left empty.");
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return { rows: kept, heldBack };
|
|
1469
|
+
}
|
|
1470
|
+
function resolveRowReferences(row, options, idMaps, selfName, report) {
|
|
1471
|
+
const values = fieldMap(row.values);
|
|
1472
|
+
const pending = fieldMap();
|
|
1473
|
+
if (!resolveUuidReferences(row, values, options, idMaps, report)) {
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
for (const [field, ref] of Object.entries(row.foreignKeys)) {
|
|
1477
|
+
if (ref.entity === selfName) {
|
|
1478
|
+
pending[field] = ref;
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
const id = idMaps.get(ref.entity)?.get(ref.localId);
|
|
1482
|
+
if (id !== undefined) {
|
|
1483
|
+
values[field] = { Id: id };
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
if (options.intendedIds?.get(ref.entity)?.has(ref.localId)) {
|
|
1487
|
+
report.errors.push(heldBackMessage(options, row, field, ref));
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
const where = ref.entity ? `${ref.entity}.csv` : "the referenced entity";
|
|
1491
|
+
if (options.plan.requiredRelationships.has(field)) {
|
|
1492
|
+
report.errors.push(`${options.plan.entityName} row ${row.line}: "${ref.localId}" in ${field} ` + `matches no row in ${where} and no record id, and ${field} is ` + "required — the row was not written.");
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
report.warnings.push(`${options.plan.entityName} row ${row.line}: "${ref.localId}" in ${field} ` + `does not match the Id of any row in ${where}, nor a record id — ` + "the relationship was left empty.");
|
|
1496
|
+
}
|
|
1497
|
+
return { ...row, values, foreignKeys: pending };
|
|
1498
|
+
}
|
|
1499
|
+
function resolveUuidReferences(row, values, options, idMaps, report) {
|
|
1500
|
+
for (const [field, ref] of Object.entries(row.uuidReferences ?? {})) {
|
|
1501
|
+
const id = idMaps.get(ref.entity)?.get(ref.localId);
|
|
1502
|
+
if (id !== undefined) {
|
|
1503
|
+
if (Object.hasOwn(values, field))
|
|
1504
|
+
values[field] = { Id: id };
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
if (options.intendedIds?.get(ref.entity)?.has(ref.localId)) {
|
|
1508
|
+
report.errors.push(heldBackMessage(options, row, field, ref));
|
|
1509
|
+
return false;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
return true;
|
|
1513
|
+
}
|
|
1514
|
+
function heldBackMessage(options, row, field, ref) {
|
|
1515
|
+
return `${options.plan.entityName} row ${row.line}: "${ref.localId}" in ${field} ` + `was not written because that ${ref.entity} row failed — ` + "this row was held back rather than written without it.";
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// src/services/seed-run/write.ts
|
|
1519
|
+
async function confirmOptionalClears(run, plans) {
|
|
1520
|
+
const { input } = run;
|
|
1521
|
+
const optionalDiffs = plans.flatMap((plan) => plan.optionalDiffs);
|
|
1522
|
+
if (optionalDiffs.length === 0 || input.options.yes === true || !canPrompt()) {
|
|
1523
|
+
return { kind: "plans", plans };
|
|
1524
|
+
}
|
|
1525
|
+
if (!await input.deps.confirmClearOptional(optionalDiffs)) {
|
|
1526
|
+
return { kind: "plans", plans };
|
|
1527
|
+
}
|
|
1528
|
+
const cleared = plans.map(withOptionalClears);
|
|
1529
|
+
describePlans(cleared);
|
|
1530
|
+
if (!hasWork(cleared)) {
|
|
1531
|
+
writeProgress(nothingToSeed(cleared));
|
|
1532
|
+
return { kind: "done", result: finishIdle(run, cleared) };
|
|
1533
|
+
}
|
|
1534
|
+
return { kind: "plans", plans: cleared };
|
|
1535
|
+
}
|
|
1536
|
+
function refuseWithoutTerminal(input, plans) {
|
|
1537
|
+
if (input.options.yes === true || canPrompt())
|
|
1538
|
+
return;
|
|
1539
|
+
const pending = plans.filter(hasEntityWork).map((plan) => plan.entityName).join(", ");
|
|
1540
|
+
return failure("missing-option", `Seeding ${pending} needs confirmation, and there is no terminal to ask.`, "Re-run with --yes to seed without confirmation, or run from an interactive terminal " + "to review each entity's changes first.", { result: RESULTS.ValidationError, errorCode: "invalid_argument" });
|
|
1541
|
+
}
|
|
1542
|
+
async function writeApproved(run, plans) {
|
|
1543
|
+
const unavailable = skippedEntities(run.summaries);
|
|
1544
|
+
for (const plan of plans) {
|
|
1545
|
+
run.input.options.signal?.throwIfAborted();
|
|
1546
|
+
await seedOne(run, plan, unavailable);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
async function seedOne(run, plan, unavailable) {
|
|
1550
|
+
if (!hasEntityWork(plan)) {
|
|
1551
|
+
run.summaries.push(nothingDone(plan));
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
const dependency = dependsOn(plan, unavailable);
|
|
1555
|
+
if (dependency !== undefined) {
|
|
1556
|
+
run.errors.push(`${plan.entityName}: rows reference ${dependency} by local Id, and ${dependency} ` + `was not seeded — nothing written for ${plan.entityName}.`);
|
|
1557
|
+
writeWarning(` ${plan.entityName}: skipped — it references ${dependency}, which was not seeded.`);
|
|
1558
|
+
run.summaries.push(emptySummary(plan.entityName, "depends-on-skipped"));
|
|
1559
|
+
unavailable.set(plan.entityName.toLowerCase(), plan.entityName);
|
|
1560
|
+
run.idMaps.delete(plan.entityName.toLowerCase());
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
const approved = run.input.options.yes === true || await run.input.deps.confirmEntity(plan);
|
|
1564
|
+
if (!approved) {
|
|
1565
|
+
writeProgress(` ${plan.entityName}: skipped by user.`);
|
|
1566
|
+
run.summaries.push(emptySummary(plan.entityName, "declined"));
|
|
1567
|
+
unavailable.set(plan.entityName.toLowerCase(), plan.entityName);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
run.input.options.signal?.throwIfAborted();
|
|
1571
|
+
await writeEntity(run, plan, unavailable);
|
|
1572
|
+
}
|
|
1573
|
+
async function writeEntity(run, plan, unavailable) {
|
|
1574
|
+
const { input, idMaps, intendedIds } = run;
|
|
1575
|
+
intendedIds.set(plan.entityName.toLowerCase(), plannedLocalIds(plan));
|
|
1576
|
+
const result = await executeEntityPlan({
|
|
1577
|
+
sdk: run.sdk,
|
|
1578
|
+
plan,
|
|
1579
|
+
folderKey: input.folderKey,
|
|
1580
|
+
workspaceDir: input.workspaceDir,
|
|
1581
|
+
journal: run.journal,
|
|
1582
|
+
signal: input.options.signal,
|
|
1583
|
+
journalScope: run.journalScope,
|
|
1584
|
+
journalLockHeld: true,
|
|
1585
|
+
idMaps,
|
|
1586
|
+
intendedIds,
|
|
1587
|
+
onProgress: writeProgress
|
|
1588
|
+
});
|
|
1589
|
+
for (const warning of result.warnings)
|
|
1590
|
+
writeWarning(warning);
|
|
1591
|
+
run.errors.push(...result.errors);
|
|
1592
|
+
writeProgress(` ${plan.entityName}: ${describeResult(result)}`);
|
|
1593
|
+
if (plan.toInsert.length > 0 && (idMaps.get(plan.entityName.toLowerCase())?.size ?? 0) === 0) {
|
|
1594
|
+
unavailable.set(plan.entityName.toLowerCase(), plan.entityName);
|
|
1595
|
+
}
|
|
1596
|
+
run.summaries.push({
|
|
1597
|
+
entityName: plan.entityName,
|
|
1598
|
+
inserted: result.inserted,
|
|
1599
|
+
updated: result.updated,
|
|
1600
|
+
unchanged: plan.unchanged,
|
|
1601
|
+
ignored: plan.ignored,
|
|
1602
|
+
failed: result.failed,
|
|
1603
|
+
attachmentsUploaded: result.attachmentsUploaded,
|
|
1604
|
+
skipped: false
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// src/services/seed-run/seed-entities.ts
|
|
1609
|
+
async function seedEntities(input) {
|
|
1610
|
+
const retry = seedRetryCommand(input.pickedWorkspace ? { ...input.options, dir: input.workspaceDir } : input.options);
|
|
1611
|
+
const read = await readSeedFiles(input);
|
|
1612
|
+
const unusable = workspaceGate(input, read, retry);
|
|
1613
|
+
if (unusable)
|
|
1614
|
+
return unusable;
|
|
1615
|
+
const connected = await connectSeedSdk(input);
|
|
1616
|
+
if (!connected.ok)
|
|
1617
|
+
return connected;
|
|
1618
|
+
const { sdk } = connected;
|
|
1619
|
+
const solution = await readSolution(input, read);
|
|
1620
|
+
printSeedTarget(input, read, solution.folderPath);
|
|
1621
|
+
const schema = await resolveLiveSchema(input, sdk, read.files);
|
|
1622
|
+
if (!schema.ok)
|
|
1623
|
+
return schema;
|
|
1624
|
+
const { lookup, present, missing } = schema;
|
|
1625
|
+
const run = await startRun(input, sdk, retry, {
|
|
1626
|
+
noSeedFile: read.absent,
|
|
1627
|
+
notInScope: missing
|
|
1628
|
+
});
|
|
1629
|
+
const [planError, plans] = await catchError(() => planAll(run, lookup, present, buildExpressionContext(input, lookup, solution)));
|
|
1630
|
+
if (planError) {
|
|
1631
|
+
if (planError instanceof SeedCycleError) {
|
|
1632
|
+
return invalidOption(planError.message, planError.instructions);
|
|
1633
|
+
}
|
|
1634
|
+
throw planError;
|
|
1635
|
+
}
|
|
1636
|
+
input.options.signal?.throwIfAborted();
|
|
1637
|
+
reportPlanning(run, plans, missing);
|
|
1638
|
+
const cleared = await confirmOptionalClears(run, plans);
|
|
1639
|
+
if (cleared.kind === "done")
|
|
1640
|
+
return cleared.result;
|
|
1641
|
+
const finalPlans = cleared.plans;
|
|
1642
|
+
if (!hasWork(finalPlans)) {
|
|
1643
|
+
if (run.errors.length === 0) {
|
|
1644
|
+
writeProgress(nothingToSeed(finalPlans));
|
|
1645
|
+
}
|
|
1646
|
+
return finishIdle(run, finalPlans);
|
|
1647
|
+
}
|
|
1648
|
+
const refused = refuseWithoutTerminal(input, finalPlans);
|
|
1649
|
+
if (refused)
|
|
1650
|
+
return refused;
|
|
1651
|
+
await writeApproved(run, finalPlans);
|
|
1652
|
+
return finish(input.workspaceDir, run.summaries, run.errors, retry);
|
|
1653
|
+
}
|
|
1654
|
+
async function startRun(input, sdk, retry, notSeeded) {
|
|
1655
|
+
const journalScope = {
|
|
1656
|
+
organizationId: input.config.organizationId,
|
|
1657
|
+
tenantId: input.config.tenantId,
|
|
1658
|
+
organizationName: input.config.organizationName,
|
|
1659
|
+
tenantName: input.config.tenantName,
|
|
1660
|
+
folderKey: input.folderKey
|
|
1661
|
+
};
|
|
1662
|
+
const { journal, warning } = await readJournal(input.workspaceDir, journalScope);
|
|
1663
|
+
if (warning)
|
|
1664
|
+
writeWarning(warning);
|
|
1665
|
+
return {
|
|
1666
|
+
input,
|
|
1667
|
+
sdk,
|
|
1668
|
+
retry,
|
|
1669
|
+
journal,
|
|
1670
|
+
journalScope,
|
|
1671
|
+
idMaps: new Map,
|
|
1672
|
+
intendedIds: new Map,
|
|
1673
|
+
summaries: [
|
|
1674
|
+
...notSeeded.noSeedFile.map((name) => emptySummary(name, "no-seed-file")),
|
|
1675
|
+
...notSeeded.notInScope.map((name) => emptySummary(name, "not-in-scope"))
|
|
1676
|
+
],
|
|
1677
|
+
errors: []
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
async function readSeedFiles(input) {
|
|
1681
|
+
const fs = getFileSystem();
|
|
1682
|
+
const read = {
|
|
1683
|
+
files: [],
|
|
1684
|
+
empty: [],
|
|
1685
|
+
invalid: [],
|
|
1686
|
+
absent: []
|
|
1687
|
+
};
|
|
1688
|
+
for (const entityName of input.entityNames) {
|
|
1689
|
+
const file = await readSeedFile(fs, input.workspaceDir, entityName);
|
|
1690
|
+
if (file.kind === "absent") {
|
|
1691
|
+
writeWarning(`Skipping "${entityName}": no seed file at ${file.path}.`);
|
|
1692
|
+
read.absent.push(entityName);
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
const { strategy, warning } = readStrategy(input.manifest.dataFabric?.seeding?.[entityName]?.strategy);
|
|
1696
|
+
if (warning)
|
|
1697
|
+
writeWarning(warning);
|
|
1698
|
+
if (file.kind === "invalid") {
|
|
1699
|
+
read.invalid.push(`${entityName}.csv could not be read — ${file.why}`);
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
if (file.table.rows.length === 0) {
|
|
1703
|
+
read.empty.push(entityName);
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
read.files.push({
|
|
1707
|
+
entityName,
|
|
1708
|
+
csv: file.csv,
|
|
1709
|
+
rows: file.table.rows.length,
|
|
1710
|
+
strategy
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
return read;
|
|
1714
|
+
}
|
|
1715
|
+
function workspaceGate(input, read, retry) {
|
|
1716
|
+
const { files, empty, invalid, absent } = read;
|
|
1717
|
+
if (invalid.length > 0) {
|
|
1718
|
+
return failure("invalid-option", `Seed files could not be read: ${invalid.join("; ")}.`, `Fix the CSV (every opening quote needs a closing one), then retry with: ${retry}`, { result: RESULTS.ValidationError, errorCode: "invalid_argument" });
|
|
1719
|
+
}
|
|
1720
|
+
if (files.length === 0 && empty.length === 0 && absent.length > 0) {
|
|
1721
|
+
return failure("invalid-option", `No seed files found in ${input.workspaceDir} for: ${absent.join(", ")}.`, `Write the templates with: ${retry} --refresh — or pass --dir <path> if the files live elsewhere.`, { result: RESULTS.ValidationError, errorCode: "invalid_argument" });
|
|
1722
|
+
}
|
|
1723
|
+
if (files.length > 0)
|
|
1724
|
+
return;
|
|
1725
|
+
for (const entityName of empty) {
|
|
1726
|
+
writeProgress(` ${entityName}: no rows in ${entityName}.csv — fill it in to seed this entity`);
|
|
1727
|
+
}
|
|
1728
|
+
writeProgress("No seeding CSV files with data found.");
|
|
1729
|
+
return {
|
|
1730
|
+
ok: true,
|
|
1731
|
+
workspaceDir: input.workspaceDir,
|
|
1732
|
+
summaries: input.entityNames.map((name) => emptySummary(name, absent.includes(name) ? "no-seed-file" : undefined))
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
async function connectSeedSdk(input) {
|
|
1736
|
+
const [sdkError, sdk] = await catchError(input.deps.createSdk({
|
|
1737
|
+
tenant: input.options.tenant,
|
|
1738
|
+
loginValidity: input.options.loginValidity
|
|
1739
|
+
}));
|
|
1740
|
+
if (sdkError || !sdk) {
|
|
1741
|
+
return failure("service-error", `Could not connect to Data Fabric: ${sdkError?.message ?? "unknown error"}`, "Run 'uip login' and check that the tenant has Data Fabric enabled.");
|
|
1742
|
+
}
|
|
1743
|
+
return { ok: true, sdk };
|
|
1744
|
+
}
|
|
1745
|
+
var SOLUTION_RESOURCE = /%\{\s*(?:Solution\.(?!Folder(?:Path|Key)\s*\}%)|UIPATH_FOLDER_ID\s*\}%)/;
|
|
1746
|
+
var SOLUTION_COLLECTION = /%\{\s*Solution\.(Processes|Assets|StorageBuckets|Connections)\[/g;
|
|
1747
|
+
function namedCollections(files) {
|
|
1748
|
+
const wanted = new Set;
|
|
1749
|
+
for (const file of files) {
|
|
1750
|
+
for (const [, name] of file.csv.matchAll(SOLUTION_COLLECTION)) {
|
|
1751
|
+
const collection = SOLUTION_COLLECTIONS.find((known) => known === name);
|
|
1752
|
+
if (collection)
|
|
1753
|
+
wanted.add(collection);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return wanted;
|
|
1757
|
+
}
|
|
1758
|
+
async function readSolution(input, read) {
|
|
1759
|
+
if (input.folderKey === undefined)
|
|
1760
|
+
return {};
|
|
1761
|
+
if (!read.files.some((file) => SOLUTION_RESOURCE.test(file.csv))) {
|
|
1762
|
+
return { folderPath: await findSolutionFolderPath(input) };
|
|
1763
|
+
}
|
|
1764
|
+
const resources = await readSolutionResources(input.config, input.folderKey, namedCollections(read.files), input.options.signal);
|
|
1765
|
+
for (const warning of resources.warnings)
|
|
1766
|
+
writeWarning(warning);
|
|
1767
|
+
return {
|
|
1768
|
+
folderPath: resources.folder?.path ?? await findSolutionFolderPath(input),
|
|
1769
|
+
resources
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
async function findSolutionFolderPath(input) {
|
|
1773
|
+
if (input.folderKey === undefined)
|
|
1774
|
+
return;
|
|
1775
|
+
const [, path] = await catchError(findFolderPath(input.config, input.folderKey, input.options.signal));
|
|
1776
|
+
return path;
|
|
1777
|
+
}
|
|
1778
|
+
function printSeedTarget(input, read, solutionFolderPath) {
|
|
1779
|
+
const fs = getFileSystem();
|
|
1780
|
+
const scope = input.folderKey ? `solution folder ${solutionFolderPath ?? input.folderKey}` : "tenant-level entities";
|
|
1781
|
+
writeProgress(`
|
|
1782
|
+
Seeding workspace: ${fs.path.resolve(input.workspaceDir)}`);
|
|
1783
|
+
writeProgress(" (edit the CSVs there to change what gets seeded)");
|
|
1784
|
+
writeProgress(`Target: ${input.config.organizationName ?? "?"} / ${input.config.tenantName ?? "?"} — ${scope}`);
|
|
1785
|
+
for (const file of read.files) {
|
|
1786
|
+
writeProgress(` ${file.entityName}: ${file.rows} row(s) — strategy: ${file.strategy}`);
|
|
1787
|
+
}
|
|
1788
|
+
for (const entityName of read.empty) {
|
|
1789
|
+
writeProgress(` ${entityName}: no rows in ${entityName}.csv — fill it in to seed this entity`);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
async function resolveLiveSchema(input, sdk, files) {
|
|
1793
|
+
const where = input.folderKey ? `folder scope ${input.folderKey}` : "the tenant-level schema";
|
|
1794
|
+
const [lookupError, lookup] = await catchError(loadLiveEntities(sdk, input.folderKey));
|
|
1795
|
+
if (lookupError || !lookup) {
|
|
1796
|
+
return failure("service-error", `Could not read the Data Fabric schema: ${lookupError?.message ?? "unknown error"}`, input.folderKey === undefined ? "If the package provisions its entities inside the solution folder, pass --folder-key." : "Check that --folder-key names the folder the solution was installed into.");
|
|
1797
|
+
}
|
|
1798
|
+
const entityFor = (file) => lookup.byName.get(file.entityName.toLowerCase());
|
|
1799
|
+
const federated = files.filter((file) => entityFor(file)?.isFederated === true).map((file) => file.entityName);
|
|
1800
|
+
if (federated.length > 0) {
|
|
1801
|
+
return failure("invalid-option", `These entities read from an external source and cannot be seeded: ${federated.join(", ")}.`, "A federated entity mirrors data held elsewhere, so records are written at the " + "source, not here. Remove those CSVs from the seed workspace, or seed the " + "native entity the package means instead.", { result: RESULTS.ValidationError });
|
|
1802
|
+
}
|
|
1803
|
+
const present = files.flatMap((file) => {
|
|
1804
|
+
const entity = entityFor(file);
|
|
1805
|
+
return entity ? [{ ...file, entity }] : [];
|
|
1806
|
+
});
|
|
1807
|
+
const missing = files.filter((file) => entityFor(file) === undefined).map((file) => file.entityName);
|
|
1808
|
+
if (present.length === 0) {
|
|
1809
|
+
return failure("service-error", `None of the seeding entities exist in ${where}: ${missing.join(", ")}.`, input.folderKey === undefined ? "New-structure packages provision entities inside the installed solution folder. " + "Re-deploy the package (the deploy records the folder in .seed-context.json) " + "or pass --folder-key <guid>." : `Folder ${input.folderKey} shows none of these entities to this login. Either the ` + "solution installed elsewhere — it installs into its own child folder under the " + "target; check 'uip solution deploy list' for the installed folder — or this login " + "has no role in that folder: an external application needs a folder role with " + "Data Fabric permissions there (the built-in Automation User role is enough).");
|
|
1810
|
+
}
|
|
1811
|
+
if (missing.length > 0) {
|
|
1812
|
+
writeWarning(`Skipping entities missing from ${where}: ${missing.join(", ")}`);
|
|
1813
|
+
}
|
|
1814
|
+
return { ok: true, lookup, present, missing };
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// src/services/seed-run/service-url.ts
|
|
1818
|
+
function readSeedServiceUrl(raw, context, tenant) {
|
|
1819
|
+
if (raw !== undefined)
|
|
1820
|
+
return readServiceUrl(raw, tenant);
|
|
1821
|
+
const recorded = context?.serviceUrl;
|
|
1822
|
+
if (recorded === undefined || recorded === "") {
|
|
1823
|
+
return { ok: true, value: undefined };
|
|
1824
|
+
}
|
|
1825
|
+
if (tenant !== undefined) {
|
|
1826
|
+
writeWarning(`Ignoring the service URL recorded in .seed-context.json (${hostOf(recorded)}): ` + "--tenant was given, and a service URL carries the login's own tenant.");
|
|
1827
|
+
return { ok: true, value: undefined };
|
|
1828
|
+
}
|
|
1829
|
+
const resolved = readServiceUrl(recorded);
|
|
1830
|
+
if (resolved.ok && resolved.value !== undefined && isLoopbackUrl(resolved.value)) {
|
|
1831
|
+
return { ok: true, value: resolved.value };
|
|
1832
|
+
}
|
|
1833
|
+
writeWarning(`Ignoring the service URL recorded in .seed-context.json (${hostOf(recorded)}): ` + "a service other than the platform's is only used with an explicit --service-url.");
|
|
1834
|
+
return { ok: true, value: undefined };
|
|
1835
|
+
}
|
|
1836
|
+
function isLoopbackUrl(value) {
|
|
1837
|
+
const [, url] = catchError(() => new URL(value));
|
|
1838
|
+
return url !== undefined && url !== null && isLoopbackHost(url);
|
|
1839
|
+
}
|
|
1840
|
+
function hostOf(value) {
|
|
1841
|
+
const [, url] = catchError(() => new URL(value));
|
|
1842
|
+
return url?.hostname ?? "not a URL";
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// src/services/seed-service.ts
|
|
1846
|
+
async function runSeed(options, deps) {
|
|
1847
|
+
const blank = rejectBlankOptions(options);
|
|
1848
|
+
if (blank)
|
|
1849
|
+
return blank;
|
|
1850
|
+
const [locateError, located] = await catchError(() => locateWorkspace(options, deps));
|
|
1851
|
+
if (locateError || !located) {
|
|
1852
|
+
return failure("service-error", `The seed workspace could not be read: ${locateError?.message ?? "unknown error"}.`, "Check that ./seeding and the package folder under it are readable and are real " + "folders, then run the same command again. Pass --dir <path> to seed a folder elsewhere.", await workspaceFault(locateError));
|
|
1853
|
+
}
|
|
1854
|
+
if (!located.ok)
|
|
1855
|
+
return located;
|
|
1856
|
+
if (located.creating) {
|
|
1857
|
+
const created = await createWorkspace(located.workspaceDir);
|
|
1858
|
+
if (created)
|
|
1859
|
+
return created;
|
|
1860
|
+
}
|
|
1861
|
+
const [lockError, release] = await catchError(acquireSeedWorkspaceLock(located.workspaceDir));
|
|
1862
|
+
if (lockError || !release) {
|
|
1863
|
+
return failure("service-error", `Could not lock the seed workspace ${located.workspaceDir}: ${lockError?.message ?? "no lock"}.`, "Wait for another deploy or seed to finish, and check that the workspace folder is " + "writable, then run the same command again.", await workspaceFault(lockError));
|
|
1864
|
+
}
|
|
1865
|
+
try {
|
|
1866
|
+
const creating = located.creating && await readSeedContext(located.workspaceDir) === undefined;
|
|
1867
|
+
return await runSeedLocked(options, deps, { ...located, creating });
|
|
1868
|
+
} finally {
|
|
1869
|
+
await catchError(release());
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
async function workspaceFault(error) {
|
|
1873
|
+
const details = error ? await extractErrorDetails(error) : undefined;
|
|
1874
|
+
return details?.errorCode === "local_permission_denied" ? { errorCode: "local_permission_denied", retry: "RetryWillNotFix" } : { errorCode: "network_error", retry: "RetryLater" };
|
|
1875
|
+
}
|
|
1876
|
+
async function createWorkspace(workspaceDir) {
|
|
1877
|
+
const fs = getFileSystem();
|
|
1878
|
+
const [mkdirError] = typeof fs.mkdir === "function" ? await catchError(fs.mkdir(workspaceDir)) : [undefined];
|
|
1879
|
+
if (mkdirError) {
|
|
1880
|
+
return failure("service-error", `Could not create the seed workspace ${workspaceDir}: ${mkdirError.message}.`, "Check that the parent folder is writable, then run the same command again.", await workspaceFault(mkdirError));
|
|
1881
|
+
}
|
|
1882
|
+
const [checkError, checked] = await catchError(async () => await namesSeedRoot(workspaceDir) ? insideSeedRoot(workspaceDir) : { ok: true, dir: workspaceDir });
|
|
1883
|
+
if (checkError || !checked) {
|
|
1884
|
+
return failure("service-error", `The seed workspace ${workspaceDir} could not be checked: ` + `${checkError?.message ?? "unknown error"}.`, "Check that ./seeding and the package folder under it are readable and are " + "real folders, then run the same command again.", await workspaceFault(checkError));
|
|
1885
|
+
}
|
|
1886
|
+
if (!checked.ok) {
|
|
1887
|
+
return failure("invalid-option", checked.message, checked.instructions, {
|
|
1888
|
+
result: RESULTS.ValidationError
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
async function runSeedLocked(options, deps, located) {
|
|
1894
|
+
const { workspaceDir, creating } = located;
|
|
1895
|
+
const recorded = await readSeedContext(workspaceDir);
|
|
1896
|
+
const packageName = options.packageName ?? recorded?.packageName;
|
|
1897
|
+
const context = packageName ? contextForPackage(recorded, packageName) : recorded;
|
|
1898
|
+
const packageVersion = options.packageVersion ?? context?.packageVersion;
|
|
1899
|
+
if (!packageName || !packageVersion) {
|
|
1900
|
+
return failure("missing-option", "The package coordinates could not be determined for this seed workspace.", "Pass --package-name <name> and --package-version <version>. A successful deploy " + "normally records them in .seed-context.json.", { result: RESULTS.ValidationError });
|
|
1901
|
+
}
|
|
1902
|
+
const serviceUrl = readSeedServiceUrl(options.serviceUrl, context, options.tenant);
|
|
1903
|
+
if (!serviceUrl.ok)
|
|
1904
|
+
return serviceUrl;
|
|
1905
|
+
const [authError, config] = await catchError(createVssConfig({
|
|
1906
|
+
tenant: options.tenant,
|
|
1907
|
+
loginValidity: options.loginValidity,
|
|
1908
|
+
serviceUrl: serviceUrl.value
|
|
1909
|
+
}));
|
|
1910
|
+
if (authError || !config) {
|
|
1911
|
+
return failure("service-error", authError?.message ?? "Could not resolve UiPath credentials.", "Run 'uip login' first. In CI, set UIPATH_CLI_ENABLE_ENV_AUTH=true with UIPATH_CLI_AUTH_TOKEN.", { result: RESULTS.AuthenticationError });
|
|
1912
|
+
}
|
|
1913
|
+
const sameTenant = checkContextTenant(recorded, config, options);
|
|
1914
|
+
if (sameTenant !== undefined)
|
|
1915
|
+
return sameTenant;
|
|
1916
|
+
const [manifestError, manifest] = await catchError(getManifest(config, packageName, packageVersion, options.signal));
|
|
1917
|
+
if (manifestError || !manifest) {
|
|
1918
|
+
return toDeployFailure(manifestError, `Could not read the manifest of ${packageName} ${packageVersion}`);
|
|
1919
|
+
}
|
|
1920
|
+
const entityNames = Object.keys(manifest.dataFabric?.seeding ?? {});
|
|
1921
|
+
if (entityNames.length === 0) {
|
|
1922
|
+
return { ok: true, workspaceDir, summaries: [] };
|
|
1923
|
+
}
|
|
1924
|
+
const unusable = unusableEntityNames(entityNames);
|
|
1925
|
+
if (unusable)
|
|
1926
|
+
return unusable;
|
|
1927
|
+
const colliding = caseCollisions(entityNames);
|
|
1928
|
+
if (colliding)
|
|
1929
|
+
return colliding;
|
|
1930
|
+
const scope = await refreshAndScope({
|
|
1931
|
+
config,
|
|
1932
|
+
manifest,
|
|
1933
|
+
context,
|
|
1934
|
+
workspaceDir,
|
|
1935
|
+
creating,
|
|
1936
|
+
packageName,
|
|
1937
|
+
packageVersion,
|
|
1938
|
+
serviceUrl: serviceUrl.value,
|
|
1939
|
+
options,
|
|
1940
|
+
lockHeld: true
|
|
1941
|
+
});
|
|
1942
|
+
if (!scope.ok)
|
|
1943
|
+
return scope;
|
|
1944
|
+
return seedEntities({
|
|
1945
|
+
config,
|
|
1946
|
+
manifest,
|
|
1947
|
+
entityNames,
|
|
1948
|
+
workspaceDir,
|
|
1949
|
+
folderKey: scope.folderKey,
|
|
1950
|
+
options,
|
|
1951
|
+
deps,
|
|
1952
|
+
pickedWorkspace: located.picked === true
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
function unusableEntityNames(entityNames) {
|
|
1956
|
+
const unusable = entityNames.filter((name) => entityNameProblem(name));
|
|
1957
|
+
if (unusable.length === 0)
|
|
1958
|
+
return;
|
|
1959
|
+
return failure("invalid-option", `The package manifest names seeding entities that cannot be seed file names: ${unusable.map((name) => JSON.stringify(name)).join(", ")}.`, "Fix the entity names under dataFabric.seeding in the package manifest and republish the package.", { result: RESULTS.ValidationError, errorCode: "invalid_argument" });
|
|
1960
|
+
}
|
|
1961
|
+
function caseCollisions(entityNames) {
|
|
1962
|
+
const byFolded = new Map;
|
|
1963
|
+
for (const name of entityNames) {
|
|
1964
|
+
const folded = name.toLowerCase();
|
|
1965
|
+
byFolded.set(folded, [...byFolded.get(folded) ?? [], name]);
|
|
1966
|
+
}
|
|
1967
|
+
const clashes = [...byFolded.values()].filter((names) => names.length > 1);
|
|
1968
|
+
if (clashes.length === 0)
|
|
1969
|
+
return;
|
|
1970
|
+
return failure("invalid-option", `The package manifest names the same seeding entity more than once, in different case: ${clashes.map((names) => names.map((name) => JSON.stringify(name)).join(" and ")).join("; ")}.`, "Keep one spelling of each entity under dataFabric.seeding in the package manifest and republish the package.", { result: RESULTS.ValidationError, errorCode: "invalid_argument" });
|
|
1971
|
+
}
|
|
1972
|
+
async function isEmptyDir(dir) {
|
|
1973
|
+
const fs = getFileSystem();
|
|
1974
|
+
if (typeof fs.readdir !== "function")
|
|
1975
|
+
return false;
|
|
1976
|
+
const [error, entries] = await catchError(fs.readdir(dir));
|
|
1977
|
+
return error === undefined && entries !== undefined && entries.length === 0;
|
|
1978
|
+
}
|
|
1979
|
+
async function locateWorkspace(options, deps) {
|
|
1980
|
+
const located = await resolveSeedDir({
|
|
1981
|
+
dir: options.dir,
|
|
1982
|
+
packageName: options.packageName,
|
|
1983
|
+
pickOne: deps.pickOne
|
|
1984
|
+
});
|
|
1985
|
+
if (located.ok) {
|
|
1986
|
+
const creating = options.packageName !== undefined && options.packageVersion !== undefined && await isEmptyDir(located.dir);
|
|
1987
|
+
return {
|
|
1988
|
+
ok: true,
|
|
1989
|
+
workspaceDir: located.dir,
|
|
1990
|
+
creating,
|
|
1991
|
+
...located.picked ? { picked: true } : {}
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
if (located.missing && options.packageName !== undefined && options.packageVersion !== undefined) {
|
|
1995
|
+
writeProgress(`No seed workspace for ${options.packageName} yet; creating it from the ` + `${options.packageVersion} seed templates.`);
|
|
1996
|
+
return {
|
|
1997
|
+
ok: true,
|
|
1998
|
+
workspaceDir: packageDir(options.packageName),
|
|
1999
|
+
creating: true
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
return failure("invalid-option", located.message, located.instructions, {
|
|
2003
|
+
result: RESULTS.ValidationError
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
async function recordSeedContext(context) {
|
|
2007
|
+
return writeSeedContext(context);
|
|
2008
|
+
}
|
|
2009
|
+
export {
|
|
2010
|
+
recordSeedContext,
|
|
2011
|
+
runSeed
|
|
2012
|
+
};
|
|
2013
|
+
|
|
2014
|
+
//# debugId=02A50C129DE0952164756E2164756E21
|