├── .gitignore
├── _config.yml
├── Attain.png
├── version.ts
├── procedure.png
├── performance
├── oak.png
├── attain.png
├── express.png
├── oak-filesend.png
├── oak-middle.png
├── attain-middle.png
├── express-middle.png
├── attain-filesend.png
├── express-sendfile.png
├── oak-middlewithfive.png
├── attain-middlewithfive.png
├── express-middlewithfive.png
└── performance.md
├── sample
├── localTest
│ ├── public
│ │ ├── test.png
│ │ └── hello.html
│ ├── index.ts
│ ├── secondRouter.ts
│ ├── router.ts
│ └── cert
│ │ ├── secret.crt
│ │ └── secret.key
├── simple.ts
├── basic
│ ├── api.ts
│ ├── main.ts
│ └── router.ts
├── postParsing
│ └── plugin.ts
└── ws
│ ├── client.ts
│ └── index.ts
├── defaultHandler
├── index.ts
├── defaultPageNotFound.ts
└── defaultError.ts
├── test
├── static
│ └── test.html
├── test_deps.ts
├── basicTest
│ ├── router.ts
│ ├── param.ts
│ └── main_test.ts
├── database
│ └── database_test.ts
└── benchmarks
│ ├── graph_test.ts
│ └── linear_test.ts
├── howto
├── autorecovery.md
└── websocket.md
├── helmat
├── hide-powered-by.ts
├── dont-sniff-mimetype.ts
├── dns-prefetch-control.ts
├── LICENSE
├── x-xxs-protection.ts
└── frame-guard.ts
├── .vscode
└── settings.json
├── goorm.manifest
├── mod.ts
├── start_test.cmd
├── core
├── database.ts
├── debug.ts
├── types.ts
├── application.ts
├── httpError.ts
├── process.ts
├── request.ts
├── utils.ts
├── router.ts
└── response.ts
├── egg.yaml
├── start_test.sh
├── .github
└── workflows
│ └── attain-ci.yaml
├── plugins
├── logger.ts
├── parser.ts
├── staticServe.ts
└── security.ts
├── LICENSE
├── recovery.ts
├── deps.ts
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /index.ts
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-minimal
--------------------------------------------------------------------------------
/Attain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/Attain.png
--------------------------------------------------------------------------------
/version.ts:
--------------------------------------------------------------------------------
1 | const version = "1.1.2";
2 |
3 | export default version;
4 |
--------------------------------------------------------------------------------
/procedure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/procedure.png
--------------------------------------------------------------------------------
/performance/oak.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/oak.png
--------------------------------------------------------------------------------
/performance/attain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/attain.png
--------------------------------------------------------------------------------
/performance/express.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/express.png
--------------------------------------------------------------------------------
/performance/oak-filesend.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/oak-filesend.png
--------------------------------------------------------------------------------
/performance/oak-middle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/oak-middle.png
--------------------------------------------------------------------------------
/performance/attain-middle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/attain-middle.png
--------------------------------------------------------------------------------
/performance/express-middle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/express-middle.png
--------------------------------------------------------------------------------
/performance/attain-filesend.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/attain-filesend.png
--------------------------------------------------------------------------------
/performance/express-sendfile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/express-sendfile.png
--------------------------------------------------------------------------------
/sample/localTest/public/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/sample/localTest/public/test.png
--------------------------------------------------------------------------------
/performance/oak-middlewithfive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/oak-middlewithfive.png
--------------------------------------------------------------------------------
/performance/attain-middlewithfive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/attain-middlewithfive.png
--------------------------------------------------------------------------------
/performance/express-middlewithfive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronwlee/attain/HEAD/performance/express-middlewithfive.png
--------------------------------------------------------------------------------
/defaultHandler/index.ts:
--------------------------------------------------------------------------------
1 | export { defaultError } from "./defaultError.ts";
2 | export { defaultPageNotFound } from "./defaultPageNotFound.ts";
3 |
--------------------------------------------------------------------------------
/test/static/test.html:
--------------------------------------------------------------------------------
1 |
Test this page is a simple html
--------------------------------------------------------------------------------
/howto/autorecovery.md:
--------------------------------------------------------------------------------
1 | # Auto Recovery
2 |
3 | ## how to recovery from general deno error?
4 | ```
5 | deno run -A --unstable https://deno.land/x/attain/recovery.ts
6 | ```
--------------------------------------------------------------------------------
/helmat/hide-powered-by.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response } from "../mod.ts";
2 |
3 | export const hidePoweredBy = () => {
4 | return (req: Request, res: Response) => {
5 | res.removeHeader("X-Powered-By");
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "[typescript]": {
4 | "editor.defaultFormatter": "denoland.vscode-deno",
5 | },
6 | "[typescriptreact]": {
7 | "editor.defaultFormatter": "denoland.vscode-deno",
8 | },
9 | }
--------------------------------------------------------------------------------
/helmat/dont-sniff-mimetype.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response } from "../mod.ts";
2 |
3 | export const dontSniffMimetype = () => {
4 | return (_req: Request, res: Response) => {
5 | res.setHeader("X-Content-Type-Options", "nosniff");
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/sample/simple.ts:
--------------------------------------------------------------------------------
1 | import { App, logger } from "../mod.ts";
2 | const app = new App();
3 |
4 | app.use(logger);
5 |
6 | app.use("/", (req, res) => {
7 | res.status(200).send("asd");
8 | });
9 |
10 | await app.listen(8000, { debug: true });
11 |
12 |
--------------------------------------------------------------------------------
/test/test_deps.ts:
--------------------------------------------------------------------------------
1 | export const test = Deno.test;
2 |
3 | export {
4 | assert,
5 | assertEquals,
6 | assertStrictEquals,
7 | assertThrows,
8 | assertThrowsAsync,
9 | } from "https://deno.land/std@0.117.0/testing/asserts.ts";
10 |
11 | export { runBenchmarks, bench } from "https://deno.land/std@0.117.0/testing/bench.ts";
12 |
--------------------------------------------------------------------------------
/sample/localTest/public/hello.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello World!
5 | Attain 0.2 / Aaron Wooseok Lee / MIT / 2020
6 |
7 |
8 | go /asd
9 |
10 |
11 |
12 | back
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/sample/basic/api.ts:
--------------------------------------------------------------------------------
1 | import { App, Request, Response } from "https://deno.land/x/attain/mod.ts";
2 |
3 | const second = new App();
4 | second.get("/", async (req: Request, res: Response) => {
5 | res.status(200).send({ status: "/api" });
6 | });
7 |
8 | second.post("/ok", (req: Request, res: Response) => {
9 | console.log("params", req.params);
10 | console.log("here im ok");
11 | res.status(200).send({ status: "/api/ok" });
12 | });
13 |
14 | export default second;
15 |
--------------------------------------------------------------------------------
/goorm.manifest:
--------------------------------------------------------------------------------
1 | {"author":"34082543_buozq_naver","name":"attain","type":"blank","detailedtype":"empty","description":"","date":"2021-12-09T13:20:26.000Z","plugins":{"goorm.plugin.blank":[{"plugin.blank.build_command":"","plugin.blank.run_as":"console","plugin.blank.run_command":"","plugin.blank.stop_command":"","name":"blank"}]},"is_user_plugin":false,"storage":"container","project_domain":[{"id":"34082543_buozq_naver","url":"deno-gql.run.goorm.io","port":"8080"}],"author_email":"stoneage95xp@naver.com","author_name":"이우석","ignore_patterns":[],"visibility":2}
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | export { App } from "./core/application.ts";
2 | export { Router } from "./core/router.ts";
3 |
4 | export { AttainRequest as Request } from "./core/request.ts";
5 | export { AttainResponse as Response } from "./core/response.ts";
6 |
7 | export { parser } from "./plugins/parser.ts";
8 | export { logger } from "./plugins/logger.ts";
9 | export { staticServe } from "./plugins/staticServe.ts";
10 | export { security } from "./plugins/security.ts";
11 |
12 | export { AttainDatabase } from "./core/database.ts"
13 |
14 | export * from "./core/types.ts";
--------------------------------------------------------------------------------
/start_test.cmd:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | deno -V
4 |
5 | echo "start basic benchmark"
6 | deno test -A --unstable test/basicTest/
7 | IF ERRORLEVEL 1 (
8 | EXIT 1
9 | )
10 |
11 | echo "start graph benchmark"
12 | deno test -A --unstable test/benchmarks/graph_test.ts
13 | IF ERRORLEVEL 1 (
14 | EXIT 1
15 | )
16 |
17 | echo "start linear benchmark"
18 | deno test -A --unstable test/benchmarks/linear_test.ts
19 | IF ERRORLEVEL 1 (
20 | EXIT 1
21 | )
22 |
23 | echo "start database test"
24 | deno test -A --unstable test/database/
25 | IF ERRORLEVEL 1 (
26 | EXIT 1
27 | )
28 |
29 | EXIT 0
--------------------------------------------------------------------------------
/core/database.ts:
--------------------------------------------------------------------------------
1 | export class DatabaseError extends Error {
2 | constructor(message: string) {
3 | super(`Database Error: ${message}`);
4 | }
5 | }
6 |
7 | // the class to expand
8 | export class AttainDatabase {
9 | static __type: string = "attain"
10 | async connect() {
11 | console.log('database connected')
12 | }
13 | }
14 |
15 | export interface NoParamConstructor {
16 | new(): I
17 | __type: string;
18 | }
19 |
20 | /*
21 | async function createDatabase(ctor: NoParamConstructor): Promise {
22 | const database = new ctor();
23 | await database.connect();
24 | return database;
25 | }
26 | */
--------------------------------------------------------------------------------
/helmat/dns-prefetch-control.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response } from "../mod.ts";
2 |
3 | export interface DnsPrefetchControlOptions {
4 | allow?: boolean;
5 | }
6 |
7 | function getHeaderValueFromOptions(
8 | options?: DnsPrefetchControlOptions,
9 | ): "on" | "off" {
10 | if (options && options.allow) {
11 | return "on";
12 | } else {
13 | return "off";
14 | }
15 | }
16 |
17 | export const dnsPrefetchControl = (options?: DnsPrefetchControlOptions) => {
18 | const headerValue = getHeaderValueFromOptions(options);
19 |
20 | return (req: Request, res: Response) => {
21 | res.setHeader("X-DNS-Prefetch-Control", headerValue);
22 | };
23 | };
24 |
--------------------------------------------------------------------------------
/egg.yaml:
--------------------------------------------------------------------------------
1 | name: attain
2 | description: A middleware web framework, inspired by express.js
3 | stable: true
4 | version: 1.1.0
5 | entry: ./mod.ts
6 | repository: https://github.com/aaronwlee/Attain
7 | files:
8 | - mod.ts
9 | - deps.ts
10 | - README.md
11 | - attain-cli.ts
12 | - Attain.png
13 | - _config.yml
14 | - version.ts
15 | - procedure.png
16 | - recovery.ts
17 | - .gitignore
18 | - LICENSE
19 | - .vscode/**/*
20 | - cli/**/*
21 | - core/**/*
22 | - defaultHandler/**/*
23 | - helmat/**/*
24 | - howto/**/*
25 | - performance/**/*
26 | - plugins/**/*
27 | - react/**/*
28 | - sample/**/*
29 | - test/**/*
30 | - viewEngine/**/*
31 |
--------------------------------------------------------------------------------
/test/basicTest/router.ts:
--------------------------------------------------------------------------------
1 | import { Router, parser } from "../../mod.ts";
2 |
3 | /**
4 | * Test purpose
5 | *
6 | * - check nested router
7 | */
8 | const router = new Router();
9 |
10 | router.get("/", (req, res) => {
11 | res.send("/router");
12 | });
13 |
14 | router.get("/second", (req, res) => {
15 | res.send("/router/second");
16 | });
17 |
18 | router.get("/second/:id", (req, res) => {
19 | res.send(`/router/second/${req.params.id}`);
20 | });
21 |
22 | router.get("/search", parser, (req, res) => {
23 | res.send(req.query);
24 | });
25 |
26 | router.post("/post", parser, async (req, res) => {
27 | res.send(req.params);
28 | });
29 |
30 | export default router;
31 |
--------------------------------------------------------------------------------
/sample/localTest/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | App,
3 | Request,
4 | Response,
5 | logger,
6 | parser,
7 | staticServe,
8 | } from "../../mod.ts";
9 | import router from "./router.ts";
10 |
11 | try {
12 | const app = new App();
13 |
14 | app.use(logger);
15 | app.use(parser);
16 |
17 | app.use("/", (req, res) => {
18 | res.status(200).send({ text: "hello" });
19 | });
20 |
21 | app.get("/error", (req, res) => {
22 | throw new Error("here");
23 | });
24 |
25 | app.error((err, req, res) => {
26 | console.log("in error handler", err);
27 | });
28 | console.log("http://localhost:3500");
29 |
30 | await app.listen(3500, { debug: true });
31 | } catch (error) {
32 | console.log("here i error, ", error);
33 | }
34 |
--------------------------------------------------------------------------------
/defaultHandler/defaultPageNotFound.ts:
--------------------------------------------------------------------------------
1 | import type { CallBackType } from "../mod.ts";
2 |
3 | export const defaultPageNotFound: CallBackType = (req, res) => {
4 | res.status(404).send(`
5 |
6 |
7 |
20 |
21 |
22 |
23 |
24 |
Sorry : '${req.url.pathname}' page not found
25 |
26 |
27 | `);
28 | };
29 |
--------------------------------------------------------------------------------
/start_test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | deno -V
4 |
5 | echo "start basic benchmark"
6 | deno test -A --unstable test/basicTest/
7 | status=$?
8 | if test $status -ne 0
9 | then
10 | echo "Error code - $status"
11 | exit $status
12 | fi
13 |
14 | echo "start graph benchmark"
15 | deno test -A --unstable test/benchmarks/graph_test.ts
16 | status=$?
17 | if test $status -ne 0
18 | then
19 | echo "Error code - $status"
20 | exit $status
21 | fi
22 |
23 | echo "start linear benchmark"
24 | deno test -A --unstable test/benchmarks/linear_test.ts
25 | status=$?
26 | if test $status -ne 0
27 | then
28 | echo "Error code - $status"
29 | exit $status
30 | fi
31 |
32 | echo "start database test"
33 | deno test -A --unstable test/database/
34 | status=$?
35 | if test $status -ne 0
36 | then
37 | echo "Error code - $status"
38 | exit $status
39 | fi
40 |
41 | exit 0
--------------------------------------------------------------------------------
/.github/workflows/attain-ci.yaml:
--------------------------------------------------------------------------------
1 |
2 | name: attain ci
3 |
4 | on: [push, pull_request]
5 |
6 | jobs:
7 | build:
8 | name: Attain test
9 | runs-on: ${{ matrix.os }}
10 | strategy:
11 | matrix:
12 | os: [macOS-latest, windows-latest, ubuntu-latest]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: denolib/setup-deno@master
17 | with:
18 | deno-version: 1.16.4
19 |
20 | - run: deno --version
21 |
22 | - name: run test (macOS)
23 | if: startsWith(matrix.os, 'mac')
24 | run: ./start_test.sh
25 |
26 | - name: run test (Linux)
27 | if: startsWith(matrix.os, 'ubuntu')
28 | run: ./start_test.sh
29 |
30 | - name: run test (windows)
31 | if: startsWith(matrix.os, 'windows')
32 | run: ./start_test.cmd
33 |
34 |
35 |
--------------------------------------------------------------------------------
/plugins/logger.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response } from "../mod.ts";
2 | import {
3 | green,
4 | cyan,
5 | red,
6 | yellow,
7 | bold,
8 | magenta,
9 | } from "../deps.ts";
10 |
11 | const colorize = (status: number) => {
12 | if (status >= 500) {
13 | return red(String(status));
14 | } else if(status >= 400) {
15 | return magenta(String(status));
16 | } else if (status >= 300) {
17 | return yellow(String(status));
18 | }
19 | return cyan(String(status));
20 | }
21 |
22 | export const logger = (_: Request, res: Response) => {
23 | res.pend(
24 | (pendReq, pendRes) => {
25 | const ms = Date.now() - pendReq.startDate;
26 | console.log(
27 | `${green(pendReq.method)} ${colorize(pendRes.getStatus!)} ${pendReq.url.pathname} - ${bold(String(ms))}ms`,
28 | );
29 | },
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/plugins/parser.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response } from "../mod.ts";
2 |
3 | const setQueryVariable = (req: Request) => {
4 | const queries = req.url.search && req.url.search.substring(1).split("&") ||
5 | [];
6 | if (queries.length > 0 && queries[0] !== "") {
7 | if (!req.query) {
8 | req.query = {};
9 | }
10 | queries.map((qs) => {
11 | const pair = qs.split("=");
12 | req.query[decodeURIComponent(pair[0])] = decodeURIComponent(
13 | pair[1] || "",
14 | );
15 | });
16 | }
17 | };
18 |
19 | export const parser = async (req: Request, res: Response) => {
20 | if (req.hasBody) {
21 | const params = await req.body();
22 | if (params.type === "json") {
23 | req.params = await params.value;
24 | } else {
25 | req.params = { value: await params.value };
26 | }
27 | }
28 | setQueryVariable(req);
29 | };
30 |
--------------------------------------------------------------------------------
/sample/postParsing/plugin.ts:
--------------------------------------------------------------------------------
1 | import {
2 | App,
3 | logger,
4 | parser,
5 | staticServe,
6 | } from "https://deno.land/x/attain/mod.ts";
7 | // import { App, logger, parser } from "https://deno.land/x/attain/mod.ts";
8 |
9 | const app = new App();
10 |
11 | // logging response method status path time
12 | app.use(logger);
13 |
14 | // parsing the request body and save it to request.params
15 | app.use(parser);
16 |
17 | // serve static files
18 | app.use(staticServe("./public"));
19 |
20 | app.use("/", (req, res) => {
21 | res.status(200).send("hello");
22 | });
23 |
24 | app.post("/submit", (req, res) => {
25 | console.log(req.params);
26 | res.status(200).send({ data: "has received" });
27 | });
28 |
29 | app.use((req, res) => {
30 | res.status(404).send("page not found");
31 | });
32 |
33 | app.listen(4000);
34 | console.log("Starting to listen at http://localhost:4000");
35 |
--------------------------------------------------------------------------------
/sample/basic/main.ts:
--------------------------------------------------------------------------------
1 | import {
2 | App,
3 | Router,
4 | Request,
5 | Response,
6 | logger,
7 | parser,
8 | } from "https://deno.land/x/attain/mod.ts";
9 | import api from "./router.ts";
10 |
11 | const app = new App();
12 |
13 | const sampleMiddleware = (req: Request, res: Response) => {
14 | console.log("before send");
15 | };
16 |
17 | app.use(logger);
18 | app.use(parser);
19 |
20 | app.use("/:id", sampleMiddleware, (req: Request, res: Response) => {
21 | console.log(req.params);
22 | res.status(200).send(`id: ${req.params.id}`);
23 | });
24 |
25 | app.use("/api", api);
26 |
27 | app.use(sampleMiddleware, (req: Request, res: Response) => {
28 | res.status(404).send(`
29 |
30 |
31 |
32 | Page not found
33 |
34 |
35 | `);
36 | });
37 |
38 | app.listen(3500, { debug: true });
39 |
40 | console.log("http://localhost:3500");
41 |
--------------------------------------------------------------------------------
/defaultHandler/defaultError.ts:
--------------------------------------------------------------------------------
1 | import type { ErrorCallBackType } from "../mod.ts";
2 |
3 | export const defaultError: ErrorCallBackType = (error: Error, req, res) => {
4 | res.status(500).send(`
5 |
6 |
7 |
20 |
21 |
22 |
23 |
24 |
${error.name ? error.name : "Danger"}: ${
25 | error.message ? error.message : error
26 | }
27 |
${error.stack ? error.stack : ""}
28 |
29 |
30 | `);
31 | };
32 |
--------------------------------------------------------------------------------
/sample/localTest/secondRouter.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "../../mod.ts";
2 | const api = new Router();
3 |
4 | const sleep = (time: number) =>
5 | new Promise((resolve) => setTimeout(() => resolve(null), time));
6 |
7 | // this will block the current request
8 | api.get("/test1", async (req, res) => {
9 | console.log("here '/test1'");
10 | await sleep(1000);
11 | res.status(200).send(`
12 |
13 |
14 |
15 | test1
16 |
17 |
18 | `);
19 | });
20 |
21 | // this will not work
22 | api.get("/error", async (req, res) => {
23 | console.log("here '/error'");
24 | throw new Error("Here i made a error");
25 | });
26 |
27 | api.get("/test3*", async (req, res) => {
28 | console.log(req.query, "here test3");
29 | res.status(200).send(`
30 |
31 |
32 |
33 | test3 done
34 |
35 |
36 | `);
37 | });
38 |
39 | export default api;
40 |
--------------------------------------------------------------------------------
/sample/basic/router.ts:
--------------------------------------------------------------------------------
1 | import { Router, Request, Response } from "https://deno.land/x/attain/mod.ts";
2 | import second from "./api.ts";
3 | const api = new Router();
4 |
5 | api.use("/", second);
6 |
7 | const sleep = (time: number) =>
8 | new Promise((resolve) => setTimeout(() => resolve(), time));
9 |
10 | // this will block the current request
11 | api.get("/hello", async (req: Request, res: Response) => {
12 | console.log("here '/hello'");
13 | await sleep(1000);
14 | res.status(200).send(`
15 |
16 |
17 |
18 | Hello
19 |
20 |
21 | `);
22 | });
23 |
24 | // this will not work
25 | api.get("/hello", async (req: Request, res: Response) => {
26 | console.log("here '/second hello'");
27 | res.status(200).send(`
28 |
29 |
30 |
31 | Second hello
32 |
33 |
34 | `);
35 | });
36 |
37 | api.use("/post", second);
38 |
39 | export default api;
40 |
--------------------------------------------------------------------------------
/plugins/staticServe.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response } from "../mod.ts";
2 | import { posix } from "../deps.ts"
3 |
4 |
5 | export const staticServe = (
6 | path: string,
7 | options?: { maxAge?: number },
8 | ) =>
9 | async (req: Request, res: Response) => {
10 | if (req.method !== "GET" && req.method !== "HEAD") {
11 | return;
12 | }
13 | const maxAge = options && options.maxAge || 0;
14 | const fullPath = posix.join(path, req.url.pathname);
15 | try {
16 | let fileInfo = await Deno.stat(fullPath);
17 | if (fileInfo.isFile) {
18 | res.setHeader("Cache-Control", `public`);
19 | res.getHeaders.append("Cache-Control", `max-age=${maxAge / 1000 | 0}`);
20 | var t = new Date();
21 | t.setSeconds(t.getSeconds() + maxAge);
22 | res.setHeader("Expires", t.toUTCString());
23 | await res.sendFile(fullPath);
24 | }
25 | } catch (e) {
26 | if (e instanceof Deno.errors.PermissionDenied) {
27 | console.error(e);
28 | }
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2022] [Aaron Wooseok Lee](https://github.com/aaronwlee)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/sample/localTest/router.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "../../mod.ts";
2 | import secondRouter from "./secondRouter.ts";
3 | const api = new Router();
4 |
5 | const sleep = (time: number) =>
6 | new Promise((resolve) => setTimeout(() => resolve(null), time));
7 |
8 | // this will block the current request
9 | api.get("/hello", async (req, res) => {
10 | console.log("here '/hello'");
11 | await sleep(1000);
12 | res.status(200).send(`
13 |
14 |
15 |
16 | Hello
17 |
18 |
19 | `);
20 | });
21 |
22 | // this will not work
23 | api.get("/second", async (req, res) => {
24 | console.log("here '/second hello'");
25 | res.status(200).send(`
26 |
27 |
28 |
29 | Second hello
30 |
31 |
32 | `);
33 | });
34 |
35 | api.get("/test(.*)", async (req, res) => {
36 | console.log(req.query, "here test");
37 | res.status(200).send(`
38 |
39 |
40 |
41 | test done
42 |
43 |
44 | `);
45 | });
46 |
47 | api.use("/second", secondRouter);
48 |
49 | export default api;
50 |
--------------------------------------------------------------------------------
/helmat/LICENSE:
--------------------------------------------------------------------------------
1 | https://github.com/helmetjs/helmet/blob/master/LICENSE
2 |
3 | (The MIT License)
4 |
5 | Copyright (c) 2012-2020 Evan Hahn, Adam Baldwin
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining
8 | a copy of this software and associated documentation files (the
9 | 'Software'), to deal in the Software without restriction, including
10 | without limitation the rights to use, copy, modify, merge, publish,
11 | distribute, sublicense, and/or sell copies of the Software, and to
12 | permit persons to whom the Software is furnished to do so, subject to
13 | the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be
16 | included in all copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/sample/localTest/cert/secret.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDajCCAlKgAwIBAgIJAOPyQVdy/UpPMA0GCSqGSIb3DQEBCwUAMCcxCzAJBgNV
3 | BAYTAlVTMRgwFgYDVQQDDA9FeGFtcGxlLVJvb3QtQ0EwIBcNMTkxMDIxMTYyODU4
4 | WhgPMjExODA5MjcxNjI4NThaMG0xCzAJBgNVBAYTAlVTMRIwEAYDVQQIDAlZb3Vy
5 | U3RhdGUxETAPBgNVBAcMCFlvdXJDaXR5MR0wGwYDVQQKDBRFeGFtcGxlLUNlcnRp
6 | ZmljYXRlczEYMBYGA1UEAwwPbG9jYWxob3N0LmxvY2FsMIIBIjANBgkqhkiG9w0B
7 | AQEFAAOCAQ8AMIIBCgKCAQEAz9svjVdf5jihUBtofd84XKdb8dEHQRJfDNKaJ4Ar
8 | baqMHAdnqi/fWtlqEEMn8gweZ7+4hshECY5mnx4Hhy7IAbePDsTTbSm01dChhlxF
9 | uvd9QuvzvrqSjSq+v4Jlau+pQIhUzzV12dF5bFvrIrGWxCZp+W7lLDZI6Pd6Su+y
10 | ZIeiwrUaPMzdUePNf2hZI/IvWCUMCIyoqrrKHdHoPuvQCW17IyxsnFQJNbmN+Rtp
11 | BQilhtwvBbggCBWhHxEdiqBaZHDw6Zl+bU7ejx1mu9A95wpQ9SCL2cRkAlz2LDOy
12 | wznrTAwGcvqvFKxlV+3HsaD7rba4kCA1Ihp5mm/dS2k94QIDAQABo1EwTzAfBgNV
13 | HSMEGDAWgBTzut+pwwDfqmMYcI9KNWRDhxcIpTAJBgNVHRMEAjAAMAsGA1UdDwQE
14 | AwIE8DAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEBAKVu
15 | vVpu5nPGAGn1SX4FQUcbn9Z5wgBkjnZxfJHJQX4sYIRlcirZviPHCZGPWex4VHC+
16 | lFMm+70YEN2uoe5jGrdgcugzx2Amc7/mLrsvvpMsaS0PlxNMcqhdM1WHbGjjdNln
17 | XICVITSKnB1fSGH6uo9CMCWw5kgPS9o4QWrLLkxnds3hoz7gVEUyi/6V65mcfFNA
18 | lof9iKcK9JsSHdBs35vpv7UKLX+96RM7Nm2Mu0yue5JiS79/zuMA/Kryxot4jv5z
19 | ecdWFl0eIyQBZmBzMw2zPUqkxEnXLiKjV8jutEg/4qovTOB6YiA41qbARXdzNA2V
20 | FYuchcTcWmnmVVRFyyU=
21 | -----END CERTIFICATE-----
--------------------------------------------------------------------------------
/helmat/x-xxs-protection.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response } from "../mod.ts";
2 |
3 | export interface XXssProtectionOptions {
4 | mode?: "block" | null;
5 | reportUri?: string;
6 | }
7 |
8 | const doesUserAgentMatchOldInternetExplorer = (
9 | userAgent: string | null | undefined,
10 | ): boolean => {
11 | if (!userAgent) {
12 | return false;
13 | }
14 |
15 | const matches = /msie\s*(\d{1,2})/i.exec(userAgent);
16 | return matches ? parseFloat(matches[1]) < 9 : false;
17 | };
18 |
19 | const getHeaderValueFromOptions = (options: XXssProtectionOptions): string => {
20 | const directives: string[] = ["1"];
21 |
22 | let isBlockMode: boolean;
23 | if ("mode" in options) {
24 | if (options.mode === "block") {
25 | isBlockMode = true;
26 | } else if (options.mode === null) {
27 | isBlockMode = false;
28 | } else {
29 | throw new Error('The `mode` option must be set to "block" or null.');
30 | }
31 | } else {
32 | isBlockMode = true;
33 | }
34 |
35 | if (isBlockMode) {
36 | directives.push("mode=block");
37 | }
38 |
39 | if (options.reportUri) {
40 | directives.push(`report=${options.reportUri}`);
41 | }
42 |
43 | return directives.join("; ");
44 | };
45 |
46 | export const xXssProtection = (options: XXssProtectionOptions = {}) => {
47 | const headerValue = getHeaderValueFromOptions(options);
48 |
49 | return (req: Request, res: Response) => {
50 | const value =
51 | doesUserAgentMatchOldInternetExplorer(req.headers.get("user-agent"))
52 | ? "0"
53 | : headerValue;
54 | res.setHeader("X-XSS-Protection", value);
55 | };
56 | };
57 |
--------------------------------------------------------------------------------
/test/basicTest/param.ts:
--------------------------------------------------------------------------------
1 | import { Router, parser } from "../../mod.ts";
2 |
3 | const sleep = (time: number = 100) =>
4 | new Promise((resolve) => {
5 | setTimeout(() => resolve(null), time);
6 | });
7 |
8 | /**
9 | * Test purpose
10 | *
11 | * - check param method
12 | */
13 | const paramTest = new Router();
14 |
15 | paramTest.param("username", async (req, res, username) => {
16 | await sleep();
17 | req.username = username;
18 | });
19 |
20 | paramTest.param("password", async (req, res, password) => {
21 | await sleep(500);
22 | if(password !== "123") {
23 | return res.status(404).send("password is not matched");
24 | }
25 | req.password = password;
26 | });
27 |
28 | paramTest.get("/:username", (req, res) => {
29 | res.send(req.username);
30 | });
31 |
32 | paramTest.get("/:username/:password", (req, res) => {
33 | res.send({ name: req.username, password: req.password });
34 | });
35 |
36 | paramTest.post("/:username/password", parser, (req, res) => {
37 | res.send({ name: req.username, password: req.params.password });
38 | });
39 |
40 | paramTest.post("/:username/post", async (req, res) => {
41 | res.send(req.username);
42 | });
43 |
44 | paramTest.patch("/:username/patch", async (req, res) => {
45 | res.send(req.username);
46 | });
47 |
48 | paramTest.put("/:username/put", async (req, res) => {
49 | res.send(req.username);
50 | });
51 |
52 | paramTest.delete("/:username/delete", async (req, res) => {
53 | res.send(req.username);
54 | });
55 |
56 | paramTest.options("/:username/options", async (req, res) => {
57 | res.send(req.username);
58 | });
59 |
60 |
61 |
62 | export default paramTest;
63 |
--------------------------------------------------------------------------------
/recovery.ts:
--------------------------------------------------------------------------------
1 | const fileName = Deno.args;
2 |
3 | // const decoder = new TextDecoder("utf-8");
4 | console.log("Auto Recovery mode with Attain");
5 | // let memoryInspect = null;
6 | const exec = async () => {
7 | const p = Deno.run({
8 | cmd: [
9 | "deno",
10 | "run",
11 | "-A",
12 | "--unstable",
13 | fileName[0],
14 | ],
15 | });
16 |
17 | // memoryInspect = setInterval(() => checkMem(p), 2000);
18 | const { code } = await p.status();
19 | console.log("code: ", code);
20 | console.warn("auto recovered!");
21 | p.close();
22 | // clearInterval(memoryInspect);
23 | await exec();
24 | };
25 |
26 | // const checkMem = async (p: any) => {
27 | // const p2 = Deno.run({
28 | // cmd: [
29 | // "wmic",
30 | // "process",
31 | // "where",
32 | // `processid=${p.pid}`,
33 | // "get",
34 | // "WorkingSetSize",
35 | // ],
36 | // stdout: "piped"
37 | // })
38 | // // "get-process chrome | select-object @{l='Private Memory (MB)'; e={$_.privatememorysize64 / 1mb}}"
39 |
40 | // const string = await p2.output();
41 | // console.log(formatBytes(parseInt(decoder.decode(string).split("\n")[1])));
42 | // }
43 |
44 | // function formatBytes(bytes: number, decimals = 4) {
45 | // if (bytes === 0) return '0 Bytes';
46 |
47 | // const k = 1024;
48 | // const dm = decimals < 0 ? 0 : decimals;
49 | // const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
50 |
51 | // const i = Math.floor(Math.log(bytes) / Math.log(k));
52 |
53 | // return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
54 | // }
55 |
56 | // await exec();
57 |
--------------------------------------------------------------------------------
/plugins/security.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response } from "../mod.ts";
2 | import {
3 | XXssProtectionOptions,
4 | xXssProtection,
5 | } from "../helmat/x-xxs-protection.ts";
6 | import { hidePoweredBy } from "../helmat/hide-powered-by.ts";
7 | import {
8 | dnsPrefetchControl,
9 | DnsPrefetchControlOptions,
10 | } from "../helmat/dns-prefetch-control.ts";
11 | import { dontSniffMimetype } from "../helmat/dont-sniff-mimetype.ts";
12 | import { frameGuard, FrameguardOptions } from "../helmat/frame-guard.ts";
13 |
14 | interface SecurityProps {
15 | xss?: XXssProtectionOptions | boolean;
16 | removePoweredBy?: boolean;
17 | DNSPrefetchControl?: DnsPrefetchControlOptions | boolean;
18 | noSniff?: boolean;
19 | frameguard?: FrameguardOptions | boolean;
20 | }
21 |
22 | export const security = (options?: SecurityProps) => {
23 | const {
24 | xss = true,
25 | removePoweredBy = true,
26 | DNSPrefetchControl = true,
27 | noSniff = true,
28 | frameguard = true,
29 | } = options || {};
30 | return function security(req: Request, res: Response) {
31 | if (xss) {
32 | typeof xss === "boolean"
33 | ? xXssProtection()(req, res)
34 | : xXssProtection(xss)(req, res);
35 | }
36 |
37 | if (removePoweredBy) {
38 | hidePoweredBy()(req, res);
39 | }
40 |
41 | if (noSniff) {
42 | dontSniffMimetype()(req, res);
43 | }
44 |
45 | if (DNSPrefetchControl) {
46 | typeof DNSPrefetchControl === "boolean"
47 | ? dnsPrefetchControl()(req, res)
48 | : dnsPrefetchControl(DNSPrefetchControl)(req, res);
49 | }
50 |
51 | if (frameguard) {
52 | typeof frameguard === "boolean"
53 | ? frameGuard()(req, res)
54 | : frameGuard(frameguard)(req, res);
55 | }
56 | };
57 | };
58 |
--------------------------------------------------------------------------------
/sample/localTest/cert/secret.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDP2y+NV1/mOKFQ
3 | G2h93zhcp1vx0QdBEl8M0pongCttqowcB2eqL99a2WoQQyfyDB5nv7iGyEQJjmaf
4 | HgeHLsgBt48OxNNtKbTV0KGGXEW6931C6/O+upKNKr6/gmVq76lAiFTPNXXZ0Xls
5 | W+sisZbEJmn5buUsNkjo93pK77Jkh6LCtRo8zN1R481/aFkj8i9YJQwIjKiqusod
6 | 0eg+69AJbXsjLGycVAk1uY35G2kFCKWG3C8FuCAIFaEfER2KoFpkcPDpmX5tTt6P
7 | HWa70D3nClD1IIvZxGQCXPYsM7LDOetMDAZy+q8UrGVX7cexoPuttriQIDUiGnma
8 | b91LaT3hAgMBAAECggEBAJABfn+BQorBP1m9s3ZJmcXvmW7+7/SwYrQCkRS+4te2
9 | 6h1dMAAj7K4HpUkhDeLPbJ1aoeCXjTPFuemRp4uL6Lvvzahgy059L7FXOyFYemMf
10 | pmQgDx5cKr6tF7yc/eDJrExuZ7urgTvouiRNxqmhuh+psZBDuXkZHwhwtQSH7uNg
11 | KBDKu0qWO73vFLcLckdGEU3+H9oIWs5xcvvOkWzyvHbRGFJSihgcRpPPHodF5xB9
12 | T/gZIoJHMmCbUMlWaSasUyNXTuvCnkvBDol8vXrMJCVzKZj9GpPDcIFdc08GSn4I
13 | pTdSNwzUcHbdERzdVU28Xt+t6W5rvp/4FWrssi4IzkUCgYEA//ZcEcBguRD4OFrx
14 | 6wbSjzCcUW1NWhzA8uTOORZi4SvndcH1cU4S2wznuHNubU1XlrGwJX6PUGebmY/l
15 | 53B5PJvStbVtZCVIxllR+ZVzRuL8wLodRHzlYH8GOzHwoa4ivSupkzl72ij1u/tI
16 | NMLGfYEKVdNd8zXIESUY88NszvsCgYEAz+MDp3xOhFaCe+CPv80A592cJcfzc8Al
17 | +rahEOu+VdN2QBZf86PIf2Bfv/t0QvnRvs1z648TuH6h83YSggOAbmfHyd789jkq
18 | UWlktIaXbVn+VaHmPTcBWTg3ZTlvG+fiFCbZXiYhm+UUf1MDqZHdiifAoyVIjV/Z
19 | YhCNJo3q39MCgYEAknrpK5t9fstwUcfyA/9OhnVaL9suVjB4V0iLn+3ovlXCywgp
20 | ryLv9X3IKi2c9144jtu3I23vFCOGz3WjKzSZnQ7LogNmy9XudNxu5jcZ1mpWHPEl
21 | iKk1F2j6Juwoek5OQRX4oHFYKHwiTOa75r3Em9Q6Fu20KVgQ24bwZafj3/sCgYAy
22 | k0AoVw2jFIjaKl/Ogclen4OFjYek+XJD9Hpq62964d866Dafx5DXrFKfGkXGpZBp
23 | owI4pK5fjC9KU8dc6g0szwLEEgPowy+QbtuZL8VXTTWbD7A75E3nrs2LStXFLDzM
24 | OkdXqF801h6Oe1vAvUPwgItVJZTpEBCK0wwD/TLPEQKBgQDRkhlTtAoHW7W6STd0
25 | A/OWc0dxhzMurpxg0bLgCqUjw1ESGrSCGhffFn0IWa8sv19VWsZuBhTgjNatZsYB
26 | AhDs/6OosT/3nJoh2/t0hYDj1FBI0lPXWYD4pesuZ5yIMrmSaAOtIzp4BGY7ui8N
27 | wOqcq/jdiHj/MKEdqOXy3YAJrA==
28 | -----END PRIVATE KEY-----
--------------------------------------------------------------------------------
/deps.ts:
--------------------------------------------------------------------------------
1 | export type {
2 | Server,
3 | } from "https://deno.land/std@0.117.0/http/server.ts";
4 |
5 |
6 | export {
7 | Status,
8 | STATUS_TEXT,
9 | } from "https://deno.land/std@0.117.0/http/http_status.ts";
10 |
11 | export {
12 | contentType,
13 | extension,
14 | lookup,
15 | } from "https://deno.land/x/media_types/mod.ts";
16 |
17 | export {
18 | deferred,
19 | } from "https://deno.land/std@0.117.0/async/mod.ts";
20 |
21 | export type { Deferred } from "https://deno.land/std@0.117.0/async/mod.ts";
22 |
23 | export {
24 | Sha1,
25 | } from "https://deno.land/std@0.117.0/hash/sha1.ts";
26 |
27 | export {
28 | pathToRegexp,
29 | match,
30 | } from "https://raw.githubusercontent.com/pillarjs/path-to-regexp/master/src/index.ts";
31 |
32 | export {
33 | red,
34 | yellow,
35 | green,
36 | cyan,
37 | bold,
38 | blue,
39 | magenta,
40 | } from "https://deno.land/std@0.117.0/fmt/colors.ts";
41 |
42 | export { default as isEmpty } from "https://raw.githubusercontent.com/lodash/lodash/master/isEmpty.js";
43 |
44 | export { extname } from "https://deno.land/std@0.117.0/path/mod.ts";
45 |
46 | export { parse } from "https://deno.land/std@0.117.0/flags/mod.ts";
47 | export { ensureDir } from "https://deno.land/std@0.117.0/fs/mod.ts";
48 | export { EventEmitter } from "https://deno.land/std@0.117.0/node/events.ts";
49 | export { listenAndServe } from "https://deno.land/std@0.117.0/http/server.ts";
50 | export {
51 | acceptWebSocket,
52 | acceptable,
53 | isWebSocketCloseEvent,
54 | } from "https://deno.land/std@0.117.0/ws/mod.ts";
55 | export {
56 | parse as yamlParse,
57 | stringify as yamlStringify,
58 | } from "https://deno.land/std@0.117.0/encoding/yaml.ts";
59 |
60 | export { assert } from "https://deno.land/std@0.117.0/testing/asserts.ts";
61 |
62 | export { equals } from "https://deno.land/std@0.117.0/bytes/mod.ts";
63 | export {
64 | posix,
65 | } from "https://deno.land/std@0.117.0/path/mod.ts";
66 |
--------------------------------------------------------------------------------
/sample/ws/client.ts:
--------------------------------------------------------------------------------
1 | import {
2 | connectWebSocket,
3 | isWebSocketCloseEvent,
4 | isWebSocketPingEvent,
5 | isWebSocketPongEvent,
6 | } from "https://deno.land/std/ws/mod.ts";
7 | import { encode } from "https://deno.land/std/encoding/utf8.ts";
8 | import { BufReader } from "https://deno.land/std/io/bufio.ts";
9 | import { TextProtoReader } from "https://deno.land/std/textproto/mod.ts";
10 | import { blue, green, red, yellow } from "https://deno.land/std/fmt/colors.ts";
11 |
12 | const endpoint = Deno.args[0] || "ws://127.0.0.1:8080/socket";
13 | /** simple websocket cli */
14 | try {
15 | const sock = await connectWebSocket(endpoint);
16 | console.log(green("ws connected! (type 'close' to quit)"));
17 |
18 | const messages = async (): Promise => {
19 | for await (const msg of sock) {
20 | if (typeof msg === "string") {
21 | console.log(yellow(`< ${msg}`));
22 | } else if (isWebSocketPingEvent(msg)) {
23 | console.log(blue("< ping"));
24 | } else if (isWebSocketPongEvent(msg)) {
25 | console.log(blue("< pong"));
26 | } else if (isWebSocketCloseEvent(msg)) {
27 | console.log(red(`closed: code=${msg.code}, reason=${msg.reason}`));
28 | }
29 | }
30 | };
31 |
32 | const cli = async (): Promise => {
33 | const tpr = new TextProtoReader(new BufReader(Deno.stdin));
34 | while (true) {
35 | await Deno.stdout.write(encode("> "));
36 | const line = await tpr.readLine();
37 | if (line === null || line === "close") {
38 | break;
39 | } else if (line === "ping") {
40 | await sock.ping();
41 | } else {
42 | await sock.send(line);
43 | }
44 | }
45 | };
46 |
47 | await Promise.race([messages(), cli()]).catch(console.error);
48 |
49 | if (!sock.isClosed) {
50 | await sock.close(1000).catch(console.error);
51 | }
52 | } catch (err) {
53 | console.error(red(`Could not connect to WebSocket: '${err}'`));
54 | }
55 |
56 | Deno.exit(0);
57 |
--------------------------------------------------------------------------------
/sample/ws/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | App,
3 | logger,
4 | security,
5 | } from "../../mod.ts";
6 | import {
7 | acceptWebSocket,
8 | isWebSocketCloseEvent,
9 | isWebSocketPingEvent,
10 | } from "https://deno.land/std/ws/mod.ts";
11 |
12 | const app = new App();
13 |
14 | app.use(logger);
15 | app.use(security());
16 |
17 | app.use((req, res) => {
18 | console.log("first start");
19 | });
20 |
21 | app.get("/hello", async (req, res) => {
22 | await res.status(200).send("hello");
23 | });
24 |
25 | app.use("/socket", async (req, res) => {
26 | console.log(req.method);
27 | const { conn, r: bufReader, w: bufWriter, headers } = req.serverRequest;
28 |
29 | try {
30 | const sock = await acceptWebSocket({
31 | conn,
32 | bufReader,
33 | bufWriter,
34 | headers,
35 | });
36 |
37 | console.log("socket connected!");
38 |
39 | try {
40 | for await (const ev of sock) {
41 | if (typeof ev === "string") {
42 | // text message
43 | console.log("ws:Text", ev);
44 | await sock.send(ev);
45 | } else if (ev instanceof Uint8Array) {
46 | // binary message
47 | console.log("ws:Binary", ev);
48 | } else if (isWebSocketPingEvent(ev)) {
49 | const [, body] = ev;
50 | // ping
51 | console.log("ws:Ping", body);
52 | } else if (isWebSocketCloseEvent(ev)) {
53 | // close
54 | const { code, reason } = ev;
55 | console.log("ws:Close", code, reason);
56 | }
57 | }
58 | } catch (err) {
59 | console.error(`failed to receive frame: ${err}`);
60 |
61 | if (!sock.isClosed) {
62 | await sock.close(1000).catch(console.error);
63 | }
64 | }
65 |
66 | console.log("socket connection end");
67 | res.end();
68 | } catch (err) {
69 | console.error(`failed to accept websocket: ${err}`);
70 | await res.status(400).send("error");
71 | }
72 | });
73 |
74 | console.log("Server at http://localhost:8080");
75 | await app.listen(8080);
76 |
--------------------------------------------------------------------------------
/helmat/frame-guard.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response } from "../mod.ts";
2 |
3 | export interface FrameguardOptions {
4 | action?: string;
5 | domain?: string;
6 | }
7 |
8 | function parseActionOption(actionOption: unknown): string {
9 | const invalidActionErr = new Error(
10 | 'action must be undefined, "DENY", "ALLOW-FROM", or "SAMEORIGIN".',
11 | );
12 |
13 | if (actionOption === undefined) {
14 | actionOption = "SAMEORIGIN";
15 | } else if (actionOption instanceof String) {
16 | actionOption = actionOption.valueOf();
17 | }
18 |
19 | let result: string;
20 | if (typeof actionOption === "string") {
21 | result = actionOption.toUpperCase();
22 | } else {
23 | throw invalidActionErr;
24 | }
25 |
26 | if (result === "ALLOWFROM") {
27 | result = "ALLOW-FROM";
28 | } else if (result === "SAME-ORIGIN") {
29 | result = "SAMEORIGIN";
30 | }
31 |
32 | if (["DENY", "ALLOW-FROM", "SAMEORIGIN"].indexOf(result) === -1) {
33 | throw invalidActionErr;
34 | }
35 |
36 | return result;
37 | }
38 |
39 | function parseDomainOption(domainOption: unknown): string {
40 | if (domainOption instanceof String) {
41 | domainOption = domainOption.valueOf();
42 | }
43 |
44 | if (typeof domainOption !== "string") {
45 | throw new Error("ALLOW-FROM action requires a string domain parameter.");
46 | } else if (!domainOption.length) {
47 | throw new Error("domain parameter must not be empty.");
48 | }
49 |
50 | return domainOption;
51 | }
52 |
53 | function getHeaderValueFromOptions(options?: FrameguardOptions): string {
54 | options = options || {};
55 |
56 | const action = parseActionOption(options.action);
57 |
58 | if (action === "ALLOW-FROM") {
59 | const domain = parseDomainOption(options.domain);
60 | return `${action} ${domain}`;
61 | } else {
62 | return action;
63 | }
64 | }
65 |
66 | export const frameGuard = (options?: FrameguardOptions) => {
67 | const headerValue = getHeaderValueFromOptions(options);
68 |
69 | return (_req: Request, res: Response) => {
70 | res.setHeader("X-Frame-Options", headerValue);
71 | };
72 | };
73 |
--------------------------------------------------------------------------------
/test/database/database_test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertEquals,
3 | bench,
4 | runBenchmarks,
5 | } from "../test_deps.ts";
6 | import { App, Router, AttainDatabase } from "../../mod.ts";
7 |
8 | const port = 8564;
9 |
10 | class SampleDatabaseClass extends AttainDatabase {
11 | #connection: boolean = false;
12 | async connect() {
13 | console.log("fake DB connected");
14 | this.#connection = true;
15 | }
16 |
17 | get connection() {
18 | return this.#connection
19 | }
20 | }
21 |
22 | async function SampleDatabaseConnector() {
23 | const instance = new SampleDatabaseClass()
24 | await instance.connect()
25 | return instance.connection
26 | }
27 |
28 | const app1 = new App();
29 | app1.database(SampleDatabaseClass)
30 | app1.use((req, res, db) => {
31 | Deno.test("'database' method with a class instance", () => {
32 | assertEquals(db.connection, true)
33 | })
34 |
35 | res.send("Ok")
36 | })
37 | app1.listen(port);
38 |
39 | await fetch(
40 | `http://localhost:${port}`,
41 | );
42 |
43 |
44 |
45 | const app2 = new App();
46 | app2.database(SampleDatabaseConnector)
47 | app2.use((req, res, db) => {
48 | Deno.test("'database' method with a function", () => {
49 | assertEquals(db, true)
50 | })
51 |
52 | res.send("Ok")
53 | })
54 | app2.listen(port + 1);
55 |
56 | await fetch(
57 | `http://localhost:${port + 1}`,
58 | );
59 |
60 |
61 | const app3 = App.startWith(SampleDatabaseClass);
62 | app3.use((req, res, db) => {
63 | Deno.test("'startWith' method with a class instance", () => {
64 | assertEquals(db.connection, true)
65 | })
66 |
67 | res.send("Ok")
68 | })
69 | app3.listen(port + 2);
70 |
71 | await fetch(
72 | `http://localhost:${port + 2}`,
73 | );
74 |
75 |
76 | const app4 = App.startWith(SampleDatabaseConnector);
77 | app4.use((req, res, db) => {
78 | Deno.test("'startWith' method with a function", () => {
79 | assertEquals(db, true)
80 | })
81 |
82 | res.send("Ok")
83 | })
84 | app4.listen(port + 3);
85 |
86 | await fetch(
87 | `http://localhost:${port + 3}`,
88 | );
89 |
90 |
91 | await app1.close();
92 | await app2.close();
93 | await app3.close();
94 | await app4.close();
--------------------------------------------------------------------------------
/test/benchmarks/graph_test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertEquals,
3 | bench,
4 | runBenchmarks,
5 | } from "../test_deps.ts";
6 | import { App, Router } from "../../mod.ts";
7 |
8 | const app = new App();
9 |
10 | const createLinearGraph = (current: number, max: number) => {
11 | const newRouter = new Router();
12 | newRouter.get(`/${current}`, (req, res) => {
13 | res.send(current.toString());
14 | });
15 | if (current !== max) {
16 | newRouter.use(createLinearGraph(current + 1, max));
17 | }
18 | return newRouter;
19 | };
20 |
21 | const createNestedGraph = (current: number, max: number) => {
22 | const newRouter = new Router();
23 | newRouter.get(`/${current}`, (req, res) => {
24 | res.send(current.toString());
25 | });
26 | if (current !== max) {
27 | newRouter.use(`/${current}`, createNestedGraph(current + 1, max));
28 | }
29 | return newRouter;
30 | };
31 |
32 | app.use(createLinearGraph(0, 100));
33 |
34 | app.use("/nested", createNestedGraph(0, 100));
35 |
36 | app.listen(9550);
37 |
38 | /**
39 | * prepare section
40 | */
41 | const temp = [];
42 | for (let i = 0; i < 101; i++) {
43 | temp.push(i);
44 | }
45 | const urlForNested = temp.join("/");
46 |
47 | bench({
48 | name: "warming up",
49 | runs: 1,
50 | async func(b: any): Promise {
51 | b.start();
52 | const response = await fetch(`http://localhost:9550/0`);
53 | const data = await response.text();
54 | assertEquals(data, "0");
55 | b.stop();
56 | },
57 | });
58 |
59 | bench({
60 | name: "GET: linear/10",
61 | runs: 1,
62 | async func(b: any): Promise {
63 | b.start();
64 | const response = await fetch(`http://localhost:9550/${100}`);
65 | const data = await response.text();
66 | assertEquals(data, "100");
67 | b.stop();
68 | },
69 | });
70 |
71 | bench({
72 | name: "GET: nested/0...100 ",
73 | runs: 1,
74 | async func(b: any): Promise {
75 | b.start();
76 | const response = await fetch(
77 | `http://localhost:9550/nested/${urlForNested}`,
78 | );
79 | const data = await response.text();
80 | assertEquals(data, "100");
81 | b.stop();
82 | },
83 | });
84 |
85 | await runBenchmarks();
86 |
87 | await app.close();
88 |
--------------------------------------------------------------------------------
/test/benchmarks/linear_test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertEquals,
3 | bench,
4 | runBenchmarks,
5 | } from "../test_deps.ts";
6 | import { App, staticServe } from "../../mod.ts";
7 |
8 | const app = new App();
9 |
10 | for (let i = 0; i < 10001; i++) {
11 | app.use(`/${i}`, (req, res) => {
12 | res.send(i.toString());
13 | });
14 | }
15 |
16 | app.listen(9555);
17 |
18 | bench({
19 | name: "warming up",
20 | runs: 1,
21 | async func(b: any): Promise {
22 | b.start();
23 | const response = await fetch(`http://localhost:9555/${0}`);
24 | const data = await response.text();
25 | assertEquals(data, "0");
26 | b.stop();
27 | },
28 | });
29 |
30 | bench({
31 | name: "GET: /0",
32 | runs: 1,
33 | async func(b: any): Promise {
34 | b.start();
35 | const response = await fetch(`http://localhost:9555/${0}`);
36 | const data = await response.text();
37 | assertEquals(data, "0");
38 | b.stop();
39 | },
40 | });
41 |
42 | bench({
43 | name: "GET: /10",
44 | runs: 1,
45 | async func(b: any): Promise {
46 | b.start();
47 | const response = await fetch(`http://localhost:9555/${10}`);
48 | const data = await response.text();
49 | assertEquals(data, "10");
50 | b.stop();
51 | },
52 | });
53 |
54 | bench({
55 | name: "GET: /100",
56 | runs: 1,
57 | async func(b: any): Promise {
58 | b.start();
59 | const response = await fetch(`http://localhost:9555/${100}`);
60 | const data = await response.text();
61 | assertEquals(data, "100");
62 | b.stop();
63 | },
64 | });
65 |
66 | bench({
67 | name: "GET: /1000",
68 | runs: 1,
69 | async func(b: any): Promise {
70 | b.start();
71 | const response = await fetch(`http://localhost:9555/${1000}`);
72 | const data = await response.text();
73 | assertEquals(data, "1000");
74 | b.stop();
75 | },
76 | });
77 |
78 | bench({
79 | name: "GET: /5000",
80 | runs: 1,
81 | async func(b: any): Promise {
82 | b.start();
83 | const response = await fetch(`http://localhost:9555/${5000}`);
84 | const data = await response.text();
85 | assertEquals(data, "5000");
86 | b.stop();
87 | },
88 | });
89 |
90 | bench({
91 | name: "GET: /10000",
92 | runs: 1,
93 | async func(b: any): Promise {
94 | b.start();
95 | const response = await fetch(`http://localhost:9555/${10000}`);
96 | const data = await response.text();
97 | assertEquals(data, "10000");
98 | b.stop();
99 | },
100 | });
101 |
102 | await runBenchmarks();
103 |
104 | await app.close();
105 |
--------------------------------------------------------------------------------
/core/debug.ts:
--------------------------------------------------------------------------------
1 | import type { MiddlewareProps, ErrorMiddlewareProps } from "./types.ts";
2 | import {
3 | yellow,
4 | cyan,
5 | green,
6 | red
7 | } from "../deps.ts";
8 |
9 |
10 | export async function circulateMiddlewares(
11 | currentMiddlewares: MiddlewareProps[],
12 | step: number = 0,
13 | ) {
14 | for (const current of currentMiddlewares) {
15 | if (current.next) {
16 | const currentIndent = step + 1;
17 | const nextIndent = step + 2;
18 | console.log(" ".repeat(step) + "{");
19 | current.method &&
20 | console.log(
21 | " ".repeat(currentIndent) + `method: ${cyan(current.method)},`,
22 | );
23 | current.url &&
24 | console.log(
25 | " ".repeat(currentIndent) + `url: ${green(current.url)},`,
26 | );
27 | console.log(" ".repeat(currentIndent) + `next: [`);
28 | circulateMiddlewares(current.next, nextIndent);
29 | console.log(" ".repeat(currentIndent) + `]`);
30 | console.log(" ".repeat(step) + "}");
31 | } else {
32 | console.log(`${" ".repeat(step)}{`);
33 | current.method &&
34 | console.log(
35 | `${" ".repeat(step + 1)}method: ${cyan(current.method)}`,
36 | );
37 | current.url &&
38 | console.log(`${" ".repeat(step + 1)}url: ${green(current.url)}`);
39 | current.paramHandlers &&
40 | console.log(
41 | `${" ".repeat(step + 1)}paramHandlers: [${
42 | red((current.paramHandlers.map((e) => e.paramName)).join(", "))
43 | }]`,
44 | );
45 | current.callBack &&
46 | console.log(
47 | `${" ".repeat(step + 1)}callBack: ${
48 | yellow(current.callBack.name || "Anonymous")
49 | }`,
50 | );
51 | console.log(`${" ".repeat(step)}},`);
52 | }
53 | }
54 | };
55 |
56 | export async function circulateErrorMiddlewares(
57 | currentErrorMiddlewares: ErrorMiddlewareProps[],
58 | step: number = 0,
59 | ) {
60 | for (const current of currentErrorMiddlewares) {
61 | if (current.next) {
62 | const currentIndent = step + 1;
63 | const nextIndent = step + 2;
64 | console.log(" ".repeat(step) + "{");
65 | current.url &&
66 | console.log(
67 | " ".repeat(currentIndent) + `url: ${green(current.url)},`,
68 | );
69 | console.log(" ".repeat(currentIndent) + `next: [`);
70 | circulateErrorMiddlewares(current.next, nextIndent);
71 | console.log(" ".repeat(currentIndent) + `]`);
72 | console.log(" ".repeat(step) + "}");
73 | } else {
74 | console.log(`${" ".repeat(step)}{`);
75 | current.url &&
76 | console.log(`${" ".repeat(step + 1)}url: ${green(current.url)}`);
77 | current.callBack &&
78 | console.log(
79 | `${" ".repeat(step + 1)}callBack: ${
80 | yellow(current.callBack.name || "Anonymous")
81 | }`,
82 | );
83 | console.log(`${" ".repeat(step)}},`);
84 | }
85 | }
86 | };
87 |
88 |
--------------------------------------------------------------------------------
/core/types.ts:
--------------------------------------------------------------------------------
1 | import type { Status } from "../deps.ts";
2 | import type { AttainRequest } from "./request.ts";
3 | import type { AttainResponse } from "./response.ts";
4 |
5 | export type ThenArg = T extends PromiseLike ? U : T;
6 |
7 | export interface AttainListenProps {
8 | debug?: boolean;
9 | secure?: boolean;
10 | hostname?: string;
11 | certFile?: string;
12 | keyFile?: string;
13 |
14 | transport?: "tcp";
15 | }
16 |
17 | export type SupportMethodType =
18 | | "HEAD"
19 | | "OPTIONS"
20 | | "ALL"
21 | | "GET"
22 | | "POST"
23 | | "PUT"
24 | | "PATCH"
25 | | "DELETE";
26 |
27 | export type CallBackType = (
28 | request: AttainRequest,
29 | response: AttainResponse,
30 | db: T,
31 | ) => Promise | void;
32 |
33 | export type ParamCallBackType = (
34 | request: AttainRequest,
35 | response: AttainResponse,
36 | param: any,
37 | db: T,
38 | ) => Promise | void;
39 |
40 | export type ErrorCallBackType = (
41 | error: any,
42 | request: AttainRequest,
43 | response: AttainResponse,
44 | db: T,
45 | ) => Promise | void;
46 |
47 | export interface MiddlewareProps {
48 | url?: string;
49 | paramHandlers?: ParamStackProps[];
50 | callBack?: CallBackType;
51 | method?: SupportMethodType;
52 | next?: MiddlewareProps[];
53 | }
54 |
55 | export interface ParamStackProps {
56 | paramName: string;
57 | callBack: ParamCallBackType;
58 | }
59 |
60 | export interface ErrorMiddlewareProps {
61 | url?: string;
62 | callBack?: ErrorCallBackType;
63 | next?: ErrorMiddlewareProps[];
64 | }
65 |
66 | // Copyright 2018-2020 the oak authors. All rights reserved. MIT license.
67 | /** A HTTP status that is an error (4XX and 5XX). */
68 | export type ErrorStatus =
69 | | Status.BadRequest
70 | | Status.Unauthorized
71 | | Status.PaymentRequired
72 | | Status.Forbidden
73 | | Status.NotFound
74 | | Status.MethodNotAllowed
75 | | Status.NotAcceptable
76 | | Status.ProxyAuthRequired
77 | | Status.RequestTimeout
78 | | Status.Conflict
79 | | Status.Gone
80 | | Status.LengthRequired
81 | | Status.PreconditionFailed
82 | | Status.RequestEntityTooLarge
83 | | Status.RequestURITooLong
84 | | Status.UnsupportedMediaType
85 | | Status.RequestedRangeNotSatisfiable
86 | | Status.ExpectationFailed
87 | | Status.Teapot
88 | | Status.MisdirectedRequest
89 | | Status.UnprocessableEntity
90 | | Status.Locked
91 | | Status.FailedDependency
92 | | Status.UpgradeRequired
93 | | Status.PreconditionRequired
94 | | Status.TooManyRequests
95 | | Status.RequestHeaderFieldsTooLarge
96 | | Status.UnavailableForLegalReasons
97 | | Status.InternalServerError
98 | | Status.NotImplemented
99 | | Status.BadGateway
100 | | Status.ServiceUnavailable
101 | | Status.GatewayTimeout
102 | | Status.HTTPVersionNotSupported
103 | | Status.VariantAlsoNegotiates
104 | | Status.InsufficientStorage
105 | | Status.LoopDetected
106 | | Status.NotExtended
107 | | Status.NetworkAuthenticationRequired;
108 |
--------------------------------------------------------------------------------
/howto/websocket.md:
--------------------------------------------------------------------------------
1 | # WEB Socket
2 | This example is combined the WebSocket with Attain
3 |
4 | ## Attain Server
5 |
6 | ```ts
7 | import {
8 | App,
9 | logger,
10 | security
11 | } from "../../mod.ts";
12 | import {
13 | acceptWebSocket,
14 | isWebSocketCloseEvent,
15 | isWebSocketPingEvent,
16 | } from "https://deno.land/std/ws/mod.ts";
17 |
18 | const app = new App();
19 |
20 | app.use(logger);
21 | app.use(security());
22 |
23 | app.use((req, res) => {
24 | console.log("first start");
25 | })
26 |
27 | app.get("/hello", async (req, res) => {
28 | await res.status(200).send("hello");
29 | });
30 |
31 | app.use("/socket", async (req, res) => {
32 | console.log(req.method)
33 | const { conn, r: bufReader, w: bufWriter, headers } = req.serverRequest;
34 |
35 | try {
36 | const sock = await acceptWebSocket({
37 | conn,
38 | bufReader,
39 | bufWriter,
40 | headers,
41 | });
42 |
43 | console.log("socket connected!");
44 |
45 | try {
46 | for await (const ev of sock) {
47 | if (typeof ev === "string") {
48 | // text message
49 | console.log("ws:Text", ev);
50 | await sock.send(ev);
51 | } else if (ev instanceof Uint8Array) {
52 | // binary message
53 | console.log("ws:Binary", ev);
54 | } else if (isWebSocketPingEvent(ev)) {
55 | const [, body] = ev;
56 | // ping
57 | console.log("ws:Ping", body);
58 | } else if (isWebSocketCloseEvent(ev)) {
59 | // close
60 | const { code, reason } = ev;
61 | console.log("ws:Close", code, reason);
62 | }
63 | }
64 | } catch (err) {
65 | console.error(`failed to receive frame: ${err}`);
66 |
67 | if (!sock.isClosed) {
68 | await sock.close(1000).catch(console.error);
69 | }
70 | }
71 |
72 | // make it sure it's end;
73 | res.end();
74 | } catch (err) {
75 | console.error(`failed to accept websocket: ${err}`);
76 | await res.status(400).send("error");
77 | }
78 | })
79 |
80 | ```
81 |
82 | ## Simple Client
83 |
84 | ```ts
85 | import {
86 | connectWebSocket,
87 | isWebSocketCloseEvent,
88 | isWebSocketPingEvent,
89 | isWebSocketPongEvent,
90 | } from "https://deno.land/std/ws/mod.ts";
91 | import { encode } from "https://deno.land/std/encoding/utf8.ts";
92 | import { BufReader } from "https://deno.land/std/io/bufio.ts";
93 | import { TextProtoReader } from "https://deno.land/std/textproto/mod.ts";
94 | import { blue, green, red, yellow } from "https://deno.land/std/fmt/colors.ts";
95 |
96 | const endpoint = Deno.args[0] || "ws://127.0.0.1:8080/socket";
97 | /** simple websocket cli */
98 | try {
99 | const sock = await connectWebSocket(endpoint);
100 | console.log(green("ws connected! (type 'close' to quit)"));
101 |
102 | const messages = async (): Promise => {
103 | for await (const msg of sock) {
104 | if (typeof msg === "string") {
105 | console.log(yellow(`< ${msg}`));
106 | } else if (isWebSocketPingEvent(msg)) {
107 | console.log(blue("< ping"));
108 | } else if (isWebSocketPongEvent(msg)) {
109 | console.log(blue("< pong"));
110 | } else if (isWebSocketCloseEvent(msg)) {
111 | console.log(red(`closed: code=${msg.code}, reason=${msg.reason}`));
112 | }
113 | }
114 | };
115 |
116 | const cli = async (): Promise => {
117 | const tpr = new TextProtoReader(new BufReader(Deno.stdin));
118 | while (true) {
119 | await Deno.stdout.write(encode("> "));
120 | const line = await tpr.readLine();
121 | if (line === null || line === "close") {
122 | break;
123 | } else if (line === "ping") {
124 | await sock.ping();
125 | } else {
126 | await sock.send(line);
127 | }
128 | }
129 | };
130 |
131 | await Promise.race([messages(), cli()]).catch(console.error);
132 |
133 | if (!sock.isClosed) {
134 | await sock.close(1000).catch(console.error);
135 | }
136 | } catch (err) {
137 | console.error(red(`Could not connect to WebSocket: '${err}'`));
138 | }
139 |
140 | Deno.exit(0);
141 | ```
142 |
143 | For more information [WS](https://deno.land/std/ws)
144 |
--------------------------------------------------------------------------------
/core/application.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "./router.ts";
2 | import { blue, cyan, green, red } from "../deps.ts";
3 | import type { AttainListenProps, ThenArg } from "./types.ts";
4 | import version from "../version.ts";
5 | import { defaultError, defaultPageNotFound } from "../defaultHandler/index.ts";
6 | import { AttainHandler } from "./process.ts";
7 | import { circulateErrorMiddlewares, circulateMiddlewares } from "./debug.ts";
8 | import type { AttainDatabase, NoParamConstructor } from "./database.ts";
9 |
10 | export class App extends Router {
11 | #serve?: Deno.Listener;
12 | #serveTLS?: Deno.Listener;
13 | #process?: Promise;
14 | #processTLS?: Promise;
15 | #database?: T;
16 | #databaseInitializer?: () => Promise;
17 | #handler?: AttainHandler;
18 |
19 | #debug = async () => {
20 | console.log(red("------- Debug Middlewares -----------------"));
21 | circulateMiddlewares(this.middlewares);
22 | console.log(red("------- End Debug Middlewares -------------\n"));
23 |
24 | console.log(red("------- Debug Error Middlewares -----------"));
25 | circulateErrorMiddlewares(this.errorMiddlewares);
26 | console.log(red("------- End Debug Error Middlewares -------\n"));
27 | };
28 |
29 | close = async () => {
30 | if (this.#serve) {
31 | this.#serve.close();
32 | await this.#process;
33 | }
34 |
35 | if (this.#serveTLS) {
36 | this.#serveTLS.close();
37 | await this.#processTLS;
38 | }
39 | };
40 |
41 | /**
42 | * Start to listen
43 | */
44 | listen = (
45 | port: number,
46 | linstenProps?: AttainListenProps,
47 | ) => {
48 | const options = { hostname: "0.0.0.0", debug: false, ...linstenProps };
49 | const mode = Deno.env.get("PRODUCTION")
50 | ? "PRODUCTION"
51 | : (Deno.env.get("DEVELOPMENT") ? "DEVELOPMENT" : "GENERAL");
52 | this.use(defaultPageNotFound);
53 | this.error(defaultError);
54 |
55 | if (options.debug) {
56 | this.#debug();
57 | }
58 |
59 | if (options.secure) {
60 | if (!options.keyFile || !options.certFile) {
61 | throw "TLS mode require keyFile and certFile options.";
62 | }
63 | }
64 |
65 | console.log(
66 | `[${blue(mode)}] ${cyan("Attain FrameWork")} ${
67 | blue("v" + version.toString())
68 | } - ${green("Ready!")}`,
69 | );
70 |
71 | if (options.secure && options.keyFile && options.certFile) {
72 | this.#serveTLS = Deno.listenTls({
73 | hostname: options.hostname,
74 | port,
75 | keyFile: options.keyFile,
76 | certFile: options.certFile,
77 | });
78 | this.#processTLS = this.#start(this.#serveTLS, options.secure);
79 | } else {
80 | this.#serve = Deno.listen({ hostname: options.hostname, port });
81 | this.#process = this.#start(this.#serve);
82 | }
83 | console.log(
84 | `Server running at ${
85 | options.secure ? "https:" : "http:"
86 | }//localhost:${port}`,
87 | );
88 | };
89 |
90 | #start = async (server: Deno.Listener, secure: boolean = false) => {
91 | if (this.#database) {
92 | await (this.#database as any).connect();
93 | this.#handler = new AttainHandler(this.#database, secure);
94 | } else if (this.#databaseInitializer) {
95 | this.#database = await this.#databaseInitializer();
96 | this.#handler = new AttainHandler(this.#database, secure);
97 | } else {
98 | this.#handler = new AttainHandler(undefined!, secure);
99 | }
100 |
101 | for await (const conn of server) {
102 | (async () => {
103 | const httpConn = Deno.serveHttp(conn);
104 | for await (const requestEvent of httpConn) {
105 | this.#handler!.execute(
106 | requestEvent!,
107 | this.middlewares as any,
108 | this.errorMiddlewares as any,
109 | );
110 | }
111 | })();
112 | }
113 | };
114 |
115 | constructor(db?: T, dbinit?: () => Promise) {
116 | super();
117 | if (db) {
118 | this.#database = db;
119 | }
120 |
121 | if (dbinit) {
122 | this.#databaseInitializer = dbinit;
123 | }
124 | }
125 |
126 | static startWith(
127 | this: T,
128 | func: () => Promise,
129 | ): App>;
130 | static startWith(
131 | this: T,
132 | dbClass: NoParamConstructor,
133 | ): App;
134 | static startWith(this: T, arg: U) {
135 | if ((arg as any).__type) {
136 | return new this(new (arg as any)());
137 | } else {
138 | return new this>(undefined, arg as any);
139 | }
140 | }
141 |
142 | public database(func: () => Promise): void;
143 | public database(
144 | dbClass: NoParamConstructor,
145 | ): void;
146 | public database(arg: any) {
147 | if (arg.__type) {
148 | this.#database = new (arg as any)();
149 | } else {
150 | this.#databaseInitializer = arg;
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/core/httpError.ts:
--------------------------------------------------------------------------------
1 | /*!
2 | * Adapted directly from http-errors at https://github.com/jshttp/http-errors
3 | * which is licensed as follows:
4 | *
5 | * The MIT License (MIT)
6 | *
7 | * Copyright (c) 2014 Jonathan Ong me@jongleberry.com
8 | * Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com
9 | *
10 | * Permission is hereby granted, free of charge, to any person obtaining a copy
11 | * of this software and associated documentation files (the "Software"), to deal
12 | * in the Software without restriction, including without limitation the rights
13 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | * copies of the Software, and to permit persons to whom the Software is
15 | * furnished to do so, subject to the following conditions:
16 | *
17 | * The above copyright notice and this permission notice shall be included in
18 | * all copies or substantial portions of the Software.
19 | *
20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 | * THE SOFTWARE.
27 | */
28 |
29 | import { Status, STATUS_TEXT } from "../deps.ts";
30 | import type { ErrorStatus } from "./types.ts";
31 |
32 | const errorStatusMap = {
33 | "BadRequest": 400,
34 | "Unauthorized": 401,
35 | "PaymentRequired": 402,
36 | "Forbidden": 403,
37 | "NotFound": 404,
38 | "MethodNotAllowed": 405,
39 | "NotAcceptable": 406,
40 | "ProxyAuthRequired": 407,
41 | "RequestTimeout": 408,
42 | "Conflict": 409,
43 | "Gone": 410,
44 | "LengthRequired": 411,
45 | "PreconditionFailed": 412,
46 | "RequestEntityTooLarge": 413,
47 | "RequestURITooLong": 414,
48 | "UnsupportedMediaType": 415,
49 | "RequestedRangeNotSatisfiable": 416,
50 | "ExpectationFailed": 417,
51 | "Teapot": 418,
52 | "MisdirectedRequest": 421,
53 | "UnprocessableEntity": 422,
54 | "Locked": 423,
55 | "FailedDependency": 424,
56 | "UpgradeRequired": 426,
57 | "PreconditionRequired": 428,
58 | "TooManyRequests": 429,
59 | "RequestHeaderFieldsTooLarge": 431,
60 | "UnavailableForLegalReasons": 451,
61 | "InternalServerError": 500,
62 | "NotImplemented": 501,
63 | "BadGateway": 502,
64 | "ServiceUnavailable": 503,
65 | "GatewayTimeout": 504,
66 | "HTTPVersionNotSupported": 505,
67 | "VariantAlsoNegotiates": 506,
68 | "InsufficientStorage": 507,
69 | "LoopDetected": 508,
70 | "NotExtended": 510,
71 | "NetworkAuthenticationRequired": 511,
72 | };
73 |
74 | /** A base class for individual classes of HTTP errors. */
75 | export class HttpError extends Error {
76 | /** Determines if details about the error should be automatically exposed
77 | * in a response. This is automatically set to `true` for 4XX errors, as
78 | * they represent errors in the request, while 5XX errors are set to `false`
79 | * as they are internal server errors and exposing details could leak
80 | * important server security information. */
81 | expose = false;
82 |
83 | /** The HTTP error status associated with this class of error. */
84 | status = Status.InternalServerError;
85 | }
86 |
87 | function createHttpErrorConstructor(
88 | status: ErrorStatus,
89 | ): E {
90 | const name = `${Status[status]}Error`;
91 | const Ctor = class extends HttpError {
92 | constructor(message?: string) {
93 | super();
94 | this.message = message || STATUS_TEXT.get(status as any)!;
95 | this.status = status;
96 | this.expose = status >= 400 && status < 500 ? true : false;
97 | Object.defineProperty(this, "name", {
98 | configurable: true,
99 | enumerable: false,
100 | value: name,
101 | writable: true,
102 | });
103 | }
104 | };
105 | return Ctor as E;
106 | }
107 |
108 | /** An object which contains an individual HTTP Error for each HTTP status
109 | * error code (4XX and 5XX). When errors are raised related to a particular
110 | * HTTP status code, they will be of the appropriate instance located on this
111 | * object. Also, context's `.throw()` will throw errors based on the passed
112 | * status code. */
113 | export const httpErrors: Record =
114 | {} as any;
115 |
116 | for (const [key, value] of Object.entries(errorStatusMap)) {
117 | httpErrors[key as keyof typeof errorStatusMap] = createHttpErrorConstructor(
118 | value,
119 | );
120 | }
121 |
122 | /** Create a specific class of `HttpError` based on the status, which defaults
123 | * to _500 Internal Server Error_.
124 | */
125 | export function createHttpError(
126 | status: ErrorStatus = 500,
127 | message?: string,
128 | ): HttpError {
129 | return new httpErrors[Status[status] as keyof typeof errorStatusMap](message);
130 | }
131 |
132 | export function isHttpError(value: any): value is HttpError {
133 | return value instanceof HttpError;
134 | }
--------------------------------------------------------------------------------
/core/process.ts:
--------------------------------------------------------------------------------
1 | import { AttainRequest } from "./request.ts";
2 | import { AttainResponse } from "./response.ts";
3 | import { checkPathAndParseURLParams } from "./utils.ts";
4 | import type {
5 | MiddlewareProps,
6 | ErrorMiddlewareProps,
7 | ParamStackProps,
8 | } from "./types.ts";
9 |
10 | /* an alternative, clearer code of the process file */
11 | export class AttainHandler {
12 | #database: DB
13 | #secure: boolean
14 |
15 | constructor(database: DB, secure: boolean) {
16 | this.#database = database;
17 | this.#secure = secure;
18 | }
19 |
20 | // main function
21 | async execute(
22 | srq: Deno.RequestEvent,
23 | middlewares: MiddlewareProps[],
24 | errorMiddlewares: ErrorMiddlewareProps[]
25 | ) {
26 | const res = new AttainResponse(srq.request);
27 | const req = new AttainRequest(srq.request, this.#secure);
28 |
29 | // execute middlewares. if any errors, execute error middlewares.
30 | try {
31 | await this.handleMiddlewares(req, res, middlewares);
32 | } catch (error) {
33 | await this.handleErrorMiddlewares(error, req, res, errorMiddlewares);
34 | }
35 |
36 | // execute pending jobs (aka - res.pend());
37 | await res.executePendingJobs(req);
38 |
39 | // if the response was created, send it. if not, close connection.
40 | if (res.getBody) {
41 | await srq.respondWith(res.getResponse);
42 | }
43 |
44 | // delete response from memory.
45 | res.destroy();
46 | }
47 |
48 | // handle regular middlewares
49 | private async handleMiddlewares(
50 | req: AttainRequest,
51 | res: AttainResponse,
52 | current: MiddlewareProps[]
53 | ) {
54 | try {
55 | const currentMethod = req.method;
56 | const currentUrl = req.url.pathname;
57 | for (const middleware of current) {
58 | // check if the middleware method matches the method in the request
59 | if (
60 | middleware.method === currentMethod || middleware.method === "ALL"
61 | ) {
62 | // if the middleware doesn't have a url, execute it. if it does, check match and execute.
63 | if (!middleware.url) {
64 | await this.#run(req, res, middleware);
65 | } else if (
66 | checkPathAndParseURLParams(req, middleware.url, currentUrl)
67 | ) {
68 | // if any params handlers in the request (aka, Router.param()), handle them.
69 | if (middleware.paramHandlers && req.params) {
70 | await this.#runWithParams(middleware.paramHandlers, req, res);
71 | // if the middleare has a res.send() method, finish the request.
72 | if (res.processDone) {
73 | break;
74 | }
75 | }
76 | // fallback middleware execution
77 | await this.#run(req, res, middleware);
78 | }
79 | }
80 | // if the middleare has a res.send() method, finish the request.
81 | if (res.processDone) {
82 | break;
83 | }
84 | }
85 | } catch (error) { // throw error to the error middlewares
86 | if (error instanceof Error) {
87 | throw error;
88 | } else {
89 | throw new Error(error);
90 | }
91 | }
92 | }
93 |
94 | // handle error middlewares
95 | private async handleErrorMiddlewares(
96 | error: Error | any,
97 | req: AttainRequest,
98 | res: AttainResponse,
99 | current: ErrorMiddlewareProps[]
100 | ) {
101 | try {
102 | const currentUrl = req.url.pathname;
103 | for (const middleware of current) {
104 | // execute all middlewares without a url or middlewares that match the current url.
105 | if (!middleware.url) {
106 | await this.#runWithError(error, req, res, middleware);
107 | } else if (
108 | checkPathAndParseURLParams(req, middleware.url, currentUrl)
109 | ) {
110 | await this.#runWithError(error, req, res, middleware);
111 | }
112 | if (res.processDone) {
113 | break;
114 | }
115 | }
116 | } catch (error) {
117 | console.error(
118 | "Attain Error: Can't handle it due to Error middlewares can't afford it.",
119 | );
120 | console.error(error);
121 | }
122 | };
123 |
124 | // execute a middleware callback if exists
125 | #run = async (
126 | req: AttainRequest,
127 | res: AttainResponse,
128 | middleware: MiddlewareProps
129 | ) => {
130 | try {
131 | middleware.callBack
132 | ? await middleware.callBack(req, res, this.#database!)
133 | : await this.handleMiddlewares(req, res, middleware.next!); /* No idea why I need this '!' */
134 | } catch (err) {
135 | throw err;
136 | }
137 | }
138 |
139 | // execute an error middleware callback if exists.
140 | #runWithError = async (
141 | err: any,
142 | req: AttainRequest,
143 | res: AttainResponse,
144 | middleware: ErrorMiddlewareProps
145 | ) => {
146 | try {
147 | middleware.callBack
148 | ? await middleware.callBack(err, req, res, this.#database!)
149 | : await this.handleErrorMiddlewares(err, req, res, middleware.next!);
150 | } catch (err) {
151 | throw err;
152 | }
153 | }
154 |
155 | // execute a paramHandler callback
156 | #runWithParams = async (
157 | paramHandlers: ParamStackProps[],
158 | req: AttainRequest,
159 | res: AttainResponse,
160 | ) => {
161 | for await (const paramHandler of paramHandlers) {
162 | await paramHandler.callBack(req, res, req.params[paramHandler.paramName], this.#database!)
163 | if (res.processDone) {
164 | break;
165 | }
166 | }
167 | };
168 | }
--------------------------------------------------------------------------------
/performance/performance.md:
--------------------------------------------------------------------------------
1 | # Performance
2 |
3 | Tested in Windows 10 system with
4 | deno 1.0.3
5 | v8 8.4.300
6 | typescript 3.9.2
7 |
8 | Powered by Apache JMeter (v5.3)
9 | Each test performed with 10 threads during the 10 seconds
10 |
11 | ## Contents
12 | - [Attain](#attain)
13 | - [Minimal test](#minimal-test)
14 | - [With logger and router](#with-logger-and-router)
15 | - [Multi middlewares](#multi-middlewares)
16 | - [Send File](#send-file)
17 | - [Express](#express)
18 | - [Minimal test](#minimal-test-1)
19 | - [With logger and router](#with-logger-and-router-1)
20 | - [Multi middlewares](#multi-middlewares-1)
21 | - [Send File](#send-file-1)
22 | - [Oak](#oak)
23 | - [Minimal test](#minimal-test-2)
24 | - [With logger and router](#with-logger-and-router-2)
25 | - [Multi middlewares](#multi-middlewares-2)
26 | - [Send File](#send-file-2)
27 |
28 | ## Attain
29 |
30 | ### minimal test
31 |
32 | ```ts
33 | import { App } from "https://deno.land/x/attain/mod.ts";
34 |
35 | const app = new App();
36 |
37 | app.use((req, res) => {
38 | res.status(200).send("Hello world!");
39 | });
40 |
41 | app.listen({ port: 3500 });
42 | ```
43 | 
44 |
45 | ### with logger and router
46 |
47 | ```ts
48 | import { App, logger } from "https://deno.land/x/attain/mod.ts";
49 |
50 | const app = new App();
51 |
52 | app.use(logger);
53 |
54 | app.use("/", (req, res) => {
55 | res.status(200).send("Hello world!");
56 | });
57 |
58 | app.listen({ port: 3500 });
59 | ```
60 |
61 | 
62 |
63 | ### multi middlewares
64 | five simple middlewares with logger and router
65 |
66 | ```ts
67 | import { App, Request, Response } from "https://deno.land/x/attain/mod.ts";
68 |
69 | const app = new App();
70 |
71 | const hello = (req: Request, res: Response) => {
72 | console.log("hello")
73 | };
74 |
75 | app.use(logger);
76 | app.use(hello, hello, hello, hello, hello);
77 | app.use("/", (req, res) => {
78 | res.status(200).send("Hello world!");
79 | });
80 |
81 | app.listen({ port: 3500 });
82 | ```
83 |
84 | 
85 |
86 | ### Send file
87 |
88 | 
89 |
90 |
91 | ## Express
92 |
93 | ### minimal test
94 |
95 | ```ts
96 | const express = require('express')
97 | const app = express()
98 | const port = 5000
99 |
100 | app.get((req, res) => {
101 | res.send('Hello World!')
102 | })
103 |
104 | app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
105 | ```
106 |
107 | 
108 |
109 | ### with logger and router
110 |
111 | ```ts
112 | const express = require('express')
113 | const app = express()
114 | const morgan = require('morgan')
115 | const port = 5000
116 |
117 | app.use(morgan('tiny'));
118 |
119 | app.get('/', (req, res) => {
120 | res.send('Hello World!')
121 | })
122 |
123 | app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
124 | ```
125 |
126 | 
127 |
128 |
129 | ### multi middlewares
130 | five simple middlewares with logger and router
131 |
132 | ```ts
133 | const express = require('express')
134 | const app = express()
135 | const morgan = require('morgan')
136 | const port = 5000
137 |
138 | const hello = (req, res, next) => {
139 | console.log("hello")
140 | next();
141 | }
142 | app.use(morgan('tiny'));
143 | app.use(hello, hello, hello, hello, hello)
144 | app.get('/', (req, res) => {
145 | res.send('Hello World!')
146 | })
147 |
148 | app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
149 | ```
150 |
151 | 
152 |
153 | ### Send file
154 |
155 | 
156 |
157 |
158 | ## Oak
159 |
160 |
161 | ### minimal test
162 |
163 | ```ts
164 | import { Application } from "https://deno.land/x/oak/mod.ts";
165 |
166 | const app = new Application();
167 |
168 | app.use((ctx) => {
169 | ctx.response.body = "Hello World!";
170 | });
171 |
172 | await app.listen({ port: 8000 });
173 | ```
174 |
175 | 
176 |
177 | ### with logger and router
178 |
179 | ```ts
180 | import { Application, Router } from "https://deno.land/x/oak/mod.ts";
181 |
182 | const app = new Application();
183 | const router = new Router();
184 | router
185 | .get("/", (context) => {
186 | context.response.body = "Hello world!";
187 | })
188 |
189 | app.use(async (ctx, next) => {
190 | await next();
191 | const rt = ctx.response.headers.get("X-Response-Time");
192 | console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`);
193 | });
194 | app.use(router.routes());
195 | app.use(router.allowedMethods());
196 |
197 |
198 | await app.listen({ port: 8000 });
199 | ```
200 |
201 | 
202 |
203 | ### multi middlewares
204 | five simple middlewares with logger and router
205 |
206 | ```ts
207 | import { Application, Router } from "https://deno.land/x/oak/mod.ts";
208 |
209 | const app = new Application();
210 | const router = new Router();
211 |
212 | const hello = (ctx: any, next: any) => {
213 | console.log("hello1");
214 | next()
215 | }
216 |
217 | router
218 | .get("/", (context) => {
219 | context.response.body = "Hello world!";
220 | })
221 |
222 | app.use(hello, hello, hello, hello, hello)
223 | app.use(async (ctx, next) => {
224 | await next();
225 | const rt = ctx.response.headers.get("X-Response-Time");
226 | console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`);
227 | });
228 | app.use(router.routes());
229 | app.use(router.allowedMethods());
230 |
231 |
232 | await app.listen({ port: 8000 });
233 | ```
234 |
235 | 
236 |
237 | ### Send file
238 |
239 | 
240 |
--------------------------------------------------------------------------------
/core/request.ts:
--------------------------------------------------------------------------------
1 | /*!
2 | * Adapted from oak/request at https://github.com/oakserver/oak/blob/master/request.ts
3 | * which is licensed as:
4 | *
5 | * The MIT License (MIT)
6 | *
7 | * Copyright (c) 2018-2020 the oak authors
8 | *
9 | * Permission is hereby granted, free of charge, to any person obtaining a copy
10 | * of this software and associated documentation files (the "Software"), to deal
11 | * in the Software without restriction, including without limitation the rights
12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | * copies of the Software, and to permit persons to whom the Software is
14 | * furnished to do so, subject to the following conditions:
15 | *
16 | * The above copyright notice and this permission notice shall be included in
17 | * all copies or substantial portions of the Software.
18 | *
19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | * THE SOFTWARE.
26 | */
27 |
28 | import type {
29 | Body,
30 | BodyBytes,
31 | BodyForm,
32 | BodyFormData,
33 | BodyJson,
34 | BodyOptions,
35 | BodyReader,
36 | BodyStream,
37 | BodyText,
38 | } from "https://deno.land/x/oak@v9.0.1/body.ts";
39 | import { RequestBody } from "https://deno.land/x/oak@v9.0.1/body.ts";
40 | import type { SupportMethodType } from "./types.ts";
41 | import { preferredCharsets } from "https://deno.land/x/oak@v9.0.1/negotiation/charset.ts";
42 | import { preferredEncodings } from "https://deno.land/x/oak@v9.0.1/negotiation/encoding.ts";
43 | import { preferredLanguages } from "https://deno.land/x/oak@v9.0.1/negotiation/language.ts";
44 | import { preferredMediaTypes } from "https://deno.land/x/oak@v9.0.1/negotiation/mediaType.ts";
45 |
46 | const decoder = new TextDecoder();
47 |
48 | /** An interface which provides information about the current request. */
49 | export class AttainRequest {
50 | #body: RequestBody;
51 | #serverRequest: Request;
52 | #url?: URL;
53 | #secure: boolean;
54 | #startDate: number;
55 |
56 | [dynamicProperty: string]: any
57 |
58 | /** Is `true` if the request has a body, otherwise `false`. */
59 | get hasBody(): boolean {
60 | return this.#body.has();
61 | }
62 |
63 | /** The `Headers` supplied in the request. */
64 | get headers(): Headers {
65 | return this.#serverRequest.headers;
66 | }
67 |
68 | /** The HTTP Method used by the request. */
69 | get method(): SupportMethodType {
70 | return this.#serverRequest.method as SupportMethodType;
71 | }
72 |
73 | /** Set to the value of the _original_ Deno server request. */
74 | get serverRequest(): Request {
75 | return this.#serverRequest;
76 | }
77 |
78 | get startDate(): number {
79 | return this.#startDate;
80 | }
81 |
82 | /** A parsed URL for the request which complies with the browser standards.
83 | * When the application's `.proxy` is `true`, this value will be based off of
84 | * the `X-Forwarded-Proto` and `X-Forwarded-Host` header values if present in
85 | * the request. */
86 | get url(): URL {
87 | return new URL(this.#serverRequest.url);
88 | }
89 |
90 | constructor(serverRequest: Request, secure = false) {
91 | this.#serverRequest = serverRequest;
92 | this.#secure = secure;
93 | this.#body = new RequestBody(serverRequest);
94 | this.#startDate = Date.now();
95 | }
96 |
97 | /** Returns an array of media types, accepted by the requestor, in order of
98 | * preference. If there are no encodings supplied by the requestor,
99 | * `undefined` is returned.
100 | */
101 | accepts(): string[] | undefined;
102 | /** For a given set of media types, return the best match accepted by the
103 | * requestor. If there are no encoding that match, then the method returns
104 | * `undefined`.
105 | */
106 | accepts(...types: string[]): string | undefined;
107 | accepts(...types: string[]): string | string[] | undefined {
108 | const acceptValue = this.#serverRequest.headers.get("Accept");
109 | if (!acceptValue) {
110 | return;
111 | }
112 | if (types.length) {
113 | return preferredMediaTypes(acceptValue, types)[0];
114 | }
115 | return preferredMediaTypes(acceptValue);
116 | }
117 |
118 | /** Returns an array of charsets, accepted by the requestor, in order of
119 | * preference. If there are no charsets supplied by the requestor,
120 | * `undefined` is returned.
121 | */
122 | acceptsCharsets(): string[] | undefined;
123 | /** For a given set of charsets, return the best match accepted by the
124 | * requestor. If there are no charsets that match, then the method returns
125 | * `undefined`. */
126 | acceptsCharsets(...charsets: string[]): string | undefined;
127 | acceptsCharsets(...charsets: string[]): string[] | string | undefined {
128 | const acceptCharsetValue = this.#serverRequest.headers.get(
129 | "Accept-Charset",
130 | );
131 | if (!acceptCharsetValue) {
132 | return;
133 | }
134 | if (charsets.length) {
135 | return preferredCharsets(acceptCharsetValue, charsets)[0];
136 | }
137 | return preferredCharsets(acceptCharsetValue);
138 | }
139 |
140 | /** Returns an array of encodings, accepted by the requestor, in order of
141 | * preference. If there are no encodings supplied by the requestor,
142 | * `undefined` is returned.
143 | */
144 | acceptsEncodings(): string[] | undefined;
145 | /** For a given set of encodings, return the best match accepted by the
146 | * requestor. If there are no encodings that match, then the method returns
147 | * `undefined`.
148 | *
149 | * **NOTE:** You should always supply `identity` as one of the encodings
150 | * to ensure that there is a match when the `Accept-Encoding` header is part
151 | * of the request.
152 | */
153 | acceptsEncodings(...encodings: string[]): string | undefined;
154 | acceptsEncodings(...encodings: string[]): string[] | string | undefined {
155 | const acceptEncodingValue = this.#serverRequest.headers.get(
156 | "Accept-Encoding",
157 | );
158 | if (!acceptEncodingValue) {
159 | return;
160 | }
161 | if (encodings.length) {
162 | return preferredEncodings(acceptEncodingValue, encodings)[0];
163 | }
164 | return preferredEncodings(acceptEncodingValue);
165 | }
166 |
167 | /** Returns an array of languages, accepted by the requestor, in order of
168 | * preference. If there are no languages supplied by the requestor,
169 | * `undefined` is returned.
170 | */
171 | acceptsLanguages(): string[] | undefined;
172 | /** For a given set of languages, return the best match accepted by the
173 | * requestor. If there are no languages that match, then the method returns
174 | * `undefined`. */
175 | acceptsLanguages(...langs: string[]): string | undefined;
176 | acceptsLanguages(...langs: string[]): string[] | string | undefined {
177 | const acceptLanguageValue = this.#serverRequest.headers.get(
178 | "Accept-Language",
179 | );
180 | if (!acceptLanguageValue) {
181 | return;
182 | }
183 | if (langs.length) {
184 | return preferredLanguages(acceptLanguageValue, langs)[0];
185 | }
186 | return preferredLanguages(acceptLanguageValue);
187 | }
188 |
189 | body(options: BodyOptions<"bytes">): BodyBytes;
190 | body(options: BodyOptions<"form">): BodyForm;
191 | body(options: BodyOptions<"form-data">): BodyFormData;
192 | body(options: BodyOptions<"json">): BodyJson;
193 | body(options: BodyOptions<"reader">): BodyReader;
194 | body(options: BodyOptions<"stream">): BodyStream;
195 | body(options: BodyOptions<"text">): BodyText;
196 | body(options?: BodyOptions): Body;
197 | body(options: BodyOptions = {}): Body | BodyReader | BodyStream {
198 | return this.#body.get(options);
199 | }
200 | }
--------------------------------------------------------------------------------
/test/basicTest/main_test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertEquals,
3 | bench,
4 | runBenchmarks,
5 | } from "../test_deps.ts";
6 | import { App, staticServe } from "../../mod.ts";
7 | import router from "./router.ts";
8 | import paramTest from "./param.ts";
9 |
10 | /**
11 | * Main part
12 | */
13 | const app = new App();
14 |
15 | app.use(staticServe("test/static"));
16 | app.use("/router", router);
17 | app.use("/param", paramTest);
18 |
19 | app.get("/", (req, res) => {
20 | res.send("/");
21 | });
22 |
23 | app.get("/sendFile", async (req, res) => {
24 | await res.sendFile("test/static/test.html");
25 | });
26 |
27 | app.get("/error", (req, res) => {
28 | throw new Error("test error");
29 | });
30 |
31 | app.error("/error", (error, req, res) => {
32 | res.send(error.message);
33 | });
34 |
35 | app.listen(8080);
36 |
37 | /**
38 | * basic route test
39 | */
40 | bench({
41 | name: "GET: / (50 times)",
42 | runs: 5,
43 | async func(b: any): Promise {
44 | b.start();
45 | const conns = [];
46 | for (let i = 0; i < 10; ++i) {
47 | conns.push(fetch("http://localhost:8080/").then((resp) => resp.text()));
48 | }
49 | for await (const i of conns) {
50 | assertEquals(await i, "/");
51 | }
52 | b.stop();
53 | },
54 | });
55 |
56 | /**
57 | * error test
58 | */
59 | bench({
60 | name: "GET: /error",
61 | runs: 1,
62 | async func(b: any): Promise {
63 | b.start();
64 | const errorTest = await fetch("http://localhost:8080/error");
65 | const data = await errorTest.text();
66 | assertEquals(data, "test error");
67 | b.stop();
68 | },
69 | });
70 |
71 | /**
72 | * static serve
73 | */
74 | bench({
75 | name: "GET: /test.html",
76 | runs: 1,
77 | async func(b: any): Promise {
78 | b.start();
79 | const staticTest = await fetch("http://localhost:8080/test.html");
80 | const staticData = await staticTest.text();
81 | assertEquals(
82 | staticData,
83 | `Test this page is a simple html
`,
84 | );
85 | b.stop();
86 | },
87 | });
88 | bench({
89 | name: "GET: /sendFile",
90 | runs: 1,
91 | async func(b: any): Promise {
92 | b.start();
93 | const sendFileTest = await fetch("http://localhost:8080/sendFile");
94 | const sendFileData = await sendFileTest.text();
95 | assertEquals(
96 | sendFileData,
97 | `Test this page is a simple html
`,
98 | );
99 | b.stop();
100 | },
101 | });
102 |
103 | /**
104 | * nested
105 | */
106 | bench({
107 | name: "GET: /router and /router/second",
108 | runs: 1,
109 | async func(b: any): Promise {
110 | b.start();
111 | const routerPath = await fetch("http://localhost:8080/router");
112 | const routerData = await routerPath.text();
113 | assertEquals(routerData, "/router");
114 |
115 | const secondPath = await fetch("http://localhost:8080/router/second");
116 | const secondData = await secondPath.text();
117 | assertEquals(secondData, "/router/second");
118 | b.stop();
119 | },
120 | });
121 |
122 | bench({
123 | name: "GET: /router/second/:id (5 times)",
124 | runs: 1,
125 | async func(b: any): Promise {
126 | b.start();
127 | const conns = [];
128 | for (let i = 0; i < 5; ++i) {
129 | conns.push(
130 | fetch(`http://localhost:8080/router/second/${i}`).then((resp) =>
131 | resp.text()
132 | ),
133 | );
134 | }
135 | await Promise.all(conns);
136 | for (const key in conns) {
137 | const data = await conns[key];
138 | assertEquals(data, `/router/second/${key}`);
139 | }
140 | b.stop();
141 | },
142 | });
143 |
144 | bench({
145 | name: "GET: /router/seach?name=aaron",
146 | runs: 1,
147 | async func(b: any): Promise {
148 | b.start();
149 | const test = await fetch("http://localhost:8080/router/search?name=aaron");
150 | const data = await test.json();
151 | assertEquals(data, { name: "aaron" });
152 | b.stop();
153 | },
154 | });
155 |
156 | bench({
157 | name: "POST: /router/post",
158 | runs: 1,
159 | async func(b: any): Promise {
160 | b.start();
161 | const test = await fetch("http://localhost:8080/router/post", {
162 | method: "POST",
163 | headers: {
164 | "Content-Type": "application/json",
165 | },
166 | body: JSON.stringify({ name: "aaron" }),
167 | });
168 | const data = await test.json();
169 | assertEquals(data, { name: "aaron" });
170 | b.stop();
171 | },
172 | });
173 |
174 | /**
175 | * param bench mark
176 | */
177 | bench({
178 | name: "GET: /param/:username",
179 | runs: 1,
180 | async func(b: any): Promise {
181 | b.start();
182 | const test = await fetch("http://localhost:8080/param/aaron");
183 | const data = await test.text();
184 | assertEquals(data, "aaron");
185 | b.stop();
186 | },
187 | });
188 |
189 | bench({
190 | name: "POST: /param/:username/post",
191 | runs: 1,
192 | async func(b: any): Promise {
193 | b.start();
194 | const test = await fetch(
195 | "http://localhost:8080/param/aaron/post",
196 | { method: "POST" },
197 | );
198 | const data = await test.text();
199 | assertEquals(data, "aaron");
200 | b.stop();
201 | },
202 | });
203 |
204 | bench({
205 | name: "PATCH: /param/:username/patch",
206 | runs: 1,
207 | async func(b: any): Promise {
208 | b.start();
209 | const test = await fetch(
210 | "http://localhost:8080/param/aaron/patch",
211 | { method: "PATCH" },
212 | );
213 | const data = await test.text();
214 | assertEquals(data, "aaron");
215 | b.stop();
216 | },
217 | });
218 |
219 | bench({
220 | name: "PUT: /param/:username/put",
221 | runs: 1,
222 | async func(b: any): Promise {
223 | b.start();
224 | const test = await fetch(
225 | "http://localhost:8080/param/aaron/put",
226 | { method: "PUT" },
227 | );
228 | const data = await test.text();
229 | assertEquals(data, "aaron");
230 | b.stop();
231 | },
232 | });
233 |
234 | bench({
235 | name: "DELETE: /param/:username/delete",
236 | runs: 1,
237 | async func(b: any): Promise {
238 | b.start();
239 | const test = await fetch(
240 | "http://localhost:8080/param/aaron/delete",
241 | { method: "DELETE" },
242 | );
243 | const data = await test.text();
244 | assertEquals(data, "aaron");
245 | b.stop();
246 | },
247 | });
248 |
249 | bench({
250 | name: "OPTIONS: /param/:username/options",
251 | runs: 1,
252 | async func(b: any): Promise {
253 | b.start();
254 | const test = await fetch(
255 | "http://localhost:8080/param/aaron/options",
256 | { method: "OPTIONS" },
257 | );
258 | const data = await test.text();
259 | assertEquals(data, "aaron");
260 | b.stop();
261 | },
262 | });
263 |
264 | bench({
265 | name: "POST: /param/:username/password",
266 | runs: 1,
267 | async func(b: any): Promise {
268 | b.start()
269 | const test = await fetch("http://localhost:8080/param/aaron/password", {
270 | method: "POST",
271 | headers: {
272 | "Content-Type": "application/json",
273 | },
274 | body: JSON.stringify({ password: "123" }),
275 | });
276 | const data = await test.json();
277 | assertEquals(data, { name: "aaron", password: "123" });
278 | b.stop()
279 | },
280 | });
281 |
282 | bench({
283 | name: "GET: /param/:username/:password",
284 | runs: 1,
285 | async func(b: any): Promise {
286 | b.start()
287 | const test = await fetch("http://localhost:8080/param/aaron/123")
288 | const data = await test.json();
289 | assertEquals(data, { name: "aaron", password: "123" });
290 | b.stop()
291 | },
292 | });
293 |
294 | bench({
295 | name: "GET: /param/:username/:password error",
296 | runs: 1,
297 | async func(b: any): Promise {
298 | b.start()
299 | const test = await fetch("http://localhost:8080/param/aaron/555")
300 | const data = await test.text();
301 | assertEquals(test.status, 404);
302 | assertEquals(data, "password is not matched");
303 | b.stop()
304 | },
305 | });
306 |
307 | await runBenchmarks();
308 |
309 | await app.close();
310 |
--------------------------------------------------------------------------------
/core/utils.ts:
--------------------------------------------------------------------------------
1 | import type { AttainRequest } from "./request.ts";
2 |
3 | import { Sha1, lookup, match, extname, parse } from "../deps.ts";
4 | import { AttainResponse } from "./response.ts";
5 |
6 | const CR = "\r".charCodeAt(0);
7 | const LF = "\n".charCodeAt(0);
8 | const HTAB = "\t".charCodeAt(0);
9 | const SPACE = " ".charCodeAt(0);
10 |
11 | function isCloser(value: unknown): value is Deno.Closer {
12 | return typeof value === "object" && value != null && "close" in value &&
13 | // deno-lint-ignore no-explicit-any
14 | typeof (value as Record)["close"] === "function";
15 | }
16 |
17 | export const DEFAULT_CHUNK_SIZE = 16_640; // 17 Kib
18 |
19 | export interface ReadableStreamFromReaderOptions {
20 | /** If the `reader` is also a `Deno.Closer`, automatically close the `reader`
21 | * when `EOF` is encountered, or a read error occurs.
22 | *
23 | * Defaults to `true`. */
24 | autoClose?: boolean;
25 |
26 | /** The size of chunks to allocate to read, the default is ~16KiB, which is
27 | * the maximum size that Deno operations can currently support. */
28 | chunkSize?: number;
29 |
30 | /** The queuing strategy to create the `ReadableStream` with. */
31 | strategy?: { highWaterMark?: number | undefined; size?: undefined };
32 | }
33 |
34 | export function readableStreamFromReader(
35 | reader: Deno.Reader | (Deno.Reader & Deno.Closer),
36 | options: ReadableStreamFromReaderOptions = {},
37 | ): ReadableStream {
38 | const {
39 | autoClose = true,
40 | chunkSize = DEFAULT_CHUNK_SIZE,
41 | strategy,
42 | } = options;
43 |
44 | return new ReadableStream({
45 | async pull(controller) {
46 | const chunk = new Uint8Array(chunkSize);
47 | try {
48 | const read = await reader.read(chunk);
49 | if (read === null) {
50 | if (isCloser(reader) && autoClose) {
51 | reader.close();
52 | }
53 | controller.close();
54 | return;
55 | }
56 | controller.enqueue(chunk.subarray(0, read));
57 | } catch (e) {
58 | controller.error(e);
59 | if (isCloser(reader)) {
60 | reader.close();
61 | }
62 | }
63 | },
64 | cancel() {
65 | if (isCloser(reader) && autoClose) {
66 | reader.close();
67 | }
68 | },
69 | }, strategy);
70 | }
71 |
72 | /** Returns the content-type based on the extension of a path. */
73 | function contentType(path: string): string | undefined {
74 | const result = lookup(extname(path));
75 | return result ? result : undefined;
76 | }
77 |
78 | export function stripEol(value: Uint8Array): Uint8Array {
79 | if (value[value.byteLength - 1] == LF) {
80 | let drop = 1;
81 | if (value.byteLength > 1 && value[value.byteLength - 2] === CR) {
82 | drop = 2;
83 | }
84 | return value.subarray(0, value.byteLength - drop);
85 | }
86 | return value;
87 | }
88 |
89 | export const checkPathAndParseURLParams = (
90 | req: AttainRequest,
91 | middlewareURL: string,
92 | currentURL: string,
93 | ) => {
94 | const matcher = match(middlewareURL, { decode: decodeURIComponent });
95 | const isMatch: any = matcher(currentURL);
96 | if (isMatch.params) {
97 | const { 0: extra, ...result } = isMatch.params;
98 | req.params = { ...req.params, ...result };
99 | }
100 | return isMatch;
101 | };
102 |
103 | export function getRandomFilename(prefix = "", extension = ""): string {
104 | return `${prefix}${
105 | new Sha1().update(crypto.getRandomValues(new Uint8Array(256))).hex()
106 | }${extension ? `.${extension}` : ""}`;
107 | }
108 |
109 | export const etag = (entity: Uint8Array, len: number) => {
110 | if (!entity) {
111 | // fast-path empty
112 | return `W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"`;
113 | }
114 |
115 | // compute hash of entity
116 | const sha1 = new Sha1();
117 | sha1.update(entity);
118 | sha1.digest();
119 | const hash = sha1.toString().substring(0, 27);
120 |
121 | return `W/"${len.toString(16)}-${hash}"`;
122 | };
123 |
124 | export function skipLWSPChar(u8: Uint8Array): Uint8Array {
125 | const result = new Uint8Array(u8.length);
126 | let j = 0;
127 | for (let i = 0; i < u8.length; i++) {
128 | if (u8[i] === SPACE || u8[i] === HTAB) continue;
129 | result[j++] = u8[i];
130 | }
131 | return result.slice(0, j);
132 | }
133 |
134 | export const fresh = (req: AttainRequest, res: AttainResponse) => {
135 | const modifiedSince = req.headers.get("if-modified-since");
136 | const noneMatch = req.headers.get("if-none-match");
137 |
138 | if (!modifiedSince && !noneMatch) {
139 | return false;
140 | }
141 |
142 | const CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/;
143 | const cacheControl = req.headers.get("cache-control");
144 | if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
145 | return false;
146 | }
147 |
148 | if (noneMatch && noneMatch !== "*") {
149 | const etag = res.getHeader("etag");
150 |
151 | if (!etag) {
152 | return false;
153 | }
154 |
155 | let etagStale = true;
156 | const matches = parseTokenList(noneMatch);
157 | for (let i = 0; i < matches.length; i++) {
158 | const match = matches[i];
159 | if (match === etag || match === "W/" + etag || "W/" + match === etag) {
160 | etagStale = false;
161 | break;
162 | }
163 | }
164 |
165 | if (etagStale) {
166 | return false;
167 | }
168 | }
169 |
170 | // if-modified-since
171 | if (modifiedSince) {
172 | const lastModified = res.getHeader("last-modified");
173 | const modifiedStale = !lastModified ||
174 | !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince));
175 |
176 | if (modifiedStale) {
177 | return false;
178 | }
179 | }
180 |
181 | return true;
182 | };
183 |
184 | const parseHttpDate = (date: any) => {
185 | const timestamp = date && Date.parse(date);
186 |
187 | // istanbul ignore next: guard against date.js Date.parse patching
188 | return typeof timestamp === "number" ? timestamp : NaN;
189 | };
190 |
191 | const parseTokenList = (str: string) => {
192 | let end = 0;
193 | const list = [];
194 | let start = 0;
195 |
196 | // gather tokens
197 | for (let i = 0, len = str.length; i < len; i++) {
198 | switch (str.charCodeAt(i)) {
199 | case 0x20:/* */
200 | if (start === end) {
201 | start = end = i + 1;
202 | }
203 | break;
204 | case 0x2c:/* , */
205 | list.push(str.substring(start, end));
206 | start = end = i + 1;
207 | break;
208 | default:
209 | end = i + 1;
210 | break;
211 | }
212 | }
213 |
214 | // final token
215 | list.push(str.substring(start, end));
216 |
217 | return list;
218 | };
219 |
220 | export const acceptParams = (str: string, index: number = 0) => {
221 | var parts = str.split(/ *; */);
222 | var ret: any = {
223 | value: parts[0],
224 | quality: 1,
225 | params: {},
226 | originalIndex: index,
227 | };
228 |
229 | for (var i = 1; i < parts.length; ++i) {
230 | var pms = parts[i].split(/ *= */);
231 | if ("q" === pms[0]) {
232 | ret.quality = parseFloat(pms[1]);
233 | } else {
234 | ret.params[pms[0]] = pms[1];
235 | }
236 | }
237 |
238 | return ret;
239 | };
240 |
241 | export const normalizeType = (type: string) => {
242 | return ~type.indexOf("/")
243 | ? acceptParams(type)
244 | : { value: lookup(type), params: {} };
245 | };
246 |
247 | export const fileStream = async (
248 | res: AttainResponse,
249 | filePath: string,
250 | ): Promise => {
251 | const [file, fileInfo] = await Promise.all(
252 | [Deno.open(filePath), Deno.stat(filePath)],
253 | );
254 |
255 | res.setHeader("content-length", fileInfo.size.toString());
256 | const contentTypeValue = contentType(filePath);
257 | if (contentTypeValue) {
258 | res.setHeader("content-type", contentTypeValue);
259 | }
260 | fileInfo.mtime &&
261 | res.setHeader("Last-Modified", fileInfo.mtime.toUTCString());
262 |
263 | return file;
264 | };
265 |
266 | export const last = (array: any[]) => {
267 | var length = array == null ? 0 : array.length;
268 | return length ? array[length - 1] : undefined;
269 | };
270 |
271 | export const getEnvFlags = () => {
272 | const options = parse(Deno.args);
273 | const container: any = {}
274 |
275 | if (options["mode"]) {
276 | container.mode = options["mode"]
277 | }
278 |
279 | if (options["env"]) {
280 | container.env = options["env"]
281 | }
282 |
283 | return container;
284 | }
285 |
--------------------------------------------------------------------------------
/core/router.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | MiddlewareProps,
3 | CallBackType,
4 | SupportMethodType,
5 | ErrorCallBackType,
6 | ErrorMiddlewareProps,
7 | ParamCallBackType,
8 | ParamStackProps,
9 | } from "./types.ts";
10 | import { App } from "./application.ts";
11 | import { isEmpty } from "../deps.ts";
12 |
13 | export class Router {
14 | #middlewares: MiddlewareProps[] = [];
15 | #errorMiddlewares: ErrorMiddlewareProps[] = [];
16 | #paramHandlerStacks: ParamStackProps[] = [];
17 |
18 | get middlewares() {
19 | return this.#middlewares;
20 | }
21 |
22 | get errorMiddlewares() {
23 | return this.#errorMiddlewares;
24 | }
25 |
26 | private isString(arg: any): boolean {
27 | return typeof arg === "string";
28 | }
29 |
30 | private appendParentsPaths(currentPath: string): string {
31 | let newPath: string = currentPath;
32 | if (currentPath !== "/") {
33 | newPath += "(.*)";
34 | }
35 | return newPath;
36 | }
37 |
38 | private appendNextPaths(
39 | parentsPath: string,
40 | middlewares: MiddlewareProps[] | ErrorMiddlewareProps[],
41 | ): MiddlewareProps[] | ErrorMiddlewareProps[] {
42 | const newMiddlewares: MiddlewareProps[] = [];
43 | middlewares.forEach((middleware: any) => {
44 | if (middleware.url && parentsPath !== "/") {
45 | const combinedUrl = `${parentsPath}${
46 | middleware.url === "/" ? "" : middleware.url
47 | }`;
48 | newMiddlewares.push({
49 | ...middleware,
50 | url: combinedUrl.includes("(.*)")
51 | ? combinedUrl
52 | : combinedUrl.replace(/\*/g, "(.*)"),
53 | next: middleware.next
54 | ? this.appendNextPaths(parentsPath, middleware.next)
55 | : middleware.next,
56 | });
57 | } else {
58 | newMiddlewares.push(middleware);
59 | }
60 | });
61 | return newMiddlewares;
62 | }
63 |
64 | /**
65 | * @false Callback
66 | * @true Instance
67 | */
68 | private isInstance(arg: any): boolean {
69 | return arg instanceof App || arg instanceof Router;
70 | }
71 |
72 | private saveMiddlewares(
73 | type: SupportMethodType,
74 | args: any[],
75 | ) {
76 | let temp: MiddlewareProps = { method: type };
77 | args.forEach((arg) => {
78 | if (this.isString(arg)) {
79 | temp.url = arg;
80 | } else {
81 | if (this.isInstance(arg)) {
82 | if (temp.url) {
83 | if (temp.url.includes("*")) {
84 | throw "If middleware has a next, the parent's middleware can't have a wildcard. : " +
85 | temp.url;
86 | }
87 | temp.next =
88 | (this.appendNextPaths(
89 | temp.url,
90 | arg.middlewares,
91 | ) as MiddlewareProps[]);
92 | temp.url = this.appendParentsPaths(temp.url);
93 | } else {
94 | temp.next = arg.middlewares;
95 | }
96 | } else {
97 | if (temp.url) {
98 | temp.url = temp.url.includes("(.*)")
99 | ? temp.url
100 | : temp.url.replace(/\*/g, "(.*)");
101 |
102 | const matchedParamHandler = this.#paramHandlerStacks.filter(paramHandler => temp.url?.includes(`:${paramHandler.paramName}`));
103 |
104 | if (!isEmpty(matchedParamHandler)) {
105 | temp.paramHandlers = matchedParamHandler;
106 | }
107 | }
108 | temp.callBack = arg as CallBackType;
109 | }
110 | }
111 |
112 | if (temp.callBack || temp.next) {
113 | this.middlewares.push(temp);
114 | temp = temp.url ? { method: type, url: temp.url } : { method: type };
115 | }
116 | });
117 | }
118 |
119 | private saveParamStacks(
120 | paramName: string,
121 | args: ParamCallBackType[],
122 | ) {
123 | args.forEach((arg) => {
124 | this.#paramHandlerStacks.push({ paramName, callBack: arg });
125 | });
126 | }
127 |
128 | private saveErrorMiddlewares(
129 | args: any[],
130 | ) {
131 | let temp: ErrorMiddlewareProps = {};
132 | args.forEach((arg) => {
133 | if (this.isString(arg)) {
134 | temp.url = arg;
135 | } else {
136 | if (this.isInstance(arg)) {
137 | if (temp.url) {
138 | if (temp.url.includes("*")) {
139 | throw new Error(
140 | `If middleware has a next, the parent's middleware can't have a wildcard. ${temp.url}`,
141 | );
142 | }
143 | temp.next =
144 | (this.appendNextPaths(
145 | temp.url,
146 | arg.errorMiddlewares,
147 | ) as ErrorMiddlewareProps[]);
148 | temp.url = this.appendParentsPaths(temp.url);
149 | } else {
150 | temp.next = arg.errorMiddlewares;
151 | }
152 | } else {
153 | if (temp.url) {
154 | temp.url = temp.url.includes("(.*)")
155 | ? temp.url
156 | : temp.url.replace(/\*/g, "(.*)");
157 | }
158 | temp.callBack = arg as ErrorCallBackType;
159 | }
160 | }
161 |
162 | if (temp.callBack || temp.next) {
163 | this.errorMiddlewares.push(temp);
164 | temp = temp.url ? { url: temp.url } : {};
165 | }
166 | });
167 | }
168 |
169 | public use(app: App | Router): void;
170 | public use(callBack: CallBackType): void;
171 | public use(...callBack: CallBackType[]): void;
172 | public use(url: string, callBack: CallBackType): void;
173 | public use(url: string, ...callBack: CallBackType[]): void;
174 | public use(url: string, app: App | Router): void;
175 | public use(
176 | ...args: any
177 | ) {
178 | this.saveMiddlewares("ALL", args);
179 | }
180 |
181 | public get(app: App | Router): void;
182 | public get(callBack: CallBackType): void;
183 | public get(...callBack: CallBackType[]): void;
184 | public get(url: string, callBack: CallBackType): void;
185 | public get(url: string, ...callBack: CallBackType[]): void;
186 | public get(url: string, app: App | Router): void;
187 | public get(
188 | ...args: any
189 | ) {
190 | this.saveMiddlewares("GET", args);
191 | }
192 |
193 | public post(app: App | Router): void;
194 | public post(callBack: CallBackType): void;
195 | public post(...callBack: CallBackType[]): void;
196 | public post(url: string, callBack: CallBackType): void;
197 | public post(url: string, ...callBack: CallBackType[]): void;
198 | public post(url: string, app: App | Router): void;
199 | public post(
200 | ...args: any
201 | ) {
202 | this.saveMiddlewares("POST", args);
203 | }
204 |
205 | public put(app: App | Router): void;
206 | public put(callBack: CallBackType): void;
207 | public put(...callBack: CallBackType[]): void;
208 | public put(url: string, callBack: CallBackType): void;
209 | public put(url: string, ...callBack: CallBackType[]): void;
210 | public put(url: string, app: App | Router): void;
211 | public put(
212 | ...args: any
213 | ) {
214 | this.saveMiddlewares("PUT", args);
215 | }
216 |
217 | public patch(app: App | Router): void;
218 | public patch(callBack: CallBackType): void;
219 | public patch(...callBack: CallBackType[]): void;
220 | public patch(url: string, callBack: CallBackType): void;
221 | public patch(url: string, ...callBack: CallBackType[]): void;
222 | public patch(url: string, app: App | Router): void;
223 | public patch(
224 | ...args: any
225 | ) {
226 | this.saveMiddlewares("PATCH", args);
227 | }
228 |
229 | public delete(app: App | Router): void;
230 | public delete(callBack: CallBackType): void;
231 | public delete(...callBack: CallBackType[]): void;
232 | public delete(url: string, callBack: CallBackType): void;
233 | public delete(url: string, ...callBack: CallBackType[]): void;
234 | public delete(url: string, app: App | Router): void;
235 | public delete(
236 | ...args: any
237 | ) {
238 | this.saveMiddlewares("DELETE", args);
239 | }
240 |
241 | public options(app: App | Router): void;
242 | public options(callBack: CallBackType): void;
243 | public options(...callBack: CallBackType[]): void;
244 | public options(url: string, callBack: CallBackType): void;
245 | public options(url: string, ...callBack: CallBackType[]): void;
246 | public options(url: string, app: App | Router): void;
247 | public options(
248 | ...args: any
249 | ) {
250 | this.saveMiddlewares("OPTIONS", args);
251 | }
252 |
253 | public error(app: App | Router): void;
254 | public error(callBack: ErrorCallBackType): void;
255 | public error(...callBack: ErrorCallBackType[]): void;
256 | public error(url: string, callBack: ErrorCallBackType): void;
257 | public error(url: string, ...callBack: ErrorCallBackType[]): void;
258 | public error(url: string, app: App | Router): void;
259 | public error(
260 | ...args: any
261 | ) {
262 | this.saveErrorMiddlewares(args);
263 | }
264 |
265 | /**
266 | * Param handler
267 | * @param {string} paramName param name ex) `username`
268 | * @param {ParamCallBackType} callBack param callback type
269 | */
270 | public param(paramName: string, ...callBack: ParamCallBackType[]) {
271 | this.saveParamStacks(paramName, callBack);
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/core/response.ts:
--------------------------------------------------------------------------------
1 | import type { CallBackType } from "./types.ts";
2 | import version from "../version.ts";
3 | import { etag, fileStream, last, normalizeType, readableStreamFromReader } from "./utils.ts";
4 | import { AttainRequest } from "./request.ts";
5 |
6 | type ContentsType = Uint8Array | Deno.Reader | string | object | boolean;
7 | function instanceOfReader(object: any): object is Deno.Reader {
8 | return "read" in object;
9 | }
10 |
11 | const generalBody = ["string", "number", "bigint", "boolean", "symbol"];
12 | const encoder = new TextEncoder();
13 |
14 | const isHtml = (value: string): boolean => {
15 | return /^\s*<(?:!DOCTYPE|html|body)/i.test(value);
16 | };
17 |
18 | export class AttainResponse {
19 | #serverRequest?: Request;
20 | #headers: Headers;
21 | #status: number;
22 | #statusText?: string;
23 | #body?: BodyInit;
24 | #pending: Function[];
25 | #resources: number[] = [];
26 | #processDone: boolean;
27 |
28 | constructor(_serverRequest: Request) {
29 | this.#serverRequest = _serverRequest;
30 | this.#headers = new Headers();
31 | this.#body = undefined;
32 | this.#status = 200;
33 | this.#pending = [];
34 | this.#processDone = false;
35 | this.setHeader("X-Powered-By", `Deno, Attain v${version}`);
36 | }
37 |
38 | destroy(): void {
39 | this.#serverRequest = undefined;
40 | for (const rid of this.#resources) {
41 | try {
42 | Deno.close(rid);
43 | } catch(e) {}
44 | }
45 | this.#resources = [];
46 | }
47 |
48 | get processDone(): boolean {
49 | return this.#processDone;
50 | }
51 |
52 | /**
53 | * Return the original ServerRequest class object.
54 | */
55 | get serverRequest(): Request {
56 | if (!this.#serverRequest) {
57 | throw new Error("already responded");
58 | }
59 | return this.#serverRequest;
60 | }
61 |
62 | /**
63 | * Return the current response object which will be used for responding
64 | */
65 | get getResponse(): Response {
66 | return new Response(this.getBody, {
67 | headers: this.getHeaders,
68 | status: this.getStatus,
69 | statusText: this.getStatusText,
70 | });
71 | }
72 |
73 | /**
74 | * Return the current header class object.
75 | */
76 | get getHeaders(): Headers {
77 | return this.#headers;
78 | }
79 |
80 | /**
81 | * Return the current status number
82 | */
83 | get getStatus(): number | undefined {
84 | return this.#status;
85 | }
86 |
87 | get getStatusText(): string | undefined {
88 | return this.#statusText;
89 | }
90 |
91 | /**
92 | * Return the current body data
93 | */
94 | get getBody() {
95 | return this.#body;
96 | }
97 |
98 | /**
99 | * Execute pend jobs, It's automatically executed after calling the `end()` or `send()`.
100 | * @param request - latest Request class object
101 | */
102 | public async executePendingJobs(request: AttainRequest): Promise {
103 | if (this.#pending.length === 0) {
104 | return;
105 | }
106 | for await (const job of this.#pending) {
107 | await job(request, this);
108 | }
109 | }
110 |
111 | /**
112 | * Pend the jobs which will execute right before responding.
113 | * @param fn - An array of callback types
114 | *
115 | * pend((afterReq, afterRes) => {...jobs})
116 | */
117 | public pend(...fn: CallBackType[]): void {
118 | this.#pending.push(...fn);
119 | }
120 |
121 | /**
122 | * Set the status
123 | * @param status - number of the http code.
124 | */
125 | public status(status: number, statusText?: string) {
126 | this.#status = status;
127 | this.#statusText = statusText;
128 | return this;
129 | }
130 |
131 | /**
132 | * Set the body without response
133 | * @param body - contents
134 | */
135 | public body(body: ContentsType) {
136 | if (generalBody.includes(typeof body)) {
137 | this.#body = encoder.encode(String(body));
138 | this.setContentType(
139 | isHtml(String(body))
140 | ? "text/html; charset=utf-8"
141 | : "text/plain; charset=utf-8",
142 | );
143 | } else if (body instanceof Uint8Array) {
144 | this.#body = body;
145 | } else if (body && instanceOfReader(body)) {
146 | this.#body = readableStreamFromReader(body);
147 | } else if (body && typeof body === "object") {
148 | this.#body = encoder.encode(JSON.stringify(body));
149 | this.setContentType("application/json; charset=utf-8");
150 | }
151 | return this;
152 | }
153 |
154 | /**
155 | * Replace the current entire header object with new Headers
156 | * @param headers - Headers(deno) class object
157 | */
158 | public setHeaders(headers: Headers) {
159 | this.#headers = headers;
160 | return this;
161 | }
162 |
163 | /**
164 | * Get the header data by a key
165 | * @param name - key
166 | */
167 | public getHeader(name: string) {
168 | return this.#headers.get(name);
169 | }
170 |
171 | /**
172 | * Set the header data
173 | * @param name - key
174 | * @param value - header data
175 | *
176 | * setHeader("Content-Type", "application/json");
177 | */
178 | public setHeader(name: string, value: string) {
179 | this.#headers.set(name, value);
180 | return this;
181 | }
182 |
183 | /**
184 | * Remove data from the header by a key.
185 | * @param name - header key
186 | */
187 | public removeHeader(name: string) {
188 | this.#headers.delete(name);
189 | return this;
190 | }
191 |
192 | /**
193 | * Set the Content-Type header
194 | * It'll append the data
195 | * @param type - content type like "application/json"
196 | */
197 | public setContentType(type: string) {
198 | if (this.getHeaders.has("Content-Type")) {
199 | const contentType = this.getHeaders.get("Content-Type");
200 | if (contentType && contentType.includes(type)) {
201 | return this;
202 | }
203 | this.#headers.append("Content-Type", type);
204 | } else {
205 | this.setHeader("Content-Type", type);
206 | }
207 | return this;
208 | }
209 |
210 | private format(obj: any) {
211 | const defaultFn = obj.default;
212 | if (defaultFn) delete obj.default;
213 | const keys: any = Object.keys(obj);
214 |
215 | const tempRequest = new AttainRequest(this.serverRequest);
216 | const key: any = keys.length > 0 ? tempRequest.accepts(keys) : false;
217 |
218 | if (key) {
219 | this.setHeader("Content-type", normalizeType(key).value);
220 | this.body(key());
221 | } else if (defaultFn) {
222 | this.body(defaultFn());
223 | } else {
224 | this.status(406);
225 | }
226 | }
227 |
228 | /**
229 | * Set the body and respond with response object.
230 | * @param contents - the body contents
231 | */
232 | public async send(contents: ContentsType): Promise {
233 | try {
234 | this.body(contents);
235 | this.end();
236 | } catch (error) {
237 | throw error;
238 | }
239 | }
240 |
241 | /**
242 | * Serve the static files
243 | * @param filePath - path of the static file
244 | *
245 | * Required `await`
246 | */
247 | public async sendFile(filePath: string): Promise {
248 | let fileInfo = await Deno.stat(filePath);
249 | if (fileInfo.isFile) {
250 | const stream = await fileStream(this, filePath);
251 | this.#resources.push(stream.rid);
252 | this.status(200).body(stream).end();
253 | } else {
254 | throw new Error(`${filePath} can't find.`);
255 | }
256 | }
257 |
258 | /**
259 | * Serve the static file and force the browser to download it.
260 | * @param filePath - path of the static file
261 | * @param name - save as the `name`
262 | *
263 | * Required `await`
264 | */
265 | public async download(filePath: string, name?: string): Promise {
266 | try {
267 | let fileName = filePath;
268 | if (!name) {
269 | const splited = filePath.split("/");
270 | fileName = last(splited);
271 | } else {
272 | const hasFileType = name.split(".").length > 1 ? true : false;
273 | if (hasFileType) {
274 | fileName = name;
275 | } else {
276 | throw `${name} dosen't have filetype`;
277 | }
278 | }
279 |
280 | const hasFileType = fileName.split(".").length > 1 ? true : false;
281 | if (hasFileType) {
282 | this.setHeader(
283 | "Content-Disposition",
284 | `attachment; filename="${fileName}"`,
285 | );
286 | } else {
287 | throw `${fileName} dosen't have filetype`;
288 | }
289 |
290 | await this.sendFile(filePath);
291 | } catch (error) {
292 | console.error(error);
293 | }
294 | }
295 |
296 | /**
297 | * Redirection
298 | * @param url
299 | */
300 | public redirect(url: string | "back") {
301 | let loc = url;
302 | if (url === "back") {
303 | loc = this.serverRequest.referrer || "/";
304 | }
305 | this.setHeader("Location", encodeURI(loc));
306 |
307 | this.format({
308 | text: function () {
309 | return 302 + ". Redirecting to " + loc;
310 | },
311 | html: function () {
312 | var u = escape(loc);
313 | return "" + 302 + '. Redirecting to ' + u +
314 | "
";
315 | },
316 | default: function () {
317 | return "";
318 | },
319 | });
320 |
321 | this.status(302).end();
322 | }
323 |
324 | /**
325 | * End the current process and respond
326 | */
327 | public async end(): Promise {
328 | try {
329 | this.setHeader("Date", new Date().toUTCString());
330 | const currentETag = this.getHeader("etag");
331 | const len = this.getHeader("content-length") ||
332 | (this.getBody as Uint8Array).length.toString();
333 | const newETag = etag(
334 | (this.getBody as Uint8Array),
335 | parseInt(len, 10),
336 | );
337 | if (currentETag && currentETag === newETag) {
338 | this.status(304);
339 | } else {
340 | this.setHeader("etag", newETag);
341 | }
342 | this.#processDone = true;
343 | } catch (error) {
344 | if (error instanceof Deno.errors.BadResource) {
345 | console.log("Connection Lost");
346 | } else {
347 | console.error(error);
348 | }
349 | }
350 | }
351 | }
352 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Attain - v1.1.2 - [Website](https://aaronwlee.github.io/attain/)
6 |
7 | 
8 | 
9 |
10 | [](https://nest.land/package/attain)
11 |
12 | A middleware web framework for Deno which is using [http](https://github.com/denoland/deno_std/tree/master/http#http) standard library inspired by [express](https://github.com/expressjs/express) and [Oak](https://github.com/oakserver/oak).
13 |
14 | Attain is blazingly fast due to handled the multi-structured middleware and routes effectively. It also strictly manage memory consumption.
15 |
16 | Only for [Deno](https://deno.land/) - __Require Deno version up to: v1.16.4__
17 |
18 | Any contributions to the code would be appreciated. :)
19 |
20 |
21 |
22 |
23 | ### Download and use
24 |
25 | ```js
26 | import { App, Router } from "https://deno.land/x/attain/mod.ts";
27 | // or
28 | import { App, Router } from "https://deno.land/x/attain@1.1.2/mod.ts";
29 | // or
30 | import { App, Router, Request, Response } from "https://raw.githubusercontent.com/aaronwlee/attain/1.1.2/mod.ts";
31 | ```
32 |
33 |
34 | ```
35 | # deno run --allow-net --unstable main.ts
36 | ```
37 |
38 | ## Contents
39 |
40 | - [Getting Start](#getting-start)
41 | - [Procedure explain](#procedure-explain)
42 | - [How To](#how-to)
43 | - [Boilerplate](#boilerplate)
44 | - [Methods and Properies](#methods-and-properies)
45 | - [Response](#response)
46 | - [Request](#request)
47 | - [Router](#router)
48 | - [App](#app)
49 | - [Nested Routing](#nested-routing)
50 | - [Extra plugins](#extra-plugins)
51 | - [More Features](#more-features)
52 | - [Performance](https://github.com/aaronwlee/attain/blob/master/performance/performance.md)
53 |
54 | ## Getting Start
55 |
56 | ```ts
57 | import { App } from "https://deno.land/x/attain/mod.ts";
58 | import type { Request, Response } from "https://deno.land/x/attain/mod.ts";
59 |
60 | const app = new App();
61 |
62 | const sampleMiddleware = (req: Request, res: Response) => {
63 | console.log("before send");
64 | };
65 |
66 | app.get("/:id", (req, res) => {
67 | console.log(req.params);
68 | res.status(200).send(`id: ${req.params.id}`);
69 | });
70 |
71 | app.use(sampleMiddleware, (req, res) => {
72 | res.status(200).send({ status: "Good" });
73 | });
74 |
75 | app.listen({ port: 3500 });
76 | ```
77 |
78 | ### Procedure explain
79 |
80 | The middleware process the function step by step based on registered order.
81 |
82 | 
83 |
84 | ```ts
85 | import { App } from "https://deno.land/x/attain/mod.ts";
86 |
87 | const app = new App();
88 |
89 | const sleep = (time: number) => {
90 | return new Promise(resolve => setTimeout(() => resolve(), time)
91 | };
92 |
93 | app.use((req, res) => {
94 | console.log("First step");
95 | }, async (req, res) => {
96 | await sleep(2000); // the current request procedure will stop here for two seconds.
97 | console.log("Second step");
98 | });
99 |
100 | app.use((req, res) => {
101 | // pend a job
102 | res.pend((afterReq, afterRes) => {
103 | console.log("Fourth step");
104 | console.log("Fifth step with error");
105 | console.log("You can finalize your procedure right before respond.")
106 | console.log("For instance, add a header or caching.")
107 | })
108 | })
109 |
110 | // last step
111 | app.use("/", (req, res) => {
112 | console.log("Third step with GET '/'");
113 | // this is the end point
114 | res.status(200).send({status: "Good"});
115 | });
116 |
117 | app.use("/", (req, res) => {
118 | console.log("Will not executed");
119 | });
120 |
121 | app.get("/error", (req, res) => {
122 | console.log("Third step with GET '/error'");
123 | throw new Error("I have error!")
124 | })
125 |
126 | app.error((err, req, res) => {
127 | console.log("Fourth step with error");
128 | console.log("A sequence of error handling.", err)
129 | res.status(500).send("Critical error.");
130 | })
131 |
132 | app.listen({ port: 3500 });
133 | ```
134 |
135 | ## How To
136 |
137 | [Web Socket Example](https://github.com/aaronwlee/Attain/tree/master/howto/websocket.md)
138 |
139 | [Auto Recovery](https://github.com/aaronwlee/Attain/tree/master/howto/autorecovery.md)
140 |
141 | ## Boilerplate
142 |
143 | [A Deno web boilerplate](https://github.com/burhanahmeed/Denamo) by [burhanahmeed](https://github.com/burhanahmeed)
144 |
145 | ## Methods and Properies
146 |
147 | ### Response
148 |
149 | Methods
150 | Getter
151 |
152 | - `getResponse(): AttainResponse`
153 | Get current response object, It will contain the body, status and headers.
154 |
155 | - `headers(): Headers`
156 | Get current header map
157 |
158 | - `getStatus(): number | undefined`
159 | Get current status
160 |
161 | - `getBody(): Uint8Array`
162 | Get current body contents
163 |
164 | Functions
165 |
166 | - `pend(...fn: CallBackType[]): void`
167 | Pend the jobs. It'll start right before responding.
168 |
169 | - `status(status: number)`
170 | Set status number
171 |
172 | - `body(body: ContentsType)`
173 | Set body. Allows setting `Uint8Array, Deno.Reader, string, object, boolean`. This will not respond.
174 |
175 | - `setHeaders(headers: Headers)`
176 | You can overwrite the response header.
177 |
178 | - `getHeader(name: string)`
179 | Get a header from the response by key name.
180 |
181 | - `setHeader(name: string, value: string)`
182 | Set a header.
183 |
184 | - `setContentType(type: string)`
185 | This is a shortcut for the "Content-Type" in the header. It will try to find "Content-Type" from the header then set or append the values.
186 |
187 | - `send(contents: ContentsType): Promise`
188 | Setting the body then executing the end() method.
189 |
190 | - `await sendFile(filePath: string): Promise`
191 | Transfers the file at the given path. Sets the Content-Type response HTTP header field based on the filename's extension.
192 | _Required to be await_
193 | These response headers might be needed to set for fully functioning
194 |
195 | | Property | Description |
196 | | ------------ | ---------------------------------------------------------------------------------------------- |
197 | | maxAge | Sets the max-age property of the Cache-Control header in milliseconds or a string in ms format |
198 | | root | Root directory for relative filenames. |
199 | | cacheControl | Enable or disable setting Cache-Control response header. |
200 |
201 | - `await download(filePath: string, name?: string): Promise`
202 | Transfers the file at the path as an "attachment". Typically, browsers will prompt the user to download and save it as a name if provided.
203 | _Required to be await_
204 |
205 | - `redirect(url: string | "back")`
206 | Redirecting the current response.
207 |
208 | - `end(): Promise`
209 | Executing the pended job then respond back to the current request. It'll end the current procedure.
210 |
211 | ### Request
212 |
213 | > [Oak](https://github.com/oakserver/oak/tree/master#request) for deno
214 |
215 | This class used Oak's request library. Check this.
216 | Note: to access Oak's `Context.params` use `Request.params`. but require to use a `app.use(parser)` plugin.
217 |
218 | ### Router
219 |
220 | Methods
221 |
222 | - `use(app: App | Router): void`
223 | - `use(callBack: CallBackType): void`
224 | - `use(...callBack: CallBackType[]): void`
225 | - `use(url: string, callBack: CallBackType): void`
226 | - `use(url: string, ...callBack: CallBackType[]): void`
227 | - `use(url: string, app: App | Router): void`
228 | - `get...`
229 | - `post...`
230 | - `put...`
231 | - `patch...`
232 | - `delete...`
233 | - `error(app: App | Router): void;`
234 | - `error(callBack: ErrorCallBackType): void;`
235 | - `error(...callBack: ErrorCallBackType[]): void;`
236 | - `error(url: string, callBack: ErrorCallBackType): void;`
237 | - `error(url: string, ...callBack: ErrorCallBackType[]): void;`
238 | - `error(url: string, app: App | Router): void;`
239 | It'll handle the error If thrown from one of the above procedures.
240 |
241 | Example
242 |
243 | ```ts
244 | app.use((req, res) => {
245 | throw new Error("Something wrong!");
246 | });
247 |
248 | app.error((error, req, res) => {
249 | console.error("I handle the Error!", error);
250 | res.status(500).send("It's critical!");
251 | });
252 | ```
253 |
254 | - `param(paramName: string, ...callback: ParamCallBackType[]): void;`
255 | Parameter handler [router.param](https://expressjs.com/en/api.html#router.param)
256 |
257 | Example
258 |
259 | ```ts
260 | const userController = new Router();
261 |
262 | userController.param("username", (req, res, username) => {
263 | const user = await User.findOne({ username: username });
264 | if (!user) {
265 | throw new Error("user not found");
266 | }
267 | req.profile = user;
268 | });
269 |
270 | userController.get("/:username", (req, res) => {
271 | res.status(200).send({ profile: req.profile });
272 | });
273 |
274 | userController.post("/:username/follow", (req, res) => {
275 | const user = await User.findById(req.payload.id);
276 | if (user.following.indexOf(req.profile._id) === -1) {
277 | user.following.push(req.profile._id);
278 | }
279 | const profile = await user.save();
280 | return res.status(200).send({ profile: profile });
281 | });
282 |
283 | export default userController;
284 | ```
285 |
286 | These are middleware methods and it's like express.js.
287 |
288 | ### App
289 |
290 | _App extends Router_
291 | Methods
292 |
293 | - `This has all router's methods`
294 |
295 | Properties
296 |
297 | - `listen(options)`
298 | Start the Attain server.
299 |
300 | ```ts
301 | options: {
302 | port: number; // required
303 | debug?: boolean; // debug mode
304 | hostname?: string; // hostname default as 0.0.0.0
305 | secure?: boolean; // https use
306 | certFile?: string; // if secure is true, it's required
307 | keyFile?: string; // if secure is true, it's required
308 | }
309 | ```
310 |
311 | - `database(dbCls)` **NEW FEATURE!**
312 | Register a database to use in all of your middleware functions.
313 | Example:
314 |
315 | ```ts
316 | /* ExampleDatabase.ts */
317 | class ExampleDatabase extends AttainDatabase {
318 | async connect() {
319 | console.log('database connected');
320 | }
321 | async getAllUsers() {
322 | return [{ name: 'Shaun' }, { name: 'Mike' }];
323 | }
324 | }
325 |
326 | /* router.ts */
327 | const router = new Router();
328 |
329 | router.get('/', async (req: Request, res: Response, db: ExampleDatabase) => {
330 | const users = await db.getAllUsers();
331 | res.status(200).send(users);
332 | })
333 |
334 | /* index.ts */
335 | const app = new App();
336 |
337 | await app.database(ExampleDatabase);
338 |
339 | app.use('/api/users', router);
340 |
341 | ```
342 |
343 | **NOTE:** for this feature to work as expected, you must:
344 | - provide a `connect()` method to your database class
345 | - extend the `AttainDatabase` class
346 |
347 |
348 | This feature is brand new and any contributins and ideas will be welcomed
349 |
350 | - `static startWith(connectFunc)`
351 | Automatically initialize the app and connect to the database with a connect function.
352 |
353 | - `static startWith(dbClass)`
354 | Automatically initialize the app create a database instance.
355 |
356 | ## Nested Routing
357 |
358 | > **Path** - router.ts
359 |
360 | **warn**: async await will block your procedures.
361 |
362 | ```ts
363 | import { Router } from "https://deno.land/x/attain/mod.ts";
364 |
365 | const api = new Router();
366 | // or
367 | // const api = new App();
368 |
369 | const sleep = (time: number) => {
370 | new Promise((resolve) => setTimeout(() => resolve(), time));
371 | };
372 |
373 | // It will stop here for 1 second.
374 | api.get("/block", async (req, res) => {
375 | console.log("here '/block'");
376 | await sleep(1000);
377 | res.status(200).send(`
378 |
379 |
380 |
381 | Hello
382 |
383 |
384 | `);
385 | });
386 |
387 | // It will not stop here
388 | api.get("/nonblock", (req, res) => {
389 | console.log("here '/nonblock'");
390 | sleep(1000).then((_) => {
391 | res.status(200).send(`
392 |
393 |
394 |
395 | Hello
396 |
397 |
398 | `);
399 | });
400 | });
401 |
402 | export default api;
403 | ```
404 |
405 | > **Path** - main.ts
406 |
407 | ```ts
408 | import { App } from "https://deno.land/x/attain/mod.ts";
409 | import api from "./router.ts";
410 |
411 | const app = new App();
412 |
413 | // nested router applied
414 | app.use("/api", api);
415 |
416 | app.use((req, res) => {
417 | res.status(404).send("page not found");
418 | });
419 |
420 | app.listen({ port: 3500 });
421 | ```
422 |
423 | ```
424 | # start with: deno run -A ./main.ts
425 | ```
426 |
427 | ## Extra plugins
428 |
429 | - **logger** : `Logging response "response - method - status - path - time"`
430 | - **parser** : `Parsing the request body and save it to request.params`
431 | - **security**: `Helping you make secure application by setting various HTTP headers` [Helmet](https://helmetjs.github.io/)
432 |
433 | ### Security options
434 |
435 | | Options | Default? |
436 | | ------------------------------------------------------- | -------- |
437 | | `xss` (adds some small XSS protections) | yes |
438 | | `removePoweredBy` (remove the X-Powered-By header) | yes |
439 | | `DNSPrefetchControl` (controls browser DNS prefetching) | yes |
440 | | `noSniff` (to keep clients from sniffing the MIME type) | yes |
441 | | `frameguard` (prevent clickjacking) | yes |
442 |
443 | - **staticServe** : `It'll serve the static files from a provided path by joining the request path.`
444 |
445 | > Out of box
446 |
447 | - [**Attain-GraphQL**](https://deno.land/x/attain_graphql#attain-graphql) : `GraphQL middleware`
448 | - [**deno_graphql**](https://deno.land/x/deno_graphql#setup-with-attain): `GraphQL middleware`
449 | - [**session**](https://deno.land/x/session): `cookie session`
450 | - [**cors**](https://deno.land/x/cors/#examples): `CORS`
451 |
452 | ```ts
453 | import {
454 | App,
455 | logger,
456 | parser,
457 | security,
458 | staticServe,
459 | } from "https://deno.land/x/Attain/mod.ts";
460 |
461 | const app = new App();
462 |
463 | // Set Extra Security setting
464 | app.use(security());
465 |
466 | // Logging response method status path time
467 | app.use(logger);
468 |
469 | // Parsing the request body and save it to request.params
470 | // Also, updated to parse the queries from search params
471 | app.use(parser);
472 |
473 | // Serve static files
474 | // This path must be started from your command line path.
475 | app.use(staticServe("./public", { maxAge: 1000 }));
476 |
477 | app.use("/", (req, res) => {
478 | res.status(200).send("hello");
479 | });
480 |
481 | app.use("/google", (req, res) => {
482 | res.redirect("https://www.google.ca");
483 | });
484 |
485 | app.use("/:id", (req, res) => {
486 | // This data has parsed by the embedded URL parser.
487 | console.log(req.params);
488 | res.status(200).send(`id: ${req.params.id}`);
489 | });
490 |
491 | app.post("/submit", (req, res) => {
492 | // By the parser middleware, the body and search query get parsed and saved.
493 | console.log(req.params);
494 | console.log(req.query);
495 | res.status(200).send({ data: "has received" });
496 | });
497 |
498 | app.listen({ port: 4000 });
499 | ```
500 |
501 | ## More Features
502 |
503 | ### Switch your database with just one line of code
504 | Using the `app.database()` option, you can switch your database with just one line of code! To use this feature, create a database class that extends the `AttainDatabase` class:
505 |
506 | ```ts
507 | class PostgresDatabase extends AttainDatabase {
508 | #client: Client
509 | async connect() {
510 | const client = new Client({
511 | user: Deno.env.get('USER'),
512 | database: Deno.env.get('DB'),
513 | hostname: Deno.env.get('HOST'),
514 | password: Deno.env.get('PASSWORD')!,
515 | port: parseInt(Deno.env.get('PORT')),
516 | });
517 | await client.connect();
518 | this.#client = client;
519 | }
520 | async getAllProducts() {
521 | const data = await this.#client.query('SELECT * FROM products');
522 | /* map data */
523 | return products;
524 | }
525 | }
526 |
527 | /* OR */
528 | class MongoDatabase extends AttainDatabase {
529 | #Product: Collection
530 | async connect() {
531 | const client = new MongoClient();
532 | await client.connectWithUri(Deno.env.get('DB_URL'));
533 | const database = client.database(Deno.env.get('DB_NAME'));
534 | this.#Product = database.collection('Product');
535 | }
536 | async getAllProducts() {
537 | return await this.#Product.findAll()
538 | }
539 | }
540 | ```
541 |
542 | Then pick one of the databases to use in your app:
543 | ```ts
544 | await app.database(MongoDatabase);
545 | /* OR */
546 | await app.database(PostgresDatabase);
547 | /* OR */
548 | const app = App.startWith(MongoDatabase);
549 |
550 | app.get('/products', (req, res, db) => {
551 | const products = await db.getAllProducts();
552 | res.status(200).send(products); /* will work the same! */
553 | })
554 |
555 | ```
556 |
557 | You can also provide a function that returns a database connection
558 | ```ts
559 | import { App, Router, Request, Response, AttainDatabase } from "./mod.ts";
560 | import { MongoClient, Database } from "https://deno.land/x/mongo@v0.9.2/mod.ts";
561 |
562 | async function DB() {
563 | const client = new MongoClient()
564 | await client.connectWithUri("mongodb://localhost:27017")
565 | const database = client.database("test")
566 | return database;
567 | }
568 |
569 | // allow auto inherit mode (auto inherit the types to the middleware)
570 |
571 | const app = App.startWith(DB);
572 | // or
573 | const app = new App()
574 | app.database(DB)
575 |
576 | // this db params will have automatically inherited types from the app<> or startWith method.
577 | app.use((req, res, db) => {
578 |
579 | })
580 | ```
581 |
582 | ---
583 |
584 | There are several modules that are directly adapted from other modules.
585 | They have preserved their individual licenses and copyrights. All of the modules,
586 | including those directly adapted are licensed under the MIT License.
587 |
588 | All additional work is copyright 2021 the Attain authors. All rights reserved.
589 |
--------------------------------------------------------------------------------