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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { z, looseBoolean, toDict, PasswordUtils } from './chunk-6KJTKSG5.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-6KJTKSG5.js';
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
- * @returns Every column but the password hash, in declaration order.
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((name) => !NEVER_LISTED.includes(name));
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 spec = widgetForColumn(column6);
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
- (row) => `<tr>${row.cells.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}<td><a href="${escapeHtml(row.url)}">View</a></td></tr>`
11849
- ).join("") : `<tr><td colspan="${view.columns.length + 1}">No records.</td></tr>`;
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: site.list().map((admin) => ({
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 message = FLASH_MESSAGES[queryString(req.query.ok)];
12011
- return message === void 0 ? [] : [message];
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
- return { session, dbSession };
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 site.list()) {
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.canCreate ? `${prefix}/m/${admin.slug()}/new` : null
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(res, renderDashboardPage(context(req, state.session), cards, metrics));
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, { values: body, errors: parsed.errors }),
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.canEdit ? `${prefix}/m/${admin.slug()}/${identity}/edit` : null,
12273
- deleteUrl: admin.canDelete ? `${prefix}/m/${admin.slug()}/${identity}/delete` : null
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, { values: row }),
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, { values: body, errors: parsed.errors }),
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 search = queryString(req.query.q);
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 ascending = sortColumn === null ? admin.orderAscending : queryString(req.query.dir) !== "desc";
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: spec.options.map((option) => ({
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 view2 of filterViews) {
12442
- if (view2.kind === "daterange") {
12443
- baseQuery[`filter_${view2.field}_from`] = view2.valueFrom;
12444
- baseQuery[`filter_${view2.field}_to`] = view2.valueTo;
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_${view2.field}`] = view2.value;
12992
+ baseQuery[`filter_${view.field}`] = view.value;
12447
12993
  }
12448
12994
  }
12449
- const sort = {};
12450
- for (const column6 of displayed) {
12451
- if (!(column6 in columns)) continue;
12452
- const active = (sortColumn ?? admin.orderKey) === column6;
12453
- const nextAscending = active ? !ascending : true;
12454
- sort[column6] = {
12455
- url: `?${buildQuery({
12456
- ...baseQuery,
12457
- sort: column6,
12458
- dir: nextAscending ? "asc" : "desc"
12459
- })}`,
12460
- active,
12461
- ascending
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
- return renderListPage(context(req, state.session), view);
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