UNPKG

iopa

Version:

API-first, Internet of Things (IoT) stack for Typescript, official implementation of the Internet Open Protocols Alliance (IOPA) reference pattern

384 lines (338 loc) 10.6 kB
/* * Internet Open Protocol Abstraction (IOPA) * Copyright (c) 2016-2022 Internet Open Protocols Alliance * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import type { IReplyIopaLegacy, IContextIopa, IReplyIopa, ICookieOptions, HandlerResult } from '@iopa/types' import { isAbsoluteURL } from '../util/url' import { StatusCode, getStatusText } from '../util/status' import { VERSION } from './constants' import { ContextCore } from './context-core' import cookies from './context-edge-cookies' import IopaMap from './map' // eslint-disable-next-line @rushstack/typedef-var const kRequest = Symbol('request') // eslint-disable-next-line @rushstack/typedef-var const kRequestClone = Symbol('requestClone') // eslint-disable-next-line @rushstack/typedef-var const kHeaders = Symbol('headers') // eslint-disable-next-line @rushstack/typedef-var const kIsFinalized = Symbol('is_finalized') // eslint-disable-next-line @rushstack/typedef-var const kResponse = Symbol('response') // eslint-disable-next-line @rushstack/typedef-var const kStatusCode = Symbol('statusCode') /** Represents IOPA Context object for any State Flow or REST Request/Response */ export class ContextEdge extends ContextCore<any> implements IContextIopa<any> { protected [kRequest]: Request protected [kHeaders]: Headers public notFound?: () => Response public response: IReplyIopa public cookies?: Record<string, any> public signedCookies?: Record<string, any> public secret?: string // URL HELPERS /** Initialize blank IopaContext object; Generic properties common to all server types included */ public init(): this { super.init() this.set('iopa.Headers', new Headers()) return this } public get body(): ReadableStream<Uint8Array> | null { return this[kRequest].body } public get headers(): Headers { return this[kRequest] ? this[kRequest].headers : this[kHeaders] } public get url(): string { return this['iopa.OriginalUrl'] } public get signal(): AbortSignal { return this[kRequest] ? this[kRequest].signal : this.get('server.AbortSignal') } public get bodyUsed(): boolean { return this[kRequest].bodyUsed } public get method(): string { return this[kRequest].method } public get 'iopa.Method'(): string { return this[kRequest].method } public set 'iopa.Method'(value: string) { /** noop */ } public get 'iopa.Headers'(): Headers { return this[kRequest] ? this[kRequest].headers : this[kHeaders] } public set 'iopa.Headers'(headers: Headers) { this[kHeaders] = headers } public get 'iopa.RawRequest'(): Request { return this[kRequest] } public set 'iopa.RawRequest'(request: Request) { this[kRequest] = request } public get 'iopa.RawRequestClone'(): Request { return this[kRequestClone] } public set 'iopa.RawRequestClone'(request: Request) { this[kRequestClone] = request } public param(): Record<string, string> public param(key: string): string | undefined public param(key?: string): string | Record<string, string> | undefined { const params = this.get('iopa.Params') || {} if (params) { if (key) { return params[key] } else { return params } } return undefined } public query(): Record<string, string> public query(key: string): string | undefined public query(key?: string): string | Record<string, string> | undefined { const params = Object.fromEntries( this.get('iopa.Url').searchParams.entries() ) if (params) { if (key) { return params[key] } else { return params } } return undefined } public async blob(): Promise<Blob> { return this[kRequest].blob() } public async arrayBuffer(): Promise<ArrayBuffer> { return this[kRequest].arrayBuffer() } public async text(): Promise<string> { return this[kRequest].text() } public async json(): Promise<any> { return this[kRequest].json() } public async formData(): Promise<FormData> { return this[kRequest].formData() } public redirect(location: any, code?: number): this { if (typeof location !== 'string') { throw new TypeError('location must be a string!') } if (!isAbsoluteURL(location)) { const url = this.get('iopa.Url') url.pathname = location location = url.toString() } this.response.status(code || 302).headers.set('location', location) return this } public respondWith( res: HandlerResult, options?: number | { status?: number; headers?: Record<string, string> } ): this { if (res instanceof Error || !this.response[kIsFinalized]) { this.response.respondWith(res, options) } return this } } /** Represents IOPA Context object for any State Flow or REST Request/Response */ export class ContextReply extends IopaMap<IReplyIopaLegacy> implements IReplyIopa { protected [kHeaders]: Headers protected [kStatusCode]: number public 'iopa.Version': string protected [kResponse]: Response | PromiseLike<Response> protected [kIsFinalized]: boolean = false public get 'iopa.StatusCode'(): number { return this[kStatusCode] || 200 } public set 'iopa.StatusCode'(value: number) { this[kStatusCode] = value } public get headers(): Headers { return this[kHeaders] } public get redirected(): boolean { const status = this.get('iopa.StatusCode') return status === 302 || status === 301 } public get ok(): boolean { const status = this.get('iopa.StatusCode') return status < 300 } public get statusText(): string { return this.get('iopa.StatusText') } public get 'iopa.Headers'(): Headers { return this[kHeaders] } public set 'iopa.Headers'(headers: Headers) { this[kHeaders] = headers } public get 'iopa.IsFinalized'(): boolean { return this[kIsFinalized] } public get 'iopa.RawResponse'(): Response | PromiseLike<Response> { if (this[kIsFinalized]) { return this[kResponse] } else { return new Response(this.get('iopa.Body'), { headers: this.get('iopa.Headers'), status: this.get('iopa.StatusCode'), statusText: this.get('iopa.StatusText') || getStatusText(this.get('iopa.StatusCode') as StatusCode) || undefined }) } } public get 'iopa.RawResponseClone'(): Response | PromiseLike<Response> { if (this[kIsFinalized]) { return Promise.resolve(this[kResponse]).then((x) => x.clone()) } else { return new Response(this.get('iopa.Body'), { headers: this.get('iopa.Headers'), status: this.get('iopa.StatusCode'), statusText: this.get('iopa.StatusText') || undefined }) } } /** Initialize blank IopaContext object; Generic properties common to all server types included */ public init(): this { this.set('iopa.Version', VERSION) this[kHeaders] = new Headers() this[kIsFinalized] = false return this } public status(code: number): this { this.set('iopa.StatusCode', code) return this } public header(field: string, value: string): this { this.get('iopa.Headers').set(field, value) return this } public cookie(name: string, value: any, options?: ICookieOptions): this { cookies.set(this.get('iopa.Headers'), name, value, options) return this } public clearCookie(name: string, options?: ICookieOptions): this { cookies.clear(this.get('iopa.Headers'), name, options) return this } public arrayBuffer(data: BufferSource): this { this.set('iopa.Body', data) return this } public formData(data: FormData | URLSearchParams): this { this.set('iopa.Body', data) return this } public blob(data: Blob): this { this.set('iopa.Body', data) return this } public json(data: any): this { this.header('Content-Type', 'application/json; charset=UTF-8') this.set('iopa.Body', JSON.stringify(data)) return this } public text(data: string): this { this.header('Content-Type', 'text/html; charset=UTF-8') this.set('iopa.Body', data) return this } public html(data: string): this { this.header('Content-Type', 'text/plain; charset=UTF-8') this.set('iopa.Body', data) return this } public respondWith( res: Omit<HandlerResult, 'null' | 'undefined'>, options?: number | { status?: number; headers?: Record<string, string> } ): this { if (this[kIsFinalized]) { return this } let status: number if (options || options === 0) { if (typeof options === 'number') { status = options } else { status = (options as any).status } } if (options && (options as any).headers) { Object.entries((options as any).headers).forEach(([key, value]) => this[kHeaders].set(key, value as any) ) } if (status) { this.status(status) } if (res === undefined || res === null) { this[kResponse] = this.get('iopa.RawResponse') this[kIsFinalized] = true } else if (res instanceof Response) { this[kResponse] = res this[kIsFinalized] = true } else if (res instanceof Error) { this.status((res as any).statusCode || status || 500).text( `${(res as any).statusCode || status || 500} ERROR ${res.name} ${ res.message }` ) } else { if (typeof res === 'number' && !status) { this.status(res) } else if (typeof res === 'string' || res instanceof String) { this.text(res as string).status(status || 200) } else if (typeof res === 'boolean') { if (res) { this.text(`true`).status(status || 200) } else { this.text(`false`).status(status || 200) } } else if ('iopa.Version' in res) { /** IOPA Reply, noop */ } else { /** JSON Object */ this.json(res).status(status || 200) } this[kResponse] = this.get('iopa.RawResponse') this[kIsFinalized] = true } return this } }