@easy-cms/core 1.23.1 → 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.23.1" : "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([
@@ -7909,6 +8240,10 @@ var EasyCMS = class {
7909
8240
  return out ?? null;
7910
8241
  });
7911
8242
  }
8243
+ /**
8244
+ * The documents matching `where` that the user may read. With `max`, counting stops after
8245
+ * `max + 1`: a result above `max` means "more than `max`", without reading every row.
8246
+ */
7912
8247
  async count(collection2, options = {}) {
7913
8248
  return this.traced("count", String(collection2), async () => {
7914
8249
  const config = this.collection(collection2);
@@ -7921,7 +8256,9 @@ var EasyCMS = class {
7921
8256
  ),
7922
8257
  options
7923
8258
  );
7924
- return this.db.count({ collection: collection2, where });
8259
+ if (options.max !== void 0 && (!Number.isInteger(options.max) || options.max < 0))
8260
+ throw new QueryError("max must be a whole number of 0 or more");
8261
+ return this.db.count({ collection: collection2, where, max: options.max });
7925
8262
  });
7926
8263
  }
7927
8264
  async create(collection2, data, options = {}) {
@@ -9046,7 +9383,7 @@ var EasyCMS = class {
9046
9383
  const parsed = parseId(id);
9047
9384
  if (parsed === void 0) return { update: false, delete: false };
9048
9385
  const guard = { enforce: true, user, context };
9049
- 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(
9050
9387
  () => true,
9051
9388
  (error) => {
9052
9389
  if (error instanceof ForbiddenError || error instanceof UnauthorizedError || error instanceof NotFoundError)
@@ -9054,7 +9391,7 @@ var EasyCMS = class {
9054
9391
  throw error;
9055
9392
  }
9056
9393
  );
9057
- return { update: await check3("update"), delete: await check3("delete") };
9394
+ return { update: await check4("update"), delete: await check4("delete") };
9058
9395
  }
9059
9396
  /** Closes the database connection. */
9060
9397
  /**
@@ -9281,7 +9618,11 @@ var EasyCMS = class {
9281
9618
  webhooks: await this.retryWebhooks(now),
9282
9619
  emails: await this.mailer.retry(now),
9283
9620
  jobs: await this.runConfigJobs(now),
9284
- 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
+ })
9285
9626
  };
9286
9627
  if (this.telemetry?.measuresQueues)
9287
9628
  await this.countQueues(now).catch(
@@ -9872,9 +10213,9 @@ var EasyCMS = class {
9872
10213
  );
9873
10214
  }
9874
10215
  /** Runs a check; a `ForbiddenError` for a document the user can't read becomes a 404. */
9875
- async hidingDenied(collection2, guard, id, check3) {
10216
+ async hidingDenied(collection2, guard, id, check4) {
9876
10217
  try {
9877
- await check3();
10218
+ await check4();
9878
10219
  } catch (error) {
9879
10220
  if (error instanceof ForbiddenError && !await this.mayRead(collection2, guard, id))
9880
10221
  throw new NotFoundError(collection2, String(id));
@@ -9970,7 +10311,7 @@ var EasyCMS = class {
9970
10311
  /** Runs afterRead hooks, populates relationships and removes what the caller may not see. */
9971
10312
  async output(config, docs, guard, options) {
9972
10313
  const read2 = await this.fieldChecker("read", guard, void 0, void 0);
9973
- const finish = async (target, doc) => {
10314
+ const finish2 = async (target, doc) => {
9974
10315
  const hooked = await this.transform(
9975
10316
  target.hooks?.afterRead,
9976
10317
  "doc",
@@ -9995,13 +10336,13 @@ var EasyCMS = class {
9995
10336
  });
9996
10337
  const docs2 = drafts ? await this.withDrafts(target, found.docs) : found.docs;
9997
10338
  return Promise.all(
9998
- docs2.map((d) => finish(target, this.pick(target, d, options)))
10339
+ docs2.map((d) => finish2(target, this.pick(target, d, options)))
9999
10340
  );
10000
10341
  };
10001
10342
  const depth = Math.max(0, Math.min(MAX_DEPTH, Math.trunc(options.depth ?? DEFAULT_DEPTH)));
10002
10343
  const localized = docs.map((d) => this.pick(config, d, options));
10003
10344
  const populated = await populate(load, this.config.collections, config.fields, localized, depth);
10004
- return Promise.all(populated.map((d) => finish(config, d)));
10345
+ return Promise.all(populated.map((d) => finish2(config, d)));
10005
10346
  }
10006
10347
  /** Creates a document: access, input filtering, hooks, validation, then afterChange. */
10007
10348
  async createDocument(config, raw, options, hookGuard) {
@@ -12455,6 +12796,7 @@ function parseDepth(url) {
12455
12796
 
12456
12797
  // src/rest/handler.ts
12457
12798
  var SCHEDULED_RANGE_LIMIT = 500;
12799
+ var LIST_TOTAL_MAX = 1e4;
12458
12800
  var CSRF_COOKIE = "ecms-csrf";
12459
12801
  var CSRF_HEADER = "x-csrf-token";
12460
12802
  var MAX_BODY_BYTES = 1024 * 1024;
@@ -12639,6 +12981,8 @@ var ADMIN_UI_ROUTES = /* @__PURE__ */ new Set([
12639
12981
  "modules",
12640
12982
  "scheduled",
12641
12983
  "access",
12984
+ "bulk",
12985
+ "total",
12642
12986
  "media-usage",
12643
12987
  "owned",
12644
12988
  "sso"
@@ -13210,6 +13554,23 @@ async function route(cms, ctx, method, path, options) {
13210
13554
  })
13211
13555
  };
13212
13556
  }
13557
+ if (second === "total" && third !== void 0 && segments.length === 3) {
13558
+ if (INTERNAL_COLLECTIONS.has(third) || !cms.config.collections.some((c) => c.slug === third)) {
13559
+ throw new HttpError(`Unknown collection "${third}"`, 404);
13560
+ }
13561
+ const { where } = parseListQuery(ctx.url);
13562
+ const total = await cms.count(third, {
13563
+ ...access,
13564
+ ...where ? { where } : {},
13565
+ draft,
13566
+ max: LIST_TOTAL_MAX
13567
+ });
13568
+ return {
13569
+ body: { total: Math.min(total, LIST_TOTAL_MAX), more: total > LIST_TOTAL_MAX }
13570
+ };
13571
+ }
13572
+ if (second === "bulk" && third === void 0)
13573
+ return { body: { docs: await bulkOperations(cms, ctx.user) } };
13213
13574
  const [, , collection3, id2, extra] = segments;
13214
13575
  if (second === "access" && collection3 && id2 && extra === void 0) {
13215
13576
  if (INTERNAL_COLLECTIONS.has(collection3) || !cms.config.collections.some((c) => c.slug === collection3)) {
@@ -13327,6 +13688,7 @@ async function route(cms, ctx, method, path, options) {
13327
13688
  if (INTERNAL_COLLECTIONS.has(collection2) || !cms.config.collections.some((c) => c.slug === collection2)) {
13328
13689
  throw new HttpError(`Unknown collection "${collection2}"`, 404);
13329
13690
  }
13691
+ if (second === "bulk") return bulkRoute(cms, ctx, method, collection2, segments.slice(2), access);
13330
13692
  if (second !== void 0 && third !== void 0) {
13331
13693
  return documentAction(cms, ctx, method, collection2, second, segments.slice(2));
13332
13694
  }
@@ -13464,6 +13826,42 @@ async function documentAction(cms, ctx, method, collection2, id, path) {
13464
13826
  }
13465
13827
  throw new HttpError("Not found", 404);
13466
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
+ }
13467
13865
  async function globalAction(cms, ctx, method, slug2, path) {
13468
13866
  const access = {
13469
13867
  overrideAccess: false,
@@ -235,9 +235,14 @@ interface Database {
235
235
  collection: string;
236
236
  ids: readonly ID[];
237
237
  }): Promise<RawDocument[]>;
238
+ /**
239
+ * With `max`, stops after `max + 1` matches (a result above `max` means "more than `max`"), so a
240
+ * count on a large collection doesn't read every row.
241
+ */
238
242
  count(args: {
239
243
  collection: string;
240
244
  where?: Where | undefined;
245
+ max?: number | undefined;
241
246
  }): Promise<number>;
242
247
  /** `data` holds every field, `createdAt` and `updatedAt`. Runs in one transaction. */
243
248
  create(args: {
@@ -1883,10 +1888,15 @@ declare class EasyCMS<C extends Config = Config> {
1883
1888
  findById<S extends Slug<C>, L extends string | undefined = undefined>(collection: S, id: ID, options?: ReadOptions & {
1884
1889
  readonly locale?: L;
1885
1890
  }): Promise<Doc<C, S, L> | null>;
1891
+ /**
1892
+ * The documents matching `where` that the user may read. With `max`, counting stops after
1893
+ * `max + 1`: a result above `max` means "more than `max`", without reading every row.
1894
+ */
1886
1895
  count<S extends Slug<C>>(collection: S, options?: {
1887
1896
  where?: Where;
1888
1897
  draft?: boolean;
1889
1898
  locale?: string;
1899
+ max?: number;
1890
1900
  } & AccessOptions): Promise<number>;
1891
1901
  create<S extends Slug<C>>(collection: S, data: Create<C, S>, options?: DepthOptions): Promise<Doc<C, S>>;
1892
1902
  /**
@@ -2180,6 +2190,10 @@ declare class EasyCMS<C extends Config = Config> {
2180
2190
  ran: number;
2181
2191
  failed: number;
2182
2192
  };
2193
+ /** Steps of bulk actions on a list's matches (100 documents each). */
2194
+ bulk: {
2195
+ steps: number;
2196
+ };
2183
2197
  }>;
2184
2198
  private runJobsNow;
2185
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-BQ53ulBF.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-BQ53ulBF.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-BQ53ulBF.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-I5C-9kXl.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-B4GC_p5n.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-VZNL7HWC.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-BQ53ulBF.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-B4GC_p5n.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-VZNL7HWC.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-BQ53ulBF.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-BQ53ulBF.js';
2
- export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-I5C-9kXl.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.23.1",
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",