@messenger-box/platform-server 10.0.3-alpha.7 → 10.0.3-alpha.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/lib/containers/containers.js +4 -1
  2. package/lib/containers/containers.js.map +1 -1
  3. package/lib/containers/context-services-from-container.d.ts +1 -1
  4. package/lib/containers/context-services-from-container.js +1 -1
  5. package/lib/containers/context-services-from-container.js.map +1 -1
  6. package/lib/graphql/resolvers/channel-member.d.ts +3 -2
  7. package/lib/graphql/resolvers/channel-member.js +30 -5
  8. package/lib/graphql/resolvers/channel-member.js.map +1 -1
  9. package/lib/graphql/resolvers/channel.d.ts +3 -2
  10. package/lib/graphql/resolvers/channel.js +279 -53
  11. package/lib/graphql/resolvers/channel.js.map +1 -1
  12. package/lib/graphql/resolvers/extended-token-account.d.ts +3 -2
  13. package/lib/graphql/resolvers/extended-token-account.js +90 -23
  14. package/lib/graphql/resolvers/extended-token-account.js.map +1 -1
  15. package/lib/graphql/resolvers/index.d.ts +1 -1
  16. package/lib/graphql/resolvers/post-thread.d.ts +1 -1
  17. package/lib/graphql/resolvers/post-thread.js +294 -132
  18. package/lib/graphql/resolvers/post-thread.js.map +1 -1
  19. package/lib/graphql/resolvers/post.d.ts +2 -3
  20. package/lib/graphql/resolvers/post.js +696 -234
  21. package/lib/graphql/resolvers/post.js.map +1 -1
  22. package/lib/graphql/resolvers/reaction.d.ts +3 -2
  23. package/lib/graphql/resolvers/reaction.js +96 -14
  24. package/lib/graphql/resolvers/reaction.js.map +1 -1
  25. package/lib/graphql/schema/channel-member.graphql +110 -21
  26. package/lib/graphql/schema/channel-member.graphql.js +1 -1
  27. package/lib/graphql/schema/channel.graphql +337 -38
  28. package/lib/graphql/schema/channel.graphql.js +1 -1
  29. package/lib/graphql/schema/post-thread.graphql +167 -21
  30. package/lib/graphql/schema/post-thread.graphql.js +1 -1
  31. package/lib/graphql/schema/post.graphql +284 -40
  32. package/lib/graphql/schema/post.graphql.js +1 -1
  33. package/lib/graphql/schema/reaction.graphql +71 -13
  34. package/lib/graphql/schema/reaction.graphql.js +1 -1
  35. package/lib/graphql/schema/services.graphql +2 -0
  36. package/lib/graphql/schema/users.graphql +76 -13
  37. package/lib/graphql/schema/users.graphql.js +1 -1
  38. package/lib/index.js +1 -1
  39. package/lib/index.js.map +1 -1
  40. package/lib/interfaces/index.d.ts +0 -1
  41. package/lib/interfaces/services.d.ts +1 -1
  42. package/lib/migrations/dbMigrations/AddPostsConfigurationsMigration.d.ts +17 -0
  43. package/lib/migrations/dbMigrations/AddPostsConfigurationsMigration.js +44 -0
  44. package/lib/migrations/dbMigrations/AddPostsConfigurationsMigration.js.map +1 -0
  45. package/lib/migrations/dbMigrations/index.d.ts +1 -0
  46. package/lib/migrations/index.d.ts +1 -0
  47. package/lib/migrations/mail-template-migration.js +1 -1
  48. package/lib/migrations/message-notification-template-migration.d.ts +1 -1
  49. package/lib/migrations/message-notification-template-migration.js +1 -1
  50. package/lib/plugins/channel-moleculer-service.d.ts +21 -1
  51. package/lib/plugins/channel-moleculer-service.js +417 -115
  52. package/lib/plugins/channel-moleculer-service.js.map +1 -1
  53. package/lib/plugins/extended-token-account-moleculer-service.d.ts +25 -1
  54. package/lib/plugins/extended-token-account-moleculer-service.js +348 -22
  55. package/lib/plugins/extended-token-account-moleculer-service.js.map +1 -1
  56. package/lib/plugins/messenger-notification-moleculer-service.d.ts +27 -3
  57. package/lib/plugins/messenger-notification-moleculer-service.js +404 -58
  58. package/lib/plugins/messenger-notification-moleculer-service.js.map +1 -1
  59. package/lib/plugins/post-moleculer-service.d.ts +85 -21
  60. package/lib/plugins/post-moleculer-service.js +986 -256
  61. package/lib/plugins/post-moleculer-service.js.map +1 -1
  62. package/lib/plugins/post-thread-moleculer-service.d.ts +33 -1
  63. package/lib/plugins/post-thread-moleculer-service.js +326 -8
  64. package/lib/plugins/post-thread-moleculer-service.js.map +1 -1
  65. package/lib/plugins/reaction-moleculer-service.js +1 -1
  66. package/lib/plugins/reaction-moleculer-service.js.map +1 -1
  67. package/lib/preferences/settings/post-settings.d.ts +2 -0
  68. package/lib/preferences/settings/post-settings.js +47 -9
  69. package/lib/preferences/settings/post-settings.js.map +1 -1
  70. package/lib/services/channel-service.d.ts +179 -33
  71. package/lib/services/channel-service.js +821 -274
  72. package/lib/services/channel-service.js.map +1 -1
  73. package/lib/services/extended-token-account-service.d.ts +130 -14
  74. package/lib/services/extended-token-account-service.js +462 -52
  75. package/lib/services/extended-token-account-service.js.map +1 -1
  76. package/lib/services/index.d.ts +1 -0
  77. package/lib/services/messenger-notification-service.d.ts +106 -13
  78. package/lib/services/messenger-notification-service.js +824 -442
  79. package/lib/services/messenger-notification-service.js.map +1 -1
  80. package/lib/services/post-service.d.ts +182 -16
  81. package/lib/services/post-service.js +731 -115
  82. package/lib/services/post-service.js.map +1 -1
  83. package/lib/services/post-thread-service.d.ts +114 -5
  84. package/lib/services/post-thread-service.js +400 -13
  85. package/lib/services/post-thread-service.js.map +1 -1
  86. package/lib/services/proxy-services/channel-microservice.d.ts +5 -3
  87. package/lib/services/proxy-services/channel-microservice.js +19 -10
  88. package/lib/services/proxy-services/channel-microservice.js.map +1 -1
  89. package/lib/services/proxy-services/messenger-notification-microservice.d.ts +128 -8
  90. package/lib/services/proxy-services/messenger-notification-microservice.js +324 -29
  91. package/lib/services/proxy-services/messenger-notification-microservice.js.map +1 -1
  92. package/lib/services/proxy-services/post-microservice.d.ts +186 -12
  93. package/lib/services/proxy-services/post-microservice.js +543 -54
  94. package/lib/services/proxy-services/post-microservice.js.map +1 -1
  95. package/lib/services/proxy-services/post-thread-microservice.d.ts +134 -3
  96. package/lib/services/proxy-services/post-thread-microservice.js +388 -6
  97. package/lib/services/proxy-services/post-thread-microservice.js.map +1 -1
  98. package/lib/services/proxy-services/reaction-microservice.d.ts +161 -3
  99. package/lib/services/proxy-services/reaction-microservice.js +474 -2
  100. package/lib/services/proxy-services/reaction-microservice.js.map +1 -1
  101. package/lib/services/reaction-service.d.ts +124 -4
  102. package/lib/services/reaction-service.js +415 -3
  103. package/lib/services/reaction-service.js.map +1 -1
  104. package/lib/services/redis-cache-manager.d.ts +18 -0
  105. package/lib/services/redis-cache-manager.js +83 -0
  106. package/lib/services/redis-cache-manager.js.map +1 -0
  107. package/lib/store/models/account-token-store.d.ts +1 -1
  108. package/lib/store/models/account-token-store.js.map +1 -1
  109. package/lib/store/models/channel.d.ts +2 -3
  110. package/lib/store/models/channel.js +181 -72
  111. package/lib/store/models/channel.js.map +1 -1
  112. package/lib/store/models/post-thread.d.ts +3 -3
  113. package/lib/store/models/post-thread.js +96 -14
  114. package/lib/store/models/post-thread.js.map +1 -1
  115. package/lib/store/models/post.d.ts +2 -3
  116. package/lib/store/models/post.js +143 -23
  117. package/lib/store/models/post.js.map +1 -1
  118. package/lib/store/models/reaction.d.ts +2 -3
  119. package/lib/store/models/reaction.js +67 -8
  120. package/lib/store/models/reaction.js.map +1 -1
  121. package/lib/store/repositories/__tests__/__fixtures__/team-repository.d.ts +3 -3
  122. package/lib/store/repositories/channel-repository.d.ts +6 -6
  123. package/lib/store/repositories/channel-repository.js +5 -2
  124. package/lib/store/repositories/channel-repository.js.map +1 -1
  125. package/lib/store/repositories/post-repository.d.ts +6 -6
  126. package/lib/store/repositories/post-repository.js +5 -2
  127. package/lib/store/repositories/post-repository.js.map +1 -1
  128. package/lib/store/repositories/post-thread-repository.d.ts +6 -6
  129. package/lib/store/repositories/post-thread-repository.js +5 -2
  130. package/lib/store/repositories/post-thread-repository.js.map +1 -1
  131. package/lib/store/repositories/reaction-repository.d.ts +6 -6
  132. package/lib/store/repositories/reaction-repository.js +5 -2
  133. package/lib/store/repositories/reaction-repository.js.map +1 -1
  134. package/lib/templates/constants/SERVER_TYPES.ts.template +0 -3
  135. package/lib/templates/repositories/ChannelRepository.ts.template +3 -3
  136. package/lib/templates/repositories/PostRepository.ts.template +3 -3
  137. package/lib/templates/repositories/PostThreadRepository.ts.template +3 -3
  138. package/lib/templates/repositories/ReactionRepository.ts.template +3 -4
  139. package/lib/templates/services/ChannelService.ts.template +280 -39
  140. package/lib/templates/services/ExtendedTokenAccountService.ts.template +104 -9
  141. package/lib/templates/services/MessengerNotificationService.ts.template +94 -19
  142. package/lib/templates/services/PostService.ts.template +184 -20
  143. package/lib/templates/services/PostThreadService.ts.template +151 -6
  144. package/lib/templates/services/ReactionService.ts.template +129 -3
  145. package/lib/templates/services/RedisCacheManager.ts.template +22 -0
  146. package/package.json +6 -5
  147. package/lib/interfaces/context.d.ts +0 -14
  148. package/lib/store/models/common-options.js +0 -20
  149. package/lib/store/models/common-options.js.map +0 -1
@@ -1,55 +1,224 @@
1
- import {__decorate,__param,__metadata}from'tslib';import'@cdm-logger/core';import {inject}from'inversify';import {set}from'lodash-es';import {SERVER_TYPES,FileRefType}from'common';import {BaseService}from'@common-stack/store-mongo';import {PubSubEngine}from'graphql-subscriptions';import {CommonType}from'@common-stack/core';import {ServiceBroker}from'moleculer';import {ChannelRepository}from'../store/repositories/channel-repository.js';import {PostRepository}from'../store/repositories/post-repository.js';import'../store/repositories/post-thread-repository.js';import'../store/repositories/reaction-repository.js';import {PostTypes}from'../preferences/settings/post-settings.js';import'../preferences/permissions/inbox-permission-contribution.js';import'../preferences/permissions/inbox-roles-permission-overwrite.js';import {config}from'../config/env-config.js';var PostService_1;
2
- let PostService = PostService_1 = class PostService extends BaseService {
1
+ import {__decorate,__param,__metadata}from'tslib';import'@cdm-logger/core';import {inject}from'inversify';import {set}from'lodash-es';import {SERVER_TYPES,FileRefType}from'common/server';import {BaseService2}from'@common-stack/store-mongo';import {PubSubEngine}from'graphql-subscriptions';import {CommonType}from'@common-stack/core';import {ServiceBroker}from'moleculer';import {DisposableCollection}from'@adminide-stack/core';import {ChannelRepository}from'../store/repositories/channel-repository.js';import {PostRepository}from'../store/repositories/post-repository.js';import'../store/repositories/post-thread-repository.js';import'../store/repositories/reaction-repository.js';import {PostTypes}from'../preferences/settings/post-settings.js';import'../preferences/permissions/inbox-permission-contribution.js';import'../preferences/permissions/inbox-roles-permission-overwrite.js';import {config}from'../config/env-config.js';var PostService_1;
2
+ /**
3
+ * Post Service Implementation
4
+ *
5
+ * This service handles comprehensive post management within the messenger platform,
6
+ * providing operations for creating, updating, managing posts and their associated
7
+ * content including file attachments, notifications, threading, and real-time
8
+ * message delivery across channels and organizations.
9
+ *
10
+ * Key capabilities:
11
+ * - Post lifecycle management (creation, updates, deletion)
12
+ * - File attachment handling and upload management
13
+ * - Real-time message delivery and notifications
14
+ * - Post threading and conversation management
15
+ * - Message status tracking (read, delivered)
16
+ * - Channel integration and message counting
17
+ * - Expo push notification integration
18
+ * - File upload link generation and management
19
+ */
20
+ // @ts-ignore - Type compatibility issue between BaseService2 and IPostService
21
+ let PostService = class PostService extends BaseService2 {
22
+ static {
23
+ PostService_1 = this;
24
+ }
3
25
  channelRepo;
4
26
  fileInfoService;
5
27
  messengerNotificationService;
6
28
  broker;
7
29
  pubsub;
30
+ redisCacheManager;
31
+ redisClient;
32
+ toDispose = new DisposableCollection();
8
33
  logger;
9
- constructor(repository, channelRepo, fileInfoService, messengerNotificationService, broker, pubsub, logger) {
34
+ // Redis cache configuration for posts
35
+ static CACHE_TTL = {
36
+ maxAge: 5 * 60,
37
+ scope: 'PUBLIC'
38
+ };
39
+ constructor(repository, channelRepo, fileInfoService, messengerNotificationService, broker, pubsub, redisCacheManager, redisClient, logger) {
10
40
  super(repository);
11
41
  this.channelRepo = channelRepo;
12
42
  this.fileInfoService = fileInfoService;
13
43
  this.messengerNotificationService = messengerNotificationService;
14
44
  this.broker = broker;
15
45
  this.pubsub = pubsub;
46
+ this.redisCacheManager = redisCacheManager;
47
+ this.redisClient = redisClient;
16
48
  this.logger = logger?.child({
17
49
  className: PostService_1.name
18
50
  });
19
51
  }
20
- createFileUploadLink(postId, filename, userId) {
21
- return this.fileInfoService.uploadByUrl({
22
- filename,
23
- refType: FileRefType.Post,
24
- ref: postId,
25
- userId
26
- });
52
+ /**
53
+ * Disposes of resources used by the service
54
+ */
55
+ dispose() {
56
+ this.toDispose.dispose();
57
+ }
58
+ // Override base service methods to handle errors internally
59
+ async create(data) {
60
+ try {
61
+ this.logger.debug('Creating post', {
62
+ data
63
+ });
64
+ // Validate required fields
65
+ if (!data) {
66
+ throw new Error('Post data is required');
67
+ }
68
+ if (!data.channel) {
69
+ throw new Error('Channel is required for post creation');
70
+ }
71
+ if (!data.author) {
72
+ throw new Error('Author is required for post creation');
73
+ }
74
+ // Add timestamps and default type
75
+ const postData = {
76
+ ...data,
77
+ type: data.type || 'Simple',
78
+ // Default to Simple type for regular messages
79
+ createdAt: new Date(),
80
+ updatedAt: new Date()
81
+ };
82
+ // Create the post using repository
83
+ // const result = await this.repository.create(postData);
84
+ const result = await super.create(postData);
85
+ if (!result) {
86
+ throw new Error('Failed to create post');
87
+ }
88
+ // Update channel statistics
89
+ try {
90
+ await this.channelRepo.model.updateOne({
91
+ _id: data.channel
92
+ }, {
93
+ lastPostAt: result.createdAt,
94
+ $inc: {
95
+ totalMsgCount: 1,
96
+ totalMsgCountRoot: 1,
97
+ 'members.$[elem].msgCountRoot': 1,
98
+ 'members.$[elem].msgCount': 1
99
+ }
100
+ }, {
101
+ arrayFilters: [{
102
+ 'elem.user': data.author
103
+ }]
104
+ });
105
+ } catch (channelError) {
106
+ this.logger.error('Error updating channel stats:', channelError);
107
+ // Don't fail the post creation if stats update fails
108
+ }
109
+ await this.pubsub.publish(`POST_CREATED.${data.channel}`, result);
110
+ await this.sendExpoNotification(result);
111
+ // Invalidate caches related to this channel
112
+ await this.invalidatePostCachesByChannel(String(data.channel));
113
+ return result;
114
+ } catch (error) {
115
+ this.logger.error('Error creating post:', error);
116
+ throw error;
117
+ }
118
+ }
119
+ async update(id, data) {
120
+ try {
121
+ const result = await super.update(id, data);
122
+ if (!result || typeof result === 'object' && 'message' in result) {
123
+ this.logger.error('Failed to update post', {
124
+ error: result
125
+ });
126
+ throw new Error('Failed to update post');
127
+ }
128
+ try {
129
+ // Invalidate caches related to this post's channel
130
+ const channelId = result?.channel?.toString?.() || data?.channel;
131
+ if (channelId) {
132
+ await this.invalidatePostCachesByChannel(String(channelId));
133
+ }
134
+ } catch (cacheError) {
135
+ this.logger.warn('Cache invalidation error after post update: %o', cacheError);
136
+ }
137
+ return result;
138
+ } catch (error) {
139
+ this.logger.error('Error updating post', {
140
+ error
141
+ });
142
+ throw error;
143
+ }
144
+ }
145
+ async delete(criteria) {
146
+ try {
147
+ const result = await super.delete(criteria);
148
+ if (typeof result !== 'boolean') {
149
+ this.logger.error('Failed to delete post', {
150
+ error: result
151
+ });
152
+ throw new Error('Failed to delete post');
153
+ }
154
+ try {
155
+ // Attempt to invalidate caches if criteria is id
156
+ const id = typeof criteria === 'string' ? criteria : criteria?.id;
157
+ if (id) {
158
+ const existing = await this.get(id).catch(() => null);
159
+ const channelId = existing?.channel?.toString?.();
160
+ if (channelId) {
161
+ await this.invalidatePostCachesByChannel(String(channelId));
162
+ }
163
+ }
164
+ } catch (cacheError) {
165
+ this.logger.warn('Cache invalidation error after post delete: %o', cacheError);
166
+ }
167
+ return result;
168
+ } catch (error) {
169
+ this.logger.error('Error deleting post', {
170
+ error
171
+ });
172
+ throw error;
173
+ }
174
+ }
175
+ /**
176
+ * Creates a file upload link for a post
177
+ *
178
+ * @description Generates a secure upload URL for attaching files to posts
179
+ *
180
+ * @param {string} postId - The post identifier
181
+ * @param {string} filename - The name of the file to upload
182
+ * @param {string} userId - The user performing the upload
183
+ * @returns {Promise<string | Error>} - Upload URL or error
184
+ */
185
+ async createFileUploadLink(postId, filename, userId) {
186
+ try {
187
+ if (!postId || !filename || !userId) {
188
+ return new Error('Post ID, filename, and user ID are required');
189
+ }
190
+ const uploadUrl = await this.fileInfoService.uploadByUrl({
191
+ filename,
192
+ refType: FileRefType.Post,
193
+ ref: postId,
194
+ userId
195
+ });
196
+ this.logger.debug('File upload link created', {
197
+ postId,
198
+ filename,
199
+ userId
200
+ });
201
+ return uploadUrl;
202
+ } catch (error) {
203
+ this.logger.error('Error creating file upload link: %o', error);
204
+ return error instanceof Error ? error : new Error('Unknown error occurred while creating upload link');
205
+ }
27
206
  }
207
+ /**
208
+ * Attaches an uploaded file to a post
209
+ *
210
+ * @description Processes and attaches a completed file upload to a post
211
+ *
212
+ * @param {string} postId - The post identifier
213
+ * @param {IUploadedFileInput} file - The uploaded file information
214
+ * @param {string} createdBy - The user who uploaded the file
215
+ * @returns {Promise<IFileInfo | Error>} - File information or error
216
+ */
28
217
  async attachUploadedFile(postId, file, createdBy) {
29
- const {
30
- id
31
- } = await this.fileInfoService.createUploadedFile({
32
- ...file,
33
- refType: FileRefType.Post,
34
- ref: postId,
35
- createdBy
36
- });
37
- const fileResponse = await this.fileInfoService.get(id);
38
- return {
39
- ...fileResponse,
40
- url: this.fileInfoService.getDefaultImage(fileResponse.url)
41
- };
42
- }
43
- createFilesUploadLink(postId, filenames, userId) {
44
- return Promise.all(filenames.map(filename => this.fileInfoService.uploadByUrl({
45
- filename,
46
- refType: FileRefType.Post,
47
- ref: postId,
48
- userId
49
- })));
50
- }
51
- attachUploadedFiles(postId, files, createdBy) {
52
- return Promise.all(files.map(async file => {
218
+ try {
219
+ if (!postId || !file || !createdBy) {
220
+ return new Error('Post ID, file data, and creator ID are required');
221
+ }
53
222
  const {
54
223
  id
55
224
  } = await this.fileInfoService.createUploadedFile({
@@ -59,103 +228,550 @@ let PostService = PostService_1 = class PostService extends BaseService {
59
228
  createdBy
60
229
  });
61
230
  const fileResponse = await this.fileInfoService.get(id);
62
- return {
231
+ const result = {
63
232
  ...fileResponse,
64
233
  url: this.fileInfoService.getDefaultImage(fileResponse.url)
65
234
  };
66
- }));
235
+ this.logger.debug('File attached to post', {
236
+ postId,
237
+ fileId: id,
238
+ createdBy
239
+ });
240
+ return result;
241
+ } catch (error) {
242
+ this.logger.error('Error attaching uploaded file: %o', error);
243
+ return error instanceof Error ? error : new Error('Unknown error occurred while attaching file');
244
+ }
67
245
  }
68
- async create(data) {
69
- const post = await super.create(data);
70
- const {
71
- channel
72
- } = data;
73
- // Accessing private property
74
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
75
- // @ts-ignore
76
- await this.channelRepo.model.updateOne({
77
- _id: channel
78
- }, {
79
- lastPostAt: post.createdAt,
80
- $inc: {
81
- totalMsgCount: 1,
82
- totalMsgCountRoot: 1,
83
- 'members.$[elem].msgCountRoot': 1,
84
- 'members.$[elem].msgCount': 1
85
- }
86
- }, {
87
- arrayFilters: [{
88
- 'elem.user': data.author
89
- }]
90
- });
91
- await this.pubsub.publish(`POST_CREATED.${channel}`, post);
92
- await this.sendExpoNotification(post);
93
- // if (post?.props?.notificationParams) await this.messengerNotificationService.sendExpoNotificationOnPost(post);
94
- this.logger.trace('Post created');
95
- return post;
246
+ /**
247
+ * Creates upload links for multiple files
248
+ *
249
+ * @description Generates secure upload URLs for multiple files to be attached to a post
250
+ *
251
+ * @param {string} postId - The post identifier
252
+ * @param {string[]} filenames - Array of file names
253
+ * @param {string} userId - The user performing the uploads
254
+ * @returns {Promise<string[] | Error>} - Array of upload URLs or error
255
+ */
256
+ async createFilesUploadLink(postId, filenames, userId) {
257
+ try {
258
+ if (!postId || !filenames || !Array.isArray(filenames) || !userId) {
259
+ return new Error('Post ID, filenames array, and user ID are required');
260
+ }
261
+ if (filenames.length === 0) {
262
+ return new Error('At least one filename is required');
263
+ }
264
+ const uploadUrls = await Promise.all(filenames.map(filename => this.fileInfoService.uploadByUrl({
265
+ filename,
266
+ refType: FileRefType.Post,
267
+ ref: postId,
268
+ userId
269
+ })));
270
+ this.logger.debug('Multiple file upload links created', {
271
+ postId,
272
+ fileCount: filenames.length,
273
+ userId
274
+ });
275
+ return uploadUrls;
276
+ } catch (error) {
277
+ this.logger.error('Error creating multiple file upload links: %o', error);
278
+ return error instanceof Error ? error : new Error('Unknown error occurred while creating upload links');
279
+ }
96
280
  }
281
+ /**
282
+ * Attaches multiple uploaded files to a post
283
+ *
284
+ * @description Processes and attaches multiple completed file uploads to a post
285
+ *
286
+ * @param {string} postId - The post identifier
287
+ * @param {IUploadedFileInput[]} files - Array of uploaded file information
288
+ * @param {string} createdBy - The user who uploaded the files
289
+ * @returns {Promise<IFileInfo[] | Error>} - Array of file information or error
290
+ */
291
+ async attachUploadedFiles(postId, files, createdBy) {
292
+ try {
293
+ if (!postId || !files || !Array.isArray(files) || !createdBy) {
294
+ return new Error('Post ID, files array, and creator ID are required');
295
+ }
296
+ if (files.length === 0) {
297
+ return new Error('At least one file is required');
298
+ }
299
+ const fileResults = await Promise.all(files.map(async file => {
300
+ const {
301
+ id
302
+ } = await this.fileInfoService.createUploadedFile({
303
+ ...file,
304
+ refType: FileRefType.Post,
305
+ ref: postId,
306
+ createdBy
307
+ });
308
+ const fileResponse = await this.fileInfoService.get(id);
309
+ return {
310
+ ...fileResponse,
311
+ url: this.fileInfoService.getDefaultImage(fileResponse.url)
312
+ };
313
+ }));
314
+ this.logger.debug('Multiple files attached to post', {
315
+ postId,
316
+ fileCount: files.length,
317
+ createdBy
318
+ });
319
+ return fileResults;
320
+ } catch (error) {
321
+ this.logger.error('Error attaching multiple uploaded files: %o', error);
322
+ return error instanceof Error ? error : new Error('Unknown error occurred while attaching files');
323
+ }
324
+ }
325
+ /**
326
+ * Creates a new post with full integration
327
+ *
328
+ * @description Creates a post with channel updates, notifications, and real-time publishing
329
+ *
330
+ * @param {IPostServiceInput} data - Post creation data
331
+ * @returns {Promise<AsDomainType<IPostModel> | Error>} - Created post or error
332
+ */
333
+ // @ts-ignore - Type compatibility issue between interface and implementation
97
334
  async createWithoutSubscription(data) {
98
- const post = await super.create(data);
99
- const {
100
- channel
101
- } = data;
102
- await this.channelRepo.model.updateOne({
103
- _id: channel
104
- }, {
105
- lastPostAt: post.createdAt,
106
- $inc: {
107
- totalMsgCount: 1,
108
- totalMsgCountRoot: 1,
109
- 'members.$[elem].msgCountRoot': 1,
110
- 'members.$[elem].msgCount': 1
111
- }
112
- }, {
113
- arrayFilters: [{
114
- 'elem.user': data.author
115
- }]
116
- });
117
- return post;
335
+ try {
336
+ if (!data) {
337
+ return new Error('Post data is required');
338
+ }
339
+ if (!data.channel) {
340
+ return new Error('Channel is required for post creation');
341
+ }
342
+ if (!data.author) {
343
+ return new Error('Author is required for post creation');
344
+ }
345
+ // Create the post
346
+ const postData = {
347
+ ...data,
348
+ createdAt: new Date(),
349
+ updatedAt: new Date()
350
+ }; // Type assertion for compatibility
351
+ const post = await this.repository.create(postData);
352
+ if (!post) {
353
+ return new Error('Failed to create post');
354
+ }
355
+ const {
356
+ channel
357
+ } = data;
358
+ // Update channel statistics without real-time publishing
359
+ // @ts-ignore - Accessing private property for channel updates
360
+ await this.channelRepo.model.updateOne({
361
+ _id: channel
362
+ }, {
363
+ lastPostAt: post.createdAt,
364
+ $inc: {
365
+ totalMsgCount: 1,
366
+ totalMsgCountRoot: 1,
367
+ 'members.$[elem].msgCountRoot': 1,
368
+ 'members.$[elem].msgCount': 1
369
+ }
370
+ }, {
371
+ arrayFilters: [{
372
+ 'elem.user': data.author
373
+ }]
374
+ });
375
+ this.logger.debug('Post created without subscription', {
376
+ postId: post.id,
377
+ channelId: channel,
378
+ authorId: data.author
379
+ });
380
+ // Invalidate caches related to this channel
381
+ await this.invalidatePostCachesByChannel(String(channel));
382
+ return post;
383
+ } catch (error) {
384
+ this.logger.error('Error creating post without subscription: %o', error);
385
+ return error instanceof Error ? error : new Error('Unknown error occurred while creating post');
386
+ }
118
387
  }
119
- deleteFile(url) {
120
- return this.fileInfoService.deleteByUrl(url);
388
+ /**
389
+ * Deletes a file by URL
390
+ *
391
+ * @description Removes a file from storage using its URL
392
+ *
393
+ * @param {string} url - The file URL to delete
394
+ * @returns {Promise<boolean | Error>} - Success status or error
395
+ */
396
+ async deleteFile(url) {
397
+ try {
398
+ if (!url) {
399
+ return new Error('File URL is required');
400
+ }
401
+ const success = await this.fileInfoService.deleteByUrl(url);
402
+ this.logger.debug('File deleted', {
403
+ url,
404
+ success
405
+ });
406
+ return success;
407
+ } catch (error) {
408
+ this.logger.error('Error deleting file: %o', error);
409
+ return error instanceof Error ? error : new Error('Unknown error occurred while deleting file');
410
+ }
121
411
  }
412
+ /**
413
+ * Marks a message as read by a user
414
+ *
415
+ * @description Updates message status to indicate it has been read by a specific user
416
+ *
417
+ * @param {IMessageIdentifier} messageId - Message identifier
418
+ * @param {string} user - User ID who read the message
419
+ * @returns {Promise<boolean | Error>} - Success status or error
420
+ */
122
421
  async readMessage(messageId, user) {
123
- await this.update(messageId.messageId, {
124
- props: {
125
- [`[${user}]`]: set({}, PostTypes.isRead, true)
422
+ try {
423
+ if (!messageId?.messageId || !user) {
424
+ return new Error('Message ID and user ID are required');
126
425
  }
127
- });
128
- return true;
426
+ await this.update(messageId.messageId, {
427
+ props: {
428
+ [`[${user}]`]: set({}, PostTypes.isRead, true)
429
+ }
430
+ });
431
+ this.logger.debug('Message marked as read', {
432
+ messageId: messageId.messageId,
433
+ userId: user
434
+ });
435
+ return true;
436
+ } catch (error) {
437
+ this.logger.error('Error marking message as read: %o', error);
438
+ return error instanceof Error ? error : new Error('Unknown error occurred while marking message as read');
439
+ }
129
440
  }
441
+ /**
442
+ * Marks a message as delivered to a user
443
+ *
444
+ * @description Updates message status to indicate it has been delivered to a specific user
445
+ *
446
+ * @param {IMessageIdentifier} messageId - Message identifier
447
+ * @param {string} user - User ID who received the message
448
+ * @returns {Promise<boolean | Error>} - Success status or error
449
+ */
130
450
  async deliverMessage(messageId, user) {
131
- await this.update(messageId.messageId, {
132
- props: {
133
- [`[${user}]`]: set({}, PostTypes.isDelivered, true)
451
+ try {
452
+ if (!messageId?.messageId || !user) {
453
+ return new Error('Message ID and user ID are required');
134
454
  }
135
- });
136
- return true;
455
+ await this.update(messageId.messageId, {
456
+ props: {
457
+ [`[${user}]`]: set({}, PostTypes.isDelivered, true)
458
+ }
459
+ });
460
+ this.logger.debug('Message marked as delivered', {
461
+ messageId: messageId.messageId,
462
+ userId: user
463
+ });
464
+ return true;
465
+ } catch (error) {
466
+ this.logger.error('Error marking message as delivered: %o', error);
467
+ return error instanceof Error ? error : new Error('Unknown error occurred while marking message as delivered');
468
+ }
137
469
  }
138
- async sendExpoNotification(post) {
139
- if (post?.props?.notificationParams && (!post?.type || post?.type !== 'ALERT')) {
140
- await this.messengerNotificationService.sendExpoNotificationOnPost(post);
141
- } else if (!post?.type || post?.type !== 'ALERT') {
142
- const notificationData = {
143
- url: config.INBOX_MESSEGE_PATH,
144
- params: {
145
- channelId: post?.channel,
146
- hideTabBar: true
147
- },
148
- screen: 'DialogMessages',
149
- other: {
150
- sound: 'default'
470
+ /**
471
+ * Creates a post with associated post thread
472
+ *
473
+ * @description Creates both a post and its associated thread for threaded conversations
474
+ *
475
+ * @param {CreatePostThreadOptions} data - Post and thread creation options
476
+ * @returns {Promise<{post: AsDomainType<IPostModel>; postThread: IPostThread} | Error>} - Created post and thread or error
477
+ */
478
+ // @ts-ignore - Type compatibility issue between interface and implementation
479
+ async createPostWithPostThread(data) {
480
+ try {
481
+ if (!data) {
482
+ return new Error('Post thread creation data is required');
483
+ }
484
+ // This would require integration with PostThreadService
485
+ // For now, return an error indicating it needs implementation
486
+ return new Error('Post thread creation is not yet implemented');
487
+ } catch (error) {
488
+ this.logger.error('Error creating post with thread: %o', error);
489
+ return error instanceof Error ? error : new Error('Unknown error occurred while creating post with thread');
490
+ }
491
+ }
492
+ /**
493
+ * Retrieves the last message in a channel
494
+ *
495
+ * @description Gets the most recent post/message from a specific channel
496
+ *
497
+ * @param {string} channelId - The channel identifier
498
+ * @returns {Promise<AsDomainType<IPostModel> | Error>} - Last message or error
499
+ */
500
+ // @ts-ignore - Type compatibility issue between interface and implementation
501
+ async getLastMessage(channelId) {
502
+ try {
503
+ if (!channelId) {
504
+ return new Error('Channel ID is required');
505
+ }
506
+ // Try Redis cache first
507
+ if (this.redisCacheManager) {
508
+ try {
509
+ const cacheKey = `${config.APP_NAME}:getLastPostByChannel`;
510
+ const variables = {
511
+ channelId
512
+ };
513
+ const cacheCtx = {
514
+ userId: 'system'
515
+ };
516
+ const cached = await this.redisCacheManager.get(cacheKey, variables, cacheCtx);
517
+ if (cached) {
518
+ this.logger.debug(`Cache hit for last post by channel ${channelId}`);
519
+ return cached;
520
+ }
521
+ } catch (cacheError) {
522
+ this.logger.warn(`Redis cache error for getLastMessage(${channelId}): ${cacheError?.message}`);
151
523
  }
152
- };
153
- await this.messengerNotificationService.sendExpoNotificationOnPost(post, notificationData);
524
+ }
525
+ // @ts-ignore - Accessing private property for direct query
526
+ const post = await this.repository.model.findOne({
527
+ channel: channelId
528
+ }).sort({
529
+ createdAt: -1
530
+ });
531
+ if (!post) {
532
+ return new Error('No messages found in channel');
533
+ }
534
+ this.logger.debug('Last message retrieved', {
535
+ channelId,
536
+ postId: post._id
537
+ });
538
+ const result = post;
539
+ // Cache the result
540
+ if (this.redisCacheManager) {
541
+ try {
542
+ const cacheKey = `${config.APP_NAME}:getLastPostByChannel`;
543
+ const variables = {
544
+ channelId
545
+ };
546
+ const cacheCtx = {
547
+ userId: 'system'
548
+ };
549
+ await this.redisCacheManager.set(cacheKey, variables, result, cacheCtx, PostService_1.CACHE_TTL);
550
+ } catch (cacheError) {
551
+ this.logger.warn(`Failed to cache last post by channel: ${cacheError?.message}`);
552
+ }
553
+ }
554
+ return result;
555
+ } catch (error) {
556
+ this.logger.error('Error getting last message: %o', error);
557
+ return error instanceof Error ? error : new Error('Unknown error occurred while getting last message');
558
+ }
559
+ }
560
+ /**
561
+ * Gets posts by channel with pagination
562
+ *
563
+ * @description Retrieves posts from a channel with pagination support
564
+ *
565
+ * @param {string} channelId - The channel identifier
566
+ * @param {number} limit - Maximum number of posts to return
567
+ * @param {number} offset - Number of posts to skip
568
+ * @returns {Promise<Array<AsDomainType<IPostModel>> | Error>} - Array of posts or error
569
+ */
570
+ async getPostsByChannel(channelId, limit = 50, offset = 0) {
571
+ try {
572
+ if (!channelId) {
573
+ return new Error('Channel ID is required');
574
+ }
575
+ // Try Redis cache first
576
+ if (this.redisCacheManager) {
577
+ try {
578
+ const cacheKey = `${config.APP_NAME}:getPostsByChannel`;
579
+ const variables = {
580
+ channelId,
581
+ limit,
582
+ offset
583
+ };
584
+ const cacheCtx = {
585
+ userId: 'system'
586
+ };
587
+ const cached = await this.redisCacheManager.get(cacheKey, variables, cacheCtx);
588
+ if (cached) {
589
+ this.logger.debug(`Cache hit for posts by channel ${channelId}`);
590
+ return cached;
591
+ }
592
+ } catch (cacheError) {
593
+ this.logger.warn(`Redis cache error for getPostsByChannel(${channelId}): ${cacheError?.message}`);
594
+ }
595
+ }
596
+ const posts = await this.getAll({
597
+ criteria: {
598
+ channel: channelId
599
+ }
600
+ });
601
+ // Apply manual pagination
602
+ const paginatedPosts = posts.slice(offset, offset + limit);
603
+ this.logger.debug('Posts retrieved by channel', {
604
+ channelId,
605
+ count: paginatedPosts.length,
606
+ limit,
607
+ offset
608
+ });
609
+ const result = paginatedPosts;
610
+ // Cache the result
611
+ if (this.redisCacheManager) {
612
+ try {
613
+ const cacheKey = `${config.APP_NAME}:getPostsByChannel`;
614
+ const variables = {
615
+ channelId,
616
+ limit,
617
+ offset
618
+ };
619
+ const cacheCtx = {
620
+ userId: 'system'
621
+ };
622
+ await this.redisCacheManager.set(cacheKey, variables, result, cacheCtx, PostService_1.CACHE_TTL);
623
+ } catch (cacheError) {
624
+ this.logger.warn(`Failed to cache posts by channel: ${cacheError?.message}`);
625
+ }
626
+ }
627
+ return result;
628
+ } catch (error) {
629
+ this.logger.error('Error getting posts by channel: %o', error);
630
+ return error instanceof Error ? error : new Error('Unknown error occurred while getting posts by channel');
631
+ }
632
+ }
633
+ /**
634
+ * Updates a post
635
+ *
636
+ * @description Updates post content and metadata
637
+ *
638
+ * @param {string} postId - The post identifier
639
+ * @param {Partial<IPostServiceInput>} updates - Update data
640
+ * @returns {Promise<AsDomainType<IPostModel> | Error>} - Updated post or error
641
+ */
642
+ async updatePost(postId, updates) {
643
+ try {
644
+ if (!postId) {
645
+ return new Error('Post ID is required');
646
+ }
647
+ if (!updates || Object.keys(updates).length === 0) {
648
+ return new Error('Updates are required');
649
+ }
650
+ // Get existing post
651
+ const existingPost = await this.get(postId);
652
+ if (!existingPost) {
653
+ return new Error('Post not found');
654
+ }
655
+ // Prepare update data
656
+ const updateData = {
657
+ ...updates,
658
+ updatedAt: new Date()
659
+ }; // Type assertion for compatibility
660
+ // Update the post
661
+ const updatedPost = await this.update(postId, updateData);
662
+ if (!updatedPost) {
663
+ return new Error('Failed to update post');
664
+ }
665
+ this.logger.debug('Post updated successfully', {
666
+ postId,
667
+ updates: Object.keys(updates)
668
+ });
669
+ return updatedPost;
670
+ } catch (error) {
671
+ this.logger.error('Error updating post: %o', error);
672
+ return error instanceof Error ? error : new Error('Unknown error occurred while updating post');
154
673
  }
155
674
  }
156
- // eslint-disable-next-line class-methods-use-this
157
- createPostWithPostThread(data) {
158
- throw new Error('Not implemented');
675
+ /**
676
+ * Deletes a post
677
+ *
678
+ * @description Soft deletes a post and its associated data
679
+ *
680
+ * @param {string} postId - The post identifier
681
+ * @returns {Promise<boolean | Error>} - Success status or error
682
+ */
683
+ async deletePost(postId) {
684
+ try {
685
+ if (!postId) {
686
+ return new Error('Post ID is required');
687
+ }
688
+ // Get post to verify it exists
689
+ const post = await this.get(postId);
690
+ if (!post) {
691
+ return new Error('Post not found');
692
+ }
693
+ // Soft delete the post
694
+ const success = await this.repository.delete({
695
+ id: postId
696
+ });
697
+ if (success) {
698
+ this.logger.debug('Post deleted successfully', {
699
+ postId
700
+ });
701
+ try {
702
+ const channelId = post?.channel?.toString?.();
703
+ if (channelId) {
704
+ await this.invalidatePostCachesByChannel(String(channelId));
705
+ }
706
+ } catch (cacheError) {
707
+ this.logger.warn('Cache invalidation error after deletePost: %o', cacheError);
708
+ }
709
+ }
710
+ return success;
711
+ } catch (error) {
712
+ this.logger.error('Error deleting post: %o', error);
713
+ return error instanceof Error ? error : new Error('Unknown error occurred while deleting post');
714
+ }
715
+ }
716
+ /**
717
+ * Sends Expo push notification for a post
718
+ *
719
+ * @description Handles push notification logic for new posts
720
+ *
721
+ * @param {any} post - The post data
722
+ * @private
723
+ */
724
+ async sendExpoNotification(post) {
725
+ try {
726
+ if (post?.props?.notificationParams && (!post?.type || post?.type !== 'ALERT')) {
727
+ await this.messengerNotificationService.sendExpoNotificationOnPost(post);
728
+ } else if (!post?.type || post?.type !== 'ALERT') {
729
+ const notificationData = {
730
+ url: config.INBOX_MESSEGE_PATH,
731
+ params: {
732
+ channelId: post?.channel,
733
+ hideTabBar: true
734
+ },
735
+ screen: 'DialogMessages',
736
+ other: {
737
+ sound: 'default'
738
+ }
739
+ };
740
+ await this.messengerNotificationService.sendExpoNotificationOnPost(post, notificationData);
741
+ }
742
+ this.logger.debug('Expo notification sent', {
743
+ postId: post?.id
744
+ });
745
+ } catch (error) {
746
+ this.logger.error('Error sending Expo notification: %o', error);
747
+ // Don't throw error as notification failure shouldn't break post creation
748
+ }
749
+ }
750
+ /**
751
+ * Invalidates cache entries related to a channel's posts
752
+ * @param channelId - Channel ID
753
+ */
754
+ async invalidatePostCachesByChannel(channelId) {
755
+ if (!this.redisCacheManager || !channelId) {
756
+ return;
757
+ }
758
+ try {
759
+ // Invalidate last post by channel
760
+ await this.redisCacheManager.del(`${config.APP_NAME}:getLastPostByChannel`, {
761
+ channelId
762
+ }, {
763
+ userId: 'system'
764
+ }, true);
765
+ // Invalidate posts by channel with any pagination (wildcard delete)
766
+ await this.redisCacheManager.del(`${config.APP_NAME}:getPostsByChannel`, {
767
+ channelId
768
+ }, {
769
+ userId: 'system'
770
+ }, true);
771
+ this.logger.debug(`Invalidated post caches for channel ${channelId}`);
772
+ } catch (error) {
773
+ this.logger.warn(`Error invalidating post caches: ${error?.message}`);
774
+ }
159
775
  }
160
776
  };
161
- PostService = PostService_1 = __decorate([__param(0, inject(SERVER_TYPES.PostRepository)), __param(1, inject(SERVER_TYPES.ChannelRepository)), __param(2, inject(SERVER_TYPES.FileInfoService)), __param(3, inject(SERVER_TYPES.MessengerNotificationService)), __param(4, inject(CommonType.MOLECULER_BROKER)), __param(5, inject('PubSub')), __param(6, inject('Logger')), __metadata("design:paramtypes", [PostRepository, ChannelRepository, Object, Object, ServiceBroker, PubSubEngine, Object])], PostService);export{PostService};//# sourceMappingURL=post-service.js.map
777
+ PostService = PostService_1 = __decorate([__param(0, inject(SERVER_TYPES.PostRepository)), __param(1, inject(SERVER_TYPES.ChannelRepository)), __param(2, inject(SERVER_TYPES.FileInfoService)), __param(3, inject(SERVER_TYPES.MessengerNotificationService)), __param(4, inject(CommonType.MOLECULER_BROKER)), __param(5, inject('PubSub')), __param(6, inject(SERVER_TYPES.IRedisCacheManager)), __param(7, inject(CommonType.REDIS_CLIENT)), __param(8, inject('Logger')), __metadata("design:paramtypes", [PostRepository, ChannelRepository, Object, Object, ServiceBroker, PubSubEngine, Object, Function, Object])], PostService);export{PostService};//# sourceMappingURL=post-service.js.map