@app-connect/core 1.7.18 → 1.7.20

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 (60) hide show
  1. package/connector/proxy/index.js +2 -1
  2. package/handlers/auth.js +30 -55
  3. package/handlers/log.js +182 -10
  4. package/handlers/plugin.js +27 -0
  5. package/handlers/user.js +31 -2
  6. package/index.js +115 -22
  7. package/lib/authSession.js +21 -12
  8. package/lib/callLogComposer.js +1 -1
  9. package/lib/debugTracer.js +20 -2
  10. package/lib/util.js +21 -4
  11. package/mcp/README.md +395 -0
  12. package/mcp/mcpHandler.js +318 -82
  13. package/mcp/tools/checkAuthStatus.js +28 -35
  14. package/mcp/tools/createCallLog.js +13 -9
  15. package/mcp/tools/createContact.js +2 -6
  16. package/mcp/tools/doAuth.js +27 -157
  17. package/mcp/tools/findContactByName.js +6 -9
  18. package/mcp/tools/findContactByPhone.js +2 -6
  19. package/mcp/tools/getGoogleFilePicker.js +5 -9
  20. package/mcp/tools/getHelp.js +2 -3
  21. package/mcp/tools/getPublicConnectors.js +55 -24
  22. package/mcp/tools/index.js +11 -36
  23. package/mcp/tools/logout.js +32 -13
  24. package/mcp/tools/rcGetCallLogs.js +3 -20
  25. package/mcp/ui/App/App.tsx +358 -0
  26. package/mcp/ui/App/components/AuthInfoForm.tsx +113 -0
  27. package/mcp/ui/App/components/AuthSuccess.tsx +22 -0
  28. package/mcp/ui/App/components/ConnectorList.tsx +82 -0
  29. package/mcp/ui/App/components/DebugPanel.tsx +43 -0
  30. package/mcp/ui/App/components/OAuthConnect.tsx +270 -0
  31. package/mcp/ui/App/lib/callTool.ts +130 -0
  32. package/mcp/ui/App/lib/debugLog.ts +41 -0
  33. package/mcp/ui/App/lib/developerPortal.ts +111 -0
  34. package/mcp/ui/App/main.css +6 -0
  35. package/mcp/ui/App/root.tsx +13 -0
  36. package/mcp/ui/dist/index.html +53 -0
  37. package/mcp/ui/index.html +13 -0
  38. package/mcp/ui/package-lock.json +6356 -0
  39. package/mcp/ui/package.json +25 -0
  40. package/mcp/ui/tsconfig.json +26 -0
  41. package/mcp/ui/vite.config.ts +16 -0
  42. package/models/llmSessionModel.js +14 -0
  43. package/models/userModel.js +3 -0
  44. package/package.json +2 -2
  45. package/releaseNotes.json +24 -0
  46. package/test/handlers/auth.test.js +31 -0
  47. package/test/handlers/plugin.test.js +287 -0
  48. package/test/lib/util.test.js +379 -1
  49. package/test/mcp/tools/createCallLog.test.js +3 -3
  50. package/test/mcp/tools/doAuth.test.js +40 -303
  51. package/test/mcp/tools/findContactByName.test.js +3 -3
  52. package/test/mcp/tools/findContactByPhone.test.js +3 -3
  53. package/test/mcp/tools/getGoogleFilePicker.test.js +7 -7
  54. package/test/mcp/tools/getPublicConnectors.test.js +49 -70
  55. package/test/mcp/tools/logout.test.js +17 -11
  56. package/mcp/SupportedPlatforms.md +0 -12
  57. package/mcp/tools/collectAuthInfo.js +0 -91
  58. package/mcp/tools/setConnector.js +0 -69
  59. package/test/mcp/tools/collectAuthInfo.test.js +0 -234
  60. package/test/mcp/tools/setConnector.test.js +0 -177
@@ -51,7 +51,7 @@ function getBasicAuth({ apiKey }) {
51
51
  return Buffer.from(`${apiKey}:`).toString('base64');
52
52
  }
53
53
 
54
- async function getUserInfo({ authHeader, hostname, additionalInfo, platform, apiKey, proxyId, proxyConfig } = {}) {
54
+ async function getUserInfo({ authHeader, hostname, additionalInfo, platform, apiKey, proxyId, proxyConfig, userEmail } = {}) {
55
55
  const cfg = proxyConfig ? proxyConfig : (await loadPlatformConfig(proxyId));
56
56
  if (!cfg || !cfg.operations?.getUserInfo) {
57
57
  // Fallback if no getUserInfo operation defined
@@ -72,6 +72,7 @@ async function getUserInfo({ authHeader, hostname, additionalInfo, platform, api
72
72
  apiKey,
73
73
  hostname,
74
74
  platform,
75
+ userEmail,
75
76
  },
76
77
  user: {},
77
78
  authHeader
package/handlers/auth.js CHANGED
@@ -7,7 +7,7 @@ const adminCore = require('./admin');
7
7
  const { Connector } = require('../models/dynamo/connectorSchema');
8
8
  const { handleDatabaseError } = require('../lib/errorHandler');
9
9
 
10
- async function onOAuthCallback({ platform, hostname, tokenUrl, query, isFromMCP = false }) {
10
+ async function onOAuthCallback({ platform, hostname, tokenUrl, query, hashedRcExtensionId, isFromMCP = false }) {
11
11
  const callbackUri = query.callbackUri;
12
12
  const apiUrl = query.apiUrl;
13
13
  const username = query.username;
@@ -54,6 +54,7 @@ async function onOAuthCallback({ platform, hostname, tokenUrl, query, isFromMCP
54
54
  refreshToken,
55
55
  tokenExpiry: isNaN(expires) ? null : expires,
56
56
  rcAccountId: query?.rcAccountId,
57
+ hashedRcExtensionId,
57
58
  proxyId
58
59
  });
59
60
  }
@@ -76,7 +77,7 @@ async function onOAuthCallback({ platform, hostname, tokenUrl, query, isFromMCP
76
77
  }
77
78
  }
78
79
 
79
- async function onApiKeyLogin({ platform, hostname, apiKey, proxyId, additionalInfo }) {
80
+ async function onApiKeyLogin({ platform, hostname, apiKey, proxyId, rcAccountId, hashedRcExtensionId, additionalInfo }) {
80
81
  const platformModule = connectorRegistry.getConnector(platform);
81
82
  const basicAuth = platformModule.getBasicAuth({ apiKey });
82
83
  const { successful, platformUserInfo, returnMessage } = await platformModule.getUserInfo({ authHeader: `Basic ${basicAuth}`, hostname, platform, additionalInfo, apiKey, proxyId });
@@ -88,6 +89,8 @@ async function onApiKeyLogin({ platform, hostname, apiKey, proxyId, additionalIn
88
89
  platform,
89
90
  hostname,
90
91
  proxyId,
92
+ hashedRcExtensionId,
93
+ rcAccountId,
91
94
  accessToken: platformUserInfo.overridingApiKey ?? apiKey
92
95
  });
93
96
  }
@@ -110,7 +113,7 @@ async function onApiKeyLogin({ platform, hostname, apiKey, proxyId, additionalIn
110
113
  }
111
114
  }
112
115
 
113
- async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken, refreshToken, tokenExpiry, rcAccountId, proxyId }) {
116
+ async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken, refreshToken, tokenExpiry, rcAccountId, hashedRcExtensionId, proxyId }) {
114
117
  const id = platformUserInfo.id;
115
118
  const name = platformUserInfo.name;
116
119
  const existingUser = await UserModel.findByPk(id);
@@ -130,6 +133,7 @@ async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken,
130
133
  refreshToken,
131
134
  tokenExpiry,
132
135
  rcAccountId,
136
+ hashedRcExtensionId,
133
137
  platformAdditionalInfo: {
134
138
  ...existingUser.platformAdditionalInfo, // keep existing platformAdditionalInfo
135
139
  ...platformAdditionalInfo,
@@ -143,57 +147,20 @@ async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken,
143
147
  }
144
148
  else {
145
149
  try {
146
- // TEMP: replace user with old ID
147
- if (id.endsWith(`-${platform}`)) {
148
- const oldID = id.split('-');
149
- const userWithOldID = await UserModel.findByPk(oldID[0]);
150
- if (userWithOldID) {
151
- await UserModel.create({
152
- id,
153
- hostname,
154
- timezoneName,
155
- timezoneOffset,
156
- platform,
157
- accessToken,
158
- refreshToken,
159
- tokenExpiry,
160
- rcAccountId,
161
- platformAdditionalInfo,
162
- userSettings: userWithOldID.userSettings
163
- });
164
- await userWithOldID.destroy();
165
- }
166
- else {
167
- await UserModel.create({
168
- id,
169
- hostname,
170
- timezoneName,
171
- timezoneOffset,
172
- platform,
173
- accessToken,
174
- refreshToken,
175
- tokenExpiry,
176
- rcAccountId,
177
- platformAdditionalInfo,
178
- userSettings: {}
179
- });
180
- }
181
- }
182
- else {
183
- await UserModel.create({
184
- id,
185
- hostname,
186
- timezoneName,
187
- timezoneOffset,
188
- platform,
189
- accessToken,
190
- refreshToken,
191
- tokenExpiry,
192
- rcAccountId,
193
- platformAdditionalInfo,
194
- userSettings: {}
195
- });
196
- }
150
+ await UserModel.create({
151
+ id,
152
+ hostname,
153
+ timezoneName,
154
+ timezoneOffset,
155
+ platform,
156
+ accessToken,
157
+ refreshToken,
158
+ tokenExpiry,
159
+ rcAccountId,
160
+ hashedRcExtensionId,
161
+ platformAdditionalInfo,
162
+ userSettings: {}
163
+ });
197
164
  }
198
165
  catch (error) {
199
166
  return handleDatabaseError(error, 'Error saving user info');
@@ -206,8 +173,16 @@ async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken,
206
173
  }
207
174
 
208
175
  async function getLicenseStatus({ userId, platform }) {
176
+ const user = await UserModel.findByPk(userId);
177
+ if (!user) {
178
+ return {
179
+ isLicenseValid: false,
180
+ licenseStatus: 'Invalid (User not found)',
181
+ licenseStatusDescription: ''
182
+ }
183
+ }
209
184
  const platformModule = connectorRegistry.getConnector(platform);
210
- const licenseStatus = await platformModule.getLicenseStatus({ userId, platform });
185
+ const licenseStatus = await platformModule.getLicenseStatus({ userId, platform, user });
211
186
  return licenseStatus;
212
187
  }
213
188
 
package/handlers/log.js CHANGED
@@ -2,6 +2,7 @@ const Op = require('sequelize').Op;
2
2
  const { CallLogModel } = require('../models/callLogModel');
3
3
  const { MessageLogModel } = require('../models/messageLogModel');
4
4
  const { UserModel } = require('../models/userModel');
5
+ const { CacheModel } = require('../models/cacheModel');
5
6
  const oauth = require('../lib/oauth');
6
7
  const { composeCallLog } = require('../lib/callLogComposer');
7
8
  const { composeSharedSMSLog } = require('../lib/sharedSMSComposer');
@@ -11,11 +12,14 @@ const { NoteCache } = require('../models/dynamo/noteCacheSchema');
11
12
  const { Connector } = require('../models/dynamo/connectorSchema');
12
13
  const moment = require('moment');
13
14
  const { getMediaReaderLinkByPlatformMediaLink } = require('../lib/util');
15
+ const axios = require('axios');
16
+ const { getPluginsFromUserSettings } = require('../lib/util');
14
17
  const logger = require('../lib/logger');
15
18
  const { handleApiError, handleDatabaseError } = require('../lib/errorHandler');
19
+ const { v4: uuidv4 } = require('uuid');
16
20
  const { AccountDataModel } = require('../models/accountDataModel');
17
21
 
18
- async function createCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
22
+ async function createCallLog({ jwtToken, platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
19
23
  try {
20
24
  let existingCallLog = null;
21
25
  try {
@@ -55,6 +59,7 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
55
59
  }
56
60
  };
57
61
  }
62
+
58
63
  const platformModule = connectorRegistry.getConnector(platform);
59
64
  const callLog = incomingData.logInfo;
60
65
  const additionalSubmission = incomingData.additionalSubmission;
@@ -115,6 +120,64 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
115
120
  name: incomingData.contactName ?? ""
116
121
  };
117
122
 
123
+
124
+ const pluginAsyncTaskIds = [];
125
+ // Plugins
126
+ const loggingPlugins = getPluginsFromUserSettings({ userSettings: user.userSettings, logType: 'call' });
127
+ for (const pluginSetting of loggingPlugins) {
128
+ const pluginId = pluginSetting.id;
129
+ let pluginDataResponse = null;
130
+ switch (pluginSetting.value.access) {
131
+ case 'public':
132
+ pluginDataResponse = await axios.get(`https://appconnect.labs.ringcentral.com/public-api/connectors/${pluginId}/manifest?type=plugin`);
133
+ break;
134
+ case 'private':
135
+ case 'shared':
136
+ pluginDataResponse = await axios.get(`https://appconnect.labs.ringcentral.com/public-api/connectors/${pluginId}/manifest?access=internal&type=connector&accountId=${user.rcAccountId}`);
137
+ break;
138
+ default:
139
+ throw new Error('Invalid plugin access');
140
+ }
141
+ const pluginData = pluginDataResponse.data;
142
+ const pluginManifest = pluginData.platforms[pluginSetting.value.name];
143
+ let pluginEndpointUrl = pluginManifest.endpointUrl;
144
+ if (!pluginEndpointUrl) {
145
+ throw new Error('Plugin URL is not set');
146
+ }
147
+ else {
148
+ // check if endpoint has query params already
149
+ if (pluginEndpointUrl.includes('?')) {
150
+ pluginEndpointUrl += `&jwtToken=${jwtToken}`;
151
+ }
152
+ else {
153
+ pluginEndpointUrl += `?jwtToken=${jwtToken}`;
154
+ }
155
+ }
156
+ if (pluginSetting.value.isAsync) {
157
+ const asyncTaskId = `${userId}-${uuidv4()}`;
158
+ pluginAsyncTaskIds.push(asyncTaskId);
159
+ await CacheModel.create({
160
+ id: asyncTaskId,
161
+ status: 'initialized',
162
+ userId,
163
+ cacheKey: `pluginTask-${pluginSetting.value.name}`,
164
+ expiry: moment().add(1, 'hour').toDate()
165
+ });
166
+ axios.post(pluginEndpointUrl, {
167
+ data: incomingData,
168
+ asyncTaskId
169
+ });
170
+ }
171
+ else {
172
+ const processedResultResponse = await axios.post(pluginEndpointUrl, {
173
+ data: incomingData
174
+ });
175
+ // eslint-disable-next-line no-param-reassign
176
+ incomingData = processedResultResponse.data;
177
+ note = incomingData.note;
178
+ }
179
+ }
180
+
118
181
  // Compose call log details centrally
119
182
  const logFormat = platformModule.getLogFormatType ? platformModule.getLogFormatType(platform, proxyConfig) : LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT;
120
183
  let composedLogDetails = '';
@@ -179,8 +242,8 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
179
242
  catch (error) {
180
243
  return handleDatabaseError(error, 'Error creating call log');
181
244
  }
245
+ return { successful: !!logId, logId, returnMessage, extraDataTracking, pluginAsyncTaskIds };
182
246
  }
183
- return { successful: !!logId, logId, returnMessage, extraDataTracking };
184
247
  } catch (e) {
185
248
  return handleApiError(e, platform, 'createCallLog', { userId });
186
249
  }
@@ -285,7 +348,7 @@ async function getCallLog({ userId, sessionIds, platform, requireDetails }) {
285
348
  }
286
349
  }
287
350
 
288
- async function updateCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
351
+ async function updateCallLog({ jwtToken, platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
289
352
  try {
290
353
  let existingCallLog = null;
291
354
  try {
@@ -299,11 +362,11 @@ async function updateCallLog({ platform, userId, incomingData, hashedAccountId,
299
362
  return handleDatabaseError(error, 'Error finding existing call log');
300
363
  }
301
364
  if (existingCallLog) {
302
- const platformModule = connectorRegistry.getConnector(platform);
303
365
  let user = await UserModel.findByPk(userId);
304
366
  if (!user || !user.accessToken) {
305
367
  return { successful: false, message: `Contact not found` };
306
368
  }
369
+ const platformModule = connectorRegistry.getConnector(platform);
307
370
  const proxyId = user.platformAdditionalInfo?.proxyId;
308
371
  let proxyConfig = null;
309
372
  if (proxyId) {
@@ -334,6 +397,61 @@ async function updateCallLog({ platform, userId, incomingData, hashedAccountId,
334
397
  break;
335
398
  }
336
399
 
400
+ const pluginAsyncTaskIds = [];
401
+ // Plugins
402
+ const plugins = getPluginsFromUserSettings({ userSettings: user.userSettings, logType: 'call' });
403
+ for (const pluginSetting of plugins) {
404
+ const pluginId = pluginSetting.id;
405
+ let pluginDataResponse = null;
406
+ switch (pluginSetting.value.access) {
407
+ case 'public':
408
+ pluginDataResponse = await axios.get(`${process.env.DEV_PORTAL_URL}/public-api/connectors/${pluginId}/manifest?type=plugin`);
409
+ break;
410
+ case 'private':
411
+ case 'shared':
412
+ pluginDataResponse = await axios.get(`${process.env.DEV_PORTAL_URL}/public-api/connectors/${pluginId}/manifest?access=internal&type=connector&accountId=${user.rcAccountId}`);
413
+ break;
414
+ default:
415
+ throw new Error('Invalid plugin access');
416
+ }
417
+ const pluginData = pluginDataResponse.data;
418
+ const pluginManifest = pluginData.platforms[pluginSetting.value.name];
419
+ let pluginEndpointUrl = pluginManifest.endpointUrl;
420
+ if (!pluginEndpointUrl) {
421
+ throw new Error('Plugin URL is not set');
422
+ }
423
+ else {
424
+ if (pluginEndpointUrl.includes('?')) {
425
+ pluginEndpointUrl += `&jwtToken=${jwtToken}`;
426
+ }
427
+ else {
428
+ pluginEndpointUrl += `?jwtToken=${jwtToken}`;
429
+ }
430
+ }
431
+ if (pluginSetting.value.isAsync) {
432
+ const asyncTaskId = `${userId}-${uuidv4()}`;
433
+ pluginAsyncTaskIds.push(asyncTaskId);
434
+ await CacheModel.create({
435
+ id: asyncTaskId,
436
+ status: 'initialized',
437
+ userId,
438
+ cacheKey: `pluginTask-${pluginSetting.value.name}`,
439
+ expiry: moment().add(1, 'hour').toDate()
440
+ });
441
+ axios.post(pluginEndpointUrl, {
442
+ data: { logInfo: incomingData },
443
+ asyncTaskId
444
+ });
445
+ }
446
+ else {
447
+ const processedResultResponse = await axios.post(pluginEndpointUrl, {
448
+ data: incomingData
449
+ });
450
+ // eslint-disable-next-line no-param-reassign
451
+ incomingData = processedResultResponse.data;
452
+ }
453
+ }
454
+
337
455
  // Fetch existing call log details once to avoid duplicate API calls
338
456
  let existingCallLogDetails = null; // Compose updated call log details centrally
339
457
  const logFormat = platformModule.getLogFormatType ? platformModule.getLogFormatType(platform, proxyConfig) : LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT;
@@ -416,12 +534,7 @@ async function updateCallLog({ platform, userId, incomingData, hashedAccountId,
416
534
  isFromSSCL,
417
535
  proxyConfig,
418
536
  });
419
- if (!extraDataTracking) {
420
- extraDataTracking = {};
421
- }
422
- extraDataTracking.withSmartNoteLog = !!incomingData.aiNote;
423
- extraDataTracking.withTranscript = !!incomingData.transcript;
424
- return { successful: true, logId: existingCallLog.thirdPartyLogId, updatedNote, returnMessage, extraDataTracking };
537
+ return { successful: true, logId: existingCallLog.thirdPartyLogId, updatedNote, returnMessage, extraDataTracking, pluginAsyncTaskIds };
425
538
  }
426
539
  return { successful: false };
427
540
  } catch (e) {
@@ -518,6 +631,65 @@ async function createMessageLog({ platform, userId, incomingData }) {
518
631
  const ownerName = incomingData.logInfo.owner?.name;
519
632
  const isSharedSMS = !!ownerName;
520
633
 
634
+ const pluginAsyncTaskIds = [];
635
+ // Plugins
636
+ const isSMS = incomingData.logInfo.messages.some(m => m.type === 'SMS');
637
+ const isFax = incomingData.logInfo.messages.some(m => m.type === 'Fax');
638
+ const smsPlugins = isSMS ? getPluginsFromUserSettings({ userSettings: user.userSettings, logType: 'sms' }) : [];
639
+ const faxPlugins = isFax ? getPluginsFromUserSettings({ userSettings: user.userSettings, logType: 'fax' }) : [];
640
+ const plugins = [...smsPlugins, ...faxPlugins];
641
+ for (const pluginSetting of plugins) {
642
+ const pluginId = pluginSetting.id;
643
+ let pluginDataResponse = null;
644
+ switch (pluginSetting.value.access) {
645
+ case 'public':
646
+ pluginDataResponse = await axios.get(`${process.env.DEV_PORTAL_URL}/public-api/connectors/${pluginId}/manifest?type=plugin`);
647
+ break;
648
+ case 'private':
649
+ case 'shared':
650
+ pluginDataResponse = await axios.get(`${process.env.DEV_PORTAL_URL}/public-api/connectors/${pluginId}/manifest?access=internal&type=connector&accountId=${user.rcAccountId}`);
651
+ break;
652
+ default:
653
+ throw new Error('Invalid plugin access');
654
+ }
655
+ const pluginData = pluginDataResponse.data;
656
+ const pluginManifest = pluginData.platforms[pluginSetting.value.name];
657
+ let pluginEndpointUrl = pluginManifest.endpointUrl;
658
+ if (!pluginEndpointUrl) {
659
+ throw new Error('Plugin URL is not set');
660
+ }
661
+ else {
662
+ if (pluginEndpointUrl.includes('?')) {
663
+ pluginEndpointUrl += `&jwtToken=${jwtToken}`;
664
+ }
665
+ else {
666
+ pluginEndpointUrl += `?jwtToken=${jwtToken}`;
667
+ }
668
+ }
669
+ if (pluginSetting.value.isAsync) {
670
+ const asyncTaskId = `${userId}-${uuidv4()}`;
671
+ pluginAsyncTaskIds.push(asyncTaskId);
672
+ await CacheModel.create({
673
+ id: asyncTaskId,
674
+ status: 'initialized',
675
+ userId,
676
+ cacheKey: `pluginTask-${pluginSetting.value.name}`,
677
+ expiry: moment().add(1, 'hour').toDate()
678
+ });
679
+ axios.post(pluginEndpointUrl, {
680
+ data: { logInfo: incomingData },
681
+ asyncTaskId
682
+ });
683
+ }
684
+ else {
685
+ const processedResultResponse = await axios.post(pluginEndpointUrl, {
686
+ data: incomingData
687
+ });
688
+ // eslint-disable-next-line no-param-reassign
689
+ incomingData = processedResultResponse.data;
690
+ }
691
+ }
692
+
521
693
  let messageIds = [];
522
694
  const correspondents = [];
523
695
  if (isGroupSMS) {
@@ -0,0 +1,27 @@
1
+ const { CacheModel } = require('../models/cacheModel');
2
+ const { Op } = require('sequelize');
3
+
4
+ async function getPluginAsyncTasks({ asyncTaskIds }) {
5
+ const caches = await CacheModel.findAll({
6
+ where: {
7
+ id: {
8
+ [Op.in]: asyncTaskIds
9
+ }
10
+ }
11
+ });
12
+ const result = caches.map(cache => ({
13
+ cacheKey: cache.cacheKey,
14
+ status: cache.status
15
+ }));
16
+ const toRemoveCaches = caches.filter(cache => cache.status === 'completed' || cache.status === 'failed');
17
+ await CacheModel.destroy({
18
+ where: {
19
+ id: {
20
+ [Op.in]: toRemoveCaches.map(cache => cache.id)
21
+ }
22
+ }
23
+ });
24
+ return result;
25
+ }
26
+
27
+ exports.getPluginAsyncTasks = getPluginAsyncTasks;
package/handlers/user.js CHANGED
@@ -49,7 +49,10 @@ async function getUserSettings({ user, rcAccessToken, rcAccountId }) {
49
49
  const keys = Object.keys(userSettingsByAdmin.userSettings).concat(Object.keys(userSettings));
50
50
  // distinct keys
51
51
  for (const key of new Set(keys)) {
52
- // from user's own settings
52
+ // marked as removed
53
+ if (userSettingsByAdmin.userSettings[key]?.isRemoved) {
54
+ continue;
55
+ }
53
56
  if ((userSettingsByAdmin.userSettings[key] === undefined || userSettingsByAdmin.userSettings[key].customizable) && userSettings[key] !== undefined) {
54
57
  result[key] = {
55
58
  customizable: true,
@@ -57,6 +60,27 @@ async function getUserSettings({ user, rcAccessToken, rcAccountId }) {
57
60
  defaultValue: userSettings[key].defaultValue,
58
61
  options: userSettings[key].options
59
62
  };
63
+ // Special case: plugins
64
+ if (key.startsWith('plugin_')) {
65
+ const config = Object.keys(result[key].value.config)?.length === 0 ? null : result[key].value.config;
66
+ if (config) {
67
+ const configFromadminSettings = userSettingsByAdmin.userSettings[key].value.config ?? {};
68
+ for (const k in config) {
69
+ // use admin setting to replace, if not customizable
70
+ if (configFromadminSettings[k] && !configFromadminSettings[k].customizable || !config[k].value && configFromadminSettings[k].value) {
71
+ config[k] = configFromadminSettings[k];
72
+ }
73
+ else {
74
+ config[k].customizable = configFromadminSettings[k].customizable;
75
+ }
76
+ }
77
+ result[key].value.config = config;
78
+ }
79
+ //Case: no config at all, use admin setting directly
80
+ else {
81
+ result[key].value.config = userSettingsByAdmin.userSettings[key].value.config;
82
+ }
83
+ }
60
84
  }
61
85
  // from admin settings
62
86
  else {
@@ -68,7 +92,7 @@ async function getUserSettings({ user, rcAccessToken, rcAccountId }) {
68
92
  return result;
69
93
  }
70
94
 
71
- async function updateUserSettings({ user, userSettings, platformName }) {
95
+ async function updateUserSettings({ user, userSettings, settingKeysToRemove, platformName }) {
72
96
  const keys = Object.keys(userSettings || {});
73
97
  let updatedSettings = {
74
98
  ...(user.userSettings || {})
@@ -76,6 +100,11 @@ async function updateUserSettings({ user, userSettings, platformName }) {
76
100
  for (const k of keys) {
77
101
  updatedSettings[k] = userSettings[k];
78
102
  }
103
+ for (const k of settingKeysToRemove) {
104
+ if (updatedSettings[k]) {
105
+ delete updatedSettings[k];
106
+ }
107
+ }
79
108
  const platformModule = connectorRegistry.getConnector(platformName);
80
109
  if (platformModule.onUpdateUserSettings) {
81
110
  const { successful, returnMessage } = await platformModule.onUpdateUserSettings({ user, userSettings, updatedSettings });