@cmssy/core 12.4.0 → 12.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -486,11 +486,11 @@ var fields = {
486
486
  table: control("table"),
487
487
  json: control("json"),
488
488
  form: control("form"),
489
- pageSelector: control("pageSelector"),
490
489
  select: choice("select"),
491
490
  radio: choice("radio"),
492
491
  multiselect: (opts) => build("multiselect", opts),
493
492
  media: (opts = {}) => build("media", opts),
493
+ pageSelector: (opts = {}) => build("pageSelector", opts),
494
494
  repeater: (opts) => build("repeater", opts),
495
495
  relation: (opts) => {
496
496
  const { model, mode, ...rest } = opts;
package/dist/index.d.cts CHANGED
@@ -2,9 +2,14 @@ export { A as AppToEditorMessage, B as BoundsMessage, C as ClickMessage, a as Cm
2
2
  export { C as CMSSY_LOCALE_HEADER, G as GraphqlRequestOptions, g as graphqlRequest, l as localizeHref, r as resolveCmssyLocale } from './locale-BS61R-fF.cjs';
3
3
  export { C as CmssyRequestError, D as DEFAULT_CMSSY_API_URL, F as FetchLike, a as FetchLikeResponse, b as FetchPageOptions, R as RetryPolicy } from './content-client-BPpRfq0h.cjs';
4
4
  import * as _cmssy_types from '@cmssy/types';
5
- import { FieldOptions, TypedField, ResolvedMediaValue, BlockPropsSchema, InferBlockContent, RelationMode, CmssyModelRecord, ResolvedMedia, VerifyCmssyWebhookOptions, CmssyWebhookEvent } from '@cmssy/types';
5
+ import { FieldOptions, TypedField, ResolvedMediaValue, PageRef, BlockPropsSchema, InferBlockContent, RelationMode, CmssyModelRecord, ResolvedMedia, VerifyCmssyWebhookOptions, CmssyWebhookEvent } from '@cmssy/types';
6
6
  export { BlockMeta, BlockPropsSchema, BlockRect, BlockSchema, BuildBlockContextExtra, CmssyBlockAuthContext, CmssyBlockContext, CmssyBlockMember, CmssyBlockPage, CmssyBlockWorkspace, CmssyBranding, CmssyClientConfig, CmssyFormDefinition, CmssyFormField, CmssyFormSettings, CmssyFormSubmitResponse, CmssyLayoutGroup, CmssyLayoutSettings, CmssyLocaleContext, CmssyLocalizedValue, CmssyModelDefinition, CmssyModelRecord, CmssyPageData, CmssyPageMeta, CmssyPageSummary, CmssyRecordList, CmssySiteConfig, CmssyWebhookEvent, CmssyWebhookOrder, FieldCondition, FieldConditionGroup, FieldConditionLogic, FieldControl, FieldDefinition, FieldOptions, FieldType, InferBlockContent, LayoutPosition, RawBlock, RawLayoutBlock, SubmitFormInput, TypedField, VerifyCmssyWebhookOptions, evaluateFieldConditionGroup, layoutPositionValues } from '@cmssy/types';
7
7
 
8
+ /**
9
+ * Whether the content is guaranteed to carry the field. Only `required` says
10
+ * so: a declared `defaultValue` seeds the editor and is not put in place at
11
+ * read time, so it promises nothing about the stored content (CMS-1220).
12
+ */
8
13
  type Declared<O> = O extends {
9
14
  required: true;
10
15
  } ? true : false;
@@ -22,6 +27,14 @@ type RelationValue<O> = O extends {
22
27
  } | {
23
28
  multiple: true;
24
29
  } ? CmssyModelRecord[] : CmssyModelRecord | undefined;
30
+ /**
31
+ * A page selector holds a list unless it is explicitly single. The default is
32
+ * the multiple one, which is the reading the editor already takes: it treats
33
+ * only `multiple: false` as single.
34
+ */
35
+ type PageSelectorValue<O> = O extends {
36
+ multiple: false;
37
+ } ? PageRef | undefined : PageRef[];
25
38
  interface RelationFieldOptions extends Omit<FieldOptions, "options" | "itemSchema"> {
26
39
  model: string;
27
40
  mode?: RelationMode;
@@ -45,11 +58,11 @@ declare const fields: {
45
58
  table: <const O extends FieldOptions>(opts?: O) => TypedField<_cmssy_types.TableValue, Declared<O>>;
46
59
  json: <const O extends FieldOptions>(opts?: O) => TypedField<_cmssy_types.JsonValue, Declared<O>>;
47
60
  form: <const O extends FieldOptions>(opts?: O) => TypedField<string, Declared<O>>;
48
- pageSelector: <const O extends FieldOptions>(opts?: O) => TypedField<_cmssy_types.PageRef[], Declared<O>>;
49
61
  select: <const O extends FieldOptions>(opts: O) => TypedField<OptionValue<O>, Declared<O>>;
50
62
  radio: <const O extends FieldOptions>(opts: O) => TypedField<OptionValue<O>, Declared<O>>;
51
63
  multiselect: <const O extends FieldOptions>(opts: O) => TypedField<OptionValue<O>[], Declared<O>>;
52
64
  media: <const O extends FieldOptions>(opts?: O) => TypedField<MediaValue<O>, Declared<O>>;
65
+ pageSelector: <const O extends FieldOptions>(opts?: O) => TypedField<PageSelectorValue<O>, Declared<O>>;
53
66
  repeater: <const O extends FieldOptions>(opts: O) => TypedField<RepeaterValue<O>, Declared<O>>;
54
67
  relation: <const O extends RelationFieldOptions>(opts: O) => TypedField<RelationValue<O>, Declared<O>>;
55
68
  };
package/dist/index.d.ts CHANGED
@@ -2,9 +2,14 @@ export { A as AppToEditorMessage, B as BoundsMessage, C as ClickMessage, a as Cm
2
2
  export { C as CMSSY_LOCALE_HEADER, G as GraphqlRequestOptions, g as graphqlRequest, l as localizeHref, r as resolveCmssyLocale } from './locale-DQZ80cew.js';
3
3
  export { C as CmssyRequestError, D as DEFAULT_CMSSY_API_URL, F as FetchLike, a as FetchLikeResponse, b as FetchPageOptions, R as RetryPolicy } from './content-client-BPpRfq0h.js';
4
4
  import * as _cmssy_types from '@cmssy/types';
5
- import { FieldOptions, TypedField, ResolvedMediaValue, BlockPropsSchema, InferBlockContent, RelationMode, CmssyModelRecord, ResolvedMedia, VerifyCmssyWebhookOptions, CmssyWebhookEvent } from '@cmssy/types';
5
+ import { FieldOptions, TypedField, ResolvedMediaValue, PageRef, BlockPropsSchema, InferBlockContent, RelationMode, CmssyModelRecord, ResolvedMedia, VerifyCmssyWebhookOptions, CmssyWebhookEvent } from '@cmssy/types';
6
6
  export { BlockMeta, BlockPropsSchema, BlockRect, BlockSchema, BuildBlockContextExtra, CmssyBlockAuthContext, CmssyBlockContext, CmssyBlockMember, CmssyBlockPage, CmssyBlockWorkspace, CmssyBranding, CmssyClientConfig, CmssyFormDefinition, CmssyFormField, CmssyFormSettings, CmssyFormSubmitResponse, CmssyLayoutGroup, CmssyLayoutSettings, CmssyLocaleContext, CmssyLocalizedValue, CmssyModelDefinition, CmssyModelRecord, CmssyPageData, CmssyPageMeta, CmssyPageSummary, CmssyRecordList, CmssySiteConfig, CmssyWebhookEvent, CmssyWebhookOrder, FieldCondition, FieldConditionGroup, FieldConditionLogic, FieldControl, FieldDefinition, FieldOptions, FieldType, InferBlockContent, LayoutPosition, RawBlock, RawLayoutBlock, SubmitFormInput, TypedField, VerifyCmssyWebhookOptions, evaluateFieldConditionGroup, layoutPositionValues } from '@cmssy/types';
7
7
 
8
+ /**
9
+ * Whether the content is guaranteed to carry the field. Only `required` says
10
+ * so: a declared `defaultValue` seeds the editor and is not put in place at
11
+ * read time, so it promises nothing about the stored content (CMS-1220).
12
+ */
8
13
  type Declared<O> = O extends {
9
14
  required: true;
10
15
  } ? true : false;
@@ -22,6 +27,14 @@ type RelationValue<O> = O extends {
22
27
  } | {
23
28
  multiple: true;
24
29
  } ? CmssyModelRecord[] : CmssyModelRecord | undefined;
30
+ /**
31
+ * A page selector holds a list unless it is explicitly single. The default is
32
+ * the multiple one, which is the reading the editor already takes: it treats
33
+ * only `multiple: false` as single.
34
+ */
35
+ type PageSelectorValue<O> = O extends {
36
+ multiple: false;
37
+ } ? PageRef | undefined : PageRef[];
25
38
  interface RelationFieldOptions extends Omit<FieldOptions, "options" | "itemSchema"> {
26
39
  model: string;
27
40
  mode?: RelationMode;
@@ -45,11 +58,11 @@ declare const fields: {
45
58
  table: <const O extends FieldOptions>(opts?: O) => TypedField<_cmssy_types.TableValue, Declared<O>>;
46
59
  json: <const O extends FieldOptions>(opts?: O) => TypedField<_cmssy_types.JsonValue, Declared<O>>;
47
60
  form: <const O extends FieldOptions>(opts?: O) => TypedField<string, Declared<O>>;
48
- pageSelector: <const O extends FieldOptions>(opts?: O) => TypedField<_cmssy_types.PageRef[], Declared<O>>;
49
61
  select: <const O extends FieldOptions>(opts: O) => TypedField<OptionValue<O>, Declared<O>>;
50
62
  radio: <const O extends FieldOptions>(opts: O) => TypedField<OptionValue<O>, Declared<O>>;
51
63
  multiselect: <const O extends FieldOptions>(opts: O) => TypedField<OptionValue<O>[], Declared<O>>;
52
64
  media: <const O extends FieldOptions>(opts?: O) => TypedField<MediaValue<O>, Declared<O>>;
65
+ pageSelector: <const O extends FieldOptions>(opts?: O) => TypedField<PageSelectorValue<O>, Declared<O>>;
53
66
  repeater: <const O extends FieldOptions>(opts: O) => TypedField<RepeaterValue<O>, Declared<O>>;
54
67
  relation: <const O extends RelationFieldOptions>(opts: O) => TypedField<RelationValue<O>, Declared<O>>;
55
68
  };
package/dist/index.js CHANGED
@@ -484,11 +484,11 @@ var fields = {
484
484
  table: control("table"),
485
485
  json: control("json"),
486
486
  form: control("form"),
487
- pageSelector: control("pageSelector"),
488
487
  select: choice("select"),
489
488
  radio: choice("radio"),
490
489
  multiselect: (opts) => build("multiselect", opts),
491
490
  media: (opts = {}) => build("media", opts),
491
+ pageSelector: (opts = {}) => build("pageSelector", opts),
492
492
  repeater: (opts) => build("repeater", opts),
493
493
  relation: (opts) => {
494
494
  const { model, mode, ...rest } = opts;
package/dist/internal.cjs CHANGED
@@ -659,131 +659,7 @@ function createCmssyClient(input) {
659
659
  return client;
660
660
  }
661
661
 
662
- // src/data/resolve-forms.ts
663
- function collectFormIds(blocks, locale, defaultLocale) {
664
- const ids = /* @__PURE__ */ new Set();
665
- for (const block of blocks) {
666
- const content = getBlockContentForLanguage(
667
- block.content,
668
- locale,
669
- defaultLocale
670
- );
671
- const formId = content.formId;
672
- if (typeof formId === "string" && formId.trim()) ids.add(formId);
673
- }
674
- return [...ids];
675
- }
676
- async function resolveForms(config, blocks, locale, defaultLocale, options) {
677
- const ids = collectFormIds(blocks, locale, defaultLocale);
678
- if (ids.length === 0) return {};
679
- const client = createCmssyClient(config);
680
- const entries = await Promise.all(
681
- ids.map(async (id) => {
682
- try {
683
- const data = await client.queryScoped(FORM_QUERY, { formId: id }, options);
684
- return [id, data.public.form.get];
685
- } catch (err) {
686
- if (typeof console !== "undefined") {
687
- console.warn(`[cmssy] failed to resolve form ${id}`, err);
688
- }
689
- return [id, null];
690
- }
691
- })
692
- );
693
- const forms = {};
694
- for (const [id, def] of entries) {
695
- if (def) forms[id] = def;
696
- }
697
- return forms;
698
- }
699
-
700
- // src/data/site-locales.ts
701
- var TTL_MS = 6e4;
702
- var FAILURE_TTL_MS = 5e3;
703
- var MAX_ENTRIES = 64;
704
- var cache = /* @__PURE__ */ new Map();
705
- function localesFromSiteConfig(siteConfig) {
706
- const defaultLocale = siteConfig?.defaultLanguage || "en";
707
- const enabled = siteConfig?.enabledLanguages ?? [];
708
- return {
709
- defaultLocale,
710
- locales: enabled.length > 0 ? enabled : [defaultLocale]
711
- };
712
- }
713
- async function loadSiteLocales(config, options) {
714
- const key = `${resolveApiUrl(config.apiUrl)}::${config.org}::${config.workspaceSlug}`;
715
- const cached = cache.get(key);
716
- if (cached && cached.expires > Date.now()) return cached.value;
717
- cache.delete(key);
718
- let value;
719
- try {
720
- const data = await graphqlRequest(
721
- config,
722
- SITE_CONFIG_QUERY,
723
- { workspaceSlug: config.workspaceSlug },
724
- { ...options, public: true, retry: options?.retry ?? {} },
725
- "site config"
726
- );
727
- value = localesFromSiteConfig(data.public?.siteConfig ?? null);
728
- } catch {
729
- value = null;
730
- }
731
- if (cache.size >= MAX_ENTRIES) cache.clear();
732
- cache.set(key, {
733
- value,
734
- expires: Date.now() + (value ? TTL_MS : FAILURE_TTL_MS)
735
- });
736
- return value;
737
- }
738
- async function resolveSiteLocales(config, options) {
739
- return await loadSiteLocales(config, options) ?? {
740
- defaultLocale: "en",
741
- locales: ["en"]
742
- };
743
- }
744
- function splitLocaleFromPath(path, siteLocales) {
745
- const first = path?.[0];
746
- if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
747
- return { locale: first, path: path.slice(1) };
748
- }
749
- return { locale: siteLocales.defaultLocale, path };
750
- }
751
-
752
- // src/data/localize-href.ts
753
- var PROTOCOL_OR_RELATIVE = /^([a-z][a-z0-9+.-]*:|\/\/)/i;
754
- function isExternalHref(href) {
755
- const value = href.trim();
756
- if (!value) return true;
757
- if (value.startsWith("#")) return true;
758
- return PROTOCOL_OR_RELATIVE.test(value);
759
- }
760
- function stripLeadingLocale(path, locale) {
761
- const segments = path.split("/");
762
- const first = segments[1];
763
- if (first && first !== locale.default && locale.enabled.includes(first)) {
764
- segments.splice(1, 1);
765
- const rest = segments.join("/");
766
- return rest === "" ? "/" : rest;
767
- }
768
- return path;
769
- }
770
- function addLocalePrefix(path, target, locale) {
771
- if (target === locale.default) return path;
772
- if (path === "/") return `/${target}`;
773
- return `/${target}${path}`;
774
- }
775
- function localizeHref(href, locale) {
776
- const value = href.trim();
777
- if (isExternalHref(value)) return href;
778
- const boundary = value.search(/[?#]/);
779
- const path = boundary === -1 ? value : value.slice(0, boundary);
780
- const suffix = boundary === -1 ? "" : value.slice(boundary);
781
- if (!path.startsWith("/")) return href;
782
- const bare = stripLeadingLocale(path, locale);
783
- return `${addLocalePrefix(bare, locale.current, locale)}${suffix}`;
784
- }
785
-
786
- // src/data/relation-resolver.ts
662
+ // src/data/block-content.ts
787
663
  var RECORDS_BY_IDS_QUERY = `query PublicRecordsByIds($workspaceId: String!, $ids: [String!]!, $locale: String) {
788
664
  public {
789
665
  model {
@@ -805,17 +681,54 @@ function storedIds(value) {
805
681
  }
806
682
  return [];
807
683
  }
684
+ function isRecord(value) {
685
+ return typeof value === "object" && value !== null && !Array.isArray(value);
686
+ }
687
+ var BLOCK_BUCKETS = ["content", "style", "advanced"];
688
+ function bucketOf(field) {
689
+ return field.tab === "style" || field.tab === "advanced" ? field.tab : "content";
690
+ }
691
+ function walkBlockFields(values, schema, visit, options = {}, path = []) {
692
+ for (const [key, field] of Object.entries(schema)) {
693
+ if (options.bucket && path.length === 0 && bucketOf(field) !== options.bucket) {
694
+ continue;
695
+ }
696
+ if (field.type === "repeater") {
697
+ const itemSchema = field.itemSchema;
698
+ const stored = values[key];
699
+ if (!itemSchema || !Array.isArray(stored)) continue;
700
+ const rows = options.copyRows ? stored.map((row) => isRecord(row) ? { ...row } : row) : stored;
701
+ if (options.copyRows) values[key] = rows;
702
+ rows.forEach((row, index) => {
703
+ if (isRecord(row)) {
704
+ walkBlockFields(row, itemSchema, visit, options, [
705
+ ...path,
706
+ key,
707
+ index
708
+ ]);
709
+ }
710
+ });
711
+ continue;
712
+ }
713
+ visit(values, key, field, path);
714
+ }
715
+ }
808
716
  function collectRefs(entries, schemas) {
809
717
  const refs = [];
810
718
  for (const entry of entries) {
811
719
  const schema = schemas[entry.type];
812
720
  if (!schema) continue;
813
- for (const [key, field] of Object.entries(schema)) {
814
- if (field.type !== "relation") continue;
815
- const model = relationModel(field);
816
- if (!model) continue;
817
- refs.push({ content: entry.content, key, field, model });
818
- }
721
+ walkBlockFields(
722
+ entry.content,
723
+ schema,
724
+ (holder, key, field) => {
725
+ if (field.type !== "relation") return;
726
+ const model = relationModel(field);
727
+ if (!model) return;
728
+ refs.push({ content: holder, key, field, model });
729
+ },
730
+ { copyRows: true, bucket: "content" }
731
+ );
819
732
  }
820
733
  return refs;
821
734
  }
@@ -825,31 +738,70 @@ function isResolvedRecord(value) {
825
738
  function isListShaped(field) {
826
739
  return field.relationMode === "all" || field.relationType === "hasMany" || field.multiple === true;
827
740
  }
828
- function normalizeRelationContent(content, schema, resolved) {
829
- for (const [key, field] of Object.entries(schema)) {
830
- if (field.type !== "relation") continue;
831
- const value = content[key];
832
- const fallback = resolved?.[key];
833
- if (isListShaped(field)) {
834
- if (Array.isArray(value)) {
835
- const records = value.filter(isResolvedRecord);
836
- if (records.length > 0 || value.length === 0) {
837
- content[key] = records;
838
- continue;
741
+ function toPageRef(value) {
742
+ if (typeof value === "string") {
743
+ return value ? { slug: value, displayName: {} } : null;
744
+ }
745
+ if (isRecord(value) && typeof value.slug === "string" && value.slug) {
746
+ return value;
747
+ }
748
+ return null;
749
+ }
750
+ function toPageRefs(value) {
751
+ const items = Array.isArray(value) ? value : [value];
752
+ return items.map(toPageRef).filter((ref) => ref !== null);
753
+ }
754
+ function fallbackHolder(resolved, path) {
755
+ let holder = resolved;
756
+ for (const step of path) {
757
+ if (holder === void 0 || holder === null) return void 0;
758
+ holder = holder[step];
759
+ }
760
+ return isRecord(holder) ? holder : void 0;
761
+ }
762
+ function normalizeBlockContent(content, schema, resolved) {
763
+ walkBlockFields(
764
+ content,
765
+ schema,
766
+ (holder, key, field, path) => {
767
+ if (field.type === "pageSelector") {
768
+ if (!(key in holder)) return;
769
+ const refs = toPageRefs(holder[key]);
770
+ if (field.multiple === false) {
771
+ if (refs[0]) holder[key] = refs[0];
772
+ else delete holder[key];
773
+ } else {
774
+ holder[key] = refs;
839
775
  }
776
+ return;
840
777
  }
841
- content[key] = Array.isArray(fallback) ? fallback.filter(isResolvedRecord) : [];
842
- } else if (!isResolvedRecord(value)) {
843
- if (value != null && value !== "" && isResolvedRecord(fallback)) {
844
- content[key] = fallback;
845
- } else {
846
- delete content[key];
778
+ if (field.type !== "relation") return;
779
+ const value = holder[key];
780
+ const fallback = fallbackHolder(resolved, path)?.[key];
781
+ if (isListShaped(field)) {
782
+ if (Array.isArray(value)) {
783
+ const records = value.filter(isResolvedRecord);
784
+ if (records.length > 0 || value.length === 0) {
785
+ holder[key] = records;
786
+ return;
787
+ }
788
+ }
789
+ holder[key] = Array.isArray(fallback) ? fallback.filter(isResolvedRecord) : [];
790
+ } else if (!isResolvedRecord(value)) {
791
+ if (value != null && value !== "" && isResolvedRecord(fallback)) {
792
+ holder[key] = fallback;
793
+ } else {
794
+ delete holder[key];
795
+ }
847
796
  }
848
- }
849
- }
797
+ },
798
+ { copyRows: true, bucket: "content" }
799
+ );
850
800
  }
851
801
  function collectionKey(ref) {
852
- return [ref.model, ref.field.sort ?? "", ref.field.limit ?? ""].join("\0");
802
+ return [ref.model, ref.field.sort ?? "", ref.field.limit ?? ""].join(
803
+ "\0"
804
+ );
853
805
  }
854
806
  async function resolveRelationContent(config, entries, schemas, locale, requestOptions = {}) {
855
807
  const refs = collectRefs(entries, schemas);
@@ -945,6 +897,142 @@ async function fetchCollections(client, collections, locale, requestOptions = {}
945
897
  return byKey;
946
898
  }
947
899
 
900
+ // src/data/resolve-forms.ts
901
+ function collectFormIds(blocks, schemas, locale, defaultLocale) {
902
+ const ids = /* @__PURE__ */ new Set();
903
+ for (const block of blocks) {
904
+ const schema = schemas[block.type];
905
+ if (!schema) continue;
906
+ const values = {
907
+ content: getBlockContentForLanguage(block.content, locale, defaultLocale),
908
+ style: asBucket(block.style),
909
+ advanced: asBucket(block.advanced)
910
+ };
911
+ for (const bucket of BLOCK_BUCKETS) {
912
+ walkBlockFields(
913
+ values[bucket],
914
+ schema,
915
+ (holder, key, field) => {
916
+ if (field.type !== "form") return;
917
+ const id = holder[key];
918
+ if (typeof id === "string" && id.trim()) ids.add(id);
919
+ },
920
+ { bucket }
921
+ );
922
+ }
923
+ }
924
+ return [...ids];
925
+ }
926
+ async function resolveForms(config, blocks, schemas, locale, defaultLocale, options) {
927
+ const ids = collectFormIds(blocks, schemas, locale, defaultLocale);
928
+ if (ids.length === 0) return {};
929
+ const client = createCmssyClient(config);
930
+ const entries = await Promise.all(
931
+ ids.map(async (id) => {
932
+ try {
933
+ const data = await client.queryScoped(FORM_QUERY, { formId: id }, options);
934
+ return [id, data.public.form.get];
935
+ } catch (err) {
936
+ if (typeof console !== "undefined") {
937
+ console.warn(`[cmssy] failed to resolve form ${id}`, err);
938
+ }
939
+ return [id, null];
940
+ }
941
+ })
942
+ );
943
+ const forms = {};
944
+ for (const [id, def] of entries) {
945
+ if (def) forms[id] = def;
946
+ }
947
+ return forms;
948
+ }
949
+
950
+ // src/data/site-locales.ts
951
+ var TTL_MS = 6e4;
952
+ var FAILURE_TTL_MS = 5e3;
953
+ var MAX_ENTRIES = 64;
954
+ var cache = /* @__PURE__ */ new Map();
955
+ function localesFromSiteConfig(siteConfig) {
956
+ const defaultLocale = siteConfig?.defaultLanguage || "en";
957
+ const enabled = siteConfig?.enabledLanguages ?? [];
958
+ return {
959
+ defaultLocale,
960
+ locales: enabled.length > 0 ? enabled : [defaultLocale]
961
+ };
962
+ }
963
+ async function loadSiteLocales(config, options) {
964
+ const key = `${resolveApiUrl(config.apiUrl)}::${config.org}::${config.workspaceSlug}`;
965
+ const cached = cache.get(key);
966
+ if (cached && cached.expires > Date.now()) return cached.value;
967
+ cache.delete(key);
968
+ let value;
969
+ try {
970
+ const data = await graphqlRequest(
971
+ config,
972
+ SITE_CONFIG_QUERY,
973
+ { workspaceSlug: config.workspaceSlug },
974
+ { ...options, public: true, retry: options?.retry ?? {} },
975
+ "site config"
976
+ );
977
+ value = localesFromSiteConfig(data.public?.siteConfig ?? null);
978
+ } catch {
979
+ value = null;
980
+ }
981
+ if (cache.size >= MAX_ENTRIES) cache.clear();
982
+ cache.set(key, {
983
+ value,
984
+ expires: Date.now() + (value ? TTL_MS : FAILURE_TTL_MS)
985
+ });
986
+ return value;
987
+ }
988
+ async function resolveSiteLocales(config, options) {
989
+ return await loadSiteLocales(config, options) ?? {
990
+ defaultLocale: "en",
991
+ locales: ["en"]
992
+ };
993
+ }
994
+ function splitLocaleFromPath(path, siteLocales) {
995
+ const first = path?.[0];
996
+ if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
997
+ return { locale: first, path: path.slice(1) };
998
+ }
999
+ return { locale: siteLocales.defaultLocale, path };
1000
+ }
1001
+
1002
+ // src/data/localize-href.ts
1003
+ var PROTOCOL_OR_RELATIVE = /^([a-z][a-z0-9+.-]*:|\/\/)/i;
1004
+ function isExternalHref(href) {
1005
+ const value = href.trim();
1006
+ if (!value) return true;
1007
+ if (value.startsWith("#")) return true;
1008
+ return PROTOCOL_OR_RELATIVE.test(value);
1009
+ }
1010
+ function stripLeadingLocale(path, locale) {
1011
+ const segments = path.split("/");
1012
+ const first = segments[1];
1013
+ if (first && first !== locale.default && locale.enabled.includes(first)) {
1014
+ segments.splice(1, 1);
1015
+ const rest = segments.join("/");
1016
+ return rest === "" ? "/" : rest;
1017
+ }
1018
+ return path;
1019
+ }
1020
+ function addLocalePrefix(path, target, locale) {
1021
+ if (target === locale.default) return path;
1022
+ if (path === "/") return `/${target}`;
1023
+ return `/${target}${path}`;
1024
+ }
1025
+ function localizeHref(href, locale) {
1026
+ const value = href.trim();
1027
+ if (isExternalHref(value)) return href;
1028
+ const boundary = value.search(/[?#]/);
1029
+ const path = boundary === -1 ? value : value.slice(0, boundary);
1030
+ const suffix = boundary === -1 ? "" : value.slice(boundary);
1031
+ if (!path.startsWith("/")) return href;
1032
+ const bare = stripLeadingLocale(path, locale);
1033
+ return `${addLocalePrefix(bare, locale.current, locale)}${suffix}`;
1034
+ }
1035
+
948
1036
  // src/locale.ts
949
1037
  var CMSSY_LOCALE_HEADER = "x-cmssy-locale";
950
1038
  async function localeForPathname(config, pathname) {
@@ -1172,7 +1260,7 @@ exports.localeForPath = localeForPath;
1172
1260
  exports.localeForPathname = localeForPathname;
1173
1261
  exports.localizeHref = localizeHref;
1174
1262
  exports.localizedPath = localizedPath;
1175
- exports.normalizeRelationContent = normalizeRelationContent;
1263
+ exports.normalizeBlockContent = normalizeBlockContent;
1176
1264
  exports.resolveApiUrl = resolveApiUrl;
1177
1265
  exports.resolveForms = resolveForms;
1178
1266
  exports.resolveInitialTarget = resolveInitialTarget;
@@ -1,5 +1,5 @@
1
1
  export { f as fetchLayouts, c as fetchPage, d as fetchPageById, e as fetchPageMeta, g as fetchPages, r as resolveApiUrl } from './content-client-BPpRfq0h.cjs';
2
- import { CmssyFormDefinition, BuildBlockContextExtra, CmssyBlockContext, CmssyClientConfig, CmssySiteConfig, RawBlock, FieldDefinition } from '@cmssy/types';
2
+ import { CmssyFormDefinition, BuildBlockContextExtra, CmssyBlockContext, CmssyClientConfig, CmssySiteConfig, FieldDefinition, RawBlock } from '@cmssy/types';
3
3
  export { CmssySiteLocales } from '@cmssy/types';
4
4
  import { G as GraphqlRequestOptions } from './locale-BS61R-fF.cjs';
5
5
  export { C as CMSSY_LOCALE_HEADER, a as localeForPath, b as localeForPathname, l as localizeHref, c as resolveSiteLocales, s as splitCmssyLocale, d as splitLocaleFromPath } from './locale-BS61R-fF.cjs';
@@ -22,17 +22,30 @@ declare function fetchSiteConfig(config: CmssyClientConfig, options?: GraphqlReq
22
22
  declare function resolveWorkspaceId(config: CmssyClientConfig, options?: GraphqlRequestOptions): Promise<string>;
23
23
  declare function clearWorkspaceIdCache(): void;
24
24
 
25
- declare function collectFormIds(blocks: RawBlock[], locale: string, defaultLocale: string): string[];
26
- declare function resolveForms(config: CmssyClientConfig, blocks: RawBlock[], locale: string, defaultLocale: string, options?: QueryScopedOptions): Promise<Record<string, CmssyFormDefinition>>;
27
-
28
25
  interface RelationContentEntry {
29
26
  type: string;
30
27
  content: Record<string, unknown>;
31
28
  }
32
29
  type BlockSchemaMap = Record<string, Record<string, FieldDefinition>>;
33
- declare function normalizeRelationContent(content: Record<string, unknown>, schema: Record<string, FieldDefinition>, resolved?: Record<string, unknown>): void;
30
+ /**
31
+ * Brings stored content in line with what the block's schema declares, for the
32
+ * field types whose stored shape differs from the authored one: a relation
33
+ * carries ids, a page selector always carries a list even when it holds one
34
+ * page.
35
+ */
36
+ declare function normalizeBlockContent(content: Record<string, unknown>, schema: Record<string, FieldDefinition>, resolved?: Record<string, unknown>): void;
34
37
  declare function resolveRelationContent(config: CmssyClientConfig, entries: RelationContentEntry[], schemas: BlockSchemaMap, locale?: string, requestOptions?: QueryScopedOptions): Promise<void>;
35
38
 
39
+ /**
40
+ * Finds the forms a page needs by reading the blocks' schemas, so a form field
41
+ * is found under whatever key it was declared with. Reading a fixed `formId`
42
+ * key instead meant `fields.form()` under any other name silently resolved to
43
+ * nothing, and a form field on the style or advanced tab was invisible either
44
+ * way: the editor writes those into their own bucket, not into content.
45
+ */
46
+ declare function collectFormIds(blocks: RawBlock[], schemas: BlockSchemaMap, locale: string, defaultLocale: string): string[];
47
+ declare function resolveForms(config: CmssyClientConfig, blocks: RawBlock[], schemas: BlockSchemaMap, locale: string, defaultLocale: string, options?: QueryScopedOptions): Promise<Record<string, CmssyFormDefinition>>;
48
+
36
49
  declare function cmssySecretsMatch(a: string, b: string): Promise<boolean>;
37
50
 
38
51
  interface CmssyDeliveryOperation {
@@ -41,4 +54,4 @@ interface CmssyDeliveryOperation {
41
54
  }
42
55
  declare const CMSSY_DELIVERY_OPERATIONS: readonly CmssyDeliveryOperation[];
43
56
 
44
- export { type BlockSchemaMap, CMSSY_DELIVERY_OPERATIONS, FORM_QUERY, MODEL_DEFINITIONS_QUERY, MODEL_RECORDS_QUERY, type RelationContentEntry, SITE_CONFIG_QUERY, SUBMIT_FORM_MUTATION, asBucket, buildBlockContext, clearWorkspaceIdCache, cmssySecretsMatch, collectFormIds, fetchSiteConfig, getBlockContentForLanguage, normalizeRelationContent, resolveForms, resolveRelationContent, resolveWorkspaceId };
57
+ export { type BlockSchemaMap, CMSSY_DELIVERY_OPERATIONS, FORM_QUERY, MODEL_DEFINITIONS_QUERY, MODEL_RECORDS_QUERY, type RelationContentEntry, SITE_CONFIG_QUERY, SUBMIT_FORM_MUTATION, asBucket, buildBlockContext, clearWorkspaceIdCache, cmssySecretsMatch, collectFormIds, fetchSiteConfig, getBlockContentForLanguage, normalizeBlockContent, resolveForms, resolveRelationContent, resolveWorkspaceId };
@@ -1,5 +1,5 @@
1
1
  export { f as fetchLayouts, c as fetchPage, d as fetchPageById, e as fetchPageMeta, g as fetchPages, r as resolveApiUrl } from './content-client-BPpRfq0h.js';
2
- import { CmssyFormDefinition, BuildBlockContextExtra, CmssyBlockContext, CmssyClientConfig, CmssySiteConfig, RawBlock, FieldDefinition } from '@cmssy/types';
2
+ import { CmssyFormDefinition, BuildBlockContextExtra, CmssyBlockContext, CmssyClientConfig, CmssySiteConfig, FieldDefinition, RawBlock } from '@cmssy/types';
3
3
  export { CmssySiteLocales } from '@cmssy/types';
4
4
  import { G as GraphqlRequestOptions } from './locale-DQZ80cew.js';
5
5
  export { C as CMSSY_LOCALE_HEADER, a as localeForPath, b as localeForPathname, l as localizeHref, c as resolveSiteLocales, s as splitCmssyLocale, d as splitLocaleFromPath } from './locale-DQZ80cew.js';
@@ -22,17 +22,30 @@ declare function fetchSiteConfig(config: CmssyClientConfig, options?: GraphqlReq
22
22
  declare function resolveWorkspaceId(config: CmssyClientConfig, options?: GraphqlRequestOptions): Promise<string>;
23
23
  declare function clearWorkspaceIdCache(): void;
24
24
 
25
- declare function collectFormIds(blocks: RawBlock[], locale: string, defaultLocale: string): string[];
26
- declare function resolveForms(config: CmssyClientConfig, blocks: RawBlock[], locale: string, defaultLocale: string, options?: QueryScopedOptions): Promise<Record<string, CmssyFormDefinition>>;
27
-
28
25
  interface RelationContentEntry {
29
26
  type: string;
30
27
  content: Record<string, unknown>;
31
28
  }
32
29
  type BlockSchemaMap = Record<string, Record<string, FieldDefinition>>;
33
- declare function normalizeRelationContent(content: Record<string, unknown>, schema: Record<string, FieldDefinition>, resolved?: Record<string, unknown>): void;
30
+ /**
31
+ * Brings stored content in line with what the block's schema declares, for the
32
+ * field types whose stored shape differs from the authored one: a relation
33
+ * carries ids, a page selector always carries a list even when it holds one
34
+ * page.
35
+ */
36
+ declare function normalizeBlockContent(content: Record<string, unknown>, schema: Record<string, FieldDefinition>, resolved?: Record<string, unknown>): void;
34
37
  declare function resolveRelationContent(config: CmssyClientConfig, entries: RelationContentEntry[], schemas: BlockSchemaMap, locale?: string, requestOptions?: QueryScopedOptions): Promise<void>;
35
38
 
39
+ /**
40
+ * Finds the forms a page needs by reading the blocks' schemas, so a form field
41
+ * is found under whatever key it was declared with. Reading a fixed `formId`
42
+ * key instead meant `fields.form()` under any other name silently resolved to
43
+ * nothing, and a form field on the style or advanced tab was invisible either
44
+ * way: the editor writes those into their own bucket, not into content.
45
+ */
46
+ declare function collectFormIds(blocks: RawBlock[], schemas: BlockSchemaMap, locale: string, defaultLocale: string): string[];
47
+ declare function resolveForms(config: CmssyClientConfig, blocks: RawBlock[], schemas: BlockSchemaMap, locale: string, defaultLocale: string, options?: QueryScopedOptions): Promise<Record<string, CmssyFormDefinition>>;
48
+
36
49
  declare function cmssySecretsMatch(a: string, b: string): Promise<boolean>;
37
50
 
38
51
  interface CmssyDeliveryOperation {
@@ -41,4 +54,4 @@ interface CmssyDeliveryOperation {
41
54
  }
42
55
  declare const CMSSY_DELIVERY_OPERATIONS: readonly CmssyDeliveryOperation[];
43
56
 
44
- export { type BlockSchemaMap, CMSSY_DELIVERY_OPERATIONS, FORM_QUERY, MODEL_DEFINITIONS_QUERY, MODEL_RECORDS_QUERY, type RelationContentEntry, SITE_CONFIG_QUERY, SUBMIT_FORM_MUTATION, asBucket, buildBlockContext, clearWorkspaceIdCache, cmssySecretsMatch, collectFormIds, fetchSiteConfig, getBlockContentForLanguage, normalizeRelationContent, resolveForms, resolveRelationContent, resolveWorkspaceId };
57
+ export { type BlockSchemaMap, CMSSY_DELIVERY_OPERATIONS, FORM_QUERY, MODEL_DEFINITIONS_QUERY, MODEL_RECORDS_QUERY, type RelationContentEntry, SITE_CONFIG_QUERY, SUBMIT_FORM_MUTATION, asBucket, buildBlockContext, clearWorkspaceIdCache, cmssySecretsMatch, collectFormIds, fetchSiteConfig, getBlockContentForLanguage, normalizeBlockContent, resolveForms, resolveRelationContent, resolveWorkspaceId };
package/dist/internal.js CHANGED
@@ -657,131 +657,7 @@ function createCmssyClient(input) {
657
657
  return client;
658
658
  }
659
659
 
660
- // src/data/resolve-forms.ts
661
- function collectFormIds(blocks, locale, defaultLocale) {
662
- const ids = /* @__PURE__ */ new Set();
663
- for (const block of blocks) {
664
- const content = getBlockContentForLanguage(
665
- block.content,
666
- locale,
667
- defaultLocale
668
- );
669
- const formId = content.formId;
670
- if (typeof formId === "string" && formId.trim()) ids.add(formId);
671
- }
672
- return [...ids];
673
- }
674
- async function resolveForms(config, blocks, locale, defaultLocale, options) {
675
- const ids = collectFormIds(blocks, locale, defaultLocale);
676
- if (ids.length === 0) return {};
677
- const client = createCmssyClient(config);
678
- const entries = await Promise.all(
679
- ids.map(async (id) => {
680
- try {
681
- const data = await client.queryScoped(FORM_QUERY, { formId: id }, options);
682
- return [id, data.public.form.get];
683
- } catch (err) {
684
- if (typeof console !== "undefined") {
685
- console.warn(`[cmssy] failed to resolve form ${id}`, err);
686
- }
687
- return [id, null];
688
- }
689
- })
690
- );
691
- const forms = {};
692
- for (const [id, def] of entries) {
693
- if (def) forms[id] = def;
694
- }
695
- return forms;
696
- }
697
-
698
- // src/data/site-locales.ts
699
- var TTL_MS = 6e4;
700
- var FAILURE_TTL_MS = 5e3;
701
- var MAX_ENTRIES = 64;
702
- var cache = /* @__PURE__ */ new Map();
703
- function localesFromSiteConfig(siteConfig) {
704
- const defaultLocale = siteConfig?.defaultLanguage || "en";
705
- const enabled = siteConfig?.enabledLanguages ?? [];
706
- return {
707
- defaultLocale,
708
- locales: enabled.length > 0 ? enabled : [defaultLocale]
709
- };
710
- }
711
- async function loadSiteLocales(config, options) {
712
- const key = `${resolveApiUrl(config.apiUrl)}::${config.org}::${config.workspaceSlug}`;
713
- const cached = cache.get(key);
714
- if (cached && cached.expires > Date.now()) return cached.value;
715
- cache.delete(key);
716
- let value;
717
- try {
718
- const data = await graphqlRequest(
719
- config,
720
- SITE_CONFIG_QUERY,
721
- { workspaceSlug: config.workspaceSlug },
722
- { ...options, public: true, retry: options?.retry ?? {} },
723
- "site config"
724
- );
725
- value = localesFromSiteConfig(data.public?.siteConfig ?? null);
726
- } catch {
727
- value = null;
728
- }
729
- if (cache.size >= MAX_ENTRIES) cache.clear();
730
- cache.set(key, {
731
- value,
732
- expires: Date.now() + (value ? TTL_MS : FAILURE_TTL_MS)
733
- });
734
- return value;
735
- }
736
- async function resolveSiteLocales(config, options) {
737
- return await loadSiteLocales(config, options) ?? {
738
- defaultLocale: "en",
739
- locales: ["en"]
740
- };
741
- }
742
- function splitLocaleFromPath(path, siteLocales) {
743
- const first = path?.[0];
744
- if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
745
- return { locale: first, path: path.slice(1) };
746
- }
747
- return { locale: siteLocales.defaultLocale, path };
748
- }
749
-
750
- // src/data/localize-href.ts
751
- var PROTOCOL_OR_RELATIVE = /^([a-z][a-z0-9+.-]*:|\/\/)/i;
752
- function isExternalHref(href) {
753
- const value = href.trim();
754
- if (!value) return true;
755
- if (value.startsWith("#")) return true;
756
- return PROTOCOL_OR_RELATIVE.test(value);
757
- }
758
- function stripLeadingLocale(path, locale) {
759
- const segments = path.split("/");
760
- const first = segments[1];
761
- if (first && first !== locale.default && locale.enabled.includes(first)) {
762
- segments.splice(1, 1);
763
- const rest = segments.join("/");
764
- return rest === "" ? "/" : rest;
765
- }
766
- return path;
767
- }
768
- function addLocalePrefix(path, target, locale) {
769
- if (target === locale.default) return path;
770
- if (path === "/") return `/${target}`;
771
- return `/${target}${path}`;
772
- }
773
- function localizeHref(href, locale) {
774
- const value = href.trim();
775
- if (isExternalHref(value)) return href;
776
- const boundary = value.search(/[?#]/);
777
- const path = boundary === -1 ? value : value.slice(0, boundary);
778
- const suffix = boundary === -1 ? "" : value.slice(boundary);
779
- if (!path.startsWith("/")) return href;
780
- const bare = stripLeadingLocale(path, locale);
781
- return `${addLocalePrefix(bare, locale.current, locale)}${suffix}`;
782
- }
783
-
784
- // src/data/relation-resolver.ts
660
+ // src/data/block-content.ts
785
661
  var RECORDS_BY_IDS_QUERY = `query PublicRecordsByIds($workspaceId: String!, $ids: [String!]!, $locale: String) {
786
662
  public {
787
663
  model {
@@ -803,17 +679,54 @@ function storedIds(value) {
803
679
  }
804
680
  return [];
805
681
  }
682
+ function isRecord(value) {
683
+ return typeof value === "object" && value !== null && !Array.isArray(value);
684
+ }
685
+ var BLOCK_BUCKETS = ["content", "style", "advanced"];
686
+ function bucketOf(field) {
687
+ return field.tab === "style" || field.tab === "advanced" ? field.tab : "content";
688
+ }
689
+ function walkBlockFields(values, schema, visit, options = {}, path = []) {
690
+ for (const [key, field] of Object.entries(schema)) {
691
+ if (options.bucket && path.length === 0 && bucketOf(field) !== options.bucket) {
692
+ continue;
693
+ }
694
+ if (field.type === "repeater") {
695
+ const itemSchema = field.itemSchema;
696
+ const stored = values[key];
697
+ if (!itemSchema || !Array.isArray(stored)) continue;
698
+ const rows = options.copyRows ? stored.map((row) => isRecord(row) ? { ...row } : row) : stored;
699
+ if (options.copyRows) values[key] = rows;
700
+ rows.forEach((row, index) => {
701
+ if (isRecord(row)) {
702
+ walkBlockFields(row, itemSchema, visit, options, [
703
+ ...path,
704
+ key,
705
+ index
706
+ ]);
707
+ }
708
+ });
709
+ continue;
710
+ }
711
+ visit(values, key, field, path);
712
+ }
713
+ }
806
714
  function collectRefs(entries, schemas) {
807
715
  const refs = [];
808
716
  for (const entry of entries) {
809
717
  const schema = schemas[entry.type];
810
718
  if (!schema) continue;
811
- for (const [key, field] of Object.entries(schema)) {
812
- if (field.type !== "relation") continue;
813
- const model = relationModel(field);
814
- if (!model) continue;
815
- refs.push({ content: entry.content, key, field, model });
816
- }
719
+ walkBlockFields(
720
+ entry.content,
721
+ schema,
722
+ (holder, key, field) => {
723
+ if (field.type !== "relation") return;
724
+ const model = relationModel(field);
725
+ if (!model) return;
726
+ refs.push({ content: holder, key, field, model });
727
+ },
728
+ { copyRows: true, bucket: "content" }
729
+ );
817
730
  }
818
731
  return refs;
819
732
  }
@@ -823,31 +736,70 @@ function isResolvedRecord(value) {
823
736
  function isListShaped(field) {
824
737
  return field.relationMode === "all" || field.relationType === "hasMany" || field.multiple === true;
825
738
  }
826
- function normalizeRelationContent(content, schema, resolved) {
827
- for (const [key, field] of Object.entries(schema)) {
828
- if (field.type !== "relation") continue;
829
- const value = content[key];
830
- const fallback = resolved?.[key];
831
- if (isListShaped(field)) {
832
- if (Array.isArray(value)) {
833
- const records = value.filter(isResolvedRecord);
834
- if (records.length > 0 || value.length === 0) {
835
- content[key] = records;
836
- continue;
739
+ function toPageRef(value) {
740
+ if (typeof value === "string") {
741
+ return value ? { slug: value, displayName: {} } : null;
742
+ }
743
+ if (isRecord(value) && typeof value.slug === "string" && value.slug) {
744
+ return value;
745
+ }
746
+ return null;
747
+ }
748
+ function toPageRefs(value) {
749
+ const items = Array.isArray(value) ? value : [value];
750
+ return items.map(toPageRef).filter((ref) => ref !== null);
751
+ }
752
+ function fallbackHolder(resolved, path) {
753
+ let holder = resolved;
754
+ for (const step of path) {
755
+ if (holder === void 0 || holder === null) return void 0;
756
+ holder = holder[step];
757
+ }
758
+ return isRecord(holder) ? holder : void 0;
759
+ }
760
+ function normalizeBlockContent(content, schema, resolved) {
761
+ walkBlockFields(
762
+ content,
763
+ schema,
764
+ (holder, key, field, path) => {
765
+ if (field.type === "pageSelector") {
766
+ if (!(key in holder)) return;
767
+ const refs = toPageRefs(holder[key]);
768
+ if (field.multiple === false) {
769
+ if (refs[0]) holder[key] = refs[0];
770
+ else delete holder[key];
771
+ } else {
772
+ holder[key] = refs;
837
773
  }
774
+ return;
838
775
  }
839
- content[key] = Array.isArray(fallback) ? fallback.filter(isResolvedRecord) : [];
840
- } else if (!isResolvedRecord(value)) {
841
- if (value != null && value !== "" && isResolvedRecord(fallback)) {
842
- content[key] = fallback;
843
- } else {
844
- delete content[key];
776
+ if (field.type !== "relation") return;
777
+ const value = holder[key];
778
+ const fallback = fallbackHolder(resolved, path)?.[key];
779
+ if (isListShaped(field)) {
780
+ if (Array.isArray(value)) {
781
+ const records = value.filter(isResolvedRecord);
782
+ if (records.length > 0 || value.length === 0) {
783
+ holder[key] = records;
784
+ return;
785
+ }
786
+ }
787
+ holder[key] = Array.isArray(fallback) ? fallback.filter(isResolvedRecord) : [];
788
+ } else if (!isResolvedRecord(value)) {
789
+ if (value != null && value !== "" && isResolvedRecord(fallback)) {
790
+ holder[key] = fallback;
791
+ } else {
792
+ delete holder[key];
793
+ }
845
794
  }
846
- }
847
- }
795
+ },
796
+ { copyRows: true, bucket: "content" }
797
+ );
848
798
  }
849
799
  function collectionKey(ref) {
850
- return [ref.model, ref.field.sort ?? "", ref.field.limit ?? ""].join("\0");
800
+ return [ref.model, ref.field.sort ?? "", ref.field.limit ?? ""].join(
801
+ "\0"
802
+ );
851
803
  }
852
804
  async function resolveRelationContent(config, entries, schemas, locale, requestOptions = {}) {
853
805
  const refs = collectRefs(entries, schemas);
@@ -943,6 +895,142 @@ async function fetchCollections(client, collections, locale, requestOptions = {}
943
895
  return byKey;
944
896
  }
945
897
 
898
+ // src/data/resolve-forms.ts
899
+ function collectFormIds(blocks, schemas, locale, defaultLocale) {
900
+ const ids = /* @__PURE__ */ new Set();
901
+ for (const block of blocks) {
902
+ const schema = schemas[block.type];
903
+ if (!schema) continue;
904
+ const values = {
905
+ content: getBlockContentForLanguage(block.content, locale, defaultLocale),
906
+ style: asBucket(block.style),
907
+ advanced: asBucket(block.advanced)
908
+ };
909
+ for (const bucket of BLOCK_BUCKETS) {
910
+ walkBlockFields(
911
+ values[bucket],
912
+ schema,
913
+ (holder, key, field) => {
914
+ if (field.type !== "form") return;
915
+ const id = holder[key];
916
+ if (typeof id === "string" && id.trim()) ids.add(id);
917
+ },
918
+ { bucket }
919
+ );
920
+ }
921
+ }
922
+ return [...ids];
923
+ }
924
+ async function resolveForms(config, blocks, schemas, locale, defaultLocale, options) {
925
+ const ids = collectFormIds(blocks, schemas, locale, defaultLocale);
926
+ if (ids.length === 0) return {};
927
+ const client = createCmssyClient(config);
928
+ const entries = await Promise.all(
929
+ ids.map(async (id) => {
930
+ try {
931
+ const data = await client.queryScoped(FORM_QUERY, { formId: id }, options);
932
+ return [id, data.public.form.get];
933
+ } catch (err) {
934
+ if (typeof console !== "undefined") {
935
+ console.warn(`[cmssy] failed to resolve form ${id}`, err);
936
+ }
937
+ return [id, null];
938
+ }
939
+ })
940
+ );
941
+ const forms = {};
942
+ for (const [id, def] of entries) {
943
+ if (def) forms[id] = def;
944
+ }
945
+ return forms;
946
+ }
947
+
948
+ // src/data/site-locales.ts
949
+ var TTL_MS = 6e4;
950
+ var FAILURE_TTL_MS = 5e3;
951
+ var MAX_ENTRIES = 64;
952
+ var cache = /* @__PURE__ */ new Map();
953
+ function localesFromSiteConfig(siteConfig) {
954
+ const defaultLocale = siteConfig?.defaultLanguage || "en";
955
+ const enabled = siteConfig?.enabledLanguages ?? [];
956
+ return {
957
+ defaultLocale,
958
+ locales: enabled.length > 0 ? enabled : [defaultLocale]
959
+ };
960
+ }
961
+ async function loadSiteLocales(config, options) {
962
+ const key = `${resolveApiUrl(config.apiUrl)}::${config.org}::${config.workspaceSlug}`;
963
+ const cached = cache.get(key);
964
+ if (cached && cached.expires > Date.now()) return cached.value;
965
+ cache.delete(key);
966
+ let value;
967
+ try {
968
+ const data = await graphqlRequest(
969
+ config,
970
+ SITE_CONFIG_QUERY,
971
+ { workspaceSlug: config.workspaceSlug },
972
+ { ...options, public: true, retry: options?.retry ?? {} },
973
+ "site config"
974
+ );
975
+ value = localesFromSiteConfig(data.public?.siteConfig ?? null);
976
+ } catch {
977
+ value = null;
978
+ }
979
+ if (cache.size >= MAX_ENTRIES) cache.clear();
980
+ cache.set(key, {
981
+ value,
982
+ expires: Date.now() + (value ? TTL_MS : FAILURE_TTL_MS)
983
+ });
984
+ return value;
985
+ }
986
+ async function resolveSiteLocales(config, options) {
987
+ return await loadSiteLocales(config, options) ?? {
988
+ defaultLocale: "en",
989
+ locales: ["en"]
990
+ };
991
+ }
992
+ function splitLocaleFromPath(path, siteLocales) {
993
+ const first = path?.[0];
994
+ if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
995
+ return { locale: first, path: path.slice(1) };
996
+ }
997
+ return { locale: siteLocales.defaultLocale, path };
998
+ }
999
+
1000
+ // src/data/localize-href.ts
1001
+ var PROTOCOL_OR_RELATIVE = /^([a-z][a-z0-9+.-]*:|\/\/)/i;
1002
+ function isExternalHref(href) {
1003
+ const value = href.trim();
1004
+ if (!value) return true;
1005
+ if (value.startsWith("#")) return true;
1006
+ return PROTOCOL_OR_RELATIVE.test(value);
1007
+ }
1008
+ function stripLeadingLocale(path, locale) {
1009
+ const segments = path.split("/");
1010
+ const first = segments[1];
1011
+ if (first && first !== locale.default && locale.enabled.includes(first)) {
1012
+ segments.splice(1, 1);
1013
+ const rest = segments.join("/");
1014
+ return rest === "" ? "/" : rest;
1015
+ }
1016
+ return path;
1017
+ }
1018
+ function addLocalePrefix(path, target, locale) {
1019
+ if (target === locale.default) return path;
1020
+ if (path === "/") return `/${target}`;
1021
+ return `/${target}${path}`;
1022
+ }
1023
+ function localizeHref(href, locale) {
1024
+ const value = href.trim();
1025
+ if (isExternalHref(value)) return href;
1026
+ const boundary = value.search(/[?#]/);
1027
+ const path = boundary === -1 ? value : value.slice(0, boundary);
1028
+ const suffix = boundary === -1 ? "" : value.slice(boundary);
1029
+ if (!path.startsWith("/")) return href;
1030
+ const bare = stripLeadingLocale(path, locale);
1031
+ return `${addLocalePrefix(bare, locale.current, locale)}${suffix}`;
1032
+ }
1033
+
946
1034
  // src/locale.ts
947
1035
  var CMSSY_LOCALE_HEADER = "x-cmssy-locale";
948
1036
  async function localeForPathname(config, pathname) {
@@ -1145,4 +1233,4 @@ var CMSSY_DELIVERY_OPERATIONS = [
1145
1233
  }
1146
1234
  ];
1147
1235
 
1148
- export { CMSSY_DELIVERY_OPERATIONS, CMSSY_LOCALE_HEADER, FORM_QUERY, MODEL_DEFINITIONS_QUERY, MODEL_RECORDS_QUERY, SITE_CONFIG_QUERY, SUBMIT_FORM_MUTATION, asBucket, buildBlockContext, clearWorkspaceIdCache, cmssyCspHeaders, cmssySecretsMatch, collectFormIds, fetchLayouts, fetchPage, fetchPageById, fetchPageMeta, fetchPages, fetchSiteConfig, getBlockContentForLanguage, isDevelopment, localeForPath, localeForPathname, localizeHref, localizedPath, normalizeRelationContent, resolveApiUrl, resolveForms, resolveInitialTarget, resolveRelationContent, resolveSiteLocales, resolveWorkspaceId, splitCmssyLocale, splitLocaleFromPath, toCspOrigin };
1236
+ export { CMSSY_DELIVERY_OPERATIONS, CMSSY_LOCALE_HEADER, FORM_QUERY, MODEL_DEFINITIONS_QUERY, MODEL_RECORDS_QUERY, SITE_CONFIG_QUERY, SUBMIT_FORM_MUTATION, asBucket, buildBlockContext, clearWorkspaceIdCache, cmssyCspHeaders, cmssySecretsMatch, collectFormIds, fetchLayouts, fetchPage, fetchPageById, fetchPageMeta, fetchPages, fetchSiteConfig, getBlockContentForLanguage, isDevelopment, localeForPath, localeForPathname, localizeHref, localizedPath, normalizeBlockContent, resolveApiUrl, resolveForms, resolveInitialTarget, resolveRelationContent, resolveSiteLocales, resolveWorkspaceId, splitCmssyLocale, splitLocaleFromPath, toCspOrigin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/core",
3
- "version": "12.4.0",
3
+ "version": "12.6.0",
4
4
  "description": "Framework-agnostic cmssy gateway: GraphQL client, block/field system, editor protocol. No React, no Next.",
5
5
  "keywords": [
6
6
  "cmssy",