Apollo gateway caching - graphql

I have service Test-Service which provides some data via graphql and Apollo Gateway. And I would like to cache requests on Apollo gateway, but it doesn't work, I still see in logs of Test-Service that it receives requests. Here is code of Apollo Gateway
import {ApolloServer} from'#apollo/server'
import {ApolloGateway, IntrospectAndCompose} from '#apollo/gateway'
import {hiveApollo} from'#graphql-hive/client'
import {KeyvAdapter} from"#apollo/utils.keyvadapter"
import {ApolloServerPluginCacheControl} from"#apollo/server/plugin/cacheControl"
import {startStandaloneServer} from'#apollo/server/standalone'
// const Keyv = require("keyv");
import {InMemoryLRUCache} from "#apollo/utils.keyvaluecache"
import responseCachePlugin from '#apollo/server-plugin-response-cache';
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{name: 'test-service', url: 'http://test-service-svc/graphql'}
],
pollIntervalInMs: 5000
})
})
async function startApolloServer() {
const server = new ApolloServer({
gateway,
plugins: [
responseCachePlugin()
],
// cache: new KeyvAdapter(new Keyv({
// url: "redis://graphql-gateway-redis:6379",
// sentinels: [
// {host: "graphql-gateway-redis", port: 26379}
// ]
// })
// )
cache: new InMemoryLRUCache(),
cacheControl: {
defaultMaxAge: 50,
},
});
const {url} = await startStandaloneServer(server, {
listen: {port: 4000}
})
console.log(`🚀 Server ready at: ${url}`)
}
startApolloServer();
As you can see I also tried using redis. And here is example schema:
type Query {
getInfo(
userId: Int
): [Response]
getInfoCached(
userId: Int
): [Response] #cacheControl(maxAge: 30)
}
type Response #cacheControl(maxAge: 30) {
id: Int,
createdAt: DateTime, #cacheControl(maxAge: 10),
firstName: String
}

Related

how to dynamically assume a role to access DynamoDB from a Lambda using appConfig?

I have two AWS stacks :
one has a dynamoDB table and "exports" (to appConfig) the tableArn, tableName and tableRoleArn (which ideally should allow access to the table).
import { App, Stack, StackProps } from '#aws-cdk/core';
import * as dynamodb from '#aws-cdk/aws-dynamodb';
import * as cdk from '#aws-cdk/core';
import * as appconfig from '#aws-cdk/aws-appconfig';
import { Effect, PolicyStatement, Role, ServicePrincipal } from '#aws-cdk/aws-iam';
export class ExportingStack extends Stack {
constructor(scope: App, id: string, props: StackProps) {
super(scope, id, props);
const table = new dynamodb.Table(this, id, {
billingMode: dynamodb.BillingMode.PROVISIONED,
readCapacity: 1,
writeCapacity: 1,
removalPolicy: cdk.RemovalPolicy.DESTROY,
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'createdAt', type: dynamodb.AttributeType.NUMBER },
pointInTimeRecovery: true
});
const tablePolicy = new PolicyStatement({
effect: Effect.ALLOW,
resources: [table.tableArn],
actions: ['*']
});
const role = new Role(this, 'tableRoleArn', {
assumedBy: new ServicePrincipal('lambda.amazonaws.com')
});
role.addToPolicy(
tablePolicy
);
const app = '***';
const environment = '***';
const profile = '***';
const strategy = 'v';
const newConfig = new appconfig.CfnHostedConfigurationVersion(this, 'ConfigurationName', {
applicationId: app,
configurationProfileId: profile,
contentType: 'application/json',
content: JSON.stringify({
tableArn: table.tableArn,
tableName: table.tableName,
tableRoleArn: role.roleArn
}),
description: 'table config'
});
const cfnDeployment = new appconfig.CfnDeployment(this, 'MyCfnDeployment', {
applicationId: app,
configurationProfileId: profile,
environmentId: environment,
configurationVersion: newConfig.ref,
deploymentStrategyId: strategy
});
}
}
The second has a function which I would like to be able to use the appConfig configuration to dynamically access the table.
import { App, CfnOutput, Stack, StackProps } from '#aws-cdk/core';
import { LayerVersion, Runtime } from '#aws-cdk/aws-lambda';
import { NodejsFunction } from '#aws-cdk/aws-lambda-nodejs';
import { Effect, PolicyStatement } from '#aws-cdk/aws-iam';
export class ConsumingStack extends Stack {
constructor(scope: App, id: string, props: StackProps) {
super(scope, id, props);
const fn = new NodejsFunction(this, 'foo', {
runtime: Runtime.NODEJS_12_X,
handler: 'foo',
entry: `stack/foo.ts`
});
fn.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
resources: ['*'],
actions: [
'ssm:*',
'appconfig:*',
'sts:*',
]
})
);
new CfnOutput(this, 'functionArn', { value: fn.functionArn});
// https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions.html
// https://github.com/aws-samples/aws-appconfig-codepipeline-cdk/blob/main/infrastructure/src/main/kotlin/com/app/config/ServerlessAppStack.kt
const appConfigLayer = LayerVersion.fromLayerVersionArn(
this,
'appconfigLayer',
'arn:aws:lambda:eu-west-2:282860088358:layer:AWS-AppConfig-Extension:47'
);
fn.addLayers(appConfigLayer);
}
}
and handler
import type { Context } from 'aws-lambda';
import fetch from 'node-fetch';
import { DynamoDB, STS } from 'aws-sdk';
import { Agent } from 'https';
export const foo = async (event: any, lambdaContext: Context): Promise<void> => {
const application = '*****';
const environment = '*****';
const configuration = '*****';
const response = await fetch(
`http://localhost:2772/applications/${application}/environments/${environment}/configurations/${configuration}`
);
const configurationData = await response.json();
console.log(configurationData);
const credentials = await assumeRole(configurationData.tableRoleArn);
const db = new DynamoDB({
credentials: {
sessionToken: credentials.sessionToken,
secretAccessKey: credentials.secretAccessKey,
accessKeyId: credentials.accessKeyId
},
apiVersion: '2012-08-10',
region: '*****',
httpOptions: {
agent: new Agent({ keepAlive: true }),
connectTimeout: 1000,
timeout: 5000
},
signatureVersion: 'v4',
maxRetries: 3
});
const item = await db
.getItem({ TableName: configurationData.tableName, Key: { id: { S: 'coolPeople' }, createdAt: { N: '0' } } }, (e) => {
console.log('e', e);
})
.promise();
console.log('item:', item?.Item?.value?.L);
};
/**
* Assume Role for cross account operations
*/
export const assumeRole = async (tableRoleArn: string): Promise<any> => {
let params = {
RoleArn: tableRoleArn,
RoleSessionName: 'RoleSessionName12345'
};
console.info('Assuming Role with params:', params);
let sts = new STS();
return new Promise((resolve, reject) => {
sts.assumeRole(params, (error, data) => {
if (error) {
console.log(`Could not assume role, error : ${JSON.stringify(error)}`);
reject({
statusCode: 400,
message: error['message']
});
} else {
console.log(`Successfully Assumed Role details data=${JSON.stringify(data)}`);
resolve({
statusCode: 200,
body: data
});
}
});
});
};
The issue is that I get this error when trying to assumeRole within the lambda.
Could not assume role, error : {"message":"User: arn:aws:sts::****:assumed-role/ConsumingStack-fooServiceRole****-***/ConsumingStack-foo****-*** is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::****:role/ExportingStack-tableRoleArn****-***","code":"AccessDenied","time":"2022-02-21T16:06:44.474Z","requestId":"****-***-****-****","statusCode":403,"retryable":false,"retryDelay":26.827985116659757}
So is it possible for a Lambda to dynamically assume a role to access a table from a different stack?
I've got it working by changing the trust relationship of the table role to be arn:aws:iam::${Stack.of(this).account}:root
import { App, Stack, StackProps } from '#aws-cdk/core';
import * as dynamodb from '#aws-cdk/aws-dynamodb';
import * as cdk from '#aws-cdk/core';
import * as appconfig from '#aws-cdk/aws-appconfig';
import { Effect, PolicyStatement, Role, ArnPrincipal } from '#aws-cdk/aws-iam';
export class ExportingStack extends Stack {
constructor(scope: App, id: string, props: StackProps) {
super(scope, id, props);
const table = new dynamodb.Table(this, id, {
billingMode: dynamodb.BillingMode.PROVISIONED,
readCapacity: 1,
writeCapacity: 1,
removalPolicy: cdk.RemovalPolicy.DESTROY,
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'createdAt', type: dynamodb.AttributeType.NUMBER },
pointInTimeRecovery: true
});
const tablePolicy = new PolicyStatement({
effect: Effect.ALLOW,
resources: [table.tableArn],
actions: ['*']
});
const role = new Role(this, 'tableRoleArn', {
assumedBy: new ArnPrincipal(`arn:aws:iam::${Stack.of(this).account}:root`)
});
role.addToPolicy(tablePolicy);
const app = '***';
const environment = '***';
const profile = '****';
const strategy = '****';
const newConfig = new appconfig.CfnHostedConfigurationVersion(this, 'myConfiguration', {
applicationId: app,
configurationProfileId: profile,
contentType: 'application/json',
content: JSON.stringify({
tableArn: table.tableArn,
tableName: table.tableName,
tableRoleArn: role.roleArn
}),
description: 'table config'
});
const cfnDeployment = new appconfig.CfnDeployment(this, 'MyCfnDeployment', {
applicationId: app,
configurationProfileId: profile,
environmentId: environment,
configurationVersion: newConfig.ref,
deploymentStrategyId: strategy
});
}
}

Apollo federation can't start gateway

Hello I want to use federation.I followed this tutorial. I can start my subgraph but when I start my gateway, I get this error:
Error: A valid schema couldn't be composed. The following composition errors were found:
Cannot extend type "Query" because it is not defined. Did you mean "User"?
Cannot extend type "Mutation" because it is not defined.
I even extended Query and Mutation but got another error.
my gateway code:
import fastify from "fastify";
import { ApolloServer } from "apollo-server-fastify";
import { ApolloGateway } from "#apollo/gateway";
const PORT = process.env.PORT || 4000;
const IP = "0.0.0.0";
const app = fastify({ trustProxy: true });
const gateway = new ApolloGateway({
serviceList: [{ name: "amazon", url: "http://localhost:4001/graphql" }],
});
(async () => {
try {
const { schema, executor } = await gateway.load();
const server = new ApolloServer({ schema, executor });
server.start().then(() => {
app.register(server.createHandler({ path: "/graphql" }));
app.listen(PORT, IP, (err) => {
if (err) {
console.error(err);
} else {
console.log("Server is ready at port 4000");
}
});
});
} catch (error) {
console.log("dick");
console.log("err", error);
}
})();
my schema in subgraph:
type Query {
getUsers: [User]!
}
type Mutation {
createUser(name: String!): Boolean!
}
type User #key(fields: "id") {
id: ID!
name: String!
}
Most likely you're using graphql version 16. Following https://www.apollographql.com/docs/federation/gateway/ you must use 15 for now (December 2021). Try:
yarn add graphql#15
Another detail you can do on latest versions is simply pass the gateway to ApolloServer, like this:
const server = new ApolloServer({ gateway });

Apollo react: combining rest and graphql with link-state

I am trying to use REST endpoints to post data and GraphQL for query and fetch along with apollo-link-state. My rest endpoint is getting hit and application is getting created. But when I try to run the query to write to cache it's not hitting the graphql endpoint. and I keep getting the following error:
Unhandled Rejection (Error): Can't find field findApplicationByUuid({"uuid":"912dc46d-2ef8-4a77-91bc-fec421f5e4bc"}) on object (ROOT_QUERY) {
"application": {
"type": "id",
"id": "$ROOT_QUERY.application",
"generated": true
}
}.
Here are my GQL query
import gql from 'graphql-tag';
const START_APP = gql`
mutation startApp($type: String!) {
application: startApp( input: { applicationType: $type})
#rest(type: "Application", path: "v1/member/application/create", method: "POST") {
uuid: applicationUuid
}
}
`;
const GET_APP = gql`
query getAppByUuid($uuid: String!) {
application: findApplicationByUuid(uuid: $uuid) {
uuid,
type,
version,
}
}
`;
export {
START_APP,
GET_APP,
};
Here is my resolver:
import { START_APP, GET_APP } from './application'
import client from '../apolloClient';
const startApp = async (_, { type }, { cache }) => {
client.mutate({
variables: { type },
mutation: START_APP,
}).then(({ data: { application } }) => {
const { uuid } = application;
const { data } = cache.readQuery({
query: GET_APP,
variables: { uuid },
});
cache.writeQuery({
query: GET_APP,
data,
});
});
};
const resolvers = {
Mutation: {
startApp,
},
};
Here are my links:
import { resolvers, defaults } from './resolvers';
const cache = new InMemoryCache();
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.map(({ message, locations, path }) =>
console.log(`[GQL Error]: Msg: ${message}, Loc: ${locations}, Path: ${path}`));
if (networkError) console.log(`[Network error]: ${networkError}`);
});
const stateLink = withClientState({
cache,
defaults,
resolvers,
});
const restLink = new RestLink({
uri: 'http://localhost:7010/api/',
credentials: 'include',
});
const batchHttpLink = new BatchHttpLink({
uri: 'http://localhost:7010/api/graphql',
credentials: 'include',
});
const httpLink = new HttpLink({
uri: 'http://loaclhost:7010/api/graphql',
credentials: 'include',
});
const link = ApolloLink.from([
errorLink,
stateLink,
restLink,
httpLink,
]);
my client
const client = new ApolloClient({
link,
cache,
});
My react component:
// Remote Mutation
const START_APP = gql`
mutation startApp($type: String!) {
startApp(type: $type) #client {
uuid
}
}
`;
const StartApp = ({ match }) => {
const { type } = match.params;
return (
<Mutation mutation={START_APP} variables={{ type }}>
{startApp => (<button onClick={startApp}>click me</button>)}
</Mutation>
)
};
When I hit the button it calls create endpoint and creates the app and returns the uuid. But the following I want to happen is hit the graphql endpoint and query for the application using the uuid returned from the rest request, and write that data to the cache/state.

Apollo 2.0.0 Graphql cookie session

Can someone help me on this, My setup was as follows prior to Apollo 2.0, I had a server.js in which i used express and graphql-server-express
I had a http only cookie session, when a user logs in I set the jwt token as a cookie and it is set in browser as http only.
On subsequent request I validate the cookie that the browser passes back. It was all working fine and I could access
the token from req.session.token in any other resolver and validate the jwt token saved in the cookie session.
server.js
import express from 'express';
import { graphqlExpress, graphiqlExpress } from 'graphql-server-express';
import { ApolloEngine } from 'apollo-engine';
import bodyParser from 'body-parser';
import cors from 'cors';
import cookieSession from 'cookie-session';
import schema from './schema/';
​
const server = express();
​
server.use(
cookieSession({
name: 'session',
keys: 'k1,k2',
maxAge: 30 * 60 * 1000,
domain: '.mydomain.com',
path: '/',
}),
);
​
const corsOptions = {
origin: 'http://local.mydomain.com:3000',
credentials: true,
methods: ['GET', 'PUT', 'POST', 'OPTIONS'],
};
​
server.use(cors(corsOptions));
​
server.use(
'/graphql',
bodyParser.json(),
graphqlExpress(req => ({
schema,
tracing: true,
context: { req },
})),
);
​
if (process.env.NODE_ENV !== 'production') {
server.use('/graphiql',graphiqlExpress({endpointURL: '/graphql'}));
}
​
const engine = new ApolloEngine({
apiKey: engineConfig.apiKey,
});
​
engine.listen(
{
port: 3000,
graphqlPaths: ['/graphql'],
expressApp: server,
},
() => {console.log('GraphiQL is now running');},
);
authenticateResolver.js
const authenticateResolver = {
Query: {
authenticate: async (root, args, context) => {
const { req } = context;
​
const auth = `Basic ${Buffer.from(`${args.username}:${args.password}`).toString('base64')}`;
​
const axiosResponse = await axios.post("localhot:8080/login, 'true',
{
headers: {
Authorization: auth,
},
});
​
if (axiosResponse.status === 200 && axiosResponse.data.token) {
req.session.token = axiosResponse.data.token;
}
return {
status: 200,
};
},
But when I upgraded to Apollo 2.0 my server.js code changed, authenticateResolver was as is.
I am now unable to access req.session.token in any subsequent requests since the cookie session is not getting set.
When I open Developer tools in chrome I cannot see the cookie being set when Authentication is called.
What am I doing wrong here ?
server.js # After Apollo 2.0 upgrade
​
import express from 'express';
import { ApolloServer, gql } from 'apollo-server-express';
import cors from 'cors';
import cookieSession from 'cookie-session';
import { mergedTypes, resolvers } from './schema/';
​
const server = express();
​
server.use(
cookieSession({
name: 'session',
keys: 'k1,k2',
maxAge: 30 * 60 * 1000,
domain: '.mydomain.com',
path: '/',
}),
);
​
const corsOptions = {
origin: 'http://local.mydomain.com:3000',
credentials: true,
methods: ['GET', 'PUT', 'POST', 'OPTIONS'],
};
​
server.use(cors(corsOptions));
​
server.listen({ port: 3000 }, () => {
console.log('Server ready');
console.log('Try your health check at: .well-known/apollo/app-health');
});
​
const apollo = new ApolloServer({
typeDefs: gql`
${mergedTypes}
`,
resolvers,
engine: false,
context: ({ req }) => ({ req }),
});
​
apollo.applyMiddleware({
server
});
Yes, If you look at the graphql playground there is a settings option if you click on that you can observe few settings, one of them being
"request.credentials": "omit" just change it to
"request.credentials": "include" and save settings and it should now work.
My code looks as follows as well:
const app = express()
app.use(
cookieSession({
name: 'session',
keys: corsConfig.cookieSecret.split(','),
maxAge: 60 * 60 * 1000,
domain: corsConfig.cookieDomain,
path: '/',
})
)
const corsOptions = {
origin: corsConfig.corsWhitelist.split(','),
credentials: true,
methods: ['GET', 'PUT', 'POST', 'OPTIONS'],
}
app.use(cors(corsOptions))
const apollo = new ApolloServer({
typeDefs: gql`
${mergedTypes}
`,
resolvers,
engine: false,
context: ({ req }) => ({ req }),
tracing: true,
debug: !process.env.PRODUCTION,
introspection: !process.env.PRODUCTION,
})
apollo.applyMiddleware({
app,
path: '/',
cors: corsOptions,
})
app.listen({ port: engineConfig.port }, () => {
console.log('🚀 - Server ready')
console.log('Try your health check at: .well-known/apollo/app-health')
})

Apollo 2.x graphIQL Can't Reach Subscription?

My Apollo queries and mutations are working perfectly. Now I'm adding a subscription and for some reason graphIql (please note the I in graphIql) can't trigger the subscription. A breakpoint in the subscription doesn't activate, and the data comes back to graphIql as null. There are no console log errors in graphIql.
Here's the relevant code:
TYPES
type Resolution {
_id: String!
name: String!
goals: [Goal]
completed: Boolean
}
type Query {
resolutions: [Resolution]
getResolutionViaId(resolutionId: String!): Resolution
}
type Mutation {
createResolution(name: String!): Resolution
}
type Subscription {
resolutionWasAdded: Resolution
}
RESOLVERS
Subscription: {
resolutionWasAdded: {
subscribe: () => pubsub.asyncIterator(RESOLUTION_SUBSCRIPTION_TOPIC)
}
}
QUERY ENTERED IN GRAPHIQL
subscription resolutionWasAdded {
resolutionWasAdded {
__typename
_id
name
completed
}
}
RESPONSE IN GRAPHIQL
{
"data": {
"resolutionWasAdded": null
}
}
What could I be missing?
UPDATE
I see I need to set up graphIql to be aware of my subscription endpoint. I've googled quite a bit but don't yet see how to do it with this client setup:
import React from "react";
import {Meteor} from "meteor/meteor";
import {render} from "react-dom";
import {ApolloProvider} from "react-apollo";
import {ApolloLink, from} from "apollo-link";
import {ApolloClient} from "apollo-client";
import {HttpLink} from "apollo-link-http";
import {InMemoryCache} from "apollo-cache-inmemory";
import {onError} from 'apollo-link-error';
import {split} from 'apollo-link';
import {WebSocketLink} from 'apollo-link-ws';
import {getMainDefinition} from 'apollo-utilities';
import {toIdValue} from 'apollo-utilities';
import App from "../../ui/App";
// Create an http link:
const httpLink = new HttpLink({
uri: Meteor.absoluteUrl("graphql"),
credentials: 'same-origin'
})
// Create a WebSocket link:
const wsLink = new WebSocketLink({
uri: `ws://localhost:3200/subscriptions`,
options: {
reconnect: true
}
});
// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const splitLink = split(
// split based on operation type
({query}) => {
const {kind, operation} = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
httpLink,
);
const authLink = new ApolloLink((operation, forward) => {
const token = Accounts._storedLoginToken();
operation.setContext(() => ({
headers: {
"meteor-login-token": token
}
}));
return forward(operation);
});
const client = new ApolloClient({
link: ApolloLink.from([
onError(({graphQLErrors, networkError}) => {
if (graphQLErrors)
graphQLErrors.map(({message, locations, path}) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
),
);
if (networkError) console.log(`[Network error]: ${networkError}`);
}),
authLink,
splitLink,
]),
cache: new InMemoryCache({})
});
const ApolloApp = () => (
<ApolloProvider client={client}>
<App/>
</ApolloProvider>
);
Meteor.startup(() => {
render(<ApolloApp/>, document.getElementById("app"));
});
What is the correct way to modify this setup so as to tell graphIql about the subscription endpoint?

Resources