I am using the #apollo/gateway for a GraphQL implementation.
When implementing a subgraph, the endpoint is an internal DNS record. I have a total of 3 microservices, see below for the code snippet:
const { ApolloGateway, IntrospectAndCompose, RemoteGraphQLDataSource } = require("#apollo/gateway");
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: "users-api", url: `users.private/graphql?` },
{ name: "cars-api", url: `cars.private/graphql?` },
{ name: "posts-api", url: `posts.private/graphql?` }
],
}),
buildService({ name, url }) {
return new RemoteGraphQLDataSource({
url,
willSendRequest({ request, context }) {
Object.keys(context.headers || {}).forEach(key => {
if (context.headers[key]) {
request.http.headers.set(key, context.headers[key]);
}
});
},
});
},
});
What I am trying to do is sending a custom header to each of the subgraphs. For example:
subgraph users-api, inject header: service=users
subgraph cars-api, inject header: service=cars
Is this possible?
Thanks in advance!
Related
I am having trouble while trying to use custom header parameter in apollo server. I have an apollo server as below:
import { ApolloServer } from 'apollo-server-lambda'
import { ApolloGateway, IntrospectAndCompose, GraphQLDataSourceProcessOptions, RemoteGraphQLDataSource } from '#apollo/gateway'
import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core'
import { GraphQLRequest } from 'apollo-server-types'
import { SignatureV4 } from '#aws-sdk/signature-v4'
import { Sha256 } from '#aws-crypto/sha256-js'
import { OutgoingHttpHeader } from 'http'
import { defaultProvider } from '#aws-sdk/credential-provider-node'
import { HttpRequest } from '#aws-sdk/protocol-http'
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
/**
* Adds the necessary IAM Authorization headers for AppSync requests
* #param request The request to Authorize
* #returns The headers to pass through to the request
*/
private async getAWSCustomHeaders(request: GraphQLRequest): Promise<{
[key: string]: OutgoingHttpHeader | undefined
}> {
const { http, ...requestWithoutHttp } = request
if (!http) return {}
const url = new URL(http.url)
//check local env
if(url.host.match(/localhost:20002/)) return {'x-api-key':'da2-fakeApiId123456'}
// If the graph service is not AppSync, we should not sign these request.
if (!url.host.match(/appsync-api/)) return {}
const httpRequest = new HttpRequest({
hostname: url.hostname,
path: url.pathname,
method: http.method,
headers: {
Host: url.host,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestWithoutHttp),
})
const signedRequest = await new SignatureV4({
region: 'eu-west-1',
credentials: defaultProvider(),
service: 'appsync',
sha256: Sha256,
}).sign(httpRequest)
return signedRequest.headers || {}
}
/**
* Customize the request to AppSync
* #param options The options to send with the request
*/
public async willSendRequest({ request, context }: GraphQLDataSourceProcessOptions) {
const customHeaders = await this.getAWSCustomHeaders(request)
if (customHeaders) {
Object.keys(customHeaders).forEach((h) => {
request.http?.headers.set(h, customHeaders[h] as string)
})
}
// context not available when introspecting
if (context.event)
Object.keys(context.event.requestContext.authorizer.lambda).forEach((h) => {
request.http?.headers.set(h, context.event.requestContext.authorizer.lambda[h] as string)
})
}
}
const server = new ApolloServer({
gateway: new ApolloGateway({
buildService({ url }) {
return new AuthenticatedDataSource({ url })
},
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'CONFIGURATIONSERVICE', url: process.env.CONFIGURATION_SERVICE_API_URL }
]
})
}),
debug: true,
context: ({ event, context, express}) => ({
headers: event.headers,
functionName: context.functionName,
event,
context,
expressRequest: express.req,
}),
introspection: true,
plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
})
exports.handler = server.createHandler({
expressGetMiddlewareOptions: {
cors: {
origin: '*',
}
}
})
When I try to execute configurationservice via playground on port 3000, I realized that I do not x-api-key header parameter and therefore I get 401 authorization. I do not understand the reason of missing header parameter that I already added in the codebase and any help would be appreciated. Thank you!
Ok I noticed that I can also add header parameters via playground application. Now it returns no authorization error.
So I have an axios request to a rapid API, my function looks like this...
//Initialize the lookup API that utalizes rapidAPI to get breach data
app.get("/lookup/:email/:function", (req, res) => {
var options = {
method: "GET",
url: "https://breachdirectory.p.rapidapi.com/",
params: { func: `${req.params.function}`, term: `${req.params.email}` },
headers: {
"x-rapidapi-host": "breachdirectory.p.rapidapi.com",
"x-rapidapi-key": `${config.RAPID_API_KEY}`,
},
};
axios
.request(options)
.then(function (response) {
res.json(response.data);
})
.catch(function (error) {
console.error(error);
});
}
});
The res.json(response.data); will show on the page a result like this:
{
"disclaimer": "This data is aggregated from BreachDirectory, HaveIBeenPwned, and Vigilante.pw.",
"info": "For full source info, request e.g. https://breachdirectory.tk/api/source?name=Animoto",
"sources": [
"123RF",
"500px",
"Adobe",
"AntiPublic",
"Apollo",
"Bitly",
"Dave",
"Disqus",
"Dropbox",
"ExploitIn",
"ShareThis",
"Straffic",
"Ticketfly",
"Tumblr",
"VerificationsIO"
]
}
I want to loop through everything in the "sources" array, and call upon the following:
https://haveibeenpwned.com/api/v3/breach/[ITEM]
So, the first one will call upon https://haveibeenpwned.com/api/v3/breach/123RF
So each result from that call will look like this:
{
"Name": "123RF",
"Title": "123RF",
"Domain": "123rf.com",
"BreachDate": "2020-03-22",
"AddedDate": "2020-11-15T00:59:50Z",
"ModifiedDate": "2020-11-15T01:07:10Z",
"PwnCount": 8661578,
"Description": "In March 2020, the stock photo site 123RF suffered a data breach which impacted over 8 million subscribers and was subsequently sold online. The breach included email, IP and physical addresses, names, phone numbers and passwords stored as MD5 hashes. The data was provided to HIBP by dehashed.com.",
"LogoPath": "https://haveibeenpwned.com/Content/Images/PwnedLogos/123RF.png",
"DataClasses": [
"Email addresses",
"IP addresses",
"Names",
"Passwords",
"Phone numbers",
"Physical addresses",
"Usernames"
],
"IsVerified": true,
"IsFabricated": false,
"IsSensitive": false,
"IsRetired": false,
"IsSpamList": false
}
I want to make my res.json send over a JSON string that will have all the sources still there, along with the "Title","Description", and "LogoPath" from the API calls that it pulled for each one of the sources. So I will have a JSON string with the sources along with the title of each source, description of each source, and LogoPath of each source.
You have two options:
Create an array of promises and run with Promise.all
app.get('/lookup/:email/:function', async (req, res) => {
var options = {
method: 'GET',
url: 'https://breachdirectory.p.rapidapi.com/',
params: { func: `${req.params.function}`, term: `${req.params.email}` },
headers: {
'x-rapidapi-host': 'breachdirectory.p.rapidapi.com',
'x-rapidapi-key': `${config.RAPID_API_KEY}`,
},
};
axios.request(options)
.then((response) => {
const requestTasks = [];
for (let item of response.data.sources) {
const itemOption = {
method: 'GET',
url: `https://haveibeenpwned.com/api/v3/breach/${item}`,
headers: {
'content-type': 'application/json; charset=utf-8'
}
};
requestTasks.push(axios.request(itemOption));
}
return Promise.all(requestTasks);
})
.then((responseList) => {
for (let response of responseList) {
console.log(response.data);
}
})
.catch((error) => {
console.error(error);
});
});
Use async/await (promise) and for await for get data from for loop
app.get('/lookup/:email/:function', async (req, res) => {
try {
var options = {
method: 'GET',
url: 'https://breachdirectory.p.rapidapi.com/',
params: { func: `${req.params.function}`, term: `${req.params.email}` },
headers: {
'x-rapidapi-host': 'breachdirectory.p.rapidapi.com',
'x-rapidapi-key': `${config.RAPID_API_KEY}`,
},
};
const response = await axios.request(options);
for await (let item of response.data.sources) {
const itemOption = {
method: 'GET',
url: `https://haveibeenpwned.com/api/v3/breach/${item}`,
headers: {
'content-type': 'application/json; charset=utf-8'
}
};
const itemResponse = await axios.request(itemOption);
console.log(itemResponse.data);
}
} catch (error) {
console.error(error);
}
});
This how I managed to make it works.
first: I didn't had any APi key (and didn't want to register to get one) So i used a dummy Api.
although the logic stay the same as i have tested the result.
second i kept all your initial url just next to the one i used.so you can easily switch back to your original url.
finally i put comment to any critical part, and i named variable in a
way that they almost describe what they do.
so you can copy past test it to understand my logic then adapt it to your use case.
here the code
// make sure to replace /lookup by /lookup/:email/:function after testing my logic
app.get('/lookup', async (req, res) => {
try {
// in this options no change just switch back to your url
var options = {
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/albums',
// url: "https://breachdirectory.p.rapidapi.com/",
// params: { func: `${req.params.function}`, term: `${req.params.email}` },
// headers: {
// 'x-rapidapi-host': 'breachdirectory.p.rapidapi.com',
// 'x-rapidapi-key': `${config.RAPID_API_KEY}`,
// },
};
// here you get all your sources list (in my case it an array of object check picture 1 bellow)
const allSources = await axios.request(options)
console.log(allSources.data);
// because my dummy api response is a huge array i slice to limited number
const reduceAllsource = allSources.data.slice(0,5);
console.log(reduceAllsource);
// note here you need to replace reduceAllsource.map by allSources.data.map
// because you don't need a sliced array
const allSourcesWithDetails = reduceAllsource.map(async (_1sourceEachtime)=>{
// here you can switch back to your original url
// make sure to replace [ITEM] by ${_1sourceEachtime}
const itemOption = await axios({
method: 'GET',
url: `https://jsonplaceholder.typicode.com/albums/${_1sourceEachtime.id}/photos`,
// url:`https://haveibeenpwned.com/api/v3/breach/[ITEM]`
headers: {
'content-type': 'application/json; charset=utf-8'
}
});
// this the place you can mix the 2 result.
const mixRes1AndRes2 ={
sources:_1sourceEachtime.title,
details:itemOption.data.slice(0,1)
}
return mixRes1AndRes2;
})
// final result look like the picture 2 below
finalRes= await Promise.all(allSourcesWithDetails);
return res.status(200).json({response: finalRes});
}
catch (error) {
console.log(error);
}
});
Picture 1
Picture 2
I am currently writing some tests for our hapi routes. The route I want to test looks like that:
server.route(
{
method: 'POST',
path: '/',
options: {
tags: ['api'],
cors: true,
handler: async (req: Hapi.Request | any, h: Hapi.ResponseObject) => {
if (!req.params.userId) {
throw Boom.badRequest();
}
return 200;
}
}});
So my test looks like this:
it('should return 200', async () => {
const request : ServerInjectOptions = {
url: '/user',
method: 'POST',
payload: {
email: 'e#email.de',
password: 'secred',
firstname: 'John',
lastname: 'Doe'
},
app: {}
};
const response = await server.inject(request);
expect(response.statusCode).toEqual(200);
});
As you can see the route expects a param in the params array with the name userId but i am not able to set the parameter on the ServerInjectOptions object. The error I get is that the property does not exist on the ServerInjectOptions type.
Is there any other way i can set the params array? I didn`t find something in the docs maybe i missed it and someone can tell me where to find it.
Thanks in advance
For the route I believe you add the name of the parameter to the path like so:
server.route(
{
method: 'POST',
path: '/:userId',
//
}});
And for the test you should be able to add your parameter to the url option:
const request : ServerInjectOptions = {
url: '/user/parameterYouNeedToAdd',
//
};
Or if the parameter is a variable:
const request : ServerInjectOptions = {
url: '/user/' + parameterYouNeedToAdd,
//
};
The following NodeJS Hapi code generate an error when querying at http://localhost:3000/documentation
If I change the path of the endpoint to something else than /models, like /users for instance, everything works well. It looks like the endpoint /models is reserved.
Any idea why any other endpoint work except /models? How can I fix it? I can't change the URL as too many people use it.
var Hapi = require('hapi'),
Inert = require('inert'),
Vision = require('vision'),
Joi = require('joi'),
HapiSwagger = require('hapi-swagger')
var server = new Hapi.Server();
server.connection({
host: 'localhost',
port: 3000
});
var swaggerOptions = {
apiVersion: "1.0"
};
server.register([
Inert,
Vision,
{
register: HapiSwagger,
options: swaggerOptions
}], function (err) {
server.start(function(){
// Add any server.route() config here
console.log('Server running at:', server.info.uri);
});
});
server.route(
{
method: 'GET',
path: '/models',
config: {
handler: function (request, reply) {
reply("list of models")
},
description: 'Get todo',
notes: 'Returns a todo item by the id passed in the path',
tags: ['api'],
validate: {
params: {
username: Joi.number()
.required()
.description('the id for the todo item')
}
}
}
}
)
server.start(function(){
// Add any server.route() config here
console.log('Server running at:', server.info.uri);
});
Yes models is part of swagger's internal structure and it looks like there is an issue in the swagger.js file when dealing with endpoints that use models as part of the URL for an endpoint.
The easy fix for this is to use a nickname. This changes the internal ref in swagger, but the UI should still say models and it will fire against your endpoint correctly.
{
method: 'GET',
path: '/models/{username}',
config: {
handler: function (request, reply) {
reply("list of models")
},
description: 'Get todo',
notes: 'Returns a todo item by the id passed in the path',
tags: ['api'],
plugins: {
'hapi-swagger': {
nickname: 'modelsapi'
}
},
validate: {
params: {
username: Joi.number()
.required()
.description('the id for the todo item')
}
}
}
}
I'm struggling to understand why I don't get anything in the Sencha view. I believe there is an issue with the json transferred between a server and the Sencha proxy. I have a server coded in node and express and a proxy reader in Sencha. But Sencha cannot read the data.
Server Side:
app.get('/weather/fulljson',function(req, res) {
var obj = {data: [{ from_user: 'world', data: 'inter' }, { from_user: 'world2', data: 'interw' }, { from_user: 'world3', data: 'interw' }]};
jsonP = false;
var cb = req.query.callback;
console.log(cb);
if (cb != null) {
jsonP = true;
res.writeHead(200, {
'Content-Type': 'text/javascript',
'connection' : 'close'
});
} else {
res.writeHead(200, {'Content-Type': "application/x-json"});
}
if (jsonP) {
res.end(cb + "(" + JSON.stringify(obj) + ");" );
}
else { res.end(JSON.stringify(obj));
}
)};
});
Sencha View:
Ext.define('Epic.view.Weather', {
xtype: 'graph',
extend: 'Ext.DataView',
//requires: ['Epic.store.SWeather'],
requires: ['Epic.model.MWeather', 'Ext.data.proxy.JsonP'],
config: {
store: {
autoLoad: true,
fields: ['from_user', 'data'],
proxy: {
type: 'jsonp',
url: 'http://localhost:3000/weather/fulljson',
reader: {
type: 'json',
rootProperty: 'data'
}
}
}
},
itemTpl: '{from_user}'
});
Response in Chrome:
Ext.data.JsonP.callback1({"data":[{"from_user":"world","data":"inter"},{"from_user":"world2","data":"interw"},{"from_user":"world3","data":"interw"}]});
But nothing appear in the view.
Did you check your store after load? is it contains data that you send? If yes - in that case something wrong with your view. You can use debugger tools and get your component and check store.
Also could you shows your model Epic.model.MWeather ? Actually if you have model, you don't have to add fields to your store.