Skip to content

Commit

Permalink
fix: recomposing writing file, wrong child entry name CA, not cA
Browse files Browse the repository at this point in the history
  • Loading branch information
WillieRuemmele committed Sep 4, 2024
1 parent f2e9353 commit 6cc84d4
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 30 deletions.
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
80 changes: 80 additions & 0 deletions src/convert/convertContext/decomposedPermissionSetFinalizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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, JsonMap } 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 (CustomLabel) keyed by label fullname
*/
permissionSetChildByPath: Map<string, PermissionSet>;
};

/**
* Merges child components that share the same parent 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 = {
permissionSetChildByPath: 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.permissionSetChildByPath.size === 0) {
return [];
}

const fullName = Array.from(this.transactionState.permissionSetChildByPath.keys())[0].split(':')[1].split('.')[0];

return [
{
component: {
type: ensure(this.permissionSetType, 'DecomposedPermissionSetFinalizer should have set PermissionSetType'),
fullName,
},
writeInfos: [
{
output: join(
ensure(this.permissionSetType?.directoryName, 'directoryName missing from PermissionSet type'),
`${fullName}.permissionset`
),
source: new JsToXml(generateXml(this.transactionState.permissionSetChildByPath)),
},
],
},
];
}
}

/** 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']: {
[XML_NS_KEY]: XML_NS_URL,
// for CustomLabels, that's `labels`
...Object.assign({}, ...children.values()),
// labels: Array.from(children.values()).filter(customLabelHasFullName).sort(sortLabelsByFullName),
},
});

// type CustomLabelWithFullName = PermissionSet & { fullName: string };
//
// const sortLabelsByFullName = (a: CustomLabelWithFullName, b: CustomLabelWithFullName): number =>
// a.fullName.localeCompare(b.fullName);
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ 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 } from '../../utils/decomposed';
import { objectHasSomeRealValues, unwrapAndOmitNS } from '../../utils/decomposed';
import type { MetadataComponent } from '../../resolve/types';
import { type MetadataType } from '../../registry/types';
import { SourceComponent } from '../../resolve/sourceComponent';
Expand All @@ -28,33 +29,29 @@ type StateSetter = (forComponent: MetadataComponent, props: Partial<Omit<Decompo
Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');

export class FilePerChildTypeMetadataTransformer extends BaseMetadataTransformer {
export class DecomposedPermissionSetTransformer extends BaseMetadataTransformer {
// eslint-disable-next-line @typescript-eslint/require-await
public async toMetadataFormat(component: SourceComponent): Promise<WriteInfo[]> {
if (component.parent) {
const { fullName: parentName } = component.parent;
const stateForParent = this.context.recomposition.transactionState.get(parentName) ?? {
component: component.parent,
children: new ComponentSet([], this.registry),
};
stateForParent.children?.add(component);
this.context.recomposition.transactionState.set(parentName, stateForParent);
} else {
const { fullName } = component;
const existing = this.context.recomposition.transactionState.get(fullName) ?? {
component,
children: new ComponentSet([], this.registry),
};
if (component.xml && existing.component && !existing.component.xml) {
// we've already found and created the parent of this component on L~38
// but now we have more information about the parent (xml) that we didn't have before, so add it
existing.component = component;
}
(component.getChildren() ?? []).map((child) => {
existing.children?.add(child);
});
this.context.recomposition.transactionState.set(fullName, existing);
}
// only need to do this once
this.context.decomposedPermissionSet.permissionSetType ??= this.registry.getTypeByName('PermissionSet');
const children = component.getChildren();

[
...children,
// because the children have the same name as the parent
// TODO: this feels wrong, I'm not sure where the parent is really parsed
new SourceComponent({
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) => {
this.context.decomposedPermissionSet.transactionState.permissionSetChildByPath.set(
`${c.xml!}:${c.fullName}`,
unwrapAndOmitNS('PermissionSet')(c.parseXmlSync()) as PermissionSet
);
});

// noop since the finalizer will push the writes to the component writer
return [];
}
Expand Down
4 changes: 2 additions & 2 deletions src/convert/transformers/metadataTransformerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { DecomposedMetadataTransformer } from './decomposedMetadataTransformer';
import { StaticResourceMetadataTransformer } from './staticResourceMetadataTransformer';
import { NonDecomposedMetadataTransformer } from './nonDecomposedMetadataTransformer';
import { LabelMetadataTransformer, LabelsMetadataTransformer } from './decomposeLabelsTransformer';
import { FilePerChildTypeMetadataTransformer } from './filePerChildTypeMetadataTransformer';
import { DecomposedPermissionSetTransformer } from './decomposedPermissionSetTransformer';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
Expand Down Expand Up @@ -43,7 +43,7 @@ export class MetadataTransformerFactory {
case 'nonDecomposed':
return new NonDecomposedMetadataTransformer(this.registry, this.context);
case 'filePerType':
return new FilePerChildTypeMetadataTransformer(this.registry, this.context);
return new DecomposedPermissionSetTransformer(this.registry, this.context);
case 'decomposedLabels':
return component.type.name === 'CustomLabels'
? new LabelsMetadataTransformer(this.registry, this.context)
Expand Down
10 changes: 9 additions & 1 deletion src/registry/presets/decomposePermissionSetBeta2.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"pageAccess": "pageaccess",
"recordTypeVisibility": "recordtypevisibility",
"tabSetting": "tabsetting",
"userPermission": "userpermission"
"userPermission": "userpermission",
"objectSettings": "objectsettings"
},
"types": {
"applicationvisibility": {
Expand Down Expand Up @@ -113,6 +114,13 @@
"suffix": "externalDataSourceAccess",
"uniqueIdElement": "externalDataSource"
},
"objectsettings": {
"id": "objectsettings",
"name": "ObjectSettings",
"directoryName": "objectSettings",
"suffix": "objectsettings",
"isAddressable": false
},
"fieldpermission": {
"directoryName": "objectSettings",
"id": "fieldpermission",
Expand Down
5 changes: 4 additions & 1 deletion src/resolve/sourceComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,10 @@ export class SourceComponent implements MetadataComponent {
}
const children = ensureArray(get(parentXml, `${this.parent.type.name}.${getXmlElement(this.type)}`)) as T[];
const uniqueElement = this.type.uniqueIdElement;
const matched = uniqueElement ? children.find((c) => getString(c, uniqueElement) === this.name) : undefined;
const matched = uniqueElement
? children.find((c) => getString(c, uniqueElement) === this.name) ??
(parentXml[this.parent.type.name as keyof T] as T)
: (parentXml[this.parent.type.name as keyof T] as T) ?? undefined;
if (!matched) {
throw new SfError(
`Invalid XML tags or unable to find matching parent xml file for ${this.type.name} "${this.name}"`
Expand Down

0 comments on commit 6cc84d4

Please sign in to comment.