Repository: macstr1k3r/trpc-nestjs-adapter Branch: master Commit: 9ad795d74f41 Files: 29 Total size: 16.7 KB Directory structure: gitextract_k256s8ck/ ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── README.MD ├── example/ │ ├── README.md │ ├── app.module.ts │ ├── domain-b/ │ │ ├── a.module.ts │ │ └── a.service.ts │ ├── index.ts │ └── init-trpc.ts ├── package.json ├── src/ │ ├── attach-trpc-to-express-app.ts │ ├── build-nest-resolver.ts │ ├── build-trpc-nest-middleware.ts │ ├── index.ts │ ├── infer-context-type.type.ts │ ├── nest-resolver.type.ts │ ├── tokens.ts │ ├── trpc-module-options.type.ts │ └── trpc.module.ts ├── test/ │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── init-trpc.ts │ ├── main.ts │ └── request-scoped.service.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [package.json] indent_style = space indent_size = 2 [.prettierrc.json] printWidth = 140 indent_style = space indent_size =4 [*] insert_final_newline = true ================================================ FILE: .eslintrc.json ================================================ { "extends": ["airbnb-base", "airbnb-typescript/base", "prettier"], "parserOptions": { "project": "./tsconfig.json" }, "rules": { "max-len": ["error", { "code": 140 }], "arrow-body-style": "off", "import/prefer-default-export": "off", "class-methods-use-this": "off" } } ================================================ FILE: .gitignore ================================================ yarn-error.log node_modules dist .vscode ================================================ FILE: .prettierignore ================================================ # Ignore artifacts: build coverage dist node_modules ================================================ FILE: .prettierrc.js ================================================ module.exports = { $schema: "http://json.schemastore.org/prettierrc", trailingComma: "all", tabWidth: 4, singleQuote: false, quoteProps: "consistent", printWidth: 140, semi: true, overrides: [ { files: "src/**/*.ts", options: { parser: "typescript", }, }, ], }; ================================================ FILE: README.MD ================================================ # tRPC - Nest.JS Adapter - Allows you to use Trpc with Nest.JS. - Allows you to use request scoped Nest.JS providers - Allows you to use Nest.JS's great module system ## Feature support I don't use all of the features tRPC has :/ Both __queries__ and __mutations__ work. I __haven't__ tested subscriptions yet. Batching __doesn't__ work. Currently it't not possible to create multiple requests from 1 single HTTP request in Nest.JS (or it's a skill issue :) ) ## How See the `example` folder in this repo, but briefly ```bash yarn add trpc-nestjs-adapter ``` `main.ts` ``` ts // Standard nest.js main.ts import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap(); ``` `app.module.ts` ```ts import { Module } from '@nestjs/common'; import { TrpcModule } from 'trpc-nestjs-adapter'; import { rootRouter } from './trpc/root-trpc.router.ts'; import { createContext } from './trpc/create-context.ts'; @Module({ imports: [ AModule, TrpcModule.forRoot({ path: '/trpc', router: rootRouter, createContext, }), ], }) export class AppModule { } ``` ### Inside of your procedures ```ts export const exampleMutation = trpc.procedure .input() .mutation(async ({ ctx })=>{ const nestService = await ctx.resolveNestDependency(SomeNestService); await nestService.someServiceMethod() }) ``` #### Note The package is marked `alpha` for a reason, but mostly It's not documented very well. If/When/As the package gains traction I'll improve the example & related docs. ================================================ FILE: example/README.md ================================================ # Example This folder is meant to be an example of how to ues `trpc-nestjs-adapter` in your codebase. Not that you can't just copy-paste this out of this repo and into yours. At the moment, use it as more of an inspiration. If/When/As the package gains traction I'll improve the example & related docs. ================================================ FILE: example/app.module.ts ================================================ import { Module } from '@nestjs/common'; import { TrpcModule } from '../lib/trpc.module'; import { AModule } from './domain-b/a.module'; import { appRouter } from './init-trpc'; @Module({ imports: [ AModule, TrpcModule.forRoot({ path: '/trpc', router: appRouter, createContext: () => ({}), }), ], }) export class AppModule { } ================================================ FILE: example/domain-b/a.module.ts ================================================ import { Module } from '@nestjs/common'; import { AService } from './a.service'; @Module({ providers: [ AService, ], }) export class AModule { } ================================================ FILE: example/domain-b/a.service.ts ================================================ import { Injectable } from '@nestjs/common'; @Injectable() export class AService { smth() { return { a: true, }; } } ================================================ FILE: example/index.ts ================================================ import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap(); ================================================ FILE: example/init-trpc.ts ================================================ import { initTRPC } from '@trpc/server'; import { InferContextType } from '../lib/infer-context-type.type'; import { AService } from './domain-b/a.service'; // You can use any variable name you like. // We use t to keep things simple. type CtxType = InferContextType; const createContext = () => ({ someValueOnContext: 'randomValue', }); const trpc = initTRPC.context().create({ }); const { router } = trpc; const publicProcedure = trpc.procedure; export const appRouter = router({ something: publicProcedure.query(async ({ ctx }) => { const service = await ctx.resolveNestDependency(AService); console.log({ service }); return service.smth(); }), }); ================================================ FILE: package.json ================================================ { "name": "trpc-nestjs-adapter", "version": "1.0.0-alpha.11", "description": "TRPC adapter for NestJS", "keywords": [ "nestjs", "nest", "trpc", "adapter" ], "author": { "name": "Darko Stojkovski", "email": "dar3.st@gmail.com" }, "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", "exports": { ".": { "types": "./dist/lib/index.d.ts", "import": "./dist/lib/index.js", "require": "./dist/lib/index.js" }, "./package.json": "./package.json" }, "scripts": { "build": "tsc", "dev": "tsc -w", "prelint": "prettier --check src/", "lint": "eslint --ext .ts src/", "lint:fix": "prettier --write src/; eslint --ext .ts src/ --fix" }, "peerDependencies": { "@nestjs/common": "^9.2.1", "@nestjs/core": "^9.2.1", "@nestjs/platform-express": "^9.2.1", "@trpc/server": "~10.14.0", "reflect-metadata": "^0.1.12", "rxjs": "^7.1.0" }, "dependencies": {}, "devDependencies": { "@nestjs/common": "^9.2.1", "@nestjs/core": "^9.2.1", "@nestjs/platform-express": "^9.2.1", "@trpc/server": "~10.14.0", "@types/node": "^16.0.0", "@typescript-eslint/eslint-plugin": "^5.48.2", "@typescript-eslint/parser": "^5.48.2", "eslint": "^8.2.0", "eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-import": "^2.25.2", "nodemon": "^2.0.20", "prettier": "^3.0.3", "reflect-metadata": "^0.1.12", "rxjs": "^7.1.0", "typescript": "^4.9.4" }, "files": [ "dist" ] } ================================================ FILE: src/attach-trpc-to-express-app.ts ================================================ import { NestExpressApplication } from "@nestjs/platform-express"; import { TrpcModuleOptions } from "./trpc-module-options.type"; import { buildTrpcNestMiddleware, BuildTrpcNestMiddlewareOptions } from "./build-trpc-nest-middleware"; interface Options extends TrpcModuleOptions, BuildTrpcNestMiddlewareOptions { expressApp: NestExpressApplication; } /** * Attaches a TRPC router to your nestExpressApp * @param options: Options */ export function attachTrpcToExpressApp({ router, moduleRef, createContext, path, expressApp }: Options): void { const trpcNestMiddleware = buildTrpcNestMiddleware({ router, moduleRef, createContext, }); expressApp.use(path, trpcNestMiddleware); } ================================================ FILE: src/build-nest-resolver.ts ================================================ /* eslint-disable no-underscore-dangle */ import { ContextIdFactory, ModuleRef } from "@nestjs/core"; export function buildNestResolver(req: any, moduleRef: ModuleRef) { // Retrieve the contextId specific to this request let contextId = ContextIdFactory.getByRequest(req); // Effectively a provider for the `REQUEST` token moduleRef.registerRequestByContextId(req, contextId); const resolveNestDependency: ModuleRef["resolve"] = (typeOrToken) => { return moduleRef.resolve(typeOrToken, contextId, { strict: false }); }; // API Candidates // const requestScopedService = await ctx.nestResolver.resolve(RequestScopedService); // const requestScopedService = await ctx.nestResolver(RequestScopedService); // const requestScopedService = await ctx.nestResolve(RequestScopedService); // const requestScopedService = await ctx.resolveNestDependency(RequestScopedService); // const requestScopedService = await ctx.resolveNest(RequestScopedService); // const requestScopedService = await ctx.resolve(RequestScopedService); return { /** * Resolves any NestJS dependency from your project. * * Return type of this function is automatically inferred. * * Returns a promise which resolves to the dependency. */ resolveNestDependency, /** * * "resets" the DI subtree from which dependencies will be resolved. * * All subsequent calls to `resolveNestDependency` will resolve dependencies from the new subtree * `REQUEST` scoped dependencies will be re-created. * * use-case: testing */ resetDiSubtree: () => { contextId = ContextIdFactory.create(); moduleRef.registerRequestByContextId(req, contextId); }, /** * used to attach anything to the request object * * This is useful if you want to have something specific to the trpc request and later access it in a NestJS provider * * examples: req scoped prisma client, req scoped logger, etc... */ attachToReqObject: (_trpc_nest_adapter_meta: Record) => { req._trpc_nest_adapter_meta = _trpc_nest_adapter_meta; }, }; } ================================================ FILE: src/build-trpc-nest-middleware.ts ================================================ import { createHTTPHandler } from "@trpc/server/adapters/standalone"; import { ModuleRef } from "@nestjs/core"; import { AnyRouter } from "@trpc/server"; import { buildNestResolver } from "./build-nest-resolver"; export interface BuildTrpcNestMiddlewareOptions { /** Your TRPC Router */ router: AnyRouter; /** The NestJS ModuleRef */ moduleRef: ModuleRef; /** A function that returns the context object as used with TRPC */ createContext: () => any; } /** * Builds an Express middleware that handles all trpc requests. * * The middleware will add a `resolveNestDependency` property to the context * * `resolveNestDependency` is a function that can be used to resolve NestJS providers * * @param req Express request object * @param moduleRef The moduleRef from the NestJS app * @param createContext A function that returns the context object as used with TRPC * @returns Express middleware which is capable of handling trpc requests */ export function buildTrpcNestMiddleware({ moduleRef, router, createContext }: BuildTrpcNestMiddlewareOptions) { return function trpcNestMiddleware(req: any, res: any) { const { resolveNestDependency, attachToReqObject, resetDiSubtree } = buildNestResolver(req, moduleRef); return createHTTPHandler({ router, createContext: () => { const userProvidedContext = createContext(); return { ...userProvidedContext, resolveNestDependency, attachToReqObject, resetDiSubtree, }; }, })(req, res); }; } ================================================ FILE: src/index.ts ================================================ export { TrpcModule } from "./trpc.module"; export type { InferContextType } from "./infer-context-type.type"; ================================================ FILE: src/infer-context-type.type.ts ================================================ import { inferAsyncReturnType } from "@trpc/server"; import { NestResolver } from "./nest-resolver.type"; export type InferContextType = TContext extends () => any ? inferAsyncReturnType & NestResolver : NestResolver; ================================================ FILE: src/nest-resolver.type.ts ================================================ /* eslint-disable max-len */ interface Type extends Function { new (...args: any[]): T; } export interface NestResolver { /** * Resolves any NestJS dependency from your project. a proxy to `moduleRef.get()` * * Return type of this function is automatically inferred. * * Returns a promise which resolves to the dependency. */ resolveNestDependency: (typeOrToken: Type | Function | string | symbol) => Promise; attachToReqObject: (anything: Record) => void; resetDiSubtree: () => void; } ================================================ FILE: src/tokens.ts ================================================ export const TRPC_ROUTER_TOKEN = Symbol("TRPC_ROUTER_TOKEN"); export const TRPC_PATH_TOKEN = Symbol("TRPC_PATH_TOKEN"); export const TRPC_CREATE_CONTEXT_TOKEN = Symbol("TRPC_CREATE_CONTEXT_TOKEN"); ================================================ FILE: src/trpc-module-options.type.ts ================================================ import { AnyRouter } from "@trpc/server"; export interface TrpcModuleOptions { path: "/trpc" | string; router: TRouter; createContext: () => any; } ================================================ FILE: src/trpc.module.ts ================================================ import { DynamicModule, Inject, Module, OnModuleInit } from "@nestjs/common"; import { HttpAdapterHost, ModuleRef } from "@nestjs/core"; import { attachTrpcToExpressApp } from "./attach-trpc-to-express-app"; import { TRPC_CREATE_CONTEXT_TOKEN, TRPC_PATH_TOKEN, TRPC_ROUTER_TOKEN } from "./tokens"; import { TrpcModuleOptions } from "./trpc-module-options.type"; @Module({}) export class TrpcModule implements OnModuleInit { constructor(private moduleRef: ModuleRef) {} @Inject() private readonly httpAdapterHost!: HttpAdapterHost; @Inject(TRPC_ROUTER_TOKEN) private readonly router!: TrpcModuleOptions["router"]; @Inject(TRPC_PATH_TOKEN) private readonly path!: TrpcModuleOptions["path"]; @Inject(TRPC_CREATE_CONTEXT_TOKEN) private readonly createContext!: TrpcModuleOptions["createContext"]; static forRoot(options: TrpcModuleOptions): DynamicModule { if (!options.createContext || !options.path || !options.router) { throw new Error("Please supply all of the required options to TrpcModule"); } return { module: TrpcModule, providers: [ { provide: TRPC_ROUTER_TOKEN, useValue: options.router }, { provide: TRPC_PATH_TOKEN, useValue: options.path }, { provide: TRPC_CREATE_CONTEXT_TOKEN, useValue: options.createContext }, ], }; } onModuleInit() { attachTrpcToExpressApp({ moduleRef: this.moduleRef, expressApp: this.httpAdapterHost.httpAdapter.getInstance(), path: this.path, createContext: this.createContext, router: this.router, }); } } ================================================ FILE: test/app.controller.ts ================================================ import { Controller, Get } from '@nestjs/common'; @Controller() export class AppController { // eslint-disable-next-line class-methods-use-this @Get('/status') status() { return { status: 'ok' }; } } ================================================ FILE: test/app.module.ts ================================================ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { appRouter } from './init-trpc'; import { RequestScopedService } from './request-scoped.service'; import { TrpcModule } from '../lib/trpc.module'; import { AModule } from '../example/domain-b/a.module'; @Module({ controllers: [AppController], providers: [ RequestScopedService, ], imports: [ AModule, TrpcModule.forRoot({ path: '/trpc', router: appRouter, createContext: () => { 'randomValue'; }, }), ], }) export class AppModule { } ================================================ FILE: test/app.service.ts ================================================ import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { // eslint-disable-next-line class-methods-use-this doesSomething() { return { done: true, }; } } ================================================ FILE: test/init-trpc.ts ================================================ import { initTRPC } from '@trpc/server'; import { RequestScopedService } from './request-scoped.service'; import { InferContextType } from '../lib/infer-context-type.type'; // You can use any variable name you like. // We use t to keep things simple. type CtxType = InferContextType; const createContext = () => ({ someValueOnContext: 'randomValue', }); const trpc = initTRPC.context().create({ }); const { router } = trpc; const publicProcedure = trpc.procedure; export const appRouter = router({ something: publicProcedure.query(async ({ ctx }) => { const requestScopedService = await ctx.resolveNestDependency(RequestScopedService); return requestScopedService.doesSomething(); }), }); ================================================ FILE: test/main.ts ================================================ import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); console.log({ app }); await app.listen(3000); } bootstrap(); ================================================ FILE: test/request-scoped.service.ts ================================================ import { Inject, Injectable } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; @Injectable() export class RequestScopedService { @Inject(REQUEST) private readonly req: any; doesSomething() { return { done: true, isRequestScoped: true, hostHeader: this.req.headers.host, }; } } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es2020", "experimentalDecorators": true, "emitDecoratorMetadata": true, "module": "commonjs", "rootDir": "./src", "declaration": true, "declarationMap": true, "sourceMap": true, "outDir": "./dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true }, "include": ["src/**/*"] }