wanas-zcrm-extractor 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/bin/cli.js +4 -1
- package/package.json +1 -1
- package/src/services/metadata/crmMetadataService.js +126 -34
package/README.md
CHANGED
|
@@ -276,17 +276,24 @@ metadata/
|
|
|
276
276
|
│
|
|
277
277
|
├── crm/ # Standard Zoho CRM metadata layout
|
|
278
278
|
│ ├── meta/
|
|
279
|
-
│ │ ├── custom_links/ # Global Custom Links
|
|
280
279
|
│ │ ├── data_sharing/ # Data Sharing Settings
|
|
281
280
|
│ │ ├── data_sharing_actions_summary/ # Data Sharing Actions Summary
|
|
282
281
|
│ │ ├── data_sharing_rules/ # Data Sharing Rules
|
|
283
|
-
│ │ ├── email_templates/ # Email Templates
|
|
282
|
+
│ │ ├── email_templates/ # Email Templates (grouped by module)
|
|
283
|
+
│ │ │ └── <Module_API_Name>/
|
|
284
|
+
│ │ │ ├── <Template_Name>.meta.json
|
|
285
|
+
│ │ │ └── <Template_Name>.html # rendered from mail_content
|
|
284
286
|
│ │ ├── global_picklists/ # Global Picklists
|
|
285
|
-
│ │ ├── inventory_templates/# Inventory Templates
|
|
287
|
+
│ │ ├── inventory_templates/# Inventory Templates (grouped by module)
|
|
288
|
+
│ │ │ └── <Module_API_Name>/
|
|
289
|
+
│ │ │ ├── <Template_Name>.meta.json
|
|
290
|
+
│ │ │ └── <Template_Name>.html # template HTML body
|
|
286
291
|
│ │ ├── territories/ # Territories
|
|
287
292
|
│ │ ├── variables/ # Global Variables
|
|
288
293
|
│ │ ├── wizards/ # CRM Wizards
|
|
289
|
-
│ │ ├── workflow_rules/ #
|
|
294
|
+
│ │ ├── workflow_rules/ # Workflow Rules (grouped by trigger module)
|
|
295
|
+
│ │ │ └── <Trigger_Module_API_Name>/
|
|
296
|
+
│ │ │ └── <Rule_Name>.json
|
|
290
297
|
│ │ ├── modules/ # Individual Module schemas
|
|
291
298
|
│ │ │ └── <Module_API_Name>/
|
|
292
299
|
│ │ │ ├── <Module_API_Name>.modules-meta.json
|
|
@@ -300,6 +307,8 @@ metadata/
|
|
|
300
307
|
│ │ │ │ └── <Layout_ID>.json
|
|
301
308
|
│ │ │ ├── custom_views/ # Custom View configurations
|
|
302
309
|
│ │ │ │ └── <View_API_Name>.custom_views-meta.json
|
|
310
|
+
│ │ │ ├── custom_links/ # Module custom links/buttons
|
|
311
|
+
│ │ │ │ └── <Link_ID>.json
|
|
303
312
|
│ │ │ ├── record_locking_configurations/ # Record Locking Rules
|
|
304
313
|
│ │ │ │ └── <Config_ID>.json
|
|
305
314
|
│ │ │ ├── related_lists/ # Related List configurations
|
package/bin/cli.js
CHANGED
|
@@ -522,8 +522,11 @@ program
|
|
|
522
522
|
console.log(` \x1b[1mStale Files Removed:\x1b[0m \x1b[33m${details.removedStale || 0}\x1b[0m (deleted on Zoho CRM since last pull)`);
|
|
523
523
|
console.log(` \x1b[1mOutput Directory:\x1b[0m \x1b[34m${outputDir}\x1b[0m`);
|
|
524
524
|
|
|
525
|
+
if (details.skipped > 0) {
|
|
526
|
+
console.log(` \x1b[1mSkipped (optional):\x1b[0m \x1b[90m${details.skipped}\x1b[0m settings not available for some modules/org (normal).`);
|
|
527
|
+
}
|
|
525
528
|
if (details.errors && details.errors.length > 0) {
|
|
526
|
-
console.log(`\n \x1b[1;33m⚠️
|
|
529
|
+
console.log(`\n \x1b[1;33m⚠️ Errors:\x1b[0m \x1b[33m${details.errors.length} unexpected API failure(s).\x1b[0m Details in the log file.`);
|
|
527
530
|
}
|
|
528
531
|
console.log('\x1b[1;32m====================================================================\x1b[0m\n');
|
|
529
532
|
break;
|
package/package.json
CHANGED
|
@@ -40,6 +40,38 @@ function getNamespaceForCategory(category) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Makes an arbitrary string safe to use as a file/directory name across
|
|
45
|
+
* platforms (strips illegal characters, collapses whitespace, trims trailing
|
|
46
|
+
* dots/spaces which Windows forbids, and caps the length).
|
|
47
|
+
* @param {*} name
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
function sanitizeFileName(name) {
|
|
51
|
+
const cleaned = String(name == null ? '' : name)
|
|
52
|
+
.replace(/[\\/:*?"<>|]/g, '_')
|
|
53
|
+
.replace(/\s+/g, '_')
|
|
54
|
+
.replace(/_+/g, '_');
|
|
55
|
+
const truncated = cleaned.slice(0, 120).replace(/^[._]+|[._]+$/g, '');
|
|
56
|
+
return truncated || 'unnamed';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns the first non-empty string value among the candidate fields (used to
|
|
61
|
+
* locate a template's HTML body, whose field name varies by template type).
|
|
62
|
+
* @param {object} item
|
|
63
|
+
* @param {string[]} fields
|
|
64
|
+
* @returns {string|null}
|
|
65
|
+
*/
|
|
66
|
+
function pickHtmlContent(item, fields) {
|
|
67
|
+
if (!item) return null;
|
|
68
|
+
for (const field of fields) {
|
|
69
|
+
const value = item[field];
|
|
70
|
+
if (typeof value === 'string' && value.trim().length > 0) return value;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
43
75
|
/**
|
|
44
76
|
* Metadata extractor service implementation for Zoho CRM V8.
|
|
45
77
|
*/
|
|
@@ -105,6 +137,7 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
105
137
|
totalCustomViews: 0,
|
|
106
138
|
removedStale: 0,
|
|
107
139
|
concurrency,
|
|
140
|
+
skipped: 0,
|
|
108
141
|
errors: []
|
|
109
142
|
};
|
|
110
143
|
|
|
@@ -125,14 +158,30 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
125
158
|
|
|
126
159
|
const fieldMap = {};
|
|
127
160
|
|
|
128
|
-
// Records a failed sub-resource fetch
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
161
|
+
// Records a failed sub-resource fetch. Many sub-resources simply don't apply
|
|
162
|
+
// to every module/org — Zoho returns 400 ("module not supported", "layout
|
|
163
|
+
// deactivated", "feature not enabled", etc.). Those are EXPECTED: they are
|
|
164
|
+
// counted as `skipped` and logged quietly (file only) so they neither spam
|
|
165
|
+
// the console nor corrupt the progress bar. Only genuinely unexpected
|
|
166
|
+
// failures are recorded as `errors` and logged loudly. Pass optional=true
|
|
167
|
+
// for supplementary resources whose absence is always normal.
|
|
168
|
+
const EXPECTED_ERROR_CODES = new Set([
|
|
169
|
+
'NO_PERMISSION', 'NOT_SUPPORTED', 'INVALID_MODULE', 'INVALID_URL_PATTERN',
|
|
170
|
+
'FEATURE_NOT_ENABLED', 'REQUIRED_PARAM_MISSING', 'MANDATORY_NOT_FOUND', 'OAUTH_SCOPE_MISMATCH'
|
|
171
|
+
]);
|
|
172
|
+
const EXPECTED_ERROR_RE = /not supported|not enabled|seems to be invalid|invalid module|deactivated|parameter is missing|expected parameter|no permission|not allowed|not available/i;
|
|
173
|
+
|
|
174
|
+
const handleResourceError = (reason, endpoint, apiName, label, optional = false) => {
|
|
175
|
+
const status = reason?.response?.status;
|
|
132
176
|
const errCode = reason?.response?.data?.code;
|
|
133
|
-
|
|
134
|
-
|
|
177
|
+
const errMsg = reason?.response?.data?.message || reason?.message || '';
|
|
178
|
+
const expected = optional || EXPECTED_ERROR_CODES.has(errCode) || EXPECTED_ERROR_RE.test(errMsg);
|
|
179
|
+
|
|
180
|
+
if (expected) {
|
|
181
|
+
stats.skipped++;
|
|
182
|
+
logInfo(`${label} not available for ${apiName} (${errCode || status || 'n/a'}): ${errMsg}`, 'CrmMetadataService.extract');
|
|
135
183
|
} else {
|
|
184
|
+
stats.errors.push({ endpoint, error: errMsg || String(reason) });
|
|
136
185
|
logError(reason, `CrmMetadataService.extract - ${label}: ${apiName}`);
|
|
137
186
|
}
|
|
138
187
|
};
|
|
@@ -259,7 +308,9 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
259
308
|
apiClient.get('/crm/v8/settings/duplicate_check_preference', { module: apiName }),
|
|
260
309
|
apiClient.get('/crm/v8/settings/record_locking_configurations', { module: apiName }),
|
|
261
310
|
apiClient.get('/crm/v8/settings/tags', { module: apiName }),
|
|
262
|
-
apiClient.get('/crm/v8/workflow_configurations', { module: apiName })
|
|
311
|
+
apiClient.get('/crm/v8/workflow_configurations', { module: apiName }),
|
|
312
|
+
// Custom links are module-scoped; the endpoint requires the module param.
|
|
313
|
+
apiClient.get('/crm/v8/settings/custom_links', { module: apiName })
|
|
263
314
|
];
|
|
264
315
|
// Record count is opt-in (reads record data; ~50 credits/module).
|
|
265
316
|
if (withCounts) {
|
|
@@ -267,7 +318,7 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
267
318
|
apiClient.get(`/crm/v8/${apiName}/actions/count`).catch(() => ({ count: 'unauthorized_or_unsupported' }))
|
|
268
319
|
);
|
|
269
320
|
}
|
|
270
|
-
const [detailR, fieldsR, layoutsR, relatedR, viewsR, dupCheckR, lockConfigR, tagsR, workflowConfigR, countR] = await Promise.allSettled(subResourceCalls);
|
|
321
|
+
const [detailR, fieldsR, layoutsR, relatedR, viewsR, dupCheckR, lockConfigR, tagsR, workflowConfigR, customLinksR, countR] = await Promise.allSettled(subResourceCalls);
|
|
271
322
|
|
|
272
323
|
// 3a. Module Details
|
|
273
324
|
if (detailR.status === 'fulfilled') {
|
|
@@ -341,7 +392,7 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
341
392
|
await outJson(path.join(mapDepDir, `${layoutId}.json`), depR, { spaces: 2 });
|
|
342
393
|
}
|
|
343
394
|
} catch (depErr) {
|
|
344
|
-
handleResourceError(depErr, `/crm/v8/settings/layouts/${layoutId}/map_dependency?module=${apiName}`, apiName, 'Layout Map Dependency');
|
|
395
|
+
handleResourceError(depErr, `/crm/v8/settings/layouts/${layoutId}/map_dependency?module=${apiName}`, apiName, 'Layout Map Dependency', true);
|
|
345
396
|
}
|
|
346
397
|
}
|
|
347
398
|
}
|
|
@@ -409,7 +460,7 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
409
460
|
await outJson(path.join(moduleSubDir, 'duplicate_check_preference.json'), dupResp, { spaces: 2 });
|
|
410
461
|
}
|
|
411
462
|
} else {
|
|
412
|
-
handleResourceError(dupCheckR.reason, `/crm/v8/settings/duplicate_check_preference?module=${apiName}`, apiName, 'Duplicate Check Preference');
|
|
463
|
+
handleResourceError(dupCheckR.reason, `/crm/v8/settings/duplicate_check_preference?module=${apiName}`, apiName, 'Duplicate Check Preference', true);
|
|
413
464
|
}
|
|
414
465
|
|
|
415
466
|
// 3e-2. Record Locking Configurations
|
|
@@ -428,7 +479,7 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
428
479
|
}
|
|
429
480
|
}
|
|
430
481
|
} else {
|
|
431
|
-
handleResourceError(lockConfigR.reason, `/crm/v8/settings/record_locking_configurations?module=${apiName}`, apiName, 'Record Locking Configurations');
|
|
482
|
+
handleResourceError(lockConfigR.reason, `/crm/v8/settings/record_locking_configurations?module=${apiName}`, apiName, 'Record Locking Configurations', true);
|
|
432
483
|
}
|
|
433
484
|
|
|
434
485
|
// 3e-3. Tags
|
|
@@ -447,7 +498,7 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
447
498
|
}
|
|
448
499
|
}
|
|
449
500
|
} else {
|
|
450
|
-
handleResourceError(tagsR.reason, `/crm/v8/settings/tags?module=${apiName}`, apiName, 'Tags');
|
|
501
|
+
handleResourceError(tagsR.reason, `/crm/v8/settings/tags?module=${apiName}`, apiName, 'Tags', true);
|
|
451
502
|
}
|
|
452
503
|
|
|
453
504
|
// 3e-4. Workflow Configurations
|
|
@@ -466,7 +517,26 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
466
517
|
}
|
|
467
518
|
}
|
|
468
519
|
} else {
|
|
469
|
-
handleResourceError(workflowConfigR.reason, `/crm/v8/workflow_configurations?module=${apiName}`, apiName, 'Workflow Configurations');
|
|
520
|
+
handleResourceError(workflowConfigR.reason, `/crm/v8/workflow_configurations?module=${apiName}`, apiName, 'Workflow Configurations', true);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 3e-4b. Custom Links (module-scoped)
|
|
524
|
+
let customLinksHasItems = false;
|
|
525
|
+
if (customLinksR.status === 'fulfilled') {
|
|
526
|
+
const clResp = customLinksR.value;
|
|
527
|
+
if (clResp && Array.isArray(clResp.custom_links) && clResp.custom_links.length > 0) {
|
|
528
|
+
customLinksHasItems = true;
|
|
529
|
+
const clDir = path.join(moduleSubDir, 'custom_links');
|
|
530
|
+
await fs.ensureDir(clDir);
|
|
531
|
+
for (const link of clResp.custom_links) {
|
|
532
|
+
const linkId = link.id || link.name;
|
|
533
|
+
if (linkId) {
|
|
534
|
+
await outJson(path.join(clDir, `${linkId}.json`), link, { spaces: 2 });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
handleResourceError(customLinksR.reason, `/crm/v8/settings/custom_links?module=${apiName}`, apiName, 'Custom Links', true);
|
|
470
540
|
}
|
|
471
541
|
|
|
472
542
|
// 3e-5. Record Count (actions/count) — only when --with-counts is set.
|
|
@@ -505,6 +575,7 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
505
575
|
if (lockHasItems) sweptInModule += await sweepDir(path.join(moduleSubDir, 'record_locking_configurations'), writtenFiles);
|
|
506
576
|
if (tagsHasItems) sweptInModule += await sweepDir(path.join(moduleSubDir, 'tags'), writtenFiles);
|
|
507
577
|
if (wfHasItems) sweptInModule += await sweepDir(path.join(moduleSubDir, 'workflow_configurations'), writtenFiles);
|
|
578
|
+
if (customLinksHasItems) sweptInModule += await sweepDir(path.join(moduleSubDir, 'custom_links'), writtenFiles);
|
|
508
579
|
stats.removedStale += sweptInModule;
|
|
509
580
|
|
|
510
581
|
stats.modulesProcessed++;
|
|
@@ -595,19 +666,46 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
595
666
|
// 4d. Fetch Additional Global Settings
|
|
596
667
|
onProgress('FETCH_GLOBAL_SETTINGS', 'Fetching additional global settings...');
|
|
597
668
|
const globalEndpoints = [
|
|
598
|
-
{ key: '
|
|
599
|
-
{ key: '
|
|
600
|
-
{ key: 'email_templates', endpoint: '/crm/v8/settings/email_templates', label: 'Email Templates', detailPath: (item) => `/crm/v8/settings/email_templates/${item.id}` },
|
|
669
|
+
{ key: 'inventory_templates', endpoint: '/crm/v8/settings/inventory_templates', label: 'Inventory Templates', detailPath: (item) => `/crm/v8/settings/inventory_templates/${item.id}`, groupBy: (it) => it.module && it.module.api_name, htmlFields: ['content', 'html_content', 'mail_content', 'body'], nameFirst: true },
|
|
670
|
+
{ key: 'email_templates', endpoint: '/crm/v8/settings/email_templates', label: 'Email Templates', detailPath: (item) => `/crm/v8/settings/email_templates/${item.id}`, groupBy: (it) => it.module && it.module.api_name, htmlFields: ['mail_content', 'content'], nameFirst: true },
|
|
601
671
|
{ key: 'wizards', endpoint: '/crm/v8/settings/wizards', label: 'Wizards', detailPath: (item) => `/crm/v8/settings/wizards/${item.id}${item.layout?.id ? '?layout_id=' + item.layout.id : ''}` },
|
|
602
672
|
{ key: 'global_picklists', endpoint: '/crm/v8/settings/global_picklists', label: 'Global Picklists', detailPath: (item) => `/crm/v8/settings/global_picklists/${item.id}` },
|
|
603
673
|
{ key: 'rules', metaKey: 'data_sharing_rules', endpoint: '/crm/v8/settings/data_sharing/rules', label: 'Data Sharing Rules', detailPath: (item) => `/crm/v8/settings/data_sharing/rules/${item.id}` },
|
|
604
674
|
{ key: 'data_sharing', endpoint: '/crm/v8/settings/data_sharing', label: 'Data Sharing Settings' },
|
|
605
675
|
{ key: 'summary', metaKey: 'data_sharing_actions_summary', endpoint: '/crm/v8/settings/data_sharing/rules/actions/summary', label: 'Data Sharing Actions Summary' },
|
|
606
676
|
{ key: 'variables', endpoint: '/crm/v8/settings/variables', label: 'Variables', detailPath: (item) => `/crm/v8/settings/variables/${item.id}${item.variable_group?.id ? '?group=' + item.variable_group.id : ''}` },
|
|
607
|
-
{ key: 'workflow_rules', endpoint: '/crm/v8/settings/automation/workflow_rules', label: 'Workflow Rules', detailPath: (item) => `/crm/v8/settings/automation/workflow_rules/${item.id}
|
|
677
|
+
{ key: 'workflow_rules', endpoint: '/crm/v8/settings/automation/workflow_rules', label: 'Workflow Rules', detailPath: (item) => `/crm/v8/settings/automation/workflow_rules/${item.id}`, groupBy: (it) => (it.trigger_module && (it.trigger_module.api_name || it.trigger_module)) || (it.module && it.module.api_name), jsonExt: '.json', nameFirst: true },
|
|
608
678
|
{ key: 'territories', endpoint: '/crm/v8/settings/territories', label: 'Territories', detailPath: (item) => `/crm/v8/settings/territories/${item.id}` }
|
|
609
679
|
];
|
|
610
680
|
|
|
681
|
+
// Writes one global-settings item: places it (optionally) in a subfolder
|
|
682
|
+
// grouped by module, names it, and emits an .html sidecar for templates.
|
|
683
|
+
const writeGlobalItem = async (detailedItem, reqConfig, itemsDir) => {
|
|
684
|
+
if (!detailedItem) return;
|
|
685
|
+
|
|
686
|
+
let targetDir = itemsDir;
|
|
687
|
+
if (reqConfig.groupBy) {
|
|
688
|
+
const groupName = sanitizeFileName(reqConfig.groupBy(detailedItem) || '_ungrouped');
|
|
689
|
+
targetDir = path.join(itemsDir, groupName);
|
|
690
|
+
await fs.ensureDir(targetDir);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const rawBase = reqConfig.nameFirst
|
|
694
|
+
? (detailedItem.name || detailedItem.api_name || detailedItem.id)
|
|
695
|
+
: (detailedItem.api_name || detailedItem.id || detailedItem.name);
|
|
696
|
+
if (!rawBase) return;
|
|
697
|
+
const base = sanitizeFileName(rawBase);
|
|
698
|
+
|
|
699
|
+
const ext = reqConfig.jsonExt || '.meta.json';
|
|
700
|
+
await outJson(path.join(targetDir, `${base}${ext}`), detailedItem, { spaces: 2 });
|
|
701
|
+
|
|
702
|
+
// For templates, write the HTML body as a standalone .html file.
|
|
703
|
+
if (reqConfig.htmlFields) {
|
|
704
|
+
const html = pickHtmlContent(detailedItem, reqConfig.htmlFields);
|
|
705
|
+
if (html) await outFile(path.join(targetDir, `${base}.html`), html, 'utf8');
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
611
709
|
for (const reqConfig of globalEndpoints) {
|
|
612
710
|
try {
|
|
613
711
|
const response = await apiClient.get(reqConfig.endpoint);
|
|
@@ -616,39 +714,33 @@ class CrmMetadataService extends BaseMetadataService {
|
|
|
616
714
|
completeMetadata[outputKey] = items;
|
|
617
715
|
const itemsDir = path.join(zcrmMetaDir, outputKey);
|
|
618
716
|
await fs.ensureDir(itemsDir);
|
|
619
|
-
|
|
717
|
+
|
|
620
718
|
if (items.length > 0 && reqConfig.detailPath) {
|
|
621
719
|
await mapWithConcurrency(items, concurrency, async (item) => {
|
|
622
|
-
|
|
623
|
-
|
|
720
|
+
let detailedItem = item;
|
|
721
|
+
if (item.id) {
|
|
722
|
+
try {
|
|
624
723
|
const detailResponse = await apiClient.get(reqConfig.detailPath(item));
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
} else {
|
|
629
|
-
const itemApiName = item.api_name || item.name;
|
|
630
|
-
if (itemApiName) await outJson(path.join(itemsDir, `${itemApiName}.meta.json`), item, { spaces: 2 });
|
|
724
|
+
detailedItem = (detailResponse[reqConfig.key] && detailResponse[reqConfig.key][0]) ? detailResponse[reqConfig.key][0] : item;
|
|
725
|
+
} catch (detailErr) {
|
|
726
|
+
detailedItem = item; // fall back to the list item on detail-fetch failure
|
|
631
727
|
}
|
|
632
|
-
} catch (detailErr) {
|
|
633
|
-
const itemApiName = item.api_name || item.id || item.name;
|
|
634
|
-
if (itemApiName) await outJson(path.join(itemsDir, `${itemApiName}.meta.json`), item, { spaces: 2 });
|
|
635
728
|
}
|
|
729
|
+
await writeGlobalItem(detailedItem, reqConfig, itemsDir);
|
|
636
730
|
});
|
|
637
731
|
} else {
|
|
638
732
|
for (const item of items) {
|
|
639
|
-
|
|
640
|
-
if (itemApiName) {
|
|
641
|
-
await outJson(path.join(itemsDir, `${itemApiName}.meta.json`), item, { spaces: 2 });
|
|
642
|
-
}
|
|
733
|
+
await writeGlobalItem(item, reqConfig, itemsDir);
|
|
643
734
|
}
|
|
644
735
|
}
|
|
645
736
|
|
|
737
|
+
// Recursive sweep reconciles grouped subfolders too (and prunes empties).
|
|
646
738
|
if (items.length > 0) {
|
|
647
739
|
const removedItems = await sweepDir(itemsDir, writtenFiles);
|
|
648
740
|
stats.removedStale += removedItems;
|
|
649
741
|
}
|
|
650
742
|
} catch (err) {
|
|
651
|
-
handleResourceError(err, reqConfig.endpoint, 'Global', reqConfig.label);
|
|
743
|
+
handleResourceError(err, reqConfig.endpoint, 'Global', reqConfig.label, true);
|
|
652
744
|
onProgress(`FETCH_${reqConfig.key.toUpperCase()}_WARNING`, `Could not fetch ${reqConfig.label}.`);
|
|
653
745
|
}
|
|
654
746
|
}
|