@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.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 import_core = require("@objectstack/core");
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" || t === "time") {
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
- function applyInMemoryAggregation(rows, ast) {
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 = d.getUTCFullYear();
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(d.getUTCDate()).padStart(2, "0")}`;
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, d.getUTCMonth(), d.getUTCDate()));
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, import_core.createLogger)({ level: "info", format: "pretty" });
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`. Only runs for single-id
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
- const behavior = fdef.type === "master_detail" ? fdef.deleteBehavior === "restrict" ? "restrict" : "cascade" : fdef.deleteBehavior || "set_null";
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
- throw new Error(
8937
- `Cannot delete ${object} (${id}): ${dependents.length} dependent ${childName} record(s) via ${fieldName}`
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
- if (typeof drv.aggregate === "function" && allStructuredSupported) {
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 import_core2 = require("@objectstack/core");
10248
+ var import_core3 = require("@objectstack/core");
10284
10249
  async function createObjectQLKernel(options = {}) {
10285
- const kernel = new import_core2.ObjectKernel();
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) {