{"id":13736247,"url":"https://github.com/aaronwlee/Attain","last_synced_at":"2025-05-08T12:32:37.120Z","repository":{"id":44612952,"uuid":"263528109","full_name":"aaronwlee/attain","owner":"aaronwlee","description":"Deno API middleware Server","archived":false,"fork":false,"pushed_at":"2021-12-10T13:48:43.000Z","size":4052,"stargazers_count":79,"open_issues_count":8,"forks_count":3,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-10-17T03:18:08.478Z","etag":null,"topics":["back-end","deno","framework","front-end","server","server-side-rendering"],"latest_commit_sha":null,"homepage":"https://aaronwlee.github.io/Attain/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aaronwlee.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-05-13T04:48:08.000Z","updated_at":"2023-05-12T16:58:56.000Z","dependencies_parsed_at":"2022-09-23T02:40:33.253Z","dependency_job_id":null,"html_url":"https://github.com/aaronwlee/attain","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aaronwlee%2Fattain","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aaronwlee%2Fattain/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aaronwlee%2Fattain/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aaronwlee%2Fattain/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aaronwlee","download_url":"https://codeload.github.com/aaronwlee/attain/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224732130,"owners_count":17360416,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["back-end","deno","framework","front-end","server","server-side-rendering"],"created_at":"2024-08-03T03:01:18.148Z","updated_at":"2024-11-15T04:31:33.112Z","avatar_url":"https://github.com/aaronwlee.png","language":"TypeScript","readme":"\u003cp align=\"center\"\u003e\n  \u003cimg width=\"380\" height=\"200\" src=\"https://github.com/aaronwlee/attain/blob/master/Attain.png?raw=true\" alt=\"attain\" /\u003e\n\u003c/p\u003e\n\n# Attain - v1.1.2 - [Website](https://aaronwlee.github.io/attain/)\n\n![attain ci](https://github.com/aaronwlee/attain/workflows/attain%20ci/badge.svg)\n![license](https://img.shields.io/github/license/aaronwlee/attain)\n\n[![nest badge](https://nest.land/badge.svg)](https://nest.land/package/attain)\n\nA 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). \n\nAttain is blazingly fast due to handled the multi-structured middleware and routes effectively. It also strictly manage memory consumption.\n\nOnly for [Deno](https://deno.land/) - __Require Deno version up to: v1.16.4__\n\nAny contributions to the code would be appreciated. :)\n\n\n\u003cbr /\u003e\n\n### Download and use\n\n```js\nimport { App, Router } from \"https://deno.land/x/attain/mod.ts\";\n// or\nimport { App, Router } from \"https://deno.land/x/attain@1.1.2/mod.ts\";\n// or\nimport { App, Router, Request, Response } from \"https://raw.githubusercontent.com/aaronwlee/attain/1.1.2/mod.ts\";\n```\n\n\n```\n# deno run --allow-net --unstable main.ts\n```\n\n## Contents\n\n- [Getting Start](#getting-start)\n  - [Procedure explain](#procedure-explain)\n- [How To](#how-to)\n- [Boilerplate](#boilerplate)\n- [Methods and Properies](#methods-and-properies)\n  - [Response](#response)\n  - [Request](#request)\n  - [Router](#router)\n  - [App](#app)\n- [Nested Routing](#nested-routing)\n- [Extra plugins](#extra-plugins)\n- [More Features](#more-features)\n- [Performance](https://github.com/aaronwlee/attain/blob/master/performance/performance.md)\n\n## Getting Start\n\n```ts\nimport { App } from \"https://deno.land/x/attain/mod.ts\";\nimport type { Request, Response } from \"https://deno.land/x/attain/mod.ts\";\n\nconst app = new App();\n\nconst sampleMiddleware = (req: Request, res: Response) =\u003e {\n  console.log(\"before send\");\n};\n\napp.get(\"/:id\", (req, res) =\u003e {\n  console.log(req.params);\n  res.status(200).send(`id: ${req.params.id}`);\n});\n\napp.use(sampleMiddleware, (req, res) =\u003e {\n  res.status(200).send({ status: \"Good\" });\n});\n\napp.listen({ port: 3500 });\n```\n\n### Procedure explain\n\nThe middleware process the function step by step based on registered order.\n\n![alt text](https://github.com/aaronwlee/attain/blob/master/procedure.png?raw=true \"procedure\")\n\n```ts\nimport { App } from \"https://deno.land/x/attain/mod.ts\";\n\nconst app = new App();\n\nconst sleep = (time: number) =\u003e {\n  return new Promise(resolve =\u003e setTimeout(() =\u003e resolve(), time)\n};\n\napp.use((req, res) =\u003e {\n  console.log(\"First step\");\n}, async (req, res) =\u003e {\n  await sleep(2000); // the current request procedure will stop here for two seconds.\n  console.log(\"Second step\");\n});\n\napp.use((req, res) =\u003e {\n  // pend a job\n  res.pend((afterReq, afterRes) =\u003e {\n    console.log(\"Fourth step\");\n    console.log(\"Fifth step with error\");\n    console.log(\"You can finalize your procedure right before respond.\")\n    console.log(\"For instance, add a header or caching.\")\n  })\n})\n\n// last step\napp.use(\"/\", (req, res) =\u003e {\n  console.log(\"Third step with GET '/'\");\n  // this is the end point\n  res.status(200).send({status: \"Good\"});\n});\n\napp.use(\"/\", (req, res) =\u003e {\n  console.log(\"Will not executed\");\n});\n\napp.get(\"/error\", (req, res) =\u003e {\n  console.log(\"Third step with GET '/error'\");\n  throw new Error(\"I have error!\")\n})\n\napp.error((err, req, res) =\u003e {\n  console.log(\"Fourth step with error\");\n  console.log(\"A sequence of error handling.\", err)\n  res.status(500).send(\"Critical error.\");\n})\n\napp.listen({ port: 3500 });\n```\n\n## How To\n\n[Web Socket Example](https://github.com/aaronwlee/Attain/tree/master/howto/websocket.md)\n\n[Auto Recovery](https://github.com/aaronwlee/Attain/tree/master/howto/autorecovery.md)\n\n## Boilerplate\n\n[A Deno web boilerplate](https://github.com/burhanahmeed/Denamo) by [burhanahmeed](https://github.com/burhanahmeed)\n\n## Methods and Properies\n\n### Response\n\nMethods\nGetter\n\n- `getResponse(): AttainResponse`\n  \u003cbr /\u003e Get current response object, It will contain the body, status and headers.\n\n- `headers(): Headers`\n  \u003cbr /\u003e Get current header map\n\n- `getStatus(): number | undefined`\n  \u003cbr /\u003e Get current status\n\n- `getBody(): Uint8Array`\n  \u003cbr /\u003e Get current body contents\n\nFunctions\n\n- `pend(...fn: CallBackType[]): void`\n  \u003cbr /\u003e Pend the jobs. It'll start right before responding.\n\n- `status(status: number)`\n  \u003cbr /\u003e Set status number\n\n- `body(body: ContentsType)`\n  \u003cbr /\u003e Set body. Allows setting `Uint8Array, Deno.Reader, string, object, boolean`. This will not respond.\n\n- `setHeaders(headers: Headers)`\n  \u003cbr /\u003e You can overwrite the response header.\n\n- `getHeader(name: string)`\n  \u003cbr /\u003e Get a header from the response by key name.\n\n- `setHeader(name: string, value: string)`\n  \u003cbr /\u003e Set a header.\n\n- `setContentType(type: string)`\n  \u003cbr /\u003e 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.\n\n- `send(contents: ContentsType): Promise\u003cvoid | this\u003e`\n  \u003cbr /\u003e Setting the body then executing the end() method.\n\n- `await sendFile(filePath: string): Promise\u003cvoid\u003e`\n  \u003cbr /\u003e Transfers the file at the given path. Sets the Content-Type response HTTP header field based on the filename's extension.\n  \u003cbr /\u003e \u003cspan style=\"color: red;\"\u003e _Required to be await_ \u003c/span\u003e\n  \u003cbr /\u003e These response headers might be needed to set for fully functioning\n\n| Property     | Description                                                                                    |\n| ------------ | ---------------------------------------------------------------------------------------------- |\n| maxAge       | Sets the max-age property of the Cache-Control header in milliseconds or a string in ms format |\n| root         | Root directory for relative filenames.                                                         |\n| cacheControl | Enable or disable setting Cache-Control response header.                                       |\n\n- `await download(filePath: string, name?: string): Promise\u003cvoid\u003e`\n  \u003cbr /\u003e 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.\n  \u003cbr /\u003e \u003cspan style=\"color: red;\"\u003e _Required to be await_ \u003c/span\u003e\n\n- `redirect(url: string | \"back\")`\n  \u003cbr /\u003e Redirecting the current response.\n\n- `end(): Promise\u003cvoid\u003e`\n  \u003cbr /\u003e Executing the pended job then respond back to the current request. It'll end the current procedure.\n\n### Request\n\n\u003e [Oak](https://github.com/oakserver/oak/tree/master#request) for deno\n\nThis class used Oak's request library. Check this.\n\u003cbr /\u003e Note: to access Oak's `Context.params` use `Request.params`. but require to use a `app.use(parser)` plugin.\n\n### Router\n\nMethods\n\n- `use(app: App | Router): void`\n- `use(callBack: CallBackType): void`\n- `use(...callBack: CallBackType[]): void`\n- `use(url: string, callBack: CallBackType): void`\n- `use(url: string, ...callBack: CallBackType[]): void`\n- `use(url: string, app: App | Router): void`\n- `get...`\n- `post...`\n- `put...`\n- `patch...`\n- `delete...`\n- `error(app: App | Router): void;`\n- `error(callBack: ErrorCallBackType): void;`\n- `error(...callBack: ErrorCallBackType[]): void;`\n- `error(url: string, callBack: ErrorCallBackType): void;`\n- `error(url: string, ...callBack: ErrorCallBackType[]): void;`\n- `error(url: string, app: App | Router): void;`\n  \u003cbr /\u003e It'll handle the error If thrown from one of the above procedures.\n\nExample\n\n```ts\napp.use((req, res) =\u003e {\n  throw new Error(\"Something wrong!\");\n});\n\napp.error((error, req, res) =\u003e {\n  console.error(\"I handle the Error!\", error);\n  res.status(500).send(\"It's critical!\");\n});\n```\n\n- `param(paramName: string, ...callback: ParamCallBackType[]): void;`\n  \u003cbr\u003e Parameter handler [router.param](https://expressjs.com/en/api.html#router.param)\n\nExample\n\n```ts\nconst userController = new Router();\n\nuserController.param(\"username\", (req, res, username) =\u003e {\n  const user = await User.findOne({ username: username });\n  if (!user) {\n    throw new Error(\"user not found\");\n  }\n  req.profile = user;\n});\n\nuserController.get(\"/:username\", (req, res) =\u003e {\n  res.status(200).send({ profile: req.profile });\n});\n\nuserController.post(\"/:username/follow\", (req, res) =\u003e {\n  const user = await User.findById(req.payload.id);\n  if (user.following.indexOf(req.profile._id) === -1) {\n    user.following.push(req.profile._id);\n  }\n  const profile = await user.save();\n  return res.status(200).send({ profile: profile });\n});\n\nexport default userController;\n```\n\nThese are middleware methods and it's like express.js.\n\n### App\n\n_App extends Router_\nMethods\n\n- `This has all router's methods`\n\nProperties\n\n- `listen(options)`\n  \u003cbr/\u003e Start the Attain server.\n\n```ts\n  options: {\n    port: number;             // required\n    debug?: boolean;          // debug mode\n    hostname?: string;        // hostname default as 0.0.0.0\n    secure?: boolean;         // https use\n    certFile?: string;        // if secure is true, it's required\n    keyFile?: string;         // if secure is true, it's required\n  }\n```\n\n- `database(dbCls)` **NEW FEATURE!**\n  \u003cbr /\u003e Register a database to use in all of your middleware functions.\n  \u003cbr /\u003e Example:\n\n```ts \n/* ExampleDatabase.ts */\nclass ExampleDatabase extends AttainDatabase {\n    async connect() {\n        console.log('database connected');\n    }\n    async getAllUsers() {\n        return [{ name: 'Shaun' }, { name: 'Mike' }];\n    }\n}\n\n/* router.ts */\nconst router = new Router();\n\nrouter.get('/', async (req: Request, res: Response, db: ExampleDatabase) =\u003e {\n  const users = await db.getAllUsers();\n  res.status(200).send(users);\n})\n\n/* index.ts */\nconst app = new App();\n\nawait app.database(ExampleDatabase);\n\napp.use('/api/users', router);\n\n```\n\n**NOTE:** for this feature to work as expected, you must:\n- provide a `connect()` method to your database class\n- extend the `AttainDatabase` class\n\n\u003cbr /\u003e \n\u003cspan style=\"color: #555;\"\u003eThis feature is brand new and any contributins and ideas will be welcomed\u003c/span\u003e\n\n- `static startWith(connectFunc)`\nAutomatically initialize the app and connect to the database with a connect function.\n\n- `static startWith(dbClass)`\nAutomatically initialize the app create a database instance.\n\n## Nested Routing\n\n\u003e **Path** - router.ts\n\n**warn**: \u003cspan style=\"color: red;\"\u003easync await\u003c/span\u003e will block your procedures.\n\n```ts\nimport { Router } from \"https://deno.land/x/attain/mod.ts\";\n\nconst api = new Router();\n// or\n// const api = new App();\n\nconst sleep = (time: number) =\u003e {\n  new Promise((resolve) =\u003e setTimeout(() =\u003e resolve(), time));\n};\n\n// It will stop here for 1 second.\napi.get(\"/block\", async (req, res) =\u003e {\n  console.log(\"here '/block'\");\n  await sleep(1000);\n  res.status(200).send(`\n  \u003c!doctype html\u003e\n  \u003chtml lang=\"en\"\u003e\n    \u003cbody\u003e\n      \u003ch1\u003eHello\u003c/h1\u003e\n    \u003c/body\u003e\n  \u003c/html\u003e\n  `);\n});\n\n// It will not stop here\napi.get(\"/nonblock\", (req, res) =\u003e {\n  console.log(\"here '/nonblock'\");\n  sleep(1000).then((_) =\u003e {\n    res.status(200).send(`\n      \u003c!doctype html\u003e\n      \u003chtml lang=\"en\"\u003e\n        \u003cbody\u003e\n          \u003ch1\u003eHello\u003c/h1\u003e\n        \u003c/body\u003e\n      \u003c/html\u003e\n      `);\n  });\n});\n\nexport default api;\n```\n\n\u003e **Path** - main.ts\n\n```ts\nimport { App } from \"https://deno.land/x/attain/mod.ts\";\nimport api from \"./router.ts\";\n\nconst app = new App();\n\n// nested router applied\napp.use(\"/api\", api);\n\napp.use((req, res) =\u003e {\n  res.status(404).send(\"page not found\");\n});\n\napp.listen({ port: 3500 });\n```\n\n```\n# start with: deno run -A ./main.ts\n```\n\n## Extra plugins\n\n- **logger** : `Logging response \"response - method - status - path - time\"`\n- **parser** : `Parsing the request body and save it to request.params`\n- **security**: `Helping you make secure application by setting various HTTP headers` [Helmet](https://helmetjs.github.io/)\n\n### Security options\n\n| Options                                                 | Default? |\n| ------------------------------------------------------- | -------- |\n| `xss` (adds some small XSS protections)                 | yes      |\n| `removePoweredBy` (remove the X-Powered-By header)      | yes      |\n| `DNSPrefetchControl` (controls browser DNS prefetching) | yes      |\n| `noSniff` (to keep clients from sniffing the MIME type) | yes      |\n| `frameguard` (prevent clickjacking)                     | yes      |\n\n- **staticServe** : `It'll serve the static files from a provided path by joining the request path.`\n\n\u003e Out of box\n\n- [**Attain-GraphQL**](https://deno.land/x/attain_graphql#attain-graphql) : `GraphQL middleware`\n- [**deno_graphql**](https://deno.land/x/deno_graphql#setup-with-attain): `GraphQL middleware`\n- [**session**](https://deno.land/x/session): `cookie session`\n- [**cors**](https://deno.land/x/cors/#examples): `CORS`\n\n```ts\nimport {\n  App,\n  logger,\n  parser,\n  security,\n  staticServe,\n} from \"https://deno.land/x/Attain/mod.ts\";\n\nconst app = new App();\n\n// Set Extra Security setting\napp.use(security());\n\n// Logging response method status path time\napp.use(logger);\n\n// Parsing the request body and save it to request.params\n// Also, updated to parse the queries from search params\napp.use(parser);\n\n// Serve static files\n// This path must be started from your command line path.\napp.use(staticServe(\"./public\", { maxAge: 1000 }));\n\napp.use(\"/\", (req, res) =\u003e {\n  res.status(200).send(\"hello\");\n});\n\napp.use(\"/google\", (req, res) =\u003e {\n  res.redirect(\"https://www.google.ca\");\n});\n\napp.use(\"/:id\", (req, res) =\u003e {\n  // This data has parsed by the embedded URL parser.\n  console.log(req.params);\n  res.status(200).send(`id: ${req.params.id}`);\n});\n\napp.post(\"/submit\", (req, res) =\u003e {\n  // By the parser middleware, the body and search query get parsed and saved.\n  console.log(req.params);\n  console.log(req.query);\n  res.status(200).send({ data: \"has received\" });\n});\n\napp.listen({ port: 4000 });\n```\n\n## More Features\n\n### Switch your database with just one line of code\nUsing 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:\n\n```ts\nclass PostgresDatabase extends AttainDatabase {\n  #client: Client\n  async connect() {\n    const client = new Client({\n      user: Deno.env.get('USER'),\n      database: Deno.env.get('DB'),\n      hostname: Deno.env.get('HOST'),\n      password: Deno.env.get('PASSWORD')!,\n      port: parseInt(Deno.env.get('PORT')),\n    });\n    await client.connect();\n    this.#client = client;\n  }\n  async getAllProducts() {\n    const data = await this.#client.query('SELECT * FROM products');\n    /* map data */\n    return products;\n  }\n}\n\n/* OR */\nclass MongoDatabase extends AttainDatabase {\n  #Product: Collection\u003cProduct\u003e\n  async connect() {\n     const client = new MongoClient();\n     await client.connectWithUri(Deno.env.get('DB_URL'));\n     const database = client.database(Deno.env.get('DB_NAME'));\n     this.#Product = database.collection('Product');\n  }\n  async getAllProducts() {\n    return await this.#Product.findAll()\n  }\n}\n```\n\nThen pick one of the databases to use in your app:\n```ts\nawait app.database(MongoDatabase);\n/* OR */\nawait app.database(PostgresDatabase);\n/* OR */\nconst app = App.startWith(MongoDatabase);\n\napp.get('/products', (req, res, db) =\u003e {\n  const products = await db.getAllProducts();\n  res.status(200).send(products); /* will work the same! */\n})\n\n```\n\nYou can also provide a function that returns a database connection\n```ts\nimport { App, Router, Request, Response, AttainDatabase } from \"./mod.ts\";\nimport { MongoClient, Database } from \"https://deno.land/x/mongo@v0.9.2/mod.ts\";\n\nasync function DB() {\n  const client = new MongoClient()\n  await client.connectWithUri(\"mongodb://localhost:27017\")\n  const database = client.database(\"test\")\n  return database;\n}\n\n// allow auto inherit mode (auto inherit the types to the middleware)\n\nconst app = App.startWith(DB);\n// or\nconst app = new App\u003cDatabase\u003e()\napp.database(DB)\n\n// this db params will have automatically inherited types from the app\u003c\u003e or startWith method.\napp.use((req, res, db) =\u003e {\n\n})\n```\n\n---\n\nThere are several modules that are directly adapted from other modules.\nThey have preserved their individual licenses and copyrights. All of the modules,\nincluding those directly adapted are licensed under the MIT License.\n\nAll additional work is copyright 2021 the Attain authors. All rights reserved.\n","funding_links":[],"categories":["Modules","基础设施"],"sub_categories":["Assistants","Deno 源"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faaronwlee%2FAttain","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faaronwlee%2FAttain","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faaronwlee%2FAttain/lists"}