@cocreate/socket-server 1.18.1 → 1.19.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ # [1.19.0](https://github.com/CoCreate-app/CoCreate-socket-server/compare/v1.18.1...v1.19.0) (2023-10-09)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * added '@cocreate/utlis' ([cc8267a](https://github.com/CoCreate-app/CoCreate-socket-server/commit/cc8267abf7ded45033929bd4c4997eb9adc89d42))
7
+ * handling of socket.id and clientId ([9e66f16](https://github.com/CoCreate-app/CoCreate-socket-server/commit/9e66f16f4e869e7ec29a261068fb61c46569f408))
8
+ * Improved handling of data.sync ([190ebd6](https://github.com/CoCreate-app/CoCreate-socket-server/commit/190ebd6c10f337fbd4e8be81caba865cfffb5c0a))
9
+ * send _id with syncMessage ([e37e2de](https://github.com/CoCreate-app/CoCreate-socket-server/commit/e37e2de8aff8945a3884c416514adc1e89fc54e2))
10
+ * update to use data.socketId and data.clientId ([125f2be](https://github.com/CoCreate-app/CoCreate-socket-server/commit/125f2bedd6090488ae26dc2228b53bb4aa63d4ff))
11
+ * use message_log ([d4b4c4a](https://github.com/CoCreate-app/CoCreate-socket-server/commit/d4b4c4ad2708cd61567d7172ac7bcb0635349a6a))
12
+
13
+
14
+ ### Features
15
+
16
+ * Authentication happens during upgrade ([c01cb0e](https://github.com/CoCreate-app/CoCreate-socket-server/commit/c01cb0eeb5fc4e135a8d9855fbab7e8b9535423d))
17
+ * get, set, delete socket functions ([9066911](https://github.com/CoCreate-app/CoCreate-socket-server/commit/9066911016072841cd08463c8354eeef892b41e3))
18
+ * method sync sent to server with sync details ([3c914b7](https://github.com/CoCreate-app/CoCreate-socket-server/commit/3c914b74d720949c8ae56f3f9159041ef1d5f24f))
19
+ * on socket connection the data is parsed from ([0b69b35](https://github.com/CoCreate-app/CoCreate-socket-server/commit/0b69b357c891da63c3e94aaca8361866f252de98))
20
+
1
21
  ## [1.18.1](https://github.com/CoCreate-app/CoCreate-socket-server/compare/v1.18.0...v1.18.1) (2023-09-18)
2
22
 
3
23
 
package/docs/index.html CHANGED
@@ -218,7 +218,6 @@
218
218
  </div>
219
219
  </div>
220
220
  </div>
221
- <script src="/apikey.js"></script>
222
221
  <script src="https://CoCreate.app/dist/CoCreate.js"></script>
223
222
  </body>
224
223
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/socket-server",
3
- "version": "1.18.1",
3
+ "version": "1.19.0",
4
4
  "description": "CoCreate-socket-server",
5
5
  "keywords": [
6
6
  "cocreate-socket",
@@ -40,8 +40,9 @@
40
40
  },
41
41
  "homepage": "https://cocreate.app/docs/CoCreate-socket-server",
42
42
  "dependencies": {
43
- "@cocreate/config": "^1.5.1",
44
- "@cocreate/uuid": "^1.7.1",
43
+ "@cocreate/config": "^1.5.2",
44
+ "@cocreate/uuid": "^1.7.2",
45
+ "@cocreate/utils": "^1.24.2",
45
46
  "ws": "7.5.9"
46
47
  }
47
48
  }
package/src/index.js CHANGED
@@ -2,105 +2,266 @@ const WebSocket = require('ws');
2
2
  const { URL } = require("url");
3
3
  const EventEmitter = require("events").EventEmitter;
4
4
  const uid = require('@cocreate/uuid')
5
+ const { ObjectId } = require('@cocreate/utils')
5
6
  const config = require('@cocreate/config')
6
7
 
7
8
  class SocketServer extends EventEmitter {
8
- constructor(server, prefix) {
9
+ constructor(server) {
9
10
  super();
11
+ this.serverId = uid.generate(12)
10
12
  this.organizations = new Map();
11
13
  this.clients = new Map();
12
- this.prefix = prefix || "ws";
14
+ this.sockets = new Map();
15
+ this.users = new Map();
16
+
13
17
  config({ organization_id: { prompt: 'Enter your organization_id: ' } })
14
18
 
15
19
  this.wss = new WebSocket.Server({ noServer: true });
20
+
16
21
  this.wss.on('headers', (headers, request) => {
17
22
  headers.push('Access-Control-Allow-Origin: *');
18
23
  });
19
24
 
25
+ this.wss.on('error', (error) => {
26
+ socket.destroy();
27
+ });
28
+
20
29
  server.on('upgrade', (request, socket, head) => {
21
- if (!this.handleUpgrade(request, socket, head)) {
22
- socket.destroy();
30
+ const self = this;
31
+ let organization_id = request.url.split('/')
32
+ organization_id = organization_id[organization_id.length - 1]
33
+
34
+ if (organization_id) {
35
+ this.wss.handleUpgrade(request, socket, head, function (socket) {
36
+ let options = decodeURIComponent(request.headers['sec-websocket-protocol'])
37
+ options = JSON.parse(options)
38
+
39
+ socket.organization_id = organization_id
40
+ socket.id = options.socketId;
41
+ socket.clientId = options.clientId;
42
+ socket.pathname = request.url
43
+ socket.origin = request.headers.origin || request.headers.host
44
+
45
+ if (socket.origin.startsWith('http'))
46
+ socket.origin = socket.origin.replace('http', 'ws')
47
+
48
+ socket.socketUrl = socket.origin + socket.pathname
49
+
50
+ if (socket.origin && socket.origin !== 'null') {
51
+ const url = new URL(socket.origin);
52
+ socket.host = url.host || socket.origin;
53
+ }
54
+
55
+ let data = {
56
+ socket,
57
+ method: 'read.object',
58
+ array: 'message_log',
59
+ $filter: {
60
+ sort: [
61
+ { key: '_id', direction: 'desc' }
62
+ ]
63
+ },
64
+ sync: true,
65
+ organization_id
66
+ }
67
+
68
+ if (options.lastSynced)
69
+ data.$filter.query = [
70
+ { key: '_id', value: options.lastSynced, operator: '$gt' }
71
+ ]
72
+ else
73
+ data.$filter.limit = 1
74
+
75
+ self.emit('read.object', data);
76
+
77
+ if (self.authenticate && options.token)
78
+ self.authenticate.decodeToken(options.token).then(({ user_id, expires }) => {
79
+ if (user_id) {
80
+ socket.user_id = user_id;
81
+ socket.expires = expires;
82
+ self.emit('userStatus', { socket, method: 'userStatus', user_id, userStatus: 'on', organization_id });
83
+ } else
84
+ this.emit('userStatus', { socket, user_id, status: 'off', organization_id });
85
+
86
+ self.onWebSocket(socket);
87
+ })
88
+ else
89
+ self.onWebSocket(socket);
90
+ })
23
91
  }
24
92
  });
25
93
  }
26
94
 
27
- handleUpgrade(req, socket, head) {
95
+ onWebSocket(socket) {
28
96
  const self = this;
29
- // const url = new URL(req.url);
30
- // const pathname = req.url;
31
- const config = this.getKeyFromUrl(req.url)
32
- if (config.type == this.prefix) {
33
- this.wss.handleUpgrade(req, socket, head, function (socket) {
34
- socket.config = config
35
- self.onWebSocket(req, socket);
36
- })
37
- return true;
38
- }
39
- return false;
40
- }
41
-
42
- onWebSocket(req, socket) {
43
- const self = this;
44
-
45
- this.addClient(socket);
97
+ this.add(socket);
46
98
 
47
99
  socket.on('message', async (message) => {
48
- self.onMessage(req, socket, message);
100
+ self.onMessage(socket, message);
49
101
  })
50
102
 
51
- socket.on('close', function () {
52
- self.removeClient(socket)
103
+ socket.on('close', () => {
104
+ self.delete(socket)
53
105
  })
54
106
 
55
107
  socket.on("error", () => {
56
- self.removeClient(socket)
108
+ self.delete(socket)
57
109
  });
58
110
 
59
- this.send(socket, { socket, method: 'connect', connectedKey: socket.config.key });
111
+ socket.send(JSON.stringify({ method: 'connect', connectedKey: socket.pathname }))
60
112
 
61
113
  }
62
114
 
63
- addClient(socket) {
64
- let organization_id = socket.config.organization_id
65
- if (!this.organizations.has(organization_id))
66
- this.organizations.set(organization_id, true)
67
-
68
- let user_id = socket.config.user_id
69
- let key = socket.config.key
70
- let clients = this.clients.get(key);
71
- if (!clients) {
72
- clients = new Map();
73
- this.clients.set(key, clients);
115
+ add(socket) {
116
+ let organization_id = socket.organization_id
117
+
118
+ let organization = this.organizations.get(organization_id)
119
+ if (!organization) {
120
+ organization = {
121
+ status: true,
122
+ clients: {}
123
+ }
124
+
125
+ this.organizations.set(organization_id, organization)
126
+
127
+ this.emit('update.object', {
128
+ method: 'update.object',
129
+ array: 'organizations',
130
+ object: {
131
+ _id: organization_id, ['$push.activeHost']: socket.socketUrl // needs socketId
132
+ },
133
+ organization_id
134
+ });
135
+
136
+ this.emit('mesh.create', {
137
+ url: socket.socketUrl,
138
+ organization_id
139
+ });
140
+
141
+ }
142
+
143
+ if (!this.clients.has(socket.clientId)) {
144
+ this.clients.set(socket.clientId, []);
145
+ organization.clients[socket.clientId] = []
146
+ }
147
+
148
+ if (!this.sockets.has(socket.id)) {
149
+ this.sockets.set(socket.id, socket);
150
+ this.clients.get(socket.clientId).push(socket);
151
+ organization.clients[socket.clientId].push(socket)
152
+ }
153
+
154
+ if (socket.user_id) {
155
+ this.emit('userStatus', { socket, user_id: socket.user_id, userStatus: 'on', organization_id });
156
+
157
+ if (!this.users.has(socket.user_id)) {
158
+ this.users.set(socket.user_id, [socket])
159
+ } else {
160
+ this.users.get(socket.user_id).push(socket)
161
+ }
162
+ }
163
+
164
+ }
165
+
166
+ get(data) {
167
+ let sockets = []
168
+ if (data.broadcast !== false) {
169
+ let organization = this.organizations.get(data.organization_id)
170
+ if (organization) {
171
+ const clients = organization.clients
172
+ for (let client of Object.keys(clients)) {
173
+ if (data.broadcastSender === false && client === data.clientId) continue
174
+
175
+ if (data.broadcastClient)
176
+ sockets.push(clients[client][0])
177
+ else
178
+ sockets.push(...clients[client])
179
+ }
180
+ } else {
181
+ // TODO: requires a messageQueue that expires
182
+ console.log('organization could not be found', data)
183
+ }
184
+ } else if (data.broadcastSender !== false) {
185
+ sockets.push(data.socket)
74
186
  }
75
- if (!clients.has(socket))
76
- clients.set(socket, true);
77
- else
78
- console.log('has client')
79
- if (user_id)
80
- this.emit('userStatus', socket, { socket, user_id, userStatus: 'on', organization_id });
187
+ return sockets
81
188
  }
82
189
 
83
- removeClient(socket) {
84
- let organization_id = socket.config.organization_id
85
- let user_id = socket.config.user_id
86
- let key = socket.config.key
87
- let clients = this.clients.get(key);
88
- clients.delete(socket);
190
+ delete(socket) {
191
+ let organization_id = socket.organization_id
192
+ if (this.organizations.has(organization_id)) {
193
+ const clients = this.organizations.get(organization_id).clients;
89
194
 
90
- if (user_id)
91
- this.emit('userStatus', socket, { socket, user_id, status: 'off', organization_id });
195
+ // Check if the client exists
196
+ if (clients && clients[socket.clientId]) {
197
+ const client = clients[socket.clientId]
92
198
 
93
- if (clients.size == 0) {
94
- this.organizations.delete(organization_id)
95
- // this.clients.delete(key);
96
- this.emit('disconnect', organization_id)
199
+ // Check if the socket exists in the client's sockets
200
+ const index = client.findIndex(item => item.id === socket.id);
201
+ if (index !== -1) {
202
+ client.splice(index, 1);
203
+ }
204
+
205
+ if (!client.length) {
206
+ delete clients[socket.clientId];
207
+ }
208
+
209
+ if (!Object.keys(clients).length) {
210
+ this.organizations.delete(socket.organization_id);
211
+ this.emit('update.object', {
212
+ method: 'update.object',
213
+ array: 'organizations',
214
+ object: {
215
+ _id: organization_id, ['$pull.activeHost']: socket.socketUrl
216
+ },
217
+ organization_id
218
+ });
219
+
220
+ this.emit('mesh.update', {
221
+ url: socket.socketUrl,
222
+ organization_id
223
+ });
224
+ }
225
+
226
+ }
227
+ }
228
+
229
+ if (this.clients.has(socket.clientId)) {
230
+ const client = this.clients.get(socket.clientId)
231
+ const index = client.findIndex(item => item.id === socket.id);
232
+ if (index !== -1) {
233
+ client.splice(index, 1);
234
+ }
235
+
236
+ if (!client.length) {
237
+ this.clients.delete(socket.clientId);
238
+ }
239
+ }
240
+
241
+ if (this.clients.size === 0) {
242
+ this.organizations.delete(socket.organization_id);
97
243
  }
98
244
 
245
+ this.sockets.delete(socket.id);
246
+
247
+ if (socket.user_id) {
248
+ let sockets = this.users.get(socket.user_id)
249
+ if (sockets) {
250
+ const index = sockets.findIndex(item => item.id === socket.id);
251
+ if (index !== -1) {
252
+ sockets.splice(index, 1);
253
+ }
254
+ if (!sockets.length) {
255
+ this.users.delete(socket.user_id);
256
+ this.emit('userStatus', { socket, user_id, status: 'off', organization_id });
257
+ }
258
+ }
259
+ }
99
260
  }
100
261
 
101
- async onMessage(req, socket, message) {
262
+ async onMessage(socket, message) {
102
263
  try {
103
- const organization_id = socket.config.organization_id
264
+ const organization_id = socket.organization_id
104
265
 
105
266
  this.emit("setBandwidth", {
106
267
  type: 'in',
@@ -108,34 +269,37 @@ class SocketServer extends EventEmitter {
108
269
  organization_id
109
270
  });
110
271
 
111
- let active = this.organizations.get(organization_id)
112
- if (active === false)
272
+ if (this.organizations.has(organization_id) && !this.organizations.get(organization_id).status)
113
273
  return this.send({ socket, method: 'Access Denied', balance: 'Your balance has fallen bellow 0' })
114
274
 
115
275
  let data = JSON.parse(message)
116
-
117
276
  if (data.method) {
118
- let user_id = null;
119
- if (this.authenticate)
120
- user_id = await this.authenticate.decodeToken(req);
277
+ if (data.method === 'region.added' || data.method === 'region.removed')
278
+ console.log('data.method: ', data.method)
279
+
280
+ if (socket.user_id && socket.expires && new Date().getTime() >= socket.expires) {
281
+ socket.user_id = socket.expires = null
282
+ this.send({ socket, method: 'updateUserStatus', userStatus: 'off', socketId: data.socketId, organization_id })
283
+ }
121
284
 
122
285
  if (this.authorize) {
286
+ if (!this.sockets.has(socket.id)) {
287
+ if (!this.organizations.get(organization_id).status)
288
+ return this.send({ socket, method: 'Access Denied', balance: 'Your balance has fallen bellow 0' })
289
+ }
290
+
123
291
  data.socket = socket
124
292
 
125
- data.host = this.getHost(req)
126
- const authorized = await this.authorize.check(data, user_id)
293
+ const authorized = await this.authorize.check(data, socket.user_id)
127
294
  if (authorized.storage === false) {
128
295
  data.database = process.env.organization_id
129
296
  data.organization_id = process.env.organization_id
130
297
 
131
- const authorized2 = await this.authorize.check(data, req, user_id)
298
+ const authorized2 = await this.authorize.check(data, req, socket.user_id)
132
299
  if (!authorized2 || authorized2.error) {
133
300
  return this.send({ socket, method: 'Access Denied', authorized2, ...data })
134
301
  }
135
302
  } else if (!authorized || authorized.error) {
136
- if (!user_id)
137
- this.send({ socket, method: 'updateUserStatus', userStatus: 'off', clientId: data.clientId, organization_id })
138
-
139
303
  return this.send({ socket, method: 'Access Denied', authorized, ...data })
140
304
  }
141
305
  // dburl is true and db does not have 'keys' array
@@ -148,15 +312,6 @@ class SocketServer extends EventEmitter {
148
312
  if (authorized.authorized)
149
313
  data = authorized.authorized
150
314
 
151
- if (user_id) {
152
- if (!socket.config.user_id) {
153
- socket.config.user_id = user_id
154
- this.emit('userStatus', { socket, method: 'userStatus', user_id, userStatus: 'on', organization_id });
155
- }
156
- } else {
157
- this.send({ socket, method: 'updateUserStatus', userStatus: 'off', clientId: data.clientId, organization_id })
158
- }
159
-
160
315
  this.emit(data.method, data);
161
316
 
162
317
  }
@@ -166,85 +321,77 @@ class SocketServer extends EventEmitter {
166
321
  }
167
322
  }
168
323
 
169
- broadcast(data) {
170
- if (!data.uid)
171
- data.uid = uid.generate()
172
-
173
- let organization_id = data.socket.config.organization_id;
174
- let url = `/${this.prefix}/${organization_id}`;
175
-
176
- let namespace = data.namespace;
177
- if (namespace) {
178
- url += `/${namespace}`
179
- }
324
+ async send(data) {
325
+ // const socket = this.sockets.get(data.socketId)
326
+ const socket = data.socket
180
327
 
328
+ if (data.sync) {
329
+ if (!data.object || !data.object.length)
330
+ return
181
331
 
182
- let rooms = data.room;
183
- if (rooms) {
184
- if (!rooms.isArray())
185
- rooms = [rooms]
186
- for (let room of rooms) {
187
- let url = url;
188
- url += `/${room}`;
332
+ for (let i = 0; i < data.object.length; i++) {
333
+ data.object[i].data._id = data.object[i]._id
334
+ data.object[i].data.sync = true
189
335
 
190
- this.send(data, url)
336
+ const authorized = await this.authorize.check(data.object[i].data, socket.user_id)
337
+ if (authorized && authorized.authorized)
338
+ socket.send(JSON.stringify(authorized.authorized));
339
+ else
340
+ socket.send(JSON.stringify(data.object[i].data));
191
341
  }
192
342
  } else {
193
- this.send(data, url)
194
- }
195
- }
343
+ const sent = []
344
+
345
+ const authorized = await this.authorize.check(data, socket.user_id)
346
+ if (authorized && authorized.authorized)
347
+ data = authorized.authorized
348
+
349
+ if (!data.method.startsWith('read.') || data.log) {
350
+ let object = { url: socket.socketUrl, data }
351
+ delete object.socket
352
+ this.emit('create.object', {
353
+ method: 'create.object',
354
+ array: 'message_log',
355
+ object,
356
+ organization_id: data.organization_id
357
+ });
358
+ }
196
359
 
197
- async send(data, url) {
198
- const socket = data.socket
199
- delete data.socket
200
-
201
- let clients
202
- if (!url)
203
- clients = new Map([[socket, true]])
204
- else
205
- clients = this.clients.get(url);
206
-
207
- if (clients) {
208
- for (let client of clients.keys()) {
209
- if (socket != client && data.broadcast != false || socket == client && data.broadcastSender != false) {
210
- const authorized = await this.authorize.check(data, socket.config.user_id)
211
- if (authorized && authorized.authorized)
212
- data = authorized.authorized
213
- let responseData = JSON.stringify(data);
214
- client.send(responseData);
215
-
216
- if (socket.config && socket.config.organization_id)
217
- this.emit("setBandwidth", {
218
- type: 'out',
219
- data: responseData,
220
- organization_id: socket.config.organization_id
221
- })
222
- }
360
+ let sockets = this.get(data);
361
+
362
+ delete data.socket
363
+
364
+ for (let i = 0; i < sockets.length; i++) {
365
+ const authorized = await this.authorize.check(data, sockets[i].user_id)
366
+ if (authorized && authorized.authorized)
367
+ sockets[i].send(JSON.stringify(authorized.authorized));
368
+ else
369
+ sockets[i].send(JSON.stringify(data));
370
+ sent.push(socket.clientId)
371
+ this.emit("setBandwidth", {
372
+ type: 'out',
373
+ data: authorized.authorized || data,
374
+ organization_id: socket.organization_id
375
+ })
223
376
  }
224
- }
225
377
 
378
+ // TODO: sent is an array of clientId's so that notification can send to subscribed clients that are not currently connected
379
+ this.emit("notification", { sent })
380
+ }
226
381
  }
227
382
 
228
- getKeyFromUrl(pathname) {
229
- var path = pathname.split("/");
230
- var params = {
231
- type: null,
232
- key: pathname
383
+ getConfigFromUrl(pathname) {
384
+ const path = pathname.split("/");
385
+ if (path[2]) {
386
+ path[2] = decodeURIComponent(path[2]);
387
+ path[2] = JSON.parse(path[2]) || {};
233
388
  }
234
- if (path.length > 0) {
235
- params.type = path[1];
236
- params.organization_id = path[2]
237
- }
238
- return params
239
- }
240
389
 
241
- getHost(req) {
242
- if (req.headers.origin && req.headers.origin !== 'null') {
243
- const url = new URL(req.headers.origin);
244
- return url.host;
245
- } else if (req.headers.host && req.headers.host !== 'null') {
246
- return req.headers.host;
390
+ const config = {
391
+ organization_id: path[1],
392
+ ...path[2]
247
393
  }
394
+ return config
248
395
  }
249
396
 
250
397
  }
package/src/mesh.js ADDED
@@ -0,0 +1,71 @@
1
+ const socket = require('@cocreate/socket-client')
2
+ const crud = require('@cocreate/crud-client')
3
+ const EventEmitter = require('events');
4
+ const eventEmitter = new EventEmitter();
5
+
6
+
7
+ const organizations = {}
8
+ const urls = new Map()
9
+ const servers = {}
10
+
11
+ async function addServer(data) {
12
+ if (!servers[data.url])
13
+ servers[data.url] = { [data.serverId]: { [data.organization_id]: true } }
14
+ else
15
+ servers[data.url][data.serverId][data.organization_id] = true
16
+
17
+ // TODO: needs to get orgaizations activeRegions
18
+ let activeRegions = await crud.send({
19
+ method: 'read.object',
20
+ array: 'organizations',
21
+ object: { _id: data.organization_id },
22
+ organization_id: data.organization_id
23
+ })
24
+
25
+ console.log('mesh activeRegions: ', activeRegions)
26
+ // socket.send({
27
+ // method: 'region.added',
28
+ // host: data.url,
29
+ // activeRegions: '',
30
+ // serverId: data.serverId,
31
+ // broadcast: false,
32
+ // broadcastSender: false,
33
+ // organization_id: data.organization_id
34
+ // })
35
+ }
36
+
37
+ function deleteServer(data) {
38
+ delete servers[data.url][data.serverId][data.organization_id]
39
+ if (!Object.keys(servers[data.url][data.serverId]).length)
40
+ delete servers[data.url][data.serverId]
41
+ if (!Object.keys(servers[data.url]).length) {
42
+ console.log('mesh deleteServer: ', data.url)
43
+
44
+ // socket.send({
45
+ // method: 'region.removed', // delete.region
46
+ // host: data.url,
47
+ // serverId: data.serverId,
48
+ // organization_id: data.organization_id
49
+ // })
50
+ socket.delete(data.url)
51
+ }
52
+
53
+ }
54
+
55
+ socket.listen('region.added', function (data) {
56
+ addServer(data)
57
+ });
58
+
59
+ socket.listen('region.removed', function (data) {
60
+ delete servers[data.url][data.serverId]
61
+ if (!Object.keys(servers[data.url]).length)
62
+ socket.delete(data.url)
63
+ });
64
+
65
+ eventEmitter.on('mesh.create', addServer);
66
+ eventEmitter.on('mesh.delete', deleteServer);
67
+
68
+ // eventEmitter.emit('customEvent', 'Hello, Node.js!');
69
+
70
+
71
+ // let response = await crud.send(request)