hierarchical-approval 2.1.0 → 2.3.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +51 -0
  3. package/dist/{ApprovalEngine-DqRIadYg.d.cts → ApprovalEngine-C1dUPnLM.d.cts} +101 -2
  4. package/dist/{ApprovalEngine-CJwnpnpL.d.ts → ApprovalEngine-Ci9urTDI.d.ts} +101 -2
  5. package/dist/{IStorageAdapter-DjRvHUF0.d.ts → IStorageAdapter-BU3sau5W.d.ts} +14 -0
  6. package/dist/{IStorageAdapter-Bk7ybd3z.d.cts → IStorageAdapter-DdHO4Rf1.d.cts} +14 -0
  7. package/dist/adapters/MemoryAdapter.cjs +4 -0
  8. package/dist/adapters/MemoryAdapter.cjs.map +1 -1
  9. package/dist/adapters/MemoryAdapter.d.cts +2 -1
  10. package/dist/adapters/MemoryAdapter.d.ts +2 -1
  11. package/dist/adapters/MemoryAdapter.js +4 -0
  12. package/dist/adapters/MemoryAdapter.js.map +1 -1
  13. package/dist/adapters/PostgresAdapter.cjs +12 -0
  14. package/dist/adapters/PostgresAdapter.cjs.map +1 -1
  15. package/dist/adapters/PostgresAdapter.d.cts +2 -1
  16. package/dist/adapters/PostgresAdapter.d.ts +2 -1
  17. package/dist/adapters/PostgresAdapter.js +12 -0
  18. package/dist/adapters/PostgresAdapter.js.map +1 -1
  19. package/dist/index.cjs +195 -0
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +3 -3
  22. package/dist/index.d.ts +3 -3
  23. package/dist/index.js +195 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/nestjs.cjs +190 -0
  26. package/dist/nestjs.cjs.map +1 -1
  27. package/dist/nestjs.d.cts +2 -2
  28. package/dist/nestjs.d.ts +2 -2
  29. package/dist/nestjs.js +190 -0
  30. package/dist/nestjs.js.map +1 -1
  31. package/dist/testing.cjs +194 -0
  32. package/dist/testing.cjs.map +1 -1
  33. package/dist/testing.d.cts +2 -2
  34. package/dist/testing.d.ts +2 -2
  35. package/dist/testing.js +194 -0
  36. package/dist/testing.js.map +1 -1
  37. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- export { A as AddCommentOptions, a as ApprovalEngine, b as ApprovalEngineOptions, c as ApprovalStatistics, d as ApproveOptions, e as ApproverResolverFn, f as ApproverWorkload, B as BulkResult, g as BusinessCalendar, C as CanApproveResult, h as CancelOptions, i as ConditionOperatorFn, j as CycleTimeStats, D as DelegateOptions, E as EscalateOptions, H as HealthResult, I as IdGeneratorFn, k as IdempotencyKeyFn, O as OrgProvider, l as OutOfOfficeProvider, m as OverrideOptions, P as PreviewChainLevel, n as PreviewResult, o as ProvideInfoOptions, R as ReassignOptions, p as RejectOptions, q as RequestInfoOptions, r as ResubmitOptions, s as RetryPolicy, S as SubmitOptions, T as TransferResult, U as UpdateDataOptions, V as ValidationResult, W as WeekendCalendarOptions, t as defaultIdGenerator, u as toComparableNumber, v as validateConditionExpression, w as weekendCalendar } from './ApprovalEngine-DqRIadYg.cjs';
2
- import { I as IStorageAdapter } from './IStorageAdapter-Bk7ybd3z.cjs';
3
- export { c as CursorPaginatedResult, C as CursorPaginationOpts, b as InstanceFilter, a as PaginatedResult, P as PaginationOpts } from './IStorageAdapter-Bk7ybd3z.cjs';
1
+ export { A as AddCommentOptions, a as ApprovalEngine, b as ApprovalEngineOptions, c as ApprovalStatistics, d as ApproveOptions, e as ApproverResolverFn, f as ApproverWorkload, B as BulkResult, g as BusinessCalendar, C as CanApproveResult, h as CancelOptions, i as ConditionOperatorFn, j as CycleTimeStats, D as DelegateOptions, E as EscalateOptions, H as HealthResult, I as IdGeneratorFn, k as IdempotencyKeyFn, l as ImportResult, O as OrgProvider, m as OutOfOfficeProvider, n as OverrideOptions, P as PreviewChainLevel, o as PreviewResult, p as ProvideInfoOptions, q as PurgeResult, R as ReassignOptions, r as RejectOptions, s as RequestInfoOptions, t as ResubmitOptions, u as RetryPolicy, S as SubmitOptions, T as TEMPLATE_BUNDLE_VERSION, v as TemplateBundle, w as TransferResult, U as UpdateDataOptions, V as ValidationResult, W as WeekendCalendarOptions, x as defaultIdGenerator, y as toComparableNumber, z as validateConditionExpression, F as weekendCalendar } from './ApprovalEngine-C1dUPnLM.cjs';
2
+ import { I as IStorageAdapter } from './IStorageAdapter-DdHO4Rf1.cjs';
3
+ export { c as CursorPaginatedResult, C as CursorPaginationOpts, b as InstanceFilter, a as PaginatedResult, P as PaginationOpts } from './IStorageAdapter-DdHO4Rf1.cjs';
4
4
  import { L as Logger } from './Logger-BplhlU7l.cjs';
5
5
  export { n as noopLogger } from './Logger-BplhlU7l.cjs';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.cjs';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { A as AddCommentOptions, a as ApprovalEngine, b as ApprovalEngineOptions, c as ApprovalStatistics, d as ApproveOptions, e as ApproverResolverFn, f as ApproverWorkload, B as BulkResult, g as BusinessCalendar, C as CanApproveResult, h as CancelOptions, i as ConditionOperatorFn, j as CycleTimeStats, D as DelegateOptions, E as EscalateOptions, H as HealthResult, I as IdGeneratorFn, k as IdempotencyKeyFn, O as OrgProvider, l as OutOfOfficeProvider, m as OverrideOptions, P as PreviewChainLevel, n as PreviewResult, o as ProvideInfoOptions, R as ReassignOptions, p as RejectOptions, q as RequestInfoOptions, r as ResubmitOptions, s as RetryPolicy, S as SubmitOptions, T as TransferResult, U as UpdateDataOptions, V as ValidationResult, W as WeekendCalendarOptions, t as defaultIdGenerator, u as toComparableNumber, v as validateConditionExpression, w as weekendCalendar } from './ApprovalEngine-CJwnpnpL.js';
2
- import { I as IStorageAdapter } from './IStorageAdapter-DjRvHUF0.js';
3
- export { c as CursorPaginatedResult, C as CursorPaginationOpts, b as InstanceFilter, a as PaginatedResult, P as PaginationOpts } from './IStorageAdapter-DjRvHUF0.js';
1
+ export { A as AddCommentOptions, a as ApprovalEngine, b as ApprovalEngineOptions, c as ApprovalStatistics, d as ApproveOptions, e as ApproverResolverFn, f as ApproverWorkload, B as BulkResult, g as BusinessCalendar, C as CanApproveResult, h as CancelOptions, i as ConditionOperatorFn, j as CycleTimeStats, D as DelegateOptions, E as EscalateOptions, H as HealthResult, I as IdGeneratorFn, k as IdempotencyKeyFn, l as ImportResult, O as OrgProvider, m as OutOfOfficeProvider, n as OverrideOptions, P as PreviewChainLevel, o as PreviewResult, p as ProvideInfoOptions, q as PurgeResult, R as ReassignOptions, r as RejectOptions, s as RequestInfoOptions, t as ResubmitOptions, u as RetryPolicy, S as SubmitOptions, T as TEMPLATE_BUNDLE_VERSION, v as TemplateBundle, w as TransferResult, U as UpdateDataOptions, V as ValidationResult, W as WeekendCalendarOptions, x as defaultIdGenerator, y as toComparableNumber, z as validateConditionExpression, F as weekendCalendar } from './ApprovalEngine-Ci9urTDI.js';
2
+ import { I as IStorageAdapter } from './IStorageAdapter-BU3sau5W.js';
3
+ export { c as CursorPaginatedResult, C as CursorPaginationOpts, b as InstanceFilter, a as PaginatedResult, P as PaginationOpts } from './IStorageAdapter-BU3sau5W.js';
4
4
  import { L as Logger } from './Logger-BplhlU7l.js';
5
5
  export { n as noopLogger } from './Logger-BplhlU7l.js';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.js';
package/dist/index.js CHANGED
@@ -909,6 +909,7 @@ function computeTimingStats(samples) {
909
909
 
910
910
  // src/engine/ApprovalEngine.ts
911
911
  var MAX_SUBWORKFLOW_DEPTH = 5;
912
+ var DEFAULT_PURGE_LIMIT = 1e3;
912
913
  var DEFAULT_MAX_REMINDERS = 3;
913
914
  var DEFAULT_MAX_ATTEMPTS = 3;
914
915
  var DEFAULT_BASE_DELAY_MS = 50;
@@ -920,6 +921,7 @@ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
920
921
  ]);
921
922
  var CYCLE_TIME_STATUSES = ["approved", "rejected", "cancelled"];
922
923
  var CYCLE_TIME_FETCH_BATCH_SIZE = 500;
924
+ var TEMPLATE_BUNDLE_VERSION = 1;
923
925
  var ApprovalEngine = class _ApprovalEngine {
924
926
  constructor(opts) {
925
927
  this.opts = opts;
@@ -3001,6 +3003,194 @@ var ApprovalEngine = class _ApprovalEngine {
3001
3003
  oldestAgeMs: Number.isFinite(row.oldest) ? now.getTime() - row.oldest : 0
3002
3004
  })).sort((a, b) => b.pending - a.pending || a.approverId.localeCompare(b.approverId));
3003
3005
  }
3006
+ /**
3007
+ * Export templates as a portable bundle.
3008
+ *
3009
+ * Approval configuration is written once and then has to travel — authored in
3010
+ * a sandbox, reviewed, promoted to production. Reading `listTemplates()` and
3011
+ * re-posting the rows carried each environment's own `id`, `tenantId` and
3012
+ * version lineage with it, which either collided on arrival or silently
3013
+ * claimed a history the target never had. This strips all of it.
3014
+ *
3015
+ * @param names - Templates to include; omit for all of them.
3016
+ */
3017
+ async exportTemplates(names) {
3018
+ const all = await this.registry.list();
3019
+ const wanted = names ? all.filter((t) => names.includes(t.name)) : all;
3020
+ if (names) {
3021
+ const missing = names.filter((n) => !all.some((t) => t.name === n));
3022
+ if (missing.length > 0) {
3023
+ throw new ApprovalTemplateNotFoundError(missing.join(", "));
3024
+ }
3025
+ }
3026
+ return {
3027
+ bundleVersion: TEMPLATE_BUNDLE_VERSION,
3028
+ exportedAt: this.clock.now(),
3029
+ templates: wanted.map((t) => {
3030
+ const {
3031
+ id: _id,
3032
+ tenantId: _tenantId,
3033
+ createdAt: _createdAt,
3034
+ version: _version,
3035
+ previousVersionId: _previousVersionId,
3036
+ ...config
3037
+ } = t;
3038
+ return config;
3039
+ })
3040
+ };
3041
+ }
3042
+ /**
3043
+ * Import a bundle produced by {@link exportTemplates}.
3044
+ *
3045
+ * **Every template is validated before any is written.** A bundle that is
3046
+ * half-applied is worse than one rejected outright: the tenant is left in a
3047
+ * state matching neither environment, and the operator has no way to tell
3048
+ * which half landed. Per-template failures during the write phase are still
3049
+ * reported individually, since a storage error can occur after validation
3050
+ * passes.
3051
+ *
3052
+ * @param bundle - The bundle to apply.
3053
+ * @param opts - `mode: 'create'` (default) refuses to touch existing
3054
+ * templates; `'upsert'` updates them. `dryRun` reports without writing.
3055
+ */
3056
+ async importTemplates(bundle, opts = {}) {
3057
+ const mode = opts.mode ?? "create";
3058
+ const dryRun = opts.dryRun ?? false;
3059
+ if (bundle.bundleVersion !== TEMPLATE_BUNDLE_VERSION) {
3060
+ throw new ApprovalValidationError(
3061
+ `Unsupported template bundle version ${bundle.bundleVersion}; this engine reads version ${TEMPLATE_BUNDLE_VERSION}.`
3062
+ );
3063
+ }
3064
+ if (!Array.isArray(bundle.templates) || bundle.templates.length === 0) {
3065
+ throw new ApprovalValidationError("Template bundle contains no templates.");
3066
+ }
3067
+ const duplicates = bundle.templates.map((t) => t.name).filter((name, i, all) => all.indexOf(name) !== i);
3068
+ if (duplicates.length > 0) {
3069
+ throw new ApprovalValidationError(
3070
+ `Template bundle contains duplicate names: ${[...new Set(duplicates)].join(", ")}.`
3071
+ );
3072
+ }
3073
+ const invalid = [];
3074
+ for (const config of bundle.templates) {
3075
+ const result2 = this.validateTemplate(config);
3076
+ if (!result2.valid) {
3077
+ invalid.push({
3078
+ name: config.name,
3079
+ message: result2.errors[0]?.message ?? "unknown validation error"
3080
+ });
3081
+ }
3082
+ }
3083
+ if (invalid.length > 0) {
3084
+ throw new ApprovalValidationError(
3085
+ `Template bundle failed validation and was not applied: ${invalid.map((e) => `${e.name}: ${e.message}`).join("; ")}`
3086
+ );
3087
+ }
3088
+ const result = { created: [], updated: [], skipped: [], errors: [], dryRun };
3089
+ for (const config of bundle.templates) {
3090
+ const existing = await this.opts.adapter.getTemplate(this.tenantId, config.name);
3091
+ try {
3092
+ if (existing && mode === "create") {
3093
+ result.skipped.push(config.name);
3094
+ continue;
3095
+ }
3096
+ if (existing) {
3097
+ if (!dryRun) await this.registry.update(config);
3098
+ result.updated.push(config.name);
3099
+ } else {
3100
+ if (!dryRun) await this.registry.define(config);
3101
+ result.created.push(config.name);
3102
+ }
3103
+ } catch (err) {
3104
+ result.errors.push({ name: config.name, message: err.message });
3105
+ }
3106
+ }
3107
+ this.logger.info("importTemplates: bundle applied", {
3108
+ tenantId: this.tenantId,
3109
+ mode,
3110
+ dryRun,
3111
+ created: result.created.length,
3112
+ updated: result.updated.length,
3113
+ skipped: result.skipped.length,
3114
+ errors: result.errors.length
3115
+ });
3116
+ return result;
3117
+ }
3118
+ /**
3119
+ * Permanently remove finished approvals older than a cut-off.
3120
+ *
3121
+ * Approval tables only grow, and data-minimisation rules eventually require
3122
+ * old records to go. There was no way to remove one, so operators reached
3123
+ * around the library and deleted rows directly — which is exactly where
3124
+ * orphaned audit rows and half-deleted instances come from.
3125
+ *
3126
+ * **Only terminal instances are eligible.** A pending approval is live work;
3127
+ * deleting one would strand a document with no way to finish and no record of
3128
+ * why. Passing a non-terminal status is rejected rather than quietly ignored,
3129
+ * because a caller who asked to purge pending work has misunderstood
3130
+ * something and should hear about it.
3131
+ *
3132
+ * This is irreversible and removes the audit trail with the instance. In many
3133
+ * deployments that trail *is* the compliance record, which is why the
3134
+ * underlying `deleteInstance` is an optional adapter method: an adapter that
3135
+ * does not implement it makes the whole operation unavailable, and this
3136
+ * throws rather than reporting a successful purge of nothing.
3137
+ *
3138
+ * @param opts - Cut-off, optional status/type scoping, safety limit, dry run.
3139
+ * @returns What was removed, or would be under `dryRun`.
3140
+ */
3141
+ async purgeInstances(opts) {
3142
+ const dryRun = opts.dryRun ?? false;
3143
+ const limit = opts.limit ?? DEFAULT_PURGE_LIMIT;
3144
+ if (!(opts.olderThan instanceof Date) || Number.isNaN(opts.olderThan.getTime())) {
3145
+ throw new ApprovalValidationError("purgeInstances requires a valid olderThan date.");
3146
+ }
3147
+ const requested = opts.statuses ?? [...TERMINAL_STATUSES];
3148
+ const nonTerminal = requested.filter((st) => !TERMINAL_STATUSES.has(st));
3149
+ if (nonTerminal.length > 0) {
3150
+ throw new ApprovalValidationError(
3151
+ `purgeInstances refuses non-terminal statuses (${nonTerminal.join(", ")}): a pending approval is live work, and removing it would strand the document it belongs to.`
3152
+ );
3153
+ }
3154
+ const deleteInstance = this.opts.adapter.deleteInstance?.bind(this.opts.adapter);
3155
+ if (!deleteInstance && !dryRun) {
3156
+ throw new ApprovalValidationError(
3157
+ "The configured storage adapter does not implement deleteInstance(), so instances cannot be purged."
3158
+ );
3159
+ }
3160
+ const result = { purged: [], scanned: 0, dryRun };
3161
+ for (const status of requested) {
3162
+ if (result.purged.length >= limit) break;
3163
+ const page = await this.opts.adapter.getInstancesByFilter(
3164
+ this.tenantId,
3165
+ {
3166
+ status,
3167
+ toDate: opts.olderThan,
3168
+ ...opts.documentType ? { documentType: opts.documentType } : {}
3169
+ },
3170
+ { limit, offset: 0 }
3171
+ );
3172
+ for (const instance of page.items) {
3173
+ if (result.purged.length >= limit) break;
3174
+ result.scanned++;
3175
+ if (!TERMINAL_STATUSES.has(instance.status)) continue;
3176
+ if (new Date(instance.createdAt) > opts.olderThan) continue;
3177
+ if (!dryRun) await deleteInstance(this.tenantId, instance.id);
3178
+ result.purged.push({
3179
+ instanceId: instance.id,
3180
+ documentId: instance.documentId,
3181
+ status: instance.status
3182
+ });
3183
+ }
3184
+ }
3185
+ this.logger.info("purgeInstances: sweep complete", {
3186
+ tenantId: this.tenantId,
3187
+ olderThan: opts.olderThan.toISOString(),
3188
+ scanned: result.scanned,
3189
+ purged: result.purged.length,
3190
+ dryRun
3191
+ });
3192
+ return result;
3193
+ }
3004
3194
  async getStatistics(filter = {}) {
3005
3195
  const statuses = [
3006
3196
  "pending",
@@ -4003,6 +4193,10 @@ var MemoryAdapter = class {
4003
4193
  return hasOverdueEscalation || isExpired || hasSLABreach || hasDelegationExpiry || hasDueReminder;
4004
4194
  }).map((i) => reviveDates(deepClone(i)));
4005
4195
  }
4196
+ async deleteInstance(tenantId, id) {
4197
+ const key = `${tenantId}:${id}`;
4198
+ return this.instances.delete(key);
4199
+ }
4006
4200
  async countInstances(tenantId, filter) {
4007
4201
  let count = 0;
4008
4202
  for (const instance of this.instances.values()) {
@@ -4091,6 +4285,6 @@ function weekendCalendar(options = {}) {
4091
4285
  };
4092
4286
  }
4093
4287
 
4094
- export { ApprovalConflictError, ApprovalEngine, ApprovalError, ApprovalForbiddenError, ApprovalNotFoundError, ApprovalTemplateNotFoundError, ApprovalValidationError, EscalationScheduler, MemoryAdapter, defaultIdGenerator, noopLogger, systemClock, toComparableNumber, validateConditionExpression, weekendCalendar };
4288
+ export { ApprovalConflictError, ApprovalEngine, ApprovalError, ApprovalForbiddenError, ApprovalNotFoundError, ApprovalTemplateNotFoundError, ApprovalValidationError, EscalationScheduler, MemoryAdapter, TEMPLATE_BUNDLE_VERSION, defaultIdGenerator, noopLogger, systemClock, toComparableNumber, validateConditionExpression, weekendCalendar };
4095
4289
  //# sourceMappingURL=index.js.map
4096
4290
  //# sourceMappingURL=index.js.map