@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.mjs CHANGED
@@ -1,3 +1,782 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/seed-loader.ts
12
+ var seed_loader_exports = {};
13
+ __export(seed_loader_exports, {
14
+ SeedLoaderService: () => SeedLoaderService
15
+ });
16
+ import { SeedLoaderConfigSchema } from "@objectstack/spec/data";
17
+ import { resolveSeedRecord } from "@objectstack/formula";
18
+ var DEFAULT_EXTERNAL_ID_FIELD, _SeedLoaderService, SeedLoaderService;
19
+ var init_seed_loader = __esm({
20
+ "src/seed-loader.ts"() {
21
+ "use strict";
22
+ DEFAULT_EXTERNAL_ID_FIELD = "name";
23
+ _SeedLoaderService = class _SeedLoaderService {
24
+ constructor(engine, metadata, logger) {
25
+ this.engine = engine;
26
+ this.metadata = metadata;
27
+ this.logger = logger;
28
+ }
29
+ // ==========================================================================
30
+ // Public API
31
+ // ==========================================================================
32
+ async load(request) {
33
+ const startTime = Date.now();
34
+ const config = request.config;
35
+ const allErrors = [];
36
+ const allResults = [];
37
+ const datasets = this.filterByEnv(request.seeds, config.env);
38
+ if (datasets.length === 0) {
39
+ return this.buildEmptyResult(config, Date.now() - startTime);
40
+ }
41
+ const objectNames = datasets.map((d) => d.object);
42
+ const graph = await this.buildDependencyGraph(objectNames);
43
+ this.logger.info("[SeedLoader] Dependency graph built", {
44
+ objects: objectNames.length,
45
+ insertOrder: graph.insertOrder,
46
+ circularDeps: graph.circularDependencies.length
47
+ });
48
+ const orderedDatasets = this.orderDatasets(datasets, graph.insertOrder);
49
+ const refMap = this.buildReferenceMap(graph);
50
+ const insertedRecords = /* @__PURE__ */ new Map();
51
+ const deferredUpdates = [];
52
+ for (const dataset of orderedDatasets) {
53
+ const result = await this.loadDataset(
54
+ dataset,
55
+ config,
56
+ refMap,
57
+ insertedRecords,
58
+ deferredUpdates,
59
+ allErrors
60
+ );
61
+ allResults.push(result);
62
+ if (config.haltOnError && result.errored > 0) {
63
+ this.logger.warn("[SeedLoader] Halting on first error", { object: dataset.object });
64
+ break;
65
+ }
66
+ }
67
+ if (config.multiPass && deferredUpdates.length > 0 && !config.dryRun) {
68
+ this.logger.info("[SeedLoader] Pass 2: resolving deferred references", {
69
+ count: deferredUpdates.length
70
+ });
71
+ await this.resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors, config.organizationId);
72
+ }
73
+ const durationMs = Date.now() - startTime;
74
+ return this.buildResult(config, graph, allResults, allErrors, durationMs);
75
+ }
76
+ async buildDependencyGraph(objectNames) {
77
+ const nodes = [];
78
+ const objectSet = new Set(objectNames);
79
+ for (const objectName of objectNames) {
80
+ const objDef = await this.metadata.getObject(objectName);
81
+ const dependsOn = [];
82
+ const references = [];
83
+ if (objDef && objDef.fields) {
84
+ const fields = objDef.fields;
85
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
86
+ if ((fieldDef.type === "lookup" || fieldDef.type === "master_detail") && fieldDef.reference) {
87
+ const targetObject = fieldDef.reference;
88
+ if (objectSet.has(targetObject) && !dependsOn.includes(targetObject)) {
89
+ dependsOn.push(targetObject);
90
+ }
91
+ references.push({
92
+ field: fieldName,
93
+ targetObject,
94
+ targetField: DEFAULT_EXTERNAL_ID_FIELD,
95
+ fieldType: fieldDef.type
96
+ });
97
+ }
98
+ }
99
+ }
100
+ nodes.push({ object: objectName, dependsOn, references });
101
+ }
102
+ const { insertOrder, circularDependencies } = this.topologicalSort(nodes);
103
+ return { nodes, insertOrder, circularDependencies };
104
+ }
105
+ async validate(datasets, config) {
106
+ const parsedConfig = SeedLoaderConfigSchema.parse({ ...config, dryRun: true });
107
+ return this.load({ seeds: datasets, config: parsedConfig });
108
+ }
109
+ // ==========================================================================
110
+ // Internal: Seed Loading
111
+ // ==========================================================================
112
+ async loadDataset(dataset, config, refMap, insertedRecords, deferredUpdates, allErrors) {
113
+ const objectName = dataset.object;
114
+ const mode = dataset.mode || config.defaultMode;
115
+ const externalId = dataset.externalId || "name";
116
+ let inserted = 0;
117
+ let updated = 0;
118
+ let skipped = 0;
119
+ let errored = 0;
120
+ let referencesResolved = 0;
121
+ let referencesDeferred = 0;
122
+ const errors = [];
123
+ if (!insertedRecords.has(objectName)) {
124
+ insertedRecords.set(objectName, /* @__PURE__ */ new Map());
125
+ }
126
+ let existingRecords;
127
+ if ((mode === "upsert" || mode === "update" || mode === "ignore") && !config.dryRun) {
128
+ existingRecords = await this.loadExistingRecords(
129
+ objectName,
130
+ externalId,
131
+ config.organizationId
132
+ );
133
+ }
134
+ const objectRefs = refMap.get(objectName) || [];
135
+ const seedNow = /* @__PURE__ */ new Date();
136
+ const seedIdentity = config.identity;
137
+ const baseEvalCtx = {
138
+ now: seedNow,
139
+ user: seedIdentity?.user,
140
+ // Fall back to the per-tenant organizationId so `os.org.id` resolves
141
+ // during per-org replay even without an explicit identity.org.
142
+ org: seedIdentity?.org ?? (config.organizationId ? { id: config.organizationId } : void 0),
143
+ env: config.env
144
+ };
145
+ for (let i = 0; i < dataset.records.length; i++) {
146
+ const seedResult = resolveSeedRecord(
147
+ dataset.records[i],
148
+ baseEvalCtx
149
+ );
150
+ if (!seedResult.ok) {
151
+ errored++;
152
+ const error = {
153
+ sourceObject: objectName,
154
+ field: "(expression)",
155
+ targetObject: objectName,
156
+ targetField: "(expression)",
157
+ attemptedValue: dataset.records[i],
158
+ recordIndex: i,
159
+ 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).`
160
+ };
161
+ errors.push(error);
162
+ allErrors.push(error);
163
+ this.logger.warn(`[SeedLoader] ${error.message}`);
164
+ continue;
165
+ }
166
+ const record = { ...seedResult.value };
167
+ if (config.organizationId && record["organization_id"] == null) {
168
+ record["organization_id"] = config.organizationId;
169
+ }
170
+ for (const ref of objectRefs) {
171
+ const fieldValue = record[ref.field];
172
+ if (fieldValue === void 0 || fieldValue === null) continue;
173
+ if (typeof fieldValue === "object") {
174
+ const wrapped = fieldValue.externalId;
175
+ 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.`;
176
+ const error = {
177
+ sourceObject: objectName,
178
+ field: ref.field,
179
+ targetObject: ref.targetObject,
180
+ targetField: ref.targetField,
181
+ attemptedValue: fieldValue,
182
+ recordIndex: i,
183
+ message: `Invalid reference for ${objectName}.${ref.field}: expected a ${ref.targetObject}.${ref.targetField} natural-key string but got an object.${hint}`
184
+ };
185
+ errors.push(error);
186
+ allErrors.push(error);
187
+ this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i });
188
+ record[ref.field] = null;
189
+ continue;
190
+ }
191
+ if (typeof fieldValue !== "string" || this.looksLikeInternalId(fieldValue)) continue;
192
+ const targetMap = insertedRecords.get(ref.targetObject);
193
+ const resolvedId = targetMap?.get(String(fieldValue));
194
+ if (resolvedId) {
195
+ record[ref.field] = resolvedId;
196
+ referencesResolved++;
197
+ } else if (!config.dryRun) {
198
+ const dbId = await this.resolveFromDatabase(ref.targetObject, ref.targetField, fieldValue, config.organizationId);
199
+ if (dbId) {
200
+ record[ref.field] = dbId;
201
+ referencesResolved++;
202
+ } else if (config.multiPass) {
203
+ record[ref.field] = null;
204
+ deferredUpdates.push({
205
+ objectName,
206
+ recordExternalId: String(record[externalId] ?? ""),
207
+ field: ref.field,
208
+ targetObject: ref.targetObject,
209
+ targetField: ref.targetField,
210
+ attemptedValue: fieldValue,
211
+ recordIndex: i
212
+ });
213
+ referencesDeferred++;
214
+ } else {
215
+ const error = {
216
+ sourceObject: objectName,
217
+ field: ref.field,
218
+ targetObject: ref.targetObject,
219
+ targetField: ref.targetField,
220
+ attemptedValue: fieldValue,
221
+ recordIndex: i,
222
+ message: `Cannot resolve reference: ${objectName}.${ref.field} = '${fieldValue}' \u2192 ${ref.targetObject}.${ref.targetField} not found`
223
+ };
224
+ errors.push(error);
225
+ allErrors.push(error);
226
+ }
227
+ } else {
228
+ const targetMap2 = insertedRecords.get(ref.targetObject);
229
+ if (!targetMap2?.has(String(fieldValue))) {
230
+ const error = {
231
+ sourceObject: objectName,
232
+ field: ref.field,
233
+ targetObject: ref.targetObject,
234
+ targetField: ref.targetField,
235
+ attemptedValue: fieldValue,
236
+ recordIndex: i,
237
+ message: `[dry-run] Reference may not resolve: ${objectName}.${ref.field} = '${fieldValue}' \u2192 ${ref.targetObject}.${ref.targetField}`
238
+ };
239
+ errors.push(error);
240
+ allErrors.push(error);
241
+ }
242
+ }
243
+ }
244
+ if (!config.dryRun) {
245
+ try {
246
+ const result = await this.writeRecord(
247
+ objectName,
248
+ record,
249
+ mode,
250
+ externalId,
251
+ existingRecords
252
+ );
253
+ if (result.action === "inserted") inserted++;
254
+ else if (result.action === "updated") updated++;
255
+ else if (result.action === "skipped") skipped++;
256
+ const externalIdValue = String(record[externalId] ?? "");
257
+ const internalId = result.id;
258
+ if (externalIdValue && internalId) {
259
+ insertedRecords.get(objectName).set(externalIdValue, String(internalId));
260
+ }
261
+ } catch (err) {
262
+ errored++;
263
+ const error = {
264
+ sourceObject: objectName,
265
+ field: "(write)",
266
+ targetObject: objectName,
267
+ targetField: externalId,
268
+ attemptedValue: record[externalId] ?? null,
269
+ recordIndex: i,
270
+ message: `Failed to write ${objectName} record #${i} (${externalId}=${String(record[externalId] ?? "")}): ${err.message}`
271
+ };
272
+ errors.push(error);
273
+ allErrors.push(error);
274
+ this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i });
275
+ }
276
+ } else {
277
+ const externalIdValue = String(record[externalId] ?? "");
278
+ if (externalIdValue) {
279
+ insertedRecords.get(objectName).set(externalIdValue, `dry-run-id-${i}`);
280
+ }
281
+ inserted++;
282
+ }
283
+ }
284
+ return {
285
+ object: objectName,
286
+ mode,
287
+ inserted,
288
+ updated,
289
+ skipped,
290
+ errored,
291
+ total: dataset.records.length,
292
+ referencesResolved,
293
+ referencesDeferred,
294
+ errors
295
+ };
296
+ }
297
+ // ==========================================================================
298
+ // Internal: Reference Resolution
299
+ // ==========================================================================
300
+ async resolveFromDatabase(targetObject, targetField, value, organizationId) {
301
+ try {
302
+ const where = { [targetField]: value };
303
+ if (organizationId) where.organization_id = organizationId;
304
+ const records = await this.engine.find(targetObject, {
305
+ where,
306
+ fields: ["id"],
307
+ limit: 1,
308
+ context: { isSystem: true }
309
+ });
310
+ if (records && records.length > 0) {
311
+ return String(records[0].id || records[0]._id);
312
+ }
313
+ } catch {
314
+ }
315
+ return null;
316
+ }
317
+ async resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors, organizationId) {
318
+ for (const deferred of deferredUpdates) {
319
+ const targetMap = insertedRecords.get(deferred.targetObject);
320
+ let resolvedId = targetMap?.get(String(deferred.attemptedValue));
321
+ if (!resolvedId) {
322
+ resolvedId = await this.resolveFromDatabase(
323
+ deferred.targetObject,
324
+ deferred.targetField,
325
+ deferred.attemptedValue,
326
+ organizationId
327
+ ) ?? void 0;
328
+ }
329
+ if (resolvedId) {
330
+ const objectRecordMap = insertedRecords.get(deferred.objectName);
331
+ const recordId = objectRecordMap?.get(deferred.recordExternalId);
332
+ if (recordId) {
333
+ try {
334
+ await this.engine.update(deferred.objectName, {
335
+ id: recordId,
336
+ [deferred.field]: resolvedId
337
+ }, { context: { isSystem: true } });
338
+ const resultEntry = allResults.find((r) => r.object === deferred.objectName);
339
+ if (resultEntry) {
340
+ resultEntry.referencesResolved++;
341
+ resultEntry.referencesDeferred--;
342
+ }
343
+ } catch (err) {
344
+ this.logger.warn("[SeedLoader] Failed to resolve deferred reference", {
345
+ object: deferred.objectName,
346
+ field: deferred.field,
347
+ error: err.message
348
+ });
349
+ }
350
+ }
351
+ } else {
352
+ const error = {
353
+ sourceObject: deferred.objectName,
354
+ field: deferred.field,
355
+ targetObject: deferred.targetObject,
356
+ targetField: deferred.targetField,
357
+ attemptedValue: deferred.attemptedValue,
358
+ recordIndex: deferred.recordIndex,
359
+ message: `Deferred reference unresolved after pass 2: ${deferred.objectName}.${deferred.field} = '${deferred.attemptedValue}' \u2192 ${deferred.targetObject}.${deferred.targetField} not found`
360
+ };
361
+ const resultEntry = allResults.find((r) => r.object === deferred.objectName);
362
+ if (resultEntry) {
363
+ resultEntry.errors.push(error);
364
+ }
365
+ allErrors.push(error);
366
+ }
367
+ }
368
+ }
369
+ async writeRecord(objectName, record, mode, externalId, existingRecords) {
370
+ const externalIdValue = record[externalId];
371
+ const existing = existingRecords?.get(String(externalIdValue ?? ""));
372
+ const opts = _SeedLoaderService.SEED_OPTIONS;
373
+ switch (mode) {
374
+ case "insert": {
375
+ const result = await this.engine.insert(objectName, record, opts);
376
+ return { action: "inserted", id: this.extractId(result) };
377
+ }
378
+ case "update": {
379
+ if (!existing) {
380
+ return { action: "skipped" };
381
+ }
382
+ const id = this.extractId(existing);
383
+ await this.engine.update(objectName, { ...record, id }, opts);
384
+ return { action: "updated", id };
385
+ }
386
+ case "upsert": {
387
+ if (existing) {
388
+ const id = this.extractId(existing);
389
+ await this.engine.update(objectName, { ...record, id }, opts);
390
+ return { action: "updated", id };
391
+ } else {
392
+ const result = await this.engine.insert(objectName, record, opts);
393
+ return { action: "inserted", id: this.extractId(result) };
394
+ }
395
+ }
396
+ case "ignore": {
397
+ if (existing) {
398
+ return { action: "skipped", id: this.extractId(existing) };
399
+ }
400
+ const result = await this.engine.insert(objectName, record, opts);
401
+ return { action: "inserted", id: this.extractId(result) };
402
+ }
403
+ case "replace": {
404
+ const result = await this.engine.insert(objectName, record, opts);
405
+ return { action: "inserted", id: this.extractId(result) };
406
+ }
407
+ default: {
408
+ const result = await this.engine.insert(objectName, record, opts);
409
+ return { action: "inserted", id: this.extractId(result) };
410
+ }
411
+ }
412
+ }
413
+ // ==========================================================================
414
+ // Internal: Dependency Graph
415
+ // ==========================================================================
416
+ /**
417
+ * Kahn's algorithm for topological sort with cycle detection.
418
+ */
419
+ topologicalSort(nodes) {
420
+ const inDegree = /* @__PURE__ */ new Map();
421
+ const adjacency = /* @__PURE__ */ new Map();
422
+ const objectSet = new Set(nodes.map((n) => n.object));
423
+ for (const node of nodes) {
424
+ inDegree.set(node.object, 0);
425
+ adjacency.set(node.object, []);
426
+ }
427
+ for (const node of nodes) {
428
+ for (const dep of node.dependsOn) {
429
+ if (objectSet.has(dep) && dep !== node.object) {
430
+ adjacency.get(dep).push(node.object);
431
+ inDegree.set(node.object, (inDegree.get(node.object) || 0) + 1);
432
+ }
433
+ }
434
+ }
435
+ const queue = [];
436
+ for (const [obj, degree] of inDegree) {
437
+ if (degree === 0) queue.push(obj);
438
+ }
439
+ const insertOrder = [];
440
+ while (queue.length > 0) {
441
+ const current = queue.shift();
442
+ insertOrder.push(current);
443
+ for (const neighbor of adjacency.get(current) || []) {
444
+ const newDegree = (inDegree.get(neighbor) || 0) - 1;
445
+ inDegree.set(neighbor, newDegree);
446
+ if (newDegree === 0) {
447
+ queue.push(neighbor);
448
+ }
449
+ }
450
+ }
451
+ const circularDependencies = [];
452
+ const remaining = nodes.filter((n) => !insertOrder.includes(n.object));
453
+ if (remaining.length > 0) {
454
+ const cycles = this.findCycles(remaining);
455
+ circularDependencies.push(...cycles);
456
+ for (const node of remaining) {
457
+ if (!insertOrder.includes(node.object)) {
458
+ insertOrder.push(node.object);
459
+ }
460
+ }
461
+ }
462
+ return { insertOrder, circularDependencies };
463
+ }
464
+ findCycles(nodes) {
465
+ const cycles = [];
466
+ const nodeMap = new Map(nodes.map((n) => [n.object, n]));
467
+ const visited = /* @__PURE__ */ new Set();
468
+ const inStack = /* @__PURE__ */ new Set();
469
+ const dfs = (current, path) => {
470
+ if (inStack.has(current)) {
471
+ const cycleStart = path.indexOf(current);
472
+ if (cycleStart !== -1) {
473
+ cycles.push([...path.slice(cycleStart), current]);
474
+ }
475
+ return;
476
+ }
477
+ if (visited.has(current)) return;
478
+ visited.add(current);
479
+ inStack.add(current);
480
+ path.push(current);
481
+ const node = nodeMap.get(current);
482
+ if (node) {
483
+ for (const dep of node.dependsOn) {
484
+ if (nodeMap.has(dep)) {
485
+ dfs(dep, [...path]);
486
+ }
487
+ }
488
+ }
489
+ inStack.delete(current);
490
+ };
491
+ for (const node of nodes) {
492
+ if (!visited.has(node.object)) {
493
+ dfs(node.object, []);
494
+ }
495
+ }
496
+ return cycles;
497
+ }
498
+ // ==========================================================================
499
+ // Internal: Helpers
500
+ // ==========================================================================
501
+ filterByEnv(datasets, env) {
502
+ if (!env) return datasets;
503
+ return datasets.filter((d) => d.env.includes(env));
504
+ }
505
+ orderDatasets(datasets, insertOrder) {
506
+ const orderMap = new Map(insertOrder.map((name, i) => [name, i]));
507
+ return [...datasets].sort((a, b) => {
508
+ const orderA = orderMap.get(a.object) ?? Number.MAX_SAFE_INTEGER;
509
+ const orderB = orderMap.get(b.object) ?? Number.MAX_SAFE_INTEGER;
510
+ return orderA - orderB;
511
+ });
512
+ }
513
+ buildReferenceMap(graph) {
514
+ const map = /* @__PURE__ */ new Map();
515
+ for (const node of graph.nodes) {
516
+ if (node.references.length > 0) {
517
+ map.set(node.object, node.references);
518
+ }
519
+ }
520
+ return map;
521
+ }
522
+ async loadExistingRecords(objectName, externalId, organizationId) {
523
+ const map = /* @__PURE__ */ new Map();
524
+ try {
525
+ const findArgs = {
526
+ fields: ["id", externalId],
527
+ context: { isSystem: true }
528
+ };
529
+ if (organizationId) findArgs.where = { organization_id: organizationId };
530
+ const records = await this.engine.find(objectName, findArgs);
531
+ for (const record of records || []) {
532
+ const key = String(record[externalId] ?? "");
533
+ if (key) {
534
+ map.set(key, record);
535
+ }
536
+ }
537
+ } catch {
538
+ }
539
+ return map;
540
+ }
541
+ looksLikeInternalId(value) {
542
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
543
+ return true;
544
+ }
545
+ if (/^[0-9a-f]{24}$/i.test(value)) {
546
+ return true;
547
+ }
548
+ return false;
549
+ }
550
+ extractId(record) {
551
+ if (!record) return void 0;
552
+ return String(record.id || record._id || "");
553
+ }
554
+ buildEmptyResult(config, durationMs) {
555
+ return {
556
+ success: true,
557
+ dryRun: config.dryRun,
558
+ dependencyGraph: { nodes: [], insertOrder: [], circularDependencies: [] },
559
+ results: [],
560
+ errors: [],
561
+ summary: {
562
+ objectsProcessed: 0,
563
+ totalRecords: 0,
564
+ totalInserted: 0,
565
+ totalUpdated: 0,
566
+ totalSkipped: 0,
567
+ totalErrored: 0,
568
+ totalReferencesResolved: 0,
569
+ totalReferencesDeferred: 0,
570
+ circularDependencyCount: 0,
571
+ durationMs
572
+ }
573
+ };
574
+ }
575
+ buildResult(config, graph, results, errors, durationMs) {
576
+ const summary = {
577
+ objectsProcessed: results.length,
578
+ totalRecords: results.reduce((sum, r) => sum + r.total, 0),
579
+ totalInserted: results.reduce((sum, r) => sum + r.inserted, 0),
580
+ totalUpdated: results.reduce((sum, r) => sum + r.updated, 0),
581
+ totalSkipped: results.reduce((sum, r) => sum + r.skipped, 0),
582
+ totalErrored: results.reduce((sum, r) => sum + r.errored, 0),
583
+ totalReferencesResolved: results.reduce((sum, r) => sum + r.referencesResolved, 0),
584
+ totalReferencesDeferred: results.reduce((sum, r) => sum + r.referencesDeferred, 0),
585
+ circularDependencyCount: graph.circularDependencies.length,
586
+ durationMs
587
+ };
588
+ const hasErrors = errors.length > 0 || summary.totalErrored > 0;
589
+ return {
590
+ success: !hasErrors,
591
+ dryRun: config.dryRun,
592
+ dependencyGraph: graph,
593
+ results,
594
+ errors,
595
+ summary
596
+ };
597
+ }
598
+ };
599
+ // ==========================================================================
600
+ // Internal: Write Operations
601
+ // ==========================================================================
602
+ /**
603
+ * Seed writes always run as a privileged system context. This bypasses
604
+ * RBAC checks (so seeds can target system tables like `sys_*`) and
605
+ * disables the SecurityPlugin's auto-injection of `organization_id` /
606
+ * `owner_id` — seeds either declare those fields explicitly per
607
+ * record, or are intentionally cross-tenant / global.
608
+ */
609
+ _SeedLoaderService.SEED_OPTIONS = { context: { isSystem: true } };
610
+ SeedLoaderService = _SeedLoaderService;
611
+ }
612
+ });
613
+
614
+ // src/build-probes.ts
615
+ var build_probes_exports = {};
616
+ __export(build_probes_exports, {
617
+ runBuildProbes: () => runBuildProbes
618
+ });
619
+ function asRec(v) {
620
+ return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
621
+ }
622
+ function asArr(v) {
623
+ return Array.isArray(v) ? v : [];
624
+ }
625
+ function resultRows(result) {
626
+ if (Array.isArray(result)) return result;
627
+ const r = asRec(result);
628
+ if (!r) return [];
629
+ if (Array.isArray(r.rows)) return r.rows;
630
+ if (Array.isArray(r.data)) return r.data;
631
+ return [];
632
+ }
633
+ async function hasRows(engine, objectName, organizationId) {
634
+ const rows = await engine.find(objectName, {
635
+ fields: ["id"],
636
+ limit: 1,
637
+ ...organizationId ? { where: { organization_id: organizationId } } : {},
638
+ context: { isSystem: true }
639
+ });
640
+ return Array.isArray(rows) && rows.length > 0;
641
+ }
642
+ async function runBuildProbes(opts) {
643
+ const issues = [];
644
+ const checked = { seeds: 0, views: 0, widgets: 0 };
645
+ const { engine, getItem, published, analytics, organizationId } = opts;
646
+ const itemCache = /* @__PURE__ */ new Map();
647
+ const readItem = async (type, name) => {
648
+ const key = `${type} ${name}`;
649
+ if (itemCache.has(key)) return itemCache.get(key);
650
+ let item;
651
+ try {
652
+ item = await getItem(type, name);
653
+ } catch {
654
+ item = void 0;
655
+ }
656
+ itemCache.set(key, item);
657
+ return item;
658
+ };
659
+ for (const p of published.filter((x) => x.type === "seed")) {
660
+ const body = asRec(await readItem("seed", p.name));
661
+ const objectName = typeof body?.object === "string" ? body.object : void 0;
662
+ if (!objectName) continue;
663
+ checked.seeds += 1;
664
+ try {
665
+ if (!await hasRows(engine, objectName, organizationId)) {
666
+ issues.push({
667
+ layer: "runtime",
668
+ severity: "error",
669
+ artifact: { type: "seed", name: p.name },
670
+ ref: { type: "object", name: objectName },
671
+ code: "seed_not_applied",
672
+ message: `Seed "${p.name}" was published but object "${objectName}" has no rows \u2014 the sample data never materialized.`,
673
+ fix: `Check the publish response's seedApplied for the load error, fix the seed rows (field names/types), and republish the seed.`
674
+ });
675
+ }
676
+ } catch (e) {
677
+ issues.push({
678
+ layer: "runtime",
679
+ severity: "error",
680
+ artifact: { type: "seed", name: p.name },
681
+ ref: { type: "object", name: objectName },
682
+ code: "seed_not_applied",
683
+ message: `Seed "${p.name}" probe could not read object "${objectName}": ${String(e?.message ?? e)}`
684
+ });
685
+ }
686
+ }
687
+ for (const p of published.filter((x) => x.type === "view")) {
688
+ const body = asRec(await readItem("view", p.name));
689
+ const config = asRec(body?.config);
690
+ const dataObj = asRec(config?.data)?.object;
691
+ const objectName = typeof body?.object === "string" ? body.object : typeof dataObj === "string" ? dataObj : void 0;
692
+ if (!objectName) continue;
693
+ checked.views += 1;
694
+ try {
695
+ await engine.find(objectName, {
696
+ fields: ["id"],
697
+ limit: 1,
698
+ ...organizationId ? { where: { organization_id: organizationId } } : {},
699
+ context: { isSystem: true }
700
+ });
701
+ } catch (e) {
702
+ issues.push({
703
+ layer: "runtime",
704
+ severity: "error",
705
+ artifact: { type: "view", name: p.name },
706
+ ref: { type: "object", name: objectName },
707
+ code: "view_read_failed",
708
+ message: `View "${p.name}" cannot read object "${objectName}": ${String(e?.message ?? e)} \u2014 it will render as an error for every user.`,
709
+ fix: `Verify object "${objectName}" published successfully (its table must exist) and that the view's binding is correct.`
710
+ });
711
+ }
712
+ }
713
+ const dashboards = published.filter((x) => x.type === "dashboard");
714
+ let widgetsToProbe = 0;
715
+ for (const p of dashboards) {
716
+ const body = asRec(await readItem("dashboard", p.name));
717
+ const widgets = asArr(body?.widgets).map(asRec).filter((w) => !!w);
718
+ const datasetBound = widgets.filter((w) => typeof w.dataset === "string" && w.dataset);
719
+ widgetsToProbe += datasetBound.length;
720
+ if (!analytics || typeof analytics.queryDataset !== "function") continue;
721
+ for (const w of datasetBound) {
722
+ const widgetId = String(w.id ?? w.title ?? "?");
723
+ const dsName = w.dataset;
724
+ const dataset = asRec(await readItem("dataset", dsName));
725
+ if (!dataset) continue;
726
+ checked.widgets += 1;
727
+ const measures = asArr(w.values).filter((v) => typeof v === "string" && v.length > 0);
728
+ const firstMeasure = asRec(asArr(dataset.measures)[0])?.name;
729
+ const selection = {
730
+ measures: measures.length ? measures : typeof firstMeasure === "string" ? [firstMeasure] : [],
731
+ dimensions: [],
732
+ limit: 1
733
+ };
734
+ if (selection.measures.length === 0) continue;
735
+ const objectName = typeof dataset.object === "string" ? dataset.object : void 0;
736
+ try {
737
+ const result = await analytics.queryDataset(dataset, selection, void 0);
738
+ const rows = resultRows(result);
739
+ if (rows.length === 0 && objectName && await hasRows(engine, objectName, organizationId)) {
740
+ issues.push({
741
+ layer: "runtime",
742
+ severity: "error",
743
+ artifact: { type: "dashboard", name: p.name },
744
+ ref: { type: "dataset", name: dsName, member: widgetId },
745
+ code: "empty_query",
746
+ 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.`,
747
+ fix: `Run the dataset query directly to see the compiled strategy/SQL; check the dataset's measure/dimension field bindings against object "${objectName}".`
748
+ });
749
+ }
750
+ } catch (e) {
751
+ issues.push({
752
+ layer: "runtime",
753
+ severity: "error",
754
+ artifact: { type: "dashboard", name: p.name },
755
+ ref: { type: "dataset", name: dsName, member: widgetId },
756
+ code: "widget_query_failed",
757
+ message: `Dashboard "${p.name}" widget "${widgetId}" query against dataset "${dsName}" failed: ${String(e?.message ?? e)}`,
758
+ fix: `Fix the dataset definition (or the widget's values/dimensions) so the query compiles, then republish.`
759
+ });
760
+ }
761
+ }
762
+ }
763
+ if (widgetsToProbe > 0 && (!analytics || typeof analytics.queryDataset !== "function")) {
764
+ issues.push({
765
+ layer: "runtime",
766
+ severity: "warning",
767
+ artifact: { type: "dashboard", name: dashboards.map((d) => d.name).join(", ") },
768
+ code: "probes_unavailable",
769
+ message: `${widgetsToProbe} dashboard widget(s) could not be probed: no analytics service is mounted on this kernel.`
770
+ });
771
+ }
772
+ return { issues, checked };
773
+ }
774
+ var init_build_probes = __esm({
775
+ "src/build-probes.ts"() {
776
+ "use strict";
777
+ }
778
+ });
779
+
1
780
  // src/registry.ts
2
781
  import { ObjectSchema } from "@objectstack/spec/data";
3
782
  import { readEnvWithDeprecation } from "@objectstack/types";
@@ -4215,12 +4994,16 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4215
4994
  item: result.item.body
4216
4995
  });
4217
4996
  await this.ensureObjectStorage(request.type, request.name);
4218
- return {
4997
+ const response = {
4219
4998
  success: true,
4220
4999
  version: result.version,
4221
5000
  seq: result.seq,
4222
5001
  message: `Published draft \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
4223
5002
  };
5003
+ if (singularType === "seed" && !request._skipSeedApply) {
5004
+ response.seedApplied = await this.applySeedBodies([result.item.body], orgId);
5005
+ }
5006
+ return response;
4224
5007
  } catch (err) {
4225
5008
  if (err instanceof ConflictError2) {
4226
5009
  const conflict = new Error(
@@ -4235,6 +5018,58 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4235
5018
  throw err;
4236
5019
  }
4237
5020
  }
5021
+ /**
5022
+ * Materialize published `seed` bodies into data rows via the SeedLoaderService
5023
+ * (externalId-keyed upsert, multi-pass for cross-seed references). Passing ALL
5024
+ * of a publish's seed bodies in ONE call lets a child seed reference a parent
5025
+ * seed's rows regardless of publish order. Best-effort: any failure is
5026
+ * returned, never thrown — publishing metadata must not be blocked by a data
5027
+ * problem, but the caller surfaces `seedApplied` so the failure is LOUD.
5028
+ */
5029
+ async applySeedBodies(bodies, organizationId) {
5030
+ try {
5031
+ const seeds = bodies.filter(
5032
+ (b) => b && typeof b.object === "string" && Array.isArray(b.records)
5033
+ );
5034
+ if (seeds.length === 0) {
5035
+ return { success: false, inserted: 0, updated: 0, error: "seed apply: no readable seed bodies" };
5036
+ }
5037
+ const { SeedLoaderService: SeedLoaderService2 } = await Promise.resolve().then(() => (init_seed_loader(), seed_loader_exports));
5038
+ const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
5039
+ const metadataAdapter = {
5040
+ getObject: async (name) => {
5041
+ const wrapper = await this.getMetaItem({
5042
+ type: "object",
5043
+ name,
5044
+ ...organizationId ? { organizationId } : {}
5045
+ });
5046
+ return wrapper?.item ?? wrapper ?? null;
5047
+ }
5048
+ };
5049
+ const loader = new SeedLoaderService2(
5050
+ this.engine,
5051
+ metadataAdapter,
5052
+ console
5053
+ );
5054
+ const request = SeedLoaderRequestSchema.parse({
5055
+ seeds,
5056
+ config: {
5057
+ defaultMode: "upsert",
5058
+ multiPass: true,
5059
+ ...organizationId ? { organizationId } : {}
5060
+ }
5061
+ });
5062
+ const r = await loader.load(request);
5063
+ return {
5064
+ success: r.success,
5065
+ inserted: r.summary.totalInserted,
5066
+ updated: r.summary.totalUpdated,
5067
+ ...r.errors?.length ? { errors: r.errors } : {}
5068
+ };
5069
+ } catch (e) {
5070
+ return { success: false, inserted: 0, updated: 0, error: e?.message ?? "seed apply failed" };
5071
+ }
5072
+ }
4238
5073
  /**
4239
5074
  * List pending DRAFT metadata (ADR-0033) for the org, optionally narrowed
4240
5075
  * by `packageId` and/or `type`. The list reads of `getMetaItems` only see
@@ -4268,14 +5103,25 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4268
5103
  const drafts = await repo.listDrafts({ packageId: request.packageId });
4269
5104
  const published = [];
4270
5105
  const failed = [];
4271
- for (const d of drafts) {
5106
+ const ordered = [
5107
+ ...drafts.filter((d) => d.type !== "seed"),
5108
+ ...drafts.filter((d) => d.type === "seed")
5109
+ ];
5110
+ const seedBodies = [];
5111
+ for (const d of ordered) {
4272
5112
  try {
5113
+ if (d.type === "seed") {
5114
+ const ref = { type: d.type, name: d.name, org: orgId ?? "env" };
5115
+ const draft = await repo.get(ref, { state: "draft" });
5116
+ if (draft?.body) seedBodies.push(draft.body);
5117
+ }
4273
5118
  const r = await this.publishMetaItem({
4274
5119
  type: d.type,
4275
5120
  name: d.name,
4276
5121
  ...request.organizationId ? { organizationId: request.organizationId } : {},
4277
5122
  ...request.actor ? { actor: request.actor } : {},
4278
- message: `publish app package '${request.packageId}'`
5123
+ message: `publish app package '${request.packageId}'`,
5124
+ _skipSeedApply: true
4279
5125
  });
4280
5126
  published.push({ type: d.type, name: d.name, version: r.version });
4281
5127
  } catch (e) {
@@ -4287,12 +5133,38 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4287
5133
  });
4288
5134
  }
4289
5135
  }
5136
+ const seedApplied = seedBodies.length > 0 ? await this.applySeedBodies(seedBodies, orgId) : void 0;
5137
+ let probes;
5138
+ if (published.length > 0) {
5139
+ try {
5140
+ const { runBuildProbes: runBuildProbes2 } = await Promise.resolve().then(() => (init_build_probes(), build_probes_exports));
5141
+ const analytics = this.getServicesRegistry?.().get("analytics");
5142
+ probes = await runBuildProbes2({
5143
+ engine: this.engine,
5144
+ getItem: async (type, name) => {
5145
+ const wrapper = await this.getMetaItem({
5146
+ type,
5147
+ name,
5148
+ ...orgId ? { organizationId: orgId } : {}
5149
+ });
5150
+ return wrapper?.item ?? wrapper ?? void 0;
5151
+ },
5152
+ published,
5153
+ ...analytics && typeof analytics.queryDataset === "function" ? { analytics } : {},
5154
+ organizationId: orgId
5155
+ });
5156
+ } catch {
5157
+ probes = void 0;
5158
+ }
5159
+ }
4290
5160
  return {
4291
5161
  success: failed.length === 0 && published.length > 0,
4292
5162
  publishedCount: published.length,
4293
5163
  failedCount: failed.length,
4294
5164
  published,
4295
- failed
5165
+ failed,
5166
+ ...seedApplied ? { seedApplied } : {},
5167
+ ...probes ? { probes } : {}
4296
5168
  };
4297
5169
  }
4298
5170
  /**
@@ -9156,6 +10028,10 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
9156
10028
  }
9157
10029
  return objects;
9158
10030
  }
10031
+
10032
+ // src/index.ts
10033
+ init_seed_loader();
10034
+ init_build_probes();
9159
10035
  export {
9160
10036
  DEFAULT_EXTENDER_PRIORITY,
9161
10037
  DEFAULT_OWNER_PRIORITY,
@@ -9170,6 +10046,7 @@ export {
9170
10046
  SECRET_REF_PREFIX,
9171
10047
  SchemaRegistry,
9172
10048
  ScopedContext,
10049
+ SeedLoaderService,
9173
10050
  SysMetadataRepository,
9174
10051
  ValidationError,
9175
10052
  applyInMemoryAggregation,
@@ -9188,6 +10065,7 @@ export {
9188
10065
  noopHookMetricsRecorder,
9189
10066
  parseFQN,
9190
10067
  parseSecretRef,
10068
+ runBuildProbes,
9191
10069
  toTitleCase,
9192
10070
  validateRecord,
9193
10071
  wrapDeclarativeHook