Skip to content

Commit

Permalink
test: export shared functions from Decomposed, add UT
Browse files Browse the repository at this point in the history
  • Loading branch information
WillieRuemmele committed Sep 6, 2024
1 parent d1077cb commit 5b55a72
Show file tree
Hide file tree
Showing 7 changed files with 628 additions and 131 deletions.
4 changes: 2 additions & 2 deletions src/Presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ 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`

Expand Down Expand Up @@ -52,7 +52,7 @@ source format

Simple fields (ex: `description`, `userLicense`) remain in the top-level `PO_Manager.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

## `decomposeSharingRulesBeta`

Expand Down
19 changes: 8 additions & 11 deletions src/convert/convertContext/decomposedPermissionSetFinalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import { ConvertTransactionFinalizer } from './transactionFinalizer';

type PermissionSetState = {
/*
* Incoming child xml (CustomLabel) keyed by label fullname
* Incoming child xml (children of PS) keyed by label name
*/
permissionSetChildByPath: Map<string, PermissionSet>;
};

/**
* Merges child components that share the same parent in the conversion pipeline
* Merges child components that share the same object in the conversion pipeline
* into a single file.
*
* Inserts unclaimed child components into the parent that belongs to the default package
Expand Down Expand Up @@ -66,15 +66,12 @@ export class DecomposedPermissionSetFinalizer extends ConvertTransactionFinalize
/** Return a json object that's built up from the mergeMap children */
const generateXml = (children: Map<string, PermissionSet>): JsonMap => ({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
['PermissionSet']: {
PermissionSet: {
[XML_NS_KEY]: XML_NS_URL,
// for CustomLabels, that's `labels`
...Object.assign({}, ...children.values()),
// labels: Array.from(children.values()).filter(customLabelHasFullName).sort(sortLabelsByFullName),
...Object.assign(
{},
// sort the children by fullName
...Object.values(Array.from(children.values()).sort((a, b) => ((a.fullName ?? '') > (b.fullName ?? '') ? -1 : 1)))
),
},
});

// type CustomLabelWithFullName = PermissionSet & { fullName: string };
//
// const sortLabelsByFullName = (a: CustomLabelWithFullName, b: CustomLabelWithFullName): number =>
// a.fullName.localeCompare(b.fullName);
22 changes: 11 additions & 11 deletions src/convert/transformers/decomposedMetadataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ const getChildWriteInfos =
return [];
};

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

const getWriteInfosWithoutMerge =
export const getWriteInfosWithoutMerge =
(defaultDirectory: string | undefined) =>
(parentXmlObject: XmlObj) =>
(component: SourceComponent): WriteInfo[] => {
Expand All @@ -233,7 +233,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,24 +272,24 @@ 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 };
export type ComposedMetadata = { tagKey: string; tagValue: AnyJson; parentType: MetadataType; childTypeId?: string };
export type ComposedMetadataWithChildType = ComposedMetadata & { childType: MetadataType };

type InfoContainer = {
export type InfoContainer = {
entryName: string;
childComponent: MetadataComponent;
/** the parsed xml */
Expand Down Expand Up @@ -318,7 +318,7 @@ const toInfoContainer =
};
};

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

const getOutputFile = (component: SourceComponent, mergeWith?: SourceComponent): string =>
export const getOutputFile = (component: SourceComponent, mergeWith?: SourceComponent): string =>
mergeWith?.xml ?? getDefaultOutput(component);
131 changes: 24 additions & 107 deletions src/convert/transformers/decomposedPermissionSetTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
*/

import { dirname, join } from 'node:path';
import fs from 'node:fs';
import { AnyJson, ensureString, JsonMap } from '@salesforce/ts-types';
import { Messages } from '@salesforce/core';
import type { PermissionSet } from '@jsforce/jsforce-node/lib/api/metadata/schema';
import { calculateRelativePath } from '../../utils/path';
import { ForceIgnore } from '../../resolve/forceIgnore';
import { objectHasSomeRealValues, unwrapAndOmitNS } from '../../utils/decomposed';
import { unwrapAndOmitNS } from '../../utils/decomposed';
import type { MetadataComponent } from '../../resolve/types';
import { type MetadataType } from '../../registry/types';
import { SourceComponent } from '../../resolve/sourceComponent';
Expand All @@ -21,8 +19,20 @@ import type { ToSourceFormatInput, WriteInfo, XmlObj } from '../types';
import { META_XML_SUFFIX, XML_NS_KEY, XML_NS_URL } from '../../common/constants';
import type { SourcePath } from '../../common/types';
import { ComponentSet } from '../../collections/componentSet';
import type { DecompositionState, DecompositionStateValue } from '../convertContext/decompositionFinalizer';
import type { DecompositionStateValue } from '../convertContext/decompositionFinalizer';
import { BaseMetadataTransformer } from './baseMetadataTransformer';
import {
addChildType,
ComposedMetadata,
forceIgnoreAllowsComponent,
getOutputFile,
getWriteInfosFromMerge,
getWriteInfosWithoutMerge,
hasChildTypeId,
InfoContainer,
setDecomposedState,
tagToChildTypeId,
} from './decomposedMetadataTransformer';

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

Expand All @@ -41,8 +51,8 @@ export class DecomposedPermissionSetTransformer extends BaseMetadataTransformer
// TODO: this feels wrong, I'm not sure why the parent (.permissionset) isn't here child.getChildren() returns children
new SourceComponent({
// because the children have the same name as the parent
name: children[0].name,
xml: children[0].xml!.replace(/(\w+\.\w+-meta\.xml)/gm, `${children[0].name}.permissionset-meta.xml`),
name: children[0]?.name,
xml: children[0]?.xml!.replace(/(\w+\.\w+-meta\.xml)/gm, `${children[0].name}.permissionset-meta.xml`),
type: this.context.decomposedPermissionSet.permissionSetType,
}),
].map((c) => {
Expand Down Expand Up @@ -84,9 +94,11 @@ export class DecomposedPermissionSetTransformer extends BaseMetadataTransformer

const writeInfosForChildren = getAndCombineChildWriteInfos(
[
// children whose type don't have a directory assigned will be written to the top level, separate them into individual WriteInfo[][]
// children whose type don't have a directory assigned will be written to the top level, separate them into individual WriteInfo[] with only one entry
// a [WriteInfo] with one entry, will result in one file
...preparedMetadata.filter((c) => !c.childComponent.type.directoryName).map((c) => [c]),
// children whose type have a directory name will be grouped accordingly, bundle these together as a WriteInfo[][] with length > 1
// a [WriteInfo, WriteInfo, ...] will be combined into a [WriteInfo] with combined contents
preparedMetadata.filter((c) => c.childComponent.type.directoryName),
],
stateSetter,
Expand Down Expand Up @@ -116,66 +128,6 @@ export class DecomposedPermissionSetTransformer extends BaseMetadataTransformer

const hasXml = (c: SourceComponent): c is SourceComponent & { xml: string } => typeof c.xml === 'string';

const getWriteInfosFromMerge =
(mergeWith: SourceComponent) =>
(stateSetter: StateSetter) =>
(parentXmlObject: XmlObj) =>
(parentComponent: SourceComponent): WriteInfo[] => {
const writeInfo = { source: new JsToXml(parentXmlObject), output: getOutputFile(parentComponent, mergeWith) };
const parentHasRealValues = objectHasSomeRealValues(parentComponent.type)(parentXmlObject);

if (mergeWith?.xml) {
// mark the component as found
stateSetter(parentComponent, { foundMerge: true });
return objectHasSomeRealValues(parentComponent.type)(mergeWith.parseXmlSync()) && !parentHasRealValues
? [] // the target file has values but this process doesn't, so we don't want to overwrite it
: [writeInfo];
}
if (objectHasSomeRealValues(parentComponent.type)(parentXmlObject)) {
// set the state but don't return any writeInfo to avoid writing "empty" (ns-only) parent files
stateSetter(parentComponent, { writeInfo });
}
return [];
};

const getWriteInfosWithoutMerge =
(defaultDirectory: string | undefined) =>
(parentXmlObject: XmlObj) =>
(component: SourceComponent): WriteInfo[] => {
const output = join(defaultDirectory ?? '', getOutputFile(component));
// if the parent would be empty
// and it exists
// and every child is addressable
// don't overwrite the existing parent
if (
!objectHasSomeRealValues(component.type)(parentXmlObject) &&
fs.existsSync(output) &&
Object.values(component.type.children ?? {}).every((child) => !child.isAddressable)
) {
return [];
} else {
return [{ source: new JsToXml(parentXmlObject), output }];
}
};

/**
* Helper for setting the decomposed transaction state
*
* @param state
*/
const setDecomposedState =
(state: DecompositionState) =>
(forComponent: MetadataComponent, props: Partial<Omit<DecompositionStateValue, 'origin'>>): void => {
const key = getKey(forComponent);
state.set(key, {
// origin gets set the first time
...(state.get(key) ?? { origin: forComponent.parent ?? forComponent }),
...(props ?? {}),
});
};

const getKey = (component: MetadataComponent): string => `${component.type.name}#${component.fullName}`;

/** for a component, parse the xml and create an json object with contents, child typeId, etc */
const getComposedMetadataEntries = async (component: SourceComponent): Promise<ComposedMetadata[]> =>
// composedMetadata might be undefined if you call toSourceFormat() from a non-source-backed Component
Expand All @@ -201,40 +153,13 @@ const getDefaultOutput = (component: MetadataComponent): SourcePath => {
return join(calculateRelativePath('source')({ self: parent?.type ?? type })(fullName)(baseName), output);
};

/** 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 =>
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;

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;
};

const getAndCombineChildWriteInfos = (
containers: InfoContainer[][],
stateSetter: StateSetter,
childrenOfMergeComponent: ComponentSet
): WriteInfo[] => {
// aggregator write info, will be returned at the end
const agg: WriteInfo[] = [];
const writeInfos: WriteInfo[] = [];
containers.forEach((c) => {
// we have multiple InfoContainers, build a map of output file => file content
// this is how we'll combine multiple children into one file
Expand Down Expand Up @@ -266,7 +191,7 @@ const getAndCombineChildWriteInfos = (
// if there's nothing to merge with, push write operation now to default location
const childInfo = info[0].childComponent;
if (!info[0].mergeWith) {
agg.push({ source, output: getDefaultOutput(childInfo) });
writeInfos.push({ source, output: getDefaultOutput(childInfo) });
return;
}
// if the merge parent has a child that can be merged with, push write
Expand All @@ -277,14 +202,14 @@ const getAndCombineChildWriteInfos = (
throw messages.createError('error_parsing_xml', [childInfo.fullName, childInfo.type.name]);
}
stateSetter(childInfo, { foundMerge: true });
agg.push({ source, output: mergeChild.xml });
writeInfos.push({ source, output: mergeChild.xml });
return;
}
// If we have a parent and the child is unaddressable without the parent, keep them
// together on the file system, meaning a new child will not be written to the default dir.
if (childInfo.type.unaddressableWithoutParent && typeof info[0].mergeWith?.xml === 'string') {
// get output path from parent
agg.push({
writeInfos.push({
source,
output: join(
dirname(info[0].mergeWith.xml),
Expand All @@ -300,7 +225,7 @@ const getAndCombineChildWriteInfos = (
return [];
});
});
return agg;
return writeInfos;
};

/** returns a data structure with lots of context information in it */
Expand All @@ -324,11 +249,3 @@ const toInfoContainer =
mergeWith,
};
};

const forceIgnoreAllowsComponent =
(forceIgnore: ForceIgnore) =>
(ic: InfoContainer): boolean =>
forceIgnore.accepts(getDefaultOutput(ic.childComponent));

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

2 comments on commit 5b55a72

@svc-cli-bot
Copy link
Collaborator

Choose a reason for hiding this comment

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

Benchmark

Benchmark suite Current: 5b55a72 Previous: 61a7637 Ratio
eda-componentSetCreate-linux 234 ms 223 ms 1.05
eda-sourceToMdapi-linux 2268 ms 2342 ms 0.97
eda-sourceToZip-linux 1883 ms 1832 ms 1.03
eda-mdapiToSource-linux 3065 ms 2983 ms 1.03
lotsOfClasses-componentSetCreate-linux 432 ms 433 ms 1.00
lotsOfClasses-sourceToMdapi-linux 3684 ms 3659 ms 1.01
lotsOfClasses-sourceToZip-linux 3188 ms 3010 ms 1.06
lotsOfClasses-mdapiToSource-linux 3613 ms 3531 ms 1.02
lotsOfClassesOneDir-componentSetCreate-linux 747 ms 727 ms 1.03
lotsOfClassesOneDir-sourceToMdapi-linux 6486 ms 6440 ms 1.01
lotsOfClassesOneDir-sourceToZip-linux 5581 ms 5363 ms 1.04
lotsOfClassesOneDir-mdapiToSource-linux 6558 ms 6474 ms 1.01

This comment was automatically generated by workflow using github-action-benchmark.

@svc-cli-bot
Copy link
Collaborator

Choose a reason for hiding this comment

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

Benchmark

Benchmark suite Current: 5b55a72 Previous: 61a7637 Ratio
eda-componentSetCreate-win32 603 ms 717 ms 0.84
eda-sourceToMdapi-win32 4265 ms 4711 ms 0.91
eda-sourceToZip-win32 3046 ms 3234 ms 0.94
eda-mdapiToSource-win32 5673 ms 6277 ms 0.90
lotsOfClasses-componentSetCreate-win32 1234 ms 1221 ms 1.01
lotsOfClasses-sourceToMdapi-win32 7635 ms 7824 ms 0.98
lotsOfClasses-sourceToZip-win32 5064 ms 5075 ms 1.00
lotsOfClasses-mdapiToSource-win32 7710 ms 7857 ms 0.98
lotsOfClassesOneDir-componentSetCreate-win32 2130 ms 2074 ms 1.03
lotsOfClassesOneDir-sourceToMdapi-win32 13815 ms 13816 ms 1.00
lotsOfClassesOneDir-sourceToZip-win32 9251 ms 9168 ms 1.01
lotsOfClassesOneDir-mdapiToSource-win32 14133 ms 14170 ms 1.00

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.