I'm wondering on how to pass the next-auth session as context to my nexus queries. The reson behind is that I want the sessions email to retrieve data from my database with nexus. I'm also using Apollo Server and next-connect here.
Here's what I tried:
The Apollo Server
import { ApolloServer } from "apollo-server-micro";
import { MicroRequest } from 'apollo-server-micro/dist/types';
import { ServerResponse } from 'http';
import { getRequestOrigin } from './../../server/get-request-origin';
import handler from "../../server/api-route";
import prisma from "../../server/db/prisma";
import { schema } from "../../server/graphql/schema";
export const config = {
api: {
bodyParser: false,
},
};
export interface GraphQLContext {
session?: {
user: {
name: string
email: string
image: string
},
expires: Date // This is the expiry of the session, not any of the tokens within the session
};
prisma: typeof prisma;
origin: string;
}
const apolloServer = new ApolloServer({
schema,
context: ({ req }): GraphQLContext => ({
session: req.user,
origin: getRequestOrigin(req),
prisma,
}),
})
const startServer = apolloServer.start();
export default handler().use((req: MicroRequest, res: ServerResponse) => {
startServer.then(() => {
apolloServer.createHandler({
path: "/api",
})(req, res);
});
});
My middleware to pass the session:
import { NextApiRequest, NextApiResponse } from "next";
import { Session } from 'next-auth';
import cookieSession from "cookie-session";
import { error } from "next/dist/build/output/log";
import { getSession } from 'next-auth/react';
import nc from "next-connect";
import { trustProxyMiddleware } from "./trust-proxy-middleware";
export interface Request extends NextApiRequest {
user?: Session | null;
}
const COOKIE_SECRET = process.env.COOKIE_SECRET;
/**
* Create an API route handler with next-connect and all the necessary middlewares
*
* #example
* ```ts
* export default handler().get((req, res) => { ... })
* ```
*/
function handler() {
if (!COOKIE_SECRET)
throw new Error(`Please add COOKIE_SECRET to your .env.local file!`);
return (
nc<Request, NextApiResponse>({
onError: (err, _, res) => {
error(err);
res.status(500).end(err.toString());
},
})
// In order for authentication to work on Vercel, req.protocol needs to be set correctly.
// However, Vercel's and Netlify's reverse proxy setup breaks req.protocol, which the custom
// trustProxyMiddleware fixes again.
.use(trustProxyMiddleware)
.use(
cookieSession({
name: "session",
keys: [COOKIE_SECRET],
maxAge: 24 * 60 * 60 * 1000 * 30,
// Do not change the lines below, they make cy.auth() work in e2e tests
secure:
process.env.NODE_ENV !== "development" &&
!process.env.INSECURE_AUTH,
signed:
process.env.NODE_ENV !== "development" &&
!process.env.INSECURE_AUTH,
})
)
.use(async (req: Request, res: NextApiResponse) => {
const session = await getSession({ req })
if (session) {
// Signed in
console.log("Session", JSON.stringify(session, null, 2))
} else {
// Not Signed in
res.status(401)
}
res.end()
})
);
}
export default handler;
And the nexus query
const queries = extendType({
type: "Query",
definition: (t) => {
t.field("currentUser", {
type: "User",
resolve: (_, __, ctx) => {
console.log(ctx);
if (!ctx.session?.user.email) return null;
return prisma.user.findUnique({
where: {
email: ctx.session?.user.email,
},
});
},
});
},
});
Related
I was trying to make a little demo with GraphQL subscriptions and GraphQL Apollo client.
I already have my GraphQL API, but when I try to use Apollo client, it looks like it doesn't complete the websocket subscribe step:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery } from '#apollo/client';
import { split, HttpLink } from '#apollo/client';
import { getMainDefinition } from '#apollo/client/utilities';
import { GraphQLWsLink } from '#apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { useSubscription } from '#apollo/react-hooks'
import reportWebVitals from './reportWebVitals';
const httpLink = new HttpLink({
uri: 'https://mygraphql.api'
});
const wsLink = new GraphQLWsLink(createClient({
url: 'wss://mygraphql.api',
options: {
reconnect: true
}
}));
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
fetchOptions: {
mode: 'no-cors',
}
});
const FAMILIES_SUBSCRIPTION = gql`
subscription{
onFamilyCreated {
id
name
}
}
`;
function LastFamily() {
const { loading, error, data } = useSubscription(FAMILIES_SUBSCRIPTION, {
variables: { },
onData: data => console.log('new data', data)
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error!</div>;
console.log(data);
const family = data.onFamilyCreated[0];
return (
<div>
<h1>{family.name}</h1>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
(<ApolloProvider client={client}>
<div>
<LastFamily />
</div>
</ApolloProvider>));
reportWebVitals();
According to graphql-transport-ws, to accomplish a success call, it should call connection_init and subscribe message. But when I open Dev Tools, it only sends "connection_init"
I'm expecting this output:
What step should I add to accomplish a successful call using graphql-transport-ws?
P.s. I'm not a React Developer, just be kind.
The solutions I'm putting up are based on #apollo/server v.4, with expressMiddleware and mongodb/mongoose on the backend and subscribeToMore with updateQuery on the client-side instead of useSubscription hook. In light of my observations, I believe there may be some issues with your backend code that require refactoring. The transport library graphql-transport-ws has been deprecated and advise to use graphql-ws. The following setup also applies as of 12.2022.
Subscription on the backend
Install the following dependencies.
$ npm i #apollo/server #graphql-tools/schema graphql-subscriptions graphql-ws ws cors body-parser mongoose graphql express
Set up the db models, I will refer to mongodb using mongoose and it might look like this one e.g.
import mongoose from 'mongoose'
const Schema = mongoose.Schema
const model = mongoose.model
const FamilySchema = new Schema({
name: {
type: String,
unique: true, //optional
trim: true,
}
})
FamilySchema.virtual('id').get(function () {
return this._id.toHexString()
})
FamilySchema.set('toJSON', {
virtuals: true,
transform: (document, retObj) => {
delete retObj.__v
},
})
const FamilyModel = model('FamilyModel', FamilySchema)
export default FamilyModel
Setup schema types & resolvers; it might look like this one e.g.
// typeDefs.js
const typeDefs = `#graphql
type Family {
id: ID!
name: String!
}
type Query {
families: [Family]!
family(familyId: ID!): Family!
}
type Mutation {
createFamily(name: String): Family
}
type Subscription {
familyCreated: Family
}
`
// resolvers.js
import { PubSub } from 'graphql-subscriptions'
import mongoose from 'mongoose'
import { GraphQLError } from 'graphql'
import FamilyModel from '../models/Family.js'
const pubsub = new PubSub()
const Family = FamilyModel
const resolvers = {
Query: {
families: async () => {
try {
const families = await Family.find({})
return families
} catch (error) {
console.error(error.message)
}
},
family: async (parent, args) => {
const family = await Family.findById(args.familyId)
return family
},
Mutation: {
createFamily: async (_, args) => {
const family = new Family({ ...args })
try {
const savedFamily = await family.save()
const createdFamily = {
id: savedFamily.id,
name: savedFamily.name
}
// resolvers for backend family subscription with object iterator FAMILY_ADDED
pubsub.publish('FAMILY_CREATED', { familyCreated: createdFamily })
return family
} catch (error) {
console.error(error.message)
}
}
},
Subscription: {
familyCreated: {
subscribe: () => pubsub.asyncIterator('FAMILY_CREATED'),
}
},
Family: {
id: async (parent, args, contextValue, info) => {
return parent.id
},
name: async (parent) => {
return parent.name
}
}
}
export default resolvers
At the main entry server file (e.g. index.js) the code might look like this one e.g.
import dotenv from 'dotenv'
import { ApolloServer } from '#apollo/server'
import { expressMiddleware } from '#apollo/server/express4'
import { ApolloServerPluginDrainHttpServer } from '#apollo/server/plugin/drainHttpServer'
import { makeExecutableSchema } from '#graphql-tools/schema'
import { WebSocketServer } from 'ws'
import { useServer } from 'graphql-ws/lib/use/ws'
import express from 'express'
import http from 'http'
import cors from 'cors'
import bodyParser from 'body-parser'
import typeDefs from './schema/tpeDefs.js'
import resolvers from './schema/resolvers.js'
import mongoose from 'mongoose'
dotenv.config()
...
mongoose.set('strictQuery', false)
let db_uri
if (process.env.NODE_ENV === 'development') {
db_uri = process.env.MONGO_DEV
}
mongoose.connect(db_uri).then(
() => {
console.log('Database connected')
},
(err) => {
console.log(err)
}
)
const startGraphQLServer = async () => {
const app = express()
const httpServer = http.createServer(app)
const schema = makeExecutableSchema({ typeDefs, resolvers })
const wsServer = new WebSocketServer({
server: httpServer,
path: '/',
})
const serverCleanup = useServer({ schema }, wsServer)
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose()
},
}
},
},
],
})
await server.start()
app.use(
'/',
cors(),
bodyParser.json(),
expressMiddleware(server)
)
const PORT = 4000
httpServer.listen(PORT, () =>
console.log(`Server is now running on http://localhost:${PORT}`)
)
}
startGraphQLServer()
Subscription on the CRA frontend
Install the following dependencies.
$ npm i #apollo/client graphql graphql-ws
General connection setup e.g.
// src/client.js
import { ApolloClient, HttpLink, InMemoryCache, split } from '#apollo/client'
import { getMainDefinition } from '#apollo/client/utilities'
import { defaultOptions } from './graphql/defaultOptions'
import { GraphQLWsLink } from '#apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
...
const baseUri = process.env.REACT_APP_BASE_URI // for the client
const wsBaseUri = process.env.REACT_APP_WS_BASE_URI // for the backend as websocket
const httpLink = new HttpLink({
uri: baseUri,
})
const wsLink = new GraphQLWsLink(
createClient({
url: wsBaseUri
})
)
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
httpLink
)
const client = new ApolloClient({
cache: new InMemoryCache(),
link: splitLink,
})
export default client
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import client from './client'
import { ApolloProvider } from '#apollo/client'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>
)
Define the operations types for the client: queries, mutation & subscription e.g.
// src/graphql.js
import { gql } from '#apollo/client'
// Queries
export const FAMILIES = gql`
query Families {
families {
id
name
}
}
`
export const FAMILY = gql`
query Family($familyId: ID) {
family {
id
name
}
}
`
// Mutation
export const CREATE_FAMILY = gql`
mutation createFamily($name: String!) {
createFamily(name: $name) {
id
name
}
}
`
// Subscription
export const FAMILY_SUBSCRIPTION = gql`
subscription {
familyCreated {
id
name
}
}
Components, it might look like this one e.g.
Apollo's useQuery hook provides us with access to a function called subscribeToMore. This function can be destructured and used to act on new data that comes in via subscription. This has the result of rendering our app real-time.
The subscribeToMore function utilizes a single object as an argument. This object requires configuration to listen for and respond to subscriptions.
At the very least, we must pass a subscription document to the document key in this object. This is a GraphQL document in which we define our subscription.
We can a updateQuery field that can be used to update the cache, similar to how we would do in a mutation.
// src/components/CreateFamilyForm.js
import { useMutation } from '#apollo/client'
import { CREATE_FAMILY, FAMILIES } from '../graphql'
...
const [createFamily, { error, loading, data }] = useMutation(CREATE_FAMILY, {
refetchQueries: [{ query: FAMILIES }], // be sure to refetchQueries after mutation
})
...
// src/components/FamilyList.js
import React, { useEffect, useState } from 'react'
import { useQuery } from '#apollo/client'
import { Families, FAMILY_SUBSCRIPTION } from '../graphql'
const { cloneDeep, orderBy } = pkg
...
export const FamilyList = () => {
const [families, setFamilies] = useState([])
const { loading, error, data, subscribeToMore } = useQuery(Families)
...
useEffect(() => {
if (data?.families) {
setFamilies(cloneDeep(data?.families)) // if you're using lodash but it can be also setFamilies(data?.families)
}
}, [data?.families])
useEffect(() => {
subscribeToMore({
document: FAMILY_SUBSCRIPTION,
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev
const newFamily = subscriptionData.data.familyCreated
if (!prev.families.find((family) => family.id === newFamily.id)) {
return Object.assign({}, prev.families, {
families: [...prev.families, newFamily],
})
} else {
return prev
}
},
})
}, [subscribeToMore])
const sorted = orderBy(families, ['names'], ['desc']) // optional; order/sort the list
...
console.log(sorted)
// map the sorted on the return statement
return(...)
END. Hard-coding some of the default resolvers are useful for ensuring that the value that you expect will returned while avoiding the return of null values. Perhaps not in every case, but for fields that refer to other models or schema.
Happy coding!
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
});
}
}
I have a TypeOrm/React app connected with mongodb database, where server is on port 4000, and client is on port 3000.
On client I created file googleAuth:
import passport from 'passport';
import {User} from './entity/User';
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const googleOptions = {
clientID: process.env.clientID,
clientSecret: process.env.clientSecret,
callbackURL: 'http://localhost:4000/auth/google/callback',
profileFields: ['id', 'email', 'first_name', 'last_name'],
};
const googleCallback = (_accessToken: any, _refreshToken: any, profile: any, done: any) => {
const matchingUser = User.findOne({googleId: profile.id});
if (matchingUser) {
done(null, matchingUser);
return;
}
const newUser = {
googleId: profile.id,
firstName: profile.name.givenName,
lastName: profile.name.familyName,
email: profile.emails && profile.emails[0] && profile.emails[0].value
};
User.create(newUser);
done(null, newUser);
}
export const googleAuth = () => passport.use(new GoogleStrategy(
googleOptions,
googleCallback
));
My index.ts(server) file looks like this:
import "reflect-metadata";
import {createConnection} from "typeorm";
import "dotenv/config"
import express from "express";
import { ApolloServer } from "apollo-server-express";
import { buildSchema } from "type-graphql";
import cors from "cors";
import { PostResolver } from "./resolvers/PostResolver";
import { UserResolver } from "./resolvers/UserResolver";
import path from 'path';
import passport from "passport";
import { googleAuth} from './googleAuth';
(async () => {
await createConnection();
googleAuth();
passport.serializeUser(function(user: any, done: any) {
done(null, user);
});
passport.deserializeUser(function(user: any, done: any) {
done(null, user);
});
const app = express();
app.use(cors());
app.use(passport.initialize());
app.get('/auth/google', passport.authenticate('google', { scope: ['email'] }));
app.get('/auth/google/callback', passport.authenticate('google', {
successRedirect: 'http://localhost:4000/graphql',
failureRedirect: 'http://localhost:4000/graphql',
}));
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [ PostResolver, UserResolver ]
}),
context: ({ req }) => ({
getUser: () => req.user,
logout: () => req.logout()
})
// context: ({ req, res }) => ({ req, res })
});
apolloServer.applyMiddleware({ app });
app.use(express.static('public'));
app.get('*', (_, res) => {
res.sendFile(path.resolve(__dirname, '../public', 'index.html'))
});
const port = process.env.PORT || 4000;
app.listen(port, () => console.log(`Server started on port ${port}`));
})();
My UserResolver:
import {
Resolver,
Query,
Ctx
} from "type-graphql";
import { User } from "../entity/User";
import { MyContext } from '../MyContext';
import { ExpressContext } from "apollo-server-express/dist/ApolloServer";
#Resolver()
export class UserResolver {
#Query(() => [User])
users() {
return User.find();
}
#Query(() => String)
getUser(#Ctx() { req }: ExpressContext) {
console.log(req);
return `your user id is: ${req.user}`;
}
}
In https://console.developers.google.com in credenatials I add
Authorised JavaScript origins: http://localhost:4000
In Authorised redirect URIs: http://localhost:4000/auth/google/callback
I can log in, but I can't get any data. I know that it's very messy code, but I need to organize this and rewrite it.
First, I want to know what domains and redirect should I add in my credentials.
Second is to configure server.
Third to create UserResolvers.
Fourth to get data with client.
I am trying to configure subscriptions with Apollo 2 and NEXT.js. I can get the client to connect to the server and they are working in the GraphQL playground, so the bad configuration must be in the withData file, or the component that handles the subscription.
When inspecting the socket connection on the network panel in chrome, the subscription payload does not get added as a frame, like it does in the GraphQL playground.
withData:
import { ApolloLink, Observable } from 'apollo-link';
import { GRAPHQL_ENDPOINT, WS_PATH } from '../config/env';
import { ApolloClient } from 'apollo-client';
import { BatchHttpLink } from 'apollo-link-batch-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { WebSocketLink } from 'apollo-link-ws';
import { createPersistedQueryLink } from 'apollo-link-persisted-queries';
import { onError } from 'apollo-link-error';
import withApollo from 'next-with-apollo';
import { withClientState } from 'apollo-link-state';
function createClient({ headers }) {
const cache = new InMemoryCache();
const request = async (operation) => {
operation.setContext({
http: {
includeExtensions: true,
includeQuery: false
},
headers
});
};
const requestLink = new ApolloLink(
(operation, forward) => new Observable((observer) => {
let handle;
Promise.resolve(operation)
.then(oper => request(oper))
.then(() => {
handle = forward(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer)
});
})
.catch(observer.error.bind(observer));
return () => {
if (handle) handle.unsubscribe();
};
})
);
return new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
console.log({ graphQLErrors });
}
if (networkError) {
console.log('Logout user');
}
}),
requestLink,
// link,
withClientState({
defaults: {
isConnected: true
},
resolvers: {
Mutation: {
updateNetworkStatus: (_, { isConnected }, { cache }) => {
cache.writeData({ data: { isConnected } });
return null;
}
}
},
cache
}),
createPersistedQueryLink().concat(
new BatchHttpLink({
uri: GRAPHQL_ENDPOINT,
credentials: 'include'
}),
process.browser
? new WebSocketLink({
uri: WS_PATH,
options: {
reconnect: true
}
})
: null
)
]),
cache
});
}
export default withApollo(createClient);
Subscription component:
import { CONVERSATION_QUERY } from '../../constants/queries';
import { CONVERSATION_SUBSCRIPTION } from '../../constants/subscriptions';
import PropTypes from 'prop-types';
import { Query } from 'react-apollo';
const Conversation = props => (
<Query
{...props}
query={CONVERSATION_QUERY}
variables={{ input: { _id: props._id } }}
>
{(payload) => {
const more = () => payload.subscribeToMore({
document: CONVERSATION_SUBSCRIPTION,
variables: { input: { conversation: props._id } },
updateQuery: (prev, { subscriptionData }) => {
console.log({ subscriptionData });
if (!subscriptionData.data.messageSent) return prev;
const data = subscriptionData;
console.log({ data });
return Object.assign({}, prev, {});
},
onError(error) {
console.log(error);
},
onSubscriptionData: (data) => {
console.log('onSubscriptionData ', data);
}
});
return props.children({ ...payload, more });
}}
</Query>
);
Conversation.propTypes = {
children: PropTypes.func.isRequired
};
export default Conversation;
The subscription that has been tested in the GraphQL playground:
import gql from 'graphql-tag';
export const CONVERSATION_SUBSCRIPTION = gql`
subscription messageSent($input: messageSentInput) {
messageSent(input: $input) {
_id
users {
_id
profile {
firstName
lastName
jobTitle
company
picture
}
}
messages {
_id
body
createdAt
read
sender {
_id
profile {
firstName
lastName
jobTitle
company
picture
}
}
}
}
}
`;
The more function is then executed in componentDidMount:
componentDidMount() {
this.props.subscribeToMore();
}
The result in the console from the log in updateQuery is:
{"data":{"messageSent":null}}
I hadn't configured my withData file properly. You need to use split from the apollo-link package to let Apollo determine if the request should be handled with http or ws. Here is my working configuration file.
import { ApolloLink, Observable } from 'apollo-link';
import { ApolloClient } from 'apollo-client';
import { BatchHttpLink } from 'apollo-link-batch-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { WebSocketLink } from 'apollo-link-ws';
import { createPersistedQueryLink } from 'apollo-link-persisted-queries';
import { getMainDefinition } from 'apollo-utilities';
import { onError } from 'apollo-link-error';
import { split } from 'apollo-link';
import withApollo from 'next-with-apollo';
import { withClientState } from 'apollo-link-state';
import { GRAPHQL_ENDPOINT, WS_PATH } from '../config/env';
function createClient({ headers }) {
const cache = new InMemoryCache();
const request = async (operation) => {
operation.setContext({
http: {
includeExtensions: true,
includeQuery: false
},
headers
});
};
const requestLink = new ApolloLink(
(operation, forward) => new Observable((observer) => {
let handle;
Promise.resolve(operation)
.then(oper => request(oper))
.then(() => {
handle = forward(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer)
});
})
.catch(observer.error.bind(observer));
return () => {
if (handle) handle.unsubscribe();
};
})
);
const httpLink = new BatchHttpLink({
uri: GRAPHQL_ENDPOINT
});
// Make sure the wsLink is only created on the browser. The server doesn't have a native implemention for websockets
const wsLink = process.browser
? new WebSocketLink({
uri: WS_PATH,
options: {
reconnect: true
}
})
: () => {
console.log('SSR');
};
// Let Apollo figure out if the request is over ws or http
const terminatingLink = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return (
kind === 'OperationDefinition'
&& operation === 'subscription'
&& process.browser
);
},
wsLink,
httpLink
);
return new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
console.error({ graphQLErrors });
}
if (networkError) {
console.error({ networkError});
}
}),
requestLink,
// link,
withClientState({
defaults: {
isConnected: true
},
resolvers: {
Mutation: {
updateNetworkStatus: (_, { isConnected }, { cache }) => {
cache.writeData({ data: { isConnected } });
return null;
}
}
},
cache
}),
// Push the links into the Apollo client
createPersistedQueryLink().concat(
// New config
terminatingLink
// Old config
// new BatchHttpLink({
// uri: GRAPHQL_ENDPOINT,
// credentials: 'include'
// })
)
]),
cache
});
}
export default withApollo(createClient);
Im using Apollo Client, Graphcool and React. I have a working login form but I need the UI to update when the user is logged in, and I need this to happen in different components.
It seems apollo-link-state is the solution for this. My code below seems to work but Im getting this error:
Missing field CurrentUserIsLoggedIn in {} in writeToStore.js
My Apollo Client setup:
import React from 'react';
import ReactDOM from 'react-dom';
// Apollo
import { ApolloProvider } from 'react-apollo';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloLink, split } from 'apollo-link';
import { withClientState } from 'apollo-link-state';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
// Components
import LoginTest from './components/App/LoginTest';
const wsLink = new WebSocketLink({
uri: `wss://subscriptions.graph.cool/v1/XXX`,
options: {
reconnect: true,
},
});
// __SIMPLE_API_ENDPOINT__ looks like: 'https://api.graph.cool/simple/v1/__SERVICE_ID__'
const httpLink = new HttpLink({
uri: 'https://api.graph.cool/simple/v1/XXX',
});
// auth
const middlewareAuthLink = new ApolloLink((operation, forward) => {
const token = localStorage.getItem('auth-token');
const authorizationHeader = token ? `Bearer ${token}` : null;
operation.setContext({
headers: {
authorization: authorizationHeader,
},
});
return forward(operation);
});
const cache = new InMemoryCache();
const defaultState = {
CurrentUserIsLoggedIn: {
__typename: 'CurrentUserIsLoggedIn',
value: false,
},
};
const stateLink = withClientState({
cache,
defaults: defaultState,
resolvers: {
Mutation: {
CurrentUserIsLoggedIn: (_, args) => {
const data = {
CurrentUserIsLoggedIn: {
__typename: 'CurrentUserIsLoggedIn',
value: args.value,
},
};
cache.writeData({ data });
},
},
},
});
const client = new ApolloClient({
cache,
link: ApolloLink.from([
stateLink,
middlewareAuthLink,
split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
httpLink,
),
]),
});
ReactDOM.render(
<ApolloProvider client={client}>
<LoginTest />
</ApolloProvider>,
document.getElementById('root'),
);
LoginTest.js:
import React from 'react';
import { graphql, compose } from 'react-apollo';
import gql from 'graphql-tag';
import App from './App';
const LoginTest = props => {
if (props.LoginServerQuery.loading) return <p>Loading...</p>;
// If the server tells us the user is logged in
if (props.LoginServerQuery.loggedInUser) {
// Then set the local logged in state to true
props.CurrentUserIsLoggedInMutation({
variables: {
value: true,
},
});
}
return <App />;
};
const CurrentUserIsLoggedInMutation = gql`
mutation CurrentUserIsLoggedInMutation($value: Boolean) {
CurrentUserIsLoggedIn(value: $value) #client {
value
}
}
`;
const LoginServerQuery = gql`
query LoginServerQuery {
loggedInUser {
id
}
}
`;
const LoginTestQuery = compose(
graphql(LoginServerQuery, { name: 'LoginServerQuery' }),
graphql(CurrentUserIsLoggedInMutation, {
name: 'CurrentUserIsLoggedInMutation',
}),
)(LoginTest);
export default LoginTestQuery;
At the moment, apollo-link-state requires you to return any result in your resolver function. It can be null too. This might be changed in the future.
const stateLink = withClientState({
cache,
defaults: defaultState,
resolvers: {
Mutation: {
CurrentUserIsLoggedIn: (_, args) => {
const data = {
CurrentUserIsLoggedIn: {
__typename: 'CurrentUserIsLoggedIn',
value: args.value,
},
};
cache.writeData({ data });
return data;
},
},
},
try adding a return statement in your mutation. Similar problem occured here with different function: apollo-link-state cache.writedata results in Missing field warning