@cms-lab/core 1.2.3 → 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;
@@ -73,6 +83,7 @@ type StrapiCmsProviderConfig = {
73
83
  type DirectusCollectionConfig = {
74
84
  type: string;
75
85
  collection: string;
86
+ routable?: boolean;
76
87
  } & CmsFieldMappingConfig;
77
88
  type DirectusCmsProviderConfig = {
78
89
  provider: "directus";
@@ -141,6 +152,7 @@ type CmsLabConfig = {
141
152
  fields?: boolean | {
142
153
  required?: RequiredFieldRule[];
143
154
  };
155
+ relationships?: RelationshipRule[];
144
156
  };
145
157
  };
146
158
  type ScanSummary = {
@@ -155,7 +167,7 @@ type ScanResult = {
155
167
  summary: ScanSummary;
156
168
  };
157
169
  type FetchLike = typeof fetch;
158
- type CheckGroup = "routes" | "seo" | "a11y" | "images" | "fields";
170
+ type CheckGroup = "routes" | "seo" | "a11y" | "images" | "fields" | "relationships";
159
171
  type ScanFilters = {
160
172
  types?: string[];
161
173
  only?: CheckGroup[];
@@ -210,4 +222,4 @@ type ScanDocumentsOptions = {
210
222
  declare function scanDocuments(options: ScanDocumentsOptions): Promise<ScanResult>;
211
223
  declare function resolveSiteHealthUrl(site: CmsLabConfig["site"]): URL;
212
224
 
213
- 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
@@ -84,6 +84,7 @@ var directusConfigSchema = z.object({
84
84
  z.object({
85
85
  type: z.string().min(1),
86
86
  collection: z.string().min(1),
87
+ routable: z.boolean().optional(),
87
88
  ...cmsFieldMappingShape
88
89
  }).strict()
89
90
  ).min(1)
@@ -142,6 +143,16 @@ var requiredFieldRuleSchema = z.object({
142
143
  path: z.string().min(1),
143
144
  severity: z.enum(["error", "warning"]).optional()
144
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();
145
156
  var checksSchema = z.object({
146
157
  routes: z.boolean().optional(),
147
158
  seo: z.union([
@@ -161,7 +172,8 @@ var checksSchema = z.object({
161
172
  z.object({
162
173
  required: z.array(requiredFieldRuleSchema).optional()
163
174
  })
164
- ]).optional()
175
+ ]).optional(),
176
+ relationships: z.array(relationshipRuleSchema).optional()
165
177
  }).strict().optional();
166
178
  var configSchema = z.object({
167
179
  site: z.object({
@@ -329,6 +341,13 @@ var explanations = [
329
341
  title: "Required CMS field is missing",
330
342
  meaning: "A document is missing a required field declared in cms-lab config.",
331
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."
332
351
  }
333
352
  ];
334
353
  function explainDiagnostic(code) {
@@ -417,6 +436,9 @@ async function scanDocuments(options) {
417
436
  if (shouldRunCheck("fields", options.config, options.filters)) {
418
437
  diagnostics.push(...checkRequiredFields(options.config, documents));
419
438
  }
439
+ if (shouldRunCheck("relationships", options.config, options.filters)) {
440
+ diagnostics.push(...checkRelationships(options.config, documents));
441
+ }
420
442
  return {
421
443
  project: options.project,
422
444
  documents,
@@ -708,6 +730,85 @@ function requiredFieldRules(config) {
708
730
  }
709
731
  return fields.required ?? [];
710
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
+ }
711
812
  function hasSeoValue(config, data, kind) {
712
813
  return seoFieldPaths(config.cms.provider, kind).some(
713
814
  (path) => !isBlank(readCmsDataPath(data, path))
@@ -835,6 +936,9 @@ function shouldRunCheck(group, config, filters) {
835
936
  if (group === "images") {
836
937
  return isCheckEnabled(config.checks?.images, true);
837
938
  }
939
+ if (group === "relationships") {
940
+ return true;
941
+ }
838
942
  return isCheckEnabled(config.checks?.fields, true);
839
943
  }
840
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.3",
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",