tempest-express-sdk 0.26.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
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';
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';
@@ -9371,6 +9371,85 @@ function adminAction(options, handler) {
9371
9371
  };
9372
9372
  }
9373
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
+
9374
9453
  // src/admin/columns.ts
9375
9454
  function humanizeField(name) {
9376
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(" ");
@@ -9464,6 +9543,8 @@ var NEVER_EDITABLE = [
9464
9543
  "id",
9465
9544
  "createdAt",
9466
9545
  "updatedAt",
9546
+ "createdBy",
9547
+ "updatedBy",
9467
9548
  "hashedPassword"
9468
9549
  ];
9469
9550
  var NEVER_LISTED = ["hashedPassword"];
@@ -9500,6 +9581,14 @@ var AdminModel = class {
9500
9581
  auditModel;
9501
9582
  /** Saved list-view presets, in declaration order. */
9502
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;
9503
9592
  actions = /* @__PURE__ */ new Map();
9504
9593
  slugOverride;
9505
9594
  listDisplayOverride;
@@ -9527,6 +9616,15 @@ var AdminModel = class {
9527
9616
  this.canDelete = options.canDelete ?? true;
9528
9617
  this.auditModel = options.auditModel ?? null;
9529
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
+ }
9530
9628
  for (const action of options.actions ?? []) {
9531
9629
  if (this.actions.has(action.name)) {
9532
9630
  throw new Error(
@@ -9541,7 +9639,9 @@ var AdminModel = class {
9541
9639
  ["listFilter", this.listFilter],
9542
9640
  ["searchFields", this.searchFields],
9543
9641
  ["readonlyFields", this.readonlyFields],
9544
- ["identityField", [this.identityField]]
9642
+ ["identityField", [this.identityField]],
9643
+ ["uploadFields", this.uploadFields],
9644
+ ["autocompleteFields", this.autocompleteFields]
9545
9645
  ]) {
9546
9646
  for (const name of names) {
9547
9647
  if (!known.has(name)) {
@@ -9723,11 +9823,15 @@ function buildFormFields(admin, options = {}) {
9723
9823
  const values = options.values ?? {};
9724
9824
  const errors = options.errors ?? {};
9725
9825
  const foreignKeys = options.foreignKeyOptions ?? {};
9826
+ const autocompleteUrls = options.autocompleteUrls ?? {};
9827
+ const autocompleteLabels = options.autocompleteLabels ?? {};
9828
+ const uploads = new Set(admin.uploadFields);
9726
9829
  return admin.editableFieldNames().flatMap((name) => {
9727
9830
  const column6 = columns[name];
9728
9831
  if (column6 === void 0) return [];
9729
9832
  const related = foreignKeys[name];
9730
- const spec = related === void 0 ? widgetForColumn(column6) : { widget: "select", step: null, options: related };
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 };
9731
9835
  const raw = name in values ? values[name] : literalDefault(column6);
9732
9836
  return [
9733
9837
  {
@@ -9739,7 +9843,9 @@ function buildFormFields(admin, options = {}) {
9739
9843
  checked: spec.widget === "checkbox" && toBoolean(raw),
9740
9844
  step: spec.step,
9741
9845
  options: spec.options,
9742
- error: errors[name] ?? null
9846
+ error: errors[name] ?? null,
9847
+ autocompleteUrl: autocompleteUrl ?? null,
9848
+ displayLabel: autocompleteLabels[name] ?? ""
9743
9849
  }
9744
9850
  ];
9745
9851
  });
@@ -9787,13 +9893,15 @@ function coerceValue(column6, widget, raw) {
9787
9893
  return raw;
9788
9894
  }
9789
9895
  }
9790
- function parseFormBody(admin, body) {
9896
+ function parseFormBody(admin, body, options = {}) {
9791
9897
  const columns = adminColumns(admin.model);
9792
9898
  const data = {};
9793
9899
  const errors = {};
9900
+ const uploads = options.uploadsAsText === true ? /* @__PURE__ */ new Set() : new Set(admin.uploadFields);
9794
9901
  for (const name of admin.editableFieldNames()) {
9795
9902
  const column6 = columns[name];
9796
9903
  if (column6 === void 0) continue;
9904
+ if (uploads.has(name)) continue;
9797
9905
  const { widget } = widgetForColumn(column6);
9798
9906
  if (widget === "checkbox") {
9799
9907
  data[name] = toBoolean(body[name]);
@@ -12079,6 +12187,7 @@ function renderListPage(context, view) {
12079
12187
  </form>
12080
12188
  <div class="tempest-admin-list__actions">
12081
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>` : ""}
12082
12191
  <a href="${escapeHtml(view.exportCsvUrl)}">Export CSV</a>
12083
12192
  <a href="${escapeHtml(view.exportJsonUrl)}">Export JSON</a>
12084
12193
  </div>
@@ -12178,6 +12287,18 @@ function renderFormField(field) {
12178
12287
  control = `<label><span>${label}${field.required ? " *" : ""}</span><select name="${name}"${required}>${blank}${options}</select></label>`;
12179
12288
  break;
12180
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;
12181
12302
  case "number":
12182
12303
  control = `<label><span>${label}${field.required ? " *" : ""}</span><input type="number" name="${name}" value="${value}"${field.step !== null ? ` step="${escapeHtml(field.step)}"` : ""}${required}></label>`;
12183
12304
  break;
@@ -12205,7 +12326,7 @@ function renderFormPage(context, view) {
12205
12326
  <a href="${escapeHtml(view.backUrl)}">\u2190 Back</a>
12206
12327
  </header>
12207
12328
  ${view.error !== null ? `<p class="tempest-admin-form__error">${escapeHtml(view.error)}</p>` : ""}
12208
- <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"' : ""}>
12209
12330
  <input type="hidden" name="csrf_token" value="${escapeHtml(context.session.csrfToken)}">
12210
12331
  ${view.fields.map(renderFormField).join("")}
12211
12332
  <div class="tempest-admin-form__actions">
@@ -12213,10 +12334,97 @@ function renderFormPage(context, view) {
12213
12334
  <a href="${escapeHtml(view.backUrl)}" class="tempest-admin-form__cancel">Cancel</a>
12214
12335
  </div>
12215
12336
  </form>
12337
+ ${view.fields.some((field) => field.widget === "autocomplete") ? AUTOCOMPLETE_SCRIPT : ""}
12216
12338
  </section>`;
12217
12339
  return renderLayout(context, `${heading} \xB7 ${context.site.title}`, body);
12218
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>`;
12219
12426
  var logger2 = new JSONLogger("tempest_express_sdk.admin.router");
12427
+ var AUTOCOMPLETE_LIMIT = 20;
12220
12428
  var AUDIT_HISTORY_LIMIT = 50;
12221
12429
  var FK_OPTION_CAP = 1e3;
12222
12430
  var FLASH_MAX_LENGTH = 300;
@@ -12246,6 +12454,7 @@ function makeAdminRouter(site, options) {
12246
12454
  const theme = resolveAdminTheme(site.theme);
12247
12455
  const showMetrics = options.showMetrics ?? true;
12248
12456
  const exportMaxRows = options.exportMaxRows ?? 5e3;
12457
+ const maxUploadBytes = options.maxUploadBytes ?? 10 * 1024 * 1024;
12249
12458
  const sessions = new AdminSessionStore({
12250
12459
  secret: options.secretKey,
12251
12460
  ...options.cookieName === void 0 ? {} : { cookieName: options.cookieName },
@@ -12503,7 +12712,8 @@ function makeAdminRouter(site, options) {
12503
12712
  mode: "create",
12504
12713
  title: admin.verboseName(),
12505
12714
  fields: buildFormFields(admin, {
12506
- foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession)
12715
+ foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession),
12716
+ autocompleteUrls: autocompleteUrlsFor(admin)
12507
12717
  }),
12508
12718
  actionUrl: `${prefix}/m/${admin.slug()}/new`,
12509
12719
  backUrl: `${prefix}/m/${admin.slug()}`,
@@ -12523,10 +12733,28 @@ function makeAdminRouter(site, options) {
12523
12733
  html(res, renderNotFound(context(req, state.session, state.visible)), 404);
12524
12734
  return;
12525
12735
  }
12526
- if (!checkCsrf(req, res, state)) return;
12527
- const body = req.body;
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);
12748
+ return;
12749
+ }
12528
12750
  const parsed = parseFormBody(admin, body);
12751
+ await applyUploads(admin, parsed.data, parsed.errors, submission.files, true);
12529
12752
  const foreignKeyOptions = await foreignKeyOptionsFor(admin, state.dbSession);
12753
+ const autocompleteLabels = await autocompleteLabelsFor(
12754
+ admin,
12755
+ body,
12756
+ state.dbSession
12757
+ );
12530
12758
  const rerender = (error, status) => {
12531
12759
  html(
12532
12760
  res,
@@ -12536,7 +12764,9 @@ function makeAdminRouter(site, options) {
12536
12764
  fields: buildFormFields(admin, {
12537
12765
  values: body,
12538
12766
  errors: parsed.errors,
12539
- foreignKeyOptions
12767
+ foreignKeyOptions,
12768
+ autocompleteUrls: autocompleteUrlsFor(admin),
12769
+ autocompleteLabels
12540
12770
  }),
12541
12771
  actionUrl: `${prefix}/m/${admin.slug()}/new`,
12542
12772
  backUrl: `${prefix}/m/${admin.slug()}`,
@@ -12545,6 +12775,10 @@ function makeAdminRouter(site, options) {
12545
12775
  status
12546
12776
  );
12547
12777
  };
12778
+ if (uploadError !== null) {
12779
+ rerender(uploadError, 400);
12780
+ return;
12781
+ }
12548
12782
  if (Object.keys(parsed.errors).length > 0) {
12549
12783
  rerender("Please fix the highlighted fields.", 400);
12550
12784
  return;
@@ -12664,6 +12898,129 @@ function makeAdminRouter(site, options) {
12664
12898
  );
12665
12899
  })
12666
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
+ );
12667
13024
  router.get(
12668
13025
  `${prefix}/m/:slug/:identity`,
12669
13026
  guarded(async (req, res) => {
@@ -12711,7 +13068,9 @@ function makeAdminRouter(site, options) {
12711
13068
  title: admin.verboseName(),
12712
13069
  fields: buildFormFields(admin, {
12713
13070
  values: row,
12714
- 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)
12715
13074
  }),
12716
13075
  actionUrl: `${prefix}/m/${admin.slug()}/${identity}/edit`,
12717
13076
  backUrl: `${prefix}/m/${admin.slug()}/${identity}`,
@@ -12732,10 +13091,28 @@ function makeAdminRouter(site, options) {
12732
13091
  html(res, renderNotFound(context(req, state.session, state.visible)), 404);
12733
13092
  return;
12734
13093
  }
12735
- if (!checkCsrf(req, res, state)) return;
12736
- const body = req.body;
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);
13106
+ return;
13107
+ }
12737
13108
  const parsed = parseFormBody(admin, body);
13109
+ await applyUploads(admin, parsed.data, parsed.errors, submission.files, false);
12738
13110
  const foreignKeyOptions = await foreignKeyOptionsFor(admin, state.dbSession);
13111
+ const autocompleteLabels = await autocompleteLabelsFor(
13112
+ admin,
13113
+ body,
13114
+ state.dbSession
13115
+ );
12739
13116
  const rerender = (error, status) => {
12740
13117
  html(
12741
13118
  res,
@@ -12745,7 +13122,9 @@ function makeAdminRouter(site, options) {
12745
13122
  fields: buildFormFields(admin, {
12746
13123
  values: body,
12747
13124
  errors: parsed.errors,
12748
- foreignKeyOptions
13125
+ foreignKeyOptions,
13126
+ autocompleteUrls: autocompleteUrlsFor(admin),
13127
+ autocompleteLabels
12749
13128
  }),
12750
13129
  actionUrl: `${prefix}/m/${admin.slug()}/${identity}/edit`,
12751
13130
  backUrl: `${prefix}/m/${admin.slug()}/${identity}`,
@@ -12754,6 +13133,10 @@ function makeAdminRouter(site, options) {
12754
13133
  status
12755
13134
  );
12756
13135
  };
13136
+ if (uploadError !== null) {
13137
+ rerender(uploadError, 400);
13138
+ return;
13139
+ }
12757
13140
  if (Object.keys(parsed.errors).length > 0) {
12758
13141
  rerender("Please fix the highlighted fields.", 400);
12759
13142
  return;
@@ -12837,6 +13220,44 @@ function makeAdminRouter(site, options) {
12837
13220
  const row = await admin.repository(dbSession).first({ [admin.identityField]: identity });
12838
13221
  return row ?? null;
12839
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
+ }
12840
13261
  async function permittedBulkActions(admin, principal) {
12841
13262
  const permitted = [];
12842
13263
  for (const option of bulkActionsFor(admin)) {
@@ -12900,6 +13321,7 @@ function makeAdminRouter(site, options) {
12900
13321
  filters: query.filterViews,
12901
13322
  sort,
12902
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,
12903
13325
  bulkActions: await permittedBulkActions(admin, state.principal),
12904
13326
  bulkUrl: `${prefix}/m/${admin.slug()}/bulk`,
12905
13327
  exportCsvUrl: exportUrl("csv"),
@@ -13024,6 +13446,7 @@ function makeAdminRouter(site, options) {
13024
13446
  const columns = adminColumns(admin.model);
13025
13447
  const options2 = {};
13026
13448
  for (const field of Object.keys(foreignKeyFields(admin))) {
13449
+ if (admin.autocompleteFields.includes(field)) continue;
13027
13450
  const column6 = columns[field];
13028
13451
  if (column6 === void 0) continue;
13029
13452
  const related = await relatedOptions(column6, dbSession);
@@ -13031,6 +13454,29 @@ function makeAdminRouter(site, options) {
13031
13454
  }
13032
13455
  return options2;
13033
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
+ }
13034
13480
  return router;
13035
13481
  }
13036
13482
  function flagAllows(admin, action) {
@@ -13122,6 +13568,55 @@ function bulkActionsFor(admin) {
13122
13568
  }
13123
13569
  return actions;
13124
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
+ }
13125
13620
  function exportValue(value) {
13126
13621
  if (value instanceof Date) return value.toISOString();
13127
13622
  if (typeof value === "bigint") return value.toString();
@@ -15154,6 +15649,6 @@ async function withTestDatabase(models, fn) {
15154
15649
  }
15155
15650
  }
15156
15651
 
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 };
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 };
15158
15653
  //# sourceMappingURL=index.js.map
15159
15654
  //# sourceMappingURL=index.js.map