@manablox/db 0.2.0 → 0.4.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 (61) hide show
  1. package/dist/index-BLAkQMJT.d.ts +877 -0
  2. package/dist/index-DrNMGM9N.d.ts +5193 -0
  3. package/dist/index.d.ts +123 -0
  4. package/dist/index.js +60 -0
  5. package/dist/repositories-pz4NeWaF.js +1928 -0
  6. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  7. package/dist/schema-Dm3RcBst.js +648 -0
  8. package/dist/schema.d.ts +2 -0
  9. package/dist/schema.js +2 -0
  10. package/dist/testing.d.ts +77 -0
  11. package/dist/testing.js +217 -0
  12. package/migrations/0009_audit-log.sql +40 -0
  13. package/migrations/0010_notifications-approvals.sql +45 -0
  14. package/migrations/meta/0009_snapshot.json +3192 -0
  15. package/migrations/meta/0010_snapshot.json +3574 -0
  16. package/migrations/meta/_journal.json +14 -0
  17. package/package.json +18 -10
  18. package/drizzle.config.ts +0 -11
  19. package/src/bootstrap.ts +0 -13
  20. package/src/cli/create-db.ts +0 -30
  21. package/src/cli/migrate.ts +0 -17
  22. package/src/client.ts +0 -44
  23. package/src/columns.ts +0 -39
  24. package/src/errors.ts +0 -50
  25. package/src/index.ts +0 -19
  26. package/src/migrate.ts +0 -21
  27. package/src/pagination.ts +0 -52
  28. package/src/query.ts +0 -213
  29. package/src/repositories/asset-usage.ts +0 -166
  30. package/src/repositories/asset.ts +0 -181
  31. package/src/repositories/content-type.ts +0 -116
  32. package/src/repositories/content.ts +0 -811
  33. package/src/repositories/index.ts +0 -40
  34. package/src/repositories/menu.ts +0 -235
  35. package/src/repositories/role.ts +0 -85
  36. package/src/repositories/space.ts +0 -83
  37. package/src/repositories/user.ts +0 -280
  38. package/src/repositories/webhook.ts +0 -46
  39. package/src/repositories/workflow.ts +0 -306
  40. package/src/schema/assets.ts +0 -108
  41. package/src/schema/auth.ts +0 -166
  42. package/src/schema/content-types.ts +0 -31
  43. package/src/schema/content.ts +0 -133
  44. package/src/schema/index.ts +0 -38
  45. package/src/schema/menus.ts +0 -61
  46. package/src/schema/relations.ts +0 -64
  47. package/src/schema/spaces.ts +0 -20
  48. package/src/schema/webhooks.ts +0 -46
  49. package/src/schema/workflows.ts +0 -92
  50. package/src/testing-fixtures.ts +0 -139
  51. package/src/testing.ts +0 -105
  52. package/test/asset-usage.test.ts +0 -101
  53. package/test/menu.test.ts +0 -126
  54. package/test/publish.test.ts +0 -130
  55. package/test/query.test.ts +0 -170
  56. package/test/role.test.ts +0 -81
  57. package/test/tree.test.ts +0 -188
  58. package/test/user.test.ts +0 -126
  59. package/test/webhook.test.ts +0 -48
  60. package/tsconfig.json +0 -4
  61. package/vitest.config.ts +0 -10
@@ -0,0 +1,877 @@
1
+ import { T as webhooks, U as contents, W as publishedContents, _ as UserRow, a as ContentApprovalRow, b as index_d_exports, d as MenuItemRow, f as MenuRow, g as SpaceRow, h as RoleRow, i as AuditEntryRow, l as ContentVersionRow, m as PushSubscriptionRow, p as NotificationRow, r as AssetVariantRow, s as ContentRow, t as AssetRow, u as MembershipRow, v as WorkflowRow, w as webhookDeliveries, y as WorkflowRunRow } from "./index-DrNMGM9N.js";
2
+ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
+ import postgres from "postgres";
4
+ import { SQL, SQL as SQL$1 } from "drizzle-orm";
5
+ import { PgColumn, PgColumn as PgColumn$1, PgTable } from "drizzle-orm/pg-core";
6
+ import { ApprovalStatus, AuditAction, AuditActor, AuditActorKind, AuditChange, AuditSortField, AuditTargetKind, ContentStatus, ContentTypeDefinition, ContentTypeInput, ContentTypeRegistry, DatabaseConfig, FilterOperator, Loose, NotificationKind, NotificationPreferences, WorkflowCursor, WorkflowRunContext, WorkflowRunStatus, WorkflowSelection, WorkflowStep, WorkflowStepLog, WorkflowTrigger } from "@manablox/core";
7
+ //#region src/client.d.ts
8
+ type Database = PostgresJsDatabase<typeof index_d_exports>;
9
+ /** What `db.transaction((tx) => …)` hands its callback. */
10
+ type Transaction = Parameters<Parameters<Database['transaction']>[0]>[0];
11
+ /**
12
+ * Anything a query can run on. Repository internals take this so the same helper serves
13
+ * a call inside a transaction and one outside it, without casting `tx` to `Database`.
14
+ */
15
+ type Executor = Database | Transaction;
16
+ type Sql = ReturnType<typeof postgres>;
17
+ interface DatabaseHandle {
18
+ db: Database;
19
+ sql: Sql;
20
+ close: () => Promise<void>;
21
+ }
22
+ interface DatabaseOptions {
23
+ /** Called once per statement sent to Postgres. Used by tests to assert query counts. */
24
+ onQuery?: (query: string) => void;
25
+ }
26
+ declare function createDatabase(config: DatabaseConfig, options?: DatabaseOptions): DatabaseHandle;
27
+ //#endregion
28
+ //#region src/query.d.ts
29
+ /**
30
+ * Both content tables share a column set, so one filter builder serves the draft and
31
+ * the published projection. Typing the parameter as the union (rather than `PgTable`)
32
+ * keeps real column references, which is what lets Drizzle bind arrays and UUIDs
33
+ * correctly instead of stringifying them into the SQL text.
34
+ */
35
+ type ContentTable = typeof contents | typeof publishedContents;
36
+ /**
37
+ * Note on `?: T | undefined` throughout the input types in this file: the workspace runs
38
+ * with `exactOptionalPropertyTypes`, under which a bare `?:` accepts an absent property
39
+ * but rejects an explicit `undefined`. Validators (Zod, and any Standard Schema) produce
40
+ * exactly that explicit `undefined` for optional fields, so input DTOs widen while the
41
+ * domain types they feed stay strict.
42
+ */
43
+ interface FieldFilter {
44
+ /** Field machine name on the content type. */
45
+ name: string;
46
+ op: FilterOperator;
47
+ value?: unknown | undefined;
48
+ }
49
+ interface ContentFilter {
50
+ spaceId?: string | undefined;
51
+ /** Content type ids. */
52
+ typeIds?: string[] | undefined;
53
+ locale?: string | undefined;
54
+ status?: 'draft' | 'published' | 'archived' | undefined;
55
+ ids?: string[] | undefined;
56
+ parentId?: string | null | undefined;
57
+ /** Restrict to the subtree below this content id (inclusive of its descendants). */
58
+ under?: string | undefined;
59
+ slug?: string | undefined;
60
+ permalink?: string | undefined;
61
+ localizationId?: string | undefined;
62
+ /** Full-text query against `title` + field contributions. */
63
+ search?: string | undefined;
64
+ fields?: FieldFilter[] | undefined;
65
+ }
66
+ interface ContentSort {
67
+ by: 'position' | 'title' | 'createdAt' | 'updatedAt' | 'publishedAt' | 'slug';
68
+ direction: 'asc' | 'desc';
69
+ }
70
+ interface Pagination {
71
+ limit: number;
72
+ offset: number;
73
+ }
74
+ /**
75
+ * Translates a filter into SQL. Every field predicate is checked against the field
76
+ * type's declared `filters` list first: an unsupported operator is a 400, and every
77
+ * supported one has an index behind it.
78
+ */
79
+ declare function buildContentWhere(table: ContentTable, filter: ContentFilter, registry: ContentTypeRegistry): SQL | undefined;
80
+ declare function buildOrderBy(sorts: ContentSort[]): SQL;
81
+ //#endregion
82
+ //#region src/pagination.d.ts
83
+ interface Paginated<T> {
84
+ items: T[];
85
+ total: number;
86
+ limit: number;
87
+ offset: number;
88
+ }
89
+ /**
90
+ * One page of a table plus the total, in one round trip: a window `count(*) over ()`
91
+ * rides along with the rows. A page past the end comes back empty and so carries no
92
+ * count; only then is the total asked for separately, so a caller paging by `total`
93
+ * still learns the true size.
94
+ */
95
+ declare function paginate<TTable extends PgTable>(db: Executor, table: TTable, options: {
96
+ where?: SQL | undefined;
97
+ orderBy: SQL | PgColumn | (SQL | PgColumn)[];
98
+ pagination: Pagination;
99
+ }): Promise<Paginated<TTable['$inferSelect']>>;
100
+ //#endregion
101
+ //#region src/repositories/asset.d.ts
102
+ interface AssetWriteData {
103
+ id?: string | undefined;
104
+ spaceId: string;
105
+ driver: string;
106
+ key: string;
107
+ filename: string;
108
+ name: string;
109
+ mimeType: string;
110
+ size: number;
111
+ width?: number | null | undefined;
112
+ height?: number | null | undefined;
113
+ duration?: number | null | undefined;
114
+ checksum?: string | null | undefined;
115
+ alt?: string | null | undefined;
116
+ title?: string | null | undefined;
117
+ meta?: Record<string, unknown> | undefined;
118
+ actorId?: string | null | undefined;
119
+ }
120
+ interface AssetFilter {
121
+ spaceId: string;
122
+ /** Prefix match on the mime type, e.g. `image/`. */
123
+ mimeType?: string | undefined;
124
+ search?: string | undefined;
125
+ }
126
+ declare class AssetRepository {
127
+ private readonly db;
128
+ constructor(db: Database);
129
+ findById(id: string): Promise<AssetRow | null>;
130
+ /**
131
+ * `spaceId` is not an optimisation. On the public instance an asset id is the only
132
+ * thing a caller supplies, and without this predicate any id resolves — including one
133
+ * belonging to another tenant sharing the process.
134
+ */
135
+ findManyByIds(ids: string[], spaceId?: string | null): Promise<AssetRow[]>;
136
+ findByChecksum(spaceId: string, checksum: string): Promise<AssetRow | null>;
137
+ list(filter: AssetFilter, pagination: Pagination): Promise<Paginated<AssetRow>>;
138
+ create(data: AssetWriteData): Promise<AssetRow>;
139
+ update(id: string, data: Loose<Pick<AssetWriteData, 'name' | 'alt' | 'title' | 'meta'>>): Promise<AssetRow>;
140
+ delete(id: string): Promise<AssetRow | null>;
141
+ /** Timestamps an import carries over; `create()` can only stamp the moment of import. */
142
+ restoreTimestamps(id: string, createdAt: Date, updatedAt: Date): Promise<void>;
143
+ variants(assetIds: string[]): Promise<AssetVariantRow[]>;
144
+ findVariant(assetId: string, preset: string, format: string): Promise<AssetVariantRow | null>;
145
+ /** Drops every variant row; the caller removes the files. */
146
+ deleteVariants(assetId: string): Promise<void>;
147
+ addVariant(data: {
148
+ assetId: string;
149
+ preset: string;
150
+ format: string;
151
+ key: string;
152
+ width?: number | null;
153
+ height?: number | null;
154
+ size: number;
155
+ }): Promise<AssetVariantRow>;
156
+ }
157
+ //#endregion
158
+ //#region src/repositories/asset-usage.d.ts
159
+ /**
160
+ * The asset → document reachability index.
161
+ *
162
+ * `published` tracks the *published projection*, not the draft: a draft that adds an
163
+ * image does not make that image public, and a draft that removes one does not make it
164
+ * private until the change is published. Every method below preserves that distinction,
165
+ * which is why the column exists rather than the table simply holding published rows.
166
+ */
167
+ declare class AssetUsageRepository {
168
+ private readonly db;
169
+ constructor(db: Database);
170
+ /** The subset of `assetIds` reachable from at least one published document. */
171
+ filterPublished(assetIds: string[]): Promise<Set<string>>;
172
+ forContent(contentId: string): Promise<Array<{
173
+ assetId: string;
174
+ published: boolean;
175
+ }>>;
176
+ /**
177
+ * Records what a *draft* references.
178
+ *
179
+ * Rows the draft dropped are removed only if they are not currently published —
180
+ * otherwise editing a draft would silently revoke access to an image the live page is
181
+ * still showing.
182
+ */
183
+ recordDraft(contentId: string, spaceId: string, assetIds: string[]): Promise<void>;
184
+ /**
185
+ * Records what the published projection references.
186
+ *
187
+ * Assets the new revision no longer uses lose their published flag but keep their row
188
+ * when the draft still references them, so the admin's "where is this used" view stays
189
+ * complete.
190
+ */
191
+ recordPublished(contentId: string, spaceId: string, assetIds: string[]): Promise<void>;
192
+ /** Unpublishing revokes every asset this document was keeping public. */
193
+ clearPublished(contentId: string): Promise<void>;
194
+ deleteForContent(contentId: string): Promise<void>;
195
+ count(): Promise<number>;
196
+ /**
197
+ * Every document with its draft and published field values, for the backfill.
198
+ *
199
+ * References are derived from field-type definitions rather than stored, so the
200
+ * backfill cannot be a SQL migration — it has to run inside the application.
201
+ */
202
+ backfillSource(): Promise<Array<{
203
+ id: string;
204
+ spaceId: string;
205
+ typeId: string;
206
+ draftFields: Record<string, unknown>;
207
+ publishedFields: Record<string, unknown> | null;
208
+ }>>;
209
+ }
210
+ //#endregion
211
+ //#region src/repositories/audit.d.ts
212
+ interface AuditAppendInput {
213
+ /** The space the action concerns; omit for an instance-wide action. */
214
+ spaceId?: string | null | undefined;
215
+ /** Who did it; the current actor (see `runAsActor`) when omitted. */
216
+ actor?: AuditActor | undefined;
217
+ action: AuditAction;
218
+ targetKind: AuditTargetKind;
219
+ targetId?: string | null | undefined;
220
+ targetLabel?: string | null | undefined;
221
+ changes?: AuditChange[] | undefined;
222
+ meta?: Record<string, unknown> | null | undefined;
223
+ at?: Date | undefined;
224
+ }
225
+ interface AuditFilter {
226
+ /** A space's entries; `null` for instance-wide entries only; absent for every entry. */
227
+ spaceId?: string | null | undefined;
228
+ actorKind?: AuditActorKind | undefined;
229
+ actorId?: string | undefined;
230
+ actions?: AuditAction[] | undefined;
231
+ targetKind?: AuditTargetKind | undefined;
232
+ targetId?: string | undefined;
233
+ from?: Date | undefined;
234
+ to?: Date | undefined;
235
+ /** Matched against the actor's label, the target's label and the action. */
236
+ search?: string | undefined;
237
+ }
238
+ interface AuditSort {
239
+ by: AuditSortField;
240
+ direction: 'asc' | 'desc';
241
+ }
242
+ /** The outcome of walking the chain: where it broke, if it did. */
243
+ interface AuditVerification {
244
+ ok: boolean;
245
+ checked: number;
246
+ /** The first entry whose hash does not match, or whose `prevHash` is not its predecessor's. */
247
+ brokenAt: {
248
+ seq: number;
249
+ id: string;
250
+ reason: 'hash' | 'link';
251
+ } | null;
252
+ }
253
+ interface AuditRepositoryOptions {
254
+ /** Told when `record` could not write; the action itself is not undone. */
255
+ onError?: ((error: unknown, input: AuditAppendInput) => void) | undefined;
256
+ /**
257
+ * Told after every entry has been committed: the realtime feed listens here, because
258
+ * an audit entry is written for every change and only after it has happened. A
259
+ * listener that throws is reported to `onError`; the entry stays written.
260
+ */
261
+ onAppended?: ((row: AuditEntryRow) => void) | undefined;
262
+ }
263
+ /**
264
+ * Appends to and reads the audit log. There is no update and no delete: the table
265
+ * refuses both, so this class offers neither.
266
+ */
267
+ declare class AuditRepository {
268
+ private readonly db;
269
+ private readonly options;
270
+ constructor(db: Database, options?: AuditRepositoryOptions);
271
+ /**
272
+ * Writes one entry, chained to the last. Writers are serialised on a transaction-level
273
+ * advisory lock, so two entries can never both claim the same predecessor; the lock
274
+ * is held for one read and one insert.
275
+ */
276
+ append(input: AuditAppendInput): Promise<AuditEntryRow>;
277
+ private write;
278
+ /**
279
+ * `append` that never throws. The write it describes has already happened; failing
280
+ * the request now would report an error for something that succeeded, so the failure
281
+ * is reported to `onError` (the host logs it) and the caller carries on.
282
+ */
283
+ record(input: AuditAppendInput): Promise<AuditEntryRow | null>;
284
+ findById(id: string): Promise<AuditEntryRow | null>;
285
+ list(filter: AuditFilter, sort?: AuditSort, pagination?: Pagination): Promise<Paginated<AuditEntryRow>>;
286
+ /** The entries about one thing, oldest first: a document's history. */
287
+ forTarget(targetKind: AuditTargetKind, targetId: string, limit?: number): Promise<AuditEntryRow[]>;
288
+ /**
289
+ * Recomputes every hash from the first entry on and checks each link to its
290
+ * predecessor. The chain is one across the instance, so this walks all of it, in
291
+ * batches; on a large log it takes a while and that is the point.
292
+ */
293
+ verify(batchSize?: number): Promise<AuditVerification>;
294
+ count(filter?: AuditFilter): Promise<number>;
295
+ }
296
+ //#endregion
297
+ //#region src/repositories/content.d.ts
298
+ interface ContentWriteData {
299
+ id?: string | undefined;
300
+ spaceId: string;
301
+ typeId: string;
302
+ locale: string;
303
+ localizationId?: string | undefined;
304
+ parentId?: string | null | undefined;
305
+ title: string;
306
+ slug: string;
307
+ fields: Record<string, unknown>;
308
+ searchText?: string | undefined;
309
+ position?: number | undefined;
310
+ status?: ContentStatus | undefined;
311
+ /** Whether the content type contributes its slug to descendants' permalinks. */
312
+ hasSlug: boolean;
313
+ actorId?: string | null | undefined;
314
+ /** Expected current version; a mismatch raises `content.version.conflict`. */
315
+ expectedVersion?: number | undefined;
316
+ }
317
+ interface TreeNode {
318
+ content: ContentRow;
319
+ depth: number;
320
+ children: TreeNode[];
321
+ }
322
+ declare class ContentRepository {
323
+ private readonly db;
324
+ private readonly registry;
325
+ constructor(db: Executor, registry: ContentTypeRegistry);
326
+ findById(id: string, published?: boolean): Promise<ContentRow | null>;
327
+ /**
328
+ * `spaceId` bounds a lookup by id to one tenant.
329
+ *
330
+ * The delivery API takes ids straight from the caller, so without it a public instance
331
+ * pinned to one space still answers for any other space's published documents.
332
+ */
333
+ findManyByIds(ids: string[], published?: boolean, spaceId?: string | null): Promise<ContentRow[]>;
334
+ /** Children of many parents in one query, for the tree loader. */
335
+ findChildrenOf(parentIds: string[], published?: boolean, spaceId?: string | null): Promise<ContentRow[]>;
336
+ findByPermalink(spaceId: string, locale: string, permalink: string, published?: boolean): Promise<ContentRow | null>;
337
+ /**
338
+ * The document a space nominates as its home, in one locale.
339
+ *
340
+ * `settings.homeContentId` names a single row, which belongs to one locale. Every
341
+ * translation of that document shares its `localizationId`, so the requested locale is
342
+ * resolved through that rather than by pinning one row per language.
343
+ */
344
+ findHome(spaceId: string, locale: string, published?: boolean): Promise<ContentRow | null>;
345
+ /**
346
+ * Every other row in a document's localization group — its translations.
347
+ */
348
+ /**
349
+ * One document per localization group, for checking that several groups exist in a
350
+ * space at once — a menu's entries, say. The locale returned is whichever sorts first;
351
+ * a caller that needs a particular one asks `localizationSiblings` for that group.
352
+ */
353
+ findByLocalizationIds(spaceId: string, localizationIds: string[]): Promise<ContentRow[]>;
354
+ localizationSiblings(spaceId: string, localizationId: string, excludeId?: string): Promise<ContentRow[]>;
355
+ /**
356
+ * Merges a few field values into rows without touching the rest of the document.
357
+ *
358
+ * Used to carry a non-localized field across a document's translations: a jsonb `||`
359
+ * so concurrent edits to *other* fields on those rows are not clobbered.
360
+ */
361
+ patchFields(ids: string[], patch: Record<string, unknown>): Promise<void>;
362
+ list(filter: ContentFilter, pagination: Pagination, sorts?: ContentSort[], published?: boolean): Promise<Paginated<ContentRow>>;
363
+ /**
364
+ * The whole tree below `rootId` in **one** query: a GiST-indexed `path <@ root`
365
+ * returns every descendant, and `nlevel()` gives the depth to rebuild the hierarchy.
366
+ */
367
+ tree(spaceId: string, locale: string, rootId?: string | null, maxDepth?: number, published?: boolean): Promise<TreeNode[]>;
368
+ /** Ancestors of a node, root first — read straight off the materialised path. */
369
+ ancestors(id: string, published?: boolean): Promise<ContentRow[]>;
370
+ create(data: ContentWriteData): Promise<ContentRow>;
371
+ update(id: string, data: ContentWriteData): Promise<ContentRow>;
372
+ /**
373
+ * Reparents a subtree with one statement. `subpath(path, nlevel(:oldPath))` is the part
374
+ * of each descendant's path *below* the moved node; prefixing it with the node's new
375
+ * path rebases the whole subtree.
376
+ */
377
+ /**
378
+ * Reparents and reorders a node in one transaction.
379
+ *
380
+ * Separate from `update` because a drag is a structural change, not an edit: it writes
381
+ * no field values, takes no version bump and records no snapshot, so an editor open on
382
+ * the document does not hit a version conflict because someone reordered the tree.
383
+ *
384
+ * `position` is the index among the destination's children, clamped to the ends.
385
+ * Siblings on both sides are renumbered densely afterwards, so positions never drift
386
+ * into ties that the tree's `position asc, title asc` ordering would resolve by name.
387
+ */
388
+ move(id: string, parentId: string | null, position: number): Promise<ContentRow>;
389
+ /**
390
+ * Writes `position = index` for a whole sibling list in one statement: the ids and
391
+ * their new positions travel as two arrays and are joined by `unnest`, so a drag in a
392
+ * forty-child section costs one round trip rather than forty. Rows already in place
393
+ * are left untouched, so their `updated_at` and version do not move either.
394
+ */
395
+ private renumber;
396
+ private moveSubtree;
397
+ /**
398
+ * Recomputes permalinks for a node and everything beneath it in one recursive CTE.
399
+ * Each level derives from *its own* parent's freshly computed value (`t.pl`), and
400
+ * `concat_ws` drops NULL segments so a type without a slug is transparent in the path.
401
+ */
402
+ private recomputePermalinks;
403
+ /** Deletes a node and its whole subtree, in both the draft and published tables. */
404
+ delete(id: string): Promise<number>;
405
+ /**
406
+ * Copies a draft into the delivery projection inside one transaction, so a reader
407
+ * never observes a partially published tree.
408
+ */
409
+ publish(id: string, actorId?: string | null): Promise<ContentRow>;
410
+ unpublish(id: string): Promise<void>;
411
+ versions(contentId: string, limit?: number): Promise<Array<{
412
+ version: number;
413
+ createdAt: Date;
414
+ createdBy: string | null;
415
+ label: string | null;
416
+ }>>;
417
+ /** The row exactly as it was at that version — `snapshot()` stores the whole row. */
418
+ versionSnapshot(contentId: string, version: number): Promise<ContentRow | null>;
419
+ /** Every stored version of these documents, oldest first, for a space export. */
420
+ history(contentIds: string[]): Promise<ContentVersionRow[]>;
421
+ /**
422
+ * Puts an imported document back the way its source had it: the version counter and
423
+ * timestamps `create()` had to invent, and the version history in place of the single
424
+ * snapshot `create()` wrote. The row's own columns are left alone otherwise.
425
+ */
426
+ restoreHistory(id: string, data: {
427
+ version: number;
428
+ createdAt: Date;
429
+ updatedAt: Date;
430
+ versions: Array<{
431
+ version: number;
432
+ label: string | null;
433
+ snapshot: Record<string, unknown>;
434
+ createdAt: Date;
435
+ createdBy: string | null;
436
+ }>;
437
+ }): Promise<void>;
438
+ private snapshot;
439
+ private findRow;
440
+ private lockRow;
441
+ private parentPath;
442
+ private parentPermalinkPath;
443
+ /** Guards against making a node its own ancestor, which would orphan the subtree. */
444
+ private assertNotOwnDescendant;
445
+ }
446
+ declare function buildTree(rows: Array<ContentRow & {
447
+ depth: number;
448
+ }>, rootId: string | null): TreeNode[];
449
+ //#endregion
450
+ //#region src/repositories/content-approval.d.ts
451
+ interface ApprovalRequestData {
452
+ spaceId: string;
453
+ contentId: string;
454
+ typeId: string;
455
+ requestedBy: string | null;
456
+ requestedByLabel: string;
457
+ requestNote?: string | null | undefined;
458
+ contentVersion?: number | null | undefined;
459
+ }
460
+ interface ApprovalDecisionData {
461
+ status: Exclude<ApprovalStatus, 'pending'>;
462
+ decidedBy: string | null;
463
+ decidedByLabel: string | null;
464
+ decisionNote?: string | null | undefined;
465
+ }
466
+ /** A pending request with the document it concerns, for the reviewers' queue. */
467
+ interface PendingApproval extends ContentApprovalRow {
468
+ content: {
469
+ id: string;
470
+ title: string;
471
+ locale: string;
472
+ typeId: string;
473
+ updatedAt: Date;
474
+ };
475
+ }
476
+ /**
477
+ * The approval history of documents. Rows are appended when someone asks and closed
478
+ * when someone decides; the "one pending per document" rule is the service's, this
479
+ * only offers the reads it needs to keep it.
480
+ */
481
+ declare class ContentApprovalRepository {
482
+ private readonly db;
483
+ constructor(db: Database);
484
+ findById(id: string): Promise<ContentApprovalRow | null>;
485
+ /** The open request on a document, if there is one. */
486
+ pendingFor(contentId: string): Promise<ContentApprovalRow | null>;
487
+ /** The most recent request on a document, whatever became of it. */
488
+ latestFor(contentId: string): Promise<ContentApprovalRow | null>;
489
+ historyFor(contentId: string, limit?: number): Promise<ContentApprovalRow[]>;
490
+ /**
491
+ * Every open request in a space, oldest first, with the document beside it. Narrowed
492
+ * to `typeIds` when the reader may only publish some types; `null` means every type.
493
+ */
494
+ pendingInSpace(spaceId: string, typeIds?: string[] | null): Promise<PendingApproval[]>;
495
+ request(data: ApprovalRequestData): Promise<ContentApprovalRow>;
496
+ decide(id: string, data: ApprovalDecisionData): Promise<ContentApprovalRow>;
497
+ }
498
+ //#endregion
499
+ //#region src/repositories/content-type.d.ts
500
+ /**
501
+ * Persistence for *runtime-defined* content types only. Code-defined types come from
502
+ * `manablox.config.ts` and are never written here — the registry merges both into one
503
+ * shape, and `source` tells the admin which are read-only.
504
+ */
505
+ declare class ContentTypeRepository {
506
+ private readonly db;
507
+ constructor(db: Database);
508
+ all(): Promise<ContentTypeDefinition[]>;
509
+ findById(id: string): Promise<ContentTypeDefinition | null>;
510
+ create(input: ContentTypeInput): Promise<ContentTypeDefinition>;
511
+ update(id: string, input: ContentTypeInput): Promise<ContentTypeDefinition>;
512
+ delete(id: string): Promise<void>;
513
+ }
514
+ //#endregion
515
+ //#region src/repositories/menu.d.ts
516
+ interface MenuWriteData {
517
+ id?: string | undefined;
518
+ spaceId: string;
519
+ name: string;
520
+ machineName: string;
521
+ description?: string | null | undefined;
522
+ }
523
+ /**
524
+ * One entry as the admin hands over the whole menu: a content entry names a
525
+ * `localizationId`, a link entry a `url`; either may carry a label. `id` is kept when
526
+ * given, so an unchanged entry keeps its identity across saves.
527
+ */
528
+ interface MenuItemInput {
529
+ id?: string | undefined;
530
+ localizationId?: string | null | undefined;
531
+ label?: string | null | undefined;
532
+ url?: string | null | undefined;
533
+ children?: MenuItemInput[] | undefined;
534
+ }
535
+ interface MenuItemNode {
536
+ item: MenuItemRow;
537
+ children: MenuItemNode[];
538
+ }
539
+ /** An entry with its document looked up for one locale; `content` is null for a link. */
540
+ interface ResolvedMenuItem {
541
+ id: string;
542
+ label: string | null;
543
+ url: string | null;
544
+ localizationId: string | null;
545
+ content: ContentRow | null;
546
+ children: ResolvedMenuItem[];
547
+ }
548
+ declare class MenuRepository {
549
+ private readonly db;
550
+ constructor(db: Database);
551
+ listBySpace(spaceId: string): Promise<MenuRow[]>;
552
+ findById(id: string): Promise<MenuRow | null>;
553
+ findByMachineName(spaceId: string, machineName: string): Promise<MenuRow | null>;
554
+ create(data: MenuWriteData): Promise<MenuRow>;
555
+ update(id: string, data: Loose<Omit<MenuWriteData, 'spaceId'>>): Promise<MenuRow>;
556
+ delete(id: string): Promise<void>;
557
+ /** Every entry of a menu, flat, in tree order within each level. */
558
+ items(menuId: string): Promise<MenuItemRow[]>;
559
+ tree(menuId: string): Promise<MenuItemNode[]>;
560
+ /**
561
+ * Replaces the whole entry tree in one transaction.
562
+ *
563
+ * A menu is edited as one document and saved as one, so this is simpler and safer than
564
+ * a per-entry API whose partial failures would leave a half-reordered menu behind.
565
+ */
566
+ setItems(menuId: string, tree: MenuItemInput[]): Promise<MenuItemNode[]>;
567
+ /** Menus that carry the document, for the editor's "used in" hint. */
568
+ menusReferencing(spaceId: string, localizationId: string): Promise<MenuRow[]>;
569
+ /** Drops every entry pointing at a document, in every menu; sub-entries cascade. */
570
+ removeContent(localizationId: string): Promise<number>;
571
+ /**
572
+ * The tree with each content entry's document for one locale. A content entry whose
573
+ * document has no row in that locale — or, on the published table, no published one —
574
+ * comes back with `content: null`; the caller decides whether to show or drop it.
575
+ */
576
+ resolve(menu: MenuRow, locale: string, published?: boolean): Promise<ResolvedMenuItem[]>;
577
+ }
578
+ //#endregion
579
+ //#region src/repositories/notification.d.ts
580
+ interface NotificationInsert {
581
+ userId: string;
582
+ spaceId?: string | null | undefined;
583
+ kind: NotificationKind;
584
+ title: string;
585
+ body?: string | undefined;
586
+ url?: string | null | undefined;
587
+ targetKind?: string | null | undefined;
588
+ targetId?: string | null | undefined;
589
+ actorId?: string | null | undefined;
590
+ actorLabel?: string | null | undefined;
591
+ meta?: Record<string, unknown> | null | undefined;
592
+ }
593
+ interface NotificationFilter {
594
+ unreadOnly?: boolean | undefined;
595
+ kinds?: NotificationKind[] | undefined;
596
+ spaceId?: string | null | undefined;
597
+ }
598
+ /**
599
+ * One person's inbox. Every method is scoped to a `userId`, so a row id from another
600
+ * account is inert: there is no way to read or change what is not yours.
601
+ */
602
+ declare class NotificationRepository {
603
+ private readonly db;
604
+ constructor(db: Database);
605
+ /** Inserts one row per recipient in one statement; an empty list writes nothing. */
606
+ insertMany(rows: NotificationInsert[]): Promise<NotificationRow[]>;
607
+ list(userId: string, filter?: NotificationFilter, pagination?: Pagination): Promise<Paginated<NotificationRow>>;
608
+ findById(userId: string, id: string): Promise<NotificationRow | null>;
609
+ countUnread(userId: string): Promise<number>;
610
+ /** Marks the named rows read; rows already read keep their original time. Returns how many changed. */
611
+ markRead(userId: string, ids: string[]): Promise<number>;
612
+ markUnread(userId: string, ids: string[]): Promise<number>;
613
+ markAllRead(userId: string): Promise<number>;
614
+ delete(userId: string, ids: string[]): Promise<number>;
615
+ deleteRead(userId: string): Promise<number>;
616
+ }
617
+ //#endregion
618
+ //#region src/repositories/role.d.ts
619
+ interface RoleWriteData {
620
+ /** Kept when given, so an import can restore a role under the id its memberships name. */
621
+ id?: string | undefined;
622
+ name: string;
623
+ machineName: string;
624
+ description?: string | null;
625
+ permissions: string[];
626
+ }
627
+ declare class RoleRepository {
628
+ private readonly db;
629
+ constructor(db: Database);
630
+ listBySpace(spaceId: string): Promise<RoleRow[]>;
631
+ findById(id: string): Promise<RoleRow | null>;
632
+ findByMachineName(spaceId: string, machineName: string): Promise<RoleRow | null>;
633
+ create(spaceId: string, data: RoleWriteData): Promise<RoleRow>;
634
+ update(id: string, data: Partial<RoleWriteData>): Promise<RoleRow>;
635
+ delete(id: string): Promise<void>;
636
+ /** How many members of the role's space hold it, by name. */
637
+ countMembers(spaceId: string, machineName: string): Promise<number>;
638
+ /**
639
+ * Drops every grant narrowed to a content type from every role, once the type is
640
+ * gone. Grants are a JSON array, so this is one statement across the roles that carry
641
+ * such a grant rather than a read-modify-write per role.
642
+ */
643
+ pruneContentType(typeId: string): Promise<void>;
644
+ }
645
+ //#endregion
646
+ //#region src/repositories/space.d.ts
647
+ interface SpaceWriteData {
648
+ id?: string | undefined;
649
+ name: string;
650
+ machineName: string;
651
+ description?: string | null | undefined;
652
+ url: string;
653
+ defaultLocale?: string | undefined;
654
+ locales?: string[] | undefined;
655
+ settings?: Record<string, unknown> | undefined;
656
+ }
657
+ declare class SpaceRepository {
658
+ private readonly db;
659
+ constructor(db: Database);
660
+ all(): Promise<SpaceRow[]>;
661
+ findManyByIds(ids: string[]): Promise<SpaceRow[]>;
662
+ findById(id: string): Promise<SpaceRow | null>;
663
+ findByMachineName(machineName: string): Promise<SpaceRow | null>;
664
+ create(data: SpaceWriteData): Promise<SpaceRow>;
665
+ update(id: string, data: Loose<SpaceWriteData>): Promise<SpaceRow>;
666
+ delete(id: string): Promise<void>;
667
+ }
668
+ //#endregion
669
+ //#region src/repositories/user.d.ts
670
+ /** The name of a role in a space: one of the built-in five, or a row in `roles`. */
671
+ type SpaceRole = string;
672
+ /** What it takes to create an account that can sign in with a password. */
673
+ interface UserCreateData {
674
+ name: string;
675
+ email: string;
676
+ role: string;
677
+ /** Already hashed; the repository never sees a plaintext password. */
678
+ passwordHash: string;
679
+ }
680
+ interface UserUpdateData {
681
+ name?: string;
682
+ email?: string;
683
+ }
684
+ declare class UserRepository {
685
+ private readonly db;
686
+ constructor(db: Database);
687
+ findById(id: string): Promise<UserRow | null>;
688
+ findManyByIds(ids: string[]): Promise<UserRow[]>;
689
+ /** Every account holding an instance role, for a fan-out to the superadmins. */
690
+ findByRole(role: string): Promise<UserRow[]>;
691
+ findByEmail(email: string): Promise<UserRow | null>;
692
+ list(pagination: Pagination, search?: string): Promise<Paginated<UserRow>>;
693
+ /**
694
+ * Users who are not members of a space, matching a search, newest first: the
695
+ * add-member picker's candidates, decided in SQL rather than by loading a page of
696
+ * users and filtering it here.
697
+ */
698
+ candidates(spaceId: string, search: string | undefined, limit: number): Promise<UserRow[]>;
699
+ count(): Promise<number>;
700
+ /**
701
+ * Inserts the user and its password credential together, so a failure on the second
702
+ * row cannot leave an account nobody can sign in to. The account row is shaped the way
703
+ * better-auth writes it on sign-up, so a sign-in later finds it as its own.
704
+ */
705
+ create(data: UserCreateData): Promise<UserRow>;
706
+ update(id: string, data: UserUpdateData): Promise<UserRow>;
707
+ delete(id: string): Promise<void>;
708
+ setBanned(id: string, banned: boolean, reason: string | null): Promise<UserRow>;
709
+ /**
710
+ * Replaces the password credential, creating it for an account that only ever signed
711
+ * in through another provider.
712
+ */
713
+ setPasswordHash(userId: string, passwordHash: string): Promise<void>;
714
+ /** Signs the user out everywhere. */
715
+ revokeSessions(userId: string): Promise<void>;
716
+ countByRole(role: string): Promise<number>;
717
+ setNotificationPreferences(id: string, preferences: NotificationPreferences): Promise<UserRow>;
718
+ /** The stored hash of the password credential, or `null` for an account without one. */
719
+ passwordHash(userId: string): Promise<string | null>;
720
+ setRole(id: string, role: string): Promise<UserRow>;
721
+ /**
722
+ * Authoritative role plus space memberships in one query.
723
+ *
724
+ * Read on every authenticated request rather than trusting the role embedded in the
725
+ * session: better-auth caches the session payload (five minutes by default), so a
726
+ * promotion or demotion would otherwise not take effect until that cache expired.
727
+ *
728
+ * A membership naming a custom role joins that role's grants; one naming a built-in
729
+ * role has none here, and the auth package answers those from its own table.
730
+ */
731
+ principal(userId: string): Promise<{
732
+ role: string;
733
+ banned: boolean;
734
+ spaces: Record<string, SpaceRole>;
735
+ permissions: Record<string, string[]>;
736
+ } | null>;
737
+ memberships(userId: string): Promise<MembershipRow[]>;
738
+ /** The user's memberships with the space each one is in, for a per-user view. */
739
+ membershipsWithSpaces(userId: string): Promise<Array<MembershipRow & {
740
+ space: SpaceRow;
741
+ }>>;
742
+ membersOf(spaceId: string): Promise<Array<MembershipRow & {
743
+ user: UserRow;
744
+ }>>;
745
+ roleIn(userId: string, spaceId: string): Promise<SpaceRole | null>;
746
+ grant(userId: string, spaceId: string, role: SpaceRole): Promise<void>;
747
+ revoke(userId: string, spaceId: string): Promise<void>;
748
+ }
749
+ //#endregion
750
+ //#region src/repositories/webhook.d.ts
751
+ type WebhookRow = typeof webhooks.$inferSelect;
752
+ type WebhookDeliveryRow = typeof webhookDeliveries.$inferSelect;
753
+ interface WebhookWriteData {
754
+ id?: string | undefined;
755
+ spaceId: string;
756
+ name: string;
757
+ url: string;
758
+ secret?: string | null | undefined;
759
+ events: string[];
760
+ enabled?: boolean | undefined;
761
+ }
762
+ interface WebhookDeliveryData {
763
+ webhookId: string;
764
+ event: string;
765
+ payload: Record<string, unknown>;
766
+ status: number | null;
767
+ error: string | null;
768
+ }
769
+ /** The webhooks of a space and the log of what was sent to them. */
770
+ declare class WebhookRepository {
771
+ private readonly db;
772
+ constructor(db: Database);
773
+ findById(id: string): Promise<WebhookRow | null>;
774
+ listBySpace(spaceId: string): Promise<WebhookRow[]>;
775
+ create(data: WebhookWriteData): Promise<WebhookRow>;
776
+ /** The switched-on webhooks of a space, for fanning an event out. */
777
+ findEnabled(spaceId: string): Promise<WebhookRow[]>;
778
+ recordDelivery(data: WebhookDeliveryData): Promise<WebhookDeliveryRow>;
779
+ deliveries(webhookId: string, limit?: number): Promise<WebhookDeliveryRow[]>;
780
+ }
781
+ //#endregion
782
+ //#region src/repositories/workflow.d.ts
783
+ interface WorkflowWriteData {
784
+ id?: string | undefined;
785
+ spaceId: string;
786
+ name: string;
787
+ description?: string | null | undefined;
788
+ enabled?: boolean | undefined;
789
+ trigger: WorkflowTrigger;
790
+ steps: WorkflowStep[];
791
+ }
792
+ interface WorkflowRunCreateData {
793
+ workflowId: string;
794
+ spaceId: string;
795
+ trigger: string;
796
+ context: WorkflowRunContext;
797
+ }
798
+ /** How many runs a workflow keeps; older ones are pruned as new ones are written. */
799
+ declare const RUNS_KEPT_PER_WORKFLOW = 200;
800
+ declare class WorkflowRepository {
801
+ private readonly db;
802
+ constructor(db: Database);
803
+ listBySpace(spaceId: string): Promise<WorkflowRow[]>;
804
+ /** Every enabled workflow of a space, for the dispatcher; every enabled one at all for the scheduler. */
805
+ listEnabled(spaceId?: string): Promise<WorkflowRow[]>;
806
+ findById(id: string): Promise<WorkflowRow | null>;
807
+ create(data: WorkflowWriteData): Promise<WorkflowRow>;
808
+ update(id: string, data: Loose<Omit<WorkflowWriteData, 'spaceId'>>): Promise<WorkflowRow>;
809
+ delete(id: string): Promise<void>;
810
+ /**
811
+ * Claims a scheduled workflow for one minute. Returns false when another process got
812
+ * there first — the update matches nothing once `lastScheduledAt` is already `minute`.
813
+ */
814
+ claimSchedule(id: string, minute: Date): Promise<boolean>;
815
+ touchRun(id: string, at: Date): Promise<void>;
816
+ createRun(data: WorkflowRunCreateData): Promise<WorkflowRunRow>;
817
+ findRun(id: string): Promise<WorkflowRunRow | null>;
818
+ listRuns(workflowId: string, limit?: number): Promise<WorkflowRunRow[]>;
819
+ /**
820
+ * Moves a run from `queued` or `waiting` to `running`, or reports that it is not
821
+ * there to be moved. The status check in the predicate is what keeps two workers off
822
+ * the same run.
823
+ */
824
+ claimRun(id: string): Promise<WorkflowRunRow | null>;
825
+ /** Runs paused by a delay step whose time has come. */
826
+ dueRuns(now: Date, limit?: number): Promise<WorkflowRunRow[]>;
827
+ saveRunProgress(id: string, data: {
828
+ status: WorkflowRunStatus;
829
+ cursor: WorkflowCursor;
830
+ log: WorkflowStepLog[];
831
+ error?: string | null | undefined;
832
+ resumeAt?: Date | null | undefined;
833
+ finished?: boolean | undefined;
834
+ }): Promise<void>;
835
+ private pruneRuns;
836
+ /** The documents a scheduled workflow's selection names, newest change first. */
837
+ selectDocuments(spaceId: string, selection: WorkflowSelection, limit?: number): Promise<ContentRow[]>;
838
+ subscriptionsFor(userIds: string[]): Promise<PushSubscriptionRow[]>;
839
+ subscriptionsOf(userId: string): Promise<PushSubscriptionRow[]>;
840
+ /** Upserts on the endpoint: a browser re-subscribing keeps one row, not two. */
841
+ subscribe(data: {
842
+ userId: string;
843
+ endpoint: string;
844
+ keys: {
845
+ p256dh: string;
846
+ auth: string;
847
+ };
848
+ userAgent: string | null;
849
+ }): Promise<PushSubscriptionRow>;
850
+ unsubscribe(userId: string, endpoint: string): Promise<void>;
851
+ /** A push service answered 404/410: the browser is gone, and so is the row. */
852
+ dropSubscription(id: string): Promise<void>;
853
+ markSubscriptionUsed(id: string): Promise<void>;
854
+ }
855
+ //#endregion
856
+ //#region src/repositories/index.d.ts
857
+ interface Repositories {
858
+ content: ContentRepository;
859
+ contentTypes: ContentTypeRepository;
860
+ spaces: SpaceRepository;
861
+ assets: AssetRepository;
862
+ assetUsages: AssetUsageRepository;
863
+ users: UserRepository;
864
+ menus: MenuRepository;
865
+ roles: RoleRepository;
866
+ workflows: WorkflowRepository;
867
+ webhooks: WebhookRepository;
868
+ audit: AuditRepository;
869
+ notifications: NotificationRepository;
870
+ approvals: ContentApprovalRepository;
871
+ }
872
+ interface RepositoriesOptions {
873
+ audit?: AuditRepositoryOptions | undefined;
874
+ }
875
+ declare function createRepositories(db: Database, registry: ContentTypeRegistry, options?: RepositoriesOptions): Repositories;
876
+ //#endregion
877
+ export { Pagination as $, ContentApprovalRepository as A, AuditRepositoryOptions as B, MenuItemNode as C, ContentTypeRepository as D, ResolvedMenuItem as E, TreeNode as F, AssetRepository as G, AuditVerification as H, buildTree as I, paginate as J, AssetWriteData as K, AuditAppendInput as L, ContentRepository as M, ContentWriteData as N, ApprovalDecisionData as O, SQL$1 as P, FieldFilter as Q, AuditFilter as R, MenuItemInput as S, MenuWriteData as T, AssetUsageRepository as U, AuditSort as V, AssetFilter as W, ContentSort as X, ContentFilter as Y, ContentTable as Z, SpaceRepository as _, WorkflowRepository as a, DatabaseOptions as at, NotificationInsert as b, WebhookDeliveryData as c, Transaction as ct, WebhookRow as d, PgColumn$1 as et, WebhookWriteData as f, UserUpdateData as g, UserRepository as h, RUNS_KEPT_PER_WORKFLOW as i, DatabaseHandle as it, PendingApproval as j, ApprovalRequestData as k, WebhookDeliveryRow as l, createDatabase as lt, UserCreateData as m, RepositoriesOptions as n, buildOrderBy as nt, WorkflowRunCreateData as o, Executor as ot, SpaceRole as p, Paginated as q, createRepositories as r, Database as rt, WorkflowWriteData as s, Sql as st, Repositories as t, buildContentWhere as tt, WebhookRepository as u, SpaceWriteData as v, MenuRepository as w, NotificationRepository as x, NotificationFilter as y, AuditRepository as z };