Adding property to nested objects in Flow causes errors - reactjs

I can add a property to a flat object in Flow without any errors.
View demo
But if I add a property to a nested object using the same pattern, Flow throws an error:
/* #flow */
type Content = {
fields: {
slug: string,
},
frontmatter: {
title: string,
}
};
type CustomContent = {
fields: {
slug: string,
},
frontmatter: {
title: string,
// This is the new property:
headerImage: string,
}
};
function doSomethingWithContent(content: Content) {
return [
content.fields.slug,
content.frontmatter.title,
];
}
function doSomethingWithCustomContent(content: CustomContent) {
return [
content.fields.slug,
content.frontmatter.title,
content.frontmatter.headerImage,
];
}
Error:
doSomethingWithContent(customContent);
^ Cannot call `doSomethingWithContent` with `customContent` bound to `content` because property `headerImage` is missing in object type [1] but exists in object type [2] in property `frontmatter`.
View demo

For this to work, the frontmatter property needs to be marked as covariant, e.g.
type Content = {
fields: {
slug: string,
},
frontmatter: {
title: string,
}
};
to
type Content = {
fields: {
slug: string,
},
+frontmatter: { // Note the "+" prefix here
title: string,
}
};
If Flow didn't error on this, it would be perfectly valid for your function to do
function doSomethingWithContent(content: Content) {
content.frontmatter = { title: "" };
which make the type of content still a valid Content, but an invalid CustomContent object. By marking the property covariant, it essentially makes the frontmatter property read-only.

Related

How to properly do nested schema with mongoose in React?

I have a Class schema that looks like this:
const ClassSchema = new mongoose.Schema({
title: {
type: String,
...
},
classImageURL: {
type: String,
...
},
imageWidth: {
type: String,
...
},
imageHeight: {
type: String,
...
}
});
module.exports = mongoose.models.Class || mongoose.model("Class", ClassSchema);
And a Subject schema that looks like this:
const SubjectSchema = new mongoose.Schema({
subjectTitle: {
type: String,
...
},
subjectImageURL: {
type: String,
...
},
imageWidth: {
type: String,
...
},
imageHeight: {
type: String,
...
},
});
module.exports =
mongoose.models.Subject || mongoose.model("Subject", SubjectSchema);
On a dynamic page named [className], I am getting the data of the particular className from the database and destructured it. Now, on the class page, I want to send a post request to the database using all the fields titled in the Subject schema. But, I also want to add the class data that I got and add it to the Subject schema.
I used a state to hold all the data:
setForm({
subjectTitle: enteredSubjectTitle,
subjectImageURL: response.data.url,
imageWidth: response.data.width,
imageHeight: response.data.height,
classDetail: classDetail // this is the data I have on the particular class data
}); // I want to add
And I tried to make changes in the Subject schema like this:
classDetail: { Class }, // I added this in the last part of the schema
It results in a post error.
How can I achieve what I want to?

How can you compare $Shape in flow on a deep nested object?

The issue I have is that I want to create a function that takes an object that is a small part of a larger object and deep merges it. However, I want it strongly typed to catch if/when the 'big' object's shape changes.
I am including a contrived example below.
/* #flow */
type ContrivedType = {
nested: {
name: string,
optionalValue1?: string,
optionalValue2?: string,
}
}
let contrived: ContrivedType = {
nested: {name: "Required Value"}
}
type UpdateObject = $Shape<ContrivedType>
const update = (updateObject: UpdateObject): void => {
contrived = {
...contrived,
...updateObject,
nested: {
...contrived?.nested,
...updateObject?.nested
}
}
}
update({nested: {name: "should work", optionalValue1: "should work"}})
update({nested: {optionalValue1: "won't work"}})
Here you can see the results
Cannot call 'update' with object literal bound to 'updateObject' because property 'name' is missing in object literal [1] but exists in object type [2] in property 'nested'. [prop-missing]
How can I make this work?
You can create your own $DeepShape type as seen in use here
The code I used is
type $DeepShape<O: Object> = Object & $Shape<$ObjMap<O, (<V: Object>(V) => $DeepShape<V>) | (<V>(V) => V)>>
and is from this Github thread
Here is the final result of the above code:
/* #flow */
type ContrivedType = {
nested: {
name: string,
optionalValue1?: string,
optionalValue2?: string,
}
}
let contrived: ContrivedType = {
nested: {name: "Required Value"}
}
type $DeepShape<O: Object> = Object & $Shape<$ObjMap<O, (<V: Object>(V) => $DeepShape<V>) | (<V>(V) => V)>>
type UpdateObject = $DeepShape<ContrivedType>
const update = (updateObject: UpdateObject): void => {
contrived = {
...contrived,
...updateObject,
nested: {
...contrived?.nested,
...updateObject?.nested
}
}
}
update({nested: {name: "should work", optionalValue1: "should work"}})
update({nested: {optionalValue1: "won't work"}})
Would love any feedback on any issues or concerns people may have.

Getting a type error from my map function

I am trying to create an array containing only id's from the state.words object (below)
MY REQUIRED OUTPUT
['word-1','word-2','word-3','word-4','word-5','word-6','word-7']
My starting data structure looks like this
words: {
'word-1': { id: 'word-1', content: 'Jimmy Yukka' ,url:'www.paulayling.me'},
'word-2': { id: 'word-2', content: 'INXS' ,url:'www.paulayling.me'},
'word-3': { id: 'word-3', content: 'Up North' ,url:'www.paulayling.me'},
'word-4': { id: 'word-4', content: 'Prince' ,url:'www.paulayling.me'},
'word-5': { id: 'word-5', content: 'Magic Moose' ,url:'www.paulayling.me'},
'word-6': { id: 'word-6', content: 'Salt n Pepper' ,url:'www.paulayling.me'},
'word-7': { id: 'word-7', content: 'Maddonna' ,url:'www.paulayling.me'}
}
The problem I have is that because it is an object array based methods will not give me the result I need eg below does not work - but I need an equivalent for an object of objects
My function is below
addNewWord() {
var currentOrder = this.state.words.map(function (book) {
return book.id
})
console.log('words stuff', currentOrder)
}
I get the error
TypeError: this.state.words.map is not a function
The function is called from the componentDidMount() function if it is relavant
map can only be used on arrays. If you want to list the keys in your words object, you can do it this way:
const result = Object.keys(this.state.words);
This will return your required output

How can I access all elements with a particular attribute in graphQL?

I have some json data in file called countryData.json structured as so:
{
"info":"success",
"stats":
[{
"id":"1",
"name":"USA",
"type":"WEST"
},
//...
I'm using graphQL to access this data. I have created an object type in the schema for countries using the following:
const CountryType = new GraphQLObjectType({
name: "Country",
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
type: { type: GraphQLString },
})
});
I want to write a query that will allow me to access all of the elements of this array that have a certain "name" value(There can be multiple with the same name). I've written the following query, but it only returns the first match in the array:
const RootQuery = new GraphQLObjectType({
name:"RootQueryType",
fields:{
country: {
type: CountryType,
args: { type: { name: GraphQLString } },
resolve(parent, args){
return _.find(countryData.stats, {name: args.name});
}
}
}
});
The "_" comes from const _ = require('lodash');
Also, how can I just get every single item in the array?
I have not recreated the code, therefore I can not check if it would be executed correctly. This is code, that should work in my opinion (without trying). If you want to return array of elements you need to implement https://lodash.com/docs/#filter. Filter will return all objects from stats, which match the argument name. This will return correctly inside resolver function, however, your schema needs adjustments to be able to return array of countries.
You need probably rewrite the arguments as follows as this is probably not correct. You can check out how queries or mutation arguments can be defined https://github.com/atherosai/express-graphql-demo/blob/feature/2-json-as-an-argument-for-graphql-mutations-and-queries/server/graphql/users/userMutations.js. I would rewrite it as follows to have argument "name"
args: { name: { type: GraphQLString } }
You need to add GraphQLList modifier, which defines, that you want to return array of CountryTypes from this query. The correct code should look something like this
const RootQuery = new GraphQLObjectType({
name:"RootQueryType",
fields:{
country: {
type: CountryType,
args: { name: { type: GraphQLString } },
resolve(parent, args){
return _.find(countryData.stats, {name: args.name});
}
},
countries: {
type: new GraphQLList(CountryType),
args: { name: { type: GraphQLString } },
resolve(parent, args){
return _.filter(countryData.stats, {name: args.name});
}
}
}
});
Now if you call query countries, you should be able to retrieve what you are expecting. I hope that it helps. If you need some further explanation, I made the article on implementing lists/arrays in GraphQL schema as I saw that many people struggle with similar issues. You can check it out here https://graphqlmastery.com/blog/graphql-list-how-to-use-arrays-in-graphql-schema
Edit: As for the question "how to retrieve every object". You can modify the code in resolver function in a way, that if the name argument is not specified you would not filter countries at all. This way you can have both cases in single query "countries".

Custom Validate an object Push to an array

I am using something similar to the following schema.
By visiting the Item page I can add related items to the Item's Related Items array field.
I would like to custom validate the object I am pushing to the Item's Related Items field, to test if a similar object exists in the array already - so that I do not get a duplicate.
In my code below, the custom validation does not fire. I expect this may be because custom validation cannot be applied to a type: [object], and should be applied to the properties of the object - but then I am unable to test the object as a whole.
const ItemsSchema = new SimpleSchema({
name: {
type: String,
label: 'Name',
},
related: {
type: [Object],
label: 'Related Items',
optional:true,
custom: function () {
let queryData = { docId: this.docId, related: this.value }
if (Meteor.isClient && this.isSet) {
Meteor.call("relatedObjectIsUniqueForThisItem", queryData,
function (error, result) {
if(!result){
console.log("not unique");
return "Invalid";
}
else{
return true;
}
});
}
}
},
'related.$.name':{
type: String,
label:'Name',
},
'related.$.code':{
type:String,
label:'Code',
min:5,
},
});
I figured out the way to handle this.
The custom validation should not be on the [object], but rather one of the properties of the object - in this case 'source' or 'code'.
Inside one of the object properties you can call this.siblingField(otherField); But it means you have to rebuild the object.
In my case :-
const ItemsSchema = new SimpleSchema({
name: {
type: String,
label: 'Name',
},
related: {
type: [Object],
label: 'Related Items',
optional:true,
},
'related.$.name':{
type: String,
label:'Name',
custom: function () {
//---------------------------
//this is the important bit
//---------------------------
let queryData = {
docId: this.docId,
related: {
name:this.value,
code:this.siblingField('code').value,
}
}
//---------------------------
//end of important bit
//---------------------------
if (Meteor.isClient && this.isSet) {
Meteor.call("relatedObjectIsUniqueForThisItem", queryData,
function (error, result) {
if(!result){
console.log("not unique");
return "Invalid";
}
else{
return true;
}
});
}
}
},
'related.$.code':{
type:String,
label:'Code',
min:5,
},
});

Resources