@easy-cms/core 0.31.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 +75 -1
- package/dist/index.js +601 -214
- 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
|
};
|
|
@@ -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@]+$/;
|
|
@@ -1802,7 +2165,7 @@ function applyFieldTypes(config) {
|
|
|
1802
2165
|
|
|
1803
2166
|
// src/load-config.ts
|
|
1804
2167
|
import { existsSync } from "fs";
|
|
1805
|
-
import { resolve } from "path";
|
|
2168
|
+
import { resolve as resolve2 } from "path";
|
|
1806
2169
|
import { createJiti } from "jiti";
|
|
1807
2170
|
|
|
1808
2171
|
// src/webhooks.ts
|
|
@@ -1994,6 +2357,8 @@ var RESERVED_SLUGS = /* @__PURE__ */ new Set([
|
|
|
1994
2357
|
"document-versions",
|
|
1995
2358
|
"scheduled-jobs",
|
|
1996
2359
|
"webhook-deliveries",
|
|
2360
|
+
"email-deliveries",
|
|
2361
|
+
"database-backups",
|
|
1997
2362
|
"api-keys",
|
|
1998
2363
|
"jobs",
|
|
1999
2364
|
"migrations",
|
|
@@ -2016,6 +2381,7 @@ function validateConfig(config) {
|
|
|
2016
2381
|
validateAdmin(config, add);
|
|
2017
2382
|
validateAdminViews(config, add);
|
|
2018
2383
|
validateUpload(config, add);
|
|
2384
|
+
validateBackups(config, add);
|
|
2019
2385
|
validateAuth(config, add);
|
|
2020
2386
|
validateCors(config.cors, add);
|
|
2021
2387
|
validateLocalization(config, add);
|
|
@@ -2025,17 +2391,17 @@ function validateConfig(config) {
|
|
|
2025
2391
|
const collectionSlugs = /* @__PURE__ */ new Set([...BUILTIN_COLLECTIONS]);
|
|
2026
2392
|
const seenCollections = /* @__PURE__ */ new Set();
|
|
2027
2393
|
collections.forEach((collection2, i) => {
|
|
2028
|
-
const
|
|
2029
|
-
if (
|
|
2394
|
+
const slug2 = checkSlug(collection2, `collections[${i}]`, seenCollections, add);
|
|
2395
|
+
if (slug2) collectionSlugs.add(slug2);
|
|
2030
2396
|
});
|
|
2031
2397
|
const menu = config.admin?.menu;
|
|
2032
2398
|
if (menu !== void 0) {
|
|
2033
2399
|
if (!Array.isArray(menu)) {
|
|
2034
2400
|
add("admin.menu", "must be an array of collection slugs", "e.g. menu: ['posts', 'media']");
|
|
2035
2401
|
} else {
|
|
2036
|
-
for (const [i,
|
|
2037
|
-
if (typeof
|
|
2038
|
-
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)}`);
|
|
2039
2405
|
}
|
|
2040
2406
|
}
|
|
2041
2407
|
}
|
|
@@ -2376,10 +2742,10 @@ function validateWebhooks(config, add) {
|
|
|
2376
2742
|
if (!WEBHOOK_EVENTS.includes(event))
|
|
2377
2743
|
add(`${path}.events`, `unknown event "${event}"`, `use: ${WEBHOOK_EVENTS.join(", ")}`);
|
|
2378
2744
|
}
|
|
2379
|
-
for (const
|
|
2380
|
-
if (!collections.has(
|
|
2381
|
-
for (const
|
|
2382
|
-
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}"`);
|
|
2383
2749
|
}
|
|
2384
2750
|
}
|
|
2385
2751
|
function validateCors(cors, add) {
|
|
@@ -2432,6 +2798,32 @@ function validateUpload(config, add) {
|
|
|
2432
2798
|
add("upload.fromURL.allowPrivate", "must be true or false");
|
|
2433
2799
|
}
|
|
2434
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
|
+
}
|
|
2435
2827
|
function asArray(value, path, add) {
|
|
2436
2828
|
if (value === void 0) return [];
|
|
2437
2829
|
if (!Array.isArray(value)) {
|
|
@@ -2441,24 +2833,24 @@ function asArray(value, path, add) {
|
|
|
2441
2833
|
return value;
|
|
2442
2834
|
}
|
|
2443
2835
|
function checkSlug(container, path, seen, add) {
|
|
2444
|
-
const
|
|
2445
|
-
if (typeof
|
|
2836
|
+
const slug2 = container.slug;
|
|
2837
|
+
if (typeof slug2 !== "string" || !SLUG_PATTERN.test(slug2)) {
|
|
2446
2838
|
add(
|
|
2447
2839
|
`${path}.slug`,
|
|
2448
|
-
`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)})`
|
|
2449
2841
|
);
|
|
2450
2842
|
return void 0;
|
|
2451
2843
|
}
|
|
2452
|
-
if (RESERVED_SLUGS.has(
|
|
2453
|
-
add(`${path}.slug`, `"${
|
|
2844
|
+
if (RESERVED_SLUGS.has(slug2)) {
|
|
2845
|
+
add(`${path}.slug`, `"${slug2}" is reserved by Easy CMS`, "choose another slug");
|
|
2454
2846
|
return void 0;
|
|
2455
2847
|
}
|
|
2456
|
-
if (seen.has(
|
|
2457
|
-
add(`${path}.slug`, `duplicate slug "${
|
|
2848
|
+
if (seen.has(slug2)) {
|
|
2849
|
+
add(`${path}.slug`, `duplicate slug "${slug2}"`);
|
|
2458
2850
|
return void 0;
|
|
2459
2851
|
}
|
|
2460
|
-
seen.add(
|
|
2461
|
-
return
|
|
2852
|
+
seen.add(slug2);
|
|
2853
|
+
return slug2;
|
|
2462
2854
|
}
|
|
2463
2855
|
function validateContainer(container, path, collectionSlugs, add) {
|
|
2464
2856
|
if (!Array.isArray(container.fields)) {
|
|
@@ -2903,7 +3295,8 @@ async function resolveConfig(input) {
|
|
|
2903
3295
|
...usesSchedule(config) ? [scheduledJobsCollection] : [],
|
|
2904
3296
|
...(config.webhooks?.length ?? 0) > 0 ? [webhookDeliveriesCollection] : [],
|
|
2905
3297
|
...config.apiKeys ? [apiKeysCollection] : [],
|
|
2906
|
-
...config.email ? [emailDeliveriesCollection] : []
|
|
3298
|
+
...config.email ? [emailDeliveriesCollection] : [],
|
|
3299
|
+
databaseBackupsCollection
|
|
2907
3300
|
],
|
|
2908
3301
|
globals: config.globals ?? [],
|
|
2909
3302
|
endpoints: config.endpoints ?? [],
|
|
@@ -2932,7 +3325,7 @@ var CONFIG_FILE_NAMES = [
|
|
|
2932
3325
|
];
|
|
2933
3326
|
function findConfigFile(cwd = process.cwd()) {
|
|
2934
3327
|
for (const name of CONFIG_FILE_NAMES) {
|
|
2935
|
-
const path =
|
|
3328
|
+
const path = resolve2(
|
|
2936
3329
|
/* turbopackIgnore: true */
|
|
2937
3330
|
cwd,
|
|
2938
3331
|
name
|
|
@@ -2951,7 +3344,7 @@ async function importConfig(file) {
|
|
|
2951
3344
|
}
|
|
2952
3345
|
async function loadConfig(options = {}) {
|
|
2953
3346
|
const cwd = options.cwd ?? process.cwd();
|
|
2954
|
-
const file = options.configFile ?
|
|
3347
|
+
const file = options.configFile ? resolve2(cwd, options.configFile) : findConfigFile(cwd);
|
|
2955
3348
|
if (!file || !existsSync(file)) {
|
|
2956
3349
|
const looked = options.configFile ?? CONFIG_FILE_NAMES.join(", ");
|
|
2957
3350
|
throw new Error(
|
|
@@ -3190,12 +3583,12 @@ async function populate(load, collections, fields, docs, depth) {
|
|
|
3190
3583
|
}
|
|
3191
3584
|
const found = /* @__PURE__ */ new Map();
|
|
3192
3585
|
await Promise.all(
|
|
3193
|
-
[...wanted].map(async ([
|
|
3194
|
-
const collection2 = bySlug.get(
|
|
3586
|
+
[...wanted].map(async ([slug2, ids]) => {
|
|
3587
|
+
const collection2 = bySlug.get(slug2);
|
|
3195
3588
|
if (!collection2) return;
|
|
3196
3589
|
const raw = await load(collection2, [...ids]);
|
|
3197
3590
|
const populated = await populate(load, collections, collection2.fields, raw, depth - 1);
|
|
3198
|
-
found.set(
|
|
3591
|
+
found.set(slug2, new Map(populated.map((d) => [d.id, d])));
|
|
3199
3592
|
})
|
|
3200
3593
|
);
|
|
3201
3594
|
for (const slot of slots) slot.assign(found.get(slot.collection) ?? /* @__PURE__ */ new Map());
|
|
@@ -3448,50 +3841,6 @@ function read(response, maxBytes, deadline) {
|
|
|
3448
3841
|
});
|
|
3449
3842
|
}
|
|
3450
3843
|
|
|
3451
|
-
// src/storage.ts
|
|
3452
|
-
import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
3453
|
-
import { dirname, isAbsolute, join, normalize, resolve as resolve2, sep } from "path";
|
|
3454
|
-
function localStorage(options = {}) {
|
|
3455
|
-
let root = resolve2(
|
|
3456
|
-
/* turbopackIgnore: true */
|
|
3457
|
-
options.dir ?? "uploads"
|
|
3458
|
-
);
|
|
3459
|
-
const pathOf = (key2) => {
|
|
3460
|
-
const path = normalize(join(
|
|
3461
|
-
/* turbopackIgnore: true */
|
|
3462
|
-
root,
|
|
3463
|
-
key2
|
|
3464
|
-
));
|
|
3465
|
-
if (!path.startsWith(root + sep)) throw new Error(`Invalid storage key "${key2}"`);
|
|
3466
|
-
return path;
|
|
3467
|
-
};
|
|
3468
|
-
return {
|
|
3469
|
-
name: "local",
|
|
3470
|
-
init({ cwd }) {
|
|
3471
|
-
const dir = options.dir ?? "uploads";
|
|
3472
|
-
root = isAbsolute(dir) ? dir : resolve2(
|
|
3473
|
-
/* turbopackIgnore: true */
|
|
3474
|
-
cwd,
|
|
3475
|
-
dir
|
|
3476
|
-
);
|
|
3477
|
-
},
|
|
3478
|
-
async put(key2, data) {
|
|
3479
|
-
const path = pathOf(key2);
|
|
3480
|
-
await mkdir(dirname(path), { recursive: true });
|
|
3481
|
-
await writeFile(path, data, { flag: "wx" });
|
|
3482
|
-
},
|
|
3483
|
-
async get(key2) {
|
|
3484
|
-
const path = pathOf(key2);
|
|
3485
|
-
const info = await stat(path).catch(() => void 0);
|
|
3486
|
-
if (!info?.isFile()) return null;
|
|
3487
|
-
return { body: new Uint8Array(await readFile(path)), size: info.size };
|
|
3488
|
-
},
|
|
3489
|
-
async delete(key2) {
|
|
3490
|
-
await rm(pathOf(key2), { force: true });
|
|
3491
|
-
}
|
|
3492
|
-
};
|
|
3493
|
-
}
|
|
3494
|
-
|
|
3495
3844
|
// src/versions.ts
|
|
3496
3845
|
var DEFAULT_MAX_VERSIONS = 50;
|
|
3497
3846
|
function versionLimit(config) {
|
|
@@ -3499,8 +3848,8 @@ function versionLimit(config) {
|
|
|
3499
3848
|
if (!versions) return void 0;
|
|
3500
3849
|
return versions === true ? DEFAULT_MAX_VERSIONS : versions.max ?? DEFAULT_MAX_VERSIONS;
|
|
3501
3850
|
}
|
|
3502
|
-
var collectionParent = (
|
|
3503
|
-
var globalParent = (
|
|
3851
|
+
var collectionParent = (slug2) => slug2;
|
|
3852
|
+
var globalParent = (slug2) => `global:${slug2}`;
|
|
3504
3853
|
function snapshotOf(fields, doc) {
|
|
3505
3854
|
const hidden = new Set(fields.filter((f) => f.hidden).map((f) => f.name));
|
|
3506
3855
|
const snapshot = {};
|
|
@@ -4022,17 +4371,17 @@ var EasyCMS = class {
|
|
|
4022
4371
|
return token ? verifyPreviewToken(this.config.secret, token) : null;
|
|
4023
4372
|
}
|
|
4024
4373
|
/** Live preview of a global. See `preview`. */
|
|
4025
|
-
async previewGlobal(
|
|
4026
|
-
const config = this.global(
|
|
4374
|
+
async previewGlobal(slug2, data, options = {}) {
|
|
4375
|
+
const config = this.global(slug2);
|
|
4027
4376
|
const guard = guardOf(options);
|
|
4028
4377
|
await this.checkGlobalAccess(config, "update", guard);
|
|
4029
|
-
const raw = asObject(data,
|
|
4378
|
+
const raw = asObject(data, slug2);
|
|
4030
4379
|
const input = await filterInput(
|
|
4031
4380
|
config.fields,
|
|
4032
4381
|
raw,
|
|
4033
4382
|
this.fieldChecker("update", guard, void 0, raw)
|
|
4034
4383
|
);
|
|
4035
|
-
const saved = await this.db.findGlobal({ slug }) ?? {};
|
|
4384
|
+
const saved = await this.db.findGlobal({ slug: slug2 }) ?? {};
|
|
4036
4385
|
const current = await this.globalDraft(config, saved);
|
|
4037
4386
|
const merged = generateSlugs(
|
|
4038
4387
|
config.fields,
|
|
@@ -4202,11 +4551,11 @@ var EasyCMS = class {
|
|
|
4202
4551
|
const [out] = await this.output(config, [existing], guard, { depth: 0, draft: true });
|
|
4203
4552
|
return out;
|
|
4204
4553
|
}
|
|
4205
|
-
async findGlobal(
|
|
4206
|
-
const config = this.global(
|
|
4554
|
+
async findGlobal(slug2, options = {}) {
|
|
4555
|
+
const config = this.global(slug2);
|
|
4207
4556
|
const guard = guardOf(options);
|
|
4208
4557
|
await this.checkGlobalAccess(config, "read", guard);
|
|
4209
|
-
const saved = await this.db.findGlobal({ slug });
|
|
4558
|
+
const saved = await this.db.findGlobal({ slug: slug2 });
|
|
4210
4559
|
const stored = saved && options.draft ? await this.globalDraft(config, saved) : saved;
|
|
4211
4560
|
const data = fillMissing(
|
|
4212
4561
|
config.fields,
|
|
@@ -4220,33 +4569,33 @@ var EasyCMS = class {
|
|
|
4220
4569
|
const { id: _id, ...doc } = out;
|
|
4221
4570
|
return doc;
|
|
4222
4571
|
}
|
|
4223
|
-
async updateGlobal(
|
|
4224
|
-
return this.saveGlobal(
|
|
4572
|
+
async updateGlobal(slug2, data, options = {}) {
|
|
4573
|
+
return this.saveGlobal(slug2, asObject(data, slug2), options);
|
|
4225
4574
|
}
|
|
4226
4575
|
/** Takes a global off the site (`status: 'draft'`). See `unpublish`. */
|
|
4227
|
-
async unpublishGlobal(
|
|
4228
|
-
if (!this.global(
|
|
4229
|
-
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");
|
|
4230
4579
|
}
|
|
4231
4580
|
/** Throws away the unpublished draft of a published global. */
|
|
4232
|
-
async discardGlobalDraft(
|
|
4233
|
-
const config = this.global(
|
|
4581
|
+
async discardGlobalDraft(slug2, options = {}) {
|
|
4582
|
+
const config = this.global(slug2);
|
|
4234
4583
|
const guard = guardOf(options);
|
|
4235
4584
|
await this.checkGlobalAccess(config, "update", guard);
|
|
4236
|
-
const saved = await this.db.findGlobal({ slug });
|
|
4585
|
+
const saved = await this.db.findGlobal({ slug: slug2 });
|
|
4237
4586
|
if (saved && await this.globalDraft(config, saved) !== saved) {
|
|
4238
|
-
await this.saveVersion(config, globalParent(
|
|
4587
|
+
await this.saveVersion(config, globalParent(slug2), 0, saved, guard);
|
|
4239
4588
|
}
|
|
4240
|
-
return this.findGlobal(
|
|
4589
|
+
return this.findGlobal(slug2, { ...options, draft: true });
|
|
4241
4590
|
}
|
|
4242
4591
|
/** Saved versions of a global, newest first. Needs update access. */
|
|
4243
|
-
async findGlobalVersions(
|
|
4244
|
-
await this.globalVersionTarget(
|
|
4245
|
-
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);
|
|
4246
4595
|
}
|
|
4247
|
-
async findGlobalVersion(
|
|
4248
|
-
const config = await this.globalVersionTarget(
|
|
4249
|
-
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);
|
|
4250
4599
|
if (!version) return null;
|
|
4251
4600
|
const data = fillMissing(
|
|
4252
4601
|
config.fields,
|
|
@@ -4260,21 +4609,21 @@ var EasyCMS = class {
|
|
|
4260
4609
|
return { ...version, data: { ...doc, updatedAt: version.createdAt } };
|
|
4261
4610
|
}
|
|
4262
4611
|
/** Makes an old version of a global current (as a draft when the global has drafts). */
|
|
4263
|
-
async restoreGlobalVersion(
|
|
4264
|
-
const config = await this.globalVersionTarget(
|
|
4265
|
-
const version = await this.versions.get(globalParent(
|
|
4266
|
-
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);
|
|
4267
4616
|
const { status: _status, ...data } = version.data;
|
|
4268
4617
|
return this.saveGlobal(
|
|
4269
|
-
|
|
4618
|
+
slug2,
|
|
4270
4619
|
config.drafts ? { ...data, status: "draft" } : data,
|
|
4271
4620
|
options,
|
|
4272
4621
|
"restore"
|
|
4273
4622
|
);
|
|
4274
4623
|
}
|
|
4275
|
-
async globalVersionTarget(
|
|
4276
|
-
const config = this.global(
|
|
4277
|
-
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`);
|
|
4278
4627
|
await this.checkGlobalAccess(config, "update", guardOf(options));
|
|
4279
4628
|
return config;
|
|
4280
4629
|
}
|
|
@@ -4285,18 +4634,18 @@ var EasyCMS = class {
|
|
|
4285
4634
|
if (version?.status !== "draft") return saved;
|
|
4286
4635
|
return { ...version.data, updatedAt: version.createdAt, status: "draft" };
|
|
4287
4636
|
}
|
|
4288
|
-
async saveGlobal(
|
|
4289
|
-
const config = this.global(
|
|
4637
|
+
async saveGlobal(slug2, raw, options, mode = "save") {
|
|
4638
|
+
const config = this.global(slug2);
|
|
4290
4639
|
const guard = guardOf(options);
|
|
4291
4640
|
await this.checkGlobalAccess(config, "update", guard);
|
|
4292
4641
|
if (config.drafts && (mode === "unpublish" || raw.status === "published"))
|
|
4293
|
-
this.checkKey(guard, { global:
|
|
4642
|
+
this.checkKey(guard, { global: slug2 }, "publish");
|
|
4294
4643
|
const input = await filterInput(
|
|
4295
4644
|
config.fields,
|
|
4296
4645
|
raw,
|
|
4297
4646
|
this.fieldChecker("update", guard, void 0, raw)
|
|
4298
4647
|
);
|
|
4299
|
-
const existing = await this.db.findGlobal({ slug }) ?? {};
|
|
4648
|
+
const existing = await this.db.findGlobal({ slug: slug2 }) ?? {};
|
|
4300
4649
|
const current = await this.globalDraft(config, existing);
|
|
4301
4650
|
const localized = mode === "restore" ? input : this.toMaps(config, input, current, options);
|
|
4302
4651
|
const merged = generateSlugs(
|
|
@@ -4320,26 +4669,26 @@ var EasyCMS = class {
|
|
|
4320
4669
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4321
4670
|
if (mode !== "unpublish" && this.separateDrafts(config) && existing.status === "published" && prepared.status === "draft") {
|
|
4322
4671
|
const draft = { ...prepared, updatedAt: now };
|
|
4323
|
-
await this.saveVersion(config, globalParent(
|
|
4324
|
-
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, {
|
|
4325
4674
|
...base,
|
|
4326
4675
|
doc: draft,
|
|
4327
4676
|
previousDoc: current,
|
|
4328
4677
|
operation: "update"
|
|
4329
4678
|
});
|
|
4330
4679
|
this.emit(config, "draft", draft);
|
|
4331
|
-
return this.findGlobal(
|
|
4680
|
+
return this.findGlobal(slug2, { ...options, draft: true });
|
|
4332
4681
|
}
|
|
4333
|
-
const doc = await this.db.updateGlobal({ slug, data: { ...prepared, updatedAt: now } });
|
|
4334
|
-
await this.saveVersion(config, globalParent(
|
|
4335
|
-
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, {
|
|
4336
4685
|
...base,
|
|
4337
4686
|
doc,
|
|
4338
4687
|
previousDoc: current,
|
|
4339
4688
|
operation: "update"
|
|
4340
4689
|
});
|
|
4341
4690
|
this.emit(config, "update", doc, existing.status);
|
|
4342
|
-
return this.findGlobal(
|
|
4691
|
+
return this.findGlobal(slug2, { ...options, draft: true });
|
|
4343
4692
|
}
|
|
4344
4693
|
/**
|
|
4345
4694
|
* What `user` may do with one document. Resolves `where`-style access
|
|
@@ -4442,6 +4791,9 @@ var EasyCMS = class {
|
|
|
4442
4791
|
async runJobs(now = /* @__PURE__ */ new Date()) {
|
|
4443
4792
|
const scheduled = await this.runScheduled(now);
|
|
4444
4793
|
await this.pruneFailedDeliveries(now);
|
|
4794
|
+
await runDueBackups(this, now).catch(
|
|
4795
|
+
(error) => this.logger.error(`Backups: ${error.message}`)
|
|
4796
|
+
);
|
|
4445
4797
|
return {
|
|
4446
4798
|
...scheduled,
|
|
4447
4799
|
webhooks: await this.retryWebhooks(now),
|
|
@@ -4511,9 +4863,9 @@ var EasyCMS = class {
|
|
|
4511
4863
|
const action = row.action;
|
|
4512
4864
|
try {
|
|
4513
4865
|
if (parent.startsWith("global:")) {
|
|
4514
|
-
const
|
|
4515
|
-
if (action === "publish") await this.saveGlobal(
|
|
4516
|
-
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");
|
|
4517
4869
|
} else if (action === "publish") {
|
|
4518
4870
|
await this.updateDocument(parent, row.doc, { status: "published" }, { depth: 0 });
|
|
4519
4871
|
} else {
|
|
@@ -4557,17 +4909,17 @@ var EasyCMS = class {
|
|
|
4557
4909
|
const { parsed } = await this.scheduleTarget(collection2, id, options);
|
|
4558
4910
|
await this.removeJob(collection2, parsed, jobId);
|
|
4559
4911
|
}
|
|
4560
|
-
async scheduleGlobal(
|
|
4561
|
-
await this.globalScheduleTarget(
|
|
4562
|
-
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);
|
|
4563
4915
|
}
|
|
4564
|
-
async scheduledGlobal(
|
|
4565
|
-
await this.globalScheduleTarget(
|
|
4566
|
-
return this.pendingJobs(`global:${
|
|
4916
|
+
async scheduledGlobal(slug2, options = {}) {
|
|
4917
|
+
await this.globalScheduleTarget(slug2, options);
|
|
4918
|
+
return this.pendingJobs(`global:${slug2}`, 0);
|
|
4567
4919
|
}
|
|
4568
|
-
async cancelGlobalSchedule(
|
|
4569
|
-
await this.globalScheduleTarget(
|
|
4570
|
-
await this.removeJob(`global:${
|
|
4920
|
+
async cancelGlobalSchedule(slug2, jobId, options = {}) {
|
|
4921
|
+
await this.globalScheduleTarget(slug2, options);
|
|
4922
|
+
await this.removeJob(`global:${slug2}`, 0, jobId);
|
|
4571
4923
|
}
|
|
4572
4924
|
async scheduleTarget(collection2, id, options) {
|
|
4573
4925
|
const config = this.collection(collection2);
|
|
@@ -4579,11 +4931,11 @@ var EasyCMS = class {
|
|
|
4579
4931
|
this.checkKey(guardOf(options), { collection: collection2 }, "publish");
|
|
4580
4932
|
return { config, parsed };
|
|
4581
4933
|
}
|
|
4582
|
-
async globalScheduleTarget(
|
|
4583
|
-
const config = this.global(
|
|
4584
|
-
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`);
|
|
4585
4937
|
await this.checkGlobalAccess(config, "update", guardOf(options));
|
|
4586
|
-
this.checkKey(guardOf(options), { global:
|
|
4938
|
+
this.checkKey(guardOf(options), { global: slug2 }, "publish");
|
|
4587
4939
|
}
|
|
4588
4940
|
async addJob(parent, doc, job, options) {
|
|
4589
4941
|
const runAt = new Date(job.at);
|
|
@@ -4717,14 +5069,14 @@ var EasyCMS = class {
|
|
|
4717
5069
|
}
|
|
4718
5070
|
// -------------------------------------------------------------------------
|
|
4719
5071
|
/** @internal */
|
|
4720
|
-
collection(
|
|
4721
|
-
const config = this.config.collections.find((c) => c.slug ===
|
|
4722
|
-
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}"`);
|
|
4723
5075
|
return config;
|
|
4724
5076
|
}
|
|
4725
|
-
global(
|
|
4726
|
-
const config = this.config.globals.find((g) => g.slug ===
|
|
4727
|
-
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}"`);
|
|
4728
5080
|
return config;
|
|
4729
5081
|
}
|
|
4730
5082
|
/** Refuses what a request's API key does not allow (see `ApiKeyPermissions`). */
|
|
@@ -4974,13 +5326,13 @@ var EasyCMS = class {
|
|
|
4974
5326
|
return current;
|
|
4975
5327
|
}
|
|
4976
5328
|
/** Runs hooks after the change is saved: failures are logged, never undo the save (NFR-REL-03). */
|
|
4977
|
-
async notify(hooks, name,
|
|
5329
|
+
async notify(hooks, name, slug2, args) {
|
|
4978
5330
|
for (const hook of hooks ?? []) {
|
|
4979
5331
|
try {
|
|
4980
5332
|
await hook(args);
|
|
4981
5333
|
} catch (error) {
|
|
4982
5334
|
this.logger.error(
|
|
4983
|
-
`${name} hook of "${
|
|
5335
|
+
`${name} hook of "${slug2}" failed: ${error instanceof Error ? error.message : String(error)}`
|
|
4984
5336
|
);
|
|
4985
5337
|
}
|
|
4986
5338
|
}
|
|
@@ -5082,9 +5434,9 @@ var EasyCMS = class {
|
|
|
5082
5434
|
const value = data[field.name];
|
|
5083
5435
|
if (field.localized && value && typeof value === "object") {
|
|
5084
5436
|
const map = { ...value };
|
|
5085
|
-
for (const [locale,
|
|
5086
|
-
if (typeof
|
|
5087
|
-
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);
|
|
5088
5440
|
}
|
|
5089
5441
|
data[field.name] = map;
|
|
5090
5442
|
} else if (typeof value === "string" && value !== "") {
|
|
@@ -5223,11 +5575,11 @@ function asObject(data, name) {
|
|
|
5223
5575
|
|
|
5224
5576
|
// src/rest/admin-modules.ts
|
|
5225
5577
|
import { createHash as createHash3 } from "crypto";
|
|
5226
|
-
import { readFile as
|
|
5578
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
5227
5579
|
import { createRequire } from "module";
|
|
5228
|
-
import { join as
|
|
5580
|
+
import { join as join3 } from "path";
|
|
5229
5581
|
function resolveAdminModule(specifier, cwd = process.cwd()) {
|
|
5230
|
-
const bases = [
|
|
5582
|
+
const bases = [join3(cwd, "package.json")];
|
|
5231
5583
|
const entry = process.argv[1];
|
|
5232
5584
|
if (entry && !specifier.startsWith(".")) bases.push(entry);
|
|
5233
5585
|
for (const base of bases) {
|
|
@@ -5261,7 +5613,7 @@ function adminModuleUrls(cms) {
|
|
|
5261
5613
|
async function readAdminModule(cms, index) {
|
|
5262
5614
|
const file = files(cms)[index];
|
|
5263
5615
|
if (!file) return void 0;
|
|
5264
|
-
const body = await
|
|
5616
|
+
const body = await readFile3(file, "utf8").catch(() => void 0);
|
|
5265
5617
|
if (body === void 0) return void 0;
|
|
5266
5618
|
return { body, etag: `"${createHash3("sha1").update(body).digest("base64url")}"` };
|
|
5267
5619
|
}
|
|
@@ -5279,7 +5631,7 @@ function parse(text) {
|
|
|
5279
5631
|
return {};
|
|
5280
5632
|
}
|
|
5281
5633
|
}
|
|
5282
|
-
function
|
|
5634
|
+
function toAdmin2(kind, row) {
|
|
5283
5635
|
const base = {
|
|
5284
5636
|
id: row.id,
|
|
5285
5637
|
state: row.state === "failed" ? "failed" : "pending",
|
|
@@ -5327,7 +5679,7 @@ async function listDeliveries(cms, kind, state, page) {
|
|
|
5327
5679
|
cms.db.count({ collection: collection2, where: { state: { equals: "pending" } } })
|
|
5328
5680
|
]);
|
|
5329
5681
|
return {
|
|
5330
|
-
docs: found.docs.map((row) =>
|
|
5682
|
+
docs: found.docs.map((row) => toAdmin2(kind, row)),
|
|
5331
5683
|
totalDocs: found.totalDocs,
|
|
5332
5684
|
page: found.page,
|
|
5333
5685
|
totalPages: found.totalPages,
|
|
@@ -5468,7 +5820,7 @@ function componentRef(component) {
|
|
|
5468
5820
|
async function allowed(access, user) {
|
|
5469
5821
|
return await evaluateAccess(access, { user }) !== false;
|
|
5470
5822
|
}
|
|
5471
|
-
async function serializeFields(fields,
|
|
5823
|
+
async function serializeFields(fields, update2, localized) {
|
|
5472
5824
|
const out = [];
|
|
5473
5825
|
for (const field of fields) {
|
|
5474
5826
|
if (field.hidden) continue;
|
|
@@ -5477,7 +5829,7 @@ async function serializeFields(fields, update, localized) {
|
|
|
5477
5829
|
if (field.required) f.required = true;
|
|
5478
5830
|
if (field.unique) f.unique = true;
|
|
5479
5831
|
if (field.defaultValue !== void 0) f.defaultValue = field.defaultValue;
|
|
5480
|
-
if (!await
|
|
5832
|
+
if (!await update2.allows(field)) f.readOnly = true;
|
|
5481
5833
|
if (field.localized && localized) f.localized = true;
|
|
5482
5834
|
if (field.position === "sidebar") f.position = "sidebar";
|
|
5483
5835
|
if (field.admin?.component || field.admin?.after?.length || field.admin?.cell) {
|
|
@@ -5524,7 +5876,7 @@ async function serializeFields(fields, update, localized) {
|
|
|
5524
5876
|
case "array":
|
|
5525
5877
|
if (field.minRows !== void 0) f.minRows = field.minRows;
|
|
5526
5878
|
if (field.maxRows !== void 0) f.maxRows = field.maxRows;
|
|
5527
|
-
f.fields = await serializeFields(field.fields,
|
|
5879
|
+
f.fields = await serializeFields(field.fields, update2, localized);
|
|
5528
5880
|
break;
|
|
5529
5881
|
case "blocks":
|
|
5530
5882
|
if (field.minRows !== void 0) f.minRows = field.minRows;
|
|
@@ -5533,12 +5885,12 @@ async function serializeFields(fields, update, localized) {
|
|
|
5533
5885
|
field.blocks.map(async (block) => ({
|
|
5534
5886
|
slug: block.slug,
|
|
5535
5887
|
...block.labels ? { labels: block.labels } : {},
|
|
5536
|
-
fields: await serializeFields(block.fields,
|
|
5888
|
+
fields: await serializeFields(block.fields, update2, localized)
|
|
5537
5889
|
}))
|
|
5538
5890
|
);
|
|
5539
5891
|
break;
|
|
5540
5892
|
case "group":
|
|
5541
|
-
f.fields = await serializeFields(field.fields,
|
|
5893
|
+
f.fields = await serializeFields(field.fields, update2, localized);
|
|
5542
5894
|
break;
|
|
5543
5895
|
}
|
|
5544
5896
|
out.push(f);
|
|
@@ -5652,11 +6004,11 @@ async function widgets(cms, user) {
|
|
|
5652
6004
|
}
|
|
5653
6005
|
|
|
5654
6006
|
// src/version.ts
|
|
5655
|
-
var VERSION = true ? "0.
|
|
6007
|
+
var VERSION = true ? "0.32.0" : "0.0.0-dev";
|
|
5656
6008
|
|
|
5657
6009
|
// src/rest/admin-status.ts
|
|
5658
6010
|
var DAY = 864e5;
|
|
5659
|
-
var has = (cms,
|
|
6011
|
+
var has = (cms, slug2) => cms.config.collections.some((c) => c.slug === slug2);
|
|
5660
6012
|
async function adminStatus(cms) {
|
|
5661
6013
|
const config = cms.config;
|
|
5662
6014
|
const attention = [];
|
|
@@ -5709,6 +6061,8 @@ async function adminStatus(cms) {
|
|
|
5709
6061
|
});
|
|
5710
6062
|
if (count > 0) attention.push({ id: "scheduled", count });
|
|
5711
6063
|
}
|
|
6064
|
+
const backups = await backupAttention(cms);
|
|
6065
|
+
if (backups) attention.push(backups);
|
|
5712
6066
|
if (!config.email) attention.push({ id: "no-email" });
|
|
5713
6067
|
if (!config.serverURL && process.env.NODE_ENV === "production")
|
|
5714
6068
|
attention.push({ id: "no-server-url" });
|
|
@@ -5743,8 +6097,8 @@ async function pickerFilter(cms, url, target, user) {
|
|
|
5743
6097
|
const spec = forCollection ?? forGlobal;
|
|
5744
6098
|
if (spec === null) return void 0;
|
|
5745
6099
|
const dot = spec.indexOf(".");
|
|
5746
|
-
const
|
|
5747
|
-
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);
|
|
5748
6102
|
const field = owner ? fieldAt(owner.fields, spec.slice(dot + 1)) : void 0;
|
|
5749
6103
|
if (field?.type !== "relationship" || field.to !== target)
|
|
5750
6104
|
throw new QueryError(`filterFor: no relationship to "${target}" at "${spec}"`);
|
|
@@ -6092,6 +6446,38 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
6092
6446
|
}
|
|
6093
6447
|
return { body: await cms.runJobs() };
|
|
6094
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
|
+
}
|
|
6095
6481
|
if (first === "admin" && second === "email") {
|
|
6096
6482
|
if (!ctx.user) throw new UnauthorizedError();
|
|
6097
6483
|
if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
|
|
@@ -6348,47 +6734,47 @@ async function documentAction(cms, ctx, method, collection2, id, path) {
|
|
|
6348
6734
|
}
|
|
6349
6735
|
throw new HttpError("Not found", 404);
|
|
6350
6736
|
}
|
|
6351
|
-
async function globalAction(cms, ctx, method,
|
|
6737
|
+
async function globalAction(cms, ctx, method, slug2, path) {
|
|
6352
6738
|
const access = { overrideAccess: false, user: ctx.user, ...parseLocale(ctx.url) };
|
|
6353
6739
|
const depth = parseDepth(ctx.url);
|
|
6354
6740
|
const [action, versionId, extra] = path;
|
|
6355
6741
|
if (action === "versions" && versionId === void 0) {
|
|
6356
6742
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
6357
|
-
return { body: await cms.findGlobalVersions(
|
|
6743
|
+
return { body: await cms.findGlobalVersions(slug2, { ...access, ...parsePage(ctx.url) }) };
|
|
6358
6744
|
}
|
|
6359
6745
|
if (action === "versions" && versionId !== void 0 && extra === void 0) {
|
|
6360
6746
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
6361
|
-
const version = await cms.findGlobalVersion(
|
|
6362
|
-
if (!version) throw new NotFoundError(`${
|
|
6747
|
+
const version = await cms.findGlobalVersion(slug2, versionId, { ...access, ...depth });
|
|
6748
|
+
if (!version) throw new NotFoundError(`${slug2} version`, versionId);
|
|
6363
6749
|
return { body: version };
|
|
6364
6750
|
}
|
|
6365
6751
|
if (action === "versions" && versionId !== void 0 && extra === "restore" && path.length === 3) {
|
|
6366
6752
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
6367
|
-
return { body: await cms.restoreGlobalVersion(
|
|
6753
|
+
return { body: await cms.restoreGlobalVersion(slug2, versionId, { ...access, ...depth }) };
|
|
6368
6754
|
}
|
|
6369
6755
|
if (action === "schedule" && path.length <= 2) {
|
|
6370
6756
|
if (path.length === 2) {
|
|
6371
6757
|
if (method !== "DELETE") throw methodNotAllowed(ctx, "DELETE");
|
|
6372
|
-
await cms.cancelGlobalSchedule(
|
|
6758
|
+
await cms.cancelGlobalSchedule(slug2, versionId, access);
|
|
6373
6759
|
return { body: { message: "Cancelled" } };
|
|
6374
6760
|
}
|
|
6375
|
-
if (method === "GET") return { body: await cms.scheduledGlobal(
|
|
6761
|
+
if (method === "GET") return { body: await cms.scheduledGlobal(slug2, access) };
|
|
6376
6762
|
if (method !== "POST") throw methodNotAllowed(ctx, "GET, POST");
|
|
6377
6763
|
const body = await readJson(ctx.request);
|
|
6378
|
-
return { status: 201, body: await cms.scheduleGlobal(
|
|
6764
|
+
return { status: 201, body: await cms.scheduleGlobal(slug2, scheduleJob(body), access) };
|
|
6379
6765
|
}
|
|
6380
6766
|
if (path.length === 1 && action === "preview") {
|
|
6381
6767
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
6382
6768
|
const body = await readJson(ctx.request);
|
|
6383
|
-
const result = await cms.previewGlobal(
|
|
6384
|
-
const token = cms.createPreviewToken({ global:
|
|
6769
|
+
const result = await cms.previewGlobal(slug2, body, { ...access, ...depth });
|
|
6770
|
+
const token = cms.createPreviewToken({ global: slug2 });
|
|
6385
6771
|
return { body: { ...result, url: result.url && withPreviewToken(result.url, token) } };
|
|
6386
6772
|
}
|
|
6387
6773
|
if (path.length === 1 && (action === "unpublish" || action === "discard-draft")) {
|
|
6388
6774
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
6389
6775
|
const options = { ...access, ...depth };
|
|
6390
6776
|
return {
|
|
6391
|
-
body: action === "unpublish" ? await cms.unpublishGlobal(
|
|
6777
|
+
body: action === "unpublish" ? await cms.unpublishGlobal(slug2, options) : await cms.discardGlobalDraft(slug2, options)
|
|
6392
6778
|
};
|
|
6393
6779
|
}
|
|
6394
6780
|
throw new HttpError("Not found", 404);
|
|
@@ -6721,7 +7107,7 @@ function generateTypes(config) {
|
|
|
6721
7107
|
for (const global2 of config.globals) {
|
|
6722
7108
|
parts.push("", globalInterface(global2, n.globals.get(global2.slug), n));
|
|
6723
7109
|
}
|
|
6724
|
-
const map = (entries) => [...entries].map(([
|
|
7110
|
+
const map = (entries) => [...entries].map(([slug2, name]) => ` ${key(slug2)}: ${name}`).join("\n");
|
|
6725
7111
|
parts.push(
|
|
6726
7112
|
"",
|
|
6727
7113
|
"/** Document type by collection slug. */",
|
|
@@ -6820,5 +7206,6 @@ export {
|
|
|
6820
7206
|
sniffMimeType,
|
|
6821
7207
|
validateConfig,
|
|
6822
7208
|
validateFields,
|
|
6823
|
-
verifyPassword
|
|
7209
|
+
verifyPassword,
|
|
7210
|
+
writeBackupFile
|
|
6824
7211
|
};
|