@objectstack/objectql 7.8.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +197 -11
- package/dist/index.d.ts +197 -11
- package/dist/index.js +531 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +531 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -2450,6 +2450,44 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2450
2450
|
}
|
|
2451
2451
|
} catch {
|
|
2452
2452
|
}
|
|
2453
|
+
if (request.previewDrafts) {
|
|
2454
|
+
try {
|
|
2455
|
+
const orgId = request.organizationId;
|
|
2456
|
+
const queryDrafts = async (oid) => {
|
|
2457
|
+
const whereClause = { type: request.type, state: "draft", organization_id: oid };
|
|
2458
|
+
if (packageId) whereClause.package_id = packageId;
|
|
2459
|
+
let rs = await this.engine.find("sys_metadata", { where: whereClause });
|
|
2460
|
+
if (!rs || rs.length === 0) {
|
|
2461
|
+
const alt = import_shared4.PLURAL_TO_SINGULAR[request.type] ?? import_shared4.SINGULAR_TO_PLURAL[request.type];
|
|
2462
|
+
if (alt) {
|
|
2463
|
+
const altWhere = { type: alt, state: "draft", organization_id: oid };
|
|
2464
|
+
if (packageId) altWhere.package_id = packageId;
|
|
2465
|
+
rs = await this.engine.find("sys_metadata", { where: altWhere });
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
return rs ?? [];
|
|
2469
|
+
};
|
|
2470
|
+
const draftRecords = [...await queryDrafts(null), ...orgId ? await queryDrafts(orgId) : []];
|
|
2471
|
+
if (draftRecords.length > 0) {
|
|
2472
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2473
|
+
for (const existing of items) {
|
|
2474
|
+
const entry = existing;
|
|
2475
|
+
if (entry && typeof entry === "object" && "name" in entry) byName.set(entry.name, entry);
|
|
2476
|
+
}
|
|
2477
|
+
for (const record of draftRecords) {
|
|
2478
|
+
const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
2479
|
+
if (data && typeof data === "object" && "name" in data) {
|
|
2480
|
+
const recPkg = record.package_id ?? void 0;
|
|
2481
|
+
if (recPkg && data._packageId === void 0) data._packageId = recPkg;
|
|
2482
|
+
data._draft = true;
|
|
2483
|
+
byName.set(data.name, data);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
items = Array.from(byName.values());
|
|
2487
|
+
}
|
|
2488
|
+
} catch {
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2453
2491
|
try {
|
|
2454
2492
|
const services = this.getServicesRegistry?.();
|
|
2455
2493
|
const metadataService = services?.get("metadata");
|
|
@@ -2508,6 +2546,34 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2508
2546
|
let item;
|
|
2509
2547
|
const orgId = request.organizationId;
|
|
2510
2548
|
const readState = request.state === "draft" ? "draft" : "active";
|
|
2549
|
+
if (request.previewDrafts && readState !== "draft") {
|
|
2550
|
+
try {
|
|
2551
|
+
const findDraft = async (oid) => {
|
|
2552
|
+
const rec = await this.engine.findOne("sys_metadata", {
|
|
2553
|
+
where: { type: request.type, name: request.name, state: "draft", organization_id: oid }
|
|
2554
|
+
});
|
|
2555
|
+
if (rec) return rec;
|
|
2556
|
+
const alt = import_shared4.PLURAL_TO_SINGULAR[request.type] ?? import_shared4.SINGULAR_TO_PLURAL[request.type];
|
|
2557
|
+
if (alt) {
|
|
2558
|
+
return await this.engine.findOne("sys_metadata", {
|
|
2559
|
+
where: { type: alt, name: request.name, state: "draft", organization_id: oid }
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
return void 0;
|
|
2563
|
+
};
|
|
2564
|
+
const draftRec = (orgId ? await findDraft(orgId) : void 0) ?? await findDraft(null);
|
|
2565
|
+
if (draftRec) {
|
|
2566
|
+
const draftItem = typeof draftRec.metadata === "string" ? JSON.parse(draftRec.metadata) : draftRec.metadata;
|
|
2567
|
+
if (draftItem && typeof draftItem === "object") {
|
|
2568
|
+
const recPkg = draftRec.package_id ?? void 0;
|
|
2569
|
+
if (recPkg && draftItem._packageId === void 0) draftItem._packageId = recPkg;
|
|
2570
|
+
draftItem._draft = true;
|
|
2571
|
+
}
|
|
2572
|
+
return { type: request.type, name: request.name, item: decorateMetadataItem(request.type, draftItem) };
|
|
2573
|
+
}
|
|
2574
|
+
} catch {
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2511
2577
|
try {
|
|
2512
2578
|
const findOverlay = async (oid) => {
|
|
2513
2579
|
const where = {
|
|
@@ -3882,6 +3948,37 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3882
3948
|
console.warn(`[Protocol] table sync failed for object '${name}': ${err?.message ?? err}`);
|
|
3883
3949
|
}
|
|
3884
3950
|
}
|
|
3951
|
+
/**
|
|
3952
|
+
* Inverse of {@link ensureObjectStorage}: drop an object's physical table.
|
|
3953
|
+
* DESTRUCTIVE — deletes the table and all its rows. Only invoked when a
|
|
3954
|
+
* delete explicitly opts into storage teardown (see {@link deleteMetaItem}'s
|
|
3955
|
+
* `dropStorage`), so publishing an object solely to preview it can be undone
|
|
3956
|
+
* without leaving an orphan table. Best-effort: a failure is logged, not
|
|
3957
|
+
* thrown — the metadata delete already succeeded, and a stray table is
|
|
3958
|
+
* reclaimed by the next sync/drop rather than blocking the delete.
|
|
3959
|
+
*/
|
|
3960
|
+
async dropObjectStorage(type, name) {
|
|
3961
|
+
if (type !== "object" && type !== "objects") return;
|
|
3962
|
+
try {
|
|
3963
|
+
await this.engine.dropObjectSchema(name);
|
|
3964
|
+
} catch (err) {
|
|
3965
|
+
console.warn(`[Protocol] table drop failed for object '${name}': ${err?.message ?? err}`);
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
/**
|
|
3969
|
+
* Guard for storage teardown on delete. Drops a physical table only when
|
|
3970
|
+
* the caller opted in AND it is safe: object types only (others have no
|
|
3971
|
+
* table), active state only (drafts were never materialised), and never a
|
|
3972
|
+
* `sys_`-prefixed platform table.
|
|
3973
|
+
*/
|
|
3974
|
+
shouldDropStorage(type, name, dropStorage, state) {
|
|
3975
|
+
if (!dropStorage) return false;
|
|
3976
|
+
const singular = import_shared4.PLURAL_TO_SINGULAR[type] ?? type;
|
|
3977
|
+
if (singular !== "object") return false;
|
|
3978
|
+
if (state !== "active") return false;
|
|
3979
|
+
if (name.startsWith("sys_")) return false;
|
|
3980
|
+
return true;
|
|
3981
|
+
}
|
|
3885
3982
|
async saveMetaItem(request) {
|
|
3886
3983
|
if (!request.item) {
|
|
3887
3984
|
throw new Error("Item data is required");
|
|
@@ -4262,6 +4359,100 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4262
4359
|
failed
|
|
4263
4360
|
};
|
|
4264
4361
|
}
|
|
4362
|
+
/**
|
|
4363
|
+
* Discard every pending DRAFT bound to a package — the NON-destructive
|
|
4364
|
+
* inverse of {@link publishPackageDrafts}. Drops only `state='draft'` rows
|
|
4365
|
+
* (via the per-item delete primitive), reverting the package to its last
|
|
4366
|
+
* published baseline; active/published metadata and physical tables are
|
|
4367
|
+
* left untouched.
|
|
4368
|
+
*
|
|
4369
|
+
* Use case: "I edited this app for a while and it turned out worse than
|
|
4370
|
+
* before — abandon all my changes." Routes through the sys_metadata path
|
|
4371
|
+
* (no metadata-service dependency, unlike `POST /packages/:id/revert`).
|
|
4372
|
+
*/
|
|
4373
|
+
async discardPackageDrafts(request) {
|
|
4374
|
+
await this.ensureOverlayIndex();
|
|
4375
|
+
const orgId = request.organizationId ?? null;
|
|
4376
|
+
const repo = this.getOverlayRepo(orgId);
|
|
4377
|
+
const drafts = await repo.listDrafts({ packageId: request.packageId });
|
|
4378
|
+
const discarded = [];
|
|
4379
|
+
const failed = [];
|
|
4380
|
+
for (const d of drafts) {
|
|
4381
|
+
try {
|
|
4382
|
+
await this.deleteMetaItem({
|
|
4383
|
+
type: d.type,
|
|
4384
|
+
name: d.name,
|
|
4385
|
+
state: "draft",
|
|
4386
|
+
...request.organizationId ? { organizationId: request.organizationId } : {},
|
|
4387
|
+
...request.actor ? { actor: request.actor } : {}
|
|
4388
|
+
});
|
|
4389
|
+
discarded.push({ type: d.type, name: d.name });
|
|
4390
|
+
} catch (e) {
|
|
4391
|
+
failed.push({
|
|
4392
|
+
type: d.type,
|
|
4393
|
+
name: d.name,
|
|
4394
|
+
error: e?.message ?? "discard failed",
|
|
4395
|
+
...e?.code ? { code: e.code } : {}
|
|
4396
|
+
});
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
return {
|
|
4400
|
+
success: failed.length === 0 && discarded.length > 0,
|
|
4401
|
+
discardedCount: discarded.length,
|
|
4402
|
+
failedCount: failed.length,
|
|
4403
|
+
discarded,
|
|
4404
|
+
failed
|
|
4405
|
+
};
|
|
4406
|
+
}
|
|
4407
|
+
/**
|
|
4408
|
+
* Delete an ENTIRE package: every `sys_metadata` row bound to it (active
|
|
4409
|
+
* AND draft) and — by default — the physical table of each object it
|
|
4410
|
+
* defined. DESTRUCTIVE: removes the app and its data. Use case: "I don't
|
|
4411
|
+
* want this package anymore."
|
|
4412
|
+
*
|
|
4413
|
+
* Set `keepData: true` to remove the metadata but preserve object tables.
|
|
4414
|
+
* The `sys_`-table guard in {@link deleteMetaItem} still applies, so
|
|
4415
|
+
* platform storage is never dropped. Drafts are removed before active rows
|
|
4416
|
+
* so each object's table is torn down once. Per-item failures are collected
|
|
4417
|
+
* without aborting the rest.
|
|
4418
|
+
*/
|
|
4419
|
+
async deletePackage(request) {
|
|
4420
|
+
const where = { package_id: request.packageId };
|
|
4421
|
+
if (request.organizationId) where.organization_id = request.organizationId;
|
|
4422
|
+
const rows = await this.engine.find("sys_metadata", { where });
|
|
4423
|
+
const dropStorage = request.keepData !== true;
|
|
4424
|
+
const ordered = [...rows].sort((a, b) => (a.state === "draft" ? 0 : 1) - (b.state === "draft" ? 0 : 1));
|
|
4425
|
+
const deleted = [];
|
|
4426
|
+
const failed = [];
|
|
4427
|
+
for (const row of ordered) {
|
|
4428
|
+
const state = row.state === "draft" ? "draft" : "active";
|
|
4429
|
+
try {
|
|
4430
|
+
await this.deleteMetaItem({
|
|
4431
|
+
type: row.type,
|
|
4432
|
+
name: row.name,
|
|
4433
|
+
state,
|
|
4434
|
+
...row.organization_id ? { organizationId: row.organization_id } : {},
|
|
4435
|
+
...request.actor ? { actor: request.actor } : {},
|
|
4436
|
+
...dropStorage ? { dropStorage: true } : {}
|
|
4437
|
+
});
|
|
4438
|
+
deleted.push({ type: row.type, name: row.name, state });
|
|
4439
|
+
} catch (e) {
|
|
4440
|
+
failed.push({
|
|
4441
|
+
type: row.type,
|
|
4442
|
+
name: row.name,
|
|
4443
|
+
error: e?.message ?? "delete failed",
|
|
4444
|
+
...e?.code ? { code: e.code } : {}
|
|
4445
|
+
});
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
return {
|
|
4449
|
+
success: failed.length === 0 && deleted.length > 0,
|
|
4450
|
+
deletedCount: deleted.length,
|
|
4451
|
+
failedCount: failed.length,
|
|
4452
|
+
deleted,
|
|
4453
|
+
failed
|
|
4454
|
+
};
|
|
4455
|
+
}
|
|
4265
4456
|
/**
|
|
4266
4457
|
* Restore the body recorded at history `toVersion` as the new
|
|
4267
4458
|
* live row. Writes a history event with `op='revert'`. 404
|
|
@@ -4494,6 +4685,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4494
4685
|
} catch {
|
|
4495
4686
|
}
|
|
4496
4687
|
}
|
|
4688
|
+
if (this.shouldDropStorage(request.type, request.name, request.dropStorage, targetState)) {
|
|
4689
|
+
await this.dropObjectStorage(singularTypeForRepo, request.name);
|
|
4690
|
+
}
|
|
4497
4691
|
await this.recordMetadataAudit({
|
|
4498
4692
|
type: request.type,
|
|
4499
4693
|
name: request.name,
|
|
@@ -4542,6 +4736,12 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4542
4736
|
};
|
|
4543
4737
|
}
|
|
4544
4738
|
await this.engine.delete("sys_metadata", { where: { id: existing.id } });
|
|
4739
|
+
{
|
|
4740
|
+
const targetState = request.state === "draft" ? "draft" : "active";
|
|
4741
|
+
if (this.shouldDropStorage(request.type, request.name, request.dropStorage, targetState)) {
|
|
4742
|
+
await this.dropObjectStorage(import_shared4.PLURAL_TO_SINGULAR[request.type] ?? request.type, request.name);
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4545
4745
|
if (this.environmentId === void 0) {
|
|
4546
4746
|
try {
|
|
4547
4747
|
const services = this.getServicesRegistry?.();
|
|
@@ -4903,6 +5103,7 @@ _ObjectStackProtocolImplementation.RUNTIME_CREATE_ALLOWED_TYPES = (() => {
|
|
|
4903
5103
|
var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
4904
5104
|
|
|
4905
5105
|
// src/engine.ts
|
|
5106
|
+
var import_node_async_hooks = require("async_hooks");
|
|
4906
5107
|
var import_kernel6 = require("@objectstack/spec/kernel");
|
|
4907
5108
|
var import_core = require("@objectstack/core");
|
|
4908
5109
|
var import_system2 = require("@objectstack/spec/system");
|
|
@@ -5395,7 +5596,7 @@ function optionValues(options) {
|
|
|
5395
5596
|
);
|
|
5396
5597
|
}
|
|
5397
5598
|
function validateOne(name, def, value) {
|
|
5398
|
-
if (def.required && isMissing(value)) {
|
|
5599
|
+
if (def.required && isMissing(value) && def.type !== "autonumber") {
|
|
5399
5600
|
return { field: name, code: "required", message: `${name} is required` };
|
|
5400
5601
|
}
|
|
5401
5602
|
if (isMissing(value)) return null;
|
|
@@ -5493,8 +5694,31 @@ var ajv = new import_ajv.default({ allErrors: true, strict: false });
|
|
|
5493
5694
|
var jsonSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
5494
5695
|
function needsPriorRecord(objectSchema) {
|
|
5495
5696
|
const rules = objectSchema?.validations;
|
|
5496
|
-
|
|
5497
|
-
return
|
|
5697
|
+
const ruleNeeds = Array.isArray(rules) && rules.some((r) => ruleNeedsPrior(r));
|
|
5698
|
+
return !!(ruleNeeds || fieldsNeedPrior(objectSchema?.fields));
|
|
5699
|
+
}
|
|
5700
|
+
function stripReadonlyWhenFields(objectSchema, data, previous, logger) {
|
|
5701
|
+
const fields = objectSchema?.fields;
|
|
5702
|
+
if (!fields || !data) return data;
|
|
5703
|
+
const merged = { ...previous ?? {}, ...data };
|
|
5704
|
+
let result = data;
|
|
5705
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
5706
|
+
if (!def?.readonlyWhen || !(name in data)) continue;
|
|
5707
|
+
const res = import_formula2.ExpressionEngine.evaluate(toExpression(def.readonlyWhen), {
|
|
5708
|
+
record: merged,
|
|
5709
|
+
previous: previous ?? void 0
|
|
5710
|
+
});
|
|
5711
|
+
if (!res.ok) {
|
|
5712
|
+
logger?.warn?.(`readonlyWhen for '${name}' failed to evaluate \u2014 change allowed through`);
|
|
5713
|
+
continue;
|
|
5714
|
+
}
|
|
5715
|
+
if (res.value === true) {
|
|
5716
|
+
if (result === data) result = { ...data };
|
|
5717
|
+
delete result[name];
|
|
5718
|
+
logger?.warn?.(`Field '${name}' is read-only (readonlyWhen) \u2014 ignoring incoming change`);
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
return result;
|
|
5498
5722
|
}
|
|
5499
5723
|
function ruleNeedsPrior(r) {
|
|
5500
5724
|
if (r == null || typeof r !== "object") return false;
|
|
@@ -5508,17 +5732,44 @@ function ruleNeedsPrior(r) {
|
|
|
5508
5732
|
}
|
|
5509
5733
|
return false;
|
|
5510
5734
|
}
|
|
5735
|
+
function isMissing2(v) {
|
|
5736
|
+
return v === void 0 || v === null || typeof v === "string" && v.trim() === "";
|
|
5737
|
+
}
|
|
5738
|
+
function fieldsNeedPrior(fields) {
|
|
5739
|
+
if (!fields) return false;
|
|
5740
|
+
return Object.values(fields).some(
|
|
5741
|
+
(f) => f && (f.requiredWhen || f.conditionalRequired || f.readonlyWhen)
|
|
5742
|
+
);
|
|
5743
|
+
}
|
|
5511
5744
|
function toExpression(cond) {
|
|
5512
5745
|
return typeof cond === "string" ? { dialect: "cel", source: cond } : cond;
|
|
5513
5746
|
}
|
|
5514
5747
|
function evaluateValidationRules(objectSchema, data, mode, opts = {}) {
|
|
5748
|
+
if (!data) return;
|
|
5515
5749
|
const rules = objectSchema?.validations;
|
|
5516
|
-
|
|
5750
|
+
const hasRules = Array.isArray(rules) && rules.length > 0;
|
|
5751
|
+
const fields = objectSchema?.fields;
|
|
5752
|
+
const hasFieldRules = fieldsNeedPrior(fields);
|
|
5753
|
+
if (!hasRules && !hasFieldRules) return;
|
|
5517
5754
|
const previous = opts.previous ?? void 0;
|
|
5518
5755
|
const merged = { ...previous ?? {}, ...data };
|
|
5519
5756
|
const ctx = { data, merged, previous, mode, logger: opts.logger };
|
|
5520
5757
|
const errors = [];
|
|
5521
|
-
|
|
5758
|
+
if (hasFieldRules && fields) {
|
|
5759
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
5760
|
+
const pred = def?.requiredWhen ?? def?.conditionalRequired;
|
|
5761
|
+
if (!pred) continue;
|
|
5762
|
+
const res = import_formula2.ExpressionEngine.evaluate(toExpression(pred), { record: merged, previous });
|
|
5763
|
+
if (!res.ok) {
|
|
5764
|
+
opts.logger?.warn?.(`requiredWhen for '${name}' failed to evaluate \u2014 skipped`);
|
|
5765
|
+
continue;
|
|
5766
|
+
}
|
|
5767
|
+
if (res.value === true && isMissing2(merged[name])) {
|
|
5768
|
+
errors.push({ field: name, code: "required", message: `${name} is required` });
|
|
5769
|
+
}
|
|
5770
|
+
}
|
|
5771
|
+
}
|
|
5772
|
+
const ordered = (hasRules ? rules : []).filter((r) => r != null && typeof r === "object").filter((r) => r.active !== false).filter((r) => {
|
|
5522
5773
|
const events = r.events ?? ["insert", "update"];
|
|
5523
5774
|
return events.includes(mode);
|
|
5524
5775
|
}).sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
|
@@ -5896,6 +6147,15 @@ function resolveMetadataItemName(key, item) {
|
|
|
5896
6147
|
}
|
|
5897
6148
|
var _ObjectQL = class _ObjectQL {
|
|
5898
6149
|
constructor(hostContext = {}) {
|
|
6150
|
+
/**
|
|
6151
|
+
* Ambient transaction store (ADR-0034). While a `transaction()` callback
|
|
6152
|
+
* runs, the active transaction handle lives here so that EVERY data
|
|
6153
|
+
* operation — including internal reads done during a write (reference
|
|
6154
|
+
* checks, hooks, expand) — automatically binds to the same connection
|
|
6155
|
+
* instead of asking the pool for another one and deadlocking on the
|
|
6156
|
+
* single-connection SQLite pool.
|
|
6157
|
+
*/
|
|
6158
|
+
this.txStore = new import_node_async_hooks.AsyncLocalStorage();
|
|
5899
6159
|
this.drivers = /* @__PURE__ */ new Map();
|
|
5900
6160
|
this.defaultDriver = null;
|
|
5901
6161
|
// Datasource mapping rules (imported from defineStack)
|
|
@@ -5937,6 +6197,12 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5937
6197
|
// getDriver()'s owner lookup would route CRUD to the wrong database. Each
|
|
5938
6198
|
// engine now owns its registry so kernels are fully isolated.
|
|
5939
6199
|
this._registry = new SchemaRegistry();
|
|
6200
|
+
/** In-memory next-value cache per `object.field` for autonumber generation,
|
|
6201
|
+
* lazily seeded from the current max in the store. */
|
|
6202
|
+
this.autonumberCounters = /* @__PURE__ */ new Map();
|
|
6203
|
+
/** Lazily-built index: child object name → roll-up summary descriptors on
|
|
6204
|
+
* parent objects that aggregate it. Invalidated when packages register. */
|
|
6205
|
+
this.summaryIndex = null;
|
|
5940
6206
|
this.hostContext = hostContext;
|
|
5941
6207
|
this.logger = hostContext.logger || (0, import_core.createLogger)({ level: "info", format: "pretty" });
|
|
5942
6208
|
if (process?.env?.OBJECTQL_STRICT_HOOKS === "1") {
|
|
@@ -6239,13 +6505,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6239
6505
|
* mask the system path.
|
|
6240
6506
|
*/
|
|
6241
6507
|
buildDriverOptions(execCtx, base) {
|
|
6242
|
-
const
|
|
6508
|
+
const tx = execCtx?.transaction !== void 0 ? execCtx.transaction : this.txStore.getStore()?.transaction;
|
|
6509
|
+
const hasTx = tx !== void 0;
|
|
6243
6510
|
const hasTenant = execCtx?.tenantId !== void 0;
|
|
6244
6511
|
const isSystem = execCtx?.isSystem === true;
|
|
6245
6512
|
if (!hasTx && !hasTenant && !isSystem) return base;
|
|
6246
6513
|
const opts = base && typeof base === "object" ? { ...base } : {};
|
|
6247
6514
|
if (hasTx && opts.transaction === void 0) {
|
|
6248
|
-
opts.transaction =
|
|
6515
|
+
opts.transaction = tx;
|
|
6249
6516
|
}
|
|
6250
6517
|
if (hasTenant && opts.tenantId === void 0) {
|
|
6251
6518
|
opts.tenantId = execCtx.tenantId;
|
|
@@ -6311,6 +6578,66 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6311
6578
|
}
|
|
6312
6579
|
return out;
|
|
6313
6580
|
}
|
|
6581
|
+
/**
|
|
6582
|
+
* Generate values for empty `autonumber` fields on insert — ONLY for drivers
|
|
6583
|
+
* that do not generate them natively (memory, mongodb). For SQL-backed objects
|
|
6584
|
+
* the driver owns a persistent, atomic `_objectstack_sequences` table and
|
|
6585
|
+
* advertises `supports.autonumber === true`; the engine then defers entirely
|
|
6586
|
+
* and never pre-fills (so the persistent sequence is the single source of
|
|
6587
|
+
* truth — see #1603). Required-validation exempts `autonumber` either way, so
|
|
6588
|
+
* a `required` record number is never rejected for "missing" — the runtime
|
|
6589
|
+
* owns the value, not the client.
|
|
6590
|
+
*
|
|
6591
|
+
* In the fallback path the next value is `max(existing) + 1`, seeded once per
|
|
6592
|
+
* `object.field` from the store then incremented in memory (monotonic within
|
|
6593
|
+
* the process, resilient to deletions). `autonumberFormat` is honored, e.g.
|
|
6594
|
+
* `CASE-{0000}` → `CASE-0042`. NOTE: this in-memory seeding is single-instance.
|
|
6595
|
+
*/
|
|
6596
|
+
async applyAutonumbers(object, record, execCtx, driverOwnsAutonumber) {
|
|
6597
|
+
if (driverOwnsAutonumber) return;
|
|
6598
|
+
const fields = this.getSchema(object)?.fields;
|
|
6599
|
+
if (!fields || typeof fields !== "object" || Array.isArray(fields)) return;
|
|
6600
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
6601
|
+
if (def?.type !== "autonumber") continue;
|
|
6602
|
+
const current = record[name];
|
|
6603
|
+
if (current != null && current !== "") continue;
|
|
6604
|
+
const key = `${object}.${name}`;
|
|
6605
|
+
let next = this.autonumberCounters.get(key);
|
|
6606
|
+
if (next == null) next = await this.seedAutonumber(object, name, execCtx);
|
|
6607
|
+
next += 1;
|
|
6608
|
+
this.autonumberCounters.set(key, next);
|
|
6609
|
+
const fmt = def.autonumberFormat ?? def.format;
|
|
6610
|
+
record[name] = this.formatAutonumber(fmt, next);
|
|
6611
|
+
}
|
|
6612
|
+
}
|
|
6613
|
+
/** Seed the autonumber counter from the current max numeric value in store. */
|
|
6614
|
+
async seedAutonumber(object, field, execCtx) {
|
|
6615
|
+
try {
|
|
6616
|
+
const rows = await this.find(object, {
|
|
6617
|
+
select: ["id", field],
|
|
6618
|
+
limit: 5e3,
|
|
6619
|
+
context: execCtx
|
|
6620
|
+
});
|
|
6621
|
+
let max = 0;
|
|
6622
|
+
for (const r of rows || []) {
|
|
6623
|
+
const v = r?.[field];
|
|
6624
|
+
if (v == null) continue;
|
|
6625
|
+
const m = String(v).match(/(\d+)(?!.*\d)/);
|
|
6626
|
+
if (m) max = Math.max(max, parseInt(m[1], 10) || 0);
|
|
6627
|
+
}
|
|
6628
|
+
return max;
|
|
6629
|
+
} catch {
|
|
6630
|
+
return 0;
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
/** Apply an autonumber format like `CASE-{0000}`; default to the bare number. */
|
|
6634
|
+
formatAutonumber(format, value) {
|
|
6635
|
+
if (!format) return String(value);
|
|
6636
|
+
const m = format.match(/\{(0+)\}/);
|
|
6637
|
+
if (!m) return format.includes("{0}") ? format.replace("{0}", String(value)) : `${format}${value}`;
|
|
6638
|
+
const padded = String(value).padStart(m[1].length, "0");
|
|
6639
|
+
return format.replace(m[0], padded);
|
|
6640
|
+
}
|
|
6314
6641
|
/**
|
|
6315
6642
|
* Register contribution (Manifest)
|
|
6316
6643
|
*
|
|
@@ -6324,6 +6651,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6324
6651
|
registerApp(manifest) {
|
|
6325
6652
|
const id = manifest.id || manifest.name;
|
|
6326
6653
|
const namespace = manifest.namespace;
|
|
6654
|
+
this.invalidateSummaryIndex();
|
|
6327
6655
|
this.logger.debug("Registering package manifest", { id, namespace });
|
|
6328
6656
|
console.warn(`[ObjectQL:registerApp] id=${id} flows=${Array.isArray(manifest.flows) ? manifest.flows.length : typeof manifest.flows} keys=${Object.keys(manifest).join(",")}`);
|
|
6329
6657
|
if (id) {
|
|
@@ -6404,6 +6732,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6404
6732
|
"pages",
|
|
6405
6733
|
"dashboards",
|
|
6406
6734
|
"reports",
|
|
6735
|
+
"datasets",
|
|
6407
6736
|
"themes",
|
|
6408
6737
|
// Automation Protocol
|
|
6409
6738
|
"flows",
|
|
@@ -6548,6 +6877,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6548
6877
|
"pages",
|
|
6549
6878
|
"dashboards",
|
|
6550
6879
|
"reports",
|
|
6880
|
+
"datasets",
|
|
6551
6881
|
"themes",
|
|
6552
6882
|
"flows",
|
|
6553
6883
|
"workflows",
|
|
@@ -6929,6 +7259,102 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6929
7259
|
}
|
|
6930
7260
|
this.logger.info("ObjectQL engine destroyed");
|
|
6931
7261
|
}
|
|
7262
|
+
/** Invalidate the cached roll-up summary index (call when metadata changes). */
|
|
7263
|
+
invalidateSummaryIndex() {
|
|
7264
|
+
this.summaryIndex = null;
|
|
7265
|
+
}
|
|
7266
|
+
/** Scan all registered objects for `summary` fields and index them by the
|
|
7267
|
+
* child object they aggregate, resolving the child→parent FK field. */
|
|
7268
|
+
buildSummaryIndex() {
|
|
7269
|
+
const index = /* @__PURE__ */ new Map();
|
|
7270
|
+
let objects = [];
|
|
7271
|
+
try {
|
|
7272
|
+
objects = this._registry.getAllObjects?.() ?? [];
|
|
7273
|
+
} catch {
|
|
7274
|
+
objects = [];
|
|
7275
|
+
}
|
|
7276
|
+
for (const parent of objects) {
|
|
7277
|
+
const fields = parent?.fields;
|
|
7278
|
+
if (!fields || typeof fields !== "object" || Array.isArray(fields)) continue;
|
|
7279
|
+
for (const [summaryField, def] of Object.entries(fields)) {
|
|
7280
|
+
const d = def;
|
|
7281
|
+
if (d?.type !== "summary" || !d.summaryOperations) continue;
|
|
7282
|
+
const so = d.summaryOperations;
|
|
7283
|
+
const childObject = so.object;
|
|
7284
|
+
const fn = so.function;
|
|
7285
|
+
if (!childObject || !fn) continue;
|
|
7286
|
+
let fkField = so.relationshipField;
|
|
7287
|
+
if (!fkField) {
|
|
7288
|
+
const child = this._registry.getObject(childObject);
|
|
7289
|
+
const cfields = child?.fields || {};
|
|
7290
|
+
for (const [cfName, cdef] of Object.entries(cfields)) {
|
|
7291
|
+
const cd = cdef;
|
|
7292
|
+
if ((cd?.type === "master_detail" || cd?.type === "lookup") && cd?.reference === parent.name) {
|
|
7293
|
+
fkField = cfName;
|
|
7294
|
+
break;
|
|
7295
|
+
}
|
|
7296
|
+
}
|
|
7297
|
+
}
|
|
7298
|
+
if (!fkField) continue;
|
|
7299
|
+
const list = index.get(childObject) ?? [];
|
|
7300
|
+
list.push({ parentObject: parent.name, summaryField, fkField, fn, sourceField: so.field });
|
|
7301
|
+
index.set(childObject, list);
|
|
7302
|
+
}
|
|
7303
|
+
}
|
|
7304
|
+
return index;
|
|
7305
|
+
}
|
|
7306
|
+
getSummaryDescriptors(childObject) {
|
|
7307
|
+
if (!this.summaryIndex) this.summaryIndex = this.buildSummaryIndex();
|
|
7308
|
+
return this.summaryIndex.get(childObject) ?? [];
|
|
7309
|
+
}
|
|
7310
|
+
/**
|
|
7311
|
+
* Recompute roll-up `summary` fields on parent records after a child write.
|
|
7312
|
+
* For each affected parent (the FK value on the changed/old child record), it
|
|
7313
|
+
* aggregates the child collection and writes the result onto the parent's
|
|
7314
|
+
* summary field. Runs in the caller's execution context so it joins the same
|
|
7315
|
+
* transaction (e.g. the cross-object batch) when one is open.
|
|
7316
|
+
*/
|
|
7317
|
+
async recomputeSummaries(childObject, records, previous, execCtx) {
|
|
7318
|
+
const descriptors = this.getSummaryDescriptors(childObject);
|
|
7319
|
+
if (descriptors.length === 0) return;
|
|
7320
|
+
const recs = Array.isArray(records) ? records : records ? [records] : [];
|
|
7321
|
+
const prevs = Array.isArray(previous) ? previous : previous ? [previous] : [];
|
|
7322
|
+
for (const desc of descriptors) {
|
|
7323
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7324
|
+
for (const r of recs) {
|
|
7325
|
+
const v = r?.[desc.fkField];
|
|
7326
|
+
if (v != null && v !== "") ids.add(String(v));
|
|
7327
|
+
}
|
|
7328
|
+
for (const p of prevs) {
|
|
7329
|
+
const v = p?.[desc.fkField];
|
|
7330
|
+
if (v != null && v !== "") ids.add(String(v));
|
|
7331
|
+
}
|
|
7332
|
+
for (const parentId of ids) {
|
|
7333
|
+
try {
|
|
7334
|
+
const rows = await this.aggregate(childObject, {
|
|
7335
|
+
where: { [desc.fkField]: parentId },
|
|
7336
|
+
aggregations: [{
|
|
7337
|
+
function: desc.fn,
|
|
7338
|
+
...desc.fn === "count" ? {} : { field: desc.sourceField },
|
|
7339
|
+
alias: "value"
|
|
7340
|
+
}],
|
|
7341
|
+
context: execCtx
|
|
7342
|
+
});
|
|
7343
|
+
let value = rows?.[0]?.value;
|
|
7344
|
+
if (value == null) value = desc.fn === "count" || desc.fn === "sum" ? 0 : null;
|
|
7345
|
+
await this.update(desc.parentObject, { id: parentId, [desc.summaryField]: value }, { context: execCtx });
|
|
7346
|
+
} catch (err) {
|
|
7347
|
+
this.logger.warn("Roll-up summary recompute failed", {
|
|
7348
|
+
childObject,
|
|
7349
|
+
parentObject: desc.parentObject,
|
|
7350
|
+
parentId,
|
|
7351
|
+
field: desc.summaryField,
|
|
7352
|
+
error: err?.message
|
|
7353
|
+
});
|
|
7354
|
+
}
|
|
7355
|
+
}
|
|
7356
|
+
}
|
|
7357
|
+
}
|
|
6932
7358
|
/**
|
|
6933
7359
|
* Post-process expand: resolve lookup/master_detail fields by batch-loading related records.
|
|
6934
7360
|
*
|
|
@@ -7144,10 +7570,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7144
7570
|
let result;
|
|
7145
7571
|
const nowSnap = /* @__PURE__ */ new Date();
|
|
7146
7572
|
const schemaForValidation = this._registry.getObject(object);
|
|
7573
|
+
const driverOwnsAutonumber = driver?.supports?.autonumber === true;
|
|
7147
7574
|
if (Array.isArray(hookContext.input.data)) {
|
|
7148
7575
|
const rows = hookContext.input.data.map(
|
|
7149
7576
|
(row) => this.applyFieldDefaults(object, row, opCtx.context, nowSnap)
|
|
7150
7577
|
);
|
|
7578
|
+
for (const r of rows) {
|
|
7579
|
+
await this.applyAutonumbers(object, r, opCtx.context, driverOwnsAutonumber);
|
|
7580
|
+
}
|
|
7151
7581
|
for (const r of rows) {
|
|
7152
7582
|
await this.encryptSecretFields(object, r, opCtx.context, hookContext.input.options);
|
|
7153
7583
|
}
|
|
@@ -7167,6 +7597,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7167
7597
|
opCtx.context,
|
|
7168
7598
|
nowSnap
|
|
7169
7599
|
);
|
|
7600
|
+
await this.applyAutonumbers(object, row, opCtx.context, driverOwnsAutonumber);
|
|
7170
7601
|
await this.encryptSecretFields(object, row, opCtx.context, hookContext.input.options);
|
|
7171
7602
|
validateRecord(schemaForValidation, row, "insert");
|
|
7172
7603
|
evaluateValidationRules(schemaForValidation, row, "insert", { logger: this.logger });
|
|
@@ -7175,6 +7606,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7175
7606
|
hookContext.event = "afterInsert";
|
|
7176
7607
|
hookContext.result = result;
|
|
7177
7608
|
await this.triggerHooks("afterInsert", hookContext);
|
|
7609
|
+
await this.recomputeSummaries(object, result, null, opCtx.context);
|
|
7178
7610
|
if (this.realtimeService) {
|
|
7179
7611
|
try {
|
|
7180
7612
|
if (Array.isArray(result)) {
|
|
@@ -7259,6 +7691,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7259
7691
|
const priorAst = { object, where: { id: hookContext.input.id }, limit: 1 };
|
|
7260
7692
|
priorRecord = await driver.findOne(object, priorAst, hookContext.input.options);
|
|
7261
7693
|
}
|
|
7694
|
+
hookContext.input.data = stripReadonlyWhenFields(updateSchema, hookContext.input.data, priorRecord, this.logger);
|
|
7262
7695
|
evaluateValidationRules(updateSchema, hookContext.input.data, "update", { previous: priorRecord, logger: this.logger });
|
|
7263
7696
|
result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
|
|
7264
7697
|
} else if (options?.multi && driver.updateMany) {
|
|
@@ -7276,6 +7709,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7276
7709
|
hookContext.result = result;
|
|
7277
7710
|
if (priorRecord) hookContext.previous = priorRecord;
|
|
7278
7711
|
await this.triggerHooks("afterUpdate", hookContext);
|
|
7712
|
+
await this.recomputeSummaries(object, result, priorRecord, opCtx.context);
|
|
7279
7713
|
if (this.realtimeService) {
|
|
7280
7714
|
try {
|
|
7281
7715
|
const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
|
|
@@ -7304,6 +7738,66 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7304
7738
|
});
|
|
7305
7739
|
return opCtx.result;
|
|
7306
7740
|
}
|
|
7741
|
+
/**
|
|
7742
|
+
* Apply referential delete behavior for relations pointing AT this record,
|
|
7743
|
+
* before it is removed. For every registered object with a `master_detail`
|
|
7744
|
+
* or `lookup` field referencing `object`, honor the field's `deleteBehavior`:
|
|
7745
|
+
* - `cascade` → delete the dependent rows (recursively, so grandchildren
|
|
7746
|
+
* are handled by each child's own delete),
|
|
7747
|
+
* - `set_null` → clear the foreign key,
|
|
7748
|
+
* - `restrict` → refuse the delete when dependents exist.
|
|
7749
|
+
* `master_detail` defaults to `cascade` (the parent owns the child
|
|
7750
|
+
* lifecycle); `lookup` defaults to `set_null`. Only runs for single-id
|
|
7751
|
+
* deletes — multi/predicate deletes skip cascade (logged).
|
|
7752
|
+
*/
|
|
7753
|
+
async cascadeDeleteRelations(object, id, context, depth = 0) {
|
|
7754
|
+
if (id == null || depth >= _ObjectQL.MAX_CASCADE_DEPTH) return;
|
|
7755
|
+
let objects;
|
|
7756
|
+
try {
|
|
7757
|
+
objects = this._registry.getAllObjects();
|
|
7758
|
+
} catch {
|
|
7759
|
+
return;
|
|
7760
|
+
}
|
|
7761
|
+
for (const child of objects) {
|
|
7762
|
+
const childName = child?.name;
|
|
7763
|
+
const fields = child?.fields;
|
|
7764
|
+
if (!childName || !fields) continue;
|
|
7765
|
+
for (const [fieldName, fdef] of Object.entries(fields)) {
|
|
7766
|
+
if (!fdef || fdef.type !== "master_detail" && fdef.type !== "lookup") continue;
|
|
7767
|
+
const ref = fdef.reference;
|
|
7768
|
+
if (!ref) continue;
|
|
7769
|
+
let resolvedRef;
|
|
7770
|
+
try {
|
|
7771
|
+
resolvedRef = this.resolveObjectName(ref);
|
|
7772
|
+
} catch {
|
|
7773
|
+
resolvedRef = void 0;
|
|
7774
|
+
}
|
|
7775
|
+
if (ref !== object && resolvedRef !== object) continue;
|
|
7776
|
+
const behavior = fdef.type === "master_detail" ? fdef.deleteBehavior === "restrict" ? "restrict" : "cascade" : fdef.deleteBehavior || "set_null";
|
|
7777
|
+
let dependents;
|
|
7778
|
+
try {
|
|
7779
|
+
dependents = await this.find(childName, { where: { [fieldName]: id }, context });
|
|
7780
|
+
} catch {
|
|
7781
|
+
continue;
|
|
7782
|
+
}
|
|
7783
|
+
if (!dependents || dependents.length === 0) continue;
|
|
7784
|
+
if (behavior === "restrict") {
|
|
7785
|
+
throw new Error(
|
|
7786
|
+
`Cannot delete ${object} (${id}): ${dependents.length} dependent ${childName} record(s) via ${fieldName}`
|
|
7787
|
+
);
|
|
7788
|
+
}
|
|
7789
|
+
for (const dep of dependents) {
|
|
7790
|
+
const depId = dep?.id;
|
|
7791
|
+
if (depId == null) continue;
|
|
7792
|
+
if (behavior === "cascade") {
|
|
7793
|
+
await this.delete(childName, { where: { id: depId }, context });
|
|
7794
|
+
} else {
|
|
7795
|
+
await this.update(childName, { id: depId, [fieldName]: null }, { context });
|
|
7796
|
+
}
|
|
7797
|
+
}
|
|
7798
|
+
}
|
|
7799
|
+
}
|
|
7800
|
+
}
|
|
7307
7801
|
async delete(object, options) {
|
|
7308
7802
|
object = this.resolveObjectName(object);
|
|
7309
7803
|
this.logger.debug("Delete operation starting", { object });
|
|
@@ -7333,7 +7827,15 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7333
7827
|
hookContext.input.options = this.buildDriverOptions(opCtx.context, hookContext.input.options);
|
|
7334
7828
|
try {
|
|
7335
7829
|
let result;
|
|
7830
|
+
let summaryPrev = null;
|
|
7831
|
+
if (hookContext.input.id && this.getSummaryDescriptors(object).length > 0) {
|
|
7832
|
+
try {
|
|
7833
|
+
summaryPrev = await this.findOne(object, { where: { id: hookContext.input.id }, context: opCtx.context });
|
|
7834
|
+
} catch {
|
|
7835
|
+
}
|
|
7836
|
+
}
|
|
7336
7837
|
if (hookContext.input.id) {
|
|
7838
|
+
await this.cascadeDeleteRelations(object, hookContext.input.id, opCtx.context);
|
|
7337
7839
|
result = await driver.delete(object, hookContext.input.id, hookContext.input.options);
|
|
7338
7840
|
} else if (options?.multi && driver.deleteMany) {
|
|
7339
7841
|
const ast = { object, where: options.where };
|
|
@@ -7344,6 +7846,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7344
7846
|
hookContext.event = "afterDelete";
|
|
7345
7847
|
hookContext.result = result;
|
|
7346
7848
|
await this.triggerHooks("afterDelete", hookContext);
|
|
7849
|
+
if (summaryPrev) await this.recomputeSummaries(object, null, summaryPrev, opCtx.context);
|
|
7347
7850
|
if (this.realtimeService) {
|
|
7348
7851
|
try {
|
|
7349
7852
|
const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
|
|
@@ -7493,7 +7996,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7493
7996
|
const trx = await drv.beginTransaction();
|
|
7494
7997
|
const trxCtx = { ...baseContext ?? {}, transaction: trx };
|
|
7495
7998
|
try {
|
|
7496
|
-
const result = await callback(trxCtx);
|
|
7999
|
+
const result = await this.txStore.run({ transaction: trx }, () => callback(trxCtx));
|
|
7497
8000
|
if (drv.commit) await drv.commit(trx);
|
|
7498
8001
|
else if (drv.commitTransaction) await drv.commitTransaction(trx);
|
|
7499
8002
|
return result;
|
|
@@ -7643,6 +8146,22 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7643
8146
|
const tableName = import_system2.StorageNameMapping.resolveTableName(obj);
|
|
7644
8147
|
await driver.syncSchema(tableName, obj);
|
|
7645
8148
|
}
|
|
8149
|
+
/**
|
|
8150
|
+
* Drop the physical storage (table/collection) backing an object — the
|
|
8151
|
+
* inverse of {@link syncObjectSchema}. DESTRUCTIVE: deletes all rows in the
|
|
8152
|
+
* table. Used by the protocol delete path when the caller explicitly opts
|
|
8153
|
+
* into storage teardown (e.g. discarding an object that was published only
|
|
8154
|
+
* to preview it). No-op when the object's driver does not expose `dropTable`.
|
|
8155
|
+
* Resolves the physical table name from the registered definition, falling
|
|
8156
|
+
* back to the bare name if the def was already removed.
|
|
8157
|
+
*/
|
|
8158
|
+
async dropObjectSchema(objectName) {
|
|
8159
|
+
const obj = this._registry.getObject(objectName);
|
|
8160
|
+
const driver = this.getDriverForObject(objectName);
|
|
8161
|
+
if (!driver || typeof driver.dropTable !== "function") return;
|
|
8162
|
+
const tableName = import_system2.StorageNameMapping.resolveTableName(obj ?? { name: objectName });
|
|
8163
|
+
await driver.dropTable(tableName);
|
|
8164
|
+
}
|
|
7646
8165
|
/**
|
|
7647
8166
|
* Get a registered driver by datasource name.
|
|
7648
8167
|
* Alias matching @objectql/core datasource() API.
|
|
@@ -7741,6 +8260,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7741
8260
|
// ============================================
|
|
7742
8261
|
/** Maximum depth for recursive expand to prevent infinite loops */
|
|
7743
8262
|
_ObjectQL.MAX_EXPAND_DEPTH = 3;
|
|
8263
|
+
_ObjectQL.MAX_CASCADE_DEPTH = 10;
|
|
7744
8264
|
var ObjectQL = _ObjectQL;
|
|
7745
8265
|
var ObjectRepository = class {
|
|
7746
8266
|
constructor(objectName, context, engine) {
|
|
@@ -7858,8 +8378,10 @@ var ScopedContext = class _ScopedContext {
|
|
|
7858
8378
|
{ ...this.executionContext, transaction: trx },
|
|
7859
8379
|
this.engine
|
|
7860
8380
|
);
|
|
8381
|
+
const txStore = this.engine?.txStore;
|
|
8382
|
+
const runIn = (fn) => txStore ? txStore.run({ transaction: trx }, fn) : fn();
|
|
7861
8383
|
try {
|
|
7862
|
-
const result = await callback(trxCtx);
|
|
8384
|
+
const result = await runIn(() => callback(trxCtx));
|
|
7863
8385
|
if (driver.commit) await driver.commit(trx);
|
|
7864
8386
|
else if (driver.commitTransaction) await driver.commitTransaction(trx);
|
|
7865
8387
|
return result;
|