From 282850489f46951fb8b80312c38c75de91165ab3 Mon Sep 17 00:00:00 2001 From: SUNNYKUMAR1232 <123469525+SUNNYKUMAR1232@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:41:56 +0530 Subject: [PATCH] feat: add poll feature --- package.json | 2 +- server/config/apolloServer.js | 3 +- server/schema/index.js | 5 +- server/schema/poll/poll.datasources.js | 89 ++++++++++++++ server/schema/poll/poll.model.js | 24 ++-- server/schema/poll/poll.mutation.js | 78 +++++++++++++ server/schema/poll/poll.query.js | 44 +++++++ server/schema/poll/poll.resolver.js | 154 +++++++++++++++++++++++++ server/schema/poll/poll.schema.js | 35 ++++++ server/schema/poll/poll.type.js | 41 +++++++ yarn.lock | 8 +- 11 files changed, 460 insertions(+), 23 deletions(-) create mode 100644 server/schema/poll/poll.datasources.js create mode 100644 server/schema/poll/poll.schema.js diff --git a/package.json b/package.json index 507a00ff..b4436dba 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,6 @@ "jsdoc": "^3.6.6", "jsdoc-to-markdown": "^6.0.1", "mocha": "^8.2.1", - "prettier": "^2.2.1" + "prettier": "^3.0.3" } } diff --git a/server/config/apolloServer.js b/server/config/apolloServer.js index 1e7a324c..4bbab968 100644 --- a/server/config/apolloServer.js +++ b/server/config/apolloServer.js @@ -11,7 +11,7 @@ const ArticleDataSources = require('../schema/article/article.datasources'); const CommentDataSources = require('../schema/comment/comment.datasources'); const CompanyDataSources = require('../schema/company/company.datasources'); const LiveDataSources = require('../schema/live/live.datasources'); - +const PollDataSources = require('../schema/poll/poll.datasources'); const APOLLO_ENDPOINT = process.env.APOLLO_ENDPOINT?.includes('herokuapp') ? process.env.APOLLO_ENDPOINT.replace('num', process.env.HEROKU_PR_NUMBER) : process.env.APOLLO_ENDPOINT; @@ -61,6 +61,7 @@ const apolloServer = (httpServer) => Company: CompanyDataSources(), Live: LiveDataSources(), Comment: CommentDataSources(), + Poll:PollDataSources() }, }), debug: !process.env.NODE_ENV || process.env.NODE_ENV === 'development', diff --git a/server/schema/index.js b/server/schema/index.js index 75a6ed8b..4ab91092 100644 --- a/server/schema/index.js +++ b/server/schema/index.js @@ -25,9 +25,7 @@ const CompanySchema = require('./company/company.schema'); const CompanyType = require('./company/company.type'); const LiveSchema = require('./live/live.schema'); const MediaSchema = require('./media/media.schema'); -const CommentSchema = require('./comment/comment.schema'); -const CommentType = require('./comment/comment.type'); - +const PollSchema = require('./poll/poll.schema') module.exports = stitchSchemas({ subschemas: [ UserSchema, @@ -39,6 +37,7 @@ module.exports = stitchSchemas({ LiveSchema, MediaSchema, CommentSchema, + PollSchema ], types: [MediaType, ContentType, UserDetailType, CategoryMapType, CompanyType, CommentType], mergeTypes: true, diff --git a/server/schema/poll/poll.datasources.js b/server/schema/poll/poll.datasources.js new file mode 100644 index 00000000..68329cd3 --- /dev/null +++ b/server/schema/poll/poll.datasources.js @@ -0,0 +1,89 @@ +const DataLoader = require('dataloader'); +const { APIError } = require('../../utils/exception'); +const PollModel = require('./poll.model'); +const ArticleModel = require('../article/article.model'); +const UserSession = require('../../utils/userAuth/session'); +const createUpdateObject = require('../../utils/createUpdateObject'); + +const findByID = new DataLoader( + async (ids) => { + try { + const _polls = await PollModel.find({ _id: ids }); + return ids.map((id) => _polls.find((_u) => _u.id.toString() === id.toString()) || null); + } catch (error) { + throw APIError(null, error); + } + }, + { + batchScheduleFn: (cb) => setTimeout(cb, 100), + } +); + +const find = (limit, offset) => PollModel.find().sort({ _id: 'desc' }).skip(offset).limit(limit); +const Create = async ( + question, + options, + optionsCount, + totalVotes, + expiry, + article, + createdBy, + session, + authToken, + mid +) => { + try { + const _poll = await PollModel.create({ + question, + options, + optionsCount, + totalVotes, + expiry, + article, + //createdBy: UserSession.valid(session, authToken) ? mid : null, + }); + return _poll; + } catch (error) { + throw APIError(null, error); + } +}; +const updateProps = async (id, updateFields, session, authToken, mid) => { + await PollModel.findByIdAndUpdate( + id, + { + ...createUpdateObject(updateFields), + //updatedBy: UserSession.valid(session, authToken) ? mid : null, + }, + { new: true } + ); +}; +const updateArticles = async (id, articles, session, authToken, mid) => { + try { + const _articleData = await ArticleModel.find({ _id: articles }); + + if (!_articleData || _articleData.length !== articles.length) { + throw APIError('NOT_FOUND', null, { reason: 'One or more of the articles provided were not found.' }); + } + return PollModel.findByIdAndUpdate( + id, + { + ...createUpdateObject({ articles }), + updatedBy: UserSession.valid(session, authToken) ? mid : null, + }, + { new: true } + ); + } catch (error) { + throw APIError(null, error); + } +}; +const remove = (id) => PollModel.findByIdAndDelete(id); +const PollDataSources = () => ({ + findByID, + find, + Create, + updateProps, + updateArticles, + remove, +}); + +module.exports = PollDataSources; diff --git a/server/schema/poll/poll.model.js b/server/schema/poll/poll.model.js index 41e59067..4881f24a 100644 --- a/server/schema/poll/poll.model.js +++ b/server/schema/poll/poll.model.js @@ -19,12 +19,10 @@ const { Schema, model } = require('mongoose'); */ const PollSchema = new Schema( { - question: [ - { - type: Object, - required: true, - }, - ], + question: { + type: String, + required: true, + }, options: [ { type: String, @@ -32,13 +30,11 @@ const PollSchema = new Schema( trim: true, }, ], - optionsCount: [ - { - type: Number, - required: true, - min: 0, - }, - ], + optionsCount: { + type: Number, + required: true, + min: 0, + }, totalVotes: { type: Number, required: false, @@ -47,7 +43,7 @@ const PollSchema = new Schema( }, expiry: { type: Date, - required: true, + required: false, min: new Date(Date.now()), }, article: [ diff --git a/server/schema/poll/poll.mutation.js b/server/schema/poll/poll.mutation.js index e69de29b..0f5dc2b8 100644 --- a/server/schema/poll/poll.mutation.js +++ b/server/schema/poll/poll.mutation.js @@ -0,0 +1,78 @@ +/** + * @module app.schema.PollMutation + * @description Poll Mutation + * + * @requires module:app.schema.scalars + * + * @version v1 + * @since 0.1.0 + */ + +const { + GraphQLObjectType, + //GraphQLString, + // GraphQLSchema, + GraphQLID, + GraphQLList, + //GraphQLBoolean, + GraphQLInt, + GraphQLNonNull, + GraphQLString, + //GraphQLDateTime, + GraphQLTime, + // GraphQLDate, + // GraphQLTime, + // GraphQLDateTime, + // GraphQLJSON, + // GraphQLJSONObject, +} = require('../scalars'); +const PollType = require('./poll.type'); +const { createPoll, updatePollProp, updatePollArticles, removePoll } = require('./poll.resolver'); +module.exports = new GraphQLObjectType({ + name: 'PollMutation', + fields: { + createPoll: { + description: 'adds a new poll', + type: PollType, + args: { + question: { type: new GraphQLNonNull(GraphQLString) }, + options: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)) }, + optionsCount: { type: new GraphQLNonNull(GraphQLInt) }, + totalVotes: { type: GraphQLInt }, + expiry: { type: GraphQLTime }, + article: { type: new GraphQLList(GraphQLID) }, + createdBy: { type: GraphQLID }, + }, + resolve: createPoll, + }, + updatePollProp: { + description: "updates a poll's properties", + type: PollType, + args: { + id: { type: new GraphQLNonNull(GraphQLID) }, + question: { type: GraphQLString }, + options: { type: GraphQLList(GraphQLString) }, + totalVotes: { type: GraphQLInt }, + optionsCount: { type: GraphQLInt }, + }, + resolve: updatePollProp, + }, + updatePollArticles: { + description: "updates a poll's articles", + type: PollType, + args: { + id: { type: new GraphQLNonNull(GraphQLID) }, + article: { type: new GraphQLList(GraphQLID) }, + }, + resolve: updatePollArticles, + }, + removePoll: { + description: 'deletes a poll by ID', + type: PollType, + args: { + id: { type: new GraphQLNonNull(GraphQLID) }, + }, + resolve: removePoll, + }, + }, +}); diff --git a/server/schema/poll/poll.query.js b/server/schema/poll/poll.query.js index e69de29b..b6d59eac 100644 --- a/server/schema/poll/poll.query.js +++ b/server/schema/poll/poll.query.js @@ -0,0 +1,44 @@ +const { + GraphQLObjectType, + //GraphQLString, + // GraphQLSchema, + GraphQLID, + GraphQLList, + GraphQLInt, + GraphQLNonNull, + // GraphQLDate, + // GraphQLTime, + // GraphQLDateTime, + // GraphQLJSON, + // GraphQLJSONObject, +} = require('../scalars'); +const { getPollByID, getListOfPolls } = require('./poll.resolver'); +const PollType = require('./poll.type'); +module.exports = new GraphQLObjectType({ + name: 'PollQuery', + fields: { + getPollByID: { + type: PollType, + args: { + id: { + type: new GraphQLNonNull(GraphQLID), + description: "The issue's mongo ID", + }, + }, + resolve: getPollByID, + }, + getListOfPolls: { + description: 'Retrieves a list of all polls.', + type: new GraphQLNonNull(GraphQLList(PollType)), + args: { + limit: { + type: GraphQLInt, + }, + offset: { + type: GraphQLInt, + }, + }, + resolve: getListOfPolls, + }, + }, +}); diff --git a/server/schema/poll/poll.resolver.js b/server/schema/poll/poll.resolver.js index e69de29b..9e4a0b71 100644 --- a/server/schema/poll/poll.resolver.js +++ b/server/schema/poll/poll.resolver.js @@ -0,0 +1,154 @@ +/** + * @module app.schema.PollResolver + * @description PollResolver + * + * @requires module:app.schema.PollType + * @requires module:app.schema.PollModel + * @requires module:app.authorization + * @version v1 + * @since 0.1.0 + */ + +const { APIError } = require('../../utils/exception'); +const getFieldNodes = require('../../utils/getFieldNodes'); +const UserPermission = require('../../utils/userAuth/permission'); + +const DEF_LIMIT = 10, + DEF_OFFSET = 0; + +const PUBLIC_FIELDS = ['id', 'question', 'options', 'optionsCount', 'totalVotes', 'expiry', 'articles']; +module.exports = { + getPollByID: async (_parent, { id }, { session, authToken, decodedToken, API: { Poll } }, { fieldNodes }) => { + try { + const _fields = getFieldNodes(fieldNodes); + if ( + _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && + !UserPermission.exists(session, authToken, decodedToken, 'poll.write.all') + ) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read the requested fields.', + }); + } + + const _poll = await Poll.findByID.load(id); + return _poll; + } catch (error) { + throw APIError(null, error); + } + }, + getListOfPolls: async ( + _parent, + { limit = DEF_LIMIT, offset = DEF_OFFSET }, + { session, authToken, decodedToken, API: { Poll } }, + { fieldNodes } + ) => { + try { + const _fields = getFieldNodes(fieldNodes); + if ( + _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && + !UserPermission.exists(session, authToken, decodedToken, 'poll.write.all') + ) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read the requested fields', + }); + } + const _polls = await Poll.find(limit, offset); + return _polls; + } catch (error) { + throw APIError(null, error); + } + }, + createPoll: ( + _parent, + { question, options, optionsCount, totalVotes, expiry, article, createdBy }, + { session, authToken, decodedToken, mid, API: { Poll } } + ) => { + try { + if (!UserPermission.exists(session, authToken, decodedToken, 'poll.write.all')) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read the requested fields', + }); + } + const _poll = Poll.Create( + question, + options, + optionsCount, + totalVotes, + expiry, + article, + createdBy, + session, + authToken, + mid + ); + return _poll; + } catch (error) { + throw APIError(null, error); + } + }, + updatePollProp: ( + _parent, + { id, question, optionsCount, options, totalVotes, expiry }, + { session, authToken, decodedToken, mid, API: { Poll } }, + { fieldNodes } + ) => { + try { + const _fields = getFieldNodes(fieldNodes); + + if ( + _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && + !UserPermission.exists(session, authToken, decodedToken, 'poll.write.all') + ) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read the requested fields', + }); + } + if (!UserPermission.exists(session, authToken, decodedToken, 'poll.write.all')) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permission to update this poll.', + }); + } + return Poll.updateProps(id, { question, options, optionsCount, totalVotes, expiry }, session, authToken, mid); + } catch (error) { + throw APIError(null, error); + } + }, + updatePollArticles: ( + _parent, + { id, articles }, + { session, authToken, decodedToken, mid, API: { Poll } }, + fieldNodes + ) => { + try { + const _fields = getFieldNodes(fieldNodes); + if ( + _fields.some((item) => !PUBLIC_FIELDS.includes(item)) && + !UserPermission.exists(session, authToken, decodedToken, 'poll.write.all') + ) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permissions to read the requested fields', + }); + } + if (!UserPermission.exists(session, authToken, decodedToken, 'poll.write.all')) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permission to update this poll.', + }); + } + return Poll.updateArticles(id, articles, session, authToken, mid); + } catch (error) { + throw APIError(null, error); + } + }, + removePoll: (_parent, { id }, { session, authToken, decodedToken, API: { Poll } }) => { + try { + if (!UserPermission.exists(session, authToken, decodedToken, 'poll.write.delete')) { + throw APIError('FORBIDDEN', null, { + reason: 'The user does not have the required permission to delete this poll.', + }); + } + return Poll.remove(id); + } catch (error) { + throw APIError(null, error); + } + }, +}; diff --git a/server/schema/poll/poll.schema.js b/server/schema/poll/poll.schema.js new file mode 100644 index 00000000..0c68ff23 --- /dev/null +++ b/server/schema/poll/poll.schema.js @@ -0,0 +1,35 @@ +/** + * @module app.schema.poll + * @description poll Schema + * + * @requires module:app.schema.scalars + * @requires module:app.schema.PollQuery + * @requires module:app.schema.PollMutation + * + * @version v1 + * @since 0.1.0 + */ + +const { + // GraphQLObjectType, + // GraphQLString, + GraphQLSchema, + // GraphQLID, + // GraphQLList, + // GraphQLBoolean, + // GraphQLInt, + // GraphQLNonNull, + // GraphQLDate, + // GraphQLTime, + // GraphQLDateTime, + // GraphQLJSON, + // GraphQLJSONObject, +} = require('../scalars'); +const PollMutation = require('./poll.mutation'); +const pollQuery = require('./poll.query'); +const PollType = require('./poll.type'); +module.exports = new GraphQLSchema({ + types: [PollType], + query: pollQuery, + mutation: PollMutation, +}); diff --git a/server/schema/poll/poll.type.js b/server/schema/poll/poll.type.js index e69de29b..da1b6196 100644 --- a/server/schema/poll/poll.type.js +++ b/server/schema/poll/poll.type.js @@ -0,0 +1,41 @@ +/** + * @module app.schema.PollType + * @description PollType + * + * @requires module:app.schema.scalars + * + * @version v1 + * @since 0.1.0 + */ + +const { + GraphQLObjectType, + GraphQLString, + // GraphQLSchema, + GraphQLID, + GraphQLList, + //GraphQLBoolean, + GraphQLInt, + // GraphQLNonNull, + // GraphQLDate, + // GraphQLTime, + GraphQLDateTime, + // GraphQLJSON, + // GraphQLJSONObject, +} = require('../scalars'); +const PollType = new GraphQLObjectType({ + name: 'Poll', + fields: () => ({ + id: { type: GraphQLID }, + question: { type: GraphQLString }, + options: { type: GraphQLList(GraphQLString) }, + optionsCount: { type: GraphQLInt }, + totalVotes: { type: GraphQLInt }, + expiry: { type: GraphQLDateTime }, + article: { type: GraphQLID }, + createdBy: { type: GraphQLID }, + updatedBy: { type: GraphQLID }, + schemaVersion: { type: GraphQLInt }, + }), +}); +module.exports = PollType; diff --git a/yarn.lock b/yarn.lock index cfe98ba0..abe510e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1834,10 +1834,10 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^2.2.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" - integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== +prettier@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" + integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== process-nextick-args@~2.0.0: version "2.0.1"