tempest-express-sdk 0.27.0 → 0.29.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-ADF7OHRH.js → chunk-7T4MZJZT.js} +3 -3
- package/dist/{chunk-ADF7OHRH.js.map → chunk-7T4MZJZT.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 +882 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +479 -2
- package/dist/index.d.ts +479 -2
- package/dist/index.js +871 -60
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
package/dist/index.cjs
CHANGED
|
@@ -9461,6 +9461,216 @@ function partitionTotal(partition) {
|
|
|
9461
9461
|
return partition.segments.reduce((total, segment) => total + segment.value, 0);
|
|
9462
9462
|
}
|
|
9463
9463
|
|
|
9464
|
+
// src/admin/logs.ts
|
|
9465
|
+
var LIFTED = /* @__PURE__ */ new Set(["level", "logger", "message", "timestamp", "stack"]);
|
|
9466
|
+
function toLogEntry(raw) {
|
|
9467
|
+
const context = {};
|
|
9468
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
9469
|
+
if (LIFTED.has(key) || value === null || value === void 0) continue;
|
|
9470
|
+
context[key] = value;
|
|
9471
|
+
}
|
|
9472
|
+
return {
|
|
9473
|
+
level: typeof raw.level === "string" ? raw.level : "info",
|
|
9474
|
+
logger: typeof raw.logger === "string" ? raw.logger : "",
|
|
9475
|
+
message: typeof raw.message === "string" ? raw.message : "",
|
|
9476
|
+
timestamp: typeof raw.timestamp === "string" ? raw.timestamp : "",
|
|
9477
|
+
stack: typeof raw.stack === "string" ? raw.stack : null,
|
|
9478
|
+
context,
|
|
9479
|
+
raw
|
|
9480
|
+
};
|
|
9481
|
+
}
|
|
9482
|
+
function filterLogEntries(entries, term) {
|
|
9483
|
+
const needle = term.trim().toLowerCase();
|
|
9484
|
+
if (needle === "") return entries;
|
|
9485
|
+
return entries.filter(
|
|
9486
|
+
(entry) => `${entry.message} ${entry.logger} ${entry.stack ?? ""}`.toLowerCase().includes(needle)
|
|
9487
|
+
);
|
|
9488
|
+
}
|
|
9489
|
+
function renderLogEntriesMarkdown(entries, options) {
|
|
9490
|
+
const lines = [
|
|
9491
|
+
"# Application logs",
|
|
9492
|
+
"",
|
|
9493
|
+
`- **Source:** \`${options.source}\``,
|
|
9494
|
+
`- **Search:** ${options.query === "" ? "_none_" : `\`${options.query}\``}`,
|
|
9495
|
+
`- **Exported:** ${entries.length} of ${options.total} matching record(s)`,
|
|
9496
|
+
""
|
|
9497
|
+
];
|
|
9498
|
+
if (entries.length < options.total) {
|
|
9499
|
+
lines.push(
|
|
9500
|
+
`> Truncated: the export is capped, so ${options.total - entries.length} older matching record(s) are not included.`,
|
|
9501
|
+
""
|
|
9502
|
+
);
|
|
9503
|
+
}
|
|
9504
|
+
for (const entry of entries) {
|
|
9505
|
+
lines.push(
|
|
9506
|
+
`## ${entry.level.toUpperCase()} \u2014 ${entry.message || "(no message)"}`,
|
|
9507
|
+
"",
|
|
9508
|
+
`- **When:** ${entry.timestamp || "unknown"}`,
|
|
9509
|
+
`- **Logger:** ${entry.logger || "unknown"}`
|
|
9510
|
+
);
|
|
9511
|
+
for (const [key, value] of Object.entries(entry.context)) {
|
|
9512
|
+
lines.push(
|
|
9513
|
+
`- **${key}:** ${typeof value === "string" ? value : JSON.stringify(value)}`
|
|
9514
|
+
);
|
|
9515
|
+
}
|
|
9516
|
+
lines.push("");
|
|
9517
|
+
if (entry.stack !== null) {
|
|
9518
|
+
lines.push("```text", entry.stack, "```", "");
|
|
9519
|
+
}
|
|
9520
|
+
}
|
|
9521
|
+
return lines.join("\n");
|
|
9522
|
+
}
|
|
9523
|
+
function renderLogEntriesJson(entries) {
|
|
9524
|
+
return JSON.stringify(
|
|
9525
|
+
entries.map((entry) => entry.raw),
|
|
9526
|
+
null,
|
|
9527
|
+
2
|
|
9528
|
+
);
|
|
9529
|
+
}
|
|
9530
|
+
|
|
9531
|
+
// src/admin/sqlConsole.ts
|
|
9532
|
+
var SqlCapability = {
|
|
9533
|
+
/** `SELECT`, `WITH … SELECT`, `EXPLAIN`, `SHOW`. */
|
|
9534
|
+
READ: "read",
|
|
9535
|
+
/** Adds rows. */
|
|
9536
|
+
INSERT: "insert",
|
|
9537
|
+
/** Changes rows. */
|
|
9538
|
+
UPDATE: "update",
|
|
9539
|
+
/** Removes rows. */
|
|
9540
|
+
DELETE: "delete",
|
|
9541
|
+
/** `CREATE` / `ALTER` / `COMMENT`. */
|
|
9542
|
+
DDL: "ddl",
|
|
9543
|
+
/** `DROP` and `TRUNCATE`: irreversible structure loss. */
|
|
9544
|
+
DROP: "drop",
|
|
9545
|
+
/**
|
|
9546
|
+
* `GRANT` / `REVOKE` / `SET`, and anything the analyser cannot classify.
|
|
9547
|
+
* Unknown statements land here on purpose, so a construct nobody anticipated
|
|
9548
|
+
* needs the most privileged capability rather than the least.
|
|
9549
|
+
*/
|
|
9550
|
+
ADMIN: "admin"
|
|
9551
|
+
};
|
|
9552
|
+
var STATEMENT_CAPABILITIES = {
|
|
9553
|
+
select: SqlCapability.READ,
|
|
9554
|
+
with: SqlCapability.READ,
|
|
9555
|
+
explain: SqlCapability.READ,
|
|
9556
|
+
show: SqlCapability.READ,
|
|
9557
|
+
desc: SqlCapability.READ,
|
|
9558
|
+
describe: SqlCapability.READ,
|
|
9559
|
+
insert: SqlCapability.INSERT,
|
|
9560
|
+
replace: SqlCapability.INSERT,
|
|
9561
|
+
update: SqlCapability.UPDATE,
|
|
9562
|
+
delete: SqlCapability.DELETE,
|
|
9563
|
+
create: SqlCapability.DDL,
|
|
9564
|
+
alter: SqlCapability.DDL,
|
|
9565
|
+
comment: SqlCapability.DDL,
|
|
9566
|
+
rename: SqlCapability.DDL,
|
|
9567
|
+
drop: SqlCapability.DROP,
|
|
9568
|
+
truncate: SqlCapability.DROP
|
|
9569
|
+
};
|
|
9570
|
+
var PARSER_HINT = "The admin SQL console needs the optional peer `node-sql-parser`. Install it with: npm install node-sql-parser";
|
|
9571
|
+
async function loadSqlParser() {
|
|
9572
|
+
try {
|
|
9573
|
+
const module = await import('node-sql-parser');
|
|
9574
|
+
const Parser = module.Parser ?? module.default?.Parser;
|
|
9575
|
+
if (Parser === void 0) throw new Error(PARSER_HINT);
|
|
9576
|
+
return new Parser();
|
|
9577
|
+
} catch {
|
|
9578
|
+
throw new Error(PARSER_HINT);
|
|
9579
|
+
}
|
|
9580
|
+
}
|
|
9581
|
+
function analyzeSql(sql5, dialect, parser) {
|
|
9582
|
+
const capabilities = /* @__PURE__ */ new Set();
|
|
9583
|
+
const tables = /* @__PURE__ */ new Set();
|
|
9584
|
+
let statements = 0;
|
|
9585
|
+
let parsed = true;
|
|
9586
|
+
let unscopedWrite = false;
|
|
9587
|
+
try {
|
|
9588
|
+
const ast = parser.astify(sql5, { database: dialect });
|
|
9589
|
+
const list = Array.isArray(ast) ? ast : [ast];
|
|
9590
|
+
statements = list.length;
|
|
9591
|
+
for (const statement of list) {
|
|
9592
|
+
const type = String(statement.type ?? "").toLowerCase();
|
|
9593
|
+
capabilities.add(STATEMENT_CAPABILITIES[type] ?? SqlCapability.ADMIN);
|
|
9594
|
+
if ((type === "update" || type === "delete") && !statement.where) {
|
|
9595
|
+
unscopedWrite = true;
|
|
9596
|
+
}
|
|
9597
|
+
}
|
|
9598
|
+
} catch {
|
|
9599
|
+
parsed = false;
|
|
9600
|
+
statements = 1;
|
|
9601
|
+
capabilities.add(SqlCapability.ADMIN);
|
|
9602
|
+
}
|
|
9603
|
+
try {
|
|
9604
|
+
for (const entry of parser.tableList(sql5, { database: dialect })) {
|
|
9605
|
+
const name = entry.split("::").pop();
|
|
9606
|
+
if (name !== void 0 && name !== "null") tables.add(name.toLowerCase());
|
|
9607
|
+
}
|
|
9608
|
+
} catch {
|
|
9609
|
+
}
|
|
9610
|
+
return {
|
|
9611
|
+
statements,
|
|
9612
|
+
capabilities: [...capabilities],
|
|
9613
|
+
tables: [...tables],
|
|
9614
|
+
parsed,
|
|
9615
|
+
unscopedWrite
|
|
9616
|
+
};
|
|
9617
|
+
}
|
|
9618
|
+
function checkSqlPolicy(analysis, policy) {
|
|
9619
|
+
const granted = new Set(policy.capabilities ?? [SqlCapability.READ]);
|
|
9620
|
+
for (const capability of analysis.capabilities) {
|
|
9621
|
+
if (!granted.has(capability)) {
|
|
9622
|
+
return {
|
|
9623
|
+
allowed: false,
|
|
9624
|
+
reason: analysis.parsed ? `This console may not run ${capability} statements.` : "The statement could not be parsed, so it needs the admin capability."
|
|
9625
|
+
};
|
|
9626
|
+
}
|
|
9627
|
+
}
|
|
9628
|
+
if ((policy.requireWhereOnWrites ?? true) && analysis.unscopedWrite) {
|
|
9629
|
+
return {
|
|
9630
|
+
allowed: false,
|
|
9631
|
+
reason: "An UPDATE or DELETE without a WHERE clause is refused."
|
|
9632
|
+
};
|
|
9633
|
+
}
|
|
9634
|
+
const denied = new Set((policy.denyTables ?? []).map((name) => name.toLowerCase()));
|
|
9635
|
+
for (const table of analysis.tables) {
|
|
9636
|
+
if (denied.has(table)) {
|
|
9637
|
+
return { allowed: false, reason: `Table "${table}" is not available here.` };
|
|
9638
|
+
}
|
|
9639
|
+
}
|
|
9640
|
+
if (policy.allowTables !== void 0) {
|
|
9641
|
+
const allowed = new Set(policy.allowTables.map((name) => name.toLowerCase()));
|
|
9642
|
+
if (analysis.tables.length === 0) {
|
|
9643
|
+
return {
|
|
9644
|
+
allowed: false,
|
|
9645
|
+
reason: "This console only runs statements naming an allowed table."
|
|
9646
|
+
};
|
|
9647
|
+
}
|
|
9648
|
+
for (const table of analysis.tables) {
|
|
9649
|
+
if (!allowed.has(table)) {
|
|
9650
|
+
return { allowed: false, reason: `Table "${table}" is not on the allow list.` };
|
|
9651
|
+
}
|
|
9652
|
+
}
|
|
9653
|
+
}
|
|
9654
|
+
return { allowed: true, reason: null };
|
|
9655
|
+
}
|
|
9656
|
+
|
|
9657
|
+
// src/admin/inlines.ts
|
|
9658
|
+
function adminInline(options) {
|
|
9659
|
+
const slug = options.model.tablename;
|
|
9660
|
+
if (typeof slug !== "string" || slug === "") {
|
|
9661
|
+
throw new Error("adminInline requires a concrete model with a tablename");
|
|
9662
|
+
}
|
|
9663
|
+
return {
|
|
9664
|
+
model: options.model,
|
|
9665
|
+
slug,
|
|
9666
|
+
fkField: options.fkField,
|
|
9667
|
+
listDisplay: options.listDisplay === void 0 ? null : [...options.listDisplay],
|
|
9668
|
+
label: options.label ?? null,
|
|
9669
|
+
editable: options.editable ?? false,
|
|
9670
|
+
canDelete: options.canDelete ?? false
|
|
9671
|
+
};
|
|
9672
|
+
}
|
|
9673
|
+
|
|
9464
9674
|
// src/admin/lenses.ts
|
|
9465
9675
|
function slugify(name) {
|
|
9466
9676
|
return name.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "lens";
|
|
@@ -9716,6 +9926,8 @@ var AdminModel = class {
|
|
|
9716
9926
|
canImport;
|
|
9717
9927
|
/** Foreign-key columns rendered as a typed search box. */
|
|
9718
9928
|
autocompleteFields;
|
|
9929
|
+
/** Related child models listed on the detail view. */
|
|
9930
|
+
inlines;
|
|
9719
9931
|
actions = /* @__PURE__ */ new Map();
|
|
9720
9932
|
slugOverride;
|
|
9721
9933
|
listDisplayOverride;
|
|
@@ -9747,6 +9959,7 @@ var AdminModel = class {
|
|
|
9747
9959
|
this.uploadStorage = options.uploadStorage ?? null;
|
|
9748
9960
|
this.canImport = options.canImport ?? false;
|
|
9749
9961
|
this.autocompleteFields = [...options.autocompleteFields ?? []];
|
|
9962
|
+
this.inlines = [...options.inlines ?? []];
|
|
9750
9963
|
if (this.uploadFields.length > 0 && this.uploadStorage === null) {
|
|
9751
9964
|
throw new Error(
|
|
9752
9965
|
`AdminModel(${this.model.tablename}).uploadFields requires an uploadStorage (e.g. LocalUploadStorage / S3UploadStorage) \u2014 without one there is nowhere to write the file.`
|
|
@@ -10025,7 +10238,9 @@ function parseFormBody(admin, body, options = {}) {
|
|
|
10025
10238
|
const data = {};
|
|
10026
10239
|
const errors = {};
|
|
10027
10240
|
const uploads = options.uploadsAsText === true ? /* @__PURE__ */ new Set() : new Set(admin.uploadFields);
|
|
10241
|
+
const only = options.only === void 0 ? null : new Set(options.only);
|
|
10028
10242
|
for (const name of admin.editableFieldNames()) {
|
|
10243
|
+
if (only !== null && !only.has(name)) continue;
|
|
10029
10244
|
const column6 = columns[name];
|
|
10030
10245
|
if (column6 === void 0) continue;
|
|
10031
10246
|
if (uploads.has(name)) continue;
|
|
@@ -12090,7 +12305,7 @@ function escapeHtml(value) {
|
|
|
12090
12305
|
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
12091
12306
|
}
|
|
12092
12307
|
function renderLayout(context, title, body) {
|
|
12093
|
-
const { site, theme, prefix, session, currentPath, navModels, messages } = context;
|
|
12308
|
+
const { site, theme, prefix, session, currentPath, navModels, navSystem, messages } = context;
|
|
12094
12309
|
const authed = session !== null;
|
|
12095
12310
|
const indexUrl = `${prefix}/`;
|
|
12096
12311
|
const navLink = (url, label, active) => `<a href="${escapeHtml(url)}" class="tempest-admin-sidebar__link${active ? " tempest-admin-sidebar__link--active" : ""}">${escapeHtml(label)}</a>`;
|
|
@@ -12100,6 +12315,9 @@ function renderLayout(context, title, body) {
|
|
|
12100
12315
|
${navLink(indexUrl, "Dashboard", currentPath === indexUrl)}
|
|
12101
12316
|
${navModels.length > 0 ? `<span class="tempest-admin-sidebar__heading">Models</span>${navModels.map(
|
|
12102
12317
|
(entry) => navLink(entry.url, entry.label, currentPath.startsWith(entry.url))
|
|
12318
|
+
).join("")}` : ""}
|
|
12319
|
+
${navSystem.length > 0 ? `<span class="tempest-admin-sidebar__heading">System</span>${navSystem.map(
|
|
12320
|
+
(entry) => navLink(entry.url, entry.label, currentPath.startsWith(entry.url))
|
|
12103
12321
|
).join("")}` : ""}
|
|
12104
12322
|
</nav>
|
|
12105
12323
|
</aside>` : "";
|
|
@@ -12359,15 +12577,102 @@ function renderDetailPage(context, view) {
|
|
|
12359
12577
|
</div>
|
|
12360
12578
|
</header>
|
|
12361
12579
|
<dl class="tempest-admin-detail__fields">${fields}</dl>
|
|
12580
|
+
${view.inlines.map((inline) => renderInline(inline, context.session?.csrfToken ?? "", view.inlineError)).join("")}
|
|
12362
12581
|
${auditPanel}
|
|
12363
12582
|
</section>`;
|
|
12364
12583
|
return renderLayout(context, `${view.title} \xB7 ${view.identity}`, body);
|
|
12365
12584
|
}
|
|
12366
|
-
function
|
|
12367
|
-
const
|
|
12585
|
+
function renderInlineCell(field) {
|
|
12586
|
+
const required = field.required ? " required" : "";
|
|
12587
|
+
const name = escapeHtml(field.name);
|
|
12588
|
+
const value = escapeHtml(field.value);
|
|
12589
|
+
let control;
|
|
12590
|
+
switch (field.widget) {
|
|
12591
|
+
case "checkbox":
|
|
12592
|
+
control = `<input type="checkbox" name="${name}" value="true"${field.checked ? " checked" : ""}>`;
|
|
12593
|
+
break;
|
|
12594
|
+
case "textarea":
|
|
12595
|
+
case "json":
|
|
12596
|
+
control = `<textarea name="${name}" rows="2"${required}>${value}</textarea>`;
|
|
12597
|
+
break;
|
|
12598
|
+
case "select": {
|
|
12599
|
+
const blank = field.required ? "" : '<option value="">\u2014 none \u2014</option>';
|
|
12600
|
+
const options = field.options.map(
|
|
12601
|
+
(option) => `<option value="${escapeHtml(option.value)}"${option.value === field.value ? " selected" : ""}>${escapeHtml(option.label)}</option>`
|
|
12602
|
+
).join("");
|
|
12603
|
+
control = `<select name="${name}"${required}>${blank}${options}</select>`;
|
|
12604
|
+
break;
|
|
12605
|
+
}
|
|
12606
|
+
case "number":
|
|
12607
|
+
control = `<input type="number" name="${name}" value="${value}"${field.step !== null ? ` step="${escapeHtml(field.step)}"` : ""}${required}>`;
|
|
12608
|
+
break;
|
|
12609
|
+
case "datetime":
|
|
12610
|
+
control = `<input type="datetime-local" name="${name}" value="${value}"${required}>`;
|
|
12611
|
+
break;
|
|
12612
|
+
case "date":
|
|
12613
|
+
control = `<input type="date" name="${name}" value="${value}"${required}>`;
|
|
12614
|
+
break;
|
|
12615
|
+
case "time":
|
|
12616
|
+
control = `<input type="time" name="${name}" value="${value}"${required}>`;
|
|
12617
|
+
break;
|
|
12618
|
+
default:
|
|
12619
|
+
control = `<input type="text" name="${name}" value="${value}"${required}>`;
|
|
12620
|
+
}
|
|
12621
|
+
const error = field.error !== null ? `<small class="tempest-admin-form__field-error">${escapeHtml(field.error)}</small>` : "";
|
|
12622
|
+
return `${control}${error}`;
|
|
12623
|
+
}
|
|
12624
|
+
function renderInline(inline, csrfToken, error) {
|
|
12625
|
+
const heading = `<header class="tempest-admin-inline__header">
|
|
12626
|
+
<h2>${escapeHtml(inline.label)}${inline.total > 0 ? ` <span class="tempest-admin-inline__count">(${escapeHtml(inline.total)})</span>` : ""}</h2>
|
|
12627
|
+
${inline.addUrl !== null ? `<a class="tempest-admin-btn" href="${escapeHtml(inline.addUrl)}">Add</a>` : ""}
|
|
12628
|
+
</header>`;
|
|
12629
|
+
const more = inline.truncated ? `<p class="tempest-admin-inline__more"><em>Showing the first ${escapeHtml(inline.rows.length)} of ${escapeHtml(inline.total)}.</em></p>` : "";
|
|
12630
|
+
if (!inline.editable) {
|
|
12631
|
+
if (inline.rows.length === 0) {
|
|
12632
|
+
return `<section class="tempest-admin-inline">${heading}<p><em>No related records.</em></p></section>`;
|
|
12633
|
+
}
|
|
12634
|
+
const head2 = `<tr>${inline.columns.map((column6) => `<th>${escapeHtml(column6)}</th>`).join("")}<th></th></tr>`;
|
|
12635
|
+
const body = inline.rows.map(
|
|
12636
|
+
(row) => `<tr>${row.cells.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}<td>${row.url === null ? "" : `<a href="${escapeHtml(row.url)}">View</a>`}</td></tr>`
|
|
12637
|
+
).join("");
|
|
12638
|
+
return `<section class="tempest-admin-inline">
|
|
12639
|
+
${heading}
|
|
12640
|
+
<div class="tempest-admin-inline__scroll">
|
|
12641
|
+
<table class="tempest-admin-inline__table">
|
|
12642
|
+
<thead>${head2}</thead>
|
|
12643
|
+
<tbody>${body}</tbody>
|
|
12644
|
+
</table>
|
|
12645
|
+
</div>
|
|
12646
|
+
${more}
|
|
12647
|
+
</section>`;
|
|
12648
|
+
}
|
|
12649
|
+
const head = `<tr>${inline.columns.map((column6) => `<th>${escapeHtml(column6)}</th>`).join("")}${inline.canDelete ? "<th>Delete</th>" : ""}</tr>`;
|
|
12650
|
+
const renderRow = (row, isNew) => `<tr${isNew ? ' class="tempest-admin-inline__new"' : ""}>${row.fields.map((field) => `<td>${renderInlineCell(field)}</td>`).join("")}${inline.canDelete ? `<td class="tempest-admin-inline__del">${isNew ? "" : `<input type="checkbox" name="row.${escapeHtml(row.key)}.__delete" value="true">`}</td>` : ""}</tr>`;
|
|
12651
|
+
const rows = inline.rows.map((row) => renderRow(row, false)).join("");
|
|
12652
|
+
const blank = inline.newRow === null ? "" : renderRow(inline.newRow, true);
|
|
12653
|
+
return `<section class="tempest-admin-inline">
|
|
12654
|
+
${heading}
|
|
12655
|
+
${error !== null ? `<p class="tempest-admin-form__error">${escapeHtml(error)}</p>` : ""}
|
|
12656
|
+
<form method="post" action="${escapeHtml(inline.formAction)}" class="tempest-admin-inline__form">
|
|
12657
|
+
<input type="hidden" name="csrf_token" value="${escapeHtml(csrfToken)}">
|
|
12658
|
+
<div class="tempest-admin-inline__scroll">
|
|
12659
|
+
<table class="tempest-admin-inline__table">
|
|
12660
|
+
<thead>${head}</thead>
|
|
12661
|
+
<tbody>${rows}${blank}</tbody>
|
|
12662
|
+
</table>
|
|
12663
|
+
</div>
|
|
12664
|
+
<div class="tempest-admin-form__actions">
|
|
12665
|
+
<button type="submit">Save ${escapeHtml(inline.label)}</button>
|
|
12666
|
+
</div>
|
|
12667
|
+
</form>
|
|
12668
|
+
${more}
|
|
12669
|
+
</section>`;
|
|
12670
|
+
}
|
|
12671
|
+
function renderAuditPanel(audit2) {
|
|
12672
|
+
const rows = audit2.fields.map(
|
|
12368
12673
|
(field) => `<dt>${escapeHtml(field.label)}</dt><dd>${field.value === "" ? "<em>\u2014</em>" : escapeHtml(field.value)}</dd>`
|
|
12369
12674
|
).join("");
|
|
12370
|
-
const history =
|
|
12675
|
+
const history = audit2.history.length > 0 ? `<ol class="tempest-admin-history">${audit2.history.map((entry) => {
|
|
12371
12676
|
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(
|
|
12372
12677
|
(change) => `<tr><td>${escapeHtml(change.field)}</td><td>${escapeHtml(change.before)}</td><td>${escapeHtml(change.after)}</td></tr>`
|
|
12373
12678
|
).join("")}</tbody></table>` : "<p><em>No field changes recorded.</em></p>";
|
|
@@ -12465,6 +12770,86 @@ function renderFormPage(context, view) {
|
|
|
12465
12770
|
</section>`;
|
|
12466
12771
|
return renderLayout(context, `${heading} \xB7 ${context.site.title}`, body);
|
|
12467
12772
|
}
|
|
12773
|
+
function renderLogsPage(context, view) {
|
|
12774
|
+
const options = view.sources.map(
|
|
12775
|
+
(source) => `<option value="${escapeHtml(source.value)}"${source.selected ? " selected" : ""}>${escapeHtml(source.label)}</option>`
|
|
12776
|
+
).join("");
|
|
12777
|
+
const rows = view.rows.length > 0 ? view.rows.map((row) => {
|
|
12778
|
+
const context_ = row.context.map(
|
|
12779
|
+
(entry) => `<span class="tempest-admin-logs__meta"><b>${escapeHtml(entry.key)}</b>: ${escapeHtml(entry.value)}</span>`
|
|
12780
|
+
).join(" ");
|
|
12781
|
+
const message = row.stack === null ? `${escapeHtml(row.message)}${context_ === "" ? "" : `<br>${context_}`}` : `<details><summary>${escapeHtml(row.message)}</summary>${context_ === "" ? "" : `<p>${context_}</p>`}<pre>${escapeHtml(row.stack)}</pre></details>`;
|
|
12782
|
+
return `<tr>
|
|
12783
|
+
<td data-label="Level"><span class="tempest-log-badge tempest-log-badge--${escapeHtml(row.level)}">${escapeHtml(row.level)}</span></td>
|
|
12784
|
+
<td data-label="When">${escapeHtml(row.timestamp)}</td>
|
|
12785
|
+
<td data-label="Logger">${escapeHtml(row.logger)}</td>
|
|
12786
|
+
<td data-label="Message">${message}</td>
|
|
12787
|
+
</tr>`;
|
|
12788
|
+
}).join("") : `<tr><td colspan="4">No log records yet. Point <code>configureFileLogging</code> at the same directory to populate this page.</td></tr>`;
|
|
12789
|
+
const body = `<section class="tempest-admin-logs">
|
|
12790
|
+
<header class="tempest-admin-list__header">
|
|
12791
|
+
<h1>Logs</h1>
|
|
12792
|
+
<p>${escapeHtml(view.total)} record${view.total === 1 ? "" : "s"}.</p>
|
|
12793
|
+
</header>
|
|
12794
|
+
<div class="tempest-admin-list__toolbar">
|
|
12795
|
+
<form method="get" class="tempest-admin-list__filters">
|
|
12796
|
+
<label><span>Source</span><select name="source">${options}</select></label>
|
|
12797
|
+
<input type="search" name="q" value="${escapeHtml(view.query)}" placeholder="Search\u2026" aria-label="Search">
|
|
12798
|
+
<button type="submit">Apply</button>
|
|
12799
|
+
</form>
|
|
12800
|
+
<div class="tempest-admin-list__actions">
|
|
12801
|
+
<a href="${escapeHtml(view.exportMarkdownUrl)}">Export Markdown</a>
|
|
12802
|
+
<a href="${escapeHtml(view.exportJsonUrl)}">Export JSON</a>
|
|
12803
|
+
</div>
|
|
12804
|
+
</div>
|
|
12805
|
+
<div class="tempest-admin-table-wrap">
|
|
12806
|
+
<table class="tempest-admin-list__table">
|
|
12807
|
+
<thead><tr><th>Level</th><th>When</th><th>Logger</th><th>Message</th></tr></thead>
|
|
12808
|
+
<tbody>${rows}</tbody>
|
|
12809
|
+
</table>
|
|
12810
|
+
</div>
|
|
12811
|
+
${view.pages > 1 ? `<nav class="tempest-admin-list__pagination" aria-label="Pagination">
|
|
12812
|
+
${view.prevUrl !== null ? `<a href="${escapeHtml(view.prevUrl)}">\u2190 Prev</a>` : ""}
|
|
12813
|
+
<span>Page ${escapeHtml(view.page)} of ${escapeHtml(view.pages)}</span>
|
|
12814
|
+
${view.nextUrl !== null ? `<a href="${escapeHtml(view.nextUrl)}">Next \u2192</a>` : ""}
|
|
12815
|
+
</nav>` : ""}
|
|
12816
|
+
<p class="tempest-admin-form__hint">Exports carry at most ${escapeHtml(view.exportMax)} records, newest first, honouring the filters above.</p>
|
|
12817
|
+
</section>`;
|
|
12818
|
+
return renderLayout(context, `Logs \xB7 ${context.site.title}`, body);
|
|
12819
|
+
}
|
|
12820
|
+
function renderSqlPage(context, view) {
|
|
12821
|
+
if (context.session === null) throw new Error("The SQL console requires a session");
|
|
12822
|
+
const result = view.rowCount === null ? "" : `<p class="tempest-admin-form__hint">${escapeHtml(view.rowCount)} row${view.rowCount === 1 ? "" : "s"}${view.durationMs === null ? "" : ` in ${escapeHtml(view.durationMs)} ms`}${view.truncated ? " (truncated by the row cap)" : ""}.</p>
|
|
12823
|
+
<div class="tempest-admin-table-wrap">
|
|
12824
|
+
<table class="tempest-admin-list__table">
|
|
12825
|
+
<thead><tr>${view.columns.map((column6) => `<th>${escapeHtml(column6)}</th>`).join("")}</tr></thead>
|
|
12826
|
+
<tbody>${view.rows.map(
|
|
12827
|
+
(row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`
|
|
12828
|
+
).join("")}</tbody>
|
|
12829
|
+
</table>
|
|
12830
|
+
</div>`;
|
|
12831
|
+
const body = `<section class="tempest-admin-sql">
|
|
12832
|
+
<header class="tempest-admin-list__header">
|
|
12833
|
+
<h1>SQL console</h1>
|
|
12834
|
+
<p>Capabilities: <code>${escapeHtml(view.capabilities.join(", "))}</code></p>
|
|
12835
|
+
</header>
|
|
12836
|
+
${view.error !== null ? `<p class="tempest-admin-form__error">${escapeHtml(view.error)}</p>` : ""}
|
|
12837
|
+
<form method="post" action="${escapeHtml(`${context.prefix}/sql`)}" class="tempest-admin-form__form">
|
|
12838
|
+
<input type="hidden" name="csrf_token" value="${escapeHtml(context.session.csrfToken)}">
|
|
12839
|
+
<div class="tempest-admin-form__field">
|
|
12840
|
+
<label>
|
|
12841
|
+
<span>Statement</span>
|
|
12842
|
+
<textarea name="sql" rows="6" spellcheck="false" required>${escapeHtml(view.sql)}</textarea>
|
|
12843
|
+
</label>
|
|
12844
|
+
</div>
|
|
12845
|
+
<div class="tempest-admin-form__actions">
|
|
12846
|
+
<button type="submit">Run</button>
|
|
12847
|
+
</div>
|
|
12848
|
+
</form>
|
|
12849
|
+
${result}
|
|
12850
|
+
</section>`;
|
|
12851
|
+
return renderLayout(context, `SQL console \xB7 ${context.site.title}`, body);
|
|
12852
|
+
}
|
|
12468
12853
|
function renderImportPage(context, view) {
|
|
12469
12854
|
if (context.session === null) throw new Error("The import page requires a session");
|
|
12470
12855
|
const summary = view.created === null ? "" : `<p class="tempest-admin-import__summary">Created ${escapeHtml(view.created)} record${view.created === 1 ? "" : "s"}.</p>`;
|
|
@@ -12550,7 +12935,67 @@ var AUTOCOMPLETE_SCRIPT = `<script>
|
|
|
12550
12935
|
});
|
|
12551
12936
|
})();
|
|
12552
12937
|
</script>`;
|
|
12938
|
+
function filesFor(dir, source) {
|
|
12939
|
+
if (source === "500") return [path.join(dir, HTTP_500_LOG_FILE)];
|
|
12940
|
+
if (source === "all") return Object.values(LEVEL_LOG_FILES).map((f) => path.join(dir, f));
|
|
12941
|
+
return [path.join(dir, LEVEL_LOG_FILES[source])];
|
|
12942
|
+
}
|
|
12943
|
+
async function readLogEntries(dir, source) {
|
|
12944
|
+
return await readEntries(filesFor(dir, source));
|
|
12945
|
+
}
|
|
12946
|
+
async function readEntries(files) {
|
|
12947
|
+
const entries = [];
|
|
12948
|
+
for (const file of files) {
|
|
12949
|
+
let content;
|
|
12950
|
+
try {
|
|
12951
|
+
content = await promises.readFile(file, "utf8");
|
|
12952
|
+
} catch {
|
|
12953
|
+
continue;
|
|
12954
|
+
}
|
|
12955
|
+
for (const line of content.split("\n")) {
|
|
12956
|
+
if (!line.trim()) continue;
|
|
12957
|
+
try {
|
|
12958
|
+
entries.push(JSON.parse(line));
|
|
12959
|
+
} catch {
|
|
12960
|
+
}
|
|
12961
|
+
}
|
|
12962
|
+
}
|
|
12963
|
+
return entries;
|
|
12964
|
+
}
|
|
12965
|
+
function makeLogsRouter(options) {
|
|
12966
|
+
const router = express3.Router();
|
|
12967
|
+
const path = options.path ?? "/logs";
|
|
12968
|
+
const guards = options.guards ?? [];
|
|
12969
|
+
const handler = (req, res, next) => {
|
|
12970
|
+
const source = req.query.source ?? "all";
|
|
12971
|
+
const validSources = ["all", "debug", "info", "warning", "error", "500"];
|
|
12972
|
+
if (!validSources.includes(source)) {
|
|
12973
|
+
res.status(422).json({ detail: "Invalid log source", code: "VALIDATION_ERROR", details: {} });
|
|
12974
|
+
return;
|
|
12975
|
+
}
|
|
12976
|
+
const page2 = Math.max(1, Number(req.query.page) || 1);
|
|
12977
|
+
const pageSize = Math.min(500, Math.max(1, Number(req.query.pageSize) || 50));
|
|
12978
|
+
readEntries(filesFor(options.dir, source)).then((entries) => {
|
|
12979
|
+
entries.reverse();
|
|
12980
|
+
const total = entries.length;
|
|
12981
|
+
const start = (page2 - 1) * pageSize;
|
|
12982
|
+
res.json({
|
|
12983
|
+
items: entries.slice(start, start + pageSize),
|
|
12984
|
+
total,
|
|
12985
|
+
page: page2,
|
|
12986
|
+
pageSize,
|
|
12987
|
+
pages: Math.ceil(total / pageSize)
|
|
12988
|
+
});
|
|
12989
|
+
}).catch(next);
|
|
12990
|
+
};
|
|
12991
|
+
router.get(path, ...guards, handler);
|
|
12992
|
+
return router;
|
|
12993
|
+
}
|
|
12553
12994
|
var logger2 = new JSONLogger("tempest_express_sdk.admin.router");
|
|
12995
|
+
var LOG_SOURCES = ["all", "debug", "info", "warning", "error", "500"];
|
|
12996
|
+
var LOG_PAGE_SIZE = 50;
|
|
12997
|
+
var LOG_EXPORT_MAX = 500;
|
|
12998
|
+
var INLINE_ROW_LIMIT = 50;
|
|
12554
12999
|
var AUTOCOMPLETE_LIMIT = 20;
|
|
12555
13000
|
var AUDIT_HISTORY_LIMIT = 50;
|
|
12556
13001
|
var FK_OPTION_CAP = 1e3;
|
|
@@ -12581,6 +13026,13 @@ function makeAdminRouter(site, options) {
|
|
|
12581
13026
|
const theme = resolveAdminTheme(site.theme);
|
|
12582
13027
|
const showMetrics = options.showMetrics ?? true;
|
|
12583
13028
|
const exportMaxRows = options.exportMaxRows ?? 5e3;
|
|
13029
|
+
const systemNav = [];
|
|
13030
|
+
if (options.logDir !== void 0) {
|
|
13031
|
+
systemNav.push({ label: "Logs", url: `${prefix}/logs` });
|
|
13032
|
+
}
|
|
13033
|
+
if (options.sqlConsole !== void 0) {
|
|
13034
|
+
systemNav.push({ label: "SQL console", url: `${prefix}/sql` });
|
|
13035
|
+
}
|
|
12584
13036
|
const maxUploadBytes = options.maxUploadBytes ?? 10 * 1024 * 1024;
|
|
12585
13037
|
const sessions = new AdminSessionStore({
|
|
12586
13038
|
secret: options.secretKey,
|
|
@@ -12602,6 +13054,7 @@ function makeAdminRouter(site, options) {
|
|
|
12602
13054
|
label: admin.verboseNamePlural(),
|
|
12603
13055
|
url: `${prefix}/m/${admin.slug()}`
|
|
12604
13056
|
})),
|
|
13057
|
+
navSystem: systemNav,
|
|
12605
13058
|
messages: flashFor(req)
|
|
12606
13059
|
});
|
|
12607
13060
|
const allows = async (principal, admin, action) => {
|
|
@@ -12812,6 +13265,187 @@ function makeAdminRouter(site, options) {
|
|
|
12812
13265
|
);
|
|
12813
13266
|
})
|
|
12814
13267
|
);
|
|
13268
|
+
if (options.logDir !== void 0) {
|
|
13269
|
+
const logDir = options.logDir;
|
|
13270
|
+
router.get(
|
|
13271
|
+
`${prefix}/logs`,
|
|
13272
|
+
guarded(async (req, res) => {
|
|
13273
|
+
const state = await authenticate(req, res);
|
|
13274
|
+
if (state === null) return;
|
|
13275
|
+
const { entries, source, search } = await readLogs(req, logDir);
|
|
13276
|
+
const page2 = Math.max(1, Number.parseInt(queryString(req.query.page), 10) || 1);
|
|
13277
|
+
const start = (page2 - 1) * LOG_PAGE_SIZE;
|
|
13278
|
+
const pages = Math.max(1, Math.ceil(entries.length / LOG_PAGE_SIZE));
|
|
13279
|
+
const exportUrl = (format) => `${prefix}/logs/export?${buildQuery({ source, q: search, format })}`;
|
|
13280
|
+
const pageUrl = (target) => `?${buildQuery({ source, q: search, page: target })}`;
|
|
13281
|
+
html(
|
|
13282
|
+
res,
|
|
13283
|
+
renderLogsPage(context(req, state.session, state.visible), {
|
|
13284
|
+
sources: LOG_SOURCES.map((value) => ({
|
|
13285
|
+
value,
|
|
13286
|
+
label: value === "500" ? "HTTP 500" : humanizeField(value),
|
|
13287
|
+
selected: value === source
|
|
13288
|
+
})),
|
|
13289
|
+
query: search,
|
|
13290
|
+
rows: entries.slice(start, start + LOG_PAGE_SIZE).map((entry) => ({
|
|
13291
|
+
level: entry.level,
|
|
13292
|
+
timestamp: entry.timestamp,
|
|
13293
|
+
logger: entry.logger,
|
|
13294
|
+
message: entry.message,
|
|
13295
|
+
stack: entry.stack,
|
|
13296
|
+
context: Object.entries(entry.context).map(([key, value]) => ({
|
|
13297
|
+
key,
|
|
13298
|
+
value: typeof value === "string" ? value : JSON.stringify(value)
|
|
13299
|
+
}))
|
|
13300
|
+
})),
|
|
13301
|
+
total: entries.length,
|
|
13302
|
+
page: page2,
|
|
13303
|
+
pages,
|
|
13304
|
+
prevUrl: page2 > 1 ? pageUrl(page2 - 1) : null,
|
|
13305
|
+
nextUrl: page2 < pages ? pageUrl(page2 + 1) : null,
|
|
13306
|
+
exportMarkdownUrl: exportUrl("md"),
|
|
13307
|
+
exportJsonUrl: exportUrl("json"),
|
|
13308
|
+
exportMax: LOG_EXPORT_MAX
|
|
13309
|
+
})
|
|
13310
|
+
);
|
|
13311
|
+
})
|
|
13312
|
+
);
|
|
13313
|
+
router.get(
|
|
13314
|
+
`${prefix}/logs/export`,
|
|
13315
|
+
guarded(async (req, res) => {
|
|
13316
|
+
const state = await authenticate(req, res);
|
|
13317
|
+
if (state === null) return;
|
|
13318
|
+
const format = queryString(req.query.format) === "json" ? "json" : "md";
|
|
13319
|
+
const { entries, source, search } = await readLogs(req, logDir);
|
|
13320
|
+
const window = entries.slice(0, LOG_EXPORT_MAX);
|
|
13321
|
+
const payload = format === "json" ? renderLogEntriesJson(window) : renderLogEntriesMarkdown(window, {
|
|
13322
|
+
source,
|
|
13323
|
+
query: search,
|
|
13324
|
+
total: entries.length
|
|
13325
|
+
});
|
|
13326
|
+
res.status(200).type(format === "json" ? "application/json" : "text/markdown; charset=utf-8").set("content-disposition", `attachment; filename="logs.${format}"`).send(payload);
|
|
13327
|
+
})
|
|
13328
|
+
);
|
|
13329
|
+
}
|
|
13330
|
+
if (options.sqlConsole !== void 0) {
|
|
13331
|
+
const console_ = options.sqlConsole;
|
|
13332
|
+
const policy = console_.policy ?? {};
|
|
13333
|
+
const dialect = console_.dialect ?? "postgresql";
|
|
13334
|
+
const capabilities = policy.capabilities ?? [SqlCapability.READ];
|
|
13335
|
+
const maxRows = policy.maxRows ?? 200;
|
|
13336
|
+
router.get(
|
|
13337
|
+
`${prefix}/sql`,
|
|
13338
|
+
guarded(async (req, res) => {
|
|
13339
|
+
const state = await authenticate(req, res);
|
|
13340
|
+
if (state === null) return;
|
|
13341
|
+
html(
|
|
13342
|
+
res,
|
|
13343
|
+
renderSqlPage(context(req, state.session, state.visible), {
|
|
13344
|
+
sql: "",
|
|
13345
|
+
capabilities: [...capabilities],
|
|
13346
|
+
error: null,
|
|
13347
|
+
columns: [],
|
|
13348
|
+
rows: [],
|
|
13349
|
+
rowCount: null,
|
|
13350
|
+
truncated: false,
|
|
13351
|
+
durationMs: null
|
|
13352
|
+
})
|
|
13353
|
+
);
|
|
13354
|
+
})
|
|
13355
|
+
);
|
|
13356
|
+
router.post(
|
|
13357
|
+
`${prefix}/sql`,
|
|
13358
|
+
guarded(async (req, res) => {
|
|
13359
|
+
const state = await authenticate(req, res);
|
|
13360
|
+
if (state === null) return;
|
|
13361
|
+
if (!checkCsrf(req, res, state)) return;
|
|
13362
|
+
const body = req.body;
|
|
13363
|
+
const sql5 = typeof body.sql === "string" ? body.sql : "";
|
|
13364
|
+
const principal = backend.displayName(state.principal);
|
|
13365
|
+
const render = (view) => {
|
|
13366
|
+
html(
|
|
13367
|
+
res,
|
|
13368
|
+
renderSqlPage(context(req, state.session, state.visible), {
|
|
13369
|
+
sql: sql5,
|
|
13370
|
+
capabilities: [...capabilities],
|
|
13371
|
+
...view
|
|
13372
|
+
}),
|
|
13373
|
+
view.error === null ? 200 : 400
|
|
13374
|
+
);
|
|
13375
|
+
};
|
|
13376
|
+
const parser = await loadSqlParser();
|
|
13377
|
+
const analysis = analyzeSql(sql5, dialect, parser);
|
|
13378
|
+
const verdict = checkSqlPolicy(analysis, policy);
|
|
13379
|
+
if (!verdict.allowed) {
|
|
13380
|
+
await audit(console_.onAudit, {
|
|
13381
|
+
sql: sql5,
|
|
13382
|
+
principal,
|
|
13383
|
+
allowed: false,
|
|
13384
|
+
reason: verdict.reason,
|
|
13385
|
+
analysis,
|
|
13386
|
+
durationMs: null,
|
|
13387
|
+
rowCount: null
|
|
13388
|
+
});
|
|
13389
|
+
render({
|
|
13390
|
+
error: verdict.reason,
|
|
13391
|
+
columns: [],
|
|
13392
|
+
rows: [],
|
|
13393
|
+
rowCount: null,
|
|
13394
|
+
truncated: false,
|
|
13395
|
+
durationMs: null
|
|
13396
|
+
});
|
|
13397
|
+
return;
|
|
13398
|
+
}
|
|
13399
|
+
const started = Date.now();
|
|
13400
|
+
let rows;
|
|
13401
|
+
try {
|
|
13402
|
+
rows = console_.run === void 0 ? await state.dbSession.raw(sql5).all() : await console_.run(sql5, state.dbSession);
|
|
13403
|
+
} catch (error) {
|
|
13404
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13405
|
+
await audit(console_.onAudit, {
|
|
13406
|
+
sql: sql5,
|
|
13407
|
+
principal,
|
|
13408
|
+
allowed: true,
|
|
13409
|
+
reason: message,
|
|
13410
|
+
analysis,
|
|
13411
|
+
durationMs: Date.now() - started,
|
|
13412
|
+
rowCount: null
|
|
13413
|
+
});
|
|
13414
|
+
render({
|
|
13415
|
+
error: message,
|
|
13416
|
+
columns: [],
|
|
13417
|
+
rows: [],
|
|
13418
|
+
rowCount: null,
|
|
13419
|
+
truncated: false,
|
|
13420
|
+
durationMs: null
|
|
13421
|
+
});
|
|
13422
|
+
return;
|
|
13423
|
+
}
|
|
13424
|
+
const durationMs = Date.now() - started;
|
|
13425
|
+
await audit(console_.onAudit, {
|
|
13426
|
+
sql: sql5,
|
|
13427
|
+
principal,
|
|
13428
|
+
allowed: true,
|
|
13429
|
+
reason: null,
|
|
13430
|
+
analysis,
|
|
13431
|
+
durationMs,
|
|
13432
|
+
rowCount: rows.length
|
|
13433
|
+
});
|
|
13434
|
+
const window = rows.slice(0, maxRows);
|
|
13435
|
+
const columns = window.length === 0 ? [] : Object.keys(window[0] ?? {});
|
|
13436
|
+
render({
|
|
13437
|
+
error: null,
|
|
13438
|
+
columns,
|
|
13439
|
+
rows: window.map(
|
|
13440
|
+
(row) => columns.map((column6) => formatCellValue(row[column6]))
|
|
13441
|
+
),
|
|
13442
|
+
rowCount: rows.length,
|
|
13443
|
+
truncated: rows.length > window.length,
|
|
13444
|
+
durationMs
|
|
13445
|
+
});
|
|
13446
|
+
})
|
|
13447
|
+
);
|
|
13448
|
+
}
|
|
12815
13449
|
router.get(
|
|
12816
13450
|
`${prefix}/m/:slug`,
|
|
12817
13451
|
guarded(async (req, res) => {
|
|
@@ -13167,6 +13801,8 @@ function makeAdminRouter(site, options) {
|
|
|
13167
13801
|
title: admin.verboseName(),
|
|
13168
13802
|
identity,
|
|
13169
13803
|
audit: await buildAuditView(admin, row, state.dbSession),
|
|
13804
|
+
inlines: await buildInlines(admin, row, state, identity),
|
|
13805
|
+
inlineError: null,
|
|
13170
13806
|
fields: admin.detailFieldNames().map((name) => ({ label: name, value: formatCellValue(row[name]) })),
|
|
13171
13807
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
13172
13808
|
editUrl: await allows(state.principal, admin, AdminPermission.EDIT) ? `${prefix}/m/${admin.slug()}/${identity}/edit` : null,
|
|
@@ -13287,6 +13923,103 @@ function makeAdminRouter(site, options) {
|
|
|
13287
13923
|
res.redirect(`${prefix}/m/${admin.slug()}/${identity}?ok=updated`);
|
|
13288
13924
|
})
|
|
13289
13925
|
);
|
|
13926
|
+
router.post(
|
|
13927
|
+
`${prefix}/m/:slug/:identity/inlines/:child`,
|
|
13928
|
+
guarded(async (req, res) => {
|
|
13929
|
+
const state = await authenticate(req, res);
|
|
13930
|
+
if (state === null) return;
|
|
13931
|
+
const admin = await resolveAdmin(req, res, state);
|
|
13932
|
+
if (admin === null) return;
|
|
13933
|
+
if (!checkCsrf(req, res, state)) return;
|
|
13934
|
+
const childSlug = String(req.params.child);
|
|
13935
|
+
const inline = admin.inlines.find(
|
|
13936
|
+
(entry) => entry.slug === childSlug && entry.editable
|
|
13937
|
+
);
|
|
13938
|
+
const childAdmin = inline === void 0 ? null : site.get(childSlug);
|
|
13939
|
+
if (inline === void 0 || childAdmin === null || !await allows(state.principal, childAdmin, AdminPermission.EDIT)) {
|
|
13940
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
13941
|
+
return;
|
|
13942
|
+
}
|
|
13943
|
+
const identity = String(req.params.identity);
|
|
13944
|
+
const parent = await findRow(admin, state.dbSession, identity);
|
|
13945
|
+
if (parent === null) {
|
|
13946
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
13947
|
+
return;
|
|
13948
|
+
}
|
|
13949
|
+
const parentId = parent[admin.identityField];
|
|
13950
|
+
const { rows: grouped, deletions } = groupInlineSubmission(
|
|
13951
|
+
req.body
|
|
13952
|
+
);
|
|
13953
|
+
const names = inlineFieldNames(childAdmin, inline);
|
|
13954
|
+
const childRepo = childAdmin.repository(state.dbSession);
|
|
13955
|
+
const actorId = backend.principalId(state.principal);
|
|
13956
|
+
const canDelete = inline.canDelete && await allows(state.principal, childAdmin, AdminPermission.DELETE);
|
|
13957
|
+
const failed = [];
|
|
13958
|
+
let formError = null;
|
|
13959
|
+
const owned = async (key) => await childRepo.first({
|
|
13960
|
+
[childAdmin.identityField]: key,
|
|
13961
|
+
[inline.fkField]: parentId
|
|
13962
|
+
});
|
|
13963
|
+
for (const [key, values] of Object.entries(grouped)) {
|
|
13964
|
+
const isNew = key.startsWith("new");
|
|
13965
|
+
if (!isNew && canDelete && deletions.has(key)) {
|
|
13966
|
+
if (await owned(key) !== null) {
|
|
13967
|
+
await childRepo.delete({ [childAdmin.identityField]: key });
|
|
13968
|
+
}
|
|
13969
|
+
continue;
|
|
13970
|
+
}
|
|
13971
|
+
if (isNew && Object.values(values).every((value) => value.trim() === "")) {
|
|
13972
|
+
continue;
|
|
13973
|
+
}
|
|
13974
|
+
const parsed = parseFormBody(childAdmin, values, { only: names });
|
|
13975
|
+
if (Object.keys(parsed.errors).length > 0) {
|
|
13976
|
+
failed.push({ key, values, errors: parsed.errors });
|
|
13977
|
+
continue;
|
|
13978
|
+
}
|
|
13979
|
+
try {
|
|
13980
|
+
if (isNew) {
|
|
13981
|
+
stampActor(childAdmin, parsed.data, actorId, true);
|
|
13982
|
+
await childRepo.create({
|
|
13983
|
+
...parsed.data,
|
|
13984
|
+
[inline.fkField]: parentId
|
|
13985
|
+
});
|
|
13986
|
+
} else {
|
|
13987
|
+
if (await owned(key) === null) continue;
|
|
13988
|
+
stampActor(childAdmin, parsed.data, actorId, false);
|
|
13989
|
+
await childRepo.update(
|
|
13990
|
+
{ [childAdmin.identityField]: key },
|
|
13991
|
+
parsed.data
|
|
13992
|
+
);
|
|
13993
|
+
}
|
|
13994
|
+
} catch (error) {
|
|
13995
|
+
formError = describeWriteFailure(childAdmin, error);
|
|
13996
|
+
failed.push({ key, values, errors: {} });
|
|
13997
|
+
}
|
|
13998
|
+
}
|
|
13999
|
+
if (failed.length > 0) {
|
|
14000
|
+
const fresh = await findRow(admin, state.dbSession, identity) ?? parent;
|
|
14001
|
+
html(
|
|
14002
|
+
res,
|
|
14003
|
+
renderDetailPage(context(req, state.session, state.visible), {
|
|
14004
|
+
title: admin.verboseName(),
|
|
14005
|
+
identity,
|
|
14006
|
+
audit: await buildAuditView(admin, fresh, state.dbSession),
|
|
14007
|
+
inlines: await buildInlines(admin, fresh, state, identity, {
|
|
14008
|
+
[childSlug]: failed
|
|
14009
|
+
}),
|
|
14010
|
+
inlineError: formError ?? "Some inline rows could not be saved.",
|
|
14011
|
+
fields: admin.detailFieldNames().map((name) => ({ label: name, value: formatCellValue(fresh[name]) })),
|
|
14012
|
+
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
14013
|
+
editUrl: await allows(state.principal, admin, AdminPermission.EDIT) ? `${prefix}/m/${admin.slug()}/${identity}/edit` : null,
|
|
14014
|
+
deleteUrl: await allows(state.principal, admin, AdminPermission.DELETE) ? `${prefix}/m/${admin.slug()}/${identity}/delete` : null
|
|
14015
|
+
}),
|
|
14016
|
+
400
|
|
14017
|
+
);
|
|
14018
|
+
return;
|
|
14019
|
+
}
|
|
14020
|
+
res.redirect(`${prefix}/m/${admin.slug()}/${identity}?ok=updated`);
|
|
14021
|
+
})
|
|
14022
|
+
);
|
|
13290
14023
|
router.post(
|
|
13291
14024
|
`${prefix}/m/:slug/:identity/delete`,
|
|
13292
14025
|
guarded(async (req, res) => {
|
|
@@ -13308,6 +14041,89 @@ function makeAdminRouter(site, options) {
|
|
|
13308
14041
|
const principal = await backend.loadPrincipal(dbSession, String(actor));
|
|
13309
14042
|
return principal === null ? String(actor) : backend.displayName(principal);
|
|
13310
14043
|
}
|
|
14044
|
+
async function buildInlines(admin, parent, state, identity, overrides = {}) {
|
|
14045
|
+
const parentId = parent[admin.identityField];
|
|
14046
|
+
const blocks = [];
|
|
14047
|
+
for (const inline of admin.inlines) {
|
|
14048
|
+
const childAdmin = site.get(inline.slug);
|
|
14049
|
+
const repository = childAdmin === null ? new tempestDbJs.BaseRepository(inline.model, state.dbSession) : childAdmin.repository(state.dbSession);
|
|
14050
|
+
const children = await repository.list({
|
|
14051
|
+
[inline.fkField]: parentId
|
|
14052
|
+
});
|
|
14053
|
+
const columns = inline.listDisplay ?? childAdmin?.listDisplayNames() ?? Object.keys(adminColumns(inline.model));
|
|
14054
|
+
const label = inline.label ?? childAdmin?.verboseNamePlural() ?? humanizeField(inline.slug);
|
|
14055
|
+
const visible = children.slice(0, INLINE_ROW_LIMIT);
|
|
14056
|
+
const editable = inline.editable && childAdmin !== null && await allows(state.principal, childAdmin, AdminPermission.EDIT);
|
|
14057
|
+
const addUrl = childAdmin !== null && await allows(state.principal, childAdmin, AdminPermission.CREATE) ? `${prefix}/m/${inline.slug}/new` : null;
|
|
14058
|
+
if (!editable || childAdmin === null) {
|
|
14059
|
+
blocks.push({
|
|
14060
|
+
label,
|
|
14061
|
+
total: children.length,
|
|
14062
|
+
columns,
|
|
14063
|
+
editable: false,
|
|
14064
|
+
canDelete: false,
|
|
14065
|
+
addUrl,
|
|
14066
|
+
formAction: "",
|
|
14067
|
+
rows: visible.map((child) => ({
|
|
14068
|
+
key: String(child[childAdmin?.identityField ?? "id"]),
|
|
14069
|
+
cells: columns.map((column6) => formatCellValue(child[column6])),
|
|
14070
|
+
fields: [],
|
|
14071
|
+
url: childAdmin === null ? null : `${prefix}/m/${inline.slug}/${String(child[childAdmin.identityField])}`
|
|
14072
|
+
})),
|
|
14073
|
+
newRow: null,
|
|
14074
|
+
truncated: children.length > visible.length
|
|
14075
|
+
});
|
|
14076
|
+
continue;
|
|
14077
|
+
}
|
|
14078
|
+
const names = inlineFieldNames(childAdmin, inline);
|
|
14079
|
+
const submitted = overrides[inline.slug];
|
|
14080
|
+
const rows = [];
|
|
14081
|
+
if (submitted !== void 0) {
|
|
14082
|
+
for (const entry of submitted) {
|
|
14083
|
+
rows.push({
|
|
14084
|
+
key: entry.key,
|
|
14085
|
+
cells: [],
|
|
14086
|
+
fields: inlineFields(
|
|
14087
|
+
childAdmin,
|
|
14088
|
+
names,
|
|
14089
|
+
entry.key,
|
|
14090
|
+
entry.values,
|
|
14091
|
+
entry.errors
|
|
14092
|
+
),
|
|
14093
|
+
url: null
|
|
14094
|
+
});
|
|
14095
|
+
}
|
|
14096
|
+
} else {
|
|
14097
|
+
for (const child of visible) {
|
|
14098
|
+
const key = String(child[childAdmin.identityField]);
|
|
14099
|
+
rows.push({
|
|
14100
|
+
key,
|
|
14101
|
+
cells: [],
|
|
14102
|
+
fields: inlineFields(childAdmin, names, key, child, {}),
|
|
14103
|
+
url: `${prefix}/m/${inline.slug}/${key}`
|
|
14104
|
+
});
|
|
14105
|
+
}
|
|
14106
|
+
}
|
|
14107
|
+
blocks.push({
|
|
14108
|
+
label,
|
|
14109
|
+
total: children.length,
|
|
14110
|
+
columns: names.map(humanizeField),
|
|
14111
|
+
editable: true,
|
|
14112
|
+
canDelete: inline.canDelete && childAdmin.canDelete,
|
|
14113
|
+
addUrl,
|
|
14114
|
+
formAction: `${prefix}/m/${admin.slug()}/${identity}/inlines/${inline.slug}`,
|
|
14115
|
+
rows,
|
|
14116
|
+
newRow: {
|
|
14117
|
+
key: "new1",
|
|
14118
|
+
cells: [],
|
|
14119
|
+
fields: inlineFields(childAdmin, names, "new1", {}, {}),
|
|
14120
|
+
url: null
|
|
14121
|
+
},
|
|
14122
|
+
truncated: children.length > visible.length
|
|
14123
|
+
});
|
|
14124
|
+
}
|
|
14125
|
+
return blocks;
|
|
14126
|
+
}
|
|
13311
14127
|
async function buildAuditView(admin, row, dbSession) {
|
|
13312
14128
|
const fields = [];
|
|
13313
14129
|
for (const name of admin.auditFieldNames()) {
|
|
@@ -13347,6 +14163,15 @@ function makeAdminRouter(site, options) {
|
|
|
13347
14163
|
const row = await admin.repository(dbSession).first({ [admin.identityField]: identity });
|
|
13348
14164
|
return row ?? null;
|
|
13349
14165
|
}
|
|
14166
|
+
async function readLogs(req, dir) {
|
|
14167
|
+
const requested = queryString(req.query.source);
|
|
14168
|
+
const source = LOG_SOURCES.includes(requested) ? requested : "all";
|
|
14169
|
+
const search = queryString(req.query.q);
|
|
14170
|
+
const raw = await readLogEntries(dir, source);
|
|
14171
|
+
const entries = filterLogEntries(raw.map(toLogEntry), search);
|
|
14172
|
+
entries.reverse();
|
|
14173
|
+
return { entries, source, search };
|
|
14174
|
+
}
|
|
13350
14175
|
function renderImport(req, state, admin, error, created, rowErrors) {
|
|
13351
14176
|
return renderImportPage(context(req, state.session, state.visible), {
|
|
13352
14177
|
title: admin.verboseNamePlural(),
|
|
@@ -13606,6 +14431,45 @@ function makeAdminRouter(site, options) {
|
|
|
13606
14431
|
}
|
|
13607
14432
|
return router;
|
|
13608
14433
|
}
|
|
14434
|
+
function groupInlineSubmission(body) {
|
|
14435
|
+
const rows = {};
|
|
14436
|
+
const deletions = /* @__PURE__ */ new Set();
|
|
14437
|
+
for (const [key, raw] of Object.entries(body)) {
|
|
14438
|
+
if (!key.startsWith("row.")) continue;
|
|
14439
|
+
const parts = key.split(".");
|
|
14440
|
+
if (parts.length !== 3) continue;
|
|
14441
|
+
const [, rowKey, field] = parts;
|
|
14442
|
+
const value = typeof raw === "string" ? raw : "";
|
|
14443
|
+
if (field === "__delete") {
|
|
14444
|
+
if (!["", "false", "off", "0", "no"].includes(value.trim().toLowerCase())) {
|
|
14445
|
+
deletions.add(rowKey);
|
|
14446
|
+
}
|
|
14447
|
+
continue;
|
|
14448
|
+
}
|
|
14449
|
+
const row = rows[rowKey] ?? {};
|
|
14450
|
+
row[field] = value;
|
|
14451
|
+
rows[rowKey] = row;
|
|
14452
|
+
}
|
|
14453
|
+
return { rows, deletions };
|
|
14454
|
+
}
|
|
14455
|
+
function inlineFieldNames(childAdmin, inline) {
|
|
14456
|
+
return childAdmin.editableFieldNames().filter(
|
|
14457
|
+
(name) => name !== inline.fkField && !childAdmin.uploadFields.includes(name) && !childAdmin.autocompleteFields.includes(name)
|
|
14458
|
+
);
|
|
14459
|
+
}
|
|
14460
|
+
function inlineFields(childAdmin, names, key, values, errors) {
|
|
14461
|
+
return buildFormFields(childAdmin, { values, errors }).filter((field) => names.includes(field.name)).map((field) => ({ ...field, name: `row.${key}.${field.name}` }));
|
|
14462
|
+
}
|
|
14463
|
+
async function audit(hook, entry) {
|
|
14464
|
+
if (hook === void 0) return;
|
|
14465
|
+
try {
|
|
14466
|
+
await hook(entry);
|
|
14467
|
+
} catch (error) {
|
|
14468
|
+
logger2.error("Admin SQL audit hook failed", {
|
|
14469
|
+
error: error instanceof Error ? error.message : String(error)
|
|
14470
|
+
});
|
|
14471
|
+
}
|
|
14472
|
+
}
|
|
13609
14473
|
function flagAllows(admin, action) {
|
|
13610
14474
|
if (action === AdminPermission.CREATE) return admin.canCreate;
|
|
13611
14475
|
if (action === AdminPermission.EDIT) return admin.canEdit;
|
|
@@ -15692,59 +16556,6 @@ function makeToolSpecRouter(spec, options = {}) {
|
|
|
15692
16556
|
});
|
|
15693
16557
|
return router;
|
|
15694
16558
|
}
|
|
15695
|
-
function filesFor(dir, source) {
|
|
15696
|
-
if (source === "500") return [path.join(dir, HTTP_500_LOG_FILE)];
|
|
15697
|
-
if (source === "all") return Object.values(LEVEL_LOG_FILES).map((f) => path.join(dir, f));
|
|
15698
|
-
return [path.join(dir, LEVEL_LOG_FILES[source])];
|
|
15699
|
-
}
|
|
15700
|
-
async function readEntries(files) {
|
|
15701
|
-
const entries = [];
|
|
15702
|
-
for (const file of files) {
|
|
15703
|
-
let content;
|
|
15704
|
-
try {
|
|
15705
|
-
content = await promises.readFile(file, "utf8");
|
|
15706
|
-
} catch {
|
|
15707
|
-
continue;
|
|
15708
|
-
}
|
|
15709
|
-
for (const line of content.split("\n")) {
|
|
15710
|
-
if (!line.trim()) continue;
|
|
15711
|
-
try {
|
|
15712
|
-
entries.push(JSON.parse(line));
|
|
15713
|
-
} catch {
|
|
15714
|
-
}
|
|
15715
|
-
}
|
|
15716
|
-
}
|
|
15717
|
-
return entries;
|
|
15718
|
-
}
|
|
15719
|
-
function makeLogsRouter(options) {
|
|
15720
|
-
const router = express3.Router();
|
|
15721
|
-
const path = options.path ?? "/logs";
|
|
15722
|
-
const guards = options.guards ?? [];
|
|
15723
|
-
const handler = (req, res, next) => {
|
|
15724
|
-
const source = req.query.source ?? "all";
|
|
15725
|
-
const validSources = ["all", "debug", "info", "warning", "error", "500"];
|
|
15726
|
-
if (!validSources.includes(source)) {
|
|
15727
|
-
res.status(422).json({ detail: "Invalid log source", code: "VALIDATION_ERROR", details: {} });
|
|
15728
|
-
return;
|
|
15729
|
-
}
|
|
15730
|
-
const page2 = Math.max(1, Number(req.query.page) || 1);
|
|
15731
|
-
const pageSize = Math.min(500, Math.max(1, Number(req.query.pageSize) || 50));
|
|
15732
|
-
readEntries(filesFor(options.dir, source)).then((entries) => {
|
|
15733
|
-
entries.reverse();
|
|
15734
|
-
const total = entries.length;
|
|
15735
|
-
const start = (page2 - 1) * pageSize;
|
|
15736
|
-
res.json({
|
|
15737
|
-
items: entries.slice(start, start + pageSize),
|
|
15738
|
-
total,
|
|
15739
|
-
page: page2,
|
|
15740
|
-
pageSize,
|
|
15741
|
-
pages: Math.ceil(total / pageSize)
|
|
15742
|
-
});
|
|
15743
|
-
}).catch(next);
|
|
15744
|
-
};
|
|
15745
|
-
router.get(path, ...guards, handler);
|
|
15746
|
-
return router;
|
|
15747
|
-
}
|
|
15748
16559
|
function createTestDatabase(models) {
|
|
15749
16560
|
const driver = tempestDbJs.NodeSqliteDriver.open(":memory:");
|
|
15750
16561
|
for (const model of models) {
|
|
@@ -15777,7 +16588,7 @@ async function withTestDatabase(models, fn) {
|
|
|
15777
16588
|
}
|
|
15778
16589
|
|
|
15779
16590
|
// src/version.ts
|
|
15780
|
-
var VERSION = "0.
|
|
16591
|
+
var VERSION = "0.29.0";
|
|
15781
16592
|
|
|
15782
16593
|
Object.defineProperty(exports, "OpenAPIRegistry", {
|
|
15783
16594
|
enumerable: true,
|
|
@@ -16014,6 +16825,7 @@ exports.S3UploadStorage = S3UploadStorage;
|
|
|
16014
16825
|
exports.SSEBroker = SSEBroker;
|
|
16015
16826
|
exports.ServerSentEvent = ServerSentEvent;
|
|
16016
16827
|
exports.SessionService = SessionService;
|
|
16828
|
+
exports.SqlCapability = SqlCapability;
|
|
16017
16829
|
exports.TOTPHelper = TOTPHelper;
|
|
16018
16830
|
exports.TaskManager = TaskManager;
|
|
16019
16831
|
exports.TelegramProvider = TelegramProvider;
|
|
@@ -16037,8 +16849,10 @@ exports.activationSchema = activationSchema;
|
|
|
16037
16849
|
exports.addLogSink = addLogSink;
|
|
16038
16850
|
exports.adminAction = adminAction;
|
|
16039
16851
|
exports.adminColumns = adminColumns;
|
|
16852
|
+
exports.adminInline = adminInline;
|
|
16040
16853
|
exports.adminLens = adminLens;
|
|
16041
16854
|
exports.adminThemeCss = adminThemeCss;
|
|
16855
|
+
exports.analyzeSql = analyzeSql;
|
|
16042
16856
|
exports.attachWebSocketHub = attachWebSocketHub;
|
|
16043
16857
|
exports.authResponseSchema = authResponseSchema;
|
|
16044
16858
|
exports.authSettingsShape = authSettingsShape;
|
|
@@ -16055,6 +16869,7 @@ exports.buildPaginationLinkHeader = buildPaginationLinkHeader;
|
|
|
16055
16869
|
exports.cached = cached3;
|
|
16056
16870
|
exports.centsField = centsField;
|
|
16057
16871
|
exports.cepField = cepField;
|
|
16872
|
+
exports.checkSqlPolicy = checkSqlPolicy;
|
|
16058
16873
|
exports.citiesByUf = citiesByUf;
|
|
16059
16874
|
exports.cnpjField = cnpjField;
|
|
16060
16875
|
exports.coerceFlag = coerceFlag;
|
|
@@ -16083,6 +16898,7 @@ exports.envBoolean = looseBoolean;
|
|
|
16083
16898
|
exports.envList = envList;
|
|
16084
16899
|
exports.escapeHtml = escapeHtml;
|
|
16085
16900
|
exports.filterForColumn = filterForColumn;
|
|
16901
|
+
exports.filterLogEntries = filterLogEntries;
|
|
16086
16902
|
exports.foreignKeyFields = foreignKeyFields;
|
|
16087
16903
|
exports.foreignKeyLabel = foreignKeyLabel;
|
|
16088
16904
|
exports.foreignKeyTable = foreignKeyTable;
|
|
@@ -16098,6 +16914,7 @@ exports.getConditions = getConditions;
|
|
|
16098
16914
|
exports.getPaginationConditions = getPaginationConditions;
|
|
16099
16915
|
exports.getRequestId = getRequestId;
|
|
16100
16916
|
exports.getState = getState;
|
|
16917
|
+
exports.groupInlineSubmission = groupInlineSubmission;
|
|
16101
16918
|
exports.hashOpaqueToken = hashOpaqueToken;
|
|
16102
16919
|
exports.hexColorField = hexColorField;
|
|
16103
16920
|
exports.humanizeField = humanizeField;
|
|
@@ -16121,6 +16938,7 @@ exports.keyByJwtSubject = keyByJwtSubject;
|
|
|
16121
16938
|
exports.latitudeField = latitudeField;
|
|
16122
16939
|
exports.listStates = listStates;
|
|
16123
16940
|
exports.loadSettings = loadSettings;
|
|
16941
|
+
exports.loadSqlParser = loadSqlParser;
|
|
16124
16942
|
exports.logEntrySchema = logEntrySchema;
|
|
16125
16943
|
exports.logSettingsShape = logSettingsShape;
|
|
16126
16944
|
exports.loginSchema = loginSchema;
|
|
@@ -16181,6 +16999,7 @@ exports.rabbitmqSettingsShape = rabbitmqSettingsShape;
|
|
|
16181
16999
|
exports.rateLimitMiddleware = rateLimitMiddleware;
|
|
16182
17000
|
exports.ratingField = ratingField;
|
|
16183
17001
|
exports.ratioField = ratioField;
|
|
17002
|
+
exports.readLogEntries = readLogEntries;
|
|
16184
17003
|
exports.redisSettingsShape = redisSettingsShape;
|
|
16185
17004
|
exports.refreshSchema = refreshSchema;
|
|
16186
17005
|
exports.registerExceptionHandlers = registerExceptionHandlers;
|
|
@@ -16191,9 +17010,13 @@ exports.renderFormPage = renderFormPage;
|
|
|
16191
17010
|
exports.renderImportPage = renderImportPage;
|
|
16192
17011
|
exports.renderLayout = renderLayout;
|
|
16193
17012
|
exports.renderListPage = renderListPage;
|
|
17013
|
+
exports.renderLogEntriesJson = renderLogEntriesJson;
|
|
17014
|
+
exports.renderLogEntriesMarkdown = renderLogEntriesMarkdown;
|
|
16194
17015
|
exports.renderLoginPage = renderLoginPage;
|
|
17016
|
+
exports.renderLogsPage = renderLogsPage;
|
|
16195
17017
|
exports.renderMfaPage = renderMfaPage;
|
|
16196
17018
|
exports.renderPasswordResetFormPage = renderPasswordResetFormPage;
|
|
17019
|
+
exports.renderSqlPage = renderSqlPage;
|
|
16197
17020
|
exports.requestIdMiddleware = requestIdMiddleware;
|
|
16198
17021
|
exports.requestTracingMiddleware = requestTracingMiddleware;
|
|
16199
17022
|
exports.requireRoles = requireRoles;
|
|
@@ -16217,6 +17040,7 @@ exports.syncFilterSchema = syncFilterSchema;
|
|
|
16217
17040
|
exports.syncPaginationSchema = syncPaginationSchema;
|
|
16218
17041
|
exports.tableNameFor = tableNameFor;
|
|
16219
17042
|
exports.toDict = toDict;
|
|
17043
|
+
exports.toLogEntry = toLogEntry;
|
|
16220
17044
|
exports.toUtc = toUtc;
|
|
16221
17045
|
exports.tokenFromUrl = tokenFromUrl;
|
|
16222
17046
|
exports.tokenPairSchema = tokenPairSchema;
|