@easy-cms/core 1.14.0 → 1.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-Q7MVE3BT.js → chunk-Q3QLUBFK.js} +524 -198
- package/dist/{chunk-RTKWMVAC.js → chunk-SKGKGKUN.js} +20 -2
- package/dist/{config-B-bT0hIn.d.ts → config-BLxN8qcy.d.ts} +23 -3
- package/dist/{handler-COsNEVx0.d.ts → handler-Ccc-CwBq.d.ts} +7 -2
- package/dist/index.d.ts +6 -4
- package/dist/index.js +2 -2
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +4 -2
- package/dist/{plugin-CI6r3SK1.d.ts → plugin-JIb70PKm.d.ts} +1 -1
- package/dist/plugin.d.ts +2 -2
- package/dist/plugin.js +1 -1
- package/package.json +7 -2
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
warnDeprecated,
|
|
36
36
|
withCharset,
|
|
37
37
|
withPrivacy
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-SKGKGKUN.js";
|
|
39
39
|
|
|
40
40
|
// src/access.ts
|
|
41
41
|
var isSystemAdmin = (user) => user?.role === "admin" && !user.apiKey && user.scoped !== true;
|
|
@@ -2314,7 +2314,10 @@ var ADAPTER_API_VERSION = 1;
|
|
|
2314
2314
|
|
|
2315
2315
|
// src/document.ts
|
|
2316
2316
|
import { randomUUID } from "crypto";
|
|
2317
|
-
|
|
2317
|
+
function isEmail(value) {
|
|
2318
|
+
const at = value.indexOf("@");
|
|
2319
|
+
return at > 0 && !value.includes("@", at + 1) && !/\s/.test(value) && value.slice(at + 2, -1).includes(".");
|
|
2320
|
+
}
|
|
2318
2321
|
var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2319
2322
|
function itemsOf(field, n) {
|
|
2320
2323
|
const unit = field.type === "upload" ? "file" : field.type === "relationship" ? "document" : "row";
|
|
@@ -2522,8 +2525,7 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
|
|
|
2522
2525
|
return raw;
|
|
2523
2526
|
}
|
|
2524
2527
|
case "email": {
|
|
2525
|
-
if (typeof raw !== "string" || !
|
|
2526
|
-
return fail("must be a valid email address");
|
|
2528
|
+
if (typeof raw !== "string" || !isEmail(raw)) return fail("must be a valid email address");
|
|
2527
2529
|
return raw.trim().toLowerCase();
|
|
2528
2530
|
}
|
|
2529
2531
|
case "number": {
|
|
@@ -3377,6 +3379,10 @@ function errorFields(error) {
|
|
|
3377
3379
|
return { name: error.name, message: error.message, stack: error.stack };
|
|
3378
3380
|
return { message: String(error) };
|
|
3379
3381
|
}
|
|
3382
|
+
var traceIds;
|
|
3383
|
+
function setTraceIds(read2) {
|
|
3384
|
+
traceIds = read2;
|
|
3385
|
+
}
|
|
3380
3386
|
function jsonLogger(write = (line, level) => (level === "info" ? process.stdout : process.stderr).write(`${line}
|
|
3381
3387
|
`)) {
|
|
3382
3388
|
const log = (level) => (message2, details) => {
|
|
@@ -3389,6 +3395,7 @@ function jsonLogger(write = (line, level) => (level === "info" ? process.stdout
|
|
|
3389
3395
|
message: message2,
|
|
3390
3396
|
...store2?.requestId ? { requestId: store2.requestId } : {},
|
|
3391
3397
|
...store2?.runId ? { runId: store2.runId } : {},
|
|
3398
|
+
...traceIds?.(),
|
|
3392
3399
|
...rest,
|
|
3393
3400
|
...error === void 0 ? {} : { error: errorFields(error) }
|
|
3394
3401
|
};
|
|
@@ -3730,6 +3737,7 @@ async function populate(load, collections, fields, docs, depth) {
|
|
|
3730
3737
|
}
|
|
3731
3738
|
function collectSlots(fields, data, bySlug, slots) {
|
|
3732
3739
|
for (const field of fields) {
|
|
3740
|
+
if (field.name === "__proto__") continue;
|
|
3733
3741
|
const value = data[field.name];
|
|
3734
3742
|
if (value === null || value === void 0) continue;
|
|
3735
3743
|
if (field.type === "group") {
|
|
@@ -4435,7 +4443,10 @@ for (const [net, prefix] of [
|
|
|
4435
4443
|
["::1", 128],
|
|
4436
4444
|
["64:ff9b:1::", 48],
|
|
4437
4445
|
["100::", 64],
|
|
4446
|
+
// Teredo and 6to4 tunnel to an IPv4 address the IPv6 one carries: any of them, private too.
|
|
4447
|
+
["2001::", 32],
|
|
4438
4448
|
["2001:db8::", 32],
|
|
4449
|
+
["2002::", 16],
|
|
4439
4450
|
["fc00::", 7],
|
|
4440
4451
|
["fe80::", 10],
|
|
4441
4452
|
["ff00::", 8]
|
|
@@ -4637,7 +4648,7 @@ function read(response, maxBytes, deadline) {
|
|
|
4637
4648
|
var keys = (list) => new Set(list.split(/\s+/).filter(Boolean));
|
|
4638
4649
|
var CONFIG = keys(`secret db serverURL cors cronSecret timeZone webhooks events jobs localization routes admin
|
|
4639
4650
|
upload auth collections globals endpoints cliCommands onRequest fieldTypes apiKeys email backups
|
|
4640
|
-
audit logs countCacheSeconds maxPage httpCache outbox search plugins`);
|
|
4651
|
+
audit logs countCacheSeconds maxPage httpCache outbox search telemetry plugins`);
|
|
4641
4652
|
var ADMIN = keys("path locale brand siteURL nav commands modules pages dashboard switcher");
|
|
4642
4653
|
var AUTH = keys(`roles rbac tokenExpiration maxLoginAttempts lockWindow trustedOrigins
|
|
4643
4654
|
resetPasswordExpiration inviteExpiration providers providerSignUp password setupCode members emails`);
|
|
@@ -5383,6 +5394,7 @@ function validateConfig(config) {
|
|
|
5383
5394
|
validateCountCache(config, add);
|
|
5384
5395
|
validateOutbox(config, add);
|
|
5385
5396
|
validateSearch(config, add);
|
|
5397
|
+
validateTelemetry(config, add);
|
|
5386
5398
|
validateMaxPage(config, add);
|
|
5387
5399
|
validateHttpCache(config, add);
|
|
5388
5400
|
validateAuth(config, add);
|
|
@@ -6036,6 +6048,18 @@ function validateOutbox(config, add) {
|
|
|
6036
6048
|
if (config.outbox !== void 0 && typeof config.outbox !== "boolean")
|
|
6037
6049
|
add("outbox", "must be true or false");
|
|
6038
6050
|
}
|
|
6051
|
+
function validateTelemetry(config, add) {
|
|
6052
|
+
const value = config.telemetry;
|
|
6053
|
+
if (value === void 0 || typeof value === "boolean") return;
|
|
6054
|
+
if (typeof value !== "object" || value === null) {
|
|
6055
|
+
add("telemetry", "must be true, false or { traces, metrics }");
|
|
6056
|
+
return;
|
|
6057
|
+
}
|
|
6058
|
+
for (const [key, v] of Object.entries(value))
|
|
6059
|
+
if (key !== "traces" && key !== "metrics")
|
|
6060
|
+
add(`telemetry.${key}`, "is not an option", "traces or metrics");
|
|
6061
|
+
else if (typeof v !== "boolean") add(`telemetry.${key}`, "must be true or false");
|
|
6062
|
+
}
|
|
6039
6063
|
function validateSearch(config, add) {
|
|
6040
6064
|
const adapter = config.search;
|
|
6041
6065
|
if (adapter !== void 0) {
|
|
@@ -6388,6 +6412,15 @@ function validateFields2(fields, path, reserved, collectionSlugs, add) {
|
|
|
6388
6412
|
add(`${path}[${i}].name`, `must be a valid identifier (got ${JSON.stringify(name)})`);
|
|
6389
6413
|
return;
|
|
6390
6414
|
}
|
|
6415
|
+
if (name === "__proto__") {
|
|
6416
|
+
add(`${path}[${i}].name`, '"__proto__" is reserved by JavaScript');
|
|
6417
|
+
return;
|
|
6418
|
+
}
|
|
6419
|
+
if (name === "constructor" || name === "prototype")
|
|
6420
|
+
warnDeprecated(
|
|
6421
|
+
"EASY_CMS_DEP006",
|
|
6422
|
+
`A field named "${name}" (${path}.${name}) works through 1.x; rename it before 2.0.`
|
|
6423
|
+
);
|
|
6391
6424
|
if (reserved.has(name)) {
|
|
6392
6425
|
add(`${path}.${name}`, `"${name}" is reserved`, "Easy CMS adds this field automatically");
|
|
6393
6426
|
}
|
|
@@ -7011,6 +7044,201 @@ function cacheableResponse(request, body, status, headers, times, tags) {
|
|
|
7011
7044
|
return new Response(request.method.toUpperCase() === "HEAD" ? null : body, { status, headers });
|
|
7012
7045
|
}
|
|
7013
7046
|
|
|
7047
|
+
// src/version.ts
|
|
7048
|
+
var VERSION = true ? "1.15.1" : "0.0.0-dev";
|
|
7049
|
+
|
|
7050
|
+
// src/telemetry.ts
|
|
7051
|
+
var DB_CALLS = /* @__PURE__ */ new Set([
|
|
7052
|
+
"find",
|
|
7053
|
+
"findPage",
|
|
7054
|
+
"findAfter",
|
|
7055
|
+
"findById",
|
|
7056
|
+
"findByIds",
|
|
7057
|
+
"count",
|
|
7058
|
+
"create",
|
|
7059
|
+
"update",
|
|
7060
|
+
"increment",
|
|
7061
|
+
"delete",
|
|
7062
|
+
"findGlobal",
|
|
7063
|
+
"updateGlobal"
|
|
7064
|
+
]);
|
|
7065
|
+
var Telemetry = class _Telemetry {
|
|
7066
|
+
constructor(api, traces, metrics) {
|
|
7067
|
+
this.api = api;
|
|
7068
|
+
this.tracer = traces ? api.trace.getTracer("easy-cms", VERSION) : void 0;
|
|
7069
|
+
if (metrics) {
|
|
7070
|
+
const meter = api.metrics.getMeter("easy-cms", VERSION);
|
|
7071
|
+
this.httpDuration = meter.createHistogram("http.server.request.duration", {
|
|
7072
|
+
unit: "s",
|
|
7073
|
+
description: "Duration of API requests"
|
|
7074
|
+
});
|
|
7075
|
+
this.dbDuration = meter.createHistogram("db.client.operation.duration", {
|
|
7076
|
+
unit: "s",
|
|
7077
|
+
description: "Duration of database calls"
|
|
7078
|
+
});
|
|
7079
|
+
this.jobRuns = meter.createCounter("easy_cms.jobs.runs", {
|
|
7080
|
+
description: "Runs of the config\u2019s jobs, by job and result"
|
|
7081
|
+
});
|
|
7082
|
+
this.jobDuration = meter.createHistogram("easy_cms.jobs.duration", {
|
|
7083
|
+
unit: "s",
|
|
7084
|
+
description: "Duration of the config\u2019s jobs"
|
|
7085
|
+
});
|
|
7086
|
+
meter.createObservableGauge("easy_cms.queue.size", {
|
|
7087
|
+
description: "Background work waiting or failed, as of the last job run"
|
|
7088
|
+
}).addCallback((result) => {
|
|
7089
|
+
for (const [queue, sizes] of Object.entries(this.queues))
|
|
7090
|
+
for (const state of ["pending", "failed"])
|
|
7091
|
+
result.observe(sizes[state], { queue, state });
|
|
7092
|
+
});
|
|
7093
|
+
meter.createObservableGauge("easy_cms.scheduled.late", {
|
|
7094
|
+
description: "Scheduled publishes more than 10 minutes late, as of the last job run"
|
|
7095
|
+
}).addCallback((result) => result.observe(this.late));
|
|
7096
|
+
}
|
|
7097
|
+
if (traces)
|
|
7098
|
+
setTraceIds(() => {
|
|
7099
|
+
const context = api.trace.getActiveSpan()?.spanContext();
|
|
7100
|
+
return context && api.isSpanContextValid(context) ? { traceId: context.traceId, spanId: context.spanId } : void 0;
|
|
7101
|
+
});
|
|
7102
|
+
}
|
|
7103
|
+
api;
|
|
7104
|
+
tracer;
|
|
7105
|
+
httpDuration;
|
|
7106
|
+
dbDuration;
|
|
7107
|
+
jobRuns;
|
|
7108
|
+
jobDuration;
|
|
7109
|
+
queues = {};
|
|
7110
|
+
late = 0;
|
|
7111
|
+
/** `undefined` without `telemetry`; throws when `@opentelemetry/api` isn't installed. */
|
|
7112
|
+
static async load(config) {
|
|
7113
|
+
if (!config) return void 0;
|
|
7114
|
+
const { traces = true, metrics = true } = config === true ? {} : config;
|
|
7115
|
+
if (!traces && !metrics) return void 0;
|
|
7116
|
+
let api;
|
|
7117
|
+
try {
|
|
7118
|
+
api = await import("@opentelemetry/api");
|
|
7119
|
+
} catch {
|
|
7120
|
+
throw new ConfigError([
|
|
7121
|
+
{
|
|
7122
|
+
path: "telemetry",
|
|
7123
|
+
message: "needs @opentelemetry/api",
|
|
7124
|
+
hint: "npm install @opentelemetry/api, and set up the OpenTelemetry SDK in your app"
|
|
7125
|
+
}
|
|
7126
|
+
]);
|
|
7127
|
+
}
|
|
7128
|
+
return new _Telemetry(api, traces, metrics);
|
|
7129
|
+
}
|
|
7130
|
+
/** Runs `fn` in a span; with traces off, just runs it. Errors mark the span and go on. */
|
|
7131
|
+
async span(name, attributes, fn, kind = "internal") {
|
|
7132
|
+
if (!this.tracer) return fn(void 0);
|
|
7133
|
+
const spanKind = { internal: 0, server: 1, client: 2 }[kind];
|
|
7134
|
+
return this.tracer.startActiveSpan(name, { kind: spanKind, attributes }, async (span) => {
|
|
7135
|
+
try {
|
|
7136
|
+
return await fn(span);
|
|
7137
|
+
} catch (error) {
|
|
7138
|
+
span.setStatus({ code: this.api.SpanStatusCode.ERROR });
|
|
7139
|
+
span.setAttribute("error.type", errorType(error));
|
|
7140
|
+
throw error;
|
|
7141
|
+
} finally {
|
|
7142
|
+
span.end();
|
|
7143
|
+
}
|
|
7144
|
+
});
|
|
7145
|
+
}
|
|
7146
|
+
/**
|
|
7147
|
+
* An API request: a server span when nothing traces it yet (`easy-cms serve`), else a span
|
|
7148
|
+
* inside the framework's (Next.js, Nuxt with an SDK).
|
|
7149
|
+
*/
|
|
7150
|
+
async request(method, route2, fn) {
|
|
7151
|
+
const started = performance.now();
|
|
7152
|
+
const kind = this.api.trace.getActiveSpan() ? "internal" : "server";
|
|
7153
|
+
const attributes = { "http.request.method": method, "http.route": route2 };
|
|
7154
|
+
let status = 500;
|
|
7155
|
+
try {
|
|
7156
|
+
return await this.span(
|
|
7157
|
+
`easy-cms ${method} ${route2}`,
|
|
7158
|
+
attributes,
|
|
7159
|
+
async (span) => {
|
|
7160
|
+
const response = await fn();
|
|
7161
|
+
status = response.status;
|
|
7162
|
+
span?.setAttribute("http.response.status_code", status);
|
|
7163
|
+
if (status >= 500) span?.setStatus({ code: this.api.SpanStatusCode.ERROR });
|
|
7164
|
+
return response;
|
|
7165
|
+
},
|
|
7166
|
+
kind
|
|
7167
|
+
);
|
|
7168
|
+
} finally {
|
|
7169
|
+
this.httpDuration?.record((performance.now() - started) / 1e3, {
|
|
7170
|
+
...attributes,
|
|
7171
|
+
"http.response.status_code": status
|
|
7172
|
+
});
|
|
7173
|
+
}
|
|
7174
|
+
}
|
|
7175
|
+
/** The database with a client span and a duration per call (`db <call> <collection>`). */
|
|
7176
|
+
database(db, system2) {
|
|
7177
|
+
return new Proxy(db, {
|
|
7178
|
+
get: (target, key, receiver) => {
|
|
7179
|
+
const value = Reflect.get(target, key, receiver);
|
|
7180
|
+
if (typeof key !== "string" || !DB_CALLS.has(key) || typeof value !== "function")
|
|
7181
|
+
return value;
|
|
7182
|
+
return (args, ...rest) => {
|
|
7183
|
+
const name = args?.collection ?? args?.slug ?? "";
|
|
7184
|
+
const attributes = {
|
|
7185
|
+
"db.system.name": system2,
|
|
7186
|
+
"db.operation.name": key,
|
|
7187
|
+
"db.collection.name": name
|
|
7188
|
+
};
|
|
7189
|
+
const started = performance.now();
|
|
7190
|
+
return this.span(
|
|
7191
|
+
`db ${key} ${name}`,
|
|
7192
|
+
attributes,
|
|
7193
|
+
() => Promise.resolve(value.call(target, args, ...rest)),
|
|
7194
|
+
"client"
|
|
7195
|
+
).finally(() => this.dbDuration?.record((performance.now() - started) / 1e3, attributes));
|
|
7196
|
+
};
|
|
7197
|
+
}
|
|
7198
|
+
});
|
|
7199
|
+
}
|
|
7200
|
+
/** A run of one of the config's jobs, for `easy_cms.jobs.*`. */
|
|
7201
|
+
jobRan(name, ok, ms) {
|
|
7202
|
+
const attributes = { job: name, result: ok ? "ok" : "failed" };
|
|
7203
|
+
this.jobRuns?.add(1, attributes);
|
|
7204
|
+
this.jobDuration?.record(ms / 1e3, { job: name });
|
|
7205
|
+
}
|
|
7206
|
+
/** Whether the queue gauges are reported (and worth counting for). */
|
|
7207
|
+
get measuresQueues() {
|
|
7208
|
+
return this.jobRuns !== void 0;
|
|
7209
|
+
}
|
|
7210
|
+
/** Queue sizes found by the last job run, for the gauges. */
|
|
7211
|
+
queuesCounted(queues, late) {
|
|
7212
|
+
this.queues = queues;
|
|
7213
|
+
this.late = late;
|
|
7214
|
+
}
|
|
7215
|
+
};
|
|
7216
|
+
function errorType(error) {
|
|
7217
|
+
if (error && typeof error === "object") {
|
|
7218
|
+
const code = error.code;
|
|
7219
|
+
if (typeof code === "string" && code) return code;
|
|
7220
|
+
const name = error.name;
|
|
7221
|
+
if (typeof name === "string" && name) return name;
|
|
7222
|
+
}
|
|
7223
|
+
return "Error";
|
|
7224
|
+
}
|
|
7225
|
+
function routeOf(path, root) {
|
|
7226
|
+
if (path === null) return "other";
|
|
7227
|
+
if (root) return path;
|
|
7228
|
+
const parts = path.split("/").filter(Boolean);
|
|
7229
|
+
const [first] = parts;
|
|
7230
|
+
const fixed = /* @__PURE__ */ new Set(["auth", "admin", "jobs", "search", "healthz", "readyz", "api-keys"]);
|
|
7231
|
+
const word = /^[a-z][a-z.-]{0,39}$/;
|
|
7232
|
+
const route2 = parts.map((part, i) => {
|
|
7233
|
+
if (i === 0) return fixed.has(part) || part === "globals" ? part : ":collection";
|
|
7234
|
+
if (first === "globals" && i === 1) return ":global";
|
|
7235
|
+
if (first === "admin" || first === "auth") return word.test(part) ? part : ":param";
|
|
7236
|
+
if (i === 1) return /^\d+$/.test(part) || !word.test(part) ? ":id" : part;
|
|
7237
|
+
return word.test(part) ? part : ":id";
|
|
7238
|
+
});
|
|
7239
|
+
return `/${route2.join("/")}`;
|
|
7240
|
+
}
|
|
7241
|
+
|
|
7014
7242
|
// src/local-api.ts
|
|
7015
7243
|
async function createEasyCMS(config, options = {}) {
|
|
7016
7244
|
const resolved3 = await resolveConfig(config);
|
|
@@ -7023,13 +7251,15 @@ async function createEasyCMS(config, options = {}) {
|
|
|
7023
7251
|
const storage = resolved3.upload.storage ?? diskStorage({ dir: resolved3.upload.dir });
|
|
7024
7252
|
await storage.init?.({ cwd });
|
|
7025
7253
|
if (resolved3.upload.privateStorage) await resolved3.upload.privateStorage.init?.({ cwd });
|
|
7026
|
-
const
|
|
7254
|
+
const telemetry = await Telemetry.load(resolved3.telemetry);
|
|
7255
|
+
const connected = await resolved3.db.init({
|
|
7027
7256
|
config: resolved3,
|
|
7028
7257
|
cwd,
|
|
7029
7258
|
schema: options.schema ?? (process.env.NODE_ENV === "production" ? "verify" : "push"),
|
|
7030
7259
|
logger,
|
|
7031
7260
|
interactive: options.interactive ?? false
|
|
7032
7261
|
});
|
|
7262
|
+
const db = telemetry ? telemetry.database(connected, dbSystem(resolved3.db.name)) : connected;
|
|
7033
7263
|
if (resolved3.outbox && db.outbox !== true) {
|
|
7034
7264
|
await db.destroy();
|
|
7035
7265
|
throw new ConfigError([
|
|
@@ -7041,6 +7271,7 @@ async function createEasyCMS(config, options = {}) {
|
|
|
7041
7271
|
]);
|
|
7042
7272
|
}
|
|
7043
7273
|
const cms = new EasyCMS(resolved3, db, logger, storage, cwd);
|
|
7274
|
+
cms.telemetry = telemetry;
|
|
7044
7275
|
if (resolved3.search?.ephemeral && process.env.NODE_ENV === "production")
|
|
7045
7276
|
logger.warn(
|
|
7046
7277
|
`Search: the "${resolved3.search.name ?? "custom"}" index lives in this process's memory: it is built again at each start, and each instance has its own. Use a search engine adapter for large sites or several instances.`
|
|
@@ -7054,6 +7285,10 @@ async function createEasyCMS(config, options = {}) {
|
|
|
7054
7285
|
);
|
|
7055
7286
|
return cms;
|
|
7056
7287
|
}
|
|
7288
|
+
function dbSystem(name) {
|
|
7289
|
+
if (name.includes("postgres") || name.includes("pglite")) return "postgresql";
|
|
7290
|
+
return name.includes("sqlite") ? "sqlite" : name;
|
|
7291
|
+
}
|
|
7057
7292
|
var toJob = (row) => ({
|
|
7058
7293
|
id: row.id,
|
|
7059
7294
|
action: row.action,
|
|
@@ -7090,6 +7325,8 @@ var EasyCMS = class {
|
|
|
7090
7325
|
outbox;
|
|
7091
7326
|
/** With `search`: keeps the index in step with published documents. */
|
|
7092
7327
|
searchIndex;
|
|
7328
|
+
/** @internal With `telemetry`: OpenTelemetry spans and metrics (set by `createEasyCMS`). */
|
|
7329
|
+
telemetry;
|
|
7093
7330
|
/** @internal */
|
|
7094
7331
|
constructor(config, db, logger = consoleLogger, storage = diskStorage({ dir: config.upload.dir }), cwd = process.cwd(), privateStorage) {
|
|
7095
7332
|
this.config = config;
|
|
@@ -7117,7 +7354,15 @@ var EasyCMS = class {
|
|
|
7117
7354
|
logger,
|
|
7118
7355
|
config.collections.some((c) => c.slug === EMAIL_DELIVERIES) ? this.emailQueue() : void 0
|
|
7119
7356
|
);
|
|
7120
|
-
this.outbox = config.collections.some((c) => c.slug === OUTBOX) ? new Outbox(
|
|
7357
|
+
this.outbox = config.collections.some((c) => c.slug === OUTBOX) ? new Outbox(
|
|
7358
|
+
db,
|
|
7359
|
+
logger,
|
|
7360
|
+
(entry, done, mark) => this.telemetry ? this.telemetry.span(
|
|
7361
|
+
`easy-cms outbox ${entry.event} ${entry.target}`,
|
|
7362
|
+
{ "easy_cms.event": entry.event, "easy_cms.collection": entry.target },
|
|
7363
|
+
() => this.runOutbox(entry, done, mark)
|
|
7364
|
+
) : this.runOutbox(entry, done, mark)
|
|
7365
|
+
) : void 0;
|
|
7121
7366
|
this.searchIndex = config.search ? new SearchIndex(
|
|
7122
7367
|
config.search,
|
|
7123
7368
|
config.collections,
|
|
@@ -7164,99 +7409,112 @@ var EasyCMS = class {
|
|
|
7164
7409
|
};
|
|
7165
7410
|
}
|
|
7166
7411
|
async find(collection2, options = {}) {
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
requested
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
if (
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
docs:
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7412
|
+
return this.traced("find", String(collection2), async () => {
|
|
7413
|
+
const config = this.collection(collection2);
|
|
7414
|
+
const guard = guardOf(options);
|
|
7415
|
+
const limit = options.limit ?? 10;
|
|
7416
|
+
const page = options.page ?? 1;
|
|
7417
|
+
if (!Number.isInteger(limit) || limit < 0)
|
|
7418
|
+
throw new QueryError("limit must be a non-negative integer");
|
|
7419
|
+
if (!Number.isInteger(page) || page < 1)
|
|
7420
|
+
throw new QueryError("page must be a positive integer");
|
|
7421
|
+
const draft = readsDrafts(guard, options.draft);
|
|
7422
|
+
const where = this.whereFor(
|
|
7423
|
+
config,
|
|
7424
|
+
draftWhere(config, draft, await this.readWhere(config, guard, options.where)),
|
|
7425
|
+
options
|
|
7426
|
+
);
|
|
7427
|
+
const requested = options.sort === void 0 ? ["-createdAt"] : [options.sort].flat();
|
|
7428
|
+
await this.checkQueryFields(config, guard, void 0, requested);
|
|
7429
|
+
const locale = this.localeOf(options);
|
|
7430
|
+
const sort = locale === void 0 ? requested : localizeSort(
|
|
7431
|
+
config.fields,
|
|
7432
|
+
requested,
|
|
7433
|
+
locale === ALL_LOCALES ? this.config.localization?.defaultLocale ?? locale : locale,
|
|
7434
|
+
this.config.localization?.locales ?? []
|
|
7435
|
+
);
|
|
7436
|
+
if (options.cursor !== void 0) {
|
|
7437
|
+
if (options.page !== void 0) throw new QueryError("Use either cursor or page, not both");
|
|
7438
|
+
if (limit === 0) throw new QueryError("A cursor needs a limit above 0");
|
|
7439
|
+
if (!this.db.findAfter)
|
|
7440
|
+
throw new QueryError("This database adapter has no cursor pagination");
|
|
7441
|
+
const query = cursorQuery(collection2, options.where, requested, locale, draft);
|
|
7442
|
+
const after = readCursor(options.cursor, query);
|
|
7443
|
+
const result2 = await this.db.findAfter({ collection: collection2, where, sort, limit, after });
|
|
7444
|
+
const found2 = draft ? await this.withDrafts(config, result2.docs) : result2.docs;
|
|
7445
|
+
const docs2 = await this.output(config, found2, guard, { ...options, draft });
|
|
7446
|
+
return {
|
|
7447
|
+
docs: docs2,
|
|
7448
|
+
limit,
|
|
7449
|
+
nextCursor: result2.hasNextPage && result2.next ? writeCursor(result2.next, query) : null,
|
|
7450
|
+
hasNextPage: result2.hasNextPage,
|
|
7451
|
+
hasPrevPage: after !== null,
|
|
7452
|
+
totalDocs: null,
|
|
7453
|
+
totalPages: null,
|
|
7454
|
+
page: null
|
|
7455
|
+
};
|
|
7456
|
+
}
|
|
7457
|
+
const args = { collection: collection2, where, sort, limit, page };
|
|
7458
|
+
const count = options.count !== false || limit === 0;
|
|
7459
|
+
const result = count ? await this.db.find(args) : this.db.findPage ? await this.db.findPage(args) : { ...await this.db.find(args), totalDocs: null, totalPages: null };
|
|
7460
|
+
const found = draft ? await this.withDrafts(config, result.docs) : result.docs;
|
|
7461
|
+
const docs = await this.output(config, found, guard, { ...options, draft });
|
|
7462
|
+
return { ...result, docs };
|
|
7463
|
+
});
|
|
7215
7464
|
}
|
|
7216
7465
|
async findById(collection2, id, options = {}) {
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7466
|
+
return this.traced("findById", String(collection2), async () => {
|
|
7467
|
+
const config = this.collection(collection2);
|
|
7468
|
+
const guard = guardOf(options);
|
|
7469
|
+
const parsed = parseId(id);
|
|
7470
|
+
if (parsed === void 0) return null;
|
|
7471
|
+
const draft = readsDrafts(guard, options.draft);
|
|
7472
|
+
const where = this.whereFor(
|
|
7473
|
+
config,
|
|
7474
|
+
draftWhere(config, draft, await this.readWhere(config, guard, void 0)),
|
|
7475
|
+
options
|
|
7476
|
+
);
|
|
7477
|
+
const doc = where ? (await this.db.find({
|
|
7478
|
+
collection: collection2,
|
|
7479
|
+
where: andWhere(where, { id: { equals: parsed } }),
|
|
7480
|
+
sort: [],
|
|
7481
|
+
limit: 1,
|
|
7482
|
+
page: 1
|
|
7483
|
+
})).docs[0] : await this.db.findById({ collection: collection2, id: parsed });
|
|
7484
|
+
if (!doc) return null;
|
|
7485
|
+
const [current] = draft ? await this.withDrafts(config, [doc]) : [doc];
|
|
7486
|
+
const [out] = await this.output(config, [current], guard, {
|
|
7487
|
+
...options,
|
|
7488
|
+
draft
|
|
7489
|
+
});
|
|
7490
|
+
return out ?? null;
|
|
7491
|
+
});
|
|
7238
7492
|
}
|
|
7239
7493
|
async count(collection2, options = {}) {
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
draftWhere(
|
|
7494
|
+
return this.traced("count", String(collection2), async () => {
|
|
7495
|
+
const config = this.collection(collection2);
|
|
7496
|
+
const where = this.whereFor(
|
|
7244
7497
|
config,
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7498
|
+
draftWhere(
|
|
7499
|
+
config,
|
|
7500
|
+
readsDrafts(guardOf(options), options.draft),
|
|
7501
|
+
await this.readWhere(config, guardOf(options), options.where)
|
|
7502
|
+
),
|
|
7503
|
+
options
|
|
7504
|
+
);
|
|
7505
|
+
return this.db.count({ collection: collection2, where });
|
|
7506
|
+
});
|
|
7251
7507
|
}
|
|
7252
7508
|
async create(collection2, data, options = {}) {
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7509
|
+
return this.traced("create", String(collection2), async () => {
|
|
7510
|
+
const config = this.collection(collection2);
|
|
7511
|
+
if (config.slug === MEDIA) {
|
|
7512
|
+
throw new ValidationError(MEDIA, [
|
|
7513
|
+
{ field: "file", message: "upload files with cms.upload() or POST multipart" }
|
|
7514
|
+
]);
|
|
7515
|
+
}
|
|
7516
|
+
return await this.createDocument(config, asObject(data, collection2), options);
|
|
7517
|
+
});
|
|
7260
7518
|
}
|
|
7261
7519
|
/**
|
|
7262
7520
|
* Stores a file and creates its `media` document (FR-UPL). The type is detected from
|
|
@@ -7711,7 +7969,14 @@ var EasyCMS = class {
|
|
|
7711
7969
|
}
|
|
7712
7970
|
}
|
|
7713
7971
|
async update(collection2, id, data, options = {}) {
|
|
7714
|
-
return
|
|
7972
|
+
return this.traced("update", String(collection2), async () => {
|
|
7973
|
+
return await this.updateDocument(
|
|
7974
|
+
collection2,
|
|
7975
|
+
id,
|
|
7976
|
+
asObject(data, collection2),
|
|
7977
|
+
options
|
|
7978
|
+
);
|
|
7979
|
+
});
|
|
7715
7980
|
}
|
|
7716
7981
|
/**
|
|
7717
7982
|
* Adds `by` (negative to take away) to a top-level number field in one database statement, so
|
|
@@ -8082,65 +8347,69 @@ var EasyCMS = class {
|
|
|
8082
8347
|
* gives the documents they own to another user; without it they have no owner.
|
|
8083
8348
|
*/
|
|
8084
8349
|
async delete(collection2, id, options = {}) {
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8350
|
+
return this.traced("delete", String(collection2), async () => {
|
|
8351
|
+
const config = this.collection(collection2);
|
|
8352
|
+
const guard = guardOf(options);
|
|
8353
|
+
const parsed = parseId(id);
|
|
8354
|
+
const existing = parsed === void 0 ? null : await this.db.findById({ collection: collection2, id: parsed });
|
|
8355
|
+
if (!existing || parsed === void 0) throw new NotFoundError(collection2, id);
|
|
8356
|
+
await this.checkDocumentAccess(config, "delete", guard, parsed, void 0);
|
|
8357
|
+
if (config.slug === USERS)
|
|
8358
|
+
await this.guardLastAdmin(parsed, existing, { ...existing, active: false });
|
|
8359
|
+
const base = this.hookArgs(config, guard);
|
|
8360
|
+
for (const hook of config.hooks?.beforeDelete ?? []) await hook({ ...base, id: parsed });
|
|
8361
|
+
if (config.slug === USERS) {
|
|
8362
|
+
const to = options.transferTo;
|
|
8363
|
+
if (to !== void 0 && to !== null) {
|
|
8364
|
+
const target = parseId(to);
|
|
8365
|
+
const heir = target === void 0 ? null : await this.db.findById({ collection: USERS, id: target });
|
|
8366
|
+
if (!heir || String(heir.id) === String(parsed))
|
|
8367
|
+
throw new ValidationError(USERS, [
|
|
8368
|
+
{ field: "transferTo", message: "must be another existing user" }
|
|
8369
|
+
]);
|
|
8370
|
+
await this.roles.transfer(parsed, heir.id);
|
|
8371
|
+
} else await this.roles.transfer(parsed, null);
|
|
8372
|
+
await this.auth.revokeSessions(parsed);
|
|
8373
|
+
await this.auth.sso.forget(parsed);
|
|
8374
|
+
}
|
|
8375
|
+
if (collection2 === MEDIA_FOLDERS) {
|
|
8376
|
+
await this.checkMoveLimit(await this.folders.privacyAfter(parsed, { private: false }));
|
|
8377
|
+
await this.folders.release(existing);
|
|
8378
|
+
}
|
|
8379
|
+
const outbox = this.outboxRow(config, "delete", guard, existing, { docId: parsed });
|
|
8380
|
+
await this.db.delete({ collection: collection2, id: parsed, outbox });
|
|
8381
|
+
if (collection2 === MEDIA_FOLDERS) {
|
|
8382
|
+
this.folders.invalidate();
|
|
8383
|
+
const parent = existing.parent ?? null;
|
|
8384
|
+
if (parent !== null) await this.settlePrivacy(parent);
|
|
8385
|
+
else await this.settleTopLevel();
|
|
8386
|
+
}
|
|
8387
|
+
if (versionLimit(config)) await this.versions.deleteAll(collectionParent(collection2), parsed);
|
|
8388
|
+
if (config.schedule) {
|
|
8389
|
+
for (const job of await this.pendingJobs(collection2, parsed))
|
|
8390
|
+
await this.db.delete({ collection: SCHEDULED_JOBS, id: job.id });
|
|
8391
|
+
}
|
|
8392
|
+
if (outbox) this.outbox?.kick();
|
|
8393
|
+
else {
|
|
8394
|
+
await this.notify(config.hooks?.afterDelete, "afterDelete", config.slug, {
|
|
8395
|
+
...base,
|
|
8396
|
+
id: parsed,
|
|
8397
|
+
doc: existing
|
|
8398
|
+
});
|
|
8399
|
+
this.emitChange(config, "delete", existing);
|
|
8400
|
+
}
|
|
8401
|
+
await this.audited(config, "delete", existing, existing, guard);
|
|
8402
|
+
const [out] = await this.output(config, [existing], guard, { depth: 0, draft: true });
|
|
8403
|
+
return out;
|
|
8404
|
+
});
|
|
8138
8405
|
}
|
|
8139
8406
|
async findGlobal(slug2, options = {}) {
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8407
|
+
return this.traced("findGlobal", String(slug2), async () => {
|
|
8408
|
+
const config = this.global(slug2);
|
|
8409
|
+
const guard = guardOf(options);
|
|
8410
|
+
await this.checkGlobalAccess(config, "read", guard);
|
|
8411
|
+
return this.readGlobal(config, this.globalKey(config, guard), guard, options);
|
|
8412
|
+
});
|
|
8144
8413
|
}
|
|
8145
8414
|
/** A global as stored under `key` (see `globalKey`); empty when there is no key. */
|
|
8146
8415
|
async readGlobal(config, key, guard, options) {
|
|
@@ -8180,7 +8449,9 @@ var EasyCMS = class {
|
|
|
8180
8449
|
return key;
|
|
8181
8450
|
}
|
|
8182
8451
|
async updateGlobal(slug2, data, options = {}) {
|
|
8183
|
-
return this.
|
|
8452
|
+
return this.traced("updateGlobal", String(slug2), async () => {
|
|
8453
|
+
return this.saveGlobal(slug2, asObject(data, slug2), options);
|
|
8454
|
+
});
|
|
8184
8455
|
}
|
|
8185
8456
|
/** Takes a global off the site (`status: 'draft'`). See `unpublish`. */
|
|
8186
8457
|
async unpublishGlobal(slug2, options = {}) {
|
|
@@ -8447,36 +8718,39 @@ var EasyCMS = class {
|
|
|
8447
8718
|
* hold fewer documents than `limit`.
|
|
8448
8719
|
*/
|
|
8449
8720
|
async search(query, options = {}) {
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
{
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8721
|
+
return this.traced("search", "", async () => {
|
|
8722
|
+
const index = this.searchIndex;
|
|
8723
|
+
if (!index) throw new QueryError("Search needs a `search` adapter in the config");
|
|
8724
|
+
const limit = options.limit ?? 10;
|
|
8725
|
+
const page = options.page ?? 1;
|
|
8726
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
8727
|
+
throw new QueryError("limit must be an integer from 1 to 100");
|
|
8728
|
+
if (!Number.isInteger(page) || page < 1)
|
|
8729
|
+
throw new QueryError("page must be an integer from 1");
|
|
8730
|
+
const collections = options.collections ?? index.searchable;
|
|
8731
|
+
for (const slug2 of collections)
|
|
8732
|
+
if (!index.configOf(slug2)) throw new QueryError(`"${slug2}" is not searchable`);
|
|
8733
|
+
const localization = this.config.localization;
|
|
8734
|
+
const asked = options.locale && options.locale !== ALL_LOCALES ? options.locale : void 0;
|
|
8735
|
+
if (asked !== void 0 && localization && !localization.locales.includes(asked))
|
|
8736
|
+
throw new QueryError(`Unknown locale "${asked}"`);
|
|
8737
|
+
const locale = localization ? asked ?? localization.defaultLocale ?? null : null;
|
|
8738
|
+
const text3 = query.trim().slice(0, 200);
|
|
8739
|
+
const empty = { hits: [], total: 0, page, limit, hasNextPage: false };
|
|
8740
|
+
if (!text3 || collections.length === 0) return empty;
|
|
8741
|
+
const { collections: _, limit: __, page: ___, ...read2 } = options;
|
|
8742
|
+
const { hits, total } = await index.search(
|
|
8743
|
+
{ query: text3, collections, locale, limit, offset: (page - 1) * limit },
|
|
8744
|
+
async (collection2, ids) => (await this.find(collection2, {
|
|
8745
|
+
...read2,
|
|
8746
|
+
...locale ? { locale } : {},
|
|
8747
|
+
where: { id: { in: ids.map((id) => parseId(id) ?? id) } },
|
|
8748
|
+
limit: ids.length,
|
|
8749
|
+
draft: false
|
|
8750
|
+
})).docs
|
|
8751
|
+
);
|
|
8752
|
+
return { hits, total, page, limit, hasNextPage: page * limit < total };
|
|
8753
|
+
});
|
|
8480
8754
|
}
|
|
8481
8755
|
/**
|
|
8482
8756
|
* Builds the search index again from the database, for every searchable collection or one:
|
|
@@ -8515,6 +8789,18 @@ var EasyCMS = class {
|
|
|
8515
8789
|
if (!found.hasNextPage) return;
|
|
8516
8790
|
}
|
|
8517
8791
|
}
|
|
8792
|
+
/** A Local API call in a span (`easy-cms <operation> <collection>`) with `telemetry`. */
|
|
8793
|
+
traced(operation, target, fn) {
|
|
8794
|
+
if (!this.telemetry) return fn();
|
|
8795
|
+
return this.telemetry.span(
|
|
8796
|
+
`easy-cms ${operation}${target ? ` ${target}` : ""}`,
|
|
8797
|
+
{
|
|
8798
|
+
"easy_cms.operation": operation,
|
|
8799
|
+
...target ? { "easy_cms.collection": target } : {}
|
|
8800
|
+
},
|
|
8801
|
+
fn
|
|
8802
|
+
);
|
|
8803
|
+
}
|
|
8518
8804
|
/** @internal Puts a failed outbox entry back in line (Settings → Jobs). */
|
|
8519
8805
|
retryOutbox(id) {
|
|
8520
8806
|
return this.outbox?.retry(id) ?? Promise.resolve(false);
|
|
@@ -8546,7 +8832,9 @@ var EasyCMS = class {
|
|
|
8546
8832
|
* scheduler and the cron endpoint (`GET <api>/jobs/run`) call.
|
|
8547
8833
|
*/
|
|
8548
8834
|
runJobs(now = /* @__PURE__ */ new Date()) {
|
|
8549
|
-
const run = this.running.then(
|
|
8835
|
+
const run = this.running.then(
|
|
8836
|
+
() => this.telemetry ? this.telemetry.span("easy-cms jobs.run", {}, () => this.runJobsNow(now)) : this.runJobsNow(now)
|
|
8837
|
+
);
|
|
8550
8838
|
this.running = run.catch(() => {
|
|
8551
8839
|
});
|
|
8552
8840
|
return run;
|
|
@@ -8573,6 +8861,10 @@ var EasyCMS = class {
|
|
|
8573
8861
|
jobs: await this.runConfigJobs(now),
|
|
8574
8862
|
outbox: await this.outbox?.run(now) ?? { ran: 0, failed: 0 }
|
|
8575
8863
|
};
|
|
8864
|
+
if (this.telemetry?.measuresQueues)
|
|
8865
|
+
await this.countQueues(now).catch(
|
|
8866
|
+
(error) => this.logger.error(`Telemetry: ${error.message}`, { error })
|
|
8867
|
+
);
|
|
8576
8868
|
const ranAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8577
8869
|
await this.db.updateGlobal({ slug: JOBS_RAN, data: { at: ranAt, updatedAt: ranAt } }).catch((error) => this.logger.error(`Jobs: ${error.message}`, { error }));
|
|
8578
8870
|
return result;
|
|
@@ -8641,19 +8933,47 @@ var EasyCMS = class {
|
|
|
8641
8933
|
const started = Date.now();
|
|
8642
8934
|
let error = null;
|
|
8643
8935
|
try {
|
|
8644
|
-
|
|
8936
|
+
const work = async () => job.run({ cms: this, now });
|
|
8937
|
+
await (this.telemetry ? this.telemetry.span(`easy-cms job ${job.name}`, { "easy_cms.job": job.name }, work) : work());
|
|
8645
8938
|
ran++;
|
|
8646
8939
|
} catch (caught) {
|
|
8647
8940
|
failed++;
|
|
8648
8941
|
error = (caught.message || String(caught)).slice(0, 500);
|
|
8649
8942
|
this.logger.error(`Job ${job.name} failed: ${error}`, { error: caught, job: job.name });
|
|
8650
8943
|
}
|
|
8944
|
+
this.telemetry?.jobRan(job.name, error === null, Date.now() - started);
|
|
8651
8945
|
await this.recordJobRun(job.name, stamp, error, Date.now() - started).catch(
|
|
8652
8946
|
(caught) => this.logger.error(`Job ${job.name}: ${caught.message}`, { error: caught })
|
|
8653
8947
|
);
|
|
8654
8948
|
}
|
|
8655
8949
|
return { ran, failed };
|
|
8656
8950
|
}
|
|
8951
|
+
/** Background work waiting and failed, for `telemetry`'s queue gauges. */
|
|
8952
|
+
async countQueues(now) {
|
|
8953
|
+
const has3 = (slug2) => this.config.collections.some((c) => c.slug === slug2);
|
|
8954
|
+
const sizes = async (collection2, pending) => ({
|
|
8955
|
+
pending: await this.db.count({ collection: collection2, where: { state: { in: pending } } }),
|
|
8956
|
+
failed: await this.db.count({ collection: collection2, where: { state: { equals: "failed" } } })
|
|
8957
|
+
});
|
|
8958
|
+
const queues = {};
|
|
8959
|
+
if (has3(OUTBOX)) queues.outbox = await sizes(OUTBOX, ["pending", "running"]);
|
|
8960
|
+
if (has3(WEBHOOK_DELIVERIES)) queues.webhooks = await sizes(WEBHOOK_DELIVERIES, ["pending"]);
|
|
8961
|
+
if (has3(EMAIL_DELIVERIES)) queues.emails = await sizes(EMAIL_DELIVERIES, ["pending"]);
|
|
8962
|
+
let late = 0;
|
|
8963
|
+
if (has3(SCHEDULED_JOBS)) {
|
|
8964
|
+
queues.scheduled = await sizes(SCHEDULED_JOBS, ["pending", "running"]);
|
|
8965
|
+
late = await this.db.count({
|
|
8966
|
+
collection: SCHEDULED_JOBS,
|
|
8967
|
+
where: {
|
|
8968
|
+
and: [
|
|
8969
|
+
{ state: { equals: "pending" } },
|
|
8970
|
+
{ runAt: { lt: new Date(now.getTime() - 10 * 6e4).toISOString() } }
|
|
8971
|
+
]
|
|
8972
|
+
}
|
|
8973
|
+
});
|
|
8974
|
+
}
|
|
8975
|
+
this.telemetry?.queuesCounted(queues, late);
|
|
8976
|
+
}
|
|
8657
8977
|
async recordJobRun(name, at, error, ms) {
|
|
8658
8978
|
const { docs } = await this.db.find({
|
|
8659
8979
|
collection: JOB_RUNS,
|
|
@@ -10750,9 +11070,6 @@ async function readAdminModule(cms, index) {
|
|
|
10750
11070
|
return { body, etag: `"${createHash6("sha1").update(body).digest("base64url")}"` };
|
|
10751
11071
|
}
|
|
10752
11072
|
|
|
10753
|
-
// src/version.ts
|
|
10754
|
-
var VERSION = true ? "1.14.0" : "0.0.0-dev";
|
|
10755
|
-
|
|
10756
11073
|
// src/rest/handler.ts
|
|
10757
11074
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
10758
11075
|
|
|
@@ -10881,7 +11198,6 @@ async function deleteFailedDeliveries(cms, kind) {
|
|
|
10881
11198
|
}
|
|
10882
11199
|
|
|
10883
11200
|
// src/rest/admin-email.ts
|
|
10884
|
-
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
10885
11201
|
var LIMIT = 5;
|
|
10886
11202
|
var WINDOW = 10 * 6e4;
|
|
10887
11203
|
var sentAt = /* @__PURE__ */ new WeakMap();
|
|
@@ -10930,7 +11246,7 @@ async function sendTestEmail(cms, user, to, locale) {
|
|
|
10930
11246
|
const adapter = cms.config.email;
|
|
10931
11247
|
if (!adapter) return { ok: false, error: "Email is not set up (`email` in the config)" };
|
|
10932
11248
|
const address = typeof to === "string" && to.trim() ? to.trim() : user.email;
|
|
10933
|
-
if (!
|
|
11249
|
+
if (!isEmail(address))
|
|
10934
11250
|
throw new ValidationError("email", [{ field: "to", message: "must be an email address" }]);
|
|
10935
11251
|
const times = sentAt.get(cms) ?? /* @__PURE__ */ new Map();
|
|
10936
11252
|
sentAt.set(cms, times);
|
|
@@ -11528,6 +11844,7 @@ async function adminStatus(cms) {
|
|
|
11528
11844
|
storage: config.upload.storage?.name ?? "local",
|
|
11529
11845
|
email: config.email ? config.email.name ?? "custom" : null,
|
|
11530
11846
|
search: config.search ? config.search.name ?? "custom" : null,
|
|
11847
|
+
telemetry: !!config.telemetry,
|
|
11531
11848
|
plugins: config.installedPlugins.map((p) => ({ ...p })),
|
|
11532
11849
|
fieldTypes: config.fieldTypes.map((t) => t.name)
|
|
11533
11850
|
},
|
|
@@ -11726,7 +12043,14 @@ var HEALTH_CHECKS = /* @__PURE__ */ new Map([
|
|
|
11726
12043
|
["readyz", "readyz"]
|
|
11727
12044
|
]);
|
|
11728
12045
|
var REQUEST_ID = /^[\w.-]{1,128}$/;
|
|
11729
|
-
|
|
12046
|
+
function respond(cms, request, url, path, root, options, production) {
|
|
12047
|
+
const run = () => respondLogged(cms, request, url, path, root, options, production);
|
|
12048
|
+
const probe = !root && (path === "/healthz" || path === "/readyz");
|
|
12049
|
+
if (!cms.telemetry || probe) return run();
|
|
12050
|
+
const route2 = root || path === null ? routeOf(path, root) : `${cms.config.routes.api}${routeOf(path, root)}`;
|
|
12051
|
+
return cms.telemetry.request(request.method, route2, run);
|
|
12052
|
+
}
|
|
12053
|
+
async function respondLogged(cms, request, url, path, root, options, production) {
|
|
11730
12054
|
const started = performance.now();
|
|
11731
12055
|
const incoming = request.headers.get("x-request-id");
|
|
11732
12056
|
const requestId = incoming && REQUEST_ID.test(incoming) ? incoming : randomUUID4();
|
|
@@ -13044,13 +13368,14 @@ function errorResponse(cms, error, headers, production) {
|
|
|
13044
13368
|
}
|
|
13045
13369
|
return new Response(JSON.stringify({ errors }), { status: error.status, headers });
|
|
13046
13370
|
}
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
|
|
13371
|
+
cms.logger.error(
|
|
13372
|
+
`REST request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
13373
|
+
{
|
|
13374
|
+
error
|
|
13375
|
+
}
|
|
13376
|
+
);
|
|
13377
|
+
const message2 = !production && error instanceof Error ? error.message : "Internal Server Error";
|
|
13378
|
+
const body = { errors: [{ message: message2, code: "INTERNAL_SERVER_ERROR" }] };
|
|
13054
13379
|
return new Response(JSON.stringify(body), { status: 500, headers });
|
|
13055
13380
|
}
|
|
13056
13381
|
|
|
@@ -13186,6 +13511,7 @@ export {
|
|
|
13186
13511
|
SSO_COOKIE,
|
|
13187
13512
|
Auth,
|
|
13188
13513
|
ADAPTER_API_VERSION,
|
|
13514
|
+
isEmail,
|
|
13189
13515
|
slugify,
|
|
13190
13516
|
applyDefaults,
|
|
13191
13517
|
fillMissing,
|
|
@@ -13227,10 +13553,10 @@ export {
|
|
|
13227
13553
|
applyPlugins,
|
|
13228
13554
|
resolveConfig,
|
|
13229
13555
|
cacheTags,
|
|
13556
|
+
VERSION,
|
|
13230
13557
|
createEasyCMS,
|
|
13231
13558
|
EasyCMS,
|
|
13232
13559
|
resolveAdminModule,
|
|
13233
|
-
VERSION,
|
|
13234
13560
|
CSRF_COOKIE,
|
|
13235
13561
|
CSRF_HEADER,
|
|
13236
13562
|
createRestHandler,
|