@app-connect/core 0.0.2 → 1.5.8

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/.env.test ADDED
@@ -0,0 +1,5 @@
1
+ NODE_ENV=test
2
+ APP_SERVER_SECRET_KEY='test-secret-key'
3
+ HASH_KEY='test-hash-key'
4
+ DISABLE_SYNC_DB_TABLE='true'
5
+ DATABASE_URL='sqlite::memory:'
package/README.md CHANGED
@@ -24,9 +24,14 @@ npm install @app-connect/core
24
24
 
25
25
  ```javascript
26
26
  const { createCoreApp, adapterRegistry } = require('@app-connect/core');
27
-
28
- // Register your CRM adapters
29
- adapterRegistry.registerAdapter('myCRM', myCRMAdapter);
27
+ const myCRMAdapter = require('./adapters/myCRM');
28
+ const manifest = require('./adapters/manifest.json');
29
+ // Set the default manifest for the adapter registry. This ensures that all adapters
30
+ // have access to the necessary configuration and metadata before registration.
31
+ adapterRegistry.setDefaultManifest(manifest);
32
+ // Register your CRM adapters. The default manifest must be set before registration
33
+ // to ensure proper initialization of the adapter with the required settings.
34
+ adapterRegistry.registerAdapter('myCRM', myCRMAdapter, manifest);
30
35
 
31
36
  // Create Express app with all core functionality
32
37
  const app = createCoreApp();
@@ -39,6 +44,69 @@ app.get('/my-custom-route', (req, res) => {
39
44
  exports.getServer = () => app;
40
45
  ```
41
46
 
47
+ ### Adapter Interface Registration
48
+
49
+ The adapter registry supports dynamic interface registration, allowing you to extend adapter functionality without modifying the original adapter:
50
+
51
+ ```javascript
52
+ const { adapterRegistry } = require('@app-connect/core');
53
+
54
+ // Register interface functions for a platform
55
+ async function customCreateCallLog({ user, contactInfo, authHeader, callLog, note }) {
56
+ // Custom implementation
57
+ return {
58
+ logId: 'custom-log-id',
59
+ returnMessage: {
60
+ message: 'Call logged with custom implementation',
61
+ messageType: 'success',
62
+ ttl: 2000
63
+ }
64
+ };
65
+ }
66
+
67
+ async function customFindContact({ user, authHeader, phoneNumber }) {
68
+ // Custom implementation
69
+ return [
70
+ {
71
+ id: 'custom-contact-id',
72
+ name: 'Custom Contact',
73
+ type: 'Contact',
74
+ phone: phoneNumber,
75
+ additionalInfo: null
76
+ }
77
+ ];
78
+ }
79
+
80
+ // Register interface functions
81
+ adapterRegistry.registerAdapterInterface('myCRM', 'createCallLog', customCreateCallLog);
82
+ adapterRegistry.registerAdapterInterface('myCRM', 'findContact', customFindContact);
83
+
84
+ // Register the base adapter
85
+ adapterRegistry.registerAdapter('myCRM', myCRMAdapter);
86
+
87
+ // Get composed adapter with interfaces
88
+ const composedAdapter = adapterRegistry.getAdapter('myCRM');
89
+ ```
90
+
91
+ **Interface-Only Adapters (No Base Adapter):**
92
+
93
+ ```javascript
94
+ // Register only interface functions, no base adapter
95
+ adapterRegistry.registerAdapterInterface('interfaceOnlyCRM', 'createCallLog', customCreateCallLog);
96
+ adapterRegistry.registerAdapterInterface('interfaceOnlyCRM', 'findContact', customFindContact);
97
+
98
+ // Get interface-only adapter
99
+ const interfaceOnlyAdapter = adapterRegistry.getAdapter('interfaceOnlyCRM');
100
+ console.log('Interface-only methods:', Object.keys(interfaceOnlyAdapter));
101
+ // Output: ['createCallLog', 'findContact']
102
+
103
+ // Later, you can add a base adapter
104
+ adapterRegistry.registerAdapter('interfaceOnlyCRM', myCRMAdapter);
105
+ const fullAdapter = adapterRegistry.getAdapter('interfaceOnlyCRM');
106
+ console.log('Full adapter methods:', Object.keys(fullAdapter));
107
+ // Output: ['getAuthType', 'getUserInfo', 'updateCallLog', 'unAuthorize', 'createContact', 'createCallLog', 'findContact']
108
+ ```
109
+
42
110
  ### Advanced Usage with Custom Middleware
43
111
 
44
112
  ```javascript
@@ -50,8 +118,12 @@ const {
50
118
  adapterRegistry
51
119
  } = require('@app-connect/core');
52
120
 
121
+ const myCRMAdapter = require('./adapters/myCRM');
122
+ const manifest = require('./adapters/manifest.json');
123
+ // Set manifest
124
+ adapterRegistry.setDefaultManifest(manifest);
53
125
  // Register adapters
54
- adapterRegistry.registerAdapter('myCRM', myCRMAdapter);
126
+ adapterRegistry.registerAdapter('myCRM', myCRMAdapter, manifest);
55
127
 
56
128
  // Initialize core services
57
129
  initializeCore();
@@ -125,13 +197,84 @@ Registers a CRM adapter.
125
197
  - `adapter` (Object): Adapter implementation
126
198
  - `manifest` (Object, optional): Adapter manifest
127
199
 
200
+ #### `adapterRegistry.registerAdapterInterface(platformName, interfaceName, interfaceFunction)`
201
+ Registers an interface function for a specific platform that will be composed with the adapter at retrieval time.
202
+
203
+ **Parameters:**
204
+ - `platformName` (String): Platform identifier (e.g., 'pipedrive', 'salesforce')
205
+ - `interfaceName` (String): Interface function name (e.g., 'createCallLog', 'findContact')
206
+ - `interfaceFunction` (Function): The interface function to register
207
+
208
+ **Example:**
209
+ ```javascript
210
+ async function customCreateCallLog({ user, contactInfo, authHeader, callLog, note }) {
211
+ // Custom implementation
212
+ return {
213
+ logId: 'custom-log-id',
214
+ returnMessage: {
215
+ message: 'Call logged with custom implementation',
216
+ messageType: 'success',
217
+ ttl: 2000
218
+ }
219
+ };
220
+ }
221
+
222
+ adapterRegistry.registerAdapterInterface('myCRM', 'createCallLog', customCreateCallLog);
223
+ ```
224
+
128
225
  #### `adapterRegistry.getAdapter(name)`
129
- Retrieves a registered adapter.
226
+ Retrieves a registered adapter with composed interfaces.
130
227
 
131
228
  **Parameters:**
132
229
  - `name` (String): Adapter name
133
230
 
134
- **Returns:** Adapter instance
231
+ **Returns:** Composed adapter object or interface-only object
232
+
233
+ **Behavior:**
234
+ - If adapter exists and interfaces exist: Returns composed adapter with both
235
+ - If adapter exists but no interfaces: Returns original adapter
236
+ - If no adapter but interfaces exist: Returns object with just interface functions
237
+ - If no adapter and no interfaces: Throws error
238
+
239
+ #### `adapterRegistry.getOriginalAdapter(name)`
240
+ Retrieves the original adapter without any composed interface functions.
241
+
242
+ **Parameters:**
243
+ - `name` (String): Adapter name
244
+
245
+ **Returns:** Original adapter object
246
+
247
+ #### `adapterRegistry.getPlatformInterfaces(platformName)`
248
+ Returns a Map of registered interface functions for a platform.
249
+
250
+ **Parameters:**
251
+ - `platformName` (String): Platform identifier
252
+
253
+ **Returns:** Map of interface functions
254
+
255
+ #### `adapterRegistry.hasPlatformInterface(platformName, interfaceName)`
256
+ Checks if a specific interface function is registered for a platform.
257
+
258
+ **Parameters:**
259
+ - `platformName` (String): Platform identifier
260
+ - `interfaceName` (String): Interface function name
261
+
262
+ **Returns:** Boolean indicating if interface exists
263
+
264
+ #### `adapterRegistry.unregisterAdapterInterface(platformName, interfaceName)`
265
+ Unregisters an interface function for a platform.
266
+
267
+ **Parameters:**
268
+ - `platformName` (String): Platform identifier
269
+ - `interfaceName` (String): Interface function name
270
+
271
+ #### `adapterRegistry.getAdapterCapabilities(platformName)`
272
+ Gets comprehensive information about an adapter including its capabilities and registered interfaces.
273
+
274
+ **Parameters:**
275
+ - `platformName` (String): Platform identifier
276
+
277
+ **Returns:** Object with adapter capabilities information
135
278
 
136
279
  ### Exported Components
137
280
 
@@ -234,9 +377,33 @@ The core package uses the following environment variables:
234
377
  - `IS_PROD` - Production environment flag
235
378
  - `DYNAMODB_LOCALHOST` - Local DynamoDB endpoint for development, used for lock cache
236
379
 
237
- ## Architecture
380
+ ## Adapter Interface Registration Benefits
381
+
382
+ ### Key Features
238
383
 
239
- The core package follows a modular architecture:
384
+ - **Composition over Mutation**: Interface functions are composed with adapters at retrieval time, preserving the original adapter
385
+ - **Dynamic Registration**: Register interface functions before or after adapter registration
386
+ - **Immutability**: Original adapter objects remain unchanged
387
+ - **Clean Separation**: Interface functions are kept separate from core adapter logic
388
+ - **Flexibility**: Support for interface-only adapters (no base adapter required)
389
+
390
+ ### Best Practices
391
+
392
+ 1. **Register Required Interfaces**: Register all required interface functions before using the adapter
393
+ 2. **Use Descriptive Names**: Use clear, descriptive names for interface functions
394
+ 3. **Handle Errors**: Implement proper error handling in interface functions
395
+ 4. **Test Composed Adapters**: Test the final composed adapter to ensure interfaces work correctly
396
+ 5. **Document Interfaces**: Document what each interface function does and expects
397
+
398
+ ### Use Cases
399
+
400
+ - **Extending Existing Adapters**: Add new functionality to existing adapters without modification
401
+ - **Progressive Enhancement**: Start with interfaces, add base adapter later
402
+ - **Testing**: Test interface functions separately from base adapters
403
+ - **Modular Development**: Develop interface functions independently
404
+ - **Plugin Architecture**: Create pluggable interface functions for different scenarios
405
+
406
+ ## Architecture
240
407
 
241
408
  ```
242
409
  Core Package
@@ -4,12 +4,67 @@ class AdapterRegistry {
4
4
  this.adapters = new Map();
5
5
  this.manifests = new Map();
6
6
  this.releaseNotes = {};
7
+ this.platformInterfaces = new Map(); // Store interface functions per platform
7
8
  }
8
9
 
9
10
  setDefaultManifest(manifest) {
10
11
  this.manifests.set('default', manifest);
11
12
  }
12
13
 
14
+ /**
15
+ * Register an interface function for a specific platform
16
+ * @param {string} platformName - Platform identifier (e.g., 'pipedrive', 'salesforce')
17
+ * @param {string} interfaceName - Interface function name (e.g., 'createCallLog', 'findContact')
18
+ * @param {Function} interfaceFunction - The interface function to register
19
+ */
20
+ registerAdapterInterface(platformName, interfaceName, interfaceFunction) {
21
+ if (typeof interfaceFunction !== 'function') {
22
+ throw new Error(`Interface function must be a function, got: ${typeof interfaceFunction}`);
23
+ }
24
+
25
+ if (!this.platformInterfaces.has(platformName)) {
26
+ this.platformInterfaces.set(platformName, new Map());
27
+ }
28
+
29
+ const platformInterfaceMap = this.platformInterfaces.get(platformName);
30
+ platformInterfaceMap.set(interfaceName, interfaceFunction);
31
+
32
+ console.log(`Registered interface function: ${platformName}.${interfaceName}`);
33
+ }
34
+
35
+ /**
36
+ * Get registered interface functions for a platform
37
+ * @param {string} platformName - Platform identifier
38
+ * @returns {Map} Map of interface functions
39
+ */
40
+ getPlatformInterfaces(platformName) {
41
+ return this.platformInterfaces.get(platformName) || new Map();
42
+ }
43
+
44
+ /**
45
+ * Check if an interface function is registered for a platform
46
+ * @param {string} platformName - Platform identifier
47
+ * @param {string} interfaceName - Interface function name
48
+ * @returns {boolean} True if interface is registered
49
+ */
50
+ hasPlatformInterface(platformName, interfaceName) {
51
+ const platformInterfaceMap = this.platformInterfaces.get(platformName);
52
+ return platformInterfaceMap ? platformInterfaceMap.has(interfaceName) : false;
53
+ }
54
+
55
+ /**
56
+ * Unregister an interface function for a platform
57
+ * @param {string} platformName - Platform identifier
58
+ * @param {string} interfaceName - Interface function name
59
+ */
60
+ unregisterAdapterInterface(platformName, interfaceName) {
61
+ const platformInterfaceMap = this.platformInterfaces.get(platformName);
62
+ if (platformInterfaceMap && platformInterfaceMap.has(interfaceName)) {
63
+ platformInterfaceMap.delete(interfaceName);
64
+ console.log(`Unregistered interface function: ${platformName}.${interfaceName}`);
65
+ }
66
+ }
67
+
13
68
  /**
14
69
  * Register an adapter with the core system
15
70
  * @param {string} platform - Platform identifier (e.g., 'pipedrive', 'salesforce')
@@ -29,16 +84,62 @@ class AdapterRegistry {
29
84
  }
30
85
 
31
86
  /**
32
- * Get adapter by platform name
87
+ * Get adapter by platform name with composed interfaces
33
88
  * @param {string} platform - Platform identifier
34
- * @returns {Object} Adapter implementation
89
+ * @returns {Object} Composed adapter with interface functions
35
90
  */
36
91
  getAdapter(platform) {
37
92
  const adapter = this.adapters.get(platform);
38
- if (!adapter) {
93
+ const platformInterfaceMap = this.platformInterfaces.get(platform);
94
+
95
+ // If no adapter and no interfaces, throw error
96
+ if (!adapter && (!platformInterfaceMap || platformInterfaceMap.size === 0)) {
39
97
  throw new Error(`Adapter not found for platform: ${platform}`);
40
98
  }
41
- return adapter;
99
+
100
+ // If no adapter but interfaces exist, create a composed object with just interfaces
101
+ if (!adapter && platformInterfaceMap && platformInterfaceMap.size > 0) {
102
+ const composedAdapter = {};
103
+
104
+ // Add interface functions to the composed adapter
105
+ for (const [interfaceName, interfaceFunction] of platformInterfaceMap) {
106
+ composedAdapter[interfaceName] = interfaceFunction;
107
+ }
108
+
109
+ console.log(`Returning interface-only adapter for platform: ${platform}`);
110
+ return composedAdapter;
111
+ }
112
+
113
+ // If adapter exists but no interfaces, return original adapter
114
+ if (adapter && (!platformInterfaceMap || platformInterfaceMap.size === 0)) {
115
+ return adapter;
116
+ }
117
+
118
+ // If both adapter and interfaces exist, create a composed object
119
+ const composedAdapter = Object.create(adapter);
120
+
121
+ // Add interface functions to the composed adapter
122
+ for (const [interfaceName, interfaceFunction] of platformInterfaceMap) {
123
+ // Only add if the interface doesn't already exist in the adapter
124
+ if (!Object.prototype.hasOwnProperty.call(adapter, interfaceName)) {
125
+ composedAdapter[interfaceName] = interfaceFunction;
126
+ }
127
+ }
128
+
129
+ return composedAdapter;
130
+ }
131
+
132
+ /**
133
+ * Get the original adapter without composed interfaces
134
+ * @param {string} platform - Platform identifier
135
+ * @returns {Object} Original adapter implementation
136
+ */
137
+ getOriginalAdapter(platform) {
138
+ const adapter = this.adapters.get(platform);
139
+ if (!adapter) {
140
+ throw new Error(`Adapter not found for platform: ${platform}`);
141
+ }
142
+ return adapter;
42
143
  }
43
144
 
44
145
  /**
@@ -98,6 +199,7 @@ class AdapterRegistry {
98
199
  unregisterAdapter(platform) {
99
200
  this.adapters.delete(platform);
100
201
  this.manifests.delete(platform);
202
+ this.platformInterfaces.delete(platform);
101
203
  console.log(`Unregistered adapter: ${platform}`);
102
204
  }
103
205
 
@@ -108,8 +210,38 @@ class AdapterRegistry {
108
210
  getReleaseNotes(platform) {
109
211
  return this.releaseNotes;
110
212
  }
213
+
214
+ /**
215
+ * Get adapter capabilities summary including composed interfaces
216
+ * @param {string} platform - Platform identifier
217
+ * @returns {Object} Adapter capabilities
218
+ */
219
+ getAdapterCapabilities(platform) {
220
+ const originalAdapter = this.getOriginalAdapter(platform);
221
+ const composedAdapter = this.getAdapter(platform);
222
+ const platformInterfaceMap = this.getPlatformInterfaces(platform);
223
+
224
+ const capabilities = {
225
+ platform,
226
+ originalMethods: Object.keys(originalAdapter),
227
+ composedMethods: Object.keys(composedAdapter),
228
+ registeredInterfaces: Array.from(platformInterfaceMap.keys()),
229
+ authType: null
230
+ };
231
+
232
+ // Get auth type if available
233
+ if (typeof originalAdapter.getAuthType === 'function') {
234
+ try {
235
+ capabilities.authType = originalAdapter.getAuthType();
236
+ } catch (error) {
237
+ capabilities.authType = 'unknown';
238
+ }
239
+ }
240
+
241
+ return capabilities;
242
+ }
111
243
  }
112
244
 
113
245
  // Export singleton instance
114
246
  const adapterRegistry = new AdapterRegistry();
115
- module.exports = adapterRegistry;
247
+ module.exports = adapterRegistry;
package/handlers/log.js CHANGED
@@ -4,8 +4,9 @@ const { MessageLogModel } = require('../models/messageLogModel');
4
4
  const { UserModel } = require('../models/userModel');
5
5
  const oauth = require('../lib/oauth');
6
6
  const errorMessage = require('../lib/generalErrorMessage');
7
- const { composeCallLog, getLogFormatType, FORMAT_TYPES } = require('../lib/callLogComposer');
7
+ const { composeCallLog, getLogFormatType } = require('../lib/callLogComposer');
8
8
  const adapterRegistry = require('../adapter/registry');
9
+ const { LOG_DETAILS_FORMAT_TYPE } = require('../lib/constants');
9
10
 
10
11
  async function createCallLog({ platform, userId, incomingData }) {
11
12
  try {
@@ -76,7 +77,7 @@ async function createCallLog({ platform, userId, incomingData }) {
76
77
  // Compose call log details centrally
77
78
  const logFormat = getLogFormatType(platform);
78
79
  let composedLogDetails = '';
79
- if (logFormat === FORMAT_TYPES.PLAIN_TEXT || logFormat === FORMAT_TYPES.HTML) {
80
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT || logFormat === LOG_DETAILS_FORMAT_TYPE.HTML || logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
80
81
  composedLogDetails = await composeCallLog({
81
82
  logFormat,
82
83
  callLog,
@@ -309,7 +310,7 @@ async function updateCallLog({ platform, userId, incomingData }) {
309
310
  let existingCallLogDetails = null; // Compose updated call log details centrally
310
311
  const logFormat = getLogFormatType(platform);
311
312
  let composedLogDetails = '';
312
- if (logFormat === FORMAT_TYPES.PLAIN_TEXT || logFormat === FORMAT_TYPES.HTML) {
313
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT || logFormat === LOG_DETAILS_FORMAT_TYPE.HTML || logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
313
314
  let existingBody = '';
314
315
  try {
315
316
  const getLogResult = await platformModule.getCallLog({
package/index.js CHANGED
@@ -76,7 +76,7 @@ function createCoreRouter() {
76
76
  const versions = Object.keys(adapterReleaseNotes);
77
77
  for (const version of versions) {
78
78
  mergedReleaseNotes[version] = {
79
- global: globalReleaseNotes[version].global,
79
+ global: globalReleaseNotes[version]?.global ?? {},
80
80
  ...adapterReleaseNotes[version] ?? {}
81
81
  };
82
82
  }
@@ -229,7 +229,8 @@ function createCoreRouter() {
229
229
  platformName = unAuthData?.platform ?? 'Unknown';
230
230
  const user = await UserModel.findByPk(unAuthData?.id);
231
231
  if (!user) {
232
- res.status(400).send();
232
+ res.status(400).send('User not found');
233
+ return;
233
234
  }
234
235
  const { isValidated, rcAccountId } = await adminCore.validateAdminRole({ rcAccessToken: req.query.rcAccessToken });
235
236
  const hashedRcAccountId = util.getHashValue(rcAccountId, process.env.HASH_KEY);
@@ -402,7 +403,8 @@ function createCoreRouter() {
402
403
  platformName = unAuthData?.platform ?? 'Unknown';
403
404
  const user = await UserModel.findByPk(unAuthData?.id);
404
405
  if (!user) {
405
- res.status(400).send();
406
+ res.status(400).send('User not found');
407
+ return;
406
408
  }
407
409
  else {
408
410
  const rcAccessToken = req.query.rcAccessToken;
package/jest.config.js ADDED
@@ -0,0 +1,57 @@
1
+ const path = require('path');
2
+
3
+ module.exports = {
4
+ // Test environment
5
+ testEnvironment: 'node',
6
+
7
+ // Test file patterns
8
+ testMatch: [
9
+ '<rootDir>/test/**/*.test.js',
10
+ '<rootDir>/**/*.test.js'
11
+ ],
12
+
13
+ // Setup files
14
+ setupFilesAfterEnv: [
15
+ '<rootDir>/test/setup.js'
16
+ ],
17
+
18
+ // Coverage configuration
19
+ collectCoverage: true,
20
+ coverageDirectory: '<rootDir>/coverage',
21
+ coverageReporters: ['text', 'lcov', 'html'],
22
+ coveragePathIgnorePatterns: [
23
+ '/node_modules/',
24
+ '/test/',
25
+ '/coverage/',
26
+ 'jest.config.js',
27
+ 'setup.js'
28
+ ],
29
+
30
+ // Module resolution
31
+ moduleDirectories: ['node_modules', '<rootDir>'],
32
+ moduleNameMapper: {
33
+ '^@app-connect/core/(.*)$': '<rootDir>/$1'
34
+ },
35
+
36
+ // Test timeout
37
+ testTimeout: 30000,
38
+
39
+ // Reporters
40
+ reporters: ['default'],
41
+
42
+ // Ignore patterns
43
+ modulePathIgnorePatterns: [
44
+ '<rootDir>/node_modules/',
45
+ '<rootDir>/coverage/',
46
+ '<rootDir>/test-results/'
47
+ ],
48
+
49
+ // Clear mocks between tests
50
+ clearMocks: true,
51
+
52
+ // Restore mocks between tests
53
+ restoreMocks: true,
54
+
55
+ // Verbose output
56
+ verbose: true
57
+ };