@easy-cms/core 1.30.0 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-JTBPGZMF.js → chunk-ZA5KIRYX.js} +585 -113
- package/dist/{config-DGATdBdP.d.ts → config-BUySbadL.d.ts} +25 -2
- package/dist/{handler-DsA868nd.d.ts → handler-rhJ8-GMQ.d.ts} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +1 -1
- package/dist/{plugin-B6lp3qVf.d.ts → plugin-DmTHvlFA.d.ts} +5 -1
- package/dist/plugin.d.ts +2 -2
- package/package.json +2 -2
|
@@ -161,6 +161,7 @@ var AUDIT_LOGS = "audit-logs";
|
|
|
161
161
|
var OUTBOX = "outbox";
|
|
162
162
|
var RATE_LIMITS = "rate-limits";
|
|
163
163
|
var BULK_OPERATIONS = "bulk-operations";
|
|
164
|
+
var SAVED_VIEWS = "saved-views";
|
|
164
165
|
var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
165
166
|
SESSIONS,
|
|
166
167
|
LOGIN_ATTEMPTS,
|
|
@@ -175,7 +176,8 @@ var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
|
175
176
|
AUDIT_LOGS,
|
|
176
177
|
OUTBOX,
|
|
177
178
|
RATE_LIMITS,
|
|
178
|
-
BULK_OPERATIONS
|
|
179
|
+
BULK_OPERATIONS,
|
|
180
|
+
SAVED_VIEWS
|
|
179
181
|
]);
|
|
180
182
|
var DEFAULT_ROLES = ["admin", "editor"];
|
|
181
183
|
var CREATED_BY_FIELD = {
|
|
@@ -563,6 +565,22 @@ var bulkOperationsCollection = {
|
|
|
563
565
|
{ name: "finishedAt", type: "text" }
|
|
564
566
|
]
|
|
565
567
|
};
|
|
568
|
+
var savedViewsCollection = {
|
|
569
|
+
slug: SAVED_VIEWS,
|
|
570
|
+
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
571
|
+
fields: [
|
|
572
|
+
{ name: "collection", type: "text", required: true, index: true },
|
|
573
|
+
{ name: "user", type: "text", required: true, index: true },
|
|
574
|
+
// view | default
|
|
575
|
+
{ name: "kind", type: "text", required: true },
|
|
576
|
+
{ name: "name", type: "text" },
|
|
577
|
+
{ name: "shared", type: "boolean" },
|
|
578
|
+
// `{ query, columns?, pageSize?, density? }`: the list's URL query, and what the browser kept.
|
|
579
|
+
{ name: "state", type: "json" },
|
|
580
|
+
// For `default`: a view's id, or `config:<id>` for one of `admin.views`.
|
|
581
|
+
{ name: "target", type: "text" }
|
|
582
|
+
]
|
|
583
|
+
};
|
|
566
584
|
var webhookDeliveriesCollection = {
|
|
567
585
|
slug: WEBHOOK_DELIVERIES,
|
|
568
586
|
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
@@ -705,6 +723,15 @@ var emailDeliveriesCollection = {
|
|
|
705
723
|
// src/audit.ts
|
|
706
724
|
import { AsyncLocalStorage } from "async_hooks";
|
|
707
725
|
import { createHmac as createHmac2 } from "crypto";
|
|
726
|
+
|
|
727
|
+
// src/csv.ts
|
|
728
|
+
function csvCell(value) {
|
|
729
|
+
const s = value === null || value === void 0 ? "" : typeof value === "string" ? value : JSON.stringify(value);
|
|
730
|
+
const safe = /^[=+\-@\t\r]/.test(s) ? `'${s}` : s;
|
|
731
|
+
return /[",\n\r]/.test(safe) ? `"${safe.replaceAll('"', '""')}"` : safe;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/audit.ts
|
|
708
735
|
var DEFAULT_AUDIT_KEEP = 365;
|
|
709
736
|
var DEFAULT_FAILED_LOGINS = 20;
|
|
710
737
|
var MAX_VALUE = 500;
|
|
@@ -877,11 +904,7 @@ var AuditLog = class {
|
|
|
877
904
|
limit: CSV_LIMIT,
|
|
878
905
|
page: 1
|
|
879
906
|
});
|
|
880
|
-
const cell =
|
|
881
|
-
const s = value === null || value === void 0 ? "" : typeof value === "string" ? value : JSON.stringify(value);
|
|
882
|
-
const safe = /^[=+\-@\t\r]/.test(s) ? `'${s}` : s;
|
|
883
|
-
return /[",\n\r]/.test(safe) ? `"${safe.replaceAll('"', '""')}"` : safe;
|
|
884
|
-
};
|
|
907
|
+
const cell = csvCell;
|
|
885
908
|
const lines = [
|
|
886
909
|
["at", "action", "target", "doc", "title", "actor", "via", "ip", "changes", "detail"].join(
|
|
887
910
|
","
|
|
@@ -1304,9 +1327,9 @@ var esc = (value) => value.replace(
|
|
|
1304
1327
|
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]
|
|
1305
1328
|
);
|
|
1306
1329
|
function email(subject, lines, button) {
|
|
1307
|
-
const
|
|
1330
|
+
const text4 = [...lines, ...button ? ["", button.url] : []].join("\n");
|
|
1308
1331
|
const html = `<div style="font-family:system-ui,sans-serif;font-size:15px;line-height:1.5;color:#1f2328">${lines.map((line) => `<p>${esc(line)}</p>`).join("")}${button ? `<p><a href="${esc(button.url)}" style="display:inline-block;padding:10px 18px;border-radius:8px;background:#2f6f5e;color:#fff;text-decoration:none;font-weight:600">${esc(button.label)}</a></p><p style="font-size:13px;color:#656d76">${esc(button.url)}</p>` : ""}</div>`;
|
|
1309
|
-
return { subject, text:
|
|
1332
|
+
return { subject, text: text4, html };
|
|
1310
1333
|
}
|
|
1311
1334
|
var hours = (expiresAt) => Math.max(1, Math.round(((expiresAt?.getTime() ?? Date.now()) - Date.now()) / 36e5));
|
|
1312
1335
|
var days = (expiresAt) => Math.max(1, Math.round(hours(expiresAt) / 24));
|
|
@@ -2597,8 +2620,8 @@ function itemsOf(field, n) {
|
|
|
2597
2620
|
return `${n} ${unit}${n === 1 ? "" : "s"}`;
|
|
2598
2621
|
}
|
|
2599
2622
|
var isEmpty = (value) => value === void 0 || value === null || value === "";
|
|
2600
|
-
function slugify(
|
|
2601
|
-
return
|
|
2623
|
+
function slugify(text4) {
|
|
2624
|
+
return text4.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
2602
2625
|
}
|
|
2603
2626
|
function applyDefaults(fields, data, localization = null) {
|
|
2604
2627
|
const result = { ...data };
|
|
@@ -2907,7 +2930,7 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
|
|
|
2907
2930
|
return fail(`must have at least ${field.minRows} rows`);
|
|
2908
2931
|
if (field.maxRows !== void 0 && raw.length > field.maxRows)
|
|
2909
2932
|
return fail(`must have at most ${field.maxRows} rows`);
|
|
2910
|
-
const
|
|
2933
|
+
const rows2 = [];
|
|
2911
2934
|
for (const [i, row] of raw.entries()) {
|
|
2912
2935
|
if (!isPlainObject(row)) {
|
|
2913
2936
|
errors.push({ field: `${path}.${i}`, message: "must be an object" });
|
|
@@ -2917,9 +2940,9 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
|
|
|
2917
2940
|
errors.push(...sub.errors);
|
|
2918
2941
|
references2.push(...sub.references);
|
|
2919
2942
|
const id = typeof row.id === "string" && row.id !== "" ? row.id : randomUUID();
|
|
2920
|
-
|
|
2943
|
+
rows2.push({ id, ...sub.data });
|
|
2921
2944
|
}
|
|
2922
|
-
return
|
|
2945
|
+
return rows2;
|
|
2923
2946
|
}
|
|
2924
2947
|
case "blocks":
|
|
2925
2948
|
return normalizeBlocks(field, raw, path, options, fail, errors, references2);
|
|
@@ -2933,7 +2956,7 @@ async function normalizeBlocks(field, raw, path, options, fail, errors, referenc
|
|
|
2933
2956
|
return fail(`must have at least ${field.minRows} blocks`);
|
|
2934
2957
|
if (field.maxRows !== void 0 && raw.length > field.maxRows)
|
|
2935
2958
|
return fail(`must have at most ${field.maxRows} blocks`);
|
|
2936
|
-
const
|
|
2959
|
+
const rows2 = [];
|
|
2937
2960
|
for (const [i, row] of raw.entries()) {
|
|
2938
2961
|
const fields = rowFields(field, row);
|
|
2939
2962
|
if (!isPlainObject(row) || !fields) {
|
|
@@ -2947,9 +2970,9 @@ async function normalizeBlocks(field, raw, path, options, fail, errors, referenc
|
|
|
2947
2970
|
errors.push(...sub.errors);
|
|
2948
2971
|
references2.push(...sub.references);
|
|
2949
2972
|
const id = typeof row.id === "string" && row.id !== "" ? row.id : randomUUID();
|
|
2950
|
-
|
|
2973
|
+
rows2.push({ id, blockType: row.blockType, ...sub.data });
|
|
2951
2974
|
}
|
|
2952
|
-
return
|
|
2975
|
+
return rows2;
|
|
2953
2976
|
}
|
|
2954
2977
|
function normalizeSelect(field, raw, fail) {
|
|
2955
2978
|
const allowed2 = field.options.map((o) => typeof o === "string" ? o : o.value);
|
|
@@ -3198,11 +3221,11 @@ async function copyDatabase(source, target, options = {}) {
|
|
|
3198
3221
|
for (const table of tables) {
|
|
3199
3222
|
let copied = 0;
|
|
3200
3223
|
for (; ; ) {
|
|
3201
|
-
const
|
|
3202
|
-
if (
|
|
3203
|
-
await to.write(table,
|
|
3204
|
-
copied +=
|
|
3205
|
-
if (
|
|
3224
|
+
const rows2 = await from.read(table, copied, BATCH);
|
|
3225
|
+
if (rows2.length === 0) break;
|
|
3226
|
+
await to.write(table, rows2);
|
|
3227
|
+
copied += rows2.length;
|
|
3228
|
+
if (rows2.length < BATCH) break;
|
|
3206
3229
|
}
|
|
3207
3230
|
total += copied;
|
|
3208
3231
|
options.onTable?.({ table, rows: copied });
|
|
@@ -3413,26 +3436,26 @@ async function writeBackupFile(cms, file) {
|
|
|
3413
3436
|
});
|
|
3414
3437
|
}
|
|
3415
3438
|
}
|
|
3416
|
-
var slug = (
|
|
3439
|
+
var slug = (text4) => text4.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "easy-cms";
|
|
3417
3440
|
function filenameFor(cms, date) {
|
|
3418
3441
|
const pad = (n) => String(n).padStart(2, "0");
|
|
3419
3442
|
const stamp = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}`;
|
|
3420
3443
|
return `${slug(cms.config.admin.brand.name || "easy-cms")}-${stamp}.db.gz`;
|
|
3421
3444
|
}
|
|
3422
3445
|
function toAdmin(row) {
|
|
3423
|
-
const
|
|
3446
|
+
const text4 = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
3424
3447
|
return {
|
|
3425
3448
|
id: row.id,
|
|
3426
3449
|
state: ["pending", "running", "done", "failed"].includes(row.state) ? row.state : "failed",
|
|
3427
3450
|
trigger: row.trigger === "scheduled" ? "scheduled" : "manual",
|
|
3428
|
-
filename:
|
|
3451
|
+
filename: text4(row.filename),
|
|
3429
3452
|
size: typeof row.size === "number" ? row.size : null,
|
|
3430
|
-
startedAt:
|
|
3431
|
-
finishedAt:
|
|
3432
|
-
error:
|
|
3433
|
-
author:
|
|
3434
|
-
downloadedBy:
|
|
3435
|
-
downloadedAt:
|
|
3453
|
+
startedAt: text4(row.startedAt),
|
|
3454
|
+
finishedAt: text4(row.finishedAt),
|
|
3455
|
+
error: text4(row.error),
|
|
3456
|
+
author: text4(row.author),
|
|
3457
|
+
downloadedBy: text4(row.downloadedBy),
|
|
3458
|
+
downloadedAt: text4(row.downloadedAt)
|
|
3436
3459
|
};
|
|
3437
3460
|
}
|
|
3438
3461
|
async function update(cms, row, data) {
|
|
@@ -3771,10 +3794,10 @@ var MediaFolders = class {
|
|
|
3771
3794
|
async rows() {
|
|
3772
3795
|
if (this.cache && Date.now() - this.cache.at < TTL) return this.cache.rows;
|
|
3773
3796
|
const found = await this.db.find({ collection: MEDIA_FOLDERS, sort: [], limit: 0, page: 1 });
|
|
3774
|
-
const
|
|
3797
|
+
const rows2 = /* @__PURE__ */ new Map();
|
|
3775
3798
|
for (const doc of found.docs) {
|
|
3776
3799
|
const permissions = doc.permissions;
|
|
3777
|
-
|
|
3800
|
+
rows2.set(String(doc.id), {
|
|
3778
3801
|
id: doc.id,
|
|
3779
3802
|
parent: doc.parent ?? null,
|
|
3780
3803
|
name: String(doc.name ?? ""),
|
|
@@ -3784,31 +3807,31 @@ var MediaFolders = class {
|
|
|
3784
3807
|
doc
|
|
3785
3808
|
});
|
|
3786
3809
|
}
|
|
3787
|
-
this.cache = { at: Date.now(), rows };
|
|
3788
|
-
return
|
|
3810
|
+
this.cache = { at: Date.now(), rows: rows2 };
|
|
3811
|
+
return rows2;
|
|
3789
3812
|
}
|
|
3790
3813
|
/** A role's level in a folder (`null`: the top level): the nearest permissions up the tree. */
|
|
3791
|
-
levelIn(
|
|
3814
|
+
levelIn(rows2, role, id) {
|
|
3792
3815
|
const seen = /* @__PURE__ */ new Set();
|
|
3793
|
-
let current = id === null ? void 0 :
|
|
3816
|
+
let current = id === null ? void 0 : rows2.get(String(id));
|
|
3794
3817
|
while (current && !seen.has(String(current.id))) {
|
|
3795
3818
|
seen.add(String(current.id));
|
|
3796
3819
|
if (current.permissions) {
|
|
3797
3820
|
const level = current.permissions[role];
|
|
3798
3821
|
return level && level in RANK ? level : "none";
|
|
3799
3822
|
}
|
|
3800
|
-
current = current.parent === null ? void 0 :
|
|
3823
|
+
current = current.parent === null ? void 0 : rows2.get(String(current.parent));
|
|
3801
3824
|
}
|
|
3802
3825
|
return "manage";
|
|
3803
3826
|
}
|
|
3804
3827
|
/** Whether files in a folder (`null`: the top level) are private: it or a folder above is. */
|
|
3805
|
-
privateIn(
|
|
3828
|
+
privateIn(rows2, id) {
|
|
3806
3829
|
const seen = /* @__PURE__ */ new Set();
|
|
3807
|
-
let current = id === null ? void 0 :
|
|
3830
|
+
let current = id === null ? void 0 : rows2.get(String(id));
|
|
3808
3831
|
while (current && !seen.has(String(current.id))) {
|
|
3809
3832
|
if (current.private) return true;
|
|
3810
3833
|
seen.add(String(current.id));
|
|
3811
|
-
current = current.parent === null ? void 0 :
|
|
3834
|
+
current = current.parent === null ? void 0 : rows2.get(String(current.parent));
|
|
3812
3835
|
}
|
|
3813
3836
|
return false;
|
|
3814
3837
|
}
|
|
@@ -3818,26 +3841,26 @@ var MediaFolders = class {
|
|
|
3818
3841
|
return this.privateIn(await this.rows(), id);
|
|
3819
3842
|
}
|
|
3820
3843
|
/** The folder and every folder inside it. */
|
|
3821
|
-
within(
|
|
3844
|
+
within(rows2, ids) {
|
|
3822
3845
|
const out = new Set(ids.map(String));
|
|
3823
3846
|
let grew = true;
|
|
3824
3847
|
while (grew) {
|
|
3825
3848
|
grew = false;
|
|
3826
|
-
for (const row of
|
|
3849
|
+
for (const row of rows2.values())
|
|
3827
3850
|
if (row.parent !== null && out.has(String(row.parent)) && !out.has(String(row.id))) {
|
|
3828
3851
|
out.add(String(row.id));
|
|
3829
3852
|
grew = true;
|
|
3830
3853
|
}
|
|
3831
3854
|
}
|
|
3832
|
-
return [...
|
|
3855
|
+
return [...rows2.values()].filter((r) => out.has(String(r.id))).map((r) => r.id);
|
|
3833
3856
|
}
|
|
3834
3857
|
/**
|
|
3835
3858
|
* Folders whose privacy follows this one: it and those inside, with their privacy now.
|
|
3836
3859
|
* @internal
|
|
3837
3860
|
*/
|
|
3838
3861
|
async subtree(id) {
|
|
3839
|
-
const
|
|
3840
|
-
return this.within(
|
|
3862
|
+
const rows2 = await this.fresh();
|
|
3863
|
+
return this.within(rows2, [id]).map((f) => ({ id: f, private: this.privateIn(rows2, f) }));
|
|
3841
3864
|
}
|
|
3842
3865
|
/**
|
|
3843
3866
|
* The privacy each folder of a subtree would have after a change to its top folder: made
|
|
@@ -3845,15 +3868,15 @@ var MediaFolders = class {
|
|
|
3845
3868
|
* @internal
|
|
3846
3869
|
*/
|
|
3847
3870
|
async privacyAfter(id, change) {
|
|
3848
|
-
const
|
|
3849
|
-
const row =
|
|
3871
|
+
const rows2 = new Map(await this.fresh());
|
|
3872
|
+
const row = rows2.get(String(id));
|
|
3850
3873
|
if (!row) return [];
|
|
3851
|
-
|
|
3874
|
+
rows2.set(String(id), {
|
|
3852
3875
|
...row,
|
|
3853
3876
|
...change.private !== void 0 ? { private: change.private } : {},
|
|
3854
3877
|
...change.parent !== void 0 ? { parent: change.parent } : {}
|
|
3855
3878
|
});
|
|
3856
|
-
return this.within(
|
|
3879
|
+
return this.within(rows2, [id]).map((f) => ({ id: f, private: this.privateIn(rows2, f) }));
|
|
3857
3880
|
}
|
|
3858
3881
|
async keyed(key, scope = {}, create = true) {
|
|
3859
3882
|
const find = async () => [...(await this.rows()).values()].find((r) => r.key === key && inScope(r.doc, scope));
|
|
@@ -3891,9 +3914,9 @@ var MediaFolders = class {
|
|
|
3891
3914
|
/** Whether a file in `folder` is in the folder with this key, or below it. @internal */
|
|
3892
3915
|
async inKeyed(key, folder, scope = {}) {
|
|
3893
3916
|
if (folder === null) return false;
|
|
3894
|
-
const
|
|
3895
|
-
const root = [...
|
|
3896
|
-
return !!root && this.within(
|
|
3917
|
+
const rows2 = await this.rows();
|
|
3918
|
+
const root = [...rows2.values()].find((r) => r.key === key && inScope(r.doc, scope));
|
|
3919
|
+
return !!root && this.within(rows2, [root.id]).some((id) => String(id) === String(folder));
|
|
3897
3920
|
}
|
|
3898
3921
|
/** The user's level in a folder (`null`: the top level). */
|
|
3899
3922
|
async level(user, id) {
|
|
@@ -3916,11 +3939,11 @@ var MediaFolders = class {
|
|
|
3916
3939
|
if (!this.applies(user)) return true;
|
|
3917
3940
|
if (operation === "create") return true;
|
|
3918
3941
|
const need = RANK[needed(collection2, operation)];
|
|
3919
|
-
const
|
|
3942
|
+
const rows2 = await this.rows();
|
|
3920
3943
|
const allowed2 = [];
|
|
3921
3944
|
let all = true;
|
|
3922
|
-
for (const row of
|
|
3923
|
-
if (RANK[this.levelIn(
|
|
3945
|
+
for (const row of rows2.values()) {
|
|
3946
|
+
if (RANK[this.levelIn(rows2, user.role, row.id)] >= need) allowed2.push(row.id);
|
|
3924
3947
|
else all = false;
|
|
3925
3948
|
}
|
|
3926
3949
|
if (all) return true;
|
|
@@ -3948,12 +3971,12 @@ var MediaFolders = class {
|
|
|
3948
3971
|
* @internal
|
|
3949
3972
|
*/
|
|
3950
3973
|
async validate(data, self) {
|
|
3951
|
-
const
|
|
3974
|
+
const rows2 = await this.fresh();
|
|
3952
3975
|
const name = typeof data.name === "string" ? data.name.trim() : "";
|
|
3953
3976
|
const parent = data.parent ?? null;
|
|
3954
3977
|
const errors = [];
|
|
3955
3978
|
if (parent !== null && self !== void 0) {
|
|
3956
|
-
let current =
|
|
3979
|
+
let current = rows2.get(String(parent));
|
|
3957
3980
|
const seen = /* @__PURE__ */ new Set();
|
|
3958
3981
|
while (current && !seen.has(String(current.id))) {
|
|
3959
3982
|
if (String(current.id) === String(self)) {
|
|
@@ -3961,10 +3984,10 @@ var MediaFolders = class {
|
|
|
3961
3984
|
break;
|
|
3962
3985
|
}
|
|
3963
3986
|
seen.add(String(current.id));
|
|
3964
|
-
current = current.parent === null ? void 0 :
|
|
3987
|
+
current = current.parent === null ? void 0 : rows2.get(String(current.parent));
|
|
3965
3988
|
}
|
|
3966
3989
|
}
|
|
3967
|
-
const taken = [...
|
|
3990
|
+
const taken = [...rows2.values()].some(
|
|
3968
3991
|
(row) => String(row.id) !== String(self) && String(row.parent) === String(parent) && row.name.trim().toLocaleLowerCase() === name.toLocaleLowerCase()
|
|
3969
3992
|
);
|
|
3970
3993
|
if (name && taken) errors.push({ field: "name", message: "is already used in this folder" });
|
|
@@ -4218,7 +4241,7 @@ var VersionStore = class {
|
|
|
4218
4241
|
async latest(parent, docs) {
|
|
4219
4242
|
const result = /* @__PURE__ */ new Map();
|
|
4220
4243
|
if (docs.length === 0) return result;
|
|
4221
|
-
const
|
|
4244
|
+
const rows2 = await this.db.find({
|
|
4222
4245
|
collection: VERSIONS,
|
|
4223
4246
|
where: {
|
|
4224
4247
|
and: [
|
|
@@ -4231,7 +4254,7 @@ var VersionStore = class {
|
|
|
4231
4254
|
limit: 0,
|
|
4232
4255
|
page: 1
|
|
4233
4256
|
});
|
|
4234
|
-
for (const row of
|
|
4257
|
+
for (const row of rows2.docs) result.set(String(row.doc), this.toVersion(row));
|
|
4235
4258
|
return result;
|
|
4236
4259
|
}
|
|
4237
4260
|
async list(parent, doc, limit, page) {
|
|
@@ -4256,24 +4279,24 @@ var VersionStore = class {
|
|
|
4256
4279
|
return row ? this.toVersion(row) : null;
|
|
4257
4280
|
}
|
|
4258
4281
|
async deleteAll(parent, doc) {
|
|
4259
|
-
const
|
|
4282
|
+
const rows2 = await this.db.find({
|
|
4260
4283
|
collection: VERSIONS,
|
|
4261
4284
|
where: { and: this.of(parent, doc) },
|
|
4262
4285
|
sort: [],
|
|
4263
4286
|
limit: 0,
|
|
4264
4287
|
page: 1
|
|
4265
4288
|
});
|
|
4266
|
-
for (const row of
|
|
4289
|
+
for (const row of rows2.docs) await this.db.delete({ collection: VERSIONS, id: row.id });
|
|
4267
4290
|
}
|
|
4268
4291
|
async prune(parent, doc, max) {
|
|
4269
|
-
const
|
|
4292
|
+
const rows2 = await this.db.find({
|
|
4270
4293
|
collection: VERSIONS,
|
|
4271
4294
|
where: { and: this.of(parent, doc) },
|
|
4272
4295
|
sort: ["-id"],
|
|
4273
4296
|
limit: 0,
|
|
4274
4297
|
page: 1
|
|
4275
4298
|
});
|
|
4276
|
-
for (const row of
|
|
4299
|
+
for (const row of rows2.docs.slice(max)) {
|
|
4277
4300
|
await this.db.delete({ collection: VERSIONS, id: row.id });
|
|
4278
4301
|
}
|
|
4279
4302
|
}
|
|
@@ -4847,12 +4870,12 @@ function toLocaleMaps(fields, input, current, locale) {
|
|
|
4847
4870
|
locale
|
|
4848
4871
|
);
|
|
4849
4872
|
} else if (hasRows(field) && Array.isArray(value)) {
|
|
4850
|
-
const
|
|
4873
|
+
const rows2 = new Map(
|
|
4851
4874
|
(Array.isArray(previous) ? previous : []).filter(isPlainObject2).map((row) => [String(row.id), row])
|
|
4852
4875
|
);
|
|
4853
4876
|
result[field.name] = value.map((row) => {
|
|
4854
4877
|
const fields2 = rowFields(field, row);
|
|
4855
|
-
return fields2 && isPlainObject2(row) ? toLocaleMaps(fields2, row,
|
|
4878
|
+
return fields2 && isPlainObject2(row) ? toLocaleMaps(fields2, row, rows2.get(String(row.id)) ?? {}, locale) : row;
|
|
4856
4879
|
});
|
|
4857
4880
|
}
|
|
4858
4881
|
}
|
|
@@ -5272,13 +5295,13 @@ function embeddedIPv4(address) {
|
|
|
5272
5295
|
return bytes ? bytes.slice(12).join(".") : void 0;
|
|
5273
5296
|
}
|
|
5274
5297
|
function ipv6Bytes(address) {
|
|
5275
|
-
let
|
|
5276
|
-
const tail = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(
|
|
5298
|
+
let text4 = address;
|
|
5299
|
+
const tail = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(text4);
|
|
5277
5300
|
if (tail) {
|
|
5278
5301
|
const [a, b, c, d] = tail.slice(1).map(Number);
|
|
5279
|
-
|
|
5302
|
+
text4 = `${text4.slice(0, tail.index)}${(a << 8 | b).toString(16)}:${(c << 8 | d).toString(16)}`;
|
|
5280
5303
|
}
|
|
5281
|
-
const [head = "", rest] =
|
|
5304
|
+
const [head = "", rest] = text4.split("::");
|
|
5282
5305
|
const left = head ? head.split(":") : [];
|
|
5283
5306
|
const right = rest === void 0 ? [] : rest ? rest.split(":") : [];
|
|
5284
5307
|
const groups = rest === void 0 ? left : [...left, ...Array(8 - left.length - right.length).fill("0"), ...right];
|
|
@@ -5476,7 +5499,7 @@ var GLOBAL = keys(
|
|
|
5476
5499
|
);
|
|
5477
5500
|
var CONTAINER_ADMIN = keys("icon order sidebar layout group");
|
|
5478
5501
|
var COLLECTION_ADMIN = keys(
|
|
5479
|
-
"icon order sidebar layout group editIn badge count empty list calendar ownerField confirmDelete"
|
|
5502
|
+
"icon order sidebar layout group editIn badge count empty list calendar ownerField confirmDelete views export"
|
|
5480
5503
|
);
|
|
5481
5504
|
var FIELD_ADMIN = keys(
|
|
5482
5505
|
"description width condition component after cell column allowCreate initialValue position filter"
|
|
@@ -5747,26 +5770,26 @@ function fieldsText(fields, data) {
|
|
|
5747
5770
|
}
|
|
5748
5771
|
function richText(node) {
|
|
5749
5772
|
if (!node || typeof node !== "object") return "";
|
|
5750
|
-
const { text:
|
|
5751
|
-
if (typeof
|
|
5773
|
+
const { text: text4, content } = node;
|
|
5774
|
+
if (typeof text4 === "string") return text4;
|
|
5752
5775
|
if (!Array.isArray(content)) return "";
|
|
5753
5776
|
const inline = content.some((child) => typeof child?.text === "string");
|
|
5754
5777
|
return content.map(richText).filter(Boolean).join(inline ? "" : "\n");
|
|
5755
5778
|
}
|
|
5756
5779
|
function searchText(fields, doc, paths) {
|
|
5757
|
-
const
|
|
5780
|
+
const text4 = {};
|
|
5758
5781
|
for (const path of paths) {
|
|
5759
5782
|
const field = fieldAt(fields, path);
|
|
5760
5783
|
let value = doc;
|
|
5761
5784
|
for (const name of path.split("."))
|
|
5762
5785
|
value = value && typeof value === "object" ? value[name] : void 0;
|
|
5763
|
-
|
|
5786
|
+
text4[path] = field ? textOf(field, value) : "";
|
|
5764
5787
|
}
|
|
5765
|
-
return
|
|
5788
|
+
return text4;
|
|
5766
5789
|
}
|
|
5767
5790
|
var segmenter = new Intl.Segmenter(void 0, { granularity: "word" });
|
|
5768
|
-
function words(
|
|
5769
|
-
return [...segmenter.segment(
|
|
5791
|
+
function words(text4) {
|
|
5792
|
+
return [...segmenter.segment(text4.normalize("NFC").toLowerCase())].filter((s) => s.isWordLike).map((s) => s.segment);
|
|
5770
5793
|
}
|
|
5771
5794
|
function memorySearch() {
|
|
5772
5795
|
const entries = /* @__PURE__ */ new Map();
|
|
@@ -7207,7 +7230,26 @@ function validateLayout(layout, container, path, add) {
|
|
|
7207
7230
|
nodes(layout, path, true);
|
|
7208
7231
|
}
|
|
7209
7232
|
function validateCollectionAdmin(admin, path, add) {
|
|
7210
|
-
const { badge, count, empty } = admin;
|
|
7233
|
+
const { badge, count, empty, views } = admin;
|
|
7234
|
+
if (views !== void 0) {
|
|
7235
|
+
if (!Array.isArray(views)) add(`${path}.views`, "must be an array");
|
|
7236
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7237
|
+
for (const [i, raw] of (Array.isArray(views) ? views : []).entries()) {
|
|
7238
|
+
const view = raw ?? {};
|
|
7239
|
+
const at = `${path}.views[${i}]`;
|
|
7240
|
+
if (typeof view.id !== "string" || !/^[a-z0-9][a-z0-9_-]*$/.test(view.id))
|
|
7241
|
+
add(`${at}.id`, 'must be lowercase letters, digits, "-" or "_"', "e.g. id: 'to-review'");
|
|
7242
|
+
else if (ids.has(view.id)) add(`${at}.id`, `duplicate view "${view.id}"`);
|
|
7243
|
+
else ids.add(view.id);
|
|
7244
|
+
if (!isLabel(view.label)) add(`${at}.label`, "must be a string or { en, th }");
|
|
7245
|
+
if (view.where !== void 0 && (typeof view.where !== "object" || view.where === null || Array.isArray(view.where)))
|
|
7246
|
+
add(`${at}.where`, "must be a where query", "e.g. { status: { equals: 'draft' } }");
|
|
7247
|
+
if (view.pageSize !== void 0 && ![10, 25, 50, 100].includes(view.pageSize))
|
|
7248
|
+
add(`${at}.pageSize`, "must be 10, 25, 50 or 100");
|
|
7249
|
+
if (view.density !== void 0 && !["comfortable", "compact"].includes(view.density))
|
|
7250
|
+
add(`${at}.density`, "must be 'comfortable' or 'compact'");
|
|
7251
|
+
}
|
|
7252
|
+
}
|
|
7211
7253
|
if (badge !== void 0) {
|
|
7212
7254
|
const b = badge ?? {};
|
|
7213
7255
|
if (typeof b.where !== "object" || b.where === null || Array.isArray(b.where))
|
|
@@ -7217,6 +7259,8 @@ function validateCollectionAdmin(admin, path, add) {
|
|
|
7217
7259
|
}
|
|
7218
7260
|
if (count !== void 0 && typeof count !== "boolean")
|
|
7219
7261
|
add(`${path}.count`, "must be true or false");
|
|
7262
|
+
if (admin.export !== void 0 && typeof admin.export !== "boolean")
|
|
7263
|
+
add(`${path}.export`, "must be true or false");
|
|
7220
7264
|
if (empty !== void 0) {
|
|
7221
7265
|
const e = empty ?? {};
|
|
7222
7266
|
if (e.description !== void 0 && !isLabel(e.description))
|
|
@@ -7981,6 +8025,7 @@ async function resolveConfig(input) {
|
|
|
7981
8025
|
...config.rateLimit ? [rateLimitsCollection] : [],
|
|
7982
8026
|
databaseBackupsCollection,
|
|
7983
8027
|
bulkOperationsCollection,
|
|
8028
|
+
savedViewsCollection,
|
|
7984
8029
|
...config.auth?.rbac ? [rolesCollection] : [],
|
|
7985
8030
|
...(config.auth?.providers?.length ?? 0) > 0 ? [userIdentitiesCollection] : [],
|
|
7986
8031
|
...config.audit ? [auditLogsCollection] : []
|
|
@@ -8101,7 +8146,7 @@ function cacheableResponse(request, body, status, headers, times, tags) {
|
|
|
8101
8146
|
}
|
|
8102
8147
|
|
|
8103
8148
|
// src/version.ts
|
|
8104
|
-
var VERSION = true ? "1.
|
|
8149
|
+
var VERSION = true ? "1.31.0" : "0.0.0-dev";
|
|
8105
8150
|
|
|
8106
8151
|
// src/telemetry.ts
|
|
8107
8152
|
var DB_CALLS = /* @__PURE__ */ new Set([
|
|
@@ -9801,12 +9846,12 @@ var EasyCMS = class {
|
|
|
9801
9846
|
if (asked !== void 0 && localization && !localization.locales.includes(asked))
|
|
9802
9847
|
throw new QueryError(`Unknown locale "${asked}"`);
|
|
9803
9848
|
const locale = localization ? asked ?? localization.defaultLocale ?? null : null;
|
|
9804
|
-
const
|
|
9849
|
+
const text4 = query.trim().slice(0, 200);
|
|
9805
9850
|
const empty = { hits: [], total: 0, page, limit, hasNextPage: false };
|
|
9806
|
-
if (!
|
|
9851
|
+
if (!text4 || collections.length === 0) return empty;
|
|
9807
9852
|
const { collections: _, limit: __, page: ___, ...read2 } = options;
|
|
9808
9853
|
const { hits, total } = await index.search(
|
|
9809
|
-
{ query:
|
|
9854
|
+
{ query: text4, collections, locale, limit, offset: (page - 1) * limit },
|
|
9810
9855
|
async (collection2, ids) => (await this.find(collection2, {
|
|
9811
9856
|
...read2,
|
|
9812
9857
|
...locale ? { locale } : {},
|
|
@@ -9960,13 +10005,13 @@ var EasyCMS = class {
|
|
|
9960
10005
|
const { jobs } = this.config;
|
|
9961
10006
|
if (jobs.length === 0) return { ran: 0, failed: 0 };
|
|
9962
10007
|
const { docs } = await this.db.find({ collection: JOB_RUNS, sort: ["name"], limit: 0, page: 1 });
|
|
9963
|
-
const
|
|
10008
|
+
const rows2 = new Map(docs.map((row) => [String(row.name), row]));
|
|
9964
10009
|
let legacy;
|
|
9965
10010
|
const stamp = now.toISOString();
|
|
9966
10011
|
let ran = 0;
|
|
9967
10012
|
let failed = 0;
|
|
9968
10013
|
for (const job of jobs) {
|
|
9969
|
-
const fresh =
|
|
10014
|
+
const fresh = rows2.has(job.name) ? await this.db.findById({ collection: JOB_RUNS, id: rows2.get(job.name)?.id }) : null;
|
|
9970
10015
|
const row = fresh ?? void 0;
|
|
9971
10016
|
if (row?.pausedAt) continue;
|
|
9972
10017
|
const schedule = jobSchedule(job, row);
|
|
@@ -10350,7 +10395,7 @@ var EasyCMS = class {
|
|
|
10350
10395
|
...options.from !== void 0 ? { gte: options.from } : {},
|
|
10351
10396
|
...options.to !== void 0 ? { lt: options.to } : {}
|
|
10352
10397
|
};
|
|
10353
|
-
const
|
|
10398
|
+
const rows2 = await this.db.find({
|
|
10354
10399
|
collection: SCHEDULED_JOBS,
|
|
10355
10400
|
where: {
|
|
10356
10401
|
and: [
|
|
@@ -10385,7 +10430,7 @@ var EasyCMS = class {
|
|
|
10385
10430
|
return result;
|
|
10386
10431
|
};
|
|
10387
10432
|
const jobs = [];
|
|
10388
|
-
for (const row of
|
|
10433
|
+
for (const row of rows2.docs) {
|
|
10389
10434
|
const parent = String(row.parent);
|
|
10390
10435
|
if (!await may2(parent)) continue;
|
|
10391
10436
|
const target = parent.startsWith("global:") ? { global: splitGlobalKey(parent.slice("global:".length)).slug } : { collection: parent };
|
|
@@ -10395,7 +10440,7 @@ var EasyCMS = class {
|
|
|
10395
10440
|
return jobs;
|
|
10396
10441
|
}
|
|
10397
10442
|
async pendingJobs(parent, doc) {
|
|
10398
|
-
const
|
|
10443
|
+
const rows2 = await this.db.find({
|
|
10399
10444
|
collection: SCHEDULED_JOBS,
|
|
10400
10445
|
where: {
|
|
10401
10446
|
and: [
|
|
@@ -10408,7 +10453,7 @@ var EasyCMS = class {
|
|
|
10408
10453
|
limit: 0,
|
|
10409
10454
|
page: 1
|
|
10410
10455
|
});
|
|
10411
|
-
return
|
|
10456
|
+
return rows2.docs.map(toJob);
|
|
10412
10457
|
}
|
|
10413
10458
|
async removeJob(parent, doc, jobId) {
|
|
10414
10459
|
const job = (await this.pendingJobs(parent, doc)).find((j) => String(j.id) === String(jobId));
|
|
@@ -11441,8 +11486,8 @@ function hasCalendarPage(config) {
|
|
|
11441
11486
|
var text2 = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
11442
11487
|
async function adminJobs(cms, user) {
|
|
11443
11488
|
const { config } = cms;
|
|
11444
|
-
const
|
|
11445
|
-
const byName = new Map(
|
|
11489
|
+
const rows2 = has(config, JOB_RUNS) ? (await cms.db.find({ collection: JOB_RUNS, sort: ["name"], limit: 0, page: 1 })).docs : [];
|
|
11490
|
+
const byName = new Map(rows2.map((row) => [String(row.name), row]));
|
|
11446
11491
|
const jobs = config.jobs.map((job) => {
|
|
11447
11492
|
const row = byName.get(job.name);
|
|
11448
11493
|
const lastRunAt = text2(row?.lastRunAt);
|
|
@@ -11816,9 +11861,9 @@ var Roles = class {
|
|
|
11816
11861
|
}
|
|
11817
11862
|
async addMissing() {
|
|
11818
11863
|
if (!this.enabled) return;
|
|
11819
|
-
const
|
|
11820
|
-
const first =
|
|
11821
|
-
const known = new Set(
|
|
11864
|
+
const rows2 = await this.rows();
|
|
11865
|
+
const first = rows2.length === 0;
|
|
11866
|
+
const known = new Set(rows2.map((r) => String(r.key)));
|
|
11822
11867
|
for (const key of this.config.auth.roles) {
|
|
11823
11868
|
if (known.has(key)) continue;
|
|
11824
11869
|
try {
|
|
@@ -11876,15 +11921,15 @@ var Roles = class {
|
|
|
11876
11921
|
// --- Settings → Roles ----------------------------------------------------
|
|
11877
11922
|
async list() {
|
|
11878
11923
|
await this.sync();
|
|
11879
|
-
const
|
|
11924
|
+
const rows2 = await this.rows();
|
|
11880
11925
|
const counts = /* @__PURE__ */ new Map();
|
|
11881
|
-
for (const row of
|
|
11926
|
+
for (const row of rows2) {
|
|
11882
11927
|
counts.set(
|
|
11883
11928
|
String(row.key),
|
|
11884
11929
|
await this.db.count({ collection: USERS, where: { role: { equals: row.key } } })
|
|
11885
11930
|
);
|
|
11886
11931
|
}
|
|
11887
|
-
const roles =
|
|
11932
|
+
const roles = rows2.map((row) => this.toRole(row, counts.get(String(row.key)) ?? 0)).sort((a, b) => rank(a, this.config) - rank(b, this.config) || a.key.localeCompare(b.key));
|
|
11888
11933
|
const decided = roles.filter((r) => r.key !== "admin").map((r) => r.permissions);
|
|
11889
11934
|
const seen = (group, slug2) => decided.some((p) => p[group]?.[slug2] !== void 0);
|
|
11890
11935
|
return {
|
|
@@ -12261,9 +12306,9 @@ var PER_PAGE = 50;
|
|
|
12261
12306
|
var collectionOf = (kind) => kind === "webhook" ? WEBHOOK_DELIVERIES : EMAIL_DELIVERIES;
|
|
12262
12307
|
var exists = (cms, kind) => cms.config.collections.some((c) => c.slug === collectionOf(kind));
|
|
12263
12308
|
var isDeliveryKind = (value) => value === "webhook" || value === "email";
|
|
12264
|
-
function parse3(
|
|
12309
|
+
function parse3(text4) {
|
|
12265
12310
|
try {
|
|
12266
|
-
const value = JSON.parse(String(
|
|
12311
|
+
const value = JSON.parse(String(text4 ?? ""));
|
|
12267
12312
|
return typeof value === "object" && value !== null ? value : {};
|
|
12268
12313
|
} catch {
|
|
12269
12314
|
return {};
|
|
@@ -12445,7 +12490,7 @@ async function sendTestEmail(cms, user, to, locale) {
|
|
|
12445
12490
|
const th = locale === "th" || locale !== "en" && cms.config.admin.locale === "th";
|
|
12446
12491
|
const time = (/* @__PURE__ */ new Date()).toISOString();
|
|
12447
12492
|
const subject = th ? `\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E17\u0E14\u0E2A\u0E2D\u0E1A\u0E08\u0E32\u0E01 ${site}` : `Test email from ${site}`;
|
|
12448
|
-
const
|
|
12493
|
+
const text4 = th ? `\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E19\u0E35\u0E49\u0E2A\u0E48\u0E07\u0E08\u0E32\u0E01\u0E2B\u0E19\u0E49\u0E32 \u0E15\u0E31\u0E49\u0E07\u0E04\u0E48\u0E32 \u2192 \u0E2D\u0E35\u0E40\u0E21\u0E25 \u0E02\u0E2D\u0E07 ${site} \u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E17\u0E14\u0E2A\u0E2D\u0E1A\u0E01\u0E32\u0E23\u0E15\u0E31\u0E49\u0E07\u0E04\u0E48\u0E32
|
|
12449
12494
|
|
|
12450
12495
|
\u0E2A\u0E48\u0E07\u0E42\u0E14\u0E22: ${user.email}
|
|
12451
12496
|
Adapter: ${adapter.name ?? "custom"}
|
|
@@ -12457,7 +12502,7 @@ Adapter: ${adapter.name ?? "custom"}
|
|
|
12457
12502
|
From: ${from}
|
|
12458
12503
|
Time: ${time}`;
|
|
12459
12504
|
try {
|
|
12460
|
-
await adapter.send({ to: address, subject, text:
|
|
12505
|
+
await adapter.send({ to: address, subject, text: text4, from });
|
|
12461
12506
|
return logsOnly(cms) ? { ok: true, logged: true } : { ok: true };
|
|
12462
12507
|
} catch (error) {
|
|
12463
12508
|
return { ok: false, error: message(error) };
|
|
@@ -12647,6 +12692,8 @@ async function collection(cms, config, user, localized, roles, context) {
|
|
|
12647
12692
|
if (config.admin?.empty) result.empty = JSON.parse(JSON.stringify(config.admin.empty));
|
|
12648
12693
|
if (config.admin?.list?.tree) result.tree = config.admin.list.tree;
|
|
12649
12694
|
if (config.admin?.list?.sort) result.defaultSort = config.admin.list.sort;
|
|
12695
|
+
if (config.admin?.views?.length) result.views = JSON.parse(JSON.stringify(config.admin.views));
|
|
12696
|
+
if (config.admin?.export === false) result.noExport = true;
|
|
12650
12697
|
if (config.admin?.calendar) result.calendar = { ...config.admin.calendar };
|
|
12651
12698
|
if (config.admin?.confirmDelete) result.confirmDelete = { ...config.admin.confirmDelete };
|
|
12652
12699
|
const owner = cms.roles.ownerOf(config.slug);
|
|
@@ -12931,21 +12978,42 @@ async function readable(cms, c, who) {
|
|
|
12931
12978
|
return false;
|
|
12932
12979
|
}
|
|
12933
12980
|
}
|
|
12934
|
-
|
|
12981
|
+
var LIST_TOTAL_MAX = 1e4;
|
|
12982
|
+
var MENU_COUNTS_TTL = 3e4;
|
|
12983
|
+
var kept = /* @__PURE__ */ new WeakMap();
|
|
12984
|
+
async function adminCounts(cms, who, options = {}) {
|
|
12985
|
+
const key = `${String(who.user.id)}:${who.user.apiKey ? "key" : "user"}:${JSON.stringify(who.context)}`;
|
|
12986
|
+
const cache = kept.get(cms) ?? /* @__PURE__ */ new Map();
|
|
12987
|
+
kept.set(cms, cache);
|
|
12988
|
+
const now = Date.now();
|
|
12989
|
+
const hit = cache.get(key);
|
|
12990
|
+
if (hit && !options.fresh && now - hit.at < MENU_COUNTS_TTL) return hit.value;
|
|
12935
12991
|
const counts = {};
|
|
12936
12992
|
const badges = {};
|
|
12993
|
+
const more = [];
|
|
12937
12994
|
await Promise.all(
|
|
12938
12995
|
listed(cms).map(async (c) => {
|
|
12939
12996
|
const read2 = { ...who, overrideAccess: false, draft: true };
|
|
12940
12997
|
try {
|
|
12941
|
-
if (c.admin?.count !== false)
|
|
12998
|
+
if (c.admin?.count !== false) {
|
|
12999
|
+
const total = await cms.count(c.slug, { ...read2, max: LIST_TOTAL_MAX });
|
|
13000
|
+
counts[c.slug] = Math.min(total, LIST_TOTAL_MAX);
|
|
13001
|
+
if (total > LIST_TOTAL_MAX) more.push(c.slug);
|
|
13002
|
+
}
|
|
12942
13003
|
if (c.admin?.badge)
|
|
12943
|
-
badges[c.slug] = await cms.count(c.slug, {
|
|
13004
|
+
badges[c.slug] = await cms.count(c.slug, {
|
|
13005
|
+
...read2,
|
|
13006
|
+
where: c.admin.badge.where,
|
|
13007
|
+
max: LIST_TOTAL_MAX
|
|
13008
|
+
});
|
|
12944
13009
|
} catch {
|
|
12945
13010
|
}
|
|
12946
13011
|
})
|
|
12947
13012
|
);
|
|
12948
|
-
|
|
13013
|
+
const value = { counts, badges, more };
|
|
13014
|
+
for (const [k, entry] of cache) if (now - entry.at >= MENU_COUNTS_TTL) cache.delete(k);
|
|
13015
|
+
cache.set(key, { at: now, value });
|
|
13016
|
+
return value;
|
|
12949
13017
|
}
|
|
12950
13018
|
async function adminSearch(cms, who, q, perCollection = 5) {
|
|
12951
13019
|
const query = q.trim().slice(0, 100);
|
|
@@ -13076,6 +13144,182 @@ async function adminStatus(cms) {
|
|
|
13076
13144
|
};
|
|
13077
13145
|
}
|
|
13078
13146
|
|
|
13147
|
+
// src/rest/export-csv.ts
|
|
13148
|
+
var EXPORT_MAX = 1e5;
|
|
13149
|
+
var STEP = 500;
|
|
13150
|
+
var SYSTEM = {
|
|
13151
|
+
id: "ID",
|
|
13152
|
+
status: { en: "Status", th: "\u0E2A\u0E16\u0E32\u0E19\u0E30" },
|
|
13153
|
+
createdAt: { en: "Created", th: "\u0E2A\u0E23\u0E49\u0E32\u0E07\u0E40\u0E21\u0E37\u0E48\u0E2D" },
|
|
13154
|
+
updatedAt: { en: "Updated", th: "\u0E41\u0E01\u0E49\u0E44\u0E02\u0E40\u0E21\u0E37\u0E48\u0E2D" }
|
|
13155
|
+
};
|
|
13156
|
+
var text3 = (label, fallback, lang) => label === void 0 ? fallback : typeof label === "string" ? label : label[lang] ?? label.en ?? Object.values(label)[0] ?? fallback;
|
|
13157
|
+
function isoInZone(value, timeZone) {
|
|
13158
|
+
const date = new Date(value);
|
|
13159
|
+
if (!timeZone || Number.isNaN(date.getTime())) return value;
|
|
13160
|
+
const parts = Object.fromEntries(
|
|
13161
|
+
new Intl.DateTimeFormat("en-CA", {
|
|
13162
|
+
timeZone,
|
|
13163
|
+
year: "numeric",
|
|
13164
|
+
month: "2-digit",
|
|
13165
|
+
day: "2-digit",
|
|
13166
|
+
hour: "2-digit",
|
|
13167
|
+
minute: "2-digit",
|
|
13168
|
+
second: "2-digit",
|
|
13169
|
+
hourCycle: "h23"
|
|
13170
|
+
}).formatToParts(date).map((p) => [p.type, p.value])
|
|
13171
|
+
);
|
|
13172
|
+
const local = `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}`;
|
|
13173
|
+
const offset = Math.round((Date.parse(`${local}Z`) - date.getTime()) / 6e4);
|
|
13174
|
+
const sign3 = offset < 0 ? "-" : "+";
|
|
13175
|
+
const abs = Math.abs(offset);
|
|
13176
|
+
return `${local}${sign3}${String(Math.floor(abs / 60)).padStart(2, "0")}:${String(abs % 60).padStart(2, "0")}`;
|
|
13177
|
+
}
|
|
13178
|
+
function cellOf(cms, field, value, key, lang) {
|
|
13179
|
+
if (value === null || value === void 0) return "";
|
|
13180
|
+
const timeZone = cms.config.timeZone;
|
|
13181
|
+
if (!field)
|
|
13182
|
+
return key === "createdAt" || key === "updatedAt" ? isoInZone(String(value), timeZone) : value;
|
|
13183
|
+
const titleOf = (slug2, doc) => {
|
|
13184
|
+
if (!doc || typeof doc !== "object") return String(doc);
|
|
13185
|
+
const d = doc;
|
|
13186
|
+
if (slug2 === "media" && typeof d.url === "string") return d.url;
|
|
13187
|
+
const collection2 = cms.config.collections.find((c) => c.slug === slug2);
|
|
13188
|
+
const title = collection2?.useAsTitle ? d[collection2.useAsTitle] : void 0;
|
|
13189
|
+
return typeof title === "string" && title !== "" ? title : String(d.id ?? "");
|
|
13190
|
+
};
|
|
13191
|
+
switch (field.type) {
|
|
13192
|
+
case "date":
|
|
13193
|
+
return isoInZone(String(value), timeZone);
|
|
13194
|
+
case "boolean":
|
|
13195
|
+
return value ? "true" : "false";
|
|
13196
|
+
case "select": {
|
|
13197
|
+
const label = (v) => {
|
|
13198
|
+
const option = field.options.find((o) => (typeof o === "string" ? o : o.value) === v);
|
|
13199
|
+
return option === void 0 || typeof option === "string" ? String(v) : text3(option.label, String(v), lang);
|
|
13200
|
+
};
|
|
13201
|
+
return Array.isArray(value) ? value.map(label).join("; ") : label(value);
|
|
13202
|
+
}
|
|
13203
|
+
case "richText":
|
|
13204
|
+
return richText(value);
|
|
13205
|
+
case "relationship":
|
|
13206
|
+
case "upload": {
|
|
13207
|
+
const one = (item) => {
|
|
13208
|
+
if (isPolymorphic(field)) {
|
|
13209
|
+
const pair = polymorphicValue(item);
|
|
13210
|
+
if (!pair) return "";
|
|
13211
|
+
const collection2 = cms.config.collections.find((c) => c.slug === pair.relationTo);
|
|
13212
|
+
const name = text3(collection2?.labels?.singular, pair.relationTo, lang);
|
|
13213
|
+
return `${name} \xB7 ${titleOf(pair.relationTo, pair.value)}`;
|
|
13214
|
+
}
|
|
13215
|
+
return titleOf(relationTargets(field)[0], item);
|
|
13216
|
+
};
|
|
13217
|
+
return Array.isArray(value) ? value.map(one).join("; ") : one(value);
|
|
13218
|
+
}
|
|
13219
|
+
default:
|
|
13220
|
+
return value;
|
|
13221
|
+
}
|
|
13222
|
+
}
|
|
13223
|
+
async function columnsOf(cms, config, asked, options) {
|
|
13224
|
+
const rules = await cms.roles.fieldRules(options.user);
|
|
13225
|
+
const read2 = new FieldAccessChecker(
|
|
13226
|
+
"read",
|
|
13227
|
+
{ user: options.user, context: options.context },
|
|
13228
|
+
rules
|
|
13229
|
+
);
|
|
13230
|
+
const readable3 = [];
|
|
13231
|
+
for (const field of config.fields)
|
|
13232
|
+
if (!field.hidden && field.name !== "password" && await read2.allows(field))
|
|
13233
|
+
readable3.push(field);
|
|
13234
|
+
const system2 = [...config.drafts ? ["status"] : [], "createdAt", "updatedAt"];
|
|
13235
|
+
const of = (key) => {
|
|
13236
|
+
const field = readable3.find((f) => f.name === key);
|
|
13237
|
+
if (field) return { key, label: field.label ?? key, field };
|
|
13238
|
+
return key in SYSTEM && (key !== "status" || config.drafts) ? { key, label: SYSTEM[key] } : void 0;
|
|
13239
|
+
};
|
|
13240
|
+
const keys2 = asked ?? [...readable3.map((f) => f.name), ...system2];
|
|
13241
|
+
const columns = keys2.filter((k) => k !== "id").map(of).filter((c) => c !== void 0);
|
|
13242
|
+
return [{ key: "id", label: SYSTEM.id }, ...columns];
|
|
13243
|
+
}
|
|
13244
|
+
async function exportCsv(cms, slug2, url, options) {
|
|
13245
|
+
const config = cms.collection(slug2);
|
|
13246
|
+
if (config.admin?.export === false)
|
|
13247
|
+
throw new ForbiddenError("Export is turned off for this collection");
|
|
13248
|
+
const q = url.searchParams;
|
|
13249
|
+
const lang = q.get("lang") ?? "en";
|
|
13250
|
+
const names = q.get("headers") === "names";
|
|
13251
|
+
const columnsParam = q.get("columns");
|
|
13252
|
+
const asked = columnsParam && columnsParam !== "all" ? columnsParam.split(",").filter(Boolean) : null;
|
|
13253
|
+
const ids = q.get("ids")?.split(",").filter(Boolean);
|
|
13254
|
+
const where = ids?.length ? { and: [...options.where ? [options.where] : [], { id: { in: ids } }] } : options.where;
|
|
13255
|
+
const read2 = {
|
|
13256
|
+
user: options.user,
|
|
13257
|
+
context: options.context,
|
|
13258
|
+
overrideAccess: false,
|
|
13259
|
+
draft: options.draft,
|
|
13260
|
+
...options.locale ? { locale: options.locale } : {},
|
|
13261
|
+
...options.fallbackLocale === false ? { fallbackLocale: false } : {}
|
|
13262
|
+
};
|
|
13263
|
+
const total = await cms.count(slug2, { ...read2, ...where ? { where } : {}, max: EXPORT_MAX });
|
|
13264
|
+
if (total > EXPORT_MAX)
|
|
13265
|
+
throw new QueryError(
|
|
13266
|
+
`More than ${EXPORT_MAX.toLocaleString("en")} documents match: filter the list further`
|
|
13267
|
+
);
|
|
13268
|
+
const columns = await columnsOf(cms, config, asked, options);
|
|
13269
|
+
const header2 = columns.map((c) => names ? c.key : text3(c.label, c.key, lang));
|
|
13270
|
+
await cms.audit.record({
|
|
13271
|
+
action: "export",
|
|
13272
|
+
target: slug2,
|
|
13273
|
+
user: options.user,
|
|
13274
|
+
detail: { rows: total, columns: columns.map((c) => c.key) }
|
|
13275
|
+
});
|
|
13276
|
+
const encoder = new TextEncoder();
|
|
13277
|
+
let cursor = "";
|
|
13278
|
+
const stream = new ReadableStream({
|
|
13279
|
+
start(controller) {
|
|
13280
|
+
controller.enqueue(encoder.encode(`\uFEFF${header2.map(csvCell).join(",")}\r
|
|
13281
|
+
`));
|
|
13282
|
+
},
|
|
13283
|
+
async pull(controller) {
|
|
13284
|
+
if (cursor === null) {
|
|
13285
|
+
controller.close();
|
|
13286
|
+
return;
|
|
13287
|
+
}
|
|
13288
|
+
try {
|
|
13289
|
+
const page = await cms.find(
|
|
13290
|
+
slug2,
|
|
13291
|
+
{
|
|
13292
|
+
...read2,
|
|
13293
|
+
...where ? { where } : {},
|
|
13294
|
+
...options.sort?.length ? { sort: options.sort } : {},
|
|
13295
|
+
limit: STEP,
|
|
13296
|
+
depth: 1,
|
|
13297
|
+
cursor
|
|
13298
|
+
}
|
|
13299
|
+
);
|
|
13300
|
+
const lines = page.docs.map(
|
|
13301
|
+
(doc) => columns.map((c) => csvCell(cellOf(cms, c.field, doc[c.key], c.key, lang))).join(",")
|
|
13302
|
+
);
|
|
13303
|
+
if (lines.length) controller.enqueue(encoder.encode(`${lines.join("\r\n")}\r
|
|
13304
|
+
`));
|
|
13305
|
+
cursor = page.nextCursor ?? null;
|
|
13306
|
+
if (cursor === null) controller.close();
|
|
13307
|
+
} catch (error) {
|
|
13308
|
+
controller.error(error);
|
|
13309
|
+
}
|
|
13310
|
+
}
|
|
13311
|
+
});
|
|
13312
|
+
const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
13313
|
+
return new Response(stream, {
|
|
13314
|
+
headers: {
|
|
13315
|
+
"content-type": "text/csv; charset=utf-8",
|
|
13316
|
+
"content-disposition": `attachment; filename="${slug2}-${day}.csv"`,
|
|
13317
|
+
"cache-control": "no-store",
|
|
13318
|
+
"x-content-type-options": "nosniff"
|
|
13319
|
+
}
|
|
13320
|
+
});
|
|
13321
|
+
}
|
|
13322
|
+
|
|
13079
13323
|
// src/rest/filter-options.ts
|
|
13080
13324
|
function fieldAt2(fields, path) {
|
|
13081
13325
|
let current = fields;
|
|
@@ -13117,12 +13361,12 @@ async function blockSummaries(cms, user, body) {
|
|
|
13117
13361
|
const summary = field.blocks.find((b) => b.slug === blockType)?.admin?.summary;
|
|
13118
13362
|
if (typeof id !== "string" || typeof summary !== "function") continue;
|
|
13119
13363
|
try {
|
|
13120
|
-
const
|
|
13364
|
+
const text4 = await summary({
|
|
13121
13365
|
fields: fields && typeof fields === "object" ? fields : {},
|
|
13122
13366
|
user,
|
|
13123
13367
|
cms
|
|
13124
13368
|
});
|
|
13125
|
-
if (typeof
|
|
13369
|
+
if (typeof text4 === "string") summaries[id] = text4;
|
|
13126
13370
|
} catch (error) {
|
|
13127
13371
|
cms.logger.warn(`admin.summary of block "${String(blockType)}" failed: ${String(error)}`);
|
|
13128
13372
|
}
|
|
@@ -13231,9 +13475,193 @@ function parseDepth(url) {
|
|
|
13231
13475
|
return depth === void 0 ? {} : { depth };
|
|
13232
13476
|
}
|
|
13233
13477
|
|
|
13478
|
+
// src/rest/saved-views.ts
|
|
13479
|
+
var MAX_OWN_VIEWS = 50;
|
|
13480
|
+
var MAX_STATE = 1e4;
|
|
13481
|
+
var QUERY_KEY = /^(where\[and\]\[\d+\]\[[^\]]+\]\[[a-z_]+\]|q|sort|scheduled|type|calendar)$/;
|
|
13482
|
+
var userKey = (user) => String(user.id);
|
|
13483
|
+
var stamps = () => {
|
|
13484
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13485
|
+
return { createdAt: now, updatedAt: now };
|
|
13486
|
+
};
|
|
13487
|
+
async function readable2(cms, slug2, who) {
|
|
13488
|
+
if (INTERNAL_COLLECTIONS.has(slug2) || !cms.config.collections.some((c) => c.slug === slug2))
|
|
13489
|
+
throw new NotFoundError("collections", slug2);
|
|
13490
|
+
try {
|
|
13491
|
+
await cms.count(slug2, { ...who, overrideAccess: false, where: { id: { exists: false } } });
|
|
13492
|
+
} catch {
|
|
13493
|
+
throw new ForbiddenError();
|
|
13494
|
+
}
|
|
13495
|
+
}
|
|
13496
|
+
function cleanState(raw) {
|
|
13497
|
+
const input = raw && typeof raw === "object" ? raw : {};
|
|
13498
|
+
const query = {};
|
|
13499
|
+
const rawQuery = input.query && typeof input.query === "object" ? input.query : {};
|
|
13500
|
+
for (const [key, value] of Object.entries(rawQuery))
|
|
13501
|
+
if (QUERY_KEY.test(key) && typeof value === "string") query[key] = value;
|
|
13502
|
+
const columns = Array.isArray(input.columns) ? input.columns.filter((c) => typeof c === "string").slice(0, 50) : void 0;
|
|
13503
|
+
const pageSize = [10, 25, 50, 100].includes(input.pageSize) ? input.pageSize : void 0;
|
|
13504
|
+
const density = input.density === "compact" || input.density === "comfortable" ? input.density : void 0;
|
|
13505
|
+
const state = {
|
|
13506
|
+
query,
|
|
13507
|
+
...columns ? { columns } : {},
|
|
13508
|
+
...pageSize ? { pageSize } : {},
|
|
13509
|
+
...density ? { density } : {}
|
|
13510
|
+
};
|
|
13511
|
+
if (JSON.stringify(state).length > MAX_STATE)
|
|
13512
|
+
throw new ValidationError(SAVED_VIEWS, [{ field: "state", message: "is too large" }]);
|
|
13513
|
+
return state;
|
|
13514
|
+
}
|
|
13515
|
+
function toView(row, user) {
|
|
13516
|
+
const mine2 = row.user === userKey(user);
|
|
13517
|
+
return {
|
|
13518
|
+
id: row.id,
|
|
13519
|
+
name: String(row.name ?? ""),
|
|
13520
|
+
shared: row.shared === true,
|
|
13521
|
+
mine: mine2,
|
|
13522
|
+
editable: mine2 || row.shared === true && isSystemAdmin(user),
|
|
13523
|
+
state: cleanState(row.state)
|
|
13524
|
+
};
|
|
13525
|
+
}
|
|
13526
|
+
async function rows(cms, slug2, where) {
|
|
13527
|
+
const found = await cms.db.find({
|
|
13528
|
+
collection: SAVED_VIEWS,
|
|
13529
|
+
where: { and: [{ collection: { equals: slug2 } }, where] },
|
|
13530
|
+
sort: ["id"],
|
|
13531
|
+
limit: 0,
|
|
13532
|
+
page: 1
|
|
13533
|
+
});
|
|
13534
|
+
return found.docs;
|
|
13535
|
+
}
|
|
13536
|
+
async function listViews(cms, slug2, who) {
|
|
13537
|
+
await readable2(cms, slug2, who);
|
|
13538
|
+
const key = userKey(who.user);
|
|
13539
|
+
const found = await rows(cms, slug2, {
|
|
13540
|
+
or: [
|
|
13541
|
+
{ and: [{ kind: { equals: "view" } }, { shared: { equals: true } }] },
|
|
13542
|
+
{ user: { equals: key } }
|
|
13543
|
+
]
|
|
13544
|
+
});
|
|
13545
|
+
const views = found.filter((r) => r.kind === "view").map((r) => toView(r, who.user));
|
|
13546
|
+
const preference = found.find((r) => r.kind === "default" && r.user === key);
|
|
13547
|
+
return {
|
|
13548
|
+
views: [...views.filter((v) => v.shared), ...views.filter((v) => !v.shared)],
|
|
13549
|
+
default: typeof preference?.target === "string" ? preference.target : null
|
|
13550
|
+
};
|
|
13551
|
+
}
|
|
13552
|
+
function checkName(name) {
|
|
13553
|
+
const trimmed = typeof name === "string" ? name.trim() : "";
|
|
13554
|
+
if (trimmed === "" || trimmed.length > 60)
|
|
13555
|
+
throw new ValidationError(SAVED_VIEWS, [
|
|
13556
|
+
{ field: "name", message: "must be 1 to 60 characters" }
|
|
13557
|
+
]);
|
|
13558
|
+
return trimmed;
|
|
13559
|
+
}
|
|
13560
|
+
async function checkUnique(cms, slug2, who, name, shared, self) {
|
|
13561
|
+
const same = await rows(cms, slug2, {
|
|
13562
|
+
and: [
|
|
13563
|
+
{ kind: { equals: "view" } },
|
|
13564
|
+
{ name: { equals: name } },
|
|
13565
|
+
shared ? { shared: { equals: true } } : { user: { equals: userKey(who.user) } }
|
|
13566
|
+
]
|
|
13567
|
+
});
|
|
13568
|
+
if (same.some((r) => r.id !== self && (shared || r.shared !== true)))
|
|
13569
|
+
throw new ValidationError(SAVED_VIEWS, [
|
|
13570
|
+
{ field: "name", message: `there is already a view named "${name}"` }
|
|
13571
|
+
]);
|
|
13572
|
+
}
|
|
13573
|
+
async function createView(cms, slug2, who, body) {
|
|
13574
|
+
await readable2(cms, slug2, who);
|
|
13575
|
+
const input = body ?? {};
|
|
13576
|
+
const name = checkName(input.name);
|
|
13577
|
+
const shared = input.shared === true;
|
|
13578
|
+
await checkUnique(cms, slug2, who, name, shared);
|
|
13579
|
+
const own = await rows(cms, slug2, {
|
|
13580
|
+
and: [{ kind: { equals: "view" } }, { user: { equals: userKey(who.user) } }]
|
|
13581
|
+
});
|
|
13582
|
+
if (own.length >= MAX_OWN_VIEWS)
|
|
13583
|
+
throw new ValidationError(SAVED_VIEWS, [
|
|
13584
|
+
{ field: "name", message: `at most ${MAX_OWN_VIEWS} views per list` }
|
|
13585
|
+
]);
|
|
13586
|
+
const row = await cms.db.create({
|
|
13587
|
+
collection: SAVED_VIEWS,
|
|
13588
|
+
data: {
|
|
13589
|
+
collection: slug2,
|
|
13590
|
+
user: userKey(who.user),
|
|
13591
|
+
kind: "view",
|
|
13592
|
+
name,
|
|
13593
|
+
shared,
|
|
13594
|
+
state: cleanState(input.state),
|
|
13595
|
+
target: null,
|
|
13596
|
+
...stamps()
|
|
13597
|
+
}
|
|
13598
|
+
});
|
|
13599
|
+
return toView(row, who.user);
|
|
13600
|
+
}
|
|
13601
|
+
async function editable(cms, slug2, who, id) {
|
|
13602
|
+
await readable2(cms, slug2, who);
|
|
13603
|
+
const row = await cms.db.findById({ collection: SAVED_VIEWS, id });
|
|
13604
|
+
if (!row || row.collection !== slug2 || row.kind !== "view")
|
|
13605
|
+
throw new NotFoundError(SAVED_VIEWS, id);
|
|
13606
|
+
const view = toView(row, who.user);
|
|
13607
|
+
if (!view.mine && !view.shared) throw new NotFoundError(SAVED_VIEWS, id);
|
|
13608
|
+
if (!view.editable) throw new ForbiddenError("Only its owner or an admin can change this view");
|
|
13609
|
+
return row;
|
|
13610
|
+
}
|
|
13611
|
+
async function updateView(cms, slug2, who, id, body) {
|
|
13612
|
+
const row = await editable(cms, slug2, who, id);
|
|
13613
|
+
const input = body ?? {};
|
|
13614
|
+
const name = input.name === void 0 ? String(row.name) : checkName(input.name);
|
|
13615
|
+
const shared = input.shared === void 0 ? row.shared === true : input.shared === true;
|
|
13616
|
+
if (name !== row.name || shared !== (row.shared === true))
|
|
13617
|
+
await checkUnique(cms, slug2, who, name, shared, id);
|
|
13618
|
+
const updated = await cms.db.update({
|
|
13619
|
+
collection: SAVED_VIEWS,
|
|
13620
|
+
id,
|
|
13621
|
+
// The whole row, as the adapter writes it.
|
|
13622
|
+
data: {
|
|
13623
|
+
...row,
|
|
13624
|
+
name,
|
|
13625
|
+
shared,
|
|
13626
|
+
...input.state !== void 0 ? { state: cleanState(input.state) } : {},
|
|
13627
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13628
|
+
}
|
|
13629
|
+
});
|
|
13630
|
+
return toView(updated, who.user);
|
|
13631
|
+
}
|
|
13632
|
+
async function deleteView(cms, slug2, who, id) {
|
|
13633
|
+
await editable(cms, slug2, who, id);
|
|
13634
|
+
await cms.db.delete({ collection: SAVED_VIEWS, id });
|
|
13635
|
+
return { deleted: 1 };
|
|
13636
|
+
}
|
|
13637
|
+
async function setDefaultView(cms, slug2, who, body) {
|
|
13638
|
+
await readable2(cms, slug2, who);
|
|
13639
|
+
const raw = body ?? {};
|
|
13640
|
+
const target = raw.view === null || raw.view === void 0 ? null : String(raw.view);
|
|
13641
|
+
const key = userKey(who.user);
|
|
13642
|
+
const current = await rows(cms, slug2, {
|
|
13643
|
+
and: [{ kind: { equals: "default" } }, { user: { equals: key } }]
|
|
13644
|
+
});
|
|
13645
|
+
for (const row of current) await cms.db.delete({ collection: SAVED_VIEWS, id: row.id });
|
|
13646
|
+
if (target !== null)
|
|
13647
|
+
await cms.db.create({
|
|
13648
|
+
collection: SAVED_VIEWS,
|
|
13649
|
+
data: {
|
|
13650
|
+
collection: slug2,
|
|
13651
|
+
user: key,
|
|
13652
|
+
kind: "default",
|
|
13653
|
+
name: null,
|
|
13654
|
+
shared: false,
|
|
13655
|
+
state: null,
|
|
13656
|
+
target,
|
|
13657
|
+
...stamps()
|
|
13658
|
+
}
|
|
13659
|
+
});
|
|
13660
|
+
return { default: target };
|
|
13661
|
+
}
|
|
13662
|
+
|
|
13234
13663
|
// src/rest/handler.ts
|
|
13235
13664
|
var SCHEDULED_RANGE_LIMIT = 500;
|
|
13236
|
-
var LIST_TOTAL_MAX = 1e4;
|
|
13237
13665
|
var CSRF_COOKIE = "ecms-csrf";
|
|
13238
13666
|
var CSRF_HEADER = "x-csrf-token";
|
|
13239
13667
|
var MAX_BODY_BYTES = 1024 * 1024;
|
|
@@ -13423,7 +13851,8 @@ var ADMIN_UI_ROUTES = /* @__PURE__ */ new Set([
|
|
|
13423
13851
|
"media-usage",
|
|
13424
13852
|
"owned",
|
|
13425
13853
|
"sso",
|
|
13426
|
-
"block-summaries"
|
|
13854
|
+
"block-summaries",
|
|
13855
|
+
"views"
|
|
13427
13856
|
]);
|
|
13428
13857
|
var CORS_METHODS = "GET, HEAD, POST, PATCH, PUT, DELETE";
|
|
13429
13858
|
var CORS_HEADERS = `authorization, content-type, ${CSRF_HEADER}`;
|
|
@@ -13800,6 +14229,27 @@ async function route(cms, ctx, method, path, options) {
|
|
|
13800
14229
|
if (parsed === void 0) throw new HttpError("Not found", 404);
|
|
13801
14230
|
return { body: await cms.roles.owned(parsed) };
|
|
13802
14231
|
}
|
|
14232
|
+
if (first === "admin" && second === "views" && third !== void 0 && segments.length <= 4) {
|
|
14233
|
+
if (!ctx.user || ctx.user.apiKey) throw new UnauthorizedError();
|
|
14234
|
+
const who = { user: ctx.user, context: ctx.context };
|
|
14235
|
+
const fourth = segments[3];
|
|
14236
|
+
if (fourth === void 0) {
|
|
14237
|
+
if (method === "GET") return { body: await listViews(cms, third, who) };
|
|
14238
|
+
if (method === "POST")
|
|
14239
|
+
return { status: 201, body: await createView(cms, third, who, await readJson(ctx.request)) };
|
|
14240
|
+
throw methodNotAllowed(ctx, "GET, POST");
|
|
14241
|
+
}
|
|
14242
|
+
if (fourth === "default") {
|
|
14243
|
+
if (method !== "PUT") throw methodNotAllowed(ctx, "PUT");
|
|
14244
|
+
return { body: await setDefaultView(cms, third, who, await readJson(ctx.request)) };
|
|
14245
|
+
}
|
|
14246
|
+
const id2 = parseId(fourth);
|
|
14247
|
+
if (id2 === void 0) throw new HttpError("Not found", 404);
|
|
14248
|
+
if (method === "PATCH")
|
|
14249
|
+
return { body: await updateView(cms, third, who, id2, await readJson(ctx.request)) };
|
|
14250
|
+
if (method === "DELETE") return { body: await deleteView(cms, third, who, id2) };
|
|
14251
|
+
throw methodNotAllowed(ctx, "PATCH, DELETE");
|
|
14252
|
+
}
|
|
13803
14253
|
if (first === "admin" && second === "block-summaries" && third === void 0) {
|
|
13804
14254
|
if (!ctx.user) throw new UnauthorizedError();
|
|
13805
14255
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
@@ -13948,7 +14398,14 @@ async function route(cms, ctx, method, path, options) {
|
|
|
13948
14398
|
if (second === "schema" && third === void 0)
|
|
13949
14399
|
return { body: await adminSchema(cms, ctx.user, ctx.url.origin, ctx.context) };
|
|
13950
14400
|
if (second === "counts" && third === void 0)
|
|
13951
|
-
return {
|
|
14401
|
+
return {
|
|
14402
|
+
body: await adminCounts(
|
|
14403
|
+
cms,
|
|
14404
|
+
{ user: ctx.user, context: ctx.context },
|
|
14405
|
+
// `?fresh`: after creating or deleting, not the numbers kept for 30 s.
|
|
14406
|
+
{ fresh: ctx.url.searchParams.has("fresh") }
|
|
14407
|
+
)
|
|
14408
|
+
};
|
|
13952
14409
|
if (second === "search" && third === void 0)
|
|
13953
14410
|
return {
|
|
13954
14411
|
body: {
|
|
@@ -14139,6 +14596,21 @@ async function route(cms, ctx, method, path, options) {
|
|
|
14139
14596
|
if (INTERNAL_COLLECTIONS.has(collection2) || !cms.config.collections.some((c) => c.slug === collection2)) {
|
|
14140
14597
|
throw new HttpError(`Unknown collection "${collection2}"`, 404);
|
|
14141
14598
|
}
|
|
14599
|
+
if (second === "export.csv" && third === void 0) {
|
|
14600
|
+
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
14601
|
+
if (!ctx.user) throw new UnauthorizedError();
|
|
14602
|
+
const { where, sort } = parseListQuery(ctx.url);
|
|
14603
|
+
return {
|
|
14604
|
+
body: await exportCsv(cms, collection2, ctx.url, {
|
|
14605
|
+
user: ctx.user,
|
|
14606
|
+
context: ctx.context,
|
|
14607
|
+
...parseLocale(ctx.url),
|
|
14608
|
+
draft,
|
|
14609
|
+
...where ? { where } : {},
|
|
14610
|
+
...sort ? { sort } : {}
|
|
14611
|
+
})
|
|
14612
|
+
};
|
|
14613
|
+
}
|
|
14142
14614
|
if (second === "bulk") return bulkRoute(cms, ctx, method, collection2, segments.slice(2), access);
|
|
14143
14615
|
if (second !== void 0 && third !== void 0) {
|
|
14144
14616
|
return documentAction(cms, ctx, method, collection2, second, segments.slice(2));
|
|
@@ -14596,10 +15068,10 @@ async function readJson(request) {
|
|
|
14596
15068
|
if (!type.toLowerCase().startsWith("application/json")) {
|
|
14597
15069
|
throw new HttpError("Content-Type must be application/json", 415);
|
|
14598
15070
|
}
|
|
14599
|
-
const
|
|
15071
|
+
const text4 = new TextDecoder().decode(await readBody(request, MAX_BODY_BYTES));
|
|
14600
15072
|
let body;
|
|
14601
15073
|
try {
|
|
14602
|
-
body = JSON.parse(
|
|
15074
|
+
body = JSON.parse(text4);
|
|
14603
15075
|
} catch {
|
|
14604
15076
|
throw new HttpError("Request body is not valid JSON", 400);
|
|
14605
15077
|
}
|