@manablox/db 0.3.0 → 0.5.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-Cyf_N5K3.d.ts → index-BLAkQMJT.d.ts} +223 -4
- package/dist/{index-rZ24t-Ln.d.ts → index-DrNMGM9N.d.ts} +858 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/{repositories-DYjzuuF6.js → repositories-pz4NeWaF.js} +407 -12
- package/dist/{schema-Bb4p16Yz.js → schema-Dm3RcBst.js} +111 -2
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +2 -2
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/migrations/0009_audit-log.sql +40 -0
- package/migrations/0010_notifications-approvals.sql +45 -0
- package/migrations/meta/0009_snapshot.json +3192 -0
- package/migrations/meta/0010_snapshot.json +3574 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { A as
|
|
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
|
|
2
|
-
import { A as
|
|
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
|
|
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 {
|
|
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 };
|