in-app-notification.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import {
  2. Types, Document, Model, Schema /* , Query */, model,
  3. } from 'mongoose';
  4. import { subDays } from 'date-fns';
  5. import ActivityDefine from '../util/activityDefine';
  6. import loggerFactory from '~/utils/logger';
  7. import Crowi from '../crowi';
  8. import { ActivityDocument } from './activity';
  9. import User = require('./user');
  10. const logger = loggerFactory('growi:models:inAppNotification');
  11. const STATUS_UNREAD = 'UNREAD';
  12. const STATUS_UNOPENED = 'UNOPENED';
  13. const STATUS_OPENED = 'OPENED';
  14. const STATUSES = [STATUS_UNREAD, STATUS_UNOPENED, STATUS_OPENED];
  15. export interface InAppNotificationDocument extends Document {
  16. _id: Types.ObjectId
  17. user: Types.ObjectId
  18. targetModel: string
  19. target: Types.ObjectId
  20. action: string
  21. activities: Types.ObjectId[]
  22. status: string
  23. createdAt: Date
  24. }
  25. export interface InAppNotificationModel extends Model<InAppNotificationDocument> {
  26. findLatestInAppNotificationsByUser(user: Types.ObjectId, skip: number, offset: number): Promise<InAppNotificationDocument[]>
  27. upsertByActivity(user: Types.ObjectId, activity: ActivityDocument, createdAt?: Date | null): Promise<InAppNotificationDocument | null>
  28. removeActivity(activity: any): any
  29. // commented out type 'Query' temporary to avoid ts error
  30. removeEmpty()/* : Query<any> */
  31. read(user: typeof User) /* : Promise<Query<any>> */
  32. open(user: typeof User, id: Types.ObjectId): Promise<InAppNotificationDocument | null>
  33. getUnreadCountByUser(user: Types.ObjectId): Promise<number | undefined>
  34. STATUS_UNREAD: string
  35. STATUS_UNOPENED: string
  36. STATUS_OPENED: string
  37. }
  38. export default (crowi: Crowi) => {
  39. const inAppCommentEvent = crowi.event('inAppNotification');
  40. const inAppNotificationSchema = new Schema<InAppNotificationDocument, InAppNotificationModel>({
  41. user: {
  42. type: Schema.Types.ObjectId,
  43. ref: 'User',
  44. index: true,
  45. require: true,
  46. },
  47. targetModel: {
  48. type: String,
  49. require: true,
  50. enum: ActivityDefine.getSupportTargetModelNames(),
  51. },
  52. target: {
  53. type: Schema.Types.ObjectId,
  54. refPath: 'targetModel',
  55. require: true,
  56. },
  57. action: {
  58. type: String,
  59. require: true,
  60. enum: ActivityDefine.getSupportActionNames(),
  61. },
  62. activities: [
  63. {
  64. type: Schema.Types.ObjectId,
  65. ref: 'Activity',
  66. },
  67. ],
  68. status: {
  69. type: String,
  70. default: STATUS_UNREAD,
  71. enum: STATUSES,
  72. index: true,
  73. require: true,
  74. },
  75. createdAt: {
  76. type: Date,
  77. default: Date.now,
  78. },
  79. });
  80. inAppNotificationSchema.virtual('actionUsers').get(function(this: InAppNotificationDocument) {
  81. const Activity = crowi.model('Activity');
  82. return Activity.getActionUsersFromActivities((this.activities as any) as ActivityDocument[]);
  83. });
  84. const transform = (doc, ret) => {
  85. // delete ret.activities
  86. };
  87. inAppNotificationSchema.set('toObject', { virtuals: true, transform });
  88. inAppNotificationSchema.set('toJSON', { virtuals: true, transform });
  89. inAppNotificationSchema.index({
  90. user: 1, target: 1, action: 1, createdAt: 1,
  91. });
  92. inAppNotificationSchema.statics.findLatestInAppNotificationsByUser = function(user, limitNum, offset) {
  93. const limit = limitNum || 10;
  94. return InAppNotification.find({ user })
  95. .sort({ createdAt: -1 })
  96. .skip(offset)
  97. .limit(limit)
  98. .populate(['user', 'target'])
  99. .populate({ path: 'activities', populate: { path: 'user' } })
  100. .exec();
  101. };
  102. inAppNotificationSchema.statics.upsertByActivity = async function(user, activity, createdAt = null) {
  103. const {
  104. _id: activityId, targetModel, target, action,
  105. } = activity;
  106. const now = createdAt || Date.now();
  107. const lastWeek = subDays(now, 7);
  108. const query = {
  109. user, target, action, createdAt: { $gt: lastWeek },
  110. };
  111. const parameters = {
  112. user,
  113. targetModel,
  114. target,
  115. action,
  116. status: STATUS_UNREAD,
  117. createdAt: now,
  118. $addToSet: { activities: activityId },
  119. };
  120. const options = {
  121. upsert: true,
  122. new: true,
  123. setDefaultsOnInsert: true,
  124. runValidators: true,
  125. };
  126. const inAppNotification = await InAppNotification.findOneAndUpdate(query, parameters, options);
  127. if (inAppNotification) {
  128. inAppCommentEvent.emit('update', inAppNotification.user);
  129. }
  130. return inAppNotification;
  131. };
  132. inAppNotificationSchema.statics.removeActivity = async function(activity) {
  133. const { _id, target, action } = activity;
  134. const query = { target, action };
  135. const parameters = { $pull: { activities: _id } };
  136. const result = await InAppNotification.updateMany(query, parameters);
  137. await InAppNotification.removeEmpty();
  138. return result;
  139. };
  140. inAppNotificationSchema.statics.removeEmpty = function() {
  141. return InAppNotification.deleteMany({ activities: { $size: 0 } });
  142. };
  143. inAppNotificationSchema.statics.read = async function(user) {
  144. const query = { user, status: STATUS_UNREAD };
  145. const parameters = { status: STATUS_UNOPENED };
  146. return InAppNotification.updateMany(query, parameters);
  147. };
  148. inAppNotificationSchema.statics.open = async function(user, id) {
  149. const query = { _id: id, user: user._id };
  150. const parameters = { status: STATUS_OPENED };
  151. const options = { new: true };
  152. const inAppNotification = await InAppNotification.findOneAndUpdate(query, parameters, options);
  153. if (inAppNotification) {
  154. inAppCommentEvent.emit('update', inAppNotification.user);
  155. }
  156. return inAppNotification;
  157. };
  158. inAppNotificationSchema.statics.getUnreadCountByUser = async function(user) {
  159. const query = { user, status: STATUS_UNREAD };
  160. try {
  161. const count = await InAppNotification.countDocuments(query);
  162. return count;
  163. }
  164. catch (err) {
  165. logger.error('Error on getUnreadCountByUser', err);
  166. throw err;
  167. }
  168. };
  169. inAppNotificationSchema.statics.STATUS_UNOPENED = function() {
  170. return STATUS_UNOPENED;
  171. };
  172. inAppNotificationSchema.statics.STATUS_UNREAD = function() {
  173. return STATUS_UNREAD;
  174. };
  175. inAppNotificationSchema.statics.STATUS_OPENED = function() {
  176. return STATUS_OPENED;
  177. };
  178. const InAppNotification = model<InAppNotificationDocument, InAppNotificationModel>('InAppNotification', inAppNotificationSchema);
  179. return InAppNotification;
  180. };