Skip to content

Commit 3143916

Browse files
Merge pull request #232 from contentstack/fix/DX-8587
fix: add global fields FVRs in export
2 parents b894e01 + 3537a04 commit 3143916

8 files changed

Lines changed: 1150 additions & 781 deletions

File tree

.talismanrc

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,74 @@
11
fileignoreconfig:
2-
- filename: pnpm-lock.yaml
3-
checksum: 4be4721031793b5ec6957b17778969b715ded2d15a6b6231b6c066735abd7233
2+
- filename: pnpm-lock.yaml
3+
checksum: cb3710ff4c9d3b8f45a8f0b3ff868d49451872ac6d085ae3ccf83d31055cf50d
4+
- filename: packages/contentstack-bulk-operations/src/messages/index.ts
5+
checksum: 097f7ed876622aed6edc3f12995a92e219f96c3d053ec078d45c762f7ebe085e
6+
- filename: packages/contentstack-bulk-operations/test/unit/base-bulk-command.test.ts
7+
checksum: 522b91a2d4bb6a73ebe5ab92c0111e74d567c4360470beefd6a7ca8d30a6e311
8+
- filename: packages/contentstack-import/test/unit/import/modules/assets.test.ts
9+
checksum: 64e3af1e307d0f001370870267fa4cedd2ae81d0f3c38c2e184e69a9a68b2b30
10+
- filename: packages/contentstack-query-export/test/unit/common-helper.test.ts
11+
checksum: a0aa399a2ec15760fe25f7e952d64792b55d3c38d2281858a48e64f0f9081344
12+
- filename: packages/contentstack-import/test/unit/utils/interactive.test.ts
13+
checksum: dad20e229e02e847b5e939fdaf987b1a6c9ff4e1529b6d96a51130f7293285a9
14+
- filename: packages/contentstack-import/test/unit/utils/common-helper.test.ts
15+
checksum: 12c8161a1cb313fa98b612d72e605f6e5190b9f9561d6e73c269c7d106e1a196
16+
- filename: packages/contentstack-apps-cli/src/messages/index.ts
17+
checksum: 5d7991adb5056a14359c5a43db995460a72234916138dbc5ec14f726faf0054b
18+
- filename: packages/contentstack-import/test/unit/utils/marketplace-app-helper.test.ts
19+
checksum: 878be77164311c0ab86629fe18fd3d1be7ff187447e4e5ab65a241b3be0a8d24
20+
- filename: packages/contentstack-import/test/unit/import/modules/global-fields.test.ts
21+
checksum: 4bd86f40b28433470eea876acc9254042c3b24556acdfdb7b15375a948acc094
22+
- filename: packages/contentstack-import/test/unit/import/modules/base-class.test.ts
23+
checksum: 7616f13a4c908eead76c1625cb146320a8a23f31732acb2ea8472e2e0a8b2692
24+
- filename: packages/contentstack-import/test/unit/import/modules/entries.test.ts
25+
checksum: 02332fc203e92039b5fcab6e2e14d14ea5444a4cba2be853009b4c4994692637
26+
- filename: packages/contentstack-import/test/unit/import/modules/index.test.ts
27+
checksum: 6c2bc4bb09dc750532e84a2d6a8198e145bd142bc21ce0d97ae0c8da55dea723
28+
- filename: packages/contentstack-import/test/unit/commands/cm/stacks/import.test.ts
29+
checksum: a52fbd82e4296734d5eb5b06cc9a10d7e2c1fe7b58d15a44d3246f0f2762d5a1
30+
- filename: packages/contentstack-migrate-rte/test/utils/index.js
31+
checksum: f67235e607a446e5b2dd624046966a1ac5af7bce128aa080ec3c420c48b8bddc
32+
- filename: packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts
33+
checksum: e5891257999e9e940956ca77f5a7834be35d259edccd41c06f77736194ad7454
34+
- filename: packages/contentstack-import/test/unit/import/modules/content-types.test.ts
35+
checksum: cd47b1694cb1edffb11fed0f2024fac631b0279ebf55113510d9121c4b957ab1
36+
- filename: packages/contentstack-bulk-operations/test/unit/utils/bulk-operation-log-handler.test.ts
37+
checksum: 8698f77bbb828df10ddc3a94726076a262fbf47d2468233c7e10e66cac42f97d
38+
- filename: packages/contentstack-migrate-rte/test/commands/json-migration.test.js
39+
checksum: 0d18641a32f7cf33d013a5a7c447c1067ba77022513b42304a727f873c741e99
40+
- filename: packages/contentstack-export/test/unit/utils/interactive.test.ts
41+
checksum: 0ea47db305f576c8c9da8cb34ae915f6b01992208ff1c4380cb7ffd93f538e5d
42+
- filename: packages/contentstack-clone/test/lib/helpers/command-helpers.test.ts
43+
checksum: 32d385a677a76faa54cb3ddfe92fb1f1f53cfab878a78e2792c823a363956bd2
44+
- filename: packages/contentstack-export-to-csv/src/messages/index.ts
45+
checksum: 54cbfb3fb83d06ce46766897066ace927c190b2383154b45423e668f63fa4392
46+
- filename: packages/contentstack-clone/test/commands/stack-clone.test.ts
47+
checksum: 4547ba010cc3c9954de49e2b2bef45288dd070c4ad000b6ae3e9a8c9bb1ad296
48+
- filename: packages/contentstack-content-type/tests/utils/index.test.ts
49+
checksum: 8510fedf5eb6078c27d6a0d65d8bff4aa2d41b545246aed220d1a6a643188ad8
50+
- filename: packages/contentstack-bulk-operations/test/unit/utils/revert-retry-handler.test.ts
51+
checksum: c34a4bdea186f4ce48171d90b8ab0f1c94162e441acb23e3a0e00cb9f9f9255f
52+
- filename: packages/contentstack-export-to-csv/test/unit/utils/error-handler.test.ts
53+
checksum: 7216dd610334e6f4a0e2701712408a273a5a2bd4a5ec30b6d6aae043e2b53cc8
54+
- filename: packages/contentstack-export/test/unit/utils/marketplace-app-helper.test.ts
55+
checksum: 3734c4cf7145ef32ad424ccbc160ea8be9b38157762aa66ade51879ab369cee9
56+
- filename: packages/contentstack-import-setup/test/unit/common-helper.test.ts
57+
checksum: 7ac4645401fd3629347a8a182f8a320f177de545a96b8027422bd68040513068
58+
- filename: packages/contentstack-export/test/unit/utils/common-helper.test.ts
59+
checksum: 928f173ca03855a516ac468dba84092fd5f66d2e41b0100bb548e0af364a4ef5
60+
- filename: packages/contentstack-export/test/unit/utils/export-config-handler.test.ts
61+
checksum: 0e7777f73da4f41f4e01e768dba1f3f62ab36778c4f9cd0b519fd1e3bb5c410f
62+
- filename: packages/contentstack-clone/test/lib/util/clone-handler.branch.test.ts
63+
checksum: db8c090aaada21d3952c5bc382228a0a11cebb63926512a5e2581bdaf5f4c2f6
64+
- filename: packages/contentstack-import-setup/test/unit/login-handler.test.ts
65+
checksum: fa7cb2aeb0873e5650beed42096ef4a8edd012a7217818ab0cab2ba6c4611934
66+
- filename: packages/contentstack-import/test/unit/import/modules/marketplace-apps.test.ts
67+
checksum: 42f4d0a15080a69423d4e82b11a6a1933c5f72930cdc4a55b86f27fdc980aeea
68+
- filename: packages/contentstack-import/test/unit/utils/login-handler.test.ts
69+
checksum: 49dd751f846f50fe00d60e7f02b5bd237d36480048bf5e06a24f1cf9fe752a1b
70+
- filename: packages/contentstack-content-type/tests/core/command.test.ts
71+
checksum: 81da32fc47083a3421f5f1d9c04149ab313cb238d6416d3182f8b6d64dc8827d
72+
- filename: packages/contentstack-content-type/tests/core/contentstack/client.test.ts
73+
checksum: da15b04aa0a7ab0e48f70b847e68c05372943279e51c22f8539b01bb48033183
474
version: '1.0'

packages/contentstack-audit/src/audit-base-command.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -340,20 +340,37 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
340340
);
341341

342342
break;
343-
case 'field-rules':
343+
case 'field-rules': {
344344
log.info('Executing field-rules audit', this.auditContext);
345-
// NOTE: We are using the fixed content-type for validation of field rules
345+
// NOTE: We are using the fixed content-type/global-field for validation of field rules.
346346
const data = this.getCtAndGfSchema();
347347
constructorParam.ctSchema = data.ctSchema;
348348
constructorParam.gfSchema = data.gfSchema;
349-
missingFieldRules = await new FieldRule(cloneDeep(constructorParam)).run();
349+
350+
// Field rules live on both content types and global fields. FieldRule.run() picks its
351+
// schema from moduleName, so invoke it once per schema (overriding moduleName per
352+
// instance) and merge the results. Merged object is keyed by schema uid; a content type
353+
// and global field sharing a uid would collide, which is not expected in practice.
354+
const ctFieldRules = await new FieldRule(
355+
cloneDeep({ ...constructorParam, moduleName: 'content-types' }),
356+
).run();
357+
let gfFieldRules: Record<string, any> = {};
358+
if (data.gfSchema?.length) {
359+
gfFieldRules = await new FieldRule(
360+
cloneDeep({ ...constructorParam, moduleName: 'global-fields' }),
361+
).run();
362+
}
363+
missingFieldRules = { ...ctFieldRules, ...gfFieldRules };
364+
350365
await this.prepareReport(module, missingFieldRules);
351-
this.getAffectedData('field-rules', dataModuleWise['content-types'], missingFieldRules);
366+
const total = (data.ctSchema?.length || 0) + (data.gfSchema?.length || 0);
367+
this.getAffectedData('field-rules', { Total: total }, missingFieldRules);
352368
log.success(
353369
`Field-rules audit completed. Found ${Object.keys(missingFieldRules || {}).length} issues`,
354370
this.auditContext,
355371
);
356372
break;
373+
}
357374
case 'composable-studio':
358375
log.info('Executing composable-studio audit', this.auditContext);
359376
missingRefsInComposableStudio = await new ComposableStudio(cloneDeep(constructorParam)).run();

packages/contentstack-audit/test/unit/modules/field-rules.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,53 @@ describe('Field Rules', () => {
140140
});
141141
});
142142

143+
describe('global field rules', () => {
144+
const gfWithRuleSchema = () => [
145+
{
146+
uid: 'gf_with_rule',
147+
title: 'GF With Rule',
148+
schema: [{ uid: 'single_line', data_type: 'text', display_name: 'Single Line' }],
149+
field_rules: [
150+
{
151+
conditions: [{ operand_field: 'single_line', operator: 'equals', value: 'x' }],
152+
actions: [{ action: 'show', target_field: 'missing_field' }],
153+
},
154+
],
155+
},
156+
];
157+
158+
fancy
159+
.stdout({ print: process.env.PRINT === 'true' || false })
160+
.stub(FieldRule.prototype, 'prepareEntryMetaData', async () => {})
161+
.stub(FieldRule.prototype, 'prerequisiteData', async () => {})
162+
.it("scans a global field's own field_rules and flags missing target fields", async () => {
163+
const gfInstance = new FieldRule({
164+
...constructorParam,
165+
moduleName: 'global-fields',
166+
gfSchema: gfWithRuleSchema() as any,
167+
});
168+
const result = await gfInstance.run();
169+
expect(result).to.have.property('gf_with_rule');
170+
expect(JSON.stringify(result)).to.include('missing_field');
171+
});
172+
173+
fancy
174+
.stdout({ print: process.env.PRINT === 'true' || false })
175+
.stub(FieldRule.prototype, 'prepareEntryMetaData', async () => {})
176+
.stub(FieldRule.prototype, 'prerequisiteData', async () => {})
177+
.it('does not flag a global field whose field_rules reference existing fields', async () => {
178+
const okSchema = gfWithRuleSchema();
179+
okSchema[0].field_rules[0].actions[0].target_field = 'single_line';
180+
const gfInstance = new FieldRule({
181+
...constructorParam,
182+
moduleName: 'global-fields',
183+
gfSchema: okSchema as any,
184+
});
185+
const result = await gfInstance.run();
186+
expect(result).to.not.have.property('gf_with_rule');
187+
});
188+
});
189+
143190
describe('writeFixContent method', () => {
144191
fancy
145192
.stdout({ print: process.env.PRINT === 'true' || false })

packages/contentstack-export/src/config/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,12 @@ const config: DefaultConfig = {
9595
globalfields: {
9696
dirName: 'global_fields',
9797
fileName: 'globalfields.json',
98-
validKeys: ['title', 'uid', 'schema', 'options', 'singleton', 'description'],
98+
validKeys: ['title', 'uid', 'field_rules', 'schema', 'options', 'singleton', 'description'],
9999
},
100100
'global-fields': {
101101
dirName: 'global_fields',
102102
fileName: 'globalfields.json',
103-
validKeys: ['title', 'uid', 'schema', 'options', 'singleton', 'description'],
103+
validKeys: ['title', 'uid', 'field_rules', 'schema', 'options', 'singleton', 'description'],
104104
},
105105
assets: {
106106
dirName: 'assets',

packages/contentstack-import/src/import/modules/content-types.ts

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { sanitizePath, log, handleAndLogError } from '@contentstack/cli-utilitie
1111
import { fsUtil, schemaTemplate, lookupExtension, lookUpTaxonomy, fileHelper } from '../../utils';
1212
import { ImportConfig, ModuleClassParams } from '../../types';
1313
import BaseClass, { ApiOptions } from './base-class';
14-
import { updateFieldRules } from '../../utils/content-type-helper';
14+
import { updateFieldRules, isGlobalFieldRule } from '../../utils/content-type-helper';
1515

1616
export default class ContentTypesImport extends BaseClass {
1717
private cTsMapperPath: string;
@@ -34,7 +34,7 @@ export default class ContentTypesImport extends BaseClass {
3434
private reqConcurrency: number;
3535
private ignoredFilesInContentTypesFolder: Map<string, string>;
3636
private titleToUIdMap: Map<string, string>;
37-
private fieldRules: Array<Record<string, unknown>>;
37+
private fieldRules: string[];
3838
private installedExtensions: Record<string, unknown>;
3939
private cTsConfig: {
4040
dirName: string;
@@ -206,13 +206,103 @@ export default class ContentTypesImport extends BaseClass {
206206
this.pendingGFs = fsUtil.readFile(this.gFsPendingPath) as any;
207207
if (!this.pendingGFs || isEmpty(this.pendingGFs)) {
208208
log.info('No pending global fields found to update.', this.importConfig.context);
209-
return;
209+
} else {
210+
await this.updatePendingGFs().catch((error) => {
211+
handleAndLogError(error, { ...this.importConfig.context });
212+
});
213+
log.success('Updated pending global fields with content type with references', this.importConfig.context);
210214
}
211-
await this.updatePendingGFs().catch((error) => {
215+
216+
// Global field rules were skipped during the content type update (see updateFieldRules) because
217+
// the embedded global field schema was not yet complete on the stack. At this point global
218+
// fields are expected to be complete (deferred ones via updatePendingGFs above; others already
219+
// applied in the global-fields module / pre-existing on the stack for module-only imports).
220+
// Re-apply the global field rules now; if global fields are still incomplete this step may fail
221+
// and will be reported below.
222+
const failedGFFieldRuleCTs = await this.updateGFFieldRules().catch((error) => {
212223
handleAndLogError(error, { ...this.importConfig.context });
224+
return [] as string[];
213225
});
214-
log.success('Updated pending global fields with content type with references', this.importConfig.context);
226+
227+
if (failedGFFieldRuleCTs.length) {
228+
// Surface the partial failure instead of claiming an unqualified success.
229+
log.error(
230+
`Content types imported, but failed to apply global field rules for: ${failedGFFieldRuleCTs.join(', ')}`,
231+
this.importConfig.context,
232+
);
233+
}
215234
log.success('Content types have been imported successfully!', this.importConfig.context);
235+
236+
}
237+
238+
/**
239+
* Applies the global field rules that were skipped during the content type update (updateFieldRules
240+
* strips rules flagged is_global_field_rule, because their paths reference an embedded global field
241+
* whose schema is not yet complete when the content type is first updated). By the time this runs,
242+
* every embedded global field is complete, so the rules validate. Runs for deferred, non-deferred
243+
* and module-only imports alike.
244+
* @returns the uids of content types whose global field rule update failed.
245+
*/
246+
async updateGFFieldRules(): Promise<string[]> {
247+
const failedCTs: string[] = [];
248+
249+
if (!this.fieldRules?.length) {
250+
log.debug('No content types with field rules; skipping global field rules update.', this.importConfig.context);
251+
return failedCTs;
252+
}
253+
254+
const cTs = (fsUtil.readFile(path.join(this.cTsFolderPath, 'schema.json')) || []) as Record<string, any>[];
255+
256+
for (const cTUid of this.fieldRules) {
257+
const contentType: any = find(cTs, { uid: cTUid });
258+
if (!contentType?.field_rules?.length) {
259+
continue;
260+
}
261+
262+
// Only content types carrying a global field rule need re-applying; the rest were fully
263+
// updated (schema + their own rules) in updateCTs.
264+
const hasGFFieldRule = contentType.field_rules.some((rule: any) => isGlobalFieldRule(rule));
265+
if (!hasGFFieldRule) {
266+
continue;
267+
}
268+
269+
log.info(`Re-applying global field rules for content type: ${contentType.uid}`, this.importConfig.context);
270+
271+
const contentTypeResponse: any = await this.stack
272+
.contentType(contentType.uid)
273+
.fetch()
274+
.catch((error: unknown) => {
275+
handleAndLogError(error, { ...this.importConfig.context, uid: contentType.uid });
276+
});
277+
if (!contentTypeResponse) {
278+
log.debug(
279+
`Skipping global field rules update for ${contentType.uid} - content type not found`,
280+
this.importConfig.context,
281+
);
282+
failedCTs.push(contentType.uid);
283+
continue;
284+
}
285+
286+
// Send the global field rules together with the content type's own non-reference rules,
287+
// NOT the raw on-disk set. updateFieldRules(..., { keepGlobalFieldRules: true }) keeps the
288+
// now-valid global field rules while still dropping reference-condition rules, which are
289+
// owned by the entries module (it remaps their entry-uid values post entry-import). Sending
290+
// the raw set here would resurrect those reference rules prematurely with stale uids.
291+
// NOTE: field_rules is a whole-array PUT — if any single rule is invalid the API rejects the
292+
// entire array, so a malformed rule would take the global field rules down with it.
293+
contentTypeResponse.field_rules = updateFieldRules(contentType, { keepGlobalFieldRules: true });
294+
await contentTypeResponse
295+
.update()
296+
.then(() => {
297+
log.success(`Re-applied global field rules for content type: ${contentType.uid}`, this.importConfig.context);
298+
})
299+
.catch((error: Error) => {
300+
handleAndLogError(error, { ...this.importConfig.context, uid: contentType.uid });
301+
failedCTs.push(contentType.uid);
302+
});
303+
}
304+
305+
return failedCTs;
216306
}
217307

218308
async seedCTs(): Promise<any> {

0 commit comments

Comments
 (0)