@cms0/cms0 0.2.11 → 0.2.12

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,3 +1,4 @@
1
+ import { attachCms0ProvenanceRoot, enableCms0ProvenanceTracking, isCms0CanvasTransportEnabled, registerCms0CollectionItemIdentity, } from "@cms0/cms0/provenance";
1
2
  import { schemaDescriptor } from "@cms0/cms0/schema-descriptors";
2
3
  import { buildZodSchemasFromDescriptor, decodeTaggedUnionValue, getUnionBranchKeys, } from "@cms0/shared";
3
4
  import { getCustomInlineTypeMetadata } from "./custom-types/registry.js";
@@ -508,6 +509,44 @@ function ensureCollectionEnvelope(data, path) {
508
509
  }
509
510
  throw new Error(`${COLLECTION_SHAPE_ERROR} Path: '${path}'.`);
510
511
  }
512
+ async function readCanvasModelRefCollectionIdentities(path, descriptor, context) {
513
+ const cache = context.canvasModelRefCollectionIdentityCache;
514
+ if (!cache)
515
+ return null;
516
+ const cacheKey = [
517
+ path,
518
+ descriptor.model,
519
+ context.options.locale ?? "",
520
+ ].join("::");
521
+ const existing = cache.get(cacheKey);
522
+ if (existing) {
523
+ return existing;
524
+ }
525
+ const pending = (async () => {
526
+ try {
527
+ const rawCollection = await context.requestJson(path, {
528
+ query: {
529
+ raw: 1,
530
+ includeId: 1,
531
+ resolveModelRefs: 0,
532
+ ...(context.options.locale ? { locale: context.options.locale } : {}),
533
+ },
534
+ });
535
+ const envelope = ensureCollectionEnvelope(rawCollection, path);
536
+ return envelope.items.map((row) => ({
537
+ relationId: extractId(row) ?? "",
538
+ modelId: extractModelRefId(row, descriptor.model, {
539
+ allowObjectIdFallback: false,
540
+ }) ?? null,
541
+ }));
542
+ }
543
+ catch {
544
+ return null;
545
+ }
546
+ })();
547
+ cache.set(cacheKey, pending);
548
+ return pending;
549
+ }
511
550
  async function normalizeModelRef(modelName, id, context, trail, isCollectionItem, inlineValue) {
512
551
  if (!id)
513
552
  return null;
@@ -610,7 +649,7 @@ async function normalizeObjectField(descriptor, path, raw, context, trail, isCol
610
649
  ...(context.options.locale ? { locale: context.options.locale } : {}),
611
650
  },
612
651
  });
613
- const normalizedChild = await normalizeArrayField(propertyDescriptor, childPath, childRaw, context, trail);
652
+ const normalizedChild = await normalizeArrayField(propertyDescriptor, childPath, childRaw, context, trail, inlineChildRaw !== undefined);
614
653
  output[propertyName] = coerceCustomTypeValue(propertyDescriptor, normalizedChild, {
615
654
  locale: context.options.locale,
616
655
  defaultLocale: context.options.defaultLocale,
@@ -653,7 +692,7 @@ async function normalizeObjectField(descriptor, path, raw, context, trail, isCol
653
692
  }
654
693
  return maybeAttachAssetUrl(output, context.options.assetUrlBuilder);
655
694
  }
656
- async function normalizeArrayField(descriptor, path, raw, context, trail) {
695
+ async function normalizeArrayField(descriptor, path, raw, context, trail, inlineResolvedItems = false) {
657
696
  const envelope = ensureCollectionEnvelope(raw, path);
658
697
  const itemDescriptor = descriptor.items;
659
698
  if (isScalarDescriptor(itemDescriptor)) {
@@ -673,14 +712,29 @@ async function normalizeArrayField(descriptor, path, raw, context, trail) {
673
712
  }
674
713
  if (isModelRefDescriptor(itemDescriptor)) {
675
714
  const inlineModelDescriptor = context.modelDescriptors.get(itemDescriptor.model);
676
- return mapWithConcurrency(envelope.items, context.options.modelNormalizationConcurrency, async (row) => {
715
+ const collectionIdentities = await readCanvasModelRefCollectionIdentities(path, itemDescriptor, context);
716
+ return mapWithConcurrency(envelope.items, context.options.modelNormalizationConcurrency, async (row, index) => {
677
717
  const inlineModel = inlineModelDescriptor
678
718
  ? extractInlineModelObject(row, inlineModelDescriptor)
679
719
  : null;
680
720
  const refId = extractModelRefId(row, itemDescriptor.model, {
681
721
  allowObjectIdFallback: false,
682
722
  }) ?? (inlineModel ? extractId(inlineModel) : null);
683
- return normalizeModelRef(itemDescriptor.model, refId, context, trail, true, row);
723
+ const normalized = await normalizeModelRef(itemDescriptor.model, refId, context, trail, true, row);
724
+ const collectionItemId = collectionIdentities?.[index]?.relationId ??
725
+ (inlineResolvedItems ? null : extractId(row));
726
+ if (!collectionItemId ||
727
+ !normalized ||
728
+ typeof normalized !== "object" ||
729
+ Array.isArray(normalized)) {
730
+ return normalized;
731
+ }
732
+ const normalizedClone = {
733
+ ...normalized,
734
+ };
735
+ registerCms0CollectionItemIdentity(normalizedClone, collectionItemId);
736
+ delete normalizedClone.__cms0CanvasCollectionItemId;
737
+ return normalizedClone;
684
738
  });
685
739
  }
686
740
  if (isObjectDescriptor(itemDescriptor)) {
@@ -1068,7 +1122,19 @@ function validateResult(resource, result, descriptor, includeIdMode, zodSchemas,
1068
1122
  throw new Error(formatValidationError(modelName, parsed.error));
1069
1123
  }
1070
1124
  }
1071
- async function runResource(resource, descriptor, baseUrl, apiKey, defaultLocale, assetUrlBuilder, zodSchemas, modelZodSchemas, globalIncludeId, sharedModelInflightCache, options, byId) {
1125
+ function attachResourceProvenance(resource, value) {
1126
+ return attachResourceProvenanceWithOptions(resource, value);
1127
+ }
1128
+ function attachResourceProvenanceWithOptions(resource, value, options) {
1129
+ if (value === null || value === undefined)
1130
+ return value;
1131
+ enableCms0ProvenanceTracking(true);
1132
+ const rootId = resource.kind === "root"
1133
+ ? resource.key
1134
+ : resource.modelName ?? resource.key;
1135
+ return attachCms0ProvenanceRoot(value, rootId, options);
1136
+ }
1137
+ async function runResource(resource, descriptor, baseUrl, apiKey, defaultLocale, assetUrlBuilder, zodSchemas, modelZodSchemas, globalIncludeId, sharedModelInflightCache, options, byId, provenanceOptions) {
1072
1138
  const responseMode = options?.response ?? "normalized";
1073
1139
  const includeIdMode = resolveIncludeIdMode(options?.includeId, globalIncludeId);
1074
1140
  const resolveModelRefs = options?.resolveModelRefs !== false;
@@ -1154,6 +1220,9 @@ async function runResource(resource, descriptor, baseUrl, apiKey, defaultLocale,
1154
1220
  modelDescriptors,
1155
1221
  modelCache: new Map(),
1156
1222
  sharedModelInflightCache,
1223
+ canvasModelRefCollectionIdentityCache: useResolvedEndpoint && isCms0CanvasTransportEnabled()
1224
+ ? new Map()
1225
+ : undefined,
1157
1226
  options: {
1158
1227
  includeIdMode,
1159
1228
  resolveModelRefs,
@@ -1178,16 +1247,16 @@ async function runResource(resource, descriptor, baseUrl, apiKey, defaultLocale,
1178
1247
  const projectedItems = applyFieldProjection(normalizedItems, options?.fields, options?.exclude);
1179
1248
  validateResult(resource, normalizedItems, descriptor, includeIdMode, zodSchemas, modelZodSchemas);
1180
1249
  return {
1181
- items: Array.isArray(projectedItems) ? projectedItems : normalizedItems,
1250
+ items: attachResourceProvenanceWithOptions(resource, Array.isArray(projectedItems) ? projectedItems : normalizedItems, provenanceOptions),
1182
1251
  total: envelope.total,
1183
1252
  };
1184
1253
  }
1185
1254
  validateResult(resource, normalizedItems, descriptor, includeIdMode, zodSchemas, modelZodSchemas);
1186
- return normalizedItems;
1255
+ return attachResourceProvenanceWithOptions(resource, normalizedItems, provenanceOptions);
1187
1256
  }
1188
1257
  const normalized = await normalizeField(resource.descriptor, normalizePath, rawData, context, new Set(), false);
1189
1258
  validateResult(resource, normalized, descriptor, includeIdMode, zodSchemas, modelZodSchemas);
1190
- return applyFieldProjection(normalized, options?.fields, options?.exclude);
1259
+ return attachResourceProvenanceWithOptions(resource, applyFieldProjection(normalized, options?.fields, options?.exclude), provenanceOptions);
1191
1260
  }
1192
1261
  export function createCmsClient(descriptor, config) {
1193
1262
  const baseUrl = normalizeBaseUrl(config.apiConfig?.baseUrl);
@@ -1223,6 +1292,29 @@ export function createCmsClient(descriptor, config) {
1223
1292
  defaultLocale: config.defaultLocale,
1224
1293
  includeIdDefault: !!config.includeId,
1225
1294
  };
1295
+ const readRoot = async (rootId, options) => {
1296
+ const entry = registry.roots.get(rootId) ??
1297
+ registry.rootsCaseInsensitive.get(rootId.toLowerCase());
1298
+ if (!entry) {
1299
+ throw new Error(`cms0: unknown root '${rootId}' for Canvas read-back.`);
1300
+ }
1301
+ return runResource(entry, descriptor, baseUrl, options?.apiKey ?? apiKey, config.defaultLocale, assetUrlBuilder, zodSchemas, modelZodSchemas, globalIncludeId, sharedModelInflightCache, {
1302
+ includeId: options?.includeId ?? true,
1303
+ locale: options?.locale ?? "all",
1304
+ }, undefined, {
1305
+ registerLiveRoot: false,
1306
+ });
1307
+ };
1308
+ globalThis.__CMS0_CANVAS_CLIENT__ = {
1309
+ apiBaseUrl: baseUrl,
1310
+ locales: clientMeta.locales,
1311
+ defaultLocale: clientMeta.defaultLocale,
1312
+ publishTriggerId: typeof config.canvas?.publishTriggerId === "string" &&
1313
+ config.canvas.publishTriggerId.trim().length > 0
1314
+ ? config.canvas.publishTriggerId.trim()
1315
+ : undefined,
1316
+ readRoot,
1317
+ };
1226
1318
  const rootProxyTarget = {
1227
1319
  models: modelsProxy,
1228
1320
  meta: clientMeta,
@@ -1250,4 +1342,5 @@ export function createCmsClient(descriptor, config) {
1250
1342
  export function cms0(config) {
1251
1343
  return createCmsClient(schemaDescriptor, config);
1252
1344
  }
1345
+ export { activateCms0CanvasTransport, attachCms0ProvenanceRoot, captureCms0Provenance, enableCms0ProvenanceTracking, readCms0CanvasTransportCollectionItemId, readCms0CollectionItemIdentity, registerCms0LiveRoot, registerCms0CollectionItemIdentity, readCms0LiveRoots, readCms0ProvenanceValueMeta, restoreCms0CanvasTransportMetadata, unwrapCms0CanvasTransportValue, } from "./provenance.js";
1253
1346
  export { resolveLocalized, toNextMetadata, toOpenGraph, toTwitter, } from "./seo.js";
@@ -68,6 +68,65 @@ function collectModelNames(sourceFiles) {
68
68
  });
69
69
  return names;
70
70
  }
71
+ function isDescriptorSourceFile(filePath) {
72
+ if (filePath.includes("node_modules"))
73
+ return false;
74
+ if (filePath.endsWith(".d.ts"))
75
+ return false;
76
+ return true;
77
+ }
78
+ function collectTypeSourceFiles(type, sourceFiles, visited) {
79
+ const { base } = unwrapOptional(type);
80
+ const aliasSymbol = base.getAliasSymbol();
81
+ const symbol = base.getSymbol();
82
+ const key = `${aliasSymbol?.getName() ?? symbol?.getName() ?? "<anon>"}:${base.getText()}`;
83
+ if (visited.has(key))
84
+ return;
85
+ visited.add(key);
86
+ if (base.isUnion()) {
87
+ base
88
+ .getUnionTypes()
89
+ .forEach((entry) => collectTypeSourceFiles(entry, sourceFiles, visited));
90
+ return;
91
+ }
92
+ if (base.isArray()) {
93
+ const elem = base.getArrayElementTypeOrThrow();
94
+ collectTypeSourceFiles(elem, sourceFiles, visited);
95
+ return;
96
+ }
97
+ const customName = resolveCustomTypeName(base);
98
+ if (customName) {
99
+ return;
100
+ }
101
+ const declarations = [
102
+ ...(aliasSymbol?.getDeclarations() ?? []),
103
+ ...(symbol?.getDeclarations() ?? []),
104
+ ];
105
+ declarations.forEach((decl) => {
106
+ const sourceFile = decl.getSourceFile();
107
+ const filePath = path.resolve(sourceFile.getFilePath());
108
+ if (!isDescriptorSourceFile(filePath))
109
+ return;
110
+ sourceFiles.set(filePath, sourceFile);
111
+ });
112
+ if (base.isObject()) {
113
+ base.getProperties().forEach((prop) => {
114
+ const decl = prop.getDeclarations()[0];
115
+ if (!decl)
116
+ return;
117
+ collectTypeSourceFiles(decl.getType(), sourceFiles, visited);
118
+ });
119
+ }
120
+ }
121
+ function collectRelevantSourceFiles(rootType, entrySourceFile) {
122
+ const sourceFiles = new Map();
123
+ const entryFilePath = path.resolve(entrySourceFile.getFilePath());
124
+ if (isDescriptorSourceFile(entryFilePath)) {
125
+ sourceFiles.set(entryFilePath, entrySourceFile);
126
+ }
127
+ collectTypeSourceFiles(rootType, sourceFiles, new Set());
128
+ return Array.from(sourceFiles.values());
129
+ }
71
130
  function collectReferencedModelNames(type, names, visited, ctx) {
72
131
  const { base } = unwrapOptional(type);
73
132
  const key = `${ctx}:${base.getText()}`;
@@ -499,19 +558,15 @@ function buildDescriptorAlt(resolved) {
499
558
  fs.existsSync(resolved.entryFile)) {
500
559
  project.addSourceFileAtPath(resolved.entryFile);
501
560
  }
502
- const sourceFiles = project.getSourceFiles().filter((sf) => {
503
- const filePath = path.resolve(sf.getFilePath());
504
- if (filePath.includes("node_modules"))
505
- return false;
506
- if (filePath.endsWith(".d.ts"))
507
- return false;
508
- const rel = path.relative(path.dirname(resolved.entryFile), filePath);
509
- return rel && !rel.startsWith("..") && !path.isAbsolute(rel);
510
- });
511
- const invocation = findRootInvocation(sourceFiles);
561
+ const entrySourceFile = project.getSourceFile(resolved.entryFile);
562
+ if (!entrySourceFile) {
563
+ throw new Error(`Could not load cms0 entry file: ${resolved.entryFile}`);
564
+ }
565
+ const invocation = findRootInvocation([entrySourceFile]);
512
566
  if (!invocation)
513
567
  throw new Error("Could not locate cms0<T>() invocation in project sources.");
514
568
  const rootType = invocation.rootType;
569
+ const sourceFiles = collectRelevantSourceFiles(rootType, entrySourceFile);
515
570
  // Restrict models to only those referenced by the root type tree and exported in scope.
516
571
  const exportedModelNames = collectModelNames(sourceFiles);
517
572
  const collisions = Array.from(customTypeNames).filter((name) => exportedModelNames.has(name));
@@ -1,4 +1,21 @@
1
1
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2
+ class PublishHttpError extends Error {
3
+ status;
4
+ constructor(status) {
5
+ super(`status ${status}`);
6
+ this.name = "PublishHttpError";
7
+ this.status = status;
8
+ }
9
+ }
10
+ function isRetryablePublishStatus(status) {
11
+ return status === 408 || status === 425 || status === 429 || status >= 500;
12
+ }
13
+ function shouldRetryPublishError(error) {
14
+ if (error instanceof PublishHttpError) {
15
+ return isRetryablePublishStatus(error.status);
16
+ }
17
+ return true;
18
+ }
2
19
  const startSpinner = (label) => {
3
20
  if (!process.stdout.isTTY) {
4
21
  console.log(`${label}...`);
@@ -73,7 +90,7 @@ async function publishDescriptor(resolved, descriptor) {
73
90
  body: JSON.stringify(payload),
74
91
  });
75
92
  if (!res.ok) {
76
- throw new Error(`status ${res.status}`);
93
+ throw new PublishHttpError(res.status);
77
94
  }
78
95
  const ready = await waitForAdminReady(resolved.apiBaseUrl.replace(/\/$/, ""), headers);
79
96
  if (!ready) {
@@ -83,13 +100,15 @@ async function publishDescriptor(resolved, descriptor) {
83
100
  return;
84
101
  }
85
102
  catch (err) {
103
+ const canRetry = attempt < maxAttempts && shouldRetryPublishError(err);
86
104
  const waitMs = Math.min(2000 * attempt, 5000);
87
- console.warn(`cms0: publish attempt ${attempt}/${maxAttempts} failed (${err instanceof Error ? err.message : err}).` + (attempt < maxAttempts ? ` retrying in ${waitMs}ms...` : ""));
88
- if (attempt < maxAttempts) {
105
+ console.warn(`cms0: publish attempt ${attempt}/${maxAttempts} failed (${err instanceof Error ? err.message : err}).` + (canRetry ? ` retrying in ${waitMs}ms...` : ""));
106
+ if (canRetry) {
89
107
  await new Promise((r) => setTimeout(r, waitMs));
90
108
  }
91
109
  else {
92
110
  console.warn("cms0: giving up on publishing descriptor for now.");
111
+ return;
93
112
  }
94
113
  }
95
114
  }
@@ -98,4 +117,4 @@ async function publishDescriptor(resolved, descriptor) {
98
117
  console.warn(`cms0: failed to publish descriptor ${err instanceof Error ? err.message : err}`, err);
99
118
  }
100
119
  }
101
- export { publishDescriptor };
120
+ export { isRetryablePublishStatus, publishDescriptor };