import { Injectable } from '@angular/core'; import { PortalDeterminationService } from '@core/services/portal-determination.service'; import { APIExternalAPI } from '@core/typings/api/external-api.typing'; import { ExternalAPISelection } from '@features/formio/component-configuration/external-api-selector-settings/external-api-selector-settings.component'; import { ArrayHelpersService, AutoTableRepositoryFactory, PaginationOptions } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { NotifierService } from '@yourcause/common/notifier'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import { get, uniq } from 'lodash'; import { BehaviorSubject, filter, map, Observable } from 'rxjs'; import { ExternalAPIResources } from './external-api.resources'; import { ExternalAPIState } from './external-api.state'; import { CreateOutboundApiRecord, OutboundApiRecordFromApi, OutboundApiService, ReadWriteTracker, ServiceDetails, ServiceType, UpdateOutboundApiRecord } from './outbound-api.typing'; @AttachYCState(ExternalAPIState) @Injectable({ providedIn: 'root' }) export class ExternalAPIService extends BaseYCService { inboundApiTableKey = 'INBOUND_APIS'; outboundApiTableKey = 'OUTBOUND_APIS'; currentlyAllowedServices = [ ServiceType.Payments, ServiceType.Batches, ServiceType.GrantManagerUsers, ServiceType.Roles, ServiceType.Workflows, ServiceType.Invitations, ServiceType.Applications, ServiceType.Forms, ServiceType.Picklists ]; constructor ( private logger: LogService, private portal: PortalDeterminationService, private externalApiResources: ExternalAPIResources, private notifier: NotifierService, private i18n: I18nService, private autoTableFactory: AutoTableRepositoryFactory, private arrayHelper: ArrayHelpersService ) { super(); } resetInboundApiTable () { const repo = this.autoTableFactory.getRepository(this.inboundApiTableKey); if (repo) { repo.reset(); } } resetOutboundApiTable () { const repo = this.autoTableFactory.getRepository(this.outboundApiTableKey); if (repo) { repo.reset(); } } private getRequests () { let reqs = this.get('requests'); if (!reqs) { reqs = {}; this.set('requests', reqs); } return reqs; } private getOrCreateSubject ( guid: string, subKey: string ) { let requests = this.getRequests(); const key = `${guid}-${subKey}`; if (!requests[key]) { requests = { ...requests, [key]: new BehaviorSubject(null) }; this.set('requests', requests); } return requests[key]; } private async kickOffRequest ( guid: string, formKey: string, body: { formField: string; applicationId: number; applicationFormId: number; formId: number; formRevisionId: number; } ) { const subject = this.getOrCreateSubject(guid, formKey); let result: APIExternalAPI.ExternalAPIResponse; try { if (this.portal.isManager) { result = await this.externalApiResources.executeExternalAPIIntegrationForManager( guid, body, '/ExecuteForApplication' ); } else if (this.portal.isApply) { if (body.applicationId && body.applicationFormId) { result = await this.externalApiResources.executeExternalAPIIntegrationForApplication( guid, body ); } else { result = await this.externalApiResources.executeExternalAPIIntegrationForApplicant( guid, body ); } if (result.responseCode !== 200) { this.notifier.error( this.i18n.translate('common:notificationErrorGettingResponse', {}, 'There was an error executing the external API request') ); console.warn('ERROR EXECUTING INTEGRATION', guid, formKey, result, body); } } subject.next(result); } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate('common:notificationErrorGettingResponse', {}, 'There was an error executing the external API request') ); } } setUpIntegrationRequest ( guid: string, formKey: string, body: { formField: string; applicationId: number; applicationFormId: number; formId: number; formRevisionId: number; } ) { this.kickOffRequest(guid, formKey, body); } // when the external api component is loaded // it'll use this function to get and kick off the request // and map the response getIntegrationRequest ( guid: string, formKey: string, responseField: string ): Observable { const sub = this.getOrCreateSubject(guid, formKey); return sub .pipe(filter(res => res !== undefined && res !== null)) .pipe(map(res => get(res.response, responseField, ''))); } // this will be fired on the ngOnDestroy for the formio.component destroyIntegrationRequest ( guid: string, formKey: string ) { const req = this.getOrCreateSubject(guid, formKey); if (req) { return req.complete(); } } getIntegrationsPaginated ( options: PaginationOptions ) { return this.externalApiResources.getExternalAPIIntegrationsPaginated(options); } async deleteIntegration ( integration: APIExternalAPI.ExternalAPIConfiguration ) { try { await this.externalApiResources.deleteExternalAPIIntegration(integration); this.resetInboundApiTable(); this.notifier.success(this.i18n.translate( 'CONFIG:notificationSuccesfulWebServiceDeletion', {}, 'Successfully deleted web service information.' )); } catch (error) { this.logger.error(error); this.notifier.error(this.i18n.translate( 'CONFIG:notificationErrorWebServiceDeletion', {}, 'There was an error deleting the web service information.' )); } } async createIntegration ( integration: APIExternalAPI.ExternalAPIPayload ) { try { await this.externalApiResources.createExternalAPIIntegration(integration); this.resetInboundApiTable(); this.notifier.success(this.i18n.translate( 'CONFIG:notificationSuccesfulWebServiceSubmission', {}, 'Successfully submitted web service information.' )); } catch (error) { this.logger.error(error); this.notifier.error(this.i18n.translate( 'CONFIG:notificationSuccesfulWebServiceSubmission', {}, 'There was an error sending the web service information.' )); } } async updateIntegration ( integration: APIExternalAPI.ExternalAPIPayload ) { try { await this.externalApiResources.updateExternalAPIIntegration(integration); this.resetInboundApiTable(); this.notifier.success(this.i18n.translate( 'CONFIG:notificationSuccesfulWebServiceSubmission', {}, 'Successfully submitted web service information.' )); } catch (error) { this.logger.error(error); this.notifier.error(this.i18n.translate( 'CONFIG:notificationSuccesfulWebServiceSubmission', {}, 'There was an error sending the web service information.' )); } } async fetchAllIntegrations () { const integrations = await this.externalApiResources.getAllExternalApiIntegrations(); this.set('integrations', integrations); } extractIds ( externalApiRequests: (ExternalAPISelection&{ relatedComponent: string })[] ): number[] { return uniq(externalApiRequests .map(req => req.integrationId)) .map(guid => { const found = this.get('integrations') .find(integration => integration.externalApiRequestGuid === guid); if (found) { return found.id; } return 0; }) .filter(id => !!id); } getOutboundApiRecords ( paginationOptions: PaginationOptions ) { return this.externalApiResources.getOutboundApiRecords( paginationOptions ); } getCurrentServices (): ServiceDetails[] { return this.arrayHelper.sort( this.currentlyAllowedServices.map((service) => { const details = this.getServiceDetails(service); return { type: service, label: details.label, supportsWrite: details.supportsWrite }; }), 'label' ); } getServiceDetails (service: ServiceType) { switch (service) { case ServiceType.Applicants: return { label: this.i18n.translate( 'common:lblApplicants', {}, 'Applicants' ), supportsWrite: false }; case ServiceType.Applications: return { label: this.i18n.translate( 'common:hdrApplications', {}, 'Applications' ), supportsWrite: true }; case ServiceType.Audiences: return { label: this.i18n.translate( 'CONFIG:hdrAudiences', {}, 'Audiences' ), supportsWrite: false }; case ServiceType.Awards: return { label: this.i18n.translate( 'common:hdrAwards', {}, 'Awards' ), supportsWrite: false }; case ServiceType.Batches: return { label: this.i18n.translate( 'MANAGE:textBatches', {}, 'Batches' ), supportsWrite: false }; case ServiceType.Budgets: return { label: this.i18n.translate( 'GLOBAL:textBudgets', {}, 'Budgets' ), supportsWrite: false }; case ServiceType.Cycles: return { label: this.i18n.translate( 'PROGRAM:textCycles', {}, 'Cycles' ), supportsWrite: false }; case ServiceType.FundingSources: return { label: this.i18n.translate( 'GLOBAL:textFundingSources', {}, 'Funding Sources' ), supportsWrite: false }; case ServiceType.GrantManagerUsers: return { label: this.i18n.translate( 'common:hdrGrantManagerUsers', {}, 'Grant Manager Users' ), supportsWrite: true }; case ServiceType.InKind: return { label: this.i18n.translate( 'GLOBAL:hdrInKind', {}, 'In Kind' ), supportsWrite: false }; case ServiceType.Invitations: return { label: this.i18n.translate( 'PROGRAM:textInvitations', {}, 'Invitations' ), supportsWrite: true }; case ServiceType.Organizations: return { label: this.i18n.translate( 'common:lblOrganizations', {}, 'Organizations' ), supportsWrite: false }; case ServiceType.Payments: return { label: this.i18n.translate( 'common:hdrPayments', {}, 'Payments' ), supportsWrite: true }; case ServiceType.Programs: return { label: this.i18n.translate( 'GLOBAL:textPrograms', {}, 'Programs' ), supportsWrite: false }; case ServiceType.Roles: return { label: this.i18n.translate( 'GLOBAL:textRoles', {}, 'Roles' ), supportsWrite: true }; case ServiceType.Tags: return { label: this.i18n.translate( 'GLOBAL:lblTags', {}, 'Tags' ), supportsWrite: false }; case ServiceType.Translations: return { label: this.i18n.translate( 'GLOBAL:textTranslations', {}, 'Translations' ), supportsWrite: false }; case ServiceType.WorkflowLevelAutomation: return { label: this.i18n.translate( 'common:hdrWorkflowLevelAutomation', {}, 'Workflow Level Automation' ), supportsWrite: false }; case ServiceType.Workflows: return { label: this.i18n.translate( 'GLOBAL:textWorkflows', {}, 'Workflows' ), supportsWrite: true }; case ServiceType.Picklists: return { label: this.i18n.translate( 'FORMS:textCustomDataTables', {}, 'Custom data tables' ), supportsWrite: true }; case ServiceType.Forms: return { label: this.i18n.translate( 'common:textForms', {}, 'Forms' ), supportsWrite: true }; } } async handleCreateOutboundApiRecord (payload: CreateOutboundApiRecord) { try { const response = await this.externalApiResources.createOutboundApiRecord(payload); this.resetOutboundApiTable(); this.notifier.success(this.i18n.translate( 'CONFIG:textSuccessAddingOutboundApi', {}, 'Successfully added the outbound API record' )); return response.token; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'CONFIG:textErrorAddingOutboundApi', {}, 'There was an error adding the outbound API record' )); return ''; } } async handleUpdateOutboundApiRecord ( clientApiTokenId: number, payload: UpdateOutboundApiRecord ) { try { await this.externalApiResources.updateOutboundApiRecord( clientApiTokenId, payload ); this.resetOutboundApiTable(); this.notifier.success(this.i18n.translate( 'CONFIG:textSuccessUpdatedOutboundApi', {}, 'Successfully updated the outbound API record' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'CONFIG:textErrorUpdatingOutboundApi', {}, 'There was an error updating the outbound API record' )); } } adaptServicesForSave ( servicesMap: Partial> ): OutboundApiService[] { return Object.keys(servicesMap) .map(key => +key as ServiceType) .map((key) => { return { clientAPIServiceTypeId: key as ServiceType, canRead: servicesMap[key].read, canWrite: servicesMap[key].write }; }).filter((service) => { // Should have at least one permission to save service return service.canRead || service.canWrite; }); } generateApiKey (clientApiTokenId: number) { return this.externalApiResources.generateApiKey(clientApiTokenId); } async handleToggleEnabled ( clientApiTokenId: number, isEnabled: boolean ) { try { await this.externalApiResources.toggleEnablingOutboundApiKey( clientApiTokenId, isEnabled ); this.resetOutboundApiTable(); this.notifier.success(this.i18n.translate( isEnabled ? 'common:textSuccessActivatingKey' : 'common:textSuccessDeactivatingKey', {}, isEnabled ? 'Successfully activated the key' : 'Successfully deactivated the key' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( isEnabled ? 'common:textErrorActivatingKey' : 'common:textErrorDeactivatingKey', {}, isEnabled ? 'There was an error activating the key' : 'There was an error deactivating the key' )); } } getOutboundApiClientMetrics () { return this.externalApiResources.getOutboundApiClientMetrics(); } }