@manablox/db 0.3.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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as menusRelations, B as accounts, C as spaces, D as contentsRelations, E as assetsRelations, F as menus, G as users, H as memberships, I as contentTypes, J as assetVariants, K as verifications, L as contentVersions, M as spacesRelations, N as usersRelations, O as membershipsRelations, P as menuItems, R as contents, S as webhooks, T as assetVariantsRelations, U as roles, V as apikeys, W as sessions, Y as assets, _ as index_d_exports, a as ContentRow, b as workflows, c as MembershipRow, d as PushSubscriptionRow, f as RoleRow, g as WorkflowRunRow, h as WorkflowRow, i as ContentInsert, j as rolesRelations, k as menuItemsRelations, l as MenuItemRow, m as UserRow, n as AssetUsageRow, o as ContentTypeRow, p as SpaceRow, q as assetUsages, r as AssetVariantRow, s as ContentVersionRow, t as AssetRow, u as MenuRow, v as pushSubscriptions, w as assetUsagesRelations, x as webhookDeliveries, y as workflowRuns, z as publishedContents } from "./index-rZ24t-Ln.js";
2
- import { A as AssetRepository, B as buildContentWhere, C as ContentRepository, D as buildTree, E as TreeNode, F as ContentSort, G as Executor, H as Database, I as ContentTable, J as createDatabase, K as Sql, L as FieldFilter, M as Paginated, N as paginate, O as AssetUsageRepository, P as ContentFilter, R as Pagination, S as ContentTypeRepository, T as SQL, U as DatabaseHandle, V as buildOrderBy, W as DatabaseOptions, _ as MenuItemInput, a as WorkflowRunCreateData, b as MenuWriteData, c as WebhookDeliveryRow, d as SpaceRole, f as UserCreateData, g as SpaceWriteData, h as SpaceRepository, i as WorkflowRepository, j as AssetWriteData, k as AssetFilter, l as WebhookRepository, m as UserUpdateData, n as createRepositories, o as WorkflowWriteData, p as UserRepository, q as Transaction, r as RUNS_KEPT_PER_WORKFLOW, s as WebhookDeliveryData, t as Repositories, u as WebhookRow, v as MenuItemNode, w as ContentWriteData, x as ResolvedMenuItem, y as MenuRepository, z as PgColumn } from "./index-Cyf_N5K3.js";
1
+ import { $ as assetUsages, A as contentsRelations, B as menus, C as workflows, D as assetUsagesRelations, E as spaces, F as spacesRelations, G as accounts, H as contentVersions, I as usersRelations, J as roles, K as apikeys, L as contentApprovals, M as menuItemsRelations, N as menusRelations, O as assetVariantsRelations, P as rolesRelations, Q as auditEntries, R as notifications, S as workflowRuns, T as webhooks, U as contents, V as contentTypes, W as publishedContents, X as users, Y as sessions, Z as verifications, _ as UserRow, a as ContentApprovalRow, b as index_d_exports, c as ContentTypeRow, d as MenuItemRow, et as assetVariants, f as MenuRow, g as SpaceRow, h as RoleRow, i as AuditEntryRow, j as membershipsRelations, k as assetsRelations, l as ContentVersionRow, m as PushSubscriptionRow, n as AssetUsageRow, o as ContentInsert, p as NotificationRow, q as memberships, r as AssetVariantRow, s as ContentRow, t as AssetRow, tt as assets, u as MembershipRow, v as WorkflowRow, w as webhookDeliveries, x as pushSubscriptions, y as WorkflowRunRow, z as menuItems } from "./index-DrNMGM9N.js";
2
+ import { $ as Pagination, A as ContentApprovalRepository, B as AuditRepositoryOptions, C as MenuItemNode, D as ContentTypeRepository, E as ResolvedMenuItem, F as TreeNode, G as AssetRepository, H as AuditVerification, I as buildTree, J as paginate, K as AssetWriteData, L as AuditAppendInput, M as ContentRepository, N as ContentWriteData, O as ApprovalDecisionData, P as SQL, Q as FieldFilter, R as AuditFilter, S as MenuItemInput, T as MenuWriteData, U as AssetUsageRepository, V as AuditSort, W as AssetFilter, X as ContentSort, Y as ContentFilter, Z as ContentTable, _ as SpaceRepository, a as WorkflowRepository, at as DatabaseOptions, b as NotificationInsert, c as WebhookDeliveryData, ct as Transaction, d as WebhookRow, et as PgColumn, f as WebhookWriteData, g as UserUpdateData, h as UserRepository, i as RUNS_KEPT_PER_WORKFLOW, it as DatabaseHandle, j as PendingApproval, k as ApprovalRequestData, l as WebhookDeliveryRow, lt as createDatabase, m as UserCreateData, n as RepositoriesOptions, nt as buildOrderBy, o as WorkflowRunCreateData, ot as Executor, p as SpaceRole, q as Paginated, r as createRepositories, rt as Database, s as WorkflowWriteData, st as Sql, t as Repositories, tt as buildContentWhere, u as WebhookRepository, v as SpaceWriteData, w as MenuRepository, x as NotificationRepository, y as NotificationFilter, z as AuditRepository } from "./index-BLAkQMJT.js";
3
3
  import { ErrorKey } from "@manablox/core";
4
4
  //#region src/bootstrap.d.ts
5
5
  /**
@@ -120,4 +120,4 @@ export declare const MIGRATIONS_FOLDER: string;
120
120
  */
121
121
  export declare function runMigrations(handle: DatabaseHandle): Promise<void>;
122
122
  //#endregion
123
- export { AssetFilter, AssetRepository, AssetRow, AssetUsageRepository, AssetUsageRow, AssetVariantRow, AssetWriteData, ContentFilter, ContentInsert, ContentRepository, ContentRow, ContentSort, ContentTable, ContentTypeRepository, ContentTypeRow, ContentVersionRow, ContentWriteData, Database, DatabaseHandle, DatabaseOptions, Executor, FieldFilter, MembershipRow, MenuItemInput, MenuItemNode, MenuItemRow, MenuRepository, MenuRow, MenuWriteData, Paginated, Pagination, type PgColumn, PushSubscriptionRow, RUNS_KEPT_PER_WORKFLOW, Repositories, ResolvedMenuItem, RoleRow, type SQL, SpaceRepository, SpaceRole, SpaceRow, SpaceWriteData, Sql, Transaction, TreeNode, UserCreateData, UserRepository, UserRow, UserUpdateData, WebhookDeliveryData, WebhookDeliveryRow, WebhookRepository, WebhookRow, WorkflowRepository, WorkflowRow, WorkflowRunCreateData, WorkflowRunRow, WorkflowWriteData, accounts, apikeys, assetUsages, assetUsagesRelations, assetVariants, assetVariantsRelations, assets, assetsRelations, buildContentWhere, buildOrderBy, buildTree, contentTypes, contentVersions, contents, contentsRelations, createDatabase, createRepositories, memberships, membershipsRelations, menuItems, menuItemsRelations, menus, menusRelations, paginate, publishedContents, pushSubscriptions, roles, rolesRelations, index_d_exports as schema, sessions, spaces, spacesRelations, users, usersRelations, verifications, webhookDeliveries, webhooks, workflowRuns, workflows };
123
+ export { ApprovalDecisionData, ApprovalRequestData, AssetFilter, AssetRepository, AssetRow, AssetUsageRepository, AssetUsageRow, AssetVariantRow, AssetWriteData, AuditAppendInput, AuditEntryRow, AuditFilter, AuditRepository, AuditRepositoryOptions, AuditSort, AuditVerification, ContentApprovalRepository, ContentApprovalRow, ContentFilter, ContentInsert, ContentRepository, ContentRow, ContentSort, ContentTable, ContentTypeRepository, ContentTypeRow, ContentVersionRow, ContentWriteData, Database, DatabaseHandle, DatabaseOptions, Executor, FieldFilter, MembershipRow, MenuItemInput, MenuItemNode, MenuItemRow, MenuRepository, MenuRow, MenuWriteData, NotificationFilter, NotificationInsert, NotificationRepository, NotificationRow, Paginated, Pagination, PendingApproval, type PgColumn, PushSubscriptionRow, RUNS_KEPT_PER_WORKFLOW, Repositories, RepositoriesOptions, ResolvedMenuItem, RoleRow, type SQL, SpaceRepository, SpaceRole, SpaceRow, SpaceWriteData, Sql, Transaction, TreeNode, UserCreateData, UserRepository, UserRow, UserUpdateData, WebhookDeliveryData, WebhookDeliveryRow, WebhookRepository, WebhookRow, WebhookWriteData, WorkflowRepository, WorkflowRow, WorkflowRunCreateData, WorkflowRunRow, WorkflowWriteData, accounts, apikeys, assetUsages, assetUsagesRelations, assetVariants, assetVariantsRelations, assets, assetsRelations, auditEntries, buildContentWhere, buildOrderBy, buildTree, contentApprovals, contentTypes, contentVersions, contents, contentsRelations, createDatabase, createRepositories, memberships, membershipsRelations, menuItems, menuItemsRelations, menus, menusRelations, notifications, paginate, publishedContents, pushSubscriptions, roles, rolesRelations, index_d_exports as schema, sessions, spaces, spacesRelations, users, usersRelations, verifications, webhookDeliveries, webhooks, workflowRuns, workflows };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { _ as applyBootstrapSql, a as UserRepository, c as ContentTypeRepository, d as AssetUsageRepository, f as AssetRepository, g as createDatabase, h as paginate, i as WebhookRepository, l as ContentRepository, m as buildOrderBy, n as RUNS_KEPT_PER_WORKFLOW, o as SpaceRepository, p as buildContentWhere, r as WorkflowRepository, s as MenuRepository, t as createRepositories, u as buildTree } from "./repositories-DYjzuuF6.js";
2
- import { A as contentVersions, B as tsvector, C as roles, D as assetUsages, E as verifications, F as idToLabel, I as labelToId, L as ltree, M as publishedContents, N as spaces, O as assetVariants, P as buildPath, R as parsePath, S as memberships, T as users, _ as menuItems, a as webhookDeliveries, b as accounts, c as assetVariantsRelations, d as membershipsRelations, f as menuItemsRelations, g as usersRelations, h as spacesRelations, i as workflows, j as contents, k as assets, l as assetsRelations, m as rolesRelations, n as pushSubscriptions, o as webhooks, p as menusRelations, r as workflowRuns, s as assetUsagesRelations, t as schema_exports, u as contentsRelations, v as menus, w as sessions, x as apikeys, y as contentTypes, z as pathDepth } from "./schema-Bb4p16Yz.js";
1
+ import { _ as buildOrderBy, a as UserRepository, b as applyBootstrapSql, c as MenuRepository, d as ContentRepository, f as buildTree, g as buildContentWhere, h as AssetRepository, i as WebhookRepository, l as ContentTypeRepository, m as AssetUsageRepository, n as RUNS_KEPT_PER_WORKFLOW, o as SpaceRepository, p as AuditRepository, r as WorkflowRepository, s as NotificationRepository, t as createRepositories, u as ContentApprovalRepository, v as paginate, y as createDatabase } from "./repositories-pz4NeWaF.js";
2
+ import { A as assetUsages, B as ltree, C as apikeys, D as users, E as sessions, F as publishedContents, H as pathDepth, I as spaces, L as buildPath, M as assets, N as contentVersions, O as verifications, P as contents, R as idToLabel, S as accounts, T as roles, U as tsvector, V as parsePath, _ as contentApprovals, a as webhookDeliveries, b as menus, c as assetVariantsRelations, d as membershipsRelations, f as menuItemsRelations, g as usersRelations, h as spacesRelations, i as workflows, j as assetVariants, k as auditEntries, l as assetsRelations, m as rolesRelations, n as pushSubscriptions, o as webhooks, p as menusRelations, r as workflowRuns, s as assetUsagesRelations, t as schema_exports, u as contentsRelations, v as notifications, w as memberships, x as contentTypes, y as menuItems, z as labelToId } from "./schema-Dm3RcBst.js";
3
3
  import { ManabloxError } from "@manablox/core";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -57,4 +57,4 @@ async function runMigrations(handle) {
57
57
  await migrate(handle.db, { migrationsFolder: MIGRATIONS_FOLDER });
58
58
  }
59
59
  //#endregion
60
- export { AssetRepository, AssetUsageRepository, ContentRepository, ContentTypeRepository, MIGRATIONS_FOLDER, MenuRepository, RUNS_KEPT_PER_WORKFLOW, SpaceRepository, UserRepository, WebhookRepository, WorkflowRepository, accounts, apikeys, applyBootstrapSql, assetUsages, assetUsagesRelations, assetVariants, assetVariantsRelations, assets, assetsRelations, buildContentWhere, buildOrderBy, buildPath, buildTree, contentTypes, contentVersions, contents, contentsRelations, createDatabase, createRepositories, idToLabel, isUniqueViolation, labelToId, ltree, memberships, membershipsRelations, menuItems, menuItemsRelations, menus, menusRelations, paginate, parsePath, pathDepth, publishedContents, pushSubscriptions, rethrowUniqueViolation, roles, rolesRelations, runMigrations, schema_exports as schema, sessions, spaces, spacesRelations, tsvector, uniqueViolation, users, usersRelations, verifications, webhookDeliveries, webhooks, workflowRuns, workflows };
60
+ export { AssetRepository, AssetUsageRepository, AuditRepository, ContentApprovalRepository, ContentRepository, ContentTypeRepository, MIGRATIONS_FOLDER, MenuRepository, NotificationRepository, RUNS_KEPT_PER_WORKFLOW, SpaceRepository, UserRepository, WebhookRepository, WorkflowRepository, accounts, apikeys, applyBootstrapSql, assetUsages, assetUsagesRelations, assetVariants, assetVariantsRelations, assets, assetsRelations, auditEntries, buildContentWhere, buildOrderBy, buildPath, buildTree, contentApprovals, contentTypes, contentVersions, contents, contentsRelations, createDatabase, createRepositories, idToLabel, isUniqueViolation, labelToId, ltree, memberships, membershipsRelations, menuItems, menuItemsRelations, menus, menusRelations, notifications, paginate, parsePath, pathDepth, publishedContents, pushSubscriptions, rethrowUniqueViolation, roles, rolesRelations, runMigrations, schema_exports as schema, sessions, spaces, spacesRelations, tsvector, uniqueViolation, users, usersRelations, verifications, webhookDeliveries, webhooks, workflowRuns, workflows };
@@ -1,8 +1,8 @@
1
- import { A as contentVersions, C as roles, D as assetUsages, F as idToLabel, M as publishedContents, N as spaces, O as assetVariants, S as memberships, T as users, _ as menuItems, a as webhookDeliveries, b as accounts, i as workflows, j as contents, k as assets, n as pushSubscriptions, o as webhooks, r as workflowRuns, t as schema_exports, v as menus, w as sessions, y as contentTypes } from "./schema-Bb4p16Yz.js";
1
+ import { A as assetUsages, D as users, E as sessions, F as publishedContents, I as spaces, M as assets, N as contentVersions, P as contents, R as idToLabel, S as accounts, T as roles, _ as contentApprovals, a as webhookDeliveries, b as menus, i as workflows, j as assetVariants, k as auditEntries, n as pushSubscriptions, o as webhooks, r as workflowRuns, t as schema_exports, v as notifications, w as memberships, x as contentTypes, y as menuItems } from "./schema-Dm3RcBst.js";
2
2
  import { drizzle } from "drizzle-orm/postgres-js";
3
3
  import postgres from "postgres";
4
- import { and, asc, desc, eq, getTableColumns, gte, inArray, isNull, lt, lte, notInArray, or, sql } from "drizzle-orm";
5
- import { ManabloxError, defineContentType } from "@manablox/core";
4
+ import { and, asc, desc, eq, getTableColumns, gt, gte, ilike, inArray, isNull, lt, lte, notInArray, or, sql } from "drizzle-orm";
5
+ import { ManabloxError, currentActor, defineContentType, hashAuditEntry } from "@manablox/core";
6
6
  import { randomUUID } from "node:crypto";
7
7
  //#region src/bootstrap.ts
8
8
  /**
@@ -59,7 +59,7 @@ async function paginate(db, table, options) {
59
59
  }
60
60
  //#endregion
61
61
  //#region src/query.ts
62
- const SORT_COLUMNS = {
62
+ const SORT_COLUMNS$1 = {
63
63
  position: "position",
64
64
  title: "title",
65
65
  createdAt: "created_at",
@@ -101,9 +101,9 @@ function buildFieldCondition(table, filter, typeIds, registry) {
101
101
  case "lte": return sql`(${text})::numeric <= ${asNumber(filter.value)}`;
102
102
  case "gt": return sql`(${text})::numeric > ${asNumber(filter.value)}`;
103
103
  case "gte": return sql`(${text})::numeric >= ${asNumber(filter.value)}`;
104
- case "contains": return sql`${text} ilike ${`%${escapeLike(String(filter.value ?? ""))}%`}`;
105
- case "startsWith": return sql`${text} ilike ${`${escapeLike(String(filter.value ?? ""))}%`}`;
106
- case "endsWith": return sql`${text} ilike ${`%${escapeLike(String(filter.value ?? ""))}`}`;
104
+ case "contains": return sql`${text} ilike ${`%${escapeLike$1(String(filter.value ?? ""))}%`}`;
105
+ case "startsWith": return sql`${text} ilike ${`${escapeLike$1(String(filter.value ?? ""))}%`}`;
106
+ case "endsWith": return sql`${text} ilike ${`%${escapeLike$1(String(filter.value ?? ""))}`}`;
107
107
  case "isNull": return sql`(${path} is null or ${path} = 'null'::jsonb)`;
108
108
  case "isNotNull": return sql`(${path} is not null and ${path} <> 'null'::jsonb)`;
109
109
  default: {
@@ -128,12 +128,12 @@ function assertOperatorAllowed(filter, typeIds, registry) {
128
128
  function buildOrderBy(sorts) {
129
129
  if (sorts.length === 0) return sql`${sql.identifier("position")} asc, ${sql.identifier("created_at")} asc`;
130
130
  return sorts.map((sort) => {
131
- const column = SORT_COLUMNS[sort.by];
131
+ const column = SORT_COLUMNS$1[sort.by];
132
132
  if (!column) throw ManabloxError.badRequest("query.sort.unsupported", { by: sort.by });
133
133
  return sql`${sql.identifier(column)} ${sql.raw(sort.direction === "desc" ? "desc" : "asc")}`;
134
134
  }).reduce((acc, part) => sql`${acc}, ${part}`);
135
135
  }
136
- const escapeLike = (value) => value.replace(/[%_\\]/g, (c) => `\\${c}`);
136
+ const escapeLike$1 = (value) => value.replace(/[%_\\]/g, (c) => `\\${c}`);
137
137
  function asStringArray(value) {
138
138
  if (!Array.isArray(value)) throw ManabloxError.badRequest("query.value.expectedArray");
139
139
  return value.map((entry) => String(entry));
@@ -210,6 +210,13 @@ var AssetRepository = class {
210
210
  const [row] = await this.db.delete(assets).where(eq(assets.id, id)).returning();
211
211
  return row ?? null;
212
212
  }
213
+ /** Timestamps an import carries over; `create()` can only stamp the moment of import. */
214
+ async restoreTimestamps(id, createdAt, updatedAt) {
215
+ await this.db.update(assets).set({
216
+ createdAt,
217
+ updatedAt
218
+ }).where(eq(assets.id, id));
219
+ }
213
220
  async variants(assetIds) {
214
221
  if (assetIds.length === 0) return [];
215
222
  return this.db.select().from(assetVariants).where(inArray(assetVariants.assetId, assetIds));
@@ -359,6 +366,189 @@ var AssetUsageRepository = class {
359
366
  }
360
367
  };
361
368
  //#endregion
369
+ //#region src/repositories/audit.ts
370
+ const SORT_COLUMNS = {
371
+ at: auditEntries.at,
372
+ action: auditEntries.action,
373
+ actorLabel: auditEntries.actorLabel,
374
+ targetKind: auditEntries.targetKind,
375
+ targetLabel: auditEntries.targetLabel
376
+ };
377
+ const escapeLike = (value) => value.replace(/[%_\\]/g, (c) => `\\${c}`);
378
+ /**
379
+ * Appends to and reads the audit log. There is no update and no delete: the table
380
+ * refuses both, so this class offers neither.
381
+ */
382
+ var AuditRepository = class {
383
+ db;
384
+ options;
385
+ constructor(db, options = {}) {
386
+ this.db = db;
387
+ this.options = options;
388
+ }
389
+ /**
390
+ * Writes one entry, chained to the last. Writers are serialised on a transaction-level
391
+ * advisory lock, so two entries can never both claim the same predecessor; the lock
392
+ * is held for one read and one insert.
393
+ */
394
+ async append(input) {
395
+ const row = await this.write(input);
396
+ if (this.options.onAppended) try {
397
+ this.options.onAppended(row);
398
+ } catch (error) {
399
+ this.options.onError?.(error, input);
400
+ }
401
+ return row;
402
+ }
403
+ async write(input) {
404
+ const actor = input.actor ?? currentActor();
405
+ const at = input.at ?? /* @__PURE__ */ new Date();
406
+ const changes = input.changes ?? [];
407
+ const meta = input.meta ?? null;
408
+ const actorDetail = actor.detail ?? null;
409
+ return this.db.transaction(async (tx) => {
410
+ await tx.execute(sql`select pg_advisory_xact_lock(hashtext('audit_entries'))`);
411
+ const [last] = await tx.select({ hash: auditEntries.hash }).from(auditEntries).orderBy(desc(auditEntries.seq)).limit(1);
412
+ const prevHash = last?.hash ?? null;
413
+ const content = {
414
+ at,
415
+ spaceId: input.spaceId ?? null,
416
+ actorKind: actor.kind,
417
+ actorId: actor.id,
418
+ actorLabel: actor.label,
419
+ actorDetail,
420
+ action: input.action,
421
+ targetKind: input.targetKind,
422
+ targetId: input.targetId ?? null,
423
+ targetLabel: input.targetLabel ?? null,
424
+ changes,
425
+ meta,
426
+ prevHash
427
+ };
428
+ const [row] = await tx.insert(auditEntries).values({
429
+ ...content,
430
+ hash: hashAuditEntry(content)
431
+ }).returning();
432
+ if (!row) throw new ManabloxError("audit.append.failed");
433
+ return row;
434
+ });
435
+ }
436
+ /**
437
+ * `append` that never throws. The write it describes has already happened; failing
438
+ * the request now would report an error for something that succeeded, so the failure
439
+ * is reported to `onError` (the host logs it) and the caller carries on.
440
+ */
441
+ async record(input) {
442
+ try {
443
+ return await this.append(input);
444
+ } catch (error) {
445
+ this.options.onError?.(error, input);
446
+ return null;
447
+ }
448
+ }
449
+ async findById(id) {
450
+ return (await this.db.select().from(auditEntries).where(eq(auditEntries.id, id)).limit(1))[0] ?? null;
451
+ }
452
+ list(filter, sort = {
453
+ by: "at",
454
+ direction: "desc"
455
+ }, pagination = {
456
+ limit: 50,
457
+ offset: 0
458
+ }) {
459
+ const column = SORT_COLUMNS[sort.by];
460
+ const primary = sort.direction === "asc" ? asc(column) : desc(column);
461
+ const tiebreak = sort.direction === "asc" ? asc(auditEntries.seq) : desc(auditEntries.seq);
462
+ return paginate(this.db, auditEntries, {
463
+ where: buildWhere(filter),
464
+ orderBy: [primary, tiebreak],
465
+ pagination
466
+ });
467
+ }
468
+ /** The entries about one thing, oldest first: a document's history. */
469
+ forTarget(targetKind, targetId, limit = 100) {
470
+ return this.db.select().from(auditEntries).where(and(eq(auditEntries.targetKind, targetKind), eq(auditEntries.targetId, targetId))).orderBy(desc(auditEntries.seq)).limit(limit);
471
+ }
472
+ /**
473
+ * Recomputes every hash from the first entry on and checks each link to its
474
+ * predecessor. The chain is one across the instance, so this walks all of it, in
475
+ * batches; on a large log it takes a while and that is the point.
476
+ */
477
+ async verify(batchSize = 500) {
478
+ let prevHash = null;
479
+ let afterSeq = 0;
480
+ let checked = 0;
481
+ for (;;) {
482
+ const rows = await this.db.select().from(auditEntries).where(gt(auditEntries.seq, afterSeq)).orderBy(asc(auditEntries.seq)).limit(batchSize);
483
+ if (rows.length === 0) break;
484
+ for (const row of rows) {
485
+ checked++;
486
+ if (row.prevHash !== prevHash) return {
487
+ ok: false,
488
+ checked,
489
+ brokenAt: {
490
+ seq: row.seq,
491
+ id: row.id,
492
+ reason: "link"
493
+ }
494
+ };
495
+ if (hashAuditEntry({
496
+ at: row.at,
497
+ spaceId: row.spaceId,
498
+ actorKind: row.actorKind,
499
+ actorId: row.actorId,
500
+ actorLabel: row.actorLabel,
501
+ actorDetail: row.actorDetail,
502
+ action: row.action,
503
+ targetKind: row.targetKind,
504
+ targetId: row.targetId,
505
+ targetLabel: row.targetLabel,
506
+ changes: row.changes,
507
+ meta: row.meta,
508
+ prevHash: row.prevHash
509
+ }) !== row.hash) return {
510
+ ok: false,
511
+ checked,
512
+ brokenAt: {
513
+ seq: row.seq,
514
+ id: row.id,
515
+ reason: "hash"
516
+ }
517
+ };
518
+ prevHash = row.hash;
519
+ afterSeq = row.seq;
520
+ }
521
+ }
522
+ return {
523
+ ok: true,
524
+ checked,
525
+ brokenAt: null
526
+ };
527
+ }
528
+ async count(filter = {}) {
529
+ const [row] = await this.db.select({ count: sql`count(*)::int` }).from(auditEntries).where(buildWhere(filter));
530
+ return row?.count ?? 0;
531
+ }
532
+ };
533
+ function buildWhere(filter) {
534
+ const conditions = [];
535
+ if (filter.spaceId === null) conditions.push(isNull(auditEntries.spaceId));
536
+ else if (filter.spaceId) conditions.push(eq(auditEntries.spaceId, filter.spaceId));
537
+ if (filter.actorKind) conditions.push(eq(auditEntries.actorKind, filter.actorKind));
538
+ if (filter.actorId) conditions.push(eq(auditEntries.actorId, filter.actorId));
539
+ if (filter.actions?.length) conditions.push(inArray(auditEntries.action, filter.actions));
540
+ if (filter.targetKind) conditions.push(eq(auditEntries.targetKind, filter.targetKind));
541
+ if (filter.targetId) conditions.push(eq(auditEntries.targetId, filter.targetId));
542
+ if (filter.from) conditions.push(gte(auditEntries.at, filter.from));
543
+ if (filter.to) conditions.push(lte(auditEntries.at, filter.to));
544
+ if (filter.search?.trim()) {
545
+ const term = `%${escapeLike(filter.search.trim())}%`;
546
+ const match = or(ilike(auditEntries.actorLabel, term), ilike(auditEntries.targetLabel, term), ilike(auditEntries.action, term));
547
+ if (match) conditions.push(match);
548
+ }
549
+ return conditions.length ? and(...conditions) : void 0;
550
+ }
551
+ //#endregion
362
552
  //#region src/repositories/content.ts
363
553
  /**
364
554
  * The columns `publish()` copies from the draft into the projection: every column the
@@ -745,6 +935,31 @@ var ContentRepository = class {
745
935
  async versionSnapshot(contentId, version) {
746
936
  return (await this.db.select({ snapshot: contentVersions.snapshot }).from(contentVersions).where(and(eq(contentVersions.contentId, contentId), eq(contentVersions.version, version))).limit(1))[0]?.snapshot ?? null;
747
937
  }
938
+ /** Every stored version of these documents, oldest first, for a space export. */
939
+ async history(contentIds) {
940
+ if (contentIds.length === 0) return [];
941
+ return this.db.select().from(contentVersions).where(inArray(contentVersions.contentId, contentIds)).orderBy(contentVersions.contentId, contentVersions.version);
942
+ }
943
+ /**
944
+ * Puts an imported document back the way its source had it: the version counter and
945
+ * timestamps `create()` had to invent, and the version history in place of the single
946
+ * snapshot `create()` wrote. The row's own columns are left alone otherwise.
947
+ */
948
+ async restoreHistory(id, data) {
949
+ await this.db.transaction(async (tx) => {
950
+ await tx.update(contents).set({
951
+ version: data.version,
952
+ createdAt: data.createdAt,
953
+ updatedAt: data.updatedAt
954
+ }).where(eq(contents.id, id));
955
+ if (data.versions.length === 0) return;
956
+ await tx.delete(contentVersions).where(eq(contentVersions.contentId, id));
957
+ await tx.insert(contentVersions).values(data.versions.map((version) => ({
958
+ ...version,
959
+ contentId: id
960
+ })));
961
+ });
962
+ }
748
963
  async snapshot(db, row, actorId) {
749
964
  await db.insert(contentVersions).values({
750
965
  contentId: row.id,
@@ -809,6 +1024,79 @@ function joinSegment(prefix, segment) {
809
1024
  return prefix ? `${prefix}/${segment}` : segment;
810
1025
  }
811
1026
  //#endregion
1027
+ //#region src/repositories/content-approval.ts
1028
+ /**
1029
+ * The approval history of documents. Rows are appended when someone asks and closed
1030
+ * when someone decides; the "one pending per document" rule is the service's, this
1031
+ * only offers the reads it needs to keep it.
1032
+ */
1033
+ var ContentApprovalRepository = class {
1034
+ db;
1035
+ constructor(db) {
1036
+ this.db = db;
1037
+ }
1038
+ async findById(id) {
1039
+ return (await this.db.select().from(contentApprovals).where(eq(contentApprovals.id, id)).limit(1))[0] ?? null;
1040
+ }
1041
+ /** The open request on a document, if there is one. */
1042
+ async pendingFor(contentId) {
1043
+ return (await this.db.select().from(contentApprovals).where(and(eq(contentApprovals.contentId, contentId), eq(contentApprovals.status, "pending"))).orderBy(desc(contentApprovals.requestedAt)).limit(1))[0] ?? null;
1044
+ }
1045
+ /** The most recent request on a document, whatever became of it. */
1046
+ async latestFor(contentId) {
1047
+ return (await this.db.select().from(contentApprovals).where(eq(contentApprovals.contentId, contentId)).orderBy(desc(contentApprovals.requestedAt)).limit(1))[0] ?? null;
1048
+ }
1049
+ historyFor(contentId, limit = 20) {
1050
+ return this.db.select().from(contentApprovals).where(eq(contentApprovals.contentId, contentId)).orderBy(desc(contentApprovals.requestedAt)).limit(limit);
1051
+ }
1052
+ /**
1053
+ * Every open request in a space, oldest first, with the document beside it. Narrowed
1054
+ * to `typeIds` when the reader may only publish some types; `null` means every type.
1055
+ */
1056
+ async pendingInSpace(spaceId, typeIds = null) {
1057
+ if (typeIds && typeIds.length === 0) return [];
1058
+ const conditions = [eq(contentApprovals.spaceId, spaceId), eq(contentApprovals.status, "pending")];
1059
+ if (typeIds) conditions.push(inArray(contentApprovals.typeId, typeIds));
1060
+ return (await this.db.select({
1061
+ approval: contentApprovals,
1062
+ content: {
1063
+ id: contents.id,
1064
+ title: contents.title,
1065
+ locale: contents.locale,
1066
+ typeId: contents.typeId,
1067
+ updatedAt: contents.updatedAt
1068
+ }
1069
+ }).from(contentApprovals).innerJoin(contents, eq(contents.id, contentApprovals.contentId)).where(and(...conditions)).orderBy(contentApprovals.requestedAt)).map((row) => ({
1070
+ ...row.approval,
1071
+ content: row.content
1072
+ }));
1073
+ }
1074
+ async request(data) {
1075
+ const [row] = await this.db.insert(contentApprovals).values({
1076
+ spaceId: data.spaceId,
1077
+ contentId: data.contentId,
1078
+ typeId: data.typeId,
1079
+ requestedBy: data.requestedBy,
1080
+ requestedByLabel: data.requestedByLabel,
1081
+ requestNote: data.requestNote ?? null,
1082
+ contentVersion: data.contentVersion ?? null
1083
+ }).returning();
1084
+ if (!row) throw new ManabloxError("content.approval.create.failed");
1085
+ return row;
1086
+ }
1087
+ async decide(id, data) {
1088
+ const [row] = await this.db.update(contentApprovals).set({
1089
+ status: data.status,
1090
+ decidedBy: data.decidedBy,
1091
+ decidedByLabel: data.decidedByLabel,
1092
+ decisionNote: data.decisionNote ?? null,
1093
+ decidedAt: /* @__PURE__ */ new Date()
1094
+ }).where(eq(contentApprovals.id, id)).returning();
1095
+ if (!row) throw ManabloxError.notFound("content.approval.notPending", { id });
1096
+ return row;
1097
+ }
1098
+ };
1099
+ //#endregion
812
1100
  //#region src/repositories/content-type.ts
813
1101
  /**
814
1102
  * Persistence for *runtime-defined* content types only. Code-defined types come from
@@ -841,6 +1129,7 @@ var ContentTypeRepository = class {
841
1129
  isPublishable: definition.isPublishable,
842
1130
  isVisibleInTree: definition.isVisibleInTree,
843
1131
  canBeVisibleInMenu: definition.canBeVisibleInMenu,
1132
+ requiresApproval: definition.requiresApproval,
844
1133
  fields: definition.fields
845
1134
  }).returning();
846
1135
  if (!row) throw new ManabloxError("contentType.create.failed");
@@ -869,6 +1158,7 @@ var ContentTypeRepository = class {
869
1158
  isPublishable: definition.isPublishable,
870
1159
  isVisibleInTree: definition.isVisibleInTree,
871
1160
  canBeVisibleInMenu: definition.canBeVisibleInMenu,
1161
+ requiresApproval: definition.requiresApproval,
872
1162
  fields: definition.fields,
873
1163
  updatedAt: /* @__PURE__ */ new Date()
874
1164
  }).where(eq(contentTypes.id, id)).returning();
@@ -892,6 +1182,7 @@ function toDefinition(row) {
892
1182
  isPublishable: row.isPublishable,
893
1183
  isVisibleInTree: row.isVisibleInTree,
894
1184
  canBeVisibleInMenu: row.canBeVisibleInMenu,
1185
+ requiresApproval: row.requiresApproval,
895
1186
  fields: row.fields,
896
1187
  source: "runtime"
897
1188
  };
@@ -1035,6 +1326,75 @@ function buildTree(rows) {
1035
1326
  return roots;
1036
1327
  }
1037
1328
  //#endregion
1329
+ //#region src/repositories/notification.ts
1330
+ /**
1331
+ * One person's inbox. Every method is scoped to a `userId`, so a row id from another
1332
+ * account is inert: there is no way to read or change what is not yours.
1333
+ */
1334
+ var NotificationRepository = class {
1335
+ db;
1336
+ constructor(db) {
1337
+ this.db = db;
1338
+ }
1339
+ /** Inserts one row per recipient in one statement; an empty list writes nothing. */
1340
+ async insertMany(rows) {
1341
+ if (rows.length === 0) return [];
1342
+ return this.db.insert(notifications).values(rows.map((row) => ({
1343
+ userId: row.userId,
1344
+ spaceId: row.spaceId ?? null,
1345
+ kind: row.kind,
1346
+ title: row.title,
1347
+ body: row.body ?? "",
1348
+ url: row.url ?? null,
1349
+ targetKind: row.targetKind ?? null,
1350
+ targetId: row.targetId ?? null,
1351
+ actorId: row.actorId ?? null,
1352
+ actorLabel: row.actorLabel ?? null,
1353
+ meta: row.meta ?? null
1354
+ }))).returning();
1355
+ }
1356
+ list(userId, filter = {}, pagination = {
1357
+ limit: 25,
1358
+ offset: 0
1359
+ }) {
1360
+ const conditions = [eq(notifications.userId, userId)];
1361
+ if (filter.unreadOnly) conditions.push(isNull(notifications.readAt));
1362
+ if (filter.kinds?.length) conditions.push(inArray(notifications.kind, filter.kinds));
1363
+ if (filter.spaceId === null) conditions.push(isNull(notifications.spaceId));
1364
+ else if (filter.spaceId) conditions.push(eq(notifications.spaceId, filter.spaceId));
1365
+ return paginate(this.db, notifications, {
1366
+ where: and(...conditions),
1367
+ orderBy: [desc(notifications.createdAt), desc(notifications.id)],
1368
+ pagination
1369
+ });
1370
+ }
1371
+ async findById(userId, id) {
1372
+ return (await this.db.select().from(notifications).where(and(eq(notifications.userId, userId), eq(notifications.id, id))).limit(1))[0] ?? null;
1373
+ }
1374
+ async countUnread(userId) {
1375
+ return (await this.db.select({ count: sql`count(*)::int` }).from(notifications).where(and(eq(notifications.userId, userId), isNull(notifications.readAt))))[0]?.count ?? 0;
1376
+ }
1377
+ /** Marks the named rows read; rows already read keep their original time. Returns how many changed. */
1378
+ async markRead(userId, ids) {
1379
+ if (ids.length === 0) return 0;
1380
+ return (await this.db.update(notifications).set({ readAt: /* @__PURE__ */ new Date() }).where(and(eq(notifications.userId, userId), inArray(notifications.id, ids), isNull(notifications.readAt))).returning({ id: notifications.id })).length;
1381
+ }
1382
+ async markUnread(userId, ids) {
1383
+ if (ids.length === 0) return 0;
1384
+ return (await this.db.update(notifications).set({ readAt: null }).where(and(eq(notifications.userId, userId), inArray(notifications.id, ids))).returning({ id: notifications.id })).length;
1385
+ }
1386
+ async markAllRead(userId) {
1387
+ return (await this.db.update(notifications).set({ readAt: /* @__PURE__ */ new Date() }).where(and(eq(notifications.userId, userId), isNull(notifications.readAt))).returning({ id: notifications.id })).length;
1388
+ }
1389
+ async delete(userId, ids) {
1390
+ if (ids.length === 0) return 0;
1391
+ return (await this.db.delete(notifications).where(and(eq(notifications.userId, userId), inArray(notifications.id, ids))).returning({ id: notifications.id })).length;
1392
+ }
1393
+ async deleteRead(userId) {
1394
+ return (await this.db.delete(notifications).where(and(eq(notifications.userId, userId), sql`${notifications.readAt} is not null`)).returning({ id: notifications.id })).length;
1395
+ }
1396
+ };
1397
+ //#endregion
1038
1398
  //#region src/repositories/role.ts
1039
1399
  var RoleRepository = class {
1040
1400
  db;
@@ -1163,6 +1523,10 @@ var UserRepository = class {
1163
1523
  if (ids.length === 0) return [];
1164
1524
  return this.db.select().from(users).where(inArray(users.id, ids));
1165
1525
  }
1526
+ /** Every account holding an instance role, for a fan-out to the superadmins. */
1527
+ async findByRole(role) {
1528
+ return this.db.select().from(users).where(eq(users.role, role));
1529
+ }
1166
1530
  async findByEmail(email) {
1167
1531
  return (await this.db.select().from(users).where(eq(users.email, email)).limit(1))[0] ?? null;
1168
1532
  }
@@ -1254,6 +1618,18 @@ var UserRepository = class {
1254
1618
  async countByRole(role) {
1255
1619
  return (await this.db.select({ count: sql`count(*)::int` }).from(users).where(eq(users.role, role)))[0]?.count ?? 0;
1256
1620
  }
1621
+ async setNotificationPreferences(id, preferences) {
1622
+ const [row] = await this.db.update(users).set({
1623
+ notificationPreferences: preferences,
1624
+ updatedAt: /* @__PURE__ */ new Date()
1625
+ }).where(eq(users.id, id)).returning();
1626
+ if (!row) throw ManabloxError.notFound("user.notFound", { id });
1627
+ return row;
1628
+ }
1629
+ /** The stored hash of the password credential, or `null` for an account without one. */
1630
+ async passwordHash(userId) {
1631
+ return (await this.db.select({ password: accounts.password }).from(accounts).where(and(eq(accounts.userId, userId), eq(accounts.providerId, CREDENTIAL_PROVIDER), eq(accounts.issuer, CREDENTIAL_ISSUER))).limit(1))[0]?.password ?? null;
1632
+ }
1257
1633
  async setRole(id, role) {
1258
1634
  const [row] = await this.db.update(users).set({
1259
1635
  role,
@@ -1346,6 +1722,22 @@ var WebhookRepository = class {
1346
1722
  async findById(id) {
1347
1723
  return (await this.db.select().from(webhooks).where(eq(webhooks.id, id)).limit(1))[0] ?? null;
1348
1724
  }
1725
+ async listBySpace(spaceId) {
1726
+ return this.db.select().from(webhooks).where(eq(webhooks.spaceId, spaceId)).orderBy(webhooks.name);
1727
+ }
1728
+ async create(data) {
1729
+ const [row] = await this.db.insert(webhooks).values({
1730
+ ...data.id ? { id: data.id } : {},
1731
+ spaceId: data.spaceId,
1732
+ name: data.name,
1733
+ url: data.url,
1734
+ secret: data.secret ?? null,
1735
+ events: data.events,
1736
+ enabled: data.enabled ?? true
1737
+ }).returning();
1738
+ if (!row) throw new ManabloxError("webhook.create.failed");
1739
+ return row;
1740
+ }
1349
1741
  /** The switched-on webhooks of a space, for fanning an event out. */
1350
1742
  async findEnabled(spaceId) {
1351
1743
  return this.db.select().from(webhooks).where(and(eq(webhooks.spaceId, spaceId), eq(webhooks.enabled, true)));
@@ -1515,7 +1907,7 @@ var WorkflowRepository = class {
1515
1907
  };
1516
1908
  //#endregion
1517
1909
  //#region src/repositories/index.ts
1518
- function createRepositories(db, registry) {
1910
+ function createRepositories(db, registry, options = {}) {
1519
1911
  return {
1520
1912
  content: new ContentRepository(db, registry),
1521
1913
  contentTypes: new ContentTypeRepository(db),
@@ -1526,8 +1918,11 @@ function createRepositories(db, registry) {
1526
1918
  menus: new MenuRepository(db),
1527
1919
  roles: new RoleRepository(db),
1528
1920
  workflows: new WorkflowRepository(db),
1529
- webhooks: new WebhookRepository(db)
1921
+ webhooks: new WebhookRepository(db),
1922
+ audit: new AuditRepository(db, options.audit),
1923
+ notifications: new NotificationRepository(db),
1924
+ approvals: new ContentApprovalRepository(db)
1530
1925
  };
1531
1926
  }
1532
1927
  //#endregion
1533
- export { applyBootstrapSql as _, UserRepository as a, ContentTypeRepository as c, AssetUsageRepository as d, AssetRepository as f, createDatabase as g, paginate as h, WebhookRepository as i, ContentRepository as l, buildOrderBy as m, RUNS_KEPT_PER_WORKFLOW as n, SpaceRepository as o, buildContentWhere as p, WorkflowRepository as r, MenuRepository as s, createRepositories as t, buildTree$1 as u };
1928
+ export { buildOrderBy as _, UserRepository as a, applyBootstrapSql as b, MenuRepository as c, ContentRepository as d, buildTree$1 as f, buildContentWhere as g, AssetRepository as h, WebhookRepository as i, ContentTypeRepository as l, AssetUsageRepository as m, RUNS_KEPT_PER_WORKFLOW as n, SpaceRepository as o, AuditRepository as p, WorkflowRepository as r, NotificationRepository as s, createRepositories as t, ContentApprovalRepository as u, paginate as v, createDatabase as y };