From f52736cc945d541586ad07c0aee16b2ca878aa96 Mon Sep 17 00:00:00 2001 From: Christian Cook <3473396+CookieCookson@users.noreply.github.com> Date: Sat, 12 Sep 2020 10:09:36 +0100 Subject: [PATCH] ETags (#43) * fix getting more statements * remove requirement for LRS to return extensions on about endpoint * add extra checks for if content type exists * change calls to return axios object instead of data * add etag support * change parts response to return axios promise --- src/XAPI.spec.ts | 150 ++++++++++++++++++++++++++++----------- src/XAPI.ts | 178 +++++++++++++++++++++++++++++------------------ 2 files changed, 217 insertions(+), 111 deletions(-) diff --git a/src/XAPI.spec.ts b/src/XAPI.spec.ts index 6797beb..f0a879c 100644 --- a/src/XAPI.spec.ts +++ b/src/XAPI.spec.ts @@ -55,16 +55,21 @@ function arrayBufferToWordArray(ab: ArrayBuffer): WordArray { describe("about resource", () => { test("can get about", () => { - return expect(xapi.getAbout()).resolves.toEqual(expect.objectContaining({ - extensions: expect.any(Object), - version: expect.any(Array) - })); + return xapi.getAbout() + .then((result) => { + return expect(result.data).toEqual(expect.objectContaining({ + version: expect.any(Array) + })); + }); }); }); describe("statement resource", () => { test("can create a statement", () => { - return expect(xapi.sendStatement(testStatement)).resolves.toHaveLength(1); + return xapi.sendStatement(testStatement) + .then((result) => { + return expect(result.data).toHaveLength(1); + }); }); test("can create a statement with a remote attachment", () => { @@ -91,7 +96,7 @@ describe("statement resource", () => { statement.attachments = [attachment]; return xapi.sendStatement(statement); }).then((result) => { - return expect(result).toHaveLength(1); + return expect(result.data).toHaveLength(1); }); }); @@ -113,17 +118,18 @@ describe("statement resource", () => { }; statement.attachments = [attachment]; return xapi.sendStatement(statement, [arrayBuffer]).then((result) => { - return expect(result).toHaveLength(1); + return expect(result.data).toHaveLength(1); }); }); test("can get a single statement", () => { - return xapi.sendStatement(testStatement).then((result) => { + return xapi.sendStatement(testStatement) + .then((result) => { return xapi.getStatement({ - statementId: result[0] + statementId: result.data[0] }); - }).then((statement) => { - return expect(statement).toHaveProperty("id"); + }).then((result) => { + return expect(result.data).toHaveProperty("id"); }); }); @@ -146,10 +152,11 @@ describe("statement resource", () => { statement.attachments = [attachment]; return xapi.sendStatement(statement, [arrayBuffer]).then((result) => { return xapi.getStatement({ - statementId: result[0], + statementId: result.data[0], attachments: true }); - }).then((parts) => { + }).then((response) => { + const parts = response.data; const attachmentData: unknown = parts[1]; return expect(attachmentData).toEqual(attachmentContent); }); @@ -157,29 +164,29 @@ describe("statement resource", () => { test("can void a single statement", () => { return xapi.sendStatement(testStatement).then((result) => { - return xapi.voidStatement(testAgent, result[0]); - }).then((voidResult) => { - return expect(voidResult).toHaveLength(1); + return xapi.voidStatement(testAgent, result.data[0]); + }).then((result) => { + return expect(result.data).toHaveLength(1); }); }); test("can get a voided statement", () => { let statementId: string; return xapi.sendStatement(testStatement).then((result) => { - statementId = result[0]; + statementId = result.data[0]; return xapi.voidStatement(testAgent, statementId); }).then(() => { return xapi.getVoidedStatement({ voidedStatementId: statementId }); - }).then((voidedStatement) => { - return expect(voidedStatement).toHaveProperty("id"); + }).then((result) => { + return expect(result.data).toHaveProperty("id"); }); }); test("can get an array of statements", () => { return xapi.getStatements().then((result) => { - return expect(result.statements).toBeTruthy(); + return expect(result.data.statements).toBeTruthy(); }); }); @@ -187,7 +194,7 @@ describe("statement resource", () => { return xapi.getStatements({ agent: testAgent }).then((result) => { - return expect(result.statements).toBeTruthy(); + return expect(result.data.statements).toBeTruthy(); }); }); @@ -195,7 +202,7 @@ describe("statement resource", () => { return xapi.getStatements({ limit: 1 }).then((result) => { - return expect(result.statements).toHaveLength(1); + return expect(result.data.statements).toHaveLength(1); }); }); @@ -203,9 +210,9 @@ describe("statement resource", () => { return xapi.getStatements({ limit: 1 }).then((result) => { - return xapi.getMoreStatements(result.more); + return xapi.getMoreStatements(result.data.more); }).then((result) => { - return expect(result.statements).toBeTruthy(); + return expect(result.data.statements).toBeTruthy(); }); }); }); @@ -217,33 +224,54 @@ describe("state resource", () => { }; test("can create state", () => { - return expect(xapi.createState(testAgent, testActivity.id, testStateId, testState)).resolves.toBeDefined(); + return xapi.createState(testAgent, testActivity.id, testStateId, testState) + .then((result) => { + return expect(result.data).toBeDefined(); + }); }); test("can set state", () => { - return expect(xapi.setState(testAgent, testActivity.id, testStateId, testState)).resolves.toBeDefined(); + return xapi.setState(testAgent, testActivity.id, testStateId, testState) + .then((result) => { + return expect(result.data).toBeDefined(); + }); }); test("can get all states", () => { - return expect(xapi.getStates(testAgent, testActivity.id)).resolves.toHaveLength(1); + return xapi.getStates(testAgent, testActivity.id) + .then((result) => { + return expect(result.data).toHaveLength(1); + }); }); test("can get a state", () => { - return expect(xapi.getState(testAgent, testActivity.id, testStateId)).resolves.toMatchObject(testState); + return xapi.getState(testAgent, testActivity.id, testStateId) + .then((result) => { + return expect(result.data).toMatchObject(testState); + }); }); test("can delete a state", () => { - return expect(xapi.deleteState(testAgent, testActivity.id, testStateId)).resolves.toBeDefined(); + return xapi.deleteState(testAgent, testActivity.id, testStateId) + .then((result) => { + return expect(result.data).toBeDefined(); + }); }); test("can delete all states", () => { - return expect(xapi.deleteStates(testAgent, testActivity.id)).resolves.toBeDefined(); + return xapi.deleteStates(testAgent, testActivity.id) + .then((result) => { + return expect(result.data).toBeDefined(); + }); }); }); describe("activities resource", () => { test("can get activity", () => { - return expect(xapi.getActivity(testActivity.id)).resolves.toMatchObject(testActivity); + return xapi.getActivity(testActivity.id) + .then((result) => { + return expect(result.data).toMatchObject(testActivity); + }); }); }); @@ -254,29 +282,50 @@ describe("activity profile resource", () => { }; test("can create activity profile", () => { - return expect(xapi.createActivityProfile(testActivity.id, testProfileId, testProfile)).resolves.toBeDefined(); + return xapi.createActivityProfile(testActivity.id, testProfileId, testProfile) + .then((result) => { + return expect(result.data).toBeDefined(); + }); }); test("can set activity profile", () => { - return expect(xapi.setActivityProfile(testActivity.id, testProfileId, testProfile)).resolves.toBeDefined(); + return xapi.getActivityProfile(testActivity.id, testProfileId) + .then((result) => { + return xapi.setActivityProfile(testActivity.id, testProfileId, testProfile, result.headers.etag, "If-Match") + .then((result) => { + return expect(result.data).toBeDefined(); + }); + }); }); test("can get all activity profiles", () => { - return expect(xapi.getActivityProfiles(testActivity.id)).resolves.toHaveLength(1); + return xapi.getActivityProfiles(testActivity.id) + .then((result) => { + return expect(result.data).toHaveLength(1); + }); }); test("can get an activity profile", () => { - return expect(xapi.getActivityProfile(testActivity.id, testProfileId)).resolves.toMatchObject(testProfile); + return xapi.getActivityProfile(testActivity.id, testProfileId) + .then((result) => { + return expect(result.data).toMatchObject(testProfile); + }); }); test("can delete an activity profile", () => { - return expect(xapi.deleteActivityProfile(testActivity.id, testProfileId)).resolves.toBeDefined(); + return xapi.deleteActivityProfile(testActivity.id, testProfileId) + .then((result) => { + return expect(result.data).toBeDefined(); + }); }); }); describe("agent resource", () => { test("can get person by agent", () => { - return expect(xapi.getAgent(testAgent)).resolves.toBeDefined(); + return xapi.getAgent(testAgent) + .then((result) => { + return expect(result.data).toBeDefined(); + }); }); }); @@ -287,22 +336,39 @@ describe("agent profile resource", () => { }; test("can create agent profile", () => { - return expect(xapi.createAgentProfile(testAgent, testProfileId, testProfile)).resolves.toBeDefined(); + return xapi.createAgentProfile(testAgent, testProfileId, testProfile) + .then((result) => { + return expect(result.data).toBeDefined(); + }); }); test("can set agent profile", () => { - return expect(xapi.setAgentProfile(testAgent, testProfileId, testProfile)).resolves.toBeDefined(); + return xapi.getAgentProfile(testAgent, testProfileId) + .then((result) => { + return xapi.setAgentProfile(testAgent, testProfileId, testProfile, result.headers.etag, "If-Match"); + }).then((result) => { + return expect(result.data).toBeDefined(); + }); }); test("can get all agent profiles", () => { - return expect(xapi.getAgentProfiles(testAgent)).resolves.toHaveLength(1); + return xapi.getAgentProfiles(testAgent) + .then((result) => { + return expect(result.data).toHaveLength(1); + }); }); test("can get an agent profile", () => { - return expect(xapi.getAgentProfile(testAgent, testProfileId)).resolves.toMatchObject(testProfile); + return xapi.getAgentProfile(testAgent, testProfileId) + .then((result) => { + return expect(result.data).toMatchObject(testProfile); + }); }); test("can delete an agent profile", () => { - return expect(xapi.deleteAgentProfile(testAgent, testProfileId)).resolves.toBeDefined(); + return xapi.deleteAgentProfile(testAgent, testProfileId) + .then((result) => { + return expect(result.data).toBeDefined(); + }); }); }); diff --git a/src/XAPI.ts b/src/XAPI.ts index 3f51207..be17657 100644 --- a/src/XAPI.ts +++ b/src/XAPI.ts @@ -5,7 +5,7 @@ import { AttachmentUsages, Resources, Verbs } from "./constants"; import { parseMultiPart, createMultiPart, MultiPart, Part } from "./helpers/multiPart"; import { getSearchQueryParamsAsObject } from "./helpers/getSearchQueryParamsAsObject"; import { calculateISO8601Duration } from "./helpers/calculateISO8601Duration"; -import axios, { AxiosRequestConfig } from "axios"; +import axios, { AxiosRequestConfig, AxiosPromise } from "axios"; export * from "./interfaces/XAPI"; export * from "./interfaces/Statement"; @@ -33,52 +33,53 @@ export default class XAPI { } // About Resource - public getAbout(): Promise { - return this.request(Resources.ABOUT); + public getAbout(): AxiosPromise { + return this.requestResource(Resources.ABOUT); } // Agents Resource - public getAgent(agent: Agent): Promise { - return this.request(Resources.AGENTS, { + public getAgent(agent: Agent): AxiosPromise { + return this.requestResource(Resources.AGENTS, { agent: agent }); } // Statement Resource - public getStatement(query: GetStatementQuery): Promise { - return this.request(Resources.STATEMENT, query); + public getStatement(query: GetStatementQuery): AxiosPromise { + return this.requestResource(Resources.STATEMENT, query); } - public getVoidedStatement(query: GetVoidedStatementQuery): Promise { - return this.request(Resources.STATEMENT, query); + public getVoidedStatement(query: GetVoidedStatementQuery): AxiosPromise { + return this.requestResource(Resources.STATEMENT, query); } - public getStatements(query?: GetStatementsQuery): Promise { - return this.request(Resources.STATEMENT, query); + public getStatements(query?: GetStatementsQuery): AxiosPromise { + return this.requestResource(Resources.STATEMENT, query); } - public getMoreStatements(more: string): Promise { - const params: {[key: string]: any} = getSearchQueryParamsAsObject(more); - return this.request(Resources.STATEMENT, params); + public getMoreStatements(more: string): AxiosPromise { + const endpoint = new URL(this.endpoint); + const url = `${endpoint.protocol}//${endpoint.host}${more}`; + return this.requestURL(url); } - public sendStatement(statement: Statement, attachments?: ArrayBuffer[]): Promise { + public sendStatement(statement: Statement, attachments?: ArrayBuffer[]): AxiosPromise { if (attachments?.length) { const multiPart: MultiPart = createMultiPart(statement, attachments); - return this.request(Resources.STATEMENT, {}, { + return this.requestResource(Resources.STATEMENT, {}, { method: "POST", headers: multiPart.header, data: multiPart.blob }); } else { - return this.request(Resources.STATEMENT, {}, { + return this.requestResource(Resources.STATEMENT, {}, { method: "POST", data: statement }); } } - public voidStatement(actor: Actor, statementId: string): Promise { + public voidStatement(actor: Actor, statementId: string): AxiosPromise { const voidStatement: Statement = { actor, verb: Verbs.VOIDED, @@ -87,15 +88,17 @@ export default class XAPI { id: statementId } }; - return this.request(Resources.STATEMENT, {}, { + return this.requestResource(Resources.STATEMENT, {}, { method: "POST", data: voidStatement }); } // State Resource - public createState(agent: Agent, activityId: string, stateId: string, state: {[key: string]: any}, registration?: string): Promise { - return this.request(Resources.STATE, { + public createState(agent: Agent, activityId: string, stateId: string, state: {[key: string]: any}, registration?: string, etag?: string, matchHeader?: "If-Match" | "If-None-Match"): AxiosPromise { + const headers = {}; + if (etag) headers[matchHeader] = etag; + return this.requestResource(Resources.STATE, { agent: agent, activityId: activityId, stateId: stateId, @@ -104,12 +107,15 @@ export default class XAPI { } : {}) }, { method: "POST", - data: state + data: state, + headers: headers }); } - public setState(agent: Agent, activityId: string, stateId: string, state: {[key: string]: any}, registration?: string): Promise { - return this.request(Resources.STATE, { + public setState(agent: Agent, activityId: string, stateId: string, state: {[key: string]: any}, registration?: string, etag?: string, matchHeader?: "If-Match" | "If-None-Match"): AxiosPromise { + const headers = {}; + if (etag) headers[matchHeader] = etag; + return this.requestResource(Resources.STATE, { agent: agent, activityId: activityId, stateId: stateId, @@ -118,12 +124,13 @@ export default class XAPI { } : {}) }, { method: "PUT", - data: state + data: state, + headers: headers }); } - public getStates(agent: Agent, activityId: string, registration?: string): Promise { - return this.request(Resources.STATE, { + public getStates(agent: Agent, activityId: string, registration?: string): AxiosPromise { + return this.requestResource(Resources.STATE, { agent: agent, activityId: activityId, ...(registration ? { @@ -132,8 +139,8 @@ export default class XAPI { }); } - public getState(agent: Agent, activityId: string, stateId: string, registration?: string): Promise<{[key: string]: any}> { - return this.request(Resources.STATE, { + public getState(agent: Agent, activityId: string, stateId: string, registration?: string): AxiosPromise<{[key: string]: any}> { + return this.requestResource(Resources.STATE, { agent: agent, activityId: activityId, stateId: stateId, @@ -143,8 +150,10 @@ export default class XAPI { }); } - public deleteState(agent: Agent, activityId: string, stateId: string, registration?: string): Promise { - return this.request(Resources.STATE, { + public deleteState(agent: Agent, activityId: string, stateId: string, registration?: string, etag?: string): AxiosPromise { + const headers = {}; + if (etag) headers["If-Match"] = etag; + return this.requestResource(Resources.STATE, { agent: agent, activityId: activityId, stateId: stateId, @@ -152,117 +161,143 @@ export default class XAPI { registration } : {}) }, { - method: "DELETE" + method: "DELETE", + headers: headers }); } - public deleteStates(agent: Agent, activityId: string, registration?: string): Promise { - return this.request(Resources.STATE, { + public deleteStates(agent: Agent, activityId: string, registration?: string, etag?: string): AxiosPromise { + const headers = {}; + if (etag) headers["If-Match"] = etag; + return this.requestResource(Resources.STATE, { agent: agent, activityId: activityId, ...(registration ? { registration } : {}) }, { - method: "DELETE" + method: "DELETE", + headers: headers }); } // Activities Resource - public getActivity(activityId: string): Promise { - return this.request(Resources.ACTIVITIES, { + public getActivity(activityId: string): AxiosPromise { + return this.requestResource(Resources.ACTIVITIES, { activityId: activityId }); } // Activity Profile Resource - public createActivityProfile(activityId: string, profileId: string, profile: {[key: string]: any}): Promise { - return this.request(Resources.ACTIVITY_PROFILE, { + public createActivityProfile(activityId: string, profileId: string, profile: {[key: string]: any}, etag?: string, matchHeader?: "If-Match" | "If-None-Match"): AxiosPromise { + const headers = {}; + if (etag) headers[matchHeader] = etag; + return this.requestResource(Resources.ACTIVITY_PROFILE, { activityId: activityId, profileId: profileId }, { method: "POST", - data: profile + data: profile, + headers: headers }); } - public setActivityProfile(activityId: string, profileId: string, profile: {[key: string]: any}): Promise { - return this.request(Resources.ACTIVITY_PROFILE, { + public setActivityProfile(activityId: string, profileId: string, profile: {[key: string]: any}, etag: string, matchHeader: "If-Match" | "If-None-Match"): AxiosPromise { + const headers = {}; + headers[matchHeader] = etag; + return this.requestResource(Resources.ACTIVITY_PROFILE, { activityId: activityId, profileId: profileId }, { method: "PUT", - data: profile + data: profile, + headers: headers }); } - public getActivityProfiles(activityId: string): Promise { - return this.request(Resources.ACTIVITY_PROFILE, { + public getActivityProfiles(activityId: string): AxiosPromise { + return this.requestResource(Resources.ACTIVITY_PROFILE, { activityId: activityId }); } - public getActivityProfile(activityId: string, profileId: string): Promise<{[key: string]: any}> { - return this.request(Resources.ACTIVITY_PROFILE, { + public getActivityProfile(activityId: string, profileId: string): AxiosPromise<{[key: string]: any}> { + return this.requestResource(Resources.ACTIVITY_PROFILE, { activityId: activityId, profileId: profileId }); } - public deleteActivityProfile(activityId: string, profileId: string): Promise { - return this.request(Resources.ACTIVITY_PROFILE, { + public deleteActivityProfile(activityId: string, profileId: string, etag?: string): AxiosPromise { + const headers = {}; + if (etag) headers["If-Match"] = etag; + return this.requestResource(Resources.ACTIVITY_PROFILE, { activityId: activityId, profileId: profileId }, { - method: "DELETE" + method: "DELETE", + headers: headers }); } // Agent Profile Resource - public createAgentProfile(agent: Agent, profileId: string, profile: {[key: string]: any}): Promise { - return this.request(Resources.AGENT_PROFILE, { + public createAgentProfile(agent: Agent, profileId: string, profile: {[key: string]: any}, etag?: string, matchHeader?: "If-Match" | "If-None-Match"): AxiosPromise { + const headers = {}; + if (etag) headers[matchHeader] = etag; + return this.requestResource(Resources.AGENT_PROFILE, { agent: agent, profileId: profileId }, { method: "POST", - data: profile + data: profile, + headers: headers }); } - public setAgentProfile(agent: Agent, profileId: string, profile: {[key: string]: any}): Promise { - return this.request(Resources.AGENT_PROFILE, { + public setAgentProfile(agent: Agent, profileId: string, profile: {[key: string]: any}, etag: string, matchHeader: "If-Match" | "If-None-Match"): AxiosPromise { + const headers = {}; + headers[matchHeader] = etag; + return this.requestResource(Resources.AGENT_PROFILE, { agent: agent, profileId: profileId }, { method: "PUT", - data: profile + data: profile, + headers: headers }); } - public getAgentProfiles(agent: Agent): Promise { - return this.request(Resources.AGENT_PROFILE, { + public getAgentProfiles(agent: Agent): AxiosPromise { + return this.requestResource(Resources.AGENT_PROFILE, { agent: agent }); } - public getAgentProfile(agent: Agent, profileId: string): Promise<{[key: string]: any}> { - return this.request(Resources.AGENT_PROFILE, { + public getAgentProfile(agent: Agent, profileId: string): AxiosPromise<{[key: string]: any}> { + return this.requestResource(Resources.AGENT_PROFILE, { agent: agent, profileId: profileId }); } - public deleteAgentProfile(agent: Agent, profileId: string): Promise { - return this.request(Resources.AGENT_PROFILE, { + public deleteAgentProfile(agent: Agent, profileId: string, etag?: string): AxiosPromise { + const headers = {}; + if (etag) headers["If-Match"] = etag; + return this.requestResource(Resources.AGENT_PROFILE, { agent: agent, profileId: profileId }, { - method: "DELETE" + method: "DELETE", + headers: headers }); } - private request(resource: Resource, params: RequestParams = {}, initExtras?: AxiosRequestConfig | undefined): Promise { + private requestResource(resource: Resource, params: RequestParams = {}, initExtras?: AxiosRequestConfig | undefined): AxiosPromise { const url = this.generateURL(resource, params); + return this.requestURL(url, initExtras); + } + + private requestURL(url: string, initExtras?: AxiosRequestConfig | undefined): AxiosPromise { return axios({ method: initExtras?.method || "GET", url: url, @@ -270,13 +305,18 @@ export default class XAPI { ...this.headers, ...initExtras?.headers }, - data: initExtras?.data, - + data: initExtras?.data }).then((response) => { - if (response.headers["content-type"].indexOf("application/json") !== -1) { - return response.data; + const contentType = response.headers["content-type"]; + if ( + !contentType || + contentType.indexOf("application/json") !== -1 || + response.data.indexOf("--") !== 2 + ) { + return response; } else { - return response.data.indexOf("--") === 2 ? parseMultiPart(response.data) : response.data; + response.data = parseMultiPart(response.data); + return response; } }); }