diff --git a/.eslintrc.js b/.eslintrc.js
index a0160fdd..3ae11f3f 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -18,7 +18,8 @@ module.exports = {
'@typescript-eslint/no-extra-semi': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-namespace': 'off',
- 'no-case-declarations': 'off'
+ 'no-case-declarations': 'off',
+ 'no-extra-semi': 'off'
},
ignorePatterns: ['example/*', 'tests/**/*']
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc2b6629..e764efac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,49 @@
+# 0.7.0 - 20 Sep 2023
+Feature:
+- rewrite type
+- rewrite Web Socket
+- add mapper method
+- add affix, prefix, suffix
+- trace
+- typeBox.Transfom
+- rewrite Type.ElysiaMeta to use TypeBox.Transform
+- new type:
+ - t.Cookie
+ - t.ObjectString
+ - t.MaybeEmpty
+ - t.Nullable
+- add `Context` to `onError`
+- lifecycle hook now accept array function
+- true encapsulation scope
+
+Improvement:
+- static Code Analysis now support rest parameter
+- breakdown dynamic router into single pipeline instead of inlining to static router to reduce memory usage
+- set `t.File` and `t.Files` to `File` instead of `Blob`
+- skip class instance merging
+- handle `UnknownContextPassToFunction`
+- [#157](https://github.com/elysiajs/elysia/pull/179) WebSocket - added unit tests and fixed example & api by @bogeychan
+- [#179](https://github.com/elysiajs/elysia/pull/179) add github action to run bun test by @arthurfiorette
+
+Breaking Change:
+- remove `ws` plugin, migrate to core
+- rename `addError` to `error`
+
+Change:
+- using single findDynamicRoute instead of inlining to static map
+- remove `mergician`
+- remove array routes due to problem with TypeScript
+
+Bug fix:
+- strictly validate response by default
+- `t.Numeric` not working on headers / query / params
+- `t.Optional(t.Object({ [name]: t.Numeric }))` causing error
+- add null check before converting `Numeric`
+- inherits store to instance plugin
+- handle class overlapping
+- [#187](https://github.com/elysiajs/elysia/pull/187) InternalServerError message fixed to "INTERNAL_SERVER_ERROR" instead of "NOT_FOUND" by @bogeychan
+- [#167](https://github.com/elysiajs/elysia/pull/167) mapEarlyResponse with aot on after handle
+
# 0.6.24 - 18 Sep 2023
Feature:
- [#149](https://github.com/elysiajs/elysia/pulls/149) support additional status codes in redirects
diff --git a/README.md b/README.md
index edfc6d7c..fcf30b9a 100644
--- a/README.md
+++ b/README.md
@@ -3,16 +3,12 @@
-Fast and friendly Bun web framework.
+Ergonomic Framework for Human
Documentation | Discord
-
- Formerly known as KingWorld
-
-
## Philosophies
Building on top of 3 philosophies:
diff --git a/bun.lockb b/bun.lockb
index 4076cf66..67a4fc16 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/example/a.ts b/example/a.ts
index 48f04fcf..d98fe5d6 100644
--- a/example/a.ts
+++ b/example/a.ts
@@ -1,33 +1,12 @@
import { Elysia, t } from '../src'
-import { prisma } from '../../../demo/24/src/auth'
+const one = new Elysia({ name: 'one' }).onRequest(() => console.log('Hello, one!'))
+const two = new Elysia().use(one)
-const setup = new Elysia()
- .state({
- prisma
- })
- .decorate({
- prisma
- })
+const three = new Elysia()
+ .use(one)
+ .use(two)
+ .get('/hello', () => 'Hello, world!')
+ .listen(3000)
-const app = new Elysia()
- .decorate({
- a: 'a'
- })
- .state({
- a: 'a'
- })
- .use(setup)
- .decorate({
- b: 'b'
- })
- .state({
- b: 'b'
- })
- .use(setup)
- .get('/', async ({ prisma }) => {
- const a = (await prisma.authKey.findFirst()) ?? 'Empty'
-
- return a
- })
- .listen(8080)
+// three.handle(new Request('http://localhost:3000/hello'))
diff --git a/example/b.ts b/example/b.ts
index 66728a14..8c5fd44e 100644
--- a/example/b.ts
+++ b/example/b.ts
@@ -1,11 +1,31 @@
-import { Elysia } from '../src'
+import Elysia from '../src'
-const app = new Elysia({
- serve: {
- // can be string, BunFile, TypedArray, Buffer, or array thereof
- key: Bun.file('./key.pem'),
- cert: Bun.file('./cert.pem'),
- // passphrase, only required if key is encrypted
- passphrase: 'super-secret'
- }
-}).listen(3000)
+const app = new Elysia()
+ .get('/', ({ set }) => {
+ set.status = "I'm a teapot"
+
+ return Bun.file('example/teapot.webp')
+ })
+ .trace(async ({ beforeHandle }) => {
+ try {
+ console.log('Start BeforeHandle')
+ const { end } = await beforeHandle
+
+ const a = await end
+ } catch {
+ console.log("A")
+ }
+ })
+ .get('/trace', () => 'a', {
+ beforeHandle: [
+ function setup() {},
+ function error() {
+ throw new Error('A')
+ },
+ function resume() {}
+ ],
+ afterHandle() {
+ console.log('After')
+ }
+ })
+ .listen(3000)
diff --git a/example/b/index.ts b/example/b/index.ts
new file mode 100644
index 00000000..c789d4a0
--- /dev/null
+++ b/example/b/index.ts
@@ -0,0 +1,55 @@
+// setup.ts
+
+import { Elysia } from '../../src'
+
+// database.ts
+
+export const databasePlugin = new Elysia({
+ name: 'database',
+ seed: 'database'
+}).decorate('database', 'a')
+
+// authentication-plugin.ts
+
+export const authenticationPlugin = new Elysia({
+ name: 'authentication',
+ seed: 'authentication'
+})
+ .use(databasePlugin)
+ .derive(async ({ headers, database }) => {
+ // logic
+ })
+
+// setup
+export const setup = new Elysia({ name: 'setup', seed: 'setup' }).use(
+ authenticationPlugin
+)
+
+// register.ts
+
+export const register = new Elysia({ prefix: '/register' })
+ .use(setup)
+ .get('/', async ({ body, set, database }) => {
+ // logic
+ })
+// login.ts
+
+export const login = new Elysia({ prefix: '/login' })
+ .use(setup)
+ .get('/', async ({ body, set, database }) => {
+ // logic
+ })
+// authentication.ts
+export const authenticationRoute = new Elysia({ prefix: '/authentication' })
+ .use(login)
+ .use(register)
+
+export const routes = new Elysia().use(authenticationRoute)
+
+export const v2 = new Elysia({ prefix: '/v2' }).use(routes)
+
+const app = new Elysia()
+ // .use(cors())
+ // .use(bearer())
+ .use(v2)
+ .listen(8080)
diff --git a/example/c.ts b/example/c.ts
index 14637ae4..3c259a03 100644
--- a/example/c.ts
+++ b/example/c.ts
@@ -1,5 +1,14 @@
-import type { Elysia } from "../src";
+import { Elysia, t } from '../src'
-export default function plugin(app: Elysia) {
- return app.get('/from-plugin', () => 'hi')
-}
\ No newline at end of file
+const app = new Elysia()
+ .post('/0.6', ({ body }) => body, {
+ body: t.Union([
+ t.Undefined(),
+ t.Object({
+ name: t.String(),
+ job: t.String(),
+ trait: t.Optional(t.String())
+ })
+ ])
+ })
+ .listen(3000)
diff --git a/example/cookie.ts b/example/cookie.ts
new file mode 100644
index 00000000..e560519c
--- /dev/null
+++ b/example/cookie.ts
@@ -0,0 +1,43 @@
+import { Elysia, getSchemaValidator, t } from '../src'
+
+const app = new Elysia()
+ // .get(
+ // '/council',
+ // ({ cookie: { council } }) =>
+ // (council.value = [
+ // {
+ // name: 'Rin',
+ // affilation: 'Administration'
+ // }
+ // ])
+ // )
+ // .get('/create', ({ cookie: { name } }) => (name.value = 'Himari'))
+ .get(
+ '/update',
+ ({ cookie: { name } }) => {
+ name.value = 'seminar: Rio'
+
+ name.value = 'seminar: Himari'
+
+ name.maxAge = 86400
+
+ return name.value
+ },
+ {
+ cookie: t.Cookie(
+ {
+ name: t.Optional(t.String())
+ },
+ {
+ secrets: 'Fischl von Luftschloss Narfidort',
+ sign: ['name']
+ }
+ )
+ }
+ )
+ // .get('/remove', ({ cookie }) => {
+ // for (const self of Object.values(cookie)) self.remove()
+
+ // return 'Deleted'
+ // })
+ .listen(3000)
diff --git a/example/lazy-module.ts b/example/lazy-module.ts
index b71855c8..c609f062 100644
--- a/example/lazy-module.ts
+++ b/example/lazy-module.ts
@@ -1,15 +1,14 @@
-import { Elysia, SCHEMA } from '../src'
+import { Elysia } from '../src'
const plugin = (app: Elysia) => app.get('/plugin', () => 'Plugin')
const asyncPlugin = async (app: Elysia) => app.get('/async', () => 'A')
const app = new Elysia()
- .decorate('a', () => 'hello')
+ // .decorate('a', () => 'hello')
.use(plugin)
- .use(asyncPlugin)
.use(import('./lazy'))
- .use((app) => app.get('/inline', () => 'inline'))
- .get('/', ({ a }) => a())
+ .use((app) => app.get('/inline', ({ store: { a } }) => 'inline'))
+ // .get('/', ({ a }) => a())
.listen(3000)
await app.modules
diff --git a/example/lazy/index.ts b/example/lazy/index.ts
index ab17b3b7..8f37daa6 100644
--- a/example/lazy/index.ts
+++ b/example/lazy/index.ts
@@ -1,5 +1,5 @@
import Elysia from "../../src";
-export const lazy = (app: Elysia) => app.get('/lazy', () => 'Hi')
+export const lazy = (app: Elysia) => app.state('a', 'b').get('/lazy', () => 'Hi')
export default lazy
diff --git a/example/rename.ts b/example/rename.ts
new file mode 100644
index 00000000..8d565ea3
--- /dev/null
+++ b/example/rename.ts
@@ -0,0 +1,32 @@
+import { Elysia, t } from '../src'
+
+// ? Elysia#83 | Proposal: Standardized way of renaming third party plugin-scoped stuff
+// this would be a plugin provided by a third party
+const myPlugin = new Elysia()
+ .decorate('myProperty', 42)
+ .model('salt', t.String())
+
+new Elysia()
+ .use(
+ myPlugin
+ // map decorator, rename "myProperty" to "renamedProperty"
+ .decorate(({ myProperty, ...decorators }) => ({
+ renamedProperty: myProperty,
+ ...decorators
+ }))
+ // map model, rename "salt" to "pepper"
+ .model(({ salt, ...models }) => ({
+ ...models,
+ pepper: salt
+ }))
+ // Add prefix
+ .prefix('decorator', 'unstable')
+ )
+ .get(
+ '/mapped',
+ ({ unstableRenamedProperty }) => unstableRenamedProperty
+ )
+ .post('/pepper', ({ body }) => body, {
+ body: 'pepper',
+ response: t.String()
+ })
diff --git a/example/schema.ts b/example/schema.ts
index 578e1f4d..8d4e0dde 100644
--- a/example/schema.ts
+++ b/example/schema.ts
@@ -1,4 +1,4 @@
-import { Elysia, t, SCHEMA, DEFS } from '../src'
+import { Elysia, t } from '../src'
const app = new Elysia()
.model({
diff --git a/example/stress/instance.ts b/example/stress/instance.ts
index b84996cb..de8b9da1 100644
--- a/example/stress/instance.ts
+++ b/example/stress/instance.ts
@@ -1,12 +1,18 @@
-import { Elysia } from '../../src'
+import { Elysia, t } from '../../src'
const total = 10_000
-const t = performance.now()
+const setup = new Elysia({ name: 'setup' })
+ .decorate('decorate', 'decorate')
+ .state('state', 'state')
+ .model('model', t.String())
+ .error('error', Error)
-for (let i = 0; i < total; i++) new Elysia()
+const t1 = performance.now()
-const took = performance.now() - t
+for (let i = 0; i < total; i++) new Elysia().use(setup)
+
+const took = performance.now() - t1
console.log(
Intl.NumberFormat().format(total),
diff --git a/example/teapot.webp b/example/teapot.webp
new file mode 100644
index 00000000..cc1b33f6
Binary files /dev/null and b/example/teapot.webp differ
diff --git a/example/trace.ts b/example/trace.ts
new file mode 100644
index 00000000..5dc59052
--- /dev/null
+++ b/example/trace.ts
@@ -0,0 +1,23 @@
+import { Elysia } from '../src'
+
+const logs = []
+
+const app = new Elysia()
+ .trace(async ({ beforeHandle, request, response }) => {
+ const { children, time: start, end } = await beforeHandle
+ for (const child of children) {
+ const { time: start, end, name } = await child
+
+ console.log(name, 'took', (await end) - start, 'ms')
+ }
+ console.log('beforeHandle took', (await end) - start)
+ })
+ .get('/', () => 'Hi', {
+ beforeHandle: [
+ function setup() {},
+ async function delay() {
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ }
+ ]
+ })
+ .listen(3000)
diff --git a/example/websocket.ts b/example/websocket.ts
index b4504500..0e5841fd 100644
--- a/example/websocket.ts
+++ b/example/websocket.ts
@@ -1,7 +1,6 @@
-import { Elysia, t, ws } from '../src'
+import { Elysia } from '../src'
const app = new Elysia()
- .use(ws())
.state('start', 'here')
.ws('/ws', {
open(ws) {
@@ -11,7 +10,7 @@ const app = new Elysia()
ws.publish('asdf', message)
}
})
- .get('/publish/:publish', ({ publish, params: { publish: text } }) => {
+ .get('/publish/:publish', ({ params: { publish: text } }) => {
app.server!.publish('asdf', text)
return text
diff --git a/package.json b/package.json
index 6f0dbccf..c9ffa012 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "elysia",
- "description": "Fast, and friendly Bun web framework",
- "version": "0.6.24",
+ "description": "Ergonomic Framework for Human",
+ "version": "0.7.0",
"author": {
"name": "saltyAom",
"url": "https://github.com/SaltyAom",
@@ -23,6 +23,13 @@
"import": "./dist/ws/index.js",
"default": "./dist/ws/index.js"
},
+ "./compose": {
+ "bun": "./dist/compose.js",
+ "node": "./dist/cjs/compose.js",
+ "require": "./dist/cjs/compose.js",
+ "import": "./dist/compose.js",
+ "default": "./dist/compose.js"
+ },
"./context": {
"bun": "./dist/context.js",
"node": "./dist/cjs/context.js",
@@ -30,12 +37,12 @@
"import": "./dist/context.js",
"default": "./dist/context.js"
},
- "./compose": {
- "bun": "./dist/compose.js",
- "node": "./dist/cjs/compose.js",
- "require": "./dist/cjs/compose.js",
- "import": "./dist/compose.js",
- "default": "./dist/compose.js"
+ "./cookie": {
+ "bun": "./dist/cookie.js",
+ "node": "./dist/cjs/cookie.js",
+ "require": "./dist/cjs/cookie.js",
+ "import": "./dist/cookie.js",
+ "default": "./dist/cookie.js"
},
"./custom-types": {
"bun": "./dist/custom-types.js",
@@ -65,6 +72,13 @@
"import": "./dist/schema.js",
"default": "./dist/schema.js"
},
+ "./trace": {
+ "bun": "./dist/trace.js",
+ "node": "./dist/cjs/trace.js",
+ "require": "./dist/cjs/trace.js",
+ "import": "./dist/trace.js",
+ "default": "./dist/trace.js"
+ },
"./types": {
"bun": "./dist/types.js",
"node": "./dist/cjs/types.js",
@@ -99,7 +113,7 @@
"test:functionality": "bun test && npm run test:node",
"test:types": "tsc --project tsconfig.test.json",
"test:node": "npm install --prefix ./test/node/cjs/ && npm install --prefix ./test/node/esm/ && node ./test/node/cjs/index.js && node ./test/node/esm/index.js",
- "dev": "bun run --hot example/http.ts",
+ "dev": "bun run --watch example/http.ts",
"build": "rimraf dist && npm run build:esm && npm run build:bun && npm run build:cjs",
"build:bun": "bun build.ts",
"build:esm": "swc src -d dist && tsc --project tsconfig.esm.json",
@@ -110,28 +124,31 @@
"release:local": "npm run build && npm run test && npm publish"
},
"dependencies": {
- "@sinclair/typebox": "^0.30.4",
+ "@sinclair/typebox": "^0.31.15",
+ "cookie": "^0.5.0",
+ "cookie-signature": "^1.2.1",
+ "eventemitter3": "^5.0.1",
"fast-querystring": "^1.1.2",
"memoirist": "0.1.4",
- "mergician": "^1.1.0",
"openapi-types": "^12.1.3"
},
"devDependencies": {
"@elysiajs/cookie": "^0.6.1",
- "@elysiajs/html": "^0.6.0",
- "@elysiajs/jwt": "^0.6.2",
- "@elysiajs/swagger": "^0.6.0",
+ "@elysiajs/html": "^0.6.6",
+ "@elysiajs/jwt": "^0.6.4",
+ "@elysiajs/swagger": "^0.6.2",
"@swc/cli": "^0.1.62",
- "@swc/core": "^1.3.71",
- "@types/node": "^20.4.5",
- "@typescript-eslint/eslint-plugin": "^6.2.0",
- "@typescript-eslint/parser": "^6.2.0",
- "bumpp": "^9.1.1",
- "bun-types": "^0.7.0",
- "eslint": "^8.45.0",
+ "@types/cookie": "^0.5.2",
+ "@types/cookie-signature": "^1.1.0",
+ "@swc/core": "^1.3.86",
+ "@typescript-eslint/eslint-plugin": "^6.7.2",
+ "@typescript-eslint/parser": "^6.7.2",
+ "bumpp": "^9.2.0",
+ "bun-types": "^1.0.2",
+ "eslint": "^8.49.0",
"expect-type": "^0.16.0",
"rimraf": "4.4.1",
- "typescript": "^5.1.6"
+ "typescript": "^5.2.2"
},
"peerDependencies": {
"@sinclair/typebox": ">= 0.28.10",
@@ -139,9 +156,6 @@
"typescript": ">= 5.0.0"
},
"peerDependenciesMeta": {
- "@elysiajs/fn": {
- "optional": true
- },
"@sinclair/typebox": {
"optional": true
},
diff --git a/src/compose.ts b/src/compose.ts
index b1668392..775f777e 100644
--- a/src/compose.ts
+++ b/src/compose.ts
@@ -1,7 +1,12 @@
-import type { Elysia } from '.'
+import { type Elysia } from '.'
+
+import { TypeCheck } from '@sinclair/typebox/compiler'
+import type { TAnySchema } from '@sinclair/typebox'
import { parse as parseQuery } from 'fast-querystring'
+import { sign as signCookie } from 'cookie-signature'
+
import { mapEarlyResponse, mapResponse, mapCompactResponse } from './handler'
import {
NotFoundError,
@@ -10,22 +15,114 @@ import {
ERROR_CODE
} from './error'
+import { parseCookie } from './cookie'
+
import type {
- ElysiaConfig,
ComposedHandler,
- HTTPMethod,
- LocalHandler,
- RegisteredHook,
+ ElysiaConfig,
+ Handler,
+ LifeCycleStore,
+ PreHandler,
SchemaValidator,
- BeforeRequestHandler
+ TraceEvent,
+ TraceReporter
} from './types'
-import type { TAnySchema } from '@sinclair/typebox'
-import { TypeCheck } from '@sinclair/typebox/compiler'
-
-const _demoHeaders = new Headers()
+const headersHasToJSON = new Headers().toJSON
const findAliases = new RegExp(` (\\w+) = context`, 'g')
+const requestId = { value: 0 }
+
+const createReport = ({
+ hasTrace,
+ hasTraceSet = false,
+ hasTraceChildren = false,
+ addFn,
+ condition = {}
+}: {
+ hasTrace: boolean | number
+ hasTraceChildren: boolean
+ hasTraceSet?: boolean
+ addFn(string: string): void
+ condition: Partial>
+}) => {
+ if (hasTrace) {
+ const microtask = hasTraceChildren
+ ? '\nawait new Promise(r => {queueMicrotask(() => queueMicrotask(r))})\n'
+ : '\nawait new Promise(r => {queueMicrotask(r)})\n'
+
+ return (
+ event: TraceEvent,
+ {
+ name,
+ attribute = '',
+ unit = 0
+ }: {
+ name?: string
+ attribute?: string
+ unit?: number
+ } = {}
+ ) => {
+ const dotIndex = event.indexOf('.')
+ const isGroup = dotIndex === -1
+
+ if (
+ event !== 'request' &&
+ event !== 'response' &&
+ !condition[
+ (isGroup
+ ? event
+ : event.slice(0, dotIndex)) as keyof typeof condition
+ ]
+ )
+ return () => {
+ if (hasTraceSet && event === 'afterHandle') addFn(microtask)
+ }
+
+ if (isGroup) name ||= event
+ else name ||= 'anonymous'
+
+ addFn(
+ '\n' +
+ `reporter.emit('event', {
+ id,
+ event: '${event}',
+ type: 'begin',
+ name: '${name}',
+ time: performance.now(),
+ ${isGroup ? `unit: ${unit},` : ''}
+ ${attribute}
+ })`.replace(/(\t| |\n)/g, '') +
+ '\n'
+ )
+
+ let handled = false
+
+ return () => {
+ if (handled) return
+
+ handled = true
+ addFn(
+ '\n' +
+ `reporter.emit('event', {
+ id,
+ event: '${event}',
+ type: 'end',
+ time: performance.now()
+ })`.replace(/(\t| |\n)/g, '') +
+ '\n'
+ )
+
+ if (hasTraceSet && event === 'afterHandle') {
+ addFn(microtask)
+ }
+ }
+ }
+ } else {
+ return () => () => {}
+ }
+}
+
export const hasReturn = (fnLiteral: string) => {
const parenthesisEnd = fnLiteral.indexOf(')')
@@ -40,7 +137,14 @@ export const hasReturn = (fnLiteral: string) => {
return fnLiteral.includes('return')
}
-const composeValidationFactory = (hasErrorHandler: boolean) => ({
+const composeValidationFactory = (
+ hasErrorHandler: boolean,
+ {
+ injectResponse = ''
+ }: {
+ injectResponse?: string
+ } = {}
+) => ({
composeValidation: (type: string, value = `c.${type}`) =>
hasErrorHandler
? `c.set.status = 400; throw new ValidationError(
@@ -53,24 +157,34 @@ ${value}
${type},
${value}
).toResponse(c.set.headers)`,
- composeResponseValidation: (value = 'r') =>
- hasErrorHandler
+ composeResponseValidation: (name = 'r') => {
+ const returnError = hasErrorHandler
? `throw new ValidationError(
'response',
response[c.set.status],
-${value}
+${name}
)`
: `return new ValidationError(
'response',
response[c.set.status],
-${value}
+${name}
).toResponse(c.set.headers)`
+
+ return `\n${injectResponse}
+ if(response[c.set.status]?.Check(${name}) === false) {
+ if(!(response instanceof Error))
+ ${returnError}
+}\n`
+ }
})
export const isFnUse = (keyword: string, fnLiteral: string) => {
fnLiteral = fnLiteral.trimStart()
fnLiteral = fnLiteral.replaceAll(/^async /g, '')
+ if (/^(\w+)\(/g.test(fnLiteral))
+ fnLiteral = fnLiteral.slice(fnLiteral.indexOf('('))
+
const argument =
// CharCode 40 is '('
fnLiteral.charCodeAt(0) === 40 || fnLiteral.startsWith('function')
@@ -84,12 +198,15 @@ export const isFnUse = (keyword: string, fnLiteral: string) => {
if (argument === '') return false
+ const restIndex =
+ argument.charCodeAt(0) === 123 ? argument.indexOf('...') : -1
+
// Using object destructuring
if (argument.charCodeAt(0) === 123) {
// Since Function already format the code, styling is enforced
if (argument.includes(keyword)) return true
- return false
+ if (restIndex === -1) return false
}
// Match dot notation and named access
@@ -101,56 +218,149 @@ export const isFnUse = (keyword: string, fnLiteral: string) => {
return true
}
+ const restAlias =
+ restIndex !== -1
+ ? argument.slice(
+ restIndex + 3,
+ argument.indexOf(' ', restIndex + 3)
+ )
+ : undefined
+
+ if (
+ fnLiteral.match(
+ new RegExp(`${restAlias}(.${keyword}|\\["${keyword}"\\])`)
+ )
+ )
+ return true
+
const aliases = [argument]
+ if (restAlias) aliases.push(restAlias)
+
for (const found of fnLiteral.matchAll(findAliases)) aliases.push(found[1])
const destructuringRegex = new RegExp(`{.*?} = (${aliases.join('|')})`, 'g')
- for (const [params] of fnLiteral.matchAll(destructuringRegex)) {
+ for (const [params] of fnLiteral.matchAll(destructuringRegex))
if (params.includes(`{ ${keyword}`) || params.includes(`, ${keyword}`))
return true
- }
return false
}
-export const findElysiaMeta = (
- type: string,
- schema: TAnySchema,
- found: string[] = [],
- parent = ''
-) => {
+const isContextPassToFunction = (fnLiteral: string) => {
+ fnLiteral = fnLiteral.trimStart()
+ fnLiteral = fnLiteral.replaceAll(/^async /g, '')
+
+ if (/^(\w+)\(/g.test(fnLiteral))
+ fnLiteral = fnLiteral.slice(fnLiteral.indexOf('('))
+
+ const argument =
+ // CharCode 40 is '('
+ fnLiteral.charCodeAt(0) === 40 || fnLiteral.startsWith('function')
+ ? // Bun: (context) => {}
+ fnLiteral.slice(
+ fnLiteral.indexOf('(') + 1,
+ fnLiteral.indexOf(')')
+ )
+ : // Node: context => {}
+ fnLiteral.slice(0, fnLiteral.indexOf('=') - 1)
+
+ if (argument === '') return false
+
+ const restIndex =
+ argument.charCodeAt(0) === 123 ? argument.indexOf('...') : -1
+
+ const restAlias =
+ restIndex !== -1
+ ? argument.slice(
+ restIndex + 3,
+ argument.indexOf(' ', restIndex + 3)
+ )
+ : undefined
+
+ const aliases = [argument]
+ if (restAlias) aliases.push(restAlias)
+
+ for (const found of fnLiteral.matchAll(findAliases)) aliases.push(found[1])
+
+ for (const alias of aliases)
+ if (new RegExp(`\\b\\w+\\([^)]*\\b${alias}\\b[^)]*\\)`).test(fnLiteral))
+ return true
+
+ const destructuringRegex = new RegExp(`{.*?} = (${aliases.join('|')})`, 'g')
+
+ for (const [renamed] of fnLiteral.matchAll(destructuringRegex))
+ if (
+ new RegExp(`\\b\\w+\\([^)]*\\b${renamed}\\b[^)]*\\)`).test(
+ fnLiteral
+ )
+ )
+ return true
+
+ return false
+}
+
+const KindSymbol = Symbol.for('TypeBox.Kind')
+
+export const hasType = (type: string, schema: TAnySchema) => {
+ if (!schema) return
+
+ if (KindSymbol in schema && schema[KindSymbol] === type) return true
+
if (schema.type === 'object') {
const properties = schema.properties as Record
- for (const key in properties) {
+ for (const key of Object.keys(properties)) {
const property = properties[key]
- const accessor = !parent ? key : parent + '.' + key
-
if (property.type === 'object') {
- findElysiaMeta(type, property, found, accessor)
- continue
+ if (hasType(type, property)) return true
} else if (property.anyOf) {
- for (const prop of property.anyOf) {
- findElysiaMeta(type, prop, found, accessor)
- }
-
- continue
+ for (let i = 0; i < property.anyOf.length; i++)
+ if (hasType(type, property.anyOf[i])) return true
}
- if (property.elysiaMeta === type) found.push(accessor)
+ if (KindSymbol in property && property[KindSymbol] === type)
+ return true
}
- if (found.length === 0) return null
+ return false
+ }
+
+ return (
+ schema.properties &&
+ KindSymbol in schema.properties &&
+ schema.properties[KindSymbol] === type
+ )
+}
+
+const TransformSymbol = Symbol.for('TypeBox.Transform')
+
+export const hasTransform = (schema: TAnySchema) => {
+ if (!schema) return
+
+ if (schema.type === 'object') {
+ const properties = schema.properties as Record
+ for (const key of Object.keys(properties)) {
+ const property = properties[key]
+
+ if (property.type === 'object') {
+ if (hasTransform(property)) return true
+ } else if (property.anyOf) {
+ for (let i = 0; i < property.anyOf.length; i++)
+ if (hasTransform(property.anyOf[i])) return true
+ }
- return found
- } else if (schema?.elysiaMeta === type) {
- if (parent) found.push(parent)
+ const hasTransformSymbol = TransformSymbol in property
+ if (hasTransformSymbol) return true
+ }
- return 'root'
+ return false
}
- return null
+ return (
+ TransformSymbol in schema ||
+ (schema.properties && TransformSymbol in schema.properties)
+ )
}
/**
@@ -187,6 +397,9 @@ const getUnionedType = (validator: TypeCheck | undefined) => {
if (!foundDifference) return type
}
+
+ // @ts-ignore
+ return validator.schema?.type
}
const matchFnReturn = /(?:return|=>) \S*\(/g
@@ -194,42 +407,40 @@ const matchFnReturn = /(?:return|=>) \S*\(/g
export const isAsync = (fn: Function) => {
if (fn.constructor.name === 'AsyncFunction') return true
- const literal = fn.toString()
-
- if (literal.match(matchFnReturn)) return true
-
- return false
+ return fn.toString().match(matchFnReturn)
}
export const composeHandler = ({
- // path,
+ path,
method,
hooks,
validator,
handler,
handleError,
- meta,
+ definitions,
+ schema,
onRequest,
- config
+ config,
+ reporter
}: {
path: string
- method: HTTPMethod
- hooks: RegisteredHook
+ method: string
+ hooks: LifeCycleStore
validator: SchemaValidator
- handler: LocalHandler
+ handler: Handler
handleError: Elysia['handleError']
- meta?: Elysia['meta']
- onRequest: BeforeRequestHandler[]
+ definitions?: Elysia['definitions']['type']
+ schema?: Elysia['schema']
+ onRequest: PreHandler[]
config: ElysiaConfig
+ reporter: TraceReporter
}): ComposedHandler => {
const hasErrorHandler =
config.forceErrorEncapsulation ||
hooks.error.length > 0 ||
typeof Bun === 'undefined' ||
- hooks.onResponse.length > 0
-
- const { composeValidation, composeResponseValidation } =
- composeValidationFactory(hasErrorHandler)
+ hooks.onResponse.length > 0 ||
+ !!hooks.trace.length
const handleResponse = hooks.onResponse.length
? `\n;(async () => {${hooks.onResponse
@@ -237,10 +448,60 @@ export const composeHandler = ({
.join(';')}})();\n`
: ''
- let fnLiteral = hasErrorHandler ? 'try {\n' : ''
+ const traceLiteral = hooks.trace.map((x) => x.toString())
+
+ let hasUnknownContext = false
+
+ if (isContextPassToFunction(handler.toString())) hasUnknownContext = true
+
+ if (!hasUnknownContext)
+ for (const [key, value] of Object.entries(hooks)) {
+ if (
+ !Array.isArray(value) ||
+ !value.length ||
+ ![
+ 'parse',
+ 'transform',
+ 'beforeHandle',
+ 'afterHandle',
+ 'onResponse'
+ ].includes(key)
+ )
+ continue
+
+ for (const handle of value) {
+ if (typeof handle !== 'function') continue
+
+ if (isContextPassToFunction(handle.toString())) {
+ hasUnknownContext = true
+ break
+ }
+ }
+
+ if (hasUnknownContext) break
+ }
+
+ const traceConditions: Record<
+ Exclude,
+ boolean
+ > = {
+ parse: traceLiteral.some((x) => isFnUse('parse', x)),
+ transform: traceLiteral.some((x) => isFnUse('transform', x)),
+ handle: traceLiteral.some((x) => isFnUse('handle', x)),
+ beforeHandle: traceLiteral.some((x) => isFnUse('beforeHandle', x)),
+ afterHandle: traceLiteral.some((x) => isFnUse('afterHandle', x)),
+ error: hasErrorHandler || traceLiteral.some((x) => isFnUse('error', x))
+ }
+
+ const hasTrace = hooks.trace.length > 0
+ let fnLiteral = ''
+
+ if (hasTrace) fnLiteral += '\nconst id = c.$$requestId\n'
+
+ fnLiteral += hasErrorHandler ? 'try {\n' : ''
const lifeCycleLiteral =
- validator || method !== 'GET'
+ validator || (method !== 'GET' && method !== 'HEAD')
? [
handler,
...hooks.transform,
@@ -250,20 +511,70 @@ export const composeHandler = ({
: []
const hasBody =
- method !== 'GET' &&
- hooks.type !== 'none' &&
- (!!validator.body ||
- !!hooks.type ||
- lifeCycleLiteral.some((fn) => isFnUse('body', fn)))
+ hasUnknownContext ||
+ (method !== 'GET' &&
+ method !== 'HEAD' &&
+ hooks.type !== 'none' &&
+ (!!validator.body ||
+ !!hooks.type ||
+ lifeCycleLiteral.some((fn) => isFnUse('body', fn))))
const hasHeaders =
+ hasUnknownContext ||
validator.headers ||
lifeCycleLiteral.some((fn) => isFnUse('headers', fn))
+ const hasCookie =
+ hasUnknownContext ||
+ validator.cookie ||
+ lifeCycleLiteral.some((fn) => isFnUse('cookie', fn))
+
+ // @ts-ignore
+ const cookieMeta = validator?.cookie?.schema as {
+ secrets?: string | string[]
+ sign: string[] | true
+ properties: { [x: string]: Object }
+ }
+
+ let encodeCookie = ''
+
+ if (cookieMeta?.sign) {
+ if (!cookieMeta.secrets)
+ throw new Error(
+ `t.Cookie required secret which is not set in (${method}) ${path}.`
+ )
+
+ const secret = !cookieMeta.secrets
+ ? undefined
+ : typeof cookieMeta.secrets === 'string'
+ ? cookieMeta.secrets
+ : cookieMeta.secrets[0]
+
+ encodeCookie += `const _setCookie = c.set.cookie
+ if(_setCookie) {`
+
+ if (cookieMeta.sign === true) {
+ // encodeCookie += `if(_setCookie['${name}']?.value) { c.set.cookie['${name}'].value = signCookie(_setCookie['${name}'].value, '${secret}') }\n`
+ encodeCookie += `for(const [key, cookie] of Object.entries(_setCookie)) {
+ c.set.cookie[key].value = signCookie(cookie.value, '${secret}')
+ }`
+ } else
+ for (const name of cookieMeta.sign) {
+ // if (!(name in cookieMeta.properties)) continue
+
+ encodeCookie += `if(_setCookie['${name}']?.value) { c.set.cookie['${name}'].value = signCookie(_setCookie['${name}'].value, '${secret}') }\n`
+ }
+
+ encodeCookie += '}\n'
+ }
+
+ const { composeValidation, composeResponseValidation } =
+ composeValidationFactory(hasErrorHandler)
+
if (hasHeaders) {
// This function is Bun specific
// @ts-ignore
- fnLiteral += _demoHeaders.toJSON
+ fnLiteral += headersHasToJSON
? `c.headers = c.request.headers.toJSON()\n`
: `c.headers = {}
for (const [key, value] of c.request.headers.entries())
@@ -271,8 +582,43 @@ export const composeHandler = ({
`
}
+ if (hasCookie) {
+ const options = cookieMeta
+ ? `{
+ secret: ${
+ cookieMeta.secrets !== undefined
+ ? typeof cookieMeta.secrets === 'string'
+ ? `'${cookieMeta.secrets}'`
+ : '[' +
+ cookieMeta.secrets.reduce(
+ (a, b) => a + `'${b}',`,
+ ''
+ ) +
+ ']'
+ : 'undefined'
+ },
+ sign: ${
+ cookieMeta.sign === true
+ ? true
+ : cookieMeta.sign !== undefined
+ ? '[' +
+ cookieMeta.sign.reduce((a, b) => a + `'${b}',`, '') +
+ ']'
+ : 'undefined'
+ }
+ }`
+ : 'undefined'
+
+ if (hasHeaders)
+ fnLiteral += `\nc.cookie = parseCookie(c.set, c.headers.cookie, ${options})\n`
+ else
+ fnLiteral += `\nc.cookie = parseCookie(c.set, c.request.headers.get('cookie'), ${options})\n`
+ }
+
const hasQuery =
- validator.query || lifeCycleLiteral.some((fn) => isFnUse('query', fn))
+ hasUnknownContext ||
+ validator.query ||
+ lifeCycleLiteral.some((fn) => isFnUse('query', fn))
if (hasQuery) {
fnLiteral += `const url = c.request.url
@@ -285,310 +631,347 @@ export const composeHandler = ({
`
}
+ const traceLiterals = hooks.trace.map((x) => x.toString())
+ const hasTraceSet = traceLiterals.some(
+ (fn) => isFnUse('set', fn) || isContextPassToFunction(fn)
+ )
+ const hasTraceChildren =
+ hasTraceSet &&
+ traceLiterals.some(
+ (fn) => fn.includes('children') || isContextPassToFunction(fn)
+ )
+
+ hasUnknownContext || hooks.trace.some((fn) => isFnUse('set', fn.toString()))
+
const hasSet =
+ hasTraceSet ||
+ hasCookie ||
lifeCycleLiteral.some((fn) => isFnUse('set', fn)) ||
onRequest.some((fn) => isFnUse('set', fn.toString()))
+ const report = createReport({
+ hasTrace,
+ hasTraceSet,
+ hasTraceChildren,
+ condition: traceConditions,
+ addFn: (word) => {
+ fnLiteral += word
+ }
+ })
+
const maybeAsync =
hasBody ||
+ hasTraceSet ||
isAsync(handler) ||
hooks.parse.length > 0 ||
hooks.afterHandle.some(isAsync) ||
hooks.beforeHandle.some(isAsync) ||
hooks.transform.some(isAsync)
+ const endParse = report('parse', {
+ unit: hooks.parse.length
+ })
+
if (hasBody) {
const type = getUnionedType(validator?.body)
- if (hooks.type || type) {
+ if (hooks.type && !Array.isArray(hooks.type)) {
if (hooks.type) {
switch (hooks.type) {
+ case 'json':
case 'application/json':
- fnLiteral += `c.body = await c.request.json();`
+ fnLiteral += `c.body = await c.request.json()\n`
break
+ case 'text':
case 'text/plain':
- fnLiteral += `c.body = await c.request.text();`
+ fnLiteral += `c.body = await c.request.text()\n`
break
+ case 'urlencoded':
case 'application/x-www-form-urlencoded':
- fnLiteral += `c.body = parseQuery(await c.request.text());`
+ fnLiteral += `c.body = parseQuery(await c.request.text())\n`
break
+ case 'arrayBuffer':
case 'application/octet-stream':
- fnLiteral += `c.body = await c.request.arrayBuffer();`
+ fnLiteral += `c.body = await c.request.arrayBuffer()\n`
break
+ case 'formdata':
case 'multipart/form-data':
fnLiteral += `c.body = {}
- const form = await c.request.formData()
- for (const key of form.keys()) {
- if (c.body[key])
- continue
+ const form = await c.request.formData()
+ for (const key of form.keys()) {
+ if (c.body[key])
+ continue
- const value = form.getAll(key)
- if (value.length === 1)
- c.body[key] = value[0]
- else c.body[key] = value
- }`
- break
- }
- } else if (type) {
- // @ts-ignore
- const schema = validator?.body?.schema
-
- switch (type) {
- case 'object':
- if (schema.elysiaMeta === 'URLEncoded') {
- fnLiteral += `c.body = parseQuery(await c.request.text())`
- } // Accept file which means it's formdata
- else if (
- validator.body!.Code().includes("custom('File")
- )
- fnLiteral += `c.body = {}
-
- const form = await c.request.formData()
- for (const key of form.keys()) {
- if (c.body[key])
- continue
-
- const value = form.getAll(key)
- if (value.length === 1)
- c.body[key] = value[0]
- else c.body[key] = value
- }`
- else {
- // Since it's an object an not accepting file
- // we can infer that it's JSON
- fnLiteral += `c.body = JSON.parse(await c.request.text())`
- }
- break
-
- default:
- fnLiteral += 'c.body = await c.request.text()'
+ const value = form.getAll(key)
+ if (value.length === 1)
+ c.body[key] = value[0]
+ else c.body[key] = value
+ }\n`
break
}
}
-
if (hooks.parse.length) fnLiteral += '}}'
} else {
- fnLiteral += '\n'
- fnLiteral += hasHeaders
- ? `let contentType = c.headers['content-type']`
- : `let contentType = c.request.headers.get('content-type')`
-
- fnLiteral += `
- if (contentType) {
- const index = contentType.indexOf(';')
- if (index !== -1) contentType = contentType.substring(0, index)\n`
+ const getAotParser = () => {
+ if (hooks.parse.length && type && !Array.isArray(hooks.type)) {
+ // @ts-ignore
+ const schema = validator?.body?.schema
+
+ switch (type) {
+ case 'object':
+ if (
+ hasType('File', schema) ||
+ hasType('Files', schema)
+ )
+ return `c.body = {}
+
+ const form = await c.request.formData()
+ for (const key of form.keys()) {
+ if (c.body[key])
+ continue
+
+ const value = form.getAll(key)
+ if (value.length === 1)
+ c.body[key] = value[0]
+ else c.body[key] = value
+ }`
+ // else {
+ // // Since it's an object an not accepting file
+ // // we can infer that it's JSON
+ // fnLiteral += `c.body = await c.request.json()\n`
+ // }
+ break
+
+ default:
+ // fnLiteral += defaultParser
+ break
+ }
+ }
+ }
- if (hooks.parse.length) {
- fnLiteral += `let used = false\n`
+ const aotParse = getAotParser()
- for (let i = 0; i < hooks.parse.length; i++) {
- const name = `bo${i}`
+ if (aotParse) fnLiteral += aotParse
+ else {
+ fnLiteral += '\n'
+ fnLiteral += hasHeaders
+ ? `let contentType = c.headers['content-type']`
+ : `let contentType = c.request.headers.get('content-type')`
- if (i !== 0) fnLiteral += `if(!used) {\n`
+ fnLiteral += `
+ if (contentType) {
+ const index = contentType.indexOf(';')
+ if (index !== -1) contentType = contentType.substring(0, index)\n`
- fnLiteral += `let ${name} = parse[${i}](c, contentType);`
- fnLiteral += `if(${name} instanceof Promise) ${name} = await ${name};`
+ if (hooks.parse.length) {
+ fnLiteral += `let used = false\n`
- fnLiteral += `
- if(${name} !== undefined) { c.body = ${name}; used = true }\n`
+ const endReport = report('parse', {
+ unit: hooks.parse.length
+ })
- if (i !== 0) fnLiteral += `}`
- }
+ for (let i = 0; i < hooks.parse.length; i++) {
+ const endUnit = report('parse.unit', {
+ name: hooks.parse[i].name
+ })
- fnLiteral += `if (!used)`
- }
+ const name = `bo${i}`
- fnLiteral += `switch (contentType) {
- case 'application/json':
- c.body = await c.request.json()
- break
+ if (i !== 0) fnLiteral += `if(!used) {\n`
- case 'text/plain':
- c.body = await c.request.text()
- break
+ fnLiteral += `let ${name} = parse[${i}](c, contentType)\n`
+ fnLiteral += `if(${name} instanceof Promise) ${name} = await ${name}\n`
+ fnLiteral += `if(${name} !== undefined) { c.body = ${name}; used = true }\n`
- case 'application/x-www-form-urlencoded':
- c.body = parseQuery(await c.request.text())
- break
+ endUnit()
- case 'application/octet-stream':
- c.body = await c.request.arrayBuffer();
- break
+ if (i !== 0) fnLiteral += `}`
+ }
- case 'multipart/form-data':
- c.body = {}
+ endReport()
+ }
- const form = await c.request.formData()
- for (const key of form.keys()) {
- if (c.body[key])
- continue
+ if (hooks.parse.length) fnLiteral += `if (!used)`
- const value = form.getAll(key)
- if (value.length === 1)
- c.body[key] = value[0]
- else c.body[key] = value
- }
+ fnLiteral += `
+ switch (contentType) {
+ case 'application/json':
+ c.body = await c.request.json()
+ break
+
+ case 'text/plain':
+ c.body = await c.request.text()
+ break
+
+ case 'application/x-www-form-urlencoded':
+ c.body = parseQuery(await c.request.text())
+ break
+
+ case 'application/octet-stream':
+ c.body = await c.request.arrayBuffer();
+ break
+
+ case 'multipart/form-data':
+ c.body = {}
+
+ const form = await c.request.formData()
+ for (const key of form.keys()) {
+ if (c.body[key])
+ continue
+
+ const value = form.getAll(key)
+ if (value.length === 1)
+ c.body[key] = value[0]
+ else c.body[key] = value
+ }
+
+ break
+ }\n`
- break
+ fnLiteral += '}\n'
}
- }\n`
}
fnLiteral += '\n'
}
- if (validator.params) {
- // @ts-ignore
- const properties = findElysiaMeta('Numeric', validator.params.schema)
+ endParse()
- if (properties) {
- switch (typeof properties) {
- case 'object':
- for (const property of properties)
- fnLiteral += `if(c.params.${property}) c.params.${property} = +c.params.${property};`
- break
- }
+ if (hooks?.transform) {
+ const endTransform = report('transform', {
+ unit: hooks.transform.length
+ })
- fnLiteral += '\n'
- }
- }
+ for (let i = 0; i < hooks.transform.length; i++) {
+ const transform = hooks.transform[i]
- if (validator.query) {
- // @ts-ignore
- const properties = findElysiaMeta('Numeric', validator.query.schema)
+ const endUnit = report('transform.unit', {
+ name: transform.name
+ })
- if (properties) {
- switch (typeof properties) {
- case 'object':
- for (const property of properties)
- fnLiteral += `if(c.query.${property}) c.query.${property} = +c.query.${property};`
- break
- }
+ // @ts-ignore
+ if (transform.$elysia === 'derive')
+ fnLiteral += isAsync(hooks.transform[i])
+ ? `Object.assign(c, await transform[${i}](c));`
+ : `Object.assign(c, transform[${i}](c));`
+ else
+ fnLiteral += isAsync(hooks.transform[i])
+ ? `await transform[${i}](c);`
+ : `transform[${i}](c);`
- fnLiteral += '\n'
+ endUnit()
}
+
+ endTransform()
}
- if (validator.headers) {
- // @ts-ignore
- const properties = findElysiaMeta('Numeric', validator.headers.schema)
+ if (validator) {
+ fnLiteral += '\n'
- if (properties) {
- switch (typeof properties) {
- case 'object':
- for (const property of properties)
- fnLiteral += `c.headers.${property} = +c.headers.${property};`
- break
- }
+ if (validator.headers) {
+ fnLiteral += `if(headers.Check(c.headers) === false) {
+ ${composeValidation('headers')}
+ }`
- fnLiteral += '\n'
+ // @ts-ignore
+ if (hasTransform(validator.headers.schema))
+ fnLiteral += `\nc.headers = headers.Decode(c.headers)\n`
}
- }
- if (validator.body) {
- // @ts-ignore
- const numericProperties = findElysiaMeta(
- 'Numeric',
- // @ts-ignore
- validator.body.schema
- )
+ if (validator.params) {
+ fnLiteral += `if(params.Check(c.params) === false) {
+ ${composeValidation('params')}
+ }`
- if (numericProperties) {
- switch (typeof numericProperties) {
- case 'string':
- fnLiteral += `c.body = +c.body;`
- break
+ // @ts-ignore
+ if (hasTransform(validator.params.schema))
+ fnLiteral += `\nc.params = params.Decode(c.params)\n`
+ }
- case 'object':
- for (const property of numericProperties)
- fnLiteral += `c.body.${property} = +c.body.${property};`
- break
- }
+ if (validator.query) {
+ fnLiteral += `if(query.Check(c.query) === false) {
+ ${composeValidation('query')}
+ }`
- fnLiteral += '\n'
+ // @ts-ignore
+ if (hasTransform(validator.query.schema))
+ // Decode doesn't work with Object.create(null)
+ fnLiteral += `\nc.query = query.Decode(Object.assign({}, c.query))\n`
}
- // @ts-ignore
- const filesProperties = findElysiaMeta('Files', validator.body.schema)
- if (filesProperties) {
- switch (typeof filesProperties) {
- case 'object':
- for (const property of filesProperties)
- fnLiteral += `if(!Array.isArray(c.body.${property})) c.body.${property} = [c.body.${property}];`
- break
- }
+ if (validator.body) {
+ fnLiteral += `if(body.Check(c.body) === false) {
+ ${composeValidation('body')}
+ }`
- fnLiteral += '\n'
+ // @ts-ignore
+ if (hasTransform(validator.body.schema))
+ fnLiteral += `\nc.body = body.Decode(c.body)\n`
}
- }
- if (hooks?.transform)
- for (let i = 0; i < hooks.transform.length; i++) {
- const transform = hooks.transform[i]
+ if (validator.cookie) {
+ fnLiteral += `const cookieValue = {}
+ for(const [key, value] of Object.entries(c.cookie))
+ cookieValue[key] = value.value
+
+ if(cookie.Check(cookieValue) === false) {
+ ${composeValidation('cookie', 'cookieValue')}
+ }`
// @ts-ignore
- if (transform.$elysia === 'derive')
- fnLiteral += isAsync(hooks.transform[i])
- ? `Object.assign(c, await transform[${i}](c));`
- : `Object.assign(c, transform[${i}](c));`
- else
- fnLiteral += isAsync(hooks.transform[i])
- ? `await transform[${i}](c);`
- : `transform[${i}](c);`
+ if (hasTransform(validator.cookie.schema))
+ fnLiteral += `\nc.cookie = params.Decode(c.cookie)\n`
}
-
- if (validator) {
- if (validator.headers)
- fnLiteral += `
- if (headers.Check(c.headers) === false) {
- ${composeValidation('headers')}
- }
- `
-
- if (validator.params)
- fnLiteral += `if(params.Check(c.params) === false) { ${composeValidation(
- 'params'
- )} }`
-
- if (validator.query)
- fnLiteral += `if(query.Check(c.query) === false) { ${composeValidation(
- 'query'
- )} }`
-
- if (validator.body)
- fnLiteral += `if(body.Check(c.body) === false) { ${composeValidation(
- 'body'
- )} }`
}
- if (hooks?.beforeHandle)
+ if (hooks?.beforeHandle) {
+ const endBeforeHandle = report('beforeHandle', {
+ unit: hooks.beforeHandle.length
+ })
+
for (let i = 0; i < hooks.beforeHandle.length; i++) {
- const name = `be${i}`
+ const endUnit = report('beforeHandle.unit', {
+ name: hooks.beforeHandle[i].name
+ })
+ const name = `be${i}`
const returning = hasReturn(hooks.beforeHandle[i].toString())
if (!returning) {
fnLiteral += isAsync(hooks.beforeHandle[i])
? `await beforeHandle[${i}](c);\n`
: `beforeHandle[${i}](c);\n`
+
+ endUnit()
} else {
fnLiteral += isAsync(hooks.beforeHandle[i])
? `let ${name} = await beforeHandle[${i}](c);\n`
: `let ${name} = beforeHandle[${i}](c);\n`
+ endUnit()
+
fnLiteral += `if(${name} !== undefined) {\n`
- if (hooks?.afterHandle) {
+ const endAfterHandle = report('afterHandle', {
+ unit: hooks.transform.length
+ })
+ if (hooks.afterHandle) {
const beName = name
for (let i = 0; i < hooks.afterHandle.length; i++) {
const returning = hasReturn(
hooks.afterHandle[i].toString()
)
+ const endUnit = report('afterHandle.unit', {
+ name: hooks.afterHandle[i].name
+ })
+
+ fnLiteral += `c.response = ${beName}\n`
+
if (!returning) {
fnLiteral += isAsync(hooks.afterHandle[i])
? `await afterHandle[${i}](c, ${beName});\n`
@@ -597,130 +980,210 @@ export const composeHandler = ({
const name = `af${i}`
fnLiteral += isAsync(hooks.afterHandle[i])
- ? `const ${name} = await afterHandle[${i}](c, ${beName});\n`
- : `const ${name} = afterHandle[${i}](c, ${beName});\n`
+ ? `const ${name} = await afterHandle[${i}](c);\n`
+ : `const ${name} = afterHandle[${i}](c);\n`
- fnLiteral += `if(${name} !== undefined) { ${beName} = ${name} }\n`
+ fnLiteral += `if(${name} !== undefined) { c.response = ${beName} = ${name} }\n`
}
+
+ endUnit()
}
}
+ endAfterHandle()
if (validator.response)
- fnLiteral += `if(response[c.set.status]?.Check(${name}) === false) {
- if(!(response instanceof Error))
- ${composeResponseValidation(name)}
- }\n`
+ fnLiteral += composeResponseValidation(name)
+ fnLiteral += encodeCookie
fnLiteral += `return mapEarlyResponse(${name}, c.set)}\n`
}
}
+ endBeforeHandle()
+ }
+
if (hooks?.afterHandle.length) {
- fnLiteral += isAsync(handler)
- ? `let r = await handler(c);\n`
- : `let r = handler(c);\n`
+ const endHandle = report('handle', {
+ name: handler.name
+ })
+
+ if (hooks.afterHandle.length)
+ fnLiteral += isAsync(handler)
+ ? `let r = c.response = await handler(c);\n`
+ : `let r = c.response = handler(c);\n`
+ else
+ fnLiteral += isAsync(handler)
+ ? `let r = await handler(c);\n`
+ : `let r = handler(c);\n`
+
+ endHandle()
+
+ const endAfterHandle = report('afterHandle', {
+ unit: hooks.afterHandle.length
+ })
for (let i = 0; i < hooks.afterHandle.length; i++) {
const name = `af${i}`
-
const returning = hasReturn(hooks.afterHandle[i].toString())
+ const endUnit = report('afterHandle.unit', {
+ name: hooks.afterHandle[i].name
+ })
+
if (!returning) {
fnLiteral += isAsync(hooks.afterHandle[i])
- ? `await afterHandle[${i}](c, r)\n`
- : `afterHandle[${i}](c, r)\n`
+ ? `await afterHandle[${i}](c)\n`
+ : `afterHandle[${i}](c)\n`
+
+ endUnit()
} else {
- fnLiteral += isAsync(hooks.afterHandle[i])
- ? `let ${name} = await afterHandle[${i}](c, r)\n`
- : `let ${name} = afterHandle[${i}](c, r)\n`
+ if (validator.response)
+ fnLiteral += isAsync(hooks.afterHandle[i])
+ ? `let ${name} = await afterHandle[${i}](c)\n`
+ : `let ${name} = afterHandle[${i}](c)\n`
+ else
+ fnLiteral += isAsync(hooks.afterHandle[i])
+ ? `let ${name} = mapEarlyResponse(await afterHandle[${i}](c), c.set)\n`
+ : `let ${name} = mapEarlyResponse(afterHandle[${i}](c), c.set)\n`
+
+ endUnit()
if (validator.response) {
fnLiteral += `if(${name} !== undefined) {`
- fnLiteral += `if(response[c.set.status]?.Check(${name}) === false) {
- if(!(response instanceof Error))
- ${composeResponseValidation(name)}
- }\n`
- }
+ fnLiteral += composeResponseValidation(name)
- fnLiteral += `${name} = mapEarlyResponse(${name}, c.set);\n`
- fnLiteral += `if(${name}) return ${name};\n`
+ fnLiteral += `${name} = mapEarlyResponse(${name}, c.set)\n`
- if (validator.response) {
- fnLiteral += '}'
+ fnLiteral += `if(${name}) {`
+ endAfterHandle()
+ fnLiteral += `return ${name} } }`
+ } else {
+ fnLiteral += `if(${name}) {`
+ endAfterHandle()
+ fnLiteral += `return ${name}}\n`
}
}
}
- if (validator.response)
- fnLiteral += `if(response[c.set.status]?.Check(r) === false) {
- if(!(response instanceof Error))
- ${composeResponseValidation()}
- }\n`
+ endAfterHandle()
+
+ fnLiteral += `r = c.response\n`
+ if (validator.response) fnLiteral += composeResponseValidation()
+
+ fnLiteral += encodeCookie
if (hasSet) fnLiteral += `return mapResponse(r, c.set)\n`
else fnLiteral += `return mapCompactResponse(r)\n`
} else {
+ const endHandle = report('handle', {
+ name: handler.name
+ })
+
if (validator.response) {
fnLiteral += isAsync(handler)
? `const r = await handler(c);\n`
: `const r = handler(c);\n`
- fnLiteral += `if(response[c.set.status]?.Check(r) === false) {
- if(!(response instanceof Error))
- ${composeResponseValidation()}
- }\n`
+ endHandle()
+
+ fnLiteral += composeResponseValidation()
+
+ report('afterHandle')()
+
+ fnLiteral += encodeCookie
if (hasSet) fnLiteral += `return mapResponse(r, c.set)\n`
else fnLiteral += `return mapCompactResponse(r)\n`
} else {
- const handled = isAsync(handler)
- ? 'await handler(c) '
- : 'handler(c)'
+ if (traceConditions.handle || hasCookie) {
+ fnLiteral += isAsync(handler)
+ ? `let r = await handler(c);\n`
+ : `let r = handler(c);\n`
- if (hasSet) fnLiteral += `return mapResponse(${handled}, c.set)\n`
- else fnLiteral += `return mapCompactResponse(${handled})\n`
+ endHandle()
+
+ report('afterHandle')()
+
+ fnLiteral += encodeCookie
+
+ if (hasSet) fnLiteral += `return mapResponse(r, c.set)\n`
+ else fnLiteral += `return mapCompactResponse(r)\n`
+ } else {
+ endHandle()
+
+ const handled = isAsync(handler)
+ ? 'await handler(c) '
+ : 'handler(c)'
+
+ report('afterHandle')()
+
+ if (hasSet)
+ fnLiteral += `return mapResponse(${handled}, c.set)\n`
+ else fnLiteral += `return mapCompactResponse(${handled})\n`
+ }
}
}
- if (hasErrorHandler) {
+ if (hasErrorHandler || handleResponse) {
fnLiteral += `
-} catch(error) {
- ${
- ''
- // hasStrictContentType ||
- // // @ts-ignore
- // validator?.body?.schema
- // ? `if(!c.body) error = parseError`
- // : ''
- }
+} catch(error) {`
+
+ if (!maybeAsync) fnLiteral += `return (async () => {`
- ${maybeAsync ? '' : 'return (async () => {'}
- const set = c.set
+ fnLiteral += `const set = c.set
if (!set.status || set.status < 300) set.status = 500
+ `
+
+ const endError = report('error', {
+ unit: hooks.error.length
+ })
+ if (hooks.error.length) {
+ for (let i = 0; i < hooks.error.length; i++) {
+ const name = `er${i}`
+ const endUnit = report('error.unit', {
+ name: hooks.error[i].name
+ })
- ${
- hooks.error.length
- ? `for (let i = 0; i < handleErrors.length; i++) {
- let handled = handleErrors[i]({
+ fnLiteral += `\nlet ${name} = handleErrors[${i}]({
request: c.request,
error: error,
set,
code: error.code ?? error[ERROR_CODE] ?? "UNKNOWN"
- })
- if (handled instanceof Promise) handled = await handled
+ })\n`
- const response = mapEarlyResponse(handled, set)
- if (response) return response
- }`
- : ''
+ if (isAsync(hooks.error[i]))
+ fnLiteral += `if (${name} instanceof Promise) ${name} = await ${name}\n`
+
+ endUnit()
+
+ fnLiteral += `${name} = mapEarlyResponse(${name}, set)\n`
+ fnLiteral += `if (${name}) {`
+ fnLiteral += `return ${name} }\n`
+ }
}
- return handleError(c.request, error, set)
- ${maybeAsync ? '' : '})()'}
-} finally {
- ${handleResponse}
-}`
+ endError()
+
+ fnLiteral += `return handleError(c, error)\n\n`
+
+ if (!maybeAsync) fnLiteral += '})()'
+
+ fnLiteral += '}'
+
+ if (handleResponse || hasTrace) {
+ fnLiteral += ` finally { `
+
+ const endResponse = report('response', {
+ unit: hooks.onResponse.length
+ })
+
+ fnLiteral += handleResponse
+
+ endResponse()
+
+ fnLiteral += `}`
+ }
}
// console.log(fnLiteral)
@@ -741,7 +1204,8 @@ export const composeHandler = ({
headers,
params,
query,
- response
+ response,
+ cookie
},
utils: {
mapResponse,
@@ -754,8 +1218,13 @@ export const composeHandler = ({
ValidationError,
InternalServerError
},
- meta,
- ERROR_CODE
+ schema,
+ definitions,
+ ERROR_CODE,
+ reporter,
+ requestId,
+ parseCookie,
+ signCookie
} = hooks
${
@@ -767,12 +1236,10 @@ export const composeHandler = ({
}
return ${maybeAsync ? 'async' : ''} function(c) {
- ${meta ? 'c["schema"] = meta["schema"]; c["defs"] = meta["defs"];' : ''}
+ ${schema && definitions ? 'c.schema = schema; c.defs = definitions;' : ''}
${fnLiteral}
}`
- // console.log(fnLiteral)
-
const createHandler = Function('hooks', fnLiteral)
return createHandler({
@@ -791,13 +1258,19 @@ export const composeHandler = ({
ValidationError,
InternalServerError
},
- meta,
- ERROR_CODE
+ schema,
+ definitions,
+ ERROR_CODE,
+ reporter,
+ requestId,
+ parseCookie,
+ signCookie
})
}
-export const composeGeneralHandler = (app: Elysia) => {
+export const composeGeneralHandler = (app: Elysia) => {
let decoratorsLiteral = ''
+ let fnLiteral = ''
// @ts-ignore
for (const key of Object.keys(app.decorators))
@@ -806,6 +1279,8 @@ export const composeGeneralHandler = (app: Elysia) => {
// @ts-ignore
const { router, staticRouter } = app
+ const hasTrace = app.event.trace.length > 0
+
const findDynamicRoute = `
const route = find(request.method, path) ${
router.root.ALL ? '?? find("ALL", path)' : ''
@@ -813,11 +1288,7 @@ export const composeGeneralHandler = (app: Elysia) => {
if (route === null)
return ${
app.event.error.length
- ? `handleError(
- request,
- notFound,
- ctx.set
- )`
+ ? `app.handleError(ctx, notFound)`
: `new Response(error404, {
status: 404
})`
@@ -830,14 +1301,18 @@ export const composeGeneralHandler = (app: Elysia) => {
let switchMap = ``
for (const [path, { code, all }] of Object.entries(staticRouter.map))
switchMap += `case '${path}':\nswitch(request.method) {\n${code}\n${
- all ?? `default: ${findDynamicRoute}`
+ all ?? `default: break map`
}}\n\n`
- let fnLiteral = `const {
+ const maybeAsync = app.event.request.some(isAsync)
+
+ fnLiteral += `const {
app,
app: { store, router, staticRouter },
mapEarlyResponse,
- NotFoundError
+ NotFoundError,
+ requestId,
+ reporter
} = data
const notFound = new NotFoundError()
@@ -851,39 +1326,86 @@ export const composeGeneralHandler = (app: Elysia) => {
${app.event.error.length ? '' : `const error404 = notFound.message.toString()`}
- return function(request) {
+ return ${maybeAsync ? 'async' : ''} function map(request) {
`
+ const traceLiteral = app.event.trace.map((x) => x.toString())
+ const report = createReport({
+ hasTrace,
+ hasTraceChildren:
+ hasTrace &&
+ traceLiteral.some(
+ (x) => x.includes('children') || isContextPassToFunction(x)
+ ),
+ condition: {
+ request: traceLiteral.some(
+ (x) => isFnUse('request', x) || isContextPassToFunction(x)
+ )
+ },
+ addFn: (word) => {
+ fnLiteral += word
+ }
+ })
+
if (app.event.request.length) {
fnLiteral += `
+ ${hasTrace ? 'const id = +requestId.value++' : ''}
+
const ctx = {
request,
store,
set: {
+ cookie: {},
headers: {},
status: 200
}
+ ${hasTrace ? ',$$requestId: +id' : ''}
${decoratorsLiteral}
}
+ `
+
+ const endReport = report('request', {
+ attribute: 'ctx',
+ unit: app.event.request.length
+ })
- try {\n`
+ fnLiteral += `try {\n`
for (let i = 0; i < app.event.request.length; i++) {
- const withReturn = hasReturn(app.event.request[i].toString())
+ const fn = app.event.request[i]
+ const withReturn = hasReturn(fn.toString())
+ const maybeAsync = isAsync(fn)
+
+ const endUnit = report('request.unit', {
+ name: app.event.request[i].name
+ })
- fnLiteral += !withReturn
- ? `mapEarlyResponse(onRequest[${i}](ctx), ctx.set);`
- : `const response = mapEarlyResponse(
- onRequest[${i}](ctx),
+ const name = `re${i}`
+
+ if (withReturn) {
+ fnLiteral += `const ${name} = mapEarlyResponse(
+ ${maybeAsync ? 'await' : ''} onRequest[${i}](ctx),
ctx.set
- )
- if (response) return response\n`
+ )\n`
+
+ endUnit()
+
+ fnLiteral += `if(${name}) return ${name}\n`
+ } else {
+ fnLiteral += `${
+ maybeAsync ? 'await' : ''
+ } onRequest[${i}](ctx)\n`
+ endUnit()
+ }
}
fnLiteral += `} catch (error) {
- return handleError(request, error, ctx.set)
- }
-
+ return app.handleError(ctx, error)
+ }`
+
+ endReport()
+
+ fnLiteral += `
const url = request.url,
s = url.indexOf('/', 11),
i = ctx.qi = url.indexOf('?', s + 1),
@@ -897,6 +1419,8 @@ export const composeGeneralHandler = (app: Elysia) => {
? url.substring(s)
: url.substring(s, qi)
+ ${hasTrace ? 'const id = +requestId.value++' : ''}
+
const ctx = {
request,
store,
@@ -906,17 +1430,30 @@ export const composeGeneralHandler = (app: Elysia) => {
headers: {},
status: 200
}
+ ${hasTrace ? ',$$requestId: id' : ''}
${decoratorsLiteral}
}`
+
+ report('request', {
+ unit: app.event.request.length,
+ attribute:
+ traceLiteral.some((x) => isFnUse('context', x)) ||
+ traceLiteral.some((x) => isFnUse('store', x)) ||
+ traceLiteral.some((x) => isFnUse('set', x))
+ ? 'ctx'
+ : ''
+ })()
}
fnLiteral += `
- switch(path) {
+ map: switch(path) {
${switchMap}
default:
- ${findDynamicRoute}
+ break
}
+
+ ${findDynamicRoute}
}`
// @ts-ignore
@@ -928,11 +1465,14 @@ export const composeGeneralHandler = (app: Elysia) => {
)({
app,
mapEarlyResponse,
- NotFoundError
+ NotFoundError,
+ // @ts-ignore
+ reporter: app.reporter,
+ requestId
})
}
-export const composeErrorHandler = (app: Elysia) => {
+export const composeErrorHandler = (app: Elysia) => {
let fnLiteral = `const {
app: { event: { error: onError, onResponse: res } },
mapResponse,
@@ -941,7 +1481,9 @@ export const composeErrorHandler = (app: Elysia) => {
return ${
app.event.error.find(isAsync) ? 'async' : ''
- } function(request, error, set) {`
+ } function(context, error) {
+ const { request, set } = context
+ `
for (let i = 0; i < app.event.error.length; i++) {
const handler = app.event.error[i]
diff --git a/src/context.ts b/src/context.ts
index c24911a3..262a8ca1 100644
--- a/src/context.ts
+++ b/src/context.ts
@@ -1,32 +1,93 @@
-import type { Elysia, TypedRoute } from '.'
+import { HTTPStatusName } from './utils'
+
+import { Cookie, type CookieOptions } from './cookie'
+
+type WithoutNullableKeys = {
+ [Key in keyof Type]-?: NonNullable
+}
+
+import type {
+ DecoratorBase,
+ RouteSchema,
+ Prettify,
+ GetPathParameter
+} from './types'
export type Context<
- Route extends TypedRoute = TypedRoute,
- Store extends Elysia['store'] = Elysia['store']
-> = {
- request: Request
- headers: undefined extends Route['headers']
- ? Record
- : Route['headers']
- query: undefined extends Route['query']
- ? Record
- : Route['query']
- params: Route['params']
- body: Route['body']
- store: Store
- path: string
-
- set: {
- headers: { [header: string]: string } & {
- ['Set-Cookie']?: string | string[]
+ Route extends RouteSchema = RouteSchema,
+ Decorators extends DecoratorBase = {
+ request: {}
+ store: {}
+ },
+ Path extends string = ''
+> = Prettify<
+ {
+ body: Route['body']
+ query: undefined extends Route['query']
+ ? Record
+ : Route['query']
+ params: undefined extends Route['params']
+ ? Path extends `${string}/${':' | '*'}${string}`
+ ? Record, string>
+ : never
+ : Route['params']
+ headers: undefined extends Route['headers']
+ ? Record
+ : Route['headers']
+ cookie: undefined extends Route['cookie']
+ ? Record>
+ : Record> &
+ WithoutNullableKeys<{
+ [key in keyof Route['cookie']]: Cookie<
+ Route['cookie'][key]
+ >
+ }>
+
+ set: {
+ headers: Record & {
+ 'Set-Cookie'?: string | string[]
+ }
+ status?: number | HTTPStatusName
+ redirect?: string
+ cookie?: Record<
+ string,
+ Prettify<
+ {
+ value: string
+ } & CookieOptions
+ >
+ >
}
- status?: number
- redirect?: string
- }
-}
+
+ path: string
+ request: Request
+ store: Decorators['store']
+ } & Decorators['request']
+>
// Use to mimic request before mapping route
export type PreContext<
- Route extends TypedRoute = TypedRoute,
- Store extends Elysia['store'] = Elysia['store']
-> = Omit, 'query' | 'params' | 'body'>
+ Route extends RouteSchema = RouteSchema,
+ Decorators extends DecoratorBase = {
+ request: {}
+ store: {}
+ }
+> = Prettify<
+ {
+ headers: undefined extends Route['headers']
+ ? Record
+ : Route['headers']
+
+ set: {
+ headers: { [header: string]: string } & {
+ ['Set-Cookie']?: string | string[]
+ }
+ status?: number
+ redirect?: string
+ }
+
+ path: string
+ request: Request
+ store: Decorators['store']
+ } & Decorators['request']
+>
diff --git a/src/cookie.ts b/src/cookie.ts
new file mode 100644
index 00000000..fd9f8815
--- /dev/null
+++ b/src/cookie.ts
@@ -0,0 +1,409 @@
+// @ts-ignore
+import { parse } from 'cookie'
+import type { Context } from './context'
+
+import { unsign as unsignCookie } from 'cookie-signature'
+import { InvalidCookieSignature } from './error'
+
+export interface CookieOptions {
+ /**
+ * Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.3|Domain Set-Cookie attribute}. By default, no
+ * domain is set, and most clients will consider the cookie to apply to only
+ * the current domain.
+ */
+ domain?: string | undefined
+
+ /**
+ * Specifies the `Date` object to be the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.1|`Expires` `Set-Cookie` attribute}. By default,
+ * no expiration is set, and most clients will consider this a "non-persistent cookie" and will delete
+ * it on a condition like exiting a web browser application.
+ *
+ * *Note* the {@link https://tools.ietf.org/html/rfc6265#section-5.3|cookie storage model specification}
+ * states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is
+ * possible not all clients by obey this, so if both are set, they should
+ * point to the same date and time.
+ */
+ expires?: Date | undefined
+ /**
+ * Specifies the boolean value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.6|`HttpOnly` `Set-Cookie` attribute}.
+ * When truthy, the `HttpOnly` attribute is set, otherwise it is not. By
+ * default, the `HttpOnly` attribute is not set.
+ *
+ * *Note* be careful when setting this to true, as compliant clients will
+ * not allow client-side JavaScript to see the cookie in `document.cookie`.
+ */
+ httpOnly?: boolean | undefined
+ /**
+ * Specifies the number (in seconds) to be the value for the `Max-Age`
+ * `Set-Cookie` attribute. The given number will be converted to an integer
+ * by rounding down. By default, no maximum age is set.
+ *
+ * *Note* the {@link https://tools.ietf.org/html/rfc6265#section-5.3|cookie storage model specification}
+ * states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is
+ * possible not all clients by obey this, so if both are set, they should
+ * point to the same date and time.
+ */
+ maxAge?: number | undefined
+ /**
+ * Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.4|`Path` `Set-Cookie` attribute}.
+ * By default, the path is considered the "default path".
+ */
+ path?: string | undefined
+ /**
+ * Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1].
+ *
+ * - `'low'` will set the `Priority` attribute to `Low`.
+ * - `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set.
+ * - `'high'` will set the `Priority` attribute to `High`.
+ *
+ * More information about the different priority levels can be found in
+ * [the specification][rfc-west-cookie-priority-00-4.1].
+ *
+ * **note** This is an attribute that has not yet been fully standardized, and may change in the future.
+ * This also means many clients may ignore this attribute until they understand it.
+ */
+ priority?: 'low' | 'medium' | 'high' | undefined
+ /**
+ * Specifies the boolean or string to be the value for the {@link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7|`SameSite` `Set-Cookie` attribute}.
+ *
+ * - `true` will set the `SameSite` attribute to `Strict` for strict same
+ * site enforcement.
+ * - `false` will not set the `SameSite` attribute.
+ * - `'lax'` will set the `SameSite` attribute to Lax for lax same site
+ * enforcement.
+ * - `'strict'` will set the `SameSite` attribute to Strict for strict same
+ * site enforcement.
+ * - `'none'` will set the SameSite attribute to None for an explicit
+ * cross-site cookie.
+ *
+ * More information about the different enforcement levels can be found in {@link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7|the specification}.
+ *
+ * *note* This is an attribute that has not yet been fully standardized, and may change in the future. This also means many clients may ignore this attribute until they understand it.
+ */
+ sameSite?: true | false | 'lax' | 'strict' | 'none' | undefined
+ /**
+ * Specifies the boolean value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.5|`Secure` `Set-Cookie` attribute}. When truthy, the
+ * `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set.
+ *
+ * *Note* be careful when setting this to `true`, as compliant clients will
+ * not send the cookie back to the server in the future if the browser does
+ * not have an HTTPS connection.
+ */
+ secure?: boolean | undefined
+
+ /**
+ * Secret key for signing cookie
+ *
+ * If array is passed, will use Key Rotation.
+ *
+ * Key rotation is when an encryption key is retired
+ * and replaced by generating a new cryptographic key.
+ */
+ secrets?: string | string[]
+}
+
+type MutateCookie = CookieOptions & {
+ value?: T
+} extends infer A
+ ? A | ((previous: A) => A)
+ : never
+
+type CookieJar = Record
+
+export class Cookie implements CookieOptions {
+ public name: string | undefined
+ private setter: Context['set'] | undefined
+
+ constructor(
+ private _value: T,
+ public property: Readonly = {}
+ ) {}
+
+ get() {
+ return this._value
+ }
+
+ get value(): T {
+ return this._value as any
+ }
+
+ set value(value: T) {
+ if (typeof value === 'object') {
+ if (JSON.stringify(this.value) === JSON.stringify(value)) return
+ } else if (this.value === value) return
+
+ this._value = value as any
+
+ this.sync()
+ }
+
+ add(config: MutateCookie): Cookie {
+ const updated = Object.assign(
+ this.property,
+ typeof config === 'function'
+ ? config(Object.assign(this.property, this.value) as any)
+ : config
+ )
+
+ if ('value' in updated) {
+ this._value = updated.value as any
+
+ delete updated.value
+ }
+
+ this.property = updated
+ return this.sync() as any
+ }
+
+ set(config: MutateCookie): Cookie {
+ const updated =
+ typeof config === 'function'
+ ? config(Object.assign(this.property, this.value) as any)
+ : config
+
+ if ('value' in updated) {
+ this._value = updated.value as any
+
+ delete updated.value
+ }
+
+ this.property = updated
+ return this.sync() as any
+ }
+
+ remove() {
+ if (this.value === undefined) return
+
+ this.set({
+ value: '' as any,
+ expires: new Date()
+ })
+ }
+
+ get domain() {
+ return this.property.domain
+ }
+
+ set domain(value) {
+ // @ts-ignore
+ this.property.domain = value
+
+ this.sync()
+ }
+
+ get expires() {
+ return this.property.expires
+ }
+
+ set expires(value) {
+ // @ts-ignore
+ this.property.expires = value
+
+ this.sync()
+ }
+
+ get httpOnly() {
+ return this.property.httpOnly
+ }
+
+ set httpOnly(value) {
+ // @ts-ignore
+ this.property.httpOnly = value
+
+ this.sync()
+ }
+
+ get maxAge() {
+ return this.property.maxAge
+ }
+
+ set maxAge(value) {
+ // @ts-ignore
+ this.property.maxAge = value
+
+ this.sync()
+ }
+
+ get path() {
+ return this.property.path
+ }
+
+ set path(value) {
+ // @ts-ignore
+ this.property.path = value
+
+ this.sync()
+ }
+
+ get priority() {
+ return this.property.priority
+ }
+
+ set priority(value) {
+ // @ts-ignore
+ this.property.priority = value
+
+ this.sync()
+ }
+
+ get sameSite() {
+ return this.property.sameSite
+ }
+
+ set sameSite(value) {
+ // @ts-ignore
+ this.property.sameSite = value
+
+ this.sync()
+ }
+
+ get secure() {
+ return this.property.secure
+ }
+
+ set secure(value) {
+ // @ts-ignore
+ this.property.secure = value
+
+ this.sync()
+ }
+
+ toString() {
+ return typeof this.value === 'object'
+ ? JSON.stringify(this.value)
+ : this.value?.toString() ?? ''
+ }
+
+ private sync() {
+ if (!this.name || !this.setter) return this
+
+ if (!this.setter.cookie)
+ this.setter.cookie = {
+ [this.name]: Object.assign(this.property, {
+ value: this.toString()
+ })
+ }
+ else
+ this.setter.cookie[this.name] = Object.assign(this.property, {
+ value: this.toString()
+ })
+
+ return this
+ }
+}
+
+export const createCookieJar = (initial: CookieJar, set: Context['set']) =>
+ new Proxy(initial as CookieJar, {
+ get(target, key: string) {
+ if (key in target) return target[key]
+
+ // @ts-ignore
+ const cookie = new Cookie(undefined)
+ // @ts-ignore
+ cookie.setter = set
+ cookie.name = key
+
+ // @ts-ignore
+ return cookie
+ },
+ set(target, key: string, value) {
+ if (!(value instanceof Cookie)) return false
+
+ if (!set.cookie) set.cookie = {}
+
+ // @ts-ignore
+ value.setter = set
+ value.name = key
+
+ // @ts-ignore
+ value.sync()
+
+ target[key] = value
+
+ return true
+ }
+ })
+
+export const parseCookie = (
+ set: Context['set'],
+ cookieString?: string | null,
+ {
+ secret,
+ sign
+ }: {
+ secret?: string | string[]
+ sign?: true | string | string[]
+ } = {}
+) => {
+ if (!cookieString) return createCookieJar({}, set)
+
+ const jar: CookieJar = {}
+ const isStringKey = typeof secret === 'string'
+
+ if (sign && sign !== true && !Array.isArray(sign)) sign = [sign]
+
+ const cookieKeys = Object.keys(parse(cookieString))
+ for (let i = 0; i < cookieKeys.length; i++) {
+ const key = cookieKeys[i]
+ let value = parse(cookieString)[key]
+
+ if (sign === true || sign?.includes(key)) {
+ if (!secret)
+ throw new Error('No secret is provided to cookie plugin')
+
+ if (isStringKey) {
+ // @ts-ignore
+ value = unsignCookie(value as string, secret)
+
+ // @ts-ignore
+ if (value === false) throw new InvalidCookieSignature(key)
+ } else {
+ let fail = true
+ for (let i = 0; i < secret.length; i++) {
+ const temp = unsignCookie(value as string, secret[i])
+
+ if (temp !== false) {
+ value = temp
+ fail = false
+ break
+ }
+ }
+
+ if (fail) throw new InvalidCookieSignature(key)
+ }
+ }
+
+ const start = (value as string).charCodeAt(0)
+ if (start === 123 || start === 91)
+ try {
+ const cookie = new Cookie(JSON.parse(value as string))
+
+ // @ts-ignore
+ cookie.setter = set
+ cookie.name = key
+
+ jar[key] = cookie
+
+ continue
+ } catch {
+ // Not empty
+ }
+
+ // @ts-ignore
+ if (!Number.isNaN(+value)) value = +value
+ // @ts-ignore
+ else if (value === 'true') value = true
+ // @ts-ignore
+ else if (value === 'false') value = false
+
+ const cookie = new Cookie(value)
+
+ // @ts-ignore
+ cookie.setter = set
+ cookie.name = key
+
+ jar[key] = cookie
+ }
+
+ return createCookieJar(jar, set)
+}
diff --git a/src/custom-types.ts b/src/custom-types.ts
index 818cd9ae..df5649eb 100644
--- a/src/custom-types.ts
+++ b/src/custom-types.ts
@@ -1,10 +1,18 @@
+import { TypeSystem } from '@sinclair/typebox/system'
import {
Type,
type SchemaOptions,
- type NumericOptions
+ type NumericOptions,
+ type TNull,
+ type TUnion,
+ type TSchema,
+ type TUndefined,
+ TProperties,
+ ObjectOptions,
+ TObject,
+ TNumber
} from '@sinclair/typebox'
-import type { TypeCheck } from '@sinclair/typebox/compiler'
-import { TypeSystem } from '@sinclair/typebox/system'
+import { type TypeCheck } from '@sinclair/typebox/compiler'
try {
TypeSystem.Format('email', (value) =>
@@ -129,39 +137,96 @@ const validateFile = (options: ElysiaTypeOptions.File, value: any) => {
return true
}
+const Files = TypeSystem.Type(
+ 'Files',
+ (options, value) => {
+ if (!Array.isArray(value)) return validateFile(options, value)
+
+ if (options.minItems && value.length < options.minItems) return false
+
+ if (options.maxItems && value.length > options.maxItems) return false
+
+ for (let i = 0; i < value.length; i++)
+ if (!validateFile(options, value[i])) return false
+
+ return true
+ }
+)
+
export const ElysiaType = {
- // Numeric type is for type reference only since it's aliased to t.Number
- Numeric: TypeSystem.Type>(
- 'Numeric',
- {} as any
- ),
- File: TypeSystem.Type('File', validateFile),
- Files: TypeSystem.Type(
- 'Files',
- (options, value) => {
- if (!Array.isArray(value)) return validateFile(options, value)
-
- if (options.minItems && value.length < options.minItems)
- return false
-
- if (options.maxItems && value.length > options.maxItems)
- return false
-
- for (let i = 0; i < value.length; i++)
- if (!validateFile(options, value[i])) return false
-
- return true
+ Numeric: (property?: NumericOptions) =>
+ Type.Transform(Type.Union([Type.String(), Type.Number(property)]))
+ .Decode((value) => {
+ const number = +value
+ if (isNaN(number)) return value
+
+ return number
+ })
+ .Encode((value) => value) as any as TNumber,
+ ObjectString: (
+ properties: T,
+ options?: ObjectOptions
+ ) =>
+ Type.Transform(
+ Type.Union([Type.String(), Type.Object(properties, options)])
+ )
+ .Decode((value) => {
+ if (typeof value === 'string')
+ try {
+ return JSON.parse(value as string)
+ } catch {
+ return value
+ }
+
+ return value
+ })
+ .Encode((value) => JSON.stringify(value)) as any as TObject,
+ File: TypeSystem.Type('File', validateFile),
+ Files: (options: ElysiaTypeOptions.Files) =>
+ Type.Transform(Type.Union([Files(options)]))
+ .Decode((value) => {
+ if (Array.isArray(value)) return value
+ return [value]
+ })
+ .Encode((value) => value),
+ Nullable: (schema: T): TUnion<[T, TNull]> =>
+ ({ ...schema, nullable: true } as any),
+ MaybeEmpty: (schema: T): TUnion<[T, TUndefined]> =>
+ Type.Union([Type.Undefined(), schema]) as any,
+ Cookie: (
+ properties: T,
+ options?: ObjectOptions & {
+ /**
+ * Secret key for signing cookie
+ *
+ * If array is passed, will use Key Rotation.
+ *
+ * Key rotation is when an encryption key is retired
+ * and replaced by generating a new cryptographic key.
+ */
+ secrets?: string | string[]
+ /**
+ * Specified cookie name to be signed globally
+ */
+ sign?: Readonly<(keyof T | (string & {}))[]>
}
- )
+ ): TObject => Type.Object(properties, options)
} as const
+export type TCookie = (typeof ElysiaType)['Cookie']
+
declare module '@sinclair/typebox' {
interface TypeBuilder {
+ ObjectString: typeof ElysiaType.ObjectString
// @ts-ignore
Numeric: typeof ElysiaType.Numeric
+ // @ts-ignore
File: typeof ElysiaType.File
+ // @ts-ignore
Files: typeof ElysiaType.Files
- URLEncoded: (typeof Type)['Object']
+ Nullable: typeof ElysiaType.Nullable
+ MaybeEmpty: typeof ElysiaType.MaybeEmpty
+ Cookie: typeof ElysiaType.Cookie
}
interface SchemaOptions {
@@ -175,27 +240,17 @@ declare module '@sinclair/typebox' {
}
}
+Type.ObjectString = ElysiaType.ObjectString
+
/**
* A Numeric string
*
* Will be parse to Number
*/
-Type.Numeric = (properties) => {
- return Type.Number({
- ...properties,
- elysiaMeta: 'Numeric'
- }) as any
-}
+Type.Numeric = ElysiaType.Numeric
-Type.URLEncoded = (property, options) =>
- Type.Object(property, {
- ...options,
- elysiaMeta: 'URLEncoded'
- })
-
-Type.File = (arg?: ElysiaTypeOptions.File) =>
+Type.File = (arg = {}) =>
ElysiaType.File({
- elysiaMeta: 'File',
default: 'File',
...arg,
extension: arg?.type,
@@ -203,7 +258,7 @@ Type.File = (arg?: ElysiaTypeOptions.File) =>
format: 'binary'
})
-Type.Files = (arg?: ElysiaTypeOptions.Files) =>
+Type.Files = (arg = {}) =>
ElysiaType.Files({
...arg,
elysiaMeta: 'Files',
@@ -218,4 +273,87 @@ Type.Files = (arg?: ElysiaTypeOptions.Files) =>
}
})
+Type.Nullable = (schema) => ElysiaType.Nullable(schema)
+Type.MaybeEmpty = ElysiaType.MaybeEmpty
+
+Type.Cookie = ElysiaType.Cookie
+
export { Type as t }
+
+// type Template =
+// | string
+// | number
+// | bigint
+// | boolean
+// | StringConstructor
+// | NumberConstructor
+// | undefined
+
+// type Join = A extends Readonly<[infer First, ...infer Rest]>
+// ? (
+// First extends Readonly
+// ? First[number]
+// : First extends StringConstructor
+// ? string
+// : First extends NumberConstructor
+// ? `${number}`
+// : First
+// ) extends infer A
+// ? Rest extends []
+// ? A extends undefined
+// ? NonNullable | ''
+// : A
+// : // @ts-ignore
+// A extends undefined
+// ? `${NonNullable}${Join}` | ''
+// : // @ts-ignore
+// `${A}${Join}`
+// : ''
+// : ''
+
+// const template = <
+// const T extends Readonly<(Template | Readonly)[]>
+// >(
+// ...p: T
+// ): Join => {
+// return a as any
+// }
+
+// const create =
+// (t: T): ((t: T) => void) =>
+// (t) =>
+// t
+
+// const optional = <
+// const T extends Readonly<(Template | Readonly)[]>
+// >(
+// ...p: T
+// ): T | undefined => {
+// return undefined
+// }
+
+// template.optional = optional
+
+// const hi = create(
+// template(
+// ['seminar', 'millennium'],
+// ':',
+// ['Rio', 'Yuuka', 'Noa', 'Koyuki'],
+// template.optional(template(',', ['Rio', 'Yuuka', 'Noa', 'Koyuki'])),
+// template.optional(template(',', ['Rio', 'Yuuka', 'Noa', 'Koyuki'])),
+// template.optional(template(',', ['Rio', 'Yuuka', 'Noa', 'Koyuki']))
+// )
+// )
+
+// hi(`seminar:Noa,Koyuki,Yuuka`)
+
+// const a = TypeCompiler.Compile(Type.String())
+
+// console.log(v.Decode.toString())
+
+// const T = Type.Transform(v.schema)
+// .Decode((value) => new Date(value)) // required: number to Date
+// .Encode((value) => value.getTime()) // required: Date to number
+
+// const decoded = Value.Decode(T, 0) // const decoded = Date(1970-01-01T00:00:00.000Z)
+// const encoded = Value.Encode(T, decoded)
diff --git a/src/dynamic-handle.ts b/src/dynamic-handle.ts
index d1eeddce..8741c3dc 100644
--- a/src/dynamic-handle.ts
+++ b/src/dynamic-handle.ts
@@ -1,29 +1,30 @@
import type { Elysia } from '.'
-import {
- ElysiaErrors,
- NotFoundError,
- ValidationError,
- ERROR_CODE
-} from './error'
import { mapEarlyResponse, mapResponse } from './handler'
+import { ElysiaErrors, NotFoundError, ValidationError } from './error'
import type { Context } from './context'
-import type { Handler, RegisteredHook, SchemaValidator } from './types'
import { parse as parseQuery } from 'fast-querystring'
+import { sign as signCookie } from 'cookie-signature'
+import { parseCookie } from './cookie'
+
+import type { Handler, LifeCycleStore, SchemaValidator } from './types'
+
+// JIT Handler
export type DynamicHandler = {
handle: Handler
content?: string
- hooks: RegisteredHook
+ hooks: LifeCycleStore
validator?: SchemaValidator
}
export const createDynamicHandler =
- (app: Elysia) =>
+ (app: Elysia) =>
async (request: Request): Promise => {
const set: Context['set'] = {
+ cookie: {},
status: 200,
headers: {}
}
@@ -39,12 +40,11 @@ export const createDynamicHandler =
context.set = set
context.store = app.store
} else {
- // @ts-ignore
context = {
set,
store: app.store,
request
- }
+ } as any as Context
}
const url = request.url,
@@ -53,12 +53,10 @@ export const createDynamicHandler =
path = q === -1 ? url.substring(s) : url.substring(s, q)
try {
- // @ts-ignore
-
for (let i = 0; i < app.event.request.length; i++) {
// @ts-ignore
const onRequest = app.event.request[i]
- let response = onRequest(context)
+ let response = onRequest(context as any)
if (response instanceof Promise) response = await response
response = mapEarlyResponse(response, set)
@@ -76,7 +74,7 @@ export const createDynamicHandler =
const { handle, hooks, validator, content } = handler.store
let body: string | Record | undefined
- if (request.method !== 'GET') {
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
if (content) {
switch (content) {
case 'application/json':
@@ -117,7 +115,6 @@ export const createDynamicHandler =
if (index !== -1)
contentType = contentType.slice(0, index)
- // @ts-ignore
for (let i = 0; i < app.event.parse.length; i++) {
// @ts-ignore
let temp = app.event.parse[i](context, contentType)
@@ -169,9 +166,44 @@ export const createDynamicHandler =
}
context.body = body
- context.params = handler?.params || {}
+ // @ts-ignore
+ context.params = handler?.params || undefined
context.query = q === -1 ? {} : parseQuery(url.substring(q + 1))
+ context.headers = {}
+ for (const [key, value] of request.headers.entries())
+ context.headers[key] = value
+
+ // @ts-ignore
+ const cookieMeta = validator?.cookie?.schema as {
+ secrets?: string | string[]
+ sign: string[] | true
+ properties: { [x: string]: Object }
+ }
+
+ context.cookie = parseCookie(
+ context.set,
+ context.headers.cookie,
+ cookieMeta
+ ? {
+ secret:
+ cookieMeta.secrets !== undefined
+ ? typeof cookieMeta.secrets === 'string'
+ ? cookieMeta.secrets
+ : cookieMeta.secrets.join(',')
+ : undefined,
+ sign:
+ cookieMeta.sign === true
+ ? true
+ : cookieMeta.sign !== undefined
+ ? typeof cookieMeta.sign === 'string'
+ ? cookieMeta.sign
+ : cookieMeta.sign.join(',')
+ : undefined
+ }
+ : undefined
+ )
+
for (let i = 0; i < hooks.transform.length; i++) {
const operation = hooks.transform[i](context)
@@ -211,6 +243,19 @@ export const createDynamicHandler =
context.query
)
+ if (validator.cookie) {
+ const cookieValue: Record = {}
+ for (const [key, value] of Object.entries(context.cookie))
+ cookieValue[key] = value.value
+
+ if (validator.cookie?.Check(cookieValue) === false)
+ throw new ValidationError(
+ 'cookie',
+ validator.cookie,
+ cookieValue
+ )
+ }
+
if (validator.body?.Check(body) === false)
throw new ValidationError('body', validator.body, body)
}
@@ -221,10 +266,17 @@ export const createDynamicHandler =
// `false` is a falsey value, check for undefined instead
if (response !== undefined) {
+ ;(
+ context as Context & {
+ response: unknown
+ }
+ ).response = response
+
for (let i = 0; i < hooks.afterHandle.length; i++) {
let newResponse = hooks.afterHandle[i](
- context,
- response
+ context as Context & {
+ response: unknown
+ }
)
if (newResponse instanceof Promise)
newResponse = await newResponse
@@ -249,9 +301,19 @@ export const createDynamicHandler =
responseValidator,
response
)
- } else
+ } else {
+ ;(
+ context as Context & {
+ response: unknown
+ }
+ ).response = response
+
for (let i = 0; i < hooks.afterHandle.length; i++) {
- let newResponse = hooks.afterHandle[i](context, response)
+ let newResponse = hooks.afterHandle[i](
+ context as Context & {
+ response: unknown
+ }
+ )
if (newResponse instanceof Promise)
newResponse = await newResponse
@@ -270,6 +332,35 @@ export const createDynamicHandler =
return result
}
}
+ }
+
+ if (context.set.cookie && cookieMeta?.sign) {
+ const secret = !cookieMeta.secrets
+ ? undefined
+ : typeof cookieMeta.secrets === 'string'
+ ? cookieMeta.secrets
+ : cookieMeta.secrets[0]
+
+ if (cookieMeta.sign === true)
+ for (const [key, cookie] of Object.entries(
+ context.set.cookie
+ ))
+ context.set.cookie[key].value = signCookie(
+ cookie.value,
+ '${secret}'
+ )
+ else
+ for (const name of cookieMeta.sign) {
+ if (!(name in cookieMeta.properties)) continue
+
+ if (context.set.cookie[name]?.value) {
+ context.set.cookie[name].value = signCookie(
+ context.set.cookie[name].value,
+ secret as any
+ )
+ }
+ }
+ }
return mapResponse(response, context.set)
} catch (error) {
@@ -277,7 +368,7 @@ export const createDynamicHandler =
set.status = (error as ElysiaErrors).status
// @ts-ignore
- return app.handleError(request, error as Error, set)
+ return app.handleError(context, error)
} finally {
// @ts-ignore
for (const onResponse of app.event.onResponse)
@@ -286,33 +377,23 @@ export const createDynamicHandler =
}
export const createDynamicErrorHandler =
- (app: Elysia) =>
- async (
- request: Request,
- error: ElysiaErrors,
- set: Context['set'] = {
- headers: {}
- }
- ) => {
+ (app: Elysia) =>
+ async (context: Context, error: ElysiaErrors) => {
+ const errorContext = Object.assign(context, error)
+ errorContext.set = context.set
+
// @ts-ignore
for (let i = 0; i < app.event.error.length; i++) {
- // @ts-ignore
- let response = app.event.error[i]({
- request,
- // @ts-ignore
- code: error.code ?? error[ERROR_CODE] ?? 'UNKNOWN',
- error,
- set
- })
+ let response = app.event.error[i](errorContext as any)
if (response instanceof Promise) response = await response
if (response !== undefined && response !== null)
- return mapResponse(response, set)
+ return mapResponse(response, context.set)
}
return new Response(
typeof error.cause === 'string' ? error.cause : error.message,
{
- headers: set.headers,
+ headers: context.set.headers,
status: error.status ?? 500
}
)
diff --git a/src/error.ts b/src/error.ts
index e4e9c14e..895c98f4 100644
--- a/src/error.ts
+++ b/src/error.ts
@@ -18,12 +18,12 @@ export type ElysiaErrors =
| NotFoundError
| ParseError
| ValidationError
+ | InvalidCookieSignature
export class InternalServerError extends Error {
code = 'INTERNAL_SERVER_ERROR'
status = 500
- constructor(message: string)
constructor(message?: string) {
super(message ?? 'INTERNAL_SERVER_ERROR')
}
@@ -33,7 +33,6 @@ export class NotFoundError extends Error {
code = 'NOT_FOUND'
status = 404
- constructor(message: string)
constructor(message?: string) {
super(message ?? 'NOT_FOUND')
}
@@ -43,12 +42,20 @@ export class ParseError extends Error {
code = 'PARSE'
status = 400
- constructor(message: string)
constructor(message?: string) {
super(message ?? 'PARSE')
}
}
+export class InvalidCookieSignature extends Error {
+ code = 'INVALID_COOKIE_SIGNATURE'
+ status = 400
+
+ constructor(public key: string, message?: string) {
+ super(message ?? `"${key}" has invalid cookie signature`)
+ }
+}
+
export class ValidationError extends Error {
code = 'VALIDATION'
status = 400
diff --git a/src/handler.ts b/src/handler.ts
index 9c6307c9..4d7a897c 100644
--- a/src/handler.ts
+++ b/src/handler.ts
@@ -1,15 +1,25 @@
-/* eslint-disable no-case-declarations */
+// @ts-ignore
+import { serialize } from 'cookie'
+import { StatusMap } from './utils'
+
import type { Context } from './context'
+import { Cookie } from './cookie'
const hasHeaderShorthand = 'toJSON' in new Headers()
+type SetResponse = Omit & {
+ status: number
+}
+
export const isNotEmpty = (obj: Object) => {
for (const x in obj) return true
return false
}
-const parseSetCookies = (headers: Headers, setCookie: string[]) => {
+export const parseSetCookies = (headers: Headers, setCookie: string[]) => {
+ if (!headers || !Array.isArray(setCookie)) return headers
+
headers.delete('Set-Cookie')
for (let i = 0; i < setCookie.length; i++) {
@@ -24,95 +34,139 @@ const parseSetCookies = (headers: Headers, setCookie: string[]) => {
return headers
}
-export const mapEarlyResponse = (
+export const cookieToHeader = (cookies: Context['set']['cookie']) => {
+ if (!cookies || typeof cookies !== 'object' || !isNotEmpty(cookies))
+ return undefined
+
+ const set: string[] = []
+
+ for (const [key, property] of Object.entries(cookies)) {
+ if (!key || !property) continue
+
+ if (Array.isArray(property.value)) {
+ for (let i = 0; i < property.value.length; i++) {
+ let value = property.value[i]
+ if (value === undefined || value === null) continue
+
+ if (typeof value === 'object') value = JSON.stringify(value)
+
+ set.push(serialize(key, value, property))
+ }
+ } else {
+ let value = property.value
+ if (value === undefined || value === null) continue
+
+ if (typeof value === 'object') value = JSON.stringify(value)
+
+ set.push(serialize(key, property.value, property))
+ }
+ }
+
+ if (set.length === 0) return undefined
+ if (set.length === 1) return set[0]
+
+ return set
+}
+
+export const mapResponse = (
response: unknown,
set: Context['set']
-): Response | undefined => {
- if (isNotEmpty(set.headers) || set.status !== 200 || set.redirect) {
+): Response => {
+ if (
+ isNotEmpty(set.headers) ||
+ set.status !== 200 ||
+ set.redirect ||
+ set.cookie
+ ) {
+ if (typeof set.status === 'string') set.status = StatusMap[set.status]
+
if (set.redirect) {
set.headers.Location = set.redirect
- if (set.status === 200) set.status = 302
+ if (!set.status || set.status < 300 || set.status >= 400)
+ set.status = 302
}
+ if (set.cookie && isNotEmpty(set.cookie))
+ set.headers['Set-Cookie'] = cookieToHeader(set.cookie)
+
if (
set.headers['Set-Cookie'] &&
Array.isArray(set.headers['Set-Cookie'])
)
- // @ts-ignore
set.headers = parseSetCookies(
new Headers(set.headers),
set.headers['Set-Cookie']
- )
+ ) as any
switch (response?.constructor?.name) {
case 'String':
case 'Blob':
- return new Response(response as string | Blob, set)
+ return new Response(response as string | Blob, {
+ status: set.status,
+ headers: set.headers
+ })
case 'Object':
case 'Array':
- return Response.json(response, set)
+ return Response.json(response, set as SetResponse)
case undefined:
- if (!response) return
+ if (!response) return new Response('', set as SetResponse)
- return Response.json(response, set)
+ return Response.json(response, set as SetResponse)
case 'Response':
- const inherits = Object.assign({}, set.headers)
+ const inherits = { ...set.headers }
if (hasHeaderShorthand)
- // @ts-ignore
set.headers = (response as Response).headers.toJSON()
else
for (const [key, value] of (
response as Response
).headers.entries())
- if (!(key in set.headers)) set.headers[key] = value
+ if (key in set.headers) set.headers[key] = value
for (const key in inherits)
(response as Response).headers.append(key, inherits[key])
- if ((response as Response).status !== set.status)
- set.status = (response as Response).status
-
return response as Response
+ case 'Error':
+ return errorToResponse(response as Error, set)
+
case 'Promise':
// @ts-ignore
- return (response as Promise).then((x) => {
- const r = mapEarlyResponse(x, set)
-
- if (r !== undefined) return r
-
- return
- })
-
- case 'Error':
- return errorToResponse(response as Error, set.headers)
+ return response.then((x) => mapResponse(x, set))
case 'Function':
- return (response as Function)()
+ return mapResponse((response as Function)(), set)
case 'Number':
case 'Boolean':
return new Response(
(response as number | boolean).toString(),
- set
+ set as SetResponse
)
- default:
- if (response instanceof Response) return response
+ case 'Cookie':
+ if (response instanceof Cookie)
+ return new Response(response.value, set as SetResponse)
+
+ return new Response(response?.toString(), set as SetResponse)
+ default:
const r = JSON.stringify(response)
if (r.charCodeAt(0) === 123) {
if (!set.headers['Content-Type'])
set.headers['Content-Type'] = 'application/json'
- return new Response(JSON.stringify(response), set) as any
+ return new Response(
+ JSON.stringify(response),
+ set as SetResponse
+ ) as any
}
- return new Response(r, set)
+ return new Response(r, set as SetResponse)
}
} else
switch (response?.constructor?.name) {
@@ -140,29 +194,34 @@ export const mapEarlyResponse = (
case 'Response':
return response as Response
+ case 'Error':
+ return errorToResponse(response as Error, set)
+
case 'Promise':
// @ts-ignore
- return (response as Promise).then((x) => {
- const r = mapEarlyResponse(x, set)
+ return (response as any as Promise).then((x) => {
+ const r = mapCompactResponse(x)
if (r !== undefined) return r
- return
+ return new Response('')
})
- case 'Error':
- return errorToResponse(response as Error, set.headers)
-
+ // ? Maybe response or Blob
case 'Function':
- return (response as Function)()
+ return mapCompactResponse((response as Function)())
case 'Number':
case 'Boolean':
return new Response((response as number | boolean).toString())
- default:
- if (response instanceof Response) return response
+ case 'Cookie':
+ if (response instanceof Cookie)
+ return new Response(response.value, set as SetResponse)
+
+ return new Response(response?.toString(), set as SetResponse)
+ default:
const r = JSON.stringify(response)
if (r.charCodeAt(0) === 123)
return new Response(JSON.stringify(response), {
@@ -175,42 +234,55 @@ export const mapEarlyResponse = (
}
}
-export const mapResponse = (
+export const mapEarlyResponse = (
response: unknown,
set: Context['set']
-): Response => {
- if (isNotEmpty(set.headers) || set.status !== 200 || set.redirect) {
+): Response | undefined => {
+ if (response === undefined || response === null) return
+
+ if (
+ isNotEmpty(set.headers) ||
+ set.status !== 200 ||
+ set.redirect ||
+ set.cookie
+ ) {
+ if (typeof set.status === 'string') set.status = StatusMap[set.status]
+
if (set.redirect) {
set.headers.Location = set.redirect
- if (set.status === 200) set.status = 302
+
+ if (!set.status || set.status < 300 || set.status >= 400)
+ set.status = 302
}
+ if (set.cookie && isNotEmpty(set.cookie))
+ set.headers['Set-Cookie'] = cookieToHeader(set.cookie)
+
if (
set.headers['Set-Cookie'] &&
Array.isArray(set.headers['Set-Cookie'])
)
- // @ts-ignore
set.headers = parseSetCookies(
new Headers(set.headers),
set.headers['Set-Cookie']
- )
+ ) as any
switch (response?.constructor?.name) {
case 'String':
case 'Blob':
- return new Response(response as string | Blob, {
- status: set.status,
- headers: set.headers
- })
+ return new Response(
+ response as string | Blob,
+ set as SetResponse
+ )
case 'Object':
case 'Array':
- return Response.json(response, set)
+ return Response.json(response, set as SetResponse)
case undefined:
- if (!response) return new Response('', set)
+ if (!response) return
- return Response.json(response, set)
+ return Response.json(response, set as SetResponse)
case 'Response':
const inherits = Object.assign({}, set.headers)
@@ -227,37 +299,53 @@ export const mapResponse = (
for (const key in inherits)
(response as Response).headers.append(key, inherits[key])
- return response as Response
+ if ((response as Response).status !== set.status)
+ set.status = (response as Response).status
- case 'Error':
- return errorToResponse(response as Error, set.headers)
+ return response as Response
case 'Promise':
// @ts-ignore
- return response.then((x) => mapResponse(x, set))
+ return (response as Promise).then((x) => {
+ const r = mapEarlyResponse(x, set)
+
+ if (r !== undefined) return r
+
+ return
+ })
+
+ case 'Error':
+ return errorToResponse(response as Error, set)
case 'Function':
- return (response as Function)()
+ return mapEarlyResponse((response as Function)(), set)
case 'Number':
case 'Boolean':
return new Response(
(response as number | boolean).toString(),
- set
+ set as SetResponse
)
- default:
- if (response instanceof Response) return response
+ case 'Cookie':
+ if (response instanceof Cookie)
+ return new Response(response.value, set as SetResponse)
+ return new Response(response?.toString(), set as SetResponse)
+
+ default:
const r = JSON.stringify(response)
if (r.charCodeAt(0) === 123) {
if (!set.headers['Content-Type'])
set.headers['Content-Type'] = 'application/json'
- return new Response(JSON.stringify(response), set) as any
+ return new Response(
+ JSON.stringify(response),
+ set as SetResponse
+ ) as any
}
- return new Response(r, set)
+ return new Response(r, set as SetResponse)
}
} else
switch (response?.constructor?.name) {
@@ -285,30 +373,33 @@ export const mapResponse = (
case 'Response':
return response as Response
- case 'Error':
- return errorToResponse(response as Error)
-
case 'Promise':
// @ts-ignore
- return (response as any as Promise).then((x) => {
- const r = mapResponse(x, set)
+ return (response as Promise).then((x) => {
+ const r = mapEarlyResponse(x, set)
if (r !== undefined) return r
- return new Response('')
+ return
})
- // ? Maybe response or Blob
+ case 'Error':
+ return errorToResponse(response as Error, set)
+
case 'Function':
- return (response as Function)()
+ return mapCompactResponse((response as Function)())
case 'Number':
case 'Boolean':
return new Response((response as number | boolean).toString())
- default:
- if (response instanceof Response) return response
+ case 'Cookie':
+ if (response instanceof Cookie)
+ return new Response(response.value, set as SetResponse)
+ return new Response(response?.toString(), set as SetResponse)
+
+ default:
const r = JSON.stringify(response)
if (r.charCodeAt(0) === 123)
return new Response(JSON.stringify(response), {
@@ -362,15 +453,13 @@ export const mapCompactResponse = (response: unknown): Response => {
// ? Maybe response or Blob
case 'Function':
- return (response as Function)()
+ return mapCompactResponse((response as Function)())
case 'Number':
case 'Boolean':
return new Response((response as number | boolean).toString())
default:
- if (response instanceof Response) return response
-
const r = JSON.stringify(response)
if (r.charCodeAt(0) === 123)
return new Response(JSON.stringify(response), {
@@ -383,7 +472,7 @@ export const mapCompactResponse = (response: unknown): Response => {
}
}
-export const errorToResponse = (error: Error, headers?: HeadersInit) =>
+export const errorToResponse = (error: Error, set?: Context['set']) =>
new Response(
JSON.stringify({
name: error?.name,
@@ -391,7 +480,7 @@ export const errorToResponse = (error: Error, headers?: HeadersInit) =>
cause: error?.cause
}),
{
- status: 500,
- headers
+ status: set?.status !== 200 ? (set?.status as number) ?? 500 : 500,
+ headers: set?.headers
}
)
diff --git a/src/index.ts b/src/index.ts
index 7ac582d9..4d321680 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,80 +1,81 @@
+import type { Serve, Server, ServerWebSocket } from 'bun'
+
import { Memoirist } from 'memoirist'
-import type { Serve, Server } from 'bun'
+import EventEmitter from 'eventemitter3'
+import type { Static, TSchema } from '@sinclair/typebox'
+
+import { createTraceListener } from './trace'
+import type { Context } from './context'
+import { ElysiaWS, websocket } from './ws'
+import type { WS } from './ws/types'
+
+import { isNotEmpty } from './handler'
+import {
+ composeHandler,
+ composeGeneralHandler,
+ composeErrorHandler
+} from './compose'
import {
mergeHook,
getSchemaValidator,
getResponseSchemaValidator,
mergeDeep,
+ mergeCookie,
checksum,
mergeLifeCycle,
filterGlobalHook,
asGlobal
} from './utils'
-import type { Context } from './context'
import {
- composeErrorHandler,
- composeGeneralHandler,
- composeHandler
-} from './compose'
+ createDynamicErrorHandler,
+ createDynamicHandler,
+ type DynamicHandler
+} from './dynamic-handle'
-import { ws } from './ws'
-import type { ElysiaWSContext, ElysiaWSOptions, WSTypedSchema } from './ws'
+import {
+ isProduction,
+ ERROR_CODE,
+ ValidationError,
+ type ParseError,
+ type NotFoundError,
+ type InternalServerError
+} from './error'
import type {
- Handler,
- RegisteredHook,
- VoidRequestHandler,
- TypedRoute,
- ElysiaInstance,
ElysiaConfig,
- HTTPMethod,
+ DecoratorBase,
+ DefinitionBase,
+ RouteBase,
+ Handler,
ComposedHandler,
+ InputSchema,
+ LocalHook,
+ MergeSchema,
+ RouteSchema,
+ UnwrapRoute,
InternalRoute,
- BodyParser,
+ HTTPMethod,
+ SchemaValidator,
+ VoidHandler,
+ PreHandler,
+ BodyHandler,
+ OptionalHandler,
+ AfterHandler,
ErrorHandler,
- TypedSchema,
- LocalHook,
- LocalHandler,
- LifeCycle,
- LifeCycleEvent,
LifeCycleStore,
- VoidLifeCycle,
- AfterRequestHandler,
- SchemaValidator,
- IsAny,
- OverwritableTypeRoute,
- MergeSchema,
- ListenCallback,
- NoReturnHandler,
MaybePromise,
Prettify,
- TypedWSRouteToEden,
- UnwrapSchema,
- ExtractPath,
- TypedSchemaToRoute,
- DeepWritable,
- Reconciliation,
- BeforeRequestHandler,
- ElysiaDefaultMeta
+ ListenCallback,
+ AddPrefix,
+ AddSuffix,
+ AddPrefixCapitalize,
+ AddSuffixCapitalize,
+ TraceReporter,
+ TraceHandler,
+ MaybeArray
} from './types'
-import type { Static, TSchema } from '@sinclair/typebox'
-
-import {
- type ValidationError,
- type ParseError,
- type NotFoundError,
- type InternalServerError,
- isProduction,
- ERROR_CODE
-} from './error'
-
-import {
- createDynamicErrorHandler,
- createDynamicHandler,
- type DynamicHandler
-} from './dynamic-handle'
/**
* ### Elysia Server
@@ -92,38 +93,34 @@ import {
*/
export default class Elysia<
BasePath extends string = '',
- Instance extends ElysiaInstance<{
- store?: Record
- request?: Record
- error?: Record
- schema?: TypedSchema
- meta?: ElysiaDefaultMeta
- }> = {
- store: {}
+ Decorators extends DecoratorBase = {
request: {}
- schema: {}
+ store: {}
+ },
+ Definitions extends DefinitionBase = {
+ type: {}
error: {}
- meta: {
- schema: {}
- defs: {}
- exposed: {}
- }
- }
+ },
+ ParentSchema extends RouteSchema = {},
+ Routes extends RouteBase = {},
+ Scoped extends boolean = false
> {
config: ElysiaConfig
private dependencies: Record = {}
- store: Instance['store'] = {}
- meta: Instance['meta'] = {
- schema: Object.create(null),
- defs: Object.create(null),
- exposed: Object.create(null)
+ store: Decorators['store'] = {}
+ private decorators = {} as Decorators['request']
+ private definitions = {
+ type: {},
+ error: {}
+ } as {
+ type: Definitions['type']
+ error: Definitions['error']
}
- // Will be applied to Context
- private decorators: Instance['request'] = {}
+ schema = {} as Routes
- event: LifeCycleStore = {
+ event: LifeCycleStore = {
start: [],
request: [],
parse: [],
@@ -131,17 +128,18 @@ export default class Elysia<
beforeHandle: [],
afterHandle: [],
onResponse: [],
+ trace: [],
error: [],
stop: []
}
- server: Server | null = null
+ reporter: TraceReporter = new EventEmitter()
- private $schema: SchemaValidator | null = null
- private error: Instance['error'] = {}
+ server: Server | null = null
+ private validator: SchemaValidator | null = null
private router = new Memoirist()
- routes: InternalRoute[] = []
+ routes: InternalRoute[] = []
private staticRouter = {
handlers: [] as ComposedHandler[],
@@ -155,19 +153,19 @@ export default class Elysia<
>,
all: ''
}
- private wsRouter: Memoirist | undefined
private dynamicRouter = new Memoirist()
private lazyLoadModules: Promise>[] = []
path: BasePath = '' as any
- constructor(config?: Partial>) {
+ constructor(config?: Partial>) {
this.config = {
forceErrorEncapsulation: false,
prefix: '',
aot: true,
strictPath: false,
scoped: false,
+ cookie: {},
...config,
seed: config?.seed === undefined ? '' : config?.seed
} as any
@@ -175,184 +173,213 @@ export default class Elysia<
private add(
method: HTTPMethod,
- path: string,
- handler: LocalHandler,
- hook?: LocalHook,
+ paths: string | Readonly,
+ handler: Handler,
+ hook?: LocalHook,
{ allowMeta = false, skipPrefix = false } = {
allowMeta: false as boolean | undefined,
skipPrefix: false as boolean | undefined
}
) {
- path =
- path === '' ? path : path.charCodeAt(0) === 47 ? path : `/${path}`
-
- if (this.config.prefix && !skipPrefix) path = this.config.prefix + path
-
- const defs = this.meta.defs
-
- if (hook?.type)
- switch (hook.type) {
- case 'text':
- hook.type = 'text/plain'
- break
-
- case 'json':
- hook.type = 'application/json'
- break
-
- case 'formdata':
- hook.type = 'multipart/form-data'
- break
-
- case 'urlencoded':
- hook.type = 'application/x-www-form-urlencoded'
- break
-
- case 'arrayBuffer':
- hook.type = 'application/octet-stream'
- break
+ if (typeof paths === 'string') paths = [paths]
+
+ for (let path of paths) {
+ path =
+ path === ''
+ ? path
+ : path.charCodeAt(0) === 47
+ ? path
+ : `/${path}`
+
+ if (this.config.prefix && !skipPrefix)
+ path = this.config.prefix + path
+
+ if (hook?.type)
+ switch (hook.type) {
+ case 'text':
+ hook.type = 'text/plain'
+ break
+
+ case 'json':
+ hook.type = 'application/json'
+ break
+
+ case 'formdata':
+ hook.type = 'multipart/form-data'
+ break
+
+ case 'urlencoded':
+ hook.type = 'application/x-www-form-urlencoded'
+ break
+
+ case 'arrayBuffer':
+ hook.type = 'application/octet-stream'
+ break
+
+ default:
+ break
+ }
- default:
- break
- }
+ const models = this.definitions.type as Record
- const validator = {
- body: getSchemaValidator(
- hook?.body ?? (this.$schema?.body as any),
- {
- dynamic: !this.config.aot,
- models: defs
- }
- ),
- headers: getSchemaValidator(
- hook?.headers ?? (this.$schema?.headers as any),
+ const cookieValidator = getSchemaValidator(
+ hook?.cookie ?? (this.validator?.cookie as any),
{
dynamic: !this.config.aot,
- models: defs,
+ models,
additionalProperties: true
}
- ),
- params: getSchemaValidator(
- hook?.params ?? (this.$schema?.params as any),
- {
- dynamic: !this.config.aot,
- models: defs
- }
- ),
- query: getSchemaValidator(
- hook?.query ?? (this.$schema?.query as any),
- {
- dynamic: !this.config.aot,
- models: defs
- }
- ),
- response: getResponseSchemaValidator(
- hook?.response ?? (this.$schema?.response as any),
- {
- dynamic: !this.config.aot,
- models: defs
- }
)
- } as any
- const hooks = mergeHook(this.event, hook as RegisteredHook)
- const loosePath = path.endsWith('/')
- ? path.slice(0, path.length - 1)
- : path + '/'
+ if (cookieValidator && isNotEmpty(this.config.cookie ?? []))
+ // @ts-ignore
+ cookieValidator.schema = mergeCookie(
+ // @ts-ignore
+ cookieValidator.schema,
+ this.config.cookie ?? {}
+ )
- if (this.config.aot === false) {
- this.dynamicRouter.add(method, path, {
- validator,
- hooks,
- content: hook?.type as string,
- handle: handler
- })
+ const validator = {
+ body: getSchemaValidator(
+ hook?.body ?? (this.validator?.body as any),
+ {
+ dynamic: !this.config.aot,
+ models
+ }
+ ),
+ headers: getSchemaValidator(
+ hook?.headers ?? (this.validator?.headers as any),
+ {
+ dynamic: !this.config.aot,
+ models,
+ additionalProperties: true
+ }
+ ),
+ params: getSchemaValidator(
+ hook?.params ?? (this.validator?.params as any),
+ {
+ dynamic: !this.config.aot,
+ models
+ }
+ ),
+ query: getSchemaValidator(
+ hook?.query ?? (this.validator?.query as any),
+ {
+ dynamic: !this.config.aot,
+ models
+ }
+ ),
+ cookie: cookieValidator,
+ response: getResponseSchemaValidator(
+ hook?.response ?? (this.validator?.response as any),
+ {
+ dynamic: !this.config.aot,
+ models
+ }
+ )
+ } as any
+
+ const hooks = mergeHook(this.event, hook)
+ const loosePath = path.endsWith('/')
+ ? path.slice(0, path.length - 1)
+ : path + '/'
- if (this.config.strictPath === false) {
- this.dynamicRouter.add(method, loosePath, {
+ if (this.config.aot === false) {
+ this.dynamicRouter.add(method, path, {
validator,
hooks,
content: hook?.type as string,
handle: handler
})
+
+ if (this.config.strictPath === false) {
+ this.dynamicRouter.add(method, loosePath, {
+ validator,
+ hooks,
+ content: hook?.type as string,
+ handle: handler
+ })
+ }
+
+ this.routes.push({
+ method,
+ path,
+ composed: null,
+ handler,
+ hooks: hooks as any
+ })
+
+ return
}
+ const mainHandler = composeHandler({
+ path,
+ method,
+ hooks,
+ validator,
+ handler,
+ handleError: this.handleError,
+ onRequest: this.event.request,
+ config: this.config,
+ definitions: allowMeta ? this.definitions.type : undefined,
+ schema: allowMeta ? this.schema : undefined,
+ reporter: this.reporter
+ })
+
this.routes.push({
method,
path,
- composed: null,
+ composed: mainHandler,
handler,
- hooks
+ hooks: hooks as any
})
- return
- }
-
- const mainHandler = composeHandler({
- path,
- method,
- hooks,
- validator,
- handler,
- handleError: this.handleError,
- meta: allowMeta ? this.meta : undefined,
- onRequest: this.event.request,
- config: this.config
- })
-
- this.routes.push({
- method,
- path,
- composed: mainHandler,
- handler,
- hooks
- })
-
- if (path.indexOf(':') === -1 && path.indexOf('*') === -1) {
- const index = this.staticRouter.handlers.length
- this.staticRouter.handlers.push(mainHandler)
+ if (path.indexOf(':') === -1 && path.indexOf('*') === -1) {
+ const index = this.staticRouter.handlers.length
+ this.staticRouter.handlers.push(mainHandler)
- this.staticRouter.variables += `const st${index} = staticRouter.handlers[${index}]\n`
-
- if (!this.staticRouter.map[path])
- this.staticRouter.map[path] = {
- code: ''
- }
+ this.staticRouter.variables += `const st${index} = staticRouter.handlers[${index}]\n`
- if (method === 'ALL')
- this.staticRouter.map[
- path
- ].all = `default: return st${index}(ctx)\n`
- else
- this.staticRouter.map[
- path
- ].code += `case '${method}': return st${index}(ctx)\n`
-
- if (!this.config.strictPath) {
- if (!this.staticRouter.map[loosePath])
- this.staticRouter.map[loosePath] = {
+ if (!this.staticRouter.map[path])
+ this.staticRouter.map[path] = {
code: ''
}
if (method === 'ALL')
this.staticRouter.map[
- loosePath
+ path
].all = `default: return st${index}(ctx)\n`
else
this.staticRouter.map[
- loosePath
+ path
].code += `case '${method}': return st${index}(ctx)\n`
+
+ if (!this.config.strictPath) {
+ if (!this.staticRouter.map[loosePath])
+ this.staticRouter.map[loosePath] = {
+ code: ''
+ }
+
+ if (method === 'ALL')
+ this.staticRouter.map[
+ loosePath
+ ].all = `default: return st${index}(ctx)\n`
+ else
+ this.staticRouter.map[
+ loosePath
+ ].code += `case '${method}': return st${index}(ctx)\n`
+ }
+ } else {
+ this.router.add(method, path, mainHandler)
+ if (!this.config.strictPath)
+ this.router.add(
+ method,
+ path.endsWith('/')
+ ? path.slice(0, path.length - 1)
+ : path + '/',
+ mainHandler
+ )
}
- } else {
- this.router.add(method, path, mainHandler)
- if (!this.config.strictPath)
- this.router.add(
- method,
- path.endsWith('/')
- ? path.slice(0, path.length - 1)
- : path + '/',
- mainHandler
- )
}
}
@@ -370,7 +397,7 @@ export default class Elysia<
* .listen(8080)
* ```
*/
- onStart(handler: VoidLifeCycle) {
+ onStart(handler: MaybeArray>) {
this.on('start', handler)
return this
@@ -389,8 +416,10 @@ export default class Elysia<
* })
* ```
*/
- onRequest(
- handler: BeforeRequestHandler
+ onRequest(
+ handler: MaybeArray<
+ PreHandler, Decorators>
+ >
) {
this.on('request', handler)
@@ -416,7 +445,7 @@ export default class Elysia<
* })
* ```
*/
- onParse(parser: BodyParser) {
+ onParse(parser: MaybeArray>) {
this.on('parse', parser)
return this
@@ -436,8 +465,10 @@ export default class Elysia<
* })
* ```
*/
- onTransform(
- handler: NoReturnHandler
+ onTransform(
+ handler: MaybeArray<
+ VoidHandler, Decorators>
+ >
) {
this.on('transform', handler)
@@ -463,10 +494,12 @@ export default class Elysia<
* })
* ```
*/
- onBeforeHandle(
- handler: Handler
+ onBeforeHandle(
+ handler: MaybeArray<
+ OptionalHandler, Decorators>
+ >
) {
- this.on('beforeHandle', handler)
+ this.on('beforeHandle', handler as any)
return this
}
@@ -487,10 +520,12 @@ export default class Elysia<
* })
* ```
*/
- onAfterHandle(
- handler: AfterRequestHandler
+ onAfterHandle(
+ handler: MaybeArray<
+ AfterHandler, Decorators>
+ >
) {
- this.on('afterHandle', handler)
+ this.on('afterHandle', handler as AfterHandler)
return this
}
@@ -511,14 +546,58 @@ export default class Elysia<
* ```
*/
- onResponse(
- handler: VoidRequestHandler
+ onResponse(
+ handler: MaybeArray<
+ VoidHandler, Decorators>
+ >
) {
this.on('response', handler)
return this
}
+ /**
+ * ### After Handle | Life cycle event
+ * Intercept request **after** main handler is called.
+ *
+ * If truthy value is returned, will be assigned as `Response`
+ *
+ * ---
+ * @example
+ * ```typescript
+ * new Elysia()
+ * .onAfterHandle((context, response) => {
+ * if(typeof response === "object")
+ * return JSON.stringify(response)
+ * })
+ * ```
+ */
+ trace(
+ handler: TraceHandler
+ ) {
+ this.reporter.on('event', createTraceListener(this.reporter, handler))
+
+ this.on('trace', handler)
+
+ return this
+ }
+
+ /**
+ * @deprecated this method will be renamed to `error` on 0.8, consider using `.error` instead
+ *
+ * ---
+ * @example
+ * ```typescript
+ * class CustomError extends Error {
+ * constructor() {
+ * super()
+ * }
+ * }
+ *
+ * new Elysia()
+ * .error('CUSTOM_ERROR', CustomError)
+ * ```
+ */
addError<
const Errors extends Record<
string,
@@ -527,25 +606,43 @@ export default class Elysia<
}
>
>(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
errors: Errors
): Elysia<
BasePath,
+ Decorators,
{
- store: Instance['store']
- error: Instance['error'] & {
- [K in NonNullable]: Errors[K] extends {
+ type: Definitions['type']
+ error: Definitions['error'] & {
+ [K in keyof Errors]: Errors[K] extends {
prototype: infer LiteralError extends Error
}
? LiteralError
: Errors[K]
}
- request: Instance['request']
- schema: Instance['schema']
- meta: Instance['meta']
- }
+ },
+ ParentSchema,
+ Routes,
+ Scoped
>
+ /**
+ * @deprecated this method will be renamed to `error` on 0.8, consider using `.error` instead
+ *
+ * ---
+ * @example
+ * ```typescript
+ * class CustomError extends Error {
+ * constructor() {
+ * super()
+ * }
+ * }
+ *
+ * new Elysia()
+ * .error({
+ * CUSTOM_ERROR: CustomError
+ * })
+ * ```
+ */
addError<
Name extends string,
const CustomError extends {
@@ -556,34 +653,22 @@ export default class Elysia<
errors: CustomError
): Elysia<
BasePath,
+ Decorators,
{
- store: Instance['store']
- error: Instance['error'] & {
+ type: Definitions['type']
+ error: Definitions['error'] & {
[name in Name]: CustomError extends {
prototype: infer LiteralError extends Error
}
? LiteralError
: CustomError
}
- request: Instance['request']
- schema: Instance['schema']
- meta: Instance['meta']
- }
+ },
+ ParentSchema,
+ Routes,
+ Scoped
>
- /**
- * Register errors
- *
- * ---
- * @example
- * ```typescript
- * new Elysia()
- * .onError(({ code }) => {
- * if(code === "NOT_FOUND")
- * return "Path not found :("
- * })
- * ```
- */
addError(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
name:
@@ -593,235 +678,378 @@ export default class Elysia<
{
prototype: Error
}
- >,
+ >
+ | Function,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
error?: {
prototype: Error
}
- ): Elysia {
- if (typeof name === 'string' && error) {
- // @ts-ignore
- error.prototype[ERROR_CODE] = name
-
- return this
- }
-
- // @ts-ignore
- for (const [code, error] of Object.entries(name))
- error.prototype[ERROR_CODE] = code
-
- return this
+ ): Elysia {
+ return this.error(name as any, error as any)
}
/**
- * ### Error | Life cycle event
- * Called when error is thrown during processing request
+ * Register errors
*
* ---
* @example
* ```typescript
+ * class CustomError extends Error {
+ * constructor() {
+ * super()
+ * }
+ * }
+ *
* new Elysia()
- * .onError(({ code }) => {
- * if(code === "NOT_FOUND")
- * return "Path not found :("
- * })
+ * .error('CUSTOM_ERROR', CustomError)
* ```
*/
- onError(handler: ErrorHandler): Elysia<
+ error<
+ const Errors extends Record<
+ string,
+ {
+ prototype: Error
+ }
+ >
+ >(
+ errors: Errors
+ ): Elysia<
BasePath,
+ Decorators,
{
- store: Instance['store']
- error: Instance['error']
- request: Instance['request']
- schema: Instance['schema']
- meta: Instance['meta']
- }
- > {
- this.on('error', handler)
-
- return this as any
- }
+ type: Definitions['type']
+ error: Definitions['error'] & {
+ [K in keyof Errors]: Errors[K] extends {
+ prototype: infer LiteralError extends Error
+ }
+ ? LiteralError
+ : Errors[K]
+ }
+ },
+ ParentSchema,
+ Routes,
+ Scoped
+ >
/**
- * ### stop | Life cycle event
- * Called after server stop serving request
+ * Register errors
*
* ---
* @example
* ```typescript
+ * class CustomError extends Error {
+ * constructor() {
+ * super()
+ * }
+ * }
+ *
* new Elysia()
- * .onStop((app) => {
- * cleanup()
+ * .error({
+ * CUSTOM_ERROR: CustomError
* })
* ```
*/
- onStop(handler: VoidLifeCycle) {
- this.on('stop', handler)
-
- return this
- }
-
- /**
- * ### on
- * Syntax sugar for attaching life cycle event by name
- *
- * Does the exact same thing as `.on[Event]()`
- *
- * ---
- * @example
- * ```typescript
- * new Elysia()
- * .on('error', ({ code }) => {
+ error<
+ Name extends string,
+ const CustomError extends {
+ prototype: Error
+ }
+ >(
+ name: Name,
+ errors: CustomError
+ ): Elysia<
+ BasePath,
+ Decorators,
+ {
+ type: Definitions['type']
+ error: Definitions['error'] & {
+ [name in Name]: CustomError extends {
+ prototype: infer LiteralError extends Error
+ }
+ ? LiteralError
+ : CustomError
+ }
+ },
+ ParentSchema,
+ Routes,
+ Scoped
+ >
+
+ /**
+ * Register errors
+ *
+ * ---
+ * @example
+ * ```typescript
+ * class CustomError extends Error {
+ * constructor() {
+ * super()
+ * }
+ * }
+ *
+ * new Elysia()
+ * .error('CUSTOM_ERROR', CustomError)
+ * ```
+ */
+ error>(
+ mapper: (decorators: Definitions['error']) => NewErrors
+ ): Elysia<
+ BasePath,
+ Decorators,
+ {
+ type: Definitions['type']
+ error: {
+ [K in keyof NewErrors]: NewErrors[K] extends {
+ prototype: infer LiteralError extends Error
+ }
+ ? LiteralError
+ : never
+ }
+ },
+ ParentSchema,
+ Routes,
+ Scoped
+ >
+
+ error(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ name:
+ | string
+ | Record<
+ string,
+ {
+ prototype: Error
+ }
+ >
+ | Function,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ error?: {
+ prototype: Error
+ }
+ ): Elysia {
+ switch (typeof name) {
+ case 'string':
+ // @ts-ignore
+ error.prototype[ERROR_CODE] = name
+
+ // @ts-ignore
+ this.definitions.error[name] = error
+
+ return this
+
+ case 'function':
+ this.definitions.error = name(this.definitions.error)
+
+ return this as any
+ }
+
+ for (const [code, error] of Object.entries(name)) {
+ // @ts-ignore
+ error.prototype[ERROR_CODE] = code
+
+ // @ts-ignore
+ this.definitions.error[code] = error
+ }
+
+ return this
+ }
+
+ /**
+ * ### Error | Life cycle event
+ * Called when error is thrown during processing request
+ *
+ * ---
+ * @example
+ * ```typescript
+ * new Elysia()
+ * .onError(({ code }) => {
* if(code === "NOT_FOUND")
* return "Path not found :("
* })
* ```
*/
- on(
- type: Event,
- handler: LifeCycle[Event]
+ onError(
+ handler: ErrorHandler<
+ Definitions['error'],
+ MergeSchema,
+ Decorators
+ >
) {
- handler = asGlobal(handler)
-
- switch (type) {
- case 'start':
- this.event.start.push(handler as LifeCycle['start'])
- break
-
- case 'request':
- this.event.request.push(handler as LifeCycle['request'])
- break
-
- case 'response':
- this.event.onResponse.push(handler as LifeCycle['response'])
- break
-
- case 'parse':
- this.event.parse.splice(
- this.event.parse.length - 1,
- 0,
- handler as BodyParser
- )
- break
+ this.on('error', handler as ErrorHandler)
- case 'transform':
- this.event.transform.push(handler as LifeCycle['transform'])
- break
+ return this
+ }
- case 'beforeHandle':
- this.event.beforeHandle.push(
- handler as LifeCycle['beforeHandle']
- )
- break
+ /**
+ * ### stop | Life cycle event
+ * Called after server stop serving request
+ *
+ * ---
+ * @example
+ * ```typescript
+ * new Elysia()
+ * .onStop((app) => {
+ * cleanup()
+ * })
+ * ```
+ */
+ onStop(handler: VoidHandler) {
+ this.on('stop', handler)
+
+ return this
+ }
+
+ /**
+ * ### on
+ * Syntax sugar for attaching life cycle event by name
+ *
+ * Does the exact same thing as `.on[Event]()`
+ *
+ * ---
+ * @example
+ * ```typescript
+ * new Elysia()
+ * .on('error', ({ code }) => {
+ * if(code === "NOT_FOUND")
+ * return "Path not found :("
+ * })
+ * ```
+ */
+ on(
+ type: Exclude | 'response',
+ handlers: MaybeArray[0]>
+ ) {
+ for (let handler of Array.isArray(handlers) ? handlers : [handlers]) {
+ handler = asGlobal(handler)
+
+ switch (type) {
+ case 'start':
+ this.event.start.push(handler as any)
+ break
+
+ case 'request':
+ this.event.request.push(handler as any)
+ break
+
+ case 'response':
+ this.event.onResponse.push(handler as any)
+ break
+
+ case 'parse':
+ this.event.parse.splice(
+ this.event.parse.length - 1,
+ 0,
+ handler as any
+ )
+ break
+
+ case 'transform':
+ this.event.transform.push(handler as any)
+ break
+
+ case 'beforeHandle':
+ this.event.beforeHandle.push(handler as any)
+ break
- case 'afterHandle':
- this.event.afterHandle.push(handler as LifeCycle['afterHandle'])
- break
+ case 'afterHandle':
+ this.event.afterHandle.push(handler as any)
+ break
+
+ case 'trace':
+ this.event.trace.push(handler as any)
+ break
- case 'error':
- this.event.error.push(handler as LifeCycle['error'])
- break
+ case 'error':
+ this.event.error.push(handler as any)
+ break
- case 'stop':
- this.event.stop.push(handler as LifeCycle['stop'])
- break
+ case 'stop':
+ this.event.stop.push(handler as any)
+ break
+ }
}
return this
}
group<
- NewElysia extends Elysia = Elysia,
- Prefix extends string = string
+ const NewElysia extends Elysia,
+ const Prefix extends string
>(
prefix: Prefix,
run: (
group: Elysia<
`${BasePath}${Prefix}`,
- {
- error: Instance['error']
- request: Instance['request']
- store: Instance['store'] & ElysiaInstance['store']
- schema: Instance['schema']
- meta: {
- schema: Instance['meta']['schema']
- defs: Instance['meta']['defs']
- exposed: {}
- }
- }
+ Decorators,
+ Definitions,
+ ParentSchema,
+ {}
>
) => NewElysia
- ): NewElysia extends Elysia<`${BasePath}${Prefix}`, infer NewInstance>
+ ): NewElysia extends Elysia<
+ any,
+ infer PluginDecorators,
+ infer PluginDefinitions,
+ infer PluginSchema,
+ any
+ >
? Elysia<
BasePath,
- {
- error: Instance['error']
- request: Instance['request']
- schema: Instance['schema']
- store: Instance['store']
- meta: Instance['meta'] & NewInstance['meta']
- }
+ PluginDecorators,
+ PluginDefinitions,
+ PluginSchema,
+ Prettify
>
: this
group<
- Schema extends TypedSchema<
- Exclude
+ const LocalSchema extends InputSchema<
+ Extract
>,
- NewElysia extends Elysia = Elysia,
- Prefix extends string = string
+ const NewElysia extends Elysia,
+ const Prefix extends string,
+ const Schema extends MergeSchema<
+ UnwrapRoute,
+ ParentSchema
+ >
>(
prefix: Prefix,
- schema: LocalHook,
+ schema: LocalHook<
+ LocalSchema,
+ Schema,
+ Decorators,
+ Definitions['error'],
+ `${BasePath}${Prefix}`
+ >,
run: (
group: Elysia<
`${BasePath}${Prefix}`,
- {
- error: Instance['error']
- request: Instance['request']
- store: Instance['store'] & ElysiaInstance['store']
- schema: {
- body: undefined extends Schema['body']
- ? Instance['schema']['body']
- : Schema['body']
- headers: undefined extends Schema['headers']
- ? Instance['schema']['headers']
- : Schema['headers']
- query: undefined extends Schema['query']
- ? Instance['schema']['query']
- : Schema['query']
- params: undefined extends Schema['params']
- ? Instance['schema']['params']
- : Schema['params']
- response: undefined extends Schema['response']
- ? Instance['schema']['response']
- : Schema['response']
- }
- meta: {
- schema: Instance['meta']['schema']
- defs: Instance['meta']['defs']
- exposed: {}
- }
- }
+ Decorators,
+ Definitions,
+ Schema,
+ {}
>
) => NewElysia
- ): NewElysia extends Elysia<`${BasePath}${Prefix}`, infer NewInstance>
+ ): NewElysia extends Elysia<
+ any,
+ infer PluginDecorators,
+ infer PluginDefinitions,
+ infer PluginSchema,
+ any
+ >
? Elysia<
BasePath,
+ Prettify,
{
- error: Instance['error']
- request: Instance['request']
- schema: Instance['schema']
- store: Instance['store']
- meta: Instance['meta'] &
- (Omit &
- Record<
- 'schema',
- {
- [Path in keyof NewInstance['meta']['schema']]: NewInstance['meta']['schema'][Path]
- }
- >)
- }
+ type: Prettify<
+ Definitions['type'] & PluginDefinitions['type']
+ >
+ error: Prettify<
+ Definitions['error'] & PluginDefinitions['error']
+ >
+ },
+ Prettify,
+ Prettify
>
: this
@@ -839,24 +1067,23 @@ export default class Elysia<
* })
* ```
*/
- group<
- Executor extends (group: Elysia) => Elysia,
- Schema extends TypedSchema<
- Exclude
- >
- >(
+ group(
prefix: string,
- schemaOrRun: LocalHook | Executor,
- run?: Executor
+ schemaOrRun:
+ | LocalHook
+ | ((
+ group: Elysia
+ ) => Elysia),
+ run?: (
+ group: Elysia
+ ) => Elysia
): this {
- const instance = new Elysia({
+ const instance = new Elysia({
...this.config,
prefix: ''
})
instance.store = this.store
- if (this.wsRouter) instance.use(ws())
-
const isSchema = typeof schemaOrRun === 'object'
const sandbox = (isSchema ? run! : schemaOrRun)(instance)
@@ -874,7 +1101,7 @@ export default class Elysia<
...(sandbox.event.onResponse as any)
]
- this.model(sandbox.meta.defs)
+ this.model(sandbox.definitions.type)
Object.values(instance.routes).forEach(
({ method, path, handler, hooks }) => {
@@ -882,28 +1109,13 @@ export default class Elysia<
if (isSchema) {
const hook = schemaOrRun
- const localHook = hooks
-
- // Same as guard
- const hasWsRoute = instance.wsRouter?.find(
- 'subscribe',
- path
- )
- if (hasWsRoute) {
- const wsRoute = instance.wsRouter!.history.find(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- ([_, wsPath]) => path === wsPath
- )
- if (!wsRoute) return
-
- return this.ws(path as any, wsRoute[2] as any)
- }
+ const localHook = hooks as LocalHook
this.add(
method,
path,
handler,
- mergeHook(hook as LocalHook, {
+ mergeHook(hook, {
...localHook,
error: !localHook.error
? sandbox.event.error
@@ -913,25 +1125,11 @@ export default class Elysia<
})
)
} else {
- const hasWsRoute = instance.wsRouter?.find(
- 'subscribe',
- path
- )
- if (hasWsRoute) {
- const wsRoute = instance.wsRouter!.history.find(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- ([_, wsPath]) => path === wsPath
- )
- if (!wsRoute) return
-
- return this.ws(path as any, wsRoute[2] as any)
- }
-
this.add(
method,
path,
handler,
- mergeHook(hooks, {
+ mergeHook(hooks as LocalHook, {
error: sandbox.event.error
}),
{
@@ -942,91 +1140,57 @@ export default class Elysia<
}
)
- if (instance.wsRouter && this.wsRouter)
- instance.wsRouter.history.forEach(([method, path, handler]) => {
- path = this.config.prefix + prefix + path
-
- if (path === '/') this.wsRouter?.add(method, prefix, handler)
- else this.wsRouter?.add(method, `${prefix}${path}`, handler)
- })
-
return this as any
}
guard<
- Schema extends TypedSchema<
- Exclude
+ const LocalSchema extends InputSchema,
+ const Route extends MergeSchema<
+ UnwrapRoute,
+ ParentSchema
>
>(
- hook: LocalHook
- ): Elysia<
- any,
- {
- error: Instance['error']
- request: Instance['request']
- store: Instance['store']
- schema: Instance['schema']
- meta: Instance['meta'] &
- Record<
- 'schema',
- {
- [key in keyof Schema]: Schema[key]
- }
- >
- }
- >
+ hook: LocalHook<
+ LocalSchema,
+ Route,
+ Decorators,
+ Definitions['error'],
+ BasePath
+ >
+ ): Elysia
guard<
- Schema extends TypedSchema<
- Exclude
+ const LocalSchema extends InputSchema<
+ Extract
>,
- NewElysia extends Elysia = Elysia
+ const NewElysia extends Elysia,
+ const Schema extends MergeSchema<
+ UnwrapRoute,
+ ParentSchema
+ >
>(
- hook: LocalHook,
+ schema: LocalHook<
+ LocalSchema,
+ Schema,
+ Decorators,
+ Definitions['error']
+ >,
run: (
- group: Elysia<
- BasePath,
- {
- error: Instance['error']
- request: Instance['request']
- store: Instance['store']
- schema: {
- body: undefined extends Schema['body']
- ? Instance['schema']['body']
- : Schema['body']
- headers: undefined extends Schema['headers']
- ? Instance['schema']['headers']
- : Schema['headers']
- query: undefined extends Schema['query']
- ? Instance['schema']['query']
- : Schema['query']
- params: undefined extends Schema['params']
- ? Instance['schema']['params']
- : Schema['params']
- response: undefined extends Schema['response']
- ? Instance['schema']['response']
- : Schema['response']
- }
- meta: Instance['meta']
- }
- >
+ group: Elysia
) => NewElysia
- ): NewElysia extends Elysia
+ ): NewElysia extends Elysia<
+ any,
+ infer PluginDecorators,
+ infer PluginDefinitions,
+ infer PluginSchema,
+ any
+ >
? Elysia<
BasePath,
- {
- error: Instance['error']
- request: Instance['request']
- store: Instance['store']
- schema: Instance['schema']
- meta: Instance['meta'] &
- Record<
- 'schema',
- {
- [key in keyof NewInstance['meta']['schema']]: NewInstance['meta']['schema'][key]
- }
- >
- }
+ PluginDecorators,
+ PluginDefinitions,
+ PluginSchema,
+ Prettify
>
: this
@@ -1054,12 +1218,14 @@ export default class Elysia<
* ```
*/
guard(
- hook: LocalHook,
- run?: (group: Elysia) => Elysia
- ): Elysia {
+ hook: LocalHook,
+ run?: (
+ group: Elysia
+ ) => Elysia
+ ): Elysia {
if (!run) {
this.event = mergeLifeCycle(this.event, hook)
- this.$schema = {
+ this.validator = {
body: hook.body,
headers: hook.headers,
params: hook.params,
@@ -1072,7 +1238,6 @@ export default class Elysia<
const instance = new Elysia()
instance.store = this.store
- if (this.wsRouter) instance.use(ws())
const sandbox = run(instance)
this.decorators = mergeDeep(this.decorators, instance.decorators)
@@ -1089,27 +1254,16 @@ export default class Elysia<
...sandbox.event.onResponse
]
- this.model(sandbox.meta.defs)
+ this.model(sandbox.definitions.type)
Object.values(instance.routes).forEach(
({ method, path, handler, hooks: localHook }) => {
- const hasWsRoute = instance.wsRouter?.find('subscribe', path)
- if (hasWsRoute) {
- const wsRoute = instance.wsRouter!.history.find(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- ([_, wsPath]) => path === wsPath
- )
- if (!wsRoute) return
-
- return this.ws(path as any, wsRoute[2] as any)
- }
-
this.add(
method,
path,
handler,
- mergeHook(hook as LocalHook, {
- ...localHook,
+ mergeHook(hook as LocalHook, {
+ ...(localHook as LocalHook),
error: !localHook.error
? sandbox.event.error
: Array.isArray(localHook.error)
@@ -1120,139 +1274,143 @@ export default class Elysia<
}
)
- if (instance.wsRouter && this.wsRouter)
- instance.wsRouter.history.forEach(([method, path, handler]) => {
- this.wsRouter?.add(method, path, handler)
- })
-
return this as any
}
// Inline Fn
- use<
- NewInstance extends ElysiaInstance,
- Params extends Elysia = Elysia
- >(
- plugin: MaybePromise<
- (
- app: Params extends Elysia
- ? IsAny extends true
- ? this
- : any
- : Params
- ) => MaybePromise>
- >
- ): Elysia<
- BasePath,
- {
- error: Instance['error'] & NewInstance['error']
- request: Reconciliation
- store: Reconciliation
- schema: Instance['schema'] & NewInstance['schema']
- meta: {
- schema: Instance['meta']['schema'] & {
- [Path in keyof NewInstance['meta']['schema'] as Path extends string
- ? `${BasePath}${Path}`
- : Path]: NewInstance['meta']['schema'][Path]
- }
- defs: Reconciliation<
- Instance['meta']['defs'],
- NewInstance['meta']['defs']
- >
- exposed: Instance['meta']['exposed'] &
- NewInstance['meta']['exposed']
- }
- }
+ use = this>(
+ plugin: MaybePromise<(app: NewElysia) => MaybePromise>
+ ): NewElysia extends Elysia<
+ any,
+ infer PluginDecorators,
+ infer PluginDefinitions,
+ infer PluginSchema,
+ any
>
+ ? Elysia<
+ BasePath,
+ {
+ request: Prettify<
+ Decorators['request'] & PluginDecorators['request']
+ >
+ store: Prettify<
+ Decorators['store'] & PluginDecorators['store']
+ >
+ },
+ {
+ type: Prettify<
+ Definitions['type'] & PluginDefinitions['type']
+ >
+ error: Prettify<
+ Definitions['error'] & PluginDefinitions['error']
+ >
+ },
+ Prettify>,
+ Routes & NewElysia['schema'],
+ Scoped
+ >
+ : this
- use(
- instance: Elysia
- ): Elysia<
- BasePath,
- {
- error: Instance['error'] & NewInstance['error']
- request: Reconciliation
- store: Reconciliation
- schema: Instance['schema'] & NewInstance['schema']
- meta: {
- schema: Instance['meta']['schema'] & {
- [Path in keyof NewInstance['meta']['schema'] as Path extends string
- ? `${BasePath}${Path}`
- : Path]: NewInstance['meta']['schema'][Path]
- }
- defs: Reconciliation<
- Instance['meta']['defs'],
- NewInstance['meta']['defs']
- >
- exposed: Instance['meta']['exposed'] &
- NewInstance['meta']['exposed']
- }
- }
+ // Entire Instance
+ use>(
+ instance: NewElysia
+ ): NewElysia extends Elysia<
+ any,
+ infer PluginDecorators,
+ infer PluginDefinitions,
+ infer PluginSchema,
+ any,
+ infer IsScoped
>
+ ? IsScoped extends true
+ ? this
+ : Elysia<
+ BasePath,
+ {
+ request: Prettify<
+ Decorators['request'] & PluginDecorators['request']
+ >
+ store: Prettify<
+ Decorators['store'] & PluginDecorators['store']
+ >
+ },
+ {
+ type: Prettify<
+ Definitions['type'] & PluginDefinitions['type']
+ >
+ error: Prettify<
+ Definitions['error'] & PluginDefinitions['error']
+ >
+ },
+ Prettify>,
+ BasePath extends ``
+ ? Routes & NewElysia['schema']
+ : Routes & AddPrefix,
+ Scoped
+ >
+ : this
// Import Fn
- use(
+ use>(
plugin: Promise<{
default: (
- elysia: Elysia
- ) => MaybePromise>
+ elysia: Elysia
+ ) => MaybePromise
}>
- ): Elysia<
- BasePath,
- {
- error: Instance['error'] & LazyLoadElysia['error']
- request: Reconciliation<
- Instance['request'],
- LazyLoadElysia['request']
- >
- store: Reconciliation
- schema: Instance['schema'] & LazyLoadElysia['schema']
- meta: {
- schema: Instance['meta']['schema'] & {
- [Path in keyof LazyLoadElysia['meta']['schema'] as Path extends string
- ? `${BasePath}${Path}`
- : Path]: LazyLoadElysia['meta']['schema'][Path]
- }
- defs: Reconciliation<
- Instance['meta']['defs'],
- LazyLoadElysia['meta']['defs']
- >
- exposed: Instance['meta']['exposed'] &
- LazyLoadElysia['meta']['exposed']
- }
- }
+ ): NewElysia extends Elysia<
+ any,
+ infer PluginDecorators,
+ infer PluginDefinitions,
+ infer PluginSchema,
+ any
>
+ ? Elysia<
+ BasePath,
+ {
+ request: Decorators['request'] & PluginDecorators['request']
+ store: Decorators['store'] & PluginDecorators['store']
+ },
+ {
+ type: Definitions['type'] & PluginDefinitions['type']
+ error: Definitions['error'] & PluginDefinitions['error']
+ },
+ MergeSchema,
+ BasePath extends ``
+ ? Routes & NewElysia['schema']
+ : Routes & AddPrefix,
+ Scoped
+ >
+ : this
- // Import inline
- use(
+ // Import entire instance
+ use>(
plugin: Promise<{
- default: (elysia: Elysia) => Elysia
+ default: LazyLoadElysia
}>
- ): Elysia<
- BasePath,
- {
- error: Instance['error'] & LazyLoadElysia['error']
- request: Reconciliation<
- Instance['request'],
- LazyLoadElysia['request']
- >
- store: Reconciliation
- schema: Instance['schema'] & LazyLoadElysia['schema']
- meta: {
- schema: Instance['meta']['schema'] & {
- [Path in keyof LazyLoadElysia['meta']['schema'] as Path extends string
- ? `${BasePath}${Path}`
- : Path]: LazyLoadElysia['meta']['schema'][Path]
- }
- defs: Reconciliation<
- Instance['meta']['defs'],
- LazyLoadElysia['meta']['defs']
- >
- exposed: Instance['meta']['exposed'] &
- LazyLoadElysia['meta']['exposed']
- }
- }
+ ): LazyLoadElysia extends Elysia<
+ any,
+ infer PluginDecorators,
+ infer PluginDefinitions,
+ infer PluginSchema,
+ any
>
+ ? Elysia<
+ BasePath,
+ {
+ request: PluginDecorators['request'] & Decorators['request']
+ store: PluginDecorators['store'] & Decorators['store']
+ },
+ {
+ type: PluginDefinitions['type'] & Definitions['type']
+
+ error: PluginDefinitions['error'] & Definitions['error']
+ },
+ MergeSchema,
+ BasePath extends ``
+ ? Routes & LazyLoadElysia['schema']
+ : Routes & AddPrefix
+ >
+ : this
/**
* ### use
@@ -1270,106 +1428,21 @@ export default class Elysia<
*/
use(
plugin:
- | Elysia
+ | Elysia
| MaybePromise<
- (app: Elysia) => MaybePromise>
+ (
+ app: Elysia
+ ) => MaybePromise>
>
| Promise<{
- default: Elysia
+ default: Elysia
}>
| Promise<{
default: (
- elysia: Elysia
- ) => MaybePromise>
+ elysia: Elysia
+ ) => MaybePromise>
}>
- ): Elysia {
- const register = (
- plugin:
- | Elysia
- | ((app: Elysia) => MaybePromise>)
- ) => {
- if (typeof plugin === 'function') {
- const instance = plugin(
- this as unknown as any
- ) as unknown as any
- if (instance instanceof Promise) {
- this.lazyLoadModules.push(instance.then((x) => x.compile()))
-
- return this as unknown as any
- }
-
- return instance
- }
-
- const isScoped = plugin.config.scoped
-
- if (!isScoped) {
- this.decorators = mergeDeep(this.decorators, plugin.decorators)
- this.state(plugin.store)
- this.model(plugin.meta.defs)
- this.addError(plugin.error)
- }
-
- const {
- config: { name, seed }
- } = plugin
-
- Object.values(plugin.routes).forEach(
- ({ method, path, handler, hooks }) => {
- const hasWsRoute = plugin.wsRouter?.find('subscribe', path)
- if (hasWsRoute) {
- const wsRoute = plugin.wsRouter!.history.find(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- ([_, wsPath]) => path === wsPath
- )
- if (!wsRoute) return
-
- return this.ws(path as any, wsRoute[2] as any)
- }
-
- this.add(
- method,
- path,
- handler,
- mergeHook(hooks, {
- error: plugin.event.error
- })
- )
- }
- )
-
- if (!isScoped)
- if (name) {
- if (!(name in this.dependencies))
- this.dependencies[name] = []
-
- const current =
- seed !== undefined
- ? checksum(name + JSON.stringify(seed))
- : 0
-
- if (
- this.dependencies[name].some(
- (checksum) => current === checksum
- )
- )
- return this
-
- this.dependencies[name].push(current)
- this.event = mergeLifeCycle(
- this.event,
- filterGlobalHook(plugin.event),
- current
- )
- } else
- this.event = mergeLifeCycle(
- this.event,
- filterGlobalHook(plugin.event)
- )
-
- return this
- }
-
+ ): Elysia {
if (plugin instanceof Promise) {
this.lazyLoadModules.push(
plugin
@@ -1384,13 +1457,114 @@ export default class Elysia<
this as unknown as any
) as unknown as Elysia
- return register(plugin.default)
+ return this._use(plugin.default)
})
.then((x) => x.compile())
)
return this as unknown as any
- } else return register(plugin)
+ } else return this._use(plugin)
+
+ return this
+ }
+
+ private _use(
+ plugin:
+ | Elysia
+ | ((
+ app: Elysia
+ ) => MaybePromise>)
+ ) {
+ if (typeof plugin === 'function') {
+ const instance = plugin(this as unknown as any) as unknown as any
+ if (instance instanceof Promise) {
+ this.lazyLoadModules.push(instance.then((x) => x.compile()))
+
+ return this as unknown as any
+ }
+
+ return instance
+ }
+
+ const { name, seed } = plugin.config
+
+ const isScoped = plugin.config.scoped
+ if (isScoped) {
+ if (name) {
+ if (!(name in this.dependencies)) this.dependencies[name] = []
+
+ const current =
+ seed !== undefined
+ ? checksum(name + JSON.stringify(seed))
+ : 0
+
+ if (
+ this.dependencies[name].some(
+ (checksum) => current === checksum
+ )
+ )
+ return this
+
+ this.dependencies[name].push(current)
+ }
+
+ plugin.model(this.definitions.type as any)
+ plugin.error(this.definitions.error as any)
+ plugin.onRequest((context) => {
+ Object.assign(context, this.decorators)
+ Object.assign(context.store, this.store)
+ })
+
+ if (plugin.config.aot) plugin.compile()
+
+ return this.mount(plugin.fetch)
+ }
+
+ this.decorate(plugin.decorators)
+ this.state(plugin.store)
+ this.model(plugin.definitions.type)
+ this.error(plugin.definitions.error)
+
+ for (const { method, path, handler, hooks } of Object.values(
+ plugin.routes
+ )) {
+ this.add(
+ method,
+ path,
+ handler,
+ mergeHook(hooks as LocalHook, {
+ error: plugin.event.error
+ })
+ )
+ }
+
+ if (!isScoped)
+ if (name) {
+ if (!(name in this.dependencies)) this.dependencies[name] = []
+
+ const current =
+ seed !== undefined
+ ? checksum(name + JSON.stringify(seed))
+ : 0
+
+ if (
+ this.dependencies[name].some(
+ (checksum) => current === checksum
+ )
+ )
+ return this
+
+ this.dependencies[name].push(current)
+ this.event = mergeLifeCycle(
+ this.event,
+ filterGlobalHook(plugin.event),
+ current
+ )
+ } else
+ this.event = mergeLifeCycle(
+ this.event,
+ filterGlobalHook(plugin.event)
+ )
return this
}
@@ -1411,10 +1585,10 @@ export default class Elysia<
const handler: Handler