@easy-cms/core 1.24.0 → 1.25.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.
@@ -155,6 +155,7 @@ var USER_IDENTITIES = "user-identities";
155
155
  var AUDIT_LOGS = "audit-logs";
156
156
  var OUTBOX = "outbox";
157
157
  var RATE_LIMITS = "rate-limits";
158
+ var BULK_OPERATIONS = "bulk-operations";
158
159
  var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
159
160
  SESSIONS,
160
161
  LOGIN_ATTEMPTS,
@@ -168,7 +169,8 @@ var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
168
169
  USER_IDENTITIES,
169
170
  AUDIT_LOGS,
170
171
  OUTBOX,
171
- RATE_LIMITS
172
+ RATE_LIMITS,
173
+ BULK_OPERATIONS
172
174
  ]);
173
175
  var DEFAULT_ROLES = ["admin", "editor"];
174
176
  var CREATED_BY_FIELD = {
@@ -528,6 +530,34 @@ var scheduledJobsCollection = {
528
530
  { name: "author", type: "number" }
529
531
  ]
530
532
  };
533
+ var bulkOperationsCollection = {
534
+ slug: BULK_OPERATIONS,
535
+ access: { read: nobody, create: nobody, update: nobody, delete: nobody },
536
+ fields: [
537
+ { name: "collection", type: "text", required: true },
538
+ // publish | unpublish | delete | move
539
+ { name: "action", type: "text", required: true },
540
+ // The list's `where`, and for `move` the folder (`null`: the top).
541
+ { name: "query", type: "json" },
542
+ { name: "folder", type: "text" },
543
+ { name: "transferTo", type: "text" },
544
+ { name: "user", type: "text", required: true, index: true },
545
+ // pending | running | cancelling | done | cancelled | failed
546
+ { name: "state", type: "text", required: true, index: true },
547
+ { name: "total", type: "number", required: true },
548
+ { name: "done", type: "number", required: true },
549
+ { name: "skipped", type: "number", required: true },
550
+ { name: "failed", type: "number", required: true },
551
+ // The last id handled: the next step continues after it, in id order.
552
+ { name: "after", type: "json" },
553
+ // The first documents skipped or failed, with why: `{ id, reason, message? }`.
554
+ { name: "problems", type: "json" },
555
+ { name: "error", type: "text" },
556
+ // A step holds the operation until then; a process that stopped lets it go.
557
+ { name: "leaseUntil", type: "text" },
558
+ { name: "finishedAt", type: "text" }
559
+ ]
560
+ };
531
561
  var webhookDeliveriesCollection = {
532
562
  slug: WEBHOOK_DELIVERIES,
533
563
  access: { read: nobody, create: nobody, update: nobody, delete: nobody },
@@ -3000,9 +3030,9 @@ async function pendingMigrations(cms) {
3000
3030
  if (pending === 0) migrated.add(cms);
3001
3031
  return pending;
3002
3032
  }
3003
- async function healthResponse(check3, instance) {
3033
+ async function healthResponse(check4, instance) {
3004
3034
  const cms = instance;
3005
- if (check3 === "healthz") return new Response("ok", { headers: { "cache-control": "no-store" } });
3035
+ if (check4 === "healthz") return new Response("ok", { headers: { "cache-control": "no-store" } });
3006
3036
  if (!cms) {
3007
3037
  return Response.json(
3008
3038
  { status: "error", checks: { startup: "error" } },
@@ -4300,6 +4330,306 @@ zzzzzzzz
4300
4330
  `;
4301
4331
  var COMMON_PASSWORDS = new Set(LIST.split(/\s+/).filter(Boolean));
4302
4332
 
4333
+ // src/bulk.ts
4334
+ var BULK_ACTIONS = ["publish", "unpublish", "delete", "move"];
4335
+ var BULK_STEP = 100;
4336
+ var BULK_MAX = 1e4;
4337
+ var LEASE_MS = 6e4;
4338
+ var PROBLEMS_KEPT = 50;
4339
+ var FINISHED_KEPT = 10;
4340
+ var ACTIVE = ["pending", "running", "cancelling"];
4341
+ function check(cms, collection2, action, options) {
4342
+ if (!BULK_ACTIONS.includes(action))
4343
+ throw new QueryError(`action must be one of ${BULK_ACTIONS.join(", ")}`);
4344
+ const config = cms.collection(collection2);
4345
+ if ((action === "publish" || action === "unpublish") && !config.drafts)
4346
+ throw new QueryError(`"${collection2}" has no drafts to publish`);
4347
+ if (action === "move" && collection2 !== MEDIA)
4348
+ throw new QueryError("Only media files move to folders");
4349
+ if (action === "move" && options.folder === void 0)
4350
+ throw new QueryError("folder is required to move files");
4351
+ }
4352
+ async function apply(cms, collection2, action, id, options) {
4353
+ const access = {
4354
+ overrideAccess: false,
4355
+ user: options.user,
4356
+ ...options.context ? { context: options.context } : {},
4357
+ depth: 0
4358
+ };
4359
+ const slug2 = collection2;
4360
+ try {
4361
+ if (action === "publish") await cms.update(slug2, id, { status: "published" }, access);
4362
+ else if (action === "unpublish") await cms.unpublish(slug2, id, access);
4363
+ else if (action === "move")
4364
+ await cms.update(slug2, id, { folder: options.folder ?? null }, access);
4365
+ else
4366
+ await cms.delete(slug2, id, {
4367
+ ...access,
4368
+ ...options.transferTo !== void 0 ? { transferTo: options.transferTo } : {}
4369
+ });
4370
+ return null;
4371
+ } catch (error) {
4372
+ if (error instanceof ForbiddenError) return { id, reason: "forbidden" };
4373
+ if (error instanceof NotFoundError) return { id, reason: "not-found" };
4374
+ return { id, reason: "error", message: error.message };
4375
+ }
4376
+ }
4377
+ async function bulkIds(cms, collection2, action, ids, options) {
4378
+ check(cms, collection2, action, options);
4379
+ if (ids.length > BULK_STEP) throw new QueryError(`ids: at most ${BULK_STEP} at a time`);
4380
+ const result = { done: 0, skipped: [], failed: [] };
4381
+ for (const id of new Set(ids)) {
4382
+ const problem = await apply(cms, collection2, action, id, options);
4383
+ if (!problem) result.done++;
4384
+ else if (problem.reason === "error") result.failed.push(problem);
4385
+ else result.skipped.push(problem);
4386
+ }
4387
+ return result;
4388
+ }
4389
+ async function startBulk(cms, collection2, action, where, options) {
4390
+ check(cms, collection2, action, options);
4391
+ const total = await cms.count(collection2, {
4392
+ ...where ? { where } : {},
4393
+ draft: true,
4394
+ overrideAccess: false,
4395
+ user: options.user,
4396
+ ...options.context ? { context: options.context } : {},
4397
+ max: BULK_MAX
4398
+ });
4399
+ if (total > BULK_MAX)
4400
+ throw new QueryError(
4401
+ `More than ${BULK_MAX.toLocaleString("en")} documents match: filter the list further`
4402
+ );
4403
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4404
+ const row = await cms.db.create({
4405
+ collection: BULK_OPERATIONS,
4406
+ data: {
4407
+ collection: collection2,
4408
+ action,
4409
+ query: where ?? null,
4410
+ folder: options.folder === void 0 ? null : JSON.stringify(options.folder),
4411
+ transferTo: options.transferTo === void 0 ? null : JSON.stringify(options.transferTo),
4412
+ user: String(options.user.id),
4413
+ state: total === 0 ? "done" : "pending",
4414
+ total,
4415
+ done: 0,
4416
+ skipped: 0,
4417
+ failed: 0,
4418
+ after: null,
4419
+ problems: [],
4420
+ error: null,
4421
+ leaseUntil: null,
4422
+ finishedAt: total === 0 ? now : null,
4423
+ createdAt: now,
4424
+ updatedAt: now
4425
+ }
4426
+ });
4427
+ await cms.audit.record({
4428
+ action: `bulk.${action}`,
4429
+ target: collection2,
4430
+ detail: { total, operation: String(row.id) }
4431
+ });
4432
+ await pruneFinished(cms, String(options.user.id));
4433
+ if (total > 0) void runBulk(cms, row.id).catch(() => {
4434
+ });
4435
+ return toOperation(row);
4436
+ }
4437
+ async function runBulk(cms, id) {
4438
+ while (await stepBulk(cms, id)) {
4439
+ }
4440
+ }
4441
+ async function stepBulk(cms, id, now = /* @__PURE__ */ new Date()) {
4442
+ const row = await cms.db.findById({ collection: BULK_OPERATIONS, id });
4443
+ if (!row || !ACTIVE.includes(String(row.state))) return false;
4444
+ const lease = row.leaseUntil ? Date.parse(String(row.leaseUntil)) : 0;
4445
+ if (lease > now.getTime()) return false;
4446
+ const claimedAt = new Date(Math.max(now.getTime(), Date.parse(String(row.updatedAt)) + 1));
4447
+ const claimed = await save(cms, row, {
4448
+ state: row.state === "pending" ? "running" : row.state,
4449
+ leaseUntil: new Date(now.getTime() + LEASE_MS).toISOString(),
4450
+ updatedAt: claimedAt.toISOString()
4451
+ });
4452
+ if (!claimed) return false;
4453
+ if (claimed.state === "cancelling") {
4454
+ await finish(cms, claimed, "cancelled");
4455
+ return false;
4456
+ }
4457
+ const user = await cms.auth.activeUser(String(claimed.user));
4458
+ if (!user) {
4459
+ await finish(cms, claimed, "failed", "The user who started it is no longer active");
4460
+ return false;
4461
+ }
4462
+ const collection2 = String(claimed.collection);
4463
+ const action = claimed.action;
4464
+ const options = {
4465
+ user,
4466
+ ...claimed.folder !== null && claimed.folder !== void 0 ? { folder: JSON.parse(String(claimed.folder)) } : {},
4467
+ ...claimed.transferTo !== null && claimed.transferTo !== void 0 ? { transferTo: JSON.parse(String(claimed.transferTo)) } : {}
4468
+ };
4469
+ const handled = Number(claimed.done) + Number(claimed.skipped) + Number(claimed.failed);
4470
+ const query = claimed.query ?? void 0;
4471
+ const after = claimed.after;
4472
+ const conditions = [query, after === null ? void 0 : { id: { gt: after } }].filter(
4473
+ (w) => w !== void 0
4474
+ );
4475
+ let found;
4476
+ try {
4477
+ found = await cms.find(collection2, {
4478
+ ...conditions.length ? { where: { and: conditions } } : {},
4479
+ sort: "id",
4480
+ limit: Math.min(BULK_STEP, BULK_MAX - handled),
4481
+ depth: 0,
4482
+ draft: true,
4483
+ count: false,
4484
+ overrideAccess: false,
4485
+ user
4486
+ });
4487
+ } catch (error) {
4488
+ await finish(cms, claimed, "failed", error.message);
4489
+ return false;
4490
+ }
4491
+ const changes = {
4492
+ done: Number(claimed.done),
4493
+ skipped: Number(claimed.skipped),
4494
+ failed: Number(claimed.failed),
4495
+ problems: [...claimed.problems ?? []],
4496
+ after
4497
+ };
4498
+ await auditContext.run({ user }, async () => {
4499
+ for (const doc of found.docs) {
4500
+ const problem = await apply(cms, collection2, action, doc.id, options);
4501
+ if (!problem) changes.done++;
4502
+ else {
4503
+ if (problem.reason === "error") changes.failed++;
4504
+ else changes.skipped++;
4505
+ if (changes.problems.length < PROBLEMS_KEPT) changes.problems.push(problem);
4506
+ }
4507
+ changes.after = doc.id;
4508
+ }
4509
+ });
4510
+ const finished = !found.hasNextPage || found.docs.length === 0 || changes.done + changes.skipped + changes.failed >= BULK_MAX;
4511
+ const at = /* @__PURE__ */ new Date();
4512
+ const saved = await save(cms, claimed, {
4513
+ ...changes,
4514
+ state: finished ? "done" : "running",
4515
+ leaseUntil: null,
4516
+ finishedAt: finished ? at.toISOString() : null,
4517
+ updatedAt: later2(at, claimed).toISOString()
4518
+ });
4519
+ if (saved) return !finished;
4520
+ const current = await cms.db.findById({ collection: BULK_OPERATIONS, id });
4521
+ if (current?.state === "cancelling") await finish(cms, { ...current, ...changes }, "cancelled");
4522
+ else if (current) await save(cms, current, { ...changes, leaseUntil: null });
4523
+ return false;
4524
+ }
4525
+ function later2(at, row) {
4526
+ return new Date(Math.max(at.getTime(), Date.parse(String(row.updatedAt)) + 1));
4527
+ }
4528
+ async function save(cms, row, changes) {
4529
+ const { id, ...data } = row;
4530
+ const next = { ...data, ...changes };
4531
+ const saved = await cms.db.update({
4532
+ collection: BULK_OPERATIONS,
4533
+ id,
4534
+ data: next,
4535
+ where: { updatedAt: { equals: row.updatedAt } }
4536
+ });
4537
+ return saved ? { ...row, ...changes } : null;
4538
+ }
4539
+ async function finish(cms, row, state, error) {
4540
+ const at = /* @__PURE__ */ new Date();
4541
+ await save(cms, row, {
4542
+ state,
4543
+ leaseUntil: null,
4544
+ finishedAt: at.toISOString(),
4545
+ updatedAt: later2(at, row).toISOString(),
4546
+ ...error ? { error } : {}
4547
+ });
4548
+ }
4549
+ async function runBulkOperations(cms, now = /* @__PURE__ */ new Date(), budgetMs = 2e4) {
4550
+ const { docs } = await cms.db.find({
4551
+ collection: BULK_OPERATIONS,
4552
+ where: { state: { in: ACTIVE } },
4553
+ sort: ["createdAt"],
4554
+ limit: 20,
4555
+ page: 1
4556
+ });
4557
+ const until = Date.now() + budgetMs;
4558
+ let steps = 0;
4559
+ for (const row of docs) {
4560
+ while (Date.now() < until) {
4561
+ const more = await stepBulk(cms, row.id, now).catch((error) => {
4562
+ cms.logger.error(`Bulk operation ${row.id}: ${error.message}`, { error });
4563
+ return false;
4564
+ });
4565
+ steps++;
4566
+ if (!more) break;
4567
+ }
4568
+ }
4569
+ return { steps };
4570
+ }
4571
+ async function bulkProgress(cms, id, user) {
4572
+ const row = await mine(cms, id, user);
4573
+ if (ACTIVE.includes(String(row.state))) await stepBulk(cms, row.id);
4574
+ return toOperation(await cms.db.findById({ collection: BULK_OPERATIONS, id: row.id }) ?? row);
4575
+ }
4576
+ async function bulkOperations(cms, user) {
4577
+ const { docs } = await cms.db.find({
4578
+ collection: BULK_OPERATIONS,
4579
+ where: { user: { equals: String(user.id) } },
4580
+ sort: ["-createdAt"],
4581
+ limit: FINISHED_KEPT + 20,
4582
+ page: 1
4583
+ });
4584
+ return docs.map(toOperation);
4585
+ }
4586
+ async function cancelBulk(cms, id, user) {
4587
+ for (let attempt = 0; attempt < 5; attempt++) {
4588
+ const row = await mine(cms, id, user);
4589
+ if (!ACTIVE.includes(String(row.state))) return toOperation(row);
4590
+ const at = /* @__PURE__ */ new Date();
4591
+ const saved = row.leaseUntil ? await save(cms, row, { state: "cancelling", updatedAt: later2(at, row).toISOString() }) : await save(cms, row, {
4592
+ state: "cancelled",
4593
+ finishedAt: at.toISOString(),
4594
+ updatedAt: later2(at, row).toISOString()
4595
+ });
4596
+ if (saved) return toOperation(saved);
4597
+ }
4598
+ return toOperation(await mine(cms, id, user));
4599
+ }
4600
+ async function mine(cms, id, user) {
4601
+ const row = await cms.db.findById({ collection: BULK_OPERATIONS, id });
4602
+ if (!row || String(row.user) !== String(user.id)) throw new NotFoundError(BULK_OPERATIONS, id);
4603
+ return row;
4604
+ }
4605
+ async function pruneFinished(cms, user) {
4606
+ const { docs } = await cms.db.find({
4607
+ collection: BULK_OPERATIONS,
4608
+ where: { and: [{ user: { equals: user } }, { state: { not_in: ACTIVE } }] },
4609
+ sort: ["-createdAt"],
4610
+ limit: 100,
4611
+ page: 1
4612
+ });
4613
+ for (const old of docs.slice(FINISHED_KEPT))
4614
+ await cms.db.delete({ collection: BULK_OPERATIONS, id: old.id });
4615
+ }
4616
+ function toOperation(row) {
4617
+ return {
4618
+ id: row.id,
4619
+ collection: String(row.collection),
4620
+ action: row.action,
4621
+ state: row.state,
4622
+ total: Number(row.total),
4623
+ done: Number(row.done),
4624
+ skipped: Number(row.skipped),
4625
+ failed: Number(row.failed),
4626
+ problems: row.problems ?? [],
4627
+ error: row.error ?? null,
4628
+ createdAt: String(row.createdAt),
4629
+ finishedAt: row.finishedAt ?? null
4630
+ };
4631
+ }
4632
+
4303
4633
  // src/localization.ts
4304
4634
  var ALL_LOCALES = "all";
4305
4635
  var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -4400,7 +4730,7 @@ function localizeSort(fields, sort, locale, locales = []) {
4400
4730
  var DELAYS = [6e4, 5 * 6e4, 30 * 6e4, 2 * 36e5, 12 * 36e5];
4401
4731
  var LEASE = 5 * 6e4;
4402
4732
  var BATCH2 = 50;
4403
- var later2 = (from, ms) => new Date(from.getTime() + ms).toISOString();
4733
+ var later3 = (from, ms) => new Date(from.getTime() + ms).toISOString();
4404
4734
  var json = (value) => value === void 0 ? null : JSON.stringify(value);
4405
4735
  var parse2 = (value) => typeof value === "string" ? JSON.parse(value) : void 0;
4406
4736
  function withoutHash(value) {
@@ -4520,7 +4850,7 @@ var Outbox = class {
4520
4850
  ...data,
4521
4851
  state: "running",
4522
4852
  attempts,
4523
- nextAttemptAt: later2(now, LEASE),
4853
+ nextAttemptAt: later3(now, LEASE),
4524
4854
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4525
4855
  };
4526
4856
  const claimed = await this.db.update({
@@ -4553,7 +4883,7 @@ var Outbox = class {
4553
4883
  data: {
4554
4884
  ...current,
4555
4885
  state: delay === void 0 ? "failed" : "pending",
4556
- nextAttemptAt: later2(now, delay ?? 0),
4886
+ nextAttemptAt: later3(now, delay ?? 0),
4557
4887
  error: message2,
4558
4888
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4559
4889
  }
@@ -4855,7 +5185,7 @@ async function fetchRemoteFile(link, options) {
4855
5185
  const deadline = Date.now() + (options.timeout ?? 15e3);
4856
5186
  const maxRedirects = options.maxRedirects ?? 3;
4857
5187
  for (let redirects = 0; ; redirects++) {
4858
- check(url, options);
5188
+ check2(url, options);
4859
5189
  const response = await get(url, options, deadline);
4860
5190
  const status = response.statusCode ?? 0;
4861
5191
  if (status >= 300 && status < 400 && response.headers.location) {
@@ -4877,7 +5207,7 @@ async function fetchRemoteFile(link, options) {
4877
5207
  };
4878
5208
  }
4879
5209
  }
4880
- function check(url, options) {
5210
+ function check2(url, options) {
4881
5211
  if (url.protocol !== "http:" && url.protocol !== "https:")
4882
5212
  throw new RemoteFileError("must be an http(s) link");
4883
5213
  if (url.username || url.password) throw new RemoteFileError("must not contain a password");
@@ -5043,7 +5373,7 @@ function distance(a, b) {
5043
5373
  }
5044
5374
  return row[b.length];
5045
5375
  }
5046
- function check2(value, path, scope, known, add) {
5376
+ function check3(value, path, scope, known, add) {
5047
5377
  if (!isObject(value)) return;
5048
5378
  for (const key of Object.keys(value)) {
5049
5379
  if (known.has(key)) continue;
@@ -5066,8 +5396,8 @@ function checkFields(fields, path, add) {
5066
5396
  if (!isObject(field)) continue;
5067
5397
  const at = `${path}.${typeof field.name === "string" ? field.name : `[${i}]`}`;
5068
5398
  const known = typeof field.type === "string" ? FIELD[field.type] : void 0;
5069
- if (known && field.customType === void 0) check2(field, at, "field", known, add);
5070
- check2(field.admin, `${at}.admin`, "fieldAdmin", FIELD_ADMIN, add);
5399
+ if (known && field.customType === void 0) check3(field, at, "field", known, add);
5400
+ check3(field.admin, `${at}.admin`, "fieldAdmin", FIELD_ADMIN, add);
5071
5401
  checkFields(field.fields, `${at}.fields`, add);
5072
5402
  if (Array.isArray(field.blocks)) {
5073
5403
  for (const block of field.blocks)
@@ -5077,21 +5407,21 @@ function checkFields(fields, path, add) {
5077
5407
  }
5078
5408
  function checkConfigKeys(config, add) {
5079
5409
  const c = config;
5080
- check2(c, "", "config", CONFIG, add);
5081
- check2(c.admin, "admin", "admin", ADMIN, add);
5410
+ check3(c, "", "config", CONFIG, add);
5411
+ check3(c.admin, "admin", "admin", ADMIN, add);
5082
5412
  const admin = isObject(c.admin) ? c.admin : {};
5083
5413
  if (Array.isArray(admin.commands)) {
5084
5414
  for (const [i, command] of admin.commands.entries())
5085
5415
  if (isObject(command) && "to" in command)
5086
5416
  add({ path: `admin.commands[${i}].to`, message: "is now `href`" });
5087
5417
  }
5088
- check2(c.auth, "auth", "auth", AUTH, add);
5089
- if (isObject(c.auth)) check2(c.auth.members, "auth.members", "members", MEMBERS, add);
5090
- check2(c.upload, "upload", "upload", UPLOAD, add);
5091
- check2(c.backups, "backups", "backups", BACKUPS, add);
5092
- if (isObject(c.audit)) check2(c.audit, "audit", "audit", AUDIT, add);
5093
- if (isObject(c.rateLimit)) check2(c.rateLimit, "rateLimit", "rateLimit", RATE_LIMIT, add);
5094
- check2(c.logs, "logs", "logs", LOGS, add);
5418
+ check3(c.auth, "auth", "auth", AUTH, add);
5419
+ if (isObject(c.auth)) check3(c.auth.members, "auth.members", "members", MEMBERS, add);
5420
+ check3(c.upload, "upload", "upload", UPLOAD, add);
5421
+ check3(c.backups, "backups", "backups", BACKUPS, add);
5422
+ if (isObject(c.audit)) check3(c.audit, "audit", "audit", AUDIT, add);
5423
+ if (isObject(c.rateLimit)) check3(c.rateLimit, "rateLimit", "rateLimit", RATE_LIMIT, add);
5424
+ check3(c.logs, "logs", "logs", LOGS, add);
5095
5425
  for (const [kind, list] of [
5096
5426
  ["collections", c.collections],
5097
5427
  ["globals", c.globals]
@@ -5101,8 +5431,8 @@ function checkConfigKeys(config, add) {
5101
5431
  for (const [i, item] of list.entries()) {
5102
5432
  if (!isObject(item)) continue;
5103
5433
  const at = `${kind}.${typeof item.slug === "string" ? item.slug : `[${i}]`}`;
5104
- check2(item, at, collection2 ? "collection" : "global", collection2 ? COLLECTION : GLOBAL, add);
5105
- check2(
5434
+ check3(item, at, collection2 ? "collection" : "global", collection2 ? COLLECTION : GLOBAL, add);
5435
+ check3(
5106
5436
  item.admin,
5107
5437
  `${at}.admin`,
5108
5438
  "admin-of",
@@ -5110,9 +5440,9 @@ function checkConfigKeys(config, add) {
5110
5440
  add
5111
5441
  );
5112
5442
  if (collection2 && isObject(item.admin) && isObject(item.admin.calendar))
5113
- check2(item.admin.calendar, `${at}.admin.calendar`, "calendar", keys("start end color"), add);
5443
+ check3(item.admin.calendar, `${at}.admin.calendar`, "calendar", keys("start end color"), add);
5114
5444
  if (isObject(item.versions))
5115
- check2(item.versions, `${at}.versions`, "versions", keys("keep"), add);
5445
+ check3(item.versions, `${at}.versions`, "versions", keys("keep"), add);
5116
5446
  checkFields(item.fields, `${at}.fields`, add);
5117
5447
  }
5118
5448
  }
@@ -5561,7 +5891,7 @@ var Webhooks = class {
5561
5891
  const claimed = await this.queue.claim(id, queued, {
5562
5892
  ...queued,
5563
5893
  attempts,
5564
- nextAttemptAt: later3(now, CLAIM2)
5894
+ nextAttemptAt: later4(now, CLAIM2)
5565
5895
  });
5566
5896
  if (!claimed) continue;
5567
5897
  const result = await this.attempt(hook, queued.event, queued.body, queued.delivery);
@@ -5575,7 +5905,7 @@ var Webhooks = class {
5575
5905
  await this.queue.update(id, {
5576
5906
  ...queued,
5577
5907
  attempts,
5578
- nextAttemptAt: later3(now, delay ?? 0),
5908
+ nextAttemptAt: later4(now, delay ?? 0),
5579
5909
  state: delay === void 0 ? "failed" : "pending",
5580
5910
  error: result.error
5581
5911
  });
@@ -5613,7 +5943,7 @@ var Webhooks = class {
5613
5943
  error
5614
5944
  });
5615
5945
  const { queue } = this;
5616
- const saved = queue ? this.saving.then(() => queue.add(entry(0, later3(/* @__PURE__ */ new Date(), CLAIM2), null))) : Promise.resolve(void 0);
5946
+ const saved = queue ? this.saving.then(() => queue.add(entry(0, later4(/* @__PURE__ */ new Date(), CLAIM2), null))) : Promise.resolve(void 0);
5617
5947
  this.saving = saved.catch(() => {
5618
5948
  });
5619
5949
  let queued;
@@ -5634,7 +5964,7 @@ var Webhooks = class {
5634
5964
  if (delay === void 0) {
5635
5965
  if (queued === void 0 || !this.queue)
5636
5966
  return this.giveUp(hook, event, result.error, attempt + 1);
5637
- const next = later3(/* @__PURE__ */ new Date(), QUEUE_DELAYS2[0]);
5967
+ const next = later4(/* @__PURE__ */ new Date(), QUEUE_DELAYS2[0]);
5638
5968
  await this.queue.update(queued, entry(attempt + 1, next, result.error));
5639
5969
  this.logger.warn(
5640
5970
  `Webhook ${hook.url} failed for ${event} (${result.error}); will retry later`
@@ -5682,7 +6012,7 @@ var Webhooks = class {
5682
6012
  );
5683
6013
  }
5684
6014
  };
5685
- var later3 = (from, ms) => new Date(from.getTime() + ms).toISOString();
6015
+ var later4 = (from, ms) => new Date(from.getTime() + ms).toISOString();
5686
6016
  var isContentEvent = (event) => WEBHOOK_EVENTS.includes(event);
5687
6017
  function matches(hook, payload) {
5688
6018
  if (!isContentEvent(payload.event)) return hook.events?.includes(payload.event) === true;
@@ -6593,7 +6923,7 @@ function validateNav(config, add) {
6593
6923
  add("admin.nav", "must be a list of groups", "e.g. nav: [{ id: 'shop', label: 'Shop' }]");
6594
6924
  else
6595
6925
  nav.forEach((group, i) => {
6596
- const check3 = (g, path, child) => {
6926
+ const check4 = (g, path, child) => {
6597
6927
  const { id, label, children, order } = g ?? {};
6598
6928
  if (typeof id !== "string" || !NAV_ID.test(id))
6599
6929
  add(`${path}.id`, "must be lowercase letters, digits and -", "e.g. 'shop'");
@@ -6606,11 +6936,11 @@ function validateNav(config, add) {
6606
6936
  else if (!Array.isArray(children)) add(`${path}.children`, "must be a list of groups");
6607
6937
  else
6608
6938
  children.forEach((c, j) => {
6609
- check3(c, `${path}.children[${j}]`, true);
6939
+ check4(c, `${path}.children[${j}]`, true);
6610
6940
  });
6611
6941
  }
6612
6942
  };
6613
- check3(group, `admin.nav[${i}]`, false);
6943
+ check4(group, `admin.nav[${i}]`, false);
6614
6944
  });
6615
6945
  }
6616
6946
  const commands = config.admin?.commands;
@@ -7388,6 +7718,7 @@ async function resolveConfig(input) {
7388
7718
  ...config.outbox ? [outboxCollection] : [],
7389
7719
  ...config.rateLimit ? [rateLimitsCollection] : [],
7390
7720
  databaseBackupsCollection,
7721
+ bulkOperationsCollection,
7391
7722
  ...config.auth?.rbac ? [rolesCollection] : [],
7392
7723
  ...(config.auth?.providers?.length ?? 0) > 0 ? [userIdentitiesCollection] : [],
7393
7724
  ...config.audit ? [auditLogsCollection] : []
@@ -7457,7 +7788,7 @@ function cacheableResponse(request, body, status, headers, times, tags) {
7457
7788
  }
7458
7789
 
7459
7790
  // src/version.ts
7460
- var VERSION = true ? "1.24.0" : "0.0.0-dev";
7791
+ var VERSION = true ? "1.25.0" : "0.0.0-dev";
7461
7792
 
7462
7793
  // src/telemetry.ts
7463
7794
  var DB_CALLS = /* @__PURE__ */ new Set([
@@ -9052,7 +9383,7 @@ var EasyCMS = class {
9052
9383
  const parsed = parseId(id);
9053
9384
  if (parsed === void 0) return { update: false, delete: false };
9054
9385
  const guard = { enforce: true, user, context };
9055
- const check3 = (operation) => this.checkDocumentAccess(config, operation, guard, parsed, void 0).then(
9386
+ const check4 = (operation) => this.checkDocumentAccess(config, operation, guard, parsed, void 0).then(
9056
9387
  () => true,
9057
9388
  (error) => {
9058
9389
  if (error instanceof ForbiddenError || error instanceof UnauthorizedError || error instanceof NotFoundError)
@@ -9060,7 +9391,7 @@ var EasyCMS = class {
9060
9391
  throw error;
9061
9392
  }
9062
9393
  );
9063
- return { update: await check3("update"), delete: await check3("delete") };
9394
+ return { update: await check4("update"), delete: await check4("delete") };
9064
9395
  }
9065
9396
  /** Closes the database connection. */
9066
9397
  /**
@@ -9287,7 +9618,11 @@ var EasyCMS = class {
9287
9618
  webhooks: await this.retryWebhooks(now),
9288
9619
  emails: await this.mailer.retry(now),
9289
9620
  jobs: await this.runConfigJobs(now),
9290
- outbox: await this.outbox?.run(now) ?? { ran: 0, failed: 0 }
9621
+ outbox: await this.outbox?.run(now) ?? { ran: 0, failed: 0 },
9622
+ bulk: await runBulkOperations(this, now).catch((error) => {
9623
+ this.logger.error(`Bulk actions: ${error.message}`, { error });
9624
+ return { steps: 0 };
9625
+ })
9291
9626
  };
9292
9627
  if (this.telemetry?.measuresQueues)
9293
9628
  await this.countQueues(now).catch(
@@ -9878,9 +10213,9 @@ var EasyCMS = class {
9878
10213
  );
9879
10214
  }
9880
10215
  /** Runs a check; a `ForbiddenError` for a document the user can't read becomes a 404. */
9881
- async hidingDenied(collection2, guard, id, check3) {
10216
+ async hidingDenied(collection2, guard, id, check4) {
9882
10217
  try {
9883
- await check3();
10218
+ await check4();
9884
10219
  } catch (error) {
9885
10220
  if (error instanceof ForbiddenError && !await this.mayRead(collection2, guard, id))
9886
10221
  throw new NotFoundError(collection2, String(id));
@@ -9976,7 +10311,7 @@ var EasyCMS = class {
9976
10311
  /** Runs afterRead hooks, populates relationships and removes what the caller may not see. */
9977
10312
  async output(config, docs, guard, options) {
9978
10313
  const read2 = await this.fieldChecker("read", guard, void 0, void 0);
9979
- const finish = async (target, doc) => {
10314
+ const finish2 = async (target, doc) => {
9980
10315
  const hooked = await this.transform(
9981
10316
  target.hooks?.afterRead,
9982
10317
  "doc",
@@ -10001,13 +10336,13 @@ var EasyCMS = class {
10001
10336
  });
10002
10337
  const docs2 = drafts ? await this.withDrafts(target, found.docs) : found.docs;
10003
10338
  return Promise.all(
10004
- docs2.map((d) => finish(target, this.pick(target, d, options)))
10339
+ docs2.map((d) => finish2(target, this.pick(target, d, options)))
10005
10340
  );
10006
10341
  };
10007
10342
  const depth = Math.max(0, Math.min(MAX_DEPTH, Math.trunc(options.depth ?? DEFAULT_DEPTH)));
10008
10343
  const localized = docs.map((d) => this.pick(config, d, options));
10009
10344
  const populated = await populate(load, this.config.collections, config.fields, localized, depth);
10010
- return Promise.all(populated.map((d) => finish(config, d)));
10345
+ return Promise.all(populated.map((d) => finish2(config, d)));
10011
10346
  }
10012
10347
  /** Creates a document: access, input filtering, hooks, validation, then afterChange. */
10013
10348
  async createDocument(config, raw, options, hookGuard) {
@@ -12646,6 +12981,7 @@ var ADMIN_UI_ROUTES = /* @__PURE__ */ new Set([
12646
12981
  "modules",
12647
12982
  "scheduled",
12648
12983
  "access",
12984
+ "bulk",
12649
12985
  "total",
12650
12986
  "media-usage",
12651
12987
  "owned",
@@ -13233,6 +13569,8 @@ async function route(cms, ctx, method, path, options) {
13233
13569
  body: { total: Math.min(total, LIST_TOTAL_MAX), more: total > LIST_TOTAL_MAX }
13234
13570
  };
13235
13571
  }
13572
+ if (second === "bulk" && third === void 0)
13573
+ return { body: { docs: await bulkOperations(cms, ctx.user) } };
13236
13574
  const [, , collection3, id2, extra] = segments;
13237
13575
  if (second === "access" && collection3 && id2 && extra === void 0) {
13238
13576
  if (INTERNAL_COLLECTIONS.has(collection3) || !cms.config.collections.some((c) => c.slug === collection3)) {
@@ -13350,6 +13688,7 @@ async function route(cms, ctx, method, path, options) {
13350
13688
  if (INTERNAL_COLLECTIONS.has(collection2) || !cms.config.collections.some((c) => c.slug === collection2)) {
13351
13689
  throw new HttpError(`Unknown collection "${collection2}"`, 404);
13352
13690
  }
13691
+ if (second === "bulk") return bulkRoute(cms, ctx, method, collection2, segments.slice(2), access);
13353
13692
  if (second !== void 0 && third !== void 0) {
13354
13693
  return documentAction(cms, ctx, method, collection2, second, segments.slice(2));
13355
13694
  }
@@ -13487,6 +13826,42 @@ async function documentAction(cms, ctx, method, collection2, id, path) {
13487
13826
  }
13488
13827
  throw new HttpError("Not found", 404);
13489
13828
  }
13829
+ async function bulkRoute(cms, ctx, method, collection2, path, access) {
13830
+ const user = ctx.user;
13831
+ if (!user) throw new UnauthorizedError();
13832
+ const [raw, action, extra] = path;
13833
+ if (raw === void 0) {
13834
+ if (method !== "POST") throw methodNotAllowed(ctx, "POST");
13835
+ const body = await readJson(ctx.request);
13836
+ const options = {
13837
+ user,
13838
+ ...access.context ? { context: access.context } : {},
13839
+ ...body.folder !== void 0 ? { folder: body.folder } : {},
13840
+ ...body.transferTo !== void 0 ? { transferTo: body.transferTo } : {}
13841
+ };
13842
+ const name = String(body.action ?? "");
13843
+ if (Array.isArray(body.ids))
13844
+ return { body: await bulkIds(cms, collection2, name, body.ids, options) };
13845
+ const where = body.where;
13846
+ if (where !== void 0 && (typeof where !== "object" || where === null || Array.isArray(where)))
13847
+ throw new QueryError("where must be an object");
13848
+ return {
13849
+ status: 202,
13850
+ body: await startBulk(cms, collection2, name, where, options)
13851
+ };
13852
+ }
13853
+ const id = parseId(raw);
13854
+ if (id === void 0) throw new HttpError("Not found", 404);
13855
+ if (action === void 0) {
13856
+ if (method !== "GET") throw methodNotAllowed(ctx, "GET");
13857
+ return { body: await bulkProgress(cms, id, user) };
13858
+ }
13859
+ if (action === "cancel" && extra === void 0) {
13860
+ if (method !== "POST") throw methodNotAllowed(ctx, "POST");
13861
+ return { body: await cancelBulk(cms, id, user) };
13862
+ }
13863
+ throw new HttpError("Not found", 404);
13864
+ }
13490
13865
  async function globalAction(cms, ctx, method, slug2, path) {
13491
13866
  const access = {
13492
13867
  overrideAccess: false,
@@ -2190,6 +2190,10 @@ declare class EasyCMS<C extends Config = Config> {
2190
2190
  ran: number;
2191
2191
  failed: number;
2192
2192
  };
2193
+ /** Steps of bulk actions on a list's matches (100 documents each). */
2194
+ bulk: {
2195
+ steps: number;
2196
+ };
2193
2197
  }>;
2194
2198
  private runJobsNow;
2195
2199
  /**
@@ -1,4 +1,4 @@
1
- import { I as ID, C as Config, E as EasyCMS, L as LocalizationConfig, F as Field, a as FieldError, b as FilterOptions, c as CreateEasyCMSOptions, R as ResolvedConfig } from './config-kFcSdrV9.js';
1
+ import { I as ID, C as Config, E as EasyCMS, L as LocalizationConfig, F as Field, a as FieldError, b as FilterOptions, c as CreateEasyCMSOptions, R as ResolvedConfig } from './config-Bsp6rkuH.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, d as EmailSetting, W as WebhookPayload } from './config-kFcSdrV9.js';
2
- export { A as ADAPTER_API_VERSION, e as ADMIN_ICONS, f as Access, g as AccessArgs, h as AccessOptions, i as AdminBrand, j as AdminCommand, k as AdminComponent, l as AdminConfig, m as AdminFieldArgs, n as AdminIcon, o as AdminLocale, p as AdminPage, q as AdminRole, r as AdminRoleChange, s as AdminRoleField, t as AdminRoleTarget, u as AdminRoleView, v as AdminRoles, w as AdminSso, x as AdminSwitcher, y as AdminViewAccess, z as AfterChangeHook, B as AfterDeleteHook, D as AfterReadHook, G as ApiKeyContext, H as ApiKeyGlobalOperation, J as ApiKeyOperation, K as ApiKeyPermissions, M as ArrayField, N as AuditChange, O as AuditConfig, Q as AuditEntry, S as AuditLog, T as AuditPage, U as AuditVerification, V as Auth, X as AuthConfig, Y as AuthProvider, Z as AuthProviderProfile, _ as AuthProviderRequest, $ as AuthUser, a0 as BackupsConfig, a1 as BeforeChangeHook, a2 as BeforeDeleteHook, a3 as BeforeValidateHook, a4 as Block, a5 as BlocksField, a6 as BooleanField, a7 as BuiltinApiKeysCollection, a8 as BuiltinMediaCollection, a9 as BuiltinMediaFoldersCollection, aa as BuiltinUsersCollection, ab as BundleHints, ac as CliCommand, ad as CollectionAccess, ae as CollectionAdmin, af as CollectionCalendarAdmin, ag as CollectionConfig, ah as CollectionDocument, ai as CollectionHooks, aj as CollectionListAdmin, ak as CollectionSearch, al as CollectionSlug, C as Config, am as ConfigError, an as ConfigIssue, ao as ContainerAdmin, c as CreateEasyCMSOptions, ap as CreateInput, aq as CreateMigrationResult, ar as CursorDocs, as as CustomField, at as CustomFieldTypes, au as DashboardWidget, av as Database, aw as DatabaseAdapter, ax as DatabaseInitArgs, ay as DatabaseTransfer, az as DateField, aA as DepthOptions, aB as DirectUpload, aC as DiskStorageOptions, aD as DocumentOf, E as EasyCMS, aE as EasyCMSError, aF as EmailAdapter, aG as EmailField, aH as EmailMessage, aI as Endpoint, aJ as EndpointRequest, aK as ErrorCode, aL as FIELD_TYPES, F as Field, aM as FieldAccess, aN as FieldAccessFn, aO as FieldAdmin, a as FieldError, aP as FieldRule, aQ as FieldType, aR as FieldTypeBase, aS as FieldTypeDefinition, aT as FieldTypeValidateContext, aU as FieldValidate, aV as FieldValidateContext, aW as FieldValue, aX as FieldsInput, aY as FieldsValue, b as FilterOptions, aZ as FilterOptionsArgs, a_ as FindAfterArgs, a$ as FindArgs, b0 as FindOptions, b1 as FolderLevel, b2 as FolderPermissions, b3 as ForbiddenError, b4 as GlobalAccess, b5 as GlobalConfig, b6 as GlobalDocument, b7 as GlobalHooks, b8 as GlobalInput, b9 as GlobalSlug, ba as GroupField, bb as ImageSize, bc as IncrementOptions, bd as InferCollection, be as InferGlobal, bf as JobConfig, bg as JsonField, bh as KeysetPage, bi as Label, bj as LayoutNode, bk as LivePreview, bl as LocaleOf, L as LocalizationConfig, bm as LogDetails, bn as Logger, bo as LoginArgs, bp as LogsConfig, bq as MediaDocument, br as MediaSize, bs as MembersConfig, bt as MembersSignup, bu as MigrationInfo, bv as NavGroup, bw as NotFoundError, bx as NumberField, by as OnRequest, bz as OnRequestArgs, bA as Operation, bB as OutboxRow, bC as PLUGIN_API_VERSION, bD as PasswordEmail, bE as PasswordEmailArgs, bF as PasswordEmailFn, bG as PasswordLinkOptions, bH as PayloadTooLargeError, bI as Plugin, bJ as PluginInfo, bK as PluginTypes, bL as PreviewTarget, bM as PreviewURL, bN as QueryError, bO as ROLE_OPERATIONS, bP as RawDocument, bQ as ReadOptions, bR as RelationshipField, bS as RequestContext, R as ResolvedConfig, bT as RichTextDocument, bU as RichTextField, bV as Role, bW as RoleOperation, bX as RolePermissions, bY as Roles, bZ as RoutesConfig, b_ as ScheduledPublish, b$ as SchemaError, c0 as SchemaMode, c1 as SearchAdapter, c2 as SearchEntry, c3 as SearchHits, c4 as SearchOptions, c5 as SearchQuery, c6 as SearchResults, c7 as SearchSetting, c8 as SelectField, c9 as SelectOption, ca as Session, cb as SidebarPanel, cc as SignupArgs, cd as SignupResult, ce as SingleSignOn, cf as SlugField, cg as SlugOf, ch as SsoOutcome, ci as SsoProviderRef, cj as StorageAdapter, ck as StoredFile, cl as TRIGRAM_LIKE_MIN, cm as TelemetryConfig, cn as TextField, co as TextareaField, cp as TooManyRequestsError, cq as TypedPlugin, cr as UnauthorizedError, cs as UncountedDocs, ct as UpdateInput, cu as UpdateOptions, cv as UploadConfig, cw as UploadField, cx as UploadFromURLConfig, cy as UserIdentity, cz as ValidationError, cA as Version, cB as VersionSummary, cC as VersionsConfig, cD as WEBHOOK_EVENTS, cE as WebhookConfig, cF as WebhookEvent, cG as Where, cH as WhereOperators, cI as anyone, cJ as consoleEmail, cK as consoleLogger, cL as createEasyCMS, cM as defineConfig, cN as defineFieldType, cO as definePlugin, cP as diskStorage, cQ as hasRows, cR as isAdmin, cS as isHasMany, cT as isSignedIn, cU as isStaff, cV as isSystemAdmin, cW as jsonLogger, cX as memorySearch, cY as mimeAllowedBy, cZ as rowFields, c_ as silentLogger, c$ as uniqueWithinOf } from './config-kFcSdrV9.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-uKrb_7kI.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-D-RXQhlD.js';
1
+ import { P as PaginatedDocs, I as ID, d as EmailSetting, W as WebhookPayload } from './config-Bsp6rkuH.js';
2
+ export { A as ADAPTER_API_VERSION, e as ADMIN_ICONS, f as Access, g as AccessArgs, h as AccessOptions, i as AdminBrand, j as AdminCommand, k as AdminComponent, l as AdminConfig, m as AdminFieldArgs, n as AdminIcon, o as AdminLocale, p as AdminPage, q as AdminRole, r as AdminRoleChange, s as AdminRoleField, t as AdminRoleTarget, u as AdminRoleView, v as AdminRoles, w as AdminSso, x as AdminSwitcher, y as AdminViewAccess, z as AfterChangeHook, B as AfterDeleteHook, D as AfterReadHook, G as ApiKeyContext, H as ApiKeyGlobalOperation, J as ApiKeyOperation, K as ApiKeyPermissions, M as ArrayField, N as AuditChange, O as AuditConfig, Q as AuditEntry, S as AuditLog, T as AuditPage, U as AuditVerification, V as Auth, X as AuthConfig, Y as AuthProvider, Z as AuthProviderProfile, _ as AuthProviderRequest, $ as AuthUser, a0 as BackupsConfig, a1 as BeforeChangeHook, a2 as BeforeDeleteHook, a3 as BeforeValidateHook, a4 as Block, a5 as BlocksField, a6 as BooleanField, a7 as BuiltinApiKeysCollection, a8 as BuiltinMediaCollection, a9 as BuiltinMediaFoldersCollection, aa as BuiltinUsersCollection, ab as BundleHints, ac as CliCommand, ad as CollectionAccess, ae as CollectionAdmin, af as CollectionCalendarAdmin, ag as CollectionConfig, ah as CollectionDocument, ai as CollectionHooks, aj as CollectionListAdmin, ak as CollectionSearch, al as CollectionSlug, C as Config, am as ConfigError, an as ConfigIssue, ao as ContainerAdmin, c as CreateEasyCMSOptions, ap as CreateInput, aq as CreateMigrationResult, ar as CursorDocs, as as CustomField, at as CustomFieldTypes, au as DashboardWidget, av as Database, aw as DatabaseAdapter, ax as DatabaseInitArgs, ay as DatabaseTransfer, az as DateField, aA as DepthOptions, aB as DirectUpload, aC as DiskStorageOptions, aD as DocumentOf, E as EasyCMS, aE as EasyCMSError, aF as EmailAdapter, aG as EmailField, aH as EmailMessage, aI as Endpoint, aJ as EndpointRequest, aK as ErrorCode, aL as FIELD_TYPES, F as Field, aM as FieldAccess, aN as FieldAccessFn, aO as FieldAdmin, a as FieldError, aP as FieldRule, aQ as FieldType, aR as FieldTypeBase, aS as FieldTypeDefinition, aT as FieldTypeValidateContext, aU as FieldValidate, aV as FieldValidateContext, aW as FieldValue, aX as FieldsInput, aY as FieldsValue, b as FilterOptions, aZ as FilterOptionsArgs, a_ as FindAfterArgs, a$ as FindArgs, b0 as FindOptions, b1 as FolderLevel, b2 as FolderPermissions, b3 as ForbiddenError, b4 as GlobalAccess, b5 as GlobalConfig, b6 as GlobalDocument, b7 as GlobalHooks, b8 as GlobalInput, b9 as GlobalSlug, ba as GroupField, bb as ImageSize, bc as IncrementOptions, bd as InferCollection, be as InferGlobal, bf as JobConfig, bg as JsonField, bh as KeysetPage, bi as Label, bj as LayoutNode, bk as LivePreview, bl as LocaleOf, L as LocalizationConfig, bm as LogDetails, bn as Logger, bo as LoginArgs, bp as LogsConfig, bq as MediaDocument, br as MediaSize, bs as MembersConfig, bt as MembersSignup, bu as MigrationInfo, bv as NavGroup, bw as NotFoundError, bx as NumberField, by as OnRequest, bz as OnRequestArgs, bA as Operation, bB as OutboxRow, bC as PLUGIN_API_VERSION, bD as PasswordEmail, bE as PasswordEmailArgs, bF as PasswordEmailFn, bG as PasswordLinkOptions, bH as PayloadTooLargeError, bI as Plugin, bJ as PluginInfo, bK as PluginTypes, bL as PreviewTarget, bM as PreviewURL, bN as QueryError, bO as ROLE_OPERATIONS, bP as RawDocument, bQ as ReadOptions, bR as RelationshipField, bS as RequestContext, R as ResolvedConfig, bT as RichTextDocument, bU as RichTextField, bV as Role, bW as RoleOperation, bX as RolePermissions, bY as Roles, bZ as RoutesConfig, b_ as ScheduledPublish, b$ as SchemaError, c0 as SchemaMode, c1 as SearchAdapter, c2 as SearchEntry, c3 as SearchHits, c4 as SearchOptions, c5 as SearchQuery, c6 as SearchResults, c7 as SearchSetting, c8 as SelectField, c9 as SelectOption, ca as Session, cb as SidebarPanel, cc as SignupArgs, cd as SignupResult, ce as SingleSignOn, cf as SlugField, cg as SlugOf, ch as SsoOutcome, ci as SsoProviderRef, cj as StorageAdapter, ck as StoredFile, cl as TRIGRAM_LIKE_MIN, cm as TelemetryConfig, cn as TextField, co as TextareaField, cp as TooManyRequestsError, cq as TypedPlugin, cr as UnauthorizedError, cs as UncountedDocs, ct as UpdateInput, cu as UpdateOptions, cv as UploadConfig, cw as UploadField, cx as UploadFromURLConfig, cy as UserIdentity, cz as ValidationError, cA as Version, cB as VersionSummary, cC as VersionsConfig, cD as WEBHOOK_EVENTS, cE as WebhookConfig, cF as WebhookEvent, cG as Where, cH as WhereOperators, cI as anyone, cJ as consoleEmail, cK as consoleLogger, cL as createEasyCMS, cM as defineConfig, cN as defineFieldType, cO as definePlugin, cP as diskStorage, cQ as hasRows, cR as isAdmin, cS as isHasMany, cT as isSignedIn, cU as isStaff, cV as isSystemAdmin, cW as jsonLogger, cX as memorySearch, cY as mimeAllowedBy, cZ as rowFields, c_ as silentLogger, c$ as uniqueWithinOf } from './config-Bsp6rkuH.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-CUzEJN31.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-Br9Pu6z9.js';
5
5
  export { FieldCondition, matchesCondition } from './conditions.js';
6
6
 
7
7
  type DeliveryKind = 'webhook' | 'email';
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  silentLogger,
29
29
  slugify,
30
30
  verifyWebhook
31
- } from "./chunk-V3SGXMJ6.js";
31
+ } from "./chunk-AAZ35NPQ.js";
32
32
  import {
33
33
  matchesCondition
34
34
  } from "./chunk-U266LV4M.js";
@@ -1,6 +1,6 @@
1
1
  import { Config, Database, EasyCMS, NavGroup, CollectionConfig, ID, RawDocument, Field, ResolvedConfig, ConfigIssue } from './index.js';
2
- export { d0 as API_KEYS, d1 as API_KEY_GLOBAL_OPERATIONS, d2 as API_KEY_OPERATIONS, d3 as DEFAULT_MAX_VERSIONS, d4 as DEFAULT_PASSWORD_EMAILS, d5 as FIELD_TYPE_BASES, d6 as FOLDER_LEVELS, d7 as MediaFolders, d8 as SSO_COOKIE, d9 as Secrets, cl as TRIGRAM_LIKE_MIN, da as codeOfStatus, cQ as hasRows, cS as isHasMany, db as isJsonLogger, dc as keyAllows, dd as logFormat, cY as mimeAllowedBy, cZ as rowFields, de as secretsOf, c$ as uniqueWithinOf } from './config-kFcSdrV9.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 isEmail, y as mergeForUpdate, z as parseId, E as platformClientIp, F as readCookie, G as validateFields, H as writeBackupFile } from './handler-D-RXQhlD.js';
2
+ export { d0 as API_KEYS, d1 as API_KEY_GLOBAL_OPERATIONS, d2 as API_KEY_OPERATIONS, d3 as DEFAULT_MAX_VERSIONS, d4 as DEFAULT_PASSWORD_EMAILS, d5 as FIELD_TYPE_BASES, d6 as FOLDER_LEVELS, d7 as MediaFolders, d8 as SSO_COOKIE, d9 as Secrets, cl as TRIGRAM_LIKE_MIN, da as codeOfStatus, cQ as hasRows, cS as isHasMany, db as isJsonLogger, dc as keyAllows, dd as logFormat, cY as mimeAllowedBy, cZ as rowFields, de as secretsOf, c$ as uniqueWithinOf } from './config-Bsp6rkuH.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 isEmail, y as mergeForUpdate, z as parseId, E as platformClientIp, F as readCookie, G as validateFields, H as writeBackupFile } from './handler-Br9Pu6z9.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
@@ -58,7 +58,7 @@ import {
58
58
  verifyPassword,
59
59
  verifyTurnstile,
60
60
  writeBackupFile
61
- } from "./chunk-V3SGXMJ6.js";
61
+ } from "./chunk-AAZ35NPQ.js";
62
62
  import {
63
63
  conditionIssues
64
64
  } from "./chunk-U266LV4M.js";
@@ -1,4 +1,4 @@
1
- import { bi as Label, F as Field, I as ID, o as AdminLocale, x as AdminSwitcher, ci as SsoProviderRef } from './config-kFcSdrV9.js';
1
+ import { bi as Label, F as Field, I as ID, o as AdminLocale, x as AdminSwitcher, ci as SsoProviderRef } from './config-Bsp6rkuH.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 { e as ADMIN_ICONS, k as AdminComponent, n as AdminIcon, $ as AuthUser, ac as CliCommand, ad as CollectionAccess, ag as CollectionConfig, C as Config, am as ConfigError, au as DashboardWidget, aE as EasyCMSError, aI as Endpoint, aJ as EndpointRequest, aK as ErrorCode, F as Field, aM as FieldAccess, a as FieldError, aR as FieldTypeBase, aS as FieldTypeDefinition, aT as FieldTypeValidateContext, b3 as ForbiddenError, b5 as GlobalConfig, I as ID, bf as JobConfig, bi as Label, bv as NavGroup, bw as NotFoundError, by as OnRequest, bz as OnRequestArgs, bC as PLUGIN_API_VERSION, bH as PayloadTooLargeError, bI as Plugin, bJ as PluginInfo, bK as PluginTypes, bN as QueryError, bS as RequestContext, cp as TooManyRequestsError, cq as TypedPlugin, cr as UnauthorizedError, cz as ValidationError, cG as Where, cN as defineFieldType, cO as definePlugin } from './config-kFcSdrV9.js';
2
- export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-uKrb_7kI.js';
1
+ export { e as ADMIN_ICONS, k as AdminComponent, n as AdminIcon, $ as AuthUser, ac as CliCommand, ad as CollectionAccess, ag as CollectionConfig, C as Config, am as ConfigError, au as DashboardWidget, aE as EasyCMSError, aI as Endpoint, aJ as EndpointRequest, aK as ErrorCode, F as Field, aM as FieldAccess, a as FieldError, aR as FieldTypeBase, aS as FieldTypeDefinition, aT as FieldTypeValidateContext, b3 as ForbiddenError, b5 as GlobalConfig, I as ID, bf as JobConfig, bi as Label, bv as NavGroup, bw as NotFoundError, by as OnRequest, bz as OnRequestArgs, bC as PLUGIN_API_VERSION, bH as PayloadTooLargeError, bI as Plugin, bJ as PluginInfo, bK as PluginTypes, bN as QueryError, bS as RequestContext, cp as TooManyRequestsError, cq as TypedPlugin, cr as UnauthorizedError, cz as ValidationError, cG as Where, cN as defineFieldType, cO as definePlugin } from './config-Bsp6rkuH.js';
2
+ export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-CUzEJN31.js';
3
3
  import './conditions.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",