From 17424d12ed27d8aac4e2e2966a8fc2d4b67abe01 Mon Sep 17 00:00:00 2001 From: Jure Triglav <juretriglav@gmail.com> Date: Sat, 25 Jul 2020 04:43:10 +0200 Subject: [PATCH] refactor: serverside improvements to models --- server/app.js | 6 ++- server/model-file/src/resolvers.js | 31 ++++++++++++- server/model-file/src/typeDefs.js | 13 +++++- server/model-manuscript/src/graphql.js | 64 ++++++++++++++++++++++++-- server/model-review/src/resolvers.js | 27 +++++++++-- server/model-review/src/typeDefs.js | 3 +- server/model-team/src/graphql.js | 2 +- server/model-team/src/team_member.js | 1 - server/model-user/src/graphql.js | 55 +++++++++++----------- 9 files changed, 161 insertions(+), 41 deletions(-) diff --git a/server/app.js b/server/app.js index daf5ac7cb6..22ada08252 100644 --- a/server/app.js +++ b/server/app.js @@ -60,8 +60,10 @@ const configureApp = app => { if (config.has('pubsweet-server.uploads')) { app.use( - '/uploads', - express.static(path.resolve(config.get('pubsweet-server.uploads'))), + '/static/uploads', + express.static( + path.join(__dirname, '..', config.get('pubsweet-server.uploads')), + ), ) } // Passport strategies diff --git a/server/model-file/src/resolvers.js b/server/model-file/src/resolvers.js index 83b647dcdc..da8ea91c7c 100644 --- a/server/model-file/src/resolvers.js +++ b/server/model-file/src/resolvers.js @@ -1,10 +1,37 @@ +const crypto = require('crypto') +const { promisify } = require('util') +const fs = require('fs-extra') +const path = require('path') +const config = require('config') + const File = require('./file') +const randomBytes = promisify(crypto.randomBytes) +const uploadsPath = config.get('pubsweet-server').uploads + +const upload = async file => { + const { stream, filename, encoding } = await file + + const raw = await randomBytes(16) + const generatedFilename = raw.toString('hex') + path.extname(filename) + const outPath = path.join(uploadsPath, generatedFilename) + + await fs.ensureDir(uploadsPath) + const outStream = fs.createWriteStream(outPath) + stream.pipe(outStream, { encoding }) + + return new Promise((resolve, reject) => { + outStream.on('finish', () => resolve(outPath)) + outStream.on('error', reject) + }) +} const resolvers = { Query: {}, Mutation: { - async createFile(_, { id, file }, ctx) { - const data = await new File(file).save() + async createFile(_, { file, meta }, ctx) { + const path = await upload(file) + meta.url = `/static/${path}` + const data = await new File(meta).save() return data }, }, diff --git a/server/model-file/src/typeDefs.js b/server/model-file/src/typeDefs.js index 9b70a794c1..34050c96a9 100644 --- a/server/model-file/src/typeDefs.js +++ b/server/model-file/src/typeDefs.js @@ -1,6 +1,17 @@ const typeDefs = ` extend type Mutation { - createFile(file: Upload!): File! + # Using a separate variable because the Upload type hides other data + createFile(file: Upload!, meta: FileMetaInput): File! + } + + input FileMetaInput { + fileType: String + filename: String + mimeType: String + object: String + objectId: ID! + label: String + size: Int } type File implements Object { diff --git a/server/model-manuscript/src/graphql.js b/server/model-manuscript/src/graphql.js index e615f43bea..25fdb53ae2 100644 --- a/server/model-manuscript/src/graphql.js +++ b/server/model-manuscript/src/graphql.js @@ -143,18 +143,74 @@ const resolvers = { }, async updateManuscript(_, { id, input }, ctx) { const data = JSON.parse(input) - const manuscript = await ctx.models.Manuscript.findById(id) + const manuscript = await ctx.models.Manuscript.query().findById(id) const update = merge({}, manuscript, data) - return ctx.models.Manuscript.update(id, update, ctx) + return ctx.models.Manuscript.query().updateAndFetchById(id, update) }, async makeDecision(_, { id, decision }, ctx) { - const manuscript = await ctx.models.Manuscript.findById(id) + const manuscript = await ctx.models.Manuscript.query().findById(id) manuscript.decision = decision manuscript.status = decision return manuscript.save() }, + async addReviewer(_, { manuscriptId, userId }, ctx) { + const manuscript = await ctx.models.Manuscript.query().findById( + manuscriptId, + ) + + const existingTeam = await manuscript + .$relatedQuery('teams') + .where('role', 'reviewer') + .first() + + // Add the reviewer to the existing team of reviewers + if (existingTeam) { + const reviewerExists = + (await existingTeam + .$relatedQuery('users') + .where('users.id', userId) + .resultSize()) > 0 + if (!reviewerExists) { + await new ctx.models.TeamMember({ + teamId: existingTeam.id, + status: 'invited', + userId, + }).save() + } + return existingTeam.$query().eager('members.[user]') + } + + // Create a new team of reviewers if it doesn't exist + const newTeam = await new ctx.models.Team({ + objectId: manuscriptId, + objectType: 'Manuscript', + members: [{ status: 'invited', userId }], + role: 'reviewer', + }).saveGraph() + + return newTeam + }, + async removeReviewer(_, { manuscriptId, userId }, ctx) { + const manuscript = await ctx.models.Manuscript.query().findById( + manuscriptId, + ) + + const reviewerTeam = await manuscript + .$relatedQuery('teams') + .where('role', 'reviewer') + .first() + + await ctx.models.TeamMember.query() + .where({ + userId, + teamId: reviewerTeam.id, + }) + .delete() + + return reviewerTeam.$query().eager('members.[user]') + }, }, Query: { async manuscript(_, { id }, ctx) { @@ -277,6 +333,8 @@ const typeDefs = ` deleteManuscript(id: ID!): ID! reviewerResponse(currentUserId: ID, action: String, teamId: ID! ): Team assignTeamEditor(id: ID!, input: String): [Team] + addReviewer(manuscriptId: ID!, userId: ID!): Team + removeReviewer(manuscriptId: ID!, userId: ID!): Team } type Manuscript implements Object { diff --git a/server/model-review/src/resolvers.js b/server/model-review/src/resolvers.js index 90b611e5b4..9a8fc508e4 100644 --- a/server/model-review/src/resolvers.js +++ b/server/model-review/src/resolvers.js @@ -5,11 +5,13 @@ const resolvers = { Mutation: { async updateReview(_, { id, input }, ctx) { if (id) { - const review = await ctx.connectors.Review.fetchOne(id, ctx) + const review = await ctx.models.Review.query().findById(id) const update = merge({}, review, input) - await ctx.connectors.Review.update(id, update, ctx) // Load Review - const rvw = await new Review(update) + const rvw = await ctx.models.Review.query().updateAndFetchById( + id, + update, + ) rvw.comments = await rvw.getComments() return rvw @@ -21,6 +23,25 @@ const resolvers = { return review }, + + async completeReview(_, { id }, ctx) { + const review = await ctx.models.Review.query().findById(id) + const manuscript = await ctx.models.Manuscript.query().findById( + review.manuscriptId, + ) + const team = await manuscript + .$relatedQuery('teams') + .where('role', 'reviewer') + .first() + + const member = await team + .$relatedQuery('members') + .where('userId', ctx.user.id) + .first() + + member.status = 'completed' + return member.save() + }, }, } diff --git a/server/model-review/src/typeDefs.js b/server/model-review/src/typeDefs.js index 0ded633743..fb39b5e390 100644 --- a/server/model-review/src/typeDefs.js +++ b/server/model-review/src/typeDefs.js @@ -1,9 +1,10 @@ const typeDefs = ` extend type Mutation { updateReview(id: ID, input: ReviewInput): Review! + completeReview(id: ID!): TeamMember } - type Review implements Object { + type Review implements Object { id: ID! created: DateTime! updated: DateTime diff --git a/server/model-team/src/graphql.js b/server/model-team/src/graphql.js index 0f1b08e636..ed95c10c5e 100644 --- a/server/model-team/src/graphql.js +++ b/server/model-team/src/graphql.js @@ -98,7 +98,7 @@ const typeDefs = ` id: ID! type: String! role: String! - name: String! + name: String object: TeamObject members: [TeamMember!] owners: [User] diff --git a/server/model-team/src/team_member.js b/server/model-team/src/team_member.js index 51236eff1e..eff8dd7326 100644 --- a/server/model-team/src/team_member.js +++ b/server/model-team/src/team_member.js @@ -43,7 +43,6 @@ class TeamMember extends BaseModel { teamId: { type: 'string', format: 'uuid' }, aliasId: { type: ['string', 'null'], format: 'uuid' }, status: { type: 'string' }, - global: { type: ['boolean', 'null'] }, }, } } diff --git a/server/model-user/src/graphql.js b/server/model-user/src/graphql.js index 56565dd4a4..241ca795f8 100644 --- a/server/model-user/src/graphql.js +++ b/server/model-user/src/graphql.js @@ -142,16 +142,16 @@ const resolvers = { return identities }, }, - LocalIdentity: { - __isTypeOf: (obj, context, info) => obj.type === 'local', - async email(obj, args, ctx, info) { - // Emails stored on user, but surfaced in local identity too - return (await ctx.loaders.User.load(obj.userId)).email - }, - }, - ExternalIdentity: { - __isTypeOf: (obj, context, info) => obj.type !== 'local', - }, + // LocalIdentity: { + // __isTypeOf: (obj, context, info) => obj.type === 'local', + // async email(obj, args, ctx, info) { + // // Emails stored on user, but surfaced in local identity too + // return (await ctx.loaders.User.load(obj.userId)).email + // }, + // }, + // ExternalIdentity: { + // __isTypeOf: (obj, context, info) => obj.type !== 'local', + // }, } const typeDefs = ` @@ -209,33 +209,34 @@ const typeDefs = ` roles: [String] } - interface Identity { + type Identity { id: ID name: String aff: String # JATS <aff> email: String # JATS <aff> type: String + identifier: String } # union Identity = Local | External # local identity (not from ORCID, etc.) - type LocalIdentity implements Identity { - id: ID - name: String - email: String - aff: String - type: String - } - - type ExternalIdentity implements Identity { - id: ID - name: String - identifier: String - email: String - aff: String - type: String - } + #type LocalIdentity implements Identity { + # id: ID + # name: String + # email: String + # aff: String + # type: String + #} + # + #type ExternalIdentity implements Identity { + # id: ID + # name: String + # identifier: String + # email: String + # aff: String + # type: String + #} input UserInput { username: String! -- GitLab