Skip to content

Commit

Permalink
feat: Add support for Action Items (#1648)
Browse files Browse the repository at this point in the history
* feat: Add support for Categories

* feat: Add support for Action Items

* minor corrections

* Add Organization-Category two way relationship

* Add Event-ActionItem two way relationship

* Add cascade delete functionality on Organization deletion

* Add tests for Categories

* Add tests for Action Items

* fix typos in comments

* Add check for action item assignee being an organization member

* update to more meaningful field names in mongoose schema

* remove the updatedBy field

* remove schema.graphql

* restore schema.graphql to upstream/develop

* update field name and make resolvers nullable

* merge upstream/develop into develop

* update generatedGraphqlTypes

* change Category name to ActionItemCategory

* update field names

* remove redundant relationships from Organization and Event

* making actionItemCategory name unique for a given Organization

* minor correction

* update Action Item inputs

* add actionItemsByOrganization query

* add constant for milliseconds in a week

* restore unwanted changes

* Revert "add constant for milliseconds in a week"

This reverts commit 0476a35.

* add constant for milliseconds in a week
  • Loading branch information
meetulr authored Jan 28, 2024
1 parent 15e8e28 commit de4debc
Show file tree
Hide file tree
Showing 68 changed files with 3,420 additions and 4 deletions.
5 changes: 5 additions & 0 deletions codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ 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",

ActionItemCategory:
"../models/ActionItemCategory#InterfaceActionItemCategory",

CheckIn: "../models/CheckIn#InterfaceCheckIn",

MessageChat: "../models/MessageChat#InterfaceMessageChat",
Expand Down
3 changes: 3 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"user.notFound": "User not found",
"user.alreadyMember": "User is already a member",
"user.profileImage.notFound": "User profile image not found",
"actionItemCategory.notFound": "Action Item Category not found",
"actionItemCategory.alreadyExists": "Action Item Category already exists",
"actionItem.notFound": "Action Item not found",
"advertisement.notFound": "Advertisement not found",
"event.notFound": "Event not found",
"organization.notFound": "Organization not found",
Expand Down
3 changes: 3 additions & 0 deletions locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"user.notFound": "Utilisateur introuvable",
"user.alreadyMember": "L'utilisateur est déjà membre",
"user.profileImage.notFound": "Image du profil utilisateur introuvable",
"actionItemCategory.notFound": "Catégorie d’élément d’action introuvable",
"actionItemCategory.alreadyExists": "La catégorie d’élément d’action existe déjà",
"actionItem.notFound": "Élément d\\’action non trouvé",
"event.notFound": "Événement non trouvé",
"organization.notFound": "Organisation introuvable",
"organization.profileImage.notFound": "Image du profil de l'organisation introuvable",
Expand Down
3 changes: 3 additions & 0 deletions locales/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"user.notFound": "उपयोगकर्ता नहीं मिला",
"user.alreadyMember": "उपयोगकर्ता पहले से ही एक सदस्य है",
"user.profileImage.notFound": "उपयोगकर्ता प्रोफ़ाइल छवि नहीं मिली",
"actionItemCategory.notFound": "श्रेणी नहीं मिली",
"actionItemCategory.alreadyExists": "यह श्रेणी पहले से मौजूद है",
"actionItem.notFound": "कार्रवाई का मद नहीं मिला",
"advertisement.notFound": "विज्ञापन नहीं मिला",
"event.notFound": "घटना नहीं मिली",
"organization.notFound": "संगठन नहीं मिला",
Expand Down
3 changes: 3 additions & 0 deletions locales/sp.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"user.notFound": "Usuario no encontrado",
"user.alreadyMember": "El usuario ya es miembro",
"user.profileImage.notFound": "No se encontró la imagen de perfil de usuario",
"actionItemCategory.notFound": "No se encontró la categoría de elemento de acción",
"actionItemCategory.alreadyExists": "Ya existe una categoría de elemento de acción",
"actionItem.notFound": "Elemento de acción no encontrado",
"event.notFound": "Evento no encontrado",
"organization.notFound": "Organización no encontrada",
"organization.profileImage.notFound": "No se encontró la imagen del perfil de la organización",
Expand Down
3 changes: 3 additions & 0 deletions locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"user.notFound": "找不到用戶",
"user.alreadyMember": "用戶已經是會員",
"user.profileImage.notFound": "未找到用戶個人資料圖像",
"actionItemCategory.notFound": "未找到措施项类别",
"actionItemCategory.alreadyExists": "措施项类别已存在",
"actionItem.notFound": "找不到操作项",
"event.notFound": "未找到事件",
"organization.notFound": "未找到組織",
"organization.profileImage.notFound": "未找到組織檔案圖像",
Expand Down
23 changes: 23 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ if (!issues) {
ENV = envSchema.parse(process.env);
}

export const ACTION_ITEM_NOT_FOUND_ERROR = {
DESC: "ActionItem not found",
CODE: "actionItem.notFound",
MESSAGE: "actionItem.notFound",
PARAM: "actionItem",
};

export const ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR = {
DESC: "ActionItemCategory not found",
CODE: "actionItemCategory.notFound",
MESSAGE: "actionItemCategory.notFound",
PARAM: "actionItemCategory",
};

export const ACTION_ITEM_CATEGORY_ALREADY_EXISTS = {
DESC: "Action Item Category already exists",
CODE: "actionItemCategory.alreadyExists",
MESSAGE: "actionItemCategory.alreadyExists",
PARAM: "actionItemCategory",
};

export const CHAT_NOT_FOUND_ERROR = {
DESC: "Chat not found",
CODE: "chat.notFound",
Expand Down Expand Up @@ -486,6 +507,8 @@ export const REDIS_HOST = process.env.REDIS_HOST || "";
export const REDIS_PORT = Number(process.env.REDIS_PORT);
export const REDIS_PASSWORD = process.env.REDIS_PASSWORD;

export const MILLISECONDS_IN_A_WEEK = 7 * 24 * 60 * 60 * 1000;

export const key = ENV.ENCRYPTION_KEY as string;
export const iv = crypto.randomBytes(16).toString("hex");

Expand Down
107 changes: 107 additions & 0 deletions src/models/ActionItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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 { InterfaceActionItemCategory } from "./ActionItemCategory";
import { MILLISECONDS_IN_A_WEEK } from "../constants";

/**
* This is an interface that represents a database(MongoDB) document for ActionItem.
*/

export interface InterfaceActionItem {
_id: Types.ObjectId;
assigneeId: PopulatedDoc<InterfaceUser & Document>;
assignerId: PopulatedDoc<InterfaceUser & Document>;
actionItemCategoryId: PopulatedDoc<InterfaceActionItemCategory & Document>;
preCompletionNotes: string;
postCompletionNotes: string;
assignmentDate: Date;
dueDate: Date;
completionDate: Date;
isCompleted: boolean;
eventId: PopulatedDoc<InterfaceEvent & Document>;
creatorId: PopulatedDoc<InterfaceUser & Document>;
createdAt: Date;
updatedAt: Date;
}

/**
* This describes the schema for a `ActionItem` that corresponds to `InterfaceActionItem` document.
* @param assigneeId - User to whom the ActionItem is assigned, refer to `User` model.
* @param assignerId - User who assigned the ActionItem, refer to the `User` model.
* @param actionItemCategoryId - ActionItemCategory to which the ActionItem is related, refer to the `ActionItemCategory` 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 isCompleted - Whether the ActionItem has been completed.
* @param eventId - Event to which the ActionItem is related, refer to the `Event` model.
* @param creatorId - User who created 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(
{
assigneeId: {
type: Schema.Types.ObjectId,
ref: "User",
required: true,
},
assignerId: {
type: Schema.Types.ObjectId,
ref: "User",
required: true,
},
actionItemCategoryId: {
type: Schema.Types.ObjectId,
ref: "ActionItemCategory",
required: true,
},
preCompletionNotes: {
type: String,
},
postCompletionNotes: {
type: String,
},
assignmentDate: {
type: Date,
required: true,
default: Date.now(),
},
dueDate: {
type: Date,
required: true,
default: Date.now() + MILLISECONDS_IN_A_WEEK,
},
completionDate: {
type: Date,
required: true,
default: Date.now() + MILLISECONDS_IN_A_WEEK,
},
isCompleted: {
type: Boolean,
required: true,
default: false,
},
eventId: {
type: Schema.Types.ObjectId,
ref: "Event",
},
creatorId: {
type: Schema.Types.ObjectId,
ref: "User",
required: true,
},
},
{ timestamps: true }
);

const actionItemModel = (): Model<InterfaceActionItem> =>
model<InterfaceActionItem>("ActionItem", actionItemSchema);

// This syntax is needed to prevent Mongoose OverwriteModelError while running tests.
export const ActionItem = (models.ActionItem ||
actionItemModel()) as ReturnType<typeof actionItemModel>;
68 changes: 68 additions & 0 deletions src/models/ActionItemCategory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 ActionItemCategory.
*/

export interface InterfaceActionItemCategory {
_id: Types.ObjectId;
name: string;
organizationId: PopulatedDoc<InterfaceOrganization & Document>;
isDisabled: boolean;
creatorId: PopulatedDoc<InterfaceUser & Document>;
createdAt: Date;
updatedAt: Date;
}

/**
* This describes the schema for a `actionItemCategory` that corresponds to `InterfaceCategory` document.
* @param name - An actionItemCategory to be selected for ActionItems.
* @param organizationId - Organization the actionItemCategory belongs to, refer to the `Organization` model.
* @param isDisabled - Whether actionItemCategory is disabled or not.
* @param creatorId - Task creator, refer to `User` model.
* @param createdAt - Time stamp of data creation.
* @param updatedAt - Time stamp of data updation.
*/

const actionItemCategorySchema = new Schema(
{
name: {
type: String,
required: true,
},
organizationId: {
type: Schema.Types.ObjectId,
ref: "Organization",
required: true,
},
isDisabled: {
type: Boolean,
required: true,
default: false,
},
creatorId: {
type: Schema.Types.ObjectId,
ref: "User",
required: true,
},
},
{ timestamps: true }
);

actionItemCategorySchema.index(
{ organizationId: 1, name: 1 },
{ unique: true }
);

const actionItemCategoryModel = (): Model<InterfaceActionItemCategory> =>
model<InterfaceActionItemCategory>(
"ActionItemCategory",
actionItemCategorySchema
);

// This syntax is needed to prevent Mongoose OverwriteModelError while running tests.
export const ActionItemCategory = (models.ActionItemCategory ||
actionItemCategoryModel()) as ReturnType<typeof actionItemCategoryModel>;
2 changes: 2 additions & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from "./ActionItem";
export * from "./Advertisement";
export * from "./ActionItemCategory";
export * from "./CheckIn";
export * from "./MessageChat";
export * from "./Comment";
Expand Down
9 changes: 9 additions & 0 deletions src/resolvers/ActionItem/actionItemCategory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ActionItemResolvers } from "../../types/generatedGraphQLTypes";
import { ActionItemCategory } from "../../models";

export const actionItemCategory: ActionItemResolvers["actionItemCategory"] =
async (parent) => {
return ActionItemCategory.findOne({
_id: parent.actionItemCategoryId,
}).lean();
};
8 changes: 8 additions & 0 deletions src/resolvers/ActionItem/assignee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ActionItemResolvers } from "../../types/generatedGraphQLTypes";
import { User } from "../../models";

export const assignee: ActionItemResolvers["assignee"] = async (parent) => {
return User.findOne({
_id: parent.assigneeId,
}).lean();
};
8 changes: 8 additions & 0 deletions src/resolvers/ActionItem/assigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ActionItemResolvers } from "../../types/generatedGraphQLTypes";
import { User } from "../../models";

export const assigner: ActionItemResolvers["assigner"] = async (parent) => {
return User.findOne({
_id: parent.assignerId,
}).lean();
};
8 changes: 8 additions & 0 deletions src/resolvers/ActionItem/creator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ActionItemResolvers } from "../../types/generatedGraphQLTypes";
import { User } from "../../models";

export const creator: ActionItemResolvers["creator"] = async (parent) => {
return User.findOne({
_id: parent.creatorId,
}).lean();
};
8 changes: 8 additions & 0 deletions src/resolvers/ActionItem/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ActionItemResolvers } from "../../types/generatedGraphQLTypes";
import { Event } from "../../models";

export const event: ActionItemResolvers["event"] = async (parent) => {
return Event.findOne({
_id: parent.eventId,
}).lean();
};
14 changes: 14 additions & 0 deletions src/resolvers/ActionItem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ActionItemResolvers } from "../../types/generatedGraphQLTypes";
import { assignee } from "./assignee";
import { assigner } from "./assigner";
import { actionItemCategory } from "./actionItemCategory";
import { event } from "./event";
import { creator } from "./creator";

export const ActionItem: ActionItemResolvers = {
assignee,
assigner,
actionItemCategory,
event,
creator,
};
10 changes: 10 additions & 0 deletions src/resolvers/ActionItemCategory/creator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ActionItemCategoryResolvers } from "../../types/generatedGraphQLTypes";
import { User } from "../../models";

export const creator: ActionItemCategoryResolvers["creator"] = async (
parent
) => {
return User.findOne({
_id: parent.creatorId,
}).lean();
};
8 changes: 8 additions & 0 deletions src/resolvers/ActionItemCategory/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ActionItemCategoryResolvers } from "../../types/generatedGraphQLTypes";
import { organization } from "./organization";
import { creator } from "./creator";

export const ActionItemCategory: ActionItemCategoryResolvers = {
organization,
creator,
};
10 changes: 10 additions & 0 deletions src/resolvers/ActionItemCategory/organization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ActionItemCategoryResolvers } from "../../types/generatedGraphQLTypes";
import { Organization } from "../../models";

export const organization: ActionItemCategoryResolvers["organization"] = async (
parent
) => {
return Organization.findOne({
_id: parent.organizationId,
}).lean();
};
12 changes: 12 additions & 0 deletions src/resolvers/Event/actionItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ActionItem } from "../../models";
import type { EventResolvers } from "../../types/generatedGraphQLTypes";
/**
* This resolver function will fetch and return the action items related to the event from database.
* @param parent - An object that is the return value of the resolver for this field's parent.
* @returns An object that contains the list of all action items related to the event.
*/
export const actionItems: EventResolvers["actionItems"] = async (parent) => {
return await ActionItem.find({
eventId: parent._id,
}).lean();
};
2 changes: 2 additions & 0 deletions src/resolvers/Event/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { attendeesCheckInStatus } from "./attendeesCheckInStatus";
import { averageFeedbackScore } from "./averageFeedbackScore";
import { feedback } from "./feedback";
import { organization } from "./organization";
import { actionItems } from "./actionItems";
import { creator } from "./creator";

export const Event: EventResolvers = {
actionItems,
attendees,
attendeesCheckInStatus,
averageFeedbackScore,
Expand Down
Loading

0 comments on commit de4debc

Please sign in to comment.