Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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: c91b9e5fba1c84c0b6de15ad2f8cce698a5c781c9db31bebb7a3ad63ee88d9e1
- filename: pnpm-lock.yaml
checksum: 9e6a3c280cfd7356f1440a592c8b4f1d6294f4ae133696680c253a029be017c7
checksum: 8405d813bbcc584a7540542acfdbc27f5b8768da60354b7eff9f6cd93c3d832d
- filename: packages/contentstack-bootstrap/src/bootstrap/utils.ts
checksum: 6e6fb00bb11b03141e5ad27eeaa4af9718dc30520c3e73970bc208cc0ba2a7d2
version: '1.0'
2,357 changes: 1,200 additions & 1,157 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-export/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"dependencies": {
"@contentstack/cli-command": "~1.7.2",
"@oclif/core": "^4.3.3",
"@contentstack/cli-variants": "~1.3.7",
"@contentstack/cli-variants": "~1.3.8",
"@contentstack/cli-utilities": "~1.17.4",
"async": "^3.2.6",
"big-json": "^3.2.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/contentstack-import/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
"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",
"@contentstack/cli-variants": "~1.3.8",
"@oclif/core": "^4.3.0",
"big-json": "^3.2.0",
"bluebird": "^3.7.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/contentstack-variants/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/cli-variants",
"version": "1.3.7",
"version": "1.3.8",
"description": "Variants plugin",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down
10 changes: 8 additions & 2 deletions packages/contentstack-variants/src/import/audiences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,16 @@ export default class Audiences extends PersonalizationAdapter<ImportConfig> {
for (const audience of audiences) {
let { name, definition, description, uid } = audience;
log.debug(`Processing audience: ${name} (${uid})`, this.config.context);


// Skip Lytics audiences - they cannot be created via API (synced from Lytics)
if (audience.source?.toUpperCase() === 'LYTICS') {
log.debug(`Skipping Lytics audience: ${name} (${uid})`, this.config.context);
continue;
}

try {
//check whether reference attributes exists or not
if (definition.rules?.length) {
if (definition?.rules?.length) {
log.debug(`Processing ${definition.rules.length} definition rules for audience: ${name}`, this.config.context);
definition.rules = lookUpAttributes(definition.rules, attributesUid);
log.debug(`Processed definition rules, remaining rules: ${definition.rules.length}`, this.config.context);
Expand Down
4 changes: 3 additions & 1 deletion packages/contentstack-variants/src/import/experiences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,11 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
let versionReqObj = lookUpAudiences(version, this.audiencesUid) as CreateExperienceVersionInput;
versionReqObj = lookUpEvents(version, this.eventsUid) as CreateExperienceVersionInput;

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

Expand Down
Loading
Loading