From 4b87e9d20c775d276d4930de451aa706503788b3 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Tue, 13 Jun 2023 14:05:09 -0400 Subject: [PATCH] finalized project --- README.md | 49 ++++ flake.nix | 22 +- src/deps.ts | 2 + src/restapi.ts | 2 + src/server.ts | 3 +- src/stagger.ts | 10 + vendor/deno.land/x/cors@v1.2.2/abcCors.ts | 86 ++++++ vendor/deno.land/x/cors@v1.2.2/attainCors.ts | 81 ++++++ vendor/deno.land/x/cors@v1.2.2/cors.ts | 291 +++++++++++++++++++ vendor/deno.land/x/cors@v1.2.2/mithCors.ts | 86 ++++++ vendor/deno.land/x/cors@v1.2.2/mod.ts | 7 + vendor/deno.land/x/cors@v1.2.2/oakCors.ts | 82 ++++++ vendor/deno.land/x/cors@v1.2.2/opineCors.ts | 81 ++++++ vendor/deno.land/x/cors@v1.2.2/types.ts | 228 +++++++++++++++ vendor/import_map.json | 1 + 15 files changed, 1027 insertions(+), 4 deletions(-) create mode 100644 README.md create mode 100644 src/stagger.ts create mode 100644 vendor/deno.land/x/cors@v1.2.2/abcCors.ts create mode 100644 vendor/deno.land/x/cors@v1.2.2/attainCors.ts create mode 100644 vendor/deno.land/x/cors@v1.2.2/cors.ts create mode 100644 vendor/deno.land/x/cors@v1.2.2/mithCors.ts create mode 100644 vendor/deno.land/x/cors@v1.2.2/mod.ts create mode 100644 vendor/deno.land/x/cors@v1.2.2/oakCors.ts create mode 100644 vendor/deno.land/x/cors@v1.2.2/opineCors.ts create mode 100644 vendor/deno.land/x/cors@v1.2.2/types.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee8184e --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# 3Dverse test task + +## Solution + +### Technology choosen + +1. [deno](https://deno.com/) instead of nodejs as platform. +2. [deno kv](https://deno.com/kv) for database. +3. [nix](https://nixos.org/) flake as build system + + +### Development environment +If the developper has nix flake installed. `nix develop` will install +all the system dependencies. Otherwise, `deno` is required; hey, +and psrecord are two useful tool to benchmark the server. + +### Running the tests. + +The best way to build the test is to run `nix build .#test` which +will output a report in `./result`. Otherwise, +`deno test --unstable --allow-read=tests tests` should run all the tests. + + +### Running the project. + + +The main file to run the server is `src/server.ts`. To command line +`deno run --unstable --allow-net src/server.ts` will run the server locally. +One can add the flag `--v8-flags=--max-heap-size=50` to limit the +allowed memory to, in this example, 50mb. + + +In a similar fashion, it is possible to run the application without +checking out the code using the command line + +```shell +deno run --v8-flags=--max-heap-size=50 --unstable --allow-net https://gitlab.com/didiercrunch/3dverse-testtask/-/raw/master/src/server.ts +``` + +## Architecture + + +```mermaid +sequenceDiagram + title: Persist file + client->>RestAPI: HTTP request + RestAPI->>FileService: Persist file metadata + FileService->>BlobService: Persist file +``` diff --git a/flake.nix b/flake.nix index 0e5ecd2..f656d8b 100644 --- a/flake.nix +++ b/flake.nix @@ -6,7 +6,7 @@ flake-utils.url = "github:numtide/flake-utils"; }; - outputs = { self, nixpkgs, flake-utils, }: + outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; @@ -22,12 +22,28 @@ installPhase = '' mv test_results.txt $out ''; - - + }; + swagger-ui-src = pkgs.stdenv.mkDerivation { + name = "swagger-ui-src"; + src = pkgs.fetchzip { + url = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.0.0.zip"; + hash = "sha256-M/P/teEMqQ0gW0k/mvsr/VnkzuTI82+OMc8PAxCQAKM="; + }; + installPhase = '' + cp -r dist $out + ''; + }; + swagger-ui = pkgs.writeShellApplication { + name = "swagger-ui"; + runtimeInputs = [swagger-ui-src pkgs.python311]; + text = '' + exec ${pkgs.python311}/bin/python -m http.server -d ${swagger-ui-src} 7766 + ''; }; in { packages = { test = test; + swagger-ui = swagger-ui; }; devShells.default = pkgs.mkShell { buildInputs = with pkgs; [deno diff --git a/src/deps.ts b/src/deps.ts index 788556c..1568ffe 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -10,6 +10,8 @@ export { TextLineStream, } from 'https://deno.land/std@0.191.0/streams/mod.ts'; export {Status} from "https://deno.land/std@0.191.0/http/http_status.ts"; +export { oakCors } from 'https://deno.land/x/cors/mod.ts'; + // oak export { Application, Context, Router } from 'https://deno.land/x/oak/mod.ts'; diff --git a/src/restapi.ts b/src/restapi.ts index 40b5b58..3a7ce0a 100644 --- a/src/restapi.ts +++ b/src/restapi.ts @@ -3,6 +3,7 @@ import { FileMetaData, FileService } from './services/mod.ts'; import { readerFromStreamReader } from './deps.ts'; import { asyncIterableToArray } from './utils.ts'; import { Vector3D, WaveFrontService } from './services/interfaces.ts'; +import { bigAndUglySwagger } from "./stagger.ts"; type Handler = (ctx: Context) => void; @@ -161,6 +162,7 @@ function deleteFileHandlerFactory(fileService: FileService): Handler { } export function buildAPI(router: Router, fileService: FileService, waveFrontService: WaveFrontService): Router { + router.get('/swagger', (ctx) => ctx.response.body = bigAndUglySwagger); router.get('/files', listFileHandlerFactory(fileService)); router.get('/files/:fileId', getFileHandlerFactory(fileService)); router.delete('/files/:fileId', deleteFileHandlerFactory(fileService)); diff --git a/src/server.ts b/src/server.ts index 0f36e2d..7a2991e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,4 @@ -import { Application, Router } from './deps.ts'; +import { Application, Router, oakCors } from './deps.ts'; import { buildAPI } from './restapi.ts'; import { DenoKVFileService } from './services/mod.ts'; import { WaveFrontServiceImpl } from './services/wavefrontService.ts'; @@ -10,6 +10,7 @@ async function main() { const waveFrontService = new WaveFrontServiceImpl(); const router = new Router({ prefix: '/v1' }); + app.use(oakCors({ origin: /^.+localhost:7766$/ })); app.use( buildAPI(router, fileService, waveFrontService).routes(), ); diff --git a/src/stagger.ts b/src/stagger.ts new file mode 100644 index 0000000..69a3dee --- /dev/null +++ b/src/stagger.ts @@ -0,0 +1,10 @@ +export const bigAndUglySwagger = ` +openapi: 3.0.3 +info: + title: Test task + description: |- + This should be swagger documentation. + version: 1.0.11 + +paths: {} +` \ No newline at end of file diff --git a/vendor/deno.land/x/cors@v1.2.2/abcCors.ts b/vendor/deno.land/x/cors@v1.2.2/abcCors.ts new file mode 100644 index 0000000..31d87a4 --- /dev/null +++ b/vendor/deno.land/x/cors@v1.2.2/abcCors.ts @@ -0,0 +1,86 @@ +import type { CorsOptions, CorsOptionsDelegate } from "./types.ts"; +import { Cors } from "./cors.ts"; + +interface Req { + method: string; + headers: { + get(headerKey: string): string | null | undefined; + }; +} + +interface Res { + status?: number | string; + headers: { + get(headerKey: string): string | null | undefined; + set(headerKey: string, headerValue: string): any; + }; +} + +/** + * abcCors middleware wrapper + * @param o CorsOptions | CorsOptionsDelegate + * @link https://github.com/tajpouria/cors/blob/master/README.md#cors + */ +export const abcCors = < + RequestT extends Req = any, + ResponseT extends Res = any, + MiddlewareT extends ( + next: (...args: any) => any, + ) => (context: { request: RequestT; response: ResponseT }) => any = any, +>( + o?: CorsOptions | CorsOptionsDelegate, +) => { + const corsOptionsDelegate = Cors.produceCorsOptionsDelegate< + CorsOptionsDelegate + >(o); + + return ((abcNext) => + async (context) => { + const next = () => abcNext(context); + + try { + const { request, response } = context; + + const options = await corsOptionsDelegate(request); + + const corsOptions = Cors.produceCorsOptions(options || {}); + const originDelegate = Cors.produceOriginDelegate(corsOptions); + + if (originDelegate) { + const requestMethod = request.method; + const getRequestHeader = (headerKey: string) => + request.headers.get(headerKey); + const getResponseHeader = (headerKey: string) => + response.headers.get(headerKey); + const setResponseHeader = (headerKey: string, headerValue: string) => + response.headers.set(headerKey, headerValue); + const setStatus = ( + statusCode: number, + ) => (response.status = statusCode); + const end = () => {}; + + const origin = await originDelegate(getRequestHeader("origin")); + + if (!origin) return next(); + else { + corsOptions.origin = origin; + + return new Cors({ + corsOptions, + requestMethod, + getRequestHeader, + getResponseHeader, + setResponseHeader, + setStatus, + next, + end, + }).configureHeaders(); + } + } + } catch (error) { + console.error(error); + } + + return next(); + }) as MiddlewareT; +}; diff --git a/vendor/deno.land/x/cors@v1.2.2/attainCors.ts b/vendor/deno.land/x/cors@v1.2.2/attainCors.ts new file mode 100644 index 0000000..ed9d461 --- /dev/null +++ b/vendor/deno.land/x/cors@v1.2.2/attainCors.ts @@ -0,0 +1,81 @@ +import type { CorsOptions, CorsOptionsDelegate } from "./types.ts"; +import { Cors } from "./cors.ts"; + +interface Req { + method: string; + headers: { + get(headerKey: string): string | null | undefined; + }; +} + +interface Res { + status: (status: number) => any; + headers: { + get(headerKey: string): string | null | undefined; + set(headerKey: string, headerValue: string): any; + }; + send: (body: any) => any; + end(...args: any): any; +} + +/** + * attainCors middleware wrapper + * @param o CorsOptions | CorsOptionsDelegate + * @link https://github.com/tajpouria/cors/blob/master/README.md#cors + */ +export const attainCors = < + RequestT extends Req = any, + ResponseT extends Res = any, + MiddlewareT extends (request: RequestT, response: ResponseT) => any = any, +>( + o?: CorsOptions | CorsOptionsDelegate, +) => { + const corsOptionsDelegate = Cors.produceCorsOptionsDelegate< + CorsOptionsDelegate + >(o); + + return async function cors(request, response) { + try { + const fakeNext = () => undefined; + const options = await corsOptionsDelegate(request); + + const corsOptions = Cors.produceCorsOptions(options || {}); + const originDelegate = Cors.produceOriginDelegate(corsOptions); + + if (originDelegate) { + const requestMethod = request.method; + const getRequestHeader = (headerKey: string) => + request.headers.get(headerKey); + const getResponseHeader = (headerKey: string) => + response.headers.get(headerKey); + const setResponseHeader = (headerKey: string, headerValue: string) => + response.headers.set(headerKey, headerValue); + const setStatus = (statusCode: number) => response.status(statusCode); + const end = response.end(); + + const origin = await originDelegate(getRequestHeader("origin")); + + if (origin) { + corsOptions.origin = origin; + + new Cors({ + corsOptions, + requestMethod, + getRequestHeader, + getResponseHeader, + setResponseHeader, + setStatus, + next: fakeNext, + end, + }).configureHeaders(); + } + } + + if (request.method === "OPTIONS") { + response.send(""); + } + } catch (error) { + console.error(error); + } + } as MiddlewareT; +}; diff --git a/vendor/deno.land/x/cors@v1.2.2/cors.ts b/vendor/deno.land/x/cors@v1.2.2/cors.ts new file mode 100644 index 0000000..dbe056d --- /dev/null +++ b/vendor/deno.land/x/cors@v1.2.2/cors.ts @@ -0,0 +1,291 @@ +import type { + CorsOptions, + CorsOptionsDelegate, + OriginDelegate, +} from "./types.ts"; + +interface DefaultCorsOptions { + origin: string; + methods: string; + preflightContinue: boolean; + optionsSuccessStatus: number; +} + +interface CorsProps { + corsOptions: ReturnType; + requestMethod: string; + getRequestHeader: (headerKey: string) => string | null | undefined; + getResponseHeader: (headerKey: string) => string | null | undefined; + setResponseHeader: (headerKey: string, headerValue: string) => any; + setStatus: (statusCode: number) => any; + next: (...args: any) => any; + end: (...args: any) => any; +} + +export class Cors { + constructor(private props: CorsProps) {} + + public static produceCorsOptions = ( + corsOptions: CorsOptions = {}, + defaultCorsOptions: DefaultCorsOptions = { + origin: "*", + methods: "GET,HEAD,PUT,PATCH,POST,DELETE", + preflightContinue: false, + optionsSuccessStatus: 204, + }, + ) => ({ + ...defaultCorsOptions, + ...corsOptions, + }); + + public static produceCorsOptionsDelegate = < + OptionsCallbackT = CorsOptionsDelegate, + >( + o?: CorsOptions | OptionsCallbackT, + ) => + typeof o === "function" + ? (o as OptionsCallbackT) + : ((((_request: any) => o) as unknown) as OptionsCallbackT); + + public static produceOriginDelegate = ( + corsOptions: CorsProps["corsOptions"], + ) => { + if (corsOptions.origin) { + if (typeof corsOptions.origin === "function") { + return corsOptions.origin as OriginDelegate; + } + + return ((_requestOrigin) => corsOptions.origin) as OriginDelegate; + } + }; + + public static isOriginAllowed = ( + requestOrigin: string | null | undefined, + allowedOrigin: CorsOptions["origin"], + ): boolean => { + if (Array.isArray(allowedOrigin)) { + return allowedOrigin.some((ao) => + Cors.isOriginAllowed(requestOrigin, ao) + ); + } else if (typeof allowedOrigin === "string") { + return requestOrigin === allowedOrigin; + } else if ( + allowedOrigin instanceof RegExp && + typeof requestOrigin === "string" + ) { + return allowedOrigin.test(requestOrigin); + } else return !!allowedOrigin; + }; + + public configureHeaders = () => { + const { + props: { + corsOptions, + requestMethod, + setResponseHeader, + setStatus, + next, + end, + }, + configureOrigin, + } = this; + + if ( + typeof requestMethod === "string" && + requestMethod.toUpperCase() === "OPTIONS" + ) { + configureOrigin() + .configureCredentials() + .configureMethods() + .configureAllowedHeaders() + .configureMaxAge() + .configureExposedHeaders(); + + if (corsOptions.preflightContinue) return next(); + else { + setStatus(corsOptions.optionsSuccessStatus); + setResponseHeader("Content-Length", "0"); + + return end(); + } + } else { + configureOrigin().configureCredentials().configureExposedHeaders(); + + return next(); + } + }; + + private configureOrigin = () => { + const { + props: { corsOptions, getRequestHeader, setResponseHeader }, + setVaryHeader, + } = this; + + if (!corsOptions.origin || corsOptions.origin === "*") { + setResponseHeader("Access-Control-Allow-Origin", "*"); + } else if (typeof corsOptions.origin === "string") { + setResponseHeader("Access-Control-Allow-Origin", corsOptions.origin); + setVaryHeader("Origin"); + } else { + const requestOrigin = getRequestHeader("origin") ?? + getRequestHeader("Origin"); + + setResponseHeader( + "Access-Control-Allow-Origin", + Cors.isOriginAllowed(requestOrigin, corsOptions.origin) + ? (requestOrigin as string) + : "false", + ); + setVaryHeader("Origin"); + } + + return this; + }; + + private configureCredentials = () => { + const { corsOptions, setResponseHeader } = this.props; + + if (corsOptions.credentials === true) { + setResponseHeader("Access-Control-Allow-Credentials", "true"); + } + + return this; + }; + + private configureMethods = () => { + const { corsOptions, setResponseHeader } = this.props; + + let methods = corsOptions.methods; + + setResponseHeader( + "Access-Control-Allow-Methods", + Array.isArray(methods) ? methods.join(",") : methods, + ); + + return this; + }; + + private configureAllowedHeaders = () => { + const { + props: { corsOptions, getRequestHeader, setResponseHeader }, + setVaryHeader, + } = this; + + let allowedHeaders = corsOptions.allowedHeaders; + + if (!allowedHeaders) { + allowedHeaders = getRequestHeader("access-control-request-headers") ?? + getRequestHeader("Access-Control-Request-Headers") ?? + undefined; + + setVaryHeader("Access-Control-request-Headers"); + } + + if (allowedHeaders?.length) { + setResponseHeader( + "Access-Control-Allow-Headers", + Array.isArray(allowedHeaders) + ? allowedHeaders.join(",") + : allowedHeaders, + ); + } + + return this; + }; + + private configureMaxAge = () => { + const { corsOptions, setResponseHeader } = this.props; + + const maxAge = (typeof corsOptions.maxAge === "number" || + typeof corsOptions.maxAge === "string") && + corsOptions.maxAge.toString(); + + if (maxAge && maxAge.length) { + setResponseHeader("Access-Control-Max-Age", maxAge); + } + + return this; + }; + + private configureExposedHeaders = () => { + const { corsOptions, setResponseHeader } = this.props; + + let exposedHeaders = corsOptions.exposedHeaders; + + if (exposedHeaders?.length) { + setResponseHeader( + "Access-Control-Expose-Headers", + Array.isArray(exposedHeaders) + ? exposedHeaders.join(",") + : exposedHeaders, + ); + } + + return this; + }; + + private setVaryHeader = (field: string) => { + const { + props: { getResponseHeader, setResponseHeader }, + appendVaryHeader, + } = this; + + let existingHeader = getResponseHeader("Vary") ?? ""; + + if ( + existingHeader && + typeof existingHeader === "string" && + (existingHeader = appendVaryHeader(existingHeader, field)) + ) { + setResponseHeader("Vary", existingHeader); + } + }; + + private appendVaryHeader = (header: string, field: string) => { + const { parseVaryHeader } = this; + + if (header === "*") return header; + + let varyHeader = header; + const fields = parseVaryHeader(field); + const headers = parseVaryHeader(header.toLocaleLowerCase()); + + if (fields.includes("*") || headers.includes("*")) return "*"; + + fields.forEach((field) => { + const fld = field.toLowerCase(); + + if (headers.includes(fld)) { + headers.push(fld); + varyHeader = varyHeader ? `${varyHeader}, ${field}` : field; + } + }); + + return varyHeader; + }; + + private parseVaryHeader = (header: string) => { + let end = 0; + const list = []; + let start = 0; + + for (let i = 0, len = header.length; i < len; i++) { + switch (header.charCodeAt(i)) { + case 0x20 /* */: + if (start === end) start = end = i + 1; + break; + case 0x2c /* , */: + list.push(header.substring(start, end)); + start = end = i + 1; + break; + default: + end = i + 1; + break; + } + } + + list.push(header.substring(start, end)); + + return list; + }; +} diff --git a/vendor/deno.land/x/cors@v1.2.2/mithCors.ts b/vendor/deno.land/x/cors@v1.2.2/mithCors.ts new file mode 100644 index 0000000..f4adcfd --- /dev/null +++ b/vendor/deno.land/x/cors@v1.2.2/mithCors.ts @@ -0,0 +1,86 @@ +import type { CorsOptions, CorsOptionsDelegate } from "./types.ts"; +import { Cors } from "./cors.ts"; + +interface Req { + serverRequest: { + method: string; + headers: { + get(headerKey: string): string | null | undefined; + }; + }; +} + +interface Res { + status?: number | string; + headers: { + get(headerKey: string): string | null | undefined; + set(headerKey: string, headerValue: string): any; + }; +} + +/** + * mithCors middleware wrapper + * @param o CorsOptions | CorsOptionsDelegate + * @link https://github.com/tajpouria/cors/blob/master/README.md#cors + */ +export const mithCors = < + RequestT extends Req = any, + ResponseT extends Res = any, + MiddlewareT extends ( + request: RequestT, + response: ResponseT, + next: (...args: any) => any, + ) => any = any, +>( + o?: CorsOptions | CorsOptionsDelegate, +) => { + const corsOptionsDelegate = Cors.produceCorsOptionsDelegate< + CorsOptionsDelegate + >(o); + + return (async (request, response, next) => { + const serverRequest = request.serverRequest; + try { + const options = await corsOptionsDelegate(request); + + const corsOptions = Cors.produceCorsOptions(options || {}); + const originDelegate = Cors.produceOriginDelegate(corsOptions); + + if (originDelegate) { + const requestMethod = serverRequest.method; + const getRequestHeader = (headerKey: string) => + serverRequest.headers.get(headerKey); + const getResponseHeader = (headerKey: string) => + response.headers.get(headerKey); + const setResponseHeader = (headerKey: string, headerValue: string) => + response.headers.set(headerKey, headerValue); + const setStatus = ( + statusCode: number, + ) => (response.status = statusCode); + const end = () => {}; + + const origin = await originDelegate(getRequestHeader("origin")); + + if (!origin) return next(); + else { + corsOptions.origin = origin; + + return new Cors({ + corsOptions, + requestMethod, + getRequestHeader, + getResponseHeader, + setResponseHeader, + setStatus, + next, + end, + }).configureHeaders(); + } + } + } catch (error) { + console.error(error); + } + + return next(); + }) as MiddlewareT; +}; diff --git a/vendor/deno.land/x/cors@v1.2.2/mod.ts b/vendor/deno.land/x/cors@v1.2.2/mod.ts new file mode 100644 index 0000000..765d12d --- /dev/null +++ b/vendor/deno.land/x/cors@v1.2.2/mod.ts @@ -0,0 +1,7 @@ +export * from "./types.ts"; + +export { oakCors } from "./oakCors.ts"; +export { abcCors } from "./abcCors.ts"; +export { opineCors } from "./opineCors.ts"; +export { mithCors } from "./mithCors.ts"; +export { attainCors } from "./attainCors.ts"; diff --git a/vendor/deno.land/x/cors@v1.2.2/oakCors.ts b/vendor/deno.land/x/cors@v1.2.2/oakCors.ts new file mode 100644 index 0000000..ea8fe52 --- /dev/null +++ b/vendor/deno.land/x/cors@v1.2.2/oakCors.ts @@ -0,0 +1,82 @@ +import type { CorsOptions, CorsOptionsDelegate } from "./types.ts"; +import { Cors } from "./cors.ts"; + +interface Req { + method: string; + headers: { + get(headerKey: string): string | null | undefined; + }; +} + +interface Res { + status?: number | string; + headers: { + get(headerKey: string): string | null | undefined; + set(headerKey: string, headerValue: string): any; + }; +} + +/** + * oakCors middleware wrapper + * @param o CorsOptions | CorsOptionsDelegate + * @link https://github.com/tajpouria/cors/blob/master/README.md#cors + */ +export const oakCors = < + RequestT extends Req = any, + ResponseT extends Res = any, + MiddlewareT extends ( + context: { request: RequestT; response: ResponseT }, + next: (...args: any) => any, + ) => any = any, +>( + o?: CorsOptions | CorsOptionsDelegate, +) => { + const corsOptionsDelegate = Cors.produceCorsOptionsDelegate< + CorsOptionsDelegate + >(o); + + return (async ({ request, response }, next) => { + try { + const options = await corsOptionsDelegate(request); + + const corsOptions = Cors.produceCorsOptions(options || {}); + const originDelegate = Cors.produceOriginDelegate(corsOptions); + + if (originDelegate) { + const requestMethod = request.method; + const getRequestHeader = (headerKey: string) => + request.headers.get(headerKey); + const getResponseHeader = (headerKey: string) => + response.headers.get(headerKey); + const setResponseHeader = (headerKey: string, headerValue: string) => + response.headers.set(headerKey, headerValue); + const setStatus = ( + statusCode: number, + ) => (response.status = statusCode); + const end = () => {}; + + const origin = await originDelegate(getRequestHeader("origin")); + + if (!origin) next(); + else { + corsOptions.origin = origin; + + return new Cors({ + corsOptions, + requestMethod, + getRequestHeader, + getResponseHeader, + setResponseHeader, + setStatus, + next, + end, + }).configureHeaders(); + } + } + } catch (error) { + console.error(error); + } + + next(); + }) as MiddlewareT; +}; diff --git a/vendor/deno.land/x/cors@v1.2.2/opineCors.ts b/vendor/deno.land/x/cors@v1.2.2/opineCors.ts new file mode 100644 index 0000000..4751249 --- /dev/null +++ b/vendor/deno.land/x/cors@v1.2.2/opineCors.ts @@ -0,0 +1,81 @@ +import type { CorsOptions, CorsOptionsDelegate } from "./types.ts"; +import { Cors } from "./cors.ts"; + +interface Req { + method: string; + headers: { + get(name: string): string | null | undefined; + }; +} + +interface Res { + setStatus(code: any): this; + set(headerKey: string, headerValue: string): this; + get(headerKey: string): string | null | undefined; + end(...args: any): any; +} + +/** + * opineCors middleware wrapper + * @param o CorsOptions | CorsOptionsDelegate + * @link https://github.com/tajpouria/cors/blob/master/README.md#cors + */ +export const opineCors = < + RequestT extends Req = Req, + ResponseT extends Res = any, + MiddlewareT extends ( + request: RequestT, + response: ResponseT, + next: (...args: any) => any, + ) => any = any, +>( + o?: CorsOptions | CorsOptionsDelegate, +) => { + const corsOptionsDelegate = Cors.produceCorsOptionsDelegate< + CorsOptionsDelegate + >(o); + + return (async (request, response, next) => { + try { + const options = await corsOptionsDelegate(request); + + const corsOptions = Cors.produceCorsOptions(options || {}); + const originDelegate = Cors.produceOriginDelegate(corsOptions); + + if (originDelegate) { + const requestMethod = request.method; + const getRequestHeader = (headerKey: string) => + request.headers.get(headerKey); + const getResponseHeader = (headerKey: string) => + response.get(headerKey); + const setResponseHeader = (headerKey: string, headerValue: string) => + response.set(headerKey, headerValue); + const setStatus = (statusCode: number) => + response.setStatus(statusCode); + const end = () => response.end(); + + const origin = await originDelegate(getRequestHeader("origin")); + + if (!origin) return next(); + else { + corsOptions.origin = origin; + + return new Cors({ + corsOptions, + requestMethod, + getRequestHeader, + getResponseHeader, + setResponseHeader, + setStatus, + next, + end, + }).configureHeaders(); + } + } + } catch (error) { + console.error(error); + } + + return next(); + }) as MiddlewareT; +}; diff --git a/vendor/deno.land/x/cors@v1.2.2/types.ts b/vendor/deno.land/x/cors@v1.2.2/types.ts new file mode 100644 index 0000000..cde0738 --- /dev/null +++ b/vendor/deno.land/x/cors@v1.2.2/types.ts @@ -0,0 +1,228 @@ +/** + * CorsOptions + * + * An Object that describes how CORS middleware should behave. The default configuration is the equivalent of: + * + * ```ts + * { + * "origin": "*", + * "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + * "preflightContinue": false, + * "optionsSuccessStatus": 204, + * } + * ``` + * + * @link https://github.com/tajpouria/cors#configuration-options + */ +export type CorsOptions = { + /** + * Configures the Access-Control-Allow-Origin CORS header. + * + * Examples: + * + * - Boolean + * + * set origin to true to reflect the request origin, as defined by req.header('Origin'): + * + * ```ts + * app.use(cors({ origin: true })); + * + * ``` + * or set it to false to disable CORS: + * + * ```ts + * app.use(cors({ origin: false })); + * + * ``` + * + * - String + * + * set origin to a specific origin. For example if you set it to "http://example.com" only requests from "http://example.com" will be allowed: + * + * ```ts + * app.use(cors({ origin: 'http://example.com' })); + * + * ``` + * + * - RegExp + * + * set origin to a regular expression pattern which will be used to test the request origin. If it's a match, the request origin will be reflected. For example the pattern /example\.com$/ will reflect any request that is coming from an origin ending with "example.com": + * + * ```ts + * app.use(cors({ origin: /example\.com$/ })); + * + * ``` + * + * - Array + * + * set origin to an array of valid origins. Each origin can be a String or a RegExp. For example ["http://example1.com", /\.example2\.com$/] will accept any request from "http://example1.com" or from a subdomain of "example2.com": + * + * ```ts + * app.use(cors({ origin: ["http://example1.com", /\.example2\.com$/] })); + * + * ``` + * + * - Function + * + * set origin to a function implementing some custom logic. The function takes the request origin as the first parameter and a callback (called as callback(err, origin), where origin is a non-function value of the origin option) as the second: + * + * ```ts + * app.use(cors({ + * origin: async (requestOrigin) => { + * await loadOriginsFromDataBase(); // Simulate asynchronous task + * return ["http://example1.com", /\.example2\.com$/]; + * }, + * })); + * + * ``` + * + */ + origin?: boolean | string | RegExp | (string | RegExp)[] | OriginDelegate; + + /** + * Configures the Access-Control-Allow-Methods CORS header. + * + * Examples: + * + * - String + * + * Expects a comma-delimited string (ex: 'GET,PUT,POST'): + * + * ```ts + * app.use(cors({ methods: 'GET,PUT,POST' })); + * + * ``` + * + * - Array + * + * or an array (ex: ['GET', 'PUT', 'POST']) + * + * ```ts + * app.use(cors({ methods: ['GET', 'PUT', 'POST'] })); + * + * ``` + * + */ + methods?: string | string[]; + + /** + * Configures the Access-Control-Allow-Headers CORS header. If not specified, defaults to reflecting the headers specified in the request's Access-Control-Request-Headers header. + * + * Examples: + * + * - String + * + * Expects a comma-delimited string (ex: 'Content-Type,Authorization'): + * + * ```ts + * app.use(cors({ allowedHeaders: 'Content-Type,Authorization' })); + * + * ``` + * + * - Array + * + * or an array (ex: ['Content-Type', 'Authorization']): + * + * ```ts + * app.use(cors({ allowedHeaders: ['Content-Type', 'Authorization'] })); + * + * ``` + * + */ + allowedHeaders?: string | string[]; + + /** + * Configures the Access-Control-Expose-Headers CORS header. If not specified, no custom headers are exposed. + * + * Examples: + * + * - String + * + * Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range'): + * + * ```ts + * app.use(cors({ exposedHeaders: 'Content-Range,X-Content-Range' })); + * + * ``` + * + */ + exposedHeaders?: string | string[]; + + /** + * Configures the Access-Control-Allow-Credentials CORS header. It is omitted by default. + * + * Examples: + * + * - Boolean + * + * Set to true to pass the header: + * + * ```ts + * app.use(cors({ credentials: true })); + * + * ``` + * + */ + credentials?: boolean; + + /** + * Configures the Access-Control-Max-Age CORS header. It is omitted by default. + * + * Examples: + * + * - Number + * + * Set to an integer to pass the header: + * + * ```ts + * app.use(cors({ maxAge: 1 })); + * + * ``` + * + */ + maxAge?: number; + + /** + * Pass the CORS preflight response to the next handler: + * + * Examples: + * + * - Boolean + * + * ```ts + * app.use(cors({ preflightContinue: true })); + * + * ``` + * + */ + preflightContinue?: boolean; + + /** + * Provides a status code to use for successful OPTIONS requests, since some legacy browsers (IE11, various SmartTVs) choke on 204. + * + * Examples: + * + * - Number + * + * ```ts + * app.use(cors({ optionsSuccessStatus: 200 })); + * + * ``` + * + */ + optionsSuccessStatus?: number; +}; + +/** + * @link https://github.com/tajpouria/cors#configuring-cors-w-dynamic-origin + */ +export type OriginDelegate = ( + requestOrigin: string | undefined | null, +) => CorsOptions["origin"] | void | Promise; + +/** + * @link https://github.com/tajpouria/cors#configuring-cors-asynchronously + */ +export type CorsOptionsDelegate = ( + request: RequestT, +) => CorsOptions | void | Promise; diff --git a/vendor/import_map.json b/vendor/import_map.json index 52786a6..7e1b8c3 100644 --- a/vendor/import_map.json +++ b/vendor/import_map.json @@ -1,5 +1,6 @@ { "imports": { + "https://deno.land/x/cors/mod.ts": "./deno.land/x/cors@v1.2.2/mod.ts", "https://deno.land/x/oak/mod.ts": "./deno.land/x/oak@v12.1.0/mod.ts", "https://deno.land/std/testing/asserts.ts": "./deno.land/std@0.191.0/testing/asserts.ts", "https://deno.land/": "./deno.land/"