Skip to content

Commit

Permalink
Merge branch 'master' into 319-sqlBatch-w-single-field
Browse files Browse the repository at this point in the history
  • Loading branch information
Andy authored Jul 4, 2018
2 parents 0505a90 + 9816e21 commit 6d5e072
Show file tree
Hide file tree
Showing 23 changed files with 140 additions and 36 deletions.
11 changes: 11 additions & 0 deletions .stickler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
linters:
shellcheck:
shell: bash
eslint:
config: './.eslintrc.js'

files:
ignore:
- 'node_modules/*'
- 'test-api/*'
- 'docs/*'
5 changes: 2 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ language: node_js
dist: precise
node_js:
- 6
- 7
- 8
- 10

deploy:
- provider: npm
skip_cleanup: true
email: [email protected]
email: [email protected]
# only tagged commits will trigger deploy. all other commits simply run the lint and tests
on:
tags: true
Expand All @@ -31,7 +31,6 @@ before_script:
# create the databases
- psql -c "CREATE DATABASE test1 LC_COLLATE 'en_US.UTF-8';" -U postgres
- psql -c "CREATE DATABASE test2 LC_COLLATE 'en_US.UTF-8';" -U postgres
- psql -c "CREATE DATABASE test3 LC_COLLATE 'en_US.UTF-8';" -U postgres
- psql -c "CREATE DATABASE demo LC_COLLATE 'en_US.UTF-8';" -U postgres
- echo "CREATE DATABASE test1" | mysql -u travis
- echo "CREATE DATABASE test2" | mysql -u travis
Expand Down
35 changes: 34 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,40 @@ Bugs and feature requests are tracked via GitHub issues. Make sure bug descripti

## Pull Requests

Begin by forking our repository and cloning your fork. Once inside the directory, run `npm install` and `npm run db-build` to prepare the fixture data. You'll need to provide a PostgreSQL URI in your environment in the `PG_URL` variable (omit the database name from the URI, but keep the trailing slash, e.g. `postgres://user:pass@localhost/`). Check the `scripts` in the `package.json` for easily running the example data and the demo server with GraphiQL. Now you can begin coding.
Begin by forking our repository and cloning your fork. Once inside the directory, you'll need to provide a PostgreSQL and MySQL URI in your environment in the `PG_URL` and `MYSQL_URL` variables (omit the database name from the URI, but keep the trailing slash, e.g. `postgres://user:pass@localhost/` and `mysql://user:pass@localhost/`).

You will also need to create the test databases in Postgres (`test1`, `test2`, and `demo`) and mysql (`test1` and `test2`), and install `sqlite3` to complete the tests.

Setting this up might look something like this on MacOS (in a perfect world).
```sh
# install SQLite3
brew install sqlite3

# install PostgreSQL and get DBs created, skipping creating a user
brew install postgres
brew services start postgres
createdb test1
createdb test2
createdb demo

# install MySQL, create a user, and a database
brew install mysql
brew services start mysql
mysql -u root -e "CREATE USER 'andy'@'localhost' IDENTIFIED BY 'password';"
mysql -u root -e "ALTER USER 'andy'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';"
mysql -u root -e "GRANT ALL PRIVILEGES ON *.* TO 'andy'@'localhost' WITH GRANT OPTION;"
mysql -u andy -p -e "CREATE DATABASE test1;"
mysql -u andy -p -e "CREATE DATABASE test2;"

export MYSQL_URL=mysql://andy:password@localhost/
export PG_URL=postgres://localhost/

npm install
npm run db-build
npm test
```

Run `npm install` and `npm run db-build` to prepare the fixture data. Check the `scripts` in the `package.json` for easily running the example data and the demo server with GraphiQL. Now you can begin coding.

Before commiting your changes, **run the lint, tests, and coverage to make sure everything is green.** After making your commits, push it up to your fork and make a pull request to our master branch. We will review it ASAP.

Expand Down
2 changes: 1 addition & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ Function for generating a `JOIN` condition.
<a name="thunk"></a>

## thunk : <code>function</code>
Rather than a constant value, its a function to dynamically return the value.
Rather than a constant value, it's a function to dynamically return the value.


| Param | Type | Description |
Expand Down
4 changes: 2 additions & 2 deletions docs/field-metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const User = new GraphQLObjectType({
},
email: {
type: GraphQLString,
// if the column name is different, it must be specified specified
// if the column name is different, it must be specified
sqlColumn: 'email_address'
},
idEncoded: {
Expand All @@ -21,7 +21,7 @@ const User = new GraphQLObjectType({
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.idEncoded)
resolve: user => toBase64(user.id)
}
})
})
Expand Down
2 changes: 1 addition & 1 deletion docs/many-to-many.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const User = new GraphQLObjectType({
//...
following: {
type: new GraphQLList(User),
// only get followers who's account is still active
// only get followees who's account is still active
where: accountTable => `${accountTable}.is_active = TRUE`,
junction: {
sqlTable: 'relationships',
Expand Down
2 changes: 1 addition & 1 deletion docs/where.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const QueryRoot = new GraphQLObjectType({
## Adding Context

Most often, we'll be asking for the *logged-in* user.
The `joinMonster` function has a second parameter which is basically an arbitrary object with useful contextual information that your `where` functions might depend on.
The `joinMonster` function has a third parameter which is basically an arbitrary object with useful contextual information that your `where` functions might depend on.
For example, you can pass in the ID of the logged in user to incorporate it into the `WHERE` condition.

```javascript
Expand Down
26 changes: 13 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"scripts": {
"build": "rm -rf dist && babel src --no-comments --out-dir dist",
"watch": "rm -rf dist && babel src --watch --source-maps true --out-dir dist",
"test": "npm run lint && bin/test --no-oracle",
"test": "bin/test --no-oracle",
"testsqlite3": "NODE_ENV=test ava test/*.js",
"testpg": "NODE_ENV=test DB=PG ava test/*.js",
"testpg-paging": "NODE_ENV=test DB=PG PAGINATE=keyset ava test/pagination/keyset-paging.js && NODE_ENV=test DB=PG PAGINATE=offset ava test/pagination/offset-paging.js",
Expand Down Expand Up @@ -71,33 +71,33 @@
"graphql": "0.6 || 0.7 || 0.8 || 0.9 || 0.10 || 0.11 || 0.12"
},
"devDependencies": {
"ava": "^0.23.0",
"ava": "^0.25.0",
"babel-cli": "^6.14.0",
"babel-eslint": "^8.0.0",
"babel-plugin-idx": "^1.5.1",
"babel-plugin-idx": "^2.2.0",
"babel-plugin-object-values-to-object-keys": "^1.0.2",
"babel-plugin-transform-do-expressions": "^6.22.0",
"babel-preset-node6": "^11.0.0",
"babel-preset-stage-1": "^6.24.1",
"babel-watch": "^2.0.3-rc0",
"eslint": "^4.7.1",
"eslint-config-airbnb-base": "^12.0.0",
"eslint": "^5.0.1",
"eslint-config-airbnb-base": "^13.0.0",
"faker": "^4.1.0",
"graphql": "^0.12.3",
"graphsiql": "0.1.0",
"jsdoc-to-markdown": "^3.0.0",
"graphql": "^0.13.0",
"graphsiql": "0.2.0",
"jsdoc-to-markdown": "^4.0.0",
"kcors": "^2.2.1",
"knex": "^0.14.0",
"knex": "^0.15.0",
"koa": "^2.0.1",
"koa-custom-graphiql": "1.0.1",
"koa-graphql": "^0.7.0",
"koa-router": "^7.1.1",
"koa-static": "^3.0.0",
"koa-static": "^5.0.0",
"mysql": "^2.14.1",
"nyc": "^11.1.0",
"nyc": "^12.0.1",
"pg": "^7.1.0",
"sinon": "^4.0.0",
"sqlite3": "^3.1.9"
"sinon": "^6.1.0",
"sqlite3": "^4.0.0"
},
"dependencies": {
"@stem/nesthydrationjs": "0.4.0",
Expand Down
1 change: 1 addition & 0 deletions src/array-to-connection.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { connectionFromArraySlice, cursorToOffset } from 'graphql-relay'
import { objToCursor, wrap, last } from './util'
import idx from 'idx'

// a function for data manipulation AFTER its nested.
// this is only necessary when using the SQL pagination
Expand Down
1 change: 1 addition & 0 deletions src/batch-planner/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { uniq, chain, map, groupBy, forIn } from 'lodash'
import arrToConnection from '../array-to-connection'
import { handleUserDbCall, maybeQuote, wrap, compileSqlAST } from '../util'
import idx from 'idx'

async function nextBatch(sqlAST, data, dbCall, context, options) {
// paginated fields are wrapped in connections. strip those off for the batching
Expand Down
1 change: 1 addition & 0 deletions src/define-object-shape.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { validateSqlAST, inspect } from './util'
import idx from 'idx'

// generate an object that defines the correct nesting shape for our GraphQL
// this will be used by the library NestHydrationJS, check out their docs
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ async function getNode(typeName, resolveInfo, context, condition, dbCall, option
const sqlAST = {}
const fieldNodes = resolveInfo.fieldNodes || resolveInfo.fieldASTs
// uses the same underlying function as the main `joinMonster`
queryAST.populateASTNode.call(resolveInfo, fieldNodes[0], fakeParentNode, sqlAST, namespace, 0, options)
queryAST.populateASTNode.call(resolveInfo, fieldNodes[0], fakeParentNode, sqlAST, namespace, 0, options, context)
queryAST.pruneDuplicateSqlDeps(sqlAST, namespace)
const { sql, shapeDefinition } = await compileSqlAST(sqlAST, context, options)
const data = arrToConnection(await handleUserDbCall(dbCall, sql, sqlAST, shapeDefinition), sqlAST)
Expand Down
8 changes: 4 additions & 4 deletions src/query-ast-to-sql-ast/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert from 'assert'
import { flatMap } from 'lodash'
import deprecate from 'deprecate'
import { getArgumentValues } from 'graphql/execution/values'
import idx from 'idx'

import AliasNamespace from '../alias-namespace'
import { wrap, ensure, unthunk, inspect } from '../util'
Expand Down Expand Up @@ -66,10 +67,9 @@ export function queryASTToSqlAST(resolveInfo, options, context) {
const parentType = resolveInfo.parentType
populateASTNode.call(resolveInfo, queryAST, parentType, sqlAST, namespace, 0, options, context)

// make sure they started this party on a table
assert.equal(
sqlAST.type,
'table',
// make sure they started this party on a table, interface or union.
assert.ok(
[ 'table', 'union' ].indexOf(sqlAST.type) > -1,
'Must call joinMonster in a resolver on a field where the type is decorated with "sqlTable".'
)

Expand Down
2 changes: 2 additions & 0 deletions src/stringifiers/dispatcher.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import assert from 'assert'
import { filter } from 'lodash'
import idx from 'idx'

import { validateSqlAST, inspect, wrap } from '../util'
import {
joinPrefix,
Expand Down
1 change: 1 addition & 0 deletions src/stringifiers/shared.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { filter } from 'lodash'
import { cursorToOffset } from 'graphql-relay'
import { wrap, cursorToObj, maybeQuote } from '../util'
import idx from 'idx'

export function joinPrefix(prefix) {
return prefix.slice(1).map(name => name + '__').join('')
Expand Down
Binary file modified test-api/data/db/demo-data.sl3
Binary file not shown.
Binary file modified test-api/data/db/test1-data.sl3
Binary file not shown.
2 changes: 1 addition & 1 deletion test-api/data/fetch.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

module.exports = function dbCall(sql, knex, context) {
if (context) {
if (context && context.response) {
context.set('X-SQL-Preview', context.response.get('X-SQL-Preview') + '%0A%0A' + sql.replace(/%/g, '%25').replace(/\n/g, '%0A'))
}
return knex.raw(sql).then(result => {
Expand Down
12 changes: 6 additions & 6 deletions test-api/data/schema/mysql.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ CREATE TABLE accounts (
first_name VARCHAR(255),
last_name VARCHAR(255),
num_legs INT DEFAULT 2,
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)
);

DROP TABLE IF EXISTS comments;
Expand All @@ -15,7 +15,7 @@ CREATE TABLE comments (
post_id INT NOT NULL,
author_id INT NOT NULL,
archived BOOLEAN DEFAULT FALSE,
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)
);

DROP TABLE IF EXISTS posts;
Expand All @@ -24,23 +24,23 @@ CREATE TABLE posts (
body TEXT NOT NULL,
author_id INT NOT NULL,
archived BOOLEAN DEFAULT FALSE,
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)
);

DROP TABLE IF EXISTS relationships;
CREATE TABLE relationships (
follower_id INT NOT NULL,
followee_id INT NOT NULL,
closeness VARCHAR(255),
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE (follower_id, followee_id)
);

DROP TABLE IF EXISTS likes;
CREATE TABLE likes (
account_id INTEGER NOT NULL,
comment_id INTEGER NOT NULL,
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE (account_id, comment_id)
);

Expand All @@ -50,6 +50,6 @@ CREATE TABLE sponsors (
first_name VARCHAR(255),
last_name VARCHAR(255),
num_legs INT DEFAULT 2,
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)
);

32 changes: 32 additions & 0 deletions test-api/schema-paginated/ContextPost.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
GraphQLObjectType,
GraphQLString,
GraphQLInt
} from 'graphql'
import {
globalIdField,
} from 'graphql-relay'
import { nodeInterface } from './Node'
import { q } from '../shared'

const { DB } = process.env

const ContextPost = new GraphQLObjectType({
description: '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',
fields: () => ({
id: {
...globalIdField(),
sqlDeps: ['id']
},
body: {
description: 'The content of the post',
type: GraphQLString
},
})
})

export default ContextPost
1 change: 0 additions & 1 deletion test-api/schema-paginated/Node.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,3 @@ const { nodeInterface, nodeField } = nodeDefinitions(
)

export { nodeInterface, nodeField }

13 changes: 12 additions & 1 deletion test-api/schema-paginated/QueryRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import knex from './database'
import { User, UserConnection } from './User'
import Sponsor from './Sponsor'
import { nodeField } from './Node'
import ContextPost from './ContextPost'

import joinMonster from '../../src/index'
import dbCall from '../data/fetch'
Expand Down Expand Up @@ -112,7 +113,17 @@ export default new GraphQLObjectType({
.catch(done)
}, options)
}
},
contextPosts: {
type: new GraphQLList(ContextPost),
resolve: (parent, args, context, resolveInfo) => {
// use the callback version this time
return joinMonster(resolveInfo, context, (sql, done) => {
knex.raw(sql)
.then(data => done(null, data))
.catch(done)
}, options)
}
}
})
})

13 changes: 13 additions & 0 deletions test/relay.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,19 @@ test('should handle a post without an author', async t => {
t.deepEqual(expect, data)
})

test('should pass context to getNode resolver', async t => {
const query = `{
node(id: "${toGlobalId('ContextPost', 1)}") {
... on ContextPost { body }
}
}`

const { data, errors } = await run(query, null, { table: 'posts' })
errCheck(t, errors)
const expect = { node: { body: 'If I could marry a programming language, it would be Haskell.' } }
t.deepEqual(expect, data)
})

test('should handle fragments recursively', async t => {
const query = `
{
Expand Down

0 comments on commit 6d5e072

Please sign in to comment.