-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
A GraphQL generic SCO. Illustrated with countries API but all concept…
…s are applicable to any GraphQL APIs.
- Loading branch information
1 parent
77e518d
commit e02ee51
Showing
15 changed files
with
5,430 additions
and
0 deletions.
There are no files selected for viewing
99 changes: 99 additions & 0 deletions
99
ServiceCallOut/GraphQL/DataAccess/GetCountriesServiceCallout.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/* | ||
This SCO gets data using graphQL from URL specified in this file | ||
The modeler simply references a user friendly query name in the Query entity and specifies the values for each of the query parameters (if needed) | ||
Thus the modeler has the full power of the rule language to specify query names and parameters based on various conditions in rule execution. | ||
You can see an example of this in PrepareQuery.ers. | ||
The query names are defined as a string enum (See QueryName enum in the vocab.ecore file) | ||
Internally, it works this way: | ||
1) The queries and all associated metadata are defined in the Queries.js. | ||
2) The modeler friendly query names are referenced in the module.exports file and points to the query metadata. | ||
3) The query metadata contains the actual query as well as: | ||
o Where to find the data of interest in the GraphQL result (pathToData) | ||
o If adding results to root, the data type of entity to add (entityType) | ||
o If adding results to a relationship, the external name of the relationship to which we will be adding the sub-payload (roleName) | ||
So, in brief: the user friendly query name maps to metadata in Queries.js. | ||
From that metadata, the GraphQL connector can process the query generically. | ||
*/ | ||
|
||
const graphQLConnector = require('./GraphQLConnector'); | ||
|
||
const graphQLSCO = { | ||
func: 'getCountriesFct', | ||
type: 'ServiceCallout', | ||
description: {'en_US': 'This function gets a set of countries from the end point https://countries.trevorblades.com/graphql.'}, | ||
extensionType: 'SERVICE_CALLOUT', | ||
name: {'en_US': 'getCountries'} | ||
}; | ||
|
||
async function getCountriesFct(corticonDataManager, serviceCalloutProperties) { | ||
const logger = corticonDataManager.getLogger(); | ||
logger.logDebug(`SCO getCountriesFct V1.0`); | ||
|
||
// This url (as well as authentication token) could be passed as a configuration property (do not add to runtime properties as these get bundled in the decision service) | ||
// See this sample on how to use configuration properties: https://github.com/corticon/corticon.js-samples/tree/master/ServiceCallOut/AccessConfigurationProperties | ||
const url = 'https://countries.trevorblades.com/graphql'; | ||
const queryMetadata = graphQLConnector.getQueryMetadata(corticonDataManager, logger); | ||
logger.logDebug(`*** SCO: Got query metadata: ${JSON.stringify(queryMetadata, 2)}`); | ||
const countriesList = await graphQLConnector.getData(url, queryMetadata.query, queryMetadata.pathToData, logger); | ||
logger.logDebug(`*** SCO: Got countries: ${JSON.stringify(countriesList, 2)}`); | ||
let countries; | ||
// When we get by continent we need to get one more level down | ||
if ( queryMetadata.pathToData2 !== undefined && queryMetadata.pathToData2 !== null ) | ||
countries = countriesList[queryMetadata.pathToData2]; | ||
else | ||
countries = countriesList; | ||
|
||
logger.logDebug(`*** SCO: Got countries: ${JSON.stringify(countries, 2)}`); | ||
|
||
// If there is an entity, let's add to it, otherwise we just add all the countries to the top level of the payload | ||
const nameEntityToAppendResultsTo = getNameEntityToAppendResultsTo(serviceCalloutProperties); | ||
const entity = getEntityToAppendTo(nameEntityToAppendResultsTo, corticonDataManager, logger); | ||
if ( entity !== null ) { | ||
logger.logDebug(`*** SCO: Adding to ${nameEntityToAppendResultsTo} entity using external name: ${queryMetadata.roleName}`); | ||
corticonDataManager.addAssociationsToEntity(entity, queryMetadata.roleName, countries); // second parameter is the attribute name as it appears in the payload | ||
} | ||
else { | ||
logger.logDebug(`*** SCO: Adding to top level based on entity type: ${queryMetadata.entityType}`); | ||
corticonDataManager.addEntitiesAndAssociations(queryMetadata.entityType, countries); | ||
} | ||
} | ||
|
||
|
||
function getNameEntityToAppendResultsTo(serviceCalloutProperties) { | ||
// Where do we append the result. | ||
// If you pass a valid entity as a run time prop then the SCO will add the data to the entity using an association. | ||
// If you do not pass one then it adds to root. | ||
if (serviceCalloutProperties['entityToAppendTo'] !== undefined && serviceCalloutProperties['entityToAppendTo'] !== null) | ||
return serviceCalloutProperties['entityToAppendTo']; | ||
else | ||
return null; // will append to root | ||
} | ||
|
||
/* | ||
Generic function to find the entity to append to | ||
*/ | ||
function getEntityToAppendTo(entityToAppendCountryTo, corticonDataManager, logger) { | ||
logger.logDebug(`*** Searching for ${entityToAppendCountryTo} entities and returning first one`); | ||
if ( entityToAppendCountryTo === undefined || entityToAppendCountryTo === null ) { | ||
logger.logDebug(`*** nothing to search for`); | ||
return null; | ||
} | ||
|
||
let entityToReturn = null; // default to not finding | ||
const entitiesSet = corticonDataManager.getEntitiesByType(entityToAppendCountryTo); | ||
for (const entity of entitiesSet) { | ||
logger.logDebug(`*** one entity ${entity.id}`); | ||
entityToReturn = entity; | ||
break; | ||
} | ||
|
||
if ( entityToReturn === null ) | ||
logger.logDebug(`*** didn't find any ${entityToAppendCountryTo} `); | ||
|
||
return entityToReturn; | ||
} | ||
|
||
exports.getCountriesFct = getCountriesFct; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
const queries = require('./Queries'); | ||
|
||
async function getData (url, graphQLQuery, pathToData, logger) { | ||
const graphQLResult = await getDataUsingGraphQL( graphQLQuery, url, logger ); | ||
if ( graphQLResult !== null ) { | ||
const results = graphQLResult[pathToData]; | ||
return results; | ||
} | ||
else | ||
throw new Error ("GQL Connector: Bad data returned by GraphQL - check log"); | ||
} | ||
|
||
function getQueryMetadata(corticonDataManager, logger) { | ||
const queryEntity = getQueryEntity(corticonDataManager, logger); // The entity containing all the data to use in the query | ||
const params = getQueryParams(queryEntity, logger); // the list of parameter values to use for substitution in the query | ||
const queryMetadataResolverFct = getQueryMetadataResolverFct(queryEntity, logger); // the function to call to invoke param substitution | ||
|
||
// do params substitution in the query and retrieve other metadata about that query | ||
const queryMetadata = queryMetadataResolverFct(params); | ||
return queryMetadata; | ||
} | ||
|
||
function getQueryEntity(corticonDataManager, logger) { | ||
logger.logError(`*** GQL Connector: Searching for Query entity`); | ||
|
||
let queryEntity = null; // default to not finding | ||
const entitiesSet = corticonDataManager.getEntitiesByType('Query'); | ||
for (const entity of entitiesSet) { | ||
logger.logError(`*** GQL Connector: Found one query`); | ||
queryEntity = entity; | ||
break; | ||
} | ||
|
||
if ( queryEntity === null ) { | ||
logger.logError(`*** GQL Connector: didn't find any Query`); | ||
throw new Error ('Payload does not have a Query entity'); | ||
} | ||
else | ||
return queryEntity; | ||
} | ||
|
||
function getQueryParams(queryEntity, logger) { | ||
logger.logError(`*** Extracting parameters from Query entity `); | ||
|
||
const params = []; | ||
|
||
//todo: null/undefined checks + iterate through all param attributes | ||
params[0] = queryEntity.parameter1; | ||
params[1] = queryEntity.parameter2; | ||
|
||
logger.logError(`*** GQL Connector: params added ${params[0]}`); | ||
return params; | ||
} | ||
|
||
function getQueryMetadataResolverFct(queryEntity, logger) { | ||
//todo: null/undefined checks | ||
const name = queryEntity.name; | ||
logger.logError(`*** GQL Connector: Extracting query name from Query entity. Found: ${name}`); | ||
|
||
const queryResolverFct = queries[name]; | ||
if ( queryResolverFct === undefined || queryResolverFct === null ) | ||
throw new Error (`*** GQL Connector: missing query resolver fct for "${name}"` ); | ||
|
||
if ( typeof queryResolverFct !== 'function' ) | ||
throw new Error (`*** GQL Connector: query resolver for "${name}" is not a function` ); | ||
|
||
logger.logDebug(`*** GQL Connector: query resolver for ${name} found`); | ||
|
||
return queryResolverFct; | ||
} | ||
|
||
/* | ||
Generic function to get data from server using fetch API querying with a GraphQL query | ||
*/ | ||
async function getDataUsingGraphQL( graphQLQuery, url, logger ) { | ||
const results = await fetch(url, { | ||
method: 'POST', | ||
headers: { | ||
"Content-Type": "application/json" | ||
}, | ||
body: JSON.stringify(graphQLQuery) | ||
}); | ||
|
||
const text = await results.text(); // Parse it as text as sometimes we may get invalid json | ||
|
||
try { | ||
const graphQL = JSON.parse(text); | ||
logger.logDebug(`***** GQL Connector: getDataUsingGraphQL got response:\n ${JSON.stringify(graphQL)}`); | ||
return graphQL.data; | ||
} | ||
catch ( e ) { | ||
logger.logError(`***** GQL Connector: getDataUsingGraphQL Bad data returned by GraphQL ${text}`); | ||
return null; | ||
} | ||
} | ||
|
||
module.exports = { getData, getQueryMetadata }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
|
||
/* | ||
This module defines the two queries available to modelers: see module.exports at bottom of the file. | ||
Query can be tried at https://lucasconstantino.github.io/graphiql-online/ | ||
https://countries.trevorblades.com/graphql | ||
query { | ||
continent(code: "OC") { | ||
countries { | ||
name | ||
code | ||
currency | ||
awsRegion | ||
languages { | ||
code | ||
name | ||
} | ||
} | ||
} | ||
} | ||
*/ | ||
function getCountriesByContinentMetadata(params) { | ||
const countriesByContinent = { | ||
query: `{ | ||
continent(code: "${params[0]}") { | ||
countries { | ||
awsRegion | ||
name | ||
code | ||
phone | ||
currency | ||
languages { | ||
code | ||
name | ||
} | ||
} | ||
} | ||
}` | ||
}; | ||
|
||
return { "query": countriesByContinent, // The GraphQL query with all the parameters resolved. | ||
"pathToData": 'continent', // The name of the attribute where to find the list of countries | ||
"pathToData2": 'countries', // The name of the attribute where to find countries array under pathToData - this is our JSON sub-payload in the GraphQL results. | ||
'entityType': 'Countries', // The Corticon vocabulary datatype of the entity we will be adding sub-payload to the root of the payload (only used when adding to root) | ||
'roleName': 'countries' // The Corticon vocabulary external name of the relationship to which we will be adding the sub-payload (only used when adding to a specific entity) - in our case the role name is "countriesOfInterest" but the external name is "countries" - see the properties of the relationship in vocab.ecore | ||
}; | ||
} | ||
|
||
|
||
/* | ||
Query can be tried at https://lucasconstantino.github.io/graphiql-online/ | ||
https://countries.trevorblades.com/graphql | ||
We intentionally return more attributes than the ones mapped into the vocabulary to show that more data | ||
can be inserted into the payload; of course no rules can accessed these additional attributes but this | ||
data will be output in the results. And it can be used by other services. | ||
query { | ||
country(code: "CH") { | ||
awsRegion | ||
name | ||
code | ||
phone | ||
currency | ||
native | ||
capital | ||
emoji | ||
continent { | ||
code | ||
} | ||
languages { | ||
code | ||
name | ||
} | ||
} | ||
} | ||
*/ | ||
function getCountryByCodeMetadata(params) { | ||
const countriesByCode = { | ||
query: `{ | ||
country(code: "${params[0]}") { | ||
awsRegion | ||
name | ||
code | ||
phone | ||
currency | ||
native | ||
capital | ||
emoji | ||
continent { | ||
code | ||
} | ||
languages { | ||
code | ||
name | ||
} | ||
} | ||
}` | ||
}; | ||
|
||
return { "query": countriesByCode, // The GraphQL query with all the parameters resolved. | ||
"pathToData": 'country', // The name of the attribute where to find the JSON sub-payload in the GraphQL results. | ||
'entityType': 'Countries', // The Corticon vocabulary datatype of the entity we will be adding sub-payload to the root of the payload (only used when adding to root) | ||
'roleName': 'countries' // The Corticon vocabulary external name of the relationship to which we will be adding the sub-payload (only used when adding to a specific entity) | ||
}; | ||
} | ||
|
||
module.exports = { "Get Countries by Continent": getCountriesByContinentMetadata, "Get Country by Code": getCountryByCodeMetadata }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<com.corticon.rulesemf.assetmodel:RuleflowAsset xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:com.corticon.rulesemf.assetmodel="http:///com/corticon/rulesemf/assetmodel.ecore" xmlns:com.corticon.rulesemf.canonicalrulemodel.ruleflow="http:///com/corticon/rulesemf/canonicalrulemodel/ruleflow.ecore" xmlns:com.corticon.rulesemf.viewrulemodel.ruleflow.flowdiagram="http:///com/corticon/rulesemf/viewrulemodel/ruleflow/flowdiagram.ecore" majorVersionNumber="7" minorVersionNumber="1" buildNumber="8022" updateStamp="_jpWUECivEe-SkvI28G1NTg" externalChecksum="2722078124:507989954:3409648236" studioType="Javascript" rulesheetAssets="SubFlows/PrepareQueryGetOneCountry.ers#/ ProcessCountries.ers#/"> | ||
<ruleflow majorVersion="1" vocabularyUpdateStamp="_NJew0PwlEe6XxI7TJTZ7WA" vocabulary="vocab.ecore#/"> | ||
<flowControlList xsi:type="com.corticon.rulesemf.canonicalrulemodel.ruleflow:ActivityNode" name="PrepareQueryGetOneCountry" order="1" ruleActivityUpdateStamp="_TGvkAfwmEe6XxI7TJTZ7WA" nextStep="#//@ruleflow/@flowControlList.1" invokes="SubFlows/PrepareQueryGetOneCountry.ers#//@ruleset"/> | ||
<flowControlList xsi:type="com.corticon.rulesemf.canonicalrulemodel.ruleflow:ActivityNode" name="Get One Country with GraphQL" order="2" ruleActivityUpdateStamp="_jpTQwiivEe-SkvI28G1NTg" nextStep="#//@ruleflow/@flowControlList.2" invokes="#//@ruleflow/@connectorList.0"/> | ||
<flowControlList xsi:type="com.corticon.rulesemf.canonicalrulemodel.ruleflow:ActivityNode" name="ProcessCountries" order="3" ruleActivityUpdateStamp="_cpzhkfwZEe6o1KyzUc0vrA" invokes="ProcessCountries.ers#//@ruleset"/> | ||
<connectorList className="GetCountriesServiceCallout.js" serviceName="getCountries"/> | ||
</ruleflow> | ||
<ruleflowViewList xsi:type="com.corticon.rulesemf.viewrulemodel.ruleflow.flowdiagram:FlowDiagram"> | ||
<flowShapeList xsi:type="com.corticon.rulesemf.viewrulemodel.ruleflow.flowdiagram:ActivityShape" x="135" y="60" width="336" height="61" outboundEdges="#//@ruleflowViewList.0/@flowEdgeList.0" activityNode="#//@ruleflow/@flowControlList.0"> | ||
<annotations name="Color" value="4259584"/> | ||
</flowShapeList> | ||
<flowShapeList xsi:type="com.corticon.rulesemf.viewrulemodel.ruleflow.flowdiagram:ActivityShape" x="135" y="225" width="331" height="69" outboundEdges="#//@ruleflowViewList.0/@flowEdgeList.1" inboundEdges="#//@ruleflowViewList.0/@flowEdgeList.0" activityNode="#//@ruleflow/@flowControlList.1"> | ||
<annotations name="Color" value="16777088"/> | ||
</flowShapeList> | ||
<flowShapeList xsi:type="com.corticon.rulesemf.viewrulemodel.ruleflow.flowdiagram:ActivityShape" x="135" y="390" width="331" height="68" inboundEdges="#//@ruleflowViewList.0/@flowEdgeList.1" activityNode="#//@ruleflow/@flowControlList.2"> | ||
<annotations name="Color" value="12632256"/> | ||
</flowShapeList> | ||
<flowEdgeList sourceShape="#//@ruleflowViewList.0/@flowShapeList.0" targetShape="#//@ruleflowViewList.0/@flowShapeList.1"/> | ||
<flowEdgeList sourceShape="#//@ruleflowViewList.0/@flowShapeList.1" targetShape="#//@ruleflowViewList.0/@flowShapeList.2"/> | ||
</ruleflowViewList> | ||
</com.corticon.rulesemf.assetmodel:RuleflowAsset> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<com.corticon.rulesemf.assetmodel:RuleflowAsset xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:com.corticon.rulesemf.assetmodel="http:///com/corticon/rulesemf/assetmodel.ecore" xmlns:com.corticon.rulesemf.canonicalrulemodel.ruleflow="http:///com/corticon/rulesemf/canonicalrulemodel/ruleflow.ecore" xmlns:com.corticon.rulesemf.viewrulemodel.ruleflow.flowdiagram="http:///com/corticon/rulesemf/viewrulemodel/ruleflow/flowdiagram.ecore" majorVersionNumber="7" minorVersionNumber="1" buildNumber="8022" updateStamp="_qnRvgSivEe-SkvI28G1NTg" externalChecksum="2722078124:507989954:3409648236" studioType="Javascript" rulesheetAssets="ProcessCountries.ers#/"> | ||
<ruleflow majorVersion="1" vocabularyUpdateStamp="_NJew0PwlEe6XxI7TJTZ7WA" vocabulary="vocab.ecore#/"> | ||
<flowControlList xsi:type="com.corticon.rulesemf.canonicalrulemodel.ruleflow:ActivityNode" name="Get Country from GraphQL" order="1" ruleActivityUpdateStamp="_qnPTQSivEe-SkvI28G1NTg" nextStep="#//@ruleflow/@flowControlList.1" invokes="#//@ruleflow/@connectorList.0"/> | ||
<flowControlList xsi:type="com.corticon.rulesemf.canonicalrulemodel.ruleflow:ActivityNode" name="ProcessCountries" order="2" ruleActivityUpdateStamp="_cpzhkfwZEe6o1KyzUc0vrA" invokes="ProcessCountries.ers#//@ruleset"/> | ||
<connectorList className="GetCountriesServiceCallout.js" serviceName="getCountries"/> | ||
</ruleflow> | ||
<ruleflowViewList xsi:type="com.corticon.rulesemf.viewrulemodel.ruleflow.flowdiagram:FlowDiagram"> | ||
<flowShapeList xsi:type="com.corticon.rulesemf.viewrulemodel.ruleflow.flowdiagram:ActivityShape" x="180" y="135" width="247" height="72" outboundEdges="#//@ruleflowViewList.0/@flowEdgeList.0" activityNode="#//@ruleflow/@flowControlList.0"> | ||
<annotations name="Color" value="16777088"/> | ||
</flowShapeList> | ||
<flowShapeList xsi:type="com.corticon.rulesemf.viewrulemodel.ruleflow.flowdiagram:ActivityShape" x="180" y="345" width="256" height="62" inboundEdges="#//@ruleflowViewList.0/@flowEdgeList.0" activityNode="#//@ruleflow/@flowControlList.1"> | ||
<annotations name="Color" value="12632256"/> | ||
</flowShapeList> | ||
<flowEdgeList sourceShape="#//@ruleflowViewList.0/@flowShapeList.0" targetShape="#//@ruleflowViewList.0/@flowShapeList.1"/> | ||
</ruleflowViewList> | ||
</com.corticon.rulesemf.assetmodel:RuleflowAsset> |
Oops, something went wrong.