@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.mjs
CHANGED
|
@@ -2773,6 +2773,13 @@ function normaliseVersionToken(v) {
|
|
|
2773
2773
|
}
|
|
2774
2774
|
return s;
|
|
2775
2775
|
}
|
|
2776
|
+
var CLONE_STRIP_FIELDS = [
|
|
2777
|
+
"id",
|
|
2778
|
+
"created_at",
|
|
2779
|
+
"created_by",
|
|
2780
|
+
"updated_at",
|
|
2781
|
+
"updated_by"
|
|
2782
|
+
];
|
|
2776
2783
|
var SERVICE_CONFIG = {
|
|
2777
2784
|
auth: { route: "/api/v1/auth", plugin: "plugin-auth" },
|
|
2778
2785
|
automation: { route: "/api/v1/automation", plugin: "plugin-automation" },
|
|
@@ -4028,6 +4035,68 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4028
4035
|
record: result
|
|
4029
4036
|
};
|
|
4030
4037
|
}
|
|
4038
|
+
/**
|
|
4039
|
+
* Clone a record — read the source, drop engine-owned columns, and
|
|
4040
|
+
* insert a fresh copy. Gated by the object's `enable.clone` capability
|
|
4041
|
+
* (default `true`; only an explicit `enable.clone === false` disables it).
|
|
4042
|
+
*
|
|
4043
|
+
* Shallow by design: it duplicates the record's own scalar/business field
|
|
4044
|
+
* values, not its related child records. The insert path re-stamps audit
|
|
4045
|
+
* columns, regenerates `autonumber` fields, and recomputes derived
|
|
4046
|
+
* (`formula`/`summary`) fields, so the copy is a valid new row rather than
|
|
4047
|
+
* a byte-identical twin. Caller-supplied `overrides` are applied last and
|
|
4048
|
+
* win over the copied values — the natural place to set a new `name`,
|
|
4049
|
+
* clear a unique field, or reset status before insert.
|
|
4050
|
+
*/
|
|
4051
|
+
async cloneData(request) {
|
|
4052
|
+
const schema = this.engine.registry.getObject(request.object);
|
|
4053
|
+
if (!schema) {
|
|
4054
|
+
const err = new Error(`Object '${request.object}' not found`);
|
|
4055
|
+
err.code = "OBJECT_NOT_FOUND";
|
|
4056
|
+
err.status = 404;
|
|
4057
|
+
err.object = request.object;
|
|
4058
|
+
throw err;
|
|
4059
|
+
}
|
|
4060
|
+
if (schema.enable?.clone === false) {
|
|
4061
|
+
const err = new Error(`Cloning is disabled for object '${request.object}'`);
|
|
4062
|
+
err.code = "CLONE_DISABLED";
|
|
4063
|
+
err.status = 403;
|
|
4064
|
+
err.object = request.object;
|
|
4065
|
+
throw err;
|
|
4066
|
+
}
|
|
4067
|
+
const ctx = request.context;
|
|
4068
|
+
const ctxOpt = ctx !== void 0 ? { context: ctx } : void 0;
|
|
4069
|
+
const source = await this.engine.findOne(
|
|
4070
|
+
request.object,
|
|
4071
|
+
{ where: { id: request.id }, ...ctxOpt }
|
|
4072
|
+
);
|
|
4073
|
+
if (!source) {
|
|
4074
|
+
const err = new Error(`Record ${request.id} not found in ${request.object}`);
|
|
4075
|
+
err.code = "RECORD_NOT_FOUND";
|
|
4076
|
+
err.status = 404;
|
|
4077
|
+
err.object = request.object;
|
|
4078
|
+
throw err;
|
|
4079
|
+
}
|
|
4080
|
+
const data = { ...source };
|
|
4081
|
+
for (const f of CLONE_STRIP_FIELDS) delete data[f];
|
|
4082
|
+
const fields = schema.fields || {};
|
|
4083
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
4084
|
+
if (!def) continue;
|
|
4085
|
+
if (def.system === true || def.type === "autonumber" || def.type === "formula" || def.type === "summary") {
|
|
4086
|
+
delete data[name];
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
if (request.overrides && typeof request.overrides === "object") {
|
|
4090
|
+
Object.assign(data, request.overrides);
|
|
4091
|
+
}
|
|
4092
|
+
const result = await this.engine.insert(request.object, data, ctxOpt);
|
|
4093
|
+
return {
|
|
4094
|
+
object: request.object,
|
|
4095
|
+
id: result.id,
|
|
4096
|
+
sourceId: request.id,
|
|
4097
|
+
record: result
|
|
4098
|
+
};
|
|
4099
|
+
}
|
|
4031
4100
|
async updateData(request) {
|
|
4032
4101
|
await this.assertVersionMatch(request.object, request.id, request.expectedVersion, request.context);
|
|
4033
4102
|
const opts = { where: { id: request.id } };
|
|
@@ -4217,145 +4286,6 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4217
4286
|
};
|
|
4218
4287
|
}
|
|
4219
4288
|
// ==========================================
|
|
4220
|
-
// Lead Convert (M10.6)
|
|
4221
|
-
// ==========================================
|
|
4222
|
-
/**
|
|
4223
|
-
* Convert a qualified Lead into an Account + Contact (+ optional
|
|
4224
|
-
* Opportunity) and mark the Lead as converted. Mirrors the Salesforce
|
|
4225
|
-
* lead-conversion model:
|
|
4226
|
-
*
|
|
4227
|
-
* - If `accountId` is provided, the lead's company info is NOT used
|
|
4228
|
-
* to create a new account; the new contact and opportunity link to
|
|
4229
|
-
* the existing account instead.
|
|
4230
|
-
* - If `contactId` is provided, no new contact is created either —
|
|
4231
|
-
* useful when the lead is a new contact at an existing account.
|
|
4232
|
-
* - `createOpportunity` defaults to true; pass `false` to convert
|
|
4233
|
-
* without producing an opportunity (some teams convert "logos
|
|
4234
|
-
* only" first).
|
|
4235
|
-
* - Lead is updated atomically: `is_converted=true`,
|
|
4236
|
-
* `converted_account`/`converted_contact`/`converted_opportunity`
|
|
4237
|
-
* pointers, `converted_date`, and `status='converted'`.
|
|
4238
|
-
*
|
|
4239
|
-
* Atomicity is enforced via the default driver's transaction support
|
|
4240
|
-
* when available; otherwise a best-effort compensation (delete
|
|
4241
|
-
* already-created child records on failure) is attempted. Permission
|
|
4242
|
-
* checks on each child object are inherited from the caller's
|
|
4243
|
-
* execution context so SecurityPlugin still gates account/contact/
|
|
4244
|
-
* opportunity creates.
|
|
4245
|
-
*/
|
|
4246
|
-
async convertLead(request) {
|
|
4247
|
-
const leadId = String(request.leadId || "").trim();
|
|
4248
|
-
if (!leadId) {
|
|
4249
|
-
const err = new Error("leadId is required");
|
|
4250
|
-
err.status = 400;
|
|
4251
|
-
err.code = "INVALID_REQUEST";
|
|
4252
|
-
throw err;
|
|
4253
|
-
}
|
|
4254
|
-
const ctx = request.context;
|
|
4255
|
-
const ctxOpt = ctx !== void 0 ? { context: ctx } : void 0;
|
|
4256
|
-
const lead = await this.engine.findOne("lead", { where: { id: leadId }, ...ctxOpt });
|
|
4257
|
-
if (!lead) {
|
|
4258
|
-
const err = new Error(`Lead '${leadId}' not found`);
|
|
4259
|
-
err.status = 404;
|
|
4260
|
-
err.code = "LEAD_NOT_FOUND";
|
|
4261
|
-
throw err;
|
|
4262
|
-
}
|
|
4263
|
-
if (lead.is_converted) {
|
|
4264
|
-
const err = new Error(`Lead '${leadId}' is already converted`);
|
|
4265
|
-
err.status = 409;
|
|
4266
|
-
err.code = "LEAD_ALREADY_CONVERTED";
|
|
4267
|
-
throw err;
|
|
4268
|
-
}
|
|
4269
|
-
const runConversion = async (trxCtx) => {
|
|
4270
|
-
const opCtx = trxCtx ?? ctx;
|
|
4271
|
-
const trxCtxOpt = opCtx !== void 0 ? { context: opCtx } : void 0;
|
|
4272
|
-
let account;
|
|
4273
|
-
if (request.accountId) {
|
|
4274
|
-
account = await this.engine.findOne("account", { where: { id: request.accountId }, ...trxCtxOpt });
|
|
4275
|
-
if (!account) {
|
|
4276
|
-
const err = new Error(`Account '${request.accountId}' not found`);
|
|
4277
|
-
err.status = 404;
|
|
4278
|
-
err.code = "ACCOUNT_NOT_FOUND";
|
|
4279
|
-
throw err;
|
|
4280
|
-
}
|
|
4281
|
-
} else {
|
|
4282
|
-
const accountPayload = {
|
|
4283
|
-
name: lead.company || `${lead.first_name ?? ""} ${lead.last_name ?? ""}`.trim() || "Untitled Account"
|
|
4284
|
-
};
|
|
4285
|
-
if (lead.industry) accountPayload.industry = lead.industry;
|
|
4286
|
-
if (lead.annual_revenue) accountPayload.annual_revenue = lead.annual_revenue;
|
|
4287
|
-
if (lead.number_of_employees) accountPayload.employees = lead.number_of_employees;
|
|
4288
|
-
if (lead.website) accountPayload.website = lead.website;
|
|
4289
|
-
if (lead.phone) accountPayload.phone = lead.phone;
|
|
4290
|
-
if (lead.address) accountPayload.billing_address = lead.address;
|
|
4291
|
-
if (lead.owner) accountPayload.owner = lead.owner;
|
|
4292
|
-
account = await this.engine.insert("account", accountPayload, trxCtxOpt);
|
|
4293
|
-
}
|
|
4294
|
-
let contact;
|
|
4295
|
-
if (request.contactId) {
|
|
4296
|
-
contact = await this.engine.findOne("contact", { where: { id: request.contactId }, ...trxCtxOpt });
|
|
4297
|
-
if (!contact) {
|
|
4298
|
-
const err = new Error(`Contact '${request.contactId}' not found`);
|
|
4299
|
-
err.status = 404;
|
|
4300
|
-
err.code = "CONTACT_NOT_FOUND";
|
|
4301
|
-
throw err;
|
|
4302
|
-
}
|
|
4303
|
-
} else {
|
|
4304
|
-
const contactPayload = {
|
|
4305
|
-
first_name: lead.first_name ?? "",
|
|
4306
|
-
last_name: lead.last_name ?? lead.company ?? "Unknown"
|
|
4307
|
-
};
|
|
4308
|
-
if (lead.salutation) contactPayload.salutation = lead.salutation;
|
|
4309
|
-
if (lead.email) contactPayload.email = lead.email;
|
|
4310
|
-
if (lead.phone) contactPayload.phone = lead.phone;
|
|
4311
|
-
if (lead.mobile) contactPayload.mobile = lead.mobile;
|
|
4312
|
-
if (lead.title) contactPayload.title = lead.title;
|
|
4313
|
-
if (lead.address) contactPayload.mailing_address = lead.address;
|
|
4314
|
-
if (lead.owner) contactPayload.owner = lead.owner;
|
|
4315
|
-
if (account?.id) contactPayload.account = account.id;
|
|
4316
|
-
contact = await this.engine.insert("contact", contactPayload, trxCtxOpt);
|
|
4317
|
-
}
|
|
4318
|
-
let opportunity = null;
|
|
4319
|
-
const shouldCreateOpp = request.createOpportunity !== false;
|
|
4320
|
-
if (shouldCreateOpp) {
|
|
4321
|
-
const oppOverrides = request.opportunity ?? {};
|
|
4322
|
-
const defaultName = oppOverrides.name || `${account?.name ?? lead.company ?? "Lead"} - New Opportunity`;
|
|
4323
|
-
const defaultClose = oppOverrides.close_date || new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
|
|
4324
|
-
const oppPayload = {
|
|
4325
|
-
name: defaultName,
|
|
4326
|
-
stage: oppOverrides.stage ?? "qualification",
|
|
4327
|
-
close_date: defaultClose
|
|
4328
|
-
};
|
|
4329
|
-
if (oppOverrides.amount !== void 0) oppPayload.amount = oppOverrides.amount;
|
|
4330
|
-
else if (lead.annual_revenue) oppPayload.amount = lead.annual_revenue;
|
|
4331
|
-
if (account?.id) oppPayload.account = account.id;
|
|
4332
|
-
if (contact?.id) oppPayload.primary_contact = contact.id;
|
|
4333
|
-
if (lead.owner) oppPayload.owner = lead.owner;
|
|
4334
|
-
if (lead.lead_source) oppPayload.lead_source = lead.lead_source;
|
|
4335
|
-
opportunity = await this.engine.insert("opportunity", oppPayload, trxCtxOpt);
|
|
4336
|
-
}
|
|
4337
|
-
const leadUpdate = {
|
|
4338
|
-
is_converted: true,
|
|
4339
|
-
status: request.convertedStatus ?? "converted",
|
|
4340
|
-
converted_account: account?.id ?? null,
|
|
4341
|
-
converted_contact: contact?.id ?? null,
|
|
4342
|
-
converted_opportunity: opportunity?.id ?? null,
|
|
4343
|
-
converted_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
4344
|
-
};
|
|
4345
|
-
const updatedLead = await this.engine.update("lead", leadUpdate, {
|
|
4346
|
-
where: { id: leadId },
|
|
4347
|
-
...trxCtxOpt
|
|
4348
|
-
});
|
|
4349
|
-
return {
|
|
4350
|
-
lead: updatedLead ?? { ...lead, ...leadUpdate },
|
|
4351
|
-
account,
|
|
4352
|
-
contact,
|
|
4353
|
-
opportunity
|
|
4354
|
-
};
|
|
4355
|
-
};
|
|
4356
|
-
return this.engine.transaction(runConversion, ctx);
|
|
4357
|
-
}
|
|
4358
|
-
// ==========================================
|
|
4359
4289
|
// Metadata Caching
|
|
4360
4290
|
// ==========================================
|
|
4361
4291
|
async getMetaItemCached(request) {
|
|
@@ -6719,11 +6649,20 @@ function validateOne(name, def, value) {
|
|
|
6719
6649
|
if (value === 0 || value === 1 || value === "0" || value === "1" || value === "true" || value === "false") return null;
|
|
6720
6650
|
return { field: name, code: "invalid_boolean", message: `${name} must be true or false` };
|
|
6721
6651
|
}
|
|
6722
|
-
if (t === "date" || t === "datetime"
|
|
6652
|
+
if (t === "date" || t === "datetime") {
|
|
6723
6653
|
if (value instanceof Date) return null;
|
|
6724
6654
|
if (typeof value === "string" && !Number.isNaN(Date.parse(value))) return null;
|
|
6725
6655
|
return { field: name, code: "invalid_date", message: `${name} must be a valid ${t} (ISO-8601)` };
|
|
6726
6656
|
}
|
|
6657
|
+
if (t === "time") {
|
|
6658
|
+
if (value instanceof Date) return null;
|
|
6659
|
+
if (typeof value === "string") {
|
|
6660
|
+
const timeOfDay = /^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d+)?)?(Z|[+-]([01]\d|2[0-3]):?[0-5]\d)?$/;
|
|
6661
|
+
const hasDate = /\d{4}-\d{2}-\d{2}/.test(value);
|
|
6662
|
+
if (timeOfDay.test(value.trim()) || hasDate && !Number.isNaN(Date.parse(value))) return null;
|
|
6663
|
+
}
|
|
6664
|
+
return { field: name, code: "invalid_time", message: `${name} must be a valid time (HH:MM or HH:MM:SS)` };
|
|
6665
|
+
}
|
|
6727
6666
|
if (t === "select" || t === "radio") {
|
|
6728
6667
|
const allowed = optionValues(def.options);
|
|
6729
6668
|
if (allowed.length > 0 && !allowed.includes(String(value))) {
|
|
@@ -7039,7 +6978,8 @@ function legalNextStates(objectSchema, field, currentState) {
|
|
|
7039
6978
|
}
|
|
7040
6979
|
|
|
7041
6980
|
// src/in-memory-aggregation.ts
|
|
7042
|
-
|
|
6981
|
+
import { calendarPartsInTzOrUtc } from "@objectstack/core";
|
|
6982
|
+
function applyInMemoryAggregation(rows, ast, timezone) {
|
|
7043
6983
|
const groupBy = ast.groupBy ?? [];
|
|
7044
6984
|
const aggregations = ast.aggregations ?? [];
|
|
7045
6985
|
if (groupBy.length === 0 && aggregations.length === 0) return rows;
|
|
@@ -7052,7 +6992,7 @@ function applyInMemoryAggregation(rows, ast) {
|
|
|
7052
6992
|
const parts = [];
|
|
7053
6993
|
for (const g of groupBy) {
|
|
7054
6994
|
const fieldName = typeof g === "string" ? g : g.alias ?? g.field;
|
|
7055
|
-
const value = projectGroupValue(row, g);
|
|
6995
|
+
const value = projectGroupValue(row, g, timezone);
|
|
7056
6996
|
key[fieldName] = value;
|
|
7057
6997
|
parts.push(`${fieldName}=${value}`);
|
|
7058
6998
|
}
|
|
@@ -7071,11 +7011,11 @@ function applyInMemoryAggregation(rows, ast) {
|
|
|
7071
7011
|
}
|
|
7072
7012
|
return out;
|
|
7073
7013
|
}
|
|
7074
|
-
function projectGroupValue(row, g) {
|
|
7014
|
+
function projectGroupValue(row, g, timezone) {
|
|
7075
7015
|
const field = typeof g === "string" ? g : g.field;
|
|
7076
7016
|
const v = row?.[field];
|
|
7077
7017
|
if (typeof g !== "string" && g.dateGranularity) {
|
|
7078
|
-
return bucketDateValue(v, g.dateGranularity);
|
|
7018
|
+
return bucketDateValue(v, g.dateGranularity, timezone);
|
|
7079
7019
|
}
|
|
7080
7020
|
return v == null ? "(null)" : String(v);
|
|
7081
7021
|
}
|
|
@@ -7153,12 +7093,11 @@ function toNumber(v) {
|
|
|
7153
7093
|
const n = Number(v);
|
|
7154
7094
|
return Number.isFinite(n) ? n : 0;
|
|
7155
7095
|
}
|
|
7156
|
-
function bucketDateValue(value, granularity) {
|
|
7096
|
+
function bucketDateValue(value, granularity, timezone) {
|
|
7157
7097
|
if (value == null) return "(null)";
|
|
7158
7098
|
const d = value instanceof Date ? value : new Date(String(value));
|
|
7159
7099
|
if (Number.isNaN(d.getTime())) return "(null)";
|
|
7160
|
-
const y = d
|
|
7161
|
-
const m = d.getUTCMonth() + 1;
|
|
7100
|
+
const { year: y, month: m, day } = calendarPartsInTzOrUtc(d, timezone);
|
|
7162
7101
|
switch (granularity) {
|
|
7163
7102
|
case "year":
|
|
7164
7103
|
return String(y);
|
|
@@ -7167,9 +7106,9 @@ function bucketDateValue(value, granularity) {
|
|
|
7167
7106
|
case "month":
|
|
7168
7107
|
return `${y}-${String(m).padStart(2, "0")}`;
|
|
7169
7108
|
case "day":
|
|
7170
|
-
return `${y}-${String(m).padStart(2, "0")}-${String(
|
|
7109
|
+
return `${y}-${String(m).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
7171
7110
|
case "week": {
|
|
7172
|
-
const target = new Date(Date.UTC(y,
|
|
7111
|
+
const target = new Date(Date.UTC(y, m - 1, day));
|
|
7173
7112
|
const dayNum = (target.getUTCDay() + 6) % 7;
|
|
7174
7113
|
target.setUTCDate(target.getUTCDate() - dayNum + 3);
|
|
7175
7114
|
const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4));
|
|
@@ -7212,16 +7151,25 @@ function planFormulaProjection(schema, requestedFields) {
|
|
|
7212
7151
|
}
|
|
7213
7152
|
return { plan };
|
|
7214
7153
|
}
|
|
7215
|
-
function applyFormulaPlan(plan, records) {
|
|
7154
|
+
function applyFormulaPlan(plan, records, execCtx, nowSnapshot) {
|
|
7216
7155
|
if (!plan.length) return;
|
|
7156
|
+
const now = nowSnapshot ?? /* @__PURE__ */ new Date();
|
|
7157
|
+
const timezone = execCtx?.timezone;
|
|
7158
|
+
const user = execCtx?.userId ? { id: String(execCtx.userId), role: execCtx?.roles?.[0] } : void 0;
|
|
7159
|
+
const org = execCtx?.tenantId ? { id: String(execCtx.tenantId) } : void 0;
|
|
7217
7160
|
for (const rec of records) {
|
|
7218
7161
|
if (rec == null) continue;
|
|
7219
7162
|
for (const fp of plan) {
|
|
7220
|
-
const r = ExpressionEngine3.evaluate(fp.expression, { record: rec });
|
|
7163
|
+
const r = ExpressionEngine3.evaluate(fp.expression, { now, timezone, user, org, record: rec });
|
|
7221
7164
|
rec[fp.name] = r.ok ? r.value : null;
|
|
7222
7165
|
}
|
|
7223
7166
|
}
|
|
7224
7167
|
}
|
|
7168
|
+
function mergeReadContext(fromQuery, fromOptions) {
|
|
7169
|
+
if (fromOptions == null) return fromQuery;
|
|
7170
|
+
if (fromQuery == null) return fromOptions;
|
|
7171
|
+
return { ...fromQuery, ...fromOptions };
|
|
7172
|
+
}
|
|
7225
7173
|
function resolveMetadataItemName(key, item) {
|
|
7226
7174
|
if (!item) return void 0;
|
|
7227
7175
|
if (item.name) return item.name;
|
|
@@ -7644,6 +7592,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7644
7592
|
if (typeof dv === "object" && dv !== null && dv.dialect && typeof dv.source === "string") {
|
|
7645
7593
|
const result = ExpressionEngine3.evaluate(dv, {
|
|
7646
7594
|
now,
|
|
7595
|
+
timezone: execCtx?.timezone,
|
|
7647
7596
|
user: execCtx?.userId ? { id: String(execCtx.userId), role: execCtx?.roles?.[0] } : void 0,
|
|
7648
7597
|
org: execCtx?.tenantId ? { id: String(execCtx.tenantId) } : void 0,
|
|
7649
7598
|
record: out,
|
|
@@ -8535,7 +8484,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8535
8484
|
// ============================================
|
|
8536
8485
|
// Data Access Methods (IDataEngine Interface)
|
|
8537
8486
|
// ============================================
|
|
8538
|
-
async find(object, query) {
|
|
8487
|
+
async find(object, query, options) {
|
|
8539
8488
|
object = this.resolveObjectName(object);
|
|
8540
8489
|
this.logger.debug("Find operation starting", { object, query });
|
|
8541
8490
|
const driver = this.getDriver(object);
|
|
@@ -8564,7 +8513,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8564
8513
|
operation: "find",
|
|
8565
8514
|
ast,
|
|
8566
8515
|
options: query,
|
|
8567
|
-
context: query?.context
|
|
8516
|
+
context: mergeReadContext(query?.context, options?.context)
|
|
8568
8517
|
};
|
|
8569
8518
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
8570
8519
|
const hookContext = {
|
|
@@ -8580,7 +8529,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8580
8529
|
hookContext.input.options = this.buildDriverOptions(opCtx.context, hookContext.input.options);
|
|
8581
8530
|
try {
|
|
8582
8531
|
let result = await driver.find(object, hookContext.input.ast, hookContext.input.options);
|
|
8583
|
-
if (Array.isArray(result)) applyFormulaPlan(_findFormula.plan, result);
|
|
8532
|
+
if (Array.isArray(result)) applyFormulaPlan(_findFormula.plan, result, opCtx.context);
|
|
8584
8533
|
if (ast.expand && Object.keys(ast.expand).length > 0 && Array.isArray(result)) {
|
|
8585
8534
|
result = await this.expandRelatedRecords(object, result, ast.expand, 0, opCtx.context);
|
|
8586
8535
|
}
|
|
@@ -8596,7 +8545,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8596
8545
|
});
|
|
8597
8546
|
return opCtx.result;
|
|
8598
8547
|
}
|
|
8599
|
-
async findOne(objectName, query) {
|
|
8548
|
+
async findOne(objectName, query, options) {
|
|
8600
8549
|
objectName = this.resolveObjectName(objectName);
|
|
8601
8550
|
this.logger.debug("FindOne operation", { objectName });
|
|
8602
8551
|
const driver = this.getDriver(objectName);
|
|
@@ -8619,12 +8568,12 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8619
8568
|
operation: "findOne",
|
|
8620
8569
|
ast,
|
|
8621
8570
|
options: query,
|
|
8622
|
-
context: query?.context
|
|
8571
|
+
context: mergeReadContext(query?.context, options?.context)
|
|
8623
8572
|
};
|
|
8624
8573
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
8625
8574
|
const findOneOpts = this.buildDriverOptions(opCtx.context);
|
|
8626
8575
|
let result = await driver.findOne(objectName, opCtx.ast, findOneOpts);
|
|
8627
|
-
if (result != null) applyFormulaPlan(_findOneFormula.plan, [result]);
|
|
8576
|
+
if (result != null) applyFormulaPlan(_findOneFormula.plan, [result], opCtx.context);
|
|
8628
8577
|
if (ast.expand && Object.keys(ast.expand).length > 0 && result != null) {
|
|
8629
8578
|
const expanded = await this.expandRelatedRecords(objectName, [result], ast.expand, 0, opCtx.context);
|
|
8630
8579
|
result = expanded[0];
|
|
@@ -8839,7 +8788,10 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8839
8788
|
* - `set_null` → clear the foreign key,
|
|
8840
8789
|
* - `restrict` → refuse the delete when dependents exist.
|
|
8841
8790
|
* `master_detail` defaults to `cascade` (the parent owns the child
|
|
8842
|
-
* lifecycle); `lookup` defaults to `set_null
|
|
8791
|
+
* lifecycle); `lookup` defaults to `set_null` — except a `set_null` default
|
|
8792
|
+
* on a REQUIRED lookup escalates to `restrict` (you can't null a NOT NULL
|
|
8793
|
+
* FK; restricting with a clear dependent-count message beats a misleading
|
|
8794
|
+
* "<field> is required" 400 from the child). Only runs for single-id
|
|
8843
8795
|
* deletes — multi/predicate deletes skip cascade (logged).
|
|
8844
8796
|
*/
|
|
8845
8797
|
async cascadeDeleteRelations(object, id, context, depth = 0) {
|
|
@@ -8865,7 +8817,10 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8865
8817
|
resolvedRef = void 0;
|
|
8866
8818
|
}
|
|
8867
8819
|
if (ref !== object && resolvedRef !== object) continue;
|
|
8868
|
-
|
|
8820
|
+
let behavior = fdef.type === "master_detail" ? fdef.deleteBehavior === "restrict" ? "restrict" : "cascade" : fdef.deleteBehavior || "set_null";
|
|
8821
|
+
if (behavior === "set_null" && fdef.required === true) {
|
|
8822
|
+
behavior = "restrict";
|
|
8823
|
+
}
|
|
8869
8824
|
let dependents;
|
|
8870
8825
|
try {
|
|
8871
8826
|
dependents = await this.find(childName, { where: { [fieldName]: id }, context });
|
|
@@ -8874,9 +8829,16 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8874
8829
|
}
|
|
8875
8830
|
if (!dependents || dependents.length === 0) continue;
|
|
8876
8831
|
if (behavior === "restrict") {
|
|
8877
|
-
|
|
8878
|
-
|
|
8832
|
+
const reason = fdef.deleteBehavior !== "restrict" && fdef.required === true ? ` (${fieldName} is required, so it cannot be cleared)` : "";
|
|
8833
|
+
const err = new Error(
|
|
8834
|
+
`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}.`
|
|
8879
8835
|
);
|
|
8836
|
+
err.code = "DELETE_RESTRICTED";
|
|
8837
|
+
err.status = 409;
|
|
8838
|
+
err.object = object;
|
|
8839
|
+
err.dependentObject = childName;
|
|
8840
|
+
err.dependentCount = dependents.length;
|
|
8841
|
+
throw err;
|
|
8880
8842
|
}
|
|
8881
8843
|
for (const dep of dependents) {
|
|
8882
8844
|
const depId = dep?.id;
|
|
@@ -8965,14 +8927,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8965
8927
|
});
|
|
8966
8928
|
return opCtx.result;
|
|
8967
8929
|
}
|
|
8968
|
-
async count(object, query) {
|
|
8930
|
+
async count(object, query, options) {
|
|
8969
8931
|
object = this.resolveObjectName(object);
|
|
8970
8932
|
const driver = this.getDriver(object);
|
|
8971
8933
|
const opCtx = {
|
|
8972
8934
|
object,
|
|
8973
8935
|
operation: "count",
|
|
8974
8936
|
options: query,
|
|
8975
|
-
context: query?.context
|
|
8937
|
+
context: mergeReadContext(query?.context, options?.context)
|
|
8976
8938
|
};
|
|
8977
8939
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
8978
8940
|
const countOpts = this.buildDriverOptions(opCtx.context);
|
|
@@ -8985,7 +8947,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8985
8947
|
});
|
|
8986
8948
|
return opCtx.result;
|
|
8987
8949
|
}
|
|
8988
|
-
async aggregate(object, query) {
|
|
8950
|
+
async aggregate(object, query, options) {
|
|
8989
8951
|
object = this.resolveObjectName(object);
|
|
8990
8952
|
const driver = this.getDriver(object);
|
|
8991
8953
|
this.logger.debug(`Aggregate on ${object} using ${driver.name}`, query);
|
|
@@ -8993,7 +8955,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8993
8955
|
object,
|
|
8994
8956
|
operation: "aggregate",
|
|
8995
8957
|
options: query,
|
|
8996
|
-
context: query?.context
|
|
8958
|
+
context: mergeReadContext(query?.context, options?.context)
|
|
8997
8959
|
};
|
|
8998
8960
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
8999
8961
|
const ast = {
|
|
@@ -9010,11 +8972,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
9010
8972
|
if (!g?.dateGranularity) return true;
|
|
9011
8973
|
return granularityCaps?.[g.dateGranularity] === true;
|
|
9012
8974
|
});
|
|
9013
|
-
|
|
8975
|
+
const tz = query.timezone;
|
|
8976
|
+
const hasDateBucket = structuredItems.some((g) => !!g?.dateGranularity);
|
|
8977
|
+
const tzRequiresInMemory = !!tz && tz !== "UTC" && hasDateBucket;
|
|
8978
|
+
if (typeof drv.aggregate === "function" && allStructuredSupported && !tzRequiresInMemory) {
|
|
9014
8979
|
return drv.aggregate(object, ast, this.buildDriverOptions(opCtx.context));
|
|
9015
8980
|
}
|
|
9016
8981
|
const raw = await driver.find(object, ast, this.buildDriverOptions(opCtx.context));
|
|
9017
|
-
return applyInMemoryAggregation(raw, ast);
|
|
8982
|
+
return applyInMemoryAggregation(raw, ast, tz);
|
|
9018
8983
|
});
|
|
9019
8984
|
return opCtx.result;
|
|
9020
8985
|
}
|