@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,2281 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SEED_CONTEXT_FILE,
|
|
3
|
+
entityNameProblem,
|
|
4
|
+
insideSeedRoot,
|
|
5
|
+
isDanglingLink,
|
|
6
|
+
isInsideWorkspace,
|
|
7
|
+
metadataIsItsOwnFile,
|
|
8
|
+
namesSeedRoot,
|
|
9
|
+
packageDir,
|
|
10
|
+
packageNameProblem,
|
|
11
|
+
pathHasExpectedTarget,
|
|
12
|
+
workspaceRoot,
|
|
13
|
+
writeFileByRename
|
|
14
|
+
} from "./tool-s9qbxa2g.js";
|
|
15
|
+
import {
|
|
16
|
+
writeWarning
|
|
17
|
+
} from "./tool-v15vxc4x.js";
|
|
18
|
+
import {
|
|
19
|
+
validateServiceUrl
|
|
20
|
+
} from "./tool-xd2ejt3h.js";
|
|
21
|
+
import {
|
|
22
|
+
encodeSegment,
|
|
23
|
+
scopeQuery,
|
|
24
|
+
vssRequestParsed
|
|
25
|
+
} from "./tool-4z1dy1rn.js";
|
|
26
|
+
import {
|
|
27
|
+
anyNumber,
|
|
28
|
+
jsonObject,
|
|
29
|
+
optionalText,
|
|
30
|
+
plainRecord
|
|
31
|
+
} from "./tool-87e2ps7f.js";
|
|
32
|
+
import {
|
|
33
|
+
exports_external
|
|
34
|
+
} from "./tool-vf2mgth0.js";
|
|
35
|
+
import {
|
|
36
|
+
catchError,
|
|
37
|
+
extractErrorMessage
|
|
38
|
+
} from "./tool-pz9qm3dv.js";
|
|
39
|
+
import {
|
|
40
|
+
getFileSystem
|
|
41
|
+
} from "./tool-hdxtgckw.js";
|
|
42
|
+
import {
|
|
43
|
+
__require
|
|
44
|
+
} from "./tool-1yh8g550.js";
|
|
45
|
+
|
|
46
|
+
// src/api/config-keys.ts
|
|
47
|
+
var RESERVED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
48
|
+
function configKeyProblem(key) {
|
|
49
|
+
if (key.trim() === "") {
|
|
50
|
+
return "a configuration key cannot be empty.";
|
|
51
|
+
}
|
|
52
|
+
if (RESERVED_KEYS.has(key)) {
|
|
53
|
+
return `"${key}" is not a valid configuration key.`;
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/api/manifest-sections.ts
|
|
59
|
+
var ConfigurationDeclarationSchema = exports_external.looseObject({
|
|
60
|
+
isRequired: exports_external.unknown().optional().transform((value) => value !== false),
|
|
61
|
+
isSecret: exports_external.unknown().optional().transform((value) => value !== undefined && value !== false),
|
|
62
|
+
description: optionalText,
|
|
63
|
+
value: optionalText
|
|
64
|
+
});
|
|
65
|
+
var ManifestConfigurationsSchema = jsonObject.transform((declared) => {
|
|
66
|
+
const declarations = new Map;
|
|
67
|
+
for (const [key, meta] of Object.entries(declared)) {
|
|
68
|
+
const parsed = ConfigurationDeclarationSchema.safeParse(meta);
|
|
69
|
+
if (parsed.success)
|
|
70
|
+
declarations.set(key, parsed.data);
|
|
71
|
+
}
|
|
72
|
+
return declarations;
|
|
73
|
+
}).catch(() => new Map);
|
|
74
|
+
var PropertyMetaSchema = exports_external.looseObject({
|
|
75
|
+
description: optionalText,
|
|
76
|
+
default: exports_external.unknown().optional(),
|
|
77
|
+
required: exports_external.unknown().optional(),
|
|
78
|
+
value: exports_external.unknown().optional()
|
|
79
|
+
});
|
|
80
|
+
var RequiredSchema = exports_external.union([
|
|
81
|
+
exports_external.boolean().transform((value) => ({ value, fromText: false })),
|
|
82
|
+
exports_external.string().trim().toLowerCase().pipe(exports_external.enum(["true", "false"])).transform((text) => ({ value: text === "true", fromText: true }))
|
|
83
|
+
]).optional();
|
|
84
|
+
var DefaultSchema = exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()]).transform(String).nullish();
|
|
85
|
+
var DeclaredConfigurationSchema = exports_external.unknown().transform(parseDeclared).optional();
|
|
86
|
+
function parseDeclared(declaration) {
|
|
87
|
+
const properties = [];
|
|
88
|
+
const warnings = [];
|
|
89
|
+
const resources = jsonObject.safeParse(declaration);
|
|
90
|
+
if (!resources.success) {
|
|
91
|
+
warnings.push("Ignoring solution.configuration in the manifest: it is not an object.");
|
|
92
|
+
return { properties, warnings };
|
|
93
|
+
}
|
|
94
|
+
for (const [resourceName, resource] of Object.entries(resources.data)) {
|
|
95
|
+
const declared = jsonObject.safeParse(resource);
|
|
96
|
+
if (!declared.success) {
|
|
97
|
+
warnings.push(`Ignoring solution.configuration.${resourceName}: it is not an object.`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const [propertyName, meta] of Object.entries(declared.data)) {
|
|
101
|
+
const parsed = toDeclaredProperty(resourceName, propertyName, meta, warnings);
|
|
102
|
+
if (parsed)
|
|
103
|
+
properties.push(parsed);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { properties, warnings };
|
|
107
|
+
}
|
|
108
|
+
function toDeclaredProperty(resourceName, propertyName, meta, warnings) {
|
|
109
|
+
const problem = configKeyProblem(propertyName);
|
|
110
|
+
if (problem) {
|
|
111
|
+
warnings.push(`Ignoring ${resourceName}.${propertyName}: ${problem}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const parsed = PropertyMetaSchema.safeParse(meta);
|
|
115
|
+
if (!parsed.success) {
|
|
116
|
+
return {
|
|
117
|
+
resourceName,
|
|
118
|
+
propertyName,
|
|
119
|
+
required: false,
|
|
120
|
+
hasManifestValue: false
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const label = `${resourceName}.${propertyName}`;
|
|
124
|
+
return {
|
|
125
|
+
resourceName,
|
|
126
|
+
propertyName,
|
|
127
|
+
description: parsed.data.description,
|
|
128
|
+
defaultValue: readDefault(parsed.data.default, label, warnings),
|
|
129
|
+
required: readRequired(parsed.data.required, label, warnings),
|
|
130
|
+
hasManifestValue: hasValue(parsed.data.value)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function hasValue(value) {
|
|
134
|
+
if (typeof value === "string")
|
|
135
|
+
return value.trim() !== "";
|
|
136
|
+
return typeof value === "number" || typeof value === "boolean";
|
|
137
|
+
}
|
|
138
|
+
function readRequired(value, label, warnings) {
|
|
139
|
+
const parsed = RequiredSchema.safeParse(value);
|
|
140
|
+
if (!parsed.success) {
|
|
141
|
+
warnings.push(`Configuration ${label}: ignoring 'required' — expected true or false, got ${describeType(value)}.`);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (parsed.data?.fromText) {
|
|
145
|
+
warnings.push(`Configuration ${label}: 'required' should be a boolean, not a string.`);
|
|
146
|
+
}
|
|
147
|
+
return parsed.data?.value === true;
|
|
148
|
+
}
|
|
149
|
+
function readDefault(value, label, warnings) {
|
|
150
|
+
const parsed = DefaultSchema.safeParse(value);
|
|
151
|
+
if (parsed.success)
|
|
152
|
+
return parsed.data ?? undefined;
|
|
153
|
+
warnings.push(`Ignoring the default for ${label}: only text, numbers and true/false can be configuration values.`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
function describeType(value) {
|
|
157
|
+
if (value === null)
|
|
158
|
+
return "null";
|
|
159
|
+
if (Array.isArray(value))
|
|
160
|
+
return "a list";
|
|
161
|
+
if (typeof value === "object")
|
|
162
|
+
return "an object";
|
|
163
|
+
return `a ${typeof value}`;
|
|
164
|
+
}
|
|
165
|
+
var SeedingEntrySchema = exports_external.looseObject({ strategy: optionalText });
|
|
166
|
+
var SeedingSchema = jsonObject.transform((declared) => {
|
|
167
|
+
const entities = Object.create(null);
|
|
168
|
+
for (const [name, entry] of Object.entries(declared)) {
|
|
169
|
+
const parsed = SeedingEntrySchema.safeParse(entry);
|
|
170
|
+
Object.defineProperty(entities, name, {
|
|
171
|
+
value: parsed.success ? parsed.data : {},
|
|
172
|
+
writable: true,
|
|
173
|
+
enumerable: true,
|
|
174
|
+
configurable: true
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return entities;
|
|
178
|
+
}).optional().catch(undefined);
|
|
179
|
+
var DeclaredNamesSchema = jsonObject.transform((declared) => Object.keys(declared)).optional().catch(undefined);
|
|
180
|
+
var ConnectionEntrySchema = exports_external.looseObject({ connector: optionalText });
|
|
181
|
+
var DeclaredConnectionsSchema = jsonObject.transform((declared) => {
|
|
182
|
+
const connectors = new Map;
|
|
183
|
+
for (const [name, entry] of Object.entries(declared)) {
|
|
184
|
+
const parsed = ConnectionEntrySchema.safeParse(entry);
|
|
185
|
+
connectors.set(name, parsed.success ? parsed.data.connector ?? "" : "");
|
|
186
|
+
}
|
|
187
|
+
return connectors;
|
|
188
|
+
}).optional().catch(undefined);
|
|
189
|
+
|
|
190
|
+
// src/api/manifest-schema.ts
|
|
191
|
+
var TenantSectionSchema = exports_external.union([jsonObject, exports_external.array(exports_external.unknown())]).optional().catch(undefined);
|
|
192
|
+
var PackageManifestSchema = exports_external.looseObject({
|
|
193
|
+
packageVersion: optionalText,
|
|
194
|
+
configurations: ManifestConfigurationsSchema.optional(),
|
|
195
|
+
solution: exports_external.looseObject({
|
|
196
|
+
artifactFilename: optionalText,
|
|
197
|
+
configuration: DeclaredConfigurationSchema,
|
|
198
|
+
processes: DeclaredNamesSchema,
|
|
199
|
+
assets: DeclaredNamesSchema,
|
|
200
|
+
connections: DeclaredConnectionsSchema
|
|
201
|
+
}).optional().catch(undefined),
|
|
202
|
+
dataFabric: exports_external.looseObject({
|
|
203
|
+
schemaFilename: exports_external.unknown().optional(),
|
|
204
|
+
seeding: SeedingSchema
|
|
205
|
+
}).optional().catch(undefined),
|
|
206
|
+
storageBuckets: TenantSectionSchema,
|
|
207
|
+
indexes: TenantSectionSchema
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// src/api/packages.ts
|
|
211
|
+
var VerticalListSchema = exports_external.looseObject({
|
|
212
|
+
verticals: exports_external.array(exports_external.string()).optional()
|
|
213
|
+
});
|
|
214
|
+
var VerticalSolutionPackageSchema = exports_external.looseObject({
|
|
215
|
+
packageName: exports_external.string(),
|
|
216
|
+
displayName: exports_external.string().nullish().catch(undefined),
|
|
217
|
+
currentVersion: optionalText
|
|
218
|
+
});
|
|
219
|
+
var PackageListSchema = exports_external.looseObject({
|
|
220
|
+
packages: exports_external.array(VerticalSolutionPackageSchema).optional()
|
|
221
|
+
});
|
|
222
|
+
var PackageVersionSchema = exports_external.looseObject({
|
|
223
|
+
version: exports_external.string(),
|
|
224
|
+
relation: exports_external.enum(["previous", "current", "next", "blocked"]).optional().catch(undefined),
|
|
225
|
+
allowed: exports_external.boolean().optional().catch(undefined),
|
|
226
|
+
requiredVersion: optionalText
|
|
227
|
+
});
|
|
228
|
+
var VersionNavSchema = exports_external.looseObject({
|
|
229
|
+
versions: exports_external.array(exports_external.union([exports_external.string(), PackageVersionSchema]))
|
|
230
|
+
});
|
|
231
|
+
async function listVerticals(config, signal) {
|
|
232
|
+
const result = await vssRequestParsed(config, "verticals", VerticalListSchema, { query: scopeQuery(config), signal });
|
|
233
|
+
return result.verticals ?? [];
|
|
234
|
+
}
|
|
235
|
+
async function listPackages(config, vertical, stage, signal) {
|
|
236
|
+
const result = await vssRequestParsed(config, "packages", PackageListSchema, { query: { ...scopeQuery(config), vertical, env: stage }, signal });
|
|
237
|
+
return result.packages ?? [];
|
|
238
|
+
}
|
|
239
|
+
async function listPackageVersions(config, packageName, options = {}) {
|
|
240
|
+
return vssRequestParsed(config, `packages/${encodeSegment("packageName", packageName)}/versions`, VersionNavSchema, {
|
|
241
|
+
query: {
|
|
242
|
+
...scopeQuery(config),
|
|
243
|
+
env: options.stage,
|
|
244
|
+
anchorVersion: options.anchorVersion,
|
|
245
|
+
after: options.after,
|
|
246
|
+
before: options.before
|
|
247
|
+
},
|
|
248
|
+
signal: options.signal
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function normalizeVersions(nav) {
|
|
252
|
+
return nav.versions.map((entry) => typeof entry === "string" ? { version: entry } : entry);
|
|
253
|
+
}
|
|
254
|
+
async function getManifest(config, packageName, version, signal) {
|
|
255
|
+
return vssRequestParsed(config, `packages/${encodeSegment("packageName", packageName)}` + `/versions/${encodeSegment("version", version)}/manifest`, PackageManifestSchema, { query: scopeQuery(config), signal });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/services/seed/seed-context.ts
|
|
259
|
+
var SeedContextSchema = exports_external.object({
|
|
260
|
+
packageName: exports_external.string().min(1).refine((name) => packageNameProblem(name) === undefined),
|
|
261
|
+
packageVersion: exports_external.string().min(1),
|
|
262
|
+
serviceUrl: optionalText,
|
|
263
|
+
organizationId: optionalText,
|
|
264
|
+
organizationName: optionalText,
|
|
265
|
+
tenantId: optionalText,
|
|
266
|
+
tenantName: optionalText,
|
|
267
|
+
folderKey: optionalText,
|
|
268
|
+
deploymentName: optionalText
|
|
269
|
+
});
|
|
270
|
+
async function writeSeedContext(context, workspaceDir) {
|
|
271
|
+
const fs = getFileSystem();
|
|
272
|
+
const dir = workspaceDir ?? packageDir(context.packageName);
|
|
273
|
+
const refuseALinkedWorkspace = async () => {
|
|
274
|
+
if (!await namesSeedRoot(dir))
|
|
275
|
+
return;
|
|
276
|
+
const inside = await insideSeedRoot(dir);
|
|
277
|
+
if (!inside.ok)
|
|
278
|
+
throw new Error(inside.message);
|
|
279
|
+
};
|
|
280
|
+
await refuseALinkedWorkspace();
|
|
281
|
+
await fs.mkdir(dir);
|
|
282
|
+
await refuseALinkedWorkspace();
|
|
283
|
+
const path = fs.path.join(dir, SEED_CONTEXT_FILE);
|
|
284
|
+
await writeFileByRename(path, JSON.stringify(context, null, 2));
|
|
285
|
+
return path;
|
|
286
|
+
}
|
|
287
|
+
async function removeSeedContext(dir) {
|
|
288
|
+
const fs = getFileSystem();
|
|
289
|
+
if (await namesSeedRoot(dir)) {
|
|
290
|
+
const inside = await insideSeedRoot(dir);
|
|
291
|
+
if (!inside.ok)
|
|
292
|
+
throw new Error(inside.message);
|
|
293
|
+
}
|
|
294
|
+
const path = fs.path.join(dir, SEED_CONTEXT_FILE);
|
|
295
|
+
if (typeof fs.stat === "function") {
|
|
296
|
+
const stats = await fs.stat(path);
|
|
297
|
+
if (stats?.isDirectory()) {
|
|
298
|
+
throw new Error(`Refusing to remove context directory ${path}.`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
await fs.rm(path);
|
|
302
|
+
}
|
|
303
|
+
async function readSeedContext(dir) {
|
|
304
|
+
const fs = getFileSystem();
|
|
305
|
+
const path = fs.path.join(dir, SEED_CONTEXT_FILE);
|
|
306
|
+
if (!await metadataIsItsOwnFile(dir, SEED_CONTEXT_FILE, fs)) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const [readError, contents] = await catchError(fs.readFile(path, "utf-8"));
|
|
310
|
+
if (readError || contents === null)
|
|
311
|
+
return;
|
|
312
|
+
const [parseError, parsed] = catchError(() => JSON.parse(contents));
|
|
313
|
+
if (parseError)
|
|
314
|
+
return;
|
|
315
|
+
return asSeedContext(parsed);
|
|
316
|
+
}
|
|
317
|
+
function asSeedContext(value) {
|
|
318
|
+
const parsed = SeedContextSchema.safeParse(value);
|
|
319
|
+
if (!parsed.success)
|
|
320
|
+
return;
|
|
321
|
+
const context = parsed.data;
|
|
322
|
+
const [urlError, normalized] = catchError(() => context.serviceUrl === undefined ? undefined : validateServiceUrl(context.serviceUrl));
|
|
323
|
+
if (urlError) {
|
|
324
|
+
writeWarning(`Ignoring the service URL recorded in ${SEED_CONTEXT_FILE}: ${urlError.message} ` + "The run goes to the platform's own service; pass --service-url to name another.");
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
packageName: context.packageName,
|
|
328
|
+
packageVersion: context.packageVersion,
|
|
329
|
+
serviceUrl: urlError ? undefined : normalized,
|
|
330
|
+
organizationId: context.organizationId,
|
|
331
|
+
organizationName: context.organizationName,
|
|
332
|
+
tenantId: context.tenantId,
|
|
333
|
+
tenantName: context.tenantName,
|
|
334
|
+
folderKey: context.folderKey,
|
|
335
|
+
deploymentName: context.deploymentName
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/api/folders.ts
|
|
340
|
+
var FOLDER_PAGE_SIZE = 1000;
|
|
341
|
+
var MAX_FOLDER_PAGES = 50;
|
|
342
|
+
async function findFolderPath(config, folderKey, signal) {
|
|
343
|
+
const [clientError, api] = await catchError(foldersApi(config));
|
|
344
|
+
if (clientError || !api)
|
|
345
|
+
return;
|
|
346
|
+
const [error, folder] = await catchError(api.foldersGetByKeyByIdentifier({ identifier: folderKey }, { signal }));
|
|
347
|
+
if (!error && folder?.fullyQualifiedName) {
|
|
348
|
+
return folder.fullyQualifiedName;
|
|
349
|
+
}
|
|
350
|
+
const [listError, folders] = await catchError(listFolderDtos(config, signal));
|
|
351
|
+
if (listError || !folders)
|
|
352
|
+
return;
|
|
353
|
+
const wanted = folderKey.toLowerCase();
|
|
354
|
+
return folders.find((entry) => entry.key?.toLowerCase() === wanted)?.fullyQualifiedName ?? undefined;
|
|
355
|
+
}
|
|
356
|
+
async function orchestratorApi(config, pick, folderKey) {
|
|
357
|
+
if (!config.tenantName) {
|
|
358
|
+
throw new Error("No tenant selected — run 'uip login tenant set <tenant>' first.");
|
|
359
|
+
}
|
|
360
|
+
if (!config.organizationId) {
|
|
361
|
+
throw new Error("The login carries no organization id, which the Orchestrator route needs — " + "run 'uip login' again.");
|
|
362
|
+
}
|
|
363
|
+
const sdk = await import("./index-7a3eqtxv.js");
|
|
364
|
+
return sdk.createApiClient(pick(sdk), {
|
|
365
|
+
tenant: config.tenantName,
|
|
366
|
+
...folderKey ? { folderKey } : {}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
function foldersApi(config) {
|
|
370
|
+
return orchestratorApi(config, (sdk) => sdk.FoldersApi);
|
|
371
|
+
}
|
|
372
|
+
async function listAllFolders(config, signal) {
|
|
373
|
+
const raw = await listFolderDtos(config, signal);
|
|
374
|
+
return raw.flatMap((folder) => {
|
|
375
|
+
const key = folder.key;
|
|
376
|
+
if (!key)
|
|
377
|
+
return [];
|
|
378
|
+
const displayName = folder.displayName ?? key;
|
|
379
|
+
return [
|
|
380
|
+
{
|
|
381
|
+
key,
|
|
382
|
+
displayName,
|
|
383
|
+
path: folder.fullyQualifiedName ?? displayName,
|
|
384
|
+
kind: folder.folderType
|
|
385
|
+
}
|
|
386
|
+
];
|
|
387
|
+
}).sort((a, b) => a.path.localeCompare(b.path));
|
|
388
|
+
}
|
|
389
|
+
async function listFolderDtos(config, signal) {
|
|
390
|
+
const api = await foldersApi(config);
|
|
391
|
+
return readAllPages("Orchestrator folders", (page) => api.foldersGet(page, { signal }));
|
|
392
|
+
}
|
|
393
|
+
async function readAllPages(what, fetchPage) {
|
|
394
|
+
const rows = [];
|
|
395
|
+
let previousFirst;
|
|
396
|
+
for (let page = 0;page < MAX_FOLDER_PAGES; page += 1) {
|
|
397
|
+
const [error, response] = await catchError(fetchPage({
|
|
398
|
+
$top: FOLDER_PAGE_SIZE,
|
|
399
|
+
$skip: page * FOLDER_PAGE_SIZE
|
|
400
|
+
}));
|
|
401
|
+
if (error) {
|
|
402
|
+
throw new Error(`Could not list ${what}: ${await extractErrorMessage(error)}`);
|
|
403
|
+
}
|
|
404
|
+
const values = response.value ?? [];
|
|
405
|
+
const first = values.length > 0 ? JSON.stringify(values[0]) : "";
|
|
406
|
+
if (values.length === 0 || first === previousFirst)
|
|
407
|
+
break;
|
|
408
|
+
previousFirst = first;
|
|
409
|
+
rows.push(...values);
|
|
410
|
+
if (values.length < FOLDER_PAGE_SIZE)
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
return rows;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/services/seed/sdk-schemas.ts
|
|
417
|
+
var flag = exports_external.boolean().optional().catch(undefined);
|
|
418
|
+
var TypeNameSchema = exports_external.union([exports_external.string(), exports_external.looseObject({ name: optionalText })]).optional().catch(undefined);
|
|
419
|
+
var FieldSchema = exports_external.looseObject({
|
|
420
|
+
name: optionalText,
|
|
421
|
+
sqlType: TypeNameSchema,
|
|
422
|
+
fieldDataType: TypeNameSchema,
|
|
423
|
+
fieldDisplayType: optionalText,
|
|
424
|
+
isRequired: flag,
|
|
425
|
+
isUnique: flag,
|
|
426
|
+
isPrimaryKey: flag,
|
|
427
|
+
isSystemField: flag,
|
|
428
|
+
isAttachment: flag,
|
|
429
|
+
isForeignKey: flag,
|
|
430
|
+
choiceSetId: optionalText,
|
|
431
|
+
referenceChoiceSet: exports_external.looseObject({ id: optionalText }).optional().catch(undefined),
|
|
432
|
+
referenceEntity: exports_external.looseObject({ name: optionalText }).optional().catch(undefined)
|
|
433
|
+
});
|
|
434
|
+
var EntitySchema = exports_external.looseObject({
|
|
435
|
+
id: optionalText,
|
|
436
|
+
name: optionalText,
|
|
437
|
+
fields: exports_external.array(exports_external.unknown()).optional().catch(undefined),
|
|
438
|
+
externalFields: exports_external.array(exports_external.unknown()).optional().catch(undefined)
|
|
439
|
+
});
|
|
440
|
+
var EntityListSchema = exports_external.array(exports_external.unknown()).nullish();
|
|
441
|
+
var ChoiceValueSchema = exports_external.looseObject({
|
|
442
|
+
name: optionalText,
|
|
443
|
+
displayName: optionalText,
|
|
444
|
+
numberId: anyNumber.optional().catch(undefined)
|
|
445
|
+
});
|
|
446
|
+
var CursorSchema = exports_external.union([
|
|
447
|
+
exports_external.string().transform((value) => ({ value })),
|
|
448
|
+
exports_external.looseObject({ value: exports_external.string() })
|
|
449
|
+
]).optional().catch(undefined);
|
|
450
|
+
var PageSchema = exports_external.looseObject({
|
|
451
|
+
items: exports_external.array(exports_external.unknown()).optional().catch(undefined),
|
|
452
|
+
hasNextPage: exports_external.unknown().optional(),
|
|
453
|
+
nextCursor: CursorSchema
|
|
454
|
+
});
|
|
455
|
+
var RecordItemsSchema = exports_external.array(jsonObject);
|
|
456
|
+
var list = exports_external.array(exports_external.unknown()).optional().catch(undefined);
|
|
457
|
+
var BatchResponseSchema = exports_external.looseObject({
|
|
458
|
+
successRecords: list,
|
|
459
|
+
records: list,
|
|
460
|
+
items: list,
|
|
461
|
+
value: list,
|
|
462
|
+
failureRecords: list
|
|
463
|
+
});
|
|
464
|
+
var WrittenRecordSchema = exports_external.looseObject({
|
|
465
|
+
Id: exports_external.unknown().optional(),
|
|
466
|
+
id: exports_external.unknown().optional()
|
|
467
|
+
});
|
|
468
|
+
var FailureRecordSchema = exports_external.looseObject({ error: optionalText });
|
|
469
|
+
var ExpandedValueSchema = exports_external.looseObject({
|
|
470
|
+
Id: optionalText,
|
|
471
|
+
Name: optionalText,
|
|
472
|
+
Size: anyNumber.optional().catch(undefined)
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// src/services/seed/entities.ts
|
|
476
|
+
function needsExpandedRecords(entity) {
|
|
477
|
+
for (const field of entity.fields.values()) {
|
|
478
|
+
if (field.isForeignKey || field.isAttachment)
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
var SYSTEM_FIELDS = new Set([
|
|
484
|
+
"Id",
|
|
485
|
+
"CreateTime",
|
|
486
|
+
"CreatedBy",
|
|
487
|
+
"UpdateTime",
|
|
488
|
+
"UpdatedBy",
|
|
489
|
+
"RecordOwner"
|
|
490
|
+
].map((name) => name.toLowerCase()));
|
|
491
|
+
async function loadLiveEntities(sdk, folderKey) {
|
|
492
|
+
const response = await sdk.entities.getAll(folderKey === undefined ? undefined : { folderKey });
|
|
493
|
+
const entities = EntityListSchema.safeParse(response);
|
|
494
|
+
if (!entities.success) {
|
|
495
|
+
throw new Error("Data Fabric sent an entity list that is not a list.");
|
|
496
|
+
}
|
|
497
|
+
const byName = new Map;
|
|
498
|
+
for (const entity of entities.data ?? []) {
|
|
499
|
+
const normalized = toSeedEntity(camelKeys(entity));
|
|
500
|
+
if (normalized)
|
|
501
|
+
byName.set(normalized.name.toLowerCase(), normalized);
|
|
502
|
+
}
|
|
503
|
+
return { byName };
|
|
504
|
+
}
|
|
505
|
+
function setOwn(target, key, value) {
|
|
506
|
+
if (key === "__proto__") {
|
|
507
|
+
Object.defineProperty(target, key, {
|
|
508
|
+
value,
|
|
509
|
+
enumerable: true,
|
|
510
|
+
writable: true,
|
|
511
|
+
configurable: true
|
|
512
|
+
});
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
target[key] = value;
|
|
516
|
+
}
|
|
517
|
+
function camelKeys(value) {
|
|
518
|
+
if (Array.isArray(value))
|
|
519
|
+
return value.map(camelKeys);
|
|
520
|
+
const record = plainRecord.safeParse(value);
|
|
521
|
+
if (!record.success)
|
|
522
|
+
return value;
|
|
523
|
+
const entries = Object.entries(record.data);
|
|
524
|
+
const out = {};
|
|
525
|
+
for (const [key, val] of entries) {
|
|
526
|
+
if (key[0] === key[0]?.toLowerCase())
|
|
527
|
+
setOwn(out, key, camelKeys(val));
|
|
528
|
+
}
|
|
529
|
+
for (const [key, val] of entries) {
|
|
530
|
+
const lowered = (key[0]?.toLowerCase() ?? "") + key.slice(1);
|
|
531
|
+
if (!Object.hasOwn(out, lowered)) {
|
|
532
|
+
setOwn(out, lowered, camelKeys(val));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return out;
|
|
536
|
+
}
|
|
537
|
+
function toSeedEntity(value) {
|
|
538
|
+
const parsed = EntitySchema.safeParse(value);
|
|
539
|
+
if (!parsed.success)
|
|
540
|
+
return;
|
|
541
|
+
const entity = parsed.data;
|
|
542
|
+
if (!entity.id || !entity.name)
|
|
543
|
+
return;
|
|
544
|
+
const fields = new Map;
|
|
545
|
+
const identityFields = [];
|
|
546
|
+
for (const item of entity.fields ?? []) {
|
|
547
|
+
const raw = FieldSchema.safeParse(item);
|
|
548
|
+
if (!raw.success || !raw.data.name)
|
|
549
|
+
continue;
|
|
550
|
+
const field = toSeedField(raw.data, raw.data.name);
|
|
551
|
+
fields.set(field.name.toLowerCase(), field);
|
|
552
|
+
if (field.isUnique && !field.isSystemField && !field.isAutoNumber) {
|
|
553
|
+
identityFields.push(field.name);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
id: entity.id,
|
|
558
|
+
name: entity.name,
|
|
559
|
+
fields,
|
|
560
|
+
identityFields,
|
|
561
|
+
isFederated: (entity.externalFields?.length ?? 0) > 0
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function toSeedField(raw, name) {
|
|
565
|
+
const displayType = raw.fieldDisplayType ?? "";
|
|
566
|
+
const dataType = dataTypeNameOf(raw);
|
|
567
|
+
const isFile = raw.isAttachment === true || displayType === "File" || dataType === "FILE";
|
|
568
|
+
return {
|
|
569
|
+
name,
|
|
570
|
+
sqlType: sqlTypeOf(raw),
|
|
571
|
+
isRequired: raw.isRequired === true,
|
|
572
|
+
isUnique: raw.isUnique === true,
|
|
573
|
+
isPrimaryKey: raw.isPrimaryKey === true,
|
|
574
|
+
isSystemField: raw.isSystemField === true || SYSTEM_FIELDS.has(name.toLowerCase()),
|
|
575
|
+
isAttachment: isFile,
|
|
576
|
+
choiceSetId: raw.choiceSetId ?? raw.referenceChoiceSet?.id,
|
|
577
|
+
isMultiChoice: displayType === "ChoiceSetMultiple" || dataType === "CHOICE_SET_MULTIPLE",
|
|
578
|
+
isLazyText: dataType === "MULTILINE_MAX",
|
|
579
|
+
isAutoNumber: displayType === "AutoNumber" || dataType === "AUTO_NUMBER",
|
|
580
|
+
isForeignKey: !isFile && (raw.isForeignKey === true || displayType === "Relationship" || dataType === "RELATIONSHIP"),
|
|
581
|
+
referenceEntityName: isFile ? undefined : raw.referenceEntity?.name
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function dataTypeNameOf(raw) {
|
|
585
|
+
const dataType = typeof raw.fieldDataType === "string" ? raw.fieldDataType : raw.fieldDataType?.name;
|
|
586
|
+
return (dataType ?? "").toUpperCase();
|
|
587
|
+
}
|
|
588
|
+
function sqlTypeOf(raw) {
|
|
589
|
+
if (typeof raw.sqlType === "string")
|
|
590
|
+
return raw.sqlType.toUpperCase();
|
|
591
|
+
if (raw.sqlType?.name)
|
|
592
|
+
return raw.sqlType.name.toUpperCase();
|
|
593
|
+
switch (dataTypeNameOf(raw)) {
|
|
594
|
+
case "BOOLEAN":
|
|
595
|
+
return "BIT";
|
|
596
|
+
case "INTEGER":
|
|
597
|
+
return "INT";
|
|
598
|
+
case "BIG_INTEGER":
|
|
599
|
+
return "BIGINT";
|
|
600
|
+
case "DECIMAL":
|
|
601
|
+
return "DECIMAL";
|
|
602
|
+
case "FLOAT":
|
|
603
|
+
case "DOUBLE":
|
|
604
|
+
return "FLOAT";
|
|
605
|
+
case "DATE":
|
|
606
|
+
return "DATE";
|
|
607
|
+
case "DATETIME":
|
|
608
|
+
return "DATETIME";
|
|
609
|
+
case "DATETIME_WITH_TZ":
|
|
610
|
+
return "DATETIMEOFFSET";
|
|
611
|
+
case "STRING":
|
|
612
|
+
case "MULTILINE_TEXT":
|
|
613
|
+
case "MULTILINE_MAX":
|
|
614
|
+
case "UUID":
|
|
615
|
+
return "NVARCHAR";
|
|
616
|
+
default:
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async function loadChoiceSetValues(sdk, choiceSetId, folderKey) {
|
|
621
|
+
const [error, items] = await catchError(readAllChoiceValues(sdk, choiceSetId, folderKey));
|
|
622
|
+
if (error || items === undefined)
|
|
623
|
+
return;
|
|
624
|
+
const byLabel = new Map;
|
|
625
|
+
const labelsById = new Map;
|
|
626
|
+
const labels = [];
|
|
627
|
+
for (const item of items) {
|
|
628
|
+
if (item.numberId === undefined)
|
|
629
|
+
continue;
|
|
630
|
+
if (item.name)
|
|
631
|
+
byLabel.set(item.name.toLowerCase(), item.numberId);
|
|
632
|
+
if (item.displayName) {
|
|
633
|
+
byLabel.set(item.displayName.toLowerCase(), item.numberId);
|
|
634
|
+
}
|
|
635
|
+
const label = item.displayName ?? item.name;
|
|
636
|
+
if (label) {
|
|
637
|
+
labels.push(label);
|
|
638
|
+
labelsById.set(item.numberId, label);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return { byLabel, labelsById, labels };
|
|
642
|
+
}
|
|
643
|
+
async function readAllChoiceValues(sdk, choiceSetId, folderKey) {
|
|
644
|
+
const scope = folderKey === undefined ? {} : { folderKey };
|
|
645
|
+
const values = [];
|
|
646
|
+
const guard = pageGuard(`choice set ${choiceSetId}`);
|
|
647
|
+
let cursor;
|
|
648
|
+
do {
|
|
649
|
+
const response = await sdk.entities.choicesets.getById(choiceSetId, cursor === undefined ? { ...scope, pageSize: RECORD_PAGE_SIZE } : { ...scope, pageSize: RECORD_PAGE_SIZE, cursor });
|
|
650
|
+
const page = PageSchema.safeParse(response);
|
|
651
|
+
if (!page.success || page.data.items === undefined)
|
|
652
|
+
return;
|
|
653
|
+
for (const item of page.data.items) {
|
|
654
|
+
const value = ChoiceValueSchema.safeParse(camelKeys(item));
|
|
655
|
+
if (value.success)
|
|
656
|
+
values.push(value.data);
|
|
657
|
+
}
|
|
658
|
+
cursor = nextCursorOf(page.data, `choice set ${choiceSetId}`);
|
|
659
|
+
guard.next(cursor);
|
|
660
|
+
} while (cursor !== undefined);
|
|
661
|
+
return values;
|
|
662
|
+
}
|
|
663
|
+
function withSystemKeyAliases(record) {
|
|
664
|
+
const out = {};
|
|
665
|
+
for (const [key, val] of Object.entries(record)) {
|
|
666
|
+
setOwn(out, key, aliasNested(val));
|
|
667
|
+
}
|
|
668
|
+
for (const key of SYSTEM_VALUE_KEYS) {
|
|
669
|
+
const lower = key.toLowerCase();
|
|
670
|
+
if (!Object.hasOwn(out, key) && Object.hasOwn(out, lower)) {
|
|
671
|
+
setOwn(out, key, out[lower]);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return out;
|
|
675
|
+
}
|
|
676
|
+
function aliasNested(value) {
|
|
677
|
+
if (Array.isArray(value))
|
|
678
|
+
return value.map(aliasNested);
|
|
679
|
+
const record = plainRecord.safeParse(value);
|
|
680
|
+
return record.success ? withSystemKeyAliases(record.data) : value;
|
|
681
|
+
}
|
|
682
|
+
var SYSTEM_VALUE_KEYS = ["Id", "Name", "Size"];
|
|
683
|
+
async function loadLiveRecords(sdk, entityId, folderKey, expand = false) {
|
|
684
|
+
const records = [];
|
|
685
|
+
const scope = {
|
|
686
|
+
...folderKey === undefined ? {} : { folderKey },
|
|
687
|
+
...expand ? { expansionLevel: 1 } : {}
|
|
688
|
+
};
|
|
689
|
+
const guard = pageGuard(`entity ${entityId}`);
|
|
690
|
+
let cursor;
|
|
691
|
+
do {
|
|
692
|
+
const response = await sdk.entities.getAllRecords(entityId, cursor === undefined ? { ...scope, pageSize: RECORD_PAGE_SIZE } : { ...scope, pageSize: RECORD_PAGE_SIZE, cursor });
|
|
693
|
+
const page = PageSchema.safeParse(response);
|
|
694
|
+
if (!page.success || page.data.items === undefined) {
|
|
695
|
+
throw new Error(`Data Fabric sent a page of entity ${entityId} records with no items list. ` + "Stopping rather than acting on a partial read.");
|
|
696
|
+
}
|
|
697
|
+
const items = RecordItemsSchema.safeParse(page.data.items);
|
|
698
|
+
if (!items.success) {
|
|
699
|
+
throw new Error(`Data Fabric sent a page of entity ${entityId} records with an item that is ` + "not a record. Stopping rather than acting on a partial read.");
|
|
700
|
+
}
|
|
701
|
+
for (const item of items.data) {
|
|
702
|
+
records.push(plainRecord.safeParse(item).success ? withSystemKeyAliases(item) : item);
|
|
703
|
+
}
|
|
704
|
+
cursor = nextCursorOf(page.data, `entity ${entityId}`);
|
|
705
|
+
guard.next(cursor);
|
|
706
|
+
} while (cursor !== undefined);
|
|
707
|
+
return records;
|
|
708
|
+
}
|
|
709
|
+
var MAX_RECORD_PAGES = 1000;
|
|
710
|
+
function nextCursorOf(page, what) {
|
|
711
|
+
if (!page.hasNextPage)
|
|
712
|
+
return;
|
|
713
|
+
if (page.nextCursor === undefined) {
|
|
714
|
+
throw new Error(`Data Fabric said there are more pages of ${what} but sent no cursor to read ` + "them. Stopping rather than acting on a partial read.");
|
|
715
|
+
}
|
|
716
|
+
return page.nextCursor;
|
|
717
|
+
}
|
|
718
|
+
function pageGuard(what) {
|
|
719
|
+
const seen = new Set;
|
|
720
|
+
let pages = 1;
|
|
721
|
+
return {
|
|
722
|
+
next(cursor) {
|
|
723
|
+
if (cursor === undefined)
|
|
724
|
+
return;
|
|
725
|
+
const key = JSON.stringify(cursor);
|
|
726
|
+
if (seen.has(key)) {
|
|
727
|
+
throw new Error(`Data Fabric returned the same page cursor twice while reading ${what}; ` + "stopping rather than reading forever.");
|
|
728
|
+
}
|
|
729
|
+
seen.add(key);
|
|
730
|
+
pages += 1;
|
|
731
|
+
if (pages > MAX_RECORD_PAGES) {
|
|
732
|
+
throw new Error(`Reading ${what} did not end after ${MAX_RECORD_PAGES} pages; stopping.`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
var RECORD_PAGE_SIZE = 1000;
|
|
738
|
+
|
|
739
|
+
// src/services/seed/csv.ts
|
|
740
|
+
function parseCsv(contents) {
|
|
741
|
+
const state = {
|
|
742
|
+
rows: [],
|
|
743
|
+
row: [],
|
|
744
|
+
field: "",
|
|
745
|
+
quoted: false,
|
|
746
|
+
quoteOpenedAt: 0,
|
|
747
|
+
fieldStarted: false,
|
|
748
|
+
rowStarted: false
|
|
749
|
+
};
|
|
750
|
+
const text = contents.replace(/^/, "");
|
|
751
|
+
let index = 0;
|
|
752
|
+
while (index < text.length) {
|
|
753
|
+
index = state.quoted ? stepQuoted(state, text, index) : stepUnquoted(state, text, index);
|
|
754
|
+
}
|
|
755
|
+
if (state.quoted) {
|
|
756
|
+
const line = text.slice(0, state.quoteOpenedAt).split(`
|
|
757
|
+
`).length;
|
|
758
|
+
throw new Error(`unterminated quoted field: the quote opened on line ${line} is never closed`);
|
|
759
|
+
}
|
|
760
|
+
if (state.rowStarted)
|
|
761
|
+
endRow(state);
|
|
762
|
+
if (state.rows.length === 0)
|
|
763
|
+
return { headers: [], rows: [] };
|
|
764
|
+
return {
|
|
765
|
+
headers: state.rows[0].map((header) => header.trim()),
|
|
766
|
+
rows: state.rows.slice(1)
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function endField(state) {
|
|
770
|
+
state.row.push(state.field);
|
|
771
|
+
state.field = "";
|
|
772
|
+
state.fieldStarted = false;
|
|
773
|
+
}
|
|
774
|
+
function endRow(state) {
|
|
775
|
+
if (!state.rowStarted) {
|
|
776
|
+
state.row = [];
|
|
777
|
+
state.field = "";
|
|
778
|
+
state.fieldStarted = false;
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
endField(state);
|
|
782
|
+
state.rows.push(state.row);
|
|
783
|
+
state.row = [];
|
|
784
|
+
state.rowStarted = false;
|
|
785
|
+
}
|
|
786
|
+
function stepQuoted(state, text, index) {
|
|
787
|
+
const char = text[index];
|
|
788
|
+
if (char !== '"') {
|
|
789
|
+
state.field += char;
|
|
790
|
+
return index + 1;
|
|
791
|
+
}
|
|
792
|
+
if (text[index + 1] === '"') {
|
|
793
|
+
state.field += '"';
|
|
794
|
+
return index + 2;
|
|
795
|
+
}
|
|
796
|
+
state.quoted = false;
|
|
797
|
+
return index + 1;
|
|
798
|
+
}
|
|
799
|
+
function stepUnquoted(state, text, index) {
|
|
800
|
+
const char = text[index];
|
|
801
|
+
if (char === '"' && !state.fieldStarted) {
|
|
802
|
+
state.quoted = true;
|
|
803
|
+
state.quoteOpenedAt = index;
|
|
804
|
+
state.fieldStarted = true;
|
|
805
|
+
state.rowStarted = true;
|
|
806
|
+
return index + 1;
|
|
807
|
+
}
|
|
808
|
+
if (char === ",") {
|
|
809
|
+
state.rowStarted = true;
|
|
810
|
+
endField(state);
|
|
811
|
+
return index + 1;
|
|
812
|
+
}
|
|
813
|
+
if (char === "\r") {
|
|
814
|
+
if (text[index + 1] === `
|
|
815
|
+
`)
|
|
816
|
+
return index + 1;
|
|
817
|
+
endRow(state);
|
|
818
|
+
return index + 1;
|
|
819
|
+
}
|
|
820
|
+
if (char === `
|
|
821
|
+
`) {
|
|
822
|
+
endRow(state);
|
|
823
|
+
return index + 1;
|
|
824
|
+
}
|
|
825
|
+
state.field += char;
|
|
826
|
+
state.fieldStarted = true;
|
|
827
|
+
if (char.trim() !== "")
|
|
828
|
+
state.rowStarted = true;
|
|
829
|
+
return index + 1;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/services/seed/expressions.ts
|
|
833
|
+
var EXPRESSION = /%\{([^}]*)\}%/g;
|
|
834
|
+
var INDEXED_LOOKUP = /^([A-Za-z]+)\.([A-Za-z]+)\['([^']+)'\]\.([A-Za-z]+)$/;
|
|
835
|
+
function resolveExpressions(raw, context) {
|
|
836
|
+
if (!raw.includes("%{"))
|
|
837
|
+
return { value: raw, errors: [] };
|
|
838
|
+
const errors = [];
|
|
839
|
+
const spans = [];
|
|
840
|
+
let value = "";
|
|
841
|
+
let at = 0;
|
|
842
|
+
let kept = false;
|
|
843
|
+
EXPRESSION.lastIndex = 0;
|
|
844
|
+
for (let match = EXPRESSION.exec(raw);match !== null; match = EXPRESSION.exec(raw)) {
|
|
845
|
+
const [text, body] = match;
|
|
846
|
+
value += raw.slice(at, match.index);
|
|
847
|
+
const shared = kept && match.index < at ? at - match.index : 0;
|
|
848
|
+
const resolved = resolveOne(body.trim(), context);
|
|
849
|
+
if (resolved === undefined)
|
|
850
|
+
errors.push(text);
|
|
851
|
+
value += resolved ?? text.slice(shared);
|
|
852
|
+
kept = resolved === undefined;
|
|
853
|
+
spans.push([match.index, match.index + text.length]);
|
|
854
|
+
at = match.index + text.length;
|
|
855
|
+
EXPRESSION.lastIndex = raw[at] === "{" && raw[at - 1] === "%" ? at - 1 : at;
|
|
856
|
+
}
|
|
857
|
+
value += raw.slice(!kept && raw[at - 1] === "%" && raw[at] === "{" ? at - 1 : at);
|
|
858
|
+
errors.push(...strayMarkers(raw, spans));
|
|
859
|
+
return { value, errors };
|
|
860
|
+
}
|
|
861
|
+
function consumed(spans, at) {
|
|
862
|
+
return spans.some(([from, to]) => at === from || at > from && at < to - 1);
|
|
863
|
+
}
|
|
864
|
+
function strayMarkers(raw, spans) {
|
|
865
|
+
const stray = [];
|
|
866
|
+
for (let at = raw.indexOf("%{");at !== -1; at = raw.indexOf("%{", at + 2)) {
|
|
867
|
+
if (consumed(spans, at))
|
|
868
|
+
continue;
|
|
869
|
+
const rest = raw.slice(at);
|
|
870
|
+
stray.push(rest.length > 40 ? `${rest.slice(0, 40)}…` : rest);
|
|
871
|
+
}
|
|
872
|
+
return stray;
|
|
873
|
+
}
|
|
874
|
+
function resolveOne(body, context) {
|
|
875
|
+
const contextValue = resolveContextVariable(body, context);
|
|
876
|
+
if (contextValue !== undefined)
|
|
877
|
+
return contextValue;
|
|
878
|
+
const solutionValue = resolveSolutionScoped(body, context);
|
|
879
|
+
if (solutionValue !== undefined)
|
|
880
|
+
return solutionValue;
|
|
881
|
+
const indexed = INDEXED_LOOKUP.exec(body);
|
|
882
|
+
if (indexed) {
|
|
883
|
+
const [, namespace, collection, name, property] = indexed;
|
|
884
|
+
return resolveIndexed(namespace, collection, name, property, context);
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
function resolveContextVariable(body, context) {
|
|
889
|
+
switch (body) {
|
|
890
|
+
case "UIPATH_BASE_URL":
|
|
891
|
+
return context.baseUrl;
|
|
892
|
+
case "UIPATH_ORG_ID":
|
|
893
|
+
return context.organizationId;
|
|
894
|
+
case "UIPATH_ORG_NAME":
|
|
895
|
+
return context.organizationName;
|
|
896
|
+
case "UIPATH_TENANT_ID":
|
|
897
|
+
return context.tenantId;
|
|
898
|
+
case "UIPATH_TENANT_NAME":
|
|
899
|
+
return context.tenantName;
|
|
900
|
+
case "UIPATH_FOLDER_KEY":
|
|
901
|
+
return context.folderKey;
|
|
902
|
+
case "UIPATH_FOLDER_ID":
|
|
903
|
+
return context.solutionFolderId;
|
|
904
|
+
case "UIPATH_FOLDER_PATH":
|
|
905
|
+
return context.solutionFolderPath;
|
|
906
|
+
default:
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function resolveSolutionScoped(body, context) {
|
|
911
|
+
switch (body) {
|
|
912
|
+
case "Solution.FolderPath":
|
|
913
|
+
return context.solutionFolderPath;
|
|
914
|
+
case "Solution.FolderKey":
|
|
915
|
+
return context.solutionFolderKey;
|
|
916
|
+
case "Solution.FolderId":
|
|
917
|
+
return context.solutionFolderId;
|
|
918
|
+
case "Solution.ParentFolderId":
|
|
919
|
+
return context.solutionParentFolderId;
|
|
920
|
+
case "Solution.ParentFolderKey":
|
|
921
|
+
return context.solutionParentFolderKey;
|
|
922
|
+
default:
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function resolveIndexed(namespace, collection, name, property, context) {
|
|
927
|
+
if (namespace === "Solution") {
|
|
928
|
+
return context.solutionResources?.get(collection)?.get(name)?.get(property);
|
|
929
|
+
}
|
|
930
|
+
if (namespace !== "DataFabric" || collection !== "Entities")
|
|
931
|
+
return;
|
|
932
|
+
const entity = name.toLowerCase();
|
|
933
|
+
if (property === "Id")
|
|
934
|
+
return context.entityIds?.get(entity);
|
|
935
|
+
if (property === "Name")
|
|
936
|
+
return context.entityNames?.get(entity);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
var INSTALLED_FOLDER_EXPRESSIONS = new Set([
|
|
940
|
+
"Solution.FolderPath",
|
|
941
|
+
"Solution.FolderKey",
|
|
942
|
+
"Solution.FolderId",
|
|
943
|
+
"Solution.ParentFolderId",
|
|
944
|
+
"Solution.ParentFolderKey",
|
|
945
|
+
"UIPATH_FOLDER_KEY",
|
|
946
|
+
"UIPATH_FOLDER_ID",
|
|
947
|
+
"UIPATH_FOLDER_PATH"
|
|
948
|
+
]);
|
|
949
|
+
function describeUnresolved(expression) {
|
|
950
|
+
const body = expression.replace(/^%\{/, "").replace(/\}%$/, "").trim();
|
|
951
|
+
if (!expression.endsWith("}%")) {
|
|
952
|
+
return `${expression} is not closed: a seed expression is written %{NAME}%. ` + "Close it, or remove the %{ if the cell is meant to hold that text.";
|
|
953
|
+
}
|
|
954
|
+
if (body === "") {
|
|
955
|
+
return `${expression} names no expression. Write the name inside it, for example ` + "%{UIPATH_TENANT_ID}%, or remove it if the cell is meant to be empty.";
|
|
956
|
+
}
|
|
957
|
+
if (INSTALLED_FOLDER_EXPRESSIONS.has(body)) {
|
|
958
|
+
return `${expression} needs the folder the solution installed into, which this seed run did ` + "not know or could not read. Seed after the install completes, or pass --folder-key or " + "--folder-path for the installed folder.";
|
|
959
|
+
}
|
|
960
|
+
if (body.startsWith("Solution.")) {
|
|
961
|
+
return `${expression} names a solution resource that was not found in the folder the ` + "solution installed into. Check that the manifest declares it and that it is " + "deployed, or replace it with a literal value in the seed file.";
|
|
962
|
+
}
|
|
963
|
+
return `${expression} is not a supported seed expression. Supported: ` + "%{UIPATH_BASE_URL}%, %{UIPATH_ORG_ID}%, %{UIPATH_ORG_NAME}%, %{UIPATH_TENANT_ID}%, " + "%{UIPATH_TENANT_NAME}%, %{UIPATH_FOLDER_KEY}%, %{UIPATH_FOLDER_ID}%, %{UIPATH_FOLDER_PATH}%, " + "%{Solution.FolderPath}%, %{Solution.Processes['Name'].Id}% and the other Solution " + "resources, %{DataFabric.Entities['Name'].Id}%.";
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/services/seed/own-keys.ts
|
|
967
|
+
function journalEntity(journal, entityName) {
|
|
968
|
+
if (Object.hasOwn(journal, entityName))
|
|
969
|
+
return journal[entityName];
|
|
970
|
+
const entries = Object.create(null);
|
|
971
|
+
return entries;
|
|
972
|
+
}
|
|
973
|
+
function putOwn(map, key, value) {
|
|
974
|
+
Object.defineProperty(map, key, {
|
|
975
|
+
value,
|
|
976
|
+
writable: true,
|
|
977
|
+
enumerable: true,
|
|
978
|
+
configurable: true
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
function fieldMap(from) {
|
|
982
|
+
const map = Object.create(null);
|
|
983
|
+
return from === undefined ? map : Object.assign(map, from);
|
|
984
|
+
}
|
|
985
|
+
function ownValue(record, key) {
|
|
986
|
+
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/services/seed/csv-cells.ts
|
|
990
|
+
var CSV_NULL = "NULL";
|
|
991
|
+
var DATE_PATTERNS = [
|
|
992
|
+
{ pattern: /^(\d{4})-(\d{1,2})-(\d{1,2})$/, order: [1, 2, 3] },
|
|
993
|
+
{ pattern: /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/, order: [3, 2, 1] },
|
|
994
|
+
{ pattern: /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/, order: [3, 2, 1] }
|
|
995
|
+
];
|
|
996
|
+
function pad(value) {
|
|
997
|
+
return value.length === 1 ? `0${value}` : value;
|
|
998
|
+
}
|
|
999
|
+
var TIME_SUFFIX = /[T ](\d{1,2}):(\d{1,2})(?::(\d{1,2})(\.\d{1,6})?)?$/;
|
|
1000
|
+
var ZONE_SUFFIX = /(Z|[+-]\d{2}:\d{2})$/i;
|
|
1001
|
+
function readCell(raw, sqlType) {
|
|
1002
|
+
const trimmed = raw.trim();
|
|
1003
|
+
if (trimmed === "")
|
|
1004
|
+
return { present: false };
|
|
1005
|
+
if (trimmed.toUpperCase() === CSV_NULL) {
|
|
1006
|
+
return { present: true, value: null };
|
|
1007
|
+
}
|
|
1008
|
+
if (trimmed === `\\${CSV_NULL}`) {
|
|
1009
|
+
return { present: true, value: CSV_NULL };
|
|
1010
|
+
}
|
|
1011
|
+
return coerce(trimmed, sqlType, raw);
|
|
1012
|
+
}
|
|
1013
|
+
function coerce(value, sqlType, raw = value) {
|
|
1014
|
+
switch ((sqlType ?? "").toUpperCase()) {
|
|
1015
|
+
case "BIT":
|
|
1016
|
+
return coerceBoolean(value);
|
|
1017
|
+
case "INT":
|
|
1018
|
+
return coerceInteger(value, false);
|
|
1019
|
+
case "BIGINT":
|
|
1020
|
+
return coerceInteger(value, true);
|
|
1021
|
+
case "DECIMAL":
|
|
1022
|
+
case "FLOAT":
|
|
1023
|
+
case "NUMERIC":
|
|
1024
|
+
return coerceNumber(value);
|
|
1025
|
+
case "DATE":
|
|
1026
|
+
return coerceDate(value);
|
|
1027
|
+
case "DATETIME":
|
|
1028
|
+
case "DATETIME2":
|
|
1029
|
+
return coerceDateTime(value, false);
|
|
1030
|
+
case "DATETIMEOFFSET":
|
|
1031
|
+
return coerceDateTime(value, true);
|
|
1032
|
+
default:
|
|
1033
|
+
return { present: true, value: raw };
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function coerceBoolean(value) {
|
|
1037
|
+
const lower = value.toLowerCase();
|
|
1038
|
+
if (["true", "1", "yes", "y"].includes(lower)) {
|
|
1039
|
+
return { present: true, value: true };
|
|
1040
|
+
}
|
|
1041
|
+
if (["false", "0", "no", "n"].includes(lower)) {
|
|
1042
|
+
return { present: true, value: false };
|
|
1043
|
+
}
|
|
1044
|
+
return { present: true, error: `"${value}" is not a boolean` };
|
|
1045
|
+
}
|
|
1046
|
+
var INTEGER_RANGES = {
|
|
1047
|
+
INT: { min: -(2n ** 31n), max: 2n ** 31n - 1n },
|
|
1048
|
+
BIGINT: { min: -(2n ** 63n), max: 2n ** 63n - 1n }
|
|
1049
|
+
};
|
|
1050
|
+
function coerceInteger(value, bigint) {
|
|
1051
|
+
if (!/^[+-]?\d+$/.test(value)) {
|
|
1052
|
+
return { present: true, error: `"${value}" is not an integer` };
|
|
1053
|
+
}
|
|
1054
|
+
const type = bigint ? "BIGINT" : "INT";
|
|
1055
|
+
const { min, max } = INTEGER_RANGES[type];
|
|
1056
|
+
const exact = BigInt(value.startsWith("+") ? value.slice(1) : value);
|
|
1057
|
+
if (exact < min || exact > max) {
|
|
1058
|
+
return {
|
|
1059
|
+
present: true,
|
|
1060
|
+
error: `"${value}" is outside the ${type} range (${min} to ${max})`
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
const parsed = Number(value);
|
|
1064
|
+
if (Number.isSafeInteger(parsed))
|
|
1065
|
+
return { present: true, value: parsed };
|
|
1066
|
+
return { present: true, value };
|
|
1067
|
+
}
|
|
1068
|
+
var DECIMAL_NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
1069
|
+
var EXACT_DIGITS = 15;
|
|
1070
|
+
function coerceNumber(value) {
|
|
1071
|
+
const parsed = DECIMAL_NUMBER.test(value) ? Number(value) : Number.NaN;
|
|
1072
|
+
if (!Number.isFinite(parsed)) {
|
|
1073
|
+
return { present: true, error: `"${value}" is not a number` };
|
|
1074
|
+
}
|
|
1075
|
+
const digits = significantDigits(value);
|
|
1076
|
+
if (digits > EXACT_DIGITS || parsed === 0 && digits > 0) {
|
|
1077
|
+
return { present: true, value };
|
|
1078
|
+
}
|
|
1079
|
+
return { present: true, value: parsed };
|
|
1080
|
+
}
|
|
1081
|
+
function significantDigits(value) {
|
|
1082
|
+
return value.replace(/^[+-]/, "").replace(/[eE].*$/, "").replace(".", "").replace(/^0+/, "").replace(/0+$/, "").length;
|
|
1083
|
+
}
|
|
1084
|
+
function coerceDate(value) {
|
|
1085
|
+
const parts = matchDateParts(value);
|
|
1086
|
+
if (!parts) {
|
|
1087
|
+
return {
|
|
1088
|
+
present: true,
|
|
1089
|
+
error: `"${value}" is not a date (use YYYY-MM-DD, DD/MM/YYYY or DD.MM.YYYY)`
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
return { present: true, value: parts };
|
|
1093
|
+
}
|
|
1094
|
+
function coerceDateTime(value, zoned) {
|
|
1095
|
+
const zoneMatch = ZONE_SUFFIX.exec(value);
|
|
1096
|
+
if (!zoned && zoneMatch) {
|
|
1097
|
+
return {
|
|
1098
|
+
present: true,
|
|
1099
|
+
error: `"${value}" carries a time zone, but this column holds a wall-clock date/time with none — drop the ${zoneMatch[1]}`
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
const zone = zoneMatch ? zoneMatch[1] : "Z";
|
|
1103
|
+
const withoutZone = zoneMatch ? value.slice(0, value.length - zoneMatch[1].length).trim() : value;
|
|
1104
|
+
const suffix = zoned ? normalizeZone(zone) : "";
|
|
1105
|
+
const timeMatch = TIME_SUFFIX.exec(withoutZone);
|
|
1106
|
+
const datePart = timeMatch ? withoutZone.slice(0, withoutZone.length - timeMatch[0].length) : withoutZone;
|
|
1107
|
+
const date = matchDateParts(datePart.trim());
|
|
1108
|
+
if (!date) {
|
|
1109
|
+
return {
|
|
1110
|
+
present: true,
|
|
1111
|
+
error: `"${value}" is not a date/time (use YYYY-MM-DDTHH:MM:SS)`
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
if (!timeMatch) {
|
|
1115
|
+
if (zoneMatch) {
|
|
1116
|
+
return {
|
|
1117
|
+
present: true,
|
|
1118
|
+
error: `"${value}" carries a time zone but no time — write the time too (YYYY-MM-DDTHH:MM:SS${zoneMatch[1]}), or drop the ${zoneMatch[1]}`
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
if (!isRealZone(zone)) {
|
|
1122
|
+
return {
|
|
1123
|
+
present: true,
|
|
1124
|
+
error: `"${value}" is not a date/time: the zone offset is out of range`
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
present: true,
|
|
1129
|
+
value: `${date}T00:00:00${suffix}`
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
const hours = pad(timeMatch[1]);
|
|
1133
|
+
const minutes = pad(timeMatch[2]);
|
|
1134
|
+
const seconds = pad(timeMatch[3] ?? "00");
|
|
1135
|
+
const fraction = timeMatch[4] ?? "";
|
|
1136
|
+
if (!isRealTime(hours, minutes, seconds) || !isRealZone(zone)) {
|
|
1137
|
+
return {
|
|
1138
|
+
present: true,
|
|
1139
|
+
error: `"${value}" is not a date/time: the time or zone offset is out of range`
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
return {
|
|
1143
|
+
present: true,
|
|
1144
|
+
value: `${date}T${hours}:${minutes}:${seconds}${fraction}${suffix}`
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function normalizeZone(zone) {
|
|
1148
|
+
return zone.toUpperCase() === "Z" ? "Z" : zone;
|
|
1149
|
+
}
|
|
1150
|
+
function isRealTime(hours, minutes, seconds) {
|
|
1151
|
+
return Number(hours) <= 23 && Number(minutes) <= 59 && Number(seconds) <= 59;
|
|
1152
|
+
}
|
|
1153
|
+
function isRealZone(zone) {
|
|
1154
|
+
if (zone.toUpperCase() === "Z")
|
|
1155
|
+
return true;
|
|
1156
|
+
const maxHours = zone.startsWith("-") ? 12 : 14;
|
|
1157
|
+
const hours = Number(zone.slice(1, 3));
|
|
1158
|
+
const minutes = Number(zone.slice(4, 6));
|
|
1159
|
+
if (minutes > 59 || hours > maxHours)
|
|
1160
|
+
return false;
|
|
1161
|
+
return hours < maxHours || minutes === 0;
|
|
1162
|
+
}
|
|
1163
|
+
function matchDateParts(value) {
|
|
1164
|
+
for (const { pattern, order } of DATE_PATTERNS) {
|
|
1165
|
+
const match = pattern.exec(value);
|
|
1166
|
+
if (!match)
|
|
1167
|
+
continue;
|
|
1168
|
+
const [yearIndex, monthIndex, dayIndex] = order;
|
|
1169
|
+
const year = match[yearIndex];
|
|
1170
|
+
const month = match[monthIndex];
|
|
1171
|
+
const day = match[dayIndex];
|
|
1172
|
+
if (!isRealDate(Number(year), Number(month), Number(day)))
|
|
1173
|
+
return;
|
|
1174
|
+
return `${year}-${pad(month)}-${pad(day)}`;
|
|
1175
|
+
}
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
function isRealDate(year, month, day) {
|
|
1179
|
+
if (year < 1 || month < 1 || month > 12 || day < 1 || day > 31) {
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
const date = new Date(0);
|
|
1183
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
1184
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/services/seed/plan-columns.ts
|
|
1188
|
+
import { createHash } from "node:crypto";
|
|
1189
|
+
function stripColumnAnnotation(header) {
|
|
1190
|
+
const trimmed = header.trim();
|
|
1191
|
+
if (!trimmed.endsWith(")"))
|
|
1192
|
+
return trimmed;
|
|
1193
|
+
const open = trimmed.lastIndexOf("(");
|
|
1194
|
+
if (open < 0 || trimmed.slice(open + 1, -1).includes(")"))
|
|
1195
|
+
return trimmed;
|
|
1196
|
+
return trimmed.slice(0, open).trim();
|
|
1197
|
+
}
|
|
1198
|
+
function mapColumns(entity, headers, warnings) {
|
|
1199
|
+
const columns = [];
|
|
1200
|
+
for (const [sourceIndex, header] of headers.entries()) {
|
|
1201
|
+
const lower = stripColumnAnnotation(header).toLowerCase();
|
|
1202
|
+
if (SYSTEM_FIELDS.has(lower))
|
|
1203
|
+
continue;
|
|
1204
|
+
const field = entity.fields.get(lower);
|
|
1205
|
+
if (!field) {
|
|
1206
|
+
warnings.push(`${entity.name}: ignoring column "${header}" — the entity has no such field.`);
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
if (field.isAutoNumber)
|
|
1210
|
+
continue;
|
|
1211
|
+
columns.push({ header, sourceIndex, field });
|
|
1212
|
+
}
|
|
1213
|
+
return columns;
|
|
1214
|
+
}
|
|
1215
|
+
function noSeedableColumns(entityName, table, columns) {
|
|
1216
|
+
if (columns.length > 0 || table.rows.length === 0)
|
|
1217
|
+
return;
|
|
1218
|
+
const ignored = table.headers.filter((header) => stripColumnAnnotation(header).toLowerCase() !== "id");
|
|
1219
|
+
return `${entityName}: no column in ${entityName}.csv matches a field of the entity` + (ignored.length > 0 ? ` (ignored: ${ignored.join(", ")})` : "") + ", so its rows were not seeded. Fix the column headers and run again.";
|
|
1220
|
+
}
|
|
1221
|
+
function seedableFields(entity) {
|
|
1222
|
+
return [...entity.fields.values()].filter((field) => !field.isSystemField && !field.isPrimaryKey && !field.isAutoNumber);
|
|
1223
|
+
}
|
|
1224
|
+
function requiredColumnsMissing(entity, columns) {
|
|
1225
|
+
const present = new Set(columns.map((column) => column.field.name.toLowerCase()));
|
|
1226
|
+
return seedableFields(entity).filter((field) => field.isRequired && !present.has(field.name.toLowerCase())).map((field) => field.name);
|
|
1227
|
+
}
|
|
1228
|
+
function optionalColumnsMissing(entity, columns) {
|
|
1229
|
+
const present = new Set(columns.map((column) => column.field.name.toLowerCase()));
|
|
1230
|
+
return seedableFields(entity).filter((field) => !field.isRequired && !present.has(field.name.toLowerCase())).map((field) => field.name);
|
|
1231
|
+
}
|
|
1232
|
+
function rowKeysFor(headers, rows) {
|
|
1233
|
+
const localIds = localIdsFor(headers, rows);
|
|
1234
|
+
const seen = new Map;
|
|
1235
|
+
return rows.map((cells, index) => {
|
|
1236
|
+
const localId = localIds[index];
|
|
1237
|
+
const base = localId ? `id:${localId}` : `h:${contentHash(headers, cells)}`;
|
|
1238
|
+
const occurrence = (seen.get(base) ?? 0) + 1;
|
|
1239
|
+
seen.set(base, occurrence);
|
|
1240
|
+
return occurrence === 1 ? base : `${base}#${occurrence}`;
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
function localIdsFor(headers, rows) {
|
|
1244
|
+
const idIndex = headers.findIndex((header) => stripColumnAnnotation(header).toLowerCase() === "id");
|
|
1245
|
+
return rows.map((cells) => idIndex >= 0 ? (cells[idIndex] ?? "").trim() : "");
|
|
1246
|
+
}
|
|
1247
|
+
function contentHash(headers, cells) {
|
|
1248
|
+
const canonical = JSON.stringify(headers.map((header, index) => [header, cells[index] ?? ""]).sort(([a], [b]) => a.localeCompare(b)));
|
|
1249
|
+
return createHash("sha256").update(canonical, "utf8").digest("hex").slice(0, 16);
|
|
1250
|
+
}
|
|
1251
|
+
function rowLabel(key, line) {
|
|
1252
|
+
return key.startsWith("id:") ? key.slice(3) : `row ${line}`;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/services/seed/plan-values.ts
|
|
1256
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1257
|
+
var EMPTY = "<empty>";
|
|
1258
|
+
var LAZY_TEXT_MARKER = /^HasValue=(true|false) Length=\d+$/i;
|
|
1259
|
+
var LAZY_TEXT_TRUNCATED = /\.\.\.\[Truncated\]$/i;
|
|
1260
|
+
function lazyTextPreview(liveText) {
|
|
1261
|
+
const marker = LAZY_TEXT_MARKER.exec(liveText);
|
|
1262
|
+
if (marker)
|
|
1263
|
+
return { hasValue: marker[1].toLowerCase() === "true" };
|
|
1264
|
+
return LAZY_TEXT_TRUNCATED.test(liveText) ? { hasValue: true } : undefined;
|
|
1265
|
+
}
|
|
1266
|
+
function truncatedPreviewText(liveText) {
|
|
1267
|
+
return LAZY_TEXT_TRUNCATED.test(liveText) ? liveText.replace(LAZY_TEXT_TRUNCATED, "") : undefined;
|
|
1268
|
+
}
|
|
1269
|
+
function matchValue(value, field) {
|
|
1270
|
+
return field.isForeignKey ? referenceId(value).toLowerCase() : comparableText(value, field).toLowerCase();
|
|
1271
|
+
}
|
|
1272
|
+
function comparableText(value, field) {
|
|
1273
|
+
return roundLikeJson(normalizeForCompare(value, field), field);
|
|
1274
|
+
}
|
|
1275
|
+
var ROUNDED_BY_JSON = new Set(["BIGINT", "DECIMAL", "NUMERIC", "FLOAT"]);
|
|
1276
|
+
function roundLikeJson(text, field) {
|
|
1277
|
+
return ROUNDED_BY_JSON.has((field.sqlType ?? "").toUpperCase()) && /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(text) ? String(Number(text)) : text;
|
|
1278
|
+
}
|
|
1279
|
+
function choiceLabels(text, choices) {
|
|
1280
|
+
if (!choices || text === "")
|
|
1281
|
+
return text;
|
|
1282
|
+
return text.split(",").map((part) => part.trim()).filter((part) => part !== "").map((part) => /^\d+$/.test(part) ? choices.labelsById.get(Number(part)) ?? part : part).join(", ");
|
|
1283
|
+
}
|
|
1284
|
+
var ATTACHED_FILE = "an attached file";
|
|
1285
|
+
function expanded(value) {
|
|
1286
|
+
const parsed = ExpandedValueSchema.safeParse(value);
|
|
1287
|
+
return parsed.success ? parsed.data : undefined;
|
|
1288
|
+
}
|
|
1289
|
+
function attachmentName(value) {
|
|
1290
|
+
if (value === null || typeof value !== "object") {
|
|
1291
|
+
return typeof value === "string" && !UUID.test(value.trim()) ? value.trim() : "";
|
|
1292
|
+
}
|
|
1293
|
+
return expanded(value)?.Name ?? "";
|
|
1294
|
+
}
|
|
1295
|
+
function hasAttachment(value) {
|
|
1296
|
+
if (value === null || value === undefined)
|
|
1297
|
+
return false;
|
|
1298
|
+
if (typeof value === "string")
|
|
1299
|
+
return value.trim() !== "";
|
|
1300
|
+
const file = expanded(value);
|
|
1301
|
+
return file?.Id !== undefined || file?.Name !== undefined;
|
|
1302
|
+
}
|
|
1303
|
+
function attachmentSize(value) {
|
|
1304
|
+
return expanded(value)?.Size;
|
|
1305
|
+
}
|
|
1306
|
+
function referenceId(value) {
|
|
1307
|
+
if (typeof value === "string")
|
|
1308
|
+
return value.trim();
|
|
1309
|
+
return expanded(value)?.Id ?? "";
|
|
1310
|
+
}
|
|
1311
|
+
function normalizeForCompare(value, field) {
|
|
1312
|
+
if (value === null || value === undefined)
|
|
1313
|
+
return "";
|
|
1314
|
+
if (typeof value === "boolean")
|
|
1315
|
+
return value ? "true" : "false";
|
|
1316
|
+
if (typeof value === "number")
|
|
1317
|
+
return String(value);
|
|
1318
|
+
if (Array.isArray(value)) {
|
|
1319
|
+
const items = value.map((item) => normalizeForCompare(item, field));
|
|
1320
|
+
return (field.isMultiChoice ? [...items].sort((left, right) => left.localeCompare(right)) : items).join(",");
|
|
1321
|
+
}
|
|
1322
|
+
if (typeof value === "object")
|
|
1323
|
+
return normalizeObject(value);
|
|
1324
|
+
if (typeof value !== "string")
|
|
1325
|
+
return "";
|
|
1326
|
+
return normalizeText(value, field);
|
|
1327
|
+
}
|
|
1328
|
+
function normalizeObject(value) {
|
|
1329
|
+
const record = expanded(value);
|
|
1330
|
+
if (record?.Id !== undefined)
|
|
1331
|
+
return record.Id;
|
|
1332
|
+
if (record?.Name !== undefined)
|
|
1333
|
+
return record.Name;
|
|
1334
|
+
return JSON.stringify(value);
|
|
1335
|
+
}
|
|
1336
|
+
function normalizeText(raw, field) {
|
|
1337
|
+
const sqlType = (field.sqlType ?? "").toUpperCase();
|
|
1338
|
+
const text = raw.trim();
|
|
1339
|
+
if (sqlType === "BIT")
|
|
1340
|
+
return normalizeBit(text);
|
|
1341
|
+
if (field.isMultiChoice) {
|
|
1342
|
+
return text.split(",").map((part) => part.trim()).filter((part) => part !== "").sort((left, right) => left.localeCompare(right)).join(",");
|
|
1343
|
+
}
|
|
1344
|
+
if (sqlType === "DATE") {
|
|
1345
|
+
return text.length >= 10 && /^\d{4}-\d{2}-\d{2}/.test(text) ? text.slice(0, 10) : text;
|
|
1346
|
+
}
|
|
1347
|
+
if (sqlType.startsWith("DATETIME"))
|
|
1348
|
+
return normalizeDateTime(text);
|
|
1349
|
+
return raw;
|
|
1350
|
+
}
|
|
1351
|
+
function normalizeBit(text) {
|
|
1352
|
+
const lower = text.toLowerCase();
|
|
1353
|
+
if (["1", "yes", "y", "true"].includes(lower))
|
|
1354
|
+
return "true";
|
|
1355
|
+
if (["0", "no", "n", "false"].includes(lower))
|
|
1356
|
+
return "false";
|
|
1357
|
+
return lower;
|
|
1358
|
+
}
|
|
1359
|
+
function normalizeDateTime(text) {
|
|
1360
|
+
const withZone = /(Z|[+-]\d{2}:\d{2})$/i.test(text) ? text : `${text}Z`;
|
|
1361
|
+
const parsed = Date.parse(withZone);
|
|
1362
|
+
if (Number.isFinite(parsed)) {
|
|
1363
|
+
return new Date(parsed).toISOString().replace(/\.000Z$/, "Z");
|
|
1364
|
+
}
|
|
1365
|
+
return text;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/services/seed/plan-cells.ts
|
|
1369
|
+
function buildRow(identity, cells, columns, input, errors, unresolved, ownLocalIds) {
|
|
1370
|
+
const parts = {
|
|
1371
|
+
values: fieldMap(),
|
|
1372
|
+
display: fieldMap(),
|
|
1373
|
+
attachments: fieldMap(),
|
|
1374
|
+
foreignKeys: fieldMap(),
|
|
1375
|
+
uuidReferences: fieldMap(),
|
|
1376
|
+
ownLocalIds
|
|
1377
|
+
};
|
|
1378
|
+
const { values, display, attachments, foreignKeys } = parts;
|
|
1379
|
+
const prefix = `${input.entityName} row ${identity.line}`;
|
|
1380
|
+
let rowFailed = false;
|
|
1381
|
+
for (const column of columns) {
|
|
1382
|
+
const error = readColumn(column, cells, input, parts, unresolved);
|
|
1383
|
+
if (error !== undefined) {
|
|
1384
|
+
errors.push(`${prefix}, column "${column.header}": ${error}`);
|
|
1385
|
+
rowFailed = true;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (rowFailed)
|
|
1389
|
+
return;
|
|
1390
|
+
const missing = missingRequiredValues(input.entity, columns, values, attachments, foreignKeys);
|
|
1391
|
+
if (missing.length > 0) {
|
|
1392
|
+
errors.push(`${prefix}: required ${missing.length === 1 ? "field" : "fields"} left empty: ${missing.join(", ")}.`);
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
const { uuidReferences } = parts;
|
|
1396
|
+
return {
|
|
1397
|
+
...identity,
|
|
1398
|
+
values,
|
|
1399
|
+
display,
|
|
1400
|
+
attachments,
|
|
1401
|
+
foreignKeys,
|
|
1402
|
+
...Object.keys(uuidReferences).length > 0 ? { uuidReferences } : {}
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
function readColumn(column, cells, input, parts, unresolved) {
|
|
1406
|
+
const raw = cells[column.sourceIndex] ?? "";
|
|
1407
|
+
const { field } = column;
|
|
1408
|
+
const resolved = resolveExpressions(raw, input.expressionContext);
|
|
1409
|
+
let value = resolved.value;
|
|
1410
|
+
if (resolved.errors.length > 0) {
|
|
1411
|
+
for (const expression of resolved.errors)
|
|
1412
|
+
unresolved.add(expression);
|
|
1413
|
+
value = "";
|
|
1414
|
+
}
|
|
1415
|
+
const text = value.trim();
|
|
1416
|
+
parts.display[field.name] = displayText(text);
|
|
1417
|
+
if (field.isAttachment) {
|
|
1418
|
+
return readAttachmentCell(text, field, input, parts.attachments) || undefined;
|
|
1419
|
+
}
|
|
1420
|
+
if (field.isForeignKey) {
|
|
1421
|
+
return readForeignKeyCell(text, field, input, parts);
|
|
1422
|
+
}
|
|
1423
|
+
const cell = readCell(value, field.sqlType);
|
|
1424
|
+
if (!cell.present)
|
|
1425
|
+
return;
|
|
1426
|
+
if (cell.error)
|
|
1427
|
+
return `${cell.error}.`;
|
|
1428
|
+
if (typeof cell.value === "string" && isDateType(field.sqlType)) {
|
|
1429
|
+
parts.display[field.name] = cell.value;
|
|
1430
|
+
}
|
|
1431
|
+
const choice = mapChoiceValue(field, cell.value, input);
|
|
1432
|
+
if (choice.error)
|
|
1433
|
+
return choice.error;
|
|
1434
|
+
parts.values[field.name] = choice.value;
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
function isDateType(sqlType) {
|
|
1438
|
+
return (sqlType ?? "").toUpperCase().startsWith("DATE");
|
|
1439
|
+
}
|
|
1440
|
+
function displayText(text) {
|
|
1441
|
+
if (text === "")
|
|
1442
|
+
return EMPTY;
|
|
1443
|
+
if (text.toUpperCase() === "NULL")
|
|
1444
|
+
return "<null>";
|
|
1445
|
+
if (text === String.raw`\NULL`)
|
|
1446
|
+
return "NULL";
|
|
1447
|
+
return text;
|
|
1448
|
+
}
|
|
1449
|
+
function readAttachmentCell(text, field, input, attachments) {
|
|
1450
|
+
if (text === "" || text.toUpperCase() === "NULL")
|
|
1451
|
+
return;
|
|
1452
|
+
if (text.includes("/") || text.includes("\\")) {
|
|
1453
|
+
return `an attachment cell must be a filename, not a path: "${text}". Put the file under ${input.entityName}/ in the seed workspace.`;
|
|
1454
|
+
}
|
|
1455
|
+
const path = `${input.entityName}/${text}`;
|
|
1456
|
+
if (input.bundledFiles && !input.bundledFiles.has(path)) {
|
|
1457
|
+
return `file not found for "${field.name}": ${path}.`;
|
|
1458
|
+
}
|
|
1459
|
+
attachments[field.name] = path;
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
function readForeignKeyCell(text, field, input, parts) {
|
|
1463
|
+
if (text === "" || text.toUpperCase() === "NULL")
|
|
1464
|
+
return;
|
|
1465
|
+
const entity = (field.referenceEntityName ?? "").toLowerCase();
|
|
1466
|
+
const reference = { entity, localId: text };
|
|
1467
|
+
if (entity === input.entity.name.toLowerCase()) {
|
|
1468
|
+
if (UUID.test(text) && !parts.ownLocalIds.has(text)) {
|
|
1469
|
+
parts.values[field.name] = { Id: text };
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
const resolvedBefore = input.idMaps?.get(entity)?.get(text);
|
|
1473
|
+
if (!parts.ownLocalIds.has(text) && resolvedBefore === undefined && field.isRequired) {
|
|
1474
|
+
return `"${text}" does not match the Id of any row in ` + `${input.entityName}.csv, and ${field.name} is required. ` + "Add that row, or put the record id of an existing " + `${input.entityName} in the cell.`;
|
|
1475
|
+
}
|
|
1476
|
+
parts.foreignKeys[field.name] = reference;
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
const known = input.idMaps?.get(entity)?.get(text);
|
|
1480
|
+
if (known) {
|
|
1481
|
+
parts.values[field.name] = { Id: known };
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
if (UUID.test(text)) {
|
|
1485
|
+
parts.values[field.name] = { Id: text };
|
|
1486
|
+
parts.uuidReferences[field.name] = reference;
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
parts.foreignKeys[field.name] = reference;
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
function mapChoiceValue(field, value, input) {
|
|
1493
|
+
if (value === undefined || value === null)
|
|
1494
|
+
return { value: value ?? null };
|
|
1495
|
+
if (!field.choiceSetId || typeof value !== "string")
|
|
1496
|
+
return { value };
|
|
1497
|
+
const choices = input.choiceValues?.get(field.name.toLowerCase());
|
|
1498
|
+
const parts = field.isMultiChoice ? value.split(",").map((part) => part.trim()).filter((part) => part !== "") : [value.trim()];
|
|
1499
|
+
const mapped = [];
|
|
1500
|
+
for (const part of parts) {
|
|
1501
|
+
const id = choices?.byLabel.get(part.toLowerCase());
|
|
1502
|
+
if (id !== undefined) {
|
|
1503
|
+
mapped.push(String(id));
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
if (/^\d+$/.test(part)) {
|
|
1507
|
+
mapped.push(part);
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
if (choices) {
|
|
1511
|
+
return {
|
|
1512
|
+
value,
|
|
1513
|
+
error: `"${part}" is not a valid choice for "${field.name}". Valid: ${choices.labels.join(", ")}.`
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
mapped.push(part);
|
|
1517
|
+
}
|
|
1518
|
+
if (field.isMultiChoice)
|
|
1519
|
+
return { value: mapped.join(",") };
|
|
1520
|
+
const only = mapped[0];
|
|
1521
|
+
return { value: /^\d+$/.test(only) ? Number(only) : only };
|
|
1522
|
+
}
|
|
1523
|
+
function missingRequiredValues(entity, columns, values, attachments, foreignKeys) {
|
|
1524
|
+
const declared = new Set(columns.map((column) => column.field.name));
|
|
1525
|
+
return seedableFields(entity).filter((field) => field.isRequired && declared.has(field.name)).filter((field) => {
|
|
1526
|
+
if (field.isAttachment) {
|
|
1527
|
+
return attachments[field.name] === undefined;
|
|
1528
|
+
}
|
|
1529
|
+
if (field.isForeignKey) {
|
|
1530
|
+
return foreignKeys[field.name] === undefined && values[field.name] === undefined;
|
|
1531
|
+
}
|
|
1532
|
+
return values[field.name] === undefined || values[field.name] === null;
|
|
1533
|
+
}).map((field) => field.name);
|
|
1534
|
+
}
|
|
1535
|
+
function relationshipKey(row, field) {
|
|
1536
|
+
const reference = row.foreignKeys[field.name];
|
|
1537
|
+
if (reference) {
|
|
1538
|
+
return `${reference.entity}\x00${reference.localId.toLowerCase()}`;
|
|
1539
|
+
}
|
|
1540
|
+
return matchValue(row.values[field.name], field);
|
|
1541
|
+
}
|
|
1542
|
+
function uniqueValuesFree(row, columns, seen, input, errors) {
|
|
1543
|
+
for (const { field } of columns) {
|
|
1544
|
+
if (!field.isUnique || field.isAttachment)
|
|
1545
|
+
continue;
|
|
1546
|
+
const value = field.isForeignKey ? relationshipKey(row, field) : normalizeForCompare(row.values[field.name], field).toLowerCase();
|
|
1547
|
+
if (value === "")
|
|
1548
|
+
continue;
|
|
1549
|
+
const tracker = seen.get(field.name) ?? new Map;
|
|
1550
|
+
seen.set(field.name, tracker);
|
|
1551
|
+
const first = tracker.get(value);
|
|
1552
|
+
if (first !== undefined) {
|
|
1553
|
+
errors.push(`${input.entityName} row ${row.line}: duplicate value "${row.display[field.name] ?? value}" for unique field "${field.name}" (first seen in row ${first}).`);
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
tracker.set(value, row.line);
|
|
1557
|
+
}
|
|
1558
|
+
return true;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// src/services/seed/plan-compare.ts
|
|
1562
|
+
function compareWithLive(row, recordId, live, columns, input, pendingFiles) {
|
|
1563
|
+
const changes = fieldMap();
|
|
1564
|
+
const optional = fieldMap();
|
|
1565
|
+
const attachmentChanges = fieldMap();
|
|
1566
|
+
const optionalClears = [];
|
|
1567
|
+
for (const { field } of columns) {
|
|
1568
|
+
const liveValue = ownValue(live, field.name);
|
|
1569
|
+
if (field.isAttachment) {
|
|
1570
|
+
if (pendingFiles?.includes(field.name) && row.attachments[field.name] !== undefined) {
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
compareAttachment({
|
|
1574
|
+
row,
|
|
1575
|
+
field,
|
|
1576
|
+
liveValue,
|
|
1577
|
+
input,
|
|
1578
|
+
changes,
|
|
1579
|
+
optional,
|
|
1580
|
+
attachmentChanges,
|
|
1581
|
+
optionalClears
|
|
1582
|
+
});
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
const scope = { row, field, liveValue, input, changes, optional };
|
|
1586
|
+
if (field.isForeignKey) {
|
|
1587
|
+
compareForeignKey(scope);
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
compareScalar(scope);
|
|
1591
|
+
}
|
|
1592
|
+
return {
|
|
1593
|
+
...row,
|
|
1594
|
+
recordId,
|
|
1595
|
+
changes,
|
|
1596
|
+
optional,
|
|
1597
|
+
attachmentChanges,
|
|
1598
|
+
optionalClears,
|
|
1599
|
+
allAttachments: row.attachments,
|
|
1600
|
+
...pendingFiles && pendingFiles.length > 0 ? { pendingFiles } : {}
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
function compareForeignKey({
|
|
1604
|
+
row,
|
|
1605
|
+
field,
|
|
1606
|
+
liveValue,
|
|
1607
|
+
input,
|
|
1608
|
+
changes,
|
|
1609
|
+
optional
|
|
1610
|
+
}) {
|
|
1611
|
+
const liveId = referenceId(liveValue);
|
|
1612
|
+
const ref = row.foreignKeys[field.name];
|
|
1613
|
+
if (ref !== undefined) {
|
|
1614
|
+
if (ref.entity !== input.entity.name.toLowerCase()) {
|
|
1615
|
+
changes[field.name] = {
|
|
1616
|
+
from: liveId || EMPTY,
|
|
1617
|
+
to: `${ref.localId} (local Id)`
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
const wanted = referenceId(row.values[field.name]);
|
|
1623
|
+
if (liveId.toLowerCase() === wanted.toLowerCase())
|
|
1624
|
+
return;
|
|
1625
|
+
const change = { from: liveId || EMPTY, to: wanted || EMPTY };
|
|
1626
|
+
if (wanted === "" && !field.isRequired) {
|
|
1627
|
+
optional[field.name] = change;
|
|
1628
|
+
} else {
|
|
1629
|
+
changes[field.name] = change;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
function compareScalar({
|
|
1633
|
+
row,
|
|
1634
|
+
field,
|
|
1635
|
+
liveValue,
|
|
1636
|
+
input,
|
|
1637
|
+
changes,
|
|
1638
|
+
optional
|
|
1639
|
+
}) {
|
|
1640
|
+
const liveText = normalizeForCompare(liveValue, field);
|
|
1641
|
+
const hasCell = Object.hasOwn(row.values, field.name);
|
|
1642
|
+
const wantedText = hasCell ? normalizeForCompare(row.values[field.name], field) : "";
|
|
1643
|
+
const preview = field.isLazyText ? lazyTextPreview(liveText) : undefined;
|
|
1644
|
+
if (preview) {
|
|
1645
|
+
compareLazyText({
|
|
1646
|
+
scope: { row, field, liveValue, input, changes, optional },
|
|
1647
|
+
liveText,
|
|
1648
|
+
liveHasValue: preview.hasValue,
|
|
1649
|
+
hasCell,
|
|
1650
|
+
wantedText
|
|
1651
|
+
});
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
if (roundLikeJson(liveText, field).toLowerCase() === roundLikeJson(wantedText, field).toLowerCase()) {
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
const choices = field.choiceSetId ? input.choiceValues?.get(field.name.toLowerCase()) : undefined;
|
|
1658
|
+
const change = {
|
|
1659
|
+
from: choiceLabels(liveText, choices) || EMPTY,
|
|
1660
|
+
to: choiceLabels(wantedText, choices) || EMPTY
|
|
1661
|
+
};
|
|
1662
|
+
if (!hasCell && !field.isRequired) {
|
|
1663
|
+
optional[field.name] = change;
|
|
1664
|
+
} else {
|
|
1665
|
+
changes[field.name] = change;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function compareLazyText(input) {
|
|
1669
|
+
const { scope, liveText, liveHasValue, hasCell, wantedText } = input;
|
|
1670
|
+
const { field, changes, optional } = scope;
|
|
1671
|
+
if (!liveHasValue) {
|
|
1672
|
+
if (wantedText !== "") {
|
|
1673
|
+
changes[field.name] = { from: EMPTY, to: wantedText };
|
|
1674
|
+
}
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (wantedText !== "") {
|
|
1678
|
+
changes[field.name] = { from: liveText, to: wantedText };
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
const change = { from: liveText, to: EMPTY };
|
|
1682
|
+
if (!hasCell && !field.isRequired) {
|
|
1683
|
+
optional[field.name] = change;
|
|
1684
|
+
} else {
|
|
1685
|
+
changes[field.name] = change;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
function withSelfReferenceChanges(row, input, idMap) {
|
|
1689
|
+
const own = input.entity.name.toLowerCase();
|
|
1690
|
+
const live = input.liveRecords.find((record) => typeof record.Id === "string" && record.Id.toLowerCase() === row.recordId.toLowerCase());
|
|
1691
|
+
const changes = fieldMap(row.changes);
|
|
1692
|
+
let changed = false;
|
|
1693
|
+
for (const [field, ref] of Object.entries(row.foreignKeys)) {
|
|
1694
|
+
if (ref.entity !== own)
|
|
1695
|
+
continue;
|
|
1696
|
+
const liveId = referenceId(live === undefined ? undefined : ownValue(live, field));
|
|
1697
|
+
const wanted = idMap.get(ref.localId);
|
|
1698
|
+
if (wanted?.toLowerCase() === liveId.toLowerCase()) {
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
changes[field] = {
|
|
1702
|
+
from: liveId || EMPTY,
|
|
1703
|
+
to: wanted ?? `${ref.localId} (local Id)`
|
|
1704
|
+
};
|
|
1705
|
+
changed = true;
|
|
1706
|
+
}
|
|
1707
|
+
return changed ? { ...row, changes } : row;
|
|
1708
|
+
}
|
|
1709
|
+
function compareAttachment({
|
|
1710
|
+
row,
|
|
1711
|
+
field,
|
|
1712
|
+
liveValue,
|
|
1713
|
+
input,
|
|
1714
|
+
changes,
|
|
1715
|
+
optional,
|
|
1716
|
+
attachmentChanges,
|
|
1717
|
+
optionalClears
|
|
1718
|
+
}) {
|
|
1719
|
+
const liveName = attachmentName(liveValue);
|
|
1720
|
+
const livePresent = hasAttachment(liveValue);
|
|
1721
|
+
const path = row.attachments[field.name];
|
|
1722
|
+
if (path === undefined) {
|
|
1723
|
+
if (livePresent) {
|
|
1724
|
+
optionalClears.push(field.name);
|
|
1725
|
+
optional[field.name] = {
|
|
1726
|
+
from: liveName || ATTACHED_FILE,
|
|
1727
|
+
to: EMPTY
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
const wantedName = path.slice(path.lastIndexOf("/") + 1);
|
|
1733
|
+
if (livePresent && !liveName) {
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
if (liveName.toLowerCase() !== wantedName.toLowerCase()) {
|
|
1737
|
+
changes[field.name] = { from: liveName || EMPTY, to: wantedName };
|
|
1738
|
+
attachmentChanges[field.name] = path;
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
const liveSize = attachmentSize(liveValue);
|
|
1742
|
+
const bundledSize = input.bundledFiles?.get(path);
|
|
1743
|
+
if (liveSize !== undefined && bundledSize !== undefined && liveSize !== bundledSize) {
|
|
1744
|
+
changes[field.name] = {
|
|
1745
|
+
from: `${liveName} (${liveSize} B)`,
|
|
1746
|
+
to: `${wantedName} (${bundledSize} B)`
|
|
1747
|
+
};
|
|
1748
|
+
attachmentChanges[field.name] = path;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// src/services/seed/plan-live.ts
|
|
1753
|
+
function recordsNewSincePlan(records, planned) {
|
|
1754
|
+
const seen = new Set(planned.map((record) => record.Id).filter((id) => typeof id === "string").map((id) => id.toLowerCase()));
|
|
1755
|
+
return records.filter((record) => typeof record.Id !== "string" || !seen.has(record.Id.toLowerCase()));
|
|
1756
|
+
}
|
|
1757
|
+
function indexLive(records, entity, columns, options = {}) {
|
|
1758
|
+
const claimed = new Set;
|
|
1759
|
+
const claim = (row, recordId) => {
|
|
1760
|
+
if (recordId === undefined || options.claimOnce === false) {
|
|
1761
|
+
return recordId;
|
|
1762
|
+
}
|
|
1763
|
+
const key = recordId.toLowerCase();
|
|
1764
|
+
if (claimed.has(key)) {
|
|
1765
|
+
options.onDoubleClaim?.(row, recordId);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
claimed.add(key);
|
|
1769
|
+
return recordId;
|
|
1770
|
+
};
|
|
1771
|
+
const byId = new Map;
|
|
1772
|
+
for (const record of records) {
|
|
1773
|
+
if (typeof record.Id === "string") {
|
|
1774
|
+
byId.set(record.Id.toLowerCase(), record);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
const identity = entity.identityFields.map((name) => entity.fields.get(name.toLowerCase())).filter((field) => field !== undefined);
|
|
1778
|
+
const filledOf = contentFields(columns);
|
|
1779
|
+
const linksOf = linkFields(columns);
|
|
1780
|
+
if (identity.length > 0) {
|
|
1781
|
+
const indexes = new Map;
|
|
1782
|
+
const indexFor = (used) => {
|
|
1783
|
+
const signature = used.map((field) => field.name).join("\x00");
|
|
1784
|
+
const cached = indexes.get(signature);
|
|
1785
|
+
if (cached)
|
|
1786
|
+
return cached;
|
|
1787
|
+
const built = keyIndex(records, used);
|
|
1788
|
+
indexes.set(signature, built);
|
|
1789
|
+
return built;
|
|
1790
|
+
};
|
|
1791
|
+
const lookup2 = (row) => {
|
|
1792
|
+
const used = identity.filter((field) => {
|
|
1793
|
+
if (unresolvedRelationship(row, field))
|
|
1794
|
+
return false;
|
|
1795
|
+
if (field.isRequired)
|
|
1796
|
+
return true;
|
|
1797
|
+
if (!Object.hasOwn(row.values, field.name))
|
|
1798
|
+
return false;
|
|
1799
|
+
return row.values[field.name] === null || normalizeForCompare(row.values[field.name], field) !== "";
|
|
1800
|
+
});
|
|
1801
|
+
const key = used.length === 0 ? undefined : identityKey(used.map((field) => [field, row.values[field.name]]));
|
|
1802
|
+
if (key === undefined) {
|
|
1803
|
+
return options.contentFallback ? {
|
|
1804
|
+
recordId: soleContentMatch(records, row, [
|
|
1805
|
+
...filledOf(row),
|
|
1806
|
+
...identity
|
|
1807
|
+
])
|
|
1808
|
+
} : {};
|
|
1809
|
+
}
|
|
1810
|
+
const { byKey, counts } = indexFor(used);
|
|
1811
|
+
const hits = counts.get(key) ?? 0;
|
|
1812
|
+
const ambiguous = ambiguityOf(row, used, hits);
|
|
1813
|
+
if (ambiguous)
|
|
1814
|
+
return { ambiguous };
|
|
1815
|
+
const recordId = byKey.get(key);
|
|
1816
|
+
return hits > 1 ? { recordId, shared: hits } : { recordId };
|
|
1817
|
+
};
|
|
1818
|
+
return liveIndex(byId, lookup2, claim);
|
|
1819
|
+
}
|
|
1820
|
+
const lookup = (row) => {
|
|
1821
|
+
const filled = filledOf(row);
|
|
1822
|
+
if (filled.length === 0)
|
|
1823
|
+
return {};
|
|
1824
|
+
const scalarHits = records.filter((record) => sameContent(record, row, filled));
|
|
1825
|
+
const hits = narrowByLinks(scalarHits, row, linksOf(row));
|
|
1826
|
+
const ambiguous = ambiguityOf(row, filled, hits.length);
|
|
1827
|
+
if (ambiguous)
|
|
1828
|
+
return { ambiguous };
|
|
1829
|
+
const hit = hits[0];
|
|
1830
|
+
if (typeof hit?.Id !== "string")
|
|
1831
|
+
return {};
|
|
1832
|
+
return hits.length > 1 ? { recordId: hit.Id, shared: hits.length } : { recordId: hit.Id };
|
|
1833
|
+
};
|
|
1834
|
+
return liveIndex(byId, lookup, claim);
|
|
1835
|
+
}
|
|
1836
|
+
function liveIndex(byId, lookup, claim) {
|
|
1837
|
+
return {
|
|
1838
|
+
byId,
|
|
1839
|
+
match: (row) => claim(row, lookup(row).recordId),
|
|
1840
|
+
ambiguous: (row) => lookup(row).ambiguous,
|
|
1841
|
+
shared: (row) => lookup(row).shared
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
function keyIndex(records, used) {
|
|
1845
|
+
const byKey = new Map;
|
|
1846
|
+
const counts = new Map;
|
|
1847
|
+
for (const record of records) {
|
|
1848
|
+
if (typeof record.Id !== "string")
|
|
1849
|
+
continue;
|
|
1850
|
+
const key = identityKey(used.map((field) => [field, ownValue(record, field.name)]));
|
|
1851
|
+
if (key === undefined)
|
|
1852
|
+
continue;
|
|
1853
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1854
|
+
if (!byKey.has(key))
|
|
1855
|
+
byKey.set(key, record.Id);
|
|
1856
|
+
}
|
|
1857
|
+
return { byKey, counts };
|
|
1858
|
+
}
|
|
1859
|
+
function ambiguityOf(row, fields, records) {
|
|
1860
|
+
if (records < 2)
|
|
1861
|
+
return;
|
|
1862
|
+
const field = roundedBigintField(row, fields);
|
|
1863
|
+
return field ? { field, records } : undefined;
|
|
1864
|
+
}
|
|
1865
|
+
function roundedBigintField(row, fields) {
|
|
1866
|
+
return fields.find((field) => {
|
|
1867
|
+
if ((field.sqlType ?? "").toUpperCase() !== "BIGINT")
|
|
1868
|
+
return false;
|
|
1869
|
+
const text = normalizeForCompare(row.values[field.name], field);
|
|
1870
|
+
return /^[+-]?\d+$/.test(text) && !Number.isSafeInteger(Number(text));
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
function contentFields(columns) {
|
|
1874
|
+
const scalars = columns.map((column) => column.field).filter((field) => !field.isForeignKey && !field.isAttachment);
|
|
1875
|
+
const relationships = columns.map((column) => column.field).filter((field) => field.isForeignKey);
|
|
1876
|
+
return (row) => {
|
|
1877
|
+
const filled = scalars.filter((field) => fills(row, field));
|
|
1878
|
+
return filled.length > 0 ? filled : relationships.filter((field) => fills(row, field));
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
function linkFields(columns) {
|
|
1882
|
+
const relationships = columns.map((column) => column.field).filter((field) => field.isForeignKey);
|
|
1883
|
+
return (row) => relationships.filter((field) => fills(row, field));
|
|
1884
|
+
}
|
|
1885
|
+
function fills(row, field) {
|
|
1886
|
+
return Object.hasOwn(row.values, field.name) && !unresolvedRelationship(row, field);
|
|
1887
|
+
}
|
|
1888
|
+
function narrowByLinks(hits, row, links) {
|
|
1889
|
+
if (hits.length < 2 || links.length === 0)
|
|
1890
|
+
return hits;
|
|
1891
|
+
const narrowed = hits.filter((record) => sameContent(record, row, links));
|
|
1892
|
+
return narrowed.length > 0 ? narrowed : hits;
|
|
1893
|
+
}
|
|
1894
|
+
function sameContent(record, row, fields) {
|
|
1895
|
+
const usePreviews = fields.some((field) => !field.isLazyText && normalizeForCompare(row.values[field.name], field) !== "");
|
|
1896
|
+
return fields.every((field) => sameValue(ownValue(record, field.name), row.values[field.name], field, usePreviews));
|
|
1897
|
+
}
|
|
1898
|
+
function sameValue(live, wanted, field, usePreviews) {
|
|
1899
|
+
const liveText = normalizeForCompare(live, field);
|
|
1900
|
+
const preview = usePreviews && field.isLazyText ? lazyTextPreview(liveText) : undefined;
|
|
1901
|
+
if (!preview)
|
|
1902
|
+
return matchValue(live, field) === matchValue(wanted, field);
|
|
1903
|
+
const wantedText = normalizeForCompare(wanted, field);
|
|
1904
|
+
if (!preview.hasValue)
|
|
1905
|
+
return wantedText === "";
|
|
1906
|
+
const shown = truncatedPreviewText(liveText);
|
|
1907
|
+
return shown === undefined ? wantedText !== "" : wantedText.toLowerCase().startsWith(shown.toLowerCase());
|
|
1908
|
+
}
|
|
1909
|
+
function soleContentMatch(records, row, fields) {
|
|
1910
|
+
if (!fields.some((field) => Object.hasOwn(row.values, field.name))) {
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
const hits = records.flatMap((record) => typeof record.Id === "string" && sameContent(record, row, fields) ? [record.Id] : []);
|
|
1914
|
+
return hits.length === 1 ? hits[0] : undefined;
|
|
1915
|
+
}
|
|
1916
|
+
function unresolvedRelationship(row, field) {
|
|
1917
|
+
return field.isForeignKey && Object.hasOwn(row.foreignKeys, field.name);
|
|
1918
|
+
}
|
|
1919
|
+
function locateRows(rows, records, entity, columns) {
|
|
1920
|
+
const live = indexLive(records, entity, columns, {
|
|
1921
|
+
claimOnce: false,
|
|
1922
|
+
contentFallback: true
|
|
1923
|
+
});
|
|
1924
|
+
const ids = rows.map((row) => live.match(row));
|
|
1925
|
+
const claims = new Map;
|
|
1926
|
+
for (const id of ids) {
|
|
1927
|
+
if (id !== undefined)
|
|
1928
|
+
claims.set(id, (claims.get(id) ?? 0) + 1);
|
|
1929
|
+
}
|
|
1930
|
+
return ids.map((id) => id !== undefined && claims.get(id) === 1 ? id : undefined);
|
|
1931
|
+
}
|
|
1932
|
+
function identityKey(parts) {
|
|
1933
|
+
const normalized = parts.map(([field, value]) => [field.name, matchValue(value, field)]);
|
|
1934
|
+
if (parts.every(([field, value]) => normalizeForCompare(value, field) === "")) {
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
return JSON.stringify(normalized);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/services/seed/plan.ts
|
|
1941
|
+
function readStrategy(raw) {
|
|
1942
|
+
if (raw === undefined)
|
|
1943
|
+
return { strategy: "insert" };
|
|
1944
|
+
const lower = raw.toLowerCase();
|
|
1945
|
+
if (lower === "insert" || lower === "upsert") {
|
|
1946
|
+
return { strategy: lower };
|
|
1947
|
+
}
|
|
1948
|
+
return {
|
|
1949
|
+
strategy: "upsert",
|
|
1950
|
+
warning: `Unknown seeding strategy "${raw}" — treating it as upsert.`
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
function buildEntityPlan(input) {
|
|
1954
|
+
const errors = [];
|
|
1955
|
+
const warnings = [];
|
|
1956
|
+
const [parseError, table] = catchError(() => parseCsv(input.csv));
|
|
1957
|
+
if (parseError || !table) {
|
|
1958
|
+
errors.push(`${input.entityName}: ${input.entityName}.csv could not be read — ${parseError?.message ?? "unknown error"}.`);
|
|
1959
|
+
return emptyPlan(input, errors, warnings, 0);
|
|
1960
|
+
}
|
|
1961
|
+
if (table.headers.length === 0) {
|
|
1962
|
+
return emptyPlan(input, errors, warnings, 0);
|
|
1963
|
+
}
|
|
1964
|
+
const columns = mapColumns(input.entity, table.headers, warnings);
|
|
1965
|
+
const noColumns = noSeedableColumns(input.entityName, table, columns);
|
|
1966
|
+
if (noColumns !== undefined) {
|
|
1967
|
+
errors.push(noColumns);
|
|
1968
|
+
return emptyPlan(input, errors, warnings, table.rows.length);
|
|
1969
|
+
}
|
|
1970
|
+
const missingRequired = requiredColumnsMissing(input.entity, columns);
|
|
1971
|
+
if (missingRequired.length > 0) {
|
|
1972
|
+
errors.push(`${input.entityName}: the CSV is missing required ${missingRequired.length === 1 ? "column" : "columns"}: ${missingRequired.join(", ")}.`);
|
|
1973
|
+
return emptyPlan(input, errors, warnings, table.rows.length);
|
|
1974
|
+
}
|
|
1975
|
+
const missingOptional = optionalColumnsMissing(input.entity, columns);
|
|
1976
|
+
if (missingOptional.length > 0) {
|
|
1977
|
+
warnings.push(`${input.entityName}: optional columns not in the CSV (Data Fabric defaults apply): ${missingOptional.join(", ")}.`);
|
|
1978
|
+
}
|
|
1979
|
+
const entityJournal = journalEntity(input.journal, input.entityName);
|
|
1980
|
+
const journalKeys = rowKeysFor(table.headers, table.rows);
|
|
1981
|
+
const localIds = localIdsFor(table.headers, table.rows);
|
|
1982
|
+
const live = indexLive(input.liveRecords, input.entity, columns, {
|
|
1983
|
+
onDoubleClaim: (row, recordId) => {
|
|
1984
|
+
warnings.push(`${input.entityName} ${row.label}: another row already matches ` + `record ${recordId}, so this one is planned as a new record. ` + "Give the entity an identity field, or make the rows tell " + "themselves apart, if that is not what you want.");
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
const state = {
|
|
1988
|
+
input,
|
|
1989
|
+
columns,
|
|
1990
|
+
live,
|
|
1991
|
+
entityJournal,
|
|
1992
|
+
errors,
|
|
1993
|
+
warnings,
|
|
1994
|
+
uniqueSeen: new Map,
|
|
1995
|
+
firstLineOfLocalId: new Map,
|
|
1996
|
+
idMap: new Map,
|
|
1997
|
+
ownLocalIds: new Set(localIds.filter((localId) => localId !== "")),
|
|
1998
|
+
unresolved: new Set,
|
|
1999
|
+
journalChanged: false,
|
|
2000
|
+
toInsert: [],
|
|
2001
|
+
matched: []
|
|
2002
|
+
};
|
|
2003
|
+
for (const [index, cells] of table.rows.entries()) {
|
|
2004
|
+
planRow(state, cells, index, journalKeys[index], localIds[index]);
|
|
2005
|
+
}
|
|
2006
|
+
const { idMap, unresolved, journalChanged, toInsert, matched } = state;
|
|
2007
|
+
for (const [index, row] of matched.entries()) {
|
|
2008
|
+
matched[index] = withSelfReferenceChanges(row, input, idMap);
|
|
2009
|
+
}
|
|
2010
|
+
if (journalChanged) {
|
|
2011
|
+
putOwn(input.journal, input.entityName, entityJournal);
|
|
2012
|
+
}
|
|
2013
|
+
if (unresolved.size > 0) {
|
|
2014
|
+
warnings.push(`${input.entityName}: unresolved expressions (replaced with empty): ${[...unresolved].sort((a, b) => a.localeCompare(b)).join(", ")}. ` + describeUnresolved([...unresolved][0]));
|
|
2015
|
+
}
|
|
2016
|
+
return {
|
|
2017
|
+
entityName: input.entityName,
|
|
2018
|
+
entityId: input.entity.id,
|
|
2019
|
+
expandRecords: needsExpandedRecords(input.entity),
|
|
2020
|
+
requiredRelationships: requiredRelationshipsOf(input.entity),
|
|
2021
|
+
strategy: input.strategy,
|
|
2022
|
+
toInsert,
|
|
2023
|
+
...classify(matched, input.strategy, false),
|
|
2024
|
+
optionalDiffs: matched.flatMap((row) => Object.entries(row.optional).map(([field, change]) => ({
|
|
2025
|
+
rowKey: row.key,
|
|
2026
|
+
label: row.label,
|
|
2027
|
+
field,
|
|
2028
|
+
live: change.from
|
|
2029
|
+
}))),
|
|
2030
|
+
idMap,
|
|
2031
|
+
journalChanged,
|
|
2032
|
+
rowCount: table.rows.length,
|
|
2033
|
+
errors,
|
|
2034
|
+
warnings,
|
|
2035
|
+
matched,
|
|
2036
|
+
locate: (rows, records) => locateRows(rows, recordsNewSincePlan(records, input.liveRecords), input.entity, columns)
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
function planRow(state, cells, index, key, localId) {
|
|
2040
|
+
const { input, columns, errors } = state;
|
|
2041
|
+
const line = index + 2;
|
|
2042
|
+
if (localId) {
|
|
2043
|
+
const first = state.firstLineOfLocalId.get(localId);
|
|
2044
|
+
if (first !== undefined) {
|
|
2045
|
+
errors.push(`${input.entityName} row ${line}: duplicate local Id "${localId}" (also on row ${first}) — local Ids must be unique per entity.`);
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
state.firstLineOfLocalId.set(localId, line);
|
|
2049
|
+
}
|
|
2050
|
+
const row = buildRow({
|
|
2051
|
+
line,
|
|
2052
|
+
key,
|
|
2053
|
+
...localId ? { localId } : {},
|
|
2054
|
+
label: rowLabel(key, line)
|
|
2055
|
+
}, cells, columns, input, errors, state.unresolved, state.ownLocalIds);
|
|
2056
|
+
if (!row)
|
|
2057
|
+
return;
|
|
2058
|
+
if (!uniqueValuesFree(row, columns, state.uniqueSeen, input, errors)) {
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
const journaled = Object.hasOwn(state.entityJournal, row.key) ? state.entityJournal[row.key] : undefined;
|
|
2062
|
+
const journaledId = journaled?.recordId;
|
|
2063
|
+
if (journaledId === undefined && !liveMatchIsClear(state, row))
|
|
2064
|
+
return;
|
|
2065
|
+
const recordId = journaledId === undefined ? adoptLiveMatch(state, row) : journaledId;
|
|
2066
|
+
if (recordId === undefined) {
|
|
2067
|
+
state.toInsert.push(row);
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
if (localId)
|
|
2071
|
+
state.idMap.set(localId, recordId);
|
|
2072
|
+
const liveRecord = state.live.byId.get(recordId.toLowerCase());
|
|
2073
|
+
state.matched.push(compareWithLive(row, recordId, liveRecord ?? {}, columns, input, journaled?.pendingFiles));
|
|
2074
|
+
}
|
|
2075
|
+
function liveMatchIsClear(state, row) {
|
|
2076
|
+
const ambiguous = state.live.ambiguous(row);
|
|
2077
|
+
if (ambiguous === undefined)
|
|
2078
|
+
return true;
|
|
2079
|
+
const { field, records } = ambiguous;
|
|
2080
|
+
const name = state.input.entityName;
|
|
2081
|
+
const reported = comparableText(row.values[field.name], field);
|
|
2082
|
+
state.errors.push(`${name} row ${row.line}, field "${field.name}": ${records} live ` + `records share the value ${reported} as the service reports it ` + "(values past 2^53 lose precision), so seed cannot tell which " + `one this row is. ${name} is not seeded until the duplicate is ` + "removed or the row is journaled.");
|
|
2083
|
+
return false;
|
|
2084
|
+
}
|
|
2085
|
+
function adoptLiveMatch(state, row) {
|
|
2086
|
+
const recordId = state.live.match(row);
|
|
2087
|
+
if (recordId === undefined)
|
|
2088
|
+
return;
|
|
2089
|
+
const shared = state.live.shared(row);
|
|
2090
|
+
if (shared !== undefined) {
|
|
2091
|
+
state.warnings.push(`${state.input.entityName} row ${row.line}: ${shared} live records match every ` + "column this row fills in, so which one it is cannot be told — the first was " + "used and the match was not recorded. Remove the duplicates, or give the " + "entity a unique field, so re-runs stay on the same record.");
|
|
2092
|
+
return recordId;
|
|
2093
|
+
}
|
|
2094
|
+
putOwn(state.entityJournal, row.key, { recordId });
|
|
2095
|
+
state.journalChanged = true;
|
|
2096
|
+
return recordId;
|
|
2097
|
+
}
|
|
2098
|
+
function withOptionalClears(plan) {
|
|
2099
|
+
return {
|
|
2100
|
+
...plan,
|
|
2101
|
+
...classify(plan.matched, plan.strategy, true)
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
function classify(matched, strategy, includeOptional) {
|
|
2105
|
+
const toUpdate = [];
|
|
2106
|
+
const toRepairAttachments = [];
|
|
2107
|
+
const ignoredRows = [];
|
|
2108
|
+
let unchanged = 0;
|
|
2109
|
+
for (const row of matched) {
|
|
2110
|
+
const { changes, clears } = requestedChanges(row, includeOptional);
|
|
2111
|
+
const pending = (row.pendingFiles ?? []).filter((field) => Object.hasOwn(row.allAttachments, field));
|
|
2112
|
+
if (Object.keys(changes).length === 0) {
|
|
2113
|
+
if (pending.length > 0) {
|
|
2114
|
+
toRepairAttachments.push(repairRow(row, pending));
|
|
2115
|
+
} else {
|
|
2116
|
+
unchanged += 1;
|
|
2117
|
+
}
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
if (strategy === "insert") {
|
|
2121
|
+
ignoredRows.push({ ...withoutMatchDetail(row), changes });
|
|
2122
|
+
if (pending.length > 0) {
|
|
2123
|
+
toRepairAttachments.push(repairRow(row, pending));
|
|
2124
|
+
}
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
2127
|
+
toUpdate.push(updateRow(row, changes, clears, pending));
|
|
2128
|
+
}
|
|
2129
|
+
return {
|
|
2130
|
+
toUpdate,
|
|
2131
|
+
toRepairAttachments,
|
|
2132
|
+
ignoredRows,
|
|
2133
|
+
ignored: ignoredRows.length,
|
|
2134
|
+
unchanged
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
function requestedChanges(row, includeOptional) {
|
|
2138
|
+
const changes = fieldMap(row.changes);
|
|
2139
|
+
if (includeOptional)
|
|
2140
|
+
Object.assign(changes, row.optional);
|
|
2141
|
+
const clears = includeOptional ? row.optionalClears : [];
|
|
2142
|
+
return { changes, clears };
|
|
2143
|
+
}
|
|
2144
|
+
function updateRow(row, changes, clears, pending) {
|
|
2145
|
+
const values = fieldMap();
|
|
2146
|
+
for (const field of Object.keys(changes)) {
|
|
2147
|
+
if (Object.hasOwn(row.attachmentChanges, field) || clears.includes(field) || Object.hasOwn(row.foreignKeys, field)) {
|
|
2148
|
+
continue;
|
|
2149
|
+
}
|
|
2150
|
+
values[field] = Object.hasOwn(row.values, field) ? row.values[field] : null;
|
|
2151
|
+
}
|
|
2152
|
+
const attachments = fieldMap(row.attachmentChanges);
|
|
2153
|
+
for (const field of pending) {
|
|
2154
|
+
attachments[field] = row.allAttachments[field];
|
|
2155
|
+
}
|
|
2156
|
+
return {
|
|
2157
|
+
...withoutMatchDetail(row),
|
|
2158
|
+
values,
|
|
2159
|
+
attachments,
|
|
2160
|
+
...clears.length > 0 ? { attachmentClears: clears } : {},
|
|
2161
|
+
changes
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
function repairRow(row, pending) {
|
|
2165
|
+
return {
|
|
2166
|
+
...withoutMatchDetail(row),
|
|
2167
|
+
values: fieldMap(),
|
|
2168
|
+
attachments: fieldMap(Object.fromEntries(pending.map((field) => [field, row.allAttachments[field]]))),
|
|
2169
|
+
pendingFiles: pending
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
function withoutMatchDetail(row) {
|
|
2173
|
+
const {
|
|
2174
|
+
optional: _optional,
|
|
2175
|
+
attachmentChanges: _attachmentChanges,
|
|
2176
|
+
optionalClears: _optionalClears,
|
|
2177
|
+
allAttachments: _allAttachments,
|
|
2178
|
+
pendingFiles: _pendingFiles,
|
|
2179
|
+
...planned
|
|
2180
|
+
} = row;
|
|
2181
|
+
return planned;
|
|
2182
|
+
}
|
|
2183
|
+
function emptyPlan(input, errors, warnings, rowCount) {
|
|
2184
|
+
return {
|
|
2185
|
+
entityName: input.entityName,
|
|
2186
|
+
entityId: input.entity.id,
|
|
2187
|
+
expandRecords: needsExpandedRecords(input.entity),
|
|
2188
|
+
requiredRelationships: requiredRelationshipsOf(input.entity),
|
|
2189
|
+
strategy: input.strategy,
|
|
2190
|
+
toInsert: [],
|
|
2191
|
+
toUpdate: [],
|
|
2192
|
+
toRepairAttachments: [],
|
|
2193
|
+
ignoredRows: [],
|
|
2194
|
+
ignored: 0,
|
|
2195
|
+
unchanged: 0,
|
|
2196
|
+
optionalDiffs: [],
|
|
2197
|
+
idMap: new Map,
|
|
2198
|
+
journalChanged: false,
|
|
2199
|
+
rowCount,
|
|
2200
|
+
errors,
|
|
2201
|
+
warnings,
|
|
2202
|
+
matched: [],
|
|
2203
|
+
locate: (rows) => rows.map(() => {
|
|
2204
|
+
return;
|
|
2205
|
+
})
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
function requiredRelationshipsOf(entity) {
|
|
2209
|
+
const names = new Set;
|
|
2210
|
+
for (const field of entity.fields.values()) {
|
|
2211
|
+
if (field.isForeignKey && field.isRequired && !field.isAttachment) {
|
|
2212
|
+
names.add(field.name);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
return names;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// src/services/seed/seed-file.ts
|
|
2219
|
+
async function readSeedFile(fs, workspaceDir, entityName) {
|
|
2220
|
+
const nameProblem = entityNameProblem(entityName);
|
|
2221
|
+
if (nameProblem) {
|
|
2222
|
+
return { kind: "invalid", path: workspaceDir, why: nameProblem };
|
|
2223
|
+
}
|
|
2224
|
+
const path = fs.path.join(workspaceDir, `${entityName}.csv`);
|
|
2225
|
+
const linkProblem = await seedFileLinkProblem(fs, workspaceDir, path);
|
|
2226
|
+
if (linkProblem)
|
|
2227
|
+
return { kind: "invalid", path, why: linkProblem };
|
|
2228
|
+
const [readError, csv] = await catchError(fs.readFile(path, "utf-8"));
|
|
2229
|
+
if (readError) {
|
|
2230
|
+
return { kind: "invalid", path, why: readError.message };
|
|
2231
|
+
}
|
|
2232
|
+
if (csv === null)
|
|
2233
|
+
return { kind: "absent", path };
|
|
2234
|
+
const [parseError, table] = catchError(() => parseCsv(csv));
|
|
2235
|
+
if (parseError || !table) {
|
|
2236
|
+
return {
|
|
2237
|
+
kind: "invalid",
|
|
2238
|
+
path,
|
|
2239
|
+
why: parseError?.message ?? "unknown error"
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
return { kind: "table", path, csv, table };
|
|
2243
|
+
}
|
|
2244
|
+
async function seedFileLinkProblem(fs, workspaceDir, path) {
|
|
2245
|
+
const [rootError, rooted] = await catchError(async () => {
|
|
2246
|
+
if (!await namesSeedRoot(workspaceDir, fs))
|
|
2247
|
+
return;
|
|
2248
|
+
return insideSeedRoot(workspaceDir, fs);
|
|
2249
|
+
});
|
|
2250
|
+
if (rootError) {
|
|
2251
|
+
return `Could not check where the workspace leads: ${rootError.message}`;
|
|
2252
|
+
}
|
|
2253
|
+
if (rooted && !rooted.ok)
|
|
2254
|
+
return rooted.message;
|
|
2255
|
+
const [checkError, inside] = await catchError(async () => {
|
|
2256
|
+
const root = await workspaceRoot(workspaceDir, fs);
|
|
2257
|
+
return await isInsideWorkspace(root, path, fs) ? root : undefined;
|
|
2258
|
+
});
|
|
2259
|
+
if (checkError) {
|
|
2260
|
+
return `Could not check where the file leads: ${checkError.message}`;
|
|
2261
|
+
}
|
|
2262
|
+
if (!inside) {
|
|
2263
|
+
return "The file is a link that leads outside the seed workspace; " + "refusing to read it. Replace the link with the file itself.";
|
|
2264
|
+
}
|
|
2265
|
+
const [danglingError, dangling] = await catchError(isDanglingLink(path, fs));
|
|
2266
|
+
if (danglingError) {
|
|
2267
|
+
return `Could not check where the file leads: ${danglingError.message}`;
|
|
2268
|
+
}
|
|
2269
|
+
if (dangling) {
|
|
2270
|
+
return "The file is a link to a path that does not exist; refusing to " + "read it. Replace the link with the file itself.";
|
|
2271
|
+
}
|
|
2272
|
+
const [aliasError, ownFile] = await catchError(pathHasExpectedTarget(inside, path, fs));
|
|
2273
|
+
if (aliasError || ownFile === undefined) {
|
|
2274
|
+
return `Could not check where the file leads: ${aliasError?.message ?? "the folder could not be listed"}`;
|
|
2275
|
+
}
|
|
2276
|
+
return ownFile ? undefined : "The file is a link to another file in the seed workspace; " + "refusing to read it — that file's rows would be seeded as " + "this entity's. Replace the link with the file itself.";
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
export { configKeyProblem, listVerticals, listPackages, listPackageVersions, normalizeVersions, getManifest, findFolderPath, orchestratorApi, listAllFolders, listFolderDtos, readAllPages, writeSeedContext, removeSeedContext, readSeedContext, BatchResponseSchema, WrittenRecordSchema, FailureRecordSchema, needsExpandedRecords, loadLiveEntities, loadChoiceSetValues, loadLiveRecords, journalEntity, putOwn, fieldMap, readStrategy, buildEntityPlan, withOptionalClears, readSeedFile };
|
|
2280
|
+
|
|
2281
|
+
//# debugId=6FF4397A8C3FBAFA64756E2164756E21
|