Skip to content

Commit 1f7d429

Browse files
Wr/decompose esr (#1492)
* chore: reset snapshots to main * fix: deploy ESR yaml and -meta * test: fix registry test * chore: bump core * chore: always decompose to yaml The schema property contents in an esr can be either yaml of json. For simplicity, given the property alwasy represents an Open API spec, the decomposed format of schema will be yaml. Recomposition will use an existing xml property, schemaUploadFileExtension, to determine the format when build the MD type. * chore: add missing expected artifacts --------- Co-authored-by: peternhale <[email protected]>
1 parent 995d97c commit 1f7d429

File tree

27 files changed

+319
-106
lines changed

27 files changed

+319
-106
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
"node": ">=18.0.0"
2626
},
2727
"dependencies": {
28-
"@salesforce/core": "^8.8.0",
29-
"@salesforce/kit": "^3.2.2",
28+
"@salesforce/core": "^8.8.2",
29+
"@salesforce/kit": "^3.2.3",
3030
"@salesforce/ts-types": "^2.0.12",
3131
"fast-levenshtein": "^3.0.0",
3232
"fast-xml-parser": "^4.5.1",

src/convert/transformers/decomposeExternalServiceRegistrationTransformer.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
import * as fs from 'node:fs/promises';
87
import * as path from 'node:path';
98
import { Readable } from 'node:stream';
109
import * as yaml from 'yaml';
@@ -16,10 +15,15 @@ import { SourceComponent } from '../../resolve';
1615
import { DEFAULT_PACKAGE_ROOT_SFDX, META_XML_SUFFIX, XML_DECL, XML_NS_KEY } from '../../common';
1716
import { BaseMetadataTransformer } from './baseMetadataTransformer';
1817

18+
type SchemaType = 'json' | 'yaml';
19+
1920
type ESR = JsonMap & {
20-
ExternalServiceRegistration: ExternalServiceRegistration;
21+
ExternalServiceRegistration: ExternalServiceRegistration &
22+
{ schemaUploadFileExtension: SchemaType };
2123
};
2224

25+
26+
2327
const xmlDeclaration = '<?xml version="1.0" encoding="UTF-8"?>\n';
2428

2529
export class DecomposeExternalServiceRegistrationTransformer extends BaseMetadataTransformer {
@@ -40,13 +44,17 @@ export class DecomposeExternalServiceRegistrationTransformer extends BaseMetadat
4044
// Extract schema content
4145
// eslint-disable-next-line no-underscore-dangle
4246
const schemaContent: string = xmlContent.schema ?? '';
43-
const schemaExtension = this.getSchemaExtension(schemaContent);
44-
const schemaFileName = `${component.fullName}.${schemaExtension}`;
47+
const schemaType = xmlContent.schemaUploadFileExtension ?? this.getSchemaType(schemaContent);
48+
const asYaml = schemaType === 'yaml' ? schemaContent : yaml.stringify(JSON.parse(schemaContent));
49+
const schemaFileName = `${component.fullName}.yaml`;
4550
const schemaFilePath = path.join(path.dirname(outputDir), schemaFileName);
4651

52+
// make sure the schema type is set
53+
xmlContent.schemaUploadFileExtension = schemaType;
54+
4755
// Write schema content to file
4856
writeInfos.push({
49-
source: Readable.from(schemaContent),
57+
source: Readable.from(asYaml),
5058
output: schemaFilePath,
5159
});
5260

@@ -84,8 +92,11 @@ export class DecomposeExternalServiceRegistrationTransformer extends BaseMetadat
8492
// Read schema content from file
8593
const schemaFileName = `${component.fullName}.yaml`; // or .json based on your logic
8694
const schemaFilePath = path.join(path.dirname(esrFilePath ?? ''), schemaFileName);
87-
// Add schema content back to ESR content
88-
esrContent.schema = await this.readSchemaFile(schemaFilePath);
95+
// load the schema content from the file
96+
const schemaContent = (await component.tree.readFile(schemaFilePath)).toString();
97+
// Add schema content back to ESR content in its original format
98+
// if the original format was JSON, then convert the yaml to json otherwise leave as is
99+
esrContent.schema = esrContent.schemaUploadFileExtension === 'json' ? JSON.stringify(yaml.parse(schemaContent), undefined, 2) : schemaContent;
89100

90101
// Write combined content back to md format
91102
this.context.decomposedExternalServiceRegistration.transactionState.esrRecords.set(component.fullName, {
@@ -97,11 +108,6 @@ export class DecomposeExternalServiceRegistrationTransformer extends BaseMetadat
97108
return [];
98109
}
99110

100-
// eslint-disable-next-line class-methods-use-this
101-
public readSchemaFile(schemaFilePath: string): Promise<string> {
102-
return fs.readFile(schemaFilePath, 'utf8');
103-
}
104-
105111
// eslint-disable-next-line class-methods-use-this
106112
private getOutputFolder(format: string, component: SourceComponent, mergeWith?: SourceComponent): string {
107113
const base = format === 'source' ? DEFAULT_PACKAGE_ROOT_SFDX : '';
@@ -110,12 +116,7 @@ export class DecomposeExternalServiceRegistrationTransformer extends BaseMetadat
110116
}
111117

112118
// eslint-disable-next-line class-methods-use-this
113-
private getSchemaExtension(content: string): string {
114-
try {
115-
yaml.parse(content);
116-
return 'yaml';
117-
} catch {
118-
return 'json';
119-
}
119+
private getSchemaType(content: string): SchemaType {
120+
return content.trim().startsWith('{') ? 'json' : 'yaml';
120121
}
121122
}

src/registry/presets/decomposeExternalServiceRegistrationBeta.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
"children": {
55
"types": {
66
"yaml": {
7-
"directoryName": "",
7+
"strategies": {
8+
"adapter": "partiallyDecomposed"
9+
},
10+
"directoryName": "externalServiceRegistrations",
811
"id": "yaml",
912
"isAddressable": false,
1013
"name": "OAS Yaml Schema",
@@ -21,7 +24,7 @@
2124
"ignoreParsedFullName": false,
2225
"name": "ExternalServiceRegistration",
2326
"strategies": {
24-
"adapter": "decomposed",
27+
"adapter": "partiallyDecomposed",
2528
"decomposition": "topLevel",
2629
"transformer": "decomposeExternalServiceRegistration"
2730
},

src/registry/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,14 @@ export type MetadataType = {
139139
* Configuration for resolving and converting components of the type.
140140
*/
141141
strategies?: {
142-
adapter: 'mixedContent' | 'matchingContentFile' | 'decomposed' | 'digitalExperience' | 'bundle' | 'default';
142+
adapter:
143+
| 'mixedContent'
144+
| 'matchingContentFile'
145+
| 'decomposed'
146+
| 'digitalExperience'
147+
| 'bundle'
148+
| 'default'
149+
| 'partiallyDecomposed';
143150
transformer?:
144151
| 'decomposed'
145152
| 'staticResource'
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright (c) 2022, 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 { basename } from 'node:path';
8+
import { SourceComponent } from '../sourceComponent';
9+
import { extName } from '../../utils';
10+
import { DefaultSourceAdapter } from './defaultSourceAdapter';
11+
12+
/**
13+
* Handles types with partially decomposed content. This means that there will be 2+ files,
14+
* one being the parent (-meta.xml) and more being the "children" - these children make up one XML tag of the parent
15+
*
16+
* __Example Types__:
17+
*
18+
* DecomposeExternalServiceRegistrationBeta Preset
19+
*
20+
* __Example Structures__:
21+
*
22+
*```text
23+
* externalServiceRegistration/
24+
* ├── myFoo.externalServiceRegistration-meta.xml
25+
* ├── myFoo.yaml
26+
* ├── myFoo.json
27+
*```
28+
*/
29+
export class PartialDecomposedAdapter extends DefaultSourceAdapter {
30+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
31+
protected populate(trigger: string, component?: SourceComponent): SourceComponent {
32+
const parentType = this.registry.getParentType(this.type.id);
33+
34+
// no children of this type,
35+
// the parent has child types
36+
// and the trigger starts with one of the parent's child's suffixes
37+
// => we have a child path
38+
if (
39+
!this.type.children &&
40+
parentType?.children &&
41+
Object.keys(parentType.children.suffixes).find((suffix) => trigger.endsWith(`.${suffix}`))
42+
) {
43+
// we have a child, return the parent for the transformer to rebundle together
44+
return new SourceComponent(
45+
{
46+
name: getName(trigger),
47+
type: parentType,
48+
49+
// change the xml to point to the parent, the transformer will reassemble all parts to form valid MD format files
50+
xml: trigger.replace(extName(trigger), 'externalServiceRegistration-meta.xml'),
51+
},
52+
this.tree,
53+
this.forceIgnore
54+
);
55+
} else {
56+
// we were given a parent
57+
return new SourceComponent(
58+
{
59+
name: getName(trigger),
60+
type: this.type,
61+
xml: trigger,
62+
},
63+
this.tree,
64+
this.forceIgnore
65+
);
66+
}
67+
}
68+
}
69+
70+
function getName(contentPath: string): string {
71+
return basename(contentPath).split('.').at(0)!;
72+
}

src/resolve/adapters/sourceAdapterFactory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { MatchingContentSourceAdapter } from './matchingContentSourceAdapter';
1616
import { MixedContentSourceAdapter } from './mixedContentSourceAdapter';
1717
import { DefaultSourceAdapter } from './defaultSourceAdapter';
1818
import { DigitalExperienceSourceAdapter } from './digitalExperienceSourceAdapter';
19+
import { PartialDecomposedAdapter } from './partialDecomposedAdapter';
1920

2021
Messages.importMessagesDirectory(__dirname);
2122
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
@@ -42,6 +43,8 @@ export class SourceAdapterFactory {
4243
return new MixedContentSourceAdapter(type, this.registry, forceIgnore, this.tree);
4344
case 'digitalExperience':
4445
return new DigitalExperienceSourceAdapter(type, this.registry, forceIgnore, this.tree);
46+
case 'partiallyDecomposed':
47+
return new PartialDecomposedAdapter(type, this.registry, forceIgnore, this.tree);
4548
case 'default':
4649
case undefined:
4750
return new DefaultSourceAdapter(type, this.registry, forceIgnore, this.tree);

test/convert/transformers/decomposedExternalServiceRegistration.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@ describe('DecomposeExternalServiceRegistrationTransformer', () => {
5252

5353
beforeEach(() => {
5454
sandbox.stub(VirtualTreeContainer.prototype, 'readFileSync').returns(Buffer.from(SAMPLE_OAS_DOC));
55-
sandbox
56-
.stub(DecomposeExternalServiceRegistrationTransformer.prototype, 'readSchemaFile')
57-
.resolves(SAMPLE_OAS_DOC);
5855
});
5956

6057
afterEach(() => {

test/mock/type-constants/decomposeExternalServiceRegistrationConstants.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ const regAcc = new RegistryAccess(getEffectiveRegistry({ presets: [presetMap.get
1313

1414
const externalServiceRegistration = regAcc.getTypeByName('ExternalServiceRegistration');
1515

16-
const MDAPI_XML_NAME = 'myESR.externalServiceRegistration';
17-
const SOURCE_XML_NAME = 'myESR.externalServiceRegistration-meta.xml';
16+
export const MDAPI_XML = 'myESR.externalServiceRegistration';
17+
export const SOURCE_META_FILE = 'myESR.externalServiceRegistration-meta.xml';
18+
export const CHILD_YAML = 'myESR.yaml';
19+
20+
export const TYPE_DIRECTORY = 'externalServiceRegistrations';
1821

1922
export const SAMPLE_OAS_DOC = `openapi: 3.0.0
2023
info:
@@ -70,14 +73,14 @@ export const MD_FORMAT_ESR = new SourceComponent(
7073
{
7174
name: 'myESR',
7275
type: externalServiceRegistration,
73-
xml: join('externalServiceRegistrations', MDAPI_XML_NAME),
76+
xml: join('externalServiceRegistrations', MDAPI_XML),
7477
},
7578
new VirtualTreeContainer([
7679
{
7780
dirPath: 'externalServiceRegistrations',
7881
children: [
7982
{
80-
name: MDAPI_XML_NAME,
83+
name: MDAPI_XML,
8184
data: Buffer.from(`<?xml version="1.0" encoding="UTF-8"?>
8285
<ExternalServiceRegistration xmlns="http://soap.sforce.com/2006/04/metadata">
8386
<description>external service</description>
@@ -105,7 +108,7 @@ export const SOURCE_FORMAT_ESR = new SourceComponent(
105108
{
106109
name: 'myESR',
107110
type: externalServiceRegistration,
108-
xml: join('main', 'default', 'externalServiceRegistrations', SOURCE_XML_NAME),
111+
xml: join('main', 'default', 'externalServiceRegistrations', SOURCE_META_FILE),
109112
},
110113
new VirtualTreeContainer([
111114
{

test/registry/registryValidation.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ describe('will run preset tests', () => {
282282
'bundle',
283283
'matchingContentFile',
284284
'decomposed',
285+
'partiallyDecomposed',
285286
'digitalExperience',
286287
]).includes(type.strategies?.adapter);
287288
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2020, 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 { expect } from 'chai';
8+
import { MetadataRegistry, RegistryAccess, SourceComponent, VirtualTreeContainer } from '../../../src';
9+
import * as decomposeExternalServiceRegistrationBeta from '../../../src/registry/presets/decomposeExternalServiceRegistrationBeta.json';
10+
import { decomposeExternalServiceRegistration } from '../../mock/type-constants';
11+
import { PartialDecomposedAdapter } from '../../../src/resolve/adapters/partialDecomposedAdapter';
12+
13+
describe('PartialDecomposedAdapter', () => {
14+
const registryAccess = new RegistryAccess(decomposeExternalServiceRegistrationBeta as MetadataRegistry);
15+
const type = registryAccess.getTypeByName('externalserviceregistration');
16+
const { CHILD_YAML, TYPE_DIRECTORY, SOURCE_META_FILE } = decomposeExternalServiceRegistration;
17+
const tree = new VirtualTreeContainer([
18+
{
19+
dirPath: TYPE_DIRECTORY,
20+
children: [SOURCE_META_FILE, CHILD_YAML],
21+
},
22+
]);
23+
const expectedComponent = new SourceComponent({ name: 'myESR', xml: SOURCE_META_FILE, type }, tree);
24+
const adapter = new PartialDecomposedAdapter(type, registryAccess, undefined, tree);
25+
26+
it('Should return expected SourceComponent when given a -meta.xml path', () => {
27+
expect(adapter.getComponent(SOURCE_META_FILE)).to.deep.equal(expectedComponent);
28+
});
29+
30+
it('Should return expected SourceComponent when given a child path', () => {
31+
const ec = new SourceComponent({ name: 'myESR', type, xml: SOURCE_META_FILE }, tree);
32+
const adapter = new PartialDecomposedAdapter(type.children!.types['yaml'], registryAccess, undefined, tree);
33+
expect(adapter.getComponent(CHILD_YAML)).to.deep.equal(ec);
34+
});
35+
});

0 commit comments

Comments
 (0)