@easy-cms/core 1.28.0 → 1.30.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.
@@ -26,9 +26,12 @@ import {
26
26
  imageDimensions,
27
27
  isHasMany,
28
28
  isPassingError,
29
+ isPolymorphic,
29
30
  isPrivateKey,
30
31
  mimeAllowed,
31
32
  mimeAllowedBy,
33
+ polymorphicValue,
34
+ relationTargets,
32
35
  rowFields,
33
36
  sizeKey,
34
37
  sniffMimeType,
@@ -38,7 +41,7 @@ import {
38
41
  warnDeprecated,
39
42
  withCharset,
40
43
  withPrivacy
41
- } from "./chunk-SYUQX2G6.js";
44
+ } from "./chunk-X4SI2QB7.js";
42
45
 
43
46
  // src/access.ts
44
47
  var isSystemAdmin = (user) => user?.role === "admin" && !user.apiKey && user.scoped !== true;
@@ -1742,6 +1745,33 @@ var SingleSignOn = class {
1742
1745
  }
1743
1746
  };
1744
1747
 
1748
+ // src/rich-text-blocks.ts
1749
+ var BLOCK_NODE = "block";
1750
+ function blockNodes(doc) {
1751
+ const found = [];
1752
+ const walk = (node, depth) => {
1753
+ if (!node || typeof node !== "object" || depth > 200) return;
1754
+ const { type, content } = node;
1755
+ if (type === BLOCK_NODE) {
1756
+ const n = node;
1757
+ if (!n.attrs || typeof n.attrs !== "object") n.attrs = {};
1758
+ found.push(n);
1759
+ return;
1760
+ }
1761
+ if (Array.isArray(content)) for (const child of content) walk(child, depth + 1);
1762
+ };
1763
+ walk(doc, 0);
1764
+ return found;
1765
+ }
1766
+ function blockOf(field, node) {
1767
+ return field.blocks?.find((block) => block.slug === node.attrs.blockType);
1768
+ }
1769
+ function blockFields(node) {
1770
+ const { fields } = node.attrs;
1771
+ return fields && typeof fields === "object" && !Array.isArray(fields) ? fields : {};
1772
+ }
1773
+ var hasBlocks = (field, value) => (field.blocks?.length ?? 0) > 0 && !!value && typeof value === "object" && value.type === "doc";
1774
+
1745
1775
  // src/access-control.ts
1746
1776
  async function evaluateAccess(access, args) {
1747
1777
  const result = await (access ?? isStaff)(args);
@@ -1777,6 +1807,14 @@ var FieldAccessChecker = class {
1777
1807
  return allowed2;
1778
1808
  }
1779
1809
  };
1810
+ async function mapBlocks(field, doc, map) {
1811
+ const copy = structuredClone(doc);
1812
+ for (const node of blockNodes(copy)) {
1813
+ const block = blockOf(field, node);
1814
+ if (block) node.attrs = { ...node.attrs, fields: await map(block.fields, blockFields(node)) };
1815
+ }
1816
+ return copy;
1817
+ }
1780
1818
  async function stripFields(fields, doc, read2) {
1781
1819
  const result = { ...doc };
1782
1820
  for (const field of fields) {
@@ -1788,6 +1826,8 @@ async function stripFields(fields, doc, read2) {
1788
1826
  const value = result[field.name];
1789
1827
  if (field.type === "group" && value && typeof value === "object") {
1790
1828
  result[field.name] = await stripFields(field.fields, value, read2);
1829
+ } else if (field.type === "richText" && hasBlocks(field, value)) {
1830
+ result[field.name] = await mapBlocks(field, value, (f, d) => stripFields(f, d, read2));
1791
1831
  } else if (hasRows(field) && Array.isArray(value)) {
1792
1832
  result[field.name] = await Promise.all(
1793
1833
  value.map((row) => {
@@ -1810,6 +1850,8 @@ async function filterInput(fields, data, update2) {
1810
1850
  const value = result[field.name];
1811
1851
  if (field.type === "group" && value && typeof value === "object" && !Array.isArray(value)) {
1812
1852
  result[field.name] = await filterInput(field.fields, value, update2);
1853
+ } else if (field.type === "richText" && hasBlocks(field, value)) {
1854
+ result[field.name] = await mapBlocks(field, value, (f, d) => filterInput(f, d, update2));
1813
1855
  } else if (hasRows(field) && Array.isArray(value)) {
1814
1856
  result[field.name] = await Promise.all(
1815
1857
  value.map((row) => {
@@ -2786,7 +2828,8 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
2786
2828
  if (!isPlainObject(raw) || raw.type !== "doc")
2787
2829
  return fail('must be a rich text document ({ type: "doc" })');
2788
2830
  const marks = docLinkMarks(raw);
2789
- if (marks.length === 0) return raw;
2831
+ const nodes = blockNodes(raw);
2832
+ if (marks.length === 0 && nodes.length === 0) return raw;
2790
2833
  const doc = structuredClone(raw);
2791
2834
  for (const mark of docLinkMarks(doc)) {
2792
2835
  const target = linkTarget(mark.attrs.doc);
@@ -2800,24 +2843,53 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
2800
2843
  };
2801
2844
  references2.push({ field: path, collection: target.relationTo, id, link: true });
2802
2845
  }
2846
+ for (const node of blockNodes(doc)) {
2847
+ const { id: rawId, blockType } = node.attrs;
2848
+ const id = typeof rawId === "string" && rawId !== "" ? rawId : randomUUID();
2849
+ const block = blockOf(field, node);
2850
+ if (!block) {
2851
+ const known = field.blocks?.map((b) => `"${b.slug}"`).join(", ");
2852
+ errors.push({
2853
+ field: `${path}.${id}`,
2854
+ message: `has a block "${String(blockType)}" this field doesn't have${known ? `: use one of ${known}` : ""}`
2855
+ });
2856
+ continue;
2857
+ }
2858
+ const input = applyDefaults(block.fields, generateSlugs(block.fields, blockFields(node)));
2859
+ const sub = await validateFields(block.fields, input, options, `${path}.${id}.`);
2860
+ errors.push(...sub.errors);
2861
+ references2.push(...sub.references);
2862
+ node.attrs = { ...node.attrs, blockType: block.slug, id, fields: sub.data };
2863
+ }
2803
2864
  return doc;
2804
2865
  }
2805
2866
  case "upload":
2806
2867
  case "relationship": {
2807
- const target = field.type === "upload" ? "media" : field.to;
2868
+ const targets = relationTargets(field);
2869
+ const poly = isPolymorphic(field);
2808
2870
  const many = field.hasMany === true;
2809
2871
  const values = many ? raw : [raw];
2810
- if (!Array.isArray(values)) return fail("must be an array of ids");
2872
+ if (!Array.isArray(values))
2873
+ return fail(poly ? "must be an array of { relationTo, value }" : "must be an array of ids");
2811
2874
  if (many && field.minRows !== void 0 && values.length < field.minRows)
2812
2875
  return fail(`must have at least ${itemsOf(field, field.minRows)}`);
2813
2876
  if (many && field.maxRows !== void 0 && values.length > field.maxRows)
2814
2877
  return fail(`must have at most ${itemsOf(field, field.maxRows)}`);
2815
- const ids = [];
2878
+ const out = [];
2816
2879
  for (const item of values) {
2817
- const id = parseId(isPlainObject(item) ? item.id : item);
2880
+ const pair = poly ? polymorphicValue(item) : void 0;
2881
+ if (poly && (!pair || !targets.includes(pair.relationTo)))
2882
+ return fail(
2883
+ pair ? `can't link to "${pair.relationTo}": use one of ${targets.map((t) => `"${t}"`).join(", ")}` : `must be ${many ? "a list of" : ""} { relationTo, value } with relationTo one of ${targets.map((t) => `"${t}"`).join(", ")}`
2884
+ );
2885
+ const target = pair ? pair.relationTo : targets[0];
2886
+ const rawId = pair ? pair.value : item;
2887
+ const id = parseId(isPlainObject(rawId) ? rawId.id : rawId);
2818
2888
  if (id === void 0)
2819
- return fail(`must be ${many ? "ids" : "an id"} of "${target}" documents`);
2820
- ids.push(id);
2889
+ return fail(
2890
+ poly ? `must have the id of a "${target}" document as value` : `must be ${many ? "ids" : "an id"} of "${target}" documents`
2891
+ );
2892
+ out.push(pair ? { relationTo: target, value: id } : id);
2821
2893
  references2.push({
2822
2894
  field: path,
2823
2895
  collection: target,
@@ -2827,7 +2899,7 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
2827
2899
  ...field.type === "upload" && field.folder && field.folderOnly ? { folderOnly: field.folder } : {}
2828
2900
  });
2829
2901
  }
2830
- return many ? ids : ids[0];
2902
+ return many ? out : out[0];
2831
2903
  }
2832
2904
  case "array": {
2833
2905
  if (!Array.isArray(raw)) return fail("must be an array");
@@ -4005,8 +4077,16 @@ function collectSlots(fields, data, bySlug, slots, linkURL) {
4005
4077
  collectSlots(fields2, row, bySlug, slots, linkURL);
4006
4078
  }
4007
4079
  }
4008
- } else if (field.type === "richText" && linkURL) {
4080
+ } else if (field.type === "richText") {
4009
4081
  if (value.type !== "doc") continue;
4082
+ for (const node of blockNodes(value)) {
4083
+ const block = blockOf(field, node);
4084
+ if (!block) continue;
4085
+ const fields2 = { ...blockFields(node) };
4086
+ node.attrs = { ...node.attrs, fields: fields2 };
4087
+ collectSlots(block.fields, fields2, bySlug, slots, linkURL);
4088
+ }
4089
+ if (!linkURL) continue;
4010
4090
  for (const mark of docLinkMarks(value)) {
4011
4091
  const target = linkTarget(mark.attrs.doc);
4012
4092
  const collection2 = target ? bySlug.get(target.relationTo) : void 0;
@@ -4021,9 +4101,34 @@ function collectSlots(fields, data, bySlug, slots, linkURL) {
4021
4101
  }
4022
4102
  });
4023
4103
  }
4104
+ } else if (isPolymorphic(field)) {
4105
+ const many = Array.isArray(value);
4106
+ const items = (many ? value : [value]).map(polymorphicValue);
4107
+ if (items.some((item) => item === void 0)) continue;
4108
+ const pairs = items;
4109
+ const found = /* @__PURE__ */ new Map();
4110
+ let waiting = 0;
4111
+ for (const relationTo of new Set(pairs.map((p) => p.relationTo))) {
4112
+ if (!bySlug.has(relationTo)) continue;
4113
+ waiting++;
4114
+ slots.push({
4115
+ collection: relationTo,
4116
+ ids: pairs.filter((p) => p.relationTo === relationTo).map((p) => p.value),
4117
+ assign: (byId) => {
4118
+ found.set(relationTo, byId);
4119
+ if (--waiting > 0) return;
4120
+ const filled = pairs.map((p) => {
4121
+ if (!bySlug.has(p.relationTo)) return p;
4122
+ const doc = found.get(p.relationTo)?.get(p.value);
4123
+ return doc ? { relationTo: p.relationTo, value: doc } : void 0;
4124
+ }).filter((p) => p !== void 0);
4125
+ data[field.name] = many ? filled : filled[0] ?? null;
4126
+ }
4127
+ });
4128
+ }
4024
4129
  } else if (field.type === "relationship" || field.type === "upload") {
4025
4130
  if (typeof value === "object" && !Array.isArray(value)) continue;
4026
- const collection2 = field.type === "upload" ? "media" : field.to;
4131
+ const collection2 = relationTargets(field)[0];
4027
4132
  if (!bySlug.has(collection2)) continue;
4028
4133
  if (Array.isArray(value)) {
4029
4134
  const ids = value;
@@ -5388,7 +5493,7 @@ var FIELD = {
5388
5493
  json: keys(BASE_FIELD),
5389
5494
  select: keys(`${BASE_FIELD} options hasMany minRows maxRows`),
5390
5495
  slug: keys(`${BASE_FIELD} from`),
5391
- richText: keys(BASE_FIELD),
5496
+ richText: keys(`${BASE_FIELD} blocks`),
5392
5497
  upload: keys(`${BASE_FIELD} hasMany minRows maxRows mimeTypes folder folderOnly filterOptions`),
5393
5498
  relationship: keys(`${BASE_FIELD} to hasMany minRows maxRows filterOptions`),
5394
5499
  array: keys(`${BASE_FIELD} fields minRows maxRows`),
@@ -5617,8 +5722,13 @@ function textOf(field, value) {
5617
5722
  return typeof value === "string" ? value : "";
5618
5723
  case "select":
5619
5724
  return [value].flat().filter((v) => typeof v === "string").join(" ");
5620
- case "richText":
5621
- return richText(value);
5725
+ case "richText": {
5726
+ const blocks = hasBlocks(field, value) ? blockNodes(value).map((node) => {
5727
+ const block = blockOf(field, node);
5728
+ return block ? fieldsText(block.fields, blockFields(node)) : "";
5729
+ }) : [];
5730
+ return [richText(value), ...blocks].filter(Boolean).join("\n");
5731
+ }
5622
5732
  case "group":
5623
5733
  return typeof value === "object" ? fieldsText(field.fields, value) : "";
5624
5734
  case "array":
@@ -6263,7 +6373,7 @@ function warnNestedUnique(config, push) {
6263
6373
  severity: "warning"
6264
6374
  });
6265
6375
  if (field.type === "group" || field.type === "array") walk(field.fields, `${at}.fields`, true);
6266
- if (field.type === "blocks")
6376
+ if (field.type === "blocks" || field.type === "richText")
6267
6377
  for (const block of Array.isArray(field.blocks) ? field.blocks : [])
6268
6378
  walk(block?.fields, `${at}.blocks.${String(block?.slug)}`, true);
6269
6379
  }
@@ -6738,7 +6848,7 @@ function validateUpload(config, add) {
6738
6848
  add(`${at}.folder`, "needs upload.folders: true", "upload: { folders: true }");
6739
6849
  if ((field.type === "group" || field.type === "array") && Array.isArray(field.fields))
6740
6850
  walk(field.fields, at);
6741
- if (field.type === "blocks" && Array.isArray(field.blocks)) {
6851
+ if ((field.type === "blocks" || field.type === "richText") && Array.isArray(field.blocks)) {
6742
6852
  for (const [b, block] of field.blocks.entries())
6743
6853
  if (Array.isArray(block?.fields)) walk(block.fields, `${at}.blocks[${b}]`);
6744
6854
  }
@@ -7346,16 +7456,24 @@ function validateField(field, path, siblings, collectionSlugs, add) {
7346
7456
  }
7347
7457
  }
7348
7458
  break;
7349
- case "relationship":
7350
- if (typeof field.to !== "string" || !collectionSlugs.has(field.to)) {
7351
- add(
7352
- `${path}.to`,
7353
- `unknown collection ${JSON.stringify(field.to)}`,
7354
- `use one of: ${[...collectionSlugs].join(", ")}`
7355
- );
7459
+ case "relationship": {
7460
+ const to = field.to;
7461
+ if (Array.isArray(to)) {
7462
+ if (to.length === 0) add(`${path}.to`, "needs at least one collection");
7463
+ if (new Set(to).size !== to.length) add(`${path}.to`, "names a collection twice");
7464
+ }
7465
+ for (const target of Array.isArray(to) ? to : [to]) {
7466
+ if (typeof target !== "string" || !collectionSlugs.has(target)) {
7467
+ add(
7468
+ `${path}.to`,
7469
+ `unknown collection ${JSON.stringify(target)}`,
7470
+ `use one of: ${[...collectionSlugs].join(", ")}`
7471
+ );
7472
+ }
7356
7473
  }
7357
7474
  validateManyRange(field, path, add);
7358
7475
  break;
7476
+ }
7359
7477
  case "upload":
7360
7478
  validateManyRange(field, path, add);
7361
7479
  if (field.folder !== void 0 && !FOLDER_KEY.test(String(field.folder)))
@@ -7449,6 +7567,57 @@ function validateField(field, path, siblings, collectionSlugs, add) {
7449
7567
  }
7450
7568
  break;
7451
7569
  }
7570
+ case "richText": {
7571
+ const blocks = field.blocks;
7572
+ if (blocks === void 0) break;
7573
+ if (!Array.isArray(blocks) || blocks.length === 0) {
7574
+ add(
7575
+ `${path}.blocks`,
7576
+ "must be a list of at least one block",
7577
+ "blocks: [{ slug: 'callout', fields: [...] }]"
7578
+ );
7579
+ break;
7580
+ }
7581
+ const seen = /* @__PURE__ */ new Set();
7582
+ for (const [i, block] of blocks.entries()) {
7583
+ const blockPath = `${path}.blocks[${i}]`;
7584
+ if (typeof block?.slug !== "string" || !SLUG_PATTERN.test(block.slug)) {
7585
+ add(
7586
+ `${blockPath}.slug`,
7587
+ 'must be lowercase letters, digits, "-" or "_", starting with a letter'
7588
+ );
7589
+ } else if (seen.has(block.slug)) {
7590
+ add(`${blockPath}.slug`, `duplicate block "${block.slug}"`);
7591
+ } else {
7592
+ seen.add(block.slug);
7593
+ }
7594
+ const summary = block?.admin?.summary;
7595
+ if (summary !== void 0 && typeof summary !== "string" && typeof summary !== "function")
7596
+ add(`${blockPath}.admin.summary`, "must be a function or the tag of a custom element");
7597
+ if (!Array.isArray(block?.fields)) {
7598
+ add(`${blockPath}.fields`, "must be an array");
7599
+ continue;
7600
+ }
7601
+ const walk = (fields, at) => {
7602
+ for (const inner of fields) {
7603
+ const here = `${at}.${inner.name}`;
7604
+ if (inner.type === "richText" || inner.type === "blocks")
7605
+ add(here, `a ${inner.type} field can't be inside a rich text block`);
7606
+ if (inner.localized)
7607
+ add(
7608
+ `${here}.localized`,
7609
+ "fields in rich text blocks can't be localized",
7610
+ "set localized on the rich text field: its blocks follow its locale"
7611
+ );
7612
+ if ((inner.type === "group" || inner.type === "array") && Array.isArray(inner.fields))
7613
+ walk(inner.fields, `${here}.fields`);
7614
+ }
7615
+ };
7616
+ walk(block.fields, `${blockPath}.fields`);
7617
+ validateFields2(block.fields, `${blockPath}.fields`, /* @__PURE__ */ new Set(), collectionSlugs, add);
7618
+ }
7619
+ break;
7620
+ }
7452
7621
  }
7453
7622
  }
7454
7623
  var ADMIN_COMPONENT_TAG = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
@@ -7853,9 +8022,9 @@ function withoutUiFields(config) {
7853
8022
  changed = true;
7854
8023
  next = { ...field, fields: inner.data, [ADMIN_FIELDS]: inner.admin };
7855
8024
  }
7856
- } else if (field.type === "blocks") {
8025
+ } else if (field.type === "blocks" || field.type === "richText" && field.blocks) {
7857
8026
  let blocksChanged = false;
7858
- const blocks = field.blocks.map((block) => {
8027
+ const blocks = (field.blocks ?? []).map((block) => {
7859
8028
  const inner = strip(block.fields);
7860
8029
  if (!inner.changed) return block;
7861
8030
  blocksChanged = true;
@@ -7932,7 +8101,7 @@ function cacheableResponse(request, body, status, headers, times, tags) {
7932
8101
  }
7933
8102
 
7934
8103
  // src/version.ts
7935
- var VERSION = true ? "1.28.0" : "0.0.0-dev";
8104
+ var VERSION = true ? "1.30.0" : "0.0.0-dev";
7936
8105
 
7937
8106
  // src/telemetry.ts
7938
8107
  var DB_CALLS = /* @__PURE__ */ new Set([
@@ -11048,9 +11217,14 @@ var EasyCMS = class {
11048
11217
  for (const ref of references2) {
11049
11218
  const filter = ref.filterOptions;
11050
11219
  if (!filter) continue;
11051
- if (!allowed2.has(filter))
11052
- allowed2.set(filter, await filter({ id: selfId, user, cms, context }));
11053
- const where = allowed2.get(filter);
11220
+ const byCollection = allowed2.get(filter) ?? /* @__PURE__ */ new Map();
11221
+ allowed2.set(filter, byCollection);
11222
+ if (!byCollection.has(ref.collection))
11223
+ byCollection.set(
11224
+ ref.collection,
11225
+ await filter({ id: selfId, relationTo: ref.collection, user, cms, context })
11226
+ );
11227
+ const where = byCollection.get(ref.collection);
11054
11228
  if (where === true) continue;
11055
11229
  const target = this.config.collections.find((c) => c.slug === ref.collection);
11056
11230
  const localization = this.config.localization;
@@ -11487,10 +11661,11 @@ function ruleFields(fields) {
11487
11661
  }
11488
11662
  function references(fields, out = /* @__PURE__ */ new Set()) {
11489
11663
  for (const field of fields) {
11490
- if (field.type === "relationship") out.add(field.to);
11491
- else if (field.type === "upload") out.add("media");
11664
+ if (field.type === "relationship" || field.type === "upload")
11665
+ for (const target of relationTargets(field)) out.add(target);
11492
11666
  else if (field.type === "group" || field.type === "array") references(field.fields, out);
11493
- else if (field.type === "blocks") for (const b of field.blocks) references(b.fields, out);
11667
+ else if (field.type === "blocks" || field.type === "richText")
11668
+ for (const b of field.blocks ?? []) references(b.fields, out);
11494
11669
  }
11495
11670
  return out;
11496
11671
  }
@@ -12350,7 +12525,7 @@ async function serializeFields(fields, checks, localized, folderId, who) {
12350
12525
  if (field.from) f.from = field.from;
12351
12526
  break;
12352
12527
  case "relationship":
12353
- f.to = field.to;
12528
+ f.to = typeof field.to === "string" ? field.to : [...field.to];
12354
12529
  if (field.hasMany) f.hasMany = true;
12355
12530
  if (field.minRows !== void 0) f.minRows = field.minRows;
12356
12531
  if (field.maxRows !== void 0) f.maxRows = field.maxRows;
@@ -12384,6 +12559,26 @@ async function serializeFields(fields, checks, localized, folderId, who) {
12384
12559
  }))
12385
12560
  );
12386
12561
  break;
12562
+ case "richText":
12563
+ if (field.blocks)
12564
+ f.blocks = await Promise.all(
12565
+ field.blocks.map(async (block) => {
12566
+ const summary = block.admin?.summary;
12567
+ return {
12568
+ slug: block.slug,
12569
+ ...block.labels ? { labels: block.labels } : {},
12570
+ fields: await serializeFields(
12571
+ adminFieldsOf(block),
12572
+ checks,
12573
+ localized,
12574
+ folderId,
12575
+ who
12576
+ ),
12577
+ ...summary !== void 0 ? { summary: typeof summary === "function" ? "server" : summary } : {}
12578
+ };
12579
+ })
12580
+ );
12581
+ break;
12387
12582
  case "group":
12388
12583
  f.fields = await serializeFields(adminFieldsOf(field), checks, localized, folderId, who);
12389
12584
  break;
@@ -12902,15 +13097,38 @@ async function pickerFilter(cms, url, target, user, context = {}) {
12902
13097
  const slug2 = dot > 0 ? spec.slice(0, dot) : "";
12903
13098
  const owner = forCollection !== null ? cms.config.collections.find((c) => c.slug === slug2) : cms.config.globals.find((g) => g.slug === slug2);
12904
13099
  const field = owner ? fieldAt2(owner.fields, spec.slice(dot + 1)) : void 0;
12905
- const to = field?.type === "upload" ? "media" : field?.type === "relationship" ? field.to : null;
12906
- if (!field || field.type !== "relationship" && field.type !== "upload" || to !== target)
13100
+ if (!field || field.type !== "relationship" && field.type !== "upload" || !relationTargets(field).includes(target))
12907
13101
  throw new QueryError(`filterFor: no relationship to "${target}" at "${spec}"`);
12908
13102
  if (!field.filterOptions) return void 0;
12909
13103
  const rawId = url.searchParams.get("filterId");
12910
13104
  const id = rawId === null || rawId === "" ? void 0 : /^\d+$/.test(rawId) ? Number(rawId) : rawId;
12911
- const where = await field.filterOptions({ id, user, cms, context });
13105
+ const where = await field.filterOptions({ id, relationTo: target, user, cms, context });
12912
13106
  return where === true ? void 0 : where;
12913
13107
  }
13108
+ async function blockSummaries(cms, user, body) {
13109
+ const input = body && typeof body === "object" ? body : {};
13110
+ const owner = typeof input.collection === "string" ? cms.config.collections.find((c) => c.slug === input.collection) : typeof input.global === "string" ? cms.config.globals.find((g) => g.slug === input.global) : void 0;
13111
+ const field = owner && typeof input.field === "string" ? fieldAt2(owner.fields, input.field) : void 0;
13112
+ if (!field || field.type !== "richText" || !field.blocks)
13113
+ throw new QueryError("block-summaries: no rich text field with blocks there");
13114
+ const summaries = {};
13115
+ for (const item of Array.isArray(input.blocks) ? input.blocks.slice(0, 200) : []) {
13116
+ const { id, blockType, fields } = item ?? {};
13117
+ const summary = field.blocks.find((b) => b.slug === blockType)?.admin?.summary;
13118
+ if (typeof id !== "string" || typeof summary !== "function") continue;
13119
+ try {
13120
+ const text3 = await summary({
13121
+ fields: fields && typeof fields === "object" ? fields : {},
13122
+ user,
13123
+ cms
13124
+ });
13125
+ if (typeof text3 === "string") summaries[id] = text3;
13126
+ } catch (error) {
13127
+ cms.logger.warn(`admin.summary of block "${String(blockType)}" failed: ${String(error)}`);
13128
+ }
13129
+ }
13130
+ return { summaries };
13131
+ }
12914
13132
 
12915
13133
  // src/rest/query.ts
12916
13134
  var MAX_DEPTH2 = 10;
@@ -13204,7 +13422,8 @@ var ADMIN_UI_ROUTES = /* @__PURE__ */ new Set([
13204
13422
  "total",
13205
13423
  "media-usage",
13206
13424
  "owned",
13207
- "sso"
13425
+ "sso",
13426
+ "block-summaries"
13208
13427
  ]);
13209
13428
  var CORS_METHODS = "GET, HEAD, POST, PATCH, PUT, DELETE";
13210
13429
  var CORS_HEADERS = `authorization, content-type, ${CSRF_HEADER}`;
@@ -13581,6 +13800,12 @@ async function route(cms, ctx, method, path, options) {
13581
13800
  if (parsed === void 0) throw new HttpError("Not found", 404);
13582
13801
  return { body: await cms.roles.owned(parsed) };
13583
13802
  }
13803
+ if (first === "admin" && second === "block-summaries" && third === void 0) {
13804
+ if (!ctx.user) throw new UnauthorizedError();
13805
+ if (method !== "POST") throw methodNotAllowed(ctx, "POST");
13806
+ const body = await readJson(ctx.request);
13807
+ return { body: await blockSummaries(cms, ctx.user, body) };
13808
+ }
13584
13809
  if (first === "admin" && second === "media-usage" && segments.length === 2) {
13585
13810
  if (!ctx.user || ctx.user.apiKey) throw new UnauthorizedError();
13586
13811
  if (method !== "GET") throw methodNotAllowed(ctx, "GET");
@@ -333,6 +333,17 @@ function mimeAllowedBy(mimeType, patterns) {
333
333
  });
334
334
  }
335
335
  var SVG = "image/svg+xml";
336
+ var isPolymorphic = (field) => field.type === "relationship" && Array.isArray(field.to);
337
+ function relationTargets(field) {
338
+ if (field.type === "upload") return ["media"];
339
+ if (field.type !== "relationship") return [];
340
+ return typeof field.to === "string" ? [field.to] : field.to;
341
+ }
342
+ function polymorphicValue(value) {
343
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
344
+ const { relationTo } = value;
345
+ return typeof relationTo === "string" ? { relationTo, value: value.value } : void 0;
346
+ }
336
347
  var hasRows = (field) => field.type === "array" || field.type === "blocks";
337
348
  function rowFields(field, row) {
338
349
  if (field.type === "array") return field.fields;
@@ -502,7 +513,7 @@ function applyFieldTypes(config) {
502
513
  if (!type) {
503
514
  if ((field.type === "group" || field.type === "array") && Array.isArray(field.fields))
504
515
  return { ...field, fields: convert(field.fields, `${here}.fields`) };
505
- if (field.type === "blocks" && Array.isArray(field.blocks))
516
+ if ((field.type === "blocks" || field.type === "richText") && Array.isArray(field.blocks))
506
517
  return {
507
518
  ...field,
508
519
  blocks: field.blocks.map((block, b) => ({
@@ -592,6 +603,9 @@ export {
592
603
  uniqueWithinOf,
593
604
  isHasMany,
594
605
  mimeAllowedBy,
606
+ isPolymorphic,
607
+ relationTargets,
608
+ polymorphicValue,
595
609
  hasRows,
596
610
  rowFields,
597
611
  TRIGRAM_LIKE_MIN,
@@ -905,6 +905,28 @@ interface RichTextDocument {
905
905
  readonly content?: readonly unknown[];
906
906
  }
907
907
  interface RichTextField extends BaseField<'richText', RichTextDocument> {
908
+ /**
909
+ * Blocks editors place between paragraphs (#328): a card, a callout, a video. Each is a node
910
+ * `{ type: 'block', attrs: { blockType, id, fields } }` in the document.
911
+ */
912
+ readonly blocks?: readonly RichTextBlock[];
913
+ }
914
+ /** Arguments of a rich text block's `admin.summary` function. */
915
+ interface BlockSummaryArgs {
916
+ readonly fields: Record<string, unknown>;
917
+ readonly user: AuthUser | null;
918
+ readonly cms: EasyCMS;
919
+ }
920
+ /** A block in rich text (`richText.blocks`): the fields of `Block`, and how its card reads. */
921
+ interface RichTextBlock extends Block {
922
+ readonly admin?: {
923
+ /**
924
+ * The text of the block's card in the editor: a function, run on the server, or the tag of a
925
+ * custom element from an admin module, which gets `fields`. Default: the block's label and
926
+ * its first value.
927
+ */
928
+ readonly summary?: string | ((args: BlockSummaryArgs) => string | Promise<string>);
929
+ };
908
930
  }
909
931
  /** References a document in the built-in `media` collection: a file or an image. */
910
932
  interface UploadField extends BaseField<'upload', never> {
@@ -933,6 +955,8 @@ interface UploadField extends BaseField<'upload', never> {
933
955
  interface FilterOptionsArgs {
934
956
  /** The document being edited; `undefined` while it is being created. */
935
957
  readonly id: ID | undefined;
958
+ /** The collection the choices come from: one of the field's `to`. */
959
+ readonly relationTo: string;
936
960
  readonly user: AuthUser | null;
937
961
  readonly cms: EasyCMS;
938
962
  /** The request's context (`onRequest`), `{}` when there is none. */
@@ -944,8 +968,11 @@ interface FilterOptionsArgs {
944
968
  */
945
969
  type FilterOptions = (args: FilterOptionsArgs) => Where | true | Promise<Where | true>;
946
970
  interface RelationshipField extends BaseField<'relationship', never> {
947
- /** Slug of the target collection. */
948
- readonly to: string;
971
+ /**
972
+ * Slug of the target collection, or several: then each value is `{ relationTo, value }`, the
973
+ * collection and the document's id (#327).
974
+ */
975
+ readonly to: string | readonly string[];
949
976
  readonly hasMany?: boolean;
950
977
  /** With `hasMany`: the fewest documents. */
951
978
  readonly minRows?: number;
@@ -989,6 +1016,17 @@ declare const isHasMany: (field: Field) => boolean;
989
1016
  * carry scripts and is allowed only when listed by name: `image/svg+xml`).
990
1017
  */
991
1018
  declare function mimeAllowedBy(mimeType: string, patterns: readonly string[]): boolean;
1019
+ /** A relationship to several collections: its values are `{ relationTo, value }` (#327). */
1020
+ declare const isPolymorphic: (field: Field) => field is RelationshipField & {
1021
+ readonly to: readonly string[];
1022
+ };
1023
+ /** The collections a relationship or upload field points to. */
1024
+ declare function relationTargets(field: Field): readonly string[];
1025
+ /** A `{ relationTo, value }` of a relationship to several collections, or `undefined`. */
1026
+ declare function polymorphicValue(value: unknown): {
1027
+ relationTo: string;
1028
+ value: unknown;
1029
+ } | undefined;
992
1030
  /** Fields whose value is a list of rows: `array` and `blocks`. */
993
1031
  declare const hasRows: (field: Field) => field is ArrayField | BlocksField;
994
1032
  /** The fields of one row of an `array` or `blocks` field (`undefined` for an unknown block). */
@@ -1162,6 +1200,14 @@ type GlobalBySlug<C extends Config, S> = WithPluginFields<C, Extract<NonNullable
1162
1200
  type RelationValue<C extends Config, S, A extends boolean> = [CollectionBySlug<C, S>] extends [
1163
1201
  never
1164
1202
  ] ? ID | Record<string, unknown> : ID | CollectionDocument<C, S & string, A>;
1203
+ /**
1204
+ * A relationship to several collections (#327): `{ relationTo, value }` per collection, so
1205
+ * checking `relationTo` narrows `value`.
1206
+ */
1207
+ type PolymorphicValue<C extends Config, To, V extends 'output' | 'input', A extends boolean> = To extends readonly (infer S)[] ? S extends string ? {
1208
+ relationTo: S;
1209
+ value: V extends 'input' ? ID : RelationValue<C, S, A>;
1210
+ } : never : never;
1165
1211
  /** The content locales of a config (`localization.locales`); `string` when not literal. */
1166
1212
  type LocaleOf<C extends Config> = C extends {
1167
1213
  readonly localization: {
@@ -1174,6 +1220,11 @@ type LocaleOf<C extends Config> = C extends {
1174
1220
  */
1175
1221
  type LocaleMap<F extends Field, C extends Config> = {
1176
1222
  [L in LocaleOf<C>]?: (F extends {
1223
+ readonly type: 'relationship';
1224
+ readonly to: infer To extends readonly string[];
1225
+ } ? F extends {
1226
+ readonly hasMany: true;
1227
+ } ? PolymorphicValue<C, To, 'input', false>[] : PolymorphicValue<C, To, 'input', false> : F extends {
1177
1228
  readonly type: 'relationship' | 'upload';
1178
1229
  } ? F extends {
1179
1230
  readonly hasMany: true;
@@ -1232,6 +1283,11 @@ type OneLocaleValue<F extends Field, C extends Config, A extends boolean> = F ex
1232
1283
  } ? F extends {
1233
1284
  readonly hasMany: true;
1234
1285
  } ? (ID | MediaDocument)[] : ID | MediaDocument : F extends {
1286
+ readonly type: 'relationship';
1287
+ readonly to: infer To extends readonly string[];
1288
+ } ? F extends {
1289
+ readonly hasMany: true;
1290
+ } ? PolymorphicValue<C, To, 'output', A>[] : PolymorphicValue<C, To, 'output', A> : F extends {
1235
1291
  readonly type: 'relationship';
1236
1292
  readonly to: infer S;
1237
1293
  } ? F extends {
@@ -1328,6 +1384,11 @@ type CollectionDocument<C extends Config, S extends CollectionSlug<C>, AllLocale
1328
1384
  type GlobalDocument<C extends Config, S extends GlobalSlug<C>, AllLocales extends boolean = false> = InferGlobal<GlobalBySlug<C, S>, C, AllLocales>;
1329
1385
  type InputValue<F extends Field> = F extends {
1330
1386
  readonly type: 'relationship';
1387
+ readonly to: infer To extends readonly string[];
1388
+ } ? F extends {
1389
+ readonly hasMany: true;
1390
+ } ? readonly PolymorphicValue<Config, To, 'input', false>[] : PolymorphicValue<Config, To, 'input', false> : F extends {
1391
+ readonly type: 'relationship';
1331
1392
  } ? F extends {
1332
1393
  readonly hasMany: true;
1333
1394
  } ? readonly ID[] : ID : F extends {
@@ -1392,6 +1453,24 @@ type CreateInput<C extends Config, S extends CollectionSlug<C>> = Simplify<Field
1392
1453
  type UpdateInput<C extends Config, S extends CollectionSlug<C>> = Partial<CreateInput<C, S>>;
1393
1454
  /** Data accepted by `updateGlobal`. */
1394
1455
  type GlobalInput<C extends Config, S extends GlobalSlug<C>> = Partial<Simplify<FieldsInput<GlobalBySlug<C, S>['fields']> & StatusInput<GlobalBySlug<C, S>>>>;
1456
+ type BlocksOfField<F> = F extends {
1457
+ readonly type: 'richText';
1458
+ readonly blocks: readonly (infer B)[];
1459
+ } ? B : never;
1460
+ /**
1461
+ * The blocks of a rich text field, e.g. `RichTextBlocks<typeof config, 'posts', 'body'>`: a
1462
+ * union of `{ blockType, id, fields }`, for typed renderers in `@easy-cms/richtext`.
1463
+ */
1464
+ type RichTextBlocks<C extends Config, S extends CollectionSlug<C>, F extends string> = BlocksOfField<Extract<CollectionBySlug<C, S>['fields'][number], {
1465
+ readonly name: F;
1466
+ }>> extends infer B ? B extends {
1467
+ readonly slug: infer Slug;
1468
+ readonly fields: infer Fs extends readonly Field[];
1469
+ } ? {
1470
+ blockType: Slug;
1471
+ id: string;
1472
+ fields: Simplify<FieldsValue<Fs, C, false>>;
1473
+ } : never : never;
1395
1474
 
1396
1475
  /** What a role may do with a collection or global, ticked in Settings → Roles. */
1397
1476
  declare const ROLE_OPERATIONS: readonly ["read", "create", "update", "delete", "publish"];
@@ -3821,5 +3900,5 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
3821
3900
  */
3822
3901
  declare function defineConfig<const TConfig extends Config>(config: TConfig): TConfig;
3823
3902
 
3824
- export { ADAPTER_API_VERSION as A, EasyCMS as E, AuditLog as _, Auth as a1, EasyCMSError as aI, FIELD_TYPES as aP, ConfigError as ar, ForbiddenError as b7, PLUGIN_API_VERSION as bE, PayloadTooLargeError as bJ, QueryError as bP, ROLE_OPERATIONS as bQ, Roles as b_, NotFoundError as by, silentLogger as c$, SchemaError as c1, ValidationError as cB, WEBHOOK_EVENTS as cF, 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 cg, TRIGRAM_LIKE_MIN as cn, TooManyRequestsError as cr, UnauthorizedError as cu, 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 j };
3825
- export type { AuditPage as $, AdminRoles as B, Config as C, AdminSso as D, Field as F, GlobalSlug as G, AdminSwitcher as H, ID as I, AdminViewAccess as J, AfterChangeHook as K, LocalizationConfig as L, AfterDeleteHook as M, AfterReadHook as N, ApiKeyContext as O, PaginatedDocs as P, ApiKeyGlobalOperation as Q, ResolvedConfig as R, ApiKeyOperation as S, ApiKeyPermissions as T, UncountedDocs as U, ArrayField as V, Where as W, AuditChange as X, AuditConfig as Y, AuditEntry as Z, CollectionSlug as a, FieldsInput as a$, AuditVerification as a0, AuthConfig as a2, AuthProvider as a3, AuthProviderProfile as a4, AuthProviderRequest as a5, AuthUser as a6, BackupsConfig as a7, BeforeChangeHook as a8, BeforeDeleteHook as a9, DatabaseAdapter as aA, DatabaseInitArgs as aB, DatabaseTransfer as aC, DateField as aD, DepthOptions as aE, DirectUpload as aF, DiskStorageOptions as aG, DocumentOf as aH, EmailAdapter as aJ, EmailField as aK, EmailMessage as aL, Endpoint as aM, EndpointRequest as aN, ErrorCode as aO, FieldAccess as aQ, FieldAccessFn as aR, FieldAdmin as aS, FieldRule as aT, FieldType as aU, FieldTypeBase as aV, FieldTypeDefinition as aW, FieldTypeValidateContext as aX, FieldValidate as aY, FieldValidateContext as aZ, FieldValue as a_, BeforeValidateHook as aa, Block as ab, BlocksField as ac, BooleanField as ad, BuiltinApiKeysCollection as ae, BuiltinMediaCollection as af, BuiltinMediaFoldersCollection as ag, BuiltinUsersCollection as ah, BundleHints as ai, CliCommand as aj, CollectionAccess as ak, CollectionAdmin as al, CollectionCalendarAdmin as am, CollectionConfig as an, CollectionHooks as ao, CollectionListAdmin as ap, CollectionSearch as aq, ConfigIssue as as, ContainerAdmin as at, CreateInput as au, CreateMigrationResult as av, CustomField as aw, CustomFieldTypes as ax, DashboardWidget as ay, Database as az, CollectionDocument as b, RoutesConfig as b$, FieldsValue as b0, FilterOptionsArgs as b1, FindAfterArgs as b2, FindArgs as b3, FindOptions as b4, FolderLevel as b5, FolderPermissions as b6, GlobalAccess as b8, GlobalConfig as b9, OnRequest as bA, OnRequestArgs as bB, Operation as bC, OutboxRow as bD, PasswordEmail as bF, PasswordEmailArgs as bG, PasswordEmailFn as bH, PasswordLinkOptions as bI, Plugin as bK, PluginInfo as bL, PluginTypes as bM, PreviewTarget as bN, PreviewURL as bO, RawDocument as bR, ReadOptions as bS, RelationshipField as bT, RequestContext as bU, RichTextDocument as bV, RichTextField as bW, Role as bX, RoleOperation as bY, RolePermissions as bZ, GlobalHooks as ba, GlobalInput as bb, GroupField as bc, ImageSize as bd, IncrementOptions as be, InferCollection as bf, InferGlobal as bg, JobConfig as bh, JsonField as bi, KeysetPage as bj, Label as bk, LayoutNode as bl, LivePreview as bm, LocaleOf as bn, LogDetails as bo, Logger as bp, LoginArgs as bq, LogsConfig as br, MediaDocument as bs, MediaSize as bt, MembersConfig as bu, MembersSignup as bv, MigrationInfo as bw, NavGroup as bx, NumberField as bz, CursorDocs as c, ScheduledPublish as c0, SchemaMode as c2, SearchAdapter as c3, SearchEntry as c4, SearchHits as c5, SearchOptions as c6, SearchQuery as c7, SearchResults as c8, SearchSetting as c9, UserIdentity as cA, Version as cC, VersionSummary as cD, VersionsConfig as cE, WebhookConfig as cG, WebhookEvent as cH, WhereOperators as cI, SelectField as ca, SelectOption as cb, Session as cc, SidebarPanel as cd, SignupArgs as ce, SignupResult as cf, SlugField as ch, SlugOf as ci, SsoOutcome as cj, SsoProviderRef as ck, StorageAdapter as cl, StoredFile as cm, TelemetryConfig as co, TextField as cp, TextareaField as cq, TypedPlugin as cs, UiField as ct, UpdateInput as cv, UpdateOptions as cw, UploadConfig as cx, UploadField as cy, UploadFromURLConfig as cz, GlobalDocument as d, Secrets as da, FieldError as e, FilterOptions as f, CreateEasyCMSOptions as g, EmailSetting as h, WebhookPayload as i, Access as k, AccessArgs as l, AccessOptions as m, AdminBrand as n, AdminCommand as o, AdminComponent as p, AdminConfig as q, AdminFieldArgs as r, AdminIcon as s, AdminLocale as t, AdminPage as u, AdminRole as v, AdminRoleChange as w, AdminRoleField as x, AdminRoleTarget as y, AdminRoleView as z };
3903
+ export { ADAPTER_API_VERSION as A, EasyCMS as E, AuditLog as _, Auth as a1, EasyCMSError as aJ, FIELD_TYPES as aQ, ConfigError as as, ForbiddenError as b8, PLUGIN_API_VERSION as bF, PayloadTooLargeError as bK, QueryError as bQ, ROLE_OPERATIONS as bR, NotFoundError as bz, jsonLogger as c$, Roles as c1, SchemaError as c4, ValidationError as cE, WEBHOOK_EVENTS as cI, anyone as cM, consoleEmail as cN, consoleLogger as cO, createEasyCMS as cP, defineConfig as cQ, defineFieldType as cR, definePlugin as cS, diskStorage as cT, hasRows as cU, isAdmin as cV, isHasMany as cW, isPolymorphic as cX, isSignedIn as cY, isStaff as cZ, isSystemAdmin as c_, SingleSignOn as cj, TRIGRAM_LIKE_MIN as cq, TooManyRequestsError as cu, UnauthorizedError as cx, memorySearch as d0, mimeAllowedBy as d1, polymorphicValue as d2, relationTargets as d3, rowFields as d4, silentLogger as d5, uniqueWithinOf as d6, API_KEYS as d7, API_KEY_GLOBAL_OPERATIONS as d8, API_KEY_OPERATIONS as d9, DEFAULT_MAX_VERSIONS as da, DEFAULT_PASSWORD_EMAILS as db, FIELD_TYPE_BASES as dc, FOLDER_LEVELS as dd, MediaFolders as de, SSO_COOKIE as df, codeOfStatus as dh, isJsonLogger as di, keyAllows as dj, logFormat as dk, secretsOf as dl, ADMIN_ICONS as j };
3904
+ export type { AuditPage as $, AdminRoles as B, Config as C, AdminSso as D, Field as F, GlobalSlug as G, AdminSwitcher as H, ID as I, AdminViewAccess as J, AfterChangeHook as K, LocalizationConfig as L, AfterDeleteHook as M, AfterReadHook as N, ApiKeyContext as O, PaginatedDocs as P, ApiKeyGlobalOperation as Q, ResolvedConfig as R, ApiKeyOperation as S, ApiKeyPermissions as T, UncountedDocs as U, ArrayField as V, Where as W, AuditChange as X, AuditConfig as Y, AuditEntry as Z, CollectionSlug as a, FieldValue as a$, AuditVerification as a0, AuthConfig as a2, AuthProvider as a3, AuthProviderProfile as a4, AuthProviderRequest as a5, AuthUser as a6, BackupsConfig as a7, BeforeChangeHook as a8, BeforeDeleteHook as a9, Database as aA, DatabaseAdapter as aB, DatabaseInitArgs as aC, DatabaseTransfer as aD, DateField as aE, DepthOptions as aF, DirectUpload as aG, DiskStorageOptions as aH, DocumentOf as aI, EmailAdapter as aK, EmailField as aL, EmailMessage as aM, Endpoint as aN, EndpointRequest as aO, ErrorCode as aP, FieldAccess as aR, FieldAccessFn as aS, FieldAdmin as aT, FieldRule as aU, FieldType as aV, FieldTypeBase as aW, FieldTypeDefinition as aX, FieldTypeValidateContext as aY, FieldValidate as aZ, FieldValidateContext as a_, BeforeValidateHook as aa, Block as ab, BlockSummaryArgs as ac, BlocksField as ad, BooleanField as ae, BuiltinApiKeysCollection as af, BuiltinMediaCollection as ag, BuiltinMediaFoldersCollection as ah, BuiltinUsersCollection as ai, BundleHints as aj, CliCommand as ak, CollectionAccess as al, CollectionAdmin as am, CollectionCalendarAdmin as an, CollectionConfig as ao, CollectionHooks as ap, CollectionListAdmin as aq, CollectionSearch as ar, ConfigIssue as at, ContainerAdmin as au, CreateInput as av, CreateMigrationResult as aw, CustomField as ax, CustomFieldTypes as ay, DashboardWidget as az, CollectionDocument as b, RoleOperation as b$, FieldsInput as b0, FieldsValue as b1, FilterOptionsArgs as b2, FindAfterArgs as b3, FindArgs as b4, FindOptions as b5, FolderLevel as b6, FolderPermissions as b7, GlobalAccess as b9, NumberField as bA, OnRequest as bB, OnRequestArgs as bC, Operation as bD, OutboxRow as bE, PasswordEmail as bG, PasswordEmailArgs as bH, PasswordEmailFn as bI, PasswordLinkOptions as bJ, Plugin as bL, PluginInfo as bM, PluginTypes as bN, PreviewTarget as bO, PreviewURL as bP, RawDocument as bS, ReadOptions as bT, RelationshipField as bU, RequestContext as bV, RichTextBlock as bW, RichTextBlocks as bX, RichTextDocument as bY, RichTextField as bZ, Role as b_, GlobalConfig as ba, GlobalHooks as bb, GlobalInput as bc, GroupField as bd, ImageSize as be, IncrementOptions as bf, InferCollection as bg, InferGlobal as bh, JobConfig as bi, JsonField as bj, KeysetPage as bk, Label as bl, LayoutNode as bm, LivePreview as bn, LocaleOf as bo, LogDetails as bp, Logger as bq, LoginArgs as br, LogsConfig as bs, MediaDocument as bt, MediaSize as bu, MembersConfig as bv, MembersSignup as bw, MigrationInfo as bx, NavGroup as by, CursorDocs as c, RolePermissions as c0, RoutesConfig as c2, ScheduledPublish as c3, SchemaMode as c5, SearchAdapter as c6, SearchEntry as c7, SearchHits as c8, SearchOptions as c9, UploadConfig as cA, UploadField as cB, UploadFromURLConfig as cC, UserIdentity as cD, Version as cF, VersionSummary as cG, VersionsConfig as cH, WebhookConfig as cJ, WebhookEvent as cK, WhereOperators as cL, SearchQuery as ca, SearchResults as cb, SearchSetting as cc, SelectField as cd, SelectOption as ce, Session as cf, SidebarPanel as cg, SignupArgs as ch, SignupResult as ci, SlugField as ck, SlugOf as cl, SsoOutcome as cm, SsoProviderRef as cn, StorageAdapter as co, StoredFile as cp, TelemetryConfig as cr, TextField as cs, TextareaField as ct, TypedPlugin as cv, UiField as cw, UpdateInput as cy, UpdateOptions as cz, GlobalDocument as d, Secrets as dg, FieldError as e, FilterOptions as f, CreateEasyCMSOptions as g, EmailSetting as h, WebhookPayload as i, Access as k, AccessArgs as l, AccessOptions as m, AdminBrand as n, AdminCommand as o, AdminComponent as p, AdminConfig as q, AdminFieldArgs as r, AdminIcon as s, AdminLocale as t, AdminPage as u, AdminRole as v, AdminRoleChange as w, AdminRoleField as x, AdminRoleTarget as y, AdminRoleView as z };
@@ -1,4 +1,4 @@
1
- import { I as ID, C as Config, E as EasyCMS, L as LocalizationConfig, F as Field, e as FieldError, f as FilterOptions, g as CreateEasyCMSOptions, R as ResolvedConfig } from './config-BLNPAco2.js';
1
+ import { I as ID, C as Config, E as EasyCMS, L as LocalizationConfig, F as Field, e as FieldError, f as FilterOptions, g as CreateEasyCMSOptions, R as ResolvedConfig } from './config-DGATdBdP.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, h as EmailSetting, i as WebhookPayload } from './config-BLNPAco2.js';
2
- export { A as ADAPTER_API_VERSION, j as ADMIN_ICONS, k as Access, l as AccessArgs, m as AccessOptions, n as AdminBrand, o as AdminCommand, p as AdminComponent, q as AdminConfig, r as AdminFieldArgs, s as AdminIcon, t as AdminLocale, u as AdminPage, v as AdminRole, w as AdminRoleChange, x as AdminRoleField, y as AdminRoleTarget, z as AdminRoleView, B as AdminRoles, D as AdminSso, H as AdminSwitcher, J as AdminViewAccess, K as AfterChangeHook, M as AfterDeleteHook, N as AfterReadHook, O as ApiKeyContext, Q as ApiKeyGlobalOperation, S as ApiKeyOperation, T as ApiKeyPermissions, V as ArrayField, X as AuditChange, Y as AuditConfig, Z as AuditEntry, _ as AuditLog, $ as AuditPage, a0 as AuditVerification, a1 as Auth, a2 as AuthConfig, a3 as AuthProvider, a4 as AuthProviderProfile, a5 as AuthProviderRequest, a6 as AuthUser, a7 as BackupsConfig, a8 as BeforeChangeHook, a9 as BeforeDeleteHook, aa as BeforeValidateHook, ab as Block, ac as BlocksField, ad as BooleanField, ae as BuiltinApiKeysCollection, af as BuiltinMediaCollection, ag as BuiltinMediaFoldersCollection, ah as BuiltinUsersCollection, ai as BundleHints, aj as CliCommand, ak as CollectionAccess, al as CollectionAdmin, am as CollectionCalendarAdmin, an as CollectionConfig, b as CollectionDocument, ao as CollectionHooks, ap as CollectionListAdmin, aq as CollectionSearch, a as CollectionSlug, C as Config, ar as ConfigError, as as ConfigIssue, at as ContainerAdmin, g as CreateEasyCMSOptions, au as CreateInput, av as CreateMigrationResult, c as CursorDocs, aw as CustomField, ax as CustomFieldTypes, ay as DashboardWidget, az as Database, aA as DatabaseAdapter, aB as DatabaseInitArgs, aC as DatabaseTransfer, aD as DateField, aE as DepthOptions, aF as DirectUpload, aG as DiskStorageOptions, aH as DocumentOf, E as EasyCMS, aI as EasyCMSError, aJ as EmailAdapter, aK as EmailField, aL as EmailMessage, aM as Endpoint, aN as EndpointRequest, aO as ErrorCode, aP as FIELD_TYPES, F as Field, aQ as FieldAccess, aR as FieldAccessFn, aS as FieldAdmin, e as FieldError, aT as FieldRule, aU as FieldType, aV as FieldTypeBase, aW as FieldTypeDefinition, aX as FieldTypeValidateContext, aY as FieldValidate, aZ as FieldValidateContext, a_ as FieldValue, a$ as FieldsInput, b0 as FieldsValue, f as FilterOptions, b1 as FilterOptionsArgs, b2 as FindAfterArgs, b3 as FindArgs, b4 as FindOptions, b5 as FolderLevel, b6 as FolderPermissions, b7 as ForbiddenError, b8 as GlobalAccess, b9 as GlobalConfig, d as GlobalDocument, ba as GlobalHooks, bb as GlobalInput, G as GlobalSlug, bc as GroupField, bd as ImageSize, be as IncrementOptions, bf as InferCollection, bg as InferGlobal, bh as JobConfig, bi as JsonField, bj as KeysetPage, bk as Label, bl as LayoutNode, bm as LivePreview, bn as LocaleOf, L as LocalizationConfig, bo as LogDetails, bp as Logger, bq as LoginArgs, br as LogsConfig, bs as MediaDocument, bt as MediaSize, bu as MembersConfig, bv as MembersSignup, bw as MigrationInfo, bx as NavGroup, by as NotFoundError, bz as NumberField, bA as OnRequest, bB as OnRequestArgs, bC as Operation, bD as OutboxRow, bE as PLUGIN_API_VERSION, bF as PasswordEmail, bG as PasswordEmailArgs, bH as PasswordEmailFn, bI as PasswordLinkOptions, bJ as PayloadTooLargeError, bK as Plugin, bL as PluginInfo, bM as PluginTypes, bN as PreviewTarget, bO as PreviewURL, bP as QueryError, bQ as ROLE_OPERATIONS, bR as RawDocument, bS as ReadOptions, bT as RelationshipField, bU as RequestContext, R as ResolvedConfig, bV as RichTextDocument, bW as RichTextField, bX as Role, bY as RoleOperation, bZ as RolePermissions, b_ as Roles, b$ as RoutesConfig, c0 as ScheduledPublish, c1 as SchemaError, c2 as SchemaMode, c3 as SearchAdapter, c4 as SearchEntry, c5 as SearchHits, c6 as SearchOptions, c7 as SearchQuery, c8 as SearchResults, c9 as SearchSetting, ca as SelectField, cb as SelectOption, cc as Session, cd as SidebarPanel, ce as SignupArgs, cf as SignupResult, cg as SingleSignOn, ch as SlugField, ci as SlugOf, cj as SsoOutcome, ck as SsoProviderRef, cl as StorageAdapter, cm as StoredFile, cn as TRIGRAM_LIKE_MIN, co as TelemetryConfig, cp as TextField, cq as TextareaField, cr as TooManyRequestsError, cs as TypedPlugin, ct as UiField, cu as UnauthorizedError, U as UncountedDocs, cv as UpdateInput, cw as UpdateOptions, cx as UploadConfig, cy as UploadField, cz as UploadFromURLConfig, cA as UserIdentity, cB as ValidationError, cC as Version, cD as VersionSummary, cE as VersionsConfig, cF as WEBHOOK_EVENTS, cG as WebhookConfig, cH as WebhookEvent, W 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-BLNPAco2.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-B7pTgG5F.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-d7cT0k4l.js';
1
+ import { P as PaginatedDocs, I as ID, h as EmailSetting, i as WebhookPayload } from './config-DGATdBdP.js';
2
+ export { A as ADAPTER_API_VERSION, j as ADMIN_ICONS, k as Access, l as AccessArgs, m as AccessOptions, n as AdminBrand, o as AdminCommand, p as AdminComponent, q as AdminConfig, r as AdminFieldArgs, s as AdminIcon, t as AdminLocale, u as AdminPage, v as AdminRole, w as AdminRoleChange, x as AdminRoleField, y as AdminRoleTarget, z as AdminRoleView, B as AdminRoles, D as AdminSso, H as AdminSwitcher, J as AdminViewAccess, K as AfterChangeHook, M as AfterDeleteHook, N as AfterReadHook, O as ApiKeyContext, Q as ApiKeyGlobalOperation, S as ApiKeyOperation, T as ApiKeyPermissions, V as ArrayField, X as AuditChange, Y as AuditConfig, Z as AuditEntry, _ as AuditLog, $ as AuditPage, a0 as AuditVerification, a1 as Auth, a2 as AuthConfig, a3 as AuthProvider, a4 as AuthProviderProfile, a5 as AuthProviderRequest, a6 as AuthUser, a7 as BackupsConfig, a8 as BeforeChangeHook, a9 as BeforeDeleteHook, aa as BeforeValidateHook, ab as Block, ac as BlockSummaryArgs, ad as BlocksField, ae as BooleanField, af as BuiltinApiKeysCollection, ag as BuiltinMediaCollection, ah as BuiltinMediaFoldersCollection, ai as BuiltinUsersCollection, aj as BundleHints, ak as CliCommand, al as CollectionAccess, am as CollectionAdmin, an as CollectionCalendarAdmin, ao as CollectionConfig, b as CollectionDocument, ap as CollectionHooks, aq as CollectionListAdmin, ar as CollectionSearch, a as CollectionSlug, C as Config, as as ConfigError, at as ConfigIssue, au as ContainerAdmin, g as CreateEasyCMSOptions, av as CreateInput, aw as CreateMigrationResult, c as CursorDocs, ax as CustomField, ay as CustomFieldTypes, az as DashboardWidget, aA as Database, aB as DatabaseAdapter, aC as DatabaseInitArgs, aD as DatabaseTransfer, aE as DateField, aF as DepthOptions, aG as DirectUpload, aH as DiskStorageOptions, aI as DocumentOf, E as EasyCMS, aJ as EasyCMSError, aK as EmailAdapter, aL as EmailField, aM as EmailMessage, aN as Endpoint, aO as EndpointRequest, aP as ErrorCode, aQ as FIELD_TYPES, F as Field, aR as FieldAccess, aS as FieldAccessFn, aT as FieldAdmin, e as FieldError, aU as FieldRule, aV as FieldType, aW as FieldTypeBase, aX as FieldTypeDefinition, aY as FieldTypeValidateContext, aZ as FieldValidate, a_ as FieldValidateContext, a$ as FieldValue, b0 as FieldsInput, b1 as FieldsValue, f as FilterOptions, b2 as FilterOptionsArgs, b3 as FindAfterArgs, b4 as FindArgs, b5 as FindOptions, b6 as FolderLevel, b7 as FolderPermissions, b8 as ForbiddenError, b9 as GlobalAccess, ba as GlobalConfig, d as GlobalDocument, bb as GlobalHooks, bc as GlobalInput, G as GlobalSlug, bd as GroupField, be as ImageSize, bf as IncrementOptions, bg as InferCollection, bh as InferGlobal, bi as JobConfig, bj as JsonField, bk as KeysetPage, bl as Label, bm as LayoutNode, bn as LivePreview, bo as LocaleOf, L as LocalizationConfig, bp as LogDetails, bq as Logger, br as LoginArgs, bs as LogsConfig, bt as MediaDocument, bu as MediaSize, bv as MembersConfig, bw as MembersSignup, bx as MigrationInfo, by as NavGroup, bz as NotFoundError, bA as NumberField, bB as OnRequest, bC as OnRequestArgs, bD as Operation, bE as OutboxRow, bF as PLUGIN_API_VERSION, bG as PasswordEmail, bH as PasswordEmailArgs, bI as PasswordEmailFn, bJ as PasswordLinkOptions, bK as PayloadTooLargeError, bL as Plugin, bM as PluginInfo, bN as PluginTypes, bO as PreviewTarget, bP as PreviewURL, bQ as QueryError, bR as ROLE_OPERATIONS, bS as RawDocument, bT as ReadOptions, bU as RelationshipField, bV as RequestContext, R as ResolvedConfig, bW as RichTextBlock, bX as RichTextBlocks, bY as RichTextDocument, bZ as RichTextField, b_ as Role, b$ as RoleOperation, c0 as RolePermissions, c1 as Roles, c2 as RoutesConfig, c3 as ScheduledPublish, c4 as SchemaError, c5 as SchemaMode, c6 as SearchAdapter, c7 as SearchEntry, c8 as SearchHits, c9 as SearchOptions, ca as SearchQuery, cb as SearchResults, cc as SearchSetting, cd as SelectField, ce as SelectOption, cf as Session, cg as SidebarPanel, ch as SignupArgs, ci as SignupResult, cj as SingleSignOn, ck as SlugField, cl as SlugOf, cm as SsoOutcome, cn as SsoProviderRef, co as StorageAdapter, cp as StoredFile, cq as TRIGRAM_LIKE_MIN, cr as TelemetryConfig, cs as TextField, ct as TextareaField, cu as TooManyRequestsError, cv as TypedPlugin, cw as UiField, cx as UnauthorizedError, U as UncountedDocs, cy as UpdateInput, cz as UpdateOptions, cA as UploadConfig, cB as UploadField, cC as UploadFromURLConfig, cD as UserIdentity, cE as ValidationError, cF as Version, cG as VersionSummary, cH as VersionsConfig, cI as WEBHOOK_EVENTS, cJ as WebhookConfig, cK as WebhookEvent, W as Where, cL as WhereOperators, cM as anyone, cN as consoleEmail, cO as consoleLogger, cP as createEasyCMS, cQ as defineConfig, cR as defineFieldType, cS as definePlugin, cT as diskStorage, cU as hasRows, cV as isAdmin, cW as isHasMany, cX as isPolymorphic, cY as isSignedIn, cZ as isStaff, c_ as isSystemAdmin, c$ as jsonLogger, d0 as memorySearch, d1 as mimeAllowedBy, d2 as polymorphicValue, d3 as relationTargets, d4 as rowFields, d5 as silentLogger, d6 as uniqueWithinOf } from './config-DGATdBdP.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-B6lp3qVf.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-DsA868nd.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-QRHRDJN5.js";
31
+ } from "./chunk-JTBPGZMF.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-SYUQX2G6.js";
56
+ } from "./chunk-X4SI2QB7.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 { 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, cn 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-BLNPAco2.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-d7cT0k4l.js';
2
+ export { d7 as API_KEYS, d8 as API_KEY_GLOBAL_OPERATIONS, d9 as API_KEY_OPERATIONS, da as DEFAULT_MAX_VERSIONS, db as DEFAULT_PASSWORD_EMAILS, dc as FIELD_TYPE_BASES, dd as FOLDER_LEVELS, de as MediaFolders, df as SSO_COOKIE, dg as Secrets, cq as TRIGRAM_LIKE_MIN, dh as codeOfStatus, cU as hasRows, cW as isHasMany, di as isJsonLogger, cX as isPolymorphic, dj as keyAllows, dk as logFormat, d1 as mimeAllowedBy, d2 as polymorphicValue, d3 as relationTargets, d4 as rowFields, dl as secretsOf, d6 as uniqueWithinOf } from './config-DGATdBdP.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-DsA868nd.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-QRHRDJN5.js";
61
+ } from "./chunk-JTBPGZMF.js";
62
62
  import {
63
63
  conditionIssues
64
64
  } from "./chunk-U266LV4M.js";
@@ -72,12 +72,15 @@ import {
72
72
  hasRows,
73
73
  imageDimensions,
74
74
  isHasMany,
75
+ isPolymorphic,
75
76
  mimeAllowed,
76
77
  mimeAllowedBy,
78
+ polymorphicValue,
79
+ relationTargets,
77
80
  rowFields,
78
81
  sniffMimeType,
79
82
  uniqueWithinOf
80
- } from "./chunk-SYUQX2G6.js";
83
+ } from "./chunk-X4SI2QB7.js";
81
84
 
82
85
  // src/typegen.ts
83
86
  var pascal = (name) => name.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
@@ -88,6 +91,7 @@ function singularize(word) {
88
91
  return word.replace(/s$/i, "");
89
92
  }
90
93
  var quote = (value) => JSON.stringify(value);
94
+ var idOr = (type) => type ? `ID | ${type}` : "ID";
91
95
  var key = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : quote(name);
92
96
  function names(config) {
93
97
  const taken = /* @__PURE__ */ new Set(["ID", "RichTextDocument", "MediaSize", "Collections", "Globals"]);
@@ -137,8 +141,9 @@ function fieldType(field, n, indent) {
137
141
  return field.hasMany ? `(${one})[]` : one;
138
142
  }
139
143
  case "relationship": {
140
- const target = n.collections.get(field.to);
141
- const one = target ? `ID | ${target}` : "ID";
144
+ const one = typeof field.to === "string" ? idOr(n.collections.get(field.to)) : field.to.map(
145
+ (slug) => `{ relationTo: ${quote(slug)}; value: ${idOr(n.collections.get(slug))} }`
146
+ ).join(" | ");
142
147
  return field.hasMany ? `(${one})[]` : one;
143
148
  }
144
149
  case "group":
@@ -229,6 +234,30 @@ function generateTypes(config) {
229
234
  for (const global of config.globals) {
230
235
  parts.push("", globalInterface(global, n.globals.get(global.slug), n));
231
236
  }
237
+ const owners = [
238
+ ...config.collections.map((c) => ({ fields: c.fields, name: n.collections.get(c.slug) })),
239
+ ...config.globals.map((g) => ({ fields: g.fields, name: n.globals.get(g.slug) }))
240
+ ];
241
+ for (const { fields, name } of owners) {
242
+ if (!name) continue;
243
+ for (const field of fields) {
244
+ if (field.type !== "richText" || !field.blocks?.length) continue;
245
+ const kinds = field.blocks.map(
246
+ (block) => `{
247
+ blockType: ${quote(block.slug)}
248
+ id: string
249
+ fields: {
250
+ ${fieldLines(block.fields, n, " ").join("\n")}
251
+ }
252
+ }`
253
+ );
254
+ parts.push(
255
+ "",
256
+ `/** A block in \`${name}.${field.name}\`: \`{ type: 'block', attrs }\` in the document. */`,
257
+ `export type ${name}${pascal(field.name)}Block = ${kinds.join(" | ")}`
258
+ );
259
+ }
260
+ }
232
261
  const map = (entries) => [...entries].map(([slug, name]) => ` ${key(slug)}: ${name}`).join("\n");
233
262
  parts.push(
234
263
  "",
@@ -303,6 +332,7 @@ export {
303
332
  isEmail,
304
333
  isHasMany,
305
334
  isJsonLogger,
335
+ isPolymorphic,
306
336
  keyAllows,
307
337
  logFormat,
308
338
  mergeForUpdate,
@@ -310,9 +340,11 @@ export {
310
340
  mimeAllowedBy,
311
341
  parseId,
312
342
  platformClientIp,
343
+ polymorphicValue,
313
344
  populate,
314
345
  rateKeys,
315
346
  readCookie,
347
+ relationTargets,
316
348
  resolveAdminModule,
317
349
  rowFields,
318
350
  secretsOf,
@@ -1,4 +1,4 @@
1
- import { bk as Label, F as Field, I as ID, t as AdminLocale, H as AdminSwitcher, ck as SsoProviderRef } from './config-BLNPAco2.js';
1
+ import { bl as Label, F as Field, I as ID, t as AdminLocale, H as AdminSwitcher, cn as SsoProviderRef } from './config-DGATdBdP.js';
2
2
  import { FieldCondition } from './conditions.js';
3
3
 
4
4
  /** A Web Component from an admin module, with its `props` (plain JSON). */
@@ -67,7 +67,8 @@ interface AdminField {
67
67
  value: string;
68
68
  }[];
69
69
  hasMany?: boolean;
70
- to?: string;
70
+ /** Several for a relationship to several collections (#327). */
71
+ to?: string | string[];
71
72
  /** An upload's allowed file types, e.g. `image/*`. */
72
73
  mimeTypes?: string[];
73
74
  /** An upload's media folder (`folder`): where its picker opens; `only`: nowhere else. */
@@ -89,6 +90,11 @@ interface AdminField {
89
90
  plural?: Label;
90
91
  };
91
92
  fields: AdminField[];
93
+ /**
94
+ * Rich text blocks (#328): the card's text comes from the server (`'server'`, the block's
95
+ * `admin.summary` function) or a custom element (its tag).
96
+ */
97
+ summary?: string;
92
98
  }[];
93
99
  /** The current user may not change this field. */
94
100
  readOnly?: boolean;
package/dist/plugin.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { j as ADMIN_ICONS, p as AdminComponent, s as AdminIcon, a6 as AuthUser, aj as CliCommand, ak as CollectionAccess, an as CollectionConfig, C as Config, ar as ConfigError, ay as DashboardWidget, aI as EasyCMSError, aM as Endpoint, aN as EndpointRequest, aO as ErrorCode, F as Field, aQ as FieldAccess, e as FieldError, aV as FieldTypeBase, aW as FieldTypeDefinition, aX as FieldTypeValidateContext, b7 as ForbiddenError, b9 as GlobalConfig, I as ID, bh as JobConfig, bk as Label, bx as NavGroup, by as NotFoundError, bA as OnRequest, bB as OnRequestArgs, bE as PLUGIN_API_VERSION, bJ as PayloadTooLargeError, bK as Plugin, bL as PluginInfo, bM as PluginTypes, bP as QueryError, bU as RequestContext, cr as TooManyRequestsError, cs as TypedPlugin, cu as UnauthorizedError, cB as ValidationError, W as Where, cO as defineFieldType, cP as definePlugin } from './config-BLNPAco2.js';
2
- export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-B7pTgG5F.js';
1
+ export { j as ADMIN_ICONS, p as AdminComponent, s as AdminIcon, a6 as AuthUser, ak as CliCommand, al as CollectionAccess, ao as CollectionConfig, C as Config, as as ConfigError, az as DashboardWidget, aJ as EasyCMSError, aN as Endpoint, aO as EndpointRequest, aP as ErrorCode, F as Field, aR as FieldAccess, e as FieldError, aW as FieldTypeBase, aX as FieldTypeDefinition, aY as FieldTypeValidateContext, b8 as ForbiddenError, ba as GlobalConfig, I as ID, bi as JobConfig, bl as Label, by as NavGroup, bz as NotFoundError, bB as OnRequest, bC as OnRequestArgs, bF as PLUGIN_API_VERSION, bK as PayloadTooLargeError, bL as Plugin, bM as PluginInfo, bN as PluginTypes, bQ as QueryError, bV as RequestContext, cu as TooManyRequestsError, cv as TypedPlugin, cx as UnauthorizedError, cE as ValidationError, W as Where, cR as defineFieldType, cS as definePlugin } from './config-DGATdBdP.js';
2
+ export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-B6lp3qVf.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-SYUQX2G6.js";
19
+ } from "./chunk-X4SI2QB7.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.28.0",
3
+ "version": "1.30.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",