@ds-sfdc/sfparty 0.0.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/LICENSE.md ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, Tim Paulaskas
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @ds-sfdc/sfparty
2
+
3
+ [![NPM](https://img.shields.io/npm/v/@ds-sfdc/sfparty.svg?label=@ds-sfdc/sfparty)](https://www.npmjs.com/package/@ds-sfdc/sfparty) [![Downloads/week](https://img.shields.io/npm/dw/@ds-sfdc/sfparty.svg)](https://npmjs.org/package/@ds-sfdc/sfparty) [![License](https://img.shields.io/badge/License-BSD%203--Clause-brightgreen.svg)](https://github.com/TimPaulaskasDS/sfparty/blob/main/LICENSE.md)
4
+
5
+ ## Using the template
6
+
7
+ This tool will split Salesforce metadata XML files into YAML parts (or JSON).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm i @ds-sfdc/sfparty
13
+ ```
14
+ ## Commands
15
+
16
+ <!-- commands -->
17
+
18
+ <!-- commandsstop -->
package/index.js ADDED
@@ -0,0 +1,535 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+ import { readFileSync } from 'fs'
4
+ import path from 'path'
5
+ import yargs from 'yargs'
6
+ import { hideBin } from 'yargs/helpers'
7
+ import winston from 'winston'
8
+ import chalk from 'chalk'
9
+ import convertHrtime from 'convert-hrtime'
10
+ import * as fileUtils from './lib/fileUtils.js'
11
+ import * as profileSplit from './lib/profile/split.js'
12
+ import * as profileCombine from './lib/profile/combine.js'
13
+ import * as permSetSplit from './lib/permset/split.js'
14
+ import * as permSetCombine from './lib/permset/combine.js'
15
+ import * as labelSplit from './lib/label/split.js'
16
+ import * as labelCombine from './lib/label/combine.js'
17
+ import * as workflowSplit from './lib/workflow/split.js'
18
+ import * as workflowCombine from './lib/workflow/combine.js'
19
+ import * as workflowDefinition from './lib/workflow/definition.js'
20
+
21
+ const startTime = process.hrtime.bigint()
22
+
23
+
24
+ global.logger = winston.createLogger({
25
+ levels: winston.config.syslog.levels,
26
+ format: winston.format.cli(),
27
+ defaultMeta: { service: 'dstools', method: 'profile' },
28
+ transports: [
29
+ new winston.transports.Console(),
30
+ ],
31
+ });
32
+
33
+ global.processed = {
34
+ total: 0,
35
+ errors: 0,
36
+ current: 1,
37
+ }
38
+
39
+ global.statusLevel = {
40
+ "warn": '🔕',
41
+ "success": chalk.greenBright('✔'),
42
+ "fail": '❗',
43
+ "working": '⏳'
44
+ }
45
+
46
+ function getRootPath(packageDir) {
47
+ let rootPath = fileUtils.find('sfdx-project.json')
48
+ let defaultDir
49
+ if (rootPath) {
50
+ global.__basedir = fileUtils.fileInfo(rootPath).dirname
51
+ let packageJSON = JSON.parse(readFileSync(rootPath))
52
+ if (Array.isArray(packageJSON.packageDirectories)) {
53
+ packageJSON.packageDirectories.every(directory => {
54
+ if (directory.default || packageJSON.packageDirectories.length == 1) defaultDir = directory.path
55
+ if (directory == packageDir) {
56
+ defaultDir = directory
57
+ return false
58
+ }
59
+ return true
60
+ })
61
+ }
62
+ } else {
63
+ global.logger.error('Could not determine base path of Salesforce source directory. No sfdx-project.json found. Please specify a source path or execute from Salesforce project directory.')
64
+ process.exit(1)
65
+ }
66
+ if (packageDir && packageDir != defaultDir) {
67
+ global.logger.error('Could not find directory in sfdx-project.json. Please specify a package directory path from the sfdx-project.json file.')
68
+ process.exit(1)
69
+ }
70
+
71
+ return defaultDir
72
+ }
73
+
74
+ let errorMessage = chalk.red('Please specify the action of ' + chalk.whiteBright.bgRedBright('split') + ' or ' + chalk.whiteBright.bgRedBright('combine') + '.')
75
+
76
+ yargs(hideBin(process.argv))
77
+ .alias('h', 'help')
78
+ .command({
79
+ command: '[split]',
80
+ alias: 'split',
81
+ description: 'splits metadata xml to json files',
82
+ builder: (yargs) => {
83
+ yargs
84
+ .example([
85
+ ['$0 split --type=profile --all'],
86
+ ['$0 split --type=permset --name="Permission Set Name"'],
87
+ ['--source=packageDir --target=dir/dir'],
88
+ ['name portion of file: [name].profile-meta.xml'],
89
+ ['Example: --name="Admin" for Admin.profile-meta.xml'],
90
+ ['\nCommands not supporting name or all parameters:'],
91
+ ['$0 split --type=label'],
92
+ ])
93
+ .options({
94
+ type: {
95
+ demand: true,
96
+ alias: 'type',
97
+ description: 'type of metadata to split',
98
+ demandOption: true,
99
+ type: 'string',
100
+ },
101
+ format: {
102
+ demand: true,
103
+ alias: 'format',
104
+ default: 'yaml',
105
+ description: 'type of output',
106
+ demandOption: true,
107
+ type: 'string',
108
+ },
109
+ name: {
110
+ alias: 'n',
111
+ description: 'name of metadata file to split',
112
+ demandOption: false,
113
+ type: 'string',
114
+ },
115
+ all: {
116
+ alias: 'a',
117
+ description: 'all metadata files of type will be split',
118
+ demandOption: false,
119
+ type: 'boolean',
120
+ },
121
+ source: {
122
+ demand: false,
123
+ alias: 's',
124
+ description: 'package directory path specified in sfdx-project.json',
125
+ type: 'string',
126
+ },
127
+ target: {
128
+ demand: false,
129
+ alias: 't',
130
+ description: 'target path to directory to create json files',
131
+ type: 'string',
132
+ }
133
+ })
134
+ .choices('type', ['label', 'permset', 'profile', 'workflow'])
135
+ .choices('format', ['json', 'yaml'])
136
+ .check((argv, options) => {
137
+ const name = argv.name
138
+ const all = argv.all
139
+
140
+ switch (argv.type) {
141
+ case 'profile':
142
+ case 'permset':
143
+ case 'workflow':
144
+ if ((typeof name != 'undefined' || name == '') && (typeof all != 'undefined' && all)) {
145
+ throw new Error(chalk.redBright('You cannot specify ' + chalk.whiteBright.bgRedBright('--name') + ' and ' + chalk.whiteBright.bgRedBright('--all') + ' at the same time.'))
146
+ } else if (typeof name == 'undefined' && (typeof all == 'undefined' || !all)) {
147
+ throw new Error(chalk.redBright('You must specify the ' + chalk.whiteBright.bgRedBright('--name') + ' parameter or use the ' + chalk.whiteBright.bgRedBright('--all') + ' switch.'))
148
+ } else {
149
+ return true // tell Yargs that the arguments passed the check
150
+ }
151
+ case 'label':
152
+ if ((typeof name != 'undefined' && name != '') || (typeof all != 'undefined' && all)) {
153
+ throw new Error(chalk.redBright('You cannot specify ' + chalk.whiteBright.bgRedBright('--name') + ' or ' + chalk.whiteBright.bgRedBright('--all') + ' when using label.'))
154
+ }
155
+ return true
156
+ }
157
+ })
158
+ },
159
+ handler: (argv) => {
160
+ let metaExtension
161
+ const fileList = []
162
+ let type = argv.type
163
+ global.format = argv.format
164
+ switch (type) {
165
+ case 'profile':
166
+ type = 'profiles'
167
+ metaExtension = '.profile-meta.xml'
168
+ break
169
+ case 'permset':
170
+ type = 'permissionsets'
171
+ metaExtension = '.permissionset-meta.xml'
172
+ break
173
+ case 'label':
174
+ type = 'labels'
175
+ metaExtension = '.labels-meta.xml'
176
+ break
177
+ case 'workflow':
178
+ type = 'workflows'
179
+ metaExtension = '.workflow-meta.xml'
180
+ break
181
+ default:
182
+ global.logger.error('Metadata type not supported: ' + type)
183
+ process.exit(1)
184
+ }
185
+
186
+ let sourceDir = argv.source || ''
187
+ let targetDir = argv.target || ''
188
+ let name = argv.name
189
+ let all = argv.all
190
+ let packageDir = getRootPath(sourceDir)
191
+
192
+ if (type == 'labels') {
193
+ name = 'CustomLabels'
194
+ }
195
+ sourceDir = path.join(global.__basedir, packageDir, 'main', 'default', type)
196
+ if (targetDir == '') {
197
+ targetDir = path.join(global.__basedir, packageDir + '-party', 'main', 'default', type)
198
+ } else {
199
+ targetDir = path.join(targetDir, 'main', 'default', type)
200
+ }
201
+ let metaDirPath = sourceDir
202
+ console.log(`${chalk.bgBlackBright('Source path:')} ${sourceDir}`)
203
+ console.log(`${chalk.bgBlackBright('Target path:')} ${targetDir}`)
204
+ console.log()
205
+
206
+ if (!all) {
207
+ let metaFilePath = path.join(metaDirPath, name)
208
+ if (!fileUtils.fileExists(metaFilePath)) {
209
+ name += metaExtension
210
+ metaFilePath = path.join(metaDirPath, name)
211
+ if (!fileUtils.fileExists(metaFilePath)) {
212
+ global.logger.error('File not found: ' + metaFilePath)
213
+ process.exit(1)
214
+ }
215
+ }
216
+ fileList.push(name)
217
+ } else {
218
+ if (fileUtils.directoryExists(sourceDir)) {
219
+ fileUtils.getFiles(sourceDir, metaExtension).forEach(file => {
220
+ fileList.push(file)
221
+ })
222
+ }
223
+ }
224
+
225
+ global.processed.total = fileList.length
226
+ console.log(`Splitting a total of ${fileList.length} file(s)`)
227
+ console.log()
228
+ const promList = []
229
+ fileList.forEach(metaFile => {
230
+ switch (type) {
231
+ case 'profiles':
232
+ const profile = new profileSplit.Profile({
233
+ sourceDir: sourceDir,
234
+ targetDir: targetDir,
235
+ metaFilePath: path.join(sourceDir, metaFile),
236
+ sequence: promList.length + 1,
237
+ })
238
+ const profProm = profile.split()
239
+ promList.push(profProm)
240
+ profProm.then(() => {
241
+ global.processed.current++
242
+ })
243
+ break
244
+ case 'permissionsets':
245
+ const permSet = new permSetSplit.Permset({
246
+ sourceDir: sourceDir,
247
+ targetDir: targetDir,
248
+ metaFilePath: path.join(sourceDir, metaFile),
249
+ sequence: promList.length + 1,
250
+ })
251
+ const permProm = permSet.split()
252
+ promList.push(permProm)
253
+ permProm.then(() => {
254
+ global.processed.current++
255
+ })
256
+ break
257
+ case 'labels':
258
+ const label = new labelSplit.CustomLabel({
259
+ sourceDir: sourceDir,
260
+ targetDir: targetDir,
261
+ metaFilePath: path.join(sourceDir, metaFile),
262
+ sequence: promList.length + 1,
263
+ })
264
+ const labelProm = label.split()
265
+ promList.push(labelProm)
266
+ labelProm.then(() => {
267
+ global.processed.current++
268
+ })
269
+ break
270
+ case 'workflows':
271
+ const workflow = new workflowSplit.Split({
272
+ metadataDefinition: workflowDefinition.metadataDefinition,
273
+ sourceDir: sourceDir,
274
+ targetDir: targetDir,
275
+ metaFilePath: path.join(sourceDir, metaFile),
276
+ sequence: promList.length + 1,
277
+ })
278
+ const workflowProm = workflow.split()
279
+ promList.push(workflowProm)
280
+ workflowProm.then((resolve, reject) => {
281
+ if (resolve == false) {
282
+ global.processed.errors++
283
+ global.processed.current--
284
+ } else {
285
+ global.processed.current++
286
+ }
287
+ })
288
+ break
289
+ }
290
+ })
291
+ Promise.allSettled(promList).then((results) => {
292
+ let message = `Split ${chalk.bgBlackBright(global.processed.current)} file(s) ${(global.processed.errors > 0) ? 'with ' + chalk.bgBlackBright.red(global.processed.errors) + ' error(s) ' : ''}in `
293
+ displayMessage(startTime, message)
294
+ })
295
+ }
296
+ })
297
+ .command({
298
+ command: '[combine]',
299
+ alias: 'combine',
300
+ description: 'combines json files into metadata xml',
301
+ builder: (yargs) => {
302
+ yargs
303
+ .example([
304
+ ['$0 combine --type=profile --all'],
305
+ ['$0 combine --type=permset --name="Permission Set Name"'],
306
+ ['--source=packageDir --target=dir/dir'],
307
+ ['name portion of file: [name].profile-meta.xml'],
308
+ ['Example: --name="Admin" for Admin.profile-meta.xml'],
309
+ ['\nCommands not supporting name or all parameters:'],
310
+ ['$0 combine --type=label'],])
311
+ .options({
312
+ type: {
313
+ demand: true,
314
+ alias: 'type',
315
+ description: 'type of metadata to combine',
316
+ demandOption: true,
317
+ type: 'string',
318
+ },
319
+ format: {
320
+ demand: true,
321
+ alias: 'format',
322
+ default: 'yaml',
323
+ description: 'type of output',
324
+ demandOption: true,
325
+ type: 'string',
326
+ },
327
+ name: {
328
+ alias: 'n',
329
+ description: 'name of metadata file to combine',
330
+ demandOption: false,
331
+ type: 'string',
332
+ },
333
+ all: {
334
+ alias: 'a',
335
+ description: 'all json files of type will be combined',
336
+ demandOption: false,
337
+ type: 'boolean',
338
+ },
339
+ source: {
340
+ demand: false,
341
+ alias: 's',
342
+ description: 'package directory path specified in sfdx-project.json',
343
+ type: 'string',
344
+ },
345
+ target: {
346
+ demand: false,
347
+ alias: 't',
348
+ description: 'target path to directory to create xml files',
349
+ type: 'string',
350
+ }
351
+ })
352
+ .choices('type', ['label', 'permset', 'profile', 'workflow'])
353
+ .choices('format', ['json', 'yaml'])
354
+ .check((argv, options) => {
355
+ const name = argv.name
356
+ const all = argv.all
357
+
358
+ switch (argv.type) {
359
+ case 'profile':
360
+ case 'permset':
361
+ case 'workflow':
362
+ if ((typeof name != 'undefined' || name == '') && (typeof all != 'undefined' && all)) {
363
+ throw new Error(chalk.redBright('You cannot specify ' + chalk.whiteBright.bgRedBright('--name') + ' and ' + chalk.whiteBright.bgRedBright('--all') + ' at the same time.'))
364
+ } else if (typeof name == 'undefined' && (typeof all == 'undefined' || !all)) {
365
+ throw new Error(chalk.redBright('You must specify the ' + chalk.whiteBright.bgRedBright('--name') + ' parameter or use the ' + chalk.whiteBright.bgRedBright('--all') + ' switch.'))
366
+ } else {
367
+ return true // tell Yargs that the arguments passed the check
368
+ }
369
+ case 'label':
370
+ if ((typeof name != 'undefined' && name != '') || (typeof all != 'undefined' && all)) {
371
+ throw new Error(chalk.redBright('You cannot specify ' + chalk.whiteBright.bgRedBright('--name') + ' or ' + chalk.whiteBright.bgRedBright('--all') + ' when using label.'))
372
+ }
373
+ return true
374
+ }
375
+ })
376
+ },
377
+ handler: (argv) => {
378
+ let processList = []
379
+ global.format = argv.format
380
+ let type = argv.type
381
+ switch (type) {
382
+ case 'profile':
383
+ type = 'profiles'
384
+ break
385
+ case 'permset':
386
+ type = 'permissionsets'
387
+ break
388
+ case 'label':
389
+ type = 'labels'
390
+ break
391
+ case 'workflow':
392
+ type = 'workflows'
393
+ break
394
+ default:
395
+ global.logger.error('Metadata type not supported: ' + type)
396
+ process.exit(1)
397
+ }
398
+
399
+ let sourceDir = argv.source || ''
400
+ let targetDir = argv.target || ''
401
+ let name = argv.name
402
+ let all = argv.all
403
+ let packageDir = getRootPath(sourceDir)
404
+
405
+ sourceDir = path.join(global.__basedir, packageDir + '-party', 'main', 'default', type)
406
+ if (targetDir == '') {
407
+ targetDir = path.join(global.__basedir, packageDir, 'main', 'default', type)
408
+ } else {
409
+ targetDir = path.join(targetDir, 'main', 'default', type)
410
+ }
411
+
412
+ console.log(`${chalk.bgBlackBright('Source path:')} ${sourceDir}`)
413
+ console.log(`${chalk.bgBlackBright('Target path:')} ${targetDir}`)
414
+ console.log()
415
+
416
+ if (type == 'labels') {
417
+ processList = fileUtils.getFiles(sourceDir, `.${global.format}`)
418
+ } else if (!all) {
419
+ let metaDirPath = path.join(sourceDir, name)
420
+ if (!fileUtils.directoryExists(metaDirPath)) {
421
+ global.logger.error('Directory not found: ' + metaDirPath)
422
+ process.exit(1)
423
+ }
424
+ processList.push(name)
425
+ } else {
426
+ processList = fileUtils.getDirectories(sourceDir)
427
+ }
428
+
429
+ global.processed.total = processList.length
430
+ console.log(`Combining a total of ${global.processed.total} file(s)`)
431
+ console.log()
432
+
433
+ const promList = []
434
+ if (type != 'labels') {
435
+ processList.forEach(metaDir => {
436
+ if (type == 'profiles') {
437
+ const profile = new profileCombine.Profile({
438
+ sourceDir: sourceDir,
439
+ targetDir: targetDir,
440
+ metaDir: metaDir,
441
+ sequence: promList.length + 1,
442
+ })
443
+ const profProm = profile.combine()
444
+ promList.push(profProm)
445
+ profProm.then(() => {
446
+ global.processed.current++
447
+ })
448
+ } else if (type == 'permissionsets') {
449
+ const permSet = new permSetCombine.Permset({
450
+ sourceDir: sourceDir,
451
+ targetDir: targetDir,
452
+ metaDir: metaDir,
453
+ sequence: promList.length + 1,
454
+ })
455
+ const permProm = permSet.combine()
456
+ promList.push(permProm)
457
+ permProm.then(() => {
458
+ global.processed.current++
459
+ })
460
+ } else if (type == 'workflows') {
461
+ const workflow = new workflowCombine.Combine({
462
+ metadataDefinition: workflowDefinition.metadataDefinition,
463
+ sourceDir: sourceDir,
464
+ targetDir: targetDir,
465
+ metaDir: metaDir,
466
+ sequence: promList.length + 1,
467
+ })
468
+ const workflowProm = workflow.combine()
469
+ promList.push(workflowProm)
470
+ workflowProm.then((resolve, reject) => {
471
+ global.processed.current++
472
+ })
473
+ }
474
+ })
475
+ } else if (type == 'labels') {
476
+ const label = new labelCombine.CustomLabel({
477
+ sourceDir: sourceDir,
478
+ targetDir: targetDir,
479
+ metaDir: sourceDir, // use sourceDir
480
+ processList: processList,
481
+ })
482
+ const labelProm = label.combine()
483
+ promList.push(labelProm)
484
+ labelProm.then(() => {
485
+ global.processed.current++
486
+ })
487
+
488
+ }
489
+
490
+ Promise.allSettled(promList).then(([result]) => {
491
+ if (result !== undefined && result.status == 'fulfilled') {
492
+ let message = `Combined ${chalk.bgBlackBright(global.processed.total)} file(s) in `
493
+ displayMessage(startTime, message)
494
+ } else {
495
+ global.logger.error((result !== undefined) ? result.reason : 'metadata error')
496
+ }
497
+ })
498
+ }
499
+ })
500
+ .demandCommand(1, errorMessage)
501
+ .example([
502
+ ['$0 split --type=profile --all'],
503
+ ['$0 split --type=profile --name="Profile Name"'],
504
+ ['$0 combine --type=permset --all'],
505
+ ['$0 combine --type=permset --name="Permission Set Name"'],
506
+ ])
507
+ .help()
508
+ .argv
509
+ .parse
510
+
511
+ function displayMessage(startTime, message) {
512
+ const diff = process.hrtime.bigint() - BigInt(startTime)
513
+ let durationMessage
514
+ let executionTime = convertHrtime(diff);
515
+ let minutes = Math.floor((executionTime.seconds + Math.round(executionTime.milliseconds / 100000)) / 60)
516
+ let seconds = Math.round((executionTime.seconds + Math.round(executionTime.milliseconds / 100000)) % 60)
517
+ if (minutes == 0 && seconds == 0) {
518
+ durationMessage = message + chalk.magentaBright(`<1s`)
519
+ } else if (minutes > 0) {
520
+ durationMessage = message + chalk.magentaBright(`${minutes}m ${seconds}s`)
521
+ } else {
522
+ durationMessage = message + chalk.magentaBright(`${seconds}s`)
523
+ }
524
+ console.log('\n' + durationMessage)
525
+ }
526
+
527
+ let callAmount = 0;
528
+ process.on('SIGINT', function () {
529
+ if (callAmount < 1) {
530
+ console.log(`✅ Received abort command`);
531
+ process.exit(1)
532
+ }
533
+
534
+ callAmount++;
535
+ })