Skip to content

Commit 3650332

Browse files
feat: Wr/decompose permission sets (#1412)
* chore: add initial dpsb2 preset / cloned from dpsb * chore: add new transformer/adapter, default for now * chore: update adapters/transformers with decomposed as base * chore: a lot of changes, writing xml in dirs, now to group by object * fix: decomposing - very messy * fix: recomposing writing file, wrong child entry name CA, not cA * fix: decomposing, recomposing * chore: remove unused adapter * refactor: first round of cleanup * test: add snapshot * refactor: combine writeInfo methods * test: filter decompPS2 preset, not valid as registry entry * chore: bump core * test: export shared functions from Decomposed, add UT * docs: update preset description * chore: code review I * chore: code review II * test: udpate test name, merge main * chore: get name from path correctly * chore: work with singular child type * test: update test name * chore: remove clean up writeInfos * fix: allow for multiple PS's, not 'full' PS * test: add snapshot variation * refactor: simplify names * chore: supporting MPD retrieve * chore: simplify MPD logic * chore: fix merge with parent
1 parent abc4d38 commit 3650332

File tree

55 files changed

+4297
-1969
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+4297
-1969
lines changed

CHANGELOG.md

Lines changed: 432 additions & 1646 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"@salesforce/kit": "^3.2.2",
3030
"@salesforce/ts-types": "^2.0.12",
3131
"fast-levenshtein": "^3.0.0",
32-
"fast-xml-parser": "^4.4.1",
32+
"fast-xml-parser": "^4.5.0",
3333
"got": "^11.8.6",
3434
"graceful-fs": "^4.2.11",
3535
"ignore": "^5.3.2",

src/Presets.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,41 @@ source format
2020
Each child of PermissionSet that is a repeated xml element (ex: ClassAccesses) is saved as a separate file
2121
Simple fields (ex: `description`, `userLicense`) remain in the top-level `myPS.permissionset-meta.xml`
2222

23-
FieldPermissions for all objects are in the same folder (they're not in sub-folders by object). This is intentional--I wanted subfolders but couldn't get it to work well.
23+
FieldPermissions for all objects are in the same folder (they're not in sub-folders by object). This is intentional
24+
25+
## `decomposePermissionSetBeta2`
26+
27+
PermissionSet is decomposed to a folder named after the PermissionSet with one file containing grouped children - there will also be a new directory "objectSettings" grouping similar objectSettings child types.
28+
29+
metadata format
30+
`/permissionsets/myPS.permissionset`
31+
32+
source format
33+
34+
```txt
35+
└─ permissionsets
36+
├─ PO_Manager
37+
│ ├─ objectSettings
38+
│ │ ├─ Account.objectSettings-meta.xml
39+
│ │ ├─ PO_Line_Item__c.objectSettings-meta.xml
40+
│ │ └─ Purchase_Order__c.objectSettings-meta.xml
41+
│ ├─ PO_Manager.applicationVisibilities-meta.xml
42+
│ ├─ PO_Manager.classAccesses-meta.xml
43+
│ ├─ PO_Manager.customPermissions-meta.xml
44+
│ ├─ PO_Manager.customSettingAccesses-meta.xml
45+
│ ├─ PO_Manager.externalCredentialPrincipalAccesses-meta.xml
46+
│ ├─ PO_Manager.externalDataSourceAccesses-meta.xml
47+
│ ├─ PO_Manager.flowAccesses-meta.xml
48+
│ ├─ PO_Manager.pageAccesses-meta.xml
49+
│ ├─ PO_Manager.permissionset-meta.xml
50+
│ └─ PO_Manager.userPermissions-meta.xml
51+
```
52+
53+
Simple fields (ex: `description`, `userLicense`) remain in the top-level `PO_Manager.permissionset-meta.xml`
54+
55+
Entries not specific to object's settings remain at the top-level, grouped into files, e.g. `ClassAccess`, `PageAccess`, `UserPermissions`...
56+
57+
Entries specific to object's settings are grouped in the `objectSettings` directory and grouped into object-specific files, e.g. `PO_Line_Item__c.objectSettings`, in there you'll find entries related to `FieldPermissions`, `TabSettings`, `ObjetPermissions` and other object-specific fields.
2458

2559
## `decomposeSharingRulesBeta`
2660

src/convert/convertContext/convertContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { NonDecompositionFinalizer } from './nonDecompositionFinalizer';
1010
import { DecompositionFinalizer } from './decompositionFinalizer';
1111
import { ConvertTransactionFinalizer } from './transactionFinalizer';
1212
import { DecomposedLabelsFinalizer } from './decomposedLabelsFinalizer';
13+
import { DecomposedPermissionSetFinalizer } from './decomposedPermissionSetFinalizer';
1314
/**
1415
* A state manager over the course of a single metadata conversion call.
1516
*/
@@ -18,6 +19,7 @@ export class ConvertContext {
1819
public readonly recomposition = new RecompositionFinalizer();
1920
public readonly nonDecomposition = new NonDecompositionFinalizer();
2021
public readonly decomposedLabels = new DecomposedLabelsFinalizer();
22+
public readonly decomposedPermissionSet = new DecomposedPermissionSetFinalizer();
2123

2224
// eslint-disable-next-line @typescript-eslint/require-await
2325
public async *executeFinalizers(defaultDirectory?: string): AsyncIterable<WriterFormat[]> {
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (c) 2023, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import { join } from 'node:path';
8+
import { ensure, ensureString } from '@salesforce/ts-types';
9+
import type { PermissionSet } from '@jsforce/jsforce-node/lib/api/metadata/schema';
10+
import { MetadataType } from '../../registry';
11+
import { XML_NS_KEY, XML_NS_URL } from '../../common/constants';
12+
import { JsToXml } from '../streams';
13+
import { WriterFormat } from '../types';
14+
import { ConvertTransactionFinalizer } from './transactionFinalizer';
15+
16+
type PermissionSetState = {
17+
/*
18+
* Incoming child xml (children of PS) which will be partial parts of a PermissionSet, keyed by the parent they belong to
19+
*/
20+
parentToChild: Map<string, PermissionSet[]>;
21+
};
22+
23+
/**
24+
* Merges child components that share the same related object (/objectSettings/<object name>.objectSettings) in the conversion pipeline
25+
* into a single file.
26+
*
27+
* Inserts unclaimed child components into the parent that belongs to the default package
28+
*/
29+
export class DecomposedPermissionSetFinalizer extends ConvertTransactionFinalizer<PermissionSetState> {
30+
public transactionState: PermissionSetState = {
31+
parentToChild: new Map(),
32+
};
33+
34+
/** to support custom presets (the only way this code should get hit at all pass in the type from a transformer that has registry access */
35+
public permissionSetType?: MetadataType;
36+
37+
// have to maintain the existing interface
38+
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
39+
public async finalize(defaultDirectory?: string): Promise<WriterFormat[]> {
40+
if (this.transactionState.parentToChild.size === 0) {
41+
return [];
42+
}
43+
44+
const agg: WriterFormat[] = [];
45+
this.transactionState.parentToChild.forEach((children, parent) => {
46+
agg.push({
47+
component: {
48+
type: ensure(this.permissionSetType, 'DecomposedPermissionSetFinalizer should have set PermissionSetType'),
49+
fullName: ensureString(parent),
50+
},
51+
writeInfos: [
52+
{
53+
output: join(
54+
ensure(this.permissionSetType?.directoryName, 'directoryName missing from PermissionSet type'),
55+
`${parent}.permissionset`
56+
),
57+
source: new JsToXml({
58+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
59+
PermissionSet: {
60+
[XML_NS_KEY]: XML_NS_URL,
61+
...Object.assign(
62+
{},
63+
// sort the children by fullName
64+
...Object.values(children.sort((a, b) => ((a.fullName ?? '') > (b.fullName ?? '') ? -1 : 1)))
65+
),
66+
},
67+
}),
68+
},
69+
],
70+
});
71+
});
72+
73+
return agg;
74+
}
75+
}

src/convert/transformers/decomposedMetadataTransformer.ts

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type { SourcePath } from '../../common/types';
2323
import { ComponentSet } from '../../collections/componentSet';
2424
import type { DecompositionState, DecompositionStateValue } from '../convertContext/decompositionFinalizer';
2525
import { BaseMetadataTransformer } from './baseMetadataTransformer';
26+
import type { ComposedMetadata, ComposedMetadataWithChildType, InfoContainer } from './types';
2627

2728
type StateSetter = (forComponent: MetadataComponent, props: Partial<Omit<DecompositionStateValue, 'origin'>>) => void;
2829

@@ -186,7 +187,7 @@ const getChildWriteInfos =
186187
return [];
187188
};
188189

189-
const getWriteInfosFromMerge =
190+
export const getWriteInfosFromMerge =
190191
(mergeWith: SourceComponent) =>
191192
(stateSetter: StateSetter) =>
192193
(parentXmlObject: XmlObj) =>
@@ -208,7 +209,7 @@ const getWriteInfosFromMerge =
208209
return [];
209210
};
210211

211-
const getWriteInfosWithoutMerge =
212+
export const getWriteInfosWithoutMerge =
212213
(defaultDirectory: string | undefined) =>
213214
(parentXmlObject: XmlObj) =>
214215
(component: SourceComponent): WriteInfo[] => {
@@ -233,7 +234,7 @@ const getWriteInfosWithoutMerge =
233234
*
234235
* @param state
235236
*/
236-
const setDecomposedState =
237+
export const setDecomposedState =
237238
(state: DecompositionState) =>
238239
(forComponent: MetadataComponent, props: Partial<Omit<DecompositionStateValue, 'origin'>>): void => {
239240
const key = getKey(forComponent);
@@ -272,32 +273,20 @@ const getDefaultOutput = (component: MetadataComponent): SourcePath => {
272273
};
273274

274275
/** use the given xmlElementName name if it exists, otherwise use see if one matches the directories */
275-
const tagToChildTypeId = ({ tagKey, type }: { tagKey: string; type: MetadataType }): string | undefined =>
276+
export const tagToChildTypeId = ({ tagKey, type }: { tagKey: string; type: MetadataType }): string | undefined =>
276277
Object.values(type.children?.types ?? {}).find((c) => c.xmlElementName === tagKey)?.id ??
277278
type.children?.directories?.[tagKey];
278279

279-
const hasChildTypeId = (cm: ComposedMetadata): cm is Required<ComposedMetadata> => !!cm.childTypeId;
280+
export const hasChildTypeId = (cm: ComposedMetadata): cm is Required<ComposedMetadata> => !!cm.childTypeId;
280281

281-
const addChildType = (cm: Required<ComposedMetadata>): ComposedMetadataWithChildType => {
282+
export const addChildType = (cm: Required<ComposedMetadata>): ComposedMetadataWithChildType => {
282283
const childType = cm.parentType.children?.types[cm.childTypeId];
283284
if (childType) {
284285
return { ...cm, childType };
285286
}
286287
throw messages.createError('error_missing_child_type_definition', [cm.parentType.name, cm.childTypeId]);
287288
};
288289

289-
type ComposedMetadata = { tagKey: string; tagValue: AnyJson; parentType: MetadataType; childTypeId?: string };
290-
type ComposedMetadataWithChildType = ComposedMetadata & { childType: MetadataType };
291-
292-
type InfoContainer = {
293-
entryName: string;
294-
childComponent: MetadataComponent;
295-
/** the parsed xml */
296-
value: JsonMap;
297-
parentComponent: SourceComponent;
298-
mergeWith?: SourceComponent;
299-
};
300-
301290
/** returns an data structure with lots of context information in it */
302291
const toInfoContainer =
303292
(mergeWith: SourceComponent | undefined) =>
@@ -318,7 +307,7 @@ const toInfoContainer =
318307
};
319308
};
320309

321-
const forceIgnoreAllowsComponent =
310+
export const forceIgnoreAllowsComponent =
322311
(forceIgnore: ForceIgnore) =>
323312
(ic: InfoContainer): boolean =>
324313
forceIgnore.accepts(getDefaultOutput(ic.childComponent));
@@ -341,5 +330,5 @@ const buildParentXml =
341330
},
342331
});
343332

344-
const getOutputFile = (component: SourceComponent, mergeWith?: SourceComponent): string =>
333+
export const getOutputFile = (component: SourceComponent, mergeWith?: SourceComponent): string =>
345334
mergeWith?.xml ?? getDefaultOutput(component);

0 commit comments

Comments
 (0)