Skip to content

Commit bade454

Browse files
committed
feat: validate referenced entry content types in entries audit
1 parent 255d0b4 commit bade454

File tree

8 files changed

+1254
-778
lines changed

8 files changed

+1254
-778
lines changed

.talismanrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ fileignoreconfig:
33
- filename: packages/contentstack-import/src/import/modules/environments.ts
44
checksum: f61c635eaec8026e0cfa80a5ab8272f7946531f6d89505dc0d247b4c7ab0eab7
55
- filename: pnpm-lock.yaml
6-
checksum: c3020538089092e55f086c39cc4c027ef3d48f6c786a217db9c5e49f55ab8380
6+
checksum: 33b0a88264d099a2594bf8f18b8b025b0e15443dce340cd2ab5021ccc9aa84b0
77
- filename: package-lock.json
8-
checksum: 099edd9ec7ed92eb61ce916511ac87e2fc1ff985efe64a25749ac88ba0d3fa7d
8+
checksum: 38142d4c1159342957985368de6d5caf77ab3198a926fe55cdfd95c7c7343100
99
- filename: packages/contentstack-bootstrap/src/bootstrap/utils.ts
1010
checksum: 5ab20e057fa9c4c300f7a882d30e1c68bbc91ed19de520488107e8c37239682a
1111
- filename: packages/contentstack-migration/README.md

package-lock.json

Lines changed: 575 additions & 370 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": "2.0.0-beta.4",
3+
"version": "2.0.0-beta.5",
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
@@ -91,6 +91,27 @@ export default class Entries extends BaseClass {
9191
return 'entries';
9292
}
9393

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

897918
const missingRefs: Record<string, any>[] = [];
898919
const { uid: data_type, display_name, reference_to } = fieldStructure;
920+
const refToList = Array.isArray(reference_to) ? reference_to : reference_to != null ? [reference_to] : [];
899921
log.debug(`Reference field UID: ${data_type}`);
900-
log.debug(`Reference to: ${reference_to?.join(', ') || 'none'}`);
922+
log.debug(`Reference to: ${refToList.join(', ') || 'none'}`);
901923
log.debug(`Found ${field?.length || 0} references to validate`);
902924

903925
for (const index in field ?? []) {
@@ -916,7 +938,13 @@ export default class Entries extends BaseClass {
916938
missingRefs.push(reference);
917939
}
918940
} else {
919-
log.debug(`Reference ${reference} is valid`);
941+
const refCtUid = refExist.ctUid;
942+
if (!this.isRefContentTypeAllowed(refCtUid, reference_to)) {
943+
log.debug(`Reference ${reference} has wrong content type: ${refCtUid} not in reference_to`);
944+
missingRefs.push(refToList.length === 1 ? { uid: reference, _content_type_uid: refCtUid } : reference);
945+
} else {
946+
log.debug(`Reference ${reference} is valid`);
947+
}
920948
}
921949
}
922950
// NOTE Can skip specific references keys (Ex, system defined keys can be skipped)
@@ -929,7 +957,13 @@ export default class Entries extends BaseClass {
929957
log.debug(`Missing reference: ${uid}`);
930958
missingRefs.push(reference);
931959
} else {
932-
log.debug(`Reference ${uid} is valid`);
960+
const refCtUid = reference._content_type_uid ?? refExist.ctUid;
961+
if (!this.isRefContentTypeAllowed(refCtUid, reference_to)) {
962+
log.debug(`Reference ${uid} has wrong content type: ${refCtUid} not in reference_to`);
963+
missingRefs.push(refToList.length === 1 ? { uid, _content_type_uid: refCtUid } : reference);
964+
} else {
965+
log.debug(`Reference ${uid} is valid`);
966+
}
933967
}
934968
}
935969
}
@@ -1704,6 +1738,16 @@ export default class Entries extends BaseClass {
17041738
missingRefs.push(reference);
17051739
}
17061740
} else {
1741+
const refCtUid = reference._content_type_uid ?? refExist.ctUid;
1742+
if (!this.isRefContentTypeAllowed(refCtUid, reference_to)) {
1743+
log.debug(`Blt reference ${reference} has wrong content type: ${refCtUid} not in reference_to`);
1744+
missingRefs.push(
1745+
Array.isArray(reference_to) && reference_to.length === 1
1746+
? { uid: reference, _content_type_uid: refCtUid }
1747+
: reference,
1748+
);
1749+
return null;
1750+
}
17071751
log.debug(`Blt reference ${reference} is valid`);
17081752
return { uid: reference, _content_type_uid: refExist.ctUid };
17091753
}
@@ -1715,6 +1759,16 @@ export default class Entries extends BaseClass {
17151759
missingRefs.push(reference);
17161760
return null;
17171761
} else {
1762+
const refCtUid = reference._content_type_uid ?? refExist.ctUid;
1763+
if (!this.isRefContentTypeAllowed(refCtUid, reference_to)) {
1764+
log.debug(`Reference ${uid} has wrong content type: ${refCtUid} not in reference_to`);
1765+
missingRefs.push(
1766+
Array.isArray(reference_to) && reference_to.length === 1
1767+
? { uid, _content_type_uid: refCtUid }
1768+
: reference,
1769+
);
1770+
return null;
1771+
}
17181772
log.debug(`Reference ${uid} is valid`);
17191773
return reference;
17201774
}
@@ -1846,6 +1900,25 @@ export default class Entries extends BaseClass {
18461900
log.debug(`JSON reference check failed for entry: ${entryUid}`);
18471901
return null;
18481902
} else {
1903+
const refCtUid = contentTypeUid ?? refExist.ctUid;
1904+
const referenceTo = (schema as any).reference_to;
1905+
if (!this.isRefContentTypeAllowed(refCtUid, referenceTo)) {
1906+
log.debug(`JSON RTE embed ${entryUid} has wrong content type: ${refCtUid} not in reference_to`);
1907+
this.missingRefs[this.currentUid].push({
1908+
tree,
1909+
uid: this.currentUid,
1910+
name: this.currentTitle,
1911+
data_type: schema.data_type,
1912+
display_name: schema.display_name,
1913+
fixStatus: this.fix ? 'Fixed' : undefined,
1914+
treeStr: tree
1915+
.map(({ name }) => name)
1916+
.filter((val) => val)
1917+
.join(' ➜ '),
1918+
missingRefs: [{ uid: entryUid, 'content-type-uid': refCtUid }],
1919+
});
1920+
return this.fix ? null : true;
1921+
}
18491922
log.debug(`Entry reference ${entryUid} is valid`);
18501923
}
18511924
} 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": "~2.0.0-beta.4",
8+
"@contentstack/cli-audit": "~2.0.0-beta.5",
99
"@contentstack/cli-command": "~2.0.0-beta",
1010
"@contentstack/cli-utilities": "~2.0.0-beta",
1111
"@contentstack/cli-variants": "~2.0.0-beta.5",

packages/contentstack/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@contentstack/cli",
33
"description": "Command-line tool (CLI) to interact with Contentstack",
4-
"version": "2.0.0-beta.12",
4+
"version": "2.0.0-beta.13",
55
"author": "Contentstack",
66
"bin": {
77
"csdx": "./bin/run.js"
@@ -22,7 +22,7 @@
2222
"prepack": "pnpm compile && oclif manifest && oclif readme"
2323
},
2424
"dependencies": {
25-
"@contentstack/cli-audit": "~2.0.0-beta.4",
25+
"@contentstack/cli-audit": "~2.0.0-beta.5",
2626
"@contentstack/cli-cm-export": "~2.0.0-beta.9",
2727
"@contentstack/cli-cm-import": "~2.0.0-beta.9",
2828
"@contentstack/cli-auth": "~2.0.0-beta.5",

0 commit comments

Comments
 (0)