Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a new command to filter the content of Profile and Pset based on multiple package.xml #90

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ If you run your CI/CD jobs inside a Docker image, you can add the plugin to your

<!-- commands -->
* [`sfdx sgd:source:delta -f <string> [-t <string>] [-r <filepath>] [-i <filepath>] [-D <filepath>] [-s <filepath>] [-W] [-o <filepath>] [-a <number>] [-d] [-n <filepath>] [-N <filepath>] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-sgdsourcedelta--f-string--t-string--r-filepath--i-filepath--d-filepath--s-filepath--w--o-filepath--a-number--d--n-filepath--n-filepath---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal)
* [`sfdx sgd:source:ppset -p <array> [-s <array>] [-t <array>] [-r <array>] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-sgdsourceppset--p-array--s-array--t-array--r-array---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal)

## `sfdx sgd:source:delta -f <string> [-t <string>] [-r <filepath>] [-i <filepath>] [-D <filepath>] [-s <filepath>] [-W] [-o <filepath>] [-a <number>] [-d] [-n <filepath>] [-N <filepath>] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`

Expand Down Expand Up @@ -184,6 +185,39 @@ OPTIONS
```

_See code: [src/commands/sgd/source/delta.ts](https://github.com/scolladon/sfdx-git-delta/blob/v5.3.0/src/commands/sgd/source/delta.ts)_

## `sfdx sgd:source:ppset -p <array> [-s <array>] [-t <array>] [-r <array>] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`

prepare profile and permission

```
USAGE
$ sfdx sgd:source:ppset -p <array> [-s <array>] [-t <array>] [-r <array>] [--json] [--loglevel
trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]

OPTIONS
-p, --packages=packages
(required) package.xml paths to use to filter profile and permission set. Delimiter: ':'

-r, --user-permissions=user-permissions
list of the userPermission to keep. Delimiter: ':'

-s, --sources=sources
sources paths where to apply the filtering (use default if empty). Delimiter: ':'

-t, --permissions-type=permissions-type
list of the permission types to filter with the package <applicationVisibilities|categoryGroupVisibilities|classAcce
sses|customMetadataTypeAccesses|customPermissions|customSettingAccesses|externalDataSourceAccesses|fieldPermissions|
layoutAssignments|objectPermissions|pageAccesses|recordTypeVisibilities|tabVisibilities|tabSettings>. Delimiter: ':'

--json
format output as json

--loglevel=(trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL)
[default: warn] logging level for this command invocation
```

_See code: [src/commands/sgd/source/ppset.ts](https://github.com/scolladon/sfdx-git-delta/blob/v5.3.0/src/commands/sgd/source/ppset.ts)_
<!-- commandsstop -->

### Windows users
Expand Down
11 changes: 11 additions & 0 deletions messages/ppset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
command: 'prepare profile and permission',
packagesFlagDescription:
"package.xml paths to use to filter profile and permission set. Delimiter: '%s'",
sourcesFlagDescription:
"sources paths where to apply the filtering (use default if empty). Delimiter: '%s'",
userPermissionsFlagDescription:
"list of the userPermission to keep. Delimiter: '%s'",
permissionsTypeFlagDescription:
"list of the permission types to filter with the package <%s>. Delimiter: '%s'",
}
36 changes: 18 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
"@oclif/command": "^1.8.16",
"@oclif/config": "^1.18.3",
"@oclif/errors": "^1.3.5",
"@salesforce/command": "^5.2.0",
"@salesforce/command": "^5.2.1",
"@salesforce/core": "^2.37.1",
"fast-xml-parser": "^4.0.8",
"fast-xml-parser": "^4.0.9",
"fs-extra": "^10.1.0",
"ignore": "^5.2.0",
"micromatch": "^4.0.5",
Expand Down Expand Up @@ -66,34 +66,34 @@
"upgrade:dependencies": "yarn yarn-upgrade-all"
},
"devDependencies": {
"@commitlint/cli": "^17.0.2",
"@commitlint/config-angular": "^17.0.0",
"@commitlint/prompt-cli": "^17.0.0",
"@commitlint/cli": "^17.0.3",
"@commitlint/config-angular": "^17.0.3",
"@commitlint/prompt-cli": "^17.0.3",
"@oclif/dev-cli": "^1.26.10",
"@oclif/plugin-help": "^5.1.12",
"@oclif/test": "^2.1.0",
"@salesforce/dev-config": "^3.0.1",
"@salesforce/dev-config": "^3.1.0",
"@types/chai": "^4.3.1",
"@types/mocha": "^9.1.1",
"@types/node": "^17.0.42",
"@typescript-eslint/eslint-plugin": "^5.27.1",
"@typescript-eslint/parser": "^5.27.1",
"@types/node": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"conventional-github-releaser": "^3.1.5",
"eslint": "^8.17.0",
"eslint": "^8.20.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-salesforce-typescript": "^0.2.8",
"eslint-config-salesforce-typescript": "^1.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^39.3.2",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-jsdoc": "^39.3.3",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.1",
"jest": "^28.1.1",
"lint-staged": "^13.0.1",
"jest": "^28.1.3",
"lint-staged": "^13.0.3",
"nyc": "^15.1.0",
"prettier": "^2.6.2",
"prettier": "^2.7.1",
"prettier-eslint": "^15.0.1",
"standard-version": "^9.5.0",
"ts-node": "^10.8.1",
"typescript": "^4.7.3",
"ts-node": "^10.9.1",
"typescript": "^4.7.4",
"yarn-upgrade-all": "^0.7.1"
},
"oclif": {
Expand Down
188 changes: 188 additions & 0 deletions src/commands/sgd/source/ppset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { flags, SfdxCommand } from '@salesforce/command'
import { Messages, SfdxProject } from '@salesforce/core'
import { AnyJson, JsonArray } from '@salesforce/ts-types'
import { findInDir } from '../../../utils/findInDir'
import * as gc from '../../../utils/gitConstants'
import {
XML_HEADER,
XML_PARSER_OPTION,
JSON_PARSER_OPTION,
} from '../../../utils/parsingConstants'

import * as fs from 'fs'
import { XMLBuilder, XMLParser } from 'fast-xml-parser'
import * as path from 'path'

// Initialize Messages with the current plugin directory
Messages.importMessagesDirectory(__dirname)
const COMMAND_NAME = 'ppset'

// Load the specific messages for this file. Messages from @salesforce/command, @salesforce/core,
// or any library that is using the messages framework can also be loaded this way.
const messages = Messages.loadMessages('sfdx-git-delta', COMMAND_NAME)
const INPUT_DELIMITER = ':'

const profilePackageMapping = {
applicationVisibilities: { xmlTag: 'CustomApplication', key: 'application' },
categoryGroupVisibilities: {
xmlTag: 'DataCategoryGroup',
key: 'dataCategoryGroup',
},
classAccesses: { xmlTag: 'ApexClass', key: 'apexClass' },
customMetadataTypeAccesses: { xmlTag: 'CustomMetadata', key: 'name' },
customPermissions: { xmlTag: 'CustomPermission', key: 'name' },
customSettingAccesses: { xmlTag: 'CustomObject', key: 'name' },
externalDataSourceAccesses: {
xmlTag: 'ExternalDataSource',
key: 'externalDataSource',
},
fieldPermissions: { xmlTag: 'CustomField', key: 'field' },
layoutAssignments: { xmlTag: 'Layout', key: 'layout' }, // recordtype
objectPermissions: { xmlTag: 'CustomObject', key: 'object' },
pageAccesses: { xmlTag: 'ApexPage', key: 'apexPage' },
recordTypeVisibilities: { xmlTag: 'RecordType', key: 'recordType' },
tabVisibilities: { xmlTag: 'CustomTab', key: 'tab' },
tabSettings: { xmlTag: 'CustomTab', key: 'tab' },
}

const FILE_READ_OPTIONS = {
encoding: gc.UTF8_ENCODING,
}

export default class Ppset extends SfdxCommand {
public static description = messages.getMessage('command', [])

protected static flagsConfig = {
packages: flags.array({
char: 'p',
description: messages.getMessage('packagesFlagDescription', [
INPUT_DELIMITER,
]),
delimiter: INPUT_DELIMITER,
map: (val: string) => path.parse(val),
required: true,
}),
sources: flags.array({
char: 's',
description: messages.getMessage('sourcesFlagDescription', [
INPUT_DELIMITER,
]),
delimiter: INPUT_DELIMITER,
map: (val: string) => path.parse(val),
}),
'permissions-type': flags.array({
char: 't',
description: messages.getMessage('permissionsTypeFlagDescription', [
Object.keys(profilePackageMapping).join('|'),
INPUT_DELIMITER,
]),
delimiter: INPUT_DELIMITER,
}),
'user-permissions': flags.array({
char: 'r',
description: messages.getMessage('userPermissionsFlagDescription', [
INPUT_DELIMITER,
]),
delimiter: INPUT_DELIMITER,
}),
}

protected static requiresProject = true

public async run(): Promise<AnyJson> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function run has 88 lines of code (exceeds 25 allowed). Consider refactoring.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function run has 87 lines of code (exceeds 25 allowed). Consider refactoring.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function run has 86 lines of code (exceeds 25 allowed). Consider refactoring.

const project = await SfdxProject.resolve()
const projectJson = await project.resolveProjectConfig()
const basePath = project.getPath()
const packageDirectories = projectJson['packageDirectories'] as JsonArray
const defaultDir = packageDirectories.reduce(
(a, v) => (v['default'] === true ? (a = v['path']) : (a = a)),
scolladon marked this conversation as resolved.
Show resolved Hide resolved
scolladon marked this conversation as resolved.
Show resolved Hide resolved
''
)

const sources = this.flags.sources || []
const userPermissions = this.flags['user-permissions'] || []
const dirList = packageDirectories.filter(dir => sources.includes(dir))
if (dirList.length === 0) {
dirList.push(defaultDir)
}

const xmlParser = new XMLParser(XML_PARSER_OPTION)
const xmlBuilder = new XMLBuilder(JSON_PARSER_OPTION)

const packages = this.flags.packages.map(packageFile =>
xmlParser.parse(
fs.readFileSync(path.format(packageFile), FILE_READ_OPTIONS)
)
)
const allowedPermissions = this.flags['permissions-type'] || []

dirList.forEach(dir =>
findInDir(
path.join(basePath, `${dir}`),
/(?!permissionsetgroup)(\.profile)|(\.permissionset)/
)
.filter(file => {
const fileName = path.parse(file).name.split('.')[0]
return (
packages.some(jsonObject =>
jsonObject?.Package[0]?.types
.filter(x => x.name === 'Profile')[0]
.members.includes(fileName)
) ||
packages.some(jsonObject =>
jsonObject?.Package[0]?.types
.filter(x => x.name === 'PermissionSet')[0]
.members.includes(fileName)
)
)
})
.forEach(file => {
// Filter content based on the package.xml and the ppset
const content = xmlParser.parse(
fs.readFileSync(file, FILE_READ_OPTIONS)
)
const permissionContent = Object.values(content)[0][0]
let authorizedKeys = Object.keys(permissionContent).filter(x =>
Object.keys(profilePackageMapping).includes(x)
)
if (allowedPermissions.length > 0) {
authorizedKeys = authorizedKeys.filter(x =>
allowedPermissions.includes(x)
)
}

authorizedKeys.forEach(permission => {
const values = new Set()
packages.forEach(jsonObject =>
jsonObject?.Package[0]?.types
.filter(
e => e.name === profilePackageMapping[permission].xmlTag
)
.forEach(element =>
Array.isArray(element.members)
? element.members.forEach(member => values.add(member))
: values.add(element.members)
)
)
permissionContent[permission] = permissionContent[
permission
].filter(element =>
values.has(element[profilePackageMapping[permission].key])
)
})

const inFileUserPermissions =
permissionContent['userPermissions'] ?? []
permissionContent['userPermissions'] =
userPermissions.length > 0
? inFileUserPermissions.filter(up =>
userPermissions.includes(up.name)
)
: []
const xmlContent = XML_HEADER + xmlBuilder.build(content)
fs.writeFileSync(file, xmlContent)
})
)
return null
}
}
21 changes: 21 additions & 0 deletions src/utils/findInDir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as fs from 'fs';
scolladon marked this conversation as resolved.
Show resolved Hide resolved
import * as path from 'path';
scolladon marked this conversation as resolved.
Show resolved Hide resolved

const findInDir = (dir: string, filter: RegExp, fileList: string[] = []): string[] => {
scolladon marked this conversation as resolved.
Show resolved Hide resolved
const files = fs.readdirSync(dir);
scolladon marked this conversation as resolved.
Show resolved Hide resolved

files.forEach(file => {
scolladon marked this conversation as resolved.
Show resolved Hide resolved
const filePath = path.join(dir, file);
scolladon marked this conversation as resolved.
Show resolved Hide resolved
const fileStat = fs.lstatSync(filePath);
scolladon marked this conversation as resolved.
Show resolved Hide resolved

if (fileStat.isDirectory()) {
scolladon marked this conversation as resolved.
Show resolved Hide resolved
findInDir(filePath, filter, fileList);
scolladon marked this conversation as resolved.
Show resolved Hide resolved
} else if (filter.test(filePath)) {
scolladon marked this conversation as resolved.
Show resolved Hide resolved
fileList.push(filePath);
scolladon marked this conversation as resolved.
Show resolved Hide resolved
}
scolladon marked this conversation as resolved.
Show resolved Hide resolved
});
scolladon marked this conversation as resolved.
Show resolved Hide resolved

return fileList;
};

export { findInDir };
scolladon marked this conversation as resolved.
Show resolved Hide resolved
15 changes: 15 additions & 0 deletions src/utils/parsingConstants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict'
const XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>\n'
const XML_PARSER_OPTION = {
ignoreAttributes: false,
ignoreNameSpace: false,
arrayMode: true,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can fix some potential issue in very edge case

}
const JSON_PARSER_OPTION = {
...XML_PARSER_OPTION,
format: true,
indentBy: ' ',
}
module.exports.XML_HEADER = XML_HEADER
module.exports.XML_PARSER_OPTION = XML_PARSER_OPTION
module.exports.JSON_PARSER_OPTION = JSON_PARSER_OPTION
Loading