Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
fileignoreconfig:
- filename: package-lock.json
checksum: 8d0430a55a8bbfe3f1be5264dfcc914616175bbd0c0ddb33b0796ccc811dbd91
checksum: 1b011574c5a640f7132f2dcabfced269cb497ddd3270524ec32abe3cb4a95cb5
- filename: pnpm-lock.yaml
checksum: 9e6a3c280cfd7356f1440a592c8b4f1d6294f4ae133696680c253a029be017c7
checksum: 91ffcd3364bcbef7dad0d25547849a572dc9ebd004999c3ede85c7730959a2e5
- filename: packages/contentstack-bootstrap/src/bootstrap/utils.ts
checksum: 6e6fb00bb11b03141e5ad27eeaa4af9718dc30520c3e73970bc208cc0ba2a7d2
version: '1.0'
1,512 changes: 827 additions & 685 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/contentstack-audit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/cli-audit",
"version": "1.17.1",
"version": "1.18.0",
"description": "Contentstack audit plugin",
"author": "Contentstack CLI",
"homepage": "https://github.com/contentstack/cli",
Expand Down
80 changes: 77 additions & 3 deletions packages/contentstack-audit/src/modules/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,46 @@ export default class Entries {
return 'entries';
}

/**
* Returns whether a referenced entry's content type is allowed by the schema's reference_to.
* @param refCtUid - Content type UID of the referenced entry (e.g. from _content_type_uid)
* @param referenceTo - Schema's reference_to (string or string[])
* @param configOverride - Optional config with skipRefs; falls back to this.config
* @returns true if allowed or check cannot be performed; false if refCtUid is not in reference_to
*/
protected isRefContentTypeAllowed(
refCtUid: string | undefined,
referenceTo: string | string[] | undefined,
configOverride?: { skipRefs?: string[] },
): boolean {
if (refCtUid === undefined) return true;
const skipRefs = configOverride?.skipRefs ?? (this.config as any).skipRefs ?? [];
if (Array.isArray(skipRefs) && skipRefs.includes(refCtUid)) return true;
if (referenceTo === undefined || referenceTo === null) return true;
const refToList = Array.isArray(referenceTo) ? referenceTo : [referenceTo];
if (refToList.length === 0) return false;
return refToList.includes(refCtUid);
}

/**
* If ref CT is not allowed, pushes to missingRefs.
* @returns true if invalid (pushed), false if valid
*/
private addInvalidRefIfNeeded(
missingRefs: Record<string, any>[],
uidValue: string,
refCtUid: string | undefined,
referenceTo: string | string[] | undefined,
fullRef: any,
logLabel: string,
): boolean {
if (this.isRefContentTypeAllowed(refCtUid, referenceTo)) return false;
log.debug(`${logLabel} has wrong content type: ${refCtUid} not in reference_to`);
const refList = Array.isArray(referenceTo) ? referenceTo : referenceTo != null ? [referenceTo] : [];
missingRefs.push(refList.length === 1 ? { uid: uidValue, _content_type_uid: refCtUid } : fullRef);
return true;
}

/**
* The `run` function checks if a folder path exists, sets the schema based on the module name,
* iterates over the schema and looks for references, and returns a list of missing references.
Expand Down Expand Up @@ -877,8 +917,9 @@ export default class Entries {

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

for (const index in field ?? []) {
Expand All @@ -897,7 +938,10 @@ export default class Entries {
missingRefs.push(reference);
}
} else {
log.debug(`Reference ${reference} is valid`);
const refCtUid = refExist.ctUid;
if (!this.addInvalidRefIfNeeded(missingRefs, reference, refCtUid, reference_to, reference, `Reference ${reference}`)) {
log.debug(`Reference ${reference} is valid`);
}
}
}
// NOTE Can skip specific references keys (Ex, system defined keys can be skipped)
Expand All @@ -910,7 +954,10 @@ export default class Entries {
log.debug(`Missing reference: ${uid}`);
missingRefs.push(reference);
} else {
log.debug(`Reference ${uid} is valid`);
const refCtUid = reference._content_type_uid ?? refExist.ctUid;
if (!this.addInvalidRefIfNeeded(missingRefs, uid, refCtUid, reference_to, reference, `Reference ${uid}`)) {
log.debug(`Reference ${uid} is valid`);
}
}
}
}
Expand Down Expand Up @@ -1685,6 +1732,10 @@ export default class Entries {
missingRefs.push(reference);
}
} else {
const refCtUid = reference._content_type_uid ?? refExist.ctUid;
if (this.addInvalidRefIfNeeded(missingRefs, reference, refCtUid, reference_to, reference, `Blt reference ${reference}`)) {
return null;
}
log.debug(`Blt reference ${reference} is valid`);
return { uid: reference, _content_type_uid: refExist.ctUid };
}
Expand All @@ -1696,6 +1747,10 @@ export default class Entries {
missingRefs.push(reference);
return null;
} else {
const refCtUid = reference._content_type_uid ?? refExist.ctUid;
if (this.addInvalidRefIfNeeded(missingRefs, uid, refCtUid, reference_to, reference, `Reference ${uid}`)) {
return null;
}
log.debug(`Reference ${uid} is valid`);
return reference;
}
Expand Down Expand Up @@ -1827,6 +1882,25 @@ export default class Entries {
log.debug(`JSON reference check failed for entry: ${entryUid}`);
return null;
} else {
const refCtUid = contentTypeUid ?? refExist.ctUid;
const referenceTo = (schema as any).reference_to;
if (!this.isRefContentTypeAllowed(refCtUid, referenceTo)) {
log.debug(`JSON RTE embed ${entryUid} has wrong content type: ${refCtUid} not in reference_to`);
this.missingRefs[this.currentUid].push({
tree,
uid: this.currentUid,
name: this.currentTitle,
data_type: schema.data_type,
display_name: schema.display_name,
fixStatus: this.fix ? 'Fixed' : undefined,
treeStr: tree
.map(({ name }) => name)
.filter((val) => val)
.join(' ➜ '),
missingRefs: [{ uid: entryUid, 'content-type-uid': refCtUid }],
});
return this.fix ? null : true;
}
log.debug(`Entry reference ${entryUid} is valid`);
}
} else {
Expand Down
174 changes: 174 additions & 0 deletions packages/contentstack-audit/test/unit/modules/entries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,55 @@ describe('Entries module', () => {

expect(result).to.be.an('array'); // Should return array of missing references
});

fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should flag reference when ref entry has wrong content type (ct2 ref when reference_to is ct1)', () => {
const ctInstance = new Entries(constructorParam);
(ctInstance as any).currentUid = 'test-entry';
(ctInstance as any).entryMetaData = [{ uid: 'blt123', ctUid: 'ct2' }]; // Entry exists but is ct2

const referenceFieldSchema = { uid: 'ref', display_name: 'Ref', data_type: 'reference', reference_to: ['ct1'] };
const entryData = [{ uid: 'blt123', _content_type_uid: 'ct2' }];
const tree = [{ uid: 'test-entry', name: 'Test Entry' }];

const result = ctInstance.validateReferenceValues(tree, referenceFieldSchema as any, entryData);

expect(result).to.have.length(1);
expect(result[0].missingRefs).to.deep.include({ uid: 'blt123', _content_type_uid: 'ct2' });
});

fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should not flag reference when ref entry has correct content type (ct1 ref when reference_to is ct1)', () => {
const ctInstance = new Entries(constructorParam);
(ctInstance as any).currentUid = 'test-entry';
(ctInstance as any).entryMetaData = [{ uid: 'blt123', ctUid: 'ct1' }];

const referenceFieldSchema = { uid: 'ref', display_name: 'Ref', data_type: 'reference', reference_to: ['ct1'] };
const entryData = [{ uid: 'blt123', _content_type_uid: 'ct1' }];
const tree = [{ uid: 'test-entry', name: 'Test Entry' }];

const result = ctInstance.validateReferenceValues(tree, referenceFieldSchema as any, entryData);

expect(result).to.have.length(0);
});

fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should normalize reference_to string and allow matching ref (ct1 when reference_to is string ct1)', () => {
const ctInstance = new Entries(constructorParam);
(ctInstance as any).currentUid = 'test-entry';
(ctInstance as any).entryMetaData = [{ uid: 'blt456', ctUid: 'ct1' }];

const referenceFieldSchema = { uid: 'ref', display_name: 'Ref', data_type: 'reference', reference_to: 'ct1' };
const entryData = [{ uid: 'blt456', _content_type_uid: 'ct1' }];
const tree = [{ uid: 'test-entry', name: 'Test Entry' }];

const result = ctInstance.validateReferenceValues(tree, referenceFieldSchema as any, entryData);

expect(result).to.have.length(0);
});
});

describe('validateModularBlocksField method', () => {
Expand Down Expand Up @@ -1365,5 +1414,130 @@ describe('Entries module', () => {

// Should not throw - method is void
});

fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should flag JSON RTE embed when ref has wrong content type (ct2 when reference_to is ct1,sys_assets)', () => {
const ctInstance = new Entries(constructorParam);
(ctInstance as any).currentUid = 'test-entry';
(ctInstance as any).missingRefs = { 'test-entry': [] };
(ctInstance as any).entryMetaData = [{ uid: 'blt123', ctUid: 'ct2' }];

const schema = {
uid: 'json_rte',
display_name: 'JSON RTE',
data_type: 'richtext',
reference_to: ['ct1', 'sys_assets'],
};
const child = {
type: 'embed',
uid: 'child-uid',
attrs: { 'entry-uid': 'blt123', 'content-type-uid': 'ct2' },
children: [],
};
const tree: Record<string, unknown>[] = [];

(ctInstance as any).jsonRefCheck(tree, schema, child);

expect((ctInstance as any).missingRefs['test-entry']).to.have.length(1);
expect((ctInstance as any).missingRefs['test-entry'][0].missingRefs).to.deep.include({
uid: 'blt123',
'content-type-uid': 'ct2',
});
});
});

describe('fixMissingReferences method', () => {
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should filter out ref when ref has wrong content type (ct2 when reference_to is ct1)', () => {
const ctInstance = new Entries({ ...constructorParam, fix: true });
(ctInstance as any).currentUid = 'test-entry';
(ctInstance as any).missingRefs = { 'test-entry': [] };
(ctInstance as any).entryMetaData = [{ uid: 'blt123', ctUid: 'ct2' }];

const field = {
uid: 'ref_field',
display_name: 'Ref',
data_type: 'reference',
reference_to: ['ct1'],
};
const entry = [{ uid: 'blt123', _content_type_uid: 'ct2' }];
const tree = [{ uid: 'test-entry', name: 'Test Entry' }];

const result = ctInstance.fixMissingReferences(tree, field as any, entry);

expect(result).to.have.length(0);
expect((ctInstance as any).missingRefs['test-entry']).to.have.length(1);
expect((ctInstance as any).missingRefs['test-entry'][0].missingRefs).to.deep.include({
uid: 'blt123',
_content_type_uid: 'ct2',
});
});
});

describe('jsonRefCheck in fix mode', () => {
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should return null when ref has wrong content type (fix mode)', () => {
const ctInstance = new Entries({ ...constructorParam, fix: true });
(ctInstance as any).currentUid = 'test-entry';
(ctInstance as any).missingRefs = { 'test-entry': [] };
(ctInstance as any).entryMetaData = [{ uid: 'blt123', ctUid: 'ct2' }];

const schema = {
uid: 'json_rte',
display_name: 'JSON RTE',
data_type: 'richtext',
reference_to: ['ct1'],
};
const child = {
type: 'embed',
uid: 'child-uid',
attrs: { 'entry-uid': 'blt123', 'content-type-uid': 'ct2' },
children: [],
};
const tree: Record<string, unknown>[] = [];

const result = (ctInstance as any).jsonRefCheck(tree, schema, child);

expect(result).to.be.null;
});
});

describe('isRefContentTypeAllowed helper', () => {
const callHelper = (refCtUid: string | undefined, referenceTo: string | string[] | undefined) => {
const ctInstance = new Entries(constructorParam);
return (ctInstance as any).isRefContentTypeAllowed(refCtUid, referenceTo);
};

fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns true when refCtUid is in reference_to', () => {
expect(callHelper('ct1', ['ct1', 'ct2'])).to.be.true;
});

fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns false when refCtUid is not in reference_to', () => {
expect(callHelper('ct2', ['ct1'])).to.be.false;
});

fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns true when reference_to is undefined', () => {
expect(callHelper('ct1', undefined)).to.be.true;
});

fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('normalizes reference_to string and allows matching refCtUid', () => {
expect(callHelper('ct1', 'ct1')).to.be.true;
expect(callHelper('ct2', 'ct1')).to.be.false;
});

fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns false when reference_to is empty array', () => {
expect(callHelper('ct1', [])).to.be.false;
});

fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns true when refCtUid is undefined', () => {
expect(callHelper(undefined, ['ct1'])).to.be.true;
});

fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns true when refCtUid is in skipRefs', () => {
expect(callHelper('sys_assets', ['ct1'])).to.be.true;
});
});
});
2 changes: 1 addition & 1 deletion packages/contentstack-import/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"author": "Contentstack",
"bugs": "https://github.com/contentstack/cli/issues",
"dependencies": {
"@contentstack/cli-audit": "~1.17.1",
"@contentstack/cli-audit": "~1.18.0",
"@contentstack/cli-command": "~1.7.2",
"@contentstack/cli-utilities": "~1.17.4",
"@contentstack/cli-variants": "~1.3.7",
Expand Down
2 changes: 1 addition & 1 deletion packages/contentstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"prepack": "pnpm compile && oclif manifest && oclif readme"
},
"dependencies": {
"@contentstack/cli-audit": "~1.17.1",
"@contentstack/cli-audit": "~1.18.0",
"@contentstack/cli-cm-export": "~1.23.2",
"@contentstack/cli-cm-import": "~1.31.3",
"@contentstack/cli-auth": "~1.7.3",
Expand Down
Loading
Loading