@easy-cms/core 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +114 -1
- package/dist/index.js +748 -266
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -116,10 +116,10 @@ function cleanPermissions(value, known) {
|
|
|
116
116
|
const pick = (group, slugs, operations) => {
|
|
117
117
|
const out = {};
|
|
118
118
|
if (typeof group !== "object" || group === null) return out;
|
|
119
|
-
for (const [
|
|
120
|
-
if (!slugs.includes(
|
|
119
|
+
for (const [slug2, ops] of Object.entries(group)) {
|
|
120
|
+
if (!slugs.includes(slug2) || API_KEY_EXCLUDED.has(slug2) || !Array.isArray(ops)) continue;
|
|
121
121
|
const valid = operations.filter((op) => ops.includes(op));
|
|
122
|
-
if (valid.length) out[
|
|
122
|
+
if (valid.length) out[slug2] = valid;
|
|
123
123
|
}
|
|
124
124
|
return out;
|
|
125
125
|
};
|
|
@@ -150,8 +150,8 @@ ${lines.join("\n")}`;
|
|
|
150
150
|
};
|
|
151
151
|
var EasyCMSError = class extends Error {
|
|
152
152
|
status;
|
|
153
|
-
constructor(
|
|
154
|
-
super(
|
|
153
|
+
constructor(message2, status) {
|
|
154
|
+
super(message2);
|
|
155
155
|
this.name = "EasyCMSError";
|
|
156
156
|
this.status = status;
|
|
157
157
|
}
|
|
@@ -172,38 +172,38 @@ var NotFoundError = class extends EasyCMSError {
|
|
|
172
172
|
}
|
|
173
173
|
};
|
|
174
174
|
var QueryError = class extends EasyCMSError {
|
|
175
|
-
constructor(
|
|
176
|
-
super(
|
|
175
|
+
constructor(message2) {
|
|
176
|
+
super(message2, 400);
|
|
177
177
|
this.name = "QueryError";
|
|
178
178
|
}
|
|
179
179
|
};
|
|
180
180
|
var SchemaError = class extends EasyCMSError {
|
|
181
|
-
constructor(
|
|
182
|
-
super(
|
|
181
|
+
constructor(message2) {
|
|
182
|
+
super(message2, 500);
|
|
183
183
|
this.name = "SchemaError";
|
|
184
184
|
}
|
|
185
185
|
};
|
|
186
186
|
var UnauthorizedError = class extends EasyCMSError {
|
|
187
|
-
constructor(
|
|
188
|
-
super(
|
|
187
|
+
constructor(message2 = "You must be logged in") {
|
|
188
|
+
super(message2, 401);
|
|
189
189
|
this.name = "UnauthorizedError";
|
|
190
190
|
}
|
|
191
191
|
};
|
|
192
192
|
var ForbiddenError = class extends EasyCMSError {
|
|
193
|
-
constructor(
|
|
194
|
-
super(
|
|
193
|
+
constructor(message2 = "You are not allowed to do this") {
|
|
194
|
+
super(message2, 403);
|
|
195
195
|
this.name = "ForbiddenError";
|
|
196
196
|
}
|
|
197
197
|
};
|
|
198
198
|
var TooManyRequestsError = class extends EasyCMSError {
|
|
199
|
-
constructor(
|
|
200
|
-
super(
|
|
199
|
+
constructor(message2) {
|
|
200
|
+
super(message2, 429);
|
|
201
201
|
this.name = "TooManyRequestsError";
|
|
202
202
|
}
|
|
203
203
|
};
|
|
204
204
|
var PayloadTooLargeError = class extends EasyCMSError {
|
|
205
|
-
constructor(
|
|
206
|
-
super(
|
|
205
|
+
constructor(message2) {
|
|
206
|
+
super(message2, 413);
|
|
207
207
|
this.name = "PayloadTooLargeError";
|
|
208
208
|
}
|
|
209
209
|
};
|
|
@@ -293,22 +293,22 @@ async function stripFields(fields, doc, read2) {
|
|
|
293
293
|
}
|
|
294
294
|
return result;
|
|
295
295
|
}
|
|
296
|
-
async function filterInput(fields, data,
|
|
296
|
+
async function filterInput(fields, data, update2) {
|
|
297
297
|
const result = { ...data };
|
|
298
298
|
for (const field of fields) {
|
|
299
299
|
if (!(field.name in result)) continue;
|
|
300
|
-
if (field.hidden ||
|
|
300
|
+
if (field.hidden || update2 && !await update2.allows(field)) {
|
|
301
301
|
delete result[field.name];
|
|
302
302
|
continue;
|
|
303
303
|
}
|
|
304
304
|
const value = result[field.name];
|
|
305
305
|
if (field.type === "group" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
306
|
-
result[field.name] = await filterInput(field.fields, value,
|
|
306
|
+
result[field.name] = await filterInput(field.fields, value, update2);
|
|
307
307
|
} else if (hasRows(field) && Array.isArray(value)) {
|
|
308
308
|
result[field.name] = await Promise.all(
|
|
309
309
|
value.map((row) => {
|
|
310
310
|
const fields2 = rowFields(field, row);
|
|
311
|
-
return fields2 && row && typeof row === "object" ? filterInput(fields2, row,
|
|
311
|
+
return fields2 && row && typeof row === "object" ? filterInput(fields2, row, update2) : row;
|
|
312
312
|
})
|
|
313
313
|
);
|
|
314
314
|
}
|
|
@@ -325,13 +325,15 @@ var VERSIONS = "document-versions";
|
|
|
325
325
|
var SCHEDULED_JOBS = "scheduled-jobs";
|
|
326
326
|
var WEBHOOK_DELIVERIES = "webhook-deliveries";
|
|
327
327
|
var EMAIL_DELIVERIES = "email-deliveries";
|
|
328
|
+
var DATABASE_BACKUPS = "database-backups";
|
|
328
329
|
var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
329
330
|
SESSIONS,
|
|
330
331
|
LOGIN_ATTEMPTS,
|
|
331
332
|
VERSIONS,
|
|
332
333
|
SCHEDULED_JOBS,
|
|
333
334
|
WEBHOOK_DELIVERIES,
|
|
334
|
-
EMAIL_DELIVERIES
|
|
335
|
+
EMAIL_DELIVERIES,
|
|
336
|
+
DATABASE_BACKUPS
|
|
335
337
|
]);
|
|
336
338
|
var DEFAULT_ROLES = ["admin", "editor"];
|
|
337
339
|
var adminOrSelf = ({ user }) => {
|
|
@@ -546,6 +548,25 @@ var webhookDeliveriesCollection = {
|
|
|
546
548
|
{ name: "error", type: "text" }
|
|
547
549
|
]
|
|
548
550
|
};
|
|
551
|
+
var databaseBackupsCollection = {
|
|
552
|
+
slug: DATABASE_BACKUPS,
|
|
553
|
+
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
554
|
+
fields: [
|
|
555
|
+
// pending | running | done | failed
|
|
556
|
+
{ name: "state", type: "text", required: true, index: true },
|
|
557
|
+
// manual | scheduled
|
|
558
|
+
{ name: "trigger", type: "text", required: true },
|
|
559
|
+
{ name: "filename", type: "text" },
|
|
560
|
+
{ name: "size", type: "number" },
|
|
561
|
+
{ name: "startedAt", type: "text", index: true },
|
|
562
|
+
{ name: "finishedAt", type: "text" },
|
|
563
|
+
{ name: "error", type: "text" },
|
|
564
|
+
// Who started it (email), for manual ones.
|
|
565
|
+
{ name: "author", type: "text" },
|
|
566
|
+
{ name: "downloadedBy", type: "text" },
|
|
567
|
+
{ name: "downloadedAt", type: "text" }
|
|
568
|
+
]
|
|
569
|
+
};
|
|
549
570
|
var emailDeliveriesCollection = {
|
|
550
571
|
slug: EMAIL_DELIVERIES,
|
|
551
572
|
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
@@ -1089,6 +1110,386 @@ var Auth = class {
|
|
|
1089
1110
|
}
|
|
1090
1111
|
};
|
|
1091
1112
|
|
|
1113
|
+
// src/backups.ts
|
|
1114
|
+
import { mkdtemp, readFile as readFile2, rm as rm2 } from "fs/promises";
|
|
1115
|
+
import { tmpdir } from "os";
|
|
1116
|
+
import { join as join2 } from "path";
|
|
1117
|
+
import { promisify } from "util";
|
|
1118
|
+
import { gzip } from "zlib";
|
|
1119
|
+
|
|
1120
|
+
// src/copy.ts
|
|
1121
|
+
var BATCH = 200;
|
|
1122
|
+
async function copyDatabase(source, target, options = {}) {
|
|
1123
|
+
const from = source.transfer;
|
|
1124
|
+
const to = target.transfer;
|
|
1125
|
+
if (!from || !to) throw new EasyCMSError("This database adapter cannot copy databases", 400);
|
|
1126
|
+
if (from.schemaHash !== to.schemaHash) {
|
|
1127
|
+
throw new EasyCMSError(
|
|
1128
|
+
"The two configs describe different collections or fields. Copy between configs with the same collections (import one config into the other and change only `db`).",
|
|
1129
|
+
400
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
const tables = from.tables();
|
|
1133
|
+
for (const table of tables) {
|
|
1134
|
+
if (await to.count(table) > 0) {
|
|
1135
|
+
throw new EasyCMSError(
|
|
1136
|
+
`The target database is not empty ("${table}" has rows). Copy into a new, empty database.`,
|
|
1137
|
+
400
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
let total = 0;
|
|
1142
|
+
for (const table of tables) {
|
|
1143
|
+
let copied = 0;
|
|
1144
|
+
for (; ; ) {
|
|
1145
|
+
const rows = await from.read(table, copied, BATCH);
|
|
1146
|
+
if (rows.length === 0) break;
|
|
1147
|
+
await to.write(table, rows);
|
|
1148
|
+
copied += rows.length;
|
|
1149
|
+
if (rows.length < BATCH) break;
|
|
1150
|
+
}
|
|
1151
|
+
total += copied;
|
|
1152
|
+
options.onTable?.({ table, rows: copied });
|
|
1153
|
+
}
|
|
1154
|
+
await to.finish();
|
|
1155
|
+
return { tables: tables.length, rows: total };
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/storage.ts
|
|
1159
|
+
import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
1160
|
+
import { dirname, isAbsolute, join, normalize, resolve, sep } from "path";
|
|
1161
|
+
function localStorage(options = {}) {
|
|
1162
|
+
let root = resolve(
|
|
1163
|
+
/* turbopackIgnore: true */
|
|
1164
|
+
options.dir ?? "uploads"
|
|
1165
|
+
);
|
|
1166
|
+
const pathOf = (key2) => {
|
|
1167
|
+
const path = normalize(join(
|
|
1168
|
+
/* turbopackIgnore: true */
|
|
1169
|
+
root,
|
|
1170
|
+
key2
|
|
1171
|
+
));
|
|
1172
|
+
if (!path.startsWith(root + sep)) throw new Error(`Invalid storage key "${key2}"`);
|
|
1173
|
+
return path;
|
|
1174
|
+
};
|
|
1175
|
+
return {
|
|
1176
|
+
name: "local",
|
|
1177
|
+
init({ cwd }) {
|
|
1178
|
+
const dir = options.dir ?? "uploads";
|
|
1179
|
+
root = isAbsolute(dir) ? dir : resolve(
|
|
1180
|
+
/* turbopackIgnore: true */
|
|
1181
|
+
cwd,
|
|
1182
|
+
dir
|
|
1183
|
+
);
|
|
1184
|
+
},
|
|
1185
|
+
async put(key2, data) {
|
|
1186
|
+
const path = pathOf(key2);
|
|
1187
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1188
|
+
await writeFile(path, data, { flag: "wx" });
|
|
1189
|
+
},
|
|
1190
|
+
async get(key2) {
|
|
1191
|
+
const path = pathOf(key2);
|
|
1192
|
+
const info = await stat(path).catch(() => void 0);
|
|
1193
|
+
if (!info?.isFile()) return null;
|
|
1194
|
+
return { body: new Uint8Array(await readFile(path)), size: info.size };
|
|
1195
|
+
},
|
|
1196
|
+
async delete(key2) {
|
|
1197
|
+
await rm(pathOf(key2), { force: true });
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// src/backups.ts
|
|
1203
|
+
var gzipAsync = promisify(gzip);
|
|
1204
|
+
var DEFAULT_BACKUP_TIME = "03:00";
|
|
1205
|
+
var DEFAULT_BACKUP_KEEP = 7;
|
|
1206
|
+
var DEFAULT_DIR = "backups";
|
|
1207
|
+
var STALE = 30 * 6e4;
|
|
1208
|
+
var PERIOD = { day: 864e5, week: 7 * 864e5 };
|
|
1209
|
+
var storages = /* @__PURE__ */ new WeakMap();
|
|
1210
|
+
function backupStorage(cms) {
|
|
1211
|
+
let storage = storages.get(cms);
|
|
1212
|
+
if (!storage) {
|
|
1213
|
+
storage = (async () => {
|
|
1214
|
+
const config = cms.config.backups;
|
|
1215
|
+
const adapter = config?.storage ?? localStorage({ dir: config?.dir ?? DEFAULT_DIR });
|
|
1216
|
+
await adapter.init?.({ cwd: cms.cwd });
|
|
1217
|
+
return adapter;
|
|
1218
|
+
})();
|
|
1219
|
+
storages.set(cms, storage);
|
|
1220
|
+
}
|
|
1221
|
+
return storage;
|
|
1222
|
+
}
|
|
1223
|
+
function backupUnavailable(cms) {
|
|
1224
|
+
if (cms.db.backup || cms.config.backups?.sqlite) return null;
|
|
1225
|
+
return `The ${cms.config.db.name} database needs the SQLite adapter to write backups: backups: { sqlite } with sqlite from @easy-cms/db-sqlite`;
|
|
1226
|
+
}
|
|
1227
|
+
async function writeBackupFile(cms, file) {
|
|
1228
|
+
if (cms.db.backup) return cms.db.backup(file);
|
|
1229
|
+
const factory = cms.config.backups?.sqlite;
|
|
1230
|
+
if (!factory) throw new EasyCMSError(backupUnavailable(cms), 400);
|
|
1231
|
+
const prefix = cms.db.tablePrefix;
|
|
1232
|
+
const scratch = `${file}.copy`;
|
|
1233
|
+
const adapter = factory({ url: `file:${scratch}`, ...prefix ? { tablePrefix: prefix } : {} });
|
|
1234
|
+
const target = await adapter.init({
|
|
1235
|
+
config: { ...cms.config, db: adapter },
|
|
1236
|
+
cwd: cms.cwd,
|
|
1237
|
+
schema: "push",
|
|
1238
|
+
logger: { ...cms.logger, info() {
|
|
1239
|
+
} }
|
|
1240
|
+
});
|
|
1241
|
+
try {
|
|
1242
|
+
await copyDatabase(cms.db, target);
|
|
1243
|
+
if (!target.backup) throw new EasyCMSError("backups.sqlite must be the SQLite adapter", 400);
|
|
1244
|
+
await target.backup(file);
|
|
1245
|
+
} finally {
|
|
1246
|
+
await target.destroy();
|
|
1247
|
+
for (const suffix of ["", "-wal", "-shm"])
|
|
1248
|
+
await rm2(`${scratch}${suffix}`, { force: true }).catch(() => {
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
var slug = (text) => text.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "easy-cms";
|
|
1253
|
+
function filenameFor(cms, date) {
|
|
1254
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1255
|
+
const stamp = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}`;
|
|
1256
|
+
return `${slug(cms.config.admin.brand.name || "easy-cms")}-${stamp}.db.gz`;
|
|
1257
|
+
}
|
|
1258
|
+
function toAdmin(row) {
|
|
1259
|
+
const text = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
1260
|
+
return {
|
|
1261
|
+
id: row.id,
|
|
1262
|
+
state: ["pending", "running", "done", "failed"].includes(row.state) ? row.state : "failed",
|
|
1263
|
+
trigger: row.trigger === "scheduled" ? "scheduled" : "manual",
|
|
1264
|
+
filename: text(row.filename),
|
|
1265
|
+
size: typeof row.size === "number" ? row.size : null,
|
|
1266
|
+
startedAt: text(row.startedAt),
|
|
1267
|
+
finishedAt: text(row.finishedAt),
|
|
1268
|
+
error: text(row.error),
|
|
1269
|
+
author: text(row.author),
|
|
1270
|
+
downloadedBy: text(row.downloadedBy),
|
|
1271
|
+
downloadedAt: text(row.downloadedAt)
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
async function update(cms, row, data) {
|
|
1275
|
+
const { id, ...rest } = row;
|
|
1276
|
+
const next = { ...rest, ...data, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1277
|
+
await cms.db.update({ collection: DATABASE_BACKUPS, id, data: next });
|
|
1278
|
+
return { ...row, ...data };
|
|
1279
|
+
}
|
|
1280
|
+
async function listBackups(cms) {
|
|
1281
|
+
const config = cms.config.backups;
|
|
1282
|
+
const storage = await backupStorage(cms);
|
|
1283
|
+
const { docs } = await cms.db.find({
|
|
1284
|
+
collection: DATABASE_BACKUPS,
|
|
1285
|
+
sort: ["-createdAt"],
|
|
1286
|
+
limit: 0,
|
|
1287
|
+
page: 1
|
|
1288
|
+
});
|
|
1289
|
+
return {
|
|
1290
|
+
settings: {
|
|
1291
|
+
every: config?.every ?? null,
|
|
1292
|
+
at: config?.at ?? DEFAULT_BACKUP_TIME,
|
|
1293
|
+
keep: config?.keep ?? DEFAULT_BACKUP_KEEP,
|
|
1294
|
+
storage: storage.name,
|
|
1295
|
+
dir: config?.storage ? null : config?.dir ?? DEFAULT_DIR,
|
|
1296
|
+
database: cms.config.db.name,
|
|
1297
|
+
unavailable: backupUnavailable(cms)
|
|
1298
|
+
},
|
|
1299
|
+
backups: docs.map(toAdmin)
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
async function inProgress(cms, now) {
|
|
1303
|
+
const { docs } = await cms.db.find({
|
|
1304
|
+
collection: DATABASE_BACKUPS,
|
|
1305
|
+
where: { state: { in: ["pending", "running"] } },
|
|
1306
|
+
sort: ["-createdAt"],
|
|
1307
|
+
limit: 10,
|
|
1308
|
+
page: 1
|
|
1309
|
+
});
|
|
1310
|
+
return docs.find((row) => now - Date.parse(String(row.createdAt)) < STALE);
|
|
1311
|
+
}
|
|
1312
|
+
async function startBackup(cms, trigger, author) {
|
|
1313
|
+
const unavailable = backupUnavailable(cms);
|
|
1314
|
+
if (unavailable) throw new EasyCMSError(unavailable, 400);
|
|
1315
|
+
const now = /* @__PURE__ */ new Date();
|
|
1316
|
+
if (await inProgress(cms, now.getTime()))
|
|
1317
|
+
throw new EasyCMSError("A backup is already running", 409);
|
|
1318
|
+
const stamp = now.toISOString();
|
|
1319
|
+
const row = await cms.db.create({
|
|
1320
|
+
collection: DATABASE_BACKUPS,
|
|
1321
|
+
data: {
|
|
1322
|
+
state: "pending",
|
|
1323
|
+
trigger,
|
|
1324
|
+
filename: null,
|
|
1325
|
+
size: null,
|
|
1326
|
+
startedAt: null,
|
|
1327
|
+
finishedAt: null,
|
|
1328
|
+
error: null,
|
|
1329
|
+
author: author ?? null,
|
|
1330
|
+
downloadedBy: null,
|
|
1331
|
+
downloadedAt: null,
|
|
1332
|
+
createdAt: stamp,
|
|
1333
|
+
updatedAt: stamp
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
void runBackup(cms, row).catch(
|
|
1337
|
+
(error) => cms.logger.error(`Backup failed: ${error.message}`)
|
|
1338
|
+
);
|
|
1339
|
+
return toAdmin(row);
|
|
1340
|
+
}
|
|
1341
|
+
async function runBackup(cms, record) {
|
|
1342
|
+
const started = /* @__PURE__ */ new Date();
|
|
1343
|
+
let row = await update(cms, record, { state: "running", startedAt: started.toISOString() });
|
|
1344
|
+
const dir = await mkdtemp(join2(tmpdir(), "easy-cms-backup-"));
|
|
1345
|
+
try {
|
|
1346
|
+
const file = join2(dir, "backup.db");
|
|
1347
|
+
await writeBackupFile(cms, file);
|
|
1348
|
+
const data = new Uint8Array(await gzipAsync(await readFile2(file)));
|
|
1349
|
+
const storage = await backupStorage(cms);
|
|
1350
|
+
const filename = filenameFor(cms, started);
|
|
1351
|
+
const key2 = await storage.get(filename) ? filename.replace(/\.db\.gz$/, `-${row.id}.db.gz`) : filename;
|
|
1352
|
+
await storage.put(key2, data, { contentType: "application/gzip" });
|
|
1353
|
+
row = await update(cms, row, {
|
|
1354
|
+
state: "done",
|
|
1355
|
+
filename: key2,
|
|
1356
|
+
size: data.byteLength,
|
|
1357
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1358
|
+
});
|
|
1359
|
+
cms.logger.info(`Backup ${key2} written (${data.byteLength} bytes)`);
|
|
1360
|
+
await pruneBackups(cms);
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
await update(cms, row, {
|
|
1363
|
+
state: "failed",
|
|
1364
|
+
error: (error.message || String(error)).slice(0, 500),
|
|
1365
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1366
|
+
});
|
|
1367
|
+
throw error;
|
|
1368
|
+
} finally {
|
|
1369
|
+
await rm2(dir, { recursive: true, force: true });
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
async function pruneBackups(cms) {
|
|
1373
|
+
const keep = cms.config.backups?.keep ?? DEFAULT_BACKUP_KEEP;
|
|
1374
|
+
const storage = await backupStorage(cms);
|
|
1375
|
+
const { docs } = await cms.db.find({
|
|
1376
|
+
collection: DATABASE_BACKUPS,
|
|
1377
|
+
where: { state: { equals: "done" } },
|
|
1378
|
+
sort: ["-createdAt"],
|
|
1379
|
+
limit: 0,
|
|
1380
|
+
page: 1
|
|
1381
|
+
});
|
|
1382
|
+
for (const row of docs.slice(keep)) {
|
|
1383
|
+
if (typeof row.filename === "string") await storage.delete(row.filename).catch(() => {
|
|
1384
|
+
});
|
|
1385
|
+
await cms.db.delete({ collection: DATABASE_BACKUPS, id: row.id });
|
|
1386
|
+
}
|
|
1387
|
+
const old = new Date(Date.now() - 30 * 864e5).toISOString();
|
|
1388
|
+
const failed = await cms.db.find({
|
|
1389
|
+
collection: DATABASE_BACKUPS,
|
|
1390
|
+
where: { and: [{ state: { equals: "failed" } }, { createdAt: { lt: old } }] },
|
|
1391
|
+
sort: ["createdAt"],
|
|
1392
|
+
limit: 100,
|
|
1393
|
+
page: 1
|
|
1394
|
+
});
|
|
1395
|
+
for (const row of failed.docs) await cms.db.delete({ collection: DATABASE_BACKUPS, id: row.id });
|
|
1396
|
+
}
|
|
1397
|
+
function lastSlot(now, at) {
|
|
1398
|
+
const [hours2, minutes] = at.split(":").map(Number);
|
|
1399
|
+
const slot = new Date(now);
|
|
1400
|
+
slot.setHours(hours2, minutes, 0, 0);
|
|
1401
|
+
if (slot.getTime() > now.getTime()) slot.setDate(slot.getDate() - 1);
|
|
1402
|
+
return slot;
|
|
1403
|
+
}
|
|
1404
|
+
async function runDueBackups(cms, now = /* @__PURE__ */ new Date()) {
|
|
1405
|
+
if (backupUnavailable(cms)) return;
|
|
1406
|
+
const { docs } = await cms.db.find({
|
|
1407
|
+
collection: DATABASE_BACKUPS,
|
|
1408
|
+
where: { state: { in: ["pending", "running"] } },
|
|
1409
|
+
sort: ["createdAt"],
|
|
1410
|
+
limit: 10,
|
|
1411
|
+
page: 1
|
|
1412
|
+
});
|
|
1413
|
+
for (const row of docs) {
|
|
1414
|
+
const age = now.getTime() - Date.parse(String(row.updatedAt ?? row.createdAt));
|
|
1415
|
+
if (age < STALE) continue;
|
|
1416
|
+
if (row.state === "pending") await runBackup(cms, row).catch(() => {
|
|
1417
|
+
});
|
|
1418
|
+
else
|
|
1419
|
+
await update(cms, row, {
|
|
1420
|
+
state: "failed",
|
|
1421
|
+
error: "Interrupted: the server stopped during the backup",
|
|
1422
|
+
finishedAt: now.toISOString()
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
const every = cms.config.backups?.every;
|
|
1426
|
+
if (!every) return;
|
|
1427
|
+
const slot = lastSlot(now, cms.config.backups?.at ?? DEFAULT_BACKUP_TIME);
|
|
1428
|
+
const latest = await cms.db.find({
|
|
1429
|
+
collection: DATABASE_BACKUPS,
|
|
1430
|
+
where: { trigger: { equals: "scheduled" } },
|
|
1431
|
+
sort: ["-createdAt"],
|
|
1432
|
+
limit: 1,
|
|
1433
|
+
page: 1
|
|
1434
|
+
});
|
|
1435
|
+
const last = latest.docs[0] ? Date.parse(String(latest.docs[0].createdAt)) : 0;
|
|
1436
|
+
const due = every === "day" ? last < slot.getTime() : last < slot.getTime() - PERIOD.week + PERIOD.day;
|
|
1437
|
+
if (!due) return;
|
|
1438
|
+
if (await inProgress(cms, now.getTime())) return;
|
|
1439
|
+
await startBackup(cms, "scheduled").catch(
|
|
1440
|
+
(error) => cms.logger.error(`Scheduled backup could not start: ${error.message}`)
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
async function backupAttention(cms, now = Date.now()) {
|
|
1444
|
+
const every = cms.config.backups?.every;
|
|
1445
|
+
if (!every) return null;
|
|
1446
|
+
const [scheduled, done] = await Promise.all([
|
|
1447
|
+
cms.db.find({
|
|
1448
|
+
collection: DATABASE_BACKUPS,
|
|
1449
|
+
where: { trigger: { equals: "scheduled" } },
|
|
1450
|
+
sort: ["-createdAt"],
|
|
1451
|
+
limit: 1,
|
|
1452
|
+
page: 1
|
|
1453
|
+
}),
|
|
1454
|
+
cms.db.find({
|
|
1455
|
+
collection: DATABASE_BACKUPS,
|
|
1456
|
+
where: { state: { equals: "done" } },
|
|
1457
|
+
sort: ["-createdAt"],
|
|
1458
|
+
limit: 1,
|
|
1459
|
+
page: 1
|
|
1460
|
+
})
|
|
1461
|
+
]);
|
|
1462
|
+
const failed = scheduled.docs[0]?.state === "failed";
|
|
1463
|
+
const lastDone = done.docs[0] ? String(done.docs[0].createdAt) : null;
|
|
1464
|
+
let since = lastDone ? Date.parse(lastDone) : void 0;
|
|
1465
|
+
if (since === void 0) {
|
|
1466
|
+
const first = await cms.db.find({ collection: USERS, sort: ["createdAt"], limit: 1, page: 1 });
|
|
1467
|
+
since = first.docs[0] ? Date.parse(String(first.docs[0].createdAt)) : now;
|
|
1468
|
+
}
|
|
1469
|
+
const overdue = now - since > 2 * PERIOD[every];
|
|
1470
|
+
return failed || overdue ? { id: "backups", failed, lastDone } : null;
|
|
1471
|
+
}
|
|
1472
|
+
async function downloadBackup(cms, id, by) {
|
|
1473
|
+
const row = await cms.db.findById({ collection: DATABASE_BACKUPS, id });
|
|
1474
|
+
if (row?.state !== "done" || typeof row.filename !== "string")
|
|
1475
|
+
throw new NotFoundError(DATABASE_BACKUPS, id);
|
|
1476
|
+
const file = await (await backupStorage(cms)).get(row.filename);
|
|
1477
|
+
if (!file)
|
|
1478
|
+
throw new EasyCMSError(`The file ${row.filename} is no longer in the backup storage`, 404);
|
|
1479
|
+
await update(cms, row, { downloadedBy: by, downloadedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1480
|
+
return { filename: row.filename, body: file.body };
|
|
1481
|
+
}
|
|
1482
|
+
async function deleteBackup(cms, id) {
|
|
1483
|
+
const row = await cms.db.findById({ collection: DATABASE_BACKUPS, id });
|
|
1484
|
+
if (!row) throw new NotFoundError(DATABASE_BACKUPS, id);
|
|
1485
|
+
if (row.state === "running" || row.state === "pending")
|
|
1486
|
+
throw new EasyCMSError("A backup in progress cannot be deleted", 409);
|
|
1487
|
+
if (typeof row.filename === "string")
|
|
1488
|
+
await (await backupStorage(cms)).delete(row.filename).catch(() => {
|
|
1489
|
+
});
|
|
1490
|
+
await cms.db.delete({ collection: DATABASE_BACKUPS, id });
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1092
1493
|
// src/config.ts
|
|
1093
1494
|
var ADMIN_ICONS = [
|
|
1094
1495
|
"file-text",
|
|
@@ -1175,44 +1576,6 @@ function configSignature(config) {
|
|
|
1175
1576
|
}
|
|
1176
1577
|
var signatures = /* @__PURE__ */ new WeakMap();
|
|
1177
1578
|
|
|
1178
|
-
// src/copy.ts
|
|
1179
|
-
var BATCH = 200;
|
|
1180
|
-
async function copyDatabase(source, target, options = {}) {
|
|
1181
|
-
const from = source.transfer;
|
|
1182
|
-
const to = target.transfer;
|
|
1183
|
-
if (!from || !to) throw new EasyCMSError("This database adapter cannot copy databases", 400);
|
|
1184
|
-
if (from.schemaHash !== to.schemaHash) {
|
|
1185
|
-
throw new EasyCMSError(
|
|
1186
|
-
"The two configs describe different collections or fields. Copy between configs with the same collections (import one config into the other and change only `db`).",
|
|
1187
|
-
400
|
|
1188
|
-
);
|
|
1189
|
-
}
|
|
1190
|
-
const tables = from.tables();
|
|
1191
|
-
for (const table of tables) {
|
|
1192
|
-
if (await to.count(table) > 0) {
|
|
1193
|
-
throw new EasyCMSError(
|
|
1194
|
-
`The target database is not empty ("${table}" has rows). Copy into a new, empty database.`,
|
|
1195
|
-
400
|
|
1196
|
-
);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
let total = 0;
|
|
1200
|
-
for (const table of tables) {
|
|
1201
|
-
let copied = 0;
|
|
1202
|
-
for (; ; ) {
|
|
1203
|
-
const rows = await from.read(table, copied, BATCH);
|
|
1204
|
-
if (rows.length === 0) break;
|
|
1205
|
-
await to.write(table, rows);
|
|
1206
|
-
copied += rows.length;
|
|
1207
|
-
if (rows.length < BATCH) break;
|
|
1208
|
-
}
|
|
1209
|
-
total += copied;
|
|
1210
|
-
options.onTable?.({ table, rows: copied });
|
|
1211
|
-
}
|
|
1212
|
-
await to.finish();
|
|
1213
|
-
return { tables: tables.length, rows: total };
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
1579
|
// src/document.ts
|
|
1217
1580
|
import { randomUUID } from "crypto";
|
|
1218
1581
|
var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -1331,7 +1694,7 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
1331
1694
|
for (const field of fields) {
|
|
1332
1695
|
const path = prefix + field.name;
|
|
1333
1696
|
const raw = input[field.name];
|
|
1334
|
-
const fail = (
|
|
1697
|
+
const fail = (message2) => errors.push({ field: path, message: message2 });
|
|
1335
1698
|
if (field.type === "group") {
|
|
1336
1699
|
if (!isEmpty(raw) && !isPlainObject(raw)) {
|
|
1337
1700
|
fail("must be an object");
|
|
@@ -1360,7 +1723,7 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
1360
1723
|
for (const locale of localization.locales) {
|
|
1361
1724
|
const own = locale === written;
|
|
1362
1725
|
const localePath = own ? path : `${path}.${locale}`;
|
|
1363
|
-
const failLocale = (
|
|
1726
|
+
const failLocale = (message2) => errors.push({ field: localePath, message: message2 });
|
|
1364
1727
|
const value2 = map[locale];
|
|
1365
1728
|
if (isEmpty(value2) || Array.isArray(value2) && value2.length === 0) {
|
|
1366
1729
|
if (own && field.required && !options.skipRequired) failLocale("is required");
|
|
@@ -1543,17 +1906,19 @@ function consoleEmail(options = {}) {
|
|
|
1543
1906
|
const sent = [];
|
|
1544
1907
|
const adapter = {
|
|
1545
1908
|
name: "console",
|
|
1909
|
+
// `log`: the admin says it prints to the server log instead of sending.
|
|
1910
|
+
describe: () => [{ key: "delivery", value: "log" }],
|
|
1546
1911
|
from: options.from ?? "Easy CMS <no-reply@localhost>",
|
|
1547
1912
|
sent,
|
|
1548
|
-
async send(
|
|
1549
|
-
sent.push(
|
|
1913
|
+
async send(message2) {
|
|
1914
|
+
sent.push(message2);
|
|
1550
1915
|
const list = (value) => value === void 0 ? void 0 : typeof value === "string" ? value : value.join(", ");
|
|
1551
1916
|
const lines = [
|
|
1552
|
-
`Email to ${list(
|
|
1553
|
-
`From: ${
|
|
1554
|
-
`Subject: ${
|
|
1917
|
+
`Email to ${list(message2.to)}${message2.cc ? `, cc ${list(message2.cc)}` : ""}`,
|
|
1918
|
+
`From: ${message2.from}${message2.replyTo ? ` (reply to ${message2.replyTo})` : ""}`,
|
|
1919
|
+
`Subject: ${message2.subject}`,
|
|
1555
1920
|
"",
|
|
1556
|
-
|
|
1921
|
+
message2.text ?? message2.html ?? ""
|
|
1557
1922
|
];
|
|
1558
1923
|
(options.log ?? console.log)(lines.join("\n"));
|
|
1559
1924
|
}
|
|
@@ -1581,14 +1946,14 @@ var Mailer = class {
|
|
|
1581
1946
|
* for the sending too. Without an `email` adapter in the config, the email is skipped with a
|
|
1582
1947
|
* warning.
|
|
1583
1948
|
*/
|
|
1584
|
-
async send(
|
|
1949
|
+
async send(message2) {
|
|
1585
1950
|
if (!this.adapter) {
|
|
1586
1951
|
this.logger.warn(
|
|
1587
|
-
`Email "${
|
|
1952
|
+
`Email "${message2.subject}" not sent: set \`email\` in the config (e.g. consoleEmail() in development)`
|
|
1588
1953
|
);
|
|
1589
1954
|
return;
|
|
1590
1955
|
}
|
|
1591
|
-
const body = JSON.stringify(
|
|
1956
|
+
const body = JSON.stringify(message2);
|
|
1592
1957
|
let id;
|
|
1593
1958
|
try {
|
|
1594
1959
|
id = await this.queue?.add({
|
|
@@ -1601,7 +1966,7 @@ var Mailer = class {
|
|
|
1601
1966
|
} catch (error) {
|
|
1602
1967
|
this.logger.error(`Could not save email: ${error.message}`);
|
|
1603
1968
|
}
|
|
1604
|
-
this.track(this.deliver(
|
|
1969
|
+
this.track(this.deliver(message2, body, id));
|
|
1605
1970
|
}
|
|
1606
1971
|
/** Waits for emails being sent, e.g. before a serverless function returns. */
|
|
1607
1972
|
async flush() {
|
|
@@ -1637,25 +2002,25 @@ var Mailer = class {
|
|
|
1637
2002
|
return { sent, failed };
|
|
1638
2003
|
}
|
|
1639
2004
|
/** One attempt now for a saved email (the admin's Retry): `undefined` when sent, else the error. */
|
|
1640
|
-
async sendNow(
|
|
2005
|
+
async sendNow(message2) {
|
|
1641
2006
|
if (!this.adapter) return "email is not set up (`email` in the config)";
|
|
1642
|
-
return this.attempt(JSON.parse(
|
|
2007
|
+
return this.attempt(JSON.parse(message2));
|
|
1643
2008
|
}
|
|
1644
2009
|
track(delivery) {
|
|
1645
2010
|
const tracked = delivery.catch((error) => this.logger.error(`Email queue failed: ${error.message}`)).finally(() => this.pending.delete(tracked));
|
|
1646
2011
|
this.pending.add(tracked);
|
|
1647
2012
|
}
|
|
1648
|
-
async deliver(
|
|
1649
|
-
const error = await this.attempt(
|
|
2013
|
+
async deliver(message2, body, id) {
|
|
2014
|
+
const error = await this.attempt(message2);
|
|
1650
2015
|
if (!error) {
|
|
1651
2016
|
if (id !== void 0) await this.queue?.remove(id);
|
|
1652
2017
|
return;
|
|
1653
2018
|
}
|
|
1654
2019
|
if (id === void 0 || !this.queue) {
|
|
1655
|
-
this.logger.error(`Email "${
|
|
2020
|
+
this.logger.error(`Email "${message2.subject}" failed: ${error}`);
|
|
1656
2021
|
return;
|
|
1657
2022
|
}
|
|
1658
|
-
this.logger.warn(`Email "${
|
|
2023
|
+
this.logger.warn(`Email "${message2.subject}" failed, retrying later: ${error}`);
|
|
1659
2024
|
await this.queue.update(id, {
|
|
1660
2025
|
message: body,
|
|
1661
2026
|
attempts: 1,
|
|
@@ -1665,12 +2030,12 @@ var Mailer = class {
|
|
|
1665
2030
|
});
|
|
1666
2031
|
}
|
|
1667
2032
|
/** `undefined` when sent, else the error. */
|
|
1668
|
-
async attempt(
|
|
2033
|
+
async attempt(message2) {
|
|
1669
2034
|
const adapter = this.adapter;
|
|
1670
|
-
const from =
|
|
2035
|
+
const from = message2.from ?? adapter.from;
|
|
1671
2036
|
if (!from) return "no sender: set `from` on the message or the email adapter";
|
|
1672
2037
|
try {
|
|
1673
|
-
await adapter.send({ ...
|
|
2038
|
+
await adapter.send({ ...message2, from });
|
|
1674
2039
|
return void 0;
|
|
1675
2040
|
} catch (error) {
|
|
1676
2041
|
return error.message || String(error);
|
|
@@ -1800,7 +2165,7 @@ function applyFieldTypes(config) {
|
|
|
1800
2165
|
|
|
1801
2166
|
// src/load-config.ts
|
|
1802
2167
|
import { existsSync } from "fs";
|
|
1803
|
-
import { resolve } from "path";
|
|
2168
|
+
import { resolve as resolve2 } from "path";
|
|
1804
2169
|
import { createJiti } from "jiti";
|
|
1805
2170
|
|
|
1806
2171
|
// src/webhooks.ts
|
|
@@ -1992,6 +2357,8 @@ var RESERVED_SLUGS = /* @__PURE__ */ new Set([
|
|
|
1992
2357
|
"document-versions",
|
|
1993
2358
|
"scheduled-jobs",
|
|
1994
2359
|
"webhook-deliveries",
|
|
2360
|
+
"email-deliveries",
|
|
2361
|
+
"database-backups",
|
|
1995
2362
|
"api-keys",
|
|
1996
2363
|
"jobs",
|
|
1997
2364
|
"migrations",
|
|
@@ -2002,7 +2369,7 @@ var SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
|
2002
2369
|
var FIELD_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2003
2370
|
function validateConfig(config) {
|
|
2004
2371
|
const issues = [];
|
|
2005
|
-
const add = (path,
|
|
2372
|
+
const add = (path, message2, hint) => issues.push(hint ? { path, message: message2, hint } : { path, message: message2 });
|
|
2006
2373
|
if (typeof config !== "object" || config === null) {
|
|
2007
2374
|
add("config", "must be an object", "export default defineConfig({ ... })");
|
|
2008
2375
|
return issues;
|
|
@@ -2014,6 +2381,7 @@ function validateConfig(config) {
|
|
|
2014
2381
|
validateAdmin(config, add);
|
|
2015
2382
|
validateAdminViews(config, add);
|
|
2016
2383
|
validateUpload(config, add);
|
|
2384
|
+
validateBackups(config, add);
|
|
2017
2385
|
validateAuth(config, add);
|
|
2018
2386
|
validateCors(config.cors, add);
|
|
2019
2387
|
validateLocalization(config, add);
|
|
@@ -2023,17 +2391,17 @@ function validateConfig(config) {
|
|
|
2023
2391
|
const collectionSlugs = /* @__PURE__ */ new Set([...BUILTIN_COLLECTIONS]);
|
|
2024
2392
|
const seenCollections = /* @__PURE__ */ new Set();
|
|
2025
2393
|
collections.forEach((collection2, i) => {
|
|
2026
|
-
const
|
|
2027
|
-
if (
|
|
2394
|
+
const slug2 = checkSlug(collection2, `collections[${i}]`, seenCollections, add);
|
|
2395
|
+
if (slug2) collectionSlugs.add(slug2);
|
|
2028
2396
|
});
|
|
2029
2397
|
const menu = config.admin?.menu;
|
|
2030
2398
|
if (menu !== void 0) {
|
|
2031
2399
|
if (!Array.isArray(menu)) {
|
|
2032
2400
|
add("admin.menu", "must be an array of collection slugs", "e.g. menu: ['posts', 'media']");
|
|
2033
2401
|
} else {
|
|
2034
|
-
for (const [i,
|
|
2035
|
-
if (typeof
|
|
2036
|
-
add(`admin.menu[${i}]`, `unknown collection ${JSON.stringify(
|
|
2402
|
+
for (const [i, slug2] of menu.entries()) {
|
|
2403
|
+
if (typeof slug2 !== "string" || !collectionSlugs.has(slug2))
|
|
2404
|
+
add(`admin.menu[${i}]`, `unknown collection ${JSON.stringify(slug2)}`);
|
|
2037
2405
|
}
|
|
2038
2406
|
}
|
|
2039
2407
|
}
|
|
@@ -2374,10 +2742,10 @@ function validateWebhooks(config, add) {
|
|
|
2374
2742
|
if (!WEBHOOK_EVENTS.includes(event))
|
|
2375
2743
|
add(`${path}.events`, `unknown event "${event}"`, `use: ${WEBHOOK_EVENTS.join(", ")}`);
|
|
2376
2744
|
}
|
|
2377
|
-
for (const
|
|
2378
|
-
if (!collections.has(
|
|
2379
|
-
for (const
|
|
2380
|
-
if (!globals.has(
|
|
2745
|
+
for (const slug2 of hook?.collections ?? [])
|
|
2746
|
+
if (!collections.has(slug2)) add(`${path}.collections`, `unknown collection "${slug2}"`);
|
|
2747
|
+
for (const slug2 of hook?.globals ?? [])
|
|
2748
|
+
if (!globals.has(slug2)) add(`${path}.globals`, `unknown global "${slug2}"`);
|
|
2381
2749
|
}
|
|
2382
2750
|
}
|
|
2383
2751
|
function validateCors(cors, add) {
|
|
@@ -2430,6 +2798,32 @@ function validateUpload(config, add) {
|
|
|
2430
2798
|
add("upload.fromURL.allowPrivate", "must be true or false");
|
|
2431
2799
|
}
|
|
2432
2800
|
var HOST_PATTERN = /^(\*|(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*)$/i;
|
|
2801
|
+
function validateBackups(config, add) {
|
|
2802
|
+
const backups = config.backups;
|
|
2803
|
+
if (backups === void 0) return;
|
|
2804
|
+
if (typeof backups !== "object" || backups === null) {
|
|
2805
|
+
add("backups", "must be an object", "e.g. backups: { every: 'day', keep: 7 }");
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
const { every, at, keep, dir, storage, sqlite } = backups;
|
|
2809
|
+
if (every !== void 0 && every !== "day" && every !== "week")
|
|
2810
|
+
add("backups.every", 'must be "day" or "week"');
|
|
2811
|
+
if (at !== void 0 && (typeof at !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(at)))
|
|
2812
|
+
add("backups.at", 'must be a time like "03:00"');
|
|
2813
|
+
if (keep !== void 0 && !(Number.isInteger(keep) && keep >= 1))
|
|
2814
|
+
add("backups.keep", "must be a whole number, 1 or more");
|
|
2815
|
+
if (dir !== void 0 && (typeof dir !== "string" || dir.trim() === ""))
|
|
2816
|
+
add("backups.dir", "must be a folder name");
|
|
2817
|
+
if (typeof dir === "string" && dir.replace(/^\.\//, "").replace(/\/+$/, "") === (config.upload?.dir ?? "uploads").replace(/^\.\//, "").replace(/\/+$/, ""))
|
|
2818
|
+
add("backups.dir", "must not be the uploads folder: uploads are served publicly");
|
|
2819
|
+
if (storage !== void 0 && (typeof storage !== "object" || storage === null || typeof storage.put !== "function"))
|
|
2820
|
+
add("backups.storage", "must be a storage adapter, e.g. s3Storage()");
|
|
2821
|
+
if (sqlite !== void 0 && typeof sqlite !== "function")
|
|
2822
|
+
add(
|
|
2823
|
+
"backups.sqlite",
|
|
2824
|
+
"must be the sqlite adapter, e.g. import { sqlite } from '@easy-cms/db-sqlite'"
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2433
2827
|
function asArray(value, path, add) {
|
|
2434
2828
|
if (value === void 0) return [];
|
|
2435
2829
|
if (!Array.isArray(value)) {
|
|
@@ -2439,24 +2833,24 @@ function asArray(value, path, add) {
|
|
|
2439
2833
|
return value;
|
|
2440
2834
|
}
|
|
2441
2835
|
function checkSlug(container, path, seen, add) {
|
|
2442
|
-
const
|
|
2443
|
-
if (typeof
|
|
2836
|
+
const slug2 = container.slug;
|
|
2837
|
+
if (typeof slug2 !== "string" || !SLUG_PATTERN.test(slug2)) {
|
|
2444
2838
|
add(
|
|
2445
2839
|
`${path}.slug`,
|
|
2446
|
-
`must be lowercase letters, digits, "-" or "_", starting with a letter (got ${JSON.stringify(
|
|
2840
|
+
`must be lowercase letters, digits, "-" or "_", starting with a letter (got ${JSON.stringify(slug2)})`
|
|
2447
2841
|
);
|
|
2448
2842
|
return void 0;
|
|
2449
2843
|
}
|
|
2450
|
-
if (RESERVED_SLUGS.has(
|
|
2451
|
-
add(`${path}.slug`, `"${
|
|
2844
|
+
if (RESERVED_SLUGS.has(slug2)) {
|
|
2845
|
+
add(`${path}.slug`, `"${slug2}" is reserved by Easy CMS`, "choose another slug");
|
|
2452
2846
|
return void 0;
|
|
2453
2847
|
}
|
|
2454
|
-
if (seen.has(
|
|
2455
|
-
add(`${path}.slug`, `duplicate slug "${
|
|
2848
|
+
if (seen.has(slug2)) {
|
|
2849
|
+
add(`${path}.slug`, `duplicate slug "${slug2}"`);
|
|
2456
2850
|
return void 0;
|
|
2457
2851
|
}
|
|
2458
|
-
seen.add(
|
|
2459
|
-
return
|
|
2852
|
+
seen.add(slug2);
|
|
2853
|
+
return slug2;
|
|
2460
2854
|
}
|
|
2461
2855
|
function validateContainer(container, path, collectionSlugs, add) {
|
|
2462
2856
|
if (!Array.isArray(container.fields)) {
|
|
@@ -2901,7 +3295,8 @@ async function resolveConfig(input) {
|
|
|
2901
3295
|
...usesSchedule(config) ? [scheduledJobsCollection] : [],
|
|
2902
3296
|
...(config.webhooks?.length ?? 0) > 0 ? [webhookDeliveriesCollection] : [],
|
|
2903
3297
|
...config.apiKeys ? [apiKeysCollection] : [],
|
|
2904
|
-
...config.email ? [emailDeliveriesCollection] : []
|
|
3298
|
+
...config.email ? [emailDeliveriesCollection] : [],
|
|
3299
|
+
databaseBackupsCollection
|
|
2905
3300
|
],
|
|
2906
3301
|
globals: config.globals ?? [],
|
|
2907
3302
|
endpoints: config.endpoints ?? [],
|
|
@@ -2930,7 +3325,7 @@ var CONFIG_FILE_NAMES = [
|
|
|
2930
3325
|
];
|
|
2931
3326
|
function findConfigFile(cwd = process.cwd()) {
|
|
2932
3327
|
for (const name of CONFIG_FILE_NAMES) {
|
|
2933
|
-
const path =
|
|
3328
|
+
const path = resolve2(
|
|
2934
3329
|
/* turbopackIgnore: true */
|
|
2935
3330
|
cwd,
|
|
2936
3331
|
name
|
|
@@ -2949,7 +3344,7 @@ async function importConfig(file) {
|
|
|
2949
3344
|
}
|
|
2950
3345
|
async function loadConfig(options = {}) {
|
|
2951
3346
|
const cwd = options.cwd ?? process.cwd();
|
|
2952
|
-
const file = options.configFile ?
|
|
3347
|
+
const file = options.configFile ? resolve2(cwd, options.configFile) : findConfigFile(cwd);
|
|
2953
3348
|
if (!file || !existsSync(file)) {
|
|
2954
3349
|
const looked = options.configFile ?? CONFIG_FILE_NAMES.join(", ");
|
|
2955
3350
|
throw new Error(
|
|
@@ -3067,9 +3462,9 @@ function localizeSort(fields, sort, locale, locales = []) {
|
|
|
3067
3462
|
|
|
3068
3463
|
// src/logger.ts
|
|
3069
3464
|
var consoleLogger = {
|
|
3070
|
-
info: (
|
|
3071
|
-
warn: (
|
|
3072
|
-
error: (
|
|
3465
|
+
info: (message2) => console.info(`[easy-cms] ${message2}`),
|
|
3466
|
+
warn: (message2) => console.warn(`[easy-cms] ${message2}`),
|
|
3467
|
+
error: (message2) => console.error(`[easy-cms] ${message2}`)
|
|
3073
3468
|
};
|
|
3074
3469
|
var silentLogger = { info() {
|
|
3075
3470
|
}, warn() {
|
|
@@ -3188,12 +3583,12 @@ async function populate(load, collections, fields, docs, depth) {
|
|
|
3188
3583
|
}
|
|
3189
3584
|
const found = /* @__PURE__ */ new Map();
|
|
3190
3585
|
await Promise.all(
|
|
3191
|
-
[...wanted].map(async ([
|
|
3192
|
-
const collection2 = bySlug.get(
|
|
3586
|
+
[...wanted].map(async ([slug2, ids]) => {
|
|
3587
|
+
const collection2 = bySlug.get(slug2);
|
|
3193
3588
|
if (!collection2) return;
|
|
3194
3589
|
const raw = await load(collection2, [...ids]);
|
|
3195
3590
|
const populated = await populate(load, collections, collection2.fields, raw, depth - 1);
|
|
3196
|
-
found.set(
|
|
3591
|
+
found.set(slug2, new Map(populated.map((d) => [d.id, d])));
|
|
3197
3592
|
})
|
|
3198
3593
|
);
|
|
3199
3594
|
for (const slot of slots) slot.assign(found.get(slot.collection) ?? /* @__PURE__ */ new Map());
|
|
@@ -3248,8 +3643,8 @@ import { BlockList, isIP } from "net";
|
|
|
3248
3643
|
var RemoteFileError = class extends Error {
|
|
3249
3644
|
/** `413` when the file is too large, otherwise `400`. */
|
|
3250
3645
|
status;
|
|
3251
|
-
constructor(
|
|
3252
|
-
super(
|
|
3646
|
+
constructor(message2, status = 400) {
|
|
3647
|
+
super(message2);
|
|
3253
3648
|
this.name = "RemoteFileError";
|
|
3254
3649
|
this.status = status;
|
|
3255
3650
|
}
|
|
@@ -3446,50 +3841,6 @@ function read(response, maxBytes, deadline) {
|
|
|
3446
3841
|
});
|
|
3447
3842
|
}
|
|
3448
3843
|
|
|
3449
|
-
// src/storage.ts
|
|
3450
|
-
import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
3451
|
-
import { dirname, isAbsolute, join, normalize, resolve as resolve2, sep } from "path";
|
|
3452
|
-
function localStorage(options = {}) {
|
|
3453
|
-
let root = resolve2(
|
|
3454
|
-
/* turbopackIgnore: true */
|
|
3455
|
-
options.dir ?? "uploads"
|
|
3456
|
-
);
|
|
3457
|
-
const pathOf = (key2) => {
|
|
3458
|
-
const path = normalize(join(
|
|
3459
|
-
/* turbopackIgnore: true */
|
|
3460
|
-
root,
|
|
3461
|
-
key2
|
|
3462
|
-
));
|
|
3463
|
-
if (!path.startsWith(root + sep)) throw new Error(`Invalid storage key "${key2}"`);
|
|
3464
|
-
return path;
|
|
3465
|
-
};
|
|
3466
|
-
return {
|
|
3467
|
-
name: "local",
|
|
3468
|
-
init({ cwd }) {
|
|
3469
|
-
const dir = options.dir ?? "uploads";
|
|
3470
|
-
root = isAbsolute(dir) ? dir : resolve2(
|
|
3471
|
-
/* turbopackIgnore: true */
|
|
3472
|
-
cwd,
|
|
3473
|
-
dir
|
|
3474
|
-
);
|
|
3475
|
-
},
|
|
3476
|
-
async put(key2, data) {
|
|
3477
|
-
const path = pathOf(key2);
|
|
3478
|
-
await mkdir(dirname(path), { recursive: true });
|
|
3479
|
-
await writeFile(path, data, { flag: "wx" });
|
|
3480
|
-
},
|
|
3481
|
-
async get(key2) {
|
|
3482
|
-
const path = pathOf(key2);
|
|
3483
|
-
const info = await stat(path).catch(() => void 0);
|
|
3484
|
-
if (!info?.isFile()) return null;
|
|
3485
|
-
return { body: new Uint8Array(await readFile(path)), size: info.size };
|
|
3486
|
-
},
|
|
3487
|
-
async delete(key2) {
|
|
3488
|
-
await rm(pathOf(key2), { force: true });
|
|
3489
|
-
}
|
|
3490
|
-
};
|
|
3491
|
-
}
|
|
3492
|
-
|
|
3493
3844
|
// src/versions.ts
|
|
3494
3845
|
var DEFAULT_MAX_VERSIONS = 50;
|
|
3495
3846
|
function versionLimit(config) {
|
|
@@ -3497,8 +3848,8 @@ function versionLimit(config) {
|
|
|
3497
3848
|
if (!versions) return void 0;
|
|
3498
3849
|
return versions === true ? DEFAULT_MAX_VERSIONS : versions.max ?? DEFAULT_MAX_VERSIONS;
|
|
3499
3850
|
}
|
|
3500
|
-
var collectionParent = (
|
|
3501
|
-
var globalParent = (
|
|
3851
|
+
var collectionParent = (slug2) => slug2;
|
|
3852
|
+
var globalParent = (slug2) => `global:${slug2}`;
|
|
3502
3853
|
function snapshotOf(fields, doc) {
|
|
3503
3854
|
const hidden = new Set(fields.filter((f) => f.hidden).map((f) => f.name));
|
|
3504
3855
|
const snapshot = {};
|
|
@@ -4020,17 +4371,17 @@ var EasyCMS = class {
|
|
|
4020
4371
|
return token ? verifyPreviewToken(this.config.secret, token) : null;
|
|
4021
4372
|
}
|
|
4022
4373
|
/** Live preview of a global. See `preview`. */
|
|
4023
|
-
async previewGlobal(
|
|
4024
|
-
const config = this.global(
|
|
4374
|
+
async previewGlobal(slug2, data, options = {}) {
|
|
4375
|
+
const config = this.global(slug2);
|
|
4025
4376
|
const guard = guardOf(options);
|
|
4026
4377
|
await this.checkGlobalAccess(config, "update", guard);
|
|
4027
|
-
const raw = asObject(data,
|
|
4378
|
+
const raw = asObject(data, slug2);
|
|
4028
4379
|
const input = await filterInput(
|
|
4029
4380
|
config.fields,
|
|
4030
4381
|
raw,
|
|
4031
4382
|
this.fieldChecker("update", guard, void 0, raw)
|
|
4032
4383
|
);
|
|
4033
|
-
const saved = await this.db.findGlobal({ slug }) ?? {};
|
|
4384
|
+
const saved = await this.db.findGlobal({ slug: slug2 }) ?? {};
|
|
4034
4385
|
const current = await this.globalDraft(config, saved);
|
|
4035
4386
|
const merged = generateSlugs(
|
|
4036
4387
|
config.fields,
|
|
@@ -4200,11 +4551,11 @@ var EasyCMS = class {
|
|
|
4200
4551
|
const [out] = await this.output(config, [existing], guard, { depth: 0, draft: true });
|
|
4201
4552
|
return out;
|
|
4202
4553
|
}
|
|
4203
|
-
async findGlobal(
|
|
4204
|
-
const config = this.global(
|
|
4554
|
+
async findGlobal(slug2, options = {}) {
|
|
4555
|
+
const config = this.global(slug2);
|
|
4205
4556
|
const guard = guardOf(options);
|
|
4206
4557
|
await this.checkGlobalAccess(config, "read", guard);
|
|
4207
|
-
const saved = await this.db.findGlobal({ slug });
|
|
4558
|
+
const saved = await this.db.findGlobal({ slug: slug2 });
|
|
4208
4559
|
const stored = saved && options.draft ? await this.globalDraft(config, saved) : saved;
|
|
4209
4560
|
const data = fillMissing(
|
|
4210
4561
|
config.fields,
|
|
@@ -4218,33 +4569,33 @@ var EasyCMS = class {
|
|
|
4218
4569
|
const { id: _id, ...doc } = out;
|
|
4219
4570
|
return doc;
|
|
4220
4571
|
}
|
|
4221
|
-
async updateGlobal(
|
|
4222
|
-
return this.saveGlobal(
|
|
4572
|
+
async updateGlobal(slug2, data, options = {}) {
|
|
4573
|
+
return this.saveGlobal(slug2, asObject(data, slug2), options);
|
|
4223
4574
|
}
|
|
4224
4575
|
/** Takes a global off the site (`status: 'draft'`). See `unpublish`. */
|
|
4225
|
-
async unpublishGlobal(
|
|
4226
|
-
if (!this.global(
|
|
4227
|
-
return this.saveGlobal(
|
|
4576
|
+
async unpublishGlobal(slug2, options = {}) {
|
|
4577
|
+
if (!this.global(slug2).drafts) throw new QueryError(`"${slug2}" has no drafts`);
|
|
4578
|
+
return this.saveGlobal(slug2, { status: "draft" }, options, "unpublish");
|
|
4228
4579
|
}
|
|
4229
4580
|
/** Throws away the unpublished draft of a published global. */
|
|
4230
|
-
async discardGlobalDraft(
|
|
4231
|
-
const config = this.global(
|
|
4581
|
+
async discardGlobalDraft(slug2, options = {}) {
|
|
4582
|
+
const config = this.global(slug2);
|
|
4232
4583
|
const guard = guardOf(options);
|
|
4233
4584
|
await this.checkGlobalAccess(config, "update", guard);
|
|
4234
|
-
const saved = await this.db.findGlobal({ slug });
|
|
4585
|
+
const saved = await this.db.findGlobal({ slug: slug2 });
|
|
4235
4586
|
if (saved && await this.globalDraft(config, saved) !== saved) {
|
|
4236
|
-
await this.saveVersion(config, globalParent(
|
|
4587
|
+
await this.saveVersion(config, globalParent(slug2), 0, saved, guard);
|
|
4237
4588
|
}
|
|
4238
|
-
return this.findGlobal(
|
|
4589
|
+
return this.findGlobal(slug2, { ...options, draft: true });
|
|
4239
4590
|
}
|
|
4240
4591
|
/** Saved versions of a global, newest first. Needs update access. */
|
|
4241
|
-
async findGlobalVersions(
|
|
4242
|
-
await this.globalVersionTarget(
|
|
4243
|
-
return this.versions.list(globalParent(
|
|
4592
|
+
async findGlobalVersions(slug2, options = {}) {
|
|
4593
|
+
await this.globalVersionTarget(slug2, options);
|
|
4594
|
+
return this.versions.list(globalParent(slug2), 0, options.limit ?? 20, options.page ?? 1);
|
|
4244
4595
|
}
|
|
4245
|
-
async findGlobalVersion(
|
|
4246
|
-
const config = await this.globalVersionTarget(
|
|
4247
|
-
const version = await this.versions.get(globalParent(
|
|
4596
|
+
async findGlobalVersion(slug2, versionId, options = {}) {
|
|
4597
|
+
const config = await this.globalVersionTarget(slug2, options);
|
|
4598
|
+
const version = await this.versions.get(globalParent(slug2), 0, versionId);
|
|
4248
4599
|
if (!version) return null;
|
|
4249
4600
|
const data = fillMissing(
|
|
4250
4601
|
config.fields,
|
|
@@ -4258,21 +4609,21 @@ var EasyCMS = class {
|
|
|
4258
4609
|
return { ...version, data: { ...doc, updatedAt: version.createdAt } };
|
|
4259
4610
|
}
|
|
4260
4611
|
/** Makes an old version of a global current (as a draft when the global has drafts). */
|
|
4261
|
-
async restoreGlobalVersion(
|
|
4262
|
-
const config = await this.globalVersionTarget(
|
|
4263
|
-
const version = await this.versions.get(globalParent(
|
|
4264
|
-
if (!version) throw new NotFoundError(`${
|
|
4612
|
+
async restoreGlobalVersion(slug2, versionId, options = {}) {
|
|
4613
|
+
const config = await this.globalVersionTarget(slug2, options);
|
|
4614
|
+
const version = await this.versions.get(globalParent(slug2), 0, versionId);
|
|
4615
|
+
if (!version) throw new NotFoundError(`${slug2} version`, versionId);
|
|
4265
4616
|
const { status: _status, ...data } = version.data;
|
|
4266
4617
|
return this.saveGlobal(
|
|
4267
|
-
|
|
4618
|
+
slug2,
|
|
4268
4619
|
config.drafts ? { ...data, status: "draft" } : data,
|
|
4269
4620
|
options,
|
|
4270
4621
|
"restore"
|
|
4271
4622
|
);
|
|
4272
4623
|
}
|
|
4273
|
-
async globalVersionTarget(
|
|
4274
|
-
const config = this.global(
|
|
4275
|
-
if (!versionLimit(config)) throw new QueryError(`"${
|
|
4624
|
+
async globalVersionTarget(slug2, options) {
|
|
4625
|
+
const config = this.global(slug2);
|
|
4626
|
+
if (!versionLimit(config)) throw new QueryError(`"${slug2}" has no versions`);
|
|
4276
4627
|
await this.checkGlobalAccess(config, "update", guardOf(options));
|
|
4277
4628
|
return config;
|
|
4278
4629
|
}
|
|
@@ -4283,18 +4634,18 @@ var EasyCMS = class {
|
|
|
4283
4634
|
if (version?.status !== "draft") return saved;
|
|
4284
4635
|
return { ...version.data, updatedAt: version.createdAt, status: "draft" };
|
|
4285
4636
|
}
|
|
4286
|
-
async saveGlobal(
|
|
4287
|
-
const config = this.global(
|
|
4637
|
+
async saveGlobal(slug2, raw, options, mode = "save") {
|
|
4638
|
+
const config = this.global(slug2);
|
|
4288
4639
|
const guard = guardOf(options);
|
|
4289
4640
|
await this.checkGlobalAccess(config, "update", guard);
|
|
4290
4641
|
if (config.drafts && (mode === "unpublish" || raw.status === "published"))
|
|
4291
|
-
this.checkKey(guard, { global:
|
|
4642
|
+
this.checkKey(guard, { global: slug2 }, "publish");
|
|
4292
4643
|
const input = await filterInput(
|
|
4293
4644
|
config.fields,
|
|
4294
4645
|
raw,
|
|
4295
4646
|
this.fieldChecker("update", guard, void 0, raw)
|
|
4296
4647
|
);
|
|
4297
|
-
const existing = await this.db.findGlobal({ slug }) ?? {};
|
|
4648
|
+
const existing = await this.db.findGlobal({ slug: slug2 }) ?? {};
|
|
4298
4649
|
const current = await this.globalDraft(config, existing);
|
|
4299
4650
|
const localized = mode === "restore" ? input : this.toMaps(config, input, current, options);
|
|
4300
4651
|
const merged = generateSlugs(
|
|
@@ -4318,26 +4669,26 @@ var EasyCMS = class {
|
|
|
4318
4669
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4319
4670
|
if (mode !== "unpublish" && this.separateDrafts(config) && existing.status === "published" && prepared.status === "draft") {
|
|
4320
4671
|
const draft = { ...prepared, updatedAt: now };
|
|
4321
|
-
await this.saveVersion(config, globalParent(
|
|
4322
|
-
await this.notify(config.hooks?.afterChange, "afterChange",
|
|
4672
|
+
await this.saveVersion(config, globalParent(slug2), 0, draft, guard);
|
|
4673
|
+
await this.notify(config.hooks?.afterChange, "afterChange", slug2, {
|
|
4323
4674
|
...base,
|
|
4324
4675
|
doc: draft,
|
|
4325
4676
|
previousDoc: current,
|
|
4326
4677
|
operation: "update"
|
|
4327
4678
|
});
|
|
4328
4679
|
this.emit(config, "draft", draft);
|
|
4329
|
-
return this.findGlobal(
|
|
4680
|
+
return this.findGlobal(slug2, { ...options, draft: true });
|
|
4330
4681
|
}
|
|
4331
|
-
const doc = await this.db.updateGlobal({ slug, data: { ...prepared, updatedAt: now } });
|
|
4332
|
-
await this.saveVersion(config, globalParent(
|
|
4333
|
-
await this.notify(config.hooks?.afterChange, "afterChange",
|
|
4682
|
+
const doc = await this.db.updateGlobal({ slug: slug2, data: { ...prepared, updatedAt: now } });
|
|
4683
|
+
await this.saveVersion(config, globalParent(slug2), 0, doc, guard);
|
|
4684
|
+
await this.notify(config.hooks?.afterChange, "afterChange", slug2, {
|
|
4334
4685
|
...base,
|
|
4335
4686
|
doc,
|
|
4336
4687
|
previousDoc: current,
|
|
4337
4688
|
operation: "update"
|
|
4338
4689
|
});
|
|
4339
4690
|
this.emit(config, "update", doc, existing.status);
|
|
4340
|
-
return this.findGlobal(
|
|
4691
|
+
return this.findGlobal(slug2, { ...options, draft: true });
|
|
4341
4692
|
}
|
|
4342
4693
|
/**
|
|
4343
4694
|
* What `user` may do with one document. Resolves `where`-style access
|
|
@@ -4370,8 +4721,8 @@ var EasyCMS = class {
|
|
|
4370
4721
|
* the email is queued; it is sent in the background and retried from the queue if sending
|
|
4371
4722
|
* fails. Without `email` in the config, the email is skipped with a warning.
|
|
4372
4723
|
*/
|
|
4373
|
-
sendEmail(
|
|
4374
|
-
return this.mailer.send(
|
|
4724
|
+
sendEmail(message2) {
|
|
4725
|
+
return this.mailer.send(message2);
|
|
4375
4726
|
}
|
|
4376
4727
|
/** Waits for emails being sent, e.g. before a serverless function returns. */
|
|
4377
4728
|
flushEmails() {
|
|
@@ -4440,6 +4791,9 @@ var EasyCMS = class {
|
|
|
4440
4791
|
async runJobs(now = /* @__PURE__ */ new Date()) {
|
|
4441
4792
|
const scheduled = await this.runScheduled(now);
|
|
4442
4793
|
await this.pruneFailedDeliveries(now);
|
|
4794
|
+
await runDueBackups(this, now).catch(
|
|
4795
|
+
(error) => this.logger.error(`Backups: ${error.message}`)
|
|
4796
|
+
);
|
|
4443
4797
|
return {
|
|
4444
4798
|
...scheduled,
|
|
4445
4799
|
webhooks: await this.retryWebhooks(now),
|
|
@@ -4509,9 +4863,9 @@ var EasyCMS = class {
|
|
|
4509
4863
|
const action = row.action;
|
|
4510
4864
|
try {
|
|
4511
4865
|
if (parent.startsWith("global:")) {
|
|
4512
|
-
const
|
|
4513
|
-
if (action === "publish") await this.saveGlobal(
|
|
4514
|
-
else await this.saveGlobal(
|
|
4866
|
+
const slug2 = parent.slice("global:".length);
|
|
4867
|
+
if (action === "publish") await this.saveGlobal(slug2, { status: "published" }, {});
|
|
4868
|
+
else await this.saveGlobal(slug2, { status: "draft" }, {}, "unpublish");
|
|
4515
4869
|
} else if (action === "publish") {
|
|
4516
4870
|
await this.updateDocument(parent, row.doc, { status: "published" }, { depth: 0 });
|
|
4517
4871
|
} else {
|
|
@@ -4530,12 +4884,12 @@ var EasyCMS = class {
|
|
|
4530
4884
|
});
|
|
4531
4885
|
} catch (error) {
|
|
4532
4886
|
failed++;
|
|
4533
|
-
const
|
|
4534
|
-
this.logger.error(`Scheduled ${action} of ${parent} ${row.doc} failed: ${
|
|
4887
|
+
const message2 = error.message;
|
|
4888
|
+
this.logger.error(`Scheduled ${action} of ${parent} ${row.doc} failed: ${message2}`);
|
|
4535
4889
|
await this.db.update({
|
|
4536
4890
|
collection: SCHEDULED_JOBS,
|
|
4537
4891
|
id: row.id,
|
|
4538
|
-
data: { ...row, state: "failed", error:
|
|
4892
|
+
data: { ...row, state: "failed", error: message2.slice(0, 500) }
|
|
4539
4893
|
});
|
|
4540
4894
|
}
|
|
4541
4895
|
}
|
|
@@ -4555,17 +4909,17 @@ var EasyCMS = class {
|
|
|
4555
4909
|
const { parsed } = await this.scheduleTarget(collection2, id, options);
|
|
4556
4910
|
await this.removeJob(collection2, parsed, jobId);
|
|
4557
4911
|
}
|
|
4558
|
-
async scheduleGlobal(
|
|
4559
|
-
await this.globalScheduleTarget(
|
|
4560
|
-
return this.addJob(`global:${
|
|
4912
|
+
async scheduleGlobal(slug2, job, options = {}) {
|
|
4913
|
+
await this.globalScheduleTarget(slug2, options);
|
|
4914
|
+
return this.addJob(`global:${slug2}`, 0, job, options);
|
|
4561
4915
|
}
|
|
4562
|
-
async scheduledGlobal(
|
|
4563
|
-
await this.globalScheduleTarget(
|
|
4564
|
-
return this.pendingJobs(`global:${
|
|
4916
|
+
async scheduledGlobal(slug2, options = {}) {
|
|
4917
|
+
await this.globalScheduleTarget(slug2, options);
|
|
4918
|
+
return this.pendingJobs(`global:${slug2}`, 0);
|
|
4565
4919
|
}
|
|
4566
|
-
async cancelGlobalSchedule(
|
|
4567
|
-
await this.globalScheduleTarget(
|
|
4568
|
-
await this.removeJob(`global:${
|
|
4920
|
+
async cancelGlobalSchedule(slug2, jobId, options = {}) {
|
|
4921
|
+
await this.globalScheduleTarget(slug2, options);
|
|
4922
|
+
await this.removeJob(`global:${slug2}`, 0, jobId);
|
|
4569
4923
|
}
|
|
4570
4924
|
async scheduleTarget(collection2, id, options) {
|
|
4571
4925
|
const config = this.collection(collection2);
|
|
@@ -4577,11 +4931,11 @@ var EasyCMS = class {
|
|
|
4577
4931
|
this.checkKey(guardOf(options), { collection: collection2 }, "publish");
|
|
4578
4932
|
return { config, parsed };
|
|
4579
4933
|
}
|
|
4580
|
-
async globalScheduleTarget(
|
|
4581
|
-
const config = this.global(
|
|
4582
|
-
if (!config.schedule) throw new QueryError(`"${
|
|
4934
|
+
async globalScheduleTarget(slug2, options) {
|
|
4935
|
+
const config = this.global(slug2);
|
|
4936
|
+
if (!config.schedule) throw new QueryError(`"${slug2}" has no schedule`);
|
|
4583
4937
|
await this.checkGlobalAccess(config, "update", guardOf(options));
|
|
4584
|
-
this.checkKey(guardOf(options), { global:
|
|
4938
|
+
this.checkKey(guardOf(options), { global: slug2 }, "publish");
|
|
4585
4939
|
}
|
|
4586
4940
|
async addJob(parent, doc, job, options) {
|
|
4587
4941
|
const runAt = new Date(job.at);
|
|
@@ -4715,14 +5069,14 @@ var EasyCMS = class {
|
|
|
4715
5069
|
}
|
|
4716
5070
|
// -------------------------------------------------------------------------
|
|
4717
5071
|
/** @internal */
|
|
4718
|
-
collection(
|
|
4719
|
-
const config = this.config.collections.find((c) => c.slug ===
|
|
4720
|
-
if (!config) throw new QueryError(`Unknown collection "${
|
|
5072
|
+
collection(slug2) {
|
|
5073
|
+
const config = this.config.collections.find((c) => c.slug === slug2);
|
|
5074
|
+
if (!config) throw new QueryError(`Unknown collection "${slug2}"`);
|
|
4721
5075
|
return config;
|
|
4722
5076
|
}
|
|
4723
|
-
global(
|
|
4724
|
-
const config = this.config.globals.find((g) => g.slug ===
|
|
4725
|
-
if (!config) throw new QueryError(`Unknown global "${
|
|
5077
|
+
global(slug2) {
|
|
5078
|
+
const config = this.config.globals.find((g) => g.slug === slug2);
|
|
5079
|
+
if (!config) throw new QueryError(`Unknown global "${slug2}"`);
|
|
4726
5080
|
return config;
|
|
4727
5081
|
}
|
|
4728
5082
|
/** Refuses what a request's API key does not allow (see `ApiKeyPermissions`). */
|
|
@@ -4972,13 +5326,13 @@ var EasyCMS = class {
|
|
|
4972
5326
|
return current;
|
|
4973
5327
|
}
|
|
4974
5328
|
/** Runs hooks after the change is saved: failures are logged, never undo the save (NFR-REL-03). */
|
|
4975
|
-
async notify(hooks, name,
|
|
5329
|
+
async notify(hooks, name, slug2, args) {
|
|
4976
5330
|
for (const hook of hooks ?? []) {
|
|
4977
5331
|
try {
|
|
4978
5332
|
await hook(args);
|
|
4979
5333
|
} catch (error) {
|
|
4980
5334
|
this.logger.error(
|
|
4981
|
-
`${name} hook of "${
|
|
5335
|
+
`${name} hook of "${slug2}" failed: ${error instanceof Error ? error.message : String(error)}`
|
|
4982
5336
|
);
|
|
4983
5337
|
}
|
|
4984
5338
|
}
|
|
@@ -5080,9 +5434,9 @@ var EasyCMS = class {
|
|
|
5080
5434
|
const value = data[field.name];
|
|
5081
5435
|
if (field.localized && value && typeof value === "object") {
|
|
5082
5436
|
const map = { ...value };
|
|
5083
|
-
for (const [locale,
|
|
5084
|
-
if (typeof
|
|
5085
|
-
map[locale] = await unique(`${field.name}.${locale}`,
|
|
5437
|
+
for (const [locale, slug2] of Object.entries(map)) {
|
|
5438
|
+
if (typeof slug2 === "string" && slug2 !== "")
|
|
5439
|
+
map[locale] = await unique(`${field.name}.${locale}`, slug2);
|
|
5086
5440
|
}
|
|
5087
5441
|
data[field.name] = map;
|
|
5088
5442
|
} else if (typeof value === "string" && value !== "") {
|
|
@@ -5221,11 +5575,11 @@ function asObject(data, name) {
|
|
|
5221
5575
|
|
|
5222
5576
|
// src/rest/admin-modules.ts
|
|
5223
5577
|
import { createHash as createHash3 } from "crypto";
|
|
5224
|
-
import { readFile as
|
|
5578
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
5225
5579
|
import { createRequire } from "module";
|
|
5226
|
-
import { join as
|
|
5580
|
+
import { join as join3 } from "path";
|
|
5227
5581
|
function resolveAdminModule(specifier, cwd = process.cwd()) {
|
|
5228
|
-
const bases = [
|
|
5582
|
+
const bases = [join3(cwd, "package.json")];
|
|
5229
5583
|
const entry = process.argv[1];
|
|
5230
5584
|
if (entry && !specifier.startsWith(".")) bases.push(entry);
|
|
5231
5585
|
for (const base of bases) {
|
|
@@ -5259,7 +5613,7 @@ function adminModuleUrls(cms) {
|
|
|
5259
5613
|
async function readAdminModule(cms, index) {
|
|
5260
5614
|
const file = files(cms)[index];
|
|
5261
5615
|
if (!file) return void 0;
|
|
5262
|
-
const body = await
|
|
5616
|
+
const body = await readFile3(file, "utf8").catch(() => void 0);
|
|
5263
5617
|
if (body === void 0) return void 0;
|
|
5264
5618
|
return { body, etag: `"${createHash3("sha1").update(body).digest("base64url")}"` };
|
|
5265
5619
|
}
|
|
@@ -5277,7 +5631,7 @@ function parse(text) {
|
|
|
5277
5631
|
return {};
|
|
5278
5632
|
}
|
|
5279
5633
|
}
|
|
5280
|
-
function
|
|
5634
|
+
function toAdmin2(kind, row) {
|
|
5281
5635
|
const base = {
|
|
5282
5636
|
id: row.id,
|
|
5283
5637
|
state: row.state === "failed" ? "failed" : "pending",
|
|
@@ -5300,13 +5654,13 @@ function toAdmin(kind, row) {
|
|
|
5300
5654
|
body: String(row.body ?? "")
|
|
5301
5655
|
};
|
|
5302
5656
|
}
|
|
5303
|
-
const
|
|
5304
|
-
const to =
|
|
5657
|
+
const message2 = parse(row.message);
|
|
5658
|
+
const to = message2.to;
|
|
5305
5659
|
return {
|
|
5306
5660
|
...base,
|
|
5307
5661
|
kind,
|
|
5308
5662
|
to: Array.isArray(to) ? to.map(String) : typeof to === "string" ? [to] : [],
|
|
5309
|
-
subject: String(
|
|
5663
|
+
subject: String(message2.subject ?? "")
|
|
5310
5664
|
};
|
|
5311
5665
|
}
|
|
5312
5666
|
async function listDeliveries(cms, kind, state, page) {
|
|
@@ -5325,7 +5679,7 @@ async function listDeliveries(cms, kind, state, page) {
|
|
|
5325
5679
|
cms.db.count({ collection: collection2, where: { state: { equals: "pending" } } })
|
|
5326
5680
|
]);
|
|
5327
5681
|
return {
|
|
5328
|
-
docs: found.docs.map((row) =>
|
|
5682
|
+
docs: found.docs.map((row) => toAdmin2(kind, row)),
|
|
5329
5683
|
totalDocs: found.totalDocs,
|
|
5330
5684
|
page: found.page,
|
|
5331
5685
|
totalPages: found.totalPages,
|
|
@@ -5381,6 +5735,82 @@ async function deleteFailedDeliveries(cms, kind) {
|
|
|
5381
5735
|
return { deleted };
|
|
5382
5736
|
}
|
|
5383
5737
|
|
|
5738
|
+
// src/rest/admin-email.ts
|
|
5739
|
+
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5740
|
+
var LIMIT = 5;
|
|
5741
|
+
var WINDOW = 10 * 6e4;
|
|
5742
|
+
var sentAt = /* @__PURE__ */ new WeakMap();
|
|
5743
|
+
var message = (error) => error instanceof Error ? error.message || error.name : String(error);
|
|
5744
|
+
function adminEmail(cms) {
|
|
5745
|
+
const adapter = cms.config.email;
|
|
5746
|
+
if (!adapter) return { configured: false, name: null, from: null, settings: [], canVerify: false };
|
|
5747
|
+
let settings = [];
|
|
5748
|
+
try {
|
|
5749
|
+
settings = [...adapter.describe?.() ?? []].map((s) => ({
|
|
5750
|
+
key: String(s.key),
|
|
5751
|
+
value: s.value === null || s.value === void 0 ? null : String(s.value),
|
|
5752
|
+
...s.source ? { source: String(s.source) } : {}
|
|
5753
|
+
}));
|
|
5754
|
+
} catch {
|
|
5755
|
+
}
|
|
5756
|
+
return {
|
|
5757
|
+
configured: true,
|
|
5758
|
+
name: adapter.name ?? "custom",
|
|
5759
|
+
from: adapter.from ?? null,
|
|
5760
|
+
settings,
|
|
5761
|
+
canVerify: typeof adapter.verify === "function"
|
|
5762
|
+
};
|
|
5763
|
+
}
|
|
5764
|
+
async function verifyEmail(cms) {
|
|
5765
|
+
const adapter = cms.config.email;
|
|
5766
|
+
if (!adapter?.verify)
|
|
5767
|
+
return { ok: false, error: "This email adapter cannot check its connection" };
|
|
5768
|
+
try {
|
|
5769
|
+
await adapter.verify();
|
|
5770
|
+
return { ok: true };
|
|
5771
|
+
} catch (error) {
|
|
5772
|
+
return { ok: false, error: message(error) };
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
async function sendTestEmail(cms, user, to, locale) {
|
|
5776
|
+
const adapter = cms.config.email;
|
|
5777
|
+
if (!adapter) return { ok: false, error: "Email is not set up (`email` in the config)" };
|
|
5778
|
+
const address = typeof to === "string" && to.trim() ? to.trim() : user.email;
|
|
5779
|
+
if (!EMAIL.test(address))
|
|
5780
|
+
throw new ValidationError("email", [{ field: "to", message: "must be an email address" }]);
|
|
5781
|
+
const times = sentAt.get(cms) ?? /* @__PURE__ */ new Map();
|
|
5782
|
+
sentAt.set(cms, times);
|
|
5783
|
+
const key2 = String(user.id);
|
|
5784
|
+
const now = Date.now();
|
|
5785
|
+
const recent = (times.get(key2) ?? []).filter((t) => now - t < WINDOW);
|
|
5786
|
+
if (recent.length >= LIMIT)
|
|
5787
|
+
throw new TooManyRequestsError("Too many test emails: try again in a few minutes");
|
|
5788
|
+
times.set(key2, [...recent, now]);
|
|
5789
|
+
const from = adapter.from;
|
|
5790
|
+
if (!from) return { ok: false, error: "No sender: set `from` on the email adapter" };
|
|
5791
|
+
const site = cms.config.admin.brand.name || "Easy CMS";
|
|
5792
|
+
const th = locale === "th" || locale !== "en" && cms.config.admin.locale === "th";
|
|
5793
|
+
const time = (/* @__PURE__ */ new Date()).toISOString();
|
|
5794
|
+
const subject = th ? `\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E17\u0E14\u0E2A\u0E2D\u0E1A\u0E08\u0E32\u0E01 ${site}` : `Test email from ${site}`;
|
|
5795
|
+
const text = 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
|
|
5796
|
+
|
|
5797
|
+
\u0E2A\u0E48\u0E07\u0E42\u0E14\u0E22: ${user.email}
|
|
5798
|
+
Adapter: ${adapter.name ?? "custom"}
|
|
5799
|
+
\u0E1C\u0E39\u0E49\u0E2A\u0E48\u0E07: ${from}
|
|
5800
|
+
\u0E40\u0E27\u0E25\u0E32: ${time}` : `This email was sent from Settings \u2192 Email in ${site} to test the settings.
|
|
5801
|
+
|
|
5802
|
+
Sent by: ${user.email}
|
|
5803
|
+
Adapter: ${adapter.name ?? "custom"}
|
|
5804
|
+
From: ${from}
|
|
5805
|
+
Time: ${time}`;
|
|
5806
|
+
try {
|
|
5807
|
+
await adapter.send({ to: address, subject, text, from });
|
|
5808
|
+
return { ok: true };
|
|
5809
|
+
} catch (error) {
|
|
5810
|
+
return { ok: false, error: message(error) };
|
|
5811
|
+
}
|
|
5812
|
+
}
|
|
5813
|
+
|
|
5384
5814
|
// src/rest/admin-schema.ts
|
|
5385
5815
|
var SETTINGS = /* @__PURE__ */ new Set([USERS, API_KEYS]);
|
|
5386
5816
|
function componentRef(component) {
|
|
@@ -5390,7 +5820,7 @@ function componentRef(component) {
|
|
|
5390
5820
|
async function allowed(access, user) {
|
|
5391
5821
|
return await evaluateAccess(access, { user }) !== false;
|
|
5392
5822
|
}
|
|
5393
|
-
async function serializeFields(fields,
|
|
5823
|
+
async function serializeFields(fields, update2, localized) {
|
|
5394
5824
|
const out = [];
|
|
5395
5825
|
for (const field of fields) {
|
|
5396
5826
|
if (field.hidden) continue;
|
|
@@ -5399,7 +5829,7 @@ async function serializeFields(fields, update, localized) {
|
|
|
5399
5829
|
if (field.required) f.required = true;
|
|
5400
5830
|
if (field.unique) f.unique = true;
|
|
5401
5831
|
if (field.defaultValue !== void 0) f.defaultValue = field.defaultValue;
|
|
5402
|
-
if (!await
|
|
5832
|
+
if (!await update2.allows(field)) f.readOnly = true;
|
|
5403
5833
|
if (field.localized && localized) f.localized = true;
|
|
5404
5834
|
if (field.position === "sidebar") f.position = "sidebar";
|
|
5405
5835
|
if (field.admin?.component || field.admin?.after?.length || field.admin?.cell) {
|
|
@@ -5446,7 +5876,7 @@ async function serializeFields(fields, update, localized) {
|
|
|
5446
5876
|
case "array":
|
|
5447
5877
|
if (field.minRows !== void 0) f.minRows = field.minRows;
|
|
5448
5878
|
if (field.maxRows !== void 0) f.maxRows = field.maxRows;
|
|
5449
|
-
f.fields = await serializeFields(field.fields,
|
|
5879
|
+
f.fields = await serializeFields(field.fields, update2, localized);
|
|
5450
5880
|
break;
|
|
5451
5881
|
case "blocks":
|
|
5452
5882
|
if (field.minRows !== void 0) f.minRows = field.minRows;
|
|
@@ -5455,12 +5885,12 @@ async function serializeFields(fields, update, localized) {
|
|
|
5455
5885
|
field.blocks.map(async (block) => ({
|
|
5456
5886
|
slug: block.slug,
|
|
5457
5887
|
...block.labels ? { labels: block.labels } : {},
|
|
5458
|
-
fields: await serializeFields(block.fields,
|
|
5888
|
+
fields: await serializeFields(block.fields, update2, localized)
|
|
5459
5889
|
}))
|
|
5460
5890
|
);
|
|
5461
5891
|
break;
|
|
5462
5892
|
case "group":
|
|
5463
|
-
f.fields = await serializeFields(field.fields,
|
|
5893
|
+
f.fields = await serializeFields(field.fields, update2, localized);
|
|
5464
5894
|
break;
|
|
5465
5895
|
}
|
|
5466
5896
|
out.push(f);
|
|
@@ -5574,11 +6004,11 @@ async function widgets(cms, user) {
|
|
|
5574
6004
|
}
|
|
5575
6005
|
|
|
5576
6006
|
// src/version.ts
|
|
5577
|
-
var VERSION = true ? "0.
|
|
6007
|
+
var VERSION = true ? "0.32.0" : "0.0.0-dev";
|
|
5578
6008
|
|
|
5579
6009
|
// src/rest/admin-status.ts
|
|
5580
6010
|
var DAY = 864e5;
|
|
5581
|
-
var has = (cms,
|
|
6011
|
+
var has = (cms, slug2) => cms.config.collections.some((c) => c.slug === slug2);
|
|
5582
6012
|
async function adminStatus(cms) {
|
|
5583
6013
|
const config = cms.config;
|
|
5584
6014
|
const attention = [];
|
|
@@ -5631,6 +6061,8 @@ async function adminStatus(cms) {
|
|
|
5631
6061
|
});
|
|
5632
6062
|
if (count > 0) attention.push({ id: "scheduled", count });
|
|
5633
6063
|
}
|
|
6064
|
+
const backups = await backupAttention(cms);
|
|
6065
|
+
if (backups) attention.push(backups);
|
|
5634
6066
|
if (!config.email) attention.push({ id: "no-email" });
|
|
5635
6067
|
if (!config.serverURL && process.env.NODE_ENV === "production")
|
|
5636
6068
|
attention.push({ id: "no-server-url" });
|
|
@@ -5665,8 +6097,8 @@ async function pickerFilter(cms, url, target, user) {
|
|
|
5665
6097
|
const spec = forCollection ?? forGlobal;
|
|
5666
6098
|
if (spec === null) return void 0;
|
|
5667
6099
|
const dot = spec.indexOf(".");
|
|
5668
|
-
const
|
|
5669
|
-
const owner = forCollection !== null ? cms.config.collections.find((c) => c.slug ===
|
|
6100
|
+
const slug2 = dot > 0 ? spec.slice(0, dot) : "";
|
|
6101
|
+
const owner = forCollection !== null ? cms.config.collections.find((c) => c.slug === slug2) : cms.config.globals.find((g) => g.slug === slug2);
|
|
5670
6102
|
const field = owner ? fieldAt(owner.fields, spec.slice(dot + 1)) : void 0;
|
|
5671
6103
|
if (field?.type !== "relationship" || field.to !== target)
|
|
5672
6104
|
throw new QueryError(`filterFor: no relationship to "${target}" at "${spec}"`);
|
|
@@ -6014,6 +6446,55 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
6014
6446
|
}
|
|
6015
6447
|
return { body: await cms.runJobs() };
|
|
6016
6448
|
}
|
|
6449
|
+
if (first === "admin" && second === "backups") {
|
|
6450
|
+
if (!ctx.user) throw new UnauthorizedError();
|
|
6451
|
+
if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
|
|
6452
|
+
const [, , id2, action, extra] = segments;
|
|
6453
|
+
if (extra !== void 0) throw new HttpError("Not found", 404);
|
|
6454
|
+
if (id2 === void 0) {
|
|
6455
|
+
if (method === "GET") return { body: await listBackups(cms) };
|
|
6456
|
+
if (method === "POST")
|
|
6457
|
+
return { status: 202, body: await startBackup(cms, "manual", ctx.user.email) };
|
|
6458
|
+
throw methodNotAllowed(ctx, "GET, POST");
|
|
6459
|
+
}
|
|
6460
|
+
const parsed = parseId(id2);
|
|
6461
|
+
if (parsed === void 0) throw new HttpError("Not found", 404);
|
|
6462
|
+
if (action === "download") {
|
|
6463
|
+
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
6464
|
+
const file = await downloadBackup(cms, parsed, ctx.user.email);
|
|
6465
|
+
return {
|
|
6466
|
+
body: new Response(file.body, {
|
|
6467
|
+
headers: {
|
|
6468
|
+
"content-type": "application/gzip",
|
|
6469
|
+
"content-disposition": `attachment; filename="${file.filename}"`,
|
|
6470
|
+
"cache-control": "no-store",
|
|
6471
|
+
"x-content-type-options": "nosniff"
|
|
6472
|
+
}
|
|
6473
|
+
})
|
|
6474
|
+
};
|
|
6475
|
+
}
|
|
6476
|
+
if (action !== void 0) throw new HttpError("Not found", 404);
|
|
6477
|
+
if (method !== "DELETE") throw methodNotAllowed(ctx, "DELETE");
|
|
6478
|
+
await deleteBackup(cms, parsed);
|
|
6479
|
+
return { body: { deleted: 1 } };
|
|
6480
|
+
}
|
|
6481
|
+
if (first === "admin" && second === "email") {
|
|
6482
|
+
if (!ctx.user) throw new UnauthorizedError();
|
|
6483
|
+
if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
|
|
6484
|
+
const [, , action, extra] = segments;
|
|
6485
|
+
if (extra !== void 0) throw new HttpError("Not found", 404);
|
|
6486
|
+
if (action === void 0) {
|
|
6487
|
+
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
6488
|
+
return { body: adminEmail(cms) };
|
|
6489
|
+
}
|
|
6490
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
6491
|
+
if (action === "verify") return { body: await verifyEmail(cms) };
|
|
6492
|
+
if (action === "test") {
|
|
6493
|
+
const body = await readJson(ctx.request);
|
|
6494
|
+
return { body: await sendTestEmail(cms, ctx.user, body.to, body.locale) };
|
|
6495
|
+
}
|
|
6496
|
+
throw new HttpError("Not found", 404);
|
|
6497
|
+
}
|
|
6017
6498
|
if (first === "admin" && second === "deliveries") {
|
|
6018
6499
|
if (!ctx.user) throw new UnauthorizedError();
|
|
6019
6500
|
if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
|
|
@@ -6253,47 +6734,47 @@ async function documentAction(cms, ctx, method, collection2, id, path) {
|
|
|
6253
6734
|
}
|
|
6254
6735
|
throw new HttpError("Not found", 404);
|
|
6255
6736
|
}
|
|
6256
|
-
async function globalAction(cms, ctx, method,
|
|
6737
|
+
async function globalAction(cms, ctx, method, slug2, path) {
|
|
6257
6738
|
const access = { overrideAccess: false, user: ctx.user, ...parseLocale(ctx.url) };
|
|
6258
6739
|
const depth = parseDepth(ctx.url);
|
|
6259
6740
|
const [action, versionId, extra] = path;
|
|
6260
6741
|
if (action === "versions" && versionId === void 0) {
|
|
6261
6742
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
6262
|
-
return { body: await cms.findGlobalVersions(
|
|
6743
|
+
return { body: await cms.findGlobalVersions(slug2, { ...access, ...parsePage(ctx.url) }) };
|
|
6263
6744
|
}
|
|
6264
6745
|
if (action === "versions" && versionId !== void 0 && extra === void 0) {
|
|
6265
6746
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
6266
|
-
const version = await cms.findGlobalVersion(
|
|
6267
|
-
if (!version) throw new NotFoundError(`${
|
|
6747
|
+
const version = await cms.findGlobalVersion(slug2, versionId, { ...access, ...depth });
|
|
6748
|
+
if (!version) throw new NotFoundError(`${slug2} version`, versionId);
|
|
6268
6749
|
return { body: version };
|
|
6269
6750
|
}
|
|
6270
6751
|
if (action === "versions" && versionId !== void 0 && extra === "restore" && path.length === 3) {
|
|
6271
6752
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
6272
|
-
return { body: await cms.restoreGlobalVersion(
|
|
6753
|
+
return { body: await cms.restoreGlobalVersion(slug2, versionId, { ...access, ...depth }) };
|
|
6273
6754
|
}
|
|
6274
6755
|
if (action === "schedule" && path.length <= 2) {
|
|
6275
6756
|
if (path.length === 2) {
|
|
6276
6757
|
if (method !== "DELETE") throw methodNotAllowed(ctx, "DELETE");
|
|
6277
|
-
await cms.cancelGlobalSchedule(
|
|
6758
|
+
await cms.cancelGlobalSchedule(slug2, versionId, access);
|
|
6278
6759
|
return { body: { message: "Cancelled" } };
|
|
6279
6760
|
}
|
|
6280
|
-
if (method === "GET") return { body: await cms.scheduledGlobal(
|
|
6761
|
+
if (method === "GET") return { body: await cms.scheduledGlobal(slug2, access) };
|
|
6281
6762
|
if (method !== "POST") throw methodNotAllowed(ctx, "GET, POST");
|
|
6282
6763
|
const body = await readJson(ctx.request);
|
|
6283
|
-
return { status: 201, body: await cms.scheduleGlobal(
|
|
6764
|
+
return { status: 201, body: await cms.scheduleGlobal(slug2, scheduleJob(body), access) };
|
|
6284
6765
|
}
|
|
6285
6766
|
if (path.length === 1 && action === "preview") {
|
|
6286
6767
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
6287
6768
|
const body = await readJson(ctx.request);
|
|
6288
|
-
const result = await cms.previewGlobal(
|
|
6289
|
-
const token = cms.createPreviewToken({ global:
|
|
6769
|
+
const result = await cms.previewGlobal(slug2, body, { ...access, ...depth });
|
|
6770
|
+
const token = cms.createPreviewToken({ global: slug2 });
|
|
6290
6771
|
return { body: { ...result, url: result.url && withPreviewToken(result.url, token) } };
|
|
6291
6772
|
}
|
|
6292
6773
|
if (path.length === 1 && (action === "unpublish" || action === "discard-draft")) {
|
|
6293
6774
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
6294
6775
|
const options = { ...access, ...depth };
|
|
6295
6776
|
return {
|
|
6296
|
-
body: action === "unpublish" ? await cms.unpublishGlobal(
|
|
6777
|
+
body: action === "unpublish" ? await cms.unpublishGlobal(slug2, options) : await cms.discardGlobalDraft(slug2, options)
|
|
6297
6778
|
};
|
|
6298
6779
|
}
|
|
6299
6780
|
throw new HttpError("Not found", 404);
|
|
@@ -6471,11 +6952,11 @@ function errorResponse(cms, error, headers, production) {
|
|
|
6471
6952
|
if (error instanceof UnauthorizedError) headers.set("www-authenticate", "Bearer");
|
|
6472
6953
|
return new Response(JSON.stringify({ errors }), { status: error.status, headers });
|
|
6473
6954
|
}
|
|
6474
|
-
const
|
|
6955
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
6475
6956
|
cms.logger.error(
|
|
6476
|
-
`REST request failed: ${error instanceof Error ? error.stack ??
|
|
6957
|
+
`REST request failed: ${error instanceof Error ? error.stack ?? message2 : message2}`
|
|
6477
6958
|
);
|
|
6478
|
-
const body = { errors: [{ message: production ? "Internal Server Error" :
|
|
6959
|
+
const body = { errors: [{ message: production ? "Internal Server Error" : message2 }] };
|
|
6479
6960
|
return new Response(JSON.stringify(body), { status: 500, headers });
|
|
6480
6961
|
}
|
|
6481
6962
|
|
|
@@ -6626,7 +7107,7 @@ function generateTypes(config) {
|
|
|
6626
7107
|
for (const global2 of config.globals) {
|
|
6627
7108
|
parts.push("", globalInterface(global2, n.globals.get(global2.slug), n));
|
|
6628
7109
|
}
|
|
6629
|
-
const map = (entries) => [...entries].map(([
|
|
7110
|
+
const map = (entries) => [...entries].map(([slug2, name]) => ` ${key(slug2)}: ${name}`).join("\n");
|
|
6630
7111
|
parts.push(
|
|
6631
7112
|
"",
|
|
6632
7113
|
"/** Document type by collection slug. */",
|
|
@@ -6725,5 +7206,6 @@ export {
|
|
|
6725
7206
|
sniffMimeType,
|
|
6726
7207
|
validateConfig,
|
|
6727
7208
|
validateFields,
|
|
6728
|
-
verifyPassword
|
|
7209
|
+
verifyPassword,
|
|
7210
|
+
writeBackupFile
|
|
6729
7211
|
};
|