@pipeline-builder/pipeline-data 3.4.114 → 3.4.116

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;
@@ -113,10 +123,12 @@ export declare abstract class CrudService<TEntity extends BaseEntity, TFilter, T
113
123
  /** Called after a soft-deleted entity is restored (undo the delete). Override
114
124
  * for entities whose restore has side-effects (e.g. re-notify). Best-effort. */
115
125
  protected onAfterRestore(_id: string, _entity: TEntity, _userId: string): Promise<void>;
116
- /** Called (best-effort) with the ids about to be hard-purged, BEFORE the
117
- * delete, so a subclass can tear down dependents that lack ON DELETE CASCADE.
118
- * Throwing aborts that batch's purge (rows stay tombstoned, retried next tick). */
119
- protected onBeforePurge(_ids: 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>;
120
132
  /** Called after rows are hard-purged, for external side-effects (e.g. plugin
121
133
  * image GC). Best-effort: errors are logged, never block the sweep. */
122
134
  protected onAfterPurge(_ids: string[]): Promise<void>;
@@ -124,8 +136,11 @@ export declare abstract class CrudService<TEntity extends BaseEntity, TFilter, T
124
136
  * shared `SOFT_DELETE_RETENTION_DAYS`; override per entity for a bespoke window. */
125
137
  protected get softDeleteRetentionMs(): number;
126
138
  /** The `purge_after` value to stamp on a soft-delete: `now + retention`, or the
127
- * spread-friendly empty object when the entity has no `purge_after` column. */
128
- private purgeAfterStamp;
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>;
129
144
  /**
130
145
  * Org → team hierarchy: when a read is widened to a parent org (`parentOrgId`
131
146
  * set), the parent's rows are outside the caller's RLS scope, so the read must
@@ -153,6 +168,12 @@ export declare abstract class CrudService<TEntity extends BaseEntity, TFilter, T
153
168
  * @param orgId - User's organization ID (optional — omit for anonymous/system-public-only access)
154
169
  */
155
170
  find(filter: Partial<TFilter>, orgId?: string, parentOrgId?: string): Promise<TEntity[]>;
171
+ /**
172
+ * Find the FIRST entity matching `filter` — a bounded (`LIMIT 1`) variant of
173
+ * `find` for the "match by filter, take one" read paths (e.g. `GET /find`), so
174
+ * they don't `SELECT` every matching org row just to return `[0]`.
175
+ */
176
+ findFirst(filter: Partial<TFilter>, orgId?: string, parentOrgId?: string): Promise<TEntity | null>;
156
177
  /**
157
178
  * Find entities with pagination and sorting
158
179
  *
@@ -210,7 +231,7 @@ export declare abstract class CrudService<TEntity extends BaseEntity, TFilter, T
210
231
  * is undefined) keep the supplied org, matching the RLS policy
211
232
  * `current_is_sysadmin() OR org_id = current_org_id()`.
212
233
  */
213
- protected enforceOrgId(data: TInsert): TInsert;
234
+ protected enforceOrgId<T>(data: T): T;
214
235
  /**
215
236
  * Create a new entity
216
237
  */
@@ -270,6 +291,11 @@ export declare abstract class CrudService<TEntity extends BaseEntity, TFilter, T
270
291
  * driver's unique-violation for the route to map to 409 — in practice the
271
292
  * tombstone IS the key holder, so this is defensive.
272
293
  */
294
+ /** Conditions matching a GENUINE tombstone (`isActive=false` AND `deletedAt IS
295
+ * NOT NULL`), optionally pinned to `id` and/or `orgId`. Single source of truth
296
+ * for restore/findDeletedById/findDeleted so the "what is a tombstone" rule
297
+ * lives in one place. */
298
+ private tombstoneConditions;
273
299
  restore(id: string, orgId: string, userId: string): Promise<TEntity | null>;
274
300
  /**
275
301
  * Read a soft-deleted entity by id (the inverse of the default `isActive=true`
@@ -277,7 +303,7 @@ export declare abstract class CrudService<TEntity extends BaseEntity, TFilter, T
277
303
  * gating before restoring. Returns null unless the row exists AND is a genuine
278
304
  * tombstone (`isActive=false` + `deletedAt IS NOT NULL`).
279
305
  */
280
- findDeletedById(id: string, orgId?: string, parentOrgId?: string): Promise<TEntity | null>;
306
+ findDeletedById(id: string, orgId?: string): Promise<TEntity | null>;
281
307
  /**
282
308
  * List an org's soft-deleted tombstones (`isActive=false` + `deletedAt IS NOT
283
309
  * NULL`), most-recently-deleted first — powers the "recently deleted" restore
@@ -288,14 +314,15 @@ export declare abstract class CrudService<TEntity extends BaseEntity, TFilter, T
288
314
  findDeleted(orgId: string, opts?: {
289
315
  limit?: number;
290
316
  offset?: number;
291
- }, parentOrgId?: string): Promise<TEntity[]>;
317
+ }): Promise<TEntity[]>;
292
318
  /**
293
319
  * Hard-delete a batch of tombstones whose purge deadline has passed
294
320
  * (`deletedAt IS NOT NULL AND purge_after < now`). Batched (`limit`) so a
295
321
  * sweep tick stays bounded; the per-service sweep loops until a tick returns
296
- * `< limit`. Runs across ALL orgs, so callers MUST invoke it from a system
297
- * (no-tenant) context. `onBeforePurge` tears down dependents lacking ON DELETE
298
- * CASCADE (inside the tx); `onAfterPurge` handles external side-effects.
322
+ * `< limit`. Runs across ALL orgs, so callers MUST invoke it from a sysadmin
323
+ * scope (`runSoftDeletePurge` establishes one). `onBeforePurge(ids, tx)` tears
324
+ * down dependents lacking ON DELETE CASCADE inside the same tx; `onAfterPurge`
325
+ * handles external side-effects.
299
326
  * Returns the number of rows purged. No-op (0) for entities without the columns.
300
327
  */
301
328
  purgeExpired(now: Date, limit?: number): Promise<number>;