@easy-cms/core 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-TMO4JTGJ.js → chunk-244WCY6S.js} +492 -86
- package/dist/{config-n7PtGl4V.d.ts → config-CHEEPk1d.d.ts} +61 -7
- package/dist/{handler-DV-1ICVz.d.ts → handler-adKUq8-U.d.ts} +1 -1
- package/dist/index.d.ts +29 -6
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +1 -1
- package/dist/{plugin-C_vknKa2.d.ts → plugin-B8xypoWd.d.ts} +1 -1
- package/dist/plugin.d.ts +2 -2
- package/package.json +1 -1
|
@@ -59,6 +59,7 @@ var JOB_RUNS = "job-runs";
|
|
|
59
59
|
var ROLES = "user-roles";
|
|
60
60
|
var USER_IDENTITIES = "user-identities";
|
|
61
61
|
var AUDIT_LOGS = "audit-logs";
|
|
62
|
+
var OUTBOX = "outbox";
|
|
62
63
|
var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
63
64
|
SESSIONS,
|
|
64
65
|
LOGIN_ATTEMPTS,
|
|
@@ -70,7 +71,8 @@ var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
|
70
71
|
JOB_RUNS,
|
|
71
72
|
ROLES,
|
|
72
73
|
USER_IDENTITIES,
|
|
73
|
-
AUDIT_LOGS
|
|
74
|
+
AUDIT_LOGS,
|
|
75
|
+
OUTBOX
|
|
74
76
|
]);
|
|
75
77
|
var DEFAULT_ROLES = ["admin", "editor"];
|
|
76
78
|
var CREATED_BY_FIELD = {
|
|
@@ -519,6 +521,31 @@ var jobRunsCollection = {
|
|
|
519
521
|
{ name: "changedAt", type: "text" }
|
|
520
522
|
]
|
|
521
523
|
};
|
|
524
|
+
var outboxCollection = {
|
|
525
|
+
slug: OUTBOX,
|
|
526
|
+
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
527
|
+
fields: [
|
|
528
|
+
// create | update | draft | delete, as the webhook event.
|
|
529
|
+
{ name: "event", type: "text", required: true },
|
|
530
|
+
// The collection's or the global's slug.
|
|
531
|
+
{ name: "target", type: "text", required: true },
|
|
532
|
+
{ name: "global", type: "boolean" },
|
|
533
|
+
{ name: "docId", type: "text" },
|
|
534
|
+
// The stored status before the save, for publish and unpublish events.
|
|
535
|
+
{ name: "before", type: "text" },
|
|
536
|
+
// JSON: the document as saved (as deleted), the one before, and the user who saved.
|
|
537
|
+
{ name: "doc", type: "textarea", required: true },
|
|
538
|
+
{ name: "previousDoc", type: "textarea" },
|
|
539
|
+
{ name: "user", type: "textarea" },
|
|
540
|
+
// Steps already done (hooks, webhooks, purge), so a retry runs only the rest.
|
|
541
|
+
{ name: "done", type: "text" },
|
|
542
|
+
{ name: "attempts", type: "number", required: true },
|
|
543
|
+
{ name: "nextAttemptAt", type: "text", required: true, index: true },
|
|
544
|
+
// pending | running | failed
|
|
545
|
+
{ name: "state", type: "text", required: true, index: true },
|
|
546
|
+
{ name: "error", type: "text" }
|
|
547
|
+
]
|
|
548
|
+
};
|
|
522
549
|
var emailDeliveriesCollection = {
|
|
523
550
|
slug: EMAIL_DELIVERIES,
|
|
524
551
|
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
@@ -572,8 +599,8 @@ function cut(value) {
|
|
|
572
599
|
if (typeof value === "string")
|
|
573
600
|
return value.length > MAX_VALUE ? `${value.slice(0, MAX_VALUE)}\u2026` : value;
|
|
574
601
|
if (value !== null && typeof value === "object") {
|
|
575
|
-
const
|
|
576
|
-
return
|
|
602
|
+
const json2 = JSON.stringify(value);
|
|
603
|
+
return json2.length > MAX_VALUE ? "\u2026" : value;
|
|
577
604
|
}
|
|
578
605
|
return value;
|
|
579
606
|
}
|
|
@@ -3797,7 +3824,8 @@ var VersionStore = class {
|
|
|
3797
3824
|
snapshot: args.snapshot,
|
|
3798
3825
|
createdAt: now,
|
|
3799
3826
|
updatedAt: now
|
|
3800
|
-
}
|
|
3827
|
+
},
|
|
3828
|
+
outbox: args.outbox
|
|
3801
3829
|
});
|
|
3802
3830
|
await this.prune(parent, doc, args.max);
|
|
3803
3831
|
return toSummary(created);
|
|
@@ -4187,6 +4215,189 @@ function localizeSort(fields, sort, locale, locales = []) {
|
|
|
4187
4215
|
);
|
|
4188
4216
|
}
|
|
4189
4217
|
|
|
4218
|
+
// src/outbox.ts
|
|
4219
|
+
var DELAYS = [6e4, 5 * 6e4, 30 * 6e4, 2 * 36e5, 12 * 36e5];
|
|
4220
|
+
var LEASE = 5 * 6e4;
|
|
4221
|
+
var BATCH2 = 50;
|
|
4222
|
+
var later2 = (from, ms) => new Date(from.getTime() + ms).toISOString();
|
|
4223
|
+
var json = (value) => value === void 0 ? null : JSON.stringify(value);
|
|
4224
|
+
var parse2 = (value) => typeof value === "string" ? JSON.parse(value) : void 0;
|
|
4225
|
+
function withoutHash(value) {
|
|
4226
|
+
if (!value || !("passwordHash" in value)) return value;
|
|
4227
|
+
const { passwordHash: _, ...rest } = value;
|
|
4228
|
+
return rest;
|
|
4229
|
+
}
|
|
4230
|
+
var Outbox = class {
|
|
4231
|
+
constructor(db, logger, runner) {
|
|
4232
|
+
this.db = db;
|
|
4233
|
+
this.logger = logger;
|
|
4234
|
+
this.runner = runner;
|
|
4235
|
+
}
|
|
4236
|
+
db;
|
|
4237
|
+
logger;
|
|
4238
|
+
runner;
|
|
4239
|
+
draining;
|
|
4240
|
+
again = false;
|
|
4241
|
+
/** The row a save writes. For a create, `doc` has no id yet: it gets the written one. */
|
|
4242
|
+
row(entry) {
|
|
4243
|
+
return {
|
|
4244
|
+
collection: OUTBOX,
|
|
4245
|
+
data: (id) => {
|
|
4246
|
+
const docId = entry.global ? null : String(entry.docId ?? id);
|
|
4247
|
+
const doc = entry.event === "create" ? { ...entry.doc, id } : entry.doc;
|
|
4248
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4249
|
+
return {
|
|
4250
|
+
event: entry.event,
|
|
4251
|
+
target: entry.target,
|
|
4252
|
+
global: entry.global,
|
|
4253
|
+
docId,
|
|
4254
|
+
before: entry.before,
|
|
4255
|
+
doc: JSON.stringify(withoutHash(doc)),
|
|
4256
|
+
previousDoc: json(withoutHash(entry.previousDoc)),
|
|
4257
|
+
user: json(withoutHash(entry.user)),
|
|
4258
|
+
done: null,
|
|
4259
|
+
attempts: 0,
|
|
4260
|
+
nextAttemptAt: now,
|
|
4261
|
+
state: "pending",
|
|
4262
|
+
error: null,
|
|
4263
|
+
createdAt: now,
|
|
4264
|
+
updatedAt: now
|
|
4265
|
+
};
|
|
4266
|
+
}
|
|
4267
|
+
};
|
|
4268
|
+
}
|
|
4269
|
+
/** Runs what is due in the background, e.g. right after a save commits. */
|
|
4270
|
+
kick() {
|
|
4271
|
+
if (this.draining) {
|
|
4272
|
+
this.again = true;
|
|
4273
|
+
return;
|
|
4274
|
+
}
|
|
4275
|
+
this.draining = (async () => {
|
|
4276
|
+
do {
|
|
4277
|
+
this.again = false;
|
|
4278
|
+
await this.run();
|
|
4279
|
+
} while (this.again);
|
|
4280
|
+
})().catch((error) => this.logger.error(`Outbox: ${error.message}`, { error })).finally(() => {
|
|
4281
|
+
this.draining = void 0;
|
|
4282
|
+
});
|
|
4283
|
+
}
|
|
4284
|
+
/** Waits until the background run started by `kick` is over. */
|
|
4285
|
+
async flush() {
|
|
4286
|
+
while (this.draining) await this.draining;
|
|
4287
|
+
}
|
|
4288
|
+
/**
|
|
4289
|
+
* Runs entries that are due: new ones, retries whose time came, and those a stopped worker
|
|
4290
|
+
* left claimed. Runs with scheduled jobs too, which catches what `kick` missed.
|
|
4291
|
+
*/
|
|
4292
|
+
async run(now = /* @__PURE__ */ new Date()) {
|
|
4293
|
+
let ran = 0;
|
|
4294
|
+
let failed = 0;
|
|
4295
|
+
for (; ; ) {
|
|
4296
|
+
const { docs } = await this.db.find({
|
|
4297
|
+
collection: OUTBOX,
|
|
4298
|
+
where: {
|
|
4299
|
+
and: [
|
|
4300
|
+
{ state: { in: ["pending", "running"] } },
|
|
4301
|
+
{ nextAttemptAt: { lte: now.toISOString() } }
|
|
4302
|
+
]
|
|
4303
|
+
},
|
|
4304
|
+
sort: ["nextAttemptAt", "id"],
|
|
4305
|
+
limit: BATCH2,
|
|
4306
|
+
page: 1
|
|
4307
|
+
});
|
|
4308
|
+
let claimed = 0;
|
|
4309
|
+
for (const row of docs) {
|
|
4310
|
+
const result = await this.runRow(row, now);
|
|
4311
|
+
if (result === void 0) continue;
|
|
4312
|
+
claimed++;
|
|
4313
|
+
if (result) ran++;
|
|
4314
|
+
else failed++;
|
|
4315
|
+
}
|
|
4316
|
+
if (docs.length < BATCH2 || claimed === 0) return { ran, failed };
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
/** Puts a failed entry back in line, e.g. from the admin. */
|
|
4320
|
+
async retry(id) {
|
|
4321
|
+
const row = await this.db.findById({ collection: OUTBOX, id });
|
|
4322
|
+
if (!row || row.state !== "failed") return false;
|
|
4323
|
+
const { id: _, ...data } = row;
|
|
4324
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4325
|
+
const updated = await this.db.update({
|
|
4326
|
+
collection: OUTBOX,
|
|
4327
|
+
id,
|
|
4328
|
+
data: { ...data, state: "pending", attempts: 0, nextAttemptAt: now, updatedAt: now },
|
|
4329
|
+
where: { state: { equals: "failed" } }
|
|
4330
|
+
});
|
|
4331
|
+
if (updated) this.kick();
|
|
4332
|
+
return updated !== null;
|
|
4333
|
+
}
|
|
4334
|
+
/** `true` when it ran, `false` when it failed, `undefined` when another worker has it. */
|
|
4335
|
+
async runRow(row, now) {
|
|
4336
|
+
const { id, ...data } = row;
|
|
4337
|
+
const attempts = Number(data.attempts ?? 0) + 1;
|
|
4338
|
+
let current = {
|
|
4339
|
+
...data,
|
|
4340
|
+
state: "running",
|
|
4341
|
+
attempts,
|
|
4342
|
+
nextAttemptAt: later2(now, LEASE),
|
|
4343
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4344
|
+
};
|
|
4345
|
+
const claimed = await this.db.update({
|
|
4346
|
+
collection: OUTBOX,
|
|
4347
|
+
id,
|
|
4348
|
+
data: current,
|
|
4349
|
+
where: {
|
|
4350
|
+
and: [{ state: { equals: data.state } }, { nextAttemptAt: { equals: data.nextAttemptAt } }]
|
|
4351
|
+
}
|
|
4352
|
+
});
|
|
4353
|
+
if (!claimed) return void 0;
|
|
4354
|
+
const done = new Set(
|
|
4355
|
+
String(data.done ?? "").split(",").filter(Boolean)
|
|
4356
|
+
);
|
|
4357
|
+
const mark = async (step) => {
|
|
4358
|
+
done.add(step);
|
|
4359
|
+
current = { ...current, done: [...done].join(","), updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4360
|
+
await this.db.update({ collection: OUTBOX, id, data: current });
|
|
4361
|
+
};
|
|
4362
|
+
try {
|
|
4363
|
+
await this.runner(entryOf(data), done, mark);
|
|
4364
|
+
await this.db.delete({ collection: OUTBOX, id });
|
|
4365
|
+
return true;
|
|
4366
|
+
} catch (error) {
|
|
4367
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
4368
|
+
const delay = DELAYS[attempts - 1];
|
|
4369
|
+
await this.db.update({
|
|
4370
|
+
collection: OUTBOX,
|
|
4371
|
+
id,
|
|
4372
|
+
data: {
|
|
4373
|
+
...current,
|
|
4374
|
+
state: delay === void 0 ? "failed" : "pending",
|
|
4375
|
+
nextAttemptAt: later2(now, delay ?? 0),
|
|
4376
|
+
error: message2,
|
|
4377
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4378
|
+
}
|
|
4379
|
+
});
|
|
4380
|
+
const what = `${data.event} of ${data.global ? "global " : ""}"${data.target}"`;
|
|
4381
|
+
if (delay === void 0)
|
|
4382
|
+
this.logger.error(`Outbox gave up on ${what} after ${attempts} attempts: ${message2}`);
|
|
4383
|
+
else this.logger.warn(`Outbox: ${what} failed (${message2}); will retry`);
|
|
4384
|
+
return false;
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
};
|
|
4388
|
+
function entryOf(row) {
|
|
4389
|
+
return {
|
|
4390
|
+
event: row.event,
|
|
4391
|
+
target: String(row.target),
|
|
4392
|
+
global: row.global === true,
|
|
4393
|
+
docId: typeof row.docId === "string" ? row.docId : null,
|
|
4394
|
+
before: typeof row.before === "string" ? row.before : null,
|
|
4395
|
+
doc: parse2(row.doc) ?? {},
|
|
4396
|
+
previousDoc: parse2(row.previousDoc),
|
|
4397
|
+
user: parse2(row.user) ?? null
|
|
4398
|
+
};
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4190
4401
|
// src/remote-file.ts
|
|
4191
4402
|
import { lookup as dnsLookup } from "dns";
|
|
4192
4403
|
import { request as httpRequest } from "http";
|
|
@@ -4426,7 +4637,7 @@ function read(response, maxBytes, deadline) {
|
|
|
4426
4637
|
var keys = (list) => new Set(list.split(/\s+/).filter(Boolean));
|
|
4427
4638
|
var CONFIG = keys(`secret db serverURL cors cronSecret timeZone webhooks events jobs localization routes admin
|
|
4428
4639
|
upload auth collections globals endpoints cliCommands onRequest fieldTypes apiKeys email backups
|
|
4429
|
-
audit logs countCacheSeconds maxPage httpCache plugins`);
|
|
4640
|
+
audit logs countCacheSeconds maxPage httpCache outbox plugins`);
|
|
4430
4641
|
var ADMIN = keys("path locale brand siteURL nav commands modules pages dashboard switcher");
|
|
4431
4642
|
var AUTH = keys(`roles rbac tokenExpiration maxLoginAttempts lockWindow trustedOrigins
|
|
4432
4643
|
resetPasswordExpiration inviteExpiration providers providerSignUp password setupCode members emails`);
|
|
@@ -4694,6 +4905,23 @@ var Webhooks = class {
|
|
|
4694
4905
|
this.track(this.deliver(hook, payload));
|
|
4695
4906
|
}
|
|
4696
4907
|
}
|
|
4908
|
+
/**
|
|
4909
|
+
* `emit` for the outbox: resolves once each delivery is saved in the queue (sent, without a
|
|
4910
|
+
* queue), so the event can count as done. Throws when saving fails; nothing is sent then.
|
|
4911
|
+
*/
|
|
4912
|
+
async emitSaved(event, target, doc) {
|
|
4913
|
+
const payload = { event, ...target, doc, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4914
|
+
for (const hook of this.hooks) {
|
|
4915
|
+
if (!matches(hook, payload)) continue;
|
|
4916
|
+
if (!this.queue) {
|
|
4917
|
+
await this.deliver(hook, payload);
|
|
4918
|
+
continue;
|
|
4919
|
+
}
|
|
4920
|
+
await new Promise(
|
|
4921
|
+
(saved, failed) => this.track(this.deliver(hook, payload, { saved: () => saved(), failed }))
|
|
4922
|
+
);
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4697
4925
|
/** Waits for deliveries in progress, e.g. before a serverless function returns. */
|
|
4698
4926
|
async flush() {
|
|
4699
4927
|
while (this.pending.size > 0) await Promise.allSettled([...this.pending]);
|
|
@@ -4717,7 +4945,7 @@ var Webhooks = class {
|
|
|
4717
4945
|
const claimed = await this.queue.claim(id, queued, {
|
|
4718
4946
|
...queued,
|
|
4719
4947
|
attempts,
|
|
4720
|
-
nextAttemptAt:
|
|
4948
|
+
nextAttemptAt: later3(now, CLAIM2)
|
|
4721
4949
|
});
|
|
4722
4950
|
if (!claimed) continue;
|
|
4723
4951
|
const result = await this.attempt(hook, queued.event, queued.body, queued.delivery);
|
|
@@ -4731,7 +4959,7 @@ var Webhooks = class {
|
|
|
4731
4959
|
await this.queue.update(id, {
|
|
4732
4960
|
...queued,
|
|
4733
4961
|
attempts,
|
|
4734
|
-
nextAttemptAt:
|
|
4962
|
+
nextAttemptAt: later3(now, delay ?? 0),
|
|
4735
4963
|
state: delay === void 0 ? "failed" : "pending",
|
|
4736
4964
|
error: result.error
|
|
4737
4965
|
});
|
|
@@ -4754,7 +4982,7 @@ var Webhooks = class {
|
|
|
4754
4982
|
).finally(() => this.pending.delete(tracked));
|
|
4755
4983
|
this.pending.add(tracked);
|
|
4756
4984
|
}
|
|
4757
|
-
async deliver(hook, payload) {
|
|
4985
|
+
async deliver(hook, payload, saving) {
|
|
4758
4986
|
const delivery = randomUUID2();
|
|
4759
4987
|
const body = JSON.stringify(payload);
|
|
4760
4988
|
const { event } = payload;
|
|
@@ -4769,13 +4997,15 @@ var Webhooks = class {
|
|
|
4769
4997
|
error
|
|
4770
4998
|
});
|
|
4771
4999
|
const { queue } = this;
|
|
4772
|
-
const saved = queue ? this.saving.then(() => queue.add(entry(0,
|
|
5000
|
+
const saved = queue ? this.saving.then(() => queue.add(entry(0, later3(/* @__PURE__ */ new Date(), CLAIM2), null))) : Promise.resolve(void 0);
|
|
4773
5001
|
this.saving = saved.catch(() => {
|
|
4774
5002
|
});
|
|
4775
5003
|
let queued;
|
|
4776
5004
|
try {
|
|
4777
5005
|
queued = await saved;
|
|
5006
|
+
saving?.saved();
|
|
4778
5007
|
} catch (error) {
|
|
5008
|
+
if (saving) return saving.failed(error);
|
|
4779
5009
|
this.logger.error(`Could not save webhook delivery: ${error.message}`, { error });
|
|
4780
5010
|
}
|
|
4781
5011
|
for (let attempt = 0; ; attempt++) {
|
|
@@ -4788,7 +5018,7 @@ var Webhooks = class {
|
|
|
4788
5018
|
if (delay === void 0) {
|
|
4789
5019
|
if (queued === void 0 || !this.queue)
|
|
4790
5020
|
return this.giveUp(hook, event, result.error, attempt + 1);
|
|
4791
|
-
const next =
|
|
5021
|
+
const next = later3(/* @__PURE__ */ new Date(), QUEUE_DELAYS2[0]);
|
|
4792
5022
|
await this.queue.update(queued, entry(attempt + 1, next, result.error));
|
|
4793
5023
|
this.logger.warn(
|
|
4794
5024
|
`Webhook ${hook.url} failed for ${event} (${result.error}); will retry later`
|
|
@@ -4833,7 +5063,7 @@ var Webhooks = class {
|
|
|
4833
5063
|
);
|
|
4834
5064
|
}
|
|
4835
5065
|
};
|
|
4836
|
-
var
|
|
5066
|
+
var later3 = (from, ms) => new Date(from.getTime() + ms).toISOString();
|
|
4837
5067
|
var isContentEvent = (event) => WEBHOOK_EVENTS.includes(event);
|
|
4838
5068
|
function matches(hook, payload) {
|
|
4839
5069
|
if (!isContentEvent(payload.event)) return hook.events?.includes(payload.event) === true;
|
|
@@ -4861,6 +5091,7 @@ var RESERVED_SLUGS = /* @__PURE__ */ new Set([
|
|
|
4861
5091
|
"audit-logs",
|
|
4862
5092
|
"api-keys",
|
|
4863
5093
|
"jobs",
|
|
5094
|
+
"outbox",
|
|
4864
5095
|
"auth",
|
|
4865
5096
|
"migrations",
|
|
4866
5097
|
"access",
|
|
@@ -4892,6 +5123,7 @@ function validateConfig(config) {
|
|
|
4892
5123
|
validateAudit(config, add);
|
|
4893
5124
|
validateLogs(config, add);
|
|
4894
5125
|
validateCountCache(config, add);
|
|
5126
|
+
validateOutbox(config, add);
|
|
4895
5127
|
validateMaxPage(config, add);
|
|
4896
5128
|
validateHttpCache(config, add);
|
|
4897
5129
|
validateAuth(config, add);
|
|
@@ -5541,6 +5773,10 @@ function validateMaxPage(config, add) {
|
|
|
5541
5773
|
if (max !== void 0 && !(Number.isInteger(max) && max >= 1))
|
|
5542
5774
|
add("maxPage", "must be a whole number, 1 or more");
|
|
5543
5775
|
}
|
|
5776
|
+
function validateOutbox(config, add) {
|
|
5777
|
+
if (config.outbox !== void 0 && typeof config.outbox !== "boolean")
|
|
5778
|
+
add("outbox", "must be true or false");
|
|
5779
|
+
}
|
|
5544
5780
|
function validateCountCache(config, add) {
|
|
5545
5781
|
const seconds = config.countCacheSeconds;
|
|
5546
5782
|
if (seconds !== void 0 && !(typeof seconds === "number" && Number.isFinite(seconds) && seconds >= 0))
|
|
@@ -6402,6 +6638,7 @@ async function resolveConfig(input) {
|
|
|
6402
6638
|
...config.apiKeys ? [apiKeysCollection] : [],
|
|
6403
6639
|
...config.email ? [emailDeliveriesCollection] : [],
|
|
6404
6640
|
...(config.jobs?.length ?? 0) > 0 ? [jobRunsCollection] : [],
|
|
6641
|
+
...config.outbox ? [outboxCollection] : [],
|
|
6405
6642
|
databaseBackupsCollection,
|
|
6406
6643
|
...config.auth?.rbac ? [rolesCollection] : [],
|
|
6407
6644
|
...(config.auth?.providers?.length ?? 0) > 0 ? [userIdentitiesCollection] : [],
|
|
@@ -6490,8 +6727,18 @@ async function createEasyCMS(config, options = {}) {
|
|
|
6490
6727
|
logger,
|
|
6491
6728
|
interactive: options.interactive ?? false
|
|
6492
6729
|
});
|
|
6730
|
+
if (resolved3.outbox && db.outbox !== true) {
|
|
6731
|
+
await db.destroy();
|
|
6732
|
+
throw new ConfigError([
|
|
6733
|
+
{
|
|
6734
|
+
path: "outbox",
|
|
6735
|
+
message: `needs a database adapter that writes outbox rows; "${resolved3.db.name}" doesn't`,
|
|
6736
|
+
hint: "use @easy-cms/db-sqlite or @easy-cms/db-postgres 1.12 or later"
|
|
6737
|
+
}
|
|
6738
|
+
]);
|
|
6739
|
+
}
|
|
6493
6740
|
const cms = new EasyCMS(resolved3, db, logger, storage, cwd);
|
|
6494
|
-
const scheduling = [...resolved3.collections, ...resolved3.globals].some((c) => c.schedule) || (resolved3.webhooks?.length ?? 0) > 0 || resolved3.email !== void 0 || resolved3.jobs.length > 0;
|
|
6741
|
+
const scheduling = [...resolved3.collections, ...resolved3.globals].some((c) => c.schedule) || (resolved3.webhooks?.length ?? 0) > 0 || resolved3.email !== void 0 || resolved3.jobs.length > 0 || resolved3.outbox === true;
|
|
6495
6742
|
if (scheduling && options.scheduler !== false) cms.startScheduler();
|
|
6496
6743
|
if (process.env.NODE_ENV === "production" && !resolved3.auth.setupCode && await cms.auth.hasUsers().catch(() => true) === false)
|
|
6497
6744
|
logger.warn(
|
|
@@ -6531,6 +6778,8 @@ var EasyCMS = class {
|
|
|
6531
6778
|
versions;
|
|
6532
6779
|
webhooks;
|
|
6533
6780
|
mailer;
|
|
6781
|
+
/** With `outbox: true`: runs after hooks and events from rows saves write. */
|
|
6782
|
+
outbox;
|
|
6534
6783
|
/** @internal */
|
|
6535
6784
|
constructor(config, db, logger = consoleLogger, storage = diskStorage({ dir: config.upload.dir }), cwd = process.cwd(), privateStorage) {
|
|
6536
6785
|
this.config = config;
|
|
@@ -6558,6 +6807,7 @@ var EasyCMS = class {
|
|
|
6558
6807
|
logger,
|
|
6559
6808
|
config.collections.some((c) => c.slug === EMAIL_DELIVERIES) ? this.emailQueue() : void 0
|
|
6560
6809
|
);
|
|
6810
|
+
this.outbox = config.collections.some((c) => c.slug === OUTBOX) ? new Outbox(db, logger, (entry, done, mark) => this.runOutbox(entry, done, mark)) : void 0;
|
|
6561
6811
|
this.auth = new Auth(this);
|
|
6562
6812
|
}
|
|
6563
6813
|
/**
|
|
@@ -7427,14 +7677,21 @@ var EasyCMS = class {
|
|
|
7427
7677
|
]);
|
|
7428
7678
|
}
|
|
7429
7679
|
const draft = { ...prepared, id: parsed, createdAt: existing.createdAt, updatedAt: now };
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
doc: draft,
|
|
7434
|
-
previousDoc: current,
|
|
7435
|
-
operation: "update"
|
|
7680
|
+
const outbox2 = this.outboxRow(config, "draft", guard, draft, {
|
|
7681
|
+
docId: parsed,
|
|
7682
|
+
previousDoc: current
|
|
7436
7683
|
});
|
|
7437
|
-
this.
|
|
7684
|
+
await this.saveVersion(config, collectionParent(collection2), parsed, draft, guard, outbox2);
|
|
7685
|
+
if (outbox2) this.outbox?.kick();
|
|
7686
|
+
else {
|
|
7687
|
+
await this.notify(config.hooks?.afterChange, "afterChange", config.slug, {
|
|
7688
|
+
...base,
|
|
7689
|
+
doc: draft,
|
|
7690
|
+
previousDoc: current,
|
|
7691
|
+
operation: "update"
|
|
7692
|
+
});
|
|
7693
|
+
this.emitChange(config, "draft", draft);
|
|
7694
|
+
}
|
|
7438
7695
|
await this.audited(config, "draft", draft, current, guard, mode);
|
|
7439
7696
|
const [out2] = await this.output(config, [draft], guard, {
|
|
7440
7697
|
...options,
|
|
@@ -7463,7 +7720,18 @@ var EasyCMS = class {
|
|
|
7463
7720
|
}
|
|
7464
7721
|
}
|
|
7465
7722
|
const data = { ...prepared, createdAt: existing.createdAt, updatedAt: now };
|
|
7466
|
-
const
|
|
7723
|
+
const outbox = this.outboxRow(
|
|
7724
|
+
config,
|
|
7725
|
+
"update",
|
|
7726
|
+
guard,
|
|
7727
|
+
{ ...data, id: parsed },
|
|
7728
|
+
{
|
|
7729
|
+
docId: parsed,
|
|
7730
|
+
previousDoc: current,
|
|
7731
|
+
before: existing.status
|
|
7732
|
+
}
|
|
7733
|
+
);
|
|
7734
|
+
const doc = options.where ? await this.db.update({ collection: collection2, id: parsed, data, where: options.where, outbox }) : await this.db.update({ collection: collection2, id: parsed, data, outbox });
|
|
7467
7735
|
if (!doc) return null;
|
|
7468
7736
|
if (moved) await moved.cleanUp();
|
|
7469
7737
|
if (collection2 === MEDIA_FOLDERS) {
|
|
@@ -7473,13 +7741,16 @@ var EasyCMS = class {
|
|
|
7473
7741
|
}
|
|
7474
7742
|
if (!live) await this.saveVersion(config, collectionParent(collection2), parsed, doc, guard);
|
|
7475
7743
|
if (password !== void 0) await this.auth.revokeSessions(parsed);
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7744
|
+
if (outbox) this.outbox?.kick();
|
|
7745
|
+
else {
|
|
7746
|
+
await this.notify(config.hooks?.afterChange, "afterChange", config.slug, {
|
|
7747
|
+
...base,
|
|
7748
|
+
doc,
|
|
7749
|
+
previousDoc: current,
|
|
7750
|
+
operation: "update"
|
|
7751
|
+
});
|
|
7752
|
+
this.emitChange(config, "update", doc, existing.status);
|
|
7753
|
+
}
|
|
7483
7754
|
await this.audited(config, "update", doc, current, guard, mode);
|
|
7484
7755
|
const [out] = await this.output(config, [doc], guard, { ...options, draft: true });
|
|
7485
7756
|
return out;
|
|
@@ -7517,7 +7788,8 @@ var EasyCMS = class {
|
|
|
7517
7788
|
await this.checkMoveLimit(await this.folders.privacyAfter(parsed, { private: false }));
|
|
7518
7789
|
await this.folders.release(existing);
|
|
7519
7790
|
}
|
|
7520
|
-
|
|
7791
|
+
const outbox = this.outboxRow(config, "delete", guard, existing, { docId: parsed });
|
|
7792
|
+
await this.db.delete({ collection: collection2, id: parsed, outbox });
|
|
7521
7793
|
if (collection2 === MEDIA_FOLDERS) {
|
|
7522
7794
|
this.folders.invalidate();
|
|
7523
7795
|
const parent = existing.parent ?? null;
|
|
@@ -7529,12 +7801,15 @@ var EasyCMS = class {
|
|
|
7529
7801
|
for (const job of await this.pendingJobs(collection2, parsed))
|
|
7530
7802
|
await this.db.delete({ collection: SCHEDULED_JOBS, id: job.id });
|
|
7531
7803
|
}
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7804
|
+
if (outbox) this.outbox?.kick();
|
|
7805
|
+
else {
|
|
7806
|
+
await this.notify(config.hooks?.afterDelete, "afterDelete", config.slug, {
|
|
7807
|
+
...base,
|
|
7808
|
+
id: parsed,
|
|
7809
|
+
doc: existing
|
|
7810
|
+
});
|
|
7811
|
+
this.emitChange(config, "delete", existing);
|
|
7812
|
+
}
|
|
7538
7813
|
await this.audited(config, "delete", existing, existing, guard);
|
|
7539
7814
|
const [out] = await this.output(config, [existing], guard, { depth: 0, draft: true });
|
|
7540
7815
|
return out;
|
|
@@ -7691,26 +7966,38 @@ var EasyCMS = class {
|
|
|
7691
7966
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7692
7967
|
if (mode !== "unpublish" && this.separateDrafts(config) && existing.status === "published" && prepared.status === "draft") {
|
|
7693
7968
|
const draft = { ...prepared, updatedAt: now };
|
|
7694
|
-
|
|
7969
|
+
const outbox2 = this.outboxRow(config, "draft", guard, draft, { previousDoc: current });
|
|
7970
|
+
await this.saveVersion(config, globalParent(key), 0, draft, guard, outbox2);
|
|
7971
|
+
if (outbox2) this.outbox?.kick();
|
|
7972
|
+
else {
|
|
7973
|
+
await this.notify(config.hooks?.afterChange, "afterChange", slug2, {
|
|
7974
|
+
...base,
|
|
7975
|
+
doc: draft,
|
|
7976
|
+
previousDoc: current,
|
|
7977
|
+
operation: "update"
|
|
7978
|
+
});
|
|
7979
|
+
this.emitChange(config, "draft", draft);
|
|
7980
|
+
}
|
|
7981
|
+
await this.audited(config, "draft", draft, current, guard, mode);
|
|
7982
|
+
return this.readGlobal(config, key, guard, { ...options, draft: true });
|
|
7983
|
+
}
|
|
7984
|
+
const data = { ...prepared, updatedAt: now };
|
|
7985
|
+
const outbox = this.outboxRow(config, "update", guard, data, {
|
|
7986
|
+
previousDoc: current,
|
|
7987
|
+
before: existing.status
|
|
7988
|
+
});
|
|
7989
|
+
const doc = await this.db.updateGlobal({ slug: key, data, outbox });
|
|
7990
|
+
await this.saveVersion(config, globalParent(key), 0, doc, guard);
|
|
7991
|
+
if (outbox) this.outbox?.kick();
|
|
7992
|
+
else {
|
|
7695
7993
|
await this.notify(config.hooks?.afterChange, "afterChange", slug2, {
|
|
7696
7994
|
...base,
|
|
7697
|
-
doc
|
|
7995
|
+
doc,
|
|
7698
7996
|
previousDoc: current,
|
|
7699
7997
|
operation: "update"
|
|
7700
7998
|
});
|
|
7701
|
-
this.emitChange(config, "
|
|
7702
|
-
await this.audited(config, "draft", draft, current, guard, mode);
|
|
7703
|
-
return this.readGlobal(config, key, guard, { ...options, draft: true });
|
|
7999
|
+
this.emitChange(config, "update", doc, existing.status);
|
|
7704
8000
|
}
|
|
7705
|
-
const doc = await this.db.updateGlobal({ slug: key, data: { ...prepared, updatedAt: now } });
|
|
7706
|
-
await this.saveVersion(config, globalParent(key), 0, doc, guard);
|
|
7707
|
-
await this.notify(config.hooks?.afterChange, "afterChange", slug2, {
|
|
7708
|
-
...base,
|
|
7709
|
-
doc,
|
|
7710
|
-
previousDoc: current,
|
|
7711
|
-
operation: "update"
|
|
7712
|
-
});
|
|
7713
|
-
this.emitChange(config, "update", doc, existing.status);
|
|
7714
8001
|
await this.audited(config, "update", doc, current, guard, mode);
|
|
7715
8002
|
return this.readGlobal(config, key, guard, { ...options, draft: true });
|
|
7716
8003
|
}
|
|
@@ -7824,9 +8111,21 @@ var EasyCMS = class {
|
|
|
7824
8111
|
}
|
|
7825
8112
|
};
|
|
7826
8113
|
}
|
|
8114
|
+
/**
|
|
8115
|
+
* Waits until the outbox (`outbox: true`) ran what saves left so far, e.g. before a serverless
|
|
8116
|
+
* function returns. What fails stays queued for its retry.
|
|
8117
|
+
*/
|
|
8118
|
+
flushOutbox() {
|
|
8119
|
+
return this.outbox?.flush() ?? Promise.resolve();
|
|
8120
|
+
}
|
|
8121
|
+
/** @internal Puts a failed outbox entry back in line (Settings → Jobs). */
|
|
8122
|
+
retryOutbox(id) {
|
|
8123
|
+
return this.outbox?.retry(id) ?? Promise.resolve(false);
|
|
8124
|
+
}
|
|
7827
8125
|
async destroy() {
|
|
7828
8126
|
if (this.schedulerTimer) clearInterval(this.schedulerTimer);
|
|
7829
8127
|
await this.running;
|
|
8128
|
+
await this.outbox?.flush();
|
|
7830
8129
|
await this.webhooks.flush();
|
|
7831
8130
|
await this.mailer.flush();
|
|
7832
8131
|
await this.db.destroy();
|
|
@@ -7873,7 +8172,8 @@ var EasyCMS = class {
|
|
|
7873
8172
|
scheduled,
|
|
7874
8173
|
webhooks: await this.retryWebhooks(now),
|
|
7875
8174
|
emails: await this.mailer.retry(now),
|
|
7876
|
-
jobs: await this.runConfigJobs(now)
|
|
8175
|
+
jobs: await this.runConfigJobs(now),
|
|
8176
|
+
outbox: await this.outbox?.run(now) ?? { ran: 0, failed: 0 }
|
|
7877
8177
|
};
|
|
7878
8178
|
const ranAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7879
8179
|
await this.db.updateGlobal({ slug: JOBS_RAN, data: { at: ranAt, updatedAt: ranAt } }).catch((error) => this.logger.error(`Jobs: ${error.message}`, { error }));
|
|
@@ -8605,18 +8905,20 @@ var EasyCMS = class {
|
|
|
8605
8905
|
if (password !== void 0) prepared.passwordHash = await hashPassword(password);
|
|
8606
8906
|
if (collection2 === MEDIA_FOLDERS) await this.validateFolder(prepared, void 0);
|
|
8607
8907
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8608
|
-
const
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
});
|
|
8908
|
+
const row = { ...prepared, createdAt: now, updatedAt: now };
|
|
8909
|
+
const outbox = this.outboxRow(config, "create", hookGuard ?? guard, row);
|
|
8910
|
+
const doc = await this.db.create({ collection: collection2, data: row, outbox });
|
|
8612
8911
|
if (collection2 === MEDIA_FOLDERS) this.folders.invalidate();
|
|
8613
8912
|
await this.saveVersion(config, collectionParent(collection2), doc.id, doc, guard);
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8913
|
+
if (outbox) this.outbox?.kick();
|
|
8914
|
+
else {
|
|
8915
|
+
await this.notify(config.hooks?.afterChange, "afterChange", collection2, {
|
|
8916
|
+
...base,
|
|
8917
|
+
doc,
|
|
8918
|
+
operation: "create"
|
|
8919
|
+
});
|
|
8920
|
+
this.emitChange(config, "create", doc);
|
|
8921
|
+
}
|
|
8620
8922
|
await this.audited(config, "create", doc, void 0, hookGuard ?? guard);
|
|
8621
8923
|
const [out] = await this.output(config, [doc], hookGuard ?? guard, { ...options, draft: true });
|
|
8622
8924
|
return out;
|
|
@@ -8674,23 +8976,21 @@ var EasyCMS = class {
|
|
|
8674
8976
|
});
|
|
8675
8977
|
}
|
|
8676
8978
|
/**
|
|
8677
|
-
* `httpCache.purge
|
|
8678
|
-
* published document saved, published, unpublished or deleted).
|
|
8679
|
-
* logged.
|
|
8979
|
+
* The tags `httpCache.purge` gets for a change: what readers see changed (anything, without
|
|
8980
|
+
* drafts; with drafts, a published document saved, published, unpublished or deleted).
|
|
8680
8981
|
*/
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
if (!purge || config.httpCache === false || event === "draft") return;
|
|
8982
|
+
purgeTags(config, event, doc, before) {
|
|
8983
|
+
if (!this.config.httpCache?.purge || config.httpCache === false || event === "draft") return;
|
|
8684
8984
|
if (config.drafts && doc.status !== "published" && before !== "published") return;
|
|
8685
|
-
|
|
8686
|
-
void Promise.resolve().then(() => purge(tags)).catch(
|
|
8687
|
-
(error) => this.logger.error(`httpCache.purge failed: ${error.message}`, { error })
|
|
8688
|
-
);
|
|
8985
|
+
return this.isGlobal(config) ? cacheTags.global(config.slug) : cacheTags.document(config.slug, doc.id);
|
|
8689
8986
|
}
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8987
|
+
isGlobal(config) {
|
|
8988
|
+
return !this.config.collections.includes(config);
|
|
8989
|
+
}
|
|
8990
|
+
/** The webhook events of a change; `before` is the stored status before it, if any. */
|
|
8991
|
+
changeEvents(config, event, doc, before) {
|
|
8992
|
+
const isGlobal = this.isGlobal(config);
|
|
8993
|
+
if (!isGlobal && INTERNAL_COLLECTIONS.has(config.slug)) return [];
|
|
8694
8994
|
const body = isGlobal ? { ...snapshotOf(config.fields, doc), updatedAt: doc.updatedAt } : {
|
|
8695
8995
|
id: doc.id,
|
|
8696
8996
|
...snapshotOf(config.fields, doc),
|
|
@@ -8698,13 +8998,77 @@ var EasyCMS = class {
|
|
|
8698
8998
|
updatedAt: doc.updatedAt
|
|
8699
8999
|
};
|
|
8700
9000
|
const target = isGlobal ? { global: config.slug } : { collection: config.slug, id: doc.id };
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
if (event === "draft" || event === "delete" || !config.drafts) return;
|
|
9001
|
+
const events = [{ event, target, body }];
|
|
9002
|
+
if (event === "draft" || event === "delete" || !config.drafts) return events;
|
|
8704
9003
|
const after = doc.status;
|
|
8705
|
-
if (after === "published" && before !== "published")
|
|
9004
|
+
if (after === "published" && before !== "published")
|
|
9005
|
+
events.push({ event: "publish", target, body });
|
|
8706
9006
|
if (after !== "published" && before === "published")
|
|
8707
|
-
|
|
9007
|
+
events.push({ event: "unpublish", target, body });
|
|
9008
|
+
return events;
|
|
9009
|
+
}
|
|
9010
|
+
/**
|
|
9011
|
+
* Sends webhook events for a change and purges the HTTP cache. Not awaited; purge failures
|
|
9012
|
+
* are logged.
|
|
9013
|
+
*/
|
|
9014
|
+
emitChange(config, event, doc, before) {
|
|
9015
|
+
for (const change of this.changeEvents(config, event, doc, before))
|
|
9016
|
+
this.webhooks.emit(change.event, change.target, change.body);
|
|
9017
|
+
const purge = this.config.httpCache?.purge;
|
|
9018
|
+
const tags = this.purgeTags(config, event, doc, before);
|
|
9019
|
+
if (!purge || !tags) return;
|
|
9020
|
+
void Promise.resolve().then(() => purge(tags)).catch(
|
|
9021
|
+
(error) => this.logger.error(`httpCache.purge failed: ${error.message}`, { error })
|
|
9022
|
+
);
|
|
9023
|
+
}
|
|
9024
|
+
/**
|
|
9025
|
+
* The outbox row of a save (`outbox: true`): written in its transaction, then run by the outbox
|
|
9026
|
+
* in place of its after hooks and events. `undefined` without the outbox, or with nothing to run.
|
|
9027
|
+
*/
|
|
9028
|
+
outboxRow(config, event, guard, doc, extra = {}) {
|
|
9029
|
+
if (!this.outbox) return void 0;
|
|
9030
|
+
const hooks = event === "delete" ? config.hooks?.afterDelete : config.hooks?.afterChange;
|
|
9031
|
+
const isGlobal = this.isGlobal(config);
|
|
9032
|
+
const effects = (isGlobal || !INTERNAL_COLLECTIONS.has(config.slug)) && ((this.config.webhooks?.length ?? 0) > 0 || this.config.httpCache?.purge !== void 0);
|
|
9033
|
+
if (!hooks?.length && !effects) return void 0;
|
|
9034
|
+
return this.outbox.row({
|
|
9035
|
+
event,
|
|
9036
|
+
target: config.slug,
|
|
9037
|
+
global: isGlobal,
|
|
9038
|
+
docId: extra.docId,
|
|
9039
|
+
before: typeof extra.before === "string" ? extra.before : null,
|
|
9040
|
+
doc,
|
|
9041
|
+
previousDoc: extra.previousDoc,
|
|
9042
|
+
user: guard.user
|
|
9043
|
+
});
|
|
9044
|
+
}
|
|
9045
|
+
/** Runs an outbox entry: what its save would have run right after it, without the outbox. */
|
|
9046
|
+
async runOutbox(entry, done, mark) {
|
|
9047
|
+
const config = entry.global ? this.config.globals.find((g) => g.slug === entry.target) : this.config.collections.find((c) => c.slug === entry.target);
|
|
9048
|
+
if (!config) return;
|
|
9049
|
+
const { doc, event } = entry;
|
|
9050
|
+
if (!done.has("hooks")) {
|
|
9051
|
+
const base = this.hookArgs(config, { enforce: false, user: entry.user, context: {} });
|
|
9052
|
+
const hooks = event === "delete" ? config.hooks?.afterDelete : config.hooks?.afterChange;
|
|
9053
|
+
const args = event === "delete" ? { ...base, id: parseId(entry.docId ?? void 0), doc } : {
|
|
9054
|
+
...base,
|
|
9055
|
+
doc,
|
|
9056
|
+
...entry.previousDoc ? { previousDoc: entry.previousDoc } : {},
|
|
9057
|
+
operation: event === "create" ? "create" : "update"
|
|
9058
|
+
};
|
|
9059
|
+
for (const hook of hooks ?? [])
|
|
9060
|
+
await hook(args);
|
|
9061
|
+
if (hooks?.length) await mark("hooks");
|
|
9062
|
+
}
|
|
9063
|
+
if (!done.has("webhooks")) {
|
|
9064
|
+
const changes = this.changeEvents(config, event, doc, entry.before);
|
|
9065
|
+
for (const change of changes)
|
|
9066
|
+
await this.webhooks.emitSaved(change.event, change.target, change.body);
|
|
9067
|
+
if (changes.length > 0 && (this.config.webhooks?.length ?? 0) > 0) await mark("webhooks");
|
|
9068
|
+
}
|
|
9069
|
+
const purge = this.config.httpCache?.purge;
|
|
9070
|
+
const tags = this.purgeTags(config, event, doc, entry.before);
|
|
9071
|
+
if (purge && tags) await purge(tags);
|
|
8708
9072
|
}
|
|
8709
9073
|
/**
|
|
8710
9074
|
* Writes the audit log entry for a change (`audit`): publishing, unpublishing and restoring
|
|
@@ -8735,7 +9099,7 @@ var EasyCMS = class {
|
|
|
8735
9099
|
);
|
|
8736
9100
|
}
|
|
8737
9101
|
/** Records a version after a save, when the collection or global keeps versions. */
|
|
8738
|
-
async saveVersion(config, parent, id, doc, guard) {
|
|
9102
|
+
async saveVersion(config, parent, id, doc, guard, outbox) {
|
|
8739
9103
|
const max = versionLimit(config);
|
|
8740
9104
|
if (max === void 0) return;
|
|
8741
9105
|
await this.versions.save({
|
|
@@ -8744,7 +9108,8 @@ var EasyCMS = class {
|
|
|
8744
9108
|
status: config.drafts ? doc.status ?? "draft" : null,
|
|
8745
9109
|
snapshot: snapshotOf(config.fields, doc),
|
|
8746
9110
|
author: guard.user?.id ?? null,
|
|
8747
|
-
max
|
|
9111
|
+
max,
|
|
9112
|
+
outbox
|
|
8748
9113
|
});
|
|
8749
9114
|
}
|
|
8750
9115
|
hookArgs(config, guard) {
|
|
@@ -9173,7 +9538,7 @@ function readCursor(cursor, query) {
|
|
|
9173
9538
|
// src/rest/admin-jobs.ts
|
|
9174
9539
|
var has = (config, slug2) => config.collections.some((c) => c.slug === slug2);
|
|
9175
9540
|
function hasJobsPage(config) {
|
|
9176
|
-
return config.jobs.length > 0 || has(config, SCHEDULED_JOBS);
|
|
9541
|
+
return config.jobs.length > 0 || has(config, SCHEDULED_JOBS) || has(config, OUTBOX);
|
|
9177
9542
|
}
|
|
9178
9543
|
var text2 = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
9179
9544
|
async function adminJobs(cms, user) {
|
|
@@ -9206,6 +9571,7 @@ async function adminJobs(cms, user) {
|
|
|
9206
9571
|
...has(config, SCHEDULED_JOBS) ? ["scheduled"] : [],
|
|
9207
9572
|
...has(config, WEBHOOK_DELIVERIES) ? ["webhooks"] : [],
|
|
9208
9573
|
...has(config, EMAIL_DELIVERIES) ? ["emails"] : [],
|
|
9574
|
+
...has(config, OUTBOX) ? ["outbox"] : [],
|
|
9209
9575
|
...config.backups?.frequency ? ["backups"] : [],
|
|
9210
9576
|
...config.audit ? ["audit"] : []
|
|
9211
9577
|
];
|
|
@@ -9214,7 +9580,8 @@ async function adminJobs(cms, user) {
|
|
|
9214
9580
|
lastRanAt: await cms.jobsLastRan(),
|
|
9215
9581
|
builtIn,
|
|
9216
9582
|
timeZone: config.timeZone ?? null,
|
|
9217
|
-
canChange: isSystemAdmin(user)
|
|
9583
|
+
canChange: isSystemAdmin(user),
|
|
9584
|
+
outbox: has(config, OUTBOX) ? await adminOutbox(cms) : null
|
|
9218
9585
|
};
|
|
9219
9586
|
if (!has(config, SCHEDULED_JOBS)) return { ...head, scheduled: null };
|
|
9220
9587
|
const { docs } = await cms.db.find({
|
|
@@ -9239,6 +9606,38 @@ async function adminJobs(cms, user) {
|
|
|
9239
9606
|
});
|
|
9240
9607
|
return { ...head, scheduled };
|
|
9241
9608
|
}
|
|
9609
|
+
async function adminOutbox(cms) {
|
|
9610
|
+
const count = (state) => cms.db.count({ collection: OUTBOX, where: { state: { in: state } } });
|
|
9611
|
+
const { docs } = await cms.db.find({
|
|
9612
|
+
collection: OUTBOX,
|
|
9613
|
+
where: { attempts: { gt: 0 } },
|
|
9614
|
+
sort: ["-updatedAt"],
|
|
9615
|
+
limit: 100,
|
|
9616
|
+
page: 1
|
|
9617
|
+
});
|
|
9618
|
+
return {
|
|
9619
|
+
waiting: await count(["pending", "running"]),
|
|
9620
|
+
failed: await count(["failed"]),
|
|
9621
|
+
entries: docs.filter((row) => text2(row.error) !== null).map((row) => ({
|
|
9622
|
+
id: row.id,
|
|
9623
|
+
event: row.event,
|
|
9624
|
+
...row.global === true ? { global: String(row.target) } : { collection: String(row.target) },
|
|
9625
|
+
doc: text2(row.docId),
|
|
9626
|
+
attempts: Number(row.attempts ?? 0),
|
|
9627
|
+
state: row.state === "failed" || row.state === "running" ? row.state : "pending",
|
|
9628
|
+
error: text2(row.error),
|
|
9629
|
+
nextAttemptAt: String(row.nextAttemptAt)
|
|
9630
|
+
}))
|
|
9631
|
+
};
|
|
9632
|
+
}
|
|
9633
|
+
async function retryOutbox(cms, user, id) {
|
|
9634
|
+
if (!isSystemAdmin(user)) throw new ForbiddenError();
|
|
9635
|
+
if (!has(cms.config, OUTBOX)) throw new NotFoundError(OUTBOX, id);
|
|
9636
|
+
const row = await cms.db.findById({ collection: OUTBOX, id: parseId(id) ?? 0 });
|
|
9637
|
+
if (!row) throw new NotFoundError(OUTBOX, id);
|
|
9638
|
+
if (!await cms.retryOutbox(row.id))
|
|
9639
|
+
throw new EasyCMSError("Only failed entries can be retried", 409);
|
|
9640
|
+
}
|
|
9242
9641
|
var TIME_OF_DAY2 = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
9243
9642
|
async function updateJob(cms, user, name, body) {
|
|
9244
9643
|
if (!isSystemAdmin(user)) throw new ForbiddenError();
|
|
@@ -9945,7 +10344,7 @@ async function readAdminModule(cms, index) {
|
|
|
9945
10344
|
}
|
|
9946
10345
|
|
|
9947
10346
|
// src/version.ts
|
|
9948
|
-
var VERSION = true ? "1.
|
|
10347
|
+
var VERSION = true ? "1.12.0" : "0.0.0-dev";
|
|
9949
10348
|
|
|
9950
10349
|
// src/rest/handler.ts
|
|
9951
10350
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
@@ -9955,7 +10354,7 @@ var PER_PAGE = 50;
|
|
|
9955
10354
|
var collectionOf = (kind) => kind === "webhook" ? WEBHOOK_DELIVERIES : EMAIL_DELIVERIES;
|
|
9956
10355
|
var exists = (cms, kind) => cms.config.collections.some((c) => c.slug === collectionOf(kind));
|
|
9957
10356
|
var isDeliveryKind = (value) => value === "webhook" || value === "email";
|
|
9958
|
-
function
|
|
10357
|
+
function parse3(text3) {
|
|
9959
10358
|
try {
|
|
9960
10359
|
const value = JSON.parse(String(text3 ?? ""));
|
|
9961
10360
|
return typeof value === "object" && value !== null ? value : {};
|
|
@@ -9974,7 +10373,7 @@ function toAdmin2(kind, row) {
|
|
|
9974
10373
|
nextAttemptAt: String(row.nextAttemptAt ?? "")
|
|
9975
10374
|
};
|
|
9976
10375
|
if (kind === "webhook") {
|
|
9977
|
-
const payload =
|
|
10376
|
+
const payload = parse3(row.body);
|
|
9978
10377
|
return {
|
|
9979
10378
|
...base,
|
|
9980
10379
|
kind,
|
|
@@ -9986,7 +10385,7 @@ function toAdmin2(kind, row) {
|
|
|
9986
10385
|
body: String(row.body ?? "")
|
|
9987
10386
|
};
|
|
9988
10387
|
}
|
|
9989
|
-
const message2 =
|
|
10388
|
+
const message2 = parse3(row.message);
|
|
9990
10389
|
const to = message2.to;
|
|
9991
10390
|
return {
|
|
9992
10391
|
...base,
|
|
@@ -11503,6 +11902,13 @@ async function route(cms, ctx, method, path, options) {
|
|
|
11503
11902
|
await cms.audit.record({ action: "jobs.run", target: "jobs", detail: result });
|
|
11504
11903
|
return { body: result };
|
|
11505
11904
|
}
|
|
11905
|
+
if (third === "outbox" && segments.length === 4) {
|
|
11906
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
11907
|
+
const id2 = segments[3];
|
|
11908
|
+
await retryOutbox(cms, ctx.user, id2);
|
|
11909
|
+
await cms.audit.record({ action: "jobs.outbox.retry", target: "jobs", detail: { id: id2 } });
|
|
11910
|
+
return { body: await adminJobs(cms, ctx.user) };
|
|
11911
|
+
}
|
|
11506
11912
|
if (third !== void 0 && segments.length === 3) {
|
|
11507
11913
|
if (method !== "PATCH") throw methodNotAllowed(ctx, "PATCH");
|
|
11508
11914
|
const { action, detail } = await updateJob(
|
|
@@ -201,6 +201,17 @@ interface CreateMigrationResult {
|
|
|
201
201
|
readonly file: string;
|
|
202
202
|
readonly statements: readonly string[];
|
|
203
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* A row written with a document, in the same transaction (`outbox: true` in the config): a write
|
|
206
|
+
* that commits leaves it, one that doesn't leaves nothing. Not written when an `update` with
|
|
207
|
+
* `where` doesn't match.
|
|
208
|
+
*/
|
|
209
|
+
interface OutboxRow {
|
|
210
|
+
/** The collection the row goes to. */
|
|
211
|
+
readonly collection: string;
|
|
212
|
+
/** The row's fields, given the written document's id (`null` for a global). */
|
|
213
|
+
data(id: ID | null): Record<string, unknown>;
|
|
214
|
+
}
|
|
204
215
|
/** A connected database bound to one config. Returned by `DatabaseAdapter.init`. */
|
|
205
216
|
interface Database {
|
|
206
217
|
find(args: FindArgs): Promise<PaginatedDocs<RawDocument>>;
|
|
@@ -231,6 +242,7 @@ interface Database {
|
|
|
231
242
|
create(args: {
|
|
232
243
|
collection: string;
|
|
233
244
|
data: Record<string, unknown>;
|
|
245
|
+
outbox?: OutboxRow | undefined;
|
|
234
246
|
}): Promise<RawDocument>;
|
|
235
247
|
/**
|
|
236
248
|
* `data` holds the whole document after merging. Runs in one transaction. With `where`, only
|
|
@@ -241,12 +253,14 @@ interface Database {
|
|
|
241
253
|
collection: string;
|
|
242
254
|
id: ID;
|
|
243
255
|
data: Record<string, unknown>;
|
|
256
|
+
outbox?: OutboxRow | undefined;
|
|
244
257
|
}): Promise<RawDocument>;
|
|
245
258
|
update(args: {
|
|
246
259
|
collection: string;
|
|
247
260
|
id: ID;
|
|
248
261
|
data: Record<string, unknown>;
|
|
249
262
|
where: Where;
|
|
263
|
+
outbox?: OutboxRow | undefined;
|
|
250
264
|
}): Promise<RawDocument | null>;
|
|
251
265
|
/**
|
|
252
266
|
* Adds `by` to a top-level number field in one statement, safe against concurrent calls; an
|
|
@@ -264,6 +278,7 @@ interface Database {
|
|
|
264
278
|
delete(args: {
|
|
265
279
|
collection: string;
|
|
266
280
|
id: ID;
|
|
281
|
+
outbox?: OutboxRow | undefined;
|
|
267
282
|
}): Promise<void>;
|
|
268
283
|
findGlobal(args: {
|
|
269
284
|
slug: string;
|
|
@@ -271,7 +286,13 @@ interface Database {
|
|
|
271
286
|
updateGlobal(args: {
|
|
272
287
|
slug: string;
|
|
273
288
|
data: Record<string, unknown>;
|
|
289
|
+
outbox?: OutboxRow | undefined;
|
|
274
290
|
}): Promise<Record<string, unknown>>;
|
|
291
|
+
/**
|
|
292
|
+
* True when `create`, `update`, `delete` and `updateGlobal` take `outbox`, a row written in
|
|
293
|
+
* the same transaction. Required by `outbox: true` in the config.
|
|
294
|
+
*/
|
|
295
|
+
readonly outbox?: boolean | undefined;
|
|
275
296
|
/** Writes a migration file for the difference between the config and the latest migration. `null` when nothing changed. */
|
|
276
297
|
createMigration(args: {
|
|
277
298
|
name: string;
|
|
@@ -1778,6 +1799,8 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
1778
1799
|
private readonly versions;
|
|
1779
1800
|
private readonly webhooks;
|
|
1780
1801
|
private readonly mailer;
|
|
1802
|
+
/** With `outbox: true`: runs after hooks and events from rows saves write. */
|
|
1803
|
+
private readonly outbox;
|
|
1781
1804
|
/**
|
|
1782
1805
|
* The user and context of a request, as the REST API works them out: its session cookie or
|
|
1783
1806
|
* Bearer token, then `onRequest`. For framework routes and server code that act for a visitor;
|
|
@@ -2038,6 +2061,11 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
2038
2061
|
flushEmails(): Promise<void>;
|
|
2039
2062
|
/** Emails queued until sent, stored in `email-deliveries`. */
|
|
2040
2063
|
private emailQueue;
|
|
2064
|
+
/**
|
|
2065
|
+
* Waits until the outbox (`outbox: true`) ran what saves left so far, e.g. before a serverless
|
|
2066
|
+
* function returns. What fails stays queued for its retry.
|
|
2067
|
+
*/
|
|
2068
|
+
flushOutbox(): Promise<void>;
|
|
2041
2069
|
destroy(): Promise<void>;
|
|
2042
2070
|
private schedulerTimer;
|
|
2043
2071
|
private running;
|
|
@@ -2065,6 +2093,11 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
2065
2093
|
ran: number;
|
|
2066
2094
|
failed: number;
|
|
2067
2095
|
};
|
|
2096
|
+
/** Outbox entries (`outbox: true`) run, and those that failed (to be retried). */
|
|
2097
|
+
outbox: {
|
|
2098
|
+
ran: number;
|
|
2099
|
+
failed: number;
|
|
2100
|
+
};
|
|
2068
2101
|
}>;
|
|
2069
2102
|
private runJobsNow;
|
|
2070
2103
|
/**
|
|
@@ -2184,13 +2217,25 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
2184
2217
|
/** Replaces published documents that have a newer draft with that draft. */
|
|
2185
2218
|
private withDrafts;
|
|
2186
2219
|
/**
|
|
2187
|
-
* `httpCache.purge
|
|
2188
|
-
* published document saved, published, unpublished or deleted).
|
|
2189
|
-
|
|
2220
|
+
* The tags `httpCache.purge` gets for a change: what readers see changed (anything, without
|
|
2221
|
+
* drafts; with drafts, a published document saved, published, unpublished or deleted).
|
|
2222
|
+
*/
|
|
2223
|
+
private purgeTags;
|
|
2224
|
+
private isGlobal;
|
|
2225
|
+
/** The webhook events of a change; `before` is the stored status before it, if any. */
|
|
2226
|
+
private changeEvents;
|
|
2227
|
+
/**
|
|
2228
|
+
* Sends webhook events for a change and purges the HTTP cache. Not awaited; purge failures
|
|
2229
|
+
* are logged.
|
|
2190
2230
|
*/
|
|
2191
|
-
private purgePublic;
|
|
2192
|
-
/** Sends webhook events for a change; `before` is the stored status before it, if any. */
|
|
2193
2231
|
private emitChange;
|
|
2232
|
+
/**
|
|
2233
|
+
* The outbox row of a save (`outbox: true`): written in its transaction, then run by the outbox
|
|
2234
|
+
* in place of its after hooks and events. `undefined` without the outbox, or with nothing to run.
|
|
2235
|
+
*/
|
|
2236
|
+
private outboxRow;
|
|
2237
|
+
/** Runs an outbox entry: what its save would have run right after it, without the outbox. */
|
|
2238
|
+
private runOutbox;
|
|
2194
2239
|
/**
|
|
2195
2240
|
* Writes the audit log entry for a change (`audit`): publishing, unpublishing and restoring
|
|
2196
2241
|
* are told apart from other saves.
|
|
@@ -3420,6 +3465,15 @@ interface Config {
|
|
|
3420
3465
|
* requests get `400`, pointing to `cursor`. Deep pages are read row by row. Default: no limit.
|
|
3421
3466
|
*/
|
|
3422
3467
|
readonly maxPage?: number;
|
|
3468
|
+
/**
|
|
3469
|
+
* Runs `afterChange` and `afterDelete` hooks, webhooks and `httpCache.purge` from an outbox:
|
|
3470
|
+
* saving a document also queues them, in the same database transaction, and a worker runs them
|
|
3471
|
+
* after it commits, retrying what fails. A save that succeeds always gets its side effects (at
|
|
3472
|
+
* least once; hooks must be safe to run again), and the response doesn't wait for them. Hooks
|
|
3473
|
+
* then get the user who saved but no `context`. Needs a migration for the `outbox` table.
|
|
3474
|
+
* Default `false`: hooks run in the request, after the save.
|
|
3475
|
+
*/
|
|
3476
|
+
readonly outbox?: boolean;
|
|
3423
3477
|
readonly plugins?: readonly Plugin[];
|
|
3424
3478
|
}
|
|
3425
3479
|
/** The config after plugins ran, validation passed and defaults were applied. */
|
|
@@ -3451,5 +3505,5 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
|
|
|
3451
3505
|
*/
|
|
3452
3506
|
declare function defineConfig<const TConfig extends Config>(config: TConfig): TConfig;
|
|
3453
3507
|
|
|
3454
|
-
export { ADAPTER_API_VERSION as A, AuditLog as J, Auth as M, ADMIN_ICONS as a, FIELD_TYPES as aD, ForbiddenError as a_, ConfigError as ac, EasyCMS as av, EasyCMSError as aw, PayloadTooLargeError as
|
|
3455
|
-
export type { BuiltinMediaCollection as $, ApiKeyOperation as B, ApiKeyPermissions as C, ArrayField as D, EmailSetting as E, AuditChange as F, AuditConfig as G, AuditEntry as H, ID as I, AuditPage as K, AuditVerification as L, AuthConfig as N, AuthProvider as O, PaginatedDocs as P, AuthProviderProfile as Q, AuthProviderRequest as R, AuthUser as S, BackupsConfig as T, BeforeChangeHook as U, BeforeDeleteHook as V, BeforeValidateHook as W, Block as X, BlocksField as Y, BooleanField as Z, BuiltinApiKeysCollection as _, GlobalAccess as a$, BuiltinMediaFoldersCollection as a0, BuiltinUsersCollection as a1, BundleHints as a2, CliCommand as a3, CollectionAccess as a4, CollectionAdmin as a5, CollectionConfig as a6, CollectionDocument as a7, CollectionHooks as a8, CollectionListAdmin as a9, Endpoint as aA, EndpointRequest as aB, ErrorCode as aC, Field as aE, FieldAccess as aF, FieldAccessFn as aG, FieldAdmin as aH, FieldError as aI, FieldRule as aJ, FieldType as aK, FieldTypeBase as aL, FieldTypeDefinition as aM, FieldTypeValidateContext as aN, FieldValidate as aO, FieldValidateContext as aP, FieldValue as aQ, FieldsInput as aR, FieldsValue as aS, FilterOptions as aT, FilterOptionsArgs as aU, FindAfterArgs as aV, FindArgs as aW, FindOptions as aX, FolderLevel as aY, FolderPermissions as aZ, CollectionSlug as aa, Config as ab, ConfigIssue as ad, ContainerAdmin as ae, CreateEasyCMSOptions as af, CreateInput as ag, CreateMigrationResult as ah, CursorDocs as ai, CustomField as aj, CustomFieldTypes as ak, DashboardWidget as al, Database as am, DatabaseAdapter as an, DatabaseInitArgs as ao, DatabaseTransfer as ap, DateField as aq, DepthOptions as ar, DirectUpload as as, DiskStorageOptions as at, DocumentOf as au, EmailAdapter as ax, EmailField as ay, EmailMessage as az, Access as b,
|
|
3508
|
+
export { ADAPTER_API_VERSION as A, AuditLog as J, Auth as M, ADMIN_ICONS as a, FIELD_TYPES as aD, ForbiddenError as a_, ConfigError as ac, EasyCMS as av, EasyCMSError as aw, PayloadTooLargeError as bD, QueryError as bJ, ROLE_OPERATIONS as bK, Roles as bV, SchemaError as bY, NotFoundError as bs, PLUGIN_API_VERSION as by, keyAllows as c$, SingleSignOn as c4, createEasyCMS as cA, defineConfig as cB, defineFieldType as cC, definePlugin as cD, diskStorage as cE, hasRows as cF, isAdmin as cG, isHasMany as cH, isSignedIn as cI, isStaff as cJ, isSystemAdmin as cK, jsonLogger as cL, mimeAllowedBy as cM, rowFields as cN, silentLogger as cO, uniqueWithinOf as cP, API_KEYS as cQ, API_KEY_GLOBAL_OPERATIONS as cR, API_KEY_OPERATIONS as cS, DEFAULT_MAX_VERSIONS as cT, DEFAULT_PASSWORD_EMAILS as cU, FIELD_TYPE_BASES as cV, FOLDER_LEVELS as cW, MediaFolders as cX, SSO_COOKIE as cY, codeOfStatus as cZ, isJsonLogger as c_, TooManyRequestsError as cd, UnauthorizedError as cf, ValidationError as cn, WEBHOOK_EVENTS as cr, anyone as cx, consoleEmail as cy, consoleLogger as cz, logFormat as d0 };
|
|
3509
|
+
export type { BuiltinMediaCollection as $, ApiKeyOperation as B, ApiKeyPermissions as C, ArrayField as D, EmailSetting as E, AuditChange as F, AuditConfig as G, AuditEntry as H, ID as I, AuditPage as K, AuditVerification as L, AuthConfig as N, AuthProvider as O, PaginatedDocs as P, AuthProviderProfile as Q, AuthProviderRequest as R, AuthUser as S, BackupsConfig as T, BeforeChangeHook as U, BeforeDeleteHook as V, BeforeValidateHook as W, Block as X, BlocksField as Y, BooleanField as Z, BuiltinApiKeysCollection as _, GlobalAccess as a$, BuiltinMediaFoldersCollection as a0, BuiltinUsersCollection as a1, BundleHints as a2, CliCommand as a3, CollectionAccess as a4, CollectionAdmin as a5, CollectionConfig as a6, CollectionDocument as a7, CollectionHooks as a8, CollectionListAdmin as a9, Endpoint as aA, EndpointRequest as aB, ErrorCode as aC, Field as aE, FieldAccess as aF, FieldAccessFn as aG, FieldAdmin as aH, FieldError as aI, FieldRule as aJ, FieldType as aK, FieldTypeBase as aL, FieldTypeDefinition as aM, FieldTypeValidateContext as aN, FieldValidate as aO, FieldValidateContext as aP, FieldValue as aQ, FieldsInput as aR, FieldsValue as aS, FilterOptions as aT, FilterOptionsArgs as aU, FindAfterArgs as aV, FindArgs as aW, FindOptions as aX, FolderLevel as aY, FolderPermissions as aZ, CollectionSlug as aa, Config as ab, ConfigIssue as ad, ContainerAdmin as ae, CreateEasyCMSOptions as af, CreateInput as ag, CreateMigrationResult as ah, CursorDocs as ai, CustomField as aj, CustomFieldTypes as ak, DashboardWidget as al, Database as am, DatabaseAdapter as an, DatabaseInitArgs as ao, DatabaseTransfer as ap, DateField as aq, DepthOptions as ar, DirectUpload as as, DiskStorageOptions as at, DocumentOf as au, EmailAdapter as ax, EmailField as ay, EmailMessage as az, Access as b, SelectOption as b$, GlobalConfig as b0, GlobalDocument as b1, GlobalHooks as b2, GlobalInput as b3, GlobalSlug as b4, GroupField as b5, ImageSize as b6, IncrementOptions as b7, InferCollection as b8, InferGlobal as b9, PasswordEmailArgs as bA, PasswordEmailFn as bB, PasswordLinkOptions as bC, Plugin as bE, PluginInfo as bF, PluginTypes as bG, PreviewTarget as bH, PreviewURL as bI, RawDocument as bL, ReadOptions as bM, RelationshipField as bN, RequestContext as bO, ResolvedConfig as bP, RichTextDocument as bQ, RichTextField as bR, Role as bS, RoleOperation as bT, RolePermissions as bU, RoutesConfig as bW, ScheduledPublish as bX, SchemaMode as bZ, SelectField as b_, JobConfig as ba, JsonField as bb, KeysetPage as bc, Label as bd, LayoutNode as be, LivePreview as bf, LocaleOf as bg, LocalizationConfig as bh, LogDetails as bi, Logger as bj, LoginArgs as bk, LogsConfig as bl, MediaDocument as bm, MediaSize as bn, MembersConfig as bo, MembersSignup as bp, MigrationInfo as bq, NavGroup as br, NumberField as bt, OnRequest as bu, OnRequestArgs as bv, Operation as bw, OutboxRow as bx, PasswordEmail as bz, AccessArgs as c, Session as c0, SidebarPanel as c1, SignupArgs as c2, SignupResult as c3, SlugField as c5, SlugOf as c6, SsoOutcome as c7, SsoProviderRef as c8, StorageAdapter as c9, StoredFile as ca, TextField as cb, TextareaField as cc, TypedPlugin as ce, UncountedDocs as cg, UpdateInput as ch, UpdateOptions as ci, UploadConfig as cj, UploadField as ck, UploadFromURLConfig as cl, UserIdentity as cm, Version as co, VersionSummary as cp, VersionsConfig as cq, WebhookConfig as cs, WebhookEvent as ct, WebhookPayload as cu, Where as cv, WhereOperators as cw, AccessOptions as d, AdminBrand as e, AdminCommand as f, AdminComponent as g, AdminConfig as h, AdminFieldArgs as i, AdminIcon as j, AdminLocale as k, AdminPage as l, AdminRole as m, AdminRoleChange as n, AdminRoleField as o, AdminRoleTarget as p, AdminRoleView as q, AdminRoles as r, AdminSso as s, AdminSwitcher as t, AdminViewAccess as u, AfterChangeHook as v, AfterDeleteHook as w, AfterReadHook as x, ApiKeyContext as y, ApiKeyGlobalOperation as z };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I as ID, ab as Config, av as EasyCMS, bh as LocalizationConfig, aE as Field, aI as FieldError, aT as FilterOptions, af as CreateEasyCMSOptions,
|
|
1
|
+
import { I as ID, ab as Config, av as EasyCMS, bh as LocalizationConfig, aE as Field, aI as FieldError, aT as FilterOptions, af as CreateEasyCMSOptions, bP as ResolvedConfig } from './config-CHEEPk1d.js';
|
|
2
2
|
|
|
3
3
|
/** Decrypts a file from `encryptBackup`; throws for another key or a changed file. */
|
|
4
4
|
declare function decryptBackup(data: Uint8Array, encryptionKey: string): Uint8Array;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { P as PaginatedDocs, I as ID, E as EmailSetting } from './config-
|
|
2
|
-
export { A as ADAPTER_API_VERSION, a as ADMIN_ICONS, b as Access, c as AccessArgs, d as AccessOptions, e as AdminBrand, f as AdminCommand, g as AdminComponent, h as AdminConfig, i as AdminFieldArgs, j as AdminIcon, k as AdminLocale, l as AdminPage, m as AdminRole, n as AdminRoleChange, o as AdminRoleField, p as AdminRoleTarget, q as AdminRoleView, r as AdminRoles, s as AdminSso, t as AdminSwitcher, u as AdminViewAccess, v as AfterChangeHook, w as AfterDeleteHook, x as AfterReadHook, y as ApiKeyContext, z as ApiKeyGlobalOperation, B as ApiKeyOperation, C as ApiKeyPermissions, D as ArrayField, F as AuditChange, G as AuditConfig, H as AuditEntry, J as AuditLog, K as AuditPage, L as AuditVerification, M as Auth, N as AuthConfig, O as AuthProvider, Q as AuthProviderProfile, R as AuthProviderRequest, S as AuthUser, T as BackupsConfig, U as BeforeChangeHook, V as BeforeDeleteHook, W as BeforeValidateHook, X as Block, Y as BlocksField, Z as BooleanField, _ as BuiltinApiKeysCollection, $ as BuiltinMediaCollection, a0 as BuiltinMediaFoldersCollection, a1 as BuiltinUsersCollection, a2 as BundleHints, a3 as CliCommand, a4 as CollectionAccess, a5 as CollectionAdmin, a6 as CollectionConfig, a7 as CollectionDocument, a8 as CollectionHooks, a9 as CollectionListAdmin, aa as CollectionSlug, ab as Config, ac as ConfigError, ad as ConfigIssue, ae as ContainerAdmin, af as CreateEasyCMSOptions, ag as CreateInput, ah as CreateMigrationResult, ai as CursorDocs, aj as CustomField, ak as CustomFieldTypes, al as DashboardWidget, am as Database, an as DatabaseAdapter, ao as DatabaseInitArgs, ap as DatabaseTransfer, aq as DateField, ar as DepthOptions, as as DirectUpload, at as DiskStorageOptions, au as DocumentOf, av as EasyCMS, aw as EasyCMSError, ax as EmailAdapter, ay as EmailField, az as EmailMessage, aA as Endpoint, aB as EndpointRequest, aC as ErrorCode, aD as FIELD_TYPES, aE as Field, aF as FieldAccess, aG as FieldAccessFn, aH as FieldAdmin, aI as FieldError, aJ as FieldRule, aK as FieldType, aL as FieldTypeBase, aM as FieldTypeDefinition, aN as FieldTypeValidateContext, aO as FieldValidate, aP as FieldValidateContext, aQ as FieldValue, aR as FieldsInput, aS as FieldsValue, aT as FilterOptions, aU as FilterOptionsArgs, aV as FindAfterArgs, aW as FindArgs, aX as FindOptions, aY as FolderLevel, aZ as FolderPermissions, a_ as ForbiddenError, a$ as GlobalAccess, b0 as GlobalConfig, b1 as GlobalDocument, b2 as GlobalHooks, b3 as GlobalInput, b4 as GlobalSlug, b5 as GroupField, b6 as ImageSize, b7 as IncrementOptions, b8 as InferCollection, b9 as InferGlobal, ba as JobConfig, bb as JsonField, bc as KeysetPage, bd as Label, be as LayoutNode, bf as LivePreview, bg as LocaleOf, bh as LocalizationConfig, bi as LogDetails, bj as Logger, bk as LoginArgs, bl as LogsConfig, bm as MediaDocument, bn as MediaSize, bo as MembersConfig, bp as MembersSignup, bq as MigrationInfo, br as NavGroup, bs as NotFoundError, bt as NumberField, bu as OnRequest, bv as OnRequestArgs, bw as Operation, bx as
|
|
3
|
-
export { A as ADMIN_API_VERSION, a as AdminCollection, b as AdminComponentRef, c as AdminElementProps, d as AdminField, e as AdminGlobal, f as AdminLayoutNode, g as AdminNavGroup, h as AdminNavItem, i as AdminNavNode, j as AdminPageRef, k as AdminPageRoute, l as AdminSchema, m as AdminWidgetRef, S as SetFieldDetail, w as warnDeprecated } from './plugin-
|
|
4
|
-
export { A as AdminBackup, a as AdminBackups, b as ApiHandlerOptions, B as BackupState, L as LoadConfigOptions, R as RestHandler, c as RestHandlerOptions, d as createApiHandler, e as createRestHandler, l as loadConfig, r as resolveConfig, s as sharedEasyCMS, f as slugify } from './handler-
|
|
1
|
+
import { P as PaginatedDocs, I as ID, E as EmailSetting } from './config-CHEEPk1d.js';
|
|
2
|
+
export { A as ADAPTER_API_VERSION, a as ADMIN_ICONS, b as Access, c as AccessArgs, d as AccessOptions, e as AdminBrand, f as AdminCommand, g as AdminComponent, h as AdminConfig, i as AdminFieldArgs, j as AdminIcon, k as AdminLocale, l as AdminPage, m as AdminRole, n as AdminRoleChange, o as AdminRoleField, p as AdminRoleTarget, q as AdminRoleView, r as AdminRoles, s as AdminSso, t as AdminSwitcher, u as AdminViewAccess, v as AfterChangeHook, w as AfterDeleteHook, x as AfterReadHook, y as ApiKeyContext, z as ApiKeyGlobalOperation, B as ApiKeyOperation, C as ApiKeyPermissions, D as ArrayField, F as AuditChange, G as AuditConfig, H as AuditEntry, J as AuditLog, K as AuditPage, L as AuditVerification, M as Auth, N as AuthConfig, O as AuthProvider, Q as AuthProviderProfile, R as AuthProviderRequest, S as AuthUser, T as BackupsConfig, U as BeforeChangeHook, V as BeforeDeleteHook, W as BeforeValidateHook, X as Block, Y as BlocksField, Z as BooleanField, _ as BuiltinApiKeysCollection, $ as BuiltinMediaCollection, a0 as BuiltinMediaFoldersCollection, a1 as BuiltinUsersCollection, a2 as BundleHints, a3 as CliCommand, a4 as CollectionAccess, a5 as CollectionAdmin, a6 as CollectionConfig, a7 as CollectionDocument, a8 as CollectionHooks, a9 as CollectionListAdmin, aa as CollectionSlug, ab as Config, ac as ConfigError, ad as ConfigIssue, ae as ContainerAdmin, af as CreateEasyCMSOptions, ag as CreateInput, ah as CreateMigrationResult, ai as CursorDocs, aj as CustomField, ak as CustomFieldTypes, al as DashboardWidget, am as Database, an as DatabaseAdapter, ao as DatabaseInitArgs, ap as DatabaseTransfer, aq as DateField, ar as DepthOptions, as as DirectUpload, at as DiskStorageOptions, au as DocumentOf, av as EasyCMS, aw as EasyCMSError, ax as EmailAdapter, ay as EmailField, az as EmailMessage, aA as Endpoint, aB as EndpointRequest, aC as ErrorCode, aD as FIELD_TYPES, aE as Field, aF as FieldAccess, aG as FieldAccessFn, aH as FieldAdmin, aI as FieldError, aJ as FieldRule, aK as FieldType, aL as FieldTypeBase, aM as FieldTypeDefinition, aN as FieldTypeValidateContext, aO as FieldValidate, aP as FieldValidateContext, aQ as FieldValue, aR as FieldsInput, aS as FieldsValue, aT as FilterOptions, aU as FilterOptionsArgs, aV as FindAfterArgs, aW as FindArgs, aX as FindOptions, aY as FolderLevel, aZ as FolderPermissions, a_ as ForbiddenError, a$ as GlobalAccess, b0 as GlobalConfig, b1 as GlobalDocument, b2 as GlobalHooks, b3 as GlobalInput, b4 as GlobalSlug, b5 as GroupField, b6 as ImageSize, b7 as IncrementOptions, b8 as InferCollection, b9 as InferGlobal, ba as JobConfig, bb as JsonField, bc as KeysetPage, bd as Label, be as LayoutNode, bf as LivePreview, bg as LocaleOf, bh as LocalizationConfig, bi as LogDetails, bj as Logger, bk as LoginArgs, bl as LogsConfig, bm as MediaDocument, bn as MediaSize, bo as MembersConfig, bp as MembersSignup, bq as MigrationInfo, br as NavGroup, bs as NotFoundError, bt as NumberField, bu as OnRequest, bv as OnRequestArgs, bw as Operation, bx as OutboxRow, by as PLUGIN_API_VERSION, bz as PasswordEmail, bA as PasswordEmailArgs, bB as PasswordEmailFn, bC as PasswordLinkOptions, bD as PayloadTooLargeError, bE as Plugin, bF as PluginInfo, bG as PluginTypes, bH as PreviewTarget, bI as PreviewURL, bJ as QueryError, bK as ROLE_OPERATIONS, bL as RawDocument, bM as ReadOptions, bN as RelationshipField, bO as RequestContext, bP as ResolvedConfig, bQ as RichTextDocument, bR as RichTextField, bS as Role, bT as RoleOperation, bU as RolePermissions, bV as Roles, bW as RoutesConfig, bX as ScheduledPublish, bY as SchemaError, bZ as SchemaMode, b_ as SelectField, b$ as SelectOption, c0 as Session, c1 as SidebarPanel, c2 as SignupArgs, c3 as SignupResult, c4 as SingleSignOn, c5 as SlugField, c6 as SlugOf, c7 as SsoOutcome, c8 as SsoProviderRef, c9 as StorageAdapter, ca as StoredFile, cb as TextField, cc as TextareaField, cd as TooManyRequestsError, ce as TypedPlugin, cf as UnauthorizedError, cg as UncountedDocs, ch as UpdateInput, ci as UpdateOptions, cj as UploadConfig, ck as UploadField, cl as UploadFromURLConfig, cm as UserIdentity, cn as ValidationError, co as Version, cp as VersionSummary, cq as VersionsConfig, cr as WEBHOOK_EVENTS, cs as WebhookConfig, ct as WebhookEvent, cu as WebhookPayload, cv as Where, cw as WhereOperators, cx as anyone, cy as consoleEmail, cz as consoleLogger, cA as createEasyCMS, cB as defineConfig, cC as defineFieldType, cD as definePlugin, cE as diskStorage, cF as hasRows, cG as isAdmin, cH as isHasMany, cI as isSignedIn, cJ as isStaff, cK as isSystemAdmin, cL as jsonLogger, cM as mimeAllowedBy, cN as rowFields, cO as silentLogger, cP as uniqueWithinOf } from './config-CHEEPk1d.js';
|
|
3
|
+
export { A as ADMIN_API_VERSION, a as AdminCollection, b as AdminComponentRef, c as AdminElementProps, d as AdminField, e as AdminGlobal, f as AdminLayoutNode, g as AdminNavGroup, h as AdminNavItem, i as AdminNavNode, j as AdminPageRef, k as AdminPageRoute, l as AdminSchema, m as AdminWidgetRef, S as SetFieldDetail, w as warnDeprecated } from './plugin-B8xypoWd.js';
|
|
4
|
+
export { A as AdminBackup, a as AdminBackups, b as ApiHandlerOptions, B as BackupState, L as LoadConfigOptions, R as RestHandler, c as RestHandlerOptions, d as createApiHandler, e as createRestHandler, l as loadConfig, r as resolveConfig, s as sharedEasyCMS, f as slugify } from './handler-adKUq8-U.js';
|
|
5
5
|
export { FieldCondition, matchesCondition } from './conditions.js';
|
|
6
6
|
|
|
7
7
|
type DeliveryKind = 'webhook' | 'email';
|
|
@@ -112,9 +112,30 @@ interface AdminScheduledJob {
|
|
|
112
112
|
global?: string;
|
|
113
113
|
doc: ID;
|
|
114
114
|
}
|
|
115
|
+
/** An outbox entry (`outbox: true`) that failed: being retried, or given up on. */
|
|
116
|
+
interface AdminOutboxEntry {
|
|
117
|
+
id: ID;
|
|
118
|
+
event: 'create' | 'update' | 'draft' | 'delete';
|
|
119
|
+
/** The document's collection, or the global (`global`), and the document's id. */
|
|
120
|
+
collection?: string;
|
|
121
|
+
global?: string;
|
|
122
|
+
doc: string | null;
|
|
123
|
+
attempts: number;
|
|
124
|
+
/** `pending`: retried at `nextAttemptAt`; `failed`: given up, until someone retries it. */
|
|
125
|
+
state: 'pending' | 'running' | 'failed';
|
|
126
|
+
error: string | null;
|
|
127
|
+
nextAttemptAt: string;
|
|
128
|
+
}
|
|
129
|
+
interface AdminOutbox {
|
|
130
|
+
/** Entries waiting to run (new, or to be retried). */
|
|
131
|
+
waiting: number;
|
|
132
|
+
failed: number;
|
|
133
|
+
/** Entries that failed at least once, latest first (at most 100). */
|
|
134
|
+
entries: AdminOutboxEntry[];
|
|
135
|
+
}
|
|
115
136
|
/** What Settings → Jobs shows (`GET <api>/admin/jobs`). */
|
|
116
137
|
/** Work Easy CMS itself does with every run of the jobs, when it is set up. */
|
|
117
|
-
type BuiltInJob = 'scheduled' | 'webhooks' | 'emails' | 'backups' | 'audit';
|
|
138
|
+
type BuiltInJob = 'scheduled' | 'webhooks' | 'emails' | 'outbox' | 'backups' | 'audit';
|
|
118
139
|
interface AdminJobs {
|
|
119
140
|
jobs: AdminJob[];
|
|
120
141
|
/** When jobs last ran (the scheduler, the cron endpoint), `null` if never. */
|
|
@@ -127,6 +148,8 @@ interface AdminJobs {
|
|
|
127
148
|
canChange: boolean;
|
|
128
149
|
/** `null` when nothing uses `schedule`. */
|
|
129
150
|
scheduled: AdminScheduledJob[] | null;
|
|
151
|
+
/** `null` without `outbox: true`. */
|
|
152
|
+
outbox: AdminOutbox | null;
|
|
130
153
|
}
|
|
131
154
|
|
|
132
155
|
interface SearchHit {
|
|
@@ -233,4 +256,4 @@ declare const cacheTags: {
|
|
|
233
256
|
declare const VERSION: string;
|
|
234
257
|
|
|
235
258
|
export { EmailSetting, ID, PaginatedDocs, VERSION, cacheTags };
|
|
236
|
-
export type { AdminAttention, AdminDeliveries, AdminDelivery, AdminEmail, AdminEmailDelivery, AdminJob, AdminJobs, AdminScheduledJob, AdminStatus, AdminWebhookDelivery, BuiltInJob, DeliveryKind, DeliveryState, EmailCheck, SearchHit };
|
|
259
|
+
export type { AdminAttention, AdminDeliveries, AdminDelivery, AdminEmail, AdminEmailDelivery, AdminJob, AdminJobs, AdminOutbox, AdminOutboxEntry, AdminScheduledJob, AdminStatus, AdminWebhookDelivery, BuiltInJob, DeliveryKind, DeliveryState, EmailCheck, SearchHit };
|
package/dist/index.js
CHANGED
package/dist/internal.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Config, Database, EasyCMS, NavGroup, CollectionConfig, ID, RawDocument, Field, ResolvedConfig, ConfigIssue } from './index.js';
|
|
2
|
-
export {
|
|
3
|
-
export { C as CONFIG_FILE_NAMES, g as CSRF_COOKIE, h as CSRF_HEADER, D as DEFAULT_ADMIN_PATH, i as DEFAULT_API_PATH, j as DEFAULT_MAX_FILE_SIZE, k as DEFAULT_TOKEN_EXPIRATION, m as applyDefaults, n as applyPlugins, o as createRootEndpointHandler, p as decryptBackup, q as fillMissing, t as findConfigFile, u as forwardedClientIp, v as generateSlugs, w as importConfig, x as mergeForUpdate, y as parseId, z as platformClientIp, E as readCookie, F as validateFields, G as writeBackupFile } from './handler-
|
|
2
|
+
export { cQ as API_KEYS, cR as API_KEY_GLOBAL_OPERATIONS, cS as API_KEY_OPERATIONS, cT as DEFAULT_MAX_VERSIONS, cU as DEFAULT_PASSWORD_EMAILS, cV as FIELD_TYPE_BASES, cW as FOLDER_LEVELS, cX as MediaFolders, cY as SSO_COOKIE, cZ as codeOfStatus, cF as hasRows, cH as isHasMany, c_ as isJsonLogger, c$ as keyAllows, d0 as logFormat, cM as mimeAllowedBy, cN as rowFields, cP as uniqueWithinOf } from './config-CHEEPk1d.js';
|
|
3
|
+
export { C as CONFIG_FILE_NAMES, g as CSRF_COOKIE, h as CSRF_HEADER, D as DEFAULT_ADMIN_PATH, i as DEFAULT_API_PATH, j as DEFAULT_MAX_FILE_SIZE, k as DEFAULT_TOKEN_EXPIRATION, m as applyDefaults, n as applyPlugins, o as createRootEndpointHandler, p as decryptBackup, q as fillMissing, t as findConfigFile, u as forwardedClientIp, v as generateSlugs, w as importConfig, x as mergeForUpdate, y as parseId, z as platformClientIp, E as readCookie, F as validateFields, G as writeBackupFile } from './handler-adKUq8-U.js';
|
|
4
4
|
export { conditionIssues } from './conditions.js';
|
|
5
5
|
|
|
6
6
|
/** The session cookie the REST API sets at login. */
|
package/dist/internal.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { bd as Label, aE as Field, I as ID, k as AdminLocale, t as AdminSwitcher,
|
|
1
|
+
import { bd as Label, aE as Field, I as ID, k as AdminLocale, t as AdminSwitcher, c8 as SsoProviderRef } from './config-CHEEPk1d.js';
|
|
2
2
|
import { FieldCondition } from './conditions.js';
|
|
3
3
|
|
|
4
4
|
/** A Web Component from an admin module, with its `props` (plain JSON). */
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { a as ADMIN_ICONS, g as AdminComponent, j as AdminIcon, S as AuthUser, a3 as CliCommand, a4 as CollectionAccess, a6 as CollectionConfig, ab as Config, ac as ConfigError, al as DashboardWidget, aw as EasyCMSError, aA as Endpoint, aB as EndpointRequest, aC as ErrorCode, aE as Field, aF as FieldAccess, aI as FieldError, aL as FieldTypeBase, aM as FieldTypeDefinition, aN as FieldTypeValidateContext, a_ as ForbiddenError, b0 as GlobalConfig, I as ID, ba as JobConfig, bd as Label, br as NavGroup, bs as NotFoundError, bu as OnRequest, bv as OnRequestArgs,
|
|
2
|
-
export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-
|
|
1
|
+
export { a as ADMIN_ICONS, g as AdminComponent, j as AdminIcon, S as AuthUser, a3 as CliCommand, a4 as CollectionAccess, a6 as CollectionConfig, ab as Config, ac as ConfigError, al as DashboardWidget, aw as EasyCMSError, aA as Endpoint, aB as EndpointRequest, aC as ErrorCode, aE as Field, aF as FieldAccess, aI as FieldError, aL as FieldTypeBase, aM as FieldTypeDefinition, aN as FieldTypeValidateContext, a_ as ForbiddenError, b0 as GlobalConfig, I as ID, ba as JobConfig, bd as Label, br as NavGroup, bs as NotFoundError, bu as OnRequest, bv as OnRequestArgs, by as PLUGIN_API_VERSION, bD as PayloadTooLargeError, bE as Plugin, bF as PluginInfo, bG as PluginTypes, bJ as QueryError, bO as RequestContext, cd as TooManyRequestsError, ce as TypedPlugin, cf as UnauthorizedError, cn as ValidationError, cv as Where, cC as defineFieldType, cD as definePlugin } from './config-CHEEPk1d.js';
|
|
2
|
+
export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-B8xypoWd.js';
|
|
3
3
|
import './conditions.js';
|