@objectstack/objectql 9.6.0 → 9.8.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 CHANGED
@@ -1065,6 +1065,30 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
1065
1065
  id: any;
1066
1066
  record: any;
1067
1067
  }>;
1068
+ /**
1069
+ * Clone a record — read the source, drop engine-owned columns, and
1070
+ * insert a fresh copy. Gated by the object's `enable.clone` capability
1071
+ * (default `true`; only an explicit `enable.clone === false` disables it).
1072
+ *
1073
+ * Shallow by design: it duplicates the record's own scalar/business field
1074
+ * values, not its related child records. The insert path re-stamps audit
1075
+ * columns, regenerates `autonumber` fields, and recomputes derived
1076
+ * (`formula`/`summary`) fields, so the copy is a valid new row rather than
1077
+ * a byte-identical twin. Caller-supplied `overrides` are applied last and
1078
+ * win over the copied values — the natural place to set a new `name`,
1079
+ * clear a unique field, or reset status before insert.
1080
+ */
1081
+ cloneData(request: {
1082
+ object: string;
1083
+ id: string;
1084
+ overrides?: Record<string, any>;
1085
+ context?: any;
1086
+ }): Promise<{
1087
+ object: string;
1088
+ id: any;
1089
+ sourceId: string;
1090
+ record: any;
1091
+ }>;
1068
1092
  updateData(request: {
1069
1093
  object: string;
1070
1094
  id: string;
@@ -1999,6 +2023,23 @@ interface OperationContext {
1999
2023
  context?: ExecutionContext;
2000
2024
  result?: any;
2001
2025
  }
2026
+ /**
2027
+ * Trailing options for the READ methods (find / findOne / count / aggregate).
2028
+ *
2029
+ * Historically the read methods took their execution context INSIDE the query
2030
+ * (`query.context`), while the WRITE methods (insert / update) took it in a
2031
+ * trailing `options.context`. That split was a footgun: the same `{ context }`
2032
+ * object is correct as the 3rd arg to `insert` but was SILENTLY DROPPED as the
2033
+ * 3rd arg to `find` — a class of bugs where an intended `isSystem` bypass just
2034
+ * vanished (e.g. control-plane reads coming back empty once org-scoping hooks
2035
+ * were added). We now ALSO accept `context` via this trailing options arg on the
2036
+ * read methods, so "execution context goes in the trailing options argument" is
2037
+ * one rule across reads and writes. `query.context` remains supported; when both
2038
+ * are given, `options.context` wins (it is the explicit channel).
2039
+ */
2040
+ interface EngineReadOptions {
2041
+ context?: ExecutionContext;
2042
+ }
2002
2043
  /**
2003
2044
  * Engine Middleware (Onion model)
2004
2045
  */
@@ -2418,8 +2459,8 @@ declare class ObjectQL implements IDataEngine {
2418
2459
  * @returns Records with expanded lookup fields (IDs replaced by full objects)
2419
2460
  */
2420
2461
  private expandRelatedRecords;
2421
- find(object: string, query?: EngineQueryOptions): Promise<any[]>;
2422
- findOne(objectName: string, query?: EngineQueryOptions): Promise<any>;
2462
+ find(object: string, query?: EngineQueryOptions, options?: EngineReadOptions): Promise<any[]>;
2463
+ findOne(objectName: string, query?: EngineQueryOptions, options?: EngineReadOptions): Promise<any>;
2423
2464
  insert(object: string, data: any | any[], options?: DataEngineInsertOptions): Promise<any>;
2424
2465
  update(object: string, data: any, options?: EngineUpdateOptions): Promise<any>;
2425
2466
  /**
@@ -2436,8 +2477,8 @@ declare class ObjectQL implements IDataEngine {
2436
2477
  */
2437
2478
  private cascadeDeleteRelations;
2438
2479
  delete(object: string, options?: EngineDeleteOptions): Promise<any>;
2439
- count(object: string, query?: EngineCountOptions): Promise<number>;
2440
- aggregate(object: string, query: EngineAggregateOptions): Promise<any[]>;
2480
+ count(object: string, query?: EngineCountOptions, options?: EngineReadOptions): Promise<number>;
2481
+ aggregate(object: string, query: EngineAggregateOptions, options?: EngineReadOptions): Promise<any[]>;
2441
2482
  /**
2442
2483
  * Run raw driver-specific commands (SQL for SqlDriver, REST for RestDriver, …).
2443
2484
  *
package/dist/index.d.ts CHANGED
@@ -1065,6 +1065,30 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
1065
1065
  id: any;
1066
1066
  record: any;
1067
1067
  }>;
1068
+ /**
1069
+ * Clone a record — read the source, drop engine-owned columns, and
1070
+ * insert a fresh copy. Gated by the object's `enable.clone` capability
1071
+ * (default `true`; only an explicit `enable.clone === false` disables it).
1072
+ *
1073
+ * Shallow by design: it duplicates the record's own scalar/business field
1074
+ * values, not its related child records. The insert path re-stamps audit
1075
+ * columns, regenerates `autonumber` fields, and recomputes derived
1076
+ * (`formula`/`summary`) fields, so the copy is a valid new row rather than
1077
+ * a byte-identical twin. Caller-supplied `overrides` are applied last and
1078
+ * win over the copied values — the natural place to set a new `name`,
1079
+ * clear a unique field, or reset status before insert.
1080
+ */
1081
+ cloneData(request: {
1082
+ object: string;
1083
+ id: string;
1084
+ overrides?: Record<string, any>;
1085
+ context?: any;
1086
+ }): Promise<{
1087
+ object: string;
1088
+ id: any;
1089
+ sourceId: string;
1090
+ record: any;
1091
+ }>;
1068
1092
  updateData(request: {
1069
1093
  object: string;
1070
1094
  id: string;
@@ -1999,6 +2023,23 @@ interface OperationContext {
1999
2023
  context?: ExecutionContext;
2000
2024
  result?: any;
2001
2025
  }
2026
+ /**
2027
+ * Trailing options for the READ methods (find / findOne / count / aggregate).
2028
+ *
2029
+ * Historically the read methods took their execution context INSIDE the query
2030
+ * (`query.context`), while the WRITE methods (insert / update) took it in a
2031
+ * trailing `options.context`. That split was a footgun: the same `{ context }`
2032
+ * object is correct as the 3rd arg to `insert` but was SILENTLY DROPPED as the
2033
+ * 3rd arg to `find` — a class of bugs where an intended `isSystem` bypass just
2034
+ * vanished (e.g. control-plane reads coming back empty once org-scoping hooks
2035
+ * were added). We now ALSO accept `context` via this trailing options arg on the
2036
+ * read methods, so "execution context goes in the trailing options argument" is
2037
+ * one rule across reads and writes. `query.context` remains supported; when both
2038
+ * are given, `options.context` wins (it is the explicit channel).
2039
+ */
2040
+ interface EngineReadOptions {
2041
+ context?: ExecutionContext;
2042
+ }
2002
2043
  /**
2003
2044
  * Engine Middleware (Onion model)
2004
2045
  */
@@ -2418,8 +2459,8 @@ declare class ObjectQL implements IDataEngine {
2418
2459
  * @returns Records with expanded lookup fields (IDs replaced by full objects)
2419
2460
  */
2420
2461
  private expandRelatedRecords;
2421
- find(object: string, query?: EngineQueryOptions): Promise<any[]>;
2422
- findOne(objectName: string, query?: EngineQueryOptions): Promise<any>;
2462
+ find(object: string, query?: EngineQueryOptions, options?: EngineReadOptions): Promise<any[]>;
2463
+ findOne(objectName: string, query?: EngineQueryOptions, options?: EngineReadOptions): Promise<any>;
2423
2464
  insert(object: string, data: any | any[], options?: DataEngineInsertOptions): Promise<any>;
2424
2465
  update(object: string, data: any, options?: EngineUpdateOptions): Promise<any>;
2425
2466
  /**
@@ -2436,8 +2477,8 @@ declare class ObjectQL implements IDataEngine {
2436
2477
  */
2437
2478
  private cascadeDeleteRelations;
2438
2479
  delete(object: string, options?: EngineDeleteOptions): Promise<any>;
2439
- count(object: string, query?: EngineCountOptions): Promise<number>;
2440
- aggregate(object: string, query: EngineAggregateOptions): Promise<any[]>;
2480
+ count(object: string, query?: EngineCountOptions, options?: EngineReadOptions): Promise<number>;
2481
+ aggregate(object: string, query: EngineAggregateOptions, options?: EngineReadOptions): Promise<any[]>;
2441
2482
  /**
2442
2483
  * Run raw driver-specific commands (SQL for SqlDriver, REST for RestDriver, …).
2443
2484
  *
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 } };
@@ -7281,6 +7350,11 @@ function applyFormulaPlan(plan, records) {
7281
7350
  }
7282
7351
  }
7283
7352
  }
7353
+ function mergeReadContext(fromQuery, fromOptions) {
7354
+ if (fromOptions == null) return fromQuery;
7355
+ if (fromQuery == null) return fromOptions;
7356
+ return { ...fromQuery, ...fromOptions };
7357
+ }
7284
7358
  function resolveMetadataItemName(key, item) {
7285
7359
  if (!item) return void 0;
7286
7360
  if (item.name) return item.name;
@@ -8594,7 +8668,7 @@ var _ObjectQL = class _ObjectQL {
8594
8668
  // ============================================
8595
8669
  // Data Access Methods (IDataEngine Interface)
8596
8670
  // ============================================
8597
- async find(object, query) {
8671
+ async find(object, query, options) {
8598
8672
  object = this.resolveObjectName(object);
8599
8673
  this.logger.debug("Find operation starting", { object, query });
8600
8674
  const driver = this.getDriver(object);
@@ -8623,7 +8697,7 @@ var _ObjectQL = class _ObjectQL {
8623
8697
  operation: "find",
8624
8698
  ast,
8625
8699
  options: query,
8626
- context: query?.context
8700
+ context: mergeReadContext(query?.context, options?.context)
8627
8701
  };
8628
8702
  await this.executeWithMiddleware(opCtx, async () => {
8629
8703
  const hookContext = {
@@ -8655,7 +8729,7 @@ var _ObjectQL = class _ObjectQL {
8655
8729
  });
8656
8730
  return opCtx.result;
8657
8731
  }
8658
- async findOne(objectName, query) {
8732
+ async findOne(objectName, query, options) {
8659
8733
  objectName = this.resolveObjectName(objectName);
8660
8734
  this.logger.debug("FindOne operation", { objectName });
8661
8735
  const driver = this.getDriver(objectName);
@@ -8678,7 +8752,7 @@ var _ObjectQL = class _ObjectQL {
8678
8752
  operation: "findOne",
8679
8753
  ast,
8680
8754
  options: query,
8681
- context: query?.context
8755
+ context: mergeReadContext(query?.context, options?.context)
8682
8756
  };
8683
8757
  await this.executeWithMiddleware(opCtx, async () => {
8684
8758
  const findOneOpts = this.buildDriverOptions(opCtx.context);
@@ -9024,14 +9098,14 @@ var _ObjectQL = class _ObjectQL {
9024
9098
  });
9025
9099
  return opCtx.result;
9026
9100
  }
9027
- async count(object, query) {
9101
+ async count(object, query, options) {
9028
9102
  object = this.resolveObjectName(object);
9029
9103
  const driver = this.getDriver(object);
9030
9104
  const opCtx = {
9031
9105
  object,
9032
9106
  operation: "count",
9033
9107
  options: query,
9034
- context: query?.context
9108
+ context: mergeReadContext(query?.context, options?.context)
9035
9109
  };
9036
9110
  await this.executeWithMiddleware(opCtx, async () => {
9037
9111
  const countOpts = this.buildDriverOptions(opCtx.context);
@@ -9044,7 +9118,7 @@ var _ObjectQL = class _ObjectQL {
9044
9118
  });
9045
9119
  return opCtx.result;
9046
9120
  }
9047
- async aggregate(object, query) {
9121
+ async aggregate(object, query, options) {
9048
9122
  object = this.resolveObjectName(object);
9049
9123
  const driver = this.getDriver(object);
9050
9124
  this.logger.debug(`Aggregate on ${object} using ${driver.name}`, query);
@@ -9052,7 +9126,7 @@ var _ObjectQL = class _ObjectQL {
9052
9126
  object,
9053
9127
  operation: "aggregate",
9054
9128
  options: query,
9055
- context: query?.context
9129
+ context: mergeReadContext(query?.context, options?.context)
9056
9130
  };
9057
9131
  await this.executeWithMiddleware(opCtx, async () => {
9058
9132
  const ast = {