@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.mjs
CHANGED
|
@@ -2386,6 +2386,44 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2386
2386
|
}
|
|
2387
2387
|
} catch {
|
|
2388
2388
|
}
|
|
2389
|
+
if (request.previewDrafts) {
|
|
2390
|
+
try {
|
|
2391
|
+
const orgId = request.organizationId;
|
|
2392
|
+
const queryDrafts = async (oid) => {
|
|
2393
|
+
const whereClause = { type: request.type, state: "draft", organization_id: oid };
|
|
2394
|
+
if (packageId) whereClause.package_id = packageId;
|
|
2395
|
+
let rs = await this.engine.find("sys_metadata", { where: whereClause });
|
|
2396
|
+
if (!rs || rs.length === 0) {
|
|
2397
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
2398
|
+
if (alt) {
|
|
2399
|
+
const altWhere = { type: alt, state: "draft", organization_id: oid };
|
|
2400
|
+
if (packageId) altWhere.package_id = packageId;
|
|
2401
|
+
rs = await this.engine.find("sys_metadata", { where: altWhere });
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
return rs ?? [];
|
|
2405
|
+
};
|
|
2406
|
+
const draftRecords = [...await queryDrafts(null), ...orgId ? await queryDrafts(orgId) : []];
|
|
2407
|
+
if (draftRecords.length > 0) {
|
|
2408
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2409
|
+
for (const existing of items) {
|
|
2410
|
+
const entry = existing;
|
|
2411
|
+
if (entry && typeof entry === "object" && "name" in entry) byName.set(entry.name, entry);
|
|
2412
|
+
}
|
|
2413
|
+
for (const record of draftRecords) {
|
|
2414
|
+
const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
2415
|
+
if (data && typeof data === "object" && "name" in data) {
|
|
2416
|
+
const recPkg = record.package_id ?? void 0;
|
|
2417
|
+
if (recPkg && data._packageId === void 0) data._packageId = recPkg;
|
|
2418
|
+
data._draft = true;
|
|
2419
|
+
byName.set(data.name, data);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
items = Array.from(byName.values());
|
|
2423
|
+
}
|
|
2424
|
+
} catch {
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2389
2427
|
try {
|
|
2390
2428
|
const services = this.getServicesRegistry?.();
|
|
2391
2429
|
const metadataService = services?.get("metadata");
|
|
@@ -2444,6 +2482,34 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2444
2482
|
let item;
|
|
2445
2483
|
const orgId = request.organizationId;
|
|
2446
2484
|
const readState = request.state === "draft" ? "draft" : "active";
|
|
2485
|
+
if (request.previewDrafts && readState !== "draft") {
|
|
2486
|
+
try {
|
|
2487
|
+
const findDraft = async (oid) => {
|
|
2488
|
+
const rec = await this.engine.findOne("sys_metadata", {
|
|
2489
|
+
where: { type: request.type, name: request.name, state: "draft", organization_id: oid }
|
|
2490
|
+
});
|
|
2491
|
+
if (rec) return rec;
|
|
2492
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
2493
|
+
if (alt) {
|
|
2494
|
+
return await this.engine.findOne("sys_metadata", {
|
|
2495
|
+
where: { type: alt, name: request.name, state: "draft", organization_id: oid }
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
return void 0;
|
|
2499
|
+
};
|
|
2500
|
+
const draftRec = (orgId ? await findDraft(orgId) : void 0) ?? await findDraft(null);
|
|
2501
|
+
if (draftRec) {
|
|
2502
|
+
const draftItem = typeof draftRec.metadata === "string" ? JSON.parse(draftRec.metadata) : draftRec.metadata;
|
|
2503
|
+
if (draftItem && typeof draftItem === "object") {
|
|
2504
|
+
const recPkg = draftRec.package_id ?? void 0;
|
|
2505
|
+
if (recPkg && draftItem._packageId === void 0) draftItem._packageId = recPkg;
|
|
2506
|
+
draftItem._draft = true;
|
|
2507
|
+
}
|
|
2508
|
+
return { type: request.type, name: request.name, item: decorateMetadataItem(request.type, draftItem) };
|
|
2509
|
+
}
|
|
2510
|
+
} catch {
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2447
2513
|
try {
|
|
2448
2514
|
const findOverlay = async (oid) => {
|
|
2449
2515
|
const where = {
|
|
@@ -3818,6 +3884,37 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3818
3884
|
console.warn(`[Protocol] table sync failed for object '${name}': ${err?.message ?? err}`);
|
|
3819
3885
|
}
|
|
3820
3886
|
}
|
|
3887
|
+
/**
|
|
3888
|
+
* Inverse of {@link ensureObjectStorage}: drop an object's physical table.
|
|
3889
|
+
* DESTRUCTIVE — deletes the table and all its rows. Only invoked when a
|
|
3890
|
+
* delete explicitly opts into storage teardown (see {@link deleteMetaItem}'s
|
|
3891
|
+
* `dropStorage`), so publishing an object solely to preview it can be undone
|
|
3892
|
+
* without leaving an orphan table. Best-effort: a failure is logged, not
|
|
3893
|
+
* thrown — the metadata delete already succeeded, and a stray table is
|
|
3894
|
+
* reclaimed by the next sync/drop rather than blocking the delete.
|
|
3895
|
+
*/
|
|
3896
|
+
async dropObjectStorage(type, name) {
|
|
3897
|
+
if (type !== "object" && type !== "objects") return;
|
|
3898
|
+
try {
|
|
3899
|
+
await this.engine.dropObjectSchema(name);
|
|
3900
|
+
} catch (err) {
|
|
3901
|
+
console.warn(`[Protocol] table drop failed for object '${name}': ${err?.message ?? err}`);
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
/**
|
|
3905
|
+
* Guard for storage teardown on delete. Drops a physical table only when
|
|
3906
|
+
* the caller opted in AND it is safe: object types only (others have no
|
|
3907
|
+
* table), active state only (drafts were never materialised), and never a
|
|
3908
|
+
* `sys_`-prefixed platform table.
|
|
3909
|
+
*/
|
|
3910
|
+
shouldDropStorage(type, name, dropStorage, state) {
|
|
3911
|
+
if (!dropStorage) return false;
|
|
3912
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
3913
|
+
if (singular !== "object") return false;
|
|
3914
|
+
if (state !== "active") return false;
|
|
3915
|
+
if (name.startsWith("sys_")) return false;
|
|
3916
|
+
return true;
|
|
3917
|
+
}
|
|
3821
3918
|
async saveMetaItem(request) {
|
|
3822
3919
|
if (!request.item) {
|
|
3823
3920
|
throw new Error("Item data is required");
|
|
@@ -4198,6 +4295,100 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4198
4295
|
failed
|
|
4199
4296
|
};
|
|
4200
4297
|
}
|
|
4298
|
+
/**
|
|
4299
|
+
* Discard every pending DRAFT bound to a package — the NON-destructive
|
|
4300
|
+
* inverse of {@link publishPackageDrafts}. Drops only `state='draft'` rows
|
|
4301
|
+
* (via the per-item delete primitive), reverting the package to its last
|
|
4302
|
+
* published baseline; active/published metadata and physical tables are
|
|
4303
|
+
* left untouched.
|
|
4304
|
+
*
|
|
4305
|
+
* Use case: "I edited this app for a while and it turned out worse than
|
|
4306
|
+
* before — abandon all my changes." Routes through the sys_metadata path
|
|
4307
|
+
* (no metadata-service dependency, unlike `POST /packages/:id/revert`).
|
|
4308
|
+
*/
|
|
4309
|
+
async discardPackageDrafts(request) {
|
|
4310
|
+
await this.ensureOverlayIndex();
|
|
4311
|
+
const orgId = request.organizationId ?? null;
|
|
4312
|
+
const repo = this.getOverlayRepo(orgId);
|
|
4313
|
+
const drafts = await repo.listDrafts({ packageId: request.packageId });
|
|
4314
|
+
const discarded = [];
|
|
4315
|
+
const failed = [];
|
|
4316
|
+
for (const d of drafts) {
|
|
4317
|
+
try {
|
|
4318
|
+
await this.deleteMetaItem({
|
|
4319
|
+
type: d.type,
|
|
4320
|
+
name: d.name,
|
|
4321
|
+
state: "draft",
|
|
4322
|
+
...request.organizationId ? { organizationId: request.organizationId } : {},
|
|
4323
|
+
...request.actor ? { actor: request.actor } : {}
|
|
4324
|
+
});
|
|
4325
|
+
discarded.push({ type: d.type, name: d.name });
|
|
4326
|
+
} catch (e) {
|
|
4327
|
+
failed.push({
|
|
4328
|
+
type: d.type,
|
|
4329
|
+
name: d.name,
|
|
4330
|
+
error: e?.message ?? "discard failed",
|
|
4331
|
+
...e?.code ? { code: e.code } : {}
|
|
4332
|
+
});
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
return {
|
|
4336
|
+
success: failed.length === 0 && discarded.length > 0,
|
|
4337
|
+
discardedCount: discarded.length,
|
|
4338
|
+
failedCount: failed.length,
|
|
4339
|
+
discarded,
|
|
4340
|
+
failed
|
|
4341
|
+
};
|
|
4342
|
+
}
|
|
4343
|
+
/**
|
|
4344
|
+
* Delete an ENTIRE package: every `sys_metadata` row bound to it (active
|
|
4345
|
+
* AND draft) and — by default — the physical table of each object it
|
|
4346
|
+
* defined. DESTRUCTIVE: removes the app and its data. Use case: "I don't
|
|
4347
|
+
* want this package anymore."
|
|
4348
|
+
*
|
|
4349
|
+
* Set `keepData: true` to remove the metadata but preserve object tables.
|
|
4350
|
+
* The `sys_`-table guard in {@link deleteMetaItem} still applies, so
|
|
4351
|
+
* platform storage is never dropped. Drafts are removed before active rows
|
|
4352
|
+
* so each object's table is torn down once. Per-item failures are collected
|
|
4353
|
+
* without aborting the rest.
|
|
4354
|
+
*/
|
|
4355
|
+
async deletePackage(request) {
|
|
4356
|
+
const where = { package_id: request.packageId };
|
|
4357
|
+
if (request.organizationId) where.organization_id = request.organizationId;
|
|
4358
|
+
const rows = await this.engine.find("sys_metadata", { where });
|
|
4359
|
+
const dropStorage = request.keepData !== true;
|
|
4360
|
+
const ordered = [...rows].sort((a, b) => (a.state === "draft" ? 0 : 1) - (b.state === "draft" ? 0 : 1));
|
|
4361
|
+
const deleted = [];
|
|
4362
|
+
const failed = [];
|
|
4363
|
+
for (const row of ordered) {
|
|
4364
|
+
const state = row.state === "draft" ? "draft" : "active";
|
|
4365
|
+
try {
|
|
4366
|
+
await this.deleteMetaItem({
|
|
4367
|
+
type: row.type,
|
|
4368
|
+
name: row.name,
|
|
4369
|
+
state,
|
|
4370
|
+
...row.organization_id ? { organizationId: row.organization_id } : {},
|
|
4371
|
+
...request.actor ? { actor: request.actor } : {},
|
|
4372
|
+
...dropStorage ? { dropStorage: true } : {}
|
|
4373
|
+
});
|
|
4374
|
+
deleted.push({ type: row.type, name: row.name, state });
|
|
4375
|
+
} catch (e) {
|
|
4376
|
+
failed.push({
|
|
4377
|
+
type: row.type,
|
|
4378
|
+
name: row.name,
|
|
4379
|
+
error: e?.message ?? "delete failed",
|
|
4380
|
+
...e?.code ? { code: e.code } : {}
|
|
4381
|
+
});
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
return {
|
|
4385
|
+
success: failed.length === 0 && deleted.length > 0,
|
|
4386
|
+
deletedCount: deleted.length,
|
|
4387
|
+
failedCount: failed.length,
|
|
4388
|
+
deleted,
|
|
4389
|
+
failed
|
|
4390
|
+
};
|
|
4391
|
+
}
|
|
4201
4392
|
/**
|
|
4202
4393
|
* Restore the body recorded at history `toVersion` as the new
|
|
4203
4394
|
* live row. Writes a history event with `op='revert'`. 404
|
|
@@ -4430,6 +4621,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4430
4621
|
} catch {
|
|
4431
4622
|
}
|
|
4432
4623
|
}
|
|
4624
|
+
if (this.shouldDropStorage(request.type, request.name, request.dropStorage, targetState)) {
|
|
4625
|
+
await this.dropObjectStorage(singularTypeForRepo, request.name);
|
|
4626
|
+
}
|
|
4433
4627
|
await this.recordMetadataAudit({
|
|
4434
4628
|
type: request.type,
|
|
4435
4629
|
name: request.name,
|
|
@@ -4478,6 +4672,12 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4478
4672
|
};
|
|
4479
4673
|
}
|
|
4480
4674
|
await this.engine.delete("sys_metadata", { where: { id: existing.id } });
|
|
4675
|
+
{
|
|
4676
|
+
const targetState = request.state === "draft" ? "draft" : "active";
|
|
4677
|
+
if (this.shouldDropStorage(request.type, request.name, request.dropStorage, targetState)) {
|
|
4678
|
+
await this.dropObjectStorage(PLURAL_TO_SINGULAR3[request.type] ?? request.type, request.name);
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4481
4681
|
if (this.environmentId === void 0) {
|
|
4482
4682
|
try {
|
|
4483
4683
|
const services = this.getServicesRegistry?.();
|
|
@@ -4839,6 +5039,7 @@ _ObjectStackProtocolImplementation.RUNTIME_CREATE_ALLOWED_TYPES = (() => {
|
|
|
4839
5039
|
var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
4840
5040
|
|
|
4841
5041
|
// src/engine.ts
|
|
5042
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
4842
5043
|
import { ExecutionContextSchema } from "@objectstack/spec/kernel";
|
|
4843
5044
|
import { createLogger } from "@objectstack/core";
|
|
4844
5045
|
import { CoreServiceName, StorageNameMapping } from "@objectstack/spec/system";
|
|
@@ -5331,7 +5532,7 @@ function optionValues(options) {
|
|
|
5331
5532
|
);
|
|
5332
5533
|
}
|
|
5333
5534
|
function validateOne(name, def, value) {
|
|
5334
|
-
if (def.required && isMissing(value)) {
|
|
5535
|
+
if (def.required && isMissing(value) && def.type !== "autonumber") {
|
|
5335
5536
|
return { field: name, code: "required", message: `${name} is required` };
|
|
5336
5537
|
}
|
|
5337
5538
|
if (isMissing(value)) return null;
|
|
@@ -5429,8 +5630,31 @@ var ajv = new Ajv({ allErrors: true, strict: false });
|
|
|
5429
5630
|
var jsonSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
5430
5631
|
function needsPriorRecord(objectSchema) {
|
|
5431
5632
|
const rules = objectSchema?.validations;
|
|
5432
|
-
|
|
5433
|
-
return
|
|
5633
|
+
const ruleNeeds = Array.isArray(rules) && rules.some((r) => ruleNeedsPrior(r));
|
|
5634
|
+
return !!(ruleNeeds || fieldsNeedPrior(objectSchema?.fields));
|
|
5635
|
+
}
|
|
5636
|
+
function stripReadonlyWhenFields(objectSchema, data, previous, logger) {
|
|
5637
|
+
const fields = objectSchema?.fields;
|
|
5638
|
+
if (!fields || !data) return data;
|
|
5639
|
+
const merged = { ...previous ?? {}, ...data };
|
|
5640
|
+
let result = data;
|
|
5641
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
5642
|
+
if (!def?.readonlyWhen || !(name in data)) continue;
|
|
5643
|
+
const res = ExpressionEngine2.evaluate(toExpression(def.readonlyWhen), {
|
|
5644
|
+
record: merged,
|
|
5645
|
+
previous: previous ?? void 0
|
|
5646
|
+
});
|
|
5647
|
+
if (!res.ok) {
|
|
5648
|
+
logger?.warn?.(`readonlyWhen for '${name}' failed to evaluate \u2014 change allowed through`);
|
|
5649
|
+
continue;
|
|
5650
|
+
}
|
|
5651
|
+
if (res.value === true) {
|
|
5652
|
+
if (result === data) result = { ...data };
|
|
5653
|
+
delete result[name];
|
|
5654
|
+
logger?.warn?.(`Field '${name}' is read-only (readonlyWhen) \u2014 ignoring incoming change`);
|
|
5655
|
+
}
|
|
5656
|
+
}
|
|
5657
|
+
return result;
|
|
5434
5658
|
}
|
|
5435
5659
|
function ruleNeedsPrior(r) {
|
|
5436
5660
|
if (r == null || typeof r !== "object") return false;
|
|
@@ -5444,17 +5668,44 @@ function ruleNeedsPrior(r) {
|
|
|
5444
5668
|
}
|
|
5445
5669
|
return false;
|
|
5446
5670
|
}
|
|
5671
|
+
function isMissing2(v) {
|
|
5672
|
+
return v === void 0 || v === null || typeof v === "string" && v.trim() === "";
|
|
5673
|
+
}
|
|
5674
|
+
function fieldsNeedPrior(fields) {
|
|
5675
|
+
if (!fields) return false;
|
|
5676
|
+
return Object.values(fields).some(
|
|
5677
|
+
(f) => f && (f.requiredWhen || f.conditionalRequired || f.readonlyWhen)
|
|
5678
|
+
);
|
|
5679
|
+
}
|
|
5447
5680
|
function toExpression(cond) {
|
|
5448
5681
|
return typeof cond === "string" ? { dialect: "cel", source: cond } : cond;
|
|
5449
5682
|
}
|
|
5450
5683
|
function evaluateValidationRules(objectSchema, data, mode, opts = {}) {
|
|
5684
|
+
if (!data) return;
|
|
5451
5685
|
const rules = objectSchema?.validations;
|
|
5452
|
-
|
|
5686
|
+
const hasRules = Array.isArray(rules) && rules.length > 0;
|
|
5687
|
+
const fields = objectSchema?.fields;
|
|
5688
|
+
const hasFieldRules = fieldsNeedPrior(fields);
|
|
5689
|
+
if (!hasRules && !hasFieldRules) return;
|
|
5453
5690
|
const previous = opts.previous ?? void 0;
|
|
5454
5691
|
const merged = { ...previous ?? {}, ...data };
|
|
5455
5692
|
const ctx = { data, merged, previous, mode, logger: opts.logger };
|
|
5456
5693
|
const errors = [];
|
|
5457
|
-
|
|
5694
|
+
if (hasFieldRules && fields) {
|
|
5695
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
5696
|
+
const pred = def?.requiredWhen ?? def?.conditionalRequired;
|
|
5697
|
+
if (!pred) continue;
|
|
5698
|
+
const res = ExpressionEngine2.evaluate(toExpression(pred), { record: merged, previous });
|
|
5699
|
+
if (!res.ok) {
|
|
5700
|
+
opts.logger?.warn?.(`requiredWhen for '${name}' failed to evaluate \u2014 skipped`);
|
|
5701
|
+
continue;
|
|
5702
|
+
}
|
|
5703
|
+
if (res.value === true && isMissing2(merged[name])) {
|
|
5704
|
+
errors.push({ field: name, code: "required", message: `${name} is required` });
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
}
|
|
5708
|
+
const ordered = (hasRules ? rules : []).filter((r) => r != null && typeof r === "object").filter((r) => r.active !== false).filter((r) => {
|
|
5458
5709
|
const events = r.events ?? ["insert", "update"];
|
|
5459
5710
|
return events.includes(mode);
|
|
5460
5711
|
}).sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
|
@@ -5832,6 +6083,15 @@ function resolveMetadataItemName(key, item) {
|
|
|
5832
6083
|
}
|
|
5833
6084
|
var _ObjectQL = class _ObjectQL {
|
|
5834
6085
|
constructor(hostContext = {}) {
|
|
6086
|
+
/**
|
|
6087
|
+
* Ambient transaction store (ADR-0034). While a `transaction()` callback
|
|
6088
|
+
* runs, the active transaction handle lives here so that EVERY data
|
|
6089
|
+
* operation — including internal reads done during a write (reference
|
|
6090
|
+
* checks, hooks, expand) — automatically binds to the same connection
|
|
6091
|
+
* instead of asking the pool for another one and deadlocking on the
|
|
6092
|
+
* single-connection SQLite pool.
|
|
6093
|
+
*/
|
|
6094
|
+
this.txStore = new AsyncLocalStorage();
|
|
5835
6095
|
this.drivers = /* @__PURE__ */ new Map();
|
|
5836
6096
|
this.defaultDriver = null;
|
|
5837
6097
|
// Datasource mapping rules (imported from defineStack)
|
|
@@ -5873,6 +6133,12 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5873
6133
|
// getDriver()'s owner lookup would route CRUD to the wrong database. Each
|
|
5874
6134
|
// engine now owns its registry so kernels are fully isolated.
|
|
5875
6135
|
this._registry = new SchemaRegistry();
|
|
6136
|
+
/** In-memory next-value cache per `object.field` for autonumber generation,
|
|
6137
|
+
* lazily seeded from the current max in the store. */
|
|
6138
|
+
this.autonumberCounters = /* @__PURE__ */ new Map();
|
|
6139
|
+
/** Lazily-built index: child object name → roll-up summary descriptors on
|
|
6140
|
+
* parent objects that aggregate it. Invalidated when packages register. */
|
|
6141
|
+
this.summaryIndex = null;
|
|
5876
6142
|
this.hostContext = hostContext;
|
|
5877
6143
|
this.logger = hostContext.logger || createLogger({ level: "info", format: "pretty" });
|
|
5878
6144
|
if (process?.env?.OBJECTQL_STRICT_HOOKS === "1") {
|
|
@@ -6175,13 +6441,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6175
6441
|
* mask the system path.
|
|
6176
6442
|
*/
|
|
6177
6443
|
buildDriverOptions(execCtx, base) {
|
|
6178
|
-
const
|
|
6444
|
+
const tx = execCtx?.transaction !== void 0 ? execCtx.transaction : this.txStore.getStore()?.transaction;
|
|
6445
|
+
const hasTx = tx !== void 0;
|
|
6179
6446
|
const hasTenant = execCtx?.tenantId !== void 0;
|
|
6180
6447
|
const isSystem = execCtx?.isSystem === true;
|
|
6181
6448
|
if (!hasTx && !hasTenant && !isSystem) return base;
|
|
6182
6449
|
const opts = base && typeof base === "object" ? { ...base } : {};
|
|
6183
6450
|
if (hasTx && opts.transaction === void 0) {
|
|
6184
|
-
opts.transaction =
|
|
6451
|
+
opts.transaction = tx;
|
|
6185
6452
|
}
|
|
6186
6453
|
if (hasTenant && opts.tenantId === void 0) {
|
|
6187
6454
|
opts.tenantId = execCtx.tenantId;
|
|
@@ -6247,6 +6514,66 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6247
6514
|
}
|
|
6248
6515
|
return out;
|
|
6249
6516
|
}
|
|
6517
|
+
/**
|
|
6518
|
+
* Generate values for empty `autonumber` fields on insert — ONLY for drivers
|
|
6519
|
+
* that do not generate them natively (memory, mongodb). For SQL-backed objects
|
|
6520
|
+
* the driver owns a persistent, atomic `_objectstack_sequences` table and
|
|
6521
|
+
* advertises `supports.autonumber === true`; the engine then defers entirely
|
|
6522
|
+
* and never pre-fills (so the persistent sequence is the single source of
|
|
6523
|
+
* truth — see #1603). Required-validation exempts `autonumber` either way, so
|
|
6524
|
+
* a `required` record number is never rejected for "missing" — the runtime
|
|
6525
|
+
* owns the value, not the client.
|
|
6526
|
+
*
|
|
6527
|
+
* In the fallback path the next value is `max(existing) + 1`, seeded once per
|
|
6528
|
+
* `object.field` from the store then incremented in memory (monotonic within
|
|
6529
|
+
* the process, resilient to deletions). `autonumberFormat` is honored, e.g.
|
|
6530
|
+
* `CASE-{0000}` → `CASE-0042`. NOTE: this in-memory seeding is single-instance.
|
|
6531
|
+
*/
|
|
6532
|
+
async applyAutonumbers(object, record, execCtx, driverOwnsAutonumber) {
|
|
6533
|
+
if (driverOwnsAutonumber) return;
|
|
6534
|
+
const fields = this.getSchema(object)?.fields;
|
|
6535
|
+
if (!fields || typeof fields !== "object" || Array.isArray(fields)) return;
|
|
6536
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
6537
|
+
if (def?.type !== "autonumber") continue;
|
|
6538
|
+
const current = record[name];
|
|
6539
|
+
if (current != null && current !== "") continue;
|
|
6540
|
+
const key = `${object}.${name}`;
|
|
6541
|
+
let next = this.autonumberCounters.get(key);
|
|
6542
|
+
if (next == null) next = await this.seedAutonumber(object, name, execCtx);
|
|
6543
|
+
next += 1;
|
|
6544
|
+
this.autonumberCounters.set(key, next);
|
|
6545
|
+
const fmt = def.autonumberFormat ?? def.format;
|
|
6546
|
+
record[name] = this.formatAutonumber(fmt, next);
|
|
6547
|
+
}
|
|
6548
|
+
}
|
|
6549
|
+
/** Seed the autonumber counter from the current max numeric value in store. */
|
|
6550
|
+
async seedAutonumber(object, field, execCtx) {
|
|
6551
|
+
try {
|
|
6552
|
+
const rows = await this.find(object, {
|
|
6553
|
+
select: ["id", field],
|
|
6554
|
+
limit: 5e3,
|
|
6555
|
+
context: execCtx
|
|
6556
|
+
});
|
|
6557
|
+
let max = 0;
|
|
6558
|
+
for (const r of rows || []) {
|
|
6559
|
+
const v = r?.[field];
|
|
6560
|
+
if (v == null) continue;
|
|
6561
|
+
const m = String(v).match(/(\d+)(?!.*\d)/);
|
|
6562
|
+
if (m) max = Math.max(max, parseInt(m[1], 10) || 0);
|
|
6563
|
+
}
|
|
6564
|
+
return max;
|
|
6565
|
+
} catch {
|
|
6566
|
+
return 0;
|
|
6567
|
+
}
|
|
6568
|
+
}
|
|
6569
|
+
/** Apply an autonumber format like `CASE-{0000}`; default to the bare number. */
|
|
6570
|
+
formatAutonumber(format, value) {
|
|
6571
|
+
if (!format) return String(value);
|
|
6572
|
+
const m = format.match(/\{(0+)\}/);
|
|
6573
|
+
if (!m) return format.includes("{0}") ? format.replace("{0}", String(value)) : `${format}${value}`;
|
|
6574
|
+
const padded = String(value).padStart(m[1].length, "0");
|
|
6575
|
+
return format.replace(m[0], padded);
|
|
6576
|
+
}
|
|
6250
6577
|
/**
|
|
6251
6578
|
* Register contribution (Manifest)
|
|
6252
6579
|
*
|
|
@@ -6260,6 +6587,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6260
6587
|
registerApp(manifest) {
|
|
6261
6588
|
const id = manifest.id || manifest.name;
|
|
6262
6589
|
const namespace = manifest.namespace;
|
|
6590
|
+
this.invalidateSummaryIndex();
|
|
6263
6591
|
this.logger.debug("Registering package manifest", { id, namespace });
|
|
6264
6592
|
console.warn(`[ObjectQL:registerApp] id=${id} flows=${Array.isArray(manifest.flows) ? manifest.flows.length : typeof manifest.flows} keys=${Object.keys(manifest).join(",")}`);
|
|
6265
6593
|
if (id) {
|
|
@@ -6340,6 +6668,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6340
6668
|
"pages",
|
|
6341
6669
|
"dashboards",
|
|
6342
6670
|
"reports",
|
|
6671
|
+
"datasets",
|
|
6343
6672
|
"themes",
|
|
6344
6673
|
// Automation Protocol
|
|
6345
6674
|
"flows",
|
|
@@ -6484,6 +6813,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6484
6813
|
"pages",
|
|
6485
6814
|
"dashboards",
|
|
6486
6815
|
"reports",
|
|
6816
|
+
"datasets",
|
|
6487
6817
|
"themes",
|
|
6488
6818
|
"flows",
|
|
6489
6819
|
"workflows",
|
|
@@ -6865,6 +7195,102 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6865
7195
|
}
|
|
6866
7196
|
this.logger.info("ObjectQL engine destroyed");
|
|
6867
7197
|
}
|
|
7198
|
+
/** Invalidate the cached roll-up summary index (call when metadata changes). */
|
|
7199
|
+
invalidateSummaryIndex() {
|
|
7200
|
+
this.summaryIndex = null;
|
|
7201
|
+
}
|
|
7202
|
+
/** Scan all registered objects for `summary` fields and index them by the
|
|
7203
|
+
* child object they aggregate, resolving the child→parent FK field. */
|
|
7204
|
+
buildSummaryIndex() {
|
|
7205
|
+
const index = /* @__PURE__ */ new Map();
|
|
7206
|
+
let objects = [];
|
|
7207
|
+
try {
|
|
7208
|
+
objects = this._registry.getAllObjects?.() ?? [];
|
|
7209
|
+
} catch {
|
|
7210
|
+
objects = [];
|
|
7211
|
+
}
|
|
7212
|
+
for (const parent of objects) {
|
|
7213
|
+
const fields = parent?.fields;
|
|
7214
|
+
if (!fields || typeof fields !== "object" || Array.isArray(fields)) continue;
|
|
7215
|
+
for (const [summaryField, def] of Object.entries(fields)) {
|
|
7216
|
+
const d = def;
|
|
7217
|
+
if (d?.type !== "summary" || !d.summaryOperations) continue;
|
|
7218
|
+
const so = d.summaryOperations;
|
|
7219
|
+
const childObject = so.object;
|
|
7220
|
+
const fn = so.function;
|
|
7221
|
+
if (!childObject || !fn) continue;
|
|
7222
|
+
let fkField = so.relationshipField;
|
|
7223
|
+
if (!fkField) {
|
|
7224
|
+
const child = this._registry.getObject(childObject);
|
|
7225
|
+
const cfields = child?.fields || {};
|
|
7226
|
+
for (const [cfName, cdef] of Object.entries(cfields)) {
|
|
7227
|
+
const cd = cdef;
|
|
7228
|
+
if ((cd?.type === "master_detail" || cd?.type === "lookup") && cd?.reference === parent.name) {
|
|
7229
|
+
fkField = cfName;
|
|
7230
|
+
break;
|
|
7231
|
+
}
|
|
7232
|
+
}
|
|
7233
|
+
}
|
|
7234
|
+
if (!fkField) continue;
|
|
7235
|
+
const list = index.get(childObject) ?? [];
|
|
7236
|
+
list.push({ parentObject: parent.name, summaryField, fkField, fn, sourceField: so.field });
|
|
7237
|
+
index.set(childObject, list);
|
|
7238
|
+
}
|
|
7239
|
+
}
|
|
7240
|
+
return index;
|
|
7241
|
+
}
|
|
7242
|
+
getSummaryDescriptors(childObject) {
|
|
7243
|
+
if (!this.summaryIndex) this.summaryIndex = this.buildSummaryIndex();
|
|
7244
|
+
return this.summaryIndex.get(childObject) ?? [];
|
|
7245
|
+
}
|
|
7246
|
+
/**
|
|
7247
|
+
* Recompute roll-up `summary` fields on parent records after a child write.
|
|
7248
|
+
* For each affected parent (the FK value on the changed/old child record), it
|
|
7249
|
+
* aggregates the child collection and writes the result onto the parent's
|
|
7250
|
+
* summary field. Runs in the caller's execution context so it joins the same
|
|
7251
|
+
* transaction (e.g. the cross-object batch) when one is open.
|
|
7252
|
+
*/
|
|
7253
|
+
async recomputeSummaries(childObject, records, previous, execCtx) {
|
|
7254
|
+
const descriptors = this.getSummaryDescriptors(childObject);
|
|
7255
|
+
if (descriptors.length === 0) return;
|
|
7256
|
+
const recs = Array.isArray(records) ? records : records ? [records] : [];
|
|
7257
|
+
const prevs = Array.isArray(previous) ? previous : previous ? [previous] : [];
|
|
7258
|
+
for (const desc of descriptors) {
|
|
7259
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7260
|
+
for (const r of recs) {
|
|
7261
|
+
const v = r?.[desc.fkField];
|
|
7262
|
+
if (v != null && v !== "") ids.add(String(v));
|
|
7263
|
+
}
|
|
7264
|
+
for (const p of prevs) {
|
|
7265
|
+
const v = p?.[desc.fkField];
|
|
7266
|
+
if (v != null && v !== "") ids.add(String(v));
|
|
7267
|
+
}
|
|
7268
|
+
for (const parentId of ids) {
|
|
7269
|
+
try {
|
|
7270
|
+
const rows = await this.aggregate(childObject, {
|
|
7271
|
+
where: { [desc.fkField]: parentId },
|
|
7272
|
+
aggregations: [{
|
|
7273
|
+
function: desc.fn,
|
|
7274
|
+
...desc.fn === "count" ? {} : { field: desc.sourceField },
|
|
7275
|
+
alias: "value"
|
|
7276
|
+
}],
|
|
7277
|
+
context: execCtx
|
|
7278
|
+
});
|
|
7279
|
+
let value = rows?.[0]?.value;
|
|
7280
|
+
if (value == null) value = desc.fn === "count" || desc.fn === "sum" ? 0 : null;
|
|
7281
|
+
await this.update(desc.parentObject, { id: parentId, [desc.summaryField]: value }, { context: execCtx });
|
|
7282
|
+
} catch (err) {
|
|
7283
|
+
this.logger.warn("Roll-up summary recompute failed", {
|
|
7284
|
+
childObject,
|
|
7285
|
+
parentObject: desc.parentObject,
|
|
7286
|
+
parentId,
|
|
7287
|
+
field: desc.summaryField,
|
|
7288
|
+
error: err?.message
|
|
7289
|
+
});
|
|
7290
|
+
}
|
|
7291
|
+
}
|
|
7292
|
+
}
|
|
7293
|
+
}
|
|
6868
7294
|
/**
|
|
6869
7295
|
* Post-process expand: resolve lookup/master_detail fields by batch-loading related records.
|
|
6870
7296
|
*
|
|
@@ -7080,10 +7506,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7080
7506
|
let result;
|
|
7081
7507
|
const nowSnap = /* @__PURE__ */ new Date();
|
|
7082
7508
|
const schemaForValidation = this._registry.getObject(object);
|
|
7509
|
+
const driverOwnsAutonumber = driver?.supports?.autonumber === true;
|
|
7083
7510
|
if (Array.isArray(hookContext.input.data)) {
|
|
7084
7511
|
const rows = hookContext.input.data.map(
|
|
7085
7512
|
(row) => this.applyFieldDefaults(object, row, opCtx.context, nowSnap)
|
|
7086
7513
|
);
|
|
7514
|
+
for (const r of rows) {
|
|
7515
|
+
await this.applyAutonumbers(object, r, opCtx.context, driverOwnsAutonumber);
|
|
7516
|
+
}
|
|
7087
7517
|
for (const r of rows) {
|
|
7088
7518
|
await this.encryptSecretFields(object, r, opCtx.context, hookContext.input.options);
|
|
7089
7519
|
}
|
|
@@ -7103,6 +7533,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7103
7533
|
opCtx.context,
|
|
7104
7534
|
nowSnap
|
|
7105
7535
|
);
|
|
7536
|
+
await this.applyAutonumbers(object, row, opCtx.context, driverOwnsAutonumber);
|
|
7106
7537
|
await this.encryptSecretFields(object, row, opCtx.context, hookContext.input.options);
|
|
7107
7538
|
validateRecord(schemaForValidation, row, "insert");
|
|
7108
7539
|
evaluateValidationRules(schemaForValidation, row, "insert", { logger: this.logger });
|
|
@@ -7111,6 +7542,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7111
7542
|
hookContext.event = "afterInsert";
|
|
7112
7543
|
hookContext.result = result;
|
|
7113
7544
|
await this.triggerHooks("afterInsert", hookContext);
|
|
7545
|
+
await this.recomputeSummaries(object, result, null, opCtx.context);
|
|
7114
7546
|
if (this.realtimeService) {
|
|
7115
7547
|
try {
|
|
7116
7548
|
if (Array.isArray(result)) {
|
|
@@ -7195,6 +7627,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7195
7627
|
const priorAst = { object, where: { id: hookContext.input.id }, limit: 1 };
|
|
7196
7628
|
priorRecord = await driver.findOne(object, priorAst, hookContext.input.options);
|
|
7197
7629
|
}
|
|
7630
|
+
hookContext.input.data = stripReadonlyWhenFields(updateSchema, hookContext.input.data, priorRecord, this.logger);
|
|
7198
7631
|
evaluateValidationRules(updateSchema, hookContext.input.data, "update", { previous: priorRecord, logger: this.logger });
|
|
7199
7632
|
result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
|
|
7200
7633
|
} else if (options?.multi && driver.updateMany) {
|
|
@@ -7212,6 +7645,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7212
7645
|
hookContext.result = result;
|
|
7213
7646
|
if (priorRecord) hookContext.previous = priorRecord;
|
|
7214
7647
|
await this.triggerHooks("afterUpdate", hookContext);
|
|
7648
|
+
await this.recomputeSummaries(object, result, priorRecord, opCtx.context);
|
|
7215
7649
|
if (this.realtimeService) {
|
|
7216
7650
|
try {
|
|
7217
7651
|
const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
|
|
@@ -7240,6 +7674,66 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7240
7674
|
});
|
|
7241
7675
|
return opCtx.result;
|
|
7242
7676
|
}
|
|
7677
|
+
/**
|
|
7678
|
+
* Apply referential delete behavior for relations pointing AT this record,
|
|
7679
|
+
* before it is removed. For every registered object with a `master_detail`
|
|
7680
|
+
* or `lookup` field referencing `object`, honor the field's `deleteBehavior`:
|
|
7681
|
+
* - `cascade` → delete the dependent rows (recursively, so grandchildren
|
|
7682
|
+
* are handled by each child's own delete),
|
|
7683
|
+
* - `set_null` → clear the foreign key,
|
|
7684
|
+
* - `restrict` → refuse the delete when dependents exist.
|
|
7685
|
+
* `master_detail` defaults to `cascade` (the parent owns the child
|
|
7686
|
+
* lifecycle); `lookup` defaults to `set_null`. Only runs for single-id
|
|
7687
|
+
* deletes — multi/predicate deletes skip cascade (logged).
|
|
7688
|
+
*/
|
|
7689
|
+
async cascadeDeleteRelations(object, id, context, depth = 0) {
|
|
7690
|
+
if (id == null || depth >= _ObjectQL.MAX_CASCADE_DEPTH) return;
|
|
7691
|
+
let objects;
|
|
7692
|
+
try {
|
|
7693
|
+
objects = this._registry.getAllObjects();
|
|
7694
|
+
} catch {
|
|
7695
|
+
return;
|
|
7696
|
+
}
|
|
7697
|
+
for (const child of objects) {
|
|
7698
|
+
const childName = child?.name;
|
|
7699
|
+
const fields = child?.fields;
|
|
7700
|
+
if (!childName || !fields) continue;
|
|
7701
|
+
for (const [fieldName, fdef] of Object.entries(fields)) {
|
|
7702
|
+
if (!fdef || fdef.type !== "master_detail" && fdef.type !== "lookup") continue;
|
|
7703
|
+
const ref = fdef.reference;
|
|
7704
|
+
if (!ref) continue;
|
|
7705
|
+
let resolvedRef;
|
|
7706
|
+
try {
|
|
7707
|
+
resolvedRef = this.resolveObjectName(ref);
|
|
7708
|
+
} catch {
|
|
7709
|
+
resolvedRef = void 0;
|
|
7710
|
+
}
|
|
7711
|
+
if (ref !== object && resolvedRef !== object) continue;
|
|
7712
|
+
const behavior = fdef.type === "master_detail" ? fdef.deleteBehavior === "restrict" ? "restrict" : "cascade" : fdef.deleteBehavior || "set_null";
|
|
7713
|
+
let dependents;
|
|
7714
|
+
try {
|
|
7715
|
+
dependents = await this.find(childName, { where: { [fieldName]: id }, context });
|
|
7716
|
+
} catch {
|
|
7717
|
+
continue;
|
|
7718
|
+
}
|
|
7719
|
+
if (!dependents || dependents.length === 0) continue;
|
|
7720
|
+
if (behavior === "restrict") {
|
|
7721
|
+
throw new Error(
|
|
7722
|
+
`Cannot delete ${object} (${id}): ${dependents.length} dependent ${childName} record(s) via ${fieldName}`
|
|
7723
|
+
);
|
|
7724
|
+
}
|
|
7725
|
+
for (const dep of dependents) {
|
|
7726
|
+
const depId = dep?.id;
|
|
7727
|
+
if (depId == null) continue;
|
|
7728
|
+
if (behavior === "cascade") {
|
|
7729
|
+
await this.delete(childName, { where: { id: depId }, context });
|
|
7730
|
+
} else {
|
|
7731
|
+
await this.update(childName, { id: depId, [fieldName]: null }, { context });
|
|
7732
|
+
}
|
|
7733
|
+
}
|
|
7734
|
+
}
|
|
7735
|
+
}
|
|
7736
|
+
}
|
|
7243
7737
|
async delete(object, options) {
|
|
7244
7738
|
object = this.resolveObjectName(object);
|
|
7245
7739
|
this.logger.debug("Delete operation starting", { object });
|
|
@@ -7269,7 +7763,15 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7269
7763
|
hookContext.input.options = this.buildDriverOptions(opCtx.context, hookContext.input.options);
|
|
7270
7764
|
try {
|
|
7271
7765
|
let result;
|
|
7766
|
+
let summaryPrev = null;
|
|
7767
|
+
if (hookContext.input.id && this.getSummaryDescriptors(object).length > 0) {
|
|
7768
|
+
try {
|
|
7769
|
+
summaryPrev = await this.findOne(object, { where: { id: hookContext.input.id }, context: opCtx.context });
|
|
7770
|
+
} catch {
|
|
7771
|
+
}
|
|
7772
|
+
}
|
|
7272
7773
|
if (hookContext.input.id) {
|
|
7774
|
+
await this.cascadeDeleteRelations(object, hookContext.input.id, opCtx.context);
|
|
7273
7775
|
result = await driver.delete(object, hookContext.input.id, hookContext.input.options);
|
|
7274
7776
|
} else if (options?.multi && driver.deleteMany) {
|
|
7275
7777
|
const ast = { object, where: options.where };
|
|
@@ -7280,6 +7782,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7280
7782
|
hookContext.event = "afterDelete";
|
|
7281
7783
|
hookContext.result = result;
|
|
7282
7784
|
await this.triggerHooks("afterDelete", hookContext);
|
|
7785
|
+
if (summaryPrev) await this.recomputeSummaries(object, null, summaryPrev, opCtx.context);
|
|
7283
7786
|
if (this.realtimeService) {
|
|
7284
7787
|
try {
|
|
7285
7788
|
const resultId = typeof result === "object" && result && "id" in result ? result.id : void 0;
|
|
@@ -7429,7 +7932,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7429
7932
|
const trx = await drv.beginTransaction();
|
|
7430
7933
|
const trxCtx = { ...baseContext ?? {}, transaction: trx };
|
|
7431
7934
|
try {
|
|
7432
|
-
const result = await callback(trxCtx);
|
|
7935
|
+
const result = await this.txStore.run({ transaction: trx }, () => callback(trxCtx));
|
|
7433
7936
|
if (drv.commit) await drv.commit(trx);
|
|
7434
7937
|
else if (drv.commitTransaction) await drv.commitTransaction(trx);
|
|
7435
7938
|
return result;
|
|
@@ -7579,6 +8082,22 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7579
8082
|
const tableName = StorageNameMapping.resolveTableName(obj);
|
|
7580
8083
|
await driver.syncSchema(tableName, obj);
|
|
7581
8084
|
}
|
|
8085
|
+
/**
|
|
8086
|
+
* Drop the physical storage (table/collection) backing an object — the
|
|
8087
|
+
* inverse of {@link syncObjectSchema}. DESTRUCTIVE: deletes all rows in the
|
|
8088
|
+
* table. Used by the protocol delete path when the caller explicitly opts
|
|
8089
|
+
* into storage teardown (e.g. discarding an object that was published only
|
|
8090
|
+
* to preview it). No-op when the object's driver does not expose `dropTable`.
|
|
8091
|
+
* Resolves the physical table name from the registered definition, falling
|
|
8092
|
+
* back to the bare name if the def was already removed.
|
|
8093
|
+
*/
|
|
8094
|
+
async dropObjectSchema(objectName) {
|
|
8095
|
+
const obj = this._registry.getObject(objectName);
|
|
8096
|
+
const driver = this.getDriverForObject(objectName);
|
|
8097
|
+
if (!driver || typeof driver.dropTable !== "function") return;
|
|
8098
|
+
const tableName = StorageNameMapping.resolveTableName(obj ?? { name: objectName });
|
|
8099
|
+
await driver.dropTable(tableName);
|
|
8100
|
+
}
|
|
7582
8101
|
/**
|
|
7583
8102
|
* Get a registered driver by datasource name.
|
|
7584
8103
|
* Alias matching @objectql/core datasource() API.
|
|
@@ -7677,6 +8196,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7677
8196
|
// ============================================
|
|
7678
8197
|
/** Maximum depth for recursive expand to prevent infinite loops */
|
|
7679
8198
|
_ObjectQL.MAX_EXPAND_DEPTH = 3;
|
|
8199
|
+
_ObjectQL.MAX_CASCADE_DEPTH = 10;
|
|
7680
8200
|
var ObjectQL = _ObjectQL;
|
|
7681
8201
|
var ObjectRepository = class {
|
|
7682
8202
|
constructor(objectName, context, engine) {
|
|
@@ -7794,8 +8314,10 @@ var ScopedContext = class _ScopedContext {
|
|
|
7794
8314
|
{ ...this.executionContext, transaction: trx },
|
|
7795
8315
|
this.engine
|
|
7796
8316
|
);
|
|
8317
|
+
const txStore = this.engine?.txStore;
|
|
8318
|
+
const runIn = (fn) => txStore ? txStore.run({ transaction: trx }, fn) : fn();
|
|
7797
8319
|
try {
|
|
7798
|
-
const result = await callback(trxCtx);
|
|
8320
|
+
const result = await runIn(() => callback(trxCtx));
|
|
7799
8321
|
if (driver.commit) await driver.commit(trx);
|
|
7800
8322
|
else if (driver.commitTransaction) await driver.commitTransaction(trx);
|
|
7801
8323
|
return result;
|