@cms0/cms0 0.2.7 → 0.2.9

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/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { schemaDescriptor } from "@cms0/cms0/schema-descriptors";
2
- import { buildZodSchemasFromDescriptor, } from "@cms0/shared";
2
+ import { buildZodSchemasFromDescriptor, decodeTaggedUnionValue, getUnionBranchKeys, } from "@cms0/shared";
3
3
  import { getCustomInlineTypeMetadata } from "./custom-types/registry.js";
4
4
  const DEFAULT_MODEL_NORMALIZATION_CONCURRENCY = 8;
5
5
  const COLLECTION_SHAPE_ERROR = "Invalid collection response. Expected array or { items, total }.";
@@ -105,6 +105,9 @@ function snakeCaseModelIdKey(modelName) {
105
105
  .toLowerCase();
106
106
  return `${snake}_id`;
107
107
  }
108
+ function isUuidLikeId(value) {
109
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
110
+ }
108
111
  function extractModelRefId(raw, modelName, options) {
109
112
  if (typeof raw === "string")
110
113
  return raw;
@@ -145,6 +148,15 @@ function extractModelRefId(raw, modelName, options) {
145
148
  function isPrimitiveDescriptor(desc) {
146
149
  return desc.kind === "primitive";
147
150
  }
151
+ function isEnumDescriptor(desc) {
152
+ return desc.kind === "enum" && Array.isArray(desc.values);
153
+ }
154
+ function isUnionDescriptor(desc) {
155
+ return desc.kind === "union" && Array.isArray(desc.anyOf);
156
+ }
157
+ function isScalarDescriptor(desc) {
158
+ return isPrimitiveDescriptor(desc) || isEnumDescriptor(desc) || isUnionDescriptor(desc);
159
+ }
148
160
  function isModelRefDescriptor(desc) {
149
161
  return desc.kind === "modelRef";
150
162
  }
@@ -162,7 +174,7 @@ function attachIdIfObject(value, id) {
162
174
  }
163
175
  return value;
164
176
  }
165
- function attachIdForPrimitiveDescriptor(descriptor, value, id) {
177
+ function attachIdForScalarDescriptor(descriptor, value, id) {
166
178
  const withId = attachIdIfObject(value, id);
167
179
  if (!withId || typeof withId !== "object" || Array.isArray(withId)) {
168
180
  return withId;
@@ -358,6 +370,23 @@ function coerceLocalizedRichTextValue(value, locale, defaultLocale) {
358
370
  }
359
371
  return projectLocalizedByLocale(normalized, locale);
360
372
  }
373
+ function coerceSeoValue(value) {
374
+ if (value == null)
375
+ return value;
376
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
377
+ return {};
378
+ }
379
+ const source = value;
380
+ const keys = Object.keys(source).filter((key) => key !== "id");
381
+ if (keys.length === 1 &&
382
+ keys[0] === "value" &&
383
+ source.value &&
384
+ typeof source.value === "object" &&
385
+ !Array.isArray(source.value)) {
386
+ return source.value;
387
+ }
388
+ return source;
389
+ }
361
390
  function coerceCustomTypeValue(descriptor, value, options) {
362
391
  const customType = descriptor.customType;
363
392
  if (!customType)
@@ -373,8 +402,81 @@ function coerceCustomTypeValue(descriptor, value, options) {
373
402
  if (customType === "LocalizedRichText") {
374
403
  return coerceLocalizedRichTextValue(value, options?.locale, options?.defaultLocale);
375
404
  }
405
+ if (customType === "Seo") {
406
+ return coerceSeoValue(value);
407
+ }
408
+ return value;
409
+ }
410
+ function coercePrimitiveValueByType(type, value) {
411
+ if (type === "string") {
412
+ if (value == null)
413
+ return "";
414
+ return typeof value === "string" ? value : String(value);
415
+ }
416
+ if (type === "number") {
417
+ if (typeof value === "number")
418
+ return Number.isFinite(value) ? value : null;
419
+ const parsed = Number(value);
420
+ return Number.isFinite(parsed) ? parsed : null;
421
+ }
422
+ if (type === "boolean") {
423
+ if (typeof value === "boolean")
424
+ return value;
425
+ if (value === "true" || value === "1" || value === 1)
426
+ return true;
427
+ if (value === "false" || value === "0" || value === 0)
428
+ return false;
429
+ return Boolean(value);
430
+ }
376
431
  return value;
377
432
  }
433
+ function coerceEnumValue(descriptor, value) {
434
+ const valueType = (descriptor.valueType ?? "string");
435
+ const coerced = coercePrimitiveValueByType(valueType, value);
436
+ const values = Array.isArray(descriptor.values)
437
+ ? descriptor.values
438
+ : [];
439
+ if (!values.length)
440
+ return coerced;
441
+ if (values.some((entry) => Object.is(entry, coerced))) {
442
+ return coerced;
443
+ }
444
+ return values[0];
445
+ }
446
+ function decodeUnionEnvelope(descriptor, raw) {
447
+ const decoded = decodeTaggedUnionValue(raw);
448
+ if (!decoded)
449
+ return null;
450
+ const branches = Array.isArray(descriptor.anyOf)
451
+ ? descriptor.anyOf
452
+ : [];
453
+ if (!branches.length)
454
+ return null;
455
+ const branchKeys = getUnionBranchKeys(descriptor);
456
+ const branchIndex = branchKeys.findIndex((key) => key === decoded.branchKey);
457
+ if (branchIndex < 0 || branchIndex >= branches.length)
458
+ return null;
459
+ const branchDescriptor = branches[branchIndex];
460
+ if (!branchDescriptor)
461
+ return null;
462
+ return {
463
+ branchDescriptor,
464
+ branchValue: decoded.value,
465
+ };
466
+ }
467
+ function decodeUnionValueFromRawField(descriptor, raw) {
468
+ const direct = decodeUnionEnvelope(descriptor, raw);
469
+ if (direct) {
470
+ return { ...direct, rowId: extractId(raw) };
471
+ }
472
+ if (raw && typeof raw === "object" && !Array.isArray(raw) && "value" in raw) {
473
+ const nested = decodeUnionEnvelope(descriptor, raw.value);
474
+ if (nested) {
475
+ return { ...nested, rowId: extractId(raw) };
476
+ }
477
+ }
478
+ return null;
479
+ }
378
480
  function ensureCollectionEnvelope(data, path) {
379
481
  if (Array.isArray(data)) {
380
482
  return { items: data, total: data.length };
@@ -446,12 +548,8 @@ async function normalizeObjectField(descriptor, path, raw, context, trail, isCol
446
548
  const source = raw && typeof raw === "object" ? raw : {};
447
549
  const output = {};
448
550
  for (const [propertyName, propertyDescriptor] of Object.entries(descriptor.properties)) {
449
- if (isPrimitiveDescriptor(propertyDescriptor)) {
450
- output[propertyName] = coerceCustomTypeValue(propertyDescriptor, source[propertyName], {
451
- locale: context.options.locale,
452
- defaultLocale: context.options.defaultLocale,
453
- assetUrlBuilder: context.options.assetUrlBuilder,
454
- });
551
+ if (isScalarDescriptor(propertyDescriptor)) {
552
+ output[propertyName] = await normalizeField(propertyDescriptor, `${path}/${propertyName}`, source[propertyName], context, trail, false);
455
553
  continue;
456
554
  }
457
555
  if (isModelRefDescriptor(propertyDescriptor)) {
@@ -539,16 +637,14 @@ async function normalizeObjectField(descriptor, path, raw, context, trail, isCol
539
637
  async function normalizeArrayField(descriptor, path, raw, context, trail) {
540
638
  const envelope = ensureCollectionEnvelope(raw, path);
541
639
  const itemDescriptor = descriptor.items;
542
- if (isPrimitiveDescriptor(itemDescriptor)) {
543
- return envelope.items.map((row) => {
544
- const value = row && typeof row === "object" && "value" in row
545
- ? row.value
546
- : row;
640
+ if (isScalarDescriptor(itemDescriptor)) {
641
+ return mapWithConcurrency(envelope.items, context.options.modelNormalizationConcurrency, async (row) => {
642
+ const normalizedValue = await normalizeField(itemDescriptor, path, row, context, trail, true);
547
643
  if (context.options.includeIdMode !== "all") {
548
- return value;
644
+ return normalizedValue;
549
645
  }
550
646
  const id = extractId(row);
551
- const valueWithId = attachIdForPrimitiveDescriptor(itemDescriptor, value, id);
647
+ const valueWithId = attachIdForScalarDescriptor(itemDescriptor, normalizedValue, id);
552
648
  const isObjectValue = valueWithId && typeof valueWithId === "object" && !Array.isArray(valueWithId);
553
649
  if (!id) {
554
650
  return valueWithId;
@@ -579,6 +675,60 @@ async function normalizeArrayField(descriptor, path, raw, context, trail) {
579
675
  }
580
676
  return envelope.items;
581
677
  }
678
+ async function normalizeInlineField(descriptor, raw, context, trail, isCollectionItem) {
679
+ if (isPrimitiveDescriptor(descriptor)) {
680
+ return coerceCustomTypeValue(descriptor, raw, {
681
+ locale: context.options.locale,
682
+ defaultLocale: context.options.defaultLocale,
683
+ assetUrlBuilder: context.options.assetUrlBuilder,
684
+ });
685
+ }
686
+ if (isEnumDescriptor(descriptor)) {
687
+ return coerceEnumValue(descriptor, raw);
688
+ }
689
+ if (isUnionDescriptor(descriptor)) {
690
+ if (raw == null)
691
+ return raw;
692
+ const decoded = decodeUnionEnvelope(descriptor, raw);
693
+ if (!decoded) {
694
+ return null;
695
+ }
696
+ return normalizeInlineField(decoded.branchDescriptor, decoded.branchValue, context, trail, isCollectionItem);
697
+ }
698
+ if (isModelRefDescriptor(descriptor)) {
699
+ const refId = extractModelRefId(raw, descriptor.model, {
700
+ allowObjectIdFallback: true,
701
+ });
702
+ if (!refId) {
703
+ return missingModelRefValue(descriptor);
704
+ }
705
+ return normalizeModelRef(descriptor.model, refId, context, trail, isCollectionItem, raw);
706
+ }
707
+ if (isArrayDescriptor(descriptor)) {
708
+ const source = Array.isArray(raw) ? raw : [];
709
+ const normalized = await mapWithConcurrency(source, context.options.modelNormalizationConcurrency, async (entry) => normalizeInlineField(descriptor.items, entry, context, trail, true));
710
+ return coerceCustomTypeValue(descriptor, normalized, {
711
+ locale: context.options.locale,
712
+ defaultLocale: context.options.defaultLocale,
713
+ assetUrlBuilder: context.options.assetUrlBuilder,
714
+ });
715
+ }
716
+ if (isObjectDescriptor(descriptor)) {
717
+ const source = raw && typeof raw === "object" && !Array.isArray(raw)
718
+ ? raw
719
+ : {};
720
+ const output = {};
721
+ for (const [propertyName, propertyDescriptor] of Object.entries(descriptor.properties)) {
722
+ output[propertyName] = await normalizeInlineField(propertyDescriptor, source[propertyName], context, trail, false);
723
+ }
724
+ return coerceCustomTypeValue(descriptor, output, {
725
+ locale: context.options.locale,
726
+ defaultLocale: context.options.defaultLocale,
727
+ assetUrlBuilder: context.options.assetUrlBuilder,
728
+ });
729
+ }
730
+ return raw;
731
+ }
582
732
  async function normalizeField(descriptor, path, raw, context, trail, isCollectionItem) {
583
733
  if (isPrimitiveDescriptor(descriptor)) {
584
734
  const value = raw && typeof raw === "object" && "value" in raw
@@ -591,10 +741,33 @@ async function normalizeField(descriptor, path, raw, context, trail, isCollectio
591
741
  });
592
742
  if (shouldIncludeObjectId(context.options.includeIdMode, isCollectionItem)) {
593
743
  const id = extractId(raw);
594
- coerced = attachIdForPrimitiveDescriptor(descriptor, coerced, id);
744
+ coerced = attachIdForScalarDescriptor(descriptor, coerced, id);
745
+ }
746
+ return coerced;
747
+ }
748
+ if (isEnumDescriptor(descriptor)) {
749
+ const value = raw && typeof raw === "object" && "value" in raw
750
+ ? raw.value
751
+ : raw;
752
+ let coerced = coerceEnumValue(descriptor, value);
753
+ if (shouldIncludeObjectId(context.options.includeIdMode, isCollectionItem)) {
754
+ const id = extractId(raw);
755
+ coerced = attachIdForScalarDescriptor(descriptor, coerced, id);
595
756
  }
596
757
  return coerced;
597
758
  }
759
+ if (isUnionDescriptor(descriptor)) {
760
+ const decoded = decodeUnionValueFromRawField(descriptor, raw);
761
+ if (!decoded) {
762
+ return null;
763
+ }
764
+ let normalized = await normalizeInlineField(decoded.branchDescriptor, decoded.branchValue, context, trail, isCollectionItem);
765
+ if (shouldIncludeObjectId(context.options.includeIdMode, isCollectionItem)) {
766
+ const id = decoded.rowId ?? extractId(raw);
767
+ normalized = attachIdForScalarDescriptor(descriptor, normalized, id);
768
+ }
769
+ return normalized;
770
+ }
598
771
  if (isModelRefDescriptor(descriptor)) {
599
772
  const inlineModelDescriptor = context.modelDescriptors.get(descriptor.model);
600
773
  const inlineModel = inlineModelDescriptor
@@ -694,6 +867,140 @@ function buildSearchParams(query) {
694
867
  }
695
868
  return params;
696
869
  }
870
+ function isPlainObject(value) {
871
+ return !!value && typeof value === "object" && !Array.isArray(value);
872
+ }
873
+ function cloneValue(value) {
874
+ if (Array.isArray(value)) {
875
+ return value.map((item) => cloneValue(item));
876
+ }
877
+ if (isPlainObject(value)) {
878
+ const out = {};
879
+ for (const [key, child] of Object.entries(value)) {
880
+ out[key] = cloneValue(child);
881
+ }
882
+ return out;
883
+ }
884
+ return value;
885
+ }
886
+ function normalizeProjectionPaths(input) {
887
+ if (!input)
888
+ return [];
889
+ const values = Array.isArray(input) ? input : [input];
890
+ const unique = new Set();
891
+ for (const raw of values) {
892
+ for (const candidate of raw.split(",")) {
893
+ const path = candidate.trim();
894
+ if (!path)
895
+ continue;
896
+ const normalized = path
897
+ .split(".")
898
+ .map((part) => part.trim())
899
+ .filter(Boolean)
900
+ .join(".");
901
+ if (!normalized)
902
+ continue;
903
+ unique.add(normalized);
904
+ }
905
+ }
906
+ return Array.from(unique).map((path) => path.split("."));
907
+ }
908
+ function mergeProjectedValue(existing, incoming) {
909
+ if (incoming === undefined)
910
+ return existing;
911
+ if (existing === undefined)
912
+ return cloneValue(incoming);
913
+ if (Array.isArray(existing) && Array.isArray(incoming)) {
914
+ const max = Math.max(existing.length, incoming.length);
915
+ const out = [];
916
+ for (let index = 0; index < max; index++) {
917
+ const next = mergeProjectedValue(existing[index], incoming[index]);
918
+ if (next !== undefined)
919
+ out.push(next);
920
+ }
921
+ return out;
922
+ }
923
+ if (isPlainObject(existing) && isPlainObject(incoming)) {
924
+ const out = { ...existing };
925
+ for (const [key, child] of Object.entries(incoming)) {
926
+ out[key] = mergeProjectedValue(out[key], child);
927
+ }
928
+ return out;
929
+ }
930
+ return cloneValue(incoming);
931
+ }
932
+ function projectValueByPath(value, tokens) {
933
+ if (!tokens.length)
934
+ return cloneValue(value);
935
+ if (Array.isArray(value)) {
936
+ const projectedItems = value
937
+ .map((item) => projectValueByPath(item, tokens))
938
+ .filter((item) => item !== undefined);
939
+ return projectedItems.length ? projectedItems : undefined;
940
+ }
941
+ if (!isPlainObject(value))
942
+ return undefined;
943
+ const [head, ...rest] = tokens;
944
+ if (!head || !(head in value))
945
+ return undefined;
946
+ const child = projectValueByPath(value[head], rest);
947
+ if (child === undefined)
948
+ return undefined;
949
+ return { [head]: child };
950
+ }
951
+ function excludeValueByPath(value, tokens) {
952
+ if (!tokens.length)
953
+ return undefined;
954
+ if (Array.isArray(value)) {
955
+ return value.map((item) => excludeValueByPath(item, tokens));
956
+ }
957
+ if (!isPlainObject(value))
958
+ return value;
959
+ const [head, ...rest] = tokens;
960
+ if (!head || !(head in value))
961
+ return cloneValue(value);
962
+ const out = { ...value };
963
+ if (!rest.length) {
964
+ delete out[head];
965
+ return out;
966
+ }
967
+ const next = excludeValueByPath(out[head], rest);
968
+ if (next === undefined) {
969
+ delete out[head];
970
+ }
971
+ else {
972
+ out[head] = next;
973
+ }
974
+ return out;
975
+ }
976
+ function applyFieldProjection(value, fields, exclude) {
977
+ const includePaths = normalizeProjectionPaths(fields);
978
+ const excludePaths = normalizeProjectionPaths(exclude);
979
+ let projected = value;
980
+ if (includePaths.length) {
981
+ let included = undefined;
982
+ for (const pathTokens of includePaths) {
983
+ const partial = projectValueByPath(value, pathTokens);
984
+ included = mergeProjectedValue(included, partial);
985
+ }
986
+ projected =
987
+ included === undefined
988
+ ? Array.isArray(value)
989
+ ? []
990
+ : isPlainObject(value)
991
+ ? {}
992
+ : undefined
993
+ : included;
994
+ }
995
+ if (excludePaths.length) {
996
+ let excluded = projected;
997
+ for (const pathTokens of excludePaths) {
998
+ excluded = excludeValueByPath(excluded, pathTokens);
999
+ }
1000
+ projected = excluded;
1001
+ }
1002
+ return projected;
1003
+ }
697
1004
  async function requestJson(baseUrl, apiKey, path, options) {
698
1005
  const params = buildSearchParams(options?.query);
699
1006
  const url = `${baseUrl}/${path}${params.toString() ? `?${params.toString()}` : ""}`;
@@ -755,6 +1062,16 @@ async function runResource(resource, descriptor, baseUrl, apiKey, defaultLocale,
755
1062
  const query = {
756
1063
  ...(options?.query ?? {}),
757
1064
  };
1065
+ if (options?.fields !== undefined && query.fields === undefined) {
1066
+ query.fields = Array.isArray(options.fields)
1067
+ ? options.fields.join(",")
1068
+ : options.fields;
1069
+ }
1070
+ if (options?.exclude !== undefined && query.exclude === undefined) {
1071
+ query.exclude = Array.isArray(options.exclude)
1072
+ ? options.exclude.join(",")
1073
+ : options.exclude;
1074
+ }
758
1075
  const shouldNormalize = responseMode !== "raw";
759
1076
  if (shouldNormalize && query.raw === undefined) {
760
1077
  query.raw = 1;
@@ -804,7 +1121,7 @@ async function runResource(resource, descriptor, baseUrl, apiKey, defaultLocale,
804
1121
  });
805
1122
  }
806
1123
  if (!shouldNormalize) {
807
- return rawData;
1124
+ return applyFieldProjection(rawData, options?.fields, options?.exclude);
808
1125
  }
809
1126
  const modelDescriptors = new Map();
810
1127
  for (const modelName of Object.keys(descriptor.models ?? {})) {
@@ -839,9 +1156,10 @@ async function runResource(resource, descriptor, baseUrl, apiKey, defaultLocale,
839
1156
  };
840
1157
  const normalizedItems = await normalizeArrayField(descriptorForCollection, resource.path, envelope, context, new Set());
841
1158
  if (responseMode === "envelope") {
1159
+ const projectedItems = applyFieldProjection(normalizedItems, options?.fields, options?.exclude);
842
1160
  validateResult(resource, normalizedItems, descriptor, includeIdMode, zodSchemas, modelZodSchemas);
843
1161
  return {
844
- items: normalizedItems,
1162
+ items: Array.isArray(projectedItems) ? projectedItems : normalizedItems,
845
1163
  total: envelope.total,
846
1164
  };
847
1165
  }
@@ -850,7 +1168,7 @@ async function runResource(resource, descriptor, baseUrl, apiKey, defaultLocale,
850
1168
  }
851
1169
  const normalized = await normalizeField(resource.descriptor, normalizePath, rawData, context, new Set(), false);
852
1170
  validateResult(resource, normalized, descriptor, includeIdMode, zodSchemas, modelZodSchemas);
853
- return normalized;
1171
+ return applyFieldProjection(normalized, options?.fields, options?.exclude);
854
1172
  }
855
1173
  export function createCmsClient(descriptor, config) {
856
1174
  const baseUrl = normalizeBaseUrl(config.apiConfig?.baseUrl);
@@ -913,3 +1231,4 @@ export function createCmsClient(descriptor, config) {
913
1231
  export function cms0(config) {
914
1232
  return createCmsClient(schemaDescriptor, config);
915
1233
  }
1234
+ export { resolveLocalized, toNextMetadata, toOpenGraph, toTwitter, } from "./seo.js";