// tslint:disable import fetch from 'node-fetch'; import { RequestInit, Response } from 'node-fetch'; import AbortController from 'abort-controller'; import { createReturnTypeValidator, ClassValidator, ValidationError } from './common'; import { schema, InternalServerError, UnauthorizedError, NotFoundError, ExistsError, Session, ServerOnlyContext, SubDomain, CNameDomain, TemplateSource, Environment, Application, Template, ExternalAccount, User, InvocationDetails, LogRecord, LogReadOpts, LogsResponse, CreateTicketResponse, TryTemplateResponse, TemplateData, InitialData, Lycan, InteractionEvent, ViewEvent, FunctionRunnerSize, Domain, Serializable, } from './interfaces'; export interface Options extends Pick { fetchImplementation?: typeof fetch; timeoutMs?: number; headers?: Record; } export class RequestError extends Error { public readonly name = 'RequestError'; constructor( message: string, /** * The original error causing this request to fail * Inherits Error in case of network or parse errors * In case of an invalid HTTP response it will contain an object with the body/trimmed text of the response */ public readonly cause: any, public readonly method: string, public readonly options: any ) { super(message); } } export class TimeoutError extends Error { public readonly name = 'TimeoutError'; constructor(message: string, public readonly method: string, public readonly options: any) { super(message); } } export { ValidationError, }; export interface Lycan { createTicket(): Promise; claimTicket(ticket: string): Promise; listTemplates(): Promise>; reportAnalytics(events: Array<(InteractionEvent) | (ViewEvent)>): Promise; tryTemplate(id: string): Promise; whoami(): Promise; listApps(): Promise>; getApp(id: string): Promise; getAppByName(name: string): Promise; deployInitial(env: string, digest: string, envVars: Array<[string, string]>): Promise; deploy(appId: string, env: string, digest: string, envVars: Array<[string, string]>): Promise; claimApp(token: string): Promise; getLogs(appId: string, env: string, opts: LogReadOpts): Promise; getLogsByName(name: string, env: string, opts: LogReadOpts): Promise; destroyApp(appId: string): Promise; destroyAppByName(name: string): Promise; } export class LycanClient { public static readonly methods = [ 'createTicket', 'claimTicket', 'listTemplates', 'reportAnalytics', 'tryTemplate', 'whoami', 'listApps', 'getApp', 'getAppByName', 'deployInitial', 'deploy', 'claimApp', 'getLogs', 'getLogsByName', 'destroyApp', 'destroyAppByName', ]; public static readonly validators: ClassValidator = createReturnTypeValidator(schema, 'Lycan'); protected readonly props = schema.definitions.Lycan.properties; public readonly validators: ClassValidator; // We don't have class name in method scope because mustache sux public constructor(public readonly serverUrl: string, protected readonly options: Options = {}) { this.validators = LycanClient.validators; } public async createTicket(options?: Options): Promise { const body = { }; const mergedOptions = { serverUrl: this.serverUrl, ...this.options, ...options, }; const { fetchImplementation, timeoutMs, headers, serverUrl, ...fetchOptions } = mergedOptions; const fetchImpl = fetchImplementation || fetch; let timeout: NodeJS.Timeout | undefined; if (timeoutMs) { const controller = new AbortController(); timeout = setTimeout(() => controller.abort(), timeoutMs); (fetchOptions as any).signal = controller.signal; } let response: Response; let responseBody: any; let responseText: string | undefined; let isJSON: boolean; try { response = await fetchImpl(`${serverUrl}/createTicket`, { ...fetchOptions, headers: { ...headers, 'Content-Type': 'application/json', }, body: JSON.stringify(body), method: 'POST', }); isJSON = (response.headers.get('content-type') || '').startsWith('application/json'); if (isJSON) { responseBody = await response.json(); } else { responseText = await response.text(); } } catch (err) { if (err.message === 'The user aborted a request.') { timeout = undefined; throw new TimeoutError('Request aborted due to timeout', 'createTicket', mergedOptions); } throw new RequestError(err.message, err, 'createTicket', mergedOptions); } finally { if (timeout) clearTimeout(timeout); } if (response.status >= 200 && response.status < 300) { const validator = this.validators.createTicket; const wrapped = { returns: responseBody }; // wrapped for coersion if (!validator(wrapped)) { throw new ValidationError('Failed to validate response', validator.errors); } return wrapped.returns as CreateTicketResponse; } else if (!isJSON) { // fall through to throw } else if (response.status === 400) { if (responseBody.name === 'ValidationError') { throw new ValidationError(responseBody.message, responseBody.errors); } } else if (response.status === 500) { throw new InternalServerError(responseBody.message); } throw new RequestError(`${response.status} - ${response.statusText}`, { responseText: responseText && responseText.slice(0, 256), responseBody }, 'createTicket', mergedOptions); } public async claimTicket(ticket: string, options?: Options): Promise { const body = { ticket, }; const mergedOptions = { serverUrl: this.serverUrl, ...this.options, ...options, }; const { fetchImplementation, timeoutMs, headers, serverUrl, ...fetchOptions } = mergedOptions; const fetchImpl = fetchImplementation || fetch; let timeout: NodeJS.Timeout | undefined; if (timeoutMs) { const controller = new AbortController(); timeout = setTimeout(() => controller.abort(), timeoutMs); (fetchOptions as any).signal = controller.signal; } let response: Response; let responseBody: any; let responseText: string | undefined; let isJSON: boolean; try { response = await fetchImpl(`${serverUrl}/claimTicket`, { ...fetchOptions, headers: { ...headers, 'Content-Type': 'application/json', }, body: JSON.stringify(body), method: 'POST', }); isJSON = (response.headers.get('content-type') || '').startsWith('application/json'); if (isJSON) { responseBody = await response.json(); } else { responseText = await response.text(); } } catch (err) { if (err.message === 'The user aborted a request.') { timeout = undefined; throw new TimeoutError('Request aborted due to timeout', 'claimTicket', mergedOptions); } throw new RequestError(err.message, err, 'claimTicket', mergedOptions); } finally { if (timeout) clearTimeout(timeout); } if (response.status >= 200 && response.status < 300) { const validator = this.validators.claimTicket; const wrapped = { returns: responseBody }; // wrapped for coersion if (!validator(wrapped)) { throw new ValidationError('Failed to validate response', validator.errors); } return wrapped.returns as string; } else if (!isJSON) { // fall through to throw } else if (response.status === 400) { if (responseBody.name === 'ValidationError') { throw new ValidationError(responseBody.message, responseBody.errors); } } else if (response.status === 500) { if (responseBody.name === 'NotFoundError') { throw new NotFoundError(responseBody.message); } throw new InternalServerError(responseBody.message); } throw new RequestError(`${response.status} - ${response.statusText}`, { responseText: responseText && responseText.slice(0, 256), responseBody }, 'claimTicket', mergedOptions); } public async listTemplates(options?: Options): Promise> { const body = { }; const mergedOptions = { serverUrl: this.serverUrl, ...this.options, ...options, }; const { fetchImplementation, timeoutMs, headers, serverUrl, ...fetchOptions } = mergedOptions; const fetchImpl = fetchImplementation || fetch; let timeout: NodeJS.Timeout | undefined; if (timeoutMs) { const controller = new AbortController(); timeout = setTimeout(() => controller.abort(), timeoutMs); (fetchOptions as any).signal = controller.signal; } let response: Response; let responseBody: any; let responseText: string | undefined; let isJSON: boolean; try { response = await fetchImpl(`${serverUrl}/listTemplates`, { ...fetchOptions, headers: { ...headers, 'Content-Type': 'application/json', }, body: JSON.stringify(body), method: 'POST', }); isJSON = (response.headers.get('content-type') || '').startsWith('application/json'); if (isJSON) { responseBody = await response.json(); } else { responseText = await response.text(); } } catch (err) { if (err.message === 'The user aborted a request.') { timeout = undefined; throw new TimeoutError('Request aborted due to timeout', 'listTemplates', mergedOptions); } throw new RequestError(err.message, err, 'listTemplates', mergedOptions); } finally { if (timeout) clearTimeout(timeout); } if (response.status >= 200 && response.status < 300) { const validator = this.validators.listTemplates; const wrapped = { returns: responseBody }; // wrapped for coersion if (!validator(wrapped)) { throw new ValidationError('Failed to validate response', validator.errors); } return wrapped.returns as Array