@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 CHANGED
@@ -636,6 +636,8 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
636
636
  variant?: "link" | "primary" | "secondary" | "danger" | "ghost" | undefined;
637
637
  confirmText?: string | undefined;
638
638
  successMessage?: string | undefined;
639
+ errorMessage?: string | undefined;
640
+ undoable?: boolean | undefined;
639
641
  resultDialog?: {
640
642
  title?: string | undefined;
641
643
  description?: string | undefined;
@@ -1065,6 +1067,30 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
1065
1067
  id: any;
1066
1068
  record: any;
1067
1069
  }>;
1070
+ /**
1071
+ * Clone a record — read the source, drop engine-owned columns, and
1072
+ * insert a fresh copy. Gated by the object's `enable.clone` capability
1073
+ * (default `true`; only an explicit `enable.clone === false` disables it).
1074
+ *
1075
+ * Shallow by design: it duplicates the record's own scalar/business field
1076
+ * values, not its related child records. The insert path re-stamps audit
1077
+ * columns, regenerates `autonumber` fields, and recomputes derived
1078
+ * (`formula`/`summary`) fields, so the copy is a valid new row rather than
1079
+ * a byte-identical twin. Caller-supplied `overrides` are applied last and
1080
+ * win over the copied values — the natural place to set a new `name`,
1081
+ * clear a unique field, or reset status before insert.
1082
+ */
1083
+ cloneData(request: {
1084
+ object: string;
1085
+ id: string;
1086
+ overrides?: Record<string, any>;
1087
+ context?: any;
1088
+ }): Promise<{
1089
+ object: string;
1090
+ id: any;
1091
+ sourceId: string;
1092
+ record: any;
1093
+ }>;
1068
1094
  updateData(request: {
1069
1095
  object: string;
1070
1096
  id: string;
@@ -1137,49 +1163,6 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
1137
1163
  totalHits: number;
1138
1164
  truncated: boolean;
1139
1165
  }>;
1140
- /**
1141
- * Convert a qualified Lead into an Account + Contact (+ optional
1142
- * Opportunity) and mark the Lead as converted. Mirrors the Salesforce
1143
- * lead-conversion model:
1144
- *
1145
- * - If `accountId` is provided, the lead's company info is NOT used
1146
- * to create a new account; the new contact and opportunity link to
1147
- * the existing account instead.
1148
- * - If `contactId` is provided, no new contact is created either —
1149
- * useful when the lead is a new contact at an existing account.
1150
- * - `createOpportunity` defaults to true; pass `false` to convert
1151
- * without producing an opportunity (some teams convert "logos
1152
- * only" first).
1153
- * - Lead is updated atomically: `is_converted=true`,
1154
- * `converted_account`/`converted_contact`/`converted_opportunity`
1155
- * pointers, `converted_date`, and `status='converted'`.
1156
- *
1157
- * Atomicity is enforced via the default driver's transaction support
1158
- * when available; otherwise a best-effort compensation (delete
1159
- * already-created child records on failure) is attempted. Permission
1160
- * checks on each child object are inherited from the caller's
1161
- * execution context so SecurityPlugin still gates account/contact/
1162
- * opportunity creates.
1163
- */
1164
- convertLead(request: {
1165
- leadId: string;
1166
- accountId?: string;
1167
- contactId?: string;
1168
- createOpportunity?: boolean;
1169
- opportunity?: {
1170
- name?: string;
1171
- amount?: number;
1172
- close_date?: string;
1173
- stage?: string;
1174
- };
1175
- convertedStatus?: string;
1176
- context?: any;
1177
- }): Promise<{
1178
- lead: any;
1179
- account: any;
1180
- contact: any;
1181
- opportunity: any | null;
1182
- }>;
1183
1166
  getMetaItemCached(request: {
1184
1167
  type: string;
1185
1168
  name: string;
@@ -1999,6 +1982,23 @@ interface OperationContext {
1999
1982
  context?: ExecutionContext;
2000
1983
  result?: any;
2001
1984
  }
1985
+ /**
1986
+ * Trailing options for the READ methods (find / findOne / count / aggregate).
1987
+ *
1988
+ * Historically the read methods took their execution context INSIDE the query
1989
+ * (`query.context`), while the WRITE methods (insert / update) took it in a
1990
+ * trailing `options.context`. That split was a footgun: the same `{ context }`
1991
+ * object is correct as the 3rd arg to `insert` but was SILENTLY DROPPED as the
1992
+ * 3rd arg to `find` — a class of bugs where an intended `isSystem` bypass just
1993
+ * vanished (e.g. control-plane reads coming back empty once org-scoping hooks
1994
+ * were added). We now ALSO accept `context` via this trailing options arg on the
1995
+ * read methods, so "execution context goes in the trailing options argument" is
1996
+ * one rule across reads and writes. `query.context` remains supported; when both
1997
+ * are given, `options.context` wins (it is the explicit channel).
1998
+ */
1999
+ interface EngineReadOptions {
2000
+ context?: ExecutionContext;
2001
+ }
2002
2002
  /**
2003
2003
  * Engine Middleware (Onion model)
2004
2004
  */
@@ -2418,8 +2418,8 @@ declare class ObjectQL implements IDataEngine {
2418
2418
  * @returns Records with expanded lookup fields (IDs replaced by full objects)
2419
2419
  */
2420
2420
  private expandRelatedRecords;
2421
- find(object: string, query?: EngineQueryOptions): Promise<any[]>;
2422
- findOne(objectName: string, query?: EngineQueryOptions): Promise<any>;
2421
+ find(object: string, query?: EngineQueryOptions, options?: EngineReadOptions): Promise<any[]>;
2422
+ findOne(objectName: string, query?: EngineQueryOptions, options?: EngineReadOptions): Promise<any>;
2423
2423
  insert(object: string, data: any | any[], options?: DataEngineInsertOptions): Promise<any>;
2424
2424
  update(object: string, data: any, options?: EngineUpdateOptions): Promise<any>;
2425
2425
  /**
@@ -2431,13 +2431,16 @@ declare class ObjectQL implements IDataEngine {
2431
2431
  * - `set_null` → clear the foreign key,
2432
2432
  * - `restrict` → refuse the delete when dependents exist.
2433
2433
  * `master_detail` defaults to `cascade` (the parent owns the child
2434
- * lifecycle); `lookup` defaults to `set_null`. Only runs for single-id
2434
+ * lifecycle); `lookup` defaults to `set_null` except a `set_null` default
2435
+ * on a REQUIRED lookup escalates to `restrict` (you can't null a NOT NULL
2436
+ * FK; restricting with a clear dependent-count message beats a misleading
2437
+ * "<field> is required" 400 from the child). Only runs for single-id
2435
2438
  * deletes — multi/predicate deletes skip cascade (logged).
2436
2439
  */
2437
2440
  private cascadeDeleteRelations;
2438
2441
  delete(object: string, options?: EngineDeleteOptions): Promise<any>;
2439
- count(object: string, query?: EngineCountOptions): Promise<number>;
2440
- aggregate(object: string, query: EngineAggregateOptions): Promise<any[]>;
2442
+ count(object: string, query?: EngineCountOptions, options?: EngineReadOptions): Promise<number>;
2443
+ aggregate(object: string, query: EngineAggregateOptions, options?: EngineReadOptions): Promise<any[]>;
2441
2444
  /**
2442
2445
  * Run raw driver-specific commands (SQL for SqlDriver, REST for RestDriver, …).
2443
2446
  *
@@ -2674,13 +2677,25 @@ declare class ScopedContext {
2674
2677
  /**
2675
2678
  * Group + aggregate raw rows according to the AST's `groupBy` /
2676
2679
  * `aggregations`. When neither is present, returns the rows unchanged.
2680
+ *
2681
+ * `timezone` (ADR-0053 Phase 2) shifts date bucketing to a reference timezone
2682
+ * so a row near a tz day-boundary lands in the right day/week/month/quarter.
2683
+ * It is only consulted by `groupBy` items carrying a `dateGranularity`; an
2684
+ * unset or `'UTC'` value keeps the historical UTC bucketing.
2677
2685
  */
2678
- declare function applyInMemoryAggregation(rows: any[], ast: Pick<QueryAST, 'groupBy' | 'aggregations'>): any[];
2686
+ declare function applyInMemoryAggregation(rows: any[], ast: Pick<QueryAST, 'groupBy' | 'aggregations'>, timezone?: string): any[];
2679
2687
  /**
2680
2688
  * Bucket a date-like value into an ISO-formatted period label. Weeks start
2681
2689
  * Monday and use ISO week numbering.
2690
+ *
2691
+ * `timezone` (ADR-0053 Phase 2) resolves the calendar day in a reference zone
2692
+ * so an instant near a tz day-boundary buckets where a user in that zone would
2693
+ * expect. An unset / `'UTC'` / invalid zone keeps the historical UTC bucketing.
2694
+ * The y/m/d are taken in the reference zone and the ISO-week math then runs on
2695
+ * a UTC date built from those parts — the parts already carry the zone shift,
2696
+ * so the week boundary lands correctly without re-applying any offset.
2682
2697
  */
2683
- declare function bucketDateValue(value: unknown, granularity: DateGranularityValue): string;
2698
+ declare function bucketDateValue(value: unknown, granularity: DateGranularityValue, timezone?: string): string;
2684
2699
 
2685
2700
  /**
2686
2701
  * Hook Execution Metrics
@@ -2876,7 +2891,7 @@ declare function wrapDeclarativeHook(meta: Hook, handler: HookHandler, opts?: Wr
2876
2891
 
2877
2892
  interface FieldValidationError {
2878
2893
  field: string;
2879
- code: 'required' | 'min_length' | 'max_length' | 'min_value' | 'max_value' | 'invalid_email' | 'invalid_url' | 'invalid_phone' | 'invalid_number' | 'invalid_boolean' | 'invalid_date' | 'invalid_option' | 'invalid_transition' | 'rule_violation' | 'invalid_format' | 'invalid_json' | 'json_schema_violation';
2894
+ code: 'required' | 'min_length' | 'max_length' | 'min_value' | 'max_value' | 'invalid_email' | 'invalid_url' | 'invalid_phone' | 'invalid_number' | 'invalid_boolean' | 'invalid_date' | 'invalid_time' | 'invalid_option' | 'invalid_transition' | 'rule_violation' | 'invalid_format' | 'invalid_json' | 'json_schema_violation';
2880
2895
  message: string;
2881
2896
  /** Allowed values for select/multiselect, when applicable. */
2882
2897
  options?: string[];
package/dist/index.d.ts CHANGED
@@ -636,6 +636,8 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
636
636
  variant?: "link" | "primary" | "secondary" | "danger" | "ghost" | undefined;
637
637
  confirmText?: string | undefined;
638
638
  successMessage?: string | undefined;
639
+ errorMessage?: string | undefined;
640
+ undoable?: boolean | undefined;
639
641
  resultDialog?: {
640
642
  title?: string | undefined;
641
643
  description?: string | undefined;
@@ -1065,6 +1067,30 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
1065
1067
  id: any;
1066
1068
  record: any;
1067
1069
  }>;
1070
+ /**
1071
+ * Clone a record — read the source, drop engine-owned columns, and
1072
+ * insert a fresh copy. Gated by the object's `enable.clone` capability
1073
+ * (default `true`; only an explicit `enable.clone === false` disables it).
1074
+ *
1075
+ * Shallow by design: it duplicates the record's own scalar/business field
1076
+ * values, not its related child records. The insert path re-stamps audit
1077
+ * columns, regenerates `autonumber` fields, and recomputes derived
1078
+ * (`formula`/`summary`) fields, so the copy is a valid new row rather than
1079
+ * a byte-identical twin. Caller-supplied `overrides` are applied last and
1080
+ * win over the copied values — the natural place to set a new `name`,
1081
+ * clear a unique field, or reset status before insert.
1082
+ */
1083
+ cloneData(request: {
1084
+ object: string;
1085
+ id: string;
1086
+ overrides?: Record<string, any>;
1087
+ context?: any;
1088
+ }): Promise<{
1089
+ object: string;
1090
+ id: any;
1091
+ sourceId: string;
1092
+ record: any;
1093
+ }>;
1068
1094
  updateData(request: {
1069
1095
  object: string;
1070
1096
  id: string;
@@ -1137,49 +1163,6 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
1137
1163
  totalHits: number;
1138
1164
  truncated: boolean;
1139
1165
  }>;
1140
- /**
1141
- * Convert a qualified Lead into an Account + Contact (+ optional
1142
- * Opportunity) and mark the Lead as converted. Mirrors the Salesforce
1143
- * lead-conversion model:
1144
- *
1145
- * - If `accountId` is provided, the lead's company info is NOT used
1146
- * to create a new account; the new contact and opportunity link to
1147
- * the existing account instead.
1148
- * - If `contactId` is provided, no new contact is created either —
1149
- * useful when the lead is a new contact at an existing account.
1150
- * - `createOpportunity` defaults to true; pass `false` to convert
1151
- * without producing an opportunity (some teams convert "logos
1152
- * only" first).
1153
- * - Lead is updated atomically: `is_converted=true`,
1154
- * `converted_account`/`converted_contact`/`converted_opportunity`
1155
- * pointers, `converted_date`, and `status='converted'`.
1156
- *
1157
- * Atomicity is enforced via the default driver's transaction support
1158
- * when available; otherwise a best-effort compensation (delete
1159
- * already-created child records on failure) is attempted. Permission
1160
- * checks on each child object are inherited from the caller's
1161
- * execution context so SecurityPlugin still gates account/contact/
1162
- * opportunity creates.
1163
- */
1164
- convertLead(request: {
1165
- leadId: string;
1166
- accountId?: string;
1167
- contactId?: string;
1168
- createOpportunity?: boolean;
1169
- opportunity?: {
1170
- name?: string;
1171
- amount?: number;
1172
- close_date?: string;
1173
- stage?: string;
1174
- };
1175
- convertedStatus?: string;
1176
- context?: any;
1177
- }): Promise<{
1178
- lead: any;
1179
- account: any;
1180
- contact: any;
1181
- opportunity: any | null;
1182
- }>;
1183
1166
  getMetaItemCached(request: {
1184
1167
  type: string;
1185
1168
  name: string;
@@ -1999,6 +1982,23 @@ interface OperationContext {
1999
1982
  context?: ExecutionContext;
2000
1983
  result?: any;
2001
1984
  }
1985
+ /**
1986
+ * Trailing options for the READ methods (find / findOne / count / aggregate).
1987
+ *
1988
+ * Historically the read methods took their execution context INSIDE the query
1989
+ * (`query.context`), while the WRITE methods (insert / update) took it in a
1990
+ * trailing `options.context`. That split was a footgun: the same `{ context }`
1991
+ * object is correct as the 3rd arg to `insert` but was SILENTLY DROPPED as the
1992
+ * 3rd arg to `find` — a class of bugs where an intended `isSystem` bypass just
1993
+ * vanished (e.g. control-plane reads coming back empty once org-scoping hooks
1994
+ * were added). We now ALSO accept `context` via this trailing options arg on the
1995
+ * read methods, so "execution context goes in the trailing options argument" is
1996
+ * one rule across reads and writes. `query.context` remains supported; when both
1997
+ * are given, `options.context` wins (it is the explicit channel).
1998
+ */
1999
+ interface EngineReadOptions {
2000
+ context?: ExecutionContext;
2001
+ }
2002
2002
  /**
2003
2003
  * Engine Middleware (Onion model)
2004
2004
  */
@@ -2418,8 +2418,8 @@ declare class ObjectQL implements IDataEngine {
2418
2418
  * @returns Records with expanded lookup fields (IDs replaced by full objects)
2419
2419
  */
2420
2420
  private expandRelatedRecords;
2421
- find(object: string, query?: EngineQueryOptions): Promise<any[]>;
2422
- findOne(objectName: string, query?: EngineQueryOptions): Promise<any>;
2421
+ find(object: string, query?: EngineQueryOptions, options?: EngineReadOptions): Promise<any[]>;
2422
+ findOne(objectName: string, query?: EngineQueryOptions, options?: EngineReadOptions): Promise<any>;
2423
2423
  insert(object: string, data: any | any[], options?: DataEngineInsertOptions): Promise<any>;
2424
2424
  update(object: string, data: any, options?: EngineUpdateOptions): Promise<any>;
2425
2425
  /**
@@ -2431,13 +2431,16 @@ declare class ObjectQL implements IDataEngine {
2431
2431
  * - `set_null` → clear the foreign key,
2432
2432
  * - `restrict` → refuse the delete when dependents exist.
2433
2433
  * `master_detail` defaults to `cascade` (the parent owns the child
2434
- * lifecycle); `lookup` defaults to `set_null`. Only runs for single-id
2434
+ * lifecycle); `lookup` defaults to `set_null` except a `set_null` default
2435
+ * on a REQUIRED lookup escalates to `restrict` (you can't null a NOT NULL
2436
+ * FK; restricting with a clear dependent-count message beats a misleading
2437
+ * "<field> is required" 400 from the child). Only runs for single-id
2435
2438
  * deletes — multi/predicate deletes skip cascade (logged).
2436
2439
  */
2437
2440
  private cascadeDeleteRelations;
2438
2441
  delete(object: string, options?: EngineDeleteOptions): Promise<any>;
2439
- count(object: string, query?: EngineCountOptions): Promise<number>;
2440
- aggregate(object: string, query: EngineAggregateOptions): Promise<any[]>;
2442
+ count(object: string, query?: EngineCountOptions, options?: EngineReadOptions): Promise<number>;
2443
+ aggregate(object: string, query: EngineAggregateOptions, options?: EngineReadOptions): Promise<any[]>;
2441
2444
  /**
2442
2445
  * Run raw driver-specific commands (SQL for SqlDriver, REST for RestDriver, …).
2443
2446
  *
@@ -2674,13 +2677,25 @@ declare class ScopedContext {
2674
2677
  /**
2675
2678
  * Group + aggregate raw rows according to the AST's `groupBy` /
2676
2679
  * `aggregations`. When neither is present, returns the rows unchanged.
2680
+ *
2681
+ * `timezone` (ADR-0053 Phase 2) shifts date bucketing to a reference timezone
2682
+ * so a row near a tz day-boundary lands in the right day/week/month/quarter.
2683
+ * It is only consulted by `groupBy` items carrying a `dateGranularity`; an
2684
+ * unset or `'UTC'` value keeps the historical UTC bucketing.
2677
2685
  */
2678
- declare function applyInMemoryAggregation(rows: any[], ast: Pick<QueryAST, 'groupBy' | 'aggregations'>): any[];
2686
+ declare function applyInMemoryAggregation(rows: any[], ast: Pick<QueryAST, 'groupBy' | 'aggregations'>, timezone?: string): any[];
2679
2687
  /**
2680
2688
  * Bucket a date-like value into an ISO-formatted period label. Weeks start
2681
2689
  * Monday and use ISO week numbering.
2690
+ *
2691
+ * `timezone` (ADR-0053 Phase 2) resolves the calendar day in a reference zone
2692
+ * so an instant near a tz day-boundary buckets where a user in that zone would
2693
+ * expect. An unset / `'UTC'` / invalid zone keeps the historical UTC bucketing.
2694
+ * The y/m/d are taken in the reference zone and the ISO-week math then runs on
2695
+ * a UTC date built from those parts — the parts already carry the zone shift,
2696
+ * so the week boundary lands correctly without re-applying any offset.
2682
2697
  */
2683
- declare function bucketDateValue(value: unknown, granularity: DateGranularityValue): string;
2698
+ declare function bucketDateValue(value: unknown, granularity: DateGranularityValue, timezone?: string): string;
2684
2699
 
2685
2700
  /**
2686
2701
  * Hook Execution Metrics
@@ -2876,7 +2891,7 @@ declare function wrapDeclarativeHook(meta: Hook, handler: HookHandler, opts?: Wr
2876
2891
 
2877
2892
  interface FieldValidationError {
2878
2893
  field: string;
2879
- code: 'required' | 'min_length' | 'max_length' | 'min_value' | 'max_value' | 'invalid_email' | 'invalid_url' | 'invalid_phone' | 'invalid_number' | 'invalid_boolean' | 'invalid_date' | 'invalid_option' | 'invalid_transition' | 'rule_violation' | 'invalid_format' | 'invalid_json' | 'json_schema_violation';
2894
+ code: 'required' | 'min_length' | 'max_length' | 'min_value' | 'max_value' | 'invalid_email' | 'invalid_url' | 'invalid_phone' | 'invalid_number' | 'invalid_boolean' | 'invalid_date' | 'invalid_time' | 'invalid_option' | 'invalid_transition' | 'rule_violation' | 'invalid_format' | 'invalid_json' | 'json_schema_violation';
2880
2895
  message: string;
2881
2896
  /** Allowed values for select/multiselect, when applicable. */
2882
2897
  options?: string[];