Skip to content

Commit 5dd254d

Browse files
committed
feat(audit): validate referenced entry content types in entries audit
1 parent 2141589 commit 5dd254d

File tree

8 files changed

+1823
-1424
lines changed

8 files changed

+1823
-1424
lines changed

.talismanrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
fileignoreconfig:
22
- filename: package-lock.json
3-
checksum: 8d0430a55a8bbfe3f1be5264dfcc914616175bbd0c0ddb33b0796ccc811dbd91
3+
checksum: 1b011574c5a640f7132f2dcabfced269cb497ddd3270524ec32abe3cb4a95cb5
44
- filename: pnpm-lock.yaml
5-
checksum: 9e6a3c280cfd7356f1440a592c8b4f1d6294f4ae133696680c253a029be017c7
5+
checksum: 91ffcd3364bcbef7dad0d25547849a572dc9ebd004999c3ede85c7730959a2e5
66
- filename: packages/contentstack-bootstrap/src/bootstrap/utils.ts
77
checksum: 6e6fb00bb11b03141e5ad27eeaa4af9718dc30520c3e73970bc208cc0ba2a7d2
88
version: '1.0'

package-lock.json

Lines changed: 827 additions & 685 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/contentstack-audit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/cli-audit",
3-
"version": "1.17.1",
3+
"version": "1.18.0",
44
"description": "Contentstack audit plugin",
55
"author": "Contentstack CLI",
66
"homepage": "https://github.com/contentstack/cli",

packages/contentstack-audit/src/modules/entries.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,27 @@ export default class Entries {
9393
return 'entries';
9494
}
9595

96+
/**
97+
* Returns whether a referenced entry's content type is allowed by the schema's reference_to.
98+
* @param refCtUid - Content type UID of the referenced entry (e.g. from _content_type_uid)
99+
* @param referenceTo - Schema's reference_to (string or string[])
100+
* @param configOverride - Optional config with skipRefs; falls back to this.config
101+
* @returns true if allowed or check cannot be performed; false if refCtUid is not in reference_to
102+
*/
103+
protected isRefContentTypeAllowed(
104+
refCtUid: string | undefined,
105+
referenceTo: string | string[] | undefined,
106+
configOverride?: { skipRefs?: string[] },
107+
): boolean {
108+
if (refCtUid === undefined) return true;
109+
const skipRefs = configOverride?.skipRefs ?? (this.config as any).skipRefs ?? [];
110+
if (Array.isArray(skipRefs) && skipRefs.includes(refCtUid)) return true;
111+
if (referenceTo === undefined || referenceTo === null) return true;
112+
const refToList = Array.isArray(referenceTo) ? referenceTo : [referenceTo];
113+
if (refToList.length === 0) return false;
114+
return refToList.includes(refCtUid);
115+
}
116+
96117
/**
97118
* The `run` function checks if a folder path exists, sets the schema based on the module name,
98119
* iterates over the schema and looks for references, and returns a list of missing references.
@@ -877,8 +898,9 @@ export default class Entries {
877898

878899
const missingRefs: Record<string, any>[] = [];
879900
const { uid: data_type, display_name, reference_to } = fieldStructure;
901+
const refToList = Array.isArray(reference_to) ? reference_to : reference_to != null ? [reference_to] : [];
880902
log.debug(`Reference field UID: ${data_type}`);
881-
log.debug(`Reference to: ${reference_to?.join(', ') || 'none'}`);
903+
log.debug(`Reference to: ${refToList.join(', ') || 'none'}`);
882904
log.debug(`Found ${field?.length || 0} references to validate`);
883905

884906
for (const index in field ?? []) {
@@ -897,7 +919,13 @@ export default class Entries {
897919
missingRefs.push(reference);
898920
}
899921
} else {
900-
log.debug(`Reference ${reference} is valid`);
922+
const refCtUid = refExist.ctUid;
923+
if (!this.isRefContentTypeAllowed(refCtUid, reference_to)) {
924+
log.debug(`Reference ${reference} has wrong content type: ${refCtUid} not in reference_to`);
925+
missingRefs.push(refToList.length === 1 ? { uid: reference, _content_type_uid: refCtUid } : reference);
926+
} else {
927+
log.debug(`Reference ${reference} is valid`);
928+
}
901929
}
902930
}
903931
// NOTE Can skip specific references keys (Ex, system defined keys can be skipped)
@@ -910,7 +938,13 @@ export default class Entries {
910938
log.debug(`Missing reference: ${uid}`);
911939
missingRefs.push(reference);
912940
} else {
913-
log.debug(`Reference ${uid} is valid`);
941+
const refCtUid = reference._content_type_uid ?? refExist.ctUid;
942+
if (!this.isRefContentTypeAllowed(refCtUid, reference_to)) {
943+
log.debug(`Reference ${uid} has wrong content type: ${refCtUid} not in reference_to`);
944+
missingRefs.push(refToList.length === 1 ? { uid, _content_type_uid: refCtUid } : reference);
945+
} else {
946+
log.debug(`Reference ${uid} is valid`);
947+
}
914948
}
915949
}
916950
}
@@ -1685,6 +1719,16 @@ export default class Entries {
16851719
missingRefs.push(reference);
16861720
}
16871721
} else {
1722+
const refCtUid = reference._content_type_uid ?? refExist.ctUid;
1723+
if (!this.isRefContentTypeAllowed(refCtUid, reference_to)) {
1724+
log.debug(`Blt reference ${reference} has wrong content type: ${refCtUid} not in reference_to`);
1725+
missingRefs.push(
1726+
Array.isArray(reference_to) && reference_to.length === 1
1727+
? { uid: reference, _content_type_uid: refCtUid }
1728+
: reference,
1729+
);
1730+
return null;
1731+
}
16881732
log.debug(`Blt reference ${reference} is valid`);
16891733
return { uid: reference, _content_type_uid: refExist.ctUid };
16901734
}
@@ -1696,6 +1740,16 @@ export default class Entries {
16961740
missingRefs.push(reference);
16971741
return null;
16981742
} else {
1743+
const refCtUid = reference._content_type_uid ?? refExist.ctUid;
1744+
if (!this.isRefContentTypeAllowed(refCtUid, reference_to)) {
1745+
log.debug(`Reference ${uid} has wrong content type: ${refCtUid} not in reference_to`);
1746+
missingRefs.push(
1747+
Array.isArray(reference_to) && reference_to.length === 1
1748+
? { uid, _content_type_uid: refCtUid }
1749+
: reference,
1750+
);
1751+
return null;
1752+
}
16991753
log.debug(`Reference ${uid} is valid`);
17001754
return reference;
17011755
}
@@ -1827,6 +1881,25 @@ export default class Entries {
18271881
log.debug(`JSON reference check failed for entry: ${entryUid}`);
18281882
return null;
18291883
} else {
1884+
const refCtUid = contentTypeUid ?? refExist.ctUid;
1885+
const referenceTo = (schema as any).reference_to;
1886+
if (!this.isRefContentTypeAllowed(refCtUid, referenceTo)) {
1887+
log.debug(`JSON RTE embed ${entryUid} has wrong content type: ${refCtUid} not in reference_to`);
1888+
this.missingRefs[this.currentUid].push({
1889+
tree,
1890+
uid: this.currentUid,
1891+
name: this.currentTitle,
1892+
data_type: schema.data_type,
1893+
display_name: schema.display_name,
1894+
fixStatus: this.fix ? 'Fixed' : undefined,
1895+
treeStr: tree
1896+
.map(({ name }) => name)
1897+
.filter((val) => val)
1898+
.join(' ➜ '),
1899+
missingRefs: [{ uid: entryUid, 'content-type-uid': refCtUid }],
1900+
});
1901+
return this.fix ? null : true;
1902+
}
18301903
log.debug(`Entry reference ${entryUid} is valid`);
18311904
}
18321905
} else {

packages/contentstack-audit/test/unit/modules/entries.test.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,55 @@ describe('Entries module', () => {
10411041

10421042
expect(result).to.be.an('array'); // Should return array of missing references
10431043
});
1044+
1045+
fancy
1046+
.stdout({ print: process.env.PRINT === 'true' || false })
1047+
.it('should flag reference when ref entry has wrong content type (ct2 ref when reference_to is ct1)', () => {
1048+
const ctInstance = new Entries(constructorParam);
1049+
(ctInstance as any).currentUid = 'test-entry';
1050+
(ctInstance as any).entryMetaData = [{ uid: 'blt123', ctUid: 'ct2' }]; // Entry exists but is ct2
1051+
1052+
const referenceFieldSchema = { uid: 'ref', display_name: 'Ref', data_type: 'reference', reference_to: ['ct1'] };
1053+
const entryData = [{ uid: 'blt123', _content_type_uid: 'ct2' }];
1054+
const tree = [{ uid: 'test-entry', name: 'Test Entry' }];
1055+
1056+
const result = ctInstance.validateReferenceValues(tree, referenceFieldSchema as any, entryData);
1057+
1058+
expect(result).to.have.length(1);
1059+
expect(result[0].missingRefs).to.deep.include({ uid: 'blt123', _content_type_uid: 'ct2' });
1060+
});
1061+
1062+
fancy
1063+
.stdout({ print: process.env.PRINT === 'true' || false })
1064+
.it('should not flag reference when ref entry has correct content type (ct1 ref when reference_to is ct1)', () => {
1065+
const ctInstance = new Entries(constructorParam);
1066+
(ctInstance as any).currentUid = 'test-entry';
1067+
(ctInstance as any).entryMetaData = [{ uid: 'blt123', ctUid: 'ct1' }];
1068+
1069+
const referenceFieldSchema = { uid: 'ref', display_name: 'Ref', data_type: 'reference', reference_to: ['ct1'] };
1070+
const entryData = [{ uid: 'blt123', _content_type_uid: 'ct1' }];
1071+
const tree = [{ uid: 'test-entry', name: 'Test Entry' }];
1072+
1073+
const result = ctInstance.validateReferenceValues(tree, referenceFieldSchema as any, entryData);
1074+
1075+
expect(result).to.have.length(0);
1076+
});
1077+
1078+
fancy
1079+
.stdout({ print: process.env.PRINT === 'true' || false })
1080+
.it('should normalize reference_to string and allow matching ref (ct1 when reference_to is string ct1)', () => {
1081+
const ctInstance = new Entries(constructorParam);
1082+
(ctInstance as any).currentUid = 'test-entry';
1083+
(ctInstance as any).entryMetaData = [{ uid: 'blt456', ctUid: 'ct1' }];
1084+
1085+
const referenceFieldSchema = { uid: 'ref', display_name: 'Ref', data_type: 'reference', reference_to: 'ct1' };
1086+
const entryData = [{ uid: 'blt456', _content_type_uid: 'ct1' }];
1087+
const tree = [{ uid: 'test-entry', name: 'Test Entry' }];
1088+
1089+
const result = ctInstance.validateReferenceValues(tree, referenceFieldSchema as any, entryData);
1090+
1091+
expect(result).to.have.length(0);
1092+
});
10441093
});
10451094

10461095
describe('validateModularBlocksField method', () => {
@@ -1365,5 +1414,130 @@ describe('Entries module', () => {
13651414

13661415
// Should not throw - method is void
13671416
});
1417+
1418+
fancy
1419+
.stdout({ print: process.env.PRINT === 'true' || false })
1420+
.it('should flag JSON RTE embed when ref has wrong content type (ct2 when reference_to is ct1,sys_assets)', () => {
1421+
const ctInstance = new Entries(constructorParam);
1422+
(ctInstance as any).currentUid = 'test-entry';
1423+
(ctInstance as any).missingRefs = { 'test-entry': [] };
1424+
(ctInstance as any).entryMetaData = [{ uid: 'blt123', ctUid: 'ct2' }];
1425+
1426+
const schema = {
1427+
uid: 'json_rte',
1428+
display_name: 'JSON RTE',
1429+
data_type: 'richtext',
1430+
reference_to: ['ct1', 'sys_assets'],
1431+
};
1432+
const child = {
1433+
type: 'embed',
1434+
uid: 'child-uid',
1435+
attrs: { 'entry-uid': 'blt123', 'content-type-uid': 'ct2' },
1436+
children: [],
1437+
};
1438+
const tree: Record<string, unknown>[] = [];
1439+
1440+
(ctInstance as any).jsonRefCheck(tree, schema, child);
1441+
1442+
expect((ctInstance as any).missingRefs['test-entry']).to.have.length(1);
1443+
expect((ctInstance as any).missingRefs['test-entry'][0].missingRefs).to.deep.include({
1444+
uid: 'blt123',
1445+
'content-type-uid': 'ct2',
1446+
});
1447+
});
1448+
});
1449+
1450+
describe('fixMissingReferences method', () => {
1451+
fancy
1452+
.stdout({ print: process.env.PRINT === 'true' || false })
1453+
.it('should filter out ref when ref has wrong content type (ct2 when reference_to is ct1)', () => {
1454+
const ctInstance = new Entries({ ...constructorParam, fix: true });
1455+
(ctInstance as any).currentUid = 'test-entry';
1456+
(ctInstance as any).missingRefs = { 'test-entry': [] };
1457+
(ctInstance as any).entryMetaData = [{ uid: 'blt123', ctUid: 'ct2' }];
1458+
1459+
const field = {
1460+
uid: 'ref_field',
1461+
display_name: 'Ref',
1462+
data_type: 'reference',
1463+
reference_to: ['ct1'],
1464+
};
1465+
const entry = [{ uid: 'blt123', _content_type_uid: 'ct2' }];
1466+
const tree = [{ uid: 'test-entry', name: 'Test Entry' }];
1467+
1468+
const result = ctInstance.fixMissingReferences(tree, field as any, entry);
1469+
1470+
expect(result).to.have.length(0);
1471+
expect((ctInstance as any).missingRefs['test-entry']).to.have.length(1);
1472+
expect((ctInstance as any).missingRefs['test-entry'][0].missingRefs).to.deep.include({
1473+
uid: 'blt123',
1474+
_content_type_uid: 'ct2',
1475+
});
1476+
});
1477+
});
1478+
1479+
describe('jsonRefCheck in fix mode', () => {
1480+
fancy
1481+
.stdout({ print: process.env.PRINT === 'true' || false })
1482+
.it('should return null when ref has wrong content type (fix mode)', () => {
1483+
const ctInstance = new Entries({ ...constructorParam, fix: true });
1484+
(ctInstance as any).currentUid = 'test-entry';
1485+
(ctInstance as any).missingRefs = { 'test-entry': [] };
1486+
(ctInstance as any).entryMetaData = [{ uid: 'blt123', ctUid: 'ct2' }];
1487+
1488+
const schema = {
1489+
uid: 'json_rte',
1490+
display_name: 'JSON RTE',
1491+
data_type: 'richtext',
1492+
reference_to: ['ct1'],
1493+
};
1494+
const child = {
1495+
type: 'embed',
1496+
uid: 'child-uid',
1497+
attrs: { 'entry-uid': 'blt123', 'content-type-uid': 'ct2' },
1498+
children: [],
1499+
};
1500+
const tree: Record<string, unknown>[] = [];
1501+
1502+
const result = (ctInstance as any).jsonRefCheck(tree, schema, child);
1503+
1504+
expect(result).to.be.null;
1505+
});
1506+
});
1507+
1508+
describe('isRefContentTypeAllowed helper', () => {
1509+
const callHelper = (refCtUid: string | undefined, referenceTo: string | string[] | undefined) => {
1510+
const ctInstance = new Entries(constructorParam);
1511+
return (ctInstance as any).isRefContentTypeAllowed(refCtUid, referenceTo);
1512+
};
1513+
1514+
fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns true when refCtUid is in reference_to', () => {
1515+
expect(callHelper('ct1', ['ct1', 'ct2'])).to.be.true;
1516+
});
1517+
1518+
fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns false when refCtUid is not in reference_to', () => {
1519+
expect(callHelper('ct2', ['ct1'])).to.be.false;
1520+
});
1521+
1522+
fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns true when reference_to is undefined', () => {
1523+
expect(callHelper('ct1', undefined)).to.be.true;
1524+
});
1525+
1526+
fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('normalizes reference_to string and allows matching refCtUid', () => {
1527+
expect(callHelper('ct1', 'ct1')).to.be.true;
1528+
expect(callHelper('ct2', 'ct1')).to.be.false;
1529+
});
1530+
1531+
fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns false when reference_to is empty array', () => {
1532+
expect(callHelper('ct1', [])).to.be.false;
1533+
});
1534+
1535+
fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns true when refCtUid is undefined', () => {
1536+
expect(callHelper(undefined, ['ct1'])).to.be.true;
1537+
});
1538+
1539+
fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns true when refCtUid is in skipRefs', () => {
1540+
expect(callHelper('sys_assets', ['ct1'])).to.be.true;
1541+
});
13681542
});
13691543
});

packages/contentstack-import/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"author": "Contentstack",
66
"bugs": "https://github.com/contentstack/cli/issues",
77
"dependencies": {
8-
"@contentstack/cli-audit": "~1.17.1",
8+
"@contentstack/cli-audit": "~1.18.0",
99
"@contentstack/cli-command": "~1.7.2",
1010
"@contentstack/cli-utilities": "~1.17.4",
1111
"@contentstack/cli-variants": "~1.3.7",

packages/contentstack/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"prepack": "pnpm compile && oclif manifest && oclif readme"
2323
},
2424
"dependencies": {
25-
"@contentstack/cli-audit": "~1.17.1",
25+
"@contentstack/cli-audit": "~1.18.0",
2626
"@contentstack/cli-cm-export": "~1.23.2",
2727
"@contentstack/cli-cm-import": "~1.31.3",
2828
"@contentstack/cli-auth": "~1.7.3",

0 commit comments

Comments
 (0)