How to connect list of objects to node in Relay - node.js

In GraphQL, I have the following structure
# represents any node with GlobalIdField
node(id: "uniqueIdOfNode") {
}
# represents actually logged user
viewer {
}
In sample MVC app schema todos are connected to the viewer so when querying you can get only those todos that belong to logged user.
However, in my case, I want to display data which is not related to a user. Let's say data is type Country which is NodeInterfaceType and I want to make a query where I ask for the list of countries. So in Relay, I can make fragment on CountryList where I get a country and pass it to React component using Relay.
If what I've written is not clear enough let me know because I'm confused with it and I'm not sure if I explained it well.
I've written GraphQL server in PHP but the code or hints can be written in node.js and I'll understand it as well.
How to do it?
Update
Some code:
schema.graphql
# A country
type Country implements NodeInterface {
# The ID of an object
id: ID!
countryId: Int
phonePrefix: String
name: String
timezone: String
}
# Representation of date and time in "Y-m-d H:i:s" format
scalar DateTime
interface NodeInterface {
# The ID of an object
id: ID!
}
type Query {
# Fetches an object given its ID
node(
# The ID of an object
id: ID!
): NodeInterface
viewer: User
countries: [Country]
}
type User implements NodeInterface {
# The ID of an object
id: ID!
username: String
userId: Int
lastLoginDateTime: DateTime
}
SampleRoute.js -used as the first route when someone opens React app
import Relay from 'react-relay';
export default class extends Relay.Route {
static queries = {
countries: () => Relay.QL`
query AppHomeRoute {
viewer
}
`,
};
static routeName = 'AppHomeRoute';
}
App.js I want to have countries list in Index component, Ideally in App js I don't want to event pass countries with props but I want them to be in Index component which will be renamed to CountryList in the future
import React, { Component } from 'react';
import Relay from 'react-relay';
import Index from './page/index';
import './App.css';
class App extends Component {
render() {
console.log(this.props);
return (
<Index country={this.props.countries} />
);
}
}
export default Relay.createContainer(App, {
fragments: {
countries: () => Relay.QL`
countries {
${Index.getFragment('country')}
}
}
// fragment F1 on Country {
// ${Index.getFragment('country')}
// },
`
}
});
Now I get an error:
SampleRoute.js:5 Uncaught Error: GraphQL validation error ``Cannot query field "viewer" on type "Query"
which I completely don't understand because when I query the server with graphiql then it works okay.
Query:
{
viewer {
id
username
}
node(id:"dXNlcjo2NjY=") {
id
username
}
countries {
name
}
}
response:
{
"data": {
"viewer": {
"id": "dXNlcjo2NjY=",
"username": "Robert"
},
"node": {
"id": "dXNlcjo2NjY=",
"username": "Robert"
},
"countries": [
{
"name": "AFGHANISTAN"
},
{
"name": "ALBANIA"
}, (...)
]
}
}

Related

Conditionally serialize the response attributes

I want to return a different response from a Game controller depending on whether or not a User owns the game or is simply invited to it. Essentially: filter out certain attributes from the response if the user is only invited to the game.
Here's a naive implementation of what I want using two different controllers:
#SerializeOptions({
groups: ['invited'],
})
#Get(':id')
async findOne(#User() user, #Param('id') id: string) {
const retGame = await this.gamesService.findOne(id);
const ability = this.caslAbilityFactory.createForUser(user);
Throw401(ability.can(Action.Read, retGame));
return retGame;
}
#SerializeOptions({
groups: ['owner'],
})
#Get('full/:id')
async findOneFull(#User() user, #Param('id') id: string) {
const retGame = await this.gamesService.findOne(id);
const ability = this.caslAbilityFactory.createForUser(user);
Throw401(ability.can(Action.FullRead, retGame));
// the main difference is ^^^^^^^^, using a different CASL rule for authorization
return retGame;
}
I'm using a different set of CASL rules to allow a "full read" or not, the full read being only allowed for the game owner. That way I can attach a different group through the SerializeOptions decorator, which allows me to conditionally expose an entity attribute:
#Column()
#Expose({ groups: ['owner'] })
inviteKey: string;
But it feels wrong to use different routes and methods to basically do the same thing, I'd like to pass a dynamic condition (user.id === ownerId) instead of a group to the Expose decorator, and I believe the best next thing would be to use an interceptor to filter certain fields from the response. I'm not sure how to proceed from there, would an interceptor be the right approach?
I have stumbled upon the same problem, where it makes sense to have one route but based on user rights give the result with some attributes filtered out from the response. Disclaimer: Since I'm fairly new to nestjs there are some gaps in my knowledge and it might be possible that it is not a proper way to achieve the intended result.
To achieve this you'd have to:
Use your already defined class with exposed/excluded attributes - using class-transformer and groups should add/remove attributes from the class when transforming to json object
Create an interceptor - this will intercept the response before it gets sent to clinet
Use the interceptor for desired routes
So in your case, it could look something like this:
db/models/game.model.ts
import { Exclude, Expose } from 'class-transformer';
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
#Entity()
export class Game {
#Exponse()
#PrimaryGeneratedColumn()
id: number;
#Column()
#Expose({ groups: ['owner'] })
inviteKey: string;
#Column()
#Expose({ groups: ['owner', 'invited']})
name: string;
}
transform.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '#nestjs/common';
import { classToPlain } from 'class-transformer';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Game } from 'db/models/game.model';
export interface Response<T> {
data: T;
}
#Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
// This is how you can get access to requesting user (asuming it is in the request)
const request = context.switchToHttp().getRequest();
const user = request.user;
/* get your group based on the CASL rules. */
const group = 'invited';
// Transform the data (filter out private fields)
return next.handle().pipe(map((data) => {
if (data instanceof Game) {
return classToPlain(game, { groups: [group] });
}
// in case there is Game[]
if (
Array.isArray(data)
&& data[0] instanceof Game
) {
return data.map(game => classToPlain(game, { groups: [group] }));
}
// in case response is something else, don't touch it
return data;
}));
}
}
games.controller.ts
// Use the interceptor here - you no longer need `/games/full/:id` route
#UseInterceptors(TransformInterceptor)
#Get(':id')
async findOne(#User() user, #Param('id') id: string) {
// Return the Game instance here, interceptor will take care of transforming it
return this.gamesService.findOne(id);
}
As a result to your GET /games/:id as user with Action.Read access the output should be
{
"id": 1,
"name": "Best game in the business"
}
As a result to your GET /games/:id as user with Action.FullRead access the output should be
{
"id": 1,
"inviteKey": "super-secret-invite-key",
"name": "Best game in the business"
}
Hopefully this helps.

Node.JS: How do you validate QueryParams in routing-controllers?

lets say you have an interface like this:
import { Get, QueryParam } from 'routing-controllers';
// ...
#Get('/students')
async getStudents(
#QueryParam('count') count?: number,
): Promise<void> {
console.log(count);
}
How do you ensure count is an int and not a float, for example? Something like this is not valid:
#IsInt() #QueryParam('count') count?: number,
IsInt can only be used on a class property, eg for a body model, not for a single parameter value. But according to. this https://github.com/typestack/routing-controllers#auto-validating-action-params it is possible:
This technique works not only with #Body but also with #Param,
#QueryParam, #BodyParam and other decorators.
I had missed this in the docs: https://github.com/typestack/routing-controllers#inject-query-parameters By injecting all of the QueryParams instead of individual QueryParam, you can validate them as a class model:
enum Roles {
Admin = "admin",
User = "user",
Guest = "guest",
}
class GetUsersQuery {
#IsPositive()
limit: number;
#IsAlpha()
city: string;
#IsEnum(Roles)
role: Roles;
#IsBoolean()
isActive: boolean;
}
#Get("/users")
getUsers(#QueryParams() query: GetUsersQuery) {
// here you can access query.role, query.limit
// and others valid query parameters
}
Also, make sure you don't use barrel-imports to import Enums, or open-api-generator will produce an error that the enum is undefined and not and object; eg avoid this: import { Roles } from '../..'

Django Graphene writing mutations with multiple layers of nested foreign keys

How do you write the schema and query for nested foreign keys? I checked the docs and found no examples of how to do this. So here was my attempt based on github and stackoverflow answers lets say I have these models:
class Address(models.Model):
name = models.CharField()
class Person(models.Model):
name = models.CharField()
address = models.ForeignKey('Address', on_delete=models.CASCADE, blank=False, null=False)
class Blog(models.Model):
person = models.ForeignKey('Person', on_delete=models.CASCADE, blank=False, null=False)
text = models.TextField()
I tried writing a schema like this:
class AddressInput(graphene.InputObjectType):
name = graphene.String(required=True)
class PersonInput(graphene.InputObjectType):
name = graphene.String(required=True)
address =graphene.Field(AddressInput)
class CreateNewBlog(graphene.Mutation):
blog=graphene.Field(BlogType)
class Arguments:
address_data = AddressInput()
person_data = PersonInput()
text = graphene.String()
#staticmethod
def mutate(root, info, person_data=None, address_data=None, **input):
address = Address.objects.create(name=address_data.name)
person = Person.objects.create(address=address, name=person_data.name)
blog = Blog.objects.create(person =person, text=input['text'])
blog.save()
return CreateNewBlog(blog=blog)
and I used a query like this:
mutation {
CreateNewBlog(person: { address: {name: "aaa"},
name: "First Last" }, text: "hi hi") {
Blog {
person{
name
address{
name
}
},
text
}
}
}
I got this error message:
{
"errors": [
{
"message": "'NoneType' object has no attribute 'name'",
"locations": [
{
"line": 32,
"column": 9
}
],
"path": [
"CreateNewBlog"
]
}
],
"data": {
"CreateNewBlog": null
}
}
I think the issue is in the way I wrote the schema.py file. Where it does not work to nest InputFields inside another InputField. Is there any other ways to write a single mutation?
Okay, a few things here. Firstly, you should generate your schema.graphql file, because that'll show you the actual final shape of the schema being built by Graphene, which would've made your debugging easier. Or you could use GraphiQL to test out your queries and lets its documentation and autocomplete do the heavy lifting for you.
But on to the specifics, your Graphene mutation definition is going to be generating a mutation that looks like this:
input AddressInput {
name: String!
}
input PersonInput {
name: String!
address: AddressInput
}
type CreateNewBlogOutput {
blog: Blog
}
type Mutation {
CreateNewBlog(addressData: AddressInput, personData: PersonInput, text: String): CreateNewBlogOutput!
}
Worth noting that there are two ways for you to supply an AddressInput here, one at root, and one inside PersonInput. This probably isn't what you're intending to do. Secondly, none of the root arguments are required, which is contributing to your error message being fairly unhelpful, because the problem is you're calling the mutation incorrect parameters but the query validator is letting it through because your types are very permissive.
I believe that if you were to run the mutation like the following, it'd actually work:
mutation {
CreateNewBlog(
personData: {
address: {
name: "aaa"
},
name: "First Last"
},
text: "hi hi"
) {
blog {
person {
name
address {
name
}
}
text
}
}
}
I only made two changes here, person was changed to personData (to match your mutation definition, Graphene does the conversation from snake case to camel case automatically), and Blog to blog in the field selection.
But lets go a little further, here's how I would have made the mutation.
class AddressInput(graphene.InputObjectType):
name = graphene.String(required=True)
class PersonInput(graphene.InputObjectType):
name = graphene.String(required=True)
address = AddressInput(required=True)
class CreateNewBlogInput(graphene.InputObjectType):
person = PersonInput(required=True)
text = graphene.String(required=True)
class CreateNewBlogPayload(graphene.ObjectType):
blog = graphene.Field(BlogType, required=True)
class CreateNewBlog(graphene.Mutation):
class Arguments:
input_data = CreateNewBlogInput(required=True, name="input")
Output = CreateNewBlogPayload
#staticmethod
def mutate(root, info, input_data):
address = Address.objects.create(name=input_data.person.address.name)
person = Person.objects.create(address=address, name=input_data.person.name)
blog = Blog.objects.create(person=person, text=input_data.text)
blog.save()
return CreateNewBlogPayload(blog=blog)
I'd also change CreateNewBlog to createNewBlog when constructing Graphene's mutation object, because the GraphQL convention is to use lower camel case for mutations.
Then you'd run it like this:
mutation {
createNewBlog(
input: {
person: {
address: {
name: "aaa"
},
name: "First Last"
}
text: "hi hi"
}
) {
blog {
person {
name
address {
name
}
}
text
}
}
}
Why wrap the entire input in a single input field? Mainly because it makes calling the mutation easier in the client when using variables, you can just provide single input arg of the correct shape rather than multiple.
// So instead of this
mutation OldCreateNewBlog($person: PersonInput, $text: String) {
createNewBlog(
personData: $person
text: $text
) {
blog {
person {
name
address {
name
}
}
text
}
}
}
// You have this
mutation NewCreateNewBlog($input: CreateNewBlogInput!) {
createNewBlog(
input: $input
) {
blog {
person {
name
address {
name
}
}
text
}
}
}
The latter makes it easier to change the input shape over time and only have to make the change in one place in client code.

Wrong data from client passes GraphQL validation

I've made simple CRUD app with React and Apollo client on NestJS server with GraphQL API.
I have this simple Mutations:
schema.gql:
type Mutation {
createUser(input: CreateUserInput!): User! // CreateUserInput type you can see in user.input.ts below
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): User!
}
user.input.ts:
import { InputType, Field } from "#nestjs/graphql";
import { EmailScalar } from "../email.scalar-type";
#InputType()
export class CreateUserInput {
// EmailScalar is a custom Scalar GraphQL Type that i took from the internet and it worked well
#Field(() => EmailScalar)
readonly email: string;
#Field()
readonly name: string;
}
"EmailScalar" type checks if "email" input has *#*.* format basically
And when i make createUser Query to GraphQL API like this:
It cannot pass validation
(because Email type works fine)
But when Query sent from client - it passes validation:
NestJS server log (from code below)
users.resolver.ts:
#Mutation(() => User)
async createUser(#Args('input') input: CreateUserInput) { // Type from user.input.ts
Logger.log(input); // log from screenshot, so if it's here it passed validation
return this.usersService.create(input); // usersService makes requests to MongoDB
}
And it gets into MongoDB
Here is client side part:
App.tsx:
...
// CreateUserInput class is not imported to App.tsx (it is at server part) but it seems to be fine with it
const ADD_USER = gql`
mutation AddMutation($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`
function App(props: any) {
const { loading, error, data } = useQuery(GET_USERS);
const [addUser] = useMutation(
ADD_USER,
{
update: (cache: any, { data: { createUser } }: any) => {
const { users } = cache.readQuery({ query: GET_USERS });
cache.writeQuery({
query: GET_USERS,
data: {
users: [createUser, ...users],
},
})
}
}
);
...
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
return <UserTable users={data.users} addUser={addUser} updateUser={updateUser} deleteUser={deleteUser} />;
}
Can someone please explain to me, how does client Query passes validation and what have i done wrong?
Even two empty strings can pass through.
Never worked with NestJS, Apollo, React or GraphQL before, so I'm kinda lost.
For full code:
https://github.com/N238635/nest-react-crud-test
This is how your custom scalar's methods are defined:
parseValue(value: string): string {
return value;
}
serialize(value: string): string {
return value;
}
parseLiteral(ast: ValueNode): string {
if (ast.kind !== Kind.STRING) {
throw new GraphQLError('Query error: Can only parse strings got a: ' + ast.kind, [ast]);
}
// Regex taken from: http://stackoverflow.com/a/46181/761555
var re = /^([\w-]+(?:\.[\w-]+)*)#((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
if (!re.test(ast.value)) {
throw new GraphQLError('Query error: Not a valid Email', [ast]);
}
return ast.value;
}
parseLiteral is called when parsing literal values inside the query (i.e. literal strings wrapped in double quotes). parseValue is called when parsing variable values. When your client sends the query, it sends the value as a variable, not as a literal value. So parseValue is used instead of parseLiteral. But your parseValue does not do any kind of validation -- you just return the value as-is. You need to implement the validation logic in both methods.
It would also be a good idea to implement the serialize method so that your scalar can be used for both input and response validation.

GraphQL arguments on Object in query

I want to execute a query like this:
{
houses(owner: "Thomas") {
id
color
cars(type: "Sports Car") {
name
year
}
}
}
But this returns an error:
"message": "Unknown argument \"type\" on field \"cars\" of type \"House\".",
However, I'm able to execute this properly:
cars(type: "Sports Car") {
name
year
}
Is what I'm trying to do even possible?
Thanks in advance!
Make sure that you have your resolver for cars set up as a sub query of houses. The result from the houses query should be passed as the root argument to the cars sub query.
type House {
id
color
cars( type: String! ): [ Car ]
}
type Car {
name
year
}
Your resolver might look like:
Query: {
async houses( root, args, context ) {
return { ... houses ... }
}
},
houses: {
async cars( root, args, context ) {
return { ... cars ... }
}
}
Then create a resolver for cars that is nested underneath the houses query. Here's an article on it if you are using graphql-tools from Apollo: Writing resolvers with graphql-tools
Hope this helps!

Resources