tempest-express-sdk 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{chunk-6KJTKSG5.js → chunk-TIW2KPT2.js} +3 -3
- package/dist/{chunk-6KJTKSG5.js.map → chunk-TIW2KPT2.js.map} +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +398 -75
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +281 -119
- package/dist/index.d.ts +281 -119
- package/dist/index.js +396 -77
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -9444,6 +9444,21 @@ var MessagingHub = class {
|
|
|
9444
9444
|
}
|
|
9445
9445
|
};
|
|
9446
9446
|
|
|
9447
|
+
// src/admin/actions.ts
|
|
9448
|
+
function slugify(label) {
|
|
9449
|
+
return label.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "action";
|
|
9450
|
+
}
|
|
9451
|
+
function adminAction(options, handler) {
|
|
9452
|
+
const name = options.name ?? slugify(options.label);
|
|
9453
|
+
if (name === "") throw new Error("adminAction requires a non-empty name or label");
|
|
9454
|
+
return {
|
|
9455
|
+
name,
|
|
9456
|
+
label: options.label,
|
|
9457
|
+
handler,
|
|
9458
|
+
dangerous: options.dangerous ?? false
|
|
9459
|
+
};
|
|
9460
|
+
}
|
|
9461
|
+
|
|
9447
9462
|
// src/admin/columns.ts
|
|
9448
9463
|
function humanizeField(name) {
|
|
9449
9464
|
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(" ");
|
|
@@ -9524,6 +9539,9 @@ function filterForColumn(column6) {
|
|
|
9524
9539
|
}
|
|
9525
9540
|
return { kind: "text", options: [] };
|
|
9526
9541
|
}
|
|
9542
|
+
function foreignKeyTable(column6) {
|
|
9543
|
+
return column6.reference?.table ?? null;
|
|
9544
|
+
}
|
|
9527
9545
|
function isSearchableColumn(column6) {
|
|
9528
9546
|
const { kind } = column6.type;
|
|
9529
9547
|
return kind === "varchar" || kind === "text" || kind === "char";
|
|
@@ -9560,6 +9578,7 @@ var AdminModel = class {
|
|
|
9560
9578
|
canEdit;
|
|
9561
9579
|
/** Whether the delete action is exposed. */
|
|
9562
9580
|
canDelete;
|
|
9581
|
+
actions = /* @__PURE__ */ new Map();
|
|
9563
9582
|
slugOverride;
|
|
9564
9583
|
listDisplayOverride;
|
|
9565
9584
|
verboseNameOverride;
|
|
@@ -9584,6 +9603,14 @@ var AdminModel = class {
|
|
|
9584
9603
|
this.canCreate = options.canCreate ?? true;
|
|
9585
9604
|
this.canEdit = options.canEdit ?? true;
|
|
9586
9605
|
this.canDelete = options.canDelete ?? true;
|
|
9606
|
+
for (const action of options.actions ?? []) {
|
|
9607
|
+
if (this.actions.has(action.name)) {
|
|
9608
|
+
throw new Error(
|
|
9609
|
+
`Duplicate admin action name "${action.name}" on ${this.model.tablename}`
|
|
9610
|
+
);
|
|
9611
|
+
}
|
|
9612
|
+
this.actions.set(action.name, action);
|
|
9613
|
+
}
|
|
9587
9614
|
const known = new Set(this.columnNames());
|
|
9588
9615
|
for (const [option, names] of [
|
|
9589
9616
|
["listDisplay", this.listDisplayOverride ?? []],
|
|
@@ -9686,6 +9713,25 @@ var AdminModel = class {
|
|
|
9686
9713
|
const skip = /* @__PURE__ */ new Set([...this.readonlyFields, ...NEVER_EDITABLE]);
|
|
9687
9714
|
return this.columnNames().filter((name) => !skip.has(name));
|
|
9688
9715
|
}
|
|
9716
|
+
/**
|
|
9717
|
+
* Return the registered custom actions, in declaration order.
|
|
9718
|
+
*
|
|
9719
|
+
* @returns The actions passed via `actions` (empty when none). The model
|
|
9720
|
+
* type is erased here, the way {@link AdminSite} erases it when it stores a
|
|
9721
|
+
* configuration — a registry keyed by slug cannot stay generic.
|
|
9722
|
+
*/
|
|
9723
|
+
customActions() {
|
|
9724
|
+
return [...this.actions.values()];
|
|
9725
|
+
}
|
|
9726
|
+
/**
|
|
9727
|
+
* Look a custom action up by name.
|
|
9728
|
+
*
|
|
9729
|
+
* @param name - The action identifier (its submitted form value).
|
|
9730
|
+
* @returns The action, or `null` when nothing matches.
|
|
9731
|
+
*/
|
|
9732
|
+
getAction(name) {
|
|
9733
|
+
return this.actions.get(name) ?? null;
|
|
9734
|
+
}
|
|
9689
9735
|
/**
|
|
9690
9736
|
* Build a repository for this model bound to a session.
|
|
9691
9737
|
*
|
|
@@ -9727,10 +9773,12 @@ function buildFormFields(admin, options = {}) {
|
|
|
9727
9773
|
const columns = adminColumns(admin.model);
|
|
9728
9774
|
const values = options.values ?? {};
|
|
9729
9775
|
const errors = options.errors ?? {};
|
|
9776
|
+
const foreignKeys = options.foreignKeyOptions ?? {};
|
|
9730
9777
|
return admin.editableFieldNames().flatMap((name) => {
|
|
9731
9778
|
const column6 = columns[name];
|
|
9732
9779
|
if (column6 === void 0) return [];
|
|
9733
|
-
const
|
|
9780
|
+
const related = foreignKeys[name];
|
|
9781
|
+
const spec = related === void 0 ? widgetForColumn(column6) : { widget: "select", step: null, options: related };
|
|
9734
9782
|
const raw = name in values ? values[name] : literalDefault(column6);
|
|
9735
9783
|
return [
|
|
9736
9784
|
{
|
|
@@ -9780,7 +9828,7 @@ function coerceValue(column6, widget, raw) {
|
|
|
9780
9828
|
throw new Error("Enter valid JSON.");
|
|
9781
9829
|
}
|
|
9782
9830
|
case "select": {
|
|
9783
|
-
const allowed = meta.values ?? [];
|
|
9831
|
+
const allowed = kind === "enum" ? meta.values ?? [] : [];
|
|
9784
9832
|
if (allowed.length > 0 && !allowed.includes(raw)) {
|
|
9785
9833
|
throw new Error(`Choose one of: ${allowed.join(", ")}.`);
|
|
9786
9834
|
}
|
|
@@ -9828,6 +9876,28 @@ function formatCellValue(value) {
|
|
|
9828
9876
|
if (typeof value === "object") return JSON.stringify(value);
|
|
9829
9877
|
return String(value);
|
|
9830
9878
|
}
|
|
9879
|
+
function foreignKeyFields(admin) {
|
|
9880
|
+
const columns = adminColumns(admin.model);
|
|
9881
|
+
const out = {};
|
|
9882
|
+
for (const name of admin.editableFieldNames()) {
|
|
9883
|
+
const column6 = columns[name];
|
|
9884
|
+
if (column6 === void 0) continue;
|
|
9885
|
+
const table = foreignKeyTable(column6);
|
|
9886
|
+
if (table !== null) out[name] = table;
|
|
9887
|
+
}
|
|
9888
|
+
return out;
|
|
9889
|
+
}
|
|
9890
|
+
function foreignKeyLabel(admin, row) {
|
|
9891
|
+
for (const field of admin.searchFields) {
|
|
9892
|
+
const value = row[field];
|
|
9893
|
+
if (typeof value === "string" && value !== "") return value;
|
|
9894
|
+
}
|
|
9895
|
+
for (const field of ["name", "title", "email", "label", "reference"]) {
|
|
9896
|
+
const value = row[field];
|
|
9897
|
+
if (typeof value === "string" && value !== "") return value;
|
|
9898
|
+
}
|
|
9899
|
+
return String(row[admin.identityField] ?? "");
|
|
9900
|
+
}
|
|
9831
9901
|
|
|
9832
9902
|
// src/admin/auth.ts
|
|
9833
9903
|
var UserModelAuthBackend = class {
|
|
@@ -11965,15 +12035,40 @@ function renderFilter(filter) {
|
|
|
11965
12035
|
return `<label><span>${escapeHtml(filter.label)}</span><input type="text" name="${escapeHtml(name)}" value="${escapeHtml(filter.value)}"></label>`;
|
|
11966
12036
|
}
|
|
11967
12037
|
function renderListPage(context, view) {
|
|
12038
|
+
const bulk = view.bulkActions.length > 0 && context.session !== null;
|
|
12039
|
+
const checkColumn = bulk ? 1 : 0;
|
|
11968
12040
|
const headers = view.columns.map((column6) => {
|
|
11969
12041
|
const state = view.sort[column6];
|
|
11970
12042
|
if (state === void 0) return `<th>${escapeHtml(column6)}</th>`;
|
|
11971
12043
|
const arrow = state.active ? state.ascending ? "\u25B2" : "\u25BC" : "\u2195";
|
|
11972
12044
|
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>`;
|
|
11973
12045
|
}).join("");
|
|
11974
|
-
const rows = view.rows.length > 0 ? view.rows.map(
|
|
11975
|
-
|
|
11976
|
-
|
|
12046
|
+
const rows = view.rows.length > 0 ? view.rows.map((row) => {
|
|
12047
|
+
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>` : "";
|
|
12048
|
+
const cells = row.cells.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("");
|
|
12049
|
+
return `<tr>${check}${cells}<td><a href="${escapeHtml(row.url)}">View</a></td></tr>`;
|
|
12050
|
+
}).join("") : `<tr><td colspan="${view.columns.length + 1 + checkColumn}">No records.</td></tr>`;
|
|
12051
|
+
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?');">
|
|
12052
|
+
<input type="hidden" name="csrf_token" value="${escapeHtml(context.session?.csrfToken)}">
|
|
12053
|
+
<div class="tempest-admin-bulk__bar">
|
|
12054
|
+
<select name="action" aria-label="Bulk action">
|
|
12055
|
+
${view.bulkActions.map(
|
|
12056
|
+
(action) => `<option value="${escapeHtml(action.value)}">${escapeHtml(action.label)}${action.dangerous ? " \u26A0" : ""}</option>`
|
|
12057
|
+
).join("")}
|
|
12058
|
+
</select>
|
|
12059
|
+
<button type="submit">Apply to selected</button>
|
|
12060
|
+
</div>` : "";
|
|
12061
|
+
const selectAllScript = bulk ? `<script>
|
|
12062
|
+
(function () {
|
|
12063
|
+
var master = document.querySelector('[data-select-all]');
|
|
12064
|
+
if (!master) return;
|
|
12065
|
+
master.addEventListener('change', function () {
|
|
12066
|
+
document.querySelectorAll('[data-row-check]').forEach(function (box) {
|
|
12067
|
+
box.checked = master.checked;
|
|
12068
|
+
});
|
|
12069
|
+
});
|
|
12070
|
+
})();
|
|
12071
|
+
</script>` : "";
|
|
11977
12072
|
const hasControls = view.searchable || view.filters.length > 0;
|
|
11978
12073
|
const body = `<section class="tempest-admin-list">
|
|
11979
12074
|
<header class="tempest-admin-list__header">
|
|
@@ -11988,14 +12083,19 @@ function renderListPage(context, view) {
|
|
|
11988
12083
|
</form>
|
|
11989
12084
|
<div class="tempest-admin-list__actions">
|
|
11990
12085
|
${view.newUrl !== null ? `<a class="tempest-admin-list__new" href="${escapeHtml(view.newUrl)}">+ New</a>` : ""}
|
|
12086
|
+
<a href="${escapeHtml(view.exportCsvUrl)}">Export CSV</a>
|
|
12087
|
+
<a href="${escapeHtml(view.exportJsonUrl)}">Export JSON</a>
|
|
11991
12088
|
</div>
|
|
11992
12089
|
</div>
|
|
12090
|
+
${bulkBar}
|
|
11993
12091
|
<div class="tempest-admin-table-wrap">
|
|
11994
12092
|
<table class="tempest-admin-list__table">
|
|
11995
|
-
<thead><tr>${headers}<th>Actions</th></tr></thead>
|
|
12093
|
+
<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>
|
|
11996
12094
|
<tbody>${rows}</tbody>
|
|
11997
12095
|
</table>
|
|
11998
12096
|
</div>
|
|
12097
|
+
${bulk ? "</form>" : ""}
|
|
12098
|
+
${selectAllScript}
|
|
11999
12099
|
${view.pages > 1 ? `<nav class="tempest-admin-list__pagination" aria-label="Pagination">
|
|
12000
12100
|
${view.prevUrl !== null ? `<a href="${escapeHtml(view.prevUrl)}">\u2190 Prev</a>` : ""}
|
|
12001
12101
|
<span>Page ${escapeHtml(view.page)} of ${escapeHtml(view.pages)}</span>
|
|
@@ -12089,11 +12189,16 @@ function renderFormPage(context, view) {
|
|
|
12089
12189
|
return renderLayout(context, `${heading} \xB7 ${context.site.title}`, body);
|
|
12090
12190
|
}
|
|
12091
12191
|
var logger2 = new JSONLogger("tempest_express_sdk.admin.router");
|
|
12192
|
+
var FK_OPTION_CAP = 1e3;
|
|
12193
|
+
var FLASH_MAX_LENGTH = 300;
|
|
12092
12194
|
var FLASH_MESSAGES = {
|
|
12093
12195
|
created: { text: "Record created.", level: "success" },
|
|
12094
12196
|
updated: { text: "Record updated.", level: "success" },
|
|
12095
12197
|
deleted: { text: "Record deleted.", level: "success" }
|
|
12096
12198
|
};
|
|
12199
|
+
async function runCustomAction(action, context) {
|
|
12200
|
+
return await action.handler(context) ?? null;
|
|
12201
|
+
}
|
|
12097
12202
|
function queryString(value) {
|
|
12098
12203
|
if (typeof value === "string") return value.trim();
|
|
12099
12204
|
if (Array.isArray(value) && typeof value[0] === "string") return value[0].trim();
|
|
@@ -12111,6 +12216,7 @@ function makeAdminRouter(site, options) {
|
|
|
12111
12216
|
const prefix = (options.prefix ?? "/admin").replace(/\/$/, "");
|
|
12112
12217
|
const theme = resolveAdminTheme(site.theme);
|
|
12113
12218
|
const showMetrics = options.showMetrics ?? true;
|
|
12219
|
+
const exportMaxRows = options.exportMaxRows ?? 5e3;
|
|
12114
12220
|
const sessions = new AdminSessionStore({
|
|
12115
12221
|
secret: options.secretKey,
|
|
12116
12222
|
...options.cookieName === void 0 ? {} : { cookieName: options.cookieName },
|
|
@@ -12134,8 +12240,17 @@ function makeAdminRouter(site, options) {
|
|
|
12134
12240
|
messages: flashFor(req)
|
|
12135
12241
|
});
|
|
12136
12242
|
const flashFor = (req) => {
|
|
12137
|
-
const
|
|
12138
|
-
|
|
12243
|
+
const fixed = FLASH_MESSAGES[queryString(req.query.ok)];
|
|
12244
|
+
if (fixed !== void 0) return [fixed];
|
|
12245
|
+
const text = queryString(req.query.flash);
|
|
12246
|
+
if (text === "") return [];
|
|
12247
|
+
const level = queryString(req.query.level);
|
|
12248
|
+
return [
|
|
12249
|
+
{
|
|
12250
|
+
text: text.slice(0, FLASH_MAX_LENGTH),
|
|
12251
|
+
level: level === "error" || level === "warning" ? level : "success"
|
|
12252
|
+
}
|
|
12253
|
+
];
|
|
12139
12254
|
};
|
|
12140
12255
|
const html = (res, body, status = 200) => {
|
|
12141
12256
|
res.status(status).type("html").send(body);
|
|
@@ -12171,7 +12286,7 @@ function makeAdminRouter(site, options) {
|
|
|
12171
12286
|
res.redirect(`${prefix}/mfa`);
|
|
12172
12287
|
return null;
|
|
12173
12288
|
}
|
|
12174
|
-
return { session, dbSession };
|
|
12289
|
+
return { session, dbSession, principal };
|
|
12175
12290
|
};
|
|
12176
12291
|
const resolveAdmin = (req, res, state) => {
|
|
12177
12292
|
const admin = site.get(String(req.params.slug));
|
|
@@ -12327,7 +12442,9 @@ function makeAdminRouter(site, options) {
|
|
|
12327
12442
|
renderFormPage(context(req, state.session), {
|
|
12328
12443
|
mode: "create",
|
|
12329
12444
|
title: admin.verboseName(),
|
|
12330
|
-
fields: buildFormFields(admin
|
|
12445
|
+
fields: buildFormFields(admin, {
|
|
12446
|
+
foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession)
|
|
12447
|
+
}),
|
|
12331
12448
|
actionUrl: `${prefix}/m/${admin.slug()}/new`,
|
|
12332
12449
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
12333
12450
|
error: null
|
|
@@ -12349,13 +12466,18 @@ function makeAdminRouter(site, options) {
|
|
|
12349
12466
|
if (!checkCsrf(req, res, state)) return;
|
|
12350
12467
|
const body = req.body;
|
|
12351
12468
|
const parsed = parseFormBody(admin, body);
|
|
12469
|
+
const foreignKeyOptions = await foreignKeyOptionsFor(admin, state.dbSession);
|
|
12352
12470
|
const rerender = (error, status) => {
|
|
12353
12471
|
html(
|
|
12354
12472
|
res,
|
|
12355
12473
|
renderFormPage(context(req, state.session), {
|
|
12356
12474
|
mode: "create",
|
|
12357
12475
|
title: admin.verboseName(),
|
|
12358
|
-
fields: buildFormFields(admin, {
|
|
12476
|
+
fields: buildFormFields(admin, {
|
|
12477
|
+
values: body,
|
|
12478
|
+
errors: parsed.errors,
|
|
12479
|
+
foreignKeyOptions
|
|
12480
|
+
}),
|
|
12359
12481
|
actionUrl: `${prefix}/m/${admin.slug()}/new`,
|
|
12360
12482
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
12361
12483
|
error
|
|
@@ -12376,6 +12498,106 @@ function makeAdminRouter(site, options) {
|
|
|
12376
12498
|
res.redirect(`${prefix}/m/${admin.slug()}?ok=created`);
|
|
12377
12499
|
})
|
|
12378
12500
|
);
|
|
12501
|
+
router.get(
|
|
12502
|
+
`${prefix}/m/:slug/export.:format`,
|
|
12503
|
+
guarded(async (req, res) => {
|
|
12504
|
+
const state = await authenticate(req, res);
|
|
12505
|
+
if (state === null) return;
|
|
12506
|
+
const admin = resolveAdmin(req, res, state);
|
|
12507
|
+
if (admin === null) return;
|
|
12508
|
+
const format = String(req.params.format);
|
|
12509
|
+
if (format !== "csv" && format !== "json") {
|
|
12510
|
+
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12511
|
+
return;
|
|
12512
|
+
}
|
|
12513
|
+
const query = await resolveListQuery(req, admin, state.dbSession);
|
|
12514
|
+
const result = await admin.repository(state.dbSession).paginate({
|
|
12515
|
+
page: 1,
|
|
12516
|
+
pageSize: exportMaxRows,
|
|
12517
|
+
...query.orderBy === void 0 ? {} : { orderBy: query.orderBy },
|
|
12518
|
+
ascending: query.ascending,
|
|
12519
|
+
...query.where === void 0 ? {} : { filters: query.where }
|
|
12520
|
+
});
|
|
12521
|
+
const columns = admin.listDisplayNames();
|
|
12522
|
+
const rows = result.items;
|
|
12523
|
+
const payload = format === "csv" ? toCsv(columns, rows) : toJson(columns, rows);
|
|
12524
|
+
res.status(200).type(format === "csv" ? "text/csv; charset=utf-8" : "application/json").set("content-disposition", `attachment; filename="${admin.slug()}.${format}"`).send(payload);
|
|
12525
|
+
})
|
|
12526
|
+
);
|
|
12527
|
+
router.post(
|
|
12528
|
+
`${prefix}/m/:slug/bulk`,
|
|
12529
|
+
guarded(async (req, res) => {
|
|
12530
|
+
const state = await authenticate(req, res);
|
|
12531
|
+
if (state === null) return;
|
|
12532
|
+
const admin = resolveAdmin(req, res, state);
|
|
12533
|
+
if (admin === null) return;
|
|
12534
|
+
if (!checkCsrf(req, res, state)) return;
|
|
12535
|
+
const body = req.body;
|
|
12536
|
+
const action = typeof body.action === "string" ? body.action : "";
|
|
12537
|
+
const raw = body.ids;
|
|
12538
|
+
const ids = (Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw]).filter((value) => typeof value === "string").filter((value) => value !== "");
|
|
12539
|
+
const listUrl = `${prefix}/m/${admin.slug()}`;
|
|
12540
|
+
const back = (message, level) => {
|
|
12541
|
+
res.redirect(`${listUrl}?${buildQuery({ flash: message, level })}`);
|
|
12542
|
+
};
|
|
12543
|
+
if (ids.length === 0) {
|
|
12544
|
+
back("No rows were selected.", "warning");
|
|
12545
|
+
return;
|
|
12546
|
+
}
|
|
12547
|
+
if (!bulkActionsFor(admin).some((option) => option.value === action)) {
|
|
12548
|
+
html(res, renderNotFound(context(req, state.session)), 400);
|
|
12549
|
+
return;
|
|
12550
|
+
}
|
|
12551
|
+
const repository = admin.repository(state.dbSession);
|
|
12552
|
+
const scope = { [admin.identityField]: { in: ids } };
|
|
12553
|
+
if (action.startsWith("custom:")) {
|
|
12554
|
+
const custom = admin.getAction(action.slice("custom:".length));
|
|
12555
|
+
if (custom === null) {
|
|
12556
|
+
html(res, renderNotFound(context(req, state.session)), 400);
|
|
12557
|
+
return;
|
|
12558
|
+
}
|
|
12559
|
+
let result;
|
|
12560
|
+
try {
|
|
12561
|
+
result = await runCustomAction(custom, {
|
|
12562
|
+
ids,
|
|
12563
|
+
repository,
|
|
12564
|
+
dbSession: state.dbSession,
|
|
12565
|
+
request: req,
|
|
12566
|
+
session: state.session,
|
|
12567
|
+
principal: state.principal
|
|
12568
|
+
});
|
|
12569
|
+
} catch (error) {
|
|
12570
|
+
logger2.error("Admin action failed", {
|
|
12571
|
+
slug: admin.slug(),
|
|
12572
|
+
action: custom.name,
|
|
12573
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12574
|
+
});
|
|
12575
|
+
back(
|
|
12576
|
+
`${custom.label} failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
12577
|
+
"error"
|
|
12578
|
+
);
|
|
12579
|
+
return;
|
|
12580
|
+
}
|
|
12581
|
+
if (result === null) {
|
|
12582
|
+
res.redirect(listUrl);
|
|
12583
|
+
return;
|
|
12584
|
+
}
|
|
12585
|
+
back(result.message, result.category ?? "success");
|
|
12586
|
+
return;
|
|
12587
|
+
}
|
|
12588
|
+
if (action === "delete") {
|
|
12589
|
+
const removed = await repository.delete(scope);
|
|
12590
|
+
back(`Deleted ${removed} record${removed === 1 ? "" : "s"}.`, "success");
|
|
12591
|
+
return;
|
|
12592
|
+
}
|
|
12593
|
+
const active = action === "activate";
|
|
12594
|
+
const changed = await repository.update(scope, { isActive: active });
|
|
12595
|
+
back(
|
|
12596
|
+
`${active ? "Activated" : "Deactivated"} ${changed} record${changed === 1 ? "" : "s"}.`,
|
|
12597
|
+
"success"
|
|
12598
|
+
);
|
|
12599
|
+
})
|
|
12600
|
+
);
|
|
12379
12601
|
router.get(
|
|
12380
12602
|
`${prefix}/m/:slug/:identity`,
|
|
12381
12603
|
guarded(async (req, res) => {
|
|
@@ -12420,7 +12642,10 @@ function makeAdminRouter(site, options) {
|
|
|
12420
12642
|
renderFormPage(context(req, state.session), {
|
|
12421
12643
|
mode: "edit",
|
|
12422
12644
|
title: admin.verboseName(),
|
|
12423
|
-
fields: buildFormFields(admin, {
|
|
12645
|
+
fields: buildFormFields(admin, {
|
|
12646
|
+
values: row,
|
|
12647
|
+
foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession)
|
|
12648
|
+
}),
|
|
12424
12649
|
actionUrl: `${prefix}/m/${admin.slug()}/${identity}/edit`,
|
|
12425
12650
|
backUrl: `${prefix}/m/${admin.slug()}/${identity}`,
|
|
12426
12651
|
error: null
|
|
@@ -12443,13 +12668,18 @@ function makeAdminRouter(site, options) {
|
|
|
12443
12668
|
if (!checkCsrf(req, res, state)) return;
|
|
12444
12669
|
const body = req.body;
|
|
12445
12670
|
const parsed = parseFormBody(admin, body);
|
|
12671
|
+
const foreignKeyOptions = await foreignKeyOptionsFor(admin, state.dbSession);
|
|
12446
12672
|
const rerender = (error, status) => {
|
|
12447
12673
|
html(
|
|
12448
12674
|
res,
|
|
12449
12675
|
renderFormPage(context(req, state.session), {
|
|
12450
12676
|
mode: "edit",
|
|
12451
12677
|
title: admin.verboseName(),
|
|
12452
|
-
fields: buildFormFields(admin, {
|
|
12678
|
+
fields: buildFormFields(admin, {
|
|
12679
|
+
values: body,
|
|
12680
|
+
errors: parsed.errors,
|
|
12681
|
+
foreignKeyOptions
|
|
12682
|
+
}),
|
|
12453
12683
|
actionUrl: `${prefix}/m/${admin.slug()}/${identity}/edit`,
|
|
12454
12684
|
backUrl: `${prefix}/m/${admin.slug()}/${identity}`,
|
|
12455
12685
|
error
|
|
@@ -12496,8 +12726,69 @@ function makeAdminRouter(site, options) {
|
|
|
12496
12726
|
}
|
|
12497
12727
|
async function renderList(req, admin, state) {
|
|
12498
12728
|
const columns = adminColumns(admin.model);
|
|
12499
|
-
const
|
|
12729
|
+
const query = await resolveListQuery(req, admin, state.dbSession);
|
|
12500
12730
|
const page2 = Math.max(1, Number.parseInt(queryString(req.query.page), 10) || 1);
|
|
12731
|
+
const result = await admin.repository(state.dbSession).paginate({
|
|
12732
|
+
page: page2,
|
|
12733
|
+
pageSize: admin.pageSize,
|
|
12734
|
+
...query.orderBy === void 0 ? {} : { orderBy: query.orderBy },
|
|
12735
|
+
ascending: query.ascending,
|
|
12736
|
+
...query.where === void 0 ? {} : { filters: query.where }
|
|
12737
|
+
});
|
|
12738
|
+
const displayed = admin.listDisplayNames();
|
|
12739
|
+
const sort = {};
|
|
12740
|
+
for (const column6 of displayed) {
|
|
12741
|
+
if (!(column6 in columns)) continue;
|
|
12742
|
+
const active = (query.sortColumn ?? admin.orderKey) === column6;
|
|
12743
|
+
const nextAscending = active ? !query.ascending : true;
|
|
12744
|
+
sort[column6] = {
|
|
12745
|
+
url: `?${buildQuery({
|
|
12746
|
+
...query.baseQuery,
|
|
12747
|
+
sort: column6,
|
|
12748
|
+
dir: nextAscending ? "asc" : "desc"
|
|
12749
|
+
})}`,
|
|
12750
|
+
active,
|
|
12751
|
+
ascending: query.ascending
|
|
12752
|
+
};
|
|
12753
|
+
}
|
|
12754
|
+
const sortParams = {
|
|
12755
|
+
sort: query.sortColumn ?? void 0,
|
|
12756
|
+
dir: query.sortColumn === null ? void 0 : query.ascending ? "asc" : "desc"
|
|
12757
|
+
};
|
|
12758
|
+
const pageUrl = (target) => `?${buildQuery({ ...query.baseQuery, ...sortParams, page: target })}`;
|
|
12759
|
+
const exportQuery = buildQuery({ ...query.baseQuery, ...sortParams });
|
|
12760
|
+
const exportUrl = (format) => `${prefix}/m/${admin.slug()}/export.${format}${exportQuery === "" ? "" : `?${exportQuery}`}`;
|
|
12761
|
+
const view = {
|
|
12762
|
+
title: admin.verboseNamePlural(),
|
|
12763
|
+
columns: displayed,
|
|
12764
|
+
rows: result.items.map((row) => {
|
|
12765
|
+
const identity = String(row[admin.identityField]);
|
|
12766
|
+
return {
|
|
12767
|
+
identity,
|
|
12768
|
+
cells: displayed.map((column6) => formatCellValue(row[column6])),
|
|
12769
|
+
url: `${prefix}/m/${admin.slug()}/${identity}`
|
|
12770
|
+
};
|
|
12771
|
+
}),
|
|
12772
|
+
total: result.total,
|
|
12773
|
+
page: result.page,
|
|
12774
|
+
pages: result.pages,
|
|
12775
|
+
prevUrl: result.page > 1 ? pageUrl(result.page - 1) : null,
|
|
12776
|
+
nextUrl: result.page < result.pages ? pageUrl(result.page + 1) : null,
|
|
12777
|
+
searchable: query.searchable.length > 0,
|
|
12778
|
+
searchValue: query.search,
|
|
12779
|
+
filters: query.filterViews,
|
|
12780
|
+
sort,
|
|
12781
|
+
newUrl: admin.canCreate ? `${prefix}/m/${admin.slug()}/new` : null,
|
|
12782
|
+
bulkActions: bulkActionsFor(admin),
|
|
12783
|
+
bulkUrl: `${prefix}/m/${admin.slug()}/bulk`,
|
|
12784
|
+
exportCsvUrl: exportUrl("csv"),
|
|
12785
|
+
exportJsonUrl: exportUrl("json")
|
|
12786
|
+
};
|
|
12787
|
+
return renderListPage(context(req, state.session), view);
|
|
12788
|
+
}
|
|
12789
|
+
async function resolveListQuery(req, admin, dbSession) {
|
|
12790
|
+
const columns = adminColumns(admin.model);
|
|
12791
|
+
const search = queryString(req.query.q);
|
|
12501
12792
|
const sortField = queryString(req.query.sort);
|
|
12502
12793
|
const sortColumn = sortField in columns ? sortField : null;
|
|
12503
12794
|
const ascending = sortColumn === null ? admin.orderAscending : queryString(req.query.dir) !== "desc";
|
|
@@ -12525,6 +12816,8 @@ function makeAdminRouter(site, options) {
|
|
|
12525
12816
|
});
|
|
12526
12817
|
continue;
|
|
12527
12818
|
}
|
|
12819
|
+
const related = await relatedOptions(column6, dbSession);
|
|
12820
|
+
const options2 = related ?? spec.options;
|
|
12528
12821
|
const value = queryString(req.query[`filter_${field}`]);
|
|
12529
12822
|
if (value !== "") {
|
|
12530
12823
|
conditions.push({
|
|
@@ -12534,11 +12827,11 @@ function makeAdminRouter(site, options) {
|
|
|
12534
12827
|
filterViews.push({
|
|
12535
12828
|
field,
|
|
12536
12829
|
label: humanizeField(field),
|
|
12537
|
-
kind: spec.kind,
|
|
12830
|
+
kind: related === null ? spec.kind : "select",
|
|
12538
12831
|
value,
|
|
12539
12832
|
valueFrom: "",
|
|
12540
12833
|
valueTo: "",
|
|
12541
|
-
options:
|
|
12834
|
+
options: options2.map((option) => ({
|
|
12542
12835
|
value: option.value,
|
|
12543
12836
|
label: option.label,
|
|
12544
12837
|
selected: option.value === value
|
|
@@ -12554,72 +12847,98 @@ function makeAdminRouter(site, options) {
|
|
|
12554
12847
|
tempestDbJs.or(...searchable.map((field) => ({ [field]: { ilike: `%${search}%` } })))
|
|
12555
12848
|
);
|
|
12556
12849
|
}
|
|
12557
|
-
const where = conditions.length === 0 ? void 0 : tempestDbJs.and(...conditions);
|
|
12558
|
-
const orderBy = sortColumn ?? admin.orderKey ?? void 0;
|
|
12559
|
-
const result = await admin.repository(state.dbSession).paginate({
|
|
12560
|
-
page: page2,
|
|
12561
|
-
pageSize: admin.pageSize,
|
|
12562
|
-
...orderBy === void 0 ? {} : { orderBy },
|
|
12563
|
-
ascending,
|
|
12564
|
-
...where === void 0 ? {} : { filters: where }
|
|
12565
|
-
});
|
|
12566
|
-
const displayed = admin.listDisplayNames();
|
|
12567
12850
|
const baseQuery = { q: search };
|
|
12568
|
-
for (const
|
|
12569
|
-
if (
|
|
12570
|
-
baseQuery[`filter_${
|
|
12571
|
-
baseQuery[`filter_${
|
|
12851
|
+
for (const view of filterViews) {
|
|
12852
|
+
if (view.kind === "daterange") {
|
|
12853
|
+
baseQuery[`filter_${view.field}_from`] = view.valueFrom;
|
|
12854
|
+
baseQuery[`filter_${view.field}_to`] = view.valueTo;
|
|
12572
12855
|
} else {
|
|
12573
|
-
baseQuery[`filter_${
|
|
12856
|
+
baseQuery[`filter_${view.field}`] = view.value;
|
|
12574
12857
|
}
|
|
12575
12858
|
}
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
dir: nextAscending ? "asc" : "desc"
|
|
12586
|
-
})}`,
|
|
12587
|
-
active,
|
|
12588
|
-
ascending
|
|
12589
|
-
};
|
|
12590
|
-
}
|
|
12591
|
-
const pageUrl = (target) => `?${buildQuery({
|
|
12592
|
-
...baseQuery,
|
|
12593
|
-
sort: sortColumn ?? void 0,
|
|
12594
|
-
dir: sortColumn === null ? void 0 : ascending ? "asc" : "desc",
|
|
12595
|
-
page: target
|
|
12596
|
-
})}`;
|
|
12597
|
-
const view = {
|
|
12598
|
-
title: admin.verboseNamePlural(),
|
|
12599
|
-
columns: displayed,
|
|
12600
|
-
rows: result.items.map((row) => {
|
|
12601
|
-
const identity = String(row[admin.identityField]);
|
|
12602
|
-
return {
|
|
12603
|
-
identity,
|
|
12604
|
-
cells: displayed.map((column6) => formatCellValue(row[column6])),
|
|
12605
|
-
url: `${prefix}/m/${admin.slug()}/${identity}`
|
|
12606
|
-
};
|
|
12607
|
-
}),
|
|
12608
|
-
total: result.total,
|
|
12609
|
-
page: result.page,
|
|
12610
|
-
pages: result.pages,
|
|
12611
|
-
prevUrl: result.page > 1 ? pageUrl(result.page - 1) : null,
|
|
12612
|
-
nextUrl: result.page < result.pages ? pageUrl(result.page + 1) : null,
|
|
12613
|
-
searchable: searchable.length > 0,
|
|
12614
|
-
searchValue: search,
|
|
12615
|
-
filters: filterViews,
|
|
12616
|
-
sort,
|
|
12617
|
-
newUrl: admin.canCreate ? `${prefix}/m/${admin.slug()}/new` : null
|
|
12859
|
+
return {
|
|
12860
|
+
search,
|
|
12861
|
+
searchable,
|
|
12862
|
+
where: conditions.length === 0 ? void 0 : tempestDbJs.and(...conditions),
|
|
12863
|
+
orderBy: sortColumn ?? admin.orderKey ?? void 0,
|
|
12864
|
+
ascending,
|
|
12865
|
+
sortColumn,
|
|
12866
|
+
filterViews,
|
|
12867
|
+
baseQuery
|
|
12618
12868
|
};
|
|
12619
|
-
|
|
12869
|
+
}
|
|
12870
|
+
async function relatedOptions(column6, dbSession) {
|
|
12871
|
+
const table = foreignKeyTable(column6);
|
|
12872
|
+
if (table === null) return null;
|
|
12873
|
+
const referenced = site.get(table);
|
|
12874
|
+
if (referenced === null) return null;
|
|
12875
|
+
const rows = await referenced.repository(dbSession).list();
|
|
12876
|
+
return rows.slice(0, FK_OPTION_CAP).map((row) => ({
|
|
12877
|
+
value: String(row[referenced.identityField]),
|
|
12878
|
+
label: foreignKeyLabel(referenced, row)
|
|
12879
|
+
}));
|
|
12880
|
+
}
|
|
12881
|
+
async function foreignKeyOptionsFor(admin, dbSession) {
|
|
12882
|
+
const columns = adminColumns(admin.model);
|
|
12883
|
+
const options2 = {};
|
|
12884
|
+
for (const field of Object.keys(foreignKeyFields(admin))) {
|
|
12885
|
+
const column6 = columns[field];
|
|
12886
|
+
if (column6 === void 0) continue;
|
|
12887
|
+
const related = await relatedOptions(column6, dbSession);
|
|
12888
|
+
if (related !== null) options2[field] = related;
|
|
12889
|
+
}
|
|
12890
|
+
return options2;
|
|
12620
12891
|
}
|
|
12621
12892
|
return router;
|
|
12622
12893
|
}
|
|
12894
|
+
function bulkActionsFor(admin) {
|
|
12895
|
+
const actions = [];
|
|
12896
|
+
const hasActiveFlag = "isActive" in adminColumns(admin.model);
|
|
12897
|
+
if (admin.canEdit && hasActiveFlag) {
|
|
12898
|
+
actions.push({ value: "activate", label: "Activate", dangerous: false });
|
|
12899
|
+
actions.push({ value: "deactivate", label: "Deactivate", dangerous: false });
|
|
12900
|
+
}
|
|
12901
|
+
if (admin.canDelete) {
|
|
12902
|
+
actions.push({ value: "delete", label: "Delete", dangerous: true });
|
|
12903
|
+
}
|
|
12904
|
+
for (const action of admin.customActions()) {
|
|
12905
|
+
actions.push({
|
|
12906
|
+
value: `custom:${action.name}`,
|
|
12907
|
+
label: action.label,
|
|
12908
|
+
dangerous: action.dangerous
|
|
12909
|
+
});
|
|
12910
|
+
}
|
|
12911
|
+
return actions;
|
|
12912
|
+
}
|
|
12913
|
+
function exportValue(value) {
|
|
12914
|
+
if (value instanceof Date) return value.toISOString();
|
|
12915
|
+
if (typeof value === "bigint") return value.toString();
|
|
12916
|
+
if (value instanceof Uint8Array) return Buffer.from(value).toString("base64");
|
|
12917
|
+
return value;
|
|
12918
|
+
}
|
|
12919
|
+
function csvField(value) {
|
|
12920
|
+
if (value === null || value === void 0) return "";
|
|
12921
|
+
const text = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
12922
|
+
if (/[",\r\n]/.test(text)) return `"${text.replace(/"/g, '""')}"`;
|
|
12923
|
+
return text;
|
|
12924
|
+
}
|
|
12925
|
+
function toCsv(columns, rows) {
|
|
12926
|
+
const lines = [columns.map(csvField).join(",")];
|
|
12927
|
+
for (const row of rows) {
|
|
12928
|
+
lines.push(columns.map((column6) => csvField(exportValue(row[column6]))).join(","));
|
|
12929
|
+
}
|
|
12930
|
+
return `${lines.join("\r\n")}\r
|
|
12931
|
+
`;
|
|
12932
|
+
}
|
|
12933
|
+
function toJson(columns, rows) {
|
|
12934
|
+
return JSON.stringify(
|
|
12935
|
+
rows.map(
|
|
12936
|
+
(row) => Object.fromEntries(columns.map((column6) => [column6, exportValue(row[column6])]))
|
|
12937
|
+
),
|
|
12938
|
+
null,
|
|
12939
|
+
2
|
|
12940
|
+
);
|
|
12941
|
+
}
|
|
12623
12942
|
function describeWriteFailure(admin, error) {
|
|
12624
12943
|
const detail = error instanceof Error ? error.message : String(error);
|
|
12625
12944
|
return `The database refused this ${admin.verboseName().toLowerCase()}: ${detail}`;
|
|
@@ -14624,7 +14943,7 @@ async function withTestDatabase(models, fn) {
|
|
|
14624
14943
|
}
|
|
14625
14944
|
|
|
14626
14945
|
// src/version.ts
|
|
14627
|
-
var VERSION = "0.
|
|
14946
|
+
var VERSION = "0.25.0";
|
|
14628
14947
|
|
|
14629
14948
|
Object.defineProperty(exports, "OpenAPIRegistry", {
|
|
14630
14949
|
enumerable: true,
|
|
@@ -14880,6 +15199,7 @@ exports.WebhookSignatureVerifier = WebhookSignatureVerifier;
|
|
|
14880
15199
|
exports.WhatsAppProvider = WhatsAppProvider;
|
|
14881
15200
|
exports.activationSchema = activationSchema;
|
|
14882
15201
|
exports.addLogSink = addLogSink;
|
|
15202
|
+
exports.adminAction = adminAction;
|
|
14883
15203
|
exports.adminColumns = adminColumns;
|
|
14884
15204
|
exports.adminThemeCss = adminThemeCss;
|
|
14885
15205
|
exports.attachWebSocketHub = attachWebSocketHub;
|
|
@@ -14926,6 +15246,9 @@ exports.envBoolean = looseBoolean;
|
|
|
14926
15246
|
exports.envList = envList;
|
|
14927
15247
|
exports.escapeHtml = escapeHtml;
|
|
14928
15248
|
exports.filterForColumn = filterForColumn;
|
|
15249
|
+
exports.foreignKeyFields = foreignKeyFields;
|
|
15250
|
+
exports.foreignKeyLabel = foreignKeyLabel;
|
|
15251
|
+
exports.foreignKeyTable = foreignKeyTable;
|
|
14929
15252
|
exports.formatCellValue = formatCellValue;
|
|
14930
15253
|
exports.formatFieldValue = formatFieldValue;
|
|
14931
15254
|
exports.generateCsrfToken = generateCsrfToken;
|