Extend the brX GraphQL Service Schema
Info: The features described on this page require a standard or premium Bloomreach Content license. Contact Bloomreach for details.
Overview
The brX GraphQL Service exposes a unified data graph for online shopping. Given the complexity of modern eCommerce, a single graph often cannot represent all required business domains.
The GraphQL ecosystem provides tools to extend and aggregate schemas. You can add new operation types, directives, or relationships between schemas as needed. This enables you to evolve your API without modifying the core service.
This page describes two common approaches for schema extension and aggregation:
- Schema Merging: Combine type definitions and resolvers from multiple local schemas into a single executable schema. Use this when you need to add commerce functionality not available out-of-the-box, such as custom Commerce Backend API extensions.
- Apollo Federation: Compose a single data graph from multiple sub-graphs. Use this to combine product data (commerce) with content data (CMS) or other domains.
The following sections provide implementation examples for both approaches.
Prerequisites
- The brX GraphQL Service must be running.
- Review the How to install the GraphQL Service guide if you have not set up the service.
Schema Merging
Schema merging allows you to control the unified schema definition. This example demonstrates schema merging with executor functions, enabling schema delegation for all brX GraphQL Service operations. Schema extensions are resolved using the new types and resolvers you define.
Setup
Apply these changes to the GraphQL Service sample project. In your project directory, install the required NPM dependencies:
npm install graphql apollo-server node-jose @graphql-tools/wrap @graphql-tools/merge @graphql-tools/schema node-fetch
Define the GraphQL Service Executor
Define an executor function to delegate GraphQL operations from the merged schema to the brX GraphQL Service sub-schema. The following example assumes the brX GraphQL Service is running locally on port 4000.
const gsExecutor = async ({ document, variables, context }) => { const query = print(document); let headers = { 'Content-Type': 'application/json', 'connector': 'brsm', }; if (context) { if (context.connector) { headers = { ...headers, 'connector': context.connector, } } if (context.authorization) { headers = { ...headers, 'authorization': context.authorization, } } } const fetchResult = await fetch('http://localhost:4000/graphql', { method: 'POST', headers, body: JSON.stringify({ query, variables }), }); return fetchResult.json(); };
Define Extension Types and Resolvers
Next, define new types and resolvers. This example introduces a CustomPayment type, allowing client applications to interact with a payment extension module in the Commerce Backend. The schema includes a payments query and a makePayment mutation. The resolvers translate these requests to the Commerce Backend and map the responses to the CustomPayment type.
const createServer = async (request) => { const typeDefs = ` type CustomPayment { id: String! } type Query { payments: [CustomPayment] } input PaymentInput { moneyAmount: Float } type Mutation { makePayment(paymentInput: PaymentInput): CustomPayment } `; const resolvers = { Query: { payments: async (parent, args, context) => { const accessData = await getAccessData(context.authorization); const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessData.accessToken.access_token}`, }; const fetchResult = await fetch('<COMMERCE_BACKEND_PAYMENT_API_URL>', { method: 'GET', headers, }); const { results } = await fetchResult.json(); return results; } }, Mutation: { makePayment: async (parent, args, context) => { const accessData = await getAccessData(context.authorization); const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessData.accessToken.access_token}`, }; const fetchResult = await fetch('<COMMERCE_BACKEND_PAYMENT_API_URL>', { method: 'POST', headers, body: JSON.stringify({ "amountPlanned": { "currencyCode": ..., "centAmount": ... } }), }); return fetchResult.json(); } }, CustomPayment: { id: payment => payment.id, }, }; ... }
The resolvers use the getAccessData helper function, described in the next section.
Merge Schemas
After defining the new types and resolvers, merge them with the brX GraphQL Service schema. Use the introspectSchema function from the graphql-tools library to retrieve the existing schema, and wrapSchema to delegate operations. Once merged, create an Apollo Server instance.
const createServer = async (request) => { ... const remoteSchema = wrapSchema({ schema: await introspectSchema(gsExecutor), executor: gsExecutor, }); const paymentSchema = makeExecutableSchema({ typeDefs, resolvers }); const mergedSchema = mergeSchemas({ schemas: [remoteSchema, paymentSchema] }); return new ApolloServer({ schema: mergedSchema, context: ({ req }) => ({ authorization: req.headers.authorization, connector: req.headers.connector }) }); }
The Apollo Server context stores the authorization and connector request header values. The executor uses these values when delegating requests to the underlying service.
Implement Access Data Helper
Define the getAccessData helper to decrypt the authorization header and extract access data (such as the access token).
As described in the Access Management documentation, the brX GraphQL Service stores the access data in the authorization header. If your schema extension interacts with the same commerce backend, you can reuse the access token for those requests.
const getAccessData = async(authorization) => { const encryptedAccessData = authorization?.substring('Bearer '.length); let accessData; if (encryptedAccessData) { try { const jwk = await jose.JWK.asKeyStore(JSON.parse(process.env.JWK_KEYSTORE)); const { payload } = await jose.JWE.createDecrypt(jwk).decrypt(encryptedAccessData); return JSON.parse(payload.toString()); } catch (e) { throw new Error(`Invalid authorization: ${e}`); } } } async function startApolloServer() { const server = await createServer(); // The `listen` method launches a web server. server.listen({ port: 4100 }).then(({ url }) => { console.log(`� Server ready at ${url}`); }); } startApolloServer();
The startApolloServer function starts the Apollo Server on port 4100.
Complete Example
Your index.js file should look similar to the following. Ensure you include all required require statements.
const graphqlService = require("@bloomreach/graphql-commerce-connector-service"); const { print } = require('graphql'); const { ApolloServer } = require('apollo-server'); const jose = require('node-jose'); const { introspectSchema, wrapSchema } = require('@graphql-tools/wrap'); const { mergeSchemas } = require('@graphql-tools/merge'); const { makeExecutableSchema } = require('@graphql-tools/schema'); const fetch = require("node-fetch"); const gsExecutor = async ({ document, variables, context }) => { const query = print(document); let headers = { 'Content-Type': 'application/json', 'connector': 'brsm', }; if (context) { if (context.connector) { headers = { ...headers, 'connector': context.connector, } } if (context.authorization) { headers = { ...headers, 'authorization': context.authorization, } } } const fetchResult = await fetch('http://localhost:4000/graphql', { method: 'POST', headers, body: JSON.stringify({ query, variables }), }); return fetchResult.json(); }; const createServer = async (request) => { const typeDefs = ` type CustomPayment { id: String! } type Query { payments: [CustomPayment] } input PaymentInput { moneyAmount: Float } type Mutation { makePayment(paymentInput: PaymentInput): CustomPayment } `; const resolvers = { Query: { payments: async (parent, args, context) => { const accessData = await getAccessData(context.authorization); const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessData.accessToken.access_token}`, }; const fetchResult = await fetch('<COMMERCE_BACKEND_PAYMENT_API_URL>', { method: 'GET', headers, }); const { results } = await fetchResult.json(); return results; } }, Mutation: { makePayment: async (parent, args, context) => { const accessData = await getAccessData(context.authorization); const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessData.accessToken.access_token}`, }; const fetchResult = await fetch('<COMMERCE_BACKEND_PAYMENT_API_URL>', { method: 'POST', headers, body: JSON.stringify({ "amountPlanned": { "currencyCode": ..., "centAmount": ... } }), }); return fetchResult.json(); } }, CustomPayment: { id: payment => payment.id, }, }; const remoteSchema = wrapSchema({ schema: await introspectSchema(gsExecutor), executor: gsExecutor, }); const paymentSchema = makeExecutableSchema({ typeDefs, resolvers }); const mergedSchema = mergeSchemas({ schemas: [remoteSchema, paymentSchema] }); return new ApolloServer({ schema: mergedSchema, context: ({ req }) => ({ authorization: req.headers.authorization, connector: req.headers.connector }) }); } const getAccessData = async(authorization) => { const encryptedAccessData = authorization?.substring('Bearer '.length); let accessData; if (encryptedAccessData) { try { const jwk = await jose.JWK.asKeyStore(JSON.parse(process.env.JWK_KEYSTORE)); const { payload } = await jose.JWE.createDecrypt(jwk).decrypt(encryptedAccessData); return JSON.parse(payload.toString()); } catch (e) { throw new Error(`Invalid authorization: ${e}`); } } } async function startApolloServer() { const server = await createServer(); // The `listen` method launches a web server. server.listen({ port: 4100 }).then(({ url }) => { console.log(`� Server ready at ${url}`); }); } startApolloServer();
Replace all placeholder values, such as <COMMERCE_BACKEND_PAYMENT_API_URL>, with the correct values for your environment.
Run the Project
Start the project locally:
node --require dotenv/config index.js
Authenticate and retrieve an authorization token:
curl --location --request POST 'http://localhost:4000/signin' \--header 'connector: <commerce_backend>' \--header 'Content-Type: application/json' \--data-raw '{}'
Verify the merged schema by sending a findItemsByKeyword query:
curl --location --request POST 'http://localhost:4100/' \
--header 'Content-Type: application/json' --header 'connector: brsm' --header 'authorization: Bearer $TOKEN' \
--data-raw '{"query":"query{\n findItemsByKeyword(\n text:\"\", limit: 1, offset: 0, \n ) {\n items{\n displayName\n itemId{\n id,\n code\n }\n description\n customAttrs {\n name, values\n }\n }\n }\n}\n","variables":{}}'
Test the makePayment mutation:
curl --location --request POST 'http://localhost:4100/' \
--header 'Content-Type: application/json' --header 'connector: <commerce_backend>' --header 'authorization: Bearer $TOKEN' \
--data-raw '{"query":"mutation { makePayment(paymentInput: { moneyAmount: 12 }) { id } }","variables":{}}'
Retrieve the newly created payment:
curl --location --request POST 'http://localhost:4100/' \
--header 'Content-Type: application/json' --header 'connector: <commerce_backend>' --header 'authorization: Bearer $TOKEN' \
--data-raw '{"query":"query { payments { id } }","variables":{}}'
If you see the payment entry, the schema extension is working as expected.
Apollo Federation
The brX GraphQL Service supports extension using Apollo Federation. This section demonstrates a basic example of graph federation, combining the brX GraphQL Service with an additional Article Service. The example uses the Apollo platform.
Setup
Create a new directory (for example, graphql-service-federation). Initialize a new NPM project:
npm init -y
Install the required dependencies:
npm install @apollo/gateway apollo-server apollo-server-express express graphql
Implementing Services
This example uses two services:
- brX GraphQL Service: Provides commerce functionality. It should be running at
http://localhost:4000/graphql. - Article Service: Provides product-related articles.
The Article Service is a simple GraphQL server that resolves articles with a single description field. You can extend this service to connect to other APIs as needed.
Create an index.js file and add the following code:
const { ApolloServer, gql } = require('apollo-server'); const { buildFederatedSchema } = require('@apollo/federation'); const typeDefs = gql` type Article { description: String! } type ItemId { id: String! code: String } extend type Item @key(fields: "itemId") { "The ItemId of a product item" itemId: ItemId! @external "Related article" article: Article } `; const resolvers = { Article: { description() { return 'Hey, this is an article description'; } }, Item: { article() { return {}; } } } const articleService = new ApolloServer({ schema: buildFederatedSchema([{ typeDefs, resolvers }]), }); articleService.listen({ port: 4001}, () => { console.log(`Article Service ready at http://localhost:4001`); });
This schema extends the Item type from the brX GraphQL Service, linking each product to a related article. You can now query both product and article data in a single request.
Configure the Gateway
After both services are running, set up the Apollo Gateway to compose the federated schema. The gateway enables clients to query across both services.
Append the following code to your index.js file:
const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway'); class CommerceConnectorDataSource extends RemoteGraphQLDataSource { willSendRequest({ request, context }) { if (context.connector) { request.http.headers.set('connector', context.connector); } if (context.authorization) { request.http.headers.set('authorization', context.authorization); } } } const gateway = new ApolloGateway({ serviceList: [ { name: 'graphql-service', url: 'http://localhost:4000/graphql' }, { name: 'article-service', url: 'http://localhost:4001' }, ], buildService({ name, url }) { return new CommerceConnectorDataSource({ url }); }, });
By default, Apollo Gateway does not forward incoming HTTP headers to implementing services. The CommerceConnectorDataSource class ensures that the authorization and connector headers are forwarded as needed.
Expose Authentication Endpoints
The brX GraphQL Service provides authentication endpoints. To support authentication through the gateway, re-expose the /signin and /signout endpoints at the gateway level.
Append the following code to your index.js file:
const express = require('express'); const { ApolloServer: ApolloServerExpress } = require('apollo-server-express'); const server = new ApolloServerExpress({ gateway, // Disable subscriptions (not currently supported with ApolloGateway) subscriptions: false, context: ({ req }) => { return { connector: req.headers.connector, authorization: req.headers.authorization, }; }, }); const app = express(); app.post('/signin', function(req, res) { res.redirect(307, 'http://localhost:4000/signin'); }); app.post('/signout', function(req, res) { res.redirect(307, 'http://localhost:4000/signout'); }); server.applyMiddleware({ app }); app.listen({ port: 4999 }, () => { console.log(`🚀 Gateway ready at http://localhost:4999`); });
Sign-in and sign-out requests sent to the gateway are redirected to the brX GraphQL Service. The server context includes the authorization and connector headers for downstream services.
Complete Example
Your index.js file should look like this:
const { ApolloServer, gql } = require('apollo-server'); const { buildFederatedSchema } = require('@apollo/federation'); const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway'); const express = require('express'); const { ApolloServer: ApolloServerExpress } = require('apollo-server-express'); const typeDefs = gql` type Article { description: String! } type ItemId { id: String! code: String } extend type Item @key(fields: "itemId") { "The ItemId of a product item" itemId: ItemId! @external "Related article" article: Article } `; const resolvers = { Article: { description() { return 'Hey, this is an article description'; } }, Item: { article() { return {}; } } } const articleService = new ApolloServer({ schema: buildFederatedSchema([{ typeDefs, resolvers }]), }); articleService.listen({ port: 4001}, () => { console.log(`Article Service ready at http://localhost:4001`); }); class CommerceConnectorDataSource extends RemoteGraphQLDataSource { willSendRequest({ request, context }) { if (context.connector) { request.http.headers.set('connector', context.connector); } if (context.authorization) { request.http.headers.set('authorization', context.authorization); } } } const gateway = new ApolloGateway({ serviceList: [ { name: 'graphql-service', url: 'http://localhost:4000/graphql' }, { name: 'article-service', url: 'http://localhost:4001' }, ], buildService({ name, url }) { return new CommerceConnectorDataSource({ url }); }, }); const server = new ApolloServerExpress({ gateway, // Disable subscriptions (not currently supported with ApolloGateway) subscriptions: false, context: ({ req }) => { return { connector: req.headers.connector, authorization: req.headers.authorization, }; }, }); const app = express(); app.post('/signin', function(req, res) { res.redirect(307, 'http://localhost:4000/signin'); }); app.post('/signout', function(req, res) { res.redirect(307, 'http://localhost:4000/signout'); }); server.applyMiddleware({ app }); app.listen({ port: 4999 }, () => { console.log(`🚀 Gateway ready at http://localhost:4999`); });
Start the application:
node index.js
You should see output similar to:
Article Service ready at http://localhost:4001
🚀 Gateway ready at http://localhost:4999
The GraphQL playground is available at http://localhost:4999/graphql.
Query Example
You can now execute GraphQL queries against the federated schema:
query findItemsByKeyword {
findItemsByKeyword(text: "", offset: 0, limit: 5) {
items {
itemId {
id
code
}
displayName
# ...additional fields
}
}
}
This query returns product data from the brX GraphQL Service. You can also query for related article data using the federated schema.
For more details on schema extension, delegation, and federation, refer to the official Apollo Federation documentation and GraphQL Tools documentation.