diff --git a/packages/server/config/test.js b/packages/server/config/test.js index 93143c4ee1e7f833d082d3b9dba44cb4125c62cc..e60429ba4eaf4cf0d47e8d9a86941b288b5b37db 100644 --- a/packages/server/config/test.js +++ b/packages/server/config/test.js @@ -4,6 +4,7 @@ const winston = require('winston') module.exports = { 'pubsweet-server': { logger: new winston.Logger({ level: 'warn' }), + port: 4000, secret: 'test', sse: false, }, diff --git a/packages/server/package.json b/packages/server/package.json index f00bae033ff9e5d4c9d8b2c5223f6de30503e523..393c8cfe8de7e9b7b33976179270d895563f4867 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -15,6 +15,7 @@ "main": "src/index.js", "dependencies": { "@pubsweet/logger": "^0.2.1", + "apollo-server-express": "^1.3.2", "authsome": "0.0.9", "bcrypt": "^1.0.2", "bluebird": "^3.5.1", @@ -24,6 +25,8 @@ "cookie-parser": "^1.4.3", "dotenv": "^4.0.0", "express": "^4.16.1", + "graphql": "^0.12.3", + "graphql-tools": "^2.18.0", "helmet": "^3.8.1", "http-status-codes": "^1.0.6", "joi": "^13.1.0", diff --git a/packages/server/src/connectors/creators.js b/packages/server/src/connectors/creators.js new file mode 100644 index 0000000000000000000000000000000000000000..6ec3e5281438e2b8c20e9b6337748bba71f10782 --- /dev/null +++ b/packages/server/src/connectors/creators.js @@ -0,0 +1,120 @@ +const authsome = require('../helpers/authsome') +const AuthorizationError = require('../errors/AuthorizationError') +const NotFoundError = require('../errors/NotFoundError') + +// check permissions or throw authroization error +async function can(userId, verb, entity) { + const permission = await authsome.can(userId, verb, entity) + if (!permission) { + throw new AuthorizationError( + `Operation not permitted: ${verb} ${entity.type || entity}`, + ) + } + // return identity if no filter function + return permission.filter || (id => id) +} + +// check 'read' permissions or throw not found error (to avoid leaking the existence of data) +async function canKnowAbout(userId, entity) { + const permission = await authsome.can(userId, 'read', entity) + if (!permission) { + throw new NotFoundError( + `Object not found: ${entity.type} with id ${entity.id}`, + ) + } + // return identity if no filter function + return permission.filter || (id => id) +} + +// create a function which creates a new entity and performs authorization checks +function createCreator(entityName, EntityModel) { + return async (input, ctx) => { + await can(ctx.user, 'create', entityName) + const entity = new EntityModel(input) + const outputFilter = await canKnowAbout(ctx.user, entity) + await can(ctx.user, 'create', entity) + + return outputFilter(await entity.save()) + } +} + +// create a function which deletes an entity and performs authorization checks +function deleteCreator(entityName, EntityModel) { + return async (id, ctx) => { + await can(ctx.user, 'delete', entityName) + const entity = await EntityModel.find(id) + const outputFilter = await canKnowAbout(ctx.user, entity) + await can(ctx.user, 'delete', entity) + + return outputFilter(await entity.delete()) + } +} + +// create a function which updates a new entity and performs authorization checks +function updateCreator(entityName, EntityModel) { + return async (id, input, ctx) => { + await can(ctx.user, 'update', entityName) + const entity = await EntityModel.find(id) + const outputFilter = await canKnowAbout(ctx.user, entity) + const inputFilter = await can(ctx.user, 'update', entity) + await entity.updateProperties(inputFilter(input)) + + return outputFilter(await entity.save()) + } +} + +// create a function which fetches all entities of the +// given model and performs authorization checks +function fetchAllCreator(entityName, EntityModel) { + return async ctx => { + await can(ctx.user, 'read', entityName) + + const entities = await EntityModel.all() + // check permissions (in parallel) and swallow exceptions + const permissions = await Promise.all( + entities.map(entity => can(ctx.user, 'read', entity).catch(() => false)), + ) + + // apply permissions + return entities.reduce((filtered, entity, index) => { + const permissionOrFilter = permissions[index] + + if (permissionOrFilter) { + filtered.push(permissionOrFilter(entity)) + } + return filtered + }, []) + } +} + +// create a function which fetches by ID a single entity +// of the given model and performs authorization checks +function fetchOneCreator(entityName, EntityModel) { + return async (id, ctx) => { + await can(ctx.user, 'read', entityName) + + const entity = await EntityModel.find(id) + const outputFilter = await canKnowAbout(ctx.user, entity) + return outputFilter(entity) + } +} + +// create a function which fetches a number of entities by ID +// and delegates authorization checks +function fetchSomeCreator(fetchOne) { + return (ids, ctx) => Promise.all(ids.map(id => fetchOne(id, ctx))) +} + +// create a connector object with fetchers for all, one and some +function connectorCreator(entityName, EntityModel) { + const create = createCreator(entityName, EntityModel) + const del = deleteCreator(entityName, EntityModel) + const update = updateCreator(entityName, EntityModel) + const fetchAll = fetchAllCreator(entityName, EntityModel) + const fetchOne = fetchOneCreator(entityName, EntityModel) + const fetchSome = fetchSomeCreator(fetchOne) + + return { create, delete: del, update, fetchAll, fetchOne, fetchSome } +} + +module.exports = { connectorCreator } diff --git a/packages/server/src/connectors/index.js b/packages/server/src/connectors/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3b9c622beb3d6e49179e7ef747d5338601520f8d --- /dev/null +++ b/packages/server/src/connectors/index.js @@ -0,0 +1,12 @@ +const { connectorCreator } = require('./creators') +const Collection = require('../models/Collection') +const Fragment = require('../models/Fragment') +const Team = require('../models/Team') +const User = require('../models/User') + +module.exports = { + collection: connectorCreator('collections', Collection), + fragment: connectorCreator('fragments', Fragment), + team: connectorCreator('teams', Team), + user: connectorCreator('users', User), +} diff --git a/packages/server/src/graphql/definitions/authentication.js b/packages/server/src/graphql/definitions/authentication.js new file mode 100644 index 0000000000000000000000000000000000000000..ac427e0547e67e45be4ee12f20ef3fc0002114a1 --- /dev/null +++ b/packages/server/src/graphql/definitions/authentication.js @@ -0,0 +1,59 @@ +const logger = require('@pubsweet/logger') +const User = require('../../models/User') +const authentication = require('../../authentication') + +const resolvers = { + Query: { + async currentUser(_, vars, ctx) { + const user = await User.find(ctx.user) + return { + user, + token: authentication.token.create(user), + } + }, + }, + Mutation: { + async loginUser(_, { input }) { + let isValid = false + let user + try { + user = await User.findByUsername(input.username) + isValid = await user.validPassword(input.password) + } catch (err) { + logger.debug(err) + } + if (!isValid) { + throw new Error('Wrong username or password.') + } + return { + user, + token: authentication.token.create(user), + } + }, + }, +} + +const typeDefs = ` + extend type Query { + # Get the currently authenticated user based on the JWT in the HTTP headers + currentUser: LoginResult + } + + extend type Mutation { + # Authenticate a user using username and password + loginUser(input: LoginUserInput): LoginResult + } + + # User details and bearer token + type LoginResult { + user: User! + token: String! + } + + input LoginUserInput { + username: String! + password: String! + } +` + +module.exports = { resolvers, typeDefs } diff --git a/packages/server/src/graphql/definitions/collection.js b/packages/server/src/graphql/definitions/collection.js new file mode 100644 index 0000000000000000000000000000000000000000..814b7b956b7223b5ce77419e912c6fdc062806ad --- /dev/null +++ b/packages/server/src/graphql/definitions/collection.js @@ -0,0 +1,51 @@ +const resolvers = { + Query: { + collection(_, { id }, ctx) { + return ctx.connectors.collection.fetchOne(id, ctx) + }, + collections(ctx) { + return ctx.connectors.collection.fetchAll(ctx) + }, + }, + Mutation: { + deleteCollection(_, { id }, ctx) { + return ctx.connectors.collection.delete(id, ctx) + }, + createCollection(_, { input }, ctx) { + return ctx.connectors.collection.create(input, ctx) + }, + }, + Collection: { + owners(collection, vars, ctx) { + return collection.owners + ? ctx.connectors.user.fetchSome(collection.owners, ctx) + : [] + }, + }, +} + +const typeDefs = ` + extend type Query { + collection(id: ID): Collection + collections: [Collection] + } + + extend type Mutation { + createCollection(input: CollectionInput): Collection + deleteCollection(id: ID): Collection + } + + type Collection { + id: ID! + type: String! + owners: [User!]! + fragments: [Fragment!]! + } + + input CollectionInput { + owners: [ID!] + fragments: [ID!] + } +` + +module.exports = { resolvers, typeDefs } diff --git a/packages/server/src/graphql/definitions/fragment.js b/packages/server/src/graphql/definitions/fragment.js new file mode 100644 index 0000000000000000000000000000000000000000..0223bdb8bafabd4683cec81e3b5ba3de4a185e9a --- /dev/null +++ b/packages/server/src/graphql/definitions/fragment.js @@ -0,0 +1,52 @@ +const resolvers = { + Query: { + fragment(_, { id }, ctx) { + return ctx.connectors.fragment.fetchOne(id, ctx) + }, + fragments(ctx) { + return ctx.connectors.fragment.fetchAll(ctx) + }, + }, + Mutation: { + deleteFragment(_, { id }, ctx) { + return ctx.connectors.fragment.delete(id, ctx) + }, + createFragment(_, { input }, ctx) { + return ctx.connectors.fragment.create(input, ctx) + }, + }, + Fragment: { + owners(fragment, vars, ctx) { + return fragment.owners + ? ctx.connectors.user.fetchSome(fragment.owners, ctx) + : [] + }, + }, +} + +const typeDefs = ` + extend type Query { + fragment(id: ID): Fragment + fragments: [Fragment] + } + + extend type Mutation { + createFragment(input: FragmentInput): Fragment + deleteFragment(id: ID): Fragment + } + + type Fragment { + id: ID! + type: String! + fragmentType: String + fragments: [Fragment!]! + owners: [User!]! + } + + input FragmentInput { + owners: [ID!] + fragments: [ID!] + } +` + +module.exports = { resolvers, typeDefs } diff --git a/packages/server/src/graphql/definitions/team.js b/packages/server/src/graphql/definitions/team.js new file mode 100644 index 0000000000000000000000000000000000000000..18f2e69bf5d4bc060d70ac2da0dd164df456824c --- /dev/null +++ b/packages/server/src/graphql/definitions/team.js @@ -0,0 +1,53 @@ +const resolvers = { + Query: { + team(_, { id }, ctx) { + return ctx.connectors.team.fetchOne(id, ctx) + }, + teams(_, vars, ctx) { + return ctx.connectors.team.fetchAll(ctx) + }, + }, + Mutation: { + deleteTeam(_, { id }, ctx) { + return ctx.connectors.team.delete(id, ctx) + }, + createTeam(_, { input }, ctx) { + return ctx.connectors.team.create(input, ctx) + }, + }, + Team: { + members(team, vars, ctx) { + return team.members + ? ctx.connectors.user.fetchSome(team.members, ctx) + : [] + }, + }, +} + +const typeDefs = ` + extend type Query { + team(id: ID): Team + teams: [Team] + } + + extend type Mutation { + createTeam(input: TeamInput): Team + deleteTeam(id: ID): Team + } + + type Team { + id: ID! + type: String! + teamType: String! + name: String! + object: ID! + members: [User!]! + } + + input TeamInput { + owners: [ID!] + fragments: [ID!] + } +` + +module.exports = { resolvers, typeDefs } diff --git a/packages/server/src/graphql/definitions/user.js b/packages/server/src/graphql/definitions/user.js new file mode 100644 index 0000000000000000000000000000000000000000..29ecbcd824730b782761a04ff9b1c2b7d42fda45 --- /dev/null +++ b/packages/server/src/graphql/definitions/user.js @@ -0,0 +1,69 @@ +const resolvers = { + Query: { + user(_, { id }, ctx) { + return ctx.connectors.user.fetchOne(id, ctx) + }, + users(_, vars, ctx) { + return ctx.connectors.user.fetchAll(ctx) + }, + }, + Mutation: { + createUser(_, { input }, ctx) { + return ctx.connectors.user.create(input, ctx) + }, + deleteUser(_, { id }, ctx) { + return ctx.connectors.user.delete(id, ctx) + }, + updateUser(_, { id, input }, ctx) { + return ctx.connectors.user.update(id, input, ctx) + }, + }, + User: { + collections(user, vars, ctx) { + return user.collections + ? ctx.connectors.collection.fetchSome(user.collections, ctx) + : [] + }, + teams(user, vars, ctx) { + return user.teams ? ctx.connectors.team.fetchSome(user.teams, ctx) : [] + }, + fragments(user, vars, ctx) { + return user.fragments + ? ctx.connectors.fragment.fetchSome(user.fragments, ctx) + : [] + }, + }, +} + +const typeDefs = ` + extend type Query { + user(id: ID): User + users: [User] + } + + extend type Mutation { + createUser(input: UserInput): User + deleteUser(id: ID): User + updateUser(id: ID, input: UserInput): User + } + + type User { + id: ID! + type: String + username: String! + email: String! + admin: Boolean + teams: [Team!]! + fragments: [Fragment!]! + collections: [Collection!]! + } + + input UserInput { + username: String! + email: String! + password: String + rev: String + } +` + +module.exports = { resolvers, typeDefs } diff --git a/packages/server/src/graphql/routes.js b/packages/server/src/graphql/routes.js new file mode 100644 index 0000000000000000000000000000000000000000..680380e1c13a7d9056883260cb28065b66e0884c --- /dev/null +++ b/packages/server/src/graphql/routes.js @@ -0,0 +1,33 @@ +const express = require('express') +const passport = require('passport') +const { graphqlExpress, graphiqlExpress } = require('apollo-server-express') +const config = require('config') + +const graphqlSchema = require('./schema') +const connectors = require('../connectors') + +const authBearerAndPublic = passport.authenticate(['bearer', 'anonymous'], { + session: false, +}) +const router = new express.Router() + +router.use( + '/graphql', + authBearerAndPublic, + graphqlExpress(req => ({ + schema: graphqlSchema, + context: { user: req.user, connectors }, + })), +) +if ( + config.has('pubsweet-server.graphiql') && + config.get('pubsweet-server.graphiql') +) { + router.get( + '/graphiql', + authBearerAndPublic, + graphiqlExpress({ endpointURL: '/graphql' }), + ) +} + +module.exports = router diff --git a/packages/server/src/graphql/schema.js b/packages/server/src/graphql/schema.js new file mode 100644 index 0000000000000000000000000000000000000000..7f0803113a0127114dd2f90e534b0d137086869b --- /dev/null +++ b/packages/server/src/graphql/schema.js @@ -0,0 +1,51 @@ +const config = require('config') +const requireRelative = require('require-relative') +const { merge } = require('lodash') +const { makeExecutableSchema } = require('graphql-tools') + +const collection = require('./definitions/collection') +const fragment = require('./definitions/fragment') +const team = require('./definitions/team') +const user = require('./definitions/user') +const authentication = require('./definitions/authentication') + +// load base types and resolvers +const typeDefs = [ + `type Query, type Mutation`, + collection.typeDefs, + fragment.typeDefs, + team.typeDefs, + user.typeDefs, + authentication.typeDefs, +] +const resolvers = merge( + {}, + collection.resolvers, + fragment.resolvers, + team.resolvers, + user.resolvers, + authentication.resolvers, +) + +// merge in component types and resolvers +if (config.has('pubsweet.components')) { + config.get('pubsweet.components').forEach(name => { + const component = requireRelative(name) + if (component.typeDefs) { + typeDefs.push(component.typeDefs) + } + if (component.resolvers) { + merge(resolvers, component.resolvers) + } + }) +} + +// merge in app-specific types and resolvers from config +if (config.has('pubsweet-server.typeDefs')) { + typeDefs.push(config.get('pubsweet-server.typeDefs')) +} +if (config.has('pubsweet-server.resolvers')) { + merge(resolvers, config.get('pubsweet-server.resolvers')) +} + +module.exports = makeExecutableSchema({ typeDefs, resolvers }) diff --git a/packages/server/src/index.js b/packages/server/src/index.js index 20d4a476594fb6af9bf4dc4c886ca52005d458fa..7110e0bf01d6d2777a1f6c2d213ac8b8432eb5d6 100644 --- a/packages/server/src/index.js +++ b/packages/server/src/index.js @@ -10,6 +10,7 @@ const helmet = require('helmet') const cookieParser = require('cookie-parser') const bodyParser = require('body-parser') const passport = require('passport') +const graphqlApi = require('./graphql/routes') const index = require('./routes/index') const api = require('./routes/api') const authsome = require('./helpers/authsome') @@ -47,9 +48,15 @@ const configureApp = app => { registerComponents(app) - // Main API + // REST API app.use('/api', api) + // GraphQL API + // temporary environment check while this stuff is in beta + if (['development', 'test'].includes(config.util.getEnv('NODE_ENV'))) { + app.use(graphqlApi) + } + // SSE update stream if (_.get('pubsweet-server.sse', config)) { app.get( diff --git a/packages/server/test/graphql_test.js b/packages/server/test/graphql_test.js new file mode 100644 index 0000000000000000000000000000000000000000..a15039018202350ff91c4bca87f74a1ff575e6a0 --- /dev/null +++ b/packages/server/test/graphql_test.js @@ -0,0 +1,226 @@ +const { omit } = require('lodash') +const authsome = require('../src/helpers/authsome') +const User = require('../src/models/User') +const Team = require('../src/models/Team') +const cleanDB = require('./helpers/db_cleaner') +const fixtures = require('./fixtures/fixtures') +const api = require('./helpers/api') +const authentication = require('../src/authentication') + +describe('GraphQL endpoint', () => { + let token + let user + beforeEach(async () => { + await cleanDB() + user = await new User(fixtures.adminUser).save() + token = authentication.token.create(user) + }) + + describe('queries', () => { + it('can resolve all users', async () => { + const { body } = await api.graphql.query( + `{ users { username, admin } }`, + {}, + token, + ) + + expect(body).toEqual({ + data: { users: [{ username: 'admin', admin: true }] }, + }) + }) + + it('can resolve user by ID', async () => { + const { body } = await api.graphql.query( + `query($id: ID) { + user(id: $id) { + username + admin + } + }`, + { id: user.id }, + token, + ) + + expect(body).toEqual({ + data: { user: { username: 'admin', admin: true } }, + }) + }) + + it('can resolve nested query', async () => { + await new Team({ ...fixtures.contributorTeam, members: [user.id] }).save() + const { body } = await api.graphql.query( + `{ users { username, teams { name } } }`, + {}, + token, + ) + + expect(body).toEqual({ + data: { + users: [{ username: 'admin', teams: [{ name: 'My contributors' }] }], + }, + }) + }) + }) + + describe('mutations', () => { + it('can create a user', async () => { + const { body } = await api.graphql.query( + `mutation($input: UserInput) { + createUser(input: $input) { username } + }`, + { + input: { + username: 'floobs', + email: 'nobody@example.com', + password: 'password', + }, + }, + token, + ) + + expect(body).toEqual({ + data: { + createUser: { username: 'floobs' }, + }, + }) + }) + + it('can update a user', async () => { + const { body } = await api.graphql.query( + `mutation($id: ID, $input: UserInput) { + updateUser(id: $id, input: $input) { username, email } + }`, + { + id: user.id, + input: { + username: 'floobs', + email: 'nobody@example.com', + rev: user.rev, + }, + }, + token, + ) + + expect(body).toEqual({ + data: { + updateUser: { username: 'floobs', email: 'nobody@example.com' }, + }, + }) + }) + + it('can delete a user', async () => { + const { body } = await api.graphql.query( + `mutation($id: ID) { + deleteUser(id: $id) { username } + }`, + { id: user.id }, + token, + ) + + expect(body).toEqual({ + data: { deleteUser: { username: 'admin' } }, + }) + }) + }) + + describe('auth', () => { + it('can log in', async () => { + const { body } = await api.graphql.query( + `mutation($input: LoginUserInput) { + loginUser(input: $input) { + user { username } + token + } + }`, + { input: { username: 'admin', password: 'admin' } }, + ) + + expect(body).toMatchObject({ + data: { + loginUser: { token: expect.any(String), user: { username: 'admin' } }, + }, + }) + }) + + it('blocks invalid login', async () => { + const { body } = await api.graphql.query( + `mutation($input: LoginUserInput) { + loginUser(input: $input) { + token + } + }`, + { input: { username: 'admin', password: 'not correct' } }, + ) + + expect(body).toMatchObject({ + data: { loginUser: null }, + errors: [{ message: 'Wrong username or password.' }], + }) + }) + + it('fetches current user from token', async () => { + const { body } = await api.graphql.query( + `{ currentUser { user { username, email } token} }`, + {}, + token, + ) + + expect(body).toMatchObject({ + data: { + currentUser: { + user: { username: 'admin', email: 'admin@admins.example.org' }, + token: expect.any(String), + }, + }, + }) + }) + + it('errors when unauthenticated', async () => { + const { body } = await api.graphql.query(`{ users { username } }`) + + expect(body).toMatchObject({ + data: { users: null }, + errors: [{ message: 'Operation not permitted: read users' }], + }) + }) + + it('filters the returned data', async () => { + jest + .spyOn(authsome, 'can') + .mockReturnValue({ filter: user => omit(user, 'admin') }) + + const { body } = await api.graphql.query( + `{ users { username, admin } }`, + {}, + token, + ) + + expect(body).toEqual({ + data: { users: [{ username: 'admin', admin: null }] }, + }) + }) + + it('returns not found if not authorized', async () => { + jest + .spyOn(authsome, 'can') + .mockReturnValueOnce(true) + .mockReturnValueOnce(false) + + const { body } = await api.graphql.query( + `query($id: ID) { + user(id: $id) { + username + admin + } + }`, + { id: user.id }, + token, + ) + + expect(body).toMatchObject({ + data: { user: null }, + errors: [{ message: `Object not found: user with id ${user.id}` }], + }) + }) + }) +}) diff --git a/packages/server/test/helpers/api.js b/packages/server/test/helpers/api.js index 44d6cc449384b68e289a40a9947fc2b6eb1a359d..7fe3fb3912bc4e67dcb8cb5e1a42f98308afac78 100644 --- a/packages/server/test/helpers/api.js +++ b/packages/server/test/helpers/api.js @@ -285,11 +285,22 @@ const upload = { }, } +const graphql = { + query: (query, variables, token) => { + const req = request(api) + .post('/graphql') + .send({ query, variables }) + if (token) req.set('Authorization', `Bearer ${token}`) + return req + }, +} + module.exports = { fragments, users, collections, teams, upload, + graphql, api, } diff --git a/packages/server/test/mocks/mock_component.js b/packages/server/test/mocks/mock_component.js index 73803ecbdd56675d122e91a9969014be9cf0caa1..2717024918f4cbb3e8eda4560ed55f7d0072f1b2 100644 --- a/packages/server/test/mocks/mock_component.js +++ b/packages/server/test/mocks/mock_component.js @@ -6,6 +6,8 @@ const mockComponent = { res.status(STATUS.OK).json({ ok: '!' }), ) }, + typeDefs: `extend type Query { test: String }`, + resolvers: { Query: { test: () => 'OK' } }, } module.exports = mockComponent diff --git a/packages/server/test/register_components_test.js b/packages/server/test/register_components_test.js index 3af0161ac694573b949d8455664059af67ae84e7..aed8e1be86fc2215a50ce6fa0a8d1a7dd4cbbbd8 100644 --- a/packages/server/test/register_components_test.js +++ b/packages/server/test/register_components_test.js @@ -13,4 +13,9 @@ describe('App startup', async () => { const res = await request(api.api).get('/mock-component') expect(res.status).toBe(STATUS.OK) }) + + it('loads graphql types and resolvers', async () => { + const res = await api.graphql.query('{ test }') + expect(res.body).toEqual({ data: { test: 'OK' } }) + }) }) diff --git a/yarn.lock b/yarn.lock index f6ad2aaca9d381467cf71736cef8fbe84e4c2aa7..a0a33b1e2d9e9e2c2a29e9b0c6ec960c5e487f09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -221,6 +221,10 @@ version "8.0.58" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.58.tgz#5b3881c0be3a646874803fee3197ea7f1ed6df90" +"@types/zen-observable@0.5.3": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.5.3.tgz#91b728599544efbb7386d8b6633693a3c2e7ade5" + JSONStream@^1.0.4: version "1.3.1" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" @@ -415,6 +419,49 @@ anymatch@^1.3.0: micromatch "^2.1.5" normalize-path "^2.0.0" +apollo-cache-control@^0.0.x: + version "0.0.9" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.0.9.tgz#77100f456fb19526d33b7f595c8ab1a2980dcbb4" + dependencies: + graphql-extensions "^0.0.x" + +apollo-link@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.0.7.tgz#42cd38a7378332fc3e41a214ff6a6e5e703a556f" + dependencies: + "@types/zen-observable" "0.5.3" + apollo-utilities "^1.0.0" + zen-observable "^0.6.0" + +apollo-server-core@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-1.3.2.tgz#f36855a3ebdc2d77b8b9c454380bf1d706105ffc" + dependencies: + apollo-cache-control "^0.0.x" + apollo-tracing "^0.1.0" + graphql-extensions "^0.0.x" + +apollo-server-express@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-1.3.2.tgz#0ff8201c0bf362804a151e1399767dae6ab7e309" + dependencies: + apollo-server-core "^1.3.2" + apollo-server-module-graphiql "^1.3.0" + +apollo-server-module-graphiql@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.3.2.tgz#0a9e4c48dece3af904fee333f95f7b9817335ca7" + +apollo-tracing@^0.1.0: + version "0.1.3" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.1.3.tgz#6820c066bf20f9d9a4eddfc023f7c83ee2601f0b" + dependencies: + graphql-extensions "^0.0.x" + +apollo-utilities@^1.0.0, apollo-utilities@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.4.tgz#560009ea5541b9fdc4ee07ebb1714ee319a76c15" + app-root-path@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46" @@ -2635,7 +2682,7 @@ core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" -core-js@^2.4.0, core-js@^2.5.0: +core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.3: version "2.5.3" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" @@ -3085,6 +3132,10 @@ depd@1.1.1, depd@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" +deprecated-decorator@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37" + des.js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" @@ -4872,6 +4923,36 @@ graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" +graphql-extensions@^0.0.x: + version "0.0.7" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.0.7.tgz#807e7c3493da45e8f8fd02c0da771a9b3f1f2d1a" + dependencies: + core-js "^2.5.3" + source-map-support "^0.5.1" + +graphql-subscriptions@^0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.5.6.tgz#0d8e960fbaaf9ecbe7900366e86da2fc143fc5b2" + dependencies: + es6-promise "^4.1.1" + iterall "^1.1.3" + +graphql-tools@^2.18.0: + version "2.18.0" + resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-2.18.0.tgz#8e2d6436f9adba1d579c1a1710ae95e7f5e7248b" + dependencies: + apollo-link "^1.0.0" + apollo-utilities "^1.0.1" + deprecated-decorator "^0.1.6" + graphql-subscriptions "^0.5.6" + uuid "^3.1.0" + +graphql@^0.12.3: + version "0.12.3" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.12.3.tgz#11668458bbe28261c0dcb6e265f515ba79f6ce07" + dependencies: + iterall "1.1.3" + growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -5982,6 +6063,10 @@ istanbul-reports@^1.1.3: dependencies: handlebars "^4.0.3" +iterall@1.1.3, iterall@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.3.tgz#1cbbff96204056dde6656e2ed2e2226d0e6d72c9" + javascript-stringify@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3" @@ -10553,6 +10638,12 @@ source-map-support@^0.5.0: dependencies: source-map "^0.6.0" +source-map-support@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.2.tgz#1a6297fd5b2e762b39688c7fc91233b60984f0a5" + dependencies: + source-map "^0.6.0" + source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -12326,6 +12417,10 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" +zen-observable@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.6.1.tgz#01dbed3bc8d02cbe9ee1112c83e04c807f647244" + zip-stream@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04"