tempest-express-sdk 0.24.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-6KJTKSG5.js → chunk-GLZYNX63.js} +3 -3
- package/dist/{chunk-6KJTKSG5.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 +777 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +625 -178
- package/dist/index.d.ts +625 -178
- package/dist/index.js +769 -111
- 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,6 +9317,60 @@ 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
|
+
|
|
9359
|
+
// src/admin/actions.ts
|
|
9360
|
+
function slugify2(label) {
|
|
9361
|
+
return label.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "action";
|
|
9362
|
+
}
|
|
9363
|
+
function adminAction(options, handler) {
|
|
9364
|
+
const name = options.name ?? slugify2(options.label);
|
|
9365
|
+
if (name === "") throw new Error("adminAction requires a non-empty name or label");
|
|
9366
|
+
return {
|
|
9367
|
+
name,
|
|
9368
|
+
label: options.label,
|
|
9369
|
+
handler,
|
|
9370
|
+
dangerous: options.dangerous ?? false
|
|
9371
|
+
};
|
|
9372
|
+
}
|
|
9373
|
+
|
|
9320
9374
|
// src/admin/columns.ts
|
|
9321
9375
|
function humanizeField(name) {
|
|
9322
9376
|
return name.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim().split(/\s+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
@@ -9397,6 +9451,9 @@ function filterForColumn(column6) {
|
|
|
9397
9451
|
}
|
|
9398
9452
|
return { kind: "text", options: [] };
|
|
9399
9453
|
}
|
|
9454
|
+
function foreignKeyTable(column6) {
|
|
9455
|
+
return column6.reference?.table ?? null;
|
|
9456
|
+
}
|
|
9400
9457
|
function isSearchableColumn(column6) {
|
|
9401
9458
|
const { kind } = column6.type;
|
|
9402
9459
|
return kind === "varchar" || kind === "text" || kind === "char";
|
|
@@ -9410,6 +9467,12 @@ var NEVER_EDITABLE = [
|
|
|
9410
9467
|
"hashedPassword"
|
|
9411
9468
|
];
|
|
9412
9469
|
var NEVER_LISTED = ["hashedPassword"];
|
|
9470
|
+
var AUDIT_FIELDS = [
|
|
9471
|
+
"createdAt",
|
|
9472
|
+
"updatedAt",
|
|
9473
|
+
"createdBy",
|
|
9474
|
+
"updatedBy"
|
|
9475
|
+
];
|
|
9413
9476
|
var AdminModel = class {
|
|
9414
9477
|
/** The managed model class. */
|
|
9415
9478
|
model;
|
|
@@ -9433,6 +9496,11 @@ var AdminModel = class {
|
|
|
9433
9496
|
canEdit;
|
|
9434
9497
|
/** Whether the delete action is exposed. */
|
|
9435
9498
|
canDelete;
|
|
9499
|
+
/** Audit-log model backing the detail timeline, or `null`. */
|
|
9500
|
+
auditModel;
|
|
9501
|
+
/** Saved list-view presets, in declaration order. */
|
|
9502
|
+
lenses;
|
|
9503
|
+
actions = /* @__PURE__ */ new Map();
|
|
9436
9504
|
slugOverride;
|
|
9437
9505
|
listDisplayOverride;
|
|
9438
9506
|
verboseNameOverride;
|
|
@@ -9457,6 +9525,16 @@ var AdminModel = class {
|
|
|
9457
9525
|
this.canCreate = options.canCreate ?? true;
|
|
9458
9526
|
this.canEdit = options.canEdit ?? true;
|
|
9459
9527
|
this.canDelete = options.canDelete ?? true;
|
|
9528
|
+
this.auditModel = options.auditModel ?? null;
|
|
9529
|
+
this.lenses = [...options.lenses ?? []];
|
|
9530
|
+
for (const action of options.actions ?? []) {
|
|
9531
|
+
if (this.actions.has(action.name)) {
|
|
9532
|
+
throw new Error(
|
|
9533
|
+
`Duplicate admin action name "${action.name}" on ${this.model.tablename}`
|
|
9534
|
+
);
|
|
9535
|
+
}
|
|
9536
|
+
this.actions.set(action.name, action);
|
|
9537
|
+
}
|
|
9460
9538
|
const known = new Set(this.columnNames());
|
|
9461
9539
|
for (const [option, names] of [
|
|
9462
9540
|
["listDisplay", this.listDisplayOverride ?? []],
|
|
@@ -9533,6 +9611,25 @@ var AdminModel = class {
|
|
|
9533
9611
|
if (this.listDisplayOverride !== null) return [...this.listDisplayOverride];
|
|
9534
9612
|
return this.columnNames().filter((name) => !NEVER_LISTED.includes(name));
|
|
9535
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
|
+
}
|
|
9536
9633
|
/**
|
|
9537
9634
|
* Return the columns the detail view renders.
|
|
9538
9635
|
*
|
|
@@ -9541,10 +9638,16 @@ var AdminModel = class {
|
|
|
9541
9638
|
* where an operator goes to see the whole record, so trimming it there would
|
|
9542
9639
|
* hide data with nowhere else to read it.
|
|
9543
9640
|
*
|
|
9544
|
-
*
|
|
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.
|
|
9545
9646
|
*/
|
|
9546
9647
|
detailFieldNames() {
|
|
9547
|
-
return this.columnNames().filter(
|
|
9648
|
+
return this.columnNames().filter(
|
|
9649
|
+
(name) => !NEVER_LISTED.includes(name) && !AUDIT_FIELDS.includes(name)
|
|
9650
|
+
);
|
|
9548
9651
|
}
|
|
9549
9652
|
/**
|
|
9550
9653
|
* Return the columns a create/edit form exposes.
|
|
@@ -9559,6 +9662,25 @@ var AdminModel = class {
|
|
|
9559
9662
|
const skip = /* @__PURE__ */ new Set([...this.readonlyFields, ...NEVER_EDITABLE]);
|
|
9560
9663
|
return this.columnNames().filter((name) => !skip.has(name));
|
|
9561
9664
|
}
|
|
9665
|
+
/**
|
|
9666
|
+
* Return the registered custom actions, in declaration order.
|
|
9667
|
+
*
|
|
9668
|
+
* @returns The actions passed via `actions` (empty when none). The model
|
|
9669
|
+
* type is erased here, the way {@link AdminSite} erases it when it stores a
|
|
9670
|
+
* configuration — a registry keyed by slug cannot stay generic.
|
|
9671
|
+
*/
|
|
9672
|
+
customActions() {
|
|
9673
|
+
return [...this.actions.values()];
|
|
9674
|
+
}
|
|
9675
|
+
/**
|
|
9676
|
+
* Look a custom action up by name.
|
|
9677
|
+
*
|
|
9678
|
+
* @param name - The action identifier (its submitted form value).
|
|
9679
|
+
* @returns The action, or `null` when nothing matches.
|
|
9680
|
+
*/
|
|
9681
|
+
getAction(name) {
|
|
9682
|
+
return this.actions.get(name) ?? null;
|
|
9683
|
+
}
|
|
9562
9684
|
/**
|
|
9563
9685
|
* Build a repository for this model bound to a session.
|
|
9564
9686
|
*
|
|
@@ -9600,10 +9722,12 @@ function buildFormFields(admin, options = {}) {
|
|
|
9600
9722
|
const columns = adminColumns(admin.model);
|
|
9601
9723
|
const values = options.values ?? {};
|
|
9602
9724
|
const errors = options.errors ?? {};
|
|
9725
|
+
const foreignKeys = options.foreignKeyOptions ?? {};
|
|
9603
9726
|
return admin.editableFieldNames().flatMap((name) => {
|
|
9604
9727
|
const column6 = columns[name];
|
|
9605
9728
|
if (column6 === void 0) return [];
|
|
9606
|
-
const
|
|
9729
|
+
const related = foreignKeys[name];
|
|
9730
|
+
const spec = related === void 0 ? widgetForColumn(column6) : { widget: "select", step: null, options: related };
|
|
9607
9731
|
const raw = name in values ? values[name] : literalDefault(column6);
|
|
9608
9732
|
return [
|
|
9609
9733
|
{
|
|
@@ -9653,7 +9777,7 @@ function coerceValue(column6, widget, raw) {
|
|
|
9653
9777
|
throw new Error("Enter valid JSON.");
|
|
9654
9778
|
}
|
|
9655
9779
|
case "select": {
|
|
9656
|
-
const allowed = meta.values ?? [];
|
|
9780
|
+
const allowed = kind === "enum" ? meta.values ?? [] : [];
|
|
9657
9781
|
if (allowed.length > 0 && !allowed.includes(raw)) {
|
|
9658
9782
|
throw new Error(`Choose one of: ${allowed.join(", ")}.`);
|
|
9659
9783
|
}
|
|
@@ -9701,6 +9825,28 @@ function formatCellValue(value) {
|
|
|
9701
9825
|
if (typeof value === "object") return JSON.stringify(value);
|
|
9702
9826
|
return String(value);
|
|
9703
9827
|
}
|
|
9828
|
+
function foreignKeyFields(admin) {
|
|
9829
|
+
const columns = adminColumns(admin.model);
|
|
9830
|
+
const out = {};
|
|
9831
|
+
for (const name of admin.editableFieldNames()) {
|
|
9832
|
+
const column6 = columns[name];
|
|
9833
|
+
if (column6 === void 0) continue;
|
|
9834
|
+
const table = foreignKeyTable(column6);
|
|
9835
|
+
if (table !== null) out[name] = table;
|
|
9836
|
+
}
|
|
9837
|
+
return out;
|
|
9838
|
+
}
|
|
9839
|
+
function foreignKeyLabel(admin, row) {
|
|
9840
|
+
for (const field of admin.searchFields) {
|
|
9841
|
+
const value = row[field];
|
|
9842
|
+
if (typeof value === "string" && value !== "") return value;
|
|
9843
|
+
}
|
|
9844
|
+
for (const field of ["name", "title", "email", "label", "reference"]) {
|
|
9845
|
+
const value = row[field];
|
|
9846
|
+
if (typeof value === "string" && value !== "") return value;
|
|
9847
|
+
}
|
|
9848
|
+
return String(row[admin.identityField] ?? "");
|
|
9849
|
+
}
|
|
9704
9850
|
|
|
9705
9851
|
// src/admin/auth.ts
|
|
9706
9852
|
var UserModelAuthBackend = class {
|
|
@@ -9958,6 +10104,8 @@ var AdminSite = class {
|
|
|
9958
10104
|
siteUrl;
|
|
9959
10105
|
/** Typed appearance overrides. */
|
|
9960
10106
|
theme;
|
|
10107
|
+
/** Business-metric cards rendered at the top of the dashboard. */
|
|
10108
|
+
dashboardCards;
|
|
9961
10109
|
registry = /* @__PURE__ */ new Map();
|
|
9962
10110
|
/**
|
|
9963
10111
|
* Initialize the site.
|
|
@@ -9970,6 +10118,7 @@ var AdminSite = class {
|
|
|
9970
10118
|
this.indexSubtitle = options.indexSubtitle ?? "Site administration";
|
|
9971
10119
|
this.siteUrl = options.siteUrl ?? null;
|
|
9972
10120
|
this.theme = options.theme ?? {};
|
|
10121
|
+
this.dashboardCards = [...options.dashboardCards ?? []];
|
|
9973
10122
|
}
|
|
9974
10123
|
/**
|
|
9975
10124
|
* Return the centered header brand text.
|
|
@@ -11789,7 +11938,7 @@ function renderMfaPage(context, error) {
|
|
|
11789
11938
|
</section>`;
|
|
11790
11939
|
return renderLayout(context, `Two-factor \xB7 ${context.site.title}`, body);
|
|
11791
11940
|
}
|
|
11792
|
-
function renderDashboardPage(context, cards, metrics) {
|
|
11941
|
+
function renderDashboardPage(context, cards, metrics, businessCards = []) {
|
|
11793
11942
|
const metricsPanel = metrics === null ? "" : `<div class="tempest-admin-stats" aria-label="System metrics">
|
|
11794
11943
|
<div class="tempest-admin-stat">
|
|
11795
11944
|
<span class="tempest-admin-stat__label">CPU</span>
|
|
@@ -11801,6 +11950,7 @@ function renderDashboardPage(context, cards, metrics) {
|
|
|
11801
11950
|
<span class="tempest-admin-stat__sub">${escapeHtml(metrics.memoryUsedGb)} / ${escapeHtml(metrics.memoryTotalGb)} GB</span>
|
|
11802
11951
|
</div>
|
|
11803
11952
|
</div>`;
|
|
11953
|
+
const business = businessCards.length > 0 ? `<div class="tempest-admin-cards" aria-label="Business metrics">${businessCards.map(renderBusinessCard).join("")}</div>` : "";
|
|
11804
11954
|
const models = cards.length > 0 ? `<div class="tempest-admin-models">${cards.map(
|
|
11805
11955
|
(card) => `<article class="tempest-admin-model-card">
|
|
11806
11956
|
<header class="tempest-admin-model-card__head">
|
|
@@ -11817,10 +11967,53 @@ function renderDashboardPage(context, cards, metrics) {
|
|
|
11817
11967
|
<h1>${escapeHtml(context.site.title)}</h1>
|
|
11818
11968
|
<p>${escapeHtml(context.site.indexSubtitle)}</p>
|
|
11819
11969
|
${metricsPanel}
|
|
11970
|
+
${business}
|
|
11820
11971
|
${models}
|
|
11821
11972
|
</section>`;
|
|
11822
11973
|
return renderLayout(context, context.site.title, body);
|
|
11823
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
|
+
}
|
|
11824
12017
|
function renderFilter(filter) {
|
|
11825
12018
|
const name = `filter_${filter.field}`;
|
|
11826
12019
|
if (filter.kind === "select") {
|
|
@@ -11838,15 +12031,40 @@ function renderFilter(filter) {
|
|
|
11838
12031
|
return `<label><span>${escapeHtml(filter.label)}</span><input type="text" name="${escapeHtml(name)}" value="${escapeHtml(filter.value)}"></label>`;
|
|
11839
12032
|
}
|
|
11840
12033
|
function renderListPage(context, view) {
|
|
12034
|
+
const bulk = view.bulkActions.length > 0 && context.session !== null;
|
|
12035
|
+
const checkColumn = bulk ? 1 : 0;
|
|
11841
12036
|
const headers = view.columns.map((column6) => {
|
|
11842
12037
|
const state = view.sort[column6];
|
|
11843
12038
|
if (state === void 0) return `<th>${escapeHtml(column6)}</th>`;
|
|
11844
12039
|
const arrow = state.active ? state.ascending ? "\u25B2" : "\u25BC" : "\u2195";
|
|
11845
12040
|
return `<th><a class="tempest-sort${state.active ? " tempest-sort--active" : ""}" href="${escapeHtml(state.url)}"><span>${escapeHtml(column6)}</span><span class="tempest-sort__arrow" aria-hidden="true">${arrow}</span></a></th>`;
|
|
11846
12041
|
}).join("");
|
|
11847
|
-
const rows = view.rows.length > 0 ? view.rows.map(
|
|
11848
|
-
|
|
11849
|
-
|
|
12042
|
+
const rows = view.rows.length > 0 ? view.rows.map((row) => {
|
|
12043
|
+
const check = bulk ? `<td class="tempest-admin-list__check"><input type="checkbox" name="ids" value="${escapeHtml(row.identity)}" data-row-check aria-label="Select row"></td>` : "";
|
|
12044
|
+
const cells = row.cells.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("");
|
|
12045
|
+
return `<tr>${check}${cells}<td><a href="${escapeHtml(row.url)}">View</a></td></tr>`;
|
|
12046
|
+
}).join("") : `<tr><td colspan="${view.columns.length + 1 + checkColumn}">No records.</td></tr>`;
|
|
12047
|
+
const bulkBar = bulk ? `<form method="post" action="${escapeHtml(view.bulkUrl)}" class="tempest-admin-bulk" onsubmit="return confirm('Apply the selected action to the checked rows?');">
|
|
12048
|
+
<input type="hidden" name="csrf_token" value="${escapeHtml(context.session?.csrfToken)}">
|
|
12049
|
+
<div class="tempest-admin-bulk__bar">
|
|
12050
|
+
<select name="action" aria-label="Bulk action">
|
|
12051
|
+
${view.bulkActions.map(
|
|
12052
|
+
(action) => `<option value="${escapeHtml(action.value)}">${escapeHtml(action.label)}${action.dangerous ? " \u26A0" : ""}</option>`
|
|
12053
|
+
).join("")}
|
|
12054
|
+
</select>
|
|
12055
|
+
<button type="submit">Apply to selected</button>
|
|
12056
|
+
</div>` : "";
|
|
12057
|
+
const selectAllScript = bulk ? `<script>
|
|
12058
|
+
(function () {
|
|
12059
|
+
var master = document.querySelector('[data-select-all]');
|
|
12060
|
+
if (!master) return;
|
|
12061
|
+
master.addEventListener('change', function () {
|
|
12062
|
+
document.querySelectorAll('[data-row-check]').forEach(function (box) {
|
|
12063
|
+
box.checked = master.checked;
|
|
12064
|
+
});
|
|
12065
|
+
});
|
|
12066
|
+
})();
|
|
12067
|
+
</script>` : "";
|
|
11850
12068
|
const hasControls = view.searchable || view.filters.length > 0;
|
|
11851
12069
|
const body = `<section class="tempest-admin-list">
|
|
11852
12070
|
<header class="tempest-admin-list__header">
|
|
@@ -11861,14 +12079,22 @@ function renderListPage(context, view) {
|
|
|
11861
12079
|
</form>
|
|
11862
12080
|
<div class="tempest-admin-list__actions">
|
|
11863
12081
|
${view.newUrl !== null ? `<a class="tempest-admin-list__new" href="${escapeHtml(view.newUrl)}">+ New</a>` : ""}
|
|
12082
|
+
<a href="${escapeHtml(view.exportCsvUrl)}">Export CSV</a>
|
|
12083
|
+
<a href="${escapeHtml(view.exportJsonUrl)}">Export JSON</a>
|
|
11864
12084
|
</div>
|
|
11865
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>` : ""}
|
|
12089
|
+
${bulkBar}
|
|
11866
12090
|
<div class="tempest-admin-table-wrap">
|
|
11867
12091
|
<table class="tempest-admin-list__table">
|
|
11868
|
-
<thead><tr>${headers}<th>Actions</th></tr></thead>
|
|
12092
|
+
<thead><tr>${bulk ? '<th class="tempest-admin-list__check"><input type="checkbox" data-select-all aria-label="Select all"></th>' : ""}${headers}<th>Actions</th></tr></thead>
|
|
11869
12093
|
<tbody>${rows}</tbody>
|
|
11870
12094
|
</table>
|
|
11871
12095
|
</div>
|
|
12096
|
+
${bulk ? "</form>" : ""}
|
|
12097
|
+
${selectAllScript}
|
|
11872
12098
|
${view.pages > 1 ? `<nav class="tempest-admin-list__pagination" aria-label="Pagination">
|
|
11873
12099
|
${view.prevUrl !== null ? `<a href="${escapeHtml(view.prevUrl)}">\u2190 Prev</a>` : ""}
|
|
11874
12100
|
<span>Page ${escapeHtml(view.page)} of ${escapeHtml(view.pages)}</span>
|
|
@@ -11883,6 +12109,7 @@ function renderDetailPage(context, view) {
|
|
|
11883
12109
|
const fields = view.fields.map(
|
|
11884
12110
|
(field) => `<dt>${escapeHtml(field.label)}</dt><dd>${field.value === "" ? "<em>\u2014</em>" : escapeHtml(field.value)}</dd>`
|
|
11885
12111
|
).join("");
|
|
12112
|
+
const auditPanel = view.audit === null ? "" : renderAuditPanel(view.audit);
|
|
11886
12113
|
const body = `<section class="tempest-admin-detail">
|
|
11887
12114
|
<header class="tempest-admin-detail__header">
|
|
11888
12115
|
<h1>${escapeHtml(view.title)} \xB7 ${escapeHtml(view.identity)}</h1>
|
|
@@ -11896,9 +12123,37 @@ function renderDetailPage(context, view) {
|
|
|
11896
12123
|
</div>
|
|
11897
12124
|
</header>
|
|
11898
12125
|
<dl class="tempest-admin-detail__fields">${fields}</dl>
|
|
12126
|
+
${auditPanel}
|
|
11899
12127
|
</section>`;
|
|
11900
12128
|
return renderLayout(context, `${view.title} \xB7 ${view.identity}`, body);
|
|
11901
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
|
+
}
|
|
11902
12157
|
function renderFormField(field) {
|
|
11903
12158
|
const required = field.required ? " required" : "";
|
|
11904
12159
|
const name = escapeHtml(field.name);
|
|
@@ -11962,11 +12217,17 @@ function renderFormPage(context, view) {
|
|
|
11962
12217
|
return renderLayout(context, `${heading} \xB7 ${context.site.title}`, body);
|
|
11963
12218
|
}
|
|
11964
12219
|
var logger2 = new JSONLogger("tempest_express_sdk.admin.router");
|
|
12220
|
+
var AUDIT_HISTORY_LIMIT = 50;
|
|
12221
|
+
var FK_OPTION_CAP = 1e3;
|
|
12222
|
+
var FLASH_MAX_LENGTH = 300;
|
|
11965
12223
|
var FLASH_MESSAGES = {
|
|
11966
12224
|
created: { text: "Record created.", level: "success" },
|
|
11967
12225
|
updated: { text: "Record updated.", level: "success" },
|
|
11968
12226
|
deleted: { text: "Record deleted.", level: "success" }
|
|
11969
12227
|
};
|
|
12228
|
+
async function runCustomAction(action, context) {
|
|
12229
|
+
return await action.handler(context) ?? null;
|
|
12230
|
+
}
|
|
11970
12231
|
function queryString(value) {
|
|
11971
12232
|
if (typeof value === "string") return value.trim();
|
|
11972
12233
|
if (Array.isArray(value) && typeof value[0] === "string") return value[0].trim();
|
|
@@ -11984,6 +12245,7 @@ function makeAdminRouter(site, options) {
|
|
|
11984
12245
|
const prefix = (options.prefix ?? "/admin").replace(/\/$/, "");
|
|
11985
12246
|
const theme = resolveAdminTheme(site.theme);
|
|
11986
12247
|
const showMetrics = options.showMetrics ?? true;
|
|
12248
|
+
const exportMaxRows = options.exportMaxRows ?? 5e3;
|
|
11987
12249
|
const sessions = new AdminSessionStore({
|
|
11988
12250
|
secret: options.secretKey,
|
|
11989
12251
|
...options.cookieName === void 0 ? {} : { cookieName: options.cookieName },
|
|
@@ -11994,21 +12256,35 @@ function makeAdminRouter(site, options) {
|
|
|
11994
12256
|
const backend = options.authBackend;
|
|
11995
12257
|
const router = express3.Router();
|
|
11996
12258
|
router.use(prefix, express3.urlencoded({ extended: false }));
|
|
11997
|
-
const context = (req, session) => ({
|
|
12259
|
+
const context = (req, session, models = site.list()) => ({
|
|
11998
12260
|
site,
|
|
11999
12261
|
theme,
|
|
12000
12262
|
prefix,
|
|
12001
12263
|
session,
|
|
12002
12264
|
currentPath: req.originalUrl.split("?")[0] ?? req.path,
|
|
12003
|
-
navModels:
|
|
12265
|
+
navModels: models.map((admin) => ({
|
|
12004
12266
|
label: admin.verboseNamePlural(),
|
|
12005
12267
|
url: `${prefix}/m/${admin.slug()}`
|
|
12006
12268
|
})),
|
|
12007
12269
|
messages: flashFor(req)
|
|
12008
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
|
+
};
|
|
12009
12276
|
const flashFor = (req) => {
|
|
12010
|
-
const
|
|
12011
|
-
|
|
12277
|
+
const fixed = FLASH_MESSAGES[queryString(req.query.ok)];
|
|
12278
|
+
if (fixed !== void 0) return [fixed];
|
|
12279
|
+
const text = queryString(req.query.flash);
|
|
12280
|
+
if (text === "") return [];
|
|
12281
|
+
const level = queryString(req.query.level);
|
|
12282
|
+
return [
|
|
12283
|
+
{
|
|
12284
|
+
text: text.slice(0, FLASH_MAX_LENGTH),
|
|
12285
|
+
level: level === "error" || level === "warning" ? level : "success"
|
|
12286
|
+
}
|
|
12287
|
+
];
|
|
12012
12288
|
};
|
|
12013
12289
|
const html = (res, body, status = 200) => {
|
|
12014
12290
|
res.status(status).type("html").send(body);
|
|
@@ -12044,14 +12320,28 @@ function makeAdminRouter(site, options) {
|
|
|
12044
12320
|
res.redirect(`${prefix}/mfa`);
|
|
12045
12321
|
return null;
|
|
12046
12322
|
}
|
|
12047
|
-
|
|
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 };
|
|
12048
12328
|
};
|
|
12049
|
-
const resolveAdmin = (req, res, state) => {
|
|
12329
|
+
const resolveAdmin = async (req, res, state, action = AdminPermission.VIEW) => {
|
|
12050
12330
|
const admin = site.get(String(req.params.slug));
|
|
12051
|
-
if (admin === null) {
|
|
12052
|
-
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);
|
|
12053
12333
|
return null;
|
|
12054
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
|
+
}
|
|
12055
12345
|
return admin;
|
|
12056
12346
|
};
|
|
12057
12347
|
const renderNotFound = (ctx) => renderDashboardPage(
|
|
@@ -12062,7 +12352,7 @@ function makeAdminRouter(site, options) {
|
|
|
12062
12352
|
const checkCsrf = (req, res, state) => {
|
|
12063
12353
|
const body = req.body;
|
|
12064
12354
|
if (csrfTokenMatches(state.session, body?.csrf_token)) return true;
|
|
12065
|
-
html(res, renderNotFound(context(req, state.session)), 403);
|
|
12355
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12066
12356
|
return false;
|
|
12067
12357
|
};
|
|
12068
12358
|
router.get(`${prefix}/static/admin.css`, (_req, res) => {
|
|
@@ -12144,7 +12434,7 @@ function makeAdminRouter(site, options) {
|
|
|
12144
12434
|
const state = await authenticate(req, res);
|
|
12145
12435
|
if (state === null) return;
|
|
12146
12436
|
const cards = [];
|
|
12147
|
-
for (const admin of
|
|
12437
|
+
for (const admin of state.visible) {
|
|
12148
12438
|
let count = null;
|
|
12149
12439
|
try {
|
|
12150
12440
|
count = await admin.repository(state.dbSession).count();
|
|
@@ -12158,9 +12448,13 @@ function makeAdminRouter(site, options) {
|
|
|
12158
12448
|
label: admin.verboseNamePlural(),
|
|
12159
12449
|
count,
|
|
12160
12450
|
url: `${prefix}/m/${admin.slug()}`,
|
|
12161
|
-
newUrl: admin.
|
|
12451
|
+
newUrl: await allows(state.principal, admin, AdminPermission.CREATE) ? `${prefix}/m/${admin.slug()}/new` : null
|
|
12162
12452
|
});
|
|
12163
12453
|
}
|
|
12454
|
+
const businessCards = [];
|
|
12455
|
+
for (const card of site.dashboardCards) {
|
|
12456
|
+
businessCards.push(await computeBusinessCard(card, state.dbSession));
|
|
12457
|
+
}
|
|
12164
12458
|
let metrics = null;
|
|
12165
12459
|
if (showMetrics) {
|
|
12166
12460
|
const snapshot2 = MetricsUtils.system();
|
|
@@ -12171,7 +12465,15 @@ function makeAdminRouter(site, options) {
|
|
|
12171
12465
|
memoryTotalGb: (snapshot2.memory.total / 1024 ** 3).toFixed(1)
|
|
12172
12466
|
};
|
|
12173
12467
|
}
|
|
12174
|
-
html(
|
|
12468
|
+
html(
|
|
12469
|
+
res,
|
|
12470
|
+
renderDashboardPage(
|
|
12471
|
+
context(req, state.session, state.visible),
|
|
12472
|
+
cards,
|
|
12473
|
+
metrics,
|
|
12474
|
+
businessCards
|
|
12475
|
+
)
|
|
12476
|
+
);
|
|
12175
12477
|
})
|
|
12176
12478
|
);
|
|
12177
12479
|
router.get(
|
|
@@ -12179,7 +12481,7 @@ function makeAdminRouter(site, options) {
|
|
|
12179
12481
|
guarded(async (req, res) => {
|
|
12180
12482
|
const state = await authenticate(req, res);
|
|
12181
12483
|
if (state === null) return;
|
|
12182
|
-
const admin = resolveAdmin(req, res, state);
|
|
12484
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12183
12485
|
if (admin === null) return;
|
|
12184
12486
|
html(res, await renderList(req, admin, state));
|
|
12185
12487
|
})
|
|
@@ -12189,18 +12491,20 @@ function makeAdminRouter(site, options) {
|
|
|
12189
12491
|
guarded(async (req, res) => {
|
|
12190
12492
|
const state = await authenticate(req, res);
|
|
12191
12493
|
if (state === null) return;
|
|
12192
|
-
const admin = resolveAdmin(req, res, state);
|
|
12494
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.CREATE);
|
|
12193
12495
|
if (admin === null) return;
|
|
12194
12496
|
if (!admin.canCreate) {
|
|
12195
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12497
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12196
12498
|
return;
|
|
12197
12499
|
}
|
|
12198
12500
|
html(
|
|
12199
12501
|
res,
|
|
12200
|
-
renderFormPage(context(req, state.session), {
|
|
12502
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12201
12503
|
mode: "create",
|
|
12202
12504
|
title: admin.verboseName(),
|
|
12203
|
-
fields: buildFormFields(admin
|
|
12505
|
+
fields: buildFormFields(admin, {
|
|
12506
|
+
foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession)
|
|
12507
|
+
}),
|
|
12204
12508
|
actionUrl: `${prefix}/m/${admin.slug()}/new`,
|
|
12205
12509
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
12206
12510
|
error: null
|
|
@@ -12213,22 +12517,27 @@ function makeAdminRouter(site, options) {
|
|
|
12213
12517
|
guarded(async (req, res) => {
|
|
12214
12518
|
const state = await authenticate(req, res);
|
|
12215
12519
|
if (state === null) return;
|
|
12216
|
-
const admin = resolveAdmin(req, res, state);
|
|
12520
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.CREATE);
|
|
12217
12521
|
if (admin === null) return;
|
|
12218
12522
|
if (!admin.canCreate) {
|
|
12219
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12523
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12220
12524
|
return;
|
|
12221
12525
|
}
|
|
12222
12526
|
if (!checkCsrf(req, res, state)) return;
|
|
12223
12527
|
const body = req.body;
|
|
12224
12528
|
const parsed = parseFormBody(admin, body);
|
|
12529
|
+
const foreignKeyOptions = await foreignKeyOptionsFor(admin, state.dbSession);
|
|
12225
12530
|
const rerender = (error, status) => {
|
|
12226
12531
|
html(
|
|
12227
12532
|
res,
|
|
12228
|
-
renderFormPage(context(req, state.session), {
|
|
12533
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12229
12534
|
mode: "create",
|
|
12230
12535
|
title: admin.verboseName(),
|
|
12231
|
-
fields: buildFormFields(admin, {
|
|
12536
|
+
fields: buildFormFields(admin, {
|
|
12537
|
+
values: body,
|
|
12538
|
+
errors: parsed.errors,
|
|
12539
|
+
foreignKeyOptions
|
|
12540
|
+
}),
|
|
12232
12541
|
actionUrl: `${prefix}/m/${admin.slug()}/new`,
|
|
12233
12542
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
12234
12543
|
error
|
|
@@ -12240,6 +12549,7 @@ function makeAdminRouter(site, options) {
|
|
|
12240
12549
|
rerender("Please fix the highlighted fields.", 400);
|
|
12241
12550
|
return;
|
|
12242
12551
|
}
|
|
12552
|
+
stampActor(admin, parsed.data, backend.principalId(state.principal), true);
|
|
12243
12553
|
try {
|
|
12244
12554
|
await admin.repository(state.dbSession).create(parsed.data);
|
|
12245
12555
|
} catch (error) {
|
|
@@ -12249,28 +12559,134 @@ function makeAdminRouter(site, options) {
|
|
|
12249
12559
|
res.redirect(`${prefix}/m/${admin.slug()}?ok=created`);
|
|
12250
12560
|
})
|
|
12251
12561
|
);
|
|
12562
|
+
router.get(
|
|
12563
|
+
`${prefix}/m/:slug/export.:format`,
|
|
12564
|
+
guarded(async (req, res) => {
|
|
12565
|
+
const state = await authenticate(req, res);
|
|
12566
|
+
if (state === null) return;
|
|
12567
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12568
|
+
if (admin === null) return;
|
|
12569
|
+
const format = String(req.params.format);
|
|
12570
|
+
if (format !== "csv" && format !== "json") {
|
|
12571
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12572
|
+
return;
|
|
12573
|
+
}
|
|
12574
|
+
const query = await resolveListQuery(req, admin, state.dbSession);
|
|
12575
|
+
const result = await admin.repository(state.dbSession).paginate({
|
|
12576
|
+
page: 1,
|
|
12577
|
+
pageSize: exportMaxRows,
|
|
12578
|
+
...query.orderBy === void 0 ? {} : { orderBy: query.orderBy },
|
|
12579
|
+
ascending: query.ascending,
|
|
12580
|
+
...query.where === void 0 ? {} : { filters: query.where }
|
|
12581
|
+
});
|
|
12582
|
+
const columns = admin.listDisplayNames();
|
|
12583
|
+
const rows = result.items;
|
|
12584
|
+
const payload = format === "csv" ? toCsv(columns, rows) : toJson(columns, rows);
|
|
12585
|
+
res.status(200).type(format === "csv" ? "text/csv; charset=utf-8" : "application/json").set("content-disposition", `attachment; filename="${admin.slug()}.${format}"`).send(payload);
|
|
12586
|
+
})
|
|
12587
|
+
);
|
|
12588
|
+
router.post(
|
|
12589
|
+
`${prefix}/m/:slug/bulk`,
|
|
12590
|
+
guarded(async (req, res) => {
|
|
12591
|
+
const state = await authenticate(req, res);
|
|
12592
|
+
if (state === null) return;
|
|
12593
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12594
|
+
if (admin === null) return;
|
|
12595
|
+
if (!checkCsrf(req, res, state)) return;
|
|
12596
|
+
const body = req.body;
|
|
12597
|
+
const action = typeof body.action === "string" ? body.action : "";
|
|
12598
|
+
const raw = body.ids;
|
|
12599
|
+
const ids = (Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw]).filter((value) => typeof value === "string").filter((value) => value !== "");
|
|
12600
|
+
const listUrl = `${prefix}/m/${admin.slug()}`;
|
|
12601
|
+
const back = (message, level) => {
|
|
12602
|
+
res.redirect(`${listUrl}?${buildQuery({ flash: message, level })}`);
|
|
12603
|
+
};
|
|
12604
|
+
if (ids.length === 0) {
|
|
12605
|
+
back("No rows were selected.", "warning");
|
|
12606
|
+
return;
|
|
12607
|
+
}
|
|
12608
|
+
if (!bulkActionsFor(admin).some((option) => option.value === action)) {
|
|
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);
|
|
12615
|
+
return;
|
|
12616
|
+
}
|
|
12617
|
+
const repository = admin.repository(state.dbSession);
|
|
12618
|
+
const scope = { [admin.identityField]: { in: ids } };
|
|
12619
|
+
if (action.startsWith("custom:")) {
|
|
12620
|
+
const custom = admin.getAction(action.slice("custom:".length));
|
|
12621
|
+
if (custom === null) {
|
|
12622
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 400);
|
|
12623
|
+
return;
|
|
12624
|
+
}
|
|
12625
|
+
let result;
|
|
12626
|
+
try {
|
|
12627
|
+
result = await runCustomAction(custom, {
|
|
12628
|
+
ids,
|
|
12629
|
+
repository,
|
|
12630
|
+
dbSession: state.dbSession,
|
|
12631
|
+
request: req,
|
|
12632
|
+
session: state.session,
|
|
12633
|
+
principal: state.principal
|
|
12634
|
+
});
|
|
12635
|
+
} catch (error) {
|
|
12636
|
+
logger2.error("Admin action failed", {
|
|
12637
|
+
slug: admin.slug(),
|
|
12638
|
+
action: custom.name,
|
|
12639
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12640
|
+
});
|
|
12641
|
+
back(
|
|
12642
|
+
`${custom.label} failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
12643
|
+
"error"
|
|
12644
|
+
);
|
|
12645
|
+
return;
|
|
12646
|
+
}
|
|
12647
|
+
if (result === null) {
|
|
12648
|
+
res.redirect(listUrl);
|
|
12649
|
+
return;
|
|
12650
|
+
}
|
|
12651
|
+
back(result.message, result.category ?? "success");
|
|
12652
|
+
return;
|
|
12653
|
+
}
|
|
12654
|
+
if (action === "delete") {
|
|
12655
|
+
const removed = await repository.delete(scope);
|
|
12656
|
+
back(`Deleted ${removed} record${removed === 1 ? "" : "s"}.`, "success");
|
|
12657
|
+
return;
|
|
12658
|
+
}
|
|
12659
|
+
const active = action === "activate";
|
|
12660
|
+
const changed = await repository.update(scope, { isActive: active });
|
|
12661
|
+
back(
|
|
12662
|
+
`${active ? "Activated" : "Deactivated"} ${changed} record${changed === 1 ? "" : "s"}.`,
|
|
12663
|
+
"success"
|
|
12664
|
+
);
|
|
12665
|
+
})
|
|
12666
|
+
);
|
|
12252
12667
|
router.get(
|
|
12253
12668
|
`${prefix}/m/:slug/:identity`,
|
|
12254
12669
|
guarded(async (req, res) => {
|
|
12255
12670
|
const state = await authenticate(req, res);
|
|
12256
12671
|
if (state === null) return;
|
|
12257
|
-
const admin = resolveAdmin(req, res, state);
|
|
12672
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12258
12673
|
if (admin === null) return;
|
|
12259
12674
|
const row = await findRow(admin, state.dbSession, String(req.params.identity));
|
|
12260
12675
|
if (row === null) {
|
|
12261
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12676
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12262
12677
|
return;
|
|
12263
12678
|
}
|
|
12264
12679
|
const identity = String(row[admin.identityField]);
|
|
12265
12680
|
html(
|
|
12266
12681
|
res,
|
|
12267
|
-
renderDetailPage(context(req, state.session), {
|
|
12682
|
+
renderDetailPage(context(req, state.session, state.visible), {
|
|
12268
12683
|
title: admin.verboseName(),
|
|
12269
12684
|
identity,
|
|
12685
|
+
audit: await buildAuditView(admin, row, state.dbSession),
|
|
12270
12686
|
fields: admin.detailFieldNames().map((name) => ({ label: name, value: formatCellValue(row[name]) })),
|
|
12271
12687
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
12272
|
-
editUrl: admin.
|
|
12273
|
-
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
|
|
12274
12690
|
})
|
|
12275
12691
|
);
|
|
12276
12692
|
})
|
|
@@ -12280,20 +12696,23 @@ function makeAdminRouter(site, options) {
|
|
|
12280
12696
|
guarded(async (req, res) => {
|
|
12281
12697
|
const state = await authenticate(req, res);
|
|
12282
12698
|
if (state === null) return;
|
|
12283
|
-
const admin = resolveAdmin(req, res, state);
|
|
12699
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.EDIT);
|
|
12284
12700
|
if (admin === null) return;
|
|
12285
12701
|
const identity = String(req.params.identity);
|
|
12286
12702
|
const row = admin.canEdit ? await findRow(admin, state.dbSession, identity) : null;
|
|
12287
12703
|
if (row === null) {
|
|
12288
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12704
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12289
12705
|
return;
|
|
12290
12706
|
}
|
|
12291
12707
|
html(
|
|
12292
12708
|
res,
|
|
12293
|
-
renderFormPage(context(req, state.session), {
|
|
12709
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12294
12710
|
mode: "edit",
|
|
12295
12711
|
title: admin.verboseName(),
|
|
12296
|
-
fields: buildFormFields(admin, {
|
|
12712
|
+
fields: buildFormFields(admin, {
|
|
12713
|
+
values: row,
|
|
12714
|
+
foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession)
|
|
12715
|
+
}),
|
|
12297
12716
|
actionUrl: `${prefix}/m/${admin.slug()}/${identity}/edit`,
|
|
12298
12717
|
backUrl: `${prefix}/m/${admin.slug()}/${identity}`,
|
|
12299
12718
|
error: null
|
|
@@ -12306,23 +12725,28 @@ function makeAdminRouter(site, options) {
|
|
|
12306
12725
|
guarded(async (req, res) => {
|
|
12307
12726
|
const state = await authenticate(req, res);
|
|
12308
12727
|
if (state === null) return;
|
|
12309
|
-
const admin = resolveAdmin(req, res, state);
|
|
12728
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.EDIT);
|
|
12310
12729
|
if (admin === null) return;
|
|
12311
12730
|
const identity = String(req.params.identity);
|
|
12312
12731
|
if (!admin.canEdit) {
|
|
12313
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12732
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12314
12733
|
return;
|
|
12315
12734
|
}
|
|
12316
12735
|
if (!checkCsrf(req, res, state)) return;
|
|
12317
12736
|
const body = req.body;
|
|
12318
12737
|
const parsed = parseFormBody(admin, body);
|
|
12738
|
+
const foreignKeyOptions = await foreignKeyOptionsFor(admin, state.dbSession);
|
|
12319
12739
|
const rerender = (error, status) => {
|
|
12320
12740
|
html(
|
|
12321
12741
|
res,
|
|
12322
|
-
renderFormPage(context(req, state.session), {
|
|
12742
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12323
12743
|
mode: "edit",
|
|
12324
12744
|
title: admin.verboseName(),
|
|
12325
|
-
fields: buildFormFields(admin, {
|
|
12745
|
+
fields: buildFormFields(admin, {
|
|
12746
|
+
values: body,
|
|
12747
|
+
errors: parsed.errors,
|
|
12748
|
+
foreignKeyOptions
|
|
12749
|
+
}),
|
|
12326
12750
|
actionUrl: `${prefix}/m/${admin.slug()}/${identity}/edit`,
|
|
12327
12751
|
backUrl: `${prefix}/m/${admin.slug()}/${identity}`,
|
|
12328
12752
|
error
|
|
@@ -12334,10 +12758,16 @@ function makeAdminRouter(site, options) {
|
|
|
12334
12758
|
rerender("Please fix the highlighted fields.", 400);
|
|
12335
12759
|
return;
|
|
12336
12760
|
}
|
|
12761
|
+
stampActor(
|
|
12762
|
+
admin,
|
|
12763
|
+
parsed.data,
|
|
12764
|
+
backend.principalId(state.principal),
|
|
12765
|
+
false
|
|
12766
|
+
);
|
|
12337
12767
|
try {
|
|
12338
12768
|
const changed = await admin.repository(state.dbSession).update({ [admin.identityField]: identity }, parsed.data);
|
|
12339
12769
|
if (changed === 0) {
|
|
12340
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12770
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12341
12771
|
return;
|
|
12342
12772
|
}
|
|
12343
12773
|
} catch (error) {
|
|
@@ -12352,10 +12782,10 @@ function makeAdminRouter(site, options) {
|
|
|
12352
12782
|
guarded(async (req, res) => {
|
|
12353
12783
|
const state = await authenticate(req, res);
|
|
12354
12784
|
if (state === null) return;
|
|
12355
|
-
const admin = resolveAdmin(req, res, state);
|
|
12785
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.DELETE);
|
|
12356
12786
|
if (admin === null) return;
|
|
12357
12787
|
if (!admin.canDelete) {
|
|
12358
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12788
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12359
12789
|
return;
|
|
12360
12790
|
}
|
|
12361
12791
|
if (!checkCsrf(req, res, state)) return;
|
|
@@ -12363,18 +12793,142 @@ function makeAdminRouter(site, options) {
|
|
|
12363
12793
|
res.redirect(`${prefix}/m/${admin.slug()}?ok=deleted`);
|
|
12364
12794
|
})
|
|
12365
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
|
+
}
|
|
12366
12836
|
async function findRow(admin, dbSession, identity) {
|
|
12367
12837
|
const row = await admin.repository(dbSession).first({ [admin.identityField]: identity });
|
|
12368
12838
|
return row ?? null;
|
|
12369
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
|
+
}
|
|
12370
12848
|
async function renderList(req, admin, state) {
|
|
12371
12849
|
const columns = adminColumns(admin.model);
|
|
12372
|
-
const
|
|
12850
|
+
const query = await resolveListQuery(req, admin, state.dbSession);
|
|
12373
12851
|
const page2 = Math.max(1, Number.parseInt(queryString(req.query.page), 10) || 1);
|
|
12852
|
+
const result = await admin.repository(state.dbSession).paginate({
|
|
12853
|
+
page: page2,
|
|
12854
|
+
pageSize: admin.pageSize,
|
|
12855
|
+
...query.orderBy === void 0 ? {} : { orderBy: query.orderBy },
|
|
12856
|
+
ascending: query.ascending,
|
|
12857
|
+
...query.where === void 0 ? {} : { filters: query.where }
|
|
12858
|
+
});
|
|
12859
|
+
const displayed = admin.listDisplayNames();
|
|
12860
|
+
const sort = {};
|
|
12861
|
+
for (const column6 of displayed) {
|
|
12862
|
+
if (!(column6 in columns)) continue;
|
|
12863
|
+
const active = (query.sortColumn ?? admin.orderKey) === column6;
|
|
12864
|
+
const nextAscending = active ? !query.ascending : true;
|
|
12865
|
+
sort[column6] = {
|
|
12866
|
+
url: `?${buildQuery({
|
|
12867
|
+
...query.baseQuery,
|
|
12868
|
+
sort: column6,
|
|
12869
|
+
dir: nextAscending ? "asc" : "desc"
|
|
12870
|
+
})}`,
|
|
12871
|
+
active,
|
|
12872
|
+
ascending: query.ascending
|
|
12873
|
+
};
|
|
12874
|
+
}
|
|
12875
|
+
const sortParams = {
|
|
12876
|
+
sort: query.sortColumn ?? void 0,
|
|
12877
|
+
dir: query.sortColumn === null ? void 0 : query.ascending ? "asc" : "desc"
|
|
12878
|
+
};
|
|
12879
|
+
const pageUrl = (target) => `?${buildQuery({ ...query.baseQuery, ...sortParams, page: target })}`;
|
|
12880
|
+
const exportQuery = buildQuery({ ...query.baseQuery, ...sortParams });
|
|
12881
|
+
const exportUrl = (format) => `${prefix}/m/${admin.slug()}/export.${format}${exportQuery === "" ? "" : `?${exportQuery}`}`;
|
|
12882
|
+
const view = {
|
|
12883
|
+
title: admin.verboseNamePlural(),
|
|
12884
|
+
columns: displayed,
|
|
12885
|
+
rows: result.items.map((row) => {
|
|
12886
|
+
const identity = String(row[admin.identityField]);
|
|
12887
|
+
return {
|
|
12888
|
+
identity,
|
|
12889
|
+
cells: displayed.map((column6) => formatCellValue(row[column6])),
|
|
12890
|
+
url: `${prefix}/m/${admin.slug()}/${identity}`
|
|
12891
|
+
};
|
|
12892
|
+
}),
|
|
12893
|
+
total: result.total,
|
|
12894
|
+
page: result.page,
|
|
12895
|
+
pages: result.pages,
|
|
12896
|
+
prevUrl: result.page > 1 ? pageUrl(result.page - 1) : null,
|
|
12897
|
+
nextUrl: result.page < result.pages ? pageUrl(result.page + 1) : null,
|
|
12898
|
+
searchable: query.searchable.length > 0,
|
|
12899
|
+
searchValue: query.search,
|
|
12900
|
+
filters: query.filterViews,
|
|
12901
|
+
sort,
|
|
12902
|
+
newUrl: await allows(state.principal, admin, AdminPermission.CREATE) ? `${prefix}/m/${admin.slug()}/new` : null,
|
|
12903
|
+
bulkActions: await permittedBulkActions(admin, state.principal),
|
|
12904
|
+
bulkUrl: `${prefix}/m/${admin.slug()}/bulk`,
|
|
12905
|
+
exportCsvUrl: exportUrl("csv"),
|
|
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
|
+
]
|
|
12919
|
+
};
|
|
12920
|
+
return renderListPage(context(req, state.session, state.visible), view);
|
|
12921
|
+
}
|
|
12922
|
+
async function resolveListQuery(req, admin, dbSession) {
|
|
12923
|
+
const columns = adminColumns(admin.model);
|
|
12924
|
+
const search = queryString(req.query.q);
|
|
12374
12925
|
const sortField = queryString(req.query.sort);
|
|
12375
12926
|
const sortColumn = sortField in columns ? sortField : null;
|
|
12376
|
-
const
|
|
12927
|
+
const lens = admin.getLens(queryString(req.query.lens));
|
|
12377
12928
|
const conditions = [];
|
|
12929
|
+
if (lens !== null && Object.keys(lens.filters).length > 0) {
|
|
12930
|
+
conditions.push(lens.filters);
|
|
12931
|
+
}
|
|
12378
12932
|
const filterViews = [];
|
|
12379
12933
|
for (const field of admin.listFilter) {
|
|
12380
12934
|
const column6 = columns[field];
|
|
@@ -12398,6 +12952,8 @@ function makeAdminRouter(site, options) {
|
|
|
12398
12952
|
});
|
|
12399
12953
|
continue;
|
|
12400
12954
|
}
|
|
12955
|
+
const related = await relatedOptions(column6, dbSession);
|
|
12956
|
+
const options2 = related ?? spec.options;
|
|
12401
12957
|
const value = queryString(req.query[`filter_${field}`]);
|
|
12402
12958
|
if (value !== "") {
|
|
12403
12959
|
conditions.push({
|
|
@@ -12407,11 +12963,11 @@ function makeAdminRouter(site, options) {
|
|
|
12407
12963
|
filterViews.push({
|
|
12408
12964
|
field,
|
|
12409
12965
|
label: humanizeField(field),
|
|
12410
|
-
kind: spec.kind,
|
|
12966
|
+
kind: related === null ? spec.kind : "select",
|
|
12411
12967
|
value,
|
|
12412
12968
|
valueFrom: "",
|
|
12413
12969
|
valueTo: "",
|
|
12414
|
-
options:
|
|
12970
|
+
options: options2.map((option) => ({
|
|
12415
12971
|
value: option.value,
|
|
12416
12972
|
label: option.label,
|
|
12417
12973
|
selected: option.value === value
|
|
@@ -12427,72 +12983,174 @@ function makeAdminRouter(site, options) {
|
|
|
12427
12983
|
or(...searchable.map((field) => ({ [field]: { ilike: `%${search}%` } })))
|
|
12428
12984
|
);
|
|
12429
12985
|
}
|
|
12430
|
-
const where = conditions.length === 0 ? void 0 : and(...conditions);
|
|
12431
|
-
const orderBy = sortColumn ?? admin.orderKey ?? void 0;
|
|
12432
|
-
const result = await admin.repository(state.dbSession).paginate({
|
|
12433
|
-
page: page2,
|
|
12434
|
-
pageSize: admin.pageSize,
|
|
12435
|
-
...orderBy === void 0 ? {} : { orderBy },
|
|
12436
|
-
ascending,
|
|
12437
|
-
...where === void 0 ? {} : { filters: where }
|
|
12438
|
-
});
|
|
12439
|
-
const displayed = admin.listDisplayNames();
|
|
12440
12986
|
const baseQuery = { q: search };
|
|
12441
|
-
for (const
|
|
12442
|
-
if (
|
|
12443
|
-
baseQuery[`filter_${
|
|
12444
|
-
baseQuery[`filter_${
|
|
12987
|
+
for (const view of filterViews) {
|
|
12988
|
+
if (view.kind === "daterange") {
|
|
12989
|
+
baseQuery[`filter_${view.field}_from`] = view.valueFrom;
|
|
12990
|
+
baseQuery[`filter_${view.field}_to`] = view.valueTo;
|
|
12445
12991
|
} else {
|
|
12446
|
-
baseQuery[`filter_${
|
|
12992
|
+
baseQuery[`filter_${view.field}`] = view.value;
|
|
12447
12993
|
}
|
|
12448
12994
|
}
|
|
12449
|
-
const
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
|
|
12453
|
-
|
|
12454
|
-
|
|
12455
|
-
|
|
12456
|
-
|
|
12457
|
-
|
|
12458
|
-
|
|
12459
|
-
|
|
12460
|
-
|
|
12461
|
-
|
|
12462
|
-
|
|
12463
|
-
|
|
12464
|
-
const pageUrl = (target) => `?${buildQuery({
|
|
12465
|
-
...baseQuery,
|
|
12466
|
-
sort: sortColumn ?? void 0,
|
|
12467
|
-
dir: sortColumn === null ? void 0 : ascending ? "asc" : "desc",
|
|
12468
|
-
page: target
|
|
12469
|
-
})}`;
|
|
12470
|
-
const view = {
|
|
12471
|
-
title: admin.verboseNamePlural(),
|
|
12472
|
-
columns: displayed,
|
|
12473
|
-
rows: result.items.map((row) => {
|
|
12474
|
-
const identity = String(row[admin.identityField]);
|
|
12475
|
-
return {
|
|
12476
|
-
identity,
|
|
12477
|
-
cells: displayed.map((column6) => formatCellValue(row[column6])),
|
|
12478
|
-
url: `${prefix}/m/${admin.slug()}/${identity}`
|
|
12479
|
-
};
|
|
12480
|
-
}),
|
|
12481
|
-
total: result.total,
|
|
12482
|
-
page: result.page,
|
|
12483
|
-
pages: result.pages,
|
|
12484
|
-
prevUrl: result.page > 1 ? pageUrl(result.page - 1) : null,
|
|
12485
|
-
nextUrl: result.page < result.pages ? pageUrl(result.page + 1) : null,
|
|
12486
|
-
searchable: searchable.length > 0,
|
|
12487
|
-
searchValue: search,
|
|
12488
|
-
filters: filterViews,
|
|
12489
|
-
sort,
|
|
12490
|
-
newUrl: admin.canCreate ? `${prefix}/m/${admin.slug()}/new` : null
|
|
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;
|
|
13000
|
+
return {
|
|
13001
|
+
search,
|
|
13002
|
+
searchable,
|
|
13003
|
+
where: conditions.length === 0 ? void 0 : and(...conditions),
|
|
13004
|
+
orderBy: sortColumn ?? lensColumn ?? admin.orderKey ?? void 0,
|
|
13005
|
+
ascending,
|
|
13006
|
+
sortColumn,
|
|
13007
|
+
filterViews,
|
|
13008
|
+
baseQuery,
|
|
13009
|
+
lens: lens?.slug ?? ""
|
|
12491
13010
|
};
|
|
12492
|
-
|
|
13011
|
+
}
|
|
13012
|
+
async function relatedOptions(column6, dbSession) {
|
|
13013
|
+
const table = foreignKeyTable(column6);
|
|
13014
|
+
if (table === null) return null;
|
|
13015
|
+
const referenced = site.get(table);
|
|
13016
|
+
if (referenced === null) return null;
|
|
13017
|
+
const rows = await referenced.repository(dbSession).list();
|
|
13018
|
+
return rows.slice(0, FK_OPTION_CAP).map((row) => ({
|
|
13019
|
+
value: String(row[referenced.identityField]),
|
|
13020
|
+
label: foreignKeyLabel(referenced, row)
|
|
13021
|
+
}));
|
|
13022
|
+
}
|
|
13023
|
+
async function foreignKeyOptionsFor(admin, dbSession) {
|
|
13024
|
+
const columns = adminColumns(admin.model);
|
|
13025
|
+
const options2 = {};
|
|
13026
|
+
for (const field of Object.keys(foreignKeyFields(admin))) {
|
|
13027
|
+
const column6 = columns[field];
|
|
13028
|
+
if (column6 === void 0) continue;
|
|
13029
|
+
const related = await relatedOptions(column6, dbSession);
|
|
13030
|
+
if (related !== null) options2[field] = related;
|
|
13031
|
+
}
|
|
13032
|
+
return options2;
|
|
12493
13033
|
}
|
|
12494
13034
|
return router;
|
|
12495
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
|
+
}
|
|
13106
|
+
function bulkActionsFor(admin) {
|
|
13107
|
+
const actions = [];
|
|
13108
|
+
const hasActiveFlag = "isActive" in adminColumns(admin.model);
|
|
13109
|
+
if (admin.canEdit && hasActiveFlag) {
|
|
13110
|
+
actions.push({ value: "activate", label: "Activate", dangerous: false });
|
|
13111
|
+
actions.push({ value: "deactivate", label: "Deactivate", dangerous: false });
|
|
13112
|
+
}
|
|
13113
|
+
if (admin.canDelete) {
|
|
13114
|
+
actions.push({ value: "delete", label: "Delete", dangerous: true });
|
|
13115
|
+
}
|
|
13116
|
+
for (const action of admin.customActions()) {
|
|
13117
|
+
actions.push({
|
|
13118
|
+
value: `custom:${action.name}`,
|
|
13119
|
+
label: action.label,
|
|
13120
|
+
dangerous: action.dangerous
|
|
13121
|
+
});
|
|
13122
|
+
}
|
|
13123
|
+
return actions;
|
|
13124
|
+
}
|
|
13125
|
+
function exportValue(value) {
|
|
13126
|
+
if (value instanceof Date) return value.toISOString();
|
|
13127
|
+
if (typeof value === "bigint") return value.toString();
|
|
13128
|
+
if (value instanceof Uint8Array) return Buffer.from(value).toString("base64");
|
|
13129
|
+
return value;
|
|
13130
|
+
}
|
|
13131
|
+
function csvField(value) {
|
|
13132
|
+
if (value === null || value === void 0) return "";
|
|
13133
|
+
const text = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
13134
|
+
if (/[",\r\n]/.test(text)) return `"${text.replace(/"/g, '""')}"`;
|
|
13135
|
+
return text;
|
|
13136
|
+
}
|
|
13137
|
+
function toCsv(columns, rows) {
|
|
13138
|
+
const lines = [columns.map(csvField).join(",")];
|
|
13139
|
+
for (const row of rows) {
|
|
13140
|
+
lines.push(columns.map((column6) => csvField(exportValue(row[column6]))).join(","));
|
|
13141
|
+
}
|
|
13142
|
+
return `${lines.join("\r\n")}\r
|
|
13143
|
+
`;
|
|
13144
|
+
}
|
|
13145
|
+
function toJson(columns, rows) {
|
|
13146
|
+
return JSON.stringify(
|
|
13147
|
+
rows.map(
|
|
13148
|
+
(row) => Object.fromEntries(columns.map((column6) => [column6, exportValue(row[column6])]))
|
|
13149
|
+
),
|
|
13150
|
+
null,
|
|
13151
|
+
2
|
|
13152
|
+
);
|
|
13153
|
+
}
|
|
12496
13154
|
function describeWriteFailure(admin, error) {
|
|
12497
13155
|
const detail = error instanceof Error ? error.message : String(error);
|
|
12498
13156
|
return `The database refused this ${admin.verboseName().toLowerCase()}: ${detail}`;
|
|
@@ -14496,6 +15154,6 @@ async function withTestDatabase(models, fn) {
|
|
|
14496
15154
|
}
|
|
14497
15155
|
}
|
|
14498
15156
|
|
|
14499
|
-
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, 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, 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 };
|
|
14500
15158
|
//# sourceMappingURL=index.js.map
|
|
14501
15159
|
//# sourceMappingURL=index.js.map
|