@easy-cms/core 1.25.1 → 1.27.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.
@@ -19,11 +19,13 @@ import {
19
19
  UnauthorizedError,
20
20
  ValidationError,
21
21
  applyFieldTypes,
22
+ errorText,
22
23
  expandMimeTypes,
23
24
  fieldTypeModules,
24
25
  hasRows,
25
26
  imageDimensions,
26
27
  isHasMany,
28
+ isPassingError,
27
29
  isPrivateKey,
28
30
  mimeAllowed,
29
31
  mimeAllowedBy,
@@ -36,7 +38,7 @@ import {
36
38
  warnDeprecated,
37
39
  withCharset,
38
40
  withPrivacy
39
- } from "./chunk-RJGKAYUU.js";
41
+ } from "./chunk-SYUQX2G6.js";
40
42
 
41
43
  // src/access.ts
42
44
  var isSystemAdmin = (user) => user?.role === "admin" && !user.apiKey && user.scoped !== true;
@@ -2517,6 +2519,32 @@ var ADAPTER_API_VERSION = 1;
2517
2519
 
2518
2520
  // src/document.ts
2519
2521
  import { randomUUID } from "crypto";
2522
+
2523
+ // src/rich-text-links.ts
2524
+ function docLinkMarks(doc) {
2525
+ const found = [];
2526
+ const walk = (node, depth) => {
2527
+ if (!node || typeof node !== "object" || depth > 200) return;
2528
+ const { marks, content } = node;
2529
+ if (Array.isArray(marks))
2530
+ for (const mark of marks) {
2531
+ const m = mark;
2532
+ const attrs = m?.attrs;
2533
+ if (m?.type === "link" && attrs && attrs.doc !== void 0 && attrs.doc !== null)
2534
+ found.push(m);
2535
+ }
2536
+ if (Array.isArray(content)) for (const child of content) walk(child, depth + 1);
2537
+ };
2538
+ walk(doc, 0);
2539
+ return found;
2540
+ }
2541
+ function linkTarget(doc) {
2542
+ if (!doc || typeof doc !== "object") return void 0;
2543
+ const { relationTo, value } = doc;
2544
+ return typeof relationTo === "string" && relationTo !== "" && value !== void 0 && value !== null ? { relationTo, value } : void 0;
2545
+ }
2546
+
2547
+ // src/document.ts
2520
2548
  function isEmail(value) {
2521
2549
  const at = value.indexOf("@");
2522
2550
  return at > 0 && !value.includes("@", at + 1) && !/\s/.test(value) && value.slice(at + 2, -1).includes(".");
@@ -2754,10 +2782,26 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
2754
2782
  } catch {
2755
2783
  return fail("must be JSON-serializable");
2756
2784
  }
2757
- case "richText":
2785
+ case "richText": {
2758
2786
  if (!isPlainObject(raw) || raw.type !== "doc")
2759
2787
  return fail('must be a rich text document ({ type: "doc" })');
2760
- return raw;
2788
+ const marks = docLinkMarks(raw);
2789
+ if (marks.length === 0) return raw;
2790
+ const doc = structuredClone(raw);
2791
+ for (const mark of docLinkMarks(doc)) {
2792
+ const target = linkTarget(mark.attrs.doc);
2793
+ const id = target ? parseId(target.value) : void 0;
2794
+ if (!target || id === void 0)
2795
+ return fail("has a link to a document without { relationTo, value }");
2796
+ mark.attrs = {
2797
+ ...mark.attrs,
2798
+ doc: { relationTo: target.relationTo, value: id },
2799
+ href: null
2800
+ };
2801
+ references2.push({ field: path, collection: target.relationTo, id, link: true });
2802
+ }
2803
+ return doc;
2804
+ }
2761
2805
  case "upload":
2762
2806
  case "relationship": {
2763
2807
  const target = field.type === "upload" ? "media" : field.to;
@@ -3912,12 +3956,12 @@ function inScope(row, scope) {
3912
3956
  // src/populate.ts
3913
3957
  var DEFAULT_DEPTH = 1;
3914
3958
  var MAX_DEPTH = 3;
3915
- async function populate(load, collections, fields, docs, depth) {
3959
+ async function populate(load, collections, fields, docs, depth, linkURL) {
3916
3960
  if (depth <= 0 || docs.length === 0) return docs;
3917
3961
  const bySlug = new Map(collections.map((c) => [c.slug, c]));
3918
3962
  const copies = docs.map((doc) => structuredClone(doc));
3919
3963
  const slots = [];
3920
- for (const doc of copies) collectSlots(fields, doc, bySlug, slots);
3964
+ for (const doc of copies) collectSlots(fields, doc, bySlug, slots, linkURL);
3921
3965
  if (slots.length === 0) return copies;
3922
3966
  const wanted = /* @__PURE__ */ new Map();
3923
3967
  for (const slot of slots) {
@@ -3931,28 +3975,52 @@ async function populate(load, collections, fields, docs, depth) {
3931
3975
  const collection2 = bySlug.get(slug2);
3932
3976
  if (!collection2) return;
3933
3977
  const raw = await load(collection2, [...ids]);
3934
- const populated = await populate(load, collections, collection2.fields, raw, depth - 1);
3978
+ const populated = await populate(
3979
+ load,
3980
+ collections,
3981
+ collection2.fields,
3982
+ raw,
3983
+ depth - 1,
3984
+ linkURL
3985
+ );
3935
3986
  found.set(slug2, new Map(populated.map((d) => [d.id, d])));
3936
3987
  })
3937
3988
  );
3938
3989
  for (const slot of slots) slot.assign(found.get(slot.collection) ?? /* @__PURE__ */ new Map());
3939
3990
  return copies;
3940
3991
  }
3941
- function collectSlots(fields, data, bySlug, slots) {
3992
+ function collectSlots(fields, data, bySlug, slots, linkURL) {
3942
3993
  for (const field of fields) {
3943
3994
  if (field.name === "__proto__") continue;
3944
3995
  const value = data[field.name];
3945
3996
  if (value === null || value === void 0) continue;
3946
3997
  if (field.type === "group") {
3947
- if (typeof value === "object") collectSlots(field.fields, value, bySlug, slots);
3998
+ if (typeof value === "object")
3999
+ collectSlots(field.fields, value, bySlug, slots, linkURL);
3948
4000
  } else if (hasRows(field)) {
3949
4001
  if (Array.isArray(value)) {
3950
4002
  for (const row of value) {
3951
4003
  const fields2 = rowFields(field, row);
3952
4004
  if (fields2 && row && typeof row === "object")
3953
- collectSlots(fields2, row, bySlug, slots);
4005
+ collectSlots(fields2, row, bySlug, slots, linkURL);
3954
4006
  }
3955
4007
  }
4008
+ } else if (field.type === "richText" && linkURL) {
4009
+ if (value.type !== "doc") continue;
4010
+ for (const mark of docLinkMarks(value)) {
4011
+ const target = linkTarget(mark.attrs.doc);
4012
+ const collection2 = target ? bySlug.get(target.relationTo) : void 0;
4013
+ if (!target || !collection2) continue;
4014
+ const id = target.value;
4015
+ slots.push({
4016
+ collection: target.relationTo,
4017
+ ids: [id],
4018
+ assign: (byId) => {
4019
+ const doc = byId.get(id);
4020
+ mark.attrs = { ...mark.attrs, href: doc ? linkURL(collection2, doc) : null };
4021
+ }
4022
+ });
4023
+ }
3956
4024
  } else if (field.type === "relationship" || field.type === "upload") {
3957
4025
  if (typeof value === "object" && !Array.isArray(value)) continue;
3958
4026
  const collection2 = field.type === "upload" ? "media" : field.to;
@@ -7309,12 +7377,37 @@ function validateField(field, path, siblings, collectionSlugs, add) {
7309
7377
  }
7310
7378
  }
7311
7379
  break;
7380
+ case "ui": {
7381
+ if (!field.admin?.component)
7382
+ add(
7383
+ `${path}.admin.component`,
7384
+ "is required on a ui field",
7385
+ "e.g. { name: 'estimate', type: 'ui', admin: { component: 'acme-estimate' } }"
7386
+ );
7387
+ const stored = [
7388
+ "required",
7389
+ "unique",
7390
+ "uniqueWithin",
7391
+ "index",
7392
+ "defaultValue",
7393
+ "validate",
7394
+ "access",
7395
+ "hidden",
7396
+ "localized"
7397
+ ];
7398
+ for (const option of stored)
7399
+ if (field[option] !== void 0)
7400
+ add(`${path}.${option}`, "has no effect on a ui field, which stores nothing");
7401
+ break;
7402
+ }
7312
7403
  case "array":
7313
7404
  case "group":
7314
7405
  if (!Array.isArray(field.fields) || field.fields.length === 0) {
7315
7406
  add(`${path}.fields`, "must contain at least one field");
7316
7407
  break;
7317
7408
  }
7409
+ if (field.fields.every((f) => f?.type === "ui"))
7410
+ add(`${path}.fields`, "must contain a field that stores a value, not only ui fields");
7318
7411
  if (field.type === "array")
7319
7412
  checkRange(field.minRows, field.maxRows, "minRows", "maxRows", path, add);
7320
7413
  validateFields2(field.fields, `${path}.fields`, /* @__PURE__ */ new Set(["id"]), collectionSlugs, add);
@@ -7734,8 +7827,59 @@ async function resolveConfig(input) {
7734
7827
  ),
7735
7828
  fieldTypes: config.fieldTypes ?? []
7736
7829
  };
7737
- resolved.add(result);
7738
- return result;
7830
+ const final = withoutUiFields(result);
7831
+ resolved.add(final);
7832
+ return final;
7833
+ }
7834
+ var ADMIN_FIELDS = /* @__PURE__ */ Symbol("easy-cms.adminFields");
7835
+ function adminFieldsOf(container) {
7836
+ return container[ADMIN_FIELDS] ?? container.fields;
7837
+ }
7838
+ function withoutUiFields(config) {
7839
+ const strip = (fields) => {
7840
+ let changed = false;
7841
+ const admin = [];
7842
+ const data = [];
7843
+ for (const field of fields) {
7844
+ if (field.type === "ui") {
7845
+ changed = true;
7846
+ admin.push(field);
7847
+ continue;
7848
+ }
7849
+ let next = field;
7850
+ if (field.type === "group" || field.type === "array") {
7851
+ const inner = strip(field.fields);
7852
+ if (inner.changed) {
7853
+ changed = true;
7854
+ next = { ...field, fields: inner.data, [ADMIN_FIELDS]: inner.admin };
7855
+ }
7856
+ } else if (field.type === "blocks") {
7857
+ let blocksChanged = false;
7858
+ const blocks = field.blocks.map((block) => {
7859
+ const inner = strip(block.fields);
7860
+ if (!inner.changed) return block;
7861
+ blocksChanged = true;
7862
+ return { ...block, fields: inner.data, [ADMIN_FIELDS]: inner.admin };
7863
+ });
7864
+ if (blocksChanged) {
7865
+ changed = true;
7866
+ next = { ...field, blocks };
7867
+ }
7868
+ }
7869
+ admin.push(next);
7870
+ data.push(next);
7871
+ }
7872
+ return { data, admin, changed };
7873
+ };
7874
+ const apply2 = (container) => {
7875
+ const inner = strip(container.fields);
7876
+ return inner.changed ? { ...container, fields: inner.data, [ADMIN_FIELDS]: inner.admin } : container;
7877
+ };
7878
+ return {
7879
+ ...config,
7880
+ collections: config.collections.map(apply2),
7881
+ globals: config.globals.map(apply2)
7882
+ };
7739
7883
  }
7740
7884
  function usesVersions(config) {
7741
7885
  return [...config.collections ?? [], ...config.globals ?? []].some((c) => c.versions);
@@ -7788,7 +7932,7 @@ function cacheableResponse(request, body, status, headers, times, tags) {
7788
7932
  }
7789
7933
 
7790
7934
  // src/version.ts
7791
- var VERSION = true ? "1.25.1" : "0.0.0-dev";
7935
+ var VERSION = true ? "1.27.0" : "0.0.0-dev";
7792
7936
 
7793
7937
  // src/telemetry.ts
7794
7938
  var DB_CALLS = /* @__PURE__ */ new Set([
@@ -9871,7 +10015,7 @@ var EasyCMS = class {
9871
10015
  if (!claimed) continue;
9872
10016
  const parent = String(row.parent);
9873
10017
  const action = row.action;
9874
- try {
10018
+ const run = async () => {
9875
10019
  if (parent.startsWith("global:")) {
9876
10020
  const { slug: slug2, scope } = splitGlobalKey(parent.slice("global:".length));
9877
10021
  if (action === "publish")
@@ -9888,6 +10032,21 @@ var EasyCMS = class {
9888
10032
  "unpublish"
9889
10033
  );
9890
10034
  }
10035
+ };
10036
+ try {
10037
+ for (let attempt = 0; ; attempt++) {
10038
+ try {
10039
+ await run();
10040
+ break;
10041
+ } catch (error) {
10042
+ const wait = SCHEDULED_RETRY_DELAYS[attempt];
10043
+ if (wait === void 0 || !isPassingError(error)) throw error;
10044
+ this.logger.warn(
10045
+ `Scheduled ${action} of ${parent} ${row.doc}: ${errorText(error)}; trying again`
10046
+ );
10047
+ await new Promise((resolve3) => setTimeout(resolve3, wait));
10048
+ }
10049
+ }
9891
10050
  await this.db.update({
9892
10051
  collection: SCHEDULED_JOBS,
9893
10052
  id: row.id,
@@ -9896,7 +10055,7 @@ var EasyCMS = class {
9896
10055
  ran++;
9897
10056
  } catch (error) {
9898
10057
  failed++;
9899
- const message2 = error.message;
10058
+ const message2 = errorText(error);
9900
10059
  this.logger.error(`Scheduled ${action} of ${parent} ${row.doc} failed: ${message2}`, {
9901
10060
  error
9902
10061
  });
@@ -9906,7 +10065,7 @@ var EasyCMS = class {
9906
10065
  data: {
9907
10066
  ...row,
9908
10067
  state: "failed",
9909
- error: message2.slice(0, 500),
10068
+ error: message2,
9910
10069
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9911
10070
  }
9912
10071
  });
@@ -9914,6 +10073,22 @@ var EasyCMS = class {
9914
10073
  }
9915
10074
  return { ran, failed };
9916
10075
  }
10076
+ /**
10077
+ * Puts a failed scheduled publish or unpublish back in the queue and runs what is due (#333).
10078
+ * Returns `false` when the job isn't a failed one.
10079
+ */
10080
+ async retryScheduled(id) {
10081
+ const row = await this.db.findById({ collection: SCHEDULED_JOBS, id });
10082
+ if (!row || row.state !== "failed") return false;
10083
+ const claimed = await this.claim(
10084
+ SCHEDULED_JOBS,
10085
+ row,
10086
+ { state: "pending", error: null, updatedAt: (/* @__PURE__ */ new Date()).toISOString() },
10087
+ { state: { equals: "failed" } }
10088
+ );
10089
+ if (claimed) await this.runScheduled();
10090
+ return claimed;
10091
+ }
9917
10092
  /** Schedules a publish or unpublish, replacing a pending job with the same action. */
9918
10093
  async schedule(collection2, id, job, options = {}) {
9919
10094
  const { parsed } = await this.scheduleTarget(collection2, id, options);
@@ -10341,7 +10516,16 @@ var EasyCMS = class {
10341
10516
  };
10342
10517
  const depth = Math.max(0, Math.min(MAX_DEPTH, Math.trunc(options.depth ?? DEFAULT_DEPTH)));
10343
10518
  const localized = docs.map((d) => this.pick(config, d, options));
10344
- const populated = await populate(load, this.config.collections, config.fields, localized, depth);
10519
+ const locale = options.locale && options.locale !== "all" ? options.locale : this.config.localization?.defaultLocale ?? this.config.admin.locale;
10520
+ const linkURL = (target, doc) => target.preview?.({ doc, locale }) ?? null;
10521
+ const populated = await populate(
10522
+ load,
10523
+ this.config.collections,
10524
+ config.fields,
10525
+ localized,
10526
+ depth,
10527
+ linkURL
10528
+ );
10345
10529
  return Promise.all(populated.map((d) => finish2(config, d)));
10346
10530
  }
10347
10531
  /** Creates a document: access, input filtering, hooks, validation, then afterChange. */
@@ -10882,11 +11066,26 @@ var EasyCMS = class {
10882
11066
  }
10883
11067
  async checkReferences(references2, guard) {
10884
11068
  const byCollection = /* @__PURE__ */ new Map();
11069
+ const errors = [];
10885
11070
  for (const ref of references2) {
10886
- if (!this.config.collections.some((c) => c.slug === ref.collection)) continue;
11071
+ const target = this.config.collections.find((c) => c.slug === ref.collection);
11072
+ if (ref.link && (!target || INTERNAL_COLLECTIONS.has(ref.collection))) {
11073
+ errors.push({
11074
+ field: ref.field,
11075
+ message: `links to "${ref.collection}", which is not a collection`
11076
+ });
11077
+ continue;
11078
+ }
11079
+ if (ref.link && typeof target?.preview !== "function") {
11080
+ errors.push({
11081
+ field: ref.field,
11082
+ message: `links to "${ref.collection}", whose documents have no URL (set its preview)`
11083
+ });
11084
+ continue;
11085
+ }
11086
+ if (!target) continue;
10887
11087
  byCollection.set(ref.collection, [...byCollection.get(ref.collection) ?? [], ref]);
10888
11088
  }
10889
- const errors = [];
10890
11089
  for (const [collection2, refs] of byCollection) {
10891
11090
  const found = await this.db.findByIds({
10892
11091
  collection: collection2,
@@ -11025,6 +11224,7 @@ function jobSchedule(job, row) {
11025
11224
  var JOBS_RAN = "easy-cms:jobs-ran";
11026
11225
  var LEGACY_JOB_RUNS = "easy-cms:jobs";
11027
11226
  var JOB_LEASE = 10 * 6e4;
11227
+ var SCHEDULED_RETRY_DELAYS = [1e3, 3e3];
11028
11228
  function durationSeconds(value) {
11029
11229
  const match = typeof value === "string" ? /^(\d+)\s*([smhd])$/.exec(value.trim()) : null;
11030
11230
  const seconds = typeof value === "number" ? value : match ? Number(match[1]) * { s: 1, m: 60, h: 3600, d: 86400 }[match[2]] : Number.NaN;
@@ -11162,6 +11362,15 @@ async function retryOutbox(cms, user, id) {
11162
11362
  if (!await cms.retryOutbox(row.id))
11163
11363
  throw new EasyCMSError("Only failed entries can be retried", 409);
11164
11364
  }
11365
+ async function retryScheduled(cms, user, id) {
11366
+ if (!isSystemAdmin(user)) throw new ForbiddenError();
11367
+ if (!has(cms.config, SCHEDULED_JOBS)) throw new NotFoundError(SCHEDULED_JOBS, id);
11368
+ const parsed = parseId(id);
11369
+ const row = parsed === void 0 ? null : await cms.db.findById({ collection: SCHEDULED_JOBS, id: parsed });
11370
+ if (!row) throw new NotFoundError(SCHEDULED_JOBS, id);
11371
+ if (!await cms.retryScheduled(row.id))
11372
+ throw new EasyCMSError("Only failed jobs can be retried", 409);
11373
+ }
11165
11374
  var TIME_OF_DAY2 = /^([01]\d|2[0-3]):[0-5]\d$/;
11166
11375
  async function updateJob(cms, user, name, body) {
11167
11376
  if (!isSystemAdmin(user)) throw new ForbiddenError();
@@ -12162,7 +12371,7 @@ async function serializeFields(fields, checks, localized, folderId, who) {
12162
12371
  case "array":
12163
12372
  if (field.minRows !== void 0) f.minRows = field.minRows;
12164
12373
  if (field.maxRows !== void 0) f.maxRows = field.maxRows;
12165
- f.fields = await serializeFields(field.fields, checks, localized, folderId, who);
12374
+ f.fields = await serializeFields(adminFieldsOf(field), checks, localized, folderId, who);
12166
12375
  break;
12167
12376
  case "blocks":
12168
12377
  if (field.minRows !== void 0) f.minRows = field.minRows;
@@ -12171,12 +12380,16 @@ async function serializeFields(fields, checks, localized, folderId, who) {
12171
12380
  field.blocks.map(async (block) => ({
12172
12381
  slug: block.slug,
12173
12382
  ...block.labels ? { labels: block.labels } : {},
12174
- fields: await serializeFields(block.fields, checks, localized, folderId, who)
12383
+ fields: await serializeFields(adminFieldsOf(block), checks, localized, folderId, who)
12175
12384
  }))
12176
12385
  );
12177
12386
  break;
12178
12387
  case "group":
12179
- f.fields = await serializeFields(field.fields, checks, localized, folderId, who);
12388
+ f.fields = await serializeFields(adminFieldsOf(field), checks, localized, folderId, who);
12389
+ break;
12390
+ // A component alone: without a label of its own, none is shown.
12391
+ case "ui":
12392
+ if (field.label === void 0) f.label = "";
12180
12393
  break;
12181
12394
  }
12182
12395
  out.push(f);
@@ -12196,7 +12409,7 @@ async function may(cms, access, user, target, operation, context) {
12196
12409
  async function collection(cms, config, user, localized, roles, context) {
12197
12410
  const target = { collection: config.slug };
12198
12411
  const fields = await serializeFields(
12199
- config.fields,
12412
+ adminFieldsOf(config),
12200
12413
  await checkers(cms, user, context),
12201
12414
  localized,
12202
12415
  folderIds(cms, user, context),
@@ -12256,10 +12469,16 @@ async function global(cms, config, user, localized, context) {
12256
12469
  versions: Boolean(config.versions),
12257
12470
  preview: typeof config.preview === "function",
12258
12471
  schedule: config.schedule === true,
12259
- fields: await serializeFields(config.fields, checks, localized, folderIds(cms, user, context), {
12260
- user,
12261
- context
12262
- }),
12472
+ fields: await serializeFields(
12473
+ adminFieldsOf(config),
12474
+ checks,
12475
+ localized,
12476
+ folderIds(cms, user, context),
12477
+ {
12478
+ user,
12479
+ context
12480
+ }
12481
+ ),
12263
12482
  permissions: {
12264
12483
  read: await may(cms, config.access?.read, user, target, "read", context),
12265
12484
  update: await may(cms, config.access?.update, user, target, "update", context),
@@ -13478,6 +13697,13 @@ async function route(cms, ctx, method, path, options) {
13478
13697
  await cms.audit.record({ action: "jobs.outbox.retry", target: "jobs", detail: { id: id2 } });
13479
13698
  return { body: await adminJobs(cms, ctx.user) };
13480
13699
  }
13700
+ if (third === "scheduled" && segments.length === 4) {
13701
+ if (method !== "POST") throw methodNotAllowed(ctx, "POST");
13702
+ const id2 = segments[3];
13703
+ await retryScheduled(cms, ctx.user, id2);
13704
+ await cms.audit.record({ action: "jobs.scheduled.retry", target: "jobs", detail: { id: id2 } });
13705
+ return { body: await adminJobs(cms, ctx.user) };
13706
+ }
13481
13707
  if (third !== void 0 && segments.length === 3) {
13482
13708
  if (method !== "PATCH") throw methodNotAllowed(ctx, "PATCH");
13483
13709
  const { action, detail } = await updateJob(
@@ -103,6 +103,32 @@ var PayloadTooLargeError = class extends EasyCMSError {
103
103
  this.name = "PayloadTooLargeError";
104
104
  }
105
105
  };
106
+ function errorText(error, max = 500) {
107
+ const parts = [];
108
+ let current = error;
109
+ for (let depth = 0; current && depth < 5; depth++) {
110
+ const message = current instanceof Error ? current.message : typeof current === "string" ? current : "";
111
+ const code = current.code;
112
+ const text2 = [message, typeof code === "string" && !message.includes(code) ? code : ""].filter(Boolean).join(" ");
113
+ if (text2 && !parts.some((p) => p.includes(text2))) parts.push(text2);
114
+ current = current.cause;
115
+ }
116
+ const text = parts.join(" \u2014 ") || String(error);
117
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
118
+ }
119
+ var PASSING = /ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|EAI_AGAIN|ENOTFOUND|fetch failed|socket hang up|network|timed? ?out|SQLITE_BUSY|database is locked|too many (clients|connections)|connection (terminated|closed|reset|refused)|server closed|stream (expired|not found)|\b50[234]\b/i;
120
+ function isPassingError(error) {
121
+ if (error instanceof EasyCMSError) return false;
122
+ let current = error;
123
+ for (let depth = 0; current && depth < 5; depth++) {
124
+ const message = current instanceof Error ? current.message : String(current);
125
+ const code = current.code;
126
+ const checked = message.startsWith("Failed query") ? "" : message;
127
+ if (PASSING.test(checked) || typeof code === "string" && PASSING.test(code)) return true;
128
+ current = current.cause;
129
+ }
130
+ return false;
131
+ }
106
132
 
107
133
  // src/media.ts
108
134
  var EXTENSIONS = {
@@ -329,7 +355,8 @@ var FIELD_TYPES = [
329
355
  "relationship",
330
356
  "array",
331
357
  "group",
332
- "blocks"
358
+ "blocks",
359
+ "ui"
333
360
  ];
334
361
 
335
362
  // src/config.ts
@@ -547,6 +574,8 @@ export {
547
574
  ForbiddenError,
548
575
  TooManyRequestsError,
549
576
  PayloadTooLargeError,
577
+ errorText,
578
+ isPassingError,
550
579
  EXTENSIONS,
551
580
  MIME_GROUPS,
552
581
  expandMimeTypes,
@@ -993,14 +993,24 @@ declare function mimeAllowedBy(mimeType: string, patterns: readonly string[]): b
993
993
  declare const hasRows: (field: Field) => field is ArrayField | BlocksField;
994
994
  /** The fields of one row of an `array` or `blocks` field (`undefined` for an unknown block). */
995
995
  declare function rowFields(field: ArrayField | BlocksField, row: unknown): readonly Field[] | undefined;
996
- type Field = CustomField | TextField | TextareaField | NumberField | BooleanField | DateField | EmailField | JsonField | SelectField | SlugField | RichTextField | UploadField | RelationshipField | ArrayField | GroupField | BlocksField;
996
+ /**
997
+ * Stores nothing: a component in the edit form (`admin.component`), e.g. a calculator, a preview
998
+ * or a button. Placed like any field (layout, groups, conditions); not in the database, the API or
999
+ * the generated types.
1000
+ */
1001
+ interface UiField extends BaseField<'ui', never> {
1002
+ readonly admin: FieldAdmin & {
1003
+ readonly component: AdminComponent;
1004
+ };
1005
+ }
1006
+ type Field = CustomField | TextField | TextareaField | NumberField | BooleanField | DateField | EmailField | JsonField | SelectField | SlugField | RichTextField | UploadField | RelationshipField | ArrayField | GroupField | BlocksField | UiField;
997
1007
  type FieldType = Field['type'];
998
1008
  /**
999
1009
  * The shortest `like` on a field with `index: 'trigram'`: trigrams are three characters, so a
1000
1010
  * shorter search can't use the index and would read every row.
1001
1011
  */
1002
1012
  declare const TRIGRAM_LIKE_MIN = 3;
1003
- declare const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "date", "email", "json", "select", "slug", "richText", "upload", "relationship", "array", "group", "blocks"];
1013
+ declare const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "date", "email", "json", "select", "slug", "richText", "upload", "relationship", "array", "group", "blocks", "ui"];
1004
1014
 
1005
1015
  type Simplify<T> = {
1006
1016
  [K in keyof T]: T[K];
@@ -1259,8 +1269,11 @@ type BlockInput<B> = B extends {
1259
1269
  id?: string;
1260
1270
  blockType: S;
1261
1271
  }> : never;
1272
+ /** The fields with a value: not `hidden`, and not `ui` fields (which store nothing). */
1262
1273
  type Visible<Fs extends readonly Field[]> = Exclude<Fs[number], {
1263
1274
  readonly hidden: true;
1275
+ } | {
1276
+ readonly type: 'ui';
1264
1277
  }>;
1265
1278
  /** Always set when read: required fields, and lists and groups (empty `[]` / `{}` otherwise). */
1266
1279
  type Present = {
@@ -2237,6 +2250,11 @@ declare class EasyCMS<C extends Config = Config> {
2237
2250
  ran: number;
2238
2251
  failed: number;
2239
2252
  }>;
2253
+ /**
2254
+ * Puts a failed scheduled publish or unpublish back in the queue and runs what is due (#333).
2255
+ * Returns `false` when the job isn't a failed one.
2256
+ */
2257
+ retryScheduled(id: ID): Promise<boolean>;
2240
2258
  /** Schedules a publish or unpublish, replacing a pending job with the same action. */
2241
2259
  schedule<S extends Slug<C>>(collection: S, id: ID, job: {
2242
2260
  action: 'publish' | 'unpublish';
@@ -3803,5 +3821,5 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
3803
3821
  */
3804
3822
  declare function defineConfig<const TConfig extends Config>(config: TConfig): TConfig;
3805
3823
 
3806
- export { ADAPTER_API_VERSION as A, EasyCMS as E, AuditLog as S, Auth as V, EasyCMSError as aE, FIELD_TYPES as aL, ConfigError as am, SchemaError as b$, ForbiddenError as b3, PLUGIN_API_VERSION as bC, PayloadTooLargeError as bH, QueryError as bN, ROLE_OPERATIONS as bO, Roles as bY, NotFoundError as bw, uniqueWithinOf as c$, WEBHOOK_EVENTS as cD, anyone as cI, consoleEmail as cJ, consoleLogger as cK, createEasyCMS as cL, defineConfig as cM, defineFieldType as cN, definePlugin as cO, diskStorage as cP, hasRows as cQ, isAdmin as cR, isHasMany as cS, isSignedIn as cT, isStaff as cU, isSystemAdmin as cV, jsonLogger as cW, memorySearch as cX, mimeAllowedBy as cY, rowFields as cZ, silentLogger as c_, SingleSignOn as ce, TRIGRAM_LIKE_MIN as cl, TooManyRequestsError as cp, UnauthorizedError as cr, ValidationError as cz, API_KEYS as d0, API_KEY_GLOBAL_OPERATIONS as d1, API_KEY_OPERATIONS as d2, DEFAULT_MAX_VERSIONS as d3, DEFAULT_PASSWORD_EMAILS as d4, FIELD_TYPE_BASES as d5, FOLDER_LEVELS as d6, MediaFolders as d7, SSO_COOKIE as d8, codeOfStatus as da, isJsonLogger as db, keyAllows as dc, logFormat as dd, secretsOf as de, ADMIN_ICONS as e };
3807
- export type { AuthUser as $, AfterDeleteHook as B, Config as C, AfterReadHook as D, Field as F, ApiKeyContext as G, ApiKeyGlobalOperation as H, ID as I, ApiKeyOperation as J, ApiKeyPermissions as K, LocalizationConfig as L, ArrayField as M, AuditChange as N, AuditConfig as O, PaginatedDocs as P, AuditEntry as Q, ResolvedConfig as R, AuditPage as T, AuditVerification as U, WebhookPayload as W, AuthConfig as X, AuthProvider as Y, AuthProviderProfile as Z, AuthProviderRequest as _, FieldError as a, FindArgs as a$, BackupsConfig as a0, BeforeChangeHook as a1, BeforeDeleteHook as a2, BeforeValidateHook as a3, Block as a4, BlocksField as a5, BooleanField as a6, BuiltinApiKeysCollection as a7, BuiltinMediaCollection as a8, BuiltinMediaFoldersCollection as a9, DepthOptions as aA, DirectUpload as aB, DiskStorageOptions as aC, DocumentOf as aD, EmailAdapter as aF, EmailField as aG, EmailMessage as aH, Endpoint as aI, EndpointRequest as aJ, ErrorCode as aK, FieldAccess as aM, FieldAccessFn as aN, FieldAdmin as aO, FieldRule as aP, FieldType as aQ, FieldTypeBase as aR, FieldTypeDefinition as aS, FieldTypeValidateContext as aT, FieldValidate as aU, FieldValidateContext as aV, FieldValue as aW, FieldsInput as aX, FieldsValue as aY, FilterOptionsArgs as aZ, FindAfterArgs as a_, BuiltinUsersCollection as aa, BundleHints as ab, CliCommand as ac, CollectionAccess as ad, CollectionAdmin as ae, CollectionCalendarAdmin as af, CollectionConfig as ag, CollectionDocument as ah, CollectionHooks as ai, CollectionListAdmin as aj, CollectionSearch as ak, CollectionSlug as al, ConfigIssue as an, ContainerAdmin as ao, CreateInput as ap, CreateMigrationResult as aq, CursorDocs as ar, CustomField as as, CustomFieldTypes as at, DashboardWidget as au, Database as av, DatabaseAdapter as aw, DatabaseInitArgs as ax, DatabaseTransfer as ay, DateField as az, FilterOptions as b, FindOptions as b0, FolderLevel as b1, FolderPermissions as b2, GlobalAccess as b4, GlobalConfig as b5, GlobalDocument as b6, GlobalHooks as b7, GlobalInput as b8, GlobalSlug as b9, Operation as bA, OutboxRow as bB, PasswordEmail as bD, PasswordEmailArgs as bE, PasswordEmailFn as bF, PasswordLinkOptions as bG, Plugin as bI, PluginInfo as bJ, PluginTypes as bK, PreviewTarget as bL, PreviewURL as bM, RawDocument as bP, ReadOptions as bQ, RelationshipField as bR, RequestContext as bS, RichTextDocument as bT, RichTextField as bU, Role as bV, RoleOperation as bW, RolePermissions as bX, RoutesConfig as bZ, ScheduledPublish as b_, GroupField as ba, ImageSize as bb, IncrementOptions as bc, InferCollection as bd, InferGlobal as be, JobConfig as bf, JsonField as bg, KeysetPage as bh, Label as bi, LayoutNode as bj, LivePreview as bk, LocaleOf as bl, LogDetails as bm, Logger as bn, LoginArgs as bo, LogsConfig as bp, MediaDocument as bq, MediaSize as br, MembersConfig as bs, MembersSignup as bt, MigrationInfo as bu, NavGroup as bv, NumberField as bx, OnRequest as by, OnRequestArgs as bz, CreateEasyCMSOptions as c, SchemaMode as c0, SearchAdapter as c1, SearchEntry as c2, SearchHits as c3, SearchOptions as c4, SearchQuery as c5, SearchResults as c6, SearchSetting as c7, SelectField as c8, SelectOption as c9, Version as cA, VersionSummary as cB, VersionsConfig as cC, WebhookConfig as cE, WebhookEvent as cF, Where as cG, WhereOperators as cH, Session as ca, SidebarPanel as cb, SignupArgs as cc, SignupResult as cd, SlugField as cf, SlugOf as cg, SsoOutcome as ch, SsoProviderRef as ci, StorageAdapter as cj, StoredFile as ck, TelemetryConfig as cm, TextField as cn, TextareaField as co, TypedPlugin as cq, UncountedDocs as cs, UpdateInput as ct, UpdateOptions as cu, UploadConfig as cv, UploadField as cw, UploadFromURLConfig as cx, UserIdentity as cy, EmailSetting as d, Secrets as d9, Access as f, AccessArgs as g, AccessOptions as h, AdminBrand as i, AdminCommand as j, AdminComponent as k, AdminConfig as l, AdminFieldArgs as m, AdminIcon as n, AdminLocale as o, AdminPage as p, AdminRole as q, AdminRoleChange as r, AdminRoleField as s, AdminRoleTarget as t, AdminRoleView as u, AdminRoles as v, AdminSso as w, AdminSwitcher as x, AdminViewAccess as y, AfterChangeHook as z };
3824
+ export { ADAPTER_API_VERSION as A, EasyCMS as E, AuditLog as S, Auth as V, EasyCMSError as aE, FIELD_TYPES as aL, ConfigError as am, SchemaError as b$, ForbiddenError as b3, PLUGIN_API_VERSION as bC, PayloadTooLargeError as bH, QueryError as bN, ROLE_OPERATIONS as bO, Roles as bY, NotFoundError as bw, silentLogger as c$, ValidationError as cA, WEBHOOK_EVENTS as cE, anyone as cJ, consoleEmail as cK, consoleLogger as cL, createEasyCMS as cM, defineConfig as cN, defineFieldType as cO, definePlugin as cP, diskStorage as cQ, hasRows as cR, isAdmin as cS, isHasMany as cT, isSignedIn as cU, isStaff as cV, isSystemAdmin as cW, jsonLogger as cX, memorySearch as cY, mimeAllowedBy as cZ, rowFields as c_, SingleSignOn as ce, TRIGRAM_LIKE_MIN as cl, TooManyRequestsError as cp, UnauthorizedError as cs, uniqueWithinOf as d0, API_KEYS as d1, API_KEY_GLOBAL_OPERATIONS as d2, API_KEY_OPERATIONS as d3, DEFAULT_MAX_VERSIONS as d4, DEFAULT_PASSWORD_EMAILS as d5, FIELD_TYPE_BASES as d6, FOLDER_LEVELS as d7, MediaFolders as d8, SSO_COOKIE as d9, codeOfStatus as db, isJsonLogger as dc, keyAllows as dd, logFormat as de, secretsOf as df, ADMIN_ICONS as e };
3825
+ export type { AuthUser as $, AfterDeleteHook as B, Config as C, AfterReadHook as D, Field as F, ApiKeyContext as G, ApiKeyGlobalOperation as H, ID as I, ApiKeyOperation as J, ApiKeyPermissions as K, LocalizationConfig as L, ArrayField as M, AuditChange as N, AuditConfig as O, PaginatedDocs as P, AuditEntry as Q, ResolvedConfig as R, AuditPage as T, AuditVerification as U, WebhookPayload as W, AuthConfig as X, AuthProvider as Y, AuthProviderProfile as Z, AuthProviderRequest as _, FieldError as a, FindArgs as a$, BackupsConfig as a0, BeforeChangeHook as a1, BeforeDeleteHook as a2, BeforeValidateHook as a3, Block as a4, BlocksField as a5, BooleanField as a6, BuiltinApiKeysCollection as a7, BuiltinMediaCollection as a8, BuiltinMediaFoldersCollection as a9, DepthOptions as aA, DirectUpload as aB, DiskStorageOptions as aC, DocumentOf as aD, EmailAdapter as aF, EmailField as aG, EmailMessage as aH, Endpoint as aI, EndpointRequest as aJ, ErrorCode as aK, FieldAccess as aM, FieldAccessFn as aN, FieldAdmin as aO, FieldRule as aP, FieldType as aQ, FieldTypeBase as aR, FieldTypeDefinition as aS, FieldTypeValidateContext as aT, FieldValidate as aU, FieldValidateContext as aV, FieldValue as aW, FieldsInput as aX, FieldsValue as aY, FilterOptionsArgs as aZ, FindAfterArgs as a_, BuiltinUsersCollection as aa, BundleHints as ab, CliCommand as ac, CollectionAccess as ad, CollectionAdmin as ae, CollectionCalendarAdmin as af, CollectionConfig as ag, CollectionDocument as ah, CollectionHooks as ai, CollectionListAdmin as aj, CollectionSearch as ak, CollectionSlug as al, ConfigIssue as an, ContainerAdmin as ao, CreateInput as ap, CreateMigrationResult as aq, CursorDocs as ar, CustomField as as, CustomFieldTypes as at, DashboardWidget as au, Database as av, DatabaseAdapter as aw, DatabaseInitArgs as ax, DatabaseTransfer as ay, DateField as az, FilterOptions as b, FindOptions as b0, FolderLevel as b1, FolderPermissions as b2, GlobalAccess as b4, GlobalConfig as b5, GlobalDocument as b6, GlobalHooks as b7, GlobalInput as b8, GlobalSlug as b9, Operation as bA, OutboxRow as bB, PasswordEmail as bD, PasswordEmailArgs as bE, PasswordEmailFn as bF, PasswordLinkOptions as bG, Plugin as bI, PluginInfo as bJ, PluginTypes as bK, PreviewTarget as bL, PreviewURL as bM, RawDocument as bP, ReadOptions as bQ, RelationshipField as bR, RequestContext as bS, RichTextDocument as bT, RichTextField as bU, Role as bV, RoleOperation as bW, RolePermissions as bX, RoutesConfig as bZ, ScheduledPublish as b_, GroupField as ba, ImageSize as bb, IncrementOptions as bc, InferCollection as bd, InferGlobal as be, JobConfig as bf, JsonField as bg, KeysetPage as bh, Label as bi, LayoutNode as bj, LivePreview as bk, LocaleOf as bl, LogDetails as bm, Logger as bn, LoginArgs as bo, LogsConfig as bp, MediaDocument as bq, MediaSize as br, MembersConfig as bs, MembersSignup as bt, MigrationInfo as bu, NavGroup as bv, NumberField as bx, OnRequest as by, OnRequestArgs as bz, CreateEasyCMSOptions as c, SchemaMode as c0, SearchAdapter as c1, SearchEntry as c2, SearchHits as c3, SearchOptions as c4, SearchQuery as c5, SearchResults as c6, SearchSetting as c7, SelectField as c8, SelectOption as c9, Version as cB, VersionSummary as cC, VersionsConfig as cD, WebhookConfig as cF, WebhookEvent as cG, Where as cH, WhereOperators as cI, Session as ca, SidebarPanel as cb, SignupArgs as cc, SignupResult as cd, SlugField as cf, SlugOf as cg, SsoOutcome as ch, SsoProviderRef as ci, StorageAdapter as cj, StoredFile as ck, TelemetryConfig as cm, TextField as cn, TextareaField as co, TypedPlugin as cq, UiField as cr, UncountedDocs as ct, UpdateInput as cu, UpdateOptions as cv, UploadConfig as cw, UploadField as cx, UploadFromURLConfig as cy, UserIdentity as cz, EmailSetting as d, Secrets as da, Access as f, AccessArgs as g, AccessOptions as h, AdminBrand as i, AdminCommand as j, AdminComponent as k, AdminConfig as l, AdminFieldArgs as m, AdminIcon as n, AdminLocale as o, AdminPage as p, AdminRole as q, AdminRoleChange as r, AdminRoleField as s, AdminRoleTarget as t, AdminRoleView as u, AdminRoles as v, AdminSso as w, AdminSwitcher as x, AdminViewAccess as y, AfterChangeHook as z };
@@ -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-Bsp6rkuH.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-BSIIyNMF.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;
@@ -79,6 +79,8 @@ interface Reference {
79
79
  readonly mimeTypes?: readonly string[];
80
80
  /** The upload's `folder` with `folderOnly`: the file must be in it, checked when saving. */
81
81
  readonly folderOnly?: string;
82
+ /** A link in rich text (#326): its collection must exist and have a URL (`preview`). */
83
+ readonly link?: true;
82
84
  }
83
85
  interface ValidateOptions {
84
86
  readonly operation: 'create' | 'update';
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-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';
1
+ import { P as PaginatedDocs, I as ID, d as EmailSetting, W as WebhookPayload } from './config-BSIIyNMF.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 UiField, cs as UnauthorizedError, ct as UncountedDocs, cu as UpdateInput, cv as UpdateOptions, cw as UploadConfig, cx as UploadField, cy as UploadFromURLConfig, cz as UserIdentity, cA as ValidationError, cB as Version, cC as VersionSummary, cD as VersionsConfig, cE as WEBHOOK_EVENTS, cF as WebhookConfig, cG as WebhookEvent, cH as Where, cI as WhereOperators, cJ as anyone, cK as consoleEmail, cL as consoleLogger, cM as createEasyCMS, cN as defineConfig, cO as defineFieldType, cP as definePlugin, cQ as diskStorage, cR as hasRows, cS as isAdmin, cT as isHasMany, cU as isSignedIn, cV as isStaff, cW as isSystemAdmin, cX as jsonLogger, cY as memorySearch, cZ as mimeAllowedBy, c_ as rowFields, c$ as silentLogger, d0 as uniqueWithinOf } from './config-BSIIyNMF.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-B0_7iJ4s.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-DK6AcW-x.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-6CJMRQO4.js";
31
+ } from "./chunk-3RZVRHBO.js";
32
32
  import {
33
33
  matchesCondition
34
34
  } from "./chunk-U266LV4M.js";
@@ -53,7 +53,7 @@ import {
53
53
  defineFieldType,
54
54
  definePlugin,
55
55
  warnDeprecated
56
- } from "./chunk-RJGKAYUU.js";
56
+ } from "./chunk-SYUQX2G6.js";
57
57
  export {
58
58
  ADAPTER_API_VERSION,
59
59
  ADMIN_API_VERSION,
@@ -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-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';
2
+ export { d1 as API_KEYS, d2 as API_KEY_GLOBAL_OPERATIONS, d3 as API_KEY_OPERATIONS, d4 as DEFAULT_MAX_VERSIONS, d5 as DEFAULT_PASSWORD_EMAILS, d6 as FIELD_TYPE_BASES, d7 as FOLDER_LEVELS, d8 as MediaFolders, d9 as SSO_COOKIE, da as Secrets, cl as TRIGRAM_LIKE_MIN, db as codeOfStatus, cR as hasRows, cT as isHasMany, dc as isJsonLogger, dd as keyAllows, de as logFormat, cZ as mimeAllowedBy, c_ as rowFields, df as secretsOf, d0 as uniqueWithinOf } from './config-BSIIyNMF.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-DK6AcW-x.js';
4
4
  export { conditionIssues } from './conditions.js';
5
5
 
6
6
  /** The session cookie the REST API sets at login. */
@@ -92,12 +92,14 @@ declare const DEFAULT_DEPTH = 1;
92
92
  declare const MAX_DEPTH = 3;
93
93
  /** Loads the documents of one collection that the caller may see. */
94
94
  type Loader = (collection: CollectionConfig, ids: readonly ID[]) => Promise<RawDocument[]>;
95
+ /** A linked document's URL, for links in rich text (#326); `null` when it has none. */
96
+ type LinkURL = (collection: CollectionConfig, doc: RawDocument) => string | null;
95
97
  /**
96
98
  * Replaces relationship and upload ids with documents, `depth` levels deep.
97
99
  * Missing (or unreadable) documents become `null` (single) or are dropped (hasMany).
98
100
  * Returns new objects; the input is not modified.
99
101
  */
100
- declare function populate(load: Loader, collections: readonly CollectionConfig[], fields: readonly Field[], docs: RawDocument[], depth: number): Promise<RawDocument[]>;
102
+ declare function populate(load: Loader, collections: readonly CollectionConfig[], fields: readonly Field[], docs: RawDocument[], depth: number, linkURL?: LinkURL): Promise<RawDocument[]>;
101
103
 
102
104
  /**
103
105
  * Finds the file of an admin module (`admin.modules`): a package export or a path relative to
package/dist/internal.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  verifyPassword,
59
59
  verifyTurnstile,
60
60
  writeBackupFile
61
- } from "./chunk-6CJMRQO4.js";
61
+ } from "./chunk-3RZVRHBO.js";
62
62
  import {
63
63
  conditionIssues
64
64
  } from "./chunk-U266LV4M.js";
@@ -77,7 +77,7 @@ import {
77
77
  rowFields,
78
78
  sniffMimeType,
79
79
  uniqueWithinOf
80
- } from "./chunk-RJGKAYUU.js";
80
+ } from "./chunk-SYUQX2G6.js";
81
81
 
82
82
  // src/typegen.ts
83
83
  var pascal = (name) => name.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
@@ -160,12 +160,15 @@ ${indent}}`
160
160
  );
161
161
  return `(${kinds.join(" | ")})[]`;
162
162
  }
163
+ // Stores nothing (and is gone from a resolved config).
164
+ case "ui":
165
+ return "never";
163
166
  }
164
167
  }
165
168
  function fieldLines(fields, n, indent) {
166
169
  const lines = [];
167
170
  for (const field of fields) {
168
- if (field.hidden) continue;
171
+ if (field.hidden || field.type === "ui") continue;
169
172
  const type = fieldType(field, n, indent);
170
173
  const alwaysPresent = field.type === "array" || field.type === "blocks" || field.type === "group" || isHasMany(field);
171
174
  if (field.required || alwaysPresent) lines.push(`${indent}${key(field.name)}: ${type}`);
@@ -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-Bsp6rkuH.js';
1
+ import { bi as Label, F as Field, I as ID, o as AdminLocale, x as AdminSwitcher, ci as SsoProviderRef } from './config-BSIIyNMF.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-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';
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, cs as UnauthorizedError, cA as ValidationError, cH as Where, cO as defineFieldType, cP as definePlugin } from './config-BSIIyNMF.js';
2
+ export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-B0_7iJ4s.js';
3
3
  import './conditions.js';
package/dist/plugin.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  defineFieldType,
17
17
  definePlugin,
18
18
  warnDeprecated
19
- } from "./chunk-RJGKAYUU.js";
19
+ } from "./chunk-SYUQX2G6.js";
20
20
  export {
21
21
  ADMIN_API_VERSION,
22
22
  ADMIN_ICONS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "1.25.1",
3
+ "version": "1.27.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",