@cms-lab/core 1.2.4 → 1.2.5

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/README.md CHANGED
@@ -40,6 +40,10 @@ routes: [
40
40
  probe hit a known-good page or endpoint while normal route probes still use
41
41
  `site.url`.
42
42
 
43
+ `checks.relationships` supports narrow equality joins across normalized
44
+ documents, for example requiring each `menu_item` to have at least one matching
45
+ `pricing` row by `id -> menu_item_id`.
46
+
43
47
  ## Release History
44
48
 
45
49
  See the repository [changelog](https://github.com/i-afaqrashid/cms-lab/blob/main/CHANGELOG.md)
package/dist/index.d.ts CHANGED
@@ -41,6 +41,16 @@ type RequiredFieldRule = {
41
41
  path: string;
42
42
  severity?: "error" | "warning";
43
43
  };
44
+ type RelationshipRule = {
45
+ from: string;
46
+ to: string;
47
+ where: {
48
+ fromField: string;
49
+ toField: string;
50
+ };
51
+ min?: number;
52
+ severity?: DiagnosticSeverity;
53
+ };
44
54
  type PrismicCmsProviderConfig = {
45
55
  provider: "prismic";
46
56
  repositoryName: string;
@@ -142,6 +152,7 @@ type CmsLabConfig = {
142
152
  fields?: boolean | {
143
153
  required?: RequiredFieldRule[];
144
154
  };
155
+ relationships?: RelationshipRule[];
145
156
  };
146
157
  };
147
158
  type ScanSummary = {
@@ -156,7 +167,7 @@ type ScanResult = {
156
167
  summary: ScanSummary;
157
168
  };
158
169
  type FetchLike = typeof fetch;
159
- type CheckGroup = "routes" | "seo" | "a11y" | "images" | "fields";
170
+ type CheckGroup = "routes" | "seo" | "a11y" | "images" | "fields" | "relationships";
160
171
  type ScanFilters = {
161
172
  types?: string[];
162
173
  only?: CheckGroup[];
@@ -211,4 +222,4 @@ type ScanDocumentsOptions = {
211
222
  declare function scanDocuments(options: ScanDocumentsOptions): Promise<ScanResult>;
212
223
  declare function resolveSiteHealthUrl(site: CmsLabConfig["site"]): URL;
213
224
 
214
- export { type CMSDocument, type CMSDocumentStatus, type CheckGroup, CmsFetchError, type CmsFieldMappingConfig, type CmsLabConfig, CmsLabError, type CmsProviderConfig, ConfigLoadError, type ContentfulCmsProviderConfig, type ContentfulContentTypeConfig, type Diagnostic, type DiagnosticExplanation, type DiagnosticSeverity, type DirectusCmsProviderConfig, type DirectusCollectionConfig, type FetchLike, type LoadedCmsLabConfig, type PrismicCmsProviderConfig, type ProjectInfo, type RequiredFieldRule, type RouteDefinition, type SanityCmsProviderConfig, type SanityContentTypeConfig, type ScanDocumentsOptions, type ScanFilters, type ScanResult, type ScanSummary, SiteUnreachableError, type StrapiCmsProviderConfig, type StrapiCollectionConfig, type StrapiLocaleConfig, type StrapiSingleTypeConfig, type WordPressCmsProviderConfig, type WordPressContentTypeConfig, createDiagnostic, defineConfig, explainDiagnostic, listDiagnosticExplanations, loadCmsLabConfig, readCmsDataPath, resolveSiteHealthUrl, scanDocuments, strapiRelationSlug, strapiRelationValue, summarizeDiagnostics, validateConfig };
225
+ export { type CMSDocument, type CMSDocumentStatus, type CheckGroup, CmsFetchError, type CmsFieldMappingConfig, type CmsLabConfig, CmsLabError, type CmsProviderConfig, ConfigLoadError, type ContentfulCmsProviderConfig, type ContentfulContentTypeConfig, type Diagnostic, type DiagnosticExplanation, type DiagnosticSeverity, type DirectusCmsProviderConfig, type DirectusCollectionConfig, type FetchLike, type LoadedCmsLabConfig, type PrismicCmsProviderConfig, type ProjectInfo, type RelationshipRule, type RequiredFieldRule, type RouteDefinition, type SanityCmsProviderConfig, type SanityContentTypeConfig, type ScanDocumentsOptions, type ScanFilters, type ScanResult, type ScanSummary, SiteUnreachableError, type StrapiCmsProviderConfig, type StrapiCollectionConfig, type StrapiLocaleConfig, type StrapiSingleTypeConfig, type WordPressCmsProviderConfig, type WordPressContentTypeConfig, createDiagnostic, defineConfig, explainDiagnostic, listDiagnosticExplanations, loadCmsLabConfig, readCmsDataPath, resolveSiteHealthUrl, scanDocuments, strapiRelationSlug, strapiRelationValue, summarizeDiagnostics, validateConfig };
package/dist/index.js CHANGED
@@ -143,6 +143,16 @@ var requiredFieldRuleSchema = z.object({
143
143
  path: z.string().min(1),
144
144
  severity: z.enum(["error", "warning"]).optional()
145
145
  });
146
+ var relationshipRuleSchema = z.object({
147
+ from: z.string().min(1),
148
+ to: z.string().min(1),
149
+ where: z.object({
150
+ fromField: z.string().min(1),
151
+ toField: z.string().min(1)
152
+ }).strict(),
153
+ min: z.number().int().min(0).optional(),
154
+ severity: z.enum(["error", "warning", "info"]).optional()
155
+ }).strict();
146
156
  var checksSchema = z.object({
147
157
  routes: z.boolean().optional(),
148
158
  seo: z.union([
@@ -162,7 +172,8 @@ var checksSchema = z.object({
162
172
  z.object({
163
173
  required: z.array(requiredFieldRuleSchema).optional()
164
174
  })
165
- ]).optional()
175
+ ]).optional(),
176
+ relationships: z.array(relationshipRuleSchema).optional()
166
177
  }).strict().optional();
167
178
  var configSchema = z.object({
168
179
  site: z.object({
@@ -330,6 +341,13 @@ var explanations = [
330
341
  title: "Required CMS field is missing",
331
342
  meaning: "A document is missing a required field declared in cms-lab config.",
332
343
  fix: "Fill the required field in the CMS, or update checks.fields.required if the field is no longer required."
344
+ },
345
+ {
346
+ code: "CMS-RELATIONSHIP-MISSING",
347
+ severity: "warning",
348
+ title: "CMS relationship rule did not match enough records",
349
+ meaning: "A relationship rule expected a document to have one or more matching related records, but cms-lab found fewer than the configured minimum.",
350
+ fix: "Check the related CMS collection, the join fields in checks.relationships, and whether the related content should be published or active."
333
351
  }
334
352
  ];
335
353
  function explainDiagnostic(code) {
@@ -418,6 +436,9 @@ async function scanDocuments(options) {
418
436
  if (shouldRunCheck("fields", options.config, options.filters)) {
419
437
  diagnostics.push(...checkRequiredFields(options.config, documents));
420
438
  }
439
+ if (shouldRunCheck("relationships", options.config, options.filters)) {
440
+ diagnostics.push(...checkRelationships(options.config, documents));
441
+ }
421
442
  return {
422
443
  project: options.project,
423
444
  documents,
@@ -709,6 +730,85 @@ function requiredFieldRules(config) {
709
730
  }
710
731
  return fields.required ?? [];
711
732
  }
733
+ function checkRelationships(config, documents) {
734
+ const rules = relationshipRules(config);
735
+ if (rules.length === 0) {
736
+ return [];
737
+ }
738
+ const documentsByType = /* @__PURE__ */ new Map();
739
+ for (const document of documents) {
740
+ documentsByType.set(document.type, [
741
+ ...documentsByType.get(document.type) ?? [],
742
+ document
743
+ ]);
744
+ }
745
+ const diagnostics = [];
746
+ for (const rule of rules) {
747
+ const min = rule.min ?? 1;
748
+ const targets = documentsByType.get(rule.to) ?? [];
749
+ for (const document of documentsByType.get(rule.from) ?? []) {
750
+ const fromValues = relationshipValues(document, rule.where.fromField);
751
+ const matchCount = targets.filter(
752
+ (target) => hasRelationshipMatch(
753
+ fromValues,
754
+ relationshipValues(target, rule.where.toField)
755
+ )
756
+ ).length;
757
+ if (matchCount >= min) {
758
+ continue;
759
+ }
760
+ diagnostics.push(
761
+ createDiagnostic({
762
+ severity: rule.severity ?? "warning",
763
+ code: "CMS-RELATIONSHIP-MISSING",
764
+ message: `Document ${document.id} of type ${document.type} has ${matchCount} ${rule.to} records matching ${rule.where.fromField} -> ${rule.where.toField}; expected at least ${min}`,
765
+ path: `relationships.${rule.from}.${rule.to}`,
766
+ source: sourceFor(config, document)
767
+ })
768
+ );
769
+ }
770
+ }
771
+ return diagnostics;
772
+ }
773
+ function relationshipRules(config) {
774
+ return config.checks?.relationships ?? [];
775
+ }
776
+ function hasRelationshipMatch(fromValues, toValues) {
777
+ if (fromValues.length === 0 || toValues.length === 0) {
778
+ return false;
779
+ }
780
+ const targetValues = new Set(toValues);
781
+ return fromValues.some((value) => targetValues.has(value));
782
+ }
783
+ function relationshipValues(document, path) {
784
+ const dataValue = readCmsDataPath(document.data, path);
785
+ if (dataValue !== void 0) {
786
+ return normalizeRelationshipValues(dataValue);
787
+ }
788
+ return normalizeRelationshipValues(
789
+ document[path]
790
+ );
791
+ }
792
+ function normalizeRelationshipValues(value) {
793
+ if (value === void 0 || value === null) {
794
+ return [];
795
+ }
796
+ if (Array.isArray(value)) {
797
+ return value.flatMap((item) => normalizeRelationshipValues(item));
798
+ }
799
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
800
+ return [String(value)];
801
+ }
802
+ const record = asRecord3(value);
803
+ if (!record) {
804
+ return [];
805
+ }
806
+ return [
807
+ ...normalizeRelationshipValues(record.id),
808
+ ...normalizeRelationshipValues(record.uid),
809
+ ...normalizeRelationshipValues(record.slug)
810
+ ];
811
+ }
712
812
  function hasSeoValue(config, data, kind) {
713
813
  return seoFieldPaths(config.cms.provider, kind).some(
714
814
  (path) => !isBlank(readCmsDataPath(data, path))
@@ -836,6 +936,9 @@ function shouldRunCheck(group, config, filters) {
836
936
  if (group === "images") {
837
937
  return isCheckEnabled(config.checks?.images, true);
838
938
  }
939
+ if (group === "relationships") {
940
+ return true;
941
+ }
839
942
  return isCheckEnabled(config.checks?.fields, true);
840
943
  }
841
944
  function shouldRunImageAltTextCheck(config, filters) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cms-lab/core",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "type": "module",
5
5
  "description": "Core config, scan, diagnostics, and checks for cms-lab.",
6
6
  "license": "MIT",