tempest-express-sdk 0.25.0 → 0.27.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-ADF7OHRH.js} +3 -3
- package/dist/{chunk-TIW2KPT2.js.map → chunk-ADF7OHRH.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 +907 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +554 -96
- package/dist/index.d.ts +554 -96
- package/dist/index.js +898 -64
- package/dist/index.js.map +1 -1
- package/package.json +7 -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-ADF7OHRH.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-ADF7OHRH.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,
|
|
@@ -9332,6 +9371,85 @@ function adminAction(options, handler) {
|
|
|
9332
9371
|
};
|
|
9333
9372
|
}
|
|
9334
9373
|
|
|
9374
|
+
// src/admin/multipart.ts
|
|
9375
|
+
var BUSBOY_HINT = "The admin's upload and CSV-import screens need the optional peer `busboy`. Install it with: npm install busboy";
|
|
9376
|
+
async function loadBusboy() {
|
|
9377
|
+
try {
|
|
9378
|
+
const module = await import('busboy');
|
|
9379
|
+
return module.default ?? module;
|
|
9380
|
+
} catch {
|
|
9381
|
+
throw new Error(BUSBOY_HINT);
|
|
9382
|
+
}
|
|
9383
|
+
}
|
|
9384
|
+
var MultipartLimitError = class extends Error {
|
|
9385
|
+
/**
|
|
9386
|
+
* @param message - The operator-facing explanation.
|
|
9387
|
+
*/
|
|
9388
|
+
constructor(message) {
|
|
9389
|
+
super(message);
|
|
9390
|
+
this.name = "MultipartLimitError";
|
|
9391
|
+
}
|
|
9392
|
+
};
|
|
9393
|
+
async function parseMultipart(req, options = {}) {
|
|
9394
|
+
const maxFileBytes = options.maxFileBytes ?? 10 * 1024 * 1024;
|
|
9395
|
+
const maxFiles = options.maxFiles ?? 10;
|
|
9396
|
+
const busboy = await loadBusboy();
|
|
9397
|
+
return await new Promise((resolve, reject) => {
|
|
9398
|
+
const fields = {};
|
|
9399
|
+
const files = [];
|
|
9400
|
+
let settled = false;
|
|
9401
|
+
const fail = (error) => {
|
|
9402
|
+
if (settled) return;
|
|
9403
|
+
settled = true;
|
|
9404
|
+
reject(error);
|
|
9405
|
+
};
|
|
9406
|
+
const parser = busboy({
|
|
9407
|
+
headers: req.headers,
|
|
9408
|
+
limits: { fileSize: maxFileBytes, files: maxFiles }
|
|
9409
|
+
});
|
|
9410
|
+
parser.on("field", ((name, value) => {
|
|
9411
|
+
fields[name] = value;
|
|
9412
|
+
}));
|
|
9413
|
+
parser.on("file", ((name, stream, info) => {
|
|
9414
|
+
const chunks = [];
|
|
9415
|
+
stream.on("data", ((chunk) => chunks.push(chunk)));
|
|
9416
|
+
stream.on("limit", () => {
|
|
9417
|
+
fail(
|
|
9418
|
+
new MultipartLimitError(
|
|
9419
|
+
`The file is larger than the ${Math.floor(maxFileBytes / 1024 / 1024)} MB limit.`
|
|
9420
|
+
)
|
|
9421
|
+
);
|
|
9422
|
+
});
|
|
9423
|
+
stream.on("end", () => {
|
|
9424
|
+
const data = Buffer.concat(chunks);
|
|
9425
|
+
const filename = (info.filename ?? "").split(/[/\\]/).pop() ?? "";
|
|
9426
|
+
if (filename === "" || data.length === 0) return;
|
|
9427
|
+
files.push({
|
|
9428
|
+
field: name,
|
|
9429
|
+
filename,
|
|
9430
|
+
contentType: info.mimeType ?? "application/octet-stream",
|
|
9431
|
+
data
|
|
9432
|
+
});
|
|
9433
|
+
});
|
|
9434
|
+
}));
|
|
9435
|
+
parser.on("filesLimit", () => {
|
|
9436
|
+
fail(new MultipartLimitError(`At most ${maxFiles} files can be uploaded at once.`));
|
|
9437
|
+
});
|
|
9438
|
+
parser.on("error", ((error) => {
|
|
9439
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
9440
|
+
}));
|
|
9441
|
+
parser.on("close", () => {
|
|
9442
|
+
if (settled) return;
|
|
9443
|
+
settled = true;
|
|
9444
|
+
resolve({ fields, files });
|
|
9445
|
+
});
|
|
9446
|
+
req.pipe(parser);
|
|
9447
|
+
});
|
|
9448
|
+
}
|
|
9449
|
+
function isMultipart(req) {
|
|
9450
|
+
return (req.header("content-type") ?? "").toLowerCase().startsWith("multipart/form-data");
|
|
9451
|
+
}
|
|
9452
|
+
|
|
9335
9453
|
// src/admin/columns.ts
|
|
9336
9454
|
function humanizeField(name) {
|
|
9337
9455
|
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(" ");
|
|
@@ -9425,9 +9543,17 @@ var NEVER_EDITABLE = [
|
|
|
9425
9543
|
"id",
|
|
9426
9544
|
"createdAt",
|
|
9427
9545
|
"updatedAt",
|
|
9546
|
+
"createdBy",
|
|
9547
|
+
"updatedBy",
|
|
9428
9548
|
"hashedPassword"
|
|
9429
9549
|
];
|
|
9430
9550
|
var NEVER_LISTED = ["hashedPassword"];
|
|
9551
|
+
var AUDIT_FIELDS = [
|
|
9552
|
+
"createdAt",
|
|
9553
|
+
"updatedAt",
|
|
9554
|
+
"createdBy",
|
|
9555
|
+
"updatedBy"
|
|
9556
|
+
];
|
|
9431
9557
|
var AdminModel = class {
|
|
9432
9558
|
/** The managed model class. */
|
|
9433
9559
|
model;
|
|
@@ -9451,6 +9577,18 @@ var AdminModel = class {
|
|
|
9451
9577
|
canEdit;
|
|
9452
9578
|
/** Whether the delete action is exposed. */
|
|
9453
9579
|
canDelete;
|
|
9580
|
+
/** Audit-log model backing the detail timeline, or `null`. */
|
|
9581
|
+
auditModel;
|
|
9582
|
+
/** Saved list-view presets, in declaration order. */
|
|
9583
|
+
lenses;
|
|
9584
|
+
/** Columns rendered as file inputs. */
|
|
9585
|
+
uploadFields;
|
|
9586
|
+
/** Backend persisting uploaded files, or `null`. */
|
|
9587
|
+
uploadStorage;
|
|
9588
|
+
/** Whether the CSV import page is exposed. */
|
|
9589
|
+
canImport;
|
|
9590
|
+
/** Foreign-key columns rendered as a typed search box. */
|
|
9591
|
+
autocompleteFields;
|
|
9454
9592
|
actions = /* @__PURE__ */ new Map();
|
|
9455
9593
|
slugOverride;
|
|
9456
9594
|
listDisplayOverride;
|
|
@@ -9476,6 +9614,17 @@ var AdminModel = class {
|
|
|
9476
9614
|
this.canCreate = options.canCreate ?? true;
|
|
9477
9615
|
this.canEdit = options.canEdit ?? true;
|
|
9478
9616
|
this.canDelete = options.canDelete ?? true;
|
|
9617
|
+
this.auditModel = options.auditModel ?? null;
|
|
9618
|
+
this.lenses = [...options.lenses ?? []];
|
|
9619
|
+
this.uploadFields = [...options.uploadFields ?? []];
|
|
9620
|
+
this.uploadStorage = options.uploadStorage ?? null;
|
|
9621
|
+
this.canImport = options.canImport ?? false;
|
|
9622
|
+
this.autocompleteFields = [...options.autocompleteFields ?? []];
|
|
9623
|
+
if (this.uploadFields.length > 0 && this.uploadStorage === null) {
|
|
9624
|
+
throw new Error(
|
|
9625
|
+
`AdminModel(${this.model.tablename}).uploadFields requires an uploadStorage (e.g. LocalUploadStorage / S3UploadStorage) \u2014 without one there is nowhere to write the file.`
|
|
9626
|
+
);
|
|
9627
|
+
}
|
|
9479
9628
|
for (const action of options.actions ?? []) {
|
|
9480
9629
|
if (this.actions.has(action.name)) {
|
|
9481
9630
|
throw new Error(
|
|
@@ -9490,7 +9639,9 @@ var AdminModel = class {
|
|
|
9490
9639
|
["listFilter", this.listFilter],
|
|
9491
9640
|
["searchFields", this.searchFields],
|
|
9492
9641
|
["readonlyFields", this.readonlyFields],
|
|
9493
|
-
["identityField", [this.identityField]]
|
|
9642
|
+
["identityField", [this.identityField]],
|
|
9643
|
+
["uploadFields", this.uploadFields],
|
|
9644
|
+
["autocompleteFields", this.autocompleteFields]
|
|
9494
9645
|
]) {
|
|
9495
9646
|
for (const name of names) {
|
|
9496
9647
|
if (!known.has(name)) {
|
|
@@ -9560,6 +9711,25 @@ var AdminModel = class {
|
|
|
9560
9711
|
if (this.listDisplayOverride !== null) return [...this.listDisplayOverride];
|
|
9561
9712
|
return this.columnNames().filter((name) => !NEVER_LISTED.includes(name));
|
|
9562
9713
|
}
|
|
9714
|
+
/**
|
|
9715
|
+
* Look a lens up by its slug.
|
|
9716
|
+
*
|
|
9717
|
+
* @param slug - The `?lens=` value.
|
|
9718
|
+
* @returns The lens, or `null` when nothing matches.
|
|
9719
|
+
*/
|
|
9720
|
+
getLens(slug) {
|
|
9721
|
+
return this.lenses.find((lens) => lens.slug === slug) ?? null;
|
|
9722
|
+
}
|
|
9723
|
+
/**
|
|
9724
|
+
* Return the audit/timestamp columns the model actually declares.
|
|
9725
|
+
*
|
|
9726
|
+
* @returns The subset of `createdAt` / `updatedAt` / `createdBy` /
|
|
9727
|
+
* `updatedBy` present on the model, in that order.
|
|
9728
|
+
*/
|
|
9729
|
+
auditFieldNames() {
|
|
9730
|
+
const known = new Set(this.columnNames());
|
|
9731
|
+
return AUDIT_FIELDS.filter((name) => known.has(name));
|
|
9732
|
+
}
|
|
9563
9733
|
/**
|
|
9564
9734
|
* Return the columns the detail view renders.
|
|
9565
9735
|
*
|
|
@@ -9568,10 +9738,16 @@ var AdminModel = class {
|
|
|
9568
9738
|
* where an operator goes to see the whole record, so trimming it there would
|
|
9569
9739
|
* hide data with nowhere else to read it.
|
|
9570
9740
|
*
|
|
9571
|
-
*
|
|
9741
|
+
* The audit/timestamp columns are held back too — they render in the detail
|
|
9742
|
+
* view's own audit panel, next to the change history, rather than scattered
|
|
9743
|
+
* among the domain fields.
|
|
9744
|
+
*
|
|
9745
|
+
* @returns Every domain column, in declaration order.
|
|
9572
9746
|
*/
|
|
9573
9747
|
detailFieldNames() {
|
|
9574
|
-
return this.columnNames().filter(
|
|
9748
|
+
return this.columnNames().filter(
|
|
9749
|
+
(name) => !NEVER_LISTED.includes(name) && !AUDIT_FIELDS.includes(name)
|
|
9750
|
+
);
|
|
9575
9751
|
}
|
|
9576
9752
|
/**
|
|
9577
9753
|
* Return the columns a create/edit form exposes.
|
|
@@ -9647,11 +9823,15 @@ function buildFormFields(admin, options = {}) {
|
|
|
9647
9823
|
const values = options.values ?? {};
|
|
9648
9824
|
const errors = options.errors ?? {};
|
|
9649
9825
|
const foreignKeys = options.foreignKeyOptions ?? {};
|
|
9826
|
+
const autocompleteUrls = options.autocompleteUrls ?? {};
|
|
9827
|
+
const autocompleteLabels = options.autocompleteLabels ?? {};
|
|
9828
|
+
const uploads = new Set(admin.uploadFields);
|
|
9650
9829
|
return admin.editableFieldNames().flatMap((name) => {
|
|
9651
9830
|
const column6 = columns[name];
|
|
9652
9831
|
if (column6 === void 0) return [];
|
|
9653
9832
|
const related = foreignKeys[name];
|
|
9654
|
-
const
|
|
9833
|
+
const autocompleteUrl = autocompleteUrls[name];
|
|
9834
|
+
const spec = uploads.has(name) ? { widget: "file", step: null, options: [] } : autocompleteUrl !== void 0 ? { widget: "autocomplete", step: null, options: [] } : related === void 0 ? widgetForColumn(column6) : { widget: "select", step: null, options: related };
|
|
9655
9835
|
const raw = name in values ? values[name] : literalDefault(column6);
|
|
9656
9836
|
return [
|
|
9657
9837
|
{
|
|
@@ -9663,7 +9843,9 @@ function buildFormFields(admin, options = {}) {
|
|
|
9663
9843
|
checked: spec.widget === "checkbox" && toBoolean(raw),
|
|
9664
9844
|
step: spec.step,
|
|
9665
9845
|
options: spec.options,
|
|
9666
|
-
error: errors[name] ?? null
|
|
9846
|
+
error: errors[name] ?? null,
|
|
9847
|
+
autocompleteUrl: autocompleteUrl ?? null,
|
|
9848
|
+
displayLabel: autocompleteLabels[name] ?? ""
|
|
9667
9849
|
}
|
|
9668
9850
|
];
|
|
9669
9851
|
});
|
|
@@ -9711,13 +9893,15 @@ function coerceValue(column6, widget, raw) {
|
|
|
9711
9893
|
return raw;
|
|
9712
9894
|
}
|
|
9713
9895
|
}
|
|
9714
|
-
function parseFormBody(admin, body) {
|
|
9896
|
+
function parseFormBody(admin, body, options = {}) {
|
|
9715
9897
|
const columns = adminColumns(admin.model);
|
|
9716
9898
|
const data = {};
|
|
9717
9899
|
const errors = {};
|
|
9900
|
+
const uploads = options.uploadsAsText === true ? /* @__PURE__ */ new Set() : new Set(admin.uploadFields);
|
|
9718
9901
|
for (const name of admin.editableFieldNames()) {
|
|
9719
9902
|
const column6 = columns[name];
|
|
9720
9903
|
if (column6 === void 0) continue;
|
|
9904
|
+
if (uploads.has(name)) continue;
|
|
9721
9905
|
const { widget } = widgetForColumn(column6);
|
|
9722
9906
|
if (widget === "checkbox") {
|
|
9723
9907
|
data[name] = toBoolean(body[name]);
|
|
@@ -10028,6 +10212,8 @@ var AdminSite = class {
|
|
|
10028
10212
|
siteUrl;
|
|
10029
10213
|
/** Typed appearance overrides. */
|
|
10030
10214
|
theme;
|
|
10215
|
+
/** Business-metric cards rendered at the top of the dashboard. */
|
|
10216
|
+
dashboardCards;
|
|
10031
10217
|
registry = /* @__PURE__ */ new Map();
|
|
10032
10218
|
/**
|
|
10033
10219
|
* Initialize the site.
|
|
@@ -10040,6 +10226,7 @@ var AdminSite = class {
|
|
|
10040
10226
|
this.indexSubtitle = options.indexSubtitle ?? "Site administration";
|
|
10041
10227
|
this.siteUrl = options.siteUrl ?? null;
|
|
10042
10228
|
this.theme = options.theme ?? {};
|
|
10229
|
+
this.dashboardCards = [...options.dashboardCards ?? []];
|
|
10043
10230
|
}
|
|
10044
10231
|
/**
|
|
10045
10232
|
* Return the centered header brand text.
|
|
@@ -11859,7 +12046,7 @@ function renderMfaPage(context, error) {
|
|
|
11859
12046
|
</section>`;
|
|
11860
12047
|
return renderLayout(context, `Two-factor \xB7 ${context.site.title}`, body);
|
|
11861
12048
|
}
|
|
11862
|
-
function renderDashboardPage(context, cards, metrics) {
|
|
12049
|
+
function renderDashboardPage(context, cards, metrics, businessCards = []) {
|
|
11863
12050
|
const metricsPanel = metrics === null ? "" : `<div class="tempest-admin-stats" aria-label="System metrics">
|
|
11864
12051
|
<div class="tempest-admin-stat">
|
|
11865
12052
|
<span class="tempest-admin-stat__label">CPU</span>
|
|
@@ -11871,6 +12058,7 @@ function renderDashboardPage(context, cards, metrics) {
|
|
|
11871
12058
|
<span class="tempest-admin-stat__sub">${escapeHtml(metrics.memoryUsedGb)} / ${escapeHtml(metrics.memoryTotalGb)} GB</span>
|
|
11872
12059
|
</div>
|
|
11873
12060
|
</div>`;
|
|
12061
|
+
const business = businessCards.length > 0 ? `<div class="tempest-admin-cards" aria-label="Business metrics">${businessCards.map(renderBusinessCard).join("")}</div>` : "";
|
|
11874
12062
|
const models = cards.length > 0 ? `<div class="tempest-admin-models">${cards.map(
|
|
11875
12063
|
(card) => `<article class="tempest-admin-model-card">
|
|
11876
12064
|
<header class="tempest-admin-model-card__head">
|
|
@@ -11887,10 +12075,53 @@ function renderDashboardPage(context, cards, metrics) {
|
|
|
11887
12075
|
<h1>${escapeHtml(context.site.title)}</h1>
|
|
11888
12076
|
<p>${escapeHtml(context.site.indexSubtitle)}</p>
|
|
11889
12077
|
${metricsPanel}
|
|
12078
|
+
${business}
|
|
11890
12079
|
${models}
|
|
11891
12080
|
</section>`;
|
|
11892
12081
|
return renderLayout(context, context.site.title, body);
|
|
11893
12082
|
}
|
|
12083
|
+
function renderBusinessCard(card) {
|
|
12084
|
+
const help = card.helpText !== null ? `<span class="tempest-admin-card__help">${escapeHtml(card.helpText)}</span>` : "";
|
|
12085
|
+
if (card.error !== null) {
|
|
12086
|
+
return `<article class="tempest-admin-card tempest-admin-card--value">
|
|
12087
|
+
<span class="tempest-admin-card__label">${escapeHtml(card.label)}</span>
|
|
12088
|
+
<span class="tempest-admin-card__value">\u2014</span>
|
|
12089
|
+
<span class="tempest-admin-card__help">${escapeHtml(card.error)}</span>
|
|
12090
|
+
</article>`;
|
|
12091
|
+
}
|
|
12092
|
+
const unit = card.unit !== null ? ` <small>${escapeHtml(card.unit)}</small>` : "";
|
|
12093
|
+
if (card.kind === "partition") {
|
|
12094
|
+
const parts = card.segments.map(
|
|
12095
|
+
(segment) => `<li>
|
|
12096
|
+
<span class="tempest-admin-card__part-label">${escapeHtml(segment.label)}</span>
|
|
12097
|
+
<span class="tempest-admin-card__part-bar"><span style="width: ${Math.round(segment.percent)}%"></span></span>
|
|
12098
|
+
<span class="tempest-admin-card__part-value">${escapeHtml(segment.value)}</span>
|
|
12099
|
+
</li>`
|
|
12100
|
+
).join("");
|
|
12101
|
+
return `<article class="tempest-admin-card tempest-admin-card--partition">
|
|
12102
|
+
<span class="tempest-admin-card__label">${escapeHtml(card.label)}</span>
|
|
12103
|
+
<ul class="tempest-admin-card__parts">${parts}</ul>
|
|
12104
|
+
${help}
|
|
12105
|
+
</article>`;
|
|
12106
|
+
}
|
|
12107
|
+
if (card.kind === "trend") {
|
|
12108
|
+
const arrow = card.direction === "up" ? "\u25B2" : card.direction === "down" ? "\u25BC" : "\u25AC";
|
|
12109
|
+
return `<article class="tempest-admin-card tempest-admin-card--trend">
|
|
12110
|
+
<span class="tempest-admin-card__label">${escapeHtml(card.label)}</span>
|
|
12111
|
+
<span class="tempest-admin-card__value">${escapeHtml(card.value)}${unit}</span>
|
|
12112
|
+
<span class="tempest-admin-card__trend tempest-admin-card__trend--${escapeHtml(card.direction)}">
|
|
12113
|
+
${arrow} ${card.percent === null ? "\u2014" : escapeHtml(card.percent)}
|
|
12114
|
+
<small>vs prev ${escapeHtml(card.previous)}</small>
|
|
12115
|
+
</span>
|
|
12116
|
+
${help}
|
|
12117
|
+
</article>`;
|
|
12118
|
+
}
|
|
12119
|
+
return `<article class="tempest-admin-card tempest-admin-card--value">
|
|
12120
|
+
<span class="tempest-admin-card__label">${escapeHtml(card.label)}</span>
|
|
12121
|
+
<span class="tempest-admin-card__value">${escapeHtml(card.value)}${unit}</span>
|
|
12122
|
+
${help}
|
|
12123
|
+
</article>`;
|
|
12124
|
+
}
|
|
11894
12125
|
function renderFilter(filter) {
|
|
11895
12126
|
const name = `filter_${filter.field}`;
|
|
11896
12127
|
if (filter.kind === "select") {
|
|
@@ -11956,10 +12187,14 @@ function renderListPage(context, view) {
|
|
|
11956
12187
|
</form>
|
|
11957
12188
|
<div class="tempest-admin-list__actions">
|
|
11958
12189
|
${view.newUrl !== null ? `<a class="tempest-admin-list__new" href="${escapeHtml(view.newUrl)}">+ New</a>` : ""}
|
|
12190
|
+
${view.importUrl !== null ? `<a href="${escapeHtml(view.importUrl)}">Import CSV</a>` : ""}
|
|
11959
12191
|
<a href="${escapeHtml(view.exportCsvUrl)}">Export CSV</a>
|
|
11960
12192
|
<a href="${escapeHtml(view.exportJsonUrl)}">Export JSON</a>
|
|
11961
12193
|
</div>
|
|
11962
12194
|
</div>
|
|
12195
|
+
${view.lenses.length > 0 ? `<nav class="tempest-admin-lenses" aria-label="Lenses">${view.lenses.map(
|
|
12196
|
+
(lens) => `<a class="tempest-admin-lens${lens.active ? " tempest-admin-lens--active" : ""}" href="${escapeHtml(lens.url)}">${escapeHtml(lens.label)}</a>`
|
|
12197
|
+
).join("")}</nav>` : ""}
|
|
11963
12198
|
${bulkBar}
|
|
11964
12199
|
<div class="tempest-admin-table-wrap">
|
|
11965
12200
|
<table class="tempest-admin-list__table">
|
|
@@ -11983,6 +12218,7 @@ function renderDetailPage(context, view) {
|
|
|
11983
12218
|
const fields = view.fields.map(
|
|
11984
12219
|
(field) => `<dt>${escapeHtml(field.label)}</dt><dd>${field.value === "" ? "<em>\u2014</em>" : escapeHtml(field.value)}</dd>`
|
|
11985
12220
|
).join("");
|
|
12221
|
+
const auditPanel = view.audit === null ? "" : renderAuditPanel(view.audit);
|
|
11986
12222
|
const body = `<section class="tempest-admin-detail">
|
|
11987
12223
|
<header class="tempest-admin-detail__header">
|
|
11988
12224
|
<h1>${escapeHtml(view.title)} \xB7 ${escapeHtml(view.identity)}</h1>
|
|
@@ -11996,9 +12232,37 @@ function renderDetailPage(context, view) {
|
|
|
11996
12232
|
</div>
|
|
11997
12233
|
</header>
|
|
11998
12234
|
<dl class="tempest-admin-detail__fields">${fields}</dl>
|
|
12235
|
+
${auditPanel}
|
|
11999
12236
|
</section>`;
|
|
12000
12237
|
return renderLayout(context, `${view.title} \xB7 ${view.identity}`, body);
|
|
12001
12238
|
}
|
|
12239
|
+
function renderAuditPanel(audit) {
|
|
12240
|
+
const rows = audit.fields.map(
|
|
12241
|
+
(field) => `<dt>${escapeHtml(field.label)}</dt><dd>${field.value === "" ? "<em>\u2014</em>" : escapeHtml(field.value)}</dd>`
|
|
12242
|
+
).join("");
|
|
12243
|
+
const history = audit.history.length > 0 ? `<ol class="tempest-admin-history">${audit.history.map((entry) => {
|
|
12244
|
+
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(
|
|
12245
|
+
(change) => `<tr><td>${escapeHtml(change.field)}</td><td>${escapeHtml(change.before)}</td><td>${escapeHtml(change.after)}</td></tr>`
|
|
12246
|
+
).join("")}</tbody></table>` : "<p><em>No field changes recorded.</em></p>";
|
|
12247
|
+
const context = entry.context !== null ? `<pre class="tempest-admin-detail__json">${escapeHtml(entry.context)}</pre>` : "";
|
|
12248
|
+
return `<li class="tempest-admin-history__item">
|
|
12249
|
+
<details>
|
|
12250
|
+
<summary>
|
|
12251
|
+
<span class="tempest-admin-history__action">${escapeHtml(entry.action)}</span>
|
|
12252
|
+
<span class="tempest-admin-history__actor">${escapeHtml(entry.actor)}</span>
|
|
12253
|
+
<span class="tempest-admin-history__at">${escapeHtml(entry.at)}</span>
|
|
12254
|
+
</summary>
|
|
12255
|
+
${changes}
|
|
12256
|
+
${context}
|
|
12257
|
+
</details>
|
|
12258
|
+
</li>`;
|
|
12259
|
+
}).join("")}</ol>` : "";
|
|
12260
|
+
return `<section class="tempest-admin-audit">
|
|
12261
|
+
<h2>Audit</h2>
|
|
12262
|
+
<dl class="tempest-admin-detail__fields">${rows}</dl>
|
|
12263
|
+
${history}
|
|
12264
|
+
</section>`;
|
|
12265
|
+
}
|
|
12002
12266
|
function renderFormField(field) {
|
|
12003
12267
|
const required = field.required ? " required" : "";
|
|
12004
12268
|
const name = escapeHtml(field.name);
|
|
@@ -12023,6 +12287,18 @@ function renderFormField(field) {
|
|
|
12023
12287
|
control = `<label><span>${label}${field.required ? " *" : ""}</span><select name="${name}"${required}>${blank}${options}</select></label>`;
|
|
12024
12288
|
break;
|
|
12025
12289
|
}
|
|
12290
|
+
case "file":
|
|
12291
|
+
control = `<label><span>${label}${field.required ? " *" : ""}</span><input type="file" name="${name}"${required}></label>${field.value === "" ? "" : `<small class="tempest-admin-form__hint">Current: ${value} \u2014 choose a file to replace it.</small>`}`;
|
|
12292
|
+
break;
|
|
12293
|
+
case "autocomplete":
|
|
12294
|
+
control = `<label><span>${label}${field.required ? " *" : ""}</span>
|
|
12295
|
+
<div class="tempest-admin-ac" data-ac data-ac-url="${escapeHtml(field.autocompleteUrl)}">
|
|
12296
|
+
<input type="text" class="tempest-admin-ac__search" value="${escapeHtml(field.displayLabel)}" placeholder="Search\u2026" autocomplete="off" data-ac-search>
|
|
12297
|
+
<input type="hidden" name="${name}" value="${value}"${required} data-ac-value>
|
|
12298
|
+
<ul class="tempest-admin-ac__results" data-ac-results hidden></ul>
|
|
12299
|
+
</div>
|
|
12300
|
+
</label>`;
|
|
12301
|
+
break;
|
|
12026
12302
|
case "number":
|
|
12027
12303
|
control = `<label><span>${label}${field.required ? " *" : ""}</span><input type="number" name="${name}" value="${value}"${field.step !== null ? ` step="${escapeHtml(field.step)}"` : ""}${required}></label>`;
|
|
12028
12304
|
break;
|
|
@@ -12050,7 +12326,7 @@ function renderFormPage(context, view) {
|
|
|
12050
12326
|
<a href="${escapeHtml(view.backUrl)}">\u2190 Back</a>
|
|
12051
12327
|
</header>
|
|
12052
12328
|
${view.error !== null ? `<p class="tempest-admin-form__error">${escapeHtml(view.error)}</p>` : ""}
|
|
12053
|
-
<form method="post" action="${escapeHtml(view.actionUrl)}" class="tempest-admin-form__form">
|
|
12329
|
+
<form method="post" action="${escapeHtml(view.actionUrl)}" class="tempest-admin-form__form"${view.fields.some((field) => field.widget === "file") ? ' enctype="multipart/form-data"' : ""}>
|
|
12054
12330
|
<input type="hidden" name="csrf_token" value="${escapeHtml(context.session.csrfToken)}">
|
|
12055
12331
|
${view.fields.map(renderFormField).join("")}
|
|
12056
12332
|
<div class="tempest-admin-form__actions">
|
|
@@ -12058,10 +12334,98 @@ function renderFormPage(context, view) {
|
|
|
12058
12334
|
<a href="${escapeHtml(view.backUrl)}" class="tempest-admin-form__cancel">Cancel</a>
|
|
12059
12335
|
</div>
|
|
12060
12336
|
</form>
|
|
12337
|
+
${view.fields.some((field) => field.widget === "autocomplete") ? AUTOCOMPLETE_SCRIPT : ""}
|
|
12061
12338
|
</section>`;
|
|
12062
12339
|
return renderLayout(context, `${heading} \xB7 ${context.site.title}`, body);
|
|
12063
12340
|
}
|
|
12341
|
+
function renderImportPage(context, view) {
|
|
12342
|
+
if (context.session === null) throw new Error("The import page requires a session");
|
|
12343
|
+
const summary = view.created === null ? "" : `<p class="tempest-admin-import__summary">Created ${escapeHtml(view.created)} record${view.created === 1 ? "" : "s"}.</p>`;
|
|
12344
|
+
const failures = view.rowErrors.length > 0 ? `<table class="tempest-admin-import__errors"><thead><tr><th>Row</th><th>Problem</th></tr></thead><tbody>${view.rowErrors.map(
|
|
12345
|
+
(failure) => `<tr><td>${escapeHtml(failure.row)}</td><td>${escapeHtml(failure.message)}</td></tr>`
|
|
12346
|
+
).join("")}</tbody></table>` : "";
|
|
12347
|
+
const body = `<section class="tempest-admin-import">
|
|
12348
|
+
<header class="tempest-admin-detail__header">
|
|
12349
|
+
<h1>Import ${escapeHtml(view.title)}</h1>
|
|
12350
|
+
<a href="${escapeHtml(view.backUrl)}">\u2190 Back</a>
|
|
12351
|
+
</header>
|
|
12352
|
+
${view.error !== null ? `<p class="tempest-admin-form__error">${escapeHtml(view.error)}</p>` : ""}
|
|
12353
|
+
${summary}
|
|
12354
|
+
${failures}
|
|
12355
|
+
<form method="post" action="${escapeHtml(view.actionUrl)}" class="tempest-admin-form__form" enctype="multipart/form-data">
|
|
12356
|
+
<input type="hidden" name="csrf_token" value="${escapeHtml(context.session.csrfToken)}">
|
|
12357
|
+
<div class="tempest-admin-form__field">
|
|
12358
|
+
<label>
|
|
12359
|
+
<span>CSV file *</span>
|
|
12360
|
+
<input type="file" name="file" accept=".csv,text/csv" required>
|
|
12361
|
+
</label>
|
|
12362
|
+
<small class="tempest-admin-form__hint">
|
|
12363
|
+
UTF-8, comma-separated, with a header row. Recognised columns:
|
|
12364
|
+
<code>${escapeHtml(view.columns.join(", "))}</code>. Unknown columns are ignored.
|
|
12365
|
+
</small>
|
|
12366
|
+
</div>
|
|
12367
|
+
<div class="tempest-admin-form__actions">
|
|
12368
|
+
<button type="submit">Import</button>
|
|
12369
|
+
<a href="${escapeHtml(view.backUrl)}" class="tempest-admin-form__cancel">Cancel</a>
|
|
12370
|
+
</div>
|
|
12371
|
+
</form>
|
|
12372
|
+
</section>`;
|
|
12373
|
+
return renderLayout(context, `Import ${view.title} \xB7 ${context.site.title}`, body);
|
|
12374
|
+
}
|
|
12375
|
+
var AUTOCOMPLETE_SCRIPT = `<script>
|
|
12376
|
+
(function () {
|
|
12377
|
+
document.querySelectorAll("[data-ac]").forEach(function (box) {
|
|
12378
|
+
var search = box.querySelector("[data-ac-search]");
|
|
12379
|
+
var value = box.querySelector("[data-ac-value]");
|
|
12380
|
+
var results = box.querySelector("[data-ac-results]");
|
|
12381
|
+
var url = box.getAttribute("data-ac-url");
|
|
12382
|
+
var timer = null;
|
|
12383
|
+
|
|
12384
|
+
function close() {
|
|
12385
|
+
results.hidden = true;
|
|
12386
|
+
results.innerHTML = "";
|
|
12387
|
+
}
|
|
12388
|
+
|
|
12389
|
+
function pick(option) {
|
|
12390
|
+
value.value = option.value;
|
|
12391
|
+
search.value = option.label;
|
|
12392
|
+
close();
|
|
12393
|
+
}
|
|
12394
|
+
|
|
12395
|
+
function run() {
|
|
12396
|
+
var term = search.value.trim();
|
|
12397
|
+
fetch(url + "?q=" + encodeURIComponent(term), { credentials: "same-origin" })
|
|
12398
|
+
.then(function (response) { return response.ok ? response.json() : { options: [] }; })
|
|
12399
|
+
.then(function (payload) {
|
|
12400
|
+
results.innerHTML = "";
|
|
12401
|
+
(payload.options || []).forEach(function (option) {
|
|
12402
|
+
var item = document.createElement("li");
|
|
12403
|
+
item.textContent = option.label;
|
|
12404
|
+
item.setAttribute("role", "option");
|
|
12405
|
+
item.addEventListener("mousedown", function (event) {
|
|
12406
|
+
event.preventDefault();
|
|
12407
|
+
pick(option);
|
|
12408
|
+
});
|
|
12409
|
+
results.appendChild(item);
|
|
12410
|
+
});
|
|
12411
|
+
results.hidden = results.children.length === 0;
|
|
12412
|
+
})
|
|
12413
|
+
.catch(close);
|
|
12414
|
+
}
|
|
12415
|
+
|
|
12416
|
+
search.addEventListener("input", function () {
|
|
12417
|
+
value.value = "";
|
|
12418
|
+
window.clearTimeout(timer);
|
|
12419
|
+
timer = window.setTimeout(run, 250);
|
|
12420
|
+
});
|
|
12421
|
+
search.addEventListener("focus", run);
|
|
12422
|
+
search.addEventListener("blur", function () { window.setTimeout(close, 150); });
|
|
12423
|
+
});
|
|
12424
|
+
})();
|
|
12425
|
+
</script>`;
|
|
12064
12426
|
var logger2 = new JSONLogger("tempest_express_sdk.admin.router");
|
|
12427
|
+
var AUTOCOMPLETE_LIMIT = 20;
|
|
12428
|
+
var AUDIT_HISTORY_LIMIT = 50;
|
|
12065
12429
|
var FK_OPTION_CAP = 1e3;
|
|
12066
12430
|
var FLASH_MAX_LENGTH = 300;
|
|
12067
12431
|
var FLASH_MESSAGES = {
|
|
@@ -12090,6 +12454,7 @@ function makeAdminRouter(site, options) {
|
|
|
12090
12454
|
const theme = resolveAdminTheme(site.theme);
|
|
12091
12455
|
const showMetrics = options.showMetrics ?? true;
|
|
12092
12456
|
const exportMaxRows = options.exportMaxRows ?? 5e3;
|
|
12457
|
+
const maxUploadBytes = options.maxUploadBytes ?? 10 * 1024 * 1024;
|
|
12093
12458
|
const sessions = new AdminSessionStore({
|
|
12094
12459
|
secret: options.secretKey,
|
|
12095
12460
|
...options.cookieName === void 0 ? {} : { cookieName: options.cookieName },
|
|
@@ -12100,18 +12465,23 @@ function makeAdminRouter(site, options) {
|
|
|
12100
12465
|
const backend = options.authBackend;
|
|
12101
12466
|
const router = express3.Router();
|
|
12102
12467
|
router.use(prefix, express3.urlencoded({ extended: false }));
|
|
12103
|
-
const context = (req, session) => ({
|
|
12468
|
+
const context = (req, session, models = site.list()) => ({
|
|
12104
12469
|
site,
|
|
12105
12470
|
theme,
|
|
12106
12471
|
prefix,
|
|
12107
12472
|
session,
|
|
12108
12473
|
currentPath: req.originalUrl.split("?")[0] ?? req.path,
|
|
12109
|
-
navModels:
|
|
12474
|
+
navModels: models.map((admin) => ({
|
|
12110
12475
|
label: admin.verboseNamePlural(),
|
|
12111
12476
|
url: `${prefix}/m/${admin.slug()}`
|
|
12112
12477
|
})),
|
|
12113
12478
|
messages: flashFor(req)
|
|
12114
12479
|
});
|
|
12480
|
+
const allows = async (principal, admin, action) => {
|
|
12481
|
+
if (!flagAllows(admin, action)) return false;
|
|
12482
|
+
if (options.accessPolicy === void 0) return true;
|
|
12483
|
+
return Boolean(await options.accessPolicy(principal, admin, action));
|
|
12484
|
+
};
|
|
12115
12485
|
const flashFor = (req) => {
|
|
12116
12486
|
const fixed = FLASH_MESSAGES[queryString(req.query.ok)];
|
|
12117
12487
|
if (fixed !== void 0) return [fixed];
|
|
@@ -12159,14 +12529,28 @@ function makeAdminRouter(site, options) {
|
|
|
12159
12529
|
res.redirect(`${prefix}/mfa`);
|
|
12160
12530
|
return null;
|
|
12161
12531
|
}
|
|
12162
|
-
|
|
12532
|
+
const visible = [];
|
|
12533
|
+
for (const admin of site.list()) {
|
|
12534
|
+
if (await allows(principal, admin, AdminPermission.VIEW)) visible.push(admin);
|
|
12535
|
+
}
|
|
12536
|
+
return { session, dbSession, principal, visible };
|
|
12163
12537
|
};
|
|
12164
|
-
const resolveAdmin = (req, res, state) => {
|
|
12538
|
+
const resolveAdmin = async (req, res, state, action = AdminPermission.VIEW) => {
|
|
12165
12539
|
const admin = site.get(String(req.params.slug));
|
|
12166
|
-
if (admin === null) {
|
|
12167
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12540
|
+
if (admin === null || !await allows(state.principal, admin, AdminPermission.VIEW)) {
|
|
12541
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12168
12542
|
return null;
|
|
12169
12543
|
}
|
|
12544
|
+
if (action !== AdminPermission.VIEW) {
|
|
12545
|
+
if (!flagAllows(admin, action)) {
|
|
12546
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12547
|
+
return null;
|
|
12548
|
+
}
|
|
12549
|
+
if (!await allows(state.principal, admin, action)) {
|
|
12550
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12551
|
+
return null;
|
|
12552
|
+
}
|
|
12553
|
+
}
|
|
12170
12554
|
return admin;
|
|
12171
12555
|
};
|
|
12172
12556
|
const renderNotFound = (ctx) => renderDashboardPage(
|
|
@@ -12177,7 +12561,7 @@ function makeAdminRouter(site, options) {
|
|
|
12177
12561
|
const checkCsrf = (req, res, state) => {
|
|
12178
12562
|
const body = req.body;
|
|
12179
12563
|
if (csrfTokenMatches(state.session, body?.csrf_token)) return true;
|
|
12180
|
-
html(res, renderNotFound(context(req, state.session)), 403);
|
|
12564
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12181
12565
|
return false;
|
|
12182
12566
|
};
|
|
12183
12567
|
router.get(`${prefix}/static/admin.css`, (_req, res) => {
|
|
@@ -12259,7 +12643,7 @@ function makeAdminRouter(site, options) {
|
|
|
12259
12643
|
const state = await authenticate(req, res);
|
|
12260
12644
|
if (state === null) return;
|
|
12261
12645
|
const cards = [];
|
|
12262
|
-
for (const admin of
|
|
12646
|
+
for (const admin of state.visible) {
|
|
12263
12647
|
let count = null;
|
|
12264
12648
|
try {
|
|
12265
12649
|
count = await admin.repository(state.dbSession).count();
|
|
@@ -12273,9 +12657,13 @@ function makeAdminRouter(site, options) {
|
|
|
12273
12657
|
label: admin.verboseNamePlural(),
|
|
12274
12658
|
count,
|
|
12275
12659
|
url: `${prefix}/m/${admin.slug()}`,
|
|
12276
|
-
newUrl: admin.
|
|
12660
|
+
newUrl: await allows(state.principal, admin, AdminPermission.CREATE) ? `${prefix}/m/${admin.slug()}/new` : null
|
|
12277
12661
|
});
|
|
12278
12662
|
}
|
|
12663
|
+
const businessCards = [];
|
|
12664
|
+
for (const card of site.dashboardCards) {
|
|
12665
|
+
businessCards.push(await computeBusinessCard(card, state.dbSession));
|
|
12666
|
+
}
|
|
12279
12667
|
let metrics = null;
|
|
12280
12668
|
if (showMetrics) {
|
|
12281
12669
|
const snapshot2 = MetricsUtils.system();
|
|
@@ -12286,7 +12674,15 @@ function makeAdminRouter(site, options) {
|
|
|
12286
12674
|
memoryTotalGb: (snapshot2.memory.total / 1024 ** 3).toFixed(1)
|
|
12287
12675
|
};
|
|
12288
12676
|
}
|
|
12289
|
-
html(
|
|
12677
|
+
html(
|
|
12678
|
+
res,
|
|
12679
|
+
renderDashboardPage(
|
|
12680
|
+
context(req, state.session, state.visible),
|
|
12681
|
+
cards,
|
|
12682
|
+
metrics,
|
|
12683
|
+
businessCards
|
|
12684
|
+
)
|
|
12685
|
+
);
|
|
12290
12686
|
})
|
|
12291
12687
|
);
|
|
12292
12688
|
router.get(
|
|
@@ -12294,7 +12690,7 @@ function makeAdminRouter(site, options) {
|
|
|
12294
12690
|
guarded(async (req, res) => {
|
|
12295
12691
|
const state = await authenticate(req, res);
|
|
12296
12692
|
if (state === null) return;
|
|
12297
|
-
const admin = resolveAdmin(req, res, state);
|
|
12693
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12298
12694
|
if (admin === null) return;
|
|
12299
12695
|
html(res, await renderList(req, admin, state));
|
|
12300
12696
|
})
|
|
@@ -12304,19 +12700,20 @@ function makeAdminRouter(site, options) {
|
|
|
12304
12700
|
guarded(async (req, res) => {
|
|
12305
12701
|
const state = await authenticate(req, res);
|
|
12306
12702
|
if (state === null) return;
|
|
12307
|
-
const admin = resolveAdmin(req, res, state);
|
|
12703
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.CREATE);
|
|
12308
12704
|
if (admin === null) return;
|
|
12309
12705
|
if (!admin.canCreate) {
|
|
12310
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12706
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12311
12707
|
return;
|
|
12312
12708
|
}
|
|
12313
12709
|
html(
|
|
12314
12710
|
res,
|
|
12315
|
-
renderFormPage(context(req, state.session), {
|
|
12711
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12316
12712
|
mode: "create",
|
|
12317
12713
|
title: admin.verboseName(),
|
|
12318
12714
|
fields: buildFormFields(admin, {
|
|
12319
|
-
foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession)
|
|
12715
|
+
foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession),
|
|
12716
|
+
autocompleteUrls: autocompleteUrlsFor(admin)
|
|
12320
12717
|
}),
|
|
12321
12718
|
actionUrl: `${prefix}/m/${admin.slug()}/new`,
|
|
12322
12719
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
@@ -12330,26 +12727,46 @@ function makeAdminRouter(site, options) {
|
|
|
12330
12727
|
guarded(async (req, res) => {
|
|
12331
12728
|
const state = await authenticate(req, res);
|
|
12332
12729
|
if (state === null) return;
|
|
12333
|
-
const admin = resolveAdmin(req, res, state);
|
|
12730
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.CREATE);
|
|
12334
12731
|
if (admin === null) return;
|
|
12335
12732
|
if (!admin.canCreate) {
|
|
12336
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12733
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12734
|
+
return;
|
|
12735
|
+
}
|
|
12736
|
+
let uploadError = null;
|
|
12737
|
+
let submission;
|
|
12738
|
+
try {
|
|
12739
|
+
submission = await readSubmission(req);
|
|
12740
|
+
} catch (error) {
|
|
12741
|
+
if (!(error instanceof MultipartLimitError)) throw error;
|
|
12742
|
+
submission = { fields: {}, files: [] };
|
|
12743
|
+
uploadError = error.message;
|
|
12744
|
+
}
|
|
12745
|
+
const body = submission.fields;
|
|
12746
|
+
if (uploadError === null && !csrfTokenMatches(state.session, body.csrf_token)) {
|
|
12747
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12337
12748
|
return;
|
|
12338
12749
|
}
|
|
12339
|
-
if (!checkCsrf(req, res, state)) return;
|
|
12340
|
-
const body = req.body;
|
|
12341
12750
|
const parsed = parseFormBody(admin, body);
|
|
12751
|
+
await applyUploads(admin, parsed.data, parsed.errors, submission.files, true);
|
|
12342
12752
|
const foreignKeyOptions = await foreignKeyOptionsFor(admin, state.dbSession);
|
|
12753
|
+
const autocompleteLabels = await autocompleteLabelsFor(
|
|
12754
|
+
admin,
|
|
12755
|
+
body,
|
|
12756
|
+
state.dbSession
|
|
12757
|
+
);
|
|
12343
12758
|
const rerender = (error, status) => {
|
|
12344
12759
|
html(
|
|
12345
12760
|
res,
|
|
12346
|
-
renderFormPage(context(req, state.session), {
|
|
12761
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12347
12762
|
mode: "create",
|
|
12348
12763
|
title: admin.verboseName(),
|
|
12349
12764
|
fields: buildFormFields(admin, {
|
|
12350
12765
|
values: body,
|
|
12351
12766
|
errors: parsed.errors,
|
|
12352
|
-
foreignKeyOptions
|
|
12767
|
+
foreignKeyOptions,
|
|
12768
|
+
autocompleteUrls: autocompleteUrlsFor(admin),
|
|
12769
|
+
autocompleteLabels
|
|
12353
12770
|
}),
|
|
12354
12771
|
actionUrl: `${prefix}/m/${admin.slug()}/new`,
|
|
12355
12772
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
@@ -12358,10 +12775,15 @@ function makeAdminRouter(site, options) {
|
|
|
12358
12775
|
status
|
|
12359
12776
|
);
|
|
12360
12777
|
};
|
|
12778
|
+
if (uploadError !== null) {
|
|
12779
|
+
rerender(uploadError, 400);
|
|
12780
|
+
return;
|
|
12781
|
+
}
|
|
12361
12782
|
if (Object.keys(parsed.errors).length > 0) {
|
|
12362
12783
|
rerender("Please fix the highlighted fields.", 400);
|
|
12363
12784
|
return;
|
|
12364
12785
|
}
|
|
12786
|
+
stampActor(admin, parsed.data, backend.principalId(state.principal), true);
|
|
12365
12787
|
try {
|
|
12366
12788
|
await admin.repository(state.dbSession).create(parsed.data);
|
|
12367
12789
|
} catch (error) {
|
|
@@ -12376,11 +12798,11 @@ function makeAdminRouter(site, options) {
|
|
|
12376
12798
|
guarded(async (req, res) => {
|
|
12377
12799
|
const state = await authenticate(req, res);
|
|
12378
12800
|
if (state === null) return;
|
|
12379
|
-
const admin = resolveAdmin(req, res, state);
|
|
12801
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12380
12802
|
if (admin === null) return;
|
|
12381
12803
|
const format = String(req.params.format);
|
|
12382
12804
|
if (format !== "csv" && format !== "json") {
|
|
12383
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12805
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12384
12806
|
return;
|
|
12385
12807
|
}
|
|
12386
12808
|
const query = await resolveListQuery(req, admin, state.dbSession);
|
|
@@ -12402,7 +12824,7 @@ function makeAdminRouter(site, options) {
|
|
|
12402
12824
|
guarded(async (req, res) => {
|
|
12403
12825
|
const state = await authenticate(req, res);
|
|
12404
12826
|
if (state === null) return;
|
|
12405
|
-
const admin = resolveAdmin(req, res, state);
|
|
12827
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12406
12828
|
if (admin === null) return;
|
|
12407
12829
|
if (!checkCsrf(req, res, state)) return;
|
|
12408
12830
|
const body = req.body;
|
|
@@ -12418,7 +12840,12 @@ function makeAdminRouter(site, options) {
|
|
|
12418
12840
|
return;
|
|
12419
12841
|
}
|
|
12420
12842
|
if (!bulkActionsFor(admin).some((option) => option.value === action)) {
|
|
12421
|
-
html(res, renderNotFound(context(req, state.session)), 400);
|
|
12843
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 400);
|
|
12844
|
+
return;
|
|
12845
|
+
}
|
|
12846
|
+
const needed = action === "delete" ? AdminPermission.DELETE : AdminPermission.EDIT;
|
|
12847
|
+
if (!await allows(state.principal, admin, needed)) {
|
|
12848
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12422
12849
|
return;
|
|
12423
12850
|
}
|
|
12424
12851
|
const repository = admin.repository(state.dbSession);
|
|
@@ -12426,7 +12853,7 @@ function makeAdminRouter(site, options) {
|
|
|
12426
12853
|
if (action.startsWith("custom:")) {
|
|
12427
12854
|
const custom = admin.getAction(action.slice("custom:".length));
|
|
12428
12855
|
if (custom === null) {
|
|
12429
|
-
html(res, renderNotFound(context(req, state.session)), 400);
|
|
12856
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 400);
|
|
12430
12857
|
return;
|
|
12431
12858
|
}
|
|
12432
12859
|
let result;
|
|
@@ -12471,28 +12898,152 @@ function makeAdminRouter(site, options) {
|
|
|
12471
12898
|
);
|
|
12472
12899
|
})
|
|
12473
12900
|
);
|
|
12901
|
+
router.get(
|
|
12902
|
+
`${prefix}/m/:slug/import`,
|
|
12903
|
+
guarded(async (req, res) => {
|
|
12904
|
+
const state = await authenticate(req, res);
|
|
12905
|
+
if (state === null) return;
|
|
12906
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.CREATE);
|
|
12907
|
+
if (admin === null) return;
|
|
12908
|
+
if (!admin.canImport) {
|
|
12909
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12910
|
+
return;
|
|
12911
|
+
}
|
|
12912
|
+
html(res, renderImport(req, state, admin, null, null, []));
|
|
12913
|
+
})
|
|
12914
|
+
);
|
|
12915
|
+
router.post(
|
|
12916
|
+
`${prefix}/m/:slug/import`,
|
|
12917
|
+
guarded(async (req, res) => {
|
|
12918
|
+
const state = await authenticate(req, res);
|
|
12919
|
+
if (state === null) return;
|
|
12920
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.CREATE);
|
|
12921
|
+
if (admin === null) return;
|
|
12922
|
+
if (!admin.canImport) {
|
|
12923
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12924
|
+
return;
|
|
12925
|
+
}
|
|
12926
|
+
let submission;
|
|
12927
|
+
try {
|
|
12928
|
+
submission = await readSubmission(req);
|
|
12929
|
+
} catch (error) {
|
|
12930
|
+
if (!(error instanceof MultipartLimitError)) throw error;
|
|
12931
|
+
html(res, renderImport(req, state, admin, error.message, null, []), 400);
|
|
12932
|
+
return;
|
|
12933
|
+
}
|
|
12934
|
+
if (!csrfTokenMatches(state.session, submission.fields.csrf_token)) {
|
|
12935
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12936
|
+
return;
|
|
12937
|
+
}
|
|
12938
|
+
const file = submission.files[0];
|
|
12939
|
+
if (file === void 0) {
|
|
12940
|
+
html(
|
|
12941
|
+
res,
|
|
12942
|
+
renderImport(req, state, admin, "Choose a CSV file to import.", null, []),
|
|
12943
|
+
400
|
|
12944
|
+
);
|
|
12945
|
+
return;
|
|
12946
|
+
}
|
|
12947
|
+
let rows;
|
|
12948
|
+
try {
|
|
12949
|
+
rows = parseCsv(file.data.toString("utf8"));
|
|
12950
|
+
} catch (error) {
|
|
12951
|
+
const message = error instanceof Error ? error.message : "Could not read the file as UTF-8 CSV.";
|
|
12952
|
+
html(res, renderImport(req, state, admin, message, null, []), 400);
|
|
12953
|
+
return;
|
|
12954
|
+
}
|
|
12955
|
+
const repository = admin.repository(state.dbSession);
|
|
12956
|
+
const actorId = backend.principalId(state.principal);
|
|
12957
|
+
const rowErrors = [];
|
|
12958
|
+
let created = 0;
|
|
12959
|
+
for (const [index, row] of rows.entries()) {
|
|
12960
|
+
const parsed = parseFormBody(admin, row, { uploadsAsText: true });
|
|
12961
|
+
const failures = Object.entries(parsed.errors);
|
|
12962
|
+
if (failures.length > 0) {
|
|
12963
|
+
rowErrors.push({
|
|
12964
|
+
row: index + 2,
|
|
12965
|
+
message: failures.map(([field, error]) => `${field}: ${error}`).join("; ")
|
|
12966
|
+
});
|
|
12967
|
+
continue;
|
|
12968
|
+
}
|
|
12969
|
+
stampActor(admin, parsed.data, actorId, true);
|
|
12970
|
+
try {
|
|
12971
|
+
await repository.create(parsed.data);
|
|
12972
|
+
created += 1;
|
|
12973
|
+
} catch (error) {
|
|
12974
|
+
rowErrors.push({
|
|
12975
|
+
row: index + 2,
|
|
12976
|
+
message: error instanceof Error ? error.message : String(error)
|
|
12977
|
+
});
|
|
12978
|
+
}
|
|
12979
|
+
}
|
|
12980
|
+
html(res, renderImport(req, state, admin, null, created, rowErrors));
|
|
12981
|
+
})
|
|
12982
|
+
);
|
|
12983
|
+
router.get(
|
|
12984
|
+
`${prefix}/m/:slug/autocomplete/:field`,
|
|
12985
|
+
guarded(async (req, res) => {
|
|
12986
|
+
if (sessions.load(req) === null) {
|
|
12987
|
+
res.status(401).json({ options: [] });
|
|
12988
|
+
return;
|
|
12989
|
+
}
|
|
12990
|
+
const state = await authenticate(req, res);
|
|
12991
|
+
if (state === null) return;
|
|
12992
|
+
const admin = site.get(String(req.params.slug));
|
|
12993
|
+
const field = String(req.params.field);
|
|
12994
|
+
if (admin === null || !admin.autocompleteFields.includes(field) || !await allows(state.principal, admin, AdminPermission.VIEW)) {
|
|
12995
|
+
res.status(404).json({ options: [] });
|
|
12996
|
+
return;
|
|
12997
|
+
}
|
|
12998
|
+
const column6 = adminColumns(admin.model)[field];
|
|
12999
|
+
const table = column6 === void 0 ? null : foreignKeyTable(column6);
|
|
13000
|
+
const referenced = table === null ? null : site.get(table);
|
|
13001
|
+
if (referenced === null) {
|
|
13002
|
+
res.json({ options: [] });
|
|
13003
|
+
return;
|
|
13004
|
+
}
|
|
13005
|
+
const term = queryString(req.query.q);
|
|
13006
|
+
const searchable = referenced.searchFields.filter((name) => {
|
|
13007
|
+
const target = adminColumns(referenced.model)[name];
|
|
13008
|
+
return target !== void 0 && isSearchableColumn(target);
|
|
13009
|
+
});
|
|
13010
|
+
const filters = term === "" || searchable.length === 0 ? void 0 : or(...searchable.map((name) => ({ [name]: { ilike: `%${term}%` } })));
|
|
13011
|
+
const page2 = await referenced.repository(state.dbSession).paginate({
|
|
13012
|
+
page: 1,
|
|
13013
|
+
pageSize: AUTOCOMPLETE_LIMIT,
|
|
13014
|
+
...filters === void 0 ? {} : { filters }
|
|
13015
|
+
});
|
|
13016
|
+
res.json({
|
|
13017
|
+
options: page2.items.map((row) => ({
|
|
13018
|
+
value: String(row[referenced.identityField]),
|
|
13019
|
+
label: foreignKeyLabel(referenced, row)
|
|
13020
|
+
}))
|
|
13021
|
+
});
|
|
13022
|
+
})
|
|
13023
|
+
);
|
|
12474
13024
|
router.get(
|
|
12475
13025
|
`${prefix}/m/:slug/:identity`,
|
|
12476
13026
|
guarded(async (req, res) => {
|
|
12477
13027
|
const state = await authenticate(req, res);
|
|
12478
13028
|
if (state === null) return;
|
|
12479
|
-
const admin = resolveAdmin(req, res, state);
|
|
13029
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12480
13030
|
if (admin === null) return;
|
|
12481
13031
|
const row = await findRow(admin, state.dbSession, String(req.params.identity));
|
|
12482
13032
|
if (row === null) {
|
|
12483
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
13033
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12484
13034
|
return;
|
|
12485
13035
|
}
|
|
12486
13036
|
const identity = String(row[admin.identityField]);
|
|
12487
13037
|
html(
|
|
12488
13038
|
res,
|
|
12489
|
-
renderDetailPage(context(req, state.session), {
|
|
13039
|
+
renderDetailPage(context(req, state.session, state.visible), {
|
|
12490
13040
|
title: admin.verboseName(),
|
|
12491
13041
|
identity,
|
|
13042
|
+
audit: await buildAuditView(admin, row, state.dbSession),
|
|
12492
13043
|
fields: admin.detailFieldNames().map((name) => ({ label: name, value: formatCellValue(row[name]) })),
|
|
12493
13044
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
12494
|
-
editUrl: admin.
|
|
12495
|
-
deleteUrl: admin.
|
|
13045
|
+
editUrl: await allows(state.principal, admin, AdminPermission.EDIT) ? `${prefix}/m/${admin.slug()}/${identity}/edit` : null,
|
|
13046
|
+
deleteUrl: await allows(state.principal, admin, AdminPermission.DELETE) ? `${prefix}/m/${admin.slug()}/${identity}/delete` : null
|
|
12496
13047
|
})
|
|
12497
13048
|
);
|
|
12498
13049
|
})
|
|
@@ -12502,22 +13053,24 @@ function makeAdminRouter(site, options) {
|
|
|
12502
13053
|
guarded(async (req, res) => {
|
|
12503
13054
|
const state = await authenticate(req, res);
|
|
12504
13055
|
if (state === null) return;
|
|
12505
|
-
const admin = resolveAdmin(req, res, state);
|
|
13056
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.EDIT);
|
|
12506
13057
|
if (admin === null) return;
|
|
12507
13058
|
const identity = String(req.params.identity);
|
|
12508
13059
|
const row = admin.canEdit ? await findRow(admin, state.dbSession, identity) : null;
|
|
12509
13060
|
if (row === null) {
|
|
12510
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
13061
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12511
13062
|
return;
|
|
12512
13063
|
}
|
|
12513
13064
|
html(
|
|
12514
13065
|
res,
|
|
12515
|
-
renderFormPage(context(req, state.session), {
|
|
13066
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12516
13067
|
mode: "edit",
|
|
12517
13068
|
title: admin.verboseName(),
|
|
12518
13069
|
fields: buildFormFields(admin, {
|
|
12519
13070
|
values: row,
|
|
12520
|
-
foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession)
|
|
13071
|
+
foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession),
|
|
13072
|
+
autocompleteUrls: autocompleteUrlsFor(admin),
|
|
13073
|
+
autocompleteLabels: await autocompleteLabelsFor(admin, row, state.dbSession)
|
|
12521
13074
|
}),
|
|
12522
13075
|
actionUrl: `${prefix}/m/${admin.slug()}/${identity}/edit`,
|
|
12523
13076
|
backUrl: `${prefix}/m/${admin.slug()}/${identity}`,
|
|
@@ -12531,27 +13084,47 @@ function makeAdminRouter(site, options) {
|
|
|
12531
13084
|
guarded(async (req, res) => {
|
|
12532
13085
|
const state = await authenticate(req, res);
|
|
12533
13086
|
if (state === null) return;
|
|
12534
|
-
const admin = resolveAdmin(req, res, state);
|
|
13087
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.EDIT);
|
|
12535
13088
|
if (admin === null) return;
|
|
12536
13089
|
const identity = String(req.params.identity);
|
|
12537
13090
|
if (!admin.canEdit) {
|
|
12538
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
13091
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
13092
|
+
return;
|
|
13093
|
+
}
|
|
13094
|
+
let uploadError = null;
|
|
13095
|
+
let submission;
|
|
13096
|
+
try {
|
|
13097
|
+
submission = await readSubmission(req);
|
|
13098
|
+
} catch (error) {
|
|
13099
|
+
if (!(error instanceof MultipartLimitError)) throw error;
|
|
13100
|
+
submission = { fields: {}, files: [] };
|
|
13101
|
+
uploadError = error.message;
|
|
13102
|
+
}
|
|
13103
|
+
const body = submission.fields;
|
|
13104
|
+
if (uploadError === null && !csrfTokenMatches(state.session, body.csrf_token)) {
|
|
13105
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12539
13106
|
return;
|
|
12540
13107
|
}
|
|
12541
|
-
if (!checkCsrf(req, res, state)) return;
|
|
12542
|
-
const body = req.body;
|
|
12543
13108
|
const parsed = parseFormBody(admin, body);
|
|
13109
|
+
await applyUploads(admin, parsed.data, parsed.errors, submission.files, false);
|
|
12544
13110
|
const foreignKeyOptions = await foreignKeyOptionsFor(admin, state.dbSession);
|
|
13111
|
+
const autocompleteLabels = await autocompleteLabelsFor(
|
|
13112
|
+
admin,
|
|
13113
|
+
body,
|
|
13114
|
+
state.dbSession
|
|
13115
|
+
);
|
|
12545
13116
|
const rerender = (error, status) => {
|
|
12546
13117
|
html(
|
|
12547
13118
|
res,
|
|
12548
|
-
renderFormPage(context(req, state.session), {
|
|
13119
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12549
13120
|
mode: "edit",
|
|
12550
13121
|
title: admin.verboseName(),
|
|
12551
13122
|
fields: buildFormFields(admin, {
|
|
12552
13123
|
values: body,
|
|
12553
13124
|
errors: parsed.errors,
|
|
12554
|
-
foreignKeyOptions
|
|
13125
|
+
foreignKeyOptions,
|
|
13126
|
+
autocompleteUrls: autocompleteUrlsFor(admin),
|
|
13127
|
+
autocompleteLabels
|
|
12555
13128
|
}),
|
|
12556
13129
|
actionUrl: `${prefix}/m/${admin.slug()}/${identity}/edit`,
|
|
12557
13130
|
backUrl: `${prefix}/m/${admin.slug()}/${identity}`,
|
|
@@ -12560,14 +13133,24 @@ function makeAdminRouter(site, options) {
|
|
|
12560
13133
|
status
|
|
12561
13134
|
);
|
|
12562
13135
|
};
|
|
13136
|
+
if (uploadError !== null) {
|
|
13137
|
+
rerender(uploadError, 400);
|
|
13138
|
+
return;
|
|
13139
|
+
}
|
|
12563
13140
|
if (Object.keys(parsed.errors).length > 0) {
|
|
12564
13141
|
rerender("Please fix the highlighted fields.", 400);
|
|
12565
13142
|
return;
|
|
12566
13143
|
}
|
|
13144
|
+
stampActor(
|
|
13145
|
+
admin,
|
|
13146
|
+
parsed.data,
|
|
13147
|
+
backend.principalId(state.principal),
|
|
13148
|
+
false
|
|
13149
|
+
);
|
|
12567
13150
|
try {
|
|
12568
13151
|
const changed = await admin.repository(state.dbSession).update({ [admin.identityField]: identity }, parsed.data);
|
|
12569
13152
|
if (changed === 0) {
|
|
12570
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
13153
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12571
13154
|
return;
|
|
12572
13155
|
}
|
|
12573
13156
|
} catch (error) {
|
|
@@ -12582,10 +13165,10 @@ function makeAdminRouter(site, options) {
|
|
|
12582
13165
|
guarded(async (req, res) => {
|
|
12583
13166
|
const state = await authenticate(req, res);
|
|
12584
13167
|
if (state === null) return;
|
|
12585
|
-
const admin = resolveAdmin(req, res, state);
|
|
13168
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.DELETE);
|
|
12586
13169
|
if (admin === null) return;
|
|
12587
13170
|
if (!admin.canDelete) {
|
|
12588
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
13171
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12589
13172
|
return;
|
|
12590
13173
|
}
|
|
12591
13174
|
if (!checkCsrf(req, res, state)) return;
|
|
@@ -12593,10 +13176,96 @@ function makeAdminRouter(site, options) {
|
|
|
12593
13176
|
res.redirect(`${prefix}/m/${admin.slug()}?ok=deleted`);
|
|
12594
13177
|
})
|
|
12595
13178
|
);
|
|
13179
|
+
async function actorLabel(actor, dbSession) {
|
|
13180
|
+
if (actor === null || actor === void 0 || actor === "") return "";
|
|
13181
|
+
const principal = await backend.loadPrincipal(dbSession, String(actor));
|
|
13182
|
+
return principal === null ? String(actor) : backend.displayName(principal);
|
|
13183
|
+
}
|
|
13184
|
+
async function buildAuditView(admin, row, dbSession) {
|
|
13185
|
+
const fields = [];
|
|
13186
|
+
for (const name of admin.auditFieldNames()) {
|
|
13187
|
+
const value = name === "createdBy" || name === "updatedBy" ? await actorLabel(row[name], dbSession) : formatCellValue(row[name]);
|
|
13188
|
+
fields.push({ label: humanizeField(name), value });
|
|
13189
|
+
}
|
|
13190
|
+
const history = [];
|
|
13191
|
+
if (admin.auditModel !== null) {
|
|
13192
|
+
const entity = admin.model.name;
|
|
13193
|
+
const entityId = String(row[admin.identityField]);
|
|
13194
|
+
const page2 = await new BaseRepository(admin.auditModel, dbSession).paginate({
|
|
13195
|
+
page: 1,
|
|
13196
|
+
pageSize: AUDIT_HISTORY_LIMIT,
|
|
13197
|
+
orderBy: "createdAt",
|
|
13198
|
+
ascending: false,
|
|
13199
|
+
filters: { entity, entityId }
|
|
13200
|
+
});
|
|
13201
|
+
for (const entry of page2.items) {
|
|
13202
|
+
const changes = entry.changes ?? {};
|
|
13203
|
+
history.push({
|
|
13204
|
+
action: String(entry.action ?? ""),
|
|
13205
|
+
at: formatCellValue(entry.createdAt),
|
|
13206
|
+
actor: await actorLabel(entry.actor, dbSession) || "\u2014",
|
|
13207
|
+
changes: Object.entries(changes).map(([field, change]) => ({
|
|
13208
|
+
field,
|
|
13209
|
+
before: formatCellValue(change?.before),
|
|
13210
|
+
after: formatCellValue(change?.after)
|
|
13211
|
+
})),
|
|
13212
|
+
context: entry.context === null || entry.context === void 0 ? null : JSON.stringify(entry.context, null, 2)
|
|
13213
|
+
});
|
|
13214
|
+
}
|
|
13215
|
+
}
|
|
13216
|
+
if (fields.length === 0 && history.length === 0) return null;
|
|
13217
|
+
return { fields, history };
|
|
13218
|
+
}
|
|
12596
13219
|
async function findRow(admin, dbSession, identity) {
|
|
12597
13220
|
const row = await admin.repository(dbSession).first({ [admin.identityField]: identity });
|
|
12598
13221
|
return row ?? null;
|
|
12599
13222
|
}
|
|
13223
|
+
function renderImport(req, state, admin, error, created, rowErrors) {
|
|
13224
|
+
return renderImportPage(context(req, state.session, state.visible), {
|
|
13225
|
+
title: admin.verboseNamePlural(),
|
|
13226
|
+
actionUrl: `${prefix}/m/${admin.slug()}/import`,
|
|
13227
|
+
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
13228
|
+
columns: admin.editableFieldNames(),
|
|
13229
|
+
error,
|
|
13230
|
+
created,
|
|
13231
|
+
rowErrors
|
|
13232
|
+
});
|
|
13233
|
+
}
|
|
13234
|
+
async function readSubmission(req) {
|
|
13235
|
+
if (!isMultipart(req)) {
|
|
13236
|
+
return { fields: req.body ?? {}, files: [] };
|
|
13237
|
+
}
|
|
13238
|
+
const parsed = await parseMultipart(req, { maxFileBytes: maxUploadBytes });
|
|
13239
|
+
return { fields: parsed.fields, files: parsed.files };
|
|
13240
|
+
}
|
|
13241
|
+
async function applyUploads(admin, data, errors, files, creating) {
|
|
13242
|
+
if (admin.uploadFields.length === 0) return;
|
|
13243
|
+
const storage2 = admin.uploadStorage;
|
|
13244
|
+
if (storage2 === null) return;
|
|
13245
|
+
const columns = adminColumns(admin.model);
|
|
13246
|
+
for (const field of admin.uploadFields) {
|
|
13247
|
+
const file = files.find((candidate) => candidate.field === field);
|
|
13248
|
+
if (file === void 0) {
|
|
13249
|
+
const column6 = columns[field];
|
|
13250
|
+
if (creating && column6 !== void 0 && !isColumnOptional(column6)) {
|
|
13251
|
+
errors[field] = "This field is required.";
|
|
13252
|
+
}
|
|
13253
|
+
continue;
|
|
13254
|
+
}
|
|
13255
|
+
const extension = file.filename.includes(".") ? `.${file.filename.split(".").pop()}` : "";
|
|
13256
|
+
const key = `${admin.slug()}/${field}/${randomUUID()}${extension}`;
|
|
13257
|
+
const saved = await storage2.save(key, file.data, { contentType: file.contentType });
|
|
13258
|
+
data[field] = saved.key;
|
|
13259
|
+
}
|
|
13260
|
+
}
|
|
13261
|
+
async function permittedBulkActions(admin, principal) {
|
|
13262
|
+
const permitted = [];
|
|
13263
|
+
for (const option of bulkActionsFor(admin)) {
|
|
13264
|
+
const needed = option.value === "delete" ? AdminPermission.DELETE : AdminPermission.EDIT;
|
|
13265
|
+
if (await allows(principal, admin, needed)) permitted.push(option);
|
|
13266
|
+
}
|
|
13267
|
+
return permitted;
|
|
13268
|
+
}
|
|
12600
13269
|
async function renderList(req, admin, state) {
|
|
12601
13270
|
const columns = adminColumns(admin.model);
|
|
12602
13271
|
const query = await resolveListQuery(req, admin, state.dbSession);
|
|
@@ -12651,21 +13320,37 @@ function makeAdminRouter(site, options) {
|
|
|
12651
13320
|
searchValue: query.search,
|
|
12652
13321
|
filters: query.filterViews,
|
|
12653
13322
|
sort,
|
|
12654
|
-
newUrl: admin.
|
|
12655
|
-
|
|
13323
|
+
newUrl: await allows(state.principal, admin, AdminPermission.CREATE) ? `${prefix}/m/${admin.slug()}/new` : null,
|
|
13324
|
+
importUrl: admin.canImport && await allows(state.principal, admin, AdminPermission.CREATE) ? `${prefix}/m/${admin.slug()}/import` : null,
|
|
13325
|
+
bulkActions: await permittedBulkActions(admin, state.principal),
|
|
12656
13326
|
bulkUrl: `${prefix}/m/${admin.slug()}/bulk`,
|
|
12657
13327
|
exportCsvUrl: exportUrl("csv"),
|
|
12658
|
-
exportJsonUrl: exportUrl("json")
|
|
13328
|
+
exportJsonUrl: exportUrl("json"),
|
|
13329
|
+
lenses: admin.lenses.length === 0 ? [] : [
|
|
13330
|
+
{
|
|
13331
|
+
label: "All",
|
|
13332
|
+
url: `?${buildQuery({ ...query.baseQuery, lens: void 0 })}`,
|
|
13333
|
+
active: query.lens === ""
|
|
13334
|
+
},
|
|
13335
|
+
...admin.lenses.map((entry) => ({
|
|
13336
|
+
label: entry.label,
|
|
13337
|
+
url: `?${buildQuery({ ...query.baseQuery, lens: entry.slug })}`,
|
|
13338
|
+
active: query.lens === entry.slug
|
|
13339
|
+
}))
|
|
13340
|
+
]
|
|
12659
13341
|
};
|
|
12660
|
-
return renderListPage(context(req, state.session), view);
|
|
13342
|
+
return renderListPage(context(req, state.session, state.visible), view);
|
|
12661
13343
|
}
|
|
12662
13344
|
async function resolveListQuery(req, admin, dbSession) {
|
|
12663
13345
|
const columns = adminColumns(admin.model);
|
|
12664
13346
|
const search = queryString(req.query.q);
|
|
12665
13347
|
const sortField = queryString(req.query.sort);
|
|
12666
13348
|
const sortColumn = sortField in columns ? sortField : null;
|
|
12667
|
-
const
|
|
13349
|
+
const lens = admin.getLens(queryString(req.query.lens));
|
|
12668
13350
|
const conditions = [];
|
|
13351
|
+
if (lens !== null && Object.keys(lens.filters).length > 0) {
|
|
13352
|
+
conditions.push(lens.filters);
|
|
13353
|
+
}
|
|
12669
13354
|
const filterViews = [];
|
|
12670
13355
|
for (const field of admin.listFilter) {
|
|
12671
13356
|
const column6 = columns[field];
|
|
@@ -12729,15 +13414,21 @@ function makeAdminRouter(site, options) {
|
|
|
12729
13414
|
baseQuery[`filter_${view.field}`] = view.value;
|
|
12730
13415
|
}
|
|
12731
13416
|
}
|
|
13417
|
+
const lensOrder = lens?.orderBy ?? null;
|
|
13418
|
+
const lensDescending = lensOrder?.startsWith("-");
|
|
13419
|
+
const lensColumn = lensOrder === null ? null : lensOrder.replace(/^-/, "");
|
|
13420
|
+
const ascending = sortColumn !== null ? queryString(req.query.dir) !== "desc" : lensColumn !== null ? !lensDescending : admin.orderAscending;
|
|
13421
|
+
if (lens !== null) baseQuery.lens = lens.slug;
|
|
12732
13422
|
return {
|
|
12733
13423
|
search,
|
|
12734
13424
|
searchable,
|
|
12735
13425
|
where: conditions.length === 0 ? void 0 : and(...conditions),
|
|
12736
|
-
orderBy: sortColumn ?? admin.orderKey ?? void 0,
|
|
13426
|
+
orderBy: sortColumn ?? lensColumn ?? admin.orderKey ?? void 0,
|
|
12737
13427
|
ascending,
|
|
12738
13428
|
sortColumn,
|
|
12739
13429
|
filterViews,
|
|
12740
|
-
baseQuery
|
|
13430
|
+
baseQuery,
|
|
13431
|
+
lens: lens?.slug ?? ""
|
|
12741
13432
|
};
|
|
12742
13433
|
}
|
|
12743
13434
|
async function relatedOptions(column6, dbSession) {
|
|
@@ -12755,6 +13446,7 @@ function makeAdminRouter(site, options) {
|
|
|
12755
13446
|
const columns = adminColumns(admin.model);
|
|
12756
13447
|
const options2 = {};
|
|
12757
13448
|
for (const field of Object.keys(foreignKeyFields(admin))) {
|
|
13449
|
+
if (admin.autocompleteFields.includes(field)) continue;
|
|
12758
13450
|
const column6 = columns[field];
|
|
12759
13451
|
if (column6 === void 0) continue;
|
|
12760
13452
|
const related = await relatedOptions(column6, dbSession);
|
|
@@ -12762,8 +13454,101 @@ function makeAdminRouter(site, options) {
|
|
|
12762
13454
|
}
|
|
12763
13455
|
return options2;
|
|
12764
13456
|
}
|
|
13457
|
+
function autocompleteUrlsFor(admin) {
|
|
13458
|
+
const urls = {};
|
|
13459
|
+
for (const field of admin.autocompleteFields) {
|
|
13460
|
+
urls[field] = `${prefix}/m/${admin.slug()}/autocomplete/${field}`;
|
|
13461
|
+
}
|
|
13462
|
+
return urls;
|
|
13463
|
+
}
|
|
13464
|
+
async function autocompleteLabelsFor(admin, row, dbSession) {
|
|
13465
|
+
const labels = {};
|
|
13466
|
+
if (row === null) return labels;
|
|
13467
|
+
const columns = adminColumns(admin.model);
|
|
13468
|
+
for (const field of admin.autocompleteFields) {
|
|
13469
|
+
const value = row[field];
|
|
13470
|
+
if (value === null || value === void 0 || value === "") continue;
|
|
13471
|
+
const column6 = columns[field];
|
|
13472
|
+
const table = column6 === void 0 ? null : foreignKeyTable(column6);
|
|
13473
|
+
const referenced = table === null ? null : site.get(table);
|
|
13474
|
+
if (referenced === null) continue;
|
|
13475
|
+
const related = await referenced.repository(dbSession).first({ [referenced.identityField]: value });
|
|
13476
|
+
if (related !== null) labels[field] = foreignKeyLabel(referenced, related);
|
|
13477
|
+
}
|
|
13478
|
+
return labels;
|
|
13479
|
+
}
|
|
12765
13480
|
return router;
|
|
12766
13481
|
}
|
|
13482
|
+
function flagAllows(admin, action) {
|
|
13483
|
+
if (action === AdminPermission.CREATE) return admin.canCreate;
|
|
13484
|
+
if (action === AdminPermission.EDIT) return admin.canEdit;
|
|
13485
|
+
if (action === AdminPermission.DELETE) return admin.canDelete;
|
|
13486
|
+
return true;
|
|
13487
|
+
}
|
|
13488
|
+
function stampActor(admin, data, actorId, creating) {
|
|
13489
|
+
const columns = adminColumns(admin.model);
|
|
13490
|
+
if (creating && "createdBy" in columns) data.createdBy = actorId;
|
|
13491
|
+
if ("updatedBy" in columns) data.updatedBy = actorId;
|
|
13492
|
+
}
|
|
13493
|
+
function formatMetric(value) {
|
|
13494
|
+
if (typeof value === "string") return value;
|
|
13495
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
13496
|
+
}
|
|
13497
|
+
async function computeBusinessCard(card, dbSession) {
|
|
13498
|
+
const base = {
|
|
13499
|
+
label: card.label,
|
|
13500
|
+
unit: null,
|
|
13501
|
+
direction: "flat",
|
|
13502
|
+
percent: null,
|
|
13503
|
+
previous: "",
|
|
13504
|
+
segments: [],
|
|
13505
|
+
helpText: card.helpText ?? null
|
|
13506
|
+
};
|
|
13507
|
+
let data;
|
|
13508
|
+
try {
|
|
13509
|
+
data = await card.compute(dbSession);
|
|
13510
|
+
} catch (error) {
|
|
13511
|
+
logger2.warning("Admin dashboard card failed", {
|
|
13512
|
+
card: card.label,
|
|
13513
|
+
error: error instanceof Error ? error.message : String(error)
|
|
13514
|
+
});
|
|
13515
|
+
return { ...base, kind: "value", value: "", error: "Could not compute this metric." };
|
|
13516
|
+
}
|
|
13517
|
+
if (data.kind === "partition") {
|
|
13518
|
+
const total = partitionTotal(data);
|
|
13519
|
+
return {
|
|
13520
|
+
...base,
|
|
13521
|
+
kind: "partition",
|
|
13522
|
+
value: formatMetric(total),
|
|
13523
|
+
segments: data.segments.map((segment) => ({
|
|
13524
|
+
label: segment.label,
|
|
13525
|
+
value: formatMetric(segment.value),
|
|
13526
|
+
percent: total === 0 ? 0 : segment.value / total * 100
|
|
13527
|
+
})),
|
|
13528
|
+
error: null
|
|
13529
|
+
};
|
|
13530
|
+
}
|
|
13531
|
+
if (data.kind === "trend") {
|
|
13532
|
+
const percent = trendPercent(data);
|
|
13533
|
+
return {
|
|
13534
|
+
...base,
|
|
13535
|
+
kind: "trend",
|
|
13536
|
+
value: formatMetric(data.value),
|
|
13537
|
+
unit: data.unit ?? null,
|
|
13538
|
+
direction: trendDirection(data),
|
|
13539
|
+
percent: percent === null ? null : `${percent >= 0 ? "+" : ""}${percent.toFixed(1)}%`,
|
|
13540
|
+
previous: formatMetric(data.previous),
|
|
13541
|
+
error: null
|
|
13542
|
+
};
|
|
13543
|
+
}
|
|
13544
|
+
return {
|
|
13545
|
+
...base,
|
|
13546
|
+
kind: "value",
|
|
13547
|
+
value: formatMetric(data.value),
|
|
13548
|
+
unit: data.unit ?? null,
|
|
13549
|
+
error: null
|
|
13550
|
+
};
|
|
13551
|
+
}
|
|
12767
13552
|
function bulkActionsFor(admin) {
|
|
12768
13553
|
const actions = [];
|
|
12769
13554
|
const hasActiveFlag = "isActive" in adminColumns(admin.model);
|
|
@@ -12783,6 +13568,55 @@ function bulkActionsFor(admin) {
|
|
|
12783
13568
|
}
|
|
12784
13569
|
return actions;
|
|
12785
13570
|
}
|
|
13571
|
+
function parseCsv(text) {
|
|
13572
|
+
const source = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
13573
|
+
const rows = [];
|
|
13574
|
+
let row = [];
|
|
13575
|
+
let field = "";
|
|
13576
|
+
let quoted = false;
|
|
13577
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
13578
|
+
const char = source[index];
|
|
13579
|
+
if (quoted) {
|
|
13580
|
+
if (char === '"') {
|
|
13581
|
+
if (source[index + 1] === '"') {
|
|
13582
|
+
field += '"';
|
|
13583
|
+
index += 1;
|
|
13584
|
+
} else {
|
|
13585
|
+
quoted = false;
|
|
13586
|
+
}
|
|
13587
|
+
} else {
|
|
13588
|
+
field += char;
|
|
13589
|
+
}
|
|
13590
|
+
continue;
|
|
13591
|
+
}
|
|
13592
|
+
if (char === '"') {
|
|
13593
|
+
quoted = true;
|
|
13594
|
+
} else if (char === ",") {
|
|
13595
|
+
row.push(field);
|
|
13596
|
+
field = "";
|
|
13597
|
+
} else if (char === "\n" || char === "\r") {
|
|
13598
|
+
if (char === "\r" && source[index + 1] === "\n") index += 1;
|
|
13599
|
+
row.push(field);
|
|
13600
|
+
rows.push(row);
|
|
13601
|
+
row = [];
|
|
13602
|
+
field = "";
|
|
13603
|
+
} else {
|
|
13604
|
+
field += char;
|
|
13605
|
+
}
|
|
13606
|
+
}
|
|
13607
|
+
if (field !== "" || row.length > 0) {
|
|
13608
|
+
row.push(field);
|
|
13609
|
+
rows.push(row);
|
|
13610
|
+
}
|
|
13611
|
+
const header = rows.shift();
|
|
13612
|
+
if (header === void 0 || header.length === 0) {
|
|
13613
|
+
throw new Error("The file has no header row.");
|
|
13614
|
+
}
|
|
13615
|
+
const keys = header.map((name) => name.trim());
|
|
13616
|
+
return rows.filter((entry) => entry.some((value) => value.trim() !== "")).map(
|
|
13617
|
+
(entry) => Object.fromEntries(keys.map((key, position) => [key, entry[position] ?? ""]))
|
|
13618
|
+
);
|
|
13619
|
+
}
|
|
12786
13620
|
function exportValue(value) {
|
|
12787
13621
|
if (value instanceof Date) return value.toISOString();
|
|
12788
13622
|
if (typeof value === "bigint") return value.toString();
|
|
@@ -14815,6 +15649,6 @@ async function withTestDatabase(models, fn) {
|
|
|
14815
15649
|
}
|
|
14816
15650
|
}
|
|
14817
15651
|
|
|
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 };
|
|
15652
|
+
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, MultipartLimitError, 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, isMultipart, 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, parseCsv, parseFormBody, parseMultipart, partitionTotal, passwordResetConfirmSchema, passwordResetRequestSchema, phoneBrField, prometheusMiddleware, rabbitmqSettingsShape, rateLimitMiddleware, redisSettingsShape, refreshSchema, registerExceptionHandlers, renderAuthResultPage, renderDashboardPage, renderDetailPage, renderFormPage, renderImportPage, 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
15653
|
//# sourceMappingURL=index.js.map
|
|
14820
15654
|
//# sourceMappingURL=index.js.map
|