Skip to content

Commit 1a63abb

Browse files
committed
fix: ensure xsi:type is set for all metadata operations
1 parent ac99a71 commit 1a63abb

File tree

7 files changed

+118
-13
lines changed

7 files changed

+118
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { wait } from '@vlocode/util';
2+
import 'jest';
3+
import { HttpRequestInfo, MetadataApi, SalesforceConnection } from '../connection';
4+
import { SoapClient } from '../soapClient';
5+
6+
function mockConnection() {
7+
const baseUrl = 'https://test.salesforce.com';
8+
const apiRequests = new Array<HttpRequestInfo>();
9+
const apiResponses: Record<string, any> = { }
10+
async function request<T>(info: HttpRequestInfo): Promise<T> {
11+
await wait(5);
12+
apiRequests.push({...info});
13+
return apiResponses[info.url];
14+
}
15+
const mock = {
16+
apiResponses,
17+
apiRequests,
18+
request,
19+
_baseUrl() { return baseUrl },
20+
instanceUrl: baseUrl,
21+
};
22+
return mock as any as (SalesforceConnection & typeof mock);
23+
}
24+
25+
function mockSoapClient() {
26+
const soapRequests = new Array<{method: string, request: any}>();
27+
const soapResponses: Record<string, { body: object }> = { }
28+
async function request<T>(method: string, request: object, options?: any): Promise<T> {
29+
await wait(5);
30+
soapRequests.push({ method, request });
31+
return (soapResponses[method] ?? ({ body: {} })) as any as T;
32+
}
33+
const mock = {
34+
soapRequests,
35+
soapResponses,
36+
request
37+
};
38+
return mock as any as (SoapClient & typeof mock);
39+
}
40+
41+
describe('MetadataApi', () => {
42+
describe('#createMetadata', () => {
43+
it('should include type attribute in create calls', async () => {
44+
// Arrange
45+
const connection = mockConnection();
46+
const soap = mockSoapClient();
47+
const sut = new MetadataApi(connection);
48+
sut['soap'] = soap;
49+
50+
// Act
51+
await sut.create('CustomMetadata', {
52+
label: 'test',
53+
fullName: 'test',
54+
values: [
55+
{ field: 'Test', value: 1 }
56+
]
57+
});
58+
59+
// Assert
60+
expect(soap.soapRequests[0].method).toBe('createMetadata');
61+
expect(soap.soapRequests[0].request.type).toBe('CustomMetadata');
62+
expect(soap.soapRequests[0].request.metadata.length).toBe(1);
63+
expect(soap.soapRequests[0].request.metadata[0].$['xsi:type']).toBe('CustomMetadata');
64+
expect(soap.soapRequests[0].request.metadata[0].label).toBe('test');
65+
expect(soap.soapRequests[0].request.metadata[0].fullName).toBe('test');
66+
});
67+
});
68+
});

packages/salesforce/src/__tests__/queryParser.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import 'jest';
22

33
import { QueryFormatter, QueryParser } from '../queryParser';
44

5-
describe('QueryParser2', () => {
5+
describe('QueryParser', () => {
66

77
describe('#parseQueryCondition', () => {
88
it('should parse single condition as string', () => {

packages/salesforce/src/connection/metadata/metadataApi.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class MetadataApi implements DeploymentApi {
6767
}
6868

6969
private normalizeMetadata(type: string, metadata: SalesforceMetadata | SalesforceMetadata[]): SalesforceMetadata[] {
70-
return asArray(metadata).map(md => setObjectProperty(md, '$.xsi:type', type));
70+
return asArray(metadata).map(md => setObjectProperty(md, '$.xsi:type', type, { create: true }));
7171
}
7272

7373
/**

packages/util/src/__tests__/object.test.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,20 @@ describe('util', () => {
4545
it('should set property at path', () => {
4646
const obj = { foo: { bar: 'test' } };
4747
const result = setObjectProperty(obj, 'foo.bar', 'set');
48-
expect(result).toEqual({ foo: { bar: 'set' } });
48+
expect(result).toStrictEqual({ foo: { bar: 'set' } });
49+
expect(obj).toStrictEqual({ foo: { bar: 'set' } });
4950
});
5051
it('should not set property at path when parent undefined', () => {
5152
const obj = { foo: { bar: 'test' } };
5253
const result: any = setObjectProperty(obj, 'foo.bar.foo.bar', 'set');
53-
expect(result).toEqual({ foo: { bar: 'test' } });
54+
expect(result).toStrictEqual({ foo: { bar: 'test' } });
55+
expect(obj).toStrictEqual({ foo: { bar: 'test' } });
5456
});
55-
it('should set property at path when parent undefined and createWhenNotFound is true', () => {
57+
it('should set property at path when parent undefined and create is true', () => {
5658
const obj = { foo: undefined };
57-
const result = setObjectProperty(obj, 'foo.bar.baz.bar', 'set', { createWhenNotFound: true });
58-
expect(result).toEqual({ foo: { bar: { baz: { bar: 'set' } } } });
59+
const result = setObjectProperty(obj, 'foo.bar.baz.bar', 'set', { create: true });
60+
expect(result).toStrictEqual({ foo: { bar: { baz: { bar: 'set' } } } });
61+
expect(obj).toStrictEqual({ foo: { bar: { baz: { bar: 'set' } } } });
5962
});
6063
});
6164
describe('#getObjectProperty', () => {

packages/util/src/__tests__/string.test.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ describe('util', () => {
1212
// eslint-disable-next-line no-template-curly-in-string
1313
expect(string.formatString('Foo ${bar} foo', { foo: 'foo'})).toEqual('Foo ${bar} foo');
1414
});
15-
it('should replace values not found in context object', () => {
16-
expect(string.formatString('Foo {bar}', { bar: 'foo'})).toEqual('Foo foo');
15+
it('should replace values found in context object', () => {
16+
expect(string.formatString('Foo {bar} test', { bar: 'foo'})).toEqual('Foo foo test');
1717
});
1818
it('should replace values found in context array', () => {
1919
expect(string.formatString('Foo {1}', ['foo', 'bar'])).toEqual('Foo bar');
@@ -24,6 +24,11 @@ describe('util', () => {
2424
it('should not replace values in nested path not found in context object', () => {
2525
expect(string.formatString('Foo {bar.foo}',{ bar: { value: 'bar' }})).toEqual('Foo {bar.foo}');
2626
});
27+
it('should replace values found in context object', () => {
28+
const value = `/services/data/v{apiVersion}/query?q=select%20NamespacePrefix%20from%20ApexClass%20where%20name%20%3D%20'DRDataPackService'%20limit%201`;
29+
const excpected = `/services/data/v47.0/query?q=select%20NamespacePrefix%20from%20ApexClass%20where%20name%20%3D%20'DRDataPackService'%20limit%201`;
30+
expect(string.formatString(value, { apiVersion: '47.0'})).toEqual(excpected);
31+
});
2732
});
2833

2934
describe('#evalExpr', () => {

packages/util/src/__tests__/xml.test.ts

+29
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,35 @@
11
import { XML } from "../xml";
22

33
describe('xml', () => {
4+
describe('#stringify', () => {
5+
it('should write attributes when set', () => {
6+
const xmlStr = '<tag attr="test"><value>foo</value></tag>';
7+
expect(XML.stringify({
8+
tag: {
9+
$: { attr: 'test' },
10+
value: 'foo'
11+
}
12+
}, undefined, { headless: true })).toBe(xmlStr);
13+
});
14+
it('should include namespace prefixes for attributes and tags', () => {
15+
const xmlStr = '<tag xsi:attr="test"><ns:value>foo</ns:value></tag>';
16+
expect(XML.stringify({
17+
tag: {
18+
$: { 'xsi:attr': 'test' },
19+
['ns:value']: 'foo'
20+
}
21+
}, undefined, { headless: true })).toBe(xmlStr);
22+
});
23+
it('should write tag body with attributes', () => {
24+
const xmlStr = '<tag attr="test">foo</tag>';
25+
expect(XML.stringify({
26+
tag: {
27+
$: { attr: 'test' },
28+
'#text': 'foo'
29+
}
30+
}, undefined, { headless: true })).toBe(xmlStr);
31+
});
32+
});
433
describe('#parse', () => {
534
it('should replace tag-value with attribute nil:true by null', () => {
635
const xmlStr = `<test><tag nil='true'></tag></test>`;

packages/util/src/object.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,11 @@ export function getObjectProperty(obj: any, prop: string) {
236236
* @param obj Object to set the property on
237237
* @param prop Property path to set
238238
* @param value Value to set at the specified path
239-
* @param options.createWhenNotFound Create an object to be able to set the specified property, otherwise does not set the property specified
239+
* @param options.create Create an object to be able to set the specified property, otherwise does not set the property specified
240240
* @returns The original obj with the property path set to the specified value;
241241
*/
242-
export function setObjectProperty<T extends object>(obj: T, prop: string, value: any, options?: { createWhenNotFound?: boolean }) : T {
243-
if (options?.createWhenNotFound) {
242+
export function setObjectProperty<T extends object>(obj: T, prop: string, value: any, options?: { create?: boolean }) : T {
243+
if (options?.create) {
244244
obj = obj ?? {} as T; // init object with a default when not set
245245
}
246246

@@ -250,7 +250,7 @@ export function setObjectProperty<T extends object>(obj: T, prop: string, value:
250250
let target = obj;
251251
for (const p of propPath) {
252252
if (target[p] === undefined || target === null) {
253-
if (!options?.createWhenNotFound) {
253+
if (!options?.create) {
254254
return obj;
255255
}
256256
}

0 commit comments

Comments
 (0)