@pipeline-builder/pipeline-data 3.4.113 → 3.4.115

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.
@@ -1,6 +1,7 @@
1
1
  import { SQL } from 'drizzle-orm';
2
2
  import type { AnyColumn } from 'drizzle-orm/column';
3
3
  import type { PgTable } from 'drizzle-orm/pg-core';
4
+ import { withTenantTx } from '../database/tenancy.js';
4
5
  /**
5
6
  * Cast Drizzle query results to a typed array.
6
7
  * Drizzle's generic return type (`PgSelectBase<...>`) doesn't narrow to our
@@ -12,6 +13,10 @@ export declare function drizzleRows<T>(rows: unknown): T[];
12
13
  export declare function drizzleCount(rows: unknown): [{
13
14
  count: number;
14
15
  }];
16
+ /** The Drizzle transaction object handed to `withTenantTx` callbacks. Passed to
17
+ * `onBeforePurge` so a subclass's dependent teardown runs in the SAME purge
18
+ * transaction (atomic with the parent DELETE), not a fresh connection. */
19
+ export type CrudTx = Parameters<Parameters<typeof withTenantTx>[0]>[0];
15
20
  /**
16
21
  * Base interface for entities with common fields
17
22
  */
@@ -53,6 +58,11 @@ export interface PaginatedResult<T> {
53
58
  /** Cursor pointing to the last item, for cursor-based pagination. */
54
59
  nextCursor?: string;
55
60
  }
61
+ /** The shared soft-delete retention window in ms (`SOFT_DELETE_RETENTION_DAYS`,
62
+ * default 30d; 0 disables purge-deadline stamping). Exposed so non-CrudService
63
+ * soft-delete paths (e.g. platform's hand-rolled dashboard/alert services)
64
+ * stamp `purge_after` with the SAME window the CrudService entities use. */
65
+ export declare function softDeleteRetentionMs(): number;
56
66
  export declare abstract class CrudService<TEntity extends BaseEntity, TFilter, TInsert, TUpdate> {
57
67
  /** Drizzle schema table for this entity */
58
68
  protected abstract get schema(): PgTable;
@@ -110,6 +120,27 @@ export declare abstract class CrudService<TEntity extends BaseEntity, TFilter, T
110
120
  protected onAfterUpdate(_id: string, _entity: TEntity, _userId: string): Promise<void>;
111
121
  /** Called after an entity is soft-deleted */
112
122
  protected onAfterDelete(_id: string, _entity: TEntity, _userId: string): Promise<void>;
123
+ /** Called after a soft-deleted entity is restored (undo the delete). Override
124
+ * for entities whose restore has side-effects (e.g. re-notify). Best-effort. */
125
+ protected onAfterRestore(_id: string, _entity: TEntity, _userId: string): Promise<void>;
126
+ /** Called with the ids about to be hard-purged, BEFORE the delete, inside the
127
+ * purge transaction (`tx`) — a subclass tearing down dependents that lack ON
128
+ * DELETE CASCADE MUST use `tx` so the teardown is atomic with the parent
129
+ * DELETE (and can't self-deadlock against its row locks). Throwing aborts that
130
+ * batch's purge (rows stay tombstoned, retried next tick). */
131
+ protected onBeforePurge(_ids: string[], _tx: CrudTx): Promise<void>;
132
+ /** Called after rows are hard-purged, for external side-effects (e.g. plugin
133
+ * image GC). Best-effort: errors are logged, never block the sweep. */
134
+ protected onAfterPurge(_ids: string[]): Promise<void>;
135
+ /** Retention before a soft-deleted row becomes purge-eligible. Defaults to the
136
+ * shared `SOFT_DELETE_RETENTION_DAYS`; override per entity for a bespoke window. */
137
+ protected get softDeleteRetentionMs(): number;
138
+ /** The `purge_after` value to stamp on a soft-delete: `now + retention`, or the
139
+ * spread-friendly empty object when the entity has no `purge_after` column.
140
+ * `protected` so subclasses with hand-rolled soft-delete paths (e.g.
141
+ * message-service's thread cascade / sysadmin moderation) stamp it too — an
142
+ * un-stamped tombstone has NULL `purge_after` and is never hard-purged. */
143
+ protected purgeAfterStamp(now: Date): Record<string, unknown>;
113
144
  /**
114
145
  * Org → team hierarchy: when a read is widened to a parent org (`parentOrgId`
115
146
  * set), the parent's rows are outside the caller's RLS scope, so the read must
@@ -241,4 +272,52 @@ export declare abstract class CrudService<TEntity extends BaseEntity, TFilter, T
241
272
  * (shared/sysadmin-managed) records. No-op for entities without an
242
273
  * `accessModifier` column. Callers pass `!isSystemAdmin(req)`. */
243
274
  restrictToPrivate?: boolean): Promise<TEntity[]>;
275
+ /**
276
+ * Restore a soft-deleted (tombstoned) entity: clear `isActive`/`deletedAt`/
277
+ * `deletedBy`/`purgeAfter` so it reappears in normal reads.
278
+ *
279
+ * Matches ONLY a genuine tombstone — `isActive = false` AND `deletedAt IS NOT
280
+ * NULL` — so a merely *deactivated* row (isActive=false, no deletedAt) is never
281
+ * silently "restored". Pinned to the caller's own org (orgId-less sysadmin
282
+ * context spans orgs, matching delete/update). Returns null when there is no
283
+ * such tombstone (already active, purged, or unknown) so the route can 404.
284
+ * A unique-key collision (a live row already holds the key) surfaces as the
285
+ * driver's unique-violation for the route to map to 409 — in practice the
286
+ * tombstone IS the key holder, so this is defensive.
287
+ */
288
+ /** Conditions matching a GENUINE tombstone (`isActive=false` AND `deletedAt IS
289
+ * NOT NULL`), optionally pinned to `id` and/or `orgId`. Single source of truth
290
+ * for restore/findDeletedById/findDeleted so the "what is a tombstone" rule
291
+ * lives in one place. */
292
+ private tombstoneConditions;
293
+ restore(id: string, orgId: string, userId: string): Promise<TEntity | null>;
294
+ /**
295
+ * Read a soft-deleted entity by id (the inverse of the default `isActive=true`
296
+ * reads) — used by restore routes to load the tombstone for access-control
297
+ * gating before restoring. Returns null unless the row exists AND is a genuine
298
+ * tombstone (`isActive=false` + `deletedAt IS NOT NULL`).
299
+ */
300
+ findDeletedById(id: string, orgId?: string): Promise<TEntity | null>;
301
+ /**
302
+ * List an org's soft-deleted tombstones (`isActive=false` + `deletedAt IS NOT
303
+ * NULL`), most-recently-deleted first — powers the "recently deleted" restore
304
+ * UI. Returns `[]` for entities without a soft-delete lifecycle. `limit` is
305
+ * clamped to [1, 200] to keep the response bounded (the retention sweep hard-
306
+ * deletes old tombstones anyway, so this is a short, self-limiting list).
307
+ */
308
+ findDeleted(orgId: string, opts?: {
309
+ limit?: number;
310
+ offset?: number;
311
+ }): Promise<TEntity[]>;
312
+ /**
313
+ * Hard-delete a batch of tombstones whose purge deadline has passed
314
+ * (`deletedAt IS NOT NULL AND purge_after < now`). Batched (`limit`) so a
315
+ * sweep tick stays bounded; the per-service sweep loops until a tick returns
316
+ * `< limit`. Runs across ALL orgs, so callers MUST invoke it from a sysadmin
317
+ * scope (`runSoftDeletePurge` establishes one). `onBeforePurge(ids, tx)` tears
318
+ * down dependents lacking ON DELETE CASCADE inside the same tx; `onAfterPurge`
319
+ * handles external side-effects.
320
+ * Returns the number of rows purged. No-op (0) for entities without the columns.
321
+ */
322
+ purgeExpired(now: Date, limit?: number): Promise<number>;
244
323
  }