diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 5b87da5..52ad7a9 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -2,13 +2,10 @@ version: '3' services: # Update this to the name of the service you want to work with in your docker-compose.yml file devbox: - image: node:12.16.3-alpine3.11 - ports: - - 3000:3000 + image: node:12.16.3-alpine3.11 tmpfs: - /workspace/node_modules:exec - - /workspace/examples/node_modules:exec volumes: # Update this to wherever you want VS Code to mount the folder of your project diff --git a/.eslintignore b/.eslintignore index b5c13ae..2ac5ce7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,5 @@ dist es lib rollup.config.js +jest.config.js +.eslintrc.js diff --git a/.prettierrc b/.prettierrc index 8dfe4ca..d9cf19c 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,6 @@ { "useTabs": false, - "printWidth": 120, + "printWidth": 90, "tabWidth": 2, "singleQuote": false, "trailingComma": "all", diff --git a/.vscode/launch.json b/.vscode/launch.json index 66a5ed5..831ccb9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,26 +1,13 @@ { "version": "0.2.0", "configurations": [ - { - "type": "node", - "name": "Debug Basic Example", - "request": "launch", - "program": "${workspaceRoot}/examples/node_modules/.bin/moapp", - "args": ["serve", "-c", "examples/configs/basic.json"], - "outFiles": ["${workspaceRoot}/examples/react-app/lib/**/*.js"], - "cwd": "${workspaceRoot}", - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "preLaunchTask": "build" - }, { "name": "Tests", "type": "node", "request": "launch", "program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js", "stopOnEntry": false, - "args": ["--runInBand", "--no-cache"], + "args": ["--runInBand", "--no-cache", "-t", "'Not Supported Annotations Test'"], "cwd": "${workspaceRoot}", "preLaunchTask": null, "runtimeExecutable": null, diff --git a/jest.config.json b/jest.config.json index 6aefaf2..cc5b971 100644 --- a/jest.config.json +++ b/jest.config.json @@ -4,12 +4,13 @@ "coverageReporters": ["cobertura", "lcov", "text"], "coverageThreshold": { "global": { - "branches": 80, - "functions": 80, - "lines": 80, - "statements": 80 + "branches": 90, + "functions": 90, + "lines": 90, + "statements": 90 } }, + "moduleDirectories": ["node_modules", "src"], "moduleFileExtensions": ["ts", "js"], "testRegex": "/test/.*\\.spec\\.(ts|js)$", "testResultsProcessor": "jest-sonar-reporter", diff --git a/package.json b/package.json index 6ff3186..72159d6 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "rollup-plugin-terser": "^6.1.0", "rollup-plugin-typescript2": "^0.27.1", "ts-jest": "^26.1.0", + "ts-transformer-keys": "^0.4.1", "typescript": "^3.9.5" }, "dependencies": { diff --git a/rollup.config.js b/rollup.config.js index c447158..2b6c771 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -9,6 +9,7 @@ import pkg from "./package.json"; const extensions = [".ts"]; const noDeclarationFiles = { compilerOptions: { declaration: false } }; +const tsconfig = "tsconfig.build.json"; const babelRuntimeVersion = pkg.dependencies["@babel/runtime"].replace(/^[^0-9]*/, ""); @@ -33,7 +34,7 @@ export default [ nodeResolve({ extensions, }), - typescript({ useTsconfigDeclarationDir: true }), + typescript({ useTsconfigDeclarationDir: true, tsconfig }), babel({ extensions, plugins: [["@babel/plugin-transform-runtime", { version: babelRuntimeVersion }]], @@ -57,6 +58,7 @@ export default [ }), typescript({ tsconfigOverride: { + tsconfig, compilerOptions: { declaration: false, module: "es2015", @@ -84,7 +86,7 @@ export default [ replace({ "process.env.NODE_ENV": JSON.stringify("production"), }), - typescript({ tsconfigOverride: noDeclarationFiles }), + typescript({ tsconfigOverride: noDeclarationFiles, tsconfig }), babel({ extensions, exclude: "node_modules/**", @@ -114,7 +116,7 @@ export default [ nodeResolve({ extensions, }), - typescript({ tsconfigOverride: noDeclarationFiles }), + typescript({ tsconfigOverride: noDeclarationFiles, tsconfig }), babel({ extensions, exclude: "node_modules/**", @@ -139,7 +141,7 @@ export default [ nodeResolve({ extensions, }), - typescript({ tsconfigOverride: noDeclarationFiles }), + typescript({ tsconfigOverride: noDeclarationFiles, tsconfig }), babel({ extensions, exclude: "node_modules/**", diff --git a/src/odata-annotations.ts b/src/odata-annotations.ts new file mode 100644 index 0000000..d2cbeba --- /dev/null +++ b/src/odata-annotations.ts @@ -0,0 +1,60 @@ +import { ODataAnnotations } from "./odata"; + +/* + * http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Instance_Annotations + * Annotations are name/value pairs that have a dot (.) as part of the name. + * $odata.context - system annotations (supported) + * @com.contoso - custom annotation + * com.contoso - also valid custom annotation + * Orders@odata.context - scoped system annotations + * Orders@com.contoso - scoped custom annotations + */ +export function getAnnotations( + obj: K, +): ODataAnnotations { + const annotations: ODataAnnotations = {}; + const receivedKeys = Object.getOwnPropertyNames(obj); + const defaultNamespaces = ["$odata", "odata"]; + const namespaceDelimiter = "."; + const propertyDelimiter = "@"; + const getSubContainer = (subKey: string) => { + if (!annotations[subKey]) annotations[subKey] = {}; + return annotations[subKey] as ODataAnnotations; + }; + for (const key of receivedKeys) { + const parts = key.split(namespaceDelimiter); + if (parts.length <= 1) + // not an annotation. Should contain '.' (dot) + continue; + + // at this point it is defintely annotation so delete it from original object + const value = obj[key]; + delete obj[key]; + + const prefix = parts[0]; // get left part and split it + const prefixParts = prefix.split(propertyDelimiter); + + // Annotation in the form of @Orders@com.contoso is not supported + // It will be added to root annotation object as is + if (prefixParts.length > 2) { + annotations[key] = value; + continue; + } + + // at this point prefixParts.length is 1 or 2 + const namespace = prefixParts[prefixParts.length - 1]; + const container = + prefixParts.length === 1 ? annotations : getSubContainer(prefixParts[0]); + + // multiple dots considered custom annotation and will be added as is preserving + // the namespace + let annotationKey = parts.length == 2 ? parts[1] : parts.slice(1).join("."); + if (defaultNamespaces.indexOf(namespace) === -1) + annotationKey = `${namespace}.${annotationKey}`; + + // set annotation + container[annotationKey] = value; + } + + return annotations; +} diff --git a/src/odata-client.ts b/src/odata-client.ts index c006944..ee78aca 100644 --- a/src/odata-client.ts +++ b/src/odata-client.ts @@ -1,5 +1,14 @@ -import { Entity, ODataOperations, ODataConfig, ODataResponse, Predicate, CountPredicate } from "odata"; +import { + Entity, + ODataOperations, + ODataConfig, + Predicate, + CountPredicate, + Annotated, + ODataError, +} from "odata"; import { ODataQueryWrapper } from "./odata-query"; +import { getAnnotations } from "./odata-annotations"; export class ODataClient implements ODataOperations { private url: string; @@ -8,20 +17,21 @@ export class ODataClient implements ODataOperations { this.url = `${this.config.baseUrl}/${this.resource}`; } - public add(entity: T): Promise> { + public add(entity: T): Promise> { const options: RequestInit = this.prepareEntityRequest("POST", entity); - return this.fetch(this.url, options); + return this.fetchAnnotated(this.url, options); } - public update(entity: T): Promise> { + public update(entity: T): Promise> { const options: RequestInit = this.prepareEntityRequest("PUT", entity); - return this.fetch(this.url, options); + return this.fetchAnnotated(this.url, options); } public async patch(entityId: string, entity: Partial): Promise { const url = `${this.url}/${entityId}`; const options: RequestInit = this.prepareEntityRequest("PATCH", entity); - await this.config.http.fetch(url, options); + const response = await this.config.http.fetch(url, options); + this.processResponse(response); } public async delete(entityId: string): Promise { @@ -34,13 +44,13 @@ export class ODataClient implements ODataOperations { await this.config.http.fetch(url, options); } - public get(entityId: string): Promise> { + public get(entityId: string): Promise> { const url = `${this.url}/${entityId}`; const options: RequestInit = this.prepareEntityRequest("GET"); - return this.fetch(url, options); + return this.fetchAnnotated(url, options); } - query(predicate?: Predicate | void): Promise> { + query(predicate?: Predicate | void): Promise> { let url = this.url; if (predicate) { const query = new ODataQueryWrapper(); @@ -48,8 +58,9 @@ export class ODataClient implements ODataOperations { url += query.get(); } const options = this.prepareEntityRequest("GET"); - return this.fetch(url, options); + return this.fetchAnnotated(url, options, "value"); } + public async count(predicate?: CountPredicate | void): Promise { let url = `${this.url}/$count`; if (predicate) { @@ -62,17 +73,25 @@ export class ODataClient implements ODataOperations { return parseInt(await response.text()); } - private async parse(response: Response): Promise> { - const text = await response.text(); - const parsed = JSON.parse(text, this.config.jsonParseReviver); + private async processResponse(response: Response): Promise { + if (response.ok) { + return response.status !== 204 ? await response.text() : ""; + } - return { - value: parsed.value, - annotations: {}, - }; + const content = await response.text(); + + if (content) { + const error = JSON.parse(content); + throw new ODataError(error); + } + + throw new Error(response.statusText); } - private prepareEntityRequest(method: string, entity: TEntity | undefined = undefined): RequestInit { + private prepareEntityRequest( + method: string, + entity: TEntity | undefined = undefined, + ): RequestInit { const body = entity ? JSON.stringify(entity) : undefined; const options: RequestInit = { body, @@ -84,8 +103,17 @@ export class ODataClient implements ODataOperations { return options; } - private async fetch(url: string, options: RequestInit): Promise> { + private async fetchAnnotated( + url: string, + options: RequestInit, + fragment: string | undefined = undefined, + ): Promise> { const response = await this.config.http.fetch(url, options); - return await this.parse(response); + const text = await this.processResponse(response); + const parsed = JSON.parse(text, this.config.jsonParseReviver); + + const result: Annotated = fragment ? parsed[fragment] : parsed; + result.$odata = getAnnotations(parsed); + return result; } } diff --git a/src/odata-context.ts b/src/odata-context.ts index f0fef28..a9433cd 100644 --- a/src/odata-context.ts +++ b/src/odata-context.ts @@ -6,8 +6,10 @@ export function oData(config: Partial): DataContext { baseUrl: config.baseUrl ?? "", http: config.http ? config.http : window, jsonParseReviver: config.jsonParseReviver, - defaultContentType: config.defaultContentType ?? "application/json; odata.metadata=minimal", + defaultContentType: + config.defaultContentType ?? "application/json; odata.metadata=minimal", }; - return (resource: string) => new ODataClient(odataConfig, resource); + return (resource: string) => + new ODataClient(odataConfig, resource); } diff --git a/src/odata-query.ts b/src/odata-query.ts index 92ee4d4..002e74f 100644 --- a/src/odata-query.ts +++ b/src/odata-query.ts @@ -55,14 +55,14 @@ export class ODataQueryWrapper implements ODataQuery, ODataCountQuery { } public get(): string { - const params = new URLSearchParams("?"); + const paramsDict: string[] = []; for (const key in this.query) { if (Object.prototype.hasOwnProperty.call(this.query, key)) { - const value = this.query[key]?.toString() ?? ""; - if (key) params.append(key, value); + const value = encodeURIComponent(this.query[key]?.toString() ?? ""); + if (key) paramsDict.push(`${key}=${value}`); } } - const str = params.toString().replace("%24", "$"); + const str = paramsDict.join("&"); return str ? "?" + str : ""; } } diff --git a/src/odata.ts b/src/odata.ts index d9261fe..a69341b 100644 --- a/src/odata.ts +++ b/src/odata.ts @@ -18,14 +18,33 @@ export interface ODataAnnotations { mediaEditLink?: string; mediaReadLink?: string; mediaContentType?: string; - [key: string]: string | unknown; + [key: string]: string | undefined | ODataAnnotations; } -export interface ODataResponse { - value: T; - annotations: ODataAnnotations; +export interface ODataErrorResponse { + error: ODataErrorInfo; } +export interface ODataErrorInfo extends ODataErrorDetails { + code: string; + message: string; + target: string; + details: [ODataErrorDetails]; + innererror: { [key: string]: unknown }; +} + +export interface ODataErrorDetails { + code: string; + target: string; + message: string; +} + +interface Annotations { + $odata: ODataAnnotations; +} + +export type Annotated = T & Annotations; + export interface ODataConfig { baseUrl: string; http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; @@ -38,12 +57,12 @@ export interface DataContext { } export interface ODataOperations { - add: (entity: T) => Promise>; - update: (entity: T) => Promise>; + add: (entity: T) => Promise>; + update: (entity: T) => Promise>; patch(entityId: string, entity: Partial): Promise; delete: (entityId: string) => Promise; - get: (entityId: string) => Promise>; - query: (predicate?: Predicate) => Promise>; + get: (entityId: string) => Promise>; + query: (predicate?: Predicate) => Promise>; count: (predicate?: CountPredicate) => Promise; } @@ -80,12 +99,21 @@ export interface ODataQueryParams { $skip?: number; $top?: number; $count?: boolean; - $search?: string; + // $search?: string; // not supported $format?: string; - $compute?: string; - $index?: number; + // $compute?: string; // not supported + // $index?: number; // not supported [key: string]: string | number | boolean | undefined; } export type Predicate = (parameters: ODataQuery) => ODataQuery; export type CountPredicate = (parameters: ODataCountQuery) => ODataCountQuery; + +export class ODataError extends Error { + /** + * + */ + constructor(public error: ODataErrorInfo) { + super(error.message); + } +} diff --git a/test/fetch-mock.ts b/test/fetch-mock.ts index b459378..994a304 100644 --- a/test/fetch-mock.ts +++ b/test/fetch-mock.ts @@ -19,12 +19,31 @@ export class FetchMock { return Promise.resolve(this.response); } - public mock200(request: string, payloadResponse: T | undefined = undefined): void { + public mock200( + request: string, + payloadResponse: T | string | undefined = undefined, + ): void { this.url = request; this.response = this.createResponse(true, 200, "Success", payloadResponse); } - public mock201(request: string, payloadResponse: T | undefined = undefined): void { + public mock400( + request: string, + payloadResponse: T | string | undefined = undefined, + ): void { + this.url = request; + this.response = this.createResponse(false, 400, "Bad Reuqest", payloadResponse); + } + + public mock500(request: string): void { + this.url = request; + this.response = this.createResponse(false, 500, "Internal Server Error"); + } + + public mock201( + request: string, + payloadResponse: T | string | undefined = undefined, + ): void { this.url = request; this.response = this.createResponse(true, 201, "Created", payloadResponse); } @@ -34,7 +53,7 @@ export class FetchMock { this.response = this.createResponse(true, 204, "No Content"); } - public assertRequest(func: assertFunc): void { + public assertRequest(func: assertFunc | undefined): void { this.assert = func; } @@ -42,9 +61,13 @@ export class FetchMock { ok: boolean, status: number, statusText: string, - payloadResponse: T | undefined = undefined, + payloadResponse: T | string | undefined = undefined, ): Response { - const content = payloadResponse ? JSON.stringify({ value: payloadResponse }) : "0"; + const content = payloadResponse + ? typeof payloadResponse === "string" + ? payloadResponse + : JSON.stringify(payloadResponse) + : ""; const response: Response = { headers: new Headers(), diff --git a/test/odata-annotations.spec.ts b/test/odata-annotations.spec.ts new file mode 100644 index 0000000..504bd1a --- /dev/null +++ b/test/odata-annotations.spec.ts @@ -0,0 +1,69 @@ +import { FetchMock } from "./fetch-mock"; +import { oData, Entity, ODataAnnotations } from "../src"; + +describe("OData Annotations Test", () => { + const mock = new FetchMock(); + const o = oData({ baseUrl: "/odata", http: mock }); + const testEntity = o("entity"); + test("System Annotations Test", async () => { + const expected = { + "$odata.context": "test1", + "$odata.metadataEtag": "test2", + id: "test3", + }; + mock.mock201("/odata/entity/0", expected); + const actual = await testEntity.get("0"); + expect(actual.id).toBe(expected.id); + expect(actual.$odata.context).toBe(expected["$odata.context"]); + expect(actual.$odata.metadataEtag).toBe(expected["$odata.metadataEtag"]); + // verify it was actually removed. + const dict = (actual as unknown) as { [key: string]: string | undefined }; + expect(dict["$odata.context"]).toBeUndefined(); + expect(dict["$odata.metadataEtag"]).toBeUndefined(); + }); + + test("Custom Annotations Test", async () => { + const expected = { + "com.contoso": "test1", + id: "test3", + }; + mock.mock201("/odata/entity/0", expected); + const actual = await testEntity.get("0"); + expect(actual.id).toBe(expected.id); + expect(actual.$odata["com.contoso"]).toBe(expected["com.contoso"]); + // verify it was actually removed. + const dict = (actual as unknown) as { [key: string]: string | undefined }; + expect(dict["com.contoso"]).toBeUndefined(); + }); + + test("Scoped Annotations Test", async () => { + const expected = { + "Orders@odata.context": "test1", + "Orders@com.contoso": "test2", + id: "test3", + }; + mock.mock201("/odata/entity/0", expected); + const actual = await testEntity.get("0"); + expect(actual.id).toBe(expected.id); + const annotations = actual.$odata["Orders"] as ODataAnnotations; + expect(annotations.context).toBe(expected["Orders@odata.context"]); + expect(annotations["com.contoso"]).toBe(expected["Orders@com.contoso"]); + // verify it was actually removed. + const dict = (actual as unknown) as { [key: string]: string | undefined }; + expect(dict["Orders@odata.context"]).toBeUndefined(); + expect(dict["Orders@com.contoso"]).toBeUndefined(); + }); + + test("Not Supported Annotations Test", async () => { + const expected = { + "com.contoso.property": "test1", + "@Orders@com.contoso": "test2", + id: "test3", + }; + mock.mock201("/odata/entity/0", expected); + const actual = await testEntity.get("0"); + expect(actual.id).toBe(expected.id); + expect(actual.$odata["com.contoso.property"]).toBe(expected["com.contoso.property"]); + expect(actual.$odata["@Orders@com.contoso"]).toBe(expected["@Orders@com.contoso"]); + }); +}); diff --git a/test/odata-client.spec.ts b/test/odata-client.spec.ts index e3c39f8..d17483b 100644 --- a/test/odata-client.spec.ts +++ b/test/odata-client.spec.ts @@ -1,41 +1,41 @@ import { FetchMock } from "./fetch-mock"; -import { oData, Entity } from "../src"; +import { oData, Entity, ODataError } from "../src"; -describe("OData Client Test", () => { +describe("OData Client Tests", () => { const mock = new FetchMock(); const o = oData({ baseUrl: "/odata", http: mock }); const testEntity = o("entity"); const expectedPayload = { id: "0", }; - test("Add Entity", async () => { + test("Add Entity Test", async () => { mock.mock201("/odata/entity", expectedPayload); mock.assertRequest((url, init) => { expect(init?.method).toBe("POST"); }); const actualPayload = await testEntity.add(expectedPayload); - expect(actualPayload.value.id).toBe(expectedPayload.id); + expect(actualPayload.id).toBe(expectedPayload.id); }); - test("Get Entity", async () => { + test("Get Entity Test", async () => { mock.mock200("/odata/entity/0", expectedPayload); mock.assertRequest((url, init) => { expect(init?.method).toBe("GET"); }); const actualPayload = await testEntity.get("0"); - expect(actualPayload.value.id).toBe(expectedPayload.id); + expect(actualPayload.id).toBe(expectedPayload.id); }); - test("Update Entity", async () => { + test("Update Entity Test", async () => { mock.mock200("/odata/entity", expectedPayload); mock.assertRequest((url, init) => { expect(init?.method).toBe("PUT"); }); const actualPayload = await testEntity.update(expectedPayload); - expect(actualPayload.value.id).toBe(expectedPayload.id); + expect(actualPayload.id).toBe(expectedPayload.id); }); - test("Patch Entity", async () => { + test("Patch Entity Test", async () => { mock.mock204("/odata/entity/0"); mock.assertRequest((url, init) => { expect(init?.method).toBe("PATCH"); @@ -43,7 +43,7 @@ describe("OData Client Test", () => { await testEntity.patch("0", {}); }); - test("Delete Entity", async () => { + test("Delete Entity Test", async () => { mock.mock204("/odata/entity/0"); mock.assertRequest((url, init) => { expect(init?.method).toBe("DELETE"); @@ -51,22 +51,44 @@ describe("OData Client Test", () => { await testEntity.delete("0"); }); - test("Get Entities", async () => { - mock.mock200("/odata/entity", [expectedPayload]); + test("Get Entities Test", async () => { + mock.mock200("/odata/entity", { value: [expectedPayload] }); mock.assertRequest((url, init) => { expect(init?.method).toBe("GET"); }); const actualPayload = await testEntity.query(); - expect(actualPayload.value.length).toBe(1); - expect(actualPayload.value[0].id).toBe(expectedPayload.id); + expect(actualPayload.length).toBe(1); + expect(actualPayload[0].id).toBe(expectedPayload.id); }); - test("Get Count", async () => { - mock.mock200("/odata/entity/$count"); + test("Get Count Test", async () => { + mock.mock200("/odata/entity/$count", "100"); mock.assertRequest((url, init) => { expect(init?.method).toBe("GET"); }); const count = await testEntity.count(); - expect(count).toBe(0); + expect(count).toBe(100); + }); + + test("OData Error Test", () => { + const errorPayload = { + error: { + code: "400", + message: "Malformed", + }, + }; + mock.assertRequest(undefined); + mock.mock400("/odata/entity", errorPayload); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const testMethod = async () => await testEntity.add({} as any); + expect(testMethod).rejects.toThrow(ODataError); + }); + + test("OData Error Test", () => { + mock.assertRequest(undefined); + mock.mock500("/odata/entity"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const testMethod = async () => await testEntity.add({} as any); + expect(testMethod).rejects.toThrow(Error); }); }); diff --git a/test/odata-query.spec.ts b/test/odata-query.spec.ts index 9c15d2c..647d7d4 100644 --- a/test/odata-query.spec.ts +++ b/test/odata-query.spec.ts @@ -6,66 +6,72 @@ describe("OData Query Test", () => { const o = oData({ baseUrl: "/odata", http: mock }); const testEntity = o("entity"); const expectedPayload = { - id: "0", + value: [ + { + id: "0", + }, + ], }; test("Select Test", async () => { - mock.mock200("/odata/entity?$select=id", [expectedPayload]); + mock.mock200("/odata/entity?$select=id", expectedPayload); const actualPayload = await testEntity.query((_) => _.select("id")); - expect(actualPayload.value.length).toBe(1); + expect(actualPayload.length).toBe(1); }); test("Expand Test", async () => { - mock.mock200("/odata/entity?$expand=id", [expectedPayload]); + mock.mock200("/odata/entity?$expand=id", expectedPayload); const actualPayload = await testEntity.query((_) => _.expand("id")); - expect(actualPayload.value.length).toBe(1); + expect(actualPayload.length).toBe(1); }); test("Top Test", async () => { - mock.mock200("/odata/entity?$top=1", [expectedPayload]); + mock.mock200("/odata/entity?$top=1", expectedPayload); const actualPayload = await testEntity.query((_) => _.top(1)); - expect(actualPayload.value.length).toBe(1); + expect(actualPayload.length).toBe(1); }); test("Skip Test", async () => { - mock.mock200("/odata/entity?$skip=1", [expectedPayload]); + mock.mock200("/odata/entity?$skip=1", expectedPayload); const actualPayload = await testEntity.query((_) => _.skip(1)); - expect(actualPayload.value.length).toBe(1); + expect(actualPayload.length).toBe(1); }); test("OrderBy Test", async () => { - mock.mock200("/odata/entity?$orderBy=test+asc", [expectedPayload]); + mock.mock200("/odata/entity?$orderBy=test%20asc", expectedPayload); const actualPayload = await testEntity.query((_) => _.orderBy("test asc")); - expect(actualPayload.value.length).toBe(1); + expect(actualPayload.length).toBe(1); }); test("Filter Test", async () => { - mock.mock200("/odata/entity?$filter=test", [expectedPayload]); + mock.mock200("/odata/entity?$filter=test", expectedPayload); const actualPayload = await testEntity.query((_) => _.filter("test")); - expect(actualPayload.value.length).toBe(1); + expect(actualPayload.length).toBe(1); }); test("Count Test", async () => { - mock.mock200("/odata/entity?$count=true", [expectedPayload]); + mock.mock200("/odata/entity?$count=true", expectedPayload); const actualPayload = await testEntity.query((_) => _.count()); - expect(actualPayload.value.length).toBe(1); + expect(actualPayload.length).toBe(1); }); test("Custom Test", async () => { - mock.mock200("/odata/entity?custom=test", [expectedPayload]); + mock.mock200("/odata/entity?custom=test", expectedPayload); const actualPayload = await testEntity.query((_) => _.custom("custom", "test")); - expect(actualPayload.value.length).toBe(1); + expect(actualPayload.length).toBe(1); }); test("Duplicate Param Test", async () => { - mock.mock200("/odata/entity?$count=false", [expectedPayload]); - const actualPayload = await testEntity.query((_) => _.count().custom("$count", "false")); - expect(actualPayload.value.length).toBe(1); + mock.mock200("/odata/entity?$count=false", expectedPayload); + const actualPayload = await testEntity.query((_) => + _.count().custom("$count", "false"), + ); + expect(actualPayload.length).toBe(1); }); test("Null Param Test", async () => { - mock.mock200("/odata/entity", [expectedPayload]); + mock.mock200("/odata/entity", expectedPayload); // eslint-disable-next-line @typescript-eslint/no-explicit-any const actualPayload = await testEntity.query((_) => _.custom("", null)); - expect(actualPayload.value.length).toBe(1); + expect(actualPayload.length).toBe(1); }); test("Count Query Test", async () => { - mock.mock200("/odata/entity/$count?$filter=test"); + mock.mock200("/odata/entity/$count?$filter=test", "100"); const actualPayload = await testEntity.count((_) => _.filter("test")); - expect(actualPayload).toBe(0); + expect(actualPayload).toBe(100); }); }); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..56fd568 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index a1cc848..8d09d48 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "baseUrl": "./src", "strict": true, "lib": ["es6", "dom"], - "types": ["node", "jest"] + "types": ["node", "jest"] }, "include": ["src/**/*.ts", "test/**/*.ts"], "exclude": ["node_modules", "examples"] diff --git a/yarn.lock b/yarn.lock index 1e333d4..dae5ccb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5079,6 +5079,11 @@ ts-jest@^26.1.0: semver "7.x" yargs-parser "18.x" +ts-transformer-keys@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/ts-transformer-keys/-/ts-transformer-keys-0.4.1.tgz#209a07caea19cf6258ee2e159d5ad20982d8bb5d" + integrity sha512-CahLCOHt6MS8Sixz5cU8XovuKOoP6hnQd91pxG3a7iuuLsdrbWLveQvKi7d/FJjRhEtVELp3bMnqvSpm+nCgKw== + tslib@1.11.2: version "1.11.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.2.tgz#9c79d83272c9a7aaf166f73915c9667ecdde3cc9"