tempest-express-sdk 0.25.0 → 0.26.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/README.md +1 -1
- package/dist/{chunk-TIW2KPT2.js → chunk-GLZYNX63.js} +3 -3
- package/dist/{chunk-TIW2KPT2.js.map → chunk-GLZYNX63.js.map} +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +394 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +380 -95
- package/dist/index.d.ts +380 -95
- package/dist/index.js +390 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { z, looseBoolean, toDict, PasswordUtils } from './chunk-
|
|
2
|
-
export { PasswordUtils, VERSION, baseAppSettingsSchema, baseAppSettingsShape, baseResponseSchema, centsField, corsSettingsShape, databaseSettingsShape, looseBoolean as envBoolean, hexColorField, latitudeField, loadSettings, longitudeField, looseBoolean, nonEmptyStrField, nonNegativeFloatField, nonNegativeIntField, percentField, portField, positiveFloatField, positiveIntField, priceField, ratingField, ratioField, serverSettingsShape, slugField, toDict, z } from './chunk-
|
|
1
|
+
import { z, looseBoolean, toDict, PasswordUtils } from './chunk-GLZYNX63.js';
|
|
2
|
+
export { PasswordUtils, VERSION, baseAppSettingsSchema, baseAppSettingsShape, baseResponseSchema, centsField, corsSettingsShape, databaseSettingsShape, looseBoolean as envBoolean, hexColorField, latitudeField, loadSettings, longitudeField, looseBoolean, nonEmptyStrField, nonNegativeFloatField, nonNegativeIntField, percentField, portField, positiveFloatField, positiveIntField, priceField, ratingField, ratioField, serverSettingsShape, slugField, toDict, z } from './chunk-GLZYNX63.js';
|
|
3
3
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
4
4
|
import { Model, column, sql, BaseRepository, RecordNotFound, detectDialect, columnsOf, NodeSqliteDriver, AsyncEngine, or, and } from 'tempest-db-js';
|
|
5
5
|
export { AsyncEngine, AsyncResult, AsyncSession, BaseRepository, Column, DeleteBuilder, InsertBuilder, Model, NoResultError, NodeSqliteDriver, PostgresDialect, RecordNotFound, SelectBuilder, SqliteDialect, SyncEngine, SyncSession, UpdateBuilder, and, belongsTo, column, columnsOf, createEngine, createSyncEngine, del, detectDialect, getDialect, hasMany, insert, join, loadRelations, not, or, parseDatabaseUrl, select, sql, update } from 'tempest-db-js';
|
|
@@ -9317,12 +9317,51 @@ var MessagingHub = class {
|
|
|
9317
9317
|
}
|
|
9318
9318
|
};
|
|
9319
9319
|
|
|
9320
|
+
// src/admin/dashboard.ts
|
|
9321
|
+
function metricCard(label, compute, helpText) {
|
|
9322
|
+
return helpText === void 0 ? { label, compute } : { label, compute, helpText };
|
|
9323
|
+
}
|
|
9324
|
+
function trendPercent(trend) {
|
|
9325
|
+
if (trend.previous === 0) return null;
|
|
9326
|
+
return (trend.value - trend.previous) / trend.previous * 100;
|
|
9327
|
+
}
|
|
9328
|
+
function trendDirection(trend) {
|
|
9329
|
+
if (trend.value > trend.previous) return "up";
|
|
9330
|
+
if (trend.value < trend.previous) return "down";
|
|
9331
|
+
return "flat";
|
|
9332
|
+
}
|
|
9333
|
+
function partitionTotal(partition) {
|
|
9334
|
+
return partition.segments.reduce((total, segment) => total + segment.value, 0);
|
|
9335
|
+
}
|
|
9336
|
+
|
|
9337
|
+
// src/admin/lenses.ts
|
|
9338
|
+
function slugify(name) {
|
|
9339
|
+
return name.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "lens";
|
|
9340
|
+
}
|
|
9341
|
+
function adminLens(options) {
|
|
9342
|
+
return {
|
|
9343
|
+
name: options.name,
|
|
9344
|
+
slug: slugify(options.name),
|
|
9345
|
+
label: options.label ?? options.name,
|
|
9346
|
+
filters: options.filters ?? {},
|
|
9347
|
+
orderBy: options.orderBy ?? null
|
|
9348
|
+
};
|
|
9349
|
+
}
|
|
9350
|
+
|
|
9351
|
+
// src/admin/permissions.ts
|
|
9352
|
+
var AdminPermission = {
|
|
9353
|
+
VIEW: "view",
|
|
9354
|
+
CREATE: "create",
|
|
9355
|
+
EDIT: "edit",
|
|
9356
|
+
DELETE: "delete"
|
|
9357
|
+
};
|
|
9358
|
+
|
|
9320
9359
|
// src/admin/actions.ts
|
|
9321
|
-
function
|
|
9360
|
+
function slugify2(label) {
|
|
9322
9361
|
return label.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "action";
|
|
9323
9362
|
}
|
|
9324
9363
|
function adminAction(options, handler) {
|
|
9325
|
-
const name = options.name ??
|
|
9364
|
+
const name = options.name ?? slugify2(options.label);
|
|
9326
9365
|
if (name === "") throw new Error("adminAction requires a non-empty name or label");
|
|
9327
9366
|
return {
|
|
9328
9367
|
name,
|
|
@@ -9428,6 +9467,12 @@ var NEVER_EDITABLE = [
|
|
|
9428
9467
|
"hashedPassword"
|
|
9429
9468
|
];
|
|
9430
9469
|
var NEVER_LISTED = ["hashedPassword"];
|
|
9470
|
+
var AUDIT_FIELDS = [
|
|
9471
|
+
"createdAt",
|
|
9472
|
+
"updatedAt",
|
|
9473
|
+
"createdBy",
|
|
9474
|
+
"updatedBy"
|
|
9475
|
+
];
|
|
9431
9476
|
var AdminModel = class {
|
|
9432
9477
|
/** The managed model class. */
|
|
9433
9478
|
model;
|
|
@@ -9451,6 +9496,10 @@ var AdminModel = class {
|
|
|
9451
9496
|
canEdit;
|
|
9452
9497
|
/** Whether the delete action is exposed. */
|
|
9453
9498
|
canDelete;
|
|
9499
|
+
/** Audit-log model backing the detail timeline, or `null`. */
|
|
9500
|
+
auditModel;
|
|
9501
|
+
/** Saved list-view presets, in declaration order. */
|
|
9502
|
+
lenses;
|
|
9454
9503
|
actions = /* @__PURE__ */ new Map();
|
|
9455
9504
|
slugOverride;
|
|
9456
9505
|
listDisplayOverride;
|
|
@@ -9476,6 +9525,8 @@ var AdminModel = class {
|
|
|
9476
9525
|
this.canCreate = options.canCreate ?? true;
|
|
9477
9526
|
this.canEdit = options.canEdit ?? true;
|
|
9478
9527
|
this.canDelete = options.canDelete ?? true;
|
|
9528
|
+
this.auditModel = options.auditModel ?? null;
|
|
9529
|
+
this.lenses = [...options.lenses ?? []];
|
|
9479
9530
|
for (const action of options.actions ?? []) {
|
|
9480
9531
|
if (this.actions.has(action.name)) {
|
|
9481
9532
|
throw new Error(
|
|
@@ -9560,6 +9611,25 @@ var AdminModel = class {
|
|
|
9560
9611
|
if (this.listDisplayOverride !== null) return [...this.listDisplayOverride];
|
|
9561
9612
|
return this.columnNames().filter((name) => !NEVER_LISTED.includes(name));
|
|
9562
9613
|
}
|
|
9614
|
+
/**
|
|
9615
|
+
* Look a lens up by its slug.
|
|
9616
|
+
*
|
|
9617
|
+
* @param slug - The `?lens=` value.
|
|
9618
|
+
* @returns The lens, or `null` when nothing matches.
|
|
9619
|
+
*/
|
|
9620
|
+
getLens(slug) {
|
|
9621
|
+
return this.lenses.find((lens) => lens.slug === slug) ?? null;
|
|
9622
|
+
}
|
|
9623
|
+
/**
|
|
9624
|
+
* Return the audit/timestamp columns the model actually declares.
|
|
9625
|
+
*
|
|
9626
|
+
* @returns The subset of `createdAt` / `updatedAt` / `createdBy` /
|
|
9627
|
+
* `updatedBy` present on the model, in that order.
|
|
9628
|
+
*/
|
|
9629
|
+
auditFieldNames() {
|
|
9630
|
+
const known = new Set(this.columnNames());
|
|
9631
|
+
return AUDIT_FIELDS.filter((name) => known.has(name));
|
|
9632
|
+
}
|
|
9563
9633
|
/**
|
|
9564
9634
|
* Return the columns the detail view renders.
|
|
9565
9635
|
*
|
|
@@ -9568,10 +9638,16 @@ var AdminModel = class {
|
|
|
9568
9638
|
* where an operator goes to see the whole record, so trimming it there would
|
|
9569
9639
|
* hide data with nowhere else to read it.
|
|
9570
9640
|
*
|
|
9571
|
-
*
|
|
9641
|
+
* The audit/timestamp columns are held back too — they render in the detail
|
|
9642
|
+
* view's own audit panel, next to the change history, rather than scattered
|
|
9643
|
+
* among the domain fields.
|
|
9644
|
+
*
|
|
9645
|
+
* @returns Every domain column, in declaration order.
|
|
9572
9646
|
*/
|
|
9573
9647
|
detailFieldNames() {
|
|
9574
|
-
return this.columnNames().filter(
|
|
9648
|
+
return this.columnNames().filter(
|
|
9649
|
+
(name) => !NEVER_LISTED.includes(name) && !AUDIT_FIELDS.includes(name)
|
|
9650
|
+
);
|
|
9575
9651
|
}
|
|
9576
9652
|
/**
|
|
9577
9653
|
* Return the columns a create/edit form exposes.
|
|
@@ -10028,6 +10104,8 @@ var AdminSite = class {
|
|
|
10028
10104
|
siteUrl;
|
|
10029
10105
|
/** Typed appearance overrides. */
|
|
10030
10106
|
theme;
|
|
10107
|
+
/** Business-metric cards rendered at the top of the dashboard. */
|
|
10108
|
+
dashboardCards;
|
|
10031
10109
|
registry = /* @__PURE__ */ new Map();
|
|
10032
10110
|
/**
|
|
10033
10111
|
* Initialize the site.
|
|
@@ -10040,6 +10118,7 @@ var AdminSite = class {
|
|
|
10040
10118
|
this.indexSubtitle = options.indexSubtitle ?? "Site administration";
|
|
10041
10119
|
this.siteUrl = options.siteUrl ?? null;
|
|
10042
10120
|
this.theme = options.theme ?? {};
|
|
10121
|
+
this.dashboardCards = [...options.dashboardCards ?? []];
|
|
10043
10122
|
}
|
|
10044
10123
|
/**
|
|
10045
10124
|
* Return the centered header brand text.
|
|
@@ -11859,7 +11938,7 @@ function renderMfaPage(context, error) {
|
|
|
11859
11938
|
</section>`;
|
|
11860
11939
|
return renderLayout(context, `Two-factor \xB7 ${context.site.title}`, body);
|
|
11861
11940
|
}
|
|
11862
|
-
function renderDashboardPage(context, cards, metrics) {
|
|
11941
|
+
function renderDashboardPage(context, cards, metrics, businessCards = []) {
|
|
11863
11942
|
const metricsPanel = metrics === null ? "" : `<div class="tempest-admin-stats" aria-label="System metrics">
|
|
11864
11943
|
<div class="tempest-admin-stat">
|
|
11865
11944
|
<span class="tempest-admin-stat__label">CPU</span>
|
|
@@ -11871,6 +11950,7 @@ function renderDashboardPage(context, cards, metrics) {
|
|
|
11871
11950
|
<span class="tempest-admin-stat__sub">${escapeHtml(metrics.memoryUsedGb)} / ${escapeHtml(metrics.memoryTotalGb)} GB</span>
|
|
11872
11951
|
</div>
|
|
11873
11952
|
</div>`;
|
|
11953
|
+
const business = businessCards.length > 0 ? `<div class="tempest-admin-cards" aria-label="Business metrics">${businessCards.map(renderBusinessCard).join("")}</div>` : "";
|
|
11874
11954
|
const models = cards.length > 0 ? `<div class="tempest-admin-models">${cards.map(
|
|
11875
11955
|
(card) => `<article class="tempest-admin-model-card">
|
|
11876
11956
|
<header class="tempest-admin-model-card__head">
|
|
@@ -11887,10 +11967,53 @@ function renderDashboardPage(context, cards, metrics) {
|
|
|
11887
11967
|
<h1>${escapeHtml(context.site.title)}</h1>
|
|
11888
11968
|
<p>${escapeHtml(context.site.indexSubtitle)}</p>
|
|
11889
11969
|
${metricsPanel}
|
|
11970
|
+
${business}
|
|
11890
11971
|
${models}
|
|
11891
11972
|
</section>`;
|
|
11892
11973
|
return renderLayout(context, context.site.title, body);
|
|
11893
11974
|
}
|
|
11975
|
+
function renderBusinessCard(card) {
|
|
11976
|
+
const help = card.helpText !== null ? `<span class="tempest-admin-card__help">${escapeHtml(card.helpText)}</span>` : "";
|
|
11977
|
+
if (card.error !== null) {
|
|
11978
|
+
return `<article class="tempest-admin-card tempest-admin-card--value">
|
|
11979
|
+
<span class="tempest-admin-card__label">${escapeHtml(card.label)}</span>
|
|
11980
|
+
<span class="tempest-admin-card__value">\u2014</span>
|
|
11981
|
+
<span class="tempest-admin-card__help">${escapeHtml(card.error)}</span>
|
|
11982
|
+
</article>`;
|
|
11983
|
+
}
|
|
11984
|
+
const unit = card.unit !== null ? ` <small>${escapeHtml(card.unit)}</small>` : "";
|
|
11985
|
+
if (card.kind === "partition") {
|
|
11986
|
+
const parts = card.segments.map(
|
|
11987
|
+
(segment) => `<li>
|
|
11988
|
+
<span class="tempest-admin-card__part-label">${escapeHtml(segment.label)}</span>
|
|
11989
|
+
<span class="tempest-admin-card__part-bar"><span style="width: ${Math.round(segment.percent)}%"></span></span>
|
|
11990
|
+
<span class="tempest-admin-card__part-value">${escapeHtml(segment.value)}</span>
|
|
11991
|
+
</li>`
|
|
11992
|
+
).join("");
|
|
11993
|
+
return `<article class="tempest-admin-card tempest-admin-card--partition">
|
|
11994
|
+
<span class="tempest-admin-card__label">${escapeHtml(card.label)}</span>
|
|
11995
|
+
<ul class="tempest-admin-card__parts">${parts}</ul>
|
|
11996
|
+
${help}
|
|
11997
|
+
</article>`;
|
|
11998
|
+
}
|
|
11999
|
+
if (card.kind === "trend") {
|
|
12000
|
+
const arrow = card.direction === "up" ? "\u25B2" : card.direction === "down" ? "\u25BC" : "\u25AC";
|
|
12001
|
+
return `<article class="tempest-admin-card tempest-admin-card--trend">
|
|
12002
|
+
<span class="tempest-admin-card__label">${escapeHtml(card.label)}</span>
|
|
12003
|
+
<span class="tempest-admin-card__value">${escapeHtml(card.value)}${unit}</span>
|
|
12004
|
+
<span class="tempest-admin-card__trend tempest-admin-card__trend--${escapeHtml(card.direction)}">
|
|
12005
|
+
${arrow} ${card.percent === null ? "\u2014" : escapeHtml(card.percent)}
|
|
12006
|
+
<small>vs prev ${escapeHtml(card.previous)}</small>
|
|
12007
|
+
</span>
|
|
12008
|
+
${help}
|
|
12009
|
+
</article>`;
|
|
12010
|
+
}
|
|
12011
|
+
return `<article class="tempest-admin-card tempest-admin-card--value">
|
|
12012
|
+
<span class="tempest-admin-card__label">${escapeHtml(card.label)}</span>
|
|
12013
|
+
<span class="tempest-admin-card__value">${escapeHtml(card.value)}${unit}</span>
|
|
12014
|
+
${help}
|
|
12015
|
+
</article>`;
|
|
12016
|
+
}
|
|
11894
12017
|
function renderFilter(filter) {
|
|
11895
12018
|
const name = `filter_${filter.field}`;
|
|
11896
12019
|
if (filter.kind === "select") {
|
|
@@ -11960,6 +12083,9 @@ function renderListPage(context, view) {
|
|
|
11960
12083
|
<a href="${escapeHtml(view.exportJsonUrl)}">Export JSON</a>
|
|
11961
12084
|
</div>
|
|
11962
12085
|
</div>
|
|
12086
|
+
${view.lenses.length > 0 ? `<nav class="tempest-admin-lenses" aria-label="Lenses">${view.lenses.map(
|
|
12087
|
+
(lens) => `<a class="tempest-admin-lens${lens.active ? " tempest-admin-lens--active" : ""}" href="${escapeHtml(lens.url)}">${escapeHtml(lens.label)}</a>`
|
|
12088
|
+
).join("")}</nav>` : ""}
|
|
11963
12089
|
${bulkBar}
|
|
11964
12090
|
<div class="tempest-admin-table-wrap">
|
|
11965
12091
|
<table class="tempest-admin-list__table">
|
|
@@ -11983,6 +12109,7 @@ function renderDetailPage(context, view) {
|
|
|
11983
12109
|
const fields = view.fields.map(
|
|
11984
12110
|
(field) => `<dt>${escapeHtml(field.label)}</dt><dd>${field.value === "" ? "<em>\u2014</em>" : escapeHtml(field.value)}</dd>`
|
|
11985
12111
|
).join("");
|
|
12112
|
+
const auditPanel = view.audit === null ? "" : renderAuditPanel(view.audit);
|
|
11986
12113
|
const body = `<section class="tempest-admin-detail">
|
|
11987
12114
|
<header class="tempest-admin-detail__header">
|
|
11988
12115
|
<h1>${escapeHtml(view.title)} \xB7 ${escapeHtml(view.identity)}</h1>
|
|
@@ -11996,9 +12123,37 @@ function renderDetailPage(context, view) {
|
|
|
11996
12123
|
</div>
|
|
11997
12124
|
</header>
|
|
11998
12125
|
<dl class="tempest-admin-detail__fields">${fields}</dl>
|
|
12126
|
+
${auditPanel}
|
|
11999
12127
|
</section>`;
|
|
12000
12128
|
return renderLayout(context, `${view.title} \xB7 ${view.identity}`, body);
|
|
12001
12129
|
}
|
|
12130
|
+
function renderAuditPanel(audit) {
|
|
12131
|
+
const rows = audit.fields.map(
|
|
12132
|
+
(field) => `<dt>${escapeHtml(field.label)}</dt><dd>${field.value === "" ? "<em>\u2014</em>" : escapeHtml(field.value)}</dd>`
|
|
12133
|
+
).join("");
|
|
12134
|
+
const history = audit.history.length > 0 ? `<ol class="tempest-admin-history">${audit.history.map((entry) => {
|
|
12135
|
+
const changes = entry.changes.length > 0 ? `<table class="tempest-admin-history__changes"><thead><tr><th>Field</th><th>Before</th><th>After</th></tr></thead><tbody>${entry.changes.map(
|
|
12136
|
+
(change) => `<tr><td>${escapeHtml(change.field)}</td><td>${escapeHtml(change.before)}</td><td>${escapeHtml(change.after)}</td></tr>`
|
|
12137
|
+
).join("")}</tbody></table>` : "<p><em>No field changes recorded.</em></p>";
|
|
12138
|
+
const context = entry.context !== null ? `<pre class="tempest-admin-detail__json">${escapeHtml(entry.context)}</pre>` : "";
|
|
12139
|
+
return `<li class="tempest-admin-history__item">
|
|
12140
|
+
<details>
|
|
12141
|
+
<summary>
|
|
12142
|
+
<span class="tempest-admin-history__action">${escapeHtml(entry.action)}</span>
|
|
12143
|
+
<span class="tempest-admin-history__actor">${escapeHtml(entry.actor)}</span>
|
|
12144
|
+
<span class="tempest-admin-history__at">${escapeHtml(entry.at)}</span>
|
|
12145
|
+
</summary>
|
|
12146
|
+
${changes}
|
|
12147
|
+
${context}
|
|
12148
|
+
</details>
|
|
12149
|
+
</li>`;
|
|
12150
|
+
}).join("")}</ol>` : "";
|
|
12151
|
+
return `<section class="tempest-admin-audit">
|
|
12152
|
+
<h2>Audit</h2>
|
|
12153
|
+
<dl class="tempest-admin-detail__fields">${rows}</dl>
|
|
12154
|
+
${history}
|
|
12155
|
+
</section>`;
|
|
12156
|
+
}
|
|
12002
12157
|
function renderFormField(field) {
|
|
12003
12158
|
const required = field.required ? " required" : "";
|
|
12004
12159
|
const name = escapeHtml(field.name);
|
|
@@ -12062,6 +12217,7 @@ function renderFormPage(context, view) {
|
|
|
12062
12217
|
return renderLayout(context, `${heading} \xB7 ${context.site.title}`, body);
|
|
12063
12218
|
}
|
|
12064
12219
|
var logger2 = new JSONLogger("tempest_express_sdk.admin.router");
|
|
12220
|
+
var AUDIT_HISTORY_LIMIT = 50;
|
|
12065
12221
|
var FK_OPTION_CAP = 1e3;
|
|
12066
12222
|
var FLASH_MAX_LENGTH = 300;
|
|
12067
12223
|
var FLASH_MESSAGES = {
|
|
@@ -12100,18 +12256,23 @@ function makeAdminRouter(site, options) {
|
|
|
12100
12256
|
const backend = options.authBackend;
|
|
12101
12257
|
const router = express3.Router();
|
|
12102
12258
|
router.use(prefix, express3.urlencoded({ extended: false }));
|
|
12103
|
-
const context = (req, session) => ({
|
|
12259
|
+
const context = (req, session, models = site.list()) => ({
|
|
12104
12260
|
site,
|
|
12105
12261
|
theme,
|
|
12106
12262
|
prefix,
|
|
12107
12263
|
session,
|
|
12108
12264
|
currentPath: req.originalUrl.split("?")[0] ?? req.path,
|
|
12109
|
-
navModels:
|
|
12265
|
+
navModels: models.map((admin) => ({
|
|
12110
12266
|
label: admin.verboseNamePlural(),
|
|
12111
12267
|
url: `${prefix}/m/${admin.slug()}`
|
|
12112
12268
|
})),
|
|
12113
12269
|
messages: flashFor(req)
|
|
12114
12270
|
});
|
|
12271
|
+
const allows = async (principal, admin, action) => {
|
|
12272
|
+
if (!flagAllows(admin, action)) return false;
|
|
12273
|
+
if (options.accessPolicy === void 0) return true;
|
|
12274
|
+
return Boolean(await options.accessPolicy(principal, admin, action));
|
|
12275
|
+
};
|
|
12115
12276
|
const flashFor = (req) => {
|
|
12116
12277
|
const fixed = FLASH_MESSAGES[queryString(req.query.ok)];
|
|
12117
12278
|
if (fixed !== void 0) return [fixed];
|
|
@@ -12159,14 +12320,28 @@ function makeAdminRouter(site, options) {
|
|
|
12159
12320
|
res.redirect(`${prefix}/mfa`);
|
|
12160
12321
|
return null;
|
|
12161
12322
|
}
|
|
12162
|
-
|
|
12323
|
+
const visible = [];
|
|
12324
|
+
for (const admin of site.list()) {
|
|
12325
|
+
if (await allows(principal, admin, AdminPermission.VIEW)) visible.push(admin);
|
|
12326
|
+
}
|
|
12327
|
+
return { session, dbSession, principal, visible };
|
|
12163
12328
|
};
|
|
12164
|
-
const resolveAdmin = (req, res, state) => {
|
|
12329
|
+
const resolveAdmin = async (req, res, state, action = AdminPermission.VIEW) => {
|
|
12165
12330
|
const admin = site.get(String(req.params.slug));
|
|
12166
|
-
if (admin === null) {
|
|
12167
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12331
|
+
if (admin === null || !await allows(state.principal, admin, AdminPermission.VIEW)) {
|
|
12332
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12168
12333
|
return null;
|
|
12169
12334
|
}
|
|
12335
|
+
if (action !== AdminPermission.VIEW) {
|
|
12336
|
+
if (!flagAllows(admin, action)) {
|
|
12337
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12338
|
+
return null;
|
|
12339
|
+
}
|
|
12340
|
+
if (!await allows(state.principal, admin, action)) {
|
|
12341
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12342
|
+
return null;
|
|
12343
|
+
}
|
|
12344
|
+
}
|
|
12170
12345
|
return admin;
|
|
12171
12346
|
};
|
|
12172
12347
|
const renderNotFound = (ctx) => renderDashboardPage(
|
|
@@ -12177,7 +12352,7 @@ function makeAdminRouter(site, options) {
|
|
|
12177
12352
|
const checkCsrf = (req, res, state) => {
|
|
12178
12353
|
const body = req.body;
|
|
12179
12354
|
if (csrfTokenMatches(state.session, body?.csrf_token)) return true;
|
|
12180
|
-
html(res, renderNotFound(context(req, state.session)), 403);
|
|
12355
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12181
12356
|
return false;
|
|
12182
12357
|
};
|
|
12183
12358
|
router.get(`${prefix}/static/admin.css`, (_req, res) => {
|
|
@@ -12259,7 +12434,7 @@ function makeAdminRouter(site, options) {
|
|
|
12259
12434
|
const state = await authenticate(req, res);
|
|
12260
12435
|
if (state === null) return;
|
|
12261
12436
|
const cards = [];
|
|
12262
|
-
for (const admin of
|
|
12437
|
+
for (const admin of state.visible) {
|
|
12263
12438
|
let count = null;
|
|
12264
12439
|
try {
|
|
12265
12440
|
count = await admin.repository(state.dbSession).count();
|
|
@@ -12273,9 +12448,13 @@ function makeAdminRouter(site, options) {
|
|
|
12273
12448
|
label: admin.verboseNamePlural(),
|
|
12274
12449
|
count,
|
|
12275
12450
|
url: `${prefix}/m/${admin.slug()}`,
|
|
12276
|
-
newUrl: admin.
|
|
12451
|
+
newUrl: await allows(state.principal, admin, AdminPermission.CREATE) ? `${prefix}/m/${admin.slug()}/new` : null
|
|
12277
12452
|
});
|
|
12278
12453
|
}
|
|
12454
|
+
const businessCards = [];
|
|
12455
|
+
for (const card of site.dashboardCards) {
|
|
12456
|
+
businessCards.push(await computeBusinessCard(card, state.dbSession));
|
|
12457
|
+
}
|
|
12279
12458
|
let metrics = null;
|
|
12280
12459
|
if (showMetrics) {
|
|
12281
12460
|
const snapshot2 = MetricsUtils.system();
|
|
@@ -12286,7 +12465,15 @@ function makeAdminRouter(site, options) {
|
|
|
12286
12465
|
memoryTotalGb: (snapshot2.memory.total / 1024 ** 3).toFixed(1)
|
|
12287
12466
|
};
|
|
12288
12467
|
}
|
|
12289
|
-
html(
|
|
12468
|
+
html(
|
|
12469
|
+
res,
|
|
12470
|
+
renderDashboardPage(
|
|
12471
|
+
context(req, state.session, state.visible),
|
|
12472
|
+
cards,
|
|
12473
|
+
metrics,
|
|
12474
|
+
businessCards
|
|
12475
|
+
)
|
|
12476
|
+
);
|
|
12290
12477
|
})
|
|
12291
12478
|
);
|
|
12292
12479
|
router.get(
|
|
@@ -12294,7 +12481,7 @@ function makeAdminRouter(site, options) {
|
|
|
12294
12481
|
guarded(async (req, res) => {
|
|
12295
12482
|
const state = await authenticate(req, res);
|
|
12296
12483
|
if (state === null) return;
|
|
12297
|
-
const admin = resolveAdmin(req, res, state);
|
|
12484
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12298
12485
|
if (admin === null) return;
|
|
12299
12486
|
html(res, await renderList(req, admin, state));
|
|
12300
12487
|
})
|
|
@@ -12304,15 +12491,15 @@ function makeAdminRouter(site, options) {
|
|
|
12304
12491
|
guarded(async (req, res) => {
|
|
12305
12492
|
const state = await authenticate(req, res);
|
|
12306
12493
|
if (state === null) return;
|
|
12307
|
-
const admin = resolveAdmin(req, res, state);
|
|
12494
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.CREATE);
|
|
12308
12495
|
if (admin === null) return;
|
|
12309
12496
|
if (!admin.canCreate) {
|
|
12310
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12497
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12311
12498
|
return;
|
|
12312
12499
|
}
|
|
12313
12500
|
html(
|
|
12314
12501
|
res,
|
|
12315
|
-
renderFormPage(context(req, state.session), {
|
|
12502
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12316
12503
|
mode: "create",
|
|
12317
12504
|
title: admin.verboseName(),
|
|
12318
12505
|
fields: buildFormFields(admin, {
|
|
@@ -12330,10 +12517,10 @@ function makeAdminRouter(site, options) {
|
|
|
12330
12517
|
guarded(async (req, res) => {
|
|
12331
12518
|
const state = await authenticate(req, res);
|
|
12332
12519
|
if (state === null) return;
|
|
12333
|
-
const admin = resolveAdmin(req, res, state);
|
|
12520
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.CREATE);
|
|
12334
12521
|
if (admin === null) return;
|
|
12335
12522
|
if (!admin.canCreate) {
|
|
12336
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12523
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12337
12524
|
return;
|
|
12338
12525
|
}
|
|
12339
12526
|
if (!checkCsrf(req, res, state)) return;
|
|
@@ -12343,7 +12530,7 @@ function makeAdminRouter(site, options) {
|
|
|
12343
12530
|
const rerender = (error, status) => {
|
|
12344
12531
|
html(
|
|
12345
12532
|
res,
|
|
12346
|
-
renderFormPage(context(req, state.session), {
|
|
12533
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12347
12534
|
mode: "create",
|
|
12348
12535
|
title: admin.verboseName(),
|
|
12349
12536
|
fields: buildFormFields(admin, {
|
|
@@ -12362,6 +12549,7 @@ function makeAdminRouter(site, options) {
|
|
|
12362
12549
|
rerender("Please fix the highlighted fields.", 400);
|
|
12363
12550
|
return;
|
|
12364
12551
|
}
|
|
12552
|
+
stampActor(admin, parsed.data, backend.principalId(state.principal), true);
|
|
12365
12553
|
try {
|
|
12366
12554
|
await admin.repository(state.dbSession).create(parsed.data);
|
|
12367
12555
|
} catch (error) {
|
|
@@ -12376,11 +12564,11 @@ function makeAdminRouter(site, options) {
|
|
|
12376
12564
|
guarded(async (req, res) => {
|
|
12377
12565
|
const state = await authenticate(req, res);
|
|
12378
12566
|
if (state === null) return;
|
|
12379
|
-
const admin = resolveAdmin(req, res, state);
|
|
12567
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12380
12568
|
if (admin === null) return;
|
|
12381
12569
|
const format = String(req.params.format);
|
|
12382
12570
|
if (format !== "csv" && format !== "json") {
|
|
12383
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12571
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12384
12572
|
return;
|
|
12385
12573
|
}
|
|
12386
12574
|
const query = await resolveListQuery(req, admin, state.dbSession);
|
|
@@ -12402,7 +12590,7 @@ function makeAdminRouter(site, options) {
|
|
|
12402
12590
|
guarded(async (req, res) => {
|
|
12403
12591
|
const state = await authenticate(req, res);
|
|
12404
12592
|
if (state === null) return;
|
|
12405
|
-
const admin = resolveAdmin(req, res, state);
|
|
12593
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12406
12594
|
if (admin === null) return;
|
|
12407
12595
|
if (!checkCsrf(req, res, state)) return;
|
|
12408
12596
|
const body = req.body;
|
|
@@ -12418,7 +12606,12 @@ function makeAdminRouter(site, options) {
|
|
|
12418
12606
|
return;
|
|
12419
12607
|
}
|
|
12420
12608
|
if (!bulkActionsFor(admin).some((option) => option.value === action)) {
|
|
12421
|
-
html(res, renderNotFound(context(req, state.session)), 400);
|
|
12609
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 400);
|
|
12610
|
+
return;
|
|
12611
|
+
}
|
|
12612
|
+
const needed = action === "delete" ? AdminPermission.DELETE : AdminPermission.EDIT;
|
|
12613
|
+
if (!await allows(state.principal, admin, needed)) {
|
|
12614
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12422
12615
|
return;
|
|
12423
12616
|
}
|
|
12424
12617
|
const repository = admin.repository(state.dbSession);
|
|
@@ -12426,7 +12619,7 @@ function makeAdminRouter(site, options) {
|
|
|
12426
12619
|
if (action.startsWith("custom:")) {
|
|
12427
12620
|
const custom = admin.getAction(action.slice("custom:".length));
|
|
12428
12621
|
if (custom === null) {
|
|
12429
|
-
html(res, renderNotFound(context(req, state.session)), 400);
|
|
12622
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 400);
|
|
12430
12623
|
return;
|
|
12431
12624
|
}
|
|
12432
12625
|
let result;
|
|
@@ -12476,23 +12669,24 @@ function makeAdminRouter(site, options) {
|
|
|
12476
12669
|
guarded(async (req, res) => {
|
|
12477
12670
|
const state = await authenticate(req, res);
|
|
12478
12671
|
if (state === null) return;
|
|
12479
|
-
const admin = resolveAdmin(req, res, state);
|
|
12672
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12480
12673
|
if (admin === null) return;
|
|
12481
12674
|
const row = await findRow(admin, state.dbSession, String(req.params.identity));
|
|
12482
12675
|
if (row === null) {
|
|
12483
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12676
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12484
12677
|
return;
|
|
12485
12678
|
}
|
|
12486
12679
|
const identity = String(row[admin.identityField]);
|
|
12487
12680
|
html(
|
|
12488
12681
|
res,
|
|
12489
|
-
renderDetailPage(context(req, state.session), {
|
|
12682
|
+
renderDetailPage(context(req, state.session, state.visible), {
|
|
12490
12683
|
title: admin.verboseName(),
|
|
12491
12684
|
identity,
|
|
12685
|
+
audit: await buildAuditView(admin, row, state.dbSession),
|
|
12492
12686
|
fields: admin.detailFieldNames().map((name) => ({ label: name, value: formatCellValue(row[name]) })),
|
|
12493
12687
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
12494
|
-
editUrl: admin.
|
|
12495
|
-
deleteUrl: admin.
|
|
12688
|
+
editUrl: await allows(state.principal, admin, AdminPermission.EDIT) ? `${prefix}/m/${admin.slug()}/${identity}/edit` : null,
|
|
12689
|
+
deleteUrl: await allows(state.principal, admin, AdminPermission.DELETE) ? `${prefix}/m/${admin.slug()}/${identity}/delete` : null
|
|
12496
12690
|
})
|
|
12497
12691
|
);
|
|
12498
12692
|
})
|
|
@@ -12502,17 +12696,17 @@ function makeAdminRouter(site, options) {
|
|
|
12502
12696
|
guarded(async (req, res) => {
|
|
12503
12697
|
const state = await authenticate(req, res);
|
|
12504
12698
|
if (state === null) return;
|
|
12505
|
-
const admin = resolveAdmin(req, res, state);
|
|
12699
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.EDIT);
|
|
12506
12700
|
if (admin === null) return;
|
|
12507
12701
|
const identity = String(req.params.identity);
|
|
12508
12702
|
const row = admin.canEdit ? await findRow(admin, state.dbSession, identity) : null;
|
|
12509
12703
|
if (row === null) {
|
|
12510
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12704
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12511
12705
|
return;
|
|
12512
12706
|
}
|
|
12513
12707
|
html(
|
|
12514
12708
|
res,
|
|
12515
|
-
renderFormPage(context(req, state.session), {
|
|
12709
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12516
12710
|
mode: "edit",
|
|
12517
12711
|
title: admin.verboseName(),
|
|
12518
12712
|
fields: buildFormFields(admin, {
|
|
@@ -12531,11 +12725,11 @@ function makeAdminRouter(site, options) {
|
|
|
12531
12725
|
guarded(async (req, res) => {
|
|
12532
12726
|
const state = await authenticate(req, res);
|
|
12533
12727
|
if (state === null) return;
|
|
12534
|
-
const admin = resolveAdmin(req, res, state);
|
|
12728
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.EDIT);
|
|
12535
12729
|
if (admin === null) return;
|
|
12536
12730
|
const identity = String(req.params.identity);
|
|
12537
12731
|
if (!admin.canEdit) {
|
|
12538
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12732
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12539
12733
|
return;
|
|
12540
12734
|
}
|
|
12541
12735
|
if (!checkCsrf(req, res, state)) return;
|
|
@@ -12545,7 +12739,7 @@ function makeAdminRouter(site, options) {
|
|
|
12545
12739
|
const rerender = (error, status) => {
|
|
12546
12740
|
html(
|
|
12547
12741
|
res,
|
|
12548
|
-
renderFormPage(context(req, state.session), {
|
|
12742
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12549
12743
|
mode: "edit",
|
|
12550
12744
|
title: admin.verboseName(),
|
|
12551
12745
|
fields: buildFormFields(admin, {
|
|
@@ -12564,10 +12758,16 @@ function makeAdminRouter(site, options) {
|
|
|
12564
12758
|
rerender("Please fix the highlighted fields.", 400);
|
|
12565
12759
|
return;
|
|
12566
12760
|
}
|
|
12761
|
+
stampActor(
|
|
12762
|
+
admin,
|
|
12763
|
+
parsed.data,
|
|
12764
|
+
backend.principalId(state.principal),
|
|
12765
|
+
false
|
|
12766
|
+
);
|
|
12567
12767
|
try {
|
|
12568
12768
|
const changed = await admin.repository(state.dbSession).update({ [admin.identityField]: identity }, parsed.data);
|
|
12569
12769
|
if (changed === 0) {
|
|
12570
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12770
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12571
12771
|
return;
|
|
12572
12772
|
}
|
|
12573
12773
|
} catch (error) {
|
|
@@ -12582,10 +12782,10 @@ function makeAdminRouter(site, options) {
|
|
|
12582
12782
|
guarded(async (req, res) => {
|
|
12583
12783
|
const state = await authenticate(req, res);
|
|
12584
12784
|
if (state === null) return;
|
|
12585
|
-
const admin = resolveAdmin(req, res, state);
|
|
12785
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.DELETE);
|
|
12586
12786
|
if (admin === null) return;
|
|
12587
12787
|
if (!admin.canDelete) {
|
|
12588
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12788
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12589
12789
|
return;
|
|
12590
12790
|
}
|
|
12591
12791
|
if (!checkCsrf(req, res, state)) return;
|
|
@@ -12593,10 +12793,58 @@ function makeAdminRouter(site, options) {
|
|
|
12593
12793
|
res.redirect(`${prefix}/m/${admin.slug()}?ok=deleted`);
|
|
12594
12794
|
})
|
|
12595
12795
|
);
|
|
12796
|
+
async function actorLabel(actor, dbSession) {
|
|
12797
|
+
if (actor === null || actor === void 0 || actor === "") return "";
|
|
12798
|
+
const principal = await backend.loadPrincipal(dbSession, String(actor));
|
|
12799
|
+
return principal === null ? String(actor) : backend.displayName(principal);
|
|
12800
|
+
}
|
|
12801
|
+
async function buildAuditView(admin, row, dbSession) {
|
|
12802
|
+
const fields = [];
|
|
12803
|
+
for (const name of admin.auditFieldNames()) {
|
|
12804
|
+
const value = name === "createdBy" || name === "updatedBy" ? await actorLabel(row[name], dbSession) : formatCellValue(row[name]);
|
|
12805
|
+
fields.push({ label: humanizeField(name), value });
|
|
12806
|
+
}
|
|
12807
|
+
const history = [];
|
|
12808
|
+
if (admin.auditModel !== null) {
|
|
12809
|
+
const entity = admin.model.name;
|
|
12810
|
+
const entityId = String(row[admin.identityField]);
|
|
12811
|
+
const page2 = await new BaseRepository(admin.auditModel, dbSession).paginate({
|
|
12812
|
+
page: 1,
|
|
12813
|
+
pageSize: AUDIT_HISTORY_LIMIT,
|
|
12814
|
+
orderBy: "createdAt",
|
|
12815
|
+
ascending: false,
|
|
12816
|
+
filters: { entity, entityId }
|
|
12817
|
+
});
|
|
12818
|
+
for (const entry of page2.items) {
|
|
12819
|
+
const changes = entry.changes ?? {};
|
|
12820
|
+
history.push({
|
|
12821
|
+
action: String(entry.action ?? ""),
|
|
12822
|
+
at: formatCellValue(entry.createdAt),
|
|
12823
|
+
actor: await actorLabel(entry.actor, dbSession) || "\u2014",
|
|
12824
|
+
changes: Object.entries(changes).map(([field, change]) => ({
|
|
12825
|
+
field,
|
|
12826
|
+
before: formatCellValue(change?.before),
|
|
12827
|
+
after: formatCellValue(change?.after)
|
|
12828
|
+
})),
|
|
12829
|
+
context: entry.context === null || entry.context === void 0 ? null : JSON.stringify(entry.context, null, 2)
|
|
12830
|
+
});
|
|
12831
|
+
}
|
|
12832
|
+
}
|
|
12833
|
+
if (fields.length === 0 && history.length === 0) return null;
|
|
12834
|
+
return { fields, history };
|
|
12835
|
+
}
|
|
12596
12836
|
async function findRow(admin, dbSession, identity) {
|
|
12597
12837
|
const row = await admin.repository(dbSession).first({ [admin.identityField]: identity });
|
|
12598
12838
|
return row ?? null;
|
|
12599
12839
|
}
|
|
12840
|
+
async function permittedBulkActions(admin, principal) {
|
|
12841
|
+
const permitted = [];
|
|
12842
|
+
for (const option of bulkActionsFor(admin)) {
|
|
12843
|
+
const needed = option.value === "delete" ? AdminPermission.DELETE : AdminPermission.EDIT;
|
|
12844
|
+
if (await allows(principal, admin, needed)) permitted.push(option);
|
|
12845
|
+
}
|
|
12846
|
+
return permitted;
|
|
12847
|
+
}
|
|
12600
12848
|
async function renderList(req, admin, state) {
|
|
12601
12849
|
const columns = adminColumns(admin.model);
|
|
12602
12850
|
const query = await resolveListQuery(req, admin, state.dbSession);
|
|
@@ -12651,21 +12899,36 @@ function makeAdminRouter(site, options) {
|
|
|
12651
12899
|
searchValue: query.search,
|
|
12652
12900
|
filters: query.filterViews,
|
|
12653
12901
|
sort,
|
|
12654
|
-
newUrl: admin.
|
|
12655
|
-
bulkActions:
|
|
12902
|
+
newUrl: await allows(state.principal, admin, AdminPermission.CREATE) ? `${prefix}/m/${admin.slug()}/new` : null,
|
|
12903
|
+
bulkActions: await permittedBulkActions(admin, state.principal),
|
|
12656
12904
|
bulkUrl: `${prefix}/m/${admin.slug()}/bulk`,
|
|
12657
12905
|
exportCsvUrl: exportUrl("csv"),
|
|
12658
|
-
exportJsonUrl: exportUrl("json")
|
|
12906
|
+
exportJsonUrl: exportUrl("json"),
|
|
12907
|
+
lenses: admin.lenses.length === 0 ? [] : [
|
|
12908
|
+
{
|
|
12909
|
+
label: "All",
|
|
12910
|
+
url: `?${buildQuery({ ...query.baseQuery, lens: void 0 })}`,
|
|
12911
|
+
active: query.lens === ""
|
|
12912
|
+
},
|
|
12913
|
+
...admin.lenses.map((entry) => ({
|
|
12914
|
+
label: entry.label,
|
|
12915
|
+
url: `?${buildQuery({ ...query.baseQuery, lens: entry.slug })}`,
|
|
12916
|
+
active: query.lens === entry.slug
|
|
12917
|
+
}))
|
|
12918
|
+
]
|
|
12659
12919
|
};
|
|
12660
|
-
return renderListPage(context(req, state.session), view);
|
|
12920
|
+
return renderListPage(context(req, state.session, state.visible), view);
|
|
12661
12921
|
}
|
|
12662
12922
|
async function resolveListQuery(req, admin, dbSession) {
|
|
12663
12923
|
const columns = adminColumns(admin.model);
|
|
12664
12924
|
const search = queryString(req.query.q);
|
|
12665
12925
|
const sortField = queryString(req.query.sort);
|
|
12666
12926
|
const sortColumn = sortField in columns ? sortField : null;
|
|
12667
|
-
const
|
|
12927
|
+
const lens = admin.getLens(queryString(req.query.lens));
|
|
12668
12928
|
const conditions = [];
|
|
12929
|
+
if (lens !== null && Object.keys(lens.filters).length > 0) {
|
|
12930
|
+
conditions.push(lens.filters);
|
|
12931
|
+
}
|
|
12669
12932
|
const filterViews = [];
|
|
12670
12933
|
for (const field of admin.listFilter) {
|
|
12671
12934
|
const column6 = columns[field];
|
|
@@ -12729,15 +12992,21 @@ function makeAdminRouter(site, options) {
|
|
|
12729
12992
|
baseQuery[`filter_${view.field}`] = view.value;
|
|
12730
12993
|
}
|
|
12731
12994
|
}
|
|
12995
|
+
const lensOrder = lens?.orderBy ?? null;
|
|
12996
|
+
const lensDescending = lensOrder?.startsWith("-");
|
|
12997
|
+
const lensColumn = lensOrder === null ? null : lensOrder.replace(/^-/, "");
|
|
12998
|
+
const ascending = sortColumn !== null ? queryString(req.query.dir) !== "desc" : lensColumn !== null ? !lensDescending : admin.orderAscending;
|
|
12999
|
+
if (lens !== null) baseQuery.lens = lens.slug;
|
|
12732
13000
|
return {
|
|
12733
13001
|
search,
|
|
12734
13002
|
searchable,
|
|
12735
13003
|
where: conditions.length === 0 ? void 0 : and(...conditions),
|
|
12736
|
-
orderBy: sortColumn ?? admin.orderKey ?? void 0,
|
|
13004
|
+
orderBy: sortColumn ?? lensColumn ?? admin.orderKey ?? void 0,
|
|
12737
13005
|
ascending,
|
|
12738
13006
|
sortColumn,
|
|
12739
13007
|
filterViews,
|
|
12740
|
-
baseQuery
|
|
13008
|
+
baseQuery,
|
|
13009
|
+
lens: lens?.slug ?? ""
|
|
12741
13010
|
};
|
|
12742
13011
|
}
|
|
12743
13012
|
async function relatedOptions(column6, dbSession) {
|
|
@@ -12764,6 +13033,76 @@ function makeAdminRouter(site, options) {
|
|
|
12764
13033
|
}
|
|
12765
13034
|
return router;
|
|
12766
13035
|
}
|
|
13036
|
+
function flagAllows(admin, action) {
|
|
13037
|
+
if (action === AdminPermission.CREATE) return admin.canCreate;
|
|
13038
|
+
if (action === AdminPermission.EDIT) return admin.canEdit;
|
|
13039
|
+
if (action === AdminPermission.DELETE) return admin.canDelete;
|
|
13040
|
+
return true;
|
|
13041
|
+
}
|
|
13042
|
+
function stampActor(admin, data, actorId, creating) {
|
|
13043
|
+
const columns = adminColumns(admin.model);
|
|
13044
|
+
if (creating && "createdBy" in columns) data.createdBy = actorId;
|
|
13045
|
+
if ("updatedBy" in columns) data.updatedBy = actorId;
|
|
13046
|
+
}
|
|
13047
|
+
function formatMetric(value) {
|
|
13048
|
+
if (typeof value === "string") return value;
|
|
13049
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
13050
|
+
}
|
|
13051
|
+
async function computeBusinessCard(card, dbSession) {
|
|
13052
|
+
const base = {
|
|
13053
|
+
label: card.label,
|
|
13054
|
+
unit: null,
|
|
13055
|
+
direction: "flat",
|
|
13056
|
+
percent: null,
|
|
13057
|
+
previous: "",
|
|
13058
|
+
segments: [],
|
|
13059
|
+
helpText: card.helpText ?? null
|
|
13060
|
+
};
|
|
13061
|
+
let data;
|
|
13062
|
+
try {
|
|
13063
|
+
data = await card.compute(dbSession);
|
|
13064
|
+
} catch (error) {
|
|
13065
|
+
logger2.warning("Admin dashboard card failed", {
|
|
13066
|
+
card: card.label,
|
|
13067
|
+
error: error instanceof Error ? error.message : String(error)
|
|
13068
|
+
});
|
|
13069
|
+
return { ...base, kind: "value", value: "", error: "Could not compute this metric." };
|
|
13070
|
+
}
|
|
13071
|
+
if (data.kind === "partition") {
|
|
13072
|
+
const total = partitionTotal(data);
|
|
13073
|
+
return {
|
|
13074
|
+
...base,
|
|
13075
|
+
kind: "partition",
|
|
13076
|
+
value: formatMetric(total),
|
|
13077
|
+
segments: data.segments.map((segment) => ({
|
|
13078
|
+
label: segment.label,
|
|
13079
|
+
value: formatMetric(segment.value),
|
|
13080
|
+
percent: total === 0 ? 0 : segment.value / total * 100
|
|
13081
|
+
})),
|
|
13082
|
+
error: null
|
|
13083
|
+
};
|
|
13084
|
+
}
|
|
13085
|
+
if (data.kind === "trend") {
|
|
13086
|
+
const percent = trendPercent(data);
|
|
13087
|
+
return {
|
|
13088
|
+
...base,
|
|
13089
|
+
kind: "trend",
|
|
13090
|
+
value: formatMetric(data.value),
|
|
13091
|
+
unit: data.unit ?? null,
|
|
13092
|
+
direction: trendDirection(data),
|
|
13093
|
+
percent: percent === null ? null : `${percent >= 0 ? "+" : ""}${percent.toFixed(1)}%`,
|
|
13094
|
+
previous: formatMetric(data.previous),
|
|
13095
|
+
error: null
|
|
13096
|
+
};
|
|
13097
|
+
}
|
|
13098
|
+
return {
|
|
13099
|
+
...base,
|
|
13100
|
+
kind: "value",
|
|
13101
|
+
value: formatMetric(data.value),
|
|
13102
|
+
unit: data.unit ?? null,
|
|
13103
|
+
error: null
|
|
13104
|
+
};
|
|
13105
|
+
}
|
|
12767
13106
|
function bulkActionsFor(admin) {
|
|
12768
13107
|
const actions = [];
|
|
12769
13108
|
const hasActiveFlag = "isActive" in adminColumns(admin.model);
|
|
@@ -14815,6 +15154,6 @@ async function withTestDatabase(models, fn) {
|
|
|
14815
15154
|
}
|
|
14816
15155
|
}
|
|
14817
15156
|
|
|
14818
|
-
export { ADMIN_CSS, ActivationService, AdminJsonSite, AdminModel, AdminSessionStore, AdminSite, AppException, AttemptThrottle, AuditAction, BaseAuditLogModel, BaseController, BaseModel, BaseOAuthClient, BaseOutboxModel, BaseService, BaseUserModel, BaseUserRefreshTokenModel, BaseUserTokenModel, CEP_PATTERN, CNPJ_PATTERN, CPF_PATTERN, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, CircuitOpenError, CompositeFeatureFlagBackend, ConflictException, DEFAULT_DOCS_FAVICON, DEFAULT_LOCALE, EmailProvider, EmailUtils, EnvFeatureFlagBackend, EventStream, ExpiredTokenException, FeatureFlags, ForbiddenException, GitHubOAuthClient, GoogleOAuthClient, GracefulShutdown, HTTPClient, HTTP_500_LOG_FILE, HTTP_500_MARKER, HttpMetrics, IDEMPOTENCY_HEADER, InvalidTokenException, JSONLogger, JWTUtils, LEVEL_LOG_FILES, LocalUploadStorage, MemoryBroker, MemoryCacheManager, MemoryFeatureFlagBackend, MemoryIdempotencyStore, MemoryRateLimitStore, MemorySessionStore, MemoryThrottleBackend, MessageCatalog, MessagingHub, MetricsUtils, MfaService, NotFoundException, OAuthError, OIDCProvider, OutboxRelay, OutboxStatus, PHONE_BR_PATTERN, PasswordResetService, REDOC_CDN_URL, REQUEST_ID_HEADER, RabbitBroker, RedisCacheManager, RedisIdempotencyStore, RedisRateLimitStore, RedisSSEBroker, RedisSessionStore, Region, RetryPolicy, S3UploadStorage, SSEBroker, ServerSentEvent, SessionService, TOTPHelper, TaskManager, TelegramProvider, TenantScopedRepository, TooManyRequestsException, TwilioSmsProvider, UF, UnauthorizedException, UserAuthService, UserModelAuthBackend, UserTokenPurpose, ValidationException, WebPushDispatcher, WebPushError, WebPushGoneError, WebSocketHub, WebhookSignatureVerifier, WhatsAppProvider, activationSchema, addLogSink, adminAction, adminColumns, adminThemeCss, attachWebSocketHub, authResponseSchema, authSettingsShape, backupDatabase, bearerToken, bodySizeLimitMiddleware, broadcastText, buildContentDisposition, buildFormFields, buildPaginationLinkHeader, cached2 as cached, cepField, citiesByUf, cnpjField, coerceFlag, configureFileLogging, configureLogging, cpfField, cpfOrCnpjField, createApp, createOpenApiRegistry, createTestDatabase, createdByColumn, csrfMiddleware, csrfTokenMatches, cursorPaginationFilterSchema, cursorPaginationSchema, decodeCursor, defaultMessageCatalog, defineEnum, deletedAtColumn, diffSnapshots, emailSettingsShape, encodeCursor, envList, escapeHtml, filterForColumn, foreignKeyFields, foreignKeyLabel, foreignKeyTable, formatCellValue, formatFieldValue, generateCsrfToken, generateOAuthState, generateOpaqueToken, generateOpenApiDocument, getAuth, getClientIp, getConditions, getPaginationConditions, getRequestId, getState, hashOpaqueToken, humanizeField, idempotencyMiddleware, inboundMessageSchema, isColumnOptional, isSearchableColumn, isValidCep, isValidCity, isValidCnpj, isValidCpf, isValidCpfCnpj, isValidPhoneBr, isValidUf, jwtSettingsShape, keyByHeader, keyByIp, keyByJwtClaim, keyByJwtSubject, listStates, logEntrySchema, logSettingsShape, loginSchema, makeAdminJsonRouter, makeAdminRouter, makeAppExceptionHandler, makeAuthRouter, makeFlagGuard, makeHealthRouter, makeJwtAuthMiddleware, makeLogsRouter, makeMetricsRouter, makeSessionMiddleware, makeToolSpecRouter, makeTwilioWebhookRouter, makeUnhandledExceptionHandler, makeWhatsAppWebhookRouter, mfaChallengeSchema, mfaCodeSchema, mfaEnrollResponseSchema, minioSettingsShape, modifyDict, mountOpenApiJson, mountRedoc, mountSwaggerUi, normalizeCep, normalizeCnpj, normalizeCpf, normalizeCpfCnpj, normalizePhoneBr, normalizeUf, notFoundHandler, onlyDigits, paginationFilterSchema, paginationSchema, parseAcceptLanguage, parseCookies, parseFormBody, passwordResetConfirmSchema, passwordResetRequestSchema, phoneBrField, prometheusMiddleware, rabbitmqSettingsShape, rateLimitMiddleware, redisSettingsShape, refreshSchema, registerExceptionHandlers, renderAuthResultPage, renderDashboardPage, renderDetailPage, renderFormPage, renderLayout, renderListPage, renderLoginPage, renderMfaPage, renderPasswordResetFormPage, requestIdMiddleware, requestTracingMiddleware, requireRoles, resolveAdminTheme, resolveDownloadPath, resolveRedocBundle, runServer, runWithRequestContext, sendBytesDownload, sendFileDownload, sessionCookie, sessionSettingsShape, setRequestId, signupSchema, snapshot, sseResponse, statesByRegion, syncFilterSchema, syncPaginationSchema, tableNameFor, toUtc, tokenFromUrl, tokenPairSchema, tokenSettingsShape, ufField, updatedByColumn, uploadSettingsShape, userPublicSchema, utcnow, validateTwilioSignature, verifyOpaqueToken, webPushKeysSchema, webPushPayloadSchema, webPushSettingsShape, webPushSubscriptionSchema, webSocketSettingsShape, widgetForColumn, withTestDatabase, wrapWithSlowQueryLog, wsEnvelopeSchema };
|
|
15157
|
+
export { ADMIN_CSS, ActivationService, AdminJsonSite, AdminModel, AdminPermission, AdminSessionStore, AdminSite, AppException, AttemptThrottle, AuditAction, BaseAuditLogModel, BaseController, BaseModel, BaseOAuthClient, BaseOutboxModel, BaseService, BaseUserModel, BaseUserRefreshTokenModel, BaseUserTokenModel, CEP_PATTERN, CNPJ_PATTERN, CPF_PATTERN, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, CircuitOpenError, CompositeFeatureFlagBackend, ConflictException, DEFAULT_DOCS_FAVICON, DEFAULT_LOCALE, EmailProvider, EmailUtils, EnvFeatureFlagBackend, EventStream, ExpiredTokenException, FeatureFlags, ForbiddenException, GitHubOAuthClient, GoogleOAuthClient, GracefulShutdown, HTTPClient, HTTP_500_LOG_FILE, HTTP_500_MARKER, HttpMetrics, IDEMPOTENCY_HEADER, InvalidTokenException, JSONLogger, JWTUtils, LEVEL_LOG_FILES, LocalUploadStorage, MemoryBroker, MemoryCacheManager, MemoryFeatureFlagBackend, MemoryIdempotencyStore, MemoryRateLimitStore, MemorySessionStore, MemoryThrottleBackend, MessageCatalog, MessagingHub, MetricsUtils, MfaService, NotFoundException, OAuthError, OIDCProvider, OutboxRelay, OutboxStatus, PHONE_BR_PATTERN, PasswordResetService, REDOC_CDN_URL, REQUEST_ID_HEADER, RabbitBroker, RedisCacheManager, RedisIdempotencyStore, RedisRateLimitStore, RedisSSEBroker, RedisSessionStore, Region, RetryPolicy, S3UploadStorage, SSEBroker, ServerSentEvent, SessionService, TOTPHelper, TaskManager, TelegramProvider, TenantScopedRepository, TooManyRequestsException, TwilioSmsProvider, UF, UnauthorizedException, UserAuthService, UserModelAuthBackend, UserTokenPurpose, ValidationException, WebPushDispatcher, WebPushError, WebPushGoneError, WebSocketHub, WebhookSignatureVerifier, WhatsAppProvider, activationSchema, addLogSink, adminAction, adminColumns, adminLens, adminThemeCss, attachWebSocketHub, authResponseSchema, authSettingsShape, backupDatabase, bearerToken, bodySizeLimitMiddleware, broadcastText, buildContentDisposition, buildFormFields, buildPaginationLinkHeader, cached2 as cached, cepField, citiesByUf, cnpjField, coerceFlag, configureFileLogging, configureLogging, cpfField, cpfOrCnpjField, createApp, createOpenApiRegistry, createTestDatabase, createdByColumn, csrfMiddleware, csrfTokenMatches, cursorPaginationFilterSchema, cursorPaginationSchema, decodeCursor, defaultMessageCatalog, defineEnum, deletedAtColumn, diffSnapshots, emailSettingsShape, encodeCursor, envList, escapeHtml, filterForColumn, foreignKeyFields, foreignKeyLabel, foreignKeyTable, formatCellValue, formatFieldValue, generateCsrfToken, generateOAuthState, generateOpaqueToken, generateOpenApiDocument, getAuth, getClientIp, getConditions, getPaginationConditions, getRequestId, getState, hashOpaqueToken, humanizeField, idempotencyMiddleware, inboundMessageSchema, isColumnOptional, isSearchableColumn, isValidCep, isValidCity, isValidCnpj, isValidCpf, isValidCpfCnpj, isValidPhoneBr, isValidUf, jwtSettingsShape, keyByHeader, keyByIp, keyByJwtClaim, keyByJwtSubject, listStates, logEntrySchema, logSettingsShape, loginSchema, makeAdminJsonRouter, makeAdminRouter, makeAppExceptionHandler, makeAuthRouter, makeFlagGuard, makeHealthRouter, makeJwtAuthMiddleware, makeLogsRouter, makeMetricsRouter, makeSessionMiddleware, makeToolSpecRouter, makeTwilioWebhookRouter, makeUnhandledExceptionHandler, makeWhatsAppWebhookRouter, metricCard, mfaChallengeSchema, mfaCodeSchema, mfaEnrollResponseSchema, minioSettingsShape, modifyDict, mountOpenApiJson, mountRedoc, mountSwaggerUi, normalizeCep, normalizeCnpj, normalizeCpf, normalizeCpfCnpj, normalizePhoneBr, normalizeUf, notFoundHandler, onlyDigits, paginationFilterSchema, paginationSchema, parseAcceptLanguage, parseCookies, parseFormBody, partitionTotal, passwordResetConfirmSchema, passwordResetRequestSchema, phoneBrField, prometheusMiddleware, rabbitmqSettingsShape, rateLimitMiddleware, redisSettingsShape, refreshSchema, registerExceptionHandlers, renderAuthResultPage, renderDashboardPage, renderDetailPage, renderFormPage, renderLayout, renderListPage, renderLoginPage, renderMfaPage, renderPasswordResetFormPage, requestIdMiddleware, requestTracingMiddleware, requireRoles, resolveAdminTheme, resolveDownloadPath, resolveRedocBundle, runServer, runWithRequestContext, sendBytesDownload, sendFileDownload, sessionCookie, sessionSettingsShape, setRequestId, signupSchema, snapshot, sseResponse, statesByRegion, syncFilterSchema, syncPaginationSchema, tableNameFor, toUtc, tokenFromUrl, tokenPairSchema, tokenSettingsShape, trendDirection, trendPercent, ufField, updatedByColumn, uploadSettingsShape, userPublicSchema, utcnow, validateTwilioSignature, verifyOpaqueToken, webPushKeysSchema, webPushPayloadSchema, webPushSettingsShape, webPushSubscriptionSchema, webSocketSettingsShape, widgetForColumn, withTestDatabase, wrapWithSlowQueryLog, wsEnvelopeSchema };
|
|
14819
15158
|
//# sourceMappingURL=index.js.map
|
|
14820
15159
|
//# sourceMappingURL=index.js.map
|