join-monster 正在参加 2021 年度 OSC 中国开源项目评选,请投票支持!
join-monster 在 2021 年度 OSC 中国开源项目评选 中已获得 {{ projectVoteCount }} 票,请投票支持!
2021 年度 OSC 中国开源项目评选 正在火热进行中,快来投票支持你喜欢的开源项目!
2021 年度 OSC 中国开源项目评选 >>> 中场回顾
join-monster 获得 2021 年度 OSC 中国开源项目评选「最佳人气项目」 !
授权协议 MIT License
开发语言 Java
操作系统 跨平台
软件类型 开源软件
所属分类 大数据数据查询
开源组织
地区 不详
投 递 者 首席测试
适用人群 未知
收录时间 2021-11-23

软件简介

join-monster npm version Build Status Documentation Status

Query Planning and Batch Data Fetching between GraphQL and SQL.

What is Join Monster?

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

Translate a GraphQL query like this:

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

...into a couple SQL queries like this:

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

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

...and get back correctly hydrated data.

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

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

Why?

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

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

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

Get Pagination out of the Box

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

Works with the RelayJS

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

Usage with GraphQL

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

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

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

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

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

Detailed instructions for set up are found in the docs.

Using with graphql-tools

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

Running the Demo

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

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

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

graphsiql

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

Future Work

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

代码

评论

点击引领话题📣
暂无内容
发表了博客
{{o.pubDate | formatDate}}

{{formatAllHtml(o.title)}}

{{parseInt(o.replyCount) | bigNumberTransform}}
{{parseInt(o.viewCount) | bigNumberTransform}}
没有更多内容
暂无内容
发表了问答
{{o.pubDate | formatDate}}

{{formatAllHtml(o.title)}}

{{parseInt(o.replyCount) | bigNumberTransform}}
{{parseInt(o.viewCount) | bigNumberTransform}}
没有更多内容
暂无内容
uglify-js 存在ReDoS漏洞
ReDoS
uglify-js 是一个 JavaScript 解析器、压缩器、压缩器和美化工具包。此软件包的受影响版本容易通过 string_template 和 decode_template 函数受到正则表达式拒绝服务 (ReDoS) 的攻击。
MPS-2022-14112
2022-08-08 18:30
needle 存在Authorization请求头泄露漏洞
信息暴露
needle 是 一款流式传输的HTTP客户端 needle 没有对重定向后的请求头做过滤处理,会把第一次请求的 Authorization 请求头也传递到重定向后的服务,导致 Authorization 请求头泄露。 攻击者可利用该漏洞被动窃取用户的 Authorization 数据。
MPS-2022-7866
2022-08-08 18:30
Npm underscore 代码注入漏洞
代码注入
Underscore.js 是一个用于 JavaScript 的实用工具带库,它为常见的功提供支持,而无需扩展任何核心 JavaScript 对象。 漏洞版本中存在代码注入漏洞,攻击者可利用该漏洞容易通过模板函数执行任意代码。
CVE-2021-23358 MPS-2021-3658
2022-08-08 18:30
Npm Braces资源管理错误漏洞
拒绝服务
Npm Braces是美国Npm公司的一个应用程序。Bash的括号扩展,以JavaScript实现。 Braces 2.3.1之前版本存在安全漏洞,攻击者可利用该漏洞使用正则表达式拒绝服务(ReDoS)攻击。
CVE-2018-1109 MPS-2021-3692
2022-08-08 18:30
Markdown-It 安全漏洞
拒绝服务
Markdown-It是一个 Markdown 解析器。 Markdown-It存在安全漏洞,该漏洞源于Markdown—它是一个Markdown解析器。在1.3.2版本之前,长度大于5万个字符的特殊模式会显著降低解析器的速度。用户应该升级到12.3.2版本以接收补丁。除了升级之外,没有已知的变通办法。
CVE-2022-21670 MPS-2021-37028
2022-08-08 18:30
nodejs 资源管理错误漏洞
拒绝服务
nodejs是是一个基于ChromeV8引擎的JavaScript运行环境通过对Chromev8引擎进行了封装以及使用事件驱动和非阻塞IO的应用让Javascript开发高性能的后台应用成为了可能。 nodejs-glob-parent 存在安全漏洞,该漏洞源于正则表达式拒绝服务。
CVE-2020-28469 MPS-2021-7827
2022-08-08 18:30
istanbul-reports 存在通过 window.opener 访问使用指向不受信任目标的 Web 链接漏洞
通过 window.opener 访问使用指向不受信任目标的 Web 链接
由于指向 https://istanbul 的链接中没有 rel 属性,因此该软件包的受影响版本容易受到反向 Tabnabbing 的攻击。
MPS-2022-13797
2022-08-08 18:30
marked 存在ReDoS漏洞
ReDoS
标记是一个低级编译器,用于解析降价而不需要长时间缓存或阻塞。此软件包的受影响版本容易受到正则表达式拒绝服务 (ReDoS) 的攻击。
MPS-2022-13864
2022-08-08 18:30
没有更多内容
加载失败,请刷新页面
点击加载更多
加载中
下一页
0 评论
0 收藏
分享
OSCHINA
登录后可查看更多优质内容
返回顶部
顶部