@objectstack/metadata-protocol 11.1.0
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/LICENSE +202 -0
- package/README.md +7 -0
- package/dist/build-probes-I3227FYL.cjs +7 -0
- package/dist/build-probes-I3227FYL.cjs.map +1 -0
- package/dist/build-probes-TLWDII7Z.js +7 -0
- package/dist/build-probes-TLWDII7Z.js.map +1 -0
- package/dist/chunk-7LOFAEHA.cjs +161 -0
- package/dist/chunk-7LOFAEHA.cjs.map +1 -0
- package/dist/chunk-HJVPAKGD.js +639 -0
- package/dist/chunk-HJVPAKGD.js.map +1 -0
- package/dist/chunk-JRNTUZG6.cjs +639 -0
- package/dist/chunk-JRNTUZG6.cjs.map +1 -0
- package/dist/chunk-KJGVCNUC.js +161 -0
- package/dist/chunk-KJGVCNUC.js.map +1 -0
- package/dist/index.cjs +4855 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1893 -0
- package/dist/index.d.ts +1893 -0
- package/dist/index.js +4855 -0
- package/dist/index.js.map +1 -0
- package/dist/seed-loader-IFRY33L4.cjs +7 -0
- package/dist/seed-loader-IFRY33L4.cjs.map +1 -0
- package/dist/seed-loader-KNXGLL2V.js +7 -0
- package/dist/seed-loader-KNXGLL2V.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/seed-loader.ts
|
|
2
|
+
var _data = require('@objectstack/spec/data');
|
|
3
|
+
var _formula = require('@objectstack/formula');
|
|
4
|
+
var DEFAULT_EXTERNAL_ID_FIELD = "name";
|
|
5
|
+
var _SeedLoaderService = class _SeedLoaderService {
|
|
6
|
+
constructor(engine, metadata, logger) {
|
|
7
|
+
this.engine = engine;
|
|
8
|
+
this.metadata = metadata;
|
|
9
|
+
this.logger = logger;
|
|
10
|
+
}
|
|
11
|
+
// ==========================================================================
|
|
12
|
+
// Public API
|
|
13
|
+
// ==========================================================================
|
|
14
|
+
async load(request) {
|
|
15
|
+
const startTime = Date.now();
|
|
16
|
+
const config = request.config;
|
|
17
|
+
const allErrors = [];
|
|
18
|
+
const allResults = [];
|
|
19
|
+
this.fallbackOrgId = config.organizationId == null ? await this.resolveSoleOrganizationId() : void 0;
|
|
20
|
+
const datasets = this.filterByEnv(request.seeds, config.env);
|
|
21
|
+
if (datasets.length === 0) {
|
|
22
|
+
return this.buildEmptyResult(config, Date.now() - startTime);
|
|
23
|
+
}
|
|
24
|
+
const objectNames = datasets.map((d) => d.object);
|
|
25
|
+
const graph = await this.buildDependencyGraph(objectNames);
|
|
26
|
+
this.logger.info("[SeedLoader] Dependency graph built", {
|
|
27
|
+
objects: objectNames.length,
|
|
28
|
+
insertOrder: graph.insertOrder,
|
|
29
|
+
circularDeps: graph.circularDependencies.length
|
|
30
|
+
});
|
|
31
|
+
const orderedDatasets = this.orderDatasets(datasets, graph.insertOrder);
|
|
32
|
+
const refMap = this.buildReferenceMap(graph);
|
|
33
|
+
const insertedRecords = /* @__PURE__ */ new Map();
|
|
34
|
+
const deferredUpdates = [];
|
|
35
|
+
for (const dataset of orderedDatasets) {
|
|
36
|
+
const result = await this.loadDataset(
|
|
37
|
+
dataset,
|
|
38
|
+
config,
|
|
39
|
+
refMap,
|
|
40
|
+
insertedRecords,
|
|
41
|
+
deferredUpdates,
|
|
42
|
+
allErrors
|
|
43
|
+
);
|
|
44
|
+
allResults.push(result);
|
|
45
|
+
if (config.haltOnError && result.errored > 0) {
|
|
46
|
+
this.logger.warn("[SeedLoader] Halting on first error", { object: dataset.object });
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (config.multiPass && deferredUpdates.length > 0 && !config.dryRun) {
|
|
51
|
+
this.logger.info("[SeedLoader] Pass 2: resolving deferred references", {
|
|
52
|
+
count: deferredUpdates.length
|
|
53
|
+
});
|
|
54
|
+
await this.resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors, config.organizationId);
|
|
55
|
+
}
|
|
56
|
+
const durationMs = Date.now() - startTime;
|
|
57
|
+
return this.buildResult(config, graph, allResults, allErrors, durationMs);
|
|
58
|
+
}
|
|
59
|
+
async buildDependencyGraph(objectNames) {
|
|
60
|
+
const nodes = [];
|
|
61
|
+
const objectSet = new Set(objectNames);
|
|
62
|
+
for (const objectName of objectNames) {
|
|
63
|
+
const objDef = await this.metadata.getObject(objectName);
|
|
64
|
+
const dependsOn = [];
|
|
65
|
+
const references = [];
|
|
66
|
+
if (objDef && objDef.fields) {
|
|
67
|
+
const fields = objDef.fields;
|
|
68
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
69
|
+
if ((fieldDef.type === "lookup" || fieldDef.type === "master_detail" || fieldDef.type === "user") && fieldDef.reference) {
|
|
70
|
+
const targetObject = fieldDef.reference;
|
|
71
|
+
if (objectSet.has(targetObject) && !dependsOn.includes(targetObject)) {
|
|
72
|
+
dependsOn.push(targetObject);
|
|
73
|
+
}
|
|
74
|
+
references.push({
|
|
75
|
+
field: fieldName,
|
|
76
|
+
targetObject,
|
|
77
|
+
targetField: DEFAULT_EXTERNAL_ID_FIELD,
|
|
78
|
+
fieldType: fieldDef.type
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
nodes.push({ object: objectName, dependsOn, references });
|
|
84
|
+
}
|
|
85
|
+
const { insertOrder, circularDependencies } = this.topologicalSort(nodes);
|
|
86
|
+
return { nodes, insertOrder, circularDependencies };
|
|
87
|
+
}
|
|
88
|
+
async validate(datasets, config) {
|
|
89
|
+
const parsedConfig = _data.SeedLoaderConfigSchema.parse({ ...config, dryRun: true });
|
|
90
|
+
return this.load({ seeds: datasets, config: parsedConfig });
|
|
91
|
+
}
|
|
92
|
+
// ==========================================================================
|
|
93
|
+
// Internal: Seed Loading
|
|
94
|
+
// ==========================================================================
|
|
95
|
+
async loadDataset(dataset, config, refMap, insertedRecords, deferredUpdates, allErrors) {
|
|
96
|
+
const objectName = dataset.object;
|
|
97
|
+
const mode = dataset.mode || config.defaultMode;
|
|
98
|
+
const externalId = dataset.externalId || "name";
|
|
99
|
+
let inserted = 0;
|
|
100
|
+
let updated = 0;
|
|
101
|
+
let skipped = 0;
|
|
102
|
+
let errored = 0;
|
|
103
|
+
let referencesResolved = 0;
|
|
104
|
+
let referencesDeferred = 0;
|
|
105
|
+
const errors = [];
|
|
106
|
+
if (!insertedRecords.has(objectName)) {
|
|
107
|
+
insertedRecords.set(objectName, /* @__PURE__ */ new Map());
|
|
108
|
+
}
|
|
109
|
+
let existingRecords;
|
|
110
|
+
if ((mode === "upsert" || mode === "update" || mode === "ignore") && !config.dryRun) {
|
|
111
|
+
existingRecords = await this.loadExistingRecords(
|
|
112
|
+
objectName,
|
|
113
|
+
externalId,
|
|
114
|
+
config.organizationId
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const objectRefs = refMap.get(objectName) || [];
|
|
118
|
+
const seedNow = /* @__PURE__ */ new Date();
|
|
119
|
+
const seedIdentity = config.identity;
|
|
120
|
+
const baseEvalCtx = {
|
|
121
|
+
now: seedNow,
|
|
122
|
+
// `id: null` is a legitimate seed-time state (the owning admin does not
|
|
123
|
+
// exist yet) that the formula EvalContext's `user.id: string` type does
|
|
124
|
+
// not yet model — cast the fallback so `os.user.id` evaluates to null.
|
|
125
|
+
user: _nullishCoalesce(_optionalChain([seedIdentity, 'optionalAccess', _ => _.user]), () => ( { id: null })),
|
|
126
|
+
// Fall back to the per-tenant organizationId so `os.org.id` resolves
|
|
127
|
+
// during per-org replay even without an explicit identity.org.
|
|
128
|
+
org: _nullishCoalesce(_optionalChain([seedIdentity, 'optionalAccess', _2 => _2.org]), () => ( (config.organizationId ? { id: config.organizationId } : void 0))),
|
|
129
|
+
env: config.env
|
|
130
|
+
};
|
|
131
|
+
for (let i = 0; i < dataset.records.length; i++) {
|
|
132
|
+
const seedResult = _formula.resolveSeedRecord.call(void 0,
|
|
133
|
+
dataset.records[i],
|
|
134
|
+
baseEvalCtx
|
|
135
|
+
);
|
|
136
|
+
if (!seedResult.ok) {
|
|
137
|
+
errored++;
|
|
138
|
+
const error = {
|
|
139
|
+
sourceObject: objectName,
|
|
140
|
+
field: "(expression)",
|
|
141
|
+
targetObject: objectName,
|
|
142
|
+
targetField: "(expression)",
|
|
143
|
+
attemptedValue: dataset.records[i],
|
|
144
|
+
recordIndex: i,
|
|
145
|
+
message: `Cannot resolve dynamic seed values for ${objectName} record #${i}: ${seedResult.error.message}. \`os.user.id\` resolves to null at seed time (the owning admin does not exist yet) and owner-style fields are assigned by the first-admin handoff \u2014 so a required, non-owner field must not depend on it. Provide a literal value or make the field optional.`
|
|
146
|
+
};
|
|
147
|
+
errors.push(error);
|
|
148
|
+
allErrors.push(error);
|
|
149
|
+
this.logger.warn(`[SeedLoader] ${error.message}`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const record = { ...seedResult.value };
|
|
153
|
+
const tenantOrg = _nullishCoalesce(config.organizationId, () => ( (/^(sys_|cloud_|ai_)/.test(objectName) ? void 0 : this.fallbackOrgId)));
|
|
154
|
+
if (tenantOrg && record["organization_id"] == null) {
|
|
155
|
+
record["organization_id"] = tenantOrg;
|
|
156
|
+
}
|
|
157
|
+
for (const ref of objectRefs) {
|
|
158
|
+
const fieldValue = record[ref.field];
|
|
159
|
+
if (fieldValue === void 0 || fieldValue === null) continue;
|
|
160
|
+
if (typeof fieldValue === "object") {
|
|
161
|
+
const wrapped = fieldValue.externalId;
|
|
162
|
+
const hint = wrapped !== void 0 ? ` Pass the natural key directly: ${ref.field}: ${JSON.stringify(wrapped)}.` : ` Pass the target's ${ref.targetField} value as a plain string.`;
|
|
163
|
+
const error = {
|
|
164
|
+
sourceObject: objectName,
|
|
165
|
+
field: ref.field,
|
|
166
|
+
targetObject: ref.targetObject,
|
|
167
|
+
targetField: ref.targetField,
|
|
168
|
+
attemptedValue: fieldValue,
|
|
169
|
+
recordIndex: i,
|
|
170
|
+
message: `Invalid reference for ${objectName}.${ref.field}: expected a ${ref.targetObject}.${ref.targetField} natural-key string but got an object.${hint}`
|
|
171
|
+
};
|
|
172
|
+
errors.push(error);
|
|
173
|
+
allErrors.push(error);
|
|
174
|
+
this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i });
|
|
175
|
+
record[ref.field] = null;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (typeof fieldValue !== "string" || this.looksLikeInternalId(fieldValue)) continue;
|
|
179
|
+
const targetMap = insertedRecords.get(ref.targetObject);
|
|
180
|
+
const resolvedId = _optionalChain([targetMap, 'optionalAccess', _3 => _3.get, 'call', _4 => _4(String(fieldValue))]);
|
|
181
|
+
if (resolvedId) {
|
|
182
|
+
record[ref.field] = resolvedId;
|
|
183
|
+
referencesResolved++;
|
|
184
|
+
} else if (!config.dryRun) {
|
|
185
|
+
const dbId = await this.resolveFromDatabase(ref.targetObject, ref.targetField, fieldValue, config.organizationId);
|
|
186
|
+
if (dbId) {
|
|
187
|
+
record[ref.field] = dbId;
|
|
188
|
+
referencesResolved++;
|
|
189
|
+
} else if (config.multiPass) {
|
|
190
|
+
record[ref.field] = null;
|
|
191
|
+
deferredUpdates.push({
|
|
192
|
+
objectName,
|
|
193
|
+
recordExternalId: String(_nullishCoalesce(record[externalId], () => ( ""))),
|
|
194
|
+
field: ref.field,
|
|
195
|
+
targetObject: ref.targetObject,
|
|
196
|
+
targetField: ref.targetField,
|
|
197
|
+
attemptedValue: fieldValue,
|
|
198
|
+
recordIndex: i
|
|
199
|
+
});
|
|
200
|
+
referencesDeferred++;
|
|
201
|
+
} else {
|
|
202
|
+
const error = {
|
|
203
|
+
sourceObject: objectName,
|
|
204
|
+
field: ref.field,
|
|
205
|
+
targetObject: ref.targetObject,
|
|
206
|
+
targetField: ref.targetField,
|
|
207
|
+
attemptedValue: fieldValue,
|
|
208
|
+
recordIndex: i,
|
|
209
|
+
message: `Cannot resolve reference: ${objectName}.${ref.field} = '${fieldValue}' \u2192 ${ref.targetObject}.${ref.targetField} not found`
|
|
210
|
+
};
|
|
211
|
+
errors.push(error);
|
|
212
|
+
allErrors.push(error);
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
const targetMap2 = insertedRecords.get(ref.targetObject);
|
|
216
|
+
if (!_optionalChain([targetMap2, 'optionalAccess', _5 => _5.has, 'call', _6 => _6(String(fieldValue))])) {
|
|
217
|
+
const error = {
|
|
218
|
+
sourceObject: objectName,
|
|
219
|
+
field: ref.field,
|
|
220
|
+
targetObject: ref.targetObject,
|
|
221
|
+
targetField: ref.targetField,
|
|
222
|
+
attemptedValue: fieldValue,
|
|
223
|
+
recordIndex: i,
|
|
224
|
+
message: `[dry-run] Reference may not resolve: ${objectName}.${ref.field} = '${fieldValue}' \u2192 ${ref.targetObject}.${ref.targetField}`
|
|
225
|
+
};
|
|
226
|
+
errors.push(error);
|
|
227
|
+
allErrors.push(error);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!config.dryRun) {
|
|
232
|
+
try {
|
|
233
|
+
const result = await this.writeRecord(
|
|
234
|
+
objectName,
|
|
235
|
+
record,
|
|
236
|
+
mode,
|
|
237
|
+
externalId,
|
|
238
|
+
existingRecords
|
|
239
|
+
);
|
|
240
|
+
if (result.action === "inserted") inserted++;
|
|
241
|
+
else if (result.action === "updated") updated++;
|
|
242
|
+
else if (result.action === "skipped") skipped++;
|
|
243
|
+
const externalIdValue = String(_nullishCoalesce(record[externalId], () => ( "")));
|
|
244
|
+
const internalId = result.id;
|
|
245
|
+
if (externalIdValue && internalId) {
|
|
246
|
+
insertedRecords.get(objectName).set(externalIdValue, String(internalId));
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
errored++;
|
|
250
|
+
const error = {
|
|
251
|
+
sourceObject: objectName,
|
|
252
|
+
field: "(write)",
|
|
253
|
+
targetObject: objectName,
|
|
254
|
+
targetField: externalId,
|
|
255
|
+
attemptedValue: _nullishCoalesce(record[externalId], () => ( null)),
|
|
256
|
+
recordIndex: i,
|
|
257
|
+
message: `Failed to write ${objectName} record #${i} (${externalId}=${String(_nullishCoalesce(record[externalId], () => ( "")))}): ${err.message}`
|
|
258
|
+
};
|
|
259
|
+
errors.push(error);
|
|
260
|
+
allErrors.push(error);
|
|
261
|
+
this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i });
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
const externalIdValue = String(_nullishCoalesce(record[externalId], () => ( "")));
|
|
265
|
+
if (externalIdValue) {
|
|
266
|
+
insertedRecords.get(objectName).set(externalIdValue, `dry-run-id-${i}`);
|
|
267
|
+
}
|
|
268
|
+
inserted++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
object: objectName,
|
|
273
|
+
mode,
|
|
274
|
+
inserted,
|
|
275
|
+
updated,
|
|
276
|
+
skipped,
|
|
277
|
+
errored,
|
|
278
|
+
total: dataset.records.length,
|
|
279
|
+
referencesResolved,
|
|
280
|
+
referencesDeferred,
|
|
281
|
+
errors
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
// ==========================================================================
|
|
285
|
+
// Internal: Reference Resolution
|
|
286
|
+
// ==========================================================================
|
|
287
|
+
/**
|
|
288
|
+
* Best-effort resolve the tenant's SOLE organization id — used to stamp
|
|
289
|
+
* business seed rows when the caller pinned no `config.organizationId` (an
|
|
290
|
+
* in-process publish has no active user session). A fresh env has exactly one
|
|
291
|
+
* org, so its seeds should carry it like a normal write instead of landing
|
|
292
|
+
* org-less (→ invisible under strict org-scoping). Returns undefined when
|
|
293
|
+
* there are zero or several orgs (genuinely ambiguous — keep the historical
|
|
294
|
+
* global/cross-tenant NULL) or when `sys_organization` is absent.
|
|
295
|
+
*/
|
|
296
|
+
async resolveSoleOrganizationId() {
|
|
297
|
+
try {
|
|
298
|
+
const rows = await this.engine.find("sys_organization", {
|
|
299
|
+
fields: ["id"],
|
|
300
|
+
limit: 2,
|
|
301
|
+
context: { isSystem: true }
|
|
302
|
+
});
|
|
303
|
+
if (Array.isArray(rows) && rows.length === 1) {
|
|
304
|
+
const id = _nullishCoalesce(_optionalChain([rows, 'access', _7 => _7[0], 'optionalAccess', _8 => _8.id]), () => ( _optionalChain([rows, 'access', _9 => _9[0], 'optionalAccess', _10 => _10._id])));
|
|
305
|
+
return id ? String(id) : void 0;
|
|
306
|
+
}
|
|
307
|
+
} catch (e) {
|
|
308
|
+
}
|
|
309
|
+
return void 0;
|
|
310
|
+
}
|
|
311
|
+
async resolveFromDatabase(targetObject, targetField, value, organizationId) {
|
|
312
|
+
try {
|
|
313
|
+
const where = { [targetField]: value };
|
|
314
|
+
if (organizationId) where.organization_id = organizationId;
|
|
315
|
+
const records = await this.engine.find(targetObject, {
|
|
316
|
+
where,
|
|
317
|
+
fields: ["id"],
|
|
318
|
+
limit: 1,
|
|
319
|
+
context: { isSystem: true }
|
|
320
|
+
});
|
|
321
|
+
if (records && records.length > 0) {
|
|
322
|
+
return String(records[0].id || records[0]._id);
|
|
323
|
+
}
|
|
324
|
+
if (targetField !== "id") {
|
|
325
|
+
const byId = { id: value };
|
|
326
|
+
if (organizationId) byId.organization_id = organizationId;
|
|
327
|
+
const idMatch = await this.engine.find(targetObject, {
|
|
328
|
+
where: byId,
|
|
329
|
+
fields: ["id"],
|
|
330
|
+
limit: 1,
|
|
331
|
+
context: { isSystem: true }
|
|
332
|
+
});
|
|
333
|
+
if (idMatch && idMatch.length > 0) {
|
|
334
|
+
return String(idMatch[0].id || idMatch[0]._id);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} catch (e2) {
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
async resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors, organizationId) {
|
|
342
|
+
for (const deferred of deferredUpdates) {
|
|
343
|
+
const targetMap = insertedRecords.get(deferred.targetObject);
|
|
344
|
+
let resolvedId = _optionalChain([targetMap, 'optionalAccess', _11 => _11.get, 'call', _12 => _12(String(deferred.attemptedValue))]);
|
|
345
|
+
if (!resolvedId) {
|
|
346
|
+
resolvedId = await _asyncNullishCoalesce(await this.resolveFromDatabase(
|
|
347
|
+
deferred.targetObject,
|
|
348
|
+
deferred.targetField,
|
|
349
|
+
deferred.attemptedValue,
|
|
350
|
+
organizationId
|
|
351
|
+
), async () => ( void 0));
|
|
352
|
+
}
|
|
353
|
+
if (resolvedId) {
|
|
354
|
+
const objectRecordMap = insertedRecords.get(deferred.objectName);
|
|
355
|
+
const recordId = _optionalChain([objectRecordMap, 'optionalAccess', _13 => _13.get, 'call', _14 => _14(deferred.recordExternalId)]);
|
|
356
|
+
if (recordId) {
|
|
357
|
+
try {
|
|
358
|
+
await this.engine.update(deferred.objectName, {
|
|
359
|
+
id: recordId,
|
|
360
|
+
[deferred.field]: resolvedId
|
|
361
|
+
}, { context: { isSystem: true } });
|
|
362
|
+
const resultEntry = allResults.find((r) => r.object === deferred.objectName);
|
|
363
|
+
if (resultEntry) {
|
|
364
|
+
resultEntry.referencesResolved++;
|
|
365
|
+
resultEntry.referencesDeferred--;
|
|
366
|
+
}
|
|
367
|
+
} catch (err) {
|
|
368
|
+
this.logger.warn("[SeedLoader] Failed to resolve deferred reference", {
|
|
369
|
+
object: deferred.objectName,
|
|
370
|
+
field: deferred.field,
|
|
371
|
+
error: err.message
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
const error = {
|
|
377
|
+
sourceObject: deferred.objectName,
|
|
378
|
+
field: deferred.field,
|
|
379
|
+
targetObject: deferred.targetObject,
|
|
380
|
+
targetField: deferred.targetField,
|
|
381
|
+
attemptedValue: deferred.attemptedValue,
|
|
382
|
+
recordIndex: deferred.recordIndex,
|
|
383
|
+
message: `Deferred reference unresolved after pass 2: ${deferred.objectName}.${deferred.field} = '${deferred.attemptedValue}' \u2192 ${deferred.targetObject}.${deferred.targetField} not found`
|
|
384
|
+
};
|
|
385
|
+
const resultEntry = allResults.find((r) => r.object === deferred.objectName);
|
|
386
|
+
if (resultEntry) {
|
|
387
|
+
resultEntry.errors.push(error);
|
|
388
|
+
}
|
|
389
|
+
allErrors.push(error);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async writeRecord(objectName, record, mode, externalId, existingRecords) {
|
|
394
|
+
const externalIdValue = record[externalId];
|
|
395
|
+
const existing = _optionalChain([existingRecords, 'optionalAccess', _15 => _15.get, 'call', _16 => _16(String(_nullishCoalesce(externalIdValue, () => ( ""))))]);
|
|
396
|
+
const opts = _SeedLoaderService.SEED_OPTIONS;
|
|
397
|
+
switch (mode) {
|
|
398
|
+
case "insert": {
|
|
399
|
+
const result = await this.engine.insert(objectName, record, opts);
|
|
400
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
401
|
+
}
|
|
402
|
+
case "update": {
|
|
403
|
+
if (!existing) {
|
|
404
|
+
return { action: "skipped" };
|
|
405
|
+
}
|
|
406
|
+
const id = this.extractId(existing);
|
|
407
|
+
await this.engine.update(objectName, { ...record, id }, opts);
|
|
408
|
+
return { action: "updated", id };
|
|
409
|
+
}
|
|
410
|
+
case "upsert": {
|
|
411
|
+
if (existing) {
|
|
412
|
+
const id = this.extractId(existing);
|
|
413
|
+
await this.engine.update(objectName, { ...record, id }, opts);
|
|
414
|
+
return { action: "updated", id };
|
|
415
|
+
} else {
|
|
416
|
+
const result = await this.engine.insert(objectName, record, opts);
|
|
417
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
case "ignore": {
|
|
421
|
+
if (existing) {
|
|
422
|
+
return { action: "skipped", id: this.extractId(existing) };
|
|
423
|
+
}
|
|
424
|
+
const result = await this.engine.insert(objectName, record, opts);
|
|
425
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
426
|
+
}
|
|
427
|
+
case "replace": {
|
|
428
|
+
const result = await this.engine.insert(objectName, record, opts);
|
|
429
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
430
|
+
}
|
|
431
|
+
default: {
|
|
432
|
+
const result = await this.engine.insert(objectName, record, opts);
|
|
433
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// ==========================================================================
|
|
438
|
+
// Internal: Dependency Graph
|
|
439
|
+
// ==========================================================================
|
|
440
|
+
/**
|
|
441
|
+
* Kahn's algorithm for topological sort with cycle detection.
|
|
442
|
+
*/
|
|
443
|
+
topologicalSort(nodes) {
|
|
444
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
445
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
446
|
+
const objectSet = new Set(nodes.map((n) => n.object));
|
|
447
|
+
for (const node of nodes) {
|
|
448
|
+
inDegree.set(node.object, 0);
|
|
449
|
+
adjacency.set(node.object, []);
|
|
450
|
+
}
|
|
451
|
+
for (const node of nodes) {
|
|
452
|
+
for (const dep of node.dependsOn) {
|
|
453
|
+
if (objectSet.has(dep) && dep !== node.object) {
|
|
454
|
+
adjacency.get(dep).push(node.object);
|
|
455
|
+
inDegree.set(node.object, (inDegree.get(node.object) || 0) + 1);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const queue = [];
|
|
460
|
+
for (const [obj, degree] of inDegree) {
|
|
461
|
+
if (degree === 0) queue.push(obj);
|
|
462
|
+
}
|
|
463
|
+
const insertOrder = [];
|
|
464
|
+
while (queue.length > 0) {
|
|
465
|
+
const current = queue.shift();
|
|
466
|
+
insertOrder.push(current);
|
|
467
|
+
for (const neighbor of adjacency.get(current) || []) {
|
|
468
|
+
const newDegree = (inDegree.get(neighbor) || 0) - 1;
|
|
469
|
+
inDegree.set(neighbor, newDegree);
|
|
470
|
+
if (newDegree === 0) {
|
|
471
|
+
queue.push(neighbor);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const circularDependencies = [];
|
|
476
|
+
const remaining = nodes.filter((n) => !insertOrder.includes(n.object));
|
|
477
|
+
if (remaining.length > 0) {
|
|
478
|
+
const cycles = this.findCycles(remaining);
|
|
479
|
+
circularDependencies.push(...cycles);
|
|
480
|
+
for (const node of remaining) {
|
|
481
|
+
if (!insertOrder.includes(node.object)) {
|
|
482
|
+
insertOrder.push(node.object);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return { insertOrder, circularDependencies };
|
|
487
|
+
}
|
|
488
|
+
findCycles(nodes) {
|
|
489
|
+
const cycles = [];
|
|
490
|
+
const nodeMap = new Map(nodes.map((n) => [n.object, n]));
|
|
491
|
+
const visited = /* @__PURE__ */ new Set();
|
|
492
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
493
|
+
const dfs = (current, path) => {
|
|
494
|
+
if (inStack.has(current)) {
|
|
495
|
+
const cycleStart = path.indexOf(current);
|
|
496
|
+
if (cycleStart !== -1) {
|
|
497
|
+
cycles.push([...path.slice(cycleStart), current]);
|
|
498
|
+
}
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (visited.has(current)) return;
|
|
502
|
+
visited.add(current);
|
|
503
|
+
inStack.add(current);
|
|
504
|
+
path.push(current);
|
|
505
|
+
const node = nodeMap.get(current);
|
|
506
|
+
if (node) {
|
|
507
|
+
for (const dep of node.dependsOn) {
|
|
508
|
+
if (nodeMap.has(dep)) {
|
|
509
|
+
dfs(dep, [...path]);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
inStack.delete(current);
|
|
514
|
+
};
|
|
515
|
+
for (const node of nodes) {
|
|
516
|
+
if (!visited.has(node.object)) {
|
|
517
|
+
dfs(node.object, []);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return cycles;
|
|
521
|
+
}
|
|
522
|
+
// ==========================================================================
|
|
523
|
+
// Internal: Helpers
|
|
524
|
+
// ==========================================================================
|
|
525
|
+
filterByEnv(datasets, env) {
|
|
526
|
+
if (!env) return datasets;
|
|
527
|
+
return datasets.filter((d) => d.env.includes(env));
|
|
528
|
+
}
|
|
529
|
+
orderDatasets(datasets, insertOrder) {
|
|
530
|
+
const orderMap = new Map(insertOrder.map((name, i) => [name, i]));
|
|
531
|
+
return [...datasets].sort((a, b) => {
|
|
532
|
+
const orderA = _nullishCoalesce(orderMap.get(a.object), () => ( Number.MAX_SAFE_INTEGER));
|
|
533
|
+
const orderB = _nullishCoalesce(orderMap.get(b.object), () => ( Number.MAX_SAFE_INTEGER));
|
|
534
|
+
return orderA - orderB;
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
buildReferenceMap(graph) {
|
|
538
|
+
const map = /* @__PURE__ */ new Map();
|
|
539
|
+
for (const node of graph.nodes) {
|
|
540
|
+
if (node.references.length > 0) {
|
|
541
|
+
map.set(node.object, node.references);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return map;
|
|
545
|
+
}
|
|
546
|
+
async loadExistingRecords(objectName, externalId, organizationId) {
|
|
547
|
+
const map = /* @__PURE__ */ new Map();
|
|
548
|
+
try {
|
|
549
|
+
const findArgs = {
|
|
550
|
+
fields: ["id", externalId],
|
|
551
|
+
context: { isSystem: true }
|
|
552
|
+
};
|
|
553
|
+
if (organizationId) findArgs.where = { organization_id: organizationId };
|
|
554
|
+
const records = await this.engine.find(objectName, findArgs);
|
|
555
|
+
for (const record of records || []) {
|
|
556
|
+
const key = String(_nullishCoalesce(record[externalId], () => ( "")));
|
|
557
|
+
if (key) {
|
|
558
|
+
map.set(key, record);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
} catch (e3) {
|
|
562
|
+
}
|
|
563
|
+
return map;
|
|
564
|
+
}
|
|
565
|
+
looksLikeInternalId(value) {
|
|
566
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
if (/^[0-9a-f]{24}$/i.test(value)) {
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
extractId(record) {
|
|
575
|
+
if (!record) return void 0;
|
|
576
|
+
return String(record.id || record._id || "");
|
|
577
|
+
}
|
|
578
|
+
buildEmptyResult(config, durationMs) {
|
|
579
|
+
return {
|
|
580
|
+
success: true,
|
|
581
|
+
dryRun: config.dryRun,
|
|
582
|
+
dependencyGraph: { nodes: [], insertOrder: [], circularDependencies: [] },
|
|
583
|
+
results: [],
|
|
584
|
+
errors: [],
|
|
585
|
+
summary: {
|
|
586
|
+
objectsProcessed: 0,
|
|
587
|
+
totalRecords: 0,
|
|
588
|
+
totalInserted: 0,
|
|
589
|
+
totalUpdated: 0,
|
|
590
|
+
totalSkipped: 0,
|
|
591
|
+
totalErrored: 0,
|
|
592
|
+
totalReferencesResolved: 0,
|
|
593
|
+
totalReferencesDeferred: 0,
|
|
594
|
+
circularDependencyCount: 0,
|
|
595
|
+
durationMs
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
buildResult(config, graph, results, errors, durationMs) {
|
|
600
|
+
const summary = {
|
|
601
|
+
objectsProcessed: results.length,
|
|
602
|
+
totalRecords: results.reduce((sum, r) => sum + r.total, 0),
|
|
603
|
+
totalInserted: results.reduce((sum, r) => sum + r.inserted, 0),
|
|
604
|
+
totalUpdated: results.reduce((sum, r) => sum + r.updated, 0),
|
|
605
|
+
totalSkipped: results.reduce((sum, r) => sum + r.skipped, 0),
|
|
606
|
+
totalErrored: results.reduce((sum, r) => sum + r.errored, 0),
|
|
607
|
+
totalReferencesResolved: results.reduce((sum, r) => sum + r.referencesResolved, 0),
|
|
608
|
+
totalReferencesDeferred: results.reduce((sum, r) => sum + r.referencesDeferred, 0),
|
|
609
|
+
circularDependencyCount: graph.circularDependencies.length,
|
|
610
|
+
durationMs
|
|
611
|
+
};
|
|
612
|
+
const hasErrors = errors.length > 0 || summary.totalErrored > 0;
|
|
613
|
+
return {
|
|
614
|
+
success: !hasErrors,
|
|
615
|
+
dryRun: config.dryRun,
|
|
616
|
+
dependencyGraph: graph,
|
|
617
|
+
results,
|
|
618
|
+
errors,
|
|
619
|
+
summary
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
// ==========================================================================
|
|
624
|
+
// Internal: Write Operations
|
|
625
|
+
// ==========================================================================
|
|
626
|
+
/**
|
|
627
|
+
* Seed writes always run as a privileged system context. This bypasses
|
|
628
|
+
* RBAC checks (so seeds can target system tables like `sys_*`) and
|
|
629
|
+
* disables the SecurityPlugin's auto-injection of `organization_id` /
|
|
630
|
+
* `owner_id` — seeds either declare those fields explicitly per
|
|
631
|
+
* record, or are intentionally cross-tenant / global.
|
|
632
|
+
*/
|
|
633
|
+
_SeedLoaderService.SEED_OPTIONS = { context: { isSystem: true } };
|
|
634
|
+
var SeedLoaderService = _SeedLoaderService;
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
exports.SeedLoaderService = SeedLoaderService;
|
|
639
|
+
//# sourceMappingURL=chunk-JRNTUZG6.cjs.map
|