@mando75/typeorm-graphql-loader
Version:
A dataloader which intelligently selects/joins the fields/relations from your TypeORM entities needed to resolve a GraphQL query
107 lines • 14.9 kB
JSON
{
"name": "@mando75/typeorm-graphql-loader",
"version": "1.7.5",
"description": "A dataloader which intelligently selects/joins the fields/relations from your TypeORM entities needed to resolve a GraphQL query",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/*",
"yarn.lock",
".gitignore",
"!/dist/__tests__"
],
"repository": {
"type": "git",
"url": "https://gitlab.com/Mando75/typeorm-graphql-loader"
},
"keywords": [
"typeorm",
"database",
"graphql",
"data",
"apollo",
"typegraphql",
"loader",
"batching",
"caching",
"resolvers",
"dataloader"
],
"author": "Bryan Muller",
"license": "MIT",
"bugs": {
"url": "https://gitlab.com/Mando75/typeorm-graphql-loader/issues"
},
"homepage": "https://gql-loader.bmuller.net",
"dependencies": {
"graphql-parse-resolve-info": "^4.12.0"
},
"devDependencies": {
"@types/chai": "^4.3.0",
"@types/chai-spies": "^1.0.3",
"@types/chance": "^1.1.3",
"@types/deep-equal": "^1.0.1",
"@types/faker": "^5.5.9",
"@types/mocha": "^9.0.0",
"@types/node": "^17.0.8",
"@types/object-path": "^0.11.1",
"@types/validator": "^13.7.1",
"@typescript-eslint/eslint-plugin": "^5.9.1",
"@typescript-eslint/parser": "^5.9.1",
"chai": "^4.3.4",
"chai-spies": "^1.0.0",
"chai-things": "^0.2.0",
"chance": "^1.1.8",
"class-validator": "^0.13.2",
"deep-equal-in-any-order": "^1.1.15",
"eslint": "^8.6.0",
"eslint-plugin-prettier": "^4.0.0",
"faker": "^5.5.3",
"graphql": "^15.8.0",
"mocha": "^9.1.4",
"mocha-lcov-reporter": "^1.3.0",
"nyc": "15.1.0",
"prettier": "^2.5.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"source-map-support": "^0.5.21",
"sqlite3": "^5.0.2",
"ts-node": "^10.4.0",
"tslib": "^2.3.1",
"type-graphql": "^1.1.1",
"typedoc": "^0.22.10",
"typeorm": "^0.2.41",
"typescript": "^4.5.4"
},
"peerDependencies": {
"graphql": ">=15.0.0",
"typeorm": ">=0.2.8"
},
"nyc": {
"include": [
"src/**/*.ts"
],
"extension": [
".ts"
],
"require": [
"ts-node/register",
"tslib"
],
"reporter": [
"lcov",
"text"
],
"sourceMap": true,
"instrument": true
},
"scripts": {
"prebuild": "pnpm lint && rimraf -rf dist",
"build": "tsc --declaration",
"publish:docs": "typedoc --options typedoc.json",
"lint": "eslint \"./src/**/*.ts\"",
"lint:fix": "pnpm lint --fix",
"test": "nyc mocha -r ts-node/register -r tslib -r source-map-support/register --full-trace src/__tests__/**/*.test.ts --timeout 5000"
},
"readme": "# TypeORM GraphQL Relation Loader\n\nA dataloader for TypeORM that makes it easy to load TypeORM relations for\nGraphQL query resolvers.\n\n\n[](https://badge.fury.io/js/%40mando75%2Ftypeorm-graphql-loader)\n\n[](https://opensource.org/licenses/MIT)\n[](https://gitlab.com/Mando75/typeorm-graphql-loader/commits/master)\n[](https://gitlab.com/Mando75/typeorm-graphql-loader/-/commits/master)\n\n\n\n## UPGRADE NOTICE\n\nThe 1.0.0 release of this package includes almost a complete rewrite\nof the source code. The public interface of the loader has changed significantly. \nAs such, upgrading from the older versions will require significant work. \n\nFor those upgrading, I highly recommend reading through the [new documentation](https://gql-loader.bmuller.net) to get an idea of the changes required.\n\n## Contents\n\n- [Description](#Description)\n- [Installation](#Installation)\n- [Usage](#Usage)\n- [Gotchas](#Gotchas)\n- [Roadmap](#Roadmap)\n- [Contributing](#Contributing)\n- [Problem](#Problem)\n- [Solution](#Solution)\n- [Acknowledgments](#Acknowledgments)\n\n## Description <a name=\"Description\">\n\nThis package provides a `GraphQLDatabaseLoader` class, which is a caching\nloader that will parse a GraphQL query info object and load the\nTypeORM fields and relations needed to resolve the query. For a more in-depth\nexplanation, see the [Problem](#Problem) and [Solution](#Solution) sections below.\n\n## Installation <a name=\"Installation\">\n\n```bash\nyarn add @mando75/typeorm-graphql-loader\n\n# OR\n\nnpm install @mando75/typeorm-graphql-loader\n```\n\nThis package requires that you have TypeORM installed as a peer dependency\n\n## Usage <a name=\"Usage\">\n\nYou should create a new GraphQLDatabaseLoader instance in each user session,\ngenerally via the GraphQLContext object. This is to help with caching and\nprevent user data from leaking between requests. The constructor takes a TypeORM\nconnection as the first argument, and a [LoaderOptions](https://gql-loader.bmuller.net/interfaces/loaderoptions.html) type as an\noptional second parameter.\n\n### Apollo Server Example\n```typescript\nimport { GraphQLDatabaseLoader } from '@mando75/typeorm-graphql-loader';\nconst connection = createConnection({...}); // Create your TypeORM connection\n\nconst apolloServer = new ApolloServer({\n schema,\n context: {\n loader: new GraphQLDatabaseLoader(connection, {/** additional options if needed**/})\n },\n});\n```\n\nThe loader will now appear in your resolver's context object:\n\n```typescript\n{ \n Query: {\n getBookById(object: any, args: {id: string }, context: MyGraphQLContext, info: GraphQLResolveInfo) {\n return context.loader\n .loadEntity(Book, \"book\")\n .where(\"book.id = :id\", { id })\n .info(info)\n .loadOne();\n }\n }\n}\n```\n\nPlease note that the loader will only return the fields and relations that\nthe client requested in the query. You can configure certain entity fields to be required or ignored via the [ConfigureLoader](https://gql-loader.bmuller.net/globals.html#configureloader) decorator.\n\nThe loader provides a thin wrapper around the TypeORM SelectQueryBuilder\nwith utility functions to help with things like adding where conditions, searching and pagination. \nFor more advanced query building, see the documentation for the \n[ejectQueryBuilder](https://gql-loader.bmuller.net/classes/graphqlquerybuilder.html#ejectquerybuilder) \nmethod.\n\nPlease refer to the [full documentation](https://gql-loader.bmuller.net) for more details on what\noptions and utilities the loader provides.\n\n## Gotchas <a name=\"Gotchas\">\n\nBecause this package reads which relations and fields to load from the GraphQL query info object, the loader only works if your schema field names match your TypeORM entity field names. If it cannot find a requested GraphQL query field, it will not return it. In this case, you will need to provide a custom resolver for that field in your GraphQL resolvers file. In this case, the loader will provide the resolver function with an `object` parameter which is an entity loaded with whichever other fields your query requested. The loader will always return an object with at least the primary key loaded, so basic method calls should be possible. The loader will automatically scan your entity and include whatever column marked as primary key in the query. \n\nThis is not a complete replacement for the [dataloader](https://github.com/graphql/dataloader) package, its purpose is different. While it does provide some batching, its primary purpose is to load the relations and fields needed to resolve the query. In most cases, you will most likely not need to use dataloader when using this package. However, I have noticed in my own use that there are occasions where this may need to be combined with dataloader to remove N + 1 queries. One such case was a custom resolver for a many-to-many relation that existed in the GraphQL Schema but not on a database level. In order to completely remove the N+1 queries from that resolver, I had to wrap the TypeORM GraphQL loader in a Facebook DataLoader. If you find that you are in a situation where the TypeORM GraphQL loader is not solving the N+1 problem, please open an issue, and I'll do my best to help you out with it. \n\nThis package has currently only been tested with Postgresql and SQLite. In theory, everything should work with the other SQL variants that TypeORM supports, as it uses the TypeORM Query Builder API to construct the database queries. If you run into any issues with other SQL dialects, please open an issue.\n\nFor help with pagination, first read [Pagination Advice](https://gitlab.com/Mando75/typeorm-graphql-loader/-/blob/master/md/pagination.md)\n\n## Roadmap <a name=\"Roadmap\">\n\n### Relay Support\n\nCurrently, the loader only supports offset pagination. I would like to add the ability to support Relay-style pagination out of the box. \n\n[Track Progress](https://gitlab.com/Mando75/typeorm-graphql-loader/-/issues/8)\n\n## Contributing <a name=\"Contributing\">\n\nThis project is developed on [GitLab.com](https://gitlab.com/Mando75/typeorm-graphql-loader). However, I realize that many developers use GitHub as their primary development platform. If you do not use and do not wish to create a GitLab account, you can open an issue in the mirrored [GitHub Repository](https://github.com/Mando75/typeorm-graphql-loader). Please note that all merge requests must be done via GitLab as the GitHub repo is a read-only mirror. \n\nWhen opening an issue, please include the following information:\n\n- Package Version\n- Database and version used\n- TypeORM version\n- GraphQL library used\n- Description of the problem\n- Example code\n\nPlease open an issue before opening any Merge Requests.\n\n## Problem <a name=\"Problem\">\n\nTypeORM is a pretty powerful tool, and it gives you quite a bit of flexibility\nin how you manage entity relations. TypeORM provides 3 ways to load your\nrelations, eagerly, manually, or lazily. For more info on how this works, see\nthe [TypeORM Documentation](https://typeorm.io/#/eager-and-lazy-relations).\n\nWhile this API is great for having fine-grained control of you data layer, it\ncan get frustrating to use in a GraphQL schema. For example, lets say we have\nthree entities, User, Author, and Book. Each Book has an Author, and each Author\nhas a User. We want to expose these relations via a GraphQL API. Our issue now\nbecomes how to resolve these relations. Let's look at how an example resolver\nfunction might try to resolve this query:\n\nQuery\n\n```graphql\nquery bookById($id: ID!) {\n book(id: $id) {\n id\n name\n author {\n id\n user {\n id\n name\n }\n }\n }\n}\n```\n\nWe could do something simple like this:\n\n```ts\nfunction findBookById(object, args, context, info) {\n return Book.findOne(args.id);\n}\n```\n\nbut then the author and user relations won't be loaded. We can remedy that by\nspecifying them in our find options like so:\n\n```ts\nfunction findBookById(object, args, context, info) {\n return Book.findOne(args.id, { relations: [\"author\", \"author.user\"] });\n}\n```\n\nhowever, this could get really nasty if we have many relations we may need.\nWell, we could just set all of our relations to eagerly load so we don't need to\nspecify them, but then we may start loading a bunch of data we may never use\nwhich isn't very performant at scale.\n\nHow about just defining a resolver for every relation and loading them as\nneeded? That could work, but it seems like a lot of work and duplication of\neffort when we've already specified our relations on the entity level. This will also lead us to a path where we will need to start creating custom loaders via [dataloader](https://github.com/graphql/dataloader) to deal with impending [N + 1](https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping) problems.\n\nAnother possible, and probably intuitive solution is to use lazy relations.\nBecause lazy relations return Promises, as long as we give the resolver an\ninstance of our Book entity, it will call each relation and wait for the Promise\nto resolve, fixing our problem. It lets us use our original resolver function:\n\n```ts\nfunction findBookById(object, args, context, info) {\n return Book.findOne(args.id);\n}\n```\n\nand GraphQL will just automatically resolve the relation promises for us\nand return the data. Seems great right? It's not. This introduces a massive N+1\nproblem. Now every time you query a sub-relation, GraphQL will inadvertently\nperform another database query to load the lazy relation. At small scale this\nisn't a problem, but the more complex your schema becomes, the harder it will\nhit your performance.\n\n## Solution <a name=\"Solution\">\n\nThis package offers a solution to take away all the worry of how you manage\nyour entity relations in the resolvers. GraphQL provides a parameter in each\nresolver function called `info`. This `info` parameter contains the entire query\ngraph, which means we can traverse it and figure out exactly which fields need to\nbe selected, and which relations need to be loaded. This is used to\ncreate one SQL query that can get all the information at once.\n\nBecause the loader uses the queryBuilder API, it does not matter if you have all\n\"normal\", \"lazy\", \"eager\" relations, or a mix of all of them. You give it your\nstarting entity, and the GraphQL query info, and it will figure out what data you\nneed and give it back to you in a structured TypeORM entity. Additionally, it\nprovides some caching functionality as well, which will dedupe identical query\nsignatures executed in the same tick.\n\n## Acknowledgments <a name=\"Acknowledgments\">\n\nThis project inspired by the work of [Weboptimizer's typeorm-loader\npackage](https://github.com/Webtomizer/typeorm-loader). I work quite a bit with\nApollo Server + TypeORM and I was looking to find a way to more efficiently pull\ndata via TypeORM for GraphQL via intelligently loading the needed relations for\na given query. I stumbled across his package, which seemed to\npromise all the functionality, but it seemed to be in a broken/unmaintained\nstate. After several months of no response from the author, and with significant\nbug fixes/features added in my fork, I decided to just make my own package. So\nthanks to Weboptimizer for doing a lot of the groundwork. Since then, I have\nalmost completely rewritten the library to be a bit more maintainable and feature\nrich, but I would still like to acknowledge the inspiration his project gave me. \n"
}