Skip to content

Commit

Permalink
feat: Wr/decompose permission sets (#1412)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
WillieRuemmele authored Sep 15, 2024
1 parent abc4d38 commit 3650332
Show file tree
Hide file tree
Showing 55 changed files with 4,297 additions and 1,969 deletions.
2,078 changes: 432 additions & 1,646 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@salesforce/kit": "^3.2.2",
"@salesforce/ts-types": "^2.0.12",
"fast-levenshtein": "^3.0.0",
"fast-xml-parser": "^4.4.1",
"fast-xml-parser": "^4.5.0",
"got": "^11.8.6",
"graceful-fs": "^4.2.11",
"ignore": "^5.3.2",
Expand Down
36 changes: 35 additions & 1 deletion src/Presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,41 @@ source format
Each child of PermissionSet that is a repeated xml element (ex: ClassAccesses) is saved as a separate file
Simple fields (ex: `description`, `userLicense`) remain in the top-level `myPS.permissionset-meta.xml`

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.
FieldPermissions for all objects are in the same folder (they're not in sub-folders by object). This is intentional

## `decomposePermissionSetBeta2`

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.

metadata format
`/permissionsets/myPS.permissionset`

source format

```txt
└─ permissionsets
├─ PO_Manager
│ ├─ objectSettings
│ │ ├─ Account.objectSettings-meta.xml
│ │ ├─ PO_Line_Item__c.objectSettings-meta.xml
│ │ └─ Purchase_Order__c.objectSettings-meta.xml
│ ├─ PO_Manager.applicationVisibilities-meta.xml
│ ├─ PO_Manager.classAccesses-meta.xml
│ ├─ PO_Manager.customPermissions-meta.xml
│ ├─ PO_Manager.customSettingAccesses-meta.xml
│ ├─ PO_Manager.externalCredentialPrincipalAccesses-meta.xml
│ ├─ PO_Manager.externalDataSourceAccesses-meta.xml
│ ├─ PO_Manager.flowAccesses-meta.xml
│ ├─ PO_Manager.pageAccesses-meta.xml
│ ├─ PO_Manager.permissionset-meta.xml
│ └─ PO_Manager.userPermissions-meta.xml
```

Simple fields (ex: `description`, `userLicense`) remain in the top-level `PO_Manager.permissionset-meta.xml`

Entries not specific to object's settings remain at the top-level, grouped into files, e.g. `ClassAccess`, `PageAccess`, `UserPermissions`...

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.

## `decomposeSharingRulesBeta`

Expand Down
2 changes: 2 additions & 0 deletions src/convert/convertContext/convertContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { NonDecompositionFinalizer } from './nonDecompositionFinalizer';
import { DecompositionFinalizer } from './decompositionFinalizer';
import { ConvertTransactionFinalizer } from './transactionFinalizer';
import { DecomposedLabelsFinalizer } from './decomposedLabelsFinalizer';
import { DecomposedPermissionSetFinalizer } from './decomposedPermissionSetFinalizer';
/**
* A state manager over the course of a single metadata conversion call.
*/
Expand All @@ -18,6 +19,7 @@ export class ConvertContext {
public readonly recomposition = new RecompositionFinalizer();
public readonly nonDecomposition = new NonDecompositionFinalizer();
public readonly decomposedLabels = new DecomposedLabelsFinalizer();
public readonly decomposedPermissionSet = new DecomposedPermissionSetFinalizer();

// eslint-disable-next-line @typescript-eslint/require-await
public async *executeFinalizers(defaultDirectory?: string): AsyncIterable<WriterFormat[]> {
Expand Down
75 changes: 75 additions & 0 deletions src/convert/convertContext/decomposedPermissionSetFinalizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { join } from 'node:path';
import { ensure, ensureString } from '@salesforce/ts-types';
import type { PermissionSet } from '@jsforce/jsforce-node/lib/api/metadata/schema';
import { MetadataType } from '../../registry';
import { XML_NS_KEY, XML_NS_URL } from '../../common/constants';
import { JsToXml } from '../streams';
import { WriterFormat } from '../types';
import { ConvertTransactionFinalizer } from './transactionFinalizer';

type PermissionSetState = {
/*
* Incoming child xml (children of PS) which will be partial parts of a PermissionSet, keyed by the parent they belong to
*/
parentToChild: Map<string, PermissionSet[]>;
};

/**
* Merges child components that share the same related object (/objectSettings/<object name>.objectSettings) in the conversion pipeline
* into a single file.
*
* Inserts unclaimed child components into the parent that belongs to the default package
*/
export class DecomposedPermissionSetFinalizer extends ConvertTransactionFinalizer<PermissionSetState> {
public transactionState: PermissionSetState = {
parentToChild: new Map(),
};

/** 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 */
public permissionSetType?: MetadataType;

// have to maintain the existing interface
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
public async finalize(defaultDirectory?: string): Promise<WriterFormat[]> {
if (this.transactionState.parentToChild.size === 0) {
return [];
}

const agg: WriterFormat[] = [];
this.transactionState.parentToChild.forEach((children, parent) => {
agg.push({
component: {
type: ensure(this.permissionSetType, 'DecomposedPermissionSetFinalizer should have set PermissionSetType'),
fullName: ensureString(parent),
},
writeInfos: [
{
output: join(
ensure(this.permissionSetType?.directoryName, 'directoryName missing from PermissionSet type'),
`${parent}.permissionset`
),
source: new JsToXml({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
PermissionSet: {
[XML_NS_KEY]: XML_NS_URL,
...Object.assign(
{},
// sort the children by fullName
...Object.values(children.sort((a, b) => ((a.fullName ?? '') > (b.fullName ?? '') ? -1 : 1)))
),
},
}),
},
],
});
});

return agg;
}
}
29 changes: 9 additions & 20 deletions src/convert/transformers/decomposedMetadataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { SourcePath } from '../../common/types';
import { ComponentSet } from '../../collections/componentSet';
import type { DecompositionState, DecompositionStateValue } from '../convertContext/decompositionFinalizer';
import { BaseMetadataTransformer } from './baseMetadataTransformer';
import type { ComposedMetadata, ComposedMetadataWithChildType, InfoContainer } from './types';

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

Expand Down Expand Up @@ -186,7 +187,7 @@ const getChildWriteInfos =
return [];
};

const getWriteInfosFromMerge =
export const getWriteInfosFromMerge =
(mergeWith: SourceComponent) =>
(stateSetter: StateSetter) =>
(parentXmlObject: XmlObj) =>
Expand All @@ -208,7 +209,7 @@ const getWriteInfosFromMerge =
return [];
};

const getWriteInfosWithoutMerge =
export const getWriteInfosWithoutMerge =
(defaultDirectory: string | undefined) =>
(parentXmlObject: XmlObj) =>
(component: SourceComponent): WriteInfo[] => {
Expand All @@ -233,7 +234,7 @@ const getWriteInfosWithoutMerge =
*
* @param state
*/
const setDecomposedState =
export const setDecomposedState =
(state: DecompositionState) =>
(forComponent: MetadataComponent, props: Partial<Omit<DecompositionStateValue, 'origin'>>): void => {
const key = getKey(forComponent);
Expand Down Expand Up @@ -272,32 +273,20 @@ const getDefaultOutput = (component: MetadataComponent): SourcePath => {
};

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

const hasChildTypeId = (cm: ComposedMetadata): cm is Required<ComposedMetadata> => !!cm.childTypeId;
export const hasChildTypeId = (cm: ComposedMetadata): cm is Required<ComposedMetadata> => !!cm.childTypeId;

const addChildType = (cm: Required<ComposedMetadata>): ComposedMetadataWithChildType => {
export const addChildType = (cm: Required<ComposedMetadata>): ComposedMetadataWithChildType => {
const childType = cm.parentType.children?.types[cm.childTypeId];
if (childType) {
return { ...cm, childType };
}
throw messages.createError('error_missing_child_type_definition', [cm.parentType.name, cm.childTypeId]);
};

type ComposedMetadata = { tagKey: string; tagValue: AnyJson; parentType: MetadataType; childTypeId?: string };
type ComposedMetadataWithChildType = ComposedMetadata & { childType: MetadataType };

type InfoContainer = {
entryName: string;
childComponent: MetadataComponent;
/** the parsed xml */
value: JsonMap;
parentComponent: SourceComponent;
mergeWith?: SourceComponent;
};

/** returns an data structure with lots of context information in it */
const toInfoContainer =
(mergeWith: SourceComponent | undefined) =>
Expand All @@ -318,7 +307,7 @@ const toInfoContainer =
};
};

const forceIgnoreAllowsComponent =
export const forceIgnoreAllowsComponent =
(forceIgnore: ForceIgnore) =>
(ic: InfoContainer): boolean =>
forceIgnore.accepts(getDefaultOutput(ic.childComponent));
Expand All @@ -341,5 +330,5 @@ const buildParentXml =
},
});

const getOutputFile = (component: SourceComponent, mergeWith?: SourceComponent): string =>
export const getOutputFile = (component: SourceComponent, mergeWith?: SourceComponent): string =>
mergeWith?.xml ?? getDefaultOutput(component);
Loading

0 comments on commit 3650332

Please sign in to comment.