├── .eslintrc.json ├── .github ├── FUNDING.yaml ├── ISSUE_TEMPLATE │ ├── 2-bug-report.yml │ ├── 3-feature-request.yml │ └── config.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.ts ├── bun.lock ├── example ├── a.ts ├── async-recursive.ts ├── body.ts ├── cookie.ts ├── counter.ts ├── custom-response.ts ├── d.ts ├── derive.ts ├── error.ts ├── extension.ts ├── file.ts ├── guard.ts ├── headers.ts ├── hook.ts ├── html-import.ts ├── http.ts ├── index.html ├── lazy-module.ts ├── lazy │ └── index.ts ├── native.ts ├── nested-schema.ts ├── params.ts ├── proxy.ts ├── redirect.ts ├── rename.ts ├── response.ts ├── router.ts ├── schema.ts ├── simple.ts ├── sleep.ts ├── spawn.ts ├── store.ts ├── stress │ ├── a.ts │ ├── instance.ts │ ├── memoir.ts │ ├── multiple-routes.ts │ └── sucrose.ts ├── takodachi.png ├── teapot.webp ├── type-inference.ts ├── upload.ts ├── video.ts └── websocket.ts ├── package.json ├── src ├── adapter │ ├── bun │ │ ├── compose.ts │ │ ├── handler-native.ts │ │ ├── handler.ts │ │ └── index.ts │ ├── index.ts │ ├── types.ts │ ├── utils.ts │ └── web-standard │ │ ├── handler.ts │ │ └── index.ts ├── compose.ts ├── context.ts ├── cookies.ts ├── dynamic-handle.ts ├── error.ts ├── formats.ts ├── index.ts ├── manifest.ts ├── parse-query.ts ├── schema.ts ├── sucrose.ts ├── trace.ts ├── type-system │ ├── format.ts │ ├── index.ts │ ├── types.ts │ └── utils.ts ├── types.ts ├── universal │ ├── env.ts │ ├── file.ts │ ├── index.ts │ ├── request.ts │ ├── server.ts │ ├── types.ts │ └── utils.ts ├── utils.ts └── ws │ ├── bun.ts │ ├── index.ts │ └── types.ts ├── test ├── a.test.ts ├── adapter │ └── web-standard │ │ ├── cookie-to-header.test.ts │ │ ├── map-compact-response.test.ts │ │ ├── map-early-response.test.ts │ │ ├── map-response.test.ts │ │ ├── set-cookie.test.ts │ │ └── utils.ts ├── aot │ ├── analysis.test.ts │ ├── generation.test.ts │ ├── has-transform.test.ts │ ├── has-type.test.ts │ └── response.test.ts ├── bun │ └── router.test.ts ├── cookie │ ├── explicit.test.ts │ ├── implicit.test.ts │ ├── response.test.ts │ └── signature.test.ts ├── core │ ├── as.test.ts │ ├── compose.test.ts │ ├── config.test.ts │ ├── context.test.ts │ ├── dynamic.test.ts │ ├── elysia.test.ts │ ├── formdata.test.ts │ ├── handle-error.test.ts │ ├── modules.test.ts │ ├── mount.test.ts │ ├── native-static.test.ts │ ├── normalize.test.ts │ ├── path.test.ts │ ├── redirect.test.ts │ ├── sanitize.test.ts │ └── stop.test.ts ├── extends │ ├── decorators.test.ts │ ├── error.test.ts │ ├── models.test.ts │ └── store.test.ts ├── hoc │ └── index.test.ts ├── images │ ├── aris-yuzu.jpg │ ├── fake.jpg │ ├── kozeki-ui.webp │ ├── midori.png │ └── millenium.jpg ├── kyuukurarin.mp4 ├── lifecycle │ ├── after-handle.test.ts │ ├── before-handle.test.ts │ ├── derive.test.ts │ ├── error.test.ts │ ├── hook-types.test.ts │ ├── map-derive.test.ts │ ├── map-resolve.test.ts │ ├── map-response.test.ts │ ├── parser.test.ts │ ├── request.test.ts │ ├── resolve.test.ts │ ├── response.test.ts │ └── transform.test.ts ├── macro │ └── macro.test.ts ├── modules.ts ├── node │ ├── .gitignore │ ├── cjs │ │ ├── bun.lockb │ │ ├── index.js │ │ ├── package.json │ │ └── pnpm-lock.yaml │ └── esm │ │ ├── bun.lockb │ │ ├── index.js │ │ ├── package.json │ │ └── pnpm-lock.yaml ├── path │ ├── group.test.ts │ ├── guard.test.ts │ └── path.test.ts ├── plugins │ ├── affix.test.ts │ ├── checksum.test.ts │ ├── error-propagation.test.ts │ └── plugin.test.ts ├── production │ └── index.test.ts ├── response │ ├── custom-response.test.ts │ ├── headers.test.ts │ ├── redirect.test.ts │ ├── static.test.ts │ └── stream.test.ts ├── sucrose │ ├── bracket-pair-range-reverse.test.ts │ ├── bracket-pair-range.test.ts │ ├── extract-main-parameter.test.ts │ ├── find-alias.test.ts │ ├── infer-body-reference.test.ts │ ├── query.test.ts │ ├── remove-colon-alias.test.ts │ ├── remove-default-parameter.test.ts │ ├── retrieve-root-parameters.test.ts │ ├── separate-function.test.ts │ └── sucrose.test.ts ├── timeout.ts ├── tracer │ ├── aot.test.ts │ ├── detail.test.ts │ ├── timing.test.ts │ └── trace.test.ts ├── type-system │ ├── array-string.test.ts │ ├── boolean-string.test.ts │ ├── coercion-number.test.ts │ ├── date.test.ts │ ├── form.test.ts │ ├── import.ts │ ├── object-string.test.ts │ ├── string-format.test.ts │ └── union-enum.test.ts ├── types │ ├── async-modules.ts │ ├── index.ts │ ├── lifecycle │ │ ├── derive.ts │ │ └── resolve.ts │ ├── macro.ts │ ├── plugins.ts │ ├── schema-standalone.ts │ └── type-system.ts ├── units │ ├── class-to-object.test.ts │ ├── deduplicate-checksum.test.ts │ ├── has-ref.test.ts │ ├── has-transform.test.ts │ ├── merge-deep.test.ts │ ├── merge-object-schemas.test.ts │ ├── numeric.test.ts │ └── replace-schema-type.test.ts ├── utils.d.ts ├── utils.ts ├── validator │ ├── body.test.ts │ ├── encode.test.ts │ ├── exact-mirror.test.ts │ ├── header.test.ts │ ├── params.test.ts │ ├── query.test.ts │ ├── response.test.ts │ ├── standalone.test.ts │ └── validator.test.ts └── ws │ ├── connection.test.ts │ ├── destructuring.test.ts │ ├── message.test.ts │ └── utils.ts ├── tsconfig.dts.json ├── tsconfig.json └── tsconfig.test.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:sonarjs/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": "latest", 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["@typescript-eslint", "sonarjs"], 17 | "rules": { 18 | "@typescript-eslint/ban-types": "off", 19 | "@typescript-eslint/no-explicit-any": "off", 20 | "no-mixed-spaces-and-tabs": "off", 21 | "@typescript-eslint/no-non-null-assertion": "off", 22 | "@typescript-eslint/no-extra-semi": "off", 23 | "@typescript-eslint/ban-ts-comment": "off", 24 | "@typescript-eslint/no-namespace": "off", 25 | "no-case-declarations": "off", 26 | "no-extra-semi": "off", 27 | "sonarjs/cognitive-complexity": "off", 28 | "sonarjs/no-all-duplicated-branches": "off" 29 | }, 30 | "ignorePatterns": ["example/*", "test/**/*"] 31 | } 32 | -------------------------------------------------------------------------------- /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | github: SaltyAom -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: Report an issue that should be fixed 3 | labels: [bug] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for submitting a bug report. It helps make Elysia.JS better. 9 | 10 | If you need help or support using Elysia.JS, and are not reporting a bug, please 11 | head over to Q&A discussions [Discussions](https://github.com/elysiajs/elysia/discussions/categories/q-a), where you can ask questions in the Q&A forum. 12 | 13 | Make sure you are running the version of Elysia.JS and Bun.Sh 14 | The bug you are experiencing may already have been fixed. 15 | 16 | Please try to include as much information as possible. 17 | 18 | - type: input 19 | attributes: 20 | label: What version of Elysia is running? 21 | description: Copy the output of `Elysia --revision` 22 | - type: input 23 | attributes: 24 | label: What platform is your computer? 25 | description: | 26 | For MacOS and Linux: copy the output of `uname -mprs` 27 | For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console 28 | - type: textarea 29 | attributes: 30 | label: What steps can reproduce the bug? 31 | description: Explain the bug and provide a code snippet that can reproduce it. 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: What is the expected behavior? 37 | description: If possible, please provide text instead of a screenshot. 38 | - type: textarea 39 | attributes: 40 | label: What do you see instead? 41 | description: If possible, please provide text instead of a screenshot. 42 | - type: textarea 43 | attributes: 44 | label: Additional information 45 | description: Is there anything else you think we should know? 46 | - type: input 47 | attributes: 48 | label: Have you try removing the `node_modules` and `bun.lockb` and try again yet? 49 | description: rm -rf node_modules && bun.lockb 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-feature-request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature Request 2 | description: Suggest an idea, feature, or enhancement 3 | labels: [enhancement] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for submitting an idea. It helps make Elysia.JS better. 9 | 10 | If you want to discuss Elysia.JS, or learn how others are using Elysia.JS, please 11 | head to our [Discord](https://discord.com/invite/y7kH46ZE) server, where you can chat among the community. 12 | - type: textarea 13 | attributes: 14 | label: What is the problem this feature would solve? 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: What is the feature you are proposing to solve the problem? 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: What alternatives have you considered? 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: 📗 Documentation Issue 4 | url: https://github.com/elysiajs/documentation/issues/new/choose 5 | about: Head over to our Documentation repository! 6 | - name: 💬 Ask a Question 7 | url: https://discord.gg/eaFJ2KDJck 8 | about: Head over to our Discord! 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: './' 5 | schedule: 6 | interval: 'daily' 7 | 8 | - package-ecosystem: 'github-actions' 9 | directory: './' 10 | schedule: 11 | interval: 'daily' 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | name: Build and test code 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup bun 17 | uses: oven-sh/setup-bun@v1 18 | with: 19 | bun-version: latest 20 | 21 | - name: Install packages 22 | run: bun install 23 | 24 | - name: Build code 25 | run: bun run build 26 | 27 | - name: Test 28 | run: bun run test 29 | 30 | - name: Publish Preview 31 | if: github.event_name == 'pull_request' 32 | run: bunx pkg-pr-new publish 33 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | defaults: 8 | run: 9 | shell: bash 10 | 11 | permissions: 12 | id-token: write 13 | 14 | env: 15 | # Enable debug logging for actions 16 | ACTIONS_RUNNER_DEBUG: true 17 | 18 | jobs: 19 | publish-npm: 20 | name: 'Publish: npm Registry' 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: 'Checkout' 24 | uses: actions/checkout@v4 25 | 26 | - name: 'Setup Bun' 27 | uses: oven-sh/setup-bun@v1 28 | with: 29 | bun-version: latest 30 | registry-url: "https://registry.npmjs.org" 31 | 32 | - uses: actions/setup-node@v4 33 | with: 34 | node-version: '20.x' 35 | registry-url: 'https://registry.npmjs.org' 36 | 37 | - name: Install packages 38 | run: bun install 39 | 40 | - name: Build code 41 | run: bun run build 42 | 43 | - name: Test 44 | run: bun run test 45 | 46 | - name: 'Publish' 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | run: | 50 | npm publish --provenance --access=public 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules 4 | build 5 | dump.rdb 6 | dist 7 | trace 8 | .scannerwork 9 | *.tsbuildinfo 10 | .wrangler 11 | .elysia 12 | heap.json 13 | server 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .github 4 | .prettierrc 5 | .eslintrc.js 6 | .eslint.json 7 | .swc.cjs.swcrc 8 | .swc.esm.swcrc 9 | .swcrc 10 | .husky 11 | .eslintrc.json 12 | 13 | bun.lock 14 | bun.lockb 15 | node_modules 16 | tsconfig.json 17 | CHANGELOG.md 18 | heap.json 19 | 20 | example 21 | tests 22 | test 23 | docs 24 | src 25 | .DS_Store 26 | test 27 | tsconfig.cjs.json 28 | tsconfig.dts.json 29 | tsconfig.esm.json 30 | tsconfig.test.json 31 | tsconfig.test.tsbuildinfo 32 | 33 | CONTRIBUTING.md 34 | CODE_OF_CONDUCT.md 35 | trace 36 | 37 | build.ts 38 | .scannerwork 39 | src 40 | server 41 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | registry=https://registry.npmjs.org 3 | always-auth=true 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 4, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "none" 7 | } 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome to Elysia.js contributing guide 2 | 3 | Thank you for investing your time in contributing to Elysia.js! Any contribution you make will be amazing :sparkles:. 4 | 5 | Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. 6 | 7 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 8 | 9 | ## Setup Local Development Environment 10 | 11 | The Elysia.js repo is using [bun](https://bun.sh). Make sure you have the [latest version of bun](https://github.com/oven-sh/bun/releases) installed in your system. To run Elysia.js locally: 12 | 13 | 1. Clone this repository 14 | 15 | 2. In the root of this project, run `bun install` to install all of the necessary dependencies 16 | 17 | 3. To run the development version, run `bun run dev` 18 | 19 | ### Unit Testing 20 | 21 | In Elysia.js, all of the test files are located inside the [`test/`](test/) directory. Unit testing are powered by [bun's test](https://github.com/oven-sh/bun/tree/main/packages/bun-internal-test). 22 | 23 | - `bun test` to run all the test inside the [`test/`](test/) directory 24 | 25 | - `bun test test/.ts` to run a specific test 26 | 27 | ## Pull Request Guidelines 28 | 29 | - Checkout a topic branch from a base branch (e.g. `main`), and merge back against that branch. 30 | 31 | - If adding a new feature: 32 | 33 | - Add accompanying test case if possible. 34 | 35 | - Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first, and have it approved before working on it. 36 | 37 | - If fixing a bug: 38 | 39 | - If you are resolving a special issue, please add the issues number in the PR's description. 40 | 41 | - Provide a detailed description of the bug in the PR. Live demo preferred. 42 | 43 | - Add appropriate test coverage if applicable. 44 | 45 | - It's OK to have multiple small commits as you work on the PR. GitHub can automatically squash them before merging. 46 | 47 | ## Thanks :purple_heart: 48 | 49 | Thanks for all your contributions and efforts towards improving Elysia.js. We thank you for being part of our :sparkles: community :sparkles:! 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 saltyAom 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | ElysiaJS logo with word ElysiaJS on the left 4 |

5 | 6 |

Ergonomic Framework for Humans

7 | 8 |

9 | Documentation | Discord | Sponsors 10 |

11 | 12 | TypeScript framework supercharged by Bun with End-to-End Type Safety, unified type system, and outstanding developer experience 13 | 14 | ```bash 15 | bun create elysia app 16 | ``` 17 | 18 | ![Elysia feature sheet including 18x faster than Express based on Techempower benchmark, Frontend RPC Connector, Advance TypeScript type, unified type single source of truth of type TypeScript runtime and documentation all at once, Made of Productivity focus on developer experience, powered by Bun, WinterCG Compliance, Fully type safe GraphQL (same author with GraphQL Mobius), documentation in one line, End-to-end type safety move fast and break nothing like tRPC, strong ecosystem most popular Bun native Web Framework](https://github.com/elysiajs/elysia/assets/35027979/d4b184ca-a622-434d-bb06-06c3110726af) 19 | 20 | ## Documentation 21 | The documentation is available on [elysiajs.com](https://elysiajs.com). 22 | 23 | ## Contributing 24 | See [Contributing Guide](CONTRIBUTING.md) and please follow our [Code of Conduct](CODE_OF_CONDUCT.md). 25 | 26 | ## Discord 27 | Come join the [Discord community channel~](https://discord.gg/eaFJ2KDJck) 28 | -------------------------------------------------------------------------------- /build.ts: -------------------------------------------------------------------------------- 1 | import { $ } from 'bun' 2 | import { build, type Options } from 'tsup' 3 | 4 | const external = ['@sinclair/typebox', 'file-type'] 5 | 6 | const tsupConfig: Options = { 7 | entry: ['src/**/*.ts'], 8 | splitting: false, 9 | sourcemap: false, 10 | clean: true, 11 | bundle: true, 12 | minifySyntax: true, 13 | minifyWhitespace: false, 14 | minifyIdentifiers: false, 15 | target: 'node20', 16 | external 17 | // outExtension() { 18 | // return { 19 | // js: '.js' 20 | // } 21 | // } 22 | } satisfies Options 23 | 24 | await Promise.all([ 25 | // ? tsup esm 26 | build({ 27 | outDir: 'dist', 28 | format: 'esm', 29 | cjsInterop: false, 30 | ...tsupConfig 31 | }), 32 | // ? tsup cjs 33 | build({ 34 | outDir: 'dist/cjs', 35 | format: 'cjs', 36 | // dts: true, 37 | ...tsupConfig 38 | }) 39 | ]) 40 | 41 | // ? Fix mjs import 42 | const glob = new Bun.Glob('./dist/**/*.mjs') 43 | 44 | for await (const entry of glob.scan('.')) { 45 | const content = await Bun.file(entry).text() 46 | 47 | await Bun.write( 48 | entry, 49 | content 50 | .replace( 51 | // Named import 52 | /(import|export)\s*\{([a-zA-Z0-9_,\s$]*)\}\s*from\s*['"]([a-zA-Z0-9./-]*[./][a-zA-Z0-9./-]*)['"]/g, 53 | '$1{$2}from"$3.mjs"' 54 | ) 55 | .replace( 56 | // Default import 57 | /(import|export) ([a-zA-Z0-9_$]+) from\s*['"]([a-zA-Z0-9./-]*[./][a-zA-Z0-9./-]*)['"]/g, 58 | '$1 $2 from"$3.mjs"' 59 | ) 60 | ) 61 | 62 | // await fs.writeFile( 63 | // entry, 64 | // (await fs.readFile(entry)) 65 | // .toString() 66 | // .replaceAll(/require\("(.+)\.js"\);/g, 'require("$1.cjs");'), 67 | // ); 68 | } 69 | 70 | await $`tsc --project tsconfig.dts.json` 71 | 72 | await Bun.build({ 73 | entrypoints: ['./src/index.ts'], 74 | outdir: './dist/bun', 75 | minify: { 76 | whitespace: true, 77 | syntax: true, 78 | identifiers: false 79 | }, 80 | target: 'bun', 81 | sourcemap: 'linked', 82 | external 83 | }) 84 | 85 | await Promise.all([ 86 | $`cp dist/*.d.ts dist/cjs`, 87 | $`cp dist/ws/*.d.ts dist/cjs/ws/` 88 | ]) 89 | 90 | await $`cp dist/index*.d.ts dist/bun` 91 | 92 | // const fsMjs = Bun.file('dist/universal/fs.mjs') 93 | // const fsMjsContent = await fsMjs.text() 94 | // Bun.write(fsMjs, fsMjsContent.replace(`require("fs")`, `await import("fs")`)) 95 | 96 | process.exit() 97 | -------------------------------------------------------------------------------- /example/a.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, sse, t } from '../src' 2 | 3 | const a = t.Object({ 4 | message: t.String(), 5 | image: t.Optional(t.Files()) 6 | }) 7 | 8 | new Elysia() 9 | .model({ 10 | a 11 | }) 12 | .post('/', ({ body }) => 'ok', { 13 | body: 'a' 14 | }) 15 | .listen(3000) 16 | -------------------------------------------------------------------------------- /example/async-recursive.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | import { req } from '../test/utils' 3 | 4 | const delay = any>( 5 | callback: T, 6 | ms = 617 7 | ): Promise> => Bun.sleep(ms).then(callback) 8 | 9 | const yay = () => delay(() => new Elysia().use(import('./lazy'))) 10 | const yay2 = () => delay(() => new Elysia().use(yay), 5) 11 | const yay3 = () => delay(() => new Elysia().use(yay2), 10) 12 | const wrapper = new Elysia().use(async () => delay(() => yay3(), 6)) 13 | 14 | const app = new Elysia().use(wrapper) 15 | 16 | await app.modules 17 | 18 | // should works 19 | app.handle(req('/lazy')) 20 | .then((x) => x.text()) 21 | .then(console.log) 22 | -------------------------------------------------------------------------------- /example/body.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../src' 2 | 3 | const app = new Elysia() 4 | // Add custom body parser 5 | .onParse(async ({ request, contentType }) => { 6 | switch (contentType) { 7 | case 'application/Elysia': 8 | return request.text() 9 | } 10 | }) 11 | .post('/', ({ body: { username } }) => `Hi ${username}`, { 12 | body: t.Object({ 13 | id: t.Number(), 14 | username: t.String() 15 | }) 16 | }) 17 | // Increase id by 1 from body before main handler 18 | .post('/transform', ({ body }) => body, { 19 | transform: ({ body }) => { 20 | body.id = body.id + 1 21 | }, 22 | body: t.Object({ 23 | id: t.Number(), 24 | username: t.String() 25 | }), 26 | detail: { 27 | summary: 'A' 28 | } 29 | }) 30 | .post('/mirror', ({ body }) => body) 31 | .listen(3000) 32 | 33 | console.log('🦊 Elysia is running at :8080') 34 | -------------------------------------------------------------------------------- /example/cookie.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../src' 2 | 3 | const app = new Elysia({ 4 | cookie: { 5 | secrets: 'Fischl von Luftschloss Narfidort', 6 | sign: ['name'] 7 | } 8 | }) 9 | .get( 10 | '/council', 11 | ({ cookie: { council } }) => 12 | (council.value = [ 13 | { 14 | name: 'Rin', 15 | affilation: 'Administration' 16 | } 17 | ]), 18 | { 19 | cookie: t.Cookie({ 20 | council: t.Array( 21 | t.Object({ 22 | name: t.String(), 23 | affilation: t.String() 24 | }) 25 | ) 26 | }) 27 | } 28 | ) 29 | .get('/create', ({ cookie: { name } }) => (name.value = 'Himari')) 30 | .get( 31 | '/update', 32 | ({ cookie: { name } }) => { 33 | name.value = 'seminar: Rio' 34 | name.value = 'seminar: Himari' 35 | name.maxAge = 86400 36 | 37 | return name.value 38 | }, 39 | { 40 | cookie: t.Cookie({ 41 | name: t.Optional(t.String()) 42 | }) 43 | } 44 | ) 45 | .listen(3000) 46 | -------------------------------------------------------------------------------- /example/counter.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | new Elysia() 4 | .state('counter', 0) 5 | .get('/', ({ store }) => store.counter++) 6 | .listen(3000) 7 | -------------------------------------------------------------------------------- /example/custom-response.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | const prettyJson = new Elysia() 4 | .mapResponse(({ response }) => { 5 | if (response instanceof Object) 6 | return new Response(JSON.stringify(response, null, 4)) 7 | }) 8 | .as('plugin') 9 | 10 | new Elysia() 11 | .use(prettyJson) 12 | .get('/', () => ({ 13 | hello: 'world' 14 | })) 15 | .listen(3000) 16 | -------------------------------------------------------------------------------- /example/d.ts: -------------------------------------------------------------------------------- 1 | interface Node[] = []> { 2 | value: T 3 | neighbors: Nodes 4 | } 5 | 6 | type A = Node<1, [B, C]> // Node<1, [Node<2>, Node<3>]> 7 | type B = Node<2, [A, D]> // Node<2, [Node<1>, Node<3>]> 8 | type C = Node<3, [A, B]> // Node<3, [Node<1>, Node<2>]> 9 | type D = Node<4, [B, C]> // Node<4, [Node<2>, Node<3>]> 10 | 11 | type FilterOut = 12 | Arr extends [ 13 | infer Head, 14 | ...infer Tail 15 | ] 16 | ? Head extends Target 17 | ? FilterOut 18 | : [Head, ...FilterOut] 19 | : [] 20 | 21 | type BFS< 22 | Root extends Node, 23 | ToFind, 24 | Searched extends Node[] = [] 25 | > = Root extends (Searched['length'] extends 0 ? never : Searched[number]) 26 | ? FilterOut extends [ 27 | infer Current extends Node 28 | ] 29 | ? BFS 30 | : never 31 | : Root['value'] extends ToFind 32 | ? Root & { __order: Searched } 33 | : Root['neighbors'] extends [ 34 | infer Current extends Node, 35 | ...infer Rest extends Node[] 36 | ] 37 | ? Current['value'] extends ToFind 38 | ? Current 39 | : Rest extends [infer Next extends Node] 40 | ? BFS 41 | : never 42 | : never 43 | 44 | 45 | type Result = BFS // Node<4, [Node<2>, Node<3>]> 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | function breadthFirstSearch( 57 | root: Node[]>, 58 | toFind: T, 59 | searched: Node[] = [] 60 | ): Node | null { 61 | if (searched.includes(root)) return null 62 | if (root.value === toFind) return root as Node 63 | 64 | for (const current of root.neighbors) { 65 | if (current.value === toFind) return current 66 | 67 | const a = breadthFirstSearch(current, toFind, [...searched, root]) 68 | } 69 | 70 | return null 71 | } 72 | -------------------------------------------------------------------------------- /example/derive.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | new Elysia() 4 | .state('counter', 0) 5 | .derive(({ store }) => ({ 6 | increase() { 7 | store.counter++ 8 | } 9 | })) 10 | .derive(({ store }) => ({ 11 | store: { 12 | doubled: store.counter * 2, 13 | tripled: store.counter * 3 14 | } 15 | })) 16 | .get('/', ({ increase, store }) => { 17 | increase() 18 | const { counter, doubled, tripled } = store 19 | 20 | return { 21 | counter, 22 | doubled, 23 | tripled 24 | } 25 | }) 26 | .listen(3000, ({ hostname, port }) => { 27 | console.log(`🦊 running at http://${hostname}:${port}`) 28 | }) 29 | -------------------------------------------------------------------------------- /example/error.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../src' 2 | 3 | new Elysia() 4 | .post('/', ({ body }) => body, { 5 | body: t.Object({ 6 | username: t.String(), 7 | password: t.String(), 8 | nested: t.Optional( 9 | t.Object({ 10 | hi: t.String() 11 | }) 12 | ) 13 | }), 14 | error({ error }) { 15 | console.log(error) 16 | } 17 | }) 18 | .listen(3000) 19 | -------------------------------------------------------------------------------- /example/file.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, file } from '../src' 2 | import Page from './index.html' 3 | 4 | /** 5 | * Example of handle single static file 6 | * 7 | * @see https://github.com/elysiajs/elysia-static 8 | */ 9 | new Elysia() 10 | .headers({ 11 | server: 'Elysia' 12 | }) 13 | .onRequest(({ request }) => { 14 | console.log(request.method, request.url) 15 | }) 16 | .get('/', Page) 17 | .get('/tako', file('./example/takodachi.png')) 18 | .get('/mika.mp4', Bun.file('test/kyuukurarin.mp4')) 19 | .listen(3000) 20 | -------------------------------------------------------------------------------- /example/guard.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../src' 2 | 3 | new Elysia() 4 | .state('name', 'salt') 5 | .get('/', ({ store: { name } }) => `Hi ${name}`, { 6 | query: t.Object({ 7 | name: t.String() 8 | }) 9 | }) 10 | // If query 'name' is not preset, skip the whole handler 11 | .guard( 12 | { 13 | query: t.Object({ 14 | name: t.String() 15 | }) 16 | }, 17 | (app) => 18 | app 19 | // Query type is inherited from guard 20 | .get('/profile', ({ query }) => `Hi`) 21 | // Store is inherited 22 | .post('/name', ({ store: { name }, body, query }) => name, { 23 | body: t.Object({ 24 | id: t.Number({ 25 | minimum: 5 26 | }), 27 | username: t.String(), 28 | profile: t.Object({ 29 | name: t.String() 30 | }) 31 | }) 32 | }) 33 | ) 34 | .listen(3000) 35 | -------------------------------------------------------------------------------- /example/headers.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | import cookie from '../src/index' 4 | 5 | new Elysia() 6 | .get('/', ({ set }) => { 7 | set.headers['x-powered-by'] = 'Elysia' 8 | set.status = 'Bad Request' 9 | }) 10 | .listen(3000) 11 | -------------------------------------------------------------------------------- /example/hook.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | new Elysia() 4 | // Create global mutable state 5 | .state('counter', 0) 6 | // Increase counter by 1 on every request on any handler 7 | .onTransform(({ store }) => { 8 | store.counter++ 9 | }) 10 | .get('/', ({ store: { counter } }) => counter, { 11 | // Increase counter only when this handler is called 12 | transform: [ 13 | ({ store }) => { 14 | store.counter++ 15 | }, 16 | ({ store }) => { 17 | store.counter++ 18 | } 19 | ] 20 | }) 21 | .listen(3000) 22 | -------------------------------------------------------------------------------- /example/html-import.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../src' 2 | import Page from './index.html' 3 | 4 | new Elysia() 5 | .get('/', Page) 6 | .get('/mika.mp4', Bun.file('test/kyuukurarin.mp4')) 7 | .listen(3000) 8 | -------------------------------------------------------------------------------- /example/http.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../src' 2 | 3 | const t1 = performance.now() 4 | 5 | const loggerPlugin = new Elysia() 6 | .get('/hi', () => 'Hi') 7 | .decorate('log', () => 'A') 8 | .decorate('date', () => new Date()) 9 | .state('fromPlugin', 'From Logger') 10 | .use((app) => app.state('abc', 'abc')) 11 | 12 | const app = new Elysia() 13 | .onRequest(({ set }) => { 14 | set.headers = { 15 | 'Access-Control-Allow-Origin': '*' 16 | } 17 | }) 18 | .use(loggerPlugin) 19 | .state('build', Date.now()) 20 | .get('/', () => 'Elysia') 21 | .get('/tako', () => Bun.file('./example/takodachi.png')) 22 | .get('/json', () => ({ 23 | hi: 'world' 24 | })) 25 | .get('/root/plugin/log', ({ log, store: { build } }) => { 26 | log() 27 | 28 | return build 29 | }) 30 | .get('/wildcard/*', () => 'Hi Wildcard') 31 | .get('/query', () => 'Elysia', { 32 | beforeHandle: ({ query }) => { 33 | console.log('Name:', query?.name) 34 | 35 | if (query?.name === 'aom') return 'Hi saltyaom' 36 | }, 37 | query: t.Object({ 38 | name: t.String() 39 | }) 40 | }) 41 | .post('/json', async ({ body }) => body, { 42 | body: t.Object({ 43 | name: t.String(), 44 | additional: t.String() 45 | }) 46 | }) 47 | .post('/transform-body', async ({ body }) => body, { 48 | beforeHandle: (ctx) => { 49 | ctx.body = { 50 | ...ctx.body, 51 | additional: 'Elysia' 52 | } 53 | }, 54 | body: t.Object({ 55 | name: t.String(), 56 | additional: t.String() 57 | }) 58 | }) 59 | .get('/id/:id', ({ params: { id } }) => id, { 60 | transform({ params }) { 61 | params.id = +params.id 62 | }, 63 | params: t.Object({ 64 | id: t.Number() 65 | }) 66 | }) 67 | .post('/new/:id', async ({ body, params }) => body, { 68 | params: t.Object({ 69 | id: t.Number() 70 | }), 71 | body: t.Object({ 72 | username: t.String() 73 | }) 74 | }) 75 | .get('/trailing-slash', () => 'A') 76 | .group('/group', (app) => 77 | app 78 | .onBeforeHandle<{ 79 | query: { 80 | name: string 81 | } 82 | }>(({ query }) => { 83 | if (query?.name === 'aom') return 'Hi saltyaom' 84 | }) 85 | .get('/', () => 'From Group') 86 | .get('/hi', () => 'HI GROUP') 87 | .get('/elysia', () => 'Welcome to Elysian Realm') 88 | .get('/fbk', () => 'FuBuKing') 89 | ) 90 | .get('/response-header', ({ set }) => { 91 | set.status = 404 92 | set.headers['a'] = 'b' 93 | 94 | return 'A' 95 | }) 96 | .get('/this/is/my/deep/nested/root', () => 'Hi') 97 | .get('/build', ({ store: { build } }) => build) 98 | .get('/ref', ({ date }) => date()) 99 | .get('/response', () => new Response('Hi')) 100 | .get('/error', () => new Error('Something went wrong')) 101 | .get('/401', ({ set }) => { 102 | set.status = 401 103 | 104 | return 'Status should be 401' 105 | }) 106 | .get('/timeout', async () => { 107 | await new Promise((resolve) => setTimeout(resolve, 2000)) 108 | 109 | return 'A' 110 | }) 111 | .all('/all', () => 'hi') 112 | .onError(({ code, error, set }) => { 113 | if (code === 'NOT_FOUND') { 114 | set.status = 404 115 | 116 | return 'Not Found :(' 117 | } 118 | }) 119 | 120 | const t2 = performance.now() 121 | 122 | app.listen(8080, ({ hostname, port }) => { 123 | console.log(`🦊 Elysia is running at http://${hostname}:${port}`) 124 | }) 125 | 126 | console.log('took', t2 - t1, 'ms') 127 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bun HTML Import 5 | 6 | 7 |

Hi

8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/lazy-module.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | const plugin = (app: Elysia) => app.get('/plugin', () => 'Plugin') 4 | const asyncPlugin = async (app: Elysia) => app.get('/async', () => 'A') 5 | 6 | const app = new Elysia() 7 | .decorate('a', () => 'hello') 8 | .use(plugin) 9 | .use(import('./lazy')) 10 | .use((app) => app.get('/inline', ({ store: { a } }) => 'inline')) 11 | .get('/', ({ a }) => a()) 12 | .listen(3000) 13 | 14 | await app.modules 15 | 16 | console.log( 17 | `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` 18 | ) 19 | -------------------------------------------------------------------------------- /example/lazy/index.ts: -------------------------------------------------------------------------------- 1 | import Elysia from '../../src' 2 | 3 | export const lazy = (app: Elysia) => 4 | app.state('a', 'b').get('/lazy', 'Hi from lazy loaded module') 5 | 6 | export default lazy 7 | -------------------------------------------------------------------------------- /example/native.ts: -------------------------------------------------------------------------------- 1 | Bun.serve({ 2 | port: 3000, 3 | fetch: (request) => { 4 | throw new Error('A') 5 | }, 6 | error(request) { 7 | return new Response('error') 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /example/nested-schema.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../src' 2 | 3 | new Elysia() 4 | .guard( 5 | { 6 | query: t.Object({ 7 | name: t.String() 8 | }) 9 | }, 10 | (app) => 11 | app 12 | .get('/', ({ query }) => 'A', { 13 | beforeHandle: ({ query }) => {}, 14 | query: t.Object({ 15 | a: t.String() 16 | }) 17 | }) 18 | .guard( 19 | { 20 | headers: t.Object({ 21 | a: t.String() 22 | }) 23 | }, 24 | (app) => 25 | app.get('/a', () => 'A', { 26 | beforeHandle: ({ query }) => {}, 27 | body: t.Object({ 28 | username: t.String() 29 | }) 30 | }) 31 | ) 32 | ) 33 | .get('*', () => 'Star now work') 34 | .listen(3000) 35 | -------------------------------------------------------------------------------- /example/params.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | const app = new Elysia() 4 | .get('/', () => 'Elysia') 5 | // Retrieve params, automatically typed 6 | .get('/id/:id', ({ params }) => params.id) 7 | .listen(3000) 8 | 9 | console.log('Listen') 10 | -------------------------------------------------------------------------------- /example/proxy.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | new Elysia() 4 | .all('/*', ({ request, params, query }) => 5 | fetch({ 6 | ...request, 7 | url: `https://macosplay.com/${params['*']}?${new URLSearchParams(query)}` 8 | }) 9 | ) 10 | .listen(3000) 11 | -------------------------------------------------------------------------------- /example/redirect.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | new Elysia() 4 | .get('/', () => 'Hi') 5 | .get('/redirect', ({ redirect }) => redirect('/')) 6 | .listen(3000) 7 | -------------------------------------------------------------------------------- /example/rename.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../src' 2 | 3 | // ? Elysia#83 | Proposal: Standardized way of renaming third party plugin-scoped stuff 4 | // this would be a plugin provided by a third party 5 | const myPlugin = new Elysia() 6 | .decorate('myProperty', 42) 7 | .model('salt', t.String()) 8 | 9 | new Elysia() 10 | .use( 11 | myPlugin 12 | // map decorator, rename "myProperty" to "renamedProperty" 13 | .decorate(({ myProperty, ...decorators }) => ({ 14 | renamedProperty: myProperty, 15 | ...decorators 16 | })) 17 | // map model, rename "salt" to "pepper" 18 | .model(({ salt, ...models }) => ({ 19 | ...models, 20 | pepper: t.String() 21 | })) 22 | // Add prefix 23 | .prefix('decorator', 'unstable') 24 | ) 25 | .get( 26 | '/mapped', 27 | ({ unstableRenamedProperty }) => unstableRenamedProperty 28 | ) 29 | .post('/pepper', ({ body }) => body, { 30 | body: 'pepper', 31 | // response: t.String() 32 | }) 33 | -------------------------------------------------------------------------------- /example/response.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | new Elysia() 4 | .get('/', ({ set }) => { 5 | set.headers['X-POWERED-BY'] = 'Elysia' 6 | 7 | // Return custom response 8 | return new Response('Shuba Shuba', { 9 | headers: { 10 | duck: 'shuba duck' 11 | }, 12 | status: 418 13 | }) 14 | }) 15 | .listen(3000) 16 | -------------------------------------------------------------------------------- /example/router.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | const prefix = (prefix: Prefix) => 4 | new Elysia({ prefix }) 5 | .state('b', 'b') 6 | .get('/2', () => 2) 7 | .guard({}, (app) => app.get('/do', () => 'something')) 8 | .group('/v2', (app) => app.guard({}, (app) => app.get('/ok', () => 1))) 9 | 10 | const a = new Elysia() 11 | .get('/a', () => 'A') 12 | .guard({}, (app) => app.get('/guard', () => 'a')) 13 | .use(prefix('prefixed')) 14 | .listen(3000) 15 | -------------------------------------------------------------------------------- /example/schema.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../src' 2 | 3 | const app = new Elysia() 4 | .model({ 5 | name: t.Object({ 6 | name: t.String() 7 | }), 8 | b: t.Object({ 9 | response: t.Number() 10 | }), 11 | authorization: t.Object({ 12 | authorization: t.String() 13 | }) 14 | }) 15 | // Strictly validate response 16 | .get('/', () => 'hi') 17 | // Strictly validate body and response 18 | .post('/', ({ body, query }) => body.id, { 19 | body: t.Object({ 20 | id: t.Number(), 21 | username: t.String(), 22 | profile: t.Object({ 23 | name: t.String() 24 | }) 25 | }) 26 | }) 27 | // Strictly validate query, params, and body 28 | .get('/query/:id', ({ query: { name }, params }) => name, { 29 | query: t.Object({ 30 | name: t.String() 31 | }), 32 | params: t.Object({ 33 | id: t.String() 34 | }), 35 | response: { 36 | 200: t.String(), 37 | 300: t.Object({ 38 | error: t.String() 39 | }) 40 | } 41 | }) 42 | .guard( 43 | { 44 | headers: 'authorization' 45 | }, 46 | (app) => 47 | app 48 | .derive(({ headers }) => ({ 49 | userId: headers.authorization 50 | })) 51 | .get('/', ({ userId }) => 'A') 52 | .post('/id/:id', ({ query, body, params, userId }) => body, { 53 | params: t.Object({ 54 | id: t.Number() 55 | }), 56 | transform({ params }) { 57 | params.id = +params.id 58 | } 59 | }) 60 | ) 61 | .listen(3000) 62 | -------------------------------------------------------------------------------- /example/simple.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | // Simple Hello World 4 | const t1 = performance.now() 5 | new Elysia() 6 | .get('/', () => 'Hi') 7 | .listen(3000) 8 | 9 | console.log(performance.now() - t1) 10 | 11 | // Bun.serve({ 12 | // port: 8080, 13 | // fetch() { 14 | // return new Response('Hi') 15 | // } 16 | // }) 17 | 18 | -------------------------------------------------------------------------------- /example/sleep.ts: -------------------------------------------------------------------------------- 1 | const sleep = (time: number) => new Promise(resolve => setTimeout(resolve, time)) 2 | 3 | Bun.serve({ 4 | port: 3000, 5 | fetch: async () => { 6 | await sleep(1000) 7 | 8 | return new Response('Hi') 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /example/spawn.ts: -------------------------------------------------------------------------------- 1 | import { $ } from 'bun' 2 | import { cpus } from 'os' 3 | 4 | const total = cpus().length - 1 5 | const ops = [] 6 | 7 | for (let i = 0; i < total; i++) 8 | ops.push($`NODE_ENV=production bun example/a.ts`) 9 | 10 | await Promise.all(ops) 11 | -------------------------------------------------------------------------------- /example/store.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | new Elysia() 4 | // Create globally mutable store 5 | .state('name', 'Fubuki') 6 | .get('/id/:id', ({ params: { id }, store: { name } }) => `${id} ${name}`) 7 | -------------------------------------------------------------------------------- /example/stress/a.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../../src' 2 | 3 | const memory = process.memoryUsage().heapTotal / 1024 / 1024 4 | 5 | const total = 500 6 | const sub = 1 7 | 8 | const app = new Elysia() 9 | const plugin = new Elysia() 10 | 11 | const t1 = performance.now() 12 | 13 | for (let i = 0; i < total * sub; i++) 14 | plugin.get(`/${i}`, () => new Response('hi'), { response: t.String() }) 15 | 16 | app.use(plugin) 17 | 18 | const t2 = performance.now() 19 | 20 | Bun.gc(true) 21 | const memoryAfter = process.memoryUsage().heapTotal / 1024 / 1024 22 | const totalRoutes = total * sub 23 | const totalTime = t2 - t1 24 | const avgTimePerRoute = totalTime / totalRoutes 25 | 26 | console.log(`${totalRoutes} routes took ${totalTime.toFixed(4)} ms`) 27 | console.log(`Average ${avgTimePerRoute.toFixed(4)} ms per route`) 28 | console.log(`${(memoryAfter - memory).toFixed(2)} MB memory used`) 29 | -------------------------------------------------------------------------------- /example/stress/instance.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../../src' 2 | 3 | const total = 100 4 | const sub = 5 5 | 6 | const app = new Elysia({ precompile: true }) 7 | 8 | const memory = process.memoryUsage().heapTotal / 1024 / 1024 9 | const t1 = performance.now() 10 | 11 | for (let i = 0; i < total; i++) { 12 | const plugin = new Elysia() 13 | 14 | for (let j = 0; j < sub; j++) plugin.get(`/${i * sub + j}`, () => 'hi') 15 | 16 | app.use(plugin) 17 | } 18 | 19 | const t2 = performance.now() 20 | 21 | Bun.gc(true) 22 | 23 | const memoryAfter = process.memoryUsage().heapTotal / 1024 / 1024 24 | console.log(+(memoryAfter - memory).toFixed(2), 'MB memory used') 25 | console.log(+(t2 - t1).toFixed(2), 'ms') 26 | -------------------------------------------------------------------------------- /example/stress/memoir.ts: -------------------------------------------------------------------------------- 1 | import { t } from '../../src' 2 | import { Memoirist } from 'memoirist' 3 | 4 | const total = 1000 5 | const stack: Memoirist[] = [] 6 | 7 | { 8 | const t1 = performance.now() 9 | const memory = process.memoryUsage().heapTotal / 1024 / 1024 10 | 11 | for (let i = 0; i < total; i++) { 12 | for (let i = 0; i < 2; i++) { 13 | const router = new Memoirist() 14 | router.add('GET', '/a', () => 'Hello, World!') 15 | router.add('GET', '/b', () => 'Hello, World!') 16 | 17 | stack.push(router) 18 | } 19 | } 20 | 21 | const memoryAfter = process.memoryUsage().heapTotal / 1024 / 1024 22 | const took = performance.now() - t1 23 | 24 | console.log( 25 | Intl.NumberFormat().format(total), 26 | 'routes took', 27 | +took.toFixed(4), 28 | 'ms' 29 | ) 30 | console.log('Average', +(took / total).toFixed(4), 'ms / route') 31 | 32 | console.log(memoryAfter - memory, 'MB memory used') 33 | } 34 | -------------------------------------------------------------------------------- /example/stress/multiple-routes.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../../src' 2 | 3 | const total = 500 4 | 5 | { 6 | const app = new Elysia() 7 | 8 | const t1 = performance.now() 9 | const memory = process.memoryUsage().heapTotal / 1024 / 1024 10 | 11 | for (let i = 0; i < total; i++) 12 | app.get(`/id/${i}`, () => 'hello', { 13 | response: t.String() 14 | }) 15 | 16 | const memoryAfter = process.memoryUsage().heapTotal / 1024 / 1024 17 | const took = performance.now() - t1 18 | 19 | console.log( 20 | Intl.NumberFormat().format(total), 21 | 'routes took', 22 | +took.toFixed(4), 23 | 'ms' 24 | ) 25 | console.log('Average', +(took / total).toFixed(4), 'ms / route') 26 | 27 | console.log(memoryAfter - memory, 'MB memory used') 28 | console.log(((memoryAfter - memory) / total) * 1024, 'KB memory used') 29 | } 30 | -------------------------------------------------------------------------------- /example/stress/sucrose.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import { sucrose } from '../../src/sucrose' 4 | 5 | const total = 100_000 6 | const t = performance.now() 7 | 8 | for (let i = 0; i < total; i++) { 9 | sucrose({ 10 | handler: function ({ query }) { 11 | query.a 12 | }, 13 | afterHandle: [], 14 | beforeHandle: [ 15 | function a({ params: { a, c: d }, ...rest }) { 16 | query.b 17 | }, 18 | ({ error }) => {} 19 | ], 20 | error: [ 21 | function a({ query, query: { a, c: d }, headers: { hello } }) { 22 | query.b 23 | }, 24 | ({ query: { f } }) => {} 25 | ], 26 | mapResponse: [], 27 | onResponse: [], 28 | parse: [], 29 | request: [], 30 | start: [], 31 | stop: [], 32 | trace: [], 33 | transform: [] 34 | }) 35 | } 36 | 37 | const took = performance.now() - t 38 | 39 | console.log( 40 | Intl.NumberFormat().format(total), 41 | 'check took', 42 | +took.toFixed(4), 43 | 'ms' 44 | ) 45 | console.log('Average', +(took / total).toFixed(4), 'ms / check') 46 | -------------------------------------------------------------------------------- /example/takodachi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/elysia/75e9366d350a6d6ba02f8ceb124f5c29e3b9dd5d/example/takodachi.png -------------------------------------------------------------------------------- /example/teapot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/elysia/75e9366d350a6d6ba02f8ceb124f5c29e3b9dd5d/example/teapot.webp -------------------------------------------------------------------------------- /example/type-inference.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | const counter = (app: Elysia) => app.state('counter', 0) 4 | 5 | new Elysia() 6 | .use(counter) 7 | .guard({}, (app) => app.get('/id/:id', ({ store: { counter } }) => counter)) 8 | .listen(3000) 9 | -------------------------------------------------------------------------------- /example/upload.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../src' 2 | import { upload } from '../test/utils' 3 | 4 | const app = new Elysia() 5 | .post('/single', ({ body: { file } }) => file, { 6 | body: t.Object({ 7 | file: t.File() 8 | }) 9 | }) 10 | .post( 11 | '/multiple', 12 | ({ body: { files } }) => files.reduce((a, b) => a + b.size, 0), 13 | { 14 | body: t.Object({ 15 | files: t.Files() 16 | }) 17 | } 18 | ) 19 | .listen(3000) 20 | 21 | const { request } = upload('/single', { 22 | file: 'millenium.jpg' 23 | }) 24 | 25 | app.handle(request) 26 | .then((r) => r.text()) 27 | .then(console.log) 28 | -------------------------------------------------------------------------------- /example/video.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from 'elysia' 2 | 3 | new Elysia() 4 | .get('/', Bun.file('test/kyuukurarin.mp4')) 5 | .listen(3000) 6 | -------------------------------------------------------------------------------- /example/websocket.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | const app = new Elysia() 4 | .state('start', 'here') 5 | .ws('/ws', { 6 | open(ws) { 7 | ws.subscribe('asdf') 8 | console.log('Open Connection:', ws.id) 9 | }, 10 | close(ws) { 11 | console.log('Closed Connection:', ws.id) 12 | }, 13 | message(ws, message) { 14 | ws.publish('asdf', message) 15 | ws.send('asdf', message) 16 | } 17 | }) 18 | .get('/publish/:publish', ({ params: { publish: text } }) => { 19 | app.server!.publish('asdf', text) 20 | 21 | return text 22 | }) 23 | .listen(3000, (server) => { 24 | console.log(`http://${server.hostname}:${server.port}`) 25 | }) 26 | -------------------------------------------------------------------------------- /src/adapter/bun/handler-native.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from '../../context' 2 | import type { AnyLocalHook, MaybePromise } from '../../types' 3 | 4 | import { mapResponse } from './handler' 5 | 6 | export const createNativeStaticHandler = ( 7 | handle: unknown, 8 | hooks: AnyLocalHook, 9 | setHeaders: Context['set']['headers'] = {} 10 | ): (() => MaybePromise) | undefined => { 11 | if (typeof handle === 'function' || handle instanceof Blob) return 12 | 13 | if ( 14 | typeof handle === 'object' && 15 | handle?.toString() === '[object HTMLBundle]' 16 | ) 17 | // Bun HTMLBundle 18 | return () => handle as any 19 | 20 | const response = mapResponse(handle, { 21 | headers: setHeaders 22 | }) 23 | 24 | if ( 25 | !hooks.parse?.length && 26 | !hooks.transform?.length && 27 | !hooks.beforeHandle?.length && 28 | !hooks.afterHandle?.length 29 | ) { 30 | if (response instanceof Promise) 31 | return response.then((response) => { 32 | if (!response) return 33 | 34 | if (!response.headers.has('content-type')) 35 | response.headers.append('content-type', 'text/plain') 36 | 37 | return response.clone() 38 | }) as any as () => Promise 39 | 40 | if (!response.headers.has('content-type')) 41 | response.headers.append('content-type', 'text/plain') 42 | 43 | return response.clone.bind(response) as any 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/adapter/index.ts: -------------------------------------------------------------------------------- 1 | export type { ElysiaAdapter } from './types' 2 | -------------------------------------------------------------------------------- /src/adapter/types.ts: -------------------------------------------------------------------------------- 1 | import type { Serve, ListenCallback } from '../universal/server' 2 | 3 | import type { AnyElysia } from '..' 4 | import type { Context } from '../context' 5 | import type { Sucrose } from '../sucrose' 6 | 7 | import type { Prettify, AnyLocalHook, MaybePromise } from '../types' 8 | import type { AnyWSLocalHook } from '../ws/types' 9 | 10 | export interface ElysiaAdapter { 11 | name: string 12 | listen( 13 | app: AnyElysia 14 | ): ( 15 | options: string | number | Partial, 16 | callback?: ListenCallback 17 | ) => void 18 | isWebStandard?: boolean 19 | handler: { 20 | /** 21 | * Map return response on every case 22 | */ 23 | mapResponse( 24 | response: unknown, 25 | set: Context['set'], 26 | ...params: unknown[] 27 | ): unknown 28 | /** 29 | * Map response on truthy value 30 | */ 31 | mapEarlyResponse( 32 | response: unknown, 33 | set: Context['set'], 34 | ...params: unknown[] 35 | ): unknown 36 | /** 37 | * Map response without cookie, status or headers 38 | */ 39 | mapCompactResponse(response: unknown, ...params: unknown[]): unknown 40 | /** 41 | * Compile inline to value 42 | * 43 | * @example 44 | * ```ts 45 | * Elysia().get('/', 'static') 46 | * ``` 47 | */ 48 | createStaticHandler?( 49 | handle: unknown, 50 | hooks: AnyLocalHook, 51 | setHeaders?: Context['set']['headers'], 52 | ...params: unknown[] 53 | ): (() => unknown) | undefined 54 | /** 55 | * If the runtime support cloning response 56 | * 57 | * eg. Bun.serve({ static }) 58 | */ 59 | createNativeStaticHandler?( 60 | handle: unknown, 61 | hooks: AnyLocalHook, 62 | setHeaders?: Context['set']['headers'], 63 | ...params: unknown[] 64 | ): (() => MaybePromise) | undefined 65 | } 66 | composeHandler: { 67 | mapResponseContext?: string 68 | /** 69 | * Declare any variable that will be used in the general handler 70 | */ 71 | declare?(inference: Sucrose.Inference): string | undefined 72 | /** 73 | * Inject variable to the general handler 74 | */ 75 | inject?: Record 76 | /** 77 | * Whether retriving headers should be using webstandard headers 78 | * 79 | * @default false 80 | */ 81 | preferWebstandardHeaders?: boolean 82 | /** 83 | * fnLiteral for parsing request headers 84 | * 85 | * @declaration 86 | * c.headers: Context headers 87 | */ 88 | headers: string 89 | /** 90 | * fnLiteral for parsing the request body 91 | * 92 | * @declaration 93 | * c.body: Context body 94 | */ 95 | parser: Prettify< 96 | Record< 97 | 'json' | 'text' | 'urlencoded' | 'arrayBuffer' | 'formData', 98 | (isOptional: boolean) => string 99 | > & { 100 | declare?: string 101 | } 102 | > 103 | } 104 | composeGeneralHandler: { 105 | parameters?: string 106 | error404( 107 | hasEventHook: boolean, 108 | hasErrorHook: boolean 109 | ): { 110 | declare: string 111 | code: string 112 | } 113 | /** 114 | * fnLiteral of the general handler 115 | * 116 | * @declaration 117 | * c: Context 118 | * p: pathname 119 | */ 120 | createContext(app: AnyElysia): string 121 | /** 122 | * Inject variable to the general handler 123 | */ 124 | inject?: Record 125 | } 126 | composeError: { 127 | declare?: string 128 | inject?: Record 129 | mapResponseContext: string 130 | validationError: string 131 | /** 132 | * Handle thrown error which is instance of Error 133 | * 134 | * Despite its name of `unknownError`, it also handle named error like `NOT_FOUND`, `VALIDATION_ERROR` 135 | * It's named `unknownError` because it also catch unknown error 136 | */ 137 | unknownError: string 138 | } 139 | ws?(app: AnyElysia, path: string, handler: AnyWSLocalHook): unknown 140 | /** 141 | * Whether or not the runtime or framework the is built on top on has a router 142 | * eg. Bun.serve.routes, uWebSocket 143 | **/ 144 | createSystemRouterHandler?( 145 | method: string, 146 | path: string, 147 | hook: AnyLocalHook, 148 | app: AnyElysia 149 | ): void 150 | } 151 | -------------------------------------------------------------------------------- /src/manifest.ts: -------------------------------------------------------------------------------- 1 | // import { stat, mkdir, writeFile } from 'fs/promises' 2 | // import type { AnyElysia } from '.' 3 | // import { checksum } from './utils' 4 | 5 | // const mkdirIfNotExists = async (path: string) => { 6 | // if ( 7 | // await stat(path) 8 | // .then(() => false) 9 | // .catch(() => true) 10 | // ) 11 | // await mkdir(path) 12 | // } 13 | 14 | // export const manifest = async (app: AnyElysia) => { 15 | // await app.modules 16 | 17 | // app.compile() 18 | 19 | // console.log(process.cwd()) 20 | 21 | // await mkdirIfNotExists('.elysia') 22 | // await mkdirIfNotExists('.elysia/routes') 23 | 24 | // const ops = []>[] 25 | 26 | // let appChecksum = 0 27 | 28 | // for (const route of app.routes) { 29 | // const { path, method } = route 30 | 31 | // const code = route.compile().toString() 32 | // const name = `.elysia/routes/${path === '' ? 'index' : path.endsWith('/') ? path.replace(/\//g, '_') + 'index' : path.replace(/\//g, '_')}.${method.toLowerCase()}.js` 33 | 34 | // appChecksum = checksum(appChecksum + path + method + code) 35 | 36 | // ops.push(writeFile(name, '//' + checksum(code) + '\n' + code)) 37 | // } 38 | 39 | // const code = app.fetch.toString() 40 | // appChecksum = checksum(appChecksum + code) 41 | 42 | // ops.push(writeFile(`.elysia/handler.js`, '//' + appChecksum + '\n' + code)) 43 | 44 | // await Promise.all(ops) 45 | 46 | // console.log('DONE') 47 | // } 48 | -------------------------------------------------------------------------------- /src/universal/env.ts: -------------------------------------------------------------------------------- 1 | import { isBun } from './utils' 2 | 3 | export const env = isBun 4 | ? Bun.env 5 | : typeof process !== 'undefined' && process?.env 6 | ? process.env 7 | : {} 8 | -------------------------------------------------------------------------------- /src/universal/file.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | import { type createReadStream as CreateReadStream } from 'fs' 3 | import { type stat as Stat } from 'fs/promises' 4 | 5 | import { isBun } from './utils' 6 | import type { BunFile } from 'bun' 7 | import type { MaybePromise } from '../types' 8 | 9 | export const mime = { 10 | aac: 'audio/aac', 11 | abw: 'application/x-abiword', 12 | ai: 'application/postscript', 13 | arc: 'application/octet-stream', 14 | avi: 'video/x-msvideo', 15 | azw: 'application/vnd.amazon.ebook', 16 | bin: 'application/octet-stream', 17 | bz: 'application/x-bzip', 18 | bz2: 'application/x-bzip2', 19 | csh: 'application/x-csh', 20 | css: 'text/css', 21 | csv: 'text/csv', 22 | doc: 'application/msword', 23 | dll: 'application/octet-stream', 24 | eot: 'application/vnd.ms-fontobject', 25 | epub: 'application/epub+zip', 26 | gif: 'image/gif', 27 | htm: 'text/html', 28 | html: 'text/html', 29 | ico: 'image/x-icon', 30 | ics: 'text/calendar', 31 | jar: 'application/java-archive', 32 | jpeg: 'image/jpeg', 33 | jpg: 'image/jpeg', 34 | js: 'application/javascript', 35 | json: 'application/json', 36 | mid: 'audio/midi', 37 | midi: 'audio/midi', 38 | mp2: 'audio/mpeg', 39 | mp3: 'audio/mpeg', 40 | mp4: 'video/mp4', 41 | mpa: 'video/mpeg', 42 | mpe: 'video/mpeg', 43 | mpeg: 'video/mpeg', 44 | mpkg: 'application/vnd.apple.installer+xml', 45 | odp: 'application/vnd.oasis.opendocument.presentation', 46 | ods: 'application/vnd.oasis.opendocument.spreadsheet', 47 | odt: 'application/vnd.oasis.opendocument.text', 48 | oga: 'audio/ogg', 49 | ogv: 'video/ogg', 50 | ogx: 'application/ogg', 51 | otf: 'font/otf', 52 | png: 'image/png', 53 | pdf: 'application/pdf', 54 | ppt: 'application/vnd.ms-powerpoint', 55 | rar: 'application/x-rar-compressed', 56 | rtf: 'application/rtf', 57 | sh: 'application/x-sh', 58 | svg: 'image/svg+xml', 59 | swf: 'application/x-shockwave-flash', 60 | tar: 'application/x-tar', 61 | tif: 'image/tiff', 62 | tiff: 'image/tiff', 63 | ts: 'application/typescript', 64 | ttf: 'font/ttf', 65 | txt: 'text/plain', 66 | vsd: 'application/vnd.visio', 67 | wav: 'audio/x-wav', 68 | weba: 'audio/webm', 69 | webm: 'video/webm', 70 | webp: 'image/webp', 71 | woff: 'font/woff', 72 | woff2: 'font/woff2', 73 | xhtml: 'application/xhtml+xml', 74 | xls: 'application/vnd.ms-excel', 75 | xlsx: 'application/vnd.ms-excel', 76 | xlsx_OLD: 77 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 78 | xml: 'application/xml', 79 | xul: 'application/vnd.mozilla.xul+xml', 80 | zip: 'application/zip', 81 | '3gp': 'video/3gpp', 82 | '3gp_DOES_NOT_CONTAIN_VIDEO': 'audio/3gpp', 83 | '3gp2': 'video/3gpp2', 84 | '3gp2_DOES_NOT_CONTAIN_VIDEO': 'audio/3gpp2', 85 | '7z': 'application/x-7z-compressed' 86 | } as const 87 | 88 | export const getFileExtension = (path: string) => { 89 | const index = path.lastIndexOf('.') 90 | if (index === -1) return '' 91 | 92 | return path.slice(index + 1) 93 | } 94 | 95 | export const file = (path: string) => new ElysiaFile(path) 96 | 97 | let createReadStream: typeof CreateReadStream 98 | let stat: typeof Stat 99 | 100 | export class ElysiaFile { 101 | readonly value: MaybePromise 102 | readonly stats: ReturnType | undefined 103 | 104 | constructor(public path: string) { 105 | if (isBun) this.value = Bun.file(path) 106 | else { 107 | // Browser 108 | // @ts-ignore 109 | if (typeof window !== 'undefined') { 110 | console.warn('Browser environment does not support file') 111 | } else { 112 | if (!createReadStream || !stat) { 113 | try { 114 | this.value = import('fs').then((fs) => { 115 | createReadStream = fs.createReadStream 116 | 117 | return fs.createReadStream(path) 118 | }) 119 | this.stats = import('fs/promises').then((fs) => { 120 | stat = fs.stat 121 | 122 | return fs.stat(path) 123 | }) 124 | } catch { 125 | // not empty 126 | } 127 | } else { 128 | this.value = createReadStream(path) 129 | this.stats = stat(path)! 130 | } 131 | } 132 | } 133 | } 134 | 135 | get type() { 136 | return ( 137 | // @ts-ignore 138 | mime[getFileExtension(this.path)] || 'application/octet-stream' 139 | ) 140 | } 141 | 142 | get length() { 143 | if (isBun) return (this.value as BunFile).size 144 | 145 | return this.stats?.then((x) => x.size) ?? 0 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/universal/index.ts: -------------------------------------------------------------------------------- 1 | export { env } from './env' 2 | export { file } from './file' 3 | export type { 4 | ErrorLike, 5 | GenericServeOptions, 6 | Serve, 7 | ServeOptions, 8 | Server, 9 | ServerWebSocketSendStatus, 10 | SocketAddress 11 | } from './server' 12 | -------------------------------------------------------------------------------- /src/universal/utils.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | export const isBun = typeof Bun !== 'undefined' 3 | // @ts-ignore 4 | export const isDeno = typeof Deno !== 'undefined' 5 | -------------------------------------------------------------------------------- /test/a.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | import { it, expect } from 'bun:test' 3 | 4 | const asyncPlugin = Promise.resolve(new Elysia({ name: 'AsyncPlugin' })) 5 | 6 | const plugin = new Elysia({ name: 'Plugin' }) 7 | .use(asyncPlugin) 8 | .get('/plugin', () => 'GET /plugin') 9 | 10 | const app = new Elysia({ name: 'App' }) 11 | .use(plugin) 12 | .get('/foo', () => 'GET /foo') 13 | 14 | it('matches the right route', async () => { 15 | const response = await app.handle(new Request('http://localhost/plugin')) 16 | const text = await response.text() 17 | expect(text).toEqual('GET /plugin') 18 | }) 19 | -------------------------------------------------------------------------------- /test/adapter/web-standard/cookie-to-header.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { serializeCookie } from '../../../src/cookies' 3 | 4 | describe('Web Standard - Cookie to Header', () => { 5 | it('return undefined on empty object', () => { 6 | const cookies = {} 7 | const result = serializeCookie(cookies) 8 | expect(result).toBeUndefined() 9 | }) 10 | 11 | it('correctly serialize a single value cookie', () => { 12 | const cookies = { 13 | cookie1: { 14 | value: 'value1' 15 | } 16 | } 17 | const result = serializeCookie(cookies) 18 | expect(result).toEqual('cookie1=value1') 19 | }) 20 | 21 | it('correctly serialize a multi-value cookie', () => { 22 | const cookies = { 23 | cookie1: { 24 | value: ['value1', 'value2'] 25 | } 26 | } 27 | 28 | // @ts-ignore 29 | const result = serializeCookie(cookies) 30 | expect(result).toEqual('cookie1=%5B%22value1%22%2C%22value2%22%5D') 31 | }) 32 | 33 | it('return undefined when the input is undefined', () => { 34 | const cookies = undefined 35 | // @ts-ignore 36 | const result = serializeCookie(cookies) 37 | expect(result).toBeUndefined() 38 | }) 39 | 40 | it('return undefined when the input is null', () => { 41 | const cookies = null 42 | // @ts-ignore 43 | const result = serializeCookie(cookies) 44 | expect(result).toBeUndefined() 45 | }) 46 | 47 | it('return undefined when the input is not an object', () => { 48 | const cookies = 'invalid' 49 | // @ts-ignore 50 | const result = serializeCookie(cookies) 51 | expect(result).toBeUndefined() 52 | }) 53 | 54 | it('return undefined when the input is an empty object', () => { 55 | const cookies = {} 56 | const result = serializeCookie(cookies) 57 | expect(result).toBeUndefined() 58 | }) 59 | 60 | it('return undefined when the input is an empty object', () => { 61 | const cookies = {} 62 | const result = serializeCookie(cookies) 63 | expect(result).toBeUndefined() 64 | }) 65 | 66 | it('return undefined when the input is an object with null values', () => { 67 | const cookies = { 68 | cookie1: null, 69 | cookie2: null 70 | } 71 | 72 | // @ts-ignore 73 | const result = serializeCookie(cookies) 74 | expect(result).toBeUndefined() 75 | }) 76 | 77 | it('return undefined when the input is an empty object', () => { 78 | const cookies = {} 79 | const result = serializeCookie(cookies) 80 | expect(result).toBeUndefined() 81 | }) 82 | 83 | it('return undefined when the input is an object with non-string or non-array values', () => { 84 | const cookies = { 85 | key1: 123, 86 | key2: true, 87 | key3: { prop: 'value' }, 88 | key4: [1, 2, 3] 89 | } 90 | // @ts-ignore 91 | const result = serializeCookie(cookies) 92 | expect(result).toBeUndefined() 93 | }) 94 | 95 | it('return undefined when the input is an empty object', () => { 96 | const cookies = {} 97 | const result = serializeCookie(cookies) 98 | expect(result).toBeUndefined() 99 | }) 100 | 101 | it('return undefined when the input is an empty object', () => { 102 | const cookies = {} 103 | const result = serializeCookie(cookies) 104 | expect(result).toBeUndefined() 105 | }) 106 | 107 | it('return undefined when the input is an empty object', () => { 108 | const cookies = {} 109 | const result = serializeCookie(cookies) 110 | expect(result).toBeUndefined() 111 | }) 112 | 113 | it('return undefined when the input is an object with non-string keys', () => { 114 | const cookies = { 1: 'value1', 2: 'value2' } 115 | // @ts-ignore 116 | const result = serializeCookie(cookies) 117 | expect(result).toBeUndefined() 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /test/adapter/web-standard/set-cookie.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { parseSetCookies } from '../../../src/adapter/utils' 3 | 4 | describe('Web Standard - Parse Set Cookie', () => { 5 | it('should handle empty arrays', () => { 6 | const headers = new Headers([]) 7 | const setCookie: string[] = [] 8 | const result = parseSetCookies(headers, setCookie) 9 | expect(result).toEqual(headers) 10 | }) 11 | 12 | it('should handle a setCookie array with one element containing a single key-value pair', () => { 13 | const headers = new Headers([]) 14 | const setCookie = ['key=value'] 15 | const result = parseSetCookies(headers, setCookie) 16 | expect(result.get('Set-Cookie')).toEqual('key=value') 17 | }) 18 | 19 | it('should handle a setCookie array with multiple elements, each containing a single key-value pair', () => { 20 | const headers = new Headers([]) 21 | const setCookie = ['key1=value1', 'key2=value2'] 22 | const result = parseSetCookies(headers, setCookie) 23 | expect(result.get('Set-Cookie')).toEqual('key1=value1, key2=value2') 24 | }) 25 | 26 | it('should handle a setCookie array with one element containing multiple key-value pairs', () => { 27 | const headers = new Headers([]) 28 | const setCookie = ['key1=value1; key2=value2'] 29 | const result = parseSetCookies(headers, setCookie) 30 | expect(result.get('Set-Cookie')).toEqual('key1=value1; key2=value2') 31 | }) 32 | 33 | it('should handle a setCookie array with multiple elements, each containing multiple key-value pairs', () => { 34 | const headers = new Headers([]) 35 | const setCookie = [ 36 | 'key1=value1; key2=value2', 37 | 'key3=value3; key4=value4' 38 | ] 39 | const result = parseSetCookies(headers, setCookie) 40 | expect(result.get('Set-Cookie')).toEqual( 41 | 'key1=value1; key2=value2, key3=value3; key4=value4' 42 | ) 43 | }) 44 | 45 | it('should handle null values', () => { 46 | const headers = null 47 | const setCookie = null 48 | // @ts-ignore 49 | const result = parseSetCookies(headers, setCookie) 50 | expect(result).toBeNull() 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /test/adapter/web-standard/utils.ts: -------------------------------------------------------------------------------- 1 | export class Passthrough { 2 | toResponse() { 3 | return this.custom 4 | } 5 | 6 | get custom() { 7 | return 'hi' 8 | } 9 | } -------------------------------------------------------------------------------- /test/aot/generation.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { describe, it, expect } from 'bun:test' 3 | import { Context, Elysia, t } from '../../src' 4 | import { post, req } from '../utils' 5 | 6 | describe('code generation', () => { 7 | it('fallback query if not presented', async () => { 8 | const app = new Elysia().get('/', () => 'hi', { 9 | query: t.Object({ 10 | id: t.Optional(t.Number()) 11 | }) 12 | }) 13 | 14 | const res = await app.handle(req('/')).then((x) => x.text()) 15 | 16 | expect(res).toBe('hi') 17 | }) 18 | 19 | it('process isFnUse', async () => { 20 | const body = { hello: 'Wanderschaffen' } 21 | 22 | const app = new Elysia() 23 | .post('/1', ({ body }) => body) 24 | .post('/2', function ({ body }) { 25 | return body 26 | }) 27 | .post('/3', (context) => { 28 | return context.body 29 | }) 30 | .post('/4', (context) => { 31 | const c = context 32 | const { body } = c 33 | 34 | return body 35 | }) 36 | .post('/5', (context) => { 37 | const _ = context, 38 | a = context 39 | const { body } = a 40 | 41 | return body 42 | }) 43 | .post('/6', () => body, { 44 | transform({ body }) { 45 | // not empty 46 | } 47 | }) 48 | .post('/7', () => body, { 49 | beforeHandle({ body }) { 50 | // not empty 51 | } 52 | }) 53 | .post('/8', () => body, { 54 | afterHandle({ body }) { 55 | // not empty 56 | } 57 | }) 58 | .post('/9', ({ ...rest }) => rest.body) 59 | 60 | const from = (number: number) => 61 | app.handle(post(`/${number}`, body)).then((r) => r.json()) 62 | 63 | const cases = Promise.all( 64 | Array(9) 65 | .fill(null) 66 | .map((_, i) => from(i + 1)) 67 | ) 68 | 69 | for (const unit of await cases) expect(unit).toEqual(body) 70 | }) 71 | 72 | it('process isContextPassToUnknown', async () => { 73 | const body = { hello: 'Wanderschaffen' } 74 | 75 | const handle = (context: Context) => context.body 76 | 77 | const app = new Elysia() 78 | .post('/1', (context) => handle(context)) 79 | .post('/2', function (context) { 80 | return handle(context) 81 | }) 82 | .post('/3', (context) => { 83 | const c = context 84 | 85 | return handle(c) 86 | }) 87 | .post('/4', (context) => { 88 | const _ = context, 89 | a = context 90 | 91 | return handle(a) 92 | }) 93 | .post('/5', () => '', { 94 | beforeHandle(context) { 95 | return handle(context) 96 | } 97 | }) 98 | .post('/6', () => body, { 99 | afterHandle(context) { 100 | return handle(context) 101 | } 102 | }) 103 | .post('/7', ({ ...rest }) => handle(rest)) 104 | 105 | const from = (number: number) => 106 | app.handle(post(`/${number}`, body)).then((r) => r.json()) 107 | 108 | const cases = Promise.all( 109 | Array(7) 110 | .fill(null) 111 | .map((_, i) => from(i + 1)) 112 | ) 113 | 114 | for (const unit of await cases) expect(unit).toEqual(body) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /test/aot/has-transform.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'bun:test' 2 | 3 | import { t } from '../../src' 4 | import { hasTransform } from '../../src/schema' 5 | 6 | describe('Has Transform', () => { 7 | it('find primitive', () => { 8 | const schema = t 9 | .Transform(t.String()) 10 | .Decode((v) => v) 11 | .Encode((v) => v) 12 | 13 | expect(hasTransform(schema)).toBe(true) 14 | }) 15 | 16 | it('find in root object', () => { 17 | const schema = t.Object({ 18 | liyue: t 19 | .Transform(t.String()) 20 | .Decode((v) => v) 21 | .Encode((v) => v) 22 | }) 23 | 24 | expect(hasTransform(schema)).toBe(true) 25 | }) 26 | 27 | it('find in nested object', () => { 28 | const schema = t.Object({ 29 | liyue: t.Object({ 30 | id: t 31 | .Transform(t.String()) 32 | .Decode((v) => v) 33 | .Encode((v) => v) 34 | }) 35 | }) 36 | 37 | expect(hasTransform(schema)).toBe(true) 38 | }) 39 | 40 | it('find in Optional', () => { 41 | const schema = t.Optional( 42 | t.Object({ 43 | prop1: t 44 | .Transform(t.String()) 45 | .Decode((v) => v) 46 | .Encode((v) => v) 47 | }) 48 | ) 49 | 50 | expect(hasTransform(schema)).toBe(true) 51 | }) 52 | 53 | it('find on multiple transform', () => { 54 | const schema = t.Object({ 55 | id: t 56 | .Transform(t.String()) 57 | .Decode((v) => v) 58 | .Encode((v) => v), 59 | name: t 60 | .Transform(t.String()) 61 | .Decode((v) => v) 62 | .Encode((v) => v) 63 | }) 64 | 65 | expect(hasTransform(schema)).toBe(true) 66 | }) 67 | 68 | it('return false on not found', () => { 69 | const schema = t.Object({ 70 | name: t.String(), 71 | age: t.Number() 72 | }) 73 | 74 | expect(hasTransform(schema)).toBe(false) 75 | }) 76 | 77 | it('found on Union', () => { 78 | const schema = t.Object({ 79 | id: t.Number(), 80 | liyue: t.Union([ 81 | t 82 | .Transform(t.String()) 83 | .Decode((v) => v) 84 | .Encode((v) => v), 85 | t.Number() 86 | ]) 87 | }) 88 | 89 | expect(hasTransform(schema)).toBe(true) 90 | }) 91 | 92 | it('Found t.Numeric', () => { 93 | const schema = t.Object({ 94 | id: t.Numeric(), 95 | liyue: t.String() 96 | }) 97 | 98 | expect(hasTransform(schema)).toBe(true) 99 | }) 100 | 101 | it('Found t.ObjectString', () => { 102 | const schema = t.Object({ 103 | id: t.String(), 104 | liyue: t.ObjectString({ 105 | name: t.String() 106 | }) 107 | }) 108 | 109 | expect(hasTransform(schema)).toBe(true) 110 | }) 111 | }) 112 | -------------------------------------------------------------------------------- /test/aot/has-type.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'bun:test' 2 | 3 | import { t } from '../../src' 4 | import { hasType } from '../../src/schema' 5 | 6 | describe('Has Transform', () => { 7 | it('find primitive', () => { 8 | const schema = t 9 | .Transform(t.File()) 10 | .Decode((v) => v) 11 | .Encode((v) => v) 12 | 13 | expect(hasType('File', schema)).toBe(true) 14 | }) 15 | 16 | it('find in root object', () => { 17 | const schema = t.Object({ 18 | liyue: t.File() 19 | }) 20 | 21 | expect(hasType('File', schema)).toBe(true) 22 | }) 23 | 24 | it('find in nested object', () => { 25 | const schema = t.Object({ 26 | liyue: t.Object({ 27 | id: t.File() 28 | }) 29 | }) 30 | 31 | expect(hasType('File', schema)).toBe(true) 32 | }) 33 | 34 | it('find in Optional', () => { 35 | const schema = t.Optional( 36 | t.Object({ 37 | prop1: t.File() 38 | }) 39 | ) 40 | 41 | expect(hasType('File', schema)).toBe(true) 42 | }) 43 | 44 | it('find on multiple transform', () => { 45 | const schema = t.Object({ 46 | id: t.File(), 47 | name: t.File() 48 | }) 49 | 50 | expect(hasType('File', schema)).toBe(true) 51 | }) 52 | 53 | it('return false on not found', () => { 54 | const schema = t.Object({ 55 | name: t.String(), 56 | age: t.Number() 57 | }) 58 | 59 | expect(hasType('File', schema)).toBe(false) 60 | }) 61 | 62 | it('found on Union', () => { 63 | const schema = t.Object({ 64 | id: t.Number(), 65 | liyue: t.Union([t.Number(), t.File()]) 66 | }) 67 | 68 | expect(hasType('File', schema)).toBe(true) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /test/cookie/explicit.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { Cookie, createCookieJar } from '../../src/cookies' 3 | import type { Context } from '../../src' 4 | 5 | const create = () => { 6 | const set: Context['set'] = { 7 | cookie: {}, 8 | headers: {} 9 | } 10 | 11 | const cookie = createCookieJar(set, {}) 12 | 13 | return { 14 | cookie, 15 | set 16 | } 17 | } 18 | 19 | describe('Explicit Cookie', () => { 20 | it('create cookie', () => { 21 | const { cookie, set } = create() 22 | cookie.name.value = 'himari' 23 | 24 | expect(set.cookie?.name).toEqual({ 25 | value: 'himari' 26 | }) 27 | }) 28 | 29 | it('add cookie attribute', () => { 30 | const { cookie, set } = create() 31 | cookie.name.value = 'himari' 32 | 33 | cookie.name.update({ 34 | domain: 'millennium.sh' 35 | }) 36 | 37 | expect(set.cookie?.name).toEqual({ 38 | value: 'himari', 39 | domain: 'millennium.sh' 40 | }) 41 | }) 42 | 43 | it('add cookie attribute without overwrite entire property', () => { 44 | const { cookie, set } = create() 45 | cookie.name.value = 'himari' 46 | cookie.name.domain = 'millennium.sh' 47 | 48 | cookie.name.update({ 49 | httpOnly: true, 50 | path: '/' 51 | }) 52 | 53 | expect(set.cookie?.name).toEqual({ 54 | value: 'himari', 55 | domain: 'millennium.sh', 56 | httpOnly: true, 57 | path: '/' 58 | }) 59 | }) 60 | 61 | it('set cookie attribute', () => { 62 | const { cookie, set } = create() 63 | cookie.name.value = 'himari' 64 | cookie.name.domain = 'millennium.sh' 65 | 66 | cookie.name.set({ 67 | httpOnly: true, 68 | path: '/' 69 | }) 70 | 71 | expect(set.cookie?.name).toEqual({ 72 | httpOnly: true, 73 | path: '/', 74 | value: 'himari' 75 | }) 76 | }) 77 | 78 | it('add cookie overwrite attribute if duplicated', () => { 79 | const { cookie, set } = create() 80 | cookie.name.set({ 81 | value: 'aru', 82 | domain: 'millennium.sh', 83 | httpOnly: true 84 | }) 85 | 86 | cookie.name.update({ 87 | domain: 'gehenna.sh' 88 | }) 89 | 90 | expect(set.cookie?.name).toEqual({ 91 | value: 'aru', 92 | domain: 'gehenna.sh', 93 | httpOnly: true 94 | }) 95 | }) 96 | 97 | it('default undefined cookie with undefined', () => { 98 | const { cookie, set } = create() 99 | cookie.name 100 | 101 | expect(cookie?.name?.value).toEqual(undefined) 102 | }) 103 | 104 | it('overwrite existing cookie', () => { 105 | const { cookie, set } = create() 106 | cookie.name.value = 'aru' 107 | cookie.name.value = 'himari' 108 | 109 | expect(set.cookie?.name).toEqual({ value: 'himari' }) 110 | }) 111 | 112 | it('remove cookie', () => { 113 | const { cookie, set } = create() 114 | cookie.name.value = 'himari' 115 | cookie.name.remove() 116 | 117 | expect(set.cookie?.name.expires?.getTime()).toBeLessThanOrEqual( 118 | Date.now() 119 | ) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /test/cookie/implicit.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { createCookieJar } from '../../src/cookies' 3 | import type { Context } from '../../src' 4 | 5 | const create = () => { 6 | const set: Context['set'] = { 7 | cookie: {}, 8 | headers: {} 9 | } 10 | 11 | const cookie = createCookieJar(set, {}) 12 | 13 | return { 14 | cookie, 15 | set 16 | } 17 | } 18 | 19 | describe('Implicit Cookie', () => { 20 | it('create cookie using setter', () => { 21 | const { 22 | cookie: { name }, 23 | set 24 | } = create() 25 | 26 | name.value = 'himari' 27 | 28 | expect(set.cookie?.name).toEqual({ 29 | value: 'himari' 30 | }) 31 | }) 32 | 33 | it('create cookie set function', () => { 34 | const { 35 | cookie: { name }, 36 | set 37 | } = create() 38 | 39 | name.set({ 40 | value: 'himari' 41 | }) 42 | 43 | expect(set.cookie?.name).toEqual({ 44 | value: 'himari' 45 | }) 46 | }) 47 | 48 | it('add cookie attribute using setter', () => { 49 | const { 50 | cookie: { name }, 51 | set 52 | } = create() 53 | 54 | name.value = 'himari' 55 | name.domain = 'millennium.sh' 56 | 57 | expect(set.cookie?.name).toEqual({ 58 | value: 'himari', 59 | domain: 'millennium.sh' 60 | }) 61 | }) 62 | 63 | it('add cookie attribute using setter', () => { 64 | const { 65 | cookie: { name }, 66 | set 67 | } = create() 68 | 69 | name.value = 'himari' 70 | name.update({ 71 | domain: 'millennium.sh' 72 | }) 73 | 74 | expect(set.cookie?.name).toEqual({ 75 | value: 'himari', 76 | domain: 'millennium.sh' 77 | }) 78 | }) 79 | 80 | it('add cookie attribute without overwrite entire property', () => { 81 | const { 82 | cookie: { name }, 83 | set 84 | } = create() 85 | 86 | name.set({ 87 | value: 'himari', 88 | domain: 'millennium.sh' 89 | }).update({ 90 | httpOnly: true, 91 | path: '/' 92 | }) 93 | 94 | expect(set.cookie?.name).toEqual({ 95 | value: 'himari', 96 | domain: 'millennium.sh', 97 | httpOnly: true, 98 | path: '/' 99 | }) 100 | }) 101 | 102 | it('set cookie attribute', () => { 103 | const { 104 | cookie: { name }, 105 | set 106 | } = create() 107 | 108 | name.set({ 109 | value: 'himari', 110 | domain: 'millennium.sh' 111 | }).set({ 112 | httpOnly: true, 113 | path: '/' 114 | }) 115 | 116 | expect(set.cookie?.name).toEqual({ 117 | httpOnly: true, 118 | path: '/', 119 | value: 'himari', 120 | }) 121 | }) 122 | 123 | it('add cookie overwrite attribute if duplicated', () => { 124 | const { 125 | cookie: { name }, 126 | set 127 | } = create() 128 | 129 | name.set({ 130 | value: 'aru', 131 | domain: 'millennium.sh', 132 | httpOnly: true 133 | }).update({ 134 | domain: 'gehenna.sh' 135 | }) 136 | 137 | expect(set.cookie?.name).toEqual({ 138 | value: 'aru', 139 | domain: 'gehenna.sh', 140 | httpOnly: true 141 | }) 142 | }) 143 | 144 | it('create cookie with empty string', () => { 145 | const { 146 | cookie: { name }, 147 | set 148 | } = create() 149 | 150 | name.value = '' 151 | 152 | expect(set.cookie?.name).toEqual({ value: '' }) 153 | }) 154 | 155 | it('Remove cookie', () => { 156 | const { 157 | cookie: { name }, 158 | set 159 | } = create() 160 | 161 | name.value = 'himari' 162 | name.remove() 163 | 164 | expect(set.cookie?.name.expires?.getTime()).toBeLessThanOrEqual( 165 | Date.now() 166 | ) 167 | }) 168 | }) 169 | -------------------------------------------------------------------------------- /test/cookie/signature.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { parseCookie, Cookie } from '../../src/cookies' 3 | import { signCookie } from '../../src/utils' 4 | 5 | describe('Parse Cookie', () => { 6 | it('handle empty cookie', async () => { 7 | const set = { 8 | headers: {}, 9 | cookie: {} 10 | } 11 | const cookieString = '' 12 | const result = await parseCookie(set, cookieString) 13 | 14 | expect(result).toEqual({}) 15 | }) 16 | 17 | it('create cookie jar from cookie string', async () => { 18 | const set = { 19 | headers: {}, 20 | cookie: {} 21 | } 22 | const cookieString = 'fischl=Princess; eula=Noble; amber=Knight' 23 | const result = await parseCookie(set, cookieString) 24 | expect(result).toEqual({ 25 | fischl: expect.any(Cookie), 26 | eula: expect.any(Cookie), 27 | amber: expect.any(Cookie) 28 | }) 29 | }) 30 | 31 | it('unsign cookie signature', async () => { 32 | const set = { 33 | headers: {}, 34 | cookie: {} 35 | } 36 | 37 | const secrets = 'Fischl von Luftschloss Narfidort' 38 | 39 | const fischl = await signCookie('fischl', secrets) 40 | const cookieString = `fischl=${fischl}` 41 | const result = await parseCookie(set, cookieString, { 42 | secrets, 43 | sign: ['fischl'] 44 | }) 45 | 46 | expect(result.fischl.value).toEqual('fischl') 47 | }) 48 | 49 | it('unsign multiple signature', async () => { 50 | const set = { 51 | headers: {}, 52 | cookie: {} 53 | } 54 | 55 | const secrets = 'Fischl von Luftschloss Narfidort' 56 | 57 | const fischl = await signCookie('fischl', secrets) 58 | const eula = await signCookie('eula', secrets) 59 | 60 | const cookieString = `fischl=${fischl}; eula=${eula}` 61 | const result = await parseCookie(set, cookieString, { 62 | secrets, 63 | sign: ['fischl', 'eula'] 64 | }) 65 | 66 | expect(result.fischl.value).toEqual('fischl') 67 | expect(result.eula.value).toEqual('eula') 68 | }) 69 | 70 | // it('parse JSON value', async () => { 71 | // const set = { 72 | // headers: {}, 73 | // cookie: {} 74 | // } 75 | 76 | // const value = { 77 | // eula: 'Vengeance will be mine' 78 | // } 79 | 80 | // const cookieString = `letter=${encodeURIComponent( 81 | // JSON.stringify(value) 82 | // )}` 83 | // const result = await parseCookie(set, cookieString) 84 | // expect(result.letter.value).toEqual(value) 85 | // }) 86 | 87 | // it('parse true', async () => { 88 | // const set = { 89 | // headers: {}, 90 | // cookie: {} 91 | // } 92 | 93 | // const cookieString = `letter=true` 94 | // const result = await parseCookie(set, cookieString) 95 | // expect(result.letter.value).toEqual(true) 96 | // }) 97 | 98 | // it('parse false', async () => { 99 | // const set = { 100 | // headers: {}, 101 | // cookie: {} 102 | // } 103 | 104 | // const cookieString = `letter=false` 105 | // const result = await parseCookie(set, cookieString) 106 | // expect(result.letter.value).toEqual(false) 107 | // }) 108 | 109 | // it('parse number', async () => { 110 | // const set = { 111 | // headers: {}, 112 | // cookie: {} 113 | // } 114 | 115 | // const cookieString = `letter=123` 116 | // const result = await parseCookie(set, cookieString) 117 | // expect(result.letter.value).toEqual(123) 118 | // }) 119 | 120 | it('Unsign signature via secret rotation', async () => { 121 | const set = { 122 | headers: {}, 123 | cookie: {} 124 | } 125 | 126 | const secret = 'Fischl von Luftschloss Narfidort' 127 | 128 | const fischl = await signCookie('fischl', secret) 129 | const cookieString = `fischl=${fischl}` 130 | const result = await parseCookie(set, cookieString, { 131 | secrets: ['New Secret', secret], 132 | sign: ['fischl'] 133 | }) 134 | 135 | expect(result.fischl.value).toEqual('fischl') 136 | }) 137 | }) 138 | -------------------------------------------------------------------------------- /test/core/compose.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { Type } from '@sinclair/typebox' 3 | import { hasAdditionalProperties } from '../../src/schema' 4 | 5 | describe('hasAdditionalProperties', () => { 6 | it('should handle object schemas without properties key', () => { 7 | const schema = Type.Intersect([ 8 | Type.Object({ a: Type.String() }), 9 | // Record schemas does not have properties key, instead it has patternProperties 10 | Type.Record(Type.Number(), Type.String()) 11 | ]) 12 | expect(hasAdditionalProperties(schema)).toBe(false) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/core/config.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'bun:test' 2 | import { Elysia, t } from '../../src' 3 | 4 | describe('config', () => { 5 | it('standard hostname', async () => { 6 | const app = new Elysia({ handler: { standardHostname: false } }).get( 7 | '/a', 8 | 'a' 9 | ) 10 | 11 | const response = await app 12 | .handle(new Request('http://a/a')) 13 | .then((x) => x.text()) 14 | 15 | expect(response).toBe('a') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/core/context.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { Elysia } from '../../src' 3 | import { req } from '../utils' 4 | 5 | describe('context', () => { 6 | describe('return route', () => { 7 | it('on aot=true', async () => { 8 | const app = new Elysia().get('/hi/:id', ({ route }) => route) 9 | const res = await app.handle(req('/hi/123')).then((x) => x.text()) 10 | expect(res).toBe('/hi/:id') 11 | }) 12 | 13 | it('on aot=false', async () => { 14 | const app = new Elysia({ aot: false }).get( 15 | '/hi/:id', 16 | ({ route }) => route 17 | ) 18 | const res = await app.handle(req('/hi/123')).then((x) => x.text()) 19 | expect(res).toBe('/hi/:id') 20 | }) 21 | }) 22 | 23 | describe('early return on macros with route data', () => { 24 | it('on aot=true', async () => { 25 | const app = new Elysia() 26 | .macro({ 27 | test: { 28 | beforeHandle({ route }) { 29 | return route 30 | } 31 | } 32 | }) 33 | .get('/hi/:id', () => 'should not returned', { 34 | test: true 35 | }) 36 | const res = await app.handle(req('/hi/123')).then((x) => x.text()) 37 | expect(res).toBe('/hi/:id') 38 | }) 39 | 40 | it('on aot=false', async () => { 41 | const app = new Elysia({ aot: false }) 42 | .macro({ 43 | test: { 44 | beforeHandle({ route }) { 45 | return route 46 | } 47 | } 48 | }) 49 | .get('/hi/:id', () => 'should not returned', { 50 | test: true 51 | }) 52 | const res = await app.handle(req('/hi/123')).then((x) => x.text()) 53 | expect(res).toBe('/hi/:id') 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/core/formdata.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t, form, file } from '../../src' 2 | import { describe, expect, it } from 'bun:test' 3 | import { req } from '../utils' 4 | 5 | describe('Form Data', () => { 6 | it('return Bun.file', async () => { 7 | const app = new Elysia().get('/', () => 8 | form({ 9 | a: 'hello', 10 | b: Bun.file('test/kyuukurarin.mp4') 11 | }) 12 | ) 13 | 14 | const contentType = await app 15 | .handle(req('/')) 16 | .then((x) => x.headers.get('content-type')) 17 | 18 | expect(contentType).toStartWith('multipart/form-data') 19 | }) 20 | 21 | it('return Elysia.file', async () => { 22 | const app = new Elysia().get('/', () => 23 | form({ 24 | a: 'hello', 25 | b: file('test/kyuukurarin.mp4') 26 | }) 27 | ) 28 | 29 | const contentType = await app 30 | .handle(req('/')) 31 | .then((x) => x.headers.get('content-type')) 32 | 33 | expect(contentType).toStartWith('multipart/form-data') 34 | }) 35 | 36 | it('return Elysia.file', async () => { 37 | const app = new Elysia().get('/', () => 38 | form({ 39 | a: 'hello', 40 | b: file('test/kyuukurarin.mp4') 41 | }) 42 | ) 43 | 44 | const contentType = await app 45 | .handle(req('/')) 46 | .then((x) => x.headers.get('content-type')) 47 | 48 | expect(contentType).toStartWith('multipart/form-data') 49 | }) 50 | 51 | it('validate formdata', async () => { 52 | const app = new Elysia().get( 53 | '/', 54 | () => 55 | form({ 56 | a: 'hello', 57 | b: file('test/kyuukurarin.mp4') 58 | }), 59 | { 60 | response: t.Form({ 61 | a: t.String(), 62 | b: t.File() 63 | }) 64 | } 65 | ) 66 | 67 | const response = await app.handle(req('/')) 68 | 69 | expect(response.status).toBe(200) 70 | expect(response.headers.get('content-type')).toStartWith( 71 | 'multipart/form-data' 72 | ) 73 | }) 74 | 75 | it('return single file', async () => { 76 | const app = new Elysia().get('/', () => file('test/kyuukurarin.mp4')) 77 | 78 | const response = await app.handle(req('/')) 79 | 80 | expect(response.status).toBe(200) 81 | expect(response.headers.get('content-type')).toStartWith('video/mp4') 82 | }) 83 | 84 | it('inline single file', async () => { 85 | const app = new Elysia().get('/', file('test/kyuukurarin.mp4')) 86 | 87 | const response = await app.handle(req('/')) 88 | 89 | expect(response.status).toBe(200) 90 | expect(response.headers.get('content-type')).toStartWith('video/mp4') 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /test/core/mount.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | 3 | import { Elysia } from '../../src' 4 | 5 | describe('Mount', () => { 6 | it('preserve request URL', async () => { 7 | const plugin = new Elysia().get('/', ({ request }) => request.url) 8 | 9 | const app = new Elysia().mount('/mount', plugin.handle) 10 | 11 | expect( 12 | await app 13 | .handle(new Request('http://elysiajs.com/mount/')) 14 | .then((x) => x.text()) 15 | ).toBe('http://elysiajs.com/') 16 | }) 17 | 18 | it('preserve request URL with query', async () => { 19 | const plugin = new Elysia().get('/', ({ request }) => request.url) 20 | 21 | const app = new Elysia().mount('/mount', plugin.handle) 22 | 23 | expect( 24 | await app 25 | .handle(new Request('http://elysiajs.com/mount/?a=1')) 26 | .then((x) => x.text()) 27 | ).toBe('http://elysiajs.com/?a=1') 28 | }) 29 | 30 | it('preserve body', async () => { 31 | const handler = async (req: Request) => { 32 | return new Response(await req.text()) 33 | } 34 | 35 | const app = new Elysia() 36 | .mount('/mount', (req) => handler(req)) 37 | .post('/not-mount', ({ body }) => body) 38 | 39 | const options = { 40 | method: 'POST', 41 | headers: { 42 | 'content-type': 'text/plain' 43 | }, 44 | body: 'sucrose' 45 | } 46 | 47 | const res = await Promise.all([ 48 | app 49 | .handle(new Request('http://elysiajs.com/mount', options)) 50 | .then((x) => x.text()), 51 | app 52 | .handle(new Request('http://elysiajs.com/not-mount', options)) 53 | .then((x) => x.text()) 54 | ]) 55 | 56 | expect(res).toEqual(['sucrose', 'sucrose']) 57 | }) 58 | 59 | it('remove wildcard path', async () => { 60 | const app = new Elysia().mount('/v1/*', (request) => { 61 | return Response.json({ 62 | path: request.url 63 | }) 64 | }) 65 | 66 | const response = await app 67 | .handle(new Request('http://localhost/v1/hello')) 68 | .then((x) => x.json()) 69 | 70 | expect(response).toEqual({ 71 | path: 'http://localhost/hello' 72 | }) 73 | }) 74 | 75 | it('preserve method', async () => { 76 | const app = new Elysia().mount((request) => { 77 | return Response.json({ 78 | method: request.method, 79 | path: request.url 80 | }) 81 | }) 82 | 83 | const response = await app 84 | .handle( 85 | new Request('http://localhost/v1/hello', { 86 | method: 'PUT' 87 | }) 88 | ) 89 | .then((x) => x.json()) 90 | 91 | expect(response).toEqual({ 92 | method: 'PUT', 93 | path: 'http://localhost/v1/hello' 94 | }) 95 | }) 96 | 97 | // https://github.com/elysiajs/elysia/issues/1070 98 | it('preserve method with prefix', async () => { 99 | const app = new Elysia().mount('/v1/*', (request) => { 100 | return Response.json({ 101 | method: request.method, 102 | path: request.url 103 | }) 104 | }) 105 | 106 | const response = await app 107 | .handle( 108 | new Request('http://localhost/v1/hello', { 109 | method: 'PUT' 110 | }) 111 | ) 112 | .then((x) => x.json()) 113 | 114 | expect(response).toEqual({ 115 | method: 'PUT', 116 | path: 'http://localhost/hello' 117 | }) 118 | }) 119 | 120 | it('preserve headers', async () => { 121 | const app = new Elysia().mount((request) => { 122 | // @ts-expect-error Bun has toJSON 123 | return Response.json(request.headers.toJSON()) 124 | }) 125 | 126 | const response = await app 127 | .handle( 128 | new Request('http://localhost/v1/hello', { 129 | method: 'PUT', 130 | headers: { 131 | 'x-test': 'test' 132 | } 133 | }) 134 | ) 135 | .then((x) => x.json()) 136 | 137 | expect(response).toEqual({ 138 | 'x-test': 'test' 139 | }) 140 | }) 141 | }) 142 | -------------------------------------------------------------------------------- /test/core/native-static.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../../src' 2 | import { describe, expect, it } from 'bun:test' 3 | 4 | describe('Native Static Response', () => { 5 | it('work', async () => { 6 | const app = new Elysia().get('/', 'Static Content') 7 | 8 | expect(app.router.response['/'].GET).toBeInstanceOf(Response) 9 | expect(await app.router.response['/'].GET.text()).toEqual('Static Content') 10 | }) 11 | 12 | it('handle plugin', async () => { 13 | const plugin = new Elysia().get('/plugin', 'Plugin') 14 | 15 | const app = new Elysia().use(plugin).get('/', 'Static Content') 16 | 17 | expect(app.router.response['/'].GET).toBeInstanceOf(Response) 18 | expect(await app.router.response['/'].GET.text()).toEqual('Static Content') 19 | 20 | expect(app.router.response['/plugin'].GET).toBeInstanceOf(Response) 21 | expect(await app.router.response['/plugin'].GET.text()).toEqual('Plugin') 22 | }) 23 | 24 | it('handle default header', async () => { 25 | const plugin = new Elysia().get('/plugin', 'Plugin') 26 | 27 | const app = new Elysia() 28 | .headers({ server: 'Elysia' }) 29 | .use(plugin) 30 | .get('/', 'Static Content') 31 | 32 | expect(app.router.response['/'].GET).toBeInstanceOf(Response) 33 | expect(app.router.response['/'].GET.headers.toJSON()).toEqual({ 34 | 'content-type': 'text/plain', 35 | server: 'Elysia' 36 | }) 37 | expect(await app.router.response['/'].GET.text()).toEqual('Static Content') 38 | 39 | expect(app.router.response['/plugin'].GET).toBeInstanceOf(Response) 40 | expect(app.router.response['/plugin'].GET.headers.toJSON()).toEqual({ 41 | 'content-type': 'text/plain', 42 | server: 'Elysia' 43 | }) 44 | expect(await app.router.response['/plugin'].GET.text()).toEqual('Plugin') 45 | }) 46 | 47 | it('turn off by config', async () => { 48 | const app = new Elysia({ nativeStaticResponse: false }).get( 49 | '/', 50 | 'Static Content' 51 | ) 52 | 53 | expect(app.router.response).not.toHaveProperty('/') 54 | }) 55 | 56 | it('handle loose path', async () => { 57 | const plugin = new Elysia().get('/plugin', 'Plugin') 58 | 59 | const app = new Elysia().use(plugin).get('/', 'Static Content') 60 | 61 | expect(app.router.response['/'].GET).toBeInstanceOf(Response) 62 | expect(await app.router.response['/'].GET.text()).toEqual('Static Content') 63 | 64 | expect(app.router.response[''].GET).toBeInstanceOf(Response) 65 | expect(await app.router.response[''].GET.text()).toEqual('Static Content') 66 | 67 | expect(app.router.response['/plugin'].GET).toBeInstanceOf(Response) 68 | expect(await app.router.response['/plugin'].GET.text()).toEqual('Plugin') 69 | 70 | expect(app.router.response['/plugin/'].GET).toBeInstanceOf(Response) 71 | expect(await app.router.response['/plugin/'].GET.text()).toEqual('Plugin') 72 | 73 | const strict = new Elysia({ strictPath: true }) 74 | .use(plugin) 75 | .get('/', 'Static Content') 76 | 77 | expect(strict.router.response['/'].GET).toBeInstanceOf(Response) 78 | expect(await strict.router.response['/'].GET.text()).toEqual( 79 | 'Static Content' 80 | ) 81 | expect(strict.router.response).not.toHaveProperty('') 82 | 83 | expect(strict.router.response['/plugin'].GET).toBeInstanceOf(Response) 84 | expect(await strict.router.response['/plugin'].GET.text()).toEqual('Plugin') 85 | expect(strict.router.response).not.toHaveProperty('/plugin/') 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /test/core/path.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | 5 | describe('handle path with spaces', () => { 6 | it('AOT on: static path', async () => { 7 | const PATH = '/y a y' 8 | 9 | const app = new Elysia().get( 10 | PATH, 11 | () => 'result from a path wirh spaces' 12 | ) 13 | 14 | const response = await app.handle( 15 | new Request(`http://localhost${PATH}`) 16 | ) 17 | 18 | expect(response.status).toBe(200) 19 | }) 20 | 21 | it('AOT off: static path', async () => { 22 | const PATH = '/y a y' 23 | 24 | const app = new Elysia({ aot: false }).get( 25 | PATH, 26 | () => 'result from a path wirh spaces' 27 | ) 28 | 29 | const response = await app.handle( 30 | new Request(`http://localhost${PATH}`) 31 | ) 32 | 33 | expect(response.status).toBe(200) 34 | }) 35 | 36 | it('AOT on: dynamic path', async () => { 37 | const app = new Elysia().get('/y a y/:id', ({ params: { id } }) => id) 38 | 39 | const response = await app.handle( 40 | new Request(`http://localhost/y a y/1`) 41 | ) 42 | 43 | expect(response.status).toBe(200) 44 | expect(await response.text()).toBe('1') 45 | }) 46 | 47 | it('AOT off: dynamic path', async () => { 48 | const app = new Elysia({ aot: false }).get( 49 | '/y a y/:id', 50 | ({ params: { id } }) => id 51 | ) 52 | 53 | const response = await app.handle( 54 | new Request(`http://localhost/y a y/1`) 55 | ) 56 | 57 | expect(response.status).toBe(200) 58 | expect(await response.text()).toBe('1') 59 | }) 60 | 61 | it('AOT on: optional dynamic path', async () => { 62 | const app = new Elysia().get( 63 | '/y a y/:id?', 64 | ({ params: { id } }) => id ?? 0 65 | ) 66 | 67 | const response = await Promise.all( 68 | [ 69 | new Request(`http://localhost/y a y`), 70 | new Request(`http://localhost/y a y/1`) 71 | ].map(app.handle) 72 | ) 73 | 74 | expect(response[0].status).toBe(200) 75 | expect(response[1].status).toBe(200) 76 | 77 | const value = await Promise.all(response.map((x) => x.text())) 78 | 79 | expect(value[0]).toBe('0') 80 | expect(value[1]).toBe('1') 81 | }) 82 | 83 | it('AOT off: optional dynamic path', async () => { 84 | const app = new Elysia({ aot: false }).get( 85 | '/y a y/:id?', 86 | ({ params: { id } }) => id ?? 0 87 | ) 88 | 89 | const response = await Promise.all( 90 | [ 91 | new Request(`http://localhost/y a y`), 92 | new Request(`http://localhost/y a y/1`) 93 | ].map(app.handle) 94 | ) 95 | 96 | expect(response[0].status).toBe(200) 97 | expect(response[1].status).toBe(200) 98 | 99 | const value = await Promise.all(response.map((x) => x.text())) 100 | 101 | expect(value[0]).toBe('0') 102 | expect(value[1]).toBe('1') 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /test/core/redirect.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { Elysia } from '../../src' 3 | import { req } from '../utils' 4 | 5 | describe('Redirect', () => { 6 | it('handles redirect without explicit status', async () => { 7 | const app = new Elysia().get('/', ({ set, redirect }) => 8 | redirect('/hello') 9 | ) 10 | 11 | const res = await app.handle(req('/')) 12 | expect(res.status).toBe(302) 13 | expect(res.headers.get('location')).toBe('/hello') 14 | }) 15 | 16 | it('handles redirect with explicit status', async () => { 17 | const app = new Elysia().get('/', ({ set, redirect }) => 18 | redirect('/hello', 303) 19 | ) 20 | 21 | const res = await app.handle(req('/')) 22 | expect(res.status).toBe(303) 23 | expect(res.headers.get('location')).toBe('/hello') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/core/sanitize.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | 3 | import { Elysia, t } from '../../src' 4 | import { post } from '../utils' 5 | 6 | describe('Sanitize', () => { 7 | it('handle single sanitize', async () => { 8 | const app = new Elysia({ 9 | sanitize: (v) => (v === 'a' ? 'ok' : v) 10 | }).post('/', ({ body }) => body, { 11 | body: t.Object({ 12 | a: t.String(), 13 | b: t.String(), 14 | c: t.String() 15 | }) 16 | }) 17 | 18 | const response = await app 19 | .handle( 20 | post('/', { 21 | a: 'a', 22 | b: 'b', 23 | c: 'c' 24 | }) 25 | ) 26 | .then((x) => x.json()) 27 | 28 | expect(response).toEqual({ a: 'ok', b: 'b', c: 'c' }) 29 | }) 30 | 31 | it('multiple sanitize', async () => { 32 | const app = new Elysia({ 33 | sanitize: [ 34 | (v) => (v === 'a' ? 'ok' : v), 35 | (v) => (v === 'b' ? 'ok' : v) 36 | ] 37 | }).post('/', ({ body }) => body, { 38 | body: t.Object({ 39 | a: t.String(), 40 | b: t.String(), 41 | c: t.String() 42 | }) 43 | }) 44 | 45 | const response = await app 46 | .handle( 47 | post('/', { 48 | a: 'a', 49 | b: 'b', 50 | c: 'c' 51 | }) 52 | ) 53 | .then((x) => x.json()) 54 | 55 | expect(response).toEqual({ a: 'ok', b: 'ok', c: 'c' }) 56 | }) 57 | 58 | it('handle sanitize in plugin from main', async () => { 59 | const plugin = new Elysia().post('/', ({ body }) => body, { 60 | body: t.Object({ 61 | a: t.String(), 62 | b: t.String(), 63 | c: t.String() 64 | }) 65 | }) 66 | 67 | const app = new Elysia({ 68 | sanitize: (v) => (v === 'a' ? 'ok' : v) 69 | }).use(plugin) 70 | 71 | const response = await app 72 | .handle( 73 | post('/', { 74 | a: 'a', 75 | b: 'b', 76 | c: 'c' 77 | }) 78 | ) 79 | .then((x) => x.json()) 80 | 81 | expect(response).toEqual({ a: 'ok', b: 'b', c: 'c' }) 82 | }) 83 | 84 | it('handle top-level string', async () => { 85 | const app = new Elysia({ 86 | sanitize: (v) => (v === 'a' ? 'ok' : v) 87 | }).post('/', ({ body }) => body, { 88 | body: t.String() 89 | }) 90 | 91 | const response = await app 92 | .handle( 93 | new Request('http://localhost', { 94 | method: 'POST', 95 | headers: { 96 | 'Content-Type': 'text/plain' 97 | }, 98 | body: 'a' 99 | }) 100 | ) 101 | .then((x) => x.text()) 102 | 103 | expect(response).toBe('ok') 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /test/core/stop.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | 3 | import { Elysia } from '../../src' 4 | 5 | describe('Stop', () => { 6 | it('shuts down the server when stop(true) is called', async () => { 7 | const app = new Elysia() 8 | app.get('/health', 'hi') 9 | 10 | const port = 8080 11 | const server = app.listen(port) 12 | 13 | await fetch(`http://localhost:${port}/health`) 14 | 15 | await server.stop(true) 16 | 17 | // Check if the server is still running 18 | try { 19 | await fetch(`http://localhost:${port}/health`) 20 | throw new Error('Server is still running after teardown') 21 | } catch (error) { 22 | expect((error as Error).message).toContain('Unable to connect') 23 | } 24 | }) 25 | 26 | it('does not shut down the server when stop(false) is called', async () => { 27 | const app = new Elysia() 28 | app.get('/health', 'hi') 29 | 30 | const port = 8081 31 | const server = app.listen(port) 32 | 33 | await fetch(`http://localhost:${port}/health`) 34 | 35 | await server.stop(false) 36 | 37 | // Check if the server is still running 38 | try { 39 | const response = await fetch(`http://localhost:${port}/health`) 40 | expect(response.status).toBe(200) 41 | expect(await response.text()).toBe('hi') 42 | } catch (error) { 43 | throw new Error('Server unexpectedly shut down') 44 | } 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /test/extends/error.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Elysia, t } from '../../src' 3 | 4 | import { describe, expect, it } from 'bun:test' 5 | import { req } from '../utils' 6 | 7 | class CustomError extends Error { 8 | constructor() { 9 | super() 10 | } 11 | } 12 | 13 | const getErrors = (app: Elysia) => 14 | // @ts-ignore 15 | Object.keys(app.definitions.error) 16 | 17 | describe('Error', () => { 18 | it('add single', async () => { 19 | const app = new Elysia().error('CUSTOM_ERROR', CustomError) 20 | 21 | expect(getErrors(app)).toEqual(['CUSTOM_ERROR']) 22 | }) 23 | 24 | it('add multiple', async () => { 25 | const app = new Elysia() 26 | .error('CUSTOM_ERROR', CustomError) 27 | .error('CUSTOM_ERROR_2', CustomError) 28 | 29 | expect(getErrors(app)).toEqual(['CUSTOM_ERROR', 'CUSTOM_ERROR_2']) 30 | }) 31 | 32 | it('add object', async () => { 33 | const app = new Elysia().error({ 34 | CUSTOM_ERROR: CustomError, 35 | CUSTOM_ERROR_2: CustomError 36 | }) 37 | 38 | expect(getErrors(app)).toEqual(['CUSTOM_ERROR', 'CUSTOM_ERROR_2']) 39 | }) 40 | 41 | // it('remap error', async () => { 42 | // const app = new Elysia() 43 | // .error({ 44 | // CUSTOM_ERROR: CustomError, 45 | // CUSTOM_ERROR_2: CustomError 46 | // }) 47 | // .error(({ CUSTOM_ERROR, ...rest }) => ({ 48 | // ...rest, 49 | // CUSTOM_ERROR_3: CustomError 50 | // })) 51 | 52 | // expect(getErrors(app)).toEqual(['CUSTOM_ERROR', 'CUSTOM_ERROR_2']) 53 | // }) 54 | 55 | it('inherits functional plugin', async () => { 56 | const plugin = (app: Elysia) => app.error('CUSTOM_ERROR', CustomError) 57 | 58 | const app = new Elysia().use(plugin) 59 | 60 | expect(getErrors(app)).toEqual(['CUSTOM_ERROR']) 61 | }) 62 | 63 | it('inherits instance plugin', async () => { 64 | const plugin = new Elysia().error('CUSTOM_ERROR', CustomError) 65 | 66 | const app = new Elysia().use(plugin) 67 | 68 | expect(getErrors(app)).toEqual(['CUSTOM_ERROR']) 69 | }) 70 | 71 | it('preserve status code base on error if not set', async () => { 72 | const app = new Elysia().onError(({ code }) => { 73 | if (code === 'NOT_FOUND') return 'UwU' 74 | }) 75 | 76 | const response = await app.handle(req('/not/found')) 77 | 78 | expect(await response.text()).toBe('UwU') 79 | expect(response.status).toBe(404) 80 | }) 81 | 82 | it('validation error should be application/json', async () => { 83 | // @ts-expect-error 84 | const app = new Elysia().get('/', () => '1', { 85 | response: t.Null() 86 | }) 87 | 88 | const response = await app.handle(req('/')) 89 | 90 | expect(response.status).toBe(422) 91 | expect(response.headers.get('content-type')).toBe('application/json') 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /test/hoc/index.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { describe, it, expect } from 'bun:test' 3 | import { Elysia } from '../../src' 4 | import { req } from '../utils' 5 | 6 | describe('HOC', () => { 7 | it('work', async () => { 8 | let called = 0 9 | 10 | const app = new Elysia() 11 | .wrap((fn) => { 12 | called++ 13 | 14 | return fn 15 | }) 16 | .get('/', () => 'ok') 17 | 18 | await app.handle(req('/')) 19 | 20 | expect(called).toBe(1) 21 | }) 22 | 23 | it('deduplicate', async () => { 24 | const plugin = new Elysia().wrap((fn) => fn) 25 | const plugin2 = new Elysia({ name: 'plugin2' }).wrap((fn) => fn) 26 | 27 | const app = new Elysia() 28 | .use(plugin) 29 | .use(plugin) 30 | .use(plugin) 31 | .use(plugin2) 32 | .use(plugin2) 33 | .get('/', () => 'ok') 34 | 35 | // @ts-expect-error 36 | expect(app.extender.higherOrderFunctions.length).toBe(2) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/images/aris-yuzu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/elysia/75e9366d350a6d6ba02f8ceb124f5c29e3b9dd5d/test/images/aris-yuzu.jpg -------------------------------------------------------------------------------- /test/images/fake.jpg: -------------------------------------------------------------------------------- 1 | evil_exe 2 | -------------------------------------------------------------------------------- /test/images/kozeki-ui.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/elysia/75e9366d350a6d6ba02f8ceb124f5c29e3b9dd5d/test/images/kozeki-ui.webp -------------------------------------------------------------------------------- /test/images/midori.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/elysia/75e9366d350a6d6ba02f8ceb124f5c29e3b9dd5d/test/images/midori.png -------------------------------------------------------------------------------- /test/images/millenium.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/elysia/75e9366d350a6d6ba02f8ceb124f5c29e3b9dd5d/test/images/millenium.jpg -------------------------------------------------------------------------------- /test/kyuukurarin.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/elysia/75e9366d350a6d6ba02f8ceb124f5c29e3b9dd5d/test/kyuukurarin.mp4 -------------------------------------------------------------------------------- /test/lifecycle/after-handle.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | import { req } from '../utils' 5 | 6 | describe('After Handle', () => { 7 | it('work global', async () => { 8 | const app = new Elysia().onAfterHandle(() => 'A').get('/', () => 'NOOP') 9 | 10 | const res = await app.handle(req('/')).then((x) => x.text()) 11 | 12 | expect(res).toBe('A') 13 | }) 14 | 15 | it('work local', async () => { 16 | const app = new Elysia().get('/', () => 'NOOP', { 17 | afterHandle() { 18 | return 'A' 19 | } 20 | }) 21 | 22 | const res = await app.handle(req('/')).then((x) => x.text()) 23 | 24 | expect(res).toBe('A') 25 | }) 26 | 27 | it('inherits from plugin', async () => { 28 | const transformType = new Elysia().onAfterHandle( 29 | { as: 'global' }, 30 | ({ response }) => { 31 | if (response === 'string') return 'number' 32 | } 33 | ) 34 | 35 | const app = new Elysia() 36 | .use(transformType) 37 | .get('/id/:id', ({ params: { id } }) => typeof id) 38 | 39 | const res = await app.handle(req('/id/1')) 40 | 41 | expect(await res.text()).toBe('number') 42 | }) 43 | 44 | it('not inherits plugin on local', async () => { 45 | const transformType = new Elysia().onAfterHandle(({ response }) => { 46 | if (response === 'string') return 'number' 47 | }) 48 | 49 | const app = new Elysia() 50 | .use(transformType) 51 | .get('/id/:id', ({ params: { id } }) => typeof id) 52 | 53 | const res = await app.handle(req('/id/1')) 54 | 55 | expect(await res.text()).toBe('string') 56 | }) 57 | 58 | it('register using on', async () => { 59 | const app = new Elysia() 60 | .on('transform', (request) => { 61 | if (request.params?.id) request.params.id = +request.params.id 62 | }) 63 | .get('/id/:id', ({ params: { id } }) => typeof id) 64 | 65 | const res = await app.handle(req('/id/1')) 66 | 67 | expect(await res.text()).toBe('number') 68 | }) 69 | 70 | it('after handle in order', async () => { 71 | let order = [] 72 | 73 | const app = new Elysia() 74 | .onAfterHandle(() => { 75 | order.push('A') 76 | }) 77 | .onAfterHandle(() => { 78 | order.push('B') 79 | }) 80 | .get('/', () => '') 81 | 82 | await app.handle(req('/')) 83 | 84 | expect(order).toEqual(['A', 'B']) 85 | }) 86 | 87 | it('accept response', async () => { 88 | const app = new Elysia().get('/', () => 'NOOP', { 89 | afterHandle({ response }) { 90 | return response 91 | } 92 | }) 93 | 94 | const res = await app.handle(req('/')).then((x) => x.text()) 95 | 96 | expect(res).toBe('NOOP') 97 | }) 98 | 99 | it('as global', async () => { 100 | const called = [] 101 | 102 | const plugin = new Elysia() 103 | .onAfterHandle({ as: 'global' }, ({ path }) => { 104 | called.push(path) 105 | }) 106 | .get('/inner', () => 'NOOP') 107 | 108 | const app = new Elysia().use(plugin).get('/outer', () => 'NOOP') 109 | 110 | const res = await Promise.all([ 111 | app.handle(req('/inner')), 112 | app.handle(req('/outer')) 113 | ]) 114 | 115 | expect(called).toEqual(['/inner', '/outer']) 116 | }) 117 | 118 | it('as local', async () => { 119 | const called = [] 120 | 121 | const plugin = new Elysia() 122 | .onAfterHandle({ as: 'local' }, ({ path }) => { 123 | called.push(path) 124 | }) 125 | .get('/inner', () => 'NOOP') 126 | 127 | const app = new Elysia().use(plugin).get('/outer', () => 'NOOP') 128 | 129 | const res = await Promise.all([ 130 | app.handle(req('/inner')), 131 | app.handle(req('/outer')) 132 | ]) 133 | 134 | expect(called).toEqual(['/inner']) 135 | }) 136 | 137 | it('support array', async () => { 138 | let total = 0 139 | 140 | const app = new Elysia() 141 | .onAfterHandle([ 142 | () => { 143 | total++ 144 | }, 145 | () => { 146 | total++ 147 | } 148 | ]) 149 | .get('/', () => 'NOOP') 150 | 151 | const res = await app.handle(req('/')) 152 | 153 | expect(total).toEqual(2) 154 | }) 155 | }) 156 | -------------------------------------------------------------------------------- /test/lifecycle/hook-types.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../../src' 2 | import { req } from '../../test/utils' 3 | import { describe, it, expect } from 'bun:test' 4 | 5 | // ? Some hooks type are already tested in there respective lifecycle tests 6 | // ? This files is to verify weird behavior of hook type in general 7 | describe('Hook Types', () => { 8 | it('should clone function to prevent hookType link', async () => { 9 | const plugin = new Elysia({ name: 'plugin' }).derive( 10 | { as: 'scoped' }, 11 | () => { 12 | return { id: 1 } 13 | } 14 | ) 15 | 16 | expect(plugin.event.transform[0].scope).toBe('scoped') 17 | 18 | const a = new Elysia().use(plugin).get('/foo', ({ id }) => { 19 | return { id, name: 'foo' } 20 | }) 21 | 22 | expect(plugin.event.transform[0].scope).toBe('scoped') 23 | 24 | const b = new Elysia().use(plugin).get('/bar', ({ id }) => { 25 | return { id, name: 'bar' } 26 | }) 27 | 28 | expect(plugin.event.transform[0].scope).toBe('scoped') 29 | 30 | const [res1, res2] = await Promise.all([ 31 | a.handle(req('/foo')).then((x) => x.json()), 32 | b.handle(req('/bar')).then((x) => x.json()) 33 | ]) 34 | 35 | expect(res1).toEqual({ id: 1, name: 'foo' }) 36 | expect(res2).toEqual({ id: 1, name: 'bar' }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/lifecycle/request.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | import { req, delay } from '../utils' 5 | 6 | describe('On Request', () => { 7 | it('inject headers to response', async () => { 8 | const app = new Elysia() 9 | .onRequest(({ set }) => { 10 | set.headers['Access-Control-Allow-Origin'] = '*' 11 | }) 12 | .get('/', () => 'hi') 13 | 14 | const res = await app.handle(req('/')) 15 | 16 | expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*') 17 | }) 18 | 19 | it('handle async', async () => { 20 | const app = new Elysia() 21 | .onRequest(async ({ set }) => { 22 | await delay(5) 23 | set.headers.name = 'llama' 24 | }) 25 | .get('/', () => 'hi') 26 | 27 | const res = await app.handle(req('/')) 28 | 29 | expect(res.headers.get('name')).toBe('llama') 30 | }) 31 | 32 | it('early return', async () => { 33 | const app = new Elysia() 34 | .onRequest(({ set }) => { 35 | set.status = 401 36 | return 'Unauthorized' 37 | }) 38 | .get('/', () => { 39 | console.log("This shouldn't be run") 40 | return "You shouldn't see this" 41 | }) 42 | 43 | const res = await app.handle(req('/')) 44 | expect(await res.text()).toBe('Unauthorized') 45 | expect(res.status).toBe(401) 46 | }) 47 | 48 | it('support array', async () => { 49 | let total = 0 50 | 51 | const app = new Elysia() 52 | .onRequest([ 53 | () => { 54 | total++ 55 | }, 56 | () => { 57 | total++ 58 | } 59 | ]) 60 | .get('/', () => 'NOOP') 61 | 62 | const res = await app.handle(req('/')) 63 | 64 | expect(total).toEqual(2) 65 | }) 66 | 67 | it('request in order', async () => { 68 | let order = [] 69 | 70 | const app = new Elysia() 71 | .onRequest(() => { 72 | order.push('A') 73 | }) 74 | .onRequest(() => { 75 | order.push('B') 76 | }) 77 | .get('/', () => '') 78 | 79 | await app.handle(req('/')) 80 | 81 | expect(order).toEqual(['A', 'B']) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /test/lifecycle/response.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | import { req } from '../utils' 5 | 6 | describe('On After Response', () => { 7 | it('inherits set if Response is return', async () => { 8 | const app = new Elysia() 9 | .onAfterResponse(({ set }) => { 10 | expect(set.status).toBe(401) 11 | }) 12 | .onError(() => { 13 | return new Response('a', { 14 | status: 401, 15 | headers: { 16 | awd: 'b' 17 | } 18 | }) 19 | }) 20 | 21 | await app.handle(req('/')) 22 | }) 23 | 24 | it('response in order', async () => { 25 | let order = [] 26 | 27 | const app = new Elysia() 28 | .onAfterResponse(() => { 29 | order.push('A') 30 | }) 31 | .onAfterResponse(() => { 32 | order.push('B') 33 | }) 34 | .get('/', () => '') 35 | 36 | await app.handle(req('/')) 37 | 38 | expect(order).toEqual(['A', 'B']) 39 | }) 40 | 41 | // it('inherits from plugin', async () => { 42 | // const transformType = new Elysia().onResponse( 43 | // { as: 'global' }, 44 | // ({ response }) => { 45 | // if (response === 'string') return 'number' 46 | // } 47 | // ) 48 | 49 | // const app = new Elysia() 50 | // .use(transformType) 51 | // .get('/id/:id', ({ params: { id } }) => typeof id) 52 | 53 | // const res = await app.handle(req('/id/1')) 54 | 55 | // expect(await res.text()).toBe('number') 56 | // }) 57 | 58 | it('as global', async () => { 59 | const called = [] 60 | 61 | const plugin = new Elysia() 62 | .onAfterResponse({ as: 'global' }, ({ path }) => { 63 | called.push(path) 64 | }) 65 | .get('/inner', () => 'NOOP') 66 | 67 | const app = new Elysia().use(plugin).get('/outer', () => 'NOOP') 68 | 69 | const res = await Promise.all([ 70 | app.handle(req('/inner')), 71 | app.handle(req('/outer')) 72 | ]) 73 | 74 | expect(called).toEqual(['/inner', '/outer']) 75 | }) 76 | 77 | it('as local', async () => { 78 | const called = [] 79 | 80 | const plugin = new Elysia() 81 | .onAfterResponse({ as: 'local' }, ({ path }) => { 82 | called.push(path) 83 | }) 84 | .get('/inner', () => 'NOOP') 85 | 86 | const app = new Elysia().use(plugin).get('/outer', () => 'NOOP') 87 | 88 | const res = await Promise.all([ 89 | app.handle(req('/inner')), 90 | app.handle(req('/outer')) 91 | ]) 92 | 93 | expect(called).toEqual(['/inner']) 94 | }) 95 | 96 | it('support array', async () => { 97 | let total = 0 98 | 99 | const app = new Elysia() 100 | .onAfterHandle([ 101 | () => { 102 | total++ 103 | }, 104 | () => { 105 | total++ 106 | } 107 | ]) 108 | .get('/', () => 'NOOP') 109 | 110 | const res = await app.handle(req('/')) 111 | 112 | expect(total).toEqual(2) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /test/modules.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | export const lazy = async (app: Elysia) => app.get('/lazy', () => 'lazy') 4 | 5 | export default lazy 6 | -------------------------------------------------------------------------------- /test/node/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json -------------------------------------------------------------------------------- /test/node/cjs/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/elysia/75e9366d350a6d6ba02f8ceb124f5c29e3b9dd5d/test/node/cjs/bun.lockb -------------------------------------------------------------------------------- /test/node/cjs/index.js: -------------------------------------------------------------------------------- 1 | if ('Bun' in globalThis) { 2 | throw new Error('❌ Use Node.js to run this test!') 3 | } 4 | 5 | const { Elysia } = require('elysia') 6 | 7 | const app = new Elysia().get('/', () => 'Node.js') 8 | 9 | const main = async () => { 10 | const response = await app.handle(new Request('http://localhost')) 11 | 12 | if ((await response.text()) !== 'Node.js') { 13 | throw new Error('❌ CommonJS Node.js failed') 14 | } 15 | 16 | console.log('✅ CommonJS Node.js works!') 17 | } 18 | main() 19 | -------------------------------------------------------------------------------- /test/node/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs", 3 | "dependencies": { 4 | "elysia": "../../.." 5 | } 6 | } -------------------------------------------------------------------------------- /test/node/cjs/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.1' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | elysia: 9 | specifier: ../../.. 10 | version: link:../../.. 11 | -------------------------------------------------------------------------------- /test/node/esm/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/elysia/75e9366d350a6d6ba02f8ceb124f5c29e3b9dd5d/test/node/esm/bun.lockb -------------------------------------------------------------------------------- /test/node/esm/index.js: -------------------------------------------------------------------------------- 1 | import { Elysia } from 'elysia' 2 | 3 | if ('Bun' in globalThis) { 4 | throw new Error('❌ Use Node.js to run this test!') 5 | } 6 | 7 | const app = new Elysia().get('/', () => 'Node.js') 8 | 9 | const response = await app.handle(new Request('http://localhost')) 10 | 11 | if ((await response.text()) !== 'Node.js') { 12 | throw new Error('❌ ESM Node.js failed') 13 | } 14 | 15 | console.log('✅ ESM Node.js works!') 16 | -------------------------------------------------------------------------------- /test/node/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "elysia": "../../.." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/node/esm/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.1' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | elysia: 9 | specifier: ../../.. 10 | version: link:../../.. 11 | -------------------------------------------------------------------------------- /test/plugins/affix.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { Elysia, t } from '../../src' 3 | import { describe, it, expect } from 'bun:test' 4 | 5 | const setup = new Elysia() 6 | .decorate('decorate', 'decorate') 7 | .state('state', 'state') 8 | .model('model', t.String()) 9 | .error('error', Error) 10 | 11 | describe('affix', () => { 12 | it('should add prefix to all decorators, states, models, and errors', () => { 13 | const app = new Elysia().use(setup).affix('prefix', 'all', 'p') 14 | 15 | expect(app.singleton.decorator).toHaveProperty('pDecorate') 16 | expect(app.singleton.store).toHaveProperty('pState') 17 | 18 | expect(app.definitions.type).toHaveProperty('pModel') 19 | 20 | expect(app.definitions.error).toHaveProperty('pError') 21 | }) 22 | 23 | it('should add suffix to all decorators, states, models, and errors', () => { 24 | const app = new Elysia().use(setup).affix('suffix', 'all', 'p') 25 | 26 | expect(app.singleton.decorator).toHaveProperty('decorateP') 27 | 28 | expect(app.singleton.store).toHaveProperty('stateP') 29 | 30 | expect(app.definitions.type).toHaveProperty('modelP') 31 | 32 | expect(app.definitions.error).toHaveProperty('errorP') 33 | }) 34 | 35 | it('should add suffix to all states', () => { 36 | const app = new Elysia().use(setup).suffix('state', 'p') 37 | 38 | expect(app.singleton.store).toHaveProperty('stateP') 39 | }) 40 | 41 | it('should add prefix to all decorators and errors', () => { 42 | const app = new Elysia() 43 | .use(setup) 44 | .prefix('decorator', 'p') 45 | .prefix('error', 'p') 46 | 47 | expect(app.singleton.decorator).toHaveProperty('pDecorate') 48 | 49 | expect(app.definitions.error).toHaveProperty('pError') 50 | }) 51 | 52 | it('should add suffix to all decorators and errors', () => { 53 | const app = new Elysia() 54 | .use(setup) 55 | .suffix('decorator', 'p') 56 | .suffix('error', 'p') 57 | 58 | expect(app.singleton.decorator).toHaveProperty('decorateP') 59 | 60 | expect(app.definitions.error).toHaveProperty('errorP') 61 | }) 62 | 63 | it('should add prefix to all models', () => { 64 | const app = new Elysia().use(setup).prefix('model', 'p') 65 | 66 | expect(app.definitions.type).toHaveProperty('pModel') 67 | }) 68 | 69 | it('should add suffix to all models', () => { 70 | const app = new Elysia().use(setup).affix('suffix', 'model', 'p') 71 | 72 | expect(app.definitions.type).toHaveProperty('modelP') 73 | }) 74 | 75 | it('should skip on empty', () => { 76 | const app = new Elysia().use(setup).suffix('all', '') 77 | 78 | expect(app.singleton.decorator).toHaveProperty('decorate') 79 | expect(app.singleton.store).toHaveProperty('state') 80 | 81 | expect(app.definitions.type).toHaveProperty('model') 82 | 83 | expect(app.definitions.error).toHaveProperty('error') 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /test/plugins/error-propagation.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { Elysia } from '../../src' 3 | import { req } from '../utils' 4 | 5 | describe('Error correctly passed to outer elysia instance', () => { 6 | it('Global error handler is run', async () => { 7 | let globalHandlerRun = false 8 | 9 | const mainApp = new Elysia().onError(() => { 10 | globalHandlerRun = true 11 | return 'Fail' 12 | }) 13 | 14 | const plugin = new Elysia().get('/foo', () => { 15 | throw new Error('Error') 16 | }) 17 | 18 | mainApp.use(plugin) 19 | 20 | const res = await (await mainApp.handle(req('/foo'))).text() 21 | 22 | expect(res).toBe('Fail') 23 | expect(globalHandlerRun).toBeTrue() 24 | }) 25 | 26 | it('Plugin global handler is executed before plugin handler', async () => { 27 | //I would expect the plugin error handler to be executed 28 | let globalHandlerRun = false 29 | let localHandlerRun = false 30 | 31 | const plugin = new Elysia({ 32 | prefix: '/a' 33 | }) 34 | .onError({ as: 'global' }, () => { 35 | localHandlerRun = true 36 | return 'FailPlugin' 37 | }) 38 | .get('/foo', () => { 39 | throw new Error('Error') 40 | }) 41 | 42 | const mainApp = new Elysia() 43 | .onError(() => { 44 | globalHandlerRun = true 45 | 46 | return 'Fail' 47 | }) 48 | .use(plugin) 49 | 50 | const res = await mainApp.handle(req('/a/foo')).then((x) => x.text()) 51 | 52 | expect(res).toBe('Fail') 53 | expect(localHandlerRun).toBeFalse() 54 | expect(globalHandlerRun).toBeTrue() 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/plugins/plugin.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | import { req } from '../utils' 5 | 6 | describe('Plugin', () => { 7 | it('await async nested plugin', async () => { 8 | const yay = async () => { 9 | await Bun.sleep(2) 10 | 11 | return new Elysia({ name: 'yay' }).get('/yay', 'yay') 12 | } 13 | 14 | const wrapper = new Elysia({ name: 'wrapper' }).use(yay()) 15 | 16 | const app = new Elysia().use(wrapper) 17 | 18 | await app.modules 19 | 20 | const response = await app.handle(req('/yay')) 21 | 22 | expect(response.status).toBe(200) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/production/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../../src' 2 | import { describe, it, expect, beforeEach } from 'bun:test' 3 | 4 | describe('NODE_ENV=production', () => { 5 | beforeEach(() => { 6 | process.env.NODE_ENV = 'production' 7 | }) 8 | 9 | it('omit error summary', async () => { 10 | const app = new Elysia() 11 | .post('/', () => 'yay', { 12 | body: t.Object({ 13 | name: t.String() 14 | }) 15 | }) 16 | 17 | const response = await app.handle( 18 | new Request('http://localhost/', { 19 | method: 'POST', 20 | body: '' 21 | }) 22 | ) 23 | 24 | const text = await response.text() 25 | expect(text).not.toEqual( 26 | 'Right side of assignment cannot be destructured' 27 | ) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/response/custom-response.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../../src' 2 | import { describe, expect, it } from 'bun:test' 3 | import { req } from '../utils' 4 | 5 | 6 | class CustomResponse extends Response { } 7 | 8 | describe('Custom Response Type', () => { 9 | it('returns custom response when set headers is not empty', async () => { 10 | const app = new Elysia() 11 | .get('/', ({ set }) => { 12 | set.headers['X-POWERED-BY'] = 'Elysia' 13 | return new CustomResponse('Shuba Shuba', { 14 | headers: { 15 | duck: 'shuba duck' 16 | }, 17 | status: 418 18 | }) 19 | }) 20 | 21 | const response = await app.handle(req('/')) 22 | 23 | expect(await response.text()).toBe('Shuba Shuba') 24 | expect(response.headers.get('duck')).toBe('shuba duck') 25 | expect(response.headers.get('X-POWERED-BY')).toBe('Elysia') 26 | expect(response.status).toBe(418) 27 | }) 28 | 29 | it('returns custom response when set headers is empty', async () => { 30 | const app = new Elysia() 31 | .get('/', () => { 32 | return new CustomResponse('Shuba Shuba') 33 | }) 34 | 35 | const response = await app.handle(req('/')) 36 | 37 | expect(await response.text()).toBe('Shuba Shuba') 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/response/headers.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | import { req } from '../utils' 5 | 6 | describe('Response Headers', () => { 7 | it('add response headers', async () => { 8 | const app = new Elysia().get('/', ({ set }) => { 9 | set.headers['x-powered-by'] = 'Elysia' 10 | 11 | return 'Hi' 12 | }) 13 | const res = await app.handle(req('/')) 14 | 15 | expect(res.headers.get('x-powered-by')).toBe('Elysia') 16 | }) 17 | 18 | it('add headers from hook', async () => { 19 | const app = new Elysia() 20 | .onTransform(({ set }) => { 21 | set.headers['x-powered-by'] = 'Elysia' 22 | }) 23 | .get('/', () => 'Hi') 24 | const res = await app.handle(req('/')) 25 | 26 | expect(res.headers.get('x-powered-by')).toBe('Elysia') 27 | }) 28 | 29 | it('add headers from plugin', async () => { 30 | const plugin = (app: Elysia) => 31 | app.onTransform(({ set }) => { 32 | set.headers['x-powered-by'] = 'Elysia' 33 | }) 34 | 35 | const app = new Elysia().use(plugin).get('/', () => 'Hi') 36 | const res = await app.handle(req('/')) 37 | 38 | expect(res.headers.get('x-powered-by')).toBe('Elysia') 39 | }) 40 | 41 | it('add headers to Response', async () => { 42 | const app = new Elysia() 43 | .onTransform(({ set }) => { 44 | set.headers['x-powered-by'] = 'Elysia' 45 | }) 46 | .get('/', () => new Response('Hi')) 47 | const res = await app.handle(req('/')) 48 | 49 | expect(res.headers.get('x-powered-by')).toBe('Elysia') 50 | }) 51 | 52 | it('add status to Response', async () => { 53 | const app = new Elysia().get('/', ({ set }) => { 54 | set.status = 401 55 | 56 | return 'Hi' 57 | }) 58 | 59 | const res = await app.handle(req('/')) 60 | 61 | expect(await res.text()).toBe('Hi') 62 | expect(res.status).toBe(401) 63 | }) 64 | 65 | it('create static header', async () => { 66 | const app = new Elysia() 67 | .headers({ 68 | 'x-powered-by': 'Elysia' 69 | }) 70 | .get('/', () => 'hi') 71 | 72 | const headers = await app.handle(req('/')).then((x) => x.headers) 73 | 74 | expect(headers.get('x-powered-by')).toBe('Elysia') 75 | }) 76 | 77 | it('accept header from plugin', async () => { 78 | const plugin = new Elysia().headers({ 79 | 'x-powered-by': 'Elysia' 80 | }) 81 | 82 | const app = new Elysia().use(plugin).get('/', () => 'hi') 83 | 84 | const headers = await app.handle(req('/')).then((x) => x.headers) 85 | 86 | expect(headers.get('x-powered-by')).toBe('Elysia') 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /test/response/redirect.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | import { req } from '../utils' 5 | 6 | describe('Response Redirect', () => { 7 | it('handle redirect', async () => { 8 | const app = new Elysia().get('/', ({ redirect }) => redirect('/skadi')) 9 | 10 | const { headers, status } = await app.handle(req('/')) 11 | 12 | expect(status).toBe(302) 13 | // @ts-expect-error 14 | expect(headers.toJSON()).toEqual({ 15 | location: '/skadi' 16 | }) 17 | }) 18 | 19 | it('handle redirect status', async () => { 20 | const app = new Elysia().get('/', ({ redirect }) => 21 | redirect('/skadi', 301) 22 | ) 23 | 24 | const { headers, status } = await app.handle(req('/')) 25 | 26 | expect(status).toBe(301) 27 | // @ts-expect-error 28 | expect(headers.toJSON()).toEqual({ 29 | location: '/skadi' 30 | }) 31 | }) 32 | 33 | it('add set.headers to redirect', async () => { 34 | const app = new Elysia().get('/', ({ redirect, set }) => { 35 | set.headers.alias = 'Abyssal Hunter' 36 | 37 | return redirect('/skadi') 38 | }) 39 | 40 | const { headers, status } = await app.handle(req('/')) 41 | 42 | expect(status).toBe(302) 43 | // @ts-expect-error 44 | expect(headers.toJSON()).toEqual({ 45 | location: '/skadi', 46 | alias: 'Abyssal Hunter' 47 | }) 48 | }) 49 | 50 | it('set multiple cookie on redirect', async () => { 51 | const app = new Elysia().get( 52 | '/', 53 | ({ cookie: { name, name2 }, redirect }) => { 54 | name.value = 'a' 55 | name2.value = 'b' 56 | 57 | return redirect('/skadi') 58 | } 59 | ) 60 | 61 | const { headers, status } = await app.handle(req('/')) 62 | 63 | expect(status).toBe(302) 64 | // @ts-expect-error 65 | expect(headers.toJSON()).toEqual({ 66 | location: '/skadi', 67 | 'set-cookie': ['name=a; Path=/', 'name2=b; Path=/'] 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /test/response/static.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | 3 | import { Elysia } from '../../src' 4 | import { req } from '../utils' 5 | 6 | describe('Static Content', () => { 7 | it('work', async () => { 8 | const app = new Elysia().get('/', 'Static Content') 9 | 10 | const response = await app.handle(req('/')).then((x) => x.text()) 11 | 12 | expect(response).toBe('Static Content') 13 | }) 14 | 15 | it('handle onRequest', async () => { 16 | const app = new Elysia() 17 | .onRequest(() => 'request') 18 | .get('/', 'Static Content') 19 | 20 | const response = await app.handle(req('/')).then((x) => x.text()) 21 | 22 | expect(response).toBe('request') 23 | }) 24 | 25 | it('inline life-cycle', async () => { 26 | const app = new Elysia().get('/', 'Static Content', { 27 | beforeHandle() { 28 | return 'beforeHandle' 29 | } 30 | }) 31 | 32 | const response = await app.handle(req('/')).then((x) => x.text()) 33 | 34 | expect(response).toBe('beforeHandle') 35 | }) 36 | 37 | it('mutate context', async () => { 38 | const app = new Elysia().get('/', 'Static Content', { 39 | beforeHandle({ set }) { 40 | set.headers['X-Powered-By'] = 'Elysia' 41 | } 42 | }) 43 | 44 | const headers = await app.handle(req('/')).then((x) => x.headers) 45 | 46 | expect(headers.get('X-Powered-By')).toBe('Elysia') 47 | }) 48 | 49 | it('set default header', async () => { 50 | const app = new Elysia() 51 | .headers({ 52 | 'X-Powered-By': 'Elysia' 53 | }) 54 | .get('/', 'Static Content') 55 | 56 | const headers = await app.handle(req('/')).then((x) => x.headers) 57 | 58 | expect(headers.get('X-Powered-By')).toBe('Elysia') 59 | }) 60 | 61 | it('handle errror after routing', async () => { 62 | const app = new Elysia().get('/', 'Static Content', { 63 | beforeHandle() { 64 | throw new Error('error') 65 | }, 66 | error() { 67 | return 'handled' 68 | } 69 | }) 70 | 71 | const response = await app.handle(req('/')).then((x) => x.text()) 72 | 73 | expect(response).toBe('handled') 74 | }) 75 | 76 | it('handle errror after routing', async () => { 77 | const app = new Elysia() 78 | .onError(() => 'handled') 79 | .onRequest(() => { 80 | throw new Error('error') 81 | }) 82 | .get('/', 'Static Content') 83 | 84 | const response = await app.handle(req('/')).then((x) => x.text()) 85 | 86 | expect(response).toBe('handled') 87 | }) 88 | 89 | it('clone content', async () => { 90 | const app = new Elysia().get('/', 'Static Content', { 91 | beforeHandle({ set }) { 92 | set.headers['X-Powered-By'] = 'Elysia' 93 | } 94 | }) 95 | 96 | await app.handle(req('/')) 97 | await app.handle(req('/')) 98 | const headers = await app.handle(req('/')).then((x) => x.headers) 99 | 100 | expect(headers.get('X-Powered-By')).toBe('Elysia') 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /test/sucrose/bracket-pair-range-reverse.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | 3 | import { bracketPairRangeReverse } from '../../src/sucrose' 4 | 5 | describe('bracket pair range reverse', () => { 6 | it('return the correct range when given a string with a single bracket pair', () => { 7 | const parameter = 'hello: { world: { a } }, elysia' 8 | const result = bracketPairRangeReverse(parameter) 9 | expect(result).toEqual([7, 23]) 10 | }) 11 | 12 | it('return the correct range when given a string with nested bracket pairs', () => { 13 | const parameter = 'hello: { world: { a } }, elysia' 14 | const result = bracketPairRangeReverse(parameter) 15 | expect(result).toEqual([7, 23]) 16 | }) 17 | 18 | it('return [-1, 0] when given a string without any bracket pairs', () => { 19 | const parameter = 'hello, elysia' 20 | const result = bracketPairRangeReverse(parameter) 21 | expect(result).toEqual([-1, 0]) 22 | }) 23 | 24 | it('return [0, 1] when given a string with a single opening bracket at the beginning', () => { 25 | const parameter = '{hello, elysia' 26 | const result = bracketPairRangeReverse(parameter) 27 | expect(result).toEqual([-1, 0]) 28 | }) 29 | 30 | it('return [parameter.length - 1, parameter.length] when given a string with a single closing bracket at the end', () => { 31 | const parameter = 'hello, elysia}' 32 | const result = bracketPairRangeReverse(parameter) 33 | expect(result).toEqual([-1, 0]) 34 | }) 35 | 36 | it('return [-1, 0] when given an empty string', () => { 37 | const parameter = '' 38 | const result = bracketPairRangeReverse(parameter) 39 | expect(result).toEqual([-1, 0]) 40 | }) 41 | 42 | it('return the correct range when given a string with multiple bracket pairs', () => { 43 | const parameter = 'hello: { world: { a } }, elysia' 44 | const result = bracketPairRangeReverse(parameter) 45 | expect(result).toEqual([7, 23]) 46 | }) 47 | 48 | it('return the correct range when given a string with an opening bracket but no closing bracket', () => { 49 | const parameter = 'hello: { world: { a }, elysia' 50 | const result = bracketPairRangeReverse(parameter) 51 | expect(result).toEqual([16, 21]) 52 | }) 53 | 54 | it('return the correct range when given a string with brackets inside quotes', () => { 55 | const parameter = 'hello: { world: { a } }, elysia' 56 | const result = bracketPairRangeReverse(parameter) 57 | expect(result).toEqual([7, 23]) 58 | }) 59 | 60 | it('return the correct range when given a string with nested bracket pairs', () => { 61 | const parameter = 'hello: { world: { a } }, elysia' 62 | const result = bracketPairRangeReverse(parameter) 63 | expect(result).toEqual([7, 23]) 64 | }) 65 | 66 | it('return the correct range when given a string with non-bracket characters', () => { 67 | const parameter = 'hello: { world: { a } }, elysia' 68 | const result = bracketPairRangeReverse(parameter) 69 | expect(result).toEqual([7, 23]) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/sucrose/bracket-pair-range.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | 3 | import { bracketPairRange } from '../../src/sucrose' 4 | 5 | describe('bracket pair range', () => { 6 | it('return the correct range when given a string with a single bracket pair', () => { 7 | const parameter = 'hello: { world: { a } }, elysia' 8 | const result = bracketPairRange(parameter) 9 | expect(result).toEqual([7, 23]) 10 | }) 11 | 12 | it('return the correct range when given a string with nested bracket pairs', () => { 13 | const parameter = 'hello: { world: { a } }, elysia' 14 | const result = bracketPairRange(parameter) 15 | expect(result).toEqual([7, 23]) 16 | }) 17 | 18 | it('return [-1, 0] when given a string without any bracket pairs', () => { 19 | const parameter = 'hello, elysia' 20 | const result = bracketPairRange(parameter) 21 | expect(result).toEqual([-1, 0]) 22 | }) 23 | 24 | it('return [0, 1] when given a string with a single opening bracket at the beginning', () => { 25 | const parameter = '{hello, elysia' 26 | const result = bracketPairRange(parameter) 27 | expect(result).toEqual([0, 14]) 28 | }) 29 | 30 | it('return [parameter.length - 1, parameter.length] when given a string with a single closing bracket at the end', () => { 31 | const parameter = 'hello, elysia}' 32 | const result = bracketPairRange(parameter) 33 | expect(result).toEqual([-1, 0]) 34 | }) 35 | 36 | it('return [-1, 0] when given an empty string', () => { 37 | const parameter = '' 38 | const result = bracketPairRange(parameter) 39 | expect(result).toEqual([-1, 0]) 40 | }) 41 | 42 | it('return the correct range when given a string with multiple bracket pairs', () => { 43 | const parameter = 'hello: { world: { a } }, elysia' 44 | const result = bracketPairRange(parameter) 45 | expect(result).toEqual([7, 23]) 46 | }) 47 | 48 | it('return the correct range when given a string with an opening bracket but no closing bracket', () => { 49 | const parameter = 'hello: { world: { a }, elysia' 50 | const result = bracketPairRange(parameter) 51 | expect(result).toEqual([0, parameter.length]) 52 | }) 53 | 54 | it('return the correct range when given a string with brackets inside quotes', () => { 55 | const parameter = 'hello: { world: { a } }, elysia' 56 | const result = bracketPairRange(parameter) 57 | expect(result).toEqual([7, 23]) 58 | }) 59 | 60 | it('return the correct range when given a string with nested bracket pairs', () => { 61 | const parameter = 'hello: { world: { a } }, elysia' 62 | const result = bracketPairRange(parameter) 63 | expect(result).toEqual([7, 23]) 64 | }) 65 | 66 | it('return the correct range when given a string with non-bracket characters', () => { 67 | const parameter = 'hello: { world: { a } }, elysia' 68 | const result = bracketPairRange(parameter) 69 | expect(result).toEqual([7, 23]) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/sucrose/extract-main-parameter.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'bun:test' 2 | import { extractMainParameter } from '../../src/sucrose' 3 | 4 | describe('extract main parameter', () => { 5 | it('extract main parameter when there is no spread operator and only one parameter', () => { 6 | const parameter = 'param1' 7 | const result = extractMainParameter(parameter) 8 | expect(result).toBe('param1') 9 | }) 10 | 11 | it('extract main parameter when there is a spread operator and only one parameter', () => { 12 | const parameter = '{ ...param1 }' 13 | const result = extractMainParameter(parameter) 14 | expect(result).toBe('param1') 15 | }) 16 | 17 | it('extract main parameter when there are multiple parameters and a spread operator', () => { 18 | const parameter = '{ param1, param2, ...param3 }' 19 | const result = extractMainParameter(parameter) 20 | expect(result).toBe('param3') 21 | }) 22 | 23 | it('return undefined when parameter is an empty string', () => { 24 | const parameter = '' 25 | const result = extractMainParameter(parameter) 26 | expect(result).toBeUndefined() 27 | }) 28 | 29 | it('return undefined when parameter is undefined', () => { 30 | const parameter = undefined 31 | // @ts-expect-error 32 | const result = extractMainParameter(parameter) 33 | expect(result).toBeUndefined() 34 | }) 35 | 36 | it('return undefined when parameter is null', () => { 37 | const parameter = null 38 | // @ts-expect-error 39 | const result = extractMainParameter(parameter) 40 | expect(result).toBeUndefined() 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/sucrose/infer-body-reference.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'bun:test' 2 | import { inferBodyReference } from '../../src/sucrose' 3 | 4 | describe('infer body reference', () => { 5 | it('infer dot notation', () => { 6 | const code = 'context.body.a' 7 | const aliases = ['context'] 8 | const inference = { 9 | query: false, 10 | headers: false, 11 | body: false, 12 | cookie: false, 13 | set: false, 14 | server: false 15 | } 16 | 17 | inferBodyReference(code, aliases, inference) 18 | 19 | expect(inference.body).toBe(true) 20 | }) 21 | 22 | it('infer property access', () => { 23 | const code = 'context["body"]["a"]' 24 | const aliases = ['context'] 25 | const inference = { 26 | query: false, 27 | headers: false, 28 | body: false, 29 | cookie: false, 30 | set: false, 31 | server: false 32 | } 33 | 34 | inferBodyReference(code, aliases, inference) 35 | 36 | expect(inference.body).toBe(true) 37 | }) 38 | 39 | it('infer multiple query', () => { 40 | const code = `{ 41 | console.log({ a: query.quack }, { 42 | b: query.duck 43 | }); 44 | const b = query.bark; 45 | return "a"; 46 | }` 47 | 48 | const aliases = ['query'] 49 | const inference = { 50 | body: false, 51 | cookie: false, 52 | headers: false, 53 | query: true, 54 | set: true, 55 | server: false 56 | } 57 | 58 | inferBodyReference(code, aliases, inference) 59 | 60 | expect(inference).toEqual({ 61 | body: false, 62 | cookie: false, 63 | headers: false, 64 | query: true, 65 | set: true, 66 | server: false 67 | }) 68 | }) 69 | 70 | // This is not use in Bun 71 | // it('infer single quote query', () => { 72 | // const code = `{ 73 | // const b = query['quack']; 74 | // return "a"; 75 | // }` 76 | 77 | // const aliases = ['query'] 78 | // const inference = { 79 | // body: false, 80 | // cookie: false, 81 | // headers: false, 82 | // queries: [], 83 | // query: true, 84 | // set: true, 85 | // unknownQueries: false 86 | // } 87 | 88 | // inferBodyReference(code, aliases, inference) 89 | 90 | // expect(inference).toEqual({ 91 | // body: false, 92 | // cookie: false, 93 | // headers: false, 94 | // queries: ['quack'], 95 | // query: true, 96 | // set: true, 97 | // unknownQueries: false 98 | // }) 99 | // }) 100 | 101 | // it('infer double quote query', () => { 102 | // const code = `{ 103 | // const b = query["quack"]; 104 | // return "a"; 105 | // }` 106 | 107 | // const aliases = ['query'] 108 | // const inference = { 109 | // body: false, 110 | // cookie: false, 111 | // headers: false, 112 | // queries: [], 113 | // query: true, 114 | // set: true, 115 | // unknownQueries: false 116 | // } 117 | 118 | // inferBodyReference(code, aliases, inference) 119 | 120 | // expect(inference).toEqual({ 121 | // body: false, 122 | // cookie: false, 123 | // headers: false, 124 | // queries: ['quack'], 125 | // query: true, 126 | // set: true, 127 | // unknownQueries: false 128 | // }) 129 | // }) 130 | 131 | it('skip dynamic quote query', () => { 132 | const code = `{ 133 | const b = query[quack]; 134 | return "a"; 135 | }` 136 | 137 | const aliases = ['query'] 138 | const inference = { 139 | body: false, 140 | cookie: false, 141 | headers: false, 142 | query: true, 143 | set: true, 144 | server: false 145 | } 146 | 147 | inferBodyReference(code, aliases, inference) 148 | 149 | expect(inference).toEqual({ 150 | body: false, 151 | cookie: false, 152 | headers: false, 153 | query: true, 154 | set: true, 155 | server: false 156 | }) 157 | }) 158 | 159 | it('infer dot notation', () => { 160 | const code = ` 161 | context.server?.upgrade(request) 162 | ` 163 | const aliases = ['context'] 164 | const inference = { 165 | query: false, 166 | headers: false, 167 | body: false, 168 | cookie: false, 169 | set: false, 170 | server: false 171 | } 172 | 173 | inferBodyReference(code, aliases, inference) 174 | 175 | expect(inference.server).toBe(true) 176 | }) 177 | }) 178 | -------------------------------------------------------------------------------- /test/sucrose/query.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | 5 | const req = (path: string = '/?name=sucrose') => 6 | new Request(`http://localhost${path}`) 7 | 8 | describe('Query', () => { 9 | it('access all using property name', async () => { 10 | const app = new Elysia().get('/', (ctx) => ctx.query) 11 | const response = await app.handle(req()) 12 | 13 | expect(await response.json()).toEqual({ name: 'sucrose' }) 14 | }) 15 | 16 | it('access all using destructuring', async () => { 17 | const app = new Elysia().get('/', ({ query }) => query) 18 | const response = await app.handle(req()) 19 | 20 | expect(await response.json()).toEqual({ name: 'sucrose' }) 21 | }) 22 | 23 | it('access single param using property name', async () => { 24 | const app = new Elysia().get('/', (ctx) => ctx.query.name) 25 | const response = await app.handle(req()) 26 | 27 | expect(await response.text()).toEqual('sucrose') 28 | }) 29 | 30 | it('access single param using destructuring', async () => { 31 | const app = new Elysia().get('/', ({ query: { name } }) => name) 32 | const response = await app.handle(req()) 33 | 34 | expect(await response.text()).toEqual('sucrose') 35 | }) 36 | 37 | it('access all using destructuring assignment', async () => { 38 | const app = new Elysia().get('/', (ctx) => { 39 | const { query } = ctx 40 | return query 41 | }) 42 | const response = await app.handle(req()) 43 | 44 | expect(await response.json()).toEqual({ name: 'sucrose' }) 45 | }) 46 | 47 | it('access all using destructuring assignment within derive', async () => { 48 | const app = new Elysia() 49 | .derive((ctx) => { 50 | const { query } = ctx 51 | return { 52 | yay() { 53 | return query 54 | } 55 | } 56 | }) 57 | .get('/', (ctx) => ctx.yay()) 58 | const response = await app.handle(req()) 59 | 60 | expect(await response.json()).toEqual({ name: 'sucrose' }) 61 | }) 62 | 63 | it('access all using property name within derive', async () => { 64 | const app = new Elysia() 65 | .derive((ctx) => { 66 | return { 67 | yay() { 68 | return ctx.query 69 | } 70 | } 71 | }) 72 | .get('/', (ctx) => ctx.yay()) 73 | 74 | const response = await app.handle(req()) 75 | 76 | expect(await response.json()).toEqual({ name: 'sucrose' }) 77 | }) 78 | 79 | it('destructured encoded & (%26) query string', async () => { 80 | const app = new Elysia() 81 | .get('/unknown', ({ query }) => query) 82 | .get('/named', ({ query: { name } }) => name) 83 | 84 | const unknown = await app 85 | .handle(req('/unknown?name=sucrose%26albedo&alias=achemist')) 86 | .then((x) => x.json()) 87 | const named = await app 88 | .handle(req('/named?name=sucrose%26albedo&alias=achemist')) 89 | .then((x) => x.text()) 90 | 91 | expect(unknown).toEqual({ name: 'sucrose&albedo', alias: 'achemist' }) 92 | expect(named).toEqual('sucrose&albedo') 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /test/sucrose/remove-colon-alias.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'bun:test' 2 | import { inferBodyReference, removeColonAlias } from '../../src/sucrose' 3 | 4 | describe('remove colon alias', () => { 5 | it('remove aliased name', () => { 6 | const code = '{ headers: rs }' 7 | 8 | expect(removeColonAlias(code)).toBe(`{ headers }`) 9 | }) 10 | 11 | it('remove aliased with part of keyword', () => { 12 | const code = '{ headers: reqHeaders }' 13 | 14 | expect(removeColonAlias(code)).toBe(`{ headers }`) 15 | }) 16 | 17 | it('remove multiple aliased', () => { 18 | const code = '{ headers: rs, query: q }' 19 | 20 | expect(removeColonAlias(code)).toBe(`{ headers, query }`) 21 | }) 22 | 23 | it('maintain same value if no aliased found', () => { 24 | const code = '{ headers, query }' 25 | 26 | expect(removeColonAlias(code)).toBe(`{ headers, query }`) 27 | }) 28 | }) -------------------------------------------------------------------------------- /test/sucrose/retrieve-root-parameters.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'bun:test' 2 | import { retrieveRootParamters } from '../../src/sucrose' 3 | 4 | describe('retrieve root parameter', () => { 5 | it('return an empty string when given an empty string', () => { 6 | const parameter = '' 7 | const result = retrieveRootParamters(parameter) 8 | expect(result).toEqual({ 9 | parameters: {}, 10 | hasParenthesis: false 11 | }) 12 | }) 13 | 14 | // Doesn't make sense as JavaScript will panic 15 | // it('return the same string when there are no brackets in the string', () => { 16 | // const parameter = 'hello world' 17 | // const result = retrieveRootParamters(parameter) 18 | // expect(result).toEqual({ 19 | // parameters: ['hello world'], 20 | // hasParenthesis: false 21 | // }) 22 | // }) 23 | 24 | it('remove brackets and their contents when they are at the root level', () => { 25 | const parameter = '({ hello: { world: { a } }, elysia })' 26 | const result = retrieveRootParamters(parameter) 27 | expect(result).toEqual({ 28 | parameters: { hello: true, elysia: true }, 29 | hasParenthesis: true 30 | }) 31 | }) 32 | 33 | it('return an empty string when given only brackets', () => { 34 | const parameter = '()' 35 | const result = retrieveRootParamters(parameter) 36 | expect(result).toEqual({ 37 | parameters: {}, 38 | hasParenthesis: false 39 | }) 40 | }) 41 | 42 | it('return an empty string when given only one bracket', () => { 43 | const parameter = '(' 44 | const result = retrieveRootParamters(parameter) 45 | expect(result).toEqual({ 46 | parameters: {}, 47 | hasParenthesis: false 48 | }) 49 | }) 50 | 51 | // Doesn't make sense as JavaScript will panic 52 | // it('return even if bracket is unbalanced', () => { 53 | // const parameter = '({ hello: { world: { a } })' 54 | // const result = retrieveRootParamters(parameter) 55 | // expect(result).toEqual({ 56 | // parameters: ['hello'], 57 | // hasParenthesis: true 58 | // }) 59 | // }) 60 | 61 | it('return the root parameters when given a string with spaces between brackets', () => { 62 | const parameter = '({ hello: { world: { a } }, elysia })' 63 | const result = retrieveRootParamters(parameter) 64 | expect(result).toEqual({ 65 | parameters: { hello: true, elysia: true }, 66 | hasParenthesis: true 67 | }) 68 | }) 69 | 70 | it('return parameter on minified bracket', () => { 71 | const parameter = '({ hello, path })' 72 | const result = retrieveRootParamters(parameter) 73 | expect(result).toEqual({ 74 | parameters: { hello: true, path: true }, 75 | hasParenthesis: true 76 | }) 77 | }) 78 | 79 | it('handle tab and new line', () => { 80 | const parameter = '({ hello: { world: { a } }, \nelysia, \teden })' 81 | const result = retrieveRootParamters(parameter) 82 | expect(result).toEqual({ 83 | parameters: { hello: true, elysia: true, eden: true }, 84 | hasParenthesis: true 85 | }) 86 | }) 87 | 88 | it('handle last parameter destructuring', () => { 89 | const parameter = '{ set, cookie: { auth } }' 90 | const result = retrieveRootParamters(parameter) 91 | expect(result).toEqual({ 92 | hasParenthesis: true, 93 | parameters: { set: true, cookie: true } 94 | }) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /test/sucrose/separate-function.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | 3 | import { separateFunction } from '../../src/sucrose' 4 | 5 | describe('Sucrose: separateFunction', () => { 6 | it('separate arrowParam', () => { 7 | const arrowParam = ({ sucrose, amber }: any) => { 8 | return 'sucrose' 9 | } 10 | 11 | expect(separateFunction(arrowParam.toString())).toEqual([ 12 | '{ sucrose, amber }', 13 | '{\n return "sucrose";\n }', 14 | { 15 | isArrowReturn: false 16 | } 17 | ]) 18 | }) 19 | 20 | it('separate arrowNoParam', () => { 21 | const arrowNoParam = () => 'sucrose' 22 | 23 | expect(separateFunction(arrowNoParam.toString())).toEqual([ 24 | '', 25 | '"sucrose"', 26 | { 27 | isArrowReturn: true 28 | } 29 | ]) 30 | }) 31 | 32 | it('separate arrowAsync', () => { 33 | const arrowAsync = async (sucrose: any) => 'sucrose' 34 | 35 | expect(separateFunction(arrowAsync.toString())).toEqual([ 36 | 'sucrose', 37 | '"sucrose"', 38 | { 39 | isArrowReturn: true 40 | } 41 | ]) 42 | }) 43 | 44 | it('separate fnParam', () => { 45 | function fnParam({ sucrose, amber }: any) { 46 | return 'sucrose' 47 | } 48 | 49 | expect(separateFunction(fnParam.toString())).toEqual([ 50 | '{ sucrose, amber }', 51 | '{\n return "sucrose";\n }', 52 | { 53 | isArrowReturn: false 54 | } 55 | ]) 56 | }) 57 | 58 | it('separate fnNoParam', () => { 59 | function fnNoParam() { 60 | return 'sucrose' 61 | } 62 | 63 | expect(separateFunction(fnNoParam.toString())).toEqual([ 64 | '', 65 | '{\n return "sucrose";\n }', 66 | { 67 | isArrowReturn: false 68 | } 69 | ]) 70 | }) 71 | 72 | it('separate fnAsync', () => { 73 | async function fnAsync(sucrose: any) { 74 | return 'sucrose' 75 | } 76 | 77 | expect(separateFunction(fnAsync.toString())).toEqual([ 78 | 'sucrose', 79 | '{\n return "sucrose";\n }', 80 | { 81 | isArrowReturn: false 82 | } 83 | ]) 84 | }) 85 | 86 | it('separate minifed arrow param', () => { 87 | const arrowParam = `({ sucrose, amber })=>{return "sucrose"}` 88 | 89 | expect(separateFunction(arrowParam.toString())).toEqual([ 90 | '{ sucrose, amber }', 91 | '{return "sucrose"}', 92 | { 93 | isArrowReturn: false 94 | } 95 | ]) 96 | }) 97 | 98 | it('separate minified arrow without whitespace in the beginning', () => { 99 | const arrowParam = `({sucrose, amber })=>{return "sucrose"}` 100 | 101 | expect(separateFunction(arrowParam.toString())).toEqual([ 102 | '{sucrose, amber }', 103 | '{return "sucrose"}', 104 | { 105 | isArrowReturn: false 106 | } 107 | ]) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /test/timeout.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../src' 2 | 3 | const largePlugin = async (app: Elysia) => { 4 | await new Promise((resolve) => setTimeout(resolve, 1000)) 5 | 6 | return app.get('/large', () => 'Hi') 7 | } 8 | 9 | export default largePlugin 10 | -------------------------------------------------------------------------------- /test/tracer/aot.test.ts: -------------------------------------------------------------------------------- 1 | import { Context, Elysia } from '../../src' 2 | import { describe, expect, it } from 'bun:test' 3 | import { req } from '../utils' 4 | 5 | describe('Trace AoT', async () => { 6 | // it('inject request report', async () => { 7 | // const app = new Elysia().trace(async () => {}).get('/', () => '') 8 | 9 | // expect(app.compile().fetch.toString()).toInclude( 10 | // `reporter.emit('event',{id,event:'request'` 11 | // ) 12 | // }) 13 | 14 | it('try-catch edge case', async () => { 15 | class Controller { 16 | static async handle(ctx: Context) { 17 | try { 18 | // @ts-ignore 19 | const { token } = ctx.body 20 | return token 21 | } catch { 22 | return 'nope' 23 | } 24 | } 25 | } 26 | 27 | const app = new Elysia().post('/', Controller.handle) 28 | 29 | const response = await app.handle( 30 | new Request('http://localhost/', { 31 | method: 'POST', 32 | body: JSON.stringify({ token: 'yay' }), 33 | headers: { 'Content-Type': 'application/json' } 34 | }) 35 | ) 36 | 37 | expect(await response.text()).toEqual('yay') 38 | }) 39 | 40 | // ! Fix me: uncomment when 1.0.0 is released 41 | // it('inject response report', async () => { 42 | // const app = new Elysia().trace(async () => {}).get('/', () => '') 43 | 44 | // expect(app.router.history[0].composed?.toString()).toInclude( 45 | // `reporter.emit('event',{id,event:'response'` 46 | // ) 47 | // }) 48 | 49 | it('handle scope', async () => { 50 | let called = 0 51 | 52 | const plugin = new Elysia() 53 | .trace(({ onHandle }) => { 54 | onHandle(() => { 55 | called++ 56 | }) 57 | }) 58 | .get('/plugin', () => 'ok') 59 | 60 | const app = new Elysia().use(plugin).get('/main', () => 'ok') 61 | 62 | await Promise.all([ 63 | app.handle(req('/plugin')), 64 | app.handle(req('/main')) 65 | ]) 66 | 67 | expect(called).toBe(1) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /test/tracer/detail.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from '../../src' 2 | import { describe, expect, it } from 'bun:test' 3 | import { post, req } from '../utils' 4 | 5 | describe('Trace Detail', async () => { 6 | it('report parse units name', async () => { 7 | const app = new Elysia() 8 | .trace(({ onParse, set }) => { 9 | onParse(({ onEvent, onStop }) => { 10 | const names = [] 11 | 12 | onEvent(({ name }) => { 13 | names.push(name) 14 | }) 15 | 16 | onStop(() => { 17 | set.headers.name = names.join(', ') 18 | }) 19 | }) 20 | }) 21 | .onParse(function luna() {}) 22 | .post('/', ({ body }) => body, { 23 | parse: [function kindred() {}] 24 | }) 25 | 26 | const { headers } = await app.handle(post('/', {})) 27 | 28 | expect(headers.get('name')).toBe('luna, kindred') 29 | }) 30 | 31 | it('report transform units name', async () => { 32 | const app = new Elysia() 33 | .trace(({ onTransform, set }) => { 34 | onTransform(({ onEvent, onStop }) => { 35 | const names = [] 36 | 37 | onEvent(({ name }) => { 38 | names.push(name) 39 | }) 40 | 41 | onStop(() => { 42 | set.headers.name = names.join(', ') 43 | }) 44 | }) 45 | }) 46 | .onTransform(function luna() {}) 47 | .get('/', () => 'a', { 48 | transform: [function kindred() {}] 49 | }) 50 | 51 | const { headers } = await app.handle(req('/')) 52 | 53 | expect(headers.get('name')).toBe('luna, kindred') 54 | }) 55 | 56 | it('report beforeHandle units name', async () => { 57 | const app = new Elysia() 58 | .trace(({ onBeforeHandle, set }) => { 59 | onBeforeHandle(({ onEvent, onStop }) => { 60 | const names = [] 61 | 62 | onEvent(({ name }) => { 63 | names.push(name) 64 | }) 65 | 66 | onStop(() => { 67 | set.headers.name = names.join(', ') 68 | }) 69 | }) 70 | }) 71 | .onBeforeHandle(function luna() {}) 72 | .get('/', () => 'a', { 73 | beforeHandle: [function kindred() {}] 74 | }) 75 | 76 | const { headers } = await app.handle(req('/')) 77 | 78 | expect(headers.get('name')).toBe('luna, kindred') 79 | }) 80 | 81 | it('report afterHandle units name', async () => { 82 | const app = new Elysia() 83 | .trace(({ onAfterHandle, set }) => { 84 | onAfterHandle(({ onEvent, onStop }) => { 85 | const names = [] 86 | 87 | onEvent(({ name }) => { 88 | names.push(name) 89 | }) 90 | 91 | onStop(() => { 92 | set.headers.name = names.join(', ') 93 | }) 94 | }) 95 | }) 96 | .onAfterHandle(function luna() {}) 97 | .get('/', () => 'a', { 98 | afterHandle: [function kindred() {}] 99 | }) 100 | 101 | const { headers } = await app.handle(req('/')) 102 | 103 | expect(headers.get('name')).toBe('luna, kindred') 104 | }) 105 | 106 | it('report mapResponse units name', async () => { 107 | const app = new Elysia() 108 | .trace(({ onMapResponse, set }) => { 109 | onMapResponse(({ onEvent, onStop }) => { 110 | const names = [] 111 | 112 | onEvent(({ name }) => { 113 | names.push(name) 114 | }) 115 | 116 | onStop(() => { 117 | set.headers.name = names.join(', ') 118 | }) 119 | }) 120 | }) 121 | .mapResponse(function luna() {}) 122 | .get('/', () => 'a', { 123 | mapResponse: [function kindred() {}] 124 | }) 125 | 126 | const { headers } = await app.handle(req('/')) 127 | 128 | expect(headers.get('name')).toBe('luna, kindred') 129 | }) 130 | 131 | it('report afterResponse units name', async () => { 132 | const app = new Elysia() 133 | .trace(({ onAfterResponse, set }) => { 134 | onAfterResponse(({ onEvent, onStop }) => { 135 | const names = [] 136 | 137 | onEvent(({ name }) => { 138 | names.push(name) 139 | }) 140 | 141 | onStop(() => { 142 | expect(names.join(', ')).toBe('luna, kindred') 143 | }) 144 | }) 145 | }) 146 | .onAfterResponse(function luna() {}) 147 | .get('/', () => 'a', { 148 | afterResponse: [function kindred() {}] 149 | }) 150 | 151 | app.handle(req('/')) 152 | }) 153 | }) 154 | -------------------------------------------------------------------------------- /test/type-system/array-string.test.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from '../../src' 2 | import { describe, expect, it } from 'bun:test' 3 | import { Value } from '@sinclair/typebox/value' 4 | import { TBoolean, TString, TypeBoxError } from '@sinclair/typebox' 5 | import { req } from '../utils' 6 | 7 | describe('TypeSystem - ArrayString', () => { 8 | it('Create', () => { 9 | // @ts-expect-error 10 | expect(Value.Create(t.ArrayString())).toBe(undefined) 11 | 12 | expect( 13 | Value.Create( 14 | t.ArrayString(t.Any(), { 15 | default: '[]' 16 | }) 17 | ) 18 | // @ts-expect-error 19 | ).toBe('[]') 20 | }) 21 | 22 | it('Check', () => { 23 | const schema = t.ArrayString(t.Number()) 24 | 25 | expect(Value.Check(schema, [1])).toBe(true) 26 | }) 27 | 28 | it('Encode', () => { 29 | const schema = t.ArrayString(t.Number()) 30 | 31 | expect(Value.Encode(schema, [1])).toBe( 32 | JSON.stringify([1]) 33 | ) 34 | 35 | expect(Value.Encode(schema, [1])).toBe( 36 | JSON.stringify([1]) 37 | ) 38 | }) 39 | 40 | it('Decode', () => { 41 | const schema = t.ArrayString(t.Number()) 42 | 43 | expect(Value.Decode(schema, '[1]')).toEqual([1]) 44 | 45 | expect(() => Value.Decode(schema, '1')).toThrow() 46 | }) 47 | 48 | it('Integrate', async () => { 49 | const app = new Elysia().post('/', ({ body }) => body, { 50 | body: t.Object({ 51 | id: t.ArrayString(t.Number()) 52 | }) 53 | }) 54 | 55 | const res1 = await app.handle( 56 | new Request('http://localhost', { 57 | method: 'POST', 58 | headers: { 'Content-Type': 'application/json' }, 59 | body: JSON.stringify({ id: JSON.stringify([1, 2, 3]) }) 60 | }) 61 | ) 62 | expect(res1.status).toBe(200) 63 | 64 | const res2 = await app.handle( 65 | new Request('http://localhost', { 66 | method: 'POST', 67 | headers: { 'Content-Type': 'application/json' }, 68 | body: JSON.stringify({ id: [1, 2, 3] }) 69 | }) 70 | ) 71 | expect(res2.status).toBe(200) 72 | 73 | const res3 = await app.handle( 74 | new Request('http://localhost', { 75 | method: 'POST', 76 | headers: { 'Content-Type': 'application/json' }, 77 | body: JSON.stringify({ id: ['a', 2, 3] }) 78 | }) 79 | ) 80 | expect(res3.status).toBe(422) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /test/type-system/boolean-string.test.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from '../../src' 2 | import { describe, expect, it } from 'bun:test' 3 | import { Value } from '@sinclair/typebox/value' 4 | import { TBoolean, TypeBoxError } from '@sinclair/typebox' 5 | import { req } from '../utils' 6 | 7 | describe('TypeSystem - BooleanString', () => { 8 | it('Create', () => { 9 | expect(Value.Create(t.BooleanString())).toBe(false) 10 | expect(Value.Create(t.BooleanString({ default: true }))).toBe(true) 11 | }) 12 | 13 | it('Check', () => { 14 | const schema = t.BooleanString() 15 | 16 | expect(Value.Check(schema, true)).toBe(true) 17 | expect(Value.Check(schema, 'true')).toBe(true) 18 | expect(Value.Check(schema, false)).toBe(true) 19 | expect(Value.Check(schema, 'false')).toBe(true) 20 | 21 | expect(Value.Check(schema, 'yay')).toBe(false) 22 | expect(Value.Check(schema, 42)).toBe(false) 23 | expect(Value.Check(schema, {})).toBe(false) 24 | expect(Value.Check(schema, undefined)).toBe(false) 25 | expect(Value.Check(schema, null)).toBe(false) 26 | }) 27 | 28 | it('Encode', () => { 29 | const schema = t.BooleanString() 30 | 31 | expect(Value.Encode(schema, true)).toBe(true) 32 | expect(Value.Encode(schema, 'true')).toBe('true') 33 | 34 | expect(Value.Encode(schema, false)).toBe(false) 35 | expect(Value.Encode(schema, 'false')).toBe('false') 36 | 37 | const error = new TypeBoxError( 38 | 'The encoded value does not match the expected schema' 39 | ) 40 | expect(() => Value.Encode(schema, 'yay')).toThrow(error) 41 | expect(() => Value.Encode(schema, 42)).toThrow(error) 42 | expect(() => Value.Encode(schema, {})).toThrow(error) 43 | expect(() => Value.Encode(schema, undefined)).toThrow(error) 44 | expect(() => Value.Encode(schema, null)).toThrow(error) 45 | }) 46 | 47 | it('Decode', () => { 48 | const schema = t.BooleanString() 49 | 50 | expect(Value.Decode(schema, true)).toBe(true) 51 | expect(Value.Decode(schema, 'true')).toBe(true) 52 | 53 | expect(Value.Decode(schema, false)).toBe(false) 54 | expect(Value.Decode(schema, 'false')).toBe(false) 55 | 56 | const error = new TypeBoxError( 57 | 'Unable to decode value as it does not match the expected schema' 58 | ) 59 | expect(() => Value.Decode(schema, 'yay')).toThrow(error) 60 | expect(() => Value.Decode(schema, 42)).toThrow(error) 61 | expect(() => Value.Decode(schema, {})).toThrow(error) 62 | expect(() => Value.Decode(schema, undefined)).toThrow(error) 63 | expect(() => Value.Decode(schema, null)).toThrow(error) 64 | }) 65 | 66 | it('Convert', () => { 67 | expect(Value.Convert(t.BooleanString(), 'true')).toBe(true) 68 | expect(Value.Convert(t.BooleanString(), 'false')).toBe(false) 69 | }) 70 | 71 | it('Integrate', async () => { 72 | const app = new Elysia().get('/', ({ query }) => query, { 73 | query: t.Object({ 74 | value: t.BooleanString() 75 | }) 76 | }) 77 | 78 | const res1 = await app.handle(req('/?value=true')) 79 | expect(res1.status).toBe(200) 80 | 81 | const res2 = await app.handle(req('/?value=false')) 82 | expect(res2.status).toBe(200) 83 | 84 | const res3 = await app.handle(req('/?value=aight')) 85 | expect(res3.status).toBe(422) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /test/type-system/coercion-number.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../../src' 2 | import { describe, it, expect } from 'bun:test' 3 | 4 | describe('Coercion - Numeric -> Number', () => { 5 | it('work', async () => { 6 | const app = new Elysia().get( 7 | '/:entityType', 8 | ({ params: { entityType } }) => entityType, 9 | { 10 | params: t.Object({ 11 | entityType: t.Number() 12 | }) 13 | } 14 | ) 15 | 16 | const response = await app.handle(new Request('http://localhost/999')) 17 | expect(response.status).toBe(200) 18 | }) 19 | 20 | it('handle property', async () => { 21 | const numberApp = new Elysia() 22 | .onError(({ code }) => code) 23 | .get('/:entityType', ({ params: { entityType } }) => entityType, { 24 | params: t.Object({ 25 | entityType: t.Number({ 26 | minimum: 0, 27 | maximum: 3, 28 | multipleOf: 1 29 | }) 30 | }) 31 | }) 32 | 33 | const numericApp = new Elysia() 34 | .onError(({ code }) => code) 35 | .get('/:entityType', ({ params: { entityType } }) => entityType, { 36 | params: t.Object({ 37 | entityType: t.Numeric({ 38 | minimum: 0, 39 | maximum: 3, 40 | multipleOf: 1 41 | }) 42 | }) 43 | }) 44 | 45 | async function expectValidResponse(response: Response) { 46 | expect(response.status).toBe(422) 47 | const body = await response.text() 48 | expect(body).not.toBe('999') 49 | expect(body).toBe('VALIDATION') 50 | } 51 | 52 | await expectValidResponse( 53 | await numberApp.handle(new Request('http://localhost/999')) 54 | ) 55 | 56 | await expectValidResponse( 57 | await numericApp.handle(new Request('http://localhost/999')) 58 | ) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/type-system/date.test.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from '../../src' 2 | import { describe, expect, it } from 'bun:test' 3 | import { Value } from '@sinclair/typebox/value' 4 | import { TBoolean, TDate, TUnion, TypeBoxError } from '@sinclair/typebox' 5 | import { post } from '../utils' 6 | 7 | describe('TypeSystem - Date', () => { 8 | it('Create', () => { 9 | expect(Value.Create(t.Date())).toBeInstanceOf(Date) 10 | }) 11 | 12 | it('No default date provided', () => { 13 | const schema = t.Date() 14 | expect(schema.default).toBeUndefined() 15 | 16 | const unionSchema = schema as unknown as TUnion 17 | for (const type of unionSchema.anyOf) { 18 | expect(type.default).toBeUndefined() 19 | } 20 | }) 21 | 22 | it('Default date provided', () => { 23 | const given = new Date('2025-01-01T00:00:00.000Z') 24 | const schema = t.Date({ default: given }) 25 | expect(schema.default).toEqual(given) 26 | 27 | const unionSchema = schema as unknown as TUnion 28 | for (const type of unionSchema.anyOf) { 29 | expect(new Date(type.default)).toEqual(given) 30 | } 31 | }) 32 | 33 | it('Check', () => { 34 | const schema = t.Date() 35 | 36 | expect(Value.Check(schema, new Date())).toEqual(true) 37 | expect(Value.Check(schema, '2021/1/1')).toEqual(true) 38 | 39 | expect(Value.Check(schema, 'yay')).toEqual(false) 40 | expect(Value.Check(schema, 42)).toEqual(true) 41 | expect(Value.Check(schema, {})).toEqual(false) 42 | expect(Value.Check(schema, undefined)).toEqual(false) 43 | expect(Value.Check(schema, null)).toEqual(false) 44 | }) 45 | 46 | it('Encode', () => { 47 | const schema = t.Date() 48 | 49 | const date = new Date() 50 | 51 | expect(Value.Encode(schema, date)).toBe( 52 | date.toISOString() 53 | ) 54 | 55 | expect(() => Value.Encode(schema, 'yay')).toThrowError() 56 | expect(() => 57 | Value.Encode(schema, Value.Decode(schema, 42)) 58 | ).not.toThrowError() 59 | expect(() => Value.Encode(schema, {})).toThrowError() 60 | expect(() => Value.Encode(schema, undefined)).toThrowError() 61 | expect(() => Value.Encode(schema, null)).toThrowError() 62 | }) 63 | 64 | it('Decode', () => { 65 | const schema = t.Date() 66 | 67 | expect(Value.Decode(schema, new Date())).toBeInstanceOf( 68 | Date 69 | ) 70 | expect(Value.Decode(schema, '2021/1/1')).toBeInstanceOf( 71 | Date 72 | ) 73 | 74 | const error = new TypeBoxError( 75 | 'Unable to decode value as it does not match the expected schema' 76 | ) 77 | expect(() => Value.Decode(schema, 'yay')).toThrow(error) 78 | expect(() => Value.Decode(schema, 42)).not.toThrow(error) 79 | expect(() => Value.Decode(schema, {})).toThrow(error) 80 | expect(() => Value.Decode(schema, undefined)).toThrow(error) 81 | expect(() => Value.Decode(schema, null)).toThrow(error) 82 | }) 83 | 84 | it('Integrate', async () => { 85 | const app = new Elysia().post('/', ({ body: { date } }) => date, { 86 | body: t.Object({ 87 | date: t.Date() 88 | }) 89 | }) 90 | 91 | const res1 = await app.handle( 92 | post('/', { 93 | date: new Date() 94 | }) 95 | ) 96 | expect(res1.status).toBe(200) 97 | 98 | const res2 = await app.handle( 99 | post('/', { 100 | date: '2021/1/1' 101 | }) 102 | ) 103 | expect(res2.status).toBe(200) 104 | 105 | const res3 = await app.handle( 106 | post('/', { 107 | date: 'Skibidi dom dom yes yes' 108 | }) 109 | ) 110 | expect(res3.status).toBe(422) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /test/type-system/form.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, file, form, t } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | 5 | import { Value } from '@sinclair/typebox/value' 6 | import { req } from '../utils' 7 | 8 | describe('TypeSystem - Form', () => { 9 | it('Create', () => { 10 | expect(Value.Create(t.Form({}))).toEqual(form({})) 11 | 12 | expect( 13 | Value.Create( 14 | t.Form( 15 | {}, 16 | { 17 | default: form({ 18 | name: 'saltyaom' 19 | }) 20 | } 21 | ) 22 | ) 23 | ).toEqual( 24 | form({ 25 | name: 'saltyaom' 26 | }) 27 | ) 28 | }) 29 | 30 | it('Check', () => { 31 | const schema = t.Form({ 32 | name: t.String(), 33 | age: t.Number() 34 | }) 35 | 36 | expect( 37 | Value.Check( 38 | schema, 39 | form({ 40 | name: 'saltyaom', 41 | age: 20 42 | }) 43 | ) 44 | ).toBe(true) 45 | 46 | try { 47 | Value.Check( 48 | schema, 49 | form({ 50 | name: 'saltyaom' 51 | }) 52 | ) 53 | expect(true).toBe(false) 54 | } catch { 55 | expect(true).toBe(true) 56 | } 57 | }) 58 | 59 | it('Integrate', async () => { 60 | const app = new Elysia() 61 | .get( 62 | '/form/:name', 63 | ({ params: { name } }) => 64 | form({ 65 | name: name as any 66 | }), 67 | { 68 | response: t.Form({ 69 | name: t.Literal('saltyaom') 70 | }) 71 | } 72 | ) 73 | .get( 74 | '/file', 75 | () => 76 | form({ 77 | teapot: file('example/teapot.webp') 78 | }), 79 | { 80 | response: t.Form({ 81 | teapot: t.File() 82 | }) 83 | } 84 | ) 85 | 86 | const res1 = await app.handle(req('/form/saltyaom')) 87 | expect(res1.status).toBe(200) 88 | 89 | const res2 = await app.handle(req('/form/felis')) 90 | expect(res2.status).toBe(422) 91 | 92 | const res3 = await app.handle(req('/file')) 93 | expect(res3.status).toBe(200) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /test/type-system/import.ts: -------------------------------------------------------------------------------- 1 | try { 2 | const { TypeSystem } = await import('../../src/type-system') 3 | console.log(TypeSystem && `✅ TypeSystem import works!`) 4 | } catch (cause) { 5 | throw new Error('❌ TypeSystem import failed', { cause }) 6 | } 7 | export {} 8 | -------------------------------------------------------------------------------- /test/type-system/object-string.test.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from '../../src' 2 | import { describe, expect, it } from 'bun:test' 3 | import { Value } from '@sinclair/typebox/value' 4 | import { req } from '../utils' 5 | 6 | describe('TypeSystem - ObjectString', () => { 7 | it('Create', () => { 8 | expect(Value.Create(t.ObjectString({}))).toBe('{}') 9 | }) 10 | 11 | it('Check', () => { 12 | const schema = t.ObjectString({ 13 | pageIndex: t.Number(), 14 | pageLimit: t.Number() 15 | }) 16 | 17 | expect(Value.Check(schema, { pageIndex: 1, pageLimit: 1 })).toBe(true) 18 | }) 19 | 20 | it('Encode', () => { 21 | const schema = t.ObjectString({ 22 | pageIndex: t.Number(), 23 | pageLimit: t.Number() 24 | }) 25 | 26 | expect( 27 | Value.Encode(schema, { 28 | pageIndex: 1, 29 | pageLimit: 1 30 | }) 31 | ).toBe(JSON.stringify({ pageIndex: 1, pageLimit: 1 })) 32 | 33 | expect( 34 | Value.Encode(schema, { 35 | pageIndex: 1, 36 | pageLimit: 1 37 | }) 38 | ).toBe(JSON.stringify({ pageIndex: 1, pageLimit: 1 })) 39 | }) 40 | 41 | it('Decode', () => { 42 | const schema = t.ObjectString({ 43 | pageIndex: t.Number(), 44 | pageLimit: t.Number() 45 | }) 46 | 47 | expect( 48 | Value.Decode( 49 | schema, 50 | JSON.stringify({ 51 | pageIndex: 1, 52 | pageLimit: 1 53 | }) 54 | ) 55 | ).toEqual({ pageIndex: 1, pageLimit: 1 }) 56 | 57 | expect(() => 58 | Value.Decode( 59 | schema, 60 | JSON.stringify({ 61 | pageLimit: 1 62 | }) 63 | ) 64 | ).toThrow() 65 | }) 66 | 67 | it('Integrate', async () => { 68 | const app = new Elysia().get('/', ({ query }) => query, { 69 | query: t.Object({ 70 | pagination: t.ObjectString({ 71 | pageIndex: t.Number(), 72 | pageLimit: t.Number() 73 | }) 74 | }) 75 | }) 76 | 77 | const res1 = await app.handle( 78 | req('/?pagination={"pageIndex":1,"pageLimit":1}') 79 | ) 80 | expect(res1.status).toBe(200) 81 | 82 | const res2 = await app.handle(req('/?pagination={"pageLimit":1}')) 83 | expect(res2.status).toBe(422) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /test/type-system/string-format.test.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from '../../src' 2 | import { describe, expect, it } from 'bun:test' 3 | import { Value } from '@sinclair/typebox/value' 4 | import { TBoolean, TString, TypeBoxError } from '@sinclair/typebox' 5 | import { req } from '../utils' 6 | 7 | describe('TypeSystem - ObjectString', () => { 8 | it('Format email', async () => { 9 | const testString = "foo@example.com"; 10 | const app = new Elysia().get( 11 | '/', 12 | ({ query }) => query, 13 | { 14 | query: t.Object({ 15 | email: t.String({ 16 | format: 'email' 17 | }) 18 | }) 19 | } 20 | ) 21 | 22 | const res1 = await app.handle(req(`/?email=${testString}`)) 23 | expect(res1.status).toBe(200); 24 | 25 | expect(await (res1).json()).toEqual({ email: testString }) 26 | }) 27 | it('Format hostname', async () => { 28 | const testString = "www"; 29 | const app = new Elysia().get( 30 | '/', 31 | ({ query }) => query, 32 | { 33 | query: t.Object({ 34 | host: t.String({ 35 | format: 'hostname' 36 | }) 37 | }) 38 | } 39 | ) 40 | 41 | const res1 = await app.handle(req(`/?host=${testString}`)) 42 | expect(res1.status).toBe(200); 43 | 44 | expect(await (res1).json()).toEqual({ host: testString }) 45 | }) 46 | it('Format date', async () => { 47 | const testString = "2024-01-01"; 48 | const app = new Elysia().get( 49 | '/', 50 | ({ query }) => query, 51 | { 52 | query: t.Object({ 53 | date: t.String({ 54 | format: 'date' 55 | }) 56 | }) 57 | } 58 | ) 59 | 60 | const res1 = await app.handle(req(`/?date=${testString}`)) 61 | expect(res1.status).toBe(200); 62 | 63 | expect(await (res1).json()).toEqual({ date: testString }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/type-system/union-enum.test.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from '../../src' 2 | import { describe, expect, it } from 'bun:test' 3 | import { Value } from '@sinclair/typebox/value' 4 | import { post } from '../utils' 5 | 6 | describe('TypeSystem - UnionEnum', () => { 7 | it('Create', () => { 8 | expect(Value.Create(t.UnionEnum(['some', 'data']))).toEqual('some') 9 | }) 10 | 11 | it('Allows readonly', () => { 12 | const readonlyArray = ['some', 'data'] as const 13 | expect(Value.Create(t.UnionEnum(readonlyArray))).toEqual('some') 14 | }) 15 | 16 | it('Check', () => { 17 | const schema = t.UnionEnum(['some', 'data', null]) 18 | 19 | expect(Value.Check(schema, 'some')).toBe(true) 20 | expect(Value.Check(schema, 'data')).toBe(true) 21 | expect(Value.Check(schema, null)).toBe(true) 22 | 23 | expect(Value.Check(schema, { deep: 2 })).toBe(false) 24 | expect(Value.Check(schema, 'yay')).toBe(false) 25 | expect(Value.Check(schema, 42)).toBe(false) 26 | expect(Value.Check(schema, {})).toBe(false) 27 | expect(Value.Check(schema, undefined)).toBe(false) 28 | }) 29 | it('JSON schema', () => { 30 | expect(t.UnionEnum(['some', 'data'])).toMatchObject({ 31 | type: 'string', 32 | enum: ['some', 'data'] 33 | }) 34 | expect(t.UnionEnum(['some', 1]).type).toBeUndefined() 35 | expect(t.UnionEnum([2, 1])).toMatchObject({ 36 | type: 'number', 37 | enum: [2, 1] 38 | }) 39 | expect(t.UnionEnum([null])).toMatchObject({ 40 | type: 'null', 41 | enum: [null] 42 | }) 43 | }) 44 | it('Integrate', async () => { 45 | const app = new Elysia().post('/', ({ body }) => body, { 46 | body: t.Object({ 47 | value: t.UnionEnum(['some', 1, null]) 48 | }) 49 | }) 50 | const res1 = await app.handle(post('/', { value: 1 })) 51 | expect(res1.status).toBe(200) 52 | 53 | const res2 = await app.handle(post('/', { value: null })) 54 | expect(res2.status).toBe(200) 55 | 56 | const res3 = await app.handle(post('/', { value: 'some' })) 57 | expect(res3.status).toBe(200) 58 | 59 | const res4 = await app.handle(post('/', { value: 'data' })) 60 | expect(res4.status).toBe(422) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /test/types/plugins.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../../src' 2 | 3 | const plugin = async (app: Elysia) => 4 | app.decorate('decorate', 'a').state('state', 'a').model({ 5 | string: t.String() 6 | }) 7 | 8 | export default plugin 9 | -------------------------------------------------------------------------------- /test/types/type-system.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { expect } from 'bun:test' 3 | import { t, Elysia, RouteSchema, Cookie, form, file } from '../../src' 4 | import { expectTypeOf } from 'expect-type' 5 | 6 | // ? ArrayString 7 | { 8 | new Elysia().post( 9 | '/', 10 | ({ body }) => { 11 | expectTypeOf().toEqualTypeOf([] as string[]) 12 | }, 13 | { 14 | body: t.ArrayString(t.String()) 15 | } 16 | ) 17 | } 18 | 19 | // ? Form 20 | { 21 | new Elysia() 22 | .get( 23 | '/', 24 | () => 25 | form({ 26 | name: 'Misono Mika', 27 | file: file('example/kyuukurarin.mp4') 28 | }), 29 | { 30 | response: t.Form({ 31 | name: t.String(), 32 | file: t.File() 33 | }) 34 | } 35 | ) 36 | .get( 37 | '/', 38 | // @ts-expect-error 39 | () => 40 | form({ 41 | file: 'a' 42 | }), 43 | { 44 | response: t.Form({ 45 | name: t.String(), 46 | file: t.File() 47 | }) 48 | } 49 | ) 50 | } 51 | 52 | // Files 53 | { 54 | new Elysia().get( 55 | '/', 56 | ({ body }) => { 57 | expectTypeOf().toEqualTypeOf<{ 58 | images: File[] 59 | }>() 60 | }, 61 | { 62 | body: t.Object({ 63 | images: t.Files({ 64 | maxSize: '4m', 65 | type: 'image' 66 | }) 67 | }) 68 | } 69 | ) 70 | } 71 | 72 | // use StaticDecode to unwrap type parameter 73 | { 74 | function addTwo(num: number) { 75 | return num + 2 76 | } 77 | 78 | new Elysia().get('', async ({ query: { foo } }) => addTwo(foo), { 79 | query: t.Object({ 80 | foo: t 81 | .Transform(t.String()) 82 | .Decode((x) => 12) 83 | .Encode((x) => x.toString()) 84 | }) 85 | }) 86 | } 87 | 88 | // handle Elysia.Ref 89 | { 90 | const Model = new Elysia().model({ 91 | hello: t.Number() 92 | }) 93 | 94 | new Elysia().use(Model).get( 95 | '', 96 | async ({ body }) => { 97 | expectTypeOf().toEqualTypeOf() 98 | }, 99 | { 100 | body: Model.Ref('hello') 101 | } 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /test/units/class-to-object.test.ts: -------------------------------------------------------------------------------- 1 | // import { describe, it, expect } from 'bun:test' 2 | // import { classToObject } from '../../src/utils' 3 | 4 | // describe('extractPropertiesAndGetters', () => { 5 | // class TestClass { 6 | // public normalProperty: string = 'normal' 7 | // private _computedProperty: number = 42 8 | 9 | // get computedProperty(): number { 10 | // return this._computedProperty 11 | // } 12 | 13 | // public method(): string { 14 | // return 'method' 15 | // } 16 | // } 17 | 18 | // it('should extract properties and getters, omitting methods', () => { 19 | // const instance = new TestClass() 20 | // const result = classToObject(instance) 21 | 22 | // // Check that normal property is copied 23 | // expect(result.normalProperty).toBe('normal') 24 | 25 | // // Check that getter is included 26 | // expect(result.computedProperty).toBe(42) 27 | 28 | // // Check that method is not included 29 | // // @ts-ignore 30 | // expect(result.method).toBeUndefined() 31 | 32 | // // Check that private property is not included 33 | // expect(result).not.toHaveProperty('_computedProperty') 34 | 35 | // // Check the structure of the result object 36 | // expect(Object.keys(result)).toEqual([ 37 | // 'normalProperty', 38 | // 'computedProperty' 39 | // ]) 40 | // }) 41 | 42 | // it('should handle objects with no getters', () => { 43 | // const obj = { a: 1, b: 2 } 44 | // const result = classToObject(obj) 45 | 46 | // expect(result).toEqual({ a: 1, b: 2 }) 47 | // }) 48 | 49 | // it('should handle empty objects', () => { 50 | // const obj = {} 51 | // const result = classToObject(obj) 52 | 53 | // expect(result).toEqual({}) 54 | // }) 55 | 56 | // it('should handle circular references', () => { 57 | // const obj: any = { a: 1 } 58 | // obj.self = obj 59 | // obj.nested = { b: 2, parent: obj } 60 | 61 | // const result = classToObject(obj) 62 | 63 | // expect(result.a).toBe(1) 64 | // expect(result.self).toBe(result) 65 | // expect(result.nested.b).toBe(2) 66 | // expect(result.nested.parent).toBe(result) 67 | // }) 68 | // }) 69 | -------------------------------------------------------------------------------- /test/units/deduplicate-checksum.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { deduplicateChecksum, checksum } from '../../src' 3 | 4 | describe('Deduplicate Checksum', () => { 5 | it('work', () => { 6 | const a = ['a', 'b', 'c', 'a'].map((x) => ({ 7 | checksum: checksum(x), 8 | fn: () => x 9 | })) 10 | 11 | deduplicateChecksum(a) 12 | 13 | expect(a).toHaveLength(3) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/units/has-ref.test.ts: -------------------------------------------------------------------------------- 1 | import { t } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | 5 | import { hasRef } from '../../src/schema' 6 | 7 | describe('has Ref', () => { 8 | it('return true if object property has ref', () => { 9 | expect( 10 | hasRef( 11 | t.Object({ 12 | a: t.String(), 13 | b: t.Ref('b'), 14 | c: t.Number() 15 | }) 16 | ) 17 | ).toBe(true) 18 | }) 19 | 20 | it('return false if object property does not have ref', () => { 21 | expect( 22 | hasRef( 23 | t.Object({ 24 | a: t.Number(), 25 | b: t.String(), 26 | c: t.Number() 27 | }) 28 | ) 29 | ).toBe(false) 30 | }) 31 | 32 | it('return true if array property contains ref', () => { 33 | expect( 34 | hasRef( 35 | t.Object({ 36 | a: t.String(), 37 | b: t.Ref('b'), 38 | c: t.Number() 39 | }) 40 | ) 41 | ).toBe(true) 42 | }) 43 | 44 | it('should return false if array does not contains ref', () => { 45 | expect( 46 | hasRef( 47 | t.Array( 48 | t.Object({ 49 | a: t.Number(), 50 | b: t.String(), 51 | c: t.Number() 52 | }) 53 | ) 54 | ) 55 | ).toBe(false) 56 | }) 57 | 58 | it('return true if nested object property has ref', () => { 59 | expect( 60 | hasRef( 61 | t.Object({ 62 | a: t.String(), 63 | b: t.Object({ 64 | a: t.String(), 65 | b: t.Array(t.Ref('b')), 66 | c: t.Number() 67 | }), 68 | c: t.Number() 69 | }) 70 | ) 71 | ).toBe(true) 72 | }) 73 | 74 | it('return true if ref is inside array', () => { 75 | expect(hasRef(t.Ref('b'))).toBe(true) 76 | }) 77 | 78 | it('return true if ref root', () => { 79 | expect(hasRef(t.Ref('b'))).toBe(true) 80 | }) 81 | 82 | it('return true if nested object property is inside Union', () => { 83 | expect( 84 | hasRef( 85 | t.Object({ 86 | a: t.String(), 87 | b: t.Union([t.String(), t.Array(t.Ref('b'))]) 88 | }) 89 | ) 90 | ).toBe(true) 91 | }) 92 | 93 | it('return true if nested object property is inside Intersect', () => { 94 | expect( 95 | hasRef( 96 | t.Object({ 97 | a: t.String(), 98 | b: t.Intersect([t.String(), t.Array(t.Ref('b'))]) 99 | }) 100 | ) 101 | ).toBe(true) 102 | }) 103 | 104 | it('return true if Ref is inside Union', () => { 105 | expect( 106 | hasRef( 107 | t.Union([ 108 | t.Object({ foo: t.String() }), 109 | t.Object({ 110 | field: t.Ref('b') 111 | }) 112 | ]) 113 | ) 114 | ).toEqual(true) 115 | }) 116 | 117 | it('return true if Ref is inside Intersect', () => { 118 | expect( 119 | hasRef( 120 | t.Intersect([ 121 | t.Object({ foo: t.String() }), 122 | t.Object({ 123 | field: t.Ref('b') 124 | }) 125 | ]) 126 | ) 127 | ).toEqual(true) 128 | }) 129 | 130 | it('return true if Ref is inside Transform', () => { 131 | expect( 132 | hasRef( 133 | t 134 | .Transform(t.Object({ id: t.Ref('b') })) 135 | .Decode((value) => value.id) 136 | .Encode((value) => ({ 137 | id: value 138 | })) 139 | ) 140 | ).toBe(true) 141 | }) 142 | 143 | it('return true if Ref is the root', () => { 144 | expect(hasRef(t.Ref('b'))).toBe(true) 145 | }) 146 | }) 147 | -------------------------------------------------------------------------------- /test/units/has-transform.test.ts: -------------------------------------------------------------------------------- 1 | import { t } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | 5 | import { hasTransform } from '../../src/schema' 6 | 7 | describe('has transform', () => { 8 | it('return true if object property has Transform', () => { 9 | expect( 10 | hasTransform( 11 | t.Object({ 12 | a: t.String(), 13 | b: t.Numeric(), 14 | c: t.Number() 15 | }) 16 | ) 17 | ).toBe(true) 18 | }) 19 | 20 | it('return false if object property does not have Transform', () => { 21 | expect( 22 | hasTransform( 23 | t.Object({ 24 | a: t.Number(), 25 | b: t.String(), 26 | c: t.Number() 27 | }) 28 | ) 29 | ).toBe(false) 30 | }) 31 | 32 | it('return true if array property contains Transform', () => { 33 | expect( 34 | hasTransform( 35 | t.Object({ 36 | a: t.String(), 37 | b: t.Numeric(), 38 | c: t.Number() 39 | }) 40 | ) 41 | ).toBe(true) 42 | }) 43 | 44 | it('should return false if array does not contains Transform', () => { 45 | expect( 46 | hasTransform( 47 | t.Array( 48 | t.Object({ 49 | a: t.Number(), 50 | b: t.String(), 51 | c: t.Number() 52 | }) 53 | ) 54 | ) 55 | ).toBe(false) 56 | }) 57 | 58 | it('return true if nested object property has Transform', () => { 59 | expect( 60 | hasTransform( 61 | t.Object({ 62 | a: t.String(), 63 | b: t.Object({ 64 | a: t.String(), 65 | b: t.Array(t.Numeric()), 66 | c: t.Number() 67 | }), 68 | c: t.Number() 69 | }) 70 | ) 71 | ).toBe(true) 72 | }) 73 | 74 | it('return true if Transform is inside array', () => { 75 | expect(hasTransform(t.Numeric())).toBe(true) 76 | }) 77 | 78 | it('return true if Transform root', () => { 79 | expect(hasTransform(t.Numeric())).toBe(true) 80 | }) 81 | 82 | it('return true if nested object property is inside Union', () => { 83 | expect( 84 | hasTransform( 85 | t.Object({ 86 | a: t.String(), 87 | b: t.Union([t.String(), t.Array(t.Numeric())]) 88 | }) 89 | ) 90 | ).toBe(true) 91 | }) 92 | 93 | it('return true if nested object property is inside Intersect', () => { 94 | expect( 95 | hasTransform( 96 | t.Object({ 97 | a: t.String(), 98 | b: t.Intersect([t.String(), t.Array(t.Numeric())]) 99 | }) 100 | ) 101 | ).toBe(true) 102 | }) 103 | 104 | it('return true if Transform is inside Intersect', () => { 105 | expect( 106 | hasTransform( 107 | t.Intersect([ 108 | t.Object({ foo: t.String() }), 109 | t.Object({ 110 | field: t 111 | .Transform(t.String()) 112 | .Decode((decoded) => ({ decoded })) 113 | .Encode((v) => v.decoded) 114 | }) 115 | ]) 116 | ) 117 | ).toEqual(true) 118 | }) 119 | 120 | it('return true if Transform is inside Union', () => { 121 | expect( 122 | hasTransform( 123 | t.Union([ 124 | t.Object({ foo: t.String() }), 125 | t.Object({ 126 | field: t 127 | .Transform(t.String()) 128 | .Decode((decoded) => ({ decoded })) 129 | .Encode((v) => v.decoded) 130 | }) 131 | ]) 132 | ) 133 | ).toEqual(true) 134 | }) 135 | 136 | it('return true when Transform is the root', () => { 137 | expect( 138 | hasTransform( 139 | t 140 | .Transform(t.Object({ id: t.String() })) 141 | .Decode((value) => value.id) 142 | .Encode((value) => ({ 143 | id: value 144 | })) 145 | ) 146 | ).toBe(true) 147 | }) 148 | }) 149 | -------------------------------------------------------------------------------- /test/units/merge-deep.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | 3 | import { Elysia } from '../../src' 4 | import { mergeDeep } from '../../src/utils' 5 | import { req } from '../utils' 6 | 7 | describe('mergeDeep', () => { 8 | it('merge empty object', () => { 9 | const result = mergeDeep({}, {}) 10 | expect(result).toEqual({}) 11 | }) 12 | 13 | it('merge non-overlapping key', () => { 14 | const result = mergeDeep({ key1: 'value1' }, { key2: 'value2' }) 15 | 16 | expect(result).toEqual({ key1: 'value1', key2: 'value2' }) 17 | }) 18 | 19 | it('merge overlapping key', () => { 20 | const result = mergeDeep( 21 | { 22 | name: 'Eula', 23 | city: 'Mondstadt' 24 | }, 25 | { 26 | name: 'Amber', 27 | affiliation: 'Knight' 28 | } 29 | ) 30 | 31 | expect(result).toEqual({ 32 | name: 'Amber', 33 | city: 'Mondstadt', 34 | affiliation: 'Knight' 35 | }) 36 | }) 37 | 38 | it('maintain overlapping class', () => { 39 | class Test { 40 | readonly name = 'test' 41 | 42 | public foo() { 43 | return this.name 44 | } 45 | } 46 | 47 | const target = { key1: Test } 48 | const source = { key2: Test } 49 | 50 | const result = mergeDeep(target, source) 51 | expect(result.key1).toBe(Test) 52 | }) 53 | 54 | it('maintain overlapping class in instance', async () => { 55 | class DbConnection { 56 | health() { 57 | return 'ok' 58 | } 59 | 60 | getUsers() { 61 | return [] 62 | } 63 | } 64 | 65 | const dbPlugin = new Elysia({ name: 'db' }).decorate( 66 | 'db', 67 | new DbConnection() 68 | ) 69 | 70 | const userRoutes = new Elysia({ prefix: '/user' }) 71 | .use(dbPlugin) 72 | .get('', ({ db }) => db.getUsers()) 73 | 74 | const app = new Elysia() 75 | .use(dbPlugin) 76 | .use(userRoutes) 77 | .get('/health', ({ db }) => db.health()) 78 | 79 | const response = await app.handle(req('/health')).then((x) => x.text()) 80 | 81 | expect(response).toBe('ok') 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /test/units/merge-object-schemas.test.ts: -------------------------------------------------------------------------------- 1 | import { t } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | 5 | import { mergeObjectSchemas } from '../../src/schema' 6 | 7 | describe('mergeDeep', () => { 8 | it('merge object', () => { 9 | const result = mergeObjectSchemas([ 10 | t.Object({ 11 | name: t.String() 12 | }), 13 | t.Object({ 14 | age: t.Number() 15 | }) 16 | ]) 17 | 18 | expect(result).toEqual({ 19 | schema: t.Object({ 20 | name: t.String(), 21 | age: t.Number() 22 | }), 23 | notObjects: [] 24 | }) 25 | }) 26 | 27 | it('handle additionalProperties true', () => { 28 | const result = mergeObjectSchemas([ 29 | t.Object( 30 | { 31 | name: t.String() 32 | }, 33 | { 34 | additionalProperties: true 35 | } 36 | ), 37 | t.Object({ 38 | age: t.Number() 39 | }) 40 | ]) 41 | 42 | expect(result).toEqual({ 43 | schema: t.Object( 44 | { 45 | name: t.String(), 46 | age: t.Number() 47 | }, 48 | { 49 | additionalProperties: true 50 | } 51 | ), 52 | notObjects: [] 53 | }) 54 | }) 55 | 56 | it('handle additionalProperties false', () => { 57 | const result = mergeObjectSchemas([ 58 | t.Object( 59 | { 60 | name: t.String() 61 | }, 62 | { 63 | additionalProperties: false 64 | } 65 | ), 66 | t.Object({ 67 | age: t.Number() 68 | }) 69 | ]) 70 | 71 | expect(result).toEqual({ 72 | schema: t.Object( 73 | { 74 | name: t.String(), 75 | age: t.Number() 76 | }, 77 | { 78 | additionalProperties: false 79 | } 80 | ), 81 | notObjects: [] 82 | }) 83 | }) 84 | 85 | it('prefers additionalProperties: false over true', () => { 86 | const result = mergeObjectSchemas([ 87 | t.Object( 88 | { 89 | name: t.String() 90 | }, 91 | { 92 | additionalProperties: true 93 | } 94 | ), 95 | t.Object( 96 | { 97 | age: t.Number() 98 | }, 99 | { 100 | additionalProperties: false 101 | } 102 | ) 103 | ]) 104 | 105 | expect(result).toEqual({ 106 | schema: t.Object( 107 | { 108 | name: t.String(), 109 | age: t.Number() 110 | }, 111 | { 112 | additionalProperties: false 113 | } 114 | ), 115 | notObjects: [] 116 | }) 117 | }) 118 | 119 | it('handle non object', () => { 120 | const result = mergeObjectSchemas([ 121 | t.Object( 122 | { 123 | name: t.String() 124 | }, 125 | { 126 | additionalProperties: false 127 | } 128 | ), 129 | t.Object({ 130 | age: t.Number() 131 | }), 132 | t.String() 133 | ]) 134 | 135 | expect(result).toEqual({ 136 | schema: t.Object( 137 | { 138 | name: t.String(), 139 | age: t.Number() 140 | }, 141 | { 142 | additionalProperties: false 143 | } 144 | ), 145 | notObjects: [t.String()] 146 | }) 147 | }) 148 | 149 | it('handle single object schema', () => { 150 | const result = mergeObjectSchemas([ 151 | t.Object( 152 | { 153 | name: t.String() 154 | }, 155 | { 156 | additionalProperties: false 157 | } 158 | ) 159 | ]) 160 | 161 | expect(result).toEqual({ 162 | schema: t.Object( 163 | { 164 | name: t.String() 165 | }, 166 | { 167 | additionalProperties: false 168 | } 169 | ), 170 | notObjects: [] 171 | }) 172 | }) 173 | 174 | it('handle single non object schema', () => { 175 | const result = mergeObjectSchemas([t.String()]) 176 | 177 | expect(result).toEqual({ 178 | schema: undefined, 179 | notObjects: [t.String()] 180 | }) 181 | }) 182 | 183 | it('handle multiple object schemas', () => { 184 | const result = mergeObjectSchemas([ 185 | t.Object( 186 | { 187 | name: t.String() 188 | }, 189 | { 190 | additionalProperties: false 191 | } 192 | ), 193 | t.Object({ 194 | age: t.Number() 195 | }), 196 | t.Object({ 197 | email: t.String() 198 | }), 199 | t.Object({ 200 | address: t.String() 201 | }) 202 | ]) 203 | 204 | expect(result).toEqual({ 205 | schema: t.Object( 206 | { 207 | name: t.String(), 208 | age: t.Number(), 209 | email: t.String(), 210 | address: t.String() 211 | }, 212 | { 213 | additionalProperties: false 214 | } 215 | ), 216 | notObjects: [] 217 | }) 218 | }) 219 | }) 220 | -------------------------------------------------------------------------------- /test/units/numeric.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { isNumericString } from '../../src/utils' 3 | 4 | describe('Numeric string', () => { 5 | it('valid string', async () => { 6 | expect(isNumericString('69')).toBe(true) 7 | expect(isNumericString('69.420')).toBe(true) 8 | expect(isNumericString('00093281')).toBe(true) 9 | expect(isNumericString(Number.MAX_SAFE_INTEGER.toString())).toBe(true) 10 | }) 11 | 12 | it('invalid string', async () => { 13 | expect(isNumericString('pizza')).toBe(false) 14 | expect(isNumericString('69,420')).toBe(false) 15 | expect(isNumericString('0O093281')).toBe(false) 16 | expect(isNumericString(crypto.randomUUID())).toBe(false) 17 | expect(isNumericString('9007199254740995')).toBe(false) 18 | expect(isNumericString('123456789012345678')).toBe(false) 19 | expect(isNumericString('123123.999999999999')).toBe(false) 20 | }) 21 | 22 | it('invalid on empty', async () => { 23 | expect(isNumericString('')).toBe(false) 24 | expect(isNumericString(' ')).toBe(false) 25 | expect(isNumericString(' ')).toBe(false) 26 | }) 27 | 28 | it('start with number', async () => { 29 | expect(isNumericString('6AAAA')).toBe(false) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/utils.d.ts: -------------------------------------------------------------------------------- 1 | export declare const req: (path: string, options?: RequestInit) => import("undici-types").Request; 2 | type MaybeArray = T | T[]; 3 | export declare const upload: (path: string, fields: Record>) => { 4 | request: import("undici-types").Request; 5 | size: number; 6 | }; 7 | export declare const post: (path: string, body?: Record) => import("undici-types").Request; 8 | export declare const delay: (delay: number) => Promise; 9 | export {}; 10 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | export const req = (path: string, options?: RequestInit) => 2 | new Request(`http://localhost${path}`, options) 3 | 4 | type MaybeArray = T | T[] 5 | 6 | export const upload = ( 7 | path: string, 8 | fields: Record< 9 | string, 10 | MaybeArray< 11 | | (string & {}) 12 | | 'aris-yuzu.jpg' 13 | | 'midori.png' 14 | | 'millenium.jpg' 15 | | 'fake.jpg' 16 | | 'kozeki-ui.webp' 17 | > 18 | > 19 | ) => { 20 | const body = new FormData() 21 | let size = 0 22 | 23 | for (const [key, value] of Object.entries(fields)) { 24 | if (Array.isArray(value)) 25 | value.forEach((value) => { 26 | const file = Bun.file(`./test/images/${value}`) 27 | size += file.size 28 | body.append(key, file) 29 | }) 30 | else if (value.includes('.')) { 31 | const file = Bun.file(`./test/images/${value}`) 32 | size += file.size 33 | body.append(key, file) 34 | } else body.append(key, value) 35 | } 36 | 37 | return { 38 | request: new Request(`http://localhost${path}`, { 39 | method: 'POST', 40 | body 41 | }), 42 | size 43 | } 44 | } 45 | 46 | export const post = (path: string, body?: Record) => 47 | new Request(`http://localhost${path}`, { 48 | method: 'POST', 49 | headers: body 50 | ? { 51 | 'Content-Type': 'application/json' 52 | } 53 | : {}, 54 | body: body ? JSON.stringify(body) : body 55 | }) 56 | 57 | export const delay = (delay: number) => 58 | new Promise((resolve) => setTimeout(resolve, delay)) 59 | -------------------------------------------------------------------------------- /test/validator/encode.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | import { req } from '../utils' 5 | 6 | describe('Encode response', () => { 7 | it('handle default status', async () => { 8 | const app = new Elysia({ 9 | encodeSchema: true 10 | }).get( 11 | '/', 12 | () => ({ 13 | id: 'hello world' 14 | }), 15 | { 16 | response: t.Object({ 17 | id: t 18 | .Transform(t.String()) 19 | .Decode((v) => v) 20 | .Encode(() => 'encoded') 21 | }) 22 | } 23 | ) 24 | 25 | const response = await app.handle(req('/')).then((x) => x.json()) 26 | 27 | expect(response).toEqual({ 28 | id: 'encoded' 29 | }) 30 | }) 31 | 32 | it('handle default named status', async () => { 33 | const app = new Elysia({ 34 | encodeSchema: true 35 | }).get( 36 | '/:id', 37 | ({ error, params: { id } }) => 38 | error(id as any, { 39 | id: 'hello world' 40 | }), 41 | { 42 | params: t.Object({ 43 | id: t.Number() 44 | }), 45 | response: { 46 | 200: t.Object({ 47 | id: t 48 | .Transform(t.String()) 49 | .Decode((v) => v) 50 | .Encode(() => 'encoded 200') 51 | }), 52 | 418: t.Object({ 53 | id: t 54 | .Transform(t.String()) 55 | .Decode((v) => v) 56 | .Encode(() => 'encoded 418') 57 | }) 58 | } 59 | } 60 | ) 61 | 62 | const response = await Promise.all([ 63 | app.handle(req('/200')).then((x) => x.json()), 64 | app.handle(req('/418')).then((x) => x.json()) 65 | ]) 66 | 67 | expect(response[0]).toEqual({ 68 | id: 'encoded 200' 69 | }) 70 | 71 | expect(response[1]).toEqual({ 72 | id: 'encoded 418' 73 | }) 74 | }) 75 | 76 | it('Encode before type check', async () => { 77 | const dto = t.Object({ 78 | value: t 79 | .Transform(t.String()) 80 | .Decode((value) => parseFloat(value)) 81 | .Encode((value) => value.toString()) 82 | }) 83 | 84 | let bodyType = '' 85 | 86 | const elysia = new Elysia({ 87 | experimental: { 88 | encodeSchema: true //open the flag! 89 | } 90 | }).post( 91 | '/', 92 | ({ body }) => { 93 | bodyType = typeof body.value 94 | 95 | return body 96 | }, 97 | { 98 | body: dto, 99 | response: dto 100 | } 101 | ) 102 | 103 | const response = await elysia 104 | .handle( 105 | new Request('http://localhost:3000/', { 106 | method: 'POST', 107 | headers: { 108 | 'Content-Type': 'application/json' 109 | }, 110 | body: JSON.stringify({ value: '1.1' }) 111 | }) 112 | ) 113 | .then((res) => res) 114 | 115 | expect(bodyType).toBe('number') 116 | expect(response.status).toBe(200) 117 | expect(await response.json()).toEqual({ value: '1.1' }) 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /test/validator/exact-mirror.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | import { post, req } from '../utils' 5 | 6 | describe('Exact Mirror', () => { 7 | it('normalize when t.Transform is provided', async () => { 8 | const app = new Elysia({ 9 | normalize: 'exactMirror' 10 | }).get('/', () => ({ count: 2, name: 'foo', extra: 1 }), { 11 | response: t.Object( 12 | { name: t.String(), count: t.Optional(t.Integer()) }, 13 | { additionalProperties: false } 14 | ) 15 | }) 16 | }) 17 | 18 | it('leave incorrect union field as-is', async () => { 19 | const app = new Elysia().post( 20 | '/test', 21 | ({ body }) => { 22 | console.log({ body }) 23 | 24 | return 'Hello Elysia' 25 | }, 26 | { 27 | body: t.Object({ 28 | foo: t.Optional( 29 | t.Nullable( 30 | t.Number({ 31 | // 'foo' but be either number, optional or nullable 32 | error: 'Must be a number' 33 | }) 34 | ) 35 | ) 36 | }) 37 | } 38 | ) 39 | 40 | const response = await app.handle( 41 | post('/test', { 42 | foo: 'asd' 43 | }) 44 | ) 45 | 46 | expect(response.status).toEqual(422) 47 | }) 48 | 49 | it('normalize array response', async () => { 50 | const app = new Elysia().get( 51 | '/', 52 | () => { 53 | return { 54 | messages: [ 55 | { 56 | message: 'Hello, world!', 57 | shouldBeRemoved: true 58 | } 59 | ] 60 | } 61 | }, 62 | { 63 | response: { 64 | 200: t.Object({ 65 | messages: t.Array( 66 | t.Object({ 67 | message: t.String() 68 | }) 69 | ) 70 | }) 71 | } 72 | } 73 | ) 74 | 75 | const response = await app.handle(req('/')).then((x) => x.json()) 76 | 77 | expect(response).toEqual({ 78 | messages: [{ message: 'Hello, world!' }] 79 | }) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /test/validator/validator.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from '../../src' 2 | 3 | import { describe, expect, it } from 'bun:test' 4 | import { post, req } from '../utils' 5 | 6 | describe('Validator Additional Case', () => { 7 | it('validate beforeHandle', async () => { 8 | const app = new Elysia() 9 | .get('/', () => 'Mutsuki need correction 💢💢💢', { 10 | beforeHandle: () => 'Mutsuki need correction 💢💢💢', 11 | response: t.String() 12 | }) 13 | .get('/invalid', () => 1 as any, { 14 | beforeHandle() { 15 | return 1 as any 16 | }, 17 | response: t.String() 18 | }) 19 | 20 | const res = await app.handle(req('/')) 21 | const invalid = await app.handle(req('/invalid')) 22 | 23 | expect(await res.text()).toBe('Mutsuki need correction 💢💢💢') 24 | expect(res.status).toBe(200) 25 | 26 | expect(invalid.status).toBe(422) 27 | }) 28 | 29 | it('validate afterHandle', async () => { 30 | const app = new Elysia() 31 | .get('/', () => 'Mutsuki need correction 💢💢💢', { 32 | afterHandle: () => 'Mutsuki need correction 💢💢💢', 33 | response: t.String() 34 | }) 35 | .get('/invalid', () => 1 as any, { 36 | afterHandle: () => 1 as any, 37 | response: t.String() 38 | }) 39 | 40 | const res = await app.handle(req('/')) 41 | const invalid = await app.handle(req('/invalid')) 42 | 43 | expect(await res.text()).toBe('Mutsuki need correction 💢💢💢') 44 | 45 | expect(res.status).toBe(200) 46 | expect(invalid.status).toBe(422) 47 | }) 48 | 49 | it('validate beforeHandle with afterHandle', async () => { 50 | const app = new Elysia() 51 | .get('/', () => 'Mutsuki need correction 💢💢💢', { 52 | beforeHandle() {}, 53 | afterHandle() { 54 | return 'Mutsuki need correction 💢💢💢' 55 | }, 56 | response: t.String() 57 | }) 58 | .get('/invalid', () => 1 as any, { 59 | afterHandle() { 60 | return 1 as any 61 | }, 62 | response: t.String() 63 | }) 64 | const res = await app.handle(req('/')) 65 | const invalid = await app.handle(req('/invalid')) 66 | 67 | expect(await res.text()).toBe('Mutsuki need correction 💢💢💢') 68 | expect(res.status).toBe(200) 69 | 70 | expect(invalid.status).toBe(422) 71 | }) 72 | 73 | it('handle guard hook', async () => { 74 | const app = new Elysia().guard( 75 | { 76 | query: t.Object({ 77 | name: t.String() 78 | }) 79 | }, 80 | (app) => 81 | app.post('/user', ({ query: { name } }) => name, { 82 | body: t.Object({ 83 | id: t.Number(), 84 | username: t.String(), 85 | profile: t.Object({ 86 | name: t.String() 87 | }) 88 | }) 89 | }) 90 | ) 91 | 92 | const body = JSON.stringify({ 93 | id: 6, 94 | username: '', 95 | profile: { 96 | name: 'A' 97 | } 98 | }) 99 | 100 | const valid = await app.handle( 101 | new Request('http://localhost/user?name=salt', { 102 | method: 'POST', 103 | body, 104 | headers: { 105 | 'content-type': 'application/json', 106 | 'content-length': body.length.toString() 107 | } 108 | }) 109 | ) 110 | 111 | expect(await valid.text()).toBe('salt') 112 | expect(valid.status).toBe(200) 113 | 114 | const invalidQuery = await app.handle( 115 | new Request('http://localhost/user', { 116 | method: 'POST', 117 | body: JSON.stringify({ 118 | id: 6, 119 | username: '', 120 | profile: { 121 | name: 'A' 122 | } 123 | }) 124 | }) 125 | ) 126 | 127 | expect(invalidQuery.status).toBe(422) 128 | 129 | const invalidBody = await app.handle( 130 | new Request('http://localhost/user?name=salt', { 131 | method: 'POST', 132 | headers: { 133 | 'content-type': 'application/json' 134 | }, 135 | body: JSON.stringify({ 136 | id: 6, 137 | username: '', 138 | profile: {} 139 | }) 140 | }) 141 | ) 142 | 143 | expect(invalidBody.status).toBe(422) 144 | }) 145 | 146 | it('inherits cookie on guard', async () => { 147 | const app = new Elysia() 148 | .guard({ 149 | cookie: t.Cookie({ session: t.String() }) 150 | }) 151 | .get('/', ({ cookie: { session } }) => 152 | session.value ? session.value : 'Empty' 153 | ) 154 | 155 | const res = await Promise.all([ 156 | app.handle(req('/')), 157 | app.handle(req('/', { headers: { Cookie: 'session=value' } })) 158 | ]) 159 | 160 | expect(res[0].status).toBe(422) 161 | expect(res[1].status).toBe(200) 162 | }) 163 | }) 164 | -------------------------------------------------------------------------------- /test/ws/connection.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'bun:test' 2 | import { Elysia } from '../../src' 3 | import { newWebsocket, wsOpen, wsClose, wsClosed } from './utils' 4 | import { req } from '../utils' 5 | 6 | describe('WebSocket connection', () => { 7 | it('should connect and close', async () => { 8 | const app = new Elysia() 9 | .ws('/ws', { 10 | message() {} 11 | }) 12 | .listen(0) 13 | 14 | const ws = newWebsocket(app.server!) 15 | 16 | await wsOpen(ws) 17 | await wsClosed(ws) 18 | app.stop() 19 | }) 20 | 21 | it('should close by server', async () => { 22 | const app = new Elysia() 23 | .ws('/ws', { 24 | message(ws) { 25 | ws.close() 26 | } 27 | }) 28 | .listen(0) 29 | 30 | const ws = newWebsocket(app.server!) 31 | 32 | await wsOpen(ws) 33 | 34 | ws.send('close me!') 35 | 36 | const { wasClean, code } = await wsClose(ws) 37 | expect(wasClean).toBe(false) 38 | expect(code).toBe(1000) // going away -> https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1 39 | 40 | app.stop() 41 | }) 42 | 43 | it('should terminate by server', async () => { 44 | const app = new Elysia() 45 | .ws('/ws', { 46 | message(ws) { 47 | ws.terminate() 48 | } 49 | }) 50 | .listen(0) 51 | 52 | const ws = newWebsocket(app.server!) 53 | 54 | await wsOpen(ws) 55 | 56 | ws.send('close me!') 57 | 58 | const { wasClean, code } = await wsClose(ws) 59 | expect(wasClean).toBe(false) 60 | expect(code).toBe(1006) // closed abnormally -> https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1 61 | 62 | app.stop() 63 | }) 64 | 65 | it('should separate get from ws', async () => { 66 | const app = new Elysia() 67 | .ws('/ws', { 68 | message() {} 69 | }) 70 | .get('/ws', () => 'hi') 71 | .listen(0) 72 | 73 | const ws = newWebsocket(app.server!) 74 | 75 | await wsOpen(ws) 76 | await wsClosed(ws) 77 | 78 | const response = await app.handle(req('/ws')).then((x) => x.text()) 79 | expect(response).toBe('hi') 80 | 81 | app.stop() 82 | }) 83 | 84 | it('should separate all from ws', async () => { 85 | const app = new Elysia() 86 | .all('/ws', () => 'hi') 87 | .ws('/ws', { 88 | message() {} 89 | }) 90 | .listen(0) 91 | 92 | const ws = newWebsocket(app.server!) 93 | 94 | await wsOpen(ws) 95 | await wsClosed(ws) 96 | 97 | const response = await app.handle(req('/ws')).then((x) => x.text()) 98 | expect(response).toBe('hi') 99 | 100 | app.stop() 101 | }) 102 | 103 | it('should separate dynamic get from ws', async () => { 104 | const app = new Elysia() 105 | .ws('/ws/:id', { 106 | message() {} 107 | }) 108 | .get('/ws/:id', () => 'hi') 109 | .listen(0) 110 | 111 | const ws = newWebsocket(app.server!, '/ws/1') 112 | 113 | await wsOpen(ws) 114 | await wsClosed(ws) 115 | 116 | const response = await app.handle(req('/ws/1')).then((x) => x.text()) 117 | expect(response).toBe('hi') 118 | 119 | app.stop() 120 | }) 121 | 122 | it('should separate dynamic all from ws', async () => { 123 | const app = new Elysia() 124 | .all('/ws/:id', () => 'hi') 125 | .ws('/ws/:id', { 126 | message() {} 127 | }) 128 | .listen(0) 129 | 130 | const ws = newWebsocket(app.server!, '/ws/1') 131 | 132 | await wsOpen(ws) 133 | await wsClosed(ws) 134 | 135 | const response = await app.handle(req('/ws/1')).then((x) => x.text()) 136 | expect(response).toBe('hi') 137 | 138 | app.stop() 139 | }) 140 | 141 | it('handle derive, resolve', async () => { 142 | let sessionId: string | undefined 143 | let user: { id: '123'; name: 'Jane Doe' } | undefined 144 | 145 | const app = new Elysia() 146 | .derive(() => ({ 147 | sessionId: '123' 148 | })) 149 | .resolve(() => ({ 150 | getUser() { 151 | return { 152 | id: '123', 153 | name: 'Jane Doe' 154 | } as const 155 | } 156 | })) 157 | .ws('/ws', { 158 | open(ws) { 159 | sessionId = ws.data.sessionId 160 | user = ws.data.getUser() 161 | } 162 | }) 163 | .listen(0) 164 | 165 | const ws = newWebsocket(app.server!, '/ws') 166 | 167 | await wsOpen(ws) 168 | await wsClosed(ws) 169 | 170 | expect(sessionId).toEqual('123') 171 | expect(user).toEqual({ 172 | id: '123', 173 | name: 'Jane Doe' 174 | }) 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /test/ws/destructuring.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'bun:test' 2 | import { Elysia } from '../../src' 3 | import { newWebsocket, wsOpen, wsClosed, wsMessage } from './utils' 4 | 5 | describe('WebSocket destructuring', () => { 6 | it('should destructure', async () => { 7 | const app = new Elysia() 8 | .ws('/ws', { 9 | async open(ws) { 10 | const { 11 | subscribe, 12 | isSubscribed, 13 | publish, 14 | unsubscribe, 15 | cork, 16 | send, 17 | // close, 18 | // terminate 19 | } = ws 20 | 21 | subscribe('asdf') 22 | const subscribed = isSubscribed('asdf') 23 | publish('asdf', 'data') 24 | unsubscribe('asdf') 25 | cork(() => ws) 26 | send('Hello!' + subscribed) 27 | // malloc error on macOS 28 | // close() 29 | // terminate() 30 | } 31 | }) 32 | .listen(0) 33 | 34 | const ws = newWebsocket(app.server!) 35 | 36 | const message = wsMessage(ws) 37 | 38 | await wsOpen(ws) 39 | 40 | const { type, data } = await message 41 | 42 | expect(type).toBe('message') 43 | expect(data).toBe('Hello!true') 44 | 45 | await wsClosed(ws) 46 | app.stop() 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/ws/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from 'bun' 2 | 3 | export const newWebsocket = (server: Server, path = '/ws') => 4 | new WebSocket(`ws://${server.hostname}:${server.port}${path}`, {}) 5 | 6 | export const wsOpen = (ws: WebSocket) => 7 | new Promise((resolve) => { 8 | ws.onopen = resolve 9 | }) 10 | 11 | export const wsClose = async (ws: WebSocket) => 12 | new Promise((resolve) => { 13 | ws.onclose = resolve 14 | }) 15 | 16 | export const wsClosed = async (ws: WebSocket) => { 17 | const closed = wsClose(ws) 18 | ws.close() 19 | await closed 20 | } 21 | 22 | export const wsMessage = (ws: WebSocket) => 23 | new Promise>((resolve) => { 24 | ws.onmessage = resolve 25 | }) 26 | --------------------------------------------------------------------------------