hierarchical-approval 2.2.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 +36 -0
  2. package/README.md +25 -0
  3. package/dist/{ApprovalEngine-CbFBAMlO.d.cts → ApprovalEngine-C1dUPnLM.d.cts} +45 -2
  4. package/dist/{ApprovalEngine-B3ChZxS7.d.ts → ApprovalEngine-Ci9urTDI.d.ts} +45 -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 +81 -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 +81 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/nestjs.cjs +77 -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 +77 -0
  30. package/dist/nestjs.js.map +1 -1
  31. package/dist/testing.cjs +81 -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 +81 -0
  36. package/dist/testing.js.map +1 -1
  37. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -915,6 +915,7 @@ function computeTimingStats(samples) {
915
915
 
916
916
  // src/engine/ApprovalEngine.ts
917
917
  var MAX_SUBWORKFLOW_DEPTH = 5;
918
+ var DEFAULT_PURGE_LIMIT = 1e3;
918
919
  var DEFAULT_MAX_REMINDERS = 3;
919
920
  var DEFAULT_MAX_ATTEMPTS = 3;
920
921
  var DEFAULT_BASE_DELAY_MS = 50;
@@ -3120,6 +3121,82 @@ var ApprovalEngine = class _ApprovalEngine {
3120
3121
  });
3121
3122
  return result;
3122
3123
  }
3124
+ /**
3125
+ * Permanently remove finished approvals older than a cut-off.
3126
+ *
3127
+ * Approval tables only grow, and data-minimisation rules eventually require
3128
+ * old records to go. There was no way to remove one, so operators reached
3129
+ * around the library and deleted rows directly — which is exactly where
3130
+ * orphaned audit rows and half-deleted instances come from.
3131
+ *
3132
+ * **Only terminal instances are eligible.** A pending approval is live work;
3133
+ * deleting one would strand a document with no way to finish and no record of
3134
+ * why. Passing a non-terminal status is rejected rather than quietly ignored,
3135
+ * because a caller who asked to purge pending work has misunderstood
3136
+ * something and should hear about it.
3137
+ *
3138
+ * This is irreversible and removes the audit trail with the instance. In many
3139
+ * deployments that trail *is* the compliance record, which is why the
3140
+ * underlying `deleteInstance` is an optional adapter method: an adapter that
3141
+ * does not implement it makes the whole operation unavailable, and this
3142
+ * throws rather than reporting a successful purge of nothing.
3143
+ *
3144
+ * @param opts - Cut-off, optional status/type scoping, safety limit, dry run.
3145
+ * @returns What was removed, or would be under `dryRun`.
3146
+ */
3147
+ async purgeInstances(opts) {
3148
+ const dryRun = opts.dryRun ?? false;
3149
+ const limit = opts.limit ?? DEFAULT_PURGE_LIMIT;
3150
+ if (!(opts.olderThan instanceof Date) || Number.isNaN(opts.olderThan.getTime())) {
3151
+ throw new ApprovalValidationError("purgeInstances requires a valid olderThan date.");
3152
+ }
3153
+ const requested = opts.statuses ?? [...TERMINAL_STATUSES];
3154
+ const nonTerminal = requested.filter((st) => !TERMINAL_STATUSES.has(st));
3155
+ if (nonTerminal.length > 0) {
3156
+ throw new ApprovalValidationError(
3157
+ `purgeInstances refuses non-terminal statuses (${nonTerminal.join(", ")}): a pending approval is live work, and removing it would strand the document it belongs to.`
3158
+ );
3159
+ }
3160
+ const deleteInstance = this.opts.adapter.deleteInstance?.bind(this.opts.adapter);
3161
+ if (!deleteInstance && !dryRun) {
3162
+ throw new ApprovalValidationError(
3163
+ "The configured storage adapter does not implement deleteInstance(), so instances cannot be purged."
3164
+ );
3165
+ }
3166
+ const result = { purged: [], scanned: 0, dryRun };
3167
+ for (const status of requested) {
3168
+ if (result.purged.length >= limit) break;
3169
+ const page = await this.opts.adapter.getInstancesByFilter(
3170
+ this.tenantId,
3171
+ {
3172
+ status,
3173
+ toDate: opts.olderThan,
3174
+ ...opts.documentType ? { documentType: opts.documentType } : {}
3175
+ },
3176
+ { limit, offset: 0 }
3177
+ );
3178
+ for (const instance of page.items) {
3179
+ if (result.purged.length >= limit) break;
3180
+ result.scanned++;
3181
+ if (!TERMINAL_STATUSES.has(instance.status)) continue;
3182
+ if (new Date(instance.createdAt) > opts.olderThan) continue;
3183
+ if (!dryRun) await deleteInstance(this.tenantId, instance.id);
3184
+ result.purged.push({
3185
+ instanceId: instance.id,
3186
+ documentId: instance.documentId,
3187
+ status: instance.status
3188
+ });
3189
+ }
3190
+ }
3191
+ this.logger.info("purgeInstances: sweep complete", {
3192
+ tenantId: this.tenantId,
3193
+ olderThan: opts.olderThan.toISOString(),
3194
+ scanned: result.scanned,
3195
+ purged: result.purged.length,
3196
+ dryRun
3197
+ });
3198
+ return result;
3199
+ }
3123
3200
  async getStatistics(filter = {}) {
3124
3201
  const statuses = [
3125
3202
  "pending",
@@ -4122,6 +4199,10 @@ var MemoryAdapter = class {
4122
4199
  return hasOverdueEscalation || isExpired || hasSLABreach || hasDelegationExpiry || hasDueReminder;
4123
4200
  }).map((i) => reviveDates(deepClone(i)));
4124
4201
  }
4202
+ async deleteInstance(tenantId, id) {
4203
+ const key = `${tenantId}:${id}`;
4204
+ return this.instances.delete(key);
4205
+ }
4125
4206
  async countInstances(tenantId, filter) {
4126
4207
  let count = 0;
4127
4208
  for (const instance of this.instances.values()) {