1414 * limitations under the License.
1515 */
1616
17- import { ErrorHandler , LogHandler , LogLevel } from '../../modules/logging' ;
18- import { Response } from './odp_types' ;
19- import { IOdpClient , OdpClient } from './odp_client' ;
17+ import { LogHandler , LogLevel } from '../../modules/logging' ;
2018import { validate } from '../../utils/json_schema_validator' ;
2119import { OdpResponseSchema } from './odp_response_schema' ;
22- import { QuerySegmentsParameters } from './query_segments_parameters' ;
23- import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory' ;
20+ import { ODP_USER_KEY } from '../../utils/enums' ;
21+ import { RequestHandler , Response as HttpResponse } from '../../utils/http_request_handler/http' ;
22+ import { Response as GraphQLResponse } from './odp_types' ;
2423
2524/**
2625 * Expected value for a qualified/valid segment
@@ -34,102 +33,145 @@ const EMPTY_SEGMENTS_COLLECTION: string[] = [];
3433 * Return value for scenarios with no valid JSON
3534 */
3635const EMPTY_JSON_RESPONSE = null ;
36+ /**
37+ * Standard message for audience querying fetch errors
38+ */
39+ const AUDIENCE_FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed' ;
3740
3841/**
3942 * Manager for communicating with the Optimizely Data Platform GraphQL endpoint
4043 */
4144export interface IGraphQLManager {
42- fetchSegments ( apiKey : string , apiHost : string , userKey : string , userValue : string , segmentsToCheck : string [ ] ) : Promise < string [ ] > ;
45+ fetchSegments ( apiKey : string , apiHost : string , userKey : string , userValue : string , segmentsToCheck : string [ ] ) : Promise < string [ ] | null > ;
4346}
4447
4548/**
46- * Concrete implementation for communicating with the Optimizely Data Platform GraphQL endpoint
49+ * Concrete implementation for communicating with the ODP GraphQL endpoint
4750 */
48- export class GraphqlManager implements IGraphQLManager {
49- private readonly _errorHandler : ErrorHandler ;
50- private readonly _logger : LogHandler ;
51- private readonly _odpClient : IOdpClient ;
51+ export class GraphQLManager implements IGraphQLManager {
52+ private readonly logger : LogHandler ;
53+ private readonly requestHandler : RequestHandler ;
5254
5355 /**
54- * Retrieves the audience segments from the Optimizely Data Platform (ODP)
55- * @param errorHandler Handler to record exceptions
56+ * Communicates with Optimizely Data Platform's GraphQL endpoint
57+ * @param requestHandler Desired request handler for testing
5658 * @param logger Collect and record events/errors for this GraphQL implementation
57- * @param client Client to use to send queries to ODP
5859 */
59- constructor ( errorHandler : ErrorHandler , logger : LogHandler , client ?: IOdpClient ) {
60- this . _errorHandler = errorHandler ;
61- this . _logger = logger ;
62-
63- this . _odpClient = client ?? new OdpClient ( this . _errorHandler ,
64- this . _logger ,
65- RequestHandlerFactory . createHandler ( this . _logger ) ) ;
60+ constructor ( requestHandler : RequestHandler , logger : LogHandler ) {
61+ this . requestHandler = requestHandler ;
62+ this . logger = logger ;
6663 }
6764
6865 /**
6966 * Retrieves the audience segments from ODP
7067 * @param apiKey ODP public key
71- * @param apiHost Fully-qualified URL of ODP
68+ * @param apiHost Host of ODP endpoint
7269 * @param userKey 'vuid' or 'fs_user_id key'
7370 * @param userValue Associated value to query for the user key
7471 * @param segmentsToCheck Audience segments to check for experiment inclusion
7572 */
76- public async fetchSegments ( apiKey : string , apiHost : string , userKey : string , userValue : string , segmentsToCheck : string [ ] ) : Promise < string [ ] > {
77- const parameters = new QuerySegmentsParameters ( {
78- apiKey,
79- apiHost,
80- userKey,
81- userValue,
82- segmentsToCheck,
83- } ) ;
84- const segmentsResponse = await this . _odpClient . querySegments ( parameters ) ;
85- if ( ! segmentsResponse ) {
86- this . _logger . log ( LogLevel . ERROR , 'Audience segments fetch failed (network error)' ) ;
73+ public async fetchSegments ( apiKey : string , apiHost : string , userKey : ODP_USER_KEY , userValue : string , segmentsToCheck : string [ ] ) : Promise < string [ ] | null > {
74+ if ( ! apiKey || ! apiHost ) {
75+ this . logger . log ( LogLevel . ERROR , `${ AUDIENCE_FETCH_FAILURE_MESSAGE } (Parameters apiKey or apiHost invalid)` ) ;
76+ return null ;
77+ }
78+
79+ if ( segmentsToCheck ?. length === 0 ) {
8780 return EMPTY_SEGMENTS_COLLECTION ;
8881 }
8982
83+ const endpoint = `${ apiHost } /v3/graphql` ;
84+ const query = this . toGraphQLJson ( userKey , userValue , segmentsToCheck ) ;
85+
86+ const segmentsResponse = await this . querySegments ( apiKey , endpoint , userKey , userValue , query ) ;
87+ if ( ! segmentsResponse ) {
88+ this . logger . log ( LogLevel . ERROR , `${ AUDIENCE_FETCH_FAILURE_MESSAGE } (network error)` ) ;
89+ return null ;
90+ }
91+
9092 const parsedSegments = this . parseSegmentsResponseJson ( segmentsResponse ) ;
9193 if ( ! parsedSegments ) {
92- this . _logger . log ( LogLevel . ERROR , 'Audience segments fetch failed (decode error)' ) ;
93- return EMPTY_SEGMENTS_COLLECTION ;
94+ this . logger . log ( LogLevel . ERROR , ` ${ AUDIENCE_FETCH_FAILURE_MESSAGE } (decode error)` ) ;
95+ return null ;
9496 }
9597
9698 if ( parsedSegments . errors ?. length > 0 ) {
9799 const errors = parsedSegments . errors . map ( ( e ) => e . message ) . join ( '; ' ) ;
98100
99- this . _logger . log ( LogLevel . WARNING , `Audience segments fetch failed (${ errors } )` ) ;
101+ this . logger . log ( LogLevel . ERROR , `${ AUDIENCE_FETCH_FAILURE_MESSAGE } (${ errors } )` ) ;
100102
101- return EMPTY_SEGMENTS_COLLECTION ;
103+ return null ;
102104 }
103105
104106 const edges = parsedSegments ?. data ?. customer ?. audiences ?. edges ;
105107 if ( ! edges ) {
106- this . _logger . log ( LogLevel . WARNING , 'Audience segments fetch failed (decode error)' ) ;
107- return EMPTY_SEGMENTS_COLLECTION ;
108+ this . logger . log ( LogLevel . ERROR , ` ${ AUDIENCE_FETCH_FAILURE_MESSAGE } (decode error)` ) ;
109+ return null ;
108110 }
109111
110112 return edges . filter ( edge => edge . node . state == QUALIFIED ) . map ( edge => edge . node . name ) ;
111113 }
112114
115+ /**
116+ * Converts the query parameters to a GraphQL JSON payload
117+ * @returns GraphQL JSON string
118+ */
119+ private toGraphQLJson = ( userKey : string , userValue : string , segmentsToCheck : string [ ] ) : string => ( [
120+ '{"query" : "query {customer"' ,
121+ `(${ userKey } : "${ userValue } ") ` ,
122+ '{audiences' ,
123+ '(subset: [' ,
124+ ...segmentsToCheck ?. map ( ( segment , index ) =>
125+ `\\"${ segment } \\"${ index < segmentsToCheck . length - 1 ? ',' : '' } ` ,
126+ ) || '' ,
127+ '] {edges {node {name state}}}}}"}' ,
128+ ] . join ( '' ) ) ;
129+
130+ /**
131+ * Handler for querying the ODP GraphQL endpoint
132+ * @param apiKey ODP API key
133+ * @param endpoint Fully-qualified GraphQL endpoint URL
134+ * @param userKey 'vuid' or 'fs_user_id'
135+ * @param userValue userKey's value
136+ * @param query GraphQL formatted query string
137+ * @returns JSON response string from ODP or null
138+ */
139+ private async querySegments ( apiKey : string , endpoint : string , userKey : string , userValue : string , query : string ) : Promise < string | null > {
140+ const method = 'POST' ;
141+ const url = endpoint ;
142+ const headers = {
143+ 'Content-Type' : 'application/json' ,
144+ 'x-api-key' : apiKey ,
145+ } ;
146+
147+ let response : HttpResponse ;
148+ try {
149+ const request = this . requestHandler . makeRequest ( url , headers , method , query ) ;
150+ response = await request . responsePromise ;
151+ } catch {
152+ return null ;
153+ }
154+
155+ return response . body ;
156+ }
157+
113158 /**
114159 * Parses JSON response
115160 * @param jsonResponse JSON response from ODP
116161 * @private
117162 * @returns Response Strongly-typed ODP Response object
118163 */
119- private parseSegmentsResponseJson ( jsonResponse : string ) : Response | null {
164+ private parseSegmentsResponseJson ( jsonResponse : string ) : GraphQLResponse | null {
120165 let jsonObject = { } ;
121166
122167 try {
123168 jsonObject = JSON . parse ( jsonResponse ) ;
124- // eslint-disable-next-line @typescript-eslint/no-explicit-any
125- } catch ( error : any ) {
126- this . _errorHandler . handleError ( error ) ;
127- this . _logger . log ( LogLevel . ERROR , 'Attempted to parse invalid segment response JSON.' ) ;
169+ } catch {
128170 return EMPTY_JSON_RESPONSE ;
129171 }
130172
131173 if ( validate ( jsonObject , OdpResponseSchema , false ) ) {
132- return jsonObject as Response ;
174+ return jsonObject as GraphQLResponse ;
133175 }
134176
135177 return EMPTY_JSON_RESPONSE ;
0 commit comments