- Fixed Angular client

This commit is contained in:
Ferdi Koomen 2022-01-28 00:19:01 +01:00
parent dc5e69abca
commit 8e05e64a72
13 changed files with 229 additions and 84 deletions

View File

@ -33,7 +33,7 @@ const config: Config.InitialOptions = {
'<rootDir>/test/e2e/client.axios.spec.ts',
'<rootDir>/test/e2e/client.babel.spec.ts',
],
modulePathIgnorePatterns: ['<rootDir>/test/e2e/generated'],
modulePathIgnorePatterns: ['<rootDir>/test/e2e/generated'],
},
],
collectCoverageFrom: ['<rootDir>/src/**/*.ts', '!<rootDir>/src/**/*.d.ts', '!<rootDir>/bin', '!<rootDir>/dist'],

View File

@ -51,7 +51,7 @@
"test:update": "jest --selectProjects UNIT --updateSnapshot",
"test:watch": "jest --selectProjects UNIT --watch",
"test:coverage": "jest --selectProjects UNIT --coverage",
"test:e2e": "jest --selectProjects E2E --runInBand",
"test:e2e": "jest --selectProjects E2E --runInBand --verbose",
"eslint": "eslint .",
"eslint:fix": "eslint . --fix",
"prepublishOnly": "yarn run clean && yarn run release",

View File

@ -1,40 +1,45 @@
const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise<HttpHeaders> => {
const token = await resolve(options, config.TOKEN);
const username = await resolve(options, config.USERNAME);
const password = await resolve(options, config.PASSWORD);
const additionalHeaders = await resolve(options, config.HEADERS);
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
const getHeaders = (config: OpenAPIConfig, options: ApiRequestOptions): Observable<HttpHeaders> => {
return forkJoin({
token: resolve(options, config.TOKEN),
username: resolve(options, config.USERNAME),
password: resolve(options, config.PASSWORD),
additionalHeaders: resolve(options, config.HEADERS),
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
.pipe(
map(({ token, username, password, additionalHeaders }) => {
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
}
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
if (options.body) {
if (options.mediaType) {
headers['Content-Type'] = options.mediaType;
} else if (isBlob(options.body)) {
headers['Content-Type'] = options.body.type || 'application/octet-stream';
} else if (isString(options.body)) {
headers['Content-Type'] = 'text/plain';
} else if (!isFormData(options.body)) {
headers['Content-Type'] = 'application/json';
}
}
if (options.body) {
if (options.mediaType) {
headers['Content-Type'] = options.mediaType;
} else if (isBlob(options.body)) {
headers['Content-Type'] = options.body.type || 'application/octet-stream';
} else if (isString(options.body)) {
headers['Content-Type'] = 'text/plain';
} else if (!isFormData(options.body)) {
headers['Content-Type'] = 'application/json';
}
}
return new HttpHeaders(headers);
return new HttpHeaders(headers);
})
);
};

View File

@ -1,8 +1,9 @@
{{>header}}
import { HttpClient, HttpHeaders } from '@angular/common/http';
import type { HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import type { HttpResponse, HttpErrorResponse } from '@angular/common/http';
import { catchError, forkJoin, map, mergeMap, of, throwError } from 'rxjs';
import type { Observable } from 'rxjs';
import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
@ -54,7 +55,7 @@ import type { OpenAPIConfig } from './OpenAPI';
{{>angular/getResponseBody}}
{{>functions/catchErrors}}
{{>functions/catchErrorCodes}}
/**
@ -62,38 +63,47 @@ import type { OpenAPIConfig } from './OpenAPI';
* @param config The OpenAPI configuration object
* @param http The Angular HTTP client
* @param options The request options from the service
* @returns CancelablePromise<T>
* @returns Observable<T>
* @throws ApiError
*/
export const request = <T>(config: OpenAPIConfig, http: HttpClient, options: ApiRequestOptions): Observable<T> => {
return new Observable<T>(subscriber => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
getHeaders(config, options).then(headers => {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
sendRequest<T>(config, options, http, url, formData, body, headers)
.subscribe(response => {
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
subscriber.next(result.body);
});
});
} catch (error) {
subscriber.error(error);
}
});
return getHeaders(config, options).pipe(
mergeMap( headers => {
return sendRequest<T>(config, options, http, url, formData, body, headers);
}),
map(response => {
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
return {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
} as ApiResult;
}),
catchError((error: HttpErrorResponse) => {
if (!error.status) {
return throwError(() => error);
}
return of({
url,
ok: error.ok,
status: error.status,
statusText: error.statusText,
body: error.statusText,
} as ApiResult);
}),
map(result => {
catchErrorCodes(options, result);
return result.body as T;
}),
catchError((error: ApiError) => {
return throwError(() => error);
}),
);
};

View File

@ -55,7 +55,7 @@ import type { OpenAPIConfig } from './OpenAPI';
{{>axios/getResponseBody}}
{{>functions/catchErrors}}
{{>functions/catchErrorCodes}}
/**
@ -86,7 +86,7 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
catchErrorCodes(options, result);
resolve(result.body);
}

View File

@ -52,7 +52,7 @@ import type { OpenAPIConfig } from './OpenAPI';
{{>fetch/getResponseBody}}
{{>functions/catchErrors}}
{{>functions/catchErrorCodes}}
/**
@ -83,7 +83,7 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
catchErrorCodes(options, result);
resolve(result.body);
}

View File

@ -1,4 +1,4 @@
const catchErrors = (options: ApiRequestOptions, result: ApiResult): void => {
const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',

View File

@ -56,7 +56,7 @@ import type { OpenAPIConfig } from './OpenAPI';
{{>node/getResponseBody}}
{{>functions/catchErrors}}
{{>functions/catchErrorCodes}}
/**
@ -87,7 +87,7 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
catchErrorCodes(options, result);
resolve(result.body);
}

View File

@ -55,7 +55,7 @@ import type { OpenAPIConfig } from './OpenAPI';
{{>xhr/getResponseBody}}
{{>functions/catchErrors}}
{{>functions/catchErrorCodes}}
/**
@ -86,7 +86,7 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
catchErrorCodes(options, result);
resolve(result.body);
}

View File

@ -26,7 +26,7 @@ import fetchGetResponseHeader from '../templates/core/fetch/getResponseHeader.hb
import fetchRequest from '../templates/core/fetch/request.hbs';
import fetchSendRequest from '../templates/core/fetch/sendRequest.hbs';
import functionBase64 from '../templates/core/functions/base64.hbs';
import functionCatchErrors from '../templates/core/functions/catchErrors.hbs';
import functionCatchErrorCodes from '../templates/core/functions/catchErrorCodes.hbs';
import functionGetFormData from '../templates/core/functions/getFormData.hbs';
import functionGetQueryString from '../templates/core/functions/getQueryString.hbs';
import functionGetUrl from '../templates/core/functions/getUrl.hbs';
@ -167,7 +167,7 @@ export const registerHandlebarTemplates = (root: {
Handlebars.registerPartial('base', Handlebars.template(partialBase));
// Generic functions used in 'request' file @see src/templates/core/request.hbs for more info
Handlebars.registerPartial('functions/catchErrors', Handlebars.template(functionCatchErrors));
Handlebars.registerPartial('functions/catchErrorCodes', Handlebars.template(functionCatchErrorCodes));
Handlebars.registerPartial('functions/getFormData', Handlebars.template(functionGetFormData));
Handlebars.registerPartial('functions/getQueryString', Handlebars.template(functionGetQueryString));
Handlebars.registerPartial('functions/getUrl', Handlebars.template(functionGetUrl));

View File

@ -469,7 +469,7 @@ const getResponseBody = async (response: Response): Promise<any> => {
return;
};
const catchErrors = (options: ApiRequestOptions, result: ApiResult): void => {
const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',
@ -519,7 +519,7 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
catchErrorCodes(options, result);
resolve(result.body);
}
@ -3370,7 +3370,7 @@ const getResponseBody = async (response: Response): Promise<any> => {
return;
};
const catchErrors = (options: ApiRequestOptions, result: ApiResult): void => {
const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',
@ -3420,7 +3420,7 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
catchErrorCodes(options, result);
resolve(result.body);
}

View File

@ -51,4 +51,68 @@ describe('v2.angular', () => {
});
expect(result).toBeDefined();
});
it('should throw known error (500)', async () => {
const error = await browser.evaluate(async () => {
try {
await new Promise<any>((resolve, reject) => {
const { ErrorService } = (window as any).api;
ErrorService.testErrorCode(500).subscribe(resolve, reject);
});
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Custom message: Internal Server Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: 'Internal Server Error',
})
);
});
it('should throw unknown error (409)', async () => {
const error = await browser.evaluate(async () => {
try {
await new Promise<any>((resolve, reject) => {
const { ErrorService } = (window as any).api;
ErrorService.testErrorCode(409).subscribe(resolve, reject);
});
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Generic Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: 'Conflict',
})
);
});
});

View File

@ -83,4 +83,70 @@ describe('v3.angular', () => {
});
expect(result).toBeDefined();
});
it('should throw known error (500)', async () => {
const error = await browser.evaluate(async () => {
try {
await new Promise<any>((resolve, reject) => {
const { ErrorService } = (window as any).api;
ErrorService.testErrorCode(500).subscribe(resolve, reject);
});
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Custom message: Internal Server Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: 'Internal Server Error',
})
);
});
it('should throw unknown error (409)', async () => {
const error = await browser.evaluate(async () => {
const { ErrorService } = (window as any).api;
ErrorService.testErrorCode(409).subscribe(console.log, console.log);
try {
await new Promise<any>((resolve, reject) => {
// const { ErrorService } = (window as any).api;
ErrorService.testErrorCode(409).subscribe(resolve, reject);
});
} catch (e) {
const error = e as any;
return JSON.stringify({
name: error.name,
message: error.message,
url: error.url,
status: error.status,
statusText: error.statusText,
body: error.body,
});
}
return;
});
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Generic Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: 'Conflict',
})
);
});
});