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
text/typescript
/*
* 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
}
}