Repository: krislefeber/nestjs-dataloader Branch: master Commit: f3acaf5570d7 Files: 20 Total size: 17.5 KB Directory structure: gitextract_g1ppmali/ ├── .github/ │ └── workflows/ │ └── nodejs.yml ├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── example/ │ ├── __tests__/ │ │ └── app.e2e-spec.ts │ ├── src/ │ │ ├── account/ │ │ │ ├── account.entity.ts │ │ │ ├── account.loader.ts │ │ │ ├── account.module.ts │ │ │ ├── account.resolver.spec.ts │ │ │ ├── account.resolver.ts │ │ │ ├── account.service.spec.ts │ │ │ └── account.service.ts │ │ ├── app.module.ts │ │ └── main.ts │ └── tsconfig.json ├── index.ts ├── nest-cli.json ├── package.json └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/nodejs.yml ================================================ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Node.js CI on: push: pull_request: branches: [master] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [14.x, 16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: yarn install --frozen-lockfile - run: yarn tsc -p . --noEmit - run: yarn test env: CI: true - name: Coveralls uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ .vscode/ dist/ ### https://raw.github.com/github/gitignore/994f99fc353f523dfe5633067805a1dd4a53040f/Node.gitignore # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .pnp.* ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Kris Lefeber Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # NestJS Dataloader ![Node.js CI](https://github.com/krislefeber/nestjs-dataloader/workflows/Node.js%20CI/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/krislefeber/nestjs-dataloader/badge.svg?branch=master)](https://coveralls.io/github/krislefeber/nestjs-dataloader?branch=master) NestJS dataloader simplifies adding [graphql/dataloader](https://github.com/graphql/dataloader) to your NestJS project. DataLoader aims to solve the common N+1 loading problem. ## Installation Install with yarn ```bash yarn add nestjs-dataloader ``` Install with npm ```bash npm install --save nestjs-dataloader ``` ## Usage ### NestDataLoader Creation We start by implementing the `NestDataLoader` interface. This tells `DataLoader` how to load our objects. ```typescript import * as DataLoader from 'dataloader'; import { Injectable } from '@nestjs/common'; import { NestDataLoader } from 'nestjs-dataloader'; ... @Injectable() export class AccountLoader implements NestDataLoader { constructor(private readonly accountService: AccountService) { } generateDataLoader(): DataLoader { return new DataLoader(keys => this.accountService.findByIds(keys)); } } ``` The first generic of the interface is the type of ID the datastore uses. The second generic is the type of object that will be returned. In the above instance, we want `DataLoader` to return instances of the `Account` class. ### Providing the NestDataLoader For each NestDataLoader we create, we need to provide it to our module. ```typescript import { Module } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import {DataLoaderInterceptor} from 'nestjs-dataloader' ... @Module({ providers: [ AccountResolver, AccountLoader, { provide: APP_INTERCEPTOR, useClass: DataLoaderInterceptor, }, ], }) export class ResolversModule { } ``` ### Using the NestDataLoader Now that we have a dataloader and our module is aware of it, we need to pass it as a parameter to an endpoint in our graphQL resolver. ```typescript import * as DataLoader from 'dataloader'; import { Loader } from 'nestjs-dataloader'; ... @Resolver(Account) export class AccountResolver { @Query(() => [Account]) public getAccounts( @Args({ name: 'ids', type: () => [String] }) ids: string[], @Loader(AccountLoader) accountLoader: DataLoader): Promise { return accountLoader.loadMany(ids); } } ``` The important thing to note is that the parameter of the `@Loader` decorator is the entity/class of the `NestDataLoader` we want to be injected to the method. The DataLoader library will handle bulk retrieval and caching of our requests. Note that the caching is stored on a per-request basis. ## Contributing Pull requests are always welcome. For major changes, please open an issue first to discuss what you would like to change. ================================================ FILE: _config.yml ================================================ theme: jekyll-theme-slate ================================================ FILE: example/__tests__/app.e2e-spec.ts ================================================ import { Test, TestingModule } from "@nestjs/testing"; import { INestApplication } from "@nestjs/common"; import request from 'supertest'; import gql from "graphql-tag"; import { AppModule } from "./../src/app.module"; import { Factory } from 'typeorm-factory' import { Account } from "../src/account/account.entity"; describe("AppModule", () => { let app: INestApplication; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); afterAll(() => app.close()); it("defined", () => expect(app).toBeDefined()); it("/graphql(POST) getAccounts", async () => { const f = new Factory(Account).attr('name', 'name') const account = await f.create() const query = request(app.getHttpServer()).post; const result = await query('/graphql',{ query: gql` query q($ids: [ID!]!) { getAccounts(ids: $ids) { id } } `, variables: { ids: [account.id], }, }); expect(result.errors).toBeUndefined() }); }); ================================================ FILE: example/src/account/account.entity.ts ================================================ import { ObjectType, Field, ID } from "@nestjs/graphql"; import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; @Entity("accounts") @ObjectType() export class Account { @PrimaryGeneratedColumn() @Field(() => ID) readonly id: string; @Column() readonly name: string; } ================================================ FILE: example/src/account/account.loader.ts ================================================ import DataLoader = require("dataloader"); import { Injectable } from "@nestjs/common"; import { NestDataLoader } from "../../.."; import { AccountService } from "./account.service"; import { Account } from "./account.entity"; @Injectable() export class AccountLoader implements NestDataLoader { constructor(private readonly accountService: AccountService) {} generateDataLoader(): DataLoader { return new DataLoader(keys => this.accountService.findByIds(keys) ); } } ================================================ FILE: example/src/account/account.module.ts ================================================ import { Module } from "@nestjs/common"; import { AccountResolver } from "./account.resolver"; import { AccountService } from "./account.service"; import { APP_INTERCEPTOR } from "@nestjs/core/constants"; import { DataLoaderInterceptor } from "../../../index"; import { Account } from "./account.entity"; import { TypeOrmModule } from "@nestjs/typeorm"; import { AccountLoader } from "./account.loader"; @Module({ imports: [TypeOrmModule.forFeature([Account])], providers: [ AccountResolver, AccountService, AccountLoader, { provide: APP_INTERCEPTOR, useClass: DataLoaderInterceptor, }, ], }) export class AccountModule {} ================================================ FILE: example/src/account/account.resolver.spec.ts ================================================ import { Test, TestingModule } from "@nestjs/testing"; import { AccountResolver } from "./account.resolver"; describe("AccountResolver", () => { let resolver: AccountResolver; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [AccountResolver], }).compile(); resolver = module.get(AccountResolver); }); it("should be defined", () => expect(resolver).toBeDefined()); }); ================================================ FILE: example/src/account/account.resolver.ts ================================================ import { Resolver, Args, Query, ID } from "@nestjs/graphql"; import * as DataLoader from "dataloader"; import { Loader } from "../../../index"; import { Account } from "./account.entity"; import { AccountLoader } from "./account.loader"; @Resolver("Account") export class AccountResolver { @Query(() => [Account]) public getAccounts( @Args({ name: "ids", type: () => [ID] }) ids: string[], @Loader(AccountLoader) accountLoader: DataLoader ): Promise<(Account | Error)[]> { return accountLoader.loadMany(ids); } } ================================================ FILE: example/src/account/account.service.spec.ts ================================================ import { Test, TestingModule } from "@nestjs/testing"; import { AccountService } from "./account.service"; import { Account } from "./account.entity"; import { getRepositoryToken } from "@nestjs/typeorm"; describe("AccountService", () => { let service: AccountService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ AccountService, { provide: getRepositoryToken(Account), useValue: {}, }, ], }).compile(); service = module.get(AccountService); }); it("should be defined", () => { expect(service).toBeDefined(); }); }); ================================================ FILE: example/src/account/account.service.ts ================================================ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Account } from "./account.entity"; import { Repository, In } from "typeorm"; @Injectable() export class AccountService { constructor( @InjectRepository(Account) private readonly accounts: Repository ) {} async findByIds(ids: readonly string[]) { return this.accounts.findByIds(ids as string[]); } } ================================================ FILE: example/src/app.module.ts ================================================ import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo"; import { Module } from "@nestjs/common"; import { GraphQLModule } from "@nestjs/graphql"; import { TypeOrmModule } from "@nestjs/typeorm"; import { join } from "path"; import { AccountModule } from "./account/account.module"; @Module({ imports: [ GraphQLModule.forRoot({ driver: ApolloDriver, autoSchemaFile: true, debug: true, }), TypeOrmModule.forRoot({ type: "sqlite", database: "sample", entities: [join(__dirname, "./**/*.entity.[t|j]s")], synchronize: true, }), AccountModule, ] }) export class AppModule {} ================================================ FILE: example/src/main.ts ================================================ import { NestFactory } from "@nestjs/core"; import { AppModule } from "./app.module"; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap(); ================================================ FILE: example/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "es6", "sourceMap": true, "outDir": "./dist", "skipLibCheck": true, }, "include": ["src"], "exclude": ["node_modules", "*.spec.ts"] } ================================================ FILE: index.ts ================================================ import { CallHandler, createParamDecorator, ExecutionContext, Injectable, InternalServerErrorException, NestInterceptor, Type, } from '@nestjs/common'; import { APP_INTERCEPTOR, ModuleRef, ContextIdFactory } from '@nestjs/core'; import { GqlExecutionContext } from '@nestjs/graphql'; import * as DataLoader from 'dataloader'; import { Observable } from 'rxjs'; /** * This interface will be used to generate the initial data loader.       * The concrete implementation should be added as a provider to your module. */ export interface NestDataLoader { /** * Should return a new instance of dataloader each time */ generateDataLoader(): DataLoader; } /** * Context key where get loader function will be stored. * This class should be added to your module providers like so: * { * provide: APP_INTERCEPTOR, * useClass: DataLoaderInterceptor, * }, */ const NEST_LOADER_CONTEXT_KEY: string = "NEST_LOADER_CONTEXT_KEY"; @Injectable() export class DataLoaderInterceptor implements NestInterceptor { constructor(private readonly moduleRef: ModuleRef) { } /** * @inheritdoc */ intercept(context: ExecutionContext, next: CallHandler): Observable { const graphqlExecutionContext = GqlExecutionContext.create(context); const ctx = graphqlExecutionContext.getContext(); if (ctx[NEST_LOADER_CONTEXT_KEY] === undefined) { ctx[NEST_LOADER_CONTEXT_KEY] = { contextId: ContextIdFactory.create(), getLoader: (type: string) : Promise> => { if (ctx[type] === undefined) { try { ctx[type] = (async () => { return (await this.moduleRef.resolve>(type, ctx[NEST_LOADER_CONTEXT_KEY].contextId, { strict: false })) .generateDataLoader(); })(); } catch (e) { throw new InternalServerErrorException(`The loader ${type} is not provided` + e); } } return ctx[type]; } }; } return next.handle(); } } /** * The decorator to be used within your graphql method. */ export const Loader = createParamDecorator(async (data: Type>, context: ExecutionContext & { [key: string]: any }) => { const ctx: any = GqlExecutionContext.create(context).getContext(); if (ctx[NEST_LOADER_CONTEXT_KEY] === undefined) { throw new InternalServerErrorException(` You should provide interceptor ${DataLoaderInterceptor.name} globally with ${APP_INTERCEPTOR} `); } return await ctx[NEST_LOADER_CONTEXT_KEY].getLoader(data); }); ================================================ FILE: nest-cli.json ================================================ { "collection": "@nestjs/schematics", "sourceRoot": "example/src", "compilerOptions": { "plugins": ["@nestjs/graphql/plugin"] } } ================================================ FILE: package.json ================================================ { "name": "nestjs-dataloader", "version": "2.0.6", "description": "A NestJS decorator for dataloader", "license": "MIT", "repository": "https://github.com/krislefeber/nestjs-dataloader", "author": "Kris Lefeber ", "main": "dist/index.js", "scripts": { "build": "tsc -p tsconfig.json", "prebuild": "rm -rf ./dist", "prepare": "tsc -p tsconfig.json", "prestart": "rm -rf ./example/dist", "start": "nest start example/src/main.ts --watch --path=example/tsconfig.json", "test": "jest" }, "keywords": [ "nestjs", "dataloader", "graphql" ], "dependencies": { "@nestjs/apollo": "^10.0.22", "dataloader": "^2.1.0", "rxjs": "^7.5.6" }, "devDependencies": { "@nestjs/cli": "^9.1.1", "@nestjs/common": "^9.0.1", "@nestjs/core": "^9.0.1", "@nestjs/graphql": "^10.0.18", "@nestjs/platform-express": "^9.0.1", "@nestjs/testing": "^9.0.1", "@nestjs/typeorm": "^9.0.1", "@types/jest": "28.1.4", "apollo-server": "3.9.0", "apollo-server-express": "^3.9.0", "graphql": "^16.5.0", "jest": "28.1.3", "reflect-metadata": "^0.1.12", "sqlite3": "^5.0.11", "supertest": "^6.2.4", "ts-jest": "^28.0.8", "typeorm": "0.2.18", "typeorm-factory": "^0.0.14", "typescript": "4.7.4" }, "types": "index.d.ts", "jest": { "transform": { "^.+\\.tsx?$": "ts-jest" }, "collectCoverage": true, "globals": { "ts-jest": { "diagnostics": { "warnOnly": true } } }, "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.ts$", "collectCoverageFrom": [ "index.ts" ] }, "files": [ "dist" ] } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "declaration": true, "removeComments": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "es6", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", "esModuleInterop": true, "skipLibCheck": true }, "exclude": ["node_modules", "dist", "example"] }