diff --git a/.prettierrc b/.prettierrc index 81710ae..0086a9e 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,7 +4,7 @@ "bracketSpacing": true, "trailingComma": "es5", "printWidth": 100, - "tabWidth": 2, + "tabWidth": 4, "useTabs": false, "endOfLine": "lf" } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 574a10a..82fb1a4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "jasmineExplorer.nodeArgv": [ + "--no-experimental-strip-types", "-r", "ts-node/register", "-r", 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 4fd3d4a..665ee77 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 @@ -55,7 +55,6 @@ describe('OAuthClientCredentialsClient', () => { expect(fakeFormData.append).toHaveBeenCalledWith('grant_type', 'client_credentials'); expect(fakeFormData.append).toHaveBeenCalledWith('client_id', clientId); expect(fakeFormData.append).toHaveBeenCalledWith('client_secret', clientSecret); - expect(fakeFormData.append).toHaveBeenCalledWith('scope', 'restricted'); expect((sut as any)._fetch).toHaveBeenCalledWith( jasmine.anything(), jasmine.objectContaining({ 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 243b2a6..6dc95cb 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 @@ -36,7 +36,6 @@ export class OAuthClientCredentialsClient implements ApiClient { body.append('grant_type', 'client_credentials'); body.append('client_id', this._clientId); body.append('client_secret', this._clientSecret); - body.append('scope', 'restricted'); const request = { method, body, diff --git a/src/common/data/table-data/index.ts b/src/common/data/table-data/index.ts index 90a8a24..31a231c 100644 --- a/src/common/data/table-data/index.ts +++ b/src/common/data/table-data/index.ts @@ -1,4 +1,4 @@ -export { TableDataClient } from './table-data-client/table-data-client'; +export { TableDataClient, isSuccessResponse, isErrorResponse, ErrorResponse } from './table-data-client/table-data-client'; export { TableDataRequest } from './table-data-client/table-data-request'; export { TableDataResponse } from './table-data-client/table-data-response'; export { ColumnSortOrder } from './table-data-form-data-builder/column-sort-order'; diff --git a/src/common/data/table-data/table-data-client/table-data-client.spec.ts b/src/common/data/table-data/table-data-client/table-data-client.spec.ts index 9633e1c..caef205 100644 --- a/src/common/data/table-data/table-data-client/table-data-client.spec.ts +++ b/src/common/data/table-data/table-data-client/table-data-client.spec.ts @@ -1,148 +1,158 @@ import { TableDataClient } from '@common'; import * as TableDataFormDataBuilder from '../table-data-form-data-builder/table-data-form-data-builder'; +import { isErrorResponse } from './table-data-client'; describe('TableDataClient', () => { - let dataTableFormDataBuilder; - let url; - let formData; - let bugSplatApiClient; - let response; - - let service: TableDataClient; - - beforeEach(() => { - url = 'https://woot.com'; - formData = { form: 'data!' }; - response = [{ Rows: [{ yee: 'ha!' }], PageData: { woo: 'hoo!' } }]; - dataTableFormDataBuilder = jasmine.createSpyObj('DataTableFormDataBuilder', [ - 'withDatabase', - 'withColumnGroups', - 'withFilterGroups', - 'withPage', - 'withPageSize', - 'withSortColumn', - 'withSortOrder', - 'build' - ]); - dataTableFormDataBuilder.withDatabase.and.returnValue(dataTableFormDataBuilder); - dataTableFormDataBuilder.withColumnGroups.and.returnValue(dataTableFormDataBuilder); - dataTableFormDataBuilder.withFilterGroups.and.returnValue(dataTableFormDataBuilder); - dataTableFormDataBuilder.withPage.and.returnValue(dataTableFormDataBuilder); - dataTableFormDataBuilder.withPageSize.and.returnValue(dataTableFormDataBuilder); - dataTableFormDataBuilder.withSortColumn.and.returnValue(dataTableFormDataBuilder); - dataTableFormDataBuilder.withSortOrder.and.returnValue(dataTableFormDataBuilder); - dataTableFormDataBuilder.build.and.returnValue(formData); - spyOn(TableDataFormDataBuilder, 'TableDataFormDataBuilder').and.returnValue(dataTableFormDataBuilder); - - const json = () => response; - bugSplatApiClient = jasmine.createSpyObj('BugSplatApiClient', ['fetch']); - bugSplatApiClient.fetch.and.resolveTo({ json }); - - service = new TableDataClient(bugSplatApiClient, url); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - describe('getData', () => { - let database; - let columnGroups; - let filterGroups; - let page; - let pageSize; - let sortColumn; - let sortOrder; - let result; - - beforeEach(async () => { - database = 'Black Rifle Coffee'; - columnGroups = ['group', 'group again']; - filterGroups = ['filter', 'anotha 1']; - page = 9; - pageSize = 9001; - sortColumn = 'sorty'; - sortOrder = 'mcOrder'; - const response = await service.postGetData({ - database, - columnGroups, - filterGroups, - page, - pageSize, - sortColumn, - sortOrder - }); - result = await response.json(); + let dataTableFormDataBuilder; + let url; + let formData; + let bugSplatApiClient; + let response; + + let service: TableDataClient; + + beforeEach(() => { + url = 'https://woot.com'; + formData = { form: 'data!' }; + response = { rows: [{ yee: 'ha!' }], pageData: { woo: 'hoo!' } }; + dataTableFormDataBuilder = jasmine.createSpyObj('DataTableFormDataBuilder', [ + 'withDatabase', + 'withColumnGroups', + 'withFilterGroups', + 'withPage', + 'withPageSize', + 'withSortColumn', + 'withSortOrder', + 'build', + ]); + dataTableFormDataBuilder.withDatabase.and.returnValue(dataTableFormDataBuilder); + dataTableFormDataBuilder.withColumnGroups.and.returnValue(dataTableFormDataBuilder); + dataTableFormDataBuilder.withFilterGroups.and.returnValue(dataTableFormDataBuilder); + dataTableFormDataBuilder.withPage.and.returnValue(dataTableFormDataBuilder); + dataTableFormDataBuilder.withPageSize.and.returnValue(dataTableFormDataBuilder); + dataTableFormDataBuilder.withSortColumn.and.returnValue(dataTableFormDataBuilder); + dataTableFormDataBuilder.withSortOrder.and.returnValue(dataTableFormDataBuilder); + dataTableFormDataBuilder.build.and.returnValue(formData); + spyOn(TableDataFormDataBuilder, 'TableDataFormDataBuilder').and.returnValue( + dataTableFormDataBuilder + ); + + const json = () => response; + bugSplatApiClient = jasmine.createSpyObj('BugSplatApiClient', ['fetch']); + bugSplatApiClient.fetch.and.resolveTo({ status: 200, json }); + + service = new TableDataClient(bugSplatApiClient, url); }); - it('should call DataTaableFormDataBuilder with database', () => { - expect(dataTableFormDataBuilder.withDatabase).toHaveBeenCalledWith(database); + it('should be created', () => { + expect(service).toBeTruthy(); }); - it('should call DatatableFormDataBuilder with columng groups', () => { - expect(dataTableFormDataBuilder.withColumnGroups).toHaveBeenCalledWith(columnGroups); + describe('getData', () => { + let database; + let columnGroups; + let filterGroups; + let page; + let pageSize; + let sortColumn; + let sortOrder; + let result; + + beforeEach(async () => { + database = 'Black Rifle Coffee'; + columnGroups = ['group', 'group again']; + filterGroups = ['filter', 'anotha 1']; + page = 9; + pageSize = 9001; + sortColumn = 'sorty'; + sortOrder = 'mcOrder'; + const response = await service.postGetData({ + database, + columnGroups, + filterGroups, + page, + pageSize, + sortColumn, + sortOrder, + }); + if (isErrorResponse(response)) { + throw new Error((await response.json()).message); + } + result = await response.json(); + }); + + it('should call DataTaableFormDataBuilder with database', () => { + expect(dataTableFormDataBuilder.withDatabase).toHaveBeenCalledWith(database); + }); + + it('should call DatatableFormDataBuilder with columng groups', () => { + expect(dataTableFormDataBuilder.withColumnGroups).toHaveBeenCalledWith(columnGroups); + }); + + it('should call DataTableFormDataBuilder with filter groups', () => { + expect(dataTableFormDataBuilder.withFilterGroups).toHaveBeenCalledWith(filterGroups); + }); + + it('should call DataTableFormDataBuilder with page', () => { + expect(dataTableFormDataBuilder.withPage).toHaveBeenCalledWith(page); + }); + + it('should call DataTableFormDataBuilder with page size', () => { + expect(dataTableFormDataBuilder.withPageSize).toHaveBeenCalledWith(pageSize); + }); + + it('should call DataTableFormDataBuilder with sort column', () => { + expect(dataTableFormDataBuilder.withSortColumn).toHaveBeenCalledWith(sortColumn); + }); + + it('should call DataTableFormDataBuilder with sort order', () => { + expect(dataTableFormDataBuilder.withSortOrder).toHaveBeenCalledWith(sortOrder); + }); + + it('should call build on DataTableFormDataBuilder', () => { + expect(dataTableFormDataBuilder.build).toHaveBeenCalled(); + }); + + it('should call fetch with correct url', () => { + expect(bugSplatApiClient.fetch).toHaveBeenCalledWith(url, jasmine.anything()); + }); + + it('should call fetch with form data from DataTableFormDataBuilder', () => { + expect(bugSplatApiClient.fetch).toHaveBeenCalledWith( + jasmine.anything(), + jasmine.objectContaining({ + body: formData, + }) + ); + }); + + it('should return value from api', () => { + expect(result).toEqual( + jasmine.objectContaining({ + rows: response.rows, + pageData: response.pageData, + }) + ); + }); + + it('should throw if api returns error response', async () => { + const expectedErrorMessage = 'Internal Server Error'; + bugSplatApiClient.fetch.and.resolveTo({ + status: 500, + message: expectedErrorMessage, + }); + try { + await service.postGetData({ + filterGroups, + page, + pageSize, + sortColumn, + sortOrder, + }); + } catch (error: any) { + expect(error).toBeInstanceOf(Error); + expect(error.message).toBe(expectedErrorMessage); + } + }); }); - - it('should call DataTableFormDataBuilder with filter groups', () => { - expect(dataTableFormDataBuilder.withFilterGroups).toHaveBeenCalledWith(filterGroups); - }); - - it('should call DataTableFormDataBuilder with page', () => { - expect(dataTableFormDataBuilder.withPage).toHaveBeenCalledWith(page); - }); - - it('should call DataTableFormDataBuilder with page size', () => { - expect(dataTableFormDataBuilder.withPageSize).toHaveBeenCalledWith(pageSize); - }); - - it('should call DataTableFormDataBuilder with sort column', () => { - expect(dataTableFormDataBuilder.withSortColumn).toHaveBeenCalledWith(sortColumn); - }); - - it('should call DataTableFormDataBuilder with sort order', () => { - expect(dataTableFormDataBuilder.withSortOrder).toHaveBeenCalledWith(sortOrder); - }); - - it('should call build on DataTableFormDataBuilder', () => { - expect(dataTableFormDataBuilder.build).toHaveBeenCalled(); - }); - - it('should call fetch with correct url', () => { - expect(bugSplatApiClient.fetch).toHaveBeenCalledWith(url, jasmine.anything()); - }); - - it('should call fetch with form data from DataTableFormDataBuilder', () => { - expect(bugSplatApiClient.fetch).toHaveBeenCalledWith( - jasmine.anything(), - jasmine.objectContaining({ - body: formData - }) - ); - }); - - it('should return value from api', () => { - expect(result).toEqual(jasmine.objectContaining({ - rows: response[0].Rows, - pageData: response[0].PageData - })); - }); - - it('should return empty array if api returns null', async () => { - bugSplatApiClient.fetch.and.resolveTo({ json: () => null }); - - const response = await service.postGetData({ - filterGroups, - page, - pageSize, - sortColumn, - sortOrder - }); - result = await response.json(); - - expect(result).toEqual(jasmine.objectContaining({ - rows: [], - pageData: {} - })); - }); - }); }); 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 ea91dd2..0bbeb6a 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,97 +1,95 @@ -import { - ApiClient, - TableDataRequest, - BugSplatResponse, - TableDataResponse, -} from '@common'; +import { ApiClient, BugSplatResponse, TableDataRequest, TableDataResponse } from '@common'; import { TableDataFormDataBuilder } from '../table-data-form-data-builder/table-data-form-data-builder'; export class TableDataClient { - constructor(private _apiClient: ApiClient, private _url: string) {} + constructor(private _apiClient: ApiClient, private _url: string) {} - // We use POST to get data in most cases because it supports longer queries - async postGetData | undefined>( - request: TableDataRequest, - formParts: Record = {} - ): Promise>> { - const factory = () => this._apiClient.createFormData(); - const formData = new TableDataFormDataBuilder(factory, formParts) - .withDatabase(request.database) - .withFilterGroups(request.filterGroups) - .withColumnGroups(request.columnGroups) - .withPage(request.page) - .withPageSize(request.pageSize) - .withSortColumn(request.sortColumn) - .withSortOrder(request.sortOrder) - .build(); - const requestInit = { - method: 'POST', - body: formData, - cache: 'no-cache', - credentials: 'include', - redirect: 'follow', - duplex: 'half', - } as RequestInit; - return this.makeRequest(this._url, requestInit); - } + // We use POST to get data in most cases because it supports longer queries + async postGetData | undefined>( + request: TableDataRequest, + formParts: Record = {} + ): Promise> | BugSplatResponse> { + const factory = () => this._apiClient.createFormData(); + const formData = new TableDataFormDataBuilder(factory, formParts) + .withDatabase(request.database) + .withFilterGroups(request.filterGroups) + .withColumnGroups(request.columnGroups) + .withPage(request.page) + .withPageSize(request.pageSize) + .withSortColumn(request.sortColumn) + .withSortOrder(request.sortOrder) + .build(); + const requestInit = { + method: 'POST', + body: formData, + cache: 'no-cache', + credentials: 'include', + redirect: 'follow', + duplex: 'half', + } as RequestInit; + return this.makeRequest(this._url, requestInit); + } - async getData | undefined>( - request: TableDataRequest - ): Promise>> { - const factory = () => this._apiClient.createFormData(); - const formData = new TableDataFormDataBuilder(factory) - .withDatabase(request.database) - .withFilterGroups(request.filterGroups) - .withColumnGroups(request.columnGroups) - .withPage(request.page) - .withPageSize(request.pageSize) - .withSortColumn(request.sortColumn) - .withSortOrder(request.sortOrder) - .entries(); - 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}`, requestInit); - } + async getData | undefined>( + request: TableDataRequest + ): Promise> | BugSplatResponse> { + const factory = () => this._apiClient.createFormData(); + const formData = new TableDataFormDataBuilder(factory) + .withDatabase(request.database) + .withFilterGroups(request.filterGroups) + .withColumnGroups(request.columnGroups) + .withPage(request.page) + .withPageSize(request.pageSize) + .withSortColumn(request.sortColumn) + .withSortOrder(request.sortOrder) + .entries(); + 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}`, requestInit); + } - private async makeRequest( - url: string, - init: RequestInit - ): Promise>> { - const response = await this._apiClient.fetch< - RawResponse> | TableDataResponse - >(url, init); - const responseData = await response.json(); + private async makeRequest( + url: string, + init: RequestInit + ): Promise> | BugSplatResponse> { + const response = await this._apiClient.fetch | ErrorResponse>(url, init); - // Handle legacy and new API responses until we can upgrade legacy APIs - const rows = - (responseData as TableDataResponse)?.rows ?? - responseData?.[0]?.Rows ?? - []; - const pageData = - (responseData as TableDataResponse)?.pageData ?? - responseData?.[0]?.PageData ?? - {}; + if (response.status !== 200) { + return response as unknown as BugSplatResponse; + } - const status = response.status; - const body = response.body; - const payload = { rows, pageData } as TableDataResponse; - const json = async () => payload; - const text = async () => JSON.stringify(payload); - return { - status, - body, - json, - text, - }; - } + const responseData = await response.json() as TableDataResponse; + const rows = responseData.rows || []; + const pageData = responseData.pageData || {}; + const status = response.status; + const body = response.body; + const payload = { rows, pageData } as TableDataResponse; + const json = async () => payload; + const text = async () => JSON.stringify(payload); + return { + status, + body, + 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]; -}>; +export type ErrorResponse = { status: number; message: string }; + +export function isSuccessResponse | undefined>( + response: BugSplatResponse | ErrorResponse> +): response is BugSplatResponse> { + return 'status' in response && response.status === 200; +} + +export function isErrorResponse | undefined>( + response: BugSplatResponse | ErrorResponse> +): response is BugSplatResponse { + return 'status' in response && response.status !== 200; +} diff --git a/src/crash/crash-api-client/crash-api-client.spec.ts b/src/crash/crash-api-client/crash-api-client.spec.ts index dba84d1..b99ba8e 100644 --- a/src/crash/crash-api-client/crash-api-client.spec.ts +++ b/src/crash/crash-api-client/crash-api-client.spec.ts @@ -277,15 +277,14 @@ describe('CrashApiClient', () => { result = await client.postNotes(database, id, notes); }); - it('should append, update true, database, id, and Comments to formData', () => { - expect(fakeFormData.append).toHaveBeenCalledWith('update', 'true'); + it('should append, database, id, and Comments to formData', () => { expect(fakeFormData.append).toHaveBeenCalledWith('database', database); expect(fakeFormData.append).toHaveBeenCalledWith('id', `${id}`); expect(fakeFormData.append).toHaveBeenCalledWith('notes', notes); }); it('should call fetch with correct route', () => { - expect(apiClient.fetch).toHaveBeenCalledWith('/api/crash/notes.php', jasmine.anything()); + expect(apiClient.fetch).toHaveBeenCalledWith('/api/crash/notes', jasmine.anything()); }); it('should call fetch with requestInit containing formData', () => { diff --git a/src/crash/crash-api-client/crash-api-client.ts b/src/crash/crash-api-client/crash-api-client.ts index 20a22d3..eac7c87 100644 --- a/src/crash/crash-api-client/crash-api-client.ts +++ b/src/crash/crash-api-client/crash-api-client.ts @@ -131,7 +131,6 @@ export class CrashApiClient { notes: string ): Promise> { const formData = this._client.createFormData(); - formData.append('update', 'true'); formData.append('database', database); formData.append('id', `${id}`); formData.append('notes', notes); @@ -145,7 +144,7 @@ export class CrashApiClient { duplex: 'half', } as RequestInit; - return this._client.fetch('/api/crash/notes.php', request); + return this._client.fetch('/api/crash/notes', request); } async postStatus( diff --git a/src/crashes/crashes-api-client/crashes-api-client.ts b/src/crashes/crashes-api-client/crashes-api-client.ts index 144f4d1..50391e7 100644 --- a/src/crashes/crashes-api-client/crashes-api-client.ts +++ b/src/crashes/crashes-api-client/crashes-api-client.ts @@ -1,46 +1,52 @@ import { - ApiClient, - BugSplatResponse, - TableDataClient, - TableDataRequest, - TableDataResponse, + ApiClient, + BugSplatResponse, + TableDataClient, + TableDataRequest, + TableDataResponse, + isErrorResponse, } from '@common'; import { CrashesApiRow, CrashesPageData } from '@crashes'; import { CrashesApiResponseRow } from '../crashes-api-row/crashes-api-row'; export class CrashesApiClient { - private _tableDataClient: TableDataClient; + private _tableDataClient: TableDataClient; - constructor(private _client: ApiClient) { - this._tableDataClient = new TableDataClient(this._client, '/api/crashes.php'); - } + constructor(private _client: ApiClient) { + this._tableDataClient = new TableDataClient(this._client, '/api/crashes.php'); + } - async deleteCrashes(database: string, ids: number[]): Promise { - const urlParams = new URLSearchParams({ database, ids: ids.join(',') }); - const request = { - method: 'DELETE', - cache: 'no-cache', - credentials: 'include', - redirect: 'follow', - duplex: 'half', - } as RequestInit; - return this._client.fetch(`/api/crashes.php?${urlParams}`, request); - } + async deleteCrashes(database: string, ids: number[]): Promise { + const urlParams = new URLSearchParams({ database, ids: ids.join(',') }); + const request = { + method: 'DELETE', + cache: 'no-cache', + credentials: 'include', + redirect: 'follow', + duplex: 'half', + } as RequestInit; + return this._client.fetch(`/api/crashes.php?${urlParams}`, request); + } - async getCrashes( - request: TableDataRequest - ): Promise> { - const response = await this._tableDataClient.postGetData< - CrashesApiResponseRow, - CrashesPageData - >(request); - const json = await response.json(); - const pageData = json.pageData; - const rows = json.rows.map((row) => new CrashesApiRow(row)); + async getCrashes( + request: TableDataRequest + ): Promise> { + const response = await this._tableDataClient.postGetData< + CrashesApiResponseRow, + CrashesPageData + >(request); - return { - rows, - pageData, - }; - } + if (isErrorResponse(response)) { + throw new Error((await response.json()).message); + } + + const responseData = await response.json(); + const pageData = responseData.pageData; + const rows = responseData.rows.map((row) => new CrashesApiRow(row)); + + return { + rows, + pageData, + }; + } } diff --git a/src/crashes/key-crash-api-client/key-crash-api-client.ts b/src/crashes/key-crash-api-client/key-crash-api-client.ts index d04a907..8e19c53 100644 --- a/src/crashes/key-crash-api-client/key-crash-api-client.ts +++ b/src/crashes/key-crash-api-client/key-crash-api-client.ts @@ -1,4 +1,4 @@ -import { ApiClient, BugSplatResponse, TableDataClient, TableDataResponse } from '@common'; +import { ApiClient, BugSplatResponse, TableDataClient, TableDataResponse, isErrorResponse } from '@common'; import { CrashesApiRow } from '@crashes'; import { CrashesApiResponseRow } from '../crashes-api-row/crashes-api-row'; import { createKeyCrashPageData, KeyCrashPageData, KeyCrashPageDataRawResponse } from './key-crash-page-data'; @@ -9,12 +9,15 @@ export class KeyCrashApiClient { private _tableDataClient: TableDataClient; constructor(private _client: ApiClient) { - this._tableDataClient = new TableDataClient(this._client, '/keycrash?data&crashTimeSpan'); + this._tableDataClient = new TableDataClient(this._client, '/api/v2/keycrash'); } async getCrashes(request: KeyCrashTableDataRequest): Promise> { const formParts = { stackKeyId: `${request.stackKeyId}` }; const response = await this._tableDataClient.postGetData(request, formParts); + if (isErrorResponse(response)) { + throw new Error((await response.json()).message); + } const json = await response.json() as Required>; const pageData = createKeyCrashPageData(json.pageData); const rows = json.rows.map(row => new CrashesApiRow(row)); diff --git a/src/summary/summary-api-client/summary-api-client.ts b/src/summary/summary-api-client/summary-api-client.ts index 24f0eb7..20ef459 100644 --- a/src/summary/summary-api-client/summary-api-client.ts +++ b/src/summary/summary-api-client/summary-api-client.ts @@ -1,9 +1,8 @@ -import { ApiClient, TableDataClient, TableDataResponse } from '@common'; +import { ApiClient, TableDataClient, TableDataResponse, isErrorResponse } from '@common'; import { SummaryApiResponseRow, SummaryApiRow } from '../summary-api-row/summary-api-row'; import { SummaryTableDataRequest } from '../summary-table-data/summary-table-data-request'; export class SummaryApiClient { - private _tableDataClient: TableDataClient; constructor(private _client: ApiClient) { @@ -18,29 +17,40 @@ export class SummaryApiClient { if (request.versions && request.versions.length) { formParts['versions'] = request.versions.join(','); } - const response = await this._tableDataClient.postGetData(request, formParts); - const json = await response.json(); - const pageData = json.pageData; - const rows = json.rows.map(row => new SummaryApiRow( - row.stackKey, - Number(row.stackKeyId), - row.firstReport, - row.lastReport, - row.crashSum, - row.techSupportSubject, - row.stackKeyDefectId, - row.stackKeyDefectUrl, - row.stackKeyDefectLabel, - row.comments, - Number(row.subKeyDepth), - Number(row.userSum), - Number(row.status) - ) + + const response = await this._tableDataClient.postGetData( + request, + formParts + ); + + if (isErrorResponse(response)) { + throw new Error((await response.json()).message); + } + + const responseData = await response.json(); + const pageData = responseData.pageData || {}; + const rows = responseData.rows.map( + (row) => + new SummaryApiRow( + row.stackKey, + Number(row.stackKeyId), + row.firstReport, + row.lastReport, + row.crashSum, + row.techSupportSubject, + row.stackKeyDefectId, + row.stackKeyDefectUrl, + row.stackKeyDefectLabel, + row.comments, + Number(row.subKeyDepth), + Number(row.userSum), + Number(row.status) + ) ); return { rows, - pageData + pageData, }; } } diff --git a/src/versions/versions-api-client/versions-api-client.spec.ts b/src/versions/versions-api-client/versions-api-client.spec.ts index f7e0e04..c711a00 100644 --- a/src/versions/versions-api-client/versions-api-client.spec.ts +++ b/src/versions/versions-api-client/versions-api-client.spec.ts @@ -85,7 +85,7 @@ describe('VersionsApiClient', () => { await versionsApiClient.deleteVersions(database, appVersions); expect(fakeBugSplatApiClient.fetch).toHaveBeenCalledWith( - `/api/versions?database=${database}&appVersions=${appVersions[0].application},${appVersions[0].version},${appVersions[1].application},${appVersions[1].version}`, + `/api/v2/versions?database=${database}&appVersions=${appVersions[0].application},${appVersions[0].version},${appVersions[1].application},${appVersions[1].version}`, jasmine.anything() ); }); @@ -116,7 +116,7 @@ describe('VersionsApiClient', () => { it('should call fetch with route containing database, application, version, and fullDumps', () => { expect(fakeBugSplatApiClient.fetch).toHaveBeenCalledWith( - `/api/versions?database=${database}&appName=${application}&appVersion=${version}&fullDumps=${fullDumps ? 1 : 0}`, + `/api/v2/versions?database=${database}&appName=${application}&appVersion=${version}&fullDumps=${fullDumps ? 1 : 0}`, jasmine.anything() ); }); @@ -144,7 +144,7 @@ describe('VersionsApiClient', () => { it('should call fetch with route containing database, application, version, and fullDumps', () => { expect(fakeBugSplatApiClient.fetch).toHaveBeenCalledWith( - `/api/versions?database=${database}&appName=${application}&appVersion=${version}&retired=${retired ? 1 : 0}`, + `/api/v2/versions?database=${database}&appName=${application}&appVersion=${version}&retired=${retired ? 1 : 0}`, jasmine.anything() ); }); @@ -175,7 +175,7 @@ describe('VersionsApiClient', () => { it('should call fetch with route containing database, application and version', () => { expect(fakeBugSplatApiClient.fetch).toHaveBeenCalledWith( - `/api/versions?database=${database}&appName=${application}&appVersion=${version}`, + `/api/v2/versions?database=${database}&appName=${application}&appVersion=${version}`, jasmine.anything() ); }); @@ -252,7 +252,7 @@ describe('VersionsApiClient', () => { it('should call fetch with correct route', () => { expect(fakeBugSplatApiClient.fetch).toHaveBeenCalledWith( - '/api/versions', + '/api/v2/versions', jasmine.anything() ); }); diff --git a/src/versions/versions-api-client/versions-api-client.ts b/src/versions/versions-api-client/versions-api-client.ts index 664be71..4218834 100644 --- a/src/versions/versions-api-client/versions-api-client.ts +++ b/src/versions/versions-api-client/versions-api-client.ts @@ -1,11 +1,19 @@ -import { ApiClient, BugSplatResponse, S3ApiClient, SymbolFile, TableDataClient, TableDataRequest, TableDataResponse } from '@common'; +import { + ApiClient, + BugSplatResponse, + S3ApiClient, + SymbolFile, + TableDataClient, + TableDataRequest, + TableDataResponse, + isErrorResponse, +} from '@common'; import { delay } from '../../common/delay'; import { VersionsApiResponseRow, VersionsApiRow } from '../versions-api-row/versions-api-row'; import { PutRetiredResponse } from './put-retired-response'; export class VersionsApiClient { - - private readonly route = '/api/versions'; + private readonly route = '/api/v2/versions'; private _s3ApiClient = new S3ApiClient(); private _tableDataClient: TableDataClient; @@ -17,32 +25,42 @@ export class VersionsApiClient { async getVersions(request: TableDataRequest): Promise> { const response = await this._tableDataClient.getData(request); + if (isErrorResponse(response)) { + throw new Error((await response.json()).message); + } const json = await response.json(); const pageData = json.pageData; - const rows = json.rows.map(row => new VersionsApiRow(row)); + const rows = json.rows.map((row) => new VersionsApiRow(row)); return { rows, - pageData + pageData, }; } - async deleteVersions(database: string, appVersions: Array<{ application: string, version: string }>): Promise { - const appVersionsParam = appVersions.reduce((prev, curr) => [...prev, curr.application, curr.version], [] as Array).join(','); + async deleteVersions( + database: string, + appVersions: Array<{ application: string; version: string }> + ): Promise { + const appVersionsParam = appVersions + .reduce((prev, curr) => [...prev, curr.application, curr.version], [] as Array) + .join(','); const route = `${this.route}?database=${database}&appVersions=${appVersionsParam}`; const request = { method: 'DELETE', cache: 'no-cache', credentials: 'include', - redirect: 'follow' + redirect: 'follow', } as RequestInit; const response = await this._client.fetch(route, request); if (response.status !== 200) { - throw new Error(`Error deleting symbols for ${database}-${appVersionsParam} status ${response.status}`); + throw new Error( + `Error deleting symbols for ${database}-${appVersionsParam} status ${response.status}` + ); } - const json = await response.json() as Response; + const json = (await response.json()) as Response; if (json.Status === 'Failed') { throw new Error(json.Error); } @@ -62,7 +80,7 @@ export class VersionsApiClient { method: 'PUT', cache: 'no-cache', credentials: 'include', - redirect: 'follow' + redirect: 'follow', } as RequestInit; return this._client.fetch(route, request); @@ -80,7 +98,7 @@ export class VersionsApiClient { method: 'PUT', cache: 'no-cache', credentials: 'include', - redirect: 'follow' + redirect: 'follow', } as RequestInit; return this._client.fetch(route, request); @@ -96,15 +114,17 @@ export class VersionsApiClient { method: 'DELETE', cache: 'no-cache', credentials: 'include', - redirect: 'follow' + redirect: 'follow', } as RequestInit; 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}`); + throw new Error( + `Error deleting symbols for ${database}-${application}-${version} status ${response.status}` + ); } - const json = await response.json() as Response; + const json = (await response.json()) as Response; if (json.Status === 'Failed') { throw new Error(json.Error); } @@ -118,20 +138,14 @@ export class VersionsApiClient { version: string, files: Array ): Promise> { - const promises = files - .map(async (file) => { - const presignedUrl = await this.getPresignedUrl( - database, - application, - version, - file - ); + const promises = files.map(async (file) => { + const presignedUrl = await this.getPresignedUrl(database, application, version, file); - const response = await this._s3ApiClient.uploadFileToPresignedUrl(presignedUrl, file); - await this._timer(1000); + const response = await this._s3ApiClient.uploadFileToPresignedUrl(presignedUrl, file); + await this._timer(1000); - return response; - }); + return response; + }); return Promise.all(promises); } @@ -167,7 +181,7 @@ export class VersionsApiClient { cache: 'no-cache', credentials: 'include', redirect: 'follow', - duplex: 'half' + duplex: 'half', } as RequestInit; const response = await this._client.fetch(this.route, request); @@ -183,7 +197,7 @@ export class VersionsApiClient { throw new Error(`Error getting presigned URL for ${file.name}`); } - const json = await response.json() as Response & { url?: string }; + const json = (await response.json()) as Response & { url?: string }; if (json.Status === 'Failed') { throw new Error(json.Error); } @@ -192,4 +206,4 @@ export class VersionsApiClient { } } -type Response = { Status: string, Error?: string }; \ No newline at end of file +type Response = { Status: string; Error?: string };