hierarchical-approval 4.0.7 → 4.1.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/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [4.1.0] - 2026-10-12
9
+
10
+ ### Changed
11
+
12
+ - `SubmitOptions` now describes what `submit` accepts: `data` and `metadata` are
13
+ optional (they default to `{}`) and `expiresAt` may be a `Date`, ISO string or
14
+ epoch milliseconds.
15
+
8
16
  ## [4.0.7] - 2026-09-28
9
17
 
10
18
  ### Fixed
@@ -113,7 +113,14 @@ declare const UpdateDataOptionsSchema: z.ZodObject<{
113
113
  reason: z.ZodOptional<z.ZodString>;
114
114
  recomputeChain: z.ZodDefault<z.ZodBoolean>;
115
115
  }, z.core.$strip>;
116
- type SubmitOptions = z.infer<typeof SubmitOptionsSchema>;
116
+ /**
117
+ * Input shape: `data` and `metadata` are optional because the schema defaults
118
+ * them. `expiresAt` is re-declared since zod types a coerced date's input as
119
+ * `unknown`, which would let any value through at compile time.
120
+ */
121
+ type SubmitOptions = Omit<z.input<typeof SubmitOptionsSchema>, 'expiresAt'> & {
122
+ expiresAt?: Date | string | number;
123
+ };
117
124
  type ApproveOptions = z.infer<typeof ApproveOptionsSchema>;
118
125
  type RejectOptions = z.infer<typeof RejectOptionsSchema>;
119
126
  type DelegateOptions = z.infer<typeof DelegateOptionsSchema>;
@@ -723,6 +730,17 @@ declare class ApprovalEngine {
723
730
  * @throws {@link ApprovalTemplateNotFoundError} if `raw.templateName` is not defined.
724
731
  * @throws {@link ApprovalValidationError} if `raw` fails schema validation, or if
725
732
  * condition evaluation leaves no levels (or a duplicate level number) to open.
733
+ *
734
+ * @example
735
+ * ```ts
736
+ * const instance = await engine.submit({
737
+ * templateName: 'purchase-order',
738
+ * documentId: 'PO-1042',
739
+ * documentType: 'purchase_order',
740
+ * submittedBy: 'alice',
741
+ * data: { amount: 12_500 },
742
+ * });
743
+ * ```
726
744
  */
727
745
  submit(raw: SubmitOptions, auditCtx?: AuditContext,
728
746
  /**
@@ -752,6 +770,14 @@ declare class ApprovalEngine {
752
770
  * configured {@link IAuthorizationPolicy}.
753
771
  * @throws {@link ApprovalValidationError} if `opts.approverId` sits on more than one
754
772
  * open parallel level and `opts.level` was not supplied to disambiguate.
773
+ *
774
+ * @example
775
+ * ```ts
776
+ * const updated = await engine.approve(instance.id, {
777
+ * approverId: 'bob',
778
+ * comment: 'Within budget',
779
+ * });
780
+ * ```
755
781
  */
756
782
  approve(instanceId: string, raw: ApproveOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
757
783
  /**
@@ -774,6 +800,15 @@ declare class ApprovalEngine {
774
800
  * @throws {@link ApprovalValidationError} if `opts.approverId` sits on more than one
775
801
  * open parallel level with `opts.level` unset, or if `opts.returnTo === 'previous'`
776
802
  * but the failing level is already the first level in the chain.
803
+ *
804
+ * @example
805
+ * ```ts
806
+ * const updated = await engine.reject(instance.id, {
807
+ * approverId: 'bob',
808
+ * reason: 'Over budget',
809
+ * returnTo: 'previous',
810
+ * });
811
+ * ```
777
812
  */
778
813
  reject(instanceId: string, raw: RejectOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
779
814
  /**
@@ -792,6 +827,16 @@ declare class ApprovalEngine {
792
827
  * configured {@link IAuthorizationPolicy}.
793
828
  * @throws {@link ApprovalValidationError} if `opts.fromApprover` sits on more than one
794
829
  * open parallel level and `opts.level` was not supplied to disambiguate.
830
+ *
831
+ * @example
832
+ * ```ts
833
+ * await engine.delegate(instance.id, {
834
+ * fromApprover: 'bob',
835
+ * toApprover: 'carol',
836
+ * reason: 'On leave',
837
+ * until: new Date('2026-11-01'),
838
+ * });
839
+ * ```
795
840
  */
796
841
  delegate(instanceId: string, raw: DelegateOptions, auditCtx?: AuditContext): Promise<void>;
797
842
  /**
@@ -1003,11 +1048,16 @@ declare class ApprovalEngine {
1003
1048
  * theirs away), or they already voted on it.
1004
1049
  *
1005
1050
  * @returns `{ eligible: true }`, or `{ eligible: false, reason }`.
1051
+ *
1052
+ * @example
1053
+ * ```ts
1054
+ * const check = await engine.canApprove(instance.id, 'bob');
1055
+ * if (!check.eligible) console.log(check.reason);
1056
+ * ```
1006
1057
  */
1007
1058
  canApprove(instanceId: string, userId: string): Promise<CanApproveResult>;
1008
1059
  /** Emergency bypass — completes the instance as 'approved', skipping remaining levels. Requires template.allowOverride = true. */
1009
1060
  override(instanceId: string, raw: OverrideOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
1010
- /** Approve multiple instances in one call. Never throws — failures collected in result.failed. */
1011
1061
  /**
1012
1062
  * Move every pending approval assigned to one person over to another.
1013
1063
  *
@@ -1030,7 +1080,15 @@ declare class ApprovalEngine {
1030
1080
  * @returns What moved, what did not, and why. With `dryRun` nothing is written.
1031
1081
  */
1032
1082
  transferApprovals(raw: TransferApprovalsOptions, auditCtx?: AuditContext): Promise<TransferResult>;
1033
- /** Approve multiple instances in one call. Never throws — failures collected in result.failed. */
1083
+ /**
1084
+ * Approve multiple instances in one call. Never throws — failures collected in result.failed.
1085
+ *
1086
+ * @example
1087
+ * ```ts
1088
+ * const { succeeded, failed } = await engine.bulkApprove(ids, { approverId: 'bob' });
1089
+ * for (const { instanceId, error } of failed) console.warn(instanceId, error.code);
1090
+ * ```
1091
+ */
1034
1092
  bulkApprove(instanceIds: string[], raw: ApproveOptions, auditCtx?: AuditContext): Promise<BulkResult>;
1035
1093
  /** Reject multiple instances in one call. Never throws — failures collected in result.failed. */
1036
1094
  bulkReject(instanceIds: string[], raw: RejectOptions, auditCtx?: AuditContext): Promise<BulkResult>;
@@ -1040,7 +1098,14 @@ declare class ApprovalEngine {
1040
1098
  * @throws {@link ApprovalNotFoundError} if no instance exists with that id.
1041
1099
  */
1042
1100
  getInstance(instanceId: string): Promise<ApprovalInstance>;
1043
- /** Page through every instance with an open level assigned to one approver. */
1101
+ /**
1102
+ * Page through every instance with an open level assigned to one approver.
1103
+ *
1104
+ * @example
1105
+ * ```ts
1106
+ * const page = await engine.getPendingFor('bob', { limit: 20, offset: 0 });
1107
+ * ```
1108
+ */
1044
1109
  getPendingFor(approverId: string, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
1045
1110
  /** Page through instances matching an arbitrary filter (status, document type, date range, ...). */
1046
1111
  queryInstances(filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
@@ -113,7 +113,14 @@ declare const UpdateDataOptionsSchema: z.ZodObject<{
113
113
  reason: z.ZodOptional<z.ZodString>;
114
114
  recomputeChain: z.ZodDefault<z.ZodBoolean>;
115
115
  }, z.core.$strip>;
116
- type SubmitOptions = z.infer<typeof SubmitOptionsSchema>;
116
+ /**
117
+ * Input shape: `data` and `metadata` are optional because the schema defaults
118
+ * them. `expiresAt` is re-declared since zod types a coerced date's input as
119
+ * `unknown`, which would let any value through at compile time.
120
+ */
121
+ type SubmitOptions = Omit<z.input<typeof SubmitOptionsSchema>, 'expiresAt'> & {
122
+ expiresAt?: Date | string | number;
123
+ };
117
124
  type ApproveOptions = z.infer<typeof ApproveOptionsSchema>;
118
125
  type RejectOptions = z.infer<typeof RejectOptionsSchema>;
119
126
  type DelegateOptions = z.infer<typeof DelegateOptionsSchema>;
@@ -723,6 +730,17 @@ declare class ApprovalEngine {
723
730
  * @throws {@link ApprovalTemplateNotFoundError} if `raw.templateName` is not defined.
724
731
  * @throws {@link ApprovalValidationError} if `raw` fails schema validation, or if
725
732
  * condition evaluation leaves no levels (or a duplicate level number) to open.
733
+ *
734
+ * @example
735
+ * ```ts
736
+ * const instance = await engine.submit({
737
+ * templateName: 'purchase-order',
738
+ * documentId: 'PO-1042',
739
+ * documentType: 'purchase_order',
740
+ * submittedBy: 'alice',
741
+ * data: { amount: 12_500 },
742
+ * });
743
+ * ```
726
744
  */
727
745
  submit(raw: SubmitOptions, auditCtx?: AuditContext,
728
746
  /**
@@ -752,6 +770,14 @@ declare class ApprovalEngine {
752
770
  * configured {@link IAuthorizationPolicy}.
753
771
  * @throws {@link ApprovalValidationError} if `opts.approverId` sits on more than one
754
772
  * open parallel level and `opts.level` was not supplied to disambiguate.
773
+ *
774
+ * @example
775
+ * ```ts
776
+ * const updated = await engine.approve(instance.id, {
777
+ * approverId: 'bob',
778
+ * comment: 'Within budget',
779
+ * });
780
+ * ```
755
781
  */
756
782
  approve(instanceId: string, raw: ApproveOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
757
783
  /**
@@ -774,6 +800,15 @@ declare class ApprovalEngine {
774
800
  * @throws {@link ApprovalValidationError} if `opts.approverId` sits on more than one
775
801
  * open parallel level with `opts.level` unset, or if `opts.returnTo === 'previous'`
776
802
  * but the failing level is already the first level in the chain.
803
+ *
804
+ * @example
805
+ * ```ts
806
+ * const updated = await engine.reject(instance.id, {
807
+ * approverId: 'bob',
808
+ * reason: 'Over budget',
809
+ * returnTo: 'previous',
810
+ * });
811
+ * ```
777
812
  */
778
813
  reject(instanceId: string, raw: RejectOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
779
814
  /**
@@ -792,6 +827,16 @@ declare class ApprovalEngine {
792
827
  * configured {@link IAuthorizationPolicy}.
793
828
  * @throws {@link ApprovalValidationError} if `opts.fromApprover` sits on more than one
794
829
  * open parallel level and `opts.level` was not supplied to disambiguate.
830
+ *
831
+ * @example
832
+ * ```ts
833
+ * await engine.delegate(instance.id, {
834
+ * fromApprover: 'bob',
835
+ * toApprover: 'carol',
836
+ * reason: 'On leave',
837
+ * until: new Date('2026-11-01'),
838
+ * });
839
+ * ```
795
840
  */
796
841
  delegate(instanceId: string, raw: DelegateOptions, auditCtx?: AuditContext): Promise<void>;
797
842
  /**
@@ -1003,11 +1048,16 @@ declare class ApprovalEngine {
1003
1048
  * theirs away), or they already voted on it.
1004
1049
  *
1005
1050
  * @returns `{ eligible: true }`, or `{ eligible: false, reason }`.
1051
+ *
1052
+ * @example
1053
+ * ```ts
1054
+ * const check = await engine.canApprove(instance.id, 'bob');
1055
+ * if (!check.eligible) console.log(check.reason);
1056
+ * ```
1006
1057
  */
1007
1058
  canApprove(instanceId: string, userId: string): Promise<CanApproveResult>;
1008
1059
  /** Emergency bypass — completes the instance as 'approved', skipping remaining levels. Requires template.allowOverride = true. */
1009
1060
  override(instanceId: string, raw: OverrideOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
1010
- /** Approve multiple instances in one call. Never throws — failures collected in result.failed. */
1011
1061
  /**
1012
1062
  * Move every pending approval assigned to one person over to another.
1013
1063
  *
@@ -1030,7 +1080,15 @@ declare class ApprovalEngine {
1030
1080
  * @returns What moved, what did not, and why. With `dryRun` nothing is written.
1031
1081
  */
1032
1082
  transferApprovals(raw: TransferApprovalsOptions, auditCtx?: AuditContext): Promise<TransferResult>;
1033
- /** Approve multiple instances in one call. Never throws — failures collected in result.failed. */
1083
+ /**
1084
+ * Approve multiple instances in one call. Never throws — failures collected in result.failed.
1085
+ *
1086
+ * @example
1087
+ * ```ts
1088
+ * const { succeeded, failed } = await engine.bulkApprove(ids, { approverId: 'bob' });
1089
+ * for (const { instanceId, error } of failed) console.warn(instanceId, error.code);
1090
+ * ```
1091
+ */
1034
1092
  bulkApprove(instanceIds: string[], raw: ApproveOptions, auditCtx?: AuditContext): Promise<BulkResult>;
1035
1093
  /** Reject multiple instances in one call. Never throws — failures collected in result.failed. */
1036
1094
  bulkReject(instanceIds: string[], raw: RejectOptions, auditCtx?: AuditContext): Promise<BulkResult>;
@@ -1040,7 +1098,14 @@ declare class ApprovalEngine {
1040
1098
  * @throws {@link ApprovalNotFoundError} if no instance exists with that id.
1041
1099
  */
1042
1100
  getInstance(instanceId: string): Promise<ApprovalInstance>;
1043
- /** Page through every instance with an open level assigned to one approver. */
1101
+ /**
1102
+ * Page through every instance with an open level assigned to one approver.
1103
+ *
1104
+ * @example
1105
+ * ```ts
1106
+ * const page = await engine.getPendingFor('bob', { limit: 20, offset: 0 });
1107
+ * ```
1108
+ */
1044
1109
  getPendingFor(approverId: string, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
1045
1110
  /** Page through instances matching an arbitrary filter (status, document type, date range, ...). */
1046
1111
  queryInstances(filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
package/dist/index.cjs CHANGED
@@ -1570,6 +1570,17 @@ var ApprovalEngine = class _ApprovalEngine {
1570
1570
  * @throws {@link ApprovalTemplateNotFoundError} if `raw.templateName` is not defined.
1571
1571
  * @throws {@link ApprovalValidationError} if `raw` fails schema validation, or if
1572
1572
  * condition evaluation leaves no levels (or a duplicate level number) to open.
1573
+ *
1574
+ * @example
1575
+ * ```ts
1576
+ * const instance = await engine.submit({
1577
+ * templateName: 'purchase-order',
1578
+ * documentId: 'PO-1042',
1579
+ * documentType: 'purchase_order',
1580
+ * submittedBy: 'alice',
1581
+ * data: { amount: 12_500 },
1582
+ * });
1583
+ * ```
1573
1584
  */
1574
1585
  async submit(raw, auditCtx, link) {
1575
1586
  const opts = parseOrThrow(() => SubmitOptionsSchema.parse(raw));
@@ -1739,6 +1750,14 @@ var ApprovalEngine = class _ApprovalEngine {
1739
1750
  * configured {@link IAuthorizationPolicy}.
1740
1751
  * @throws {@link ApprovalValidationError} if `opts.approverId` sits on more than one
1741
1752
  * open parallel level and `opts.level` was not supplied to disambiguate.
1753
+ *
1754
+ * @example
1755
+ * ```ts
1756
+ * const updated = await engine.approve(instance.id, {
1757
+ * approverId: 'bob',
1758
+ * comment: 'Within budget',
1759
+ * });
1760
+ * ```
1742
1761
  */
1743
1762
  async approve(instanceId, raw, auditCtx) {
1744
1763
  const opts = parseOrThrow(() => ApproveOptionsSchema.parse(raw));
@@ -1980,6 +1999,15 @@ var ApprovalEngine = class _ApprovalEngine {
1980
1999
  * @throws {@link ApprovalValidationError} if `opts.approverId` sits on more than one
1981
2000
  * open parallel level with `opts.level` unset, or if `opts.returnTo === 'previous'`
1982
2001
  * but the failing level is already the first level in the chain.
2002
+ *
2003
+ * @example
2004
+ * ```ts
2005
+ * const updated = await engine.reject(instance.id, {
2006
+ * approverId: 'bob',
2007
+ * reason: 'Over budget',
2008
+ * returnTo: 'previous',
2009
+ * });
2010
+ * ```
1983
2011
  */
1984
2012
  async reject(instanceId, raw, auditCtx) {
1985
2013
  const opts = parseOrThrow(() => RejectOptionsSchema.parse(raw));
@@ -2123,6 +2151,16 @@ var ApprovalEngine = class _ApprovalEngine {
2123
2151
  * configured {@link IAuthorizationPolicy}.
2124
2152
  * @throws {@link ApprovalValidationError} if `opts.fromApprover` sits on more than one
2125
2153
  * open parallel level and `opts.level` was not supplied to disambiguate.
2154
+ *
2155
+ * @example
2156
+ * ```ts
2157
+ * await engine.delegate(instance.id, {
2158
+ * fromApprover: 'bob',
2159
+ * toApprover: 'carol',
2160
+ * reason: 'On leave',
2161
+ * until: new Date('2026-11-01'),
2162
+ * });
2163
+ * ```
2126
2164
  */
2127
2165
  async delegate(instanceId, raw, auditCtx) {
2128
2166
  const opts = parseOrThrow(() => DelegateOptionsSchema.parse(raw));
@@ -3381,6 +3419,12 @@ var ApprovalEngine = class _ApprovalEngine {
3381
3419
  * theirs away), or they already voted on it.
3382
3420
  *
3383
3421
  * @returns `{ eligible: true }`, or `{ eligible: false, reason }`.
3422
+ *
3423
+ * @example
3424
+ * ```ts
3425
+ * const check = await engine.canApprove(instance.id, 'bob');
3426
+ * if (!check.eligible) console.log(check.reason);
3427
+ * ```
3384
3428
  */
3385
3429
  async canApprove(instanceId, userId) {
3386
3430
  let instance;
@@ -3485,7 +3529,6 @@ var ApprovalEngine = class _ApprovalEngine {
3485
3529
  await this.afterDecision(overridden);
3486
3530
  return overridden;
3487
3531
  }
3488
- /** Approve multiple instances in one call. Never throws — failures collected in result.failed. */
3489
3532
  /**
3490
3533
  * Move every pending approval assigned to one person over to another.
3491
3534
  *
@@ -3575,7 +3618,15 @@ var ApprovalEngine = class _ApprovalEngine {
3575
3618
  });
3576
3619
  return result;
3577
3620
  }
3578
- /** Approve multiple instances in one call. Never throws — failures collected in result.failed. */
3621
+ /**
3622
+ * Approve multiple instances in one call. Never throws — failures collected in result.failed.
3623
+ *
3624
+ * @example
3625
+ * ```ts
3626
+ * const { succeeded, failed } = await engine.bulkApprove(ids, { approverId: 'bob' });
3627
+ * for (const { instanceId, error } of failed) console.warn(instanceId, error.code);
3628
+ * ```
3629
+ */
3579
3630
  async bulkApprove(instanceIds, raw, auditCtx) {
3580
3631
  const opts = parseOrThrow(() => ApproveOptionsSchema.parse(raw));
3581
3632
  this.guardBulkSize(instanceIds);
@@ -3618,7 +3669,14 @@ var ApprovalEngine = class _ApprovalEngine {
3618
3669
  async getInstance(instanceId) {
3619
3670
  return this.requireInstance(instanceId);
3620
3671
  }
3621
- /** Page through every instance with an open level assigned to one approver. */
3672
+ /**
3673
+ * Page through every instance with an open level assigned to one approver.
3674
+ *
3675
+ * @example
3676
+ * ```ts
3677
+ * const page = await engine.getPendingFor('bob', { limit: 20, offset: 0 });
3678
+ * ```
3679
+ */
3622
3680
  async getPendingFor(approverId, opts) {
3623
3681
  return this.opts.adapter.getInstancesByApprover(this.tenantId, approverId, opts);
3624
3682
  }