@objectstack/runtime 3.2.1 → 3.2.3
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +19 -0
- package/dist/index.cjs +568 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -2
- package/dist/index.d.ts +48 -2
- package/dist/index.js +557 -12
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/app-plugin.ts +52 -14
- package/src/http-dispatcher.test.ts +3 -3
- package/src/http-dispatcher.ts +4 -4
- package/src/index.ts +1 -0
- package/src/seed-loader.test.ts +1123 -0
- package/src/seed-loader.ts +713 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/runtime@3.2.
|
|
2
|
+
> @objectstack/runtime@3.2.3 build /home/runner/work/spec/spec/packages/runtime
|
|
3
3
|
> tsup --config ../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[
|
|
14
|
-
[
|
|
15
|
-
[
|
|
16
|
-
[
|
|
17
|
-
[
|
|
18
|
-
[
|
|
13
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m82.74 KB[39m
|
|
14
|
+
[32mCJS[39m [1mdist/index.cjs.map [22m[32m172.94 KB[39m
|
|
15
|
+
[32mCJS[39m ⚡️ Build success in 164ms
|
|
16
|
+
[32mESM[39m [1mdist/index.js [22m[32m80.18 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m172.87 KB[39m
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 198ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.cts [22m[
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 7975ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m23.61 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.cts [22m[32m23.61 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @objectstack/runtime
|
|
2
2
|
|
|
3
|
+
## 3.2.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @objectstack/spec@3.2.3
|
|
8
|
+
- @objectstack/core@3.2.3
|
|
9
|
+
- @objectstack/types@3.2.3
|
|
10
|
+
- @objectstack/rest@3.2.3
|
|
11
|
+
|
|
12
|
+
## 3.2.2
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [46defbb]
|
|
17
|
+
- @objectstack/spec@3.2.2
|
|
18
|
+
- @objectstack/core@3.2.2
|
|
19
|
+
- @objectstack/rest@3.2.2
|
|
20
|
+
- @objectstack/types@3.2.2
|
|
21
|
+
|
|
3
22
|
## 3.2.1
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -16,6 +18,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
18
20
|
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
19
29
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
30
|
|
|
21
31
|
// src/index.ts
|
|
@@ -31,6 +41,7 @@ __export(index_exports, {
|
|
|
31
41
|
RouteGroupBuilder: () => import_rest.RouteGroupBuilder,
|
|
32
42
|
RouteManager: () => import_rest.RouteManager,
|
|
33
43
|
Runtime: () => Runtime,
|
|
44
|
+
SeedLoaderService: () => SeedLoaderService,
|
|
34
45
|
createDispatcherPlugin: () => createDispatcherPlugin,
|
|
35
46
|
createRestApiPlugin: () => import_rest.createRestApiPlugin
|
|
36
47
|
});
|
|
@@ -111,6 +122,518 @@ var DriverPlugin = class {
|
|
|
111
122
|
}
|
|
112
123
|
};
|
|
113
124
|
|
|
125
|
+
// src/seed-loader.ts
|
|
126
|
+
var DEFAULT_EXTERNAL_ID_FIELD = "name";
|
|
127
|
+
var SeedLoaderService = class {
|
|
128
|
+
constructor(engine, metadata, logger) {
|
|
129
|
+
this.engine = engine;
|
|
130
|
+
this.metadata = metadata;
|
|
131
|
+
this.logger = logger;
|
|
132
|
+
}
|
|
133
|
+
// ==========================================================================
|
|
134
|
+
// Public API
|
|
135
|
+
// ==========================================================================
|
|
136
|
+
async load(request) {
|
|
137
|
+
const startTime = Date.now();
|
|
138
|
+
const config = request.config;
|
|
139
|
+
const allErrors = [];
|
|
140
|
+
const allResults = [];
|
|
141
|
+
const datasets = this.filterByEnv(request.datasets, config.env);
|
|
142
|
+
if (datasets.length === 0) {
|
|
143
|
+
return this.buildEmptyResult(config, Date.now() - startTime);
|
|
144
|
+
}
|
|
145
|
+
const objectNames = datasets.map((d) => d.object);
|
|
146
|
+
const graph = await this.buildDependencyGraph(objectNames);
|
|
147
|
+
this.logger.info("[SeedLoader] Dependency graph built", {
|
|
148
|
+
objects: objectNames.length,
|
|
149
|
+
insertOrder: graph.insertOrder,
|
|
150
|
+
circularDeps: graph.circularDependencies.length
|
|
151
|
+
});
|
|
152
|
+
const orderedDatasets = this.orderDatasets(datasets, graph.insertOrder);
|
|
153
|
+
const refMap = this.buildReferenceMap(graph);
|
|
154
|
+
const insertedRecords = /* @__PURE__ */ new Map();
|
|
155
|
+
const deferredUpdates = [];
|
|
156
|
+
for (const dataset of orderedDatasets) {
|
|
157
|
+
const result = await this.loadDataset(
|
|
158
|
+
dataset,
|
|
159
|
+
config,
|
|
160
|
+
refMap,
|
|
161
|
+
insertedRecords,
|
|
162
|
+
deferredUpdates,
|
|
163
|
+
allErrors
|
|
164
|
+
);
|
|
165
|
+
allResults.push(result);
|
|
166
|
+
if (config.haltOnError && result.errored > 0) {
|
|
167
|
+
this.logger.warn("[SeedLoader] Halting on first error", { object: dataset.object });
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (config.multiPass && deferredUpdates.length > 0 && !config.dryRun) {
|
|
172
|
+
this.logger.info("[SeedLoader] Pass 2: resolving deferred references", {
|
|
173
|
+
count: deferredUpdates.length
|
|
174
|
+
});
|
|
175
|
+
await this.resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors);
|
|
176
|
+
}
|
|
177
|
+
const durationMs = Date.now() - startTime;
|
|
178
|
+
return this.buildResult(config, graph, allResults, allErrors, durationMs);
|
|
179
|
+
}
|
|
180
|
+
async buildDependencyGraph(objectNames) {
|
|
181
|
+
const nodes = [];
|
|
182
|
+
const objectSet = new Set(objectNames);
|
|
183
|
+
for (const objectName of objectNames) {
|
|
184
|
+
const objDef = await this.metadata.getObject(objectName);
|
|
185
|
+
const dependsOn = [];
|
|
186
|
+
const references = [];
|
|
187
|
+
if (objDef && objDef.fields) {
|
|
188
|
+
const fields = objDef.fields;
|
|
189
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
190
|
+
if ((fieldDef.type === "lookup" || fieldDef.type === "master_detail") && fieldDef.reference) {
|
|
191
|
+
const targetObject = fieldDef.reference;
|
|
192
|
+
if (objectSet.has(targetObject) && !dependsOn.includes(targetObject)) {
|
|
193
|
+
dependsOn.push(targetObject);
|
|
194
|
+
}
|
|
195
|
+
references.push({
|
|
196
|
+
field: fieldName,
|
|
197
|
+
targetObject,
|
|
198
|
+
targetField: DEFAULT_EXTERNAL_ID_FIELD,
|
|
199
|
+
fieldType: fieldDef.type
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
nodes.push({ object: objectName, dependsOn, references });
|
|
205
|
+
}
|
|
206
|
+
const { insertOrder, circularDependencies } = this.topologicalSort(nodes);
|
|
207
|
+
return { nodes, insertOrder, circularDependencies };
|
|
208
|
+
}
|
|
209
|
+
async validate(datasets, config) {
|
|
210
|
+
const { SeedLoaderConfigSchema } = await import("@objectstack/spec/data");
|
|
211
|
+
const parsedConfig = SeedLoaderConfigSchema.parse({ ...config, dryRun: true });
|
|
212
|
+
return this.load({ datasets, config: parsedConfig });
|
|
213
|
+
}
|
|
214
|
+
// ==========================================================================
|
|
215
|
+
// Internal: Dataset Loading
|
|
216
|
+
// ==========================================================================
|
|
217
|
+
async loadDataset(dataset, config, refMap, insertedRecords, deferredUpdates, allErrors) {
|
|
218
|
+
const objectName = dataset.object;
|
|
219
|
+
const mode = dataset.mode || config.defaultMode;
|
|
220
|
+
const externalId = dataset.externalId || "name";
|
|
221
|
+
let inserted = 0;
|
|
222
|
+
let updated = 0;
|
|
223
|
+
let skipped = 0;
|
|
224
|
+
let errored = 0;
|
|
225
|
+
let referencesResolved = 0;
|
|
226
|
+
let referencesDeferred = 0;
|
|
227
|
+
const errors = [];
|
|
228
|
+
if (!insertedRecords.has(objectName)) {
|
|
229
|
+
insertedRecords.set(objectName, /* @__PURE__ */ new Map());
|
|
230
|
+
}
|
|
231
|
+
let existingRecords;
|
|
232
|
+
if ((mode === "upsert" || mode === "update" || mode === "ignore") && !config.dryRun) {
|
|
233
|
+
existingRecords = await this.loadExistingRecords(objectName, externalId);
|
|
234
|
+
}
|
|
235
|
+
const objectRefs = refMap.get(objectName) || [];
|
|
236
|
+
for (let i = 0; i < dataset.records.length; i++) {
|
|
237
|
+
const record = { ...dataset.records[i] };
|
|
238
|
+
for (const ref of objectRefs) {
|
|
239
|
+
const fieldValue = record[ref.field];
|
|
240
|
+
if (fieldValue === void 0 || fieldValue === null) continue;
|
|
241
|
+
if (typeof fieldValue !== "string" || this.looksLikeInternalId(fieldValue)) continue;
|
|
242
|
+
const targetMap = insertedRecords.get(ref.targetObject);
|
|
243
|
+
const resolvedId = targetMap?.get(String(fieldValue));
|
|
244
|
+
if (resolvedId) {
|
|
245
|
+
record[ref.field] = resolvedId;
|
|
246
|
+
referencesResolved++;
|
|
247
|
+
} else if (!config.dryRun) {
|
|
248
|
+
const dbId = await this.resolveFromDatabase(ref.targetObject, ref.targetField, fieldValue);
|
|
249
|
+
if (dbId) {
|
|
250
|
+
record[ref.field] = dbId;
|
|
251
|
+
referencesResolved++;
|
|
252
|
+
} else if (config.multiPass) {
|
|
253
|
+
record[ref.field] = null;
|
|
254
|
+
deferredUpdates.push({
|
|
255
|
+
objectName,
|
|
256
|
+
recordExternalId: String(record[externalId] ?? ""),
|
|
257
|
+
field: ref.field,
|
|
258
|
+
targetObject: ref.targetObject,
|
|
259
|
+
targetField: ref.targetField,
|
|
260
|
+
attemptedValue: fieldValue,
|
|
261
|
+
recordIndex: i
|
|
262
|
+
});
|
|
263
|
+
referencesDeferred++;
|
|
264
|
+
} else {
|
|
265
|
+
const error = {
|
|
266
|
+
sourceObject: objectName,
|
|
267
|
+
field: ref.field,
|
|
268
|
+
targetObject: ref.targetObject,
|
|
269
|
+
targetField: ref.targetField,
|
|
270
|
+
attemptedValue: fieldValue,
|
|
271
|
+
recordIndex: i,
|
|
272
|
+
message: `Cannot resolve reference: ${objectName}.${ref.field} = '${fieldValue}' \u2192 ${ref.targetObject}.${ref.targetField} not found`
|
|
273
|
+
};
|
|
274
|
+
errors.push(error);
|
|
275
|
+
allErrors.push(error);
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
const targetMap2 = insertedRecords.get(ref.targetObject);
|
|
279
|
+
if (!targetMap2?.has(String(fieldValue))) {
|
|
280
|
+
const error = {
|
|
281
|
+
sourceObject: objectName,
|
|
282
|
+
field: ref.field,
|
|
283
|
+
targetObject: ref.targetObject,
|
|
284
|
+
targetField: ref.targetField,
|
|
285
|
+
attemptedValue: fieldValue,
|
|
286
|
+
recordIndex: i,
|
|
287
|
+
message: `[dry-run] Reference may not resolve: ${objectName}.${ref.field} = '${fieldValue}' \u2192 ${ref.targetObject}.${ref.targetField}`
|
|
288
|
+
};
|
|
289
|
+
errors.push(error);
|
|
290
|
+
allErrors.push(error);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!config.dryRun) {
|
|
295
|
+
try {
|
|
296
|
+
const result = await this.writeRecord(
|
|
297
|
+
objectName,
|
|
298
|
+
record,
|
|
299
|
+
mode,
|
|
300
|
+
externalId,
|
|
301
|
+
existingRecords
|
|
302
|
+
);
|
|
303
|
+
if (result.action === "inserted") inserted++;
|
|
304
|
+
else if (result.action === "updated") updated++;
|
|
305
|
+
else if (result.action === "skipped") skipped++;
|
|
306
|
+
const externalIdValue = String(record[externalId] ?? "");
|
|
307
|
+
const internalId = result.id;
|
|
308
|
+
if (externalIdValue && internalId) {
|
|
309
|
+
insertedRecords.get(objectName).set(externalIdValue, String(internalId));
|
|
310
|
+
}
|
|
311
|
+
} catch (err) {
|
|
312
|
+
errored++;
|
|
313
|
+
this.logger.warn(`[SeedLoader] Failed to write ${objectName} record`, {
|
|
314
|
+
error: err.message,
|
|
315
|
+
recordIndex: i
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
const externalIdValue = String(record[externalId] ?? "");
|
|
320
|
+
if (externalIdValue) {
|
|
321
|
+
insertedRecords.get(objectName).set(externalIdValue, `dry-run-id-${i}`);
|
|
322
|
+
}
|
|
323
|
+
inserted++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
object: objectName,
|
|
328
|
+
mode,
|
|
329
|
+
inserted,
|
|
330
|
+
updated,
|
|
331
|
+
skipped,
|
|
332
|
+
errored,
|
|
333
|
+
total: dataset.records.length,
|
|
334
|
+
referencesResolved,
|
|
335
|
+
referencesDeferred,
|
|
336
|
+
errors
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// ==========================================================================
|
|
340
|
+
// Internal: Reference Resolution
|
|
341
|
+
// ==========================================================================
|
|
342
|
+
async resolveFromDatabase(targetObject, targetField, value) {
|
|
343
|
+
try {
|
|
344
|
+
const records = await this.engine.find(targetObject, {
|
|
345
|
+
filter: { [targetField]: value },
|
|
346
|
+
select: ["id"],
|
|
347
|
+
limit: 1
|
|
348
|
+
});
|
|
349
|
+
if (records && records.length > 0) {
|
|
350
|
+
return String(records[0].id || records[0]._id);
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
async resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors) {
|
|
357
|
+
for (const deferred of deferredUpdates) {
|
|
358
|
+
const targetMap = insertedRecords.get(deferred.targetObject);
|
|
359
|
+
let resolvedId = targetMap?.get(String(deferred.attemptedValue));
|
|
360
|
+
if (!resolvedId) {
|
|
361
|
+
resolvedId = await this.resolveFromDatabase(
|
|
362
|
+
deferred.targetObject,
|
|
363
|
+
deferred.targetField,
|
|
364
|
+
deferred.attemptedValue
|
|
365
|
+
) ?? void 0;
|
|
366
|
+
}
|
|
367
|
+
if (resolvedId) {
|
|
368
|
+
const objectRecordMap = insertedRecords.get(deferred.objectName);
|
|
369
|
+
const recordId = objectRecordMap?.get(deferred.recordExternalId);
|
|
370
|
+
if (recordId) {
|
|
371
|
+
try {
|
|
372
|
+
await this.engine.update(deferred.objectName, {
|
|
373
|
+
id: recordId,
|
|
374
|
+
[deferred.field]: resolvedId
|
|
375
|
+
});
|
|
376
|
+
const resultEntry = allResults.find((r) => r.object === deferred.objectName);
|
|
377
|
+
if (resultEntry) {
|
|
378
|
+
resultEntry.referencesResolved++;
|
|
379
|
+
resultEntry.referencesDeferred--;
|
|
380
|
+
}
|
|
381
|
+
} catch (err) {
|
|
382
|
+
this.logger.warn("[SeedLoader] Failed to resolve deferred reference", {
|
|
383
|
+
object: deferred.objectName,
|
|
384
|
+
field: deferred.field,
|
|
385
|
+
error: err.message
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
const error = {
|
|
391
|
+
sourceObject: deferred.objectName,
|
|
392
|
+
field: deferred.field,
|
|
393
|
+
targetObject: deferred.targetObject,
|
|
394
|
+
targetField: deferred.targetField,
|
|
395
|
+
attemptedValue: deferred.attemptedValue,
|
|
396
|
+
recordIndex: deferred.recordIndex,
|
|
397
|
+
message: `Deferred reference unresolved after pass 2: ${deferred.objectName}.${deferred.field} = '${deferred.attemptedValue}' \u2192 ${deferred.targetObject}.${deferred.targetField} not found`
|
|
398
|
+
};
|
|
399
|
+
const resultEntry = allResults.find((r) => r.object === deferred.objectName);
|
|
400
|
+
if (resultEntry) {
|
|
401
|
+
resultEntry.errors.push(error);
|
|
402
|
+
}
|
|
403
|
+
allErrors.push(error);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// ==========================================================================
|
|
408
|
+
// Internal: Write Operations
|
|
409
|
+
// ==========================================================================
|
|
410
|
+
async writeRecord(objectName, record, mode, externalId, existingRecords) {
|
|
411
|
+
const externalIdValue = record[externalId];
|
|
412
|
+
const existing = existingRecords?.get(String(externalIdValue ?? ""));
|
|
413
|
+
switch (mode) {
|
|
414
|
+
case "insert": {
|
|
415
|
+
const result = await this.engine.insert(objectName, record);
|
|
416
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
417
|
+
}
|
|
418
|
+
case "update": {
|
|
419
|
+
if (!existing) {
|
|
420
|
+
return { action: "skipped" };
|
|
421
|
+
}
|
|
422
|
+
const id = this.extractId(existing);
|
|
423
|
+
await this.engine.update(objectName, { ...record, id });
|
|
424
|
+
return { action: "updated", id };
|
|
425
|
+
}
|
|
426
|
+
case "upsert": {
|
|
427
|
+
if (existing) {
|
|
428
|
+
const id = this.extractId(existing);
|
|
429
|
+
await this.engine.update(objectName, { ...record, id });
|
|
430
|
+
return { action: "updated", id };
|
|
431
|
+
} else {
|
|
432
|
+
const result = await this.engine.insert(objectName, record);
|
|
433
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
case "ignore": {
|
|
437
|
+
if (existing) {
|
|
438
|
+
return { action: "skipped", id: this.extractId(existing) };
|
|
439
|
+
}
|
|
440
|
+
const result = await this.engine.insert(objectName, record);
|
|
441
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
442
|
+
}
|
|
443
|
+
case "replace": {
|
|
444
|
+
const result = await this.engine.insert(objectName, record);
|
|
445
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
446
|
+
}
|
|
447
|
+
default: {
|
|
448
|
+
const result = await this.engine.insert(objectName, record);
|
|
449
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ==========================================================================
|
|
454
|
+
// Internal: Dependency Graph
|
|
455
|
+
// ==========================================================================
|
|
456
|
+
/**
|
|
457
|
+
* Kahn's algorithm for topological sort with cycle detection.
|
|
458
|
+
*/
|
|
459
|
+
topologicalSort(nodes) {
|
|
460
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
461
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
462
|
+
const objectSet = new Set(nodes.map((n) => n.object));
|
|
463
|
+
for (const node of nodes) {
|
|
464
|
+
inDegree.set(node.object, 0);
|
|
465
|
+
adjacency.set(node.object, []);
|
|
466
|
+
}
|
|
467
|
+
for (const node of nodes) {
|
|
468
|
+
for (const dep of node.dependsOn) {
|
|
469
|
+
if (objectSet.has(dep) && dep !== node.object) {
|
|
470
|
+
adjacency.get(dep).push(node.object);
|
|
471
|
+
inDegree.set(node.object, (inDegree.get(node.object) || 0) + 1);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const queue = [];
|
|
476
|
+
for (const [obj, degree] of inDegree) {
|
|
477
|
+
if (degree === 0) queue.push(obj);
|
|
478
|
+
}
|
|
479
|
+
const insertOrder = [];
|
|
480
|
+
while (queue.length > 0) {
|
|
481
|
+
const current = queue.shift();
|
|
482
|
+
insertOrder.push(current);
|
|
483
|
+
for (const neighbor of adjacency.get(current) || []) {
|
|
484
|
+
const newDegree = (inDegree.get(neighbor) || 0) - 1;
|
|
485
|
+
inDegree.set(neighbor, newDegree);
|
|
486
|
+
if (newDegree === 0) {
|
|
487
|
+
queue.push(neighbor);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const circularDependencies = [];
|
|
492
|
+
const remaining = nodes.filter((n) => !insertOrder.includes(n.object));
|
|
493
|
+
if (remaining.length > 0) {
|
|
494
|
+
const cycles = this.findCycles(remaining);
|
|
495
|
+
circularDependencies.push(...cycles);
|
|
496
|
+
for (const node of remaining) {
|
|
497
|
+
if (!insertOrder.includes(node.object)) {
|
|
498
|
+
insertOrder.push(node.object);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return { insertOrder, circularDependencies };
|
|
503
|
+
}
|
|
504
|
+
findCycles(nodes) {
|
|
505
|
+
const cycles = [];
|
|
506
|
+
const nodeMap = new Map(nodes.map((n) => [n.object, n]));
|
|
507
|
+
const visited = /* @__PURE__ */ new Set();
|
|
508
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
509
|
+
const dfs = (current, path) => {
|
|
510
|
+
if (inStack.has(current)) {
|
|
511
|
+
const cycleStart = path.indexOf(current);
|
|
512
|
+
if (cycleStart !== -1) {
|
|
513
|
+
cycles.push([...path.slice(cycleStart), current]);
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (visited.has(current)) return;
|
|
518
|
+
visited.add(current);
|
|
519
|
+
inStack.add(current);
|
|
520
|
+
path.push(current);
|
|
521
|
+
const node = nodeMap.get(current);
|
|
522
|
+
if (node) {
|
|
523
|
+
for (const dep of node.dependsOn) {
|
|
524
|
+
if (nodeMap.has(dep)) {
|
|
525
|
+
dfs(dep, [...path]);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
inStack.delete(current);
|
|
530
|
+
};
|
|
531
|
+
for (const node of nodes) {
|
|
532
|
+
if (!visited.has(node.object)) {
|
|
533
|
+
dfs(node.object, []);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return cycles;
|
|
537
|
+
}
|
|
538
|
+
// ==========================================================================
|
|
539
|
+
// Internal: Helpers
|
|
540
|
+
// ==========================================================================
|
|
541
|
+
filterByEnv(datasets, env) {
|
|
542
|
+
if (!env) return datasets;
|
|
543
|
+
return datasets.filter((d) => d.env.includes(env));
|
|
544
|
+
}
|
|
545
|
+
orderDatasets(datasets, insertOrder) {
|
|
546
|
+
const orderMap = new Map(insertOrder.map((name, i) => [name, i]));
|
|
547
|
+
return [...datasets].sort((a, b) => {
|
|
548
|
+
const orderA = orderMap.get(a.object) ?? Number.MAX_SAFE_INTEGER;
|
|
549
|
+
const orderB = orderMap.get(b.object) ?? Number.MAX_SAFE_INTEGER;
|
|
550
|
+
return orderA - orderB;
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
buildReferenceMap(graph) {
|
|
554
|
+
const map = /* @__PURE__ */ new Map();
|
|
555
|
+
for (const node of graph.nodes) {
|
|
556
|
+
if (node.references.length > 0) {
|
|
557
|
+
map.set(node.object, node.references);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return map;
|
|
561
|
+
}
|
|
562
|
+
async loadExistingRecords(objectName, externalId) {
|
|
563
|
+
const map = /* @__PURE__ */ new Map();
|
|
564
|
+
try {
|
|
565
|
+
const records = await this.engine.find(objectName, {
|
|
566
|
+
select: ["id", externalId]
|
|
567
|
+
});
|
|
568
|
+
for (const record of records || []) {
|
|
569
|
+
const key = String(record[externalId] ?? "");
|
|
570
|
+
if (key) {
|
|
571
|
+
map.set(key, record);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
return map;
|
|
577
|
+
}
|
|
578
|
+
looksLikeInternalId(value) {
|
|
579
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
if (/^[0-9a-f]{24}$/i.test(value)) {
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
extractId(record) {
|
|
588
|
+
if (!record) return void 0;
|
|
589
|
+
return String(record.id || record._id || "");
|
|
590
|
+
}
|
|
591
|
+
buildEmptyResult(config, durationMs) {
|
|
592
|
+
return {
|
|
593
|
+
success: true,
|
|
594
|
+
dryRun: config.dryRun,
|
|
595
|
+
dependencyGraph: { nodes: [], insertOrder: [], circularDependencies: [] },
|
|
596
|
+
results: [],
|
|
597
|
+
errors: [],
|
|
598
|
+
summary: {
|
|
599
|
+
objectsProcessed: 0,
|
|
600
|
+
totalRecords: 0,
|
|
601
|
+
totalInserted: 0,
|
|
602
|
+
totalUpdated: 0,
|
|
603
|
+
totalSkipped: 0,
|
|
604
|
+
totalErrored: 0,
|
|
605
|
+
totalReferencesResolved: 0,
|
|
606
|
+
totalReferencesDeferred: 0,
|
|
607
|
+
circularDependencyCount: 0,
|
|
608
|
+
durationMs
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
buildResult(config, graph, results, errors, durationMs) {
|
|
613
|
+
const summary = {
|
|
614
|
+
objectsProcessed: results.length,
|
|
615
|
+
totalRecords: results.reduce((sum, r) => sum + r.total, 0),
|
|
616
|
+
totalInserted: results.reduce((sum, r) => sum + r.inserted, 0),
|
|
617
|
+
totalUpdated: results.reduce((sum, r) => sum + r.updated, 0),
|
|
618
|
+
totalSkipped: results.reduce((sum, r) => sum + r.skipped, 0),
|
|
619
|
+
totalErrored: results.reduce((sum, r) => sum + r.errored, 0),
|
|
620
|
+
totalReferencesResolved: results.reduce((sum, r) => sum + r.referencesResolved, 0),
|
|
621
|
+
totalReferencesDeferred: results.reduce((sum, r) => sum + r.referencesDeferred, 0),
|
|
622
|
+
circularDependencyCount: graph.circularDependencies.length,
|
|
623
|
+
durationMs
|
|
624
|
+
};
|
|
625
|
+
const hasErrors = errors.length > 0 || summary.totalErrored > 0;
|
|
626
|
+
return {
|
|
627
|
+
success: !hasErrors,
|
|
628
|
+
dryRun: config.dryRun,
|
|
629
|
+
dependencyGraph: graph,
|
|
630
|
+
results,
|
|
631
|
+
errors,
|
|
632
|
+
summary
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
114
637
|
// src/app-plugin.ts
|
|
115
638
|
var AppPlugin = class {
|
|
116
639
|
constructor(bundle) {
|
|
@@ -180,20 +703,52 @@ var AppPlugin = class {
|
|
|
180
703
|
};
|
|
181
704
|
if (seedDatasets.length > 0) {
|
|
182
705
|
ctx.logger.info(`[AppPlugin] Found ${seedDatasets.length} seed datasets for ${appId}`);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
706
|
+
const normalizedDatasets = seedDatasets.filter((d) => d.object && Array.isArray(d.records)).map((d) => ({
|
|
707
|
+
...d,
|
|
708
|
+
object: toFQN(d.object)
|
|
709
|
+
}));
|
|
710
|
+
try {
|
|
711
|
+
const metadata = ctx.getService("metadata");
|
|
712
|
+
if (metadata) {
|
|
713
|
+
const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
|
|
714
|
+
const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
|
|
715
|
+
const request = SeedLoaderRequestSchema.parse({
|
|
716
|
+
datasets: normalizedDatasets,
|
|
717
|
+
config: { defaultMode: "upsert", multiPass: true }
|
|
718
|
+
});
|
|
719
|
+
const result = await seedLoader.load(request);
|
|
720
|
+
ctx.logger.info("[Seeder] Seed loading complete", {
|
|
721
|
+
inserted: result.summary.totalInserted,
|
|
722
|
+
updated: result.summary.totalUpdated,
|
|
723
|
+
errors: result.errors.length
|
|
724
|
+
});
|
|
725
|
+
} else {
|
|
726
|
+
ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
|
|
727
|
+
for (const dataset of normalizedDatasets) {
|
|
728
|
+
ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
|
|
729
|
+
for (const record of dataset.records) {
|
|
730
|
+
try {
|
|
731
|
+
await ql.insert(dataset.object, record);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
ctx.logger.info("[Seeder] Data seeding complete.");
|
|
738
|
+
}
|
|
739
|
+
} catch (err) {
|
|
740
|
+
ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
|
|
741
|
+
for (const dataset of normalizedDatasets) {
|
|
187
742
|
for (const record of dataset.records) {
|
|
188
743
|
try {
|
|
189
|
-
await ql.insert(
|
|
190
|
-
} catch (
|
|
191
|
-
ctx.logger.warn(`[Seeder] Failed to insert ${
|
|
744
|
+
await ql.insert(dataset.object, record);
|
|
745
|
+
} catch (insertErr) {
|
|
746
|
+
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
|
|
192
747
|
}
|
|
193
748
|
}
|
|
194
749
|
}
|
|
750
|
+
ctx.logger.info("[Seeder] Data seeding complete (fallback).");
|
|
195
751
|
}
|
|
196
|
-
ctx.logger.info("[Seeder] Data seeding complete.");
|
|
197
752
|
}
|
|
198
753
|
};
|
|
199
754
|
this.bundle = bundle;
|
|
@@ -601,21 +1156,21 @@ var HttpDispatcher = class {
|
|
|
601
1156
|
* Handles Analytics requests
|
|
602
1157
|
* path: sub-path after /analytics/
|
|
603
1158
|
*/
|
|
604
|
-
async handleAnalytics(path, method, body,
|
|
1159
|
+
async handleAnalytics(path, method, body, _context) {
|
|
605
1160
|
const analyticsService = await this.getService(import_system.CoreServiceName.enum.analytics);
|
|
606
1161
|
if (!analyticsService) return { handled: false };
|
|
607
1162
|
const m = method.toUpperCase();
|
|
608
1163
|
const subPath = path.replace(/^\/+/, "");
|
|
609
1164
|
if (subPath === "query" && m === "POST") {
|
|
610
|
-
const result = await analyticsService.query(body
|
|
1165
|
+
const result = await analyticsService.query(body);
|
|
611
1166
|
return { handled: true, response: this.success(result) };
|
|
612
1167
|
}
|
|
613
1168
|
if (subPath === "meta" && m === "GET") {
|
|
614
|
-
const result = await analyticsService.
|
|
1169
|
+
const result = await analyticsService.getMeta();
|
|
615
1170
|
return { handled: true, response: this.success(result) };
|
|
616
1171
|
}
|
|
617
1172
|
if (subPath === "sql" && m === "POST") {
|
|
618
|
-
const result = await analyticsService.generateSql(body
|
|
1173
|
+
const result = await analyticsService.generateSql(body);
|
|
619
1174
|
return { handled: true, response: this.success(result) };
|
|
620
1175
|
}
|
|
621
1176
|
return { handled: false };
|
|
@@ -1648,6 +2203,7 @@ __reExport(index_exports, require("@objectstack/core"), module.exports);
|
|
|
1648
2203
|
RouteGroupBuilder,
|
|
1649
2204
|
RouteManager,
|
|
1650
2205
|
Runtime,
|
|
2206
|
+
SeedLoaderService,
|
|
1651
2207
|
createDispatcherPlugin,
|
|
1652
2208
|
createRestApiPlugin,
|
|
1653
2209
|
...require("@objectstack/core")
|