@nlabs/reaktor 0.1.0

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 (154) hide show
  1. package/.vscode/extensions.json +15 -0
  2. package/.vscode/settings.json +82 -0
  3. package/README.md +211 -0
  4. package/index.d.ts +1 -0
  5. package/index.js +5 -0
  6. package/lex.config.js +4 -0
  7. package/lib/config.d.ts +21 -0
  8. package/lib/config.js +127 -0
  9. package/lib/data/conversations.d.ts +6 -0
  10. package/lib/data/conversations.js +201 -0
  11. package/lib/data/dynamodb.d.ts +8 -0
  12. package/lib/data/dynamodb.js +139 -0
  13. package/lib/data/email.d.ts +7 -0
  14. package/lib/data/email.js +164 -0
  15. package/lib/data/files.d.ts +16 -0
  16. package/lib/data/files.js +407 -0
  17. package/lib/data/groups.d.ts +13 -0
  18. package/lib/data/groups.js +354 -0
  19. package/lib/data/images.d.ts +12 -0
  20. package/lib/data/images.js +668 -0
  21. package/lib/data/index.d.ts +19 -0
  22. package/lib/data/index.js +24 -0
  23. package/lib/data/ios.d.ts +6 -0
  24. package/lib/data/ios.js +302 -0
  25. package/lib/data/locations.d.ts +3 -0
  26. package/lib/data/locations.js +132 -0
  27. package/lib/data/messages.d.ts +9 -0
  28. package/lib/data/messages.js +248 -0
  29. package/lib/data/notifications.d.ts +5 -0
  30. package/lib/data/notifications.js +42 -0
  31. package/lib/data/payments.d.ts +11 -0
  32. package/lib/data/payments.js +748 -0
  33. package/lib/data/posts.d.ts +14 -0
  34. package/lib/data/posts.js +458 -0
  35. package/lib/data/reactions.d.ts +6 -0
  36. package/lib/data/reactions.js +218 -0
  37. package/lib/data/s3.d.ts +6 -0
  38. package/lib/data/s3.js +103 -0
  39. package/lib/data/search.d.ts +3 -0
  40. package/lib/data/search.js +98 -0
  41. package/lib/data/sms.d.ts +3 -0
  42. package/lib/data/sms.js +59 -0
  43. package/lib/data/subscription.d.ts +7 -0
  44. package/lib/data/subscription.js +284 -0
  45. package/lib/data/tags.d.ts +14 -0
  46. package/lib/data/tags.js +304 -0
  47. package/lib/data/users.d.ts +12 -0
  48. package/lib/data/users.js +312 -0
  49. package/lib/index.d.ts +3 -0
  50. package/lib/index.js +8 -0
  51. package/lib/types/apps.d.ts +44 -0
  52. package/lib/types/apps.js +2 -0
  53. package/lib/types/arangodb.d.ts +17 -0
  54. package/lib/types/arangodb.js +2 -0
  55. package/lib/types/auth.d.ts +9 -0
  56. package/lib/types/auth.js +2 -0
  57. package/lib/types/conversations.d.ts +6 -0
  58. package/lib/types/conversations.js +2 -0
  59. package/lib/types/email.d.ts +12 -0
  60. package/lib/types/email.js +2 -0
  61. package/lib/types/files.d.ts +28 -0
  62. package/lib/types/files.js +2 -0
  63. package/lib/types/google.d.ts +27 -0
  64. package/lib/types/google.js +2 -0
  65. package/lib/types/groups.d.ts +22 -0
  66. package/lib/types/groups.js +2 -0
  67. package/lib/types/images.d.ts +25 -0
  68. package/lib/types/images.js +2 -0
  69. package/lib/types/index.d.ts +17 -0
  70. package/lib/types/index.js +22 -0
  71. package/lib/types/locations.d.ts +21 -0
  72. package/lib/types/locations.js +2 -0
  73. package/lib/types/messages.d.ts +12 -0
  74. package/lib/types/messages.js +2 -0
  75. package/lib/types/notifications.d.ts +19 -0
  76. package/lib/types/notifications.js +2 -0
  77. package/lib/types/payments.d.ts +119 -0
  78. package/lib/types/payments.js +2 -0
  79. package/lib/types/posts.d.ts +20 -0
  80. package/lib/types/posts.js +2 -0
  81. package/lib/types/reactions.d.ts +4 -0
  82. package/lib/types/reactions.js +2 -0
  83. package/lib/types/tags.d.ts +10 -0
  84. package/lib/types/tags.js +2 -0
  85. package/lib/types/users.d.ts +78 -0
  86. package/lib/types/users.js +2 -0
  87. package/lib/utils/analytics.d.ts +3 -0
  88. package/lib/utils/analytics.js +47 -0
  89. package/lib/utils/arangodb.d.ts +9 -0
  90. package/lib/utils/arangodb.js +98 -0
  91. package/lib/utils/auth.d.ts +2 -0
  92. package/lib/utils/auth.js +43 -0
  93. package/lib/utils/index.d.ts +5 -0
  94. package/lib/utils/index.js +10 -0
  95. package/lib/utils/objects.d.ts +3 -0
  96. package/lib/utils/objects.js +34 -0
  97. package/lib/utils/redis.d.ts +1 -0
  98. package/lib/utils/redis.js +15 -0
  99. package/package.json +75 -0
  100. package/src/config.ts +121 -0
  101. package/src/data/conversations.ts +183 -0
  102. package/src/data/dynamodb.ts +157 -0
  103. package/src/data/email.ts +164 -0
  104. package/src/data/files.ts +352 -0
  105. package/src/data/groups.ts +308 -0
  106. package/src/data/images.ts +606 -0
  107. package/src/data/index.ts +23 -0
  108. package/src/data/ios.ts +249 -0
  109. package/src/data/locations.ts +114 -0
  110. package/src/data/messages.ts +237 -0
  111. package/src/data/notifications.ts +48 -0
  112. package/src/data/payments.ts +675 -0
  113. package/src/data/posts.ts +508 -0
  114. package/src/data/reactions.ts +186 -0
  115. package/src/data/s3.ts +117 -0
  116. package/src/data/search.ts +74 -0
  117. package/src/data/sms.ts +60 -0
  118. package/src/data/subscription.ts +228 -0
  119. package/src/data/tags.ts +230 -0
  120. package/src/data/users.ts +256 -0
  121. package/src/index.ts +7 -0
  122. package/src/types/apps.ts +57 -0
  123. package/src/types/arangodb.ts +23 -0
  124. package/src/types/auth.ts +19 -0
  125. package/src/types/conversations.ts +11 -0
  126. package/src/types/email.ts +17 -0
  127. package/src/types/files.ts +33 -0
  128. package/src/types/google.ts +37 -0
  129. package/src/types/groups.ts +28 -0
  130. package/src/types/images.ts +33 -0
  131. package/src/types/index.ts +21 -0
  132. package/src/types/locations.ts +25 -0
  133. package/src/types/messages.ts +16 -0
  134. package/src/types/notifications.ts +26 -0
  135. package/src/types/payments.ts +134 -0
  136. package/src/types/posts.ts +25 -0
  137. package/src/types/reactions.ts +8 -0
  138. package/src/types/tags.ts +14 -0
  139. package/src/types/users.ts +89 -0
  140. package/src/utils/analytics.ts +41 -0
  141. package/src/utils/arangodb.ts +100 -0
  142. package/src/utils/auth.ts +28 -0
  143. package/src/utils/index.ts +9 -0
  144. package/src/utils/objects.ts +34 -0
  145. package/src/utils/redis.ts +17 -0
  146. package/templates/email/layout.html +279 -0
  147. package/templates/email/passwordForgot.html +15 -0
  148. package/templates/email/passwordRecovery.html +12 -0
  149. package/templates/email/verifyEmail.html +15 -0
  150. package/templates/sms/passwordForgot.txt +1 -0
  151. package/templates/sms/passwordRecovery.txt +1 -0
  152. package/templates/sms/verifyEmail.txt +1 -0
  153. package/templates/sms/verifyPhone.txt +1 -0
  154. package/tsconfig.json +45 -0
@@ -0,0 +1,508 @@
1
+ /**
2
+ * Copyright (c) 2019-Present, Nitrogen Labs, Inc.
3
+ * Copyrights licensed under the MIT License. See the accompanying LICENSE file for terms.
4
+ */
5
+ import {createHash, parseChar, parseId, parseString, parseVarChar} from '@nlabs/utils';
6
+ import {aql, Database} from 'arangojs';
7
+ import {AqlQuery} from 'arangojs/lib/cjs/aql-query';
8
+ import {ArrayCursor} from 'arangojs/lib/cjs/cursor';
9
+ import flatten from 'lodash/flatten';
10
+ import uniqBy from 'lodash/uniqBy';
11
+
12
+ import {ApiContext, ArangoDBLimit, FileType, GroupType, PostType, TagType} from '../types';
13
+ import {getLimit, useDb} from '../utils';
14
+ import {updateFiles} from './files';
15
+ import {extractTags} from './tags';
16
+
17
+ // const eventCategory: string = 'posts';
18
+
19
+ export const getPostList = (context: ApiContext, from: number, to: number): Promise<PostType[]> => {
20
+ // const action: string = 'getListByApp';
21
+ const {database} = context;
22
+ const limit: ArangoDBLimit = getLimit(from, to);
23
+ const aqlQry: string = `FOR p IN posts
24
+ FILTER !!p.parent == false
25
+ LET reactions = (
26
+ FOR post, r IN INBOUND p._id reactions
27
+ COLLECT reactionName = r.value INTO reactionItems
28
+ RETURN {name: reactionName, count: LENGTH(reactionItems[*].r.value)}
29
+ )
30
+ FOR u IN users
31
+ FILTER p.userId == u._key
32
+ ${limit.aql}
33
+ SORT p.added
34
+ RETURN DISTINCT MERGE(p, {user:u, reactions:reactions})`;
35
+
36
+ return useDb(database).query(aqlQry)
37
+ .then((cursor: ArrayCursor) => cursor.all())
38
+ .catch((error: Error) => {
39
+ throw error;
40
+ });
41
+ };
42
+
43
+ export const getPostListByGroup = (context: ApiContext, groupId: string, from: number, to: number): Promise<PostType[]> => {
44
+ // const action: string = 'getListByGroup';
45
+ const {database, userId: sessionId} = context;
46
+
47
+ // Group id
48
+ const formatGroupId: string = parseId(groupId);
49
+ const db = useDb(database);
50
+ const aqlQry: string = `FOR u, g IN INBOUND ${formatGroupId} hasGroup
51
+ FILTER u._key == ${sessionId}
52
+ RETURN g`;
53
+
54
+ return db.query(aqlQry)
55
+ .then((cursor: ArrayCursor) => cursor.all())
56
+ .then((groups: GroupType[] = []) => {
57
+ if(groups.length) {
58
+ const limit: ArangoDBLimit = getLimit(from, to);
59
+ const postAqlQry: string = `FOR p IN posts
60
+ FILTER p.groupId == "${formatGroupId}" && !!p.parent == false
61
+ LET reactions = (
62
+ FOR post, r IN INBOUND p._id reactions
63
+ COLLECT reactionName = r.value INTO reactionItems
64
+ RETURN {name: reactionName, count: LENGTH(reactionItems[*].r.value)}
65
+ )
66
+ FOR u IN users
67
+ FILTER p.userId == u._key
68
+ ${limit.aql}
69
+ SORT p.added
70
+ RETURN DISTINCT MERGE(p, {user:u, reactions:reactions})`;
71
+
72
+ return db.query(postAqlQry)
73
+ .then((cursor: ArrayCursor) => cursor.all())
74
+ .catch((error: Error) => {
75
+ throw error;
76
+ });
77
+ }
78
+
79
+ return [];
80
+ })
81
+ .catch((error: Error) => {
82
+ throw error;
83
+ });
84
+ };
85
+
86
+ export const getPostListByLatest = (context: ApiContext, from: number, to: number): Promise<PostType[]> => {
87
+ // const action: string = 'getListByLatest';
88
+ const {database} = context;
89
+ const limit: ArangoDBLimit = getLimit(from, to);
90
+ const aqlQry: string = `FOR p IN posts
91
+ FILTER p.privacy == "public" && !!p.parent == false
92
+ LET reactions = (
93
+ FOR post, r IN INBOUND p._id reactions
94
+ COLLECT reactionName = r.value INTO reactionItems
95
+ RETURN {name: reactionName, count: LENGTH(reactionItems[*].r.value)}
96
+ )
97
+ FOR u IN users
98
+ FILTER p.userId == u._key
99
+ ${limit.aql}
100
+ SORT p.added
101
+ RETURN DISTINCT MERGE(p, {user:u, reactions:reactions})`;
102
+
103
+ return useDb(database).query(aqlQry)
104
+ .then((cursor: ArrayCursor) => cursor.all())
105
+ .catch((error: Error) => {
106
+ throw error;
107
+ });
108
+ };
109
+
110
+ export const getPostListByTags = (context: ApiContext, tagNames: string[], from?: number, to?: number): Promise<PostType[]> => {
111
+ // const action: string = 'getListByTags';
112
+ const {database} = context;
113
+
114
+ return Promise.all(
115
+ tagNames.map((tagName: string) => {
116
+ const formatTagId: string = createHash(`tag-${tagName}`, null);
117
+ const limit: ArangoDBLimit = getLimit(from, to);
118
+ const aqlQry: string = `FOR p, e IN OUTBOUND "${`tags/${formatTagId}`}" isTagged
119
+ FOR u IN users
120
+ LET reactions = (
121
+ FOR post, r IN INBOUND p._id reactions
122
+ COLLECT reactionName = r.value INTO reactionItems
123
+ RETURN {name: reactionName, count: LENGTH(reactionItems[*].r.value)}
124
+ )
125
+ FILTER e.type == 'posts' && p.userId == u._key
126
+ ${limit.aql}
127
+ SORT p.added
128
+ RETURN DISTINCT MERGE(p, {user:u, reactions:reactions})`;
129
+
130
+ return useDb(database).query(aqlQry)
131
+ .then((cursor: ArrayCursor) => cursor.all())
132
+ .catch((error: Error) => {
133
+ throw error;
134
+ });
135
+ }))
136
+ .then((results) => uniqBy(flatten(results), '_key'))
137
+ .catch((error: Error) => {
138
+ throw error;
139
+ });
140
+ };
141
+
142
+ export const getPostListByUser = (context: ApiContext, userId: string, from: number, to: number): Promise<PostType[]> => {
143
+ // const action: string = 'getListByUser';
144
+ const {database} = context;
145
+ const formatUserId: string = parseId(userId);
146
+ const limit: ArangoDBLimit = getLimit(from, to);
147
+ const aqlQry: string = `FOR p IN posts
148
+ FILTER p.userId == "${formatUserId}" && !!p.parent == false
149
+ LET reactions = (
150
+ FOR post, r IN INBOUND p._id reactions
151
+ COLLECT reactionName = r.value INTO reactionItems
152
+ RETURN {name: reactionName, count: LENGTH(reactionItems[*].r.value)}
153
+ )
154
+ FOR u IN users
155
+ FILTER p.userId == u._key
156
+ ${limit.aql}
157
+ SORT p.added
158
+ RETURN DISTINCT MERGE(p, {user:u, reactions:reactions})`;
159
+
160
+ return useDb(database).query(aqlQry)
161
+ .then((cursor: ArrayCursor) => cursor.all())
162
+ .catch((error: Error) => {
163
+ throw error;
164
+ });
165
+ };
166
+
167
+ export const getPost = (context: ApiContext, itemId: string): Promise<PostType> => {
168
+ // const action: string = 'getItem';
169
+ const {database, userId: sessionId} = context;
170
+ const formatItemId: string = parseId(itemId);
171
+ const db = useDb(database);
172
+ const aqlQry: AqlQuery = aql`FOR p IN posts
173
+ FILTER p._key == ${formatItemId}
174
+ LIMIT 1
175
+ RETURN p`;
176
+
177
+ return db.query(aqlQry)
178
+ .then((cursor: ArrayCursor) => cursor.next())
179
+ .then((post: PostType = {}) => {
180
+ const {
181
+ _key,
182
+ groupId,
183
+ privacy = 'default'
184
+ }: PostType = post;
185
+
186
+ // Query based on privacy level
187
+ let privacyAqlQry: AqlQuery;
188
+
189
+ if(groupId && privacy === 'group') {
190
+ privacyAqlQry = aql`FOR p IN posts
191
+ FOR user IN users
192
+ FILTER p._key == ${_key} && user._key == p.userId
193
+ LET reactions = (
194
+ FOR post, r IN INBOUND p._id reactions
195
+ COLLECT reactionName = r.value INTO reactionItems
196
+ RETURN {name: reactionName, count: LENGTH(reactionItems[*].r.value)}
197
+ )
198
+ FOR group IN groups
199
+ FILTER group._key == p.groupId
200
+ FOR u, e IN OUTBOUND group._id isGrouped
201
+ FILTER u._key == ${sessionId}
202
+ LIMIT 1
203
+ RETURN MERGE(p, {user: user, reactions: reactions})`;
204
+ } else if(privacy === 'public') {
205
+ privacyAqlQry = aql`FOR p IN posts
206
+ FOR user IN users
207
+ FILTER p._key == ${_key} && user._key == p.userId
208
+ LET reactions = (
209
+ FOR post, r IN INBOUND p._id reactions
210
+ COLLECT reactionName = r.value INTO reactionItems
211
+ RETURN {name: reactionName, count: LENGTH(reactionItems[*].r.value)}
212
+ )
213
+ LIMIT 1
214
+ RETURN MERGE(p, {user: user, reactions: reactions})`;
215
+ }
216
+
217
+ if(privacyAqlQry) {
218
+ return db.query(privacyAqlQry)
219
+ .then((cursor: ArrayCursor) => cursor.next())
220
+ .then((filteredPost: PostType = {}) => filteredPost)
221
+ .catch((error: Error) => {
222
+ throw error;
223
+ });
224
+ }
225
+
226
+ return {};
227
+ })
228
+ .catch((error: Error) => {
229
+ throw error;
230
+ });
231
+ };
232
+
233
+ export const getPostComments = (context: ApiContext, itemId: string, from: number, to: number): Promise<PostType[]> => {
234
+ // const action: string = 'getComments';
235
+ const {database, userId: sessionId} = context;
236
+ const formatItemId: string = parseId(itemId);
237
+
238
+ // Get the parent post to get restrictions
239
+ const db = useDb(database);
240
+ const aqlQry: AqlQuery = aql`FOR p IN posts
241
+ FILTER p._key == ${formatItemId}
242
+ LIMIT 1
243
+ RETURN p`;
244
+
245
+ return db.query(aqlQry)
246
+ .then((cursor: ArrayCursor) => cursor.next())
247
+ .then((post: PostType = {}) => {
248
+ const {
249
+ _key,
250
+ groupId,
251
+ privacy = 'public'
252
+ }: PostType = post;
253
+
254
+ // Query based on privacy level
255
+ let privacyAqlQry: string;
256
+ const limit = getLimit(from, to);
257
+
258
+ if(groupId && privacy === 'group') {
259
+ privacyAqlQry = `FOR p IN posts
260
+ FOR user IN users
261
+ FILTER p.parent == "${_key}" && user._key == p.userId
262
+ LET reactions = (
263
+ FOR post, r IN INBOUND p._id reactions
264
+ COLLECT reactionName = r.value INTO reactionItems
265
+ RETURN {name: reactionName, count: LENGTH(reactionItems[*].r.value)}
266
+ )
267
+ FOR group IN groups
268
+ FILTER group._key == p.groupId
269
+ FOR u, e IN OUTBOUND group._id isGrouped
270
+ FILTER u._key == "${sessionId}"
271
+ SORT p.added
272
+ ${limit.aql}
273
+ RETURN MERGE(p, {user: user, reactions: reactions})`;
274
+ } else if(privacy === 'public') {
275
+ privacyAqlQry = `FOR p IN posts
276
+ FOR user IN users
277
+ FILTER p.parent == "${_key}" && user._key == p.userId
278
+ LET reactions = (
279
+ FOR post, r IN INBOUND p._id reactions
280
+ COLLECT reactionName = r.value INTO reactionItems
281
+ RETURN {name: reactionName, count: LENGTH(reactionItems[*].r.value)}
282
+ )
283
+ SORT p.added
284
+ ${limit.aql}
285
+ RETURN MERGE(p, {user: user, reactions: reactions})`;
286
+ }
287
+
288
+ if(privacyAqlQry) {
289
+ return db.query(privacyAqlQry)
290
+ .then((cursor: ArrayCursor) => cursor.all())
291
+ .catch((error: Error) => {
292
+ throw error;
293
+ });
294
+ }
295
+
296
+ return [];
297
+ })
298
+ .catch((error: Error) => {
299
+ throw error;
300
+ });
301
+ };
302
+
303
+ export const addPost = (context: ApiContext, item: PostType): Promise<PostType> => {
304
+ // const action: string = 'add';
305
+ const {database, userId: sessionId} = context;
306
+
307
+ const {
308
+ groupId = '',
309
+ name = '',
310
+ parentId = '',
311
+ privacy = 'public',
312
+ text = '',
313
+ title = ''
314
+ }: PostType = item;
315
+
316
+ const now: number = Date.now();
317
+
318
+ const insert: PostType = {
319
+ _key: createHash(`post-${sessionId}`),
320
+ added: now,
321
+ groupId: parseId(groupId),
322
+ modified: now,
323
+ name: parseString(name, 160),
324
+ parentId: parseId(parentId),
325
+ privacy: parseVarChar(privacy, 16),
326
+ text: parseString(text, 20000),
327
+ title: parseString(title, 160),
328
+ userId: sessionId
329
+ };
330
+ const db: Database = useDb(database);
331
+ const aqlQry: AqlQuery = aql`INSERT ${insert} IN posts RETURN NEW`;
332
+
333
+ return db.query(aqlQry)
334
+ .then((cursor: ArrayCursor) => cursor.next())
335
+ .then((post: PostType = {}) => {
336
+ const {_key: postKey} = post;
337
+
338
+ // Update linked tags within posts
339
+ return extractTags(db, 'posts', postKey, insert.text)
340
+ .then((tagList: TagType[]) => {
341
+ post.tags = tagList;
342
+ return post;
343
+ });
344
+ })
345
+ .catch((error: Error) => {
346
+ throw error;
347
+ });
348
+ };
349
+
350
+ export const updatePost = (context: ApiContext, item: PostType): Promise<PostType> => {
351
+ // const action: string = 'update';
352
+ const {database, userId: sessionId} = context;
353
+ const now: number = Date.now();
354
+ const {
355
+ groupId,
356
+ id,
357
+ name,
358
+ parentId,
359
+ privacy,
360
+ text
361
+ }: PostType = item;
362
+
363
+ const updatedPost: PostType = {
364
+ modified: now
365
+ };
366
+
367
+ if(name) {
368
+ updatedPost.name = parseString(name, 160);
369
+ }
370
+
371
+ if(text) {
372
+ updatedPost.text = parseString(text, 640);
373
+ }
374
+
375
+ if(privacy) {
376
+ updatedPost.privacy = parseVarChar(privacy, 16);
377
+ }
378
+
379
+ if(parent) {
380
+ updatedPost.parentId = parseId(parentId);
381
+ }
382
+ const update: any = updatedPost;
383
+
384
+ let formatId: string = parseId(id);
385
+ formatId = formatId === '' ? createHash(`post-${sessionId}`) : formatId;
386
+ const formatGroupId: string = parseId(groupId);
387
+ const insert: any = {
388
+ ...update,
389
+ _key: formatId,
390
+ added: now,
391
+ groupId: formatGroupId,
392
+ privacy,
393
+ userId: sessionId
394
+ };
395
+ const db: Database = useDb(database);
396
+ const aqlQry: AqlQuery = aql`UPSERT {_key: ${id}, userId: ${sessionId}}
397
+ INSERT ${insert}
398
+ UPDATE ${update}
399
+ IN posts RETURN NEW`;
400
+
401
+ return db.query(aqlQry)
402
+ .then((cursor: ArrayCursor) => cursor.next())
403
+ .then((updatedPost: PostType = {}) => {
404
+ const {_key: updatedPostKey} = updatedPost;
405
+
406
+ // Update linked tags
407
+ return extractTags(db, 'posts', updatedPostKey, update.text || '')
408
+ .then((tagList = []) => {
409
+ updatedPost.tags = tagList;
410
+
411
+ // Update linked files
412
+ const files: FileType[] = updatedPost.files || [];
413
+
414
+ if(files.length) {
415
+ return updateFiles(db, id, files)
416
+ .then((fileList = []) => {
417
+ updatedPost.files = fileList;
418
+ return updatedPost;
419
+ });
420
+ }
421
+
422
+ updatedPost.files = [];
423
+ return updatedPost;
424
+ });
425
+ })
426
+ .catch((error: Error) => {
427
+ throw error;
428
+ });
429
+ };
430
+
431
+ export const deletePost = (context: ApiContext, itemId: string): Promise<PostType> => {
432
+ // const action: string = 'delete';
433
+ const {database, userId: sessionId} = context;
434
+ const formatItemId: string = parseId(itemId);
435
+ const db: Database = useDb(database);
436
+ const aqlQry = aql`FOR p IN posts
437
+ FILTER p._key == ${formatItemId} && p.userId == ${sessionId}
438
+ LIMIT 1
439
+ REMOVE p IN posts
440
+ RETURN OLD`;
441
+
442
+ return db.query(aqlQry)
443
+ .then((cursor: ArrayCursor) => cursor.next())
444
+ .then((post: PostType = {}) => {
445
+ if(post) {
446
+ // Remove tag links
447
+ const edgeAqlQry: AqlQuery = aql`FOR t IN isTagged
448
+ FILTER t._to == ${formatItemId}
449
+ REMOVE t IN isTagged`;
450
+
451
+ return db.query(edgeAqlQry)
452
+ .then(() => {
453
+ // Remove attached files
454
+ const fileAqlQry: AqlQuery = aql`FOR f IN hasFile
455
+ FILTER f._to == ${formatItemId}
456
+ REMOVE f IN hasFile`;
457
+
458
+ return db.query(fileAqlQry)
459
+ .then(() => post)
460
+ .catch((error: Error) => {
461
+ throw error;
462
+ });
463
+ })
464
+ .catch((error: Error) => {
465
+ throw error;
466
+ });
467
+ }
468
+ return {};
469
+ })
470
+ .catch((error: Error) => {
471
+ throw error;
472
+ });
473
+ };
474
+
475
+ export const cleanPosts = (database: string): Promise<number> => {
476
+ // Remove all messages that are over 60 days and not saved
477
+ const aqlQry: AqlQuery = aql`FOR p IN posts
478
+ FILTER p.added < DATE_TIMESTAMP(DATE_SUBTRACT(DATE_NOW(), 60, 'day')) && p.type == 1
479
+ REMOVE p IN posts
480
+ RETURN OLD`;
481
+
482
+ return useDb(database).query(aqlQry)
483
+ .then((cursor: ArrayCursor) => cursor.all())
484
+ .then((results: PostType[] = []) => results.length)
485
+ .catch((error: Error) => {
486
+ throw error;
487
+ });
488
+ }
489
+
490
+ export const createPostEdge = (db: Database, file: FileType, postId: string): Promise<FileType> => {
491
+ const edgeCollection = db.edgeCollection('isPosted');
492
+ const fileId: string = parseId(file.id);
493
+ const edgeId: string = createHash(`file-${postId}-${fileId}`);
494
+ const formatPostId: string = parseId(postId);
495
+ const fileType: string = parseChar(file.fileType, 16);
496
+
497
+ const edge: any = {
498
+ _key: edgeId,
499
+ added: Date.now(),
500
+ type: fileType
501
+ };
502
+
503
+ return edgeCollection.save(edge, `posts/${formatPostId}`, `files/${fileId}`)
504
+ .then(() => file)
505
+ .catch((error: Error) => {
506
+ throw error;
507
+ });
508
+ };
@@ -0,0 +1,186 @@
1
+ import {createHash, parseChar, parseId, parseNum} from '@nlabs/utils';
2
+ import {aql, Database, EdgeCollection} from 'arangojs';
3
+ import {AqlQuery} from 'arangojs/lib/cjs/aql-query';
4
+ import {ArrayCursor} from 'arangojs/lib/cjs/cursor';
5
+
6
+ import {ApiContext, QueryFilter, ReactionType, UserReactionQuery, UserReactionType, UserType} from '../types';
7
+ import {logError, logException, useDb} from '../utils';
8
+
9
+ const eventCategory: string = 'reactions';
10
+
11
+ export const addGroupReaction = (context: ApiContext, params: UserReactionType = {}): Promise<boolean> => {
12
+ const action: string = 'reaction';
13
+ const {database, userId: sessionId} = context;
14
+
15
+ if(!sessionId) {
16
+ return logException({
17
+ action,
18
+ category: eventCategory,
19
+ label: 'unauthorized',
20
+ value: 'invalid_session'
21
+ }, context).then(() => null);
22
+ }
23
+
24
+ const {id: itemId, value: itemValue = 'like'} = params;
25
+ const formatItemId: string = parseId(itemId);
26
+ const formatValue: string = parseChar(itemValue, 32);
27
+ const edgeCollection = useDb(database).edgeCollection('hasReaction');
28
+
29
+ // Remove existing likes
30
+ const groupDocId: string = `groups/${formatItemId}`;
31
+ const userDocId: string = `users/${sessionId}`;
32
+ const edgeId: string = createHash(`reaction-${formatItemId}-${sessionId}`);
33
+ const edge: any = {
34
+ _key: edgeId,
35
+ added: Date.now(),
36
+ type: 'group',
37
+ value: formatValue
38
+ };
39
+
40
+ return edgeCollection.save(edge, userDocId, groupDocId).then(() => true);
41
+ };
42
+
43
+ export const getGroupReactions = (context: ApiContext, params: UserReactionType = {}): Promise<UserReactionType> => {
44
+ const action: string = 'getReactions';
45
+ const {database} = context;
46
+ const {id: groupId}: UserReactionType = params;
47
+ const groupDocId: string = `groups/${parseId(groupId)}`;
48
+
49
+ // Query
50
+ const aqlQry: AqlQuery = aql`FOR g, r IN INBOUND ${groupDocId} hasReaction
51
+ COLLECT reactionName = r.value INTO reactionItems
52
+ RETURN {value: reactionName, count: LENGTH(reactionItems[*].r.value)}`;
53
+
54
+ return useDb(database).query(aqlQry)
55
+ .then((cursor: ArrayCursor) => cursor.all())
56
+ .catch((error: Error) => logError({
57
+ action,
58
+ category: eventCategory,
59
+ label: 'db_error'
60
+ }, error, context).then(() => null));
61
+ };
62
+
63
+ export const getReactionsByUser = (context: ApiContext, groupId: string): Promise<UserReactionType[]> => {
64
+ const action: string = 'getReactionsByUser';
65
+ const {database, userId: sessionId} = context;
66
+ const groupDocId: string = `groups/${parseId(groupId)}`;
67
+ const userDocId: string = `users/${sessionId}`;
68
+
69
+ // Query
70
+ const aqlQry: AqlQuery = aql`FOR g, r IN INBOUND ${groupDocId} hasReaction
71
+ FILTER r._from == ${userDocId}
72
+ COLLECT reactionName = r.value INTO reactionItems
73
+ RETURN {value: reactionName, count: LENGTH(reactionItems[*].r.value)}`;
74
+
75
+ return useDb(database).query(aqlQry)
76
+ .then((cursor: ArrayCursor) => cursor.all())
77
+ .catch((error: Error) => logError({
78
+ action,
79
+ category: eventCategory,
80
+ label: 'db_error'
81
+ }, error, context).then(() => null));
82
+ };
83
+
84
+ export const getUsersByReaction = (context: ApiContext, params: UserReactionQuery = {}): Promise<UserType[]> => {
85
+ const action: string = 'getUsersByReaction';
86
+ const {database, userId: sessionId} = context;
87
+
88
+ if(!sessionId) {
89
+ return logException({
90
+ action,
91
+ category: eventCategory,
92
+ label: 'unauthorized',
93
+ value: 'invalid_session'
94
+ }, context).then(() => null);
95
+ }
96
+
97
+ const {filters = [], id: groupId, value}: UserReactionQuery = params;
98
+ const reaction = parseChar(value, 32);
99
+ const groupDocId = `groups/${parseId(groupId)}`;
100
+ const filterStr: string = filters
101
+ .map((filter: QueryFilter) => {
102
+ const {conditional, name, value: queryValue}: QueryFilter = filter;
103
+ let filterCond: string = conditional;
104
+
105
+ if(conditional !== '>=' && conditional !== '<=' && conditional !== '>' && conditional !== '<') {
106
+ filterCond = '==';
107
+ }
108
+
109
+ switch(name) {
110
+ case 'added':
111
+ return `r.added ${filterCond} ${parseNum(queryValue)}`;
112
+ default:
113
+ return '';
114
+ }
115
+ })
116
+ .concat([
117
+ `r.value == "${reaction}"`,
118
+ 'u._id == r._from'
119
+ ])
120
+ .join(' && ');
121
+
122
+ // Query
123
+ const aqlQry: string = `FOR g, r IN INBOUND "${groupDocId}" hasReaction
124
+ FOR u IN users
125
+ FILTER ${filterStr}
126
+ RETURN u`;
127
+
128
+ return useDb(database).query(aqlQry)
129
+ .then((cursor: ArrayCursor) => cursor.all())
130
+ .catch((error: Error) => logError({
131
+ action,
132
+ category: eventCategory,
133
+ label: 'db_error'
134
+ }, error, context).then(() => null));
135
+ };
136
+
137
+
138
+ export const postReaction = (context: ApiContext, postId: string, type: string = 'like'): Promise<ReactionType> => {
139
+ const {database, userId: sessionId} = context;
140
+ const formatItemId: string = parseId(postId);
141
+ const db: Database = useDb(database);
142
+ const edgeCollection: EdgeCollection = db.edgeCollection('reactions');
143
+ const now: number = Date.now();
144
+
145
+ // Remove existing reaction to post
146
+ const postDocId: string = `posts/${formatItemId}`;
147
+ const userDocId: string = `users/${sessionId}`;
148
+ const aqlQry: AqlQuery = aql`FOR p, r IN INBOUND ${postDocId} reaction
149
+ FILTER r.type == "posts" && r._from == ${userDocId}
150
+ REMOVE r IN reactions
151
+ RETURN r`;
152
+
153
+ return db.query(aqlQry)
154
+ .then(() => {
155
+ const edgeId = createHash(`reaction-${postId}-${sessionId}`);
156
+ const edge: any = {
157
+ _key: edgeId,
158
+ added: now,
159
+ type: 'posts',
160
+ value: type
161
+ };
162
+
163
+ return edgeCollection.save(edge, userDocId, postDocId)
164
+ .then(() => {
165
+ const reactionAqlQry: AqlQuery = aql`LET reactions = (
166
+ FOR post, r IN INBOUND p._id reactions
167
+ COLLECT reactionName = r.value INTO reactionItems
168
+ RETURN {id: p._id, type: reactionName, count: LENGTH(reactionItems[*].r.value)}
169
+ )
170
+ RETURN reactions`;
171
+
172
+ return db.query(reactionAqlQry)
173
+ .then((cursor: ArrayCursor) => cursor.next())
174
+ .then((result = {reactions: []}) => result)
175
+ .catch((error: Error) => {
176
+ throw error;
177
+ });
178
+ })
179
+ .catch((error: Error) => {
180
+ throw error;
181
+ });
182
+ })
183
+ .catch((error: Error) => {
184
+ throw error;
185
+ });
186
+ };