Commit 697c1ff7 authored by Jure's avatar Jure

Merge branch 'data-loaders-registration' into 'master'

feat(pubsweet-server): add loaders object to gql context

See merge request !578
parents 56f8887d 4e444124
Pipeline #13099 passed with stages
in 12 minutes and 35 seconds
const path = require('path')
module.exports = {
'pubsweet-server': {
db: {
// temporary database name set by jest-environment-db
database: global.__testDbName || 'test',
},
pool: { min: 0, max: 10, idleTimeoutMillis: 1000 },
port: 4000,
secret: 'test',
uploads: 'uploads',
},
authsome: {
mode: path.resolve(__dirname, '..', 'test', 'helpers', 'authsome_mode'),
teams: {},
},
pubsweet: {
components: [
'@pubsweet/model-user',
'@pubsweet/model-team',
'@pubsweet/model-fragment',
],
},
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Fragment queries lists fragments 1`] = `
Array [
Object {
"fragmentType": "testing",
"owners": Array [
Object {
"username": "testuser",
},
],
},
Object {
"fragmentType": "testing2",
"owners": Array [
Object {
"username": "anotheruser",
},
],
},
]
`;
process.env.NODE_CONFIG = `{"pubsweet":{
"components":[
"@pubsweet/model-user",
"@pubsweet/model-team",
"@pubsweet/model-fragment"
]
}}`
const { model: User } = require('@pubsweet/model-user')
const { dbCleaner, api } = require('pubsweet-server/test')
const { fixtures } = require('@pubsweet/model-user/test')
const authentication = require('pubsweet-server/src/authentication')
const Fragment = require('../src/fragment')
const { db } = require('@pubsweet/db-manager')
describe('Fragment queries', () => {
let token
let user
let otherUser
beforeEach(async () => {
await dbCleaner()
user = await new User(fixtures.user).save()
otherUser = await new User(fixtures.otherUser).save()
token = authentication.token.create(user)
})
it('lists fragments', async () => {
await Fragment.query().insert({
fragmentType: 'testing',
owners: [user.id],
})
await Fragment.query().insert({
fragmentType: 'testing2',
owners: [otherUser.id],
})
const whereIn = jest.spyOn(db.client, 'query')
const { body } = await api.graphql.query(
`query {
fragments {
fragmentType
owners {
username
}
}
}`,
{},
token,
)
// Gets the actual query that the database was called with
const lastCall = whereIn.mock.calls[whereIn.mock.calls.length - 1][1]
expect(lastCall.sql).toBe(
'select "users".* from "users" where "id" in ($1, $2)',
)
expect(lastCall.bindings.sort).toBe([user.id, otherUser.id].sort)
expect(body.data.fragments).toMatchSnapshot()
})
})
module.exports = async (userId, operation, object, context) => true
const path = require('path')
process.env.NODE_CONFIG_DIR = path.resolve(__dirname, '..', 'config')
......@@ -42,39 +42,37 @@ const resolvers = {
})
},
},
User: {
teams: (parent, _, ctx) =>
ctx.connectors.User.fetchRelated(parent.id, 'teams', undefined, ctx),
},
Team: {
// async members(team, { where }, ctx) {
// return team.members
// ? team.members
// : ctx.connectors.Team.fetchRelated(team.id, 'members', where, ctx)
// },
members(team, { where }, ctx) {
return ctx.connectors.Team.fetchRelated(team.id, 'members', where, ctx)
},
object(team, vars, ctx) {
const { objectId, objectType } = team
return objectId && objectType ? { objectId, objectType } : null
},
},
// TeamMember: {
// async user(teamMember, vars, ctx) {
// return teamMember.user
// ? teamMember.user
// : ctx.connectors.TeamMember.fetchRelated(
// teamMember.id,
// 'user',
// undefined,
// ctx,
// )
// },
// async alias(teamMember, vars, ctx) {
// return teamMember.alias
// ? teamMember.alias
// : ctx.connectors.TeamMember.fetchRelated(
// teamMember.id,
// 'alias',
// undefined,
// ctx,
// )
// },
// },
TeamMember: {
user(teamMember, vars, ctx) {
return ctx.connectors.TeamMember.fetchRelated(
teamMember.id,
'user',
undefined,
ctx,
)
},
alias(teamMember, vars, ctx) {
return ctx.connectors.TeamMember.fetchRelated(
teamMember.id,
'alias',
undefined,
ctx,
)
},
},
}
const typeDefs = `
......@@ -89,6 +87,10 @@ const typeDefs = `
updateTeam(id: ID, input: TeamInput): Team
}
extend type User {
teams: [Team]
}
type Team {
id: ID!
type: String!
......
const logger = require('@pubsweet/logger')
const { AuthorizationError, ConflictError } = require('@pubsweet/errors')
const eager = 'teams.members.[user, alias]'
// const eager = 'teams.members.[user, alias]'
const eager = undefined
const resolvers = {
Query: {
......@@ -93,7 +94,6 @@ const typeDefs = `
username: String
email: String
admin: Boolean
teams: [Team]
}
input UserInput {
......
......@@ -188,6 +188,15 @@
],
"testEnvironment": "jest-environment-db"
},
{
"rootDir": "<rootDir>/components/server/model-fragment",
"displayName": "model-fragment",
"testRegex": "/test/.*_test.js$",
"setupFilesAfterEnv": [
"<rootDir>/test/jest-setup.js"
],
"testEnvironment": "jest-environment-db"
},
{
"rootDir": "<rootDir>/components/server/component-password-reset-server",
"displayName": "component-password-reset-server",
......
const resolvers = {
Query: {
manuscript(_, { id }, ctx) {
return ctx.connectors.Manuscript.fetchOne(id, ctx)
return ctx.loaders.Manuscript.load(id)
},
manuscripts(_, { where }, ctx) {
return ctx.connectors.Manuscript.fetchAll(where, ctx)
......
......@@ -21,7 +21,7 @@ const resolvers = {
helpers: { can, canKnowAbout },
} = require('pubsweet-server')
const manuscript = await Manuscript.find(id)
const manuscript = await ctx.loaders.Manuscript.load(id)
const outputFilter = await canKnowAbout(ctx.user, manuscript)
const currentAndUpdate = {
......
......@@ -23,6 +23,7 @@
"colors": "^1.1.2",
"config": "^3.0.1",
"cookie-parser": "^1.4.3",
"dataloader": "^1.4.0",
"dotenv": "^4.0.0",
"express": "^4.16.1",
"fs-extra": "^7.0.1",
......
const { ref, lit } = require('objection')
const { NotFoundError } = require('@pubsweet/errors')
const notFoundError = (property, value, className) =>
new NotFoundError(`Object not found: ${className} with ${property} ${value}`)
// create a function which creates a new entity and performs authorization checks
const createCreator = (entityName, EntityModel) => async (
input,
......@@ -138,7 +143,11 @@ const fetchOneCreator = (entityName, EntityModel) => async (
) => {
await ctx.helpers.can(ctx.user, 'read', entityName)
const entity = await EntityModel.find(id, options)
const entity = await ctx.loaders[entityName].load(id)
if (!entity) {
throw notFoundError('id', id, entityName)
}
const outputFilter = await ctx.helpers.canKnowAbout(ctx.user, entity)
return outputFilter(entity)
......@@ -156,7 +165,7 @@ const fetchRelatedCreator = (entityName, EntityModel) => async (
ctx,
) => {
let entities
const entity = await EntityModel.find(id)
const entity = await ctx.loaders[entityName].load(id)
if (where) {
entities = await entity.$relatedQuery(relation).where(where)
} else {
......
......@@ -7,6 +7,7 @@ const config = require('config')
const schema = require('./schema')
const connectors = require('../connectors')
const loaders = require('./loaders')
const authBearerAndPublic = passport.authenticate(['bearer', 'anonymous'], {
session: false,
......@@ -27,9 +28,10 @@ const api = app => {
const server = new ApolloServer({
schema,
context: ({ req, res }) => ({
user: req.user,
connectors,
helpers,
connectors,
user: req.user,
loaders: loaders(),
}),
formatError: err => {
const error = err.originalError || err
......
const config = require('config')
const DataLoader = require('dataloader')
const tryRequireRelative = require('../helpers/tryRequireRelative')
// Require components here so that the requires are done only once per app runtime
let components = []
if (
config.has('pubsweet.components') &&
Array.isArray(config.get('pubsweet.components'))
) {
components = config.get('pubsweet.components')
components = components.map(componentName =>
tryRequireRelative(componentName),
)
}
const defaultLoader = model =>
new DataLoader(async ids => {
const results = await model.query().whereIn('id', ids)
// We map over ids so that the DataLoader API is always matched,
// i.e. array of keys in, array of results out (even if some records are not found)
return ids.map(id => results.find(result => result.id === id))
})
module.exports = () => {
const loaders = {}
components.forEach(component => {
if (component.model && component.modelName) {
// Sets up the default loader, that gets model instances by id
// You can use it with e.g. context.loaders.User.load(id)
loaders[component.modelName] = defaultLoader(component.model)
// Allows for custom model loaders, that can be used e.g.
// context.loaders.User.customLoader.load(id)
if (component.modelLoaders) {
Object.keys(component.modelLoaders).forEach(loaderName => {
loaders[component.modelName][loaderName] = new DataLoader(
component.modelLoaders[loaderName],
)
})
}
} else if (component.models) {
// If there are multiple models specified in a single component
// each can specify its own loaders
component.models.forEach(model => {
loaders[model.modelName] = defaultLoader(model.model)
if (model.modelLoaders) {
Object.keys(model.modelLoaders).forEach(loaderName => {
loaders[model.modelName][loaderName] = new DataLoader(
model.loaders[loaderName],
)
})
}
})
}
// Allows for top-level loaders, which you can use like so:
// context.loaders.yourCustomLoader.load()
if (component.loaders) {
Object.keys(component.loaders).forEach(loaderName => {
loaders[loaderName] = new DataLoader(component.loaders[loaderName])
})
}
})
return loaders
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment