@pipeline-builder/pipeline-data 0.0.1
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/LICENSE +202 -0
- package/README.md +61 -0
- package/drizzle.config.ts +17 -0
- package/lib/api/access-control-builder.d.ts +148 -0
- package/lib/api/access-control-builder.js +199 -0
- package/lib/api/crud-service.d.ts +343 -0
- package/lib/api/crud-service.js +849 -0
- package/lib/api/query-builders.d.ts +82 -0
- package/lib/api/query-builders.js +460 -0
- package/lib/api/reporting-service.d.ts +685 -0
- package/lib/api/reporting-service.js +1564 -0
- package/lib/api/soft-delete-sweep.d.ts +65 -0
- package/lib/api/soft-delete-sweep.js +124 -0
- package/lib/api/viewer-context.d.ts +29 -0
- package/lib/api/viewer-context.js +63 -0
- package/lib/core/query-filters.d.ts +263 -0
- package/lib/core/query-filters.js +4 -0
- package/lib/database/drizzle-schema.d.ts +7357 -0
- package/lib/database/drizzle-schema.js +66 -0
- package/lib/database/index.d.ts +5 -0
- package/lib/database/index.js +8 -0
- package/lib/database/migrator.d.ts +20 -0
- package/lib/database/migrator.js +80 -0
- package/lib/database/postgres-connection.d.ts +201 -0
- package/lib/database/postgres-connection.js +413 -0
- package/lib/database/retry-strategy.d.ts +68 -0
- package/lib/database/retry-strategy.js +122 -0
- package/lib/database/schema/alert.d.ts +588 -0
- package/lib/database/schema/alert.js +130 -0
- package/lib/database/schema/compliance.d.ts +3319 -0
- package/lib/database/schema/compliance.js +377 -0
- package/lib/database/schema/dashboard.d.ts +474 -0
- package/lib/database/schema/dashboard.js +129 -0
- package/lib/database/schema/message-attachment.d.ts +183 -0
- package/lib/database/schema/message-attachment.js +40 -0
- package/lib/database/schema/message.d.ts +411 -0
- package/lib/database/schema/message.js +114 -0
- package/lib/database/schema/pipeline-template.d.ts +466 -0
- package/lib/database/schema/pipeline-template.js +80 -0
- package/lib/database/schema/pipeline.d.ts +1613 -0
- package/lib/database/schema/pipeline.js +332 -0
- package/lib/database/schema/plugin.d.ts +749 -0
- package/lib/database/schema/plugin.js +166 -0
- package/lib/database/tenancy.d.ts +103 -0
- package/lib/database/tenancy.js +167 -0
- package/lib/index.d.ts +32 -0
- package/lib/index.js +38 -0
- package/package.json +161 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { SQL } from 'drizzle-orm';
|
|
2
|
+
import type { AnyColumn } from 'drizzle-orm/column';
|
|
3
|
+
import type { PgTable } from 'drizzle-orm/pg-core';
|
|
4
|
+
import { withTenantTx } from '../database/tenancy.js';
|
|
5
|
+
/**
|
|
6
|
+
* Cast Drizzle query results to a typed array.
|
|
7
|
+
* Drizzle's generic return type (`PgSelectBase<...>`) doesn't narrow to our
|
|
8
|
+
* entity generics, so an explicit cast is needed. Centralised here so every
|
|
9
|
+
* call-site stays one-liner clean and the cast is documented in one place.
|
|
10
|
+
*/
|
|
11
|
+
export declare function drizzleRows<T>(rows: unknown): T[];
|
|
12
|
+
/** Cast a Drizzle aggregate result to extract `[{ count: number }]`. */
|
|
13
|
+
export declare function drizzleCount(rows: unknown): [{
|
|
14
|
+
count: number;
|
|
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];
|
|
20
|
+
/**
|
|
21
|
+
* Base interface for entities with common fields
|
|
22
|
+
*/
|
|
23
|
+
export interface BaseEntity {
|
|
24
|
+
id: string;
|
|
25
|
+
orgId: string;
|
|
26
|
+
isDefault: boolean;
|
|
27
|
+
createdAt: Date;
|
|
28
|
+
updatedAt: Date;
|
|
29
|
+
createdBy: string;
|
|
30
|
+
updatedBy: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Pagination and sorting options
|
|
35
|
+
*/
|
|
36
|
+
export interface QueryOptions {
|
|
37
|
+
limit?: number;
|
|
38
|
+
offset?: number;
|
|
39
|
+
sortBy?: string;
|
|
40
|
+
sortOrder?: 'asc' | 'desc';
|
|
41
|
+
/** When true, runs a separate COUNT(*) query to include exact total. Default: false. */
|
|
42
|
+
includeTotal?: boolean;
|
|
43
|
+
/** Cursor-based pagination: fetch rows after this cursor value (uses sortBy column). */
|
|
44
|
+
cursor?: string;
|
|
45
|
+
/** Sparse fieldset: column names to select. Returns all columns when omitted. */
|
|
46
|
+
fields?: string[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Paginated result with metadata
|
|
50
|
+
*/
|
|
51
|
+
export interface PaginatedResult<T> {
|
|
52
|
+
data: T[];
|
|
53
|
+
/** Total count of matching entities. Only present when includeTotal is true. */
|
|
54
|
+
total?: number;
|
|
55
|
+
limit: number;
|
|
56
|
+
offset: number;
|
|
57
|
+
hasMore: boolean;
|
|
58
|
+
/** Cursor pointing to the last item, for cursor-based pagination. */
|
|
59
|
+
nextCursor?: string;
|
|
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;
|
|
66
|
+
export declare abstract class CrudService<TEntity extends BaseEntity, TFilter, TInsert, TUpdate> {
|
|
67
|
+
/** Drizzle schema table for this entity */
|
|
68
|
+
protected abstract get schema(): PgTable;
|
|
69
|
+
/**
|
|
70
|
+
* Typed column view of {@link schema}. `PgTable` exposes no static columns, so
|
|
71
|
+
* the base class previously reached for `(this.schema as any).id` etc. at each
|
|
72
|
+
* use; centralize that single unavoidable cast here so callers get structured,
|
|
73
|
+
* typed access instead. `visibility` is optional (not every entity has it)
|
|
74
|
+
* and the index signature covers dynamic sparse-fieldset column lookup.
|
|
75
|
+
*/
|
|
76
|
+
private get cols();
|
|
77
|
+
/**
|
|
78
|
+
* Build SQL conditions for filtering entities.
|
|
79
|
+
* `parentOrgId` (org → team hierarchy) is optional and only honored by
|
|
80
|
+
* services that opt into parent-inherited visibility (e.g. plugins).
|
|
81
|
+
*/
|
|
82
|
+
protected abstract buildConditions(filter: Partial<TFilter>, orgId?: string, parentOrgId?: string): SQL[];
|
|
83
|
+
/** Get the schema column for sorting by field name */
|
|
84
|
+
protected abstract getSortColumn(sortBy: string): AnyColumn | null;
|
|
85
|
+
/** Get the project column for setDefault scoping (null if entity has no project scope) */
|
|
86
|
+
protected abstract getProjectColumn(): AnyColumn | null;
|
|
87
|
+
/**
|
|
88
|
+
* Get the tenant org-id column. Used both to scope `setDefault` and to pin
|
|
89
|
+
* every write (update/delete/bulkDelete) to the caller's own org — so it MUST
|
|
90
|
+
* be the tenant `orgId` column, not a display/name column.
|
|
91
|
+
*/
|
|
92
|
+
protected abstract getOrgColumn(): AnyColumn;
|
|
93
|
+
/** Get the unique constraint columns for onConflictDoUpdate */
|
|
94
|
+
protected abstract get conflictTarget(): AnyColumn[];
|
|
95
|
+
private readonly _logger;
|
|
96
|
+
constructor();
|
|
97
|
+
/** Build conditions for a single entity by ID.
|
|
98
|
+
* `parentOrgId` widens visibility to a parent org's public rows (org → team
|
|
99
|
+
* hierarchy) — same opt-in semantics as `find`/`findPaginated`. Omitted by the
|
|
100
|
+
* write path (`writeConditions`), which must stay own-org scoped. */
|
|
101
|
+
private idConditions;
|
|
102
|
+
/**
|
|
103
|
+
* Conditions for a MUTATION (update/delete) of a single entity.
|
|
104
|
+
*
|
|
105
|
+
* `idConditions` reuses the READ access-control clause, which also matches
|
|
106
|
+
* system-org / other-org PUBLIC records — so without an extra ownership pin any
|
|
107
|
+
* tenant could update or soft-delete another org's (or the system org's) public
|
|
108
|
+
* records. AND a strict `orgId === caller` predicate: the read clause's
|
|
109
|
+
* public-OR branch requires `org_id = 'system'`, which then can't be satisfied,
|
|
110
|
+
* leaving only own-org rows. orgId-less (sysadmin) context keeps full access.
|
|
111
|
+
*/
|
|
112
|
+
private writeConditions;
|
|
113
|
+
/** Exact id equality, lower-cased to match `buildIdFilter`'s full-UUID branch.
|
|
114
|
+
* `schema` is typed `PgTable` (no static `.id`), so cast — same pattern the
|
|
115
|
+
* sort/default helpers use for column access. */
|
|
116
|
+
private exactIdCondition;
|
|
117
|
+
/** Called after a new entity is created */
|
|
118
|
+
protected onAfterCreate(_entity: TEntity, _userId: string): Promise<void>;
|
|
119
|
+
/** Called after an entity is updated */
|
|
120
|
+
protected onAfterUpdate(_id: string, _entity: TEntity, _userId: string): Promise<void>;
|
|
121
|
+
/** Called after an entity is soft-deleted */
|
|
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>;
|
|
144
|
+
/**
|
|
145
|
+
* Org → team hierarchy: when a read is widened to a parent org (`parentOrgId`
|
|
146
|
+
* set), the parent's rows are outside the caller's RLS scope, so the read must
|
|
147
|
+
* run under sysadmin context — the access-control WHERE clause (own + parent +
|
|
148
|
+
* system public, built server-side from a trusted JWT claim) is then the
|
|
149
|
+
* authoritative tenancy gate. Without `parentOrgId` it stays RLS-scoped.
|
|
150
|
+
*
|
|
151
|
+
* INVARIANT (why the sysadmin bypass is safe): the WHERE clause is always
|
|
152
|
+
* server-built by `buildConditions(filter, orgId, parentOrgId)` from values
|
|
153
|
+
* derived from the validated JWT (`orgId` = active org, `parentOrgId` = the
|
|
154
|
+
* active org's parent). None of them is a raw user-supplied predicate, so the
|
|
155
|
+
* bypass only ever widens the read to the caller's own subtree + system public
|
|
156
|
+
* rows — it can NOT be steered at an arbitrary org. NOTE: this does remove the
|
|
157
|
+
* RLS backstop for the widened read; a single-GUC `app.org_id` can't express
|
|
158
|
+
* the "own OR parent OR system" set the policy would need, so replacing the
|
|
159
|
+
* bypass would require a tenancy-layer change (e.g. a multi-org RLS policy).
|
|
160
|
+
* Deferred deliberately — do not swap this for a blanket bypass without
|
|
161
|
+
* re-checking the WHERE is still fully server-built.
|
|
162
|
+
*/
|
|
163
|
+
private runRead;
|
|
164
|
+
/**
|
|
165
|
+
* Find entities matching filter criteria
|
|
166
|
+
*
|
|
167
|
+
* @param filter - Filter criteria
|
|
168
|
+
* @param orgId - User's organization ID (optional — omit for anonymous/system-public-only access)
|
|
169
|
+
*/
|
|
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>;
|
|
177
|
+
/**
|
|
178
|
+
* Find entities with pagination and sorting
|
|
179
|
+
*
|
|
180
|
+
* @param filter - Filter criteria
|
|
181
|
+
* @param orgId - User's organization ID (optional — omit for anonymous/system-public-only access)
|
|
182
|
+
* @param options - Pagination and sorting options
|
|
183
|
+
*/
|
|
184
|
+
findPaginated(filter: Partial<TFilter>, orgId?: string, options?: QueryOptions, parentOrgId?: string): Promise<PaginatedResult<TEntity>>;
|
|
185
|
+
/**
|
|
186
|
+
* Build a column selection map for sparse fieldsets.
|
|
187
|
+
* Falls back to full select if no matching columns found.
|
|
188
|
+
*
|
|
189
|
+
* `sortBy` (when set) is always folded into the projection even if the caller's
|
|
190
|
+
* `fields` omits it — cursor pagination reads the next cursor from the sort
|
|
191
|
+
* column's value on the last row, so dropping it from the SELECT would yield an
|
|
192
|
+
* undefined cursor and stall paging. Included the same way `id` always is.
|
|
193
|
+
*/
|
|
194
|
+
private buildFieldSelect;
|
|
195
|
+
/**
|
|
196
|
+
* Count entities matching filter criteria
|
|
197
|
+
*
|
|
198
|
+
* @param filter - Filter criteria
|
|
199
|
+
* @param orgId - User's organization ID (optional — omit for anonymous/system-public-only access)
|
|
200
|
+
* @param parentOrgId - Org → team hierarchy: widen the count to the parent org's
|
|
201
|
+
* public rows. MUST mirror `find`/`findPaginated`'s widening (same
|
|
202
|
+
* `buildConditions` args + sysadmin-scoped read via `runRead`) so a paged
|
|
203
|
+
* list's `total` matches the rows a widened `find()` returns — otherwise the
|
|
204
|
+
* count omits the parent's rows and reports a narrower total than the data set.
|
|
205
|
+
*/
|
|
206
|
+
count(filter: Partial<TFilter>, orgId?: string, parentOrgId?: string): Promise<number>;
|
|
207
|
+
/**
|
|
208
|
+
* Find a single entity by ID
|
|
209
|
+
*
|
|
210
|
+
* @param id - Entity ID
|
|
211
|
+
* @param orgId - User's organization ID (optional — omit for anonymous/system-public-only access)
|
|
212
|
+
* @param parentOrgId - Org → team hierarchy: widen visibility to the parent
|
|
213
|
+
* org's public rows (so a team can fetch a parent's public entity by id).
|
|
214
|
+
* Same opt-in + sysadmin-scoped read as `find`/`findPaginated`; the
|
|
215
|
+
* access-control WHERE clause is the authoritative tenancy gate.
|
|
216
|
+
*/
|
|
217
|
+
findById(id: string, orgId?: string, parentOrgId?: string): Promise<TEntity | null>;
|
|
218
|
+
/**
|
|
219
|
+
* Defense-in-depth tenant stamp. RLS is in owner-bypass mode, so the app layer
|
|
220
|
+
* is the only tenant gate on writes. A NON-sysadmin caller may only write into
|
|
221
|
+
* its OWN org, so pin the row to the trusted tenant-context org.
|
|
222
|
+
*
|
|
223
|
+
* The stamp fires whenever the payload's `orgId` differs from the context org —
|
|
224
|
+
* which covers BOTH a forged/mismatched `data.orgId` AND an ABSENT one. The
|
|
225
|
+
* absent case is the important tenant-boundary fix: the `org_id` column DEFAULTs
|
|
226
|
+
* to `SYSTEM_ORG_ID` (schema/pipeline.ts, schema/plugin.ts), so an insert that
|
|
227
|
+
* omits `orgId` would otherwise land the tenant's row in the public system
|
|
228
|
+
* catalog — a cross-tenant fail-open. Always injecting `ctx.orgId` closes it.
|
|
229
|
+
*
|
|
230
|
+
* Sysadmin (and out-of-context worker/system paths, where `getTenantContext()`
|
|
231
|
+
* is undefined) keep the supplied org, matching the RLS policy
|
|
232
|
+
* `current_is_sysadmin() OR org_id = current_org_id()`.
|
|
233
|
+
*/
|
|
234
|
+
protected enforceOrgId<T>(data: T, isCreate?: boolean): T;
|
|
235
|
+
/**
|
|
236
|
+
* Create a new entity
|
|
237
|
+
*/
|
|
238
|
+
create(data: TInsert, userId: string): Promise<TEntity>;
|
|
239
|
+
/**
|
|
240
|
+
* Update an existing entity
|
|
241
|
+
*/
|
|
242
|
+
update(id: string, data: Partial<TUpdate>, orgId: string, userId: string): Promise<TEntity | null>;
|
|
243
|
+
/**
|
|
244
|
+
* Delete an entity (soft delete by setting isActive = false)
|
|
245
|
+
*/
|
|
246
|
+
delete(id: string, orgId: string, userId: string): Promise<TEntity | null>;
|
|
247
|
+
/**
|
|
248
|
+
* Set an entity as the default for a project/organization scope.
|
|
249
|
+
* Marks all other entities as non-default, then sets the specified entity.
|
|
250
|
+
* Uses a transaction to ensure atomicity.
|
|
251
|
+
*/
|
|
252
|
+
setDefault(project: string, org: string, id: string, userId: string): Promise<TEntity>;
|
|
253
|
+
/**
|
|
254
|
+
* Update multiple entities matching filter
|
|
255
|
+
*/
|
|
256
|
+
updateMany(filter: Partial<TFilter>, data: Partial<TUpdate>, orgId: string, userId: string): Promise<TEntity[]>;
|
|
257
|
+
/**
|
|
258
|
+
* Create multiple entities in a single batch insert.
|
|
259
|
+
* Uses upsert (onConflictDoUpdate) — all rows are inserted in one query per chunk.
|
|
260
|
+
* Chunks of 100 to stay within PostgreSQL parameter limits.
|
|
261
|
+
*/
|
|
262
|
+
/**
|
|
263
|
+
* Build the `onConflictDoUpdate` SET for a bulk upsert. Unlike a single-row
|
|
264
|
+
* create (which can spread the one row's data), a batched insert has many rows,
|
|
265
|
+
* so each updatable column is written from `excluded.*` (the would-be-inserted
|
|
266
|
+
* value) — otherwise an existing row's fields are silently discarded and only
|
|
267
|
+
* timestamps bump. Skips identity/immutable (`id`, `createdAt`, `createdBy`)
|
|
268
|
+
* and the conflict-target columns; `updatedAt`/`updatedBy` get the fresh values.
|
|
269
|
+
*/
|
|
270
|
+
private buildBulkUpsertSet;
|
|
271
|
+
bulkCreate(items: TInsert[], userId: string): Promise<TEntity[]>;
|
|
272
|
+
/**
|
|
273
|
+
* Soft-delete multiple entities by IDs in a single batch operation.
|
|
274
|
+
*/
|
|
275
|
+
bulkDelete(ids: string[], orgId: string, userId: string,
|
|
276
|
+
/** When true, only PRIVATE records are deleted — mirrors the single-delete
|
|
277
|
+
* `requirePublicAccess` gate so a non-sysadmin can't bulk-delete public
|
|
278
|
+
* (shared/sysadmin-managed) records. No-op for entities without an
|
|
279
|
+
* `visibility` column. Callers pass `!isSystemAdmin(req)`. */
|
|
280
|
+
restrictToPrivate?: boolean): Promise<TEntity[]>;
|
|
281
|
+
/**
|
|
282
|
+
* Restore a soft-deleted (tombstoned) entity: clear `isActive`/`deletedAt`/
|
|
283
|
+
* `deletedBy`/`purgeAfter` so it reappears in normal reads.
|
|
284
|
+
*
|
|
285
|
+
* Matches ONLY a genuine tombstone — `isActive = false` AND `deletedAt IS NOT
|
|
286
|
+
* NULL` — so a merely *deactivated* row (isActive=false, no deletedAt) is never
|
|
287
|
+
* silently "restored". Pinned to the caller's own org (orgId-less sysadmin
|
|
288
|
+
* context spans orgs, matching delete/update). Returns null when there is no
|
|
289
|
+
* such tombstone (already active, purged, or unknown) so the route can 404.
|
|
290
|
+
* A unique-key collision (a live row already holds the key) surfaces as the
|
|
291
|
+
* driver's unique-violation for the route to map to 409 — in practice the
|
|
292
|
+
* tombstone IS the key holder, so this is defensive.
|
|
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;
|
|
299
|
+
restore(id: string, orgId: string, userId: string): Promise<TEntity | null>;
|
|
300
|
+
/**
|
|
301
|
+
* Read a soft-deleted entity by id (the inverse of the default `isActive=true`
|
|
302
|
+
* reads) — used by restore routes to load the tombstone for access-control
|
|
303
|
+
* gating before restoring. Returns null unless the row exists AND is a genuine
|
|
304
|
+
* tombstone (`isActive=false` + `deletedAt IS NOT NULL`).
|
|
305
|
+
*/
|
|
306
|
+
findDeletedById(id: string, orgId?: string): Promise<TEntity | null>;
|
|
307
|
+
/**
|
|
308
|
+
* List an org's soft-deleted tombstones (`isActive=false` + `deletedAt IS NOT
|
|
309
|
+
* NULL`), most-recently-deleted first — powers the "recently deleted" restore
|
|
310
|
+
* UI. Returns `[]` for entities without a soft-delete lifecycle. `limit` is
|
|
311
|
+
* clamped to [1, 200] to keep the response bounded (the retention sweep hard-
|
|
312
|
+
* deletes old tombstones anyway, so this is a short, self-limiting list).
|
|
313
|
+
*/
|
|
314
|
+
findDeleted(orgId: string, opts?: {
|
|
315
|
+
limit?: number;
|
|
316
|
+
offset?: number;
|
|
317
|
+
}): Promise<TEntity[]>;
|
|
318
|
+
/**
|
|
319
|
+
* Hard-delete a batch of tombstones whose purge deadline has passed
|
|
320
|
+
* (`deletedAt IS NOT NULL AND purge_after < now`). Batched (`limit`) so a
|
|
321
|
+
* sweep tick stays bounded; the per-service sweep loops until a tick returns
|
|
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.
|
|
326
|
+
* Returns the number of rows purged. No-op (0) for entities without the columns.
|
|
327
|
+
*/
|
|
328
|
+
purgeExpired(now: Date, limit?: number): Promise<number>;
|
|
329
|
+
/**
|
|
330
|
+
* On-demand hard-delete of a SINGLE tombstone by id — the manual counterpart
|
|
331
|
+
* to the retention sweep's `purgeExpired`. Permanently removes the row ONLY
|
|
332
|
+
* when it is a genuine tombstone (`isActive=false` + `deletedAt IS NOT NULL`),
|
|
333
|
+
* optionally pinned to `orgId` (an orgId-less sysadmin context spans orgs,
|
|
334
|
+
* matching restore/delete). Runs the same `onBeforePurge` dependent teardown
|
|
335
|
+
* inside the tx and the same `onAfterPurge` side-effects as the sweep.
|
|
336
|
+
*
|
|
337
|
+
* Ignores `purgeAfter`: a manual purge finalizes an already-soft-deleted item
|
|
338
|
+
* immediately rather than waiting for the retention deadline. Returns the
|
|
339
|
+
* purged id, or null when there is no such tombstone (already active, purged,
|
|
340
|
+
* out of scope, or unknown) so the route can 404.
|
|
341
|
+
*/
|
|
342
|
+
purgeById(id: string, orgId?: string): Promise<string | null>;
|
|
343
|
+
}
|