zoho crm flow added

This commit is contained in:
yashwin-foxy 2025-09-11 17:12:47 +05:30
parent 1dae751ce4
commit 53e0f7ba50
12 changed files with 736 additions and 28 deletions

3
.env Normal file
View File

@ -0,0 +1,3 @@
ZOHO_CLIENT_ID=1000.PY0FB5ABLLFK2WIBDCNCGKQ0EUIJMY
ZOHO_CLIENT_SECRET=772c42df00054668efb6a5839f1874b1dc89e1a127
ZOHO_REDIRECT_URI=centralizedreportingsystem://oauth/callback

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
node_modules/
.env
uploads/
npm-debug.log*
coverage/

View File

@ -7,11 +7,11 @@ const session = require('../../auth/session.service');
async function login(req, res) {
const { email, password } = req.body;
const user = await repo.findByEmail(email);
if (!user) return res.status(401).json({ status: 'error', message: 'Invalid credentials', errorCode: 'BAD_CREDENTIALS', timestamp: new Date().toISOString() });
if (!user) return res.status(401).json({ status: 'error', message: 'User Not Found', errorCode: 'BAD_CREDENTIALS', timestamp: new Date().toISOString() });
const ok = await bcrypt.compare(password, user.passwordHash);
if (!ok) return res.status(401).json({ status: 'error', message: 'Invalid credentials', errorCode: 'BAD_CREDENTIALS', timestamp: new Date().toISOString() });
const accessToken = jwtService.sign({ uuid: user.uuid, role: user.role });
const refreshToken = jwtService.sign({ uuid: user.uuid, type: 'refresh' }, { expiresIn: '7d' });
const accessToken = jwtService.sign({ id: user.id, uuid: user.uuid, role: user.role });
const refreshToken = jwtService.sign({ id: user.id, uuid: user.uuid, type: 'refresh' }, { expiresIn: '7d' });
await session.storeRefreshToken(user.uuid, refreshToken);
const displayName = [user.firstName, user.lastName].filter(Boolean).join(' ');
res.json(
@ -36,7 +36,7 @@ async function refresh(req, res) {
if (payload.type !== 'refresh') throw new Error('Invalid token');
const stored = await session.getRefreshToken(payload.uuid);
if (stored !== refreshToken) return res.status(401).json({ status: 'error', message: 'Invalid refresh token', errorCode: 'INVALID_REFRESH', timestamp: new Date().toISOString() });
const accessToken = jwtService.sign({ uuid: payload.uuid, role: payload.role });
const accessToken = jwtService.sign({ id: payload.id, uuid: payload.uuid, role: payload.role });
res.json(success('Token refreshed', { accessToken }));
} catch (e) {
return res.status(401).json({ status: 'error', message: 'Invalid refresh token', errorCode: 'INVALID_REFRESH', timestamp: new Date().toISOString() });

View File

@ -0,0 +1,48 @@
const { success, failure } = require('../../utils/response');
const IntegrationService = require('../../services/integration/integrationService');
async function getData(req, res) {
try {
const { provider, service, resource, page, limit, filters } = req.query;
console.log('query is', req.query);
const integrationService = new IntegrationService(req.user.id);
const params = { page, limit };
if (filters) {
try {
params.filters = JSON.parse(filters);
} catch (e) {
return res.status(400).json(failure('Invalid filters format', 'INVALID_FILTERS'));
}
}
const data = await integrationService.getData(provider, service, resource, params);
res.json(success(`${provider} ${service} ${resource} data`, data));
} catch (error) {
res.status(400).json(failure(error.message, 'INTEGRATION_ERROR'));
}
}
async function getServices(req, res) {
try {
const { provider } = req.query;
const integrationService = new IntegrationService(req.user.id);
const services = await integrationService.getAvailableServices(provider);
res.json(success(`${provider} available services`, services));
} catch (error) {
res.status(400).json(failure(error.message, 'INTEGRATION_ERROR'));
}
}
async function getResources(req, res) {
try {
const { provider, service } = req.query;
const integrationService = new IntegrationService(req.user.id);
const resources = await integrationService.getAvailableResources(provider, service);
res.json(success(`${provider} ${service} available resources`, resources));
} catch (error) {
res.status(400).json(failure(error.message, 'INTEGRATION_ERROR'));
}
}
module.exports = { getData, getServices, getResources };

View File

@ -59,8 +59,8 @@ async function exchangeZohoToken(req, res) {
await userAuthTokenRepo.upsertToken({
userId: id,
serviceName: service_name,
accessToken: encrypt(access_token),
refreshToken: refresh_token ? encrypt(refresh_token) : null,
accessToken: access_token,
refreshToken: refresh_token? refresh_token : null,
expiresAt
});

View File

@ -0,0 +1,59 @@
const express = require('express');
const Joi = require('joi');
const { getData, getServices, getResources } = require('../controllers/integrationController');
const auth = require('../middlewares/auth');
const ZohoHandler = require('../../integrations/zoho/handler');
const router = express.Router();
function validate(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.query, { abortEarly: false, stripUnknown: true });
if (error) {
return res.status(400).json({
status: 'error',
message: 'Validation failed',
errorCode: 'VALIDATION_ERROR',
details: error.details,
timestamp: new Date().toISOString()
});
}
req.query = value;
next();
};
}
// Get data from any provider/service/resource
const dataSchema = Joi.object({
provider: Joi.string().valid('zoho', 'hubspot', 'quickbooks', 'bamboohr').required(),
service: Joi.string().required(),
resource: Joi.string().required(),
page: Joi.number().min(1).default(1),
limit: Joi.number().min(1).max(100).default(20),
filters: Joi.string().optional()
});
router.get('/data', auth, validate(dataSchema), getData);
// Get available services
const servicesSchema = Joi.object({
provider: Joi.string().valid('zoho', 'hubspot', 'quickbooks', 'bamboohr').required()
});
router.get('/services', auth, validate(servicesSchema), getServices);
// Get available resources
const resourcesSchema = Joi.object({
provider: Joi.string().valid('zoho', 'hubspot', 'quickbooks', 'bamboohr').required(),
service: Joi.string().required()
});
router.get('/resources', auth, validate(resourcesSchema), getResources);
// Webhook endpoints (no auth required - uses signature verification)
const zohoHandler = new ZohoHandler();
router.post('/webhooks/zoho/crm', zohoHandler.handleCrmWebhook.bind(zohoHandler));
router.post('/webhooks/zoho/people', zohoHandler.handlePeopleWebhook.bind(zohoHandler));
router.post('/webhooks/zoho/projects', zohoHandler.handleProjectsWebhook.bind(zohoHandler));
module.exports = router;

View File

@ -10,6 +10,7 @@ const { success } = require('./utils/response');
const config = require('./config');
const userRoutes = require('./api/routes/userRoutes');
const authRoutes = require('./api/routes/authRoutes');
const integrationRoutes = require('./api/routes/integrationRoutes');
const sequelize = require('./db/pool');
const app = express();
@ -39,6 +40,7 @@ app.get('/health', async (req, res) => {
app.use(`${config.app.apiPrefix}/auth`, authRoutes);
app.use(`${config.app.apiPrefix}/users`, userRoutes);
app.use(`${config.app.apiPrefix}/integrations`, integrationRoutes);
module.exports = app;

View File

@ -0,0 +1,139 @@
const axios = require('axios');
const { decrypt, encrypt } = require('../../utils/crypto');
const userAuthTokenRepo = require('../../data/repositories/userAuthTokenRepository');
const ZohoMapper = require('./mapper');
class ZohoClient {
constructor(userId) {
this.userId = userId;
this.baseUrl = 'https://www.zohoapis.com';
}
async getTokens() {
const token = await userAuthTokenRepo.findByUserAndService(this.userId, 'zoho');
if (!token) {
throw new Error('Zoho tokens not found. Please authenticate first.');
}
return {
accessToken: decrypt(token.accessToken),
refreshToken: token.refreshToken ? decrypt(token.refreshToken) : null,
expiresAt: token.expiresAt
};
}
async makeRequest(endpoint, options = {}) {
const { accessToken } = await this.getTokens();
console.log('i am in make request with token',accessToken)
const url = `${this.baseUrl}${endpoint}`;
const config = {
...options,
headers: {
'Authorization': `Zoho-oauthtoken ${accessToken}`,
'Content-Type': 'application/json',
...options.headers
}
};
try {
const response = await axios(url, config);
return response.data;
} catch (error) {
if (error.response?.status === 401) {
await this.refreshToken();
const newTokens = await this.getTokens();
config.headers.Authorization = `Zoho-oauthtoken ${newTokens.accessToken}`;
const retryResponse = await axios(url, config);
return retryResponse.data;
}
throw error;
}
}
async refreshToken() {
const { refreshToken } = await this.getTokens();
if (!refreshToken) {
throw new Error('No refresh token available');
}
const params = new URLSearchParams();
params.append('refresh_token', refreshToken);
params.append('client_id', process.env.ZOHO_CLIENT_ID);
params.append('client_secret', process.env.ZOHO_CLIENT_SECRET);
params.append('grant_type', 'refresh_token');
const response = await axios.post('https://accounts.zoho.com/oauth/v2/token', params.toString(), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
const { access_token, expires_in } = response.data;
const expiresAt = expires_in ? new Date(Date.now() + expires_in * 1000) : null;
await userAuthTokenRepo.upsertToken({
userId: this.userId,
serviceName: 'zoho',
accessToken: encrypt(access_token),
refreshToken: refreshToken ? encrypt(refreshToken) : null,
expiresAt
});
}
// Zoho CRM methods
async getLeads(params = {}) {
const response = await this.makeRequest('/crm/v2/Leads', { params });
return ZohoMapper.mapApiResponse(response, 'leads');
}
async getContacts(params = {}) {
const response = await this.makeRequest('/crm/v2/Contacts', { params });
return ZohoMapper.mapApiResponse(response, 'contacts');
}
async getDeals(params = {}) {
const response = await this.makeRequest('/crm/v2/Deals', { params });
return ZohoMapper.mapApiResponse(response, 'deals');
}
async getTasks(params = {}) {
const response = await this.makeRequest('/crm/v2/Tasks', { params });
return ZohoMapper.mapApiResponse(response, 'tasks');
}
// Zoho People methods
async getEmployees(params = {}) {
const response = await this.makeRequest('/people/api/v1/employees', { params });
return ZohoMapper.mapApiResponse(response, 'employees');
}
// Zoho Projects methods
async getProjects(params = {}) {
const response = await this.makeRequest('/projects/v1/projects', { params });
return ZohoMapper.mapApiResponse(response, 'projects');
}
async getProjectTasks(projectId, params = {}) {
const response = await this.makeRequest(`/projects/v1/projects/${projectId}/tasks`, { params });
return ZohoMapper.mapApiResponse(response, 'tasks');
}
async getAllProjectTasks(params = {}) {
const response = await this.makeRequest('/projects/v1/tasks', { params });
return ZohoMapper.mapApiResponse(response, 'tasks');
}
// Service discovery
getAvailableServices() {
return ['crm', 'people', 'projects'];
}
getAvailableResources(service) {
const resources = {
'crm': ['leads', 'contacts', 'deals', 'tasks'],
'people': ['employees', 'departments'],
'projects': ['projects', 'tasks', 'timesheets']
};
return resources[service] || [];
}
}
module.exports = ZohoClient;

View File

@ -0,0 +1,254 @@
// Webhook/event handler for Zoho integration
const crypto = require('crypto');
const logger = require('../../utils/logger');
class ZohoHandler {
constructor() {
this.webhookSecret = process.env.ZOHO_WEBHOOK_SECRET || 'changeme';
}
// Verify webhook signature from Zoho
verifyWebhookSignature(payload, signature, secret = null) {
const webhookSecret = secret || this.webhookSecret;
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
// Handle Zoho CRM webhook events
async handleCrmWebhook(req, res) {
try {
const signature = req.headers['x-zoho-signature'];
const payload = JSON.stringify(req.body);
if (!this.verifyWebhookSignature(payload, signature)) {
logger.warn('Invalid Zoho CRM webhook signature', {
correlationId: logger.getCorrelationId(req),
ip: req.ip
});
return res.status(401).json({ status: 'error', message: 'Invalid signature' });
}
const { event, data } = req.body;
logger.info('Zoho CRM webhook received', {
correlationId: logger.getCorrelationId(req),
event,
recordId: data?.id
});
// Process different CRM events
switch (event) {
case 'leads.create':
await this.handleLeadCreated(data);
break;
case 'leads.update':
await this.handleLeadUpdated(data);
break;
case 'leads.delete':
await this.handleLeadDeleted(data);
break;
case 'contacts.create':
await this.handleContactCreated(data);
break;
case 'contacts.update':
await this.handleContactUpdated(data);
break;
case 'deals.create':
await this.handleDealCreated(data);
break;
case 'deals.update':
await this.handleDealUpdated(data);
break;
default:
logger.warn('Unknown Zoho CRM event', { event });
}
res.json({ status: 'success', message: 'Webhook processed' });
} catch (error) {
logger.error('Zoho CRM webhook processing failed', {
correlationId: logger.getCorrelationId(req),
error: error.message,
stack: error.stack
});
res.status(500).json({ status: 'error', message: 'Webhook processing failed' });
}
}
// Handle Zoho People webhook events
async handlePeopleWebhook(req, res) {
try {
const signature = req.headers['x-zoho-signature'];
const payload = JSON.stringify(req.body);
if (!this.verifyWebhookSignature(payload, signature)) {
logger.warn('Invalid Zoho People webhook signature', {
correlationId: logger.getCorrelationId(req),
ip: req.ip
});
return res.status(401).json({ status: 'error', message: 'Invalid signature' });
}
const { event, data } = req.body;
logger.info('Zoho People webhook received', {
correlationId: logger.getCorrelationId(req),
event,
employeeId: data?.id
});
// Process different People events
switch (event) {
case 'employees.create':
await this.handleEmployeeCreated(data);
break;
case 'employees.update':
await this.handleEmployeeUpdated(data);
break;
case 'employees.delete':
await this.handleEmployeeDeleted(data);
break;
default:
logger.warn('Unknown Zoho People event', { event });
}
res.json({ status: 'success', message: 'Webhook processed' });
} catch (error) {
logger.error('Zoho People webhook processing failed', {
correlationId: logger.getCorrelationId(req),
error: error.message,
stack: error.stack
});
res.status(500).json({ status: 'error', message: 'Webhook processing failed' });
}
}
// Handle Zoho Projects webhook events
async handleProjectsWebhook(req, res) {
try {
const signature = req.headers['x-zoho-signature'];
const payload = JSON.stringify(req.body);
if (!this.verifyWebhookSignature(payload, signature)) {
logger.warn('Invalid Zoho Projects webhook signature', {
correlationId: logger.getCorrelationId(req),
ip: req.ip
});
return res.status(401).json({ status: 'error', message: 'Invalid signature' });
}
const { event, data } = req.body;
logger.info('Zoho Projects webhook received', {
correlationId: logger.getCorrelationId(req),
event,
projectId: data?.id
});
// Process different Projects events
switch (event) {
case 'projects.create':
await this.handleProjectCreated(data);
break;
case 'projects.update':
await this.handleProjectUpdated(data);
break;
case 'tasks.create':
await this.handleTaskCreated(data);
break;
case 'tasks.update':
await this.handleTaskUpdated(data);
break;
default:
logger.warn('Unknown Zoho Projects event', { event });
}
res.json({ status: 'success', message: 'Webhook processed' });
} catch (error) {
logger.error('Zoho Projects webhook processing failed', {
correlationId: logger.getCorrelationId(req),
error: error.message,
stack: error.stack
});
res.status(500).json({ status: 'error', message: 'Webhook processing failed' });
}
}
// CRM Event Handlers
async handleLeadCreated(data) {
logger.info('Lead created', { leadId: data.id, name: data.Full_Name });
// Add your business logic here - e.g., sync to other systems
}
async handleLeadUpdated(data) {
logger.info('Lead updated', { leadId: data.id, name: data.Full_Name });
// Add your business logic here
}
async handleLeadDeleted(data) {
logger.info('Lead deleted', { leadId: data.id });
// Add your business logic here
}
async handleContactCreated(data) {
logger.info('Contact created', { contactId: data.id, name: `${data.First_Name} ${data.Last_Name}` });
// Add your business logic here
}
async handleContactUpdated(data) {
logger.info('Contact updated', { contactId: data.id, name: `${data.First_Name} ${data.Last_Name}` });
// Add your business logic here
}
async handleDealCreated(data) {
logger.info('Deal created', { dealId: data.id, name: data.Deal_Name, amount: data.Amount });
// Add your business logic here
}
async handleDealUpdated(data) {
logger.info('Deal updated', { dealId: data.id, name: data.Deal_Name, amount: data.Amount });
// Add your business logic here
}
// People Event Handlers
async handleEmployeeCreated(data) {
logger.info('Employee created', { employeeId: data.id, name: `${data.firstName} ${data.lastName}` });
// Add your business logic here
}
async handleEmployeeUpdated(data) {
logger.info('Employee updated', { employeeId: data.id, name: `${data.firstName} ${data.lastName}` });
// Add your business logic here
}
async handleEmployeeDeleted(data) {
logger.info('Employee deleted', { employeeId: data.id });
// Add your business logic here
}
// Projects Event Handlers
async handleProjectCreated(data) {
logger.info('Project created', { projectId: data.id, name: data.name });
// Add your business logic here
}
async handleProjectUpdated(data) {
logger.info('Project updated', { projectId: data.id, name: data.name });
// Add your business logic here
}
async handleTaskCreated(data) {
logger.info('Task created', { taskId: data.id, name: data.name, projectId: data.project?.id });
// Add your business logic here
}
async handleTaskUpdated(data) {
logger.info('Task updated', { taskId: data.id, name: data.name, projectId: data.project?.id });
// Add your business logic here
}
}
module.exports = ZohoHandler;

View File

@ -0,0 +1,152 @@
// Data mapping/transformation utilities for Zoho integration
class ZohoMapper {
// Map Zoho CRM Lead to standardized format
static mapLead(zohoLead) {
return {
id: zohoLead.id,
name: zohoLead.Full_Name || zohoLead.Lead_Name,
email: zohoLead.Email,
phone: zohoLead.Phone,
company: zohoLead.Company,
status: zohoLead.Lead_Status,
source: zohoLead.Lead_Source,
createdTime: zohoLead.Created_Time,
modifiedTime: zohoLead.Modified_Time,
owner: zohoLead.Owner?.name,
// Map custom fields if they exist
customFields: this.mapCustomFields(zohoLead)
};
}
// Map Zoho CRM Contact to standardized format
static mapContact(zohoContact) {
return {
id: zohoContact.id,
firstName: zohoContact.First_Name,
lastName: zohoContact.Last_Name,
email: zohoContact.Email,
phone: zohoContact.Phone,
account: zohoContact.Account_Name,
title: zohoContact.Title,
department: zohoContact.Department,
createdTime: zohoContact.Created_Time,
modifiedTime: zohoContact.Modified_Time,
owner: zohoContact.Owner?.name,
customFields: this.mapCustomFields(zohoContact)
};
}
// Map Zoho CRM Deal to standardized format
static mapDeal(zohoDeal) {
return {
id: zohoDeal.id,
name: zohoDeal.Deal_Name,
amount: zohoDeal.Amount,
stage: zohoDeal.Stage,
probability: zohoDeal.Probability,
closeDate: zohoDeal.Closing_Date,
account: zohoDeal.Account_Name,
contact: zohoDeal.Contact_Name,
createdTime: zohoDeal.Created_Time,
modifiedTime: zohoDeal.Modified_Time,
owner: zohoDeal.Owner?.name,
customFields: this.mapCustomFields(zohoDeal)
};
}
// Map Zoho People Employee to standardized format
static mapEmployee(zohoEmployee) {
return {
id: zohoEmployee.id,
employeeId: zohoEmployee.employeeId,
firstName: zohoEmployee.firstName,
lastName: zohoEmployee.lastName,
email: zohoEmployee.email,
phone: zohoEmployee.phone,
department: zohoEmployee.department,
designation: zohoEmployee.designation,
reportingTo: zohoEmployee.reportingTo,
joiningDate: zohoEmployee.joiningDate,
status: zohoEmployee.status,
customFields: this.mapCustomFields(zohoEmployee)
};
}
// Map Zoho Projects Project to standardized format
static mapProject(zohoProject) {
return {
id: zohoProject.id,
name: zohoProject.name,
description: zohoProject.description,
status: zohoProject.status,
startDate: zohoProject.start_date,
endDate: zohoProject.end_date,
owner: zohoProject.owner?.name,
createdTime: zohoProject.created_time,
customFields: this.mapCustomFields(zohoProject)
};
}
// Map Zoho Projects Task to standardized format
static mapTask(zohoTask) {
return {
id: zohoTask.id,
name: zohoTask.name,
description: zohoTask.description,
status: zohoTask.status,
priority: zohoTask.priority,
startDate: zohoTask.start_date,
endDate: zohoTask.end_date,
assignee: zohoTask.assignee?.name,
project: zohoTask.project?.name,
customFields: this.mapCustomFields(zohoTask)
};
}
// Map custom fields from Zoho response
static mapCustomFields(zohoRecord) {
const customFields = {};
if (zohoRecord.Custom_Fields) {
zohoRecord.Custom_Fields.forEach(field => {
customFields[field.api_name] = field.value;
});
}
return customFields;
}
// Map multiple records using appropriate mapper
static mapRecords(records, recordType) {
const mapperMap = {
'leads': this.mapLead,
'contacts': this.mapContact,
'deals': this.mapDeal,
'employees': this.mapEmployee,
'projects': this.mapProject,
'tasks': this.mapTask
};
const mapper = mapperMap[recordType];
if (!mapper) {
throw new Error(`No mapper found for record type: ${recordType}`);
}
return records.map(record => mapper(record));
}
// Map Zoho API response to standardized format
static mapApiResponse(zohoResponse, recordType) {
const records = zohoResponse.data || [];
// const mappedRecords = this.mapRecords(records, recordType);
return {
data: records,
info: {
count: zohoResponse.info?.count || mappedRecords.length,
moreRecords: zohoResponse.info?.more_records || false,
page: zohoResponse.info?.page || 1
}
};
}
}
module.exports = ZohoMapper;

View File

@ -1,21 +0,0 @@
const { failure } = require('../utils/response');
const logger = require('../utils/logger');
module.exports = function errorHandler(err, req, res, next) {
const correlationId = logger.getCorrelationId(req);
const status = err.status || 500;
const errorCode = err.code || 'INTERNAL_SERVER_ERROR';
const message = status === 500 ? 'Something went wrong' : err.message || 'Error';
logger.error('Request failed', {
correlationId,
path: req.originalUrl,
method: req.method,
status,
errorCode,
stack: status === 500 ? err.stack : undefined
});
res.status(status).json(failure(message, errorCode));
};

View File

@ -0,0 +1,73 @@
const ZohoClient = require('../../integrations/zoho/client');
class IntegrationService {
constructor(userId) {
this.userId = userId;
this.clients = {
zoho: new ZohoClient(userId)
};
}
async getData(provider, service, resource, params = {}) {
const client = this.clients[provider];
if (!client) {
throw new Error(`Provider ${provider} not supported`);
}
const method = this.getMethodName(service, resource);
if (!client[method]) {
throw new Error(`Resource ${resource} not supported for ${provider} ${service}`);
}
// Handle special cases that require additional parameters
if (method === 'getTasks' && service === 'projects' && params.projectId) {
return await client.getProjectTasks(params.projectId, params);
}
// Handle getAllProjectTasks for projects tasks without projectId
if (method === 'getTasks' && service === 'projects' && !params.projectId) {
return await client.getAllProjectTasks(params);
}
// Handle CRM tasks (no special parameters needed)
if (method === 'getTasks' && service === 'crm') {
return await client[method](params);
}
return await client[method](params);
}
getMethodName(service, resource) {
const resourceMap = {
'leads': 'Leads',
'contacts': 'Contacts',
'deals': 'Deals',
'employees': 'Employees',
'projects': 'Projects',
'tasks': 'Tasks',
'timesheets': 'Timesheets'
};
const resourceName = resourceMap[resource] || resource;
return `get${resourceName}`;
}
async getAvailableServices(provider) {
const client = this.clients[provider];
if (!client) {
throw new Error(`Provider ${provider} not supported`);
}
return client.getAvailableServices();
}
async getAvailableResources(provider, service) {
const client = this.clients[provider];
if (!client) {
throw new Error(`Provider ${provider} not supported`);
}
return client.getAvailableResources(service);
}
}
module.exports = IntegrationService;