Skip to content
Snippets Groups Projects
graphql.js 11.8 KiB
Newer Older
Giannis Kopanas's avatar
Giannis Kopanas committed
const merge = require('lodash/merge')
const form = require('../../../app/storage/forms/submit.json')
const { ref } = require('objection')
Giannis Kopanas's avatar
Giannis Kopanas committed

const resolvers = {
  Mutation: {
    async createManuscript(_, vars, ctx) {
      const { Team } = require('@pubsweet/models')

Giannis Kopanas's avatar
Giannis Kopanas committed
      const { meta, files } = vars.input

      // We want the submission information to be stored as JSONB
      // but we want the input to come in as a JSON string
      const submission = vars.input.submission
        ? JSON.parse(vars.input.submission)
        : {}

Giannis Kopanas's avatar
Giannis Kopanas committed
      const emptyManuscript = {
        meta: Object.assign(meta, {
          notes: [
            {
              notesType: 'fundingAcknowledgement',
              content: '',
            },
            {
              notesType: 'specialInstructions',
              content: '',
            },
          ],
        }),
        status: 'new',
Jure's avatar
Jure committed
        submitterId: ctx.user.id,
      // eslint-disable-next-line
Jure's avatar
Jure committed
      const manuscript = await new ctx.models.Manuscript(
        emptyManuscript,
      ).saveGraph()

      // Create two channels: 1. free for all involved, 2. editorial
Jure's avatar
Jure committed
      const allChannel = new ctx.models.Channel({
        manuscriptId: manuscript.id,
        topic: 'Manuscript discussion',
        type: 'all',
      }).save()

Jure's avatar
Jure committed
      const editorialChannel = new ctx.models.Channel({
        manuscriptId: manuscript.id,
        topic: 'Editorial discussion',
        type: 'editorial',
      }).save()

Giannis Kopanas's avatar
Giannis Kopanas committed
      manuscript.manuscriptVersions = []
      manuscript.files = []
      files.map(async file => {
        const newFile = Object.assign({}, file, {
          fileType: 'manuscript',
          object: 'Manuscript',
          objectId: manuscript.id,
        })
        manuscript.files.push(
          // eslint-disable-next-line
Jure's avatar
Jure committed
          await new ctx.models.File(newFile).save(),
Giannis Kopanas's avatar
Giannis Kopanas committed
      })

      manuscript.reviews = []

      const createdTeam = await Team.query().upsertGraphAndFetch(
        {
          role: 'author',
          name: 'Author',
Giannis Kopanas's avatar
Giannis Kopanas committed
          objectId: manuscript.id,
          objectType: 'Manuscript',
Jure's avatar
Jure committed
          members: [{ user: { id: ctx.user.id } }],
        { relate: true },
      )
Giannis Kopanas's avatar
Giannis Kopanas committed

      manuscript.teams = [createdTeam]
      manuscript.channels = [allChannel, editorialChannel]
Giannis Kopanas's avatar
Giannis Kopanas committed
      return manuscript
    },
    async deleteManuscript(_, { id }, ctx) {
      const deleteManuscript = []
Jure's avatar
Jure committed
      const manuscript = await ctx.models.Manuscript.find(id)
Giannis Kopanas's avatar
Giannis Kopanas committed

      deleteManuscript.push(manuscript.id)
      if (manuscript.parentId) {
Jure's avatar
Jure committed
        const parentManuscripts = await ctx.models.Manuscript.findByField(
Giannis Kopanas's avatar
Giannis Kopanas committed
          'parent_id',
          manuscript.parentId,
        )

        parentManuscripts.forEach(manuscript => {
          deleteManuscript.push(manuscript.id)
        })
      }

      // Delete Manuscript
      if (deleteManuscript.length > 0) {
        deleteManuscript.forEach(async manuscript => {
Jure's avatar
Jure committed
          await ctx.models.Manuscript.query().deleteById(manuscript)
Giannis Kopanas's avatar
Giannis Kopanas committed
        })
      }
      return id
    },
    async reviewerResponse(_, { currentUserId, action, teamId }, context) {
      const { Team, Review } = require('@pubsweet/models')

Giannis Kopanas's avatar
Giannis Kopanas committed
      if (action !== 'accepted' && action !== 'rejected')
        throw new Error(
          `Invalid action provided to handleInvitation:
           Must be either "accepted" or "rejected"`,
        )

      const team = await Team.query()
        .findById(teamId)
        .eager('members')
      team.members = team.members.map(m => {
Jure's avatar
Jure committed
        if (m.userId === currentUserId) {
          m.status = action
        return m
Giannis Kopanas's avatar
Giannis Kopanas committed
      })
      if (!team) throw new Error('No team was found')

Jure's avatar
Jure committed
      await new Team(team).saveGraph()
Giannis Kopanas's avatar
Giannis Kopanas committed

      if (action === 'accepted') {
        const review = {
          recommendation: '',
          isDecision: false,
          userId: currentUserId,
          manuscriptId: team.objectId,
Giannis Kopanas's avatar
Giannis Kopanas committed
        }
        await new Review(review).save()
      }

      return team
    },
    async updateManuscript(_, { id, input }, ctx) {
      const data = JSON.parse(input)
      const manuscript = await ctx.models.Manuscript.query().findById(id)
Giannis Kopanas's avatar
Giannis Kopanas committed
      const update = merge({}, manuscript, data)
      return ctx.models.Manuscript.query().updateAndFetchById(id, update)
    async makeDecision(_, { id, decision }, ctx) {
      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',
        name: 'Reviewers',
      }).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]')
    },
Giannis Kopanas's avatar
Giannis Kopanas committed
  },
  Query: {
    async manuscript(_, { id }, ctx) {
      const Manuscript = require('./manuscript')

      const manuscript = await Manuscript.query()
        .findById(id)
        .eager('[teams, channels, reviews]')
Giannis Kopanas's avatar
Giannis Kopanas committed
      if (!manuscript.meta) {
        manuscript.meta = {}
      }
      manuscript.meta.notes = (manuscript.meta || {}).notes || [
        {
          notesType: 'fundingAcknowledgement',
          content: '',
        },
        {
          notesType: 'specialInstructions',
          content: '',
        },
      ]
      manuscript.decision = ''
Jure's avatar
Jure committed
      manuscript.files = await ctx.models.File.findByObject({
Giannis Kopanas's avatar
Giannis Kopanas committed
        object: 'Manuscript',
        object_id: manuscript.id,
      })

      manuscript.manuscriptVersions = await manuscript.getManuscriptVersions()
      // manuscript.channel = await ctx.connectors.Channel.model.find(
      //   manuscript.channelId,
      // )
Giannis Kopanas's avatar
Giannis Kopanas committed
      return manuscript
    },
    async manuscripts(_, { where }, ctx) {
Jure's avatar
Jure committed
      return ctx.models.Manuscript.query().eager('[teams, reviews]')
    async paginatedManuscripts(_, { sort, offset, limit, filter }, ctx) {
Jure's avatar
Jure committed
      const query = ctx.models.Manuscript.query().eager('submitter')

      if (filter && filter.status) {
        query.where({ status: filter.status })
      }

      const totalCount = await query.resultSize()

      if (sort) {
        const [sortName, sortDirection] = sort.split('_')

        query.orderBy(ref(sortName), sortDirection)
        // }
        // // e.g. 'created_DESC' into 'created' and 'DESC' arguments
        // query.orderBy(...sort.split('_'))
      }

      if (limit) {
        query.limit(limit)
      }

      if (offset) {
        query.offset(offset)
      }

      const manuscripts = await query
      return {
        totalCount,
        manuscripts,
      }

      // return ctx.connectors.User.fetchAll(where, ctx, { eager })
    },
Giannis Kopanas's avatar
Giannis Kopanas committed
    async getFile() {
      return form
    },
  },
  // We want submission into to come out as a stringified JSON, so that we don't have to
  // change our queries if the submission form changes. We still want to store it as JSONB
  // so that we can easily search through the information within.
  Manuscript: {
    submission(parent) {
      return JSON.stringify(parent.submission)
    },
    async reviews(parent, _, ctx) {
      return parent.reviews
        ? parent.reviews
        : (
            await ctx.models.Manuscript.query().findById(parent.id)
          ).$relatedQuery('reviews')
    },
    async teams(parent, _, ctx) {
      return parent.teams
        ? parent.teams
        : (
            await ctx.models.Manuscript.query().findById(parent.id)
          ).$relatedQuery('teams')
    },
  },
  ManuscriptVersion: {
    submission(parent) {
const typeDefs = `
  extend type Query {
    globalTeams: [Team]
    manuscript(id: ID!): Manuscript!
    manuscripts: [Manuscript]!
    paginatedManuscripts(sort: String, offset: Int, limit: Int, filter: ManuscriptsFilter): PaginatedManuscripts
  input ManuscriptsFilter {
    status: String
  }

  type PaginatedManuscripts {
    totalCount: Int
    manuscripts: [Manuscript]
  }

  # enum ManuscriptsSort {
  #   meta->>'title'_DESC
  #   created_ASC
  #   created_DESC
  #   updated_ASC
  #   updated_DESC
  # }

  extend type Mutation {
    createManuscript(input: ManuscriptInput): Manuscript!
    updateManuscript(id: ID!, input: String): Manuscript!
    makeDecision(id: ID!, decision: String): Manuscript!
    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 {
    id: ID!
    created: DateTime!
    updated: DateTime
    manuscriptVersions: [ManuscriptVersion]
    files: [File]
    teams: [Team]
    reviews: [Review!]
    status: String
    decision: String
    suggestions: Suggestions
    authors: [Author]
    meta: ManuscriptMeta
    submission: String
    channels: [Channel]
    submitter: User
  }

  type ManuscriptVersion implements Object {
    id: ID!
    created: DateTime!
    updated: DateTime
    files: [File]
    teams: [Team]
    reviews: [Review]
    status: String
    formState: String
    decision: String
    suggestions: Suggestions
    authors: [Author]
    meta: ManuscriptMeta
    submission: String
  }

  input ManuscriptInput {
    files: [FileInput]
    meta: ManuscriptMetaInput
    submission: String
  }

  input ManuscriptMetaInput {
    title: String
    source: String
  }

  input FileInput {
    filename: String
    url: String
    mimeType: String
    size: Int
  }

  type Author {
    firstName: String
    lastName: String
    email: String
    affiliation: String
  }

  type Suggestion {
    suggested: String
    opposed: String
  }

  type Suggestions {
    reviewers: Suggestion
    editors: Suggestion
  }

  type ManuscriptMeta {
    title: String!
    source: String
    articleType: String
    declarations: Declarations
    articleSections: [String]
    articleIds: [ArticleId]
    abstract: String
    subjects: [String]
    history: [MetaDate]
    publicationDates: [MetaDate]
    notes: [Note]
    keywords: String
  }

  type ArticleId {
    pubIdType: String
    id: String
  }

  type MetaDate {
    type: String
    date: DateTime
  }

  type Declarations {
    openData: String
    openPeerReview: String
    preregistered: String
    previouslySubmitted: String
    researchNexus: String
    streamlinedReview: String
  }

  type Note implements Object {
    id: ID!
    created: DateTime!
    updated: DateTime
    notesType: String
    content: String
  }
`

module.exports = {
  typeDefs,
  resolvers,
}