From 32cb70c58ead57586a945e33c88fd3ee56a65832 Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 31 Jan 2025 20:11:46 +0700 Subject: [PATCH] feat(contract, server)!: rewrite builders (#110) * wip * types tests * wip * 100% test coverage * fix types * fix types * remove ~type and decorated method * procedure builders * rename * router builders * builder * remove redundant method * wip * router * wip * fix isLazy * procedure tests * router tests * builder tests * 100% test coverage * wip * fixed * fix types tests * wip * fix types * ORPCErrorConstructorMap * improve * wip * improve * improve * wip * improve * improve * fix * prefix lazy * fix * procedure implementer * type tests * tests * index * variants tests * setRouterContract * fix * docs * docs * tests * clear mock before each tests * lint fixed * fix docs * fix restrict context * fix types * improve middlewares --- apps/content/content/docs/contract-first.mdx | 7 +- apps/content/content/docs/index.mdx | 6 +- .../content/docs/openapi/generator.mdx | 2 +- apps/content/content/docs/server/context.mdx | 12 +- apps/content/content/docs/server/contract.mdx | 7 +- .../content/docs/server/error-handling.mdx | 5 +- apps/content/content/docs/server/lazy.mdx | 4 +- .../content/docs/server/middleware.mdx | 9 +- .../content/content/docs/server/procedure.mdx | 15 +- .../content/docs/server/server-action.mdx | 4 +- apps/content/content/home/landing.mdx | 7 +- apps/content/examples/contract.ts | 11 +- apps/content/examples/middleware.ts | 6 +- apps/content/examples/server.ts | 6 +- eslint.config.js | 1 + package.json | 4 +- .../client/src/adapters/fetch/orpc-link.ts | 12 +- packages/client/src/client.test-d.ts | 8 +- packages/client/src/client.ts | 6 +- packages/client/src/types.ts | 2 +- packages/client/tests/helpers.ts | 10 +- packages/contract/package.json | 2 +- .../contract/src/builder-variants.test-d.ts | 330 ++++++ packages/contract/src/builder-variants.ts | 115 ++ packages/contract/src/builder.test-d.ts | 153 ++- packages/contract/src/builder.test.ts | 162 +-- packages/contract/src/builder.ts | 164 +-- packages/contract/src/client-utils.test-d.ts | 3 +- packages/contract/src/client-utils.test.ts | 4 +- packages/contract/src/client-utils.ts | 3 +- packages/contract/src/config.ts | 4 +- packages/contract/src/error-map.test-d.ts | 11 + packages/contract/src/error-map.test.ts | 9 + packages/contract/src/error-map.ts | 57 +- packages/contract/src/error-orpc.test.ts | 100 +- packages/contract/src/error-orpc.ts | 82 +- packages/contract/src/error-utils.test-d.ts | 27 + packages/contract/src/error-utils.test.ts | 118 ++ packages/contract/src/error-utils.ts | 81 ++ packages/contract/src/error.test.ts | 7 + packages/contract/src/index.ts | 20 +- packages/contract/src/meta.test.ts | 6 + packages/contract/src/meta.ts | 5 + .../procedure-builder-with-input.test-d.ts | 87 -- .../src/procedure-builder-with-input.test.ts | 98 -- .../src/procedure-builder-with-input.ts | 47 - .../procedure-builder-with-output.test-d.ts | 88 -- .../src/procedure-builder-with-output.test.ts | 98 -- .../src/procedure-builder-with-output.ts | 47 - .../contract/src/procedure-builder.test-d.ts | 81 -- .../contract/src/procedure-builder.test.ts | 100 -- packages/contract/src/procedure-builder.ts | 50 - .../contract/src/procedure-client.test-d.ts | 34 +- packages/contract/src/procedure-client.ts | 2 +- .../src/procedure-decorated.test-d.ts | 99 -- .../contract/src/procedure-decorated.test.ts | 120 -- packages/contract/src/procedure-decorated.ts | 65 - packages/contract/src/procedure.test-d.ts | 12 - packages/contract/src/procedure.test.ts | 31 +- packages/contract/src/procedure.ts | 33 +- packages/contract/src/route.test-d.ts | 29 - packages/contract/src/route.test.ts | 37 +- packages/contract/src/route.ts | 70 +- .../contract/src/router-builder.test-d.ts | 179 --- packages/contract/src/router-builder.test.ts | 99 -- packages/contract/src/router-builder.ts | 100 -- packages/contract/src/router-client.test-d.ts | 26 +- packages/contract/src/router-client.ts | 6 +- packages/contract/src/router.test-d.ts | 151 ++- packages/contract/src/router.test.ts | 32 + packages/contract/src/router.ts | 74 +- packages/contract/src/schema-utils.test-d.ts | 28 - packages/contract/src/schema.test-d.ts | 70 ++ .../{schema-utils.test.ts => schema.test.ts} | 2 +- .../src/{schema-utils.ts => schema.ts} | 20 + packages/contract/src/types.test-d.ts | 46 +- packages/contract/src/types.ts | 21 - packages/contract/tests/shared.ts | 59 + .../adapters/fetch/openapi-handler.test.ts | 37 +- .../src/adapters/fetch/openapi-handler.ts | 21 +- .../adapters/fetch/openapi-payload-codec.ts | 6 +- .../fetch/openapi-procedure-matcher.ts | 11 +- .../src/adapters/fetch/schema-coercer.ts | 2 +- packages/openapi/src/openapi-generator.ts | 14 +- .../src/openapi-input-structure-parser.ts | 7 +- .../src/openapi-output-structure-parser.ts | 6 +- packages/openapi/src/schema-converter.ts | 4 +- packages/openapi/src/utils.ts | 37 +- packages/react-query/src/types.ts | 12 +- packages/react-query/src/utils-general.ts | 2 +- packages/react-query/src/utils-procedure.ts | 12 +- .../react-query/src/utils-router.test-d.ts | 8 +- .../src/adapters/fetch/orpc-handler.test-d.ts | 4 +- .../src/adapters/fetch/orpc-handler.test.ts | 23 +- .../server/src/adapters/fetch/orpc-handler.ts | 3 +- .../src/adapters/fetch/orpc-payload-codec.ts | 5 +- .../adapters/fetch/orpc-procedure-matcher.ts | 9 +- packages/server/src/adapters/fetch/types.ts | 2 +- packages/server/src/adapters/next/serve.ts | 10 +- packages/server/src/adapters/node/types.ts | 4 +- .../server/src/builder-variants.test-d.ts | 1047 +++++++++++++++++ packages/server/src/builder-variants.ts | 302 +++++ .../builder-with-errors-middlewares.test-d.ts | 163 --- .../builder-with-errors-middlewares.test.ts | 179 --- .../src/builder-with-errors-middlewares.ts | 165 --- .../server/src/builder-with-errors.test-d.ts | 210 ---- .../server/src/builder-with-errors.test.ts | 183 --- packages/server/src/builder-with-errors.ts | 188 --- .../src/builder-with-middlewares.test-d.ts | 150 --- .../src/builder-with-middlewares.test.ts | 171 --- .../server/src/builder-with-middlewares.ts | 161 --- packages/server/src/builder.test-d.ts | 464 ++++++-- packages/server/src/builder.test.ts | 271 +++-- packages/server/src/builder.ts | 298 +++-- packages/server/src/context.test-d.ts | 12 + packages/server/src/context.test.ts | 6 + packages/server/src/context.ts | 11 +- packages/server/src/error.test-d.ts | 42 - packages/server/src/error.test.ts | 47 - packages/server/src/error.ts | 46 - packages/server/src/hidden.test.ts | 113 +- packages/server/src/hidden.ts | 27 +- .../src/implementer-chainable.test-d.ts | 131 --- .../server/src/implementer-chainable.test.ts | 146 --- packages/server/src/implementer-chainable.ts | 72 -- .../src/implementer-procedure.test-d.ts | 335 ++++++ packages/server/src/implementer-procedure.ts | 118 ++ .../server/src/implementer-variants.test-d.ts | 147 +++ packages/server/src/implementer-variants.ts | 39 + packages/server/src/implementer.test-d.ts | 212 ++++ packages/server/src/implementer.test.ts | 153 +++ packages/server/src/implementer.ts | 192 +++ packages/server/src/index.ts | 21 +- packages/server/src/lazy-decorated.test-d.ts | 90 -- packages/server/src/lazy-decorated.test.ts | 54 - packages/server/src/lazy-decorated.ts | 33 - packages/server/src/lazy-utils.test-d.ts | 11 +- packages/server/src/lazy-utils.test.ts | 30 +- packages/server/src/lazy-utils.ts | 28 +- packages/server/src/lazy.test-d.ts | 53 - packages/server/src/lazy.test.ts | 54 +- packages/server/src/lazy.ts | 36 +- .../server/src/middleware-decorated.test-d.ts | 142 +-- packages/server/src/middleware-decorated.ts | 52 +- packages/server/src/middleware-utils.test.ts | 29 + packages/server/src/middleware-utils.ts | 25 + packages/server/src/middleware.test-d.ts | 111 +- packages/server/src/middleware.ts | 15 +- .../procedure-builder-with-input.test-d.ts | 129 -- .../src/procedure-builder-with-input.test.ts | 128 -- .../src/procedure-builder-with-input.ts | 112 -- .../procedure-builder-with-output.test-d.ts | 97 -- .../src/procedure-builder-with-output.test.ts | 101 -- .../src/procedure-builder-with-output.ts | 97 -- .../server/src/procedure-builder.test-d.ts | 100 -- packages/server/src/procedure-builder.test.ts | 106 -- packages/server/src/procedure-builder.ts | 90 -- .../server/src/procedure-client.test-d.ts | 282 +---- packages/server/src/procedure-client.test.ts | 44 +- packages/server/src/procedure-client.ts | 38 +- .../server/src/procedure-decorated.test-d.ts | 391 +++--- .../server/src/procedure-decorated.test.ts | 317 ++--- packages/server/src/procedure-decorated.ts | 137 +-- .../src/procedure-implementer.test-d.ts | 157 --- .../server/src/procedure-implementer.test.ts | 86 -- packages/server/src/procedure-implementer.ts | 85 -- packages/server/src/procedure-utils.test-d.ts | 61 +- packages/server/src/procedure-utils.test.ts | 38 +- packages/server/src/procedure.test-d.ts | 31 +- packages/server/src/procedure.test.ts | 31 +- packages/server/src/procedure.ts | 60 +- .../src/router-accessible-lazy.test-d.ts | 21 + .../server/src/router-accessible-lazy.test.ts | 33 + packages/server/src/router-accessible-lazy.ts | 34 + packages/server/src/router-builder.test-d.ts | 278 ----- packages/server/src/router-builder.test.ts | 329 ------ packages/server/src/router-builder.ts | 172 --- packages/server/src/router-client.test-d.ts | 152 +-- packages/server/src/router-client.test.ts | 103 +- packages/server/src/router-client.ts | 17 +- .../server/src/router-implementer.test-d.ts | 127 -- .../server/src/router-implementer.test.ts | 107 -- packages/server/src/router-implementer.ts | 66 -- packages/server/src/router-utils.ts | 54 - packages/server/src/router.test-d.ts | 349 ++---- packages/server/src/router.test.ts | 166 ++- packages/server/src/router.ts | 143 ++- packages/server/src/types.ts | 13 - packages/server/tests/shared.ts | 56 + packages/server/tsconfig.json | 2 +- packages/shared/src/chain.ts | 7 + packages/shared/src/hook.ts | 2 +- packages/shared/src/index.ts | 1 + packages/vue-colada/src/utils-general.ts | 2 +- packages/vue-colada/src/utils-procedure.ts | 8 +- .../vue-colada/src/utils-router.test-d.ts | 8 +- packages/vue-query/src/types.ts | 12 +- packages/vue-query/src/utils-general.ts | 2 +- packages/vue-query/src/utils-procedure.ts | 12 +- packages/vue-query/src/utils-router.test-d.ts | 8 +- packages/zod/src/schemas.ts | 4 +- playgrounds/contract-openapi/src/orpc.ts | 12 +- .../contract-openapi/src/router/planet.ts | 12 +- playgrounds/expressjs/src/orpc.ts | 24 +- playgrounds/expressjs/src/router/planet.ts | 12 +- playgrounds/nextjs/src/orpc.ts | 4 +- playgrounds/nextjs/src/router/planet.ts | 12 +- playgrounds/nuxt/server/orpc.ts | 24 +- playgrounds/nuxt/server/router/planet.ts | 12 +- playgrounds/openapi/src/orpc.ts | 41 +- playgrounds/openapi/src/router/planet.ts | 12 +- pnpm-lock.yaml | 205 ++-- 212 files changed, 6439 insertions(+), 9509 deletions(-) create mode 100644 packages/contract/src/builder-variants.test-d.ts create mode 100644 packages/contract/src/builder-variants.ts create mode 100644 packages/contract/src/error-map.test-d.ts create mode 100644 packages/contract/src/error-map.test.ts create mode 100644 packages/contract/src/error-utils.test-d.ts create mode 100644 packages/contract/src/error-utils.test.ts create mode 100644 packages/contract/src/error-utils.ts create mode 100644 packages/contract/src/error.test.ts create mode 100644 packages/contract/src/meta.test.ts create mode 100644 packages/contract/src/meta.ts delete mode 100644 packages/contract/src/procedure-builder-with-input.test-d.ts delete mode 100644 packages/contract/src/procedure-builder-with-input.test.ts delete mode 100644 packages/contract/src/procedure-builder-with-input.ts delete mode 100644 packages/contract/src/procedure-builder-with-output.test-d.ts delete mode 100644 packages/contract/src/procedure-builder-with-output.test.ts delete mode 100644 packages/contract/src/procedure-builder-with-output.ts delete mode 100644 packages/contract/src/procedure-builder.test-d.ts delete mode 100644 packages/contract/src/procedure-builder.test.ts delete mode 100644 packages/contract/src/procedure-builder.ts delete mode 100644 packages/contract/src/procedure-decorated.test-d.ts delete mode 100644 packages/contract/src/procedure-decorated.test.ts delete mode 100644 packages/contract/src/procedure-decorated.ts delete mode 100644 packages/contract/src/procedure.test-d.ts delete mode 100644 packages/contract/src/route.test-d.ts delete mode 100644 packages/contract/src/router-builder.test-d.ts delete mode 100644 packages/contract/src/router-builder.test.ts delete mode 100644 packages/contract/src/router-builder.ts create mode 100644 packages/contract/src/router.test.ts delete mode 100644 packages/contract/src/schema-utils.test-d.ts create mode 100644 packages/contract/src/schema.test-d.ts rename packages/contract/src/{schema-utils.test.ts => schema.test.ts} (92%) rename packages/contract/src/{schema-utils.ts => schema.ts} (57%) create mode 100644 packages/contract/tests/shared.ts create mode 100644 packages/server/src/builder-variants.test-d.ts create mode 100644 packages/server/src/builder-variants.ts delete mode 100644 packages/server/src/builder-with-errors-middlewares.test-d.ts delete mode 100644 packages/server/src/builder-with-errors-middlewares.test.ts delete mode 100644 packages/server/src/builder-with-errors-middlewares.ts delete mode 100644 packages/server/src/builder-with-errors.test-d.ts delete mode 100644 packages/server/src/builder-with-errors.test.ts delete mode 100644 packages/server/src/builder-with-errors.ts delete mode 100644 packages/server/src/builder-with-middlewares.test-d.ts delete mode 100644 packages/server/src/builder-with-middlewares.test.ts delete mode 100644 packages/server/src/builder-with-middlewares.ts create mode 100644 packages/server/src/context.test-d.ts create mode 100644 packages/server/src/context.test.ts delete mode 100644 packages/server/src/error.test-d.ts delete mode 100644 packages/server/src/error.test.ts delete mode 100644 packages/server/src/error.ts delete mode 100644 packages/server/src/implementer-chainable.test-d.ts delete mode 100644 packages/server/src/implementer-chainable.test.ts delete mode 100644 packages/server/src/implementer-chainable.ts create mode 100644 packages/server/src/implementer-procedure.test-d.ts create mode 100644 packages/server/src/implementer-procedure.ts create mode 100644 packages/server/src/implementer-variants.test-d.ts create mode 100644 packages/server/src/implementer-variants.ts create mode 100644 packages/server/src/implementer.test-d.ts create mode 100644 packages/server/src/implementer.test.ts create mode 100644 packages/server/src/implementer.ts delete mode 100644 packages/server/src/lazy-decorated.test-d.ts delete mode 100644 packages/server/src/lazy-decorated.test.ts delete mode 100644 packages/server/src/lazy-decorated.ts delete mode 100644 packages/server/src/lazy.test-d.ts create mode 100644 packages/server/src/middleware-utils.test.ts create mode 100644 packages/server/src/middleware-utils.ts delete mode 100644 packages/server/src/procedure-builder-with-input.test-d.ts delete mode 100644 packages/server/src/procedure-builder-with-input.test.ts delete mode 100644 packages/server/src/procedure-builder-with-input.ts delete mode 100644 packages/server/src/procedure-builder-with-output.test-d.ts delete mode 100644 packages/server/src/procedure-builder-with-output.test.ts delete mode 100644 packages/server/src/procedure-builder-with-output.ts delete mode 100644 packages/server/src/procedure-builder.test-d.ts delete mode 100644 packages/server/src/procedure-builder.test.ts delete mode 100644 packages/server/src/procedure-builder.ts delete mode 100644 packages/server/src/procedure-implementer.test-d.ts delete mode 100644 packages/server/src/procedure-implementer.test.ts delete mode 100644 packages/server/src/procedure-implementer.ts create mode 100644 packages/server/src/router-accessible-lazy.test-d.ts create mode 100644 packages/server/src/router-accessible-lazy.test.ts create mode 100644 packages/server/src/router-accessible-lazy.ts delete mode 100644 packages/server/src/router-builder.test-d.ts delete mode 100644 packages/server/src/router-builder.test.ts delete mode 100644 packages/server/src/router-builder.ts delete mode 100644 packages/server/src/router-implementer.test-d.ts delete mode 100644 packages/server/src/router-implementer.test.ts delete mode 100644 packages/server/src/router-implementer.ts delete mode 100644 packages/server/src/router-utils.ts delete mode 100644 packages/server/src/types.ts create mode 100644 packages/server/tests/shared.ts create mode 100644 packages/shared/src/chain.ts diff --git a/apps/content/content/docs/contract-first.mdx b/apps/content/content/docs/contract-first.mdx index d53423ed..45d4ada2 100644 --- a/apps/content/content/docs/contract-first.mdx +++ b/apps/content/content/docs/contract-first.mdx @@ -90,16 +90,15 @@ export type Outputs = InferContractRouterOutputs With your contract defined, implement the server logic: ```ts twoslash -import { os, ORPCError } from '@orpc/server' +import { implement, ORPCError } from '@orpc/server' import { contract } from 'examples/contract' -export const pub = os.contract(contract) // Ensure every implement must be match contract -export const authed = os +export const pub = implement(contract) // Ensure every implement must be match contract +export const authed = pub .use(({ context, path, next }, input) => { /** put auth logic here */ return next({}) }) - .contract(contract) export const router = pub.router({ getUser: pub.getUser.handler(({ input, context }) => { diff --git a/apps/content/content/docs/index.mdx b/apps/content/content/docs/index.mdx index 5dc4493b..6dca3c66 100644 --- a/apps/content/content/docs/index.mdx +++ b/apps/content/content/docs/index.mdx @@ -62,7 +62,7 @@ import { z } from 'zod' export type Context = { user?: { id: string } } // global pub, authed completely optional -export const pub = os.context() +export const pub = os.$context() export const authed = pub.use(({ context, path, next }, input) => { /** put auth logic here */ return next({}) @@ -98,9 +98,7 @@ export const router = pub.router({ ) .use(async ({ context, path, next }, input) => { if (!context.user) { - throw new ORPCError({ - code: 'UNAUTHORIZED', - }) + throw new ORPCError('UNAUTHORIZED') } const result = await next({ diff --git a/apps/content/content/docs/openapi/generator.mdx b/apps/content/content/docs/openapi/generator.mdx index 32bc7698..a7f1519f 100644 --- a/apps/content/content/docs/openapi/generator.mdx +++ b/apps/content/content/docs/openapi/generator.mdx @@ -19,7 +19,7 @@ import { z } from 'zod' export type Context = { user?: { id: string } } // global pub, authed completely optional -export const pub = os.context() +export const pub = os.$context() export const authed = pub.use(({ context, path, next }, input) => { /** put auth logic here */ return next({}) diff --git a/apps/content/content/docs/server/context.mdx b/apps/content/content/docs/server/context.mdx index 0660f854..afbaa00f 100644 --- a/apps/content/content/docs/server/context.mdx +++ b/apps/content/content/docs/server/context.mdx @@ -18,9 +18,9 @@ import { z } from 'zod' import { headers } from 'next/headers' type ORPCContext = { db: 'fake-db', user?: { id: string } } -const base = os.context() // define `initial context` +const base = os.$context() // define `initial context` -const pub = os.context().use(async ({ context, path, next }, input) => { +const pub = os.$context().use(async ({ context, path, next }, input) => { const headersList = await headers() // the `user` is `middleware context`, because it is created by middleware @@ -58,13 +58,13 @@ const base = os.use(async ({ context, path, next }, input) => { }) const authMid = os - .context<{ db: string }>() // this middleware depends on some context + .$context<{ db: string }>() // this middleware depends on some context .middleware(async ({ context, next, path }, input) => { const headersList = await headers() const user = headersList.get('Authorization') ? { id: 'example' } : undefined if (!user) { - throw new ORPCError({ code: 'UNAUTHORIZED' }) + throw new ORPCError('UNAUTHORIZED') } return next({ @@ -108,11 +108,11 @@ import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fe type ORPCContext = { user?: { id: string }, db: 'fake-db' } -const base = os.context() +const base = os.$context() const authMid = base.middleware(({ context, next, path }, input) => { if(!context.user) { - throw new ORPCError({ code: 'UNAUTHORIZED' }) + throw new ORPCError('UNAUTHORIZED') } return next({ diff --git a/apps/content/content/docs/server/contract.mdx b/apps/content/content/docs/server/contract.mdx index 615d8521..11e9580f 100644 --- a/apps/content/content/docs/server/contract.mdx +++ b/apps/content/content/docs/server/contract.mdx @@ -78,18 +78,17 @@ export const contract = oc.router({ All `server` features are available, except the input, output, and route parts, which are defined in the contract. ```ts twoslash -import { os } from '@orpc/server' +import { implement } from '@orpc/server' import { contract } from 'examples/contract' export type Context = { user?: { id: string } } -export const base = os.context() -export const pub = base.contract(contract) // Ensure every implement must be match contract +export const base = implement(contract).$context() +export const pub = base export const authed = base .use(({ context, path, next }, input) => { /** put auth logic here */ return next({}) }) - .contract(contract) export const router = pub.router({ getUser: pub.getUser.handler(({ input, context }) => { diff --git a/apps/content/content/docs/server/error-handling.mdx b/apps/content/content/docs/server/error-handling.mdx index 2f5b851a..ebfb90b5 100644 --- a/apps/content/content/docs/server/error-handling.mdx +++ b/apps/content/content/docs/server/error-handling.mdx @@ -26,7 +26,7 @@ const createPost = os throw errors.CONFLICT({ data: { why: 'some reason' } }) // is the same as, but this is not typed - throw new ORPCError({ code: 'CONFLICT', data: { why: 'some reason' } }) + throw new ORPCError('CONFLICT', { data: { why: 'some reason' } }) // throw errors.ANY_CODE() }) @@ -82,8 +82,7 @@ const ping = os } }) .handler(({ input, context }) => { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Not found', status: 404, // Optional: custom default behavior data: { something: 'include in the body and send to the client' } // pass data to the client diff --git a/apps/content/content/docs/server/lazy.mdx b/apps/content/content/docs/server/lazy.mdx index e87ccdaa..e10f8ce5 100644 --- a/apps/content/content/docs/server/lazy.mdx +++ b/apps/content/content/docs/server/lazy.mdx @@ -16,7 +16,7 @@ Here's how you can set up and use them: ```typescript twoslash import { os, call } from '@orpc/server' -const pub = os.context<{ user?: { id: string } }>() +const pub = os.$context<{ user?: { id: string } }>() // Define a router with lazy loading const router = pub.router({ @@ -48,7 +48,7 @@ const output = await call(router.lazy.getUser, { id: '123' }) ```ts twoslash import { os } from '@orpc/server' - const pub = os.context<{ user?: { id: string } }>() + const pub = os.$context<{ user?: { id: string } }>() const lazy = pub.prefix('/some').lazy(() => import('examples/server')) ``` diff --git a/apps/content/content/docs/server/middleware.mdx b/apps/content/content/docs/server/middleware.mdx index f1e359da..59a0ef62 100644 --- a/apps/content/content/docs/server/middleware.mdx +++ b/apps/content/content/docs/server/middleware.mdx @@ -12,12 +12,11 @@ import { os, ORPCError } from '@orpc/server' export type Context = { user?: { id: string } } -export const pub = os.context() +export const pub = os.$context() const authMiddleware = pub.middleware(async ({ context, next, path }, input) => { if (!context.user) { - throw new ORPCError({ - code: 'UNAUTHORIZED', + throw new ORPCError('UNAUTHORIZED', { message: 'You need to log in first', }) } @@ -86,13 +85,13 @@ type Context = { } } -export const pub = os.context() +export const pub = os.$context() // Any procedure using this middleware will infer context.user as NonNullable const authMiddleware = pub .middleware(async ({ context, next, path }, input) => { if (!context.user) { - throw new ORPCError({ code: 'UNAUTHORIZED' }) + throw new ORPCError('UNAUTHORIZED') } return next({ diff --git a/apps/content/content/docs/server/procedure.mdx b/apps/content/content/docs/server/procedure.mdx index 533f343d..bbd85b41 100644 --- a/apps/content/content/docs/server/procedure.mdx +++ b/apps/content/content/docs/server/procedure.mdx @@ -21,13 +21,11 @@ import { os, ORPCError } from '@orpc/server' // Define context type for full type inference const pub = os - .context<{user?: {id: string}}>() - .config({ // optional, change some default configuration - initialRoute: { - method: 'DELETE', // change default method to DELETE - inputStructure: 'detailed', // change default input structure to detailed - outputStructure: 'detailed' // change default output structure to detailed - } + .$context<{user?: {id: string}}>() // optional, set config + .$route({ // optional, change initial route + method: 'DELETE', // change default method to DELETE + inputStructure: 'detailed', // change default input structure to detailed + outputStructure: 'detailed' // change default output structure to detailed }) const getUser = pub @@ -40,8 +38,7 @@ const getUser = pub // Example: Authentication check if (!context.user) { - throw new ORPCError({ - code: 'UNAUTHORIZED', + throw new ORPCError('UNAUTHORIZED', { message: 'Authentication required' }) } diff --git a/apps/content/content/docs/server/server-action.mdx b/apps/content/content/docs/server/server-action.mdx index 366f8ffb..72457f55 100644 --- a/apps/content/content/docs/server/server-action.mdx +++ b/apps/content/content/docs/server/server-action.mdx @@ -43,7 +43,7 @@ When calling `.actionable()`, you can pass a context function that provides addi import { os } from '@orpc/server' import { z } from 'zod' -const pub = os.context<{ db: string }>() +const pub = os.$context<{ db: string }>() export const getting = pub .input(z.object({ @@ -77,7 +77,7 @@ const authed = os.use(async ({ next }) => { const user = headersList.get('Authorization') ? { id: 'example' } : undefined if (!user) { - throw new ORPCError({ code: 'UNAUTHORIZED' }) + throw new ORPCError('UNAUTHORIZED') } return next({ diff --git a/apps/content/content/home/landing.mdx b/apps/content/content/home/landing.mdx index 224cbe8b..3098ab6c 100644 --- a/apps/content/content/home/landing.mdx +++ b/apps/content/content/home/landing.mdx @@ -115,12 +115,12 @@ convert `1992` into a `bigint` and seamlessly parse objects like `user`. ```tsx type ORPCContext = { db: DB, user?: { id: string } } -const pub = os.context() +const pub = os.$context() const createPost = pub .use(({ context, path, next }, input) => { if(!context.user){ - throw new ORPCError({ code: 'UNAUTHORIZED' }) + throw new ORPCError('UNAUTHORIZED') } return next({ @@ -144,8 +144,9 @@ const getUserContract = oc .input({/*something*/}) .output({/*something*/}) +const os = implement(getUserContract) + const getUser = os - .contract(getUserContract) .handler(({ input }) => /* handle user */) ``` diff --git a/apps/content/examples/contract.ts b/apps/content/examples/contract.ts index 9aec05fd..cbe36f64 100644 --- a/apps/content/examples/contract.ts +++ b/apps/content/examples/contract.ts @@ -1,6 +1,6 @@ import type { InferContractRouterInputs, InferContractRouterOutputs } from '@orpc/contract' import { oc } from '@orpc/contract' -import { ORPCError, os } from '@orpc/server' +import { implement, ORPCError } from '@orpc/server' import { oz, ZodCoercer } from '@orpc/zod' import { z } from 'zod' @@ -68,14 +68,13 @@ export type Outputs = InferContractRouterOutputs // Implement the contract export type Context = { user?: { id: string } } -export const base = os.context() -export const pub = base.contract(contract) // Ensure every implement must be match contract +export const base = implement(contract).$context() +export const pub = base export const authed = base .use(({ context, path, next }, input) => { /** put auth logic here */ return next({}) }) - .contract(contract) export const router = pub.router({ getUser: pub.getUser.handler(({ input, context }) => { @@ -89,9 +88,7 @@ export const router = pub.router({ getPost: pub.posts.getPost .use(async ({ context, path, next }, input) => { if (!context.user) { - throw new ORPCError({ - code: 'UNAUTHORIZED', - }) + throw new ORPCError('UNAUTHORIZED') } const result = await next({ diff --git a/apps/content/examples/middleware.ts b/apps/content/examples/middleware.ts index 1273f62a..97bcd92a 100644 --- a/apps/content/examples/middleware.ts +++ b/apps/content/examples/middleware.ts @@ -5,11 +5,11 @@ import { z } from 'zod' export type Context = { user?: { id: string } } -export const pub = os.context() +export const pub = os.$context() export const authMiddleware = pub.middleware(async ({ context, next, path, procedure }, input) => { if (!context.user) { - throw new ORPCError({ code: 'UNAUTHORIZED' }) + throw new ORPCError('UNAUTHORIZED') } const result = await next({ context: { user: context.user } }) @@ -23,7 +23,7 @@ export const canEditPost = authMiddleware.concat( // Now you expect to have id in input async ({ context, next }, input: { id: string }) => { if (context.user.id !== input.id) { - throw new ORPCError({ code: 'UNAUTHORIZED' }) + throw new ORPCError('UNAUTHORIZED') } return next({}) diff --git a/apps/content/examples/server.ts b/apps/content/examples/server.ts index 19c30dfe..6b9a6096 100644 --- a/apps/content/examples/server.ts +++ b/apps/content/examples/server.ts @@ -6,7 +6,7 @@ import { z } from 'zod' export type Context = { user?: { id: string } } // global pub, authed completely optional -export const pub = os.context() +export const pub = os.$context() export const authed = pub.use(({ context, path, next }, input) => { /** put auth logic here */ return next() @@ -46,9 +46,7 @@ export const router = pub.router({ ) .use(async ({ context, path, next }, input) => { if (!context?.user) { - throw new ORPCError({ - code: 'UNAUTHORIZED', - }) + throw new ORPCError('UNAUTHORIZED') } const result = await next({ diff --git a/eslint.config.js b/eslint.config.js index a96a6a7d..0c8d08e3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -9,6 +9,7 @@ export default antfu({ 'react-refresh/only-export-components': 'off', 'react/prefer-destructuring-assignment': 'off', 'react/no-context-provider': 'off', + 'ts/method-signature-style': ['error', 'method'], }, }, { files: ['**/*.test.ts', '**/*.test.tsx', '**/*.test-d.ts', '**/*.test-d.tsx', 'apps/content/examples/**', 'playgrounds/**'], diff --git a/package.json b/package.json index b69337c1..64d00768 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@testing-library/jest-dom": "^6.6.2", "@testing-library/react": "^16.0.1", "@types/node": "^22.9.0", - "@vitest/coverage-v8": "^2.1.1", + "@vitest/coverage-v8": "^3.0.4", "eslint": "^9.15.0", "eslint-plugin-format": "^0.1.2", "eslint-plugin-react-hooks": "^5.0.0", @@ -41,7 +41,7 @@ "simple-git-hooks": "^2.11.1", "tsup": "^8.3.0", "typescript": "5.7.2", - "vitest": "^2.1.8" + "vitest": "^3.0.4" }, "simple-git-hooks": { "pre-commit": "pnpm lint-staged" diff --git a/packages/client/src/adapters/fetch/orpc-link.ts b/packages/client/src/adapters/fetch/orpc-link.ts index 0b55c5e3..bd44f064 100644 --- a/packages/client/src/adapters/fetch/orpc-link.ts +++ b/packages/client/src/adapters/fetch/orpc-link.ts @@ -24,7 +24,7 @@ export interface RPCLinkOptions { * * @default 'POST' */ - method?: (path: readonly string[], input: unknown, context: TClientContext) => Promisable + method?(path: readonly string[], input: unknown, context: TClientContext): Promisable /** * The method to use when the payload cannot safely pass to the server with method return from method function. @@ -34,7 +34,7 @@ export interface RPCLinkOptions { */ fallbackMethod?: HTTPMethod - headers?: (path: readonly string[], input: unknown, context: TClientContext) => Promisable> + headers?(path: readonly string[], input: unknown, context: TClientContext): Promisable> fetch?: FetchWithContext @@ -82,12 +82,11 @@ export class RPCLink implements ClientLink { if (!response.ok) { if (ORPCError.isValidJSON(decoded)) { - throw new ORPCError(decoded) + throw ORPCError.fromJSON(decoded) } - throw new ORPCError({ + throw new ORPCError('INTERNAL_SERVER_ERROR', { status: response.status, - code: 'INTERNAL_SERVER_ERROR', message: 'Internal server error', cause: decoded, }) @@ -142,8 +141,7 @@ export class RPCLink implements ClientLink { } } - throw new ORPCError({ - code: 'BAD_REQUEST', + throw new ORPCError('BAD_REQUEST', { message: 'Cannot encode the request, please check the url length or payload.', }) } diff --git a/packages/client/src/client.test-d.ts b/packages/client/src/client.test-d.ts index 2cf12877..5f395f12 100644 --- a/packages/client/src/client.test-d.ts +++ b/packages/client/src/client.test-d.ts @@ -1,6 +1,6 @@ import type { Client } from '@orpc/contract' import { oc } from '@orpc/contract' -import { os } from '@orpc/server' +import { implement, os } from '@orpc/server' import { z } from 'zod' import { createORPCClient } from './client' @@ -17,10 +17,10 @@ describe('createORPCClient', () => { }, }) - const ping = os.contract(pingContract).handler(name => `ping ${name}`) - const pong = os.contract(pongContract).handler(num => `pong ${num}`) + const ping = implement(pingContract).handler(name => `ping ${name}`) + const pong = implement(pongContract).handler(num => `pong ${num}`) - const router = os.contract(contractRouter).router({ + const router = implement(contractRouter).router({ ping, nested: os.lazy(() => Promise.resolve({ default: { pong: os.lazy(() => Promise.resolve({ default: pong })), diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 6d659354..0cfaaf56 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -1,5 +1,5 @@ import type { Client, ContractRouter, ContractRouterClient } from '@orpc/contract' -import type { ANY_ROUTER, RouterClient } from '@orpc/server' +import type { AnyRouter, RouterClient } from '@orpc/server' import type { ClientLink } from './types' export interface createORPCClientOptions { @@ -9,12 +9,12 @@ export interface createORPCClientOptions { path?: string[] } -export function createORPCClient, TClientContext = unknown>( +export function createORPCClient, TClientContext = unknown>( link: ClientLink, options?: createORPCClientOptions, ): TRouter extends ContractRouter ? ContractRouterClient - : TRouter extends ANY_ROUTER // put this in lower priority than ContractRouter, will make createORPCClient can work without @orpc/server + : TRouter extends AnyRouter // put this in lower priority than ContractRouter, will make createORPCClient can work without @orpc/server ? RouterClient : never { const path = options?.path ?? [] diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index 44740e93..78abed3b 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -1,5 +1,5 @@ import type { ClientOptions } from '@orpc/contract' export interface ClientLink { - call: (path: readonly string[], input: unknown, options: ClientOptions) => Promise + call(path: readonly string[], input: unknown, options: ClientOptions): Promise } diff --git a/packages/client/tests/helpers.ts b/packages/client/tests/helpers.ts index e1ebb8f1..56e8c5ea 100644 --- a/packages/client/tests/helpers.ts +++ b/packages/client/tests/helpers.ts @@ -1,5 +1,5 @@ import { oc } from '@orpc/contract' -import { os } from '@orpc/server' +import { implement, os } from '@orpc/server' import { RPCHandler } from '@orpc/server/fetch' import { z } from 'zod' import { oz } from '../../zod/src' @@ -69,10 +69,10 @@ export const contract = oc.router({ }, }) -export const router = os.contract(contract).router({ +export const router = implement(contract).router({ post: os.lazy(() => Promise.resolve({ // this lazy help tests more real, more complex default: { - find: os.contract(contract.post.find).handler(async ({ input, errors }) => { + find: implement(contract.post.find).handler(async ({ input, errors }) => { if (input.id === 'NOT_FOUND') { throw errors.NOT_FOUND({ data: input, @@ -84,7 +84,7 @@ export const router = os.contract(contract).router({ title: `title-${input.id}`, } }), - list: os.contract(contract.post.list).handler(async ({ input, errors }) => { + list: implement(contract.post.list).handler(async ({ input, errors }) => { if (input.keyword === 'TOO_MANY_REQUESTS') { throw errors.TOO_MANY_REQUESTS({ data: input, @@ -101,7 +101,7 @@ export const router = os.contract(contract).router({ nextCursor: input.cursor + 1, } }), - create: os.contract(contract.post.create).handler(async ({ input, errors }) => { + create: implement(contract.post.create).handler(async ({ input, errors }) => { if (input.title === 'CONFLICT') { throw errors.CONFLICT({ data: input, diff --git a/packages/contract/package.json b/packages/contract/package.json index 5a651bf4..7c0c3c3c 100644 --- a/packages/contract/package.json +++ b/packages/contract/package.json @@ -43,7 +43,7 @@ }, "dependencies": { "@orpc/shared": "workspace:*", - "@standard-schema/spec": "1.0.0-beta.4" + "@standard-schema/spec": "1.0.0-rc.0" }, "devDependencies": { "arktype": "2.0.0-rc.26", diff --git a/packages/contract/src/builder-variants.test-d.ts b/packages/contract/src/builder-variants.test-d.ts new file mode 100644 index 00000000..b4fad993 --- /dev/null +++ b/packages/contract/src/builder-variants.test-d.ts @@ -0,0 +1,330 @@ +import type { OmitChainMethodDeep } from '@orpc/shared' +import type { ContractBuilder } from './builder' +import type { ContractProcedureBuilder, ContractProcedureBuilderWithInput, ContractProcedureBuilderWithInputOutput, ContractProcedureBuilderWithOutput, ContractRouterBuilder } from './builder-variants' +import type { MergedErrorMap } from './error-map' +import type { ContractProcedure } from './procedure' +import type { AdaptedContractRouter } from './router' +import { type baseErrorMap, type BaseMeta, generalSchema, type inputSchema, type outputSchema, ping, pong } from '../tests/shared' + +const generalBuilder = {} as ContractBuilder + +describe('ContractProcedureBuilder', () => { + const builder = {} as ContractProcedureBuilder + + it('backward compatibility', () => { + const expected = {} as OmitChainMethodDeep + + expectTypeOf(builder).toMatchTypeOf(expected) + expectTypeOf().toEqualTypeOf() + }) + + it('.errors', () => { + expectTypeOf(builder.errors({ INVALID: { message: 'invalid' }, OVERRIDE: { message: 'override' } })).toEqualTypeOf< + ContractProcedureBuilder< + typeof inputSchema, + typeof outputSchema, + MergedErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.errors({ TOO_MANY_REQUESTS: { data: {} } }) + }) + + it('.meta', () => { + expectTypeOf(builder.meta({ log: true })).toEqualTypeOf< + ContractProcedureBuilder< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid meta + builder.meta({ meta: 'INVALID' }) + }) + + it('.route', () => { + expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf< + ContractProcedureBuilder< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid method + builder.route({ method: 'INVALID' }) + }) + + it('.input', () => { + expectTypeOf(builder.input(generalSchema)).toEqualTypeOf< + ContractProcedureBuilderWithInput< + typeof generalSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.input({}) + }) + + it('.output', () => { + expectTypeOf(builder.output(generalSchema)).toEqualTypeOf< + ContractProcedureBuilderWithOutput< + typeof inputSchema, + typeof generalSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.output({}) + }) +}) + +describe('ContractProcedureBuilderWithInput', () => { + const builder = {} as ContractProcedureBuilderWithInput + + it('backward compatibility', () => { + const expected = {} as OmitChainMethodDeep + + expectTypeOf(builder).toMatchTypeOf(expected) + expectTypeOf().toEqualTypeOf() + }) + + it('.errors', () => { + expectTypeOf(builder.errors({ INVALID: { message: 'invalid' }, OVERRIDE: { message: 'override' } })).toEqualTypeOf< + ContractProcedureBuilderWithInput< + typeof inputSchema, + typeof outputSchema, + MergedErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.errors({ TOO_MANY_REQUESTS: { data: {} } }) + }) + + it('.meta', () => { + expectTypeOf(builder.meta({ log: true })).toEqualTypeOf< + ContractProcedureBuilderWithInput< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid meta + builder.meta({ meta: 'INVALID' }) + }) + + it('.route', () => { + expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf< + ContractProcedureBuilderWithInput< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid method + builder.route({ method: 'INVALID' }) + }) + + it('.output', () => { + expectTypeOf(builder.output(generalSchema)).toEqualTypeOf< + ContractProcedureBuilderWithInputOutput< + typeof inputSchema, + typeof generalSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.output({}) + }) +}) + +describe('ContractProcedureBuilderWithOutput', () => { + const builder = {} as ContractProcedureBuilderWithOutput + + it('backward compatibility', () => { + const expected = {} as OmitChainMethodDeep + + expectTypeOf(builder).toMatchTypeOf(expected) + expectTypeOf().toEqualTypeOf() + }) + + it('.errors', () => { + expectTypeOf(builder.errors({ INVALID: { message: 'invalid' }, OVERRIDE: { message: 'override' } })).toEqualTypeOf< + ContractProcedureBuilderWithOutput< + typeof inputSchema, + typeof outputSchema, + MergedErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.errors({ TOO_MANY_REQUESTS: { data: {} } }) + }) + + it('.meta', () => { + expectTypeOf(builder.meta({ log: true })).toEqualTypeOf< + ContractProcedureBuilderWithOutput< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid meta + builder.meta({ meta: 'INVALID' }) + }) + + it('.route', () => { + expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf< + ContractProcedureBuilderWithOutput< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid method + builder.route({ method: 'INVALID' }) + }) + + it('.input', () => { + expectTypeOf(builder.input(generalSchema)).toEqualTypeOf< + ContractProcedureBuilderWithInputOutput< + typeof generalSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.input({}) + }) +}) + +it('ContractProcedureBuilderWithInputOutput', () => { + const builder = {} as ContractProcedureBuilderWithInputOutput + + it('backward compatibility', () => { + const expected = {} as OmitChainMethodDeep + + expectTypeOf(builder).toMatchTypeOf(expected) + expectTypeOf().toEqualTypeOf() + }) + + it('.errors', () => { + expectTypeOf(builder.errors({ INVALID: { message: 'invalid' }, OVERRIDE: { message: 'override' } })).toEqualTypeOf< + ContractProcedureBuilderWithInputOutput< + typeof inputSchema, + typeof outputSchema, + MergedErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.errors({ TOO_MANY_REQUESTS: { data: {} } }) + }) + + it('.meta', () => { + expectTypeOf(builder.meta({ log: true })).toEqualTypeOf< + ContractProcedureBuilderWithInputOutput< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid meta + builder.meta({ meta: 'INVALID' }) + }) + + it('.route', () => { + expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf< + ContractProcedureBuilderWithInputOutput< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid method + builder.route({ method: 'INVALID' }) + }) +}) + +describe('ContractRouterBuilder', () => { + const builder = {} as ContractRouterBuilder + + it('backward compatibility', () => { + const expected = {} as OmitChainMethodDeep + + // expectTypeOf(builder).toMatchTypeOf(expected) + expectTypeOf().toEqualTypeOf() + }) + + it('.prefix', () => { + expectTypeOf(builder.prefix('/api')).toEqualTypeOf< + ContractRouterBuilder + >() + + // @ts-expect-error - invalid prefix + builder.prefix(1) + }) + + it('.tag', () => { + expectTypeOf(builder.tag('tag1', 'tag2')).toEqualTypeOf< + ContractRouterBuilder + >() + + // @ts-expect-error - invalid tag + builder.tag(1) + }) + + it('.router', () => { + const router = { + ping, + pong, + } + + expectTypeOf(builder.router(router)).toEqualTypeOf< + AdaptedContractRouter + >() + + // @ts-expect-error - invalid router + builder.router(123) + + builder.router({ + // @ts-expect-error - conflict meta def + ping: {} as ContractProcedure< + undefined, + typeof outputSchema, + typeof baseErrorMap, + { mode?: number } + >, + }) + }) +}) diff --git a/packages/contract/src/builder-variants.ts b/packages/contract/src/builder-variants.ts new file mode 100644 index 00000000..92c1be4d --- /dev/null +++ b/packages/contract/src/builder-variants.ts @@ -0,0 +1,115 @@ +import type { ErrorMap, MergedErrorMap } from './error-map' +import type { Meta } from './meta' +import type { ContractProcedure } from './procedure' +import type { HTTPPath, Route } from './route' +import type { AdaptContractRouterOptions, AdaptedContractRouter, ContractRouter } from './router' +import type { Schema } from './schema' + +export interface ContractProcedureBuilder< + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> extends ContractProcedure { + errors( + errors: U, + ): ContractProcedureBuilder, TMeta> + + meta( + meta: TMeta, + ): ContractProcedureBuilder + + route( + route: Route, + ): ContractProcedureBuilder + + input( + schema: U, + ): ContractProcedureBuilderWithInput + + output( + schema: U, + ): ContractProcedureBuilderWithOutput +} + +export interface ContractProcedureBuilderWithInput< + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +>extends ContractProcedure { + errors( + errors: U, + ): ContractProcedureBuilderWithInput, TMeta> + + meta( + meta: TMeta, + ): ContractProcedureBuilderWithInput + + route( + route: Route, + ): ContractProcedureBuilderWithInput + + output( + schema: U, + ): ContractProcedureBuilderWithInputOutput +} + +export interface ContractProcedureBuilderWithOutput< + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> extends ContractProcedure { + errors( + errors: U, + ): ContractProcedureBuilderWithOutput, TMeta> + + meta( + meta: TMeta, + ): ContractProcedureBuilderWithOutput + + route( + route: Route, + ): ContractProcedureBuilderWithOutput + + input( + schema: U, + ): ContractProcedureBuilderWithInputOutput +} + +export interface ContractProcedureBuilderWithInputOutput< + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> extends ContractProcedure { + errors( + errors: U, + ): ContractProcedureBuilderWithInputOutput, TMeta> + + meta( + meta: TMeta, + ): ContractProcedureBuilderWithInputOutput + + route( + route: Route, + ): ContractProcedureBuilderWithInputOutput +} + +export interface ContractRouterBuilder< + TErrorMap extends ErrorMap, + TMeta extends Meta, +> { + '~orpc': AdaptContractRouterOptions + + 'errors'( + errors: U, + ): ContractRouterBuilder, TMeta> + + 'prefix'(prefix: HTTPPath): ContractRouterBuilder + + 'tag'(...tags: string[]): ContractRouterBuilder + + 'router'>(router: T): AdaptedContractRouter +} diff --git a/packages/contract/src/builder.test-d.ts b/packages/contract/src/builder.test-d.ts index 80090489..00d44ff0 100644 --- a/packages/contract/src/builder.test-d.ts +++ b/packages/contract/src/builder.test-d.ts @@ -1,94 +1,128 @@ -import type { ReadonlyDeep } from '@orpc/shared' -import type { ContractBuilder, GetInitialRoute, MergeContractBuilderConfig } from './builder' -import type { StrictErrorMap } from './error-map' +import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../tests/shared' +import type { ContractBuilder } from './builder' +import type { ContractProcedureBuilder, ContractProcedureBuilderWithInput, ContractProcedureBuilderWithOutput, ContractRouterBuilder } from './builder-variants' +import type { MergedErrorMap } from './error-map' import type { ContractProcedure } from './procedure' -import type { ContractProcedureBuilder } from './procedure-builder' -import type { ContractProcedureBuilderWithInput } from './procedure-builder-with-input' -import type { ContractProcedureBuilderWithOutput } from './procedure-builder-with-output' -import type { MergeRoute, StrictRoute } from './route' -import type { AdaptedContractRouter, ContractRouterBuilder } from './router-builder' -import { z } from 'zod' +import type { AdaptedContractRouter } from './router' +import { generalSchema, ping, pong } from '../tests/shared' -const schema = z.object({ value: z.string() }) +const builder = {} as ContractBuilder -const baseErrorMap = { - BASE: { - data: z.object({ - message: z.string(), - }), - }, -} +describe('ContractBuilder', () => { + it('is a contract procedure', () => { + expectTypeOf(builder).toMatchTypeOf< + ContractProcedure< + typeof inputSchema, + typeof outputSchema, + Record, + BaseMeta + > + >() + }) -type BaseErrorMap = StrictErrorMap + it('.$meta', () => { + type MetaDef = { meta1?: string, meta2?: number } -type Config = ReadonlyDeep<{ initialRoute: { description: 'from initial' } }> + expectTypeOf(builder.$meta({ meta1: 'value' })).toEqualTypeOf< + ContractBuilder + >() -const builder = {} as ContractBuilder + // @ts-expect-error - invalid initial meta + builder.$meta({ meta1: 123 }) + }) -describe('ContractBuilder', () => { - it('is a contract procedure', () => { - expectTypeOf(builder).toMatchTypeOf>() + it('.$route', () => { + expectTypeOf(builder.$route({ method: 'GET', path: '/api' })).toEqualTypeOf< + typeof builder + >() + + // @ts-expect-error - method is invalid + builder.$route({ method: 'INVALID' }) }) - it('.config', () => { - expectTypeOf(builder.config({ initialRoute: { description: 'from config' } })).toEqualTypeOf< + it('.errors', () => { + expectTypeOf(builder.errors({ INVALID: { message: 'invalid' }, OVERRIDE: { message: 'override' } })).toEqualTypeOf< ContractBuilder< - MergeContractBuilderConfig>, - BaseErrorMap + typeof inputSchema, + typeof outputSchema, + MergedErrorMap, + BaseMeta > >() - // @ts-expect-error - invalid method - builder.config({ initialRoute: { method: 'HI' } }) + // @ts-expect-error - schema is invalid + builder.errors({ TOO_MANY_REQUESTS: { data: {} } }) }) - it('.errors', () => { - const errors = { BAD_GATEWAY: { data: schema } } as const - - expectTypeOf(builder.errors(errors)) - .toEqualTypeOf < ContractBuilder>>() + it('.meta', () => { + expectTypeOf(builder.meta({ log: true })).toEqualTypeOf< + ContractProcedureBuilder< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() - // @ts-expect-error - not allow redefine error map - builder.errors({ BASE: baseErrorMap.BASE }) + // @ts-expect-error - invalid meta + builder.meta({ meta: 'INVALID' }) }) it('.route', () => { expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf< ContractProcedureBuilder< - BaseErrorMap, - MergeRoute>, ReadonlyDeep<{ method: 'GET' }>> + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta > >() // @ts-expect-error - invalid method - builder.route({ method: 'HE' }) + builder.route({ method: 'INVALID' }) }) it('.input', () => { - expectTypeOf(builder.input(schema)).toEqualTypeOf< + expectTypeOf(builder.input(generalSchema)).toEqualTypeOf< ContractProcedureBuilderWithInput< - typeof schema, - BaseErrorMap, - StrictRoute> + typeof generalSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta > >() + + // @ts-expect-error - schema is invalid + builder.input({}) }) it('.output', () => { - expectTypeOf(builder.output(schema)).toEqualTypeOf< - ContractProcedureBuilderWithOutput>> + expectTypeOf(builder.output(generalSchema)).toEqualTypeOf< + ContractProcedureBuilderWithOutput< + typeof inputSchema, + typeof generalSchema, + typeof baseErrorMap, + BaseMeta + > >() + + // @ts-expect-error - schema is invalid + builder.output({}) }) it('.prefix', () => { - expectTypeOf(builder.prefix('/api')).toEqualTypeOf>() + expectTypeOf(builder.prefix('/api')).toEqualTypeOf< + ContractRouterBuilder + >() // @ts-expect-error - invalid prefix builder.prefix(1) }) it('.tag', () => { - expectTypeOf(builder.tag('tag1', 'tag2')).toEqualTypeOf>() + expectTypeOf(builder.tag('tag1', 'tag2')).toEqualTypeOf< + ContractRouterBuilder + >() // @ts-expect-error - invalid tag builder.tag(1) @@ -96,22 +130,25 @@ describe('ContractBuilder', () => { it('.router', () => { const router = { - ping: {} as ContractProcedure, - pong: {} as ContractProcedure, Record>, + ping, + pong, } - expectTypeOf(builder.router(router)).toEqualTypeOf>() + expectTypeOf(builder.router(router)).toEqualTypeOf< + AdaptedContractRouter + >() - const invalidErrorMap = { - BASE: { - ...baseErrorMap.BASE, - status: 400, - }, - } + // @ts-expect-error - invalid router + builder.router(123) builder.router({ - // @ts-expect-error - error map is not match - ping: {} as ContractProcedure>, + // @ts-expect-error - conflict meta def + ping: {} as ContractProcedure< + undefined, + typeof outputSchema, + typeof baseErrorMap, + { mode?: number } + >, }) }) }) diff --git a/packages/contract/src/builder.test.ts b/packages/contract/src/builder.test.ts index e98b729d..bf793327 100644 --- a/packages/contract/src/builder.test.ts +++ b/packages/contract/src/builder.test.ts @@ -1,45 +1,22 @@ -import { z } from 'zod' +import { baseErrorMap, baseMeta, baseRoute, generalSchema, inputSchema, outputSchema, ping, pong } from '../tests/shared' import { ContractBuilder } from './builder' -import { ContractProcedure } from './procedure' -import { ContractProcedureBuilder } from './procedure-builder' -import { ContractProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ContractProcedureBuilderWithOutput } from './procedure-builder-with-output' -import { ContractRouterBuilder } from './router-builder' - -vi.mock('./router-builder', async (origin) => { - const ContractRouterBuilder = vi.fn() - ContractRouterBuilder.prototype.router = vi.fn(() => '__router__') - - return { - ContractRouterBuilder, - } -}) - -const ContractRouterBuilderRouterSpy = vi.spyOn(ContractRouterBuilder.prototype, 'router') +import { mergeErrorMap } from './error-map' +import { isContractProcedure } from './procedure' +import * as Router from './router' -const schema = z.object({ value: z.string() }) +const adaptContractRouterSpy = vi.spyOn(Router, 'adaptContractRouter') -const baseErrorMap = { - BASE: { - data: z.object({ - message: z.string(), - }), - }, +const def = { + errorMap: baseErrorMap, + outputSchema, + inputSchema, + route: baseRoute, + meta: baseMeta, + prefix: '/adapt' as const, + tags: ['adapt'], } -const builder = new ContractBuilder({ - errorMap: baseErrorMap, - OutputSchema: undefined, - InputSchema: undefined, - config: { - initialRoute: { - description: 'from initial', - }, - }, - route: { - description: 'from initial', - }, -}) +const builder = new ContractBuilder(def) beforeEach(() => { vi.clearAllMocks() @@ -47,85 +24,110 @@ beforeEach(() => { describe('contractBuilder', () => { it('is a contract procedure', () => { - expect(builder).toBeInstanceOf(ContractProcedure) + expect(builder).toSatisfy(isContractProcedure) }) - it('.config', () => { - const applied = builder.config({ initialRoute: { description: 'from config' } }) + it('.$meta', () => { + const meta = { dev: true, log: true } + const applied = builder.$meta(meta) expect(applied).toBeInstanceOf(ContractBuilder) expect(applied).not.toBe(builder) - expect(applied['~orpc'].config).toEqual({ initialRoute: { description: 'from config' } }) + expect(applied['~orpc']).toEqual({ + ...def, + meta, + }) + }) + + it('.$route', () => { + const route = { path: '/api', method: 'GET' } as const + + const applied = builder.$route(route) + expect(applied).toBeInstanceOf(ContractBuilder) + expect(applied['~orpc']).toEqual({ + ...def, + route, + }) }) it('.errors', () => { - const errors = { BAD_GATEWAY: { data: schema } } as const + const errors = { BAD_GATEWAY: { data: outputSchema }, OVERRIDE: { message: 'override' } } as const const applied = builder.errors(errors) expect(applied).toBeInstanceOf(ContractBuilder) expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual({ - ...baseErrorMap, - ...errors, + expect(applied['~orpc']).toEqual({ + ...def, + errorMap: mergeErrorMap(def.errorMap, errors), + }) + }) + + it('.meta', () => { + const meta = { dev: true, log: true } + const applied = builder.meta(meta) + expect(applied).toBeInstanceOf(ContractBuilder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + meta: { ...def.meta, ...meta }, }) - expect(applied['~orpc'].config).toEqual({ initialRoute: { description: 'from initial' } }) }) it('.route', () => { const route = { method: 'GET', path: '/path' } as const const applied = builder.route(route) - expect(applied).toBeInstanceOf(ContractProcedureBuilder) - expect(applied['~orpc'].route).toEqual({ ...route, description: 'from initial' }) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) + expect(applied).toBeInstanceOf(ContractBuilder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + route: { ...def.route, ...route }, + }) }) it('.input', () => { - const applied = builder.input(schema) - expect(applied).toBeInstanceOf(ContractProcedureBuilderWithInput) - expect(applied['~orpc'].InputSchema).toEqual(schema) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].route).toEqual({ description: 'from initial' }) + const applied = builder.input(generalSchema) + expect(applied).toBeInstanceOf(ContractBuilder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + inputSchema: generalSchema, + }) }) it('.output', () => { - const applied = builder.output(schema) - expect(applied).toBeInstanceOf(ContractProcedureBuilderWithOutput) - expect(applied['~orpc'].OutputSchema).toEqual(schema) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].route).toEqual({ description: 'from initial' }) + const applied = builder.output(generalSchema) + expect(applied).toBeInstanceOf(ContractBuilder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + outputSchema: generalSchema, + }) }) it('.prefix', () => { const applied = builder.prefix('/api') - expect(applied).toBeInstanceOf(ContractRouterBuilder) - expect(applied).toBe(vi.mocked(ContractRouterBuilder).mock.results[0]!.value) - expect(ContractRouterBuilder).toHaveBeenCalledWith({ - prefix: '/api', - errorMap: baseErrorMap, + expect(applied).toBeInstanceOf(ContractBuilder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + prefix: '/adapt/api', }) }) it('.tag', () => { const applied = builder.tag('tag1', 'tag2') - expect(applied).toBeInstanceOf(ContractRouterBuilder) - expect(applied).toBe(vi.mocked(ContractRouterBuilder).mock.results[0]!.value) - expect(ContractRouterBuilder).toHaveBeenCalledWith({ - tags: ['tag1', 'tag2'], - errorMap: baseErrorMap, + expect(applied).toBeInstanceOf(ContractBuilder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + tags: ['adapt', 'tag1', 'tag2'], }) }) it('.router', () => { - const router = { - ping: {} as any, - pong: {} as any, - } - + const router = { ping, pong } const applied = builder.router(router) - - expect(applied).toBe(ContractRouterBuilderRouterSpy.mock.results[0]!.value) - expect(ContractRouterBuilderRouterSpy).toHaveBeenCalledWith(router) - expect(ContractRouterBuilder).toHaveBeenCalledWith({ - errorMap: baseErrorMap, - }) + expect(applied).toBe(adaptContractRouterSpy.mock.results[0]?.value) + expect(adaptContractRouterSpy).toHaveBeenCalledOnce() + expect(adaptContractRouterSpy).toHaveBeenCalledWith(router, def) }) }) diff --git a/packages/contract/src/builder.ts b/packages/contract/src/builder.ts index 210775e3..1e5b2992 100644 --- a/packages/contract/src/builder.ts +++ b/packages/contract/src/builder.ts @@ -1,114 +1,128 @@ -import type { ErrorMap, ErrorMapGuard, ErrorMapSuggestions, StrictErrorMap } from './error-map' +import type { ContractProcedureBuilder, ContractProcedureBuilderWithInput, ContractProcedureBuilderWithOutput, ContractRouterBuilder } from './builder-variants' import type { ContractProcedureDef } from './procedure' -import type { HTTPPath, MergeRoute, Route, StrictRoute } from './route' -import type { ContractRouter } from './router' -import type { AdaptedContractRouter } from './router-builder' -import type { Schema, SchemaInput, SchemaOutput } from './types' +import type { AdaptContractRouterOptions, AdaptedContractRouter, ContractRouter } from './router' +import type { Schema } from './schema' +import { type ErrorMap, type MergedErrorMap, mergeErrorMap } from './error-map' +import { mergeMeta, type Meta } from './meta' import { ContractProcedure } from './procedure' -import { ContractProcedureBuilder } from './procedure-builder' -import { ContractProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ContractProcedureBuilderWithOutput } from './procedure-builder-with-output' -import { mergeRoute } from './route' -import { ContractRouterBuilder } from './router-builder' +import { type HTTPPath, mergePrefix, mergeRoute, mergeTags, type Route } from './route' +import { adaptContractRouter } from './router' -export interface ContractBuilderConfig { - initialRoute?: Route +export interface ContractBuilderDef< + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> extends ContractProcedureDef, AdaptContractRouterOptions { } -export type MergeContractBuilderConfig = Omit & B +export class ContractBuilder< + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> extends ContractProcedure { + declare '~orpc': ContractBuilderDef -export type GetInitialRoute = T['initialRoute'] extends Route - ? T['initialRoute'] - : Record - -export interface ContractBuilderDef - extends ContractProcedureDef>> { - config: TConfig -} + constructor(def: ContractBuilderDef) { + super(def) -export class ContractBuilder - extends ContractProcedure> { - declare '~orpc': ContractBuilderDef + this['~orpc'].prefix = def.prefix + this['~orpc'].tags = def.tags + } - constructor(def: ContractBuilderDef) { - super(def) - this['~orpc'].config = def.config + /** + * Reset initial meta + */ + $meta( + initialMeta: U, + ): ContractBuilder { + return new ContractBuilder({ + ...this['~orpc'], + meta: initialMeta, + }) } - config(config: U): ContractBuilder, TErrorMap> { + /** + * Reset initial route + */ + $route( + initialRoute: Route, + ): ContractBuilder { return new ContractBuilder({ ...this['~orpc'], - config: { - ...this['~orpc'].config, - ...config, - } as any, + route: initialRoute, }) } - errors & ErrorMapSuggestions>( + errors( errors: U, - ): ContractBuilder & TErrorMap> { + ): ContractBuilder, TMeta> { return new ContractBuilder({ ...this['~orpc'], - errorMap: { - ...this['~orpc'].errorMap, - ...errors, - }, + errorMap: mergeErrorMap(this['~orpc'].errorMap, errors), }) } - route(route: U): ContractProcedureBuilder>, U>> { - return new ContractProcedureBuilder({ + meta( + meta: TMeta, + ): ContractProcedureBuilder { + return new ContractBuilder({ + ...this['~orpc'], + meta: mergeMeta(this['~orpc'].meta, meta), + }) as any + } + + route( + route: Route, + ): ContractProcedureBuilder { + return new ContractBuilder({ + ...this['~orpc'], route: mergeRoute(this['~orpc'].route, route), - InputSchema: undefined, - OutputSchema: undefined, - errorMap: this['~orpc'].errorMap, }) } - input(schema: U, example?: SchemaInput): ContractProcedureBuilderWithInput>> { - return new ContractProcedureBuilderWithInput({ - route: this['~orpc'].route, - InputSchema: schema, - inputExample: example, - OutputSchema: undefined, - errorMap: this['~orpc'].errorMap, + input( + schema: U, + ): ContractProcedureBuilderWithInput { + return new ContractBuilder({ + ...this['~orpc'], + inputSchema: schema, }) } - output(schema: U, example?: SchemaOutput): ContractProcedureBuilderWithOutput>> { - return new ContractProcedureBuilderWithOutput({ - route: this['~orpc'].route, - OutputSchema: schema, - outputExample: example, - InputSchema: undefined, - errorMap: this['~orpc'].errorMap, + output( + schema: U, + ): ContractProcedureBuilderWithOutput { + return new ContractBuilder({ + ...this['~orpc'], + outputSchema: schema, }) } - prefix(prefix: U): ContractRouterBuilder { - return new ContractRouterBuilder({ - prefix, - errorMap: this['~orpc'].errorMap, - tags: undefined, + prefix(prefix: HTTPPath): ContractRouterBuilder { + return new ContractBuilder({ + ...this['~orpc'], + prefix: mergePrefix(this['~orpc'].prefix, prefix), }) } - tag(...tags: U): ContractRouterBuilder { - return new ContractRouterBuilder({ - tags, - errorMap: this['~orpc'].errorMap, - prefix: undefined, + tag(...tags: string[]): ContractRouterBuilder { + return new ContractBuilder({ + ...this['~orpc'], + tags: mergeTags(this['~orpc'].tags, tags), }) } - router>>( - router: T, - ): AdaptedContractRouter { - return new ContractRouterBuilder({ - errorMap: this['~orpc'].errorMap, - prefix: undefined, - tags: undefined, - }).router(router) + router>(router: T): AdaptedContractRouter { + return adaptContractRouter(router, this['~orpc']) } } + +export const oc = new ContractBuilder({ + errorMap: {}, + inputSchema: undefined, + outputSchema: undefined, + route: {}, + meta: {}, +}) diff --git a/packages/contract/src/client-utils.test-d.ts b/packages/contract/src/client-utils.test-d.ts index e6332099..55ed027f 100644 --- a/packages/contract/src/client-utils.test-d.ts +++ b/packages/contract/src/client-utils.test-d.ts @@ -1,6 +1,7 @@ import type { Client } from './client' +import type { ORPCError } from './error-orpc' import { safe } from './client-utils' -import { isDefinedError, type ORPCError } from './error-orpc' +import { isDefinedError } from './error-utils' it('safe', async () => { const client = {} as Client> diff --git a/packages/contract/src/client-utils.test.ts b/packages/contract/src/client-utils.test.ts index df7c92de..ad7f39f2 100644 --- a/packages/contract/src/client-utils.test.ts +++ b/packages/contract/src/client-utils.test.ts @@ -9,11 +9,11 @@ it('safe', async () => { const r2 = await safe(Promise.reject(e2)) expect(r2).toEqual([undefined, e2, false]) - const e3 = new ORPCError({ code: 'BAD_GATEWAY', defined: true }) + const e3 = new ORPCError('BAD_GATEWAY', { defined: true }) const r3 = await safe(Promise.reject(e3)) expect(r3).toEqual([undefined, e3, true]) - const e4 = new ORPCError({ code: 'BAD_GATEWAY' }) + const e4 = new ORPCError('BAD_GATEWAY') const r4 = await safe(Promise.reject(e4)) expect(r4).toEqual([undefined, e4, false]) }) diff --git a/packages/contract/src/client-utils.ts b/packages/contract/src/client-utils.ts index aef733e9..37f1dfa7 100644 --- a/packages/contract/src/client-utils.ts +++ b/packages/contract/src/client-utils.ts @@ -1,5 +1,6 @@ import type { ClientPromiseResult } from './client' -import { isDefinedError, type ORPCError } from './error-orpc' +import type { ORPCError } from './error-orpc' +import { isDefinedError } from './error-utils' export type SafeResult = | [output: TOutput, error: undefined, isDefinedError: false] diff --git a/packages/contract/src/config.ts b/packages/contract/src/config.ts index aeb06c6b..8aa750b4 100644 --- a/packages/contract/src/config.ts +++ b/packages/contract/src/config.ts @@ -1,4 +1,4 @@ -import type { HTTPMethod, InputStructure, Route } from './route' +import type { HTTPMethod, InputStructure } from './route' export interface ContractConfig { defaultMethod: HTTPMethod @@ -6,7 +6,6 @@ export interface ContractConfig { defaultSuccessDescription: string defaultInputStructure: InputStructure defaultOutputStructure: InputStructure - defaultInitialRoute: Route } const DEFAULT_CONFIG: ContractConfig = { @@ -15,7 +14,6 @@ const DEFAULT_CONFIG: ContractConfig = { defaultSuccessDescription: 'OK', defaultInputStructure: 'compact', defaultOutputStructure: 'compact', - defaultInitialRoute: {}, } export function fallbackContractConfig(key: T, value: ContractConfig[T] | undefined): ContractConfig[T] { diff --git a/packages/contract/src/error-map.test-d.ts b/packages/contract/src/error-map.test-d.ts new file mode 100644 index 00000000..b002d095 --- /dev/null +++ b/packages/contract/src/error-map.test-d.ts @@ -0,0 +1,11 @@ +import type { MergedErrorMap } from './error-map' + +it('MergedErrorMap', () => { + expectTypeOf< + MergedErrorMap<{ BASE: { message: string } }, { INVALID: { message: string } }> + >().toMatchTypeOf<{ BASE: { message: string }, INVALID: { message: string } }>() + + expectTypeOf< + MergedErrorMap<{ BASE: { message: string }, INVALID: { status: number } }, { INVALID: { message: string } }> + >().toMatchTypeOf<{ BASE: { message: string }, INVALID: { message: string } }>() +}) diff --git a/packages/contract/src/error-map.test.ts b/packages/contract/src/error-map.test.ts new file mode 100644 index 00000000..4615b6d2 --- /dev/null +++ b/packages/contract/src/error-map.test.ts @@ -0,0 +1,9 @@ +import { baseErrorMap } from '../tests/shared' +import { mergeErrorMap } from './error-map' + +it('mergeErrorMap', () => { + expect(mergeErrorMap(baseErrorMap, baseErrorMap)).toEqual(baseErrorMap) + expect(mergeErrorMap(baseErrorMap, { OVERRIDE: {}, INVALID: {} })).toEqual( + { OVERRIDE: {}, INVALID: {}, BASE: baseErrorMap.BASE }, + ) +}) diff --git a/packages/contract/src/error-map.ts b/packages/contract/src/error-map.ts index a1feb9cf..20d573b7 100644 --- a/packages/contract/src/error-map.ts +++ b/packages/contract/src/error-map.ts @@ -1,62 +1,19 @@ -import type { CommonORPCErrorCode } from './error-orpc' -import type { Schema } from './types' +import type { ORPCErrorCode } from './error-orpc' +import type { Schema } from './schema' export type ErrorMapItem = { - /** - * - * @default 500 - */ status?: number message?: string description?: string data?: TDataSchema } -export interface ErrorMap { - [k: string]: ErrorMapItem +export type ErrorMap = { + [key in ORPCErrorCode]?: ErrorMapItem } -/** - * const U extends ErrorMap & ErrorMapGuard & ErrorMapSuggestions - * - * Purpose: - * - Helps `U` suggest `CommonORPCErrorCode` to the user when typing. - * - * Why not replace `ErrorMap` with `ErrorMapSuggestions`? - * - `ErrorMapSuggestions` has a drawback: it allows `undefined` values for items. - * - `ErrorMapGuard` uses `Partial`, which can introduce `undefined` values. - * - * This could lead to unintended behavior where `undefined` values override `TErrorMap`, - * potentially resulting in a `never` type after merging. - * - * Recommendation: - * - Use `ErrorMapSuggestions` to assist users in typing correctly but do not replace `ErrorMap`. - * - Ensure `ErrorMapGuard` is adjusted to prevent `undefined` values. - */ -export type ErrorMapSuggestions = { - [key in CommonORPCErrorCode | (string & {})]?: ErrorMapItem -} - -/** - * `U` extends `ErrorMap` & `ErrorMapGuard` - * - * `ErrorMapGuard` is a utility type that ensures `U` cannot redefine the structure of `TErrorMap`. - * It achieves this by setting each key in `TErrorMap` to `never`, effectively preventing any redefinition. - * - * Why not just use `Partial`? - * - Allowing users to redefine existing error map items would require using `StrictErrorMap`. - * - However, I prefer not to use `StrictErrorMap` frequently, due to perceived performance concerns, - * though this has not been benchmarked and is based on personal preference. - * - */ -export type ErrorMapGuard = { - [K in keyof TErrorMap]?: never -} +export type MergedErrorMap = Omit & T2 -/** - * Since `undefined` has a specific meaning (it use default value), - * we ensure all additional properties in each item of the ErrorMap are explicitly set to `undefined`. - */ -export type StrictErrorMap = { - [K in keyof T]: T[K] & Partial, keyof T[K]>, undefined>> +export function mergeErrorMap(errorMap1: T1, errorMap2: T2): MergedErrorMap { + return { ...errorMap1, ...errorMap2 } } diff --git a/packages/contract/src/error-orpc.test.ts b/packages/contract/src/error-orpc.test.ts index ed7dd18e..8bc5b5ce 100644 --- a/packages/contract/src/error-orpc.test.ts +++ b/packages/contract/src/error-orpc.test.ts @@ -1,6 +1,4 @@ -import type { ErrorMap } from './error-map' -import { z } from 'zod' -import { fallbackORPCErrorMessage, fallbackORPCErrorStatus, isDefinedError, ORPCError, validateORPCError } from './error-orpc' +import { fallbackORPCErrorMessage, fallbackORPCErrorStatus, ORPCError } from './error-orpc' it('fallbackORPCErrorStatus', () => { expect(fallbackORPCErrorStatus('BAD_GATEWAY', 500)).toBe(500) @@ -18,7 +16,7 @@ it('fallbackORPCErrorMessage', () => { describe('oRPCError', () => { it('works', () => { - const error = new ORPCError({ defined: true, code: 'BAD_GATEWAY', status: 500, message: 'message', data: 'data', cause: 'cause' }) + const error = new ORPCError('BAD_GATEWAY', { defined: true, status: 500, message: 'message', data: 'data', cause: 'cause' }) expect(error.defined).toBe(true) expect(error.code).toBe('BAD_GATEWAY') expect(error.status).toBe(500) @@ -28,27 +26,27 @@ describe('oRPCError', () => { }) it('default defined=false', () => { - const error = new ORPCError({ code: 'BAD_GATEWAY' }) + const error = new ORPCError('BAD_GATEWAY') expect(error.defined).toBe(false) }) it('fallback status', () => { - const error = new ORPCError({ code: 'BAD_GATEWAY' }) + const error = new ORPCError('BAD_GATEWAY') expect(error.status).toBe(502) }) it('fallback message', () => { - const error = new ORPCError({ code: 'BAD_GATEWAY' }) + const error = new ORPCError('BAD_GATEWAY') expect(error.message).toBe('Bad Gateway') }) it('oRPCError throw when invalid status', () => { - expect(() => new ORPCError({ code: 'BAD_GATEWAY', status: 100 })).toThrowError() - expect(() => new ORPCError({ code: 'BAD_GATEWAY', status: -1 })).toThrowError() + expect(() => new ORPCError('BAD_GATEWAY', { status: 100 })).toThrowError() + expect(() => new ORPCError('BAD_GATEWAY', { status: -1 })).toThrowError() }) it('toJSON', () => { - const error = new ORPCError({ code: 'BAD_GATEWAY', status: 500, message: 'message', data: 'data', cause: 'cause' }) + const error = new ORPCError('BAD_GATEWAY', { status: 500, message: 'message', data: 'data', cause: 'cause' }) expect(error.toJSON()).toEqual({ defined: false, code: 'BAD_GATEWAY', @@ -58,76 +56,26 @@ describe('oRPCError', () => { }) }) + it('fromJSON', () => { + const error = ORPCError.fromJSON({ + defined: true, + code: 'BAD_GATEWAY', + status: 500, + message: 'message', + data: 'data', + }) + expect(error.defined).toBe(true) + expect(error.code).toBe('BAD_GATEWAY') + expect(error.status).toBe(500) + expect(error.message).toBe('message') + expect(error.data).toBe('data') + }) + it('isValidJSON', () => { - const error = new ORPCError({ code: 'BAD_GATEWAY', status: 500, message: 'message', data: 'data', cause: 'cause' }) + const error = new ORPCError('BAD_GATEWAY', { status: 500, message: 'message', data: 'data', cause: 'cause' }) expect(ORPCError.isValidJSON(error.toJSON())).toBe(true) expect(ORPCError.isValidJSON({})).toBe(false) expect(ORPCError.isValidJSON({ defined: true })).toBe(false) expect(ORPCError.isValidJSON({ defined: true, code: 'BAD_GATEWAY', status: 500, message: 'message', data: 'data' })).toBe(true) }) }) - -it('isDefinedError', () => { - expect(isDefinedError(new ORPCError({ code: 'BAD_GATEWAY' }))).toBe(false) - expect(isDefinedError(new ORPCError({ defined: true, code: 'BAD_GATEWAY' }))).toBe(true) - expect(isDefinedError({ defined: true, code: 'BAD_GATEWAY' })).toBe(false) -}) - -describe('validateORPCError', () => { - const errors: ErrorMap = { - BAD_GATEWAY: { - data: z.object({ - value: z.string().transform(v => Number.parseInt(v)), - }), - }, - CONFLICT: { - }, - } - - it('ignore not-match errors when defined=false', async () => { - const e1 = new ORPCError({ code: 'BAD_GATEWAY', status: 501 }) - expect(await validateORPCError(errors, e1)).toBe(e1) - - const e2 = new ORPCError({ code: 'NOT_FOUND' }) - expect(await validateORPCError(errors, e2)).toBe(e2) - - const e3 = new ORPCError({ code: 'BAD_GATEWAY', data: 'invalid' }) - expect(await validateORPCError(errors, e3)).toBe(e3) - }) - - it('modify not-match errors when defined=true', async () => { - const e1 = new ORPCError({ defined: true, code: 'BAD_GATEWAY', status: 501 }) - const v1 = await validateORPCError(errors, e1) - expect(v1).not.toBe(e1) - expect({ ...v1 }).toEqual({ ...e1, defined: false }) - - const e2 = new ORPCError({ defined: true, code: 'NOT_FOUND' }) - const v2 = await validateORPCError(errors, e2) - expect(v2).not.toBe(e2) - expect({ ...v2 }).toEqual({ ...e2, defined: false }) - - const e3 = new ORPCError({ defined: true, code: 'BAD_GATEWAY', data: 'invalid' }) - const v3 = await validateORPCError(errors, e3) - expect(v3).not.toBe(e3) - expect({ ...v3 }).toEqual({ ...e3, defined: false }) - }) - - it('ignore match errors when defined=true and data schema is undefined', async () => { - const e1 = new ORPCError({ defined: true, code: 'CONFLICT' }) - expect(await validateORPCError(errors, e1)).toBe(e1) - }) - - it('return new error when defined=true and data schema is undefined with match error', async () => { - const e1 = new ORPCError({ code: 'CONFLICT' }) - const v1 = await validateORPCError(errors, e1) - expect(v1).not.toBe(e1) - expect({ ...v1 }).toEqual({ ...e1, defined: true }) - }) - - it('return new with defined=true and validated data with match errors', async () => { - const e1 = new ORPCError({ code: 'BAD_GATEWAY', data: { value: '123' } }) - const v1 = await validateORPCError(errors, e1) - expect(v1).not.toBe(e1) - expect({ ...v1 }).toEqual({ ...e1, defined: true, data: { value: 123 } }) - }) -}) diff --git a/packages/contract/src/error-orpc.ts b/packages/contract/src/error-orpc.ts index 066ae446..12aab480 100644 --- a/packages/contract/src/error-orpc.ts +++ b/packages/contract/src/error-orpc.ts @@ -1,5 +1,5 @@ import type { ErrorMap, ErrorMapItem } from './error-map' -import type { SchemaOutput } from './types' +import type { SchemaOutput } from './schema' import { isPlainObject } from '@orpc/shared' export type ORPCErrorFromErrorMap = { @@ -92,40 +92,44 @@ export const COMMON_ORPC_ERROR_DEFS = { export type CommonORPCErrorCode = keyof typeof COMMON_ORPC_ERROR_DEFS -export type ORPCErrorOptions = - & ErrorOptions - & { defined?: boolean, code: TCode, status?: number, message?: string } - & (undefined extends TData ? { data?: TData } : { data: TData }) +export type ORPCErrorCode = CommonORPCErrorCode | (string & {}) -export function fallbackORPCErrorStatus(code: CommonORPCErrorCode | (string & {}), status: number | undefined): number { +export function fallbackORPCErrorStatus(code: ORPCErrorCode, status: number | undefined): number { return status ?? (COMMON_ORPC_ERROR_DEFS as any)[code]?.status ?? 500 } -export function fallbackORPCErrorMessage(code: CommonORPCErrorCode | (string & {}), message: string | undefined): string { +export function fallbackORPCErrorMessage(code: ORPCErrorCode, message: string | undefined): string { return message || (COMMON_ORPC_ERROR_DEFS as any)[code]?.message || code } -export class ORPCError extends Error { +export type ORPCErrorOptions< TData> = + & ErrorOptions + & { defined?: boolean, status?: number, message?: string } + & (undefined extends TData ? { data?: TData } : { data: TData }) + +export type ORPCErrorOptionsRest = + | [options: ORPCErrorOptions] + | (undefined extends TData ? [] : never) + +export class ORPCError extends Error { readonly defined: boolean readonly code: TCode readonly status: number readonly data: TData - constructor(options: ORPCErrorOptions) { - if (options.status && (options.status < 400 || options.status >= 600)) { + constructor(code: TCode, ...[options]: ORPCErrorOptionsRest) { + if (options?.status && (options.status < 400 || options.status >= 600)) { throw new Error('[ORPCError] The error status code must be in the 400-599 range.') } - const message = fallbackORPCErrorMessage(options.code, options.message) + const message = fallbackORPCErrorMessage(code, options?.message) super(message, options) - this.code = options.code - this.status = fallbackORPCErrorStatus(options.code, options.status) - this.defined = options.defined ?? false - - // data only optional when TData is undefinable so can safely cast here - this.data = options.data as TData + this.code = code + this.status = fallbackORPCErrorStatus(code, options?.status) + this.defined = options?.defined ?? false + this.data = options?.data as TData // data only optional when TData is undefinable so can safely cast here } toJSON(): ORPCErrorJSON { @@ -138,7 +142,11 @@ export class ORPCError } } - static isValidJSON(json: unknown): json is ORPCErrorJSON { + static fromJSON(json: ORPCErrorJSON): ORPCError { + return new ORPCError(json.code, json) + } + + static isValidJSON(json: unknown): json is ORPCErrorJSON { return isPlainObject(json) && 'defined' in json && typeof json.defined === 'boolean' @@ -152,41 +160,3 @@ export class ORPCError } export type ORPCErrorJSON = Pick, 'defined' | 'code' | 'status' | 'message' | 'data'> - -export function isDefinedError(error: T): error is Extract> { - return error instanceof ORPCError && error.defined -} - -export async function validateORPCError(map: ErrorMap, error: ORPCError): Promise> { - const { code, status, message, data, cause, defined } = error - const config = map?.[error.code] - - if (!config || fallbackORPCErrorStatus(error.code, config.status) !== error.status) { - return defined - ? new ORPCError({ defined: false, code, status, message, data, cause }) - : error - } - - if (!config.data) { - return defined - ? error - : new ORPCError({ defined: true, code, status, message, data, cause }) - } - - const validated = await config.data['~standard'].validate(error.data) - - if (validated.issues) { - return defined - ? new ORPCError({ defined: false, code, status, message, data, cause }) - : error - } - - return new ORPCError({ - defined: true, - code, - status, - message, - data: validated.value, - cause, - }) -} diff --git a/packages/contract/src/error-utils.test-d.ts b/packages/contract/src/error-utils.test-d.ts new file mode 100644 index 00000000..e57f221a --- /dev/null +++ b/packages/contract/src/error-utils.test-d.ts @@ -0,0 +1,27 @@ +import type { baseErrorMap } from '../tests/shared' +import type { ORPCError } from './error-orpc' +import type { ORPCErrorConstructorMap } from './error-utils' +import { isDefinedError } from './error-utils' + +it('isDefinedError', () => { + const orpcError = {} as ORPCError<'CODE', { value: string }> | ORPCError<'BASE', { value: number }> + const error = {} as Error | typeof orpcError + + if (isDefinedError(error)) { + expectTypeOf(error).toEqualTypeOf(orpcError) + } +}) + +it('ORPCErrorConstructorMap', () => { + const constructors = {} as ORPCErrorConstructorMap + + const error = constructors.BASE({ data: { output: 123 } }) + expectTypeOf(error).toEqualTypeOf>() + + // @ts-expect-error - invalid data + constructors.BASE({ data: { output: '123' } }) + // @ts-expect-error - missing data + constructors.BASE() + // can call without data if it is optional + constructors.OVERRIDE() +}) diff --git a/packages/contract/src/error-utils.test.ts b/packages/contract/src/error-utils.test.ts new file mode 100644 index 00000000..ac38c86c --- /dev/null +++ b/packages/contract/src/error-utils.test.ts @@ -0,0 +1,118 @@ +import type { ErrorMap } from './error-map' +import { z } from 'zod' +import { outputSchema } from '../tests/shared' +import { ORPCError } from './error-orpc' +import { createORPCErrorConstructorMap, isDefinedError, validateORPCError } from './error-utils' + +it('isDefinedError', () => { + expect(isDefinedError(new ORPCError('BAD_GATEWAY'))).toBe(false) + expect(isDefinedError(new ORPCError('BAD_GATEWAY', { defined: true }))).toBe(true) + expect(isDefinedError({ defined: true, code: 'BAD_GATEWAY' })).toBe(false) +}) + +describe('createORPCErrorConstructorMap', () => { + const errors = { + BAD_GATEWAY: { + status: 588, + message: 'default message', + data: outputSchema, + }, + } + + const constructors = createORPCErrorConstructorMap(errors) + + it('works', () => { + const error = constructors.BAD_GATEWAY({ data: { output: 123 }, cause: 'cause' }) + + expect(error).toBeInstanceOf(ORPCError) + expect(error.code).toEqual('BAD_GATEWAY') + expect(error.status).toBe(588) + expect(error.defined).toBe(true) + expect(error.message).toBe('default message') + expect(error.data).toEqual({ output: 123 }) + expect(error.cause).toBe('cause') + }) + + it('can override message', () => { + expect( + constructors.BAD_GATEWAY({ message: 'custom message', data: { output: 123 } }).message, + ).toBe('custom message') + }) + + it('can arbitrary access', () => { + // @ts-expect-error - invalid access + const error = constructors.ANY_THING({ data: 'DATA', message: 'MESSAGE', cause: 'cause' }) + + expect(error).toBeInstanceOf(ORPCError) + expect(error.code).toEqual('ANY_THING') + expect(error.status).toBe(500) + expect(error.defined).toBe(false) + expect(error.message).toBe('MESSAGE') + expect(error.data).toEqual('DATA') + expect(error.cause).toBe('cause') + }) + + it('not proxy when access with symbol', () => { + // @ts-expect-error - invalid access + expect(constructors[Symbol('something')]).toBeUndefined() + }) +}) + +describe('validateORPCError', () => { + const errors: ErrorMap = { + BAD_GATEWAY: { + data: z.object({ + value: z.string().transform(v => Number.parseInt(v)), + }), + }, + CONFLICT: { + }, + } + + it('ignore not-match errors when defined=false', async () => { + const e1 = new ORPCError('BAD_GATEWAY', { status: 501 }) + expect(await validateORPCError(errors, e1)).toBe(e1) + + const e2 = new ORPCError('NOT_FOUND') + expect(await validateORPCError(errors, e2)).toBe(e2) + + const e3 = new ORPCError('BAD_GATEWAY', { data: 'invalid' }) + expect(await validateORPCError(errors, e3)).toBe(e3) + }) + + it('modify not-match errors when defined=true', async () => { + const e1 = new ORPCError('BAD_GATEWAY', { defined: true, status: 501 }) + const v1 = await validateORPCError(errors, e1) + expect(v1).not.toBe(e1) + expect({ ...v1 }).toEqual({ ...e1, defined: false }) + + const e2 = new ORPCError('NOT_FOUND', { defined: true }) + const v2 = await validateORPCError(errors, e2) + expect(v2).not.toBe(e2) + expect({ ...v2 }).toEqual({ ...e2, defined: false }) + + const e3 = new ORPCError('BAD_GATEWAY', { defined: true, data: 'invalid' }) + const v3 = await validateORPCError(errors, e3) + expect(v3).not.toBe(e3) + expect({ ...v3 }).toEqual({ ...e3, defined: false }) + }) + + it('ignore match errors when defined=true and data schema is undefined', async () => { + const e1 = new ORPCError('CONFLICT', { defined: true }) + expect(await validateORPCError(errors, e1)).toBe(e1) + }) + + it('return new error when defined=true and data schema is undefined with match error', async () => { + const e1 = new ORPCError('CONFLICT') + const v1 = await validateORPCError(errors, e1) + expect(v1).not.toBe(e1) + expect({ ...v1 }).toEqual({ ...e1, defined: true }) + }) + + it('return new with defined=true and validated data with match errors', async () => { + const e1 = new ORPCError('BAD_GATEWAY', { data: { value: '123' } }) + const v1 = await validateORPCError(errors, e1) + expect(v1).not.toBe(e1) + expect({ ...v1 }).toEqual({ ...e1, defined: true, data: { value: 123 } }) + }) +}) diff --git a/packages/contract/src/error-utils.ts b/packages/contract/src/error-utils.ts new file mode 100644 index 00000000..7398d414 --- /dev/null +++ b/packages/contract/src/error-utils.ts @@ -0,0 +1,81 @@ +import type { ErrorMap, ErrorMapItem } from './error-map' +import type { ORPCErrorCode, ORPCErrorOptions } from './error-orpc' +import type { SchemaInput } from './schema' +import { fallbackORPCErrorStatus, ORPCError } from './error-orpc' + +export function isDefinedError(error: T): error is Extract> { + return error instanceof ORPCError && error.defined +} + +export type ORPCErrorConstructorMapItemOptions = Omit, 'defined' | 'status'> + +export type ORPCErrorConstructorMapItemRest = + | [options: ORPCErrorConstructorMapItemOptions] + | (undefined extends TData ? [] : never) + +export type ORPCErrorConstructorMapItem = + (...rest: ORPCErrorConstructorMapItemRest) => ORPCError + +export type ORPCErrorConstructorMap = { + [K in keyof T]: K extends ORPCErrorCode + ? T[K] extends ErrorMapItem + ? ORPCErrorConstructorMapItem> + : never + : never +} + +export function createORPCErrorConstructorMap(errors: T): ORPCErrorConstructorMap { + /** + * Must use proxy to make sure any arbitrary access can be handled. + */ + const proxy = new Proxy(errors, { + get(target, code) { + if (typeof code !== 'string') { + return Reflect.get(target, code) + } + + const item: ORPCErrorConstructorMapItem = (...[options]) => { + const config = errors[code] + + return new ORPCError(code, { + defined: Boolean(config), + status: config?.status, + message: options?.message ?? config?.message, + data: options?.data, + cause: options?.cause, + }) + } + + return item + }, + }) + + return proxy as any +} + +export async function validateORPCError(map: ErrorMap, error: ORPCError): Promise> { + const { code, status, message, data, cause, defined } = error + const config = map?.[error.code] + + if (!config || fallbackORPCErrorStatus(error.code, config.status) !== error.status) { + return defined + ? new ORPCError(code, { defined: false, status, message, data, cause }) + : error + } + + if (!config.data) { + return defined + ? error + : new ORPCError(code, { defined: true, status, message, data, cause }) + } + + const validated = await config.data['~standard'].validate(error.data) + + if (validated.issues) { + return defined + ? new ORPCError(code, { defined: false, status, message, data, cause }) + : error + } + + return new ORPCError(code, { defined: true, status, message, data: validated.value, cause }) +} diff --git a/packages/contract/src/error.test.ts b/packages/contract/src/error.test.ts new file mode 100644 index 00000000..672dda89 --- /dev/null +++ b/packages/contract/src/error.test.ts @@ -0,0 +1,7 @@ +import { ValidationError } from './error' + +it('validationError', () => { + const error = new ValidationError({ message: 'message', issues: [{ message: 'message' }] }) + expect(error).toBeInstanceOf(Error) + expect(error.issues).toEqual([{ message: 'message' }]) +}) diff --git a/packages/contract/src/index.ts b/packages/contract/src/index.ts index cd5df7fe..0a71fe51 100644 --- a/packages/contract/src/index.ts +++ b/packages/contract/src/index.ts @@ -1,31 +1,19 @@ /** unnoq */ -import { ContractBuilder } from './builder' - export * from './builder' +export * from './builder-variants' export * from './client' export * from './client-utils' export * from './config' export * from './error' export * from './error-map' export * from './error-orpc' +export * from './error-utils' +export * from './meta' export * from './procedure' -export * from './procedure-builder' -export * from './procedure-builder-with-input' -export * from './procedure-builder-with-output' export * from './procedure-client' -export * from './procedure-decorated' export * from './route' export * from './router' -export * from './router-builder' export * from './router-client' -export * from './schema-utils' +export * from './schema' export * from './types' - -export const oc = new ContractBuilder({ - config: {}, - route: {}, - errorMap: {}, - InputSchema: undefined, - OutputSchema: undefined, -}) diff --git a/packages/contract/src/meta.test.ts b/packages/contract/src/meta.test.ts new file mode 100644 index 00000000..68431441 --- /dev/null +++ b/packages/contract/src/meta.test.ts @@ -0,0 +1,6 @@ +import { mergeMeta } from './meta' + +it('mergeMeta', () => { + expect(mergeMeta({}, { a: 2 })).toEqual({ a: 2 }) + expect(mergeMeta({ a: 1, b: 1 }, { a: 2 })).toEqual({ a: 2, b: 1 }) +}) diff --git a/packages/contract/src/meta.ts b/packages/contract/src/meta.ts new file mode 100644 index 00000000..2220d191 --- /dev/null +++ b/packages/contract/src/meta.ts @@ -0,0 +1,5 @@ +export type Meta = Record + +export function mergeMeta(meta1: T, meta2: T): T { + return { ...meta1, ...meta2 } +} diff --git a/packages/contract/src/procedure-builder-with-input.test-d.ts b/packages/contract/src/procedure-builder-with-input.test-d.ts deleted file mode 100644 index f8b8af53..00000000 --- a/packages/contract/src/procedure-builder-with-input.test-d.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { ReadonlyDeep } from '@orpc/shared' -import type { StrictErrorMap } from './error-map' -import type { ContractProcedure } from './procedure' -import type { ContractProcedureBuilderWithInput } from './procedure-builder-with-input' -import type { DecoratedContractProcedure } from './procedure-decorated' -import type { PrefixRoute, UnshiftTagRoute } from './route' -import { z } from 'zod' - -const baseErrorMap = { - BASE: { - status: 500, - data: z.object({ - message: z.string(), - }), - }, -} - -const inputSchema = z.object({ input: z.string().transform(v => Number.parseInt(v)) }) - -const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) - -type BaseRoute = { path: '/base' } - -const builder = {} as ContractProcedureBuilderWithInput - -describe('DecoratedContractProcedure', () => { - it('is a contract procedure', () => { - expectTypeOf(builder).toMatchTypeOf>() - }) - - it('.errors', () => { - const errors = { BAD_GATEWAY: { data: z.object({ message: z.string() }) } } as const - - expectTypeOf(builder.errors(errors)) - .toEqualTypeOf< - ContractProcedureBuilderWithInput, BaseRoute> - >() - - // @ts-expect-error - not allow redefine error map - builder.errors({ BASE: baseErrorMap.BASE }) - }) - - it('.route', () => { - expectTypeOf(builder.route({ method: 'GET', tags: ['tag'] })).toEqualTypeOf< - ContractProcedureBuilderWithInput< - typeof inputSchema, - typeof baseErrorMap, - ReadonlyDeep<{ method: 'GET', tags: ['tag'] }> & BaseRoute - > - >() - - // @ts-expect-error - invalid method - builder.route({ method: 'HE' }) - }) - - it('.prefix', () => { - expectTypeOf(builder.prefix('/api')).toEqualTypeOf< - ContractProcedureBuilderWithInput< - typeof inputSchema, - typeof baseErrorMap, - PrefixRoute - > - >() - - // @ts-expect-error - invalid prefix - builder.prefix(1) - }) - - it('.unshiftTag', () => { - expectTypeOf(builder.unshiftTag('tag', 'tag2')).toEqualTypeOf< - ContractProcedureBuilderWithInput< - typeof inputSchema, - typeof baseErrorMap, - UnshiftTagRoute - > - >() - - // @ts-expect-error - invalid tag - builder.unshiftTag(1) - }) - - it('.output', () => { - expectTypeOf(builder.output(schema)).toEqualTypeOf< - DecoratedContractProcedure - >() - }) -}) diff --git a/packages/contract/src/procedure-builder-with-input.test.ts b/packages/contract/src/procedure-builder-with-input.test.ts deleted file mode 100644 index f18fe0b5..00000000 --- a/packages/contract/src/procedure-builder-with-input.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { z } from 'zod' -import { ContractProcedure } from './procedure' -import { ContractProcedureBuilderWithInput } from './procedure-builder-with-input' -import { DecoratedContractProcedure } from './procedure-decorated' - -const baseErrorMap = { - BASE: { - status: 500, - data: z.object({ - message: z.string(), - }), - }, -} - -const baseRoute = { - method: 'GET', - path: '/v1/users', - tags: ['tag'], -} as const - -const inputSchema = z.object({ input: z.string().transform(v => Number.parseInt(v)) }) - -const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) - -const builder = new ContractProcedureBuilderWithInput({ InputSchema: inputSchema, OutputSchema: undefined, errorMap: baseErrorMap, route: baseRoute }) - -describe('decoratedContractProcedure', () => { - it('is a procedure', () => { - expect(builder).toBeInstanceOf(ContractProcedure) - }) - - it('.errors', () => { - const errors = { BAD_GATEWAY: { data: z.object({ message: z.string() }) } } as const - - const applied = builder.errors(errors) - - expect(applied).toBeInstanceOf(ContractProcedureBuilderWithInput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual({ - ...baseErrorMap, - ...errors, - }) - expect(applied['~orpc'].InputSchema).toEqual(inputSchema) - expect(applied['~orpc'].route).toEqual(baseRoute) - }) - - it('.route', () => { - const applied = builder.route({ method: 'PATCH', description: 'new message' }) - - expect(applied).toBeInstanceOf(ContractProcedureBuilderWithInput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].InputSchema).toEqual(inputSchema) - expect(applied['~orpc'].route).toEqual({ - method: 'PATCH', - description: 'new message', - path: '/v1/users', - tags: ['tag'], - }) - }) - - it('.prefix', () => { - const applied = builder.prefix('/api') - - expect(applied).toBeInstanceOf(ContractProcedureBuilderWithInput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].InputSchema).toEqual(inputSchema) - expect(applied['~orpc'].route).toEqual({ - method: 'GET', - path: '/api/v1/users', - tags: ['tag'], - }) - }) - - it('.unshiftTag', () => { - const applied = builder.unshiftTag('tag2', 'tag3') - - expect(applied).toBeInstanceOf(ContractProcedureBuilderWithInput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].InputSchema).toEqual(inputSchema) - expect(applied['~orpc'].route).toEqual({ - method: 'GET', - tags: ['tag2', 'tag3', 'tag'], - path: '/v1/users', - }) - }) - - it('.output', () => { - const applied = builder.output(schema) - expect(applied).toBeInstanceOf(DecoratedContractProcedure) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].route).toEqual(baseRoute) - expect(applied['~orpc'].InputSchema).toEqual(inputSchema) - expect(applied['~orpc'].OutputSchema).toEqual(schema) - }) -}) diff --git a/packages/contract/src/procedure-builder-with-input.ts b/packages/contract/src/procedure-builder-with-input.ts deleted file mode 100644 index 47ad8d3d..00000000 --- a/packages/contract/src/procedure-builder-with-input.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { ErrorMap, ErrorMapGuard, ErrorMapSuggestions, StrictErrorMap } from './error-map' -import type { HTTPPath, MergeRoute, PrefixRoute, Route, UnshiftTagRoute } from './route' -import type { Schema, SchemaOutput } from './types' -import { ContractProcedure } from './procedure' -import { DecoratedContractProcedure } from './procedure-decorated' - -/** - * `ContractProcedureBuilderWithInput` is a branch of `ContractProcedureBuilder` which it has input schema. - * - * Why? - * - prevents override input schema after .input - */ -export class ContractProcedureBuilderWithInput< - TInputSchema extends Schema, - TErrorMap extends ErrorMap, - TRoute extends Route, -> extends ContractProcedure { - errors & ErrorMapSuggestions>( - errors: U, - ): ContractProcedureBuilderWithInput & TErrorMap, TRoute> { - const decorated = DecoratedContractProcedure.decorate(this).errors(errors) - return new ContractProcedureBuilderWithInput(decorated['~orpc']) - } - - route(route: U): ContractProcedureBuilderWithInput> { - const decorated = DecoratedContractProcedure.decorate(this).route(route) - return new ContractProcedureBuilderWithInput(decorated['~orpc']) - } - - prefix(prefix: U): ContractProcedureBuilderWithInput> { - const decorated = DecoratedContractProcedure.decorate(this).prefix(prefix) - return new ContractProcedureBuilderWithInput(decorated['~orpc']) - } - - unshiftTag(...tags: U): ContractProcedureBuilderWithInput> { - const decorated = DecoratedContractProcedure.decorate(this).unshiftTag(...tags) - return new ContractProcedureBuilderWithInput(decorated['~orpc']) - } - - output(schema: U, example?: SchemaOutput): DecoratedContractProcedure { - return new DecoratedContractProcedure({ - ...this['~orpc'], - OutputSchema: schema, - outputExample: example, - }) - } -} diff --git a/packages/contract/src/procedure-builder-with-output.test-d.ts b/packages/contract/src/procedure-builder-with-output.test-d.ts deleted file mode 100644 index db2584f1..00000000 --- a/packages/contract/src/procedure-builder-with-output.test-d.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { ReadonlyDeep } from '@orpc/shared' -import type { StrictErrorMap } from './error-map' -import type { ContractProcedure } from './procedure' -import type { ContractProcedureBuilderWithOutput } from './procedure-builder-with-output' -import type { DecoratedContractProcedure } from './procedure-decorated' -import type { PrefixRoute, UnshiftTagRoute } from './route' -import { z } from 'zod' - -const baseErrorMap = { - BASE: { - status: 500, - data: z.object({ - message: z.string(), - }), - }, -} - -const outputSchema = z.object({ input: z.string().transform(v => Number.parseInt(v)) }) - -const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) - -type BaseRoute = { path: '/base' } - -const builder = {} as ContractProcedureBuilderWithOutput - -describe('DecoratedContractProcedure', () => { - it('is a contract procedure', () => { - expectTypeOf(builder).toMatchTypeOf< - ContractProcedure - >() - }) - - it('.errors', () => { - const errors = { BAD_GATEWAY: { data: z.object({ message: z.string() }) } } as const - - expectTypeOf(builder.errors(errors)).toEqualTypeOf< - ContractProcedureBuilderWithOutput, BaseRoute> - >() - - // @ts-expect-error - not allow redefine error map - builder.errors({ BASE: baseErrorMap.BASE }) - }) - - it('.route', () => { - expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf< - ContractProcedureBuilderWithOutput< - typeof outputSchema, - typeof baseErrorMap, - ReadonlyDeep<{ method: 'GET' }> & BaseRoute - > - >() - - // @ts-expect-error - invalid method - builder.route({ method: 'HE' }) - }) - - it('.prefix', () => { - expectTypeOf(builder.prefix('/api')).toEqualTypeOf< - ContractProcedureBuilderWithOutput< - typeof outputSchema, - typeof baseErrorMap, - PrefixRoute - > - >() - - // @ts-expect-error - invalid prefix - builder.prefix(1) - }) - - it('.unshiftTag', () => { - expectTypeOf(builder.unshiftTag('tag', 'tag2')).toEqualTypeOf< - ContractProcedureBuilderWithOutput< - typeof outputSchema, - typeof baseErrorMap, - UnshiftTagRoute - > - >() - - // @ts-expect-error - invalid tag - builder.unshiftTag(1) - }) - - it('.input', () => { - expectTypeOf(builder.input(schema)).toEqualTypeOf< - DecoratedContractProcedure - >() - }) -}) diff --git a/packages/contract/src/procedure-builder-with-output.test.ts b/packages/contract/src/procedure-builder-with-output.test.ts deleted file mode 100644 index 83f6c535..00000000 --- a/packages/contract/src/procedure-builder-with-output.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { z } from 'zod' -import { ContractProcedure } from './procedure' -import { ContractProcedureBuilderWithOutput } from './procedure-builder-with-output' -import { DecoratedContractProcedure } from './procedure-decorated' - -const baseErrorMap = { - BASE: { - status: 500, - data: z.object({ - message: z.string(), - }), - }, -} - -const baseRoute = { - method: 'GET', - path: '/v1/users', - tags: ['tag'], -} as const - -const outputSchema = z.object({ input: z.string().transform(v => Number.parseInt(v)) }) - -const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) - -const builder = new ContractProcedureBuilderWithOutput({ InputSchema: undefined, OutputSchema: outputSchema, errorMap: baseErrorMap, route: baseRoute }) - -describe('decoratedContractProcedure', () => { - it('is a procedure', () => { - expect(builder).toBeInstanceOf(ContractProcedure) - }) - - it('.errors', () => { - const errors = { BAD_GATEWAY: { data: z.object({ message: z.string() }) } } as const - - const applied = builder.errors(errors) - - expect(applied).toBeInstanceOf(ContractProcedureBuilderWithOutput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual({ - ...baseErrorMap, - ...errors, - }) - expect(applied['~orpc'].OutputSchema).toEqual(outputSchema) - expect(applied['~orpc'].route).toEqual(baseRoute) - }) - - it('.route', () => { - const applied = builder.route({ method: 'PATCH', description: 'new message' }) - - expect(applied).toBeInstanceOf(ContractProcedureBuilderWithOutput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].OutputSchema).toEqual(outputSchema) - expect(applied['~orpc'].route).toEqual({ - method: 'PATCH', - description: 'new message', - path: '/v1/users', - tags: ['tag'], - }) - }) - - it('.prefix', () => { - const applied = builder.prefix('/api') - - expect(applied).toBeInstanceOf(ContractProcedureBuilderWithOutput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].OutputSchema).toEqual(outputSchema) - expect(applied['~orpc'].route).toEqual({ - method: 'GET', - path: '/api/v1/users', - tags: ['tag'], - }) - }) - - it('.unshiftTag', () => { - const applied = builder.unshiftTag('tag2', 'tag3') - - expect(applied).toBeInstanceOf(ContractProcedureBuilderWithOutput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].OutputSchema).toEqual(outputSchema) - expect(applied['~orpc'].route).toEqual({ - method: 'GET', - tags: ['tag2', 'tag3', 'tag'], - path: '/v1/users', - }) - }) - - it('.input', () => { - const applied = builder.input(schema) - expect(applied).toBeInstanceOf(DecoratedContractProcedure) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].route).toEqual(baseRoute) - expect(applied['~orpc'].InputSchema).toEqual(schema) - expect(applied['~orpc'].OutputSchema).toEqual(outputSchema) - }) -}) diff --git a/packages/contract/src/procedure-builder-with-output.ts b/packages/contract/src/procedure-builder-with-output.ts deleted file mode 100644 index 568679a0..00000000 --- a/packages/contract/src/procedure-builder-with-output.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { ErrorMap, ErrorMapGuard, ErrorMapSuggestions, StrictErrorMap } from './error-map' -import type { HTTPPath, MergeRoute, PrefixRoute, Route, UnshiftTagRoute } from './route' -import type { Schema, SchemaInput } from './types' -import { ContractProcedure } from './procedure' -import { DecoratedContractProcedure } from './procedure-decorated' - -/** - * `ContractProcedureBuilderWithOutput` is a branch of `ContractProcedureBuilder` which it has output schema. - * - * Why? - * - prevents override output schema after .output - */ -export class ContractProcedureBuilderWithOutput< - TOutputSchema extends Schema, - TErrorMap extends ErrorMap, - TRoute extends Route, -> extends ContractProcedure { - errors & ErrorMapSuggestions>( - errors: U, - ): ContractProcedureBuilderWithOutput & TErrorMap, TRoute> { - const decorated = DecoratedContractProcedure.decorate(this).errors(errors) - return new ContractProcedureBuilderWithOutput(decorated['~orpc']) - } - - route(route: U): ContractProcedureBuilderWithOutput> { - const decorated = DecoratedContractProcedure.decorate(this).route(route) - return new ContractProcedureBuilderWithOutput(decorated['~orpc']) - } - - prefix(prefix: U): ContractProcedureBuilderWithOutput> { - const decorated = DecoratedContractProcedure.decorate(this).prefix(prefix) - return new ContractProcedureBuilderWithOutput(decorated['~orpc']) - } - - unshiftTag(...tags: U): ContractProcedureBuilderWithOutput> { - const decorated = DecoratedContractProcedure.decorate(this).unshiftTag(...tags) - return new ContractProcedureBuilderWithOutput(decorated['~orpc']) - } - - input(schema: U, example?: SchemaInput): DecoratedContractProcedure { - return new DecoratedContractProcedure({ - ...this['~orpc'], - InputSchema: schema, - inputExample: example, - }) - } -} diff --git a/packages/contract/src/procedure-builder.test-d.ts b/packages/contract/src/procedure-builder.test-d.ts deleted file mode 100644 index b069bf74..00000000 --- a/packages/contract/src/procedure-builder.test-d.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { ReadonlyDeep } from '@orpc/shared' -import type { StrictErrorMap } from './error-map' -import type { ContractProcedure } from './procedure' -import type { ContractProcedureBuilder } from './procedure-builder' -import type { ContractProcedureBuilderWithInput } from './procedure-builder-with-input' -import type { ContractProcedureBuilderWithOutput } from './procedure-builder-with-output' -import type { PrefixRoute, UnshiftTagRoute } from './route' -import { z } from 'zod' - -const baseErrorMap = { - BASE: { - status: 500, - data: z.object({ - message: z.string(), - }), - }, -} - -const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) - -type BaseRoute = { path: '/base' } - -const builder = {} as ContractProcedureBuilder - -describe('DecoratedContractProcedure', () => { - it('is a contract procedure', () => { - expectTypeOf(builder).toMatchTypeOf>() - }) - - it('.errors', () => { - const errors = { BAD_GATEWAY: { data: z.object({ message: z.string() }) } } as const - - expectTypeOf(builder.errors(errors)) - .toEqualTypeOf < ContractProcedureBuilder, BaseRoute>>() - - // @ts-expect-error - not allow redefine error map - builder.errors({ BASE: baseErrorMap.BASE }) - }) - - it('.route', () => { - expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf< - ContractProcedureBuilder< - typeof baseErrorMap, - ReadonlyDeep<{ method: 'GET' }> & BaseRoute - > - >() - - // @ts-expect-error - invalid method - builder.route({ method: 'HE' }) - }) - - it('.prefix', () => { - expectTypeOf(builder.prefix('/api')).toEqualTypeOf< - ContractProcedureBuilder> - >() - - // @ts-expect-error - invalid prefix - builder.prefix(1) - }) - - it('.unshiftTag', () => { - expectTypeOf(builder.unshiftTag('tag', 'tag2')).toEqualTypeOf< - ContractProcedureBuilder> - >() - - // @ts-expect-error - invalid tag - builder.unshiftTag(1) - }) - - it('.input', () => { - expectTypeOf(builder.input(schema)).toEqualTypeOf< - ContractProcedureBuilderWithInput - >() - }) - - it('.output', () => { - expectTypeOf(builder.output(schema)).toEqualTypeOf< - ContractProcedureBuilderWithOutput - >() - }) -}) diff --git a/packages/contract/src/procedure-builder.test.ts b/packages/contract/src/procedure-builder.test.ts deleted file mode 100644 index 138be079..00000000 --- a/packages/contract/src/procedure-builder.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { z } from 'zod' -import { ContractProcedure } from './procedure' -import { ContractProcedureBuilder } from './procedure-builder' -import { ContractProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ContractProcedureBuilderWithOutput } from './procedure-builder-with-output' - -const baseErrorMap = { - BASE: { - status: 500, - data: z.object({ - message: z.string(), - }), - }, -} - -const baseRoute = { - method: 'GET', - path: '/v1/users', - tags: ['tag'], -} as const - -const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) - -const builder = new ContractProcedureBuilder({ InputSchema: undefined, OutputSchema: undefined, errorMap: baseErrorMap, route: baseRoute }) - -describe('decoratedContractProcedure', () => { - it('is a procedure', () => { - expect(builder).toBeInstanceOf(ContractProcedure) - }) - - it('.errors', () => { - const errors = { BAD_GATEWAY: { data: z.object({ message: z.string() }) } } as const - - const applied = builder.errors(errors) - - expect(applied).toBeInstanceOf(ContractProcedureBuilder) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual({ - ...baseErrorMap, - ...errors, - }) - expect(applied['~orpc'].route).toEqual(baseRoute) - }) - - it('.route', () => { - const applied = builder.route({ method: 'PATCH', description: 'new message' }) - - expect(applied).toBeInstanceOf(ContractProcedureBuilder) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].route).toEqual({ - method: 'PATCH', - description: 'new message', - path: '/v1/users', - tags: ['tag'], - }) - }) - - it('.prefix', () => { - const applied = builder.prefix('/api') - - expect(applied).toBeInstanceOf(ContractProcedureBuilder) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].route).toEqual({ - method: 'GET', - path: '/api/v1/users', - tags: ['tag'], - }) - }) - - it('.unshiftTag', () => { - const applied = builder.unshiftTag('tag2', 'tag3') - - expect(applied).toBeInstanceOf(ContractProcedureBuilder) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].route).toEqual({ - method: 'GET', - tags: ['tag2', 'tag3', 'tag'], - path: '/v1/users', - }) - }) - - it('.input', () => { - const applied = builder.input(schema) - expect(applied).toBeInstanceOf(ContractProcedureBuilderWithInput) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].route).toEqual(baseRoute) - expect(applied['~orpc'].InputSchema).toEqual(schema) - }) - - it('.output', () => { - const applied = builder.output(schema) - expect(applied).toBeInstanceOf(ContractProcedureBuilderWithOutput) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].route).toEqual(baseRoute) - expect(applied['~orpc'].OutputSchema).toEqual(schema) - }) -}) diff --git a/packages/contract/src/procedure-builder.ts b/packages/contract/src/procedure-builder.ts deleted file mode 100644 index 2f387dc5..00000000 --- a/packages/contract/src/procedure-builder.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { ErrorMap, ErrorMapGuard, ErrorMapSuggestions, StrictErrorMap } from './error-map' -import type { HTTPPath, MergeRoute, PrefixRoute, Route, UnshiftTagRoute } from './route' -import type { Schema, SchemaInput, SchemaOutput } from './types' -import { ContractProcedure } from './procedure' -import { ContractProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ContractProcedureBuilderWithOutput } from './procedure-builder-with-output' -import { DecoratedContractProcedure } from './procedure-decorated' - -export class ContractProcedureBuilder< - TErrorMap extends ErrorMap, - TRoute extends Route, -> extends ContractProcedure { - errors & ErrorMapSuggestions>( - errors: U, - ): ContractProcedureBuilder & TErrorMap, TRoute> { - const decorated = DecoratedContractProcedure.decorate(this).errors(errors) - return new ContractProcedureBuilder(decorated['~orpc']) - } - - route(route: U): ContractProcedureBuilder> { - const decorated = DecoratedContractProcedure.decorate(this).route(route) - return new ContractProcedureBuilder(decorated['~orpc']) - } - - prefix(prefix: U): ContractProcedureBuilder> { - const decorated = DecoratedContractProcedure.decorate(this).prefix(prefix) - return new ContractProcedureBuilder(decorated['~orpc']) - } - - unshiftTag(...tags: U): ContractProcedureBuilder> { - const decorated = DecoratedContractProcedure.decorate(this).unshiftTag(...tags) - return new ContractProcedureBuilder(decorated['~orpc']) - } - - input(schema: U, example?: SchemaInput): ContractProcedureBuilderWithInput { - return new ContractProcedureBuilderWithInput({ - ...this['~orpc'], - InputSchema: schema, - inputExample: example, - }) - } - - output(schema: U, example?: SchemaOutput): ContractProcedureBuilderWithOutput { - return new ContractProcedureBuilderWithOutput({ - ...this['~orpc'], - OutputSchema: schema, - outputExample: example, - }) - } -} diff --git a/packages/contract/src/procedure-client.test-d.ts b/packages/contract/src/procedure-client.test-d.ts index 416789db..8b3a5d6f 100644 --- a/packages/contract/src/procedure-client.test-d.ts +++ b/packages/contract/src/procedure-client.test-d.ts @@ -1,31 +1,19 @@ +import type { baseErrorMap, inputSchema, outputSchema } from '../tests/shared' import type { Client } from './client' import type { ORPCError } from './error-orpc' import type { ContractProcedureClient } from './procedure-client' -import { z } from 'zod' - -const schema = z.object({ - value: z.string().transform(() => 1), -}) -const baseErrors = { - CODE: { - data: z.object({ - why: z.string(), - }), - }, -} describe('ContractProcedureClient', () => { - it('compatible with Client', () => { - expectTypeOf< - ContractProcedureClient<'context', typeof schema, typeof schema, typeof baseErrors> - >().toEqualTypeOf>>() - - expectTypeOf< - ContractProcedureClient<'context', typeof schema, typeof schema, typeof baseErrors> - >().not.toEqualTypeOf>>() - + it('is a client', () => { expectTypeOf< - ContractProcedureClient<'context', typeof schema, typeof schema, typeof baseErrors> - >().not.toEqualTypeOf>>() + ContractProcedureClient<'context', typeof inputSchema, typeof outputSchema, typeof baseErrorMap> + >().toEqualTypeOf< + Client< + 'context', + { input: number }, + { output: string }, + Error | ORPCError<'BASE', { output: string }> | ORPCError<'OVERRIDE', unknown> + > + >() }) }) diff --git a/packages/contract/src/procedure-client.ts b/packages/contract/src/procedure-client.ts index e2d7dfa4..071ab6ec 100644 --- a/packages/contract/src/procedure-client.ts +++ b/packages/contract/src/procedure-client.ts @@ -1,7 +1,7 @@ import type { Client } from './client' import type { ErrorFromErrorMap } from './error' import type { ErrorMap } from './error-map' -import type { Schema, SchemaInput, SchemaOutput } from './types' +import type { Schema, SchemaInput, SchemaOutput } from './schema' export type ContractProcedureClient< TClientContext, diff --git a/packages/contract/src/procedure-decorated.test-d.ts b/packages/contract/src/procedure-decorated.test-d.ts deleted file mode 100644 index 065993bd..00000000 --- a/packages/contract/src/procedure-decorated.test-d.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { ReadonlyDeep } from '@orpc/shared' -import type { StrictErrorMap } from './error-map' -import type { PrefixRoute, UnshiftTagRoute } from './route' -import { z } from 'zod' -import { ContractProcedure } from './procedure' -import { DecoratedContractProcedure } from './procedure-decorated' - -const baseErrorMap = { - BASE: { - status: 500, - data: z.object({ - message: z.string(), - }), - }, -} - -const InputSchema = z.object({ input: z.string().transform(val => Number(val)) }) -const OutputSchema = z.object({ output: z.string().transform(val => Number(val)) }) - -const baseRoute = { - path: '/api/v1/users', -} as const - -const decorated = new DecoratedContractProcedure({ InputSchema, OutputSchema, errorMap: baseErrorMap, route: baseRoute }) - -describe('DecoratedContractProcedure', () => { - it('is a contract procedure', () => { - expectTypeOf(decorated).toMatchTypeOf< - ContractProcedure - >() - }) - - it('.decorate', () => { - expectTypeOf( - DecoratedContractProcedure.decorate(new ContractProcedure({ InputSchema, OutputSchema, errorMap: baseErrorMap, route: baseRoute })), - ).toEqualTypeOf< - DecoratedContractProcedure - >() - - expectTypeOf( - DecoratedContractProcedure.decorate(decorated), - ).toEqualTypeOf< - DecoratedContractProcedure - >() - }) - - it('.errors', () => { - const errors = { BAD_GATEWAY: { data: z.object({ message: z.string() }) } } as const - - expectTypeOf(decorated.errors(errors)).toEqualTypeOf< - DecoratedContractProcedure, typeof baseRoute> - >() - - // @ts-expect-error - not allow redefine error map - decorated.errors({ BASE: baseErrorMap.BASE }) - }) - - it('.route', () => { - expectTypeOf(decorated.route({ method: 'GET' })).toEqualTypeOf< - DecoratedContractProcedure< - typeof InputSchema, - typeof OutputSchema, - typeof baseErrorMap, - ReadonlyDeep<{ method: 'GET' }> & typeof baseRoute - > - >() - - // @ts-expect-error - invalid method - decorated.route({ method: 'HE' }) - }) - - it('.prefix', () => { - expectTypeOf(decorated.prefix('/api')).toEqualTypeOf< - DecoratedContractProcedure< - typeof InputSchema, - typeof OutputSchema, - typeof baseErrorMap, - PrefixRoute - > - >() - - // @ts-expect-error - invalid prefix - decorated.prefix(1) - }) - - it('.unshiftTag', () => { - expectTypeOf(decorated.unshiftTag('tag', 'tag2')).toEqualTypeOf< - DecoratedContractProcedure< - typeof InputSchema, - typeof OutputSchema, - typeof baseErrorMap, - UnshiftTagRoute - > - >() - - // @ts-expect-error - invalid tag - decorated.unshiftTag(1) - }) -}) diff --git a/packages/contract/src/procedure-decorated.test.ts b/packages/contract/src/procedure-decorated.test.ts deleted file mode 100644 index 39e3ccdd..00000000 --- a/packages/contract/src/procedure-decorated.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { z } from 'zod' -import { ContractProcedure } from './procedure' -import { DecoratedContractProcedure } from './procedure-decorated' - -const baseErrorMap = { - BASE: { - status: 500, - data: z.object({ - message: z.string(), - }), - }, -} - -const InputSchema = z.object({ input: z.string().transform(val => Number(val)) }) -const OutputSchema = z.object({ output: z.string().transform(val => Number(val)) }) - -const baseRoute = { - method: 'GET', - path: '/v1/users', - tags: ['tag'], -} as const - -const decorated = new DecoratedContractProcedure({ InputSchema, OutputSchema, errorMap: baseErrorMap, route: baseRoute }) - -describe('decoratedContractProcedure', () => { - it('is a procedure', () => { - expect(decorated).toBeInstanceOf(ContractProcedure) - }) - - it('.decorate', () => { - const applied = DecoratedContractProcedure.decorate(new ContractProcedure({ InputSchema, OutputSchema, errorMap: baseErrorMap, route: baseRoute })) - - expect(applied).toEqual(decorated) - expect(applied).not.toBe(decorated) - - expect(DecoratedContractProcedure.decorate(decorated)) - .toBe(decorated) - }) - - it('.errors', () => { - const errors = { BAD_GATEWAY: { data: z.object({ message: z.string() }) } } as const - - const applied = decorated.errors(errors) - - expect(applied).toBeInstanceOf(DecoratedContractProcedure) - expect(applied).not.toBe(decorated) - expect(applied['~orpc'].errorMap).toEqual({ - ...baseErrorMap, - ...errors, - }) - expect(applied['~orpc'].InputSchema).toEqual(InputSchema) - expect(applied['~orpc'].OutputSchema).toEqual(OutputSchema) - expect(applied['~orpc'].route).toEqual(baseRoute) - }) - - it('.route', () => { - const applied = decorated.route({ method: 'PATCH', description: 'new message' }) - - expect(applied).toBeInstanceOf(DecoratedContractProcedure) - expect(applied).not.toBe(decorated) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].InputSchema).toEqual(InputSchema) - expect(applied['~orpc'].OutputSchema).toEqual(OutputSchema) - expect(applied['~orpc'].route).toEqual({ - method: 'PATCH', - description: 'new message', - path: '/v1/users', - tags: ['tag'], - }) - }) - - describe('.prefix', () => { - it('when has path', () => { - const applied = decorated.prefix('/api') - - expect(applied).toBeInstanceOf(DecoratedContractProcedure) - expect(applied).not.toBe(decorated) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].InputSchema).toEqual(InputSchema) - expect(applied['~orpc'].OutputSchema).toEqual(OutputSchema) - expect(applied['~orpc'].route).toEqual({ - method: 'GET', - path: '/api/v1/users', - tags: ['tag'], - }) - }) - - it('when has no path', () => { - const decorated = new DecoratedContractProcedure({ InputSchema, OutputSchema, errorMap: baseErrorMap, route: {} }) - const applied = decorated.prefix('/api') - expect(applied['~orpc'].route).toEqual({}) - }) - }) - - describe('.unshiftTag', () => { - it('works', () => { - const applied = decorated.unshiftTag('tag2', 'tag3') - - expect(applied).toBeInstanceOf(DecoratedContractProcedure) - expect(applied).not.toBe(decorated) - expect(applied['~orpc'].errorMap).toEqual(baseErrorMap) - expect(applied['~orpc'].InputSchema).toEqual(InputSchema) - expect(applied['~orpc'].OutputSchema).toEqual(OutputSchema) - expect(applied['~orpc'].route).toEqual({ - method: 'GET', - tags: ['tag2', 'tag3', 'tag'], - path: '/v1/users', - }) - }) - - it('decorated without existing tag', () => { - const decorated = new DecoratedContractProcedure({ InputSchema, OutputSchema, errorMap: baseErrorMap, route: {} }) - - const applied = decorated.unshiftTag('tag', 'tag2') - expect(applied['~orpc'].route).toEqual({ - tags: ['tag', 'tag2'], - }) - }) - }) -}) diff --git a/packages/contract/src/procedure-decorated.ts b/packages/contract/src/procedure-decorated.ts deleted file mode 100644 index 948e47b4..00000000 --- a/packages/contract/src/procedure-decorated.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { ErrorMap, ErrorMapGuard, ErrorMapSuggestions, StrictErrorMap } from './error-map' -import type { Schema } from './types' -import { ContractProcedure } from './procedure' -import { type HTTPPath, mergeRoute, type MergeRoute, prefixRoute, type PrefixRoute, type Route, unshiftTagRoute, type UnshiftTagRoute } from './route' - -export class DecoratedContractProcedure< - TInputSchema extends Schema, - TOutputSchema extends Schema, - TErrorMap extends ErrorMap, - TRoute extends Route, -> extends ContractProcedure { - static decorate< - UInputSchema extends Schema, - UOutputSchema extends Schema, - UErrorMap extends ErrorMap, - URoute extends Route, - >( - procedure: ContractProcedure, - ): DecoratedContractProcedure { - if (procedure instanceof DecoratedContractProcedure) { - return procedure - } - - return new DecoratedContractProcedure(procedure['~orpc']) - } - - errors & ErrorMapSuggestions>( - errors: U, - ): DecoratedContractProcedure & TErrorMap, TRoute> { - return new DecoratedContractProcedure({ - ...this['~orpc'], - errorMap: { - ...this['~orpc'].errorMap, - ...errors, - }, - }) - } - - route( - route: U, - ): DecoratedContractProcedure> { - return new DecoratedContractProcedure({ - ...this['~orpc'], - route: mergeRoute(this['~orpc'].route, route), - }) - } - - prefix( - prefix: U, - ): DecoratedContractProcedure> { - return new DecoratedContractProcedure({ - ...this['~orpc'], - route: prefixRoute(this['~orpc'].route, prefix), - }) - } - - unshiftTag( - ...tags: U - ): DecoratedContractProcedure> { - return new DecoratedContractProcedure({ - ...this['~orpc'], - route: unshiftTagRoute(this['~orpc'].route, tags), - }) - } -} diff --git a/packages/contract/src/procedure.test-d.ts b/packages/contract/src/procedure.test-d.ts deleted file mode 100644 index 30dbd1c1..00000000 --- a/packages/contract/src/procedure.test-d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ANY_CONTRACT_PROCEDURE } from './procedure' -import { isContractProcedure } from './procedure' - -describe('isContractProcedure', () => { - it('works', () => { - const procedure = {} as unknown - - if (isContractProcedure(procedure)) { - expectTypeOf(procedure).toEqualTypeOf() - } - }) -}) diff --git a/packages/contract/src/procedure.test.ts b/packages/contract/src/procedure.test.ts index b6f8798e..e5978ed2 100644 --- a/packages/contract/src/procedure.test.ts +++ b/packages/contract/src/procedure.test.ts @@ -1,41 +1,41 @@ -import { z } from 'zod' +import { ping, pong } from '../tests/shared' import { ContractProcedure, isContractProcedure } from './procedure' describe('contractProcedure', () => { it('throws error when route.successStatus is not between 200 and 299', () => { expect( - () => new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined, route: { successStatus: 100 }, errorMap: {} }), + () => new ContractProcedure({ ...ping['~orpc'], route: { successStatus: 100 } }), ).toThrowError() expect( - () => new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined, route: { successStatus: 300 }, errorMap: {} }), + () => new ContractProcedure({ ...ping['~orpc'], route: { successStatus: 300 } }), ).toThrowError() expect( - () => new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined, route: { successStatus: 299 }, errorMap: {} }), + () => new ContractProcedure({ ...ping['~orpc'], route: { successStatus: 299 } }), ).not.toThrowError() }) it('throws error when errorMap has invalid status code', () => { expect( () => new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - route: { }, + ...ping['~orpc'], errorMap: { BAD_GATEWAY: { status: 100 } }, }), ).toThrowError() expect( - () => new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined, route: { }, errorMap: { - BAD_GATEWAY: { status: 600 }, - } }), + () => new ContractProcedure({ + ...ping['~orpc'], + errorMap: { + BAD_GATEWAY: { status: 600 }, + }, + }), ).toThrowError() }) }) describe('isContractProcedure', () => { it('works', () => { - expect(new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined, errorMap: {}, route: {} })).toSatisfy(isContractProcedure) - expect(new ContractProcedure({ InputSchema: z.object({}), OutputSchema: undefined, errorMap: {}, route: {} })).toSatisfy(isContractProcedure) - expect(new ContractProcedure({ InputSchema: z.object({}), OutputSchema: undefined, route: {}, errorMap: {} })).toSatisfy(isContractProcedure) + expect(ping).toSatisfy(isContractProcedure) + expect(pong).toSatisfy(isContractProcedure) expect({}).not.toSatisfy(isContractProcedure) expect(true).not.toSatisfy(isContractProcedure) expect(1).not.toSatisfy(isContractProcedure) @@ -43,8 +43,7 @@ describe('isContractProcedure', () => { }) it('works with raw object', () => { - expect(Object.assign({}, new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined, errorMap: {}, route: {} }))).toSatisfy(isContractProcedure) - expect(Object.assign({}, new ContractProcedure({ InputSchema: z.object({}), OutputSchema: undefined, errorMap: {}, route: {} }))).toSatisfy(isContractProcedure) - expect(Object.assign({}, new ContractProcedure({ InputSchema: z.object({}), OutputSchema: undefined, route: {}, errorMap: {} }))).toSatisfy(isContractProcedure) + expect(Object.assign({}, ping)).toSatisfy(isContractProcedure) + expect(Object.assign({}, pong)).toSatisfy(isContractProcedure) }) }) diff --git a/packages/contract/src/procedure.ts b/packages/contract/src/procedure.ts index f7327bb7..0d12b44f 100644 --- a/packages/contract/src/procedure.ts +++ b/packages/contract/src/procedure.ts @@ -1,18 +1,18 @@ import type { ErrorMap } from './error-map' +import type { Meta } from './meta' import type { Route } from './route' -import type { Schema, SchemaOutput } from './types' +import type { Schema } from './schema' export interface ContractProcedureDef< TInputSchema extends Schema, TOutputSchema extends Schema, TErrorMap extends ErrorMap, - TRoute extends Route, + TMeta extends Meta, > { - route: TRoute - InputSchema: TInputSchema - inputExample?: SchemaOutput - OutputSchema: TOutputSchema - outputExample?: SchemaOutput + meta: TMeta + route: Route + inputSchema: TInputSchema + outputSchema: TOutputSchema errorMap: TErrorMap } @@ -20,12 +20,11 @@ export class ContractProcedure< TInputSchema extends Schema, TOutputSchema extends Schema, TErrorMap extends ErrorMap, - TRoute extends Route, + TMeta extends Meta, > { - '~type' = 'ContractProcedure' as const - '~orpc': ContractProcedureDef + '~orpc': ContractProcedureDef - constructor(def: ContractProcedureDef) { + constructor(def: ContractProcedureDef) { if (def.route?.successStatus && (def.route.successStatus < 200 || def.route?.successStatus > 299)) { throw new Error('[ContractProcedure] The successStatus must be between 200 and 299') } @@ -38,10 +37,9 @@ export class ContractProcedure< } } -export type ANY_CONTRACT_PROCEDURE = ContractProcedure -export type WELL_CONTRACT_PROCEDURE = ContractProcedure +export type AnyContractProcedure = ContractProcedure -export function isContractProcedure(item: unknown): item is ANY_CONTRACT_PROCEDURE { +export function isContractProcedure(item: unknown): item is AnyContractProcedure { if (item instanceof ContractProcedure) { return true } @@ -49,14 +47,13 @@ export function isContractProcedure(item: unknown): item is ANY_CONTRACT_PROCEDU return ( (typeof item === 'object' || typeof item === 'function') && item !== null - && '~type' in item - && item['~type'] === 'ContractProcedure' && '~orpc' in item && typeof item['~orpc'] === 'object' && item['~orpc'] !== null - && 'InputSchema' in item['~orpc'] - && 'OutputSchema' in item['~orpc'] + && 'inputSchema' in item['~orpc'] + && 'outputSchema' in item['~orpc'] && 'errorMap' in item['~orpc'] && 'route' in item['~orpc'] + && 'meta' in item['~orpc'] ) } diff --git a/packages/contract/src/route.test-d.ts b/packages/contract/src/route.test-d.ts deleted file mode 100644 index d24b56e3..00000000 --- a/packages/contract/src/route.test-d.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { MergePrefix, MergeRoute, MergeTags, PrefixRoute, UnshiftTagRoute } from './route' - -it('MergeRoute', () => { - expectTypeOf>().toMatchTypeOf<{ path: '/v1' }>() - expectTypeOf>().toMatchTypeOf<{ path: '/v1', method: 'GET' }>() - expectTypeOf>().toMatchTypeOf<{ path: '/v1', method: 'GET' }>() -}) - -it('PrefixRoute', () => { - expectTypeOf>().toMatchTypeOf<{ tags: ['tag'] }>() - expectTypeOf>().toMatchTypeOf<{ path: '/v1/api' }>() - expectTypeOf>().toMatchTypeOf<{ path: '/v1/api', method: 'GET' }>() -}) - -it('UnshiftTagRoute', () => { - expectTypeOf>().toMatchTypeOf<{ path: '/api', tags: ['tag2'] }>() - expectTypeOf>().toMatchTypeOf<{ tags: ['tag2', 'tag'] }>() -}) - -it('MergePrefix', () => { - expectTypeOf>().toEqualTypeOf<'/v1'>() - expectTypeOf>().toEqualTypeOf<'/api/v1'>() -}) - -it('MergeTags', () => { - expectTypeOf>().toEqualTypeOf<['tag']>() - expectTypeOf>().toEqualTypeOf<['tag', 'tag2']>() - expectTypeOf>().toEqualTypeOf<['tag', 'tag', 'tag2']>() -}) diff --git a/packages/contract/src/route.test.ts b/packages/contract/src/route.test.ts index cb66f932..8aa377b0 100644 --- a/packages/contract/src/route.test.ts +++ b/packages/contract/src/route.test.ts @@ -1,4 +1,4 @@ -import { mergePrefix, mergeRoute, mergeTags, prefixRoute, unshiftTagRoute } from './route' +import { adaptRoute, mergePrefix, mergeRoute, mergeTags, prefixRoute, unshiftTagRoute } from './route' it('mergeRoute', () => { expect(mergeRoute({ path: '/api' }, { path: '/v1' })).toEqual({ path: '/v1' }) @@ -28,3 +28,38 @@ it('mergeTags', () => { expect(mergeTags(['tag'], ['tag2'])).toEqual(['tag', 'tag2']) expect(mergeTags(['tag'], ['tag', 'tag2'])).toEqual(['tag', 'tag', 'tag2']) }) + +it('adaptRoute', () => { + const route = { + path: '/api/v1', + tags: ['tag'], + description: 'description', + } as const + + expect(adaptRoute(route, { + prefix: '/adapt', + tags: ['adapt'], + })).toEqual({ + path: '/adapt/api/v1', + tags: ['adapt', 'tag'], + description: 'description', + }) + + expect(adaptRoute(route, { + prefix: '/adapt', + })).toEqual({ + path: '/adapt/api/v1', + tags: ['tag'], + description: 'description', + }) + + expect(adaptRoute(route, { + tags: ['adapt'], + })).toEqual({ + path: '/api/v1', + tags: ['adapt', 'tag'], + description: 'description', + }) + + expect(adaptRoute(route, {})).toBe(route) +}) diff --git a/packages/contract/src/route.ts b/packages/contract/src/route.ts index 0205422f..e04a51d0 100644 --- a/packages/contract/src/route.ts +++ b/packages/contract/src/route.ts @@ -72,64 +72,50 @@ export interface Route { outputStructure?: OutputStructure } -/** - * Since `undefined` has a specific meaning (it use default value), - * we ensure all additional properties in each item of the ErrorMap are explicitly set to `undefined`. - */ -export type StrictRoute = T & Partial, undefined>> - -export type MergeRoute = Omit & B - -export function mergeRoute(a: A, b: B): MergeRoute { - return { - ...a, - ...b, - } +export function mergeRoute(a: Route, b: Route): Route { + return { ...a, ...b } } -export type PrefixRoute = - TRoute['path'] extends HTTPPath ? Omit & { - // I don't know why but we need recheck TPrefix here to make typescript happy on [DecoratedContractProcedure.prefix] - path: TPrefix extends HTTPPath ? `${TPrefix}${TRoute['path']}` : TRoute['path'] - } - : TRoute - -export function prefixRoute( - route: TRoute, - prefix: TPrefix, -): PrefixRoute { +export function prefixRoute(route: Route, prefix: HTTPPath): Route { if (!route.path) { - return route as any + return route } return { ...route, - path: `${prefix}${route.path}` as any, + path: `${prefix}${route.path}`, } } - -export type UnshiftTagRoute = Omit & { - tags: TRoute['tags'] extends string[] ? [...TTags, ...TRoute['tags']] : TTags -} - -export function unshiftTagRoute( - route: TRoute, - tags: TTags, -): UnshiftTagRoute { +export function unshiftTagRoute(route: Route, tags: readonly string[]): Route { return { ...route, - tags: [...tags, ...route.tags ?? []] as any, + tags: [...tags, ...route.tags ?? []], } } -export type MergePrefix = A extends HTTPPath ? `${A}${B}` : B +export function mergePrefix(a: HTTPPath | undefined, b: HTTPPath): HTTPPath { + return a ? `${a}${b}` : b +} + +export function mergeTags(a: readonly string[] | undefined, b: readonly string[]): readonly string[] { + return a ? [...a, ...b] : b +} -export function mergePrefix(a: A, b: B): MergePrefix { - return a ? `${a}${b}` : b as any +export interface AdaptRouteOptions { + prefix?: HTTPPath + tags?: readonly string[] } -export type MergeTags = A extends readonly string[] ? [...A, ...B] : B +export function adaptRoute(route: Route, options: AdaptRouteOptions): Route { + let router = route + + if (options.prefix) { + router = prefixRoute(router, options.prefix) + } + + if (options.tags) { + router = unshiftTagRoute(router, options.tags) + } -export function mergeTags(a: A, b: B): MergeTags { - return a ? [...a, ...b] : b as any + return router } diff --git a/packages/contract/src/router-builder.test-d.ts b/packages/contract/src/router-builder.test-d.ts deleted file mode 100644 index 5ed60133..00000000 --- a/packages/contract/src/router-builder.test-d.ts +++ /dev/null @@ -1,179 +0,0 @@ -import type { StrictErrorMap } from './error-map' -import type { PrefixRoute, UnshiftTagRoute } from './route' -import type { AdaptedContractRouter, ContractRouterBuilder } from './router-builder' -import { z } from 'zod' -import { ContractProcedure } from './procedure' -import { DecoratedContractProcedure } from './procedure-decorated' - -const schema = z.object({ - value: z.string().transform(() => 1), -}) - -const baseErrors = { - BASE: { - status: 401, - data: z.string(), - }, -} as const - -const baseRoute = { path: '/procedure' } as const - -const ping = new ContractProcedure({ InputSchema: schema, OutputSchema: undefined, route: baseRoute, errorMap: baseErrors }) -const pinged = DecoratedContractProcedure.decorate(ping) - -const pong = new ContractProcedure({ InputSchema: undefined, OutputSchema: schema, errorMap: {}, route: {} }) -const ponged = DecoratedContractProcedure.decorate(pong) - -const router = { - ping, - pinged, - pong, - ponged, - nested: { - ping, - pinged, - pong, - ponged, - }, -} - -const builder = {} as ContractRouterBuilder - -describe('AdaptedContractRouter', () => { - it('decorate and add extra context', () => { - const extraErrors = { - CODE: { - message: 'Error', - data: z.string(), - }, - } as const - - type G = AdaptedContractRouter['ping'] - - expectTypeOf['ping']>().toEqualTypeOf< - DecoratedContractProcedure< - typeof schema, - undefined, - typeof baseErrors & typeof extraErrors, - PrefixRoute, '/api'> - > - >() - - expectTypeOf['pinged']>().toEqualTypeOf< - DecoratedContractProcedure< - typeof schema, - undefined, - typeof baseErrors & typeof extraErrors, - PrefixRoute, '/api'> - > - >() - - expectTypeOf['nested']['pong']>().toEqualTypeOf< - DecoratedContractProcedure< - undefined, - typeof schema, - typeof extraErrors, - PrefixRoute, ['api']>, '/api'> - > - >() - }) - - it('throw error on invalid procedure', () => { - const router = { - a: 1, - } - - // @ts-expect-error - invalid procedure - type Adapted = AdaptedContractRouter - }) -}) - -describe('router', () => { - it('return adapted router', () => { - const routed = builder.router(router) - - expectTypeOf(routed).toEqualTypeOf>() - - expectTypeOf(builder.router(ping)).toEqualTypeOf>() - }) - - it('throw on conflict error map', () => { - builder.router({ ping: {} as ContractProcedure }) - // @ts-expect-error conflict - builder.router({ ping: {} as ContractProcedure }) - }) - - it('only required partial match error map', () => { - expectTypeOf(builder.router({ ping: {} as ContractProcedure })).toEqualTypeOf<{ - ping: DecoratedContractProcedure< - any, - any, - { OTHER: { status: number } } & typeof baseErrors, - PrefixRoute, '/api'> - > - }>() - }) - - it('throw error on invalid router', () => { - const router = { - a: 1, - } - - // @ts-expect-error - invalid router - const routed = builder.router(router) - }) -}) - -describe('prefix', () => { - it('return ContractRouterBuilder', () => { - const routed = builder.prefix('/api') - expectTypeOf(routed).toEqualTypeOf>() - }) - - it('require valid prefix', () => { - builder.prefix('/api') - - // @ts-expect-error - invalid prefix - builder.prefix(1) - // @ts-expect-error - invalid prefix - builder.prefix('') - }) -}) - -describe('tag', () => { - it('return ContractRouterBuilder', () => { - const applied = builder.tag('tag') - expectTypeOf(applied).toEqualTypeOf>() - }) - - it('require valid tag', () => { - builder.tag('tag') - - // @ts-expect-error - invalid tag - builder.tag(1) - // @ts-expect-error - invalid tag - builder.tag({}) - }) -}) - -describe('errors', () => { - const errors = { - BAD: { - status: 500, - data: schema, - }, - } - - it('merge old one', () => { - expectTypeOf(builder.errors(errors)).toEqualTypeOf< - ContractRouterBuilder & typeof baseErrors, '/api', ['api']> - >() - }) - - it('prevent redefine errorMap', () => { - // @ts-expect-error - not allow redefine errorMap - builder.errors({ BASE: baseErrors.BASE }) - // @ts-expect-error - not allow redefine errorMap --- even with undefined - builder.errors({ BASE: undefined }) - }) -}) diff --git a/packages/contract/src/router-builder.test.ts b/packages/contract/src/router-builder.test.ts deleted file mode 100644 index 7b54cde6..00000000 --- a/packages/contract/src/router-builder.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { z } from 'zod' -import { ContractProcedure } from './procedure' -import { DecoratedContractProcedure } from './procedure-decorated' -import { ContractRouterBuilder } from './router-builder' - -const schema = z.object({ - value: z.string(), -}) - -const baseErrorMap = { - BASE: { - status: 401, - data: z.string(), - }, -} - -const procedure = new ContractProcedure({ - InputSchema: schema, - OutputSchema: undefined, - route: { path: '/procedure', tags: ['p1'] }, - errorMap: baseErrorMap, -}) -const decorated = DecoratedContractProcedure.decorate(procedure) - -const router = { - procedure, - decorated, - nested: { - procedure, - decorated, - }, -} - -const builderErrorMap = { - BUILDER: { - status: 401, - data: z.string(), - }, -} - -const builder = new ContractRouterBuilder({ - prefix: '/api', - tags: ['tag1', 'tag2'], - errorMap: builderErrorMap, -}) - -describe('prefix', () => { - it('works', () => { - expect(builder.prefix('/1').prefix('/2')['~orpc'].prefix).toEqual('/api/1/2') - }) -}) - -describe('tag', () => { - it('works', () => { - expect(builder.tag('1', '2').tag('3')['~orpc'].tags).toEqual(['tag1', 'tag2', '1', '2', '3']) - }) -}) - -describe('errors', () => { - const errors = { - BAD: { - status: 500, - data: schema, - }, - } - - it('merge old one', () => { - expect(builder.errors(errors)['~orpc'].errorMap).toEqual({ - ...errors, - ...builder['~orpc'].errorMap, - }) - }) -}) - -describe('router', () => { - it('adapt all procedures', () => { - const routed = builder.router(router) - - expect(routed.procedure).instanceOf(DecoratedContractProcedure) - expect(routed.procedure['~orpc'].route?.path).toEqual('/api/procedure') - expect(routed.procedure['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'p1']) - expect(routed.procedure['~orpc'].errorMap).toEqual({ ...builderErrorMap, ...baseErrorMap }) - - expect(routed.decorated).instanceOf(DecoratedContractProcedure) - expect(routed.decorated['~orpc'].route?.path).toEqual('/api/procedure') - expect(routed.decorated['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'p1']) - expect(routed.decorated['~orpc'].errorMap).toEqual({ ...builderErrorMap, ...baseErrorMap }) - - expect(routed.nested.procedure).instanceOf(DecoratedContractProcedure) - expect(routed.nested.procedure['~orpc'].route?.path).toEqual('/api/procedure') - expect(routed.nested.procedure['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'p1']) - expect(routed.nested.procedure['~orpc'].errorMap).toEqual({ ...builderErrorMap, ...baseErrorMap }) - - expect(routed.nested.decorated).instanceOf(DecoratedContractProcedure) - expect(routed.nested.decorated['~orpc'].route?.path).toEqual('/api/procedure') - expect(routed.nested.decorated['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'p1']) - expect(routed.nested.decorated['~orpc'].errorMap).toEqual({ ...builderErrorMap, ...baseErrorMap }) - }) -}) diff --git a/packages/contract/src/router-builder.ts b/packages/contract/src/router-builder.ts deleted file mode 100644 index 90596be4..00000000 --- a/packages/contract/src/router-builder.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { ErrorMap, ErrorMapGuard, ErrorMapSuggestions, StrictErrorMap } from './error-map' -import type { ContractProcedure } from './procedure' -import type { ContractRouter } from './router' -import { isContractProcedure } from './procedure' -import { DecoratedContractProcedure } from './procedure-decorated' -import { type HTTPPath, mergePrefix, type MergePrefix, mergeTags, type MergeTags, type PrefixRoute, type Route, type UnshiftTagRoute } from './route' - -export type AdaptRoute = - TPrefix extends HTTPPath - ? PrefixRoute : TRoute, TPrefix> - : TTags extends readonly string[] - ? UnshiftTagRoute - : TRoute - -export type AdaptedContractRouter< - TContract extends ContractRouter, - TErrorMapExtra extends ErrorMap, - TPrefix extends HTTPPath | undefined, - TTags extends readonly string[] | undefined, -> = { - [K in keyof TContract]: TContract[K] extends - ContractProcedure - ? DecoratedContractProcedure> - : TContract[K] extends ContractRouter - ? AdaptedContractRouter - : never -} - -export interface ContractRouterBuilderDef { - errorMap: TErrorMap - prefix: TPrefix - tags: TTags -} - -export class ContractRouterBuilder { - '~type' = 'ContractProcedure' as const - '~orpc': ContractRouterBuilderDef - - constructor(def: ContractRouterBuilderDef) { - this['~orpc'] = def - } - - prefix(prefix: U): ContractRouterBuilder, TTags> { - return new ContractRouterBuilder({ - ...this['~orpc'], - prefix: mergePrefix(this['~orpc'].prefix, prefix), - }) - } - - tag(...tags: U): ContractRouterBuilder> { - return new ContractRouterBuilder({ - ...this['~orpc'], - tags: mergeTags(this['~orpc'].tags, tags), - }) - } - - errors & ErrorMapSuggestions>( - errors: U, - ): ContractRouterBuilder & TErrorMap, TPrefix, TTags> { - return new ContractRouterBuilder({ - ...this['~orpc'], - errorMap: { - ...this['~orpc'].errorMap, - ...errors, - }, - }) - } - - router>>( - router: T, - ): AdaptedContractRouter { - if (isContractProcedure(router)) { - let decorated = DecoratedContractProcedure.decorate(router) - - if (this['~orpc'].tags) { - decorated = decorated.unshiftTag(...this['~orpc'].tags) - } - - if (this['~orpc'].prefix) { - decorated = decorated.prefix(this['~orpc'].prefix) - } - - /** - * The `router` (T) has been validated to ensure no conflicts with `TErrorMap`, - * allowing us to safely cast here. - */ - decorated = decorated.errors(this['~orpc'].errorMap as any) - - return decorated as any - } - - const adapted: ContractRouter = {} - - for (const key in router) { - adapted[key] = this.router(router[key]!) - } - - return adapted as any - } -} diff --git a/packages/contract/src/router-client.test-d.ts b/packages/contract/src/router-client.test-d.ts index 708349f0..47ae3e82 100644 --- a/packages/contract/src/router-client.test-d.ts +++ b/packages/contract/src/router-client.test-d.ts @@ -1,40 +1,18 @@ import type { NestedClient } from './client' import type { ContractRouterClient } from './router-client' -import { z } from 'zod' -import { ContractProcedure } from './procedure' -import { DecoratedContractProcedure } from './procedure-decorated' - -const schema = z.object({ - value: z.string().transform(() => 1), -}) - -const baseError = { - BASE: { - data: z.string(), - }, -} - -const ping = new ContractProcedure({ InputSchema: schema, OutputSchema: undefined, route: { path: '/procedure' }, errorMap: baseError }) -const pinged = DecoratedContractProcedure.decorate(ping) - -const pong = new ContractProcedure({ InputSchema: undefined, OutputSchema: schema, errorMap: {}, route: {} }) -const ponged = DecoratedContractProcedure.decorate(pong) +import { ping, pong } from '../tests/shared' const router = { ping, - pinged, pong, - ponged, nested: { ping, - pinged, pong, - ponged, }, } describe('ContractRouterClient', () => { - it('compatible with NestedClient', () => { + it('is a NestedClient', () => { expectTypeOf>().toMatchTypeOf>() expectTypeOf>().not.toMatchTypeOf>() }) diff --git a/packages/contract/src/router-client.ts b/packages/contract/src/router-client.ts index 2fa2d3f8..21fafb98 100644 --- a/packages/contract/src/router-client.ts +++ b/packages/contract/src/router-client.ts @@ -1,10 +1,10 @@ import type { ContractProcedure } from './procedure' import type { ContractProcedureClient } from './procedure-client' -import type { ContractRouter } from './router' +import type { AnyContractRouter } from './router' -export type ContractRouterClient, TClientContext> = +export type ContractRouterClient = TRouter extends ContractProcedure ? ContractProcedureClient : { - [K in keyof TRouter]: TRouter[K] extends ContractRouter ? ContractRouterClient : never + [K in keyof TRouter]: TRouter[K] extends AnyContractRouter ? ContractRouterClient : never } diff --git a/packages/contract/src/router.test-d.ts b/packages/contract/src/router.test-d.ts index 34d37c88..d9e32fd2 100644 --- a/packages/contract/src/router.test-d.ts +++ b/packages/contract/src/router.test-d.ts @@ -1,84 +1,111 @@ -import type { ContractRouter, InferContractRouterInputs, InferContractRouterOutputs } from './router' -import { z } from 'zod' -import { ContractProcedure } from './procedure' -import { DecoratedContractProcedure } from './procedure-decorated' - -const schema = z.object({ - value: z.string().transform(() => 1), -}) - -const errorMap = { BAD_GATEWAY: { data: schema } } as const - -type SchemaIn = { value: string } -type SchemaOut = { value: number } - -const ping = new ContractProcedure({ InputSchema: schema, OutputSchema: undefined, route: { path: '/procedure' }, errorMap }) -const pinged = DecoratedContractProcedure.decorate(ping) - -const pong = new ContractProcedure({ InputSchema: undefined, OutputSchema: schema, errorMap: {}, route: {} }) -const ponged = DecoratedContractProcedure.decorate(pong) +import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../tests/shared' +import type { MergedErrorMap } from './error-map' +import type { Meta } from './meta' +import type { ContractProcedure } from './procedure' +import type { AdaptedContractRouter, ContractRouter, InferContractRouterInputs, InferContractRouterOutputs } from './router' +import { ping, pong } from '../tests/shared' const router = { ping, - pinged, pong, - ponged, nested: { ping, - pinged, pong, - ponged, }, } describe('ContractRouter', () => { - it('ErrorMap', () => { - expectTypeOf(ping).toMatchTypeOf>() - - expectTypeOf(pong).not.toMatchTypeOf>() - expectTypeOf(router).not.toMatchTypeOf>() - - expectTypeOf(pong).toMatchTypeOf>>() - expectTypeOf(router).toMatchTypeOf>>() - }) - - it('procedure also is a contract router', () => { - expectTypeOf(ping).toMatchTypeOf>() + describe('meta def', () => { + it('works', () => { + expectTypeOf(ping).toMatchTypeOf>() + expectTypeOf(pong).toMatchTypeOf>() + expectTypeOf(router).toMatchTypeOf>() + + expectTypeOf(ping).not.toMatchTypeOf>() + }) + + it('not allow conflict meta def', () => { + expectTypeOf({ + ping: {} as ContractProcedure< + undefined, + typeof outputSchema, + typeof baseErrorMap, + { mode?: number } + >, + }).not.toMatchTypeOf>() + }) + + it('works when meta def is wider', () => { + expectTypeOf({ + ping: {} as ContractProcedure< + undefined, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta & { extra?: string } + >, + }).toMatchTypeOf>() + }) + + it('works when meta def is narrower', () => { + expectTypeOf({ + ping: {} as ContractProcedure< + undefined, + typeof outputSchema, + typeof baseErrorMap, + Omit + >, + }).toMatchTypeOf>() + }) }) +}) - it('just an object and accepts both procedures and decorated procedures', () => { - expectTypeOf({ router }).toMatchTypeOf>() - }) +it('AdaptedContractRouter', () => { + const adapted = {} as AdaptedContractRouter + + expectTypeOf(adapted.ping).toEqualTypeOf< + ContractProcedure< + typeof inputSchema, + typeof outputSchema, + MergedErrorMap<{ INVALID: { status: number }, BASE2: { message: string } }, typeof baseErrorMap>, + BaseMeta + > + >() + + expectTypeOf(adapted.nested.ping).toEqualTypeOf< + ContractProcedure< + typeof inputSchema, + typeof outputSchema, + MergedErrorMap<{ INVALID: { status: number }, BASE2: { message: string } }, typeof baseErrorMap>, + BaseMeta + > + >() + + expectTypeOf(adapted.pong).toEqualTypeOf< + ContractProcedure< + undefined, + undefined, + MergedErrorMap<{ INVALID: { status: number }, BASE2: { message: string } }, Record>, + Meta + > + >() }) -describe('InferContractRouterInputs', () => { - it('works', () => { - type Inputs = InferContractRouterInputs +it('InferContractRouterInputs', () => { + type Inputs = InferContractRouterInputs - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf<{ input: number }>() + expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() - }) + expectTypeOf().toEqualTypeOf<{ input: number }>() + expectTypeOf().toEqualTypeOf() }) -describe('InferContractRouterOutputs', () => { - it('works', () => { - type Outputs = InferContractRouterOutputs +it('InferContractRouterOutputs', () => { + type Outputs = InferContractRouterOutputs - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf<{ output: string }>() + expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() - }) + expectTypeOf().toEqualTypeOf<{ output: string }>() + expectTypeOf().toEqualTypeOf() }) diff --git a/packages/contract/src/router.test.ts b/packages/contract/src/router.test.ts new file mode 100644 index 00000000..3d452a8d --- /dev/null +++ b/packages/contract/src/router.test.ts @@ -0,0 +1,32 @@ +import { ping, pong } from '../tests/shared' +import { adaptRoute } from './route' +import { adaptContractRouter } from './router' + +const router = { + ping, + pong, + nested: { + ping, + pong, + }, +} + +it('adaptContractRouter', () => { + const errorMap = { + INVALID: { message: 'INVALID' }, + OVERRIDE: { message: 'OVERRIDE' }, + } + const adapted = adaptContractRouter(router, { errorMap, prefix: '/adapt', tags: ['adapt'] }) + + expect(adapted.ping['~orpc'].errorMap).toEqual({ ...errorMap, ...ping['~orpc'].errorMap }) + expect(adapted.ping['~orpc'].route).toEqual(adaptRoute(ping['~orpc'].route, { prefix: '/adapt', tags: ['adapt'] })) + + expect(adapted.pong['~orpc'].errorMap).toEqual({ ...errorMap, ...pong['~orpc'].errorMap }) + expect(adapted.pong['~orpc'].route).toEqual(adaptRoute(pong['~orpc'].route, { prefix: '/adapt', tags: ['adapt'] })) + + expect(adapted.nested.ping['~orpc'].errorMap).toEqual({ ...errorMap, ...ping['~orpc'].errorMap }) + expect(adapted.nested.ping['~orpc'].route).toEqual(adaptRoute(ping['~orpc'].route, { prefix: '/adapt', tags: ['adapt'] })) + + expect(adapted.nested.pong['~orpc'].errorMap).toEqual({ ...errorMap, ...pong['~orpc'].errorMap }) + expect(adapted.nested.pong['~orpc'].route).toEqual(adaptRoute(pong['~orpc'].route, { prefix: '/adapt', tags: ['adapt'] })) +}) diff --git a/packages/contract/src/router.ts b/packages/contract/src/router.ts index bae0da97..cc15d0b1 100644 --- a/packages/contract/src/router.ts +++ b/packages/contract/src/router.ts @@ -1,21 +1,77 @@ -import type { ErrorMap } from './error-map' -import type { ContractProcedure } from './procedure' -import type { SchemaInput, SchemaOutput } from './types' +import type { Meta } from './meta' +import type { SchemaInput, SchemaOutput } from './schema' +import { type ErrorMap, type MergedErrorMap, mergeErrorMap } from './error-map' +import { ContractProcedure, isContractProcedure } from './procedure' +import { adaptRoute, type HTTPPath } from './route' -export type ContractRouter = ContractProcedure | { - [k: string]: ContractRouter +export type ContractRouter = + | ContractProcedure + | { + [k: string]: ContractRouter + } + +export type AnyContractRouter = ContractRouter + +export type AdaptedContractRouter< + TContract extends AnyContractRouter, + TErrorMap extends ErrorMap, +> = { + [K in keyof TContract]: TContract[K] extends + ContractProcedure + ? ContractProcedure, UMeta> + : TContract[K] extends AnyContractRouter + ? AdaptedContractRouter + : never +} + +export interface AdaptContractRouterOptions { + errorMap: TErrorMap + prefix?: HTTPPath + tags?: readonly string[] +} + +export function adaptContractRouter, TErrorMap extends ErrorMap>( + contract: TRouter, + options: AdaptContractRouterOptions, +): AdaptedContractRouter { + if (isContractProcedure(contract)) { + const adapted = new ContractProcedure({ + ...contract['~orpc'], + errorMap: mergeErrorMap(options.errorMap, contract['~orpc'].errorMap), + route: adaptRoute(contract['~orpc'].route, options), + }) + + return adapted as any + } + + const adapted: Record = {} + + for (const key in contract) { + adapted[key] = adaptContractRouter(contract[key]!, options) + } + + return adapted as any } -export type InferContractRouterInputs> = +export type InferContractRouterInputs = T extends ContractProcedure ? SchemaInput : { - [K in keyof T]: T[K] extends ContractRouter ? InferContractRouterInputs : never + [K in keyof T]: T[K] extends AnyContractRouter ? InferContractRouterInputs : never } -export type InferContractRouterOutputs> = +export type InferContractRouterOutputs = T extends ContractProcedure ? SchemaOutput : { - [K in keyof T]: T[K] extends ContractRouter ? InferContractRouterOutputs : never + [K in keyof T]: T[K] extends AnyContractRouter ? InferContractRouterOutputs : never } + +export type ContractRouterToErrorMap = + T extends ContractProcedure ? UErrorMap : + { + [K in keyof T]: T[K] extends AnyContractRouter ? ContractRouterToErrorMap : never + }[keyof T] + +export type ContractRouterToMeta = + T extends ContractRouter ? UMeta : never diff --git a/packages/contract/src/schema-utils.test-d.ts b/packages/contract/src/schema-utils.test-d.ts deleted file mode 100644 index 3804a2ed..00000000 --- a/packages/contract/src/schema-utils.test-d.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { SchemaInput, SchemaOutput } from './types' -import { type } from './schema-utils' - -describe('type', () => { - it('without map', () => { - const schema = type() - - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - }) - - it('with map', () => { - const schema2 = type((val) => { - expectTypeOf(val).toEqualTypeOf() - - return Number(val) - }) - - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - - // @ts-expect-error - map is required when TInput !== TOutput - type() - - // @ts-expect-error - output not match number - type(() => '123') - }) -}) diff --git a/packages/contract/src/schema.test-d.ts b/packages/contract/src/schema.test-d.ts new file mode 100644 index 00000000..93bebbd4 --- /dev/null +++ b/packages/contract/src/schema.test-d.ts @@ -0,0 +1,70 @@ +import { type as arktypeType } from 'arktype' +import * as v from 'valibot' +import { z } from 'zod' +import { type Schema, type SchemaInput, type SchemaOutput, type } from './schema' + +const zod = z.object({ + value: z.string().transform(() => 123), +}) + +const valibot = v.object({ + value: v.pipe(v.string(), v.transform(() => 123)), +}) + +// How convert value into number? +const arktype = arktypeType({ + value: 'string', +}) + +describe('Schema', () => { + it('assignable', () => { + const _undefined: Schema = undefined + const _zod: Schema = zod + const _valibot: Schema = valibot + const _arktype: Schema = arktype + }) +}) + +describe('SchemaInput', () => { + it('inferable', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf<{ value: string }>() + expectTypeOf>().toEqualTypeOf<{ value: string }>() + expectTypeOf>().toEqualTypeOf<{ value: string }>() + }) +}) + +describe('SchemaOutput', () => { + it('inferable', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf<{ value: number }>() + expectTypeOf>().toEqualTypeOf<{ value: number }>() + expectTypeOf>().toEqualTypeOf<{ value: string }>() + }) +}) + +describe('type', () => { + it('without map', () => { + const schema = type() + + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + }) + + it('with map', () => { + const schema2 = type((val) => { + expectTypeOf(val).toEqualTypeOf() + + return Number(val) + }) + + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + + // @ts-expect-error - map is required when TInput !== TOutput + type() + + // @ts-expect-error - output not match number + type(() => '123') + }) +}) diff --git a/packages/contract/src/schema-utils.test.ts b/packages/contract/src/schema.test.ts similarity index 92% rename from packages/contract/src/schema-utils.test.ts rename to packages/contract/src/schema.test.ts index ce8e0583..34c03617 100644 --- a/packages/contract/src/schema-utils.test.ts +++ b/packages/contract/src/schema.test.ts @@ -1,4 +1,4 @@ -import { type } from './schema-utils' +import { type } from './schema' describe('type', async () => { it('without map', async () => { diff --git a/packages/contract/src/schema-utils.ts b/packages/contract/src/schema.ts similarity index 57% rename from packages/contract/src/schema-utils.ts rename to packages/contract/src/schema.ts index 1616ffb8..5b2ad5bb 100644 --- a/packages/contract/src/schema-utils.ts +++ b/packages/contract/src/schema.ts @@ -1,6 +1,26 @@ import type { IsEqual, Promisable } from '@orpc/shared' import type { StandardSchemaV1 } from '@standard-schema/spec' +export type Schema = StandardSchemaV1 | undefined + +export type SchemaInput< + TSchema extends Schema, + TFallback = unknown, +> = TSchema extends undefined + ? TFallback + : TSchema extends StandardSchemaV1 + ? StandardSchemaV1.InferInput + : TFallback + +export type SchemaOutput< + TSchema extends Schema, + TFallback = unknown, +> = TSchema extends undefined + ? TFallback + : TSchema extends StandardSchemaV1 + ? StandardSchemaV1.InferOutput + : TFallback + export type TypeRest = | [map: (input: TInput) => Promisable] | (IsEqual extends true ? [] : never) diff --git a/packages/contract/src/types.test-d.ts b/packages/contract/src/types.test-d.ts index 1f350726..87c10a8e 100644 --- a/packages/contract/src/types.test-d.ts +++ b/packages/contract/src/types.test-d.ts @@ -1,44 +1,8 @@ -import type { Schema, SchemaInput, SchemaOutput } from './types' -import { type } from 'arktype' -import * as v from 'valibot' -import { z } from 'zod' +import type { AbortSignal } from './types' -const zod = z.object({ - value: z.string().transform(() => 123), -}) - -const valibot = v.object({ - value: v.pipe(v.string(), v.transform(() => 123)), -}) - -// How convert value into number? -const arktype = type({ - value: 'string', -}) - -describe('Schema', () => { - it('assignable', () => { - const _undefined: Schema = undefined - const _zod: Schema = zod - const _valibot: Schema = valibot - const _arktype: Schema = arktype - }) -}) - -describe('SchemaInput', () => { - it('inferable', () => { - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf<{ value: string }>() - expectTypeOf>().toEqualTypeOf<{ value: string }>() - expectTypeOf>().toEqualTypeOf<{ value: string }>() - }) -}) +it('AbortSignal', () => { + const controller = new AbortController() + const signal = controller.signal -describe('SchemaOutput', () => { - it('inferable', () => { - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf<{ value: number }>() - expectTypeOf>().toEqualTypeOf<{ value: number }>() - expectTypeOf>().toEqualTypeOf<{ value: string }>() - }) + expectTypeOf().toEqualTypeOf(signal) }) diff --git a/packages/contract/src/types.ts b/packages/contract/src/types.ts index 59425c53..7f8faa13 100644 --- a/packages/contract/src/types.ts +++ b/packages/contract/src/types.ts @@ -1,24 +1,3 @@ import type { FindGlobalInstanceType } from '@orpc/shared' -import type { StandardSchemaV1 } from '@standard-schema/spec' - -export type Schema = StandardSchemaV1 | undefined - -export type SchemaInput< - TSchema extends Schema, - TFallback = unknown, -> = TSchema extends undefined - ? TFallback - : TSchema extends StandardSchemaV1 - ? StandardSchemaV1.InferInput - : TFallback - -export type SchemaOutput< - TSchema extends Schema, - TFallback = unknown, -> = TSchema extends undefined - ? TFallback - : TSchema extends StandardSchemaV1 - ? StandardSchemaV1.InferOutput - : TFallback export type AbortSignal = FindGlobalInstanceType<'AbortSignal'> diff --git a/packages/contract/tests/shared.ts b/packages/contract/tests/shared.ts new file mode 100644 index 00000000..69216a5f --- /dev/null +++ b/packages/contract/tests/shared.ts @@ -0,0 +1,59 @@ +import type { Meta } from '../src/meta' +import { z } from 'zod' +import { ContractProcedure } from '../src' + +export const inputSchema = z.object({ input: z.number().transform(n => `${n}`) }) + +export const outputSchema = z.object({ output: z.number().transform(n => `${n}`) }) + +export const generalSchema = z.object({ general: z.number().transform(n => `${n}`) }) + +export const baseErrorMap = { + BASE: { + data: outputSchema, + }, + OVERRIDE: {}, +} + +export const baseRoute = { path: '/base' } as const + +export type BaseMeta = { mode?: string, log?: boolean } + +export const baseMeta: BaseMeta = { + mode: 'dev', +} + +export const ping = new ContractProcedure< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta +>({ + inputSchema, + outputSchema, + errorMap: baseErrorMap, + meta: baseMeta, + route: baseRoute, +}) + +export const pong = new ContractProcedure< + undefined, + undefined, + Record, + Meta +>({ + errorMap: {}, + inputSchema: undefined, + outputSchema: undefined, + meta: {}, + route: {}, +}) + +export const router = { + ping, + pong, + nested: { + ping, + pong, + }, +} diff --git a/packages/openapi/src/adapters/fetch/openapi-handler.test.ts b/packages/openapi/src/adapters/fetch/openapi-handler.test.ts index 975d93fb..52dbb96b 100644 --- a/packages/openapi/src/adapters/fetch/openapi-handler.test.ts +++ b/packages/openapi/src/adapters/fetch/openapi-handler.test.ts @@ -1,5 +1,4 @@ import type { Router } from 'hono/router' -import { ContractProcedure } from '@orpc/contract' import { createProcedureClient, os, Procedure } from '@orpc/server' import { LinearRouter } from 'hono/router/linear-router' import { PatternRouter } from 'hono/router/pattern-router' @@ -205,16 +204,15 @@ describe.each(hono)('openAPIHandler: %s', (_, HonoConstructor) => { it('custom success status', async () => { const router = { ping: new Procedure({ - contract: new ContractProcedure({ - route: { - method: 'GET', - path: '/ping', - successStatus: 298, - }, - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - }), + meta: {}, + route: { + method: 'GET', + path: '/ping', + successStatus: 298, + }, + inputSchema: undefined, + outputSchema: undefined, + errorMap: {}, handler: vi.fn(), middlewares: [], inputValidationIndex: 0, @@ -234,15 +232,14 @@ describe.each(hono)('openAPIHandler: %s', (_, HonoConstructor) => { it('custom method', async () => { const router = { ping: new Procedure({ - contract: new ContractProcedure({ - route: { - method: 'DELETE', - path: '/ping', - }, - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - }), + meta: {}, + route: { + method: 'DELETE', + path: '/ping', + }, + inputSchema: undefined, + outputSchema: undefined, + errorMap: {}, handler: vi.fn(), middlewares: [], inputValidationIndex: 0, diff --git a/packages/openapi/src/adapters/fetch/openapi-handler.ts b/packages/openapi/src/adapters/fetch/openapi-handler.ts index 60b191a0..66d32c7d 100644 --- a/packages/openapi/src/adapters/fetch/openapi-handler.ts +++ b/packages/openapi/src/adapters/fetch/openapi-handler.ts @@ -1,4 +1,4 @@ -import type { ANY_PROCEDURE, Context, Router, WithSignal } from '@orpc/server' +import type { AnyProcedure, Context, Router } from '@orpc/server' import type { FetchHandler, FetchHandleRest, FetchHandleResult } from '@orpc/server/fetch' import type { Params } from 'hono/router' import type { PublicInputStructureCompact } from './input-structure-compact' @@ -13,7 +13,7 @@ import { type Hono, OpenAPIProcedureMatcher, type PublicOpenAPIProcedureMatcher import { CompositeSchemaCoercer, type SchemaCoercer } from './schema-coercer' export type OpenAPIHandlerOptions = - & Hooks + & Hooks & { jsonSerializer?: PublicJSONSerializer procedureMatcher?: PublicOpenAPIProcedureMatcher @@ -62,11 +62,11 @@ export class OpenAPIHandler implements FetchHandler { return { matched: false, response: undefined } } - const contractDef = matched.procedure['~orpc'].contract['~orpc'] + const def = matched.procedure['~orpc'] const input = await this.decodeInput(matched.procedure, matched.params, request) - const coercedInput = this.compositeSchemaCoercer.coerce(contractDef.InputSchema, input) + const coercedInput = this.compositeSchemaCoercer.coerce(def.inputSchema, input) const client = createProcedureClient(matched.procedure, { context, @@ -79,7 +79,7 @@ export class OpenAPIHandler implements FetchHandler { const response = new Response(body, { headers: resHeaders, - status: fallbackContractConfig('defaultSuccessStatus', contractDef.route?.successStatus), + status: fallbackContractConfig('defaultSuccessStatus', def.route?.successStatus), }) return { matched: true, response } @@ -127,8 +127,8 @@ export class OpenAPIHandler implements FetchHandler { } } - private async decodeInput(procedure: ANY_PROCEDURE, params: Params, request: Request): Promise { - const inputStructure = fallbackContractConfig('defaultInputStructure', procedure['~orpc'].contract['~orpc'].route?.inputStructure) + private async decodeInput(procedure: AnyProcedure, params: Params, request: Request): Promise { + const inputStructure = fallbackContractConfig('defaultInputStructure', procedure['~orpc'].route.inputStructure) const url = new URL(request.url) const query = url.searchParams @@ -151,11 +151,11 @@ export class OpenAPIHandler implements FetchHandler { } private encodeOutput( - procedure: ANY_PROCEDURE, + procedure: AnyProcedure, output: unknown, accept: string | undefined, ): { body: string | Blob | FormData | undefined, headers?: Headers } { - const outputStructure = fallbackContractConfig('defaultOutputStructure', procedure['~orpc'].contract['~orpc'].route?.outputStructure) + const outputStructure = fallbackContractConfig('defaultOutputStructure', procedure['~orpc'].route.outputStructure) if (outputStructure === 'compact') { return this.payloadCodec.encode(output, accept) @@ -218,8 +218,7 @@ export class OpenAPIHandler implements FetchHandler { private convertToORPCError(e: unknown): ORPCError { return e instanceof ORPCError ? e - : new ORPCError({ - code: 'INTERNAL_SERVER_ERROR', + : new ORPCError('INTERNAL_SERVER_ERROR', { message: 'Internal server error', cause: e, }) diff --git a/packages/openapi/src/adapters/fetch/openapi-payload-codec.ts b/packages/openapi/src/adapters/fetch/openapi-payload-codec.ts index 7522c148..69a24197 100644 --- a/packages/openapi/src/adapters/fetch/openapi-payload-codec.ts +++ b/packages/openapi/src/adapters/fetch/openapi-payload-codec.ts @@ -60,8 +60,7 @@ export class OpenAPIPayloadCodec { return this.encodeAsFormData(handledPayload) } - throw new ORPCError({ - code: 'NOT_ACCEPTABLE', + throw new ORPCError('NOT_ACCEPTABLE', { message: `Unsupported content-type: ${accept}`, }) } @@ -202,8 +201,7 @@ export class OpenAPIPayloadCodec { }) } catch (e) { - throw new ORPCError({ - code: 'BAD_REQUEST', + throw new ORPCError('BAD_REQUEST', { message: 'Cannot parse request/response. Please check the request/response body and Content-Type header.', cause: e, }) diff --git a/packages/openapi/src/adapters/fetch/openapi-procedure-matcher.ts b/packages/openapi/src/adapters/fetch/openapi-procedure-matcher.ts index 1750fb91..4b589b8f 100644 --- a/packages/openapi/src/adapters/fetch/openapi-procedure-matcher.ts +++ b/packages/openapi/src/adapters/fetch/openapi-procedure-matcher.ts @@ -1,19 +1,20 @@ +import type { AnyProcedure, AnyRouter } from '@orpc/server' import type { Router as BaseHono, ParamIndexMap, Params } from 'hono/router' import { fallbackContractConfig, type HTTPPath } from '@orpc/contract' -import { type ANY_PROCEDURE, type ANY_ROUTER, getLazyRouterPrefix, getRouterChild, isProcedure, unlazy } from '@orpc/server' +import { getLazyRouterPrefix, getRouterChild, isProcedure, unlazy } from '@orpc/server' import { mapValues } from '@orpc/shared' import { forEachContractProcedure, standardizeHTTPPath } from '../../utils' export type Hono = BaseHono<[string, string[]]> -type PendingRouter = { path: string[], router: ANY_ROUTER } +type PendingRouter = { path: string[], router: AnyRouter } export class OpenAPIProcedureMatcher { private pendingRouters: PendingRouter[] constructor( private readonly hono: Hono, - private readonly router: ANY_ROUTER, + private readonly router: AnyRouter, ) { this.pendingRouters = [{ path: [], router }] } @@ -21,7 +22,7 @@ export class OpenAPIProcedureMatcher { async match( method: string, pathname: string, - ): Promise<{ path: string[], procedure: ANY_PROCEDURE, params: Params } | undefined> { + ): Promise<{ path: string[], procedure: AnyProcedure, params: Params } | undefined> { await this.handlePendingRouters(pathname) const [matches, paramStash] = this.hono.match(method, pathname) @@ -73,7 +74,7 @@ export class OpenAPIProcedureMatcher { } } - private add(path: string[], router: ANY_ROUTER): void { + private add(path: string[], router: AnyRouter): void { const lazies = forEachContractProcedure({ path, router }, ({ path, contract }) => { const method = fallbackContractConfig('defaultMethod', contract['~orpc'].route?.method) const httpPath = contract['~orpc'].route?.path diff --git a/packages/openapi/src/adapters/fetch/schema-coercer.ts b/packages/openapi/src/adapters/fetch/schema-coercer.ts index 0ccb4237..9732d815 100644 --- a/packages/openapi/src/adapters/fetch/schema-coercer.ts +++ b/packages/openapi/src/adapters/fetch/schema-coercer.ts @@ -1,7 +1,7 @@ import type { Schema } from '@orpc/contract' export interface SchemaCoercer { - coerce: (schema: Schema, value: unknown) => unknown + coerce(schema: Schema, value: unknown): unknown } export class CompositeSchemaCoercer implements SchemaCoercer { diff --git a/packages/openapi/src/openapi-generator.ts b/packages/openapi/src/openapi-generator.ts index bcba8348..b13d6ba2 100644 --- a/packages/openapi/src/openapi-generator.ts +++ b/packages/openapi/src/openapi-generator.ts @@ -1,4 +1,4 @@ -import type { ANY_ROUTER } from '@orpc/server' +import type { AnyRouter } from '@orpc/server' import type { PublicOpenAPIInputStructureParser } from './openapi-input-structure-parser' import type { PublicOpenAPIOutputStructureParser } from './openapi-output-structure-parser' import type { PublicOpenAPIPathParser } from './openapi-path-parser' @@ -93,7 +93,7 @@ export class OpenAPIGenerator { this.strictErrorResponses = options?.strictErrorResponses ?? true } - async generate(router: ContractRouter | ANY_ROUTER, doc: Omit): Promise { + async generate(router: ContractRouter | AnyRouter, doc: Omit): Promise { const builder = new OpenApiBuilder({ ...doc, openapi: '3.1.1', @@ -146,20 +146,16 @@ export class OpenAPIGenerator { responses[fallbackContractConfig('defaultSuccessStatus', def.route?.successStatus)] = { description: fallbackContractConfig('defaultSuccessDescription', def.route?.successDescription), content: resBodySchema !== undefined - ? this.contentBuilder.build(resBodySchema, { - example: def.outputExample, - }) + ? this.contentBuilder.build(resBodySchema) : undefined, headers: resHeadersSchema !== undefined - ? this.parametersBuilder.buildHeadersObject(resHeadersSchema, { - example: def.outputExample, - }) + ? this.parametersBuilder.buildHeadersObject(resHeadersSchema) : undefined, } const errors = group(Object.entries(def.errorMap ?? {}) .filter(([_, config]) => config) - .map(([code, config]) => ({ + .map(([code, config]: any) => ({ ...config, code, status: fallbackORPCErrorStatus(code, config?.status), diff --git a/packages/openapi/src/openapi-input-structure-parser.ts b/packages/openapi/src/openapi-input-structure-parser.ts index 44767a95..95d663a0 100644 --- a/packages/openapi/src/openapi-input-structure-parser.ts +++ b/packages/openapi/src/openapi-input-structure-parser.ts @@ -1,8 +1,9 @@ +import type { AnyContractProcedure } from '@orpc/contract' import type { PublicOpenAPIPathParser } from './openapi-path-parser' import type { JSONSchema, ObjectSchema } from './schema' import type { SchemaConverter } from './schema-converter' import type { PublicSchemaUtils } from './schema-utils' -import { type ANY_CONTRACT_PROCEDURE, fallbackContractConfig } from '@orpc/contract' +import { fallbackContractConfig } from '@orpc/contract' import { OpenAPIError } from './openapi-error' export interface OpenAPIInputStructureParseResult { @@ -19,8 +20,8 @@ export class OpenAPIInputStructureParser { private readonly pathParser: PublicOpenAPIPathParser, ) { } - parse(contract: ANY_CONTRACT_PROCEDURE, structure: 'compact' | 'detailed'): OpenAPIInputStructureParseResult { - const inputSchema = this.schemaConverter.convert(contract['~orpc'].InputSchema, { strategy: 'input' }) + parse(contract: AnyContractProcedure, structure: 'compact' | 'detailed'): OpenAPIInputStructureParseResult { + const inputSchema = this.schemaConverter.convert(contract['~orpc'].inputSchema, { strategy: 'input' }) const method = fallbackContractConfig('defaultMethod', contract['~orpc'].route?.method) const httpPath = contract['~orpc'].route?.path diff --git a/packages/openapi/src/openapi-output-structure-parser.ts b/packages/openapi/src/openapi-output-structure-parser.ts index a92abe91..7f1a9a36 100644 --- a/packages/openapi/src/openapi-output-structure-parser.ts +++ b/packages/openapi/src/openapi-output-structure-parser.ts @@ -1,4 +1,4 @@ -import type { ANY_CONTRACT_PROCEDURE } from '@orpc/contract' +import type { AnyContractProcedure } from '@orpc/contract' import type { JSONSchema, ObjectSchema } from './schema' import type { SchemaConverter } from './schema-converter' import type { PublicSchemaUtils } from './schema-utils' @@ -15,8 +15,8 @@ export class OpenAPIOutputStructureParser { private readonly schemaUtils: PublicSchemaUtils, ) { } - parse(contract: ANY_CONTRACT_PROCEDURE, structure: 'compact' | 'detailed'): OpenAPIOutputStructureParseResult { - const outputSchema = this.schemaConverter.convert(contract['~orpc'].OutputSchema, { strategy: 'output' }) + parse(contract: AnyContractProcedure, structure: 'compact' | 'detailed'): OpenAPIOutputStructureParseResult { + const outputSchema = this.schemaConverter.convert(contract['~orpc'].outputSchema, { strategy: 'output' }) // TODO: refactor and remove this logic if (this.schemaUtils.isAnySchema(outputSchema)) { diff --git a/packages/openapi/src/schema-converter.ts b/packages/openapi/src/schema-converter.ts index 9b21dbec..e78f379d 100644 --- a/packages/openapi/src/schema-converter.ts +++ b/packages/openapi/src/schema-converter.ts @@ -6,9 +6,9 @@ export interface SchemaConvertOptions { } export interface SchemaConverter { - condition: (schema: Schema, options: SchemaConvertOptions) => boolean + condition(schema: Schema, options: SchemaConvertOptions): boolean - convert: (schema: Schema, options: SchemaConvertOptions) => JSONSchema.JSONSchema + convert(schema: Schema, options: SchemaConvertOptions): JSONSchema.JSONSchema } export class CompositeSchemaConverter implements SchemaConverter { diff --git a/packages/openapi/src/utils.ts b/packages/openapi/src/utils.ts index 686f0ac4..f6840479 100644 --- a/packages/openapi/src/utils.ts +++ b/packages/openapi/src/utils.ts @@ -1,20 +1,20 @@ -import type { ContractRouter, HTTPPath, WELL_CONTRACT_PROCEDURE } from '@orpc/contract' -import type { ANY_PROCEDURE, ANY_ROUTER, Lazy } from '@orpc/server' +import type { AnyContractProcedure, AnyContractRouter, HTTPPath } from '@orpc/contract' +import type { AnyProcedure, AnyRouter, Lazy } from '@orpc/server' import { isContractProcedure } from '@orpc/contract' -import { isLazy, isProcedure, unlazy } from '@orpc/server' +import { getRouterContract, isLazy, unlazy } from '@orpc/server' export interface EachLeafOptions { - router: ContractRouter | ANY_ROUTER + router: AnyContractRouter | AnyRouter path: string[] } export interface EachLeafCallbackOptions { - contract: WELL_CONTRACT_PROCEDURE + contract: AnyContractProcedure path: string[] } export interface EachContractLeafResultItem { - router: Lazy | Lazy | ANY_PROCEDURE> + router: Lazy | Lazy | AnyProcedure> path: string[] } @@ -22,7 +22,22 @@ export function forEachContractProcedure( options: EachLeafOptions, callback: (options: EachLeafCallbackOptions) => void, result: EachContractLeafResultItem[] = [], + isCurrentRouterContract = false, ): EachContractLeafResultItem[] { + const hiddenContract = getRouterContract(options.router) + + if (!isCurrentRouterContract && hiddenContract) { + return forEachContractProcedure( + { + path: options.path, + router: hiddenContract, + }, + callback, + result, + true, + ) + } + if (isLazy(options.router)) { result.push({ router: options.router, @@ -30,14 +45,6 @@ export function forEachContractProcedure( }) } - // - else if (isProcedure(options.router)) { - callback({ - contract: options.router['~orpc'].contract, - path: options.path, - }) - } - // else if (isContractProcedure(options.router)) { callback({ @@ -64,7 +71,7 @@ export function forEachContractProcedure( } export async function forEachAllContractProcedure( - router: ContractRouter | ANY_ROUTER, + router: AnyContractRouter | AnyRouter, callback: (options: EachLeafCallbackOptions) => void, ) { const pending: EachLeafOptions[] = [{ diff --git a/packages/react-query/src/types.ts b/packages/react-query/src/types.ts index 86328f0a..201190e2 100644 --- a/packages/react-query/src/types.ts +++ b/packages/react-query/src/types.ts @@ -11,8 +11,8 @@ export type InferCursor = T extends { cursor?: any } ? T['cursor'] : never export interface QueryOptionsBase { queryKey: QueryKey - queryFn: (ctx: QueryFunctionContext) => Promise - retry?: (failureCount: number, error: TError) => boolean // this make tanstack can infer the TError type + queryFn(ctx: QueryFunctionContext): Promise + retry?(failureCount: number, error: TError): boolean // this make tanstack can infer the TError type } export type QueryOptionsExtra = @@ -24,8 +24,8 @@ export type QueryOptionsExtra { queryKey: QueryKey - queryFn: (ctx: QueryFunctionContext>) => Promise - retry?: (failureCount: number, error: TError) => boolean // this make tanstack can infer the TError type + queryFn(ctx: QueryFunctionContext>): Promise + retry?(failureCount: number, error: TError): boolean // this make tanstack can infer the TError type initialPageParam: undefined } @@ -41,8 +41,8 @@ export type InfiniteOptionsExtra { mutationKey: QueryKey - mutationFn: (input: TInput) => Promise - retry?: (failureCount: number, error: TError) => boolean // this make tanstack can infer the TError type + mutationFn(input: TInput): Promise + retry?(failureCount: number, error: TError): boolean // this make tanstack can infer the TError type } export type MutationOptionsExtra = diff --git a/packages/react-query/src/utils-general.ts b/packages/react-query/src/utils-general.ts index 934347de..c8c7b5e5 100644 --- a/packages/react-query/src/utils-general.ts +++ b/packages/react-query/src/utils-general.ts @@ -6,7 +6,7 @@ import { buildKey } from './key' * Utils at any level (procedure or router) */ export interface GeneralUtils { - key: (options?: BuildKeyOptions) => QueryKey + key(options?: BuildKeyOptions): QueryKey } export function createGeneralUtils( diff --git a/packages/react-query/src/utils-procedure.ts b/packages/react-query/src/utils-procedure.ts index 9acf1ec8..6328a776 100644 --- a/packages/react-query/src/utils-procedure.ts +++ b/packages/react-query/src/utils-procedure.ts @@ -7,19 +7,19 @@ import { buildKey } from './key' * Utils at procedure level */ export interface ProcedureUtils { - queryOptions: >( + queryOptions>( ...opts: [options: U] | (undefined extends TInput & TClientContext ? [] : never) - ) => IsEqual> extends true + ): IsEqual> extends true ? QueryOptionsBase : Omit, keyof U> & U - infiniteOptions: >( + infiniteOptions>( options: U - ) => Omit, keyof U> & U + ): Omit, keyof U> & U - mutationOptions: >( + mutationOptions>( ...opt: [options: U] | (undefined extends TClientContext ? [] : never) - ) => IsEqual> extends true + ): IsEqual> extends true ? MutationOptionsBase : Omit, keyof U> & U } diff --git a/packages/react-query/src/utils-router.test-d.ts b/packages/react-query/src/utils-router.test-d.ts index 7fdf9cce..b0014ffa 100644 --- a/packages/react-query/src/utils-router.test-d.ts +++ b/packages/react-query/src/utils-router.test-d.ts @@ -3,7 +3,7 @@ import type { RouterClient } from '@orpc/server' import type { GeneralUtils } from './utils-general' import type { ProcedureUtils } from './utils-procedure' import { oc } from '@orpc/contract' -import { os } from '@orpc/server' +import { implement, os } from '@orpc/server' import { z } from 'zod' import { createGeneralUtils } from './utils-general' import { createProcedureUtils } from './utils-procedure' @@ -16,10 +16,10 @@ const contractRouter = oc.router({ pong: pongContract, }) -const ping = os.contract(pingContract).handler(({ input }) => `ping ${input.name}`).callable() -const pong = os.contract(pongContract).handler(num => `pong ${num}`).callable() +const ping = implement(pingContract).handler(({ input }) => `ping ${input.name}`).callable() +const pong = implement(pongContract).handler(num => `pong ${num}`).callable() -const router = os.contract(contractRouter).router({ +const router = implement(contractRouter).router({ ping, pong: os.lazy(() => Promise.resolve({ default: pong })), }) diff --git a/packages/server/src/adapters/fetch/orpc-handler.test-d.ts b/packages/server/src/adapters/fetch/orpc-handler.test-d.ts index e7f6fdc8..b659ad40 100644 --- a/packages/server/src/adapters/fetch/orpc-handler.test-d.ts +++ b/packages/server/src/adapters/fetch/orpc-handler.test-d.ts @@ -1,4 +1,3 @@ -import type { WithSignal } from '../..' import type { FetchHandleResult } from './types' import { os } from '../..' import { RPCHandler } from './orpc-handler' @@ -6,7 +5,7 @@ import { RPCHandler } from './orpc-handler' describe('rpcHandler', () => { it('hooks', () => { const router = { - ping: os.context<{ userId?: string }>().handler(() => 'pong'), + ping: os.$context<{ userId?: string }>().handler(() => 'pong'), } const handler = new RPCHandler(router, { @@ -14,7 +13,6 @@ describe('rpcHandler', () => { expectTypeOf(state.input).toEqualTypeOf() expectTypeOf(state.output).toEqualTypeOf() expectTypeOf(context).toEqualTypeOf<{ userId?: string }>() - expectTypeOf(meta).toEqualTypeOf() }, }) }) diff --git a/packages/server/src/adapters/fetch/orpc-handler.test.ts b/packages/server/src/adapters/fetch/orpc-handler.test.ts index 8a81566f..21d78633 100644 --- a/packages/server/src/adapters/fetch/orpc-handler.test.ts +++ b/packages/server/src/adapters/fetch/orpc-handler.test.ts @@ -1,4 +1,3 @@ -import { ContractProcedure } from '@orpc/contract' import { describe, expect, it, vi } from 'vitest' import { lazy } from '../../lazy' import { Procedure } from '../../procedure' @@ -11,24 +10,22 @@ vi.mock('../../procedure-client', () => ({ describe('rpcHandler', () => { const ping = new Procedure({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - route: {}, - }), + inputSchema: undefined, + outputSchema: undefined, + errorMap: {}, + route: {}, + meta: {}, handler: vi.fn(), middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0, }) const pong = new Procedure({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - route: {}, - }), + inputSchema: undefined, + outputSchema: undefined, + errorMap: {}, + route: {}, + meta: {}, handler: vi.fn(), middlewares: [], inputValidationIndex: 0, diff --git a/packages/server/src/adapters/fetch/orpc-handler.ts b/packages/server/src/adapters/fetch/orpc-handler.ts index fab695c7..5ef536d9 100644 --- a/packages/server/src/adapters/fetch/orpc-handler.ts +++ b/packages/server/src/adapters/fetch/orpc-handler.ts @@ -68,8 +68,7 @@ export class RPCHandler implements FetchHandler { catch (e) { const error = e instanceof ORPCError ? e - : new ORPCError({ - code: 'INTERNAL_SERVER_ERROR', + : new ORPCError('INTERNAL_SERVER_ERROR', { message: 'Internal server error', cause: e, }) diff --git a/packages/server/src/adapters/fetch/orpc-payload-codec.ts b/packages/server/src/adapters/fetch/orpc-payload-codec.ts index 3493a347..d55568fd 100644 --- a/packages/server/src/adapters/fetch/orpc-payload-codec.ts +++ b/packages/server/src/adapters/fetch/orpc-payload-codec.ts @@ -103,11 +103,10 @@ export class ORPCPayloadCodec { const json = await re.json() - return SuperJSON.deserialize(json) + return SuperJSON.deserialize(json as any) } catch (e) { - throw new ORPCError({ - code: 'BAD_REQUEST', + throw new ORPCError('BAD_REQUEST', { message: 'Cannot parse request/response. Please check the request/response body and Content-Type header.', cause: e, }) diff --git a/packages/server/src/adapters/fetch/orpc-procedure-matcher.ts b/packages/server/src/adapters/fetch/orpc-procedure-matcher.ts index 41d7d643..d85596e3 100644 --- a/packages/server/src/adapters/fetch/orpc-procedure-matcher.ts +++ b/packages/server/src/adapters/fetch/orpc-procedure-matcher.ts @@ -1,15 +1,16 @@ -import type { ANY_PROCEDURE } from '../../procedure' +import type { AnyProcedure } from '../../procedure' +import type { AnyRouter } from '../../router' import { trim } from '@orpc/shared' import { unlazy } from '../../lazy' import { isProcedure } from '../../procedure' -import { type ANY_ROUTER, getRouterChild } from '../../router' +import { getRouterChild } from '../../router' export class ORPCProcedureMatcher { constructor( - private readonly router: ANY_ROUTER, + private readonly router: AnyRouter, ) { } - async match(pathname: string): Promise<{ path: string[], procedure: ANY_PROCEDURE } | undefined> { + async match(pathname: string): Promise<{ path: string[], procedure: AnyProcedure } | undefined> { const path = trim(pathname, '/').split('/').map(decodeURIComponent) const match = getRouterChild(this.router, ...path) diff --git a/packages/server/src/adapters/fetch/types.ts b/packages/server/src/adapters/fetch/types.ts index cb82c3bb..f74c7d83 100644 --- a/packages/server/src/adapters/fetch/types.ts +++ b/packages/server/src/adapters/fetch/types.ts @@ -12,5 +12,5 @@ export type FetchHandleRest = export type FetchHandleResult = { matched: true, response: Response } | { matched: false, response: undefined } export interface FetchHandler { - handle: (request: Request, ...rest: FetchHandleRest) => Promise + handle(request: Request, ...rest: FetchHandleRest): Promise } diff --git a/packages/server/src/adapters/next/serve.ts b/packages/server/src/adapters/next/serve.ts index 32d97d7d..b00564a2 100644 --- a/packages/server/src/adapters/next/serve.ts +++ b/packages/server/src/adapters/next/serve.ts @@ -12,11 +12,11 @@ export type ServeRest = | (Record extends T ? [] : never) export interface ServeResult { - GET: (req: NextRequest) => Promise - POST: (req: NextRequest) => Promise - PUT: (req: NextRequest) => Promise - PATCH: (req: NextRequest) => Promise - DELETE: (req: NextRequest) => Promise + GET(req: NextRequest): Promise + POST(req: NextRequest): Promise + PUT(req: NextRequest): Promise + PATCH(req: NextRequest): Promise + DELETE(req: NextRequest): Promise } export function serve(handler: FetchHandler, ...[options]: ServeRest): ServeResult { diff --git a/packages/server/src/adapters/node/types.ts b/packages/server/src/adapters/node/types.ts index 20dcf076..81c029f3 100644 --- a/packages/server/src/adapters/node/types.ts +++ b/packages/server/src/adapters/node/types.ts @@ -6,7 +6,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http' import type { Context } from '../../context' export type RequestHandleOptions = - & { prefix?: HTTPPath, beforeSend?: (response: Response, context: T) => Promisable } + & { prefix?: HTTPPath, beforeSend?(response: Response, context: T): Promisable } & (Record extends T ? { context?: T } : { context: T }) export type RequestHandleRest = @@ -16,5 +16,5 @@ export type RequestHandleRest = export type RequestHandleResult = { matched: true } | { matched: false } export interface RequestHandler { - handle: (req: IncomingMessage, res: ServerResponse, ...rest: RequestHandleRest) => Promise + handle(req: IncomingMessage, res: ServerResponse, ...rest: RequestHandleRest): Promise } diff --git a/packages/server/src/builder-variants.test-d.ts b/packages/server/src/builder-variants.test-d.ts new file mode 100644 index 00000000..5247d137 --- /dev/null +++ b/packages/server/src/builder-variants.test-d.ts @@ -0,0 +1,1047 @@ +import type { ContractProcedure, ErrorMap, MergedErrorMap, ORPCErrorConstructorMap, Schema } from '@orpc/contract' +import type { OmitChainMethodDeep } from '@orpc/shared' +import type { Builder } from './builder' +import type { BuilderWithMiddlewares, ProcedureBuilder, ProcedureBuilderWithInput, ProcedureBuilderWithInputOutput, ProcedureBuilderWithOutput, RouterBuilder } from './builder-variants' +import type { Context } from './context' +import type { Lazy } from './lazy' +import type { MiddlewareOutputFn } from './middleware' +import type { Procedure } from './procedure' +import type { DecoratedProcedure } from './procedure-decorated' +import type { AdaptedRouter } from './router' +import { type baseErrorMap, type BaseMeta, generalSchema, type inputSchema, type outputSchema } from '../../contract/tests/shared' +import { type CurrentContext, type InitialContext, router } from '../tests/shared' + +const generalBuilder = {} as Builder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta +> + +describe('BuilderWithMiddlewares', () => { + const builder = {} as BuilderWithMiddlewares< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + + it('backward compatibility', () => { + const expected = {} as OmitChainMethodDeep< + typeof generalBuilder, + '$config' | '$context' | '$meta' | '$route' | 'middleware' + > + + // expectTypeOf(builder).toMatchTypeOf(expected) + expectTypeOf().toEqualTypeOf() + }) + + it('is a contract procedure', () => { + expectTypeOf(builder).toMatchTypeOf< + ContractProcedure< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + }) + + it('.errors', () => { + expectTypeOf(builder.errors({ INVALID: { message: 'invalid' }, OVERRIDE: { message: 'override' } })).toEqualTypeOf< + BuilderWithMiddlewares< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + MergedErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.errors({ TOO_MANY_REQUESTS: { data: {} } }) + }) + + it('.use', () => { + const applied = builder.use(({ context, next, path, procedure, errors, signal }, input, output) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }) + + expectTypeOf(applied).toEqualTypeOf< + BuilderWithMiddlewares< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error --- conflict context + builder.use(({ next }) => next({ context: { db: 123 } })) + // conflict but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf() + // @ts-expect-error --- input is not match + builder.use(({ next }, input: 'invalid') => next({})) + // @ts-expect-error --- output is not match + builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + }) + + it('.meta', () => { + expectTypeOf(builder.meta({ log: true })).toEqualTypeOf< + BuilderWithMiddlewares< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid meta + builder.meta({ meta: 'INVALID' }) + }) + + it('.route', () => { + expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf< + ProcedureBuilder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid method + builder.route({ method: 'INVALID' }) + }) + + it('.input', () => { + expectTypeOf(builder.input(generalSchema)).toEqualTypeOf< + ProcedureBuilderWithInput< + InitialContext, + CurrentContext, + typeof generalSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.input({}) + }) + + it('.output', () => { + expectTypeOf(builder.output(generalSchema)).toEqualTypeOf< + ProcedureBuilderWithOutput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof generalSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.output({}) + }) + + it('.handler', () => { + const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(signal).toEqualTypeOf>() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return { output: 456 } + }) + + expectTypeOf(procedure).toMatchTypeOf< + DecoratedProcedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + >() + }) +}) + +describe('ProcedureBuilder', () => { + const builder = {} as ProcedureBuilder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + + it('backward compatibility', () => { + const expected = {} as OmitChainMethodDeep< + typeof generalBuilder, + '$config' | '$context' | '$meta' | '$route' | 'middleware' | 'prefix' | 'tag' | 'router' | 'lazy' + > + + expectTypeOf(builder).toMatchTypeOf(expected) + expectTypeOf().toEqualTypeOf() + }) + + it('is a contract procedure', () => { + expectTypeOf(builder).toMatchTypeOf< + ContractProcedure< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + }) + + it('.errors', () => { + expectTypeOf(builder.errors({ INVALID: { message: 'invalid' }, OVERRIDE: { message: 'override' } })).toEqualTypeOf< + ProcedureBuilder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + MergedErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.errors({ TOO_MANY_REQUESTS: { data: {} } }) + }) + + it('.use', () => { + const applied = builder.use(({ context, next, path, procedure, errors, signal }, input, output) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }) + + expectTypeOf(applied).toEqualTypeOf< + ProcedureBuilder< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error --- conflict context + builder.use(({ next }) => next({ context: { db: 123 } })) + // conflict but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf() + // @ts-expect-error --- input is not match + builder.use(({ next }, input: 'invalid') => next({})) + // @ts-expect-error --- output is not match + builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + }) + + it('.meta', () => { + expectTypeOf(builder.meta({ log: true })).toEqualTypeOf< + ProcedureBuilder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid meta + builder.meta({ meta: 'INVALID' }) + }) + + it('.route', () => { + expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf< + ProcedureBuilder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid method + builder.route({ method: 'INVALID' }) + }) + + it('.input', () => { + expectTypeOf(builder.input(generalSchema)).toEqualTypeOf< + ProcedureBuilderWithInput< + InitialContext, + CurrentContext, + typeof generalSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.input({}) + }) + + it('.output', () => { + expectTypeOf(builder.output(generalSchema)).toEqualTypeOf< + ProcedureBuilderWithOutput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof generalSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.output({}) + }) + + it('.handler', () => { + const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(signal).toEqualTypeOf>() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return { output: 456 } + }) + + expectTypeOf(procedure).toMatchTypeOf< + DecoratedProcedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + >() + }) +}) + +describe('ProcedureBuilderWithInput', () => { + const builder = {} as ProcedureBuilderWithInput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + + it('backward compatibility', () => { + const expected = {} as OmitChainMethodDeep< + typeof generalBuilder, + '$config' | '$context' | '$meta' | '$route' | 'middleware' | 'prefix' | 'tag' | 'router' | 'lazy' | 'input' + > + + expectTypeOf(builder).toMatchTypeOf(expected) + expectTypeOf().toEqualTypeOf() + }) + + it('is a contract procedure', () => { + expectTypeOf(builder).toMatchTypeOf< + ContractProcedure< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + }) + + it('.errors', () => { + expectTypeOf(builder.errors({ INVALID: { message: 'invalid' }, OVERRIDE: { message: 'override' } })).toEqualTypeOf< + ProcedureBuilderWithInput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + MergedErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.errors({ TOO_MANY_REQUESTS: { data: {} } }) + }) + + describe('.use', () => { + it('without map input', () => { + const applied = builder.use(({ context, next, path, procedure, errors, signal }, input, output) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }) + + expectTypeOf(applied).toEqualTypeOf< + ProcedureBuilderWithInput< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error --- conflict context + builder.use(({ next }) => next({ context: { db: 123 } })) + // conflict but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf() + // @ts-expect-error --- input is not match + builder.use(({ next }, input: 'invalid') => next({})) + // @ts-expect-error --- output is not match + builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + }) + + it('with map input', () => { + const applied = builder.use(({ context, next, path, procedure, errors, signal }, input: { mapped: boolean }, output) => { + expectTypeOf(input).toEqualTypeOf<{ mapped: boolean }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }, (input) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + + return { mapped: true } + }) + + expectTypeOf(applied).toEqualTypeOf< + ProcedureBuilderWithInput< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + builder.use( + ({ context, next, path, procedure, errors, signal }, input: { mapped: boolean }, output) => next(), + // @ts-expect-error --- invalid map input + input => ({ invalid: true }), + ) + + // @ts-expect-error --- conflict context + builder.use(({ next }) => next({ context: { db: 123 } }), input => ({ mapped: true })) + // conflict but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }), input => ({ mapped: true }))).toMatchTypeOf() + // @ts-expect-error --- input is not match + builder.use(({ next }, input: 'invalid') => next({}), input => ({ mapped: true })) + // @ts-expect-error --- output is not match + builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}), input => ({ mapped: true })) + }) + }) + + it('.meta', () => { + expectTypeOf(builder.meta({ log: true })).toEqualTypeOf< + ProcedureBuilderWithInput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid meta + builder.meta({ meta: 'INVALID' }) + }) + + it('.route', () => { + expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf< + ProcedureBuilderWithInput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid method + builder.route({ method: 'INVALID' }) + }) + + it('.output', () => { + expectTypeOf(builder.output(generalSchema)).toEqualTypeOf< + ProcedureBuilderWithInputOutput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof generalSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.output({}) + }) + + it('.handler', () => { + const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(signal).toEqualTypeOf>() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return { output: 456 } + }) + + expectTypeOf(procedure).toMatchTypeOf< + DecoratedProcedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + >() + }) +}) + +describe('ProcedureBuilderWithOutput', () => { + const builder = {} as ProcedureBuilderWithOutput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + + it('backward compatibility', () => { + const expected = {} as OmitChainMethodDeep< + typeof generalBuilder, + '$config' | '$context' | '$meta' | '$route' | 'middleware' | 'prefix' | 'tag' | 'router' | 'lazy' | 'output' + > + + expectTypeOf(builder).toMatchTypeOf(expected) + expectTypeOf().toEqualTypeOf() + }) + + it('is a contract procedure', () => { + expectTypeOf(builder).toMatchTypeOf< + ContractProcedure< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + }) + + it('.errors', () => { + expectTypeOf(builder.errors({ INVALID: { message: 'invalid' }, OVERRIDE: { message: 'override' } })).toEqualTypeOf< + ProcedureBuilderWithOutput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + MergedErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.errors({ TOO_MANY_REQUESTS: { data: {} } }) + }) + + it('.use', () => { + const applied = builder.use(({ context, next, path, procedure, errors, signal }, input, output) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }) + + expectTypeOf(applied).toEqualTypeOf< + ProcedureBuilderWithOutput< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error --- conflict context + builder.use(({ next }) => next({ context: { db: 123 } })) + // conflict but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf() + // @ts-expect-error --- input is not match + builder.use(({ next }, input: 'invalid') => next({})) + // @ts-expect-error --- output is not match + builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + }) + + it('.meta', () => { + expectTypeOf(builder.meta({ log: true })).toEqualTypeOf< + ProcedureBuilderWithOutput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid meta + builder.meta({ meta: 'INVALID' }) + }) + + it('.route', () => { + expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf< + ProcedureBuilderWithOutput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid method + builder.route({ method: 'INVALID' }) + }) + + it('.input', () => { + expectTypeOf(builder.input(generalSchema)).toEqualTypeOf< + ProcedureBuilderWithInputOutput< + InitialContext, + CurrentContext, + typeof generalSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.input({}) + }) + + it('.handler', () => { + const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(signal).toEqualTypeOf>() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return { output: 456 } + }) + + expectTypeOf(procedure).toMatchTypeOf< + DecoratedProcedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + >() + }) +}) + +describe('ProcedureBuilderWithInputOutput', () => { + const builder = {} as ProcedureBuilderWithInputOutput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + + it('backward compatibility', () => { + const expected = {} as OmitChainMethodDeep< + typeof generalBuilder, + '$config' | '$context' | '$meta' | '$route' | 'middleware' | 'prefix' | 'tag' | 'router' | 'lazy' | 'input' | 'output' + > + + expectTypeOf(builder).toMatchTypeOf(expected) + expectTypeOf().toEqualTypeOf() + }) + + it('is a contract procedure', () => { + expectTypeOf(builder).toMatchTypeOf< + ContractProcedure< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + }) + + it('.errors', () => { + expectTypeOf(builder.errors({ INVALID: { message: 'invalid' }, OVERRIDE: { message: 'override' } })).toEqualTypeOf< + ProcedureBuilderWithInputOutput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + MergedErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - schema is invalid + builder.errors({ TOO_MANY_REQUESTS: { data: {} } }) + }) + + describe('.use', () => { + it('without map input', () => { + const applied = builder.use(({ context, next, path, procedure, errors, signal }, input, output) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }) + + expectTypeOf(applied).toEqualTypeOf< + ProcedureBuilderWithInputOutput< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error --- conflict context + builder.use(({ next }) => next({ context: { db: 123 } })) + // conflict but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf() + // @ts-expect-error --- input is not match + builder.use(({ next }, input: 'invalid') => next({})) + // @ts-expect-error --- output is not match + builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + }) + + it('with map input', () => { + const applied = builder.use(({ context, next, path, procedure, errors, signal }, input: { mapped: boolean }, output) => { + expectTypeOf(input).toEqualTypeOf<{ mapped: boolean }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }, (input) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + + return { mapped: true } + }) + + expectTypeOf(applied).toEqualTypeOf< + ProcedureBuilderWithInputOutput< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + builder.use( + ({ context, next, path, procedure, errors, signal }, input: { mapped: boolean }, output) => next(), + // @ts-expect-error --- invalid map input + input => ({ invalid: true }), + ) + + // @ts-expect-error --- conflict context + builder.use(({ next }) => next({ context: { db: 123 } }), input => ({ mapped: true })) + // conflict but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }), input => ({ mapped: true }))).toMatchTypeOf() + // @ts-expect-error --- input is not match + builder.use(({ next }, input: 'invalid') => next({}), input => ({ mapped: true })) + // @ts-expect-error --- output is not match + builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}), input => ({ mapped: true })) + }) + }) + + it('.meta', () => { + expectTypeOf(builder.meta({ log: true })).toEqualTypeOf< + ProcedureBuilderWithInputOutput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid meta + builder.meta({ meta: 'INVALID' }) + }) + + it('.route', () => { + expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf< + ProcedureBuilderWithInputOutput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid method + builder.route({ method: 'INVALID' }) + }) + + it('.handler', () => { + const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(signal).toEqualTypeOf>() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return { output: 456 } + }) + + expectTypeOf(procedure).toMatchTypeOf< + DecoratedProcedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + >() + }) +}) + +describe('RouterBuilder', () => { + const builder = {} as RouterBuilder< + InitialContext, + CurrentContext, + typeof baseErrorMap, + BaseMeta + > + + it('backward compatibility', () => { + const expected = {} as OmitChainMethodDeep< + typeof generalBuilder, + '$config' | '$context' | '$meta' | '$route' | 'middleware' | 'meta' | 'route' | 'input' | 'output' | 'handler' + > + + // expectTypeOf(builder).toMatchTypeOf(expected) + expectTypeOf().toEqualTypeOf() + }) + + it('.prefix', () => { + expectTypeOf(builder.prefix('/test')).toEqualTypeOf< + RouterBuilder + >() + + // @ts-expect-error - invalid prefix + builder.prefix(123) + }) + + it('.tag', () => { + expectTypeOf(builder.tag('test', 'test2')).toEqualTypeOf< + RouterBuilder + >() + }) + + it('.router', () => { + expectTypeOf(builder.router(router)).toEqualTypeOf< + AdaptedRouter + >() + + builder.router({ + // @ts-expect-error - initial context is not match + ping: {} as Procedure<{ invalid: true }, Context, undefined, undefined, unknown, Record, BaseMeta>, + }) + + builder.router({ + // @ts-expect-error - meta def is not match + ping: {} as Procedure< + Context, + Context, + undefined, + undefined, + unknown, + Record, + { invalid: true } + >, + }) + }) + + it('.lazy', () => { + expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf< + AdaptedRouter, InitialContext, typeof baseErrorMap> + >() + + // @ts-expect-error - initial context is not match + builder.lazy(() => Promise.resolve({ + default: { + ping: {} as Procedure<{ invalid: true }, Context, undefined, undefined, unknown, Record, BaseMeta>, + }, + })) + + // @ts-expect-error - meta def is not match + builder.lazy(() => Promise.resolve({ + default: { + ping: {} as Procedure< + Context, + Context, + undefined, + undefined, + unknown, + Record, + { invalid: true } + >, + }, + })) + }) +}) diff --git a/packages/server/src/builder-variants.ts b/packages/server/src/builder-variants.ts new file mode 100644 index 00000000..3a151cf5 --- /dev/null +++ b/packages/server/src/builder-variants.ts @@ -0,0 +1,302 @@ +import type { ContractRouter, ErrorMap, HTTPPath, MergedErrorMap, Meta, ORPCErrorConstructorMap, Route, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { BuilderDef } from './builder' +import type { ConflictContextGuard, Context, MergedContext } from './context' +import type { FlattenLazy } from './lazy-utils' +import type { MapInputMiddleware, Middleware } from './middleware' +import type { ProcedureHandler } from './procedure' +import type { DecoratedProcedure } from './procedure-decorated' +import type { AdaptedRouter, AdaptRouterOptions, Router } from './router' + +export interface BuilderWithMiddlewares< + TInitialContext extends Context, + TCurrentContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> { + '~orpc': BuilderDef + + 'errors'( + errors: U, + ): BuilderWithMiddlewares, TMeta> + + 'use'( + middleware: Middleware< + TCurrentContext, + UOutContext, + SchemaOutput, + SchemaInput, + ORPCErrorConstructorMap, + TMeta + >, + ): ConflictContextGuard> & + BuilderWithMiddlewares, TInputSchema, TOutputSchema, TErrorMap, TMeta> + + 'meta'( + meta: TMeta, + ): BuilderWithMiddlewares + + 'route'( + route: Route, + ): ProcedureBuilder + + 'input'( + schema: USchema, + ): ProcedureBuilderWithInput + + 'output'( + schema: USchema, + ): ProcedureBuilderWithOutput + + 'handler'>( + handler: ProcedureHandler, + ): DecoratedProcedure + + 'prefix'(prefix: HTTPPath): RouterBuilder + + 'tag'(...tags: string[]): RouterBuilder + + 'router'>>( + router: U + ): AdaptedRouter + + 'lazy'>>( + loader: () => Promise<{ default: U }>, + ): AdaptedRouter, TInitialContext, TErrorMap> +} + +export interface ProcedureBuilder< + TInitialContext extends Context, + TCurrentContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> { + '~orpc': BuilderDef + + 'errors'( + errors: U, + ): ProcedureBuilder, TMeta> + + 'use'( + middleware: Middleware< + TCurrentContext, + UOutContext, + SchemaOutput, + SchemaInput, + ORPCErrorConstructorMap, + TMeta + >, + ): ConflictContextGuard> & + ProcedureBuilder, TInputSchema, TOutputSchema, TErrorMap, TMeta> + + 'meta'( + meta: TMeta, + ): ProcedureBuilder + + 'route'( + route: Route, + ): ProcedureBuilder + + 'input'( + schema: USchema, + ): ProcedureBuilderWithInput + + 'output'( + schema: USchema, + ): ProcedureBuilderWithOutput + + 'handler'>( + handler: ProcedureHandler, + ): DecoratedProcedure +} + +export interface ProcedureBuilderWithInput< + TInitialContext extends Context, + TCurrentContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> { + '~orpc': BuilderDef + + 'errors'( + errors: U, + ): ProcedureBuilderWithInput, TMeta> + + 'use'( + middleware: Middleware< + TCurrentContext, + UOutContext, + SchemaOutput, + SchemaInput, + ORPCErrorConstructorMap, + TMeta + >, + ): ConflictContextGuard> & + ProcedureBuilderWithInput, TInputSchema, TOutputSchema, TErrorMap, TMeta> + + 'use'( + middleware: Middleware< + TCurrentContext, + UOutContext, + UInput, + SchemaInput, + ORPCErrorConstructorMap, + TMeta + >, + mapInput: MapInputMiddleware, UInput>, + ): ConflictContextGuard> & + ProcedureBuilderWithInput, TInputSchema, TOutputSchema, TErrorMap, TMeta> + + 'meta'( + meta: TMeta, + ): ProcedureBuilderWithInput + + 'route'( + route: Route, + ): ProcedureBuilderWithInput + + 'output'( + schema: USchema, + ): ProcedureBuilderWithInputOutput + + 'handler'>( + handler: ProcedureHandler, + ): DecoratedProcedure +} + +export interface ProcedureBuilderWithOutput< + TInitialContext extends Context, + TCurrentContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> { + '~orpc': BuilderDef + + 'errors'( + errors: U, + ): ProcedureBuilderWithOutput, TMeta> + + 'use'( + middleware: Middleware< + TCurrentContext, + UOutContext, + SchemaOutput, + SchemaInput, + ORPCErrorConstructorMap, + TMeta + >, + ): ConflictContextGuard> & + ProcedureBuilderWithOutput, TInputSchema, TOutputSchema, TErrorMap, TMeta> + + 'meta'( + meta: TMeta, + ): ProcedureBuilderWithOutput + + 'route'( + route: Route, + ): ProcedureBuilderWithOutput + + 'input'( + schema: USchema, + ): ProcedureBuilderWithInputOutput + + 'handler'>( + handler: ProcedureHandler, + ): DecoratedProcedure +} + +export interface ProcedureBuilderWithInputOutput< + TInitialContext extends Context, + TCurrentContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> { + '~orpc': BuilderDef + + 'errors'( + errors: U, + ): ProcedureBuilderWithInputOutput, TMeta> + + 'use'( + middleware: Middleware< + TCurrentContext, + UOutContext, + SchemaOutput, + SchemaInput, + ORPCErrorConstructorMap, + TMeta + >, + ): ConflictContextGuard> & + ProcedureBuilderWithInputOutput, TInputSchema, TOutputSchema, TErrorMap, TMeta> + + 'use'( + middleware: Middleware< + TCurrentContext, + UOutContext, + UInput, + SchemaInput, + ORPCErrorConstructorMap, + TMeta + >, + mapInput: MapInputMiddleware, UInput>, + ): ConflictContextGuard> & + ProcedureBuilderWithInputOutput, TInputSchema, TOutputSchema, TErrorMap, TMeta> + + 'meta'( + meta: TMeta, + ): ProcedureBuilderWithInputOutput + + 'route'( + route: Route, + ): ProcedureBuilderWithInputOutput + + 'handler'>( + handler: ProcedureHandler, + ): DecoratedProcedure +} + +export interface RouterBuilder< + TInitialContext extends Context, + TCurrentContext extends Context, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> { + '~orpc': AdaptRouterOptions + + 'errors'( + errors: U, + ): RouterBuilder, TMeta> + + 'use'( + middleware: Middleware< + TCurrentContext, + UOutContext, + unknown, + unknown, + ORPCErrorConstructorMap, + TMeta + >, + ): ConflictContextGuard> & + RouterBuilder, TErrorMap, TMeta> + + 'prefix'(prefix: HTTPPath): RouterBuilder + + 'tag'(...tags: string[]): RouterBuilder + + 'router'>>( + router: U + ): AdaptedRouter + + 'lazy'>>( + loader: () => Promise<{ default: U }>, + ): AdaptedRouter, TInitialContext, TErrorMap> +} diff --git a/packages/server/src/builder-with-errors-middlewares.test-d.ts b/packages/server/src/builder-with-errors-middlewares.test-d.ts deleted file mode 100644 index 0ab65904..00000000 --- a/packages/server/src/builder-with-errors-middlewares.test-d.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { Route } from '@orpc/contract' -import type { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares' -import type { Context } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { Lazy } from './lazy' -import type { MiddlewareOutputFn } from './middleware' -import type { ANY_PROCEDURE, Procedure } from './procedure' -import type { ProcedureBuilder } from './procedure-builder' -import type { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import type { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import type { DecoratedProcedure } from './procedure-decorated' -import type { AdaptedRouter, RouterBuilder } from './router-builder' -import { z } from 'zod' - -const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - -const baseErrors = { - BASE: { - data: z.string(), - }, -} - -const errors = { - CODE: { - status: 404, - data: z.object({ why: z.string() }), - }, -} - -const builder = {} as BuilderWithErrorsMiddlewares<{ db: string }, { db: string, auth?: boolean }, typeof baseErrors> - -describe('BuilderWithErrorsMiddlewares', () => { - it('.errors', () => { - expectTypeOf(builder.errors(errors)).toEqualTypeOf>() - - // @ts-expect-error --- not allow redefine error map - builder.errors({ BASE: baseErrors.BASE }) - }) - - it('.use', () => { - const applied = builder.use(({ context, next, path, procedure, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string, auth?: boolean }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({ - context: { - extra: true, - }, - }) - }) - - expectTypeOf(applied).toEqualTypeOf < BuilderWithErrorsMiddlewares < { db: string }, { db: string, auth?: boolean } & { extra: boolean }, typeof baseErrors>>() - - // @ts-expect-error --- conflict context - builder.use(({ next }) => next({ db: 123 })) - // @ts-expect-error --- input is not match - builder.use(({ next }, input: 'invalid') => next({})) - // @ts-expect-error --- output is not match - builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) - }) - - it('.route', () => { - expectTypeOf(builder.route({ path: '/test', method: 'GET' })).toEqualTypeOf< - ProcedureBuilder<{ db: string }, { db: string, auth?: boolean }, typeof baseErrors> - >() - }) - - it('.input', () => { - expectTypeOf(builder.input(schema)).toEqualTypeOf< - ProcedureBuilderWithInput<{ db: string }, { db: string, auth?: boolean }, typeof schema, typeof baseErrors> - >() - }) - - it('.output', () => { - expectTypeOf(builder.output(schema)).toEqualTypeOf< - ProcedureBuilderWithOutput<{ db: string }, { db: string, auth?: boolean }, typeof schema, typeof baseErrors> - >() - }) - - it('.handler', () => { - const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string, auth?: boolean }>() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(signal).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return 456 - }) - - expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, number, typeof baseErrors, Route> - >() - }) - - it('.prefix', () => { - expectTypeOf(builder.prefix('/test')).toEqualTypeOf< - RouterBuilder<{ db: string }, { db: string, auth?: boolean }, typeof baseErrors> - >() - }) - - it('.tag', () => { - expectTypeOf(builder.tag('test', 'test2')).toEqualTypeOf< - RouterBuilder<{ db: string }, { db: string, auth?: boolean }, typeof baseErrors> - >() - }) - - it('.router', () => { - const router = { - ping: {} as Procedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, unknown, typeof errors, Route>, - pong: {} as Procedure, Route>, - } - - expectTypeOf(builder.router(router)).toEqualTypeOf< - AdaptedRouter<{ db: string }, typeof router, typeof baseErrors> - >() - - builder.router({ - // @ts-expect-error - context is not match - ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors>, - }) - - const invalidErrorMap = { - BASE: { - ...baseErrors.BASE, - status: 400, - }, - } - - builder.router({ - // @ts-expect-error - error map is not match - ping: {} as Procedure, - }) - }) - - it('.lazy', () => { - const router = { - ping: {} as Procedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, unknown, typeof errors, Route>, - pong: {} as Procedure, Route>, - } - - expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf< - AdaptedRouter<{ db: string }, Lazy, typeof baseErrors> - >() - - // @ts-expect-error - context is not match - builder.lazy(() => Promise.resolve({ default: { - ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors, Route>, - } })) - - // @ts-expect-error - error map is not match - builder.lazy(() => Promise.resolve({ - default: { - ping: {} as Procedure, - }, - })) - }) -}) diff --git a/packages/server/src/builder-with-errors-middlewares.test.ts b/packages/server/src/builder-with-errors-middlewares.test.ts deleted file mode 100644 index 2581498f..00000000 --- a/packages/server/src/builder-with-errors-middlewares.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { z } from 'zod' -import { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares' -import { unlazy } from './lazy' -import { ProcedureBuilder } from './procedure-builder' -import { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import { DecoratedProcedure } from './procedure-decorated' -import { RouterBuilder } from './router-builder' - -vi.mock('./router-builder', async (origin) => { - const RouterBuilder = vi.fn() - RouterBuilder.prototype.router = vi.fn(() => '__router__') - RouterBuilder.prototype.lazy = vi.fn(() => '__lazy__') - - return { - RouterBuilder, - } -}) - -const RouterBuilderRouterSpy = vi.spyOn(RouterBuilder.prototype, 'router') -const RouterBuilderLazySpy = vi.spyOn(RouterBuilder.prototype, 'lazy') - -const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - -const errors = { - CODE: { - status: 404, - data: z.object({ why: z.string() }), - }, -} - -const baseErrors = { - BASE: { - status: 404, - data: z.object({ why: z.string() }), - }, -} - -const mid = vi.fn() - -const builder = new BuilderWithErrorsMiddlewares({ - middlewares: [mid], - errorMap: baseErrors, - inputValidationIndex: 1, - outputValidationIndex: 1, - config: { - initialRoute: { - description: 'from initial', - }, - }, -}) - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('builder', () => { - it('.errors', () => { - const applied = builder.errors(errors) - expect(applied).not.toBe(builder) - expect(applied).toBeInstanceOf(BuilderWithErrorsMiddlewares) - expect(applied['~orpc'].errorMap).toEqual({ ...baseErrors, ...errors }) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].config).toEqual({ initialRoute: { description: 'from initial' } }) - }) - - it('.use', () => { - const mid2 = vi.fn() - const applied = builder.use(mid2) - expect(applied).toBeInstanceOf(BuilderWithErrorsMiddlewares) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual(baseErrors) - expect(applied['~orpc'].middlewares).toEqual([mid, mid2]) - expect(applied['~orpc'].inputValidationIndex).toEqual(2) - expect(applied['~orpc'].outputValidationIndex).toEqual(2) - expect(applied['~orpc'].config).toEqual({ initialRoute: { description: 'from initial' } }) - }) - - it('.route', () => { - const route = { path: '/test', method: 'GET' } as const - const applied = builder.route(route) - expect(applied).toBeInstanceOf(ProcedureBuilder) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ ...route, description: 'from initial' }) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - }) - - it('.input', () => { - const applied = builder.input(schema) - expect(applied).toBeInstanceOf(ProcedureBuilderWithInput) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toEqual(schema) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ description: 'from initial' }) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - }) - - it('.output', () => { - const applied = builder.output(schema) - expect(applied).toBeInstanceOf(ProcedureBuilderWithOutput) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toEqual(schema) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ description: 'from initial' }) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - }) - - it('.handler', () => { - const handler = vi.fn() - const applied = builder.handler(handler) - expect(applied).toBeInstanceOf(DecoratedProcedure) - expect(applied['~orpc'].handler).toEqual(handler) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ description: 'from initial' }) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - }) - - it('.prefix', () => { - const applied = builder.prefix('/test') - expect(applied).toBe(vi.mocked(RouterBuilder).mock.results[0]!.value) - expect(applied).toBeInstanceOf(RouterBuilder) - expect(RouterBuilder).toHaveBeenCalledWith(expect.objectContaining({ - middlewares: [mid], - errorMap: baseErrors, - prefix: '/test', - })) - }) - - it('.tag', () => { - const applied = builder.tag('test', 'test2') - expect(applied).toBe(vi.mocked(RouterBuilder).mock.results[0]!.value) - expect(applied).toBeInstanceOf(RouterBuilder) - expect(RouterBuilder).toHaveBeenCalledWith(expect.objectContaining({ - middlewares: [mid], - errorMap: baseErrors, - tags: ['test', 'test2'], - })) - }) - - it('.router', () => { - const router = { - ping: {} as any, - pong: {} as any, - } - - const applied = builder.router(router) - - expect(applied).toBe(RouterBuilderRouterSpy.mock.results[0]!.value) - expect(RouterBuilderRouterSpy).toHaveBeenCalledWith(router) - expect(RouterBuilder).toHaveBeenCalledWith(expect.objectContaining({ - middlewares: [mid], - errorMap: baseErrors, - })) - }) - - it('.lazy', () => { - const router = { - ping: {} as any, - pong: {} as any, - } - - const applied = builder.lazy(() => Promise.resolve({ default: router })) - - expect(applied).toBe(RouterBuilderLazySpy.mock.results[0]!.value) - expect(RouterBuilderLazySpy).toHaveBeenCalledWith(expect.any(Function)) - expect(RouterBuilder).toHaveBeenCalledWith(expect.objectContaining({ - middlewares: [mid], - errorMap: baseErrors, - })) - expect(unlazy(RouterBuilderLazySpy.mock.results[0]!.value)).resolves.toEqual({ default: '__lazy__' }) - }) -}) diff --git a/packages/server/src/builder-with-errors-middlewares.ts b/packages/server/src/builder-with-errors-middlewares.ts deleted file mode 100644 index 4059883a..00000000 --- a/packages/server/src/builder-with-errors-middlewares.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type { ContractBuilderConfig, ContractRouter, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, HTTPPath, Route, Schema, SchemaInput, SchemaOutput, StrictErrorMap } from '@orpc/contract' -import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { FlattenLazy } from './lazy' -import type { Middleware } from './middleware' -import type { ProcedureHandler } from './procedure' -import type { Router } from './router' -import type { AdaptedRouter } from './router-builder' -import { ContractProcedure, fallbackContractConfig } from '@orpc/contract' -import { ProcedureBuilder } from './procedure-builder' -import { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import { DecoratedProcedure } from './procedure-decorated' -import { RouterBuilder } from './router-builder' - -export interface BuilderWithErrorsMiddlewaresDef< - TInitialContext extends Context, - TCurrentContext extends Context, - TErrorMap extends ErrorMap, -> { - __initialContext?: TypeInitialContext - __currentContext?: TypeCurrentContext - errorMap: TErrorMap - middlewares: Middleware[] - inputValidationIndex: number - outputValidationIndex: number - config: ContractBuilderConfig -} - -/** - * `BuilderWithErrorsMiddlewares` is a combination of `BuilderWithErrorsMiddlewares` and `BuilderWithErrors`. - * - * Why? - * - prevents .middleware after .use (can mislead the behavior) - * - prevents .contract after .errors (add error map to existing contract can make the contract invalid) - * - prevents .context after .use (middlewares required current context, so it tricky when change the current context) - * - */ -export class BuilderWithErrorsMiddlewares< - TInitialContext extends Context, - TCurrentContext extends Context, - TErrorMap extends ErrorMap, -> { - '~type' = 'BuilderWithErrorsMiddlewares' as const - '~orpc': BuilderWithErrorsMiddlewaresDef - - constructor(def: BuilderWithErrorsMiddlewaresDef) { - this['~orpc'] = def - } - - errors & ErrorMapSuggestions>( - errors: U, - ): BuilderWithErrorsMiddlewares { - return new BuilderWithErrorsMiddlewares({ - ...this['~orpc'], - errorMap: { - ...this['~orpc'].errorMap, - ...errors, - }, - }) - } - - use( - middleware: Middleware>, - ): ConflictContextGuard - & BuilderWithErrorsMiddlewares { - const builder = new BuilderWithErrorsMiddlewares({ - config: this['~orpc'].config, - errorMap: this['~orpc'].errorMap, - inputValidationIndex: this['~orpc'].inputValidationIndex + 1, - outputValidationIndex: this['~orpc'].outputValidationIndex + 1, - middlewares: [...this['~orpc'].middlewares, middleware], - }) - - return builder as typeof builder & ConflictContextGuard - } - - route(route: Route): ProcedureBuilder { - return new ProcedureBuilder({ - ...this['~orpc'], - contract: new ContractProcedure({ - route: { - ...this['~orpc'].config.initialRoute, - ...route, - }, - InputSchema: undefined, - OutputSchema: undefined, - errorMap: this['~orpc'].errorMap, - }), - }) - } - - input( - schema: USchema, - example?: SchemaInput, - ): ProcedureBuilderWithInput { - return new ProcedureBuilderWithInput({ - ...this['~orpc'], - contract: new ContractProcedure({ - route: fallbackContractConfig('defaultInitialRoute', this['~orpc'].config.initialRoute), - OutputSchema: undefined, - InputSchema: schema, - inputExample: example, - errorMap: this['~orpc'].errorMap, - }), - }) - } - - output( - schema: USchema, - example?: SchemaOutput, - ): ProcedureBuilderWithOutput { - return new ProcedureBuilderWithOutput({ - ...this['~orpc'], - contract: new ContractProcedure({ - route: fallbackContractConfig('defaultInitialRoute', this['~orpc'].config.initialRoute), - InputSchema: undefined, - OutputSchema: schema, - outputExample: example, - errorMap: this['~orpc'].errorMap, - }), - }) - } - - handler( - handler: ProcedureHandler, - ): DecoratedProcedure { - return new DecoratedProcedure({ - ...this['~orpc'], - contract: new ContractProcedure({ - route: fallbackContractConfig('defaultInitialRoute', this['~orpc'].config.initialRoute), - InputSchema: undefined, - OutputSchema: undefined, - errorMap: this['~orpc'].errorMap, - }), - handler, - }) - } - - prefix(prefix: HTTPPath): RouterBuilder { - return new RouterBuilder({ - ...this['~orpc'], - prefix, - }) - } - - tag(...tags: string[]): RouterBuilder { - return new RouterBuilder({ - ...this['~orpc'], - tags, - }) - } - - router>>>>( - router: U, - ): AdaptedRouter { - return new RouterBuilder(this['~orpc']).router(router) - } - - lazy>>>>( - loader: () => Promise<{ default: U }>, - ): AdaptedRouter, TErrorMap> { - return new RouterBuilder(this['~orpc']).lazy(loader) - } -} diff --git a/packages/server/src/builder-with-errors.test-d.ts b/packages/server/src/builder-with-errors.test-d.ts deleted file mode 100644 index ff6fef39..00000000 --- a/packages/server/src/builder-with-errors.test-d.ts +++ /dev/null @@ -1,210 +0,0 @@ -import type { Route, StrictErrorMap } from '@orpc/contract' -import type { BuilderWithErrors } from './builder-with-errors' -import type { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares' -import type { Context } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { Lazy } from './lazy' -import type { MiddlewareOutputFn } from './middleware' -import type { DecoratedMiddleware } from './middleware-decorated' -import type { ANY_PROCEDURE, Procedure } from './procedure' -import type { ProcedureBuilder } from './procedure-builder' -import type { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import type { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import type { DecoratedProcedure } from './procedure-decorated' -import type { AdaptedRouter, RouterBuilder } from './router-builder' -import { z } from 'zod' - -const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - -const baseErrors = { - BASE: { - data: z.string(), - }, -} - -const errors = { - CODE: { - status: 404, - data: z.object({ why: z.string() }), - }, -} - -const builder = {} as BuilderWithErrors<{ db: string }, typeof baseErrors> - -describe('BuilderWithErrors', () => { - it('.context', () => { - expectTypeOf(builder.context()).toEqualTypeOf>() - expectTypeOf(builder.context<{ db: string, anything: string }>()).toEqualTypeOf>() - - // @ts-expect-error - new context must satisfy old context - builder.context<{ anything: string }>() - }) - - it('.config', () => { - expectTypeOf(builder.config({ initialRoute: { method: 'GET' } })).toEqualTypeOf() - - // @ts-expect-error - invalid method - builder.config({ initialRoute: { method: 'HE' } }) - }) - - it('.middleware', () => { - const mid = builder.middleware(({ context, next, path, procedure, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({ - context: { - extra: true, - }, - }) - }) - - expectTypeOf(mid).toEqualTypeOf< - DecoratedMiddleware<{ db: string }, { extra: boolean }, unknown, any, ORPCErrorConstructorMap> - >() - - const mid2 = builder.middleware(({ next }, input: 'input', output: MiddlewareOutputFn<'output'>) => next({})) - - expectTypeOf(mid2).toEqualTypeOf< - DecoratedMiddleware<{ db: string }, Record, 'input', 'output', ORPCErrorConstructorMap> - >() - - // @ts-expect-error --- conflict context - builder.middleware(({ next }) => next({ db: 123 })) - }) - - it('.errors', () => { - expectTypeOf(builder.errors(errors)).toEqualTypeOf < BuilderWithErrors<{ db: string }, StrictErrorMap & typeof baseErrors>>() - - // @ts-expect-error --- not allow redefine error map - builder.errors({ BASE: baseErrors.BASE }) - }) - - it('.use', () => { - const applied = builder.use(({ context, next, path, procedure, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({ - context: { - extra: true, - }, - }) - }) - - expectTypeOf(applied).toEqualTypeOf < BuilderWithErrorsMiddlewares < { db: string }, { db: string } & { extra: boolean }, typeof baseErrors>>() - - // @ts-expect-error --- conflict context - builder.use(({ next }) => next({ db: 123 })) - // @ts-expect-error --- input is not match - builder.use(({ next }, input: 'invalid') => next({})) - // @ts-expect-error --- output is not match - builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) - }) - - it('.route', () => { - expectTypeOf(builder.route({ path: '/test', method: 'GET' })).toEqualTypeOf< - ProcedureBuilder<{ db: string }, { db: string }, typeof baseErrors> - >() - }) - - it('.input', () => { - expectTypeOf(builder.input(schema)).toEqualTypeOf< - ProcedureBuilderWithInput<{ db: string }, { db: string }, typeof schema, typeof baseErrors> - >() - }) - - it('.output', () => { - expectTypeOf(builder.output(schema)).toEqualTypeOf< - ProcedureBuilderWithOutput<{ db: string }, { db: string }, typeof schema, typeof baseErrors> - >() - }) - - it('.handler', () => { - const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string }>() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(signal).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return 456 - }) - - expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure<{ db: string }, { db: string }, undefined, undefined, number, typeof baseErrors, Route> - >() - }) - - it('.prefix', () => { - expectTypeOf(builder.prefix('/test')).toEqualTypeOf< - RouterBuilder<{ db: string }, { db: string }, typeof baseErrors> - >() - }) - - it('.tag', () => { - expectTypeOf(builder.tag('test', 'test2')).toEqualTypeOf< - RouterBuilder<{ db: string }, { db: string }, typeof baseErrors> - >() - }) - - it('.router', () => { - const router = { - ping: {} as Procedure<{ db: string }, { db: string }, undefined, undefined, unknown, typeof errors, Route>, - pong: {} as Procedure, Route>, - } - - expectTypeOf(builder.router(router)).toEqualTypeOf< - AdaptedRouter<{ db: string }, typeof router, typeof baseErrors> - >() - - builder.router({ - // @ts-expect-error - context is not match - ping: {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown, typeof errors>, - }) - - const invalidErrorMap = { - BASE: { - ...baseErrors.BASE, - status: 400, - }, - } - - builder.router({ - // @ts-expect-error - error map is not match - ping: {} as ContractProcedure, - }) - }) - - it('.lazy', () => { - const router = { - ping: {} as Procedure<{ db: string }, { db: string }, undefined, undefined, unknown, typeof errors, Route>, - pong: {} as Procedure, Route>, - } - - expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf< - AdaptedRouter<{ db: string }, Lazy, typeof baseErrors> - >() - - // @ts-expect-error - context is not match - builder.lazy(() => Promise.resolve({ default: { - ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors, Route>, - } })) - - // @ts-expect-error - error map is not match - builder.lazy(() => Promise.resolve({ - default: { - ping: {} as Procedure, - }, - })) - }) -}) diff --git a/packages/server/src/builder-with-errors.test.ts b/packages/server/src/builder-with-errors.test.ts deleted file mode 100644 index a3721a01..00000000 --- a/packages/server/src/builder-with-errors.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { z } from 'zod' -import { BuilderWithErrors } from './builder-with-errors' -import { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares' -import { unlazy } from './lazy' -import * as middlewareDecorated from './middleware-decorated' -import { ProcedureBuilder } from './procedure-builder' -import { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import { DecoratedProcedure } from './procedure-decorated' -import { RouterBuilder } from './router-builder' - -vi.mock('./router-builder', async (origin) => { - const RouterBuilder = vi.fn() - RouterBuilder.prototype.router = vi.fn(() => '__router__') - RouterBuilder.prototype.lazy = vi.fn(() => '__lazy__') - - return { - RouterBuilder, - } -}) - -const decorateMiddlewareSpy = vi.spyOn(middlewareDecorated, 'decorateMiddleware') -const RouterBuilderRouterSpy = vi.spyOn(RouterBuilder.prototype, 'router') -const RouterBuilderLazySpy = vi.spyOn(RouterBuilder.prototype, 'lazy') - -const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - -const errors = { - CODE: { - status: 404, - data: z.object({ why: z.string() }), - }, -} - -const baseErrors = { - BASE: { - status: 404, - data: z.object({ why: z.string() }), - }, -} - -const config = { - initialRoute: { - description: 'from initial', - }, - initialInputValidationIndex: 99, - initialOutputValidationIndex: 99, -} - -const builder = new BuilderWithErrors({ - errorMap: baseErrors, - config, -}) - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('builder', () => { - it('.config', () => { - const applied = builder.config({ initialRoute: { method: 'GET' } }) - expect(applied).instanceOf(BuilderWithErrors) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].config).toEqual({ - ...config, - initialRoute: { method: 'GET' }, - }) - }) - - it('.context', () => { - const applied = builder.context() - expect(applied).toBe(builder) - }) - - it('.middleware', () => { - const fn = vi.fn() - const mid = builder.middleware(fn) - - expect(mid).toBe(decorateMiddlewareSpy.mock.results[0]!.value) - expect(decorateMiddlewareSpy).toHaveBeenCalledWith(fn) - }) - - it('.errors', () => { - const applied = builder.errors(errors) - expect(applied).toBeInstanceOf(BuilderWithErrors) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].errorMap).toEqual({ ...baseErrors, ...errors }) - expect(applied['~orpc'].config).toEqual(config) - }) - - it('.use', () => { - const mid = vi.fn() - const applied = builder.use(mid) - expect(applied).toBeInstanceOf(BuilderWithErrorsMiddlewares) - expect(applied['~orpc'].errorMap).toEqual(baseErrors) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(100) - expect(applied['~orpc'].outputValidationIndex).toEqual(100) - expect(applied['~orpc'].config).toEqual(config) - }) - - it('.route', () => { - const route = { path: '/test', method: 'GET' } as const - const applied = builder.route(route) - expect(applied).toBeInstanceOf(ProcedureBuilder) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ ...route, description: 'from initial' }) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - expect(applied['~orpc'].inputValidationIndex).toEqual(99) - expect(applied['~orpc'].outputValidationIndex).toEqual(99) - }) - - it('.input', () => { - const applied = builder.input(schema) - expect(applied).toBeInstanceOf(ProcedureBuilderWithInput) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toEqual(schema) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ description: 'from initial' }) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - expect(applied['~orpc'].inputValidationIndex).toEqual(99) - expect(applied['~orpc'].outputValidationIndex).toEqual(99) - }) - - it('.output', () => { - const applied = builder.output(schema) - expect(applied).toBeInstanceOf(ProcedureBuilderWithOutput) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toEqual(schema) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ description: 'from initial' }) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - expect(applied['~orpc'].inputValidationIndex).toEqual(99) - expect(applied['~orpc'].outputValidationIndex).toEqual(99) - }) - - it('.handler', () => { - const handler = vi.fn() - const applied = builder.handler(handler) - expect(applied).toBeInstanceOf(DecoratedProcedure) - expect(applied['~orpc'].handler).toEqual(handler) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ description: 'from initial' }) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - expect(applied['~orpc'].inputValidationIndex).toEqual(99) - expect(applied['~orpc'].outputValidationIndex).toEqual(99) - }) - - it('.prefix', () => { - const applied = builder.prefix('/test') - expect(applied).toBe(vi.mocked(RouterBuilder).mock.results[0]!.value) - expect(applied).toBeInstanceOf(RouterBuilder) - expect(RouterBuilder).toHaveBeenCalledWith(expect.objectContaining({ errorMap: baseErrors, prefix: '/test' })) - }) - - it('.tag', () => { - const applied = builder.tag('test', 'test2') - expect(applied).toBe(vi.mocked(RouterBuilder).mock.results[0]!.value) - expect(applied).toBeInstanceOf(RouterBuilder) - expect(RouterBuilder).toHaveBeenCalledWith(expect.objectContaining({ errorMap: baseErrors, tags: ['test', 'test2'] })) - }) - - it('.router', () => { - const router = { - ping: {} as any, - pong: {} as any, - } - - const applied = builder.router(router) - - expect(applied).toBe(RouterBuilderRouterSpy.mock.results[0]!.value) - expect(RouterBuilderRouterSpy).toHaveBeenCalledWith(router) - expect(RouterBuilder).toHaveBeenCalledWith(expect.objectContaining({ errorMap: baseErrors })) - }) - - it('.lazy', () => { - const router = { - ping: {} as any, - pong: {} as any, - } - - const applied = builder.lazy(() => Promise.resolve({ default: router })) - - expect(applied).toBe(RouterBuilderLazySpy.mock.results[0]!.value) - expect(RouterBuilderLazySpy).toHaveBeenCalledWith(expect.any(Function)) - expect(RouterBuilder).toHaveBeenCalledWith(expect.objectContaining({ errorMap: baseErrors })) - expect(unlazy(RouterBuilderLazySpy.mock.results[0]!.value)).resolves.toEqual({ default: '__lazy__' }) - }) -}) diff --git a/packages/server/src/builder-with-errors.ts b/packages/server/src/builder-with-errors.ts deleted file mode 100644 index bb00f488..00000000 --- a/packages/server/src/builder-with-errors.ts +++ /dev/null @@ -1,188 +0,0 @@ -import type { ContractBuilderConfig, ContractRouter, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, HTTPPath, Route, Schema, SchemaInput, SchemaOutput, StrictErrorMap } from '@orpc/contract' -import type { BuilderConfig } from './builder' -import type { Context, TypeInitialContext } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { FlattenLazy } from './lazy' -import type { Middleware } from './middleware' -import type { DecoratedMiddleware } from './middleware-decorated' -import type { ProcedureHandler } from './procedure' -import type { Router } from './router' -import type { AdaptedRouter } from './router-builder' -import { ContractProcedure, fallbackContractConfig } from '@orpc/contract' -import { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares' -import { fallbackConfig } from './config' -import { decorateMiddleware } from './middleware-decorated' -import { ProcedureBuilder } from './procedure-builder' -import { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import { DecoratedProcedure } from './procedure-decorated' -import { RouterBuilder } from './router-builder' - -export interface BuilderWithErrorsDef { - __initialContext?: TypeInitialContext - errorMap: TErrorMap - config: BuilderConfig -} - -/** - * `BuilderWithErrors` is a branch of `Builder` which it has error map. - * - * Why? - * - prevents .contract after .errors (add error map to existing contract can make the contract invalid) - * - */ -export class BuilderWithErrors { - '~type' = 'BuilderWithErrors' as const - '~orpc': BuilderWithErrorsDef - - constructor(def: BuilderWithErrorsDef) { - this['~orpc'] = def - } - - config(config: ContractBuilderConfig): BuilderWithErrors { - return new BuilderWithErrors({ - ...this['~orpc'], - config: { - ...this['~orpc'].config, - ...config, - }, - }) - } - - context(): BuilderWithErrors { - return this as any // just change at type level so safely cast here - } - - errors & ErrorMapSuggestions>( - errors: U, - ): BuilderWithErrors & TErrorMap> { - return new BuilderWithErrors({ - ...this['~orpc'], - errorMap: { - ...this['~orpc'].errorMap, - ...errors, - }, - }) - } - - middleware( - middleware: Middleware>, - ): DecoratedMiddleware> { - return decorateMiddleware(middleware) - } - - use( - middleware: Middleware>, - ): BuilderWithErrorsMiddlewares { - return new BuilderWithErrorsMiddlewares({ - ...this['~orpc'], - inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex) + 1, - outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex) + 1, - middlewares: [middleware], - }) - } - - route(route: Route): ProcedureBuilder { - return new ProcedureBuilder({ - middlewares: [], - inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), - outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex), - contract: new ContractProcedure({ - route: { - ...fallbackContractConfig('defaultInitialRoute', this['~orpc'].config.initialRoute), - ...route, - }, - InputSchema: undefined, - OutputSchema: undefined, - errorMap: this['~orpc'].errorMap, - }), - }) - } - - input( - schema: USchema, - example?: SchemaInput, - ): ProcedureBuilderWithInput { - return new ProcedureBuilderWithInput({ - middlewares: [], - inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), - outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex), - contract: new ContractProcedure({ - route: fallbackContractConfig('defaultInitialRoute', this['~orpc'].config.initialRoute), - OutputSchema: undefined, - InputSchema: schema, - inputExample: example, - errorMap: this['~orpc'].errorMap, - }), - }) - } - - output( - schema: USchema, - example?: SchemaOutput, - ): ProcedureBuilderWithOutput { - return new ProcedureBuilderWithOutput({ - middlewares: [], - inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), - outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex), - contract: new ContractProcedure({ - route: fallbackContractConfig('defaultInitialRoute', this['~orpc'].config.initialRoute), - InputSchema: undefined, - OutputSchema: schema, - outputExample: example, - errorMap: this['~orpc'].errorMap, - }), - }) - } - - handler( - handler: ProcedureHandler, - ): DecoratedProcedure { - return new DecoratedProcedure({ - middlewares: [], - inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), - outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex), - contract: new ContractProcedure({ - route: fallbackContractConfig('defaultInitialRoute', this['~orpc'].config.initialRoute), - InputSchema: undefined, - OutputSchema: undefined, - errorMap: this['~orpc'].errorMap, - }), - handler, - }) - } - - prefix(prefix: HTTPPath): RouterBuilder { - return new RouterBuilder({ - middlewares: [], - errorMap: this['~orpc'].errorMap, - prefix, - }) - } - - tag(...tags: string[]): RouterBuilder { - return new RouterBuilder({ - middlewares: [], - errorMap: this['~orpc'].errorMap, - tags, - }) - } - - router>>>( - router: U, - ): AdaptedRouter { - return new RouterBuilder({ - middlewares: [], - ...this['~orpc'], - }).router(router) - } - - lazy>>>>( - loader: () => Promise<{ default: U }>, - ): AdaptedRouter, TErrorMap> { - return new RouterBuilder({ - middlewares: [], - ...this['~orpc'], - }).lazy(loader) - } -} diff --git a/packages/server/src/builder-with-middlewares.test-d.ts b/packages/server/src/builder-with-middlewares.test-d.ts deleted file mode 100644 index 0c30229f..00000000 --- a/packages/server/src/builder-with-middlewares.test-d.ts +++ /dev/null @@ -1,150 +0,0 @@ -import type { Route } from '@orpc/contract' -import type { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares' -import type { BuilderWithMiddlewares } from './builder-with-middlewares' -import type { Context } from './context' -import type { ChainableImplementer } from './implementer-chainable' -import type { Lazy } from './lazy' -import type { MiddlewareOutputFn } from './middleware' -import type { ANY_PROCEDURE, Procedure } from './procedure' -import type { ProcedureBuilder } from './procedure-builder' -import type { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import type { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import type { DecoratedProcedure } from './procedure-decorated' -import type { RouterBuilder } from './router-builder' -import type { UnshiftedMiddlewaresRouter } from './router-utils' -import { oc } from '@orpc/contract' -import { z } from 'zod' - -const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - -const errors = { - CODE: { - status: 404, - data: z.object({ why: z.string() }), - }, -} - -const builder = {} as BuilderWithMiddlewares<{ db: string }, { db: string, auth?: boolean }> - -describe('BuilderWithMiddlewares', () => { - it('.use', () => { - const applied = builder.use(({ context, next, path, procedure, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string, auth?: boolean }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({ - context: { - extra: true, - }, - }) - }) - - expectTypeOf(applied).toEqualTypeOf>() - - // @ts-expect-error --- conflict context - builder.use(({ next }) => next({ context: { db: 123 } })) - // conflict but not detected - expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf() - // @ts-expect-error --- input is not match - builder.use(({ next }, input: 'invalid') => next({})) - // @ts-expect-error --- output is not match - builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) - }) - - it('.errors', () => { - expectTypeOf(builder.errors(errors)).toEqualTypeOf>() - }) - - it('.route', () => { - expectTypeOf(builder.route({ path: '/test', method: 'GET' })).toEqualTypeOf< - ProcedureBuilder<{ db: string }, { db: string, auth?: boolean }, Record> - >() - }) - - it('.input', () => { - expectTypeOf(builder.input(schema)).toEqualTypeOf< - ProcedureBuilderWithInput<{ db: string }, { db: string, auth?: boolean }, typeof schema, Record> - >() - }) - - it('.output', () => { - expectTypeOf(builder.output(schema)).toEqualTypeOf< - ProcedureBuilderWithOutput<{ db: string }, { db: string, auth?: boolean }, typeof schema, Record> - >() - }) - - it('.handler', () => { - const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string, auth?: boolean }>() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(signal).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return 456 - }) - - expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, number, Record, Route> - >() - }) - - it('.prefix', () => { - expectTypeOf(builder.prefix('/test')).toEqualTypeOf< - RouterBuilder<{ db: string }, { db: string, auth?: boolean }, Record> - >() - }) - - it('.tag', () => { - expectTypeOf(builder.tag('test', 'test2')).toEqualTypeOf< - RouterBuilder<{ db: string }, { db: string, auth?: boolean }, Record> - >() - }) - - it('.router', () => { - const router = { - ping: {} as Procedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, unknown, typeof errors, Route>, - pong: {} as Procedure, Route>, - } - - expectTypeOf(builder.router(router)).toEqualTypeOf< - UnshiftedMiddlewaresRouter - >() - - builder.router({ - // @ts-expect-error - context is not match - ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors>, - }) - }) - - it('.lazy', () => { - const router = { - ping: {} as Procedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, unknown, typeof errors, Route>, - pong: {} as Procedure, Route>, - } - - expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf< - UnshiftedMiddlewaresRouter, { db: string }> - >() - - // @ts-expect-error - context is not match - builder.lazy(() => Promise.resolve({ default: { - ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors, Route>, - } })) - }) - - it('.contract', () => { - const contract = oc.router({ - ping: oc.input(schema).output(schema), - }) - - expectTypeOf(builder.contract(contract)).toEqualTypeOf< - ChainableImplementer<{ db: string }, { db: string, auth?: boolean }, typeof contract> - >() - }) -}) diff --git a/packages/server/src/builder-with-middlewares.test.ts b/packages/server/src/builder-with-middlewares.test.ts deleted file mode 100644 index 28b847d4..00000000 --- a/packages/server/src/builder-with-middlewares.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { oc } from '@orpc/contract' -import { z } from 'zod' -import { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares' -import { BuilderWithMiddlewares } from './builder-with-middlewares' -import * as implementerChainable from './implementer-chainable' -import { unlazy } from './lazy' -import { ProcedureBuilder } from './procedure-builder' -import { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import { DecoratedProcedure } from './procedure-decorated' -import { RouterBuilder } from './router-builder' -import * as routerUtils from './router-utils' - -const unshiftMiddlewaresRouterSpy = vi.spyOn(routerUtils, 'unshiftMiddlewaresRouter') - -vi.mock('./router-builder', async (origin) => { - const RouterBuilder = vi.fn() - RouterBuilder.prototype.router = vi.fn(() => '__router__') - RouterBuilder.prototype.lazy = vi.fn(() => '__lazy__') - - return { - RouterBuilder, - } -}) - -const RouterBuilderRouterSpy = vi.spyOn(RouterBuilder.prototype, 'router') -const RouterBuilderLazySpy = vi.spyOn(RouterBuilder.prototype, 'lazy') -const createChainableImplementerSpy = vi.spyOn(implementerChainable, 'createChainableImplementer') - -const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - -const errors = { - CODE: { - status: 404, - data: z.object({ why: z.string() }), - }, -} - -const mid = vi.fn() - -const builder = new BuilderWithMiddlewares({ - middlewares: [mid], - inputValidationIndex: 1, - outputValidationIndex: 1, - config: { - initialRoute: { - description: 'from initial', - }, - }, -}) - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('builderWithMiddlewares', () => { - it('.use', () => { - const mid2 = vi.fn() - const applied = builder.use(mid2) - expect(applied).toBeInstanceOf(BuilderWithMiddlewares) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].middlewares).toEqual([mid, mid2]) - expect(applied['~orpc'].inputValidationIndex).toEqual(2) - expect(applied['~orpc'].outputValidationIndex).toEqual(2) - expect(applied['~orpc'].config).toEqual({ initialRoute: { description: 'from initial' } }) - }) - - it('.errors', () => { - const applied = builder.errors(errors) - expect(applied).toBeInstanceOf(BuilderWithErrorsMiddlewares) - expect(applied['~orpc'].errorMap).toEqual(errors) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].config).toEqual({ initialRoute: { description: 'from initial' } }) - }) - - it('.route', () => { - const route = { path: '/test', method: 'GET' } as const - const applied = builder.route(route) - expect(applied).toBeInstanceOf(ProcedureBuilder) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ ...route, description: 'from initial' }) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - }) - - it('.input', () => { - const applied = builder.input(schema) - expect(applied).toBeInstanceOf(ProcedureBuilderWithInput) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toEqual(schema) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ description: 'from initial' }) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - }) - - it('.output', () => { - const applied = builder.output(schema) - expect(applied).toBeInstanceOf(ProcedureBuilderWithOutput) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toEqual(schema) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ description: 'from initial' }) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - }) - - it('.handler', () => { - const handler = vi.fn() - const applied = builder.handler(handler) - expect(applied).toBeInstanceOf(DecoratedProcedure) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ description: 'from initial' }) - expect(applied['~orpc'].handler).toEqual(handler) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - }) - - it('.prefix', () => { - const applied = builder.prefix('/test') - expect(applied).toBe(vi.mocked(RouterBuilder).mock.results[0]!.value) - expect(applied).toBeInstanceOf(RouterBuilder) - expect(RouterBuilder).toHaveBeenCalledWith(expect.objectContaining({ middlewares: [mid], prefix: '/test' })) - }) - - it('.tag', () => { - const applied = builder.tag('test', 'test2') - expect(applied).toBe(vi.mocked(RouterBuilder).mock.results[0]!.value) - expect(applied).toBeInstanceOf(RouterBuilder) - expect(RouterBuilder).toHaveBeenCalledWith(expect.objectContaining({ middlewares: [mid], tags: ['test', 'test2'] })) - }) - - it('.router', () => { - const router = { - ping: {} as any, - pong: {} as any, - } - - const applied = builder.router(router) - - expect(applied).toBe(unshiftMiddlewaresRouterSpy.mock.results[0]!.value) - expect(unshiftMiddlewaresRouterSpy).toHaveBeenCalledWith(router, expect.objectContaining({ middlewares: [mid] })) - }) - - it('.lazy', () => { - const router = { - ping: {} as any, - pong: {} as any, - } - - const applied = builder.lazy(() => Promise.resolve({ default: router })) - expect(applied).toBe(unshiftMiddlewaresRouterSpy.mock.results[0]!.value) - expect(unshiftMiddlewaresRouterSpy).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ middlewares: [mid] })) - expect(unlazy(unshiftMiddlewaresRouterSpy.mock.results[0]!.value)).resolves.toEqual({ default: router }) - }) - - it('.contract', () => { - const contract = oc.router({ - ping: oc.input(schema).output(schema), - }) - - const applied = builder.contract(contract) - - expect(applied).toBe(createChainableImplementerSpy.mock.results[0]!.value) - expect(createChainableImplementerSpy).toHaveBeenCalledWith(contract, expect.objectContaining({ - middlewares: [mid], - inputValidationIndex: 1, - outputValidationIndex: 1, - })) - }) -}) diff --git a/packages/server/src/builder-with-middlewares.ts b/packages/server/src/builder-with-middlewares.ts deleted file mode 100644 index a30b38c2..00000000 --- a/packages/server/src/builder-with-middlewares.ts +++ /dev/null @@ -1,161 +0,0 @@ -import type { ContractBuilderConfig, ContractRouter, ErrorMap, ErrorMapSuggestions, HTTPPath, Route, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' -import type { Middleware } from './middleware' -import type { ProcedureHandler } from './procedure' -import type { Router } from './router' -import type { UnshiftedMiddlewaresRouter } from './router-utils' -import { ContractProcedure, fallbackContractConfig } from '@orpc/contract' -import { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares' -import { type ChainableImplementer, createChainableImplementer } from './implementer-chainable' -import { flatLazy, type FlattenLazy, lazy } from './lazy' -import { ProcedureBuilder } from './procedure-builder' -import { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import { DecoratedProcedure } from './procedure-decorated' -import { RouterBuilder } from './router-builder' -import { unshiftMiddlewaresRouter } from './router-utils' - -/** - * `BuilderWithMiddlewares` is a branch of `Builder` which it has middlewares. - * - * Why? - * - prevents .middleware after .use (can mislead the behavior) - * - prevents .context after .use (middlewares required current context, so it tricky when change the current context) - * - */ -export interface BuilderWithMiddlewaresDef { - __initialContext?: TypeInitialContext - __currentContext?: TypeCurrentContext - config: ContractBuilderConfig - middlewares: Middleware[] - inputValidationIndex: number - outputValidationIndex: number -} - -export class BuilderWithMiddlewares { - '~type' = 'BuilderHasMiddlewares' as const - '~orpc': BuilderWithMiddlewaresDef - - constructor(def: BuilderWithMiddlewaresDef) { - this['~orpc'] = def - } - - use( - middleware: Middleware >, - ): ConflictContextGuard - & BuilderWithMiddlewares { - const builder = new BuilderWithMiddlewares({ - config: this['~orpc'].config, - inputValidationIndex: this['~orpc'].inputValidationIndex + 1, - outputValidationIndex: this['~orpc'].outputValidationIndex + 1, - middlewares: [...this['~orpc'].middlewares, middleware], - }) - - return builder as typeof builder & ConflictContextGuard - } - - errors( - errors: U, - ): BuilderWithErrorsMiddlewares { - return new BuilderWithErrorsMiddlewares({ - ...this['~orpc'], - errorMap: errors, - }) - } - - route(route: Route): ProcedureBuilder> { - return new ProcedureBuilder({ - ...this['~orpc'], - contract: new ContractProcedure({ - route: { - ...this['~orpc'].config.initialRoute, - ...route, - }, - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - }), - }) - } - - input( - schema: USchema, - example?: SchemaInput, - ): ProcedureBuilderWithInput> { - return new ProcedureBuilderWithInput({ - ...this['~orpc'], - contract: new ContractProcedure({ - route: fallbackContractConfig('defaultInitialRoute', this['~orpc'].config.initialRoute), - OutputSchema: undefined, - InputSchema: schema, - inputExample: example, - errorMap: {}, - }), - }) - } - - output( - schema: USchema, - example?: SchemaOutput, - ): ProcedureBuilderWithOutput> { - return new ProcedureBuilderWithOutput({ - ...this['~orpc'], - contract: new ContractProcedure({ - route: fallbackContractConfig('defaultInitialRoute', this['~orpc'].config.initialRoute), - InputSchema: undefined, - OutputSchema: schema, - outputExample: example, - errorMap: {}, - }), - }) - } - - handler( - handler: ProcedureHandler>, - ): DecoratedProcedure, Route> { - return new DecoratedProcedure({ - ...this['~orpc'], - contract: new ContractProcedure({ - route: fallbackContractConfig('defaultInitialRoute', this['~orpc'].config.initialRoute), - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - }), - handler, - }) - } - - prefix(prefix: HTTPPath): RouterBuilder> { - return new RouterBuilder({ - middlewares: this['~orpc'].middlewares, - errorMap: {}, - prefix, - }) - } - - tag(...tags: string[]): RouterBuilder> { - return new RouterBuilder({ - middlewares: this['~orpc'].middlewares, - errorMap: {}, - tags, - }) - } - - router>( - router: U, - ): UnshiftedMiddlewaresRouter { - return unshiftMiddlewaresRouter(router, this['~orpc']) - } - - lazy>( - loader: () => Promise<{ default: U }>, - ): UnshiftedMiddlewaresRouter, TInitialContext> { - return unshiftMiddlewaresRouter(flatLazy(lazy(loader)), this['~orpc']) - } - - contract>( - contract: U, - ): ChainableImplementer { - return createChainableImplementer(contract, this['~orpc']) - } -} diff --git a/packages/server/src/builder.test-d.ts b/packages/server/src/builder.test-d.ts index 1d61563d..db4b72e8 100644 --- a/packages/server/src/builder.test-d.ts +++ b/packages/server/src/builder.test-d.ts @@ -1,197 +1,425 @@ -import type { Route, StrictErrorMap } from '@orpc/contract' +import type { ContractProcedure, ErrorMap, MergedErrorMap, ORPCErrorConstructorMap, Schema } from '@orpc/contract' +import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../../contract/tests/shared' +import type { CurrentContext, InitialContext } from '../tests/shared' import type { Builder } from './builder' -import type { BuilderWithErrors } from './builder-with-errors' -import type { BuilderWithMiddlewares } from './builder-with-middlewares' +import type { BuilderWithMiddlewares, ProcedureBuilder, ProcedureBuilderWithInput, ProcedureBuilderWithOutput, RouterBuilder } from './builder-variants' import type { Context } from './context' -import type { ChainableImplementer } from './implementer-chainable' import type { Lazy } from './lazy' -import type { DecoratedLazy } from './lazy-decorated' import type { MiddlewareOutputFn } from './middleware' import type { DecoratedMiddleware } from './middleware-decorated' -import type { ANY_PROCEDURE, Procedure } from './procedure' -import type { ProcedureBuilder } from './procedure-builder' -import type { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import type { ProcedureBuilderWithOutput } from './procedure-builder-with-output' +import type { Procedure } from './procedure' import type { DecoratedProcedure } from './procedure-decorated' -import type { RouterBuilder } from './router-builder' -import { oc } from '@orpc/contract' -import { z } from 'zod' +import type { AdaptedRouter } from './router' +import { generalSchema } from '../../contract/tests/shared' +import { router } from '../tests/shared' + +const builder = {} as Builder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta +> -const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) +describe('Builder', () => { + it('is a contract procedure', () => { + expectTypeOf(builder).toMatchTypeOf< + ContractProcedure< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + }) -const errors = { - CODE: { - status: 404, - data: z.object({ why: z.string() }), - }, -} + it('.$config', () => { + const applied = builder.$config({ + initialInputValidationIndex: Number.NEGATIVE_INFINITY, + initialOutputValidationIndex: Number.POSITIVE_INFINITY, + }) -const builder = {} as Builder<{ db: string }> + expectTypeOf(applied).toEqualTypeOf< + Builder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() -describe('Builder', () => { - it('.context', () => { - expectTypeOf(builder.context()).toEqualTypeOf>() - expectTypeOf(builder.context<{ db: string, anything: string }>()).toEqualTypeOf>() + builder.$config({ + // @ts-expect-error - must be number + initialInputValidationIndex: 'INVALID', + }) + }) - // @ts-expect-error - new context must satisfy old context - builder.context<{ anything: string }>() + it('.$context', () => { + expectTypeOf(builder.$context()).toEqualTypeOf< + Builder< + Context, + Context, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + expectTypeOf(builder.$context<{ anything: string }>()).toEqualTypeOf< + Builder< + { anything: string }, + { anything: string }, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() }) - it('.config', () => { - expectTypeOf(builder.config({ initialRoute: { method: 'GET' } })).toEqualTypeOf>() + it('.$meta', () => { + expectTypeOf(builder.$meta<{ auth?: boolean }>({})).toEqualTypeOf< + Builder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + { auth?: boolean } + > + >() - // @ts-expect-error - invalid method - builder.config({ initialRoute: { method: 'HE' } }) + // @ts-expect-error - initial meta is required + builder.$meta<{ auth?: boolean }>() + // @ts-expect-error - auth is missing in initial meta + builder.$meta<{ auth: boolean }>({}) }) - it('.middleware', () => { - const mid = builder.middleware(({ context, next, path, procedure, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({ - context: { - extra: true, - }, - }) - }) - - expectTypeOf(mid).toEqualTypeOf< - DecoratedMiddleware<{ db: string }, { extra: boolean }, unknown, any, Record> + it('.$route', () => { + expectTypeOf(builder.$route({ method: 'GET' })).toEqualTypeOf< + Builder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > >() - const mid2 = builder.middleware(({ next }, input: 'input', output: MiddlewareOutputFn<'output'>) => next({})) + // @ts-expect-error - invalid method + builder.$route({ method: 'INVALID' }) + }) - expectTypeOf(mid2).toEqualTypeOf< - DecoratedMiddleware<{ db: string }, Record, 'input', 'output', Record> - >() + describe('.middleware', () => { + it('works', () => { + expectTypeOf( + builder.middleware(({ context, next, path, procedure, errors, signal }, input, output) => { + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }), + ).toEqualTypeOf< + DecoratedMiddleware, BaseMeta> + >() + + // @ts-expect-error --- conflict context + builder.middleware(({ next }) => next({ db: 123 })) + }) - // @ts-expect-error --- conflict context - builder.middleware(({ next }) => next({ db: 123 })) + it('can type input and output', () => { + expectTypeOf( + builder.middleware(({ next }, input: 'input', output: MiddlewareOutputFn<'output'>) => next()), + ).toEqualTypeOf< + DecoratedMiddleware, 'input', 'output', ORPCErrorConstructorMap, BaseMeta> + >() + }) }) it('.errors', () => { - expectTypeOf(builder.errors(errors)).toEqualTypeOf>>() + expectTypeOf( + builder.errors({ + BAD_GATEWAY: { message: 'BAD' }, + OVERRIDE: { message: 'OVERRIDE' }, + }), + ).toEqualTypeOf< + Builder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + MergedErrorMap, + BaseMeta + > + >() + + // @ts-expect-error - invalid schema + builder.errors({ BAD_GATEWAY: { data: {} } }) }) - it('.use', () => { - const applied = builder.use(({ context, next, path, procedure, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({ - context: { - extra: true, - }, + describe('.use', () => { + it('without map input', () => { + const applied = builder.use(({ context, next, path, procedure, errors, signal }, input, output) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) }) + + expectTypeOf(applied).toEqualTypeOf< + BuilderWithMiddlewares< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error --- conflict context + builder.use(({ next }) => next({ context: { db: 123 } })) + // conflict but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf() + // @ts-expect-error --- input is not match + builder.use(({ next }, input: 'invalid') => next({})) + // @ts-expect-error --- output is not match + builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + }) + + it('with map input', () => { + const applied = builder.use(({ context, next, path, procedure, errors, signal }, input: { mapped: boolean }, output) => { + expectTypeOf(input).toEqualTypeOf<{ mapped: boolean }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }, (input) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + + return { mapped: true } + }) + + expectTypeOf(applied).toEqualTypeOf< + BuilderWithMiddlewares< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + builder.use( + ({ context, next, path, procedure, errors, signal }, input: { mapped: boolean }, output) => next(), + // @ts-expect-error --- invalid map input + input => ({ invalid: true }), + ) + + // @ts-expect-error --- conflict context + builder.use(({ next }) => next({ context: { db: 123 } }), input => ({ mapped: true })) + // conflict but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }), input => ({ mapped: true }))).toMatchTypeOf() + // @ts-expect-error --- input is not match + builder.use(({ next }, input: 'invalid') => next({}), input => ({ mapped: true })) + // @ts-expect-error --- output is not match + builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}), input => ({ mapped: true })) }) + }) - expectTypeOf(applied).toEqualTypeOf>() + it('.meta', () => { + expectTypeOf(builder.meta({ log: true })).toEqualTypeOf< + ProcedureBuilder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() - // @ts-expect-error --- conflict context - builder.use(({ next }) => next({ context: { db: 123 } })) - // conflict but not detected - expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf() - // @ts-expect-error --- input is not match - builder.use(({ next }, input: 'invalid') => next({})) - // @ts-expect-error --- output is not match - builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + // @ts-expect-error - invalid meta + builder.meta({ log: 'INVALID' }) }) it('.route', () => { expectTypeOf(builder.route({ path: '/test', method: 'GET' })).toEqualTypeOf< - ProcedureBuilder<{ db: string }, { db: string }, Record> + ProcedureBuilder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > >() + + // @ts-expect-error - invalid method + builder.route({ method: 'INVALID' }) }) it('.input', () => { - expectTypeOf(builder.input(schema)).toEqualTypeOf< - ProcedureBuilderWithInput<{ db: string }, { db: string }, typeof schema, Record> + expectTypeOf(builder.input(generalSchema)).toEqualTypeOf< + ProcedureBuilderWithInput< + InitialContext, + CurrentContext, + typeof generalSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > >() + + // @ts-expect-error - invalid schema + builder.input({}) }) it('.output', () => { - expectTypeOf(builder.output(schema)).toEqualTypeOf< - ProcedureBuilderWithOutput<{ db: string }, { db: string }, typeof schema, Record> + expectTypeOf(builder.output(generalSchema)).toEqualTypeOf< + ProcedureBuilderWithOutput< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof generalSchema, + typeof baseErrorMap, + BaseMeta + > >() + + // @ts-expect-error - invalid schema + builder.output({}) }) it('.handler', () => { const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string }>() - expectTypeOf(procedure).toEqualTypeOf() + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() expectTypeOf(path).toEqualTypeOf() expectTypeOf(signal).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() - return 456 + return { output: 456 } }) expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure<{ db: string }, { db: string }, undefined, undefined, number, Record, Route> + DecoratedProcedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > >() }) it('.prefix', () => { expectTypeOf(builder.prefix('/test')).toEqualTypeOf< - RouterBuilder<{ db: string }, { db: string }, Record> + RouterBuilder >() + + // @ts-expect-error - invalid prefix + builder.prefix(123) }) it('.tag', () => { expectTypeOf(builder.tag('test', 'test2')).toEqualTypeOf< - RouterBuilder<{ db: string }, { db: string }, Record> + RouterBuilder >() }) it('.router', () => { - const router = { - ping: {} as Procedure<{ db: string }, { db: string }, undefined, undefined, unknown, typeof errors, Route>, - pong: {} as Procedure, Route>, - } - expectTypeOf(builder.router(router)).toEqualTypeOf< - typeof router + AdaptedRouter >() builder.router({ - // @ts-expect-error - context is not match - ping: {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown, typeof errors>, + // @ts-expect-error - initial context is not match + ping: {} as Procedure<{ invalid: true }, Context, undefined, undefined, unknown, Record, BaseMeta>, + }) + + builder.router({ + // @ts-expect-error - meta def is not match + ping: {} as Procedure< + Context, + Context, + undefined, + undefined, + unknown, + Record, + { invalid: true } + >, }) }) it('.lazy', () => { - const router = { - ping: {} as Procedure<{ db: string }, { db: string }, undefined, undefined, unknown, typeof errors, Route>, - pong: {} as Procedure, Route>, - } - expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf< - DecoratedLazy> + AdaptedRouter, InitialContext, typeof baseErrorMap> >() - // @ts-expect-error - context is not match - builder.lazy(() => Promise.resolve({ default: { - ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors, Route>, - } })) - }) - - it('.contract', () => { - const contract = oc.router({ - ping: oc.input(schema).output(schema), - }) - - expectTypeOf(builder.contract(contract)).toEqualTypeOf< - ChainableImplementer<{ db: string }, { db: string }, typeof contract> - >() + // @ts-expect-error - initial context is not match + builder.lazy(() => Promise.resolve({ + default: { + ping: {} as Procedure<{ invalid: true }, Context, undefined, undefined, unknown, Record, BaseMeta>, + }, + })) + + // @ts-expect-error - meta def is not match + builder.lazy(() => Promise.resolve({ + default: { + ping: {} as Procedure< + Context, + Context, + undefined, + undefined, + unknown, + Record, + { invalid: true } + >, + }, + })) }) }) diff --git a/packages/server/src/builder.test.ts b/packages/server/src/builder.test.ts index 02b03c1f..fcbe2101 100644 --- a/packages/server/src/builder.test.ts +++ b/packages/server/src/builder.test.ts @@ -1,165 +1,228 @@ -import { oc } from '@orpc/contract' -import { z } from 'zod' +import { isContractProcedure } from '@orpc/contract' +import { baseErrorMap, baseMeta, baseRoute, generalSchema, inputSchema, outputSchema } from '../../contract/tests/shared' +import { router } from '../tests/shared' import { Builder } from './builder' -import { BuilderWithErrors } from './builder-with-errors' -import { BuilderWithMiddlewares } from './builder-with-middlewares' -import * as implementerChainable from './implementer-chainable' import { unlazy } from './lazy' -import * as middlewareDecorated from './middleware-decorated' -import { ProcedureBuilder } from './procedure-builder' -import { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' +import * as MiddlewareDecorated from './middleware-decorated' import { DecoratedProcedure } from './procedure-decorated' -import { RouterBuilder } from './router-builder' +import * as Router from './router' -const decorateMiddlewareSpy = vi.spyOn(middlewareDecorated, 'decorateMiddleware') -const createChainableImplementerSpy = vi.spyOn(implementerChainable, 'createChainableImplementer') +const decorateMiddlewareSpy = vi.spyOn(MiddlewareDecorated, 'decorateMiddleware') +const adaptRouterSpy = vi.spyOn(Router, 'adaptRouter') -const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) +const mid = vi.fn() -const errors = { - CODE: { - status: 404, - data: z.object({ why: z.string() }), +const def = { + config: { + initialInputValidationIndex: 11, + initialOutputValidationIndex: 22, }, + middlewares: [mid], + errorMap: baseErrorMap, + inputSchema, + outputSchema, + inputValidationIndex: 99, + meta: baseMeta, + outputValidationIndex: 88, + route: baseRoute, + prefix: '/adapt' as const, + tags: ['adapt'], } -const config = { - initialRoute: { - description: 'from initial', - }, - initialInputValidationIndex: 99, - initialOutputValidationIndex: 99, -} +const builder = new Builder(def) -const builder = new Builder({ - config, +beforeEach(() => { + vi.clearAllMocks() }) describe('builder', () => { - it('.config', () => { - const applied = builder.config({ initialRoute: { method: 'GET' } }) + it('is a contract procedure', () => { + expect(builder).toSatisfy(isContractProcedure) + }) + + it('.$config', () => { + const config = { + initialInputValidationIndex: Number.NEGATIVE_INFINITY, + initialOutputValidationIndex: Number.POSITIVE_INFINITY, + } + const applied = builder.$config(config) + expect(applied).instanceOf(Builder) expect(applied).not.toBe(builder) - expect(applied['~orpc'].config).toEqual({ - ...config, - initialRoute: { method: 'GET' }, + expect(applied['~orpc']).toEqual({ + ...def, + config, + inputValidationIndex: Number.NEGATIVE_INFINITY, + outputValidationIndex: Number.POSITIVE_INFINITY, }) }) - it('.context', () => { - const applied = builder.context() - expect(applied).toBe(builder) + it('.$context', () => { + const applied = builder.$context() + + expect(applied).instanceOf(Builder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + middlewares: [], + inputValidationIndex: 11, + outputValidationIndex: 22, + }) + }) + + it('.$meta', () => { + const meta = { mode: 'test' } + const applied = builder.$meta(meta) + + expect(applied).instanceOf(Builder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + meta, + }) + }) + + it('.$route', () => { + const route = { method: 'GET', description: 'test' } as const + const applied = builder.$route(route) + + expect(applied).instanceOf(Builder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + route, + }) }) it('.middleware', () => { - const fn = vi.fn() - const mid = builder.middleware(fn) + const mid = vi.fn() + const applied = builder.middleware(mid) - expect(mid).toBe(decorateMiddlewareSpy.mock.results[0]!.value) - expect(decorateMiddlewareSpy).toHaveBeenCalledWith(fn) + expect(applied).toBe(decorateMiddlewareSpy.mock.results[0]?.value) + expect(decorateMiddlewareSpy).toBeCalledTimes(1) + expect(decorateMiddlewareSpy).toBeCalledWith(mid) }) it('.errors', () => { + const errors = { BAD_GATEWAY: { message: 'BAD' } } + const applied = builder.errors(errors) - expect(applied).toBeInstanceOf(BuilderWithErrors) - expect(applied['~orpc'].errorMap).toEqual(errors) - expect(applied['~orpc'].config).toEqual(config) + expect(applied).instanceOf(Builder) + expect(applied).not.toBe(builder) + + expect(applied['~orpc']).toEqual({ + ...def, + errorMap: { ...def.errorMap, ...errors }, + }) }) it('.use', () => { - const mid = vi.fn() - const applied = builder.use(mid) - expect(applied).toBeInstanceOf(BuilderWithMiddlewares) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].config).toEqual(config) - expect(applied['~orpc'].inputValidationIndex).toEqual(100) - expect(applied['~orpc'].outputValidationIndex).toEqual(100) + const mid2 = vi.fn() + const applied = builder.use(mid2) + + expect(applied).instanceOf(Builder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + middlewares: [mid, mid2], + }) + }) + + it('.meta', () => { + const meta = { log: true } as any + const applied = builder.meta(meta) + + expect(applied).instanceOf(Builder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + meta: { ...def.meta, ...meta }, + }) }) it('.route', () => { - const route = { path: '/test', method: 'GET' } as const + const route = { description: 'test' } as any const applied = builder.route(route) - expect(applied).toBeInstanceOf(ProcedureBuilder) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ ...route, description: 'from initial' }) - expect(applied['~orpc'].inputValidationIndex).toEqual(99) - expect(applied['~orpc'].outputValidationIndex).toEqual(99) + + expect(applied).instanceOf(Builder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + route: { ...def.route, ...route }, + }) }) it('.input', () => { - const applied = builder.input(schema) - expect(applied).toBeInstanceOf(ProcedureBuilderWithInput) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toEqual(schema) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ description: 'from initial' }) - expect(applied['~orpc'].inputValidationIndex).toEqual(99) - expect(applied['~orpc'].outputValidationIndex).toEqual(99) + const applied = builder.input(generalSchema) + + expect(applied).instanceOf(Builder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + inputSchema: generalSchema, + inputValidationIndex: 12, + }) }) it('.output', () => { - const applied = builder.output(schema) - expect(applied).toBeInstanceOf(ProcedureBuilderWithOutput) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toEqual(schema) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ description: 'from initial' }) - expect(applied['~orpc'].inputValidationIndex).toEqual(99) - expect(applied['~orpc'].outputValidationIndex).toEqual(99) + const applied = builder.output(generalSchema) + + expect(applied).instanceOf(Builder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + outputSchema: generalSchema, + outputValidationIndex: 23, + }) }) it('.handler', () => { const handler = vi.fn() const applied = builder.handler(handler) - expect(applied).toBeInstanceOf(DecoratedProcedure) - expect(applied['~orpc'].contract['~orpc'].route).toEqual({ description: 'from initial' }) - expect(applied['~orpc'].handler).toEqual(handler) - expect(applied['~orpc'].inputValidationIndex).toEqual(99) - expect(applied['~orpc'].outputValidationIndex).toEqual(99) + + expect(applied).instanceOf(DecoratedProcedure) + expect(applied['~orpc']).toEqual({ + ...def, + handler, + }) }) it('.prefix', () => { const applied = builder.prefix('/test') - expect(applied).toBeInstanceOf(RouterBuilder) - expect(applied['~orpc'].prefix).toEqual('/test') + + expect(applied).instanceOf(Builder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + prefix: '/adapt/test', + }) }) it('.tag', () => { - const applied = builder.tag('test', 'test2') - expect(applied).toBeInstanceOf(RouterBuilder) - expect(applied['~orpc'].tags).toEqual(['test', 'test2']) + const applied = builder.tag('test') + + expect(applied).instanceOf(Builder) + expect(applied).not.toBe(builder) + expect(applied['~orpc']).toEqual({ + ...def, + tags: ['adapt', 'test'], + }) }) it('.router', () => { - const router = { - ping: {} as any, - pong: {} as any, - } - - const applied = builder.router(router) + const applied = builder.router(router as any) - expect(applied).toBe(router) + expect(applied).toBe(adaptRouterSpy.mock.results[0]?.value) + expect(adaptRouterSpy).toBeCalledTimes(1) + expect(adaptRouterSpy).toBeCalledWith(router, def) }) it('.lazy', () => { - const router = { - ping: {} as any, - pong: {} as any, - } - - const applied = builder.lazy(() => Promise.resolve({ default: router })) + const applied = builder.lazy(() => Promise.resolve({ default: router as any })) - expect(unlazy(applied)).resolves.toEqual({ default: router }) - }) - - it('.contract', () => { - const contract = oc.router({ - ping: oc.input(schema).output(schema), - }) - - const applied = builder.contract(contract) - - expect(applied).toBe(createChainableImplementerSpy.mock.results[0]!.value) - expect(createChainableImplementerSpy).toHaveBeenCalledWith(contract, { - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, - }) + expect(applied).toBe(adaptRouterSpy.mock.results[0]?.value) + expect(adaptRouterSpy).toBeCalledTimes(1) + expect(adaptRouterSpy).toBeCalledWith(expect.any(Object), def) + expect(unlazy(adaptRouterSpy.mock.calls[0]![0])).resolves.toEqual({ default: router }) }) }) diff --git a/packages/server/src/builder.ts b/packages/server/src/builder.ts index b4846ab6..5bc6e1b5 100644 --- a/packages/server/src/builder.ts +++ b/packages/server/src/builder.ts @@ -1,186 +1,238 @@ -import type { ContractBuilderConfig, ContractRouter, ErrorMap, ErrorMapSuggestions, HTTPPath, Route, Schema, SchemaInput, SchemaOutput, StrictErrorMap } from '@orpc/contract' -import type { ConflictContextGuard, Context, TypeInitialContext } from './context' -import type { DecoratedLazy } from './lazy-decorated' -import type { Middleware } from './middleware' +import type { ContractProcedureDef, ContractRouter, ErrorMap, HTTPPath, MergedErrorMap, Meta, ORPCErrorConstructorMap, Route, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { BuilderWithMiddlewares, ProcedureBuilder, ProcedureBuilderWithInput, ProcedureBuilderWithOutput, RouterBuilder } from './builder-variants' +import type { ConflictContextGuard, Context, MergedContext } from './context' +import type { FlattenLazy } from './lazy-utils' +import type { AnyMiddleware, MapInputMiddleware, Middleware } from './middleware' import type { DecoratedMiddleware } from './middleware-decorated' import type { ProcedureHandler } from './procedure' -import type { Router } from './router' -import { ContractProcedure, fallbackContractConfig } from '@orpc/contract' -import { BuilderWithErrors } from './builder-with-errors' -import { BuilderWithMiddlewares } from './builder-with-middlewares' +import type { AdaptedRouter, AdaptRouterOptions, Router } from './router' +import { mergeErrorMap, mergeMeta, mergePrefix, mergeRoute, mergeTags } from '@orpc/contract' import { fallbackConfig } from './config' -import { type ChainableImplementer, createChainableImplementer } from './implementer-chainable' -import { flatLazy, type FlattenLazy, lazy } from './lazy' -import { decorateLazy } from './lazy-decorated' +import { lazy } from './lazy' +import { flatLazy } from './lazy-utils' import { decorateMiddleware } from './middleware-decorated' -import { ProcedureBuilder } from './procedure-builder' -import { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' +import { addMiddleware } from './middleware-utils' import { DecoratedProcedure } from './procedure-decorated' -import { RouterBuilder } from './router-builder' +import { adaptRouter } from './router' -export interface BuilderConfig extends ContractBuilderConfig { +export interface BuilderConfig { initialInputValidationIndex?: number initialOutputValidationIndex?: number } -export interface BuilderDef { - __initialContext?: TypeInitialContext +export interface BuilderDef< + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> extends ContractProcedureDef, AdaptRouterOptions< TErrorMap> { + middlewares: AnyMiddleware[] + inputValidationIndex: number + outputValidationIndex: number config: BuilderConfig } -export class Builder { - '~type' = 'Builder' as const - '~orpc': BuilderDef - - constructor(def: BuilderDef) { +export class Builder< + TInitialContext extends Context, + TCurrentContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> { + '~orpc': BuilderDef + + constructor(def: BuilderDef) { this['~orpc'] = def } - config(config: ContractBuilderConfig): Builder { + /** + * Reset config + */ + $config(config: BuilderConfig): Builder { + const inputValidationCount = this['~orpc'].inputValidationIndex - fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex) + const outputValidationCount = this['~orpc'].outputValidationIndex - fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex) + return new Builder({ ...this['~orpc'], - config: { - ...this['~orpc'].config, - ...config, - }, + config, + inputValidationIndex: fallbackConfig('initialInputValidationIndex', config.initialInputValidationIndex) + inputValidationCount, + outputValidationIndex: fallbackConfig('initialOutputValidationIndex', config.initialOutputValidationIndex) + outputValidationCount, }) } - context(): Builder { - return this as any // just change at type level so safely cast here + /** + * Reset initial context + */ + $context(): Builder { + return new Builder({ + ...this['~orpc'], + middlewares: [], + inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), + outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex), + }) } - middleware( - middleware: Middleware>, - ): DecoratedMiddleware> { + /** + * Reset initial meta + */ + $meta( + initialMeta: U, + ): Builder { + return new Builder({ + ...this['~orpc'], + meta: initialMeta, + }) + } + + /** + * Reset initial route + */ + $route( + initialRoute: Route, + ): Builder { + return new Builder({ + ...this['~orpc'], + route: initialRoute, + }) + } + + middleware( // = any here is important to make middleware can be used in any output by default + middleware: Middleware, TMeta>, + ): DecoratedMiddleware, TMeta> { // ORPCErrorConstructorMap ensures middleware can used in any procedure return decorateMiddleware(middleware) } - errors(errors: U): BuilderWithErrors> { - return new BuilderWithErrors({ + errors( + errors: U, + ): Builder, TMeta > { + return new Builder({ ...this['~orpc'], - errorMap: errors as StrictErrorMap, + errorMap: mergeErrorMap(this['~orpc'].errorMap, errors), }) } use( - middleware: Middleware>, - ): ConflictContextGuard - & BuilderWithMiddlewares { - const builder = new BuilderWithMiddlewares({ + middleware: Middleware< + TCurrentContext, + UOutContext, + SchemaOutput, + SchemaInput, + ORPCErrorConstructorMap, + TMeta + >, + ): ConflictContextGuard> & + BuilderWithMiddlewares, TInputSchema, TOutputSchema, TErrorMap, TMeta> + + use( + middleware: Middleware< + TCurrentContext, + UOutContext, + UInput, + SchemaInput, + ORPCErrorConstructorMap, + TMeta + >, + mapInput: MapInputMiddleware, UInput>, + ): ConflictContextGuard> & + BuilderWithMiddlewares, TInputSchema, TOutputSchema, TErrorMap, TMeta> + + use( + middleware: AnyMiddleware, + mapInput?: MapInputMiddleware, + ): any { + const mapped = mapInput + ? decorateMiddleware(middleware).mapInput(mapInput) + : middleware + + return new Builder({ ...this['~orpc'], - inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex) + 1, - outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex) + 1, - middlewares: [middleware as any], // FIXME: I believe we can remove `as any` here + middlewares: addMiddleware(this['~orpc'].middlewares, mapped), }) + } - return builder as typeof builder & ConflictContextGuard + meta( + meta: TMeta, + ): ProcedureBuilder { + return new Builder({ + ...this['~orpc'], + meta: mergeMeta(this['~orpc'].meta, meta), + }) } - route(route: Route): ProcedureBuilder> { - return new ProcedureBuilder({ - middlewares: [], - inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), - outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex), - contract: new ContractProcedure({ - route: { - ...this['~orpc'].config.initialRoute, - ...route, - }, - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - }), + route( + route: Route, + ): ProcedureBuilder { + return new Builder({ + ...this['~orpc'], + route: mergeRoute(this['~orpc'].route, route), }) } input( schema: USchema, - example?: SchemaInput, - ): ProcedureBuilderWithInput> { - return new ProcedureBuilderWithInput({ - middlewares: [], - inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), - outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex), - contract: new ContractProcedure({ - route: fallbackContractConfig('defaultInitialRoute', this['~orpc'].config.initialRoute), - OutputSchema: undefined, - InputSchema: schema, - inputExample: example, - errorMap: {}, - }), + ): ProcedureBuilderWithInput { + return new Builder({ + ...this['~orpc'], + inputSchema: schema, + inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex) + this['~orpc'].middlewares.length, }) } output( schema: USchema, - example?: SchemaOutput, - ): ProcedureBuilderWithOutput> { - return new ProcedureBuilderWithOutput({ - middlewares: [], - inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), - outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex), - contract: new ContractProcedure({ - route: fallbackContractConfig('defaultInitialRoute', this['~orpc'].config.initialRoute), - InputSchema: undefined, - OutputSchema: schema, - outputExample: example, - errorMap: {}, - }), + ): ProcedureBuilderWithOutput { + return new Builder({ + ...this['~orpc'], + outputSchema: schema, + outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex) + this['~orpc'].middlewares.length, }) } - handler( - handler: ProcedureHandler>, - ): DecoratedProcedure, Route> { + handler>( + handler: ProcedureHandler, + ): DecoratedProcedure { return new DecoratedProcedure({ - middlewares: [], - inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), - outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex), - contract: new ContractProcedure({ - route: fallbackContractConfig('defaultInitialRoute', this['~orpc'].config.initialRoute), - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - }), + ...this['~orpc'], handler, }) } - prefix(prefix: HTTPPath): RouterBuilder> { - return new RouterBuilder({ - middlewares: [], - errorMap: {}, - prefix, - }) + prefix( + prefix: HTTPPath, + ): RouterBuilder { + return new Builder({ + ...this['~orpc'], + prefix: mergePrefix(this['~orpc'].prefix, prefix), + }) as any } - tag(...tags: string[]): RouterBuilder> { - return new RouterBuilder({ - middlewares: [], - errorMap: {}, - tags, - }) + tag(...tags: string[]): RouterBuilder { + return new Builder({ + ...this['~orpc'], + tags: mergeTags(this['~orpc'].tags, tags), + }) as any } - router>(router: U): U { - return router + router>>(router: U): AdaptedRouter { + return adaptRouter(router, this['~orpc']) } - lazy>( + lazy>>( loader: () => Promise<{ default: U }>, - ): DecoratedLazy> { - return decorateLazy(flatLazy(lazy(loader))) - } - - contract>( - contract: U, - ): ChainableImplementer { - return createChainableImplementer(contract, { - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, - }) + ): AdaptedRouter, TInitialContext, TErrorMap> { + return adaptRouter(flatLazy(lazy(loader)), this['~orpc']) } } + +export const os = new Builder({ + config: {}, + route: {}, + meta: {}, + errorMap: {}, + inputSchema: undefined, + outputSchema: undefined, + inputValidationIndex: fallbackConfig('initialInputValidationIndex'), + outputValidationIndex: fallbackConfig('initialOutputValidationIndex'), + middlewares: [], +}) diff --git a/packages/server/src/context.test-d.ts b/packages/server/src/context.test-d.ts new file mode 100644 index 00000000..d5ed70d5 --- /dev/null +++ b/packages/server/src/context.test-d.ts @@ -0,0 +1,12 @@ +import type { ConflictContextGuard, MergedContext } from './context' + +it('MergedContext', () => { + expectTypeOf>().toMatchTypeOf<{ a: string, b: number }>() + expectTypeOf>().toMatchTypeOf<{ a: never }>() +}) + +it('ConflictContextGuard', () => { + expectTypeOf>>().toEqualTypeOf() + expectTypeOf>>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() +}) diff --git a/packages/server/src/context.test.ts b/packages/server/src/context.test.ts new file mode 100644 index 00000000..87699ec4 --- /dev/null +++ b/packages/server/src/context.test.ts @@ -0,0 +1,6 @@ +import { mergeContext } from './context' + +it('mergeContext', () => { + expect(mergeContext({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 }) + expect(mergeContext({ a: 1 }, { a: 2 })).toEqual({ a: 2 }) +}) diff --git a/packages/server/src/context.ts b/packages/server/src/context.ts index 1577b7a6..69fa71eb 100644 --- a/packages/server/src/context.ts +++ b/packages/server/src/context.ts @@ -2,9 +2,16 @@ import type { IsNever } from '@orpc/shared' export type Context = Record -export type TypeInitialContext = (type: T) => any +export type TypeInitialContext = (type: T) => unknown -export type TypeCurrentContext = { type: T } +export type MergedContext = T & U + +export function mergeContext( + context: T, + other: U, +): MergedContext { + return { ...context, ...other } +} export type ConflictContextGuard = true extends IsNever | { [K in keyof T]: IsNever }[keyof T] diff --git a/packages/server/src/error.test-d.ts b/packages/server/src/error.test-d.ts deleted file mode 100644 index 237f7ac8..00000000 --- a/packages/server/src/error.test-d.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { ORPCError } from '@orpc/contract' -import type { ORPCErrorConstructorMap } from './error' -import { z } from 'zod' - -const schema1 = z.object({ val: z.string().transform(v => Number(v)) }) -const schema2 = z.object({ why: z.string() }) - -describe('ORPCErrorConstructorMap', () => { - it('works with undefined', () => { - expectTypeOf>>().toEqualTypeOf>() - }) - - it('works with error map', () => { - const constructors = {} as ORPCErrorConstructorMap<{ - BAD_GATEWAY: { - status: 502 - message: 'Bad Gateway' - data: typeof schema1 - } - UNAUTHORIZED: { - status: 401 - message: 'Unauthorized' - data: typeof schema2 - } - PAYMENT_REQUIRED: { - status: 402 - } - }> - - expectTypeOf(constructors.BAD_GATEWAY({ data: { val: '123' } })).toEqualTypeOf>() - expectTypeOf(constructors.UNAUTHORIZED({ data: { why: '123' } })).toEqualTypeOf>() - expectTypeOf(constructors.PAYMENT_REQUIRED({})).toEqualTypeOf>() - expectTypeOf(constructors.PAYMENT_REQUIRED()).toEqualTypeOf>() - - // @ts-expect-error - invalid data - constructors.BAD_GATEWAY({ data: { val: 123 } }) - // @ts-expect-error - data is required - constructors.BAD_GATEWAY({ data: {} }) - // @ts-expect-error - required options - constructors.BAD_GATEWAY() - }) -}) diff --git a/packages/server/src/error.test.ts b/packages/server/src/error.test.ts deleted file mode 100644 index da78ddea..00000000 --- a/packages/server/src/error.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ORPCError } from '@orpc/contract' -import { z } from 'zod' -import { createORPCErrorConstructorMap } from './error' - -const baseErrors = { - BAD_GATEWAY: { - message: '__message__', - data: z.object({ - val: z.string().transform(v => Number(v)), - }), - }, - UNAUTHORIZED: { - status: 499, - message: '__message__', - data: z.object({ - why: z.string(), - }), - }, - PAYMENT_REQUIRED: {}, -} as const - -describe('createORPCErrorConstructorMap', () => { - const constructors = createORPCErrorConstructorMap(baseErrors) - - it('create ORPC Error', () => { - expect(constructors.BAD_GATEWAY({ data: { val: '123' } })).toBeInstanceOf(ORPCError) - expect(constructors.BAD_GATEWAY({ data: { val: '123' }, cause: '__cause__', message: '__message__' })).toMatchObject({ - defined: true, - code: 'BAD_GATEWAY', - status: 502, - message: '__message__', - data: { val: '123' }, - cause: '__cause__', - }) - }) - - it('fallback message', () => { - const error = constructors.BAD_GATEWAY({ data: { val: '123' } }) - - expect(error.message).toBe('__message__') - }) - - it('inherit status', () => { - const error = constructors.UNAUTHORIZED({ data: { why: '123' } }) - expect(error.status).toBe(499) - }) -}) diff --git a/packages/server/src/error.ts b/packages/server/src/error.ts deleted file mode 100644 index 069508b7..00000000 --- a/packages/server/src/error.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { ErrorMap, ErrorMapItem, ORPCErrorOptions, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import { ORPCError } from '@orpc/contract' - -export type ORPCErrorConstructorMapItemOptions = Omit, 'defined' | 'code' | 'status'> - -export type ORPCErrorConstructorMapItemRest = - | [options: ORPCErrorConstructorMapItemOptions] - | (undefined extends TData ? [] : never) - -export type ORPCErrorConstructorMapItem = - (...rest: ORPCErrorConstructorMapItemRest>) => ORPCError> - -export type ORPCErrorConstructorMap = { - [K in keyof T]: K extends string - ? T[K] extends ErrorMapItem - ? ORPCErrorConstructorMapItem - : never - : never -} - -export function createORPCErrorConstructorMap(errors: T): ORPCErrorConstructorMap { - const constructors = {} as ORPCErrorConstructorMap - - for (const code in errors) { - const config = errors[code] - - if (!config) { - continue - } - - const constructor: ORPCErrorConstructorMapItem = (...[options]) => { - return new ORPCError({ - code, - defined: true, - status: config.status, - message: options?.message ?? config.message, - data: options?.data, - cause: options?.cause, - }) - } - - constructors[code] = constructor as any - } - - return constructors -} diff --git a/packages/server/src/hidden.test.ts b/packages/server/src/hidden.test.ts index 8668a3af..7e9706e0 100644 --- a/packages/server/src/hidden.test.ts +++ b/packages/server/src/hidden.test.ts @@ -1,62 +1,99 @@ -import { deepSetLazyRouterPrefix, getLazyRouterPrefix } from './hidden' +import { oc } from '@orpc/contract' +import { router } from '../tests/shared' +import { deepSetLazyRouterPrefix, getLazyRouterPrefix, getRouterContract, setRouterContract } from './hidden' +import { lazy, unlazy } from './lazy' +import { createAccessibleLazyRouter } from './router-accessible-lazy' -describe('deepSetLazyRouterPrefix', () => { - it('sets prefix on root object', () => { - const obj = { value: 1 } - const prefixed = deepSetLazyRouterPrefix(obj, '/api') - expect(getLazyRouterPrefix(prefixed)).toBe('/api') - expect(prefixed.value).toBe(1) +describe('setRouterContract', () => { + const ping = oc.route({}) + const baseContract = { ping } + const nestedContract = { ping, nested: { ping } } + + it('sets contract on empty object', () => { + const obj = {} + const router = setRouterContract(obj, baseContract) + expect(getRouterContract(router)).toBe(baseContract) }) - it('sets prefix on all nested objects', () => { - const obj = { - l1: { - l2: { - l3: { value: 42 }, + it('preserves original object properties', () => { + const obj = { existingProp: 'value' } as any + const router = setRouterContract(obj, baseContract) + expect(router.existingProp).toBe('value') + expect(getRouterContract(router)).toBe(baseContract) + }) + + it('handles nested contracts', () => { + const obj = { nested: { value: 42 } } as any + const router = setRouterContract(obj, nestedContract) + expect(getRouterContract(router)).toBe(nestedContract) + expect(router.nested.value).toBe(42) + expect(getRouterContract(router.nested)).toBeUndefined() + }) + + it('allows contract overwriting', () => { + const obj = {} + const router1 = setRouterContract(obj, baseContract) + const router2 = setRouterContract(router1, nestedContract) + expect(getRouterContract(router2)).toBe(nestedContract) + }) +}) + +describe('deepSetLazyRouterPrefix', () => { + it('prefix on root and nested lazy', async () => { + const obj = createAccessibleLazyRouter(lazy(() => Promise.resolve({ + default: { + l1: { + l2: { + l3: { value: router.ping }, + }, }, }, - } + }))) const prefixed = deepSetLazyRouterPrefix(obj, '/api') expect(getLazyRouterPrefix(prefixed)).toBe('/api') expect(getLazyRouterPrefix(prefixed.l1)).toBe('/api') expect(getLazyRouterPrefix(prefixed.l1.l2)).toBe('/api') expect(getLazyRouterPrefix(prefixed.l1.l2.l3)).toBe('/api') - expect(prefixed.l1.l2.l3.value).toBe(42) + expect(getLazyRouterPrefix(prefixed.l1.l2.l3.value)).toBe('/api') + expect(await unlazy(prefixed.l1.l2.l3.value)).toEqual(await unlazy(router.ping)) }) - it('handles functions in objects', () => { - const obj = { - fn: () => 42, - nested: { fn: () => 43 }, - } - const prefixed = deepSetLazyRouterPrefix(obj, '/api') - expect(getLazyRouterPrefix(prefixed.fn)).toBe('/api') - expect(getLazyRouterPrefix(prefixed.nested.fn)).toBe('/api') - expect(prefixed.fn()).toBe(42) - expect(prefixed.nested.fn()).toBe(43) - }) + it('can override old prefix', async () => { + const obj = createAccessibleLazyRouter(lazy(() => Promise.resolve({ + default: { + l1: { + l2: { + l3: { value: router.ping }, + }, + }, + }, + }))) - it('allows prefix override', () => { - const obj = { value: 1 } const prefixed1 = deepSetLazyRouterPrefix(obj, '/api') const prefixed2 = deepSetLazyRouterPrefix(prefixed1, '/v2') expect(getLazyRouterPrefix(prefixed1)).toBe('/api') + expect(getLazyRouterPrefix(prefixed1.l1.l2.l3.value)).toBe('/api') expect(getLazyRouterPrefix(prefixed2)).toBe('/v2') - expect(prefixed2.value).toBe(1) + expect(getLazyRouterPrefix(prefixed2.l1.l2.l3.value)).toBe('/v2') + + expect(await unlazy(obj)).toEqual(await unlazy(obj)) }) - it('handles nested prefix override', () => { + it('not prefix on non-lazy', () => { const obj = { - l1: { value: 1 }, - l2: { value: 2 }, - } - const prefixed1 = deepSetLazyRouterPrefix(obj, '/api') - const prefixed2 = deepSetLazyRouterPrefix(prefixed1.l1, '/v2') + l1: { + l2: { + l3: { value: router.ping }, + value: 1, // not lazy + }, + }, + } as any - expect(getLazyRouterPrefix(prefixed1)).toBe('/api') - expect(getLazyRouterPrefix(prefixed1.l2)).toBe('/api') - expect(getLazyRouterPrefix(prefixed2)).toBe('/v2') - expect(prefixed2.value).toBe(1) + const prefixed = deepSetLazyRouterPrefix(obj, '/api') as any + + expect(getLazyRouterPrefix(prefixed)).toBe('/api') + expect(getLazyRouterPrefix(prefixed.l1)).toBe(undefined) + expect(getLazyRouterPrefix(prefixed.l1.l2.value)).toBe(undefined) }) }) diff --git a/packages/server/src/hidden.ts b/packages/server/src/hidden.ts index fcbfe5d2..7747c8ab 100644 --- a/packages/server/src/hidden.ts +++ b/packages/server/src/hidden.ts @@ -1,13 +1,34 @@ -import type { HTTPPath } from '@orpc/contract' +import type { AnyContractRouter, ContractRouter, HTTPPath } from '@orpc/contract' +import type { Lazy } from './lazy' +import type { AnyRouter } from './router' +import { isLazy } from './lazy' + +const ROUTER_CONTRACT_SYMBOL = Symbol('ORPC_ROUTER_CONTRACT') + +export function setRouterContract(obj: T, contract: AnyContractRouter): T { + return new Proxy(obj, { + get(target, key) { + if (key === ROUTER_CONTRACT_SYMBOL) { + return contract + } + + return Reflect.get(target, key) + }, + }) +} + +export function getRouterContract(obj: object): ContractRouter | undefined { + return (obj as any)[ROUTER_CONTRACT_SYMBOL] +} const LAZY_ROUTER_PREFIX_SYMBOL = Symbol('ORPC_LAZY_ROUTER_PREFIX') -export function deepSetLazyRouterPrefix(router: T, prefix: HTTPPath): T { +export function deepSetLazyRouterPrefix>(router: T, prefix: HTTPPath): T { return new Proxy(router, { get(target, key) { if (key !== LAZY_ROUTER_PREFIX_SYMBOL) { const val = Reflect.get(target, key) - if (val && (typeof val === 'object' || typeof val === 'function')) { + if (isLazy(val)) { return deepSetLazyRouterPrefix(val, prefix) } diff --git a/packages/server/src/implementer-chainable.test-d.ts b/packages/server/src/implementer-chainable.test-d.ts deleted file mode 100644 index 17149c27..00000000 --- a/packages/server/src/implementer-chainable.test-d.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { MergeRoute, StrictRoute } from '@orpc/contract' -import type { Context, TypeCurrentContext, TypeInitialContext } from './context' -import type { ChainableImplementer } from './implementer-chainable' -import type { Middleware } from './middleware' -import type { ProcedureImplementer } from './procedure-implementer' -import type { RouterImplementer } from './router-implementer' -import { oc } from '@orpc/contract' -import { z } from 'zod' -import { createChainableImplementer } from './implementer-chainable' - -const schema = z.object({ val: z.string().transform(val => Number(val)) }) - -const route = { method: 'GET', path: '/ping' } as const -const ping = oc.input(schema).output(schema) -const pong = oc.route(route) - -const contract = oc.router({ - ping, - pong, - nested: { - ping, - pong, - }, -}) - -describe('ChainableImplementer', () => { - it('with procedure', () => { - expectTypeOf(createChainableImplementer(ping, { middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0 })).toEqualTypeOf< - ProcedureImplementer, StrictRoute>> - >() - - expectTypeOf(createChainableImplementer(pong, { middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0 })).toEqualTypeOf< - ProcedureImplementer, MergeRoute>, typeof route>> - >() - }) - - it('with router', () => { - const implementer = createChainableImplementer(contract, { middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0 }) - - expectTypeOf(implementer).toMatchTypeOf< - Omit, '~type' | '~orpc'> - >() - - expectTypeOf(implementer.ping).toEqualTypeOf< - ProcedureImplementer, StrictRoute>> - >() - - expectTypeOf(implementer.pong).toEqualTypeOf< - ProcedureImplementer, MergeRoute>, typeof route>> - >() - - expectTypeOf(implementer.nested).toMatchTypeOf< - Omit, '~type' | '~orpc'> - >() - - expectTypeOf(implementer.nested.ping).toEqualTypeOf< - ProcedureImplementer, StrictRoute>> - >() - - expectTypeOf(implementer.nested.pong).toEqualTypeOf< - ProcedureImplementer, MergeRoute>, typeof route>> - >() - }) - - it('not expose properties of router implementer', () => { - const implementer = createChainableImplementer(contract, { middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0 }) - - expectTypeOf(implementer).not.toHaveProperty('~orpc') - expectTypeOf(implementer).not.toHaveProperty('~type') - expectTypeOf(implementer.router).not.toHaveProperty('~orpc') - expectTypeOf(implementer.router).not.toHaveProperty('~type') - }) - - it('works on conflicted', () => { - const contract = oc.router({ - use: ping, - router: { - use: ping, - router: pong, - }, - }) - - const implementer = createChainableImplementer(contract, { middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0 }) - - expectTypeOf(implementer).toMatchTypeOf< - Omit, '~type' | '~orpc'> - >() - - expectTypeOf(implementer.use).toMatchTypeOf< - ProcedureImplementer, Record> - >() - - expectTypeOf(implementer.router).toMatchTypeOf< - Omit, '~type' | '~orpc'> - >() - - expectTypeOf(implementer.router.use).toMatchTypeOf< - ProcedureImplementer, Record> - >() - - expectTypeOf(implementer.router.router).toMatchTypeOf< - ProcedureImplementer, Record & typeof route> - >() - }) -}) - -describe('createChainableImplementer', () => { - it('with procedure', () => { - const implementer = createChainableImplementer(ping, { middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0 }) - expectTypeOf(implementer).toEqualTypeOf>() - }) - - it('with router', () => { - const implementer = createChainableImplementer(contract, { middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0 }) - expectTypeOf(implementer).toEqualTypeOf>() - }) - - it('with middlewares', () => { - const mid = {} as Middleware<{ auth: boolean }, { db: string }, unknown, unknown, Record> - const implementer = createChainableImplementer(contract, { - __initialContext: {} as TypeInitialContext<{ auth: boolean }>, - __currentContext: {} as TypeCurrentContext<{ auth: boolean } & { db: string }>, - middlewares: [mid], - inputValidationIndex: 1, - outputValidationIndex: 1, - }) - expectTypeOf(implementer).toEqualTypeOf< - ChainableImplementer<{ auth: boolean }, { auth: boolean } & { db: string }, typeof contract> - >() - }) -}) diff --git a/packages/server/src/implementer-chainable.test.ts b/packages/server/src/implementer-chainable.test.ts deleted file mode 100644 index 06cd5102..00000000 --- a/packages/server/src/implementer-chainable.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { oc } from '@orpc/contract' -import { z } from 'zod' -import { createChainableImplementer } from './implementer-chainable' -import { ProcedureImplementer } from './procedure-implementer' -import { RouterImplementer } from './router-implementer' - -describe('createChainableImplementer', () => { - const schema = z.object({ val: z.string().transform(val => Number(val)) }) - - const ping = oc.input(schema).output(schema) - const pong = oc.route({ method: 'GET', path: '/ping' }) - - const contract = { - ping, - pong, - nested: { - ping, - pong, - }, - } - - const mid1 = vi.fn() - const mid2 = vi.fn() - const mid3 = vi.fn() - - it('with procedure', () => { - const implementer = createChainableImplementer(ping, { - middlewares: [mid1, mid2], - inputValidationIndex: 2, - outputValidationIndex: 2, - }) - - expect(implementer).toBeInstanceOf(ProcedureImplementer) - expect(implementer['~orpc'].middlewares).toEqual([mid1, mid2]) - expect(implementer['~orpc'].inputValidationIndex).toEqual(2) - expect(implementer['~orpc'].outputValidationIndex).toEqual(2) - expect(implementer['~orpc'].contract).toBe(ping) - }) - - it('with router', () => { - const implementer = createChainableImplementer(contract, { - middlewares: [mid1, mid2], - inputValidationIndex: 2, - outputValidationIndex: 2, - }) - - expect(implementer.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) - expect(implementer.use(mid3)['~orpc'].contract).toBe(contract) - - expect(implementer.ping).toBeInstanceOf(ProcedureImplementer) - expect(implementer.ping['~orpc'].middlewares).toEqual([mid1, mid2]) - expect(implementer.ping['~orpc'].inputValidationIndex).toEqual(2) - expect(implementer.ping['~orpc'].outputValidationIndex).toEqual(2) - expect(implementer.ping['~orpc'].contract).toBe(ping) - - expect(implementer.pong).toBeInstanceOf(ProcedureImplementer) - expect(implementer.pong['~orpc'].middlewares).toEqual([mid1, mid2]) - expect(implementer.pong['~orpc'].inputValidationIndex).toEqual(2) - expect(implementer.pong['~orpc'].outputValidationIndex).toEqual(2) - expect(implementer.pong['~orpc'].contract).toBe(pong) - - expect(implementer.nested.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) - expect(implementer.nested.use(mid3)['~orpc'].contract).toBe(contract.nested) - - expect(implementer.nested.ping).toBeInstanceOf(ProcedureImplementer) - expect(implementer.nested.ping['~orpc'].middlewares).toEqual([mid1, mid2]) - expect(implementer.nested.ping['~orpc'].inputValidationIndex).toEqual(2) - expect(implementer.nested.ping['~orpc'].outputValidationIndex).toEqual(2) - expect(implementer.nested.ping['~orpc'].contract).toBe(contract.nested.ping) - - expect(implementer.nested.pong).toBeInstanceOf(ProcedureImplementer) - expect(implementer.nested.pong['~orpc'].middlewares).toEqual([mid1, mid2]) - expect(implementer.nested.pong['~orpc'].inputValidationIndex).toEqual(2) - expect(implementer.nested.pong['~orpc'].outputValidationIndex).toEqual(2) - expect(implementer.nested.pong['~orpc'].contract).toBe(contract.nested.pong) - }) - - describe('on conflicted', () => { - const contract = { - 'use': ping, - 'router': { - use: ping, - router: pong, - }, - '~orpc': { - use: ping, - router: pong, - }, - '~type': { - use: ping, - router: pong, - }, - } - - const implementer = createChainableImplementer(contract, { - middlewares: [mid1, mid2], - inputValidationIndex: 2, - outputValidationIndex: 2, - }) - - it('still works', () => { - expect(implementer.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) - expect(implementer.use(mid3)['~orpc'].contract).toBe(contract) - - expect(implementer.use).toBeTypeOf('function') - expect(implementer.use.use(mid3)).toBeInstanceOf(ProcedureImplementer) - expect(implementer.use.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) - expect(implementer.use.use(mid3)['~orpc'].inputValidationIndex).toEqual(2) - expect(implementer.use.use(mid3)['~orpc'].outputValidationIndex).toEqual(2) - expect(implementer.use['~orpc'].contract).toBe(ping) - - expect(implementer.router).toBeTypeOf('function') - expect(implementer.router.use(mid3)).toBeInstanceOf(RouterImplementer) - expect(implementer.router.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) - expect(implementer.router.use(mid3)['~orpc'].contract).toBe(contract.router) - - expect(implementer.router.router).toBeTypeOf('function') - expect(implementer.router.router.use(mid3)).toBeInstanceOf(ProcedureImplementer) - expect(implementer.router.router.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) - expect(implementer.use.use(mid3)['~orpc'].inputValidationIndex).toEqual(2) - expect(implementer.use.use(mid3)['~orpc'].outputValidationIndex).toEqual(2) - expect(implementer.router.router['~orpc'].contract).toBe(contract.router.router) - - expect(implementer.router.use).toBeTypeOf('function') - expect(implementer.router.use.use(mid3)).toBeInstanceOf(ProcedureImplementer) - expect(implementer.router.use.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) - expect(implementer.use.use(mid3)['~orpc'].inputValidationIndex).toEqual(2) - expect(implementer.use.use(mid3)['~orpc'].outputValidationIndex).toEqual(2) - expect(implementer.router.use['~orpc'].contract).toBe(contract.router.use) - - expect(implementer['~orpc'].use).toBeTypeOf('function') - expect(implementer['~orpc'].use.use(mid3)).toBeInstanceOf(ProcedureImplementer) - expect(implementer['~orpc'].use.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) - expect(implementer.use.use(mid3)['~orpc'].inputValidationIndex).toEqual(2) - expect(implementer.use.use(mid3)['~orpc'].outputValidationIndex).toEqual(2) - expect(implementer['~orpc'].use['~orpc'].contract).toBe(contract.router.use) - }) - - it('not recursive on symbol', () => { - expect((implementer as any)[Symbol('something')]).toBeUndefined() - expect((implementer.use as any)[Symbol('something')]).toBeUndefined() - expect((implementer.router as any)[Symbol('something')]).toBeUndefined() - expect((implementer.router.use as any)[Symbol('something')]).toBeUndefined() - }) - }) -}) diff --git a/packages/server/src/implementer-chainable.ts b/packages/server/src/implementer-chainable.ts deleted file mode 100644 index b6d68c1c..00000000 --- a/packages/server/src/implementer-chainable.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { Context, TypeCurrentContext, TypeInitialContext } from './context' -import type { Middleware } from './middleware' -import { type ContractProcedure, type ContractRouter, isContractProcedure } from '@orpc/contract' -import { createCallableObject } from '@orpc/shared' -import { ProcedureImplementer } from './procedure-implementer' -import { RouterImplementer } from './router-implementer' - -export type ChainableImplementer< - TInitialContext extends Context, - TCurrentContext extends Context, - TContract extends ContractRouter, -> = TContract extends ContractProcedure - ? ProcedureImplementer - : { - [K in keyof TContract]: TContract[K] extends ContractRouter ? ChainableImplementer : never - } & Omit, '~type' | '~orpc'> - -export function createChainableImplementer< - TInitialContext extends Context, - TCurrentContext extends Context, - TContract extends ContractRouter, ->( - contract: TContract, - options: { - __initialContext?: TypeInitialContext - __currentContext?: TypeCurrentContext - middlewares: Middleware[] - inputValidationIndex: number - outputValidationIndex: number - }, -): ChainableImplementer { - if (isContractProcedure(contract)) { - const implementer = new ProcedureImplementer({ - contract, - middlewares: options.middlewares, - inputValidationIndex: options.inputValidationIndex, - outputValidationIndex: options.outputValidationIndex, - }) - - return implementer as any - } - - const chainable = {} as ChainableImplementer - - for (const key in contract) { - (chainable as any)[key] = createChainableImplementer(contract[key]!, options) - } - - const routerImplementer = new RouterImplementer({ - contract, - middlewares: options.middlewares, - }) - - const merged = new Proxy(chainable, { - get(target, key) { - const next = Reflect.get(target, key) as ChainableImplementer | undefined - const method = Reflect.get(routerImplementer, key) - - if (typeof key !== 'string' || typeof method !== 'function') { - return next - } - - if (!next) { - return method.bind(routerImplementer) - } - - return createCallableObject(next, method.bind(routerImplementer)) - }, - }) - - return merged as any -} diff --git a/packages/server/src/implementer-procedure.test-d.ts b/packages/server/src/implementer-procedure.test-d.ts new file mode 100644 index 00000000..871f6804 --- /dev/null +++ b/packages/server/src/implementer-procedure.test-d.ts @@ -0,0 +1,335 @@ +import type { Client, ClientRest, ContractProcedure, ErrorFromErrorMap, ErrorMap, ORPCErrorConstructorMap, Schema } from '@orpc/contract' +import type { OmitChainMethodDeep } from '@orpc/shared' +import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../../contract/tests/shared' +import type { CurrentContext, InitialContext } from '../tests/shared' +import type { Builder } from './builder' +import type { Context } from './context' +import type { ImplementedProcedure, ProcedureImplementer } from './implementer-procedure' +import type { MiddlewareOutputFn } from './middleware' +import type { Procedure } from './procedure' +import type { DecoratedProcedure } from './procedure-decorated' + +const generalBuilder = {} as Builder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta +> + +const generalDecoratedProcedure = {} as DecoratedProcedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta +> + +describe('ImplementedProcedure', () => { + const implemented = {} as ImplementedProcedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + + it('backward compatibility', () => { + const expected = {} as OmitChainMethodDeep< + typeof generalDecoratedProcedure, + 'meta' | 'route' | 'errors' + > + + expectTypeOf(implemented).toMatchTypeOf(expected) + expectTypeOf().toEqualTypeOf() + }) + + it('is a procedure', () => { + expectTypeOf(implemented).toMatchTypeOf< + Procedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + >() + }) + + describe('.use', () => { + it('without map input', () => { + const applied = implemented.use(({ context, next, path, procedure, errors }, input, output) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf>() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }) + + expectTypeOf(applied).toEqualTypeOf< + DecoratedProcedure< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error --- conflict context + implemented.use(({ next }) => next({ context: { db: 123 } })) + // @ts-expect-error --- input is not match + implemented.use(({ next }, input: 'invalid') => next({})) + // @ts-expect-error --- output is not match + implemented.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + // conflict context but not detected + expectTypeOf(implemented.use(({ next }) => next({ context: { db: undefined } }))).toEqualTypeOf() + }) + + it('with map input', () => { + const applied = implemented.use(({ context, next, path, procedure, errors }, input: { mapped: string }, output) => { + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf>() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }, (input) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + return { mapped: input.input } + }) + + expectTypeOf(applied).toEqualTypeOf< + DecoratedProcedure< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error --- conflict context + implemented.use(({ next }) => ({ context: { db: 123 } }), () => {}) + // @ts-expect-error --- input is not match + implemented.use(({ next }, input: 'invalid') => next({}), () => {}) + // @ts-expect-error --- output is not match + implemented.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}), () => {}) + // conflict context but not detected + expectTypeOf(implemented.use(({ next }) => next({ context: { db: undefined } }), () => {})).toEqualTypeOf() + }) + }) + + it('.callable', () => { + const applied = implemented.callable({ + context: async (clientContext: 'client-context') => ({ db: 'postgres' }), + }) + + expectTypeOf(applied).toEqualTypeOf< + Procedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + & Client<'client-context', { input: number }, { output: string }, ErrorFromErrorMap> + >() + }) + + it('.actionable', () => { + const applied = implemented.actionable({ + context: async (clientContext: 'client-context') => ({ db: 'postgres' }), + }) + + expectTypeOf(applied).toEqualTypeOf< + Procedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + & ((...rest: ClientRest<'client-context', { input: number }>) => Promise<{ output: string }>) + >() + }) +}) + +describe('ProcedureImplementer', () => { + const builder = {} as ProcedureImplementer< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + + it('backward compatibility', () => { + const expected = {} as OmitChainMethodDeep< + typeof generalBuilder, + '$config' | '$context' | '$meta' | '$route' | 'middleware' | 'prefix' | 'tag' | 'router' | 'lazy' | 'input' | 'output' | 'meta' | 'route' | 'errors' + > + + expectTypeOf(builder).toMatchTypeOf(expected) + expectTypeOf().toEqualTypeOf() + }) + + it('is a contract procedure', () => { + expectTypeOf(builder).toMatchTypeOf< + ContractProcedure< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + }) + + describe('.use', () => { + it('without map input', () => { + const applied = builder.use(({ context, next, path, procedure, errors, signal }, input, output) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }) + + expectTypeOf(applied).toEqualTypeOf< + ProcedureImplementer< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + // @ts-expect-error --- conflict context + builder.use(({ next }) => next({ context: { db: 123 } })) + // conflict but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf() + // @ts-expect-error --- input is not match + builder.use(({ next }, input: 'invalid') => next({})) + // @ts-expect-error --- output is not match + builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + }) + + it('with map input', () => { + const applied = builder.use(({ context, next, path, procedure, errors, signal }, input: { mapped: boolean }, output) => { + expectTypeOf(input).toEqualTypeOf<{ mapped: boolean }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }, (input) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + + return { mapped: true } + }) + + expectTypeOf(applied).toEqualTypeOf< + ProcedureImplementer< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() + + builder.use( + ({ context, next, path, procedure, errors, signal }, input: { mapped: boolean }, output) => next(), + // @ts-expect-error --- invalid map input + input => ({ invalid: true }), + ) + + // @ts-expect-error --- conflict context + builder.use(({ next }) => next({ context: { db: 123 } }), input => ({ mapped: true })) + // conflict but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }), input => ({ mapped: true }))).toMatchTypeOf() + // @ts-expect-error --- input is not match + builder.use(({ next }, input: 'invalid') => next({}), input => ({ mapped: true })) + // @ts-expect-error --- output is not match + builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}), input => ({ mapped: true })) + }) + }) + + it('.handler', () => { + const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(signal).toEqualTypeOf>() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(signal).toEqualTypeOf>() + + return { output: 456 } + }) + + expectTypeOf(procedure).toMatchTypeOf< + ImplementedProcedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + >() + }) +}) diff --git a/packages/server/src/implementer-procedure.ts b/packages/server/src/implementer-procedure.ts new file mode 100644 index 00000000..ecb3046c --- /dev/null +++ b/packages/server/src/implementer-procedure.ts @@ -0,0 +1,118 @@ +import type { ClientRest, ErrorMap, Meta, ORPCErrorConstructorMap, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { BuilderDef } from './builder' +import type { ConflictContextGuard, Context, MergedContext } from './context' +import type { MapInputMiddleware, Middleware } from './middleware' +import type { Procedure, ProcedureHandler } from './procedure' +import type { CreateProcedureClientRest, ProcedureClient } from './procedure-client' +import type { DecoratedProcedure } from './procedure-decorated' + +/** + * Like `DecoratedProcedure`, but removed all method that can change the contract. + */ +export interface ImplementedProcedure< + TInitialContext extends Context, + TCurrentContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + THandlerOutput extends SchemaInput, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> extends Procedure { + use: (( + middleware: Middleware< + TCurrentContext, + U, + SchemaOutput, + THandlerOutput, + ORPCErrorConstructorMap, + TMeta + >, + ) => ConflictContextGuard> + & DecoratedProcedure, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>) & (( + middleware: Middleware< + TCurrentContext, + UOutContext, + UInput, + THandlerOutput, + ORPCErrorConstructorMap, + TMeta + >, + mapInput: MapInputMiddleware, UInput>, + ) => ConflictContextGuard> + & DecoratedProcedure, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>) + + /** + * Make this procedure callable (works like a function while still being a procedure). + */ + callable(...rest: CreateProcedureClientRest): & Procedure + & ProcedureClient < TClientContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap > + + /** + * Make this procedure compatible with server action (the same as .callable, but the type is compatible with server action). + */ + actionable(...rest: CreateProcedureClientRest): & Procedure + & ((...rest: ClientRest>) => Promise>) +} + +/** + * Like `ProcedureBuilderWithoutHandler`, but removed all method that can change the contract. + */ +export interface ProcedureImplementer< + TInitialContext extends Context, + TCurrentContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + TErrorMap extends ErrorMap, + TMeta extends Meta, +> { + '~orpc': BuilderDef + + 'use': (( + middleware: Middleware< + TCurrentContext, + U, + SchemaOutput, + SchemaInput, + ORPCErrorConstructorMap, + TMeta + >, + ) => ConflictContextGuard> + & ProcedureImplementer< + TInitialContext, + MergedContext, + TInputSchema, + TOutputSchema, + TErrorMap, + TMeta + >) & (( + middleware: Middleware< + TCurrentContext, + UOutContext, + UInput, + SchemaInput, + ORPCErrorConstructorMap, + TMeta + >, + mapInput: MapInputMiddleware, UInput>, + ) => ConflictContextGuard> + & ProcedureImplementer< + TInitialContext, + MergedContext, + TInputSchema, + TOutputSchema, + TErrorMap, + TMeta + >) + + 'handler'>( + handler: ProcedureHandler, + ): ImplementedProcedure< + TInitialContext, + TCurrentContext, + TInputSchema, + TOutputSchema, + UFuncOutput, + TErrorMap, + TMeta + > +} diff --git a/packages/server/src/implementer-variants.test-d.ts b/packages/server/src/implementer-variants.test-d.ts new file mode 100644 index 00000000..04590a8a --- /dev/null +++ b/packages/server/src/implementer-variants.test-d.ts @@ -0,0 +1,147 @@ +import type { ErrorMap, Meta, ORPCErrorConstructorMap, Schema } from '@orpc/contract' +import type { baseErrorMap, BaseMeta, inputSchema, outputSchema, router } from '../../contract/tests/shared' +import type { CurrentContext, InitialContext } from '../tests/shared' +import type { Context, MergedContext } from './context' +import type { ImplementerInternal } from './implementer' +import type { ProcedureImplementer } from './implementer-procedure' +import type { ImplementerInternalWithMiddlewares } from './implementer-variants' +import type { Lazy } from './lazy' +import type { MiddlewareOutputFn } from './middleware' +import type { Procedure } from './procedure' +import type { AdaptedRouter } from './router' +import { router as implRouter } from '../tests/shared' + +describe('ImplementerWithMiddlewares', () => { + const implementer = {} as ImplementerInternalWithMiddlewares + it('backwards compatibility with Implementer', () => { + const _: typeof implementer = {} as ImplementerInternal + }) + + describe('router level', () => { + it('.use', () => { + const applied = implementer.nested.use(({ context, next, path, procedure, errors, signal }, input, output) => { + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }) + + expectTypeOf(applied).toMatchTypeOf< + ImplementerInternalWithMiddlewares< + typeof router['nested'], + InitialContext, + MergedContext + > + >() + + // @ts-expect-error --- conflict context + implementer.use(({ next }) => next({ context: { db: 123 } })) + // conflict but not detected + expectTypeOf(implementer.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf() + // @ts-expect-error --- input is not match + implementer.use(({ next }, input: 'invalid') => next({})) + // @ts-expect-error --- output is not match + implementer.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + }) + + it('.router', () => { + expectTypeOf(implementer.router(implRouter)).toEqualTypeOf< + AdaptedRouter> + >() + + implementer.router({ + // @ts-expect-error - initial context is not match + ping: {} as Procedure<{ invalid: true }, Context, undefined, undefined, unknown, Record, BaseMeta>, + }) + + implementer.router({ + // @ts-expect-error - meta def is not match + ping: {} as Procedure< + Context, + Context, + undefined, + undefined, + unknown, + Record, + { invalid: true } + >, + }) + + // @ts-expect-error - missing implementation + implementer.router({ + ping: implRouter.ping, + }) + }) + + it('.lazy', () => { + expectTypeOf(implementer.lazy(() => Promise.resolve({ default: implRouter }))).toEqualTypeOf< + AdaptedRouter, InitialContext, Record> + >() + + // @ts-expect-error - initial context is not match + implementer.lazy(() => Promise.resolve({ + default: { + ping: {} as Procedure<{ invalid: true }, Context, undefined, undefined, unknown, Record, BaseMeta>, + }, + })) + + // @ts-expect-error - meta def is not match + implementer.lazy(() => Promise.resolve({ + default: { + ping: {} as Procedure< + Context, + Context, + undefined, + undefined, + unknown, + Record, + { invalid: true } + >, + }, + })) + + // @ts-expect-error - missing implementation + implementer.lazy(() => Promise.resolve({ + default: { + ping: implRouter.ping, + }, + })) + }) + }) + + it('each procedure is a ProcedureImplementer', () => { + type ExpectedPing = ProcedureImplementer< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + + type ExpectedPong = ProcedureImplementer< + InitialContext, + CurrentContext, + undefined, + undefined, + Record, + Meta + > + + expectTypeOf(implementer.ping).toEqualTypeOf() + expectTypeOf(implementer.nested.ping).toEqualTypeOf() + expectTypeOf(implementer.pong).toEqualTypeOf() + expectTypeOf(implementer.nested.pong).toEqualTypeOf() + }) +}) diff --git a/packages/server/src/implementer-variants.ts b/packages/server/src/implementer-variants.ts new file mode 100644 index 00000000..27f4e8ad --- /dev/null +++ b/packages/server/src/implementer-variants.ts @@ -0,0 +1,39 @@ +import type { AnyContractRouter, ContractProcedure, ContractRouter, ContractRouterToErrorMap, ORPCErrorConstructorMap } from '@orpc/contract' +import type { ConflictContextGuard, Context, MergedContext } from './context' +import type { ProcedureImplementer } from './implementer-procedure' +import type { FlattenLazy } from './lazy-utils' +import type { Middleware } from './middleware' +import type { AdaptedRouter, Router } from './router' + +export type ImplementerInternalWithMiddlewares< + TContract extends AnyContractRouter, + TInitialContext extends Context, + TCurrentContext extends Context, +> = + &( + TContract extends ContractProcedure + ? ProcedureImplementer + : TContract extends ContractRouter ? { + use( + middleware: Middleware< + TCurrentContext, + U, + unknown, + unknown, + ORPCErrorConstructorMap>, + UMeta + >, + ): ConflictContextGuard> + & ImplementerInternalWithMiddlewares> + + router>(router: U): AdaptedRouter> + + lazy>( + loader: () => Promise<{ default: U }> + ): AdaptedRouter, TInitialContext, Record> + } & { + [K in keyof TContract]: TContract[K] extends AnyContractRouter + ? ImplementerInternalWithMiddlewares + : never + } : never + ) diff --git a/packages/server/src/implementer.test-d.ts b/packages/server/src/implementer.test-d.ts new file mode 100644 index 00000000..5a6c452f --- /dev/null +++ b/packages/server/src/implementer.test-d.ts @@ -0,0 +1,212 @@ +import type { ErrorMap, Meta, ORPCErrorConstructorMap, Schema } from '@orpc/contract' +import type { baseErrorMap, BaseMeta, inputSchema, outputSchema, router } from '../../contract/tests/shared' +import type { CurrentContext, InitialContext } from '../tests/shared' +import type { Context, MergedContext } from './context' +import type { Implementer } from './implementer' +import type { ProcedureImplementer } from './implementer-procedure' +import type { ImplementerInternalWithMiddlewares } from './implementer-variants' +import type { Lazy } from './lazy' +import type { MiddlewareOutputFn } from './middleware' +import type { DecoratedMiddleware } from './middleware-decorated' +import type { Procedure } from './procedure' +import type { AdaptedRouter } from './router' +import { router as implRouter } from '../tests/shared' + +describe('Implementer', () => { + const implementer = {} as Implementer + + describe('root level', () => { + it('.$context', () => { + const applied = implementer.$context<{ anything: string }>() + + expectTypeOf(applied).toMatchTypeOf< + Implementer + >() + }) + + it('.$config', () => { + const applied = implementer.$config({ + initialInputValidationIndex: Number.NEGATIVE_INFINITY, + }) + + expectTypeOf(applied).toMatchTypeOf< + Implementer + >() + }) + }) + + describe('router level', () => { + describe('.middleware', () => { + it('works', () => { + const mid = implementer.nested.middleware(({ context, next, path, procedure, errors, signal }, input, output) => { + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }) + + expectTypeOf(mid).toMatchTypeOf< + DecoratedMiddleware< + CurrentContext, + { extra: boolean }, + unknown, + any, + ORPCErrorConstructorMap, + Meta | BaseMeta + > + >() + + // @ts-expect-error --- conflict context + implementer.middleware(({ next }) => next({ db: 123 })) + }) + + it('can type input and output', () => { + expectTypeOf( + implementer.middleware(({ next }, input: 'input', output: MiddlewareOutputFn<'output'>) => next()), + ).toEqualTypeOf< + DecoratedMiddleware< + CurrentContext, + Record, + 'input', + 'output', + ORPCErrorConstructorMap, + Meta | BaseMeta + > + >() + }) + }) + + it('.use', () => { + const applied = implementer.nested.use(({ context, next, path, procedure, errors, signal }, input, output) => { + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>>() + expectTypeOf(signal).toEqualTypeOf>() + + return next({ + context: { + extra: true, + }, + }) + }) + + expectTypeOf(applied).toMatchTypeOf< + ImplementerInternalWithMiddlewares> + >() + + // @ts-expect-error --- conflict context + implementer.use(({ next }) => next({ context: { db: 123 } })) + // conflict but not detected + expectTypeOf(implementer.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf() + // @ts-expect-error --- input is not match + implementer.use(({ next }, input: 'invalid') => next({})) + // @ts-expect-error --- output is not match + implementer.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + }) + + it('.router', () => { + expectTypeOf(implementer.router(implRouter)).toEqualTypeOf< + AdaptedRouter> + >() + + implementer.router({ + // @ts-expect-error - initial context is not match + ping: {} as Procedure<{ invalid: true }, Context, undefined, undefined, unknown, Record, BaseMeta>, + }) + + implementer.router({ + // @ts-expect-error - meta def is not match + ping: {} as Procedure< + Context, + Context, + undefined, + undefined, + unknown, + Record, + { invalid: true } + >, + }) + + // @ts-expect-error - missing implementation + implementer.router({ + ping: implRouter.ping, + }) + }) + + it('.lazy', () => { + expectTypeOf(implementer.lazy(() => Promise.resolve({ default: implRouter }))).toEqualTypeOf< + AdaptedRouter, InitialContext, Record> + >() + + // @ts-expect-error - initial context is not match + implementer.lazy(() => Promise.resolve({ + default: { + ping: {} as Procedure<{ invalid: true }, Context, undefined, undefined, unknown, Record, BaseMeta>, + }, + })) + + // @ts-expect-error - meta def is not match + implementer.lazy(() => Promise.resolve({ + default: { + ping: {} as Procedure< + Context, + Context, + undefined, + undefined, + unknown, + Record, + { invalid: true } + >, + }, + })) + + // @ts-expect-error - missing implementation + implementer.lazy(() => Promise.resolve({ + default: { + ping: implRouter.ping, + }, + })) + }) + }) + + it('each procedure is a ProcedureImplementer', () => { + type ExpectedPing = ProcedureImplementer< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + + type ExpectedPong = ProcedureImplementer< + InitialContext, + CurrentContext, + undefined, + undefined, + Record, + Meta + > + + expectTypeOf(implementer.ping).toEqualTypeOf() + expectTypeOf(implementer.nested.ping).toEqualTypeOf() + expectTypeOf(implementer.pong).toEqualTypeOf() + expectTypeOf(implementer.nested.pong).toEqualTypeOf() + }) +}) diff --git a/packages/server/src/implementer.test.ts b/packages/server/src/implementer.test.ts new file mode 100644 index 00000000..af61e4c0 --- /dev/null +++ b/packages/server/src/implementer.test.ts @@ -0,0 +1,153 @@ +import { router as contract } from '../../contract/tests/shared' +import { router } from '../tests/shared' +import { Builder } from './builder' +import { getRouterContract } from './hidden' +import * as Hidden from './hidden' +import { implement } from './implementer' +import { isLazy, unlazy } from './lazy' +import * as MiddlewareDecorated from './middleware-decorated' +import * as Router from './router' + +const setRouterContractSpy = vi.spyOn(Hidden, 'setRouterContract') +const decorateMiddlewareSpy = vi.spyOn(MiddlewareDecorated, 'decorateMiddleware') +const adaptRouterSpy = vi.spyOn(Router, 'adaptRouter') + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('implement', () => { + const rawImplementer = implement(contract) + + const mid = vi.fn() + const config = { + initialInputValidationIndex: Number.POSITIVE_INFINITY, + } + + const implementer = rawImplementer.$config(config).use(mid) + + describe('root level', () => { + it('.$context', () => { + const applied = rawImplementer.$context<{ anything: string }>() + + expect(applied).toBe(rawImplementer) + }) + + it('.$config', () => { + const config = { + initialInputValidationIndex: Number.NEGATIVE_INFINITY, + } + const applied = rawImplementer.$config(config) + + expect(applied.nested.ping['~orpc'].config).toBe(config) + }) + }) + + describe('router level', () => { + it('.middleware', () => { + const mid = vi.fn() + const applied = rawImplementer.nested.middleware(mid) + expect(applied).toBe(decorateMiddlewareSpy.mock.results[0]?.value) + expect(decorateMiddlewareSpy).toHaveBeenCalledOnce() + expect(decorateMiddlewareSpy).toHaveBeenCalledWith(mid) + }) + + it('.use', () => { + const mid = vi.fn() + const applied = rawImplementer.nested.use(mid) + + expect(applied.ping['~orpc'].middlewares).toEqual([mid]) + }) + + it('.router', () => { + const applied = implementer.nested.router(router as any) + + expect(getRouterContract(applied)).toBe(contract.nested) + expect(applied).toBe(setRouterContractSpy.mock.results[0]?.value) + + expect(setRouterContractSpy).toHaveBeenCalledOnce() + expect(setRouterContractSpy).toHaveBeenCalledWith(adaptRouterSpy.mock.results[0]?.value, contract.nested) + + expect(adaptRouterSpy).toHaveBeenCalledOnce() + expect(adaptRouterSpy).toHaveBeenCalledWith(router, { + middlewares: [mid], + errorMap: {}, + }) + }) + + it('.lazy', () => { + const applied = implementer.nested.lazy(() => Promise.resolve({ default: router as any })) + + expect(getRouterContract(applied)).toBe(contract.nested) + expect(applied).toBe(setRouterContractSpy.mock.results[0]?.value) + + expect(setRouterContractSpy).toHaveBeenCalledOnce() + expect(setRouterContractSpy).toHaveBeenCalledWith(adaptRouterSpy.mock.results[0]?.value, contract.nested) + + expect(adaptRouterSpy).toHaveBeenCalledOnce() + expect(adaptRouterSpy).toHaveBeenCalledWith(expect.any(Object), { + middlewares: [mid], + errorMap: {}, + }) + + const lazied = adaptRouterSpy.mock.calls[0]?.[0] + + expect(lazied).toSatisfy(isLazy) + expect(unlazy(lazied)).resolves.toEqual({ default: router }) + }) + }) + + it('each procedure is a ProcedureImplementer', () => { + const pingBuilder = (builder: any) => { + expect(builder).toBeInstanceOf(Builder) + expect(builder['~orpc']).toEqual({ + ...contract.ping['~orpc'], + config, + middlewares: [mid], + inputValidationIndex: Number.POSITIVE_INFINITY, + outputValidationIndex: 1, + }) + + return true + } + + const pongBuilder = (builder: any) => { + expect(builder).toBeInstanceOf(Builder) + expect(builder['~orpc']).toEqual({ + ...contract.pong['~orpc'], + config, + middlewares: [mid], + inputValidationIndex: Number.POSITIVE_INFINITY, + outputValidationIndex: 1, + }) + + return true + } + + expect(implementer.ping).toSatisfy(pingBuilder) + expect(implementer.nested.ping).toSatisfy(pingBuilder) + + expect(implementer.pong).toSatisfy(pongBuilder) + expect(implementer.nested.pong).toSatisfy(pongBuilder) + }) + + it('contract & method has the same name', () => { + const conflictedContract = { + $context: { + $context: contract, + }, + use: { + use: contract, + }, + } + + const implementer = implement(conflictedContract) + + expect(implementer.$context.$context.nested.ping).toBeInstanceOf(Builder) + expect(implementer.use.use.nested.ping).toBeInstanceOf(Builder) + + expect((implementer as any)[Symbol.for('test')]).toBeUndefined() + expect((implementer.$context as any)[Symbol.for('test')]).toBeUndefined() + expect((implementer.use as any)[Symbol.for('test')]).toBeUndefined() + }) +}) diff --git a/packages/server/src/implementer.ts b/packages/server/src/implementer.ts new file mode 100644 index 00000000..ef6ab99f --- /dev/null +++ b/packages/server/src/implementer.ts @@ -0,0 +1,192 @@ +import type { AnyContractRouter, ContractProcedure, ContractRouter, ContractRouterToErrorMap, ContractRouterToMeta, ORPCErrorConstructorMap } from '@orpc/contract' +import type { ConflictContextGuard, Context, MergedContext } from './context' +import type { ProcedureImplementer } from './implementer-procedure' +import type { ImplementerInternalWithMiddlewares } from './implementer-variants' +import type { AnyMiddleware, Middleware } from './middleware' +import { isContractProcedure } from '@orpc/contract' +import { Builder, type BuilderConfig } from './builder' +import { fallbackConfig } from './config' +import { setRouterContract } from './hidden' +import { lazy } from './lazy' +import { flatLazy, type FlattenLazy } from './lazy-utils' +import { type DecoratedMiddleware, decorateMiddleware } from './middleware-decorated' +import { addMiddleware } from './middleware-utils' +import { type AdaptedRouter, adaptRouter, type Router } from './router' + +export type ImplementerInternal< + TContract extends AnyContractRouter, + TInitialContext extends Context, + TCurrentContext extends Context, +> = + &( + TContract extends ContractProcedure + ? ProcedureImplementer + : TContract extends ContractRouter ? { + middleware( // = any here is important to make middleware can be used in any output by default + middleware: Middleware< + TCurrentContext, + UOutContext, + TInput, + TOutput, + ORPCErrorConstructorMap>, + ContractRouterToMeta + >, + ): DecoratedMiddleware, UMeta> // ORPCErrorConstructorMap ensures middleware can used in any procedure + + use( + middleware: Middleware< + TCurrentContext, + U, + unknown, + unknown, + ORPCErrorConstructorMap>, + UMeta + >, + ): ConflictContextGuard> + & ImplementerInternalWithMiddlewares> + + router>(router: U): AdaptedRouter> + + lazy>( + loader: () => Promise<{ default: U }> + ): AdaptedRouter, TInitialContext, Record> + } & { + [K in keyof TContract]: TContract[K] extends AnyContractRouter + ? ImplementerInternal + : never + } : never + ) + +export function implementerInternal< + TContract extends AnyContractRouter, + TInitialContext extends Context, + TCurrentContext extends Context, +>( + contract: TContract, + config: BuilderConfig, + middlewares: AnyMiddleware[], +): ImplementerInternal { + if (isContractProcedure(contract)) { + const impl = new Builder({ + ...contract['~orpc'], + config, + middlewares, + inputValidationIndex: fallbackConfig('initialInputValidationIndex', config?.initialInputValidationIndex) + middlewares.length, + outputValidationIndex: fallbackConfig('initialOutputValidationIndex', config?.initialOutputValidationIndex) + middlewares.length, + }) + + return impl as any + } + + const impl = new Proxy(contract, { + get: (target, key) => { + let method: any + + if (key === 'middleware') { + method = (mid: any) => decorateMiddleware(mid) + } + else if (key === 'use') { + method = (mid: any) => { + return implementerInternal( + contract, + config, + addMiddleware(middlewares, mid), + ) + } + } + else if (key === 'router') { + method = (router: any) => { + const adapted = adaptRouter(router, { + middlewares, + errorMap: {}, + }) + + return setRouterContract(adapted, contract) + } + } + else if (key === 'lazy') { + method = (loader: any) => { + const adapted = adaptRouter(flatLazy(lazy(loader)) as any, { + middlewares, + errorMap: {}, + }) + + return setRouterContract(adapted, contract) + } + } + + const next = Reflect.get(target, key) + + if (!next || (typeof next !== 'function' && typeof next !== 'object')) { + return method ?? next + } + + const nextImpl = implementerInternal(next as any, config, middlewares) + + if (method) { + return new Proxy(method, { + get(_, key) { + return Reflect.get(nextImpl, key) + }, + }) + } + + return nextImpl + }, + }) + + return impl as any +} + +export type Implementer< + TContract extends AnyContractRouter, + TInitialContext extends Context, + TCurrentContext extends Context , +> = + & { + $context(): Implementer + $config(config: BuilderConfig): Implementer + } + & ImplementerInternal + +export function implement< + TContract extends AnyContractRouter, + TInitialContext extends Context, + TCurrentContext extends Context, +>( + contract: TContract, + config: BuilderConfig = {}, +): Implementer { + const implInternal = implementerInternal(contract, config, []) + + const impl = new Proxy(implInternal, { + get: (target, key) => { + let method: any + + if (key === '$context') { + method = () => impl + } + else if (key === '$config') { + method = (config: BuilderConfig) => implement(contract, config) + } + + const next = Reflect.get(target, key) + + if (!next || (typeof next !== 'function' && typeof next !== 'object')) { + return method ?? next + } + + if (method) { + return new Proxy(method, { + get(_, key) { + return Reflect.get(next, key) + }, + }) + } + + return next + }, + }) + + return impl as any +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 402293d4..ada3e413 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,30 +1,21 @@ -import { Builder } from './builder' - export * from './builder' +export * from './builder-variants' export * from './config' export * from './context' -export * from './error' export * from './hidden' -export * from './implementer-chainable' +export * from './implementer' +export * from './implementer-procedure' +export * from './implementer-variants' export * from './lazy' -export * from './lazy-decorated' +export * from './lazy-utils' export * from './middleware' export * from './middleware-decorated' export * from './procedure' -export * from './procedure-builder' export * from './procedure-client' export * from './procedure-decorated' -export * from './procedure-implementer' export * from './procedure-utils' export * from './router' -export * from './router-builder' +export * from './router-accessible-lazy' export * from './router-client' -export * from './router-implementer' -export * from './router-utils' -export * from './types' export { isDefinedError, ORPCError, safe, type } from '@orpc/contract' - -export const os = new Builder({ - config: {}, -}) diff --git a/packages/server/src/lazy-decorated.test-d.ts b/packages/server/src/lazy-decorated.test-d.ts deleted file mode 100644 index c6581f11..00000000 --- a/packages/server/src/lazy-decorated.test-d.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { Context } from './context' -import type { Lazy } from './lazy' -import type { DecoratedLazy } from './lazy-decorated' -import type { ANY_PROCEDURE, Procedure } from './procedure' -import type { DecoratedProcedure } from './procedure-decorated' -import type { ANY_ROUTER } from './router' -import { z } from 'zod' -import { lazy } from './lazy' -import { decorateLazy } from './lazy-decorated' - -const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - -const route = { method: 'GET', path: '/ping' } as const -const ping = {} as Procedure, typeof route> -const pong = {} as DecoratedProcedure, Record> - -const lazyPing = lazy(() => Promise.resolve({ default: ping })) -const lazyPong = lazy(() => Promise.resolve({ default: pong })) - -const router = { - ping, - pong, - nested: { - ping, - pong, - }, -} - -const lazyRouter = lazy(() => Promise.resolve({ - default: { - ping: lazyPing, - pong, - nested: lazy(() => Promise.resolve({ - default: { - ping, - pong: lazyPong, - }, - })), - }, -})) - -describe('DecoratedLazy', () => { - it('with procedure', () => { - const decorated = {} as DecoratedLazy - - expectTypeOf(decorated).toMatchTypeOf>() - }) - - it('with router', () => { - const decorated = {} as DecoratedLazy - - expectTypeOf(decorated).toMatchTypeOf>() - expectTypeOf({ router: decorated }).toMatchTypeOf() - - expectTypeOf(decorated.ping).toMatchTypeOf>() - expectTypeOf(decorated.pong).toMatchTypeOf>() - - expectTypeOf(decorated.nested).toMatchTypeOf>() - expectTypeOf({ router: decorated.nested }).toMatchTypeOf() - - expectTypeOf(decorated.nested.ping).toMatchTypeOf>() - expectTypeOf(decorated.nested.pong).toMatchTypeOf>() - }) - - it('flat lazy', () => { - expectTypeOf>().toEqualTypeOf>() - expectTypeOf>().toEqualTypeOf>() - expectTypeOf>>().toEqualTypeOf>() - - expectTypeOf['ping']>().toEqualTypeOf['ping']>() - expectTypeOf['pong']>().toEqualTypeOf['pong']>() - expectTypeOf['nested']['ping']>().toEqualTypeOf['nested']['ping']>() - expectTypeOf['nested']['pong']>().toEqualTypeOf['nested']['pong']>() - - // @ts-expect-error - lazy loader is diff - expectTypeOf['nested']>().toEqualTypeOf['nested']>() - }) -}) - -it('decorateLazy', () => { - expectTypeOf(decorateLazy(lazyPing)).toEqualTypeOf>() - expectTypeOf(decorateLazy(lazyPong)).toEqualTypeOf>() - expectTypeOf(decorateLazy(lazy(() => Promise.resolve({ default: router })))).toEqualTypeOf>() - - // @ts-expect-error - invalid lazy - decorateLazy(ping) - - // @ts-expect-error - invalid lazy - decorateLazy(router) -}) diff --git a/packages/server/src/lazy-decorated.test.ts b/packages/server/src/lazy-decorated.test.ts deleted file mode 100644 index 37f062bc..00000000 --- a/packages/server/src/lazy-decorated.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ContractProcedure } from '@orpc/contract' -import { z } from 'zod' -import { isLazy, lazy, unlazy } from './lazy' -import { decorateLazy } from './lazy-decorated' -import { Procedure } from './procedure' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('decorated lazy', () => { - const schema = z.object({ val: z.string().transform(val => Number(val)) }) - - const ping = new Procedure({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: undefined, - errorMap: {}, - route: {}, - }), - handler: vi.fn(), - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, - }) - - const lazyPing = lazy(() => Promise.resolve({ default: ping })) - - it('still a lazy', async () => { - expect(decorateLazy(lazyPing)).toSatisfy(isLazy) - expect((await unlazy(decorateLazy(lazyPing))).default).toBe(ping) - }) - - it('nested access return lazy', async () => { - const l2 = lazy(() => Promise.resolve({ default: { ping } })) - const decoratedL2 = decorateLazy(l2) - expect(decoratedL2.ping).toSatisfy(isLazy) - expect((await unlazy(decoratedL2.ping)).default).toBe(ping) - - const l3 = lazy(() => Promise.resolve({ default: { ping: lazyPing } })) - const decoratedL3 = decorateLazy(l3) - expect(decoratedL3.ping).toSatisfy(isLazy) - expect((await unlazy(decoratedL3.ping)).default).toBe(ping) - }) - - it('return undefined when not exists child', () => { - const decorated = decorateLazy(lazy(() => Promise.resolve({ default: { ping: { pong: lazyPing } } }))) as any - - const child = decorated.ping.pong.peng.pang.p - - expect(child).toSatisfy(isLazy) - expect(unlazy(child)).resolves.toEqual({ default: undefined }) - }) -}) diff --git a/packages/server/src/lazy-decorated.ts b/packages/server/src/lazy-decorated.ts deleted file mode 100644 index 0ec1f983..00000000 --- a/packages/server/src/lazy-decorated.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Lazy } from './lazy' -import type { ANY_PROCEDURE } from './procedure' -import { flatLazy } from './lazy' -import { type ANY_ROUTER, getRouterChild } from './router' - -export type DecoratedLazy = T extends Lazy - ? DecoratedLazy - : Lazy - & ( - T extends ANY_PROCEDURE - ? unknown - : T extends ANY_ROUTER ? { - [K in keyof T]: DecoratedLazy - } : unknown - ) - -export function decorateLazy>(lazied: T): DecoratedLazy { - const flattenLazy = flatLazy(lazied) - - const recursive = new Proxy(flattenLazy, { - get(target, key) { - if (typeof key !== 'string') { - return Reflect.get(target, key) - } - - const next = getRouterChild(flattenLazy, key) - - return decorateLazy(next) - }, - }) - - return recursive as any -} diff --git a/packages/server/src/lazy-utils.test-d.ts b/packages/server/src/lazy-utils.test-d.ts index 13fe13e9..180664ae 100644 --- a/packages/server/src/lazy-utils.test-d.ts +++ b/packages/server/src/lazy-utils.test-d.ts @@ -1,10 +1,17 @@ +import type { ping } from '../tests/shared' import type { Lazy } from './lazy' -import type { ANY_PROCEDURE } from './procedure' +import type { FlattenLazy } from './lazy-utils' +import type { AnyProcedure } from './procedure' import { lazy } from './lazy' import { createLazyProcedureFormAnyLazy } from './lazy-utils' it('createLazyProcedureFormAnyLazy return a Lazy', async () => { const lazyPing = lazy(() => Promise.resolve({ default: {} as unknown })) const lazyProcedure = createLazyProcedureFormAnyLazy(lazyPing) - expectTypeOf(lazyProcedure).toEqualTypeOf>() + expectTypeOf(lazyProcedure).toEqualTypeOf>() +}) + +it('FlattenLazy', () => { + expectTypeOf>>>().toEqualTypeOf>() + expectTypeOf>>>>().toEqualTypeOf>() }) diff --git a/packages/server/src/lazy-utils.test.ts b/packages/server/src/lazy-utils.test.ts index 124f7ada..4ab1c5bc 100644 --- a/packages/server/src/lazy-utils.test.ts +++ b/packages/server/src/lazy-utils.test.ts @@ -1,22 +1,20 @@ -import { ContractProcedure } from '@orpc/contract' +import { ping } from '../tests/shared' import { isLazy, lazy, unlazy } from './lazy' -import { createLazyProcedureFormAnyLazy } from './lazy-utils' -import { Procedure } from './procedure' +import { createLazyProcedureFormAnyLazy, flatLazy } from './lazy-utils' + +it('flatLazy', () => { + const lazied = lazy(() => Promise.resolve({ + default: lazy(() => Promise.resolve({ + default: lazy(() => Promise.resolve({ default: ping })), + })), + })) + + const flatten = flatLazy(lazied) + expect(flatten).toSatisfy(isLazy) + expect(unlazy(flatten)).resolves.toEqual({ default: ping }) +}) describe('createLazyProcedureFormAnyLazy', () => { - const ping = new Procedure({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - route: {}, - }), - handler: vi.fn(), - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, - }) - it('return a Lazy', async () => { const lazyPing = lazy(() => Promise.resolve({ default: ping })) diff --git a/packages/server/src/lazy-utils.ts b/packages/server/src/lazy-utils.ts index 881f9784..8aa0bb91 100644 --- a/packages/server/src/lazy-utils.ts +++ b/packages/server/src/lazy-utils.ts @@ -1,8 +1,30 @@ import type { Lazy } from './lazy' -import { flatLazy, lazy, unlazy } from './lazy' -import { type ANY_PROCEDURE, isProcedure } from './procedure' +import { isLazy, lazy, unlazy } from './lazy' +import { type AnyProcedure, isProcedure } from './procedure' -export function createLazyProcedureFormAnyLazy(lazied: Lazy): Lazy { +export type FlattenLazy = T extends Lazy + ? FlattenLazy + : Lazy + +export function flatLazy>(lazied: T): FlattenLazy { + const flattenLoader = async () => { + let current = await unlazy(lazied) + + while (true) { + if (!isLazy(current.default)) { + break + } + + current = await unlazy(current.default) + } + + return current + } + + return lazy(flattenLoader) as any +} + +export function createLazyProcedureFormAnyLazy(lazied: Lazy): Lazy { const lazyProcedure = lazy(async () => { const { default: maybeProcedure } = await unlazy(flatLazy(lazied)) diff --git a/packages/server/src/lazy.test-d.ts b/packages/server/src/lazy.test-d.ts deleted file mode 100644 index 01186cbb..00000000 --- a/packages/server/src/lazy.test-d.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Context } from './context' -import type { ANY_LAZY, FlattenLazy, Lazy } from './lazy' -import type { Procedure } from './procedure' -import { flatLazy, isLazy, lazy, unlazy } from './lazy' - -const procedure = {} as Procedure, Record> - -const router = { procedure } - -it('lazy', () => { - expectTypeOf( - lazy(() => Promise.resolve({ default: procedure })), - ).toMatchTypeOf>() - - expectTypeOf( - lazy(() => Promise.resolve({ default: router })), - ).toMatchTypeOf>() -}) - -it('isLazy', () => { - const item = {} as unknown - - if (isLazy(item)) { - expectTypeOf(item).toEqualTypeOf() - } -}) - -it('unwrapLazy', () => { - expectTypeOf( - unlazy(lazy(() => Promise.resolve({ default: procedure }))), - ).toMatchTypeOf>() - - expectTypeOf( - unlazy(lazy(() => Promise.resolve({ default: router }))), - ).toMatchTypeOf>() -}) - -it('FlattenLazy', () => { - expectTypeOf>>>().toMatchTypeOf>() - expectTypeOf < FlattenLazy>>>>().toMatchTypeOf>() -}) - -it('flatLazy', () => { - expectTypeOf( - flatLazy(lazy(() => Promise.resolve({ default: lazy(() => Promise.resolve({ default: procedure })) }))), - ).toMatchTypeOf>() - - expectTypeOf( - flatLazy(lazy(() => Promise.resolve({ default: lazy(() => Promise.resolve({ - default: lazy(() => Promise.resolve({ default: router })), - })) }))), - ).toMatchTypeOf>() -}) diff --git a/packages/server/src/lazy.test.ts b/packages/server/src/lazy.test.ts index dca11ea2..23f0559f 100644 --- a/packages/server/src/lazy.test.ts +++ b/packages/server/src/lazy.test.ts @@ -1,51 +1,11 @@ -import { ContractProcedure } from '@orpc/contract' -import { flatLazy, isLazy, lazy, LAZY_LOADER_SYMBOL, unlazy } from './lazy' -import { Procedure } from './procedure' +import { ping } from '../tests/shared' +import { isLazy, lazy, unlazy } from './lazy' -const procedure = new Procedure({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - route: {}, - }), - handler: vi.fn(), - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, -}) - -const router = { procedure } - -it('lazy', () => { - const procedureLoader = () => Promise.resolve({ default: procedure }) - const routerLoader = () => Promise.resolve({ default: router }) - - expect(lazy(procedureLoader)).toSatisfy(isLazy) - expect(lazy(routerLoader)).toSatisfy(isLazy) - - expect(lazy(procedureLoader)[LAZY_LOADER_SYMBOL]).toBe(procedureLoader) - expect(lazy(routerLoader)[LAZY_LOADER_SYMBOL]).toBe(routerLoader) -}) +it('lazy & isLazy & unlazy ', () => { + const lazied = lazy(() => Promise.resolve({ default: ping })) + expect(lazied).toSatisfy(isLazy) + expect(unlazy(lazied)).resolves.toEqual({ default: ping }) -it('isLazy', () => { - expect(lazy(() => Promise.resolve({ default: procedure }))).toSatisfy(isLazy) - expect(lazy(() => Promise.resolve({ default: router }))).toSatisfy(isLazy) expect({}).not.toSatisfy(isLazy) - expect(undefined).not.toSatisfy(isLazy) -}) - -it('unwrapLazy', async () => { - const lazied = lazy(() => Promise.resolve({ default: 'root' })) - - expect(unlazy(lazied)).resolves.toEqual({ default: 'root' }) - expect((await unlazy(lazy(() => Promise.resolve({ default: lazied })))).default).toSatisfy(isLazy) -}) - -it('flatLazy', () => { - const lazied = lazy(() => Promise.resolve({ default: 'root' })) - - expect(flatLazy(lazied)[LAZY_LOADER_SYMBOL]()).resolves.toEqual({ default: 'root' }) - expect(flatLazy(lazy(() => Promise.resolve({ default: lazied })))[LAZY_LOADER_SYMBOL]()).resolves.toEqual({ default: 'root' }) - expect(flatLazy(lazy(() => Promise.resolve({ default: lazy(() => Promise.resolve({ default: lazied })) })))[LAZY_LOADER_SYMBOL]()).resolves.toEqual({ default: 'root' }) + expect(true).not.toSatisfy(isLazy) }) diff --git a/packages/server/src/lazy.ts b/packages/server/src/lazy.ts index 05dee30f..52d8370c 100644 --- a/packages/server/src/lazy.ts +++ b/packages/server/src/lazy.ts @@ -1,12 +1,20 @@ +import type { HTTPPath } from '@orpc/contract' + export const LAZY_LOADER_SYMBOL: unique symbol = Symbol('ORPC_LAZY_LOADER') +export interface LazyMeta { + prefix?: HTTPPath +} + export interface Lazy { - [LAZY_LOADER_SYMBOL]: () => Promise<{ default: T }> + [LAZY_LOADER_SYMBOL](): Promise<{ default: T }> } export type Lazyable = T | Lazy -export type ANY_LAZY = Lazy +export interface LazyOptions { + prefix?: HTTPPath +} export function lazy(loader: () => Promise<{ default: T }>): Lazy { return { @@ -14,7 +22,7 @@ export function lazy(loader: () => Promise<{ default: T }>): Lazy { } } -export function isLazy(item: unknown): item is ANY_LAZY { +export function isLazy(item: unknown): item is Lazy { return ( (typeof item === 'object' || typeof item === 'function') && item !== null @@ -26,25 +34,3 @@ export function isLazy(item: unknown): item is ANY_LAZY { export function unlazy>(lazied: T): Promise<{ default: T extends Lazy ? U : T }> { return isLazy(lazied) ? lazied[LAZY_LOADER_SYMBOL]() : Promise.resolve({ default: lazied }) } - -export type FlattenLazy = T extends Lazy - ? FlattenLazy - : Lazy - -export function flatLazy(lazied: T): FlattenLazy { - const flattenLoader = async () => { - let current = await unlazy(lazied) - - while (true) { - if (!isLazy(current.default)) { - break - } - - current = await unlazy(current.default) - } - - return current - } - - return lazy(flattenLoader) as any -} diff --git a/packages/server/src/middleware-decorated.test-d.ts b/packages/server/src/middleware-decorated.test-d.ts index fd9869e8..a41668a6 100644 --- a/packages/server/src/middleware-decorated.test-d.ts +++ b/packages/server/src/middleware-decorated.test-d.ts @@ -1,89 +1,95 @@ -import type { Context } from './context' +import type { ORPCErrorConstructorMap } from '@orpc/contract' +import type { baseErrorMap, BaseMeta } from '../../contract/tests/shared' +import type { CurrentContext } from '../tests/shared' import type { Middleware } from './middleware' import type { DecoratedMiddleware } from './middleware-decorated' -describe('decorateMiddleware', () => { - const decorated = {} as DecoratedMiddleware<{ user?: string }, { auth: true, user: string }, { name: string }, unknown, Record> +const decorated = {} as DecoratedMiddleware< + CurrentContext, + { extra: boolean }, + { input: string }, + { output: number }, + ORPCErrorConstructorMap, + BaseMeta +> - it('assignable to middleware', () => { - const decorated = {} as DecoratedMiddleware, { input: 'input' }, unknown, Record> - const mid: Middleware, { input: 'input' }, unknown, Record> = decorated - - const decorated2 = {} as DecoratedMiddleware> - const mid2: Middleware> = decorated2 - }) - - it('can map input', () => { - const mapped = decorated.mapInput((input: 'something') => ({ name: input })) - - expectTypeOf(mapped).toEqualTypeOf< - DecoratedMiddleware<{ user?: string }, { auth: true, user: string }, 'something', unknown, Record> +describe('DecoratedMiddleware', () => { + it('is a middleware', () => { + expectTypeOf(decorated).toMatchTypeOf< + Middleware< + CurrentContext, + { extra: boolean }, + { input: string }, + { output: number }, + ORPCErrorConstructorMap, + BaseMeta + > >() }) - it('can concat', () => { - const mapped = decorated.concat( - ({ next }, input: { age: number }) => next({ context: { db: true } }), - ) + it('.mapInput', () => { + const mapped = decorated.mapInput((input: 'input') => ({ input })) expectTypeOf(mapped).toEqualTypeOf< DecoratedMiddleware< - { user?: string }, - { auth: true, user: string } & { db: boolean }, - { name: string } & { age: number }, - unknown, - Record + CurrentContext, + { extra: boolean }, + 'input', + { output: number }, + ORPCErrorConstructorMap, + BaseMeta > >() }) - it('can concat with map input', () => { - const mapped = decorated.concat( - ({ next }, input: { age: number }) => next({ context: { db: true } }), - (input: { year: number }) => ({ age: 123 }), - ) - - expectTypeOf(mapped).toEqualTypeOf< - DecoratedMiddleware< - { user?: string }, - { auth: true, user: string } & { db: boolean }, - { name: string } & { year: number }, - unknown, - Record - > - >() + describe('.concat', () => { + it('without map input', () => { + const mapped = decorated.concat( + ({ next }, input: { input2: string }) => next({ context: { extra2: true } }), + ) - decorated.concat( - ({ next }, input: { age: number }) => next({ context: { db: true } }), - // @ts-expect-error - invalid return input - (input: { year: number }) => ({ age: '123' }), - ) - }) + expectTypeOf(mapped).toEqualTypeOf< + DecoratedMiddleware< + CurrentContext, + { extra: boolean } & { extra2: boolean }, + { input: string } & { input2: string }, + { output: number }, + ORPCErrorConstructorMap, + BaseMeta + > + >() - it('can concat and prevent conflict on context', () => { - const mapped = decorated.concat( - ({ next }) => next({ context: { db: true } }), - ) + decorated.concat( + // @ts-expect-error - conflict context + ({ next }) => next({ context: { extra: 'invalid' } }), + ) + }) - expectTypeOf(mapped).toEqualTypeOf< - DecoratedMiddleware< - { user?: string }, - { auth: true, user: string } & { db: boolean }, - { name: string }, - unknown, - Record - > - >() + it('with map input', () => { + const mapped = decorated.concat( + ({ next }, input) => { + expectTypeOf(input).toEqualTypeOf<{ input: { input2: string } }>() + return next({ context: { extra2: true } }) + }, + (input: { input2: string }) => ({ input }), + ) - decorated.concat( - // @ts-expect-error - user is not assignable to existing user context - (input, context, meta) => next({ context: { user: true } }), - ) + expectTypeOf(mapped).toEqualTypeOf< + DecoratedMiddleware< + CurrentContext, + { extra: boolean } & { extra2: boolean }, + { input: string } & { input2: string }, + { output: number }, + ORPCErrorConstructorMap, + BaseMeta + > + >() - decorated.concat( - // @ts-expect-error - user is not assignable to existing user context - (input, context, meta) => next({ context: { user: true } }), - () => 'anything', - ) + decorated.concat( + // @ts-expect-error - conflict context + ({ next }) => next({ context: { extra: 'invalid' } }), + input => ({ mapped: input }), + ) + }) }) }) diff --git a/packages/server/src/middleware-decorated.ts b/packages/server/src/middleware-decorated.ts index aaf68121..51b63866 100644 --- a/packages/server/src/middleware-decorated.ts +++ b/packages/server/src/middleware-decorated.ts @@ -1,6 +1,6 @@ -import type { Context } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { ANY_MAP_INPUT_MIDDLEWARE, ANY_MIDDLEWARE, MapInputMiddleware, Middleware, MiddlewareNextFn } from './middleware' +import type { Meta, ORPCErrorConstructorMap } from '@orpc/contract' +import type { Context, MergedContext } from './context' +import type { AnyMiddleware, MapInputMiddleware, Middleware, MiddlewareNextFn } from './middleware' export interface DecoratedMiddleware< TInContext extends Context, @@ -8,45 +8,52 @@ export interface DecoratedMiddleware< TInput, TOutput, TErrorConstructorMap extends ORPCErrorConstructorMap, -> extends Middleware { - concat: (( + TMeta extends Meta, +> extends Middleware { + concat( middleware: Middleware< TInContext & TOutContext, UOutContext, UInput & TInput, TOutput, - TErrorConstructorMap + TErrorConstructorMap, + TMeta >, - ) => DecoratedMiddleware< + ): DecoratedMiddleware< TInContext, - TOutContext & UOutContext, + MergedContext, UInput & TInput, TOutput, - TErrorConstructorMap - >) & (< + TErrorConstructorMap, + TMeta + > + + concat< UOutContext extends Context, - UInput = TInput, - UMappedInput = unknown, + UInput, + UMappedInput, >( middleware: Middleware< TInContext & TOutContext, UOutContext, UMappedInput, TOutput, - TErrorConstructorMap + TErrorConstructorMap, + TMeta >, mapInput: MapInputMiddleware, - ) => DecoratedMiddleware< + ): DecoratedMiddleware< TInContext, TOutContext & UOutContext, UInput & TInput, TOutput, - TErrorConstructorMap - >) + TErrorConstructorMap, + TMeta + > - mapInput: ( + mapInput( map: MapInputMiddleware, - ) => DecoratedMiddleware + ): DecoratedMiddleware } export function decorateMiddleware< @@ -55,10 +62,11 @@ export function decorateMiddleware< TInput, TOutput, TErrorConstructorMap extends ORPCErrorConstructorMap, + TMeta extends Meta, >( - middleware: Middleware, -): DecoratedMiddleware { - const decorated = middleware as DecoratedMiddleware + middleware: Middleware, +): DecoratedMiddleware { + const decorated = middleware as DecoratedMiddleware decorated.mapInput = (mapInput) => { const mapped = decorateMiddleware( @@ -68,7 +76,7 @@ export function decorateMiddleware< return mapped as any } - decorated.concat = (concatMiddleware: ANY_MIDDLEWARE, mapInput?: ANY_MAP_INPUT_MIDDLEWARE) => { + decorated.concat = (concatMiddleware: AnyMiddleware, mapInput?: MapInputMiddleware) => { const mapped = mapInput ? decorateMiddleware(concatMiddleware).mapInput(mapInput) : concatMiddleware diff --git a/packages/server/src/middleware-utils.test.ts b/packages/server/src/middleware-utils.test.ts new file mode 100644 index 00000000..810327f0 --- /dev/null +++ b/packages/server/src/middleware-utils.test.ts @@ -0,0 +1,29 @@ +import { addMiddleware, dedupeMiddlewares, mergeMiddlewares } from './middleware-utils' + +const mid1 = vi.fn() +const mid2 = vi.fn() +const mid3 = vi.fn() + +it('dedupeMiddlewares', () => { + expect(dedupeMiddlewares([mid1, mid2], [])).toEqual([]) + expect(dedupeMiddlewares([], [mid1, mid2])).toEqual([mid1, mid2]) + expect(dedupeMiddlewares([mid1], [mid2])).toEqual([mid2]) + expect(dedupeMiddlewares([mid1], [mid1, mid2])).toEqual([mid2]) + expect(dedupeMiddlewares([mid1, mid3], [mid1, mid2])).toEqual([mid2]) + expect(dedupeMiddlewares([mid1, mid3], [mid1, mid2, mid3])).toEqual([mid2, mid3]) +}) + +it('mergeMiddlewares', () => { + expect(mergeMiddlewares([mid1, mid2], [])).toEqual([mid1, mid2]) + expect(mergeMiddlewares([], [mid1, mid2])).toEqual([mid1, mid2]) + expect(mergeMiddlewares([mid1], [mid2])).toEqual([mid1, mid2]) + expect(mergeMiddlewares([mid1], [mid1, mid2])).toEqual([mid1, mid2]) + expect(mergeMiddlewares([mid1, mid3], [mid1, mid2])).toEqual([mid1, mid3, mid2]) + expect(mergeMiddlewares([mid1, mid3], [mid1, mid2, mid3])).toEqual([mid1, mid3, mid2, mid3]) +}) + +it('addMiddleware', () => { + expect(addMiddleware([], mid1)).toEqual([mid1]) + expect(addMiddleware([mid1], mid2)).toEqual([mid1, mid2]) + expect(addMiddleware([mid1], mid2)).toEqual([mid1, mid2]) +}) diff --git a/packages/server/src/middleware-utils.ts b/packages/server/src/middleware-utils.ts new file mode 100644 index 00000000..db3a6e26 --- /dev/null +++ b/packages/server/src/middleware-utils.ts @@ -0,0 +1,25 @@ +import type { AnyMiddleware } from './middleware' + +export function dedupeMiddlewares(compare: AnyMiddleware[], middlewares: AnyMiddleware[]): AnyMiddleware[] { + let min = 0 + + for (let i = 0; i < middlewares.length; i++) { + const index = compare.indexOf(middlewares[i]!, min) + + if (index === -1) { + return middlewares.slice(i) + } + + min = index + 1 + } + + return [] +} + +export function mergeMiddlewares(first: AnyMiddleware[], second: AnyMiddleware[]): AnyMiddleware[] { + return [...first, ...dedupeMiddlewares(first, second)] +} + +export function addMiddleware(middlewares: AnyMiddleware[], addition: AnyMiddleware): AnyMiddleware[] { + return [...middlewares, addition] +} diff --git a/packages/server/src/middleware.test-d.ts b/packages/server/src/middleware.test-d.ts index 48141ed2..f38ef8a3 100644 --- a/packages/server/src/middleware.test-d.ts +++ b/packages/server/src/middleware.test-d.ts @@ -1,112 +1,55 @@ +import type { ErrorMap, ORPCErrorConstructorMap, Schema } from '@orpc/contract' +import type { baseErrorMap, BaseMeta } from '../../contract/tests/shared' import type { Context } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { Middleware, MiddlewareNextFn, MiddlewareOptions, MiddlewareOutputFn } from './middleware' -import type { ANY_PROCEDURE } from './procedure' -import { z } from 'zod' - -const baseErrors = { - code: { - status: 500, - message: 'Internal Server Error', - data: z.object({ - why: z.string(), - }), - }, -} +import type { Middleware, MiddlewareNextFn, MiddlewareOutputFn } from './middleware' +import type { Procedure } from './procedure' describe('middleware', () => { it('just a function', () => { - const mid: Middleware<{ auth: boolean }, Record, unknown, unknown, ORPCErrorConstructorMap> = ({ context, path, procedure, signal, next, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(signal).toEqualTypeOf>() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(next).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({}) - } - - const mid2: Middleware<{ auth: boolean }, Record, unknown, unknown, Record> = async ({ context, path, procedure, signal, next }, input, output) => { - expectTypeOf(input).toEqualTypeOf() + const mid: Middleware< + { auth: boolean }, + Record, + { input: number }, + { output: string }, + ORPCErrorConstructorMap, + BaseMeta + > = ({ context, path, procedure, signal, next, errors }, input, output) => { + expectTypeOf(input).toEqualTypeOf<{ input: number }>() expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf< + Procedure + >() expectTypeOf(signal).toEqualTypeOf>() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(next).toEqualTypeOf>() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(next).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() - return await next() + return next() } // @ts-expect-error - missing return type const mid3: Middleware<{ auth: boolean }, undefined, unknown, unknown> = () => { } - - // @ts-expect-error - missing return type - const mid4: Middleware<{ auth: boolean }, undefined, unknown, unknown> = async () => { - } }) it('require return valid extra context', () => { - const mid0: Middleware, unknown, unknown, Record> = ({ next }) => { - return next({ }) + const mid0: Middleware, unknown, unknown, Record, BaseMeta> = ({ next }) => { + return next() } - const mid: Middleware> = ({ next }) => { + const mid: Middleware, BaseMeta> = ({ next }) => { return next({ context: { userId: '1' } }) } // @ts-expect-error invalid extra context - const mid2: Middleware = ({ next }) => { + const mid2: Middleware = ({ next }) => { return next({ context: { userId: 1 } }) } - const mid3: Middleware> = ({ next }) => { - return next({ - context: { - userId: '1', - }, - }) - } - }) - - it('can type input', () => { - const mid: Middleware, { id: string }, unknown, Record> = ({ next }, input) => { - expectTypeOf(input).toEqualTypeOf<{ id: string }>() - - return next({}) - } - }) - - it('can type output', () => { - const mid: Middleware, unknown, { id: string }, Record> = async ({ next }, input, output) => { - const result = await next({}) - - expectTypeOf(result.output).toEqualTypeOf<{ id: string }>() - - return output({ id: '1' }) + // @ts-expect-error require return extra context + const mid3: Middleware = ({ next }) => { + return next() } - - const mid2: Middleware, unknown, { id: string }, Record> = async (_, __, output) => { - // @ts-expect-error invalid output - return output({ id: 123 }) - } - }) - - it('can infer types from function', () => { - const handler = ({ next }: MiddlewareOptions<{ context: 'context' }, 'output', Record>, input: 'input', output: MiddlewareOutputFn<'output'>) => { - return next({ context: { extra: 'extra' as const } }) - } - - type Inferred = typeof handler extends Middleware - ? [TInContext, TOutContext, TInput, TOutput, TErrorConstructorMap] - : never - - expectTypeOf().toEqualTypeOf< - [{ context: 'context' }, { extra: 'extra' }, 'input', 'output', Record] - >() }) }) diff --git a/packages/server/src/middleware.ts b/packages/server/src/middleware.ts index 6d1fdc9c..9a2358f8 100644 --- a/packages/server/src/middleware.ts +++ b/packages/server/src/middleware.ts @@ -1,8 +1,7 @@ +import type { AbortSignal, ErrorMap, Meta, ORPCErrorConstructorMap, Schema } from '@orpc/contract' import type { Promisable } from '@orpc/shared' import type { Context } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { ANY_PROCEDURE } from './procedure' -import type { AbortSignal } from './types' +import type { Procedure } from './procedure' export type MiddlewareResult = Promisable<{ output: TOutput @@ -29,10 +28,11 @@ export interface MiddlewareOptions< TInContext extends Context, TOutput, TErrorConstructorMap extends ORPCErrorConstructorMap, + TMeta extends Meta, > { context: TInContext path: string[] - procedure: ANY_PROCEDURE + procedure: Procedure signal?: AbortSignal next: MiddlewareNextFn errors: TErrorConstructorMap @@ -44,9 +44,10 @@ export interface Middleware< TInput, TOutput, TErrorConstructorMap extends ORPCErrorConstructorMap, + TMeta extends Meta, > { ( - options: MiddlewareOptions, + options: MiddlewareOptions, input: TInput, output: MiddlewareOutputFn, ): Promisable< @@ -54,14 +55,12 @@ export interface Middleware< > } -export type ANY_MIDDLEWARE = Middleware +export type AnyMiddleware = Middleware export interface MapInputMiddleware { (input: TInput): TMappedInput } -export type ANY_MAP_INPUT_MIDDLEWARE = MapInputMiddleware - export function middlewareOutputFn(output: TOutput): MiddlewareResult, TOutput> { return { output, context: {} } } diff --git a/packages/server/src/procedure-builder-with-input.test-d.ts b/packages/server/src/procedure-builder-with-input.test-d.ts deleted file mode 100644 index b3ded772..00000000 --- a/packages/server/src/procedure-builder-with-input.test-d.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { Route } from '@orpc/contract' -import type { ORPCErrorConstructorMap } from './error' -import type { MiddlewareOutputFn } from './middleware' -import type { ANY_PROCEDURE } from './procedure' -import type { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import type { DecoratedProcedure } from './procedure-decorated' -import type { ProcedureImplementer } from './procedure-implementer' -import { z } from 'zod' - -const baseErrors = { - BASE: { - status: 402, - message: 'default message', - data: z.object({ - why: z.string(), - }), - }, -} - -const inputSchema = z.object({ input: z.string().transform(v => Number.parseInt(v)) }) - -const builder = {} as ProcedureBuilderWithInput<{ db: string }, { db: string } & { auth?: boolean }, typeof inputSchema, typeof baseErrors> - -const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) - -describe('ProcedureBuilderWithInput', () => { - it('.errors', () => { - const errors = { CODE: { message: 'MESSAGE' } } - - expectTypeOf(builder.errors(errors)).toEqualTypeOf< - ProcedureBuilderWithInput<{ db: string }, { db: string } & { auth?: boolean }, typeof inputSchema, typeof baseErrors & typeof errors> - >() - - // @ts-expect-error - not allow redefine error map - builder.errors({ BASE: baseErrors.BASE }) - }) - - it('.route', () => { - expectTypeOf(builder.route({ tags: ['a'] })).toEqualTypeOf() - }) - - describe('.use', () => { - it('without map input', () => { - const applied = builder.use(({ context, next, path, procedure, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf<{ input: number }>() - expectTypeOf(context).toEqualTypeOf<{ db: string } & { auth?: boolean }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({ - context: { - extra: true, - }, - }) - }) - - expectTypeOf(applied).toEqualTypeOf< - ProcedureBuilderWithInput<{ db: string }, { db: string } & { auth?: boolean } & { extra: boolean }, typeof inputSchema, typeof baseErrors> - >() - - // @ts-expect-error --- conflict context - builder.use(({ next }) => next({ context: { db: 123 } })) - // @ts-expect-error --- input is not match - builder.use(({ next }, input: 'invalid') => next({})) - // @ts-expect-error --- output is not match - builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) - // conflict context but not detected - expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toEqualTypeOf() - }) - - it('with map input', () => { - const applied = builder.use(({ context, next, path, procedure, errors }, input: { mapped: number }, output) => { - expectTypeOf(input).toEqualTypeOf<{ mapped: number }>() - expectTypeOf(context).toEqualTypeOf<{ db: string } & { auth?: boolean }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({ - context: { - extra: true, - }, - }) - }, (input) => { - expectTypeOf(input).toEqualTypeOf<{ input: number }>() - return { mapped: input.input } - }) - - expectTypeOf(applied).toEqualTypeOf< - ProcedureBuilderWithInput<{ db: string }, { db: string } & { auth?: boolean } & { extra: boolean }, typeof inputSchema, typeof baseErrors> - >() - - // @ts-expect-error --- conflict context - builder.use(({ next }) => ({ context: { db: 123 } }), () => {}) - // @ts-expect-error --- input is not match - builder.use(({ next }, input: 'invalid') => next({}), () => {}) - // @ts-expect-error --- output is not match - builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}), () => {}) - // conflict context but not detected - expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }), () => {})).toEqualTypeOf() - }) - }) - - it('.output', () => { - expectTypeOf(builder.output(schema)).toEqualTypeOf< - ProcedureImplementer<{ db: string }, { db: string } & { auth?: boolean }, typeof inputSchema, typeof schema, typeof baseErrors, Route> - >() - }) - - it('.handler', () => { - const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { - expectTypeOf(input).toEqualTypeOf<{ input: number }>() - expectTypeOf(context).toEqualTypeOf<{ db: string } & { auth?: boolean }>() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(signal).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return 456 - }) - - expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure<{ db: string }, { db: string } & { auth?: boolean }, typeof inputSchema, undefined, number, typeof baseErrors, Route> - >() - }) -}) diff --git a/packages/server/src/procedure-builder-with-input.test.ts b/packages/server/src/procedure-builder-with-input.test.ts deleted file mode 100644 index 51be76ba..00000000 --- a/packages/server/src/procedure-builder-with-input.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { ContractProcedure } from '@orpc/contract' -import { z } from 'zod' -import * as middlewareDecorated from './middleware-decorated' -import { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import { DecoratedProcedure } from './procedure-decorated' -import { ProcedureImplementer } from './procedure-implementer' - -const decorateMiddlewareSpy = vi.spyOn(middlewareDecorated, 'decorateMiddleware') - -const baseErrors = { - BASE: { - status: 402, - message: 'default message', - data: z.object({ - why: z.string(), - }), - }, -} - -const mid = vi.fn() - -const inputSchema = z.object({ input: z.string().transform(v => Number.parseInt(v)) }) - -const builder = new ProcedureBuilderWithInput({ - middlewares: [mid], - inputValidationIndex: 1, - outputValidationIndex: 1, - contract: new ContractProcedure({ - InputSchema: inputSchema, - OutputSchema: undefined, - errorMap: baseErrors, - route: {}, - }), -}) - -const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) - -describe('procedureBuilderWithInput', () => { - it('.errors', () => { - const errors = { CODE: { message: 'MESSAGE' } } - const applied = builder.errors(errors) - - expect(applied).toBeInstanceOf(ProcedureBuilderWithInput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toEqual(inputSchema) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual({ - ...baseErrors, - ...errors, - }) - }) - - it('.route', () => { - const applied = builder.route({ tags: ['a'] }) - - expect(applied).toBeInstanceOf(ProcedureBuilderWithInput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toEqual(inputSchema) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) - - describe('.use', () => { - it('without map input', () => { - const mid2 = vi.fn() - - const applied = builder.use(mid2) - - expect(applied).toBeInstanceOf(ProcedureBuilderWithInput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].middlewares).toEqual([mid, mid2]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(2) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toEqual(inputSchema) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) - - it('with map input', () => { - const mappedMid = vi.fn() - const mapInput = vi.fn(() => mappedMid) - decorateMiddlewareSpy.mockReturnValueOnce({ mapInput } as any) - - const mid2 = vi.fn() - const mid2MapInput = vi.fn() - - const applied = builder.use(mid2, mid2MapInput) - - expect(applied).toBeInstanceOf(ProcedureBuilderWithInput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].middlewares).toEqual([mid, mappedMid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(2) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toEqual(inputSchema) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - - expect(decorateMiddlewareSpy).toHaveBeenCalledWith(mid2) - expect(mapInput).toHaveBeenCalledWith(mid2MapInput) - }) - }) - - it('.output', () => { - const applied = builder.output(schema) - - expect(applied).toBeInstanceOf(ProcedureImplementer) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toEqual(inputSchema) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toEqual(schema) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) - - it('.handler', () => { - const handler = vi.fn() - const applied = builder.handler(handler) - - expect(applied).toBeInstanceOf(DecoratedProcedure) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toEqual(inputSchema) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) -}) diff --git a/packages/server/src/procedure-builder-with-input.ts b/packages/server/src/procedure-builder-with-input.ts deleted file mode 100644 index bdaeb6ac..00000000 --- a/packages/server/src/procedure-builder-with-input.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { ContractProcedure, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, Route, Schema, SchemaOutput } from '@orpc/contract' -import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { MapInputMiddleware, Middleware } from './middleware' -import type { ProcedureHandler } from './procedure' -import { ContractProcedureBuilderWithInput, DecoratedContractProcedure } from '@orpc/contract' -import { decorateMiddleware } from './middleware-decorated' -import { DecoratedProcedure } from './procedure-decorated' -import { ProcedureImplementer } from './procedure-implementer' - -export interface ProcedureBuilderWithInputDef< - TInitialContext extends Context, - TCurrentContext extends Context, - TInputSchema extends Schema, - TErrorMap extends ErrorMap, -> { - __initialContext?: TypeInitialContext - __currentContext?: TypeCurrentContext - contract: ContractProcedure - middlewares: Middleware[] - inputValidationIndex: number - outputValidationIndex: number -} - -/** - * `ProcedureBuilderWithInput` is a branch of `ProcedureBuilder` which it has input schema. - * - * Why? - * - prevents override input schema after .input - * - allows .use between .input and .output - * - */ -export class ProcedureBuilderWithInput< - TInitialContext extends Context, - TCurrentContext extends Context, - TInputSchema extends Schema, - TErrorMap extends ErrorMap, -> { - '~type' = 'ProcedureBuilderWithInput' as const - '~orpc': ProcedureBuilderWithInputDef - - constructor(def: ProcedureBuilderWithInputDef) { - this['~orpc'] = def - } - - errors & ErrorMapSuggestions>( - errors: U, - ): ProcedureBuilderWithInput { - return new ProcedureBuilderWithInput({ - ...this['~orpc'], - contract: DecoratedContractProcedure - .decorate(this['~orpc'].contract) - .errors(errors), - }) - } - - route(route: Route): ProcedureBuilderWithInput { - return new ProcedureBuilderWithInput({ - ...this['~orpc'], - contract: DecoratedContractProcedure - .decorate(this['~orpc'].contract) - .route(route), - }) - } - - use( - middleware: Middleware, unknown, ORPCErrorConstructorMap>, - ): ConflictContextGuard - & ProcedureBuilderWithInput - - use( - middleware: Middleware>, - mapInput: MapInputMiddleware, UInput>, - ): ConflictContextGuard & - ProcedureBuilderWithInput - - use( - middleware: Middleware, - mapInput?: MapInputMiddleware, - ): ProcedureBuilderWithInput { - const maybeWithMapInput = mapInput - ? decorateMiddleware(middleware).mapInput(mapInput) - : middleware - - // TODO: order of middleware before/after validation? - - return new ProcedureBuilderWithInput({ - ...this['~orpc'], - outputValidationIndex: this['~orpc'].outputValidationIndex + 1, - middlewares: [...this['~orpc'].middlewares, maybeWithMapInput], - }) - } - - output( - schema: U, - example?: SchemaOutput, - ): ProcedureImplementer { - return new ProcedureImplementer({ - ...this['~orpc'], - contract: new ContractProcedureBuilderWithInput(this['~orpc'].contract['~orpc']).output(schema, example), - }) - } - - handler( - handler: ProcedureHandler, - ): DecoratedProcedure { - return new DecoratedProcedure({ - ...this['~orpc'], - handler, - }) - } -} diff --git a/packages/server/src/procedure-builder-with-output.test-d.ts b/packages/server/src/procedure-builder-with-output.test-d.ts deleted file mode 100644 index eaa77df2..00000000 --- a/packages/server/src/procedure-builder-with-output.test-d.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { Route } from '@orpc/contract' -import type { ORPCErrorConstructorMap } from './error' -import type { MiddlewareOutputFn } from './middleware' -import type { ANY_PROCEDURE } from './procedure' -import type { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import type { DecoratedProcedure } from './procedure-decorated' -import type { ProcedureImplementer } from './procedure-implementer' -import { z } from 'zod' - -const baseErrors = { - BASE: { - status: 402, - message: 'default message', - data: z.object({ - why: z.string(), - }), - }, -} - -const outputSchema = z.object({ output: z.string().transform(v => Number.parseInt(v)) }) - -const builder = {} as ProcedureBuilderWithOutput<{ db: string }, { db: string } & { auth?: boolean }, typeof outputSchema, typeof baseErrors> - -const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) - -describe('ProcedureBuilderWithOutput', () => { - it('.errors', () => { - const errors = { CODE: { message: 'MESSAGE' } } - - expectTypeOf(builder.errors(errors)).toEqualTypeOf< - ProcedureBuilderWithOutput < { db: string }, { db: string } & { auth?: boolean }, typeof outputSchema, typeof baseErrors & typeof errors> - >() - - // @ts-expect-error - not allow redefine error map - builder.errors({ BASE: baseErrors.BASE }) - }) - - it('.route', () => { - expectTypeOf(builder.route({ tags: ['a'] })).toEqualTypeOf() - }) - - describe('.use', () => { - const applied = builder.use(({ context, next, path, procedure, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string } & { auth?: boolean }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({ - context: { - extra: true, - }, - }) - }) - - expectTypeOf(applied).toEqualTypeOf< - ProcedureBuilderWithOutput < { db: string }, { db: string } & { auth?: boolean } & { extra: boolean }, typeof outputSchema, typeof baseErrors> - >() - - // @ts-expect-error --- conflict context - builder.use(({ next }) => next({ context: { db: 123 } })) - // @ts-expect-error --- input is not match - builder.use(({ next }, input: 'invalid') => next({})) - // @ts-expect-error --- output is not match - builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) - // conflict context but not detected - expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toEqualTypeOf() - }) - - it('.input', () => { - expectTypeOf(builder.input(schema)).toEqualTypeOf< - ProcedureImplementer<{ db: string }, { db: string } & { auth?: boolean }, typeof schema, typeof outputSchema, typeof baseErrors, Route> - >() - }) - - it('.handler', () => { - const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string } & { auth?: boolean }>() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(signal).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return { output: '123' } - }) - - expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure<{ db: string }, { db: string } & { auth?: boolean }, undefined, typeof outputSchema, { output: string }, typeof baseErrors, Route> - >() - - // @ts-expect-error --- invalid output - builder.handler(() => ({ output: 123 })) - }) -}) diff --git a/packages/server/src/procedure-builder-with-output.test.ts b/packages/server/src/procedure-builder-with-output.test.ts deleted file mode 100644 index ad5bc5ed..00000000 --- a/packages/server/src/procedure-builder-with-output.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { ContractProcedure } from '@orpc/contract' -import { z } from 'zod' -import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import { DecoratedProcedure } from './procedure-decorated' -import { ProcedureImplementer } from './procedure-implementer' - -const baseErrors = { - BASE: { - status: 402, - message: 'default message', - data: z.object({ - why: z.string(), - }), - }, -} - -const mid = vi.fn() - -const outputSchema = z.object({ input: z.string().transform(v => Number.parseInt(v)) }) - -const builder = new ProcedureBuilderWithOutput({ - middlewares: [mid], - inputValidationIndex: 1, - outputValidationIndex: 1, - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: outputSchema, - errorMap: baseErrors, - route: {}, - }), -}) - -const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) - -describe('procedureBuilderWithOutput', () => { - it('.errors', () => { - const errors = { CODE: { message: 'MESSAGE' } } - const applied = builder.errors(errors) - - expect(applied).toBeInstanceOf(ProcedureBuilderWithOutput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toEqual(outputSchema) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual({ - ...baseErrors, - ...errors, - }) - }) - - it('.route', () => { - const applied = builder.route({ tags: ['a'] }) - - expect(applied).toBeInstanceOf(ProcedureBuilderWithOutput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toEqual(outputSchema) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) - - it('.use', () => { - const mid2 = vi.fn() - - const applied = builder.use(mid2) - - expect(applied).toBeInstanceOf(ProcedureBuilderWithOutput) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].middlewares).toEqual([mid, mid2]) - expect(applied['~orpc'].inputValidationIndex).toEqual(2) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toEqual(outputSchema) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) - - it('.input', () => { - const applied = builder.input(schema) - - expect(applied).toBeInstanceOf(ProcedureImplementer) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toEqual(outputSchema) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toEqual(schema) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) - - it('.handler', () => { - const handler = vi.fn() - const applied = builder.handler(handler) - - expect(applied).toBeInstanceOf(DecoratedProcedure) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toEqual(outputSchema) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) -}) diff --git a/packages/server/src/procedure-builder-with-output.ts b/packages/server/src/procedure-builder-with-output.ts deleted file mode 100644 index e3963503..00000000 --- a/packages/server/src/procedure-builder-with-output.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { ContractProcedure, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, Route, Schema, SchemaInput } from '@orpc/contract' -import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { Middleware } from './middleware' -import type { ProcedureHandler } from './procedure' -import { ContractProcedureBuilderWithOutput, DecoratedContractProcedure } from '@orpc/contract' -import { DecoratedProcedure } from './procedure-decorated' -import { ProcedureImplementer } from './procedure-implementer' - -export interface ProcedureBuilderWithOutputDef< - TInitialContext extends Context, - TCurrentContext extends Context, - TOutputSchema extends Schema, - TErrorMap extends ErrorMap, -> { - __initialContext?: TypeInitialContext - __currentContext?: TypeCurrentContext - contract: ContractProcedure - middlewares: Middleware[] - inputValidationIndex: number - outputValidationIndex: number -} - -/** - * `ProcedureBuilderWithOutput` is a branch of `ProcedureBuilder` which it has output schema. - * - * Why? - * - prevents override output schema after .output - * - allows .use between .input and .output - * - */ -export class ProcedureBuilderWithOutput< - TInitialContext extends Context, - TCurrentContext extends Context, - TOutputSchema extends Schema, - TErrorMap extends ErrorMap, -> { - '~type' = 'ProcedureBuilderWithOutput' as const - '~orpc': ProcedureBuilderWithOutputDef - - constructor(def: ProcedureBuilderWithOutputDef) { - this['~orpc'] = def - } - - errors & ErrorMapSuggestions>( - errors: U, - ): ProcedureBuilderWithOutput { - return new ProcedureBuilderWithOutput({ - ...this['~orpc'], - contract: DecoratedContractProcedure - .decorate(this['~orpc'].contract) - .errors(errors), - }) - } - - route(route: Route): ProcedureBuilderWithOutput { - return new ProcedureBuilderWithOutput({ - ...this['~orpc'], - contract: DecoratedContractProcedure - .decorate(this['~orpc'].contract) - .route(route), - }) - } - - use( - middleware: Middleware, ORPCErrorConstructorMap>, - ): ConflictContextGuard - & ProcedureBuilderWithOutput { - const builder = new ProcedureBuilderWithOutput({ - contract: this['~orpc'].contract, - outputValidationIndex: this['~orpc'].outputValidationIndex, - inputValidationIndex: this['~orpc'].inputValidationIndex + 1, - middlewares: [...this['~orpc'].middlewares, middleware], - }) - - return builder as typeof builder & ConflictContextGuard - } - - input( - schema: U, - example?: SchemaInput, - ): ProcedureImplementer { - return new ProcedureImplementer({ - ...this['~orpc'], - contract: new ContractProcedureBuilderWithOutput(this['~orpc'].contract['~orpc']).input(schema, example), - }) - } - - handler>( - handler: ProcedureHandler, - ): DecoratedProcedure { - return new DecoratedProcedure({ - ...this['~orpc'], - handler, - }) - } -} diff --git a/packages/server/src/procedure-builder.test-d.ts b/packages/server/src/procedure-builder.test-d.ts deleted file mode 100644 index 704ada93..00000000 --- a/packages/server/src/procedure-builder.test-d.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { Route } from '@orpc/contract' -import type { ORPCErrorConstructorMap } from './error' -import type { MiddlewareOutputFn } from './middleware' -import type { ANY_PROCEDURE } from './procedure' -import type { ProcedureBuilder } from './procedure-builder' -import type { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import type { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import type { DecoratedProcedure } from './procedure-decorated' -import { z } from 'zod' - -const baseErrors = { - BASE: { - status: 402, - message: 'default message', - data: z.object({ - why: z.string(), - }), - }, -} - -const builder = {} as ProcedureBuilder<{ db: string }, { db: string } & { auth?: boolean }, typeof baseErrors> - -const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) - -describe('ProcedureBuilder', () => { - it('.errors', () => { - const errors = { CODE: { message: 'MESSAGE' } } - - expectTypeOf(builder.errors(errors)).toEqualTypeOf< - ProcedureBuilder < { db: string }, { db: string } & { auth?: boolean }, typeof baseErrors & typeof errors> - >() - - // @ts-expect-error - not allow redefine error map - builder.errors({ BASE: baseErrors.BASE }) - }) - - it('.route', () => { - expectTypeOf(builder.route({ tags: ['a'] })).toEqualTypeOf() - }) - - it('.use', () => { - const applied = builder.use(({ context, next, path, procedure, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string } & { auth?: boolean }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({ - context: { - extra: true, - }, - }) - }) - - expectTypeOf(applied).toEqualTypeOf< - ProcedureBuilder < { db: string }, { db: string } & { auth?: boolean } & { extra: boolean }, typeof baseErrors> - >() - - // @ts-expect-error --- conflict context - builder.use(({ next }) => next({ context: { db: 123 } })) - // @ts-expect-error --- input is not match - builder.use(({ next }, input: 'invalid') => next({})) - // @ts-expect-error --- output is not match - builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) - - // conflict context but not detected - expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toEqualTypeOf() - }) - - it('.input', () => { - expectTypeOf(builder.input(schema)).toEqualTypeOf< - ProcedureBuilderWithInput < { db: string }, { db: string } & { auth?: boolean }, typeof schema, typeof baseErrors> - >() - }) - - it('.output', () => { - expectTypeOf(builder.output(schema)).toEqualTypeOf< - ProcedureBuilderWithOutput<{ db: string }, { db: string } & { auth?: boolean }, typeof schema, typeof baseErrors> - >() - }) - - it('.handler', () => { - const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string } & { auth?: boolean }>() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(signal).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return 456 - }) - - expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure<{ db: string }, { db: string } & { auth?: boolean }, undefined, undefined, number, typeof baseErrors, Route> - >() - }) -}) diff --git a/packages/server/src/procedure-builder.test.ts b/packages/server/src/procedure-builder.test.ts deleted file mode 100644 index 0e32d6b8..00000000 --- a/packages/server/src/procedure-builder.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { ContractProcedure } from '@orpc/contract' -import { z } from 'zod' -import { ProcedureBuilder } from './procedure-builder' -import { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import { DecoratedProcedure } from './procedure-decorated' - -const baseErrors = { - BASE: { - status: 402, - message: 'default message', - data: z.object({ - why: z.string(), - }), - }, -} - -const mid = vi.fn() - -const builder = new ProcedureBuilder({ - middlewares: [mid], - inputValidationIndex: 1, - outputValidationIndex: 1, - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - errorMap: baseErrors, - route: {}, - }), -}) - -const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) - -describe('procedureBuilder', () => { - it('.errors', () => { - const errors = { CODE: { message: 'MESSAGE' } } - const applied = builder.errors(errors) - - expect(applied).toBeInstanceOf(ProcedureBuilder) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual({ - ...baseErrors, - ...errors, - }) - }) - - it('.route', () => { - const applied = builder.route({ tags: ['a'] }) - - expect(applied).toBeInstanceOf(ProcedureBuilder) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) - - it('.use', () => { - const mid2 = vi.fn() - - const applied = builder.use(mid2) - - expect(applied).toBeInstanceOf(ProcedureBuilder) - expect(applied).not.toBe(builder) - expect(applied['~orpc'].middlewares).toEqual([mid, mid2]) - expect(applied['~orpc'].inputValidationIndex).toEqual(2) - expect(applied['~orpc'].outputValidationIndex).toEqual(2) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) - - it('.input', () => { - const applied = builder.input(schema) - - expect(applied).toBeInstanceOf(ProcedureBuilderWithInput) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toEqual(schema) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) - - it('.output', () => { - const applied = builder.output(schema) - - expect(applied).toBeInstanceOf(ProcedureBuilderWithOutput) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toEqual(schema) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) - - it('.handler', () => { - const handler = vi.fn() - const applied = builder.handler(handler) - - expect(applied).toBeInstanceOf(DecoratedProcedure) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) -}) diff --git a/packages/server/src/procedure-builder.ts b/packages/server/src/procedure-builder.ts deleted file mode 100644 index 4801a504..00000000 --- a/packages/server/src/procedure-builder.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { ContractProcedure, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, Route, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { Middleware } from './middleware' -import type { ProcedureHandler } from './procedure' -import { ContractProcedureBuilder, DecoratedContractProcedure } from '@orpc/contract' -import { ProcedureBuilderWithInput } from './procedure-builder-with-input' -import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' -import { DecoratedProcedure } from './procedure-decorated' - -export interface ProcedureBuilderDef { - __initialContext?: TypeInitialContext - __currentContext?: TypeCurrentContext - contract: ContractProcedure - middlewares: Middleware[] - inputValidationIndex: number - outputValidationIndex: number -} - -export class ProcedureBuilder { - '~type' = 'ProcedureBuilder' as const - '~orpc': ProcedureBuilderDef - - constructor(def: ProcedureBuilderDef) { - this['~orpc'] = def - } - - errors & ErrorMapSuggestions>( - errors: U, - ): ProcedureBuilder { - return new ProcedureBuilder({ - ...this['~orpc'], - contract: DecoratedContractProcedure - .decorate(this['~orpc'].contract) - .errors(errors), - }) - } - - route(route: Route): ProcedureBuilder { - return new ProcedureBuilder({ - ...this['~orpc'], - contract: DecoratedContractProcedure - .decorate(this['~orpc'].contract) - .route(route), - }) - } - - use( - middleware: Middleware< - TCurrentContext, - U, - unknown, - unknown, - ORPCErrorConstructorMap - >, - ): ConflictContextGuard - & ProcedureBuilder { - const builder = new ProcedureBuilder({ - contract: this['~orpc'].contract, - inputValidationIndex: this['~orpc'].inputValidationIndex + 1, - outputValidationIndex: this['~orpc'].outputValidationIndex + 1, - middlewares: [...this['~orpc'].middlewares, middleware as any], - }) - - return builder as typeof builder & ConflictContextGuard - } - - input(schema: U, example?: SchemaInput): ProcedureBuilderWithInput { - return new ProcedureBuilderWithInput({ - ...this['~orpc'], - contract: new ContractProcedureBuilder(this['~orpc'].contract['~orpc']).input(schema, example), - }) - } - - output(schema: U, example?: SchemaOutput): ProcedureBuilderWithOutput { - return new ProcedureBuilderWithOutput({ - ...this['~orpc'], - contract: new ContractProcedureBuilder(this['~orpc'].contract['~orpc']).output(schema, example), - }) - } - - handler( - handler: ProcedureHandler, - ): DecoratedProcedure { - return new DecoratedProcedure({ - ...this['~orpc'], - handler, - }) - } -} diff --git a/packages/server/src/procedure-client.test-d.ts b/packages/server/src/procedure-client.test-d.ts index 81561920..d1905151 100644 --- a/packages/server/src/procedure-client.test-d.ts +++ b/packages/server/src/procedure-client.test-d.ts @@ -1,244 +1,80 @@ -import type { Client, ORPCError, Route } from '@orpc/contract' -import type { Context } from './context' -import type { Procedure } from './procedure' -import type { ProcedureClient } from './procedure-client' -import type { Meta, WithSignal } from './types' -import { z } from 'zod' -import { lazy } from './lazy' -import { createProcedureClient } from './procedure-client' - -beforeEach(() => { - vi.resetAllMocks() -}) - -const baseSchema = z.string().transform(v => Number(v)) -const baseErrors = { - CODE: { - data: z.object({ why: z.string().transform(v => Number(v)) }), - }, -} - -const optionalBaseSchema = baseSchema.optional() +import type { baseErrorMap, inputSchema, outputSchema } from '../../contract/tests/shared' +import { type Client, type ORPCError, safe } from '@orpc/contract' +import { ping, pong } from '../tests/shared' +import { createProcedureClient, type ProcedureClient } from './procedure-client' describe('ProcedureClient', () => { - const fn: ProcedureClient = async (...[input, options]) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(options).toEqualTypeOf<(WithSignal & { context?: unknown }) | undefined>() - return 123 - } - - const fnWithOptionalInput: ProcedureClient = async (...args) => { - const [input, options] = args - - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(options).toEqualTypeOf<(WithSignal & { context?: unknown }) | undefined>() - return 123 - } - - it('just a client', () => { - expectTypeOf(fn).toEqualTypeOf>>() + const client = {} as ProcedureClient< + 'client-context', + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap + > + + it('is a client', () => { + expectTypeOf(client).toMatchTypeOf< + Client< + 'client-context', + { input: number }, + { output: string }, + Error | ORPCError<'BASE', { output: string }> + > + >() }) - it('just a function', () => { - expectTypeOf(fn).toMatchTypeOf<(input: string, options: WithSignal & { context?: unknown }) => Promise>() - expectTypeOf(fnWithOptionalInput).toMatchTypeOf<(input: string | undefined, options: WithSignal & { context?: unknown }) => Promise>() - }) + it('works', async () => { + const [output, error, isDefined] = await safe(client({ input: 123 }, { context: 'client-context' })) - it('infer correct input', () => { - fn('123') - fnWithOptionalInput('123') + if (!error) { + expectTypeOf(output).toEqualTypeOf<{ output: string }>() + } - // @ts-expect-error - invalid input - fn(123) - // @ts-expect-error - invalid input - fnWithOptionalInput(123) + if (isDefined) { + expectTypeOf(error).toEqualTypeOf | ORPCError<'OVERRIDE', unknown>>() + } // @ts-expect-error - invalid input - fn({}) - // @ts-expect-error - invalid input - fnWithOptionalInput({}) + client({ input: 'INVALID' }, { context: 'client-context' }) + // @ts-expect-error - invalid client context + client({ input: 123 }, { context: 'INVALID' }) + // @ts-expect-error - client context is required + client({ input: 123 }) }) - it('accept signal', () => { - fn('123', { signal: new AbortSignal() }) - fnWithOptionalInput('123', { signal: new AbortSignal() }) + it('can fallback to handler output', async () => { + const client = {} as ProcedureClient - // @ts-expect-error - invalid signal - fn('123', { signal: 1234 }) - // @ts-expect-error - invalid signal - fnWithOptionalInput('123', { signal: 1234 }) - }) + const output = await client({ input: 123 }) - it('can accept call without args', () => { - expectTypeOf(fnWithOptionalInput()).toMatchTypeOf>() - // @ts-expect-error - input is required - expectTypeOf(fn()).toEqualTypeOf>() - }) - - describe('context', () => { - it('can accept context', () => { - const client = {} as ProcedureClient<{ userId: string }, typeof baseSchema, typeof baseSchema, string, typeof baseErrors> - - client('input', { context: { userId: '123' } }) - // @ts-expect-error - invalid context - client('input', { context: { userId: 123 } }) - // @ts-expect-error - context is required - client('input') - }) - - it('optional options when context is optional', () => { - const client = {} as ProcedureClient - - client('input') - client('input', { context: { userId: '123' } }) - }) - - it('can call without args when both input and context are optional', () => { - const client = {} as ProcedureClient - - client() - client('input', { context: { userId: '123' } }) - // @ts-expect-error - input is invalid - client(123, { context: { userId: '123' } }) - // @ts-expect-error - context is invalid - client('input', { context: { userId: 123 } }) - }) + expectTypeOf(output).toEqualTypeOf<{ handler: number }>() }) }) describe('createProcedureClient', () => { - const schema = z.object({ val: z.string().transform(v => Number(v)) }) - const baseErrors = { - CODE: { - data: z.object({ why: z.string().transform(v => Number(v)) }), - }, - } - const procedure = {} as Procedure - const procedureWithContext = {} as Procedure<{ userId: string }, { db: string }, typeof schema, typeof schema, { val: string }, Record, Route> - - it('just a client', () => { - const client = createProcedureClient(procedure) - - expectTypeOf(client).toEqualTypeOf>() - }) - - it('context can be optional and can be a sync or async function', () => { - createProcedureClient(procedure) - - createProcedureClient(procedure, { - context: {}, - }) - - // @ts-expect-error - missing context - createProcedureClient(procedureWithContext) - - // @ts-expect-error - missing context - createProcedureClient(procedureWithContext, {}) - - createProcedureClient(procedureWithContext, { - context: { userId: '123' }, - }) - - createProcedureClient(procedureWithContext, { - // @ts-expect-error invalid context - context: { userId: 123 }, - }) - - createProcedureClient(procedureWithContext, { - context: () => ({ userId: '123' }), - }) - - createProcedureClient(procedureWithContext, { - // @ts-expect-error invalid context - context: () => ({ userId: 123 }), - }) - - createProcedureClient(procedureWithContext, { - context: async () => ({ userId: '123' }), - }) - - createProcedureClient(procedureWithContext, { - // @ts-expect-error invalid context - context: async () => ({ userId: 123 }), - }) - }) - - it('accept hooks', () => { - createProcedureClient(procedure, { - async interceptor(input, context, meta) { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf Promise<{ val: number }> }>() - - return { val: 123 } - }, - - onStart(state, context, meta) { - expectTypeOf(state).toEqualTypeOf<{ status: 'pending', input: unknown, output: undefined, error: undefined }>() - expectTypeOf(context).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf() - }, - - onSuccess(state, context, meta) { - expectTypeOf(state).toEqualTypeOf<{ status: 'success', input: unknown, output: { val: number }, error: undefined }>() - expectTypeOf(context).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf() - }, - - onError(state, context, meta) { - expectTypeOf(state).toEqualTypeOf<{ status: 'error', input: unknown, output: undefined, error: Error }>() - expectTypeOf(context).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf() - }, - - onFinish(state, context, meta) { - expectTypeOf(state).toEqualTypeOf<{ status: 'success', input: unknown, output: { val: number }, error: undefined } | { status: 'error', input: unknown, output: undefined, error: Error }>() - expectTypeOf(context).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf() - }, - }) - }) - - it('accept paths', () => { - createProcedureClient(procedure, { - path: ['users'], - }) - - // @ts-expect-error - invalid path - createProcedureClient(procedure, { - path: [123], - }) - }) - - it('with client context', () => { - const client = createProcedureClient(procedure, { - context: async (clientContext: { cache?: boolean } | undefined) => { - return {} - }, - }) - - client({ val: '123' }) - client({ val: '123' }, { context: { cache: true } }) + it('works with context', () => { + createProcedureClient(ping, { context: { db: 'postgres' } }) + createProcedureClient(ping, { context: () => ({ db: 'postgres' }) }) + createProcedureClient(ping, { context: async () => ({ db: 'postgres' }) }) // @ts-expect-error - invalid context - client({ val: '123' }, { context: { cache: '123' } }) + createProcedureClient(ping, { context: { db: 123 } }) + // @ts-expect-error - context is required + createProcedureClient(ping) + // context is optional + createProcedureClient(pong) }) -}) -it('support lazy procedure', () => { - const schema = z.object({ val: z.string().transform(v => Number(v)) }) - const procedure = {} as Procedure<{ userId?: string }, { userId?: string }, typeof schema, typeof schema, { val: string }, Record, Route> - const lazied = lazy(() => Promise.resolve({ default: procedure })) - - const client = createProcedureClient(lazied, { - context: async () => ({ userId: 'string' }), - path: ['users'], - - onSuccess(state, context, meta) { - expectTypeOf(state).toEqualTypeOf<{ status: 'success', input: unknown, output: { val: number }, error: undefined }>() - expectTypeOf(context).toEqualTypeOf<{ userId: string }>() - expectTypeOf(meta).toEqualTypeOf() - }, + it('can type client context', () => { + const client = createProcedureClient(ping, { context: (clientContext: 'client-context') => ({ db: 'postgres' }) }) + + expectTypeOf(client).toEqualTypeOf< + ProcedureClient< + 'client-context', + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap + > + >() }) - - expectTypeOf(client).toEqualTypeOf>>() }) diff --git a/packages/server/src/procedure-client.test.ts b/packages/server/src/procedure-client.test.ts index 5f8451ef..20d798c5 100644 --- a/packages/server/src/procedure-client.test.ts +++ b/packages/server/src/procedure-client.test.ts @@ -1,6 +1,5 @@ -import { ContractProcedure, ORPCError, validateORPCError } from '@orpc/contract' +import { createORPCErrorConstructorMap, ORPCError, validateORPCError } from '@orpc/contract' import { z } from 'zod' -import { createORPCErrorConstructorMap } from './error' import { isLazy, lazy, unlazy } from './lazy' import { Procedure } from './procedure' import { createProcedureClient } from './procedure-client' @@ -8,10 +7,6 @@ import { createProcedureClient } from './procedure-client' vi.mock('@orpc/contract', async origin => ({ ...await origin(), validateORPCError: vi.fn((map, error) => error), -})) - -vi.mock('./error', async origin => ({ - ...await origin(), createORPCErrorConstructorMap: vi.fn(), })) @@ -32,16 +27,15 @@ const baseErrors = { } const procedure = new Procedure({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: schema, - errorMap: baseErrors, - route: {}, - }), + inputSchema: schema, + outputSchema: schema, + errorMap: baseErrors, + route: {}, handler, middlewares: [preMid1, preMid2, postMid1, postMid2], inputValidationIndex: 2, outputValidationIndex: 2, + meta: {}, }) const procedureCases = [ @@ -469,8 +463,8 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce }) it('validate ORPC Error', async () => { - const e1 = new ORPCError({ code: 'BAD_REQUEST' }) - const e2 = new ORPCError({ code: 'BAD_REQUEST', defined: true }) + const e1 = new ORPCError('BAD_REQUEST') + const e2 = new ORPCError('BAD_REQUEST', { defined: true }) handler.mockRejectedValueOnce(e1) vi.mocked(validateORPCError).mockReturnValueOnce(Promise.resolve(e2)) @@ -501,12 +495,11 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce it('still work without InputSchema', async () => { const procedure = new Procedure({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: schema, - errorMap: {}, - route: {}, - }), + inputSchema: undefined, + outputSchema: schema, + errorMap: {}, + route: {}, + meta: {}, handler, middlewares: [], inputValidationIndex: 0, @@ -523,12 +516,11 @@ it('still work without InputSchema', async () => { it('still work without OutputSchema', async () => { const procedure = new Procedure({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: undefined, - errorMap: {}, - route: {}, - }), + inputSchema: schema, + outputSchema: undefined, + errorMap: {}, + route: {}, + meta: {}, handler, middlewares: [], inputValidationIndex: 0, diff --git a/packages/server/src/procedure-client.ts b/packages/server/src/procedure-client.ts index 9068d140..1266ccd9 100644 --- a/packages/server/src/procedure-client.ts +++ b/packages/server/src/procedure-client.ts @@ -3,11 +3,9 @@ import type { Hooks, Value } from '@orpc/shared' import type { Context } from './context' import type { Lazyable } from './lazy' import type { MiddlewareNextFn } from './middleware' -import type { ANY_PROCEDURE, Procedure, ProcedureHandlerOptions } from './procedure' -import type { Meta } from './types' -import { ORPCError, validateORPCError, ValidationError } from '@orpc/contract' +import type { AnyProcedure, Procedure, ProcedureHandlerOptions } from './procedure' +import { createORPCErrorConstructorMap, ORPCError, validateORPCError, ValidationError } from '@orpc/contract' import { executeWithHooks, toError, value } from '@orpc/shared' -import { createORPCErrorConstructorMap } from './error' import { unlazy } from './lazy' import { middlewareOutputFn } from './middleware' @@ -38,7 +36,7 @@ export type CreateProcedureClientOptions< | { context: Value } | (Record extends TInitialContext ? Record : never) ) - & Hooks, TInitialContext, Meta> + & Hooks, TInitialContext, any> export type CreateProcedureClientRest< TInitialContext extends Context, @@ -65,14 +63,14 @@ export function createProcedureClient< const { default: procedure } = await unlazy(lazyableProcedure) const context = await value(options?.context ?? {}, callerOptions?.context) as TInitialContext - const errors = createORPCErrorConstructorMap(procedure['~orpc'].contract['~orpc'].errorMap) + const errors = createORPCErrorConstructorMap(procedure['~orpc'].errorMap) const executeOptions = { input, context, errors, path, - procedure, + procedure: procedure as AnyProcedure, signal: callerOptions?.signal, } @@ -92,23 +90,24 @@ export function createProcedureClient< throw toError(e) } - const validated = await validateORPCError(procedure['~orpc'].contract['~orpc'].errorMap, e) + const validated = await validateORPCError(procedure['~orpc'].errorMap, e) throw validated } } } -async function validateInput(procedure: ANY_PROCEDURE, input: unknown): Promise { - const schema = procedure['~orpc'].contract['~orpc'].InputSchema - if (!schema) +async function validateInput(procedure: AnyProcedure, input: unknown): Promise { + const schema = procedure['~orpc'].inputSchema + + if (!schema) { return input + } const result = await schema['~standard'].validate(input) if (result.issues) { - throw new ORPCError({ + throw new ORPCError('BAD_REQUEST', { message: 'Input validation failed', - code: 'BAD_REQUEST', data: { issues: result.issues, }, @@ -119,16 +118,17 @@ async function validateInput(procedure: ANY_PROCEDURE, input: unknown): Promise< return result.value } -async function validateOutput(procedure: ANY_PROCEDURE, output: unknown): Promise { - const schema = procedure['~orpc'].contract['~orpc'].OutputSchema - if (!schema) +async function validateOutput(procedure: AnyProcedure, output: unknown): Promise { + const schema = procedure['~orpc'].outputSchema + + if (!schema) { return output + } const result = await schema['~standard'].validate(output) if (result.issues) { - throw new ORPCError({ + throw new ORPCError('INTERNAL_SERVER_ERROR', { message: 'Output validation failed', - code: 'INTERNAL_SERVER_ERROR', cause: new ValidationError({ message: 'Output validation failed', issues: result.issues }), }) } @@ -136,7 +136,7 @@ async function validateOutput(procedure: ANY_PROCEDURE, output: unknown): Promis return result.value } -async function executeProcedureInternal(procedure: ANY_PROCEDURE, options: ProcedureHandlerOptions): Promise { +async function executeProcedureInternal(procedure: AnyProcedure, options: ProcedureHandlerOptions): Promise { const middlewares = procedure['~orpc'].middlewares const inputValidationIndex = Math.min(Math.max(0, procedure['~orpc'].inputValidationIndex), middlewares.length) const outputValidationIndex = Math.min(Math.max(0, procedure['~orpc'].outputValidationIndex), middlewares.length) diff --git a/packages/server/src/procedure-decorated.test-d.ts b/packages/server/src/procedure-decorated.test-d.ts index db2580e1..2e979a90 100644 --- a/packages/server/src/procedure-decorated.test-d.ts +++ b/packages/server/src/procedure-decorated.test-d.ts @@ -1,268 +1,213 @@ -import type { Client, ClientRest, ORPCError, Route } from '@orpc/contract' +import type { Client, ClientRest, ErrorFromErrorMap, ErrorMap, MergedErrorMap, ORPCErrorConstructorMap, Schema } from '@orpc/contract' +import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../../contract/tests/shared' +import type { CurrentContext, InitialContext } from '../tests/shared' import type { Context } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { Middleware, MiddlewareOutputFn } from './middleware' -import type { ANY_PROCEDURE, Procedure } from './procedure' +import type { MiddlewareOutputFn } from './middleware' +import type { Procedure } from './procedure' import type { DecoratedProcedure } from './procedure-decorated' -import { z } from 'zod' -const baseSchema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) -const baseErrors = { - CODE: { - data: z.object({ why: z.string() }), - }, -} -const route = { method: 'GET', path: '/ping' } as const -const decorated = {} as DecoratedProcedure<{ auth: boolean }, { auth: boolean } & { db: string }, typeof baseSchema, typeof baseSchema, { val: string }, typeof baseErrors, typeof route> +const builder = {} as DecoratedProcedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta +> + +describe('DecoratedProcedure', () => { + it('is a procedure', () => { + expectTypeOf(builder).toMatchTypeOf< + Procedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + >() + }) -// like decorated but lost route when trying change route -const decoratedLostContract = {} as DecoratedProcedure<{ auth: boolean }, { auth: boolean } & { db: string }, typeof baseSchema, typeof baseSchema, { val: string }, typeof baseErrors, Route> + it('.errors', () => { + const applied = builder.errors({ + BAD_GATEWAY: { message: 'BAD_GATEWAY' }, + OVERRIDE: { message: 'OVERRIDE' }, + }) -describe('self chainable', () => { - it('prefix', () => { - expectTypeOf(decorated.prefix('/test')).toEqualTypeOf< - typeof decoratedLostContract + expectTypeOf(applied).toEqualTypeOf< + DecoratedProcedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + MergedErrorMap, + BaseMeta + > >() - // @ts-expect-error - invalid prefix - decorated.prefix('') - // @ts-expect-error - invalid prefix - decorated.prefix(1) + // @ts-expect-error - invalid schema + builder.errors({ BAD_GATEWAY: { data: {} } }) + // @ts-expect-error - not allow redefine error map + builder.errors({ BASE: baseErrorMap.BASE }) }) - it('route', () => { - expectTypeOf(decorated.route({ path: '/test', method: 'GET' })).toEqualTypeOf< - typeof decoratedLostContract + it('.meta', () => { + const applied = builder.meta({ mode: 'dev', log: true }) + + expectTypeOf(applied).toEqualTypeOf< + DecoratedProcedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > >() - expectTypeOf(decorated.route({ - path: '/test', - method: 'GET', - description: 'description', - summary: 'summary', - deprecated: true, - tags: ['tag1', 'tag2'], - })).toEqualTypeOf< - typeof decoratedLostContract + + // @ts-expect-error - invalid method + builder.meta({ log: 'INVALID' }) + }) + + it('.route', () => { + const applied = builder.route({ method: 'POST', path: '/v2/users', tags: ['tag'] }) + + expectTypeOf(applied).toEqualTypeOf< + DecoratedProcedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > >() // @ts-expect-error - invalid method - decorated.route({ method: 'PUTT' }) - // @ts-expect-error - invalid path - decorated.route({ path: 1 }) - // @ts-expect-error - invalid tags - decorated.route({ tags: [1] }) + builder.route({ method: 'INVALID' }) }) - describe('errors', () => { - const errors = { - BAD_GATEWAY: { - data: z.object({ - why: z.string(), - }), - }, - } + describe('.use', () => { + it('without map input', () => { + const applied = builder.use(({ context, next, path, procedure, errors }, input, output) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(path).toEqualTypeOf() + expectTypeOf(procedure).toEqualTypeOf>() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() - it('merge errors', () => { - const i = decorated.errors(errors) + return next({ + context: { + extra: true, + }, + }) + }) - expectTypeOf(i).toEqualTypeOf< + expectTypeOf(applied).toEqualTypeOf< DecoratedProcedure< - { auth: boolean }, - { auth: boolean } & { db: string }, - typeof baseSchema, - typeof baseSchema, - { val: string }, - typeof baseErrors & typeof errors, - typeof route + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta > >() - }) - it('prevent redefine old errorMap', () => { - // @ts-expect-error - not allow redefine errorMap - decorated.errors({ CODE: baseErrors.CODE }) - // @ts-expect-error - not allow redefine errorMap --- even with undefined - decorated.errors({ CODE: undefined }) + // @ts-expect-error --- conflict context + builder.use(({ next }) => next({ context: { db: 123 } })) + // @ts-expect-error --- input is not match + builder.use(({ next }, input: 'invalid') => next({})) + // @ts-expect-error --- output is not match + builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + // conflict context but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toEqualTypeOf() }) - }) - it('use middleware', () => { - const i = decorated - .use(({ context, path, next, procedure, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf<{ val: number }>() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean } & { db: string }>() + it('with map input', () => { + const applied = builder.use(({ context, next, path, procedure, errors }, input: { mapped: string }, output) => { + expectTypeOf(context).toEqualTypeOf() expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() + expectTypeOf(procedure).toEqualTypeOf>() + expectTypeOf(output).toEqualTypeOf>() + expectTypeOf(errors).toEqualTypeOf>() return next({ context: { - dev: true, + extra: true, }, }) + }, (input) => { + expectTypeOf(input).toEqualTypeOf<{ input: string }>() + return { mapped: input.input } }) - .use(({ context, path, next, procedure, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf<{ val: number }>() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean } & { db: string } & { dev: boolean }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - return next({}) - }) - - expectTypeOf(i).toEqualTypeOf< - DecoratedProcedure< - { auth: boolean }, - { auth: boolean } & { db: string } & { dev: boolean } & Record, - typeof baseSchema, - typeof baseSchema, - { val: string }, - typeof baseErrors, - typeof route - > - >() - }) - - it('use middleware with map input', () => { - const mid = {} as Middleware> + expectTypeOf(applied).toEqualTypeOf< + DecoratedProcedure< + InitialContext, + CurrentContext & { extra: boolean }, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + >() - const i = decorated.use(mid, (input) => { - expectTypeOf(input).toEqualTypeOf<{ val: number }>() - return input.val + // @ts-expect-error --- conflict context + builder.use(({ next }) => ({ context: { db: 123 } }), () => {}) + // @ts-expect-error --- input is not match + builder.use(({ next }, input: 'invalid') => next({}), () => {}) + // @ts-expect-error --- output is not match + builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}), () => {}) + // conflict context but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }), () => {})).toEqualTypeOf() }) - - expectTypeOf(i).toEqualTypeOf< - DecoratedProcedure< - { auth: boolean }, - { auth: boolean } & { db: string } & { extra: boolean }, - typeof baseSchema, - typeof baseSchema, - { val: string }, - typeof baseErrors, - typeof route - > - >() - - // @ts-expect-error - invalid input - decorated.use(mid) - - // @ts-expect-error - invalid mapped input - decorated.use(mid, input => input) }) - it('prevent conflict on context', () => { - decorated.use(({ context, path, next }, input) => next({})) - decorated.use(({ context, path, next }, input) => next({ context: { id: '1' } })) - decorated.use(({ context, path, next }, input) => next({ context: { id: '1', extra: true } })) - decorated.use(({ context, path, next }, input) => next({ context: { auth: true } })) - - decorated.use(({ context, path, next }, input) => next({}), () => 'anything') - decorated.use(({ context, path, next }, input) => next({ context: { id: '1' } }), () => 'anything') - decorated.use(({ context, path, next }, input) => next({ context: { id: '1', extra: true } }), () => 'anything') - decorated.use(({ context, path, next }, input) => next({ context: { auth: true } }), () => 'anything') - - // @ts-expect-error - conflict with context - decorated.use(({ context, path, next }, input) => next({ context: { auth: 1 } })) - - // @ts-expect-error - conflict with context - decorated.use(({ context, path, next }, input) => next({ context: { auth: 1, extra: true } })) - - // @ts-expect-error - conflict with context - decorated.use(({ context, path, next }, input) => next({ context: { auth: 1 } }), () => 'anything') - - // @ts-expect-error - conflict with context - decorated.use(({ context, path, next }, input) => next({ context: { auth: 1, extra: true } }), () => 'anything') - }) - - it('handle middleware with output is typed', () => { - const mid1 = {} as Middleware, unknown, any, Record> - const mid2 = {} as Middleware, unknown, { val: string }, Record> - const mid3 = {} as Middleware, unknown, unknown, Record> - const mid4 = {} as Middleware, unknown, { val: number }, Record> - - decorated.use(mid1) - decorated.use(mid2) - - // @ts-expect-error - required used any for output - decorated.use(mid3) - // @ts-expect-error - output is not match - decorated.use(mid4) - }) - - it('unshiftTag', () => { - expectTypeOf(decorated.unshiftTag('test')).toEqualTypeOf< - DecoratedProcedure<{ auth: boolean }, { auth: boolean } & { db: string }, typeof baseSchema, typeof baseSchema, { val: string }, typeof baseErrors, Route> - >() - expectTypeOf(decorated.unshiftTag('test', 'test2', 'test3')).toEqualTypeOf< - DecoratedProcedure<{ auth: boolean }, { auth: boolean } & { db: string }, typeof baseSchema, typeof baseSchema, { val: string }, typeof baseErrors, Route> - >() - - // @ts-expect-error - invalid tag - decorated.unshiftTag(1) - // @ts-expect-error - invalid tag - decorated.unshiftTag('123', 2) - }) - - it('unshiftMiddleware', () => { - const mid1 = {} as Middleware, unknown, any, Record> - const mid2 = {} as Middleware<{ auth: boolean }, Record, unknown, any, Record> - const mid3 = {} as Middleware<{ auth: boolean }, { dev: boolean }, unknown, { val: number }, Record> - - expectTypeOf(decorated.unshiftMiddleware(mid1)).toEqualTypeOf() - expectTypeOf(decorated.unshiftMiddleware(mid1, mid2)).toEqualTypeOf() - expectTypeOf(decorated.unshiftMiddleware(mid1, mid2, mid3)).toEqualTypeOf() - - const mid4 = {} as Middleware<{ auth: 'invalid' }, Record, unknown, any, Record> - const mid5 = {} as Middleware<{ auth: boolean }, Record, { val: number }, any, Record> - const mid7 = {} as Middleware<{ db: string }, Record, unknown, { val: number }, Record> - const mid8 = {} as Middleware> - - // @ts-expect-error - context is not match - decorated.unshiftMiddleware(mid4) - // @ts-expect-error - input is not match - decorated.unshiftMiddleware(mid5) - // @ts-expect-error - context is not match - decorated.unshiftMiddleware(mid7) - // extra context is conflict with context - expectTypeOf(decorated.unshiftMiddleware(mid8)).toEqualTypeOf() - // @ts-expect-error - invalid middleware - decorated.unshiftMiddleware(mid4, mid5, mid7, mid8) - - const mid9 = {} as Middleware> - const mid10 = {} as Middleware> - - decorated.unshiftMiddleware(mid9) - // @ts-expect-error - extra context of mid10 is conflict with extra context of mid9 - decorated.unshiftMiddleware(mid9, mid10) - - // @ts-expect-error - invalid middleware - decorated.unshiftMiddleware(1) - // @ts-expect-error - invalid middleware - decorated.unshiftMiddleware(() => { }, 1) - }) - - it('callable', () => { - const callable = decorated.callable({ - context: async (clientContext: 'something') => ({ auth: true }), + it('.callable', () => { + const applied = builder.callable({ + context: async (clientContext: 'client-context') => ({ db: 'postgres' }), }) - expectTypeOf(callable).toEqualTypeOf< - & Procedure<{ auth: boolean }, { auth: boolean } & { db: string }, typeof baseSchema, typeof baseSchema, { val: string }, typeof baseErrors, typeof route> - & Client<'something', { val: string }, { val: number }, Error | ORPCError<'CODE', { why: string }>> + expectTypeOf(applied).toEqualTypeOf< + Procedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + & Client<'client-context', { input: number }, { output: string }, ErrorFromErrorMap> >() }) - it('actionable', () => { - const actionable = decorated.actionable({ - context: async (clientContext: 'something') => ({ auth: true }), + it('.actionable', () => { + const applied = builder.actionable({ + context: async (clientContext: 'client-context') => ({ db: 'postgres' }), }) - expectTypeOf(actionable).toEqualTypeOf< - & Procedure<{ auth: boolean }, { auth: boolean } & { db: string }, typeof baseSchema, typeof baseSchema, { val: string }, typeof baseErrors, typeof route> - & ((...rest: ClientRest<'something', { val: string }>) => Promise<{ val: number }>) + expectTypeOf(applied).toEqualTypeOf< + Procedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta + > + & ((...rest: ClientRest<'client-context', { input: number }>) => Promise<{ output: string }>) >() }) }) diff --git a/packages/server/src/procedure-decorated.test.ts b/packages/server/src/procedure-decorated.test.ts index 3f20d37e..6f7e898c 100644 --- a/packages/server/src/procedure-decorated.test.ts +++ b/packages/server/src/procedure-decorated.test.ts @@ -1,9 +1,15 @@ -import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' -import { isProcedure, Procedure } from './procedure' +import { ping } from '../tests/shared' +import { isProcedure } from './procedure' import { createProcedureClient } from './procedure-client' import { DecoratedProcedure } from './procedure-decorated' +vi.mock('./middleware-decorated', () => ({ + decorateMiddleware: vi.fn(mid => ({ + mapInput: vi.fn(map => [mid, map]), + })), +})) + vi.mock('./procedure-client', async original => ({ ...await original(), createProcedureClient: vi.fn(() => vi.fn()), @@ -13,98 +19,12 @@ beforeEach(() => { vi.clearAllMocks() }) -const handler = vi.fn(() => ({ val: '123' })) -const mid = vi.fn(({ next }, _, __) => next({})) - -const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) -const baseErrors = { - CODE: { - status: 500, - data: z.object({ why: z.string() }), - }, -} -const decorated = new DecoratedProcedure({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: schema, - route: { path: '/test', method: 'GET', deprecated: true, description: 'des', summary: 'sum', tags: ['hi'] }, - inputExample: { val: 123 }, - outputExample: { val: 456 }, - errorMap: baseErrors, - }), - handler, - middlewares: [mid], - inputValidationIndex: 1, - outputValidationIndex: 1, -}) - -describe('decorate', () => { - it('works', () => { - const procedure = new Procedure({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: schema, - route: { }, - errorMap: baseErrors, - }), - handler, - middlewares: [mid], - inputValidationIndex: 0, - outputValidationIndex: 0, - }) - - expect(DecoratedProcedure.decorate(procedure)).toBeInstanceOf(DecoratedProcedure) - expect(DecoratedProcedure.decorate(procedure)).not.toBe(procedure) - expect(DecoratedProcedure.decorate(procedure)['~orpc']).toBe(procedure['~orpc']) - }) - - it('do nothing when procedure is already decorated', () => { - expect(DecoratedProcedure.decorate(decorated)).toBe(decorated) - }) -}) - -describe('self chainable', () => { - it('prefix', () => { - const prefixed = decorated.prefix('/test') +const def = ping['~orpc'] - expect(prefixed).not.toBe(decorated) - - expect(prefixed).toSatisfy(isProcedure) - expect(prefixed['~orpc'].contract['~orpc'].route?.path).toBe('/test/test') - expect(prefixed['~orpc'].contract['~orpc'].errorMap).toBe(baseErrors) - expect(prefixed['~orpc'].contract['~orpc'].InputSchema).toBe(schema) - expect(prefixed['~orpc'].contract['~orpc'].OutputSchema).toBe(schema) - expect(prefixed['~orpc'].inputValidationIndex).toEqual(1) - expect(prefixed['~orpc'].outputValidationIndex).toEqual(1) - expect(prefixed['~orpc'].middlewares).toEqual([mid]) - expect(prefixed['~orpc'].handler).toBe(handler) - }) - - it('route', () => { - const route = { path: '/test', method: 'GET', tags: ['hiu'] } as const - const routed = decorated.route(route) +const decorated = new DecoratedProcedure(def) - expect(routed).not.toBe(decorated) - expect(routed).toSatisfy(isProcedure) - expect(routed['~orpc'].contract['~orpc'].route).toEqual({ - path: '/test', - method: 'GET', - deprecated: true, - description: 'des', - summary: 'sum', - tags: ['hiu'], - }) - - expect(routed['~orpc'].contract['~orpc'].errorMap).toBe(baseErrors) - expect(routed['~orpc'].contract['~orpc'].InputSchema).toBe(schema) - expect(routed['~orpc'].contract['~orpc'].OutputSchema).toBe(schema) - expect(routed['~orpc'].middlewares).toEqual([mid]) - expect(routed['~orpc'].inputValidationIndex).toEqual(1) - expect(routed['~orpc'].outputValidationIndex).toEqual(1) - expect(routed['~orpc'].handler).toBe(handler) - }) - - it('errors', () => { +describe('decoratedProcedure', () => { + it('.error', () => { const errors = { BAD_GATEWAY: { data: z.object({ @@ -114,191 +34,92 @@ describe('self chainable', () => { } const applied = decorated.errors(errors) - expect(applied).not.toBe(decorated) - expect(applied).toSatisfy(isProcedure) - expect(applied['~orpc'].contract['~orpc'].errorMap).toEqual({ - ...baseErrors, - ...errors, - }) + expect(applied).toBeInstanceOf(DecoratedProcedure) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toBe(schema) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toBe(schema) - expect(applied['~orpc'].middlewares).toEqual([mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].handler).toBe(handler) - }) - - it('use middleware', () => { - const extraMid = vi.fn() - - const applied = decorated.use(extraMid) - - expect(applied).not.toBe(decorated) - expect(applied).toSatisfy(isProcedure) - expect(applied['~orpc'].middlewares).toEqual([mid, extraMid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - - expect(applied['~orpc'].contract['~orpc'].errorMap).toBe(baseErrors) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toBe(schema) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toBe(schema) - expect(applied['~orpc'].handler).toBe(handler) + expect(applied['~orpc']).toEqual({ + ...def, + errorMap: { ...def.errorMap, ...errors }, + }) }) - it('use middleware with map input', () => { - const extraMid = vi.fn() - const map = vi.fn() + it('.meta', () => { + const meta = { mode: 'test' } as const - const applied = decorated.use(extraMid, map) + const applied = decorated.meta(meta) expect(applied).not.toBe(decorated) - expect(applied).toSatisfy(isProcedure) - expect(applied['~orpc'].middlewares).toEqual([mid, expect.any(Function)]) - expect(applied['~orpc'].inputValidationIndex).toEqual(1) - expect(applied['~orpc'].outputValidationIndex).toEqual(1) - expect(applied['~orpc'].contract['~orpc'].errorMap).toBe(baseErrors) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toBe(schema) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toBe(schema) - expect(applied['~orpc'].handler).toBe(handler) - - extraMid.mockReturnValueOnce('__extra__') - map.mockReturnValueOnce('__map__') - - expect((applied as any)['~orpc'].middlewares[1]({}, 'input', '__output__')).toBe('__extra__') - - expect(map).toBeCalledTimes(1) - expect(map).toBeCalledWith('input') + expect(applied).toBeInstanceOf(DecoratedProcedure) - expect(extraMid).toBeCalledTimes(1) - expect(extraMid).toBeCalledWith({}, '__map__', '__output__') - }) - - it('unshiftTag', () => { - const tagged = decorated.unshiftTag('test', 'test2', 'test3') - expect(tagged).not.toBe(decorated) - expect(tagged).toSatisfy(isProcedure) - expect(tagged['~orpc'].contract['~orpc'].route?.tags).toEqual(['test', 'test2', 'test3', 'hi']) - - expect(tagged['~orpc'].contract['~orpc'].errorMap).toBe(baseErrors) - expect(tagged['~orpc'].contract['~orpc'].InputSchema).toBe(schema) - expect(tagged['~orpc'].contract['~orpc'].OutputSchema).toBe(schema) - expect(tagged['~orpc'].middlewares).toEqual([mid]) - expect(tagged['~orpc'].inputValidationIndex).toEqual(1) - expect(tagged['~orpc'].outputValidationIndex).toEqual(1) - expect(tagged['~orpc'].handler).toBe(handler) + expect(applied['~orpc']).toEqual({ + ...def, + meta: { ...def.meta, ...meta }, + }) }) - it('unshiftMiddleware', () => { - const mid1 = vi.fn() - const mid2 = vi.fn() + it('.route', () => { + const route = { path: '/test', method: 'GET', tags: ['hiu'] } as const - const applied = decorated.unshiftMiddleware(mid1, mid2) + const applied = decorated.route(route) expect(applied).not.toBe(decorated) - expect(applied).toSatisfy(isProcedure) - expect(applied['~orpc'].middlewares).toEqual([mid1, mid2, mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(3) - expect(applied['~orpc'].outputValidationIndex).toEqual(3) + expect(applied).toBeInstanceOf(DecoratedProcedure) - expect(applied['~orpc'].contract['~orpc'].errorMap).toBe(baseErrors) - expect(applied['~orpc'].contract['~orpc'].InputSchema).toBe(schema) - expect(applied['~orpc'].contract['~orpc'].OutputSchema).toBe(schema) - expect(applied['~orpc'].handler).toBe(handler) + expect(applied['~orpc']).toEqual({ + ...def, + route: { ...def.route, ...route }, + }) }) - describe('unshiftMiddleware --- prevent duplicate', () => { - const mid1 = vi.fn() - const mid2 = vi.fn() - const mid3 = vi.fn() - const mid4 = vi.fn() - const mid5 = vi.fn() - - it('no duplicate', () => { - const applied = decorated.unshiftMiddleware(mid1, mid2) + describe('.use', () => { + it('without map input', () => { + const mid = vi.fn() - expect(applied['~orpc'].middlewares).toEqual([mid1, mid2, mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(3) - expect(applied['~orpc'].outputValidationIndex).toEqual(3) - }) + const applied = decorated.use(mid) + expect(applied).not.toBe(decorated) + expect(applied).toBeInstanceOf(DecoratedProcedure) - it('case 1', () => { - const applied = decorated.unshiftMiddleware(mid1, mid2).unshiftMiddleware(mid1, mid3) - expect(applied['~orpc'].middlewares).toEqual([mid1, mid3, mid2, mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(4) - expect(applied['~orpc'].outputValidationIndex).toEqual(4) + expect(applied['~orpc']).toEqual({ + ...def, + middlewares: [...def.middlewares, mid], + }) }) - it('case 2', () => { - const applied = decorated.unshiftMiddleware(mid1, mid2, mid3, mid4).unshiftMiddleware(mid1, mid4, mid2, mid3) - expect(applied['~orpc'].middlewares).toEqual([mid1, mid4, mid2, mid3, mid4, mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(6) - expect(applied['~orpc'].outputValidationIndex).toEqual(6) - }) + it('with map input', () => { + const mid = vi.fn() + const map = vi.fn() - it('case 3', () => { - const applied = decorated.unshiftMiddleware(mid1, mid5, mid2, mid3, mid4).unshiftMiddleware(mid1, mid4, mid2, mid3) - expect(applied['~orpc'].middlewares).toEqual([mid1, mid4, mid2, mid3, mid5, mid2, mid3, mid4, mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(9) - expect(applied['~orpc'].outputValidationIndex).toEqual(9) - }) + const applied = decorated.use(mid, map) + expect(applied).not.toBe(decorated) + expect(applied).toBeInstanceOf(DecoratedProcedure) - it('case 4', () => { - const applied = decorated.unshiftMiddleware(mid2, mid2).unshiftMiddleware(mid1, mid2) - expect(applied['~orpc'].middlewares).toEqual([mid1, mid2, mid2, mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(4) - expect(applied['~orpc'].outputValidationIndex).toEqual(4) - }) - - it('case 5', () => { - const applied = decorated.unshiftMiddleware(mid2, mid2).unshiftMiddleware(mid1, mid2, mid2) - expect(applied['~orpc'].middlewares).toEqual([mid1, mid2, mid2, mid]) - expect(applied['~orpc'].inputValidationIndex).toEqual(4) - expect(applied['~orpc'].outputValidationIndex).toEqual(4) + expect(applied['~orpc']).toEqual({ + ...def, + middlewares: [...def.middlewares, [mid, map]], + }) }) }) - describe('callable', () => { - it('works', () => { - const options = { context: { auth: true } } - - const callable = decorated.callable(options) + it('.callable', () => { + const options = { context: { db: 'postgres' } } - expect(callable).toBeInstanceOf(Function) - expect(callable).toSatisfy(isProcedure) - expect(createProcedureClient).toBeCalledTimes(1) - expect(createProcedureClient).toBeCalledWith(decorated, options) - }) - - it('can not chain after callable', () => { - const mid2 = vi.fn() - - const applied = decorated.callable({ - context: { auth: true }, - }) + const applied = decorated.callable(options) + expect(applied).toBeInstanceOf(Function) + expect(applied).toSatisfy(isProcedure) - expect(applied).not.haveOwnPropertyDescriptor('use') - }) + expect(createProcedureClient).toBeCalledTimes(1) + expect(createProcedureClient).toBeCalledWith(decorated, options) + expect(applied).not.haveOwnPropertyDescriptor('use') }) - describe('actionable', () => { - it('works', () => { - const options = { context: { auth: true } } - const actionable = decorated.actionable(options) - - expect(actionable).toBeInstanceOf(Function) - expect(actionable).toSatisfy(isProcedure) - expect(createProcedureClient).toBeCalledTimes(1) - expect(createProcedureClient).toBeCalledWith(decorated, options) - }) + it('.actionable', () => { + const options = { context: { db: 'postgres' } } - it('can not chain after actionable', () => { - const mid2 = vi.fn() + const applied = decorated.actionable(options) - const applied = decorated.actionable({ - context: { auth: true }, - }) + expect(applied).toBeInstanceOf(Function) + expect(applied).toSatisfy(isProcedure) - expect(applied).not.haveOwnPropertyDescriptor('use') - }) + expect(createProcedureClient).toBeCalledTimes(1) + expect(createProcedureClient).toBeCalledWith(decorated, options) + expect(applied).not.haveOwnPropertyDescriptor('use') }) }) diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts index 03a74662..104f291e 100644 --- a/packages/server/src/procedure-decorated.ts +++ b/packages/server/src/procedure-decorated.ts @@ -1,10 +1,10 @@ -import type { ClientRest, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, HTTPPath, Route, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { ConflictContextGuard, Context } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { ANY_MIDDLEWARE, MapInputMiddleware, Middleware } from './middleware' +import type { ClientRest, ErrorMap, MergedErrorMap, Meta, ORPCErrorConstructorMap, Route, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { ConflictContextGuard, Context, MergedContext } from './context' +import type { AnyMiddleware, MapInputMiddleware, Middleware } from './middleware' import type { CreateProcedureClientRest, ProcedureClient } from './procedure-client' -import { DecoratedContractProcedure } from '@orpc/contract' +import { mergeErrorMap, mergeMeta, mergeRoute } from '@orpc/contract' import { decorateMiddleware } from './middleware-decorated' +import { addMiddleware } from './middleware-utils' import { Procedure } from './procedure' import { createProcedureClient } from './procedure-client' @@ -15,50 +15,40 @@ export class DecoratedProcedure< TOutputSchema extends Schema, THandlerOutput extends SchemaInput, TErrorMap extends ErrorMap, - TRoute extends Route, -> extends Procedure { - static decorate< - UInitialContext extends Context, - UCurrentContext extends Context, - UInputSchema extends Schema, - UOutputSchema extends Schema, - UHandlerOutput extends SchemaInput, - UErrorMap extends ErrorMap, - URoute extends Route, - >( - procedure: Procedure, - ) { - if (procedure instanceof DecoratedProcedure) { - return procedure - } - - return new DecoratedProcedure(procedure['~orpc']) - } - - prefix( - prefix: HTTPPath, - ): DecoratedProcedure { + TMeta extends Meta, +> extends Procedure { + errors( + errors: U, + ): DecoratedProcedure< + TInitialContext, + TCurrentContext, + TInputSchema, + TOutputSchema, + THandlerOutput, + MergedErrorMap, + TMeta + > { return new DecoratedProcedure({ ...this['~orpc'], - contract: DecoratedContractProcedure.decorate(this['~orpc'].contract).prefix(prefix), + errorMap: mergeErrorMap(this['~orpc'].errorMap, errors), }) } - route( - route: Route, - ): DecoratedProcedure { + meta( + meta: TMeta, + ): DecoratedProcedure { return new DecoratedProcedure({ ...this['~orpc'], - contract: DecoratedContractProcedure.decorate(this['~orpc'].contract).route(route), + meta: mergeMeta(this['~orpc'].meta, meta), }) } - errors & ErrorMapSuggestions>( - errors: U, - ): DecoratedProcedure { + route( + route: Route, + ): DecoratedProcedure { return new DecoratedProcedure({ ...this['~orpc'], - contract: DecoratedContractProcedure.decorate(this['~orpc'].contract).errors(errors), + route: mergeRoute(this['~orpc'].route, route), }) } @@ -68,10 +58,11 @@ export class DecoratedProcedure< U, SchemaOutput, THandlerOutput, - ORPCErrorConstructorMap + ORPCErrorConstructorMap, + TMeta >, - ): ConflictContextGuard - & DecoratedProcedure + ): ConflictContextGuard> + & DecoratedProcedure, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta> use( middleware: Middleware< @@ -79,77 +70,29 @@ export class DecoratedProcedure< UOutContext, UInput, THandlerOutput, - ORPCErrorConstructorMap + ORPCErrorConstructorMap, + TMeta >, mapInput: MapInputMiddleware, UInput>, - ): ConflictContextGuard - & DecoratedProcedure + ): ConflictContextGuard> + & DecoratedProcedure, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta> - use(middleware: Middleware, mapInput?: MapInputMiddleware): DecoratedProcedure { - const middleware_ = mapInput + use(middleware: AnyMiddleware, mapInput?: MapInputMiddleware): DecoratedProcedure { + const mapped = mapInput ? decorateMiddleware(middleware).mapInput(mapInput) : middleware return new DecoratedProcedure({ ...this['~orpc'], - middlewares: [...this['~orpc'].middlewares, middleware_], - }) - } - - unshiftTag( - ...tags: string[] - ): DecoratedProcedure { - return new DecoratedProcedure({ - ...this['~orpc'], - contract: DecoratedContractProcedure.decorate(this['~orpc'].contract).unshiftTag(...tags), + middlewares: addMiddleware(this['~orpc'].middlewares, mapped), }) } - unshiftMiddleware( - ...middlewares: Middleware< - TInitialContext, - U, - unknown, - SchemaOutput, - ORPCErrorConstructorMap - >[] - ): ConflictContextGuard - & DecoratedProcedure { - // FIXME: this is a hack to make the type checker happy, but it's not a good solution - const castedMiddlewares = middlewares as ANY_MIDDLEWARE[] - - if (this['~orpc'].middlewares.length) { - let min = 0 - - for (let i = 0; i < this['~orpc'].middlewares.length; i++) { - const index = castedMiddlewares.indexOf(this['~orpc'].middlewares[i]!, min) - - if (index === -1) { - castedMiddlewares.push(...this['~orpc'].middlewares.slice(i)) - break - } - - min = index + 1 - } - } - - const numNewMiddlewares = castedMiddlewares.length - this['~orpc'].middlewares.length - - const decorated = new DecoratedProcedure({ - ...this['~orpc'], - inputValidationIndex: this['~orpc'].inputValidationIndex + numNewMiddlewares, - outputValidationIndex: this['~orpc'].outputValidationIndex + numNewMiddlewares, - middlewares: castedMiddlewares, - }) - - return decorated as typeof decorated & ConflictContextGuard - } - /** * Make this procedure callable (works like a function while still being a procedure). */ callable(...rest: CreateProcedureClientRest): - & Procedure + & Procedure & ProcedureClient { return Object.assign(createProcedureClient(this, ...rest), { '~type': 'Procedure' as const, @@ -161,7 +104,7 @@ export class DecoratedProcedure< * Make this procedure compatible with server action (the same as .callable, but the type is compatible with server action). */ actionable(...rest: CreateProcedureClientRest): - & Procedure + & Procedure & ((...rest: ClientRest>) => Promise>) { return this.callable(...rest) } diff --git a/packages/server/src/procedure-implementer.test-d.ts b/packages/server/src/procedure-implementer.test-d.ts deleted file mode 100644 index 7b38f1bd..00000000 --- a/packages/server/src/procedure-implementer.test-d.ts +++ /dev/null @@ -1,157 +0,0 @@ -import type { Context } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { Middleware, MiddlewareOutputFn } from './middleware' -import type { ANY_PROCEDURE } from './procedure' -import type { DecoratedProcedure } from './procedure-decorated' -import type { ProcedureImplementer } from './procedure-implementer' -import { z } from 'zod' - -const baseSchema = z.object({ base: z.string().transform(v => Number.parseInt(v)) }) -const baseErrors = { - PAYMENT_REQUIRED: { - status: 402, - message: 'default message', - data: baseSchema, - }, -} -const route = { method: 'GET', path: '/ping' } as const - -const implementer = {} as ProcedureImplementer<{ id?: string }, { id?: string } & { extra: true }, typeof baseSchema, typeof baseSchema, typeof baseErrors, typeof route> - -describe('self chainable', () => { - it('use middleware', () => { - const i = implementer - .use(({ context, path, next, procedure, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf<{ base: number }>() - expectTypeOf(context).toEqualTypeOf<{ id?: string } & { extra: true }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({ - context: { - auth: true, - }, - }) - }) - .use(({ context, path, next, procedure, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf<{ base: number }>() - expectTypeOf(context).toEqualTypeOf<{ id?: string } & { auth: boolean } & { extra: true }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({}) - }) - - const a = {} as ProcedureImplementer< - { id?: string }, - { id?: string } & { auth: boolean } & { extra: true } & Record, - typeof baseSchema, - typeof baseSchema, - typeof baseErrors, - typeof route - > - - expectTypeOf(i).toEqualTypeOf(a) - }) - - it('use middleware with map input', () => { - const mid: Middleware> = ({ next }) => { - return next({ - context: { id: 'string', extra: true as const }, - }) - } - - const i = implementer.use(mid, (input) => { - expectTypeOf(input).toEqualTypeOf<{ base: number }>() - return input.base - }) - - expectTypeOf(i).toEqualTypeOf< - ProcedureImplementer< - { id?: string }, - { id?: string } & { extra: true } & { id: string, extra: true }, - typeof baseSchema, - typeof baseSchema, - typeof baseErrors, - typeof route - > - >() - - // @ts-expect-error - invalid input - implementer.use(mid) - - // @ts-expect-error - invalid mapped input - implementer.use(mid, input => input) - }) - - it('prevent conflict on context', () => { - implementer.use(({ context, path, next }, input) => next({})) - implementer.use(({ context, path, next }, input) => next({ context: { id: '1' } })) - implementer.use(({ context, path, next }, input) => next({ context: { id: '1', extra: true } })) - implementer.use(({ context, path, next }, input) => next({ context: { auth: true } })) - - implementer.use(({ context, path, next }, input) => next({}), () => 'anything') - implementer.use(({ context, path, next }, input) => next({ context: { id: '1' } }), () => 'anything') - implementer.use(({ context, path, next }, input) => next({ context: { id: '1', extra: true } }), () => 'anything') - implementer.use(({ context, path, next }, input) => next({ context: { auth: true } }), () => 'anything') - - // @ts-expect-error - conflict with context - implementer.use(({ context, path, next }, input) => next({ context: { id: 1 } })) - - // @ts-expect-error - conflict with context - implementer.use(({ context, path, next }, input) => next({ context: { id: 1, extra: true } })) - - // @ts-expect-error - conflict with context - implementer.use(({ context, path, next }, input) => next({ context: { id: 1 } }), () => 'anything') - - // @ts-expect-error - conflict with context - implementer.use(({ context, path, next }, input) => next({ context: { id: 1, extra: true } }), () => 'anything') - - // conflict context but not detected - expectTypeOf(implementer.use(({ next }) => next({ context: { extra: undefined } }))).toEqualTypeOf() - expectTypeOf(implementer.use(({ next }) => next({ context: { extra: undefined } }), () => {})).toEqualTypeOf() - }) - - it('handle middleware with output is typed', () => { - const mid1 = {} as Middleware, unknown, any, Record> - const mid2 = {} as Middleware, unknown, { base: string }, Record> - const mid3 = {} as Middleware, unknown, unknown, Record> - const mid4 = {} as Middleware, unknown, { base: number }, Record> - - implementer.use(mid1) - implementer.use(mid2) - // @ts-expect-error - required used any for output - implementer.use(mid3) - // @ts-expect-error - output is not match - implementer.use(mid4) - }) -}) - -describe('to DecoratedProcedure', () => { - it('handler', () => { - const procedure = implementer.handler(({ input, context, procedure, path, signal, errors }) => { - expectTypeOf(context).toEqualTypeOf<{ id?: string } & { extra: true }>() - expectTypeOf(input).toEqualTypeOf<{ base: number }>() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(signal).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return { base: '1' } - }) - - expectTypeOf(procedure).toEqualTypeOf< - DecoratedProcedure<{ id?: string }, { id?: string } & { extra: true }, typeof baseSchema, typeof baseSchema, { base: string }, typeof baseErrors, typeof route> - >() - - // @ts-expect-error - invalid output - implementer.handler(() => ({ base: 1 })) - - // @ts-expect-error - invalid output - implementer.handler(() => {}) - }) -}) diff --git a/packages/server/src/procedure-implementer.test.ts b/packages/server/src/procedure-implementer.test.ts deleted file mode 100644 index 5e7e9edd..00000000 --- a/packages/server/src/procedure-implementer.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { ContractProcedure } from '@orpc/contract' -import { z } from 'zod' -import { isProcedure } from './procedure' -import { ProcedureImplementer } from './procedure-implementer' - -const baseSchema = z.object({ base: z.string().transform(v => Number.parseInt(v)) }) -const baseErrors = { - PAYMENT_REQUIRED: { - status: 402, - message: 'default message', - data: baseSchema, - }, -} -const baseMid = vi.fn() - -const implementer = new ProcedureImplementer({ - contract: new ContractProcedure({ - InputSchema: baseSchema, - OutputSchema: baseSchema, - errorMap: baseErrors, - route: {}, - }), - middlewares: [baseMid], - inputValidationIndex: 1, - outputValidationIndex: 1, -}) - -describe('self chainable', () => { - it('use middleware', () => { - const mid1 = vi.fn() - const mid2 = vi.fn() - const i = implementer.use(mid1).use(mid2) - - expect(i).not.toBe(implementer) - expect(i).toBeInstanceOf(ProcedureImplementer) - expect(i['~orpc'].middlewares).toEqual([baseMid, mid1, mid2]) - expect(i['~orpc'].contract['~orpc'].InputSchema).toEqual(baseSchema) - expect(i['~orpc'].contract['~orpc'].OutputSchema).toEqual(baseSchema) - expect(i['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - expect(i['~orpc'].inputValidationIndex).toEqual(1) - expect(i['~orpc'].outputValidationIndex).toEqual(1) - }) - - it('use middleware with map input', () => { - const mid = vi.fn() - const map = vi.fn() - - const i = implementer.use(mid, map) - - expect(i).not.toBe(implementer) - expect(i).toBeInstanceOf(ProcedureImplementer) - expect(i['~orpc'].middlewares).toEqual([baseMid, expect.any(Function)]) - expect(i['~orpc'].contract['~orpc'].InputSchema).toEqual(baseSchema) - expect(i['~orpc'].contract['~orpc'].OutputSchema).toEqual(baseSchema) - expect(i['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - expect(i['~orpc'].inputValidationIndex).toEqual(1) - expect(i['~orpc'].outputValidationIndex).toEqual(1) - - map.mockReturnValueOnce('__input__') - mid.mockReturnValueOnce('__mid__') - - expect((i as any)['~orpc'].middlewares[1]({}, 'input', '__output__')).toBe('__mid__') - - expect(map).toBeCalledTimes(1) - expect(map).toBeCalledWith('input') - - expect(mid).toBeCalledTimes(1) - expect(mid).toBeCalledWith({}, '__input__', '__output__') - }) -}) - -describe('to DecoratedProcedure', () => { - it('handler', () => { - const handler = vi.fn() - const procedure = implementer.handler(handler) - - expect(procedure).toSatisfy(isProcedure) - expect(procedure['~orpc'].handler).toBe(handler) - expect(procedure['~orpc'].middlewares).toEqual([baseMid]) - expect(procedure['~orpc'].contract['~orpc'].InputSchema).toEqual(baseSchema) - expect(procedure['~orpc'].contract['~orpc'].OutputSchema).toEqual(baseSchema) - expect(procedure['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - expect(procedure['~orpc'].inputValidationIndex).toEqual(1) - expect(procedure['~orpc'].outputValidationIndex).toEqual(1) - }) -}) diff --git a/packages/server/src/procedure-implementer.ts b/packages/server/src/procedure-implementer.ts deleted file mode 100644 index b02fafa8..00000000 --- a/packages/server/src/procedure-implementer.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { ContractProcedure, ErrorMap, Route, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { ANY_MAP_INPUT_MIDDLEWARE, ANY_MIDDLEWARE, MapInputMiddleware, Middleware } from './middleware' -import type { ProcedureHandler } from './procedure' -import { decorateMiddleware } from './middleware-decorated' -import { DecoratedProcedure } from './procedure-decorated' - -export type ProcedureImplementerDef< - TInitialContext extends Context, - TCurrentContext extends Context, - TInputSchema extends Schema, - TOutputSchema extends Schema, - TErrorMap extends ErrorMap, - TRoute extends Route, -> = { - __initialContext?: TypeInitialContext - __currentContext?: TypeCurrentContext - contract: ContractProcedure - middlewares: Middleware[] - inputValidationIndex: number - outputValidationIndex: number -} - -export class ProcedureImplementer< - TInitialContext extends Context, - TCurrentContext extends Context, - TInputSchema extends Schema, - TOutputSchema extends Schema, - TErrorMap extends ErrorMap, - TRoute extends Route, -> { - '~type' = 'ProcedureImplementer' as const - '~orpc': ProcedureImplementerDef - - constructor(def: ProcedureImplementerDef) { - this['~orpc'] = def - } - - use( - middleware: Middleware< - TCurrentContext, - U, - SchemaOutput, - SchemaInput, - ORPCErrorConstructorMap - >, - ): ConflictContextGuard - & ProcedureImplementer - - use( - middleware: Middleware< - TCurrentContext, - UOutContext, - UInput, - SchemaInput, - ORPCErrorConstructorMap - >, - mapInput: MapInputMiddleware, UInput>, - ): ConflictContextGuard - & ProcedureImplementer - - use( - middleware: ANY_MIDDLEWARE, - mapInput?: ANY_MAP_INPUT_MIDDLEWARE, - ): ProcedureImplementer { - const mappedMiddleware = mapInput - ? decorateMiddleware(middleware).mapInput(mapInput) - : middleware - - return new ProcedureImplementer({ - ...this['~orpc'], - middlewares: [...this['~orpc'].middlewares, mappedMiddleware], - }) - } - - handler>( - handler: ProcedureHandler, - ): DecoratedProcedure { - return new DecoratedProcedure({ - ...this['~orpc'], - handler, - }) - } -} diff --git a/packages/server/src/procedure-utils.test-d.ts b/packages/server/src/procedure-utils.test-d.ts index ac6ed6b1..ec4ff1ae 100644 --- a/packages/server/src/procedure-utils.test-d.ts +++ b/packages/server/src/procedure-utils.test-d.ts @@ -1,53 +1,26 @@ import type { ORPCError } from '@orpc/contract' -import type { Context } from './context' -import type { Procedure } from './procedure' import { safe } from '@orpc/contract' -import { z } from 'zod' +import { ping, pong } from '../tests/shared' import { call } from './procedure-utils' -const schema = z.object({ - val: z.string().transform(v => Number.parseInt(v)), -}) - -const baseErrors = { - CODE: { - data: z.object({ - why: z.string(), - }), - }, -} - -const procedure = {} as Procedure -const procedureWithContext = {} as Procedure<{ db: string }, { auth: boolean }, typeof schema, typeof schema, { val: string }, typeof baseErrors, { description: 'procedureWithContext' }> - -describe('call', () => { - it('infer input', async () => { - call(procedure, { val: '123' }) - // @ts-expect-error - invalid input - call(procedure, { val: 123 }) - }) +it('call', async () => { + const [output, error, isDefined] = await safe(call(ping, { input: 123 }, { context: { db: 'postgres' } })) - it('infer output', async () => { - const output = await call(procedure, { val: '123' }) - expectTypeOf(output).toEqualTypeOf<{ val: number }>() - }) + if (!error) { + expectTypeOf(output).toEqualTypeOf<{ output: string }>() + } - it('infer error', async () => { - const [output, error] = await safe(call(procedure, { val: '123' })) + if (isDefined) { + expectTypeOf(error).toEqualTypeOf | ORPCError<'OVERRIDE', unknown>>() + } - expectTypeOf(error).toEqualTypeOf< - | undefined - | Error - | ORPCError<'CODE', { why: string }> - >() - }) + // @ts-expect-error - invalid input + call(ping, { input: '123' }, { context: { db: 'postgres' } }) - it('infer context', async () => { - call(procedure, { val: '123' }) - call(procedureWithContext, { val: '123' }, { context: { db: 'postgres' } }) - // @ts-expect-error - context is required - call(procedureWithContext, { val: '123' }) - // @ts-expect-error - invalid context - call(procedureWithContext, { val: '123' }, { context: { db: 1 } }) - }) + // can call without third argument if all context fields is optional + call(pong, { input: 123 }) + // @ts-expect-error - context is required + call(ping, { input: 123 }) + // @ts-expect-error - invalid context + call(ping, { input: 123 }, { context: { db: 123 } }) }) diff --git a/packages/server/src/procedure-utils.test.ts b/packages/server/src/procedure-utils.test.ts index 6a1b1772..c2792afe 100644 --- a/packages/server/src/procedure-utils.test.ts +++ b/packages/server/src/procedure-utils.test.ts @@ -1,5 +1,4 @@ -import { ContractProcedure } from '@orpc/contract' -import { Procedure } from './procedure' +import { ping } from '../tests/shared' import { createProcedureClient } from './procedure-client' import { call } from './procedure-utils' @@ -8,31 +7,16 @@ vi.mock('./procedure-client', async original => ({ createProcedureClient: vi.fn(() => vi.fn()), })) -const procedure = new Procedure({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - route: {}, - }), - handler: () => { }, - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, -}) - -describe('call', () => { - it('works', async () => { - const client = vi.fn(async () => '__output__') - vi.mocked(createProcedureClient).mockReturnValueOnce(client) +it('call', async () => { + const client = vi.fn(async () => '__output__') + vi.mocked(createProcedureClient).mockReturnValueOnce(client) - const options = { context: { db: 'postgres' } } - const output = await call(procedure, 'input', options) + const options = { context: { db: 'postgres' } } + const output = await call(ping, { input: 123 }, options) - expect(output).toBe('__output__') - expect(createProcedureClient).toBeCalledTimes(1) - expect(createProcedureClient).toBeCalledWith(procedure, options) - expect(client).toBeCalledTimes(1) - expect(client).toBeCalledWith('input') - }) + expect(output).toBe('__output__') + expect(createProcedureClient).toBeCalledTimes(1) + expect(createProcedureClient).toBeCalledWith(ping, options) + expect(client).toBeCalledTimes(1) + expect(client).toBeCalledWith({ input: 123 }) }) diff --git a/packages/server/src/procedure.test-d.ts b/packages/server/src/procedure.test-d.ts index f1c77303..a5b10396 100644 --- a/packages/server/src/procedure.test-d.ts +++ b/packages/server/src/procedure.test-d.ts @@ -1,12 +1,27 @@ -import type { ANY_PROCEDURE } from './procedure' -import { isProcedure } from './procedure' +import type { ContractProcedure } from '@orpc/contract' +import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../../contract/tests/shared' +import type { CurrentContext, InitialContext } from '../tests/shared' +import type { Procedure } from './procedure' -describe('isProcedure', () => { - it('works', () => { - const item = {} as unknown +const procedure = {} as Procedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta +> - if (isProcedure(item)) { - expectTypeOf(item).toEqualTypeOf() - } +describe('Procedure', () => { + it('is a contract procedure', () => { + expectTypeOf(procedure).toMatchTypeOf< + ContractProcedure< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + BaseMeta + > + >() }) }) diff --git a/packages/server/src/procedure.test.ts b/packages/server/src/procedure.test.ts index 6c5eee18..186f38d1 100644 --- a/packages/server/src/procedure.test.ts +++ b/packages/server/src/procedure.test.ts @@ -1,27 +1,10 @@ -import { ContractProcedure } from '@orpc/contract' -import { isProcedure, Procedure } from './procedure' +import { ping } from '../tests/shared' +import { isProcedure } from './procedure' -describe('isProcedure', () => { - const procedure = new Procedure({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - route: {}, - }), - handler: () => {}, - inputValidationIndex: 0, - outputValidationIndex: 0, - middlewares: [], - }) +it('isProcedure', () => { + expect(ping).toSatisfy(isProcedure) + expect(Object.assign({}, ping)).toSatisfy(isProcedure) - it('works', () => { - expect(procedure).toSatisfy(isProcedure) - expect({}).not.toSatisfy(isProcedure) - expect(true).not.toSatisfy(isProcedure) - }) - - it('works with raw object', () => { - expect(Object.assign({}, procedure)).toSatisfy(isProcedure) - }) + expect({}).not.toSatisfy(isProcedure) + expect(true).not.toSatisfy(isProcedure) }) diff --git a/packages/server/src/procedure.ts b/packages/server/src/procedure.ts index ec3e856f..b37037bc 100644 --- a/packages/server/src/procedure.ts +++ b/packages/server/src/procedure.ts @@ -1,21 +1,19 @@ -import type { ContractProcedure, ErrorMap, Route, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { AbortSignal, ContractProcedureDef, ErrorMap, Meta, ORPCErrorConstructorMap, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { Promisable } from '@orpc/shared' import type { Context, TypeInitialContext } from './context' -import type { ORPCErrorConstructorMap } from './error' -import type { Lazy } from './lazy' -import type { Middleware } from './middleware' -import type { AbortSignal } from './types' +import type { AnyMiddleware } from './middleware' import { isContractProcedure } from '@orpc/contract' export interface ProcedureHandlerOptions< TCurrentContext extends Context, TInput, TErrorConstructorMap extends ORPCErrorConstructorMap, + TMeta extends Meta, > { context: TCurrentContext input: TInput path: string[] - procedure: ANY_PROCEDURE + procedure: Procedure signal?: AbortSignal errors: TErrorConstructorMap } @@ -26,23 +24,13 @@ export interface ProcedureHandler< TOutputSchema extends Schema, THandlerOutput extends SchemaInput, TErrorMap extends ErrorMap, + TMeta extends Meta, > { ( - opt: ProcedureHandlerOptions, ORPCErrorConstructorMap> + opt: ProcedureHandlerOptions, ORPCErrorConstructorMap, TMeta> ): Promisable> } -/** - * Why is `ErrorConstructorMap` passed to `middlewares` as `any`? - * Why is `ErrorMap` passed to `ProcedureHandler` as `any`? - * - * Passing `ErrorMap/ErrorConstructorMap` directly to `Middleware/ProcedureHandler` - * causes unexpected errors in the router (the root cause is unclear, but it occurs consistently). - * To avoid these issues, `any` is used as a workaround. - * - * This approach is still functional because `ProcedureDef` can infer the `ErrorMap` from `ContractProcedure`. - * The only downside is that direct access to them requires careful type checking to ensure safety. - */ export interface ProcedureDef< TInitialContext extends Context, TCurrentContext extends Context, @@ -50,14 +38,13 @@ export interface ProcedureDef< TOutputSchema extends Schema, THandlerOutput extends SchemaInput, TErrorMap extends ErrorMap, - TRoute extends Route, -> { + TMeta extends Meta, +> extends ContractProcedureDef { __initialContext?: TypeInitialContext - middlewares: Middleware[] + middlewares: AnyMiddleware[] inputValidationIndex: number outputValidationIndex: number - contract: ContractProcedure - handler: ProcedureHandler + handler: ProcedureHandler } export class Procedure< @@ -67,36 +54,27 @@ export class Procedure< TOutputSchema extends Schema, THandlerOutput extends SchemaInput, TErrorMap extends ErrorMap, - TRoute extends Route, + TMeta extends Meta, > { - '~type' = 'Procedure' as const - '~orpc': ProcedureDef + '~orpc': ProcedureDef - constructor(def: ProcedureDef) { + constructor(def: ProcedureDef) { this['~orpc'] = def } } -export type ANY_PROCEDURE = Procedure -export type WELL_PROCEDURE = Procedure -export type ANY_LAZY_PROCEDURE = Lazy +export type AnyProcedure = Procedure -export function isProcedure(item: unknown): item is ANY_PROCEDURE { +export function isProcedure(item: unknown): item is AnyProcedure { if (item instanceof Procedure) { return true } return ( - (typeof item === 'object' || typeof item === 'function') - && item !== null - && '~type' in item - && item['~type'] === 'Procedure' - && '~orpc' in item - && typeof item['~orpc'] === 'object' - && item['~orpc'] !== null - && 'contract' in item['~orpc'] - && isContractProcedure(item['~orpc'].contract) + isContractProcedure(item) + && 'middlewares' in item['~orpc'] + && 'inputValidationIndex' in item['~orpc'] + && 'outputValidationIndex' in item['~orpc'] && 'handler' in item['~orpc'] - && typeof item['~orpc'].handler === 'function' ) } diff --git a/packages/server/src/router-accessible-lazy.test-d.ts b/packages/server/src/router-accessible-lazy.test-d.ts new file mode 100644 index 00000000..185e8995 --- /dev/null +++ b/packages/server/src/router-accessible-lazy.test-d.ts @@ -0,0 +1,21 @@ +import type { Lazy } from './lazy' +import type { AccessibleLazyRouter } from './router-accessible-lazy' +import { ping, pong } from '../tests/shared' + +const router = { + ping, + pong, + nested: { + ping, + pong, + }, +} + +it('AccessibleLazyRouter', () => { + const accessible = {} as AccessibleLazyRouter + + expectTypeOf(accessible.ping).toEqualTypeOf >() + expectTypeOf(accessible.pong).toEqualTypeOf >() + expectTypeOf(accessible.nested.ping).toEqualTypeOf >() + expectTypeOf(accessible.nested.pong).toEqualTypeOf >() +}) diff --git a/packages/server/src/router-accessible-lazy.test.ts b/packages/server/src/router-accessible-lazy.test.ts new file mode 100644 index 00000000..f30d22e2 --- /dev/null +++ b/packages/server/src/router-accessible-lazy.test.ts @@ -0,0 +1,33 @@ +import { ping, pong, router } from '../tests/shared' +import { isLazy, lazy, unlazy } from './lazy' +import { createAccessibleLazyRouter } from './router-accessible-lazy' + +describe('createAccessibleLazyRouter', () => { + const accessible = createAccessibleLazyRouter(lazy(() => Promise.resolve({ default: router }))) + + it('works', () => { + expect(accessible).toSatisfy(isLazy) + expect(unlazy(accessible)).resolves.toEqual({ default: router }) + + expect(accessible.ping).toSatisfy(isLazy) + expect(unlazy(accessible.ping)).resolves.toEqual({ default: ping }) + + expect(accessible.nested.ping).toSatisfy(isLazy) + expect(unlazy(accessible.nested.ping)).resolves.toEqual({ default: ping }) + + expect(accessible.pong).toSatisfy(isLazy) + expect(unlazy(accessible.pong)).resolves.toEqual({ default: pong }) + + expect(accessible.nested.pong).toSatisfy(isLazy) + expect(unlazy(accessible.nested.pong)).resolves.toEqual({ default: pong }) + }) + + it('is Lazy when access undefined router', () => { + expect((accessible as any).a.b.c.d.e).toSatisfy(isLazy) + expect(unlazy((accessible as any).a.b.c.d.e)).resolves.toEqual({ default: undefined }) + }) + + it('not recursive with symbol', () => { + expect((accessible as any)[Symbol.for('test')]).toEqual(undefined) + }) +}) diff --git a/packages/server/src/router-accessible-lazy.ts b/packages/server/src/router-accessible-lazy.ts new file mode 100644 index 00000000..ddfc6429 --- /dev/null +++ b/packages/server/src/router-accessible-lazy.ts @@ -0,0 +1,34 @@ +import type { Lazy } from './lazy' +import type { AnyProcedure } from './procedure' +import type { AnyRouter } from './router' +import { flatLazy } from './lazy-utils' +import { getRouterChild } from './router' + +export type AccessibleLazyRouter> = + T extends Lazy> + ? AccessibleLazyRouter + : Lazy & ( + T extends AnyProcedure | undefined + ? unknown + : { + [K in keyof T]: T[K] extends AnyRouter ? AccessibleLazyRouter : never + } + ) + +export function createAccessibleLazyRouter>(lazied: T): AccessibleLazyRouter { + const flattenLazy = flatLazy(lazied) + + const recursive = new Proxy(flattenLazy, { + get(target, key) { + if (typeof key !== 'string') { + return Reflect.get(target, key) + } + + const next = getRouterChild(flattenLazy, key) + + return createAccessibleLazyRouter(next) + }, + }) + + return recursive as any +} diff --git a/packages/server/src/router-builder.test-d.ts b/packages/server/src/router-builder.test-d.ts deleted file mode 100644 index ba2aca81..00000000 --- a/packages/server/src/router-builder.test-d.ts +++ /dev/null @@ -1,278 +0,0 @@ -import type { Route, StrictErrorMap } from '@orpc/contract' -import type { Context } from './context' -import type { Lazy } from './lazy' -import type { DecoratedLazy } from './lazy-decorated' -import type { Middleware, MiddlewareOutputFn } from './middleware' -import type { ANY_PROCEDURE, Procedure } from './procedure' -import type { DecoratedProcedure } from './procedure-decorated' -import type { AdaptedRouter, RouterBuilder } from './router-builder' -import { z } from 'zod' -import { lazy } from './lazy' - -const baseErrors = { - BASE: { - data: z.object({ why: z.string() }), - }, -} - -const builder = {} as RouterBuilder<{ auth: boolean }, { auth: boolean } & { db: string }, typeof baseErrors> - -const route = { method: 'GET', path: '/ping' } as const - -describe('AdaptedRouter', () => { - const ping = {} as Procedure<{ auth: boolean }, { auth: boolean } & { db: string }, undefined, undefined, unknown, typeof baseErrors, typeof route> - const pong = {} as Procedure, Record> - - it('without lazy', () => { - const router = { - ping, - pong, - nested: { - ping, - pong, - }, - } - const adapted = {} as AdaptedRouter<{ log: true, auth: boolean }, typeof router, typeof baseErrors> - - expectTypeOf(adapted.ping).toEqualTypeOf< - DecoratedProcedure<{ log: true, auth: boolean }, { auth: boolean } & { db: string }, undefined, undefined, unknown, typeof baseErrors, Route> - >() - expectTypeOf(adapted.pong).toEqualTypeOf< - DecoratedProcedure<{ log: true, auth: boolean }, Context, undefined, undefined, unknown, Record & typeof baseErrors, Route> - >() - expectTypeOf(adapted.nested.ping).toEqualTypeOf< - DecoratedProcedure<{ log: true, auth: boolean }, { auth: boolean } & { db: string }, undefined, undefined, unknown, typeof baseErrors, Route> - >() - expectTypeOf(adapted.nested.pong).toEqualTypeOf< - DecoratedProcedure<{ log: true, auth: boolean }, Context, undefined, undefined, unknown, Record & typeof baseErrors, Route> - >() - }) - - it('with lazy', () => { - const router = { - ping: lazy(() => Promise.resolve({ default: ping })), - pong, - nested: lazy(() => Promise.resolve({ - default: { - ping, - pong: lazy(() => Promise.resolve({ default: pong })), - }, - })), - } - - const adapted = {} as AdaptedRouter<{ log: true }, typeof router, typeof baseErrors> - - expectTypeOf(adapted.ping).toEqualTypeOf - >>() - expectTypeOf(adapted.pong).toEqualTypeOf< - DecoratedProcedure<{ log: true }, Context, undefined, undefined, unknown, Record & typeof baseErrors, Route> - >() - expectTypeOf(adapted.nested.ping).toEqualTypeOf - >>() - expectTypeOf(adapted.nested.pong).toEqualTypeOf & typeof baseErrors, Route> - >>() - }) - - it('with procedure', () => { - expectTypeOf>().toEqualTypeOf< - DecoratedProcedure<{ log: boolean }, { auth: boolean } & { db: string }, undefined, undefined, unknown, typeof baseErrors, Route> - >() - - expectTypeOf, typeof baseErrors>>().toEqualTypeOf< - DecoratedLazy> - >() - }) -}) - -describe('self chainable', () => { - it('prefix', () => { - expectTypeOf(builder.prefix('/test')).toEqualTypeOf() - - // @ts-expect-error - invalid prefix - builder.prefix('') - // @ts-expect-error - invalid prefix - builder.prefix(1) - }) - - it('tag', () => { - expectTypeOf(builder.tag('test')).toEqualTypeOf() - expectTypeOf(builder.tag('test', 'test2', 'test3')).toEqualTypeOf() - - // @ts-expect-error - invalid tag - builder.tag(1) - // @ts-expect-error - invalid tag - builder.tag('123', 2) - }) - - it('use middleware', () => { - builder.use(({ next, context, path, procedure, signal, errors }, input, output) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean } & { db: string }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(signal).toEqualTypeOf>() - expectTypeOf(errors).toEqualTypeOf>() - - return next({}) - }) - - const mid1 = {} as Middleware<{ auth: boolean }, Record, unknown, unknown, Record> - const mid2 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, unknown, Record> - const mid3 = {} as Middleware<{ auth: boolean, db: string }, { dev: string }, unknown, unknown, Record> - - expectTypeOf(builder.use(mid1)).toEqualTypeOf< - RouterBuilder<{ auth: boolean }, { auth: boolean } & { db: string } & Record, typeof baseErrors> - >() - expectTypeOf(builder.use(mid2)).toEqualTypeOf< - RouterBuilder<{ auth: boolean }, { auth: boolean } & { db: string } & { dev: string }, typeof baseErrors> - >() - expectTypeOf(builder.use(mid3)).toEqualTypeOf< - RouterBuilder < { auth: boolean }, { auth: boolean } & { db: string } & { dev: string }, typeof baseErrors> - >() - - const mid4 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, { val: string }, Record> - const mid5 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, { val: number }, Record> - const mid6 = {} as Middleware<{ auth: 'invalid' }, Context, any, unknown, Record> - - // @ts-expect-error - invalid middleware - builder.use(mid4) - // @ts-expect-error - invalid middleware - builder.use(mid5) - // @ts-expect-error - invalid middleware - builder.use(mid6) - // @ts-expect-error - invalid middleware - builder.use(true) - // @ts-expect-error - invalid middleware - builder.use(() => {}) - - // conflict context but not detected - expectTypeOf(builder.use(({ next }) => next({ context: { auth: undefined } }))).toEqualTypeOf() - }) - - it('errors', () => { - const errors = { - WRONG: { - data: z.object({ why: z.string() }), - }, - } - - const applied = builder.errors(errors) - - expectTypeOf(applied).toEqualTypeOf< - RouterBuilder<{ auth: boolean }, { auth: boolean } & { db: string }, StrictErrorMap & typeof baseErrors> - >() - - // @ts-expect-error - not allow redefine errors - builder.errors({ BASE: baseErrors.BASE }) - // @ts-expect-error - not allow redefine errors --- even with undefined - builder.errors({ BASE: undefined }) - }) -}) - -describe('to AdaptedRouter', () => { - const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }, typeof baseErrors, typeof route> - const pong = {} as Procedure, Route> - - const wrongPing = {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, Record, typeof route> - - it('router without lazy', () => { - const router = { ping, pong, nested: { ping, pong } } - expectTypeOf(builder.router(router)).toEqualTypeOf >() - }) - - it('router with lazy', () => { - const router = { - ping: lazy(() => Promise.resolve({ default: ping })), - pong, - nested: lazy(() => Promise.resolve({ - default: { - ping, - pong: lazy(() => Promise.resolve({ default: pong })), - }, - })), - } - - expectTypeOf(builder.router(router)).toEqualTypeOf< - AdaptedRouter< - { auth: boolean }, - typeof router, - typeof baseErrors - > - >() - - builder.router({ ping: lazy(() => Promise.resolve({ default: ping })) }) - // @ts-expect-error - context is not match - builder.router({ wrongPing: lazy(() => Promise.resolve({ default: wrongPing })) }) - }) - - it('procedure as a router', () => { - expectTypeOf(builder.router(ping)).toEqualTypeOf< - AdaptedRouter< - { auth: boolean }, - typeof ping, - typeof baseErrors - > - >() - - expectTypeOf(builder.router(lazy(() => Promise.resolve({ default: ping })))).toEqualTypeOf< - AdaptedRouter< - { auth: boolean }, - Lazy, - typeof baseErrors - > - >() - }) -}) - -describe('to Decorated Adapted Lazy', () => { - const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }, typeof baseErrors, typeof route> - const pong = {} as Procedure, Route> - - const wrongPing = {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, Record, typeof route> - - it('router without lazy', () => { - const router = { - ping, - pong, - nested: { - ping, - pong, - }, - } - - expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf< - DecoratedLazy> - >() - - builder.lazy(() => Promise.resolve({ default: { ping } })) - // @ts-expect-error - context is not match - builder.lazy(() => Promise.resolve({ default: { wrongPing } })) - }) - - it('router with lazy', () => { - const router = { - ping: lazy(() => Promise.resolve({ default: ping })), - pong, - nested: lazy(() => Promise.resolve({ - default: { - ping, - pong: lazy(() => Promise.resolve({ default: pong })), - }, - })), - } - - expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf< - DecoratedLazy> - >() - - builder.lazy(() => Promise.resolve({ default: { ping: lazy(() => Promise.resolve({ default: ping })) } })) - // @ts-expect-error - context is not match - builder.lazy(() => Promise.resolve({ default: { wrongPing: lazy(() => Promise.resolve({ default: wrongPing })) } })) - }) -}) diff --git a/packages/server/src/router-builder.test.ts b/packages/server/src/router-builder.test.ts deleted file mode 100644 index b2389315..00000000 --- a/packages/server/src/router-builder.test.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { ContractProcedure } from '@orpc/contract' -import { z } from 'zod' -import { getLazyRouterPrefix } from './hidden' -import { isLazy, lazy, unlazy } from './lazy' -import { isProcedure, Procedure } from './procedure' -import { RouterBuilder } from './router-builder' - -const mid1 = vi.fn() -const mid2 = vi.fn() - -const baseErrors = { - BASE: { - data: z.object({ why: z.string() }), - }, -} - -const builder = new RouterBuilder({ - middlewares: [mid1, mid2], - prefix: '/prefix', - tags: ['tag1', 'tag2'], - errorMap: baseErrors, -}) - -it('prevent dynamic params on prefix', () => { - expect(() => builder.prefix('/{id}')).toThrowError() - expect(() => new RouterBuilder({ prefix: '/{id}', middlewares: [], errorMap: baseErrors })).toThrowError() -}) - -describe('self chainable', () => { - it('prefix', () => { - const prefixed = builder.prefix('/test') - expect(prefixed).not.toBe(builder) - expect(prefixed).toBeInstanceOf(RouterBuilder) - expect(prefixed['~orpc'].prefix).toBe('/prefix/test') - }) - - it('prefix --- still work without pre prefix', () => { - const builder = new RouterBuilder({ middlewares: [], errorMap: baseErrors }) - - const prefixed = builder.prefix('/test') - expect(prefixed).not.toBe(builder) - expect(prefixed).toBeInstanceOf(RouterBuilder) - expect(prefixed['~orpc'].prefix).toBe('/test') - }) - - it('tag', () => { - const tagged = builder.tag('test1', 'test2') - expect(tagged).not.toBe(builder) - expect(tagged).toBeInstanceOf(RouterBuilder) - expect(tagged['~orpc'].tags).toEqual(['tag1', 'tag2', 'test1', 'test2']) - }) - - it('tag --- still work without pre tag', () => { - const builder = new RouterBuilder({ middlewares: [], errorMap: baseErrors }) - - const tagged = builder.tag('test1', 'test2') - expect(tagged).not.toBe(builder) - expect(tagged).toBeInstanceOf(RouterBuilder) - expect(tagged['~orpc'].tags).toEqual(['test1', 'test2']) - }) - - it('use middleware', () => { - const mid3 = vi.fn() - const mid4 = vi.fn() - - const applied = builder.use(mid3).use(mid4) - expect(applied).not.toBe(builder) - expect(applied).toBeInstanceOf(RouterBuilder) - expect(applied['~orpc'].middlewares).toEqual([mid1, mid2, mid3, mid4]) - }) - - it('use middleware --- still work without pre middleware', () => { - const builder = new RouterBuilder({ middlewares: [], errorMap: baseErrors }) - - const applied = builder.use(mid1).use(mid2) - expect(applied).not.toBe(builder) - expect(applied).toBeInstanceOf(RouterBuilder) - expect(applied['~orpc'].middlewares).toEqual([mid1, mid2]) - }) - - it('errors', () => { - const errors = { - BAD: { - message: 'bad', - code: 400, - data: z.object({ val: z.string().transform(v => Number.parseInt(v)) }), - }, - } - - const applied = builder.errors(errors) - expect(applied).not.toBe(builder) - expect(applied).toBeInstanceOf(RouterBuilder) - - expect(applied['~orpc'].errorMap).toEqual({ - ...baseErrors, - ...errors, - }) - }) -}) - -describe('adapt router', () => { - const pMid1 = vi.fn() - const pMid2 = vi.fn() - - const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - const ping = new Procedure({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: undefined, - route: { - tags: ['tag3', 'tag4'], - }, - errorMap: {}, - }), - handler: vi.fn(), - middlewares: [mid1, pMid1, pMid2], - inputValidationIndex: 3, - outputValidationIndex: 3, - }) - const pong = new Procedure({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: schema, - route: { - method: 'GET', - path: '/pong', - description: 'desc', - }, - errorMap: {}, - }), - handler: vi.fn(), - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, - }) - - const router = { - ping, - pong, - nested: { - ping, - pong, - }, - } - - const routerWithLazy = { - ping: lazy(() => Promise.resolve({ default: ping })), - pong, - nested: lazy(() => Promise.resolve({ default: { - ping, - pong: lazy(() => Promise.resolve({ default: pong })), - } })), - } - - it('router without lazy', () => { - const adapted = builder.router(router) - - expect(adapted.ping).toSatisfy(isProcedure) - expect(adapted.ping['~orpc'].handler).toBe(ping['~orpc'].handler) - expect(adapted.ping['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) - expect(adapted.ping['~orpc'].contract['~orpc'].route?.path).toBe(undefined) - expect(adapted.ping['~orpc'].contract['~orpc'].route?.method).toBe(undefined) - expect(adapted.ping['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) - expect(adapted.ping['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - - expect(adapted.pong).toSatisfy(isProcedure) - expect(adapted.pong['~orpc'].handler).toBe(pong['~orpc'].handler) - expect(adapted.pong['~orpc'].middlewares).toEqual([mid1, mid2]) - expect(adapted.pong['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') - expect(adapted.pong['~orpc'].contract['~orpc'].route?.method).toBe('GET') - expect(adapted.pong['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) - expect(adapted.pong['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - - expect(adapted.nested.ping).toSatisfy(isProcedure) - expect(adapted.nested.ping['~orpc'].handler).toBe(ping['~orpc'].handler) - expect(adapted.nested.ping['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) - expect(adapted.nested.ping['~orpc'].contract['~orpc'].route?.path).toBe(undefined) - expect(adapted.nested.ping['~orpc'].contract['~orpc'].route?.method).toBe(undefined) - expect(adapted.nested.ping['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) - expect(adapted.nested.ping['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - - expect(adapted.nested.pong).toSatisfy(isProcedure) - expect(adapted.nested.pong['~orpc'].handler).toBe(pong['~orpc'].handler) - expect(adapted.nested.pong['~orpc'].middlewares).toEqual([mid1, mid2]) - expect(adapted.nested.pong['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') - expect(adapted.nested.pong['~orpc'].contract['~orpc'].route?.method).toBe('GET') - expect(adapted.nested.pong['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) - expect(adapted.nested.pong['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) - - it('router with lazy', async () => { - const adapted = builder.router(routerWithLazy) as any - - expect(getLazyRouterPrefix(adapted.ping)).toBe('/prefix') - expect(getLazyRouterPrefix(adapted.pong)).toBe(undefined) - expect(getLazyRouterPrefix(adapted.nested)).toBe('/prefix') - expect(getLazyRouterPrefix(adapted.nested.ping)).toBe('/prefix') - expect(getLazyRouterPrefix(adapted.nested.pong)).toBe('/prefix') - - expect(adapted.ping).toSatisfy(isLazy) - expect((await unlazy(adapted.ping) as any).default).toSatisfy(isProcedure) - expect((await unlazy(adapted.ping) as any).default['~orpc'].handler).toBe(ping['~orpc'].handler) - expect((await unlazy(adapted.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) - expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) - expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) - expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) - expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - - expect(adapted.pong).toSatisfy(isProcedure) - expect(adapted.pong['~orpc'].handler).toBe(pong['~orpc'].handler) - expect(adapted.pong['~orpc'].middlewares).toEqual([mid1, mid2]) - expect(adapted.pong['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') - expect(adapted.pong['~orpc'].contract['~orpc'].route?.method).toBe('GET') - expect(adapted.pong['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) - expect(adapted.pong['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - - expect(adapted.nested.ping).toSatisfy(isLazy) - expect((await unlazy(adapted.nested.ping) as any).default).toSatisfy(isProcedure) - expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].handler).toBe(ping['~orpc'].handler) - expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) - expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) - expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) - expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) - expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - - expect(adapted.nested.pong).toSatisfy(isLazy) - expect((await unlazy(adapted.nested.pong) as any).default).toSatisfy(isProcedure) - expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].handler).toBe(pong['~orpc'].handler) - expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].middlewares).toEqual([mid1, mid2]) - expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') - expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.method).toBe('GET') - expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) - expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) - - it('router lazy with nested lazy', async () => { - const adapted = builder.lazy(() => Promise.resolve({ default: routerWithLazy })) as any - - expect(getLazyRouterPrefix(adapted.ping)).toBe('/prefix') - expect(getLazyRouterPrefix(adapted.pong)).toBe('/prefix') - expect(getLazyRouterPrefix(adapted.nested)).toBe('/prefix') - expect(getLazyRouterPrefix(adapted.nested.ping)).toBe('/prefix') - expect(getLazyRouterPrefix(adapted.nested.pong)).toBe('/prefix') - - expect(adapted.ping).toSatisfy(isLazy) - expect((await unlazy(adapted.ping) as any).default).toSatisfy(isProcedure) - expect((await unlazy(adapted.ping) as any).default['~orpc'].handler).toBe(ping['~orpc'].handler) - expect((await unlazy(adapted.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) - expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) - expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) - expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) - expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - - expect(adapted.pong).toSatisfy(isLazy) - expect((await unlazy(adapted.pong) as any).default).toSatisfy(isProcedure) - expect((await unlazy(adapted.pong) as any).default['~orpc'].handler).toBe(pong['~orpc'].handler) - expect((await unlazy(adapted.pong) as any).default['~orpc'].middlewares).toEqual([mid1, mid2]) - expect((await unlazy(adapted.pong) as any).default['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') - expect((await unlazy(adapted.pong) as any).default['~orpc'].contract['~orpc'].route?.method).toBe('GET') - expect((await unlazy(adapted.pong) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) - expect((await unlazy(adapted.pong) as any).default['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - - expect(adapted.nested.ping).toSatisfy(isLazy) - expect((await unlazy(adapted.nested.ping) as any).default).toSatisfy(isProcedure) - expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].handler).toBe(ping['~orpc'].handler) - expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) - expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) - expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) - expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) - expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - - expect(adapted.nested.pong).toSatisfy(isLazy) - expect((await unlazy(adapted.nested.pong) as any).default).toSatisfy(isProcedure) - expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].handler).toBe(pong['~orpc'].handler) - expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].middlewares).toEqual([mid1, mid2]) - expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') - expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.method).toBe('GET') - expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) - expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].errorMap).toEqual(baseErrors) - }) - - it('support procedure as a router', async () => { - const adapted = builder.router(ping) - - expect(adapted).toSatisfy(isProcedure) - expect(adapted['~orpc'].handler).toBe(ping['~orpc'].handler) - expect(adapted['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) - expect(adapted['~orpc'].contract['~orpc'].route?.path).toBe(undefined) - expect(adapted['~orpc'].contract['~orpc'].route?.method).toBe(undefined) - expect(adapted['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) - - const adaptedLazy = builder.router(lazy(() => Promise.resolve({ default: ping }))) - - expect(adaptedLazy).toSatisfy(isLazy) - expect((await unlazy(adaptedLazy) as any).default).toSatisfy(isProcedure) - expect((await unlazy(adaptedLazy) as any).default['~orpc'].handler).toBe(ping['~orpc'].handler) - expect((await unlazy(adaptedLazy) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) - expect((await unlazy(adaptedLazy) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) - expect((await unlazy(adaptedLazy) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) - }) - - it('can concat LAZY_ROUTER_PREFIX_SYMBOL', () => { - const adapted = builder.prefix('/hi').router(builder.router(routerWithLazy)) as any - expect(getLazyRouterPrefix(adapted.ping)).toBe('/prefix/hi/prefix') - }) - - it('works with LAZY_ROUTER_PREFIX_SYMBOL when prefix is not set', () => { - const builderWithoutPrefix = new RouterBuilder({ middlewares: [], errorMap: baseErrors }) - const adapted = builderWithoutPrefix.router(routerWithLazy) as any - expect(getLazyRouterPrefix(adapted.ping)).toBe(undefined) - expect(getLazyRouterPrefix(adapted.pong)).toBe(undefined) - - const adapted2 = builderWithoutPrefix.router(builder.router(routerWithLazy) as any) as any - expect(getLazyRouterPrefix(adapted2.ping)).toBe('/prefix') - expect(getLazyRouterPrefix(adapted2.pong)).toBe(undefined) - }) - - it('getLazyRouterPrefix works', () => { - expect(getLazyRouterPrefix({})).toBe(undefined) - expect(getLazyRouterPrefix(builder.router(routerWithLazy).ping)).toBe('/prefix') - expect(getLazyRouterPrefix(builder.router(routerWithLazy).pong)).toBe(undefined) - }) - - it('deepSetLazyRouterPrefix not recursive on Symbol', () => { - const adapted = builder.router(routerWithLazy) as any - - expect(adapted.nested[Symbol('anything')]).toBe(undefined) - }) -}) diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts deleted file mode 100644 index 8f1973d1..00000000 --- a/packages/server/src/router-builder.ts +++ /dev/null @@ -1,172 +0,0 @@ -import type { ContractRouter, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, HTTPPath, Route, StrictErrorMap } from '@orpc/contract' -import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' -import type { FlattenLazy, Lazy } from './lazy' -import type { ANY_MIDDLEWARE, Middleware } from './middleware' -import type { ANY_PROCEDURE, Procedure } from './procedure' -import type { ANY_ROUTER, Router } from './router' -import { mergePrefix, mergeTags } from '@orpc/contract' -import { deepSetLazyRouterPrefix, getLazyRouterPrefix } from './hidden' -import { flatLazy, isLazy, lazy, unlazy } from './lazy' -import { type DecoratedLazy, decorateLazy } from './lazy-decorated' -import { isProcedure } from './procedure' -import { DecoratedProcedure } from './procedure-decorated' - -export type AdaptedRouter< - TInitialContext extends Context, - TRouter extends ANY_ROUTER, - TErrorMapExtra extends ErrorMap, -> = TRouter extends Lazy - ? DecoratedLazy> - : TRouter extends Procedure - ? DecoratedProcedure - : { - [K in keyof TRouter]: TRouter[K] extends ANY_ROUTER ? AdaptedRouter : never - } - -export type RouterBuilderDef< - TInitialContext extends Context, - TCurrentContext extends Context, - TErrorMap extends ErrorMap, -> = { - __initialContext?: TypeInitialContext - __currentContext?: TypeCurrentContext - prefix?: HTTPPath - tags?: readonly string[] - middlewares: Middleware[] - errorMap: TErrorMap -} - -export class RouterBuilder< - TInitialContext extends Context, - TCurrentContext extends Context, - TErrorMap extends ErrorMap, -> { - '~type' = 'RouterBuilder' as const - '~orpc': RouterBuilderDef - - constructor(def: RouterBuilderDef) { - this['~orpc'] = def - - if (def.prefix && def.prefix.includes('{')) { - throw new Error(` - Dynamic routing in prefix not supported yet. - Please remove "{" from "${def.prefix}". - `) - } - } - - prefix(prefix: HTTPPath): RouterBuilder { - return new RouterBuilder({ - ...this['~orpc'], - prefix: mergePrefix(this['~orpc'].prefix, prefix), - }) - } - - tag(...tags: string[]): RouterBuilder { - return new RouterBuilder({ - ...this['~orpc'], - tags: mergeTags(this['~orpc'].tags, tags), - }) - } - - errors & ErrorMapSuggestions>( - errors: U, - ): RouterBuilder & TErrorMap> { - return new RouterBuilder({ - ...this['~orpc'], - errorMap: { - ...this['~orpc'].errorMap, - ...errors, - }, - }) - } - - use( - middleware: Middleware >, - ): ConflictContextGuard - & RouterBuilder { - const builder = new RouterBuilder({ - tags: this['~orpc'].tags, - prefix: this['~orpc'].prefix, - errorMap: this['~orpc'].errorMap, - middlewares: [...this['~orpc'].middlewares, middleware as any], - }) - - return builder as typeof builder & ConflictContextGuard - } - - router>>>( - router: U, - ): AdaptedRouter { - const adapted = adapt(router, this['~orpc']) - return adapted as any - } - - lazy>>>( - loader: () => Promise<{ default: U }>, - ): AdaptedRouter, TErrorMap> { - const adapted = adapt(flatLazy(lazy(loader)), this['~orpc']) - return adapted as any - } -} - -function adapt( - item: ANY_ROUTER, - options: { - middlewares?: ANY_MIDDLEWARE[] - tags?: readonly string[] - prefix?: HTTPPath - errorMap: ErrorMap - }, -): ANY_ROUTER { - if (isLazy(item)) { - const adaptedLazy = decorateLazy(lazy(async () => { - const routerOrProcedure = (await unlazy(item)).default as ANY_ROUTER | ANY_PROCEDURE - const adapted = adapt(routerOrProcedure, options) - - return { default: adapted } - })) - - const lazyPrefix = getLazyRouterPrefix(item) - if (options.prefix || lazyPrefix) { - const prefixed = deepSetLazyRouterPrefix(adaptedLazy, `${options.prefix ?? ''}${lazyPrefix ?? ''}` as any) - return prefixed - } - - return adaptedLazy - } - - if (isProcedure(item)) { - let decorated = DecoratedProcedure.decorate(item) - - if (options.tags?.length) { - decorated = decorated.unshiftTag(...options.tags) - } - - if (options.prefix) { - decorated = decorated.prefix(options.prefix) - } - - if (options.middlewares?.length) { - decorated = decorated.unshiftMiddleware(...options.middlewares) - } - - // it prevent unnecessary call especially when implements a contract - if (Object.keys(options.errorMap).length) { - /** - * The error map has been protected from conflicts at the type level, - * so it is safe to cast here. - */ - decorated = decorated.errors(options.errorMap as any) - } - - return decorated - } - - const adapted = {} as Record - for (const key in item) { - adapted[key] = adapt(item[key]!, options) - } - - return adapted -} diff --git a/packages/server/src/router-client.test-d.ts b/packages/server/src/router-client.test-d.ts index 4270ae45..349ce4eb 100644 --- a/packages/server/src/router-client.test-d.ts +++ b/packages/server/src/router-client.test-d.ts @@ -1,146 +1,30 @@ -import type { Client, NestedClient, ORPCError, Route } from '@orpc/contract' -import type { Context } from './context' -import type { Procedure } from './procedure' -import type { Meta } from './types' -import { z } from 'zod' -import { lazy } from './lazy' -import { createRouterClient, type RouterClient } from './router-client' +import type { Client, ErrorFromErrorMap, NestedClient } from '@orpc/contract' +import type { baseErrorMap } from '../../contract/tests/shared' +import type { router } from '../tests/shared' +import type { RouterClient } from './router-client' -const schema = z.object({ val: z.string().transform(val => Number(val)) }) -const baseErrors = { - CODE: { - data: z.object({ why: z.string().transform(v => Number(v)) }), - }, -} - -const route = { method: 'GET', path: '/ping' } as const - -const ping = {} as Procedure -const pong = {} as Procedure<{ auth: boolean }, { auth: boolean }, undefined, undefined, unknown, Record, Route> - -const router = { - ping, - pong, - nested: { - ping, - pong, - }, -} - -const routerWithLazy = { - ping: lazy(() => Promise.resolve({ default: ping })), - pong, - nested: lazy(() => Promise.resolve({ default: { - ping, - pong: lazy(() => Promise.resolve({ default: pong })), - } })), -} +const routerClient = {} as RouterClient describe('RouterClient', () => { - it('compatible with NestedClient', () => { - expectTypeOf>().toMatchTypeOf>() - expectTypeOf>().not.toMatchTypeOf>() + it('is a nested client', () => { + expectTypeOf(routerClient).toMatchTypeOf>() }) - it('router without lazy', () => { - const client = {} as RouterClient - - expectTypeOf(client.ping).toEqualTypeOf< - Client> - >() - expectTypeOf(client.pong).toEqualTypeOf< - Client + it('works', () => { + expectTypeOf(routerClient.ping).toEqualTypeOf< + Client<'client-context', { input: number }, { output: string }, ErrorFromErrorMap> >() - expectTypeOf(client.nested.ping).toEqualTypeOf< - Client> + expectTypeOf(routerClient.nested.ping).toEqualTypeOf< + Client<'client-context', { input: number }, { output: string }, ErrorFromErrorMap> >() - expectTypeOf(client.nested.pong).toEqualTypeOf< - Client - >() - }) - it('support lazy', () => { - expectTypeOf>().toEqualTypeOf>() - }) - - it('support procedure as router', () => { - expectTypeOf>().toEqualTypeOf>>() - }) -}) - -describe('createRouterClient', () => { - it('return RouterClient', () => { - const client = createRouterClient(router, { - context: { auth: true }, - }) - - expectTypeOf(client).toMatchTypeOf>() - - const client2 = createRouterClient(routerWithLazy, { - context: { auth: true }, - }) - expectTypeOf(client2).toMatchTypeOf>() - }) - - it('required context when needed', () => { - createRouterClient({ ping }) - - createRouterClient({ pong }, { - context: { auth: true }, - }) - - createRouterClient({ pong }, { - context: () => ({ auth: true }), - }) - - createRouterClient({ pong }, { - context: async () => ({ auth: true }), - }) - - createRouterClient({ pong }, { - // @ts-expect-error --- invalid context - context: { auth: 'invalid' }, - }) - - // @ts-expect-error --- missing context - createRouterClient({ pong }) - }) - - it('support hooks', () => { - createRouterClient(router, { - context: { auth: true }, - onSuccess: async ({ output }, context, meta) => { - expectTypeOf(output).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf() - }, - }) - }) - - it('support base path', () => { - createRouterClient({ ping }, { - context: { auth: true }, - path: ['users'], - }) - - // @ts-expect-error --- invalid path - createRouterClient({ ping }, { - context: { auth: true }, - path: [123], - }) - }) - - it('with client context', () => { - const client = createRouterClient(router, { - context: async (clientContext: { cache?: boolean } | undefined) => { - return { auth: true } - }, - }) + expectTypeOf(routerClient.pong).toEqualTypeOf< + Client<'client-context', unknown, unknown, Error> + >() - client.ping({ val: '123' }) - client.ping({ val: '123' }, { context: { cache: true } }) - // @ts-expect-error - invalid context - client.ping({ val: '123' }, { context: { cache: '123' } }) + expectTypeOf(routerClient.nested.pong).toEqualTypeOf< + Client<'client-context', unknown, unknown, Error> + >() }) }) diff --git a/packages/server/src/router-client.test.ts b/packages/server/src/router-client.test.ts index 03ce4378..492511fa 100644 --- a/packages/server/src/router-client.test.ts +++ b/packages/server/src/router-client.test.ts @@ -1,7 +1,5 @@ -import { ContractProcedure } from '@orpc/contract' -import { z } from 'zod' -import { lazy, unlazy } from './lazy' -import { Procedure } from './procedure' +import { ping, pong, router } from '../tests/shared' +import { unlazy } from './lazy' import { createProcedureClient } from './procedure-client' import { createRouterClient } from './router-client' @@ -14,43 +12,8 @@ beforeEach(() => { }) describe('createRouterClient', () => { - const schema = z.object({ val: z.string().transform(v => Number(v)) }) - const ping = new Procedure({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: schema, - errorMap: {}, - route: {}, - }), - handler: vi.fn(() => ({ val: '123' })), - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, - }) - const pong = new Procedure({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - route: {}, - }), - handler: vi.fn(() => ('output')), - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, - }) - - const router = { - ping: lazy(() => Promise.resolve({ default: ping })), - pong, - nested: lazy(() => Promise.resolve({ default: { - ping, - pong: lazy(() => Promise.resolve({ default: pong })), - } })), - } - const client = createRouterClient(router, { - context: { auth: true }, + context: { db: 'postgres' }, path: ['users'], }) @@ -59,7 +22,7 @@ describe('createRouterClient', () => { expect(createProcedureClient).toBeCalledTimes(1) expect(createProcedureClient).toBeCalledWith(pong, expect.objectContaining({ - context: { auth: true }, + context: { db: 'postgres' }, path: ['users', 'pong'], })) @@ -68,52 +31,18 @@ describe('createRouterClient', () => { }) it('work with lazy', async () => { - expect(client.ping({ val: '123' })).toEqual('__mocked__') - - expect(createProcedureClient).toBeCalledTimes(1) - expect(createProcedureClient).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ - context: { auth: true }, - path: ['users', 'ping'], - })) - - expect((await unlazy(vi.mocked(createProcedureClient as any).mock.calls[0]![0])).default).toBe(ping) - - expect(vi.mocked(createProcedureClient).mock.results[0]?.value).toBeCalledTimes(1) - expect(vi.mocked(createProcedureClient).mock.results[0]?.value).toBeCalledWith({ val: '123' }) - }) - - it('work with nested lazy', async () => { - expect(client.nested.ping({ val: '123' })).toEqual('__mocked__') + expect(client.nested.ping({ input: 123 })).toEqual('__mocked__') expect(createProcedureClient).toBeCalledTimes(2) - expect(createProcedureClient).toHaveBeenNthCalledWith(2, expect.any(Object), expect.objectContaining({ - context: { auth: true }, + expect(createProcedureClient).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ + context: { db: 'postgres' }, path: ['users', 'nested', 'ping'], })) - const lazied = vi.mocked(createProcedureClient as any).mock.calls[1]![0] - expect(await unlazy(lazied)).toEqual({ default: ping }) + expect((await unlazy(vi.mocked(createProcedureClient as any).mock.calls[1]![0])).default).toBe(ping) expect(vi.mocked(createProcedureClient).mock.results[1]?.value).toBeCalledTimes(1) - expect(vi.mocked(createProcedureClient).mock.results[1]?.value).toBeCalledWith({ val: '123' }) - }) - - it('work with procedure as router', () => { - const client = createRouterClient(ping, { - context: { auth: true }, - path: ['users'], - }) - - expect(client({ val: '123' })).toEqual('__mocked__') - - expect(createProcedureClient).toBeCalledTimes(1) - expect(createProcedureClient).toHaveBeenCalledWith(ping, expect.objectContaining({ - context: { auth: true }, - path: ['users'], - })) - - expect(vi.mocked(createProcedureClient).mock.results[0]?.value).toBeCalledTimes(1) - expect(vi.mocked(createProcedureClient).mock.results[0]?.value).toBeCalledWith({ val: '123' }) + expect(vi.mocked(createProcedureClient).mock.results[1]?.value).toBeCalledWith({ input: 123 }) }) it('hooks', async () => { @@ -124,7 +53,7 @@ describe('createRouterClient', () => { const interceptor = vi.fn() const client = createRouterClient(router, { - context: { auth: true }, + context: { db: 'postgres' }, onStart, onSuccess, onError, @@ -136,7 +65,7 @@ describe('createRouterClient', () => { expect(createProcedureClient).toBeCalledTimes(1) expect(createProcedureClient).toHaveBeenCalledWith(pong, expect.objectContaining({ - context: { auth: true }, + context: { db: 'postgres' }, path: ['pong'], onStart, onSuccess, @@ -152,19 +81,19 @@ describe('createRouterClient', () => { it('return undefined if access the undefined key', async () => { const client = createRouterClient({ - ping, + pong, }) // @ts-expect-error --- invalid access - expect(client.router).toBeUndefined() + expect(client.undefined).toBeUndefined() }) it('works without base path', async () => { const client = createRouterClient({ - ping, + pong, }) - expect(client.ping({ val: '123' })).toEqual('__mocked__') - expect(vi.mocked(createProcedureClient).mock.calls[0]![1]!.path).toEqual(['ping']) + expect(client.pong({ val: '123' })).toEqual('__mocked__') + expect(vi.mocked(createProcedureClient).mock.calls[0]![1]!.path).toEqual(['pong']) }) }) diff --git a/packages/server/src/router-client.ts b/packages/server/src/router-client.ts index c72de7b0..444ba6ad 100644 --- a/packages/server/src/router-client.ts +++ b/packages/server/src/router-client.ts @@ -2,25 +2,22 @@ import type { Hooks, Value } from '@orpc/shared' import type { Lazy } from './lazy' import type { Procedure } from './procedure' import type { CreateProcedureClientRest, ProcedureClient } from './procedure-client' -import type { Meta } from './types' +import type { AnyRouter, Router } from './router' import { isLazy } from './lazy' import { createLazyProcedureFormAnyLazy } from './lazy-utils' import { isProcedure } from './procedure' import { createProcedureClient } from './procedure-client' -import { type ANY_ROUTER, getRouterChild, type Router } from './router' +import { getRouterChild } from './router' -/** - * FIXME: separate RouterClient and ContractRouterClient, don't mix them - */ -export type RouterClient = TRouter extends Lazy +export type RouterClient = TRouter extends Lazy ? RouterClient : TRouter extends Procedure ? ProcedureClient : { - [K in keyof TRouter]: TRouter[K] extends ANY_ROUTER ? RouterClient : never + [K in keyof TRouter]: TRouter[K] extends AnyRouter ? RouterClient : never } -export type CreateRouterClientOptions = +export type CreateRouterClientOptions = & { /** * This is helpful for logging and analytics. @@ -32,10 +29,10 @@ export type CreateRouterClientOptions = & (TRouter extends Router ? undefined extends UContext ? { context?: Value } : { context: Value } : never) - & Hooks ? UContext : never, Meta> + & Hooks ? UContext : never, any> export function createRouterClient< - TRouter extends ANY_ROUTER, + TRouter extends AnyRouter, TClientContext, >( router: TRouter | Lazy, diff --git a/packages/server/src/router-implementer.test-d.ts b/packages/server/src/router-implementer.test-d.ts deleted file mode 100644 index 68041e6a..00000000 --- a/packages/server/src/router-implementer.test-d.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { DecoratedLazy } from './lazy-decorated' -import type { Middleware, MiddlewareOutputFn } from './middleware' -import type { ANY_PROCEDURE } from './procedure' -import type { AdaptedRouter } from './router-builder' -import type { RouterImplementer } from './router-implementer' -import { oc } from '@orpc/contract' -import { z } from 'zod' -import { lazy } from './lazy' -import { Procedure } from './procedure' - -const schema = z.object({ val: z.string().transform(val => Number(val)) }) - -const ping = oc.input(schema).output(schema) -const pong = oc.route({ method: 'GET', path: '/ping' }) - -const contract = oc.router({ - ping, - pong, - nested: { - ping, - pong, - }, -}) - -const pingImpl = new Procedure({ - contract: ping, - handler: vi.fn(), - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, -}) - -const pongImpl = new Procedure({ - contract: pong, - handler: vi.fn(), - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, -}) - -const router = { - ping: pingImpl, - pong: pongImpl, - nested: { - ping: pingImpl, - pong: pongImpl, - }, -} - -const routerWithLazy = { - ping: lazy(() => Promise.resolve({ default: pingImpl })), - pong: pongImpl, - nested: lazy(() => Promise.resolve({ - default: { - ping: pingImpl, - pong: lazy(() => Promise.resolve({ default: pongImpl })), - }, - })), -} - -const implementer = {} as RouterImplementer<{ auth: boolean }, { auth: boolean } & { db: string }, typeof contract> - -describe('self chainable', () => { - it('use middleware', () => { - implementer.use(({ context, path, errors, next, procedure, signal }, input, output) => { - expectTypeOf(context).toEqualTypeOf<{ auth: boolean } & { db: string }>() - expectTypeOf(path).toEqualTypeOf() - expectTypeOf(errors).toEqualTypeOf>() - expectTypeOf(procedure).toEqualTypeOf() - expectTypeOf(signal).toEqualTypeOf() - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(output).toEqualTypeOf>() - - return next({}) - }) - - const mid1 = {} as Middleware<{ auth: boolean }, { auth: boolean }, unknown, unknown, Record> - const mid2 = {} as Middleware<{ auth: boolean }, { auth: boolean } & { dev: string }, unknown, unknown, Record> - const mid3 = {} as Middleware<{ auth: boolean, db: string }, { auth: boolean, db: string } & { dev: string }, unknown, unknown, Record> - - expectTypeOf(implementer.use(mid1)).toEqualTypeOf() - expectTypeOf(implementer.use(mid2)).toEqualTypeOf< - RouterImplementer<{ auth: boolean }, { auth: boolean } & { db: string } & { dev: string }, typeof contract> - >() - expectTypeOf(implementer.use(mid3)).toEqualTypeOf< - RouterImplementer<{ auth: boolean }, { auth: boolean } & { db: string } & { auth: boolean, db: string } & { dev: string }, typeof contract> - >() - - const mid4 = {} as Middleware<{ auth: boolean }, { auth: boolean, dev: string }, unknown, { val: string }, Record> - const mid5 = {} as Middleware<{ auth: boolean }, { auth: boolean, dev: string }, unknown, { val: number }, Record> - const mid6 = {} as Middleware<{ auth: 'invalid' }, { auth: 'invalid' }, any, any, Record> - - // @ts-expect-error - invalid middleware - implementer.use(mid4) - // @ts-expect-error - invalid middleware - implementer.use(mid5) - // @ts-expect-error - invalid middleware - implementer.use(mid6) - // @ts-expect-error - invalid middleware - implementer.use(true) - // @ts-expect-error - invalid middleware - implementer.use(() => {}) - - // conflict context but not detected - expectTypeOf(implementer.use(({ next }) => next({ context: { auth: undefined } }))).toEqualTypeOf() - }) -}) - -it('to AdaptedRouter', () => { - expectTypeOf(implementer.router(router)).toMatchTypeOf< - AdaptedRouter<{ auth: boolean }, typeof router, Record> - >() - - expectTypeOf(implementer.router(routerWithLazy)).toMatchTypeOf< - AdaptedRouter<{ auth: boolean }, typeof routerWithLazy, Record> - >() -}) - -it('to AdaptedLazy', () => { - expectTypeOf(implementer.lazy(() => Promise.resolve({ default: router }))).toMatchTypeOf< - DecoratedLazy>> - >() - - expectTypeOf(implementer.lazy(() => Promise.resolve({ default: routerWithLazy }))).toMatchTypeOf< - DecoratedLazy>> - >() -}) diff --git a/packages/server/src/router-implementer.test.ts b/packages/server/src/router-implementer.test.ts deleted file mode 100644 index e90441de..00000000 --- a/packages/server/src/router-implementer.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { oc } from '@orpc/contract' -import { z } from 'zod' -import { unlazy } from './lazy' -import { Procedure } from './procedure' -import { RouterImplementer } from './router-implementer' -import { unshiftMiddlewaresRouter } from './router-utils' - -vi.mock('./router-utils', () => ({ - unshiftMiddlewaresRouter: vi.fn(() => ({ mocked: true })), -})) - -beforeEach(() => { - vi.clearAllMocks() -}) - -const schema = z.object({ val: z.string().transform(val => Number(val)) }) - -const ping = oc.input(schema).output(schema) -const pong = oc.route({ method: 'GET', path: '/ping' }) - -const contract = oc.router({ - ping, - pong, - nested: { - ping, - pong, - }, -}) - -const pingImpl = new Procedure({ - contract: ping, - handler: vi.fn(), - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, -}) - -const pongImpl = new Procedure({ - contract: pong, - handler: vi.fn(), - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, -}) - -const router = { - ping: pingImpl, - pong: pongImpl, - nested: { - ping: pingImpl, - pong: pongImpl, - }, -} - -const mid = vi.fn() -const implementer = new RouterImplementer({ - contract, - middlewares: [mid], -}) - -describe('self chainable', () => { - it('use middleware', () => { - const mid1 = vi.fn() - const mid2 = vi.fn() - const mid3 = vi.fn() - - const implementer = new RouterImplementer({ - contract, - middlewares: [], - }) - - const applied1 = implementer.use(mid1) - expect(applied1).not.toBe(implementer) - expect(applied1).toBeInstanceOf(RouterImplementer) - expect(applied1['~orpc'].middlewares).toEqual([mid1]) - - const applied2 = applied1.use(mid2).use(mid3) - expect(applied2['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) - }) -}) - -describe('to AdaptedRouter', () => { - it('works', () => { - expect(implementer.router(router)).toEqual({ mocked: true }) - - expect(unshiftMiddlewaresRouter).toBeCalledTimes(1) - expect(unshiftMiddlewaresRouter).toBeCalledWith(router, expect.objectContaining({ - middlewares: [mid], - })) - }) -}) - -describe('to AdaptedLazy', () => { - it('works', () => { - const loader = () => Promise.resolve({ default: router }) - expect(implementer.lazy(loader)).toEqual({ mocked: true }) - - expect(unshiftMiddlewaresRouter).toBeCalledTimes(1) - expect(unshiftMiddlewaresRouter).toBeCalledWith(expect.any(Object), expect.objectContaining({ - middlewares: [mid], - })) - - expect(unlazy(vi.mocked(unshiftMiddlewaresRouter).mock.calls[0]![0])) - .resolves - .toEqual({ default: router }) - }) -}) diff --git a/packages/server/src/router-implementer.ts b/packages/server/src/router-implementer.ts deleted file mode 100644 index c9545ccb..00000000 --- a/packages/server/src/router-implementer.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { ContractRouter } from '@orpc/contract' -import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' -import type { FlattenLazy } from './lazy' -import type { Middleware } from './middleware' -import type { Router, RouterToContract } from './router' -import { flatLazy, lazy } from './lazy' -import { type UnshiftedMiddlewaresRouter, unshiftMiddlewaresRouter } from './router-utils' - -export type EqualContractGuard, TRouter extends Router> = - TContract extends RouterToContract ? unknown : never - -export interface RouterImplementerDef< - TInitialContext extends Context, - TCurrentContext extends Context, - TContract extends ContractRouter, -> { - __initialContext?: TypeInitialContext - __currentContext?: TypeCurrentContext - middlewares: Middleware[] - contract: TContract -} - -export class RouterImplementer< - TInitialContext extends Context, - TCurrentContext extends Context, - TContract extends ContractRouter, -> { - '~type' = 'RouterImplementer' as const - '~orpc': RouterImplementerDef - - constructor(def: RouterImplementerDef) { - this['~orpc'] = def - } - - use( - middleware: Middleware< - TCurrentContext, - U, - unknown, - unknown, - Record - >, - ): ConflictContextGuard - & RouterImplementer { - const builder = new RouterImplementer({ - contract: this['~orpc'].contract, - middlewares: [...this['~orpc'].middlewares, middleware], - }) - - return builder as typeof builder & ConflictContextGuard - } - - router>( - router: U, - ): EqualContractGuard & UnshiftedMiddlewaresRouter { - const applied = unshiftMiddlewaresRouter(router, this['~orpc']) - return applied as typeof applied & EqualContractGuard - } - - lazy>( - loader: () => Promise<{ default: U }>, - ): EqualContractGuard & UnshiftedMiddlewaresRouter, TInitialContext> { - const applied = unshiftMiddlewaresRouter(flatLazy(lazy(loader)), this['~orpc']) - return applied as typeof applied & EqualContractGuard - } -} diff --git a/packages/server/src/router-utils.ts b/packages/server/src/router-utils.ts deleted file mode 100644 index f03cef81..00000000 --- a/packages/server/src/router-utils.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { Context, TypeInitialContext } from './context' -import type { Lazy } from './lazy' -import type { ANY_MIDDLEWARE } from './middleware' -import type { ANY_ROUTER } from './router' -import { isLazy, lazy, unlazy } from './lazy' -import { type DecoratedLazy, decorateLazy } from './lazy-decorated' -import { isProcedure, type Procedure } from './procedure' -import { DecoratedProcedure } from './procedure-decorated' - -export type UnshiftedMiddlewaresRouter = - TRouter extends Lazy - ? DecoratedLazy> - : TRouter extends Procedure - ? DecoratedProcedure - : { - [K in keyof TRouter]: TRouter[K] extends ANY_ROUTER ? UnshiftedMiddlewaresRouter : never - } - -export function unshiftMiddlewaresRouter( - router: TRouter, - options: { - __initialContext?: TypeInitialContext - middlewares: ANY_MIDDLEWARE[] - }, -): UnshiftedMiddlewaresRouter { - if (isLazy(router)) { - const applied = decorateLazy(lazy(async () => { - const unlaziedRouter = (await unlazy(router)).default as ANY_ROUTER - const applied = unshiftMiddlewaresRouter(unlaziedRouter, options) as any - - return { default: applied } - })) - - return applied as any - } - - if (isProcedure(router)) { - let decorated = DecoratedProcedure.decorate(router) - - if (options.middlewares.length) { - decorated = decorated.unshiftMiddleware(...options.middlewares) - } - - return decorated as any - } - - const applied = {} as Record - - for (const key in router) { - applied[key] = unshiftMiddlewaresRouter(router[key]!, options) - } - - return applied as any -} diff --git a/packages/server/src/router.test-d.ts b/packages/server/src/router.test-d.ts index 4c81d49d..5cf961d8 100644 --- a/packages/server/src/router.test-d.ts +++ b/packages/server/src/router.test-d.ts @@ -1,282 +1,97 @@ -import type { Route } from '@orpc/contract' +import type { MergedErrorMap, Meta } from '@orpc/contract' +import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../../contract/tests/shared' +import type { CurrentContext, InitialContext } from '../tests/shared' import type { Context } from './context' -import type { ANY_LAZY, Lazy } from './lazy' +import type { Lazy } from './lazy' import type { Procedure } from './procedure' -import type { ANY_ROUTER, InferRouterInputs, InferRouterOutputs, Router } from './router' -import { oc } from '@orpc/contract' -import { z } from 'zod' -import { lazy } from './lazy' -import { getRouterChild } from './router' - -const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - -const baseErrors = { - CODE: { - data: z.object({ why: z.string() }), - }, -} as const - -const route = { method: 'GET', path: '/ping' } as const - -const ping = {} as Procedure<{ auth: boolean }, { auth: boolean, db: string }, typeof schema, typeof schema, { val: string }, typeof baseErrors, typeof route> -const pong = {} as Procedure, Route> - -const router = { - ping: lazy(() => Promise.resolve({ default: ping })), - pong, - nested: { - ping, - pong, - }, - lazy: lazy(() => Promise.resolve({ default: { - ping: lazy(() => Promise.resolve({ default: ping })), - pong, - nested: lazy(() => Promise.resolve({ default: { - ping: lazy(() => Promise.resolve({ default: ping })), - pong, - } })), - } })), -} - -it('InferRouterInputs', () => { - type Inputs = InferRouterInputs - - expectTypeOf().toEqualTypeOf<{ val: string }>() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf<{ val: string }>() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf<{ val: string }>() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf<{ val: string }>() - expectTypeOf().toEqualTypeOf() -}) - -it('InferRouterOutputs', () => { - type Outputs = InferRouterOutputs - - expectTypeOf().toEqualTypeOf<{ val: number }>() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf<{ val: number }>() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf<{ val: number }>() - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf<{ val: number }>() - expectTypeOf().toEqualTypeOf() -}) +import type { AdaptedRouter, InferRouterInputs, InferRouterOutputs, Router } from './router' +import { ping, pong, router } from '../tests/shared' describe('Router', () => { - it('require match context', () => { - const ping = {} as Procedure<{ auth: boolean }, { auth: boolean, db: string }, undefined, undefined, unknown, Record, typeof route> - const pong = {} as Procedure<{ auth: string }, { auth: string }, undefined, undefined, unknown, Record, Route> - - const router: Router<{ auth: boolean, userId: string }, any> = { - ping, - // @ts-expect-error auth is not match - pong, - nested: { - ping, - // @ts-expect-error auth is not match - pong, - }, - - pingLazy: lazy(() => Promise.resolve({ default: ping })), - // @ts-expect-error auth is not match - pongLazy: lazy(() => Promise.resolve({ default: pong })), - - nestedLazy1: lazy(() => Promise.resolve({ - default: { - ping, - }, - })), - - nestedLazy2: lazy(() => Promise.resolve({ - default: { - ping: lazy(() => Promise.resolve({ default: ping })), - }, - })), + it('context', () => { + expectTypeOf(ping).toMatchTypeOf>() + expectTypeOf(pong).toMatchTypeOf>() + expectTypeOf(router).toMatchTypeOf>() - // @ts-expect-error auth is not match - nestedLazy3: lazy(() => Promise.resolve({ - default: { - pong, - }, - })), - - // @ts-expect-error auth is not match - nestedLazy4: lazy(() => Promise.resolve({ - default: { - nested: { - pong: lazy(() => Promise.resolve({ default: pong })), - }, - }, - })), - - nestedLazy6: lazy(() => Promise.resolve({ - default: { - nested: lazy(() => Promise.resolve({ - default: { - pingLazy: lazy(() => Promise.resolve({ default: ping })), - }, - })), - }, - })), - - // @ts-expect-error auth is not match - nestedLazy5: lazy(() => Promise.resolve({ - default: { - nested: lazy(() => Promise.resolve({ - default: { - pongLazy: lazy(() => Promise.resolve({ default: pong })), - }, - })), - }, - })), - } + expectTypeOf(ping).not.toMatchTypeOf>() }) +}) - it('require match contract', () => { - const contract = oc.router({ - ping: oc.input(schema), - pong: oc.output(schema), - - nested: oc.router({ - ping: oc.input(schema), - pong: oc.output(schema), - }), - }) - - const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, undefined, unknown, Record, Record> - const pong = {} as Procedure, Record> - - const router1: Router<{ auth: boolean, userId: string }, typeof contract> = { - ping, - pong, - nested: { - ping, - pong, - }, - } - - const router2: Router<{ auth: boolean, userId: string }, typeof contract> = { - ping, - pong: lazy(() => Promise.resolve({ default: pong })), - nested: { - ping: lazy(() => Promise.resolve({ default: ping })), - pong, - }, - } - - const router3: Router<{ auth: boolean, userId: string }, typeof contract> = { - ping: lazy(() => Promise.resolve({ default: ping })), - pong, - nested: lazy(() => Promise.resolve({ - default: { - ping: lazy(() => Promise.resolve({ default: ping })), - pong, - }, - })), - } - - // @ts-expect-error missing - const router4: Router<{ auth: boolean, userId: string }, typeof contract> = {} - - const router39: Router<{ auth: boolean, userId: string }, typeof contract> = { - // @ts-expect-error wrong ping - ping: pong, - pong, - nested: { - ping, - // @ts-expect-error wrong pong - pong: ping, - }, - } - - const router565: Router<{ auth: boolean, userId: string }, typeof contract> = { - // @ts-expect-error wrong ping - ping: lazy(() => Promise.resolve({ default: pong })), - pong, - nested: { - ping, - // @ts-expect-error wrong pong - pong: lazy(() => Promise.resolve({ default: ping })), - }, - } - - const router343: Router<{ auth: boolean, userId: string }, typeof contract> = { - // @ts-expect-error wrong ping - ping: lazy(() => Promise.resolve({ default: pong })), - pong, - // @ts-expect-error wrong nested - nested: lazy(() => Promise.resolve({ - default: { - ping: lazy(() => Promise.resolve({ default: ping })), - pong: lazy(() => Promise.resolve({ default: ping })), - }, - })), - } - }) - - it('require match contract and errorMap', () => { - const pingContract = oc.input(schema).errors({ - BAD_GATEWAY: { - status: 502, - data: z.object({ - val: z.string().transform(val => Number(val)), - }), - }, - }) - - const routerContract = { - ping: pingContract, - } - - expectTypeOf({ - ping: {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, undefined, unknown, typeof pingContract['~orpc']['errorMap'], Record>, - }).toMatchTypeOf>() - - const likeErrors = { - BAD_GATEWAY: { - status: 502, - data: z.object({ - val: z.string().transform(val => Number(val)), - }), - }, - } - - expectTypeOf({ - ping: {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, undefined, unknown, typeof likeErrors, typeof route>, - }).not.toMatchTypeOf>() - }) +it('InferRouterInputs', () => { + type Inferred = InferRouterInputs - it('support procedure as a router', () => { - const router1: Router<{ auth: boolean, userId: string }, any> = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, undefined, unknown, Record, Record> - // @ts-expect-error - invalid context - const router2: Router<{ auth: boolean, userId: string }, any> = {} as Procedure<{ auth: boolean, dev: boolean }, { db: string }, typeof schema, undefined, unknown> + expectTypeOf().toEqualTypeOf<{ input: number }>() + expectTypeOf().toEqualTypeOf<{ input: number }>() - const pingContract = oc.input(schema) - const router3: Router<{ auth: boolean, userId: string }, typeof pingContract> = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, undefined, unknown, Record, Record> - // @ts-expect-error - mismatch contract - const router4: Router<{ auth: boolean, userId: string }, typeof pingContract> = {} as Procedure<{ auth: boolean }, { db: string }, undefined, undefined, unknown> - }) + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() }) -describe('getRouterChild', () => { - it('works', () => { - getRouterChild({}) - getRouterChild(router) - getRouterChild(lazy(() => Promise.resolve({ default: router }))) - getRouterChild(lazy(() => Promise.resolve({ default: undefined }))) +it('InferRouterOutputs', () => { + type Inferred = InferRouterOutputs - // @ts-expect-error --- invalid router - getRouterChild(1) + expectTypeOf().toEqualTypeOf<{ output: string }>() + expectTypeOf().toEqualTypeOf<{ output: string }>() - expectTypeOf(getRouterChild({})).toEqualTypeOf | undefined>() - }) + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() +}) - it('return lazy if router is lazy', () => { - expectTypeOf( - getRouterChild(lazy(() => Promise.resolve({ default: router })), 'a', 'b'), - ) - .toMatchTypeOf() - }) +it('AdaptedRouter', () => { + type TErrorMap = { INVALID: { message: string }, OVERRIDE: { message: string } } + type Applied = AdaptedRouter + + expectTypeOf().toEqualTypeOf< + Lazy< + Procedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + MergedErrorMap , + BaseMeta + > + > + >() + + expectTypeOf().toEqualTypeOf< + Lazy< + Procedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + MergedErrorMap, + BaseMeta + > + > + >() + + expectTypeOf().toEqualTypeOf< + Procedure< + InitialContext, + Context, + undefined, + undefined, + unknown, + MergedErrorMap>, + Meta + > + >() + + expectTypeOf().toEqualTypeOf< + Lazy< + Procedure< + InitialContext, + Context, + undefined, + undefined, + unknown, + MergedErrorMap>, + Meta + > + > + >() }) diff --git a/packages/server/src/router.test.ts b/packages/server/src/router.test.ts index 6763e72e..3d0ad4d7 100644 --- a/packages/server/src/router.test.ts +++ b/packages/server/src/router.test.ts @@ -1,108 +1,88 @@ -import { ContractProcedure } from '@orpc/contract' -import { z } from 'zod' -import { isLazy, lazy, unlazy } from './lazy' -import { Procedure } from './procedure' -import { getRouterChild } from './router' - -describe('getRouterChild', () => { - const schema = z.object({ val: z.string().transform(val => Number(val)) }) - - const ping = new Procedure({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: schema, - errorMap: {}, - route: {}, - }), - handler: vi.fn(() => ({ val: '123' })), - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, - }) - const pong = new Procedure({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - errorMap: {}, - route: {}, - }), - handler: vi.fn(() => ('output')), - middlewares: [], - inputValidationIndex: 0, - outputValidationIndex: 0, - }) +import { ping, pingMiddleware, pong, router } from '../tests/shared' +import { getLazyRouterPrefix } from './hidden' +import { isLazy, unlazy } from './lazy' +import { isProcedure } from './procedure' +import { adaptRouter, getRouterChild } from './router' + +it('adaptRouter', () => { + const mid = vi.fn() - it('with procedure as router', () => { - expect(getRouterChild(ping)).toBe(ping) - expect(getRouterChild(ping, '~orpc')).toBe(undefined) - expect(getRouterChild(ping, '~type')).toBe(undefined) + const extraErrorMap = { EXTRA: {} } + const adapted = adaptRouter(router, { + errorMap: extraErrorMap, + middlewares: [mid, pingMiddleware], + prefix: '/adapt', + tags: ['adapt'], }) - it('with router', () => { - const router = { - ping, - pong, - nested: { - ping, - pong, + const satisfyAdaptedPing = ({ default: adaptedPing }: any) => { + expect(adaptedPing).toSatisfy(isProcedure) + expect(adaptedPing).not.toBe(ping) + expect(adaptedPing['~orpc']).toMatchObject({ + ...ping['~orpc'], + errorMap: { ...ping['~orpc'].errorMap, ...extraErrorMap }, + middlewares: [mid, ...ping['~orpc'].middlewares], + route: { + ...ping['~orpc'].route, + tags: ['adapt'], + path: '/adapt/base', }, - } - - expect(getRouterChild(router, 'ping')).toBe(ping) - expect(getRouterChild(router, 'pong')).toBe(pong) - expect(getRouterChild(router, 'nested')).toBe(router.nested) - expect(getRouterChild(router, 'nested', 'ping')).toBe(ping) - expect(getRouterChild(router, 'nested', 'pong')).toBe(pong) - expect(getRouterChild(router, 'nested', '~orpc')).toBe(undefined) - expect(getRouterChild(router, 'nested', 'ping', '~orpc')).toBe(undefined) - expect(getRouterChild(router, 'nested', 'pue', '~orpc', 'peng', 'pue')).toBe(undefined) - }) + inputValidationIndex: 2, + outputValidationIndex: 2, + }) + + return true + } - it('with lazy router', async () => { - const lazyPing = lazy(() => Promise.resolve({ default: ping })) - const lazyPong = lazy(() => Promise.resolve({ default: pong })) - - const lazyNested = lazy(() => Promise.resolve({ - default: { - ping, - pong: lazyPong, - nested2: lazy(() => Promise.resolve({ - default: { - ping, - pong: lazyPong, - }, - })), + const satisfyAdaptedPong = ({ default: adaptedPong }: any) => { + expect(adaptedPong).toSatisfy(isProcedure) + expect(adaptedPong).not.toBe(pong) + expect(adaptedPong['~orpc']).toEqual({ + ...pong['~orpc'], + errorMap: { ...pong['~orpc'].errorMap, ...extraErrorMap }, + middlewares: [mid, pingMiddleware], + route: { + tags: ['adapt'], }, - })) + inputValidationIndex: 2, + outputValidationIndex: 2, + }) - const router = { - ping: lazyPing, - pong, - nested: lazyNested, - } + return true + } - expect(await unlazy(getRouterChild(router, 'ping'))).toEqual({ default: ping }) - expect(getRouterChild(router, 'pong')).toBe(pong) + expect(adapted.ping).toSatisfy(isLazy) + expect(unlazy(adapted.ping)).resolves.toSatisfy(satisfyAdaptedPing) + expect(getLazyRouterPrefix(adapted.ping)).toBe('/adapt') - expect(getRouterChild(router, 'nested')).toSatisfy(isLazy) - expect(getRouterChild(router, 'nested', 'ping')).toSatisfy(isLazy) - expect(getRouterChild(router, 'nested', 'pong')).toSatisfy(isLazy) + expect(adapted.nested).toSatisfy(isLazy) + expect(getLazyRouterPrefix(adapted.nested)).toBe('/adapt') - expect(getRouterChild(router, 'nested')).toBe(lazyNested) - expect(await unlazy(getRouterChild(router, 'nested', 'ping'))).toEqual({ default: ping }) - expect(await unlazy(getRouterChild(router, 'nested', 'pong'))).toEqual({ default: pong }) + expect(adapted.nested.ping).toSatisfy(isLazy) + expect(unlazy(adapted.nested.ping)).resolves.toSatisfy(satisfyAdaptedPing) + expect(getLazyRouterPrefix(adapted.nested.ping)).toBe('/adapt') - expect(getRouterChild(router, 'nested', '~orpc')).toSatisfy(isLazy) - expect(await unlazy(getRouterChild(router, 'nested', '~orpc'))).toEqual({ default: undefined }) + expect({ default: adapted.pong }).toSatisfy(satisfyAdaptedPong) - expect(await unlazy(getRouterChild(router, 'nested', 'nested2', 'pong'))).toEqual({ default: pong }) - expect(await unlazy(getRouterChild(router, 'nested', 'nested2', 'peo', 'pue', 'cu', 'la'))).toEqual({ default: undefined }) - }) + expect(adapted.nested.pong).toSatisfy(isLazy) + expect(unlazy(adapted.nested.pong)).resolves.toSatisfy(satisfyAdaptedPong) + expect(getLazyRouterPrefix(adapted.nested.pong)).toBe('/adapt') +}) - it('support Lazy', async () => { - const lazied = lazy(() => Promise.resolve({ default: undefined })) +it('getRouterChild', () => { + expect(getRouterChild(router, 'pong')).toEqual(pong) + expect(getRouterChild(router, 'pong', 'not-exist')).toEqual(undefined) - expect(await unlazy(getRouterChild(lazied, 'ping'))).toEqual({ default: undefined }) - expect(await unlazy(getRouterChild(lazied, 'ping', '~orpc'))).toEqual({ default: undefined }) - }) + expect(getRouterChild(router, 'ping')).toSatisfy(isLazy) + expect(unlazy(getRouterChild(router, 'ping'))).resolves.toEqual({ default: ping }) + + expect(getRouterChild(router, 'ping', 'not-exist')).toSatisfy(isLazy) + expect(unlazy(getRouterChild(router, 'ping', 'not-exist'))).resolves.toEqual({ default: undefined }) + + expect(getRouterChild(router, 'nested', 'pong')).toSatisfy(isLazy) + expect(unlazy(getRouterChild(router, 'nested', 'pong'))).resolves.toEqual({ default: pong }) + + expect(getRouterChild(router, 'not-exist', 'not-exist')).toEqual(undefined) + expect(getRouterChild(router, 'nested', 'not-exist', 'not-exist')).toSatisfy(isLazy) + expect(unlazy(getRouterChild(router, 'nested', 'not-exist', 'not-exist'))).resolves.toEqual({ default: undefined }) }) diff --git a/packages/server/src/router.ts b/packages/server/src/router.ts index fa794d88..217bc895 100644 --- a/packages/server/src/router.ts +++ b/packages/server/src/router.ts @@ -1,54 +1,137 @@ -import type { ContractProcedure, ContractRouter, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { AnyContractRouter, ContractProcedure, ErrorMap, HTTPPath, MergedErrorMap, SchemaInput, SchemaOutput } from '@orpc/contract' import type { Context } from './context' -import type { ANY_LAZY, Lazy, Lazyable } from './lazy' -import type { ANY_PROCEDURE, Procedure } from './procedure' -import { flatLazy, isLazy, lazy, unlazy } from './lazy' -import { isProcedure } from './procedure' +import type { Lazy, Lazyable } from './lazy' +import type { AnyMiddleware } from './middleware' +import type { AnyProcedure } from './procedure' +import { adaptRoute, mergeErrorMap, mergePrefix } from '@orpc/contract' +import { deepSetLazyRouterPrefix, getLazyRouterPrefix } from './hidden' +import { isLazy, lazy, unlazy } from './lazy' +import { flatLazy } from './lazy-utils' +import { mergeMiddlewares } from './middleware-utils' +import { isProcedure, Procedure } from './procedure' +import { type AccessibleLazyRouter, createAccessibleLazyRouter } from './router-accessible-lazy' export type Router< TInitialContext extends Context, - TContract extends ContractRouter, + TContract extends AnyContractRouter, > = Lazyable< - TContract extends ContractProcedure - ? Procedure + TContract extends ContractProcedure + ? Procedure : { - [K in keyof TContract]: TContract[K] extends ContractRouter ? Router : never + [K in keyof TContract]: TContract[K] extends AnyContractRouter ? Router : never } > -export type RouterToContract> = - T extends Lazy> - ? RouterToContract - : T extends Procedure - ? ContractProcedure - : { - [K in keyof T]: T[K] extends Router ? RouterToContract : never - } +export type AnyRouter = Router -export type ANY_ROUTER = Router - -export type InferRouterInputs = - T extends Lazy ? InferRouterInputs +export type InferRouterInputs = + T extends Lazy ? InferRouterInputs : T extends Procedure ? SchemaInput : { - [K in keyof T]: T[K] extends ANY_ROUTER ? InferRouterInputs : never + [K in keyof T]: T[K] extends AnyRouter ? InferRouterInputs : never } -export type InferRouterOutputs = - T extends Lazy ? InferRouterOutputs +export type InferRouterOutputs = + T extends Lazy ? InferRouterOutputs : T extends Procedure ? SchemaOutput : { - [K in keyof T]: T[K] extends ANY_ROUTER ? InferRouterOutputs : never + [K in keyof T]: T[K] extends AnyRouter ? InferRouterOutputs : never } +export type AdaptedRouter< + TRouter extends AnyRouter, + TInitialContext extends Context, + TErrorMap extends ErrorMap, +> = TRouter extends Lazy + ? AccessibleLazyRouter> + : TRouter extends Procedure< + any, + infer UCurrentContext, + infer UInputSchema, + infer UOutputSchema, + infer UFuncOutput, + infer UErrorMap, + infer UMeta + > + ? Procedure< + TInitialContext, + UCurrentContext, + UInputSchema, + UOutputSchema, + UFuncOutput, + MergedErrorMap, + UMeta + > + : { + [K in keyof TRouter]: TRouter[K] extends AnyRouter ? AdaptedRouter : never + } + +export interface AdaptRouterOptions< TErrorMap extends ErrorMap> { + middlewares: AnyMiddleware[] + tags?: readonly string[] + prefix?: HTTPPath + errorMap: TErrorMap +} + +export function adaptRouter< + TRouter extends AnyRouter, + TInitialContext extends Context, + TErrorMap extends ErrorMap, +>( + router: TRouter, + options: AdaptRouterOptions, +): AdaptedRouter { + if (isLazy(router)) { + const adapted = lazy(async () => { + const unlaziedRouter = (await unlazy(router)).default + const adapted = adaptRouter(unlaziedRouter, options) + return { default: adapted } + }) + + const accessible = createAccessibleLazyRouter(adapted) + + const currentPrefix = getLazyRouterPrefix(router) + const prefix = currentPrefix ? mergePrefix(options.prefix, currentPrefix) : options.prefix + + if (prefix) { + return deepSetLazyRouterPrefix(accessible, prefix) as any + } + + return accessible as any + } + + if (isProcedure(router)) { + const newMiddlewares = mergeMiddlewares(options.middlewares, router['~orpc'].middlewares) + const newMiddlewareAdded = newMiddlewares.length - router['~orpc'].middlewares.length + + const adapted = new Procedure({ + ...router['~orpc'], + route: adaptRoute(router['~orpc'].route, options), + errorMap: mergeErrorMap(options.errorMap, router['~orpc'].errorMap), + middlewares: newMiddlewares, + inputValidationIndex: router['~orpc'].inputValidationIndex + newMiddlewareAdded, + outputValidationIndex: router['~orpc'].outputValidationIndex + newMiddlewareAdded, + }) + + return adapted as any + } + + const adapted = {} as Record + for (const key in router) { + adapted[key] = adaptRouter(router[key]!, options) + } + + return adapted as any +} + export function getRouterChild< - T extends ANY_ROUTER | Lazy, ->(router: T, ...path: string[]): T extends ANY_LAZY - ? Lazy | Lazy> | Lazy - : ANY_ROUTER | Lazy | undefined { - let current: ANY_ROUTER | Lazy | undefined = router + T extends AnyRouter | Lazy, +>(router: T, ...path: string[]): T extends Lazy + ? Lazy | Lazy> | Lazy + : AnyRouter | Lazy | undefined { + let current: AnyRouter | Lazy | undefined = router for (let i = 0; i < path.length; i++) { const segment = path[i]! diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts deleted file mode 100644 index 8467051e..00000000 --- a/packages/server/src/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { FindGlobalInstanceType } from '@orpc/shared' -import type { ANY_PROCEDURE } from './procedure' - -export type AbortSignal = FindGlobalInstanceType<'AbortSignal'> - -export interface WithSignal { - signal?: AbortSignal -} - -export interface Meta extends WithSignal { - path: string[] - procedure: ANY_PROCEDURE -} diff --git a/packages/server/tests/shared.ts b/packages/server/tests/shared.ts new file mode 100644 index 00000000..14dc031f --- /dev/null +++ b/packages/server/tests/shared.ts @@ -0,0 +1,56 @@ +import type { Meta } from '@orpc/contract' +import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../../contract/tests/shared' +import type { Context } from '../src' +import { ping as pingContract, pong as pongContract } from '../../contract/tests/shared' +import { lazy, Procedure } from '../src' + +export type InitialContext = { db: string } +export type CurrentContext = InitialContext & { auth: boolean } + +export const pingHandler = vi.fn() +export const pingMiddleware = vi.fn(({ next }) => next()) + +export const ping = new Procedure< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + { output: number }, + typeof baseErrorMap, + BaseMeta +>({ + ...pingContract['~orpc'], + middlewares: [pingMiddleware], + handler: pingHandler, + inputValidationIndex: 1, + outputValidationIndex: 1, +}) + +export const pongHandler = vi.fn() + +export const pong = new Procedure< + Context, + Context, + undefined, + undefined, + unknown, + Record, + Meta +>({ + ...pongContract['~orpc'], + middlewares: [], + handler: pongHandler, + inputValidationIndex: 0, + outputValidationIndex: 0, +}) + +export const router = { + ping: lazy(() => Promise.resolve({ default: ping })), + pong, + nested: lazy(() => Promise.resolve({ + default: { + ping, + pong: lazy(() => Promise.resolve({ default: pong })), + }, + })), +} diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index ec96f8ab..486a1492 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.lib.json", "compilerOptions": { - "lib": ["ES2022", "DOM"], + "lib": ["ES2022"], "types": [] }, "references": [ diff --git a/packages/shared/src/chain.ts b/packages/shared/src/chain.ts new file mode 100644 index 00000000..c11d2a74 --- /dev/null +++ b/packages/shared/src/chain.ts @@ -0,0 +1,7 @@ +import type { AnyFunction } from './function' + +export type OmitChainMethodDeep = { + [P in keyof Omit]: T[P] extends AnyFunction + ? ((...args: Parameters) => OmitChainMethodDeep, K>) + : T[P] +} diff --git a/packages/shared/src/hook.ts b/packages/shared/src/hook.ts index dc0ad40a..4536e748 100644 --- a/packages/shared/src/hook.ts +++ b/packages/shared/src/hook.ts @@ -6,7 +6,7 @@ export type OnSuccessState = { status: 'success', input: TInput export type OnErrorState = { status: 'error', input: TInput, output: undefined, error: Error } export interface BaseHookMeta { - next: () => Promise + next(): Promise } export interface Hooks & { next?: never }) | undefined> { diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 9eaa3100..73f3c423 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,3 +1,4 @@ +export * from './chain' export * from './constants' export * from './error' export * from './function' diff --git a/packages/vue-colada/src/utils-general.ts b/packages/vue-colada/src/utils-general.ts index 53ca4bd7..8644b0f6 100644 --- a/packages/vue-colada/src/utils-general.ts +++ b/packages/vue-colada/src/utils-general.ts @@ -8,7 +8,7 @@ import { deepUnref } from './utils' * Utils at any level (procedure or router) */ export interface GeneralUtils { - key: (options?: MaybeDeepRef>) => EntryKey + key(options?: MaybeDeepRef>): EntryKey } export function createGeneralUtils( diff --git a/packages/vue-colada/src/utils-procedure.ts b/packages/vue-colada/src/utils-procedure.ts index 39944110..e65df8fe 100644 --- a/packages/vue-colada/src/utils-procedure.ts +++ b/packages/vue-colada/src/utils-procedure.ts @@ -8,13 +8,13 @@ import { deepUnref } from './utils' * Utils at procedure level */ export interface ProcedureUtils { - queryOptions: >( + queryOptions>( ...opt: [options: U] | (undefined extends TInput & TClientContext ? [] : never) - ) => QueryOptions + ): QueryOptions - mutationOptions: >( + mutationOptions>( ...opt: [options: U] | (undefined extends TClientContext ? [] : never) - ) => MutationOptions + ): MutationOptions } export function createProcedureUtils( diff --git a/packages/vue-colada/src/utils-router.test-d.ts b/packages/vue-colada/src/utils-router.test-d.ts index 7fdf9cce..b0014ffa 100644 --- a/packages/vue-colada/src/utils-router.test-d.ts +++ b/packages/vue-colada/src/utils-router.test-d.ts @@ -3,7 +3,7 @@ import type { RouterClient } from '@orpc/server' import type { GeneralUtils } from './utils-general' import type { ProcedureUtils } from './utils-procedure' import { oc } from '@orpc/contract' -import { os } from '@orpc/server' +import { implement, os } from '@orpc/server' import { z } from 'zod' import { createGeneralUtils } from './utils-general' import { createProcedureUtils } from './utils-procedure' @@ -16,10 +16,10 @@ const contractRouter = oc.router({ pong: pongContract, }) -const ping = os.contract(pingContract).handler(({ input }) => `ping ${input.name}`).callable() -const pong = os.contract(pongContract).handler(num => `pong ${num}`).callable() +const ping = implement(pingContract).handler(({ input }) => `ping ${input.name}`).callable() +const pong = implement(pongContract).handler(num => `pong ${num}`).callable() -const router = os.contract(contractRouter).router({ +const router = implement(contractRouter).router({ ping, pong: os.lazy(() => Promise.resolve({ default: pong })), }) diff --git a/packages/vue-query/src/types.ts b/packages/vue-query/src/types.ts index 28ca31a2..f9362166 100644 --- a/packages/vue-query/src/types.ts +++ b/packages/vue-query/src/types.ts @@ -31,8 +31,8 @@ export type QueryOptionsExtra { queryKey: ComputedRef - queryFn: (ctx: QueryFunctionContext) => Promise - retry?: (failureCount: number, error: TError) => boolean // this help tanstack can infer TError + queryFn(ctx: QueryFunctionContext): Promise + retry?(failureCount: number, error: TError): boolean // this help tanstack can infer TError } export type InfiniteOptionsExtra = @@ -45,9 +45,9 @@ export type InfiniteOptionsExtra { queryKey: ComputedRef - queryFn: (ctx: QueryFunctionContext>) => Promise + queryFn(ctx: QueryFunctionContext>): Promise initialPageParam: undefined - retry?: (failureCount: number, error: TError) => boolean // this help tanstack can infer TError + retry?(failureCount: number, error: TError): boolean // this help tanstack can infer TError } export type MutationOptionsExtra = @@ -61,6 +61,6 @@ export type MutationOptionsExtra { mutationKey: QueryKey - mutationFn: (input: TInput) => Promise - retry?: (failureCount: number, error: TError) => boolean // this help tanstack can infer TError + mutationFn(input: TInput): Promise + retry?(failureCount: number, error: TError): boolean // this help tanstack can infer TError } diff --git a/packages/vue-query/src/utils-general.ts b/packages/vue-query/src/utils-general.ts index cc667594..72db24eb 100644 --- a/packages/vue-query/src/utils-general.ts +++ b/packages/vue-query/src/utils-general.ts @@ -8,7 +8,7 @@ import { deepUnref } from './utils' * Utils at any level (procedure or router) */ export interface GeneralUtils { - key: (options?: MaybeDeepRef>) => QueryKey + key(options?: MaybeDeepRef>): QueryKey } export function createGeneralUtils( diff --git a/packages/vue-query/src/utils-procedure.ts b/packages/vue-query/src/utils-procedure.ts index 34f2a91f..04363976 100644 --- a/packages/vue-query/src/utils-procedure.ts +++ b/packages/vue-query/src/utils-procedure.ts @@ -9,19 +9,19 @@ import { deepUnref } from './utils' * Utils at procedure level */ export interface ProcedureUtils { - queryOptions: >( + queryOptions>( ...opt: [options: U] | (undefined extends TInput & TClientContext ? [] : never) - ) => IsEqual> extends true + ): IsEqual> extends true ? QueryOptionsBase : Omit, keyof U> & U - infiniteOptions: >( + infiniteOptions>( options: U - ) => Omit, keyof U> & U + ): Omit, keyof U> & U - mutationOptions: >( + mutationOptions>( ...opt: [options: U] | (undefined extends TClientContext ? [] : never) - ) => IsEqual> extends true + ): IsEqual> extends true ? MutationOptionsBase : Omit, keyof U> & U } diff --git a/packages/vue-query/src/utils-router.test-d.ts b/packages/vue-query/src/utils-router.test-d.ts index 0aab1997..5624f120 100644 --- a/packages/vue-query/src/utils-router.test-d.ts +++ b/packages/vue-query/src/utils-router.test-d.ts @@ -3,7 +3,7 @@ import type { RouterClient } from '@orpc/server' import type { GeneralUtils } from './utils-general' import type { ProcedureUtils } from './utils-procedure' import { oc } from '@orpc/contract' -import { os } from '@orpc/server' +import { implement, os } from '@orpc/server' import { z } from 'zod' import { createGeneralUtils } from './utils-general' import { createProcedureUtils } from './utils-procedure' @@ -16,10 +16,10 @@ const contractRouter = oc.router({ pong: pongContract, }) -const ping = os.contract(pingContract).handler(({ input }) => `ping ${input.name}`).callable() -const pong = os.contract(pongContract).handler(num => `pong ${num}`).callable() +const ping = implement(pingContract).handler(({ input }) => `ping ${input.name}`).callable() +const pong = implement(pongContract).handler(num => `pong ${num}`).callable() -const router = os.contract(contractRouter).router({ +const router = implement(contractRouter).router({ ping, pong: os.lazy(() => Promise.resolve({ default: pong })), }) diff --git a/packages/zod/src/schemas.ts b/packages/zod/src/schemas.ts index d59f49d7..dcbfd76c 100644 --- a/packages/zod/src/schemas.ts +++ b/packages/zod/src/schemas.ts @@ -98,10 +98,10 @@ function composeParams(options: { export function file( params?: string | CustomParams | ((input: unknown) => CustomParams), ): ZodType, ZodTypeDef, InstanceType> & { - type: ( + type( mimeType: string, params?: string | CustomParams | ((input: unknown) => CustomParams), - ) => ZodEffects< + ): ZodEffects< ZodType, ZodTypeDef, InstanceType>, InstanceType, InstanceType diff --git a/playgrounds/contract-openapi/src/orpc.ts b/playgrounds/contract-openapi/src/orpc.ts index 85ca296e..1cb5e682 100644 --- a/playgrounds/contract-openapi/src/orpc.ts +++ b/playgrounds/contract-openapi/src/orpc.ts @@ -1,6 +1,6 @@ import type { z } from 'zod' import type { UserSchema } from './schemas/user' -import { ORPCError, os } from '@orpc/server' +import { implement, ORPCError } from '@orpc/server' import { contract } from './contract' export interface ORPCContext { @@ -8,7 +8,7 @@ export interface ORPCContext { db?: any } -const base = os.context() +const base = implement(contract).$context() const logMid = base.middleware(async ({ context, path, next }, input) => { const start = Date.now() @@ -24,9 +24,7 @@ const logMid = base.middleware(async ({ context, path, next }, input) => { const authMid = base.middleware(({ context, next, path }, input) => { if (!context.user) { - throw new ORPCError({ - code: 'UNAUTHORIZED', - }) + throw new ORPCError('UNAUTHORIZED') } return next({ @@ -36,5 +34,5 @@ const authMid = base.middleware(({ context, next, path }, input) => { }) }) -export const pub = base.use(logMid).contract(contract) -export const authed = base.use(logMid).use(authMid).contract(contract) +export const pub = base.use(logMid) +export const authed = base.use(logMid).use(authMid) diff --git a/playgrounds/contract-openapi/src/router/planet.ts b/playgrounds/contract-openapi/src/router/planet.ts index 421a1cf0..37e9a954 100644 --- a/playgrounds/contract-openapi/src/router/planet.ts +++ b/playgrounds/contract-openapi/src/router/planet.ts @@ -31,8 +31,7 @@ export const findPlanet = pub.planet.find.handler( const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -46,8 +45,7 @@ export const updatePlanet = authed.planet.update.handler( const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -65,8 +63,7 @@ export const updatePlanetImage = authed.planet.updateImage.handler( const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -82,8 +79,7 @@ export const deletePlanet = authed.planet.delete.handler( const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } diff --git a/playgrounds/expressjs/src/orpc.ts b/playgrounds/expressjs/src/orpc.ts index 2a95254c..0c20898d 100644 --- a/playgrounds/expressjs/src/orpc.ts +++ b/playgrounds/expressjs/src/orpc.ts @@ -7,23 +7,23 @@ export interface ORPCContext { db?: any } -export const pub = os.context().use(async ({ context, path, next }, input) => { - const start = Date.now() +export const pub = os + .$context() + .use(async ({ context, path, next }, input) => { + const start = Date.now() - try { - return await next({}) - } - finally { + try { + return await next({}) + } + finally { // eslint-disable-next-line no-console - console.log(`[${path.join('/')}] ${Date.now() - start}ms`) - } -}) + console.log(`[${path.join('/')}] ${Date.now() - start}ms`) + } + }) export const authed = pub.use(({ context, path, next }, input) => { if (!context.user) { - throw new ORPCError({ - code: 'UNAUTHORIZED', - }) + throw new ORPCError('UNAUTHORIZED') } return next({ diff --git a/playgrounds/expressjs/src/router/planet.ts b/playgrounds/expressjs/src/router/planet.ts index dfa3a792..80826a18 100644 --- a/playgrounds/expressjs/src/router/planet.ts +++ b/playgrounds/expressjs/src/router/planet.ts @@ -66,8 +66,7 @@ export const findPlanet = pub const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -87,8 +86,7 @@ export const updatePlanet = authed const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -117,8 +115,7 @@ export const updatePlanetImage = authed const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -144,8 +141,7 @@ export const deletePlanet = authed const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } diff --git a/playgrounds/nextjs/src/orpc.ts b/playgrounds/nextjs/src/orpc.ts index 9ef81323..4b2b4155 100644 --- a/playgrounds/nextjs/src/orpc.ts +++ b/playgrounds/nextjs/src/orpc.ts @@ -41,9 +41,7 @@ export const pub = base export const authed = pub.use(async ({ context, path, next }, input) => { if (!context.user) { - throw new ORPCError({ - code: 'UNAUTHORIZED', - }) + throw new ORPCError('UNAUTHORIZED') } return next({ diff --git a/playgrounds/nextjs/src/router/planet.ts b/playgrounds/nextjs/src/router/planet.ts index dfa3a792..80826a18 100644 --- a/playgrounds/nextjs/src/router/planet.ts +++ b/playgrounds/nextjs/src/router/planet.ts @@ -66,8 +66,7 @@ export const findPlanet = pub const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -87,8 +86,7 @@ export const updatePlanet = authed const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -117,8 +115,7 @@ export const updatePlanetImage = authed const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -144,8 +141,7 @@ export const deletePlanet = authed const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } diff --git a/playgrounds/nuxt/server/orpc.ts b/playgrounds/nuxt/server/orpc.ts index 2a95254c..0c20898d 100644 --- a/playgrounds/nuxt/server/orpc.ts +++ b/playgrounds/nuxt/server/orpc.ts @@ -7,23 +7,23 @@ export interface ORPCContext { db?: any } -export const pub = os.context().use(async ({ context, path, next }, input) => { - const start = Date.now() +export const pub = os + .$context() + .use(async ({ context, path, next }, input) => { + const start = Date.now() - try { - return await next({}) - } - finally { + try { + return await next({}) + } + finally { // eslint-disable-next-line no-console - console.log(`[${path.join('/')}] ${Date.now() - start}ms`) - } -}) + console.log(`[${path.join('/')}] ${Date.now() - start}ms`) + } + }) export const authed = pub.use(({ context, path, next }, input) => { if (!context.user) { - throw new ORPCError({ - code: 'UNAUTHORIZED', - }) + throw new ORPCError('UNAUTHORIZED') } return next({ diff --git a/playgrounds/nuxt/server/router/planet.ts b/playgrounds/nuxt/server/router/planet.ts index dfa3a792..80826a18 100644 --- a/playgrounds/nuxt/server/router/planet.ts +++ b/playgrounds/nuxt/server/router/planet.ts @@ -66,8 +66,7 @@ export const findPlanet = pub const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -87,8 +86,7 @@ export const updatePlanet = authed const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -117,8 +115,7 @@ export const updatePlanetImage = authed const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -144,8 +141,7 @@ export const deletePlanet = authed const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } diff --git a/playgrounds/openapi/src/orpc.ts b/playgrounds/openapi/src/orpc.ts index 2a95254c..517a442c 100644 --- a/playgrounds/openapi/src/orpc.ts +++ b/playgrounds/openapi/src/orpc.ts @@ -7,28 +7,29 @@ export interface ORPCContext { db?: any } -export const pub = os.context().use(async ({ context, path, next }, input) => { - const start = Date.now() +export const pub = os + .$context() + .use(async ({ context, path, next }, input) => { + const start = Date.now() - try { - return await next({}) - } - finally { + try { + return await next({}) + } + finally { // eslint-disable-next-line no-console - console.log(`[${path.join('/')}] ${Date.now() - start}ms`) - } -}) + console.log(`[${path.join('/')}] ${Date.now() - start}ms`) + } + }) -export const authed = pub.use(({ context, path, next }, input) => { - if (!context.user) { - throw new ORPCError({ - code: 'UNAUTHORIZED', - }) - } +export const authed = pub + .use(({ context, path, next }, input) => { + if (!context.user) { + throw new ORPCError('UNAUTHORIZED') + } - return next({ - context: { - user: context.user, - }, + return next({ + context: { + user: context.user, + }, + }) }) -}) diff --git a/playgrounds/openapi/src/router/planet.ts b/playgrounds/openapi/src/router/planet.ts index dfa3a792..80826a18 100644 --- a/playgrounds/openapi/src/router/planet.ts +++ b/playgrounds/openapi/src/router/planet.ts @@ -66,8 +66,7 @@ export const findPlanet = pub const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -87,8 +86,7 @@ export const updatePlanet = authed const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -117,8 +115,7 @@ export const updatePlanetImage = authed const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } @@ -144,8 +141,7 @@ export const deletePlanet = authed const planet = planets.find(planet => planet.id === input.id) if (!planet) { - throw new ORPCError({ - code: 'NOT_FOUND', + throw new ORPCError('NOT_FOUND', { message: 'Planet not found', }) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e6eba2f..04e8be5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^3.9.2 - version: 3.12.1(@eslint-react/eslint-plugin@1.22.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@typescript-eslint/utils@8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.3(eslint@9.17.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.1.0(eslint@9.17.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.16(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0)) + version: 3.12.1(@eslint-react/eslint-plugin@1.22.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@typescript-eslint/utils@8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.3(eslint@9.17.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.1.0(eslint@9.17.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.16(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)) '@eslint-react/eslint-plugin': specifier: ^1.16.2 version: 1.22.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2) @@ -24,8 +24,8 @@ importers: specifier: ^22.9.0 version: 22.10.2 '@vitest/coverage-v8': - specifier: ^2.1.1 - version: 2.1.8(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0)) + specifier: ^3.0.4 + version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)) eslint: specifier: ^9.15.0 version: 9.17.0(jiti@2.4.2) @@ -54,8 +54,8 @@ importers: specifier: 5.7.2 version: 5.7.2 vitest: - specifier: ^2.1.8 - version: 2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0) + specifier: ^3.0.4 + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1) apps/content: dependencies: @@ -176,8 +176,8 @@ importers: specifier: workspace:* version: link:../shared '@standard-schema/spec': - specifier: 1.0.0-beta.4 - version: 1.0.0-beta.4 + specifier: 1.0.0-rc.0 + version: 1.0.0-rc.0 devDependencies: arktype: specifier: 2.0.0-rc.26 @@ -750,8 +750,9 @@ packages: resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} engines: {node: '>=6.9.0'} - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} '@clack/core@0.4.0': resolution: {integrity: sha512-YJCYBsyJfNDaTbvDUVSJ3SgSuPrcujarRgkJ5NLjexDZKvaOiVVJvAQYx8lIgG0qRT8ff0fPgqyBCVivanIZ+A==} @@ -2380,6 +2381,9 @@ packages: '@standard-schema/spec@1.0.0-beta.4': resolution: {integrity: sha512-d3IxtzLo7P1oZ8s8YNvxzBUXRXojSut8pbPrTYtzsc5sn4+53jVqbk66pQerSZbZSJZQux6LkclB/+8IDordHg==} + '@standard-schema/spec@1.0.0-rc.0': + resolution: {integrity: sha512-DcY/ICFcZIESNTLTexIT108HOqd1FtxsiLV4ZYGluySWyjF6TZ6troNyXjiqoHU6j0wN3A6SmYnTA5CHQp9blw==} + '@stylistic/eslint-plugin@2.12.1': resolution: {integrity: sha512-fubZKIHSPuo07FgRTn6S4Nl0uXPRPYVNpyZzIDGfp7Fny6JjNus6kReLD7NI380JXi4HtUTSOZ34LBuNPO1XLQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2651,11 +2655,11 @@ packages: vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 - '@vitest/coverage-v8@2.1.8': - resolution: {integrity: sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==} + '@vitest/coverage-v8@3.0.4': + resolution: {integrity: sha512-f0twgRCHgbs24Dp8cLWagzcObXMcuKtAwgxjJV/nnysPAJJk1JiKu/W0gIehZLmkljhJXU/E0/dmuQzsA/4jhA==} peerDependencies: - '@vitest/browser': 2.1.8 - vitest: 2.1.8 + '@vitest/browser': 3.0.4 + vitest: 3.0.4 peerDependenciesMeta: '@vitest/browser': optional: true @@ -2673,34 +2677,34 @@ packages: vitest: optional: true - '@vitest/expect@2.1.8': - resolution: {integrity: sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==} + '@vitest/expect@3.0.4': + resolution: {integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==} - '@vitest/mocker@2.1.8': - resolution: {integrity: sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==} + '@vitest/mocker@3.0.4': + resolution: {integrity: sha512-gEef35vKafJlfQbnyOXZ0Gcr9IBUsMTyTLXsEQwuyYAerpHqvXhzdBnDFuHLpFqth3F7b6BaFr4qV/Cs1ULx5A==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 + vite: ^5.0.0 || ^6.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@2.1.8': - resolution: {integrity: sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==} + '@vitest/pretty-format@3.0.4': + resolution: {integrity: sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==} - '@vitest/runner@2.1.8': - resolution: {integrity: sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==} + '@vitest/runner@3.0.4': + resolution: {integrity: sha512-dKHzTQ7n9sExAcWH/0sh1elVgwc7OJ2lMOBrAm73J7AH6Pf9T12Zh3lNE1TETZaqrWFXtLlx3NVrLRb5hCK+iw==} - '@vitest/snapshot@2.1.8': - resolution: {integrity: sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==} + '@vitest/snapshot@3.0.4': + resolution: {integrity: sha512-+p5knMLwIk7lTQkM3NonZ9zBewzVp9EVkVpvNta0/PlFWpiqLaRcF4+33L1it3uRUCh0BGLOaXPPGEjNKfWb4w==} - '@vitest/spy@2.1.8': - resolution: {integrity: sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==} + '@vitest/spy@3.0.4': + resolution: {integrity: sha512-sXIMF0oauYyUy2hN49VFTYodzEAu744MmGcPR3ZBsPM20G+1/cSW/n1U+3Yu/zHxX2bIDe1oJASOkml+osTU6Q==} - '@vitest/utils@2.1.8': - resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==} + '@vitest/utils@3.0.4': + resolution: {integrity: sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==} '@vue-macros/common@1.15.1': resolution: {integrity: sha512-O0ZXaladWXwHplQnSjxLbB/G1KpdWCUNJPNYVHIxHonGex1BGpoB4fBZZLgddHgAiy18VZG/Iu5L0kwG+SV7JQ==} @@ -5458,6 +5462,9 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.2: + resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} + pathval@2.0.0: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} @@ -6432,6 +6439,9 @@ packages: tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.10: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} @@ -6440,8 +6450,8 @@ packages: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} - tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} tinyspy@3.0.2: @@ -6815,6 +6825,11 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-node@3.0.4: + resolution: {integrity: sha512-7JZKEzcYV2Nx3u6rlvN8qdo3QV7Fxyt6hx+CCKz9fbWxdX5IvUOmTWEAxMrWxaiSf7CKGLJQ5rFu8prb/jBjOA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite-plugin-checker@0.8.0: resolution: {integrity: sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g==} engines: {node: '>=14.16'} @@ -6935,20 +6950,23 @@ packages: yaml: optional: true - vitest@2.1.8: - resolution: {integrity: sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==} - engines: {node: ^18.0.0 || >=20.0.0} + vitest@3.0.4: + resolution: {integrity: sha512-6XG8oTKy2gnJIFTHP6LD7ExFeNLxiTkK3CfMvT7IfR8IN+BYICCf0lXUQmX7i7JoxUP8QmeP4mTnWXgflu4yjw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.8 - '@vitest/ui': 2.1.8 + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.0.4 + '@vitest/ui': 3.0.4 happy-dom: '*' jsdom: '*' peerDependenciesMeta: '@edge-runtime/vm': optional: true + '@types/debug': + optional: true '@types/node': optional: true '@vitest/browser': @@ -7180,7 +7198,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/eslint-config@3.12.1(@eslint-react/eslint-plugin@1.22.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@typescript-eslint/utils@8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.3(eslint@9.17.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.1.0(eslint@9.17.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.16(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0))': + '@antfu/eslint-config@3.12.1(@eslint-react/eslint-plugin@1.22.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@typescript-eslint/utils@8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.3(eslint@9.17.0(jiti@2.4.2)))(eslint-plugin-react-hooks@5.1.0(eslint@9.17.0(jiti@2.4.2)))(eslint-plugin-react-refresh@0.4.16(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))': dependencies: '@antfu/install-pkg': 0.5.0 '@clack/prompts': 0.9.0 @@ -7189,7 +7207,7 @@ snapshots: '@stylistic/eslint-plugin': 2.12.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2) '@typescript-eslint/eslint-plugin': 8.18.2(@typescript-eslint/parser@8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2) '@typescript-eslint/parser': 8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2) - '@vitest/eslint-plugin': 1.1.20(@typescript-eslint/utils@8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0)) + '@vitest/eslint-plugin': 1.1.20(@typescript-eslint/utils@8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)) eslint: 9.17.0(jiti@2.4.2) eslint-config-flat-gitignore: 0.3.0(eslint@9.17.0(jiti@2.4.2)) eslint-flat-config-utils: 0.4.0 @@ -7441,7 +7459,7 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@bcoe/v8-coverage@0.2.3': {} + '@bcoe/v8-coverage@1.0.2': {} '@clack/core@0.4.0': dependencies: @@ -9012,6 +9030,8 @@ snapshots: '@standard-schema/spec@1.0.0-beta.4': {} + '@standard-schema/spec@1.0.0-rc.0': {} + '@stylistic/eslint-plugin@2.12.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)': dependencies: '@typescript-eslint/utils': 8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2) @@ -9369,10 +9389,10 @@ snapshots: vite: 6.0.6(@types/node@22.10.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1) vue: 3.5.13(typescript@5.7.2) - '@vitest/coverage-v8@2.1.8(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0))': + '@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))': dependencies: '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 0.2.3 + '@bcoe/v8-coverage': 1.0.2 debug: 4.4.0(supports-color@9.4.0) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -9382,58 +9402,58 @@ snapshots: magicast: 0.3.5 std-env: 3.8.0 test-exclude: 7.0.1 - tinyrainbow: 1.2.0 - vitest: 2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0) + tinyrainbow: 2.0.0 + vitest: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1) transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.1.20(@typescript-eslint/utils@8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0))': + '@vitest/eslint-plugin@1.1.20(@typescript-eslint/utils@8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))': dependencies: '@typescript-eslint/utils': 8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2) eslint: 9.17.0(jiti@2.4.2) optionalDependencies: typescript: 5.7.2 - vitest: 2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0) + vitest: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1) - '@vitest/expect@2.1.8': + '@vitest/expect@3.0.4': dependencies: - '@vitest/spy': 2.1.8 - '@vitest/utils': 2.1.8 + '@vitest/spy': 3.0.4 + '@vitest/utils': 3.0.4 chai: 5.1.2 - tinyrainbow: 1.2.0 + tinyrainbow: 2.0.0 - '@vitest/mocker@2.1.8(vite@5.4.11(@types/node@22.10.2)(terser@5.37.0))': + '@vitest/mocker@3.0.4(vite@6.0.6(@types/node@22.10.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))': dependencies: - '@vitest/spy': 2.1.8 + '@vitest/spy': 3.0.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.11(@types/node@22.10.2)(terser@5.37.0) + vite: 6.0.6(@types/node@22.10.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1) - '@vitest/pretty-format@2.1.8': + '@vitest/pretty-format@3.0.4': dependencies: - tinyrainbow: 1.2.0 + tinyrainbow: 2.0.0 - '@vitest/runner@2.1.8': + '@vitest/runner@3.0.4': dependencies: - '@vitest/utils': 2.1.8 - pathe: 1.1.2 + '@vitest/utils': 3.0.4 + pathe: 2.0.2 - '@vitest/snapshot@2.1.8': + '@vitest/snapshot@3.0.4': dependencies: - '@vitest/pretty-format': 2.1.8 + '@vitest/pretty-format': 3.0.4 magic-string: 0.30.17 - pathe: 1.1.2 + pathe: 2.0.2 - '@vitest/spy@2.1.8': + '@vitest/spy@3.0.4': dependencies: tinyspy: 3.0.2 - '@vitest/utils@2.1.8': + '@vitest/utils@3.0.4': dependencies: - '@vitest/pretty-format': 2.1.8 + '@vitest/pretty-format': 3.0.4 loupe: 3.1.2 - tinyrainbow: 1.2.0 + tinyrainbow: 2.0.0 '@vue-macros/common@1.15.1(rollup@4.29.1)(vue@3.5.13(typescript@5.7.2))': dependencies: @@ -13029,6 +13049,8 @@ snapshots: pathe@1.1.2: {} + pathe@2.0.2: {} + pathval@2.0.0: {} perfect-debounce@1.0.0: {} @@ -14106,6 +14128,8 @@ snapshots: tinyexec@0.3.1: {} + tinyexec@0.3.2: {} + tinyglobby@0.2.10: dependencies: fdir: 6.4.2(picomatch@4.0.2) @@ -14113,7 +14137,7 @@ snapshots: tinypool@1.0.2: {} - tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} tinyspy@3.0.2: {} @@ -14495,6 +14519,27 @@ snapshots: - supports-color - terser + vite-node@3.0.4(@types/node@22.10.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1): + dependencies: + cac: 6.7.14 + debug: 4.4.0(supports-color@9.4.0) + es-module-lexer: 1.6.0 + pathe: 2.0.2 + vite: 6.0.6(@types/node@22.10.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-plugin-checker@0.8.0(eslint@9.17.0(jiti@2.4.2))(optionator@0.9.4)(typescript@5.7.2)(vite@6.0.6(@types/node@22.10.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)): dependencies: '@babel/code-frame': 7.26.2 @@ -14573,32 +14618,34 @@ snapshots: tsx: 4.19.2 yaml: 2.6.1 - vitest@2.1.8(@types/node@22.10.2)(jsdom@25.0.1)(terser@5.37.0): + vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1): dependencies: - '@vitest/expect': 2.1.8 - '@vitest/mocker': 2.1.8(vite@5.4.11(@types/node@22.10.2)(terser@5.37.0)) - '@vitest/pretty-format': 2.1.8 - '@vitest/runner': 2.1.8 - '@vitest/snapshot': 2.1.8 - '@vitest/spy': 2.1.8 - '@vitest/utils': 2.1.8 + '@vitest/expect': 3.0.4 + '@vitest/mocker': 3.0.4(vite@6.0.6(@types/node@22.10.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)) + '@vitest/pretty-format': 3.0.4 + '@vitest/runner': 3.0.4 + '@vitest/snapshot': 3.0.4 + '@vitest/spy': 3.0.4 + '@vitest/utils': 3.0.4 chai: 5.1.2 debug: 4.4.0(supports-color@9.4.0) expect-type: 1.1.0 magic-string: 0.30.17 - pathe: 1.1.2 + pathe: 2.0.2 std-env: 3.8.0 tinybench: 2.9.0 - tinyexec: 0.3.1 + tinyexec: 0.3.2 tinypool: 1.0.2 - tinyrainbow: 1.2.0 - vite: 5.4.11(@types/node@22.10.2)(terser@5.37.0) - vite-node: 2.1.8(@types/node@22.10.2)(terser@5.37.0) + tinyrainbow: 2.0.0 + vite: 6.0.6(@types/node@22.10.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1) + vite-node: 3.0.4(@types/node@22.10.2)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1) why-is-node-running: 2.3.0 optionalDependencies: + '@types/debug': 4.1.12 '@types/node': 22.10.2 jsdom: 25.0.1 transitivePeerDependencies: + - jiti - less - lightningcss - msw @@ -14608,6 +14655,8 @@ snapshots: - sugarss - supports-color - terser + - tsx + - yaml vscode-jsonrpc@6.0.0: {}