@objectstack/objectql 9.8.0 → 9.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +21 -47
- package/dist/index.d.ts +21 -47
- package/dist/index.js +54 -163
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +50 -159
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -4286,145 +4286,6 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
4286
4286
|
};
|
|
4287
4287
|
}
|
|
4288
4288
|
// ==========================================
|
|
4289
|
-
// Lead Convert (M10.6)
|
|
4290
|
-
// ==========================================
|
|
4291
|
-
/**
|
|
4292
|
-
* Convert a qualified Lead into an Account + Contact (+ optional
|
|
4293
|
-
* Opportunity) and mark the Lead as converted. Mirrors the Salesforce
|
|
4294
|
-
* lead-conversion model:
|
|
4295
|
-
*
|
|
4296
|
-
* - If `accountId` is provided, the lead's company info is NOT used
|
|
4297
|
-
* to create a new account; the new contact and opportunity link to
|
|
4298
|
-
* the existing account instead.
|
|
4299
|
-
* - If `contactId` is provided, no new contact is created either —
|
|
4300
|
-
* useful when the lead is a new contact at an existing account.
|
|
4301
|
-
* - `createOpportunity` defaults to true; pass `false` to convert
|
|
4302
|
-
* without producing an opportunity (some teams convert "logos
|
|
4303
|
-
* only" first).
|
|
4304
|
-
* - Lead is updated atomically: `is_converted=true`,
|
|
4305
|
-
* `converted_account`/`converted_contact`/`converted_opportunity`
|
|
4306
|
-
* pointers, `converted_date`, and `status='converted'`.
|
|
4307
|
-
*
|
|
4308
|
-
* Atomicity is enforced via the default driver's transaction support
|
|
4309
|
-
* when available; otherwise a best-effort compensation (delete
|
|
4310
|
-
* already-created child records on failure) is attempted. Permission
|
|
4311
|
-
* checks on each child object are inherited from the caller's
|
|
4312
|
-
* execution context so SecurityPlugin still gates account/contact/
|
|
4313
|
-
* opportunity creates.
|
|
4314
|
-
*/
|
|
4315
|
-
async convertLead(request) {
|
|
4316
|
-
const leadId = String(request.leadId || "").trim();
|
|
4317
|
-
if (!leadId) {
|
|
4318
|
-
const err = new Error("leadId is required");
|
|
4319
|
-
err.status = 400;
|
|
4320
|
-
err.code = "INVALID_REQUEST";
|
|
4321
|
-
throw err;
|
|
4322
|
-
}
|
|
4323
|
-
const ctx = request.context;
|
|
4324
|
-
const ctxOpt = ctx !== void 0 ? { context: ctx } : void 0;
|
|
4325
|
-
const lead = await this.engine.findOne("lead", { where: { id: leadId }, ...ctxOpt });
|
|
4326
|
-
if (!lead) {
|
|
4327
|
-
const err = new Error(`Lead '${leadId}' not found`);
|
|
4328
|
-
err.status = 404;
|
|
4329
|
-
err.code = "LEAD_NOT_FOUND";
|
|
4330
|
-
throw err;
|
|
4331
|
-
}
|
|
4332
|
-
if (lead.is_converted) {
|
|
4333
|
-
const err = new Error(`Lead '${leadId}' is already converted`);
|
|
4334
|
-
err.status = 409;
|
|
4335
|
-
err.code = "LEAD_ALREADY_CONVERTED";
|
|
4336
|
-
throw err;
|
|
4337
|
-
}
|
|
4338
|
-
const runConversion = async (trxCtx) => {
|
|
4339
|
-
const opCtx = trxCtx ?? ctx;
|
|
4340
|
-
const trxCtxOpt = opCtx !== void 0 ? { context: opCtx } : void 0;
|
|
4341
|
-
let account;
|
|
4342
|
-
if (request.accountId) {
|
|
4343
|
-
account = await this.engine.findOne("account", { where: { id: request.accountId }, ...trxCtxOpt });
|
|
4344
|
-
if (!account) {
|
|
4345
|
-
const err = new Error(`Account '${request.accountId}' not found`);
|
|
4346
|
-
err.status = 404;
|
|
4347
|
-
err.code = "ACCOUNT_NOT_FOUND";
|
|
4348
|
-
throw err;
|
|
4349
|
-
}
|
|
4350
|
-
} else {
|
|
4351
|
-
const accountPayload = {
|
|
4352
|
-
name: lead.company || `${lead.first_name ?? ""} ${lead.last_name ?? ""}`.trim() || "Untitled Account"
|
|
4353
|
-
};
|
|
4354
|
-
if (lead.industry) accountPayload.industry = lead.industry;
|
|
4355
|
-
if (lead.annual_revenue) accountPayload.annual_revenue = lead.annual_revenue;
|
|
4356
|
-
if (lead.number_of_employees) accountPayload.employees = lead.number_of_employees;
|
|
4357
|
-
if (lead.website) accountPayload.website = lead.website;
|
|
4358
|
-
if (lead.phone) accountPayload.phone = lead.phone;
|
|
4359
|
-
if (lead.address) accountPayload.billing_address = lead.address;
|
|
4360
|
-
if (lead.owner) accountPayload.owner = lead.owner;
|
|
4361
|
-
account = await this.engine.insert("account", accountPayload, trxCtxOpt);
|
|
4362
|
-
}
|
|
4363
|
-
let contact;
|
|
4364
|
-
if (request.contactId) {
|
|
4365
|
-
contact = await this.engine.findOne("contact", { where: { id: request.contactId }, ...trxCtxOpt });
|
|
4366
|
-
if (!contact) {
|
|
4367
|
-
const err = new Error(`Contact '${request.contactId}' not found`);
|
|
4368
|
-
err.status = 404;
|
|
4369
|
-
err.code = "CONTACT_NOT_FOUND";
|
|
4370
|
-
throw err;
|
|
4371
|
-
}
|
|
4372
|
-
} else {
|
|
4373
|
-
const contactPayload = {
|
|
4374
|
-
first_name: lead.first_name ?? "",
|
|
4375
|
-
last_name: lead.last_name ?? lead.company ?? "Unknown"
|
|
4376
|
-
};
|
|
4377
|
-
if (lead.salutation) contactPayload.salutation = lead.salutation;
|
|
4378
|
-
if (lead.email) contactPayload.email = lead.email;
|
|
4379
|
-
if (lead.phone) contactPayload.phone = lead.phone;
|
|
4380
|
-
if (lead.mobile) contactPayload.mobile = lead.mobile;
|
|
4381
|
-
if (lead.title) contactPayload.title = lead.title;
|
|
4382
|
-
if (lead.address) contactPayload.mailing_address = lead.address;
|
|
4383
|
-
if (lead.owner) contactPayload.owner = lead.owner;
|
|
4384
|
-
if (account?.id) contactPayload.account = account.id;
|
|
4385
|
-
contact = await this.engine.insert("contact", contactPayload, trxCtxOpt);
|
|
4386
|
-
}
|
|
4387
|
-
let opportunity = null;
|
|
4388
|
-
const shouldCreateOpp = request.createOpportunity !== false;
|
|
4389
|
-
if (shouldCreateOpp) {
|
|
4390
|
-
const oppOverrides = request.opportunity ?? {};
|
|
4391
|
-
const defaultName = oppOverrides.name || `${account?.name ?? lead.company ?? "Lead"} - New Opportunity`;
|
|
4392
|
-
const defaultClose = oppOverrides.close_date || new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
|
|
4393
|
-
const oppPayload = {
|
|
4394
|
-
name: defaultName,
|
|
4395
|
-
stage: oppOverrides.stage ?? "qualification",
|
|
4396
|
-
close_date: defaultClose
|
|
4397
|
-
};
|
|
4398
|
-
if (oppOverrides.amount !== void 0) oppPayload.amount = oppOverrides.amount;
|
|
4399
|
-
else if (lead.annual_revenue) oppPayload.amount = lead.annual_revenue;
|
|
4400
|
-
if (account?.id) oppPayload.account = account.id;
|
|
4401
|
-
if (contact?.id) oppPayload.primary_contact = contact.id;
|
|
4402
|
-
if (lead.owner) oppPayload.owner = lead.owner;
|
|
4403
|
-
if (lead.lead_source) oppPayload.lead_source = lead.lead_source;
|
|
4404
|
-
opportunity = await this.engine.insert("opportunity", oppPayload, trxCtxOpt);
|
|
4405
|
-
}
|
|
4406
|
-
const leadUpdate = {
|
|
4407
|
-
is_converted: true,
|
|
4408
|
-
status: request.convertedStatus ?? "converted",
|
|
4409
|
-
converted_account: account?.id ?? null,
|
|
4410
|
-
converted_contact: contact?.id ?? null,
|
|
4411
|
-
converted_opportunity: opportunity?.id ?? null,
|
|
4412
|
-
converted_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
4413
|
-
};
|
|
4414
|
-
const updatedLead = await this.engine.update("lead", leadUpdate, {
|
|
4415
|
-
where: { id: leadId },
|
|
4416
|
-
...trxCtxOpt
|
|
4417
|
-
});
|
|
4418
|
-
return {
|
|
4419
|
-
lead: updatedLead ?? { ...lead, ...leadUpdate },
|
|
4420
|
-
account,
|
|
4421
|
-
contact,
|
|
4422
|
-
opportunity
|
|
4423
|
-
};
|
|
4424
|
-
};
|
|
4425
|
-
return this.engine.transaction(runConversion, ctx);
|
|
4426
|
-
}
|
|
4427
|
-
// ==========================================
|
|
4428
4289
|
// Metadata Caching
|
|
4429
4290
|
// ==========================================
|
|
4430
4291
|
async getMetaItemCached(request) {
|
|
@@ -6788,11 +6649,20 @@ function validateOne(name, def, value) {
|
|
|
6788
6649
|
if (value === 0 || value === 1 || value === "0" || value === "1" || value === "true" || value === "false") return null;
|
|
6789
6650
|
return { field: name, code: "invalid_boolean", message: `${name} must be true or false` };
|
|
6790
6651
|
}
|
|
6791
|
-
if (t === "date" || t === "datetime"
|
|
6652
|
+
if (t === "date" || t === "datetime") {
|
|
6792
6653
|
if (value instanceof Date) return null;
|
|
6793
6654
|
if (typeof value === "string" && !Number.isNaN(Date.parse(value))) return null;
|
|
6794
6655
|
return { field: name, code: "invalid_date", message: `${name} must be a valid ${t} (ISO-8601)` };
|
|
6795
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
|
+
}
|
|
6796
6666
|
if (t === "select" || t === "radio") {
|
|
6797
6667
|
const allowed = optionValues(def.options);
|
|
6798
6668
|
if (allowed.length > 0 && !allowed.includes(String(value))) {
|
|
@@ -7108,7 +6978,8 @@ function legalNextStates(objectSchema, field, currentState) {
|
|
|
7108
6978
|
}
|
|
7109
6979
|
|
|
7110
6980
|
// src/in-memory-aggregation.ts
|
|
7111
|
-
|
|
6981
|
+
import { calendarPartsInTzOrUtc } from "@objectstack/core";
|
|
6982
|
+
function applyInMemoryAggregation(rows, ast, timezone) {
|
|
7112
6983
|
const groupBy = ast.groupBy ?? [];
|
|
7113
6984
|
const aggregations = ast.aggregations ?? [];
|
|
7114
6985
|
if (groupBy.length === 0 && aggregations.length === 0) return rows;
|
|
@@ -7121,7 +6992,7 @@ function applyInMemoryAggregation(rows, ast) {
|
|
|
7121
6992
|
const parts = [];
|
|
7122
6993
|
for (const g of groupBy) {
|
|
7123
6994
|
const fieldName = typeof g === "string" ? g : g.alias ?? g.field;
|
|
7124
|
-
const value = projectGroupValue(row, g);
|
|
6995
|
+
const value = projectGroupValue(row, g, timezone);
|
|
7125
6996
|
key[fieldName] = value;
|
|
7126
6997
|
parts.push(`${fieldName}=${value}`);
|
|
7127
6998
|
}
|
|
@@ -7140,11 +7011,11 @@ function applyInMemoryAggregation(rows, ast) {
|
|
|
7140
7011
|
}
|
|
7141
7012
|
return out;
|
|
7142
7013
|
}
|
|
7143
|
-
function projectGroupValue(row, g) {
|
|
7014
|
+
function projectGroupValue(row, g, timezone) {
|
|
7144
7015
|
const field = typeof g === "string" ? g : g.field;
|
|
7145
7016
|
const v = row?.[field];
|
|
7146
7017
|
if (typeof g !== "string" && g.dateGranularity) {
|
|
7147
|
-
return bucketDateValue(v, g.dateGranularity);
|
|
7018
|
+
return bucketDateValue(v, g.dateGranularity, timezone);
|
|
7148
7019
|
}
|
|
7149
7020
|
return v == null ? "(null)" : String(v);
|
|
7150
7021
|
}
|
|
@@ -7222,12 +7093,11 @@ function toNumber(v) {
|
|
|
7222
7093
|
const n = Number(v);
|
|
7223
7094
|
return Number.isFinite(n) ? n : 0;
|
|
7224
7095
|
}
|
|
7225
|
-
function bucketDateValue(value, granularity) {
|
|
7096
|
+
function bucketDateValue(value, granularity, timezone) {
|
|
7226
7097
|
if (value == null) return "(null)";
|
|
7227
7098
|
const d = value instanceof Date ? value : new Date(String(value));
|
|
7228
7099
|
if (Number.isNaN(d.getTime())) return "(null)";
|
|
7229
|
-
const y = d
|
|
7230
|
-
const m = d.getUTCMonth() + 1;
|
|
7100
|
+
const { year: y, month: m, day } = calendarPartsInTzOrUtc(d, timezone);
|
|
7231
7101
|
switch (granularity) {
|
|
7232
7102
|
case "year":
|
|
7233
7103
|
return String(y);
|
|
@@ -7236,9 +7106,9 @@ function bucketDateValue(value, granularity) {
|
|
|
7236
7106
|
case "month":
|
|
7237
7107
|
return `${y}-${String(m).padStart(2, "0")}`;
|
|
7238
7108
|
case "day":
|
|
7239
|
-
return `${y}-${String(m).padStart(2, "0")}-${String(
|
|
7109
|
+
return `${y}-${String(m).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
7240
7110
|
case "week": {
|
|
7241
|
-
const target = new Date(Date.UTC(y,
|
|
7111
|
+
const target = new Date(Date.UTC(y, m - 1, day));
|
|
7242
7112
|
const dayNum = (target.getUTCDay() + 6) % 7;
|
|
7243
7113
|
target.setUTCDate(target.getUTCDate() - dayNum + 3);
|
|
7244
7114
|
const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4));
|
|
@@ -7281,12 +7151,16 @@ function planFormulaProjection(schema, requestedFields) {
|
|
|
7281
7151
|
}
|
|
7282
7152
|
return { plan };
|
|
7283
7153
|
}
|
|
7284
|
-
function applyFormulaPlan(plan, records) {
|
|
7154
|
+
function applyFormulaPlan(plan, records, execCtx, nowSnapshot) {
|
|
7285
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;
|
|
7286
7160
|
for (const rec of records) {
|
|
7287
7161
|
if (rec == null) continue;
|
|
7288
7162
|
for (const fp of plan) {
|
|
7289
|
-
const r = ExpressionEngine3.evaluate(fp.expression, { record: rec });
|
|
7163
|
+
const r = ExpressionEngine3.evaluate(fp.expression, { now, timezone, user, org, record: rec });
|
|
7290
7164
|
rec[fp.name] = r.ok ? r.value : null;
|
|
7291
7165
|
}
|
|
7292
7166
|
}
|
|
@@ -7718,6 +7592,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
7718
7592
|
if (typeof dv === "object" && dv !== null && dv.dialect && typeof dv.source === "string") {
|
|
7719
7593
|
const result = ExpressionEngine3.evaluate(dv, {
|
|
7720
7594
|
now,
|
|
7595
|
+
timezone: execCtx?.timezone,
|
|
7721
7596
|
user: execCtx?.userId ? { id: String(execCtx.userId), role: execCtx?.roles?.[0] } : void 0,
|
|
7722
7597
|
org: execCtx?.tenantId ? { id: String(execCtx.tenantId) } : void 0,
|
|
7723
7598
|
record: out,
|
|
@@ -8654,7 +8529,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8654
8529
|
hookContext.input.options = this.buildDriverOptions(opCtx.context, hookContext.input.options);
|
|
8655
8530
|
try {
|
|
8656
8531
|
let result = await driver.find(object, hookContext.input.ast, hookContext.input.options);
|
|
8657
|
-
if (Array.isArray(result)) applyFormulaPlan(_findFormula.plan, result);
|
|
8532
|
+
if (Array.isArray(result)) applyFormulaPlan(_findFormula.plan, result, opCtx.context);
|
|
8658
8533
|
if (ast.expand && Object.keys(ast.expand).length > 0 && Array.isArray(result)) {
|
|
8659
8534
|
result = await this.expandRelatedRecords(object, result, ast.expand, 0, opCtx.context);
|
|
8660
8535
|
}
|
|
@@ -8698,7 +8573,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8698
8573
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
8699
8574
|
const findOneOpts = this.buildDriverOptions(opCtx.context);
|
|
8700
8575
|
let result = await driver.findOne(objectName, opCtx.ast, findOneOpts);
|
|
8701
|
-
if (result != null) applyFormulaPlan(_findOneFormula.plan, [result]);
|
|
8576
|
+
if (result != null) applyFormulaPlan(_findOneFormula.plan, [result], opCtx.context);
|
|
8702
8577
|
if (ast.expand && Object.keys(ast.expand).length > 0 && result != null) {
|
|
8703
8578
|
const expanded = await this.expandRelatedRecords(objectName, [result], ast.expand, 0, opCtx.context);
|
|
8704
8579
|
result = expanded[0];
|
|
@@ -8913,7 +8788,10 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8913
8788
|
* - `set_null` → clear the foreign key,
|
|
8914
8789
|
* - `restrict` → refuse the delete when dependents exist.
|
|
8915
8790
|
* `master_detail` defaults to `cascade` (the parent owns the child
|
|
8916
|
-
* 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
|
|
8917
8795
|
* deletes — multi/predicate deletes skip cascade (logged).
|
|
8918
8796
|
*/
|
|
8919
8797
|
async cascadeDeleteRelations(object, id, context, depth = 0) {
|
|
@@ -8939,7 +8817,10 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8939
8817
|
resolvedRef = void 0;
|
|
8940
8818
|
}
|
|
8941
8819
|
if (ref !== object && resolvedRef !== object) continue;
|
|
8942
|
-
|
|
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
|
+
}
|
|
8943
8824
|
let dependents;
|
|
8944
8825
|
try {
|
|
8945
8826
|
dependents = await this.find(childName, { where: { [fieldName]: id }, context });
|
|
@@ -8948,9 +8829,16 @@ var _ObjectQL = class _ObjectQL {
|
|
|
8948
8829
|
}
|
|
8949
8830
|
if (!dependents || dependents.length === 0) continue;
|
|
8950
8831
|
if (behavior === "restrict") {
|
|
8951
|
-
|
|
8952
|
-
|
|
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}.`
|
|
8953
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;
|
|
8954
8842
|
}
|
|
8955
8843
|
for (const dep of dependents) {
|
|
8956
8844
|
const depId = dep?.id;
|
|
@@ -9084,11 +8972,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
9084
8972
|
if (!g?.dateGranularity) return true;
|
|
9085
8973
|
return granularityCaps?.[g.dateGranularity] === true;
|
|
9086
8974
|
});
|
|
9087
|
-
|
|
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) {
|
|
9088
8979
|
return drv.aggregate(object, ast, this.buildDriverOptions(opCtx.context));
|
|
9089
8980
|
}
|
|
9090
8981
|
const raw = await driver.find(object, ast, this.buildDriverOptions(opCtx.context));
|
|
9091
|
-
return applyInMemoryAggregation(raw, ast);
|
|
8982
|
+
return applyInMemoryAggregation(raw, ast, tz);
|
|
9092
8983
|
});
|
|
9093
8984
|
return opCtx.result;
|
|
9094
8985
|
}
|