Get localized data from multilingual schema with GraphQL query - javascript

I have following MongoDB schema.
Item {
_id: ObjectId,
translations: [{
language: String
name: String
}]
}
So my Item instance could look something like this.
{
_id: ObjectId("5ba3bf09d3121aba3ba2f488"),
translations: [
{
language: "en"
name: "a Car"
},
{
language: "de",
name: "der Wagen"
}]
}
And I want to be able to query my data with specific language with Graphql this way.
{
item(where: {language: "en"}) {
name
}
}
So it would produce nice output with shape like this.
{
name: "a Car"
}
Please can you tell me some good practice or nice way I can setup my Graphql resolvers map?
I'm using Apollo Server.
Thank you very much!

A general solution for a language specific query (with more than one field) could be:
passing the language parameter to the query resolver
store the language on the resolver context
use the language from the context wherever needed
Query:
query {
item(language: "en") {
name
otherField
}
}
Resolver:
{
item: (_, { language }, context) => {
context.language = language;
return {
name: (_, context) => getNameByLang(context.language),
otherField: (_, context) => getOtherByLang(context.language),
};
},
}
Or if there's only one translated field:
query {
item {
name(language: "en")
}
}
so you get the language directly in the name resolver as an argument.
{
item: () => ({
name: ({ language }) => getNameByLang(language),
})
}

Related

JOI validation for a string with json as a value

i am trying to validate the string using the JOI package available in npm, i checked this documentation which has many useful string formats e.g. date, IP, base64 but i need to validate the following JSON which contains a stringified JSON as a value, and there is no example in the documentation for such case
{
"id": 232,
"name": "Trojan Horse",
"file": "download.exe",
"infected": true,
"engines": "['Norton', 'AVG', 'NOD32']"
}
So for example what if i want to check engines have valid JSON value and have at-least one engine defined if infected key is set to true
The following schema works only if the engines value is written as parsed JSON
Joi.object().keys({
id: Joi.number().required(),
name: Joi.string().min(5).required(),
file: Joi.string().min(3).required(),
infected: Joi.boolean().required(),
engines: Joi.array().when('infected', {
is: Joi.exists().valid(true),
then: Joi.min(1).required()
})
});
What you need to do is to create a custom JOI validator by extending the array validator of the JOI package and using that custom validator for the engines property.
const custom = Joi.extend({
type: 'array',
base: Joi.array(),
coerce: {
from: 'string',
method(value, helpers) {
if (typeof value !== 'string' ||
value[0] !== '[' && !/^\s*\[/.test(value)) {
return;
}
try {
return { value: JSON.parse(value) };
}
catch (ignoreErr) { }
}
}
});
const schema = Joi.object({
id: Joi.number().required(),
name: Joi.string().min(5).required(),
file: Joi.string().min(3).required(),
infected: Joi.boolean().required(),
engines: custom.array().when('infected', {
is: true,
then: custom.array().min(1).required()
})
})
const validateTest = async (joiSchema, testObject) => {
try {
const value = await joiSchema.validateAsync(testObject);
console.log(value);
}
catch (err) {
console.error(err)
}
};
validateTest(schema, {
"id": 232,
"name": "Trojan Horse",
"file": "download.exe",
"infected": true,
"engines": `["Norton", "AVG", "NOD32"]`
})
You can see more examples like that here

Mutating an array with apollo

I have a server side schema with this mutation type
type Mutation {
updateSettings(settings: SettingsInput): Settings
}
input SettingsInput {
repositories: [RepositoryInput]
}
input RepositoryInput {
id: String
name: String
url: String
}
I can mutate this exactly like I want to if I use a client such as Altair, with this query:
mutation{
updateSettings(settings: {
repositories: [
{
name: "name1"
url: "url1"
},
{
name: "name2"
url: "url2"
}
]
}){
repositories {
id
name
url
}
}
}
However I am struggling to get it working when using Apollo
The best I can get is this
import { SubscriptionClient } from "subscriptions-transport-ws";
import { gql } from "apollo-boost";
import { WebSocketLink } from "apollo-link-ws";
const wsClient = new SubscriptionClient("ws://localhost:5001/graphql", {
reconnect: true
});
const client = new WebSocketLink(wsClient);
const UPDATE_SETTINGS = gql`
mutation UpdateSettings($settings: SettingsInput) {
updateSettings(settings: $settings) {
repositories {
id
name
url
}
}
}
`;
client
.request({
query: UPDATE_SETTINGS,
variables: { repository: [{name: "name1", url:"url1"},
{name: "name2", url:"url2"}]}
})
I am obviously missing something. The client doesn't seem to be aware of the servers SettingsInput, but I can't really figure out how to create a query for the client that takes complex objects or arrays as variables.
Am I going about this in a totaly weird way or how do I go about sending "complex" mutations to the server from an apollo client?
The client aside, you also need to fix the variables object you're passing in. The variable you've defined in your operation is named settings, but you are only passing in a variable named repository. Additionally, the shape of this variable doesn't match SettingsInput as shown in your schema. variables should look something like:
const variables = {
settings: {
repositories: [
{
name: 'name1',
url: 'url1'
},
{
name: 'name2',
url: 'url2'
},
],
},
}

How to handle an entitiy relation without bidirectional links in normalizr

I'm adding normalizr to a redux application and I'm having a tricky time handling a unidirectional link in the data returned from an API.
Two separate API calls are made, the first one returns an author:
author = {
...authorFields,
}
The second one returns a list of books:
books = [
{
...bookFields,
authorId,
},
...
]
Here is my normalizr schema:
const book = new schema.Entity('books')
const author = new schema.Entity('authors', {
books: [ book ],
})
book.define('book', {
author,
})
In our API response, we only have a link in the book, and not contained in the author. Just calling normalize() on the case won't give us the nice normalized state we want.
My target state is this (the normalized state that we would get if the author response from the API contained the books as nested entities):
{
authors: {
"101": {
...authorFields
books: [
"1001",
"1002",
etc...
]
}
books {
"1001" : {
"1001": {
...bookFields,
author: "101",
},
"1002": {
...bookFields,
author: "101",
}
}
}
}
My approach was to handle things in the reducer.
Here's the books action creator (I'm using thunk):
function fetchBooksForAuthor(authorId) {
return (dispatch, _, schema) => {
return getBooksFromAPIByAuthor(authorId)
.then(books => {
const normalizedData = normalize(books, [ schema.book ])
dispatch(addBooks({
authorId,
...normalizedData
}))
})
}
}
Here are the reducers:
function books(state = {}, action) {
switch (action.type) {
case ADD_BOOKS:
return {
...state,
...action.payload.entities.books
}
...
}
}
function authors(state = {}, action) {
switch (action.type) {
case ADD_BOOKS:
const authorId = action.payload.authorId
const author = state[authorId]
return {
...state,
[authorId]: {
...author,
...action.payload.result
}
}
...
}
}
Would this be the best way to handle this type of case?
Is there any way of handling this type of case entirely inside normalizr? If not, does anyone have any suggestions as to the best way to handle this? Add another link in the API ^^?

GraphQL.js - use Interface as a default (fallback) type in resolveType function

I'm trying to return generic type in resolveType function if none of the provided types is matched. The example below shows this problem: API worked like a charm supporting UserType and MovieType until in database was added BookType (not supported by GraphQL schema).
const {
graphql,
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLNonNull,
GraphQLList,
GraphQLInterfaceType
} = require("graphql");
const DATA = [
{
// UserType
name: "catherine woolf",
nick: "catherine"
},
{
// MovieType
name: "cat woman",
director: "Jack Wolfgang"
},
{
// --- missing type --- (BookType)
name: "cats secrets",
author: "Nicky Glace"
}
];
const resolveType = data => {
if (data.nick) {
return UserType;
}
if (data.director) {
return MovieType;
}
};
const SearchableType = new GraphQLInterfaceType({
name: "Searchable",
fields: {
name: { type: GraphQLString }
},
resolveType: resolveType
});
const UserType = new GraphQLObjectType({
name: "User",
interfaces: [SearchableType],
fields: {
name: { type: GraphQLString },
nick: { type: GraphQLString }
}
});
const MovieType = new GraphQLObjectType({
name: "Movie",
interfaces: [SearchableType],
fields: {
name: { type: GraphQLString },
director: { type: GraphQLString }
}
});
const schema = new GraphQLSchema({
types: [MovieType, UserType, SearchableType],
query: new GraphQLObjectType({
name: "RootQueryType",
fields: {
search: {
type: new GraphQLList(SearchableType),
args: {
text: { type: new GraphQLNonNull(GraphQLString) }
},
resolve(_, { text }) {
return DATA.filter(d => d.name.indexOf(text) !== -1);
}
}
}
})
});
const query = `
{
search(text: "cat") {
name
... on User {
nick
}
... on Movie {
director
}
}
}
`;
graphql(schema, query).then(result => {
console.log(JSON.stringify(result, null, 2));
});
So now this code ends with error:
"Abstract type Searchable must resolve to an Object type at runtime for field RootQueryType.search with value \"[object Object]\", received \"undefined\". Either the Searchable type should provide a \"resolveType\" function or each possible types should provide an \"isTypeOf\" function."
This is nothing surprising since currently resolveType may not return any type.
Workaround
Crate type containing the same field like interface SearchableType (1 to 1 implementation):
const _SearchableType = new GraphQLObjectType({
name: '_Searchable',
interfaces: [SearchableType],
fields: {
name: { type: GraphQLString },
}
});
Use it as a fallback type:
const resolveType = data => {
if (data.nick) {
return UserType;
}
if (data.director) {
return MovieType;
}
return _SearchableType;
};
And add it to types in schema definition:
types: [MovieType, UserType, SearchableType, _SearchableType],
But the problem with this solution is presence of this dummy _SearchableType in documentation like this:
Question
Is there any way to return interface SearchableType or equivalent of it in resolveType? The key point for me is hiding of this "fallback type" in a documentation.
GraphQL is strongly typed and doesn't support generics or some kind of "fallback" mechanism when resolving unions and interfaces. At the end of the day, if your underlying data layer is returning some type that you have not yet implemented in your schema, the simplest solution is to simply add that type to your schema. Migrations to your database and changes to your schema should go hand-in-hand.
If you want to derive your schema from your storage layer, I would suggest looking into something like PostGraphile (formerly PostGraphQL).
That said, if you're bent on using a workaround, you could just fallback to one of the existing types:
const resolveType = data => {
if (data.nick) {
return UserType
}
return MovieType
}
Now a book's name will still be accessible, provided you query it on the interface and not one of the types. The only downside to this approach is that movie-specific fields will be returned for a book and will resolve to null, but that won't cause any issues unless they're specifically defined as non-null in your schema.

Redux normalizr - nested API responses

How can I use normalizr to deal with nested standardised JSON API responses that are key via the { data: ... } standard?
For example a Book
{
data: {
title: 'Lord of the Rings',
pages: 9250,
publisher: {
data: {
name: 'HarperCollins LLC',
address: 'Big building next to the river',
city: 'Amsterdam'
},
},
author: {
data: {
name: 'J.R.R Tolkien',
country: 'UK',
age: 124,
}
}
}
}
How would I design schemas to deal with the nested data key?
For each entity in your response, you should create it's own schema.In your example, we have three entities - books, authors and publishers:
// schemas.js
import { Schema } from 'normalizr';
const bookSchema = new Schema('book');
const publisherSchema = new Schema('publisher');
const authorSchema = new Schema('author');
If some entity contains nested data which should be normalized, we need to use define method of it schema.This method accepts an object with nesting rules.If we need to normalize publisher and author props of book entity, we should pass an object to define function with same structure as our response:
// schemas.js
bookSchema.define({
data: {
publisher: publisherSchema,
author: authorSchema
}
});
Now we can normalize our response:
import { normalize } from 'normalizr';
import { bookSchema } from './schemas.js';
const response = {
data: {
title: 'Lord of the Rings',
pages: 9250,
publisher: {
data: {
name: 'HarperCollins LLC',
address: 'Big building next to the river',
city: 'Amsterdam'
},
},
author: {
data: {
name: 'J.R.R Tolkien',
country: 'UK',
age: 124,
}
}
}
}
const data = normalize(response, bookSchema);
I believe what you're after is the use of the assignEntity function which can be passed in the options of normalize. In this instance it lets us, where appropriate, filter out the redundant data properties and go straight to the values underneath.
Effectively assignEntity let's you control how each key of data is normalized. Take a look here for a little more on how it works.
I put this together as a demonstration, take a look: http://requirebin.com/?gist=b7d89679202a202d72c7eee24f5408b6. Here's a snippet:
book.define({
data: {
publisher: publisher,
author: author,
characters: normalizr.arrayOf(character)
}}
);
publisher.define({
data: {
country: country
}
});
const result = normalizr.normalize(response, book, { assignEntity: function (output, key, value, input) {
if (key === 'data') {
Object.keys(value).forEach(function(d){
output[d] = value[d];
})
} else {
output[key] = value;
}
}});
Also see in particular Ln 29, where the array of characters has some objects with the information nested within data and some without. All are normalized correctly.
I also added some parts to show how it works with arrays and deeply nested data, see the country model within publisher.
With the data provided you will need a slug due to the absence of id's, which each schema also contains in the example.
Normalizr is fantastic, I hope that helps explain a little more about it :)

Categories