@objectstack/objectql 8.0.1 → 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +177 -5
- package/dist/index.d.ts +177 -5
- package/dist/index.js +892 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +882 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,6 +30,775 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
30
|
));
|
|
28
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
32
|
|
|
33
|
+
// src/seed-loader.ts
|
|
34
|
+
var seed_loader_exports = {};
|
|
35
|
+
__export(seed_loader_exports, {
|
|
36
|
+
SeedLoaderService: () => SeedLoaderService
|
|
37
|
+
});
|
|
38
|
+
var import_data2, import_formula, DEFAULT_EXTERNAL_ID_FIELD, _SeedLoaderService, SeedLoaderService;
|
|
39
|
+
var init_seed_loader = __esm({
|
|
40
|
+
"src/seed-loader.ts"() {
|
|
41
|
+
"use strict";
|
|
42
|
+
import_data2 = require("@objectstack/spec/data");
|
|
43
|
+
import_formula = require("@objectstack/formula");
|
|
44
|
+
DEFAULT_EXTERNAL_ID_FIELD = "name";
|
|
45
|
+
_SeedLoaderService = class _SeedLoaderService {
|
|
46
|
+
constructor(engine, metadata, logger) {
|
|
47
|
+
this.engine = engine;
|
|
48
|
+
this.metadata = metadata;
|
|
49
|
+
this.logger = logger;
|
|
50
|
+
}
|
|
51
|
+
// ==========================================================================
|
|
52
|
+
// Public API
|
|
53
|
+
// ==========================================================================
|
|
54
|
+
async load(request) {
|
|
55
|
+
const startTime = Date.now();
|
|
56
|
+
const config = request.config;
|
|
57
|
+
const allErrors = [];
|
|
58
|
+
const allResults = [];
|
|
59
|
+
const datasets = this.filterByEnv(request.seeds, config.env);
|
|
60
|
+
if (datasets.length === 0) {
|
|
61
|
+
return this.buildEmptyResult(config, Date.now() - startTime);
|
|
62
|
+
}
|
|
63
|
+
const objectNames = datasets.map((d) => d.object);
|
|
64
|
+
const graph = await this.buildDependencyGraph(objectNames);
|
|
65
|
+
this.logger.info("[SeedLoader] Dependency graph built", {
|
|
66
|
+
objects: objectNames.length,
|
|
67
|
+
insertOrder: graph.insertOrder,
|
|
68
|
+
circularDeps: graph.circularDependencies.length
|
|
69
|
+
});
|
|
70
|
+
const orderedDatasets = this.orderDatasets(datasets, graph.insertOrder);
|
|
71
|
+
const refMap = this.buildReferenceMap(graph);
|
|
72
|
+
const insertedRecords = /* @__PURE__ */ new Map();
|
|
73
|
+
const deferredUpdates = [];
|
|
74
|
+
for (const dataset of orderedDatasets) {
|
|
75
|
+
const result = await this.loadDataset(
|
|
76
|
+
dataset,
|
|
77
|
+
config,
|
|
78
|
+
refMap,
|
|
79
|
+
insertedRecords,
|
|
80
|
+
deferredUpdates,
|
|
81
|
+
allErrors
|
|
82
|
+
);
|
|
83
|
+
allResults.push(result);
|
|
84
|
+
if (config.haltOnError && result.errored > 0) {
|
|
85
|
+
this.logger.warn("[SeedLoader] Halting on first error", { object: dataset.object });
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (config.multiPass && deferredUpdates.length > 0 && !config.dryRun) {
|
|
90
|
+
this.logger.info("[SeedLoader] Pass 2: resolving deferred references", {
|
|
91
|
+
count: deferredUpdates.length
|
|
92
|
+
});
|
|
93
|
+
await this.resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors, config.organizationId);
|
|
94
|
+
}
|
|
95
|
+
const durationMs = Date.now() - startTime;
|
|
96
|
+
return this.buildResult(config, graph, allResults, allErrors, durationMs);
|
|
97
|
+
}
|
|
98
|
+
async buildDependencyGraph(objectNames) {
|
|
99
|
+
const nodes = [];
|
|
100
|
+
const objectSet = new Set(objectNames);
|
|
101
|
+
for (const objectName of objectNames) {
|
|
102
|
+
const objDef = await this.metadata.getObject(objectName);
|
|
103
|
+
const dependsOn = [];
|
|
104
|
+
const references = [];
|
|
105
|
+
if (objDef && objDef.fields) {
|
|
106
|
+
const fields = objDef.fields;
|
|
107
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
108
|
+
if ((fieldDef.type === "lookup" || fieldDef.type === "master_detail") && fieldDef.reference) {
|
|
109
|
+
const targetObject = fieldDef.reference;
|
|
110
|
+
if (objectSet.has(targetObject) && !dependsOn.includes(targetObject)) {
|
|
111
|
+
dependsOn.push(targetObject);
|
|
112
|
+
}
|
|
113
|
+
references.push({
|
|
114
|
+
field: fieldName,
|
|
115
|
+
targetObject,
|
|
116
|
+
targetField: DEFAULT_EXTERNAL_ID_FIELD,
|
|
117
|
+
fieldType: fieldDef.type
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
nodes.push({ object: objectName, dependsOn, references });
|
|
123
|
+
}
|
|
124
|
+
const { insertOrder, circularDependencies } = this.topologicalSort(nodes);
|
|
125
|
+
return { nodes, insertOrder, circularDependencies };
|
|
126
|
+
}
|
|
127
|
+
async validate(datasets, config) {
|
|
128
|
+
const parsedConfig = import_data2.SeedLoaderConfigSchema.parse({ ...config, dryRun: true });
|
|
129
|
+
return this.load({ seeds: datasets, config: parsedConfig });
|
|
130
|
+
}
|
|
131
|
+
// ==========================================================================
|
|
132
|
+
// Internal: Seed Loading
|
|
133
|
+
// ==========================================================================
|
|
134
|
+
async loadDataset(dataset, config, refMap, insertedRecords, deferredUpdates, allErrors) {
|
|
135
|
+
const objectName = dataset.object;
|
|
136
|
+
const mode = dataset.mode || config.defaultMode;
|
|
137
|
+
const externalId = dataset.externalId || "name";
|
|
138
|
+
let inserted = 0;
|
|
139
|
+
let updated = 0;
|
|
140
|
+
let skipped = 0;
|
|
141
|
+
let errored = 0;
|
|
142
|
+
let referencesResolved = 0;
|
|
143
|
+
let referencesDeferred = 0;
|
|
144
|
+
const errors = [];
|
|
145
|
+
if (!insertedRecords.has(objectName)) {
|
|
146
|
+
insertedRecords.set(objectName, /* @__PURE__ */ new Map());
|
|
147
|
+
}
|
|
148
|
+
let existingRecords;
|
|
149
|
+
if ((mode === "upsert" || mode === "update" || mode === "ignore") && !config.dryRun) {
|
|
150
|
+
existingRecords = await this.loadExistingRecords(
|
|
151
|
+
objectName,
|
|
152
|
+
externalId,
|
|
153
|
+
config.organizationId
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const objectRefs = refMap.get(objectName) || [];
|
|
157
|
+
const seedNow = /* @__PURE__ */ new Date();
|
|
158
|
+
const seedIdentity = config.identity;
|
|
159
|
+
const baseEvalCtx = {
|
|
160
|
+
now: seedNow,
|
|
161
|
+
user: seedIdentity?.user,
|
|
162
|
+
// Fall back to the per-tenant organizationId so `os.org.id` resolves
|
|
163
|
+
// during per-org replay even without an explicit identity.org.
|
|
164
|
+
org: seedIdentity?.org ?? (config.organizationId ? { id: config.organizationId } : void 0),
|
|
165
|
+
env: config.env
|
|
166
|
+
};
|
|
167
|
+
for (let i = 0; i < dataset.records.length; i++) {
|
|
168
|
+
const seedResult = (0, import_formula.resolveSeedRecord)(
|
|
169
|
+
dataset.records[i],
|
|
170
|
+
baseEvalCtx
|
|
171
|
+
);
|
|
172
|
+
if (!seedResult.ok) {
|
|
173
|
+
errored++;
|
|
174
|
+
const error = {
|
|
175
|
+
sourceObject: objectName,
|
|
176
|
+
field: "(expression)",
|
|
177
|
+
targetObject: objectName,
|
|
178
|
+
targetField: "(expression)",
|
|
179
|
+
attemptedValue: dataset.records[i],
|
|
180
|
+
recordIndex: i,
|
|
181
|
+
message: `Cannot resolve dynamic seed values for ${objectName} record #${i}: ${seedResult.error.message}. Records using cel\`os.user.id\` / cel\`os.org.id\` require a seed identity \u2014 ensure a system/admin user exists before seeding (see SeedLoaderConfig.identity).`
|
|
182
|
+
};
|
|
183
|
+
errors.push(error);
|
|
184
|
+
allErrors.push(error);
|
|
185
|
+
this.logger.warn(`[SeedLoader] ${error.message}`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const record = { ...seedResult.value };
|
|
189
|
+
if (config.organizationId && record["organization_id"] == null) {
|
|
190
|
+
record["organization_id"] = config.organizationId;
|
|
191
|
+
}
|
|
192
|
+
for (const ref of objectRefs) {
|
|
193
|
+
const fieldValue = record[ref.field];
|
|
194
|
+
if (fieldValue === void 0 || fieldValue === null) continue;
|
|
195
|
+
if (typeof fieldValue === "object") {
|
|
196
|
+
const wrapped = fieldValue.externalId;
|
|
197
|
+
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.`;
|
|
198
|
+
const error = {
|
|
199
|
+
sourceObject: objectName,
|
|
200
|
+
field: ref.field,
|
|
201
|
+
targetObject: ref.targetObject,
|
|
202
|
+
targetField: ref.targetField,
|
|
203
|
+
attemptedValue: fieldValue,
|
|
204
|
+
recordIndex: i,
|
|
205
|
+
message: `Invalid reference for ${objectName}.${ref.field}: expected a ${ref.targetObject}.${ref.targetField} natural-key string but got an object.${hint}`
|
|
206
|
+
};
|
|
207
|
+
errors.push(error);
|
|
208
|
+
allErrors.push(error);
|
|
209
|
+
this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i });
|
|
210
|
+
record[ref.field] = null;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (typeof fieldValue !== "string" || this.looksLikeInternalId(fieldValue)) continue;
|
|
214
|
+
const targetMap = insertedRecords.get(ref.targetObject);
|
|
215
|
+
const resolvedId = targetMap?.get(String(fieldValue));
|
|
216
|
+
if (resolvedId) {
|
|
217
|
+
record[ref.field] = resolvedId;
|
|
218
|
+
referencesResolved++;
|
|
219
|
+
} else if (!config.dryRun) {
|
|
220
|
+
const dbId = await this.resolveFromDatabase(ref.targetObject, ref.targetField, fieldValue, config.organizationId);
|
|
221
|
+
if (dbId) {
|
|
222
|
+
record[ref.field] = dbId;
|
|
223
|
+
referencesResolved++;
|
|
224
|
+
} else if (config.multiPass) {
|
|
225
|
+
record[ref.field] = null;
|
|
226
|
+
deferredUpdates.push({
|
|
227
|
+
objectName,
|
|
228
|
+
recordExternalId: String(record[externalId] ?? ""),
|
|
229
|
+
field: ref.field,
|
|
230
|
+
targetObject: ref.targetObject,
|
|
231
|
+
targetField: ref.targetField,
|
|
232
|
+
attemptedValue: fieldValue,
|
|
233
|
+
recordIndex: i
|
|
234
|
+
});
|
|
235
|
+
referencesDeferred++;
|
|
236
|
+
} else {
|
|
237
|
+
const error = {
|
|
238
|
+
sourceObject: objectName,
|
|
239
|
+
field: ref.field,
|
|
240
|
+
targetObject: ref.targetObject,
|
|
241
|
+
targetField: ref.targetField,
|
|
242
|
+
attemptedValue: fieldValue,
|
|
243
|
+
recordIndex: i,
|
|
244
|
+
message: `Cannot resolve reference: ${objectName}.${ref.field} = '${fieldValue}' \u2192 ${ref.targetObject}.${ref.targetField} not found`
|
|
245
|
+
};
|
|
246
|
+
errors.push(error);
|
|
247
|
+
allErrors.push(error);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
const targetMap2 = insertedRecords.get(ref.targetObject);
|
|
251
|
+
if (!targetMap2?.has(String(fieldValue))) {
|
|
252
|
+
const error = {
|
|
253
|
+
sourceObject: objectName,
|
|
254
|
+
field: ref.field,
|
|
255
|
+
targetObject: ref.targetObject,
|
|
256
|
+
targetField: ref.targetField,
|
|
257
|
+
attemptedValue: fieldValue,
|
|
258
|
+
recordIndex: i,
|
|
259
|
+
message: `[dry-run] Reference may not resolve: ${objectName}.${ref.field} = '${fieldValue}' \u2192 ${ref.targetObject}.${ref.targetField}`
|
|
260
|
+
};
|
|
261
|
+
errors.push(error);
|
|
262
|
+
allErrors.push(error);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!config.dryRun) {
|
|
267
|
+
try {
|
|
268
|
+
const result = await this.writeRecord(
|
|
269
|
+
objectName,
|
|
270
|
+
record,
|
|
271
|
+
mode,
|
|
272
|
+
externalId,
|
|
273
|
+
existingRecords
|
|
274
|
+
);
|
|
275
|
+
if (result.action === "inserted") inserted++;
|
|
276
|
+
else if (result.action === "updated") updated++;
|
|
277
|
+
else if (result.action === "skipped") skipped++;
|
|
278
|
+
const externalIdValue = String(record[externalId] ?? "");
|
|
279
|
+
const internalId = result.id;
|
|
280
|
+
if (externalIdValue && internalId) {
|
|
281
|
+
insertedRecords.get(objectName).set(externalIdValue, String(internalId));
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
errored++;
|
|
285
|
+
const error = {
|
|
286
|
+
sourceObject: objectName,
|
|
287
|
+
field: "(write)",
|
|
288
|
+
targetObject: objectName,
|
|
289
|
+
targetField: externalId,
|
|
290
|
+
attemptedValue: record[externalId] ?? null,
|
|
291
|
+
recordIndex: i,
|
|
292
|
+
message: `Failed to write ${objectName} record #${i} (${externalId}=${String(record[externalId] ?? "")}): ${err.message}`
|
|
293
|
+
};
|
|
294
|
+
errors.push(error);
|
|
295
|
+
allErrors.push(error);
|
|
296
|
+
this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i });
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
const externalIdValue = String(record[externalId] ?? "");
|
|
300
|
+
if (externalIdValue) {
|
|
301
|
+
insertedRecords.get(objectName).set(externalIdValue, `dry-run-id-${i}`);
|
|
302
|
+
}
|
|
303
|
+
inserted++;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
object: objectName,
|
|
308
|
+
mode,
|
|
309
|
+
inserted,
|
|
310
|
+
updated,
|
|
311
|
+
skipped,
|
|
312
|
+
errored,
|
|
313
|
+
total: dataset.records.length,
|
|
314
|
+
referencesResolved,
|
|
315
|
+
referencesDeferred,
|
|
316
|
+
errors
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
// ==========================================================================
|
|
320
|
+
// Internal: Reference Resolution
|
|
321
|
+
// ==========================================================================
|
|
322
|
+
async resolveFromDatabase(targetObject, targetField, value, organizationId) {
|
|
323
|
+
try {
|
|
324
|
+
const where = { [targetField]: value };
|
|
325
|
+
if (organizationId) where.organization_id = organizationId;
|
|
326
|
+
const records = await this.engine.find(targetObject, {
|
|
327
|
+
where,
|
|
328
|
+
fields: ["id"],
|
|
329
|
+
limit: 1,
|
|
330
|
+
context: { isSystem: true }
|
|
331
|
+
});
|
|
332
|
+
if (records && records.length > 0) {
|
|
333
|
+
return String(records[0].id || records[0]._id);
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
async resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors, organizationId) {
|
|
340
|
+
for (const deferred of deferredUpdates) {
|
|
341
|
+
const targetMap = insertedRecords.get(deferred.targetObject);
|
|
342
|
+
let resolvedId = targetMap?.get(String(deferred.attemptedValue));
|
|
343
|
+
if (!resolvedId) {
|
|
344
|
+
resolvedId = await this.resolveFromDatabase(
|
|
345
|
+
deferred.targetObject,
|
|
346
|
+
deferred.targetField,
|
|
347
|
+
deferred.attemptedValue,
|
|
348
|
+
organizationId
|
|
349
|
+
) ?? void 0;
|
|
350
|
+
}
|
|
351
|
+
if (resolvedId) {
|
|
352
|
+
const objectRecordMap = insertedRecords.get(deferred.objectName);
|
|
353
|
+
const recordId = objectRecordMap?.get(deferred.recordExternalId);
|
|
354
|
+
if (recordId) {
|
|
355
|
+
try {
|
|
356
|
+
await this.engine.update(deferred.objectName, {
|
|
357
|
+
id: recordId,
|
|
358
|
+
[deferred.field]: resolvedId
|
|
359
|
+
}, { context: { isSystem: true } });
|
|
360
|
+
const resultEntry = allResults.find((r) => r.object === deferred.objectName);
|
|
361
|
+
if (resultEntry) {
|
|
362
|
+
resultEntry.referencesResolved++;
|
|
363
|
+
resultEntry.referencesDeferred--;
|
|
364
|
+
}
|
|
365
|
+
} catch (err) {
|
|
366
|
+
this.logger.warn("[SeedLoader] Failed to resolve deferred reference", {
|
|
367
|
+
object: deferred.objectName,
|
|
368
|
+
field: deferred.field,
|
|
369
|
+
error: err.message
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
const error = {
|
|
375
|
+
sourceObject: deferred.objectName,
|
|
376
|
+
field: deferred.field,
|
|
377
|
+
targetObject: deferred.targetObject,
|
|
378
|
+
targetField: deferred.targetField,
|
|
379
|
+
attemptedValue: deferred.attemptedValue,
|
|
380
|
+
recordIndex: deferred.recordIndex,
|
|
381
|
+
message: `Deferred reference unresolved after pass 2: ${deferred.objectName}.${deferred.field} = '${deferred.attemptedValue}' \u2192 ${deferred.targetObject}.${deferred.targetField} not found`
|
|
382
|
+
};
|
|
383
|
+
const resultEntry = allResults.find((r) => r.object === deferred.objectName);
|
|
384
|
+
if (resultEntry) {
|
|
385
|
+
resultEntry.errors.push(error);
|
|
386
|
+
}
|
|
387
|
+
allErrors.push(error);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async writeRecord(objectName, record, mode, externalId, existingRecords) {
|
|
392
|
+
const externalIdValue = record[externalId];
|
|
393
|
+
const existing = existingRecords?.get(String(externalIdValue ?? ""));
|
|
394
|
+
const opts = _SeedLoaderService.SEED_OPTIONS;
|
|
395
|
+
switch (mode) {
|
|
396
|
+
case "insert": {
|
|
397
|
+
const result = await this.engine.insert(objectName, record, opts);
|
|
398
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
399
|
+
}
|
|
400
|
+
case "update": {
|
|
401
|
+
if (!existing) {
|
|
402
|
+
return { action: "skipped" };
|
|
403
|
+
}
|
|
404
|
+
const id = this.extractId(existing);
|
|
405
|
+
await this.engine.update(objectName, { ...record, id }, opts);
|
|
406
|
+
return { action: "updated", id };
|
|
407
|
+
}
|
|
408
|
+
case "upsert": {
|
|
409
|
+
if (existing) {
|
|
410
|
+
const id = this.extractId(existing);
|
|
411
|
+
await this.engine.update(objectName, { ...record, id }, opts);
|
|
412
|
+
return { action: "updated", id };
|
|
413
|
+
} else {
|
|
414
|
+
const result = await this.engine.insert(objectName, record, opts);
|
|
415
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
case "ignore": {
|
|
419
|
+
if (existing) {
|
|
420
|
+
return { action: "skipped", id: this.extractId(existing) };
|
|
421
|
+
}
|
|
422
|
+
const result = await this.engine.insert(objectName, record, opts);
|
|
423
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
424
|
+
}
|
|
425
|
+
case "replace": {
|
|
426
|
+
const result = await this.engine.insert(objectName, record, opts);
|
|
427
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
428
|
+
}
|
|
429
|
+
default: {
|
|
430
|
+
const result = await this.engine.insert(objectName, record, opts);
|
|
431
|
+
return { action: "inserted", id: this.extractId(result) };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// ==========================================================================
|
|
436
|
+
// Internal: Dependency Graph
|
|
437
|
+
// ==========================================================================
|
|
438
|
+
/**
|
|
439
|
+
* Kahn's algorithm for topological sort with cycle detection.
|
|
440
|
+
*/
|
|
441
|
+
topologicalSort(nodes) {
|
|
442
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
443
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
444
|
+
const objectSet = new Set(nodes.map((n) => n.object));
|
|
445
|
+
for (const node of nodes) {
|
|
446
|
+
inDegree.set(node.object, 0);
|
|
447
|
+
adjacency.set(node.object, []);
|
|
448
|
+
}
|
|
449
|
+
for (const node of nodes) {
|
|
450
|
+
for (const dep of node.dependsOn) {
|
|
451
|
+
if (objectSet.has(dep) && dep !== node.object) {
|
|
452
|
+
adjacency.get(dep).push(node.object);
|
|
453
|
+
inDegree.set(node.object, (inDegree.get(node.object) || 0) + 1);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const queue = [];
|
|
458
|
+
for (const [obj, degree] of inDegree) {
|
|
459
|
+
if (degree === 0) queue.push(obj);
|
|
460
|
+
}
|
|
461
|
+
const insertOrder = [];
|
|
462
|
+
while (queue.length > 0) {
|
|
463
|
+
const current = queue.shift();
|
|
464
|
+
insertOrder.push(current);
|
|
465
|
+
for (const neighbor of adjacency.get(current) || []) {
|
|
466
|
+
const newDegree = (inDegree.get(neighbor) || 0) - 1;
|
|
467
|
+
inDegree.set(neighbor, newDegree);
|
|
468
|
+
if (newDegree === 0) {
|
|
469
|
+
queue.push(neighbor);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const circularDependencies = [];
|
|
474
|
+
const remaining = nodes.filter((n) => !insertOrder.includes(n.object));
|
|
475
|
+
if (remaining.length > 0) {
|
|
476
|
+
const cycles = this.findCycles(remaining);
|
|
477
|
+
circularDependencies.push(...cycles);
|
|
478
|
+
for (const node of remaining) {
|
|
479
|
+
if (!insertOrder.includes(node.object)) {
|
|
480
|
+
insertOrder.push(node.object);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return { insertOrder, circularDependencies };
|
|
485
|
+
}
|
|
486
|
+
findCycles(nodes) {
|
|
487
|
+
const cycles = [];
|
|
488
|
+
const nodeMap = new Map(nodes.map((n) => [n.object, n]));
|
|
489
|
+
const visited = /* @__PURE__ */ new Set();
|
|
490
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
491
|
+
const dfs = (current, path) => {
|
|
492
|
+
if (inStack.has(current)) {
|
|
493
|
+
const cycleStart = path.indexOf(current);
|
|
494
|
+
if (cycleStart !== -1) {
|
|
495
|
+
cycles.push([...path.slice(cycleStart), current]);
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (visited.has(current)) return;
|
|
500
|
+
visited.add(current);
|
|
501
|
+
inStack.add(current);
|
|
502
|
+
path.push(current);
|
|
503
|
+
const node = nodeMap.get(current);
|
|
504
|
+
if (node) {
|
|
505
|
+
for (const dep of node.dependsOn) {
|
|
506
|
+
if (nodeMap.has(dep)) {
|
|
507
|
+
dfs(dep, [...path]);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
inStack.delete(current);
|
|
512
|
+
};
|
|
513
|
+
for (const node of nodes) {
|
|
514
|
+
if (!visited.has(node.object)) {
|
|
515
|
+
dfs(node.object, []);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return cycles;
|
|
519
|
+
}
|
|
520
|
+
// ==========================================================================
|
|
521
|
+
// Internal: Helpers
|
|
522
|
+
// ==========================================================================
|
|
523
|
+
filterByEnv(datasets, env) {
|
|
524
|
+
if (!env) return datasets;
|
|
525
|
+
return datasets.filter((d) => d.env.includes(env));
|
|
526
|
+
}
|
|
527
|
+
orderDatasets(datasets, insertOrder) {
|
|
528
|
+
const orderMap = new Map(insertOrder.map((name, i) => [name, i]));
|
|
529
|
+
return [...datasets].sort((a, b) => {
|
|
530
|
+
const orderA = orderMap.get(a.object) ?? Number.MAX_SAFE_INTEGER;
|
|
531
|
+
const orderB = orderMap.get(b.object) ?? Number.MAX_SAFE_INTEGER;
|
|
532
|
+
return orderA - orderB;
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
buildReferenceMap(graph) {
|
|
536
|
+
const map = /* @__PURE__ */ new Map();
|
|
537
|
+
for (const node of graph.nodes) {
|
|
538
|
+
if (node.references.length > 0) {
|
|
539
|
+
map.set(node.object, node.references);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return map;
|
|
543
|
+
}
|
|
544
|
+
async loadExistingRecords(objectName, externalId, organizationId) {
|
|
545
|
+
const map = /* @__PURE__ */ new Map();
|
|
546
|
+
try {
|
|
547
|
+
const findArgs = {
|
|
548
|
+
fields: ["id", externalId],
|
|
549
|
+
context: { isSystem: true }
|
|
550
|
+
};
|
|
551
|
+
if (organizationId) findArgs.where = { organization_id: organizationId };
|
|
552
|
+
const records = await this.engine.find(objectName, findArgs);
|
|
553
|
+
for (const record of records || []) {
|
|
554
|
+
const key = String(record[externalId] ?? "");
|
|
555
|
+
if (key) {
|
|
556
|
+
map.set(key, record);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
return map;
|
|
562
|
+
}
|
|
563
|
+
looksLikeInternalId(value) {
|
|
564
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
if (/^[0-9a-f]{24}$/i.test(value)) {
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
extractId(record) {
|
|
573
|
+
if (!record) return void 0;
|
|
574
|
+
return String(record.id || record._id || "");
|
|
575
|
+
}
|
|
576
|
+
buildEmptyResult(config, durationMs) {
|
|
577
|
+
return {
|
|
578
|
+
success: true,
|
|
579
|
+
dryRun: config.dryRun,
|
|
580
|
+
dependencyGraph: { nodes: [], insertOrder: [], circularDependencies: [] },
|
|
581
|
+
results: [],
|
|
582
|
+
errors: [],
|
|
583
|
+
summary: {
|
|
584
|
+
objectsProcessed: 0,
|
|
585
|
+
totalRecords: 0,
|
|
586
|
+
totalInserted: 0,
|
|
587
|
+
totalUpdated: 0,
|
|
588
|
+
totalSkipped: 0,
|
|
589
|
+
totalErrored: 0,
|
|
590
|
+
totalReferencesResolved: 0,
|
|
591
|
+
totalReferencesDeferred: 0,
|
|
592
|
+
circularDependencyCount: 0,
|
|
593
|
+
durationMs
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
buildResult(config, graph, results, errors, durationMs) {
|
|
598
|
+
const summary = {
|
|
599
|
+
objectsProcessed: results.length,
|
|
600
|
+
totalRecords: results.reduce((sum, r) => sum + r.total, 0),
|
|
601
|
+
totalInserted: results.reduce((sum, r) => sum + r.inserted, 0),
|
|
602
|
+
totalUpdated: results.reduce((sum, r) => sum + r.updated, 0),
|
|
603
|
+
totalSkipped: results.reduce((sum, r) => sum + r.skipped, 0),
|
|
604
|
+
totalErrored: results.reduce((sum, r) => sum + r.errored, 0),
|
|
605
|
+
totalReferencesResolved: results.reduce((sum, r) => sum + r.referencesResolved, 0),
|
|
606
|
+
totalReferencesDeferred: results.reduce((sum, r) => sum + r.referencesDeferred, 0),
|
|
607
|
+
circularDependencyCount: graph.circularDependencies.length,
|
|
608
|
+
durationMs
|
|
609
|
+
};
|
|
610
|
+
const hasErrors = errors.length > 0 || summary.totalErrored > 0;
|
|
611
|
+
return {
|
|
612
|
+
success: !hasErrors,
|
|
613
|
+
dryRun: config.dryRun,
|
|
614
|
+
dependencyGraph: graph,
|
|
615
|
+
results,
|
|
616
|
+
errors,
|
|
617
|
+
summary
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
// ==========================================================================
|
|
622
|
+
// Internal: Write Operations
|
|
623
|
+
// ==========================================================================
|
|
624
|
+
/**
|
|
625
|
+
* Seed writes always run as a privileged system context. This bypasses
|
|
626
|
+
* RBAC checks (so seeds can target system tables like `sys_*`) and
|
|
627
|
+
* disables the SecurityPlugin's auto-injection of `organization_id` /
|
|
628
|
+
* `owner_id` — seeds either declare those fields explicitly per
|
|
629
|
+
* record, or are intentionally cross-tenant / global.
|
|
630
|
+
*/
|
|
631
|
+
_SeedLoaderService.SEED_OPTIONS = { context: { isSystem: true } };
|
|
632
|
+
SeedLoaderService = _SeedLoaderService;
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// src/build-probes.ts
|
|
637
|
+
var build_probes_exports = {};
|
|
638
|
+
__export(build_probes_exports, {
|
|
639
|
+
runBuildProbes: () => runBuildProbes
|
|
640
|
+
});
|
|
641
|
+
function asRec(v) {
|
|
642
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
643
|
+
}
|
|
644
|
+
function asArr(v) {
|
|
645
|
+
return Array.isArray(v) ? v : [];
|
|
646
|
+
}
|
|
647
|
+
function resultRows(result) {
|
|
648
|
+
if (Array.isArray(result)) return result;
|
|
649
|
+
const r = asRec(result);
|
|
650
|
+
if (!r) return [];
|
|
651
|
+
if (Array.isArray(r.rows)) return r.rows;
|
|
652
|
+
if (Array.isArray(r.data)) return r.data;
|
|
653
|
+
return [];
|
|
654
|
+
}
|
|
655
|
+
async function hasRows(engine, objectName, organizationId) {
|
|
656
|
+
const rows = await engine.find(objectName, {
|
|
657
|
+
fields: ["id"],
|
|
658
|
+
limit: 1,
|
|
659
|
+
...organizationId ? { where: { organization_id: organizationId } } : {},
|
|
660
|
+
context: { isSystem: true }
|
|
661
|
+
});
|
|
662
|
+
return Array.isArray(rows) && rows.length > 0;
|
|
663
|
+
}
|
|
664
|
+
async function runBuildProbes(opts) {
|
|
665
|
+
const issues = [];
|
|
666
|
+
const checked = { seeds: 0, views: 0, widgets: 0 };
|
|
667
|
+
const { engine, getItem, published, analytics, organizationId } = opts;
|
|
668
|
+
const itemCache = /* @__PURE__ */ new Map();
|
|
669
|
+
const readItem = async (type, name) => {
|
|
670
|
+
const key = `${type} ${name}`;
|
|
671
|
+
if (itemCache.has(key)) return itemCache.get(key);
|
|
672
|
+
let item;
|
|
673
|
+
try {
|
|
674
|
+
item = await getItem(type, name);
|
|
675
|
+
} catch {
|
|
676
|
+
item = void 0;
|
|
677
|
+
}
|
|
678
|
+
itemCache.set(key, item);
|
|
679
|
+
return item;
|
|
680
|
+
};
|
|
681
|
+
for (const p of published.filter((x) => x.type === "seed")) {
|
|
682
|
+
const body = asRec(await readItem("seed", p.name));
|
|
683
|
+
const objectName = typeof body?.object === "string" ? body.object : void 0;
|
|
684
|
+
if (!objectName) continue;
|
|
685
|
+
checked.seeds += 1;
|
|
686
|
+
try {
|
|
687
|
+
if (!await hasRows(engine, objectName, organizationId)) {
|
|
688
|
+
issues.push({
|
|
689
|
+
layer: "runtime",
|
|
690
|
+
severity: "error",
|
|
691
|
+
artifact: { type: "seed", name: p.name },
|
|
692
|
+
ref: { type: "object", name: objectName },
|
|
693
|
+
code: "seed_not_applied",
|
|
694
|
+
message: `Seed "${p.name}" was published but object "${objectName}" has no rows \u2014 the sample data never materialized.`,
|
|
695
|
+
fix: `Check the publish response's seedApplied for the load error, fix the seed rows (field names/types), and republish the seed.`
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
} catch (e) {
|
|
699
|
+
issues.push({
|
|
700
|
+
layer: "runtime",
|
|
701
|
+
severity: "error",
|
|
702
|
+
artifact: { type: "seed", name: p.name },
|
|
703
|
+
ref: { type: "object", name: objectName },
|
|
704
|
+
code: "seed_not_applied",
|
|
705
|
+
message: `Seed "${p.name}" probe could not read object "${objectName}": ${String(e?.message ?? e)}`
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
for (const p of published.filter((x) => x.type === "view")) {
|
|
710
|
+
const body = asRec(await readItem("view", p.name));
|
|
711
|
+
const config = asRec(body?.config);
|
|
712
|
+
const dataObj = asRec(config?.data)?.object;
|
|
713
|
+
const objectName = typeof body?.object === "string" ? body.object : typeof dataObj === "string" ? dataObj : void 0;
|
|
714
|
+
if (!objectName) continue;
|
|
715
|
+
checked.views += 1;
|
|
716
|
+
try {
|
|
717
|
+
await engine.find(objectName, {
|
|
718
|
+
fields: ["id"],
|
|
719
|
+
limit: 1,
|
|
720
|
+
...organizationId ? { where: { organization_id: organizationId } } : {},
|
|
721
|
+
context: { isSystem: true }
|
|
722
|
+
});
|
|
723
|
+
} catch (e) {
|
|
724
|
+
issues.push({
|
|
725
|
+
layer: "runtime",
|
|
726
|
+
severity: "error",
|
|
727
|
+
artifact: { type: "view", name: p.name },
|
|
728
|
+
ref: { type: "object", name: objectName },
|
|
729
|
+
code: "view_read_failed",
|
|
730
|
+
message: `View "${p.name}" cannot read object "${objectName}": ${String(e?.message ?? e)} \u2014 it will render as an error for every user.`,
|
|
731
|
+
fix: `Verify object "${objectName}" published successfully (its table must exist) and that the view's binding is correct.`
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const dashboards = published.filter((x) => x.type === "dashboard");
|
|
736
|
+
let widgetsToProbe = 0;
|
|
737
|
+
for (const p of dashboards) {
|
|
738
|
+
const body = asRec(await readItem("dashboard", p.name));
|
|
739
|
+
const widgets = asArr(body?.widgets).map(asRec).filter((w) => !!w);
|
|
740
|
+
const datasetBound = widgets.filter((w) => typeof w.dataset === "string" && w.dataset);
|
|
741
|
+
widgetsToProbe += datasetBound.length;
|
|
742
|
+
if (!analytics || typeof analytics.queryDataset !== "function") continue;
|
|
743
|
+
for (const w of datasetBound) {
|
|
744
|
+
const widgetId = String(w.id ?? w.title ?? "?");
|
|
745
|
+
const dsName = w.dataset;
|
|
746
|
+
const dataset = asRec(await readItem("dataset", dsName));
|
|
747
|
+
if (!dataset) continue;
|
|
748
|
+
checked.widgets += 1;
|
|
749
|
+
const measures = asArr(w.values).filter((v) => typeof v === "string" && v.length > 0);
|
|
750
|
+
const firstMeasure = asRec(asArr(dataset.measures)[0])?.name;
|
|
751
|
+
const selection = {
|
|
752
|
+
measures: measures.length ? measures : typeof firstMeasure === "string" ? [firstMeasure] : [],
|
|
753
|
+
dimensions: [],
|
|
754
|
+
limit: 1
|
|
755
|
+
};
|
|
756
|
+
if (selection.measures.length === 0) continue;
|
|
757
|
+
const objectName = typeof dataset.object === "string" ? dataset.object : void 0;
|
|
758
|
+
try {
|
|
759
|
+
const result = await analytics.queryDataset(dataset, selection, void 0);
|
|
760
|
+
const rows = resultRows(result);
|
|
761
|
+
if (rows.length === 0 && objectName && await hasRows(engine, objectName, organizationId)) {
|
|
762
|
+
issues.push({
|
|
763
|
+
layer: "runtime",
|
|
764
|
+
severity: "error",
|
|
765
|
+
artifact: { type: "dashboard", name: p.name },
|
|
766
|
+
ref: { type: "dataset", name: dsName, member: widgetId },
|
|
767
|
+
code: "empty_query",
|
|
768
|
+
message: `Dashboard "${p.name}" widget "${widgetId}" returns NO data from dataset "${dsName}" although object "${objectName}" has rows \u2014 the widget will render empty for every user.`,
|
|
769
|
+
fix: `Run the dataset query directly to see the compiled strategy/SQL; check the dataset's measure/dimension field bindings against object "${objectName}".`
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
} catch (e) {
|
|
773
|
+
issues.push({
|
|
774
|
+
layer: "runtime",
|
|
775
|
+
severity: "error",
|
|
776
|
+
artifact: { type: "dashboard", name: p.name },
|
|
777
|
+
ref: { type: "dataset", name: dsName, member: widgetId },
|
|
778
|
+
code: "widget_query_failed",
|
|
779
|
+
message: `Dashboard "${p.name}" widget "${widgetId}" query against dataset "${dsName}" failed: ${String(e?.message ?? e)}`,
|
|
780
|
+
fix: `Fix the dataset definition (or the widget's values/dimensions) so the query compiles, then republish.`
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (widgetsToProbe > 0 && (!analytics || typeof analytics.queryDataset !== "function")) {
|
|
786
|
+
issues.push({
|
|
787
|
+
layer: "runtime",
|
|
788
|
+
severity: "warning",
|
|
789
|
+
artifact: { type: "dashboard", name: dashboards.map((d) => d.name).join(", ") },
|
|
790
|
+
code: "probes_unavailable",
|
|
791
|
+
message: `${widgetsToProbe} dashboard widget(s) could not be probed: no analytics service is mounted on this kernel.`
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return { issues, checked };
|
|
795
|
+
}
|
|
796
|
+
var init_build_probes = __esm({
|
|
797
|
+
"src/build-probes.ts"() {
|
|
798
|
+
"use strict";
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
|
|
30
802
|
// src/index.ts
|
|
31
803
|
var index_exports = {};
|
|
32
804
|
__export(index_exports, {
|
|
@@ -43,6 +815,7 @@ __export(index_exports, {
|
|
|
43
815
|
SECRET_REF_PREFIX: () => SECRET_REF_PREFIX,
|
|
44
816
|
SchemaRegistry: () => SchemaRegistry,
|
|
45
817
|
ScopedContext: () => ScopedContext,
|
|
818
|
+
SeedLoaderService: () => SeedLoaderService,
|
|
46
819
|
SysMetadataRepository: () => SysMetadataRepository,
|
|
47
820
|
ValidationError: () => ValidationError,
|
|
48
821
|
applyInMemoryAggregation: () => applyInMemoryAggregation,
|
|
@@ -61,6 +834,7 @@ __export(index_exports, {
|
|
|
61
834
|
noopHookMetricsRecorder: () => noopHookMetricsRecorder,
|
|
62
835
|
parseFQN: () => parseFQN,
|
|
63
836
|
parseSecretRef: () => parseSecretRef,
|
|
837
|
+
runBuildProbes: () => runBuildProbes,
|
|
64
838
|
toTitleCase: () => toTitleCase,
|
|
65
839
|
validateRecord: () => validateRecord,
|
|
66
840
|
wrapDeclarativeHook: () => wrapDeclarativeHook
|
|
@@ -1568,7 +2342,7 @@ var SysMetadataRepository = class {
|
|
|
1568
2342
|
|
|
1569
2343
|
// src/protocol.ts
|
|
1570
2344
|
var import_metadata_core2 = require("@objectstack/metadata-core");
|
|
1571
|
-
var
|
|
2345
|
+
var import_data3 = require("@objectstack/spec/data");
|
|
1572
2346
|
var import_shared4 = require("@objectstack/spec/shared");
|
|
1573
2347
|
var import_ui2 = require("@objectstack/spec/ui");
|
|
1574
2348
|
var import_system = require("@objectstack/spec/system");
|
|
@@ -2937,8 +3711,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2937
3711
|
} catch {
|
|
2938
3712
|
}
|
|
2939
3713
|
}
|
|
2940
|
-
if ((0,
|
|
2941
|
-
parsedFilter = (0,
|
|
3714
|
+
if ((0, import_data3.isFilterAST)(parsedFilter)) {
|
|
3715
|
+
parsedFilter = (0, import_data3.parseFilterAST)(parsedFilter);
|
|
2942
3716
|
}
|
|
2943
3717
|
options.where = parsedFilter;
|
|
2944
3718
|
}
|
|
@@ -4279,12 +5053,16 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4279
5053
|
item: result.item.body
|
|
4280
5054
|
});
|
|
4281
5055
|
await this.ensureObjectStorage(request.type, request.name);
|
|
4282
|
-
|
|
5056
|
+
const response = {
|
|
4283
5057
|
success: true,
|
|
4284
5058
|
version: result.version,
|
|
4285
5059
|
seq: result.seq,
|
|
4286
5060
|
message: `Published draft \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
|
|
4287
5061
|
};
|
|
5062
|
+
if (singularType === "seed" && !request._skipSeedApply) {
|
|
5063
|
+
response.seedApplied = await this.applySeedBodies([result.item.body], orgId);
|
|
5064
|
+
}
|
|
5065
|
+
return response;
|
|
4288
5066
|
} catch (err) {
|
|
4289
5067
|
if (err instanceof import_metadata_core2.ConflictError) {
|
|
4290
5068
|
const conflict = new Error(
|
|
@@ -4299,6 +5077,58 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4299
5077
|
throw err;
|
|
4300
5078
|
}
|
|
4301
5079
|
}
|
|
5080
|
+
/**
|
|
5081
|
+
* Materialize published `seed` bodies into data rows via the SeedLoaderService
|
|
5082
|
+
* (externalId-keyed upsert, multi-pass for cross-seed references). Passing ALL
|
|
5083
|
+
* of a publish's seed bodies in ONE call lets a child seed reference a parent
|
|
5084
|
+
* seed's rows regardless of publish order. Best-effort: any failure is
|
|
5085
|
+
* returned, never thrown — publishing metadata must not be blocked by a data
|
|
5086
|
+
* problem, but the caller surfaces `seedApplied` so the failure is LOUD.
|
|
5087
|
+
*/
|
|
5088
|
+
async applySeedBodies(bodies, organizationId) {
|
|
5089
|
+
try {
|
|
5090
|
+
const seeds = bodies.filter(
|
|
5091
|
+
(b) => b && typeof b.object === "string" && Array.isArray(b.records)
|
|
5092
|
+
);
|
|
5093
|
+
if (seeds.length === 0) {
|
|
5094
|
+
return { success: false, inserted: 0, updated: 0, error: "seed apply: no readable seed bodies" };
|
|
5095
|
+
}
|
|
5096
|
+
const { SeedLoaderService: SeedLoaderService2 } = await Promise.resolve().then(() => (init_seed_loader(), seed_loader_exports));
|
|
5097
|
+
const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
|
|
5098
|
+
const metadataAdapter = {
|
|
5099
|
+
getObject: async (name) => {
|
|
5100
|
+
const wrapper = await this.getMetaItem({
|
|
5101
|
+
type: "object",
|
|
5102
|
+
name,
|
|
5103
|
+
...organizationId ? { organizationId } : {}
|
|
5104
|
+
});
|
|
5105
|
+
return wrapper?.item ?? wrapper ?? null;
|
|
5106
|
+
}
|
|
5107
|
+
};
|
|
5108
|
+
const loader = new SeedLoaderService2(
|
|
5109
|
+
this.engine,
|
|
5110
|
+
metadataAdapter,
|
|
5111
|
+
console
|
|
5112
|
+
);
|
|
5113
|
+
const request = SeedLoaderRequestSchema.parse({
|
|
5114
|
+
seeds,
|
|
5115
|
+
config: {
|
|
5116
|
+
defaultMode: "upsert",
|
|
5117
|
+
multiPass: true,
|
|
5118
|
+
...organizationId ? { organizationId } : {}
|
|
5119
|
+
}
|
|
5120
|
+
});
|
|
5121
|
+
const r = await loader.load(request);
|
|
5122
|
+
return {
|
|
5123
|
+
success: r.success,
|
|
5124
|
+
inserted: r.summary.totalInserted,
|
|
5125
|
+
updated: r.summary.totalUpdated,
|
|
5126
|
+
...r.errors?.length ? { errors: r.errors } : {}
|
|
5127
|
+
};
|
|
5128
|
+
} catch (e) {
|
|
5129
|
+
return { success: false, inserted: 0, updated: 0, error: e?.message ?? "seed apply failed" };
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
4302
5132
|
/**
|
|
4303
5133
|
* List pending DRAFT metadata (ADR-0033) for the org, optionally narrowed
|
|
4304
5134
|
* by `packageId` and/or `type`. The list reads of `getMetaItems` only see
|
|
@@ -4332,14 +5162,25 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4332
5162
|
const drafts = await repo.listDrafts({ packageId: request.packageId });
|
|
4333
5163
|
const published = [];
|
|
4334
5164
|
const failed = [];
|
|
4335
|
-
|
|
5165
|
+
const ordered = [
|
|
5166
|
+
...drafts.filter((d) => d.type !== "seed"),
|
|
5167
|
+
...drafts.filter((d) => d.type === "seed")
|
|
5168
|
+
];
|
|
5169
|
+
const seedBodies = [];
|
|
5170
|
+
for (const d of ordered) {
|
|
4336
5171
|
try {
|
|
5172
|
+
if (d.type === "seed") {
|
|
5173
|
+
const ref = { type: d.type, name: d.name, org: orgId ?? "env" };
|
|
5174
|
+
const draft = await repo.get(ref, { state: "draft" });
|
|
5175
|
+
if (draft?.body) seedBodies.push(draft.body);
|
|
5176
|
+
}
|
|
4337
5177
|
const r = await this.publishMetaItem({
|
|
4338
5178
|
type: d.type,
|
|
4339
5179
|
name: d.name,
|
|
4340
5180
|
...request.organizationId ? { organizationId: request.organizationId } : {},
|
|
4341
5181
|
...request.actor ? { actor: request.actor } : {},
|
|
4342
|
-
message: `publish app package '${request.packageId}'
|
|
5182
|
+
message: `publish app package '${request.packageId}'`,
|
|
5183
|
+
_skipSeedApply: true
|
|
4343
5184
|
});
|
|
4344
5185
|
published.push({ type: d.type, name: d.name, version: r.version });
|
|
4345
5186
|
} catch (e) {
|
|
@@ -4351,12 +5192,38 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4351
5192
|
});
|
|
4352
5193
|
}
|
|
4353
5194
|
}
|
|
5195
|
+
const seedApplied = seedBodies.length > 0 ? await this.applySeedBodies(seedBodies, orgId) : void 0;
|
|
5196
|
+
let probes;
|
|
5197
|
+
if (published.length > 0) {
|
|
5198
|
+
try {
|
|
5199
|
+
const { runBuildProbes: runBuildProbes2 } = await Promise.resolve().then(() => (init_build_probes(), build_probes_exports));
|
|
5200
|
+
const analytics = this.getServicesRegistry?.().get("analytics");
|
|
5201
|
+
probes = await runBuildProbes2({
|
|
5202
|
+
engine: this.engine,
|
|
5203
|
+
getItem: async (type, name) => {
|
|
5204
|
+
const wrapper = await this.getMetaItem({
|
|
5205
|
+
type,
|
|
5206
|
+
name,
|
|
5207
|
+
...orgId ? { organizationId: orgId } : {}
|
|
5208
|
+
});
|
|
5209
|
+
return wrapper?.item ?? wrapper ?? void 0;
|
|
5210
|
+
},
|
|
5211
|
+
published,
|
|
5212
|
+
...analytics && typeof analytics.queryDataset === "function" ? { analytics } : {},
|
|
5213
|
+
organizationId: orgId
|
|
5214
|
+
});
|
|
5215
|
+
} catch {
|
|
5216
|
+
probes = void 0;
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
4354
5219
|
return {
|
|
4355
5220
|
success: failed.length === 0 && published.length > 0,
|
|
4356
5221
|
publishedCount: published.length,
|
|
4357
5222
|
failedCount: failed.length,
|
|
4358
5223
|
published,
|
|
4359
|
-
failed
|
|
5224
|
+
failed,
|
|
5225
|
+
...seedApplied ? { seedApplied } : {},
|
|
5226
|
+
...probes ? { probes } : {}
|
|
4360
5227
|
};
|
|
4361
5228
|
}
|
|
4362
5229
|
/**
|
|
@@ -5132,11 +5999,11 @@ function collectSecretFields(schema) {
|
|
|
5132
5999
|
|
|
5133
6000
|
// src/engine.ts
|
|
5134
6001
|
var import_shared5 = require("@objectstack/spec/shared");
|
|
5135
|
-
var
|
|
6002
|
+
var import_formula4 = require("@objectstack/formula");
|
|
5136
6003
|
var import_spec = require("@objectstack/spec");
|
|
5137
6004
|
|
|
5138
6005
|
// src/hook-wrappers.ts
|
|
5139
|
-
var
|
|
6006
|
+
var import_formula2 = require("@objectstack/formula");
|
|
5140
6007
|
|
|
5141
6008
|
// src/hook-metrics.ts
|
|
5142
6009
|
var noopHookMetricsRecorder = {
|
|
@@ -5213,10 +6080,10 @@ function wrapDeclarativeHook(meta, handler, opts = {}) {
|
|
|
5213
6080
|
if (meta.condition) {
|
|
5214
6081
|
const expr = typeof meta.condition === "string" ? { dialect: "cel", source: meta.condition } : meta.condition;
|
|
5215
6082
|
if (expr.source && expr.source.trim()) {
|
|
5216
|
-
const check =
|
|
6083
|
+
const check = import_formula2.ExpressionEngine.compile(expr);
|
|
5217
6084
|
if (check.ok) {
|
|
5218
6085
|
conditionFn = (record) => {
|
|
5219
|
-
const r =
|
|
6086
|
+
const r = import_formula2.ExpressionEngine.evaluate(expr, { record: record ?? {} });
|
|
5220
6087
|
if (!r.ok) {
|
|
5221
6088
|
logger.warn("[hook] condition evaluation failed; treating as false", {
|
|
5222
6089
|
hook: meta.name,
|
|
@@ -5688,7 +6555,7 @@ function validateRecord(objectSchema, data, mode) {
|
|
|
5688
6555
|
}
|
|
5689
6556
|
|
|
5690
6557
|
// src/validation/rule-validator.ts
|
|
5691
|
-
var
|
|
6558
|
+
var import_formula3 = require("@objectstack/formula");
|
|
5692
6559
|
var import_ajv = __toESM(require("ajv"));
|
|
5693
6560
|
var ajv = new import_ajv.default({ allErrors: true, strict: false });
|
|
5694
6561
|
var jsonSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -5704,7 +6571,7 @@ function stripReadonlyWhenFields(objectSchema, data, previous, logger) {
|
|
|
5704
6571
|
let result = data;
|
|
5705
6572
|
for (const [name, def] of Object.entries(fields)) {
|
|
5706
6573
|
if (!def?.readonlyWhen || !(name in data)) continue;
|
|
5707
|
-
const res =
|
|
6574
|
+
const res = import_formula3.ExpressionEngine.evaluate(toExpression(def.readonlyWhen), {
|
|
5708
6575
|
record: merged,
|
|
5709
6576
|
previous: previous ?? void 0
|
|
5710
6577
|
});
|
|
@@ -5759,7 +6626,7 @@ function evaluateValidationRules(objectSchema, data, mode, opts = {}) {
|
|
|
5759
6626
|
for (const [name, def] of Object.entries(fields)) {
|
|
5760
6627
|
const pred = def?.requiredWhen ?? def?.conditionalRequired;
|
|
5761
6628
|
if (!pred) continue;
|
|
5762
|
-
const res =
|
|
6629
|
+
const res = import_formula3.ExpressionEngine.evaluate(toExpression(pred), { record: merged, previous });
|
|
5763
6630
|
if (!res.ok) {
|
|
5764
6631
|
opts.logger?.warn?.(`requiredWhen for '${name}' failed to evaluate \u2014 skipped`);
|
|
5765
6632
|
continue;
|
|
@@ -5830,7 +6697,7 @@ function checkStateMachine(rule, mode, data, previous) {
|
|
|
5830
6697
|
}
|
|
5831
6698
|
function checkPredicate(rule, record, previous, logger) {
|
|
5832
6699
|
const expr = toExpression(rule.condition);
|
|
5833
|
-
const result =
|
|
6700
|
+
const result = import_formula3.ExpressionEngine.evaluate(expr, {
|
|
5834
6701
|
record,
|
|
5835
6702
|
previous: previous ?? void 0
|
|
5836
6703
|
});
|
|
@@ -5928,7 +6795,7 @@ function checkJsonSchema(rule, data, logger) {
|
|
|
5928
6795
|
return null;
|
|
5929
6796
|
}
|
|
5930
6797
|
function checkConditional(rule, ctx) {
|
|
5931
|
-
const result =
|
|
6798
|
+
const result = import_formula3.ExpressionEngine.evaluate(toExpression(rule.when), {
|
|
5932
6799
|
record: ctx.merged,
|
|
5933
6800
|
previous: ctx.previous ?? void 0
|
|
5934
6801
|
});
|
|
@@ -6109,7 +6976,7 @@ function planFormulaProjection(schema, requestedFields) {
|
|
|
6109
6976
|
if (def?.type === "formula" && def.expression) {
|
|
6110
6977
|
const expr = typeof def.expression === "string" ? { dialect: "cel", source: def.expression } : def.expression;
|
|
6111
6978
|
plan.push({ name: f, expression: expr });
|
|
6112
|
-
|
|
6979
|
+
import_formula4.ExpressionEngine.compile(expr);
|
|
6113
6980
|
} else if (Array.isArray(requestedFields) && requestedFields.length > 0) {
|
|
6114
6981
|
projected.add(f);
|
|
6115
6982
|
}
|
|
@@ -6131,7 +6998,7 @@ function applyFormulaPlan(plan, records) {
|
|
|
6131
6998
|
for (const rec of records) {
|
|
6132
6999
|
if (rec == null) continue;
|
|
6133
7000
|
for (const fp of plan) {
|
|
6134
|
-
const r =
|
|
7001
|
+
const r = import_formula4.ExpressionEngine.evaluate(fp.expression, { record: rec });
|
|
6135
7002
|
rec[fp.name] = r.ok ? r.value : null;
|
|
6136
7003
|
}
|
|
6137
7004
|
}
|
|
@@ -6556,7 +7423,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6556
7423
|
if (f.defaultValue == null) continue;
|
|
6557
7424
|
const dv = f.defaultValue;
|
|
6558
7425
|
if (typeof dv === "object" && dv !== null && dv.dialect && typeof dv.source === "string") {
|
|
6559
|
-
const result =
|
|
7426
|
+
const result = import_formula4.ExpressionEngine.evaluate(dv, {
|
|
6560
7427
|
now,
|
|
6561
7428
|
user: execCtx?.userId ? { id: String(execCtx.userId), role: execCtx?.roles?.[0] } : void 0,
|
|
6562
7429
|
org: execCtx?.tenantId ? { id: String(execCtx.tenantId) } : void 0,
|
|
@@ -9215,6 +10082,10 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
|
|
|
9215
10082
|
}
|
|
9216
10083
|
return objects;
|
|
9217
10084
|
}
|
|
10085
|
+
|
|
10086
|
+
// src/index.ts
|
|
10087
|
+
init_seed_loader();
|
|
10088
|
+
init_build_probes();
|
|
9218
10089
|
// Annotate the CommonJS export names for ESM import in node:
|
|
9219
10090
|
0 && (module.exports = {
|
|
9220
10091
|
DEFAULT_EXTENDER_PRIORITY,
|
|
@@ -9230,6 +10101,7 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
|
|
|
9230
10101
|
SECRET_REF_PREFIX,
|
|
9231
10102
|
SchemaRegistry,
|
|
9232
10103
|
ScopedContext,
|
|
10104
|
+
SeedLoaderService,
|
|
9233
10105
|
SysMetadataRepository,
|
|
9234
10106
|
ValidationError,
|
|
9235
10107
|
applyInMemoryAggregation,
|
|
@@ -9248,6 +10120,7 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
|
|
|
9248
10120
|
noopHookMetricsRecorder,
|
|
9249
10121
|
parseFQN,
|
|
9250
10122
|
parseSecretRef,
|
|
10123
|
+
runBuildProbes,
|
|
9251
10124
|
toTitleCase,
|
|
9252
10125
|
validateRecord,
|
|
9253
10126
|
wrapDeclarativeHook
|