@objectstack/objectql 9.7.0 → 9.9.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 +66 -51
- package/dist/index.d.ts +66 -51
- package/dist/index.js +136 -171
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +132 -167
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -2832,6 +2832,13 @@ function normaliseVersionToken(v) {
|
|
|
2832
2832
|
}
|
|
2833
2833
|
return s;
|
|
2834
2834
|
}
|
|
2835
|
+
var CLONE_STRIP_FIELDS = [
|
|
2836
|
+
"id",
|
|
2837
|
+
"created_at",
|
|
2838
|
+
"created_by",
|
|
2839
|
+
"updated_at",
|
|
2840
|
+
"updated_by"
|
|
2841
|
+
];
|
|
2835
2842
|
var SERVICE_CONFIG = {
|
|
2836
2843
|
auth: { route: "/api/v1/auth", plugin: "plugin-auth" },
|
|
2837
2844
|
automation: { route: "/api/v1/automation", plugin: "plugin-automation" },
|
|
@@ -4087,6 +4094,68 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4087
4094
|
record: result
|
|
4088
4095
|
};
|
|
4089
4096
|
}
|
|
4097
|
+
/**
|
|
4098
|
+
* Clone a record — read the source, drop engine-owned columns, and
|
|
4099
|
+
* insert a fresh copy. Gated by the object's `enable.clone` capability
|
|
4100
|
+
* (default `true`; only an explicit `enable.clone === false` disables it).
|
|
4101
|
+
*
|
|
4102
|
+
* Shallow by design: it duplicates the record's own scalar/business field
|
|
4103
|
+
* values, not its related child records. The insert path re-stamps audit
|
|
4104
|
+
* columns, regenerates `autonumber` fields, and recomputes derived
|
|
4105
|
+
* (`formula`/`summary`) fields, so the copy is a valid new row rather than
|
|
4106
|
+
* a byte-identical twin. Caller-supplied `overrides` are applied last and
|
|
4107
|
+
* win over the copied values — the natural place to set a new `name`,
|
|
4108
|
+
* clear a unique field, or reset status before insert.
|
|
4109
|
+
*/
|
|
4110
|
+
async cloneData(request) {
|
|
4111
|
+
const schema = this.engine.registry.getObject(request.object);
|
|
4112
|
+
if (!schema) {
|
|
4113
|
+
const err = new Error(`Object '${request.object}' not found`);
|
|
4114
|
+
err.code = "OBJECT_NOT_FOUND";
|
|
4115
|
+
err.status = 404;
|
|
4116
|
+
err.object = request.object;
|
|
4117
|
+
throw err;
|
|
4118
|
+
}
|
|
4119
|
+
if (schema.enable?.clone === false) {
|
|
4120
|
+
const err = new Error(`Cloning is disabled for object '${request.object}'`);
|
|
4121
|
+
err.code = "CLONE_DISABLED";
|
|
4122
|
+
err.status = 403;
|
|
4123
|
+
err.object = request.object;
|
|
4124
|
+
throw err;
|
|
4125
|
+
}
|
|
4126
|
+
const ctx = request.context;
|
|
4127
|
+
const ctxOpt = ctx !== void 0 ? { context: ctx } : void 0;
|
|
4128
|
+
const source = await this.engine.findOne(
|
|
4129
|
+
request.object,
|
|
4130
|
+
{ where: { id: request.id }, ...ctxOpt }
|
|
4131
|
+
);
|
|
4132
|
+
if (!source) {
|
|
4133
|
+
const err = new Error(`Record ${request.id} not found in ${request.object}`);
|
|
4134
|
+
err.code = "RECORD_NOT_FOUND";
|
|
4135
|
+
err.status = 404;
|
|
4136
|
+
err.object = request.object;
|
|
4137
|
+
throw err;
|
|
4138
|
+
}
|
|
4139
|
+
const data = { ...source };
|
|
4140
|
+
for (const f of CLONE_STRIP_FIELDS) delete data[f];
|
|
4141
|
+
const fields = schema.fields || {};
|
|
4142
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
4143
|
+
if (!def) continue;
|
|
4144
|
+
if (def.system === true || def.type === "autonumber" || def.type === "formula" || def.type === "summary") {
|
|
4145
|
+
delete data[name];
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
if (request.overrides && typeof request.overrides === "object") {
|
|
4149
|
+
Object.assign(data, request.overrides);
|
|
4150
|
+
}
|
|
4151
|
+
const result = await this.engine.insert(request.object, data, ctxOpt);
|
|
4152
|
+
return {
|
|
4153
|
+
object: request.object,
|
|
4154
|
+
id: result.id,
|
|
4155
|
+
sourceId: request.id,
|
|
4156
|
+
record: result
|
|
4157
|
+
};
|
|
4158
|
+
}
|
|
4090
4159
|
async updateData(request) {
|
|
4091
4160
|
await this.assertVersionMatch(request.object, request.id, request.expectedVersion, request.context);
|
|
4092
4161
|
const opts = { where: { id: request.id } };
|
|
@@ -4276,145 +4345,6 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4276
4345
|
};
|
|
4277
4346
|
}
|
|
4278
4347
|
// ==========================================
|
|
4279
|
-
// Lead Convert (M10.6)
|
|
4280
|
-
// ==========================================
|
|
4281
|
-
/**
|
|
4282
|
-
* Convert a qualified Lead into an Account + Contact (+ optional
|
|
4283
|
-
* Opportunity) and mark the Lead as converted. Mirrors the Salesforce
|
|
4284
|
-
* lead-conversion model:
|
|
4285
|
-
*
|
|
4286
|
-
* - If `accountId` is provided, the lead's company info is NOT used
|
|
4287
|
-
* to create a new account; the new contact and opportunity link to
|
|
4288
|
-
* the existing account instead.
|
|
4289
|
-
* - If `contactId` is provided, no new contact is created either —
|
|
4290
|
-
* useful when the lead is a new contact at an existing account.
|
|
4291
|
-
* - `createOpportunity` defaults to true; pass `false` to convert
|
|
4292
|
-
* without producing an opportunity (some teams convert "logos
|
|
4293
|
-
* only" first).
|
|
4294
|
-
* - Lead is updated atomically: `is_converted=true`,
|
|
4295
|
-
* `converted_account`/`converted_contact`/`converted_opportunity`
|
|
4296
|
-
* pointers, `converted_date`, and `status='converted'`.
|
|
4297
|
-
*
|
|
4298
|
-
* Atomicity is enforced via the default driver's transaction support
|
|
4299
|
-
* when available; otherwise a best-effort compensation (delete
|
|
4300
|
-
* already-created child records on failure) is attempted. Permission
|
|
4301
|
-
* checks on each child object are inherited from the caller's
|
|
4302
|
-
* execution context so SecurityPlugin still gates account/contact/
|
|
4303
|
-
* opportunity creates.
|
|
4304
|
-
*/
|
|
4305
|
-
async convertLead(request) {
|
|
4306
|
-
const leadId = String(request.leadId || "").trim();
|
|
4307
|
-
if (!leadId) {
|
|
4308
|
-
const err = new Error("leadId is required");
|
|
4309
|
-
err.status = 400;
|
|
4310
|
-
err.code = "INVALID_REQUEST";
|
|
4311
|
-
throw err;
|
|
4312
|
-
}
|
|
4313
|
-
const ctx = request.context;
|
|
4314
|
-
const ctxOpt = ctx !== void 0 ? { context: ctx } : void 0;
|
|
4315
|
-
const lead = await this.engine.findOne("lead", { where: { id: leadId }, ...ctxOpt });
|
|
4316
|
-
if (!lead) {
|
|
4317
|
-
const err = new Error(`Lead '${leadId}' not found`);
|
|
4318
|
-
err.status = 404;
|
|
4319
|
-
err.code = "LEAD_NOT_FOUND";
|
|
4320
|
-
throw err;
|
|
4321
|
-
}
|
|
4322
|
-
if (lead.is_converted) {
|
|
4323
|
-
const err = new Error(`Lead '${leadId}' is already converted`);
|
|
4324
|
-
err.status = 409;
|
|
4325
|
-
err.code = "LEAD_ALREADY_CONVERTED";
|
|
4326
|
-
throw err;
|
|
4327
|
-
}
|
|
4328
|
-
const runConversion = async (trxCtx) => {
|
|
4329
|
-
const opCtx = trxCtx ?? ctx;
|
|
4330
|
-
const trxCtxOpt = opCtx !== void 0 ? { context: opCtx } : void 0;
|
|
4331
|
-
let account;
|
|
4332
|
-
if (request.accountId) {
|
|
4333
|
-
account = await this.engine.findOne("account", { where: { id: request.accountId }, ...trxCtxOpt });
|
|
4334
|
-
if (!account) {
|
|
4335
|
-
const err = new Error(`Account '${request.accountId}' not found`);
|
|
4336
|
-
err.status = 404;
|
|
4337
|
-
err.code = "ACCOUNT_NOT_FOUND";
|
|
4338
|
-
throw err;
|
|
4339
|
-
}
|
|
4340
|
-
} else {
|
|
4341
|
-
const accountPayload = {
|
|
4342
|
-
name: lead.company || `${lead.first_name ?? ""} ${lead.last_name ?? ""}`.trim() || "Untitled Account"
|
|
4343
|
-
};
|
|
4344
|
-
if (lead.industry) accountPayload.industry = lead.industry;
|
|
4345
|
-
if (lead.annual_revenue) accountPayload.annual_revenue = lead.annual_revenue;
|
|
4346
|
-
if (lead.number_of_employees) accountPayload.employees = lead.number_of_employees;
|
|
4347
|
-
if (lead.website) accountPayload.website = lead.website;
|
|
4348
|
-
if (lead.phone) accountPayload.phone = lead.phone;
|
|
4349
|
-
if (lead.address) accountPayload.billing_address = lead.address;
|
|
4350
|
-
if (lead.owner) accountPayload.owner = lead.owner;
|
|
4351
|
-
account = await this.engine.insert("account", accountPayload, trxCtxOpt);
|
|
4352
|
-
}
|
|
4353
|
-
let contact;
|
|
4354
|
-
if (request.contactId) {
|
|
4355
|
-
contact = await this.engine.findOne("contact", { where: { id: request.contactId }, ...trxCtxOpt });
|
|
4356
|
-
if (!contact) {
|
|
4357
|
-
const err = new Error(`Contact '${request.contactId}' not found`);
|
|
4358
|
-
err.status = 404;
|
|
4359
|
-
err.code = "CONTACT_NOT_FOUND";
|
|
4360
|
-
throw err;
|
|
4361
|
-
}
|
|
4362
|
-
} else {
|
|
4363
|
-
const contactPayload = {
|
|
4364
|
-
first_name: lead.first_name ?? "",
|
|
4365
|
-
last_name: lead.last_name ?? lead.company ?? "Unknown"
|
|
4366
|
-
};
|
|
4367
|
-
if (lead.salutation) contactPayload.salutation = lead.salutation;
|
|
4368
|
-
if (lead.email) contactPayload.email = lead.email;
|
|
4369
|
-
if (lead.phone) contactPayload.phone = lead.phone;
|
|
4370
|
-
if (lead.mobile) contactPayload.mobile = lead.mobile;
|
|
4371
|
-
if (lead.title) contactPayload.title = lead.title;
|
|
4372
|
-
if (lead.address) contactPayload.mailing_address = lead.address;
|
|
4373
|
-
if (lead.owner) contactPayload.owner = lead.owner;
|
|
4374
|
-
if (account?.id) contactPayload.account = account.id;
|
|
4375
|
-
contact = await this.engine.insert("contact", contactPayload, trxCtxOpt);
|
|
4376
|
-
}
|
|
4377
|
-
let opportunity = null;
|
|
4378
|
-
const shouldCreateOpp = request.createOpportunity !== false;
|
|
4379
|
-
if (shouldCreateOpp) {
|
|
4380
|
-
const oppOverrides = request.opportunity ?? {};
|
|
4381
|
-
const defaultName = oppOverrides.name || `${account?.name ?? lead.company ?? "Lead"} - New Opportunity`;
|
|
4382
|
-
const defaultClose = oppOverrides.close_date || new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
|
|
4383
|
-
const oppPayload = {
|
|
4384
|
-
name: defaultName,
|
|
4385
|
-
stage: oppOverrides.stage ?? "qualification",
|
|
4386
|
-
close_date: defaultClose
|
|
4387
|
-
};
|
|
4388
|
-
if (oppOverrides.amount !== void 0) oppPayload.amount = oppOverrides.amount;
|
|
4389
|
-
else if (lead.annual_revenue) oppPayload.amount = lead.annual_revenue;
|
|
4390
|
-
if (account?.id) oppPayload.account = account.id;
|
|
4391
|
-
if (contact?.id) oppPayload.primary_contact = contact.id;
|
|
4392
|
-
if (lead.owner) oppPayload.owner = lead.owner;
|
|
4393
|
-
if (lead.lead_source) oppPayload.lead_source = lead.lead_source;
|
|
4394
|
-
opportunity = await this.engine.insert("opportunity", oppPayload, trxCtxOpt);
|
|
4395
|
-
}
|
|
4396
|
-
const leadUpdate = {
|
|
4397
|
-
is_converted: true,
|
|
4398
|
-
status: request.convertedStatus ?? "converted",
|
|
4399
|
-
converted_account: account?.id ?? null,
|
|
4400
|
-
converted_contact: contact?.id ?? null,
|
|
4401
|
-
converted_opportunity: opportunity?.id ?? null,
|
|
4402
|
-
converted_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
4403
|
-
};
|
|
4404
|
-
const updatedLead = await this.engine.update("lead", leadUpdate, {
|
|
4405
|
-
where: { id: leadId },
|
|
4406
|
-
...trxCtxOpt
|
|
4407
|
-
});
|
|
4408
|
-
return {
|
|
4409
|
-
lead: updatedLead ?? { ...lead, ...leadUpdate },
|
|
4410
|
-
account,
|
|
4411
|
-
contact,
|
|
4412
|
-
opportunity
|
|
4413
|
-
};
|
|
4414
|
-
};
|
|
4415
|
-
return this.engine.transaction(runConversion, ctx);
|
|
4416
|
-
}
|
|
4417
|
-
// ==========================================
|
|
4418
4348
|
// Metadata Caching
|
|
4419
4349
|
// ==========================================
|
|
4420
4350
|
async getMetaItemCached(request) {
|
|
@@ -6247,7 +6177,7 @@ var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
|
6247
6177
|
// src/engine.ts
|
|
6248
6178
|
var import_node_async_hooks = require("async_hooks");
|
|
6249
6179
|
var import_kernel6 = require("@objectstack/spec/kernel");
|
|
6250
|
-
var
|
|
6180
|
+
var import_core2 = require("@objectstack/core");
|
|
6251
6181
|
var import_system2 = require("@objectstack/spec/system");
|
|
6252
6182
|
|
|
6253
6183
|
// src/secret-fields.ts
|
|
@@ -6778,11 +6708,20 @@ function validateOne(name, def, value) {
|
|
|
6778
6708
|
if (value === 0 || value === 1 || value === "0" || value === "1" || value === "true" || value === "false") return null;
|
|
6779
6709
|
return { field: name, code: "invalid_boolean", message: `${name} must be true or false` };
|
|
6780
6710
|
}
|
|
6781
|
-
if (t === "date" || t === "datetime"
|
|
6711
|
+
if (t === "date" || t === "datetime") {
|
|
6782
6712
|
if (value instanceof Date) return null;
|
|
6783
6713
|
if (typeof value === "string" && !Number.isNaN(Date.parse(value))) return null;
|
|
6784
6714
|
return { field: name, code: "invalid_date", message: `${name} must be a valid ${t} (ISO-8601)` };
|
|
6785
6715
|
}
|
|
6716
|
+
if (t === "time") {
|
|
6717
|
+
if (value instanceof Date) return null;
|
|
6718
|
+
if (typeof value === "string") {
|
|
6719
|
+
const timeOfDay = /^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d+)?)?(Z|[+-]([01]\d|2[0-3]):?[0-5]\d)?$/;
|
|
6720
|
+
const hasDate = /\d{4}-\d{2}-\d{2}/.test(value);
|
|
6721
|
+
if (timeOfDay.test(value.trim()) || hasDate && !Number.isNaN(Date.parse(value))) return null;
|
|
6722
|
+
}
|
|
6723
|
+
return { field: name, code: "invalid_time", message: `${name} must be a valid time (HH:MM or HH:MM:SS)` };
|
|
6724
|
+
}
|
|
6786
6725
|
if (t === "select" || t === "radio") {
|
|
6787
6726
|
const allowed = optionValues(def.options);
|
|
6788
6727
|
if (allowed.length > 0 && !allowed.includes(String(value))) {
|
|
@@ -7098,7 +7037,8 @@ function legalNextStates(objectSchema, field, currentState) {
|
|
|
7098
7037
|
}
|
|
7099
7038
|
|
|
7100
7039
|
// src/in-memory-aggregation.ts
|
|
7101
|
-
|
|
7040
|
+
var import_core = require("@objectstack/core");
|
|
7041
|
+
function applyInMemoryAggregation(rows, ast, timezone) {
|
|
7102
7042
|
const groupBy = ast.groupBy ?? [];
|
|
7103
7043
|
const aggregations = ast.aggregations ?? [];
|
|
7104
7044
|
if (groupBy.length === 0 && aggregations.length === 0) return rows;
|
|
@@ -7111,7 +7051,7 @@ function applyInMemoryAggregation(rows, ast) {
|
|
|
7111
7051
|
const parts = [];
|
|
7112
7052
|
for (const g of groupBy) {
|
|
7113
7053
|
const fieldName = typeof g === "string" ? g : g.alias ?? g.field;
|
|
7114
|
-
const value = projectGroupValue(row, g);
|
|
7054
|
+
const value = projectGroupValue(row, g, timezone);
|
|
7115
7055
|
key[fieldName] = value;
|
|
7116
7056
|
parts.push(`${fieldName}=${value}`);
|
|
7117
7057
|
}
|
|
@@ -7130,11 +7070,11 @@ function applyInMemoryAggregation(rows, ast) {
|
|
|
7130
7070
|
}
|
|
7131
7071
|
return out;
|
|
7132
7072
|
}
|
|
7133
|
-
function projectGroupValue(row, g) {
|
|
7073
|
+
function projectGroupValue(row, g, timezone) {
|
|
7134
7074
|
const field = typeof g === "string" ? g : g.field;
|
|
7135
7075
|
const v = row?.[field];
|
|
7136
7076
|
if (typeof g !== "string" && g.dateGranularity) {
|
|
7137
|
-
return bucketDateValue(v, g.dateGranularity);
|
|
7077
|
+
return bucketDateValue(v, g.dateGranularity, timezone);
|
|
7138
7078
|
}
|
|
7139
7079
|
return v == null ? "(null)" : String(v);
|
|
7140
7080
|
}
|
|
@@ -7212,12 +7152,11 @@ function toNumber(v) {
|
|
|
7212
7152
|
const n = Number(v);
|
|
7213
7153
|
return Number.isFinite(n) ? n : 0;
|
|
7214
7154
|
}
|
|
7215
|
-
function bucketDateValue(value, granularity) {
|
|
7155
|
+
function bucketDateValue(value, granularity, timezone) {
|
|
7216
7156
|
if (value == null) return "(null)";
|
|
7217
7157
|
const d = value instanceof Date ? value : new Date(String(value));
|
|
7218
7158
|
if (Number.isNaN(d.getTime())) return "(null)";
|
|
7219
|
-
const y =
|
|
7220
|
-
const m = d.getUTCMonth() + 1;
|
|
7159
|
+
const { year: y, month: m, day } = (0, import_core.calendarPartsInTzOrUtc)(d, timezone);
|
|
7221
7160
|
switch (granularity) {
|
|
7222
7161
|
case "year":
|
|
7223
7162
|
return String(y);
|
|
@@ -7226,9 +7165,9 @@ function bucketDateValue(value, granularity) {
|
|
|
7226
7165
|
case "month":
|
|
7227
7166
|
return `${y}-${String(m).padStart(2, "0")}`;
|
|
7228
7167
|
case "day":
|
|
7229
|
-
return `${y}-${String(m).padStart(2, "0")}-${String(
|
|
7168
|
+
return `${y}-${String(m).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
7230
7169
|
case "week": {
|
|
7231
|
-
const target = new Date(Date.UTC(y,
|
|
7170
|
+
const target = new Date(Date.UTC(y, m - 1, day));
|
|
7232
7171
|
const dayNum = (target.getUTCDay() + 6) % 7;
|
|
7233
7172
|
target.setUTCDate(target.getUTCDate() - dayNum + 3);
|
|
7234
7173
|
const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4));
|
|
@@ -7271,16 +7210,25 @@ function planFormulaProjection(schema, requestedFields) {
|
|
|
7271
7210
|
}
|
|
7272
7211
|
return { plan };
|
|
7273
7212
|
}
|
|
7274
|
-
function applyFormulaPlan(plan, records) {
|
|
7213
|
+
function applyFormulaPlan(plan, records, execCtx, nowSnapshot) {
|
|
7275
7214
|
if (!plan.length) return;
|
|
7215
|
+
const now = nowSnapshot ?? /* @__PURE__ */ new Date();
|
|
7216
|
+
const timezone = execCtx?.timezone;
|
|
7217
|
+
const user = execCtx?.userId ? { id: String(execCtx.userId), role: execCtx?.roles?.[0] } : void 0;
|
|
7218
|
+
const org = execCtx?.tenantId ? { id: String(execCtx.tenantId) } : void 0;
|
|
7276
7219
|
for (const rec of records) {
|
|
7277
7220
|
if (rec == null) continue;
|
|
7278
7221
|
for (const fp of plan) {
|
|
7279
|
-
const r = import_formula4.ExpressionEngine.evaluate(fp.expression, { record: rec });
|
|
7222
|
+
const r = import_formula4.ExpressionEngine.evaluate(fp.expression, { now, timezone, user, org, record: rec });
|
|
7280
7223
|
rec[fp.name] = r.ok ? r.value : null;
|
|
7281
7224
|
}
|
|
7282
7225
|
}
|
|
7283
7226
|
}
|
|
7227
|
+
function mergeReadContext(fromQuery, fromOptions) {
|
|
7228
|
+
if (fromOptions == null) return fromQuery;
|
|
7229
|
+
if (fromQuery == null) return fromOptions;
|
|
7230
|
+
return { ...fromQuery, ...fromOptions };
|
|
7231
|
+
}
|
|
7284
7232
|
function resolveMetadataItemName(key, item) {
|
|
7285
7233
|
if (!item) return void 0;
|
|
7286
7234
|
if (item.name) return item.name;
|
|
@@ -7349,7 +7297,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7349
7297
|
* parent objects that aggregate it. Invalidated when packages register. */
|
|
7350
7298
|
this.summaryIndex = null;
|
|
7351
7299
|
this.hostContext = hostContext;
|
|
7352
|
-
this.logger = hostContext.logger || (0,
|
|
7300
|
+
this.logger = hostContext.logger || (0, import_core2.createLogger)({ level: "info", format: "pretty" });
|
|
7353
7301
|
if (process?.env?.OBJECTQL_STRICT_HOOKS === "1") {
|
|
7354
7302
|
this._strictHookBinding = true;
|
|
7355
7303
|
}
|
|
@@ -7703,6 +7651,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7703
7651
|
if (typeof dv === "object" && dv !== null && dv.dialect && typeof dv.source === "string") {
|
|
7704
7652
|
const result = import_formula4.ExpressionEngine.evaluate(dv, {
|
|
7705
7653
|
now,
|
|
7654
|
+
timezone: execCtx?.timezone,
|
|
7706
7655
|
user: execCtx?.userId ? { id: String(execCtx.userId), role: execCtx?.roles?.[0] } : void 0,
|
|
7707
7656
|
org: execCtx?.tenantId ? { id: String(execCtx.tenantId) } : void 0,
|
|
7708
7657
|
record: out,
|
|
@@ -8594,7 +8543,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8594
8543
|
// ============================================
|
|
8595
8544
|
// Data Access Methods (IDataEngine Interface)
|
|
8596
8545
|
// ============================================
|
|
8597
|
-
async find(object, query) {
|
|
8546
|
+
async find(object, query, options) {
|
|
8598
8547
|
object = this.resolveObjectName(object);
|
|
8599
8548
|
this.logger.debug("Find operation starting", { object, query });
|
|
8600
8549
|
const driver = this.getDriver(object);
|
|
@@ -8623,7 +8572,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8623
8572
|
operation: "find",
|
|
8624
8573
|
ast,
|
|
8625
8574
|
options: query,
|
|
8626
|
-
context: query?.context
|
|
8575
|
+
context: mergeReadContext(query?.context, options?.context)
|
|
8627
8576
|
};
|
|
8628
8577
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
8629
8578
|
const hookContext = {
|
|
@@ -8639,7 +8588,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8639
8588
|
hookContext.input.options = this.buildDriverOptions(opCtx.context, hookContext.input.options);
|
|
8640
8589
|
try {
|
|
8641
8590
|
let result = await driver.find(object, hookContext.input.ast, hookContext.input.options);
|
|
8642
|
-
if (Array.isArray(result)) applyFormulaPlan(_findFormula.plan, result);
|
|
8591
|
+
if (Array.isArray(result)) applyFormulaPlan(_findFormula.plan, result, opCtx.context);
|
|
8643
8592
|
if (ast.expand && Object.keys(ast.expand).length > 0 && Array.isArray(result)) {
|
|
8644
8593
|
result = await this.expandRelatedRecords(object, result, ast.expand, 0, opCtx.context);
|
|
8645
8594
|
}
|
|
@@ -8655,7 +8604,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8655
8604
|
});
|
|
8656
8605
|
return opCtx.result;
|
|
8657
8606
|
}
|
|
8658
|
-
async findOne(objectName, query) {
|
|
8607
|
+
async findOne(objectName, query, options) {
|
|
8659
8608
|
objectName = this.resolveObjectName(objectName);
|
|
8660
8609
|
this.logger.debug("FindOne operation", { objectName });
|
|
8661
8610
|
const driver = this.getDriver(objectName);
|
|
@@ -8678,12 +8627,12 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8678
8627
|
operation: "findOne",
|
|
8679
8628
|
ast,
|
|
8680
8629
|
options: query,
|
|
8681
|
-
context: query?.context
|
|
8630
|
+
context: mergeReadContext(query?.context, options?.context)
|
|
8682
8631
|
};
|
|
8683
8632
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
8684
8633
|
const findOneOpts = this.buildDriverOptions(opCtx.context);
|
|
8685
8634
|
let result = await driver.findOne(objectName, opCtx.ast, findOneOpts);
|
|
8686
|
-
if (result != null) applyFormulaPlan(_findOneFormula.plan, [result]);
|
|
8635
|
+
if (result != null) applyFormulaPlan(_findOneFormula.plan, [result], opCtx.context);
|
|
8687
8636
|
if (ast.expand && Object.keys(ast.expand).length > 0 && result != null) {
|
|
8688
8637
|
const expanded = await this.expandRelatedRecords(objectName, [result], ast.expand, 0, opCtx.context);
|
|
8689
8638
|
result = expanded[0];
|
|
@@ -8898,7 +8847,10 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8898
8847
|
* - `set_null` → clear the foreign key,
|
|
8899
8848
|
* - `restrict` → refuse the delete when dependents exist.
|
|
8900
8849
|
* `master_detail` defaults to `cascade` (the parent owns the child
|
|
8901
|
-
* lifecycle); `lookup` defaults to `set_null
|
|
8850
|
+
* lifecycle); `lookup` defaults to `set_null` — except a `set_null` default
|
|
8851
|
+
* on a REQUIRED lookup escalates to `restrict` (you can't null a NOT NULL
|
|
8852
|
+
* FK; restricting with a clear dependent-count message beats a misleading
|
|
8853
|
+
* "<field> is required" 400 from the child). Only runs for single-id
|
|
8902
8854
|
* deletes — multi/predicate deletes skip cascade (logged).
|
|
8903
8855
|
*/
|
|
8904
8856
|
async cascadeDeleteRelations(object, id, context, depth = 0) {
|
|
@@ -8924,7 +8876,10 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8924
8876
|
resolvedRef = void 0;
|
|
8925
8877
|
}
|
|
8926
8878
|
if (ref !== object && resolvedRef !== object) continue;
|
|
8927
|
-
|
|
8879
|
+
let behavior = fdef.type === "master_detail" ? fdef.deleteBehavior === "restrict" ? "restrict" : "cascade" : fdef.deleteBehavior || "set_null";
|
|
8880
|
+
if (behavior === "set_null" && fdef.required === true) {
|
|
8881
|
+
behavior = "restrict";
|
|
8882
|
+
}
|
|
8928
8883
|
let dependents;
|
|
8929
8884
|
try {
|
|
8930
8885
|
dependents = await this.find(childName, { where: { [fieldName]: id }, context });
|
|
@@ -8933,9 +8888,16 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8933
8888
|
}
|
|
8934
8889
|
if (!dependents || dependents.length === 0) continue;
|
|
8935
8890
|
if (behavior === "restrict") {
|
|
8936
|
-
|
|
8937
|
-
|
|
8891
|
+
const reason = fdef.deleteBehavior !== "restrict" && fdef.required === true ? ` (${fieldName} is required, so it cannot be cleared)` : "";
|
|
8892
|
+
const err = new Error(
|
|
8893
|
+
`Cannot delete ${object} (${id}): ${dependents.length} dependent ${childName} record(s) reference it via ${fieldName}${reason}. Delete or reassign them first, or set deleteBehavior:'cascade' on ${childName}.${fieldName}.`
|
|
8938
8894
|
);
|
|
8895
|
+
err.code = "DELETE_RESTRICTED";
|
|
8896
|
+
err.status = 409;
|
|
8897
|
+
err.object = object;
|
|
8898
|
+
err.dependentObject = childName;
|
|
8899
|
+
err.dependentCount = dependents.length;
|
|
8900
|
+
throw err;
|
|
8939
8901
|
}
|
|
8940
8902
|
for (const dep of dependents) {
|
|
8941
8903
|
const depId = dep?.id;
|
|
@@ -9024,14 +8986,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
9024
8986
|
});
|
|
9025
8987
|
return opCtx.result;
|
|
9026
8988
|
}
|
|
9027
|
-
async count(object, query) {
|
|
8989
|
+
async count(object, query, options) {
|
|
9028
8990
|
object = this.resolveObjectName(object);
|
|
9029
8991
|
const driver = this.getDriver(object);
|
|
9030
8992
|
const opCtx = {
|
|
9031
8993
|
object,
|
|
9032
8994
|
operation: "count",
|
|
9033
8995
|
options: query,
|
|
9034
|
-
context: query?.context
|
|
8996
|
+
context: mergeReadContext(query?.context, options?.context)
|
|
9035
8997
|
};
|
|
9036
8998
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
9037
8999
|
const countOpts = this.buildDriverOptions(opCtx.context);
|
|
@@ -9044,7 +9006,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
9044
9006
|
});
|
|
9045
9007
|
return opCtx.result;
|
|
9046
9008
|
}
|
|
9047
|
-
async aggregate(object, query) {
|
|
9009
|
+
async aggregate(object, query, options) {
|
|
9048
9010
|
object = this.resolveObjectName(object);
|
|
9049
9011
|
const driver = this.getDriver(object);
|
|
9050
9012
|
this.logger.debug(`Aggregate on ${object} using ${driver.name}`, query);
|
|
@@ -9052,7 +9014,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
9052
9014
|
object,
|
|
9053
9015
|
operation: "aggregate",
|
|
9054
9016
|
options: query,
|
|
9055
|
-
context: query?.context
|
|
9017
|
+
context: mergeReadContext(query?.context, options?.context)
|
|
9056
9018
|
};
|
|
9057
9019
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
9058
9020
|
const ast = {
|
|
@@ -9069,11 +9031,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
9069
9031
|
if (!g?.dateGranularity) return true;
|
|
9070
9032
|
return granularityCaps?.[g.dateGranularity] === true;
|
|
9071
9033
|
});
|
|
9072
|
-
|
|
9034
|
+
const tz = query.timezone;
|
|
9035
|
+
const hasDateBucket = structuredItems.some((g) => !!g?.dateGranularity);
|
|
9036
|
+
const tzRequiresInMemory = !!tz && tz !== "UTC" && hasDateBucket;
|
|
9037
|
+
if (typeof drv.aggregate === "function" && allStructuredSupported && !tzRequiresInMemory) {
|
|
9073
9038
|
return drv.aggregate(object, ast, this.buildDriverOptions(opCtx.context));
|
|
9074
9039
|
}
|
|
9075
9040
|
const raw = await driver.find(object, ast, this.buildDriverOptions(opCtx.context));
|
|
9076
|
-
return applyInMemoryAggregation(raw, ast);
|
|
9041
|
+
return applyInMemoryAggregation(raw, ast, tz);
|
|
9077
9042
|
});
|
|
9078
9043
|
return opCtx.result;
|
|
9079
9044
|
}
|
|
@@ -10280,9 +10245,9 @@ var ObjectQLPlugin = class {
|
|
|
10280
10245
|
};
|
|
10281
10246
|
|
|
10282
10247
|
// src/kernel-factory.ts
|
|
10283
|
-
var
|
|
10248
|
+
var import_core3 = require("@objectstack/core");
|
|
10284
10249
|
async function createObjectQLKernel(options = {}) {
|
|
10285
|
-
const kernel = new
|
|
10250
|
+
const kernel = new import_core3.ObjectKernel();
|
|
10286
10251
|
await kernel.use(new ObjectQLPlugin());
|
|
10287
10252
|
if (options.plugins) {
|
|
10288
10253
|
for (const plugin of options.plugins) {
|