Skip to content

Commit

Permalink
adding support for select and filter in functions #168
Browse files Browse the repository at this point in the history
  • Loading branch information
AleksandrRogov committed Apr 11, 2024
1 parent 618280f commit 7c1d664
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 67 deletions.
4 changes: 2 additions & 2 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ expand | `Expand[]` | `retrieve`, `retrieveMultiple`, `create`, `update`, `upser
fetchXml | `string` | `fetch`, `fetchAll` | Property that sets FetchXML - a proprietary query language that provides capabilities to perform aggregation.
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)
fileName | `string` | `uploadFile` | **D365 Web API v9.1+** Specifies the name of the file
filter | String | `retrieve`, `retrieveMultiple`, `retrieveAll` | Use the $filter system query option to set criteria for which entities will be returned.
filter | String | `retrieve`, `retrieveMultiple`, `retrieveAll`, `callFunction` | Use the $filter system query option to set criteria for which entities will be returned.
functionName | `string` | `callFunction` | Name of a D365 Web Api function.
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" }`.
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)
Expand All @@ -377,7 +377,7 @@ partitionId | `string` | `create`, `update`, `upsert`, `delete`, `retrieve`, `re
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!
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.
savedQuery | `string` | `retrieve` | A String representing the GUID value of the saved query.
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.
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.
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)
timeout | `number` | All | Sets a number of milliseconds before a request times out.
token | `string` | All | Authorization Token. If set, onTokenRefresh will not be called.
Expand Down
9 changes: 8 additions & 1 deletion src/dynamics-web-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,8 +610,11 @@ export class DynamicsWebApi {

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

const functionParameters = Utility.buildFunctionParameters(internalRequest.parameters);

internalRequest.method = "GET";
internalRequest._additionalUrl = internalRequest.functionName + Utility.buildFunctionParameters(internalRequest.parameters);
internalRequest._additionalUrl = internalRequest.functionName + functionParameters.key;
internalRequest.queryParams = functionParameters.queryParams;
internalRequest._isUnboundRequest = !internalRequest.collection;
internalRequest.functionName = "callFunction";

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

export interface BoundFunctionRequest extends UnboundFunctionRequest, Request {
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ export declare namespace Core {
headers: any;
async?: boolean;
}

type FunctionParameters = {
key: string,
queryParams?: string[]
}
}

declare global {
Expand Down
30 changes: 14 additions & 16 deletions src/utils/Utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,38 +23,36 @@ export class Utility {
* @param {Object} [parameters] - Function's input parameters. Example: { param1: "test", param2: 3 }.
* @returns {string}
*/
static buildFunctionParameters(parameters?: any): string {
static buildFunctionParameters(parameters?: any): Core.FunctionParameters {
if (parameters) {
const parameterNames = Object.keys(parameters);
let functionParameters = "";
let urlQuery = "";
const functionParams: string[] = [];
const urlQuery: string[] = [];

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

if (value == null) continue;

if (typeof value === "string" && !value.startsWith("Microsoft.Dynamics.CRM") && !isUuid(value)) {
value = "'" + value + "'";
value = `'${value}'`;
} else if (typeof value === "object") {
value = JSON.stringify(value);
}

if (i > 1) {
functionParameters += ",";
urlQuery += "&";
}

functionParameters += parameterName + "=@p" + i;
urlQuery += "@p" + i + "=" + (extractUuid(value) || value);
functionParams.push(`${parameterName}=@p${i}`);
urlQuery.push(`@p${i}=${extractUuid(value) || value}`);
}

if (urlQuery) urlQuery = "?" + urlQuery;

return "(" + functionParameters + ")" + urlQuery;
return {
key: `(${functionParams.join(",")})`,
queryParams: urlQuery,
};
} else {
return "()";
return {
key: "()",
};
}
}

Expand Down
24 changes: 14 additions & 10 deletions tests/common.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,34 @@ describe("Utility.", function () {
describe("buildFunctionParameters - ", function () {
it("no parameters", function () {
const result = Utility.buildFunctionParameters();
expect(result).to.equal("()");
expect(result).to.deep.equal({ key: "()" });
});
it("1 parameter == null", function () {
const result = Utility.buildFunctionParameters({ param1: null });
expect(result).to.equal("()");
expect(result).to.deep.equal({ key: "()", queryParams: [] });
});
it("1 parameter", function () {
const result = Utility.buildFunctionParameters({ param1: "value1" });
expect(result).to.equal("(param1=@p1)?@p1='value1'");
expect(result).to.deep.equal({ key: "(param1=@p1)", queryParams: ["@p1='value1'"] });
});
it("2 parameters", function () {
const result = Utility.buildFunctionParameters({ param1: "value1", param2: 2 });
expect(result).to.equal("(param1=@p1,param2=@p2)?@p1='value1'&@p2=2");
expect(result).to.deep.equal({ key: "(param1=@p1,param2=@p2)", queryParams: ["@p1='value1'", "@p2=2"] });
});
it("3 parameters", function () {
const result = Utility.buildFunctionParameters({ param1: "value1", param2: 2, param3: "value2" });
expect(result).to.equal("(param1=@p1,param2=@p2,param3=@p3)?@p1='value1'&@p2=2&@p3='value2'");
expect(result).to.deep.equal({ key: "(param1=@p1,param2=@p2,param3=@p3)", queryParams: ["@p1='value1'", "@p2=2", "@p3='value2'"] });
});
it("object parameter", function () {
const result = Utility.buildFunctionParameters({ param1: { test1: "value", "@odata.type": "account" } });
expect(result).to.equal('(param1=@p1)?@p1={"test1":"value","@odata.type":"account"}');
expect(result).to.deep.equal({ key: "(param1=@p1)", queryParams: ['@p1={"test1":"value","@odata.type":"account"}'] });
});
it("Microsoft.Dynamics.CRM namespace parameter", function () {
const result = Utility.buildFunctionParameters({ param1: "Microsoft.Dynamics.CRM.Enum'Type'", param2: 2, param3: "value2" });
expect(result).to.equal("(param1=@p1,param2=@p2,param3=@p3)?@p1=Microsoft.Dynamics.CRM.Enum'Type'&@p2=2&@p3='value2'");
expect(result).to.deep.equal({
key: "(param1=@p1,param2=@p2,param3=@p3)",
queryParams: ["@p1=Microsoft.Dynamics.CRM.Enum'Type'", "@p2=2", "@p3='value2'"],
});
});
it("Guid parameter", function () {
const result = Utility.buildFunctionParameters({
Expand All @@ -50,9 +53,10 @@ describe("Utility.", function () {
param3: "value2",
param4: "fb15ee32-524d-41be-b6a0-7d0f28055d52",
});
expect(result).to.equal(
"(param1=@p1,param2=@p2,param3=@p3,param4=@p4)?@p1=Microsoft.Dynamics.CRM.Enum'Type'&@p2=2&@p3='value2'&@p4=fb15ee32-524d-41be-b6a0-7d0f28055d52"
);
expect(result).to.deep.equal({
key: "(param1=@p1,param2=@p2,param3=@p3,param4=@p4)",
queryParams: ["@p1=Microsoft.Dynamics.CRM.Enum'Type'", "@p2=2", "@p3='value2'", "@p4=fb15ee32-524d-41be-b6a0-7d0f28055d52"],
});
});
});

Expand Down
97 changes: 59 additions & 38 deletions tests/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,16 @@ describe("dynamicsWebApi.retrieveMultiple -", () => {
const url = new URL(mocks.responses.multipleWithLinkAndCount().oDataNextLink);
scope = nock(url.origin, {
reqheaders: {
prefer: "odata.maxpagesize=10"
}
prefer: "odata.maxpagesize=10",
},
})
.get(url.pathname + url.search)
.reply((uri, body) => {
const checkUrl = new URL(uri, url.origin);
if ((checkUrl.pathname + checkUrl.search) !== (url.pathname + url.search))
return
[
mocks.responses.errorResponse.status,
mocks.responses.errorResponse.responseText,
mocks.responses.errorResponse.responseHeaders
];

return [response.status, response.responseText, response.responseHeaders]
if (checkUrl.pathname + checkUrl.search !== url.pathname + url.search) return;
[mocks.responses.errorResponse.status, mocks.responses.errorResponse.responseText, mocks.responses.errorResponse.responseHeaders];

return [response.status, response.responseText, response.responseHeaders];
});
});

Expand All @@ -102,15 +97,14 @@ describe("dynamicsWebApi.retrieveMultiple -", () => {
collection: "tests",
select: ["name"],
count: true,
maxPageSize: 10
maxPageSize: 10,
};

try{
const object = await dynamicsWebApiTest.retrieveMultiple(dwaRequest, mocks.responses.multipleWithLinkAndCount().oDataNextLink)
try {
const object = await dynamicsWebApiTest.retrieveMultiple(dwaRequest, mocks.responses.multipleWithLinkAndCount().oDataNextLink);

expect(object).to.deep.equal(mocks.responses.multipleWithLinkAndCount());
}
catch(error){
} catch (error) {
console.error(error);
throw error;
}
Expand All @@ -128,21 +122,16 @@ describe("dynamicsWebApi.retrieveMultiple -", () => {
const url = new URL(mocks.responses.multipleWithLinkAndCount().oDataNextLink);
scope = nock(url.origin, {
reqheaders: {
prefer: "odata.maxpagesize=10"
}
prefer: "odata.maxpagesize=10",
},
})
.get(url.pathname + url.search)
.reply((uri, body) => {
const checkUrl = new URL(uri, url.origin);
if ((checkUrl.pathname + checkUrl.search) !== (url.pathname + url.search))
return
[
mocks.responses.errorResponse.status,
mocks.responses.errorResponse.responseText,
mocks.responses.errorResponse.responseHeaders
];

return [response.status, response.responseText, response.responseHeaders]
if (checkUrl.pathname + checkUrl.search !== url.pathname + url.search) return;
[mocks.responses.errorResponse.status, mocks.responses.errorResponse.responseText, mocks.responses.errorResponse.responseHeaders];

return [response.status, response.responseText, response.responseHeaders];
});
});

Expand All @@ -155,19 +144,18 @@ describe("dynamicsWebApi.retrieveMultiple -", () => {
collection: "tests",
select: ["name"],
count: true,
maxPageSize: 10
maxPageSize: 10,
};

try{
try {
const dynamicsWebApiSlash = dynamicsWebApiTest.initializeInstance();
dynamicsWebApiSlash.setConfig({
serverUrl: mocks.serverUrl + "/"
serverUrl: mocks.serverUrl + "/",
});
const object = await dynamicsWebApiSlash.retrieveMultiple(dwaRequest, mocks.responses.multipleWithLinkAndCount().oDataNextLink)
const object = await dynamicsWebApiSlash.retrieveMultiple(dwaRequest, mocks.responses.multipleWithLinkAndCount().oDataNextLink);

expect(object).to.deep.equal(mocks.responses.multipleWithLinkAndCount());
}
catch(error){
} catch (error) {
console.error(error);
throw error;
}
Expand Down Expand Up @@ -383,16 +371,14 @@ describe("dynamicsWebApi.executeBatch -", () => {
dynamicsWebApiTest.create({ collection: "records", data: { firstname: "Test", lastname: "Batch!" }, contentId: "1" });
dynamicsWebApiTest.create({ data: { firstname: "Test1", lastname: "Batch!" }, contentId: "$1" });

try{
const object = await dynamicsWebApiTest
.executeBatch();
try {
const object = await dynamicsWebApiTest.executeBatch();

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

expect(object[0]).to.be.eq(mocks.data.testEntityId);
expect(object[1]).to.be.undefined;
}
catch(error) {
} catch (error) {
console.error(error);
throw error;
}
Expand Down Expand Up @@ -614,3 +600,38 @@ describe("dynamicsWebApi: custom headers - ", () => {
});
});
});

describe("dynamicsWebApi.callFunction -", () => {
describe("unbound", function () {
let scope: nock.Scope;
before(function () {
const response = mocks.responses.response200;
scope = nock(mocks.webApiUrl)
.get("/FUN(param1=@p1,param2=@p2)?$select=field1,field2&$filter=field1 eq 1&@p1=%27value1%27&@p2=2")
.reply(response.status, response.responseText, response.responseHeaders);
});

after(function () {
nock.cleanAll();
});

it("(composite, with parameters) returns a correct response", async () => {
try {
const object = await dynamicsWebApiTest.callFunction({
functionName: "FUN",
parameters: { param1: "value1", param2: 2 },
select: ["field1", "field2"],
filter: "field1 eq 1"
});

expect(object).to.deep.equal(mocks.data.testEntity);
} catch (error) {
throw error;
}
});

it("all requests have been made", function () {
expect(scope.isDone()).to.be.true;
});
});
});

0 comments on commit 7c1d664

Please sign in to comment.