Git Product home page Git Product logo

typeorm-graphql-loader's Introduction

TypeORM GraphQL Relation Loader

A dataloader for TypeORM that makes it easy to load TypeORM relations for GraphQL query resolvers.

npm version npm License: MIT pipeline status coverage report

UPGRADE NOTICE

The 1.0.0 release of this package includes almost a complete rewrite of the source code. The public interface of the loader has changed significantly. As such, upgrading from the older versions will require significant work.

For those upgrading, I highly recommend reading through the new documentation to get an idea of the changes required.

Contents

Description

This package provides a GraphQLDatabaseLoader class, which is a caching loader that will parse a GraphQL query info object and load the TypeORM fields and relations needed to resolve the query. For a more in-depth explanation, see the Problem and Solution sections below.

Installation

yarn add @mando75/typeorm-graphql-loader

# OR

npm install @mando75/typeorm-graphql-loader

This package requires that you have TypeORM installed as a peer dependency

Usage

You should create a new GraphQLDatabaseLoader instance in each user session, generally via the GraphQLContext object. This is to help with caching and prevent user data from leaking between requests. The constructor takes a TypeORM connection as the first argument, and a LoaderOptions type as an optional second parameter.

Apollo Server Example

import { GraphQLDatabaseLoader } from '@mando75/typeorm-graphql-loader';
const connection = createConnection({...}); // Create your TypeORM connection

const apolloServer = new ApolloServer({
  schema,
  context: {
    loader: new GraphQLDatabaseLoader(connection, {/** additional options if needed**/})
  },
});

The loader will now appear in your resolver's context object:

{ 
  Query: {
    getBookById(object: any, args: {id: string }, context: MyGraphQLContext, info: GraphQLResolveInfo) {
        return context.loader
            .loadEntity(Book, "book")
            .where("book.id = :id", { id })
            .info(info)
            .loadOne();
    }
  }
}

Please note that the loader will only return the fields and relations that the client requested in the query. You can configure certain entity fields to be required or ignored via the ConfigureLoader decorator.

The loader provides a thin wrapper around the TypeORM SelectQueryBuilder with utility functions to help with things like adding where conditions, searching and pagination. For more advanced query building, see the documentation for the ejectQueryBuilder method.

Please refer to the full documentation for more details on what options and utilities the loader provides.

Gotchas

Because 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.

This is not a complete replacement for the 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.

This 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.

For help with pagination, first read Pagination Advice

Roadmap

Relay Support

Currently, the loader only supports offset pagination. I would like to add the ability to support Relay-style pagination out of the box.

Track Progress

Contributing

This project is developed on GitLab.com. 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. Please note that all merge requests must be done via GitLab as the GitHub repo is a read-only mirror.

When opening an issue, please include the following information:

  • Package Version
  • Database and version used
  • TypeORM version
  • GraphQL library used
  • Description of the problem
  • Example code

Please open an issue before opening any Merge Requests.

Problem

TypeORM is a pretty powerful tool, and it gives you quite a bit of flexibility in how you manage entity relations. TypeORM provides 3 ways to load your relations, eagerly, manually, or lazily. For more info on how this works, see the TypeORM Documentation.

While this API is great for having fine-grained control of you data layer, it can get frustrating to use in a GraphQL schema. For example, lets say we have three entities, User, Author, and Book. Each Book has an Author, and each Author has a User. We want to expose these relations via a GraphQL API. Our issue now becomes how to resolve these relations. Let's look at how an example resolver function might try to resolve this query:

Query

query bookById($id: ID!) {
  book(id: $id) {
    id
    name
    author {
      id
      user {
        id
        name
      }
    }
  }
}

We could do something simple like this:

function findBookById(object, args, context, info) {
  return Book.findOne(args.id);
}

but then the author and user relations won't be loaded. We can remedy that by specifying them in our find options like so:

function findBookById(object, args, context, info) {
  return Book.findOne(args.id, { relations: ["author", "author.user"] });
}

however, this could get really nasty if we have many relations we may need. Well, we could just set all of our relations to eagerly load so we don't need to specify them, but then we may start loading a bunch of data we may never use which isn't very performant at scale.

How about just defining a resolver for every relation and loading them as needed? That could work, but it seems like a lot of work and duplication of effort 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 to deal with impending N + 1 problems.

Another possible, and probably intuitive solution is to use lazy relations. Because lazy relations return Promises, as long as we give the resolver an instance of our Book entity, it will call each relation and wait for the Promise to resolve, fixing our problem. It lets us use our original resolver function:

function findBookById(object, args, context, info) {
  return Book.findOne(args.id);
}

and GraphQL will just automatically resolve the relation promises for us and return the data. Seems great right? It's not. This introduces a massive N+1 problem. Now every time you query a sub-relation, GraphQL will inadvertently perform another database query to load the lazy relation. At small scale this isn't a problem, but the more complex your schema becomes, the harder it will hit your performance.

Solution

This package offers a solution to take away all the worry of how you manage your entity relations in the resolvers. GraphQL provides a parameter in each resolver function called info. This info parameter contains the entire query graph, which means we can traverse it and figure out exactly which fields need to be selected, and which relations need to be loaded. This is used to create one SQL query that can get all the information at once.

Because the loader uses the queryBuilder API, it does not matter if you have all "normal", "lazy", "eager" relations, or a mix of all of them. You give it your starting entity, and the GraphQL query info, and it will figure out what data you need and give it back to you in a structured TypeORM entity. Additionally, it provides some caching functionality as well, which will dedupe identical query signatures executed in the same tick.

Acknowledgments

This project inspired by the work of Weboptimizer's typeorm-loader package. I work quite a bit with Apollo Server + TypeORM and I was looking to find a way to more efficiently pull data via TypeORM for GraphQL via intelligently loading the needed relations for a given query. I stumbled across his package, which seemed to promise all the functionality, but it seemed to be in a broken/unmaintained state. After several months of no response from the author, and with significant bug fixes/features added in my fork, I decided to just make my own package. So thanks to Weboptimizer for doing a lot of the groundwork. Since then, I have almost completely rewritten the library to be a bit more maintainable and feature rich, but I would still like to acknowledge the inspiration his project gave me.

typeorm-graphql-loader's People

Contributors

blitss avatar mando75 avatar nicobritos avatar roemerb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

typeorm-graphql-loader's Issues

Question : How can i use this loader without attaching GraphQLDatabaseLoader with predefined TypeOrm connection in GQL config?

I am running a few micro-services with multi tenancy (Database isolation) and central GraphQL server, hence TypeOrm connection is created on the fly after checking TenantId sent through GraphQL headers which happen from micro-service.

Is there anyway i can pass the gql args and info to these services and generate query builder from there instead of attaching GraphQLDatabaseLoader with predefined connection to context?

Pagination query not selecting correct fields

When using LoadManyPaginated, it passes in the queried fields from the root of the query. This works if you are only returning an array of entities, but this means you cannot return pagination metadata.

This appears to work, but only if you don't query for anything more than the primary column, which is fetched by default.

Not really sure what a good solution is here. The LoadManyPaginated function could accept a string as an option that specifies which field to drill into to find the selections for the entity?

I've added a test that reproduces the issue here.

Incorrect typing for `where`

I believe where should use the same type for querying as TypeORM (FindManyOptions<Entity> or FindOneOptions<Entity>). Is this correct? If so, I'd be happy to submit a PR updating it.

use a related table for where() and order()

Hey there, first of all great work with the lib!! it was very easy and fun to consume and implement, and like magic - performance for nested query are sky rocketing.

However, I couldn't find anywhere online an explanation on how to query in the "where" clause foreign-keyed tables. say I have an Item table for which I've written a graphql query, and it has a categoryId linking it to another entity called category (N:1). fetching the category fields works awesome, but when trying to do a search / where with fields like 'category.name', it fails trying to find the table. the reason is that in the SQL only my main entity got a descent alias (item), but the category table got a GUID for an alias or something like that. how can I force a join with my alias? if it was a regular typeorm query builder I would just use innerJoinAndSelect, but couldn't find such an equivalent in this lib...

column "createdat" does not exist

Got an error when trying to sort the users data in 'DESC' order.

const user = await loader
      .loadEntity(User, 'user')
      .info(info)
      .order({ createdAt: 'DESC' })
      .loadMany();

User Entity definition

@ObjectType()
@Entity()
export class User extends BaseEntity {
  @Field(type => ID)
  @PrimaryGeneratedColumn('uuid')
  id: string;

................
................

  @Field(type => Date)
  @CreateDateColumn()
  createdAt: Date;

  @Field(type => Date)
  @UpdateDateColumn()
  updatedAt: Date;
}

GraphQL Error log on playground

"errors": [
    {
      "message": "column \"createdat\" does not exist",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "me"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "message": "column \"createdat\" does not exist",
          "name": "QueryFailedError",
          "length": 111,
          "severity": "ERROR",
          "code": "42703",
          "position": "6222",
          "file": "parse_relation.c",
          "line": "3359",
          "routine": "errorMissingColumn",

How to properly implement resolvers?

I'm having troubles getting this library to work.
This is my (simplified) schema:

type Query {
  projects: [Project]
}

type Project {
  id: ID!
  name: String!
  activities: [Activity]
}

type Activity {
  id: ID!
  project: Project!
}

The entities:

@Entity()
export class Activity {
  @PrimaryGeneratedColumn("uuid")
  id!: string

  @ManyToOne(
    type => Project,
    project => project.activities,
    { eager: true }
  )
  project!: Project
}

@Entity()
export class Project {
  @PrimaryGeneratedColumn("uuid")
  id!: string

  @Column("varchar", { length: 100, unique: true })
  name!: string

  @OneToMany(
    type => Activity,
    activity => activity.project,
    { cascade: true }
  )
  activities!: Array<Activity>
}

The resolver:

const resolvers: IResolvers = {
  Query: {
    projects(root: any, args: any, { loader }: Context, info: GraphQLResolveInfo): Promise<Array<Project>> {
      return loader.loadMany(Project, {}, info)
    },
  },
  Project: {
    async activities(
      project: Project,
      args: any,
      { loader }: Context,
      info: GraphQLResolveInfo
    ): Promise<Array<Activity>> {
      return loader.loadMany<Activity>(Activity, { project }, info)
    },
  },
}

I was first implementing my resolvers without your library but used standard TypeORM connection and everything worked as expected (but of cause with the N+1 problem) and my test cases were successful .
To solve the N+1 problem I've reworked the resolvers to use the loader.

Problem 1)
This seems to be very fragile. I have a test case that uses this test-query:

{ 
  projects {
    id
    activities {
      id
      project {
        id
      }
    }
  }
}

When I execute the test 1 out of 5 times it runs successfully but most of the time there are test failures. Sometimes I get GraphQLError: Transaction already started for the given connection, commit current transaction before starting a new one. and when I log the sql statements I see a ROLLBACK being done. Sometimes it executes the query but some data is missing in the result (some activities are missing from projects).

Problem 2)
It seems to be not solving the N+1 problem. I have another test case that uses this query:

{ 
  projects {
    id
    activities {
      id
    }
  }
}

The total number of SELECT statements here is numberOfProjects + 1 so for example when I have 100 projects with 10 activities each it executes 101 queries. It's doing 1 SELECT on the projects table and 100 on the activities table.
If I change it to this query:

{ 
  projects {
    id
    activities {
      id
      project {
        id
        activities {
          id
        }
      }
    }
  }
}

it executes 198 SELECTs.

I assume that my implementation of the resolver is wrong but I'm not sure how I should adjust it.

I also tried to add a resolver function for ' Activity' like this:

const resolvers = {
  ...
  Activity: {
    async project(
      activity: Activity,
      args: any,
      { loader }: Context,
      info: GraphQLResolveInfo
    ): Promise<Project | undefined> {
      return loader.loadOne<Project>(Project, { activities: [activity] }, info)
    },
  },
}

But this wasn't solving the problem but instead now I got a timeout for the last test-case.

Can you give any hints on how to correctly implement the resolvers or where my problem is?

Redundant typeof BaseEntity typing

I noticed that both GraphQLDatabaseLoader and GraphQLQueryBuilder use generic extending typeof BaseEntity which is incorrect in some cases because Entity might not be extended from BaseEntity. It enables some difficulties to implement correct typing in real use. It seems everything works great if entity isn't extended from BaseEntity, so I think it's redundant typing. Please, fix it.

Support Data Mapper entities

Hi there,

Does this currently support typeorm entities following the Data Mapper approach? (not inheriting from BaseEntity).

Currently I get the following error for all of my entities:

Argument of type 'typeof MyEntity' is not assignable to parameter of type 'typeof BaseEntity'.
  Type 'typeof MyEntity' is missing the following properties from type 'typeof BaseEntity': useConnection, getRepository, target, hasId, and 19 more.

Ordering of sub relations

Given an Order and Items relational query, how do I order the items by a field as well as the orders ?

    return loader
        .loadEntity(Order, "order")
        .info(info)
        .order({'order.date': 'DESC'})
        .loadMany();
@ObjectType()
@Entity()
export class Order extends BaseEntity {

    @Field((type) => Int)
    @PrimaryGeneratedColumn()
    id: number;

    @FieldI(type) => String)
    @Column()
    text: string;

    @Field((type) => Date)
    @Column()
    date: Date;

    @Field((type) => [Item], { nullable: true })
    @OneToMany((type) => Item, item => item.order)
    items Item[];
}

@ObjectType()
@Entity()
export class Item extends BaseEntity {

    @Field((type) => Int)
    @PrimaryGeneratedColumn()
    id: number;

    @Field()
    @Column()
    text: string;

    @Field()
    @Column()
    _number: number;

    @Field((type) => Post)
    @ManyToOne((type) => Order, (order) => order.items)
    @JoinColumn({ name: 'order_id' })
    item: Item;
};


I want to be able to add the equivalent to a ```.order({{'item._number': 'ASC'})``` to order items within orders by ```_number```

Small fix for graphqlQueryBuilder.ts

Hi,
it seems that the problem with TS choosing the wrong overload for
fields.reduce((o: Hash<Selection>, ast: FieldNode)...
in graphqlQueryBuilder.ts can be resolved with
const fields = info.fieldNodes as Array<FieldNode>;
instead of
const fields = info.fieldNodes;

Congrats for the great project - I implemented the same functionality a while ago, but I like your version better.

[Question] How does this library handle field resolvers?

Hey, thanks for your work on this library, very helpful.

I had a question about field resolvers, how would one avoid the N+1 problem and use this library to handle fields that fetch external data or are calculated based on other fields or even model data?

Support for named columns

named columns are not supported by the package. so for example this entity:

@ObjectType()
@Entity()
export class Author extends BaseEntity {
  ...
  @Field()
  @Column("varchar", { name: "mobile" })
  phone!: string;
...
}

the column "phone" has its original name as "mobile" in the database.
right now when you query against this model all the values for the other columns are returned but not for this column.

Possible Solution:
I was able to fix this by adding/changing some lines in the GraphQLQueryResolver.ts file.
specifically on line 86 - 95.

fields.forEach(field => {
// Make sure we account for embedded types
const propertyName: string = field.propertyName;
const databaseName: string = field.databaseName;

  queryBuilder = queryBuilder.addSelect(
    this._formatter.columnSelection(alias, propertyName),
    this._formatter.aliasField(alias, databaseName)
  );
});

I used the field.databaseName as the alias to access the value for each field.

I will try add a PR, as soon as I was able get the tests ready.

Thanks!

Just wanted to say thanks for this great package! Very useful!

Keep up the good work.

Support nested/inherited types fields

Currently, this doesn't seem to support nested or inherited fields as defined in the TypeORM Entity

@ObjectType()
@Entity()
export default class Thing1 {
  @PrimaryGeneratedColumn()
  id: number;

  @Field(type => NestedType)
  @Column(type => NestedType)
  nested: NestedType;
}

@ObjectType()
export default class NestedType {
  @Field(type => Date, { nullable: true })
  @Column({ nullable: true })
  someDate: Date;
}

The query it produces doesn't know that nestedSomeDate is a column in the DB

ConfigureLoader Not working!

hi
the function ConfigureLoader not working
used it for require select fields
version 1.7.2
also the version 1.7.3 compile with errors (no export for GraphQLDatabaseLoader)

@Field(() => Int) @Column("integer", { nullable: false }) @ConfigureLoader({ required: true }) index: number;

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.