From 498f448988c03a8b5249309ec330c92d257f382a Mon Sep 17 00:00:00 2001 From: Bobby Galli Date: Mon, 9 Jan 2023 15:13:57 -0500 Subject: [PATCH] feat!: allow body reuse (#92) * feat!: allow re-use of response json * feat!: safer typing * chore: fix imports * chore: remove old type * chore: remove unneeded casts * chore: npm audit fix * chore: fix dependabot error * feat!: actually fix the clone issue this time * chore: fix table-data-client type args BREAKING CHANGE: api client now returns Response instead of BugSplatResponse to allow cloning. BugSplatResponse `json` no longer returns type `any`. --- package-lock.json | 62 +++++++------------ package.json | 2 +- spec/fakes/common/response.ts | 55 +++++++++++++--- src/common/client/api-client.ts | 7 ++- .../bugsplat-api-client.spec.ts | 17 ++--- .../bugsplat-api-client.ts | 19 ++++-- .../bugsplat-login-response.ts | 3 + ...auth-client-credentials-api-client.spec.ts | 31 ++++++---- .../oauth-client-credentials-api-client.ts | 36 ++++++----- .../oauth-login-response.ts | 4 ++ .../table-data-client/table-data-client.ts | 36 ++++++----- .../table-data-client/table-data-response.ts | 4 +- .../crash-api-client/crash-api-client.ts | 24 ++++--- src/crash/crash-details/crash-details.ts | 2 +- .../crashes-api-client/crashes-api-client.ts | 9 +-- .../events-api-client/events-api-client.ts | 11 ++-- src/post/crash-post-client.ts | 15 ++--- src/post/post-crash-response.ts | 3 + .../summary-table-data-client.ts | 23 ++++--- .../put-retired-response.ts | 3 + .../versions-api-client.e2e.ts | 2 +- .../versions-api-client.ts | 39 ++++++------ 22 files changed, 248 insertions(+), 159 deletions(-) create mode 100644 src/common/client/bugsplat-api-client/bugsplat-login-response.ts create mode 100644 src/common/client/oauth-client-credentials-api-client/oauth-login-response.ts create mode 100644 src/post/post-crash-response.ts create mode 100644 src/versions/versions-api-client/put-retired-response.ts diff --git a/package-lock.json b/package-lock.json index d43c1b4..307aa4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "jasmine-reporters": "^2.4.0", "semantic-release": "^19.0.2", "ts-node": "^10.0.0", - "tsconfig-paths": "^3.11.0", + "tsconfig-paths": "^4.1.2", "tsconfig-replace-paths": "^0.0.5", "typescript": "^4.4.3" }, @@ -869,12 +869,6 @@ "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -3020,15 +3014,15 @@ "dev": true }, "node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, "bin": { "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/jsonfile": { @@ -7502,15 +7496,17 @@ } }, "node_modules/tsconfig-paths": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz", - "integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.2.tgz", + "integrity": "sha512-uhxiMgnXQp1IR622dUXI+9Ehnws7i/y6xvpZB9IbUVOPy0muvdvgXeZOn88UcGPiT98Vp3rJPTa8bFoalZ3Qhw==", "dev": true, "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.0", + "json5": "^2.2.2", + "minimist": "^1.2.6", "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, "node_modules/tsconfig-replace-paths": { @@ -8526,12 +8522,6 @@ "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, "@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -10154,13 +10144,10 @@ "dev": true }, "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true }, "jsonfile": { "version": "6.1.0", @@ -13368,14 +13355,13 @@ } }, "tsconfig-paths": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz", - "integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.2.tgz", + "integrity": "sha512-uhxiMgnXQp1IR622dUXI+9Ehnws7i/y6xvpZB9IbUVOPy0muvdvgXeZOn88UcGPiT98Vp3rJPTa8bFoalZ3Qhw==", "dev": true, "requires": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.0", + "json5": "^2.2.2", + "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, diff --git a/package.json b/package.json index 756c91f..fccc11c 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "jasmine-reporters": "^2.4.0", "semantic-release": "^19.0.2", "ts-node": "^10.0.0", - "tsconfig-paths": "^3.11.0", + "tsconfig-paths": "^4.1.2", "tsconfig-replace-paths": "^0.0.5", "typescript": "^4.4.3" } diff --git a/spec/fakes/common/response.ts b/spec/fakes/common/response.ts index 2408437..d2db152 100644 --- a/spec/fakes/common/response.ts +++ b/spec/fakes/common/response.ts @@ -1,13 +1,50 @@ -export function createFakeResponseBody( - status: number, - json: any = {}, +export function createFakeResponseBody( + status = 200, + json = {} as T, ok = true, - headers: any = [] -) { - return { + headers = new Map() +): FakeResponseBody { + return new FakeResponseBody( status, + json, ok, - json: async() => (json), - headers: { get: () => headers } - }; + headers + ); +} + +export class FakeResponseBody { + + private _json: T; + private _headers: Map; + + get headers(): Map { + return this._headers; + } + + constructor( + public readonly status = 200, + json = {} as T, + public readonly ok = true, + headers = new Map() as Map + ) { + this._json = json; + this._headers = headers; + } + + async json(): Promise { + return this._json; + } + + async text(): Promise { + return JSON.stringify(this._json); + } + + clone(): FakeResponseBody { + return new FakeResponseBody( + this.status, + this._json, + this.ok, + this._headers + ); + } } \ No newline at end of file diff --git a/src/common/client/api-client.ts b/src/common/client/api-client.ts index ad31059..124c908 100644 --- a/src/common/client/api-client.ts +++ b/src/common/client/api-client.ts @@ -1,9 +1,10 @@ export interface ApiClient { createFormData(): FormData; - fetch(route: string, init?: RequestInit): Promise; + fetch(route: string, init?: RequestInit): Promise>; } -export interface BugSplatResponse { +export interface BugSplatResponse { status: number; - json: () => Promise; // eslint-disable-line @typescript-eslint/no-explicit-any + json: () => Promise; + text: () => Promise; } diff --git a/src/common/client/bugsplat-api-client/bugsplat-api-client.spec.ts b/src/common/client/bugsplat-api-client/bugsplat-api-client.spec.ts index 7d78658..7bb74f8 100644 --- a/src/common/client/bugsplat-api-client/bugsplat-api-client.spec.ts +++ b/src/common/client/bugsplat-api-client/bugsplat-api-client.spec.ts @@ -14,17 +14,18 @@ describe('BugSplatApiClient', () => { let expectedStatus; let expectedJson; let fakeFormData; - let fakeSuccessReponseBody; + let fakeSuccessResponseBody; beforeEach(() => { + const headers = new Map([['set-cookie', cookie]]); appendSpy = jasmine.createSpy(); expectedStatus = 'success'; expectedJson = { success: 'true' }; fakeFormData = { append: appendSpy, toString: () => 'BugSplat rocks!' }; - fakeSuccessReponseBody = createFakeResponseBody(expectedStatus, expectedJson, true, cookie); + fakeSuccessResponseBody = createFakeResponseBody(expectedStatus, expectedJson, true, headers); client = createFakeBugSplatApiClient( Environment.Node, - fakeSuccessReponseBody, + fakeSuccessResponseBody, fakeFormData ); }); @@ -52,7 +53,7 @@ describe('BugSplatApiClient', () => { it('should call fetch with include credentials in request init', async () => { client = createFakeBugSplatApiClient( Environment.WebBrowser, - fakeSuccessReponseBody, + fakeSuccessResponseBody, fakeFormData ); @@ -84,8 +85,10 @@ describe('BugSplatApiClient', () => { }); }); - it('should return result', () => { - expect(result).toEqual(fakeSuccessReponseBody); + it('should return result', async () => { + const expectedJson = await fakeSuccessResponseBody.json(); + const resultJson = await result.json(); + expect(resultJson).toEqual(jasmine.objectContaining(expectedJson as Record)); }); }); @@ -117,7 +120,7 @@ describe('BugSplatApiClient', () => { }); it('should return result', () => { - expect(result).toEqual(fakeSuccessReponseBody); + expect(result).toEqual(fakeSuccessResponseBody); }); describe('error', () => { diff --git a/src/common/client/bugsplat-api-client/bugsplat-api-client.ts b/src/common/client/bugsplat-api-client/bugsplat-api-client.ts index 1bb46e3..317d59d 100644 --- a/src/common/client/bugsplat-api-client/bugsplat-api-client.ts +++ b/src/common/client/bugsplat-api-client/bugsplat-api-client.ts @@ -1,4 +1,5 @@ import { ApiClient, bugsplatAppHostUrl, BugSplatResponse, Environment } from '@common'; +import { BugSplatLoginResponse } from './bugsplat-login-response'; export class BugSplatApiClient implements ApiClient { private _createFormData = () => new FormData(); @@ -34,7 +35,7 @@ export class BugSplatApiClient implements ApiClient { return this._createFormData(); } - async fetch(route: string, init: RequestInit = {}): Promise { + async fetch(route: string, init: RequestInit = {}): Promise> { if (!init.headers) { init.headers = {}; } @@ -48,21 +49,29 @@ export class BugSplatApiClient implements ApiClient { } const url = new URL(route, this._host); - return this._fetch(url.href, init); + const response = await this._fetch(url.href, init); + const status = response.status; + + return { + status, + json: async () => response.clone().json(), + text: async () => response.clone().text() + }; } - async login(email: string, password: string): Promise { + async login(email: string, password: string): Promise> { const url = new URL('/api/authenticatev3', this._host); const formData = this._createFormData(); formData.append('email', email); formData.append('password', password); formData.append('Login', 'Login'); - const response = await this._fetch(url.href, { + const request = { method: 'POST', body: formData, cache: 'no-cache', redirect: 'follow' - }); + } as RequestInit; + const response = await this._fetch(url.href, request); if (response.status === 401) { throw new Error('Could not authenticate, check credentials and try again'); diff --git a/src/common/client/bugsplat-api-client/bugsplat-login-response.ts b/src/common/client/bugsplat-api-client/bugsplat-login-response.ts new file mode 100644 index 0000000..721b409 --- /dev/null +++ b/src/common/client/bugsplat-api-client/bugsplat-login-response.ts @@ -0,0 +1,3 @@ +export interface BugSplatLoginResponse { + user: string; +} \ No newline at end of file diff --git a/src/common/client/oauth-client-credentials-api-client/oauth-client-credentials-api-client.spec.ts b/src/common/client/oauth-client-credentials-api-client/oauth-client-credentials-api-client.spec.ts index 2f75ecf..afe7a82 100644 --- a/src/common/client/oauth-client-credentials-api-client/oauth-client-credentials-api-client.spec.ts +++ b/src/common/client/oauth-client-credentials-api-client/oauth-client-credentials-api-client.spec.ts @@ -1,13 +1,15 @@ import { createFakeFormData } from '@spec/fakes/common/form-data'; -import { createFakeResponseBody } from '@spec/fakes/common/response'; +import { createFakeResponseBody, FakeResponseBody } from '@spec/fakes/common/response'; +import { BugSplatResponse } from 'dist/esm'; +import { OAuthLoginResponse } from 'dist/esm/common/client/oauth-client-credentials-api-client/oauth-login-response'; import { OAuthClientCredentialsClient } from './oauth-client-credentials-api-client'; describe('OAuthClientCredentialsClient', () => { - let clientId; - let clientSecret; - let fakeAuthorizeResponseBody; - let fakeAuthorizeResult; - let fakeFetchResponseBody; + let clientId: string; + let clientSecret: string; + let fakeAuthorizeResponseBody: FakeResponseBody; + let fakeAuthorizeResult: AuthorizeResult; + let fakeFetchResponseBody: FakeResponseBody; let fakeFetchResult; let fakeFormData; let host; @@ -38,7 +40,7 @@ describe('OAuthClientCredentialsClient', () => { }); describe('login', () => { - let result; + let result: BugSplatResponse; beforeEach(async () => result = await sut.login()); @@ -70,7 +72,7 @@ describe('OAuthClientCredentialsClient', () => { }); describe('error', () => { - it('should return useful error message when authenication fails', async () => { + it('should return useful error message when authentication fails', async () => { const failureResponseBody = createFakeResponseBody(200, { error: 'invalid_client' }); sut = createFakeOAuthClientCredentialsClient( 'blah', @@ -91,7 +93,7 @@ describe('OAuthClientCredentialsClient', () => { describe('fetch', () => { let route; let headers; - let result; + let result: BugSplatResponse; beforeEach(async () => { route = '/what/will/we/do/with/a/drunken/sailor'; @@ -129,8 +131,10 @@ describe('OAuthClientCredentialsClient', () => { })); }); - it('should return result', () => { - expect(result).toEqual(fakeFetchResponseBody); + it('should return result', async () => { + const expectedJson = await fakeFetchResponseBody.json(); + const resultJson = await result.json(); + expect(resultJson).toEqual(jasmine.objectContaining(expectedJson as Record)); }); describe('error', () => { @@ -159,3 +163,8 @@ function createFakeOAuthClientCredentialsClient( (client)._createFormData = () => formData; return client; } + +interface AuthorizeResult { + access_token: string; + token_type: string; +} \ No newline at end of file diff --git a/src/common/client/oauth-client-credentials-api-client/oauth-client-credentials-api-client.ts b/src/common/client/oauth-client-credentials-api-client/oauth-client-credentials-api-client.ts index a717a53..73e036d 100644 --- a/src/common/client/oauth-client-credentials-api-client/oauth-client-credentials-api-client.ts +++ b/src/common/client/oauth-client-credentials-api-client/oauth-client-credentials-api-client.ts @@ -1,4 +1,6 @@ import { ApiClient, bugsplatAppHostUrl, BugSplatResponse } from '@common'; +import { OAuthLoginResponse } from './oauth-login-response'; + export class OAuthClientCredentialsClient implements ApiClient { private _accessToken = ''; @@ -26,7 +28,7 @@ export class OAuthClientCredentialsClient implements ApiClient { return client; } - async login(): Promise { + async login(): Promise> { const url = `${this._host}/oauth2/authorize`; const method = 'POST'; const body = this.createFormData(); @@ -37,31 +39,27 @@ export class OAuthClientCredentialsClient implements ApiClient { const request = { method, body - }; + } as RequestInit; - const response = await this.fetch(url, request); + const response = await this.fetch(url, request); const responseJson = await response.json(); - const status = response.status; - if (responseJson.error === 'invalid_client') { + if ((responseJson as ErrorResponse).error === 'invalid_client') { throw new Error('Could not authenticate, check credentials and try again'); } - this._accessToken = responseJson.access_token; - this._tokenType = responseJson.token_type; + const loginResponse = responseJson as OAuthLoginResponse; + this._accessToken = loginResponse.access_token; + this._tokenType = loginResponse.token_type; - const json = async () => responseJson; - return { - status, - json - }; + return response as BugSplatResponse; } createFormData(): FormData { return this._createFormData(); } - async fetch(route: string, init?: RequestInit): Promise { + async fetch(route: string, init?: RequestInit): Promise> { const url = new URL(route, this._host); init = init ?? {}; @@ -72,11 +70,19 @@ export class OAuthClientCredentialsClient implements ApiClient { init.headers['Authorization'] = `${this._tokenType} ${this._accessToken}`; const response = await this._fetch(url.href, init); + const status = response.status; - if (response.status === 401) { + if (status === 401) { throw new Error('Could not authenticate, check credentials and try again'); } - return response; + return { + status, + json: async () => response.clone().json(), + text: async () => response.clone().text() + }; } } + +type ErrorResponse = { error: string }; +type LoginResponse = ErrorResponse | OAuthLoginResponse; \ No newline at end of file diff --git a/src/common/client/oauth-client-credentials-api-client/oauth-login-response.ts b/src/common/client/oauth-client-credentials-api-client/oauth-login-response.ts new file mode 100644 index 0000000..f32a30f --- /dev/null +++ b/src/common/client/oauth-client-credentials-api-client/oauth-login-response.ts @@ -0,0 +1,4 @@ +export interface OAuthLoginResponse { + access_token: string; + token_type: string; +} \ No newline at end of file diff --git a/src/common/data/table-data/table-data-client/table-data-client.ts b/src/common/data/table-data/table-data-client/table-data-client.ts index fc0feee..c28d026 100644 --- a/src/common/data/table-data/table-data-client/table-data-client.ts +++ b/src/common/data/table-data/table-data-client/table-data-client.ts @@ -1,4 +1,4 @@ -import { ApiClient, TableDataRequest, BugSplatResponse } from '@common'; +import { ApiClient, TableDataRequest, BugSplatResponse, TableDataResponse } from '@common'; import { TableDataFormDataBuilder } from '../table-data-form-data-builder/table-data-form-data-builder'; export class TableDataClient { @@ -6,7 +6,7 @@ export class TableDataClient { constructor(private _apiClient: ApiClient, private _url: string) { } // We use POST to get data in most cases because it supports longer queries - async postGetData(request: TableDataRequest): Promise { + async postGetData | undefined)>(request: TableDataRequest): Promise>> { const factory = () => this._apiClient.createFormData(); const formData = new TableDataFormDataBuilder(factory) .withDatabase(request.database) @@ -17,17 +17,17 @@ export class TableDataClient { .withSortColumn(request.sortColumn) .withSortOrder(request.sortOrder) .build(); - const init = { + const requestInit = { method: 'POST', body: formData, cache: 'no-cache', credentials: 'include', redirect: 'follow' - }; - return this.makeRequest(this._url, init); + } as RequestInit; + return this.makeRequest(this._url, requestInit); } - async getData(request: TableDataRequest): Promise { + async getData | undefined)>(request: TableDataRequest): Promise>> { const factory = () => this._apiClient.createFormData(); const formData = new TableDataFormDataBuilder(factory) .withDatabase(request.database) @@ -38,30 +38,36 @@ export class TableDataClient { .withSortColumn(request.sortColumn) .withSortOrder(request.sortOrder) .entries(); - const init = { + const requestInit = { method: 'GET', cache: 'no-cache', credentials: 'include', redirect: 'follow' - }; + } as RequestInit; const queryParams = new URLSearchParams(formData).toString(); - return this.makeRequest(`${this._url}?${queryParams}`, init); + return this.makeRequest(`${this._url}?${queryParams}`, requestInit); } - private async makeRequest(url: string, init: RequestInit): Promise { - const response = await this._apiClient.fetch(url, init); + private async makeRequest(url: string, init: RequestInit): Promise>> { + const response = await this._apiClient.fetch>>(url, init); const responseData = await response.json(); const rows = responseData ? responseData[0]?.Rows : []; const pageData = responseData ? responseData[0]?.PageData : {}; - + const status = response.status; - const json = async () => ({ rows, pageData }); + const payload = { rows, pageData } as TableDataResponse; + const json = async () => payload; + const text = async () => JSON.stringify(payload); return { status, - json + json, + text }; } } - +// https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as +export type RawResponse = Array<{ + [Property in keyof T as Capitalize]: T[Property] +}> diff --git a/src/common/data/table-data/table-data-client/table-data-response.ts b/src/common/data/table-data/table-data-client/table-data-response.ts index 62185fa..ef30d97 100644 --- a/src/common/data/table-data/table-data-client/table-data-response.ts +++ b/src/common/data/table-data/table-data-client/table-data-response.ts @@ -1,4 +1,4 @@ -export interface TableDataResponse { +export interface TableDataResponse> { rows: Array; - pageData?: Record; + pageData?: U; } diff --git a/src/crash/crash-api-client/crash-api-client.ts b/src/crash/crash-api-client/crash-api-client.ts index 75a83a1..b98aa20 100644 --- a/src/crash/crash-api-client/crash-api-client.ts +++ b/src/crash/crash-api-client/crash-api-client.ts @@ -1,6 +1,7 @@ import { ApiClient } from '@common'; import { CrashDetails } from '@crash'; import ac from 'argument-contracts'; +import { CrashDetailsConstructorOptions } from '../crash-details/crash-details'; export class CrashApiClient { @@ -22,19 +23,19 @@ export class CrashApiClient { cache: 'no-cache', credentials: 'include', redirect: 'follow' - }; + } as RequestInit; - const response = await this._client.fetch('/api/crash/data', init); + const response = await this._client.fetch('/api/crash/data', init); const json = await response.json(); if (response.status !== 200) { - throw new Error(json.message); + throw new Error((json as Error).message); } - return new CrashDetails(json); + return new CrashDetails(json as CrashDetailsConstructorOptions); } - async reprocessCrash(database: string, crashId: number, force = false): Promise<{ success: boolean }> { + async reprocessCrash(database: string, crashId: number, force = false): Promise { ac.assertNonWhiteSpaceString(database, 'database'); ac.assertBoolean(force, 'force'); if (crashId <= 0) { @@ -51,15 +52,20 @@ export class CrashApiClient { cache: 'no-cache', credentials: 'include', redirect: 'follow' - }; + } as RequestInit; - const response = await this._client.fetch('/api/crash/reprocess', init); + const response = await this._client.fetch('/api/crash/reprocess', init); const json = await response.json(); if (response.status !== 202) { - throw new Error(json.message); + throw new Error((json as ErrorResponse).message); } - return json; + return json as SuccessResponse; } } + +type SuccessResponse = { success: boolean }; +type ErrorResponse = { message: string }; +type GetCrashByIdResponse = CrashDetailsConstructorOptions | ErrorResponse; +type ReprocessCrashResponse = SuccessResponse | ErrorResponse; \ No newline at end of file diff --git a/src/crash/crash-details/crash-details.ts b/src/crash/crash-details/crash-details.ts index de55bb3..5a05acb 100644 --- a/src/crash/crash-details/crash-details.ts +++ b/src/crash/crash-details/crash-details.ts @@ -19,7 +19,7 @@ export enum DefectTrackerType { Assembla = 'Assembla' } -interface CrashDetailsConstructorOptions { +export interface CrashDetailsConstructorOptions { processed: ProcessingStatus; additionalFiles?: Array; diff --git a/src/crashes/crashes-api-client/crashes-api-client.ts b/src/crashes/crashes-api-client/crashes-api-client.ts index 41bca71..d5ff8f9 100644 --- a/src/crashes/crashes-api-client/crashes-api-client.ts +++ b/src/crashes/crashes-api-client/crashes-api-client.ts @@ -1,5 +1,6 @@ import { ApiClient, BugSplatResponse, TableDataClient, TableDataRequest, TableDataResponse } from '@common'; import { CrashesApiRow } from '@crashes'; +import { CrashesApiResponseRow } from '../crashes-api-row/crashes-api-row'; export class CrashesApiClient { @@ -10,7 +11,7 @@ export class CrashesApiClient { } async getCrashes(request: TableDataRequest): Promise> { - const response = await this._tableDataClient.postGetData(request); + const response = await this._tableDataClient.postGetData(request); const json = await response.json(); const pageData = json.pageData; const rows = json.rows.map(row => new CrashesApiRow(row)); @@ -32,14 +33,14 @@ export class CrashesApiClient { formData.append('id', `${id}`); formData.append('Comments', notes); - const init = { + const request = { method: 'POST', body: formData, cache: 'no-cache', credentials: 'include', redirect: 'follow' - }; + } as RequestInit; - return this._client.fetch('/allcrash?data', init); + return this._client.fetch('/allcrash?data', request); } } \ No newline at end of file diff --git a/src/events/events-api-client/events-api-client.ts b/src/events/events-api-client/events-api-client.ts index e2bcab0..1f28bae 100644 --- a/src/events/events-api-client/events-api-client.ts +++ b/src/events/events-api-client/events-api-client.ts @@ -1,5 +1,5 @@ import { ApiClient } from '@common'; -import { convertEventsToEventStreamEvents, Event } from '@events'; +import { convertEventsToEventStreamEvents, Event, EventResponseObject } from '@events'; import ac from 'argument-contracts'; export enum EventType { @@ -34,12 +34,15 @@ export class EventsApiClient { private async getEvents(route: string): Promise> { const response = await this._client.fetch(route); - const json = await response.json(); + const json = await response.json() as SuccessResponse | ErrorResponse; if (response.status !== 200) { - throw new Error(json.message); + throw new Error((json as ErrorResponse).message); } - return convertEventsToEventStreamEvents(json.events); + return convertEventsToEventStreamEvents((json as SuccessResponse).events); } } + +type SuccessResponse = { events: Array }; +type ErrorResponse = { message: string }; \ No newline at end of file diff --git a/src/post/crash-post-client.ts b/src/post/crash-post-client.ts index 2efab2f..1f7f877 100644 --- a/src/post/crash-post-client.ts +++ b/src/post/crash-post-client.ts @@ -1,5 +1,6 @@ -import { BugSplatApiClient, UploadableFile, BugSplatResponse, Environment, S3ApiClient } from '@common'; +import { BugSplatApiClient, BugSplatResponse, Environment, S3ApiClient, UploadableFile } from '@common'; import { CrashType } from '@post'; +import { PostCrashResponse } from './post-crash-response'; export class CrashPostClient { @@ -24,7 +25,7 @@ export class CrashPostClient { type: CrashType, file: UploadableFile, md5 = '' - ): Promise { + ): Promise> { const uploadUrl = await this.getCrashUploadUrl( this._database, application, @@ -56,7 +57,7 @@ export class CrashPostClient { + `&appName=${application}` + `&appVersion=${version}` + `&crashPostSize=${size}`; - const response = await this._processorApiClient.fetch(route); + const response = await this._processorApiClient.fetch<{ url: string}>(route); if (response.status === 429) { throw new Error('Failed to get crash upload URL, too many requests'); } @@ -77,7 +78,7 @@ export class CrashPostClient { crashType: CrashType, md5: string, processor?: string, - ): Promise { + ): Promise> { const route = '/api/commitS3CrashUpload'; const formData = this._processorApiClient.createFormData(); formData.append('database', database); @@ -91,13 +92,13 @@ export class CrashPostClient { formData.append('processor', processor); } - const init = { + const request = { method: 'POST', body: formData, cache: 'no-cache', redirect: 'follow' - }; + } as RequestInit; - return this._processorApiClient.fetch(route, init); + return this._processorApiClient.fetch(route, request); } } \ No newline at end of file diff --git a/src/post/post-crash-response.ts b/src/post/post-crash-response.ts new file mode 100644 index 0000000..b1c4d87 --- /dev/null +++ b/src/post/post-crash-response.ts @@ -0,0 +1,3 @@ +export interface PostCrashResponse { + crashId: number; +} \ No newline at end of file diff --git a/src/summary/summary-table-data/summary-table-data-client.ts b/src/summary/summary-table-data/summary-table-data-client.ts index 4c4ba39..b212ae3 100644 --- a/src/summary/summary-table-data/summary-table-data-client.ts +++ b/src/summary/summary-table-data/summary-table-data-client.ts @@ -1,4 +1,6 @@ -import { ApiClient, BugSplatResponse, TableDataFormDataBuilder } from '@common'; +import { ApiClient, BugSplatResponse, TableDataFormDataBuilder, TableDataResponse } from '@common'; +import { RawResponse } from 'src/common/data/table-data/table-data-client/table-data-client'; +import { SummaryApiResponseRow } from '../summary-api-row/summary-api-row'; import { SummaryTableDataRequest } from './summary-table-data-request'; export class SummaryTableDataClient { @@ -6,7 +8,7 @@ export class SummaryTableDataClient { constructor(private _apiClient: ApiClient, private _url: string) { } // We use POST to get data in most cases because it supports longer queries - async postGetData(request: SummaryTableDataRequest): Promise { + async postGetData(request: SummaryTableDataRequest): Promise>> { const factory = () => this._apiClient.createFormData(); const formData = new TableDataFormDataBuilder(factory) .withDatabase(request.database) @@ -19,27 +21,30 @@ export class SummaryTableDataClient { .withSortColumn(request.sortColumn) .withSortOrder(request.sortOrder) .build(); - const init = { + const requestInit = { method: 'POST', body: formData, cache: 'no-cache', credentials: 'include', redirect: 'follow' - }; - return this.makeRequest(this._url, init); + } as RequestInit; + return this.makeRequest(this._url, requestInit); } - private async makeRequest(url: string, init: RequestInit): Promise { - const response = await this._apiClient.fetch(url, init); + private async makeRequest(url: string, init: RequestInit): Promise>> { + const response = await this._apiClient.fetch>>(url, init); const responseData = await response.json(); const rows = responseData ? responseData[0]?.Rows : []; const pageData = responseData ? responseData[0]?.PageData : {}; const status = response.status; - const json = async () => ({ rows, pageData }); + const payload = { rows, pageData }; + const json = async () => payload; + const text = async () => JSON.stringify(payload); return { status, - json + json, + text }; } } diff --git a/src/versions/versions-api-client/put-retired-response.ts b/src/versions/versions-api-client/put-retired-response.ts new file mode 100644 index 0000000..d2aeeb6 --- /dev/null +++ b/src/versions/versions-api-client/put-retired-response.ts @@ -0,0 +1,3 @@ +export interface PutRetiredResponse { + retired: 0 | 1; +} \ No newline at end of file diff --git a/src/versions/versions-api-client/versions-api-client.e2e.ts b/src/versions/versions-api-client/versions-api-client.e2e.ts index 2c4ea6f..3ffe50a 100644 --- a/src/versions/versions-api-client/versions-api-client.e2e.ts +++ b/src/versions/versions-api-client/versions-api-client.e2e.ts @@ -60,7 +60,7 @@ describe('VersionsApiClient', () => { describe('putRetired', () => { it('should return 200 for put with valid database, application, and version', async () => { const retired = true; - + const result = await client.putRetired( database, application, diff --git a/src/versions/versions-api-client/versions-api-client.ts b/src/versions/versions-api-client/versions-api-client.ts index 752b780..f532928 100644 --- a/src/versions/versions-api-client/versions-api-client.ts +++ b/src/versions/versions-api-client/versions-api-client.ts @@ -1,6 +1,7 @@ import { ApiClient, UploadableFile, BugSplatResponse, S3ApiClient, TableDataClient, TableDataRequest, TableDataResponse } from '@common'; import { lastValueFrom, timer } from 'rxjs'; -import { VersionsApiRow } from '../versions-api-row/versions-api-row'; +import { VersionsApiResponseRow, VersionsApiRow } from '../versions-api-row/versions-api-row'; +import { PutRetiredResponse } from './put-retired-response'; export class VersionsApiClient { @@ -16,7 +17,7 @@ export class VersionsApiClient { } async getVersions(request: TableDataRequest): Promise> { - const response = await this._tableDataClient.getData(request); + const response = await this._tableDataClient.getData(request); const json = await response.json(); const pageData = json.pageData; const rows = json.rows.map(row => new VersionsApiRow(row)); @@ -35,14 +36,14 @@ export class VersionsApiClient { ): Promise { const fullDumpsFlag = fullDumps ? '1' : '0'; const route = `${this.route}?database=${database}&appName=${application}&appVersion=${version}&fullDumps=${fullDumpsFlag}`; - const init = { + const request = { method: 'PUT', cache: 'no-cache', credentials: 'include', redirect: 'follow' - }; + } as RequestInit; - return this._client.fetch(route, init); + return this._client.fetch(route, request); } async putRetired( @@ -50,17 +51,17 @@ export class VersionsApiClient { application: string, version: string, retired: boolean - ): Promise { + ): Promise> { const retiredFlag = retired ? '1' : '0'; const route = `${this.route}?database=${database}&appName=${application}&appVersion=${version}&retired=${retiredFlag}`; - const init = { + const request = { method: 'PUT', cache: 'no-cache', credentials: 'include', redirect: 'follow' - }; + } as RequestInit; - return this._client.fetch(route, init); + return this._client.fetch(route, request); } async deleteSymbols( @@ -69,19 +70,19 @@ export class VersionsApiClient { version: string ): Promise { const route = `${this.route}?database=${database}&appName=${application}&appVersion=${version}`; - const init = { + const request = { method: 'DELETE', cache: 'no-cache', credentials: 'include', redirect: 'follow' - }; + } as RequestInit; - const response = await this._client.fetch(route, init); + const response = await this._client.fetch(route, request); if (response.status !== 200) { throw new Error(`Error deleting symbols for ${database}-${application}-${version} status ${response.status}`); } - const json = await response.json(); + const json = await response.json() as Response; if (json.Status === 'Failed') { throw new Error(json.Error); } @@ -129,15 +130,15 @@ export class VersionsApiClient { formData.append('appVersion', appVersion); formData.append('size', size.toString()); formData.append('symFileName', symFileName); - const init = { + const request = { method: 'POST', body: formData, cache: 'no-cache', credentials: 'include', redirect: 'follow' - }; + } as RequestInit; - const response = await this._client.fetch(this.route, init); + const response = await this._client.fetch(this.route, request); if (response.status === 429) { throw new Error('Error getting presigned URL, too many requests'); } @@ -150,11 +151,13 @@ export class VersionsApiClient { throw new Error(`Error getting presigned URL for ${symFileName}`); } - const json = await response.json(); + const json = await response.json() as Response & { url?: string }; if (json.Status === 'Failed') { throw new Error(json.Error); } - return json.url; + return json.url as string; } } + +type Response = { Status: string, Error?: string }; \ No newline at end of file