From 5e0c8885c247897c6c974f00b27017a0ed9b21f3 Mon Sep 17 00:00:00 2001 From: meetul Date: Wed, 3 Jan 2024 22:37:50 +0530 Subject: [PATCH] feat: Add support for Categories --- codegen.ts | 4 + src/constants.ts | 7 ++ src/models/ActionItem.ts | 109 ++++++++++++++++++ src/models/Category.ts | 67 +++++++++++ src/models/index.ts | 2 + src/resolvers/Category/createdBy.ts | 8 ++ src/resolvers/Category/index.ts | 10 ++ src/resolvers/Category/org.ts | 8 ++ src/resolvers/Category/updatedBy.ts | 8 ++ src/resolvers/Mutation/createCategory.ts | 79 +++++++++++++ src/resolvers/Mutation/index.ts | 4 + src/resolvers/Mutation/updateCategory.ts | 84 ++++++++++++++ .../Query/categoriesByOrganization.ts | 18 +++ src/resolvers/Query/category.ts | 29 +++++ src/resolvers/Query/index.ts | 4 + src/resolvers/index.ts | 2 + src/typeDefs/inputs.ts | 5 + src/typeDefs/mutations.ts | 4 + src/typeDefs/queries.ts | 4 + src/typeDefs/types.ts | 30 +++++ 20 files changed, 486 insertions(+) create mode 100644 src/models/ActionItem.ts create mode 100644 src/models/Category.ts create mode 100644 src/resolvers/Category/createdBy.ts create mode 100644 src/resolvers/Category/index.ts create mode 100644 src/resolvers/Category/org.ts create mode 100644 src/resolvers/Category/updatedBy.ts create mode 100644 src/resolvers/Mutation/createCategory.ts create mode 100644 src/resolvers/Mutation/updateCategory.ts create mode 100644 src/resolvers/Query/categoriesByOrganization.ts create mode 100644 src/resolvers/Query/category.ts diff --git a/codegen.ts b/codegen.ts index d0f9fc4a950..5415a4fb3e5 100644 --- a/codegen.ts +++ b/codegen.ts @@ -25,6 +25,10 @@ const config: CodegenConfig = { // functionality is useful because what we retrieve from the database and what we choose to return from a graphql server // could be completely different fields. Address to models here is relative to the location of generated types. mappers: { + ActionItem: "../models/ActionItem#InterfaceActionItem", + + Category: "../models/Category#InterfaceCategory", + CheckIn: "../models/CheckIn#InterfaceCheckIn", MessageChat: "../models/MessageChat#InterfaceMessageChat", diff --git a/src/constants.ts b/src/constants.ts index 7980d0b5f4b..61987a32785 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,6 +7,13 @@ if (!issues) { ENV = envSchema.parse(process.env); } +export const CATEGORY_NOT_FOUND_ERROR = { + DESC: "Category not found", + CODE: "category.notFound", + MESSAGE: "category.notFound", + PARAM: "category", +}; + export const CHAT_NOT_FOUND_ERROR = { DESC: "Chat not found", CODE: "chat.notFound", diff --git a/src/models/ActionItem.ts b/src/models/ActionItem.ts new file mode 100644 index 00000000000..105a46abf60 --- /dev/null +++ b/src/models/ActionItem.ts @@ -0,0 +1,109 @@ +import type { PopulatedDoc, Types, Document, Model } from "mongoose"; +import { Schema, model, models } from "mongoose"; +import type { InterfaceUser } from "./User"; +import type { InterfaceEvent } from "./Event"; +import type { InterfaceCategory } from "./Category"; + +/** + * This is an interface that represents a database(MongoDB) document for ActionItem. + */ + +export interface InterfaceActionItem { + _id: Types.ObjectId; + assignedTo: PopulatedDoc; + assignedBy: PopulatedDoc; + category: PopulatedDoc; + preCompletionNotes: string; + postCompletionNotes: string; + assignmentDate: Date; + dueDate: Date; + completionDate: Date; + completed: boolean; + eventId: PopulatedDoc; + createdBy: PopulatedDoc; + updatedBy: PopulatedDoc; + createdAt: Date; + updatedAt: Date; +} + +/** + * This describes the schema for a `ActionItem` that corresponds to `InterfaceActionItem` document. + * @param assignedTo - User to whom the ActionItem is assigned, refer to `User` model. + * @param assignedBy - User who assigned the ActionItem, refer to the `User` model. + * @param category - Category to which the ActionItem is related, refer to the `Category` model. + * @param preCompletionNotes - Notes prior to completion. + * @param postCompletionNotes - Notes on completion. + * @param assignmentDate - Date of assignment. + * @param dueDate - Due date. + * @param completionDate - Completion date. + * @param completed - Whether the ActionItem has been completed. + * @param eventId - Event to which the ActionItem is related, refer to the `Event` model. + * @param createdBy - User who created the ActionItem, refer to the `User` model. + * @param updatedBy - User who last updated the ActionItem, refer to the `User` model. + * @param createdAt - Timestamp when the ActionItem was created. + * @param updatedAt - Timestamp when the ActionItem was last updated. + */ + +const actionItemSchema = new Schema( + { + assignedTo: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + assignedBy: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + category: { + type: Schema.Types.ObjectId, + ref: "Category", + required: true, + }, + preCompletionNotes: { + type: String, + }, + postCompletionNotes: { + type: String, + }, + assignmentDate: { + type: Date, + default: Date.now(), + }, + dueDate: { + type: Date, + default: Date.now() + 7 * 24 * 60 * 60 * 1000, + }, + completionDate: { + type: Date, + default: Date.now() + 7 * 24 * 60 * 60 * 1000, + }, + completed: { + type: Boolean, + required: true, + }, + eventId: { + type: Schema.Types.ObjectId, + ref: "Event", + }, + createdBy: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + updatedBy: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + }, + { timestamps: true } +); + +const actionItemModel = (): Model => + model("ActionItem", actionItemSchema); + +// This syntax is needed to prevent Mongoose OverwriteModelError while running tests. +export const ActionItem = (models.ActionItem || + actionItemModel()) as ReturnType; diff --git a/src/models/Category.ts b/src/models/Category.ts new file mode 100644 index 00000000000..f644ed9b6fc --- /dev/null +++ b/src/models/Category.ts @@ -0,0 +1,67 @@ +import type { PopulatedDoc, Types, Document, Model } from "mongoose"; +import { Schema, model, models } from "mongoose"; +import type { InterfaceUser } from "./User"; +import type { InterfaceOrganization } from "./Organization"; + +/** + * This is an interface that represents a database(MongoDB) document for Category. + */ + +export interface InterfaceCategory { + _id: Types.ObjectId; + category: string; + org: PopulatedDoc; + disabled: boolean; + createdBy: PopulatedDoc; + updatedBy: PopulatedDoc; + createdAt: Date; + updatedAt: Date; +} + +/** + * This describes the schema for a `category` that corresponds to `InterfaceCategory` document. + * @param category - A category to be selected for ActionItems. + * @param org - Organization the category belongs to, refer to the `Organization` model. + * @param disabled - Whether category is disabled or not. + * @param createdBy - Task creator, refer to `User` model. + * @param updatedBy - Task creator, refer to `User` model. + * @param createdAt - Time stamp of data creation. + * @param updatedAt - Time stamp of data updation. + */ + +const categorySchema = new Schema( + { + category: { + type: String, + required: true, + }, + org: { + type: Schema.Types.ObjectId, + ref: "Organization", + required: true, + }, + disabled: { + type: Boolean, + default: false, + }, + createdBy: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + updatedBy: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + }, + { timestamps: true } +); + +const categoryModel = (): Model => + model("Category", categorySchema); + +// This syntax is needed to prevent Mongoose OverwriteModelError while running tests. +export const Category = (models.Category || categoryModel()) as ReturnType< + typeof categoryModel +>; diff --git a/src/models/index.ts b/src/models/index.ts index e63bd4e63d1..9af8aae40e1 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,4 +1,6 @@ +export * from "./ActionItem"; export * from "./Advertisement"; +export * from "./Category"; export * from "./CheckIn"; export * from "./MessageChat"; export * from "./Comment"; diff --git a/src/resolvers/Category/createdBy.ts b/src/resolvers/Category/createdBy.ts new file mode 100644 index 00000000000..64fa5ca4cda --- /dev/null +++ b/src/resolvers/Category/createdBy.ts @@ -0,0 +1,8 @@ +import type { CategoryResolvers } from "../../types/generatedGraphQLTypes"; +import { User } from "../../models"; + +export const createdBy: CategoryResolvers["createdBy"] = async (parent) => { + return User.findOne({ + _id: parent.createdBy, + }).lean(); +}; diff --git a/src/resolvers/Category/index.ts b/src/resolvers/Category/index.ts new file mode 100644 index 00000000000..2413266db9e --- /dev/null +++ b/src/resolvers/Category/index.ts @@ -0,0 +1,10 @@ +import type { CategoryResolvers } from "../../types/generatedGraphQLTypes"; +import { org } from "./org"; +import { createdBy } from "./createdBy"; +import { updatedBy } from "./updatedBy"; + +export const Category: CategoryResolvers = { + org, + createdBy, + updatedBy, +}; diff --git a/src/resolvers/Category/org.ts b/src/resolvers/Category/org.ts new file mode 100644 index 00000000000..88de244b098 --- /dev/null +++ b/src/resolvers/Category/org.ts @@ -0,0 +1,8 @@ +import type { CategoryResolvers } from "../../types/generatedGraphQLTypes"; +import { Organization } from "../../models"; + +export const org: CategoryResolvers["org"] = async (parent) => { + return Organization.findOne({ + _id: parent.org, + }).lean(); +}; diff --git a/src/resolvers/Category/updatedBy.ts b/src/resolvers/Category/updatedBy.ts new file mode 100644 index 00000000000..9e19f9679ef --- /dev/null +++ b/src/resolvers/Category/updatedBy.ts @@ -0,0 +1,8 @@ +import type { CategoryResolvers } from "../../types/generatedGraphQLTypes"; +import { User } from "../../models"; + +export const updatedBy: CategoryResolvers["updatedBy"] = async (parent) => { + return User.findOne({ + _id: parent.updatedBy, + }).lean(); +}; diff --git a/src/resolvers/Mutation/createCategory.ts b/src/resolvers/Mutation/createCategory.ts new file mode 100644 index 00000000000..1e2d4b16633 --- /dev/null +++ b/src/resolvers/Mutation/createCategory.ts @@ -0,0 +1,79 @@ +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { User, Category, Organization } from "../../models"; +import { errors, requestContext } from "../../libraries"; +import { + USER_NOT_FOUND_ERROR, + ORGANIZATION_NOT_FOUND_ERROR, +} from "../../constants"; + +import { adminCheck } from "../../utilities"; +import { findOrganizationsInCache } from "../../services/OrganizationCache/findOrganizationsInCache"; +import { cacheOrganizations } from "../../services/OrganizationCache/cacheOrganizations"; + +/** + * This function enables to create a task. + * @param _parent - parent of current request + * @param args - payload provided with the request + * @param context - context of entire application + * @remarks The following checks are done: + * 1. If the User exists + * 2. If the Organization exists + * 3. Is the User is Authorized + * @returns Created Category + */ + +export const createCategory: MutationResolvers["createCategory"] = async ( + _parent, + args, + context +) => { + const currentUser = await User.findOne({ + _id: context.userId, + }); + + // Checks whether currentUser with _id == context.userId exists. + if (currentUser === null) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + let organization; + + const organizationFoundInCache = await findOrganizationsInCache([args.orgId]); + + organization = organizationFoundInCache[0]; + + if (organizationFoundInCache[0] == null) { + organization = await Organization.findOne({ + _id: args.orgId, + }).lean(); + + await cacheOrganizations([organization!]); + } + + // Checks whether the organization with _id === args.orgId exists. + if (!organization) { + throw new errors.NotFoundError( + requestContext.translate(ORGANIZATION_NOT_FOUND_ERROR.MESSAGE), + ORGANIZATION_NOT_FOUND_ERROR.CODE, + ORGANIZATION_NOT_FOUND_ERROR.PARAM + ); + } + + // Checks whether the user is authorized to perform the operation + await adminCheck(context.userId, organization); + + // Creates new category. + const createdCategory = await Category.create({ + category: args.category, + org: args.orgId, + createdBy: context.userId, + updatedBy: context.userId, + }); + + // Returns created category. + return createdCategory.toObject(); +}; diff --git a/src/resolvers/Mutation/index.ts b/src/resolvers/Mutation/index.ts index 9ec94b8aed9..b768bca7e87 100644 --- a/src/resolvers/Mutation/index.ts +++ b/src/resolvers/Mutation/index.ts @@ -32,6 +32,7 @@ import { createAdvertisement } from "./createAdvertisement"; import { createPost } from "./createPost"; import { createSampleOrganization } from "./createSampleOrganization"; import { createTask } from "./createTask"; +import { createCategory } from "./createCategory"; import { createUserTag } from "./createUserTag"; import { deleteDonationById } from "./deleteDonationById"; import { forgotPassword } from "./forgotPassword"; @@ -79,6 +80,7 @@ import { unblockUser } from "./unblockUser"; import { unlikeComment } from "./unlikeComment"; import { unlikePost } from "./unlikePost"; import { unregisterForEventByUser } from "./unregisterForEventByUser"; +import { updateCategory } from "./updateCategory"; import { updateEvent } from "./updateEvent"; import { updateEventProject } from "./updateEventProject"; import { updateLanguage } from "./updateLanguage"; @@ -126,6 +128,7 @@ export const Mutation: MutationResolvers = { createPost, createSampleOrganization, createTask, + createCategory, createUserTag, deleteDonationById, deleteAdvertisementById, @@ -174,6 +177,7 @@ export const Mutation: MutationResolvers = { unlikeComment, unlikePost, unregisterForEventByUser, + updateCategory, updateEvent, updateEventProject, updateLanguage, diff --git a/src/resolvers/Mutation/updateCategory.ts b/src/resolvers/Mutation/updateCategory.ts new file mode 100644 index 00000000000..19a18ac640e --- /dev/null +++ b/src/resolvers/Mutation/updateCategory.ts @@ -0,0 +1,84 @@ +import { + CATEGORY_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + USER_NOT_FOUND_ERROR, +} from "../../constants"; +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { errors, requestContext } from "../../libraries"; +import { User, Category } from "../../models"; +import { Types } from "mongoose"; +/** + * This function enables to update a task. + * @param _parent - parent of current request + * @param args - payload provided with the request + * @param context - context of entire application + * @remarks The following checks are done: + * 1. If the user exists. + * 2. If the category exists. + * 3. If the user is authorized. + * @returns Updated category. + */ +export const updateCategory: MutationResolvers["updateCategory"] = async ( + _parent, + args, + context +) => { + const currentUser = await User.findOne({ + _id: context.userId, + }); + + // Checks if the user exists + if (currentUser === null) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + const category = await Category.findOne({ + _id: args.id, + }).lean(); + + // Checks if the category exists + if (!category) { + throw new errors.NotFoundError( + requestContext.translate(CATEGORY_NOT_FOUND_ERROR.MESSAGE), + CATEGORY_NOT_FOUND_ERROR.CODE, + CATEGORY_NOT_FOUND_ERROR.PARAM + ); + } + + const currentUserIsOrgAdmin = currentUser.adminFor.some( + (ogranizationId) => + ogranizationId === category.org || + Types.ObjectId(ogranizationId).equals(category.org) + ); + + // Checks if the user is authorized for the operation. + if ( + currentUserIsOrgAdmin === false && + currentUser.userType !== "SUPERADMIN" + ) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM + ); + } + + const updatedCategory = await Category.findOneAndUpdate( + { + _id: args.id, + }, + { + ...(args.data as any), + updatedBy: context.userId, + }, + { + new: true, + } + ).lean(); + + return updatedCategory; +}; diff --git a/src/resolvers/Query/categoriesByOrganization.ts b/src/resolvers/Query/categoriesByOrganization.ts new file mode 100644 index 00000000000..775ba7542a0 --- /dev/null +++ b/src/resolvers/Query/categoriesByOrganization.ts @@ -0,0 +1,18 @@ +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import { Category } from "../../models"; +/** + * This query will fetch all categories for the organization from database. + * @param _parent- + * @param args - An object that contains `orderBy` to sort the object as specified and `id` of the Organization. + * @returns An `categories` object that holds all categories with `ACTIVE` status for the Organization. + */ +export const categoriesByOrganization: QueryResolvers["categoriesByOrganization"] = + async (_parent, args) => { + const categories = await Category.find({ + org: args.orgId, + }) + .populate("org") + .lean(); + + return categories; + }; diff --git a/src/resolvers/Query/category.ts b/src/resolvers/Query/category.ts new file mode 100644 index 00000000000..ed4a3809fd1 --- /dev/null +++ b/src/resolvers/Query/category.ts @@ -0,0 +1,29 @@ +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import { Category } from "../../models"; +import { errors } from "../../libraries"; +import { CATEGORY_NOT_FOUND_ERROR } from "../../constants"; +/** + * This query will fetch the category with given id from the database. + * @param _parent- + * @param args - An object that contains `id` of the category that need to be fetched. + * @returns An `category` object. If the `category` object is null then it throws `NotFoundError` error. + * @remarks You can learn about GraphQL `Resolvers` + * {@link https://www.apollographql.com/docs/apollo-server/data/resolvers/ | here}. + */ +export const category: QueryResolvers["category"] = async (_parent, args) => { + const category = await Category.findOne({ + _id: args.id, + }) + .populate("org") + .lean(); + + if (!category) { + throw new errors.NotFoundError( + CATEGORY_NOT_FOUND_ERROR.DESC, + CATEGORY_NOT_FOUND_ERROR.CODE, + CATEGORY_NOT_FOUND_ERROR.PARAM + ); + } + + return category; +}; diff --git a/src/resolvers/Query/index.ts b/src/resolvers/Query/index.ts index 6c4bbd2a2ce..a5ffff5dbb3 100644 --- a/src/resolvers/Query/index.ts +++ b/src/resolvers/Query/index.ts @@ -1,4 +1,6 @@ import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import { category } from "./category"; +import { categoriesByOrganization } from "./categoriesByOrganization"; import { checkAuth } from "./checkAuth"; import { customDataByOrganization } from "./customDataByOrganization"; import { customFieldsByOrganization } from "./customFieldsByOrganization"; @@ -29,6 +31,8 @@ import { getAdvertisements } from "./getAdvertisements"; import { usersConnection } from "./usersConnection"; export const Query: QueryResolvers = { + category, + categoriesByOrganization, checkAuth, customFieldsByOrganization, customDataByOrganization, diff --git a/src/resolvers/index.ts b/src/resolvers/index.ts index 198c139d2a8..625656c7f56 100644 --- a/src/resolvers/index.ts +++ b/src/resolvers/index.ts @@ -1,4 +1,5 @@ import type { Resolvers } from "../types/generatedGraphQLTypes"; +import { Category } from "./Category"; import { CheckIn } from "./CheckIn"; import { Comment } from "./Comment"; import { DirectChat } from "./DirectChat"; @@ -49,6 +50,7 @@ const resolvers: Resolvers = { Query, Subscription, Task, + Category, User, UserTag, diff --git a/src/typeDefs/inputs.ts b/src/typeDefs/inputs.ts index 40763e6904c..c6fb0f7c577 100644 --- a/src/typeDefs/inputs.ts +++ b/src/typeDefs/inputs.ts @@ -307,6 +307,11 @@ export const inputs = gql` completed: Boolean } + input UpdateCategoryInput { + category: String + disabled: Boolean + } + input AddressInput { city: String countryCode: CountryCode diff --git a/src/typeDefs/mutations.ts b/src/typeDefs/mutations.ts index 715ad92de79..7f1b6f3afff 100644 --- a/src/typeDefs/mutations.ts +++ b/src/typeDefs/mutations.ts @@ -105,6 +105,8 @@ export const mutations = gql` createTask(data: TaskInput!, eventProjectId: ID!): Task! @auth + createCategory(category: String!, orgId: ID!): Category! @auth + deleteAdvertisementById(id: ID!): DeletePayload! deleteDonationById(id: ID!): DeletePayload! @@ -231,6 +233,8 @@ export const mutations = gql` updateTask(id: ID!, data: UpdateTaskInput!): Task @auth + updateCategory(id: ID!, data: UpdateCategoryInput!): Category @auth + updateUserProfile(data: UpdateUserInput, file: String): User! @auth updateUserPassword(data: UpdateUserPasswordInput!): User! @auth diff --git a/src/typeDefs/queries.ts b/src/typeDefs/queries.ts index f890e73e223..59414c60652 100644 --- a/src/typeDefs/queries.ts +++ b/src/typeDefs/queries.ts @@ -7,6 +7,10 @@ export const queries = gql` type Query { adminPlugin(orgId: ID!): [Plugin] + category(id: ID!): Category @auth + + categoriesByOrganization(orgId: ID!): [Category] @auth + checkAuth: User! @auth customFieldsByOrganization(id: ID!): [OrganizationCustomField] diff --git a/src/typeDefs/types.ts b/src/typeDefs/types.ts index b8a001493b9..df053be44fc 100644 --- a/src/typeDefs/types.ts +++ b/src/typeDefs/types.ts @@ -16,6 +16,25 @@ export const types = gql` refreshToken: String! } + # Action Item for a Category + type ActionItem { + _id: ID! + assignedTo: User! + assignedBy: User! + category: Category! + preCompletionNotes: String + postCompletionNotes: String + assignmentDate: Date! + dueDate: Date! + completionDate: Date! + completed: Boolean! + eventId: Event + createdBy: User! + updatedBy: User! + createdAt: Date! + updatedAt: Date! + } + # Stores the detail of an check in of an user in an event type CheckIn { _id: ID! @@ -342,6 +361,17 @@ export const types = gql` volunteers: [User] } + type Category { + _id: ID! + category: String! + org: Organization! + disabled: Boolean! + createdBy: User! + updatedBy: User! + createdAt: Date! + updatedAt: Date! + } + type Translation { lang_code: String en_value: String