@ds-sfdc/sfparty 0.0.0 → 1.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.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ npx commitlint --edit $1
@@ -0,0 +1 @@
1
+ module.exports = { extends: ['@commitlint/config-conventional'] };
package/package.json CHANGED
@@ -1,21 +1,22 @@
1
1
  {
2
2
  "name": "@ds-sfdc/sfparty",
3
- "version": "0.0.0",
4
- "description": "Split Salesforce metadata XML file into YAML (or JSON) parts, and combine back to XML",
3
+ "version": "1.0.0",
4
+ "description": "Salesforce metadata XML splitter for CI/CD",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/TimPaulaskasDS/sfparty.git"
9
9
  },
10
10
  "bin": {
11
- "sfparty": "index.js"
11
+ "sfparty": "src/index.js"
12
12
  },
13
- "main": "index.js",
13
+ "main": "src/index.js",
14
14
  "scripts": {
15
- "test": "mocha 'tests/**/*.js' --recursive--reporter spec"
15
+ "test": "mocha 'tests/**/*.js' --recursive--reporter spec",
16
+ "postinstall": "husky install"
16
17
  },
17
18
  "keywords": [
18
- "xml,split,combine,yaml,json"
19
+ "salesforce,metadata,xml,split,yaml,json"
19
20
  ],
20
21
  "author": "Tim Paulaskas",
21
22
  "license": "BSD-3-Clause",
@@ -32,6 +33,9 @@
32
33
  "yargs": "^17.6.2"
33
34
  },
34
35
  "devDependencies": {
36
+ "@commitlint/cli": "^17.4.0",
37
+ "@commitlint/config-conventional": "^17.4.0",
38
+ "husky": "^8.0.3",
35
39
  "mocha": "^10.2.0",
36
40
  "nodemon": "^2.0.20",
37
41
  "should": "^7.1.0"
@@ -44,7 +48,6 @@
44
48
  },
45
49
  "homepage": "https://github.com/TimPaulaskasDS/sfparty#readme",
46
50
  "directories": {
47
- "example": "examples",
48
51
  "lib": "lib",
49
52
  "test": "tests"
50
53
  }
@@ -8,15 +8,13 @@ import winston from 'winston'
8
8
  import chalk from 'chalk'
9
9
  import convertHrtime from 'convert-hrtime'
10
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'
11
+ import * as pkgObj from '../package.json' assert { type: "json" }
12
+ import * as metadataSplit from './party/split.js'
13
+ import * as metadataCombine from './party/combine.js'
14
+ import * as labelDefinition from './meta/CustomLabels.js'
15
+ import * as profileDefinition from './meta/Profiles.js'
16
+ import * as permsetDefinition from './meta/PermissionSets.js'
17
+ import * as workflowDefinition from './meta/Workflows.js'
20
18
 
21
19
  const startTime = process.hrtime.bigint()
22
20
 
@@ -24,7 +22,7 @@ const startTime = process.hrtime.bigint()
24
22
  global.logger = winston.createLogger({
25
23
  levels: winston.config.syslog.levels,
26
24
  format: winston.format.cli(),
27
- defaultMeta: { service: 'dstools', method: 'profile' },
25
+ defaultMeta: { service: 'sfparty' },
28
26
  transports: [
29
27
  new winston.transports.Console(),
30
28
  ],
@@ -36,11 +34,38 @@ global.processed = {
36
34
  current: 1,
37
35
  }
38
36
 
39
- global.statusLevel = {
37
+ global.icons = {
40
38
  "warn": '🔕',
41
39
  "success": chalk.greenBright('✔'),
42
40
  "fail": '❗',
43
- "working": '⏳'
41
+ "working": '⏳',
42
+ "party": '🎉',
43
+ }
44
+
45
+ const typeArray = [
46
+ 'label',
47
+ 'profile',
48
+ 'permset',
49
+ 'workflow',
50
+ ]
51
+
52
+ const metaTypes = {
53
+ label: {
54
+ type: labelDefinition.metadataDefinition.filetype,
55
+ definition: labelDefinition.metadataDefinition,
56
+ },
57
+ profile: {
58
+ type: profileDefinition.metadataDefinition.filetype,
59
+ definition: profileDefinition.metadataDefinition,
60
+ },
61
+ permset: {
62
+ type: permsetDefinition.metadataDefinition.filetype,
63
+ definition: permsetDefinition.metadataDefinition,
64
+ },
65
+ workflow: {
66
+ type: workflowDefinition.metadataDefinition.filetype,
67
+ definition: workflowDefinition.metadataDefinition,
68
+ },
44
69
  }
45
70
 
46
71
  function getRootPath(packageDir) {
@@ -70,9 +95,30 @@ function getRootPath(packageDir) {
70
95
 
71
96
  return defaultDir
72
97
  }
73
-
74
98
  let errorMessage = chalk.red('Please specify the action of ' + chalk.whiteBright.bgRedBright('split') + ' or ' + chalk.whiteBright.bgRedBright('combine') + '.')
75
99
 
100
+ displayHeader() // display header mast
101
+
102
+ function displayHeader() {
103
+ const table = {
104
+ topLeft: '╭',
105
+ topRight: '╮',
106
+ bottomLeft: '╰',
107
+ bottomRight: '╯',
108
+ horizontal: '─',
109
+ vertical: '│',
110
+ }
111
+ let versionString = `sfparty v${pkgObj.default.version}${(process.stdout.columns > pkgObj.default.description.length + 15) ? ' - ' + pkgObj.default.description : ''}`
112
+ let titleMessage = `${global.icons.party} ${chalk.yellowBright(versionString)} ${global.icons.party}`
113
+ titleMessage = titleMessage.padEnd((process.stdout.columns / 2) + versionString.length / 1.65)
114
+ titleMessage = titleMessage.padStart(process.stdout.columns)
115
+ titleMessage = chalk.blackBright(table.vertical) + ' ' + titleMessage + ' ' + chalk.blackBright(table.vertical)
116
+ console.log(`${chalk.blackBright(table.topLeft + table.horizontal.repeat(process.stdout.columns - 2) + table.topRight)}`)
117
+ console.log(titleMessage)
118
+ console.log(`${chalk.blackBright(table.bottomLeft + table.horizontal.repeat(process.stdout.columns - 2) + table.bottomRight)}`)
119
+ console.log()
120
+ }
121
+
76
122
  yargs(hideBin(process.argv))
77
123
  .alias('h', 'help')
78
124
  .command({
@@ -131,7 +177,7 @@ yargs(hideBin(process.argv))
131
177
  type: 'string',
132
178
  }
133
179
  })
134
- .choices('type', ['label', 'permset', 'profile', 'workflow'])
180
+ .choices('type', typeArray)
135
181
  .choices('format', ['json', 'yaml'])
136
182
  .check((argv, options) => {
137
183
  const name = argv.name
@@ -157,31 +203,15 @@ yargs(hideBin(process.argv))
157
203
  })
158
204
  },
159
205
  handler: (argv) => {
160
- let metaExtension
206
+ if (!typeArray.includes(argv.type)) {
207
+ global.logger.error('Metadata type not supported: ' + type)
208
+ process.exit(1)
209
+ }
161
210
  const fileList = []
162
- let type = argv.type
211
+ const typeObj = metaTypes[argv.type]
212
+ const type = typeObj.type
213
+ const metaExtension = `.${type}-meta.xml`
163
214
  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
215
 
186
216
  let sourceDir = argv.source || ''
187
217
  let targetDir = argv.target || ''
@@ -189,14 +219,14 @@ yargs(hideBin(process.argv))
189
219
  let all = argv.all
190
220
  let packageDir = getRootPath(sourceDir)
191
221
 
192
- if (type == 'labels') {
193
- name = 'CustomLabels'
222
+ if (type == metaTypes.label.type) {
223
+ name = metaTypes.label.definition.root
194
224
  }
195
- sourceDir = path.join(global.__basedir, packageDir, 'main', 'default', type)
225
+ sourceDir = path.join(global.__basedir, packageDir, 'main', 'default', typeObj.definition.directory)
196
226
  if (targetDir == '') {
197
- targetDir = path.join(global.__basedir, packageDir + '-party', 'main', 'default', type)
227
+ targetDir = path.join(global.__basedir, packageDir + '-party', 'main', 'default', typeObj.definition.directory)
198
228
  } else {
199
- targetDir = path.join(targetDir, 'main', 'default', type)
229
+ targetDir = path.join(targetDir, 'main', 'default', typeObj.definition.directory)
200
230
  }
201
231
  let metaDirPath = sourceDir
202
232
  console.log(`${chalk.bgBlackBright('Source path:')} ${sourceDir}`)
@@ -227,70 +257,27 @@ yargs(hideBin(process.argv))
227
257
  console.log()
228
258
  const promList = []
229
259
  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
- }
260
+ const metadataItem = new metadataSplit.Split({
261
+ metadataDefinition: typeObj.definition,
262
+ sourceDir: sourceDir,
263
+ targetDir: targetDir,
264
+ metaFilePath: path.join(sourceDir, metaFile),
265
+ sequence: promList.length + 1,
266
+ })
267
+ const metadataItemProm = metadataItem.split()
268
+ promList.push(metadataItemProm)
269
+ metadataItemProm.then((resolve, reject) => {
270
+ if (resolve == false) {
271
+ global.processed.errors++
272
+ global.processed.current--
273
+ } else {
274
+ global.processed.current++
275
+ }
276
+ })
290
277
  })
291
278
  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)
279
+ let message = `Split ${chalk.bgBlackBright((global.processed.current > promList.length) ? promList.length : global.processed.current)} file(s) ${(global.processed.errors > 0) ? 'with ' + chalk.bgBlackBright.red(global.processed.errors) + ' error(s) ' : ''}in `
280
+ displayMessageAndDuration(startTime, message)
294
281
  })
295
282
  }
296
283
  })
@@ -349,7 +336,7 @@ yargs(hideBin(process.argv))
349
336
  type: 'string',
350
337
  }
351
338
  })
352
- .choices('type', ['label', 'permset', 'profile', 'workflow'])
339
+ .choices('type', typeArray)
353
340
  .choices('format', ['json', 'yaml'])
354
341
  .check((argv, options) => {
355
342
  const name = argv.name
@@ -375,26 +362,14 @@ yargs(hideBin(process.argv))
375
362
  })
376
363
  },
377
364
  handler: (argv) => {
365
+ if (!typeArray.includes(argv.type)) {
366
+ global.logger.error('Metadata type not supported: ' + type)
367
+ process.exit(1)
368
+ }
378
369
  let processList = []
379
370
  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
- }
371
+ const typeObj = metaTypes[argv.type]
372
+ const type = typeObj.type
398
373
 
399
374
  let sourceDir = argv.source || ''
400
375
  let targetDir = argv.target || ''
@@ -402,19 +377,19 @@ yargs(hideBin(process.argv))
402
377
  let all = argv.all
403
378
  let packageDir = getRootPath(sourceDir)
404
379
 
405
- sourceDir = path.join(global.__basedir, packageDir + '-party', 'main', 'default', type)
380
+ sourceDir = path.join(global.__basedir, packageDir + '-party', 'main', 'default', typeObj.definition.directory)
406
381
  if (targetDir == '') {
407
- targetDir = path.join(global.__basedir, packageDir, 'main', 'default', type)
382
+ targetDir = path.join(global.__basedir, packageDir, 'main', 'default', typeObj.definition.directory)
408
383
  } else {
409
- targetDir = path.join(targetDir, 'main', 'default', type)
384
+ targetDir = path.join(targetDir, 'main', 'default', typeObj.definition.directory)
410
385
  }
411
386
 
412
387
  console.log(`${chalk.bgBlackBright('Source path:')} ${sourceDir}`)
413
388
  console.log(`${chalk.bgBlackBright('Target path:')} ${targetDir}`)
414
389
  console.log()
415
390
 
416
- if (type == 'labels') {
417
- processList = fileUtils.getFiles(sourceDir, `.${global.format}`)
391
+ if (type == metaTypes.label.type) {
392
+ processList.push(metaTypes.label.definition.root)
418
393
  } else if (!all) {
419
394
  let metaDirPath = path.join(sourceDir, name)
420
395
  if (!fileUtils.directoryExists(metaDirPath)) {
@@ -431,69 +406,33 @@ yargs(hideBin(process.argv))
431
406
  console.log()
432
407
 
433
408
  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({
409
+ processList.forEach(metaDir => {
410
+ const metadataItem = new metadataCombine.Combine({
411
+ metadataDefinition: typeObj.definition,
477
412
  sourceDir: sourceDir,
478
413
  targetDir: targetDir,
479
- metaDir: sourceDir, // use sourceDir
480
- processList: processList,
414
+ metaDir: metaDir,
415
+ sequence: promList.length + 1,
481
416
  })
482
- const labelProm = label.combine()
483
- promList.push(labelProm)
484
- labelProm.then(() => {
417
+ const metadataItemProm = metadataItem.combine()
418
+ promList.push(metadataItemProm)
419
+ metadataItemProm.then((resolve, reject) => {
485
420
  global.processed.current++
486
421
  })
422
+ })
487
423
 
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
- }
424
+ Promise.allSettled(promList).then((results) => {
425
+ let successes = 0
426
+ let errors = 0
427
+ results.forEach(result => {
428
+ if (result.value == true) {
429
+ successes++
430
+ } else if (result.value == false) {
431
+ errors++
432
+ }
433
+ })
434
+ let message = `Combined ${chalk.bgBlackBright(successes)} file(s) ${(errors > 0) ? 'with ' + chalk.bgBlackBright(errors) + 'error(s) ' : ''}in `
435
+ displayMessageAndDuration(startTime, message)
497
436
  })
498
437
  }
499
438
  })
@@ -508,7 +447,7 @@ yargs(hideBin(process.argv))
508
447
  .argv
509
448
  .parse
510
449
 
511
- function displayMessage(startTime, message) {
450
+ function displayMessageAndDuration(startTime, message) {
512
451
  const diff = process.hrtime.bigint() - BigInt(startTime)
513
452
  let durationMessage
514
453
  let executionTime = convertHrtime(diff);
@@ -37,23 +37,31 @@ export function deleteDirectory(dirPath, recursive = false) {
37
37
 
38
38
  export function getFiles(dirPath, filter = undefined) {
39
39
  const filesList = []
40
- fs.readdirSync(dirPath).forEach(file => {
41
- if (!filter) {
42
- filesList.push(file)
43
- } else {
44
- if (file.endsWith(filter)) {
40
+ if (directoryExists(dirPath)) {
41
+ fs.readdirSync(dirPath).forEach(file => {
42
+ if (!filter) {
45
43
  filesList.push(file)
44
+ } else {
45
+ if (file.endsWith(filter)) {
46
+ filesList.push(file)
47
+ }
46
48
  }
47
- }
48
- })
49
- filesList.sort()
50
- return filesList
49
+ })
50
+ filesList.sort()
51
+ return filesList
52
+ } else {
53
+ return []
54
+ }
51
55
  }
52
56
 
53
57
  export function getDirectories(dirPath) {
54
- return fs.readdirSync(dirPath, { withFileTypes: true })
58
+ if (directoryExists(dirPath)) {
59
+ return fs.readdirSync(dirPath, { withFileTypes: true })
55
60
  .filter(dirent => dirent.isDirectory())
56
61
  .map(dirent => dirent.name)
62
+ } else {
63
+ return []
64
+ }
57
65
  }
58
66
 
59
67
  export function deleteFile(filePath) {
@@ -1,5 +1,13 @@
1
- export const labelDefinition = {
1
+ export const metadataDefinition = {
2
2
  metaUrl: 'https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_customlabels.htm',
3
+ directory: 'labels',
4
+ filetype: 'labels',
5
+ root: 'CustomLabels',
6
+ main: [
7
+ '$',
8
+ ],
9
+ singleFiles: [
10
+ ],
3
11
  directories: [
4
12
  'labels',
5
13
  ],
@@ -9,4 +17,7 @@ export const labelDefinition = {
9
17
  keyOrder: {
10
18
  labels: ['fullName', 'shortDescription', 'categories', 'protected', 'language', 'value'],
11
19
  },
20
+ xmlOrder: {
21
+ labels: ['fullName'],
22
+ }
12
23
  }
@@ -1,43 +1,41 @@
1
- export const permsetDefinition = {
1
+ export const metadataDefinition = {
2
2
  metaUrl: 'https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_permissionset.htm',
3
+ directory: 'permissionsets',
4
+ filetype: 'permissionset',
5
+ root: 'PermissionSet',
3
6
  main: [
4
7
  'label',
5
8
  'description',
6
9
  'custom',
7
10
  'hasActivationRequired',
8
11
  'license',
9
- 'userLicense', // Replaced by license
12
+ 'userLicense', // Replaced by license
13
+ '$',
10
14
  ],
11
15
  singleFiles: [
12
16
  'applicationVisibilities',
13
- // 'categoryGroupVisibilities', // PROFILE ONLY
14
17
  'classAccesses',
15
18
  'customMetadataTypeAccesses',
16
19
  'customPermissions',
17
20
  'customSettingAccesses',
18
21
  'externalDataSourceAccesses',
19
22
  'flowAccesses',
20
- // 'layoutAssignments', // PROFILE ONLY
21
- // 'loginHours', // PROFILE ONLY
22
- // 'loginIpRanges', // PROFILE ONLY
23
23
  'pageAccesses',
24
- // 'profileActionOverrides', // PROFILE ONLY
25
24
  'tabSettings',
26
- // 'tabVisibilities', // PROFILE ONLY
27
25
  'userPermissions',
28
26
  ],
29
27
  directories: [
30
28
  'fieldPermissions',
31
- // 'loginFlows', // PROFILE ONLY
32
29
  'objectPermissions',
33
30
  'recordTypeVisibilities',
34
31
  ],
35
- ignore: [
36
- '$',
32
+ splitObjects: [
33
+ 'fieldPermissions',
34
+ 'objectPermissions',
35
+ 'recordTypeVisibilities',
37
36
  ],
38
37
  sortKeys: {
39
38
  'applicationVisibilities': 'application',
40
- // 'categoryGroupVisibilities': 'dataCategoryGroup', // PROFILE ONLY
41
39
  'classAccesses': 'apexClass',
42
40
  'customMetadataTypeAccesses': 'name',
43
41
  'customPermissions': 'name',
@@ -45,15 +43,10 @@ export const permsetDefinition = {
45
43
  'externalDataSourceAccesses': 'externalDataSource',
46
44
  'fieldPermissions': 'field',
47
45
  'flowAccesses': 'flow',
48
- // 'layoutAssignments': 'layout', // PROFILE ONLY
49
- // 'loginFlows': 'friendlyName', // PROFILE ONLY
50
- // 'loginIpRanges': 'startAddress', // PROFILE ONLY
51
46
  'objectPermissions': 'object',
52
47
  'pageAccesses': 'apexPage',
53
- // 'profileActionOverrides': 'pageOrSobjectType', // PROFILE ONLY
54
48
  'recordTypeVisibilities': 'recordType',
55
49
  'tabSettings': 'tab',
56
- // 'tabVisibilities': 'tab', // PROFILE ONLY
57
50
  'userPermissions': 'name',
58
51
  },
59
52
  keyOrder: {