{"id":13499230,"url":"https://github.com/join-monster/join-monster","last_synced_at":"2025-05-14T01:11:31.452Z","repository":{"id":38268071,"uuid":"68319821","full_name":"join-monster/join-monster","owner":"join-monster","description":"A GraphQL to SQL query execution layer for query planning and batch data fetching.","archived":false,"fork":false,"pushed_at":"2024-10-19T12:15:35.000Z","size":4930,"stargazers_count":2693,"open_issues_count":31,"forks_count":222,"subscribers_count":49,"default_branch":"master","last_synced_at":"2025-05-09T01:14:20.148Z","etag":null,"topics":["graphql","graphql-js","join","nodejs","sql","sql-query"],"latest_commit_sha":null,"homepage":"http://join-monster.readthedocs.io/en/latest/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/join-monster.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-09-15T18:22:09.000Z","updated_at":"2025-05-06T15:08:33.000Z","dependencies_parsed_at":"2023-07-12T14:49:37.298Z","dependency_job_id":"09419c00-84af-48bf-b26b-acba70bb283b","html_url":"https://github.com/join-monster/join-monster","commit_stats":{"total_commits":592,"total_committers":62,"mean_commits":9.548387096774194,"dds":"0.48479729729729726","last_synced_commit":"46d71bed28173b10ce6306035267eba8595a2429"},"previous_names":["stems/join-monster","acarl005/join-monster"],"tags_count":99,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/join-monster%2Fjoin-monster","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/join-monster%2Fjoin-monster/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/join-monster%2Fjoin-monster/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/join-monster%2Fjoin-monster/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/join-monster","download_url":"https://codeload.github.com/join-monster/join-monster/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253356256,"owners_count":21895670,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["graphql","graphql-js","join","nodejs","sql","sql-query"],"created_at":"2024-07-31T22:00:31.109Z","updated_at":"2025-05-14T01:11:26.443Z","avatar_url":"https://github.com/join-monster.png","language":"JavaScript","readme":"\u003c!-- Use fully qualified URL for the images so they'll also be visible from the NPM page too --\u003e\n![join-monster](https://raw.githubusercontent.com/join-monster/join-monster/master/docs/img/join_monster.png)\n[![npm version](https://badge.fury.io/js/join-monster.svg)](https://badge.fury.io/js/join-monster) [![Build Status](https://travis-ci.org/join-monster/join-monster.svg?branch=master)](https://travis-ci.org/join-monster/join-monster) [![Documentation Status](https://readthedocs.org/projects/join-monster/badge/?version=latest)](http://join-monster.readthedocs.io/en/latest/?badge=latest)\n\n### Query Planning and Batch Data Fetching between GraphQL and SQL.\n\n- Read the Documentation: [latest](http://join-monster.readthedocs.io/en/latest/), [stable](https://join-monster.readthedocs.io/en/v3.3.5/)\n- Try Demo: [basic version](https://join-monster-demo.onrender.com/graphql?query=%257B%250A%2520%2520user%28id%253A%25202%29%2520%257B%250A%2520%2520%2520%2520fullName%250A%2520%2520%2520%2520email%250A%2520%2520%2520%2520posts%2520%257B%250A%2520%2520%2520%2520%2520%2520id%250A%2520%2520%2520%2520%2520%2520body%250A%2520%2520%2520%2520%2520%2520comments%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520id%250A%2520%2520%2520%2520%2520%2520%2520%2520body%250A%2520%2520%2520%2520%2520%2520%2520%2520author%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520id%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520fullName%250A%2520%2520%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%257D%250A%2520%2520%257D%250A%257D) or [paginated version](https://join-monster-demo.onrender.com/graphql-relay?query=%257B%250A%2520%2520node%28id%253A%2520%2522VXNlcjoy%2522%29%2520%257B%250A%2520%2520%2520%2520...%2520on%2520User%2520%257B%2520id%252C%2520fullName%2520%257D%250A%2520%2520%257D%250A%2520%2520user%28id%253A%25202%29%2520%257B%250A%2520%2520%2520%2520id%250A%2520%2520%2520%2520fullName%250A%2520%2520%2520%2520posts%28first%253A%25202%252C%2520after%253A%2520%2522eyJpZCI6NDh9%2522%29%2520%257B%250A%2520%2520%2520%2520%2520%2520pageInfo%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520hasNextPage%250A%2520%2520%2520%2520%2520%2520%2520%2520startCursor%250A%2520%2520%2520%2520%2520%2520%2520%2520endCursor%250A%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%2520%2520edges%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520cursor%250A%2520%2520%2520%2520%2520%2520%2520%2520node%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520id%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520body%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520comments%2520%28first%253A%25203%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520pageInfo%2520%257B%2520hasNextPage%2520%257D%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520edges%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520node%2520%257B%2520id%252C%2520body%2520%257D%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%257D%250A%2520%2520%257D%250A%257D%250A)\n- [demo code](https://github.com/join-monster/join-monster/tree/master/test-api)\n- [Supported SQL Dialects (DB Vendors)](http://join-monster.readthedocs.io/en/latest/dialects/)\n\n## What is Join Monster?\n\nEfficient query planning and data fetching for SQL.\nUse `JOIN`s and/or batched requests to retrieve all your data.\nIt takes a GraphQL query and your schema and automatically generates the SQL.\nSend that SQL to your database and get back all the data needed to resolve with only one or a few round-trips to the database.\n\nTranslate a GraphQL query like this:\n\n```graphql\n{\n  user(id: 2) {\n    fullName\n    email\n    posts {\n      id\n      body\n      comments {\n        body\n        author { fullName }\n      }\n    }\n  }\n}\n```\n\n...into a couple SQL queries like this:\n\n```sql\nSELECT\n  \"user\".\"id\" AS \"id\",\n  \"user\".\"email_address\" AS \"email_address\",\n  \"posts\".\"id\" AS \"posts__id\",\n  \"posts\".\"body\" AS \"posts__body\",\n  \"user\".\"first_name\" AS \"first_name\",\n  \"user\".\"last_name\" AS \"last_name\"\nFROM accounts AS \"user\"\nLEFT JOIN posts AS \"posts\" ON \"user\".id = \"posts\".author_id\nWHERE \"user\".id = 2\n\n-- then get the right comments for each post\nSELECT\n  \"comments\".\"id\" AS \"id\",\n  \"comments\".\"body\" AS \"body\",\n  \"author\".\"id\" AS \"author__id\",\n  \"author\".\"first_name\" AS \"author__first_name\",\n  \"author\".\"last_name\" AS \"author__last_name\",\n  \"comments\".\"post_id\" AS \"post_id\"\nFROM comments AS \"comments\"\nLEFT JOIN accounts AS \"author\" ON \"comments\".author_id = \"author\".id\nWHERE \"comments\".archived = FALSE AND \"comments\".\"post_id\" IN (2,8,11,12) -- the post IDs from the previous query \n```\n\n...and get back correctly hydrated data.\n\n```js\n{\n  \"user\": {\n    \"fullName\": \"Yasmine Rolfson\",\n    \"email\": \"Earl.Koss41@yahoo.com\",\n    \"posts\": [\n      {\n        \"id\": 2,\n        \"body\": \"Harum unde maiores est quasi totam consequuntur. Necessitatibus doloribus ut totam dolore omnis quos error eos. Rem nostrum assumenda eius veniam fugit dicta in consequuntur. Ut porro dolorem aliquid qui magnam a.\",\n        \"comments\": [\n          {\n            \"body\": \"The AI driver is down, program the multi-byte sensor so we can parse the SAS bandwidth!\",\n            \"author\": { \"fullName\": \"Yasmine Rolfson\" }\n          },\n          {\n            \"body\": \"Try to program the SMS transmitter, maybe it will synthesize the optical firewall!\",\n            \"author\": { \"fullName\": \"Ole Barrows\" }\n          },\n        ]\n      },\n      // other posts omitted for clarity...\n    ]\n  }\n}\n```\n\nIt works on top of Facebook's [graphql-js](https://github.com/graphql/graphql-js) reference implementation.\nAll you have to do is add a few properties to the objects in your schema and call the `joinMonster` function.\nA SQL query is \"compiled\" for you to send to the DBMS.\nThe data-fetching is efficiently batched.\nThe data is then hydrated into the right shape for your GraphQL schema.\n\n## Why?\n\nMore details on the \"round-trip\" (a.k.a. N+1) problem are [here](http://join-monster.readthedocs.io/en/latest/problem/).\n\n- [X] **Batching** - Fetch all the data in a single, or a few, database query(s).\n- [X] **Efficient** - No over-fetching data. Retrieve only the data that the client actually requested.\n- [X] **Maintainability** - SQL is automatically generated and adaptive. No need to manually write queries or update them when the schema changes.\n- [X] **Declarative** - Simply define the *data requirements* of the GraphQL fields on the SQL columns.\n- [X] **Unobtrusive** - Coexists with your custom resolve functions and existing schemas. Use it on the whole graph or only in parts. Retain the power and expressiveness in defining your schema.\n- [X] **Object-relational impedance mismatch** - Don't bother duplicating a bunch of object definitions in an ORM. Let GraphQL do your object mapping *for you*.\n\nSince it works with the reference implementation, the API is all very familiar. Join Monster is a tool built on top to add batch data fetching. You add some special properties along-side the schema definition that Join Monster knows to look for. The use of [graphql-js](https://github.com/graphql/graphql-js) does not change. You still define your types the same way. You can write resolve functions to manipulate the data from Join Monster, or incorporate data from elsewhere without breaking out of your \"join-monsterized\" schema.\n\n## Get Pagination out of the Box\n\nJoin Monster has support for several different implementations of pagination, all based on the interface in the [Relay Connection Specification](https://facebook.github.io/relay/graphql/connections.htm). Using Relay on the client is totally optional!\n\n## Works with the RelayJS\n\nGreat helpers for the **Node Interface** and automatic pagination for **Connection Types**. [See docs](http://join-monster.readthedocs.io/en/latest/relay/).\n\n\n## Usage with GraphQL\n\n```shell\n$ npm install join-monster\n```\n\n1. Take your `GraphQLObjectType` from [graphql-js](https://github.com/graphql/graphql-js) and add the SQL table name.\n2. Do the fields need values from some SQL columns? Computed columns? Add some additional properties like `sqlColumn`, `sqlDeps`, or `sqlExpr` to the fields. Join Monster will look at these when analyzing the query.\n3. Got some relations? Write a function that tells Join Monster how to `JOIN` your tables and it will hydrate hierarchies of data.\n4. Resolve any type (and all its descendants) by calling `joinMonster` in its resolver. All it needs is the `resolveInfo` and a callback to send the (one) SQL query to the database. Voila! All your data is returned to the resolver.\n\n```javascript\nimport joinMonster from 'join-monster'\nimport {\n  GraphQLObjectType,\n  GraphQLList,\n  GraphQLString,\n  GraphQLInt\n  // and some other stuff\n} from 'graphql'\n\nconst User = new GraphQLObjectType({\n  name: 'User',\n  sqlTable: 'accounts', // the SQL table for this object type is called \"accounts\"\n  uniqueKey: 'id', // the id in each row is unique for this table\n  fields: () =\u003e ({\n    id: {\n      // the column name is assumed to be the same as the field name\n      type: GraphQLInt\n    },\n    email: {\n      type: GraphQLString,\n      // if the column name is different, it must be specified specified\n      sqlColumn: 'email_address'\n    },\n    idEncoded: {\n      description: 'The ID base-64 encoded',\n      type: GraphQLString,\n      // this field uses a sqlColumn and applies a resolver function on the value\n      // if a resolver is present, the `sqlColumn` MUST be specified even if it is the same name as the field\n      sqlColumn: 'id',\n      resolve: user =\u003e toBase64(user.idEncoded)\n    },\n    fullName: {\n      description: \"A user's first and last name\",\n      type: GraphQLString,\n      // perhaps there is no 1-to-1 mapping of field to column\n      // this field depends on multiple columns\n      sqlDeps: [ 'first_name', 'last_name' ],\n      // compute the value with a resolver\n      resolve: user =\u003e `${user.first_name} ${user.last_name}`\n    },\n    capitalizedLastName: {\n      type: GraphQLString,\n      // do a computed column in SQL with raw expression\n      sqlExpr: (table, args) =\u003e `UPPER(${table}.last_name)`\n    },\n    // got tables inside tables??\n    // get it with a JOIN!\n    posts: {\n      description: \"A List of posts this user has written.\",\n      type: new GraphQLList(Post),\n      // a function to generate the join condition from the table aliases\n      sqlJoin(userTable, postTable) {\n        return `${userTable}.id = ${postTable}.author_id`\n      }\n    },\n    // got a relationship but don't want to add another JOIN?\n    // get this in a second batch request\n    comments: {\n      description: \"The comment they have written\",\n      type: new GraphQLList(Comment),\n      // specify which columns to match up the values\n      sqlBatch: {\n        thisKey: 'author_id',\n        parentKey: 'id'\n      }\n    },\n    // many-to-many relations are supported too\n    following: {\n      description: \"Other users that this user is following.\",\n      type: new GraphQLList(User),\n      // name the table that holds the two foreign keys\n      junction: {\n        sqlTable: 'relationships',\n        sqlJoins: [\n          // first the parent table to the junction\n          (followerTable, junctionTable, args) =\u003e `${followerTable}.id = ${junctionTable}.follower_id`,\n          // then the junction to the child\n          (junctionTable, followeeTable, args) =\u003e `${junctionTable}.followee_id = ${followeeTable}.id`\n        ]\n      }\n    },\n    numLegs: {\n      description: 'Number of legs this user has.',\n      type: GraphQLInt,\n      // data isn't coming from the SQL table? no problem! joinMonster will ignore this field\n      resolve: () =\u003e 2\n    }\n  })\n})\n\nconst Comment = new GraphQLObjectType({\n  name: 'Comment',\n  sqlTable: 'comments',\n  uniqueKey: 'id',\n  fields: () =\u003e ({\n    // id and body column names are the same\n    id: {\n      type: GraphQLInt\n    },\n    body: {\n      type: GraphQLString\n    }\n  })\n})\n\nconst Post = new GraphQLObjectType({\n  name: 'Post',\n  sqlTable: 'posts',\n  uniqueKey: 'id',\n  fields: () =\u003e ({\n    id: {\n      type: GraphQLInt\n    },\n    body: {\n      type: GraphQLString\n    }\n  })\n})\n\nexport const QueryRoot = new GraphQLObjectType({\n  name: 'Query',\n  fields: () =\u003e ({\n    // place this user type in the schema\n    user: {\n      type: User,\n      // let client search for users by `id`\n      args: {\n        id: { type: GraphQLInt }\n      },\n      // how to write the WHERE condition\n      where: (usersTable, args, context) =\u003e {\n        if (args.id) return `${usersTable}.id = ${args.id}`\n      },\n      resolve: (parent, args, context, resolveInfo) =\u003e {\n        // resolve the user and the comments and any other descendants in a single request and return the data!\n        // all you need to pass is the `resolveInfo` and a callback for querying the database\n        return joinMonster(resolveInfo, {}, sql =\u003e {\n          // knex is a query library for SQL databases\n          return knex.raw(sql)\n        })\n      }\n    }\n  })\n})\n```\n\nDetailed instructions for set up are found in the [docs](http://join-monster.readthedocs.io/en/latest/data-model).\n\n### Using with `graphql-tools`\n\nThe GraphQL schema language doesn't let you add arbitrary properties to the type definitions. If you're using something like the Apollo [graphql-tools](https://github.com/apollographql/graphql-tools) package to write your code with the schema language, you'll need an **adapter**. See the [join-monster-graphql-tools-adapter](https://github.com/join-monster/join-monster-graphql-tools-adapter) if you want to use this with `graphql-tools`.\n\n## Running the Demo\n\n```shell\n$ git clone https://github.com/join-monster/join-monster.git\n$ cd join-monster\n$ npm install\n$ npm start\n# go to http://localhost:3000/graphql\n\n# if you also want to run the paginated version, create postgres database from the dump provided\npsql $YOUR_DATABASE \u003c data/paginated-demo-dump.sql\nDATABASE_URL=postgres://$USER:$PASS@$HOST/$YOUR_DATABASE npm start\n# go to http://localhost:3000/graphql-relay\n```\n\nExplore the schema, try out some queries, and see what the resulting SQL queries and responses look like in [our custom version of GraphiQL](https://github.com/join-monster/join-monster/tree/master/test-api/graphsiql/README.md)!\n\n![graphsiql](https://raw.githubusercontent.com/join-monster/join-monster/master/docs/img/graphsiql.png)\n\n**There's still a lot of work to do. Please feel free to fork and submit a Pull Request!**\n\n## Future Work\n\n- [ ] Support custom `ORDER BY` expressions [#138](https://github.com/join-monster/join-monster/issues/138).\n- [ ] Support binding parameters [#169](https://github.com/join-monster/join-monster/issues/169).\n- [ ] Write static [Flow](https://flow.org/) types.\n- [ ] Support \"lookup tables\" where a column might be an enum code to look up in another table.\n- [ ] Support \"hyperjunctions\" where many-to-many relations can join through multiple junction tables.\n- [ ] Cover more SQL dialects, like MSSQL and DB2.\n\n","funding_links":[],"categories":["JavaScript","Libraries","\u003ca name=\"JavaScript\"\u003e\u003c/a\u003eJavaScript","sql"],"sub_categories":["JavaScript Libraries"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoin-monster%2Fjoin-monster","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoin-monster%2Fjoin-monster","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoin-monster%2Fjoin-monster/lists"}