@easy-cms/core 0.5.0 → 0.7.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 +193 -10
- package/dist/index.js +748 -79
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -82,6 +82,31 @@ var PayloadTooLargeError = class extends EasyCMSError {
|
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
// src/fields.ts
|
|
86
|
+
var hasRows = (field) => field.type === "array" || field.type === "blocks";
|
|
87
|
+
function rowFields(field, row) {
|
|
88
|
+
if (field.type === "array") return field.fields;
|
|
89
|
+
const type = row && typeof row === "object" ? row.blockType : void 0;
|
|
90
|
+
return field.blocks.find((block) => block.slug === type)?.fields;
|
|
91
|
+
}
|
|
92
|
+
var FIELD_TYPES = [
|
|
93
|
+
"text",
|
|
94
|
+
"textarea",
|
|
95
|
+
"number",
|
|
96
|
+
"boolean",
|
|
97
|
+
"date",
|
|
98
|
+
"email",
|
|
99
|
+
"json",
|
|
100
|
+
"select",
|
|
101
|
+
"slug",
|
|
102
|
+
"richText",
|
|
103
|
+
"upload",
|
|
104
|
+
"relationship",
|
|
105
|
+
"array",
|
|
106
|
+
"group",
|
|
107
|
+
"blocks"
|
|
108
|
+
];
|
|
109
|
+
|
|
85
110
|
// src/access-control.ts
|
|
86
111
|
async function evaluateAccess(access, args) {
|
|
87
112
|
const result = await (access ?? isLoggedIn)(args);
|
|
@@ -123,11 +148,12 @@ async function stripFields(fields, doc, read) {
|
|
|
123
148
|
const value = result[field.name];
|
|
124
149
|
if (field.type === "group" && value && typeof value === "object") {
|
|
125
150
|
result[field.name] = await stripFields(field.fields, value, read);
|
|
126
|
-
} else if (field
|
|
151
|
+
} else if (hasRows(field) && Array.isArray(value)) {
|
|
127
152
|
result[field.name] = await Promise.all(
|
|
128
|
-
value.map(
|
|
129
|
-
|
|
130
|
-
|
|
153
|
+
value.map((row) => {
|
|
154
|
+
const fields2 = rowFields(field, row);
|
|
155
|
+
return fields2 && row && typeof row === "object" ? stripFields(fields2, row, read) : row;
|
|
156
|
+
})
|
|
131
157
|
);
|
|
132
158
|
}
|
|
133
159
|
}
|
|
@@ -144,11 +170,12 @@ async function filterInput(fields, data, update) {
|
|
|
144
170
|
const value = result[field.name];
|
|
145
171
|
if (field.type === "group" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
146
172
|
result[field.name] = await filterInput(field.fields, value, update);
|
|
147
|
-
} else if (field
|
|
173
|
+
} else if (hasRows(field) && Array.isArray(value)) {
|
|
148
174
|
result[field.name] = await Promise.all(
|
|
149
|
-
value.map(
|
|
150
|
-
|
|
151
|
-
|
|
175
|
+
value.map((row) => {
|
|
176
|
+
const fields2 = rowFields(field, row);
|
|
177
|
+
return fields2 && row && typeof row === "object" ? filterInput(fields2, row, update) : row;
|
|
178
|
+
})
|
|
152
179
|
);
|
|
153
180
|
}
|
|
154
181
|
}
|
|
@@ -161,10 +188,14 @@ var MEDIA = "media";
|
|
|
161
188
|
var SESSIONS = "sessions";
|
|
162
189
|
var LOGIN_ATTEMPTS = "login-attempts";
|
|
163
190
|
var VERSIONS = "document-versions";
|
|
191
|
+
var SCHEDULED_JOBS = "scheduled-jobs";
|
|
192
|
+
var WEBHOOK_DELIVERIES = "webhook-deliveries";
|
|
164
193
|
var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
165
194
|
SESSIONS,
|
|
166
195
|
LOGIN_ATTEMPTS,
|
|
167
|
-
VERSIONS
|
|
196
|
+
VERSIONS,
|
|
197
|
+
SCHEDULED_JOBS,
|
|
198
|
+
WEBHOOK_DELIVERIES
|
|
168
199
|
]);
|
|
169
200
|
var DEFAULT_ROLES = ["admin", "editor"];
|
|
170
201
|
var adminOrSelf = ({ user }) => {
|
|
@@ -345,6 +376,38 @@ var versionsCollection = {
|
|
|
345
376
|
{ name: "snapshot", type: "json", required: true }
|
|
346
377
|
]
|
|
347
378
|
};
|
|
379
|
+
var scheduledJobsCollection = {
|
|
380
|
+
slug: SCHEDULED_JOBS,
|
|
381
|
+
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
382
|
+
fields: [
|
|
383
|
+
// Collection slug, or `global:<slug>`.
|
|
384
|
+
{ name: "parent", type: "text", required: true, index: true },
|
|
385
|
+
// Document id; 0 for globals.
|
|
386
|
+
{ name: "doc", type: "number", required: true, index: true },
|
|
387
|
+
{ name: "action", type: "text", required: true },
|
|
388
|
+
{ name: "runAt", type: "text", required: true, index: true },
|
|
389
|
+
// pending | done | failed
|
|
390
|
+
{ name: "state", type: "text", required: true, index: true },
|
|
391
|
+
{ name: "error", type: "text" },
|
|
392
|
+
{ name: "author", type: "number" }
|
|
393
|
+
]
|
|
394
|
+
};
|
|
395
|
+
var webhookDeliveriesCollection = {
|
|
396
|
+
slug: WEBHOOK_DELIVERIES,
|
|
397
|
+
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
398
|
+
fields: [
|
|
399
|
+
{ name: "url", type: "text", required: true },
|
|
400
|
+
{ name: "event", type: "text", required: true },
|
|
401
|
+
// The JSON body as first sent: retries are byte-for-byte the same, signature included.
|
|
402
|
+
{ name: "body", type: "textarea", required: true },
|
|
403
|
+
{ name: "delivery", type: "text", required: true },
|
|
404
|
+
{ name: "attempts", type: "number", required: true },
|
|
405
|
+
{ name: "nextAttemptAt", type: "text", required: true, index: true },
|
|
406
|
+
// pending | failed
|
|
407
|
+
{ name: "state", type: "text", required: true, index: true },
|
|
408
|
+
{ name: "error", type: "text" }
|
|
409
|
+
]
|
|
410
|
+
};
|
|
348
411
|
|
|
349
412
|
// src/auth/password.ts
|
|
350
413
|
import { randomBytes, scrypt, timingSafeEqual } from "crypto";
|
|
@@ -654,11 +717,12 @@ function applyDefaults(fields, data, localization = null) {
|
|
|
654
717
|
isPlainObject(value) ? value : {},
|
|
655
718
|
localization
|
|
656
719
|
);
|
|
657
|
-
} else if (field
|
|
720
|
+
} else if (hasRows(field) && !field.localized) {
|
|
658
721
|
if (Array.isArray(value)) {
|
|
659
|
-
result[field.name] = value.map(
|
|
660
|
-
|
|
661
|
-
|
|
722
|
+
result[field.name] = value.map((row) => {
|
|
723
|
+
const fields2 = rowFields(field, row);
|
|
724
|
+
return fields2 && isPlainObject(row) ? applyDefaults(fields2, row, localization) : row;
|
|
725
|
+
});
|
|
662
726
|
}
|
|
663
727
|
} else if (field.localized && localization && field.defaultValue !== void 0) {
|
|
664
728
|
const map = isPlainObject(value) ? value : {};
|
|
@@ -681,7 +745,7 @@ function fillMissing(fields, data) {
|
|
|
681
745
|
if (field.type === "group") {
|
|
682
746
|
result[field.name] = fillMissing(field.fields, isPlainObject(value) ? value : {});
|
|
683
747
|
} else if (value === void 0) {
|
|
684
|
-
result[field.name] = field
|
|
748
|
+
result[field.name] = hasRows(field) || isHasMany(field) ? [] : null;
|
|
685
749
|
}
|
|
686
750
|
}
|
|
687
751
|
return result;
|
|
@@ -711,10 +775,11 @@ function generateSlugs(fields, data, localization = null) {
|
|
|
711
775
|
}
|
|
712
776
|
} else if (field.type === "group" && isPlainObject(result[field.name])) {
|
|
713
777
|
result[field.name] = generateSlugs(field.fields, result[field.name], localization);
|
|
714
|
-
} else if (field
|
|
715
|
-
result[field.name] = result[field.name].map(
|
|
716
|
-
|
|
717
|
-
|
|
778
|
+
} else if (hasRows(field) && Array.isArray(result[field.name])) {
|
|
779
|
+
result[field.name] = result[field.name].map((row) => {
|
|
780
|
+
const fields2 = rowFields(field, row);
|
|
781
|
+
return fields2 && isPlainObject(row) ? generateSlugs(fields2, row, localization) : row;
|
|
782
|
+
});
|
|
718
783
|
}
|
|
719
784
|
}
|
|
720
785
|
return result;
|
|
@@ -779,9 +844,9 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
779
844
|
const localePath = own ? path : `${path}.${locale}`;
|
|
780
845
|
const failLocale = (message) => errors.push({ field: localePath, message });
|
|
781
846
|
const value2 = map[locale];
|
|
782
|
-
if (isEmpty(value2)) {
|
|
847
|
+
if (isEmpty(value2) || Array.isArray(value2) && value2.length === 0) {
|
|
783
848
|
if (own && field.required && !options.skipRequired) failLocale("is required");
|
|
784
|
-
out[locale] = null;
|
|
849
|
+
out[locale] = hasRows(field) || isHasMany(field) ? [] : null;
|
|
785
850
|
if (own && !field.required) await runCustom(field, null, options, failLocale);
|
|
786
851
|
continue;
|
|
787
852
|
}
|
|
@@ -803,7 +868,7 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
803
868
|
}
|
|
804
869
|
if (isEmpty(raw) || Array.isArray(raw) && raw.length === 0) {
|
|
805
870
|
if (field.required && !options.skipRequired) fail("is required");
|
|
806
|
-
data[field.name] = field
|
|
871
|
+
data[field.name] = hasRows(field) || isHasMany(field) ? [] : null;
|
|
807
872
|
if (!field.required) await runCustom(field, null, options, fail);
|
|
808
873
|
continue;
|
|
809
874
|
}
|
|
@@ -905,10 +970,36 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
|
|
|
905
970
|
}
|
|
906
971
|
return rows;
|
|
907
972
|
}
|
|
973
|
+
case "blocks":
|
|
974
|
+
return normalizeBlocks(field, raw, path, options, fail, errors, references);
|
|
908
975
|
case "group":
|
|
909
976
|
return raw;
|
|
910
977
|
}
|
|
911
978
|
}
|
|
979
|
+
async function normalizeBlocks(field, raw, path, options, fail, errors, references) {
|
|
980
|
+
if (!Array.isArray(raw)) return fail("must be an array of blocks");
|
|
981
|
+
if (field.minRows !== void 0 && raw.length < field.minRows)
|
|
982
|
+
return fail(`must have at least ${field.minRows} blocks`);
|
|
983
|
+
if (field.maxRows !== void 0 && raw.length > field.maxRows)
|
|
984
|
+
return fail(`must have at most ${field.maxRows} blocks`);
|
|
985
|
+
const rows = [];
|
|
986
|
+
for (const [i, row] of raw.entries()) {
|
|
987
|
+
const fields = rowFields(field, row);
|
|
988
|
+
if (!isPlainObject(row) || !fields) {
|
|
989
|
+
errors.push({
|
|
990
|
+
field: `${path}.${i}.blockType`,
|
|
991
|
+
message: `must be one of: ${field.blocks.map((b) => b.slug).join(", ")}`
|
|
992
|
+
});
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
const sub = await validateFields(fields, row, options, `${path}.${i}.`);
|
|
996
|
+
errors.push(...sub.errors);
|
|
997
|
+
references.push(...sub.references);
|
|
998
|
+
const id = typeof row.id === "string" && row.id !== "" ? row.id : randomUUID();
|
|
999
|
+
rows.push({ id, blockType: row.blockType, ...sub.data });
|
|
1000
|
+
}
|
|
1001
|
+
return rows;
|
|
1002
|
+
}
|
|
912
1003
|
function normalizeSelect(field, raw, fail) {
|
|
913
1004
|
const allowed2 = field.options.map((o) => typeof o === "string" ? o : o.value);
|
|
914
1005
|
const values = field.hasMany ? raw : [raw];
|
|
@@ -920,29 +1011,172 @@ function normalizeSelect(field, raw, fail) {
|
|
|
920
1011
|
return field.hasMany ? [...new Set(values)] : values[0];
|
|
921
1012
|
}
|
|
922
1013
|
|
|
923
|
-
// src/fields.ts
|
|
924
|
-
var FIELD_TYPES = [
|
|
925
|
-
"text",
|
|
926
|
-
"textarea",
|
|
927
|
-
"number",
|
|
928
|
-
"boolean",
|
|
929
|
-
"date",
|
|
930
|
-
"email",
|
|
931
|
-
"json",
|
|
932
|
-
"select",
|
|
933
|
-
"slug",
|
|
934
|
-
"richText",
|
|
935
|
-
"upload",
|
|
936
|
-
"relationship",
|
|
937
|
-
"array",
|
|
938
|
-
"group"
|
|
939
|
-
];
|
|
940
|
-
|
|
941
1014
|
// src/load-config.ts
|
|
942
1015
|
import { existsSync } from "fs";
|
|
943
1016
|
import { resolve } from "path";
|
|
944
1017
|
import { createJiti } from "jiti";
|
|
945
1018
|
|
|
1019
|
+
// src/webhooks.ts
|
|
1020
|
+
import { createHmac as createHmac2, randomUUID as randomUUID2 } from "crypto";
|
|
1021
|
+
var WEBHOOK_EVENTS = [
|
|
1022
|
+
"create",
|
|
1023
|
+
"update",
|
|
1024
|
+
"delete",
|
|
1025
|
+
"publish",
|
|
1026
|
+
"unpublish",
|
|
1027
|
+
"draft"
|
|
1028
|
+
];
|
|
1029
|
+
var RETRY_DELAYS = [1e3, 5e3];
|
|
1030
|
+
var QUEUE_DELAYS = [6e4, 5 * 6e4, 30 * 6e4, 2 * 36e5, 6 * 36e5, 12 * 36e5];
|
|
1031
|
+
var CLAIM = 5 * 6e4;
|
|
1032
|
+
var TIMEOUT = 1e4;
|
|
1033
|
+
var Webhooks = class {
|
|
1034
|
+
constructor(hooks, logger, queue, fetcher = (...args) => fetch(...args), retryDelays = RETRY_DELAYS) {
|
|
1035
|
+
this.hooks = hooks;
|
|
1036
|
+
this.logger = logger;
|
|
1037
|
+
this.queue = queue;
|
|
1038
|
+
this.fetcher = fetcher;
|
|
1039
|
+
this.retryDelays = retryDelays;
|
|
1040
|
+
}
|
|
1041
|
+
hooks;
|
|
1042
|
+
logger;
|
|
1043
|
+
queue;
|
|
1044
|
+
fetcher;
|
|
1045
|
+
retryDelays;
|
|
1046
|
+
pending = /* @__PURE__ */ new Set();
|
|
1047
|
+
emit(event, target, doc) {
|
|
1048
|
+
if (this.hooks.length === 0) return;
|
|
1049
|
+
const payload = { event, ...target, doc, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1050
|
+
for (const hook of this.hooks) {
|
|
1051
|
+
if (!matches(hook, payload)) continue;
|
|
1052
|
+
this.track(this.deliver(hook, payload));
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
/** Waits for deliveries in progress, e.g. before a serverless function returns. */
|
|
1056
|
+
async flush() {
|
|
1057
|
+
while (this.pending.size > 0) await Promise.allSettled([...this.pending]);
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Tries queued deliveries whose next attempt is due. Runs with scheduled jobs: every minute
|
|
1061
|
+
* in a server, or from the cron endpoint.
|
|
1062
|
+
*/
|
|
1063
|
+
async retry(now = /* @__PURE__ */ new Date()) {
|
|
1064
|
+
if (!this.queue) return { sent: 0, failed: 0 };
|
|
1065
|
+
let sent = 0;
|
|
1066
|
+
let failed = 0;
|
|
1067
|
+
for (const entry of await this.queue.due(now, 50)) {
|
|
1068
|
+
const { id, ...queued } = entry;
|
|
1069
|
+
const hook = this.hooks.find((h) => h.url === queued.url);
|
|
1070
|
+
if (!hook) {
|
|
1071
|
+
await this.queue.remove(id);
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
const attempts = queued.attempts + 1;
|
|
1075
|
+
await this.queue.update(id, { ...queued, attempts, nextAttemptAt: later(now, CLAIM) });
|
|
1076
|
+
const result = await this.attempt(hook, queued.event, queued.body, queued.delivery);
|
|
1077
|
+
if (result.ok) {
|
|
1078
|
+
await this.queue.remove(id);
|
|
1079
|
+
sent++;
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
const delay = QUEUE_DELAYS[attempts - this.retryDelays.length - 1];
|
|
1083
|
+
if (delay === void 0) failed++;
|
|
1084
|
+
await this.queue.update(id, {
|
|
1085
|
+
...queued,
|
|
1086
|
+
attempts,
|
|
1087
|
+
nextAttemptAt: later(now, delay ?? 0),
|
|
1088
|
+
state: delay === void 0 ? "failed" : "pending",
|
|
1089
|
+
error: result.error
|
|
1090
|
+
});
|
|
1091
|
+
if (delay === void 0) this.giveUp(hook, queued.event, result.error, attempts);
|
|
1092
|
+
}
|
|
1093
|
+
return { sent, failed };
|
|
1094
|
+
}
|
|
1095
|
+
track(delivery) {
|
|
1096
|
+
const tracked = delivery.catch((error) => this.logger.error(`Webhook queue failed: ${error.message}`)).finally(() => this.pending.delete(tracked));
|
|
1097
|
+
this.pending.add(tracked);
|
|
1098
|
+
}
|
|
1099
|
+
async deliver(hook, payload) {
|
|
1100
|
+
const delivery = randomUUID2();
|
|
1101
|
+
const body = JSON.stringify(payload);
|
|
1102
|
+
const { event } = payload;
|
|
1103
|
+
let queued;
|
|
1104
|
+
for (let attempt = 0; ; attempt++) {
|
|
1105
|
+
const result = await this.attempt(hook, event, body, delivery);
|
|
1106
|
+
if (result.ok) {
|
|
1107
|
+
if (queued !== void 0) await this.queue?.remove(queued);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
const entry = (nextAttemptAt) => ({
|
|
1111
|
+
url: hook.url,
|
|
1112
|
+
event,
|
|
1113
|
+
body,
|
|
1114
|
+
delivery,
|
|
1115
|
+
attempts: attempt + 1,
|
|
1116
|
+
nextAttemptAt,
|
|
1117
|
+
state: "pending",
|
|
1118
|
+
error: result.error
|
|
1119
|
+
});
|
|
1120
|
+
const delay = this.retryDelays[attempt];
|
|
1121
|
+
if (delay === void 0) {
|
|
1122
|
+
if (!this.queue) return this.giveUp(hook, event, result.error, attempt + 1);
|
|
1123
|
+
const next = entry(later(/* @__PURE__ */ new Date(), QUEUE_DELAYS[0]));
|
|
1124
|
+
if (queued === void 0) await this.queue.add(next);
|
|
1125
|
+
else await this.queue.update(queued, next);
|
|
1126
|
+
this.logger.warn(
|
|
1127
|
+
`Webhook ${hook.url} failed for ${event} (${result.error}); will retry later`
|
|
1128
|
+
);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (this.queue && queued === void 0) {
|
|
1132
|
+
queued = await this.queue.add(entry(later(/* @__PURE__ */ new Date(), CLAIM)));
|
|
1133
|
+
}
|
|
1134
|
+
await new Promise((resolve3) => setTimeout(resolve3, delay));
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/** One POST. Client errors other than rate limiting count as done: retrying won't help. */
|
|
1138
|
+
async attempt(hook, event, body, delivery) {
|
|
1139
|
+
const headers = {
|
|
1140
|
+
"content-type": "application/json",
|
|
1141
|
+
"user-agent": "easy-cms-webhooks",
|
|
1142
|
+
"x-easy-cms-event": event,
|
|
1143
|
+
"x-easy-cms-delivery": delivery,
|
|
1144
|
+
...hook.headers
|
|
1145
|
+
};
|
|
1146
|
+
if (hook.secret) {
|
|
1147
|
+
headers["x-easy-cms-signature"] = `sha256=${createHmac2("sha256", hook.secret).update(body).digest("hex")}`;
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
const response = await this.fetcher(hook.url, {
|
|
1151
|
+
method: "POST",
|
|
1152
|
+
headers,
|
|
1153
|
+
body,
|
|
1154
|
+
signal: AbortSignal.timeout(TIMEOUT)
|
|
1155
|
+
});
|
|
1156
|
+
if (response.ok || response.status < 500 && response.status !== 429) {
|
|
1157
|
+
if (!response.ok)
|
|
1158
|
+
this.logger.warn(`Webhook ${hook.url} answered ${response.status} to ${event}`);
|
|
1159
|
+
return { ok: true };
|
|
1160
|
+
}
|
|
1161
|
+
return { ok: false, error: `status ${response.status}` };
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
return { ok: false, error: error.message };
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
giveUp(hook, event, error, attempts) {
|
|
1167
|
+
this.logger.error(
|
|
1168
|
+
`Webhook ${hook.url} failed for ${event} after ${attempts} attempts: ${error}`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
var later = (from, ms) => new Date(from.getTime() + ms).toISOString();
|
|
1173
|
+
function matches(hook, payload) {
|
|
1174
|
+
if (hook.events && !hook.events.includes(payload.event)) return false;
|
|
1175
|
+
if (payload.collection !== void 0)
|
|
1176
|
+
return !hook.collections || hook.collections.includes(payload.collection);
|
|
1177
|
+
return !hook.globals || hook.globals.includes(payload.global);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
946
1180
|
// src/validate-config.ts
|
|
947
1181
|
var MIN_SECRET_LENGTH = 32;
|
|
948
1182
|
var BUILTIN_COLLECTIONS = ["users", "media"];
|
|
@@ -952,6 +1186,9 @@ var RESERVED_SLUGS = /* @__PURE__ */ new Set([
|
|
|
952
1186
|
"sessions",
|
|
953
1187
|
"login-attempts",
|
|
954
1188
|
"document-versions",
|
|
1189
|
+
"scheduled-jobs",
|
|
1190
|
+
"webhook-deliveries",
|
|
1191
|
+
"jobs",
|
|
955
1192
|
"migrations",
|
|
956
1193
|
"access"
|
|
957
1194
|
]);
|
|
@@ -974,6 +1211,7 @@ function validateConfig(config) {
|
|
|
974
1211
|
validateAuth(config, add);
|
|
975
1212
|
validateCors(config.cors, add);
|
|
976
1213
|
validateLocalization(config, add);
|
|
1214
|
+
validateWebhooks(config, add);
|
|
977
1215
|
const collections = asArray(config.collections, "collections", add);
|
|
978
1216
|
const globals = asArray(config.globals, "globals", add);
|
|
979
1217
|
const collectionSlugs = /* @__PURE__ */ new Set([...BUILTIN_COLLECTIONS]);
|
|
@@ -1063,7 +1301,9 @@ var LOCALIZABLE = /* @__PURE__ */ new Set([
|
|
|
1063
1301
|
"json",
|
|
1064
1302
|
"select",
|
|
1065
1303
|
"upload",
|
|
1066
|
-
"relationship"
|
|
1304
|
+
"relationship",
|
|
1305
|
+
"blocks",
|
|
1306
|
+
"array"
|
|
1067
1307
|
]);
|
|
1068
1308
|
function validateLocalization(config, add) {
|
|
1069
1309
|
const localization = config.localization;
|
|
@@ -1090,7 +1330,7 @@ function validateLocalization(config, add) {
|
|
|
1090
1330
|
}
|
|
1091
1331
|
}
|
|
1092
1332
|
}
|
|
1093
|
-
const walk = (fields, path) => {
|
|
1333
|
+
const walk = (fields, path, insideLocalized = false) => {
|
|
1094
1334
|
if (!Array.isArray(fields)) return;
|
|
1095
1335
|
for (const field of fields) {
|
|
1096
1336
|
if (typeof field?.name !== "string") continue;
|
|
@@ -1108,18 +1348,52 @@ function validateLocalization(config, add) {
|
|
|
1108
1348
|
`${field.type} fields cannot be localized`,
|
|
1109
1349
|
"localize the fields inside it instead"
|
|
1110
1350
|
);
|
|
1111
|
-
} else if (
|
|
1112
|
-
add(
|
|
1351
|
+
} else if (insideLocalized) {
|
|
1352
|
+
add(
|
|
1353
|
+
`${fieldPath}.localized`,
|
|
1354
|
+
"is inside a localized field, which already holds one value per locale"
|
|
1355
|
+
);
|
|
1113
1356
|
}
|
|
1114
1357
|
}
|
|
1358
|
+
const inside = insideLocalized || field.localized === true;
|
|
1115
1359
|
if (field.type === "group" || field.type === "array")
|
|
1116
|
-
walk(field.fields, `${fieldPath}.fields
|
|
1360
|
+
walk(field.fields, `${fieldPath}.fields`, inside);
|
|
1361
|
+
if (field.type === "blocks")
|
|
1362
|
+
for (const block of field.blocks ?? [])
|
|
1363
|
+
walk(block?.fields, `${fieldPath}.blocks.${block?.slug}`, inside);
|
|
1117
1364
|
}
|
|
1118
1365
|
};
|
|
1119
1366
|
for (const [i, c] of (config.collections ?? []).entries())
|
|
1120
1367
|
walk(c?.fields, `collections[${i}].fields`);
|
|
1121
1368
|
for (const [i, g] of (config.globals ?? []).entries()) walk(g?.fields, `globals[${i}].fields`);
|
|
1122
1369
|
}
|
|
1370
|
+
function validateWebhooks(config, add) {
|
|
1371
|
+
const hooks = config.webhooks;
|
|
1372
|
+
if (hooks === void 0) return;
|
|
1373
|
+
if (!Array.isArray(hooks)) {
|
|
1374
|
+
add("webhooks", "must be a list", "webhooks: [{ url: 'https://example.com/hook' }]");
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
const collections = /* @__PURE__ */ new Set([
|
|
1378
|
+
...BUILTIN_COLLECTIONS,
|
|
1379
|
+
...(config.collections ?? []).map((c) => c?.slug)
|
|
1380
|
+
]);
|
|
1381
|
+
const globals = new Set((config.globals ?? []).map((g) => g?.slug));
|
|
1382
|
+
for (const [i, hook] of hooks.entries()) {
|
|
1383
|
+
const path = `webhooks[${i}]`;
|
|
1384
|
+
if (typeof hook?.url !== "string" || !/^https?:\/\/\S+$/.test(hook.url)) {
|
|
1385
|
+
add(`${path}.url`, "must be an http(s) URL");
|
|
1386
|
+
}
|
|
1387
|
+
for (const event of hook?.events ?? []) {
|
|
1388
|
+
if (!WEBHOOK_EVENTS.includes(event))
|
|
1389
|
+
add(`${path}.events`, `unknown event "${event}"`, `use: ${WEBHOOK_EVENTS.join(", ")}`);
|
|
1390
|
+
}
|
|
1391
|
+
for (const slug of hook?.collections ?? [])
|
|
1392
|
+
if (!collections.has(slug)) add(`${path}.collections`, `unknown collection "${slug}"`);
|
|
1393
|
+
for (const slug of hook?.globals ?? [])
|
|
1394
|
+
if (!globals.has(slug)) add(`${path}.globals`, `unknown global "${slug}"`);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1123
1397
|
function validateCors(cors, add) {
|
|
1124
1398
|
if (cors === void 0 || cors === "*") return;
|
|
1125
1399
|
if (!Array.isArray(cors)) {
|
|
@@ -1187,6 +1461,9 @@ function validateContainer(container, path, collectionSlugs, add) {
|
|
|
1187
1461
|
add(`${path}.versions.max`, "must be a positive integer");
|
|
1188
1462
|
}
|
|
1189
1463
|
}
|
|
1464
|
+
if (container.schedule && !container.drafts) {
|
|
1465
|
+
add(`${path}.schedule`, "needs `drafts: true`", "scheduling publishes and unpublishes drafts");
|
|
1466
|
+
}
|
|
1190
1467
|
if (container.preview !== void 0 && typeof container.preview !== "function") {
|
|
1191
1468
|
add(
|
|
1192
1469
|
`${path}.preview`,
|
|
@@ -1271,6 +1548,43 @@ function validateField(field, path, siblings, collectionSlugs, add) {
|
|
|
1271
1548
|
checkRange(field.minRows, field.maxRows, "minRows", "maxRows", path, add);
|
|
1272
1549
|
validateFields2(field.fields, `${path}.fields`, /* @__PURE__ */ new Set(["id"]), collectionSlugs, add);
|
|
1273
1550
|
break;
|
|
1551
|
+
case "blocks": {
|
|
1552
|
+
if (!Array.isArray(field.blocks) || field.blocks.length === 0) {
|
|
1553
|
+
add(
|
|
1554
|
+
`${path}.blocks`,
|
|
1555
|
+
"must contain at least one block",
|
|
1556
|
+
"blocks: [{ slug: 'hero', fields: [...] }]"
|
|
1557
|
+
);
|
|
1558
|
+
break;
|
|
1559
|
+
}
|
|
1560
|
+
checkRange(field.minRows, field.maxRows, "minRows", "maxRows", path, add);
|
|
1561
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1562
|
+
for (const [i, block] of field.blocks.entries()) {
|
|
1563
|
+
const blockPath = `${path}.blocks[${i}]`;
|
|
1564
|
+
if (typeof block?.slug !== "string" || !SLUG_PATTERN.test(block.slug)) {
|
|
1565
|
+
add(
|
|
1566
|
+
`${blockPath}.slug`,
|
|
1567
|
+
'must be lowercase letters, digits, "-" or "_", starting with a letter'
|
|
1568
|
+
);
|
|
1569
|
+
} else if (seen.has(block.slug)) {
|
|
1570
|
+
add(`${blockPath}.slug`, `duplicate block "${block.slug}"`);
|
|
1571
|
+
} else {
|
|
1572
|
+
seen.add(block.slug);
|
|
1573
|
+
}
|
|
1574
|
+
if (!Array.isArray(block?.fields)) {
|
|
1575
|
+
add(`${blockPath}.fields`, "must be an array");
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
validateFields2(
|
|
1579
|
+
block.fields,
|
|
1580
|
+
`${blockPath}.fields`,
|
|
1581
|
+
/* @__PURE__ */ new Set(["id", "blockType"]),
|
|
1582
|
+
collectionSlugs,
|
|
1583
|
+
add
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
break;
|
|
1587
|
+
}
|
|
1274
1588
|
}
|
|
1275
1589
|
}
|
|
1276
1590
|
function checkRange(min, max, minName, maxName, path, add) {
|
|
@@ -1362,7 +1676,9 @@ async function resolveConfig(input) {
|
|
|
1362
1676
|
collections: [
|
|
1363
1677
|
...config.collections ?? [],
|
|
1364
1678
|
...internalCollections,
|
|
1365
|
-
...usesVersions(config) ? [versionsCollection] : []
|
|
1679
|
+
...usesVersions(config) ? [versionsCollection] : [],
|
|
1680
|
+
...usesSchedule(config) ? [scheduledJobsCollection] : [],
|
|
1681
|
+
...(config.webhooks?.length ?? 0) > 0 ? [webhookDeliveriesCollection] : []
|
|
1366
1682
|
],
|
|
1367
1683
|
globals: config.globals ?? []
|
|
1368
1684
|
};
|
|
@@ -1372,6 +1688,9 @@ async function resolveConfig(input) {
|
|
|
1372
1688
|
function usesVersions(config) {
|
|
1373
1689
|
return [...config.collections ?? [], ...config.globals ?? []].some((c) => c.versions);
|
|
1374
1690
|
}
|
|
1691
|
+
function usesSchedule(config) {
|
|
1692
|
+
return [...config.collections ?? [], ...config.globals ?? []].some((c) => c.schedule);
|
|
1693
|
+
}
|
|
1375
1694
|
|
|
1376
1695
|
// src/load-config.ts
|
|
1377
1696
|
var CONFIG_FILE_NAMES = [
|
|
@@ -1430,16 +1749,20 @@ function pickLocale(fields, data, locale, localization, fallback) {
|
|
|
1430
1749
|
const result = { ...data };
|
|
1431
1750
|
for (const field of fields) {
|
|
1432
1751
|
const value = result[field.name];
|
|
1433
|
-
if (field.
|
|
1434
|
-
result[field.name] = pickLocale(field.fields, value, locale, localization, fallback);
|
|
1435
|
-
} else if (field.type === "array" && Array.isArray(value)) {
|
|
1436
|
-
result[field.name] = value.map(
|
|
1437
|
-
(row) => isPlainObject2(row) ? pickLocale(field.fields, row, locale, localization, fallback) : row
|
|
1438
|
-
);
|
|
1439
|
-
} else if (field.localized) {
|
|
1752
|
+
if (field.localized) {
|
|
1440
1753
|
const map = isPlainObject2(value) ? value : {};
|
|
1441
1754
|
const own = map[locale];
|
|
1442
|
-
|
|
1755
|
+
const blank = isEmpty2(own) || Array.isArray(own) && own.length === 0;
|
|
1756
|
+
const picked = blank && fallback ? map[localization.defaultLocale] ?? null : own ?? null;
|
|
1757
|
+
const list = hasRows(field) || "hasMany" in field && field.hasMany === true;
|
|
1758
|
+
result[field.name] = picked === null && list ? [] : picked;
|
|
1759
|
+
} else if (field.type === "group" && isPlainObject2(value)) {
|
|
1760
|
+
result[field.name] = pickLocale(field.fields, value, locale, localization, fallback);
|
|
1761
|
+
} else if (hasRows(field) && Array.isArray(value)) {
|
|
1762
|
+
result[field.name] = value.map((row) => {
|
|
1763
|
+
const fields2 = rowFields(field, row);
|
|
1764
|
+
return fields2 && isPlainObject2(row) ? pickLocale(fields2, row, locale, localization, fallback) : row;
|
|
1765
|
+
});
|
|
1443
1766
|
}
|
|
1444
1767
|
}
|
|
1445
1768
|
return result;
|
|
@@ -1450,57 +1773,66 @@ function toLocaleMaps(fields, input, current, locale) {
|
|
|
1450
1773
|
if (!Object.hasOwn(input, field.name)) continue;
|
|
1451
1774
|
const value = input[field.name];
|
|
1452
1775
|
const previous = current[field.name];
|
|
1453
|
-
if (field.
|
|
1776
|
+
if (field.localized) {
|
|
1777
|
+
const map = isPlainObject2(previous) ? previous : {};
|
|
1778
|
+
result[field.name] = { ...map, [locale]: value };
|
|
1779
|
+
} else if (field.type === "group" && isPlainObject2(value)) {
|
|
1454
1780
|
result[field.name] = toLocaleMaps(
|
|
1455
1781
|
field.fields,
|
|
1456
1782
|
value,
|
|
1457
1783
|
isPlainObject2(previous) ? previous : {},
|
|
1458
1784
|
locale
|
|
1459
1785
|
);
|
|
1460
|
-
} else if (field
|
|
1786
|
+
} else if (hasRows(field) && Array.isArray(value)) {
|
|
1461
1787
|
const rows = new Map(
|
|
1462
1788
|
(Array.isArray(previous) ? previous : []).filter(isPlainObject2).map((row) => [String(row.id), row])
|
|
1463
1789
|
);
|
|
1464
|
-
result[field.name] = value.map(
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
const map = isPlainObject2(previous) ? previous : {};
|
|
1469
|
-
result[field.name] = { ...map, [locale]: value };
|
|
1790
|
+
result[field.name] = value.map((row) => {
|
|
1791
|
+
const fields2 = rowFields(field, row);
|
|
1792
|
+
return fields2 && isPlainObject2(row) ? toLocaleMaps(fields2, row, rows.get(String(row.id)) ?? {}, locale) : row;
|
|
1793
|
+
});
|
|
1470
1794
|
}
|
|
1471
1795
|
}
|
|
1472
1796
|
return result;
|
|
1473
1797
|
}
|
|
1474
|
-
function localizePath(fields, path, locale) {
|
|
1798
|
+
function localizePath(fields, path, locale, locales = []) {
|
|
1475
1799
|
const segments = path.split(".");
|
|
1476
1800
|
let list = fields;
|
|
1477
1801
|
for (let i = 0; i < segments.length; i++) {
|
|
1478
1802
|
const field = list.find((f) => f.name === segments[i]);
|
|
1479
1803
|
if (!field) return path;
|
|
1804
|
+
if (field.localized) {
|
|
1805
|
+
const next = segments[i + 1];
|
|
1806
|
+
if (next !== void 0 && locales.includes(next)) return path;
|
|
1807
|
+
return [...segments.slice(0, i + 1), locale, ...segments.slice(i + 1)].join(".");
|
|
1808
|
+
}
|
|
1480
1809
|
if (field.type === "group" || field.type === "array") {
|
|
1481
1810
|
list = field.fields;
|
|
1482
1811
|
continue;
|
|
1483
1812
|
}
|
|
1484
|
-
if (field.
|
|
1813
|
+
if (field.type === "blocks") {
|
|
1814
|
+
list = field.blocks.flatMap((block) => block.fields);
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1485
1817
|
return path;
|
|
1486
1818
|
}
|
|
1487
1819
|
return path;
|
|
1488
1820
|
}
|
|
1489
|
-
function localizeWhere(fields, where, locale) {
|
|
1821
|
+
function localizeWhere(fields, where, locale, locales = []) {
|
|
1490
1822
|
if (!where) return where;
|
|
1491
1823
|
const out = {};
|
|
1492
1824
|
for (const [key2, value] of Object.entries(where)) {
|
|
1493
1825
|
if ((key2 === "and" || key2 === "or") && Array.isArray(value)) {
|
|
1494
|
-
out[key2] = value.map((w) => localizeWhere(fields, w, locale));
|
|
1826
|
+
out[key2] = value.map((w) => localizeWhere(fields, w, locale, locales));
|
|
1495
1827
|
} else {
|
|
1496
|
-
out[localizePath(fields, key2, locale)] = value;
|
|
1828
|
+
out[localizePath(fields, key2, locale, locales)] = value;
|
|
1497
1829
|
}
|
|
1498
1830
|
}
|
|
1499
1831
|
return out;
|
|
1500
1832
|
}
|
|
1501
|
-
function localizeSort(fields, sort, locale) {
|
|
1833
|
+
function localizeSort(fields, sort, locale, locales = []) {
|
|
1502
1834
|
return sort.map(
|
|
1503
|
-
(entry) => entry.startsWith("-") ? `-${localizePath(fields, entry.slice(1), locale)}` : localizePath(fields, entry, locale)
|
|
1835
|
+
(entry) => entry.startsWith("-") ? `-${localizePath(fields, entry.slice(1), locale, locales)}` : localizePath(fields, entry, locale, locales)
|
|
1504
1836
|
);
|
|
1505
1837
|
}
|
|
1506
1838
|
|
|
@@ -1644,10 +1976,16 @@ function collectSlots(fields, data, bySlug, slots) {
|
|
|
1644
1976
|
if (value === null || value === void 0) continue;
|
|
1645
1977
|
if (field.type === "group") {
|
|
1646
1978
|
if (typeof value === "object") collectSlots(field.fields, value, bySlug, slots);
|
|
1647
|
-
} else if (field
|
|
1648
|
-
if (Array.isArray(value))
|
|
1649
|
-
for (const row of value)
|
|
1979
|
+
} else if (hasRows(field)) {
|
|
1980
|
+
if (Array.isArray(value)) {
|
|
1981
|
+
for (const row of value) {
|
|
1982
|
+
const fields2 = rowFields(field, row);
|
|
1983
|
+
if (fields2 && row && typeof row === "object")
|
|
1984
|
+
collectSlots(fields2, row, bySlug, slots);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1650
1987
|
} else if (field.type === "relationship" || field.type === "upload") {
|
|
1988
|
+
if (typeof value === "object" && !Array.isArray(value)) continue;
|
|
1651
1989
|
const collection2 = field.type === "upload" ? "media" : field.to;
|
|
1652
1990
|
if (!bySlug.has(collection2)) continue;
|
|
1653
1991
|
if (Array.isArray(value)) {
|
|
@@ -1862,8 +2200,19 @@ async function createEasyCMS(config, options = {}) {
|
|
|
1862
2200
|
logger,
|
|
1863
2201
|
interactive: options.interactive ?? false
|
|
1864
2202
|
});
|
|
1865
|
-
|
|
2203
|
+
const cms = new EasyCMS(resolved2, db, logger, storage);
|
|
2204
|
+
const scheduling = [...resolved2.collections, ...resolved2.globals].some((c) => c.schedule) || (resolved2.webhooks?.length ?? 0) > 0;
|
|
2205
|
+
if (scheduling && options.scheduler !== false) cms.startScheduler();
|
|
2206
|
+
return cms;
|
|
1866
2207
|
}
|
|
2208
|
+
var toJob = (row) => ({
|
|
2209
|
+
id: row.id,
|
|
2210
|
+
action: row.action,
|
|
2211
|
+
runAt: String(row.runAt),
|
|
2212
|
+
state: row.state,
|
|
2213
|
+
error: row.error ?? null,
|
|
2214
|
+
author: row.author ?? null
|
|
2215
|
+
});
|
|
1867
2216
|
var EasyCMS = class {
|
|
1868
2217
|
config;
|
|
1869
2218
|
db;
|
|
@@ -1873,12 +2222,18 @@ var EasyCMS = class {
|
|
|
1873
2222
|
/** Where uploaded files are stored. */
|
|
1874
2223
|
storage;
|
|
1875
2224
|
versions;
|
|
2225
|
+
webhooks;
|
|
1876
2226
|
constructor(config, db, logger = consoleLogger, storage = localStorage({ dir: config.upload.dir })) {
|
|
1877
2227
|
this.config = config;
|
|
1878
2228
|
this.db = db;
|
|
1879
2229
|
this.logger = logger;
|
|
1880
2230
|
this.storage = storage;
|
|
1881
2231
|
this.versions = new VersionStore(db);
|
|
2232
|
+
this.webhooks = new Webhooks(
|
|
2233
|
+
config.webhooks ?? [],
|
|
2234
|
+
logger,
|
|
2235
|
+
config.collections.some((c) => c.slug === WEBHOOK_DELIVERIES) ? this.webhookQueue() : void 0
|
|
2236
|
+
);
|
|
1882
2237
|
this.auth = new Auth(this);
|
|
1883
2238
|
}
|
|
1884
2239
|
async find(collection2, options = {}) {
|
|
@@ -1899,7 +2254,8 @@ var EasyCMS = class {
|
|
|
1899
2254
|
const sort = locale === void 0 ? requested : localizeSort(
|
|
1900
2255
|
config.fields,
|
|
1901
2256
|
requested,
|
|
1902
|
-
locale === ALL_LOCALES ? this.config.localization?.defaultLocale ?? locale : locale
|
|
2257
|
+
locale === ALL_LOCALES ? this.config.localization?.defaultLocale ?? locale : locale,
|
|
2258
|
+
this.config.localization?.locales ?? []
|
|
1903
2259
|
);
|
|
1904
2260
|
const result = await this.db.find({ collection: collection2, where, sort, limit, page });
|
|
1905
2261
|
const found = options.draft ? await this.withDrafts(config, result.docs) : result.docs;
|
|
@@ -2268,6 +2624,7 @@ var EasyCMS = class {
|
|
|
2268
2624
|
previousDoc: current,
|
|
2269
2625
|
operation: "update"
|
|
2270
2626
|
});
|
|
2627
|
+
this.emit(config, "draft", draft);
|
|
2271
2628
|
const [out2] = await this.output(config, [draft], guard, {
|
|
2272
2629
|
...options,
|
|
2273
2630
|
draft: true
|
|
@@ -2289,6 +2646,7 @@ var EasyCMS = class {
|
|
|
2289
2646
|
previousDoc: current,
|
|
2290
2647
|
operation: "update"
|
|
2291
2648
|
});
|
|
2649
|
+
this.emit(config, "update", doc, existing.status);
|
|
2292
2650
|
const [out] = await this.output(config, [doc], guard, { ...options, draft: true });
|
|
2293
2651
|
return out;
|
|
2294
2652
|
}
|
|
@@ -2306,11 +2664,16 @@ var EasyCMS = class {
|
|
|
2306
2664
|
if (config.slug === USERS) await this.auth.revokeSessions(parsed);
|
|
2307
2665
|
await this.db.delete({ collection: collection2, id: parsed });
|
|
2308
2666
|
if (versionLimit(config)) await this.versions.deleteAll(collectionParent(collection2), parsed);
|
|
2667
|
+
if (config.schedule) {
|
|
2668
|
+
for (const job of await this.pendingJobs(collection2, parsed))
|
|
2669
|
+
await this.db.delete({ collection: SCHEDULED_JOBS, id: job.id });
|
|
2670
|
+
}
|
|
2309
2671
|
await this.notify(config.hooks?.afterDelete, "afterDelete", config.slug, {
|
|
2310
2672
|
...base,
|
|
2311
2673
|
id: parsed,
|
|
2312
2674
|
doc: existing
|
|
2313
2675
|
});
|
|
2676
|
+
this.emit(config, "delete", existing);
|
|
2314
2677
|
const [out] = await this.output(config, [existing], guard, { depth: 0, draft: true });
|
|
2315
2678
|
return out;
|
|
2316
2679
|
}
|
|
@@ -2437,6 +2800,7 @@ var EasyCMS = class {
|
|
|
2437
2800
|
previousDoc: current,
|
|
2438
2801
|
operation: "update"
|
|
2439
2802
|
});
|
|
2803
|
+
this.emit(config, "draft", draft);
|
|
2440
2804
|
return this.findGlobal(slug, { ...options, draft: true });
|
|
2441
2805
|
}
|
|
2442
2806
|
const doc = await this.db.updateGlobal({ slug, data: { ...prepared, updatedAt: now } });
|
|
@@ -2447,6 +2811,7 @@ var EasyCMS = class {
|
|
|
2447
2811
|
previousDoc: current,
|
|
2448
2812
|
operation: "update"
|
|
2449
2813
|
});
|
|
2814
|
+
this.emit(config, "update", doc, existing.status);
|
|
2450
2815
|
return this.findGlobal(slug, { ...options, draft: true });
|
|
2451
2816
|
}
|
|
2452
2817
|
/**
|
|
@@ -2468,9 +2833,226 @@ var EasyCMS = class {
|
|
|
2468
2833
|
return { update: await check("update"), delete: await check("delete") };
|
|
2469
2834
|
}
|
|
2470
2835
|
/** Closes the database connection. */
|
|
2836
|
+
/**
|
|
2837
|
+
* Waits until webhook deliveries in progress are done. Call it before a serverless function
|
|
2838
|
+
* returns, which may otherwise stop them.
|
|
2839
|
+
*/
|
|
2840
|
+
flushWebhooks() {
|
|
2841
|
+
return this.webhooks.flush();
|
|
2842
|
+
}
|
|
2471
2843
|
async destroy() {
|
|
2844
|
+
if (this.schedulerTimer) clearInterval(this.schedulerTimer);
|
|
2845
|
+
await this.running;
|
|
2846
|
+
await this.webhooks.flush();
|
|
2472
2847
|
await this.db.destroy();
|
|
2473
2848
|
}
|
|
2849
|
+
// --- Scheduled publishing --------------------------------------------------
|
|
2850
|
+
schedulerTimer;
|
|
2851
|
+
running = Promise.resolve();
|
|
2852
|
+
/** Runs `runJobs` every `interval` ms in this process (`createEasyCMS` does this by default). */
|
|
2853
|
+
startScheduler(interval = 6e4) {
|
|
2854
|
+
if (this.schedulerTimer) return;
|
|
2855
|
+
this.schedulerTimer = setInterval(() => {
|
|
2856
|
+
this.running = this.running.then(
|
|
2857
|
+
() => this.runJobs().catch(
|
|
2858
|
+
(error) => this.logger.error(`Scheduled jobs failed: ${error.message}`)
|
|
2859
|
+
)
|
|
2860
|
+
);
|
|
2861
|
+
}, interval);
|
|
2862
|
+
this.schedulerTimer.unref?.();
|
|
2863
|
+
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Runs due scheduled publishes and unpublishes, and retries failed webhook deliveries. What the
|
|
2866
|
+
* scheduler and the cron endpoint (`GET <api>/jobs/run`) call.
|
|
2867
|
+
*/
|
|
2868
|
+
async runJobs(now = /* @__PURE__ */ new Date()) {
|
|
2869
|
+
const scheduled = await this.runScheduled(now);
|
|
2870
|
+
return { ...scheduled, webhooks: await this.retryWebhooks(now) };
|
|
2871
|
+
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Tries webhook deliveries that failed earlier and are due for another attempt. Failed
|
|
2874
|
+
* deliveries are kept in the database, so they survive restarts.
|
|
2875
|
+
*/
|
|
2876
|
+
retryWebhooks(now = /* @__PURE__ */ new Date()) {
|
|
2877
|
+
return this.webhooks.retry(now);
|
|
2878
|
+
}
|
|
2879
|
+
/** Failed webhook deliveries, stored in `webhook-deliveries`. */
|
|
2880
|
+
webhookQueue() {
|
|
2881
|
+
const collection2 = WEBHOOK_DELIVERIES;
|
|
2882
|
+
const toRow = (d, createdAt) => {
|
|
2883
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2884
|
+
return { ...d, createdAt: createdAt ?? now, updatedAt: now };
|
|
2885
|
+
};
|
|
2886
|
+
return {
|
|
2887
|
+
add: async (delivery) => (await this.db.create({ collection: collection2, data: toRow(delivery) })).id,
|
|
2888
|
+
update: async (id, delivery) => {
|
|
2889
|
+
const current = await this.db.findById({ collection: collection2, id });
|
|
2890
|
+
await this.db.update({
|
|
2891
|
+
collection: collection2,
|
|
2892
|
+
id,
|
|
2893
|
+
data: toRow(delivery, current?.createdAt)
|
|
2894
|
+
});
|
|
2895
|
+
},
|
|
2896
|
+
remove: async (id) => {
|
|
2897
|
+
await this.db.delete({ collection: collection2, id });
|
|
2898
|
+
},
|
|
2899
|
+
due: async (now, limit) => {
|
|
2900
|
+
const { docs } = await this.db.find({
|
|
2901
|
+
collection: collection2,
|
|
2902
|
+
where: {
|
|
2903
|
+
and: [{ state: { equals: "pending" } }, { nextAttemptAt: { lte: now.toISOString() } }]
|
|
2904
|
+
},
|
|
2905
|
+
sort: ["nextAttemptAt"],
|
|
2906
|
+
limit,
|
|
2907
|
+
page: 1
|
|
2908
|
+
});
|
|
2909
|
+
return docs;
|
|
2910
|
+
}
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2914
|
+
* Publishes or unpublishes documents whose scheduled time has come. Safe to call often, e.g.
|
|
2915
|
+
* from a cron job (`GET <api>/jobs/run`) where no process keeps running.
|
|
2916
|
+
*/
|
|
2917
|
+
async runScheduled(now = /* @__PURE__ */ new Date()) {
|
|
2918
|
+
if (!this.config.collections.some((c) => c.slug === SCHEDULED_JOBS))
|
|
2919
|
+
return { ran: 0, failed: 0 };
|
|
2920
|
+
const due = await this.db.find({
|
|
2921
|
+
collection: SCHEDULED_JOBS,
|
|
2922
|
+
where: {
|
|
2923
|
+
and: [{ state: { equals: "pending" } }, { runAt: { lte: now.toISOString() } }]
|
|
2924
|
+
},
|
|
2925
|
+
sort: ["runAt"],
|
|
2926
|
+
limit: 100,
|
|
2927
|
+
page: 1
|
|
2928
|
+
});
|
|
2929
|
+
let failed = 0;
|
|
2930
|
+
for (const row of due.docs) {
|
|
2931
|
+
const parent = String(row.parent);
|
|
2932
|
+
const action = row.action;
|
|
2933
|
+
try {
|
|
2934
|
+
if (parent.startsWith("global:")) {
|
|
2935
|
+
const slug = parent.slice("global:".length);
|
|
2936
|
+
if (action === "publish") await this.saveGlobal(slug, { status: "published" }, {});
|
|
2937
|
+
else await this.saveGlobal(slug, { status: "draft" }, {}, "unpublish");
|
|
2938
|
+
} else if (action === "publish") {
|
|
2939
|
+
await this.updateDocument(parent, row.doc, { status: "published" }, { depth: 0 });
|
|
2940
|
+
} else {
|
|
2941
|
+
await this.updateDocument(
|
|
2942
|
+
parent,
|
|
2943
|
+
row.doc,
|
|
2944
|
+
{ status: "draft" },
|
|
2945
|
+
{ depth: 0 },
|
|
2946
|
+
"unpublish"
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2949
|
+
await this.db.update({
|
|
2950
|
+
collection: SCHEDULED_JOBS,
|
|
2951
|
+
id: row.id,
|
|
2952
|
+
data: { ...row, state: "done" }
|
|
2953
|
+
});
|
|
2954
|
+
} catch (error) {
|
|
2955
|
+
failed++;
|
|
2956
|
+
const message = error.message;
|
|
2957
|
+
this.logger.error(`Scheduled ${action} of ${parent} ${row.doc} failed: ${message}`);
|
|
2958
|
+
await this.db.update({
|
|
2959
|
+
collection: SCHEDULED_JOBS,
|
|
2960
|
+
id: row.id,
|
|
2961
|
+
data: { ...row, state: "failed", error: message.slice(0, 500) }
|
|
2962
|
+
});
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
return { ran: due.docs.length - failed, failed };
|
|
2966
|
+
}
|
|
2967
|
+
/** Schedules a publish or unpublish, replacing a pending job with the same action. */
|
|
2968
|
+
async schedule(collection2, id, job, options = {}) {
|
|
2969
|
+
const { parsed } = await this.scheduleTarget(collection2, id, options);
|
|
2970
|
+
return this.addJob(collection2, parsed, job, options);
|
|
2971
|
+
}
|
|
2972
|
+
/** Pending jobs of a document, soonest first. */
|
|
2973
|
+
async scheduled(collection2, id, options = {}) {
|
|
2974
|
+
const { parsed } = await this.scheduleTarget(collection2, id, options);
|
|
2975
|
+
return this.pendingJobs(collection2, parsed);
|
|
2976
|
+
}
|
|
2977
|
+
async cancelSchedule(collection2, id, jobId, options = {}) {
|
|
2978
|
+
const { parsed } = await this.scheduleTarget(collection2, id, options);
|
|
2979
|
+
await this.removeJob(collection2, parsed, jobId);
|
|
2980
|
+
}
|
|
2981
|
+
async scheduleGlobal(slug, job, options = {}) {
|
|
2982
|
+
await this.globalScheduleTarget(slug, options);
|
|
2983
|
+
return this.addJob(`global:${slug}`, 0, job, options);
|
|
2984
|
+
}
|
|
2985
|
+
async scheduledGlobal(slug, options = {}) {
|
|
2986
|
+
await this.globalScheduleTarget(slug, options);
|
|
2987
|
+
return this.pendingJobs(`global:${slug}`, 0);
|
|
2988
|
+
}
|
|
2989
|
+
async cancelGlobalSchedule(slug, jobId, options = {}) {
|
|
2990
|
+
await this.globalScheduleTarget(slug, options);
|
|
2991
|
+
await this.removeJob(`global:${slug}`, 0, jobId);
|
|
2992
|
+
}
|
|
2993
|
+
async scheduleTarget(collection2, id, options) {
|
|
2994
|
+
const config = this.collection(collection2);
|
|
2995
|
+
if (!config.schedule) throw new QueryError(`"${collection2}" has no schedule`);
|
|
2996
|
+
const parsed = parseId(id);
|
|
2997
|
+
const existing = parsed === void 0 ? null : await this.db.findById({ collection: collection2, id: parsed });
|
|
2998
|
+
if (!existing || parsed === void 0) throw new NotFoundError(collection2, id);
|
|
2999
|
+
await this.checkDocumentAccess(config, "update", guardOf(options), parsed, void 0);
|
|
3000
|
+
return { config, parsed };
|
|
3001
|
+
}
|
|
3002
|
+
async globalScheduleTarget(slug, options) {
|
|
3003
|
+
const config = this.global(slug);
|
|
3004
|
+
if (!config.schedule) throw new QueryError(`"${slug}" has no schedule`);
|
|
3005
|
+
await this.checkGlobalAccess(config, "update", guardOf(options));
|
|
3006
|
+
}
|
|
3007
|
+
async addJob(parent, doc, job, options) {
|
|
3008
|
+
const runAt = new Date(job.at);
|
|
3009
|
+
const errors = [];
|
|
3010
|
+
if (job.action !== "publish" && job.action !== "unpublish")
|
|
3011
|
+
errors.push({ field: "action", message: 'must be "publish" or "unpublish"' });
|
|
3012
|
+
if (Number.isNaN(runAt.getTime())) errors.push({ field: "at", message: "must be a valid date" });
|
|
3013
|
+
if (errors.length > 0) throw new ValidationError(SCHEDULED_JOBS, errors);
|
|
3014
|
+
for (const pending of await this.pendingJobs(parent, doc)) {
|
|
3015
|
+
if (pending.action === job.action)
|
|
3016
|
+
await this.db.delete({ collection: SCHEDULED_JOBS, id: pending.id });
|
|
3017
|
+
}
|
|
3018
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3019
|
+
const row = await this.db.create({
|
|
3020
|
+
collection: SCHEDULED_JOBS,
|
|
3021
|
+
data: {
|
|
3022
|
+
parent,
|
|
3023
|
+
doc,
|
|
3024
|
+
action: job.action,
|
|
3025
|
+
runAt: runAt.toISOString(),
|
|
3026
|
+
state: "pending",
|
|
3027
|
+
error: null,
|
|
3028
|
+
author: guardOf(options).user?.id ?? null,
|
|
3029
|
+
createdAt: now,
|
|
3030
|
+
updatedAt: now
|
|
3031
|
+
}
|
|
3032
|
+
});
|
|
3033
|
+
return toJob(row);
|
|
3034
|
+
}
|
|
3035
|
+
async pendingJobs(parent, doc) {
|
|
3036
|
+
const rows = await this.db.find({
|
|
3037
|
+
collection: SCHEDULED_JOBS,
|
|
3038
|
+
where: {
|
|
3039
|
+
and: [
|
|
3040
|
+
{ parent: { equals: parent } },
|
|
3041
|
+
{ doc: { equals: doc } },
|
|
3042
|
+
{ state: { equals: "pending" } }
|
|
3043
|
+
]
|
|
3044
|
+
},
|
|
3045
|
+
sort: ["runAt"],
|
|
3046
|
+
limit: 0,
|
|
3047
|
+
page: 1
|
|
3048
|
+
});
|
|
3049
|
+
return rows.docs.map(toJob);
|
|
3050
|
+
}
|
|
3051
|
+
async removeJob(parent, doc, jobId) {
|
|
3052
|
+
const job = (await this.pendingJobs(parent, doc)).find((j) => String(j.id) === String(jobId));
|
|
3053
|
+
if (!job) throw new NotFoundError("scheduled job", jobId);
|
|
3054
|
+
await this.db.delete({ collection: SCHEDULED_JOBS, id: job.id });
|
|
3055
|
+
}
|
|
2474
3056
|
// -------------------------------------------------------------------------
|
|
2475
3057
|
/** @internal */
|
|
2476
3058
|
collection(slug) {
|
|
@@ -2499,11 +3081,11 @@ var EasyCMS = class {
|
|
|
2499
3081
|
});
|
|
2500
3082
|
if (access === true) return;
|
|
2501
3083
|
if (access !== false) {
|
|
2502
|
-
const
|
|
3084
|
+
const matches2 = await this.db.count({
|
|
2503
3085
|
collection: config.slug,
|
|
2504
3086
|
where: andWhere(access, { id: { equals: id } })
|
|
2505
3087
|
});
|
|
2506
|
-
if (
|
|
3088
|
+
if (matches2 > 0) return;
|
|
2507
3089
|
}
|
|
2508
3090
|
throw deny(guard.user);
|
|
2509
3091
|
}
|
|
@@ -2613,6 +3195,7 @@ var EasyCMS = class {
|
|
|
2613
3195
|
doc,
|
|
2614
3196
|
operation: "create"
|
|
2615
3197
|
});
|
|
3198
|
+
this.emit(config, "create", doc);
|
|
2616
3199
|
const [out] = await this.output(config, [doc], hookGuard ?? guard, { ...options, draft: true });
|
|
2617
3200
|
return out;
|
|
2618
3201
|
}
|
|
@@ -2650,7 +3233,8 @@ var EasyCMS = class {
|
|
|
2650
3233
|
return localizeWhere(
|
|
2651
3234
|
config.fields,
|
|
2652
3235
|
where,
|
|
2653
|
-
locale === ALL_LOCALES ? localization.defaultLocale : locale
|
|
3236
|
+
locale === ALL_LOCALES ? localization.defaultLocale : locale,
|
|
3237
|
+
localization.locales
|
|
2654
3238
|
);
|
|
2655
3239
|
}
|
|
2656
3240
|
/** Drafts are kept as versions (instead of unpublishing) when both are enabled. */
|
|
@@ -2667,6 +3251,24 @@ var EasyCMS = class {
|
|
|
2667
3251
|
return version && doc.status === "published" && version.status === "draft" ? draftOf(doc, version) : doc;
|
|
2668
3252
|
});
|
|
2669
3253
|
}
|
|
3254
|
+
/** Sends webhook events for a change; `before` is the stored status before it, if any. */
|
|
3255
|
+
emit(config, event, doc, before) {
|
|
3256
|
+
const isGlobal = !this.config.collections.includes(config);
|
|
3257
|
+
if (!isGlobal && INTERNAL_COLLECTIONS.has(config.slug)) return;
|
|
3258
|
+
const body = isGlobal ? { ...snapshotOf(config.fields, doc), updatedAt: doc.updatedAt } : {
|
|
3259
|
+
id: doc.id,
|
|
3260
|
+
...snapshotOf(config.fields, doc),
|
|
3261
|
+
createdAt: doc.createdAt,
|
|
3262
|
+
updatedAt: doc.updatedAt
|
|
3263
|
+
};
|
|
3264
|
+
const target = isGlobal ? { global: config.slug } : { collection: config.slug, id: doc.id };
|
|
3265
|
+
this.webhooks.emit(event, target, body);
|
|
3266
|
+
if (event === "draft" || event === "delete" || !config.drafts) return;
|
|
3267
|
+
const after = doc.status;
|
|
3268
|
+
if (after === "published" && before !== "published") this.webhooks.emit("publish", target, body);
|
|
3269
|
+
if (after !== "published" && before === "published")
|
|
3270
|
+
this.webhooks.emit("unpublish", target, body);
|
|
3271
|
+
}
|
|
2670
3272
|
/** Records a version after a save, when the collection or global keeps versions. */
|
|
2671
3273
|
async saveVersion(config, parent, id, doc, guard) {
|
|
2672
3274
|
const max = versionLimit(config);
|
|
@@ -2951,6 +3553,17 @@ async function serializeFields(fields, update, localized) {
|
|
|
2951
3553
|
if (field.maxRows !== void 0) f.maxRows = field.maxRows;
|
|
2952
3554
|
f.fields = await serializeFields(field.fields, update, localized);
|
|
2953
3555
|
break;
|
|
3556
|
+
case "blocks":
|
|
3557
|
+
if (field.minRows !== void 0) f.minRows = field.minRows;
|
|
3558
|
+
if (field.maxRows !== void 0) f.maxRows = field.maxRows;
|
|
3559
|
+
f.blocks = await Promise.all(
|
|
3560
|
+
field.blocks.map(async (block) => ({
|
|
3561
|
+
slug: block.slug,
|
|
3562
|
+
...block.labels ? { labels: block.labels } : {},
|
|
3563
|
+
fields: await serializeFields(block.fields, update, localized)
|
|
3564
|
+
}))
|
|
3565
|
+
);
|
|
3566
|
+
break;
|
|
2954
3567
|
case "group":
|
|
2955
3568
|
f.fields = await serializeFields(field.fields, update, localized);
|
|
2956
3569
|
break;
|
|
@@ -2965,6 +3578,7 @@ async function collection(config, user, localized) {
|
|
|
2965
3578
|
drafts: config.drafts === true,
|
|
2966
3579
|
versions: Boolean(config.versions),
|
|
2967
3580
|
preview: typeof config.preview === "function",
|
|
3581
|
+
schedule: config.schedule === true,
|
|
2968
3582
|
fields: await serializeFields(
|
|
2969
3583
|
config.fields,
|
|
2970
3584
|
new FieldAccessChecker("update", { user }),
|
|
@@ -2987,6 +3601,7 @@ async function global(config, user, localized) {
|
|
|
2987
3601
|
drafts: config.drafts === true,
|
|
2988
3602
|
versions: Boolean(config.versions),
|
|
2989
3603
|
preview: typeof config.preview === "function",
|
|
3604
|
+
schedule: config.schedule === true,
|
|
2990
3605
|
fields: await serializeFields(
|
|
2991
3606
|
config.fields,
|
|
2992
3607
|
new FieldAccessChecker("update", { user }),
|
|
@@ -3225,6 +3840,16 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
3225
3840
|
}
|
|
3226
3841
|
throw new HttpError("Not found", 404);
|
|
3227
3842
|
}
|
|
3843
|
+
if (first === "jobs" && second === "run" && third === void 0) {
|
|
3844
|
+
if (method !== "GET" && method !== "POST") throw methodNotAllowed(ctx, "GET, POST");
|
|
3845
|
+
const secret = cms.config.cronSecret ?? process.env.CRON_SECRET;
|
|
3846
|
+
const header = ctx.request.headers.get("authorization") ?? "";
|
|
3847
|
+
const byCron = !!secret && safeEqual(header, `Bearer ${secret}`);
|
|
3848
|
+
if (!byCron && ctx.user?.role !== "admin") {
|
|
3849
|
+
throw ctx.user ? new HttpError("Forbidden", 403) : new UnauthorizedError();
|
|
3850
|
+
}
|
|
3851
|
+
return { body: await cms.runJobs() };
|
|
3852
|
+
}
|
|
3228
3853
|
if (first === "admin") {
|
|
3229
3854
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
3230
3855
|
if (!ctx.user) throw new UnauthorizedError();
|
|
@@ -3349,6 +3974,20 @@ async function documentAction(cms, ctx, method, collection2, id, path) {
|
|
|
3349
3974
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
3350
3975
|
return { body: await cms.restoreVersion(collection2, id, versionId, { ...access, ...depth }) };
|
|
3351
3976
|
}
|
|
3977
|
+
if (action === "schedule" && path.length <= 2) {
|
|
3978
|
+
if (path.length === 2) {
|
|
3979
|
+
if (method !== "DELETE") throw methodNotAllowed(ctx, "DELETE");
|
|
3980
|
+
await cms.cancelSchedule(collection2, id, versionId, access);
|
|
3981
|
+
return { body: { message: "Cancelled" } };
|
|
3982
|
+
}
|
|
3983
|
+
if (method === "GET") return { body: await cms.scheduled(collection2, id, access) };
|
|
3984
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "GET, POST");
|
|
3985
|
+
const body = await readJson(ctx.request);
|
|
3986
|
+
return {
|
|
3987
|
+
status: 201,
|
|
3988
|
+
body: await cms.schedule(collection2, id, scheduleJob(body), access)
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3352
3991
|
if (path.length === 1 && action === "preview") {
|
|
3353
3992
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
3354
3993
|
const body = await readJson(ctx.request);
|
|
@@ -3383,6 +4022,17 @@ async function globalAction(cms, ctx, method, slug, path) {
|
|
|
3383
4022
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
3384
4023
|
return { body: await cms.restoreGlobalVersion(slug, versionId, { ...access, ...depth }) };
|
|
3385
4024
|
}
|
|
4025
|
+
if (action === "schedule" && path.length <= 2) {
|
|
4026
|
+
if (path.length === 2) {
|
|
4027
|
+
if (method !== "DELETE") throw methodNotAllowed(ctx, "DELETE");
|
|
4028
|
+
await cms.cancelGlobalSchedule(slug, versionId, access);
|
|
4029
|
+
return { body: { message: "Cancelled" } };
|
|
4030
|
+
}
|
|
4031
|
+
if (method === "GET") return { body: await cms.scheduledGlobal(slug, access) };
|
|
4032
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "GET, POST");
|
|
4033
|
+
const body = await readJson(ctx.request);
|
|
4034
|
+
return { status: 201, body: await cms.scheduleGlobal(slug, scheduleJob(body), access) };
|
|
4035
|
+
}
|
|
3386
4036
|
if (path.length === 1 && action === "preview") {
|
|
3387
4037
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
3388
4038
|
const body = await readJson(ctx.request);
|
|
@@ -3399,6 +4049,12 @@ async function globalAction(cms, ctx, method, slug, path) {
|
|
|
3399
4049
|
}
|
|
3400
4050
|
throw new HttpError("Not found", 404);
|
|
3401
4051
|
}
|
|
4052
|
+
function scheduleJob(body) {
|
|
4053
|
+
return {
|
|
4054
|
+
action: body.action,
|
|
4055
|
+
at: typeof body.at === "string" ? body.at : ""
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
3402
4058
|
function parseLocale(url) {
|
|
3403
4059
|
const locale = url.searchParams.get("locale");
|
|
3404
4060
|
const fallback = url.searchParams.get("fallback-locale");
|
|
@@ -3416,8 +4072,8 @@ function withPreviewToken(url, token) {
|
|
|
3416
4072
|
}
|
|
3417
4073
|
function requirePreview(cms, ctx, target) {
|
|
3418
4074
|
const opened = cms.verifyPreviewToken(ctx.url.searchParams.get("preview"));
|
|
3419
|
-
const
|
|
3420
|
-
if (!
|
|
4075
|
+
const matches2 = opened !== null && ("global" in target ? "global" in opened && opened.global === target.global : "collection" in opened && opened.collection === target.collection && opened.id === target.id);
|
|
4076
|
+
if (!matches2) throw new UnauthorizedError("Invalid or expired preview token");
|
|
3421
4077
|
}
|
|
3422
4078
|
function parsePage(url) {
|
|
3423
4079
|
const { page, limit } = parseListQuery(url);
|
|
@@ -3633,6 +4289,16 @@ ${indent}}`;
|
|
|
3633
4289
|
${indent} id: string
|
|
3634
4290
|
${fieldLines(field.fields, n, `${indent} `).join("\n")}
|
|
3635
4291
|
${indent}}[]`;
|
|
4292
|
+
case "blocks": {
|
|
4293
|
+
const kinds = field.blocks.map(
|
|
4294
|
+
(block) => `{
|
|
4295
|
+
${indent} id: string
|
|
4296
|
+
${indent} blockType: ${quote(block.slug)}
|
|
4297
|
+
${fieldLines(block.fields, n, `${indent} `).join("\n")}
|
|
4298
|
+
${indent}}`
|
|
4299
|
+
);
|
|
4300
|
+
return `(${kinds.join(" | ")})[]`;
|
|
4301
|
+
}
|
|
3636
4302
|
}
|
|
3637
4303
|
}
|
|
3638
4304
|
function fieldLines(fields, n, indent) {
|
|
@@ -3640,7 +4306,7 @@ function fieldLines(fields, n, indent) {
|
|
|
3640
4306
|
for (const field of fields) {
|
|
3641
4307
|
if (field.hidden) continue;
|
|
3642
4308
|
const type = fieldType(field, n, indent);
|
|
3643
|
-
const alwaysPresent = field.type === "array" || field.type === "group" || (field.type === "select" || field.type === "relationship") && field.hasMany;
|
|
4309
|
+
const alwaysPresent = field.type === "array" || field.type === "blocks" || field.type === "group" || (field.type === "select" || field.type === "relationship") && field.hasMany;
|
|
3644
4310
|
if (field.required || alwaysPresent) lines.push(`${indent}${key(field.name)}: ${type}`);
|
|
3645
4311
|
else lines.push(`${indent}${key(field.name)}?: ${type} | null`);
|
|
3646
4312
|
}
|
|
@@ -3748,6 +4414,7 @@ export {
|
|
|
3748
4414
|
TooManyRequestsError,
|
|
3749
4415
|
UnauthorizedError,
|
|
3750
4416
|
ValidationError,
|
|
4417
|
+
WEBHOOK_EVENTS,
|
|
3751
4418
|
anyone,
|
|
3752
4419
|
applyDefaults,
|
|
3753
4420
|
consoleLogger,
|
|
@@ -3758,6 +4425,7 @@ export {
|
|
|
3758
4425
|
findConfigFile,
|
|
3759
4426
|
generateSlugs,
|
|
3760
4427
|
generateTypes,
|
|
4428
|
+
hasRows,
|
|
3761
4429
|
hashPassword,
|
|
3762
4430
|
imageDimensions,
|
|
3763
4431
|
importConfig,
|
|
@@ -3771,6 +4439,7 @@ export {
|
|
|
3771
4439
|
populate,
|
|
3772
4440
|
readCookie,
|
|
3773
4441
|
resolveConfig,
|
|
4442
|
+
rowFields,
|
|
3774
4443
|
silentLogger,
|
|
3775
4444
|
singularize,
|
|
3776
4445
|
slugify,
|