@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 +4 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.js +105 -1
- package/package.json +1 -1
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) {
|