Giter Club home page Giter Club logo

join-monster's Introduction

join-monster npm version Build Status Documentation Status

Query Planning and Batch Data Fetching between GraphQL and SQL.

What is Join Monster?

Efficient query planning and data fetching for SQL. Use JOINs and/or batched requests to retrieve all your data. It takes a GraphQL query and your schema and automatically generates the SQL. Send that SQL to your database and get back all the data needed to resolve with only one or a few round-trips to the database.

Translate a GraphQL query like this:

{
  user(id: 2) {
    fullName
    email
    posts {
      id
      body
      comments {
        body
        author { fullName }
      }
    }
  }
}

...into a couple SQL queries like this:

SELECT
  "user"."id" AS "id",
  "user"."email_address" AS "email_address",
  "posts"."id" AS "posts__id",
  "posts"."body" AS "posts__body",
  "user"."first_name" AS "first_name",
  "user"."last_name" AS "last_name"
FROM accounts AS "user"
LEFT JOIN posts AS "posts" ON "user".id = "posts".author_id
WHERE "user".id = 2

-- then get the right comments for each post
SELECT
  "comments"."id" AS "id",
  "comments"."body" AS "body",
  "author"."id" AS "author__id",
  "author"."first_name" AS "author__first_name",
  "author"."last_name" AS "author__last_name",
  "comments"."post_id" AS "post_id"
FROM comments AS "comments"
LEFT JOIN accounts AS "author" ON "comments".author_id = "author".id
WHERE "comments".archived = FALSE AND "comments"."post_id" IN (2,8,11,12) -- the post IDs from the previous query 

...and get back correctly hydrated data.

{
  "user": {
    "fullName": "Yasmine Rolfson",
    "email": "[email protected]",
    "posts": [
      {
        "id": 2,
        "body": "Harum unde maiores est quasi totam consequuntur. Necessitatibus doloribus ut totam dolore omnis quos error eos. Rem nostrum assumenda eius veniam fugit dicta in consequuntur. Ut porro dolorem aliquid qui magnam a.",
        "comments": [
          {
            "body": "The AI driver is down, program the multi-byte sensor so we can parse the SAS bandwidth!",
            "author": { "fullName": "Yasmine Rolfson" }
          },
          {
            "body": "Try to program the SMS transmitter, maybe it will synthesize the optical firewall!",
            "author": { "fullName": "Ole Barrows" }
          },
        ]
      },
      // other posts omitted for clarity...
    ]
  }
}

It works on top of Facebook's graphql-js reference implementation. All you have to do is add a few properties to the objects in your schema and call the joinMonster function. A SQL query is "compiled" for you to send to the DBMS. The data-fetching is efficiently batched. The data is then hydrated into the right shape for your GraphQL schema.

Why?

More details on the "round-trip" (a.k.a. N+1) problem are here.

  • Batching - Fetch all the data in a single, or a few, database query(s).
  • Efficient - No over-fetching data. Retrieve only the data that the client actually requested.
  • Maintainability - SQL is automatically generated and adaptive. No need to manually write queries or update them when the schema changes.
  • Declarative - Simply define the data requirements of the GraphQL fields on the SQL columns.
  • Unobtrusive - Coexists with your custom resolve functions and existing schemas. Use it on the whole graph or only in parts. Retain the power and expressiveness in defining your schema.
  • Object-relational impedance mismatch - Don't bother duplicating a bunch of object definitions in an ORM. Let GraphQL do your object mapping for you.

Since it works with the reference implementation, the API is all very familiar. Join Monster is a tool built on top to add batch data fetching. You add some special properties along-side the schema definition that Join Monster knows to look for. The use of graphql-js does not change. You still define your types the same way. You can write resolve functions to manipulate the data from Join Monster, or incorporate data from elsewhere without breaking out of your "join-monsterized" schema.

Get Pagination out of the Box

Join Monster has support for several different implementations of pagination, all based on the interface in the Relay Connection Specification. Using Relay on the client is totally optional!

Works with the RelayJS

Great helpers for the Node Interface and automatic pagination for Connection Types. See docs.

Usage with GraphQL

$ npm install join-monster
  1. Take your GraphQLObjectType from graphql-js and add the SQL table name.
  2. Do the fields need values from some SQL columns? Computed columns? Add some additional properties like sqlColumn, sqlDeps, or sqlExpr to the fields. Join Monster will look at these when analyzing the query.
  3. Got some relations? Write a function that tells Join Monster how to JOIN your tables and it will hydrate hierarchies of data.
  4. Resolve any type (and all its descendants) by calling joinMonster in its resolver. All it needs is the resolveInfo and a callback to send the (one) SQL query to the database. Voila! All your data is returned to the resolver.
import joinMonster from 'join-monster'
import {
  GraphQLObjectType,
  GraphQLList,
  GraphQLString,
  GraphQLInt
  // and some other stuff
} from 'graphql'

const User = new GraphQLObjectType({
  name: 'User',
  sqlTable: 'accounts', // the SQL table for this object type is called "accounts"
  uniqueKey: 'id', // the id in each row is unique for this table
  fields: () => ({
    id: {
      // the column name is assumed to be the same as the field name
      type: GraphQLInt
    },
    email: {
      type: GraphQLString,
      // if the column name is different, it must be specified specified
      sqlColumn: 'email_address'
    },
    idEncoded: {
      description: 'The ID base-64 encoded',
      type: GraphQLString,
      // this field uses a sqlColumn and applies a resolver function on the value
      // if a resolver is present, the `sqlColumn` MUST be specified even if it is the same name as the field
      sqlColumn: 'id',
      resolve: user => toBase64(user.idEncoded)
    },
    fullName: {
      description: "A user's first and last name",
      type: GraphQLString,
      // perhaps there is no 1-to-1 mapping of field to column
      // this field depends on multiple columns
      sqlDeps: [ 'first_name', 'last_name' ],
      // compute the value with a resolver
      resolve: user => `${user.first_name} ${user.last_name}`
    },
    capitalizedLastName: {
      type: GraphQLString,
      // do a computed column in SQL with raw expression
      sqlExpr: (table, args) => `UPPER(${table}.last_name)`
    },
    // got tables inside tables??
    // get it with a JOIN!
    posts: {
      description: "A List of posts this user has written.",
      type: new GraphQLList(Post),
      // a function to generate the join condition from the table aliases
      sqlJoin(userTable, postTable) {
        return `${userTable}.id = ${postTable}.author_id`
      }
    },
    // got a relationship but don't want to add another JOIN?
    // get this in a second batch request
    comments: {
      description: "The comment they have written",
      type: new GraphQLList(Comment),
      // specify which columns to match up the values
      sqlBatch: {
        thisKey: 'author_id',
        parentKey: 'id'
      }
    },
    // many-to-many relations are supported too
    following: {
      description: "Other users that this user is following.",
      type: new GraphQLList(User),
      // name the table that holds the two foreign keys
      junction: {
        sqlTable: 'relationships',
        sqlJoins: [
          // first the parent table to the junction
          (followerTable, junctionTable, args) => `${followerTable}.id = ${junctionTable}.follower_id`,
          // then the junction to the child
          (junctionTable, followeeTable, args) => `${junctionTable}.followee_id = ${followeeTable}.id`
        ]
      }
    },
    numLegs: {
      description: 'Number of legs this user has.',
      type: GraphQLInt,
      // data isn't coming from the SQL table? no problem! joinMonster will ignore this field
      resolve: () => 2
    }
  })
})

const Comment = new GraphQLObjectType({
  name: 'Comment',
  sqlTable: 'comments',
  uniqueKey: 'id',
  fields: () => ({
    // id and body column names are the same
    id: {
      type: GraphQLInt
    },
    body: {
      type: GraphQLString
    }
  })
})

const Post = new GraphQLObjectType({
  name: 'Post',
  sqlTable: 'posts',
  uniqueKey: 'id',
  fields: () => ({
    id: {
      type: GraphQLInt
    },
    body: {
      type: GraphQLString
    }
  })
})

export const QueryRoot = new GraphQLObjectType({
  name: 'Query',
  fields: () => ({
    // place this user type in the schema
    user: {
      type: User,
      // let client search for users by `id`
      args: {
        id: { type: GraphQLInt }
      },
      // how to write the WHERE condition
      where: (usersTable, args, context) => {
        if (args.id) return `${usersTable}.id = ${args.id}`
      },
      resolve: (parent, args, context, resolveInfo) => {
        // resolve the user and the comments and any other descendants in a single request and return the data!
        // all you need to pass is the `resolveInfo` and a callback for querying the database
        return joinMonster(resolveInfo, {}, sql => {
          // knex is a query library for SQL databases
          return knex.raw(sql)
        })
      }
    }
  })
})

Detailed instructions for set up are found in the docs.

Using with graphql-tools

The GraphQL schema language doesn't let you add arbitrary properties to the type definitions. If you're using something like the Apollo graphql-tools package to write your code with the schema language, you'll need an adapter. See the join-monster-graphql-tools-adapter if you want to use this with graphql-tools.

Running the Demo

$ git clone https://github.com/stems/join-monster-demo.git
$ cd join-monster-demo
$ npm install
$ npm start
# go to http://localhost:3000/graphql

# if you also want to run the paginated version, create postgres database from the dump provided
psql $YOUR_DATABASE < data/paginated-demo-dump.sql
DATABASE_URL=postgres://$USER:$PASS@$HOST/$YOUR_DATABASE npm start
# go to http://localhost:3000/graphql-relay

Explore the schema, try out some queries, and see what the resulting SQL queries and responses look like in our custom version of GraphiQL!

graphsiql

There's still a lot of work to do. Please feel free to fork and submit a Pull Request!

Future Work

  • Address this known bug #126.
  • Support custom ORDER BY expressions #138.
  • Support binding parameters #169.
  • Write static Flow types.
  • Support "lookup tables" where a column might be an enum code to look up in another table.
  • Support "hyperjunctions" where many-to-many relations can join through multiple junction tables.
  • Cover more SQL dialects, like MSSQL and DB2.

join-monster's People

Contributors

airhorns avatar alainnahalliday avatar alexbbt avatar azabujuban avatar benbro avatar bradzacher avatar bs1180 avatar bueche avatar dahannes avatar dependabot-preview[bot] avatar dependabot[bot] avatar dreamcodez avatar eedrah avatar glennmatthys avatar greenkeeper[bot] avatar greenkeeperio-bot avatar idangozlan avatar joserochadocarmo avatar jovincroninwilesmith avatar kamek-pf avatar langpavel avatar lorensr avatar matthewgonzalez avatar maxpain avatar sangheestyle avatar stickler-ci avatar theobat avatar vasdhara avatar wtho avatar zachrdz 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  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  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

join-monster's Issues

Joining on scalars rather than objects

Hi !

So here is my use case, I'm dealing with some fields that need to be translated in my database :

some_table
id int
some_value int
...
some_table_i18n
id int references some_table (id)
language varchar(2) ...
some_string varchar(100)
...

some_table holds all standard fields, some_table_i18n holds all fields that need to be translated, it has a reference to some_table.
Pretty standard way to handle translations in a relational database. My GraphQL schema is based on some_table.

My GraphQL API should let me do two things :

  • ask for the someString field and get a value translated in the user's language (by looking at HTTP headers)
  • ask for a someStringIntl field, an object that can return the different values of the string (this field will be used by a CMS)

A query could look like this :

...
{
    someTable {
       someValue // -> GraphQLInt
       someString // -> GraphQLString
       someStringIntl {
           en // -> GraphQLString
           fr // -> GraphQLString
       }
}
...

Maybe I missed something, but it looks like Join Monster does not support joins on scalars : I can't use sqlJoin if the type of my field is not GraphQLObjectType, and in the case of someString, it isn't (as it is just a string).

For now, I'm cheating with a view that simply merges both tables based on IDs. Doing so, everything looks like it's already in the table from Join Monster's point of view.
This resolves my first point, but I'm kind of stuck on the other.

Any idea how to tackle this ? :)

How to Order By using join table's column rather than main table for pagination

Below is my current query which sorts based on the main table EXM_EXPENSES. I want the user to supply the sort as "type_name" which should allow to sort on p.name on table EXM_EXPENSE_TYPES "p"

SELECT
  "a"."EXPENSE_ID" AS "b",
  "a"."LOCATION" AS "o",
  "p"."EXPENSE_TYPE_ID" AS "p__q",
  "p"."NAME" AS "p__r"
FROM (
  SELECT *
  FROM EXM_EXPENSES "a"
  ORDER BY "a"."START_DATE" DESC, "a"."CREATION_DATE" DESC
  FETCH FIRST 26 ROWS ONLY
) "a"
LEFT JOIN EXM_EXPENSE_TYPES "p" ON "a".EXPENSE_TYPE_ID = "p".EXPENSE_TYPE_ID
ORDER BY "a"."START_DATE" DESC, "a"."CREATION_DATE" DESC

Here is my query

{
 items (first:25) {
    pageInfo {
        hasNextPage
        startCursor
        endCursor
      }
      edges {
        cursor
        node {

      ExpenseId
      Location
    	Type {
        ExpenseTypeName
      }
         }
      }
    }
}

And below is my queryRoot. How do I pass the join column name as argument to the below query? Currently it only has sortKey: args =>, can we also pass the name of the join tables in here?

   items: {
        type: ExpenseConnection,
        args: {
           expenseSource: {
              type: GraphQLString 
           },
           sortingBy: {
              type: GraphQLString 
           },
           sortingOrder: {
              type: GraphQLString 
           }, 
           ...PAGINATE === 'offset' ? forwardConnectionArgs : connectionArgs
        },
        sqlPaginate: true,
        // use "keyset" pagination, an implementation based on a unique sorting key
        sortKey: args => {
        return {
          order: getSortOrder(args.sortingOrder),
          key: getSortColumn(args.sortingBy)
        }

Ignore column

Is there a way to ignore a field to not get into the sql query?

const StatusType = new GraphQLObjectType({
    name: 'Status',
    sqlTable: 'status',
    uniqueKey: 'id',
    description: 'description',
    fields: () => {
        return {
            id: {
                type: GraphQLInt,
            },
            name: {
                type: GraphQLString
            },
            customField: {
                sqlignore:true
                type: GraphQLString
            },
        };
    }
});

select id, name from status...and not select id, name,customField from status.

Thanks.

Provide data from many-to-many join tables.

We store data about a relationship between to entities in their cross join table. Consider a schema where there is a person, an organization and a personOrganization cross table. In the cross table we will store a role field. I would like to be able to discover that information with a graphql query such that:

{
  person {
    organization {
      role
    }
  }
}

Does this seem rational? It may be that I will have to define that property on the organization type and determine from the context of the lookup to query that cross table, and do this outside of Join Monster.

Where condition get table joined name

Hi !
I'm trying to add a where condition on a query, but I can't get my table name :

const ProductLang = new ObjectType({
  name: 'ProductLang',
  sqlTable: 'product_lang',
  uniqueKey: 'id',
  fields: {
    id: { type: new NonNull(ID) },
    product_id: { type: new NonNull(ID) },
    lang: {
      type: Lang,
      sqlJoin(productLangTable, langTable) {
        return `${langTable}.id = ${productLangTable}.lang_id`
      }
    },
    description: { type: StringType },
  },
});
const products = {
  type: new List(ProductLang),
  description: 'Return products',
  args: {
    lang: {
      type: StringType,
      defaultValue: 1
    }
  },
  where: (productTable, args, context) => {
    console.log('I would love to see langTable name',  productTable);
    //here I can log "products" but I want to get "lang" also
  }

How can I get my joined table name in the where condition ?
Ideally, my query should look like :

SELECT * FROM product_lang JOIN lang ON (product_lang.lang_id=lang.id) WHERE lang.name='fr'

Do you have any idea of how to get this name ?

looped\recursive query not working correctly

Hey guys, thanks for the hard work!

Say I have a simple schema for a forum which looks roughly like you would expect:

User {
    nickname: String
    threads: [Thread]
}

Thread {
    title: String
    text: String
    comments: [Comment]
    creator: User
}

Comment {
    commenter: User
    text: String
}

And say I want to show in my ui a specific thread with all its comments, for every comment to show the commenter, BUT! for every commenter, to show all his last threads when the user hovers over his name for example.

If I got graphql right, my query will look like this:

Thread(title: 'bla bla'){
    title,
    text,
    comments {
        text,
        commenter {
            nickname,
            threads {
                title
            }
        }
    }
}

(If this is a graphql anti-pattern please stop me here and I'll be really happy if you could describe why or link me with a solution)

When I tried to send a query like that to my join monster code, the problem was very simple -

Join monster adds WHERE title = 'bla bla' to the end of our sql query since we queried Thread(title:'bla bla'), so it won't return the threads of the commenters whos titles are different than 'bla bla'.

(even this is not exact since its not about the where clause its more about the join, which is now only on the threads we've filtered... I don't understand it well enough to even explain how this could be solved :) )

How can I solve this problem??

Migration from Sequelize

Hi, I've just discovered this package and I think it's awesome.
I'm using Sequelize as an ORM but I'd like to use join-monster for querying my database. How would I migrate from Sequelize to use join-monster ? Also, what to do with my already created resolvers for creating/updating/deleting ? Can I use them with join-monster ?

Incorrect where queries

If I add a where clause to a paginated query, I think it gets added to the wrong place in the resulting SQL.

For example, here's a simple filter on the demo dataset.

If I run the following query, I would expect to get all 5 users from the database, and those which don't have any matching posts would have edges: []. :

query {
  users(first: 10) {
    edges {
      node {
        id
        fullName
        posts {
          edges {
            node {
              id
            }
          }
        }
      }
    }
  }
}

Instead only three users are returned, the ones with matching posts.

This is the SQL output:

SELECT
  "users"."id" AS "id",
  "posts"."id" AS "posts__id",
  "posts"."created_at" AS "posts__created_at",
  "users"."first_name" AS "first_name",
  "users"."last_name" AS "last_name"
FROM (
  SELECT * FROM accounts
  WHERE TRUE
  ORDER BY id ASC
  LIMIT 11
) AS "users"
LEFT JOIN LATERAL (
  SELECT * FROM posts
  WHERE "users".id = posts.author_id
  ORDER BY created_at DESC, id DESC
  LIMIT ALL
) AS "posts" ON "users".id = "posts".author_id
WHERE "posts".created_at < '2015-12-15'::date
ORDER BY "users"."id" ASC, "posts"."created_at" DESC, "posts"."id" DESC

I think the where clause is in the wrong place - it should be within the lateral join. I can't just expand the sqlJoin definition because it doesn't have access to the query args or context.

nest expects param data to be in the form of a plain object or an array of plain objects (forming a table)

So, I'm in trouble on the return knex.raw, what am I doing wrong?, always returns the error below...
My knex is ok, and return the data...Thanks, this software help me a lot!!!!

const CursoType = new GraphQLObjectType({
  name: 'Curso',
  sqlTable: 'curso',
  uniqueKey: 'id',
  description: 'Isto representa um curso',
  fields: () => ({
    id: {
      type: GraphQLInt
    },
    name: {
      type: GraphQLString
    }
  })
})

RootQuery ...
cursos: {
      description: 'Isto representa um curso',
      type: new GraphQLList(CursoType),
      resolve: (parent, args, context, ast) => {
        return joinMonster(ast, context, sql => {
          return knex.raw(sql)
        })
      }
    }
{
  "data": {
    "academic": {
      "cursos": null
    }
  },
  "errors": [
    {
      "message": "nest expects param data to be in the form of a plain object or an array of plain objects (forming a table)",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ]
    }
  ]
}

When I add count field and add sqlExpr it gives me error "TypeError: Cannot read property 'length' of undefined"

I have a Type like below

const CashExpense = new GraphQLObjectType({
  description: 'Cash Expense Items',
  name: 'CashExpense',
  fields: () => ({
    ExpenseItem: {
      type: new GraphQLList(Expense),
      description: 'List of cash expense items.',
    },
    Count: {
      type: GraphQLInt,
      description: 'Count of Cash Expense Items',
      sqlExpr: table => `(SELECT count(*) FROM XXX WHERE EXPENSE_SOURCE = 'CASH')`
    }
  })
})
export default CashExpense

which used below Type

const Expense = new GraphQLObjectType({
  description: 'Expense information',
  name: 'Expense',
  sqlTable: 'XXX',
  uniqueKey: 'ID',
  fields: () => ({
    ExpenseId: {
      type: GraphQLFloat,
      description: 'Unique identifier of a specific expense item.',
      sqlColumn: 'D'
    },
    PersonId: {
      type: GraphQLFloat,
      description: 'Person identifier of the individual on the corporate card.',
      sqlColumn: 'PERSON_ID'
    },
.....

And my queryroot is like below

....
cashItems: {
      type: CashExpense,     
      resolve: (parent, args, context, resolveInfo) => {
        // joinMonster with handle batching all the data fetching for the users and it's children. Determines everything it needs to from the "resolveInfo", which includes the parsed GraphQL query AST and your schema definition
        return joinMonster(resolveInfo, context, sql => dbCall(sql, knex, context))
      }
    },
....

And my GraphQL query is as below

{
  cashItems { 
    Count
    ExpenseItem {
      ExpenseId
      PersonId
...
    }
  }
}

I get below error in console. Also it does not print anything when I start in debug mode.

{ TypeError: Cannot read property 'length' of undefined
    at pruneDuplicateSqlDeps (/node_modules/join-monster/dist/queryASTToSqlAST.js:317:24)
    at Object.queryASTToSqlAST (/node_modules/join-monster/dist/queryASTToSqlAST.js:37:3)
    at /node_modules/join-monster/dist/index.js:9:29
    at Generator.next (<anonymous>)
    at step (/node_modules/join-monster/dist/index.js:84:191)
    at /node_modules/join-monster/dist/index.js:84:437
    at /node_modules/join-monster/dist/index.js:84:99
    at joinMonster (/node_modules/join-monster/dist/index.js:19:17)
    at resolve (/schema/QueryRoot.js:34:16)
    at resolveOrError (/node_modules/graphql/execution/execute.js:454:12)
    at resolveField (/node_modules/graphql/execution/execute.js:445:16)
    at /node_modules/graphql/execution/execute.js:252:18
    at Array.reduce (native)
    at executeFields (/node_modules/graphql/execution/execute.js:249:42)
    at executeOperation (/node_modules/graphql/execution/execute.js:190:10)
    at /node_modules/graphql/execution/execute.js:116:13
  message: 'Cannot read property \'length\' of undefined',
  locations: [ { line: 15, column: 3 } ],
  path: [ 'cashItems' ] }

Lists of GraphQLNonNull items not working

Part of schema:

const QueryRoot = new GraphQLObjectType({
	name: 'Query',
	fields: () => ({
		friends: {
			type: new GraphQLList(new GraphQLNonNull(Friend))
		}
	})
})

Error:

{
  "data": null,
  "errors": [
    {
      "message": "Cannot read property 'length' of undefined",
      "locations": [
        {
          "line": 8,
          "column": 3
        }
      ],
      "path": [
        "friends"
      ]
    }
  ]
}

Stacktrace:

  API TypeError: Cannot read property 'length' of undefined
  API     at pruneDuplicateSqlDeps (/Users/maxpain/dev/projects/fastcup/backend/node_modules/join-monster/dist/queryASTToSqlAST.js:263:24)
  API     at Object.queryASTToSqlAST (/Users/maxpain/dev/projects/fastcup/backend/node_modules/join-monster/dist/queryASTToSqlAST.js:35:3)
  API     at /Users/maxpain/dev/projects/fastcup/backend/node_modules/join-monster/dist/index.js:9:29
  API     at Generator.next (<anonymous>)
  API     at step (/Users/maxpain/dev/projects/fastcup/backend/node_modules/join-monster/dist/index.js:117:191)
  API     at /Users/maxpain/dev/projects/fastcup/backend/node_modules/join-monster/dist/index.js:117:437
  API     at /Users/maxpain/dev/projects/fastcup/backend/node_modules/join-monster/dist/index.js:117:99
  API     at joinMonster (/Users/maxpain/dev/projects/fastcup/backend/node_modules/join-monster/dist/index.js:17:17)
  API     at resolve (/Users/maxpain/dev/projects/fastcup/backend/.build/webpack:/src/schemas/index.js:71:12)
  API     at resolveOrError (/Users/maxpain/dev/projects/fastcup/backend/node_modules/graphql/execution/execute.js:474:12)
  API     at resolveField (/Users/maxpain/dev/projects/fastcup/backend/node_modules/graphql/execution/execute.js:460:16)
  API     at /Users/maxpain/dev/projects/fastcup/backend/node_modules/graphql/execution/execute.js:274:18
  API     at Array.reduce (native)
  API     at executeFields (/Users/maxpain/dev/projects/fastcup/backend/node_modules/graphql/execution/execute.js:271:42)
  API     at executeOperation (/Users/maxpain/dev/projects/fastcup/backend/node_modules/graphql/execution/execute.js:212:10)
  API     at /Users/maxpain/dev/projects/fastcup/backend/node_modules/graphql/execution/execute.js:120:13
  API     at Object.execute (/Users/maxpain/dev/projects/fastcup/backend/node_modules/graphql/execution/execute.js:119:10)
  API     at doRunQuery (/Users/maxpain/dev/projects/fastcup/backend/node_modules/apollo-server/src/core/runQuery.ts:126:16)
  API     at resolvedPromise.then (/Users/maxpain/dev/projects/fastcup/backend/node_modules/apollo-server/src/core/runQuery.ts:59:39)
  API     at process._tickCallback (internal/process/next_tick.js:103:7)

Implement GROUP BY

Implement the GROUP BY strategy brainstormed earlier to subquery using the sqlJoin as the where and taking user-specified grouping columns and aggregate functions on the field.

Custom resolving of child object that maps to SQL table

I'm getting the following error

If an Object type maps to a SQL table and has a child which is 
another Object type that also maps to a SQL table, you must define \"sqlJoin\", \"sqlBatch\", or \"junctionTable\" 
on that field to tell joinMonster how to fetch it. Check the \"otherUser\" field on the \"Relationship\" type.

However in this case my intention is to define a completely custom resolver for the child, despite the fact that it is another object type which also maps to a SQL table. Is that possible?

Basically I'd like to get joinMonster to ignore a specific field and let me resolve it myself.

Arrays in query arguments become undefined

It seems that Join Monster doesn't support list values in the query arguments that get passed to the where condition - they become undefined.

For example:

export default new GraphQLObjectType({
  name: 'Query',
  fields: () => ({
    users: {
      type: new GraphQLList(User),
      args: {
        ids: {
          type: new GraphQLList(GraphQLInt)
        }
      },
      where: (usersTable, args) => {
        // args is now { ids: undefined }
        if (args.ids) {
          return `${usersTable}.id IN ${args.ids}`
        }
      },
      resolve: (root, args, context, resolveInfo) => {
        // args is { ids: [1, 2] } at this point
        return joinMonster(resolveInfo, context, sql => knex.raw(sql))
      }
    }
  })
})

And this, otherwise valid, query:

query {
  users(ids: [1, 2]) {
    id
    name
  }
}

How to append sql expression at the root level

I want to add "OFFSET ${args.offset} ROWS FETCH NEXT ${args.count} ROWS ONLY" at the end of the sql generated by joinMonster for the list of invoices. This is a workaround for pagination as its not supported for oracledb. The below is not working. Basically I want to generate the sql

select * from invoices ORDER BY invoice_id OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY
var fieldTypes = ({
    version: {
      type: GraphQLString,
      resolve: () => joinMonster.version
    },
    invoices: {
      type: new GraphQLList(Invoice),
       args: {
        offset: {
          description: 'offset',
          type: new GraphQLNonNull(GraphQLInt)
        },
        count: {
          description: 'count',
          type: new GraphQLNonNull(GraphQLInt)
        }
      },
      sqlExpr: (table, args) => ` OFFSET ${args.offset} ROWS FETCH NEXT ${args.count} ROWS ONLY`,
      resolve: (parent, args, context, resolveInfo) => {
        // joinMonster with handle batching all the data fetching for the users and it's children. Determines everything it needs to from the "resolveInfo", which includes the parsed GraphQL query AST and your schema definition
        return joinMonster(resolveInfo, context, sql => dbCall(sql, knex, context))
      }
    }

Issue with apollo client.

I tried query with apollo client for graphql server that use join-monster and I got this error:

"GraphQL error: Cannot read property 'type' of undefined"

error

Not working with my own setup and join-monster-demo. I don't know this is problem with join monster or apollo client itself.

My apollo code look like this:

class App extends React.Component {
    render() {
         const { users } = this.props.data;
         console.log(users)
         return (
                // view here
         );
   }
}

const USER = gql`
{
  users {
    email
    fullName
  }
}
`;

const withData = graphql(USER)(App);

How to get the total using offset Pagination?

I am using the offset pagination

invoicesByOffset: {
      type: InvoiceConnection,
      args: connectionArgs,
      sqlPaginate: true,
      orderBy: {
        INVOICE_ID: 'desc'
      },
      resolve: async (parent, args, context, resolveInfo) => {
           return joinMonster(resolveInfo, context, sql => dbCall(sql, knex, context), options)
      } 

I also have below in the Invoice Type

const connectionConfig = { nodeType: Invoice }
if (PAGINATE === 'offset') {
  connectionConfig.connectionFields = {
    total: { type: GraphQLInt }
  }
}

I tried below to get the count but did not work. Can total be only retrieved programmatically?

{
   invoicesByOffset (first:2) {      
    pageInfo {
        hasNextPage
        startCursor
        endCursor
      }
      edges {
        cursor
        node {  
      	total
    		invoice_id
    		invoice_num
        invoice_amount
        allLines {
          line_number
        }
        }
      }
    }
}

Promises in where queries

If I use a promise in a where query, it gets stringified:

where: async (courseTable, args, { viewer }) => {
  await permissionsCheck(viewer)
  return `${courseTable}.owner_id = viewer.id`
}
where: (courseTable, args, { viewer }) => {
   return Promise.resolve('')
}

Gets turned into:
...WHERE "course".id = 1 AND [object Promise]\nORDER BY ...

Thanks again for all your work on this.

Whats the purpose of refetching the id in nodeDefenitions?

@acarl005 Amazing staff! Thanks for join-monster

But I don't get why there's a need to refetch the id in nodeDefinitions

const { nodeInterface, nodeField } = nodeDefinitions(
  // this function resolves an ID to an object
  (globalId, context, resolveInfo) => {
    // parse the globalID
    const { type, id } = fromGlobalId(globalId)
 
    return joinMonster.getNode(type, resolveInfo, context,
      table => `${table}.id = ${id}`,    // why? 
      sql => {
        if (context) {
          context.set('X-SQL-Preview', sql.replace(/\n/g, '%0A'))
        }
        return knex.raw(sql)
      },
      options
    )
  },
  // this function determines the type. `joinMonster` figures it out for you and attaches the type to the result in the "__type__" property
  obj => obj.__type__
)

Why not just return the type since we know it already?

const { nodeInterface, nodeField } = nodeDefinitions(
  // this function resolves an ID to an object
  (globalId, context, resolveInfo) => {
const { type, id } = fromGlobalId(globalId);
return FunctionThatGetsObject(type)   // returns e.g UserType
} 
.... 

Limit & Offset is not applied on complete join query

Hi Team,
Started exploring join monster for one of our project. It's very easy and pretty happy with the performance too!!

We are facing some challenges in applying limit & offset. Need your help!!

We have a GraphQL Query as below:

 {
  orders(first: 10){
    edges {
      node {
        id
        name
        order_items( filter: "{ \"type\": { \"$in\": [ \"PC\"]}}" ) {
          name
          id
        }
      }
    }
  }
}

In above GraphQL Query, order_items: filter is in json format which is converted to SQL and used in where condition of order_lines.

SQL Generated As Below:

SELECT
  "orders"."id" AS "id",
  "orders"."$total" AS "$total",
  "orders"."name" AS "name", 
  "order_items"."id" AS "order_items__id",
  "order_items"."name" AS "order_items__name"
FROM (
  SELECT *, count(*) OVER () AS "$total"
  FROM eds.orders
  WHERE TRUE
  ORDER BY "id" DESC
  LIMIT 11 OFFSET 0
) "orders"
LEFT JOIN sh.order_items "order_items" ON "orders".id = "order_items".order_id
WHERE type in ( 'PC' )
ORDER BY "orders"."id" DESC

In above generated SQL query from join monster, first we are applying order by, limit & offset on orders table and then we are applying left join on the second table i.e order_items and condition.

We want to apply limit, order by on both orders and order_items table.
Please let us know how to achieve that or any other alternate way.

Order By without Relay dependency

How hard would it be to add support for Order By without the relay dependency?

My use case is pretty simple, I have a bunch of states for an entity I would order them based on their creation date. Right now I don't see how to do that decently (I could annotate the query generated by join monster but that sounds like a bad idea).

I feel like you don't need relay to support pagination in an app, and pagination is not the only use case for an ORDER BY in a join query anyway. I'd be happy to submit a PR if there is no red flag.

Composite uniqueKey not working

Example: user has a friends list (join), when user has no friends, gql server still returns object with empty (null) fields in friends array.

Friends postgres schema (user_id and friend_id โ€” primary keys):

CREATE TABLE "public"."friends" (
	"user_id" int4 NOT NULL,
	"friend_id" int4 NOT NULL,
	"date" timestamp(6) NOT NULL DEFAULT now(),
	"state" "public"."friends_state" NOT NULL
)

This query:

query {
  user {
    id
    nickName
    friends {
      date
      user {
        id
      }
    }
  }
}

returns this response:

{
  "data": {
    "user": {
      "id": 251586469,
      "nickName": "MAXPAIN",
      "friends": [
        {
          "date": null,
          "user": null
        }
      ]
    }
  }
}

GraphQL schema and resolvers:

User.js

import {
	GraphQLObjectType,
	GraphQLList,
	GraphQLString,
	GraphQLInt
} from 'graphql'

import Friend from '../friend'

export default new GraphQLObjectType({
	description: 'A user type',
	name: 'User',
	sqlTable: 'users',
	uniqueKey: 'id',
	fields: () => ({
		id: {
			type: GraphQLInt
		},
		nickName: {
			type: GraphQLString,
			sqlColumn: 'nick_name'
		},
		friends: {
			description: 'A list of user friends',
			type: new GraphQLList(Friend),
			sqlJoin: (userTable, friendTable) => `${friendTable}.user_id = ${userTable}.id`
		}
	})
})

Friend.js

import {
	GraphQLObjectType,
	GraphQLString
} from 'graphql'

import User from '../user'

export default new GraphQLObjectType({
	description: 'A friend type',
	name: 'Friend',
	sqlTable: 'friends',
	uniqueKey: ['user_id', 'friend_id'],
	fields: () => ({
		user: {
			type: User,
			sqlJoin: (friendTable, userTable) => `${userTable}.id = ${friendTable}.friend_id`
		},
		date: {
			type: GraphQLString
		}
	})
})

Dump:

  API Tue, 24 Jan 2017 15:52:53 GMT join-monster
  API SQL_AST
  API  { type: 'table',
  API   name: 'users',
  API   as: 'user',
  API   fieldName: 'user',
  API   grabMany: false,
  API   where: [Function: where],
  API   children:
  API    [ { type: 'column', name: 'id', fieldName: 'id', as: 'id' },
  API      { type: 'column', name: 'id', fieldName: 'id', as: 'id' },
  API      { type: 'column',
  API        name: 'nick_name',
  API        fieldName: 'nickName',
  API        as: 'nick_name' },
  API      { type: 'table',
  API        name: 'friends',
  API        as: 'friends',
  API        fieldName: 'friends',
  API        grabMany: true,
  API        sqlJoin: [Function: sqlJoin],
  API        children:
  API         [ { type: 'composite',
  API             name: [ 'user_id', 'friend_id' ],
  API             fieldName: 'user_id#friend_id',
  API             as: 'user_id#friend_id' },
  API           { type: 'column', name: 'date', fieldName: 'date', as: 'date' },
  API           { type: 'table',
  API             name: 'users',
  API             as: 'user$',
  API             fieldName: 'user',
  API             grabMany: false,
  API             sqlJoin: [Function: sqlJoin],
  API             children:
  API              [ { type: 'column', name: 'id', fieldName: 'id', as: 'id' },
  API                { type: 'column', name: 'id', fieldName: 'id', as: 'id' },
  API                { type: 'columnDeps', names: {} } ] },
  API           { type: 'columnDeps', names: {} } ] },
  API      { type: 'columnDeps', names: {} } ] }
  API  +176ms
  API Tue, 24 Jan 2017 15:52:53 GMT join-monster
  API SQL
  API  SELECT
  API   "user"."id" AS "id",
  API   "user"."nick_name" AS "nick_name",
  API   CONCAT("friends"."user_id", "friends"."friend_id") AS "friends__user_id#friend_id",
  API   "friends"."date" AS "friends__date",
  API   "user$"."id" AS "friends__user$__id"
  API FROM users AS "user"
  API LEFT JOIN friends AS "friends" ON "friends".user_id = "user".id
  API LEFT JOIN users AS "user$" ON "user$".id = "friends".friend_id
  API WHERE "user".id = 251586469
  API  +4ms
  API Tue, 24 Jan 2017 15:52:53 GMT join-monster
  API SHAPE_DEFINITION
  API  { id: 'id',
  API   nickName: 'nick_name',
  API   friends:
  API    [ { 'user_id#friend_id': 'friends__user_id#friend_id',
  API        date: 'friends__date',
  API        user: { id: 'friends__user$__id' } } ] }
  API  +1ms
  API Tue, 24 Jan 2017 15:52:53 GMT join-monster
  API RAW DATA
  API  [ anonymous {
  API     id: 251586469,
  API     nick_name: 'MAXPAIN',
  API     'friends__user_id#friend_id': '',
  API     friends__date: null,
  API     'friends__user$__id': null } ]
  API  +60ms

sqlPaginate not query LIMIT AND OFFSET STATEMENT.

I have set sqlPaginate to true but look like LIMIT and OFFSET STATEMENT not run. Any missing part on my code below:

// connectionArgs import from graphql-relay but I not using relay at all.
 comments: {
           sqlColumn: 'comments',
           args: connectionArgs,
           sqlPaginate: true,
           orderBy: 'id',
           type: CommentType,
           sqlJoin: (todoTable, commentTable) => `${todoTable}.id = ${commentTable}.todo_id`
}

Query (console.log):

SELECT
  "todos"."id" AS "id",
  "todos"."task" AS "task",
  "comments"."id" AS "comments__id",
  "comments"."title" AS "comments__title"
FROM todos AS "todos"
LEFT JOIN comments AS "comments" ON "todos".id = "comments".todo_id

Dialect: pg

GraphQL Fragments and Relay Connections (pagination)

Relay connections fields not supports fragmentation.

GraphQL Query:

query {
  friends(first: 2, state: INCOMING) {
  	...connectionFragment
  }
}

fragment connectionFragment on FriendConnection {
  edges {
    node {
      id
      date
      user {
        ...CommonUserInfo
      }
    }
  }
}

fragment CommonUserInfo on User {
	id
	nickName
	avatarUrl
}

Throws this error:

{
  "data": {
    "friends": {
      "edges": [
        {
          "node": null
        }
      ]
    }
  },
  "errors": [
    {
      "message": "Cannot return null for non-nullable field Friend.user.",
      "locations": [
        {
          "line": 12,
          "column": 7
        }
      ],
      "path": [
        "friends",
        "edges",
        0,
        "node",
        "user"
      ]
    }
  ]
}

But, without fragments, it's works:

query {
  friends(first: 2, state: INCOMING) {
  	edges {
          node {
             id
             date
             user {
               ...CommonUserInfo
             }
         }
       }
  }
}

fragment CommonUserInfo on User {
	id
	nickName
	avatarUrl
}
{
  "data": {
    "friends": {
      "edges": [
        {
          "node": {
            "id": "251586469_123338061",
            "date": "Sun Feb 05 2017 13:00:25 GMT+0300 (MSK)",
            "user": {
              "id": "123338061",
              "nickName": "ะ ัƒััŒะบะฐ",
              "avatarUrl": "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/b2/b293016bb375f0fab5e11c038298620c441f810b_full.jpg"
            }
          }
        }
      ]
    }
  }
}

Source code:

friends: {
	type: FriendConnection,
	sqlPaginate: true,
	sortKey: {
		order: 'DESC',
		key: 'date'
	},
	args: {
		...connectionArgs,
		id: {
			description: 'Steam AccountID',
			type: GraphQLInt
		},
		state: {
			description: 'Current state of friend',
			type: FriendsState
		}
	},

	where: (friendsTable, { id, state }, { user }) => `
		${friendsTable}.user_id = ${id || user.id}
		${state === 'ALL' ? '' : `AND ${friendsTable}.state = '${state || 'ACCEPTED'}'`}
	`,

	resolve(parent, args, context, resolveInfo) {
		if (!args.id && !context.user) {
			return
		}

		return joinMonster(resolveInfo, context, sql =>
			postgres.query(sql)
		, options)
	}
}

Field's resolvers not working

const Match = new GraphQLObjectType({
	description: 'A match type',
	name: 'Match',
	sqlTable: 'matches',
	uniqueKey: 'id',
	fields: () => ({
		id: { type: new GraphQLNonNull(GraphQLID) },
		members: {
			type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(MatchMember))),
			// sqlJoin: (matchesTable, matchMembersTable) =>
			// 	`${matchMembersTable}.match_id = ${matchesTable}.id`

			where: (matchMembersTable, args, { matchID }) => `
				${matchMembersTable}.match_id = ${matchID}
			`,

			resolve(root, args, context, resolveInfo) {
				context.matchID = root.id

				return joinMonster(resolveInfo, context, sql =>
					postgres.query(sql)
				, { dialect: 'pg' })
			}
		}
      })
})

Query:

{
  match {
    id
    members {
      id
    }
  }
}

Error:

{
  "data": {
    "match": null
  },
  "errors": [
    {
      "message": "syntax error at or near \"FROM\"",
      "locations": [
        {
          "line": 8,
          "column": 3
        }
      ],
      "path": [
        "match"
      ]
    }
  ]
}

Logs:

SQL_AST
  API  { type: 'table',
  API   name: 'matches',
  API   as: 'match',
  API   fieldName: 'match',
  API   grabMany: false,
  API   where: [Function: where],
  API   children:
  API    [ { type: 'column', name: 'id', fieldName: 'id', as: 'id' },
  API      { type: 'column', name: 'id', fieldName: 'id', as: 'id' },
  API      { type: 'table',
  API        name: 'match_members',
  API        as: 'members',
  API        fieldName: 'members',
  API        grabMany: true,
  API        where: [Function: where],
  API        children:
  API         [ { type: 'composite',
  API             name: [ 'match_id', 'user_id' ],
  API             fieldName: 'match_id#user_id',
  API             as: 'match_id#user_id' },
  API           { type: 'columnDeps',
  API             names: { match_id: 'match_id', user_id: 'user_id' } } ] },
  API      { type: 'columnDeps', names: {} } ] }
  API  +848ms
  API Thu, 16 Feb 2017 16:59:41 GMT join-monster
  API SQL
  API  SELECT
  API   "match"."id" AS "id",
  API   NULLIF(CONCAT("members"."match_id", "members"."user_id"), '') AS "members__match_id#user_id",
  API   "members"."match_id" AS "members__match_id",
  API   "members"."user_id" AS "members__user_id"
  API FROM matches AS "match"
  API FROM match_members AS "members"
  API WHERE "match".id = 9 AND
  API 				"members".match_id = undefined
  API
  API  +282ms
  API Thu, 16 Feb 2017 16:59:41 GMT join-monster
  API SHAPE_DEFINITION
  API  { id: 'id',
  API   members:
  API    [ { 'match_id#user_id': 'members__match_id#user_id',
  API        match_id: 'members__match_id',
  API        user_id: 'members__user_id' } ] }
  API  +1ms
  API error: syntax error at or near "FROM"
  API     at Connection.parseE (/Users/maxpain/dev/projects/fastcup/backend/node_modules/pg/lib/connection.js:554:11)
  API     at Connection.parseMessage (/Users/maxpain/dev/projects/fastcup/backend/node_modules/pg/lib/connection.js:381:17)
  API     at Socket.<anonymous> (/Users/maxpain/dev/projects/fastcup/backend/node_modules/pg/lib/connection.js:117:22)
  API     at emitOne (events.js:96:13)
  API     at Socket.emit (events.js:189:7)
  API     at readableAddChunk (_stream_readable.js:176:18)
  API     at Socket.Readable.push (_stream_readable.js:134:10)
  API     at TCP.onread (net.js:551:20)
  API  +58ms

Authorization for specific fields/nodes

In my case some users are private or public. If the user is private only accepted followers can view their content. Some fields such as email are only accessed by the creator.
So before using join-monster my authorization logic looked like this:

// UserType fields 

posts: { 
     type: new GraphQLList(PostType), 
     resolve(type, {viewer}) => getPosts(viewer, type.id) 
 },
email: { 
     type: GraphQLString, 
     resolve(type, {viewer}) => getEmail(viewer, type.id) 
 },
...

getPosts would check if viewer is allowed to see content (public/private) and getEmail would check if viewer is the owner. This allows to easily control what data is called from the database.

Since join-monster gathers all fields together and creates one query, it's hard to control what data can be accessed and I haven't found any API for it. Do you have any suggestions?

NB: I am using Relay.js

Getting null in the nested object data

I am using oracle db as database using oracledb driver.

I am querying the invoice table and also getting the batch table information by joining batch_id from invoice table and batch_id from batch table.

Here is the my root level object that calls the joinmonster.

import joinMonster from 'join-monster'
import knex from './database'
import dbCall from '../data/fetch'
import Invoice from './Invoice'

var fieldTypes = ({
    version: {
      type: GraphQLString,
      resolve: () => joinMonster.version
    },
    invoices: {
      type: new GraphQLList(Invoice),
      resolve: (parent, args, context, resolveInfo) => {
        // joinMonster with handle batching all the data fetching for the users and it's children. Determines everything it needs to from the "resolveInfo", which includes the parsed GraphQL query AST and your schema definition
        return joinMonster(resolveInfo, context, sql => dbCall(sql, knex, context))
      }
    },
    invoice: {
      type: Invoice,
      args: {
        id: {
          description: 'Invoice Id',
          type: new GraphQLNonNull(GraphQLInt)
        }
      },
      // this function generates the WHERE condition
      where: (invoicesTable, args, context) => { // eslint-disable-line no-unused-vars
        return `${invoicesTable}.invoice_id = ${args.id}`
      },
      resolve: (parent, args, context, resolveInfo) => {
        return joinMonster(resolveInfo, context, sql => dbCall(sql, knex, context))
      }
    }
  })
  
export default new GraphQLObjectType({
  description: 'global query object',
  name: 'Query',
  fields: () => fieldTypes
})

And here is my Invoice object

import {
  GraphQLObjectType,
  GraphQLList,
  GraphQLString,
  GraphQLInt,
  GraphQLFloat
} from 'graphql'

import knex from './database'
// import Comment from './Comment'
// import Post from './Post'
import { sortBy } from 'lodash'

const { STRATEGY } = process.env

const Batch = new GraphQLObjectType({
  description: 'batch for the invoice ID',
  name: 'Batch',
  // tell join monster the expression for the table
  uniqueKey: 'BATCH_ID',
  sqlTable: 'BATCHES',
  // one of the columns must be unique for deduplication purposes
  fields: () => ({
    batch_id: {
      // no `sqlColumn` and no `resolve`. assumed that the column name is the same as the field name: id
      type: GraphQLInt,
      sqlColumn: 'BATCH_ID'
    },
    batch_name: {
      // no `sqlColumn` and no `resolve`. assumed that the column name is the same as the field name: id
      type: GraphQLString,
      sqlColumn: 'BATCH_NAME',
     // resolve: batch => "hello"
    },
    created_by: {
      type: GraphQLString,
      // specify the SQL column
      sqlColumn: 'CREATED_BY'
    }
  })
})

const Invoice = new GraphQLObjectType({
  description: 'Invoice information',
  name: 'Invoice',
  // tell join monster the expression for the table
  sqlTable: 'INVOICES',
  // one of the columns must be unique for deduplication purposes
  uniqueKey: 'INVOICE_ID',
  fields: () => ({
    invoice_id: {
      // no `sqlColumn` and no `resolve`. assumed that the column name is the same as the field name: id
      type: GraphQLInt,
      sqlColumn: 'INVOICE_ID'
    },
    invoice_num: {
      type: GraphQLString,
      // specify the SQL column
      sqlColumn: 'INVOICE_NUM'
    },
    invoice_amount: {
      description: 'INVOICE_AMOUNT',
      type: GraphQLString,
      sqlColumn: 'INVOICE_AMOUNT',
    },
    invoice_currency_code: {
      description: 'currency code',
      type: GraphQLString,
      // depends on multiple SQL columns
      sqlColumn: 'INVOICE_CURRENCY_CODE',
    },
    invoice_currency_code: {
      description: 'currency code',
      type: GraphQLString,
      // depends on multiple SQL columns
      sqlColumn: 'INVOICE_CURRENCY_CODE',
    },
    batches: {
      description: 'Batch information',
      type: Batch,
      sqlJoin: (invoiceTable, batchTable, args, context) => `${invoiceTable}."BATCH_ID" = ${batchTable}."BATCH_ID"`
    }
  })
})

export default Invoice

I get the batch information as null. see below screenshots

screen shot 2017-02-24 at 1 50 53 pm

screen shot 2017-02-24 at 1 50 59 pm

The sql query is as below that printed inside the joinmonster that is correct and returns data when I run it against Oracle db

SELECT
invoice.INVOICE_ID AS INVOICE_ID,
invoice.INVOICE_NUM AS INVOICE_NUM,
invoice.INVOICE_AMOUNT AS INVOICE_AMOUNT,
invoice.INVOICE_CURRENCY_CODE AS INVOICE_CURRENCY_CODE,
batches.BATCH_ID AS batches__BATCH_ID,
batches.BATCH_NAME AS batches__BATCH_NAME
FROM INVOICES invoice
LEFT JOIN BATCHES batches ON invoice.BATCH_ID = batches.BATCH_ID
WHERE invoice.invoice_id = 183517

screen shot 2017-02-24 at 1 53 18 pm

Highlight the drawback on fetching one to many relations

I think it would be fair to mention that whenever join-monster is joining a one to many, it will retrieve excessive data that will be thrown out and it gets pretty bad as soon as you nest them.

For example, in the case of user <-> comments, user data is duplicated for each comment. If you have user <-> comments <-> ratings where a given user have 10 comments and each comments have 20 ratings, the query will actually fetch for the given user:

  • user data 200 times
  • each comment, 20 times

getting the count of the records using join-monster for root level type

This is a very common use case in every UI that we have.

UseCase - Get the count of the records along with the records.

{
  Items (itemSource:"Cash"{ 
    Count
    expenseItems {
       ExpenseId
       PersonId
    #...
  }
}

I need the below way (sqlColumn: 'count(*)') to get the count. Is this supported?

const ExpenseCount = new GraphQLObjectType({
  description: 'Cash Expense Items',
  name: 'ExpenseCount',
  sqlTable: 'EXM_EXPENSES',
  uniqueKey: 'EXPENSE_ID',
  fields: () => ({
    Count: {
      type: GraphQLInt,
      description: 'Count of Expense Items',
      sqlColumn: 'count(*)'
    }
  })
})

I tried in different ways but none works. Not sure if join-monster supports the count at root type?

Approach 1:
Tried adding the count to the Connection and it gives me an error. Also it does not print the arguments in the below block which is passed in the graphql query

Const connectionConfig = { nodeType: Expense }
if (PAGINATE === 'offset') {
  connectionConfig.connectionFields = {
    Count: {
      type: GraphQLInt,
      description: 'Count of Cash Expense Items',
      sqlExpr: table => `(SELECT count(*) FROM EXM_EXPENSES WHERE EXPENSE_SOURCE = 'CASH')`,
      resolve: async (parent, args, context, resolveInfo) => {
          console.log("print in the connection: " + args.expenseSource)
          return joinMonster(resolveInfo, context, sql => dbCall(sql, knex, context), options)
        } 
    }
  }
}
const { connectionType: ExpenseConnection } = connectionDefinitions(connectionConfig)
export { Expense, ExpenseConnection }

Error that I get is.

{ AssertionError: Must call joinMonster in a resolver on a field where the type is decorated with "sqlTable".
    at Object.queryASTToSqlAST (node_modules/join-monster/dist/queryASTToSqlAST.js:37:20)
    at node_modules/join-monster/dist/index.js:9:29
    at Generator.next (<anonymous>)
    at step (node_modules/join-monster/dist/index.js:85:191)
    at node_modules/join-monster/dist/index.js:85:437
    at node_modules/join-monster/dist/index.js:85:99
    at joinMonster (node_modules/join-monster/dist/index.js:19:17)
    at schema/Expense.js:167:18
    at Generator.next (<anonymous>)
    at step (schema/Expense.js:50:191)
    at schema/Expense.js:50:437
    at schema/Expense.js:50:99
    at resolve (schema/Expense.js:165:7)
    at resolveOrError (node_modules/graphql/execution/execute.js:454:12)
    at resolveField (node_modules/graphql/execution/execute.js:445:16)
    at node_modules/graphql/execution/execute.js:252:18
  message: 'Must call joinMonster in a resolver on a field where the type is decorated with "sqlTable".',
  locations: [ { line: 17, column: 5 } ],
  path: [ 'items', 'Count' ] }

Approach 2:
Created a new Type just for count

const ExpenseCount = new GraphQLObjectType({
  description: 'Cash Expense Items',
  name: 'ExpenseCount',
  sqlTable: 'EXM_EXPENSES',
  uniqueKey: 'EXPENSE_ID',
  fields: () => ({
    Count: {
      type: GraphQLInt,
      description: 'Count of Expense Items',
      sqlExpr: table => `(SELECT count(*) FROM EXM_EXPENSES)`
    }
  })
})

Here is my queryroot

    countItems: {
        type: ExpenseCount,
        args: {
          expenseSource: {
              type: GraphQLString 
           }
        },
        where: (itemsTable, args, context) => { // eslint-disable-line no-unused-vars
              return `${itemsTable}.EXPENSE_SOURCE = 'CASH'
          }

It gives me wrong count as the below query is not formed correct. I need to append the where clause to this sqlExpr but it does inner query.

SELECT
  "a"."EXPENSE_ID" AS "b",
  (SELECT count(*) FROM EXM_EXPENSES) AS "c"
FROM EXM_EXPENSES "a"
WHERE "a".EXPENSE_SOURCE = 'CASH'

I want this query to be executed

SELECT count(*) FROM EXM_EXPENSES a WHERE "a".EXPENSE_SOURCE = 'CASH'

SQL Injection in arguments

In the example below, if the argument gameID includes the sql injection, it will be included inside final SQL query.

serverMaps: {
	sqlPaginate: true,
	type: ServerMapConnection,
	args: {
		...connectionArgs,
		gameID: {
			type: new GraphQLNonNull(GraphQLID)
		}
	},

	sortKey: {
		order: 'ASC',
		key: 'name'
	},

	where: (mapsTable, { gameID }) => `
		${mapsTable}.game_id = ${gameID}
	`,

	resolve: (parent, args, context, resolveInfo) =>
		joinMonster(resolveInfo, context, sql =>
			postgres.query(sql)
		, options)
}

Got error with mysql and mariadb

I tried with sqlite and it work fine but with mysql/mariadb I got these error.

Error:

ER_PARSE_ERROR: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near

Full error:

"errors": [
    {
      "message": "SELECT\n  \"todos\".\"id\" AS \"id\",\n  \"todos\".\"task\" AS \"task\",\n  \"comments\".\"id\" AS \"comments__id\",\n  \"comments\".\"title\" AS \"comments__title\"\nFROM todos AS \"todos\"\nLEFT JOIN comments AS \"comments\" ON \"todos\".id = \"comments\".todoId - ER_PARSE_ERROR: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '.\"id\" AS \"id\",\n  \"todos\".\"task\" AS \"task\",\n  \"comments\".\"id\" AS \"comments__id\",\n' at line 1",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ]

version:

join-monster ^0.4.2
MySQL 5.7.16
MariaDB 10

How can I execute deep where condition?

I have a model Offer related to Game.
I expose a query for offers, and I want to filter the results using a field from Game (let's say platform).
How can I manage to do it?

From the docs all I could find is to use the where condition on the Offer table,
how can I pass on the where condition to the Game table?

Offer object:

const OfferType = new GraphQLObjectType({
  name: 'Offer',
  sqlTable: 'Offer',
  uniqueKey: 'id',
  description: 'This object represents the offers for the specified user.',
  fields: () => ({
    id: {
      description: 'The id of the offer.',
      type: GraphQLInt,
    },
    game: {
      description: 'The corresponding game of the offer.',
      type: GameType,
      sqlJoin: (thisTable, joinWith) => `${thisTable}.gameId = ${joinWith}.id`,
    },
  }),
});

Game object:

const GameType = new GraphQLObjectType({
  name: 'Game',
  sqlTable: 'Game',
  uniqueKey: ['name', 'platform'],
  fields: () => ({
    id: {
      description: 'The ID of the game.',
      type: GraphQLInt,
    },
    platform: {
      type: PlatformType,
      description: 'The corresponding platform for this game.',
    },
  }),
});

offers query:

const offers = {
  type: new GraphQLList(OfferType),
  args: {
    id: {
      type: GraphQLInt,
    },
    platform: {
      type: PlatformType,
      description: 'Query offers for the specified platform.',
    },
  },
  resolve: (_, args, context, resolveInfo) =>
    context.select(resolveInfo, args),
};

Multiple Tables (Unions)

We have a data schema where a table X can reference either table Y1 or Y2. In GQL the resolve function thus resolves either Y1 or Y2 which we put into a union Y. In that case the query function would have to execute a query on Y1 and Y2 the fields in Y1 and Y2 are a little different (1 and 2 are respective major versions of the tables. eg. one might have a field the other might not or it might be of an incompatible type).

Is something like this expressible to join monster ?

Is there any way to add additional columns to sqlbatch query?

sqlBatch: {
// which column to match up to the users
thisKey: 'post_id',
// the other column to compare to
parentKey: 'id'
}

I want to add additional column "comments"."xyz" with condition like below where 8989 is coming from parent (user) argument.
WHERE "comments"."post_id" IN (2,8,11,12) and "comments"."xyz" =8989

{
  user(xyz: 8989) {
    fullName
    email
    posts {
      id
      body
      comments {
        id
        body
        author {
          id
          fullName
        }
      }
    }
  }
}

1:n sorting by different criteria (+ pagination)

Hi there!

Thank you for ๐ŸŽ‰ awesome ๐Ÿ’ฏ project!

Are multiple sorting keys (and better, with pagination) supported?
It is possible to pass in argument which can be used to determine sorting key?

For example if I have product (1โ†’n) customer relation and want sort all customers which buyed product by date OR customer_name (OR something different...) by Keyset Paging

It is obvious that I need different cursors for each case.
From every cursor should be determined what pagination method was used in previous query OR I need sorting columns (and limit)
One example of pagination args: first:5, sort: ["customer_name NULLS FIRST", "delivery_date DESC", "id"]

Any clues or ideas how to implement this?

Directive support for SQL-JS links

Is support for directives in future plans?

I created graphql-utilities and all types and schema are driven using the schema language. It would be great if join-monster supported directives to define stuff like sqlTable and uniqueKey.

It could look something like this:

user @table("users") @pkey("id") {
  # ... selection set
}

Fragment Support

Hi,

I would be great if Join Monster could support GraphQL Fragments.

Jovin

capital letter no supported

need to double quote the identifiers in the SQL so that it will recognize capital letters in column and table names

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.