Преглед изворни кода

Merge branch 'match-noti' of fyindr/siimee into dev

tags/0.0.1^2
maeda пре 3 година
родитељ
комит
d26721c13a

+ 69
- 15
backend/db/data-generator/mock.js Прегледај датотеку

100
         {
100
         {
101
             response_key_id: 2,
101
             response_key_id: 2,
102
             response_key_category: 'openness',
102
             response_key_category: 'openness',
103
-            response_key_prompt: 'are you open with your emotions with everyone',
103
+            response_key_prompt:
104
+                'are you open with your emotions with everyone',
104
             response_key_description: 'first round draft scoring question',
105
             response_key_description: 'first round draft scoring question',
105
         },
106
         },
106
         {
107
         {
107
             response_key_id: 3,
108
             response_key_id: 3,
108
             response_key_category: 'bravery',
109
             response_key_category: 'bravery',
109
-            response_key_prompt: 'do you speak-up when you feel something is wrong',
110
+            response_key_prompt:
111
+                'do you speak-up when you feel something is wrong',
110
             response_key_description: 'first round draft scoring question',
112
             response_key_description: 'first round draft scoring question',
111
         },
113
         },
112
         {
114
         {
119
         {
121
         {
120
             response_key_id: 5,
122
             response_key_id: 5,
121
             response_key_category: 'honesty',
123
             response_key_category: 'honesty',
122
-            response_key_prompt: 'when telling a story do you exaggerate for dramatic effect',
124
+            response_key_prompt:
125
+                'when telling a story do you exaggerate for dramatic effect',
123
             response_key_description: 'first round draft scoring question',
126
             response_key_description: 'first round draft scoring question',
124
         },
127
         },
125
         {
128
         {
126
             response_key_id: 6,
129
             response_key_id: 6,
127
             response_key_category: 'respect',
130
             response_key_category: 'respect',
128
-            response_key_prompt: 'do you treat difficult people as well as you treat your close friends',
131
+            response_key_prompt:
132
+                'do you treat difficult people as well as you treat your close friends',
129
             response_key_description: 'first round draft scoring question',
133
             response_key_description: 'first round draft scoring question',
130
         },
134
         },
131
         {
135
         {
144
             response_key_id: 9,
148
             response_key_id: 9,
145
             response_key_category: 'profile',
149
             response_key_category: 'profile',
146
             response_key_prompt: 'language',
150
             response_key_prompt: 'language',
147
-            response_key_description: 'programming and spoken language preference',
151
+            response_key_description:
152
+                'programming and spoken language preference',
148
         },
153
         },
149
         {
154
         {
150
             response_key_id: 10,
155
             response_key_id: 10,
151
             response_key_category: 'profile',
156
             response_key_category: 'profile',
152
             response_key_prompt: 'duration',
157
             response_key_prompt: 'duration',
153
-            response_key_description: 'duration preference for hours able to dedicate to work',
158
+            response_key_description:
159
+                'duration preference for hours able to dedicate to work',
154
         },
160
         },
155
         {
161
         {
156
             response_key_id: 11,
162
             response_key_id: 11,
157
             response_key_category: 'profile',
163
             response_key_category: 'profile',
158
             response_key_prompt: 'presence',
164
             response_key_prompt: 'presence',
159
-            response_key_description: 'location preference for where work happens',
165
+            response_key_description:
166
+                'location preference for where work happens',
160
         },
167
         },
161
         {
168
         {
162
             response_key_id: 12,
169
             response_key_id: 12,
186
             response_key_id: 16,
193
             response_key_id: 16,
187
             response_key_category: 'profile',
194
             response_key_category: 'profile',
188
             response_key_prompt: 'distance',
195
             response_key_prompt: 'distance',
189
-            response_key_description: 'preference for commuting distance cutoff',
196
+            response_key_description:
197
+                'preference for commuting distance cutoff',
190
         },
198
         },
191
     ],
199
     ],
192
     responses: [],
200
     responses: [],
193
-    memberships: [
194
-       
195
-    ],
196
-    groupings: [
197
-       
198
-    ],
201
+    memberships: [],
202
+    groupings: [],
199
     messages: [],
203
     messages: [],
200
     match_queues: [
204
     match_queues: [
201
-        
205
+        { match_queue_id: 1, profile_id: 45, target_id: 62, is_deleted: false },
206
+        { match_queue_id: 2, profile_id: 62, target_id: 45, is_deleted: false },
207
+        { match_queue_id: 3, profile_id: 62, target_id: 46, is_deleted: false },
208
+        { match_queue_id: 4, profile_id: 46, target_id: 62, is_deleted: false },
209
+        { match_queue_id: 5, profile_id: 45, target_id: 46, is_deleted: false },
210
+        { match_queue_id: 6, profile_id: 46, target_id: 45, is_deleted: false },
211
+        { match_queue_id: 7, profile_id: 46, target_id: 44, is_deleted: false },
212
+        { match_queue_id: 8, profile_id: 46, target_id: 43, is_deleted: false },
213
+        { match_queue_id: 9, profile_id: 46, target_id: 42, is_deleted: false },
214
+        {
215
+            match_queue_id: 10,
216
+            profile_id: 46,
217
+            target_id: 41,
218
+            is_deleted: false,
219
+        },
220
+        {
221
+            match_queue_id: 11,
222
+            profile_id: 46,
223
+            target_id: 40,
224
+            is_deleted: false,
225
+        },
226
+        {
227
+            match_queue_id: 12,
228
+            profile_id: 40,
229
+            target_id: 46,
230
+            is_deleted: false,
231
+        },
232
+        {
233
+            match_queue_id: 13,
234
+            profile_id: 41,
235
+            target_id: 46,
236
+            is_deleted: false,
237
+        },
238
+        {
239
+            match_queue_id: 14,
240
+            profile_id: 42,
241
+            target_id: 46,
242
+            is_deleted: false,
243
+        },
244
+        {
245
+            match_queue_id: 15,
246
+            profile_id: 43,
247
+            target_id: 46,
248
+            is_deleted: false,
249
+        },
250
+        {
251
+            match_queue_id: 16,
252
+            profile_id: 44,
253
+            target_id: 46,
254
+            is_deleted: false,
255
+        },
202
     ],
256
     ],
203
 }
257
 }

+ 111
- 3
backend/lib/plugins/notification.js Прегледај датотеку

1
+const _allStreams = {}
2
+
1
 const NotificationRoute = require('../routes/notification')
3
 const NotificationRoute = require('../routes/notification')
2
-const { onEvent } = require('../services/notification')
4
+
5
+/** Heavily lifted from: https://github.com/mtharrison/susie/blob/master/lib/index.js */
6
+
7
+const Stream = require('stream')
8
+const PassThrough = Stream.PassThrough
9
+const Transform = Stream.Transform
10
+
11
+const ENDER = { event: 'end', data: '' }
12
+
13
+/**
14
+ * Stringify a stream
15
+ * ?: I don't really get what this is doing
16
+ * @param {Stream} event
17
+ * @returns {string}
18
+ */
19
+const _stringifyEvent = function (event) {
20
+    let str = ''
21
+    const endl = '\r\n'
22
+    for (const i in event) {
23
+        let val = event[i]
24
+        if (val instanceof Buffer) {
25
+            val = val.toString()
26
+        }
27
+        if (typeof val === 'object') {
28
+            val = JSON.stringify(val)
29
+        }
30
+        str += i + ': ' + val + endl
31
+    }
32
+    str += endl
33
+    return str
34
+}
35
+
36
+/**
37
+ * Transform extension
38
+ * ?: I don't really get what this is doing
39
+ * @param {object} options
40
+ * @param {object} objectMode
41
+ */
42
+class Transformer extends Transform {
43
+    constructor(options, objectMode) {
44
+        super({ objectMode })
45
+        options = options || {}
46
+        this.counter = 1
47
+        this.event = options.event || null
48
+        this.generateId = options.generateId
49
+            ? options.generateId
50
+            : () => this.counter++
51
+    }
52
+    _transform(chunk, encoding, callback) {
53
+        const event = {
54
+            id: this.generateId(chunk),
55
+            data: chunk,
56
+        }
57
+        if (this.event) {
58
+            event.event = this.event
59
+        }
60
+        this.push(_stringifyEvent(event))
61
+        callback()
62
+    }
63
+    _flush(callback) {
64
+        this.push(_stringifyEvent(ENDER))
65
+        callback()
66
+    }
67
+}
68
+
69
+/**
70
+ * Callback to decorate server toolkit (h)
71
+ * !: Currently we only support ObjectMode streams
72
+ * ?: I don't really get what this is doing
73
+ * @param {Stream} event stream input
74
+ * @param {Toolkit} h hapi common response toolkit
75
+ * @param {object} streamOptions
76
+ */
77
+const _event = (event, h, streamOptions) => {
78
+    let active
79
+    if (event instanceof Stream.Readable) {
80
+        if (event._readableState.objectMode) {
81
+            active = new PassThrough()
82
+            const through = new Transformer(streamOptions, true)
83
+            through.pipe(active)
84
+            event.pipe(through)
85
+        }
86
+        return h
87
+            .response(active)
88
+            .header('content-type', 'text/event-stream')
89
+            .header('content-encoding', 'identity')
90
+    }
91
+}
92
+
93
+/**
94
+ * Takes an open HTTP stream and writes
95
+ * a msg to it, then fires notification plugin's
96
+ * _event callback
97
+ * @param {object} msg you want to send
98
+ * @param {string} name <profileId>.<eventType>
99
+ * @param {boolean} shouldInitialize
100
+ * @param {Toolkit} h hapi common response toolkit
101
+ */
102
+const onNotify = (name, msg, h, shouldInitialize = false) => {
103
+    if (shouldInitialize) {
104
+        _allStreams[name] = new PassThrough({ objectMode: true })
105
+    }
106
+    if (!_allStreams[name]) return
107
+    _allStreams[name].write(msg)
108
+
109
+    return _event(_allStreams[name], h, { event: name })
110
+}
3
 
111
 
4
 module.exports = {
112
 module.exports = {
5
     name: 'notification-plugin',
113
     name: 'notification-plugin',
6
     version: '1.0.0',
114
     version: '1.0.0',
7
-    register: async (server, options) => {
115
+    register: async server => {
8
         await server.route(NotificationRoute)
116
         await server.route(NotificationRoute)
9
-        server.decorate('toolkit', 'event', onEvent)
117
+        server.method('notify', onNotify)
10
     },
118
     },
11
 }
119
 }

+ 1
- 1
backend/lib/routes/membership/active.js Прегледај датотеку

53
                 profileId,
53
                 profileId,
54
                 membershipType,
54
                 membershipType,
55
             )
55
             )
56
-
56
+            console.log('groupings :>> ', groupings)
57
             /**
57
             /**
58
              * Heavily process the result by storing just a profile_id
58
              * Heavily process the result by storing just a profile_id
59
              * and attach complete profiles
59
              * and attach complete profiles

+ 31
- 9
backend/lib/routes/membership/join.js Прегледај датотеку

27
     response: Joi.object({
27
     response: Joi.object({
28
         memberships: Joi.array().items(),
28
         memberships: Joi.array().items(),
29
         hasMatch: Joi.boolean(),
29
         hasMatch: Joi.boolean(),
30
+        groupings: Joi.array().items(),
30
     }).label('grouping_membership_list'),
31
     }).label('grouping_membership_list'),
31
     error: errorSchema.single,
32
     error: errorSchema.single,
32
 }
33
 }
48
          */
49
          */
49
         handler: async function (request, h) {
50
         handler: async function (request, h) {
50
             try {
51
             try {
52
+                console.log('---')
51
                 const { membershipService } = request.server.services()
53
                 const { membershipService } = request.server.services()
52
 
54
 
53
                 /** Grab payload info */
55
                 /** Grab payload info */
67
                 // !: You should only be associated with a single company too
69
                 // !: You should only be associated with a single company too
68
 
70
 
69
                 /** User membership service method to create membership */
71
                 /** User membership service method to create membership */
70
-                const memberships = await membershipService.joinGrouping(
71
-                    profileId,
72
-                    res.target_id,
73
-                    groupingToWrite,
74
-                    role,
72
+                const { memberships, groupings } =
73
+                    await membershipService.joinGrouping(
74
+                        profileId,
75
+                        res.target_id,
76
+                        groupingToWrite,
77
+                        role,
78
+                    )
79
+                const hasMatch = memberships.every(
80
+                    membership => membership && membership.is_active == true,
75
                 )
81
                 )
76
-                // console.log(memberships)
77
 
82
 
83
+                if (hasMatch) {
84
+                    request.server.methods.notify(
85
+                        `${profileId}.stonk`,
86
+                        {
87
+                            name: `${res.target_id} Match Fffound`,
88
+                            type: 'info',
89
+                        },
90
+                        h,
91
+                    )
92
+                    request.server.methods.notify(
93
+                        `${res.target_id}.stonk`,
94
+                        {
95
+                            name: `${profileId} Match Fffound`,
96
+                            type: 'info',
97
+                        },
98
+                        h,
99
+                    )
100
+                }
78
                 return h
101
                 return h
79
                     .response({
102
                     .response({
80
                         ok: true,
103
                         ok: true,
81
                         handler: pluginConfig.handlerType,
104
                         handler: pluginConfig.handlerType,
82
                         data: {
105
                         data: {
83
                             memberships,
106
                             memberships,
84
-                            hasMatch: memberships.every(
85
-                                membership => membership.is_active == true,
86
-                            ),
107
+                            hasMatch,
108
+                            groupings,
87
                         },
109
                         },
88
                     })
110
                     })
89
                     .code(200)
111
                     .code(200)

+ 16
- 23
backend/lib/routes/notification/index.js Прегледај датотеку

3
 const errorSchema = require('../../schemas/errors')
3
 const errorSchema = require('../../schemas/errors')
4
 const params = require('../../schemas/params')
4
 const params = require('../../schemas/params')
5
 
5
 
6
-const Stream = require('stream')
7
-const PassThrough = require('stream').PassThrough
8
-
9
 const pluginConfig = {
6
 const pluginConfig = {
10
     handlerType: 'notifictaion',
7
     handlerType: 'notifictaion',
11
     docs: {
8
     docs: {
28
         cors: true,
25
         cors: true,
29
         handler: async (request, h) => {
26
         handler: async (request, h) => {
30
             const { profile_id } = request.params
27
             const { profile_id } = request.params
31
-            const input = new PassThrough({ objectMode: true })
32
-            const eventType = 'stonk'
33
-
34
-            const msg = {
35
-                profile_id,
36
-                name: 'BDGRS',
37
-                price: (500 + Math.floor(Math.random() * 100)).toString(),
38
-                order: null,
39
-                type: 'info',
40
-            }
41
-
42
-            // Write to the input stream
43
-            setInterval(() => {
44
-                msg.order = Math.floor(Math.random() * 2) === 1 ? 'BUY' : 'SELL'
45
-                input.write(msg)
46
-            }, 5000)
47
 
28
 
48
-            // h.event() Added at plugin registration
49
-            // h is the toolkit
50
-            const streamOptions = { event: `${profile_id}.${eventType}` }
51
-            return h.event(input, h, streamOptions)
29
+            /**
30
+             * Write the initial stream
31
+             * !: this must remain open for notifications to work
32
+             */
33
+            return request.server.methods.notify(
34
+                `${profile_id}.stonk`,
35
+                {
36
+                    profile_id,
37
+                    name: 'BDGRS',
38
+                    price: (500 + Math.floor(Math.random() * 100)).toString(),
39
+                    order: Math.floor(Math.random() * 2) === 1 ? 'BUY' : 'SELL',
40
+                    type: 'info',
41
+                },
42
+                h,
43
+                true,
44
+            )
52
         },
45
         },
53
 
46
 
54
         /** Validate based on validators object */
47
         /** Validate based on validators object */

+ 10
- 5
backend/lib/routes/profile/patch-queue.js Прегледај датотеку

16
 
16
 
17
 const responseSchemas = {
17
 const responseSchemas = {
18
     response: Joi.array().items(
18
     response: Joi.array().items(
19
-        Joi.alternatives().try(Joi.number(), profileSchema.single),
19
+        Joi.alternatives().try(
20
+            Joi.number().optional(),
21
+            profileSchema.single.optional(),
22
+        ),
20
     ),
23
     ),
21
     error: errorSchema.single,
24
     error: errorSchema.single,
22
 }
25
 }
47
                 reinsert,
50
                 reinsert,
48
             )
51
             )
49
             const queueIds = updatedQueue.map(entry => entry.target_id)
52
             const queueIds = updatedQueue.map(entry => entry.target_id)
53
+
50
             const res = {
54
             const res = {
51
                 ok: true,
55
                 ok: true,
52
                 handler: pluginConfig.handlerType,
56
                 handler: pluginConfig.handlerType,
53
-                data: queueIds,
54
-            }
55
-            if (include_profile) {
56
-                res.data = await profileService.getProfilesFor(queueIds)
57
+                data:
58
+                    include_profile == true
59
+                        ? await profileService.getProfilesFor(queueIds)
60
+                        : queueIds,
57
             }
61
             }
62
+
58
             try {
63
             try {
59
                 return h.response(res).code(200)
64
                 return h.response(res).code(200)
60
             } catch (err) {
65
             } catch (err) {

+ 2
- 2
backend/lib/schemas/responses.js Прегледај датотеку

6
     response_key_id: Joi.number(),
6
     response_key_id: Joi.number(),
7
     response_id: Joi.number(),
7
     response_id: Joi.number(),
8
     profile_id: Joi.number(),
8
     profile_id: Joi.number(),
9
-    val: Joi.string(),
9
+    val: Joi.string().allow(null, ''),
10
 }).label('response_single')
10
 }).label('response_single')
11
 
11
 
12
 const singleResponseKey = Joi.object({
12
 const singleResponseKey = Joi.object({
20
     single: singleResponse,
20
     single: singleResponse,
21
     list: Joi.array().items(singleResponse).label('response_list'),
21
     list: Joi.array().items(singleResponse).label('response_list'),
22
     key: singleResponseKey,
22
     key: singleResponseKey,
23
-    keys: Joi.array().items(singleResponseKey).label('question_list')
23
+    keys: Joi.array().items(singleResponseKey).label('question_list'),
24
 }
24
 }

+ 3
- 2
backend/lib/services/matchqueue.js Прегледај датотеку

74
      */
74
      */
75
     async markAsDeleted(profileId, targetId, reinsert) {
75
     async markAsDeleted(profileId, targetId, reinsert) {
76
         const { MatchQueue } = this.server.models()
76
         const { MatchQueue } = this.server.models()
77
+        /** Always set row to deleted */
77
         await MatchQueue.query()
78
         await MatchQueue.query()
79
+            .where('profile_id', profileId)
80
+            .andWhere('target_id', targetId)
78
             .patch({
81
             .patch({
79
                 is_deleted: true,
82
                 is_deleted: true,
80
             })
83
             })
81
-            .where('profile_id', profileId)
82
-            .andWhere('target_id', targetId)
83
             .first()
84
             .first()
84
 
85
 
85
         if (reinsert) {
86
         if (reinsert) {

+ 27
- 42
backend/lib/services/membership.js Прегледај датотеку

10
      * @param {number} profileId
10
      * @param {number} profileId
11
      * @returns {Array} List of all grouping_ids for user
11
      * @returns {Array} List of all grouping_ids for user
12
      */
12
      */
13
-    async _getGroupingIdsForProfileId(profileId, type, active) {
13
+    async _getGroupingIdsForProfileId(profileId) {
14
         const { Membership } = this.server.models()
14
         const { Membership } = this.server.models()
15
 
15
 
16
         /** Grab every Membership associated with this id */
16
         /** Grab every Membership associated with this id */
17
-        let allMemberships = []
18
-
19
-        if (type && active == 'any') {
20
-            allMemberships = await Membership.query()
21
-                .where({ profile_id: profileId })
22
-                .where({ membership_type: type })
23
-        } else if (type) {
24
-            allMemberships = await Membership.query()
25
-                .where({ profile_id: profileId })
26
-                .where({ membership_type: type })
27
-                .where({ is_active: true })
28
-        } else if (active == 'any') {
29
-            allMemberships = await Membership.query().where({
30
-                profile_id: profileId,
31
-            })
32
-        } else {
33
-            allMemberships = await Membership.query()
34
-                .where({ profile_id: profileId })
35
-                .where({ is_active: true })
36
-        }
37
-
38
-        /** Copy a list of the just the Groupings */
17
+        const allMemberships = await Membership.query().where({
18
+            profile_id: profileId,
19
+        })
20
+        /** Copy a list of the just the Grouping ids */
39
         const groupingIdsToGrab = allMemberships.map(
21
         const groupingIdsToGrab = allMemberships.map(
40
             membership => membership.grouping_id,
22
             membership => membership.grouping_id,
41
         )
23
         )
43
         /** Uncomment to dedupe the list just in case */
25
         /** Uncomment to dedupe the list just in case */
44
         return [...new Set(groupingIdsToGrab)]
26
         return [...new Set(groupingIdsToGrab)]
45
     }
27
     }
46
-
28
+    async _getGroupings(groupingIds, txn) {
29
+        const { Grouping } = this.server.models()
30
+        return await Grouping.query(txn).whereIn('grouping_id', groupingIds)
31
+    }
47
     /**
32
     /**
48
      * Internal method to create a new grouping
33
      * Internal method to create a new grouping
49
      * @param {object} groupingToTry from payload data
34
      * @param {object} groupingToTry from payload data
81
      */
66
      */
82
     async findGroupingsByProfileId(profileId, type) {
67
     async findGroupingsByProfileId(profileId, type) {
83
         const { Grouping } = this.server.models()
68
         const { Grouping } = this.server.models()
84
-
85
         const dedupedGroupings = await this._getGroupingIdsForProfileId(
69
         const dedupedGroupings = await this._getGroupingIdsForProfileId(
86
             profileId,
70
             profileId,
87
             type,
71
             type,
88
-            'any',
89
         )
72
         )
90
-
73
+        console.log('dedupedGroupings :>> ', dedupedGroupings)
91
         /** Grab just the Groupings this id has a Membership for */
74
         /** Grab just the Groupings this id has a Membership for */
92
         return await Grouping.query()
75
         return await Grouping.query()
93
             .whereIn('grouping_id', dedupedGroupings)
76
             .whereIn('grouping_id', dedupedGroupings)
95
     }
78
     }
96
 
79
 
97
     async _groupingIdsInCommon(profileId, targetId) {
80
     async _groupingIdsInCommon(profileId, targetId) {
98
-        const dedupedUserGroupingIds = await this._getGroupingIdsForProfileId(
99
-            profileId,
100
-        )
101
-        const dedupedTargetGroupingIds = await this._getGroupingIdsForProfileId(
102
-            targetId,
103
-        )
104
-
105
-        /** Return true if both people have a group in common */
106
-        return dedupedUserGroupingIds.filter(groupingId =>
107
-            dedupedTargetGroupingIds.includes(groupingId),
108
-        )
81
+        const uids = await this._getGroupingIdsForProfileId(profileId)
82
+        const tids = await this._getGroupingIdsForProfileId(targetId)
83
+        const common = []
84
+        for (let i in uids) {
85
+            if (tids.indexOf(uids[i]) !== -1) common.push(uids[i])
86
+        }
87
+        return common.sort((x, y) => x - y)
109
     }
88
     }
89
+
110
     async _patchMembership(memberships, profileId, patch) {
90
     async _patchMembership(memberships, profileId, patch) {
111
         const { Membership } = this.server.models()
91
         const { Membership } = this.server.models()
112
 
92
 
114
         for (let membershipInfo of memberships) {
94
         for (let membershipInfo of memberships) {
115
             await Membership.query()
95
             await Membership.query()
116
                 .where('membership_id', membershipInfo.membership_id)
96
                 .where('membership_id', membershipInfo.membership_id)
117
-                .where('user_id', profileId)
97
+                .where('profile_id', profileId)
118
                 .patch(patch)
98
                 .patch(patch)
119
         }
99
         }
120
     }
100
     }
139
 
119
 
140
         if (matchingGroupingIds.length) {
120
         if (matchingGroupingIds.length) {
141
             /** Grab all memberships associated with groupingIds */
121
             /** Grab all memberships associated with groupingIds */
142
-            const memberships = await Membership.query().whereIn(
122
+            let memberships = await Membership.query().whereIn(
143
                 'grouping_id',
123
                 'grouping_id',
144
                 matchingGroupingIds,
124
                 matchingGroupingIds,
145
             )
125
             )
150
             })
130
             })
151
 
131
 
152
             /** Make a new query to get updated information */
132
             /** Make a new query to get updated information */
153
-            return await Membership.query().whereIn(
133
+            memberships = await Membership.query().whereIn(
154
                 'grouping_id',
134
                 'grouping_id',
155
                 matchingGroupingIds,
135
                 matchingGroupingIds,
156
             )
136
             )
137
+            const groupings = await this._getGroupings(matchingGroupingIds, txn)
138
+            return { memberships, groupings }
157
         } else {
139
         } else {
158
             /**
140
             /**
159
              * If both have NO grouping in common, create a membership
141
              * If both have NO grouping in common, create a membership
183
                 is_active: false,
165
                 is_active: false,
184
             })
166
             })
185
 
167
 
186
-            return [userMembership, targetMembership]
168
+            return {
169
+                memberships: [userMembership, targetMembership],
170
+                groupings: [],
171
+            }
187
         }
172
         }
188
     }
173
     }
189
 
174
 

+ 0
- 128
backend/lib/services/notification.js Прегледај датотеку

1
-/** Heavily lifted from: https://github.com/mtharrison/susie/blob/master/lib/index.js */
2
-
3
-const Stream = require('stream')
4
-const PassThrough = Stream.PassThrough
5
-const Transform = Stream.Transform
6
-
7
-const ENDER = { event: 'end', data: '' }
8
-
9
-/**
10
- * Stringify a stream
11
- * ?: I don't really get what this is doing
12
- * @param {Stream} event
13
- * @returns {string}
14
- */
15
-const stringifyEvent = function (event) {
16
-    let str = ''
17
-    const endl = '\r\n'
18
-    for (const i in event) {
19
-        let val = event[i]
20
-        if (val instanceof Buffer) {
21
-            val = val.toString()
22
-        }
23
-        if (typeof val === 'object') {
24
-            val = JSON.stringify(val)
25
-        }
26
-        str += i + ': ' + val + endl
27
-    }
28
-    str += endl
29
-    return str
30
-}
31
-
32
-/**
33
- * Transform extension
34
- * ?: I don't really get what this is doing
35
- * @param {object} options
36
- * @param {object} objectMode
37
- */
38
-class Transformer extends Transform {
39
-    constructor(options, objectMode) {
40
-        super({ objectMode })
41
-        options = options || {}
42
-        this.counter = 1
43
-        this.event = options.event || null
44
-        this.generateId = options.generateId
45
-            ? options.generateId
46
-            : () => {
47
-                  return this.counter++
48
-              }
49
-    }
50
-    _transform(chunk, encoding, callback) {
51
-        const event = {
52
-            id: this.generateId(chunk),
53
-            data: chunk,
54
-        }
55
-        if (this.event) {
56
-            event.event = this.event
57
-        }
58
-        this.push(stringifyEvent(event))
59
-        callback()
60
-    }
61
-    _flush(callback) {
62
-        this.push(stringifyEvent(ENDER))
63
-        callback()
64
-    }
65
-}
66
-
67
-/**
68
- * Take an event stream and write content to another stream
69
- * ?: Save this for future extension
70
- * @param {Stream} event stream input
71
- * @param {Stream} stream to write to
72
- */
73
-const writeEvent = function (event, stream) {
74
-    if (event) {
75
-        stream.write(stringifyEvent(event))
76
-    } else {
77
-        // closing time
78
-        stream.write(stringifyEvent(ENDER))
79
-        stream.end()
80
-    }
81
-}
82
-
83
-/**
84
- * Callback to decorate server toolkit (h)
85
- * !: Currently we only support ObjectMode streams
86
- * ?: I don't really get what this is doing
87
- * @param {Stream} event stream input
88
- * @param {Toolkit} h hapi common response toolkit
89
- * @param {object} streamOptions
90
- */
91
-const onEvent = (event, h, streamOptions) => {
92
-    // const state = h.request.plugins.notifications = h.request.plugins.notifications || {}
93
-    let active
94
-    if (event instanceof Stream.Readable) {
95
-        if (event._readableState.objectMode) {
96
-            const through = new Transformer(streamOptions, true)
97
-            active = new PassThrough()
98
-            through.pipe(active)
99
-            event.pipe(through)
100
-        }
101
-        // else {
102
-        //     stream = new Transformer(streamOptions, false)
103
-        //     event.pipe(stream)
104
-        // }
105
-        console.log('streamOptions :', streamOptions)
106
-        return h
107
-            .response(active)
108
-            .header('content-type', 'text/event-stream')
109
-            .header('content-encoding', 'identity')
110
-    }
111
-    // Uncomment to do stream state stuff
112
-    // handle a first object arg
113
-    // if (!state.stream) {
114
-    //     active = new PassThrough()
115
-    //     state.stream = active
116
-    //     state.mode = 'object'
117
-    //     const response = h.response(active)
118
-    //         .header('content-type', 'text/event-stream')
119
-    //         .header('content-encoding', 'identity')
120
-    //     writeEvent(event, active)
121
-    //     return response
122
-    // }
123
-    // already have an object stream flowing, just write next event
124
-    // active = state.stream
125
-    // internals.writeEvent(event, active)
126
-}
127
-
128
-module.exports = { onEvent }

+ 1003
- 0
frontend/package-lock.json
Разлика између датотеке није приказан због своје велике величине
Прегледај датотеку


+ 1
- 0
frontend/package.json Прегледај датотеку

22
         "@prettier/plugin-pug": "^1.19.2",
22
         "@prettier/plugin-pug": "^1.19.2",
23
         "@vitejs/plugin-vue": "^2.2.4",
23
         "@vitejs/plugin-vue": "^2.2.4",
24
         "@vue/compiler-sfc": "^3.2.31",
24
         "@vue/compiler-sfc": "^3.2.31",
25
+        "ava": "^4.3.3",
25
         "cross-env": "^7.0.3",
26
         "cross-env": "^7.0.3",
26
         "eslint": "^8.11.0",
27
         "eslint": "^8.11.0",
27
         "eslint-config-prettier": "^8.5.0",
28
         "eslint-config-prettier": "^8.5.0",

+ 14
- 18
frontend/src/App.vue Прегледај датотеку

1
 <template lang="pug">
1
 <template lang="pug">
2
 w-app
2
 w-app
3
-    div.nav(style="display: flex; justify-content: space-between;")
3
+    div.nav(v-if="isLoggedIn" style="display: flex; justify-content: space-between;")
4
         header
4
         header
5
             h2 home - profile: {{ getPid }}
5
             h2 home - profile: {{ getPid }}
6
         w-drawer(v-model="openDrawer")
6
         w-drawer(v-model="openDrawer")
7
-            h2 content
7
+            SideBar(
8
+                :pid="getPid"
9
+                @updatePid="setPid"
10
+                @hide="showSidebar = false"
11
+            )
8
         w-button(@click="openDrawer = true" outline="")
12
         w-button(@click="openDrawer = true" outline="")
9
             | Active Chats
13
             | Active Chats
10
-    SideBar(
11
-        v-if="showSidebar"
12
-        :pid="getPid"
13
-        @updatePid="setPid"
14
-        @hide="showSidebar = false"
15
-    )
16
     RouterView(
14
     RouterView(
15
+        v-if="isLoggedIn"
17
         :pid="getPid"
16
         :pid="getPid"
18
         @updatePid="setPid"
17
         @updatePid="setPid"
19
         @show-sidebar="showSidebar = !showSidebar"
18
         @show-sidebar="showSidebar = !showSidebar"
38
         openDrawer: false,
37
         openDrawer: false,
39
     }),
38
     }),
40
     computed: {
39
     computed: {
41
-        getPid: () => (currentProfile.id.value ? currentProfile.id.value : 999),
40
+        getPid: () => {
41
+            return currentProfile.id.value ? currentProfile.id.value : null
42
+        },
43
+        isLoggedIn: () => {
44
+            return currentProfile.isLoggedIn
45
+        },
42
     },
46
     },
43
     async created() {
47
     async created() {
44
         /** Get questions so we can compare against profile responses */
48
         /** Get questions so we can compare against profile responses */
62
             if (currentProfile.isLoggedIn) {
66
             if (currentProfile.isLoggedIn) {
63
                 currentProfile.logout()
67
                 currentProfile.logout()
64
             }
68
             }
65
-            await currentProfile.login(profileId)
66
-
67
-            if (currentProfile.isLoggedIn) {
68
-                console.warn(
69
-                    `setting up Chatter and Toaster for ${this.getPid}...`,
70
-                )
71
-                currentProfile.setupChatter()
72
-                currentProfile.setupToaster(this.$waveui.notify)
73
-            }
69
+            await currentProfile.login(profileId, this.$waveui.notify)
74
             console.log('---')
70
             console.log('---')
75
             //  step 3: subscribe to the this.subscriptions array
71
             //  step 3: subscribe to the this.subscriptions array
76
             // view current subscriptions on the currentProfile.chatter.subscriptions
72
             // view current subscriptions on the currentProfile.chatter.subscriptions

+ 31
- 20
frontend/src/components/ProfileCardList.vue Прегледај датотеку

19
                     h4 {{ profile.name }}
19
                     h4 {{ profile.name }}
20
                     nav.swipe_icons
20
                     nav.swipe_icons
21
                         //- Accept
21
                         //- Accept
22
-                        button(@click="chat(profile.pid)") chat
23
-                        button(@click="accept") Accept
22
+                        button(@click="accept(profile.pid)") Accept
24
                         button(@click="view(profile.pid)") view
23
                         button(@click="view(profile.pid)") view
25
                         //- Pass
24
                         //- Pass
26
-                        button(@click="pass") Pass
25
+                        button(@click="pass(profile.pid)") Pass
27
     
26
     
28
     .match-layout(
27
     .match-layout(
29
         v-else
28
         v-else
39
                     h4 {{ profile.name }}
38
                     h4 {{ profile.name }}
40
                     nav.swipe_icons
39
                     nav.swipe_icons
41
                         button(@click="view(profile.pid)") view
40
                         button(@click="view(profile.pid)") view
42
-                        button(@click="chat(profile.pid)") chat
43
 </template>
41
 </template>
44
 
42
 
45
 <script setup>
43
 <script setup>
46
 import { useRouter } from 'vue-router'
44
 import { useRouter } from 'vue-router'
47
 import {
45
 import {
48
     updateQueueByProfileId,
46
     updateQueueByProfileId,
49
-    fetchMembershipsByProfileId,
50
     postMembershipByProfileId,
47
     postMembershipByProfileId,
51
     currentProfile,
48
     currentProfile,
52
 } from '../services'
49
 } from '../services'
89
 
86
 
90
 // AHP Button behavior
87
 // AHP Button behavior
91
 
88
 
92
-const accept = async () => {
89
+const accept = async targetId => {
90
+    if (targetId == props.pid) return
93
     // need to pass these arguments (profileId, targetId, status)
91
     // need to pass these arguments (profileId, targetId, status)
94
     // the url structure is
92
     // the url structure is
95
     // const charmander = await db.get(`/profile/{profile_id}/queue/{target_id}/delete?include_profile=true&reinsert=false`)
93
     // const charmander = await db.get(`/profile/{profile_id}/queue/{target_id}/delete?include_profile=true&reinsert=false`)
96
     // http://localhost:3001/api/profile/38/queue/9/delete?include_profile=true&reinsert=true
94
     // http://localhost:3001/api/profile/38/queue/9/delete?include_profile=true&reinsert=true
97
     const profileId = props.pid
95
     const profileId = props.pid
98
-    const targetId = props.profiles[0].pid
99
-    updateQueueByProfileId(profileId, targetId, false)
100
-    // TODO: next step is grouping/membership
101
-    const checkMembership = await fetchMembershipsByProfileId(profileId)
102
-    if (!checkMembership.length) {
103
-        postMembershipByProfileId({ profileId, targetId })
96
+    await updateQueueByProfileId(profileId, targetId, false)
97
+    const { membershipMatch, groupingName } = await postMembershipByProfileId({
98
+        profileId,
99
+        targetId,
100
+    })
101
+
102
+    // Reuse old grouping name if theres a match
103
+    let channel = groupingName
104
+    console.log('membershipMatch :>> ', membershipMatch)
105
+    if (membershipMatch?.hasMatch) {
106
+        const [time, intiator, target] = groupingName.split('_')
107
+        channel = membershipMatch.groupings[0].grouping_name
108
+        console.log('channel :>> ', channel)
104
     }
109
     }
110
+    await subscribeToChannel(channel)
105
     emit('reload')
111
     emit('reload')
106
 }
112
 }
113
+
107
 const view = pid => {
114
 const view = pid => {
108
     router.push({ path: `/matches/${pid}` })
115
     router.push({ path: `/matches/${pid}` })
109
 }
116
 }
110
-const chat = async pid => {
117
+
118
+const subscribeToChannel = async channelName => {
111
     // create a chatter reference from the current profile
119
     // create a chatter reference from the current profile
112
     const chatter = currentProfile.chatter
120
     const chatter = currentProfile.chatter
113
     // console.log('mock sender:', pid)
121
     // console.log('mock sender:', pid)
114
-    
115
-    /**  publish a new message to the chatter with the channel and the message & title is optional
122
+
123
+    /**
124
+     * publish a new message to the chatter with the channel and the message & title is optional
116
      */
125
      */
117
-    const res = await chatter.publish(chatter.subscriptions[1], {
126
+    // You MUST send chatter channels as an array in an object
127
+    chatter.subscribe({ channels: [channelName] })
128
+    const res = await chatter.publish(channelName, {
118
         title: 'New Message',
129
         title: 'New Message',
119
-        description: 'This is the checking to see if we are subscribed to a channel!',
130
+        description: `This is the checking to see if we are subscribed to the ${channelName} channel!`,
120
     })
131
     })
121
     // PubNub response will be a timecode of when the message was published
132
     // PubNub response will be a timecode of when the message was published
122
     console.log('res:', res)
133
     console.log('res:', res)
123
-
124
     //router.push({ path: `/chat/${pid}` })
134
     //router.push({ path: `/chat/${pid}` })
125
 }
135
 }
126
-const pass = () => {
127
-    const targetId = props.profiles[0].pid
136
+
137
+const pass = targetId => {
138
+    if (targetId == props.pid) return
128
     updateQueueByProfileId(props.pid, targetId, true)
139
     updateQueueByProfileId(props.pid, targetId, true)
129
     emit('reload')
140
     emit('reload')
130
 }
141
 }

+ 1
- 1
frontend/src/components/SideBar.vue Прегледај датотеку

1
 <template lang="pug">
1
 <template lang="pug">
2
 aside.sidebar
2
 aside.sidebar
3
     .temp-control-box
3
     .temp-control-box
4
-        input(v-model="switchToPID")
4
+        input(v-model="switchToPID" @keyup.enter="$emit('updatePid', switchToPID)")
5
         button(@click="$emit('updatePid', switchToPID)") switch profile
5
         button(@click="$emit('updatePid', switchToPID)") switch profile
6
     
6
     
7
     Messages(:matches="matches" :pid="pid")
7
     Messages(:matches="matches" :pid="pid")

+ 21
- 34
frontend/src/services/chat.service.js Прегледај датотеку

1
 import PubNub from 'pubnub'
1
 import PubNub from 'pubnub'
2
 
2
 
3
-// custom services 
4
-import {fetchMembershipsByProfileId} from '../services/grouping.service'
3
+// custom services
4
+import { fetchMembershipsByProfileId } from '../services/grouping.service'
5
 
5
 
6
 /**
6
 /**
7
  * Provider method holder
7
  * Provider method holder
49
     title: 'testing',
49
     title: 'testing',
50
     description: 'hello world!',
50
     description: 'hello world!',
51
 })
51
 })
52
-const MAIN_CHANNEL = 'Channel-Barcelona'
52
+const MAIN_CHANNEL = 'Channel-Siimee'
53
 
53
 
54
 /** Singleton that holds all our chat information */
54
 /** Singleton that holds all our chat information */
55
 class Chatter {
55
 class Chatter {
58
      * @return {Chatter} our chatter instance object
58
      * @return {Chatter} our chatter instance object
59
      */
59
      */
60
     constructor() {
60
     constructor() {
61
-        // Map of each active chat
62
-        // * this is where we will store all of our groupings on from the backend on user login...
63
-        this.groupings = {}
64
-
65
         // Our pubnub instance
61
         // Our pubnub instance
66
         this.provider = null
62
         this.provider = null
67
 
63
 
70
 
66
 
71
         // Setup the main channel
67
         // Setup the main channel
72
         //  subscriptions array will be built dynamically from the "this.groupings" object
68
         //  subscriptions array will be built dynamically from the "this.groupings" object
73
-        this.subscriptions = [MAIN_CHANNEL, 'Channel-LosAngeles']
69
+        this.subscriptions = [MAIN_CHANNEL]
74
         this.listeners = {
70
         this.listeners = {
75
             status: async e => {
71
             status: async e => {
76
-                await this.publish(this.subscriptions[2], testMessage)
72
+                // await this.publish(this.subscriptions[0], testMessage)
77
                 if (e.category !== 'PNConnectedCategory') return
73
                 if (e.category !== 'PNConnectedCategory') return
78
             },
74
             },
79
             message: this._onMessage,
75
             message: this._onMessage,
98
     async setup(uuid) {
94
     async setup(uuid) {
99
         this.uuid = `${uuid}`
95
         this.uuid = `${uuid}`
100
         this.provider = await setupPubnub(this.uuid)
96
         this.provider = await setupPubnub(this.uuid)
101
-            
97
+
102
         //  step 1: build the this.groupings object from the backend
98
         //  step 1: build the this.groupings object from the backend
103
-        // ? .then() to wait for the groupings to be fetched before subscribing to channels
104
-        this.getGroupingsByProfileId(this.uuid).then(() => {
105
-            this._listenFor({ listeners: this.listeners })
106
-            this._subscribe(this.subscriptions)
107
-        })
108
-        
109
-        console.log('this.subscriptions', this.subscriptions)
110
-       
99
+        // ? .await for the groupings to be fetched before subscribing to channels
100
+        await this._setupAllChannels(this.uuid)
101
+        this._listenFor({ listeners: this.listeners })
102
+        this.subscribe({ channels: this.subscriptions })
111
     }
103
     }
112
     /**
104
     /**
113
      * Send a message to a channel
105
      * Send a message to a channel
119
      */
111
      */
120
     async publish(channel, message) {
112
     async publish(channel, message) {
121
         console.log('publishing message to channel:', channel)
113
         console.log('publishing message to channel:', channel)
122
-        return await providerMethods['publish']({ channel, message })
114
+        return providerMethods.publish({ channel, message })
123
     }
115
     }
124
     /**
116
     /**
125
      * Subscribe to a channels
117
      * Subscribe to a channels
126
      * Facade so we can hide provider specific methods
118
      * Facade so we can hide provider specific methods
127
      * @param {array} channels
119
      * @param {array} channels
128
      */
120
      */
129
-    _subscribe(channels) {
130
-        providerMethods['subscribe']({ channels })
121
+    subscribe({ channels }) {
122
+        providerMethods.subscribe({ channels })
131
     }
123
     }
132
     /**
124
     /**
133
      * Listen to events and set callbacks
125
      * Listen to events and set callbacks
134
      * Facade so we can hide provider specific methods
126
      * Facade so we can hide provider specific methods
135
      */
127
      */
136
     _listenFor({ listeners }) {
128
     _listenFor({ listeners }) {
137
-        providerMethods['listen'](listeners)
129
+        providerMethods.listen(listeners)
138
     }
130
     }
139
-    
140
     //  step 2: build the this.subscriptions array from the this.groupings object
131
     //  step 2: build the this.subscriptions array from the this.groupings object
141
     // fetch all groupings for this profile and then store them in the chatter groupings object for reference
132
     // fetch all groupings for this profile and then store them in the chatter groupings object for reference
142
-    async getGroupingsByProfileId(profileId) {
143
-        console.log('fetching groupings for profileId:', profileId)
133
+    async _setupAllChannels(profileId) {
144
         const groupings = await fetchMembershipsByProfileId(profileId)
134
         const groupings = await fetchMembershipsByProfileId(profileId)
145
-        this.groupings = groupings
146
-        this.createChannelNamesByGroupings(this.groupings)
135
+        console.log(`fetched groupings for profileId: ${profileId}`, groupings)
136
+        groupings.forEach(grouping => {
137
+            this.subscriptions.push(grouping.grouping_name)
138
+        })
147
     }
139
     }
148
-    // building a list of channel names from the groupings object.grouping_name
149
-    createChannelNamesByGroupings(groupings) {
150
-        groupings.forEach(item => {
151
-            this.subscriptions.push(item.grouping_name)
152
-        });
153
-        
154
-        
140
+    stop() {
141
+        console.warn('chatter stop no implemented')
155
     }
142
     }
156
 }
143
 }
157
 
144
 

+ 3
- 4
frontend/src/services/grouping.service.js Прегледај датотеку

34
     targetId,
34
     targetId,
35
     groupingType = 'match',
35
     groupingType = 'match',
36
 }) => {
36
 }) => {
37
-    const utcDateInSeconds = Date.now()/1000
37
+    const utcDateInSeconds = Date.now() / 1000
38
     const membership = {
38
     const membership = {
39
         target_id: targetId,
39
         target_id: targetId,
40
         grouping_type: groupingType,
40
         grouping_type: groupingType,
41
         grouping_name: `${utcDateInSeconds}_${profileId}_${targetId}`,
41
         grouping_name: `${utcDateInSeconds}_${profileId}_${targetId}`,
42
     }
42
     }
43
-    console.warn(`${groupingType} created between ${profileId} and ${targetId}`)
44
-    const createdMembershipRecord = await db.post(
43
+    const membershipMatch = await db.post(
45
         `/membership/${profileId}/join`,
44
         `/membership/${profileId}/join`,
46
         membership,
45
         membership,
47
     )
46
     )
48
-    return createdMembershipRecord
47
+    return { membershipMatch, groupingName: membership.grouping_name }
49
 }
48
 }
50
 export { fetchMembershipsByProfileId, postMembershipByProfileId }
49
 export { fetchMembershipsByProfileId, postMembershipByProfileId }

+ 7
- 2
frontend/src/services/login.service.js Прегледај датотеку

57
      * @param {number} profileId
57
      * @param {number} profileId
58
      * @returns {number} stored reactive id
58
      * @returns {number} stored reactive id
59
      */
59
      */
60
-    async login(profileId) {
60
+    async login(profileId, cb) {
61
         console.warn('logging in:', profileId)
61
         console.warn('logging in:', profileId)
62
         this.id.value = parseInt(profileId)
62
         this.id.value = parseInt(profileId)
63
+
64
+        this.setupChatter()
65
+        this.setupToaster(cb)
66
+
63
         return this.id.value
67
         return this.id.value
64
     }
68
     }
65
     logout() {
69
     logout() {
70
+        console.warn('logging out:', this.id.value)
66
         this.id.value = null
71
         this.id.value = null
67
         if (this.toaster) {
72
         if (this.toaster) {
68
             this.toaster.stop()
73
             this.toaster.stop()
69
         }
74
         }
70
         if (this.chatter) {
75
         if (this.chatter) {
71
-            this.toaster.stop()
76
+            this.chatter.stop()
72
         }
77
         }
73
     }
78
     }
74
 
79
 

+ 13
- 12
frontend/src/services/queue.service.js Прегледај датотеку

8
  */
8
  */
9
 const fetchQueueByProfileId = async profileId => {
9
 const fetchQueueByProfileId = async profileId => {
10
     let queue
10
     let queue
11
-    
11
+
12
     try {
12
     try {
13
-        queue = await db.get(
14
-            `/profile/${profileId}/queue?include_profile=true`,
15
-        )
16
-        if(!queue?.length) {
13
+        queue = await db.get(`/profile/${profileId}/queue?include_profile=true`)
14
+        if (!queue?.length) {
17
             throw 'Could not retrieve match queue. Please take the survey and rescore.'
15
             throw 'Could not retrieve match queue. Please take the survey and rescore.'
18
         }
16
         }
19
     } catch (err) {
17
     } catch (err) {
20
         console.error(err)
18
         console.error(err)
21
     }
19
     }
22
 
20
 
23
-    return queue ? queue.map(profileData => {
24
-        return new Profile({ email: 'fixme@gmail.com', ...profileData })
25
-    }) : []
21
+    return queue
22
+        ? queue.map(profileData => {
23
+              return new Profile({ email: 'fixme@gmail.com', ...profileData })
24
+          })
25
+        : []
26
 }
26
 }
27
 
27
 
28
 /**
28
 /**
35
 const updateQueueByProfileId = async (profileId, targetId, reinsert) => {
35
 const updateQueueByProfileId = async (profileId, targetId, reinsert) => {
36
     const updateQueue = await db.patch(
36
     const updateQueue = await db.patch(
37
         `/profile/${profileId}/queue/${targetId}/delete?include_profile=true&reinsert=${reinsert}`,
37
         `/profile/${profileId}/queue/${targetId}/delete?include_profile=true&reinsert=${reinsert}`,
38
-        [ targetId ],
39
     )
38
     )
40
-    return updateQueue ? updateQueue.map(profileData => {
41
-        return new Profile({ email: 'fixme@gmail.com', ...profileData })
42
-    }) : []
39
+    return updateQueue
40
+        ? updateQueue.map(profileData => {
41
+              return new Profile({ email: 'fixme@gmail.com', ...profileData })
42
+          })
43
+        : []
43
 }
44
 }
44
 
45
 
45
 export { fetchQueueByProfileId, updateQueueByProfileId }
46
 export { fetchQueueByProfileId, updateQueueByProfileId }

Loading…
Откажи
Сачувај