From c755902ee6dc9958fd8540297c898ecf3bf9297d Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Mon, 10 Aug 2020 19:28:53 -0400 Subject: [PATCH] GraphQL v15 config via extensions (#418) * Switch to using GraphQL 15's extensions for join-monster config in a schema GraphQL 15 doesn't let schema authors attach arbitrary properties to schema objects anymore, so join-monster's config style has to change. There's an `extensions` property that works great for this, let's use that! * Update docs to reflect new extensions configuration setup * Bump join-monster version to indicate breaking change * Update TypeScript types to export strongly typed extension interfaces After https://github.com/graphql/graphql-js/pull/2465 , we can now use TypeScript declaration merging to augment the graphql-types nice and cleanly. Woop woop! * Add a changelog entry explaining how to migrate to the new extensions format * Fix a couple broken TypeScript types for thunking and add TypeScript tests --- bin/test | 3 + docs/CHANGELOG.md | 115 +++- docs/aggregation.md | 21 +- docs/arbitrary-depth.md | 66 +- docs/batch-many-many.md | 30 +- docs/batch-one-many.md | 27 +- docs/field-metadata.md | 43 +- docs/how-it-works.md | 20 +- docs/many-to-many.md | 100 +-- docs/map-to-table.md | 48 +- docs/order-by.md | 75 +- docs/pagination.md | 162 +++-- docs/relay.md | 9 +- docs/start-joins.md | 11 +- docs/unions.md | 178 +++-- docs/where.md | 28 +- package-lock.json | 299 +++++++- package.json | 10 +- src/index.d.ts | 166 +++-- src/index.js | 16 +- src/query-ast-to-sql-ast/index.js | 139 ++-- src/util.js | 7 +- test-api/schema-basic/Authored/Interface.js | 52 +- test-api/schema-basic/Authored/Union.js | 46 +- test-api/schema-basic/Comment.js | 119 ++-- test-api/schema-basic/Post.js | 115 ++-- test-api/schema-basic/QueryRoot.js | 46 +- test-api/schema-basic/Sponsor.js | 38 +- test-api/schema-basic/User.js | 339 ++++++---- .../schema-paginated/Authored/Interface.js | 52 +- test-api/schema-paginated/Authored/Union.js | 46 +- test-api/schema-paginated/Comment.js | 74 +- test-api/schema-paginated/ContextPost.js | 14 +- test-api/schema-paginated/Post.js | 167 +++-- test-api/schema-paginated/QueryRoot.js | 60 +- test-api/schema-paginated/Sponsor.js | 34 +- test-api/schema-paginated/User.js | 639 ++++++++++-------- test-d/index.test-d.ts | 218 ++++++ 38 files changed, 2471 insertions(+), 1161 deletions(-) create mode 100644 test-d/index.test-d.ts diff --git a/bin/test b/bin/test index 784136f2..341a1197 100755 --- a/bin/test +++ b/bin/test @@ -104,5 +104,8 @@ if [ $PG = 1 ]; then STRATEGY=mix MINIFY=1 npm run testpg-paging fi +echo -e "${YELLOW} testing typescript definitions${NC}" +npm run testtsd + echo -e "${GREEN} PASSED ${NC}" diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 98a0dca6..261a33e3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,37 +1,109 @@ +### v3.0.0 (unreleased) + +**Breaking changes:** + +- Update GraphQL requirement to version 15, which supports a new `extensions` property where join-monster config lives. The config keys and values are largely unchanged, but now they must be nested under an `extensions: { joinMonster: ... }}` property on the GraphQLObjectTypes and fields using join-monster. To upgrade, you must move any non-standard keys off of your `GraphQLObjectType`s or field configs into the `extensions` of the same field. So, something like this: + +```javascript +const User = new GraphQLObjectType({ + name: 'User', + sqlTable: 'users', + uniqueKey: 'id', + fields: () => ({ + id: { + type: GraphQLInt + }, + email: { + type: GraphQLString, + sqlColumn: 'email_address' + }, + }) +} +``` + +becomes this: + +```javascript +const User = new GraphQLObjectType({ + name: 'User', + extensions: { + joinMonster: { + sqlTable: 'users', + uniqueKey: 'id' + } + }, + fields: () => ({ + id: { + type: GraphQLInt + }, + email: { + type: GraphQLString, + extensions: { + joinMonster: { + sqlColumn: 'email_address' + } + } + } + }) +} +``` + +The resulting code is sadly more verbose, but the only supported way of layering in extra information to a GraphQL schema going forward, and safer in the presence of other GraphQL schema extensions. + +**Note**: There are two configuration keys which have changed beyond just becoming nested in the `extensions` property: + +- `jmIgnoreAll` has been renamed to `ignoreAll` +- `jmIgnoreTable` has been renamed to `ignoreTable` + +The old names for these configuration options will no longer work so please be sure to update. + ### v2.1.2 (May 25, 2020) -#### Fixed + +#### Fixed + - Connections inside union fragments [#407](https://github.com/join-monster/join-monster/pull/407) ### v2.1.1 (Nov. 21, 2019) -#### Fixed + +#### Fixed + - Updated vulnerable version of lodash (`eed0264`) ### v2.1.0 (Aug. 25, 2018) + - Numerous bug fixes - TypeScript type definitions - New 'mysql8' dialect which supports some pagination ### v2.0.13 (Sep. 4, 2017) + - Don't write to debug module unless it's actually enabled. ### v2.0.9 (Aug. 23, 2017) + - Properly format instances of Buffer. ### v2.0.8 (Aug. 16, 2017) + - Support duplicate fields without aliases off the query root type. ### v2.0.6 (Aug. 11, 2017) + - Add SQL AST node to sqlJoin callback signature. ### v2.0.5 (Aug. 11, 2017) + - Remove the use of `Proxy` to improve compatibility. ### v2.0.4 (Aug. 8, 2017) + - Add option for custom dialect modules. - Various bug fixes. ### v2.0.0 (Jun. 25, 2017) + **New features:** + - `LIMIT` functionality, supported on all fields. - Fetch columns from junction tables. - For fields with junctions, you can now specify `WHERE` and `ORDER BY` clauses on the junction table or the main table, including paginated fields. @@ -39,6 +111,7 @@ - Better ability to write `where` functions that depend on args and info from the parent/ancestors. **Breaking changes:** + - Fields with junctions have a new interface in order to support the new features. - Any `where`, `orderBy`, and `sortKey` on many-to-many paginated fields used to be applied to the junction table. This has changed, and will be applied to the main table instead in order to be consistent with non-paginated junctions. If the old behavior is desired, you can nest those properties inside the `junction` object, which is part of the new API. - Change 4th parameter of `where` and `sqlExpr` to the field's SQL AST Node, which is a lot more useful. @@ -134,30 +207,36 @@ // created_at: 'DESC', // id: 'ASC' //} - + // you could also place a `where` at either } ``` ### v1.2.1 (Mar. 28, 2017) + - Add `jmIgnoreAll` and `jmIgnoreTable`. - Make `sqlTable` a thunk. - Bug fix with recursively nested union and interface type fragments. - Bug fix with for batch on a single-type parent. ### v1.2.0 (Mar. 16, 2017) + - Add an API for GraphQLInterfaceType ### v1.1.1 (Mar. 13, 2017) + - Add an API for GraphQLUnionType ### v1.1.0 (Mar. 11, 2017) + - Add Oracle as supported dialect. ### v1.0.1 (Mar. 8, 2017) + - Add `ORDER BY` support for non-paginated fields. ### v1.0.0 (Feb. 28, 2017) + - Batching capabilities added. - MariaDB can do pagination on batches. - `sqlExpr` can now be asynchronous. @@ -173,94 +252,120 @@ - `joinTable` is deprecated. It was renamed to `junctionTable` to avoid over-use of the word "join". - `'standard'` dialect is deprecated because nothing really implements the standard. The new default is `'sqlite3'`. - - ### v0.9.10 (Feb. 16, 2017) + - Bug fixes with recursive fragments and argument parsing. ### v0.9.9 (Feb. 3, 2017) + - Add `context` to the `sqlJoin` parameters. - Support async in `sqlJoin`. ### v0.9.8 (Feb. 2, 2017) + - Expose parent table aliases to `where` function. ### v0.9.5 (Jan. 24, 2017) + - Fix bug for Postgres where `CONCAT` returns `''` instead of `NULL`. ### v0.9.4 (Jan. 22, 2017) + - Expose GraphQL args to `sqlJoin` function. ### v0.9.3 (Jan. 14, 2017) + - Add support for fragments on interface types. ### v0.9.2 (Jan. 5, 2017) + - Fix bug when composite keys contain timestamps or dates in PG dialect. - Patch SQL injection risk. ### v0.9.0 (Jan. 4, 2017) + - More automatic fetching using `getNode` implemented. ### v0.8.0 (Dec. 19, 2016) + - Expose the `getSQL` method for getting only the converted SQL. ### v0.7.0 (Dec. 16, 2016) + - Introducing raw SQL expressions for computed columns. ### v0.6.0 (Dec. 2, 2016) + - Support asynchronicity in the `where` function. ### v0.5.8 (Dec. 1, 2016) + - Add null check to node interface handler. ### v0.5.7 (Nov. 29, 2016) + - Fix bug with `WHERE` conditions on paginated fields. ### v0.5.6 (Nov. 14, 2016) + - Fix bug with query variables on the Node interface. ### v0.5.5 (Nov. 13, 2016) + - Add support for dynamic sort keys on paginated fields. Sort keys can now be functions that receive the GraphQL arguments. ### v0.5.4 (Nov. 10, 2016) + - Add support for query variables. ### v0.5.2 (Nov. 6, 2016) + - Relay connection type names are no longer required to end with "Connection". ### v0.5.1 (Nov. 5, 2016) + - Fix problem with introspection queries. ### v0.5.0 (Nov. 4, 2016) + - Add dialect for MySQL/MariaDB. ### v0.4.1 (Oct 21, 2016) + - Fix bug with de-duplication of objects. ### v0.4.0 (Oct 20, 2016) + - Add Postgres dialect option. - Support SQL pagination based on integer offsets. - Support SQL pagination based on a sort key(s). ### v0.3.7 (Oct 16, 2016) + - Fix bug with nested fragments. ### v0.3.6 (Oct 13, 2016) + - Option to minify the raw data column names. ### v0.3.5 (Oct 9, 2016) + - Add test coverage tools. ### v0.3.4 (Oct 6, 2016) + - Add helper method for getting data for Relay's Node type. - Fix bug with Union and Interface types. ### v0.3.2 (Oct 5, 2016) + - Add support for specifying schema names for your SQL tables. ### v0.3.1 (Oct 4, 2016) + - Detect Relay connection type and fetch data for it. ### v0.3.0 (Oct 3, 2016) + - Unique keys required for every table. Necessary for achieving good performance during object shaping/nesting. - Composite keys supported for the unique key. diff --git a/docs/aggregation.md b/docs/aggregation.md index 63078944..01951fa2 100644 --- a/docs/aggregation.md +++ b/docs/aggregation.md @@ -13,8 +13,13 @@ const Post = new GraphQLObjectType({ description: 'The number of comments on this post', type: GraphQLInt, // use a correlated subquery in a raw SQL expression to do things like aggregation - sqlExpr: postTable => `(SELECT count(*) FROM comments WHERE post_id = ${postTable}.id AND archived = FALSE)` - }, + extensions: { + joinMonster: { + sqlExpr: postTable => + `(SELECT count(*) FROM comments WHERE post_id = ${postTable}.id AND archived = FALSE)` + } + } + } }) }) ``` @@ -57,8 +62,13 @@ const Post = new GraphQLObjectType({ fields: () => ({ commentsWithoutJoin: { type: new GraphQLList(SimpleComment), - sqlExpr: postTable => `(SELECT json_agg(comments) FROM comments WHERE comments.post_id = ${postTable}.id AND comments.archived = FALSE)` - }, + extensions: { + joinMonster: { + sqlExpr: postTable => + `(SELECT json_agg(comments) FROM comments WHERE comments.post_id = ${postTable}.id AND comments.archived = FALSE)` + } + } + } }) }) ``` @@ -76,7 +86,7 @@ This should work without any additional data munging if you're using `knex`, as commentsWithoutJoin { id body - authorId + authorId } } } @@ -98,4 +108,3 @@ FROM accounts AS "user" LEFT JOIN posts AS "posts" ON "user".id = "posts".author_id WHERE "user".id = 2 ``` - diff --git a/docs/arbitrary-depth.md b/docs/arbitrary-depth.md index f6d21b4f..3f01ceef 100644 --- a/docs/arbitrary-depth.md +++ b/docs/arbitrary-depth.md @@ -19,7 +19,12 @@ const Post = new GraphQLObjectType({ author: { description: 'The user that created the post', type: User, - sqlJoin: (postTable, userTable, args, context) => `${postTable}.author_id = ${userTable}.id` + extensions: { + joinMonster: { + sqlJoin: (postTable, userTable, args, context) => + `${postTable}.author_id = ${userTable}.id` + } + } } }) }) @@ -31,12 +36,22 @@ const Comment = new GraphQLObjectType({ post: { description: 'The post that the comment belongs to', type: Post, - sqlJoin: (commentTable, postTable) => `${commentTable}.post_id = ${postTable}.id` + extensions: { + joinMonster: { + sqlJoin: (commentTable, postTable) => + `${commentTable}.post_id = ${postTable}.id` + } + } }, author: { description: 'The user who wrote the comment', type: User, - sqlJoin: (commentTable, userTable) => `${commentTable}.author_id = ${userTable}.id` + extensions: { + joinMonster: { + sqlJoin: (commentTable, userTable) => + `${commentTable}.author_id = ${userTable}.id` + } + } } }) }) @@ -46,14 +61,22 @@ Now you can get the comments the user has written, the post on which each commen ```graphql { - users { - id, email, fullName + users { + id + email + fullName comments { - id, body - author { fullName } + id + body + author { + fullName + } post { - id, body - author { fullName } + id + body + author { + fullName + } } } } @@ -71,11 +94,21 @@ const User = new GraphQLObjectType({ //... posts: { type: new GraphQLList(Post), - sqlJoin: (userTable, postTable, args) => `${userTable}.id = ${postTable}.author_id` + extensions: { + joinMonster: { + sqlJoin: (userTable, postTable, args) => + `${userTable}.id = ${postTable}.author_id` + } + } }, comments: { type: new GraphQLList(Comment), - sqlJoin: (userTable, commentTable, args) => `${userTable}.id = ${commentTable}.author_id` + extensions: { + joinMonster: { + sqlJoin: (userTable, commentTable, args) => + `${userTable}.id = ${commentTable}.author_id` + } + } } }) }) @@ -91,15 +124,18 @@ const Post = new GraphQLObjectType({ comments: { description: 'The comments on this post', type: new GraphQLList(Comment), - // the JOIN condition also checks that the comment is not archived - sqlJoin: (postTable, commentTable) => `${postTable}.id = ${commentTable}.post_id AND ${commentTable}.archived = FALSE`, + extensions: { + joinMonster: { + // the JOIN condition also checks that the comment is not archived + sqlJoin: (postTable, commentTable) => + `${postTable}.id = ${commentTable}.post_id AND ${commentTable}.archived = FALSE` + } + } } }) }) - ``` Again, the data is all fetched in a single query thanks to `JOIN`s. However, doing all these joins can be cumbersome on the database. We can split it into two, or perhaps more, separate queries to reduce the number of joins in the next section. - diff --git a/docs/batch-many-many.md b/docs/batch-many-many.md index b1180452..37cd0a85 100644 --- a/docs/batch-many-many.md +++ b/docs/batch-many-many.md @@ -12,18 +12,23 @@ const User = new GraphQLObjectType({ following: { description: 'Users that this user is following', type: new GraphQLList(User), - // batching many-to-many is supported too - junction: { - sqlTable: 'relationships', - // this table has no primary key, but the combination of these two columns is unique - uniqueKey: [ 'follower_id', 'followee_id' ], - sqlBatch: { - // the matching column in the junction table - thisKey: 'follower_id', - // the column to match in the user table - parentKey: 'id', - // how to join the related table to the junction table - sqlJoin: (junctionTable, followeeTable) => `${junctionTable}.followee_id = ${followeeTable}.id` + extensions: { + joinMonster: { + // batching many-to-many is supported too + junction: { + sqlTable: 'relationships', + // this table has no primary key, but the combination of these two columns is unique + uniqueKey: ['follower_id', 'followee_id'], + sqlBatch: { + // the matching column in the junction table + thisKey: 'follower_id', + // the column to match in the user table + parentKey: 'id', + // how to join the related table to the junction table + sqlJoin: (junctionTable, followeeTable) => + `${junctionTable}.followee_id = ${followeeTable}.id` + } + } } } } @@ -37,4 +42,3 @@ In addition to the changes made on the previous page, the plan now has 3 databas ![query-plan-3](img/query-plan-3.png) Requests for the followees and for the comments are independent, and are sent concurrently. - diff --git a/docs/batch-one-many.md b/docs/batch-one-many.md index ccb782bb..2011a01f 100644 --- a/docs/batch-one-many.md +++ b/docs/batch-one-many.md @@ -14,17 +14,21 @@ const Post = new GraphQLObjectType({ comments: { description: 'The comments on this post', type: new GraphQLList(Comment), - // instead of doing yet another JOIN, we'll get these comments in a separate batch - // sqlJoin: (postTable, commentTable) => `${postTable}.id = ${commentTable}.post_id AND ${commentTable}.archived = FALSE`, - sqlBatch: { - // which column to match up to the users - thisKey: 'post_id', - // the other column to compare to - parentKey: 'id' - }, - // sqlBatch works with the `where` function too. get only non-archived comments - where: table => `${table}.archived = FALSE` - }, + extensions: { + joinMonster: { + // instead of doing yet another JOIN, we'll get these comments in a separate batch + // sqlJoin: (postTable, commentTable) => `${postTable}.id = ${commentTable}.post_id AND ${commentTable}.archived = FALSE`, + sqlBatch: { + // which column to match up to the users + thisKey: 'post_id', + // the other column to compare to + parentKey: 'id' + }, + // sqlBatch works with the `where` function too. get only non-archived comments + where: table => `${table}.archived = FALSE` + } + } + } }) }) ``` @@ -91,4 +95,3 @@ Two database queries are made regardless of the number of posts, another way to Although this also works perfectly fine for a one-to-one relation, it is not recommended. Not much is gained by batching on a one-to-one since using a simple `JOIN` would not burden the database greatly. - diff --git a/docs/field-metadata.md b/docs/field-metadata.md index 7de3b7f1..76e566bb 100644 --- a/docs/field-metadata.md +++ b/docs/field-metadata.md @@ -12,13 +12,21 @@ const User = new GraphQLObjectType({ }, email: { type: GraphQLString, - // if the column name is different, it must be specified - sqlColumn: 'email_address' + extensions: { + joinMonster: { + // if the column name is different, it must be specified + sqlColumn: 'email_address' + } + } }, idEncoded: { description: 'The ID base-64 encoded', type: GraphQLString, - sqlColumn: 'id', + extensions: { + joinMonster: { + sqlColumn: 'id' + } + }, // 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 resolve: user => toBase64(user.id) @@ -52,11 +60,15 @@ const User = new GraphQLObjectType({ //... fields: () => ({ fullName: { - description: 'A user\'s first and last name', + 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' ], + extensions: { + joinMonster: { + // perhaps there is no 1-to-1 mapping of field to column + // this field depends on multiple columns + sqlDeps: ['first_name', 'last_name'] + } + }, resolve: user => `${user.first_name} ${user.last_name}` } }) @@ -71,15 +83,22 @@ const User = new GraphQLObjectType({ fields: () => ({ capitalizedLastName: { type: GraphQLString, - // do a computed column in SQL with raw expression - sqlExpr: (table, args) => `UPPER(${table}.last_name)` + extensions: { + joinMonster: { + // do a computed column in SQL with raw expression + sqlExpr: (table, args) => `UPPER(${table}.last_name)` + } + } }, fullNameAnotherWay: { description: 'Another way we can get the full name.', type: GraphQLString, - sqlExpr: table => `${table}.first_name || ' ' || ${table}.last_name` - }, + extensions: { + joinMonster: { + sqlExpr: table => `${table}.first_name || ' ' || ${table}.last_name` + } + } + } }) }) ``` - diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 84252cc6..d3c3bc03 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -35,11 +35,19 @@ const User = new GraphQLObjectType({ fields: () => ({ id: { type: GraphQLInt, - sqlColumn: 'id' + extensions: { + joinMonster: { + sqlColumn: 'id' + } + } }, email: { type: GraphQLString, - sqlColumn: 'email_address' + extensions: { + joinMonster: { + sqlColumn: 'email_address' + } + } }, immortal: { type: graphQLBoolean, @@ -47,7 +55,12 @@ const User = new GraphQLObjectType({ }, posts: { type: new GraphQLList(Post), - sqlJoin: (userTable, postTable) => `${userTable}.id = ${postTable}.author_id` + extensions: { + joinMonster: { + sqlJoin: (userTable, postTable) => + `${userTable}.id = ${postTable}.author_id` + } + } } }) }) @@ -76,4 +89,3 @@ users: { } } ``` - diff --git a/docs/many-to-many.md b/docs/many-to-many.md index 2f7c579f..fb04ca5e 100644 --- a/docs/many-to-many.md +++ b/docs/many-to-many.md @@ -10,15 +10,21 @@ const User = new GraphQLObjectType({ following: { description: 'Users that this user is following', type: new GraphQLList(User), - junction: { - // name the table that holds the two foreign keys - 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` - ] + extensions: { + joinMonster: { + junction: { + // name the table that holds the two foreign keys + 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` + ] + } + } } } }) @@ -29,7 +35,7 @@ Now we have a self-referential, many-to-many relationship. ```grapql { - users { + users { id email fullName @@ -51,12 +57,18 @@ const Comment = new GraphQLObjectType({ likers: { description: 'Which users have liked this comment', type: new GraphQLList(User), - junction: { - sqlTable: 'likes', - sqlJoins: [ - (commentTable, likesTable) => `${commentTable}.id = ${likesTable}.comment_id`, - (likesTable, userTable) => `${likesTable}.account_id = ${userTable}.id` - ] + extensions: { + joinMonster: { + junction: { + sqlTable: 'likes', + sqlJoins: [ + (commentTable, likesTable) => + `${commentTable}.id = ${likesTable}.comment_id`, + (likesTable, userTable) => + `${likesTable}.account_id = ${userTable}.id` + ] + } + } } } }) @@ -67,23 +79,30 @@ const Comment = new GraphQLObjectType({ In a similar manner, `where` can be added to this field, and it will apply to the `accounts` table for the followees. You can also add a `where` in the `junction` object to apply a `WHERE` clause on the junction table. -```javascript +```js const User = new GraphQLObjectType({ //... fields: () => ({ //... following: { type: new GraphQLList(User), - // only get followees who's account is still active - where: accountTable => `${accountTable}.is_active = TRUE`, - junction: { - sqlTable: 'relationships', - // filter out where they are following themselves - where: junctionTable => `${junctionTable}.follower_id <> ${junctionTable}.followee_id` - sqlJoins: [ - (followerTable, junctionTable, args) => `${followerTable}.id = ${junctionTable}.follower_id`, - (junctionTable, followeeTable, args) => `${junctionTable}.followee_id = ${followeeTable}.id` - ] + extensions: { + joinMonster: { + // only get followees who's account is still active + where: accountTable => `${accountTable}.is_active = TRUE`, + junction: { + sqlTable: 'relationships', + // filter out where they are following themselves + where: junctionTable => + `${junctionTable}.follower_id <> ${junctionTable}.followee_id`, + sqlJoins: [ + (followerTable, junctionTable, args) => + `${followerTable}.id = ${junctionTable}.follower_id`, + (junctionTable, followeeTable, args) => + `${junctionTable}.followee_id = ${followeeTable}.id` + ] + } + } } } }) @@ -126,17 +145,23 @@ const User = new GraphQLObjectType({ }, following: { type: new GraphQLList(User), - junction: { - sqlTable: 'relationships', - include: { - closeness: { - sqlColumn: 'closeness' + extensions: { + joinMonster: { + junction: { + sqlTable: 'relationships', + include: { + closeness: { + sqlColumn: 'closeness' + } + }, + sqlJoins: [ + (followerTable, junctionTable, args) => + `${followerTable}.id = ${junctionTable}.follower_id`, + (junctionTable, followeeTable, args) => + `${junctionTable}.followee_id = ${followeeTable}.id` + ] } - }, - sqlJoins: [ - (followerTable, junctionTable, args) => `${followerTable}.id = ${junctionTable}.follower_id`, - (junctionTable, followeeTable, args) => `${junctionTable}.followee_id = ${followeeTable}.id` - ] + } } } }) @@ -165,4 +190,3 @@ So now the query would look something like this: ``` We've completed the schema diagram! We can theoretically resolve any GraphQL query with one SQL query! In the next section we'll see how we can batch the request different to reduce the number of joins. - diff --git a/docs/map-to-table.md b/docs/map-to-table.md index b32fd857..66e077b7 100644 --- a/docs/map-to-table.md +++ b/docs/map-to-table.md @@ -7,9 +7,15 @@ We also need a unique identifier so it's unambiguous which objects are distinct ```javascript const User = new GraphQLObjectType({ name: 'User', - sqlTable: 'accounts', // the SQL table for this object type is called "accounts" - uniqueKey: 'id', // id is different for every row - fields: () => ({ /*...*/ }) + extensions: { + joinMonster: { + sqlTable: 'accounts', // the SQL table for this object type is called "accounts" + uniqueKey: 'id' // id is different for every row + } + }, + fields: () => ({ + /*...*/ + }) }) ``` @@ -20,9 +26,15 @@ If your table is on a SQL schema that is not the default, e.g. `public`, you can ```javascript const User = new GraphQLObjectType({ name: 'User', - sqlTable: 'public."Accounts"', // the SQL table is on the schema "public" called "Accounts" - uniqueKey: 'id', - fields: () => ({ /*...*/ }) + extensions: { + joinMonster: { + sqlTable: 'public."Accounts"', // the SQL table is on the schema "public" called "Accounts" + uniqueKey: 'id' + } + }, + fields: () => ({ + /*...*/ + }) }) ``` @@ -31,9 +43,15 @@ The `sqlTable` can generalize to any **table expression**. Instead of a physical ```javascript const User = new GraphQLObjectType({ name: 'User', - sqlTable: '(SELECT * FROM accounts WHERE active = 1)', // this can be an expression that generates a TABLE - uniqueKey: 'id', - fields: () => ({ /*...*/ }) + extensions: { + joinMonster: { + sqlTable: '(SELECT * FROM accounts WHERE active = 1)', // this can be an expression that generates a TABLE + uniqueKey: 'id' + } + }, + fields: () => ({ + /*...*/ + }) }) ``` @@ -59,8 +77,14 @@ Just make `uniqueKey` an array of string instead of a string. Join Monster will ```javascript const User = new GraphQLObjectType({ name: 'User', - sqlTable: 'accounts', - uniqueKey: [ 'generation', 'first_name', 'last_name' ], - fields: () => ({ /*...*/ }) + extensions: { + joinMonster: { + sqlTable: 'accounts', + uniqueKey: ['generation', 'first_name', 'last_name'] + } + }, + fields: () => ({ + /*...*/ + }) }) ``` diff --git a/docs/order-by.md b/docs/order-by.md index e92d6f4c..c39368cf 100644 --- a/docs/order-by.md +++ b/docs/order-by.md @@ -10,12 +10,17 @@ const User = new GraphQLObjectType({ //... comments: { type: new GraphQLList(Comment), - // order these alphabetically, then by "id" if the comment body is the same - orderBy: { - body: 'asc', - id: 'desc' - }, - sqlJoin: (userTable, commentTable, args) => `${userTable}.id = ${commentTable}.author_id` + extensions: { + joinMonster: { + // order these alphabetically, then by "id" if the comment body is the same + orderBy: { + body: 'asc', + id: 'desc' + }, + sqlJoin: (userTable, commentTable, args) => + `${userTable}.id = ${commentTable}.author_id` + } + } } }) }) @@ -25,11 +30,15 @@ const QueryRoot = new GraphQLObjectType({ fields: () => ({ users: { type: new GraphQLList(User), - orderBy: { - id: 'asc' - }, - resolve: (parent, args, context, resolveInfo) => { - // joinMonster + extensions: { + joinMonster: { + orderBy: { + id: 'asc' + }, + resolve: (parent, args, context, resolveInfo) => { + // joinMonster + } + } } } }) @@ -78,13 +87,18 @@ const User = new GraphQLObjectType({ args: { by: { type: ColumnEnum } }, - orderBy: args => { - const sortBy = args.by || 'id' - return { - [sortBy]: 'desc' + extensions: { + joinMonster: { + orderBy: args => { + const sortBy = args.by || 'id' + return { + [sortBy]: 'desc' + } + }, + sqlJoin: (userTable, commentTable, args) => + `${userTable}.id = ${commentTable}.author_id` } - }, - sqlJoin: (userTable, commentTable, args) => `${userTable}.id = ${commentTable}.author_id` + } } }) }) @@ -99,19 +113,24 @@ const User = new GraphQLObjectType({ //... following: { type: new GraphQLList(User), - // order by the user id - orderBy: { id: 'DESC' }, - junction: { - sqlTable: 'relationships', - // this would have been equivalent - //orderBy: { followee_id: 'DESC' }, - sqlJoins: [ - (followerTable, junctionTable, args) => `${followerTable}.id = ${junctionTable}.follower_id`, - (junctionTable, followeeTable, args) => `${junctionTable}.followee_id = ${followeeTable}.id` - ] + extensions: { + joinMonster: { + // order by the user id + orderBy: { id: 'DESC' }, + junction: { + sqlTable: 'relationships', + // this would have been equivalent + //orderBy: { followee_id: 'DESC' }, + sqlJoins: [ + (followerTable, junctionTable, args) => + `${followerTable}.id = ${junctionTable}.follower_id`, + (junctionTable, followeeTable, args) => + `${junctionTable}.followee_id = ${followeeTable}.id` + ] + } + } } } }) }) ``` - diff --git a/docs/pagination.md b/docs/pagination.md index 47d7d51e..4625a43a 100644 --- a/docs/pagination.md +++ b/docs/pagination.md @@ -52,27 +52,41 @@ const User = new GraphQLObjectType({ // ... id: { type: GraphQLInt, - sqlColumn: 'id' + extensions: { + joinMonster: { + sqlColumn: 'id' + } + } }, comments: { type: CommentConnection, // accept the standard args for connections, e.g. `first`, `after`... args: connectionArgs, - // write the JOIN as you normally would. you can do a `sqlBatch` instead - sqlJoin: (userTable, commentTable) => `${userTable}.id = ${commentTable}.author_id`, + extensions: { + joinMonster: { + // write the JOIN as you normally would. you can do a `sqlBatch` instead + sqlJoin: (userTable, commentTable) => + `${userTable}.id = ${commentTable}.author_id` + } + }, // joinMonster give us an array, use the helper to slice the array based on the args resolve: (user, args) => { return connectionFromArray(user.comments, args) } }, posts: { - type: PostConnection, + type: PostConnection, args: connectionArgs, - sqlJoin: (userTable, postTable) => `${userTable}.id = ${postTable}.author_id`, + extensions: { + joinMonster: { + sqlJoin: (userTable, postTable) => + `${userTable}.id = ${postTable}.author_id` + } + }, resolve: (user, args) => { return connectionFromArray(user.posts, args) } - }, + } }) }) ``` @@ -107,17 +121,22 @@ const User = new GraphQLObjectType({ type: CommentConnection, // this implementation only supports forward pagination args: forwardConnectionArgs, - // tell join monster to paginate the queries in SQL - sqlPaginate: true, - // specify what to order on - orderBy: 'id', - // join is the same as before - sqlJoin: (userTable, commentTable) => `${userTable}.id = ${commentTable}.author_id` - // or you could have used batching - //sqlBatch: { - // thisKey: 'author_id', - // parentKey: 'id' - //} + extensions: { + joinMonster: { + // tell join monster to paginate the queries in SQL + sqlPaginate: true, + // specify what to order on + orderBy: 'id', + // join is the same as before + sqlJoin: (userTable, commentTable) => + `${userTable}.id = ${commentTable}.author_id` + // or you could have used batching + //sqlBatch: { + // thisKey: 'author_id', + // parentKey: 'id' + //} + } + } } }) }) @@ -134,14 +153,19 @@ const User = new GraphQLObjectType({ type: CommentConnection, // this time only forward pagination works args: forwardConnectionArgs, - sqlPaginate: true, - // orders on both `created_at` and `id`. the first property is the primary sort column. - // it only sorts on `id` if `created_at` is equivalent - orderBy: { - created_at: 'desc', - id: 'asc' - }, - sqlJoin: (userTable, commentTable) => `${userTable}.id = ${commentTable}.author_id` + extensions: { + joinMonster: { + sqlPaginate: true, + // orders on both `created_at` and `id`. the first property is the primary sort column. + // it only sorts on `id` if `created_at` is equivalent + orderBy: { + created_at: 'desc', + id: 'asc' + }, + sqlJoin: (userTable, commentTable) => + `${userTable}.id = ${commentTable}.author_id` + } + } } }) }) @@ -252,17 +276,22 @@ const User = new GraphQLObjectType({ posts: { description: 'A list of Posts the user has written', // this is now a connection type - type: PostConnection, + type: PostConnection, args: connectionArgs, - sqlPaginate: true, - // use "keyset" pagination, an implementation based on a unique sorting key - // they will be sorted on `id` descending. - sortKey: { - order: 'DESC', - key: 'id' - }, - sqlJoin: (userTable, postTable) => `${userTable}.id = ${postTable}.author_id` - }, + extensions: { + joinMonster: { + sqlPaginate: true, + // use "keyset" pagination, an implementation based on a unique sorting key + // they will be sorted on `id` descending. + sortKey: { + order: 'DESC', + key: 'id' + }, + sqlJoin: (userTable, postTable) => + `${userTable}.id = ${postTable}.author_id` + } + } + } }) }) ``` @@ -289,14 +318,19 @@ const User = new GraphQLObjectType({ // this is now a connection type type: CommentConnection, args: connectionArgs, - sqlPaginate: true, - // orders on both `created_at` and `id`. the first property is the primary sort column. - // it only sorts on `id` if `created_at` is equivalent - sortKey: { - order: 'desc', - key: [ 'created_at', 'id' ] - }, - sqlJoin: (userTable, commentTable) => `${userTable}.id = ${commentTable}.author_id` + extensions: { + joinMonster: { + sqlPaginate: true, + // orders on both `created_at` and `id`. the first property is the primary sort column. + // it only sorts on `id` if `created_at` is equivalent + sortKey: { + order: 'desc', + key: ['created_at', 'id'] + }, + sqlJoin: (userTable, commentTable) => + `${userTable}.id = ${commentTable}.author_id` + } + } } }) }) @@ -352,20 +386,24 @@ const Post = new GraphQLObjectType({ // ... comments: { type: CommentConnection, - sqlPaginate: true, args: connectionArgs, - sortKey: { - order: 'desc', - key: [ 'created_at', 'id' ] - }, - sqlBatch: { - // which column to match up to the users - thisKey: 'post_id', - // the other column to compare to - parentKey: 'id' - }, - where: table => `${table}.archived = FALSE` - }, + extensions: { + joinMonster: { + sqlPaginate: true, + sortKey: { + order: 'desc', + key: ['created_at', 'id'] + }, + sqlBatch: { + // which column to match up to the users + thisKey: 'post_id', + // the other column to compare to + parentKey: 'id' + }, + where: table => `${table}.archived = FALSE` + } + } + } }) }) ``` @@ -382,13 +420,17 @@ const Post = new GraphQLObjectType({ // ... only3Comments: { type: new GraphQLList(Comment), - orderBy: { id: 'desc' }, - limit: 3, - sqlJoin: (userTable, commentTable) => `${commentTable}.author_id = ${userTable}.id` + extensions: { + joinMonster: { + orderBy: { id: 'desc' }, + limit: 3, + sqlJoin: (userTable, commentTable) => + `${commentTable}.author_id = ${userTable}.id` + } + } } }) }) ``` The `limit` can be an integer or a function that returns an integer. This feature is only supported if pagination is supported for you SQL dialect. - diff --git a/docs/relay.md b/docs/relay.md index aa00548c..13ca4748 100644 --- a/docs/relay.md +++ b/docs/relay.md @@ -22,8 +22,12 @@ const User = new GraphQLObjectType({ globalId: { description: 'The global ID for the Relay spec', ...globalIdField(), - sqlDeps: [ 'id' ] - }, + extensions: { + joinMonster: { + sqlDeps: ['id'] + } + } + } //... }) }) @@ -90,4 +94,3 @@ const { nodeInterface, nodeField } = nodeDefinitions( obj => obj.__type__ ) ``` - diff --git a/docs/start-joins.md b/docs/start-joins.md index 7af988a6..d00dd8ed 100644 --- a/docs/start-joins.md +++ b/docs/start-joins.md @@ -33,9 +33,14 @@ const User = new GraphQLObjectType({ //... comments: { type: new GraphQLList(Comment), - // a function to generate the join condition from the table aliases - // NOTE: you must double-quote any case-sensitive column names the table aliases are already quoted - sqlJoin: (userTable, commentTable, args) => `${userTable}.id = ${commentTable}.author_id` + extensions: { + joinMonster: { + // a function to generate the join condition from the table aliases + // NOTE: you must double-quote any case-sensitive column names the table aliases are already quoted + sqlJoin: (userTable, commentTable, args) => + `${userTable}.id = ${commentTable}.author_id` + } + } } }) }) diff --git a/docs/unions.md b/docs/unions.md index f314ba67..16b30994 100644 --- a/docs/unions.md +++ b/docs/unions.md @@ -14,24 +14,30 @@ If table `foo` has columns `a` and `b` and table `bar` has columns `a` and `c`, ```js const FooBar = new GraphQLUnionType({ name: 'FooBar', - types: [ Foo, Bar ], - // a derived table that combines the two tables into one via a UNION - sqlTable: `( - SELECT - a, - b, - NULL as c - FROM foo - UNION - SELECT - a, - NULL as b, - c - FROM bar - )`, - // specify unique key - uniqueKey: 'a', - resolveType: () => { /* TODO */ } + types: [Foo, Bar], + extensions: { + joinMonster: { + // a derived table that combines the two tables into one via a UNION + sqlTable: `( + SELECT + a, + b, + NULL as c + FROM foo + UNION + SELECT + a, + NULL as b, + c + FROM bar + )`, + // specify unique key + uniqueKey: 'a' + } + }, + resolveType: () => { + /* TODO */ + } }) ``` @@ -49,27 +55,33 @@ We could add a computed column to guarantee a unique value. There is, however, s ```js const Authored = new GraphQLUnionType({ name: 'Authored', - types: () => [ Comment, Post ], - sqlTable: `( - SELECT - id, - body, - author_id, - NULL AS post_id, -- post has no post_id, so fill that in with NULL - 'Post' AS "$type" - FROM posts - UNION ALL - SELECT - id, - body, - author_id, - post_id, - 'Comment' AS "$type" - FROM comments - )`, - // the combination of `id` and `$type` will always be unique - uniqueKey: [ 'id', '$type' ], - resolveType: obj => { /* TODO */ } + types: () => [Comment, Post], + extensions: { + joinMonster: { + sqlTable: `( + SELECT + id, + body, + author_id, + NULL AS post_id, -- post has no post_id, so fill that in with NULL + 'Post' AS "$type" + FROM posts + UNION ALL + SELECT + id, + body, + author_id, + post_id, + 'Comment' AS "$type" + FROM comments + )`, + // the combination of `id` and `$type` will always be unique + uniqueKey: ['id', '$type'] + } + }, + resolveType: obj => { + /* TODO */ + } }) ``` @@ -80,11 +92,15 @@ Join Monster provides an optional `alwaysFetch` property which forces that colum ```js const Authored = new GraphQLUnionType({ name: 'Authored', - types: () => [ Comment, Post ], - sqlTable: `(...)`, - uniqueKey: [ 'id', '$type' ], - // tells join monster to always fetch the $type in the hydrated data - alwaysFetch: '$type', + types: () => [Comment, Post], + extensions: { + joinMonster: { + sqlTable: `(...)`, + uniqueKey: ['id', '$type'], + // tells join monster to always fetch the $type in the hydrated data + alwaysFetch: '$type' + } + }, // easily gleaned from the column we added in SQL resolveType: obj => obj.$type }) @@ -98,30 +114,33 @@ This is GraphQL's other option for polymorphic types, but it has shared fields w Join Monster treats these nearly the same. The difference is that you must decorate the `fields` on the interface type defintion. - ```js const Authored = new GraphQLInterfaceType({ name: 'Authored', - sqlTable: `( - SELECT - id, - body, - author_id, - NULL AS post_id, - 'Post' AS "$type" - FROM posts - UNION ALL - SELECT - id, - body, - author_id, - post_id, - 'Comment' AS "$type" - FROM comments - )`, - // the combination of `id` and `$type` will always be unique - uniqueKey: [ 'id', '$type' ], - alwaysFetch: '$type', + extensions: { + joinMonster: { + sqlTable: `( + SELECT + id, + body, + author_id, + NULL AS post_id, + 'Post' AS "$type" + FROM posts + UNION ALL + SELECT + id, + body, + author_id, + post_id, + 'Comment' AS "$type" + FROM comments + )`, + // the combination of `id` and `$type` will always be unique + uniqueKey: ['id', '$type'], + alwaysFetch: '$type' + } + }, fields: () => ({ id: { // still assumed to have the same column name as the field name @@ -131,9 +150,13 @@ const Authored = new GraphQLInterfaceType({ type: GraphQLString }, authorId: { - // but this column name is different type: GraphQLInt, - sqlColumn: 'author_id' + extensions: { + joinMonster: { + // but this column name is different + sqlColumn: 'author_id' + } + } } }), resolveType: obj => obj.$type @@ -151,14 +174,19 @@ const User = new GraphQLObjectType({ // ... writtenMaterial: { type: new GraphQLList(Authored), - orderBy: 'id', - // how to join on the derived table - sqlJoin: (userTable, unionTable) => `${userTable}.id = ${unionTable}.author_id` - // or we could have done it in a batch - // sqlBatch: { - // thisKey: 'author_id', - // parentKey: 'id' - // } + extensions: { + joinMonster: { + orderBy: 'id', + // how to join on the derived table + sqlJoin: (userTable, unionTable) => + `${userTable}.id = ${unionTable}.author_id` + // or we could have done it in a batch + // sqlBatch: { + // thisKey: 'author_id', + // parentKey: 'id' + // } + } + } } }) }) diff --git a/docs/where.md b/docs/where.md index 37354834..6dd0f095 100644 --- a/docs/where.md +++ b/docs/where.md @@ -20,8 +20,12 @@ const QueryRoot = new GraphQLObjectType({ args: { id: { type: new GraphQLNonNull(GraphQLInt) } }, - where: (usersTable, args, context) => { - return `${usersTable}.id = ${args.id}` + extensions: { + joinMonster: { + where: (usersTable, args, context) => { + return `${usersTable}.id = ${args.id}` + } + } }, resolve: (parent, args, context, resolveInfo) => { return joinMonster(resolveInfo, {}, sql => { @@ -37,7 +41,7 @@ Now you can handle queries like this, which return a single user. ```graphql { - user(id: 1) { + user(id: 1) { id email fullName @@ -61,9 +65,13 @@ const QueryRoot = new GraphQLObjectType({ args: { lastName: GraphQLString }, - where: (usersTable, args, context) => { - return escape(`${usersTable}.last_name = %L`, args.lastName) - }, + extensions: { + joinMonster: { + where: (usersTable, args, context) => { + return escape(`${usersTable}.last_name = %L`, args.lastName) + } + } + } // ... } }) @@ -87,8 +95,12 @@ For example, you can pass in the ID of the logged in user to incorporate it into return knex.raw(sql) }) }, - where: (usersTable, args, context) => { - return `${usersTable}.id = ${context.id}` + extensions: { + joinMonster: { + where: (usersTable, args, context) => { + return `${usersTable}.id = ${context.id}` + } + } } } ``` diff --git a/package-lock.json b/package-lock.json index eaaa6982..36cc5a68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "join-monster", - "version": "2.1.2", + "version": "3.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3182,6 +3182,12 @@ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "dev": true }, + "@types/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", + "dev": true + }, "@types/node": { "version": "14.0.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.11.tgz", @@ -4247,6 +4253,17 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + } + }, "caniuse-lite": { "version": "1.0.30001078", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001078.tgz", @@ -4857,6 +4874,24 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + } + } + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -5478,6 +5513,82 @@ } } }, + "eslint-formatter-pretty": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-formatter-pretty/-/eslint-formatter-pretty-4.0.0.tgz", + "integrity": "sha512-QgdeZxQwWcN0TcXXNZJiS6BizhAANFhCzkE7Yl9HKB7WjElzwED6+FbbZB2gji8ofgJTGPqKm6VRCNT3OGCeEw==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "eslint-rule-docs": "^1.1.5", + "log-symbols": "^4.0.0", + "plur": "^4.0.0", + "string-width": "^4.2.0", + "supports-hyperlinks": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "requires": { + "chalk": "^4.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "eslint-rule-docs": { + "version": "1.1.201", + "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.201.tgz", + "integrity": "sha512-HS327MkM3ebCcjAQMkhNYZbN/4Eu/NO5ipDK8uNVPqUrAPRUsXkuuEfE+DEx4YItkszKp4ND1F3hN8BwfXdx0w==", + "dev": true + }, "eslint-scope": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", @@ -7114,13 +7225,10 @@ "dev": true }, "graphql": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.13.2.tgz", - "integrity": "sha512-QZ5BL8ZO/B20VA8APauGBg3GyEgZ19eduvpLWoq5x7gMmWnHoy8rlQWPLmWgFvo1yNgjSEFMesmS4R6pPr7xog==", - "dev": true, - "requires": { - "iterall": "^1.2.1" - } + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.2.0.tgz", + "integrity": "sha512-tsceRyHfgzZo+ee0YK3o8f0CR0cXAXxRlxoORWFo/CoM1bVy3UXGWeyzBcf+Y6oqPvO27BDmOEVATcunOO/MrQ==", + "dev": true }, "graphql-relay": { "version": "0.6.0", @@ -7157,6 +7265,12 @@ } } }, + "hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -7719,6 +7833,12 @@ "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", "dev": true }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -7992,12 +8112,6 @@ "istanbul-lib-report": "^3.0.0" } }, - "iterall": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", - "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", - "dev": true - }, "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -8687,6 +8801,12 @@ "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", "dev": true }, + "map-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", + "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", + "dev": true + }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", @@ -8789,6 +8909,41 @@ } } }, + "meow": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-7.0.1.tgz", + "integrity": "sha512-tBKIQqVrAHqwit0vfuFPY3LlzJYkEOFyKa3bPgxzNl6q/RtN8KQ+ALYEASYuFayzSAsjlhXj/JZ10rH85Q6TUw==", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "arrify": "^2.0.1", + "camelcase": "^6.0.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "^4.0.2", + "normalize-package-data": "^2.5.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.13.1", + "yargs-parser": "^18.1.3" + }, + "dependencies": { + "camelcase": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz", + "integrity": "sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==", + "dev": true + }, + "type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true + } + } + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8849,6 +9004,12 @@ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -8864,6 +9025,31 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, + "minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "dependencies": { + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + } + } + }, "minipass": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", @@ -10199,6 +10385,12 @@ "escape-goat": "^2.0.0" } }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true + }, "randomatic": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", @@ -10280,6 +10472,17 @@ } } }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -10614,6 +10817,16 @@ "resolve": "^1.1.6" } }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, "reduce-extract": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/reduce-extract/-/reduce-extract-1.0.0.tgz", @@ -11667,6 +11880,15 @@ "is-utf8": "^0.2.1" } }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -11732,6 +11954,27 @@ } } }, + "supports-hyperlinks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", + "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "table": { "version": "5.4.6", "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", @@ -12009,12 +12252,40 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", "dev": true }, + "trim-newlines": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", + "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", + "dev": true + }, "trim-off-newlines": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", "dev": true }, + "tsd": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.13.1.tgz", + "integrity": "sha512-+UYM8LRG/M4H8ISTg2ow8SWi65PS7Os+4DUnyiQLbJysXBp2DEmws9SMgBH+m8zHcJZqUJQ+mtDWJXP1IAvB2A==", + "dev": true, + "requires": { + "eslint-formatter-pretty": "^4.0.0", + "globby": "^11.0.1", + "meow": "^7.0.1", + "path-exists": "^4.0.0", + "read-pkg-up": "^7.0.0", + "update-notifier": "^4.1.0" + }, + "dependencies": { + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, "tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", diff --git a/package.json b/package.json index 2682bc75..4ea7de6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "join-monster", - "version": "2.1.2", + "version": "3.0.0", "description": "A GraphQL to SQL query execution layer for batch data fetching.", "main": "dist/index.js", "engines": { @@ -21,6 +21,7 @@ "testoracle-paging": "NODE_ENV=test DB=ORACLE PAGINATE=offset ava test/pagination/offset-paging.js && NODE_ENV=test DB=ORACLE PAGINATE=keyset ava test/pagination/keyset-paging.js", "testmysql": "NODE_ENV=test DB=MYSQL ava test/*.js", "testmysql-paging": "NODE_ENV=test DB=MYSQL PAGINATE=offset ava test/pagination/offset-paging.js && NODE_ENV=test DB=MYSQL PAGINATE=keyset ava test/pagination/keyset-paging.js", + "testtsd": "npm run build && tsd", "coverage": "nyc --reporter=html npm run test", "view-coverage": "open coverage/index.html", "lint": "eslint src test", @@ -73,7 +74,7 @@ }, "homepage": "https://github.com/join-monster/join-monster#readme", "peerDependencies": { - "graphql": "0.6 || 0.7 || 0.8 || 0.9 || 0.10 || 0.11 || 0.12 || 0.13" + "graphql": "^15.2.0" }, "devDependencies": { "@ava/babel": "^1.0.1", @@ -93,7 +94,7 @@ "eslint-config-airbnb-base": "^14.1.0", "eslint-config-prettier": "^6.11.0", "faker": "^4.1.0", - "graphql": "^0.13.0", + "graphql": "^15.2.0", "graphsiql": "0.2.0", "idx": "^2.5.6", "jsdoc-to-markdown": "^5.0.0", @@ -108,7 +109,8 @@ "nyc": "^15.0.1", "pg": "^8.2.1", "sinon": "^9.0.2", - "sqlite3": "^4.2.0" + "sqlite3": "^4.2.0", + "tsd": "^0.13.1" }, "dependencies": { "@stem/nesthydrationjs": "0.4.0", diff --git a/src/index.d.ts b/src/index.d.ts index 0cec5d69..6a9d7bfa 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,88 +1,150 @@ - import * as graphql from 'graphql' +export type Maybe = null | undefined | T // Extend graphql objects and fields -declare module 'graphql/type/definition' { - type SqlJoin = (table1: string, table2: string, args: TArgs, context: TContext, sqlASTNode: any) => string - type Where = (usersTable: string, args: TArgs, context: TContext, sqlASTNode: any) => string | void - type Order = 'ASC' | 'asc' | 'DESC' | 'desc' - type OrderBy = string | { [key: string]: Order } - type ThunkWithArgsCtx = ((args: TArgs, context: TContext) => T) | T; +export type SqlJoin = ( + table1: string, + table2: string, + args: TArgs, + context: TContext, + sqlASTNode: any +) => string +export type Where = ( + usersTable: string, + args: TArgs, + context: TContext, + sqlASTNode: any +) => string | void +export type Order = 'ASC' | 'asc' | 'DESC' | 'desc' +export type OrderBy = string | { [key: string]: Order } +export type ThunkWithArgsCtx = + | ((args: TArgs, context: TContext) => T) + | T - export interface GraphQLObjectTypeConfig { - alwaysFetch?: string - sqlTable?: ThunkWithArgsCtx - uniqueKey?: string | string[] - } +export interface ObjectTypeExtension { + alwaysFetch?: string + sqlTable?: ThunkWithArgsCtx + uniqueKey?: string | string[] +} - export interface GraphQLFieldConfig { - jmIgnoreAll?: boolean - jmIgnoreTable?: boolean - junction?: { - include?: ThunkWithArgsCtx<{ - sqlColumn?: string - sqlExpr?: string - sqlDeps?: string | string[] - }, TContext, TArgs> - orderBy?: ThunkWithArgsCtx - sortKey?: ThunkWithArgsCtx<{ +export interface FieldConfigExtension { + ignoreAll?: boolean + ignoreTable?: boolean + junction?: { + include?: ThunkWithArgsCtx< + { + [column: string]: { + sqlColumn?: string + sqlExpr?: string + sqlDeps?: string | string[] + } + }, + TContext, + TArgs + > + orderBy?: ThunkWithArgsCtx + sortKey?: ThunkWithArgsCtx< + { order: Order key: string | string[] - }, TContext, TArgs> - sqlBatch?: { - thisKey: string - parentKey: string - sqlJoin: SqlJoin - } - sqlJoins?: [SqlJoin, SqlJoin] - sqlTable: ThunkWithArgsCtx - uniqueKey?: string | string[] - where?: Where - } - limit?: ThunkWithArgsCtx - orderBy?: ThunkWithArgsCtx - sortKey?: ThunkWithArgsCtx<{ - order: Order - key: string | string[] - }, TContext, TArgs> + }, + TContext, + TArgs + > sqlBatch?: { thisKey: string parentKey: string + sqlJoin: SqlJoin } - sqlColumn?: string - sqlDeps?: string[] - sqlExpr?: (table: string, args: TArgs, context: TContext, sqlASTNode: any) => string - sqlJoin?: SqlJoin - sqlPaginate?: boolean + sqlJoins?: [SqlJoin, SqlJoin] + sqlTable: ThunkWithArgsCtx + uniqueKey?: string | string[] where?: Where } + limit?: ThunkWithArgsCtx + orderBy?: ThunkWithArgsCtx + sortKey?: ThunkWithArgsCtx< + { + order: Order + key: string | string[] + }, + TContext, + TArgs + > + sqlBatch?: { + thisKey: string + parentKey: string + } + sqlColumn?: string + sqlDeps?: string[] + sqlExpr?: ( + table: string, + args: TArgs, + context: TContext, + sqlASTNode: any + ) => string + sqlJoin?: SqlJoin + sqlPaginate?: boolean + where?: Where } -export interface GraphQLUnionTypeConfig { +export interface UnionTypeExtension { sqlTable?: string uniqueKey?: string | string[] alwaysFetch?: string } -export interface GraphQLInterfaceTypeConfig { +export interface InterfaceTypeExtension { sqlTable?: string uniqueKey?: string | string[] alwaysFetch?: string } +declare module 'graphql' { + interface GraphQLObjectTypeExtensions { + joinMonster?: ObjectTypeExtension + } + interface GraphQLFieldExtensions< + TSource, + TContext, + TArgs = { [argName: string]: any } + > { + joinMonster?: FieldConfigExtension + } + interface GraphQLUnionTypeExtensions { + joinMonster?: UnionTypeExtension + } + interface GraphQLInterfaceTypeExtensions { + joinMonster?: InterfaceTypeExtension + } +} + // JoinMonster lib interface -interface DialectModule { name: string } +interface DialectModule { + name: string +} type Dialect = 'pg' | 'oracle' | 'mariadb' | 'mysql' | 'mysql8' | 'sqlite3' -type JoinMonsterOptions = { minify?: boolean, dialect?: Dialect, dialectModule?: DialectModule } +type JoinMonsterOptions = { + minify?: boolean + dialect?: Dialect + dialectModule?: DialectModule +} type Rows = any -type DbCallCallback = (sql:string, done: (err?: any, rows?: Rows) => void) => void +type DbCallCallback = ( + sql: string, + done: (err?: any, rows?: Rows) => void +) => void type DbCallPromise = (sql: string) => Promise -type DbCall = DbCallCallback | DbCallPromise -declare function joinMonster(resolveInfo: any, context: any, dbCall: DbCallCallback | DbCallPromise, options?: JoinMonsterOptions) : Promise +declare function joinMonster( + resolveInfo: any, + context: any, + dbCall: DbCallCallback | DbCallPromise, + options?: JoinMonsterOptions +): Promise export default joinMonster diff --git a/src/index.js b/src/index.js index cc1c805e..7eaf650e 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,12 @@ import * as queryAST from './query-ast-to-sql-ast' import arrToConnection from './array-to-connection' import AliasNamespace from './alias-namespace' import nextBatch from './batch-planner' -import { buildWhereFunction, handleUserDbCall, compileSqlAST } from './util' +import { + buildWhereFunction, + handleUserDbCall, + compileSqlAST, + getConfigFromSchemaObject +} from './util' /* _ _ _ _ ___ __ _| | | |__ __ _ ___| | __ @@ -135,7 +140,7 @@ async function getNode( const type = resolveInfo.schema._typeMap[typeName] assert(type, `Type "${typeName}" not found in your schema.`) assert( - type._typeConfig.sqlTable, + getConfigFromSchemaObject(type).sqlTable, `joinMonster can't fetch a ${typeName} as a Node unless it has "sqlTable" tagged.` ) @@ -148,7 +153,12 @@ async function getNode( node: { type, name: type.name.toLowerCase(), - where + args: {}, + extensions: { + joinMonster: { + where + } + } } } } diff --git a/src/query-ast-to-sql-ast/index.js b/src/query-ast-to-sql-ast/index.js index dc09b507..45f9b818 100644 --- a/src/query-ast-to-sql-ast/index.js +++ b/src/query-ast-to-sql-ast/index.js @@ -5,7 +5,13 @@ import { getArgumentValues } from 'graphql/execution/values' import idx from 'idx' import AliasNamespace from '../alias-namespace' -import { wrap, ensure, unthunk, inspect } from '../util' +import { + wrap, + ensure, + unthunk, + inspect, + getConfigFromSchemaObject +} from '../util' class SQLASTNode { constructor(parentNode, props) { @@ -129,15 +135,24 @@ export function populateASTNode( let fieldIncludes if (idx(sqlASTNode, _ => _.parent.junction.include[fieldName])) { fieldIncludes = sqlASTNode.parent.junction.include[fieldName] + field = { ...field, - ...fieldIncludes + extensions: { + ...(field.extensions || {}), + joinMonster: { + ...getConfigFromSchemaObject(field), + ...fieldIncludes + } + } } sqlASTNode.fromOtherTable = sqlASTNode.parent.junction.as } + const fieldConfig = getConfigFromSchemaObject(field) + // allow for explicit ignoring of fields - if (field.jmIgnoreAll) { + if (fieldConfig.ignoreAll) { sqlASTNode.type = 'noop' return } @@ -168,34 +183,34 @@ export function populateASTNode( gqlType = stripNonNullType(stripped.gqlType) queryASTNode = stripped.queryASTNode // we'll set a flag for pagination. - if (field.sqlPaginate) { + if (fieldConfig.sqlPaginate) { sqlASTNode.paginate = true } - } else if (field.sqlPaginate) { + } else if (fieldConfig.sqlPaginate) { throw new Error( `To paginate the ${gqlType.name} type, it must be a GraphQLObjectType that fulfills the relay spec. The type must have a "pageInfo" and "edges" field. https://facebook.github.io/relay/graphql/connections.htm` ) } - // the typeConfig has all the keyes from the GraphQLObjectType definition - const config = gqlType._typeConfig + const config = getConfigFromSchemaObject(gqlType) // is this a table in SQL? if ( - !field.jmIgnoreTable && + !fieldConfig.ignoreTable && TABLE_TYPES.includes(gqlType.constructor.name) && config.sqlTable ) { if (depth >= 1) { assert( - !field.junctionTable, + !fieldConfig.junctionTable, '"junctionTable" has been replaced with a new API.' ) assert( - field.sqlJoin || field.sqlBatch || field.junction, + fieldConfig.sqlJoin || fieldConfig.sqlBatch || fieldConfig.junction, `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 "junction" on that field to tell joinMonster how to fetch it. - Or you can ignore it with "jmIgnoreTable". Check the "${fieldName}" field on the "${parentTypeNode.name}" type.` + you must define "sqlJoin", "sqlBatch", or "junction" on that field's extensions to tell joinMonster how to fetch it. + Or you can ignore it with "ignoreTable". + Check the extensions.joinMonster property of "${fieldName}" field on the "${parentTypeNode.name}" type.` ) } handleTable.call( @@ -211,27 +226,27 @@ export function populateASTNode( context ) // is this a computed column from a raw expression? - } else if (field.sqlExpr) { + } else if (fieldConfig.sqlExpr) { sqlASTNode.type = 'expression' - sqlASTNode.sqlExpr = field.sqlExpr + sqlASTNode.sqlExpr = fieldConfig.sqlExpr let aliasFrom = (sqlASTNode.fieldName = field.name) if (sqlASTNode.defferedFrom) { aliasFrom += '@' + parentTypeNode.name } sqlASTNode.as = namespace.generate('column', aliasFrom) // is it just a column? if they specified a sqlColumn or they didn't define a resolver, yeah - } else if (field.sqlColumn || !field.resolve) { + } else if (fieldConfig.sqlColumn || !field.resolve) { sqlASTNode.type = 'column' - sqlASTNode.name = field.sqlColumn || field.name + sqlASTNode.name = fieldConfig.sqlColumn || field.name let aliasFrom = (sqlASTNode.fieldName = field.name) if (sqlASTNode.defferedFrom) { aliasFrom += '@' + parentTypeNode.name } sqlASTNode.as = namespace.generate('column', aliasFrom) // or maybe it just depends on some SQL columns - } else if (field.sqlDeps) { + } else if (fieldConfig.sqlDeps) { sqlASTNode.type = 'columnDeps' - sqlASTNode.names = field.sqlDeps + sqlASTNode.names = fieldConfig.sqlDeps // maybe this node wants no business with your SQL, because it has its own resolver } else { sqlASTNode.type = 'noop' @@ -249,7 +264,8 @@ function handleTable( options, context ) { - const config = gqlType._typeConfig + const config = getConfigFromSchemaObject(gqlType) + const fieldConfig = getConfigFromSchemaObject(field) sqlASTNode.type = 'table' const sqlTable = unthunk(config.sqlTable, sqlASTNode.args || {}, context) @@ -259,9 +275,9 @@ function handleTable( // if thats taken, this function will just add an underscore to the end to make it unique sqlASTNode.as = namespace.generate('table', field.name) - if (field.orderBy && !sqlASTNode.orderBy) { + if (fieldConfig.orderBy && !sqlASTNode.orderBy) { sqlASTNode.orderBy = handleOrderBy( - unthunk(field.orderBy, sqlASTNode.args || {}, context) + unthunk(fieldConfig.orderBy, sqlASTNode.args || {}, context) ) } @@ -271,8 +287,8 @@ function handleTable( sqlASTNode.fieldName = field.name sqlASTNode.grabMany = grabMany - if (field.where) { - sqlASTNode.where = field.where + if (fieldConfig.where) { + sqlASTNode.where = fieldConfig.where } /* @@ -281,12 +297,12 @@ function handleTable( */ // are they doing a one-to-many sql join? - if (field.sqlJoin) { - sqlASTNode.sqlJoin = field.sqlJoin + if (fieldConfig.sqlJoin) { + sqlASTNode.sqlJoin = fieldConfig.sqlJoin // or a many-to-many? - } else if (field.junction) { + } else if (fieldConfig.junction) { const junctionTable = unthunk( - ensure(field.junction, 'sqlTable'), + ensure(fieldConfig.junction, 'sqlTable'), sqlASTNode.args || {}, context ) @@ -294,42 +310,42 @@ function handleTable( sqlTable: junctionTable, as: namespace.generate('table', junctionTable) }) - if (field.junction.include) { + if (fieldConfig.junction.include) { junction.include = unthunk( - field.junction.include, + fieldConfig.junction.include, sqlASTNode.args || {}, context ) } - if (field.junction.orderBy) { + if (fieldConfig.junction.orderBy) { junction.orderBy = handleOrderBy( - unthunk(field.junction.orderBy, sqlASTNode.args || {}, context) + unthunk(fieldConfig.junction.orderBy, sqlASTNode.args || {}, context) ) } - if (field.junction.where) { - junction.where = field.junction.where + if (fieldConfig.junction.where) { + junction.where = fieldConfig.junction.where } // are they joining or batching? - if (field.junction.sqlJoins) { - junction.sqlJoins = field.junction.sqlJoins - } else if (field.junction.sqlBatch) { + if (fieldConfig.junction.sqlJoins) { + junction.sqlJoins = fieldConfig.junction.sqlJoins + } else if (fieldConfig.junction.sqlBatch) { children.push({ - ...keyToASTChild(ensure(field.junction, 'uniqueKey'), namespace), + ...keyToASTChild(ensure(fieldConfig.junction, 'uniqueKey'), namespace), fromOtherTable: junction.as }) junction.sqlBatch = { - sqlJoin: ensure(field.junction.sqlBatch, 'sqlJoin'), + sqlJoin: ensure(fieldConfig.junction.sqlBatch, 'sqlJoin'), thisKey: { ...columnToASTChild( - ensure(field.junction.sqlBatch, 'thisKey'), + ensure(fieldConfig.junction.sqlBatch, 'thisKey'), namespace ), fromOtherTable: junction.as }, parentKey: columnToASTChild( - ensure(field.junction.sqlBatch, 'parentKey'), + ensure(fieldConfig.junction.sqlBatch, 'parentKey'), namespace ) } @@ -337,19 +353,26 @@ function handleTable( throw new Error('junction requires either a `sqlJoins` or `sqlBatch`') } // or are they doing a one-to-many with batching - } else if (field.sqlBatch) { + } else if (fieldConfig.sqlBatch) { sqlASTNode.sqlBatch = { - thisKey: columnToASTChild(ensure(field.sqlBatch, 'thisKey'), namespace), + thisKey: columnToASTChild( + ensure(fieldConfig.sqlBatch, 'thisKey'), + namespace + ), parentKey: columnToASTChild( - ensure(field.sqlBatch, 'parentKey'), + ensure(fieldConfig.sqlBatch, 'parentKey'), namespace ) } } - if (field.limit) { - assert(field.orderBy, '`orderBy` is required with `limit`') - sqlASTNode.limit = unthunk(field.limit, sqlASTNode.args || {}, context) + if (fieldConfig.limit) { + assert(fieldConfig.orderBy, '`orderBy` is required with `limit`') + sqlASTNode.limit = unthunk( + fieldConfig.limit, + sqlASTNode.args || {}, + context + ) } if (sqlASTNode.paginate) { @@ -793,25 +816,31 @@ export function pruneDuplicateSqlDeps(sqlAST, namespace) { } function getSortColumns(field, sqlASTNode, context) { - if (field.sortKey) { - sqlASTNode.sortKey = unthunk(field.sortKey, sqlASTNode.args || {}, context) + const fieldConfig = getConfigFromSchemaObject(field) + + if (fieldConfig.sortKey) { + sqlASTNode.sortKey = unthunk( + fieldConfig.sortKey, + sqlASTNode.args || {}, + context + ) } - if (field.orderBy) { + if (fieldConfig.orderBy) { sqlASTNode.orderBy = handleOrderBy( - unthunk(field.orderBy, sqlASTNode.args || {}, context) + unthunk(fieldConfig.orderBy, sqlASTNode.args || {}, context) ) } - if (field.junction) { - if (field.junction.sortKey) { + if (fieldConfig.junction) { + if (fieldConfig.junction.sortKey) { sqlASTNode.junction.sortKey = unthunk( - field.junction.sortKey, + fieldConfig.junction.sortKey, sqlASTNode.args || {}, context ) } - if (field.junction.orderBy) { + if (fieldConfig.junction.orderBy) { sqlASTNode.junction.orderBy = handleOrderBy( - unthunk(field.junction.orderBy, sqlASTNode.args || {}, context) + unthunk(fieldConfig.junction.orderBy, sqlASTNode.args || {}, context) ) } } diff --git a/src/util.js b/src/util.js index 01488105..69ffbbef 100644 --- a/src/util.js +++ b/src/util.js @@ -1,5 +1,6 @@ import util from 'util' import assert from 'assert' +import idx from 'idx' import { nest } from '@stem/nesthydrationjs' import stringifySQL from './stringifiers/dispatcher' import resolveUnions from './resolve-unions' @@ -51,6 +52,10 @@ export function validateSqlAST(topNode) { assert(topNode.sqlJoin == null, 'root level field can not have "sqlJoin"') } +export function getConfigFromSchemaObject(fieldOrType) { + return idx(fieldOrType, _ => _.extensions.joinMonster) || {} +} + export function objToCursor(obj) { const str = JSON.stringify(obj) return Buffer.from(str).toString('base64') @@ -129,7 +134,7 @@ export function buildWhereFunction(type, condition, options) { const quote = ['mysql', 'mysql8', 'mariadb'].includes(name) ? '`' : '"' // determine the unique key so we know what to search by - const uniqueKey = type._typeConfig.uniqueKey + const uniqueKey = getConfigFromSchemaObject(type).uniqueKey // handle composite keys if (Array.isArray(uniqueKey)) { diff --git a/test-api/schema-basic/Authored/Interface.js b/test-api/schema-basic/Authored/Interface.js index 99b3f33c..939fcc8f 100644 --- a/test-api/schema-basic/Authored/Interface.js +++ b/test-api/schema-basic/Authored/Interface.js @@ -6,27 +6,31 @@ const { DB } = process.env export default new GraphQLInterfaceType({ name: 'AuthoredInterface', - sqlTable: `( - SELECT - ${q('id', DB)}, - ${q('body', DB)}, - ${q('author_id', DB)}, - NULL AS ${q('post_id', DB)}, - ${q('created_at', DB)}, - 'Post' AS ${q('$type', DB)} - FROM ${q('posts', DB)} - UNION ALL - SELECT - ${q('id', DB)}, - ${q('body', DB)}, - ${q('author_id', DB)}, - ${q('post_id', DB)}, - ${q('created_at', DB)}, - 'Comment' AS ${q('$type', DB)} - FROM ${q('comments', DB)} - )`, - uniqueKey: ['id', '$type'], - alwaysFetch: '$type', + extensions: { + joinMonster: { + sqlTable: `( + SELECT + ${q('id', DB)}, + ${q('body', DB)}, + ${q('author_id', DB)}, + NULL AS ${q('post_id', DB)}, + ${q('created_at', DB)}, + 'Post' AS ${q('$type', DB)} + FROM ${q('posts', DB)} + UNION ALL + SELECT + ${q('id', DB)}, + ${q('body', DB)}, + ${q('author_id', DB)}, + ${q('post_id', DB)}, + ${q('created_at', DB)}, + 'Comment' AS ${q('$type', DB)} + FROM ${q('comments', DB)} + )`, + uniqueKey: ['id', '$type'], + alwaysFetch: '$type' + } + }, fields: () => ({ id: { type: GraphQLInt @@ -36,7 +40,11 @@ export default new GraphQLInterfaceType({ }, authorId: { type: GraphQLInt, - sqlColumn: 'author_id' + extensions: { + joinMonster: { + sqlColumn: 'author_id' + } + } } }), resolveType: obj => obj.$type diff --git a/test-api/schema-basic/Authored/Union.js b/test-api/schema-basic/Authored/Union.js index a8a1bfe1..ae784675 100644 --- a/test-api/schema-basic/Authored/Union.js +++ b/test-api/schema-basic/Authored/Union.js @@ -8,27 +8,31 @@ const { DB } = process.env export default new GraphQLUnionType({ name: 'AuthoredUnion', - sqlTable: `( - SELECT - ${q('id', DB)}, - ${q('body', DB)}, - ${q('author_id', DB)}, - NULL AS ${q('post_id', DB)}, - ${q('created_at', DB)}, - 'Post' AS ${q('$type', DB)} - FROM ${q('posts', DB)} - UNION ALL - SELECT - ${q('id', DB)}, - ${q('body', DB)}, - ${q('author_id', DB)}, - ${q('post_id', DB)}, - ${q('created_at', DB)}, - 'Comment' AS ${q('$type', DB)} - FROM ${q('comments', DB)} - )`, - uniqueKey: ['id', '$type'], + extensions: { + joinMonster: { + sqlTable: `( + SELECT + ${q('id', DB)}, + ${q('body', DB)}, + ${q('author_id', DB)}, + NULL AS ${q('post_id', DB)}, + ${q('created_at', DB)}, + 'Post' AS ${q('$type', DB)} + FROM ${q('posts', DB)} + UNION ALL + SELECT + ${q('id', DB)}, + ${q('body', DB)}, + ${q('author_id', DB)}, + ${q('post_id', DB)}, + ${q('created_at', DB)}, + 'Comment' AS ${q('$type', DB)} + FROM ${q('comments', DB)} + )`, + uniqueKey: ['id', '$type'], + alwaysFetch: '$type' + } + }, types: () => [Comment, Post], - alwaysFetch: '$type', resolveType: obj => obj.$type }) diff --git a/test-api/schema-basic/Comment.js b/test-api/schema-basic/Comment.js index 82cc2b9c..06e41575 100644 --- a/test-api/schema-basic/Comment.js +++ b/test-api/schema-basic/Comment.js @@ -16,8 +16,12 @@ const { STRATEGY, DB } = process.env export default new GraphQLObjectType({ description: 'Comments on posts', name: 'Comment', - sqlTable: q('comments', DB), - uniqueKey: 'id', + extensions: { + joinMonster: { + sqlTable: q('comments', DB), + uniqueKey: 'id' + } + }, interfaces: () => [Authored], fields: () => ({ id: { @@ -29,62 +33,85 @@ export default new GraphQLObjectType({ }, postId: { type: GraphQLInt, - sqlColumn: 'post_id' + extensions: { + joinMonster: { + sqlColumn: 'post_id' + } + } }, post: { description: 'The post that the comment belongs to', type: Post, - ...(STRATEGY === 'batch' - ? { - sqlBatch: { - thisKey: 'id', - parentKey: 'post_id' - } - } - : { - sqlJoin: (commentTable, postTable) => - `${commentTable}.${q('post_id', DB)} = ${postTable}.${q( - 'id', - DB - )}` - }) + extensions: { + joinMonster: { + ...(STRATEGY === 'batch' + ? { + sqlBatch: { + thisKey: 'id', + parentKey: 'post_id' + } + } + : { + sqlJoin: (commentTable, postTable) => + `${commentTable}.${q('post_id', DB)} = ${postTable}.${q( + 'id', + DB + )}` + }) + } + } }, authorId: { type: GraphQLInt, - sqlColumn: 'author_id' + extensions: { + joinMonster: { + sqlColumn: 'author_id' + } + } }, author: { description: 'The user who wrote the comment', type: User, - ...(STRATEGY === 'batch' - ? { - sqlBatch: { - thisKey: 'id', - parentKey: 'author_id' - } - } - : { - sqlJoin: (commentTable, userTable) => - `${commentTable}.${q('author_id', DB)} = ${userTable}.${q( - 'id', - DB - )}` - }) + extensions: { + joinMonster: { + ...(STRATEGY === 'batch' + ? { + sqlBatch: { + thisKey: 'id', + parentKey: 'author_id' + } + } + : { + sqlJoin: (commentTable, userTable) => + `${commentTable}.${q('author_id', DB)} = ${userTable}.${q( + 'id', + DB + )}` + }) + } + } }, likers: { description: 'Which users have liked this comment', type: new GraphQLList(User), - junction: { - sqlTable: q('likes', DB), - sqlJoins: [ - (commentTable, likesTable) => - `${commentTable}.${q('id', DB)} = ${likesTable}.${q( - 'comment_id', - DB - )}`, - (likesTable, userTable) => - `${likesTable}.${q('account_id', DB)} = ${userTable}.${q('id', DB)}` - ] + extensions: { + joinMonster: { + junction: { + sqlTable: q('likes', DB), + sqlJoins: [ + (commentTable, likesTable) => + `${commentTable}.${q('id', DB)} = ${likesTable}.${q( + 'comment_id', + DB + )}`, + (likesTable, userTable) => + `${likesTable}.${q('account_id', DB)} = ${userTable}.${q( + 'id', + DB + )}` + ] + } + } } }, archived: { @@ -93,7 +120,11 @@ export default new GraphQLObjectType({ createdAt: { description: 'When this was created', type: GraphQLString, - sqlColumn: 'created_at' + extensions: { + joinMonster: { + sqlColumn: 'created_at' + } + } } }) }) diff --git a/test-api/schema-basic/Post.js b/test-api/schema-basic/Post.js index 3177d0ef..b01a282a 100644 --- a/test-api/schema-basic/Post.js +++ b/test-api/schema-basic/Post.js @@ -16,8 +16,12 @@ const { STRATEGY, DB } = process.env export default new GraphQLObjectType({ description: 'A post from a user', name: 'Post', - sqlTable: q('posts', DB), - uniqueKey: 'id', + extensions: { + joinMonster: { + sqlTable: q('posts', DB), + uniqueKey: 'id' + } + }, interfaces: () => [Authored], fields: () => ({ id: { @@ -29,22 +33,33 @@ export default new GraphQLObjectType({ }, authorId: { type: GraphQLInt, - sqlColumn: 'author_id' + extensions: { + joinMonster: { + sqlColumn: 'author_id' + } + } }, author: { description: 'The user that created the post', type: User, - ...(STRATEGY === 'batch' - ? { - sqlBatch: { - thisKey: 'id', - parentKey: 'author_id' - } - } - : { - sqlJoin: (postTable, userTable) => - `${postTable}.${q('author_id', DB)} = ${userTable}.${q('id', DB)}` - }) + extensions: { + joinMonster: { + ...(STRATEGY === 'batch' + ? { + sqlBatch: { + thisKey: 'id', + parentKey: 'author_id' + } + } + : { + sqlJoin: (postTable, userTable) => + `${postTable}.${q('author_id', DB)} = ${userTable}.${q( + 'id', + DB + )}` + }) + } + } }, comments: { description: 'The comments on this post', @@ -53,42 +68,50 @@ export default new GraphQLObjectType({ active: { type: GraphQLBoolean }, asc: { type: GraphQLBoolean } }, - orderBy: args => ({ id: args.asc ? 'asc' : 'desc' }), - ...(['batch', 'mix'].includes(STRATEGY) - ? { - sqlBatch: { - thisKey: 'post_id', - parentKey: 'id' - }, - where: (table, args) => - args.active - ? `${table}.${q('archived', DB)} = ${bool(false, DB)}` - : null - } - : { - sqlJoin: (postTable, commentTable, args) => - `${commentTable}.${q('post_id', DB)} = ${postTable}.${q( - 'id', - DB - )} ${ - args.active - ? `AND ${commentTable}.${q('archived', DB)} = ${bool( - false, - DB - )}` - : '' - }` - }) + extensions: { + joinMonster: { + orderBy: args => ({ id: args.asc ? 'asc' : 'desc' }), + ...(['batch', 'mix'].includes(STRATEGY) + ? { + sqlBatch: { + thisKey: 'post_id', + parentKey: 'id' + }, + where: (table, args) => + args.active + ? `${table}.${q('archived', DB)} = ${bool(false, DB)}` + : null + } + : { + sqlJoin: (postTable, commentTable, args) => + `${commentTable}.${q('post_id', DB)} = ${postTable}.${q( + 'id', + DB + )} ${ + args.active + ? `AND ${commentTable}.${q('archived', DB)} = ${bool( + false, + DB + )}` + : '' + }` + }) + } + } }, numComments: { description: 'How many comments this post has', type: GraphQLInt, - // you can info from a correlated subquery - sqlExpr: table => - `(SELECT count(*) from ${q('comments', DB)} WHERE ${table}.${q( - 'id', - DB - )} = ${q('comments', DB)}.${q('post_id', DB)})` + extensions: { + joinMonster: { + // you can info from a correlated subquery + sqlExpr: table => + `(SELECT count(*) from ${q('comments', DB)} WHERE ${table}.${q( + 'id', + DB + )} = ${q('comments', DB)}.${q('post_id', DB)})` + } + } }, archived: { type: GraphQLBoolean diff --git a/test-api/schema-basic/QueryRoot.js b/test-api/schema-basic/QueryRoot.js index 92f7f99d..feae1a02 100644 --- a/test-api/schema-basic/QueryRoot.js +++ b/test-api/schema-basic/QueryRoot.js @@ -54,9 +54,13 @@ export default new GraphQLObjectType({ args: { ids: { type: new GraphQLList(GraphQLInt) } }, - where: (table, args) => - args.ids ? `${table}.id IN (${args.ids.join(',')})` : null, - orderBy: 'id', + extensions: { + joinMonster: { + where: (table, args) => + args.ids ? `${table}.id IN (${args.ids.join(',')})` : null, + orderBy: 'id' + } + }, resolve: async (parent, args, context, resolveInfo) => { return joinMonster( resolveInfo, @@ -82,15 +86,21 @@ export default new GraphQLObjectType({ type: GraphQLInt } }, - where: (usersTable, args, context) => { - // eslint-disable-line no-unused-vars - if (args.id) return `${usersTable}.${q('id', DB)} = ${args.id}` - if (args.idEncoded) - return `${usersTable}.${q('id', DB)} = ${fromBase64(args.idEncoded)}` - if (args.idAsync) - return Promise.resolve( - `${usersTable}.${q('id', DB)} = ${args.idAsync}` - ) + extensions: { + joinMonster: { + where: (usersTable, args, context) => { + // eslint-disable-line no-unused-vars + if (args.id) return `${usersTable}.${q('id', DB)} = ${args.id}` + if (args.idEncoded) + return `${usersTable}.${q('id', DB)} = ${fromBase64( + args.idEncoded + )}` + if (args.idAsync) + return Promise.resolve( + `${usersTable}.${q('id', DB)} = ${args.idAsync}` + ) + } + } }, resolve: (parent, args, context, resolveInfo) => { return joinMonster( @@ -109,10 +119,14 @@ export default new GraphQLObjectType({ type: GraphQLBoolean } }, - where: (sponsorsTable, args, context) => { - // eslint-disable-line no-unused-vars - if (args.filterLegless) - return `${sponsorsTable}.${q('num_legs', DB)} IS NULL` + extensions: { + joinMonster: { + where: (sponsorsTable, args, context) => { + // eslint-disable-line no-unused-vars + if (args.filterLegless) + return `${sponsorsTable}.${q('num_legs', DB)} IS NULL` + } + } }, resolve: (parent, args, context, resolveInfo) => { // use the callback version this time diff --git a/test-api/schema-basic/Sponsor.js b/test-api/schema-basic/Sponsor.js index c309e051..09c71f1f 100644 --- a/test-api/schema-basic/Sponsor.js +++ b/test-api/schema-basic/Sponsor.js @@ -8,21 +8,37 @@ const { DB } = process.env const Sponsor = new GraphQLObjectType({ description: 'people who have given money', name: 'Sponsor', - sqlTable: q('sponsors', DB), - uniqueKey: ['generation', 'first_name', 'last_name'], + extensions: { + joinMonster: { + sqlTable: q('sponsors', DB), + uniqueKey: ['generation', 'first_name', 'last_name'] + } + }, interfaces: [Person], fields: () => ({ firstName: { type: GraphQLString, - sqlColumn: 'first_name' + extensions: { + joinMonster: { + sqlColumn: 'first_name' + } + } }, lastName: { type: GraphQLString, - sqlColumn: 'last_name' + extensions: { + joinMonster: { + sqlColumn: 'last_name' + } + } }, fullName: { type: GraphQLString, - sqlDeps: ['first_name', 'last_name'], + extensions: { + joinMonster: { + sqlDeps: ['first_name', 'last_name'] + } + }, resolve: sponsor => `${sponsor.first_name} ${sponsor.last_name}` }, generation: { @@ -31,12 +47,20 @@ const Sponsor = new GraphQLObjectType({ numLegs: { description: 'How many legs this user has', type: GraphQLInt, - sqlColumn: 'num_legs' + extensions: { + joinMonster: { + sqlColumn: 'num_legs' + } + } }, numFeet: { description: 'How many feet this user has', type: GraphQLInt, - sqlDeps: ['num_legs'], + extensions: { + joinMonster: { + sqlDeps: ['num_legs'] + } + }, resolve: user => user.num_legs } }) diff --git a/test-api/schema-basic/User.js b/test-api/schema-basic/User.js index 5b9d7d62..c0a519b7 100644 --- a/test-api/schema-basic/User.js +++ b/test-api/schema-basic/User.js @@ -22,8 +22,12 @@ const { STRATEGY, DB } = process.env const User = new GraphQLObjectType({ description: 'a stem contract account', name: 'User', - sqlTable: () => q('accounts', DB), - uniqueKey: 'id', + extensions: { + joinMonster: { + sqlTable: () => q('accounts', DB), + uniqueKey: 'id' + } + }, interfaces: [Person], fields: () => ({ id: { @@ -31,29 +35,50 @@ const User = new GraphQLObjectType({ }, email: { type: GraphQLString, - sqlColumn: 'email_address' + extensions: { + joinMonster: { + sqlColumn: 'email_address' + } + } }, idEncoded: { description: 'The ID base-64 encoded', type: GraphQLString, - sqlColumn: 'id', + extensions: { + joinMonster: { + sqlColumn: 'id' + } + }, resolve: user => toBase64(user.idEncoded) }, globalId: { description: 'The global ID for the Relay spec', ...globalIdField('User'), - sqlDeps: ['id'] + extensions: { + joinMonster: { + sqlDeps: ['id'] + } + } }, fullName: { description: "A user's first and last name", type: GraphQLString, - sqlDeps: ['first_name', 'last_name'], + extensions: { + joinMonster: { + sqlDeps: ['first_name', 'last_name'] + } + }, resolve: user => `${user.first_name} ${user.last_name}` }, capitalizedLastName: { description: 'The last name WITH CAPS LOCK', type: GraphQLString, - sqlExpr: (table, args, context) => `upper(${table}.${q('last_name', DB)})` // eslint-disable-line no-unused-vars + extensions: { + joinMonster: { + sqlExpr: (table, args, context) => + `upper(${table}.${q('last_name', DB)})` // eslint-disable-line no-unused-vars + } + } }, comments: { description: "Comments the user has written on people's posts", @@ -64,32 +89,36 @@ const User = new GraphQLObjectType({ type: GraphQLBoolean } }, - orderBy: { id: 'asc' }, - ...(['batch', 'mix'].includes(STRATEGY) - ? { - sqlBatch: { - thisKey: 'author_id', - parentKey: 'id' - }, - where: (table, args) => - args.active - ? `${table}.${q('archived', DB)} = ${bool(false, DB)}` - : null - } - : { - sqlJoin: (userTable, commentTable, args) => - `${commentTable}.${q('author_id', DB)} = ${userTable}.${q( - 'id', - DB - )} ${ - args.active - ? `AND ${commentTable}.${q('archived', DB)} = ${bool( - false, - DB - )}` - : '' - }` - }) + extensions: { + joinMonster: { + orderBy: { id: 'asc' }, + ...(['batch', 'mix'].includes(STRATEGY) + ? { + sqlBatch: { + thisKey: 'author_id', + parentKey: 'id' + }, + where: (table, args) => + args.active + ? `${table}.${q('archived', DB)} = ${bool(false, DB)}` + : null + } + : { + sqlJoin: (userTable, commentTable, args) => + `${commentTable}.${q('author_id', DB)} = ${userTable}.${q( + 'id', + DB + )} ${ + args.active + ? `AND ${commentTable}.${q('archived', DB)} = ${bool( + false, + DB + )}` + : '' + }` + }) + } + } }, posts: { description: 'A list of Posts the user has written', @@ -100,22 +129,29 @@ const User = new GraphQLObjectType({ type: GraphQLBoolean } }, - where: (table, args) => - args.active - ? `${table}.${q('archived', DB)} = ${bool(false, DB)}` - : null, - orderBy: { body: 'desc' }, - ...(STRATEGY === 'batch' - ? { - sqlBatch: { - thisKey: 'author_id', - parentKey: 'id' - } - } - : { - sqlJoin: (userTable, postTable) => - `${postTable}.${q('author_id', DB)} = ${userTable}.${q('id', DB)}` - }) + extensions: { + joinMonster: { + where: (table, args) => + args.active + ? `${table}.${q('archived', DB)} = ${bool(false, DB)}` + : null, + orderBy: { body: 'desc' }, + ...(STRATEGY === 'batch' + ? { + sqlBatch: { + thisKey: 'author_id', + parentKey: 'id' + } + } + : { + sqlJoin: (userTable, postTable) => + `${postTable}.${q('author_id', DB)} = ${userTable}.${q( + 'id', + DB + )}` + }) + } + } }, following: { description: 'Users that this user is following', @@ -125,70 +161,89 @@ const User = new GraphQLObjectType({ oldestFirst: { type: GraphQLBoolean }, intimacy: { type: IntimacyLevel } }, - orderBy: 'first_name', - where: (table, args) => - args.name ? `${table}.${q('first_name', DB)} = '${args.name}'` : false, - junction: { - sqlTable: q('relationships', DB), - orderBy: args => (args.oldestFirst ? { followee_id: 'desc' } : null), - where: (table, args) => - args.intimacy - ? `${table}.${q('closeness', DB)} = '${args.intimacy}'` - : false, - include: { - friendship: { - sqlColumn: 'closeness', - jmIgnoreAll: false - }, - intimacy: { - sqlExpr: table => `${table}.${q('closeness', DB)}`, - jmIgnoreAll: false - }, - closeness: { - sqlDeps: ['closeness'], - jmIgnoreAll: false - } - }, - ...(['batch', 'mix'].includes(STRATEGY) - ? { - uniqueKey: ['follower_id', 'followee_id'], - sqlBatch: { - thisKey: 'follower_id', - parentKey: 'id', - sqlJoin: (relationTable, followeeTable) => - `${relationTable}.${q( - 'followee_id', - DB - )} = ${followeeTable}.${q('id', DB)}` + extensions: { + joinMonster: { + orderBy: 'first_name', + where: (table, args) => + args.name + ? `${table}.${q('first_name', DB)} = '${args.name}'` + : false, + junction: { + sqlTable: q('relationships', DB), + orderBy: args => + args.oldestFirst ? { followee_id: 'desc' } : null, + where: (table, args) => + args.intimacy + ? `${table}.${q('closeness', DB)} = '${args.intimacy}'` + : false, + include: { + friendship: { + sqlColumn: 'closeness', + ignoreAll: false + }, + intimacy: { + sqlExpr: table => `${table}.${q('closeness', DB)}`, + ignoreAll: false + }, + closeness: { + sqlDeps: ['closeness'], + ignoreAll: false } - } - : { - sqlJoins: [ - (followerTable, relationTable) => - `${followerTable}.${q('id', DB)} = ${relationTable}.${q( - 'follower_id', - DB - )}`, - (relationTable, followeeTable) => - `${relationTable}.${q( - 'followee_id', - DB - )} = ${followeeTable}.${q('id', DB)}` - ] - }) + }, + ...(['batch', 'mix'].includes(STRATEGY) + ? { + uniqueKey: ['follower_id', 'followee_id'], + sqlBatch: { + thisKey: 'follower_id', + parentKey: 'id', + sqlJoin: (relationTable, followeeTable) => + `${relationTable}.${q( + 'followee_id', + DB + )} = ${followeeTable}.${q('id', DB)}` + } + } + : { + sqlJoins: [ + (followerTable, relationTable) => + `${followerTable}.${q('id', DB)} = ${relationTable}.${q( + 'follower_id', + DB + )}`, + (relationTable, followeeTable) => + `${relationTable}.${q( + 'followee_id', + DB + )} = ${followeeTable}.${q('id', DB)}` + ] + }) + } + } } }, friendship: { type: GraphQLString, - jmIgnoreAll: true + extensions: { + joinMonster: { + ignoreAll: true + } + } }, intimacy: { type: GraphQLString, - jmIgnoreAll: true + extensions: { + joinMonster: { + ignoreAll: true + } + } }, closeness: { type: GraphQLString, - jmIgnoreAll: true + extensions: { + joinMonster: { + ignoreAll: true + } + } }, favNums: { type: new GraphQLList(GraphQLInt), @@ -197,49 +252,65 @@ const User = new GraphQLObjectType({ numLegs: { description: 'How many legs this user has', type: GraphQLInt, - sqlColumn: 'num_legs' + extensions: { + joinMonster: { + sqlColumn: 'num_legs' + } + } }, numFeet: { description: 'How many feet this user has', type: GraphQLInt, - sqlDeps: ['num_legs'], + extensions: { + joinMonster: { + sqlDeps: ['num_legs'] + } + }, resolve: user => user.num_legs }, writtenMaterial1: { type: new GraphQLList(AuthoredUnion), - orderBy: 'id', - ...(STRATEGY === 'batch' - ? { - sqlBatch: { - thisKey: 'author_id', - parentKey: 'id' - } - } - : { - sqlJoin: (userTable, unionTable) => - `${userTable}.${q('id', DB)} = ${unionTable}.${q( - 'author_id', - DB - )}` - }) + extensions: { + joinMonster: { + orderBy: 'id', + ...(STRATEGY === 'batch' + ? { + sqlBatch: { + thisKey: 'author_id', + parentKey: 'id' + } + } + : { + sqlJoin: (userTable, unionTable) => + `${userTable}.${q('id', DB)} = ${unionTable}.${q( + 'author_id', + DB + )}` + }) + } + } }, writtenMaterial2: { type: new GraphQLList(AuthoredInterface), - orderBy: 'id', - ...(STRATEGY === 'batch' - ? { - sqlBatch: { - thisKey: 'author_id', - parentKey: 'id' - } - } - : { - sqlJoin: (userTable, unionTable) => - `${userTable}.${q('id', DB)} = ${unionTable}.${q( - 'author_id', - DB - )}` - }) + extensions: { + joinMonster: { + orderBy: 'id', + ...(STRATEGY === 'batch' + ? { + sqlBatch: { + thisKey: 'author_id', + parentKey: 'id' + } + } + : { + sqlJoin: (userTable, unionTable) => + `${userTable}.${q('id', DB)} = ${unionTable}.${q( + 'author_id', + DB + )}` + }) + } + } } }) }) diff --git a/test-api/schema-paginated/Authored/Interface.js b/test-api/schema-paginated/Authored/Interface.js index 527bc60a..d0776b56 100644 --- a/test-api/schema-paginated/Authored/Interface.js +++ b/test-api/schema-paginated/Authored/Interface.js @@ -13,27 +13,31 @@ const { DB, PAGINATE } = process.env export const Authored = new GraphQLInterfaceType({ name: 'AuthoredInterface', - sqlTable: `( - SELECT - ${q('id', DB)}, - ${q('body', DB)}, - ${q('author_id', DB)}, - NULL AS ${q('post_id', DB)}, - ${q('created_at', DB)}, - 'Post' AS ${q('$type', DB)} - FROM ${q('posts', DB)} - UNION ALL - SELECT - ${q('id', DB)}, - ${q('body', DB)}, - ${q('author_id', DB)}, - ${q('post_id', DB)}, - ${q('created_at', DB)}, - 'Comment' AS ${q('$type', DB)} - FROM ${q('comments', DB)} - )`, - uniqueKey: ['id', '$type'], - alwaysFetch: '$type', + extensions: { + joinMonster: { + sqlTable: `( + SELECT + ${q('id', DB)}, + ${q('body', DB)}, + ${q('author_id', DB)}, + NULL AS ${q('post_id', DB)}, + ${q('created_at', DB)}, + 'Post' AS ${q('$type', DB)} + FROM ${q('posts', DB)} + UNION ALL + SELECT + ${q('id', DB)}, + ${q('body', DB)}, + ${q('author_id', DB)}, + ${q('post_id', DB)}, + ${q('created_at', DB)}, + 'Comment' AS ${q('$type', DB)} + FROM ${q('comments', DB)} + )`, + uniqueKey: ['id', '$type'], + alwaysFetch: '$type' + } + }, fields: () => ({ id: { type: GraphQLID @@ -43,7 +47,11 @@ export const Authored = new GraphQLInterfaceType({ }, authorId: { type: GraphQLInt, - sqlColumn: 'author_id' + extensions: { + joinMonster: { + sqlColumn: 'author_id' + } + } } }), resolveType: obj => obj.$type diff --git a/test-api/schema-paginated/Authored/Union.js b/test-api/schema-paginated/Authored/Union.js index a8a1bfe1..ae784675 100644 --- a/test-api/schema-paginated/Authored/Union.js +++ b/test-api/schema-paginated/Authored/Union.js @@ -8,27 +8,31 @@ const { DB } = process.env export default new GraphQLUnionType({ name: 'AuthoredUnion', - sqlTable: `( - SELECT - ${q('id', DB)}, - ${q('body', DB)}, - ${q('author_id', DB)}, - NULL AS ${q('post_id', DB)}, - ${q('created_at', DB)}, - 'Post' AS ${q('$type', DB)} - FROM ${q('posts', DB)} - UNION ALL - SELECT - ${q('id', DB)}, - ${q('body', DB)}, - ${q('author_id', DB)}, - ${q('post_id', DB)}, - ${q('created_at', DB)}, - 'Comment' AS ${q('$type', DB)} - FROM ${q('comments', DB)} - )`, - uniqueKey: ['id', '$type'], + extensions: { + joinMonster: { + sqlTable: `( + SELECT + ${q('id', DB)}, + ${q('body', DB)}, + ${q('author_id', DB)}, + NULL AS ${q('post_id', DB)}, + ${q('created_at', DB)}, + 'Post' AS ${q('$type', DB)} + FROM ${q('posts', DB)} + UNION ALL + SELECT + ${q('id', DB)}, + ${q('body', DB)}, + ${q('author_id', DB)}, + ${q('post_id', DB)}, + ${q('created_at', DB)}, + 'Comment' AS ${q('$type', DB)} + FROM ${q('comments', DB)} + )`, + uniqueKey: ['id', '$type'], + alwaysFetch: '$type' + } + }, types: () => [Comment, Post], - alwaysFetch: '$type', resolveType: obj => obj.$type }) diff --git a/test-api/schema-paginated/Comment.js b/test-api/schema-paginated/Comment.js index 5844f400..560b766c 100644 --- a/test-api/schema-paginated/Comment.js +++ b/test-api/schema-paginated/Comment.js @@ -19,13 +19,21 @@ const { PAGINATE, DB } = process.env export const Comment = new GraphQLObjectType({ description: 'Comments on posts', name: 'Comment', - sqlTable: `(SELECT * FROM ${q('comments', DB)})`, - uniqueKey: 'id', + extensions: { + joinMonster: { + sqlTable: `(SELECT * FROM ${q('comments', DB)})`, + uniqueKey: 'id' + } + }, interfaces: () => [nodeInterface, Authored], fields: () => ({ id: { ...globalIdField(), - sqlDeps: ['id'] + extensions: { + joinMonster: { + sqlDeps: ['id'] + } + } }, body: { description: 'The content of the comment', @@ -34,18 +42,33 @@ export const Comment = new GraphQLObjectType({ post: { description: 'The post that the comment belongs to', type: Post, - sqlJoin: (commentTable, postTable) => - `${commentTable}.${q('post_id', DB)} = ${postTable}.${q('id', DB)}` + extensions: { + joinMonster: { + sqlJoin: (commentTable, postTable) => + `${commentTable}.${q('post_id', DB)} = ${postTable}.${q('id', DB)}` + } + } }, authorId: { type: GraphQLInt, - sqlColumn: 'author_id' + extensions: { + joinMonster: { + sqlColumn: 'author_id' + } + } }, author: { description: 'The user who wrote the comment', type: User, - sqlJoin: (commentTable, userTable) => - `${commentTable}.${q('author_id', DB)} = ${userTable}.${q('id', DB)}` + extensions: { + joinMonster: { + sqlJoin: (commentTable, userTable) => + `${commentTable}.${q('author_id', DB)} = ${userTable}.${q( + 'id', + DB + )}` + } + } }, archived: { type: GraphQLBoolean @@ -53,23 +76,34 @@ export const Comment = new GraphQLObjectType({ likers: { description: 'Which users have liked this comment', type: new GraphQLList(User), - junction: { - sqlTable: 'likes', - sqlJoins: [ - (commentTable, likesTable) => - `${commentTable}.${q('id', DB)} = ${likesTable}.${q( - 'comment_id', - DB - )}`, - (likesTable, userTable) => - `${likesTable}.${q('account_id', DB)} = ${userTable}.${q('id', DB)}` - ] + extensions: { + joinMonster: { + junction: { + sqlTable: 'likes', + sqlJoins: [ + (commentTable, likesTable) => + `${commentTable}.${q('id', DB)} = ${likesTable}.${q( + 'comment_id', + DB + )}`, + (likesTable, userTable) => + `${likesTable}.${q('account_id', DB)} = ${userTable}.${q( + 'id', + DB + )}` + ] + } + } } }, createdAt: { description: 'When this was created', type: GraphQLString, - sqlColumn: 'created_at' + extensions: { + joinMonster: { + sqlColumn: 'created_at' + } + } } }) }) diff --git a/test-api/schema-paginated/ContextPost.js b/test-api/schema-paginated/ContextPost.js index c7ae509a..165a6a5c 100644 --- a/test-api/schema-paginated/ContextPost.js +++ b/test-api/schema-paginated/ContextPost.js @@ -10,12 +10,20 @@ const ContextPost = new GraphQLObjectType({ 'A post from a user. This object is used in a context test and must be given a context.table to resolve.', name: 'ContextPost', interfaces: () => [nodeInterface], - sqlTable: (_, context) => `(SELECT * FROM ${q(context.table, DB)})`, - uniqueKey: 'id', + extensions: { + joinMonster: { + sqlTable: (_, context) => `(SELECT * FROM ${q(context.table, DB)})`, + uniqueKey: 'id' + } + }, fields: () => ({ id: { ...globalIdField(), - sqlDeps: ['id'] + extensions: { + joinMonster: { + sqlDeps: ['id'] + } + } }, body: { description: 'The content of the post', diff --git a/test-api/schema-paginated/Post.js b/test-api/schema-paginated/Post.js index d46355ae..5be54981 100644 --- a/test-api/schema-paginated/Post.js +++ b/test-api/schema-paginated/Post.js @@ -24,13 +24,21 @@ const { PAGINATE, STRATEGY, DB } = process.env export const Post = new GraphQLObjectType({ description: 'A post from a user', name: 'Post', - sqlTable: `(SELECT * FROM ${q('posts', DB)})`, - uniqueKey: 'id', + extensions: { + joinMonster: { + sqlTable: `(SELECT * FROM ${q('posts', DB)})`, + uniqueKey: 'id' + } + }, interfaces: () => [nodeInterface, Authored], fields: () => ({ id: { ...globalIdField(), - sqlDeps: ['id'] + extensions: { + joinMonster: { + sqlDeps: ['id'] + } + } }, body: { description: 'The content of the post', @@ -38,22 +46,33 @@ export const Post = new GraphQLObjectType({ }, authorId: { type: GraphQLInt, - sqlColumn: 'author_id' + extensions: { + joinMonster: { + sqlColumn: 'author_id' + } + } }, author: { description: 'The user that created the post', type: User, - ...(STRATEGY === 'batch' - ? { - sqlBatch: { - thisKey: 'id', - parentKey: 'author_id' - } - } - : { - sqlJoin: (postTable, userTable) => - `${postTable}.${q('author_id', DB)} = ${userTable}.${q('id', DB)}` - }) + extensions: { + joinMonster: { + ...(STRATEGY === 'batch' + ? { + sqlBatch: { + thisKey: 'id', + parentKey: 'author_id' + } + } + : { + sqlJoin: (postTable, userTable) => + `${postTable}.${q('author_id', DB)} = ${userTable}.${q( + 'id', + DB + )}` + }) + } + } }, comments: { description: 'The comments on this post', @@ -62,72 +81,86 @@ export const Post = new GraphQLObjectType({ active: { type: GraphQLBoolean }, ...(PAGINATE === 'offset' ? forwardConnectionArgs : connectionArgs) }, - sqlPaginate: !!PAGINATE, - ...do { - if (PAGINATE === 'offset') { - ;({ orderBy: 'id' }) - } else if (PAGINATE === 'keyset') { - ;({ - sortKey: { - order: 'DESC', - key: 'id' + resolve: PAGINATE + ? undefined + : (post, args) => { + post.comments.sort((a, b) => a.id - b.id) + return connectionFromArray(post.comments, args) + }, + extensions: { + joinMonster: { + sqlPaginate: !!PAGINATE, + ...do { + if (PAGINATE === 'offset') { + ;({ orderBy: 'id' }) + } else if (PAGINATE === 'keyset') { + ;({ + sortKey: { + order: 'DESC', + key: 'id' + } + }) + } else { + { + } } - }) - } else { - ;({ - resolve: (user, args) => { - user.comments.sort((a, b) => a.id - b.id) - return connectionFromArray(user.comments, args) + }, + ...do { + if (STRATEGY === 'batch' || STRATEGY === 'mix') { + ;({ + sqlBatch: { + thisKey: 'post_id', + parentKey: 'id' + }, + where: (table, args) => + args.active + ? `${table}.${q('archived', DB)} = ${bool(false, DB)}` + : null + }) + } else { + ;({ + sqlJoin: (postTable, commentTable, args) => + `${commentTable}.${q('post_id', DB)} = ${postTable}.${q( + 'id', + DB + )} ${ + args.active + ? `AND ${commentTable}.${q('archived', DB)} = ${bool( + false, + DB + )}` + : '' + }` + }) } - }) - } - }, - ...do { - if (STRATEGY === 'batch' || STRATEGY === 'mix') { - ;({ - sqlBatch: { - thisKey: 'post_id', - parentKey: 'id' - }, - where: (table, args) => - args.active - ? `${table}.${q('archived', DB)} = ${bool(false, DB)}` - : null - }) - } else { - ;({ - sqlJoin: (postTable, commentTable, args) => - `${commentTable}.${q('post_id', DB)} = ${postTable}.${q( - 'id', - DB - )} ${ - args.active - ? `AND ${commentTable}.${q('archived', DB)} = ${bool( - false, - DB - )}` - : '' - }` - }) + } } } }, numComments: { description: 'How many comments this post has', type: GraphQLInt, - // you can info from a correlated subquery - sqlExpr: table => - `(SELECT count(*) from ${q('comments', DB)} WHERE ${table}.${q( - 'id', - DB - )} = comments.${q('post_id', DB)})` + extensions: { + joinMonster: { + // you can info from a correlated subquery + sqlExpr: table => + `(SELECT count(*) from ${q('comments', DB)} WHERE ${table}.${q( + 'id', + DB + )} = comments.${q('post_id', DB)})` + } + } }, archived: { type: GraphQLBoolean }, createdAt: { type: GraphQLString, - sqlColumn: 'created_at' + extensions: { + joinMonster: { + sqlColumn: 'created_at' + } + } } }) }) diff --git a/test-api/schema-paginated/QueryRoot.js b/test-api/schema-paginated/QueryRoot.js index 2a4c8d45..40821c28 100644 --- a/test-api/schema-paginated/QueryRoot.js +++ b/test-api/schema-paginated/QueryRoot.js @@ -60,28 +60,32 @@ export default new GraphQLObjectType({ search: { type: GraphQLString }, ...(PAGINATE === 'offset' ? forwardConnectionArgs : connectionArgs) }, - sqlPaginate: !!PAGINATE, - ...do { - if (PAGINATE === 'offset') { - ;({ orderBy: 'id' }) - } else if (PAGINATE === 'keyset') { - ;({ - sortKey: { - order: 'asc', - key: 'id' + extensions: { + joinMonster: { + sqlPaginate: !!PAGINATE, + ...do { + if (PAGINATE === 'offset') { + ;({ orderBy: 'id' }) + } else if (PAGINATE === 'keyset') { + ;({ + sortKey: { + order: 'asc', + key: 'id' + } + }) } - }) + }, + where: (table, args) => { + // this is naughty. do not allow un-escaped GraphQLString inputs into the WHERE clause... + if (args.search) + return `(lower(${table}.${q('first_name', DB)}) LIKE lower('%${ + args.search + }%') OR lower(${table}.${q('last_name', DB)}) LIKE lower('%${ + args.search + }%'))` + } } }, - where: (table, args) => { - // this is naughty. do not allow un-escaped GraphQLString inputs into the WHERE clause... - if (args.search) - return `(lower(${table}.${q('first_name', DB)}) LIKE lower('%${ - args.search - }%') OR lower(${table}.${q('last_name', DB)}) LIKE lower('%${ - args.search - }%'))` - }, resolve: async (parent, args, context, resolveInfo) => { const data = await joinMonster( resolveInfo, @@ -94,8 +98,12 @@ export default new GraphQLObjectType({ }, usersFirst2: { type: new GraphQLList(User), - limit: 2, - orderBy: 'id', + extensions: { + joinMonster: { + limit: 2, + orderBy: 'id' + } + }, resolve: (parent, args, context, resolveInfo) => { return joinMonster( resolveInfo, @@ -113,9 +121,13 @@ export default new GraphQLObjectType({ type: GraphQLInt } }, - where: (usersTable, args, context) => { - // eslint-disable-line no-unused-vars - if (args.id) return `${usersTable}.${q('id', DB)} = ${args.id}` + extensions: { + joinMonster: { + where: (usersTable, args, context) => { + // eslint-disable-line no-unused-vars + if (args.id) return `${usersTable}.${q('id', DB)} = ${args.id}` + } + } }, resolve: (parent, args, context, resolveInfo) => { return joinMonster( diff --git a/test-api/schema-paginated/Sponsor.js b/test-api/schema-paginated/Sponsor.js index d7c3e2f2..f92fcfad 100644 --- a/test-api/schema-paginated/Sponsor.js +++ b/test-api/schema-paginated/Sponsor.js @@ -3,16 +3,28 @@ import { GraphQLObjectType, GraphQLString, GraphQLInt } from 'graphql' const Sponsor = new GraphQLObjectType({ description: 'people who have given money', name: 'Sponsor', - sqlTable: '"sponsors"', - uniqueKey: ['generation', 'first_name', 'last_name'], + extensions: { + joinMonster: { + sqlTable: '"sponsors"', + uniqueKey: ['generation', 'first_name', 'last_name'] + } + }, fields: () => ({ firstName: { type: GraphQLString, - sqlColumn: 'first_name' + extensions: { + joinMonster: { + sqlColumn: 'first_name' + } + } }, lastName: { type: GraphQLString, - sqlColumn: 'last_name' + extensions: { + joinMonster: { + sqlColumn: 'last_name' + } + } }, generation: { type: GraphQLInt @@ -20,13 +32,21 @@ const Sponsor = new GraphQLObjectType({ numLegs: { description: 'How many legs this user has', type: GraphQLInt, - sqlColumn: 'num_legs' + extensions: { + joinMonster: { + sqlColumn: 'num_legs' + } + } }, numFeet: { description: 'How many feet this user has', type: GraphQLInt, - sqlDeps: ['num_legs'], - resolve: user => user.num_legs + extensions: { + joinMonster: { + sqlDeps: ['num_legs'] + }, + resolve: user => user.num_legs + } } }) }) diff --git a/test-api/schema-paginated/User.js b/test-api/schema-paginated/User.js index 6d45779d..eafee77f 100644 --- a/test-api/schema-paginated/User.js +++ b/test-api/schema-paginated/User.js @@ -27,23 +27,39 @@ const { PAGINATE, STRATEGY, DB } = process.env const User = new GraphQLObjectType({ description: 'a stem contract account', name: 'User', - sqlTable: `(SELECT * FROM ${q('accounts', DB)})`, - uniqueKey: 'id', + extensions: { + joinMonster: { + sqlTable: `(SELECT * FROM ${q('accounts', DB)})`, + uniqueKey: 'id' + } + }, interfaces: [nodeInterface], fields: () => ({ id: { description: 'The global ID for the Relay spec', ...globalIdField('User'), - sqlDeps: ['id'] + extensions: { + joinMonster: { + sqlDeps: ['id'] + } + } }, email: { type: GraphQLString, - sqlColumn: 'email_address' + extensions: { + joinMonster: { + sqlColumn: 'email_address' + } + } }, fullName: { description: "A user's first and last name", type: GraphQLString, - sqlDeps: ['first_name', 'last_name'], + extensions: { + joinMonster: { + sqlDeps: ['first_name', 'last_name'] + } + }, resolve: user => `${user.first_name} ${user.last_name}` }, comments: { @@ -53,74 +69,84 @@ const User = new GraphQLObjectType({ active: { type: GraphQLBoolean }, ...(PAGINATE === 'offset' ? forwardConnectionArgs : connectionArgs) }, - sqlPaginate: !!PAGINATE, - ...do { - if (PAGINATE === 'offset') { - ;({ orderBy: 'id' }) - } else if (PAGINATE === 'keyset') { - ;({ - sortKey: { - order: 'desc', - key: 'id' + resolve: PAGINATE + ? undefined + : (user, args) => { + user.comments.sort((a, b) => a.id - b.id) + return connectionFromArray(user.comments, args) + }, + extensions: { + joinMonster: { + sqlPaginate: !!PAGINATE, + ...do { + if (PAGINATE === 'offset') { + ;({ orderBy: 'id' }) + } else if (PAGINATE === 'keyset') { + ;({ + sortKey: { + order: 'desc', + key: 'id' + } + }) + } else { + { + } } - }) - } else { - ;({ - resolve: (user, args) => { - user.comments.sort((a, b) => a.id - b.id) - return connectionFromArray(user.comments, args) + }, + ...do { + if (STRATEGY === 'batch' || STRATEGY === 'mix') { + ;({ + sqlBatch: { + thisKey: 'author_id', + parentKey: 'id' + }, + where: (table, args) => + args.active + ? `${table}.${q('archived', DB)} = ${bool(false, DB)}` + : null + }) + } else { + ;({ + sqlJoin: (userTable, commentTable, args) => + `${commentTable}.${q('author_id', DB)} = ${userTable}.${q( + 'id', + DB + )} ${ + args.active + ? `AND ${commentTable}.${q('archived', DB)} = ${bool( + false, + DB + )}` + : '' + }` + }) } - }) - } - }, - ...do { - if (STRATEGY === 'batch' || STRATEGY === 'mix') { - ;({ - sqlBatch: { - thisKey: 'author_id', - parentKey: 'id' - }, - where: (table, args) => - args.active - ? `${table}.${q('archived', DB)} = ${bool(false, DB)}` - : null - }) - } else { - ;({ - sqlJoin: (userTable, commentTable, args) => - `${commentTable}.${q('author_id', DB)} = ${userTable}.${q( - 'id', - DB - )} ${ - args.active - ? `AND ${commentTable}.${q('archived', DB)} = ${bool( - false, - DB - )}` - : '' - }` - }) + } } } }, commentsLast2: { type: new GraphQLList(Comment), - orderBy: { id: 'desc' }, - limit: () => 2, - ...(STRATEGY === 'batch' - ? { - sqlBatch: { - thisKey: 'author_id', - parentKey: 'id' - } - } - : { - sqlJoin: (userTable, commentTable) => - `${commentTable}.${q('author_id', DB)} = ${userTable}.${q( - 'id', - DB - )}` - }) + extensions: { + joinMonster: { + orderBy: { id: 'desc' }, + limit: () => 2, + ...(STRATEGY === 'batch' + ? { + sqlBatch: { + thisKey: 'author_id', + parentKey: 'id' + } + } + : { + sqlJoin: (userTable, commentTable) => + `${commentTable}.${q('author_id', DB)} = ${userTable}.${q( + 'id', + DB + )}` + }) + } + } }, posts: { description: 'A list of Posts the user has written', @@ -129,52 +155,61 @@ const User = new GraphQLObjectType({ search: { type: GraphQLString }, ...(PAGINATE === 'offset' ? forwardConnectionArgs : connectionArgs) }, - sqlPaginate: !!PAGINATE, - ...do { - if (PAGINATE === 'offset') { - ;({ - orderBy: args => ({ - // eslint-disable-line no-unused-vars - created_at: 'desc', - id: 'asc' - }) - }) - } else if (PAGINATE === 'keyset') { - ;({ - sortKey: args => ({ - // eslint-disable-line no-unused-vars - order: 'desc', - key: ['created_at', 'id'] - }) - }) - } else { - ;({ - resolve: (user, args) => { - user.posts.sort((a, b) => a.id - b.id) - return connectionFromArray(user.posts, args) + resolve: PAGINATE + ? undefined + : (user, args) => { + user.posts.sort((a, b) => a.id - b.id) + return connectionFromArray(user.posts, args) + }, + extensions: { + joinMonster: { + sqlPaginate: !!PAGINATE, + ...do { + if (PAGINATE === 'offset') { + ;({ + orderBy: args => ({ + // eslint-disable-line no-unused-vars + created_at: 'desc', + id: 'asc' + }) + }) + } else if (PAGINATE === 'keyset') { + ;({ + sortKey: args => ({ + // eslint-disable-line no-unused-vars + order: 'desc', + key: ['created_at', 'id'] + }) + }) + } else { + { + } } - }) - } - }, - where: (table, args) => { - if (args.search) - return `lower(${table}.${q('body', DB)}) LIKE lower('%${ - args.search - }%')` - }, - ...do { - if (STRATEGY === 'batch') { - ;({ - sqlBatch: { - thisKey: 'author_id', - parentKey: 'id' + }, + where: (table, args) => { + if (args.search) + return `lower(${table}.${q('body', DB)}) LIKE lower('%${ + args.search + }%')` + }, + ...do { + if (STRATEGY === 'batch') { + ;({ + sqlBatch: { + thisKey: 'author_id', + parentKey: 'id' + } + }) + } else { + ;({ + sqlJoin: (userTable, postTable) => + `${postTable}.${q('author_id', DB)} = ${userTable}.${q( + 'id', + DB + )}` + }) } - }) - } else { - ;({ - sqlJoin: (userTable, postTable) => - `${postTable}.${q('author_id', DB)} = ${userTable}.${q('id', DB)}` - }) + } } } }, @@ -186,148 +221,158 @@ const User = new GraphQLObjectType({ intimacy: { type: IntimacyLevel }, sortOnMain: { type: GraphQLBoolean } }, - where: table => `${table}.${q('email_address', DB)} IS NOT NULL`, - sqlPaginate: !!PAGINATE, - ...do { - if (PAGINATE === 'offset') { - ;({ - orderBy: args => - args.sortOnMain - ? { - created_at: 'ASC', - id: 'ASC' - } - : null - }) - } else if (PAGINATE === 'keyset') { - ;({ - sortKey: args => - args.sortOnMain - ? { - order: 'ASC', - key: ['created_at', 'id'] - } - : null - }) - } else { - ;({ - resolve: (user, args) => { - return connectionFromArray(user.following, args) - } - }) - } - }, - junction: { - sqlTable: `(SELECT * FROM ${q('relationships', DB)})`, - where: (table, args) => - args.intimacy - ? `${table}.${q('closeness', DB)} = '${args.intimacy}'` - : null, - include: { - friendship: { - sqlColumn: 'closeness', - jmIgnoreAll: false + resolve: PAGINATE + ? undefined + : (user, args) => { + return connectionFromArray(user.following, args) }, - intimacy: { - sqlExpr: table => `${table}.${q('closeness', DB)}`, - jmIgnoreAll: false + extensions: { + joinMonster: { + where: table => `${table}.${q('email_address', DB)} IS NOT NULL`, + sqlPaginate: !!PAGINATE, + ...do { + if (PAGINATE === 'offset') { + ;({ + orderBy: args => + args.sortOnMain + ? { + created_at: 'ASC', + id: 'ASC' + } + : null + }) + } else if (PAGINATE === 'keyset') { + ;({ + sortKey: args => + args.sortOnMain + ? { + order: 'ASC', + key: ['created_at', 'id'] + } + : null + }) + } else { + { + } + } }, - closeness: { - sqlDeps: ['closeness'], - jmIgnoreAll: false - } - }, - ...do { - if (PAGINATE === 'offset') { - ;({ - orderBy: args => - args.sortOnMain - ? null - : { - created_at: 'DESC', - followee_id: 'ASC' - } - }) - } else if (PAGINATE === 'keyset') { - ;({ - sortKey: args => - args.sortOnMain - ? null - : { - order: 'ASC', - key: ['created_at', 'followee_id'] - } - }) - } - }, - ...do { - if (STRATEGY === 'batch' || STRATEGY === 'mix') { - ;({ - uniqueKey: ['follower_id', 'followee_id'], - sqlBatch: { - thisKey: 'follower_id', - parentKey: 'id', - sqlJoin: (relationTable, followeeTable) => - `${relationTable}.${q( - 'followee_id', - DB - )} = ${followeeTable}.${q('id', DB)}` + junction: { + sqlTable: `(SELECT * FROM ${q('relationships', DB)})`, + where: (table, args) => + args.intimacy + ? `${table}.${q('closeness', DB)} = '${args.intimacy}'` + : null, + include: { + friendship: { + sqlColumn: 'closeness', + ignoreAll: false + }, + intimacy: { + sqlExpr: table => `${table}.${q('closeness', DB)}`, + ignoreAll: false + }, + closeness: { + sqlDeps: ['closeness'], + ignoreAll: false } - }) - } else { - ;({ - sqlJoins: [ - (followerTable, relationTable) => - `${followerTable}.${q('id', DB)} = ${relationTable}.${q( - 'follower_id', - DB - )}`, - (relationTable, followeeTable) => - `${relationTable}.${q( - 'followee_id', - DB - )} = ${followeeTable}.${q('id', DB)}` - ] - }) + }, + ...do { + if (PAGINATE === 'offset') { + ;({ + orderBy: args => + args.sortOnMain + ? null + : { + created_at: 'DESC', + followee_id: 'ASC' + } + }) + } else if (PAGINATE === 'keyset') { + ;({ + sortKey: args => + args.sortOnMain + ? null + : { + order: 'ASC', + key: ['created_at', 'followee_id'] + } + }) + } + }, + ...do { + if (STRATEGY === 'batch' || STRATEGY === 'mix') { + ;({ + uniqueKey: ['follower_id', 'followee_id'], + sqlBatch: { + thisKey: 'follower_id', + parentKey: 'id', + sqlJoin: (relationTable, followeeTable) => + `${relationTable}.${q( + 'followee_id', + DB + )} = ${followeeTable}.${q('id', DB)}` + } + }) + } else { + ;({ + sqlJoins: [ + (followerTable, relationTable) => + `${followerTable}.${q('id', DB)} = ${relationTable}.${q( + 'follower_id', + DB + )}`, + (relationTable, followeeTable) => + `${relationTable}.${q( + 'followee_id', + DB + )} = ${followeeTable}.${q('id', DB)}` + ] + }) + } + } } } } }, followingFirst: { type: new GraphQLList(User), - limit: 1, - orderBy: 'followee_id', - junction: { - sqlTable: q('relationships', DB), - ...do { - if (STRATEGY === 'batch' || STRATEGY === 'mix') { - ;({ - uniqueKey: ['follower_id', 'followee_id'], - sqlBatch: { - thisKey: 'follower_id', - parentKey: 'id', - sqlJoin: (relationTable, followeeTable) => - `${relationTable}.${q( - 'followee_id', - DB - )} = ${followeeTable}.${q('id', DB)}` + extensions: { + joinMonster: { + limit: 1, + orderBy: 'followee_id', + junction: { + sqlTable: q('relationships', DB), + ...do { + if (STRATEGY === 'batch' || STRATEGY === 'mix') { + ;({ + uniqueKey: ['follower_id', 'followee_id'], + sqlBatch: { + thisKey: 'follower_id', + parentKey: 'id', + sqlJoin: (relationTable, followeeTable) => + `${relationTable}.${q( + 'followee_id', + DB + )} = ${followeeTable}.${q('id', DB)}` + } + }) + } else { + ;({ + sqlJoins: [ + (followerTable, relationTable) => + `${followerTable}.${q('id', DB)} = ${relationTable}.${q( + 'follower_id', + DB + )}`, + (relationTable, followeeTable) => + `${relationTable}.${q( + 'followee_id', + DB + )} = ${followeeTable}.${q('id', DB)}` + ] + }) } - }) - } else { - ;({ - sqlJoins: [ - (followerTable, relationTable) => - `${followerTable}.${q('id', DB)} = ${relationTable}.${q( - 'follower_id', - DB - )}`, - (relationTable, followeeTable) => - `${relationTable}.${q( - 'followee_id', - DB - )} = ${followeeTable}.${q('id', DB)}` - ] - }) + } } } } @@ -335,57 +380,75 @@ const User = new GraphQLObjectType({ writtenMaterial: { type: AuthoredConnection, args: PAGINATE === 'offset' ? forwardConnectionArgs : connectionArgs, - sqlPaginate: !!PAGINATE, - ...do { - if (PAGINATE === 'offset') { - ;({ - orderBy: { - id: 'ASC', - created_at: 'ASC' - } - }) - } else if (PAGINATE === 'keyset') { - ;({ - sortKey: { - order: 'ASC', - key: ['id', 'created_at'] - } - }) - } else { - ;({ - orderBy: 'id', - resolve: (user, args) => { - return connectionFromArray(user.following, args) + resolve: PAGINATE + ? undefined + : (user, args) => { + return connectionFromArray(user.following, args) + }, + extensions: { + joinMonster: { + sqlPaginate: !!PAGINATE, + ...do { + if (PAGINATE === 'offset') { + ;({ + orderBy: { + id: 'ASC', + created_at: 'ASC' + } + }) + } else if (PAGINATE === 'keyset') { + ;({ + sortKey: { + order: 'ASC', + key: ['id', 'created_at'] + } + }) + } else { + ;({ + orderBy: 'id' + }) } - }) + }, + ...(STRATEGY === 'batch' + ? { + sqlBatch: { + thisKey: 'author_id', + parentKey: 'id' + } + } + : { + sqlJoin: (userTable, unionTable) => + `${userTable}.${q('id', DB)} = ${unionTable}.${q( + 'author_id', + DB + )}` + }) } - }, - ...(STRATEGY === 'batch' - ? { - sqlBatch: { - thisKey: 'author_id', - parentKey: 'id' - } - } - : { - sqlJoin: (userTable, unionTable) => - `${userTable}.${q('id', DB)} = ${unionTable}.${q( - 'author_id', - DB - )}` - }) + } }, friendship: { type: GraphQLString, - jmIgnoreAll: true + extensions: { + joinMonster: { + ignoreAll: true + } + } }, intimacy: { type: GraphQLString, - jmIgnoreAll: true + extensions: { + joinMonster: { + ignoreAll: true + } + } }, closeness: { type: GraphQLString, - jmIgnoreAll: true + extensions: { + joinMonster: { + ignoreAll: true + } + } }, favNums: { type: new GraphQLList(GraphQLInt), @@ -394,12 +457,20 @@ const User = new GraphQLObjectType({ numLegs: { description: 'How many legs this user has', type: GraphQLInt, - sqlColumn: 'num_legs' + extensions: { + joinMonster: { + sqlColumn: 'num_legs' + } + } }, numFeet: { description: 'How many feet this user has', type: GraphQLInt, - sqlDeps: ['num_legs'], + extensions: { + joinMonster: { + sqlDeps: ['num_legs'] + } + }, resolve: user => user.num_legs } }) diff --git a/test-d/index.test-d.ts b/test-d/index.test-d.ts new file mode 100644 index 00000000..3efa1983 --- /dev/null +++ b/test-d/index.test-d.ts @@ -0,0 +1,218 @@ +import { expectType } from 'tsd' +import joinMonster from '..' +import { GraphQLObjectType, GraphQLList } from 'graphql' + +type ExampleContext = { + foo: 'bar' +} +type ExampleArgs = { [key: string]: any } + +// test table level extensions +const User = new GraphQLObjectType({ + name: 'User', + extensions: { + joinMonster: { + sqlTable: 'accounts', + uniqueKey: 'id', + alwaysFetch: 'createdAt' + } + }, + fields: {} +}) + +// test field extensions +new GraphQLObjectType({ + name: 'User', + fields: () => ({ + following: { + type: new GraphQLList(User), + extensions: { + joinMonster: { + ignoreAll: true, + ignoreTable: true, + limit: 10, + orderBy: { + foo: 'ASC', + bar: 'DESC' + }, + sortKey: { + order: 'ASC', + key: ['id'] + }, + sqlBatch: { + thisKey: 'foo', + parentKey: 'bar' + }, + sqlColumn: 'foo', + sqlDeps: ['bar', 'baz'], + sqlExpr: (table, args, context) => { + expectType(table) + expectType(args) + expectType(context) + return 'expr' + }, + sqlJoin: (table1, table2, args, context) => { + expectType(table1) + expectType(table2) + expectType(args) + expectType(context) + return 'foo' + }, + sqlPaginate: true, + where: (table, args, context) => { + expectType(table) + expectType(args) + expectType(context) + return `${table}.is_active = TRUE` + } + } + } + } + }) +}) + +// test thunked field extensions +new GraphQLObjectType({ + name: 'User', + fields: () => ({ + following: { + type: new GraphQLList(User), + extensions: { + joinMonster: { + limit: (args, context) => { + expectType(args) + expectType(context) + return 10 + }, + orderBy: (args, context) => { + expectType(args) + expectType(context) + return { + foo: 'ASC', + bar: 'DESC' + } + }, + sortKey: (args, context) => { + expectType(args) + expectType(context) + return { + order: 'ASC', + key: ['id'] + } + } + } + } + } + }) +}) + +// test junction includes +new GraphQLObjectType({ + name: 'User', + fields: () => ({ + following: { + type: new GraphQLList(User), + extensions: { + joinMonster: { + where: accountTable => `${accountTable}.is_active = TRUE`, + junction: { + sqlTable: 'relationships', + orderBy: { + foo: 'ASC', + bar: 'DESC' + }, + sortKey: { + order: 'ASC', + key: ['id'] + }, + sqlBatch: { + thisKey: 'foo', + parentKey: 'bar', + sqlJoin: (table1, table2, args, context) => { + expectType(table1) + expectType(table2) + + return 'foo' + } + }, + include: { + closeness: { + sqlColumn: 'closeness' + } + }, + sqlJoins: [ + (followerTable, junctionTable, args, context) => { + expectType(followerTable) + expectType(junctionTable) + expectType(args) + expectType(context) + return `${followerTable}.id = ${junctionTable}.follower_id` + }, + (junctionTable, followeeTable, args, context) => { + expectType(followeeTable) + expectType(junctionTable) + expectType(args) + expectType(context) + return `${junctionTable}.followee_id = ${followeeTable}.id` + } + ] + } + } + } + } + }) +}) + +// test thunked junction includes +new GraphQLObjectType({ + name: 'User', + fields: () => ({ + following: { + type: new GraphQLList(User), + extensions: { + joinMonster: { + where: accountTable => `${accountTable}.is_active = TRUE`, + junction: { + sqlTable: (args, context) => { + expectType(args) + expectType(context) + return 'relationships' + }, + orderBy: (args, context) => { + expectType(args) + expectType(context) + return { + foo: 'ASC', + bar: 'DESC' + } + }, + sortKey: (args, context) => { + expectType(args) + expectType(context) + return { + order: 'ASC', + key: ['id'] + } + }, + include: (args, context) => { + expectType(args) + expectType(context) + + return { + closeness: { + sqlColumn: 'closeness' + } + } + }, + where: (junctionTable, args, context) => { + expectType(junctionTable) + expectType(args) + expectType(context) + return `${junctionTable}.follower_id <> ${junctionTable}.followee_id` + } + } + } + } + } + }) +})