Skip to content

Commit d65b8c8

Browse files
Merge pull request #2431 from contentstack/development
staging PR
2 parents fb1c4e3 + 44fd133 commit d65b8c8

File tree

17 files changed

+3348
-2870
lines changed

17 files changed

+3348
-2870
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: 10993a3e930b77c3c6e09afcb50c23e6a8a901302172b699e92b0a4bfec0cffa
44
- filename: pnpm-lock.yaml
5-
checksum: 9e6a3c280cfd7356f1440a592c8b4f1d6294f4ae133696680c253a029be017c7
5+
checksum: 3e47ed021491e9f3c21d25e4ea72a1101b51f16ebedd279019df1792b72417a4
66
- filename: packages/contentstack-bootstrap/src/bootstrap/utils.ts
77
checksum: 6e6fb00bb11b03141e5ad27eeaa4af9718dc30520c3e73970bc208cc0ba2a7d2
88
version: '1.0'

package-lock.json

Lines changed: 1635 additions & 1526 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: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,46 @@ 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+
117+
/**
118+
* If ref CT is not allowed, pushes to missingRefs.
119+
* @returns true if invalid (pushed), false if valid
120+
*/
121+
private addInvalidRefIfNeeded(
122+
missingRefs: Record<string, any>[],
123+
uidValue: string,
124+
refCtUid: string | undefined,
125+
referenceTo: string | string[] | undefined,
126+
fullRef: any,
127+
logLabel: string,
128+
): boolean {
129+
if (this.isRefContentTypeAllowed(refCtUid, referenceTo)) return false;
130+
log.debug(`${logLabel} has wrong content type: ${refCtUid} not in reference_to`);
131+
const refList = Array.isArray(referenceTo) ? referenceTo : referenceTo != null ? [referenceTo] : [];
132+
missingRefs.push(refList.length === 1 ? { uid: uidValue, _content_type_uid: refCtUid } : fullRef);
133+
return true;
134+
}
135+
96136
/**
97137
* The `run` function checks if a folder path exists, sets the schema based on the module name,
98138
* iterates over the schema and looks for references, and returns a list of missing references.
@@ -877,8 +917,9 @@ export default class Entries {
877917

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

884925
for (const index in field ?? []) {
@@ -897,7 +938,10 @@ export default class Entries {
897938
missingRefs.push(reference);
898939
}
899940
} else {
900-
log.debug(`Reference ${reference} is valid`);
941+
const refCtUid = refExist.ctUid;
942+
if (!this.addInvalidRefIfNeeded(missingRefs, reference, refCtUid, reference_to, reference, `Reference ${reference}`)) {
943+
log.debug(`Reference ${reference} is valid`);
944+
}
901945
}
902946
}
903947
// NOTE Can skip specific references keys (Ex, system defined keys can be skipped)
@@ -910,7 +954,10 @@ export default class Entries {
910954
log.debug(`Missing reference: ${uid}`);
911955
missingRefs.push(reference);
912956
} else {
913-
log.debug(`Reference ${uid} is valid`);
957+
const refCtUid = reference._content_type_uid ?? refExist.ctUid;
958+
if (!this.addInvalidRefIfNeeded(missingRefs, uid, refCtUid, reference_to, reference, `Reference ${uid}`)) {
959+
log.debug(`Reference ${uid} is valid`);
960+
}
914961
}
915962
}
916963
}
@@ -1685,6 +1732,10 @@ export default class Entries {
16851732
missingRefs.push(reference);
16861733
}
16871734
} else {
1735+
const refCtUid = reference._content_type_uid ?? refExist.ctUid;
1736+
if (this.addInvalidRefIfNeeded(missingRefs, reference, refCtUid, reference_to, reference, `Blt reference ${reference}`)) {
1737+
return null;
1738+
}
16881739
log.debug(`Blt reference ${reference} is valid`);
16891740
return { uid: reference, _content_type_uid: refExist.ctUid };
16901741
}
@@ -1696,6 +1747,10 @@ export default class Entries {
16961747
missingRefs.push(reference);
16971748
return null;
16981749
} else {
1750+
const refCtUid = reference._content_type_uid ?? refExist.ctUid;
1751+
if (this.addInvalidRefIfNeeded(missingRefs, uid, refCtUid, reference_to, reference, `Reference ${uid}`)) {
1752+
return null;
1753+
}
16991754
log.debug(`Reference ${uid} is valid`);
17001755
return reference;
17011756
}
@@ -1827,6 +1882,25 @@ export default class Entries {
18271882
log.debug(`JSON reference check failed for entry: ${entryUid}`);
18281883
return null;
18291884
} else {
1885+
const refCtUid = contentTypeUid ?? refExist.ctUid;
1886+
const referenceTo = (schema as any).reference_to;
1887+
if (!this.isRefContentTypeAllowed(refCtUid, referenceTo)) {
1888+
log.debug(`JSON RTE embed ${entryUid} has wrong content type: ${refCtUid} not in reference_to`);
1889+
this.missingRefs[this.currentUid].push({
1890+
tree,
1891+
uid: this.currentUid,
1892+
name: this.currentTitle,
1893+
data_type: schema.data_type,
1894+
display_name: schema.display_name,
1895+
fixStatus: this.fix ? 'Fixed' : undefined,
1896+
treeStr: tree
1897+
.map(({ name }) => name)
1898+
.filter((val) => val)
1899+
.join(' ➜ '),
1900+
missingRefs: [{ uid: entryUid, 'content-type-uid': refCtUid }],
1901+
});
1902+
return this.fix ? null : true;
1903+
}
18301904
log.debug(`Entry reference ${entryUid} is valid`);
18311905
}
18321906
} 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-export/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"dependencies": {
88
"@contentstack/cli-command": "~1.7.2",
99
"@oclif/core": "^4.3.3",
10-
"@contentstack/cli-variants": "~1.3.7",
10+
"@contentstack/cli-variants": "~1.3.8",
1111
"@contentstack/cli-utilities": "~1.17.4",
1212
"async": "^3.2.6",
1313
"big-json": "^3.2.0",

packages/contentstack-import/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
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",
11-
"@contentstack/cli-variants": "~1.3.7",
11+
"@contentstack/cli-variants": "~1.3.8",
1212
"@oclif/core": "^4.3.0",
1313
"big-json": "^3.2.0",
1414
"bluebird": "^3.7.2",

packages/contentstack-variants/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-variants",
3-
"version": "1.3.7",
3+
"version": "1.3.8",
44
"description": "Variants plugin",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",

packages/contentstack-variants/src/import/audiences.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,16 @@ export default class Audiences extends PersonalizationAdapter<ImportConfig> {
7070
for (const audience of audiences) {
7171
let { name, definition, description, uid } = audience;
7272
log.debug(`Processing audience: ${name} (${uid})`, this.config.context);
73-
73+
74+
// Skip Lytics audiences - they cannot be created via API (synced from Lytics)
75+
if (audience.source?.toUpperCase() === 'LYTICS') {
76+
log.debug(`Skipping Lytics audience: ${name} (${uid})`, this.config.context);
77+
continue;
78+
}
79+
7480
try {
7581
//check whether reference attributes exists or not
76-
if (definition.rules?.length) {
82+
if (definition?.rules?.length) {
7783
log.debug(`Processing ${definition.rules.length} definition rules for audience: ${name}`, this.config.context);
7884
definition.rules = lookUpAttributes(definition.rules, attributesUid);
7985
log.debug(`Processed definition rules, remaining rules: ${definition.rules.length}`, this.config.context);

packages/contentstack-variants/src/import/experiences.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,11 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
203203
let versionReqObj = lookUpAudiences(version, this.audiencesUid) as CreateExperienceVersionInput;
204204
versionReqObj = lookUpEvents(version, this.eventsUid) as CreateExperienceVersionInput;
205205

206-
if (versionReqObj && versionReqObj.status) {
206+
if (versionReqObj && versionReqObj.status && (versionReqObj.variants?.length ?? 0) > 0) {
207207
versionMap[versionReqObj.status] = versionReqObj;
208208
log.debug(`Mapped version with status: ${versionReqObj.status}`, this.config.context);
209+
} else if (versionReqObj?.status && !(versionReqObj.variants?.length ?? 0)) {
210+
log.warn(`Skipping version ${versionReqObj.status}: no valid variants (all had unmapped Lytics audiences)`, this.config.context);
209211
}
210212
});
211213

0 commit comments

Comments
 (0)