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 :)
Related
I am using React with nextJS to do web developer,I want to render a list on my web page, the list information comes from the server(I use axios get function to get the information). However some JSON objects are lack of some information like the name, address and so on. My solution is to use a If- else to handle different kind of JSON object. Here is my code:
getPatientList(currentPage).then((res: any) => {
console.log("Response in ini: " , res);
//console.log(res[0].resource.name[0].given[0]);
const data: any = [];
res.map((patient: any) => {
if ("name" in patient.resource) {
let info = {
id: patient.resource.id,
//name:"test",
name: patient.resource.name[0].given[0],
birthDate: patient.resource.birthDate,
gender: patient.resource.gender,
};
data.push(info);
} else {
let info = {
id: patient.resource.id,
name: "Unknow",
//name: patient.resource.name[0].given[0],
birthDate: patient.resource.birthDate,
gender: patient.resource.gender,
};
data.push(info);
}
});
Is there any more clever of efficient way to solve this problem? I am new to TS and React
Use the conditional operator instead to alternate between the possible names. You should also return directly from the .map callback instead of pushing to an outside variable.
getPatientList(currentPage).then((res) => {
const mapped = res.map(({ resource }) => ({
id: resource.id,
// want to correct the spelling below?
name: "name" in resource ? resource.name[0].given[0] : "Unknow",
birthDate: resource.birthDate,
gender: resource.gender,
}));
// do other stuff with mapped
})
I have an array of user objects which I fetch from a (Firestore) database looking similar to this:
let users = [
{ name: 'max', role: 1, company_id: 14 },
{ name: 'steve', role: 3, company_id: 29 },
{ name: 'anna', role: 5, company_id: 124 }
];
There are two tables user_roles and companies where I need to fetch data like the respective role name or company name from. I'm achieving it by looping through the users array and using observables to get data from Firestore. The fetched data is added to the respective user so my users array now contains info about the name of the user's company.
for (let user of users) {
// Define all observables
const role_data = firebase.getRoleData(user.role);
const company_data = firebase.getCompanyData(user.company_id);
// Join observables to one stream
const join_stream = combineLatest([role_data, company_data]);
// Subscribe to that joined stream and fetch info
join_stream.subscribe(([role_data, company_data]) => {
user.role_name = role_data['role_name'];
user.company_name = company_data['company_name'];
});
}
Is there any way to detect when the for loop is done so I can show/hide a loading spinner? I thought of implementing a counter checking the length of the users array but that doesn't seem to be an elegant way.
I doesn't have to be solved by using rxjs or similar, every working solution or hint is welcome. Maybe some async stuff? I haven't found anything suitable.
Thank you in advance!
Hope I understood correctly. Is this what you're looking for?
let users = [
{ name: 'max', role: 1, company_id: 14 },
{ name: 'steve', role: 3, company_id: 29 },
{ name: 'anna', role: 5, company_id: 124 }
];
for (let [i, user] of users.entries()) {
// Define all observables
// ...
// Join observables to one stream
// ...
// Subscribe to that joined stream and fetch info
// ...
if (i+1 === users.length) {
console.log('Finished!');
}
}
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'
},
],
},
}
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),
})
}
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.