UNPKG

@mediarithmics/plugins-nodejs-sdk

Version:

This is the mediarithmics nodejs to help plugin developers bootstrapping their plugin without having to deal with most of the plugin boilerplate

616 lines (537 loc) 22.3 kB
import { isWebDomainRealmFilter } from './../../api/core/webdomain/UserAgentIdentifierRealmSelectionInterface'; /* eslint-disable @typescript-eslint/no-misused-promises */ /* eslint-disable @typescript-eslint/unbound-method */ import express from 'express'; import _ from 'lodash'; import { PluginProperty, PluginPropertyResponse } from '../../'; import { AudienceSegmentExternalFeedResource, AudienceSegmentexternalResourceResponse, AudienceSegmentResource, AudienceSegmentResourceResponse, } from '../../api/core/audiencesegment/AudienceSegmentInterface'; import { BatchUpdateHandler } from '../../api/core/batchupdate/BatchUpdateHandler'; import { BatchUpdatePluginResponse, BatchUpdateRequest } from '../../api/core/batchupdate/BatchUpdateInterface'; import { RealmFilter, UserAgentIdentifierRealmSelectionResource, UserAgentIdentifierRealmSelectionResourcesResponse, } from '../../api/core/webdomain/UserAgentIdentifierRealmSelectionInterface'; import { BatchedUserSegmentUpdatePluginResponse, ExternalSegmentAuthenticationResponse, ExternalSegmentAuthenticationStatusQueryResponse, ExternalSegmentConnectionPluginResponse, ExternalSegmentCreationPluginResponse, ExternalSegmentDynamicPropertyValuesQueryResponse, ExternalSegmentLogoutResponse, ExternalSegmentTroubleshootResponse, MissingRealmError, UserSegmentUpdatePluginResponse, } from '../../api/plugin/audiencefeedconnector/AudienceFeedConnectorPluginResponseInterface'; import { AudienceFeedBatchContext, ExternalSegmentAuthenticationRequest, ExternalSegmentAuthenticationStatusQueryRequest, ExternalSegmentConnectionRequest, ExternalSegmentCreationRequest, ExternalSegmentDynamicPropertyValuesQueryRequest, ExternalSegmentLogoutRequest, ExternalSegmentTroubleshootActions, ExternalSegmentTroubleshootRequest, UserSegmentUpdateRequest, } from '../../api/plugin/audiencefeedconnector/AudienceFeedConnectorRequestInterface'; import { BasePlugin, PropertiesWrapper } from '../common'; export interface AudienceFeedConnectorBaseInstanceContext { feed: AudienceSegmentExternalFeedResource; feedProperties: PropertiesWrapper; } abstract class GenericAudienceFeedConnectorBasePlugin< T, R extends BatchedUserSegmentUpdatePluginResponse<T> | UserSegmentUpdatePluginResponse, > extends BasePlugin<AudienceFeedConnectorBaseInstanceContext> { constructor(enableThrottling = false) { super(enableThrottling); this.initExternalSegmentCreation(); this.initExternalSegmentConnection(); this.initUserSegmentUpdate(); this.initTroubleshoot(); this.initAuthenticationStatusQuery(); this.initAuthentication(); this.initLogoutQuery(); this.initDynamicPropertyValuesQuery(); } async fetchAudienceSegment(feedId: string): Promise<AudienceSegmentResource> { const response = await super.requestGatewayHelper<AudienceSegmentResourceResponse>( 'GET', `${this.outboundPlatformUrl}/v1/audience_segment_external_feeds/${feedId}/audience_segment`, ); this.logger.debug(`Fetched External Segment: FeedId: ${feedId}`, response); return response.data; } async fetchUserAgentIdentifierRealms(datamartId: string): Promise<Array<UserAgentIdentifierRealmSelectionResource>> { const response = await super.requestGatewayHelper<UserAgentIdentifierRealmSelectionResourcesResponse>( 'GET', `${this.outboundPlatformUrl}/v1/datamarts/${datamartId}/user_agent_identifier_realm_selections`, ); this.logger.debug(`Fetched user agent identifier realms for the datamart with id: ${datamartId}`); return response.data; } async checkUserAgentIdentifierRealm(datamartId: string, realmFilter: RealmFilter): Promise<void> { const realms = await this.fetchUserAgentIdentifierRealms(datamartId); const hasRealm = realms.some((realm) => { if (isWebDomainRealmFilter(realmFilter)) { return realm.realm_type === realmFilter.realmType && realm.web_domain.sld_name === realmFilter.sld_name; } else return realm.realm_type === realmFilter.realmType; }); if (!hasRealm) { throw new MissingRealmError(datamartId, realmFilter); } } async fetchAudienceFeed(feedId: string): Promise<AudienceSegmentExternalFeedResource> { const response = await super.requestGatewayHelper<AudienceSegmentexternalResourceResponse>( 'GET', `${this.outboundPlatformUrl}/v1/audience_segment_external_feeds/${feedId}`, ); this.logger.debug(`Fetched External Feed: ${feedId}`, { response }); return response.data; } // Method to build an instance context // To be overriden to get a cutom behavior async fetchAudienceFeedProperties(feedId: string): Promise<PluginProperty[]> { const response = await super.requestGatewayHelper<PluginPropertyResponse>( 'GET', `${this.outboundPlatformUrl}/v1/audience_segment_external_feeds/${feedId}/properties`, ); this.logger.debug(`Fetched External Feed Properties: ${feedId}`, { response }); return response.data; } async createAudienceFeedProperties(feedId: string, property: PluginProperty): Promise<PluginProperty[]> { const response = await super.requestGatewayHelper<PluginPropertyResponse>( 'POST', `${this.outboundPlatformUrl}/v1/audience_segment_external_feeds/${feedId}/properties`, property, ); this.logger.debug(`Created External Feed Properties: ${feedId}`, { response }); return response.data; } async updateAudienceFeedProperties(feedId: string, property: PluginProperty): Promise<PluginProperty[]> { const response = await super.requestGatewayHelper<PluginPropertyResponse>( 'PUT', `${this.outboundPlatformUrl}/v1/audience_segment_external_feeds/${feedId}/properties/technical_name=${property.technical_name}`, property, ); this.logger.debug(`Updated External Feed Properties: ${feedId}`, { response }); return response.data; } // This is a default provided implementation protected async instanceContextBuilder(feedId: string): Promise<AudienceFeedConnectorBaseInstanceContext> { const audienceFeedP = this.fetchAudienceFeed(feedId); const audienceFeedPropsP = this.fetchAudienceFeedProperties(feedId); const results = await Promise.all([audienceFeedP, audienceFeedPropsP]); const audienceFeed = results[0]; const audienceFeedProps = results[1]; const context: AudienceFeedConnectorBaseInstanceContext = { feed: audienceFeed, feedProperties: new PropertiesWrapper(audienceFeedProps), }; return context; } protected abstract onExternalSegmentCreation( request: ExternalSegmentCreationRequest, instanceContext: AudienceFeedConnectorBaseInstanceContext, ): Promise<ExternalSegmentCreationPluginResponse>; protected abstract onExternalSegmentConnection( request: ExternalSegmentConnectionRequest, instanceContext: AudienceFeedConnectorBaseInstanceContext, ): Promise<ExternalSegmentConnectionPluginResponse>; protected abstract onUserSegmentUpdate( request: UserSegmentUpdateRequest, instanceContext: AudienceFeedConnectorBaseInstanceContext, ): Promise<R>; protected onTroubleshoot( request: ExternalSegmentTroubleshootRequest, instanceContext: AudienceFeedConnectorBaseInstanceContext, ): Promise<ExternalSegmentTroubleshootResponse> { return Promise.resolve({ status: 'not_implemented' }); } protected onAuthenticationStatusQuery( request: ExternalSegmentAuthenticationStatusQueryRequest, ): Promise<ExternalSegmentAuthenticationStatusQueryResponse> { return Promise.resolve({ status: 'not_implemented' }); } protected onAuthentication( request: ExternalSegmentAuthenticationRequest, ): Promise<ExternalSegmentAuthenticationResponse> { return Promise.resolve({ status: 'not_implemented' }); } protected onLogout(request: ExternalSegmentLogoutRequest): Promise<ExternalSegmentLogoutResponse> { return Promise.resolve({ status: 'not_implemented' }); } protected onDynamicPropertyValuesQuery( request: ExternalSegmentDynamicPropertyValuesQueryRequest, ): Promise<ExternalSegmentDynamicPropertyValuesQueryResponse> { return Promise.resolve({ status: 'not_implemented' }); } protected async getInstanceContext( feedId: string, forceRefresh?: boolean, ): Promise<AudienceFeedConnectorBaseInstanceContext> { if (forceRefresh || !this.pluginCache.get(feedId)) { void this.pluginCache.put( feedId, this.instanceContextBuilder(feedId).catch((error) => { this.logger.error(`Error while caching instance context`, error); this.pluginCache.del(feedId); throw error; }), this.getInstanceContextCacheExpiration(), ); } return this.pluginCache.get(feedId) as Promise<AudienceFeedConnectorBaseInstanceContext>; } protected emptyBodyFilter(req: express.Request, res: express.Response, next: express.NextFunction) { if (!req.body || _.isEmpty(req.body)) { const msg = { error: 'Missing request body', }; this.logger.error(`POST /v1/${req.url} : %s`, JSON.stringify(msg)); res.status(500).json(msg); } else { next(); } } private initExternalSegmentCreation(): void { this.app.post( '/v1/external_segment_creation', this.emptyBodyFilter, async (req: express.Request, res: express.Response) => { try { this.logger.debug('POST /v1/external_segment_creation', { request: req.body }); if (!this.httpIsReady()) { throw new Error('Plugin not initialized'); } const request = req.body as ExternalSegmentCreationRequest; if (!this.onExternalSegmentCreation) { throw new Error('No External Segment Creation listener registered!'); } const instanceContext = await this.getInstanceContext(request.feed_id, true); const response = await this.onExternalSegmentCreation(request, instanceContext); const pluginResponse: ExternalSegmentCreationPluginResponse = { status: response.status, visibility: response.visibility || 'PUBLIC', }; if (response.message) { pluginResponse.message = response.message; } const statusCode = response.status === 'ok' ? 200 : 500; this.logger.debug(`FeedId: ${request.feed_id} - External segment creation returning: ${statusCode}`, { response, }); return res.status(statusCode).send(JSON.stringify(pluginResponse)); } catch (error) { this.logger.error('Something bad happened on creation', error); const pluginResponse: ExternalSegmentCreationPluginResponse = { status: 'error', message: `${(error as Error).message}`, visibility: error.visibility === 'PUBLIC' ? 'PUBLIC' : 'PRIVATE', }; return res.status(500).send(pluginResponse); } }, ); } private initExternalSegmentConnection(): void { this.app.post( '/v1/external_segment_connection', this.emptyBodyFilter, async (req: express.Request, res: express.Response) => { try { this.logger.debug('POST /v1/external_segment_connection', { request: req.body }); if (!this.httpIsReady()) { throw new Error('Plugin not initialized'); } const request = req.body as ExternalSegmentConnectionRequest; if (!this.onExternalSegmentConnection) { throw new Error('No External Segment Connection listener registered!'); } const instanceContext = await this.getInstanceContext(request.feed_id); const response = await this.onExternalSegmentConnection(request, instanceContext); const pluginResponse: ExternalSegmentConnectionPluginResponse = { status: response.status, }; if (response.message) { pluginResponse.message = response.message; } let statusCode; switch (response.status) { case 'external_segment_not_ready_yet': statusCode = 502; break; case 'ok': statusCode = 200; break; case 'error': statusCode = 500; break; default: statusCode = 500; } this.logger.debug(`FeedId: ${request.feed_id} - External segment connection returning: ${statusCode}`, { response, }); return res.status(statusCode).send(JSON.stringify(pluginResponse)); } catch (error) { this.logger.error('Something bad happened on connection', error); return res.status(500).send({ status: 'error', message: `${(error as Error).message}` }); } }, ); } private initUserSegmentUpdate(): void { this.app.post( '/v1/user_segment_update', this.emptyBodyFilter, async (req: express.Request, res: express.Response) => { try { this.logger.debug('POST /v1/user_segment_update', { request: req.body }); const request = req.body as UserSegmentUpdateRequest; if (!this.onUserSegmentUpdate) { throw new Error('No User Segment Update listener registered!'); } const instanceContext = await this.getInstanceContext(request.feed_id); const response: R = await this.onUserSegmentUpdate(request, instanceContext); if (response.next_msg_delay_in_ms) { res.set('x-mics-next-msg-delay', response.next_msg_delay_in_ms.toString()); } let statusCode: number; switch (response.status) { case 'ok': statusCode = 200; break; case 'error': statusCode = 500; break; case 'retry': statusCode = 429; break; case 'no_eligible_identifier': statusCode = 400; break; default: statusCode = 500; } this.logger.debug(`FeedId: ${request.feed_id} - External segment update returning: ${statusCode}`, { response, }); return res.status(statusCode).send(JSON.stringify(response)); } catch (error) { this.logger.error('Something bad happened on update', error); return res.status(500).send({ status: 'error', message: `${(error as Error).message}` }); } }, ); } private initTroubleshoot(): void { this.app.post('/v1/troubleshoot', this.emptyBodyFilter, async (req: express.Request, res: express.Response) => { try { this.logger.debug('POST /v1/troubleshoot', { request: req.body }); const request = req.body as ExternalSegmentTroubleshootRequest; if (!ExternalSegmentTroubleshootActions.includes(request.action)) { const response: ExternalSegmentTroubleshootResponse = { status: 'not_implemented', message: `Action ${request.action} not supported`, }; return res.status(400).send(JSON.stringify(response)); } const instanceContext = await this.getInstanceContext(request.feed_id); const response = await this.onTroubleshoot(request, instanceContext); let statusCode: number; switch (response.status) { case 'ok': statusCode = 200; break; case 'error': statusCode = 500; break; case 'not_implemented': statusCode = 400; break; default: statusCode = 500; } this.logger.debug(`FeedId: ${request.feed_id} - Troubleshoot returning: ${statusCode}`, { response }); return res.status(statusCode).send(JSON.stringify(response)); } catch (error) { this.logger.error('Something bad happened on troubleshoot', error); return res.status(500).send({ status: 'error', message: `${(error as Error).message}` }); } }); } private initAuthenticationStatusQuery(): void { this.app.post( '/v1/authentication_status_queries', this.emptyBodyFilter, async (req: express.Request, res: express.Response) => { try { const request = req.body as ExternalSegmentAuthenticationStatusQueryRequest; const response = await this.onAuthenticationStatusQuery(request); let statusCode: number; switch (response.status) { case 'authenticated': case 'not_authenticated': statusCode = 200; break; case 'error': statusCode = 500; break; case 'not_implemented': statusCode = 400; break; default: statusCode = 500; } this.logger.debug( `Request: ${JSON.stringify(request)} - Authentication status query returning: ${statusCode}`, { response, }, ); return res.status(statusCode).send(JSON.stringify(response)); } catch (error) { this.logger.error('Something bad happened on authentication status query', error); return res.status(500).send({ status: 'error', message: `${(error as Error).message}` }); } }, ); } private initAuthentication(): void { this.app.post('/v1/authentication', this.emptyBodyFilter, async (req: express.Request, res: express.Response) => { try { const request = req.body as ExternalSegmentAuthenticationRequest; const response = await this.onAuthentication(request); let statusCode: number; switch (response.status) { case 'ok': statusCode = 200; break; case 'error': statusCode = 500; break; case 'not_implemented': statusCode = 400; break; default: statusCode = 500; } this.logger.debug(`Request: ${JSON.stringify(req.body)} - Authentication returning: ${statusCode}`, { response, }); return res.status(statusCode).send(JSON.stringify(response)); } catch (error) { this.logger.error('Something bad happened on authentication', error); return res.status(500).send({ status: 'error', message: `${(error as Error).message}` }); } }); } private initLogoutQuery(): void { this.app.post('/v1/logout', this.emptyBodyFilter, async (req: express.Request, res: express.Response) => { try { const request = req.body as ExternalSegmentLogoutRequest; const response = await this.onLogout(request); let statusCode: number; switch (response.status) { case 'ok': statusCode = 200; break; case 'error': statusCode = 500; break; case 'not_implemented': statusCode = 400; break; default: statusCode = 500; } this.logger.debug(`Request: ${JSON.stringify(request)} - Logout query returning: ${statusCode}`, { response, }); return res.status(statusCode).send(JSON.stringify(response)); } catch (error) { this.logger.error('Something bad happened on logout query', error); return res.status(500).send({ status: 'error', message: `${(error as Error).message}` }); } }); } private initDynamicPropertyValuesQuery(): void { this.app.post( '/v1/dynamic_property_values_queries', this.emptyBodyFilter, async (req: express.Request, res: express.Response) => { try { const request = req.body as ExternalSegmentDynamicPropertyValuesQueryRequest; const response = await this.onDynamicPropertyValuesQuery(request); let statusCode: number; switch (response.status) { case 'ok': statusCode = 200; break; case 'error': statusCode = 500; break; case 'not_implemented': statusCode = 400; break; default: statusCode = 500; } this.logger.debug( `Request: ${JSON.stringify(request)} - Dynamic property values query returning: ${statusCode}`, { response, }, ); return res.status(statusCode).send(JSON.stringify(response)); } catch (error) { this.logger.error('Something bad happened on dynamic property values query', error); return res.status(500).send({ status: 'error', message: `${(error as Error).message}` }); } }, ); } } export abstract class BatchedAudienceFeedConnectorBasePlugin<T> extends GenericAudienceFeedConnectorBasePlugin< T, BatchedUserSegmentUpdatePluginResponse<T> > { constructor(enableThrottling = false) { super(enableThrottling); const batchUpdateHandler = new BatchUpdateHandler<AudienceFeedBatchContext, T>( this.app, this.emptyBodyFilter, this.logger, ); batchUpdateHandler.registerRoute(async (request) => { const instanceContext = await this.getInstanceContext(request.context.feed_id); return this.onBatchUpdate(request, instanceContext); }); } protected abstract onBatchUpdate( request: BatchUpdateRequest<AudienceFeedBatchContext, T>, instanceContext: AudienceFeedConnectorBaseInstanceContext, ): Promise<BatchUpdatePluginResponse>; } export abstract class AudienceFeedConnectorBasePlugin extends GenericAudienceFeedConnectorBasePlugin< void, UserSegmentUpdatePluginResponse > { constructor(enableThrottling = false) { super(enableThrottling); this.initBatchUpdate(); } private initBatchUpdate(): void { this.app.post('/v1/batch_update', this.emptyBodyFilter, async (req: express.Request, res: express.Response) => { res.status(500).send({ status: 'error', message: "Plugin doesn't support batch update" }); }); } }