diff --git a/CHANGELOG.md b/CHANGELOG.md index 774e1099a..e69e6f795 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +# 1.4.0 - 13 Sep 2025 +Feature: +- standard validator +- macro schema, macro extension, macro detail +- lifecycle type soundness +- type inference reduced by ~11% +- [#861](https://github.com/elysiajs/elysia/issues/861) missing HEAD method when register route with GET + +Improvement +- [#861](https://github.com/elysiajs/elysia/issues/861) automatically add HEAD method when defining GET route + +Change +- ObjectString/ArrayString no longer produce default value by default due to security reasons +- Cookie now dynamically parse when format is likely JSON +- export `fileType` for external file type validation for accurate response +- ObjectString/ArrayString no longer produce default value by default due to security reasons +- Cookie now dynamically parse when format is likely JSON + +Breaking Change +- remove macro v1 due to non type soundness +- remove `error` function, use `status` instead +- deprecation notice for `response` in `mapResponse`, `afterResponse`, use `responseValue` instead + # 1.3.22 - 6 Sep 2025 Bug fix: - safely unwrap t.Record diff --git a/bun.lock b/bun.lock index ac1d63024..d75a66c95 100644 --- a/bun.lock +++ b/bun.lock @@ -5,32 +5,35 @@ "name": "elysia", "dependencies": { "cookie": "^1.0.2", - "exact-mirror": "0.1.6", + "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1", }, "devDependencies": { + "@elysiajs/openapi": "^1.3.11", "@types/bun": "^1.2.16", "@types/cookie": "^1.0.0", "@types/fast-decode-uri-component": "^1.0.0", "@typescript-eslint/eslint-plugin": "^8.30.1", "@typescript-eslint/parser": "^8.30.1", + "arktype": "^2.1.22", "eslint": "^9.24.0", "eslint-plugin-security": "^3.0.1", "eslint-plugin-sonarjs": "^3.0.2", "expect-type": "^1.2.1", "file-type": "^20.4.1", "memoirist": "^0.4.0", - "mitata": "^1.0.34", "prettier": "^3.5.3", "tsup": "^8.4.0", "typescript": "^5.8.3", + "valibot": "^1.1.0", + "zod": "^4.1.5", }, "optionalDependencies": { - "@sinclair/typebox": "^0.34.33", - "openapi-types": "^12.1.3", + "@sinclair/typebox": ">= 0.34.0 < 1", + "openapi-types": ">= 12.0.0", }, "peerDependencies": { - "@sinclair/typebox": ">= 0.34.0", + "@sinclair/typebox": ">= 0.34.0 < 1", "exact-mirror": ">= 0.0.9", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", @@ -39,6 +42,12 @@ }, }, "packages": { + "@ark/schema": ["@ark/schema@0.49.0", "", { "dependencies": { "@ark/util": "0.49.0" } }, "sha512-GphZBLpW72iS0v4YkeUtV3YIno35Gimd7+ezbPO9GwEi9kzdUrPVjvf6aXSBAfHikaFc/9pqZOpv3pOXnC71tw=="], + + "@ark/util": ["@ark/util@0.49.0", "", {}, "sha512-/BtnX7oCjNkxi2vi6y1399b+9xd1jnCrDYhZ61f0a+3X8x8DxlK52VgEEzyuC2UQMPACIfYrmHkhD3lGt2GaMA=="], + + "@elysiajs/openapi": ["@elysiajs/openapi@1.3.11", "", { "dependencies": { "@sinclair/typemap": "^0.10.1", "openapi-types": "^12.1.3" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-+jwAEIDbIGmSKjl/kePwdDaHFgsIX/cEpw01sSjzm7SosjwZuQjJDVgDCab2p68lIO+VUOsvkKuP+5Sm5ehCvQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], @@ -177,7 +186,9 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.2", "", { "os": "win32", "cpu": "x64" }, "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA=="], - "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="], + + "@sinclair/typemap": ["@sinclair/typemap@0.10.1", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.30", "valibot": "^1.0.0", "zod": "^3.24.1" } }, "sha512-UXR0fhu/n3c9B6lB+SLI5t1eVpt9i9CdDrp2TajRe3LbKiUhCTZN2kSfJhjPnpc3I59jMRIhgew7+0HlMi08mg=="], "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], @@ -227,6 +238,8 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "arktype": ["arktype@2.1.22", "", { "dependencies": { "@ark/schema": "0.49.0", "@ark/util": "0.49.0" } }, "sha512-xdzl6WcAhrdahvRRnXaNwsipCgHuNoLobRqhiP8RjnfL9Gp947abGlo68GAIyLtxbD+MLzNyH2YR4kEqioMmYQ=="], + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], "array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="], @@ -313,6 +326,8 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "elysia": ["elysia@1.3.21", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.6", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-LLfDSoVA5fBoqKQfMJyzmHLkya8zMbEYwd7DS7v2iQB706mgzWg0gufXl58cFALErcvSayplrkDvjkmlYTkIZQ=="], + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], @@ -363,7 +378,7 @@ "eventsource-parser": ["eventsource-parser@3.0.1", "", {}, "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA=="], - "exact-mirror": ["exact-mirror@0.1.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-EXGDixoDotCGrXCce63zmGHDA+3Id6PPkIwshBHuB10dwVc4YV4gfaYLuysHOxyURmwyt4UL186ann0oYa2CFQ=="], + "exact-mirror": ["exact-mirror@0.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="], "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], @@ -577,8 +592,6 @@ "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "mitata": ["mitata@1.0.34", "", {}, "sha512-Mc3zrtNBKIMeHSCQ0XqRLo1vbdIx1wvFV9c8NJAiyho6AjNfMY8bVhbS12bwciUdd1t4rj8099CH3N3NFahaUA=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], @@ -793,6 +806,8 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "valibot": ["valibot@1.1.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], @@ -819,7 +834,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], + "zod": ["zod@4.1.5", "", {}, "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], @@ -827,8 +842,16 @@ "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + "@modelcontextprotocol/sdk/zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "elysia/@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="], + + "elysia/exact-mirror": ["exact-mirror@0.1.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-EXGDixoDotCGrXCce63zmGHDA+3Id6PPkIwshBHuB10dwVc4YV4gfaYLuysHOxyURmwyt4UL186ann0oYa2CFQ=="], + + "eslint/zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], + "eslint-plugin-sonarjs/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], diff --git a/example/a.ts b/example/a.ts index 9987cefac..bc37c2dc9 100644 --- a/example/a.ts +++ b/example/a.ts @@ -1,11 +1,16 @@ -import { Elysia, t } from '../src' -import { req } from '../test/utils' +import { Elysia, t } from 'elysia' -const app = new Elysia() - .get('/', () => 'SAFE', { - query: t.Record(t.String(), t.String()) - }) - -const response = await app.handle(req('/?x=1')) - -console.log(response.status) +new Elysia().group( + '/id/:id', + { + params: t.Object({ + id: t.Number() + }) + }, + (app) => + app.get('/:name', ({ params }) => params, { + params: t.Object({ + name: t.String() + }) + }) +) diff --git a/example/custom-response.ts b/example/custom-response.ts index 981ef2758..b46ad333b 100644 --- a/example/custom-response.ts +++ b/example/custom-response.ts @@ -5,7 +5,7 @@ const prettyJson = new Elysia() if (response instanceof Object) return new Response(JSON.stringify(response, null, 4)) }) - .as('plugin') + .as('scoped') new Elysia() .use(prettyJson) diff --git a/example/openapi.ts b/example/openapi.ts new file mode 100644 index 000000000..b3b92fc56 --- /dev/null +++ b/example/openapi.ts @@ -0,0 +1,76 @@ +import { Elysia, t } from '../src' +import { openapi as OpenAPI } from '@elysiajs/openapi' +import { fromTypes } from '@elysiajs/openapi/gen' + +const openapi = (a: any) => + new Elysia().use((app) => { + app.use( + // @ts-ignore + OpenAPI(a) + ) + + return app + }) + +// Elysia 1.4: lifecycle event type soundness +export const app = new Elysia() + .use( + openapi({ + references: fromTypes('example/openapi.ts') + }) + ) + .macro({ + auth: { + response: { + 409: t.Literal('Conflict') + }, + beforeHandle({ status }) { + if (Math.random() < 0.05) return status(410) + }, + resolve: () => ({ a: 'a' }) + } + }) + .onError(({ status }) => { + if (Math.random() < 0.05) return status(400) + }) + .resolve(({ status }) => { + if (Math.random() < 0.05) return status(401) + }) + .onBeforeHandle([ + ({ status }) => { + if (Math.random() < 0.05) return status(402) + }, + ({ status }) => { + if (Math.random() < 0.05) return status(403) + } + ]) + .guard({ + beforeHandle: [ + ({ status }) => { + if (Math.random() < 0.05) return status(405) + }, + ({ status }) => { + if (Math.random() < 0.05) return status(406) + } + ], + afterHandle({ status }) { + if (Math.random() < 0.05) return status(407) + }, + error({ status }) { + if (Math.random() < 0.05) return status(408) + } + }) + .post( + '/', + ({ status }) => + Math.random() < 0.05 ? status(409, 'Conflict') : 'Type Soundness', + { + auth: true, + response: { + 411: t.Literal('Length Required') + } + } + ) + .listen(3000) + +// app['~Routes']['post']['response'] diff --git a/example/websocket.ts b/example/websocket.ts index 1cd09ad9b..8c4125e76 100644 --- a/example/websocket.ts +++ b/example/websocket.ts @@ -12,7 +12,7 @@ const app = new Elysia() }, message(ws, message) { ws.publish('asdf', message) - ws.send('asdf', message) + ws.send(message) } }) .get('/publish/:publish', ({ params: { publish: text } }) => { diff --git a/package.json b/package.json index da428d519..76618b8d0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "elysia", "description": "Ergonomic Framework for Human", - "version": "1.3.21", + "version": "1.4.0", "author": { "name": "saltyAom", "url": "https://github.com/SaltyAom", @@ -184,35 +184,38 @@ }, "dependencies": { "cookie": "^1.0.2", - "exact-mirror": "0.1.6", + "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1" }, "devDependencies": { + "@elysiajs/openapi": "^1.3.11", "@types/bun": "^1.2.16", "@types/cookie": "^1.0.0", "@types/fast-decode-uri-component": "^1.0.0", "@typescript-eslint/eslint-plugin": "^8.30.1", "@typescript-eslint/parser": "^8.30.1", + "arktype": "^2.1.22", "eslint": "^9.24.0", "eslint-plugin-security": "^3.0.1", "eslint-plugin-sonarjs": "^3.0.2", "expect-type": "^1.2.1", "file-type": "^20.4.1", "memoirist": "^0.4.0", - "mitata": "^1.0.34", "prettier": "^3.5.3", "tsup": "^8.4.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "valibot": "^1.1.0", + "zod": "^4.1.5" }, "peerDependencies": { - "@sinclair/typebox": ">= 0.34.0", + "@sinclair/typebox": ">= 0.34.0 < 1", "exact-mirror": ">= 0.0.9", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalDependencies": { - "@sinclair/typebox": "^0.34.33", - "openapi-types": "^12.1.3" + "@sinclair/typebox": ">= 0.34.0 < 1", + "openapi-types": ">= 12.0.0" } } diff --git a/src/adapter/bun/compose.ts b/src/adapter/bun/compose.ts index e94d5c666..b0b059157 100644 --- a/src/adapter/bun/compose.ts +++ b/src/adapter/bun/compose.ts @@ -7,7 +7,7 @@ import { status } from '../../error' import { ELYSIA_TRACE } from '../../trace' import type { AnyElysia } from '../..' -import type { InternalRoute } from '../../types' +import type { InternalRoute, InputSchema } from '../../types' const allocateIf = (value: string, condition: unknown) => condition ? value : '' @@ -38,7 +38,9 @@ const createContext = ( const needsQuery = inference.query || !!route.hooks.query || - !!route.standaloneValidators?.find((x) => x.query) || + !!(route.hooks.standaloneValidator as InputSchema[])?.find( + (x) => x.query + ) || app.event.request?.length if (needsQuery) fnLiteral += getQi @@ -65,7 +67,6 @@ const createContext = ( hasTrace || inference.url || needsQuery ) + `redirect,` + - `error:status,` + `status,` + `set:{headers:` + (isNotEmpty(defaultHeaders) @@ -131,7 +132,9 @@ export const createBunRouteHandler = (app: AnyElysia, route: InternalRoute) => { const needsQuery = inference.query || !!route.hooks.query || - !!route.standaloneValidators?.find((x) => x.query) + !!(route.hooks.standaloneValidator as InputSchema[])?.find( + (x) => x.query + ) // inference.query require declaring const 'qi' if (hasTrace || needsQuery || app.event.request?.length) { diff --git a/src/adapter/bun/index.ts b/src/adapter/bun/index.ts index 36b2faa8b..3629e5066 100644 --- a/src/adapter/bun/index.ts +++ b/src/adapter/bun/index.ts @@ -485,7 +485,7 @@ export const BunAdapter: ElysiaAdapter = { ) as any const handleResponse = createHandleWSResponse(validateResponse) - const parseMessage = createWSMessageParser(parse) + const parseMessage = createWSMessageParser(parse as any) let _id: string | undefined diff --git a/src/adapter/web-standard/index.ts b/src/adapter/web-standard/index.ts index aaf9254fe..ad58d97ba 100644 --- a/src/adapter/web-standard/index.ts +++ b/src/adapter/web-standard/index.ts @@ -107,7 +107,6 @@ export const WebStandardAdapter: ElysiaAdapter = { `path:p,` + `url:u,` + `redirect,` + - `error:status,` + `status,` + `set:{headers:` diff --git a/src/compose.ts b/src/compose.ts index 7962ad821..490fa6d62 100644 --- a/src/compose.ts +++ b/src/compose.ts @@ -10,7 +10,11 @@ import { } from '@sinclair/typebox' import decode from 'fast-decode-uri-component' -import { parseQuery, parseQueryFromURL } from './parse-query' +import { + parseQuery, + parseQueryFromURL, + parseQueryStandardSchema +} from './parse-query' import { ELYSIA_REQUEST_ID, @@ -21,7 +25,8 @@ import { signCookie, isNotEmpty, encodePath, - mergeCookie + mergeCookie, + getResponseLength } from './utils' import { isBun } from './universal/utils' import { ParseError, status } from './error' @@ -39,13 +44,14 @@ import { ElysiaTypeCheck, getCookieValidator, getSchemaValidator, + hasElysiaMeta, hasType, isUnion, unwrapImportSchema } from './schema' import { Sucrose, sucrose } from './sucrose' import { parseCookie, type CookieOptions } from './cookies' -import { validateFileExtension } from './type-system/utils' +import { fileType } from './type-system/utils' import type { TraceEvent } from './trace' import type { @@ -225,10 +231,10 @@ const composeValidationFactory = ({ isStaticResponse?: boolean hasSanitize?: boolean }) => ({ - validate: (type: string, value = `c.${type}`) => - `c.set.status=422;throw new ValidationError('${type}',validator.${type},${value})`, + validate: (type: string, value = `c.${type}`, error?: string) => + `c.set.status=422;throw new ValidationError('${type}',validator.${type},${value}${error ? ',' + error : ''})`, response: (name = 'r') => { - if (isStaticResponse) return '' + if (isStaticResponse || !validator.response) return '' let code = injectResponse + '\n' @@ -237,10 +243,24 @@ const composeValidationFactory = ({ `c.set.status=${name}.code\n` + `${name}=${name}.response` + `}` + + `if(${name} instanceof Response === false)` + `switch(c.set.status){` for (const [status, value] of Object.entries(validator.response!)) { - code += `\ncase ${status}:if(${name} instanceof Response)break\n` + code += `\ncase ${status}:\n` + + if (value.provider === 'standard') { + code += + `let vare${status}=validator.response[${status}].Check(${name})\n` + + `if(vare${status} instanceof Promise)vare${status}=await vare${status}\n` + + `if(vare${status}.issues)` + + `throw new ValidationError('response',validator.response[${status}],${name},vare${status}.issues)\n` + + `${name}=vare${status}.value\n` + + `c.set.status=${status}\n` + + 'break\n' + + continue + } let noValidate = value.schema?.noValidate === true @@ -649,216 +669,53 @@ export const composeHandler = ({ } if (hasQuery) { - const destructured = < - { - key: string - isArray: boolean - isNestedObjectArray: boolean - isObject: boolean - anyOf: boolean - }[] - >[] - - const schema = validator.query?.schema - if ( - schema && - (schema.type === 'object' || - (schema[Kind] === 'Import' && schema.$defs?.[schema.$ref])) - ) { - const properties = - schema.properties ?? schema.$defs?.[schema.$ref]?.properties - - if (properties && !validator.query!.hasAdditionalProperties) - for (const [key, _value] of Object.entries(properties)) { - let value = _value as TAnySchema - - const isArray = - value.type === 'array' || - !!value.anyOf?.some( - (v: TSchema) => - v.type === 'string' && - v.format === 'ArrayString' - ) - - // @ts-ignore - if ( - value && - OptionalKind in value && - value.type === 'array' && - value.items - ) - value = value.items - - const { type, anyOf } = value - - destructured.push({ - key, - isArray, - isNestedObjectArray: - (isArray && value.items?.type === 'object') || - !!value.items?.anyOf?.some( - (x: TSchema) => - x.type === 'object' || x.type === 'array' - ), - isObject: - type === 'object' || - anyOf?.some( - (v: TSchema) => - v.type === 'string' && - v.format === 'ArrayString' - ), - anyOf: !!anyOf - }) - } - } - - if (!destructured.length) { - fnLiteral += - 'if(c.qi===-1){' + - 'c.query={}' + - '}else{' + - 'c.query=parseQueryFromURL(c.url,c.qi+1)' + - '}' - } else { - fnLiteral += 'if(c.qi!==-1){' + `let url='&'+c.url.slice(c.qi+1)\n` - - let index = 0 - for (const { - key, - isArray, - isObject, - isNestedObjectArray, - anyOf - } of destructured) { - const encoded = encodeURIComponent(key) - - const init = - (index === 0 ? 'let ' : '') + - `memory=url.indexOf('&${encoded}=')` + - `\nlet a${index}\n` - - if (isArray) { - fnLiteral += init - - if (isNestedObjectArray) - fnLiteral += - `while(memory!==-1){` + - `const start=memory+${encoded.length + 2}\n` + - `memory=url.indexOf('&',start)\n` + - `if(a${index}===undefined)\n` + - `a${index}=''\n` + - `else\n` + - `a${index}+=','\n` + - `let temp\n` + - `if(memory===-1)temp=decodeURIComponent(url.slice(start).replace(/\\+/g,' '))\n` + - `else temp=decodeURIComponent(url.slice(start, memory).replace(/\\+/g,' '))\n` + - `const charCode=temp.charCodeAt(0)\n` + - `if(charCode!==91&&charCode !== 123)\n` + - `temp='"'+temp+'"'\n` + - `a${index}+=temp\n` + - `if(memory===-1)break\n` + - `memory=url.indexOf('&${encoded}=',memory)\n` + - `if(memory===-1)break` + - `}` + - `try{` + - `if(a${index}.charCodeAt(0)===91)` + - `a${index} = JSON.parse(a${index})\n` + - `else\n` + - `a${index}=JSON.parse('['+a${index}+']')` + - `}catch{}\n` - else - fnLiteral += - `while(memory!==-1){` + - `const start=memory+${encoded.length + 2}\n` + - `memory=url.indexOf('&',start)\n` + - `if(a${index}===undefined)` + - `a${index}=[]\n` + - `if(memory===-1){` + - `const temp=decodeURIComponent(url.slice(start).replace(/\\+/g,' '))\n` + - `if(temp.includes(',')){a${index}=a${index}.concat(temp.split(','))}` + - `else{a${index}.push(decodeURIComponent(url.slice(start).replace(/\\+/g,' ')))}\n` + - `break` + - `}else{` + - `const temp=decodeURIComponent(url.slice(start, memory).replace(/\\+/g,' '))\n` + - `if(temp.includes(',')){a${index}=a${index}.concat(temp.split(','))}` + - `else{a${index}.push(temp)}\n` + - `}` + - `memory=url.indexOf('&${encoded}=',memory)\n` + - `if(memory===-1) break\n` + - `}` - } else if (isObject) - fnLiteral += - init + - `if(memory!==-1){` + - `const start=memory+${encoded.length + 2}\n` + - `memory=url.indexOf('&',start)\n` + - `if(memory===-1)a${index}=decodeURIComponent(url.slice(start).replace(/\\+/g,' '))` + - `else a${index}=decodeURIComponent(url.slice(start,memory).replace(/\\+/g,' '))` + - `if(a${index}!==undefined)` + - `try{` + - `a${index}=JSON.parse(a${index})` + - `}catch{}` + - '}' - // Might be union primitive and array - else { - fnLiteral += - init + - `if(memory!==-1){` + - `const start=memory+${encoded.length + 2}\n` + - `memory=url.indexOf('&',start)\n` + - `if(memory===-1)a${index}=decodeURIComponent(url.slice(start).replace(/\\+/g,' '))\n` + - `else{` + - `a${index}=decodeURIComponent(url.slice(start,memory).replace(/\\+/g,' '))` - - if (anyOf) - fnLiteral += - `\nlet deepMemory=url.indexOf('&${encoded}=',memory)\n` + - `if(deepMemory!==-1){` + - `a${index}=[a${index}]\n` + - `let first=true\n` + - `while(true){` + - `const start=deepMemory+${encoded.length + 2}\n` + - `if(first)first=false\n` + - `else deepMemory = url.indexOf('&', start)\n` + - `let value\n` + - `if(deepMemory===-1)value=url.slice(start).replace(/\\+/g,' ')\n` + - `else value=url.slice(start, deepMemory).replace(/\\+/g,' ')\n` + - `value=decodeURIComponent(value)\n` + - `if(value===null){if(deepMemory===-1){break}else{continue}}\n` + - `const vStart=value.charCodeAt(0)\n` + - `const vEnd=value.charCodeAt(value.length - 1)\n` + - `if((vStart===91&&vEnd===93)||(vStart===123&&vEnd===125))\n` + - `try{` + - `a${index}.push(JSON.parse(value))` + - `}catch{` + - `a${index}.push(value)` + - `}` + - `if(deepMemory===-1)break` + - `}}` + let arrayProperties: Record = {} + let objectProperties: Record = {} + let hasArrayProperty = false + let hasObjectProperty = false + + if (validator.query?.schema) { + const schema = unwrapImportSchema(validator.query?.schema) + + if (Kind in schema && schema.properties) { + for (const [key, value] of Object.entries(schema.properties)) { + if (hasElysiaMeta('ArrayQuery', value as TSchema)) { + arrayProperties[key] = 1 + hasArrayProperty = true + } - fnLiteral += '}}' + if (hasElysiaMeta('ObjectString', value as TSchema)) { + objectProperties[key] = 1 + hasObjectProperty = true + } } - - index++ - fnLiteral += '\n' } - - fnLiteral += - `c.query={` + - destructured - .map(({ key }, index) => `'${key}':a${index}`) - .join(',') + - `}` - - // If there are no query parameters, set it to an empty object - fnLiteral += `} else c.query = {}\n` } + + fnLiteral += + 'if(c.qi===-1){' + + 'c.query=Object.create(null)' + + '}else{' + + `c.query=parseQueryFromURL(c.url,c.qi+1,${ + // + hasArrayProperty ? JSON.stringify(arrayProperties) : undefined + },${ + // + hasObjectProperty ? JSON.stringify(objectProperties) : undefined + })` + + '}' } const isAsyncHandler = typeof handler === 'function' && isAsync(handler) const saveResponse = - hasTrace || hooks.afterResponse?.length ? 'c.response= ' : '' + hasTrace || hooks.afterResponse?.length ? 'c.response=c.responseValue= ' : '' + + const responseKeys = Object.keys(validator.response ?? {}) + const hasMultipleResponses = responseKeys.length > 1 + const hasSingle200 = + responseKeys.length === 0 || + (responseKeys.length === 1 && responseKeys[0] === '200') const maybeAsync = hasCookie || @@ -868,7 +725,15 @@ export const composeHandler = ({ !!hooks.afterHandle?.some(isAsync) || !!hooks.beforeHandle?.some(isAsync) || !!hooks.transform?.some(isAsync) || - !!hooks.mapResponse?.some(isAsync) + !!hooks.mapResponse?.some(isAsync) || + validator.body?.provider === 'standard' || + validator.headers?.provider === 'standard' || + validator.query?.provider === 'standard' || + validator.params?.provider === 'standard' || + validator.cookie?.provider === 'standard' || + Object.values(validator.response ?? {}).find( + (x) => x.provider === 'standard' + ) const maybeStream = (typeof handler === 'function' ? isGenerator(handler as any) : false) || @@ -876,12 +741,6 @@ export const composeHandler = ({ !!hooks.afterHandle?.some(isGenerator) || !!hooks.transform?.some(isGenerator) - const responseKeys = Object.keys(validator.response ?? {}) - const hasMultipleResponses = responseKeys.length > 1 - const hasSingle200 = - responseKeys.length === 0 || - (responseKeys.length === 1 && responseKeys[0] === '200') - const hasSet = inference.cookie || inference.set || @@ -1299,7 +1158,14 @@ export const composeHandler = ({ if (validator.headers.isOptional) fnLiteral += `if(isNotEmpty(c.headers)){` - if (validator.body?.schema?.noValidate !== true) + if (validator.headers?.provider === 'standard') { + fnLiteral += + `let vah=validator.headers.Check(c.headers)\n` + + `if(vah instanceof Promise)vah=await vah\n` + + `if(vah.issues){` + + validation.validate('headers', undefined, 'vah.issues') + + '}else{c.headers=vah.value}\n' + } else if (validator.headers?.schema?.noValidate !== true) fnLiteral += `if(validator.headers.Check(c.headers) === false){` + validation.validate('headers') + @@ -1334,7 +1200,14 @@ export const composeHandler = ({ fnLiteral += `c.params['${key}']??=${parsed}\n` } - if (validator.params?.schema?.noValidate !== true) + if (validator.params.provider === 'standard') { + fnLiteral += + `let vap=validator.params.Check(c.params)\n` + + `if(vap instanceof Promise)vap=await vap\n` + + `if(vap.issues){` + + validation.validate('params', undefined, 'vap.issues') + + '}else{c.params=vap.value}\n' + } else if (validator.params?.schema?.noValidate !== true) fnLiteral += `if(validator.params.Check(c.params)===false){` + validation.validate('params') + @@ -1348,7 +1221,7 @@ export const composeHandler = ({ } if (validator.query) { - if (validator.query.hasDefault) + if (Kind in validator.query?.schema && validator.query.hasDefault) for (const [key, value] of Object.entries( Value.Default( // @ts-ignore @@ -1365,29 +1238,44 @@ export const composeHandler = ({ if (parsed !== undefined) fnLiteral += `if(c.query['${key}']===undefined)c.query['${key}']=${parsed}\n` - - fnLiteral += composeCleaner({ - name: 'c.query', - schema: validator.query, - type: 'query', - normalize - }) } + fnLiteral += composeCleaner({ + name: 'c.query', + schema: validator.query, + type: 'query', + normalize + }) + if (validator.query.isOptional) fnLiteral += `if(isNotEmpty(c.query)){` - if (validator.query?.schema?.noValidate !== true) + if (validator.query.provider === 'standard') { + fnLiteral += + `let vaq=validator.query.Check(c.query)\n` + + `if(vaq instanceof Promise)vaq=await vaq\n` + + `if(vaq.issues){` + + validation.validate('query', undefined, 'vaq.issues') + + '}else{c.query=vaq.value}\n' + } else if (validator.query?.schema?.noValidate !== true) fnLiteral += `if(validator.query.Check(c.query)===false){` + validation.validate('query') + `}` - if (validator.query.hasTransform) + if (validator.query.hasTransform) { + // TypeBox Decode only work with single Decode at the time + // If we have multiple Decode, it will handle only the first one + // For query, we decode it twice to ensure that it works fnLiteral += coerceTransformDecodeError( - `c.query=validator.query.Decode(Object.assign({},c.query))\n`, + `c.query=validator.query.Decode(c.query)\n`, 'query' ) + fnLiteral += coerceTransformDecodeError( + `c.query=validator.query.Decode(c.query)\n`, + 'query' + ) + } if (validator.query.isOptional) fnLiteral += `}` } @@ -1403,10 +1291,8 @@ export const composeHandler = ({ let value = Value.Default( validator.body.schema, validator.body.schema.type === 'object' || - (validator.body.schema[Kind] === 'Import' && - validator.body.schema.$defs[ - validator.body.schema.$ref - ][Kind] === 'Object') + unwrapImportSchema(validator.body.schema)[Kind] === + 'Object' ? {} : undefined ) @@ -1451,7 +1337,14 @@ export const composeHandler = ({ normalize }) - if (validator.body?.schema?.noValidate !== true) { + if (validator.body.provider === 'standard') { + fnLiteral += + `let vab=validator.body.Check(c.body)\n` + + `if(vab instanceof Promise)vab=await vab\n` + + `if(vab.issues){` + + validation.validate('body', undefined, 'vab.issues') + + '}else{c.body=vab.value}\n' + } else if (validator.body?.schema?.noValidate !== true) { if (validator.body.isOptional) fnLiteral += `if(isNotEmptyObject&&validator.body.Check(c.body)===false){` + @@ -1471,7 +1364,14 @@ export const composeHandler = ({ normalize }) - if (validator.body?.schema?.noValidate !== true) { + if (validator.body.provider === 'standard') { + fnLiteral += + `let vab=validator.body.Check(c.body)\n` + + `if(vab instanceof Promise)vab=await vab\n` + + `if(vab.issues){` + + validation.validate('body', undefined, 'vab.issues') + + '}else{c.body=vab.value}\n' + } else if (validator.body?.schema?.noValidate !== true) { if (validator.body.isOptional) fnLiteral += `if(isNotEmptyObject&&validator.body.Check(c.body)===false){` + @@ -1533,7 +1433,7 @@ export const composeHandler = ({ continue if (validatorLength) validateFile += ',' - validateFile += `validateFileExtension(c.body.${k},${JSON.stringify(v.extension)},'body.${k}')` + validateFile += `fileType(c.body.${k},${JSON.stringify(v.extension)},'body.${k}')` validatorLength++ } @@ -1577,7 +1477,7 @@ export const composeHandler = ({ continue if (i) validateFile += ',' - validateFile += `validateFileExtension(c.body.${k},${JSON.stringify(v.extension)},'body.${k}')` + validateFile += `fileType(c.body.${k},${JSON.stringify(v.extension)},'body.${k}')` i++ } @@ -1592,52 +1492,42 @@ export const composeHandler = ({ if (validator.cookie) { // ! Get latest app.config.cookie - const cookieValidator = getCookieValidator({ - // @ts-expect-error private property - modules: app.definitions.typebox, - validator: validator.cookie as any, - defaultConfig: app.config.cookie, - dynamic: !!app.config.aot, - config: validator.cookie?.config ?? {}, - normalize: app.config.normalize, - // @ts-expect-error - models: app.definitions.type - })! + validator.cookie.config = mergeCookie( + validator.cookie.config, + validator.cookie?.config ?? {} + ) fnLiteral += `const cookieValue={}\n` + `for(const [key,value] of Object.entries(c.cookie))` + `cookieValue[key]=value.value\n` - if (cookieValidator.hasDefault) - for (const [key, value] of Object.entries( - Value.Default(cookieValidator.schema, {}) as Object - )) { - fnLiteral += `cookieValue['${key}'] = ${ - typeof value === 'object' - ? JSON.stringify(value) - : value - }\n` - } - - if (cookieValidator.isOptional) + if (validator.cookie.isOptional) fnLiteral += `if(isNotEmpty(c.cookie)){` - if (validator.body?.schema?.noValidate !== true) { + if (validator.cookie.provider === 'standard') { + fnLiteral += + `let vac=validator.cookie.Check(c.body)\n` + + `if(vac instanceof Promise)vac=await vac\n` + + `if(vac.issues){` + + validation.validate('cookie', undefined, 'vac.issues') + + '}else{c.body=vac.value}\n' + } else if (validator.body?.schema?.noValidate !== true) { fnLiteral += `if(validator.cookie.Check(cookieValue)===false){` + validation.validate('cookie', 'cookieValue') + '}' } - if (cookieValidator.hasTransform) - fnLiteral += coerceTransformDecodeError( - `for(const [key,value] of Object.entries(validator.cookie.Decode(cookieValue)))` + - `c.cookie[key].value=value\n`, - 'cookie' - ) + // if (validator.cookie.hasTransform) + // fnLiteral += coerceTransformDecodeError( + // `for(const [key,value] of Object.entries(validator.cookie.Decode(cookieValue))){` + + // `c.cookie[key].value=value` + + // `}`, + // 'cookie' + // ) - if (cookieValidator.isOptional) fnLiteral += `}` + if (validator.cookie.isOptional) fnLiteral += `}` } } @@ -1719,11 +1609,13 @@ export const composeHandler = ({ total: hooks.afterHandle?.length }) - if(hooks.afterHandle?.length) { + if (hooks.afterHandle?.length) { for (let i = 0; i < hooks.afterHandle.length; i++) { const hook = hooks.afterHandle[i] const returning = hasReturn(hook) - const endUnit = reporter.resolveChild(hook.fn.name) + const endUnit = reporter.resolveChild( + hook.fn.name + ) fnLiteral += `c.response = be\n` @@ -1736,7 +1628,7 @@ export const composeHandler = ({ ? `af=await e.afterHandle[${i}](c)\n` : `af=e.afterHandle[${i}](c)\n` - fnLiteral += `if(af!==undefined) c.response=be=af\n` + fnLiteral += `if(af!==undefined) c.response=c.responseValue=be=af\n` } endUnit('af') @@ -1745,14 +1637,15 @@ export const composeHandler = ({ reporter.resolve() } - if (validator.response) fnLiteral += validation.response('be') + if (validator.response) + fnLiteral += validation.response('be') const mapResponseReporter = report('mapResponse', { total: hooks.mapResponse?.length }) if (hooks.mapResponse?.length) { - fnLiteral += `c.response=be\n` + fnLiteral += `c.response=c.responseValue=be\n` for (let i = 0; i < hooks.mapResponse.length; i++) { const mapResponse = hooks.mapResponse[i] @@ -1764,7 +1657,7 @@ export const composeHandler = ({ fnLiteral += `if(mr===undefined){` + `mr=${isAsyncName(mapResponse) ? 'await ' : ''}e.mapResponse[${i}](c)\n` + - `if(mr!==undefined)be=c.response=mr` + + `if(mr!==undefined)be=c.response=c.responseValue=mr` + '}' endUnit() @@ -1791,8 +1684,8 @@ export const composeHandler = ({ if (hooks.afterHandle?.length) fnLiteral += isAsyncHandler - ? `let r=c.response=await ${handle}\n` - : `let r=c.response=${handle}\n` + ? `let r=c.response=c.responseValue=await ${handle}\n` + : `let r=c.response=c.responseValue=${handle}\n` else fnLiteral += isAsyncHandler ? `let r=await ${handle}\n` @@ -1804,7 +1697,7 @@ export const composeHandler = ({ total: hooks.afterHandle?.length }) - if(hooks.afterHandle?.length) { + if (hooks.afterHandle?.length) { for (let i = 0; i < hooks.afterHandle.length; i++) { const hook = hooks.afterHandle[i] const returning = hasReturn(hook) @@ -1829,12 +1722,12 @@ export const composeHandler = ({ fnLiteral += validation.response('af') - fnLiteral += `c.response=af}` + fnLiteral += `c.response=c.responseValue=af}` } else { fnLiteral += `if(af!==undefined){` reporter.resolve() - fnLiteral += `c.response=af}` + fnLiteral += `c.response=c.responseValue=af}` } } } @@ -1863,7 +1756,7 @@ export const composeHandler = ({ `mr=${ isAsyncName(mapResponse) ? 'await ' : '' }e.mapResponse[${i}](c)\n` + - `if(mr!==undefined)r=c.response=mr\n` + `if(mr!==undefined)r=c.response=c.responseValue=mr\n` endUnit() } @@ -1890,7 +1783,7 @@ export const composeHandler = ({ }) if (hooks.mapResponse?.length) { - fnLiteral += '\nc.response=r\n' + fnLiteral += '\nc.response=c.responseValue=r\n' for (let i = 0; i < hooks.mapResponse.length; i++) { const mapResponse = hooks.mapResponse[i] @@ -1902,7 +1795,7 @@ export const composeHandler = ({ fnLiteral += `\nif(mr===undefined){` + `mr=${isAsyncName(mapResponse) ? 'await ' : ''}e.mapResponse[${i}](c)\n` + - `if(mr!==undefined)r=c.response=mr` + + `if(mr!==undefined)r=c.response=c.responseValue=mr` + `}\n` endUnit() @@ -1939,7 +1832,7 @@ export const composeHandler = ({ total: hooks.mapResponse?.length }) if (hooks.mapResponse?.length) { - fnLiteral += 'c.response= r\n' + fnLiteral += 'c.response=c.responseValue= r\n' for (let i = 0; i < hooks.mapResponse.length; i++) { const mapResponse = hooks.mapResponse[i] @@ -1951,7 +1844,7 @@ export const composeHandler = ({ fnLiteral += `if(mr===undefined){` + `mr=${isAsyncName(mapResponse) ? 'await ' : ''}e.mapResponse[${i}](c)\n` + - `if(mr!==undefined)r=c.response=mr` + + `if(mr!==undefined)r=c.response=c.responseValue=mr` + `}` endUnit() @@ -2041,7 +1934,7 @@ export const composeHandler = ({ ) fnLiteral += - `c.response=er\n` + + `c.response=c.responseValue=er\n` + `mep=e.mapResponse[${i}](c)\n` + `if(mep instanceof Promise)er=await er\n` + `if(mep!==undefined)er=mep\n` @@ -2093,7 +1986,7 @@ export const composeHandler = ({ allocateIf(`ValidationError,`, hasValidation) + allocateIf(`ParseError`, hasBody) + `},` + - `validateFileExtension,` + + `fileType,` + `schema,` + `definitions,` + `ERROR_CODE,` + @@ -2136,13 +2029,17 @@ export const composeHandler = ({ isNotEmpty, utils: { parseQuery: hasBody ? parseQuery : undefined, - parseQueryFromURL: hasQuery ? parseQueryFromURL : undefined + parseQueryFromURL: hasQuery + ? validator.query?.provider === 'standard' + ? parseQueryStandardSchema + : parseQueryFromURL + : undefined }, error: { ValidationError: hasValidation ? ValidationError : undefined, ParseError: hasBody ? ParseError : undefined }, - validateFileExtension, + fileType, schema: app.router.history, // @ts-expect-error definitions: app.definitions.type, @@ -2293,6 +2190,7 @@ export const composeGeneralHandler = (app: AnyElysia) => { const adapter = app['~adapter'].composeGeneralHandler app.router.http.build() + const isWebstandard = app['~adapter'].isWebStandard const hasTrace = app.event.trace?.length let fnLiteral = '' @@ -2300,13 +2198,30 @@ export const composeGeneralHandler = (app: AnyElysia) => { const router = app.router let findDynamicRoute = router.http.root.WS - ? `const route=router.find(r.method === "GET" && r.headers.get('upgrade')==='websocket'?'WS':r.method,p)` + ? `const route=router.find(r.method==='GET'&&r.headers.get('upgrade')==='websocket'?'WS':r.method,p)` : `const route=router.find(r.method,p)` - findDynamicRoute += router.http.root.ALL ? '??router.find("ALL",p)\n' : '\n' + findDynamicRoute += router.http.root.ALL ? `??router.find('ALL',p)\n` : '\n' + + if (isWebstandard) + findDynamicRoute += + `if(r.method==='HEAD'){` + + `const route=router.find('GET',p)\n` + + 'if(route){' + + `c.params=route.params\n` + + `const _res=route.store.handler?route.store.handler(c):route.store.compile()(c)\n` + + `if(_res)` + + 'return getResponseLength(_res).then((length)=>{' + + `_res.headers.set('content-length', length)\n` + + `return new Response(null,{status:_res.status,statusText:_res.statusText,headers:_res.headers})\n` + + '})' + + '}' + + '}' let afterResponse = `c.error=notFound\n` if (app.event.afterResponse?.length && !app.event.error) { + afterResponse = '\nc.error=notFound\n' + const prefix = app.event.afterResponse.some(isAsync) ? 'async' : '' afterResponse += `\nsetImmediate(${prefix}()=>{` @@ -2322,7 +2237,7 @@ export const composeGeneralHandler = (app: AnyElysia) => { // @ts-ignore if (app.inference.query) afterResponse += - 'if(c.qi===-1){' + + '\nif(c.qi===-1){' + 'c.query={}' + '}else{' + 'c.query=parseQueryFromURL(c.url,c.qi+1)' + @@ -2356,15 +2271,35 @@ export const composeGeneralHandler = (app: AnyElysia) => { if ('GET' in methods || 'WS' in methods) { switchMap += `case 'GET':` - if ('WS' in methods) + if ('WS' in methods) { switchMap += `if(r.headers.get('upgrade')==='websocket')` + `return ht[${methods.WS}].composed(c)\n` + if ('GET' in methods === false) { + if ('ALL' in methods) + switchMap += `return ht[${methods.ALL}].composed(c)\n` + else switchMap += `break map\n` + } + } + if ('GET' in methods) switchMap += `return ht[${methods.GET}].composed(c)\n` } + if ( + isWebstandard && + ('GET' in methods || 'ALL' in methods) && + 'HEAD' in methods === false + ) + switchMap += + `case 'HEAD':` + + `const _res=ht[${methods.GET ?? methods.ALL}].composed(c)\n` + + 'return getResponseLength(_res).then((length)=>{' + + `_res.headers.set('content-length', length)\n` + + `return new Response(null,{status:_res.status,statusText:_res.statusText,headers:_res.headers})\n` + + '})\n' + for (const [method, index] of Object.entries(methods)) { if (method === 'ALL' || method === 'GET' || method === 'WS') continue @@ -2394,6 +2329,7 @@ export const composeGeneralHandler = (app: AnyElysia) => { `handleError,` + `status,` + `redirect,` + + `getResponseLength,` + // @ts-ignore allocateIf(`parseQueryFromURL,`, app.inference.query) + allocateIf(`ELYSIA_TRACE,`, hasTrace) + @@ -2459,6 +2395,7 @@ export const composeGeneralHandler = (app: AnyElysia) => { handleError, status, redirect, + getResponseLength, // @ts-ignore parseQueryFromURL: app.inference.query ? parseQueryFromURL : undefined, ELYSIA_TRACE: hasTrace ? ELYSIA_TRACE : undefined, @@ -2563,10 +2500,7 @@ export const composeErrorHandler = (app: AnyElysia) => { if (adapter.declare) fnLiteral += adapter.declare const saveResponse = - hasTrace || - !!hooks.afterResponse?.length - ? 'context.response = ' - : '' + hasTrace || !!hooks.afterResponse?.length ? 'context.response = ' : '' if (app.event.error) for (let i = 0; i < app.event.error.length; i++) { @@ -2604,7 +2538,7 @@ export const composeErrorHandler = (app: AnyElysia) => { ) fnLiteral += - `context.response=_r` + + `context.response=context.responseValue=_r` + `_r=${isAsyncName(mapResponse) ? 'await ' : ''}onMapResponse[${i}](context)\n` endUnit() @@ -2632,7 +2566,7 @@ export const composeErrorHandler = (app: AnyElysia) => { fnLiteral += `if(error instanceof Error){` + afterResponse() + - `\nif(typeof error.toResponse==='function')return context.response=error.toResponse()\n` + + `\nif(typeof error.toResponse==='function')return context.response=context.responseValue=error.toResponse()\n` + adapter.unknownError + `\n}` @@ -2642,7 +2576,7 @@ export const composeErrorHandler = (app: AnyElysia) => { }) fnLiteral += - '\nif(!context.response)context.response=error.message??error\n' + '\nif(!context.response)context.response=context.responseValue=error.message??error\n' if (hooks.mapResponse?.length) { fnLiteral += 'let mr\n' @@ -2657,7 +2591,7 @@ export const composeErrorHandler = (app: AnyElysia) => { fnLiteral += `if(mr===undefined){` + `mr=${isAsyncName(mapResponse) ? 'await ' : ''}onMapResponse[${i}](context)\n` + - `if(mr!==undefined)error=context.response=mr` + + `if(mr!==undefined)error=context.response=context.responseValue=mr` + '}' endUnit() diff --git a/src/context.ts b/src/context.ts index 0f862dbcd..ae82c9345 100644 --- a/src/context.ts +++ b/src/context.ts @@ -12,7 +12,8 @@ import type { Prettify, ResolvePath, SingletonBase, - HTTPHeaders + HTTPHeaders, + InputSchema } from './types' type InvertedStatusMapKey = keyof InvertedStatusMap @@ -102,7 +103,6 @@ export type ErrorContext< route: string request: Request store: Singleton['store'] - response: Route['response'] } & Singleton['decorator'] & Singleton['derive'] & Singleton['resolve'] @@ -121,27 +121,42 @@ export type Context< Path extends string | undefined = undefined > = Prettify< { - body: PrettifyIfObject + body: PrettifyIfObject query: undefined extends Route['query'] - ? Record - : PrettifyIfObject + ? {} extends NonNullable + ? Record + : Singleton['resolve']['query'] + : PrettifyIfObject params: undefined extends Route['params'] ? undefined extends Path - ? Record + ? {} extends NonNullable + ? Record + : Singleton['resolve']['params'] : Path extends `${string}/${':' | '*'}${string}` ? ResolvePath : never - : PrettifyIfObject + : PrettifyIfObject headers: undefined extends Route['headers'] - ? Record - : PrettifyIfObject + ? {} extends NonNullable + ? Record + : Singleton['resolve']['headers'] + : PrettifyIfObject< + Route['headers'] & Singleton['resolve']['headers'] + > cookie: undefined extends Route['cookie'] - ? Record> - : Record> & { - [key in keyof Route['cookie']]-?: Cookie< - Route['cookie'][key] + ? Record> + : Record> & + Prettify< + { + [key in keyof Route['cookie']]-?: Cookie< + Route['cookie'][key] + > + } & { + [key in keyof Singleton['resolve']['cookie']]-?: Cookie< + Singleton['resolve']['cookie'][key] + > + } > - } server: Server | null redirect: Redirect @@ -206,37 +221,9 @@ export type Context< response: T // @ts-ignore trust me bro ) => ElysiaCustomStatusResponse - - /** - * @deprecated use `status` instead - */ - error: {} extends Route['response'] - ? typeof status - : < - const Code extends - | keyof Route['response'] - | InvertedStatusMap[Extract< - InvertedStatusMapKey, - keyof Route['response'] - >], - const T extends Code extends keyof Route['response'] - ? Route['response'][Code] - : Code extends keyof StatusMap - ? // @ts-ignore StatusMap[Code] always valid because Code generic check - Route['response'][StatusMap[Code]] - : never - >( - code: Code, - response: T - // @ts-ignore trust me bro - ) => ElysiaCustomStatusResponse - - // response?: IsNever extends true - // ? unknown - // : Route['response'][keyof Route['response']] } & Singleton['decorator'] & Singleton['derive'] & - Singleton['resolve'] + Omit > // Use to mimic request before mapping route @@ -261,10 +248,6 @@ export type PreContext< redirect?: string } - /** - * @deprecated use `status` instead - */ - error: typeof status status: typeof status } & Singleton['decorator'] > diff --git a/src/cookies.ts b/src/cookies.ts index 47518c108..c1dc42b18 100644 --- a/src/cookies.ts +++ b/src/cookies.ts @@ -322,13 +322,25 @@ export const parseCookie = async ( const jar: Record = {} - const cookies = parse(cookieString) for (const [name, v] of Object.entries(cookies)) { if (v === undefined) continue let value = decode(v) + if (value) { + const starts = value.charCodeAt(0) + const ends = value.charCodeAt(value.length - 1) + + if ( + (starts === 123 && ends === 125) || + (starts === 91 && ends === 93) + ) + try { + value = JSON.parse(value) + } catch {} + } + if (sign === true || sign?.includes(name)) { if (!secrets) throw new Error('No secret is provided to cookie plugin') diff --git a/src/error.ts b/src/error.ts index 1f91d5365..fc8fcafaf 100644 --- a/src/error.ts +++ b/src/error.ts @@ -8,6 +8,7 @@ import type { import { StatusMap, InvertedStatusMap } from './utils' import type { ElysiaTypeCheck } from './schema' +import { StandardSchemaV1Like } from './types' // ? Cloudflare worker support const env = @@ -78,10 +79,11 @@ export const status = < response?: T ) => new ElysiaCustomStatusResponse(code, response as any) -/** - * @deprecated use `Elysia.status` instead - */ -export const error = status +const a = status(403, 'a') +const b = status(403, 'b') + +type a = typeof a +type b = typeof b export class InternalServerError extends Error { code = 'INTERNAL_SERVER_ERROR' @@ -254,118 +256,166 @@ export class ValidationError extends Error { constructor( public type: string, - public validator: TSchema | TypeCheck | ElysiaTypeCheck, + public validator: + | TSchema + | TypeCheck + | ElysiaTypeCheck + | StandardSchemaV1Like, public value: unknown, errors?: ValueErrorIterator ) { - if ( - value && - typeof value === 'object' && - value instanceof ElysiaCustomStatusResponse - ) - value = value.response - - const error = - errors?.First() ?? - ('Errors' in validator - ? validator.Errors(value).First() - : Value.Errors(validator, value).First()) - - const accessor = error?.path || 'root' - - // @ts-ignore private field - const schema = validator?.schema ?? validator - + let message = '' + let error let expected + let customError - if (!isProduction) { - try { - expected = Value.Create(schema) - } catch (error) { - expected = { - type: 'Could not create expected value', - // @ts-expect-error - message: error?.message, - error + if ( + // @ts-ignore + validator?.provider === 'standard' || + '~standard' in validator || + // @ts-ignore + (validator.schema && '~standard' in validator.schema) + ) { + const standard = // @ts-ignore + ('~standard' in validator ? validator : validator.schema)[ + '~standard' + ] + + const _errors = errors ?? standard.validate(value).issues + + error = _errors?.[0] + + if (isProduction) + message = JSON.stringify({ + type: 'validation', + on: type, + found: value + }) + else + message = JSON.stringify( + { + type: 'validation', + on: type, + property: error.path?.[0] || 'root', + message: error?.message, + summary: error?.problem, + expected, + found: value, + errors + }, + null, + 2 + ) + + customError = error?.message + } else { + if ( + value && + typeof value === 'object' && + value instanceof ElysiaCustomStatusResponse + ) + value = value.response + + error = + errors?.First() ?? + ('Errors' in validator + ? validator.Errors(value).First() + : Value.Errors(validator, value).First()) + + const accessor = error?.path || 'root' + + // @ts-ignore private field + const schema = validator?.schema ?? validator + + if (!isProduction) { + try { + expected = Value.Create(schema) + } catch (error) { + expected = { + type: 'Could not create expected value', + // @ts-expect-error + message: error?.message, + error + } } } - } - - const customError = - error?.schema?.message || error?.schema?.error !== undefined - ? typeof error.schema.error === 'function' - ? error.schema.error( - isProduction - ? { - type: 'validation', - on: type, - found: value - } - : { - type: 'validation', - on: type, - value, - property: accessor, - message: error?.message, - summary: mapValueError(error).summary, - found: value, - expected, - errors: - 'Errors' in validator - ? [ - ...validator.Errors( - value - ) - ].map(mapValueError) - : [ - ...Value.Errors( - validator, - value - ) - ].map(mapValueError) - }, - validator - ) - : error.schema.error - : undefined - - let message = '' - if (customError !== undefined) { - message = - typeof customError === 'object' - ? JSON.stringify(customError) - : customError + '' - } else if (isProduction) { - message = JSON.stringify({ - type: 'validation', - on: type, - found: value - }) - } else { - message = JSON.stringify( - { + customError = + error?.schema?.message || error?.schema?.error !== undefined + ? typeof error.schema.error === 'function' + ? error.schema.error( + isProduction + ? { + type: 'validation', + on: type, + found: value + } + : { + type: 'validation', + on: type, + value, + property: accessor, + message: error?.message, + summary: + mapValueError(error).summary, + found: value, + expected, + errors: + 'Errors' in validator + ? [ + ...validator.Errors( + value + ) + ].map(mapValueError) + : [ + ...Value.Errors( + validator, + value + ) + ].map(mapValueError) + }, + validator + ) + : error.schema.error + : undefined + + if (customError !== undefined) { + message = + typeof customError === 'object' + ? JSON.stringify(customError) + : customError + '' + } else if (isProduction) { + message = JSON.stringify({ type: 'validation', on: type, - property: accessor, - message: error?.message, - summary: mapValueError(error).summary, - expected, - found: value, - errors: - 'Errors' in validator - ? [...validator.Errors(value)].map(mapValueError) - : [...Value.Errors(validator, value)].map( - mapValueError - ) - }, - null, - 2 - ) + found: value + }) + } else { + message = JSON.stringify( + { + type: 'validation', + on: type, + property: accessor, + message: error?.message, + summary: mapValueError(error).summary, + expected, + found: value, + errors: + 'Errors' in validator + ? [...validator.Errors(value)].map( + mapValueError + ) + : [...Value.Errors(validator, value)].map( + mapValueError + ) + }, + null, + 2 + ) + } } super(message) - this.valueError = error this.expected = expected this.customError = customError @@ -394,6 +444,8 @@ export class ValidationError extends Error { } get model() { + if ('~standard' in this.validator) return this.validator + return ValidationError.simplifyModel(this.validator) } diff --git a/src/formats.ts b/src/formats.ts index f145e93d6..5726f62a7 100644 --- a/src/formats.ts +++ b/src/formats.ts @@ -192,21 +192,21 @@ function regex(str: string): boolean { /** * @license - * + * * MIT License - * + * * Copyright (c) 2020 Evgeny Poberezkin - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/src/index.ts b/src/index.ts index 589cbc30e..033670998 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,11 +2,10 @@ import { Memoirist } from 'memoirist' import { Kind, type TObject, - type Static, type TSchema, type TModule, type TRef, - type TProperties + type TAnySchema } from '@sinclair/typebox' import fastDecodeURIComponent from 'fast-decode-uri-component' @@ -43,7 +42,9 @@ import { encodePath, lifeCycleToArray, supportPerMethodInlineHandler, - redirect + redirect, + emptySchema, + insertStandaloneValidator } from './utils' import { @@ -52,7 +53,8 @@ import { getSchemaValidator, getResponseSchemaValidator, getCookieValidator, - ElysiaTypeCheck + ElysiaTypeCheck, + queryCoercions } from './schema' import { composeHandler, @@ -68,9 +70,7 @@ import { mergeLifeCycle, filterGlobalHook, asHookType, - traceBackMacro, - replaceUrlPath, - createMacroManager + replaceUrlPath } from './utils' import { @@ -80,13 +80,13 @@ import { } from './dynamic-handle' import { + status, ERROR_CODE, ValidationError, type ParseError, type NotFoundError, type InternalServerError, - ElysiaCustomStatusResponse, - status + type ElysiaCustomStatusResponse } from './error' import type { TraceHandler } from './trace' @@ -132,11 +132,9 @@ import type { InlineHandler, HookContainer, LifeCycleType, - MacroQueue, EphemeralType, ExcludeElysiaResponse, ModelValidator, - BaseMacroFn, ContextAppendType, Reconcile, AfterResponseHandler, @@ -145,19 +143,26 @@ import type { JoinPath, ValidatorLayer, MergeElysiaInstances, - HookMacroFn, - ResolveHandler, - ResolveResolutions, - UnwrapTypeModule, + Macro, MacroToContext, StandaloneValidator, GuardSchemaType, Or, - PrettifySchema, - MergeStandaloneSchema, - IsNever, DocumentDecoration, - AfterHandler + AfterHandler, + NonResolvableMacroKey, + StandardSchemaV1Like, + ElysiaHandlerToResponseSchema, + ElysiaHandlerToResponseSchemas, + ExtractErrorFromHandle, + ElysiaHandlerToResponseSchemaAmbiguous, + GuardLocalHook, + PickIfExists, + SimplifyToSchema, + UnionResponseStatus, + CreateEdenResponse, + MacroProperty, + MaybeValueOrVoidFunction } from './types' export type AnyElysia = Elysia @@ -194,6 +199,7 @@ export default class Elysia< macro: {} macroFn: {} parser: {} + response: {} }, const out Routes extends RouteBase = {}, // ? scoped @@ -202,6 +208,7 @@ export default class Elysia< resolve: {} schema: {} standaloneSchema: {} + response: {} }, // ? local const in out Volatile extends EphemeralType = { @@ -209,6 +216,7 @@ export default class Elysia< resolve: {} schema: {} standaloneSchema: {} + response: {} } > { config: ElysiaConfig @@ -241,12 +249,12 @@ export default class Elysia< protected definitions = { typebox: t.Module({}), - type: {} as Record, + type: {} as Record, error: {} as Record } protected extender = { - macros: [], + macro: {}, higherOrderFunctions: []>[] } @@ -450,52 +458,14 @@ export default class Elysia< return this } - private applyMacro(localHook: AnyLocalHook) { - if (this.extender.macros.length) { - const manage = createMacroManager({ - globalHook: this.event, - localHook - }) - - const manager: MacroManager = { - events: { - global: this.event, - local: localHook - }, - get onParse() { - return manage('parse') as any - }, - get onTransform() { - return manage('transform') as any - }, - get onBeforeHandle() { - return manage('beforeHandle') as any - }, - get onAfterHandle() { - return manage('afterHandle') as any - }, - get mapResponse() { - return manage('mapResponse') as any - }, - get onAfterResponse() { - return manage('afterResponse') as any - }, - get onError() { - return manage('error') as any - } - } - - for (const macro of this.extender.macros) - traceBackMacro(macro.fn(manager), localHook, manage) - } - } - get models(): { [K in keyof Definitions['typebox']]: ModelValidator< Definitions['typebox'][K] > } & { - modules: TModule + modules: + | TModule> + | Extract } { const models: Record> = {} @@ -518,32 +488,36 @@ export default class Elysia< options?: { allowMeta?: boolean skipPrefix?: boolean - }, - standaloneValidators?: InputSchema[] + } ) { const skipPrefix = options?.skipPrefix ?? false const allowMeta = options?.allowMeta ?? false localHook ??= {} - if (standaloneValidators === undefined) { - standaloneValidators = [] + this.applyMacro(localHook) - if (this.standaloneValidator.local) - standaloneValidators = standaloneValidators.concat( - this.standaloneValidator.local - ) + let standaloneValidators = [] as InputSchema[] - if (this.standaloneValidator.scoped) - standaloneValidators = standaloneValidators.concat( - this.standaloneValidator.scoped - ) + if (localHook.standaloneValidator) + standaloneValidators = standaloneValidators.concat( + localHook.standaloneValidator + ) - if (this.standaloneValidator.global) - standaloneValidators = standaloneValidators.concat( - this.standaloneValidator.global - ) - } + if (this.standaloneValidator.local) + standaloneValidators = standaloneValidators.concat( + this.standaloneValidator.local + ) + + if (this.standaloneValidator.scoped) + standaloneValidators = standaloneValidators.concat( + this.standaloneValidator.scoped + ) + + if (this.standaloneValidator.global) + standaloneValidators = standaloneValidators.concat( + this.standaloneValidator.global + ) if (path !== '' && path.charCodeAt(0) !== 47) path = '/' + path if (this.config.prefix && !skipPrefix) path = this.config.prefix + path @@ -655,7 +629,7 @@ export default class Elysia< models, normalize, coerce: true, - additionalCoerce: stringToStructureCoercions(), + additionalCoerce: queryCoercions(), validators: standaloneValidators.map( (x) => x.query ), @@ -668,7 +642,7 @@ export default class Elysia< models, normalize, validators: standaloneValidators.map( - (x) => x.response + (x) => x.response as any ), sanitize }) @@ -744,8 +718,7 @@ export default class Elysia< models, normalize, coerce: true, - additionalCoerce: - stringToStructureCoercions(), + additionalCoerce: queryCoercions(), validators: standaloneValidators.map( (x) => x.query ), @@ -769,7 +742,7 @@ export default class Elysia< models, normalize, validators: standaloneValidators.map( - (x) => x.response + (x) => x.response as any ), sanitize } @@ -802,11 +775,14 @@ export default class Elysia< localHook.detail ) - this.applyMacro(localHook) - const hooks = isNotEmpty(this.event) ? mergeHook(this.event, localHookToLifeCycleStore(localHook)) - : lifeCycleToArray(localHookToLifeCycleStore(localHook)) + : { ...lifeCycleToArray(localHookToLifeCycleStore(localHook)) } + + if (standaloneValidators.length) + Object.assign(hooks, { + standaloneValidator: standaloneValidators + }) if (this.config.aot === false) { const validator = createValidator() @@ -857,8 +833,7 @@ export default class Elysia< composed: null, handler: handle, compile: undefined as any, - hooks, - standaloneValidators + hooks }) return @@ -881,7 +856,6 @@ export default class Elysia< headers: Object.assign({}, this.setHeaders) }, status, - error: status, store: this.store } @@ -1014,11 +988,6 @@ export default class Elysia< handler: handle, hooks }, - standaloneValidators.length - ? { - standaloneValidators - } - : undefined, localHook.webSocket ? { websocket: localHook.websocket as any } : undefined @@ -1133,31 +1102,105 @@ export default class Elysia< * }) * ``` */ - onRequest( - handler: MaybeArray< - PreHandler< + onRequest< + const Schema extends RouteSchema, + const Handler extends PreHandler< + MergeSchema< + Schema, MergeSchema< - Schema, - MergeSchema< - Volatile['schema'], - MergeSchema - > - > & - Metadata['standaloneSchema'] & - Ephemeral['standaloneSchema'] & - Volatile['standaloneSchema'], - { - decorator: Singleton['decorator'] - store: Singleton['store'] - derive: {} - resolve: {} - } - > + Volatile['schema'], + MergeSchema + > + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + { + decorator: Singleton['decorator'] + store: Singleton['store'] + derive: {} + resolve: {} + } > - ) { + >( + handler: Handler + ): Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + Ephemeral, + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchema + > + } + > + + /** + * ### request | Life cycle event + * Called on every new request is accepted + * + * --- + * @example + * ```typescript + * new Elysia() + * .onRequest(({ method, url }) => { + * saveToAnalytic({ method, url }) + * }) + * ``` + */ + onRequest< + const Schema extends RouteSchema, + const Handlers extends PreHandler< + MergeSchema< + Schema, + MergeSchema< + Volatile['schema'], + MergeSchema + > + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + { + decorator: Singleton['decorator'] + store: Singleton['store'] + derive: {} + resolve: {} + } + >[] + >( + handler: Handlers + ): Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + Ephemeral, + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchema + > + } + > + + onRequest(handler: MaybeArray): any { this.on('request', handler as any) - return this + return this as any } /** @@ -1225,7 +1268,7 @@ export default class Elysia< * ``` */ onParse( - options: { as?: Type }, + options: { as: Type }, parser: MaybeArray< BodyHandler< MergeSchema< @@ -1280,7 +1323,7 @@ export default class Elysia< ): this onParse( - options: { as?: LifeCycleType } | MaybeArray | string, + options: { as: LifeCycleType } | MaybeArray | string, handler?: MaybeArray ): unknown { if (!handler) { @@ -1291,7 +1334,7 @@ export default class Elysia< } return this.on( - options as { as?: LifeCycleType }, + options as { as: LifeCycleType }, 'parse', handler as any ) @@ -1353,6 +1396,7 @@ export default class Elysia< macro: Metadata['macro'] macroFn: Metadata['macroFn'] parser: Metadata['parser'] & { [K in Parser]: Handler } + response: Metadata['response'] }, Routes, Ephemeral, @@ -1421,7 +1465,7 @@ export default class Elysia< const Schema extends RouteSchema, const Type extends LifeCycleType >( - options: { as?: Type }, + options: { as: Type }, handler: MaybeArray< TransformHandler< MergeSchema< @@ -1472,13 +1516,13 @@ export default class Elysia< ): this onTransform( - options: { as?: LifeCycleType } | MaybeArray, + options: { as: LifeCycleType } | MaybeArray, handler?: MaybeArray ) { if (!handler) return this.on('transform', options as any) return this.on( - options as { as?: LifeCycleType }, + options as { as: LifeCycleType }, 'transform', handler as any ) @@ -1505,7 +1549,7 @@ export default class Elysia< | ElysiaCustomStatusResponse, const Type extends LifeCycleType >( - options: { as?: Type }, + options: { as: Type }, resolver: ( context: Prettify< Context< @@ -1561,7 +1605,17 @@ export default class Elysia< > }, Definitions, - Metadata, + { + schema: Metadata['schema'] + standaloneSchema: Metadata['standaloneSchema'] + macro: Metadata['macro'] + macroFn: Metadata['macroFn'] + parser: Metadata['parser'] + response: UnionResponseStatus< + Metadata['response'], + ExtractErrorFromHandle + > + }, Routes, Ephemeral, Volatile @@ -1581,6 +1635,10 @@ export default class Elysia< > schema: Ephemeral['schema'] standaloneSchema: Ephemeral['standaloneSchema'] + response: UnionResponseStatus< + Ephemeral['response'], + ExtractErrorFromHandle + > }, Volatile > @@ -1599,6 +1657,10 @@ export default class Elysia< > schema: Volatile['schema'] standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ExtractErrorFromHandle + > } > @@ -1656,11 +1718,15 @@ export default class Elysia< > schema: Volatile['schema'] standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ExtractErrorFromHandle + > } > resolve( - optionsOrResolve: { as?: LifeCycleType } | Function, + optionsOrResolve: { as: LifeCycleType } | Function, resolve?: Function ) { if (!resolve) { @@ -1709,6 +1775,10 @@ export default class Elysia< resolve: ExcludeElysiaResponse schema: Volatile['schema'] standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ExtractErrorFromHandle + > } > @@ -1718,7 +1788,7 @@ export default class Elysia< | ElysiaCustomStatusResponse, const Type extends LifeCycleType >( - options: { as?: Type }, + options: { as: Type }, mapper: ( context: Context< MergeSchema< @@ -1763,7 +1833,17 @@ export default class Elysia< resolve: ExcludeElysiaResponse }, Definitions, - Metadata, + { + schema: Metadata['schema'] + standaloneSchema: Metadata['standaloneSchema'] + macro: Metadata['macro'] + macroFn: Metadata['macroFn'] + parser: Metadata['parser'] + response: UnionResponseStatus< + Metadata['response'], + ExtractErrorFromHandle + > + }, Routes, Ephemeral, Volatile @@ -1783,6 +1863,10 @@ export default class Elysia< > schema: Ephemeral['schema'] standaloneSchema: Ephemeral['standaloneSchema'] + response: UnionResponseStatus< + Ephemeral['response'], + ExtractErrorFromHandle + > }, Volatile > @@ -1801,11 +1885,15 @@ export default class Elysia< > schema: Volatile['schema'] standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ExtractErrorFromHandle + > } > mapResolve( - optionsOrResolve: Function | { as?: LifeCycleType }, + optionsOrResolve: Function | { as: LifeCycleType }, mapper?: Function ) { if (!mapper) { @@ -1840,27 +1928,45 @@ export default class Elysia< * }) * ``` */ - onBeforeHandle( - handler: MaybeArray< - OptionalHandler< + onBeforeHandle< + const Schema extends RouteSchema, + const Handler extends OptionalHandler< + MergeSchema< + Schema, MergeSchema< - Schema, - MergeSchema< - Volatile['schema'], - MergeSchema - >, - BasePath - > & - Metadata['standaloneSchema'] & - Ephemeral['standaloneSchema'] & - Volatile['standaloneSchema'], - Singleton & { - derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & Volatile['resolve'] - } - > + Volatile['schema'], + MergeSchema + >, + BasePath + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Singleton & { + derive: Ephemeral['derive'] & Volatile['derive'] + resolve: Ephemeral['resolve'] & Volatile['resolve'] + } > - ): this + >( + handler: Handler + ): Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + Ephemeral, + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchema + > + } + > /** * ### Before Handle | Life cycle event @@ -1883,63 +1989,300 @@ export default class Elysia< */ onBeforeHandle< const Schema extends RouteSchema, - const Type extends LifeCycleType - >( - options: { as?: Type }, - handler: MaybeArray< - OptionalHandler< + const Handlers extends OptionalHandler< + MergeSchema< + Schema, MergeSchema< - Schema, - MergeSchema< - Volatile['schema'], - MergeSchema - >, - BasePath - > & - Metadata['standaloneSchema'] & - Ephemeral['standaloneSchema'] & - Volatile['standaloneSchema'] & - 'global' extends Type - ? { params: Record } - : 'scoped' extends Type - ? { params: Record } - : {}, - Singleton & - ('global' extends Type - ? { - derive: Partial< - Ephemeral['derive'] & Volatile['derive'] - > - resolve: Partial< - Ephemeral['resolve'] & Volatile['resolve'] - > - } - : 'scoped' extends Type - ? { - derive: Ephemeral['derive'] & - Partial - resolve: Ephemeral['resolve'] & - Partial - } - : { - derive: Ephemeral['derive'] & - Volatile['derive'] - resolve: Ephemeral['resolve'] & - Volatile['resolve'] - }), + Volatile['schema'], + MergeSchema + >, BasePath + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Singleton & { + derive: Ephemeral['derive'] & Volatile['derive'] + resolve: Ephemeral['resolve'] & Volatile['resolve'] + } + >[] + >( + handlers: Handlers + ): Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + Ephemeral, + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchemas > + } + > + + /** + * ### Before Handle | Life cycle event + * Execute after validation and before the main route handler. + * + * If truthy value is returned, will be assigned as `Response` and skip the main handler + * + * --- + * @example + * ```typescript + * new Elysia() + * .onBeforeHandle(({ params: { id }, status }) => { + * if(id && !isExisted(id)) { + * status(401) + * + * return "Unauthorized" + * } + * }) + * ``` + */ + onBeforeHandle< + const Schema extends RouteSchema, + const Type extends LifeCycleType, + const Handler extends OptionalHandler< + MergeSchema< + Schema, + MergeSchema< + Volatile['schema'], + MergeSchema + >, + BasePath + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'] & + 'global' extends Type + ? { params: Record } + : 'scoped' extends Type + ? { params: Record } + : {}, + Singleton & + ('global' extends Type + ? { + derive: Partial< + Ephemeral['derive'] & Volatile['derive'] + > + resolve: Partial< + Ephemeral['resolve'] & Volatile['resolve'] + > + } + : 'scoped' extends Type + ? { + derive: Ephemeral['derive'] & + Partial + resolve: Ephemeral['resolve'] & + Partial + } + : { + derive: Ephemeral['derive'] & Volatile['derive'] + resolve: Ephemeral['resolve'] & + Volatile['resolve'] + }), + BasePath > - ): this + >( + options: { as: Type }, + handler: Handler + ): Type extends 'global' + ? Elysia< + BasePath, + Singleton, + Definitions, + { + schema: Metadata['schema'] + standaloneSchema: Metadata['standaloneSchema'] + macro: Metadata['macro'] + macroFn: Metadata['macroFn'] + parser: Metadata['parser'] + response: UnionResponseStatus< + Metadata['response'], + ElysiaHandlerToResponseSchema + > + }, + Routes, + Ephemeral, + Volatile + > + : Type extends 'scoped' + ? Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + { + derive: Ephemeral['derive'] + resolve: Ephemeral['resolve'] + schema: Ephemeral['schema'] + standaloneSchema: Ephemeral['standaloneSchema'] + response: UnionResponseStatus< + Ephemeral['response'], + ElysiaHandlerToResponseSchema + > + }, + Volatile + > + : Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + Ephemeral, + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchema + > + } + > + + /** + * ### Before Handle | Life cycle event + * Execute after validation and before the main route handler. + * + * If truthy value is returned, will be assigned as `Response` and skip the main handler + * + * --- + * @example + * ```typescript + * new Elysia() + * .onBeforeHandle(({ params: { id }, status }) => { + * if(id && !isExisted(id)) { + * status(401) + * + * return "Unauthorized" + * } + * }) + * ``` + */ + onBeforeHandle< + const Schema extends RouteSchema, + const Type extends LifeCycleType, + const Handlers extends OptionalHandler< + MergeSchema< + Schema, + MergeSchema< + Volatile['schema'], + MergeSchema + >, + BasePath + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'] & + 'global' extends Type + ? { params: Record } + : 'scoped' extends Type + ? { params: Record } + : {}, + Singleton & + ('global' extends Type + ? { + derive: Partial< + Ephemeral['derive'] & Volatile['derive'] + > + resolve: Partial< + Ephemeral['resolve'] & Volatile['resolve'] + > + } + : 'scoped' extends Type + ? { + derive: Ephemeral['derive'] & + Partial + resolve: Ephemeral['resolve'] & + Partial + } + : { + derive: Ephemeral['derive'] & Volatile['derive'] + resolve: Ephemeral['resolve'] & + Volatile['resolve'] + }), + BasePath + >[] + >( + options: { as: Type }, + handlers: Handlers + ): Type extends 'global' + ? Elysia< + BasePath, + Singleton, + Definitions, + { + schema: Metadata['schema'] + standaloneSchema: Metadata['standaloneSchema'] + macro: Metadata['macro'] + macroFn: Metadata['macroFn'] + parser: Metadata['parser'] + response: UnionResponseStatus< + Metadata['response'], + ElysiaHandlerToResponseSchemas + > + }, + Routes, + Ephemeral, + Volatile + > + : Type extends 'scoped' + ? Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + { + derive: Ephemeral['derive'] + resolve: Ephemeral['resolve'] + schema: Ephemeral['schema'] + standaloneSchema: Ephemeral['standaloneSchema'] + response: UnionResponseStatus< + Ephemeral['response'], + ElysiaHandlerToResponseSchemas + > + }, + Volatile + > + : Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + Ephemeral, + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchemas + > + } + > onBeforeHandle( - options: { as?: LifeCycleType } | MaybeArray, + options: { as: LifeCycleType } | MaybeArray, handler?: MaybeArray - ) { + ): any { if (!handler) return this.on('beforeHandle', options as any) return this.on( - options as { as?: LifeCycleType }, + options as { as: LifeCycleType }, 'beforeHandle', handler as any ) @@ -1961,27 +2304,221 @@ export default class Elysia< * }) * ``` */ - onAfterHandle( - handler: MaybeArray< - AfterHandler< + onAfterHandle< + const Schema extends RouteSchema, + const Handler extends AfterHandler< + MergeSchema< + Schema, MergeSchema< - Schema, - MergeSchema< - Volatile['schema'], - MergeSchema - >, - BasePath - > & - Metadata['standaloneSchema'] & - Ephemeral['standaloneSchema'] & - Volatile['standaloneSchema'], - Singleton & { - derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & Volatile['resolve'] - } + Volatile['schema'], + MergeSchema + >, + BasePath + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Singleton & { + derive: Ephemeral['derive'] & Volatile['derive'] + resolve: Ephemeral['resolve'] & Volatile['resolve'] + } + > + >( + handler: Handler + ): Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + Ephemeral, + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchema > + } + > + + /** + * ### 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) + * }) + * ``` + */ + onAfterHandle< + const Schema extends RouteSchema, + const Handlers extends AfterHandler< + MergeSchema< + Schema, + MergeSchema< + Volatile['schema'], + MergeSchema + >, + BasePath + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Singleton & { + derive: Ephemeral['derive'] & Volatile['derive'] + resolve: Ephemeral['resolve'] & Volatile['resolve'] + } + >[] + >( + handlers: Handlers + ): Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + Ephemeral, + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchemas + > + } + > + + /** + * ### 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) + * }) + * ``` + */ + onAfterHandle< + const Schema extends RouteSchema, + const Type extends LifeCycleType, + const Handler extends AfterHandler< + MergeSchema< + Schema, + MergeSchema< + Volatile['schema'], + MergeSchema + >, + BasePath + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'] & + 'global' extends Type + ? { params: Record } + : 'scoped' extends Type + ? { params: Record } + : {}, + Singleton & + ('global' extends Type + ? { + derive: Partial< + Ephemeral['derive'] & Volatile['derive'] + > + resolve: Partial< + Ephemeral['resolve'] & Volatile['resolve'] + > + } + : 'scoped' extends Type + ? { + derive: Ephemeral['derive'] & + Partial + resolve: Ephemeral['resolve'] & + Partial + } + : { + derive: Ephemeral['derive'] & Volatile['derive'] + resolve: Ephemeral['resolve'] & + Volatile['resolve'] + }) > - ): this + >( + options: { as: Type }, + handler: Handler + ): Type extends 'global' + ? Elysia< + BasePath, + Singleton, + Definitions, + { + schema: Metadata['schema'] + standaloneSchema: Metadata['standaloneSchema'] + macro: Metadata['macro'] + macroFn: Metadata['macroFn'] + parser: Metadata['parser'] + response: UnionResponseStatus< + Metadata['response'], + ElysiaHandlerToResponseSchema + > + }, + Routes, + Ephemeral, + Volatile + > + : Type extends 'scoped' + ? Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + { + derive: Ephemeral['derive'] + resolve: Ephemeral['resolve'] + schema: Ephemeral['schema'] + standaloneSchema: Ephemeral['standaloneSchema'] + response: UnionResponseStatus< + Ephemeral['response'], + ElysiaHandlerToResponseSchema + > + }, + Volatile + > + : Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + Ephemeral, + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchema + > + } + > /** * ### After Handle | Life cycle event @@ -2001,62 +2538,116 @@ export default class Elysia< */ onAfterHandle< const Schema extends RouteSchema, - const Type extends LifeCycleType - >( - options: { as?: LifeCycleType }, - handler: MaybeArray< - AfterHandler< + const Type extends LifeCycleType, + const Handlers extends AfterHandler< + MergeSchema< + Schema, MergeSchema< - Schema, - MergeSchema< - Volatile['schema'], - MergeSchema - >, - BasePath - > & - Metadata['standaloneSchema'] & - Ephemeral['standaloneSchema'] & - Volatile['standaloneSchema'] & - 'global' extends Type + Volatile['schema'], + MergeSchema + >, + BasePath + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'] & + 'global' extends Type + ? { params: Record } + : 'scoped' extends Type ? { params: Record } + : {}, + Singleton & + ('global' extends Type + ? { + derive: Partial< + Ephemeral['derive'] & Volatile['derive'] + > + resolve: Partial< + Ephemeral['resolve'] & Volatile['resolve'] + > + } : 'scoped' extends Type - ? { params: Record } - : {}, - Singleton & - ('global' extends Type ? { - derive: Partial< - Ephemeral['derive'] & Volatile['derive'] - > - resolve: Partial< - Ephemeral['resolve'] & Volatile['resolve'] - > + derive: Ephemeral['derive'] & + Partial + resolve: Ephemeral['resolve'] & + Partial } - : 'scoped' extends Type - ? { - derive: Ephemeral['derive'] & - Partial - resolve: Ephemeral['resolve'] & - Partial - } - : { - derive: Ephemeral['derive'] & - Volatile['derive'] - resolve: Ephemeral['resolve'] & - Volatile['resolve'] - }) + : { + derive: Ephemeral['derive'] & Volatile['derive'] + resolve: Ephemeral['resolve'] & + Volatile['resolve'] + }) + >[] + >( + options: { as: Type }, + handler: Handlers + ): Type extends 'global' + ? Elysia< + BasePath, + Singleton, + Definitions, + { + schema: Metadata['schema'] + standaloneSchema: Metadata['standaloneSchema'] + macro: Metadata['macro'] + macroFn: Metadata['macroFn'] + parser: Metadata['parser'] + response: UnionResponseStatus< + Metadata['response'], + ElysiaHandlerToResponseSchemas + > + }, + Routes, + Ephemeral, + Volatile > - > - ): this + : Type extends 'scoped' + ? Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + { + derive: Ephemeral['derive'] + resolve: Ephemeral['resolve'] + schema: Ephemeral['schema'] + standaloneSchema: Ephemeral['standaloneSchema'] + response: UnionResponseStatus< + Ephemeral['response'], + ElysiaHandlerToResponseSchemas + > + }, + Volatile + > + : Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + Ephemeral, + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchemas + > + } + > onAfterHandle( - options: { as?: LifeCycleType } | MaybeArray, + options: { as: LifeCycleType } | MaybeArray, handler?: MaybeArray - ) { + ): any { if (!handler) return this.on('afterHandle', options as any) return this.on( - options as { as?: LifeCycleType }, + options as { as: LifeCycleType }, 'afterHandle', handler as any ) @@ -2117,7 +2708,7 @@ export default class Elysia< * ``` */ mapResponse( - options: { as?: Type }, + options: { as: Type }, handler: MaybeArray< MapResponse< MergeSchema< @@ -2164,13 +2755,13 @@ export default class Elysia< ): this mapResponse( - options: { as?: LifeCycleType } | MaybeArray, + options: { as: LifeCycleType } | MaybeArray, handler?: MaybeArray ) { if (!handler) return this.on('mapResponse', options as any) return this.on( - options as { as?: LifeCycleType }, + options as { as: LifeCycleType }, 'mapResponse', handler as any ) @@ -2230,7 +2821,7 @@ export default class Elysia< const Schema extends RouteSchema, const Type extends LifeCycleType >( - options: { as?: Type }, + options: { as: Type }, handler: MaybeArray< AfterResponseHandler< MergeSchema< @@ -2277,13 +2868,13 @@ export default class Elysia< ): this onAfterResponse( - options: { as?: LifeCycleType } | MaybeArray, + options: { as: LifeCycleType } | MaybeArray, handler?: MaybeArray ) { if (!handler) return this.on('afterResponse', options as any) return this.on( - options as { as?: LifeCycleType }, + options as { as: LifeCycleType }, 'afterResponse', handler as any ) @@ -2326,7 +2917,7 @@ export default class Elysia< * ``` */ trace( - options: { as?: LifeCycleType }, + options: { as: LifeCycleType }, handler: MaybeArray> ): this @@ -2347,7 +2938,7 @@ export default class Elysia< * ``` */ trace( - options: { as?: LifeCycleType } | MaybeArray, + options: { as: LifeCycleType } | MaybeArray, handler?: MaybeArray ) { if (!handler) { @@ -2359,7 +2950,7 @@ export default class Elysia< for (const fn of handler) this.on( - options as { as?: LifeCycleType }, + options as { as: LifeCycleType }, 'trace', createTracer(fn as any) as any ) @@ -2477,63 +3068,169 @@ export default class Elysia< ): Elysia< BasePath, Singleton, - { - typebox: Definitions['typebox'] - error: { - [K in keyof NewErrors]: NewErrors[K] extends { - prototype: infer LiteralError extends Error - } - ? LiteralError - : never - } - }, + { + typebox: Definitions['typebox'] + error: { + [K in keyof NewErrors]: NewErrors[K] extends { + prototype: infer LiteralError extends Error + } + ? LiteralError + : never + } + }, + Metadata, + Routes, + Ephemeral, + Volatile + > + + 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 + } + ): AnyElysia { + 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 as any + + this.definitions.error[code] = error as any + } + + 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 :(" + * }) + * ``` + */ + onError< + const Schema extends RouteSchema, + const Handler extends ErrorHandler< + Definitions['error'], + MergeSchema< + Schema, + MergeSchema< + Volatile['schema'], + MergeSchema + > + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Singleton, + Ephemeral, + Volatile + > + >( + handler: Handler + ): Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + Ephemeral, + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchema + > + } + > + + /** + * ### 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 :(" + * }) + * ``` + */ + onError< + const Schema extends RouteSchema, + const Handlers extends ErrorHandler< + Definitions['error'], + MergeSchema< + Schema, + MergeSchema< + Volatile['schema'], + MergeSchema + > + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Singleton, + Ephemeral, + Volatile + >[] + >( + handler: Handlers + ): Elysia< + BasePath, + Singleton, + Definitions, Metadata, Routes, Ephemeral, - Volatile - > - - 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 - } - ): AnyElysia { - 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 as any - - this.definitions.error[code] = error as any + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchemas + > } - - return this - } + > /** * ### Error | Life cycle event @@ -2549,26 +3246,121 @@ export default class Elysia< * }) * ``` */ - onError( - handler: MaybeArray< - ErrorHandler< - Definitions['error'], + onError< + const Schema extends RouteSchema, + const Type extends LifeCycleType, + const Handler extends ErrorHandler< + Definitions['error'], + MergeSchema< + Schema, MergeSchema< - Schema, - MergeSchema< - Volatile['schema'], - MergeSchema - > - > & - Metadata['standaloneSchema'] & - Ephemeral['standaloneSchema'] & - Volatile['standaloneSchema'], + Volatile['schema'], + MergeSchema + > + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Type extends 'global' + ? { + store: Singleton['store'] + decorator: Singleton['decorator'] + derive: Singleton['derive'] & + Ephemeral['derive'] & + Volatile['derive'] + resolve: Singleton['resolve'] & + Ephemeral['resolve'] & + Volatile['resolve'] + } + : Type extends 'scoped' + ? { + store: Singleton['store'] + decorator: Singleton['decorator'] + derive: Singleton['derive'] & Ephemeral['derive'] + resolve: Singleton['resolve'] & Ephemeral['resolve'] + } + : Singleton, + Type extends 'global' + ? Ephemeral + : { + derive: Partial + resolve: Partial + schema: Ephemeral['schema'] + standaloneSchema: Ephemeral['standaloneSchema'] + response: Ephemeral['response'] + }, + Type extends 'global' + ? Ephemeral + : Type extends 'scoped' + ? Ephemeral + : { + derive: Partial + resolve: Partial + schema: Ephemeral['schema'] + standaloneSchema: Ephemeral['standaloneSchema'] + response: Ephemeral['response'] + } + > + >( + options: { as: Type }, + handler: Handler + ): Type extends 'global' + ? Elysia< + BasePath, Singleton, + Definitions, + { + schema: Metadata['schema'] + standaloneSchema: Metadata['standaloneSchema'] + macro: Metadata['macro'] + macroFn: Metadata['macroFn'] + parser: Metadata['parser'] + response: UnionResponseStatus< + Metadata['response'], + ElysiaHandlerToResponseSchema + > + }, + Routes, Ephemeral, Volatile > - > - ): this + : Type extends 'scoped' + ? Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + { + derive: Ephemeral['derive'] + resolve: Ephemeral['resolve'] + schema: Ephemeral['schema'] + standaloneSchema: Ephemeral['standaloneSchema'] + response: UnionResponseStatus< + Ephemeral['response'], + ElysiaHandlerToResponseSchema + > + }, + Volatile + > + : Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + Ephemeral, + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchema + > + } + > /** * ### Error | Life cycle event @@ -2586,64 +3378,119 @@ export default class Elysia< */ onError< const Schema extends RouteSchema, - const Scope extends LifeCycleType - >( - options: { as?: Scope }, - handler: MaybeArray< - ErrorHandler< - Definitions['error'], + const Type extends LifeCycleType, + const Handlers extends ErrorHandler< + Definitions['error'], + MergeSchema< + Schema, MergeSchema< - Schema, - MergeSchema< - Volatile['schema'], - MergeSchema - > - > & - Metadata['standaloneSchema'] & - Ephemeral['standaloneSchema'] & - Volatile['standaloneSchema'], - Scope extends 'global' + Volatile['schema'], + MergeSchema + > + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Type extends 'global' + ? { + store: Singleton['store'] + decorator: Singleton['decorator'] + derive: Singleton['derive'] & + Ephemeral['derive'] & + Volatile['derive'] + resolve: Singleton['resolve'] & + Ephemeral['resolve'] & + Volatile['resolve'] + } + : Type extends 'scoped' ? { store: Singleton['store'] decorator: Singleton['decorator'] - derive: Singleton['derive'] & - Ephemeral['derive'] & - Volatile['derive'] - resolve: Singleton['resolve'] & - Ephemeral['resolve'] & - Volatile['resolve'] + derive: Singleton['derive'] & Ephemeral['derive'] + resolve: Singleton['resolve'] & Ephemeral['resolve'] } - : Scope extends 'scoped' - ? { - store: Singleton['store'] - decorator: Singleton['decorator'] - derive: Singleton['derive'] & - Ephemeral['derive'] - resolve: Singleton['resolve'] & - Ephemeral['resolve'] - } - : Singleton, - Scope extends 'global' + : Singleton, + Type extends 'global' + ? Ephemeral + : { + derive: Partial + resolve: Partial + schema: Ephemeral['schema'] + standaloneSchema: Ephemeral['standaloneSchema'] + response: Ephemeral['response'] + }, + Type extends 'global' + ? Ephemeral + : Type extends 'scoped' ? Ephemeral : { derive: Partial resolve: Partial schema: Ephemeral['schema'] standaloneSchema: Ephemeral['standaloneSchema'] - }, - Scope extends 'global' - ? Ephemeral - : Scope extends 'scoped' - ? Ephemeral - : { - derive: Partial - resolve: Partial - schema: Ephemeral['schema'] - standaloneSchema: Ephemeral['standaloneSchema'] - } + response: Ephemeral['response'] + } + >[] + >( + options: { as: Type }, + handler: Handlers + ): Type extends 'global' + ? Elysia< + BasePath, + Singleton, + Definitions, + { + schema: Metadata['schema'] + standaloneSchema: Metadata['standaloneSchema'] + macro: Metadata['macro'] + macroFn: Metadata['macroFn'] + parser: Metadata['parser'] + response: UnionResponseStatus< + Metadata['response'], + ElysiaHandlerToResponseSchemas + > + }, + Routes, + Ephemeral, + Volatile > - > - ): this + : Type extends 'scoped' + ? Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + { + derive: Ephemeral['derive'] + resolve: Ephemeral['resolve'] + schema: Ephemeral['schema'] + standaloneSchema: Ephemeral['standaloneSchema'] + response: UnionResponseStatus< + Ephemeral['response'], + ElysiaHandlerToResponseSchemas + > + }, + Volatile + > + : Elysia< + BasePath, + Singleton, + Definitions, + Metadata, + Routes, + Ephemeral, + { + derive: Volatile['derive'] + resolve: Volatile['resolve'] + schema: Volatile['schema'] + standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ElysiaHandlerToResponseSchemas + > + } + > /** * ### Error | Life cycle event @@ -2660,13 +3507,13 @@ export default class Elysia< * ``` */ onError( - options: { as?: LifeCycleType } | MaybeArray, + options: { as: LifeCycleType } | MaybeArray, handler?: MaybeArray - ) { + ): any { if (!handler) return this.on('error', options as any) return this.on( - options as { as?: LifeCycleType }, + options as { as: LifeCycleType }, 'error', handler as any ) @@ -2731,13 +3578,13 @@ export default class Elysia< * ``` */ on( - options: { as?: LifeCycleType }, + options: { as: LifeCycleType }, type: Event, handlers: MaybeArray[0]> ): this on( - optionsOrType: { as?: LifeCycleType } | string, + optionsOrType: { as: LifeCycleType } | string, typeOrHandlers: MaybeArray | string, handlers?: MaybeArray ) { @@ -2894,10 +3741,17 @@ export default class Elysia< MergeSchema, Metadata['schema'] > - standaloneSchema: Metadata['standaloneSchema'] + standaloneSchema: Prettify< + Metadata['standaloneSchema'] & + Volatile['standaloneSchema'] & + Ephemeral['standaloneSchema'] + > macro: Metadata['macro'] macroFn: Metadata['macroFn'] parser: Metadata['parser'] + response: Metadata['response'] & + Ephemeral['response'] & + Volatile['response'] }, Routes, { @@ -2905,12 +3759,14 @@ export default class Elysia< resolve: {} schema: {} standaloneSchema: {} + response: {} }, { derive: {} resolve: {} schema: {} standaloneSchema: {} + response: {} } > @@ -2924,15 +3780,17 @@ export default class Elysia< derive: Prettify resolve: Prettify schema: MergeSchema - standaloneSchema: PrettifySchema< + standaloneSchema: Prettify< Volatile['standaloneSchema'] & Ephemeral['standaloneSchema'] > + response: Prettify }, { derive: {} resolve: {} schema: {} standaloneSchema: {} + response: {} } > @@ -3018,6 +3876,7 @@ export default class Elysia< macro: Metadata['macro'] macroFn: Metadata['macroFn'] parser: Metadata['parser'] + response: Metadata['response'] }, {}, Ephemeral, @@ -3037,7 +3896,8 @@ export default class Elysia< group< const Prefix extends string, const NewElysia extends AnyElysia, - const Input extends InputSchema, + const Input extends Metadata['macro'] & + InputSchema, const Schema extends MergeSchema< UnwrapRoute< Input, @@ -3046,27 +3906,22 @@ export default class Elysia< >, Metadata['schema'] > & - Metadata['standaloneSchema'], - const Resolutions extends MaybeArray< - ResolveHandler< - Schema, - Singleton & { - derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & Volatile['resolve'] - } - > - > + Metadata['standaloneSchema'] >( prefix: Prefix, schema: LocalHook< Input, - Schema, + // @ts-ignore + Schema & { + response: {} + return: {} + resolve: {} + }, Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] resolve: Ephemeral['resolve'] & Volatile['resolve'] }, Definitions['error'], - Metadata['macro'], keyof Metadata['parser'] >, run: ( @@ -3083,17 +3938,17 @@ export default class Elysia< resolve: Prettify< Singleton['resolve'] & Ephemeral['resolve'] & - Volatile['resolve'] & - ResolveResolutions + Volatile['resolve'] > }, Definitions, { - schema: Prettify + schema: Schema standaloneSchema: Metadata['standaloneSchema'] macro: Metadata['macro'] macroFn: Metadata['macroFn'] parser: Metadata['parser'] + response: Metadata['response'] }, {}, Ephemeral, @@ -3166,12 +4021,20 @@ export default class Elysia< this.model(sandbox.definitions.type) Object.values(instance.router.history).forEach( - ({ method, path, handler, hooks, standaloneValidators }) => { + ({ method, path, handler, hooks }) => { path = (isSchema ? '' : (this.config.prefix ?? '')) + prefix + path if (isSchema) { - const hook = schemaOrRun + const { + body, + headers, + query, + params, + cookie, + response, + ...hook + } = schemaOrRun const localHook = hooks as AnyLocalHook this.add( @@ -3190,10 +4053,17 @@ export default class Elysia< : [ localHook.error, ...(sandbox.event.error ?? []) - ] + ], + standaloneValidator: { + body, + headers, + query, + params, + cookie, + response + } }), - undefined, - standaloneValidators + undefined ) } else { this.add( @@ -3205,8 +4075,7 @@ export default class Elysia< }), { skipPrefix: true - }, - standaloneValidators + } ) } } @@ -3215,42 +4084,58 @@ export default class Elysia< return this as any } + /** + * ### guard + * Encapsulate and pass hook into all child handler + * + * --- + * @example + * ```typescript + * import { t } from 'elysia' + * + * new Elysia() + * .guard({ + * body: t.Object({ + * username: t.String(), + * password: t.String() + * }) + * }) + * ``` + */ guard< - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, + const Input extends Metadata['macro'] & + InputSchema, const Schema extends MergeSchema< - UnwrapRoute, + UnwrapRoute, Metadata['schema'] >, - const Macro extends Metadata['macro'], const MacroContext extends MacroToContext< Metadata['macroFn'], - NoInfer + NoInfer> >, const GuardType extends GuardSchemaType, - const AsType extends LifeCycleType + const AsType extends LifeCycleType, + const BeforeHandle extends MaybeArray< + OptionalHandler + >, + const AfterHandle extends MaybeArray>, + const ErrorHandle extends MaybeArray< + ErrorHandler + > >( - hook: { - /** - * @default 'override' - */ - as?: AsType - /** - * @default 'standalone' - * @since 1.3.0 - */ - schema?: GuardType - } & LocalHook< - LocalSchema, + hook: GuardLocalHook< + Input, Schema, Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] resolve: Ephemeral['resolve'] & Volatile['resolve'] }, - Definitions['error'], - Macro, - keyof Metadata['parser'] + keyof Metadata['parser'], + GuardType, + AsType, + BeforeHandle, + AfterHandle, + ErrorHandle > ): Or< GuardSchemaType extends GuardType ? true : false, @@ -3269,17 +4154,37 @@ export default class Elysia< Ephemeral, { derive: Volatile['derive'] - resolve: Prettify - schema: Prettify< - MergeSchema< - UnwrapRoute< - LocalSchema, - Definitions['typebox'] - >, - Metadata['schema'] - > + resolve: Prettify< + Volatile['resolve'] & + // @ts-ignore + MacroContext['resolve'] + > + schema: {} extends PickIfExists< + Input, + keyof InputSchema + > + ? Volatile['schema'] + : Prettify< + MergeSchema< + UnwrapRoute< + Input, + Definitions['typebox'] + >, + Metadata['schema'] + > + > + standaloneSchema: Prettify< + Volatile['standaloneSchema'] & + SimplifyToSchema + > + response: Prettify< + Volatile['response'] & + ElysiaHandlerToResponseSchemaAmbiguous & + ElysiaHandlerToResponseSchemaAmbiguous & + ElysiaHandlerToResponseSchemaAmbiguous & + // @ts-ignore + MacroContext['return'] > - standaloneSchema: Volatile['standaloneSchema'] } > : AsType extends 'global' @@ -3290,25 +4195,43 @@ export default class Elysia< store: Singleton['store'] derive: Singleton['derive'] resolve: Prettify< - Singleton['resolve'] & MacroContext + Singleton['resolve'] & + // @ts-ignore + MacroContext['resolve'] > }, Definitions, { - schema: Prettify< - MergeSchema< - UnwrapRoute< - LocalSchema, - Definitions['typebox'], - BasePath - >, - Metadata['schema'] - > + schema: {} extends PickIfExists< + Input, + keyof InputSchema + > + ? Metadata['schema'] + : Prettify< + MergeSchema< + UnwrapRoute< + Input, + Definitions['typebox'], + BasePath + >, + Metadata['schema'] + > + > + standaloneSchema: Prettify< + Metadata['standaloneSchema'] & + SimplifyToSchema > - standaloneSchema: Metadata['standaloneSchema'] macro: Metadata['macro'] macroFn: Metadata['macroFn'] parser: Metadata['parser'] + response: Prettify< + Metadata['response'] & + ElysiaHandlerToResponseSchemaAmbiguous & + ElysiaHandlerToResponseSchemaAmbiguous & + ElysiaHandlerToResponseSchemaAmbiguous & + // @ts-ignore + MacroContext['return'] + > }, Routes, Ephemeral, @@ -3323,18 +4246,37 @@ export default class Elysia< { derive: Ephemeral['derive'] resolve: Prettify< - Ephemeral['resolve'] & MacroContext + Ephemeral['resolve'] & + // @ts-ignore + MacroContext['resolve'] > - schema: Prettify< - MergeSchema< - UnwrapRoute< - LocalSchema, - Definitions['typebox'] - >, - Metadata['schema'] & Ephemeral['schema'] - > + schema: {} extends PickIfExists< + Input, + keyof InputSchema + > + ? EphemeralType['schema'] + : Prettify< + MergeSchema< + UnwrapRoute< + Input, + Definitions['typebox'] + >, + Metadata['schema'] & + Ephemeral['schema'] + > + > + standaloneSchema: Prettify< + Ephemeral['standaloneSchema'] & + SimplifyToSchema + > + response: Prettify< + Ephemeral['response'] & + ElysiaHandlerToResponseSchemaAmbiguous & + ElysiaHandlerToResponseSchemaAmbiguous & + ElysiaHandlerToResponseSchemaAmbiguous & + // @ts-ignore + MacroContext['return'] > - standaloneSchema: Ephemeral['standaloneSchema'] }, Volatile > @@ -3351,10 +4293,33 @@ export default class Elysia< Ephemeral, { derive: Volatile['derive'] - resolve: Prettify + resolve: Prettify< + Volatile['resolve'] & + // @ts-ignore + MacroContext['resolve'] + > schema: Volatile['schema'] - standaloneSchema: Volatile['standaloneSchema'] & - UnwrapRoute + standaloneSchema: Prettify< + SimplifyToSchema & + ({} extends PickIfExists< + Input, + keyof InputSchema + > + ? Volatile['standaloneSchema'] + : Volatile['standaloneSchema'] & + UnwrapRoute< + Input, + Definitions['typebox'] + >) + > + response: Prettify< + Volatile['response'] & + ElysiaHandlerToResponseSchemaAmbiguous & + ElysiaHandlerToResponseSchemaAmbiguous & + ElysiaHandlerToResponseSchemaAmbiguous & + // @ts-ignore + MacroContext['return'] + > } > : AsType extends 'global' @@ -3365,21 +4330,39 @@ export default class Elysia< store: Singleton['store'] derive: Singleton['derive'] resolve: Prettify< - Singleton['resolve'] & MacroContext + Singleton['resolve'] & + // @ts-ignore + MacroContext['resolve'] > }, Definitions, { schema: Metadata['schema'] - standaloneSchema: UnwrapRoute< - LocalSchema, - Definitions['typebox'], - BasePath - > & - Metadata['standaloneSchema'] + standaloneSchema: Prettify< + SimplifyToSchema & + ({} extends PickIfExists< + Input, + keyof InputSchema + > + ? Metadata['standaloneSchema'] + : UnwrapRoute< + Input, + Definitions['typebox'], + BasePath + > & + Metadata['standaloneSchema']) + > macro: Metadata['macro'] macroFn: Metadata['macroFn'] parser: Metadata['parser'] + response: Prettify< + Metadata['response'] & + ElysiaHandlerToResponseSchemaAmbiguous & + ElysiaHandlerToResponseSchemaAmbiguous & + ElysiaHandlerToResponseSchemaAmbiguous & + // @ts-ignore + MacroContext['return'] + > }, Routes, Ephemeral, @@ -3394,136 +4377,58 @@ export default class Elysia< { derive: Ephemeral['derive'] resolve: Prettify< - Ephemeral['resolve'] & MacroContext + Ephemeral['resolve'] & + // @ts-ignore + MacroContext['resolve'] > schema: Ephemeral['schema'] - standaloneSchema: Ephemeral['standaloneSchema'] & - UnwrapRoute + standaloneSchema: Prettify< + SimplifyToSchema & + ({} extends PickIfExists< + Input, + keyof InputSchema + > + ? Ephemeral['standaloneSchema'] + : Ephemeral['standaloneSchema'] & + UnwrapRoute< + Input, + Definitions['typebox'] + >) + > + response: Prettify< + Ephemeral['response'] & + ElysiaHandlerToResponseSchemaAmbiguous & + ElysiaHandlerToResponseSchemaAmbiguous & + ElysiaHandlerToResponseSchemaAmbiguous & + // @ts-ignore + MacroContext['return'] + > }, Volatile > guard< - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, - const Schema extends MergeSchema< - UnwrapRoute, - Metadata['schema'] - >, - const Macro extends Metadata['macro'], - const MacroContext extends MacroToContext< - Metadata['macroFn'], - NoInfer - > - >( - hook: LocalHook< - LocalSchema, - Schema, - Singleton & { - derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & - Volatile['resolve'] & - MacroContext - }, - Definitions['error'], - Macro, - keyof Metadata['parser'] - > - ): Elysia< - BasePath, - Singleton, - Definitions, - Metadata, - Routes, - Ephemeral, - { - derive: Volatile['derive'] - resolve: Prettify - schema: Prettify< - MergeSchema< - UnwrapRoute, - MergeSchema< - Volatile['schema'], - MergeSchema - > - > - > - standaloneSchema: Metadata['standaloneSchema'] - } - > - - guard< - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, - const NewElysia extends AnyElysia, - const Schema extends MergeSchema< - UnwrapRoute, - Metadata['schema'] - >, - const Macro extends Metadata['macro'], - const MacroContext extends MacroToContext< - Metadata['macroFn'], - NoInfer - > - >( - run: ( - group: Elysia< - BasePath, - { - decorator: Singleton['decorator'] - store: Singleton['store'] - derive: Singleton['derive'] - resolve: Singleton['resolve'] & MacroContext - }, - Definitions, - { - schema: Prettify - standaloneSchema: Metadata['standaloneSchema'] - macro: Metadata['macro'] - macroFn: Metadata['macroFn'] - parser: Metadata['parser'] - }, - {}, - Ephemeral, - Volatile - > - ) => NewElysia - ): Elysia< - BasePath, - Singleton, - Definitions, - Metadata, - Prettify, - Ephemeral, - Volatile - > - - guard< - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, + const Input extends Metadata['macro'] & + InputSchema, const NewElysia extends AnyElysia, const Schema extends MergeSchema< - UnwrapRoute, + UnwrapRoute, Metadata['schema'] >, - const Macro extends Metadata['macro'], const MacroContext extends MacroToContext< Metadata['macroFn'], - NoInfer + NoInfer> > >( schema: LocalHook< - LocalSchema, - Schema, + Input, + // @ts-ignore + Schema & MacroContext, Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] resolve: Ephemeral['resolve'] & Volatile['resolve'] }, Definitions['error'], - Macro, keyof Metadata['parser'] >, run: ( @@ -3533,15 +4438,20 @@ export default class Elysia< decorator: Singleton['decorator'] store: Singleton['store'] derive: Singleton['derive'] - resolve: Prettify + resolve: Prettify< + Singleton['resolve'] & + // @ts-ignore + MacroContext['resolve'] + > }, Definitions, { - schema: Prettify + schema: Schema standaloneSchema: Metadata['standaloneSchema'] macro: Metadata['macro'] macroFn: Metadata['macroFn'] parser: Metadata['parser'] + response: Metadata['response'] }, {}, Ephemeral, @@ -3557,9 +4467,14 @@ export default class Elysia< Ephemeral, { derive: Volatile['derive'] - resolve: Prettify + resolve: Prettify< + Volatile['resolve'] & + // @ts-ignore + MacroContext['resolve'] + > schema: Volatile['schema'] standaloneSchema: Volatile['standaloneSchema'] + response: Volatile['response'] } > @@ -3574,12 +4489,10 @@ export default class Elysia< * * new Elysia() * .guard({ - * schema: { - * body: t.Object({ - * username: t.String(), - * password: t.String() - * }) - * } + * body: t.Object({ + * username: t.String(), + * password: t.String() + * }) * }, app => app * .get("/", () => 'Hi') * .get("/name", () => 'Elysia') @@ -3621,10 +4534,11 @@ export default class Elysia< if (!this.standaloneValidator[type]) this.standaloneValidator[type] = [] - const response = - hook?.response || - typeof hook?.response === 'string' || - (hook?.response && Kind in hook.response) + const response = !hook?.response + ? undefined + : typeof hook.response === 'string' || + Kind in hook.response || + '~standard' in hook.response ? { 200: hook.response } @@ -3705,7 +4619,17 @@ export default class Elysia< this.model(sandbox.definitions.type) Object.values(instance.router.history).forEach( - ({ method, path, handler, hooks: localHook }) => { + ({ method, path, handler, hooks }) => { + const { + body, + headers, + query, + params, + cookie, + response, + ...localHook + } = hooks + this.add( method, path, @@ -3722,7 +4646,15 @@ export default class Elysia< : [ localHook.error, ...(sandbox.event.error ?? []) - ] + ], + standaloneValidator: { + body, + headers, + query, + params, + cookie, + response + } }) ) } @@ -3797,7 +4729,7 @@ export default class Elysia< schema: Prettify< Ephemeral['schema'] & Partial > - standaloneSchema: PrettifySchema< + standaloneSchema: Prettify< Ephemeral['standaloneSchema'] & Partial > @@ -3808,12 +4740,15 @@ export default class Elysia< derive: Prettify< Ephemeral['derive'] & Partial > + response: Prettify< + Ephemeral['response'] & NewElysia['~Ephemeral']['response'] + > }, { schema: Prettify< Volatile['schema'] & Partial > - standaloneSchema: PrettifySchema< + standaloneSchema: Prettify< Volatile['standaloneSchema'] & Partial > @@ -3823,6 +4758,9 @@ export default class Elysia< derive: Prettify< Volatile['derive'] & Partial > + response: Prettify< + Volatile['response'] & NewElysia['~Volatile']['response'] + > } > @@ -3925,7 +4863,7 @@ export default class Elysia< Volatile['schema'] & Partial > - standaloneSchema: PrettifySchema< + standaloneSchema: Prettify< Volatile['standaloneSchema'] & Partial > @@ -3937,6 +4875,9 @@ export default class Elysia< Volatile['derive'] & Partial > + response: Prettify< + Volatile['response'] & LazyLoadElysia['~Ephemeral']['response'] + > }> > @@ -4094,16 +5035,14 @@ export default class Elysia< method, path, handler, - hooks, - standaloneValidators + hooks } of Object.values(plugin.router.history)) this.add( method, path, handler, hooks, - undefined, - standaloneValidators + undefined ) if (plugin === this) return @@ -4195,9 +5134,10 @@ export default class Elysia< ({ checksum }) => current === checksum ) ) { - this.extender.macros = this.extender.macros.concat( - plugin.extender.macros - ) + this.extender.macro = { + ...this.extender.macro, + ...plugin.extender.macro + } this.extender.higherOrderFunctions = this.extender.higherOrderFunctions.concat( @@ -4205,10 +5145,11 @@ export default class Elysia< ) } } else { - if (plugin.extender.macros.length) - this.extender.macros = this.extender.macros.concat( - plugin.extender.macros - ) + if (isNotEmpty(plugin.extender.macro)) + this.extender.macro = { + ...this.extender.macro, + ...plugin.extender.macro + } if (plugin.extender.higherOrderFunctions.length) this.extender.higherOrderFunctions = @@ -4217,9 +5158,6 @@ export default class Elysia< ) } - // ! Deduplicate current instance - deduplicateChecksum(this.extender.macros) - if (plugin.extender.higherOrderFunctions.length) { deduplicateChecksum(this.extender.higherOrderFunctions) @@ -4258,27 +5196,16 @@ export default class Elysia< if (isNotEmpty(plugin.definitions.error)) this.error(plugin.definitions.error as any) - if (isNotEmpty(plugin.definitions.error)) - plugin.extender.macros = this.extender.macros.concat( - plugin.extender.macros - ) + if (isNotEmpty(plugin.extender.macro)) + this.extender.macro = { + ...this.extender.macro, + ...plugin.extender.macro + } - for (const { - method, - path, - handler, - hooks, - standaloneValidators - } of Object.values(plugin.router.history)) { - this.add( - method, - path, - handler, - hooks, - undefined, - standaloneValidators - ) - } + for (const { method, path, handler, hooks } of Object.values( + plugin.router.history + )) + this.add(method, path, handler, hooks) if (name) { if (!(name in this.dependencies)) this.dependencies[name] = [] @@ -4355,23 +5282,38 @@ export default class Elysia< return this } - macro( - macro: ( - route: MacroManager< - MergeSchema< - Metadata['schema'], - MergeSchema - > & - Metadata['standaloneSchema'] & - Ephemeral['standaloneSchema'] & - Volatile['standaloneSchema'], + macro< + const Name extends string, + const Input extends Metadata['macro'] & + InputSchema, + const Schema extends MergeSchema< + UnwrapRoute, + MergeSchema< + Volatile['schema'], + MergeSchema + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'] + >, + const MacroContext extends MacroToContext< + Metadata['macroFn'], + Omit, + Definitions['typebox'] + >, + const Property extends MaybeValueOrVoidFunction< + MacroProperty< + Schema & MacroContext, Singleton & { derive: Partial resolve: Partial }, Definitions['error'] > - ) => NewMacro + > + >( + name: Name, + macro: (Input extends any ? Input : Prettify) & Property ): Elysia< BasePath, Singleton, @@ -4379,9 +5321,16 @@ export default class Elysia< { schema: Metadata['schema'] standaloneSchema: Metadata['standaloneSchema'] - macro: Metadata['macro'] & Partial> - macroFn: Metadata['macroFn'] & NewMacro + macro: Metadata['macro'] & { + [name in Name]?: Property extends (a: infer Params) => any + ? Params + : boolean + } + macroFn: Metadata['macroFn'] & { + [name in Name]: Property + } parser: Metadata['parser'] + response: Metadata['response'] }, Routes, Ephemeral, @@ -4389,7 +5338,10 @@ export default class Elysia< > macro< - const NewMacro extends HookMacroFn< + const Input extends Metadata['macro'] & + InputSchema, + const NewMacro extends Macro< + Input, Metadata['schema'], Singleton & { derive: Partial @@ -4409,53 +5361,119 @@ export default class Elysia< macro: Metadata['macro'] & Partial> macroFn: Metadata['macroFn'] & NewMacro parser: Metadata['parser'] + response: Metadata['response'] }, Routes, Ephemeral, Volatile > - macro(macro: Function | Record) { - if (typeof macro === 'function') { - const hook: MacroQueue = { - checksum: checksum( - JSON.stringify({ - name: this.config.name, - seed: this.config.seed, - content: macro.toString() - }) - ), - fn: macro as any + macro(macroOrName: string | Macro, macro?: Macro) { + if (typeof macroOrName === 'string' && !macro) + throw new Error('Macro function is required') + + if (typeof macroOrName === 'string') + this.extender.macro[macroOrName] = macro! + else + this.extender.macro = { + ...this.extender.macro, + ...macroOrName } - this.extender.macros.push(hook) - } else if (typeof macro === 'object') { - for (const name of Object.keys(macro)) - if (typeof macro[name] === 'object') { - const actualValue = { ...(macro[name] as Object) } + return this as any + } - macro[name] = (v: boolean) => { - if (v === true) return actualValue - } + private applyMacro( + localHook: AnyLocalHook, + appliable: AnyLocalHook = localHook, + { + iteration = 0, + applied = {} + }: { iteration?: number; applied?: { [key: number]: true } } = {} + ) { + if (iteration >= 16) return + const macro = this.extender.macro + + for (let [key, value] of Object.entries(appliable)) { + if (key in macro === false) continue + + const macroHook = + typeof macro[key] === 'function' + ? macro[key](value) + : macro[key] + + if ( + !macroHook || + (typeof macro[key] === 'object' && value === false) + ) + return + + const seed = checksum(key + JSON.stringify(macroHook.seed ?? value)) + if (seed in applied) continue + + applied[seed] = true + + for (let [k, value] of Object.entries(macroHook)) { + if (k === 'seed') continue + + if (k in emptySchema) { + insertStandaloneValidator( + localHook, + k as keyof RouteSchema, + value + ) + delete localHook[key] + continue } - const hook: MacroQueue = { - checksum: checksum( - JSON.stringify({ - name: this.config.name, - seed: this.config.seed, - content: Object.entries(macro) - .map(([k, v]) => `${k}+${v}`) - .join(',') - }) - ), - fn: () => macro - } + if (k === 'detail') { + if (!localHook.detail) localHook.detail = {} + localHook.detail = mergeDeep(localHook.detail, value) - this.extender.macros.push(hook) - } + delete localHook[key] + continue + } - return this as any + if (k in macro) { + this.applyMacro( + localHook, + { [k]: value }, + { applied, iteration: iteration + 1 } + ) + + delete localHook[key] + continue + } + + if ( + (k === 'derive' || k === 'resolve') && + typeof value === 'function' + ) + // @ts-ignore + value = { + fn: value, + subType: k + } as HookContainer + + switch (typeof localHook[k]) { + case 'function': + localHook[k] = [localHook[k], value] + break + + case 'object': + if (Array.isArray(localHook[k])) + (localHook[k] as any[]).push(value) + else localHook[k] = [localHook[k], value] + break + + case 'undefined': + localHook[k] = value + break + } + + delete localHook[key] + } + } } mount( @@ -4611,44 +5629,45 @@ export default class Elysia< */ get< const Path extends string, - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, + const Input extends Metadata['macro'] & + InputSchema, const Schema extends MergeSchema< UnwrapRoute< - LocalSchema, + Input, Definitions['typebox'], JoinPath >, MergeSchema< Volatile['schema'], MergeSchema - > - > & - Metadata['standaloneSchema'] & - Ephemeral['standaloneSchema'] & - Volatile['standaloneSchema'], - const Macro extends Metadata['macro'], + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'] + >, + const MacroContext extends MacroToContext< + Metadata['macroFn'], + Omit, + Definitions['typebox'] + >, const Decorator extends Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & - Volatile['resolve'] & - MacroToContext + resolve: Ephemeral['resolve'] & Volatile['resolve'] }, const Handle extends InlineHandler< NoInfer, Decorator, - JoinPath + // @ts-ignore + MacroContext > >( path: Path, handler: Handle, hook?: LocalHook< - LocalSchema, - Schema, - Decorator, + Input, + // @ts-ignore + Schema & MacroContext, Definitions['error'], - Macro, keyof Metadata['parser'] > ): Elysia< @@ -4660,15 +5679,30 @@ export default class Elysia< CreateEden< JoinPath, { - get: { - body: Schema['body'] - params: IsNever extends true - ? ResolvePath - : Schema['params'] - query: Schema['query'] - headers: Schema['headers'] - response: ComposeElysiaResponse - } + get: CreateEdenResponse< + Path, + Schema, + MacroContext, + ComposeElysiaResponse< + Schema & + MacroContext & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Handle, + UnionResponseStatus< + Metadata['response'], + UnionResponseStatus< + Ephemeral['response'], + UnionResponseStatus< + Volatile['response'], + // @ts-ignore + MacroContext['return'] & {} + > + > + > + > + > } >, Ephemeral, @@ -4697,12 +5731,11 @@ export default class Elysia< */ post< const Path extends string, - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, + const Input extends Metadata['macro'] & + InputSchema, const Schema extends MergeSchema< UnwrapRoute< - LocalSchema, + Input, Definitions['typebox'], JoinPath >, @@ -4714,27 +5747,30 @@ export default class Elysia< Metadata['standaloneSchema'] & Ephemeral['standaloneSchema'] & Volatile['standaloneSchema'], - const Macro extends Metadata['macro'], const Decorator extends Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & - Volatile['resolve'] & - MacroToContext + resolve: Ephemeral['resolve'] & Volatile['resolve'] }, + const MacroContext extends MacroToContext< + Metadata['macroFn'], + Omit, + Definitions['typebox'] + >, const Handle extends InlineHandler< NoInfer, - Decorator, - JoinPath + NoInfer, + // @ts-ignore + MacroContext > >( path: Path, handler: Handle, hook?: LocalHook< - LocalSchema, - Schema, + Input, + // @ts-ignore + Schema & MacroContext, Decorator, Definitions['error'], - Macro, keyof Metadata['parser'] > ): Elysia< @@ -4746,15 +5782,30 @@ export default class Elysia< CreateEden< JoinPath, { - post: { - body: Schema['body'] - params: IsNever extends true - ? ResolvePath - : Schema['params'] - query: Schema['query'] - headers: Schema['headers'] - response: ComposeElysiaResponse - } + post: CreateEdenResponse< + Path, + Schema, + MacroContext, + ComposeElysiaResponse< + Schema & + MacroContext & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Handle, + UnionResponseStatus< + Metadata['response'], + UnionResponseStatus< + Ephemeral['response'], + UnionResponseStatus< + Volatile['response'], + // @ts-ignore + MacroContext['return'] & {} + > + > + > + > + > } >, Ephemeral, @@ -4783,12 +5834,11 @@ export default class Elysia< */ put< const Path extends string, - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, + const Input extends Metadata['macro'] & + InputSchema, const Schema extends MergeSchema< UnwrapRoute< - LocalSchema, + Input, Definitions['typebox'], JoinPath >, @@ -4800,27 +5850,29 @@ export default class Elysia< Metadata['standaloneSchema'] & Ephemeral['standaloneSchema'] & Volatile['standaloneSchema'], - const Macro extends Metadata['macro'], const Decorator extends Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & - Volatile['resolve'] & - MacroToContext + resolve: Ephemeral['resolve'] & Volatile['resolve'] }, + const MacroContext extends MacroToContext< + Metadata['macroFn'], + Omit, + Definitions['typebox'] + >, const Handle extends InlineHandler< NoInfer, - Decorator, - JoinPath + NoInfer, + // @ts-ignore + MacroContext > >( path: Path, handler: Handle, hook?: LocalHook< - LocalSchema, - Schema, - Decorator, + Input, + // @ts-ignore + Schema & MacroContext, Definitions['error'], - Macro, keyof Metadata['parser'] > ): Elysia< @@ -4832,15 +5884,30 @@ export default class Elysia< CreateEden< JoinPath, { - put: { - body: Schema['body'] - params: IsNever extends true - ? ResolvePath - : Schema['params'] - query: Schema['query'] - headers: Schema['headers'] - response: ComposeElysiaResponse - } + put: CreateEdenResponse< + Path, + Schema, + MacroContext, + ComposeElysiaResponse< + Schema & + MacroContext & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Handle, + UnionResponseStatus< + Metadata['response'], + UnionResponseStatus< + Ephemeral['response'], + UnionResponseStatus< + Volatile['response'], + // @ts-ignore + MacroContext['return'] & {} + > + > + > + > + > } >, Ephemeral, @@ -4869,12 +5936,11 @@ export default class Elysia< */ patch< const Path extends string, - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, + const Input extends Metadata['macro'] & + InputSchema, const Schema extends MergeSchema< UnwrapRoute< - LocalSchema, + Input, Definitions['typebox'], JoinPath >, @@ -4886,27 +5952,29 @@ export default class Elysia< Metadata['standaloneSchema'] & Ephemeral['standaloneSchema'] & Volatile['standaloneSchema'], - const Macro extends Metadata['macro'], const Decorator extends Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & - Volatile['resolve'] & - MacroToContext + resolve: Ephemeral['resolve'] & Volatile['resolve'] }, + const MacroContext extends MacroToContext< + Metadata['macroFn'], + Omit, + Definitions['typebox'] + >, const Handle extends InlineHandler< NoInfer, - Decorator, - JoinPath + NoInfer, + // @ts-ignore + MacroContext > >( path: Path, handler: Handle, hook?: LocalHook< - LocalSchema, - Schema, - Decorator, + Input, + // @ts-ignore + Schema & MacroContext, Definitions['error'], - Macro, keyof Metadata['parser'] > ): Elysia< @@ -4918,15 +5986,30 @@ export default class Elysia< CreateEden< JoinPath, { - patch: { - body: Schema['body'] - params: IsNever extends true - ? ResolvePath - : Schema['params'] - query: Schema['query'] - headers: Schema['headers'] - response: ComposeElysiaResponse - } + patch: CreateEdenResponse< + Path, + Schema, + MacroContext, + ComposeElysiaResponse< + Schema & + MacroContext & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Handle, + UnionResponseStatus< + Metadata['response'], + UnionResponseStatus< + Ephemeral['response'], + UnionResponseStatus< + Volatile['response'], + // @ts-ignore + MacroContext['return'] & {} + > + > + > + > + > } >, Ephemeral, @@ -4955,12 +6038,11 @@ export default class Elysia< */ delete< const Path extends string, - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, + const Input extends Metadata['macro'] & + InputSchema, const Schema extends MergeSchema< UnwrapRoute< - LocalSchema, + Input, Definitions['typebox'], JoinPath >, @@ -4972,27 +6054,29 @@ export default class Elysia< Metadata['standaloneSchema'] & Ephemeral['standaloneSchema'] & Volatile['standaloneSchema'], - const Macro extends Metadata['macro'], const Decorator extends Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & - Volatile['resolve'] & - MacroToContext + resolve: Ephemeral['resolve'] & Volatile['resolve'] }, + const MacroContext extends MacroToContext< + Metadata['macroFn'], + Omit, + Definitions['typebox'] + >, const Handle extends InlineHandler< NoInfer, - Decorator, - JoinPath + NoInfer, + // @ts-ignore + MacroContext > >( path: Path, handler: Handle, hook?: LocalHook< - LocalSchema, - Schema, - Decorator, + Input, + // @ts-ignore + Schema & MacroContext, Definitions['error'], - Macro, keyof Metadata['parser'] > ): Elysia< @@ -5004,15 +6088,30 @@ export default class Elysia< CreateEden< JoinPath, { - delete: { - body: Schema['body'] - params: IsNever extends true - ? ResolvePath - : Schema['params'] - query: Schema['query'] - headers: Schema['headers'] - response: ComposeElysiaResponse - } + delete: CreateEdenResponse< + Path, + Schema, + MacroContext, + ComposeElysiaResponse< + Schema & + MacroContext & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Handle, + UnionResponseStatus< + Metadata['response'], + UnionResponseStatus< + Ephemeral['response'], + UnionResponseStatus< + Volatile['response'], + // @ts-ignore + MacroContext['return'] & {} + > + > + > + > + > } >, Ephemeral, @@ -5041,12 +6140,11 @@ export default class Elysia< */ options< const Path extends string, - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, + const Input extends Metadata['macro'] & + InputSchema, const Schema extends MergeSchema< UnwrapRoute< - LocalSchema, + Input, Definitions['typebox'], JoinPath >, @@ -5058,27 +6156,29 @@ export default class Elysia< Metadata['standaloneSchema'] & Ephemeral['standaloneSchema'] & Volatile['standaloneSchema'], - const Macro extends Metadata['macro'], const Decorator extends Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & - Volatile['resolve'] & - MacroToContext + resolve: Ephemeral['resolve'] & Volatile['resolve'] }, + const MacroContext extends MacroToContext< + Metadata['macroFn'], + Omit, + Definitions['typebox'] + >, const Handle extends InlineHandler< NoInfer, - Decorator, - JoinPath + NoInfer, + // @ts-ignore + MacroContext > >( path: Path, handler: Handle, hook?: LocalHook< - LocalSchema, - Schema, - Decorator, + Input, + // @ts-ignore + Schema & MacroContext, Definitions['error'], - Macro, keyof Metadata['parser'] > ): Elysia< @@ -5090,15 +6190,30 @@ export default class Elysia< CreateEden< JoinPath, { - options: { - body: Schema['body'] - params: IsNever extends true - ? ResolvePath - : Schema['params'] - query: Schema['query'] - headers: Schema['headers'] - response: ComposeElysiaResponse - } + options: CreateEdenResponse< + Path, + Schema, + MacroContext, + ComposeElysiaResponse< + Schema & + MacroContext & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Handle, + UnionResponseStatus< + Metadata['response'], + UnionResponseStatus< + Ephemeral['response'], + UnionResponseStatus< + Volatile['response'], + // @ts-ignore + MacroContext['return'] & {} + > + > + > + > + > } >, Ephemeral, @@ -5127,12 +6242,11 @@ export default class Elysia< */ all< const Path extends string, - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, + const Input extends Metadata['macro'] & + InputSchema, const Schema extends MergeSchema< UnwrapRoute< - LocalSchema, + Input, Definitions['typebox'], JoinPath >, @@ -5144,27 +6258,29 @@ export default class Elysia< Metadata['standaloneSchema'] & Ephemeral['standaloneSchema'] & Volatile['standaloneSchema'], - const Macro extends Metadata['macro'], const Decorator extends Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & - Volatile['resolve'] & - MacroToContext + resolve: Ephemeral['resolve'] & Volatile['resolve'] }, + const MacroContext extends MacroToContext< + Metadata['macroFn'], + Omit, + Definitions['typebox'] + >, const Handle extends InlineHandler< NoInfer, - Decorator, - JoinPath + NoInfer, + // @ts-ignore + MacroContext > >( path: Path, handler: Handle, hook?: LocalHook< - LocalSchema, - Schema, - Decorator, + Input, + // @ts-ignore + Schema & MacroContext, Definitions['error'], - Macro, keyof Metadata['parser'] > ): Elysia< @@ -5176,15 +6292,30 @@ export default class Elysia< CreateEden< JoinPath, { - [method in string]: { - body: Schema['body'] - params: IsNever extends true - ? ResolvePath - : Schema['params'] - query: Schema['query'] - headers: Schema['headers'] - response: ComposeElysiaResponse - } + [method in string]: CreateEdenResponse< + Path, + Schema, + MacroContext, + ComposeElysiaResponse< + Schema & + MacroContext & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Handle, + UnionResponseStatus< + Metadata['response'], + UnionResponseStatus< + Ephemeral['response'], + UnionResponseStatus< + Volatile['response'], + // @ts-ignore + MacroContext['return'] & {} + > + > + > + > + > } >, Ephemeral, @@ -5213,12 +6344,11 @@ export default class Elysia< */ head< const Path extends string, - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, + const Input extends Metadata['macro'] & + InputSchema, const Schema extends MergeSchema< UnwrapRoute< - LocalSchema, + Input, Definitions['typebox'], JoinPath >, @@ -5230,27 +6360,29 @@ export default class Elysia< Metadata['standaloneSchema'] & Ephemeral['standaloneSchema'] & Volatile['standaloneSchema'], - const Macro extends Metadata['macro'], const Decorator extends Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & - Volatile['resolve'] & - MacroToContext + resolve: Ephemeral['resolve'] & Volatile['resolve'] }, + const MacroContext extends MacroToContext< + Metadata['macroFn'], + Omit, + Definitions['typebox'] + >, const Handle extends InlineHandler< NoInfer, - Decorator, - JoinPath + NoInfer, + // @ts-ignore + MacroContext > >( path: Path, handler: Handle, hook?: LocalHook< - LocalSchema, - Schema, - Decorator, + Input, + // @ts-ignore + Schema & MacroContext, Definitions['error'], - Macro, keyof Metadata['parser'] > ): Elysia< @@ -5262,15 +6394,30 @@ export default class Elysia< CreateEden< JoinPath, { - head: { - body: Schema['body'] - params: IsNever extends true - ? ResolvePath - : Schema['params'] - query: Schema['query'] - headers: Schema['headers'] - response: ComposeElysiaResponse - } + head: CreateEdenResponse< + Path, + Schema, + MacroContext, + ComposeElysiaResponse< + Schema & + MacroContext & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Handle, + UnionResponseStatus< + Metadata['response'], + UnionResponseStatus< + Ephemeral['response'], + UnionResponseStatus< + Volatile['response'], + // @ts-ignore + MacroContext['return'] & {} + > + > + > + > + > } >, Ephemeral, @@ -5299,12 +6446,11 @@ export default class Elysia< */ connect< const Path extends string, - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, + const Input extends Metadata['macro'] & + InputSchema, const Schema extends MergeSchema< UnwrapRoute< - LocalSchema, + Input, Definitions['typebox'], JoinPath >, @@ -5316,27 +6462,29 @@ export default class Elysia< Metadata['standaloneSchema'] & Ephemeral['standaloneSchema'] & Volatile['standaloneSchema'], - const Macro extends Metadata['macro'], const Decorator extends Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & - Volatile['resolve'] & - MacroToContext + resolve: Ephemeral['resolve'] & Volatile['resolve'] }, + const MacroContext extends MacroToContext< + Metadata['macroFn'], + Omit, + Definitions['typebox'] + >, const Handle extends InlineHandler< NoInfer, - Decorator, - JoinPath + NoInfer, + // @ts-ignore + MacroContext > >( path: Path, handler: Handle, hook?: LocalHook< - LocalSchema, - Schema, - Decorator, + Input, + // @ts-ignore + Schema & MacroContext, Definitions['error'], - Macro, keyof Metadata['parser'] > ): Elysia< @@ -5348,15 +6496,30 @@ export default class Elysia< CreateEden< JoinPath, { - connect: { - body: Schema['body'] - params: IsNever extends true - ? ResolvePath - : Schema['params'] - query: Schema['query'] - headers: Schema['headers'] - response: ComposeElysiaResponse - } + connect: CreateEdenResponse< + Path, + Schema, + MacroContext, + ComposeElysiaResponse< + Schema & + MacroContext & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Handle, + UnionResponseStatus< + Metadata['response'], + UnionResponseStatus< + Ephemeral['response'], + UnionResponseStatus< + Volatile['response'], + // @ts-ignore + MacroContext['return'] & {} + > + > + > + > + > } >, Ephemeral, @@ -5386,12 +6549,11 @@ export default class Elysia< route< const Method extends HTTPMethod, const Path extends string, - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, + const Input extends Metadata['macro'] & + InputSchema, const Schema extends MergeSchema< UnwrapRoute< - LocalSchema, + Input, Definitions['typebox'], JoinPath >, @@ -5403,28 +6565,30 @@ export default class Elysia< Metadata['standaloneSchema'] & Ephemeral['standaloneSchema'] & Volatile['standaloneSchema'], - const Macro extends Metadata['macro'], const Decorator extends Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] - resolve: Ephemeral['resolve'] & - Volatile['resolve'] & - MacroToContext + resolve: Ephemeral['resolve'] & Volatile['resolve'] }, + const MacroContext extends MacroToContext< + Metadata['macroFn'], + Omit, + Definitions['typebox'] + >, const Handle extends InlineHandler< NoInfer, - Decorator, - JoinPath + NoInfer, + // @ts-ignore + MacroContext > >( method: Method, path: Path, handler: Handle, hook?: LocalHook< - LocalSchema, - Schema, - Decorator, + Input, + // @ts-ignore + Schema & MacroContext, Definitions['error'], - Macro, keyof Metadata['parser'] > & { config?: { @@ -5441,15 +6605,30 @@ export default class Elysia< CreateEden< JoinPath, { - [method in Method]: { - body: Schema['body'] - params: IsNever extends true - ? ResolvePath - : Schema['params'] - query: Schema['query'] - headers: Schema['headers'] - response: ComposeElysiaResponse - } + [method in Method]: CreateEdenResponse< + Path, + Schema, + MacroContext, + ComposeElysiaResponse< + Schema & + MacroContext & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + Handle, + UnionResponseStatus< + Metadata['response'], + UnionResponseStatus< + Ephemeral['response'], + UnionResponseStatus< + Volatile['response'], + // @ts-ignore + MacroContext['return'] & {} + > + > + > + > + > } >, Ephemeral, @@ -5479,12 +6658,11 @@ export default class Elysia< */ ws< const Path extends string, - const LocalSchema extends InputSchema< - keyof Definitions['typebox'] & string - >, + const Input extends Metadata['macro'] & + InputSchema, const Schema extends MergeSchema< UnwrapRoute< - LocalSchema, + Input, Definitions['typebox'], JoinPath >, @@ -5492,20 +6670,26 @@ export default class Elysia< Volatile['schema'], MergeSchema > - >, - const Macro extends Metadata['macro'] + > & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + const MacroContext extends MacroToContext< + Metadata['macroFn'], + Omit + > >( path: Path, options: WSLocalHook< - LocalSchema, + Input, Schema, Singleton & { derive: Ephemeral['derive'] & Volatile['derive'] resolve: Ephemeral['resolve'] & Volatile['resolve'] & - MacroToContext - }, - Macro + // @ts-ignore + MacroContext['resolve'] + } > ): Elysia< BasePath, @@ -5516,19 +6700,34 @@ export default class Elysia< CreateEden< JoinPath, { - subscribe: { - body: Schema['body'] - params: IsNever extends true - ? ResolvePath - : Schema['params'] - query: Schema['query'] - headers: Schema['headers'] - response: {} extends Schema['response'] - ? unknown - : Schema['response'] extends { [200]: any } - ? Schema['response'][200] - : unknown - } + subscribe: CreateEdenResponse< + Path, + Schema, + MacroContext, + ComposeElysiaResponse< + Schema & + MacroContext & + Metadata['standaloneSchema'] & + Ephemeral['standaloneSchema'] & + Volatile['standaloneSchema'], + {} extends Schema['response'] + ? unknown + : Schema['response'] extends { [200]: any } + ? Schema['response'][200] + : unknown, + UnionResponseStatus< + Metadata['response'], + UnionResponseStatus< + Ephemeral['response'], + UnionResponseStatus< + Volatile['response'], + // @ts-ignore + MacroContext['return'] & {} + > + > + > + > + > } >, Ephemeral, @@ -6119,6 +7318,10 @@ export default class Elysia< resolve: Volatile['resolve'] schema: Volatile['schema'] standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ExtractErrorFromHandle + > } > @@ -6144,7 +7347,7 @@ export default class Elysia< | void, const Type extends LifeCycleType >( - options: { as?: Type }, + options: { as: Type }, transform: ( context: Prettify< Context< @@ -6201,7 +7404,17 @@ export default class Elysia< resolve: Singleton['resolve'] }, Definitions, - Metadata, + { + schema: Metadata['schema'] + standaloneSchema: Metadata['standaloneSchema'] + macro: Metadata['macro'] + macroFn: Metadata['macroFn'] + parser: Metadata['parser'] + response: UnionResponseStatus< + Metadata['response'], + ExtractErrorFromHandle + > + }, Routes, Ephemeral, Volatile @@ -6221,6 +7434,10 @@ export default class Elysia< resolve: Ephemeral['resolve'] schema: Ephemeral['schema'] standaloneSchema: Ephemeral['standaloneSchema'] + response: UnionResponseStatus< + Ephemeral['response'], + ExtractErrorFromHandle + > }, Volatile > @@ -6239,11 +7456,15 @@ export default class Elysia< resolve: Ephemeral['resolve'] schema: Volatile['schema'] standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ExtractErrorFromHandle + > } > derive( - optionsOrTransform: { as?: LifeCycleType } | Function, + optionsOrTransform: { as: LifeCycleType } | Function, transform?: Function ) { if (!transform) { @@ -6259,7 +7480,10 @@ export default class Elysia< return this.onTransform(optionsOrTransform as any, hook as any) as any } - model( + model< + const Name extends string, + const Model extends TSchema | StandardSchemaV1Like + >( name: Name, model: Model ): Elysia< @@ -6277,7 +7501,9 @@ export default class Elysia< Volatile > - model( + model< + const Recorder extends Record + >( record: Recorder ): Elysia< BasePath, @@ -6292,13 +7518,13 @@ export default class Elysia< Volatile > - model>( + model>( mapper: ( - decorators: Definitions['typebox'] extends infer Models extends - Record + decorators: Definitions['typebox'] extends infer Models ? { - [type in keyof Models]: TRef + [Name in keyof Models]: Models[Name] extends TSchema + ? TRef + : Models[Name] } : {} ) => NewType @@ -6307,12 +7533,13 @@ export default class Elysia< Singleton, { typebox: { - [key in keyof NewType]: NewType[key] extends TRef - ? // @ts-expect-error - Definitions['typebox'][key] - : NewType[key] + [Name in keyof NewType]: NewType[Name] extends TRef< + Name & string + > + ? // @ts-ignore + Definitions['typebox'][Name] + : NewType[Name] } - type: { [x in keyof NewType]: Static } error: Definitions['error'] }, Metadata, @@ -6321,10 +7548,29 @@ export default class Elysia< Volatile > - model(name: string | Record | Function, model?: TSchema) { + model( + name: + | string + | Record + | Function, + model?: TAnySchema | StandardSchemaV1Like + ): AnyElysia { + const onlyTypebox = < + A extends Record + >( + a: A + ): Extract => { + const res = {} as Record + for (const key in a) if (!('~standard' in a[key])) res[key] = a[key] + return res as Extract + } + switch (typeof name) { case 'object': - const parsedSchemas = {} as Record + const parsedTypebox = {} as Record< + string, + TSchema | StandardSchemaV1Like + > const kvs = Object.entries(name) @@ -6333,14 +7579,18 @@ export default class Elysia< for (const [key, value] of kvs) { if (key in this.definitions.type) continue - parsedSchemas[key] = this.definitions.type[key] = value - parsedSchemas[key].$id ??= `#/components/schemas/${key}` + if ('~standard' in value) { + this.definitions.type[key] = value + } else { + parsedTypebox[key] = this.definitions.type[key] = value + parsedTypebox[key].$id ??= `#/components/schemas/${key}` + } } // @ts-expect-error this.definitions.typebox = t.Module({ ...(this.definitions.typebox['$defs'] as TModule<{}>), - ...parsedSchemas + ...parsedTypebox } as any) return this @@ -6348,36 +7598,46 @@ export default class Elysia< case 'function': const result = name(this.definitions.type) this.definitions.type = result - this.definitions.typebox = t.Module(result as any) + this.definitions.typebox = t.Module(onlyTypebox(result)) - return this as any + return this case 'string': if (!model) break + this.definitions.type[name] = model + + if ('~standard' in model) return this + const newModel = { ...model, id: model.$id ?? `#/components/schemas/${name}` } - this.definitions.type[name] = model this.definitions.typebox = t.Module({ ...(this.definitions.typebox['$defs'] as TModule<{}>), ...newModel } as any) - return this as any + + return this } - ;(this.definitions.type as Record)[name] = model! + if (!model) return this + + this.definitions.type[name] = model! + if ('~standard' in model) return this + this.definitions.typebox = t.Module({ ...this.definitions.typebox['$defs'], [name]: model! - } as any) + }) - return this as any + return this } - Ref(key: K) { + Ref & string>( + key: K + ) { return t.Ref(key) } @@ -6408,6 +7668,10 @@ export default class Elysia< resolve: Volatile['resolve'] schema: Volatile['schema'] standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ExtractErrorFromHandle + > } > @@ -6417,7 +7681,7 @@ export default class Elysia< | ElysiaCustomStatusResponse, const Type extends LifeCycleType >( - options: { as?: Type }, + options: { as: Type }, mapper: ( context: Context< {}, @@ -6453,14 +7717,24 @@ export default class Elysia< { decorator: Singleton['decorator'] store: Singleton['store'] - derive: Singleton['derive'] - resolve: Prettify< - Singleton['resolve'] & + derive: Prettify< + Singleton['derive'] & ExcludeElysiaResponse > + resolve: Singleton['resolve'] }, Definitions, - Metadata, + { + schema: Metadata['schema'] + standaloneSchema: Metadata['standaloneSchema'] + macro: Metadata['macro'] + macroFn: Metadata['macroFn'] + parser: Metadata['parser'] + response: UnionResponseStatus< + Metadata['response'], + ExtractErrorFromHandle + > + }, Routes, Ephemeral, Volatile @@ -6473,13 +7747,17 @@ export default class Elysia< Metadata, Routes, { - derive: Ephemeral['derive'] - resolve: Prettify< - Ephemeral['resolve'] & + derive: Prettify< + Ephemeral['derive'] & ExcludeElysiaResponse > + resolve: Ephemeral['resolve'] schema: Ephemeral['schema'] standaloneSchema: Ephemeral['standaloneSchema'] + response: UnionResponseStatus< + Ephemeral['response'], + ExtractErrorFromHandle + > }, Volatile > @@ -6498,11 +7776,15 @@ export default class Elysia< > schema: Volatile['schema'] standaloneSchema: Volatile['standaloneSchema'] + response: UnionResponseStatus< + Volatile['response'], + ExtractErrorFromHandle + > } > mapDerive( - optionsOrDerive: { as?: LifeCycleType } | Function, + optionsOrDerive: { as: LifeCycleType } | Function, mapper?: Function ) { if (!mapper) { @@ -6800,7 +8082,7 @@ export default class Elysia< export { Elysia } export { t } from './type-system' -export { validationDetail } from './type-system/utils' +export { validationDetail, fileType } from './type-system/utils' export type { ElysiaTypeCustomError, ElysiaTypeCustomErrorCallback @@ -6841,7 +8123,6 @@ export { export { status, - error, mapValueError, ParseError, NotFoundError, @@ -6887,12 +8168,9 @@ export type { InferHandler, ResolvePath, MapResponse, - MacroQueue, BaseMacro, MacroManager, - BaseMacroFn, MacroToProperty, - ResolveMacroContext, MergeElysiaInstances, MaybeArray, ModelValidator, @@ -6910,7 +8188,8 @@ export type { InlineHandler, ResolveHandler, TransformHandler, - HTTPHeaders + HTTPHeaders, + EmptyRouteSchema } from './types' export { env } from './universal/env' diff --git a/src/parse-query.ts b/src/parse-query.ts index 6fe91de44..705187be0 100644 --- a/src/parse-query.ts +++ b/src/parse-query.ts @@ -1,31 +1,33 @@ import decode from 'fast-decode-uri-component' +// bit flags +const KEY_HAS_PLUS = 1 +const KEY_NEEDS_DECODE = 2 +const VALUE_HAS_PLUS = 4 +const VALUE_NEEDS_DECODE = 8 + // Parse query without array export function parseQueryFromURL( input: string, - startIndex: number = 0 + startIndex: number = 0, + array?: { [key: string]: 1 }, + object?: { [key: string]: 1 } ): Record { const result = Object.create(null) - // bit flags - const KEY_PLUS_FLAG = 1 - const KEY_DECODE_FLAG = 2 - const VALUE_PLUS_FLAG = 4 - const VALUE_DECODE_FLAG = 8 - let flags = 0 + + const inputLength = input.length let startingIndex = startIndex - 1 let equalityIndex = startingIndex - const inputLength = input.length - // Main parsing loop - for (let i = startIndex; i < inputLength; i++) + for (let i = 0; i < inputLength; i++) switch (input.charCodeAt(i)) { // '&' case 38: - processKeyValuePair(i) + processKeyValuePair(input, i) - // Reset for next pair + // Reset state variables startingIndex = i equalityIndex = i flags = 0 @@ -35,56 +37,204 @@ export function parseQueryFromURL( // '=' case 61: if (equalityIndex <= startingIndex) equalityIndex = i - else flags |= VALUE_DECODE_FLAG + // If '=' character occurs again, we should decode the input + else flags |= VALUE_NEEDS_DECODE break // '+' case 43: - if (equalityIndex > startingIndex) flags |= VALUE_PLUS_FLAG - else flags |= KEY_PLUS_FLAG + if (equalityIndex > startingIndex) flags |= VALUE_HAS_PLUS + else flags |= KEY_HAS_PLUS break // '%' case 37: - if (equalityIndex > startingIndex) flags |= VALUE_DECODE_FLAG - else flags |= KEY_DECODE_FLAG + if (equalityIndex > startingIndex) flags |= VALUE_NEEDS_DECODE + else flags |= KEY_NEEDS_DECODE break } - // Process the last pair if there is one - processKeyValuePair(inputLength) + // Process the last pair if needed + if (startingIndex < inputLength) processKeyValuePair(input, inputLength) return result - function processKeyValuePair(endIndex: number) { + function processKeyValuePair(input: string, endIndex: number) { const hasBothKeyValuePair = equalityIndex > startingIndex - const keyEndIndex = hasBothKeyValuePair ? equalityIndex : endIndex + const effectiveEqualityIndex = hasBothKeyValuePair + ? equalityIndex + : endIndex - // Extract and process key only if the slice is not empty - if (keyEndIndex <= startingIndex + 1) return + const keySlice = input.slice(startingIndex + 1, effectiveEqualityIndex) - let keySlice = input.slice(startingIndex + 1, keyEndIndex) - if (flags & KEY_PLUS_FLAG) keySlice = keySlice.replace(/\+/g, ' ') - if (flags & KEY_DECODE_FLAG) keySlice = decode(keySlice) || keySlice + // Skip processing if key is empty + if (!hasBothKeyValuePair && keySlice.length === 0) return - // Only add to result if this key doesn't already exist - if (result[keySlice] !== undefined) return + let finalKey = keySlice + if (flags & KEY_HAS_PLUS) finalKey = finalKey.replace(/\+/g, ' ') + if (flags & KEY_NEEDS_DECODE) finalKey = decode(finalKey) || finalKey - // Process value if it exists let finalValue = '' if (hasBothKeyValuePair) { - finalValue = input.slice(equalityIndex + 1, endIndex) + let valueSlice = input.slice(equalityIndex + 1, endIndex) + if (flags & VALUE_HAS_PLUS) + valueSlice = valueSlice.replace(/\+/g, ' ') + if (flags & VALUE_NEEDS_DECODE) + valueSlice = decode(valueSlice) || valueSlice + finalValue = valueSlice + } + + const currentValue = result[finalKey] - if (flags & VALUE_PLUS_FLAG) - finalValue = finalValue.replace(/\+/g, ' ') - if (flags & VALUE_DECODE_FLAG) - finalValue = decode(finalValue) || finalValue + if (array?.[finalKey]) { + if (finalValue.charCodeAt(0) === 91) { + if (object?.[finalKey]) + finalValue = JSON.parse(finalValue) as any + else finalValue = finalValue.slice(1, -1).split(',') as any + + if (currentValue === undefined) result[finalKey] = finalValue + else if (Array.isArray(currentValue)) + currentValue.push(...finalValue) + else { + result[finalKey] = finalValue + result[finalKey].unshift(currentValue) + } + } else { + if (currentValue === undefined) result[finalKey] = finalValue + else if (Array.isArray(currentValue)) + currentValue.push(finalValue) + else result[finalKey] = [currentValue, finalValue] + } + } else { + result[finalKey] = finalValue } + } +} + +/** + * @callback parse + * @param {string} input + */ +export function parseQueryStandardSchema( + input: string, + startIndex: number = 0 +) { + const result = Object.create(null) as Record - result[keySlice] = finalValue + let flags = 0 + + const inputLength = input.length + let startingIndex = startIndex - 1 + let equalityIndex = startingIndex + + for (let i = 0; i < inputLength; i++) + switch (input.charCodeAt(i)) { + // '&' + case 38: + processKeyValuePair(input, i) + + // Reset state variables + startingIndex = i + equalityIndex = i + flags = 0 + + break + + // '=' + case 61: + if (equalityIndex <= startingIndex) equalityIndex = i + // If '=' character occurs again, we should decode the input + else flags |= VALUE_NEEDS_DECODE + + break + + // '+' + case 43: + if (equalityIndex > startingIndex) flags |= VALUE_HAS_PLUS + else flags |= KEY_HAS_PLUS + + break + + // '%' + case 37: + if (equalityIndex > startingIndex) flags |= VALUE_NEEDS_DECODE + else flags |= KEY_NEEDS_DECODE + + break + } + + // Process the last pair if needed + if (startingIndex < inputLength) processKeyValuePair(input, inputLength) + + return result + + function processKeyValuePair(input: string, endIndex: number) { + const hasBothKeyValuePair = equalityIndex > startingIndex + const effectiveEqualityIndex = hasBothKeyValuePair + ? equalityIndex + : endIndex + + const keySlice = input.slice(startingIndex + 1, effectiveEqualityIndex) + + // Skip processing if key is empty + if (!hasBothKeyValuePair && keySlice.length === 0) return + + let finalKey = keySlice + if (flags & KEY_HAS_PLUS) finalKey = finalKey.replace(/\+/g, ' ') + if (flags & KEY_NEEDS_DECODE) finalKey = decode(finalKey) || finalKey + + let finalValue = '' + if (hasBothKeyValuePair) { + let valueSlice = input.slice(equalityIndex + 1, endIndex) + if (flags & VALUE_HAS_PLUS) + valueSlice = valueSlice.replace(/\+/g, ' ') + if (flags & VALUE_NEEDS_DECODE) + valueSlice = decode(valueSlice) || valueSlice + finalValue = valueSlice + } + + const currentValue = result[finalKey] + + if ( + finalValue.charCodeAt(0) === 91 && + finalValue.charCodeAt(finalValue.length - 1) === 93 + ) { + try { + // @ts-ignore + finalValue = JSON.parse(finalValue) + } catch { + // If JSON parsing fails, treat it as a regular string + } + + if (currentValue === undefined) result[finalKey] = finalValue + else if (Array.isArray(currentValue)) currentValue.push(finalValue) + else result[finalKey] = [currentValue, finalValue] + } else if ( + finalValue.charCodeAt(0) === 123 && + finalValue.charCodeAt(finalValue.length - 1) === 125 + ) { + try { + // @ts-ignore + finalValue = JSON.parse(finalValue) + } catch { + // If JSON parsing fails, treat it as a regular string + } + + if (currentValue === undefined) result[finalKey] = finalValue + else if (Array.isArray(currentValue)) currentValue.push(finalValue) + else result[finalKey] = [currentValue, finalValue] + } else { + if (finalValue.includes(',')) + // @ts-ignore + finalValue = finalValue.split(',') + + if (currentValue === undefined) result[finalKey] = finalValue + else if (Array.isArray(currentValue)) currentValue.push(finalValue) + else result[finalKey] = [currentValue, finalValue] + } } } @@ -95,12 +245,7 @@ export function parseQueryFromURL( export function parseQuery(input: string) { const result = Object.create(null) as Record - // bit flags let flags = 0 - const KEY_HAS_PLUS = 1 - const KEY_NEEDS_DECODE = 2 - const VALUE_HAS_PLUS = 4 - const VALUE_NEEDS_DECODE = 8 const inputLength = input.length let startingIndex = -1 diff --git a/src/schema.ts b/src/schema.ts index 59088062d..2ed452c04 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -18,7 +18,7 @@ import { import { t, type TypeCheck } from './type-system' -import { mergeCookie, randomId } from './utils' +import { mergeCookie, mergeDeep, randomId } from './utils' import { mapValueError } from './error' import type { CookieOptions } from './cookies' @@ -26,13 +26,17 @@ import type { ElysiaConfig, InputSchema, MaybeArray, - StandaloneInputSchema + StandaloneInputSchema, + StandardSchemaV1LikeValidate } from './types' +import type { StandardSchemaV1Like } from './types' + type MapValueError = ReturnType export interface ElysiaTypeCheck extends Omit, 'schema'> { + provider: 'typebox' | 'standard' schema: T config: Object Clean?(v: unknown): T @@ -150,6 +154,59 @@ export const hasType = (type: string, schema: TAnySchema) => { ) } +export const hasElysiaMeta = (meta: string, _schema: TAnySchema): boolean => { + if (!_schema) return false + + // @ts-expect-error private property + const schema: TAnySchema = (_schema as TypeCheck)?.schema ?? _schema + + if (schema.elysiaMeta === meta) return true + + if (schema[Kind] === 'Import' && _schema.References) + return _schema + .References() + .some((schema: TSchema) => hasElysiaMeta(meta, schema)) + + if (schema.anyOf) + return schema.anyOf.some((schema: TSchema) => + hasElysiaMeta(meta, schema) + ) + if (schema.someOf) + return schema.someOf.some((schema: TSchema) => + hasElysiaMeta(meta, schema) + ) + if (schema.allOf) + return schema.allOf.some((schema: TSchema) => + hasElysiaMeta(meta, schema) + ) + if (schema.not) + return schema.not.some((schema: TSchema) => hasElysiaMeta(meta, schema)) + + 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 (hasElysiaMeta(meta, property)) return true + } else if (property.anyOf) { + for (let i = 0; i < property.anyOf.length; i++) + if (hasElysiaMeta(meta, property.anyOf[i])) return true + } + + return schema.elysiaMeta === meta + } + + return false + } + + if (schema.type === 'array' && schema.items && !Array.isArray(schema.items)) + return hasElysiaMeta(meta, schema.items) + + return false +} + export const hasProperty = ( expectedProperty: string, _schema: TAnySchema | TypeCheck | ElysiaTypeCheck @@ -483,9 +540,9 @@ const _replaceSchemaType = ( v.default === '{}' ) { transform = t.ObjectString(properties, rest) - value.default = JSON.stringify( - Value.Create(t.Object(properties)) - ) + // value.default = JSON.stringify( + // Value.Create(t.Object(properties)) + // ) value.properties = properties } @@ -497,7 +554,7 @@ const _replaceSchemaType = ( v.default === '[]' ) { transform = t.ArrayString(items, rest) - value.default = JSON.stringify(Value.Create(t.Array(items))) + // value.default = JSON.stringify(Value.Create(t.Array(items))) value.items = items } @@ -686,7 +743,9 @@ const createCleaner = (schema: TAnySchema) => (value: unknown) => { // const caches = >>{} -export const getSchemaValidator = ( +export const getSchemaValidator = < + T extends TSchema | StandardSchemaV1Like | string | undefined +>( s: T, { models = {}, @@ -694,14 +753,16 @@ export const getSchemaValidator = ( modules, normalize = false, additionalProperties = false, + forceAdditionalProperties = false, coerce = false, additionalCoerce = [], validators, sanitize }: { - models?: Record + models?: Record modules?: TModule additionalProperties?: boolean + forceAdditionalProperties?: boolean dynamic?: boolean normalize?: ElysiaConfig<''>['normalize'] coerce?: boolean @@ -742,30 +803,33 @@ export const getSchemaValidator = ( return replaceSchemaType(schema, additionalCoerce) } - const mapSchema = (s: string | TSchema | undefined): TSchema => { - let schema: TSchema + const mapSchema = ( + s: string | TSchema | StandardSchemaV1Like | undefined + ): TSchema | StandardSchemaV1Like => { + if (s && typeof s !== 'string' && '~standard' in s) + return s as StandardSchemaV1Like if (!s) return undefined as any + let schema: TSchema | StandardSchemaV1Like + if (typeof s !== 'string') schema = s else { - // if (s in caches) return caches[s] as any - - const isArray = s.endsWith('[]') - const key = isArray ? s.substring(0, s.length - 2) : s - schema = - (modules as TModule<{}, {}> | undefined)?.Import( - key as never - ) ?? models[key] + // @ts-expect-error private property + modules && s in modules.$defs + ? (modules as TModule<{}, {}>).Import(s as never) + : models[s] - if (isArray) schema = t.Array(schema) + if (!schema) return undefined as any } - if (!schema) return undefined as any - let _doesHaveRef: boolean - if (schema[Kind] !== 'Import' && (_doesHaveRef = hasRef(schema))) { + if ( + Kind in schema && + schema[Kind] !== 'Import' && + (_doesHaveRef = hasRef(schema)) + ) { const id = randomId() if (doesHaveRef === undefined) doesHaveRef = _doesHaveRef @@ -779,34 +843,196 @@ export const getSchemaValidator = ( schema = model.Import(id) } - if (schema[Kind] === 'Import') { - const newDefs: Record = {} + if (Kind in schema) { + if (schema[Kind] === 'Import') { + const newDefs: Record = {} - for (const [key, value] of Object.entries(schema.$defs)) - newDefs[key] = replaceSchema(value as TSchema) + for (const [key, value] of Object.entries(schema.$defs)) + newDefs[key] = replaceSchema(value as TSchema) - const key = schema.$ref - schema = t.Module(newDefs).Import(key) - } else if (coerce || additionalCoerce) schema = replaceSchema(schema) + const key = schema.$ref + schema = t.Module(newDefs).Import(key) + } else if (coerce || additionalCoerce) + schema = replaceSchema(schema) + } return schema } let schema = mapSchema(s) + let _validators = validators + + if ( + '~standard' in schema || + (validators?.length && + validators.some( + (x) => x && typeof x !== 'string' && '~standard' in x + )) + ) { + const typeboxSubValidator = ( + schema: TSchema + ): StandardSchemaV1LikeValidate => { + let mirror: Function + if (normalize === true || normalize === 'exactMirror') + try { + mirror = createMirror(schema as TSchema, { + TypeCompiler, + sanitize: sanitize?.(), + modules + }) + } catch { + console.warn( + 'Failed to create exactMirror. Please report the following code to https://github.com/elysiajs/elysia/issues' + ) + console.warn(schema) + mirror = createCleaner(schema as TSchema) + } + + const vali = getSchemaValidator(schema, { + models, + modules, + dynamic, + normalize, + additionalProperties: true, + forceAdditionalProperties: true, + coerce, + additionalCoerce + })! + + // @ts-ignore + vali.Decode = mirror + + // @ts-ignore + return (v) => { + if (vali.Check(v)) { + return { + value: vali.Decode(v) + } + } else + return { + issues: [...vali.Errors(v)] + } + } + } + + const mainCheck = schema['~standard'] + ? schema['~standard'].validate + : typeboxSubValidator(schema as TSchema) + + let checkers = [] + if (validators?.length) + for (const validator of validators) { + if (!validator) continue + if (typeof validator === 'string') continue + + if (validator?.['~standard']) { + checkers.push(validator['~standard']) + continue + } + + if (Kind in validator) { + checkers.push(typeboxSubValidator(validator)) + continue + } + } + + async function Check(value: unknown) { + let v = mainCheck(value) + if (v instanceof Promise) v = await v + if (v.issues) return v - if (validators?.length) { + const values = <(Record | unknown[])[]>[] + + if (v && typeof v === 'object') values.push(v.value as any) + + for (let i = 0; i < checkers.length; i++) { + // @ts-ignore + v = checkers[i].validate(value) + if (v instanceof Promise) v = await v + if (v.issues) return v + + // @ts-ignore + if (v && typeof v === 'object') values.push(v.value) + } + + if (!values.length) return { value: v } + if (values.length === 1) return { value: values[0] } + if (values.length === 2) + return { value: mergeDeep(values[0], values[1]) } + + let newValue = mergeDeep(values[0], values[1]) + + for (let i = 2; i < values.length; i++) + newValue = mergeDeep(newValue, values[i]) + + return { value: newValue } + } + + const validator: ElysiaTypeCheck = { + provider: 'standard', + schema, + references: '', + checkFunc: () => {}, + code: '', + // @ts-ignore + Check, + // @ts-ignore + Errors: (value: unknown) => Check(value)?.then?.((x) => x?.issues), + Code: () => '', + // @ts-ignore + Decode: Check, + // @ts-ignore + Encode: (value: unknown) => value, + hasAdditionalProperties: false, + hasDefault: false, + isOptional: false, + hasTransform: false, + hasRef: false + } + + validator.parse = (v) => { + try { + return validator.Decode(validator.Clean?.(v) ?? v) + } catch (error) { + throw [...validator.Errors(v)].map(mapValueError) + } + } + + validator.safeParse = (v) => { + try { + return { + success: true, + data: validator.Decode(validator.Clean?.(v) ?? v), + error: null + } + } catch (error) { + const errors = [...compiled.Errors(v)].map(mapValueError) + + return { + success: false, + data: null, + error: errors[0]?.summary, + errors + } + } + } + + return validator as any + } else if (validators?.length) { let hasAdditional = false + const validators = _validators as TSchema[] + const { schema: mergedObjectSchema, notObjects } = mergeObjectSchemas([ schema, - ...validators.map(mapSchema) + ...(validators.map(mapSchema) as TSchema[]) ]) if (notObjects) { schema = t.Intersect([ ...(mergedObjectSchema ? [mergedObjectSchema] : []), ...notObjects.map((x) => { - const schema = mapSchema(x) + const schema = mapSchema(x) as TSchema if ( schema.type === 'object' && @@ -832,7 +1058,8 @@ export const getSchemaValidator = ( } else { if ( schema.type === 'object' && - 'additionalProperties' in schema === false + ('additionalProperties' in schema === false || + forceAdditionalProperties) ) schema.additionalProperties = additionalProperties else @@ -854,133 +1081,270 @@ export const getSchemaValidator = ( } if (dynamic) { - const validator: ElysiaTypeCheck = { - schema, - references: '', - checkFunc: () => {}, - code: '', - // @ts-expect-error - Check: (value: unknown) => Value.Check(schema, value), - Errors: (value: unknown) => Value.Errors(schema, value), - Code: () => '', - Clean: createCleaner(schema), - Decode: (value: unknown) => Value.Decode(schema, value), - Encode: (value: unknown) => Value.Encode(schema, value), - get hasAdditionalProperties() { - if ('~hasAdditionalProperties' in this) - return this['~hasAdditionalProperties'] as boolean + if (Kind in schema) { + const validator: ElysiaTypeCheck = { + provider: 'typebox', + schema, + references: '', + checkFunc: () => {}, + code: '', + // @ts-expect-error + Check: (value: unknown) => Value.Check(schema, value), + Errors: (value: unknown) => Value.Errors(schema, value), + Code: () => '', + Clean: createCleaner(schema), + Decode: (value: unknown) => Value.Decode(schema, value), + Encode: (value: unknown) => Value.Encode(schema, value), + get hasAdditionalProperties() { + if ('~hasAdditionalProperties' in this) + return this['~hasAdditionalProperties'] as boolean + + return (this['~hasAdditionalProperties'] = + hasAdditionalProperties(schema)) + }, + get hasDefault() { + if ('~hasDefault' in this) return this['~hasDefault'] - return (this['~hasAdditionalProperties'] = - hasAdditionalProperties(schema)) - }, - get hasDefault() { - if ('~hasDefault' in this) return this['~hasDefault'] + return (this['~hasDefault'] = hasProperty( + 'default', + schema + )) + }, + get isOptional() { + if ('~isOptional' in this) return this['~isOptional']! - return (this['~hasDefault'] = hasProperty('default', schema)) - }, - get isOptional() { - if ('~isOptional' in this) return this['~isOptional']! + return (this['~isOptional'] = isOptional(schema)) + }, + get hasTransform() { + if ('~hasTransform' in this) return this['~hasTransform']! - return (this['~isOptional'] = isOptional(schema)) - }, - get hasTransform() { - if ('~hasTransform' in this) return this['~hasTransform']! + return (this['~hasTransform'] = hasTransform(schema)) + }, + '~hasRef': doesHaveRef, + get hasRef() { + if ('~hasRef' in this) return this['~hasRef']! - return (this['~hasTransform'] = hasTransform(schema)) - }, - '~hasRef': doesHaveRef, - get hasRef() { - if ('~hasRef' in this) return this['~hasRef']! + return (this['~hasRef'] = hasTransform(schema)) + } + } + + if (schema.config) { + validator.config = schema.config - return (this['~hasRef'] = hasTransform(schema)) + if (validator?.schema?.config) delete validator.schema.config } - } - if (schema.config) { - validator.config = schema.config + if (normalize && schema.additionalProperties === false) { + if (normalize === true || normalize === 'exactMirror') { + try { + validator.Clean = createMirror(schema, { + TypeCompiler, + sanitize: sanitize?.(), + modules + }) + } catch { + console.warn( + 'Failed to create exactMirror. Please report the following code to https://github.com/elysiajs/elysia/issues' + ) + console.warn(schema) + validator.Clean = createCleaner(schema) + } + } else validator.Clean = createCleaner(schema) + } - if (validator?.schema?.config) delete validator.schema.config - } + validator.parse = (v) => { + try { + return validator.Decode(validator.Clean?.(v) ?? v) + } catch (error) { + throw [...validator.Errors(v)].map(mapValueError) + } + } - if (normalize && schema.additionalProperties === false) { - if (normalize === true || normalize === 'exactMirror') { + validator.safeParse = (v) => { try { - validator.Clean = createMirror(schema, { - TypeCompiler, - sanitize: sanitize?.(), - modules - }) - } catch { - console.warn( - 'Failed to create exactMirror. Please report the following code to https://github.com/elysiajs/elysia/issues' - ) - console.warn(schema) - validator.Clean = createCleaner(schema) + return { + success: true, + data: validator.Decode(validator.Clean?.(v) ?? v), + error: null + } + } catch (error) { + const errors = [...compiled.Errors(v)].map(mapValueError) + + return { + success: false, + data: null, + error: errors[0]?.summary, + errors + } } - } else validator.Clean = createCleaner(schema) - } + } - validator.parse = (v) => { - try { - return validator.Decode(validator.Clean?.(v) ?? v) - } catch (error) { - throw [...validator.Errors(v)].map(mapValueError) + // if (cacheKey) caches[cacheKey] = validator + + return validator as any + } else { + const validator: ElysiaTypeCheck = { + provider: 'standard', + schema, + references: '', + checkFunc: () => {}, + code: '', + // @ts-ignore + Check: (v) => schema['~standard'].validate(v), + // @ts-ignore + Errors(value: unknown) { + // @ts-ignore + const response = schema['~standard'].validate(value) + + if (response instanceof Promise) + throw Error( + 'Async validation is not supported in non-dynamic schema' + ) + + return response.issues + }, + Code: () => '', + // @ts-ignore + Decode(value) { + // @ts-ignore + const response = schema['~standard'].validate(value) + + if (response instanceof Promise) + throw Error( + 'Async validation is not supported in non-dynamic schema' + ) + + return response + }, + // @ts-ignore + Encode: (value: unknown) => value, + hasAdditionalProperties: false, + hasDefault: false, + isOptional: false, + hasTransform: false, + hasRef: false } - } - validator.safeParse = (v) => { - try { - return { - success: true, - data: validator.Decode(validator.Clean?.(v) ?? v), - error: null + validator.parse = (v) => { + try { + return validator.Decode(validator.Clean?.(v) ?? v) + } catch (error) { + throw [...validator.Errors(v)].map(mapValueError) } - } catch (error) { - const errors = [...compiled.Errors(v)].map(mapValueError) + } - return { - success: false, - data: null, - error: errors[0]?.summary, - errors + validator.safeParse = (v) => { + try { + return { + success: true, + data: validator.Decode(validator.Clean?.(v) ?? v), + error: null + } + } catch (error) { + const errors = [...compiled.Errors(v)].map(mapValueError) + + return { + success: false, + data: null, + error: errors[0]?.summary, + errors + } } } - } - // if (cacheKey) caches[cacheKey] = validator + // if (cacheKey) caches[cacheKey] = validator - return validator as any + return validator as any + } } - const compiled = TypeCompiler.Compile( - schema, - Object.values(models) - ) as any as ElysiaTypeCheck + let compiled: ElysiaTypeCheck - if (schema.config) { - compiled.config = schema.config + if (Kind in schema) { + compiled = TypeCompiler.Compile( + schema, + Object.values(models).filter((x) => Kind in x) + ) as any + compiled.provider = 'typebox' - if (compiled?.schema?.config) delete compiled.schema.config - } + if (schema.config) { + compiled.config = schema.config - if (normalize === true || normalize === 'exactMirror') { - try { - compiled.Clean = createMirror(schema, { - TypeCompiler, - sanitize: sanitize?.(), - modules - }) - } catch (error) { - console.warn( - 'Failed to create exactMirror. Please report the following code to https://github.com/elysiajs/elysia/issues' - ) - console.dir(schema, { - depth: null - }) + if (compiled?.schema?.config) delete compiled.schema.config + } + if (normalize === true || normalize === 'exactMirror') { + try { + compiled.Clean = createMirror(schema, { + TypeCompiler, + sanitize: sanitize?.(), + modules + }) + } catch (error) { + console.warn( + 'Failed to create exactMirror. Please report the following code to https://github.com/elysiajs/elysia/issues' + ) + console.dir(schema, { + depth: null + }) + + compiled.Clean = createCleaner(schema) + } + } else if (normalize === 'typebox') compiled.Clean = createCleaner(schema) + } else { + compiled = { + provider: 'standard', + schema, + references: '', + checkFunc(value: unknown) { + // @ts-ignore + const response = schema['~standard'].validate(value) + + if (response instanceof Promise) + throw Error( + 'Async validation is not supported in non-dynamic schema' + ) + + return response + }, + code: '', + // @ts-ignore + Check: (v) => schema['~standard'].validate(v), + // @ts-ignore + Errors(value: unknown) { + // @ts-ignore + const response = schema['~standard'].validate(value) + + if (response instanceof Promise) + throw Error( + 'Async validation is not supported in non-dynamic schema' + ) + + return response.issues + }, + Code: () => '', + // @ts-ignore + Decode(value) { + // @ts-ignore + const response = schema['~standard'].validate(value) + + if (response instanceof Promise) + throw Error( + 'Async validation is not supported in non-dynamic schema' + ) + + return response + }, + // @ts-ignore + Encode: (value: unknown) => value, + hasAdditionalProperties: false, + hasDefault: false, + isOptional: false, + hasTransform: false, + hasRef: false } - } else if (normalize === 'typebox') compiled.Clean = createCleaner(schema) + } compiled.parse = (v) => { try { @@ -1009,36 +1373,37 @@ export const getSchemaValidator = ( } } - Object.assign(compiled, { - get hasAdditionalProperties() { - if ('~hasAdditionalProperties' in this) - return this['~hasAdditionalProperties'] - - return (this['~hasAdditionalProperties'] = - hasAdditionalProperties(compiled)) - }, - get hasDefault() { - if ('~hasDefault' in this) return this['~hasDefault'] - - return (this['~hasDefault'] = hasProperty('default', compiled)) - }, - get isOptional() { - if ('~isOptional' in this) return this['~isOptional']! - - return (this['~isOptional'] = isOptional(compiled)) - }, - get hasTransform() { - if ('~hasTransform' in this) return this['~hasTransform']! - - return (this['~hasTransform'] = hasTransform(schema)) - }, - get hasRef() { - if ('~hasRef' in this) return this['~hasRef']! - - return (this['~hasRef'] = hasRef(schema)) - }, - '~hasRef': doesHaveRef - } as ElysiaTypeCheck) + if (Kind in schema) + Object.assign(compiled, { + get hasAdditionalProperties() { + if ('~hasAdditionalProperties' in this) + return this['~hasAdditionalProperties'] + + return (this['~hasAdditionalProperties'] = + hasAdditionalProperties(compiled)) + }, + get hasDefault() { + if ('~hasDefault' in this) return this['~hasDefault'] + + return (this['~hasDefault'] = hasProperty('default', compiled)) + }, + get isOptional() { + if ('~isOptional' in this) return this['~isOptional']! + + return (this['~isOptional'] = isOptional(compiled)) + }, + get hasTransform() { + if ('~hasTransform' in this) return this['~hasTransform']! + + return (this['~hasTransform'] = hasTransform(schema)) + }, + get hasRef() { + if ('~hasRef' in this) return this['~hasRef']! + + return (this['~hasRef'] = hasRef(schema)) + }, + '~hasRef': doesHaveRef + } as ElysiaTypeCheck) // if (cacheKey) caches[cacheKey] = compiled @@ -1133,7 +1498,7 @@ export const getResponseSchemaValidator = ( sanitize }: { modules: TModule - models?: Record + models?: Record additionalProperties?: boolean dynamic?: boolean normalize?: ElysiaConfig<''>['normalize'] @@ -1150,37 +1515,41 @@ export const getResponseSchemaValidator = ( validators = validators.slice(1) } - let maybeSchemaOrRecord: TSchema | Record + let maybeSchemaOrRecord: + | TSchema + | StandardSchemaV1Like + | Record + // @ts-ignore if (typeof s !== 'string') maybeSchemaOrRecord = s! else { - const isArray = s.endsWith('[]') - const key = isArray ? s.substring(0, s.length - 2) : s + maybeSchemaOrRecord = // @ts-expect-error private property + modules && s in modules.$defs + ? (modules as TModule<{}, {}>).Import(s as never) + : models[s] - maybeSchemaOrRecord = - (modules as TModule<{}, {}>).Import(key as never) ?? models[key] - - if (isArray) - maybeSchemaOrRecord = t.Array(maybeSchemaOrRecord as TSchema) + if (!maybeSchemaOrRecord) return undefined as any } if (!maybeSchemaOrRecord) return - if (Kind in maybeSchemaOrRecord) { + if (Kind in maybeSchemaOrRecord || '~standard' in maybeSchemaOrRecord) return { - 200: getSchemaValidator(maybeSchemaOrRecord, { - modules, - models, - additionalProperties, - dynamic, - normalize, - coerce: false, - additionalCoerce: [], - validators: validators.map((x) => x![200]), - sanitize - }) + 200: getSchemaValidator( + maybeSchemaOrRecord as TSchema | StandardSchemaV1Like, + { + modules, + models, + additionalProperties, + dynamic, + normalize, + coerce: false, + additionalCoerce: [], + validators: validators.map((x) => x![200]), + sanitize + } + )! } - } const record: Record> = {} @@ -1193,10 +1562,12 @@ export const getResponseSchemaValidator = ( if (maybeNameOrSchema in models) { const schema = models[maybeNameOrSchema] + if (!schema) return + // Inherits model maybe already compiled record[+status] = - Kind in schema - ? getSchemaValidator(schema, { + Kind in schema || '~standard' in schema + ? getSchemaValidator(schema as TSchema, { modules, models, additionalProperties, @@ -1206,8 +1577,8 @@ export const getResponseSchemaValidator = ( additionalCoerce: [], validators: validators.map((x) => x![+status]), sanitize - }) - : schema + })! + : (schema as ElysiaTypeCheck) } return undefined @@ -1215,7 +1586,7 @@ export const getResponseSchemaValidator = ( // Inherits model maybe already compiled record[+status] = - Kind in maybeNameOrSchema + Kind in maybeNameOrSchema || '~standard' in maybeNameOrSchema ? getSchemaValidator(maybeNameOrSchema as TSchema, { modules, models, @@ -1227,7 +1598,7 @@ export const getResponseSchemaValidator = ( validators: validators.map((x) => x![+status]), sanitize }) - : maybeNameOrSchema + : (maybeNameOrSchema as ElysiaTypeCheck) }) return record @@ -1253,6 +1624,26 @@ export const stringToStructureCoercions = () => { return _stringToStructureCoercions } +let _queryCoercions: ReplaceSchemaTypeOptions[] + +export const queryCoercions = () => { + if (!_queryCoercions) { + _queryCoercions = [ + { + from: t.Object({}), + to: () => t.ObjectString({}), + excludeRoot: true + }, + { + from: t.Array(t.Any()), + to: () => t.ArrayQuery(t.Any()) + } + ] satisfies ReplaceSchemaTypeOptions[] + } + + return _queryCoercions +} + let _coercePrimitiveRoot: ReplaceSchemaTypeOptions[] export const coercePrimitiveRoot = () => { @@ -1284,27 +1675,37 @@ export const getCookieValidator = ({ validators, sanitize }: { - validator: TSchema | string | undefined + validator: + | TSchema + | StandardSchemaV1Like + | ElysiaTypeCheck + | string + | undefined modules: TModule defaultConfig: CookieOptions | undefined config: CookieOptions dynamic: boolean normalize: ElysiaConfig<''>['normalize'] | undefined - models: Record | undefined + models: Record | undefined validators?: InputSchema['cookie'][] sanitize?: () => ExactMirrorInstruction['sanitize'] }) => { - let cookieValidator = getSchemaValidator(validator, { - modules, - dynamic, - models, - normalize, - additionalProperties: true, - coerce: true, - additionalCoerce: stringToStructureCoercions(), - validators, - sanitize - }) + let cookieValidator = + // @ts-ignore + validator?.provider + ? (validator as ElysiaTypeCheck) + : // @ts-ignore + getSchemaValidator(validator, { + modules, + dynamic, + models, + normalize, + additionalProperties: true, + coerce: true, + additionalCoerce: stringToStructureCoercions(), + validators, + sanitize + }) if (cookieValidator) cookieValidator.config = mergeCookie(cookieValidator.config, config) @@ -1364,6 +1765,8 @@ export const getCookieValidator = ({ // } export const unwrapImportSchema = (schema: TSchema): TSchema => - schema[Kind] === 'Import' && schema.$defs[schema.$ref][Kind] === 'Object' + schema && + schema[Kind] === 'Import' && + schema.$defs[schema.$ref][Kind] === 'Object' ? schema.$defs[schema.$ref] : schema diff --git a/src/type-system/index.ts b/src/type-system/index.ts index 009980bd8..97b712d7c 100644 --- a/src/type-system/index.ts +++ b/src/type-system/index.ts @@ -49,6 +49,7 @@ import { ValidationError } from '../error' import { parseDateTimeEmptySpace } from './format' import { replaceSchemaType } from '../schema' import { isJSDocDeprecatedTag } from 'typescript' +import { ElysiaFile } from '../universal/file' const t = Object.assign({}, Type) as unknown as Omit< JavaScriptTypeBuilder, @@ -281,17 +282,20 @@ export const ElysiaType = { const schema = t.Object(properties, options) const compiler = compile(schema) - const defaultValue = JSON.stringify(compiler.Create()) - return t .Transform( - t.Union([ - t.String({ - format: 'ObjectString', - default: defaultValue - }), - schema - ]) + t.Union( + [ + t.String({ + format: 'ObjectString', + default: '{}' + }), + schema + ], + { + elysiaMeta: 'ObjectString' + } + ) ) .Decode((value) => { if (typeof value === 'string') { @@ -333,13 +337,13 @@ export const ElysiaType = { } // has , (as used in nuqs) - if (value.indexOf(',') !== -1) { - // const newValue = value.split(',').map((v) => v.trim()) + // if (value.indexOf(',') !== -1) { + // // const newValue = value.split(',').map((v) => v.trim()) - if (!compiler.Check(value)) throw compiler.Error(value) + // if (!compiler.Check(value)) throw compiler.Error(value) - return compiler.Decode(value) - } + // return compiler.Decode(value) + // } if (isProperty) return value @@ -393,6 +397,72 @@ export const ElysiaType = { }) as any as TArray }, + ArrayQuery: ( + children: T = t.String() as any, + options?: ArrayOptions + ) => { + const schema = t.Array(children, options) + const compiler = compile(schema) + + const decode = (value: string) => { + // has , (as used in nuqs) + if (value.indexOf(',') !== -1) + return compiler.Decode(value.split(',')) + + return [value] + } + + return t + .Transform( + t.Union( + [ + t.String({ + default: options?.default + }), + schema + ], + { + elysiaMeta: 'ArrayQuery' + } + ) + ) + .Decode((value) => { + if (Array.isArray(value)) { + let values = [] + + for (let i = 0; i < value.length; i++) { + const v = value[i] + if (typeof v === 'string') { + const t = decode(v) + if (Array.isArray(t)) values = values.concat(t) + else values.push(t) + + continue + } + + values.push(v) + } + + return values + } + + if (typeof value === 'string') return decode(value) + + // Is probably transformed, unable to check schema + return value + }) + .Encode((value) => { + let original + if (typeof value === 'string') + value = tryParse((original = value), schema) + + if (!compiler.Check(value)) + throw new ValidationError('property', schema, value) + + return original ?? JSON.stringify(value) + }) as any as TArray + }, + File: createType( 'File', validateFile @@ -539,6 +609,7 @@ export const ElysiaType = { t.BooleanString = ElysiaType.BooleanString t.ObjectString = ElysiaType.ObjectString t.ArrayString = ElysiaType.ArrayString +t.ArrayQuery = ElysiaType.ArrayQuery t.Numeric = ElysiaType.Numeric t.Integer = ElysiaType.Integer diff --git a/src/type-system/types.ts b/src/type-system/types.ts index eb6b6166e..03f4fbe9f 100644 --- a/src/type-system/types.ts +++ b/src/type-system/types.ts @@ -16,6 +16,7 @@ import type { TypeCheck } from '@sinclair/typebox/compiler' import { ElysiaFormData } from '../utils' import type { CookieOptions } from '../cookies' import type { MaybeArray } from '../types' +import { ElysiaFile } from '../universal/file' export type FileUnit = number | `${number}${'k' | 'm'}` diff --git a/src/type-system/utils.ts b/src/type-system/utils.ts index 65f788064..7ff024f1f 100644 --- a/src/type-system/utils.ts +++ b/src/type-system/utils.ts @@ -13,7 +13,8 @@ import { InvalidFileType, ValidationError } from '../error' import type { ElysiaTypeCustomErrorCallback, FileOptions, - FileUnit + FileUnit, + FileType } from './types' import type { MaybeArray } from '../types' @@ -124,15 +125,15 @@ export const fileTypeFromBlob = (file: Blob | File) => { }) } -export const validateFileExtension = async ( +export const fileType = async ( file: MaybeArray, - extension: string | string[], + extension: FileType | FileType[], // @ts-ignore name = file?.name ?? '' ): Promise => { if (Array.isArray(file)) { await Promise.all( - file.map((f) => validateFileExtension(f, extension, name)) + file.map((f) => fileType(f, extension, name)) ) return true @@ -153,6 +154,10 @@ export const validateFileExtension = async ( throw new InvalidFileType(name, extension) } +/** + * This only check file extension based on it's name / mimetype + * to actual check the file type, use `validateFileExtension` instead + */ export const validateFile = (options: FileOptions, value: any) => { if (value instanceof ElysiaFile) return true @@ -164,8 +169,6 @@ export const validateFile = (options: FileOptions, value: any) => { if (options.maxSize && value.size > parseFileUnit(options.maxSize)) return false - // This only check file extension based on it's name / mimetype - // to actual check the file type, use `validateFileExtension` instead if (options.extension) { if (typeof options.extension === 'string') return checkFileExtension(value.type, options.extension) diff --git a/src/types.ts b/src/types.ts index 0dec442a2..698a194ca 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import type { Elysia, AnyElysia } from '.' +import type { Elysia, AnyElysia, InvertedStatusMap } from '.' import type { ElysiaFile } from './universal/file' import type { Serve } from './universal/server' @@ -33,12 +33,47 @@ import type { import type { AnyWSLocalHook } from './ws/types' import type { WebSocketHandler } from './ws/bun' -import type { Instruction as ExactMirrorInstruction } from 'exact-mirror' -type PartialServe = Partial +import type { Instruction as ExactMirrorInstruction } from 'exact-mirror' export type IsNever = [T] extends [never] ? true : false +export type PickIfExists = {} extends T + ? {} + : { + // @ts-ignore + [P in K as P extends keyof T ? P : never]: T[P] + } + +// Standard Schema reduce to bare minimum to save inference time +export interface StandardSchemaV1Like< + in out Input = unknown, + in out Output = Input +> { + readonly '~standard': { + readonly types?: + | { + readonly input: Input + readonly output: Output + } + | undefined + } +} + +// ? Fast check if the generic is enforced to StandardSchemaV1Like +export interface FastStandardSchemaV1Like { + readonly '~standard': {} +} + +export type StandardSchemaV1LikeValidate = ( + v: T +) => MaybePromise< + { value: T; issues?: never } | { value?: never; issues: unknown[] } +> + +export type AnySchema = TSchema | StandardSchemaV1Like +export type FastAnySchema = TAnySchema | FastStandardSchemaV1Like + export interface ElysiaConfig { /** * @default BunAdapter @@ -66,7 +101,7 @@ export interface ElysiaConfig { * * @see https://bun.sh/docs/api/http */ - serve?: PartialServe + serve?: Partial /** * OpenAPI documentation (use in Swagger) * @@ -166,6 +201,8 @@ export interface ElysiaConfig { * - 'exactMirror': use Elysia's custom exact-mirror which precompile a schema * - 'typebox': Since this uses dynamic Value.Clean, it have performance impact * + * Note: This option only works when Elysia schema is provided, doesn't work with Standard Schema + * * @default true */ normalize?: boolean | 'exactMirror' | 'typebox' @@ -205,12 +242,14 @@ export interface ValidatorLayer { } export interface StandaloneInputSchema { - body?: TSchema | Name | `${Name}[]` - headers?: TSchema | Name | `${Name}[]` - query?: TSchema | Name | `${Name}[]` - params?: TSchema | Name | `${Name}[]` - cookie?: TSchema | Name | `${Name}[]` - response?: { [status in number]: `${Name}[]` | Name | TSchema } + body?: AnySchema | Name | `${Name}[]` + headers?: AnySchema | Name | `${Name}[]` + query?: AnySchema | Name | `${Name}[]` + params?: AnySchema | Name | `${Name}[]` + cookie?: AnySchema | Name | `${Name}[]` + response?: { + [status in number]: `${Name}[]` | Name | AnySchema + } } export interface StandaloneValidator { @@ -236,7 +275,7 @@ export type GetPathParameter = ? IsPathParameter | GetPathParameter : IsPathParameter -export type ResolvePath = Prettify< +type _ResolvePath = Prettify< { [Param in GetPathParameter as Param extends `${string}?` ? never @@ -248,6 +287,10 @@ export type ResolvePath = Prettify< } > +export type ResolvePath = Path extends PathParameterLike + ? _ResolvePath + : {} + export type Or = T1 extends true ? true : T2 extends true @@ -357,15 +400,20 @@ export interface SingletonBase { resolve: Record } +export interface PossibleResponse { + [status: number]: unknown +} + export interface EphemeralType { derive: SingletonBase['derive'] resolve: SingletonBase['resolve'] schema: MetadataBase['schema'] standaloneSchema: MetadataBase['schema'] + response: PossibleResponse } export interface DefinitionBase { - typebox: Record + typebox: Record error: Record } @@ -375,8 +423,9 @@ export interface MetadataBase { schema: RouteSchema standaloneSchema: MetadataBase['schema'] macro: BaseMacro - macroFn: BaseMacroFn + macroFn: Macro parser: Record> + response: PossibleResponse } export interface RouteSchema { @@ -392,10 +441,8 @@ interface OptionalField { [OptionalKind]: 'Optional' } -type TrimArrayName = T extends `${infer Name}[]` ? Name : T - export type UnwrapSchema< - Schema extends TSchema | string | undefined, + Schema extends AnySchema | string | undefined, Definitions extends DefinitionBase['typebox'] = {} > = undefined extends Schema ? unknown @@ -403,6 +450,7 @@ export type UnwrapSchema< ? Schema extends OptionalField ? Partial< TImport< + // @ts-expect-error Internal typebox already filter for TSchema Definitions & { readonly __elysia: Schema }, @@ -410,27 +458,31 @@ export type UnwrapSchema< >['static'] > : TImport< + // @ts-expect-error Internal typebox already filter for TSchema Definitions & { readonly __elysia: Schema }, '__elysia' >['static'] - : Schema extends `${infer Key}[]` - ? Definitions extends Record< - Key, - infer NamedSchema extends TAnySchema - > - ? NamedSchema['static'][] - : TImport>['static'][] + : Schema extends FastStandardSchemaV1Like + ? // @ts-ignore Schema is StandardSchemaV1Like + NonNullable['output'] : Schema extends string - ? Definitions extends keyof Schema - ? // @ts-ignore Definitions is always a Record - NamedSchema['static'] - : TImport['static'] + ? Schema extends keyof Definitions + ? Definitions[Schema] extends TAnySchema + ? TImport< + // @ts-expect-error Internal typebox already filter for TSchema + Definitions, + Schema + >['static'] + : NonNullable< + Definitions[Schema]['~standard']['types'] + >['output'] + : unknown : unknown export type UnwrapBodySchema< - Schema extends TSchema | string | undefined, + Schema extends AnySchema | string | undefined, Definitions extends DefinitionBase['typebox'] = {} > = undefined extends Schema ? unknown @@ -438,6 +490,7 @@ export type UnwrapBodySchema< ? Schema extends OptionalField ? Partial< TImport< + // @ts-expect-error Internal typebox already filter for TSchema Definitions & { readonly __elysia: Schema }, @@ -445,23 +498,28 @@ export type UnwrapBodySchema< >['static'] > | null : TImport< + // @ts-expect-error Internal typebox already filter for TSchema Definitions & { readonly __elysia: Schema }, '__elysia' >['static'] - : Schema extends `${infer Key}[]` - ? Definitions extends Record< - Key, - infer NamedSchema extends TAnySchema - > - ? NamedSchema['static'][] - : TImport>['static'][] + : Schema extends FastStandardSchemaV1Like + ? // @ts-ignore Schema is StandardSchemaV1Like + NonNullable['output'] : Schema extends string - ? Definitions extends keyof Schema - ? // @ts-ignore Definitions is always a Record - NamedSchema['static'] - : TImport['static'] + ? Schema extends keyof Definitions + ? Definitions[Schema] extends TAnySchema + ? TImport< + // @ts-expect-error Internal typebox already filter for TSchema + Definitions, + Schema + >['static'] + : // @ts-ignore Schema is StandardSchemaV1Like + NonNullable< + Definitions[Schema]['~standard']['types'] + >['output'] + : unknown : unknown export interface UnwrapRoute< @@ -478,21 +536,29 @@ export interface UnwrapRoute< ? ResolvePath : UnwrapSchema cookie: UnwrapSchema - response: Schema['response'] extends TSchema | string + response: Schema['response'] extends FastAnySchema | string ? { - 200: CoExist< - UnwrapSchema, - File, - ElysiaFile | Blob - > + 200: UnwrapSchema< + Schema['response'], + Definitions + > extends infer A + ? A extends File + ? File | ElysiaFile + : A + : unknown } - : Schema['response'] extends { [status in number]: TAnySchema | string } + : Schema['response'] extends { + [status in number]: FastAnySchema | string + } ? { - [k in keyof Schema['response']]: CoExist< - UnwrapSchema, - File, - ElysiaFile | Blob - > + [k in keyof Schema['response']]: UnwrapSchema< + Schema['response'][k], + Definitions + > extends infer A + ? A extends File + ? File | ElysiaFile + : A + : unknown } : unknown | void } @@ -516,7 +582,7 @@ export interface UnwrapGroupGuardRoute< params: UnwrapSchema extends infer A extends Record ? A - : Path extends `${string}/${':' | '*'}${string}` + : Path extends PathParameterLike ? Record, string> : never cookie: UnwrapSchema extends infer A extends @@ -622,57 +688,63 @@ export type HTTPMethod = | 'UNSUBSCRIBE' | 'ALL' -export interface InputSchema { - body?: TSchema | Name | `${Name}[]` - headers?: TSchema | Name | `${Name}[]` - query?: TSchema | Name | `${Name}[]` - params?: TSchema | Name | `${Name}[]` - cookie?: TSchema | Name | `${Name}[]` +export interface InputSchema { + body?: AnySchema | Name + headers?: AnySchema | Name + query?: AnySchema | Name + params?: AnySchema | Name + cookie?: AnySchema | Name response?: - | TSchema - | { [status in number]: TSchema } + | AnySchema + | { [status in number]: AnySchema } | Name - | `${Name}[]` - | { [status in number]: `${Name}[]` | Name | TSchema } + | { + [status in number]: Name | AnySchema + } } -export interface PrettifySchema { - body: unknown extends A['body'] ? A['body'] : Prettify - headers: unknown extends A['headers'] - ? A['headers'] - : Prettify - query: unknown extends A['query'] ? A['query'] : Prettify - params: unknown extends A['params'] ? A['params'] : Prettify - cookie: unknown extends A['cookie'] ? A['cookie'] : Prettify - response: unknown extends A['response'] - ? A['response'] - : Prettify -} +type PathParameterLike = `${string}/${':' | '*'}${string}` -export interface MergeSchema< - in out A extends RouteSchema, - in out B extends RouteSchema, +export type MergeSchema< + A extends RouteSchema, + B extends RouteSchema, Path extends string = '' -> { - body: undefined extends A['body'] ? B['body'] : A['body'] - headers: undefined extends A['headers'] ? B['headers'] : A['headers'] - query: undefined extends A['query'] ? B['query'] : A['query'] - params: IsNever extends true - ? IsNever extends true - ? ResolvePath - : B['params'] - : IsNever extends true - ? A['params'] - : Prettify> - cookie: undefined extends A['cookie'] ? B['cookie'] : A['cookie'] - response: {} extends A['response'] - ? {} extends B['response'] - ? {} - : B['response'] - : {} extends B['response'] - ? A['response'] - : A['response'] & Omit -} +> = {} extends A + ? Path extends PathParameterLike + ? Omit & { params: ResolvePath } + : B + : {} extends B + ? Path extends PathParameterLike + ? Omit & { params: ResolvePath } + : A + : { + body: undefined extends A['body'] ? B['body'] : A['body'] + headers: undefined extends A['headers'] + ? B['headers'] + : A['headers'] + query: undefined extends A['query'] ? B['query'] : A['query'] + params: IsNever extends true + ? IsNever extends true + ? ResolvePath + : B['params'] + : IsNever extends true + ? A['params'] + : Prettify< + B['params'] & + Omit + > + cookie: undefined extends A['cookie'] + ? B['cookie'] + : A['cookie'] + response: {} extends A['response'] + ? {} extends B['response'] + ? {} + : B['response'] + : {} extends B['response'] + ? A['response'] + : A['response'] & + Omit + } export interface MergeStandaloneSchema< in out A extends RouteSchema, @@ -783,69 +855,275 @@ export type MacroContextBlacklistKey = | 'tags' | keyof RouteSchema -type ReturnTypeIfPossible = T extends AnyContextFn ? ReturnType : T +type ReturnTypeIfPossible = false extends Enabled + ? {} + : T extends (...a: any) => infer R + ? R + : T type AnyElysiaCustomStatusResponse = ElysiaCustomStatusResponse -type MacroResolveLike = MaybeArray< - ( - ...v: any - ) => MaybePromise< - Record | void | AnyElysiaCustomStatusResponse - > -> - -type ResolveMacroPropertyLike = { - resolve: MacroResolveLike -} - -type ResolveMacroFnLike = (...v: any[]) => ResolveMacroPropertyLike +type FunctionArrayReturnType = + // If nothing is provided, it will be resolved as any + any[] extends T + ? never + : T extends any[] + ? _FunctionArrayReturnType + : // @ts-ignore + Awaited> + +type _FunctionArrayReturnType = T extends [ + infer Fn, + ...infer Rest +] + ? _FunctionArrayReturnType< + Rest, + Awaited< + // @ts-ignore Trust me bro + ReturnType + > extends infer A + ? IsNever extends true + ? Carry + : A | Carry + : Carry + > + : Carry -type MergeAllMacroContext = - UnionToIntersection extends infer O - ? { [K in keyof O]: O[K] } - : never +type FunctionArrayReturnTypeNonNullable = + // If nothing is provided, it will be resolved as any + any[] extends T + ? never + : T extends any[] + ? _FunctionArrayReturnTypeNonNullable + : // @ts-ignore + NonNullable>> + +type _FunctionArrayReturnTypeNonNullable = T extends [ + infer Fn, + ...infer Rest +] + ? _FunctionArrayReturnTypeNonNullable< + Rest, + NonNullable< + Awaited< + // @ts-ignore Trust me bro + ReturnType + > + > extends infer A + ? IsNever extends true + ? Carry + : A | Carry + : Carry + > + : Carry -type UnionToIntersection = (U extends any ? (x: U) => any : never) extends ( - x: infer I -) => any - ? I - : never +type ExtractResolveFromMacro = + IsNever extends true + ? {} + : A extends AnyElysiaCustomStatusResponse + ? A + : Exclude extends infer A + ? IsAny extends true + ? {} + : A + : {} + +type ExtractOnlyResponseFromMacro = + IsNever extends true + ? {} + : {} extends A + ? {} + : Extract extends infer A + ? IsNever extends true + ? {} + : { + return: A extends ElysiaCustomStatusResponse< + any, + infer Value, + infer Status + > + ? { + [status in Status]: IsAny extends true + ? // @ts-ignore status is always in Status Map + InvertedStatusMap[Status] + : Value + } + : {} + } + : {} -type ExtractMacroContext = +type ExtractAllResponseFromMacro = IsNever extends true ? {} - : Exclude, AnyElysiaCustomStatusResponse | void> + : { + return: UnionToIntersect< + A extends ElysiaCustomStatusResponse< + any, + infer Value, + infer Status + > + ? { + [status in Status]: IsAny extends true + ? // @ts-ignore status is always in Status Map + InvertedStatusMap[Status] + : Value + } + : Exclude< + A, + AnyElysiaCustomStatusResponse + > extends infer A + ? IsAny extends true + ? {} + : // FunctionArrayReturnType + NonNullable extends A + ? {} + : undefined extends A + ? {} + : { + 200: A + } + : {} + > + } // There's only resolve that can add new properties to Context export type MacroToContext< - MacroFn extends BaseMacroFn = {}, - SelectedMacro extends MetadataBase['macro'] = {} -> = {} extends SelectedMacro - ? {} - : MergeAllMacroContext<{ - [key in keyof SelectedMacro as MacroFn[key] extends ResolveMacroFnLike - ? key - : MacroFn[key] extends ResolveMacroPropertyLike - ? true extends SelectedMacro[key] - ? key - : never - : never]: ExtractMacroContext< - ResolveResolutions< - // @ts-expect-error type is checked in key mapping - ReturnTypeIfPossible['resolve'] + in out MacroFn extends Macro = {}, + in out SelectedMacro extends BaseMacro = {}, + in out Definitions extends DefinitionBase['typebox'] = {}, + in out R extends 1[] = [1] +> = Prettify< + {} extends SelectedMacro + ? {} + : R['length'] extends 16 + ? {} + : UnionToIntersect< + { + [key in keyof SelectedMacro]: ReturnTypeIfPossible< + MacroFn[key], + SelectedMacro[key] + > extends infer Value + ? { + resolve: true extends SelectedMacro[key] + ? ExtractResolveFromMacro< + Extract< + Exclude< + FunctionArrayReturnType< + // @ts-ignore Trust me bro + Value['resolve'] + >, + AnyElysiaCustomStatusResponse + >, + Record + > + > + : {} + } & UnwrapMacroSchema< + // @ts-ignore Trust me bro + Value, + Definitions + > & + ExtractAllResponseFromMacro< + FunctionArrayReturnTypeNonNullable< + // @ts-expect-error type is checked in key mapping + Value['beforeHandle'] + > + > & + ExtractAllResponseFromMacro< + FunctionArrayReturnTypeNonNullable< + // @ts-expect-error type is checked in key mapping + Value['afterHandle'] + > + > & + ExtractAllResponseFromMacro< + // @ts-expect-error type is checked in key mapping + FunctionArrayReturnType + > & + ExtractOnlyResponseFromMacro< + FunctionArrayReturnTypeNonNullable< + // @ts-expect-error type is checked in key mapping + Value['resolve'] + > + > & + MacroToContext< + MacroFn, + // @ts-ignore trust me bro + Pick< + Value, + Extract + >, + Definitions, + [...R, 1] + > + : {} + }[keyof SelectedMacro] > - > - }> +> + +type UnwrapMacroSchema< + T extends Partial>, + Definitions extends DefinitionBase['typebox'] = {} +> = UnwrapRoute< + { + body: 'body' extends keyof T ? T['body'] : undefined + headers: 'headers' extends keyof T ? T['headers'] : undefined + query: 'query' extends keyof T ? T['query'] : undefined + params: 'params' extends keyof T ? T['params'] : undefined + cookie: 'cookie' extends keyof T ? T['cookie'] : undefined + response: 'response' extends keyof T ? T['response'] : undefined + }, + Definitions +> + +export type SimplifyToSchema> = + IsUnknown extends false + ? _SimplifyToSchema + : IsUnknown extends false + ? _SimplifyToSchema + : IsUnknown extends false + ? _SimplifyToSchema + : IsUnknown extends false + ? _SimplifyToSchema + : IsUnknown extends false + ? _SimplifyToSchema + : IsUnknown extends false + ? _SimplifyToSchema + : {} + +export type _SimplifyToSchema> = Omit< + { + body: T['body'] + headers: T['headers'] + query: T['query'] + params: T['params'] + cookie: T['cookie'] + response: T['response'] + }, + | ('body' extends keyof T ? never : 'body') + | ('headers' extends keyof T ? never : 'headers') + | ('query' extends keyof T ? never : 'query') + | ('params' extends keyof T ? never : 'params') + | ('cookie' extends keyof T ? never : 'cookie') + | ('response' extends keyof T ? never : 'response') +> type InlineHandlerResponse = { [Status in keyof Route]: ElysiaCustomStatusResponse< // @ts-ignore Status is always a number Status, - Route[Status] + Route[Status], + Status > }[keyof Route] +type InlineResponse = + | string + | number + | boolean + | Record + | Response + | AnyElysiaCustomStatusResponse + export type InlineHandler< Route extends RouteSchema = {}, Singleton extends SingletonBase = { @@ -854,27 +1132,44 @@ export type InlineHandler< derive: {} resolve: {} }, - Path extends string | undefined = undefined + MacroContext extends { + response: PossibleResponse + return: PossibleResponse + resolve: Record + } = { + response: {} + return: {} + resolve: {} + } > = - | ((context: Context) => + | InlineResponse + | (( + context: Context< + Route & MacroContext, + Singleton & { resolve: MacroContext['resolve'] } & { + resolve: { + q: Prettify + } + } + > + ) => | Response | MaybePromise< {} extends Route['response'] ? unknown : - | (Route['response'] extends { 200: any } + | (Route['response'] extends { + 200: any + } ? Route['response'][200] : unknown) // This could be possible because of set.status | Route['response'][keyof Route['response']] - | InlineHandlerResponse + | InlineHandlerResponse< + Route['response'] & + MacroContext['response'] + > >) - | ({} extends Route['response'] - ? string | number | boolean | object - : Route['response'] extends { 200: any } - ? Route['response'][200] - : string | number | boolean | object) - | ElysiaCustomStatusResponse export type OptionalHandler< in out Route extends RouteSchema = {}, @@ -904,6 +1199,12 @@ export type AfterHandler< Path extends string | undefined = undefined > = ( context: Context & { + responseValue: {} extends Route['response'] + ? unknown + : Route['response'][keyof Route['response']] + /** + * @deprecated use `context.responseValue` instead + */ response: {} extends Route['response'] ? unknown : Route['response'][keyof Route['response']] @@ -927,17 +1228,17 @@ export type MapResponse< }, Path extends string | undefined = undefined > = ( - context: Context< - Omit, - Singleton & { - derive: { - response: {} extends Route['response'] - ? unknown - : Route['response'] - } - }, - Path - > + context: Context & { + responseValue: {} extends Route['response'] + ? unknown + : Route['response'][keyof Route['response']] + /** + * @deprecated use `context.responseValue` instead + */ + response: {} extends Route['response'] + ? unknown + : Route['response'][keyof Route['response']] + } ) => MaybePromise // Handler< @@ -1033,6 +1334,12 @@ export type AfterResponseHandler< } > = ( context: Context & { + responseValue: {} extends Route['response'] + ? unknown + : Route['response'][keyof Route['response']] + /** + * @deprecated use `context.responseValue` instead + */ response: {} extends Route['response'] ? unknown : Route['response'][keyof Route['response']] @@ -1058,6 +1365,7 @@ export type ErrorHandler< resolve: {} schema: {} standaloneSchema: {} + response: {} }, // ? local in out Volatile extends EphemeralType = { @@ -1065,6 +1373,7 @@ export type ErrorHandler< resolve: {} schema: {} standaloneSchema: {} + response: {} } > = ( context: ErrorContext< @@ -1218,21 +1527,13 @@ export type ErrorHandler< ) ) => any | Promise -export type DocumentDecoration = Partial & { +export interface DocumentDecoration extends Partial { /** * Pass `true` to hide route from OpenAPI/swagger document * */ hide?: boolean } -// export type DeriveHandler< -// Singleton extends SingletonBase, -// in out Derivative extends Record | void = Record< -// string, -// unknown -// > | void -// > = (context: Context<{}, Singleton>) => MaybePromise - export type ResolveHandler< in out Route extends RouteSchema, in out Singleton extends SingletonBase, @@ -1242,102 +1543,230 @@ export type ResolveHandler< | void = Record | AnyElysiaCustomStatusResponse | void > = (context: Context) => MaybePromise -type AnyContextFn = (context?: any) => any - -// export type ResolveDerivatives< -// T extends MaybeArray> | undefined -// > = -// IsNever extends true -// ? any[] extends T -// ? {} -// : ReturnType -// : ResolveDerivativesArray - -export type ResolveDerivativesArray< - T extends any[], - Carry extends Record = {} -> = T extends [infer Fn extends AnyContextFn, ...infer Rest] - ? ReturnType extends infer Value extends Record - ? ResolveDerivativesArray - : ResolveDerivativesArray - : Prettify - -export type ResolveResolutions> = +export type ResolveReturnType> = // If no macro are provided, it will be resolved as any any[] extends T ? {} - : IsNever extends true - ? any[] extends T - ? {} - : ReturnType - : ResolveResolutionsArray - -export type ResolveResolutionsArray< - T extends any[], - Carry extends Record = {} -> = T extends [infer Fn extends AnyContextFn, ...infer Rest] - ? ReturnType extends infer Value extends Record - ? ResolveResolutionsArray - : ResolveResolutionsArray + : Exclude< + // @ts-ignore Trust me bro + Awaited>, + AnyElysiaCustomStatusResponse + > extends infer Value extends Record + ? Value + : {} + +type _ResolveReturnTypeArray = T extends [ + infer Fn, + ...infer Rest +] + ? Exclude< + // @ts-ignore Trust me bro + Awaited>, + AnyElysiaCustomStatusResponse + > extends infer Value extends Record + ? _ResolveReturnTypeArray + : _ResolveReturnTypeArray : Prettify -export type AnyLocalHook = LocalHook +export type AnyLocalHook = LocalHook + +export interface BaseHookLifeCycle< + in out Schema extends RouteSchema, + in out Singleton extends SingletonBase, + in out Errors extends { [key in string]: Error }, + in out Parser extends keyof any = '' +> { + detail?: DocumentDecoration + /** + * Short for 'Content-Type' + * + * Available: + * - 'none': do not parse body + * - 'text' / 'text/plain': parse body as string + * - 'json' / 'application/json': parse body as json + * - 'formdata' / 'multipart/form-data': parse body as form-data + * - 'urlencoded' / 'application/x-www-form-urlencoded: parse body as urlencoded + * - 'arraybuffer': parse body as readable stream + */ + parse?: MaybeArray | ContentType | Parser> + /** + * Transform context's value + */ + transform?: MaybeArray> + /** + * Execute before main handler + */ + beforeHandle?: MaybeArray> + /** + * Execute after main handler + */ + afterHandle?: MaybeArray> + /** + * Execute after main handler + */ + mapResponse?: MaybeArray> + /** + * Execute after response is sent + */ + afterResponse?: MaybeArray> + /** + * Catch error + */ + error?: MaybeArray> + tags?: DocumentDecoration['tags'] +} + +export type CreateDecorator< + Singleton extends SingletonBase, + Ephemeral extends EphemeralType, + Volatile extends EphemeralType +> = {} extends Ephemeral + ? {} extends Volatile + ? Singleton + : Singleton & Volatile + : {} extends Volatile + ? Singleton & Ephemeral + : Singleton & Ephemeral & Volatile + +export type AnyBaseHookLifeCycle = BaseHookLifeCycle + +export type NonResolvableMacroKey = + | keyof AnyBaseHookLifeCycle + | keyof InputSchema + +interface RouteSchemaWithResolvedMacro extends RouteSchema { + response: PossibleResponse + return: PossibleResponse + resolve: Record +} export type LocalHook< - LocalSchema extends InputSchema, - Schema extends RouteSchema, + Input extends BaseMacro, + Schema extends RouteSchemaWithResolvedMacro, Singleton extends SingletonBase, Errors extends { [key in string]: Error }, - Macro extends BaseMacro, Parser extends keyof any = '' -> = Prettify & - // Kind of an inference hack, I have no idea why it work either - (LocalSchema extends any ? LocalSchema : Prettify) & { - detail?: DocumentDecoration - /** - * Short for 'Content-Type' - * - * Available: - * - 'none': do not parse body - * - 'text' / 'text/plain': parse body as string - * - 'json' / 'application/json': parse body as json - * - 'formdata' / 'multipart/form-data': parse body as form-data - * - 'urlencoded' / 'application/x-www-form-urlencoded: parse body as urlencoded - * - 'arraybuffer': parse body as readable stream - */ - parse?: MaybeArray< - BodyHandler | ContentType | Parser - > - /** - * Transform context's value - */ - transform?: MaybeArray> - /** - * Execute before main handler - */ - beforeHandle?: MaybeArray> - /** - * Execute after main handler - */ - afterHandle?: MaybeArray> - /** - * Execute after main handler - */ - mapResponse?: MaybeArray> - /** - * Execute after response is sent - */ - afterResponse?: MaybeArray> - /** - * Catch error - */ - error?: MaybeArray> - tags?: DocumentDecoration['tags'] - } +> = (Input extends any ? Input : Prettify) & { + detail?: DocumentDecoration + /** + * Short for 'Content-Type' + * + * Available: + * - 'none': do not parse body + * - 'text' / 'text/plain': parse body as string + * - 'json' / 'application/json': parse body as json + * - 'formdata' / 'multipart/form-data': parse body as form-data + * - 'urlencoded' / 'application/x-www-form-urlencoded: parse body as urlencoded + * - 'arraybuffer': parse body as readable stream + */ + parse?: MaybeArray< + | BodyHandler + | ContentType + | Parser + > + /** + * Transform context's value + */ + transform?: MaybeArray< + TransformHandler + > + /** + * Execute before main handler + */ + beforeHandle?: MaybeArray< + OptionalHandler + > + /** + * Execute after main handler + */ + afterHandle?: MaybeArray< + AfterHandler + > + /** + * Execute after main handler + */ + mapResponse?: MaybeArray< + MapResponse + > + /** + * Execute after response is sent + */ + afterResponse?: MaybeArray< + AfterResponseHandler + > + /** + * Catch error + */ + error?: MaybeArray< + ErrorHandler + > + tags?: DocumentDecoration['tags'] +} + +export type GuardLocalHook< + Input extends BaseMacro | undefined, + Schema extends RouteSchema, + Singleton extends SingletonBase, + Parser extends keyof any, + GuardType extends GuardSchemaType, + AsType extends LifeCycleType, + BeforeHandle extends MaybeArray>, + AfterHandle extends MaybeArray>, + ErrorHandle extends MaybeArray> +> = (Input extends any ? Input : Prettify) & { + /** + * @default 'override' + */ + as?: AsType + /** + * @default 'standalone' + * @since 1.3.0 + */ + schema?: GuardType + + detail?: DocumentDecoration + /** + * Short for 'Content-Type' + * + * Available: + * - 'none': do not parse body + * - 'text' / 'text/plain': parse body as string + * - 'json' / 'application/json': parse body as json + * - 'formdata' / 'multipart/form-data': parse body as form-data + * - 'urlencoded' / 'application/x-www-form-urlencoded: parse body as urlencoded + * - 'arraybuffer': parse body as readable stream + */ + parse?: MaybeArray | ContentType | Parser> + /** + * Transform context's value + */ + transform?: MaybeArray> + /** + * Execute before main handler + */ + beforeHandle?: BeforeHandle + /** + * Execute after main handler + */ + afterHandle?: AfterHandle + /** + * Execute after main handler + */ + mapResponse?: MaybeArray> + /** + * Execute after response is sent + */ + afterResponse?: MaybeArray> + /** + * Catch error + */ + error?: ErrorHandle + tags?: DocumentDecoration['tags'] +} export type ComposedHandler = (context: Context) => MaybePromise @@ -1349,7 +1778,6 @@ export interface InternalRoute { handler: Handler hooks: AnyLocalHook websocket?: AnyWSLocalHook - standaloneValidators?: InputSchema[] } export interface SchemaValidator { @@ -1408,7 +1836,9 @@ export type BaseMacro = Record< string | number | boolean | Object | undefined | null > -export type BaseMacroFn< +export type MaybeValueOrVoidFunction = T | ((...a: any) => void | T) + +export interface MacroProperty< in out TypedRoute extends RouteSchema = {}, in out Singleton extends SingletonBase = { decorator: {} @@ -1417,65 +1847,24 @@ export type BaseMacroFn< resolve: {} }, in out Errors extends Record = {} -> = { - [K in keyof any]: (...a: any) => void | { - onParse?(fn: MaybeArray>): unknown - onParse?( - options: MacroOptions, - fn: MaybeArray> - ): unknown - - onTransform?( - fn: MaybeArray> - ): unknown - onTransform?( - options: MacroOptions, - fn: MaybeArray> - ): unknown - - onBeforeHandle?( - fn: MaybeArray> - ): unknown - onBeforeHandle?( - options: MacroOptions, - fn: MaybeArray> - ): unknown - - onAfterHandle?( - fn: MaybeArray> - ): unknown - onAfterHandle?( - options: MacroOptions, - fn: MaybeArray> - ): unknown - - onError?( - fn: MaybeArray> - ): unknown - onError?( - options: MacroOptions, - fn: MaybeArray> - ): unknown - - mapResponse?( - fn: MaybeArray> - ): unknown - mapResponse?( - options: MacroOptions, - fn: MaybeArray> - ): unknown - - onAfterResponse?( - fn: MaybeArray> - ): unknown - onAfterResponse?( - options: MacroOptions, - fn: MaybeArray> - ): unknown - } +> { + /** + * Deduplication similar to Elysia.constructor.seed + */ + seed?: unknown + parse?: MaybeArray> + transform?: MaybeArray> + beforeHandle?: MaybeArray> + afterHandle?: MaybeArray> + error?: MaybeArray> + mapResponse?: MaybeArray> + afterResponse?: MaybeArray> + resolve?: MaybeArray> + detail?: DocumentDecoration } -export type HookMacroFn< +export interface Macro< + in out Input extends BaseMacro = {}, in out TypedRoute extends RouteSchema = {}, in out Singleton extends SingletonBase = { decorator: {} @@ -1484,47 +1873,20 @@ export type HookMacroFn< resolve: {} }, in out Errors extends Record = {} -> = { - [K in keyof any]: - | { - parse?: MaybeArray> - transform?: MaybeArray> - beforeHandle?: MaybeArray< - OptionalHandler - > - afterHandle?: MaybeArray> - error?: MaybeArray> - mapResponse?: MaybeArray> - afterResponse?: MaybeArray< - AfterResponseHandler - > - resolve?: MaybeArray> - } - | ((...a: any) => { - parse?: MaybeArray> - transform?: MaybeArray> - beforeHandle?: MaybeArray< - OptionalHandler - > - afterHandle?: MaybeArray> - error?: MaybeArray> - mapResponse?: MaybeArray> - afterResponse?: MaybeArray< - AfterResponseHandler - > - resolve?: MaybeArray> - } | void) +> { + [K: keyof any]: MaybeValueOrVoidFunction< + Input & MacroProperty + > } -export type MacroToProperty< - in out T extends BaseMacroFn | HookMacroFn -> = Prettify<{ - [K in keyof T]: T[K] extends Function - ? T[K] extends (a: infer Params) => any - ? Params +export type MacroToProperty> = + Prettify<{ + [K in keyof T]: T[K] extends Function + ? T[K] extends (a: infer Params) => any + ? Params + : boolean : boolean - : boolean -}> + }> interface MacroOptions { insert?: 'before' | 'after' @@ -1541,6 +1903,15 @@ export interface MacroManager< }, in out Errors extends Record = {} > { + body(schema: InputSchema['body']): unknown + headers(schema: InputSchema['headers']): unknown + query(schema: InputSchema['query']): unknown + params(schema: InputSchema['params']): unknown + cookie(schema: InputSchema['cookie']): unknown + response(schema: InputSchema['response']): unknown + + detail(detail: DocumentDecoration): unknown + onParse(fn: MaybeArray>): unknown onParse( options: MacroOptions, @@ -1595,10 +1966,6 @@ export interface MacroManager< } } -export type MacroQueue = HookContainer< - (manager: MacroManager) => unknown -> - type _CreateEden< Path extends string, Property extends Record = {} @@ -1617,65 +1984,115 @@ type RemoveStartingSlash = T extends `/${infer Rest}` ? Rest : T export type CreateEden< Path extends string, Property extends Record = {} -> = Path extends '' | '/' - ? Property - : _CreateEden, Property> - -export type ComposeElysiaResponse< - Schema extends RouteSchema, - Handle -> = Handle extends (...a: any[]) => infer A - ? _ComposeElysiaResponse> - : _ComposeElysiaResponse> +> = Path extends `/${infer Rest}` + ? _CreateEden + : Path extends '' | '/' + ? Property + : _CreateEden -export type EmptyRouteSchema = { +export interface EmptyRouteSchema { body: unknown headers: unknown query: unknown params: {} cookie: unknown - response: {} + response: unknown } -type _ComposeElysiaResponse = Prettify< - (Schema['response'] extends { 200: any } - ? { - 200: Replace - } - : { - 200: Handle extends AnyElysiaCustomStatusResponse - ? - | Exclude - | Extract< - Handle, - ElysiaCustomStatusResponse<200, any, 200> - >['response'] - : Handle extends Generator - ? AsyncGenerator - : Handle extends ReadableStream - ? AsyncGenerator - : Replace - }) & - ExtractErrorFromHandle & - ({} extends Omit - ? {} - : Omit) & - (EmptyRouteSchema extends Schema +type Extract200 = T extends AnyElysiaCustomStatusResponse + ? + | Exclude + | Extract>['response'] + : T + +export type IsUnknown = [unknown] extends [T] + ? IsAny extends true + ? false + : true + : false + +export type ValueToResponseSchema = ExtractErrorFromHandle & + (Extract200 extends infer R200 + ? undefined extends R200 ? {} - : { - 422: { - type: 'validation' - on: string - summary?: string - message?: string - found?: unknown - property?: string - expected?: string - } - }) + : IsNever extends true + ? {} + : { 200: R200 } + : {}) + +export type ValueOrFunctionToResponseSchema = T extends ( + ...a: any +) => MaybePromise + ? ValueToResponseSchema + : ValueToResponseSchema + +export type ElysiaHandlerToResponseSchema = + Prettify< + Handle extends (...a: any) => MaybePromise + ? ValueToResponseSchema> + : {} + > + +export type ElysiaHandlerToResponseSchemas< + Handle extends Function[], + Carry extends PossibleResponse = {} +> = Handle extends [infer Current, ...infer Rest] + ? ElysiaHandlerToResponseSchemas< + // @ts-ignore Trust me bro + Rest, + // @ts-ignore trust me bro + UnionResponseStatus, Carry> + > + : Prettify + +export type ElysiaHandlerToResponseSchemaAmbiguous< + Schemas extends MaybeArray +> = + MaybeArray<(...a: any) => any> extends Schemas + ? {} + : Schemas extends Function + ? ElysiaHandlerToResponseSchema + : Schemas extends Function[] + ? ElysiaHandlerToResponseSchemas + : {} + +type ReconcileStatus< + A extends Record, + B extends Record +> = { + // @ts-ignore Trust me bro + [K in keyof A | keyof B]: K extends keyof A ? A[K] : B[K] +} + +export type ComposeElysiaResponse< + in out Schema extends RouteSchema, + in out Handle, + in out Possibility extends PossibleResponse +> = Prettify< + ReconcileStatus< + // @ts-ignore + Schema['response'], + UnionResponseStatus< + ValueOrFunctionToResponseSchema, + Possibility & + (EmptyRouteSchema extends Pick + ? {} + : { + 422: { + type: 'validation' + on: string + summary?: string + message?: string + found?: unknown + property?: string + expected?: string + } + }) + > + > > -type ExtractErrorFromHandle = { +export type ExtractErrorFromHandle = { [ErrorResponse in Extract< Handle, AnyElysiaCustomStatusResponse @@ -1703,18 +2120,21 @@ export type MergeElysiaInstances< macro: {} macroFn: {} parser: {} + response: {} }, Ephemeral extends EphemeralType = { derive: {} resolve: {} schema: {} standaloneSchema: {} + response: {} }, Volatile extends EphemeralType = { derive: {} resolve: {} schema: {} standaloneSchema: {} + response: {} }, Routes extends RouteBase = {} > = Instances extends [ @@ -1747,10 +2167,19 @@ export type MergeElysiaInstances< export type LifeCycleType = 'global' | 'local' | 'scoped' export type GuardSchemaType = 'override' | 'standalone' +type PartialIf = Condition extends true + ? Partial + : T + // Exclude return error() -export type ExcludeElysiaResponse = Exclude< - undefined extends Awaited ? Partial> : Awaited, - AnyElysiaCustomStatusResponse +export type ExcludeElysiaResponse = PartialIf< + Exclude, AnyElysiaCustomStatusResponse> extends infer A + ? IsNever extends true + ? {} + : // Intersect all union and fallback never to {} + UnionToIntersect + : {}, + undefined extends Awaited ? true : false > export type InferContext< @@ -1771,12 +2200,11 @@ export type InferHandler< Path extends string = T['~Prefix'], Schema extends RouteSchema = T['~Metadata']['schema'] > = InlineHandler< - MergeSchema, + MergeSchema, T['~Singleton'] & { derive: T['~Ephemeral']['derive'] & T['~Volatile']['derive'] resolve: T['~Ephemeral']['resolve'] & T['~Volatile']['resolve'] - }, - Path + } > export interface ModelValidatorError extends ValueError { @@ -1802,23 +2230,6 @@ export type UnionToIntersect = ( ? I : never -export type ResolveMacroContext< - Macro extends BaseMacro, - MacroFn extends BaseMacroFn -> = UnionToIntersect< - { - [K in keyof Macro]-?: undefined extends Macro[K] - ? never - : K extends keyof MacroFn - ? ReturnType extends infer A extends { - [K in keyof any]: any - } - ? A - : never - : never - }[keyof Macro] -> - export type ContextAppendType = 'append' | 'override' // new Elysia() @@ -2052,3 +2463,45 @@ export type SSEPayload< /** data to send */ data?: Data } + +export type UnionResponseStatus = {} extends A + ? B + : {} extends B + ? A + : { + [key in keyof A | keyof B]: key extends keyof A + ? key extends keyof B + ? A[key] | B[key] + : A[key] + : key extends keyof B + ? B[key] + : never + } + +export type CreateEdenResponse< + Path extends string, + Schema extends RouteSchema, + MacroContext extends RouteSchema, + // This should be handled by ComposeElysiaResponse + Res extends PossibleResponse +> = RouteSchema extends MacroContext + ? { + body: Schema['body'] + params: IsNever extends true + ? ResolvePath + : Schema['params'] + query: Schema['query'] + headers: Schema['headers'] + response: Res + } + : { + body: Prettify + params: IsNever< + keyof (Schema['params'] & MacroContext['params']) + > extends true + ? ResolvePath + : Prettify + query: Prettify + headers: Prettify + response: Res + } diff --git a/src/utils.ts b/src/utils.ts index 37b35d97a..1ebf84224 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,4 @@ +import { isStringTextContainingNode } from 'typescript' import type { Sucrose } from './sucrose' import type { TraceHandler } from './trace' @@ -20,7 +21,8 @@ import type { SchemaValidator, AnyLocalHook, SSEPayload, - Prettify + Prettify, + RouteSchema } from './types' import { ElysiaFile } from './universal/file' @@ -56,16 +58,28 @@ export const mergeDeep = < options?: { skipKeys?: string[] override?: boolean + mergeArray?: boolean } ): A & B => { const skipKeys = options?.skipKeys const override = options?.override ?? true + const mergeArray = options?.mergeArray ?? true if (!isObject(target) || !isObject(source)) return target as A & B for (const [key, value] of Object.entries(source)) { if (skipKeys?.includes(key)) continue + if (mergeArray && Array.isArray(value)) { + target[key as keyof typeof target] = Array.isArray( + (target as any)[key] + ) + ? [...(target as any)[key], ...value] + : (target[key as keyof typeof target] = value as any) + + continue + } + if (!isObject(value) || !(key in target) || isClass(value)) { if (override || !(key in target)) target[key as keyof typeof target] = value @@ -76,7 +90,7 @@ export const mergeDeep = < target[key as keyof typeof target] = mergeDeep( (target as any)[key] as any, value, - { skipKeys, override } + { skipKeys, override, mergeArray } ) } @@ -87,7 +101,8 @@ export const mergeCookie = ( b: B ): A & B => { const v = mergeDeep(Object.assign({}, a), b, { - skipKeys: ['properties'] + skipKeys: ['properties'], + mergeArray: false }) as A & B // @ts-expect-error @@ -556,6 +571,7 @@ export const StatusMap = { 'Range Not Satisfiable': 416, 'Expectation Failed': 417, "I'm a teapot": 418, + 'Enhance Your Calm': 420, 'Misdirected Request': 421, 'Unprocessable Content': 422, Locked: 423, @@ -644,119 +660,31 @@ export const unsignCookie = async (input: string, secret: string | null) => { return expectedInput === input ? tentativeValue : false } -export const traceBackMacro = ( - extension: unknown, - property: Record, - manage: ReturnType +export const insertStandaloneValidator = ( + hook: { standaloneValidator: InputSchema[] }, + name: Name, + value: InputSchema[Name] ) => { - if (!extension || typeof extension !== 'object' || !property) return - - for (const [key, value] of Object.entries(property)) { - if (primitiveHookMap[key] || !(key in extension)) continue - - const v = extension[ - key as unknown as keyof typeof extension - ] as BaseMacro[string] - - if (typeof v === 'function') { - const hook = v(value) - - if (typeof hook === 'object') - for (const [k, v] of Object.entries(hook)) - manage(k as keyof LifeCycleStore)({ - fn: v as any - }) - } - - delete property[key as unknown as keyof typeof extension] - } -} - -export const createMacroManager = - ({ - globalHook, - localHook - }: { - globalHook: Partial - localHook: Partial - }) => - (stackName: keyof LifeCycleStore) => - ( - type: - | { - insert?: 'before' | 'after' - stack?: 'global' | 'local' - } - | MaybeArray, - fn?: MaybeArray - ) => { - if (typeof type === 'function') - type = { - fn: type - } - - // @ts-expect-error this is available in macro v2 - if (stackName === 'resolve') { - type = { - ...type, - subType: 'resolve' + if ( + !hook.standaloneValidator?.length || + !Array.isArray(hook.standaloneValidator) + ) { + hook.standaloneValidator = [ + { + [name]: value } - } - - if (!localHook[stackName]) localHook[stackName] = [] - if (typeof localHook[stackName] === 'function') - localHook[stackName] = [localHook[stackName]] - if (!Array.isArray(localHook[stackName])) - localHook[stackName] = [localHook[stackName]] - - if ('fn' in type || Array.isArray(type)) { - if (Array.isArray(type)) - localHook[stackName] = ( - localHook[stackName] as unknown[] - ).concat(type) as any - else localHook[stackName].push(type) - - return - } - - const { insert = 'after', stack = 'local' } = type + ] + return + } - if (typeof fn === 'function') fn = { fn } + const last = hook.standaloneValidator[hook.standaloneValidator.length - 1] - if (stack === 'global') { - if (!Array.isArray(fn)) { - if (insert === 'before') { - ;(globalHook[stackName] as any[]).unshift(fn) - } else { - ;(globalHook[stackName] as any[]).push(fn) - } - } else { - if (insert === 'before') { - globalHook[stackName] = fn.concat( - globalHook[stackName] as any - ) as any - } else { - globalHook[stackName] = ( - globalHook[stackName] as any[] - ).concat(fn) - } - } - } else { - if (!Array.isArray(fn)) { - if (insert === 'before') { - ;(localHook[stackName] as any[]).unshift(fn) - } else { - ;(localHook[stackName] as any[]).push(fn) - } - } else { - if (insert === 'before') { - localHook[stackName] = fn.concat(localHook[stackName]) - } else { - localHook[stackName] = localHook[stackName].concat(fn) - } - } - } - } + if (name in last) + hook.standaloneValidator.push({ + [name]: value + }) + else last[name] = value +} const parseNumericString = (message: string | number): number | null => { if (typeof message === 'number') return message @@ -1209,3 +1137,27 @@ export const sse = < return payload as any } + +export async function getResponseLength(response: Response) { + if (response.bodyUsed || !response.body) return 0 + + let length = 0 + const reader = response.body.getReader() + + while (true) { + const { done, value } = await reader.read() + if (done) break + length += value.byteLength + } + + return length +} + +export const emptySchema = { + headers: true, + cookie: true, + query: true, + params: true, + body: true, + response: true +} as const satisfies RouteSchema diff --git a/src/ws/types.ts b/src/ws/types.ts index 1d8b182b3..5a9c9e22e 100644 --- a/src/ws/types.ts +++ b/src/ws/types.ts @@ -121,48 +121,46 @@ export type WSParseHandler = ( message: unknown ) => MaybePromise -export type AnyWSLocalHook = WSLocalHook +export type AnyWSLocalHook = WSLocalHook export type WSLocalHook< - LocalSchema extends InputSchema, + Input extends BaseMacro, Schema extends RouteSchema, Singleton extends SingletonBase, - Macro extends MetadataBase['macro'] -> = Prettify & - (LocalSchema extends any ? LocalSchema : Prettify) & { - detail?: DocumentDecoration - /** - * Headers to register to websocket before `upgrade` - */ - upgrade?: Record | ((context: Context) => unknown) - parse?: MaybeArray> +> = Prettify & { + detail?: DocumentDecoration + /** + * Headers to register to websocket before `upgrade` + */ + upgrade?: Record | ((context: Context) => unknown) + parse?: MaybeArray> - /** - * Transform context's value - */ - transform?: MaybeArray> - /** - * Execute before main handler - */ - beforeHandle?: MaybeArray> - /** - * Execute after main handler - */ - afterHandle?: MaybeArray> - /** - * Execute after main handler - */ - mapResponse?: MaybeArray> - /** - * Execute after response is sent - */ - afterResponse?: MaybeArray> - /** - * Catch error - */ - error?: MaybeArray> - tags?: DocumentDecoration['tags'] - } & TypedWebSocketHandler< + /** + * Transform context's value + */ + transform?: MaybeArray> + /** + * Execute before main handler + */ + beforeHandle?: MaybeArray> + /** + * Execute after main handler + */ + afterHandle?: MaybeArray> + /** + * Execute after main handler + */ + mapResponse?: MaybeArray> + /** + * Execute after response is sent + */ + afterResponse?: MaybeArray> + /** + * Catch error + */ + error?: MaybeArray> + tags?: DocumentDecoration['tags'] +} & TypedWebSocketHandler< Omit, 'body'> & { body: never }, diff --git a/test/adapter/web-standard/map-early-response.test.ts b/test/adapter/web-standard/map-early-response.test.ts index 607a817bf..527a9cc5a 100644 --- a/test/adapter/web-standard/map-early-response.test.ts +++ b/test/adapter/web-standard/map-early-response.test.ts @@ -235,6 +235,7 @@ describe('Web Standard - Map Early Response', () => { expect(response).toBeInstanceOf(Response) expect(await response?.text()).toEqual('Shiroko') + // @ts-ignore expect(response?.headers.toJSON()).toEqual({ ...headers, name: 'Himari' diff --git a/test/adapter/web-standard/map-response.test.ts b/test/adapter/web-standard/map-response.test.ts index 309a99ef0..ae827fc7f 100644 --- a/test/adapter/web-standard/map-response.test.ts +++ b/test/adapter/web-standard/map-response.test.ts @@ -276,6 +276,7 @@ describe('Web Standard - Map Response', () => { expect(response).toBeInstanceOf(Response) expect(await response.text()).toEqual('Shiroko') + // @ts-ignore expect(response.headers.toJSON()).toEqual({ ...headers, name: 'Himari' diff --git a/test/aot/analysis.test.ts b/test/aot/analysis.test.ts index 10ef0cca6..69851ec43 100644 --- a/test/aot/analysis.test.ts +++ b/test/aot/analysis.test.ts @@ -102,7 +102,7 @@ describe('Static code analysis', () => { } const app = new Elysia().post('/json', (c) => c.body, { - type: 'json' + parse: 'json' }) const res = await app.handle(post('/json', body)).then((x) => x.json()) diff --git a/test/aot/response.test.ts b/test/aot/response.test.ts index f3a5ed309..c29b33abb 100644 --- a/test/aot/response.test.ts +++ b/test/aot/response.test.ts @@ -6,7 +6,6 @@ import { signCookie } from '../../src/utils' const secrets = 'We long for the seven wailings. We bear the koan of Jericho.' const getCookies = (response: Response) => - // @ts-expect-error response.headers.getAll('Set-Cookie').map((x) => { return decodeURIComponent(x) }) diff --git a/test/cookie/response.test.ts b/test/cookie/response.test.ts index 96f97e4fe..f7126b235 100644 --- a/test/cookie/response.test.ts +++ b/test/cookie/response.test.ts @@ -285,9 +285,7 @@ describe('Cookie Response', () => { const response = await app.handle( req('/council', { headers: { - cookie: - 'council=' + - encodeURIComponent(JSON.stringify(expected)) + cookie: 'council=' + JSON.stringify(expected) } }) ) @@ -296,32 +294,33 @@ describe('Cookie Response', () => { expect(await response.json()).toEqual(expected) }) - it("don't parse cookie type unless specified", async () => { - let value: string | undefined - - const app = new Elysia().get( - '/council', - ({ cookie: { council } }) => (value = council.value) - ) - - const expected = { - name: 'Rin', - affilation: 'Administration' - } - - const response = await app.handle( - req('/council', { - headers: { - cookie: - 'council=' + - encodeURIComponent(JSON.stringify(expected)) - } - }) - ) - - expect(response.status).toBe(200) - expect(value).toEqual(JSON.stringify(expected)) - }) + // this is removed there's no way to accurately determine object on Standard Schema + // it("don't parse cookie type unless specified", async () => { + // let value: unknown + + // const app = new Elysia().get( + // '/council', + // ({ cookie: { council } }) => (value = council.value) + // ) + + // const expected = { + // name: 'Rin', + // affilation: 'Administration' + // } + + // const response = await app.handle( + // req('/council', { + // headers: { + // cookie: + // 'council=' + + // encodeURIComponent(JSON.stringify(expected)) + // } + // }) + // ) + + // expect(response.status).toBe(200) + // expect(value).toEqual(JSON.stringify(expected)) + // }) it('handle optional at root', async () => { const app = new Elysia().get('/', ({ cookie: { id } }) => id.value, { @@ -355,7 +354,6 @@ describe('Cookie Response', () => { return 'a' }) - // @ts-expect-error const res = app.handle(req('/')).then((x) => x.headers.toJSON()) // @ts-expect-error @@ -392,13 +390,14 @@ describe('Cookie Response', () => { }) .get('/', () => 'Hello, world!') - const res = await app.handle( - new Request('http://localhost:3000/', { - headers: { - cookie: 'test=Hello, world!' - } - }) - ) + const res = await app + .handle( + new Request('http://localhost:3000/', { + headers: { + cookie: 'test=Hello, world!' + } + }) + ) .then((x) => x.headers) expect(res.getSetCookie()).toEqual([]) diff --git a/test/core/elysia.test.ts b/test/core/elysia.test.ts index b844dd7b2..f5363a150 100644 --- a/test/core/elysia.test.ts +++ b/test/core/elysia.test.ts @@ -123,7 +123,6 @@ describe('Edge Case', () => { const response = await app .handle(req('/')) - // @ts-expect-error .then((x) => x.headers.toJSON()) expect(response['set-cookie']).toHaveLength(1) @@ -388,4 +387,34 @@ describe('Edge Case', () => { expect(response.status).toBe(200) }) + + it('automatically handle HEAD request for GET static path', async () => { + const app = new Elysia().get('/', () => 'hello world') + + const response = await app.handle( + new Request('http://localhost', { + method: 'HEAD' + }) + ) + + expect(response.status).toBe(200) + expect(response.headers.toJSON()).toEqual({ + 'content-length': '11', + }) + }) + + it('automatically handle HEAD request for GET dynamic path', async () => { + const app = new Elysia().get('/:id', () => 'hello world') + + const response = await app.handle( + new Request('http://localhost/1', { + method: 'HEAD' + }) + ) + + expect(response.status).toBe(200) + expect(response.headers.toJSON()).toEqual({ + 'content-length': '11', + }) + }) }) diff --git a/test/core/handle-error.test.ts b/test/core/handle-error.test.ts index fc4a946c5..1122f4a7c 100644 --- a/test/core/handle-error.test.ts +++ b/test/core/handle-error.test.ts @@ -1,4 +1,4 @@ -import { Elysia, InternalServerError, NotFoundError, error, t } from '../../src' +import { Elysia, InternalServerError, NotFoundError, status, t } from '../../src' import { describe, expect, it } from 'bun:test' import { req } from '../utils' @@ -142,8 +142,8 @@ describe('Handle Error', () => { }) it('handle thrown error function', async () => { - const app = new Elysia().get('/', () => { - throw error(404, 'Not Found :(') + const app = new Elysia().get('/', ({ status }) => { + throw status(404, 'Not Found :(') }) const response = await app.handle(req('/')) @@ -153,8 +153,8 @@ describe('Handle Error', () => { }) it('handle thrown Response', async () => { - const app = new Elysia().get('/', () => { - throw error(404, 'Not Found :(') + const app = new Elysia().get('/', ({ status }) => { + throw status(404, 'Not Found :(') }) const response = await app.handle(req('/')) @@ -253,8 +253,8 @@ describe('Handle Error', () => { const response: Response = await new Elysia() .get('/', () => 'Hello', { - beforeHandle({ error }) { - throw error("I'm a teapot", { message: 'meow!' }) + beforeHandle({ status }) { + throw status("I'm a teapot", { message: 'meow!' }) } }) // @ts-expect-error private property @@ -265,7 +265,7 @@ describe('Handle Error', () => { headers: {} } }, - error(422, value) as any + status(422, value) as any ) expect(await response.json()).toEqual(value) @@ -288,8 +288,8 @@ describe('Handle Error', () => { }) }) .get('/', () => 'Hello', { - beforeHandle({ error }) { - throw error("I'm a teapot", { message: 'meow!' }) + beforeHandle({ status }) { + throw status("I'm a teapot", { message: 'meow!' }) } }) // @ts-expect-error private property @@ -300,7 +300,7 @@ describe('Handle Error', () => { headers: {} } }, - error(422, value) as any + status(422, value) as any ) expect(await response.text()).toBe('Don Quixote') diff --git a/test/core/mount.test.ts b/test/core/mount.test.ts index 1e523f352..0f44df444 100644 --- a/test/core/mount.test.ts +++ b/test/core/mount.test.ts @@ -119,7 +119,6 @@ describe('Mount', () => { it('preserve headers', async () => { const app = new Elysia().mount((request) => { - // @ts-expect-error Bun has toJSON return Response.json(request.headers.toJSON()) }) diff --git a/test/core/native-static.test.ts b/test/core/native-static.test.ts index 0acbff6f2..227f121ae 100644 --- a/test/core/native-static.test.ts +++ b/test/core/native-static.test.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import { Elysia } from '../../src' import { describe, expect, it } from 'bun:test' diff --git a/test/core/normalize.test.ts b/test/core/normalize.test.ts index 312783c32..c4355690e 100644 --- a/test/core/normalize.test.ts +++ b/test/core/normalize.test.ts @@ -75,7 +75,8 @@ describe('Normalize', () => { it('normalize multiple response', async () => { const app = new Elysia().get( '/', - ({ error }) => error(418, { name: 'Nagisa', hifumi: 'daisuki' }), + // @ts-ignore + ({ status }) => status(418, { name: 'Nagisa', hifumi: 'daisuki' }), { response: { 200: t.Object({ @@ -100,7 +101,8 @@ describe('Normalize', () => { normalize: false }).get( '/', - ({ error }) => error(418, { name: 'Nagisa', hifumi: 'daisuki' }), + // @ts-ignore + ({ status }) => status(418, { name: 'Nagisa', hifumi: 'daisuki' }), { response: { 200: t.Object({ diff --git a/test/extends/models.test.ts b/test/extends/models.test.ts index 4e53787d2..936d30d46 100644 --- a/test/extends/models.test.ts +++ b/test/extends/models.test.ts @@ -125,8 +125,8 @@ describe('Model', () => { }) }) .post('/arr', ({ body }) => body, { - response: 'number[]', - body: 'number[]', + response: 'number', + body: 'number', }) const correct = await app.handle( @@ -149,7 +149,7 @@ describe('Model', () => { headers: { 'content-type': 'application/json' }, - body: JSON.stringify([1, 2]) + body: 1 }) ) @@ -332,14 +332,14 @@ describe('Model', () => { 400: 'res' } }) - .get('/400', ({ error }) => error(400, 'ok'), { + .get('/400', ({ status }) => status(400, 'ok'), { response: { 200: 'res', 400: 'res' } }) // @ts-expect-error - .get('/error', ({ error }) => error(400, 1), { + .get('/error', ({ status }) => status(400, 1), { response: { 200: 'res', 400: 'res' diff --git a/test/lifecycle/after-handle.test.ts b/test/lifecycle/after-handle.test.ts index d1a5dc40a..53d6d14ae 100644 --- a/test/lifecycle/after-handle.test.ts +++ b/test/lifecycle/after-handle.test.ts @@ -88,6 +88,24 @@ describe('After Handle', () => { const app = new Elysia().get('/', () => 'NOOP', { afterHandle({ response }) { return response + }, + mapResponse() { + + } + }) + + const res = await app.handle(req('/')).then((x) => x.text()) + + expect(res).toBe('NOOP') + }) + + it('accept responseValue', async () => { + const app = new Elysia().get('/', () => 'NOOP', { + afterHandle({ responseValue }) { + return responseValue + }, + mapResponse() { + } }) diff --git a/test/lifecycle/derive.test.ts b/test/lifecycle/derive.test.ts index de8f2ff21..451639226 100644 --- a/test/lifecycle/derive.test.ts +++ b/test/lifecycle/derive.test.ts @@ -1,4 +1,4 @@ -import { Elysia, error } from '../../src' +import { Elysia } from '../../src' import { describe, expect, it } from 'bun:test' import { req } from '../utils' @@ -182,9 +182,7 @@ describe('derive', () => { it('handle error', async () => { const app = new Elysia() - .derive(() => { - return error(418) - }) + .derive(({ status }) => status(418)) .get('/', () => '') const res = await app.handle(req('/')).then((x) => x.text()) diff --git a/test/lifecycle/error.test.ts b/test/lifecycle/error.test.ts index 23c285c67..7eb7b974b 100644 --- a/test/lifecycle/error.test.ts +++ b/test/lifecycle/error.test.ts @@ -4,7 +4,6 @@ import { InternalServerError, ParseError, ValidationError, - error, t, validationDetail } from '../../src' @@ -127,8 +126,8 @@ describe('error', () => { it.each([true, false])( 'return correct number status on error function with aot: %p', async (aot) => { - const app = new Elysia({ aot }).get('/', ({ error }) => - error(418, 'I am a teapot') + const app = new Elysia({ aot }).get('/', ({ status }) => + status(418, 'I am a teapot') ) const response = await app.handle(req('/')) @@ -140,8 +139,8 @@ describe('error', () => { it.each([true, false])( 'return correct named status on error function with aot: %p', async (aot) => { - const app = new Elysia({ aot }).get('/', ({ error }) => - error("I'm a teapot", 'I am a teapot') + const app = new Elysia({ aot }).get('/', ({ status }) => + status("I'm a teapot", 'I am a teapot') ) const response = await app.handle(req('/')) @@ -153,7 +152,7 @@ describe('error', () => { it.each([true, false])( 'return correct number status without value on error function with aot: %p', async (aot) => { - const app = new Elysia({ aot }).get('/', ({ error }) => error(418)) + const app = new Elysia({ aot }).get('/', ({ status }) => status(418)) const response = await app.handle(req('/')) @@ -165,8 +164,8 @@ describe('error', () => { it.each([true, false])( 'return correct named status without value on error function with aot: %p', async (aot) => { - const app = new Elysia({ aot }).get('/', ({ error }) => - error("I'm a teapot") + const app = new Elysia({ aot }).get('/', ({ status }) => + status("I'm a teapot") ) const response = await app.handle(req('/')) diff --git a/test/lifecycle/hook-types.test.ts b/test/lifecycle/hook-types.test.ts index 8b001ef04..250b04256 100644 --- a/test/lifecycle/hook-types.test.ts +++ b/test/lifecycle/hook-types.test.ts @@ -13,19 +13,19 @@ describe('Hook Types', () => { } ) - expect(plugin.event.transform[0].scope).toBe('scoped') + expect(plugin.event.transform?.[0].scope).toBe('scoped') const a = new Elysia().use(plugin).get('/foo', ({ id }) => { return { id, name: 'foo' } }) - expect(plugin.event.transform[0].scope).toBe('scoped') + expect(plugin.event.transform?.[0].scope).toBe('scoped') const b = new Elysia().use(plugin).get('/bar', ({ id }) => { return { id, name: 'bar' } }) - expect(plugin.event.transform[0].scope).toBe('scoped') + expect(plugin.event.transform?.[0].scope).toBe('scoped') const [res1, res2] = await Promise.all([ a.handle(req('/foo')).then((x) => x.json()), diff --git a/test/lifecycle/map-response.test.ts b/test/lifecycle/map-response.test.ts index 8b23ef65b..6477ae13c 100644 --- a/test/lifecycle/map-response.test.ts +++ b/test/lifecycle/map-response.test.ts @@ -103,6 +103,19 @@ describe('Map Response', () => { expect(res).toBe('Hutao') }) + it('inherit response using responseValue', async () => { + const app = new Elysia().get('/', () => 'Hu', { + mapResponse({ responseValue }) { + if (typeof responseValue === 'string') + return new Response(responseValue + 'tao') + } + }) + + const res = await app.handle(req('/')).then((x) => x.text()) + + expect(res).toBe('Hutao') + }) + it('inherit set', async () => { const app = new Elysia().get('/', () => 'Hu', { mapResponse({ response, set }) { @@ -123,6 +136,26 @@ describe('Map Response', () => { expect(res.get('X-Series')).toBe('Genshin') }) + it('inherit set using responseValue', async () => { + const app = new Elysia().get('/', () => 'Hu', { + mapResponse({ responseValue, set }) { + set.headers['X-Powered-By'] = 'Elysia' + + if (typeof responseValue === 'string') + return new Response(responseValue + 'tao', { + headers: { + 'X-Series': 'Genshin' + } + }) + } + }) + + const res = await app.handle(req('/')).then((x) => x.headers) + + expect(res.get('X-Powered-By')).toBe('Elysia') + expect(res.get('X-Series')).toBe('Genshin') + }) + it('return async', async () => { const app = new Elysia() .mapResponse(async () => new Response('A')) @@ -238,12 +271,46 @@ describe('Map Response', () => { expect(response).toBe('aru') }) + it('mapResponse in error using responseValue', async () => { + class CustomClass { + constructor(public name: string) {} + } + + const app = new Elysia() + .trace(() => {}) + .onError(() => new CustomClass('aru')) + .mapResponse(({ responseValue }) => { + if (responseValue instanceof CustomClass) + return new Response(responseValue.name) + }) + .get('/', () => { + throw new Error('Hello') + }) + + const response = await app.handle(req('/')).then((x) => x.text()) + + expect(response).toBe('aru') + }) + // https://github.com/elysiajs/elysia/issues/965 it('mapResponse with after handle', async () => { const app = new Elysia() .onAfterHandle(() => {}) .mapResponse((context) => { - return context.response + return new Response(context.response + '') + }) + .get('/', async () => 'aru') + + const response = await app.handle(req('/')).then((x) => x.text()) + + expect(response).toBe('aru') + }) + + it('mapResponse with after handle using responseValue', async () => { + const app = new Elysia() + .onAfterHandle(() => {}) + .mapResponse((context) => { + return new Response(context.responseValue + '') }) .get('/', async () => 'aru') diff --git a/test/lifecycle/parser.test.ts b/test/lifecycle/parser.test.ts index 605b84165..fc1f5547e 100644 --- a/test/lifecycle/parser.test.ts +++ b/test/lifecycle/parser.test.ts @@ -428,6 +428,7 @@ describe('Parser', () => { const app = new Elysia() .onError((ctx) => { + // @ts-ignore code = ctx.code }) .post('/', () => '', { diff --git a/test/lifecycle/resolve.test.ts b/test/lifecycle/resolve.test.ts index ff7c151fc..266be95cf 100644 --- a/test/lifecycle/resolve.test.ts +++ b/test/lifecycle/resolve.test.ts @@ -1,4 +1,4 @@ -import { Elysia, error } from '../../src' +import { Elysia } from '../../src' import { describe, expect, it } from 'bun:test' import { req } from '../utils' @@ -211,8 +211,8 @@ describe('resolve', () => { it('handle error', async () => { const route = new Elysia() - .resolve(() => { - return error(418) + .resolve(({ status }) => { + return status(418) }) .get('/', () => '') diff --git a/test/lifecycle/response.test.ts b/test/lifecycle/response.test.ts index bd5e7fea5..3641a50fe 100644 --- a/test/lifecycle/response.test.ts +++ b/test/lifecycle/response.test.ts @@ -60,22 +60,57 @@ describe('On After Response', () => { expect(order).toEqual(['A', 'B']) }) - // it('inherits from plugin', async () => { - // const transformType = new Elysia().onResponse( - // { as: 'global' }, - // ({ response }) => { - // if (response === 'string') return 'number' - // } - // ) - - // const app = new Elysia() - // .use(transformType) - // .get('/id/:id', ({ params: { id } }) => typeof id) - - // const res = await app.handle(req('/id/1')) - - // expect(await res.text()).toBe('number') - // }) + it('inherits from plugin', async () => { + let type = '' + + const afterResponse = new Elysia().onAfterResponse( + { as: 'global' }, + ({ response }) => { + type = typeof response + } + ) + + const app = new Elysia() + .use(afterResponse) + .get('/id/:id', ({ params: { id } }) => id, { + params: t.Object({ + id: t.Number() + }) + }) + + await app.handle(req('/id/1')) + + // wait for next tick + await Bun.sleep(1) + + expect(type).toBe('number') + }) + + it('inherits from plugin using responseValue', async () => { + let type = '' + + const afterResponse = new Elysia().onAfterResponse( + { as: 'global' }, + ({ responseValue }) => { + type = typeof responseValue + } + ) + + const app = new Elysia() + .use(afterResponse) + .get('/id/:id', ({ params: { id } }) => id, { + params: t.Object({ + id: t.Number() + }) + }) + + await app.handle(req('/id/1')) + + // wait for next tick + await Bun.sleep(1) + + expect(type).toBe('number') + }) it('as global', async () => { const called = [] diff --git a/test/lifecycle/transform.test.ts b/test/lifecycle/transform.test.ts index 34fb0ee31..51693a5c8 100644 --- a/test/lifecycle/transform.test.ts +++ b/test/lifecycle/transform.test.ts @@ -41,17 +41,13 @@ describe('Transform', () => { it('group transform', async () => { const app = new Elysia() - .group('/scoped', (app) => + .group('/scoped/id/:id', (app) => app - .onTransform<{ - params: { - id: number - } | null - }>((request) => { - if (request.params?.id) - request.params.id = +request.params.id + .onTransform(({ params }) => { + // @ts-ignore + if (params.id) params.id = +params.id }) - .get('/id/:id', ({ params: { id } }) => typeof id) + .get('', ({ params: { id } }) => typeof id) ) .get('/id/:id', ({ params: { id } }) => typeof id) @@ -71,6 +67,7 @@ describe('Transform', () => { }, 'global' >({ as: 'global' }, (request) => { + // @ts-ignore if (request.params?.id) request.params.id = +request.params.id }) diff --git a/test/macro/macro.test.ts b/test/macro/macro.test.ts index c3a219e67..73d057016 100644 --- a/test/macro/macro.test.ts +++ b/test/macro/macro.test.ts @@ -1,18 +1,19 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { describe, it, expect } from 'bun:test' -import Elysia, { error, t } from '../../src' -import { req } from '../utils' +import Elysia, { t } from '../../src' +import { post, req } from '../utils' +import { status } from '../../dist/cjs' describe('Macro', () => { - it('work', async () => { + it('trace back', async () => { let answer: string | undefined const app = new Elysia() - .macro(() => ({ + .macro({ hi(config: string) { answer = config } - })) + }) .get('/', () => 'Hello World', { hi: 'Hello World' }) @@ -22,268 +23,226 @@ describe('Macro', () => { expect(answer).toBe('Hello World') }) - it('accept function', async () => { - let answer: string | undefined - + it('work', async () => { const app = new Elysia() - .macro(() => ({ - hi(fn: () => any) { - fn() + .macro({ + hi(beforeHandle: () => any) { + return { + beforeHandle + } } - })) + }) .get('/', () => 'Hello World', { - hi() { - answer = 'Hello World' - } + hi: () => 'Hello World' }) - await app.handle(req('/')) + const response = await app.handle(req('/')).then((x) => x.text()) - expect(answer).toBe('Hello World') + expect(response).toBe('Hello World') }) - it('create custom life-cycle', async () => { + it('appends parse', async () => { const app = new Elysia() - .macro(({ onBeforeHandle }) => ({ + .macro({ hi(fn: () => any) { - onBeforeHandle(fn) + return { + parse: fn + } } - })) + }) .get('/', () => 'Hello World', { - hi: () => 'Hello World' + hi: () => {} }) - const response = await app.handle(req('/')).then((x) => x.text()) - - expect(response).toBe('Hello World') + expect(app.router.history[0].hooks.parse?.length).toEqual(1) }) - it('insert after on local stack by default', async () => { - const orders: number[] = [] - + it('appends parse array', async () => { const app = new Elysia() - .macro(({ onBeforeHandle }) => ({ + .macro({ hi(fn: () => any) { - onBeforeHandle(fn) + return { + parse: [fn, () => {}] + } } - })) - .onBeforeHandle(() => { - orders.push(1) }) .get('/', () => 'Hello World', { - beforeHandle() { - orders.push(2) - }, - hi: () => { - orders.push(3) - } + hi: () => {} }) - await app.handle(req('/')) - - expect(orders).toEqual([1, 2, 3]) + expect(app.router.history[0].hooks.parse?.length).toEqual(2) }) - it('insert after on local stack', async () => { - const orders: number[] = [] - + it('appends transform', async () => { const app = new Elysia() - .macro(({ onBeforeHandle }) => ({ + .macro({ hi(fn: () => any) { - onBeforeHandle({ insert: 'after', stack: 'local' }, fn) + return { + transform: fn + } } - })) - .onBeforeHandle(() => { - orders.push(1) }) .get('/', () => 'Hello World', { - beforeHandle() { - orders.push(2) - }, - hi: () => { - orders.push(3) - } + hi: () => {} }) - await app.handle(req('/')) - - expect(orders).toEqual([1, 2, 3]) + expect(app.router.history[0].hooks.transform?.length).toEqual(1) }) - it('insert before on local stack', async () => { - const orders: number[] = [] - + it('appends transform array', async () => { const app = new Elysia() - .macro(({ onBeforeHandle }) => ({ + .macro({ hi(fn: () => any) { - onBeforeHandle({ insert: 'before', stack: 'local' }, fn) + return { + transform: [fn, () => {}] + } } - })) - .onBeforeHandle(() => { - orders.push(1) }) .get('/', () => 'Hello World', { - beforeHandle() { - orders.push(3) - }, - hi: () => { - orders.push(2) - } + hi: () => {} }) - await app.handle(req('/')) - - expect(orders).toEqual([1, 2, 3]) + expect(app.router.history[0].hooks.transform?.length).toEqual(2) }) - it('insert after on global stack', async () => { - const orders: number[] = [] - + it('appends beforeHandle', async () => { const app = new Elysia() - .macro(({ onBeforeHandle }) => ({ + .macro({ hi(fn: () => any) { - onBeforeHandle({ insert: 'after', stack: 'global' }, fn) + return { + beforeHandle: fn + } } - })) - .onBeforeHandle(() => { - orders.push(1) }) .get('/', () => 'Hello World', { - beforeHandle() { - orders.push(3) - }, - hi: () => { - orders.push(2) - } + hi: () => {} }) - await app.handle(req('/')) - - expect(orders).toEqual([1, 2, 3]) + expect(app.router.history[0].hooks.beforeHandle?.length).toEqual(1) }) - it('insert before on global stack', async () => { - const orders: number[] = [] - + it('appends beforeHandle array', async () => { const app = new Elysia() - .macro(({ onBeforeHandle }) => ({ + .macro({ hi(fn: () => any) { - onBeforeHandle({ insert: 'before', stack: 'global' }, fn) + return { + beforeHandle: [fn, () => {}] + } } - })) - .onBeforeHandle(() => { - orders.push(2) }) .get('/', () => 'Hello World', { - beforeHandle() { - orders.push(3) - }, - hi: () => { - orders.push(1) - } + hi: () => {} }) - await app.handle(req('/')) - - expect(orders).toEqual([1, 2, 3]) + expect(app.router.history[0].hooks.beforeHandle?.length).toEqual(2) }) - it('appends onParse', async () => { + it('appends afterHandle', async () => { const app = new Elysia() - .macro(({ onParse }) => ({ + .macro({ hi(fn: () => any) { - onParse(fn) + return { + afterHandle: fn + } } - })) + }) .get('/', () => 'Hello World', { hi: () => {} }) - expect(app.router.history[0].hooks.parse?.length).toEqual(1) + expect(app.router.history[0].hooks.afterHandle?.length).toEqual(1) }) - it('appends onTransform', async () => { + it('appends afterHandle array', async () => { const app = new Elysia() - .macro(({ onTransform }) => ({ + .macro({ hi(fn: () => any) { - onTransform(fn) + return { + afterHandle: [fn, () => {}] + } } - })) + }) .get('/', () => 'Hello World', { hi: () => {} }) - expect(app.router.history[0].hooks.transform?.length).toEqual(1) + expect(app.router.history[0].hooks.afterHandle?.length).toEqual(2) }) - it('appends onBeforeHandle', async () => { + it('appends error', async () => { const app = new Elysia() - .macro(({ onBeforeHandle }) => ({ + .macro({ hi(fn: () => any) { - onBeforeHandle(fn) + return { + error: fn + } } - })) + }) .get('/', () => 'Hello World', { hi: () => {} }) - expect(app.router.history[0].hooks.beforeHandle?.length).toEqual(1) + expect(app.router.history[0].hooks.error?.length).toEqual(1) }) - it('appends onAfterHandle', async () => { + it('appends error array', async () => { const app = new Elysia() - .macro(({ onAfterHandle }) => ({ + .macro({ hi(fn: () => any) { - onAfterHandle(fn) + return { + error: [fn, () => {}] + } } - })) + }) .get('/', () => 'Hello World', { hi: () => {} }) - expect(app.router.history[0].hooks.afterHandle?.length).toEqual(1) + expect(app.router.history[0].hooks.error?.length).toEqual(2) }) - it('appends onError', async () => { + it('appends afterResponse', async () => { const app = new Elysia() - .macro(({ onError }) => ({ + .macro({ hi(fn: () => any) { - onError(fn) + return { + afterResponse: fn + } } - })) + }) .get('/', () => 'Hello World', { hi: () => {} }) - expect(app.router.history[0].hooks.error?.length).toEqual(1) + expect(app.router.history[0].hooks.afterResponse?.length).toEqual(1) }) - it('appends onAfterResponse', async () => { + it('appends afterResponse array', async () => { const app = new Elysia() - .macro(({ onAfterResponse }) => ({ + .macro({ hi(fn: () => any) { - onAfterResponse(fn) + return { + afterResponse: [fn, () => {}] + } } - })) + }) .get('/', () => 'Hello World', { hi: () => {} }) - expect(app.router.history[0].hooks.afterResponse?.length).toEqual(1) + expect(app.router.history[0].hooks.afterResponse?.length).toEqual(2) }) it('handle deduplication', async () => { let call = 0 - const a = new Elysia({ name: 'a', seed: 'awdawd' }).macro( - ({ onBeforeHandle }) => ({ - a(_: string) { - onBeforeHandle(() => { - call++ - }) + const a = new Elysia({ name: 'a', seed: 'awdawd' }).macro({ + a: { + beforeHandle() { + call++ } - }) - ) + } + }) const b = new Elysia({ name: 'b', seed: 'add' }) .use(a) @@ -293,7 +252,7 @@ describe('Macro', () => { .use(a) .use(b) .get('/', () => 'Hello World', { - a: 'a' + a: true }) await app.handle(req('/')) @@ -301,18 +260,18 @@ describe('Macro', () => { expect(call).toBe(1) }) - it('propagation macro without inaccurate deduplication in guard', async () => { + it('propagate macro without inaccurate deduplication in guard', async () => { let call = 0 - const base = new Elysia({ name: 'base' }).macro( - ({ onBeforeHandle }) => ({ - auth: (role: 'teacher' | 'student' | 'admin' | 'noLogin') => { - onBeforeHandle(() => { + const base = new Elysia({ name: 'base' }).macro({ + auth(role: 'teacher' | 'student' | 'admin' | 'noLogin') { + return { + beforeHandle() { call++ - }) + } } - }) - ) + } + }) const app = new Elysia() // ? Deduplication check @@ -338,11 +297,11 @@ describe('Macro', () => { it('inherits macro from plugin without name', async () => { let called = 0 - const plugin = new Elysia().macro(() => ({ - hi(config: string) { + const plugin = new Elysia().macro({ + hi(_: string) { called++ } - })) + }) const app = new Elysia() .use(plugin) @@ -357,18 +316,20 @@ describe('Macro', () => { expect(called).toBe(1) }) - it('handle nested macro', async () => { - const authGuard = new Elysia().macro(({ onBeforeHandle }) => ({ + it('handle macro from plugin', async () => { + const authGuard = new Elysia().macro({ requiredUser(value: boolean) { - onBeforeHandle(async () => { - if (value) - return error(401, { - code: 'S000002', - message: 'Unauthorized' - }) - }) + return { + beforeHandle: async () => { + if (value) + return status(401, { + code: 'S000002', + message: 'Unauthorized' + }) + } + } } - })) + }) const testRoute = new Elysia({ prefix: '/test', @@ -394,13 +355,15 @@ describe('Macro', () => { let called = 0 const plugin = new Elysia() - .macro(({ onBeforeHandle }) => ({ + .macro({ count(_: boolean) { - onBeforeHandle((ctx) => { - called++ - }) + return { + beforeHandle(ctx) { + called++ + } + } } - })) + }) .get('/', () => 'hi', { count: true }) @@ -413,15 +376,16 @@ describe('Macro', () => { }) it('inherits macro in group', async () => { - const authGuard = new Elysia().macro(({ onBeforeHandle }) => ({ + const authGuard = new Elysia().macro({ isAuth(shouldAuth: boolean) { - if (shouldAuth) { - onBeforeHandle(({ cookie: { session }, error }) => { - if (!session.value) return error(418) - }) - } + if (shouldAuth) + return { + beforeHandle({ cookie: { session }, status }) { + if (!session.value) return status(418) + } + } } - })) + }) const app = new Elysia().use(authGuard).group('/posts', (app) => app.get('/', () => 'a', { @@ -435,15 +399,16 @@ describe('Macro', () => { }) it('inherits macro in guard', async () => { - const authGuard = new Elysia().macro(({ onBeforeHandle }) => ({ + const authGuard = new Elysia().macro({ isAuth(shouldAuth: boolean) { - if (shouldAuth) { - onBeforeHandle(({ cookie: { session }, error }) => { - if (!session.value) return error(418) - }) - } + if (shouldAuth) + return { + beforeHandle({ cookie: { session }, status }) { + if (!session.value) return status(418) + } + } } - })) + }) const app = new Elysia().use(authGuard).guard({}, (app) => app.get('/posts', () => 'a', { @@ -456,16 +421,17 @@ describe('Macro', () => { expect(status).toBe(418) }) - it('inherits macro in group', async () => { - const authGuard = new Elysia().macro(({ onBeforeHandle }) => ({ + it('inherits macro from plugin', async () => { + const authGuard = new Elysia().macro({ isAuth(shouldAuth: boolean) { - if (shouldAuth) { - onBeforeHandle(({ cookie: { session }, error }) => { - if (!session.value) return error(418) - }) - } + if (shouldAuth) + return { + beforeHandle({ cookie: { session }, status }) { + if (!session.value) return status(418) + } + } } - })) + }) const app = new Elysia().use(authGuard).use((app) => app.get('/posts', () => 'a', { @@ -482,15 +448,14 @@ describe('Macro', () => { const called = [] const plugin = new Elysia().get('/hello', () => 'hello', { + // @ts-ignore hello: 'nagisa' }) new Elysia() - .macro(() => { - return { - hello(a: string) { - called.push(a) - } + .macro({ + hello(a: string) { + called.push(a) } }) .use(plugin) @@ -505,13 +470,14 @@ describe('Macro', () => { let registered = 0 let called = 0 - const a = new Elysia({ name: 'a' }).macro(({ onBeforeHandle }) => { - return { - isSignIn() { - registered++ - onBeforeHandle(() => { + const a = new Elysia({ name: 'a' }).macro({ + isSignIn() { + registered++ + + return { + beforeHandle() { called++ - }) + } } } }) @@ -581,7 +547,7 @@ describe('Macro', () => { const plugin = new Elysia() .macro({ account: (a: boolean) => ({ - resolve: ({ error }) => ({ + resolve: () => ({ account: 'A' }) }) @@ -615,7 +581,7 @@ describe('Macro', () => { const plugin = new Elysia() .macro({ account: (a: boolean) => ({ - resolve: ({ error }) => ({ + resolve: () => ({ account: 'A' }) }) @@ -650,7 +616,7 @@ describe('Macro', () => { const plugin = new Elysia() .macro({ account: (a: boolean) => ({ - resolve: ({ error }) => ({ + resolve: () => ({ account: 'A' }) }) @@ -685,7 +651,7 @@ describe('Macro', () => { const plugin = new Elysia() .macro({ account: (a: boolean) => ({ - resolve: ({ error }) => ({ + resolve: () => ({ account: 'A' }) }) @@ -720,8 +686,8 @@ describe('Macro', () => { const plugin = new Elysia() .macro({ account: (a: boolean) => ({ - resolve: ({ error }) => { - if (Math.random() > 2) return error(401) + resolve: () => { + if (Math.random() > 2) return status(401) return { account: 'A' @@ -758,8 +724,8 @@ describe('Macro', () => { const plugin = new Elysia() .macro({ account: (a: boolean) => ({ - resolve: async ({ error }) => { - if (Math.random() > 2) return error(401) + resolve: async () => { + if (Math.random() > 2) return status(401) return { account: 'A' @@ -809,8 +775,8 @@ describe('Macro', () => { }) .get( '/', - ({ user, error }) => { - if (!user) return error(401) + ({ user, status }) => { + if (!user) return status(401) return { hello: 'hanabi' } }, @@ -903,6 +869,7 @@ describe('Macro', () => { // @ts-expect-error Property `a` does not exist a, b, + // @ts-expect-error Property `c` does not exist c }) => ({ a, b, c }), { @@ -930,4 +897,546 @@ describe('Macro', () => { expect(d).toEqual({ a: 'a', b: undefined }) expect(e).toEqual({ a: undefined, b: 'b', c: 10 }) }) + + it('validate', async () => { + const app = new Elysia() + .macro({ + sartre: { + params: t.Object({ sartre: t.Literal('Sartre') }) + }, + focou: { + query: t.Object({ focou: t.Literal('Focou') }) + }, + lilith: { + body: t.Object({ lilith: t.Literal('Lilith') }) + } + }) + .post('/:sartre', ({ body }) => body, { + sartre: true, + focou: true, + lilith: true + }) + + expect(app.routes[0].hooks.standaloneValidator.length).toBe(1) + + const valid = await app.handle( + post('/Sartre?focou=Focou', { + lilith: 'Lilith' + }) + ) + + expect(valid.status).toBe(200) + expect(await valid.json()).toEqual({ + lilith: 'Lilith' + }) + + const invalid1 = await app.handle( + post('/Sartre?focou=Focou', { + lilith: 'Not Lilith' + }) + ) + + expect(invalid1.status).toBe(422) + + const invalid2 = await app.handle( + post('/Not Sartre?focou=Focou', { + lilith: 'Lilith' + }) + ) + + expect(invalid2.status).toBe(422) + + const invalid3 = await app.handle( + post('/Sartre?focou=Not Focou', { + lilith: 'Lilith' + }) + ) + + expect(invalid3.status).toBe(422) + }) + + it('merge validation', async () => { + const app = new Elysia() + .macro({ + sartre: { + body: t.Object({ sartre: t.Literal('Sartre') }) + }, + focou: { + body: t.Object({ focou: t.Literal('Focou') }) + }, + lilith: { + body: t.Object({ lilith: t.Literal('Lilith') }) + } + }) + .post('/', ({ body }) => body, { + sartre: true, + focou: true, + lilith: true + }) + + expect(app.routes[0].hooks.standaloneValidator.length).toBe(3) + + const response = await app.handle( + post('/', { + sartre: 'Sartre', + focou: 'Focou', + lilith: 'Lilith' + }) + ) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ + sartre: 'Sartre', + focou: 'Focou', + lilith: 'Lilith' + }) + + const invalid1 = await app.handle( + post('/', { + sartre: 'Not Sartre', + focou: 'Focou', + lilith: 'Lilith' + }) + ) + + expect(invalid1.status).toBe(422) + + const invalid2 = await app.handle( + post('/', { + sartre: 'Sartre', + focou: 'Not Focou', + lilith: 'Lilith' + }) + ) + + expect(invalid2.status).toBe(422) + + const invalid3 = await app.handle( + post('/', { + sartre: 'Sartre', + focou: 'Focou', + lilith: 'Not Lilith' + }) + ) + + expect(invalid3.status).toBe(422) + }) + + it('extends', async () => { + const app = new Elysia() + .macro({ + sartre: { + body: t.Object({ sartre: t.Literal('Sartre') }) + }, + focou: { + body: t.Object({ focou: t.Literal('Focou') }) + }, + lilith: { + sartre: true, + focou: true, + body: t.Object({ lilith: t.Literal('Lilith') }) + } + }) + .post('/', ({ body }) => body, { + lilith: true + }) + + expect(app.routes[0].hooks.standaloneValidator.length).toBe(3) + + const response = await app.handle( + post('/', { + sartre: 'Sartre', + focou: 'Focou', + lilith: 'Lilith' + }) + ) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ + sartre: 'Sartre', + focou: 'Focou', + lilith: 'Lilith' + }) + + const invalid1 = await app.handle( + post('/', { + sartre: 'Not Sartre', + focou: 'Focou', + lilith: 'Lilith' + }) + ) + + expect(invalid1.status).toBe(422) + + const invalid2 = await app.handle( + post('/', { + sartre: 'Sartre', + focou: 'Not Focou', + lilith: 'Lilith' + }) + ) + + expect(invalid2.status).toBe(422) + + const invalid3 = await app.handle( + post('/', { + sartre: 'Sartre', + focou: 'Focou', + lilith: 'Not Lilith' + }) + ) + + expect(invalid3.status).toBe(422) + }) + + it('create detail if not exists', () => { + const app = new Elysia() + .macro({ + lilith: { + detail: { + summary: 'Lilith', + description: 'Lilith description' + } + } + }) + .post('/', ({ body }) => body, { + lilith: true + }) + + const route = app.routes[0] + + expect(route.hooks.detail).toEqual({ + summary: 'Lilith', + description: 'Lilith description' + }) + }) + + it('modify detail', () => { + const app = new Elysia() + .macro({ + lilith: { + detail: { + summary: 'Lilith' + } + } + }) + .post('/', ({ body }) => body, { + lilith: true, + detail: { + description: 'Lilith description' + } + }) + + const route = app.routes[0] + + expect(route.hooks.detail).toEqual({ + summary: 'Lilith', + description: 'Lilith description' + }) + }) + + it('deduplicate static object default', () => { + const app = new Elysia() + .macro({ + sartre: { + body: t.Object({ sartre: t.Literal('Sartre') }) + }, + focou: { + sartre: true, + body: t.Object({ focou: t.Literal('Focou') }) + }, + lilith: { + sartre: true, + focou: true, + body: t.Object({ lilith: t.Literal('Lilith') }) + } + }) + .post('/', ({ body }) => body, { + lilith: true + }) + + const route = app.routes[0] + + expect(route.hooks.standaloneValidator.length).toBe(3) + }) + + it('deduplicate function macro by default', () => { + const app = new Elysia() + .macro({ + sartre(enabled: boolean) { + return { + body: t.Object({ sartre: t.Literal('Sartre') }) + } + }, + focou: { + sartre: true, + body: t.Object({ focou: t.Literal('Focou') }) + }, + lilith: { + sartre: true, + focou: true, + body: t.Object({ lilith: t.Literal('Lilith') }) + } + }) + .post('/', ({ body }) => body, { + lilith: true, + sartre: false + }) + + const route = app.routes[0] + + // This is 4 because + // 1. lilith + // 2. focou + // 3. sartre from focou + // 4. sartre with false flag + expect(route.hooks.standaloneValidator.length).toBe(4) + }) + + it('deduplicate function macro when argument is similar', () => { + const app = new Elysia() + .macro({ + sartre(enabled: boolean) { + return { + body: t.Object({ sartre: t.Literal('Sartre') }) + } + }, + focou: { + sartre: true, + body: t.Object({ focou: t.Literal('Focou') }) + }, + lilith: { + sartre: true, + focou: true, + body: t.Object({ lilith: t.Literal('Lilith') }) + } + }) + .post('/', ({ body }) => body, { + lilith: true, + sartre: true + }) + + const route = app.routes[0] + + // This is 4 because + // 1. lilith + // 2. focou + // 3. sartre from focou + expect(route.hooks.standaloneValidator.length).toBe(3) + }) + + it('deduplicate programmatically', () => { + const app = new Elysia() + .macro({ + sartre(tag: string) { + return { + seed: tag, + body: t.Object({ sartre: t.Literal('Sartre') }), + detail: { + tags: [tag] + } + } + }, + focou: { + sartre: 'npc', + body: t.Object({ focou: t.Literal('Focou') }) + }, + lilith: { + sartre: 'philosopher', + focou: true, + body: t.Object({ lilith: t.Literal('Lilith') }) + } + }) + .post('/', ({ body }) => body, { + lilith: true + }) + + const route = app.routes[0] + + expect(route.hooks.standaloneValidator.length).toBe(4) + expect(route.hooks.detail).toEqual({ + tags: ['philosopher', 'npc'] + }) + }) + + it('handle macro name', async () => { + const app = new Elysia() + .macro('sartre', { + params: t.Object({ sartre: t.Literal('Sartre') }) + }) + .macro({ + focou: { + query: t.Object({ focou: t.Literal('Focou') }) + }, + lilith: { + body: t.Object({ lilith: t.Literal('Lilith') }) + } + }) + .post('/:sartre', ({ body }) => body, { + sartre: true, + focou: true, + lilith: true + }) + + expect(app.routes[0].hooks.standaloneValidator.length).toBe(1) + + const valid = await app.handle( + post('/Sartre?focou=Focou', { + lilith: 'Lilith' + }) + ) + + expect(valid.status).toBe(200) + expect(await valid.json()).toEqual({ + lilith: 'Lilith' + }) + + const invalid1 = await app.handle( + post('/Sartre?focou=Focou', { + lilith: 'Not Lilith' + }) + ) + + expect(invalid1.status).toBe(422) + + const invalid2 = await app.handle( + post('/Not Sartre?focou=Focou', { + lilith: 'Lilith' + }) + ) + + expect(invalid2.status).toBe(422) + + const invalid3 = await app.handle( + post('/Sartre?focou=Not Focou', { + lilith: 'Lilith' + }) + ) + + expect(invalid3.status).toBe(422) + }) + + it('handle macro name with function', async () => { + const app = new Elysia() + .macro('sartre', (_: boolean) => ({ + params: t.Object({ sartre: t.Literal('Sartre') }) + })) + .macro({ + focou: { + query: t.Object({ focou: t.Literal('Focou') }) + }, + lilith: { + body: t.Object({ lilith: t.Literal('Lilith') }) + } + }) + .post('/:sartre', ({ body }) => body, { + sartre: true, + focou: true, + lilith: true + }) + + expect(app.routes[0].hooks.standaloneValidator.length).toBe(1) + + const valid = await app.handle( + post('/Sartre?focou=Focou', { + lilith: 'Lilith' + }) + ) + + expect(valid.status).toBe(200) + expect(await valid.json()).toEqual({ + lilith: 'Lilith' + }) + + const invalid1 = await app.handle( + post('/Sartre?focou=Focou', { + lilith: 'Not Lilith' + }) + ) + + expect(invalid1.status).toBe(422) + + const invalid2 = await app.handle( + post('/Not Sartre?focou=Focou', { + lilith: 'Lilith' + }) + ) + + expect(invalid2.status).toBe(422) + + const invalid3 = await app.handle( + post('/Sartre?focou=Not Focou', { + lilith: 'Lilith' + }) + ) + + expect(invalid3.status).toBe(422) + }) + + it('handle macro name extends', async () => { + const app = new Elysia() + .macro('sartre', { + body: t.Object({ sartre: t.Literal('Sartre') }) + }) + .macro({ + focou: { + sartre: true, + body: t.Object({ focou: t.Literal('Focou') }) + }, + lilith: { + focou: true, + body: t.Object({ lilith: t.Literal('Lilith') }) + } + }) + .post('/', ({ body }) => body, { + lilith: true + }) + + expect(app.routes[0].hooks.standaloneValidator.length).toBe(3) + + const response = await app.handle( + post('/', { + sartre: 'Sartre', + focou: 'Focou', + lilith: 'Lilith' + }) + ) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ + sartre: 'Sartre', + focou: 'Focou', + lilith: 'Lilith' + }) + + const invalid1 = await app.handle( + post('/', { + sartre: 'Not Sartre', + focou: 'Focou', + lilith: 'Lilith' + }) + ) + + expect(invalid1.status).toBe(422) + + const invalid2 = await app.handle( + post('/', { + sartre: 'Sartre', + focou: 'Not Focou', + lilith: 'Lilith' + }) + ) + + expect(invalid2.status).toBe(422) + + const invalid3 = await app.handle( + post('/', { + sartre: 'Sartre', + focou: 'Focou', + lilith: 'Not Lilith' + }) + ) + + expect(invalid3.status).toBe(422) + }) }) diff --git a/test/path/group.test.ts b/test/path/group.test.ts index 98bd7e649..a9ed56a84 100644 --- a/test/path/group.test.ts +++ b/test/path/group.test.ts @@ -247,4 +247,23 @@ describe('group', () => { expect(response).toEqual('a') }) + + it('cast callback function schema to standaloneValidator', async () => { + const app = new Elysia().group( + '/group/:id', + { params: t.Object({ id: t.Number() }) }, + (app) => + app.get('/:name', ({ params }) => params, { + params: t.Object({ name: t.String() }) + }) + ) + + const valid = app.handle(req('/group/1/saltyaom')).then((x) => x.json()) + const invalid = app + .handle(req('/group/a/saltyaom')) + .then((x) => x.status) + + expect(await valid).toEqual({ id: 1, name: 'saltyaom' }) + expect(await invalid).toBe(422) + }) }) diff --git a/test/path/guard.test.ts b/test/path/guard.test.ts index e4abcfa28..ed99f016b 100644 --- a/test/path/guard.test.ts +++ b/test/path/guard.test.ts @@ -420,4 +420,23 @@ describe('guard', () => { '500' ]) }) + + it('cast callback function schema to standaloneValidator', async () => { + const app = new Elysia().guard( + { params: t.Object({ id: t.Number() }) }, + (app) => + app.get('/guard/:id/:name', ({ params }) => params, { + params: t.Object({ name: t.String() }) + }) + ) + + const valid = app.handle(req('/guard/1/saltyaom')).then((x) => x.json()) + const invalid = app + .handle(req('/guard/a/saltyaom')) + .then((x) => x.status) + + expect(await valid).toEqual({ id: 1, name: 'saltyaom' }) + expect(await invalid).toBe(422) + }) + }) diff --git a/test/plugins/checksum.test.ts b/test/plugins/checksum.test.ts index a549588bc..7f7d3ee4a 100644 --- a/test/plugins/checksum.test.ts +++ b/test/plugins/checksum.test.ts @@ -304,7 +304,7 @@ describe('Checksum', () => { let i = 0 const plugin = new Elysia().use( - new Elysia({ prefix: '/call', scoped: true }) + new Elysia({ prefix: '/call' }) .derive(() => { i++ // <-- should not be called, when requesting /asdf return { test: 'test' } diff --git a/test/response/redirect.test.ts b/test/response/redirect.test.ts index 12d568aa9..58b6377ec 100644 --- a/test/response/redirect.test.ts +++ b/test/response/redirect.test.ts @@ -10,7 +10,6 @@ describe('Response Redirect', () => { const { headers, status } = await app.handle(req('/')) expect(status).toBe(302) - // @ts-expect-error expect(headers.toJSON()).toEqual({ location: '/skadi' }) @@ -24,7 +23,6 @@ describe('Response Redirect', () => { const { headers, status } = await app.handle(req('/')) expect(status).toBe(301) - // @ts-expect-error expect(headers.toJSON()).toEqual({ location: '/skadi' }) @@ -40,7 +38,6 @@ describe('Response Redirect', () => { const { headers, status } = await app.handle(req('/')) expect(status).toBe(302) - // @ts-expect-error expect(headers.toJSON()).toEqual({ location: '/skadi', alias: 'Abyssal Hunter' diff --git a/test/standard-schema/reference.test.ts b/test/standard-schema/reference.test.ts new file mode 100644 index 000000000..8ccaaca5a --- /dev/null +++ b/test/standard-schema/reference.test.ts @@ -0,0 +1,353 @@ +import { Elysia } from '../../src' +import { describe, it, expect } from 'bun:test' +import { z } from 'zod' +import { post, req } from '../utils' + +describe('Standard Schema Validate', () => { + it('validate body', async () => { + const app = new Elysia() + .model({ + body: z.object({ + id: z.number() + }) + }) + .post('/', ({ body }) => body, { + body: 'body' + }) + + const value = await app + .handle( + post('/', { + id: 1 + }) + ) + .then((x) => x.json()) + + expect(value).toEqual({ id: 1 }) + + const invalid = await app.handle( + post('/', { + id: '1' + }) + ) + + expect(invalid.status).toBe(422) + }) + + it('validate query', async () => { + const app = new Elysia() + .model({ + query: z.object({ + id: z.coerce.number() + }) + }) + .get('/', ({ query }) => query, { + query: 'query' + }) + + const value = await app.handle(req('/?id=1')).then((x) => x.json()) + + expect(value).toEqual({ id: 1 }) + + const invalid = await app.handle(req('/?id=a')) + + expect(invalid.status).toBe(422) + }) + + it('validate params', async () => { + const app = new Elysia() + .model({ + params: z.object({ + id: z.coerce.number() + }) + }) + .get('/user/:id', ({ params }) => params, { + params: 'params' + }) + + const value = await app.handle(req('/user/1')).then((x) => x.json()) + + expect(value).toEqual({ id: 1 }) + + const invalid = await app.handle(req('/user/a')) + + expect(invalid.status).toBe(422) + }) + + it('validate headers', async () => { + const app = new Elysia() + .model({ + headers: z.object({ + id: z.coerce.number() + }) + }) + .get('/', ({ headers }) => headers, { + headers: 'headers' + }) + + const value = await app + .handle( + req('/', { + headers: { + id: '1' + } + }) + ) + .then((x) => x.json()) + + expect(value).toEqual({ id: 1 }) + + const invalid = await app.handle(req('/', {})) + + expect(invalid.status).toBe(422) + }) + + it('validate single response', async () => { + const app = new Elysia() + .model({ + response: z.boolean() + }) + .get( + '/:name', + // @ts-expect-error + ({ params: { name } }) => + name === 'lilith' ? undefined : true, + { + response: 'response' + } + ) + + const exists = await app.handle(req('/fouco')) + const nonExists = await app.handle(req('/lilith')) + + expect(exists.status).toBe(200) + expect(nonExists.status).toBe(422) + }) + + it('validate multiple response', async () => { + const app = new Elysia() + .model({ + 'response.404': z.literal('lilith'), + 'response.418': z.literal('fouco') + }) + .get( + '/:name', + ({ params: { name }, status }) => + name === 'lilith' + ? status(404, 'lilith') + : status(418, name as any), + { + response: { + 404: 'response.404', + 418: 'response.418' + } + } + ) + + const exists = await app.handle(req('/fouco')) + const nonExists = await app.handle(req('/lilith')) + + expect(exists.status).toBe(418) + expect(nonExists.status).toBe(404) + + const invalid = await app.handle(req('/unknown')) + expect(invalid.status).toBe(422) + }) + + it('validate multiple schema together', async () => { + const app = new Elysia() + .model({ + body: z.object({ + id: z.number() + }), + query: z.object({ + limit: z.coerce.number() + }), + params: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + 'response.404': z.literal('lilith'), + 'response.418': z.literal('fouco') + }) + .post( + '/:name', + ({ params: { name }, status }) => + name === 'lilith' + ? status(404, 'lilith') + : status(418, name as any), + { + body: 'body', + query: 'query', + params: 'params', + response: { + 404: 'response.404', + 418: 'response.418' + } + } + ) + + const responses = await Promise.all( + [ + post('/lilith?limit=1', { + id: 1 + }), + post('/fouco?limit=10', { + id: 2 + }), + post('/unknown?limit=10', { + id: 2 + }), + post('/fouco?limit=a', { + id: 2 + }), + post('/fouco?limit=10', { + id: '2' + }), + post('/fouco', {}) + ].map((x) => app.handle(x).then((x) => x.status)) + ) + + expect(responses[0]).toEqual(404) + expect(responses[1]).toEqual(418) + expect(responses[2]).toEqual(422) + expect(responses[3]).toEqual(422) + expect(responses[4]).toEqual(422) + expect(responses[5]).toEqual(422) + }) + + it('merge guard', async () => { + const app = new Elysia() + .model({ + body: z.object({ + id: z.number() + }), + query: z.object({ + limit: z.coerce.number() + }), + params: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + 'response.404': z.literal('lilith'), + 'response.418': z.literal('fouco') + }) + .guard({ + body: 'body', + query: 'query', + response: { + 404: 'response.404' + } + }) + .post( + '/:name', + ({ params: { name }, status }) => + name === 'lilith' + ? status(404, 'lilith') + : status(418, name as any), + { + params: 'params', + response: { + 418: 'response.418' + } + } + ) + + const responses = await Promise.all( + [ + post('/lilith?limit=1', { + id: 1 + }), + post('/fouco?limit=10', { + id: 2 + }), + post('/unknown?limit=10', { + id: 2 + }), + post('/fouco?limit=a', { + id: 2 + }), + post('/fouco?limit=10', { + id: '2' + }), + post('/fouco', {}) + ].map((x) => app.handle(x).then((x) => x.status)) + ) + + expect(responses[0]).toEqual(404) + expect(responses[1]).toEqual(418) + expect(responses[2]).toEqual(422) + expect(responses[3]).toEqual(422) + expect(responses[4]).toEqual(422) + expect(responses[5]).toEqual(422) + }) + + it('merge plugin', async () => { + const plugin = new Elysia() + .model({ + body: z.object({ + id: z.number() + }), + query: z.object({ + limit: z.coerce.number() + }), + params: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + 'response.404': z.literal('lilith'), + 'response.418': z.literal('fouco') + }) + .guard({ + as: 'scoped', + body: 'body', + query: 'query', + response: { + 404: 'response.404' + } + }) + + const app = new Elysia() + .use(plugin) + .post( + '/:name', + ({ params: { name }, status }) => + name === 'lilith' + ? status(404, 'lilith') + : status(418, name as any), + { + params: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + response: { + 418: 'response.418' + } + } + ) + + const responses = await Promise.all( + [ + post('/lilith?limit=1', { + id: 1 + }), + post('/fouco?limit=10', { + id: 2 + }), + post('/unknown?limit=10', { + id: 2 + }), + post('/fouco?limit=a', { + id: 2 + }), + post('/fouco?limit=10', { + id: '2' + }), + post('/fouco', {}) + ].map((x) => app.handle(x).then((x) => x.status)) + ) + + expect(responses[0]).toEqual(404) + expect(responses[1]).toEqual(418) + expect(responses[2]).toEqual(422) + expect(responses[3]).toEqual(422) + expect(responses[4]).toEqual(422) + expect(responses[5]).toEqual(422) + }) +}) diff --git a/test/standard-schema/standalone.test.ts b/test/standard-schema/standalone.test.ts new file mode 100644 index 000000000..68820e861 --- /dev/null +++ b/test/standard-schema/standalone.test.ts @@ -0,0 +1,576 @@ +import { Elysia, t } from '../../src' +import { describe, it, expect } from 'bun:test' +import { z } from 'zod' +import * as v from 'valibot' +import { type } from 'arktype' +import { post, req } from '../utils' + +describe('Standard Schema Standalone', () => { + it('validate and normalize body', async () => { + const app = new Elysia() + .guard({ + schema: 'standalone', + body: z.object({ + id: z.number() + }) + }) + .post('/', ({ body }) => body, { + body: t.Object({ + name: t.Literal('lilith') + }) + }) + + const value = await app + .handle( + post('/', { + id: 1, + name: 'lilith', + extra: false + }) + ) + .then((x) => x.json()) + + expect(value).toEqual({ id: 1, name: 'lilith' }) + + const invalid = await app.handle( + post('/', { + id: '1', + name: 'fouco', + extra: false + }) + ) + + expect(invalid.status).toBe(422) + }) + + it('validate query', async () => { + const app = new Elysia() + .guard({ + schema: 'standalone', + query: z.object({ + id: z.coerce.number() + }) + }) + .get('/', ({ query }) => query, { + query: t.Object({ + name: t.Literal('lilith') + }) + }) + + const value = await app + .handle(req('/?id=1&name=lilith&extra=true')) + .then((x) => x.json()) + + expect(value).toEqual({ id: 1, name: 'lilith' }) + + const invalid = await app.handle(req('/?id=a&name=fouco')) + + expect(invalid.status).toBe(422) + }) + + it('validate and normalize params', async () => { + const app = new Elysia() + .guard({ + schema: 'standalone', + params: z.object({ + id: z.coerce.number() + }) + }) + .get('/:name/:id', ({ params }) => params, { + params: t.Object({ + name: t.Literal('lilith') + }) + }) + + const value = await app.handle(req('/lilith/1')).then((x) => x.json()) + + expect(value).toEqual({ id: 1, name: 'lilith' }) + + const invalid = await app.handle(req('/user/a?name=fouco')) + expect(invalid.status).toBe(422) + }) + + it('validate and normalize headers', async () => { + const app = new Elysia() + .guard({ + schema: 'standalone', + headers: z.object({ + id: z.coerce.number() + }) + }) + .get('/', ({ headers }) => headers, { + headers: t.Object({ + name: t.Literal('lilith') + }) + }) + + const value = await app + .handle( + req('/', { + headers: { + id: '1', + name: 'lilith', + extra: 'false' + } + }) + ) + .then((x) => x.json()) + + expect(value).toEqual({ id: 1, name: 'lilith' }) + + const invalid = await app.handle( + req('/', { + headers: { + id: 'a', + name: 'fouco' + } + }) + ) + + expect(invalid.status).toBe(422) + }) + + it('validate and normalize single response', async () => { + const app = new Elysia() + .guard({ + schema: 'standalone', + response: z.object({ + id: z.number() + }) + }) + .get( + '/:name', + // @ts-expect-error + ({ params: { name } }) => ({ + name, + id: name !== 'lilith' ? undefined : 1, + extra: false + }), + { + response: t.Object({ + name: t.Literal('lilith') + }) + } + ) + + const valid = await app.handle(req('/lilith')).then((x) => x.json()) + + expect(valid).toEqual({ id: 1, name: 'lilith' }) + + const invalid = await app.handle(req('/focou')) + expect(invalid.status).toBe(422) + }) + + it('validate and normalize multiple response', async () => { + const app = new Elysia() + .guard({ + schema: 'standalone', + response: { + 404: z.object({ + id: z.number() + }), + 418: z.object({ + id: z.number() + }) + } + }) + .get( + '/:name', + ({ params: { name }, status }) => + name === 'lilith' + ? status(404, { + name, + id: 1, + extra: false + }) + : status(418, { + // @ts-expect-error + name, + id: 2, + extra: false + }), + { + response: { + 404: t.Object({ + name: t.Literal('lilith') + }), + 418: t.Object({ + name: t.Literal('fouco') + }) + } + } + ) + + const lilith = await app.handle(req('/lilith')).then((x) => x.json()) + const fouco = await app.handle(req('/fouco')).then((x) => x.json()) + + expect(lilith).toEqual({ id: 1, name: 'lilith' }) + expect(fouco).toEqual({ id: 2, name: 'fouco' }) + + const invalid = await app.handle(req('/unknown')) + expect(invalid.status).toBe(422) + }) + + it('validate multiple schema together', async () => { + const app = new Elysia() + .onError(({ error, code }) => { + if (code !== 'VALIDATION') console.log(error) + }) + .guard({ + schema: 'standalone', + body: z.object({ + name: z.string() + }), + query: z.object({ + id: z.coerce.number() + }), + params: z.object({ + id: z.coerce.number() + }), + response: { + 404: z.object({ + id: z.number() + }), + 418: z.object({ + id: z.number() + }) + } + }) + .post( + '/:name/:id', + ({ params: { name, id }, status }) => + name === 'lilith' + ? status(404, { + name, + id, + extra: true + }) + : status(418, { + name, + id, + extra: true + }), + { + body: t.Object({ + id: t.Number() + }), + query: t.Object({ + limit: t.Number() + }), + params: t.Object({ + name: t.UnionEnum(['fouco', 'lilith']) + }), + response: { + 404: z.object({ name: z.literal('lilith') }), + 418: z.object({ name: z.literal('fouco') }) + } + } + ) + + const responses = await Promise.all( + [ + post('/lilith/1?limit=1&id=1', { + id: 1, + name: 'lilith' + }), + post('/fouco/2?limit=10&id=2', { + id: 2, + name: 'fouco' + }), + post('/unknown/2?limit=10&id=2', { + id: 2, + name: 'fouco' + }), + post('/fouco/2?limit=a&id=2', { + id: 2, + name: 'fouco' + }), + post('/fouco/2?limit=10&id=2', { + id: '2', + name: 'fouco' + }), + post('/fouco/2', { + id: 2, + name: 'fouco' + }), + post('/fouco/a?limit=10&id=2', {}) + ].map((x) => app.handle(x).then((x) => x.status)) + ) + + expect(responses[0]).toEqual(404) + expect(responses[1]).toEqual(418) + expect(responses[2]).toEqual(422) + expect(responses[3]).toEqual(422) + expect(responses[4]).toEqual(422) + expect(responses[5]).toEqual(422) + expect(responses[6]).toEqual(422) + }) + + it('merge plugin', async () => { + const plugin = new Elysia().guard({ + as: 'scoped', + schema: 'standalone', + response: { + 404: z.object({ + id: z.number() + }), + 418: z.object({ + id: z.number() + }) + } + }) + + const app = new Elysia().use(plugin).get( + '/:name', + ({ params: { name }, status }) => + name === 'lilith' + ? status(404, { + name, + id: 1, + extra: false + }) + : status(418, { + // @ts-expect-error + name, + id: 2, + extra: false + }), + { + response: { + 404: t.Object({ + name: t.Literal('lilith') + }), + 418: t.Object({ + name: t.Literal('fouco') + }) + } + } + ) + + const lilith = await app.handle(req('/lilith')).then((x) => x.json()) + const fouco = await app.handle(req('/fouco')).then((x) => x.json()) + + expect(lilith).toEqual({ id: 1, name: 'lilith' }) + expect(fouco).toEqual({ id: 2, name: 'fouco' }) + + const invalid = await app.handle(req('/unknown')) + expect(invalid.status).toBe(422) + }) + + it('validate non-typebox schema', async () => { + const app = new Elysia() + .guard({ + schema: 'standalone', + body: z.object({ + name: z.string() + }), + query: z.object({ + id: z.coerce.number() + }), + params: z.object({ + id: z.coerce.number() + }), + response: { + 404: z.object({ + id: z.number() + }), + 418: z.object({ + id: z.number() + }) + } + }) + .post( + '/:name/:id', + ({ params: { name, id }, status }) => + name === 'lilith' + ? status(404, { + name, + id, + extra: true + }) + : status(418, { + name, + id, + extra: true + }), + { + body: v.object({ + id: v.number() + }), + query: v.object({ + limit: v.pipe( + v.string(), + v.transform(Number), + v.number() + ) + }), + params: v.object({ + name: v.union([v.literal('fouco'), v.literal('lilith')]) + }), + response: { + 404: v.object({ name: v.literal('lilith') }), + 418: v.object({ name: v.literal('fouco') }) + } + } + ) + + const responses = await Promise.all( + [ + post('/lilith/1?limit=1&id=1', { + id: 1, + name: 'lilith' + }), + post('/fouco/2?limit=10&id=2', { + id: 2, + name: 'fouco' + }), + post('/unknown/2?limit=10&id=2', { + id: 2, + name: 'fouco' + }), + post('/fouco/2?limit=a&id=2', { + id: 2, + name: 'fouco' + }), + post('/fouco/2?limit=10&id=2', { + id: '2', + name: 'fouco' + }), + post('/fouco/2', { + id: 2, + name: 'fouco' + }), + post('/fouco/a?limit=10&id=2', {}) + ].map((x) => app.handle(x).then((x) => x.status)) + ) + + expect(responses[0]).toEqual(404) + expect(responses[1]).toEqual(418) + expect(responses[2]).toEqual(422) + expect(responses[3]).toEqual(422) + expect(responses[4]).toEqual(422) + expect(responses[5]).toEqual(422) + expect(responses[6]).toEqual(422) + }) + + it('validate 3 schema validators without TypeBox', async () => { + const app = new Elysia() + .guard({ + schema: 'standalone', + body: z.object({ + name: z.string() + }), + query: z.object({ + id: z.coerce.number() + }), + params: z.object({ + id: z.coerce.number() + }), + response: { + 404: z.object({ + id: z.number() + }), + 418: z.object({ + id: z.number() + }) + } + }) + .guard({ + schema: 'standalone', + body: type({ + world: '"fantasy"' + }), + query: type({ + world: '"fantasy"' + }), + response: { + 404: type({ + world: '"fantasy"' + }), + 418: type({ + world: '"fantasy"' + }) + } + }) + .post( + '/:name/:id', + ({ params: { name, id }, status }) => + name === 'lilith' + ? status(404, { + name, + id, + world: 'fantasy' + }) + : status(418, { + name, + id, + world: 'fantasy' + }), + { + body: v.object({ + id: v.number() + }), + query: v.object({ + limit: v.pipe( + v.string(), + v.transform(Number), + v.number() + ) + }), + params: v.object({ + name: v.union([v.literal('fouco'), v.literal('lilith')]) + }), + response: { + 404: v.object({ name: v.literal('lilith') }), + 418: v.object({ name: v.literal('fouco') }) + } + } + ) + + const responses = await Promise.all( + [ + post('/lilith/1?limit=1&id=1&world=fantasy', { + id: 1, + name: 'lilith', + world: 'fantasy' + }), + post('/fouco/2?limit=10&id=2&world=fantasy', { + id: 2, + name: 'fouco', + world: 'fantasy' + }), + post('/unknown/2?limit=10&id=2&world=fantasy', { + id: 2, + name: 'fouco', + world: 'fantasy' + }), + post('/unknown/2?limit=10&id=2', { + id: 2, + name: 'fouco' + }), + post('/fouco/2?limit=a&id=2', { + id: 2, + name: 'fouco' + }), + post('/fouco/2?limit=10&id=2', { + id: '2', + name: 'fouco' + }), + post('/fouco/2', { + id: 2, + name: 'fouco' + }), + post('/fouco/a?limit=10&id=2', {}) + ].map((x) => app.handle(x).then((x) => x.status)) + ) + + expect(responses[0]).toEqual(404) + expect(responses[1]).toEqual(418) + expect(responses[2]).toEqual(422) + expect(responses[3]).toEqual(422) + expect(responses[4]).toEqual(422) + expect(responses[5]).toEqual(422) + expect(responses[6]).toEqual(422) + expect(responses[7]).toEqual(422) + }) +}) diff --git a/test/standard-schema/validate.test.ts b/test/standard-schema/validate.test.ts new file mode 100644 index 000000000..1a8fad6fe --- /dev/null +++ b/test/standard-schema/validate.test.ts @@ -0,0 +1,302 @@ +import { Elysia } from '../../src' +import { describe, it, expect } from 'bun:test' +import { z } from 'zod' +import { post, req } from '../utils' + +describe('Standard Schema Validate', () => { + it('validate body', async () => { + const app = new Elysia().post('/', ({ body }) => body, { + body: z.object({ + id: z.number() + }) + }) + + const value = await app + .handle( + post('/', { + id: 1 + }) + ) + .then((x) => x.json()) + + expect(value).toEqual({ id: 1 }) + + const invalid = await app.handle( + post('/', { + id: '1' + }) + ) + + expect(invalid.status).toBe(422) + }) + + it('validate query', async () => { + const app = new Elysia().get('/', ({ query }) => query, { + query: z.object({ + id: z.coerce.number() + }) + }) + + const value = await app.handle(req('/?id=1')).then((x) => x.json()) + + expect(value).toEqual({ id: 1 }) + + const invalid = await app.handle(req('/?id=a')) + + expect(invalid.status).toBe(422) + }) + + it('validate params', async () => { + const app = new Elysia().get('/user/:id', ({ params }) => params, { + params: z.object({ + id: z.coerce.number() + }) + }) + + const value = await app.handle(req('/user/1')).then((x) => x.json()) + + expect(value).toEqual({ id: 1 }) + + const invalid = await app.handle(req('/user/a')) + + expect(invalid.status).toBe(422) + }) + + it('validate headers', async () => { + const app = new Elysia().get('/', ({ headers }) => headers, { + headers: z.object({ + id: z.coerce.number() + }) + }) + + const value = await app + .handle( + req('/', { + headers: { + id: '1' + } + }) + ) + .then((x) => x.json()) + + expect(value).toEqual({ id: 1 }) + + const invalid = await app.handle(req('/', {})) + + expect(invalid.status).toBe(422) + }) + + it('validate single response', async () => { + const app = new Elysia().get( + '/:name', + // @ts-expect-error + ({ params: { name } }) => (name === 'lilith' ? undefined : true), + { + response: z.boolean() + } + ) + + const exists = await app.handle(req('/fouco')) + const nonExists = await app.handle(req('/lilith')) + + expect(exists.status).toBe(200) + expect(nonExists.status).toBe(422) + }) + + it('validate multiple response', async () => { + const app = new Elysia().get( + '/:name', + ({ params: { name }, status }) => + name === 'lilith' + ? status(404, 'lilith') + : status(418, name as any), + { + response: { + 404: z.literal('lilith'), + 418: z.literal('fouco') + } + } + ) + + const exists = await app.handle(req('/fouco')) + const nonExists = await app.handle(req('/lilith')) + + expect(exists.status).toBe(418) + expect(nonExists.status).toBe(404) + + const invalid = await app.handle(req('/unknown')) + expect(invalid.status).toBe(422) + }) + + it('validate multiple schema together', async () => { + const app = new Elysia().post( + '/:name', + ({ params: { name }, status }) => + name === 'lilith' + ? status(404, 'lilith') + : status(418, name as any), + { + body: z.object({ + id: z.number() + }), + query: z.object({ + limit: z.coerce.number() + }), + params: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + response: { + 404: z.literal('lilith'), + 418: z.literal('fouco') + } + } + ) + + const responses = await Promise.all( + [ + post('/lilith?limit=1', { + id: 1 + }), + post('/fouco?limit=10', { + id: 2 + }), + post('/unknown?limit=10', { + id: 2 + }), + post('/fouco?limit=a', { + id: 2 + }), + post('/fouco?limit=10', { + id: '2' + }), + post('/fouco', {}) + ].map((x) => app.handle(x).then((x) => x.status)) + ) + + expect(responses[0]).toEqual(404) + expect(responses[1]).toEqual(418) + expect(responses[2]).toEqual(422) + expect(responses[3]).toEqual(422) + expect(responses[4]).toEqual(422) + expect(responses[5]).toEqual(422) + }) + + it('merge guard', async () => { + const app = new Elysia() + .guard({ + body: z.object({ + id: z.number() + }), + query: z.object({ + limit: z.coerce.number() + }), + response: { + 404: z.literal('lilith') + } + }) + .post( + '/:name', + ({ params: { name }, status }) => + name === 'lilith' + ? status(404, 'lilith') + : status(418, name as any), + { + params: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + response: { + 418: z.literal('fouco') + } + } + ) + + const responses = await Promise.all( + [ + post('/lilith?limit=1', { + id: 1 + }), + post('/fouco?limit=10', { + id: 2 + }), + post('/unknown?limit=10', { + id: 2 + }), + post('/fouco?limit=a', { + id: 2 + }), + post('/fouco?limit=10', { + id: '2' + }), + post('/fouco', {}) + ].map((x) => app.handle(x).then((x) => x.status)) + ) + + expect(responses[0]).toEqual(404) + expect(responses[1]).toEqual(418) + expect(responses[2]).toEqual(422) + expect(responses[3]).toEqual(422) + expect(responses[4]).toEqual(422) + expect(responses[5]).toEqual(422) + }) + + it('merge plugin', async () => { + const plugin = new Elysia().guard({ + as: 'scoped', + body: z.object({ + id: z.number() + }), + query: z.object({ + limit: z.coerce.number() + }), + response: { + 404: z.literal('lilith') + } + }) + + const app = new Elysia() + .use(plugin) + .post( + '/:name', + ({ params: { name }, status }) => + name === 'lilith' + ? status(404, 'lilith') + : status(418, name as any), + { + params: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + response: { + 418: z.literal('fouco') + } + } + ) + + const responses = await Promise.all( + [ + post('/lilith?limit=1', { + id: 1 + }), + post('/fouco?limit=10', { + id: 2 + }), + post('/unknown?limit=10', { + id: 2 + }), + post('/fouco?limit=a', { + id: 2 + }), + post('/fouco?limit=10', { + id: '2' + }), + post('/fouco', {}) + ].map((x) => app.handle(x).then((x) => x.status)) + ) + + expect(responses[0]).toEqual(404) + expect(responses[1]).toEqual(418) + expect(responses[2]).toEqual(422) + expect(responses[3]).toEqual(422) + expect(responses[4]).toEqual(422) + expect(responses[5]).toEqual(422) + }) +}) diff --git a/test/sucrose/infer-body-reference.test.ts b/test/sucrose/infer-body-reference.test.ts index 0ac88570d..ae0335483 100644 --- a/test/sucrose/infer-body-reference.test.ts +++ b/test/sucrose/infer-body-reference.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'bun:test' -import { inferBodyReference } from '../../src/sucrose' +import { inferBodyReference, Sucrose } from '../../src/sucrose' describe('infer body reference', () => { it('infer dot notation', () => { @@ -11,12 +11,15 @@ describe('infer body reference', () => { body: false, cookie: false, set: false, - server: false - } + server: false, + path: false, + route: false, + url: false + } satisfies Sucrose.Inference inferBodyReference(code, aliases, inference) - expect(inference.body).toBe(true) + expect(inference.body as boolean).toBe(true) }) it('infer property access', () => { @@ -28,12 +31,15 @@ describe('infer body reference', () => { body: false, cookie: false, set: false, - server: false - } + server: false, + path: false, + route: false, + url: false + } satisfies Sucrose.Inference inferBodyReference(code, aliases, inference) - expect(inference.body).toBe(true) + expect(inference.body as boolean).toBe(true) }) it('infer multiple query', () => { @@ -52,8 +58,11 @@ describe('infer body reference', () => { headers: false, query: true, set: true, - server: false - } + server: false, + path: false, + route: false, + url: false + } satisfies Sucrose.Inference inferBodyReference(code, aliases, inference) @@ -63,7 +72,10 @@ describe('infer body reference', () => { headers: false, query: true, set: true, - server: false + server: false, + path: false, + route: false, + url: false }) }) @@ -141,8 +153,11 @@ describe('infer body reference', () => { headers: false, query: true, set: true, - server: false - } + server: false, + path: false, + route: false, + url: false + } satisfies Sucrose.Inference inferBodyReference(code, aliases, inference) @@ -152,7 +167,10 @@ describe('infer body reference', () => { headers: false, query: true, set: true, - server: false + server: false, + path: false, + route: false, + url: false }) }) @@ -167,11 +185,14 @@ describe('infer body reference', () => { body: false, cookie: false, set: false, - server: false - } + server: false, + path: false, + route: false, + url: false + } satisfies Sucrose.Inference inferBodyReference(code, aliases, inference) - expect(inference.server).toBe(true) + expect(inference.server as boolean).toBe(true) }) }) diff --git a/test/tracer/trace.test.ts b/test/tracer/trace.test.ts index bc7fe4b78..4c7043470 100644 --- a/test/tracer/trace.test.ts +++ b/test/tracer/trace.test.ts @@ -31,7 +31,7 @@ describe('trace', () => { clearTimeout(timeout) }) - const b = new Elysia({ scoped: true }).get('/scoped', () => 'hi') + const b = new Elysia().get('/scoped', () => 'hi') const app = new Elysia() .use(a) diff --git a/test/type-system/boolean-string.test.ts b/test/type-system/boolean-string.test.ts index bea1cf55d..a203fb24e 100644 --- a/test/type-system/boolean-string.test.ts +++ b/test/type-system/boolean-string.test.ts @@ -63,10 +63,10 @@ describe('TypeSystem - BooleanString', () => { expect(() => Value.Decode(schema, null)).toThrow(error) }) - it('Convert', () => { - expect(Value.Convert(t.BooleanString(), 'true')).toBe(true) - expect(Value.Convert(t.BooleanString(), 'false')).toBe(false) - }) + // it('Convert', () => { + // expect(Value.Convert(t.BooleanString(), 'true')).toBe(true) + // expect(Value.Convert(t.BooleanString(), 'false')).toBe(false) + // }) it('Integrate', async () => { const app = new Elysia().get('/', ({ query }) => query, { diff --git a/test/type-system/uint8array.test.ts b/test/type-system/uint8array.test.ts index d714e1f45..7e0b07875 100644 --- a/test/type-system/uint8array.test.ts +++ b/test/type-system/uint8array.test.ts @@ -63,11 +63,7 @@ describe('TypeSystem - Uint8Array', () => { // }) it('Integrate', async () => { - const app = new Elysia().post('/', ({ body }) => { - console.log(body) - - return body - }, { + const app = new Elysia().post('/', ({ body }) => body, { body: t.Uint8Array(), response: t.Uint8Array() }) diff --git a/test/type-system/union-enum.test.ts b/test/type-system/union-enum.test.ts index d8532e5e2..96985e0a4 100644 --- a/test/type-system/union-enum.test.ts +++ b/test/type-system/union-enum.test.ts @@ -14,11 +14,10 @@ describe('TypeSystem - UnionEnum', () => { }) it('Check', () => { - const schema = t.UnionEnum(['some', 'data', null]) + const schema = t.UnionEnum(['some', 'data']) expect(Value.Check(schema, 'some')).toBe(true) expect(Value.Check(schema, 'data')).toBe(true) - expect(Value.Check(schema, null)).toBe(true) expect(Value.Check(schema, { deep: 2 })).toBe(false) expect(Value.Check(schema, 'yay')).toBe(false) @@ -26,6 +25,7 @@ describe('TypeSystem - UnionEnum', () => { expect(Value.Check(schema, {})).toBe(false) expect(Value.Check(schema, undefined)).toBe(false) }) + it('JSON schema', () => { expect(t.UnionEnum(['some', 'data'])).toMatchObject({ type: 'string', @@ -36,27 +36,21 @@ describe('TypeSystem - UnionEnum', () => { type: 'number', enum: [2, 1] }) - expect(t.UnionEnum([null])).toMatchObject({ - type: 'null', - enum: [null] - }) }) + it('Integrate', async () => { const app = new Elysia().post('/', ({ body }) => body, { body: t.Object({ - value: t.UnionEnum(['some', 1, null]) + value: t.UnionEnum(['some', 1]) }) }) const res1 = await app.handle(post('/', { value: 1 })) expect(res1.status).toBe(200) - const res2 = await app.handle(post('/', { value: null })) + const res2 = await app.handle(post('/', { value: 'some' })) expect(res2.status).toBe(200) - const res3 = await app.handle(post('/', { value: 'some' })) - expect(res3.status).toBe(200) - - const res4 = await app.handle(post('/', { value: 'data' })) - expect(res4.status).toBe(422) + const res3 = await app.handle(post('/', { value: 'data' })) + expect(res3.status).toBe(422) }) }) diff --git a/test/types/index.ts b/test/types/index.ts index feff09ee9..fe8032445 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -2,12 +2,12 @@ import { t, Elysia, - RouteSchema, Cookie, - error, file, sse, - SSEPayload + SSEPayload, + status, + form } from '../../src' import { expectTypeOf } from 'expect-type' @@ -67,7 +67,7 @@ app.model({ // ? unwrap cookie expectTypeOf< - Record> & { + Record> & { username: Cookie password: Cookie } @@ -94,18 +94,16 @@ app.model({ '/', ({ body }) => { // ? unwrap body type - expectTypeOf< - { - username: string - password: string - }[] - >().toEqualTypeOf() + expectTypeOf<{ + username: string + password: string + }>().toEqualTypeOf() return body }, { - body: 't[]', - response: 't[]' + body: 't', + response: 't' } ) @@ -219,10 +217,10 @@ app.model({ '/', ({ body }) => { expectTypeOf().not.toBeUnknown() - expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() }, { - body: 'string[]' + body: 'string' } ) .model({ @@ -460,10 +458,10 @@ const b = app .post( '/', ({ body }) => { - expectTypeOf().toEqualTypeOf<'c'[]>() + expectTypeOf().toEqualTypeOf<'c'>() }, { - body: 'c[]' + body: 'c' } ) @@ -553,10 +551,10 @@ app.use(plugin) ({ body, decorate, store: { state } }) => { expectTypeOf().toBeString() expectTypeOf().toBeString() - expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() }, { - body: 'string[]' + body: 'string' } ) @@ -693,7 +691,7 @@ app.use(plugin).group( expectTypeOf().toEqualTypeOf<{ body: unknown - params: Record + params: {} query: unknown headers: unknown response: { @@ -733,14 +731,24 @@ app.use(plugin).group( type Route = App['v1']['a']['subscribe'] expectTypeOf().toEqualTypeOf<{ body: string - params: Record + params: {} query: { name: string } headers: { authorization: string } - response: unknown + response: { + 422: { + type: 'validation' + on: string + summary?: string + message?: string + found?: unknown + property?: string + expected?: string + } + } }>() } @@ -753,9 +761,9 @@ app.use(plugin).group( expectTypeOf().toEqualTypeOf<{ body: unknown - headers: unknown - query: unknown params: {} + query: unknown + headers: unknown response: { 200: string } @@ -959,9 +967,9 @@ app.group( expectTypeOf().toEqualTypeOf<{ body: unknown - headers: unknown + params: {} query: unknown - params: Record + headers: unknown response: { 200: string } @@ -986,9 +994,9 @@ app.group( test: { get: { body: unknown - headers: unknown + params: {} query: unknown - params: Record + headers: unknown response: { 200: string } @@ -1163,10 +1171,8 @@ const a = app }) { - app.macro(() => { - return { - a(a: string) {} - } + app.macro({ + a(a: string) {} }) .get('/', () => {}, { // ? Should contains macro @@ -1177,10 +1183,8 @@ const a = app // @ts-expect-error a: 1 }) - .macro(() => { - return { - b(a: number) {} - } + .macro({ + b(a: number) {} }) .get('/', () => {}, { // ? Should merge macro @@ -1243,7 +1247,7 @@ const a = app params: {} query: unknown headers: unknown - response: unknown + response: {} } } } @@ -1255,14 +1259,13 @@ const a = app // ? Handle error status { const a = new Elysia() - .get('/', ({ error }) => error(418, 'a'), { + .get('/', ({ status }) => status(418, 'a'), { response: { 200: t.String(), 418: t.Literal('a') } }) - // @ts-expect-error - .get('/', ({ error }) => error(418, 'b'), { + .get('/', ({ status }) => status(418, 'b' as any), { response: { 200: t.String(), 418: t.Literal('a') @@ -1277,18 +1280,18 @@ const a = app .get('/true', () => true) .post('', () => 'a', { response: { 201: t.String() } }) .post('/true', () => true, { response: { 202: t.Boolean() } }) - .get('/error', ({ error }) => error("I'm a teapot", 'a')) + .get('/error', ({ status }) => status("I'm a teapot", 'a')) .post('/mirror', ({ body }) => body) .get('/immutable', '1') - .get('/immutable-error', ({ error }) => error("I'm a teapot", 'a')) - .get('/async', async ({ error }) => { - if (Math.random() > 0.5) return error("I'm a teapot", 'Nagisa') + .get('/immutable-error', ({ status }) => status("I'm a teapot", 'a')) + .get('/async', async ({ status }) => { + if (Math.random() > 0.5) return status("I'm a teapot", 'Nagisa') return 'Hifumi' }) - .get('/default-error-code', ({ error }) => { - if (Math.random() > 0.5) return error(418, 'Nagisa') - if (Math.random() > 0.5) return error(401) + .get('/default-error-code', ({ status }) => { + if (Math.random() > 0.5) return status(418, 'Nagisa') + if (Math.random() > 0.5) return status(401) return 'Hifumi' }) @@ -1301,7 +1304,7 @@ const a = app expectTypeOf().toEqualTypeOf<{ 200: string - readonly 201: string + 201: string 422: { type: 'validation' on: string @@ -1319,7 +1322,7 @@ const a = app expectTypeOf().toEqualTypeOf<{ 200: boolean - readonly 202: boolean + 202: boolean 422: { type: 'validation' on: string @@ -1332,20 +1335,16 @@ const a = app }>() expectTypeOf().toEqualTypeOf<{ - 200: never 418: 'a' }>() - expectTypeOf().toEqualTypeOf<{ - 200: unknown - }>() + expectTypeOf().toEqualTypeOf<{}>() expectTypeOf().toEqualTypeOf<{ 200: '1' }>() expectTypeOf().toEqualTypeOf<{ - 200: never 418: 'a' }>() @@ -1414,12 +1413,12 @@ app.get('/', ({ set }) => { const child = new Elysia().get( '/', () => { - return { + return form({ a: file('test/kyuukurarin.mp4') - } + }) }, { - response: t.Object({ + response: t.Form({ a: t.File() }) } @@ -1643,9 +1642,9 @@ type a = keyof {} 401: t.Boolean() } }) - .get('/plugin', ({ error }) => { - error('Payment Required', 20) - return error(401, true) + .get('/plugin', ({ status }) => { + status('Payment Required', 20) + return status(401, true) }) const app = new Elysia().use(plugin).get('/', () => 'ok') @@ -1670,7 +1669,7 @@ type a = keyof {} 401: t.Boolean() } }) - .get('/plugin', error(401, true)) + .get('/plugin', status(401, true)) const app = new Elysia().use(plugin).get('/', 'ok') } @@ -1905,9 +1904,9 @@ type a = keyof {} 401: t.Boolean() } }) - .get('/plugin', ({ error }) => { - error('Payment Required', 20) - return error(401, true) + .get('/plugin', ({ status }) => { + status('Payment Required', 20) + return status(401, true) }) const app = new Elysia().use(plugin).get('/', () => 'ok') @@ -1932,7 +1931,7 @@ type a = keyof {} 401: t.Boolean() } }) - .get('/plugin', error(401, true)) + .get('/plugin', status(401, true)) const app = new Elysia().use(plugin).get('/', 'ok') } @@ -2014,32 +2013,46 @@ type a = keyof {} { new Elysia() .onParse(({ params }) => { - expectTypeOf().toEqualTypeOf<{}>() + expectTypeOf().toEqualTypeOf< + Record + >() }) .derive(({ params }) => { - expectTypeOf().toEqualTypeOf<{}>() + expectTypeOf().toEqualTypeOf< + Record + >() return {} }) .resolve(({ params }) => { - expectTypeOf().toEqualTypeOf<{}>() + expectTypeOf().toEqualTypeOf() return {} }) .onTransform(({ params }) => { - expectTypeOf().toEqualTypeOf<{}>() + expectTypeOf().toEqualTypeOf< + Record + >() }) .onBeforeHandle(({ params }) => { - expectTypeOf().toEqualTypeOf<{}>() + expectTypeOf().toEqualTypeOf< + Record + >() }) .onAfterHandle(({ params }) => { - expectTypeOf().toEqualTypeOf<{}>() + expectTypeOf().toEqualTypeOf< + Record + >() }) .mapResponse(({ params }) => { - expectTypeOf().toEqualTypeOf<{}>() + expectTypeOf().toEqualTypeOf< + Record + >() }) .onAfterResponse(({ params }) => { - expectTypeOf().toEqualTypeOf<{}>() + expectTypeOf().toEqualTypeOf< + Record + >() }) } @@ -2273,12 +2286,12 @@ type a = keyof {} return { beforeHandle({ - error, + status, cookie: { token }, store: { session } }) { if (!token.value) - return error(401, { + return status(401, { success: false, message: 'Unauthorized' }) @@ -2291,7 +2304,7 @@ type a = keyof {} session[token.value as unknown as number] if (!username) - return error(401, { + return status(401, { success: false, message: 'Unauthorized' }) @@ -2442,8 +2455,8 @@ type a = keyof {} expectTypeOf().toEqualTypeOf() }, { - a: true, - beforeHandle: (c) => {} + a: true + // beforeHandle: (c) => {} } ) .ws('/', { @@ -2567,7 +2580,7 @@ type a = keyof {} expectTypeOf< (typeof app)['~Routes']['get']['response'][200] >().toEqualTypeOf< - AsyncGenerator< + Generator< | { readonly data: 'a' } @@ -2595,7 +2608,7 @@ type a = keyof {} expectTypeOf< (typeof app)['~Routes']['get']['response'][200] >().toEqualTypeOf< - AsyncGenerator< + Generator< | { readonly data: 'a' } @@ -2649,13 +2662,9 @@ type a = keyof {} expectTypeOf< (typeof app)['~Routes']['get']['response'][200] >().toEqualTypeOf< - AsyncGenerator< - { - readonly data: 'a' - }, - void, - unknown - > + ReadableStream<{ + readonly data: 'a' + }> >() } @@ -2669,5 +2678,5 @@ type a = keyof {} expectTypeOf< (typeof app)['~Routes']['get']['response'][200] - >().toEqualTypeOf>() + >().toEqualTypeOf>() } diff --git a/test/types/lifecycle/soundness.ts b/test/types/lifecycle/soundness.ts new file mode 100644 index 000000000..c76687d31 --- /dev/null +++ b/test/types/lifecycle/soundness.ts @@ -0,0 +1,1852 @@ +import { Cookie, Elysia, ElysiaCustomStatusResponse, t } from '../../../src' +import { expectTypeOf } from 'expect-type' +import { Prettify } from '../../../src/types' + +// Handle resolve property correctly +{ + const app = new Elysia().resolve(({ status }) => { + if (Math.random() > 0.05) return status(401) + + return { + name: 'mokou' + } + }) + + type Resolve = (typeof app)['~Volatile']['resolve'] + expectTypeOf().toEqualTypeOf<{ + name: 'mokou' + }> +} + +// Handle resolve property without any data +{ + const app = new Elysia().resolve(({ status }) => { + if (Math.random() > 0.05) return status(401) + }) + + type Resolve = (typeof app)['~Volatile']['resolve'] + expectTypeOf().toEqualTypeOf<{}> +} + +// Type soundness of lifecycle event in local +{ + const app = new Elysia() + .onError(({ status }) => { + if (Math.random() > 0.05) return status(400) + }) + .resolve(({ status }) => { + if (Math.random() > 0.05) return status(401) + }) + .onBeforeHandle([ + ({ status }) => { + if (Math.random() > 0.05) return status(402) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(403) + } + ]) + .guard({ + beforeHandle: [ + ({ status }) => { + if (Math.random() > 0.05) return status(405) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(406) + } + ], + afterHandle({ status }) { + if (Math.random() > 0.05) return status(407) + }, + error({ status }) { + if (Math.random() > 0.05) return status(408) + } + }) + .get('/', ({ body, status }) => + Math.random() > 0.05 ? status(409) : ('Hello World' as const) + ) + + type Lifecycle = Prettify<(typeof app)['~Volatile']['response']> + + expectTypeOf().toEqualTypeOf<{ + 400: 'Bad Request' + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + }>() + + type Route = Prettify<(typeof app)['~Routes']['get']['response']> + + expectTypeOf().toEqualTypeOf<{ + 200: 'Hello World' + 400: 'Bad Request' + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + 409: 'Conflict' + }> +} + +// Type soundness of lifecycle event in scoped +{ + const app = new Elysia() + .onError(({ status }) => { + if (Math.random() > 0.05) return status(400) + }) + .resolve(({ status }) => { + if (Math.random() > 0.05) return status(401) + }) + .onBeforeHandle([ + ({ status }) => { + if (Math.random() > 0.05) return status(402) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(403) + } + ]) + .guard({ + beforeHandle: [ + ({ status }) => { + if (Math.random() > 0.05) return status(405) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(406) + } + ], + afterHandle({ status }) { + if (Math.random() > 0.05) return status(407) + }, + error({ status }) { + if (Math.random() > 0.05) return status(408) + } + }) + .as('scoped') + .get('/', ({ body, status }) => + Math.random() > 0.05 ? status(409) : ('Hello World' as const) + ) + + type Lifecycle = Prettify<(typeof app)['~Ephemeral']['response']> + + expectTypeOf().toEqualTypeOf<{ + 400: 'Bad Request' + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + }>() + + type Route = Prettify<(typeof app)['~Routes']['get']['response']> + + expectTypeOf().toEqualTypeOf<{ + 200: 'Hello World' + 400: 'Bad Request' + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + 409: 'Conflict' + }> +} + +// Type soundness of lifecycle event in global +{ + const app = new Elysia() + .onError(({ status }) => { + if (Math.random() > 0.05) return status(400) + }) + .resolve(({ status }) => { + if (Math.random() > 0.05) return status(401) + }) + .onBeforeHandle([ + ({ status }) => { + if (Math.random() > 0.05) return status(402) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(403) + } + ]) + .guard({ + beforeHandle: [ + ({ status }) => { + if (Math.random() > 0.05) return status(405) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(406) + } + ], + afterHandle({ status }) { + if (Math.random() > 0.05) return status(407) + }, + error({ status }) { + if (Math.random() > 0.05) return status(408) + } + }) + .as('global') + .get('/', ({ body, status }) => + Math.random() > 0.05 ? status(409) : ('Hello World' as const) + ) + + type Lifecycle = Prettify<(typeof app)['~Metadata']['response']> + + expectTypeOf().toEqualTypeOf<{ + 400: 'Bad Request' + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + }>() + + type Route = Prettify<(typeof app)['~Routes']['get']['response']> + + expectTypeOf().toEqualTypeOf<{ + 200: 'Hello World' + 400: 'Bad Request' + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + 409: 'Conflict' + }> +} + +// All together now +{ + const app = new Elysia() + .macro({ + auth: { + response: { + 409: t.Literal('Conflict') + }, + beforeHandle({ status }) { + if (Math.random() < 0.05) return status(410) + }, + resolve: () => ({ a: 'a' as const }) + } + }) + .onError(({ status }) => { + if (Math.random() < 0.05) return status(400) + }) + .resolve(({ status }) => { + if (Math.random() < 0.05) return status(401) + + return { + b: 'b' as const + } + }) + .onBeforeHandle([ + ({ status }) => { + if (Math.random() < 0.05) return status(402) + }, + ({ status }) => { + if (Math.random() < 0.05) return status(403) + } + ]) + .guard({ + beforeHandle: [ + ({ status }) => { + if (Math.random() < 0.05) return status(405) + }, + ({ status }) => { + if (Math.random() < 0.05) return status(406) + } + ], + afterHandle({ status }) { + if (Math.random() < 0.05) return status(407) + }, + error({ status }) { + if (Math.random() < 0.05) return status(408) + } + }) + .post( + '/', + ({ status, a, b }) => { + if (Math.random() < 0.05) return status(409, 'Conflict') + + expectTypeOf().toEqualTypeOf<'a'>() + expectTypeOf().toEqualTypeOf<'b'>() + + return 'Type Soundness' + }, + { + auth: true, + response: { + 411: t.Literal('Length Required') + } + } + ) + + type Lifecycle = (typeof app)['~Routes']['post']['response'] + + expectTypeOf().toEqualTypeOf<{ + 200: 'Type Soundness' + 400: 'Bad Request' + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + 409: 'Conflict' + 410: 'Gone' + 411: 'Length Required' + 422: { + type: 'validation' + on: string + summary?: string + message?: string + found?: unknown + property?: string + expected?: string + } + }>() +} + +// Macro without schema should not have 422 +{ + const app = new Elysia() + .macro({ + auth: { + beforeHandle({ status }) { + if (Math.random() < 0.05) return status(410) + } + } + }) + .get('/', () => 'Hello World' as const, { + auth: true + }) + + type Route = (typeof app)['~Routes']['get']['response'] + + expectTypeOf().toEqualTypeOf<{ + 200: 'Hello World' + 410: 'Gone' + }>() +} + +// Macro with schema should have 422 +{ + const app = new Elysia() + .macro({ + auth: { + response: { + 401: t.Literal('Unauthorized') + }, + beforeHandle({ status }) { + if (Math.random() < 0.05) return status(410) + } + } + }) + .get('/', () => 'Hello World' as const, { + auth: true + }) + + type Route = (typeof app)['~Routes']['get']['response'] + + expectTypeOf().toEqualTypeOf<{ + 200: 'Hello World' + 401: 'Unauthorized' + 410: 'Gone' + 422: { + type: 'validation' + on: string + summary?: string + message?: string + found?: unknown + property?: string + expected?: string + } + }>() +} + +// Macro should inject schema +{ + const app = new Elysia() + .macro({ + auth: { + body: t.Object({ + name: t.Literal('lilith') + }), + query: t.Object({ + name: t.Literal('lilith') + }), + headers: t.Object({ + name: t.Literal('lilith') + }), + params: t.Object({ + name: t.Literal('lilith') + }), + cookie: t.Object({ + name: t.Literal('lilith') + }), + response: { + 401: t.Literal('Unauthorized') + }, + beforeHandle({ status }) { + if (Math.random() < 0.05) return status(410) + } + } + }) + .get( + '/', + ({ headers, body, cookie, params, query, status }) => { + expectTypeOf().toEqualTypeOf<{ + name: 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'lilith' + }>() + + expectTypeOf().toEqualTypeOf< + Record> & { + name: Cookie<'lilith'> + } + >() + + expectTypeOf().toEqualTypeOf<{ + name: 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'lilith' + }>() + + if (Math.random() > 0.5) return status(401, 'Unauthorized') + + if (Math.random() > 0.5) + // @ts-expect-error + return status(401, 'Unauthorize') + + return 'Hello World' as const + }, + { + auth: true + } + ) + + type Route = (typeof app)['~Routes']['get']['response'] + + expectTypeOf().toEqualTypeOf<{ + 200: 'Hello World' + 401: 'Unauthorized' + 410: 'Gone' + 422: { + type: 'validation' + on: string + summary?: string + message?: string + found?: unknown + property?: string + expected?: string + } + }>() +} + +// Macro should inject schema to guard +{ + const app = new Elysia() + .macro({ + auth: { + body: t.Object({ + name: t.Literal('lilith') + }), + query: t.Object({ + name: t.Literal('lilith') + }), + headers: t.Object({ + name: t.Literal('lilith') + }), + params: t.Object({ + name: t.Literal('lilith') + }), + cookie: t.Object({ + name: t.Literal('lilith') + }), + response: { + 401: t.Literal('Unauthorized') + }, + beforeHandle({ status }) { + if (Math.random() < 0.05) return status(410) + } + } + }) + .guard({ + auth: true + }) + .get('/', ({ headers, body, cookie, params, query, status }) => { + expectTypeOf().toEqualTypeOf<{ + name: 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'lilith' + }>() + + expectTypeOf().toEqualTypeOf< + Record> & { + name: Cookie<'lilith'> + } + >() + + expectTypeOf().toEqualTypeOf<{ + name: 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'lilith' + }>() + + if (Math.random() > 0.5) return status(401, 'Unauthorized') + + if (Math.random() > 0.5) + // @ts-expect-error + return status(401, 'Unauthorize') + + return 'Hello World' as const + }) + + app['~Volatile']['standaloneSchema']['response']['401'] + type Route = (typeof app)['~Routes']['get']['response'] + + expectTypeOf().toEqualTypeOf<{ + 200: 'Hello World' + 401: 'Unauthorized' + 410: 'Gone' + 422: { + type: 'validation' + on: string + summary?: string + message?: string + found?: unknown + property?: string + expected?: string + } + }>() +} + +// Guard should extract possible status 1 +{ + const app = new Elysia().guard({ + beforeHandle({ status }) { + if (Math.random() < 0.05) return status(410) + } + }) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 410: 'Gone' + }>() +} + +// Guard should extract possible status 2 +{ + const app = new Elysia().guard({ + afterHandle({ status }) { + return status(411) + } + }) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 411: 'Length Required' + }>() +} + +// Guard should extract possible status 3 +{ + const app = new Elysia().guard({ + error: [ + ({ status }) => { + return status(412) + }, + ({ status }) => { + if (Math.random() > 0.5) return status(413) + } + ] + }) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 412: 'Precondition Failed' + 413: 'Payload Too Large' + }>() +} + +// Guard should extract possible status 4 +{ + const app = new Elysia().guard({ + beforeHandle({ status }) { + if (Math.random() < 0.05) return status(410) + }, + error: [ + ({ status }) => { + return status(412) + }, + ({ status }) => { + if (Math.random() > 0.5) return status(413) + } + ] + }) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 410: 'Gone' + 412: 'Precondition Failed' + 413: 'Payload Too Large' + }>() +} + +// Guard should extract possible status 5 +{ + const app = new Elysia() + .macro({ + a: { + beforeHandle({ status }) { + return status(409) + } + } + }) + .guard({ + a: true, + beforeHandle({ status }) { + if (Math.random() < 0.05) return status(410) + }, + error: [ + ({ status }) => { + return status(412) + }, + ({ status }) => { + if (Math.random() > 0.5) return status(413) + } + ] + }) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 409: 'Conflict' + 410: 'Gone' + 412: 'Precondition Failed' + 413: 'Payload Too Large' + }>() +} + +// Macro should extract possible status 1 +{ + const app = new Elysia() + .macro({ + a: { + beforeHandle({ status }) { + if (Math.random() < 0.05) return status(410) + } + } + }) + .guard({ + a: true + }) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 410: 'Gone' + }>() +} + +// Macro should extract possible status 2 +{ + const app = new Elysia() + .macro({ + a: { + afterHandle({ status }) { + return status(411) + } + } + }) + .guard({ + a: true + }) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 411: 'Length Required' + }>() +} + +// Macro should extract possible status 3 +{ + const app = new Elysia() + .macro({ + a: { + error({ status }) { + if (Math.random() > 0.5) return status(412) + } + } + }) + .guard({ + a: true + }) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 412: 'Precondition Failed' + }>() +} + +// Macro should extract possible status 4 +{ + const app = new Elysia() + .macro({ + a: { + beforeHandle({ status }) { + if (Math.random() > 0.5) return status(410) + }, + afterHandle({ status }) { + if (Math.random() > 0.5) return status(411) + } + }, + b: { + error({ status }) { + if (Math.random() > 0.5) return status(412) + } + } + }) + .guard({ + a: true, + b: true + }) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 410: 'Gone' + 411: 'Length Required' + 412: 'Precondition Failed' + }>() +} + +// Guard should cast to scoped +{ + const app = new Elysia() + .macro({ + a: { + beforeHandle({ status }) { + if (Math.random() > 0.5) return status(410) + }, + afterHandle({ status }) { + if (Math.random() > 0.5) return status(411) + } + }, + b: { + error({ status }) { + if (Math.random() > 0.5) return status(412) + } + } + }) + .guard({ + as: 'scoped', + a: true, + b: true + }) + + expectTypeOf<(typeof app)['~Ephemeral']['response']>().toEqualTypeOf<{ + 410: 'Gone' + 411: 'Length Required' + 412: 'Precondition Failed' + }>() +} + +// Guard should cast to global +{ + const app = new Elysia() + .macro({ + a: { + beforeHandle({ status }) { + if (Math.random() > 0.5) return status(410) + }, + afterHandle({ status }) { + if (Math.random() > 0.5) return status(411) + } + }, + b: { + error({ status }) { + if (Math.random() > 0.5) return status(412) + } + } + }) + .guard({ + as: 'global', + a: true, + b: true + }) + + expectTypeOf<(typeof app)['~Metadata']['response']>().toEqualTypeOf<{ + 410: 'Gone' + 411: 'Length Required' + 412: 'Precondition Failed' + }>() +} + +// Unwrap ElysiaCustomStatusResponse value in resolve macro automatically +{ + const app = new Elysia() + .macro({ + auth: { + resolve({ status }) { + if (Math.random() > 0.5) return status(401) + + return { user: 'saltyaom' } as const + } + } + }) + .get('/', ({ user }) => user, { + auth: true + }) + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'saltyaom' + 401: 'Unauthorized' + }>() +} + +// Unwrap beforeHandle 200 status +{ + const app = new Elysia() + .macro({ + auth: { + beforeHandle({ status }) { + if (Math.random() > 0.5) return status(401) + + if (Math.random() > 0.5) return 'lilith' + } + } + }) + .get('/', () => 'fouco' as const, { + auth: true + }) + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'lilith' | 'fouco' + 401: 'Unauthorized' + }>() +} + +// Reconcile response +{ + const app = new Elysia() + .onBeforeHandle(({ status }) => + Math.random() > 0.5 ? status(404, 'lilith') : 'lilith' + ) + .get('/', ({ status }) => + Math.random() > 0.5 ? status(404, 'fouco') : 'fouco' + ) + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'lilith' | 'fouco' + 404: 'lilith' | 'fouco' + }>() +} + +// onBeforeHandle +{ + const app = new Elysia() + .onBeforeHandle(({ status }) => + Math.random() > 0.5 ? status(404, 'lilith') : 'lilith' + ) + .onBeforeHandle([ + ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : 'fouco', + ({ status }) => + Math.random() > 0.5 ? status(418, 'sartre') : 'sartre' + ]) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 200: 'fouco' | 'sartre' | 'lilith' + 401: 'fouco' + 404: 'lilith' + 418: 'sartre' + }>() +} + +// onBeforeHandle scoped +{ + const app = new Elysia() + .onBeforeHandle({ as: 'scoped' }, ({ status }) => + Math.random() > 0.5 ? status(404, 'lilith') : 'lilith' + ) + .onBeforeHandle({ as: 'scoped' }, [ + ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : 'fouco', + ({ status }) => + Math.random() > 0.5 ? status(418, 'sartre') : 'sartre' + ]) + + expectTypeOf<(typeof app)['~Ephemeral']['response']>().toEqualTypeOf<{ + 200: 'fouco' | 'sartre' | 'lilith' + 401: 'fouco' + 404: 'lilith' + 418: 'sartre' + }>() +} + +// onBeforeHandle global +{ + const app = new Elysia() + .onBeforeHandle({ as: 'global' }, ({ status }) => + Math.random() > 0.5 ? status(404, 'lilith') : 'lilith' + ) + .onBeforeHandle({ as: 'global' }, [ + ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : 'fouco', + ({ status }) => + Math.random() > 0.5 ? status(418, 'sartre') : 'sartre' + ]) + + expectTypeOf<(typeof app)['~Metadata']['response']>().toEqualTypeOf<{ + 200: 'fouco' | 'sartre' | 'lilith' + 401: 'fouco' + 404: 'lilith' + 418: 'sartre' + }>() +} + +// onAfterHandle local +{ + const app = new Elysia() + .onAfterHandle(({ status }) => + Math.random() > 0.5 ? status(404, 'lilith') : 'lilith' + ) + .onAfterHandle([ + ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : 'fouco', + ({ status }) => + Math.random() > 0.5 ? status(418, 'sartre') : 'sartre' + ]) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 200: 'fouco' | 'sartre' | 'lilith' + 401: 'fouco' + 404: 'lilith' + 418: 'sartre' + }>() +} + +// onAfterHandle scoped +{ + const app = new Elysia() + .onAfterHandle({ as: 'scoped' }, ({ status }) => + Math.random() > 0.5 ? status(404, 'lilith') : 'lilith' + ) + .onAfterHandle({ as: 'scoped' }, [ + ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : 'fouco', + ({ status }) => + Math.random() > 0.5 ? status(418, 'sartre') : 'sartre' + ]) + + expectTypeOf<(typeof app)['~Ephemeral']['response']>().toEqualTypeOf<{ + 200: 'fouco' | 'sartre' | 'lilith' + 401: 'fouco' + 404: 'lilith' + 418: 'sartre' + }>() +} + +// onAfterHandle global +{ + const app = new Elysia() + .onAfterHandle({ as: 'global' }, ({ status }) => + Math.random() > 0.5 ? status(404, 'lilith') : 'lilith' + ) + .onAfterHandle({ as: 'global' }, [ + ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : 'fouco', + ({ status }) => + Math.random() > 0.5 ? status(418, 'sartre') : 'sartre' + ]) + + expectTypeOf<(typeof app)['~Metadata']['response']>().toEqualTypeOf<{ + 200: 'fouco' | 'sartre' | 'lilith' + 401: 'fouco' + 404: 'lilith' + 418: 'sartre' + }>() +} + +// onError local +{ + const app = new Elysia() + .onError(({ status }) => + Math.random() > 0.5 ? status(404, 'lilith') : 'lilith' + ) + .onError([ + ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : 'fouco', + ({ status }) => + Math.random() > 0.5 ? status(418, 'sartre') : 'sartre' + ]) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 200: 'fouco' | 'sartre' | 'lilith' + 401: 'fouco' + 404: 'lilith' + 418: 'sartre' + }>() +} + +// onError scoped +{ + const app = new Elysia() + .onError({ as: 'scoped' }, ({ status }) => + Math.random() > 0.5 ? status(404, 'lilith') : 'lilith' + ) + .onError({ as: 'scoped' }, [ + ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : 'fouco', + ({ status }) => + Math.random() > 0.5 ? status(418, 'sartre') : 'sartre' + ]) + + expectTypeOf<(typeof app)['~Ephemeral']['response']>().toEqualTypeOf<{ + 200: 'fouco' | 'sartre' | 'lilith' + 401: 'fouco' + 404: 'lilith' + 418: 'sartre' + }>() +} + +// onError global +{ + const app = new Elysia() + .onError({ as: 'global' }, ({ status }) => + Math.random() > 0.5 ? status(404, 'lilith') : 'lilith' + ) + .onError({ as: 'global' }, [ + ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : 'fouco', + ({ status }) => + Math.random() > 0.5 ? status(418, 'sartre') : 'sartre' + ]) + + expectTypeOf<(typeof app)['~Metadata']['response']>().toEqualTypeOf<{ + 200: 'fouco' | 'sartre' | 'lilith' + 401: 'fouco' + 404: 'lilith' + 418: 'sartre' + }>() +} + +// resolve local +{ + const app = new Elysia() + .resolve(({ status }) => + Math.random() > 0.5 + ? status(401, 'sartre') + : { friends: ['lilith'] } + ) + .resolve(({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : { friends: ['lilith'] } + ) + .get('/', ({ friends, status }) => { + if (Math.random() > 0.5) return status(401, friends[0]) + + return 'NOexistenceN' + }) + + expectTypeOf<(typeof app)['~Volatile']['resolve']>().toEqualTypeOf<{ + readonly friends: readonly ['lilith'] + }>() + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 401: 'sartre' | 'fouco' + }>() + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'sartre' | 'fouco' | 'lilith' + }>() +} + +// resolve scoped +{ + const app = new Elysia() + .resolve({ as: 'scoped' }, ({ status }) => + Math.random() > 0.5 + ? status(401, 'sartre') + : { friends: ['lilith'] } + ) + .resolve({ as: 'scoped' }, ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : { friends: ['lilith'] } + ) + .get('/', ({ friends, status }) => { + if (Math.random() > 0.5) return status(401, friends[0]) + + return 'NOexistenceN' + }) + + expectTypeOf<(typeof app)['~Ephemeral']['resolve']>().toEqualTypeOf<{ + readonly friends: readonly ['lilith'] + }>() + + expectTypeOf<(typeof app)['~Ephemeral']['response']>().toEqualTypeOf<{ + 401: 'sartre' | 'fouco' + }>() + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'sartre' | 'fouco' | 'lilith' + }>() +} + +// resolve global +{ + const app = new Elysia() + .resolve({ as: 'global' }, ({ status }) => + Math.random() > 0.5 + ? status(401, 'sartre') + : { friends: ['lilith'] } + ) + .resolve({ as: 'global' }, ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : { friends: ['lilith'] } + ) + .get('/', ({ friends, status }) => { + if (Math.random() > 0.5) return status(401, friends[0]) + + return 'NOexistenceN' + }) + + expectTypeOf<(typeof app)['~Singleton']['resolve']>().toEqualTypeOf<{ + readonly friends: readonly ['lilith'] + }>() + + expectTypeOf<(typeof app)['~Metadata']['response']>().toEqualTypeOf<{ + 401: 'sartre' | 'fouco' + }>() + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'sartre' | 'fouco' | 'lilith' + }>() +} + +// mapResolve local +{ + const app = new Elysia() + .mapResolve(({ status }) => + Math.random() > 0.5 + ? status(401, 'sartre') + : { friends: ['lilith'] } + ) + .mapResolve(({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : { friends: ['lilith'] } + ) + .get('/', ({ friends, status }) => { + if (Math.random() > 0.5) return status(401, friends[0]) + + return 'NOexistenceN' + }) + + expectTypeOf<(typeof app)['~Volatile']['resolve']>().toEqualTypeOf<{ + readonly friends: readonly ['lilith'] + }>() + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 401: 'sartre' | 'fouco' + }>() + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'sartre' | 'fouco' | 'lilith' + }>() +} + +// mapResolve scoped +{ + const app = new Elysia() + .mapResolve({ as: 'scoped' }, ({ status }) => + Math.random() > 0.5 + ? status(401, 'sartre') + : { friends: ['lilith'] } + ) + .mapResolve({ as: 'scoped' }, ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : { friends: ['lilith'] } + ) + .get('/', ({ friends, status }) => { + if (Math.random() > 0.5) return status(401, friends[0]) + + return 'NOexistenceN' + }) + + expectTypeOf<(typeof app)['~Ephemeral']['resolve']>().toEqualTypeOf<{ + readonly friends: readonly ['lilith'] + }>() + + expectTypeOf<(typeof app)['~Ephemeral']['response']>().toEqualTypeOf<{ + 401: 'sartre' | 'fouco' + }>() + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'sartre' | 'fouco' | 'lilith' + }>() +} + +// mapResolve global +{ + const app = new Elysia() + .mapResolve({ as: 'global' }, ({ status }) => + Math.random() > 0.5 + ? status(401, 'sartre') + : { friends: ['lilith'] } + ) + .mapResolve({ as: 'global' }, ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : { friends: ['lilith'] } + ) + .get('/', ({ friends, status }) => { + if (Math.random() > 0.5) return status(401, friends[0]) + + return 'NOexistenceN' + }) + + expectTypeOf<(typeof app)['~Singleton']['resolve']>().toEqualTypeOf<{ + readonly friends: readonly ['lilith'] + }>() + + expectTypeOf<(typeof app)['~Metadata']['response']>().toEqualTypeOf<{ + 401: 'sartre' | 'fouco' + }>() + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'sartre' | 'fouco' | 'lilith' + }>() +} + +// derive local +{ + const app = new Elysia() + .derive(({ status }) => + Math.random() > 0.5 + ? status(401, 'sartre') + : { friends: ['lilith'] } + ) + .derive(({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : { friends: ['lilith'] } + ) + .get('/', ({ friends, status }) => { + if (Math.random() > 0.5) return status(401, friends[0]) + + return 'NOexistenceN' + }) + + expectTypeOf<(typeof app)['~Volatile']['derive']>().toEqualTypeOf<{ + readonly friends: readonly ['lilith'] + }>() + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 401: 'sartre' | 'fouco' + }>() + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'sartre' | 'fouco' | 'lilith' + }>() +} + +// derive scoped +{ + const app = new Elysia() + .derive({ as: 'scoped' }, ({ status }) => + Math.random() > 0.5 + ? status(401, 'sartre') + : { friends: ['lilith'] } + ) + .derive({ as: 'scoped' }, ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : { friends: ['lilith'] } + ) + .get('/', ({ friends, status }) => { + if (Math.random() > 0.5) return status(401, friends[0]) + + return 'NOexistenceN' + }) + + expectTypeOf<(typeof app)['~Ephemeral']['derive']>().toEqualTypeOf<{ + readonly friends: readonly ['lilith'] + }>() + + expectTypeOf<(typeof app)['~Ephemeral']['response']>().toEqualTypeOf<{ + 401: 'sartre' | 'fouco' + }>() + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'sartre' | 'fouco' | 'lilith' + }>() +} + +// derive global +{ + const app = new Elysia() + .derive({ as: 'global' }, ({ status }) => + Math.random() > 0.5 + ? status(401, 'sartre') + : { friends: ['lilith'] } + ) + .derive({ as: 'global' }, ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : { friends: ['lilith'] } + ) + .get('/', ({ friends, status }) => { + if (Math.random() > 0.5) return status(401, friends[0]) + + return 'NOexistenceN' + }) + + expectTypeOf<(typeof app)['~Singleton']['derive']>().toEqualTypeOf<{ + readonly friends: readonly ['lilith'] + }>() + + expectTypeOf<(typeof app)['~Metadata']['response']>().toEqualTypeOf<{ + 401: 'sartre' | 'fouco' + }>() + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'sartre' | 'fouco' | 'lilith' + }>() +} + +// mapDerive local +{ + const app = new Elysia() + .mapDerive(({ status }) => + Math.random() > 0.5 + ? status(401, 'sartre') + : { friends: ['lilith'] } + ) + .mapDerive(({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : { friends: ['lilith'] } + ) + .get('/', ({ friends, status }) => { + if (Math.random() > 0.5) return status(401, friends[0]) + + return 'NOexistenceN' + }) + + expectTypeOf<(typeof app)['~Volatile']['derive']>().toEqualTypeOf<{ + readonly friends: readonly ['lilith'] + }>() + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 401: 'sartre' | 'fouco' + }>() + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'sartre' | 'fouco' | 'lilith' + }>() +} + +// mapDerive scoped +{ + const app = new Elysia() + .mapDerive({ as: 'scoped' }, ({ status }) => + Math.random() > 0.5 + ? status(401, 'sartre') + : { friends: ['lilith'] } + ) + .mapDerive({ as: 'scoped' }, ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : { friends: ['lilith'] } + ) + .get('/', ({ friends, status }) => { + if (Math.random() > 0.5) return status(401, friends[0]) + + return 'NOexistenceN' + }) + + expectTypeOf<(typeof app)['~Ephemeral']['derive']>().toEqualTypeOf<{ + readonly friends: readonly ['lilith'] + }>() + + expectTypeOf<(typeof app)['~Ephemeral']['response']>().toEqualTypeOf<{ + 401: 'sartre' | 'fouco' + }>() + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'sartre' | 'fouco' | 'lilith' + }>() +} + +// mapDerive global +{ + const app = new Elysia() + .mapDerive({ as: 'global' }, ({ status }) => + Math.random() > 0.5 + ? status(401, 'sartre') + : { friends: ['lilith'] } + ) + .mapDerive({ as: 'global' }, ({ status }) => + Math.random() > 0.5 ? status(401, 'fouco') : { friends: ['lilith'] } + ) + .get('/', ({ friends, status }) => { + if (Math.random() > 0.5) return status(401, friends[0]) + + return 'NOexistenceN' + }) + + expectTypeOf<(typeof app)['~Singleton']['derive']>().toEqualTypeOf<{ + readonly friends: readonly ['lilith'] + }>() + + expectTypeOf<(typeof app)['~Metadata']['response']>().toEqualTypeOf<{ + 401: 'sartre' | 'fouco' + }>() + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'sartre' | 'fouco' | 'lilith' + }>() +} + +// Guard local +{ + const app = new Elysia() + .macro({ + q: { + beforeHandle: [ + ({ status }) => { + if (Math.random() > 0.05) return status(401) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(402) + } + ], + afterHandle({ status }) { + if (Math.random() > 0.05) return status(403) + }, + error({ status }) { + if (Math.random() > 0.05) return status(404, 'lilith') + } + } + }) + .guard({ + q: true, + beforeHandle: [ + ({ status }) => { + if (Math.random() > 0.05) return status(405) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(406) + } + ], + afterHandle({ status }) { + if (Math.random() > 0.05) return status(407) + }, + error({ status }) { + if (Math.random() > 0.05) return status(408) + } + }) + .get('/', () => 'NOexistenceN' as const) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 404: 'lilith' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + }>() + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 404: 'lilith' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + }>() +} + +// Guard scoped +{ + const app = new Elysia() + .macro({ + q: { + beforeHandle: [ + ({ status }) => { + if (Math.random() > 0.05) return status(401) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(402) + } + ], + afterHandle({ status }) { + if (Math.random() > 0.05) return status(403) + }, + error({ status }) { + if (Math.random() > 0.05) return status(404, 'lilith') + } + } + }) + .guard({ + as: 'scoped', + q: true, + beforeHandle: [ + ({ status }) => { + if (Math.random() > 0.05) return status(405) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(406) + } + ], + afterHandle({ status }) { + if (Math.random() > 0.05) return status(407) + }, + error({ status }) { + if (Math.random() > 0.05) return status(408) + } + }) + .get('/', () => 'NOexistenceN' as const) + + expectTypeOf<(typeof app)['~Ephemeral']['response']>().toEqualTypeOf<{ + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 404: 'lilith' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + }>() + + type A = keyof (typeof app)['~Routes']['get']['response'] + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 404: 'lilith' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + }>() +} + +// Guard global +{ + const app = new Elysia() + .macro({ + q: { + beforeHandle: [ + ({ status }) => { + if (Math.random() > 0.05) return status(401) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(402) + } + ], + afterHandle({ status }) { + if (Math.random() > 0.05) return status(403) + }, + error({ status }) { + if (Math.random() > 0.05) return status(404, 'lilith') + } + } + }) + .guard({ + as: 'global', + q: true, + beforeHandle: [ + ({ status }) => { + if (Math.random() > 0.05) return status(405) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(406) + } + ], + afterHandle({ status }) { + if (Math.random() > 0.05) return status(407) + }, + error({ status }) { + if (Math.random() > 0.05) return status(408) + } + }) + .get('/', () => 'NOexistenceN' as const) + + expectTypeOf<(typeof app)['~Metadata']['response']>().toEqualTypeOf<{ + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 404: 'lilith' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + }>() + + type A = keyof (typeof app)['~Routes']['get']['response'] + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 404: 'lilith' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + }>() +} + +// Multiple macro +{ + const app = new Elysia() + .macro({ + q: { + beforeHandle: [ + ({ status }) => { + if (Math.random() > 0.05) return status(401) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(402) + } + ], + afterHandle({ status }) { + if (Math.random() > 0.05) return status(403) + }, + error({ status }) { + if (Math.random() > 0.05) return status(404, 'lilith') + } + }, + a: { + beforeHandle: ({ status }) => { + if (Math.random() > 0.05) return status(405) + }, + afterHandle: [ + ({ status }) => { + if (Math.random() > 0.05) return status(406) + }, + ({ status }) => { + if (Math.random() > 0.05) return status(407) + } + ], + error({ status }) { + if (Math.random() > 0.05) return status(408) + } + } + }) + + .guard({ + q: true, + a: true + }) + .get('/', () => 'NOexistenceN' as const) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 404: 'lilith' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + }>() + + type A = keyof (typeof app)['~Routes']['get']['response'] + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'NOexistenceN' + 401: 'Unauthorized' + 402: 'Payment Required' + 403: 'Forbidden' + 404: 'lilith' + 405: 'Method Not Allowed' + 406: 'Not Acceptable' + 407: 'Proxy Authentication Required' + 408: 'Request Timeout' + }>() +} + +// merge possible path +{ + const app = new Elysia() + .onBeforeHandle(({ status }) => { + if (Math.random() > 0.05) return 'fouco' as const + if (Math.random() > 0.05) return 'sartre' as const + if (Math.random() > 0.05) return status(404, 'lilith') + }) + .get('/', () => 'lilith' as const) + + expectTypeOf<(typeof app)['~Volatile']['response']>().toEqualTypeOf<{ + 200: 'fouco' | 'sartre' + 404: 'lilith' + }> + + expectTypeOf<(typeof app)['~Routes']['get']['response']>().toEqualTypeOf<{ + 200: 'fouco' | 'sartre' | 'lilith' + 404: 'lilith' + }>() +} + +// Macro Context should add to output declaration +{ + const app = new Elysia() + .macro({ + a: { + query: t.Object({ + name: t.Literal('lilith') + }), + cookie: t.Object({ + name: t.Literal('lilith') + }), + params: t.Object({ + name: t.Literal('lilith') + }), + body: t.Object({ + name: t.Literal('lilith') + }), + headers: t.Object({ + name: t.Literal('lilith') + }), + response: { + 403: t.Object({ + name: t.Literal('lilith') + }) + } + } + }) + .post('/', ({ body }) => 'b' as const, { + a: true, + beforeHandle({ body }) { + expectTypeOf(body).toEqualTypeOf<{ + name: 'lilith' + }>() + } + }) + + expectTypeOf<(typeof app)['~Routes']['post']>().toEqualTypeOf<{ + body: { + name: 'lilith' + } + params: { + name: 'lilith' + } + query: { + name: 'lilith' + } + headers: { + name: 'lilith' + } + response: { + 200: 'b' + 403: { + name: 'lilith' + } + 422: { + type: 'validation' + on: string + summary?: string + message?: string + found?: unknown + property?: string + expected?: string + } + } + }>() +} + +// Macro Context schema and inline schema works together even in inline lifecycle +{ + const app = new Elysia() + .macro({ + withFriends: { + body: t.Object({ + friends: t.Tuple([t.Literal('Sartre'), t.Literal('Fouco')]) + }) + } + }) + .post( + '/', + ({ body }) => { + expectTypeOf(body).toEqualTypeOf<{ + name: 'Lilith' + friends: ['Sartre', 'Fouco'] + }>() + + return body + }, + { + body: t.Object({ + name: t.Literal('Lilith') + }), + withFriends: true, + response: { + 418: t.Literal('Teapot') + }, + beforeHandle({ body }) { + expectTypeOf(body).toEqualTypeOf<{ + name: 'Lilith' + friends: ['Sartre', 'Fouco'] + }>() + } + } + ) + + expectTypeOf<(typeof app)['~Routes']['post']>().toEqualTypeOf<{ + body: { + name: 'Lilith' + friends: ['Sartre', 'Fouco'] + } + params: {} + query: {} + headers: {} + response: { + 200: { + name: 'Lilith' + friends: ['Sartre', 'Fouco'] + } + 418: 'Teapot' + 422: { + type: 'validation' + on: string + summary?: string + message?: string + found?: unknown + property?: string + expected?: string + } + } + }>() +} + +// resolve for lifecycle event +{ + new Elysia() + .macro('auth', { + headers: t.Object({ authorization: t.String() }), + resolve: ({ status }) => + Math.random() > 0.5 + ? { role: 'user' } + : status(401, 'not authorized') + }) + .post('/', ({ role }) => role, { + auth: true, + beforeHandle: ({ role }) => {} + }) +} diff --git a/test/types/macro.ts b/test/types/macro.ts index 13abb5ec7..fed8f0684 100644 --- a/test/types/macro.ts +++ b/test/types/macro.ts @@ -1,4 +1,4 @@ -import { Elysia } from '../../src' +import { Elysia, t } from '../../src' import { expectTypeOf } from 'expect-type' // guard handle resolve macro @@ -6,7 +6,7 @@ import { expectTypeOf } from 'expect-type' const plugin = new Elysia() .macro({ account: (a: boolean) => ({ - resolve: ({ error }) => ({ + resolve: () => ({ account: 'A' }) }) @@ -32,7 +32,7 @@ import { expectTypeOf } from 'expect-type' const plugin = new Elysia() .macro({ account: (a: boolean) => ({ - resolve: ({ error }) => ({ + resolve: () => ({ account: 'A' }) }) @@ -60,7 +60,7 @@ import { expectTypeOf } from 'expect-type' const plugin = new Elysia() .macro({ account: (a: boolean) => ({ - resolve: ({ error }) => ({ + resolve: () => ({ account: 'A' }) }) @@ -89,7 +89,7 @@ import { expectTypeOf } from 'expect-type' const plugin = new Elysia() .macro({ account: (a: boolean) => ({ - resolve: ({ error }) => ({ + resolve: () => ({ account: 'A' }) }) @@ -116,8 +116,8 @@ import { expectTypeOf } from 'expect-type' const plugin = new Elysia() .macro({ account: (a: boolean) => ({ - resolve: ({ error }) => { - if (Math.random() > 0.5) return error(401) + resolve: ({ status }) => { + if (Math.random() > 0.5) return status(401) return { account: 'A' @@ -146,8 +146,8 @@ import { expectTypeOf } from 'expect-type' const plugin = new Elysia() .macro({ account: (a: boolean) => ({ - resolve: async ({ error }) => { - if (Math.random() > 0.5) return error(401) + resolve: async ({ status }) => { + if (Math.random() > 0.5) return status(401) return { account: 'A' @@ -230,3 +230,68 @@ import { expectTypeOf } from 'expect-type' } ) } + +// resolve with custom status +{ + const app = new Elysia() + .macro({ + auth: { + resolve: [ + ({ status }) => { + if (Math.random() > 0.5) return status(401) + + return { user: 'saltyaom' } as const + } + ] + } + }) + .get('/', ({ user }) => user, { + auth: true + }) +} + +// retrieve resolve conditionally +const app = new Elysia() + .macro({ + user: (enabled: true) => ({ + resolve() { + if (!enabled) return + + return { + user: 'a' + } + } + }) + }) + .get( + '/', + ({ user, status }) => { + if (!user) return status(401) + + return { hello: 'hanabi' } + }, + { + user: true + } + ) + +// Macro name extends macro +{ + new Elysia() + .macro('a', { + body: t.Object({ a: t.Literal('A') }), + beforeHandle({ body }) { + expectTypeOf(body).toEqualTypeOf<{ a: 'A' }>() + } + }) + .macro('b', { + a: true, + body: t.Object({ b: t.Literal('B') }), + beforeHandle({ body }) { + expectTypeOf(body).toEqualTypeOf<{ + a: 'A' + b: 'B' + }>() + } + }) +} diff --git a/test/types/schema-standalone.ts b/test/types/schema-standalone.ts index 9977896ac..74d12437e 100644 --- a/test/types/schema-standalone.ts +++ b/test/types/schema-standalone.ts @@ -869,7 +869,7 @@ import { expectTypeOf } from 'expect-type' }>() expectTypeOf().toEqualTypeOf< - Record> & { + Record> & { family: Cookie name: Cookie } @@ -907,7 +907,7 @@ import { expectTypeOf } from 'expect-type' }>() expectTypeOf().toEqualTypeOf< - Record> & { + Record> & { name: Cookie } >() @@ -944,7 +944,7 @@ import { expectTypeOf } from 'expect-type' }>() expectTypeOf().toEqualTypeOf< - Record> & { + Record> & { name: Cookie } >() @@ -1002,7 +1002,7 @@ import { expectTypeOf } from 'expect-type' }>() expectTypeOf().toEqualTypeOf< - Record> & { + Record> & { family: Cookie name: Cookie } @@ -1044,7 +1044,7 @@ import { expectTypeOf } from 'expect-type' }>() expectTypeOf().toEqualTypeOf< - Record> & { + Record> & { family: Cookie name: Cookie } @@ -1082,7 +1082,7 @@ import { expectTypeOf } from 'expect-type' }>() expectTypeOf().toEqualTypeOf< - Record> & { + Record> & { name: Cookie } >() @@ -1140,7 +1140,7 @@ import { expectTypeOf } from 'expect-type' }>() expectTypeOf().toEqualTypeOf< - Record> & { + Record> & { family: Cookie name: Cookie } @@ -1182,7 +1182,7 @@ import { expectTypeOf } from 'expect-type' }>() expectTypeOf().toEqualTypeOf< - Record> & { + Record> & { family: Cookie name: Cookie } @@ -1224,7 +1224,7 @@ import { expectTypeOf } from 'expect-type' }>() expectTypeOf().toEqualTypeOf< - Record> & { + Record> & { family: Cookie name: Cookie } diff --git a/test/types/standard-schema/index.ts b/test/types/standard-schema/index.ts new file mode 100644 index 000000000..54e05ecae --- /dev/null +++ b/test/types/standard-schema/index.ts @@ -0,0 +1,505 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Cookie, Elysia, t } from '../../../src' + +import z from 'zod' + +import { expectTypeOf } from 'expect-type' + +// ? handle standard schema +{ + new Elysia().post( + '/:name', + ({ + params, + params: { name }, + body, + query, + headers, + cookie, + status + }) => { + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf< + Record> & { + name: Cookie<'fouco' | 'lilith'> + } + >() + + // @ts-expect-error + status(404, 'fouco') + + // @ts-expect-error + status(418, 'lilith') + + return name === 'lilith' + ? status(404, 'lilith') + : status(418, name as any) + }, + { + body: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + query: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + params: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + headers: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + cookie: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + response: { + 404: z.literal('lilith'), + 418: z.literal('fouco') + } + } + ) +} + +// ? handle standard schema single response +{ + new Elysia() + .get('/lilith', () => 'lilith' as const, { + response: z.literal('lilith') + }) + .get('/lilith', 'lilith', { + response: z.literal('lilith') + }) + // @ts-expect-error + .get('/lilith', () => 'focou' as const, { + response: z.literal('lilith') + }) +} + +// ? handle standard schema from reference +{ + new Elysia() + .model({ + body: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + query: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + params: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + headers: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + cookie: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + 'response.404': z.literal('lilith'), + 'response.418': z.literal('fouco') + }) + .post( + '/:name', + ({ + params, + params: { name }, + body, + query, + headers, + cookie, + status + }) => { + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf< + Record> & { + name: Cookie<'fouco' | 'lilith'> + } + >() + + // @ts-expect-error + status(404, 'fouco') + + // @ts-expect-error + status(418, 'lilith') + + return name === 'lilith' + ? status(404, 'lilith') + : status(418, name as any) + }, + { + body: 'body', + query: 'query', + params: 'params', + headers: 'headers', + cookie: 'cookie', + response: { + 404: 'response.404', + 418: 'response.418' + } + } + ) +} + +// ? handle standard schema from guard +{ + new Elysia() + .guard({ + body: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + query: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + params: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + headers: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + cookie: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + response: { + 404: z.literal('lilith'), + 418: z.literal('fouco') + } + }) + .post( + '/:name', + ({ + params, + params: { name }, + body, + query, + headers, + cookie, + status + }) => { + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf< + Record> & { + name: Cookie<'fouco' | 'lilith'> + } + >() + + // @ts-expect-error + status(404, 'fouco') + + // @ts-expect-error + status(418, 'lilith') + + return name === 'lilith' + ? status(404, 'lilith') + : status(418, name as any) + } + ) +} + +// ? merge standard schema response status from guard +{ + new Elysia() + .guard({ + response: { + 418: z.literal('fouco') + } + }) + .get('/lilith', () => 'lilith' as const, { + response: z.literal('lilith') + }) + .get('/lilith', 'lilith', { + response: z.literal('lilith') + }) + // @ts-expect-error + .get('/lilith', () => 'focou' as const, { + response: z.literal('lilith') + }) + .get('/fouco', ({ status }) => status(418, 'fouco'), { + response: z.literal('lilith') + }) + // @ts-expect-error + .get('/fouco', ({ status }) => status(418, 'lilith'), { + response: z.literal('lilith') + }) +} + +// ? merge standalone standard schema +{ + new Elysia() + .guard({ + schema: 'standalone', + body: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + query: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + params: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + headers: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + cookie: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + response: { + 404: z.object({ + name: z.literal('lilith') + }), + 418: z.object({ + name: z.literal('fouco') + }) + } + }) + .post( + '/:name', + ({ + params, + params: { name }, + body, + query, + headers, + cookie, + status + }) => { + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + q: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + q: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + q: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + q: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf< + Record> & { + name: Cookie<'fouco' | 'lilith'> + q: Cookie<'fouco' | 'lilith'> + } + >() + + status(404, { + // @ts-expect-error + name: 'fouco', + // @ts-expect-error + q: 'fouco' + }) + + status(418, { + // @ts-expect-error + name: 'lilith', + // @ts-expect-error + q: 'lilith' + }) + + return name === 'lilith' + ? status(404, { + name, + q: 'lilith' + }) + : status(418, { + name, + q: 'fouco' + }) + }, + { + body: t.Object({ + q: t.UnionEnum(['lilith', 'fouco']) + }), + query: t.Object({ + q: t.UnionEnum(['lilith', 'fouco']) + }), + params: t.Object({ + q: t.UnionEnum(['lilith', 'fouco']) + }), + headers: t.Object({ + q: t.UnionEnum(['lilith', 'fouco']) + }), + cookie: t.Object({ + q: t.UnionEnum(['lilith', 'fouco']) + }), + response: { + 404: t.Object({ + q: t.Literal('lilith') + }), + 418: t.Object({ + q: t.Literal('fouco') + }) + } + } + ) +} + +// ? merge standalone standard schema from plugin +{ + const plugin = new Elysia().guard({ + as: 'scoped', + schema: 'standalone', + body: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + query: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + params: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + headers: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + cookie: z.object({ + name: z.literal('fouco').or(z.literal('lilith')) + }), + response: { + 404: z.object({ + name: z.literal('lilith') + }), + 418: z.object({ + name: z.literal('fouco') + }) + } + }) + + new Elysia().use(plugin).post( + '/:name', + ({ + params, + params: { name }, + body, + query, + headers, + cookie, + status + }) => { + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + q: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + q: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + q: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf<{ + name: 'fouco' | 'lilith' + q: 'fouco' | 'lilith' + }>() + + expectTypeOf().toEqualTypeOf< + Record> & { + q: Cookie<'fouco' | 'lilith'> + name: Cookie<'fouco' | 'lilith'> + } + >() + + status(404, { + // @ts-expect-error + name: 'fouco', + // @ts-expect-error + q: 'fouco' + }) + + status(418, { + // @ts-expect-error + name: 'lilith', + // @ts-expect-error + q: 'lilith' + }) + + return name === 'lilith' + ? status(404, { + name, + q: 'lilith' + }) + : status(418, { + name, + q: 'fouco' + }) + }, + { + body: t.Object({ + q: t.UnionEnum(['lilith', 'fouco']) + }), + query: t.Object({ + q: t.UnionEnum(['lilith', 'fouco']) + }), + params: t.Object({ + q: t.UnionEnum(['lilith', 'fouco']) + }), + headers: t.Object({ + q: t.UnionEnum(['lilith', 'fouco']) + }), + cookie: t.Object({ + q: t.UnionEnum(['lilith', 'fouco']) + }), + response: { + 404: t.Object({ + q: t.Literal('lilith') + }), + 418: t.Object({ + q: t.Literal('fouco') + }) + } + } + ) +} diff --git a/test/validator/encode.test.ts b/test/validator/encode.test.ts index 21950ed80..03cadbe53 100644 --- a/test/validator/encode.test.ts +++ b/test/validator/encode.test.ts @@ -34,8 +34,8 @@ describe('Encode response', () => { encodeSchema: true }).get( '/:id', - ({ error, params: { id } }) => - error(id as any, { + ({ status, params: { id } }) => + status(id as any, { id: 'hello world' }), { diff --git a/test/validator/query.test.ts b/test/validator/query.test.ts index f557121c1..7ee0e7bd9 100644 --- a/test/validator/query.test.ts +++ b/test/validator/query.test.ts @@ -256,15 +256,13 @@ describe('Query Validator', () => { '/', ({ query }) => query?.name ?? 'sucrose', { - query: t.Optional( - t.Object( - { - name: t.String() - }, - { - additionalProperties: true - } - ) + query: t.Object( + { + name: t.Optional(t.String()) + }, + { + additionalProperties: true + } ) } ) @@ -332,7 +330,6 @@ describe('Query Validator', () => { check() { const { state } = ctx.query - // @ts-expect-error if (!checker.check(ctx, name, state ?? ctx.query.state)) throw new Error('State mismatch') } @@ -930,7 +927,7 @@ describe('Query Validator', () => { { query: t.Object({ id: t - .Transform(t.Array(t.UnionEnum(['test', 'foo']))) + .Transform(t.UnionEnum(['test', 'foo'])) .Decode((id) => ({ value: id })) .Encode((id) => id.value) }) @@ -943,7 +940,7 @@ describe('Query Validator', () => { expect(response).toEqual({ id: { - value: ['test'] + value: 'test' }, type: 'object' }) @@ -970,19 +967,17 @@ describe('Query Validator', () => { it('handle coerce TransformDecodeError', async () => { let err: Error | undefined - const app = new Elysia() - .get('/', ({ query }) => query, { - query: t.Object({ - year: t.Numeric({ minimum: 1900, maximum: 2160 }) - }), - error({ code, error }) { - switch (code) { - case 'VALIDATION': - err = error - } + const app = new Elysia().get('/', ({ query }) => query, { + query: t.Object({ + year: t.Numeric({ minimum: 1900, maximum: 2160 }) + }), + error({ code, error }) { + switch (code) { + case 'VALIDATION': + err = error } - }) - .listen(0) + } + }) await app.handle(req('?year=3000')) @@ -1026,4 +1021,87 @@ describe('Query Validator', () => { $test: 2 }) }) + + it("don't populate object query on failed validation", async () => { + const app = new Elysia().get('/', ({ query }) => query, { + query: t.Object({ + filter: t.Object({ + latlng: t.Object({ + within: t.Object({ + ne: t.Number(), + sw: t.Number() + }) + }), + zoom: t.Object({ + equalTo: t.Number({ + minimum: 0, + maximum: 20, + multipleOf: 1 + }) + }) + }) + }) + }) + + const filter = JSON.stringify({ + latlng: { + within: { + ne: 1, + sw: 1 + } + }, + zoom: { + equalTo: 2 + } + }) + + const valid = await app.handle( + new Request(`http://localhost:3000/?filter=${filter}`) + ) + const invalid1 = await app.handle(new Request(`http://localhost:3000`)) + const invalid2 = await app.handle( + new Request( + `http://localhost:3000?filter=${JSON.stringify({ zoom: { equalTo: 21 } })}` + ) + ) + + expect(valid.status).toBe(200) + expect(invalid1.status).toBe(422) + expect(invalid2.status).toBe(422) + }) + + it("don't populate array query on failed validation", async () => { + const app = new Elysia().get('/', ({ query }) => query, { + query: t.Object({ + party: t.Array( + t.Object({ + name: t.String() + }) + ) + }) + }) + + const filter = JSON.stringify([ + { + name: 'lilith' + }, + { + name: 'fouco' + } + ]) + + const valid = await app.handle( + new Request(`http://localhost:3000/?party=${filter}`) + ) + const invalid1 = await app.handle(new Request(`http://localhost:3000`)) + const invalid2 = await app.handle( + new Request( + `http://localhost:3000?filter=[]` + ) + ) + + expect(valid.status).toBe(200) + expect(invalid1.status).toBe(422) + expect(invalid2.status).toBe(422) + }) }) diff --git a/test/validator/response.test.ts b/test/validator/response.test.ts index ee4080a67..24a030745 100644 --- a/test/validator/response.test.ts +++ b/test/validator/response.test.ts @@ -1,4 +1,4 @@ -import { Elysia, error, t } from '../../src' +import { Elysia, t } from '../../src' import { describe, expect, it } from 'bun:test' import { post, req, upload } from '../utils' @@ -295,18 +295,22 @@ describe('Response Validator', () => { }) it('validate response per status with error()', async () => { - const app = new Elysia().get('/', () => error(418, 'I am a teapot'), { - response: { - 200: t.String(), - 418: t.String() + const app = new Elysia().get( + '/', + ({ status }) => status(418, 'I am a teapot'), + { + response: { + 200: t.String(), + 418: t.String() + } } - }) + ) }) it('use inline error from handler', async () => { const app = new Elysia().get( '/', - ({ error }) => error(418, 'I am a teapot'), + ({ status }) => status(418, 'I am a teapot'), { response: { 200: t.String(), @@ -379,7 +383,7 @@ describe('Response Validator', () => { it('return error response with validator', async () => { const app = new Elysia() - .get('/ok', ({ error }) => 'ok', { + .get('/ok', () => 'ok', { response: { 200: t.String(), 418: t.Literal('Kirifuji Nagisa'), @@ -388,7 +392,7 @@ describe('Response Validator', () => { }) .get( '/error', - ({ error }) => error("I'm a teapot", 'Kirifuji Nagisa'), + ({ status }) => status("I'm a teapot", 'Kirifuji Nagisa'), { response: { 200: t.String(), @@ -399,8 +403,8 @@ describe('Response Validator', () => { ) .get( '/validate-error', - // @ts-expect-error - ({ error }) => error("I'm a teapot", 'Nagisa'), + // @ts-ignore + ({ status }) => status("I'm a teapot", 'Nagisa'), { response: { 200: t.String(), diff --git a/test/ws/aot.test.ts b/test/ws/aot.test.ts index 734764ce4..0ce34e73e 100644 --- a/test/ws/aot.test.ts +++ b/test/ws/aot.test.ts @@ -10,7 +10,6 @@ describe('WebSocket with AoT disabled', () => { }) .listen(0) - // @ts-expect-error some properties are missing const ws = newWebsocket(app.server!) await wsOpen(ws)