Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c5d5b3d
chore: wip
peternhale Jan 6, 2025
e307240
chore: license year
iowillhoit Jan 6, 2025
87eb63f
chore(release): 12.11.0 [skip ci]
svc-cli-bot Jan 6, 2025
3e0eeff
chore: only check md file (#1475)
iowillhoit Jan 6, 2025
4af7d65
fix: add workflow flow actions to decomposed workflow preset and allo…
mcarvin8 Jan 6, 2025
417f8ea
chore(release): 12.11.1 [skip ci]
svc-cli-bot Jan 6, 2025
e23df2f
W-17279149 Register MD APIs to metadata registry (#1472)
IdanRoas Jan 7, 2025
ff1c9f3
chore(release): 12.11.2 [skip ci]
svc-cli-bot Jan 7, 2025
e509950
chore: auto-update metadata coverage in METADATA_SUPPORT.md [no ci]
svc-cli-bot Jan 7, 2025
bf10db9
fix: update snapshot (#1478)
WillieRuemmele Jan 8, 2025
8f4918a
chore(release): 12.11.3 [skip ci]
svc-cli-bot Jan 8, 2025
c201414
feat(mdTypes): register tua viz and ws metadata types (#1479)
PraveenViswanathan Jan 8, 2025
cd1b3b9
chore(release): 12.12.0 [skip ci]
svc-cli-bot Jan 8, 2025
7098545
chore: auto-update metadata coverage in METADATA_SUPPORT.md [no ci]
svc-cli-bot Jan 8, 2025
00792f0
fix: resolve strict dirs before suffixes for potential metadata files…
shetzel Jan 9, 2025
8c57c40
chore(release): 12.12.1 [skip ci]
svc-cli-bot Jan 9, 2025
e963eb4
chore: wip
peternhale Jan 8, 2025
484663a
chore: wip
peternhale Jan 8, 2025
5f64455
chore: wip
peternhale Jan 9, 2025
fc61ee0
chore: encoded in MD, chars in SD (#1485)
WillieRuemmele Jan 13, 2025
57bc064
chore: wip
peternhale Jan 16, 2025
35ad23a
chore: wip
peternhale Jan 21, 2025
f5d552b
chore: update md xml to include xml header
peternhale Jan 21, 2025
fbd0528
chore: temp (#1490)
WillieRuemmele Jan 21, 2025
aa5599f
Merge branch 'main' of github.com:forcedotcom/source-deploy-retrieve …
peternhale Jan 22, 2025
995d97c
chore: wip
peternhale Jan 22, 2025
1f7d429
Wr/decompose esr (#1492)
WillieRuemmele Jan 24, 2025
17b1a11
Merge branch 'main' into phale/decompose-esr
peternhale Jan 24, 2025
a89796b
chore: fix test
peternhale Jan 24, 2025
b01a935
chore: address review suggestions
peternhale Jan 24, 2025
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
2,197 changes: 458 additions & 1,739 deletions CHANGELOG.md

Large diffs are not rendered by default.

1,339 changes: 669 additions & 670 deletions METADATA_SUPPORT.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
"node": ">=18.0.0"
},
"dependencies": {
"@salesforce/core": "^8.8.0",
"@salesforce/kit": "^3.2.2",
"@salesforce/core": "^8.8.2",
"@salesforce/kit": "^3.2.3",
"@salesforce/ts-types": "^2.0.12",
"fast-levenshtein": "^3.0.0",
"fast-xml-parser": "^4.5.1",
Expand All @@ -37,7 +37,8 @@
"jszip": "^3.10.1",
"mime": "2.6.0",
"minimatch": "^9.0.5",
"proxy-agent": "^6.4.0"
"proxy-agent": "^6.4.0",
"yaml": "^2.6.1"
},
"devDependencies": {
"@jsforce/jsforce-node": "^3.6.3",
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 @@ -11,6 +11,7 @@ import { DecompositionFinalizer } from './decompositionFinalizer';
import { ConvertTransactionFinalizer } from './transactionFinalizer';
import { DecomposedLabelsFinalizer } from './decomposedLabelsFinalizer';
import { DecomposedPermissionSetFinalizer } from './decomposedPermissionSetFinalizer';
import { DecomposedExternalServiceRegistrationFinalizer } from './decomposedExternalServiceRegistrationFinalizer';
/**
* A state manager over the course of a single metadata conversion call.
*/
Expand All @@ -20,6 +21,7 @@ export class ConvertContext {
public readonly nonDecomposition = new NonDecompositionFinalizer();
public readonly decomposedLabels = new DecomposedLabelsFinalizer();
public readonly decomposedPermissionSet = new DecomposedPermissionSetFinalizer();
public readonly decomposedExternalServiceRegistration = new DecomposedExternalServiceRegistrationFinalizer();

// eslint-disable-next-line @typescript-eslint/require-await
public async *executeFinalizers(defaultDirectory?: string): AsyncIterable<WriterFormat[]> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2025, 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 type { ExternalServiceRegistration } from '@jsforce/jsforce-node/lib/api/metadata/schema';
import { ensure, ensureString } from '@salesforce/ts-types';
import { WriterFormat } from '../types';
import { MetadataType } from '../../registry';
import { JsToXml } from '../streams';
import { ConvertTransactionFinalizer } from './transactionFinalizer';

type ExternalServiceRegistrationState = {
esrRecords: Map<string, ExternalServiceRegistration>;
};

export class DecomposedExternalServiceRegistrationFinalizer extends ConvertTransactionFinalizer<ExternalServiceRegistrationState> {
/** 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 externalServiceRegistration?: MetadataType;
public transactionState: ExternalServiceRegistrationState = {
esrRecords: new Map<string, ExternalServiceRegistration>(),
};
// eslint-disable-next-line class-methods-use-this
public defaultDir: string | undefined;

public finalize(defaultDirectory: string | undefined): Promise<WriterFormat[]> {
this.defaultDir = defaultDirectory;
const writerFormats: WriterFormat[] = [];
this.transactionState.esrRecords.forEach((esrRecord, parent) =>
writerFormats.push({
component: {
type: ensure(this.externalServiceRegistration, 'DecomposedESRFinalizer should have set .ESR'),
fullName: ensureString(parent),
},
writeInfos: [
{
output: join(
ensure(this.externalServiceRegistration?.directoryName, 'directory name missing'),
`${parent}.externalServiceRegistration`
),
source: new JsToXml({ ExternalServiceRegistration: { ...esrRecord } }),
},
],
})
);
return Promise.resolve(writerFormats);
}
}
4 changes: 2 additions & 2 deletions src/convert/transformers/baseMetadataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
*/
import { MetadataTransformer, WriteInfo } from '../types';
import { ConvertContext } from '../convertContext/convertContext';
import { SourceComponent } from '../../resolve/sourceComponent';
import { RegistryAccess } from '../../registry/registryAccess';
import { SourceComponent } from '../../resolve';
import { RegistryAccess } from '../../registry';

export abstract class BaseMetadataTransformer implements MetadataTransformer {
public readonly context: ConvertContext;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* 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 * as path from 'node:path';
import { Readable } from 'node:stream';
import * as yaml from 'yaml';
import { XMLBuilder } from 'fast-xml-parser';
import type { ExternalServiceRegistration } from '@jsforce/jsforce-node/lib/api/metadata/schema';
import { WriteInfo } from '../types';
import { SourceComponent } from '../../resolve';
import { DEFAULT_PACKAGE_ROOT_SFDX, META_XML_SUFFIX, XML_DECL, XML_NS_KEY } from '../../common';
import { BaseMetadataTransformer } from './baseMetadataTransformer';

type SchemaType = 'json' | 'yaml';

type ESR = {
ExternalServiceRegistration: ExternalServiceRegistration & { schemaUploadFileExtension: SchemaType };
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Any way this can be a stronger type? JsonMap is pretty generic.


const xmlDeclaration = '<?xml version="1.0" encoding="UTF-8"?>\n';

export class DecomposeExternalServiceRegistrationTransformer extends BaseMetadataTransformer {
public async toSourceFormat(input: {
component: SourceComponent;
mergeWith?: SourceComponent | undefined;
}): Promise<WriteInfo[]> {
this.context.decomposedExternalServiceRegistration.externalServiceRegistration ??=
this.registry.getTypeByName('ExternalServiceRegistration');
const writeInfos: WriteInfo[] = [];
const { component } = input;
const outputDir = path.join(
this.getOutputFolder('source', component),
this.context.decomposedExternalServiceRegistration.externalServiceRegistration.directoryName
);
const xmlContent = { ...(await component.parseXml<ESR>()).ExternalServiceRegistration };

// Extract schema content
const schemaContent: string = xmlContent.schema ?? '';
const schemaType = xmlContent.schemaUploadFileExtension ?? this.getSchemaType(schemaContent);
const asYaml = schemaType === 'yaml' ? schemaContent : yaml.stringify(JSON.parse(schemaContent));
const schemaFileName = `${component.fullName}.yaml`;
const schemaFilePath = path.join(path.dirname(outputDir), schemaFileName);

// make sure the schema type is set
xmlContent.schemaUploadFileExtension = schemaType;

// Write schema content to file
writeInfos.push({
source: Readable.from(asYaml),
output: schemaFilePath,
});

// Remove schema content from ESR content
delete xmlContent.schema;

// Write remaining ESR content to file
const esrFileName = `${component.fullName}.externalServiceRegistration`;
const esrFilePath = path.join(path.dirname(outputDir), `${esrFileName}${META_XML_SUFFIX}`);
const xmlBuilder = new XMLBuilder({
format: true,
ignoreAttributes: false,
suppressUnpairedNode: true,
processEntities: true,
indentBy: ' ',
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const source = xmlBuilder.build({ ExternalServiceRegistration: xmlContent });
writeInfos.push({
source: Readable.from(Buffer.from(xmlDeclaration + source)),
output: esrFilePath,
});

return writeInfos;
}

public async toMetadataFormat(component: SourceComponent): Promise<WriteInfo[]> {
// only need to do this once
this.context.decomposedExternalServiceRegistration.externalServiceRegistration ??=
this.registry.getTypeByName('ExternalServiceRegistration');
const esrFilePath = component.xml;
const esrContent = { ...(await component.parseXml<ESR>()).ExternalServiceRegistration };

// Read schema content from file
const schemaFileName = `${component.fullName}.yaml`; // or .json based on your logic
const schemaFilePath = path.join(path.dirname(esrFilePath ?? ''), schemaFileName);
// load the schema content from the file
const schemaContent = (await component.tree.readFile(schemaFilePath)).toString();
// Add schema content back to ESR content in its original format
// if the original format was JSON, then convert the yaml to json otherwise leave as is
esrContent.schema =
esrContent.schemaUploadFileExtension === 'json'
? JSON.stringify(yaml.parse(schemaContent), undefined, 2)
: schemaContent;

// Write combined content back to md format
this.context.decomposedExternalServiceRegistration.transactionState.esrRecords.set(component.fullName, {
// @ts-expect-error Object literal may only specify known properties
[XML_NS_KEY]: XML_DECL,
...esrContent,
});

return [];
}

// eslint-disable-next-line class-methods-use-this
private getOutputFolder(format: string, component: SourceComponent, mergeWith?: SourceComponent): string {
const base = format === 'source' ? DEFAULT_PACKAGE_ROOT_SFDX : '';
const { type } = mergeWith ?? component;
return path.join(base, type.directoryName);
}

// eslint-disable-next-line class-methods-use-this
private getSchemaType(content: string): SchemaType {
return content.trim().startsWith('{') ? 'json' : 'yaml';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class DecomposedPermissionSetTransformer extends BaseMetadataTransformer
}

/**
* will decomopse a .permissionset into a directory containing files, and an 'objectSettings' folder for object-specific settings
* will decompose a .permissionset into a directory containing files, and an 'objectSettings' folder for object-specific settings
*
* @param {SourceComponent} component A SourceComponent representing a metadata-formatted permission set
* @param {SourceComponent | undefined} mergeWith any existing source-formatted permission sets to be merged with, think existing source merging with new information from a retrieve
Expand Down
12 changes: 6 additions & 6 deletions src/convert/transformers/metadataTransformerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,22 @@
*/
import { Messages } from '@salesforce/core';
import { MetadataTransformer } from '../types';
import { SourceComponent } from '../../resolve/sourceComponent';
import { SourceComponent } from '../../resolve';
import { ConvertContext } from '../convertContext/convertContext';
import { RegistryAccess } from '../../registry/registryAccess';
import { RegistryAccess } from '../../registry';
import { DefaultMetadataTransformer } from './defaultMetadataTransformer';
import { DecomposedMetadataTransformer } from './decomposedMetadataTransformer';
import { StaticResourceMetadataTransformer } from './staticResourceMetadataTransformer';
import { NonDecomposedMetadataTransformer } from './nonDecomposedMetadataTransformer';
import { LabelMetadataTransformer, LabelsMetadataTransformer } from './decomposeLabelsTransformer';
import { DecomposedPermissionSetTransformer } from './decomposedPermissionSetTransformer';
import { DecomposeExternalServiceRegistrationTransformer } from './decomposeExternalServiceRegistrationTransformer';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');

export class MetadataTransformerFactory {
private registry: RegistryAccess;
private context: ConvertContext;

public constructor(registry: RegistryAccess, context = new ConvertContext()) {
public constructor(private readonly registry: RegistryAccess, private readonly context = new ConvertContext()) {
this.registry = registry;
this.context = context;
}
Expand All @@ -48,6 +46,8 @@ export class MetadataTransformerFactory {
return component.type.name === 'CustomLabels'
? new LabelsMetadataTransformer(this.registry, this.context)
: new LabelMetadataTransformer(this.registry, this.context);
case 'decomposeExternalServiceRegistration':
return new DecomposeExternalServiceRegistrationTransformer(this.registry, this.context);
default:
throw messages.createError('error_missing_transformer', [type.name, transformerId]);
}
Expand Down
43 changes: 43 additions & 0 deletions src/registry/presets/decomposeExternalServiceRegistrationBeta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"types": {
"externalserviceregistration": {
"children": {
"types": {
"yaml": {
"strategies": {
"adapter": "partiallyDecomposed"
},
"directoryName": "externalServiceRegistrations",
"id": "yaml",
"isAddressable": false,
"name": "OAS Yaml Schema",
"suffix": "yaml",
"xmlElementName": "schema"
}
},
"suffixes": {
"yaml": "yaml"
}
},
"directoryName": "externalServiceRegistrations",
"id": "externalserviceregistration",
"ignoreParsedFullName": false,
"name": "ExternalServiceRegistration",
"strategies": {
"adapter": "partiallyDecomposed",
"decomposition": "topLevel",
"transformer": "decomposeExternalServiceRegistration"
},
"suffix": "externalServiceRegistration",
"supportsPartialDelete": false
}
},
"suffixes": {
"yaml": "yaml",
"externalServiceRegistration": "externalserviceregistration"
},
"strictDirectoryNames": {},
"childTypes": {
"yaml": "externalserviceregistration"
}
}
2 changes: 2 additions & 0 deletions src/registry/presets/presetMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as decomposePermissionSetBeta from './decomposePermissionSetBeta.json';
import * as decomposePermissionSetBeta2 from './decomposePermissionSetBeta2.json';
import * as decomposeSharingRulesBeta from './decomposeSharingRulesBeta.json';
import * as decomposeWorkflowBeta from './decomposeWorkflowBeta.json';
import * as decomposeExternalServiceRegistrationBeta from './decomposeExternalServiceRegistrationBeta.json';

export const presetMap = new Map<string, MetadataRegistry>([
['decomposeCustomLabelsBeta2', decomposeCustomLabelsBeta2 as MetadataRegistry],
Expand All @@ -22,4 +23,5 @@ export const presetMap = new Map<string, MetadataRegistry>([
['decomposePermissionSetBeta2', decomposePermissionSetBeta2 as MetadataRegistry],
['decomposeSharingRulesBeta', decomposeSharingRulesBeta as MetadataRegistry],
['decomposeWorkflowBeta', decomposeWorkflowBeta as MetadataRegistry],
['decomposeExternalServiceRegistrationBeta', decomposeExternalServiceRegistrationBeta as MetadataRegistry],
]);
12 changes: 10 additions & 2 deletions src/registry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,22 @@ export type MetadataType = {
* Configuration for resolving and converting components of the type.
*/
strategies?: {
adapter: 'mixedContent' | 'matchingContentFile' | 'decomposed' | 'digitalExperience' | 'bundle' | 'default';
adapter:
| 'mixedContent'
| 'matchingContentFile'
| 'decomposed'
| 'digitalExperience'
| 'bundle'
| 'default'
| 'partiallyDecomposed';
transformer?:
| 'decomposed'
| 'staticResource'
| 'nonDecomposed'
| 'standard'
| 'decomposedLabels'
| 'decomposedPermissionSet';
| 'decomposedPermissionSet'
| 'decomposeExternalServiceRegistration';
decomposition?: 'topLevel' | 'folderPerType';
recomposition?: 'startEmpty';
};
Expand Down
Loading
Loading