It seems like I'm not getting something fundamental with graphql.
I am trying to get a user by it's id which is in turn the result of another query. In this case a query on some session data.
I don't understand why the error occurs.
Here's my code:
{
session(key: "558fd6c627267d737d11e758f1ae48cae71fc9b584e2882926ad5470c88d7c3ace08c9c7") {
userId
expires
user(id: userId) {
name
}
}
}
And I get
Unknown argument "id" on field "user" of type "Session"
My schema looks like this:
type Session {
userId: String,
expires: String,
user: User
}
type User {
_id: String
name: String
email: String
firstName: String
lastName: String
}
type Query {
session(key: String!): Session
user(id: String!): User
}
Addendum Feb 23 2017
I apologize that I wasn't sufficiently explicit about the corresponding resolvers in my initial post. Yes, I my resolvers are defined and e. g. the query works for session if I don't add users.
Here's my root:
{
Query: {
async user(parentValue, args, req) {
let user = await adapters.users.byId(args.id);
return user;
},
async session(parentValue, args, req) {
let session = await adapters.session(args.key);
let userId = session.session.userId;
let expires = session.expires;
return {userId: userId, expires: expires};
}
}
}
You need to create some resolver function on field user in type Session, because GraphQL does not know how to return the user. In graphql-js it would look like that
const Session = new GraphQLObjectType({
name: 'Session',
fields: {
userId: { type: GraphQLString },
expires: { type: GraphQLString },
user: {
type: User, // it is your GraphQL type
resolve: function(obj, args, context){
// obj is the session object returned by ID specified in the query
// it contains attributes userId and expires
// however graphql does not know you want to use the userId in order to retrieve user of this session
// that is why you need to specify the value for field user in type Session
return db.User.findById(obj.userId).then(function(user){
return user;
});
}
}
}
});
It is just a simple example from Node.js to show you how you could return the user instance. You have the possibility of specifying how to retrieve values of every field in each GraphQL type (each field can have it's own resolve function).
I suggest you read the GraphQL documentation concerning root and resolvers
EDIT
I will show you an example how you can deal with such a situation. Let's consider two types: User and Session
type User {
_id: String
name: String
}
type Session {
expires: String
user: User
}
If you want your query to return Session object together with it's User, you do not need to include the userId field in Session type.
You can solve this situation in two ways. First one is to use single resolver for the session query.
{
Query: {
async session(parentValue, args, req) {
let session = await adapters.session(args.key);
// now you have your session object with expires and userId attributes
// in order to return session with user from query, you can simply retrieve user with userId
let user = await adapter.users.byId(session.userId);
return { expires: session.expires, user: user }
}
}
}
This is your first option. With that solution you can return session object with user assigned to it via single query and without extra resolve methods on any fields of Session type. The second solution is the one I have shown previously, where you use resolve method on field user of Session type - so you simply tell GraphQL how it should obtain the value for this field.
Related
I would like to restrict my GraphQL API with User Authentication and Authorization.
All Keystone.JS documentation is talking about AdminUI authentication, which I'm not interested in at the moment.
Facts:
I want to have some social logins (no basic email/password)
I want to use JWT Bearer Tokens
Other than that you can suggest any possible way to achieve this.
My thoughts were:
I could have Firebase Authentication (which can use Google Sign-in, Apple Sign-in etc.) be done on the client-side (frontend) which would then upon successful authentication somehow connect this to my API and register user (?).
Firebase client SDK would also fetch tokens which I could validate on the server-side (?)
What is troubling is that I can't figure out how to do this in a GraphQL environment, and much less in a Keystone-wrapped GraphQL environment.
How does anyone do basic social authentication for their API made in Keystone?
Keystone authentication is independent of the Admin-UI. If you are not restricting your list with proper access control the authentication is useless. Default access is that it is open to all.
you can set default authentication at keystone level which is merged with the access control at list level.
Admin Ui Authentication
Admin UI only supports password authentication, meaning you can not go to /admin/signin page and authenticate there using other authentication mechanism. The Admin Ui is using cookie authentication. cookies are also set when you login using any other login method outside of admin-ui. This means that you can use any means of authentication outside of admin-ui and come back to admin ui and you will find yourself signed in.
Social Authentication:
Social authentication is done using passportjs and auth-passport package. there is documentation to make this work. Single Step Account Creation example is when you create user from social auth automatically without needing extra information (default is name and email). Multi Step Account Creation is when you want to capture more information like preferred username, have them accept the EULA or prompt for birthdate or gender etc.
JWT
I dont believe Keystone does pure JWT, all they do is set keystone object id in the cookie or the token is a signed version of item id (user item id) which can be decrypted only by the internal session manager using cookie secret.
Using Firebase to authenticate user
this is the flow of authentication after you create a custom mutation in keystone graphql.
client -> authenticate with Firebase -> get token -> send token to server -> server verifies the token with firebase using admin sdk -> authenticate existing user by finding the firebase id -> or create (single step) a user or reject auth call (multi step) and let client send more data like age, gender etc. and then create the user -> send token
here is the example of phone auth I did, you can also use passport based firebase package and implement your own solution.
keystone.extendGraphQLSchema({
mutations: [
{
schema: 'authenticateWithFirebase(token: String!): authenticateUserOutput',
resolver: async (obj, { token: fireToken }, context) => {
const now = Date.now();
const firebaseToken = await firebase.auth().verifyIdToken(fireToken);
const { uid, phone_number: phone } = firebaseToken;
const { errors, data } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
query findUserFromId($phone: String!, $uid: String!) {
firebaseUser: allUsers(where: { phone: $phone, firebaseId:$uid }) {
id
name
phone
firebaseId
}
}`,
variables: { phone, uid },
});
if (errors || !data.firebaseUser || !data.firebaseUser.length) {
console.error(errors, `Unable to find user-authenticate`);
throw errors || new Error('unknown_user');
}
const item = data.firebaseUser[0];
const token = await context.startAuthedSession({ item, list: { key: 'User' } });
return { item, token };
},
},
{
schema: 'signupWithFirebase(token: String!, name: String!, email: String): authenticateUserOutput',
resolver: async (obj, { token: fireToken, name, email }, context) => {
const firebaseToken = await firebase.auth().verifyIdToken(fireToken);
const { uid, phone_number: phone } = firebaseToken;
const { errors, data } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
query findUserFromId($phone: String!, $uid: String!) {
firebaseUser: allUsers(where: { phone: $phone, firebaseId:$uid }) {
id
name
phone
firebaseId
}
}`,
variables: { phone, uid },
});
if (errors) {
throw errors;
}
if (data.firebaseUser && data.firebaseUser.length) {
throw new Error('User already exist');
}
const { errors: signupErrors, data: signupData } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
mutation createUser($data: UserCreateInput){
user: createUser(data: $data) {
id
name
firebaseId
email
phone
}
}`,
variables: { data: { name, phone: phone, firebaseId: uid, email, wallet: { create: { walletId: generateWalletId() } }, cart: { create: { lineItems: { disconnectAll: true } } } } },
});
if (signupErrors || !signupData.user) {
throw signupErrors ? signupErrors.message : 'error creating user';
}
const item = signupData.user;
const token = await context.startAuthedSession({ item, list: { key: 'User' } });
return { item, token };
},
},
],
})
I'm writing some client-side customisations for a model driven app and I need to update an entry in an N:N relationship. I have been following the documentation here. I can read from the relationship table, but trying to update an entry or create a new entry is failing.
The N:N relationship is between a custom entity new_lastask and the built-in systemuser entity. The name generated for the N:N relationship is new_new_lastask_systemuser. I have managed to create and update other records, and I understand that you need to use the Schema name with the exact casing, not the name of the field and also the #odata.bind syntax when updating a lookup field, but I can't figure out what the name of the field should be.
The example code below tries to find a N:N record with a given user and switch it for another user, I have given an example with the updateRecord method, but I have tried with createRecord too and I get the same error.
// taskId is the Guid of the custom task entity, and userId is the guid of the user
var query = "?$filter=new_lastaskid eq " + taskId;
Xrm.WebApi.retrieveMultipleRecords("new_new_lastask_systemuser", query).then(
function success(result) {
for (var i = 0; i < result.entities.length; i++) {
if (result.entities[i].systemuserid===oldUserId) {
// found the offering user, replace them
var data = {
"systemuserid#odata.bind": "/systemusers" + newUserId
}
// try to just change the value of the user attached to the N:N record
Xrm.WebApi.updateRecord(tableName, result.entities[i].new_new_lastask_systemuserid, data).then(
function success(result) {
console.log("Successfully updated");
// perform operations on record update
},
function (error) {
console.log(error.message);
// An error occurred while validating input parameters: Microsoft.OData.ODataException:
// An undeclared property 'systemuserid' which only has property annotations in the payload...
}
);
}
}
},
function (error) {
console.log(error.message);
// handle error conditions
}
);
This is the same error I was getting when trying to update any lookup field on my entity when I was trying to use the name instead of the Schema Name, so I thought it might be related to the capitalisation of the field, so I have tried many variations (SystemUserId, User, SystemUser, systemuser, user, userid) and nothing works.
What should the Schema name be of the lookup field on my N:N table, or am I going about handling modifications to these the wrong way via this API?
I just could not get it to work via the Client API, so i just made the call to the web api using fetch, following the document from the 8.2 api here:
fetch('https://yoururl.crm.dynamics.com/api/data/v9.0/new_lastasks' + taskId + "/new_new_lastask_licensee/$ref", {
method: 'post',
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0"
},
body: JSON.stringify(
{"#odata.id": "https://yoururl.crm.dynamics.com/api/data/v9.0/systemusers" + currentUser }
)
}).then(function(response) {
console.log(response)
})
So, if I am testing pages in a vacuum without much interaction with the backend, it works great. I am having issues with actually interacting with my UI if it hits any type of service. Basically, nothing is Auth'd. I try programmatically setCookie, no dice. I try to read the cookie, nope. Btw, my whole site requires a login.
cy.setCookie('sess', ';askjdfa;skdjfa;skdjfa;skdjfa;skfjd');<-- does not work
cy.getCookie('sess').should('exist') <-- does not work
I am having an issue on really the best way to "test" this. For example, I have an account section that a user can "update" their personals. I try, fill out the form (via UI testing), but the submission is rejected, no Auth. EVEN THOUGH I just logged in (via UI testing). - I know I need to remove that since it is bad practice to UI-Login for every section of my site.
So, I don't know how to stub graphql calls with cy.request(). Here is my mutation.
mutation Login($email: Email!, $password: String!) {
login(email: $email, password: $password) {
userID
firstName
lastName
}
}
Right now, I am importing the login spec for each section of the site i am testing. I know this is an anti-pattern. Like to solve this problem.
My AUTH (cookie) is not being set. Even when I try to set it, programmatically, doesn't work.
Maybe I should just stub out my graphql mutations? How?
Lastly, IF I am stubbing out my graphql mututations, how do I update the session ( via my main session query ). If I can get these mutations to work, then refreshing the page will get my my updated data, so I'm not completely needing this part.
Any ideas?
I didn't do the stub and all those, as you were asking how the mutation would work with cy.request in my other post. I did it this way and it just basically works. Hopefully this would help
I created a const first though
export const join_graphQL = (query, extra={}) => {
return `mutation {
${query}(join: { email: "${extra.email}", id: "${extra.id}" }) {
id, name, email
}
}`
};
request config const
export const graphqlReqConfig = (body={}, api=graphQlapi, method='POST') => {
return {
method,
body,
url: api,
failOnStatusCode: false
}
};
mutation query with cy.request
const mutationQuery = join_graphQL('mutationName', {
email: "email",
id: 38293
});
cy.request(graphqlReqConfig({
query: mutationQuery
})).then((res) => {
const data = res.body.data['mutationName']; // your result
});
hopefully it's not too messy to see.
basically the fields need to be string such as "${extra.email}" else it will give you error. Not sure how the graphql works deeply but if I just do ${extra.email} I would get an error which I forgot what error it was.
Here's a simpler way of handling a mutation with cy.request
const mutation = `
mutation {
updateUser(id: 1, firstName: "test") {
firstName
lastName
id
role
}
}`
cy.request({
url: url,
method: 'POST',
body: { query: mutation },
headers: {
Authorization: `Bearer ${token}`,
},
})
I want to add more details to the access token which is generated on calling the user login method. I have extended the builtin user model with my own Customer model. My login method looks like below:
getToken(userData, data, callback) {
const Customer = app.models.Customer;
Customer.login(
{ email: userData.email, password: userData.password },
function (error, accessToken) {
if (error) {
callback(error);
} else {
callback(null, accessToken);
}
}
)
}
I want to add data to the accessToken on login. How do I achieve that ? Basically I want to add more information to accessToken other than userId. I also want to retain userId in the token.
Thanks
Using Passportjs with Sails for user authentication on a JSON body (no query/form parameters).
The userObject is a bit more complex than just { "username": username, "password": password }. It is a tree and, among other data, contains a tenancy key, to be used on a multitenancy system.
{
"userObj" : {
"tenant" : id,
"email" : email,
"password" : password
}
Since Passport only accepts two parameters, username and password, I tried "cheating" on Passport configuration on the custom middleware:
passport.use(new LocalStrategy(
{
usernameField: 'customUserData',
passwordField: 'password'
},
function(usernameObject, password, done) {
var actualUser = usernameObject.email;
var tenant = usernameObject.tenant;
....
}
)
);
And during the actual authentication call, I rearranged:
req.body is JSON
req.body.customUserData = {};
req.body.customUserData.email= req.body.userObj.email;
req.body.customUserData.tenant = req.body.userObj.tenant;
req.body.password = req.body.userObj.password;
and then the authentication call:
passport.authenticate('local', function(err, user, info){ ... });
My expectation was that Passport would:
parse the JSON payload, grabbing customUserData
pass customerData as first parameter in the function (usernameObject)
allow me to get tenant and email for further processing
But this doesn't seem to be happening. Before I try debuggin other parts of the code, I need to know if this approach would theoretically work.
Or else, how would you configure and pass username + tenancy to Passport?