Skip to content

Commit 79af654

Browse files
authored
Merge pull request #307 from contentstack/enhc/DX-9464
feat(global-fields): switch to per-file export/import format
2 parents 5326571 + 52d8909 commit 79af654

11 files changed

Lines changed: 175 additions & 88 deletions

File tree

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import {
1414
CLIProgressManager,
1515
clearProgressModuleSetting,
1616
readContentTypeSchemas,
17+
readGlobalFieldSchemas,
1718
generateUid
1819
} from '@contentstack/cli-utilities';
19-
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
20+
import { createWriteStream, existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
2021
import config from './config';
2122
import { print } from './util/log';
2223
import { auditMsg } from './messages';
@@ -507,13 +508,9 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
507508
*/
508509
getCtAndGfSchema() {
509510
const ctDirPath = join(this.sharedConfig.basePath, this.sharedConfig.moduleConfig['content-types'].dirName);
510-
const gfPath = join(
511-
this.sharedConfig.basePath,
512-
this.sharedConfig.moduleConfig['global-fields'].dirName,
513-
this.sharedConfig.moduleConfig['global-fields'].fileName,
514-
);
511+
const gfDirPath = join(this.sharedConfig.basePath, this.sharedConfig.moduleConfig['global-fields'].dirName);
515512

516-
const gfSchema = existsSync(gfPath) ? (JSON.parse(readFileSync(gfPath, 'utf8')) as ContentTypeStruct[]) : [];
513+
const gfSchema = (readGlobalFieldSchemas(gfDirPath) || []) as ContentTypeStruct[];
517514
const ctSchema = (readContentTypeSchemas(ctDirPath) || []) as ContentTypeStruct[];
518515

519516
return { ctSchema, gfSchema };

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const config = {
2424
'global-fields': {
2525
name: 'global field',
2626
dirName: 'global_fields',
27-
fileName: 'globalfields.json',
27+
fileName: 'globalfields.json', // Not used - reads from individual files via readGlobalFieldSchemas
2828
},
2929
entries: {
3030
name: 'entries',

packages/contentstack-audit/test/unit/audit-base-command.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,30 @@ describe('AuditBaseCommand class', () => {
369369
gfSchema: [],
370370
});
371371
});
372+
373+
fancy
374+
.stdout({ print: process.env.PRINT === 'true' || false })
375+
.stub(winston.transports, 'File', () => fsTransport)
376+
.stub(winston, 'createLogger', createMockWinstonLogger)
377+
.stub(fs, 'createWriteStream', () => new PassThrough())
378+
.it('should return per-uid global field schemas and skip globalfields.json', async () => {
379+
class CMD extends AuditBaseCommand {
380+
async run() {
381+
// Point basePath at mock/contents which has global_fields/gf_1.json (per-uid)
382+
// and global_fields/globalfields.json (bulk legacy — must be ignored)
383+
this.sharedConfig.basePath = resolve(__dirname, 'mock', 'contents');
384+
return this.getCtAndGfSchema();
385+
}
386+
}
387+
388+
const result = await CMD.run([]);
389+
// gf_1.json is read; globalfields.json is excluded by readGlobalFieldSchemas
390+
expect(result.gfSchema).to.be.an('array');
391+
expect(result.gfSchema.length).to.equal(1);
392+
expect((result.gfSchema[0] as any).uid).to.equal('gf_1');
393+
// content_types dir absent → ctSchema empty
394+
expect(result.ctSchema).to.deep.equal([]);
395+
});
372396
});
373397

374398
describe('Progress Manager Integration', () => {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"uid": "gf_1",
3+
"title": "GF - 1",
4+
"schema": [
5+
{
6+
"data_type": "text",
7+
"display_name": "Single Line Textbox",
8+
"uid": "single_line",
9+
"mandatory": false,
10+
"multiple": false,
11+
"non_localizable": false,
12+
"unique": false
13+
}
14+
]
15+
}

packages/contentstack-export/src/export/modules/global-fields.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,12 @@ export default class GlobalFieldsExport extends BaseClass {
1717
};
1818
private globalFieldsConfig: {
1919
dirName?: string;
20-
fileName?: string;
2120
validKeys?: string[];
2221
fetchConcurrency?: number;
2322
writeConcurrency?: number;
2423
limit?: number;
2524
};
2625
private globalFieldsDirPath: string;
27-
private globalFields: Record<string, unknown>[];
2826

2927
constructor({ exportConfig, stackAPIClient }: ModuleClassParams) {
3028
super({ exportConfig, stackAPIClient });
@@ -41,7 +39,6 @@ export default class GlobalFieldsExport extends BaseClass {
4139
sanitizePath(getExportBasePath(exportConfig)),
4240
sanitizePath(this.globalFieldsConfig.dirName),
4341
);
44-
this.globalFields = [];
4542
this.applyQueryFilters(this.qs, 'global-fields');
4643
this.exportConfig.context.module = MODULE_CONTEXTS.GLOBAL_FIELDS;
4744
this.currentModuleName = MODULE_NAMES[MODULE_CONTEXTS.GLOBAL_FIELDS];
@@ -66,20 +63,13 @@ export default class GlobalFieldsExport extends BaseClass {
6663

6764
if (totalCount === 0) {
6865
log.info(messageHandler.parse('GLOBAL_FIELDS_NOT_FOUND'), this.exportConfig.context);
69-
const globalFieldsFilePath = path.join(this.globalFieldsDirPath, this.globalFieldsConfig.fileName);
70-
log.debug(`Writing global fields to: ${globalFieldsFilePath}`, this.exportConfig.context);
71-
fsUtil.writeFile(globalFieldsFilePath, this.globalFields);
7266
this.completeProgress(true);
7367
return;
7468
}
7569

7670
progress.updateStatus('Fetching global fields...');
7771
await this.getGlobalFields();
7872

79-
const globalFieldsFilePath = path.join(this.globalFieldsDirPath, this.globalFieldsConfig.fileName);
80-
log.debug(`Writing global fields to: ${globalFieldsFilePath}`, this.exportConfig.context);
81-
fsUtil.writeFile(globalFieldsFilePath, this.globalFields);
82-
8373

8474

8575
this.completeProgressWithMessage();
@@ -133,14 +123,15 @@ export default class GlobalFieldsExport extends BaseClass {
133123
delete globalField[key];
134124
}
135125
}
136-
this.globalFields.push(globalField);
126+
const filePath = path.join(this.globalFieldsDirPath, `${globalField.uid}.json`);
127+
fsUtil.writeFile(filePath, globalField);
137128

138129
// Track progress for each global field
139130
this.progressManager?.tick(true, `global-field: ${globalField.uid}`);
140131
});
141132

142133
log.debug(
143-
`Sanitization complete. Total global fields processed: ${this.globalFields.length}`,
134+
`Sanitization complete. Total global fields processed: ${globalFields.length}`,
144135
this.exportConfig.context,
145136
);
146137
}

packages/contentstack-export/test/unit/export/modules/global-fields.test.ts

Lines changed: 50 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,9 @@ describe('ExportGlobalFields', () => {
112112
});
113113
});
114114

115-
it('should initialize empty globalFields array', () => {
116-
expect(exportGlobalFields.globalFields).to.be.an('array');
117-
expect(exportGlobalFields.globalFields.length).to.equal(0);
115+
it('should initialize global fields dir path', () => {
116+
expect(exportGlobalFields.globalFieldsDirPath).to.be.a('string');
117+
expect(exportGlobalFields.globalFieldsDirPath).to.include('global_fields');
118118
});
119119

120120
it('should set correct directory path', () => {
@@ -140,12 +140,12 @@ describe('ExportGlobalFields', () => {
140140

141141
await exportGlobalFields.getGlobalFields();
142142

143-
// Verify global fields were processed
144-
expect(exportGlobalFields.globalFields.length).to.equal(2);
145-
// Verify invalid keys were removed
146-
expect(exportGlobalFields.globalFields[0].invalidKey).to.be.undefined;
147-
expect(exportGlobalFields.globalFields[0].uid).to.equal('gf-1');
148-
expect(exportGlobalFields.globalFields[0].title).to.equal('Field 1');
143+
// Verify each global field written to its own per-uid file (not globalfields.json)
144+
const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub;
145+
expect(writeFileStub.callCount).to.equal(2);
146+
expect(writeFileStub.calledWith(sinon.match(/gf-1\.json/))).to.be.true;
147+
expect(writeFileStub.calledWith(sinon.match(/gf-2\.json/))).to.be.true;
148+
expect(writeFileStub.neverCalledWith(sinon.match(/globalfields\.json/))).to.be.true;
149149
});
150150

151151
it('should call getGlobalFields recursively when more fields exist', async () => {
@@ -171,9 +171,10 @@ describe('ExportGlobalFields', () => {
171171

172172
await exportGlobalFields.getGlobalFields();
173173

174-
// Verify multiple calls were made
174+
// Verify multiple calls were made and each item written as per-uid file
175175
expect(callCount).to.be.greaterThan(1);
176-
expect(exportGlobalFields.globalFields.length).to.equal(150);
176+
const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub;
177+
expect(writeFileStub.callCount).to.equal(150);
177178
});
178179

179180
it('should handle API errors gracefully', async () => {
@@ -201,11 +202,11 @@ describe('ExportGlobalFields', () => {
201202
}),
202203
});
203204

204-
const initialCount = exportGlobalFields.globalFields.length;
205+
const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub;
205206
await exportGlobalFields.getGlobalFields();
206207

207-
// Verify no new global fields were added
208-
expect(exportGlobalFields.globalFields.length).to.equal(initialCount);
208+
// No items → writeFile never called
209+
expect(writeFileStub.called).to.be.false;
209210
});
210211

211212
it('should handle empty items array', async () => {
@@ -218,11 +219,11 @@ describe('ExportGlobalFields', () => {
218219
}),
219220
});
220221

221-
const initialCount = exportGlobalFields.globalFields.length;
222+
const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub;
222223
await exportGlobalFields.getGlobalFields();
223224

224-
// Verify no processing occurred with null items
225-
expect(exportGlobalFields.globalFields.length).to.equal(initialCount);
225+
// Null items → writeFile never called
226+
expect(writeFileStub.called).to.be.false;
226227
});
227228

228229
it('should update query params with skip value', async () => {
@@ -251,28 +252,33 @@ describe('ExportGlobalFields', () => {
251252

252253
exportGlobalFields.sanitizeAttribs(globalFields);
253254

254-
// Verify invalid keys were removed
255-
expect(exportGlobalFields.globalFields[0].invalidKey).to.be.undefined;
256-
expect(exportGlobalFields.globalFields[0].uid).to.equal('gf-1');
257-
expect(exportGlobalFields.globalFields[0].title).to.equal('Field 1');
258-
expect(exportGlobalFields.globalFields[0].validKey).to.equal('value1');
255+
// sanitizeAttribs modifies the array elements in-place
256+
expect(globalFields[0].invalidKey).to.be.undefined;
257+
expect(globalFields[0].uid).to.equal('gf-1');
258+
expect(globalFields[0].title).to.equal('Field 1');
259+
expect(globalFields[0].validKey).to.equal('value1');
260+
// Each field written to its own per-uid file (never to globalfields.json)
261+
const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub;
262+
expect(writeFileStub.calledWith(sinon.match(/gf-1\.json/))).to.be.true;
263+
expect(writeFileStub.calledWith(sinon.match(/gf-2\.json/))).to.be.true;
264+
expect(writeFileStub.neverCalledWith(sinon.match(/globalfields\.json/))).to.be.true;
259265
});
260266

261267
it('should handle global fields without required keys', () => {
262268
const globalFields = [{ uid: 'gf-1', invalidKey: 'remove' }];
263269

264270
exportGlobalFields.sanitizeAttribs(globalFields);
265271

266-
expect(exportGlobalFields.globalFields[0]).to.exist;
267-
expect(exportGlobalFields.globalFields[0].invalidKey).to.be.undefined;
272+
expect(globalFields[0]).to.exist;
273+
expect(globalFields[0].invalidKey).to.be.undefined;
268274
});
269275

270276
it('should handle empty global fields array', () => {
271277
const globalFields: any[] = [];
272278

273279
exportGlobalFields.sanitizeAttribs(globalFields);
274280

275-
expect(exportGlobalFields.globalFields.length).to.equal(0);
281+
expect(globalFields.length).to.equal(0);
276282
});
277283

278284
it('should keep only valid keys from validKeys config', () => {
@@ -289,7 +295,7 @@ describe('ExportGlobalFields', () => {
289295

290296
exportGlobalFields.sanitizeAttribs(globalFields);
291297

292-
const processedField = exportGlobalFields.globalFields[0];
298+
const processedField = globalFields[0];
293299

294300
// Should only keep uid, title, validKey
295301
expect(processedField.keyToRemove1).to.be.undefined;
@@ -298,6 +304,10 @@ describe('ExportGlobalFields', () => {
298304
expect(processedField.uid).to.equal('gf-1');
299305
expect(processedField.title).to.equal('Field 1');
300306
expect(processedField.validKey).to.equal('value1');
307+
// Written to per-uid file path
308+
const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub;
309+
expect(writeFileStub.calledWith(sinon.match(/gf-1\.json/))).to.be.true;
310+
expect(writeFileStub.neverCalledWith(sinon.match(/globalfields\.json/))).to.be.true;
301311
});
302312
});
303313

@@ -322,16 +332,14 @@ describe('ExportGlobalFields', () => {
322332

323333
await exportGlobalFields.start();
324334

325-
// Verify global fields were processed
326-
expect(exportGlobalFields.globalFields.length).to.equal(2);
327-
expect(exportGlobalFields.globalFields[0].uid).to.equal('gf-1');
328-
expect(exportGlobalFields.globalFields[1].uid).to.equal('gf-2');
329-
// Verify file was written
330-
expect(writeFileStub.called).to.be.true;
335+
// Verify each field written to its own per-uid file (not globalfields.json)
336+
expect(writeFileStub.calledWith(sinon.match(/gf-1\.json/))).to.be.true;
337+
expect(writeFileStub.calledWith(sinon.match(/gf-2\.json/))).to.be.true;
338+
expect(writeFileStub.neverCalledWith(sinon.match(/globalfields\.json/))).to.be.true;
331339
expect(makeDirectoryStub.called).to.be.true;
332340
});
333341

334-
it('should handle empty global fields and still write file', async () => {
342+
it('should return early when no global fields found', async () => {
335343
const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub;
336344

337345
mockStackClient.globalField.returns({
@@ -343,12 +351,10 @@ describe('ExportGlobalFields', () => {
343351
}),
344352
});
345353

346-
exportGlobalFields.globalFields = [];
347354
await exportGlobalFields.start();
348355

349-
// Verify writeFile was called even with empty array
350-
expect(writeFileStub.called).to.be.true;
351-
expect(exportGlobalFields.globalFields.length).to.equal(0);
356+
// start() exits early on count=0 — no per-uid files written
357+
expect(writeFileStub.called).to.be.false;
352358
});
353359

354360
it('should handle errors during export without throwing', async () => {
@@ -398,9 +404,11 @@ describe('ExportGlobalFields', () => {
398404

399405
await exportGlobalFields.start();
400406

401-
// Verify all fields were processed
402-
expect(exportGlobalFields.globalFields.length).to.equal(150);
407+
// Verify all fields processed — one writeFile call per item, no globalfields.json
408+
const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub;
409+
expect(writeFileStub.callCount).to.equal(150);
403410
expect(callCount).to.be.greaterThan(1);
411+
expect(writeFileStub.neverCalledWith(sinon.match(/globalfields\.json/))).to.be.true;
404412
});
405413

406414
it('should call makeDirectory and writeFile with correct paths', async () => {
@@ -418,9 +426,10 @@ describe('ExportGlobalFields', () => {
418426

419427
await exportGlobalFields.start();
420428

421-
// Verify directories and files were created
429+
// Verify directory created and per-uid file written (not globalfields.json)
422430
expect(makeDirectoryStub.called).to.be.true;
423-
expect(writeFileStub.called).to.be.true;
431+
expect(writeFileStub.calledWith(sinon.match(/gf-1\.json/))).to.be.true;
432+
expect(writeFileStub.neverCalledWith(sinon.match(/globalfields\.json/))).to.be.true;
424433
});
425434
});
426435
});

packages/contentstack-import-setup/src/config/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ const config: DefaultConfig = {
8787
},
8888
'global-fields': {
8989
dirName: 'global_fields',
90-
fileName: 'globalfields.json',
90+
fileName: 'globalfields.json', // Not used - reads individual {uid}.json files
9191
dependencies: ['extensions', 'marketplace-apps'],
9292
},
9393
'marketplace-apps': {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ const config: DefaultConfig = {
177177
},
178178
'global-fields': {
179179
dirName: 'global_fields',
180-
fileName: 'globalfields.json',
180+
fileName: 'globalfields.json', // Not used - reads individual {uid}.json files
181181
validKeys: ['title', 'uid', 'schema', 'options', 'singleton', 'description'],
182182
limit: 100,
183183
},

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
import * as path from 'path';
88
import { find, cloneDeep, map } from 'lodash';
9-
import { sanitizePath, log, handleAndLogError, readContentTypeSchemas } from '@contentstack/cli-utilities';
9+
import { sanitizePath, log, handleAndLogError, readContentTypeSchemas, readGlobalFieldSchemas } from '@contentstack/cli-utilities';
1010
import { PATH_CONSTANTS } from '../../constants';
1111
import { ImportConfig, ModuleClassParams } from '../../types';
1212
import BaseClass, { ApiOptions } from './base-class';
@@ -50,7 +50,6 @@ export default class ContentTypesImport extends BaseClass {
5050
};
5151
private gFsConfig: {
5252
dirName: string;
53-
fileName: string;
5453
validKeys: string[];
5554
limit: number;
5655
writeConcurrency?: number;
@@ -105,6 +104,7 @@ export default class ContentTypesImport extends BaseClass {
105104
['__master.json', 'true'],
106105
['__priority.json', 'true'],
107106
[PATH_CONSTANTS.FILES.SCHEMA, 'true'],
107+
['globalfields.json', 'true'],
108108
['.DS_Store', 'true'],
109109
]);
110110

@@ -584,7 +584,7 @@ export default class ContentTypesImport extends BaseClass {
584584
'CONTENT TYPES: Analyzing import data...',
585585
async () => {
586586
const cts = readContentTypeSchemas(this.cTsFolderPath);
587-
const gfs = fsUtil.readFile(path.resolve(this.gFsFolderPath, this.gFsConfig.fileName));
587+
const gfs = readGlobalFieldSchemas(this.gFsFolderPath);
588588
const pendingGfs = fsUtil.readFile(this.gFsPendingPath);
589589
const pendingExt = fsUtil.readFile(this.extPendingPath);
590590
return [cts, gfs, pendingGfs, pendingExt];

0 commit comments

Comments
 (0)