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