I have an Apollo GraphQL client running in react native. It connects to a lambda instance running graphQL. My problem is that I am trying to send a mutate request to the server (have not setup queries yet), and the server is getting the following and declaring a syntax error(Expected Name, found String \"operationName\").
When I was testing the graphQL server, the requests looked like the ones specified below. Is Apollo Client not formatting the requests properly (if so why not) or is it functioning as intended?
Body sent from Apollo client to graphQL lambda:
{
"operationName": "createUser",
"variables": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane#doe.com",
"username": "jane_doe.com",
"provider": "none"
"jwt": "none"
},
"query": "mutation createUser($firstName: String!, $lastName: String!, $email: String!, $username: String!, $provider: String, $jwt: String!) {
createUser(firstName: $firstName, lastName: $lastName, email: $email, username: $username, provider: $provider, jwt: $jwt) {
createdAt
__typename
}
}"}
A Normal request that works from Postman.
mutation {
createUser(firstName: "Jane", lastName: "Doe", email: "jane#doe.com", username: "jane_doe.com", jwt: "none", provider: "none") {
firstName
}
}
Code from react-native app
// The mutation in the render function
<Mutation mutation={createUserMutation}>
{(createUser, error) => {
console.log('error-----------', error);
// If there is an error throw the error
if (error) {
console.log('error----------', error);
}
if (createUser) {
// If the response has data load the response data via the createPlayer property.
return (
<LoginButton
onPress={() => {
this.signIn(createUser);
}}
/>
);
}
// By default it is loading the result so just return loading...
return <Text>Loading...</Text>;
}}
</Mutation>
// The signin function called when the user presses the login button
async signIn(createUser) {
...
try {
Auth.signIn(un, password)
.then(data => {
...
this.createUserFunc(
createUser,
'Jane',
'Doe',
'jane#doe.com',
'jane_doe.com',
'none',
'none'
);
}
...
}
// The create user function called from the signin function
createUserFunc = (func, firstName, lastName, email, username, provider, jwt) => {
const newUser = {
firstName,
lastName,
email,
username,
provider,
jwt,
};
func({variables: newUser});
};
// The gql syntax mutation
const createUserMutation = gql`
mutation createUser(
$firstName: String!
$lastName: String!
$email: String!
$username: String!
$provider: String
$jwt: String!
) {
createUser(
firstName: $firstName
lastName: $lastName
email: $email
username: $username
provider: $provider
jwt: $jwt
) {
createdAt
}
}
`;
Most GraphQL servers that accept requests over HTTP are listening to two different types of content (indicated with the Content-Type header): application/graphql and application/json. You server seems to only listen to requests with a application/graphql body.
The problem with Content-Type: application/graphql is that GraphQL execution consist out of up to three parameters that can be supplied by the client:
The query (required)
The variable values of the query
The operation name
This enables query documents to be entirely static. But if the content of the request is only the GraphQL query, the other parameters need to go somewhere else. In theory they could be supplied as GET parameters but usually all clients use the JSON format to supply all three as outlined here.
As Daniel has pointed out you can use a GraphQL server implementation for your framework/technology of choice to handle that for you.
Alternatively you would have to react to the header of the request yourself (which could be a good exercise but you are probably going to miss an edge case that the library authors have thought of).
Related
I am trying to login to a Keystone 5 GraphQL API. I have setup the app so that I can login via the Admin Console, but I want to login from a Svelte application.
I keep finding references to the code below (I am new to GraphQL) but don't know how to use it.
mutation signin($identity: String, $secret: String) {
authenticate: authenticateUserWithPassword(email: $identity, password: $secret) {
item {
id
}
}
}
If I post that query "as is" I get an authentication error, so I must be hitting the correct endpoint.
If I change the code to include my username and password
mutation signin("myusername", "mypassword") {
authenticate: authenticateUserWithPassword(email: $identity, password: $secret) {
item {
id
}
}
}
I get a bad request error.
Can anyone tell me how I send username/password credentials correctly in order to log in.
The full code I am sending is this
import { onMount } from 'svelte';
let users = [];
onMount(() => {
fetch("http://localhost:4000/admin/api", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `mutation signin($identity: String, $secret: String) {
authenticate: authenticateUserWithPassword(email: $identity, password: $secret) {
item {
id
}
}
}`
})
}).then(res => res.json())
.then(data => {
console.log(data)
})
})
Here is the response I get
{"errors":[{"message":"[passwordAuth:failure] Authentication failed","locations":[{"line":2,"column":3}],"path":["authenticate"],"extensions":{"code":"INTERNAL_SERVER_ERROR","exception":{"stacktrace":["Error: [passwordAuth:failure] Authentication failed"," at ListAuthProvider._authenticateMutation (/Users/simon/development/projects/keystone/meetings-api/node_modules/#keystonejs/keystone/lib/providers/listAuth.js:250:13)"]}},"uid":"ckwqtreql0016z9sl2s81af6w","name":"GraphQLError"}],"data":{"authenticate":null},"extensions":{"tracing":{"version":1,"startTime":"2021-12-03T20:13:44.762Z","endTime":"2021-12-03T20:13:44.926Z","duration":164684813,"execution":{"resolvers":[{"path":["authenticate"],"parentType":"Mutation","fieldName":"authenticateUserWithPassword","returnType":"authenticateUserOutput","startOffset":2469132,"duration":159500839}]}}}}
I found the answer eventually.
You have to provide an extra object in your body called variables
variables: {
var1: "value1",
var2: "value2"
}
Those variables will then replace the placehodlers in the query like $var1 or $var2
Here is the full fetch code that works.
fetch("http://localhost:4000/admin/api", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `mutation signin($identity: String, $secret: String) {
authenticate: authenticateUserWithPassword(email: $identity, password: $secret) {
item {
id
}
}
}`,
variables: {
identity: "myusername",
secret: "mypassword"
}
})
}).then(res => res.json())
.then(data => {
console.log(data)
})
It's a shame that KeystoneJS don't provide any full code examples in their documentation. It would have saved me hours of searching.
As you say #PrestonDocks, if your query defines variables, you need to supply the variable values in a separate top level object. For the benefit of others I'll link to the GraphQL docs on this.
The alternative is to not use variables and to in-line your values in the query itself, like this:
mutation signin {
authenticate: authenticateUserWithPassword(
email: "me#example.com",
password: "Reindeer Flotilla"
) {
item {
id
}
}
}
But variables usually end up being neater.
I'm trying to test login page of my react app using testcafe. After success login app should fetch user from API (on localhost:5000) and display name & surname on the home page.
Fetching user in my app:
const raw = await axios.get(`http://localhost:5000/api/user/${uid}`);
tests/login.test.ts
// Used to mock POST /login-user
const loginMock = RequestMock()
.onRequestTo("http://localhost:5000/api/login-user")
.respond(
{
success: true,
id: "123456789101",
},
200,
{ "Access-Control-Allow-Origin": "*" }
);
// Used to mock GET /user/:id
const getUserMock = RequestMock()
.onRequestTo("http://localhost:5000/api/user/:id")
.respond(
{
success: true,
user: {
name: "John",
surname: "McBain",
},
},
200,
{ "Access-Control-Allow-Origin": "*" }
);
test.requestHooks([loginMock, getUserMock])("should login user", async t => {
/* login user testing*/
...
// Check GET /user
const UserInfo = await Selector(".auth span");
await browser
.expect(await UserInfo.textContent)
.eql("Account: John McBain"); // Failed here "Account undefined undefined" != "Account John McBain"
});
POST /login-user request is working fine, but GET /user/:id request is failed
I think the problem is that I'm incorrectly write dynamic url (/:id/). How can I do it correctly?
My express server function for GET /user/:id here:
app.get("/api/user/:id", (req, res) => {
const userId = req.params.id;
...
});
In the case of dynamic URLs you should use RegExp instead of string in your RequestMock.onRequestTo method. The result RegExp relates to your id possible symbols, for example:
const mock = RequestMock()
.onRequestTo(/\/id=[0-9]+$/)
.respond(/*...*/);
Result:
[respond is replaced by mock] https://example.com/id=13234
[respond is not replaced by mock] https://example.com/id=23443-wrong-part
[respond is not replaced by mock] https://example.com/id=wrong-non-number-id
Apollo-client's query:
apolloClient
.query({
query: authQuery,
variables: {
login: payload.login,
password: payload.password,
},
})
.then((res) => console.log(res)
Contents of authQuery (it's inside a file.gql):
query auth {
data
}
i always get the following response:
{
data: null
loading: false
networkStatus: 7
stale: true
}
Though in graphiQL i am getting correct response:
Query in a GraphiQL
{
auth(login:"root#admin", password:"1234")
}
And response:
{
"data": {
"auth": "eyJhbGciOi8"
}
}
I suspect my file.gql is the culprit? Or variables inside query not being read?
I had to wrap my query this way:
query Auth($login: String!, $password: String!) {
auth (login: $login, password: $password)
}
When you're using params with your Query, you always have to wrap it this way.
I'm starting to work on an express API using graphql with apollo-server-express and graphql-tools. My register user process steps are:
User submit user name, email and password.
Server send an email to user by Mailgun with unique link generated by uuid.
User follow the link to verify the registration.
But I'm in struggle at how to bind the mutation in the resolver. See snippets:
server.js
const buildOptions = async (req, res, done) => {
const user = await authenticate(req, mongo.Users)
return {
schema,
context: {
dataloaders: buildDataloaders(mongo),
mongo,
user
},
}
done()
}
// JWT setting
app.use('/graphAPI',
jwt({
secret: JWT_SECRET,
credentialsRequired: false,
}),
graphqlExpress(buildOptions),
res => data => res.send(JSON.stringify(data))
)
Mutation on resolver
signupUser: async (root, data, {mongo: { Users }}) => {
// Check existed accounts,
// if account is not exist, assign new account
const existed = await Users.findOne({email: data.email})
if (!existed) {
// create a token for sending email
const registrationToken = {
token: uuid.v4(),
created_at: new Date(),
expireAfterSeconds: 3600000 * 6 // half day
}
const newUser = {
name: data.name,
email: data.email,
password: await bcrypt.hash(data.password, 10),
created_at: new Date(),
verification_token: registrationToken,
is_verified: false,
}
const response = await Users.insert(newUser)
// send and email to user
await verifyEmail(newUser)
return Object.assign({id: response.insertedIds[0]}, newUser)
}
// Throw error when account existed
const error = new Error('Email existed')
error.status = 409
throw error
},
// VERIFY USER
// Set verify to true (after user click on the link)
// Add user to mailist
verifiedUser: async (root, data, {mongo: { Users }}) => {
await Users.updateOne(
{ email: data.email },
{
set: {is_verified: true},
unset: {verification_token: {token: ''}}
}
)
},
route config
routes.get('/verify?:token', (req, res, next) => {
res.render('verified', {title: 'Success'})
})
the route config is where I stuck, because the object is passed to all resolvers via the context inside graphqlExpress
Any one help me out or suggest for me any articles related. Thanks so much.
You will need 3 graphql endpoints and 1 apollo http endpoint for proper workflow.
Optionally you can combine 3 graphql endpoints in one, but then it will be a one big function with a lot of different responsibilities.
1# graphql endpoint: changepass-request
expects email param
check if user with such email found in db:
generate code
save it in the local account node
send code to the user email with http link to confirm code:
http://yoursite.com/auth/verify?code=1234
return redirect_uri: http://yoursite.com/auth/confirm-code
for UI page with prompt for confirmation code
2# graphql endpoint: changepass-confirm
expects code param:
if user with such code found in db, return redirect_uri to UI page with prompt for new pass with confirmation code in params: http://yoursite.com/auth/change-pass?code=1234
3# graphql endpoint: changepass-complete
expects code and new pass:
hash new password
search in db for local account with such code
3a. if not found:
return error with redirect_uri to login page:
http://yoursite.com/auth?success=false&message="Confirmation code is not correct, try again."
3b. if found:
change password for new, return success status with redirect_uri to login page:
http://yoursite.com/auth?success=true&message="ok"
4# apollo HTTP endpoint: http://yoursite.com/auth/verify?code=1234
if no code provided:
redirect to UI registration page with error message in params:
http://yoursite.com/auth?success=false&message="Confirmation code is not correct, try again."
if code provided: search in db for local account with such code
1a. if user not found:
redirect to reg ui with err mess in params:
http://yoursite.com/auth?success=false&message="Confirmation code is not correct, try again."
1.b if user found:
redirect to ui page with new password prompt and attach new code to params
I didn't put any code above, so you can use this workflow in other auth scenarios.
It seems like rather than utilizing the verifiedUser endpoint, it would be simpler to just keep that logic inside the controller for the /verify route. Something like:
routes.get('/verify?:token', (req, res) => {
Users.updateOne(
{ verification_token: { token } },
{
$set: {is_verified: true},
$unset: {verification_token: {token: ''}}
},
(err, data) => {
const status = err ? 'Failure' : 'Success'
res.render('verified', {title: status})
}
)
})
Having trouble doing this - is it even possible?
Sign-up Email Verification is off, and I'm doing this in the config:
BackandProvider.setAppName( 'test' );
BackandProvider.runSigninAfterSignup( true );
// ... tokens, etc.
Getting this back in the response after hitting the /1/user/signup endpoint:
data : {
currentStatus : 1,
listOfPossibleStatus : [...],
message : "The user is ready to sign in",
token : "...",
username : "tester#test.com"
}
Do I need to make another API call? Can't find where and with which params.
Yes, you must make another API call to get token after sign up. If you use the Backand SDK by default it makes the second call.
$scope.signup = function (form) {
return Backand.signup(form.firstName, form.lastName,
form.username, form.password,
form.password,
{company: form.company})
.then(function (response) {
$scope.getUserDetails();
return response;
});
};
If you lool at the SDK code, this is what happens there:
self.signup = function (firstName, lastName, email, password, confirmPassword, parameters) {
return http({
method: 'POST',
url: config.apiUrl + urls.signup,
headers: {
'SignUpToken': config.signUpToken
},
data: {
firstName: firstName,
lastName: lastName,
email: email,
password: password,
confirmPassword: confirmPassword,
parameters: parameters
}
}).then(function (response) {
$rootScope.$broadcast(EVENTS.SIGNUP);
if (config.runSigninAfterSignup
&& response.data.currentStatus === 1) {
return self.signin(email, password);
}
return response;
})
};