@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 +28 -0
- package/README.md +18 -0
- package/index.js +535 -0
- package/lib/fileUtils.js +165 -0
- package/lib/label/combine.js +203 -0
- package/lib/label/definition.js +12 -0
- package/lib/label/split.js +213 -0
- package/lib/permset/combine.js +286 -0
- package/lib/permset/definition.js +74 -0
- package/lib/permset/split.js +287 -0
- package/lib/profile/combine.js +309 -0
- package/lib/profile/definition.js +55 -0
- package/lib/profile/split.js +983 -0
- package/lib/workflow/combine.js +330 -0
- package/lib/workflow/definition.js +55 -0
- package/lib/workflow/split.js +278 -0
- package/nodemon.json +4 -0
- package/package.json +51 -0
- package/sfdx-project.json +11 -0
- package/tests/root.spec.js +14 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import os from 'os'
|
|
3
|
+
import { readFileSync, writeFileSync, utimesSync } from 'fs'
|
|
4
|
+
import logUpdate from 'log-update'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
import convertHrtime from 'convert-hrtime'
|
|
7
|
+
import cliSpinners from 'cli-spinners'
|
|
8
|
+
import * as fileUtils from '../fileUtils.js'
|
|
9
|
+
import { profileDefinition } from './definition.js'
|
|
10
|
+
import * as xml2js from 'xml2js'
|
|
11
|
+
import * as yaml from 'js-yaml'
|
|
12
|
+
|
|
13
|
+
const spinner = cliSpinners['dots']
|
|
14
|
+
|
|
15
|
+
export class Profile {
|
|
16
|
+
#xml = ''
|
|
17
|
+
#types = []
|
|
18
|
+
#spinnerMessage = ''
|
|
19
|
+
#index = 0
|
|
20
|
+
#startTime = 0
|
|
21
|
+
#fileName = ''
|
|
22
|
+
#errorMessage = ''
|
|
23
|
+
#fileStats
|
|
24
|
+
#root = 'Profile'
|
|
25
|
+
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.sourceDir = config.sourceDir
|
|
28
|
+
this.targetDir = config.targetDir
|
|
29
|
+
this.metaDir = config.metaDir
|
|
30
|
+
this.sequence = config.sequence
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
combine() {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const that = this
|
|
36
|
+
if (!fileUtils.directoryExists(path.join(that.sourceDir, that.metaDir))) reject(that.metaDir)
|
|
37
|
+
|
|
38
|
+
that.metaDir = fileUtils.getDirectories(that.sourceDir).find(element => element.toLowerCase() == that.metaDir.toLowerCase())
|
|
39
|
+
|
|
40
|
+
that.#xml = `<?xml version="1.0" encoding="UTF-8"?>${os.EOL}`
|
|
41
|
+
that.#xml += `<Profile xmlns="https://soap.sforce.com/2006/04/metadata">${os.EOL}`
|
|
42
|
+
|
|
43
|
+
profileDefinition.main.forEach(key => { that.#types.push(key) })
|
|
44
|
+
profileDefinition.singleFiles.forEach(key => { that.#types.push(key) })
|
|
45
|
+
profileDefinition.directories.forEach(key => { that.#types.push(key) })
|
|
46
|
+
that.#types.sort()
|
|
47
|
+
|
|
48
|
+
setFileName(that)
|
|
49
|
+
processProfile(that)
|
|
50
|
+
|
|
51
|
+
saveXML(that)
|
|
52
|
+
resolve(that.metaDir)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
function setFileName(that) {
|
|
56
|
+
const fileName = path.join(that.sourceDir, that.metaDir, `main.${global.format}`)
|
|
57
|
+
if (fileUtils.fileExists(fileName)) {
|
|
58
|
+
const data = readFileSync(fileName, { encoding: 'utf8', flag: 'r' })
|
|
59
|
+
const result = (global.format == 'yaml') ? yaml.load(data) : JSON.parse(data)
|
|
60
|
+
that.#fileStats = fileUtils.fileInfo(fileName).stats
|
|
61
|
+
that.#fileName = path.join(that.targetDir, result.name + '.profile-meta.xml')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function processProfile(that) {
|
|
66
|
+
that.#startTime = process.hrtime.bigint()
|
|
67
|
+
that.#spinnerMessage = `[%1] of ${global.processed.total} - Profile: [%4]${chalk.yellowBright(that.metaDir)}[%2][%3]`
|
|
68
|
+
logUpdate(that.#spinnerMessage
|
|
69
|
+
.replace('[%1]', that.sequence.toString().padStart(global.processed.total.toString().length, ' '))
|
|
70
|
+
.replace('[%2]', '')
|
|
71
|
+
.replace('[%3]', '')
|
|
72
|
+
.replace('[%4]', '')
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
that.#types.forEach(key => {
|
|
76
|
+
let myLocation
|
|
77
|
+
if (profileDefinition.main.includes(key)) {
|
|
78
|
+
myLocation = 'main'
|
|
79
|
+
} else if (profileDefinition.directories.includes(key)) {
|
|
80
|
+
myLocation = 'directory'
|
|
81
|
+
} else if (profileDefinition.singleFiles.includes(key)) {
|
|
82
|
+
myLocation = 'file'
|
|
83
|
+
}
|
|
84
|
+
logUpdate(that.#spinnerMessage
|
|
85
|
+
.replace('[%1]', that.sequence.toString().padStart(global.processed.total.toString().length, ' '))
|
|
86
|
+
.replace('[%2]', `\n${chalk.magentaBright(nextFrame(that))} ${key}`)
|
|
87
|
+
.replace('[%3]', `${that.#errorMessage}`)
|
|
88
|
+
.replace('[%4]', `${global.statusLevel.working} `)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
switch (myLocation) {
|
|
92
|
+
case 'main':
|
|
93
|
+
processMain(that, key)
|
|
94
|
+
break
|
|
95
|
+
case 'file':
|
|
96
|
+
processFile(that, key)
|
|
97
|
+
break
|
|
98
|
+
case 'directory':
|
|
99
|
+
processDirectory(that, key)
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function processMain(that, key) {
|
|
106
|
+
const fileName = path.join(that.sourceDir, that.metaDir, `main.${global.format}`)
|
|
107
|
+
if (fileUtils.fileExists(fileName)) {
|
|
108
|
+
const data = readFileSync(fileName, { encoding: 'utf8', flag: 'r' })
|
|
109
|
+
const result = (global.format == 'yaml') ? yaml.load(data) : JSON.parse(data)
|
|
110
|
+
if (result[key] !== undefined) that.#xml += `\t<${key}>${result[key]}</${key}>${os.EOL}`
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function processFile(that, key) {
|
|
115
|
+
const fileName = path.join(that.sourceDir, that.metaDir, `${key}.${global.format}`)
|
|
116
|
+
if (fileUtils.fileExists(fileName)) {
|
|
117
|
+
switch (key) {
|
|
118
|
+
case 'categoryGroupVisibilities':
|
|
119
|
+
categoryGroupVisibilities(that, key)
|
|
120
|
+
break
|
|
121
|
+
// TODO case 'loginHours':
|
|
122
|
+
default:
|
|
123
|
+
if (profileDefinition.singleFiles.includes(key)) {
|
|
124
|
+
genericXML(that, key)
|
|
125
|
+
break
|
|
126
|
+
} else {
|
|
127
|
+
that.#errorMessage += `\n${global.statusLevel.warn} Not processed: ${key}`
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function processDirectory(that, key) {
|
|
134
|
+
switch (key) {
|
|
135
|
+
case 'objectPermissions':
|
|
136
|
+
objectPermissions(that, key)
|
|
137
|
+
break
|
|
138
|
+
default:
|
|
139
|
+
if (profileDefinition.directories.includes(key)) {
|
|
140
|
+
genericDirectoryXML(that, key)
|
|
141
|
+
break
|
|
142
|
+
} else {
|
|
143
|
+
that.#errorMessage += `\n${global.statusLevel.warn} Not processed: ${key}`
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getTimeDiff(startTime, endTime = process.hrtime.bigint()) {
|
|
150
|
+
const diff = BigInt(endTime) - BigInt(startTime)
|
|
151
|
+
let executionTime = convertHrtime(diff)
|
|
152
|
+
executionTime.seconds = Math.round(executionTime.seconds)
|
|
153
|
+
executionTime.milliseconds = Math.round(executionTime.milliseconds / 1000)
|
|
154
|
+
if (executionTime.milliseconds == 0 && executionTime.nanoseconds > 0) executionTime.milliseconds = 1
|
|
155
|
+
return executionTime
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function saveXML(that) {
|
|
159
|
+
fileUtils.createDirectory(that.targetDir)
|
|
160
|
+
that.#xml += '</Profile>\n'
|
|
161
|
+
writeFileSync(that.#fileName, that.#xml)
|
|
162
|
+
utimesSync(that.#fileName, that.#fileStats.atime, that.#fileStats.mtime)
|
|
163
|
+
|
|
164
|
+
let executionTime = getTimeDiff(BigInt(that.#startTime))
|
|
165
|
+
let durationMessage = `${executionTime.seconds}.${executionTime.milliseconds}s`
|
|
166
|
+
let stateIcon = (that.#errorMessage == '') ? global.statusLevel.success : global.statusLevel.fail
|
|
167
|
+
logUpdate(that.#spinnerMessage
|
|
168
|
+
.replace('[%1]', that.sequence.toString().padStart(global.processed.total.toString().length, ' '))
|
|
169
|
+
.replace('[%2]', `. Processed in ${durationMessage}.`)
|
|
170
|
+
.replace('[%3]', `${that.#errorMessage}`)
|
|
171
|
+
.replace('[%4]', `${stateIcon} `)
|
|
172
|
+
)
|
|
173
|
+
logUpdate.done()
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function nextFrame(that) {
|
|
178
|
+
return spinner.frames[that.#index = ++that.#index % spinner.frames.length]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function genericXML(that, key) {
|
|
182
|
+
const fileName = path.join(that.sourceDir, that.metaDir, `${key}.${global.format}`)
|
|
183
|
+
const builder = new xml2js.Builder({ cdata: false, headless: true, rootName: that.#root })
|
|
184
|
+
|
|
185
|
+
if (fileUtils.fileExists(fileName)) {
|
|
186
|
+
const data = readFileSync(fileName, { encoding: 'utf8', flag: 'r' })
|
|
187
|
+
const result = (global.format == 'yaml') ? yaml.load(data) : JSON.parse(data)
|
|
188
|
+
result[key] = sortJSONKeys(sortJSON(result[key], profileDefinition.sortKeys[key]))
|
|
189
|
+
that.#xml += builder.buildObject(result)
|
|
190
|
+
.replace(`<${that.#root}>${os.EOL}`, '')
|
|
191
|
+
.replace(`</${that.#root}>`, '')
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function genericDirectoryXML(that, key) {
|
|
196
|
+
let dirPath = path.join(that.sourceDir, that.metaDir, key)
|
|
197
|
+
if (!fileUtils.directoryExists(dirPath)) return
|
|
198
|
+
|
|
199
|
+
let fileList = fileUtils.getFiles(dirPath, `.${global.format}`).sort()
|
|
200
|
+
fileList.forEach(fileName => {
|
|
201
|
+
const builder = new xml2js.Builder({ cdata: false, headless: true, rootName: that.#root })
|
|
202
|
+
const data = readFileSync(path.join(dirPath, fileName), { encoding: 'utf8', flag: 'r' })
|
|
203
|
+
const result = (global.format == 'yaml') ? yaml.load(data) : JSON.parse(data)
|
|
204
|
+
const object = result.object
|
|
205
|
+
result[key] = sortJSONKeys(sortJSON(result[key], profileDefinition.sortKeys[key]))
|
|
206
|
+
result[key].forEach(element => {
|
|
207
|
+
Object.keys(element).forEach(tag => {
|
|
208
|
+
if (tag == profileDefinition.sortKeys[key] && object) {
|
|
209
|
+
[element][tag] = `${object}.${element[tag]}`
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
that.#xml += builder.buildObject(result)
|
|
213
|
+
.replace(`<${that.#root}>${os.EOL}`, '')
|
|
214
|
+
.replace(`</${that.#root}>`, '')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function categoryGroupVisibilities(that, key) {
|
|
221
|
+
const fileName = path.join(that.sourceDir, that.metaDir, `${key}.${global.format}`)
|
|
222
|
+
if (fileUtils.fileExists(fileName)) {
|
|
223
|
+
const data = readFileSync(fileName, { encoding: 'utf8', flag: 'r' })
|
|
224
|
+
const result = (global.format == 'yaml') ? yaml.load(data) : JSON.parse(data)
|
|
225
|
+
result[key] = sortJSONKeys(sortJSON(result[key], profileDefinition.sortKeys[key]))
|
|
226
|
+
result[key].forEach(element => {
|
|
227
|
+
that.#xml += `\t<${key}>${os.EOL}`
|
|
228
|
+
Object.keys(element).forEach(tag => {
|
|
229
|
+
// dataCategories is an array of values which must be handled separately
|
|
230
|
+
if (tag == 'dataCategories') {
|
|
231
|
+
sortJSON(element.dataCategories).forEach(category => {
|
|
232
|
+
that.#xml += `\t\t<${tag}>${category}</${tag}>${os.EOL}`
|
|
233
|
+
})
|
|
234
|
+
} else {
|
|
235
|
+
that.#xml += `\t\t<${tag}>${element[tag]}</${tag}>${os.EOL}`
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
that.#xml += `\t</${key}>${os.EOL}`
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function objectPermissions(that, key) {
|
|
244
|
+
let dirPath = path.join(that.sourceDir, that.metaDir, key)
|
|
245
|
+
if (!fileUtils.directoryExists(dirPath)) return
|
|
246
|
+
|
|
247
|
+
let fileList = fileUtils.getFiles(dirPath, `.${global.format}`).sort((a, b) => a.localeCompare(b))
|
|
248
|
+
fileList.forEach(fileName => {
|
|
249
|
+
const data = readFileSync(path.join(dirPath, fileName), { encoding: 'utf8', flag: 'r' })
|
|
250
|
+
const result = (global.format == 'yaml') ? yaml.load(data) : JSON.parse(data)
|
|
251
|
+
result[key]['object'] = result.object
|
|
252
|
+
result[key] = sortJSONKeys(result[key])
|
|
253
|
+
that.#xml += `\t<${key}>${os.EOL}`
|
|
254
|
+
Object.keys(result[key]).forEach(element => {
|
|
255
|
+
that.#xml += `\t\t<${element}>${result[key][element]}</${element}>${os.EOL}`
|
|
256
|
+
})
|
|
257
|
+
that.#xml += `\t</${key}>${os.EOL}`
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
// end of functions
|
|
261
|
+
// end of combine
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// end of class
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function sortJSON(json, key) {
|
|
268
|
+
if (Array.isArray(json)) {
|
|
269
|
+
json.sort((a, b) => {
|
|
270
|
+
if (a[key] < b[key]) return -1
|
|
271
|
+
if (a[key] > b[key]) return 1
|
|
272
|
+
return 0
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
return json
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function sortJSONKeys(json) {
|
|
279
|
+
// sort json keys alphabetically
|
|
280
|
+
if (Array.isArray(json)) {
|
|
281
|
+
json.forEach(function (part, index) {
|
|
282
|
+
this[index] = Object.keys(this[index])
|
|
283
|
+
.sort((a, b) => {
|
|
284
|
+
if (a < b) return -1
|
|
285
|
+
if (a > b) return 1
|
|
286
|
+
return 0
|
|
287
|
+
})
|
|
288
|
+
.reduce((accumulator, key) => {
|
|
289
|
+
accumulator[key] = this[index][key]
|
|
290
|
+
|
|
291
|
+
return accumulator
|
|
292
|
+
}, {})
|
|
293
|
+
}, json)
|
|
294
|
+
|
|
295
|
+
} else {
|
|
296
|
+
json = Object.keys(json)
|
|
297
|
+
.sort((a, b) => {
|
|
298
|
+
if (a < b) return -1
|
|
299
|
+
if (a > b) return 1
|
|
300
|
+
return 0
|
|
301
|
+
})
|
|
302
|
+
.reduce((accumulator, key) => {
|
|
303
|
+
accumulator[key] = json[key]
|
|
304
|
+
|
|
305
|
+
return accumulator
|
|
306
|
+
}, {})
|
|
307
|
+
}
|
|
308
|
+
return json
|
|
309
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const profileDefinition = {
|
|
2
|
+
metaUrl: 'https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_profile.htm',
|
|
3
|
+
main: [
|
|
4
|
+
'custom',
|
|
5
|
+
'description',
|
|
6
|
+
'fullName',
|
|
7
|
+
'userLicense',
|
|
8
|
+
],
|
|
9
|
+
singleFiles: [
|
|
10
|
+
'applicationVisibilities',
|
|
11
|
+
'categoryGroupVisibilities',
|
|
12
|
+
'classAccesses',
|
|
13
|
+
'customMetadataTypeAccesses',
|
|
14
|
+
'customPermissions',
|
|
15
|
+
'customSettingAccesses',
|
|
16
|
+
'externalDataSourceAccesses',
|
|
17
|
+
'flowAccesses',
|
|
18
|
+
'layoutAssignments',
|
|
19
|
+
// TODO 'loginHours',
|
|
20
|
+
'loginIpRanges',
|
|
21
|
+
'pageAccesses',
|
|
22
|
+
// TODO 'profileActionOverrides',
|
|
23
|
+
'tabVisibilities',
|
|
24
|
+
'userPermissions',
|
|
25
|
+
],
|
|
26
|
+
directories: [
|
|
27
|
+
'fieldPermissions',
|
|
28
|
+
'loginFlows',
|
|
29
|
+
'objectPermissions',
|
|
30
|
+
'recordTypeVisibilities',
|
|
31
|
+
],
|
|
32
|
+
ignore: [
|
|
33
|
+
'$',
|
|
34
|
+
],
|
|
35
|
+
sortKeys: {
|
|
36
|
+
'applicationVisibilities': 'application',
|
|
37
|
+
'categoryGroupVisibilities': 'dataCategoryGroup',
|
|
38
|
+
'classAccesses': 'apexClass',
|
|
39
|
+
'customMetadataTypeAccesses': 'name',
|
|
40
|
+
'customPermissions': 'name',
|
|
41
|
+
'customSettingAccesses': 'name',
|
|
42
|
+
'externalDataSourceAccesses': 'externalDataSource',
|
|
43
|
+
'fieldPermissions': 'field',
|
|
44
|
+
'flowAccesses': 'flow',
|
|
45
|
+
'layoutAssignments': 'layout',
|
|
46
|
+
'loginFlows': 'friendlyName',
|
|
47
|
+
'loginIpRanges': 'startAddress',
|
|
48
|
+
'objectPermissions': 'object',
|
|
49
|
+
'pageAccesses': 'apexPage',
|
|
50
|
+
'profileActionOverrides': 'pageOrSobjectType',
|
|
51
|
+
'recordTypeVisibilities': 'recordType',
|
|
52
|
+
'tabVisibilities': 'tab',
|
|
53
|
+
'userPermissions': 'name',
|
|
54
|
+
}
|
|
55
|
+
}
|