@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,352 @@
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 {get as httpGet} from '@nlabs/rip-hunter';
6
+ import {createHash, parseId, parseString} from '@nlabs/utils';
7
+ import {aql, Database} from 'arangojs';
8
+ import {AqlQuery} from 'arangojs/lib/cjs/aql-query';
9
+ import {ArrayCursor} from 'arangojs/lib/cjs/cursor';
10
+ import {google} from 'googleapis';
11
+ import {UserError} from 'graphql-errors';
12
+ import * as request from 'request-promise';
13
+ import {ApiContext} from 'types/auth';
14
+
15
+ import {Config} from '../config';
16
+ import {FileType} from '../types/files';
17
+ import {useDb} from '../utils';
18
+ import {resizeSaveImage} from './images';
19
+ import {createPostEdge} from './posts';
20
+
21
+ const youtube = google.youtube({auth: Config.get('google.key'), version: 'v3'});
22
+ request.defaults({encoding: null});
23
+
24
+ // const eventCategory: string = 'files';
25
+
26
+ // Upload file
27
+ export const addFile = (context: ApiContext, item: FileType = {}): Promise<FileType> => {
28
+ const {appId: sessionApp, database, userId: sessionId, userType} = context;
29
+ const {
30
+ description = '',
31
+ fileType = '',
32
+ id,
33
+ name = '',
34
+ url = ''
35
+ } = item;
36
+
37
+ // Id
38
+ const fileId: string = id ? parseId(id) : createHash(`file-${sessionId}-${sessionApp}`);
39
+
40
+ // Name
41
+ const isUrl: boolean = url !== '';
42
+
43
+ // If no name, get it from url path
44
+ let formatName: string = parseString(name, 160);
45
+ let formatType: string = parseString(fileType, 16);
46
+
47
+ if(formatName === '' && isUrl) {
48
+ formatName = url.substring(url.lastIndexOf('/') + 1);
49
+ }
50
+
51
+ if(formatType === '') {
52
+ const nameArr: string[] = formatName.split('.');
53
+ const ext: string = nameArr[nameArr.length - 1];
54
+
55
+ switch(ext) {
56
+ case 'jpeg':
57
+ case 'jpg':
58
+ formatType = 'image/jpeg';
59
+ break;
60
+ case 'png':
61
+ formatType = 'image/png';
62
+ break;
63
+ case 'zip':
64
+ formatType = 'application/zip';
65
+ break;
66
+ default:
67
+ break;
68
+ }
69
+ }
70
+
71
+ let isImage: boolean;
72
+
73
+ switch(formatType) {
74
+ case 'image/jpeg':
75
+ case 'image/png':
76
+ isImage = true;
77
+ break;
78
+ default:
79
+ isImage = false;
80
+ break;
81
+ }
82
+
83
+ // Description
84
+ const formatDesc: string = parseString(description, 500);
85
+
86
+ // Only allow file uploads to premium users
87
+ if(!isImage && userType !== 2) {
88
+ throw new UserError('account_restriction');
89
+ }
90
+
91
+ const saveToDb = (insert: FileType) => {
92
+ const aqlQry: AqlQuery = aql`INSERT ${insert} IN files RETURN NEW`;
93
+
94
+ return useDb(database).query(aqlQry)
95
+ .then((cursor: ArrayCursor) => cursor.next())
96
+ .then((file = {}) => file)
97
+ .catch((error: Error) => {
98
+ throw error;
99
+ });
100
+ };
101
+
102
+ const uploadFile = (buf: Buffer, uploadType: string) => {
103
+ const now: number = Date.now();
104
+
105
+ // If image, resize and create a thumbnail
106
+ if(isImage) {
107
+ return resizeSaveImage(sessionId, fileId, buf, uploadType)
108
+ .then((resizedImage: FileType) => {
109
+ const insert: FileType = {
110
+ ...resizedImage,
111
+ _key: fileId,
112
+ added: now,
113
+ description: formatDesc,
114
+ fileType: formatType,
115
+ modified: now,
116
+ name: formatName,
117
+ userId: sessionId
118
+ };
119
+
120
+ return saveToDb(insert);
121
+ })
122
+ .catch((error: Error) => {
123
+ throw error;
124
+ });
125
+ }
126
+ const insert: FileType = {
127
+ _key: fileId,
128
+ added: now,
129
+ description: formatDesc,
130
+ fileType: formatType,
131
+ modified: now,
132
+ name: formatName,
133
+ userId: sessionId
134
+ };
135
+
136
+ return saveToDb(insert);
137
+ };
138
+
139
+ // If file is a url path, download the file and save
140
+ if(isUrl) {
141
+ return request.get({encoding: null, uri: url})
142
+ .then((body) => uploadFile(new Buffer(body, 'binary'), formatType))
143
+ .catch(() => {
144
+ throw new UserError('file_request');
145
+ });
146
+ } else if(item.base64 !== '') {
147
+ const buffer: Buffer = new Buffer(item.base64);
148
+ return uploadFile(buffer, formatType);
149
+ }
150
+ throw new Error('file_required');
151
+ };
152
+
153
+ // Giphy
154
+ export const getGiphyTrends = (context: ApiContext, limit: number = 30): Promise<any[]> => {
155
+ const gifUrl: string = `http://api.giphy.com/v1/gifs/trending?api_key=${Config.get('giphy.key')}&limit=${limit}`;
156
+
157
+ return httpGet(gifUrl)
158
+ .then((res: Response) => res.json())
159
+ .then((json) => json.data.map((gifImage = {id: null, images: null}) => {
160
+ const {
161
+ id,
162
+ images: {
163
+ original: {url = ''} = {},
164
+ fixed_height_small: {url: thumb = ''} = {}
165
+ } = {}
166
+ } = gifImage;
167
+
168
+ return {
169
+ id,
170
+ thumb,
171
+ type: 'giphy',
172
+ url
173
+ };
174
+ }));
175
+ };
176
+
177
+ export const getGiphySearch = (context: ApiContext, query: string, limit: number = 30): Promise<any[]> => {
178
+ const formatQuery: string = encodeURI(query);
179
+ const gifUrl: string = `http://api.giphy.com/v1/gifs/search?q=${formatQuery}&api_key=${Config.get('giphy.key')}&limit=${limit}`;
180
+
181
+ return fetch(gifUrl)
182
+ .then((res: Response) => res.json())
183
+ .then((json) => json.data.map((gifImage = {id: null, images: null}) => {
184
+ const {
185
+ id,
186
+ images: {
187
+ original: {url = ''} = {},
188
+ fixed_height_small: {url: thumb = ''} = {}
189
+ } = {}
190
+ } = gifImage;
191
+
192
+ return {
193
+ id,
194
+ thumb,
195
+ type: 'giphy',
196
+ url
197
+ };
198
+ }));
199
+ };
200
+
201
+ export const getYouTubeTrends = (context: ApiContext, limit: number = 30): Promise<any[]> => {
202
+ return new Promise((resolve, reject) => {
203
+ youtube.videos.list({
204
+ chart: 'mostPopular',
205
+ maxResults: limit,
206
+ part: 'snippet',
207
+ regionCode: 'US'
208
+ }, (error: Error, data: any) => {
209
+ if(error) {
210
+ console.error(error);
211
+ reject(new Error(error[0].message));
212
+ } else if(data) {
213
+ const list = data.items.map((item) => ({
214
+ id: item.id,
215
+ thumb: item.snippet.thumbnails.high.url,
216
+ type: 'youtube',
217
+ url: `http://www.youtube.com/embed/${item.id}`
218
+ }));
219
+
220
+ resolve(list);
221
+ }
222
+ });
223
+ });
224
+ };
225
+
226
+ export const getYouTubeSearch = (context: ApiContext, query: string, limit: number = 30): Promise<any[]> => {
227
+ return new Promise((resolve, reject) => {
228
+ youtube.search.list({
229
+ maxResults: limit,
230
+ part: 'snippet',
231
+ q: query,
232
+ regionCode: 'US'
233
+ }, (error: Error, data: any) => {
234
+ if(error) {
235
+ console.error(error);
236
+ reject(new Error(error[0].message));
237
+ } else if(data) {
238
+ const {items} = data;
239
+ const list = items.map((item) => ({
240
+ id: item.id,
241
+ thumb: item.snippet.thumbnails.high.url,
242
+ type: 'youtube',
243
+ url: `http://www.youtube.com/embed/${item.id}`
244
+ }));
245
+
246
+ resolve(list);
247
+ }
248
+ });
249
+ });
250
+ };
251
+
252
+ // Files
253
+ export const getPathUserFiles = (userId: string, filename: string): string => {
254
+ return `users/${userId}/files/${filename}`;
255
+ };
256
+
257
+ export const getUrlUserFiles = (userId: string, filename: string, dir: string = 'files', type: string = 'profile'): string => {
258
+ if(filename) {
259
+ return `https://box.${Config.get('app.url')}/users/${userId}/${dir}/${filename}`;
260
+ }
261
+
262
+ if(type === 'profile') {
263
+ return `https://box.${Config.get('app.url')}/defaults/user_bk.jpg`;
264
+ }
265
+
266
+ return `https://box.${Config.get('app.url')}/defaults/user_wh.jpg`;
267
+ };
268
+
269
+ export const linkFiles = (db: Database, files: FileType[], postId: string): Promise<any> => {
270
+ return Promise.all(
271
+ files.map((file: FileType) => createFile(db, file)
272
+ .then((file: FileType) => createPostEdge(db, file, postId)))
273
+ );
274
+ };
275
+
276
+ export const updateFiles = (db: Database, postId: string, files: FileType[]): Promise<any> => {
277
+ const edgeCollection = db.edgeCollection('isPosted');
278
+
279
+ return edgeCollection.inEdges(postId)
280
+ .then((edges) => {
281
+ if(edges.length) {
282
+ // Remove linked edges
283
+ return Promise.all(
284
+ edges.map((edge) => {
285
+ const {_key: edgeKey} = edge;
286
+ const aqlQry: AqlQuery = aql`REMOVE {_key:${edgeKey}} IN isPosted`;
287
+
288
+ return db.query(aqlQry).catch((error: Error) => {
289
+ throw error;
290
+ });
291
+ }))
292
+ .then(() => {
293
+ if(files.length) {
294
+ // Link files
295
+ return linkFiles(db, files, postId).then(() => files);
296
+ }
297
+ return files;
298
+ });
299
+ } else if(files.length) {
300
+ // Link files
301
+ return linkFiles(db, files, postId).then(() => files);
302
+ }
303
+ return files;
304
+ })
305
+ .catch((error: Error) => {
306
+ throw error;
307
+ });
308
+ };
309
+
310
+ export const createFile = (db: Database, file: FileType): Promise<FileType> => {
311
+ const insert: any = {
312
+ _key: file.id,
313
+ added: Date.now()
314
+ };
315
+
316
+ const aqlQry: AqlQuery = aql`UPSERT {_key: ${file.id}}
317
+ INSERT ${insert}
318
+ UPDATE {}
319
+ IN files RETURN NEW`;
320
+
321
+ return db.query(aqlQry)
322
+ .then((cursor: ArrayCursor) => cursor.next())
323
+ .then((updatedFile: FileType = {}) => updatedFile)
324
+ .catch((error: Error) => {
325
+ throw error;
326
+ });
327
+ };
328
+
329
+ export const encodeBase64 = (buffer: Buffer): string => {
330
+ return new Buffer(buffer).toString('base64');
331
+ }
332
+
333
+ export const decodeBase64 = (dataString: string): object => {
334
+ // const getData = (str: string) => str.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/) || [];
335
+ const getData = (str: string) => str.match(/^data:([A-Za-z-+/]+);base64,(.+)$/) || [];
336
+ let matches = getData(dataString);
337
+
338
+ if(matches.length !== 3) {
339
+ // If invalid make sure we don't need to decode
340
+ matches = getData(decodeURIComponent(dataString));
341
+
342
+ // Check it again.
343
+ if(matches.length !== 3) {
344
+ throw Error('Invalid input string');
345
+ }
346
+ }
347
+
348
+ return {
349
+ data: new Buffer(matches[2], 'base64'),
350
+ type: matches[1]
351
+ };
352
+ };
@@ -0,0 +1,308 @@
1
+ import {createHash, parseChar, parseId} from '@nlabs/utils';
2
+ import {aql, Database} from 'arangojs';
3
+ import {AqlQuery} from 'arangojs/lib/cjs/aql-query';
4
+ import {ArrayCursor} from 'arangojs/lib/cjs/cursor';
5
+ import flatten from 'lodash/flatten';
6
+ import uniqBy from 'lodash/uniqBy';
7
+
8
+ import {ArangoDBLimit} from '../types/arangodb';
9
+ import {ApiContext} from '../types/auth';
10
+ import {GroupEdgeType, GroupType, GroupUserType} from '../types/groups';
11
+ import {getLimit, logError, useDb} from '../utils';
12
+ import {extractTags} from './tags';
13
+
14
+ /**
15
+ * Copyright (c) 2019-Present, Nitrogen Labs, Inc.
16
+ * Copyrights licensed under the MIT License. See the accompanying LICENSE file for terms.
17
+ */
18
+
19
+ const eventCategory: string = 'groups';
20
+
21
+ export const getGroupList = (context: ApiContext, from: number, to: number): Promise<GroupType[]> => {
22
+ const action: string = 'getListByApp';
23
+ const {database} = context;
24
+ const limit: ArangoDBLimit = getLimit(from, to);
25
+ const aqlQry: string = `FOR g in groups
26
+ LET users = (
27
+ FOR u, l IN OUTBOUND g._id isGrouped
28
+ RETURN MERGE (u, {type:l.type})
29
+ )
30
+ ${limit.aql}
31
+ SORT g.added
32
+ RETURN DISTINCT MERGE(g, {users:users})`;
33
+
34
+ return useDb(database).query(aqlQry)
35
+ .then((cursor: ArrayCursor) => cursor.all())
36
+ .catch((error: Error) => logError({
37
+ action,
38
+ category: eventCategory,
39
+ label: 'db_error'
40
+ }, error, context).then(() => null));
41
+ };
42
+
43
+ export const getGroupListByUser = (context: ApiContext, from: number, to: number): Promise<GroupType[]> => {
44
+ // const action: string = 'getListByUser';
45
+ const {database, userId: sessionId} = context;
46
+ const limit: ArangoDBLimit = getLimit(from, to);
47
+ const aqlQry: string = `FOR g, e IN INBOUND "${`users/${sessionId}`}" isGrouped
48
+ LET users = (
49
+ FOR u, l IN OUTBOUND g._id isGrouped
50
+ RETURN MERGE (u, {type:l.type})
51
+ )
52
+ ${limit.aql}
53
+ SORT g.added
54
+ RETURN DISTINCT MERGE(g, {users:users})`;
55
+
56
+ return useDb(database).query(aqlQry)
57
+ .then((cursor: ArrayCursor) => cursor.all())
58
+ .catch((error: Error) => {
59
+ throw error;
60
+ });
61
+ };
62
+
63
+ export const getGroupListByTags = (context: ApiContext, tags: string[], from: number, to: number): Promise<GroupType[]> => {
64
+ const action: string = 'getListByTags';
65
+ const {database} = context;
66
+
67
+ return Promise.all(
68
+ (tags || []).map((tagName: string) => {
69
+ const formatTagName: string = parseId(tagName);
70
+ const limit: ArangoDBLimit = getLimit(from, to);
71
+ const aqlQry: string = `FOR p, e IN OUTBOUND "${`tags/${formatTagName}`}" isTagged
72
+ FOR u IN users
73
+ LET likes = (
74
+ FOR post, like IN INBOUND p._id likes
75
+ FILTER like.value == 'like'
76
+ RETURN like
77
+ )
78
+ LET dislikes = (
79
+ FOR post, like IN INBOUND p._id likes
80
+ FILTER like.value == 'dislike'
81
+ RETURN like
82
+ )
83
+ FILTER e.type == 'post' && p.userId == u._key
84
+ ${limit.aql}
85
+ SORT p.added
86
+ RETURN DISTINCT MERGE(p, {user:u, likes:LENGTH(likes), dislikes:LENGTH(dislikes)})`;
87
+
88
+ return useDb(database).query(aqlQry).then((cursor: ArrayCursor) => cursor.all());
89
+ })
90
+ )
91
+ .then((results) => uniqBy(flatten(results), '_key'))
92
+ .catch((error: Error) => logError({
93
+ action,
94
+ category: eventCategory,
95
+ label: 'db_error'
96
+ }, error, context).then(() => null));
97
+ };
98
+
99
+ export const getGroup = (context: ApiContext, itemId: string): Promise<GroupType> => {
100
+ const action: string = 'getItem';
101
+ const {database, userId: sessionId} = context;
102
+ const formatItemId: string = parseId(itemId);
103
+
104
+ const aqlQry: AqlQuery = aql`FOR g, e IN INBOUND ${`users/${sessionId}`} isGrouped
105
+ FILTER g._key == ${formatItemId}
106
+ LET users = (
107
+ FOR u, l IN OUTBOUND g._id isGrouped
108
+ RETURN MERGE (u, {type:l.type})
109
+ )
110
+ LIMIT 1
111
+ RETURN DISTINCT MERGE(g, {users:users})`;
112
+
113
+ return useDb(database).query(aqlQry)
114
+ .then((cursor: ArrayCursor) => cursor.next())
115
+ .then((group: GroupType = {}) => group)
116
+ .catch((error: Error) => logError({
117
+ action,
118
+ category: eventCategory,
119
+ label: 'db_error'
120
+ }, error, context).then(() => null));
121
+ };
122
+
123
+ export const getGroupDetails = (context: ApiContext, groupId: string): Promise<GroupType> => {
124
+ const action: string = 'getDetails';
125
+ const {database} = context;
126
+ const formatGroupId: string = parseId(groupId);
127
+ const aqlQry: AqlQuery = aql`FOR g IN groups
128
+ FILTER g._key == ${formatGroupId}
129
+ LIMIT 1
130
+ RETURN g`;
131
+
132
+ return useDb(database).query(aqlQry)
133
+ .then((cursor: ArrayCursor) => cursor.next())
134
+ .then((group: GroupType = {}) => group)
135
+ .catch((error: Error) => logError({
136
+ action,
137
+ category: eventCategory,
138
+ label: 'db_error'
139
+ }, error, context).then(() => null));
140
+ };
141
+
142
+ export const addGroup = (context: ApiContext, item: GroupType = {}): Promise<GroupType> => {
143
+ const action: string = 'add';
144
+ const {database, userId: sessionId} = context;
145
+ const now: number = Date.now();
146
+ const {
147
+ description = '',
148
+ name = 'Untitled',
149
+ type
150
+ }: GroupType = item;
151
+ const id: string = createHash(`group-${sessionId}`);
152
+ const formatDesc: string = description.substr(0, 640);
153
+ const formatType: string = parseChar(type, 16) || 'private';
154
+ const insert: GroupType = {
155
+ _key: id,
156
+ added: now,
157
+ description: formatDesc,
158
+ modified: now,
159
+ name,
160
+ type: formatType
161
+ };
162
+ const db: Database = useDb(database);
163
+ const aqlQry: AqlQuery = aql`INSERT ${insert} IN groups RETURN NEW`;
164
+
165
+ return db.query(aqlQry)
166
+ .then((cursor: ArrayCursor) => cursor.next())
167
+ .then((group: GroupType = {}) => {
168
+ const itemType: string = 'groups';
169
+
170
+ // Update linked tags
171
+ const {_key: groupId}: GroupType = group;
172
+ return extractTags(db, itemType, groupId, formatDesc)
173
+ .then(() => createGroupEdge(database, sessionId, groupId, 'admin').then(() => group));
174
+ })
175
+ .catch((error: Error) => logError({
176
+ action,
177
+ category: eventCategory,
178
+ label: 'db_error'
179
+ }, error, context).then(() => null));
180
+ };
181
+
182
+ export const updateGroup = (context: ApiContext, item: GroupType = {}): Promise<GroupType> => {
183
+ const action: string = 'update';
184
+ const {database} = context;
185
+ const {
186
+ description = '',
187
+ id,
188
+ name = 'Untitled'
189
+ }: GroupType = item;
190
+ const itemId: string = parseId(id);
191
+ const now: number = Date.now();
192
+ const formatDesc: string = description.substr(0, 640);
193
+ const update: any = {
194
+ description: formatDesc,
195
+ modified: now,
196
+ name
197
+ };
198
+ const db: Database = useDb(database);
199
+ const aqlQry: AqlQuery = aql`UPDATE ${itemId} WITH ${update} IN groups RETURN NEW`;
200
+
201
+ return db.query(aqlQry)
202
+ .then((cursor: ArrayCursor) => cursor.next())
203
+ .then((group: GroupType = {}) => {
204
+ const {_key: groupKey} = group;
205
+ const itemType: string = 'groups';
206
+
207
+ // Update linked tags
208
+ return extractTags(db, itemType, groupKey, description).then(() => group);
209
+ })
210
+ .catch((error: Error) => logError({
211
+ action,
212
+ category: eventCategory,
213
+ label: 'db_error'
214
+ }, error, context).then(() => null));
215
+ };
216
+
217
+ export const deleteGroup = (context: ApiContext, itemId: string): Promise<GroupType> => {
218
+ const action: string = 'delete';
219
+ const {database} = context;
220
+ const formatItemId: string = parseId(itemId);
221
+ const aqlQry: AqlQuery = aql`FOR g IN groups
222
+ FILTER g._key == ${formatItemId}
223
+ REMOVE g IN groups
224
+ RETURN OLD`;
225
+
226
+ return useDb(database).query(aqlQry)
227
+ .then((cursor: ArrayCursor) => cursor.next())
228
+ .then((group: GroupType = {}) => group)
229
+ .catch((error: Error) => logError({
230
+ action,
231
+ category: eventCategory,
232
+ label: 'db_error'
233
+ }, error, context).then(() => null));
234
+ };
235
+
236
+ export const getGroupsByReaction = (context: ApiContext, reaction: string): Promise<GroupType[]> => {
237
+ const action: string = 'getGroupsByReaction';
238
+ const {database, userId: sessionId} = context;
239
+ const formatReaction: string = parseChar(reaction, 32);
240
+ const userDocId: string = `users/${sessionId}`;
241
+
242
+ // Query
243
+ const aqlQry: AqlQuery = aql`FOR u, r IN OUTBOUND ${userDocId} hasReaction
244
+ FILTER r.value == ${formatReaction}
245
+ COLLECT reactionName = r.value INTO reactionItems
246
+ RETURN {value: reactionName, count: LENGTH(reactionItems[*].r.value)}`;
247
+
248
+ return useDb(database).query(aqlQry)
249
+ .then((cursor: ArrayCursor) => cursor.all())
250
+ .catch((error: Error) => logError({
251
+ action,
252
+ category: eventCategory,
253
+ label: 'db_error'
254
+ }, error, context).then(() => null));
255
+ };
256
+
257
+ export const isGrouped = (database: string, userId: string, groupId: string): Promise<GroupUserType> => {
258
+ const action: string = 'isGrouped';
259
+ const formatUserId: string = parseId(userId);
260
+ const formatGroupId: string = parseId(groupId);
261
+ const aqlQry: AqlQuery = aql`FOR g IN groups
262
+ FILTER g._key == ${formatGroupId}
263
+ FOR u IN INBOUND g._id isGrouped
264
+ FILTER u._key == ${formatUserId}
265
+ LIMIT 1
266
+ RETURN u`;
267
+
268
+ return useDb(database).query(aqlQry)
269
+ .then((cursor: ArrayCursor) => cursor.all())
270
+ .then((results = []) => {
271
+ const isValid = !!results.length;
272
+
273
+ return {
274
+ groupId,
275
+ isValid,
276
+ userId
277
+ };
278
+ })
279
+ .catch((error: Error) => logError({
280
+ action,
281
+ category: eventCategory,
282
+ label: 'db_error'
283
+ }, error, {id: userId}).then(() => null));
284
+ };
285
+
286
+ export const createGroupEdge = (database: string, userId: string, groupId: string, type: string): Promise<GroupEdgeType> => {
287
+ const action: string = 'createGroupEdge';
288
+ const formatUserId: string = parseId(userId);
289
+ const formatGroupId: string = parseId(groupId);
290
+ const formatType: string = parseChar(type, 16);
291
+
292
+ const now: number = Date.now();
293
+ const edgeCollection = useDb(database).edgeCollection('isGrouped');
294
+ const edgeId: string = createHash(`group-${formatUserId}-${formatGroupId}`);
295
+
296
+ const edge: any = {
297
+ _key: edgeId,
298
+ added: now,
299
+ type: formatType
300
+ };
301
+
302
+ return edgeCollection.save(edge, `users/${formatUserId}`, `groups/${formatGroupId}`)
303
+ .catch((error: Error) => logError({
304
+ action,
305
+ category: eventCategory,
306
+ label: 'db_error'
307
+ }, error, {id: formatUserId}).then(() => null));
308
+ };