Skip to content

Commit 7c1d664

Browse files
adding support for select and filter in functions #168
1 parent 618280f commit 7c1d664

File tree

6 files changed

+102
-67
lines changed

6 files changed

+102
-67
lines changed

.github/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ expand | `Expand[]` | `retrieve`, `retrieveMultiple`, `create`, `update`, `upser
353353
fetchXml | `string` | `fetch`, `fetchAll` | Property that sets FetchXML - a proprietary query language that provides capabilities to perform aggregation.
354354
fieldName | `string` | `uploadFile`, `downloadFile`, `deleteRequest` | **D365 Web API v9.1+** Use this option to specify the name of the file attribute in Dynamics 365. [More Info](https://docs.microsoft.com/en-us/powerapps/developer/common-data-service/file-attributes)
355355
fileName | `string` | `uploadFile` | **D365 Web API v9.1+** Specifies the name of the file
356-
filter | String | `retrieve`, `retrieveMultiple`, `retrieveAll` | Use the $filter system query option to set criteria for which entities will be returned.
356+
filter | String | `retrieve`, `retrieveMultiple`, `retrieveAll`, `callFunction` | Use the $filter system query option to set criteria for which entities will be returned.
357357
functionName | `string` | `callFunction` | Name of a D365 Web Api function.
358358
headers | `Object` | All | `v2.1+` Custom headers to supply with a request. These headers will override configuraiton headers if the identical ones were set. For example: `{ "my-header": "value", "another-header": "another-value" }`.
359359
ifmatch | `string` | `retrieve`, `update`, `upsert`, `deleteRecord` | Sets If-Match header value that enables to use conditional retrieval or optimistic concurrency in applicable requests. [More Info](https://msdn.microsoft.com/en-us/library/mt607711.aspx)
@@ -377,7 +377,7 @@ partitionId | `string` | `create`, `update`, `upsert`, `delete`, `retrieve`, `re
377377
queryParams | `string[]` | All | Custom query parameters. Can also be used to set the [parameter aliases](https://docs.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query-data-web-api#use-parameter-aliases-with-system-query-options) for "$filter" and "$orderBy". **Important!** These parameters ARE NOT URI encoded!
378378
returnRepresentation | `boolean` | `create`, `update`, `upsert` | Sets Prefer header request with value "return=representation". Use this property to return just created or updated entity in a single request.
379379
savedQuery | `string` | `retrieve` | A String representing the GUID value of the saved query.
380-
select | `string[]` | `retrieve`, `retrieveMultiple`, `retrieveAll`, `update`, `upsert` | An array (of Strings) representing the $select OData System Query Option to control which attributes will be returned.
380+
select | `string[]` | `retrieve`, `retrieveMultiple`, `retrieveAll`, `update`, `upsert`, `callFunction` | An array (of Strings) representing the $select OData System Query Option to control which attributes will be returned.
381381
signal | `AbortSignal` | All | Specifies an `AbortSignal` that can be used to abort a request if required via an `AbortController` object. [More Info](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
382382
timeout | `number` | All | Sets a number of milliseconds before a request times out.
383383
token | `string` | All | Authorization Token. If set, onTokenRefresh will not be called.

src/dynamics-web-api.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,8 +610,11 @@ export class DynamicsWebApi {
610610

611611
ErrorHelper.stringParameterCheck(internalRequest.functionName, `DynamicsWebApi.callFunction`, parameterName);
612612

613+
const functionParameters = Utility.buildFunctionParameters(internalRequest.parameters);
614+
613615
internalRequest.method = "GET";
614-
internalRequest._additionalUrl = internalRequest.functionName + Utility.buildFunctionParameters(internalRequest.parameters);
616+
internalRequest._additionalUrl = internalRequest.functionName + functionParameters.key;
617+
internalRequest.queryParams = functionParameters.queryParams;
615618
internalRequest._isUnboundRequest = !internalRequest.collection;
616619
internalRequest.functionName = "callFunction";
617620

@@ -1430,6 +1433,10 @@ export interface UnboundFunctionRequest extends BaseRequest {
14301433
functionName: string;
14311434
/**Function's input parameters. Example: { param1: "test", param2: 3 }. */
14321435
parameters?: any;
1436+
/**An Array(of Strings) representing the $select OData System Query Option to control which attributes will be returned. */
1437+
select?: string[];
1438+
/**Use the $filter system query option to set criteria for which entities will be returned. */
1439+
filter?: string;
14331440
}
14341441

14351442
export interface BoundFunctionRequest extends UnboundFunctionRequest, Request {

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ export declare namespace Core {
166166
headers: any;
167167
async?: boolean;
168168
}
169+
170+
type FunctionParameters = {
171+
key: string,
172+
queryParams?: string[]
173+
}
169174
}
170175

171176
declare global {

src/utils/Utility.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,38 +23,36 @@ export class Utility {
2323
* @param {Object} [parameters] - Function's input parameters. Example: { param1: "test", param2: 3 }.
2424
* @returns {string}
2525
*/
26-
static buildFunctionParameters(parameters?: any): string {
26+
static buildFunctionParameters(parameters?: any): Core.FunctionParameters {
2727
if (parameters) {
2828
const parameterNames = Object.keys(parameters);
29-
let functionParameters = "";
30-
let urlQuery = "";
29+
const functionParams: string[] = [];
30+
const urlQuery: string[] = [];
3131

32-
for (var i = 1; i <= parameterNames.length; i++) {
32+
for (let i = 1; i <= parameterNames.length; i++) {
3333
const parameterName = parameterNames[i - 1];
3434
let value = parameters[parameterName];
3535

3636
if (value == null) continue;
3737

3838
if (typeof value === "string" && !value.startsWith("Microsoft.Dynamics.CRM") && !isUuid(value)) {
39-
value = "'" + value + "'";
39+
value = `'${value}'`;
4040
} else if (typeof value === "object") {
4141
value = JSON.stringify(value);
4242
}
4343

44-
if (i > 1) {
45-
functionParameters += ",";
46-
urlQuery += "&";
47-
}
48-
49-
functionParameters += parameterName + "=@p" + i;
50-
urlQuery += "@p" + i + "=" + (extractUuid(value) || value);
44+
functionParams.push(`${parameterName}=@p${i}`);
45+
urlQuery.push(`@p${i}=${extractUuid(value) || value}`);
5146
}
5247

53-
if (urlQuery) urlQuery = "?" + urlQuery;
54-
55-
return "(" + functionParameters + ")" + urlQuery;
48+
return {
49+
key: `(${functionParams.join(",")})`,
50+
queryParams: urlQuery,
51+
};
5652
} else {
57-
return "()";
53+
return {
54+
key: "()",
55+
};
5856
}
5957
}
6058

tests/common.spec.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,34 @@ describe("Utility.", function () {
1717
describe("buildFunctionParameters - ", function () {
1818
it("no parameters", function () {
1919
const result = Utility.buildFunctionParameters();
20-
expect(result).to.equal("()");
20+
expect(result).to.deep.equal({ key: "()" });
2121
});
2222
it("1 parameter == null", function () {
2323
const result = Utility.buildFunctionParameters({ param1: null });
24-
expect(result).to.equal("()");
24+
expect(result).to.deep.equal({ key: "()", queryParams: [] });
2525
});
2626
it("1 parameter", function () {
2727
const result = Utility.buildFunctionParameters({ param1: "value1" });
28-
expect(result).to.equal("(param1=@p1)?@p1='value1'");
28+
expect(result).to.deep.equal({ key: "(param1=@p1)", queryParams: ["@p1='value1'"] });
2929
});
3030
it("2 parameters", function () {
3131
const result = Utility.buildFunctionParameters({ param1: "value1", param2: 2 });
32-
expect(result).to.equal("(param1=@p1,param2=@p2)?@p1='value1'&@p2=2");
32+
expect(result).to.deep.equal({ key: "(param1=@p1,param2=@p2)", queryParams: ["@p1='value1'", "@p2=2"] });
3333
});
3434
it("3 parameters", function () {
3535
const result = Utility.buildFunctionParameters({ param1: "value1", param2: 2, param3: "value2" });
36-
expect(result).to.equal("(param1=@p1,param2=@p2,param3=@p3)?@p1='value1'&@p2=2&@p3='value2'");
36+
expect(result).to.deep.equal({ key: "(param1=@p1,param2=@p2,param3=@p3)", queryParams: ["@p1='value1'", "@p2=2", "@p3='value2'"] });
3737
});
3838
it("object parameter", function () {
3939
const result = Utility.buildFunctionParameters({ param1: { test1: "value", "@odata.type": "account" } });
40-
expect(result).to.equal('(param1=@p1)?@p1={"test1":"value","@odata.type":"account"}');
40+
expect(result).to.deep.equal({ key: "(param1=@p1)", queryParams: ['@p1={"test1":"value","@odata.type":"account"}'] });
4141
});
4242
it("Microsoft.Dynamics.CRM namespace parameter", function () {
4343
const result = Utility.buildFunctionParameters({ param1: "Microsoft.Dynamics.CRM.Enum'Type'", param2: 2, param3: "value2" });
44-
expect(result).to.equal("(param1=@p1,param2=@p2,param3=@p3)?@p1=Microsoft.Dynamics.CRM.Enum'Type'&@p2=2&@p3='value2'");
44+
expect(result).to.deep.equal({
45+
key: "(param1=@p1,param2=@p2,param3=@p3)",
46+
queryParams: ["@p1=Microsoft.Dynamics.CRM.Enum'Type'", "@p2=2", "@p3='value2'"],
47+
});
4548
});
4649
it("Guid parameter", function () {
4750
const result = Utility.buildFunctionParameters({
@@ -50,9 +53,10 @@ describe("Utility.", function () {
5053
param3: "value2",
5154
param4: "fb15ee32-524d-41be-b6a0-7d0f28055d52",
5255
});
53-
expect(result).to.equal(
54-
"(param1=@p1,param2=@p2,param3=@p3,param4=@p4)?@p1=Microsoft.Dynamics.CRM.Enum'Type'&@p2=2&@p3='value2'&@p4=fb15ee32-524d-41be-b6a0-7d0f28055d52"
55-
);
56+
expect(result).to.deep.equal({
57+
key: "(param1=@p1,param2=@p2,param3=@p3,param4=@p4)",
58+
queryParams: ["@p1=Microsoft.Dynamics.CRM.Enum'Type'", "@p2=2", "@p3='value2'", "@p4=fb15ee32-524d-41be-b6a0-7d0f28055d52"],
59+
});
5660
});
5761
});
5862

tests/main.spec.ts

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,16 @@ describe("dynamicsWebApi.retrieveMultiple -", () => {
7575
const url = new URL(mocks.responses.multipleWithLinkAndCount().oDataNextLink);
7676
scope = nock(url.origin, {
7777
reqheaders: {
78-
prefer: "odata.maxpagesize=10"
79-
}
78+
prefer: "odata.maxpagesize=10",
79+
},
8080
})
8181
.get(url.pathname + url.search)
8282
.reply((uri, body) => {
8383
const checkUrl = new URL(uri, url.origin);
84-
if ((checkUrl.pathname + checkUrl.search) !== (url.pathname + url.search))
85-
return
86-
[
87-
mocks.responses.errorResponse.status,
88-
mocks.responses.errorResponse.responseText,
89-
mocks.responses.errorResponse.responseHeaders
90-
];
91-
92-
return [response.status, response.responseText, response.responseHeaders]
84+
if (checkUrl.pathname + checkUrl.search !== url.pathname + url.search) return;
85+
[mocks.responses.errorResponse.status, mocks.responses.errorResponse.responseText, mocks.responses.errorResponse.responseHeaders];
86+
87+
return [response.status, response.responseText, response.responseHeaders];
9388
});
9489
});
9590

@@ -102,15 +97,14 @@ describe("dynamicsWebApi.retrieveMultiple -", () => {
10297
collection: "tests",
10398
select: ["name"],
10499
count: true,
105-
maxPageSize: 10
100+
maxPageSize: 10,
106101
};
107102

108-
try{
109-
const object = await dynamicsWebApiTest.retrieveMultiple(dwaRequest, mocks.responses.multipleWithLinkAndCount().oDataNextLink)
103+
try {
104+
const object = await dynamicsWebApiTest.retrieveMultiple(dwaRequest, mocks.responses.multipleWithLinkAndCount().oDataNextLink);
110105

111106
expect(object).to.deep.equal(mocks.responses.multipleWithLinkAndCount());
112-
}
113-
catch(error){
107+
} catch (error) {
114108
console.error(error);
115109
throw error;
116110
}
@@ -128,21 +122,16 @@ describe("dynamicsWebApi.retrieveMultiple -", () => {
128122
const url = new URL(mocks.responses.multipleWithLinkAndCount().oDataNextLink);
129123
scope = nock(url.origin, {
130124
reqheaders: {
131-
prefer: "odata.maxpagesize=10"
132-
}
125+
prefer: "odata.maxpagesize=10",
126+
},
133127
})
134128
.get(url.pathname + url.search)
135129
.reply((uri, body) => {
136130
const checkUrl = new URL(uri, url.origin);
137-
if ((checkUrl.pathname + checkUrl.search) !== (url.pathname + url.search))
138-
return
139-
[
140-
mocks.responses.errorResponse.status,
141-
mocks.responses.errorResponse.responseText,
142-
mocks.responses.errorResponse.responseHeaders
143-
];
144-
145-
return [response.status, response.responseText, response.responseHeaders]
131+
if (checkUrl.pathname + checkUrl.search !== url.pathname + url.search) return;
132+
[mocks.responses.errorResponse.status, mocks.responses.errorResponse.responseText, mocks.responses.errorResponse.responseHeaders];
133+
134+
return [response.status, response.responseText, response.responseHeaders];
146135
});
147136
});
148137

@@ -155,19 +144,18 @@ describe("dynamicsWebApi.retrieveMultiple -", () => {
155144
collection: "tests",
156145
select: ["name"],
157146
count: true,
158-
maxPageSize: 10
147+
maxPageSize: 10,
159148
};
160149

161-
try{
150+
try {
162151
const dynamicsWebApiSlash = dynamicsWebApiTest.initializeInstance();
163152
dynamicsWebApiSlash.setConfig({
164-
serverUrl: mocks.serverUrl + "/"
153+
serverUrl: mocks.serverUrl + "/",
165154
});
166-
const object = await dynamicsWebApiSlash.retrieveMultiple(dwaRequest, mocks.responses.multipleWithLinkAndCount().oDataNextLink)
155+
const object = await dynamicsWebApiSlash.retrieveMultiple(dwaRequest, mocks.responses.multipleWithLinkAndCount().oDataNextLink);
167156

168157
expect(object).to.deep.equal(mocks.responses.multipleWithLinkAndCount());
169-
}
170-
catch(error){
158+
} catch (error) {
171159
console.error(error);
172160
throw error;
173161
}
@@ -383,16 +371,14 @@ describe("dynamicsWebApi.executeBatch -", () => {
383371
dynamicsWebApiTest.create({ collection: "records", data: { firstname: "Test", lastname: "Batch!" }, contentId: "1" });
384372
dynamicsWebApiTest.create({ data: { firstname: "Test1", lastname: "Batch!" }, contentId: "$1" });
385373

386-
try{
387-
const object = await dynamicsWebApiTest
388-
.executeBatch();
374+
try {
375+
const object = await dynamicsWebApiTest.executeBatch();
389376

390377
expect(object.length).to.be.eq(2);
391378

392379
expect(object[0]).to.be.eq(mocks.data.testEntityId);
393380
expect(object[1]).to.be.undefined;
394-
}
395-
catch(error) {
381+
} catch (error) {
396382
console.error(error);
397383
throw error;
398384
}
@@ -614,3 +600,38 @@ describe("dynamicsWebApi: custom headers - ", () => {
614600
});
615601
});
616602
});
603+
604+
describe("dynamicsWebApi.callFunction -", () => {
605+
describe("unbound", function () {
606+
let scope: nock.Scope;
607+
before(function () {
608+
const response = mocks.responses.response200;
609+
scope = nock(mocks.webApiUrl)
610+
.get("/FUN(param1=@p1,param2=@p2)?$select=field1,field2&$filter=field1 eq 1&@p1=%27value1%27&@p2=2")
611+
.reply(response.status, response.responseText, response.responseHeaders);
612+
});
613+
614+
after(function () {
615+
nock.cleanAll();
616+
});
617+
618+
it("(composite, with parameters) returns a correct response", async () => {
619+
try {
620+
const object = await dynamicsWebApiTest.callFunction({
621+
functionName: "FUN",
622+
parameters: { param1: "value1", param2: 2 },
623+
select: ["field1", "field2"],
624+
filter: "field1 eq 1"
625+
});
626+
627+
expect(object).to.deep.equal(mocks.data.testEntity);
628+
} catch (error) {
629+
throw error;
630+
}
631+
});
632+
633+
it("all requests have been made", function () {
634+
expect(scope.isDone()).to.be.true;
635+
});
636+
});
637+
});

0 commit comments

Comments
 (0)