Using when with objects in array in Yup - javascript

I have the following object:
const values = {
people: [
{
name: "Mark",
age: null
},
{
name: "Shark",
age: 31
}
],
isAgeRequired: false
};
When isAgeRequired flips to true, in my Yup schema I would like to make the age value for each object in the people array require.
I have the following yup schema definition, but right now it seems to validate successfully even if the above requirements are not met:
const validator = object({
people: array(
object({
name: string().required(),
age: number()
.nullable()
.when("isAgeRequired", {
is: true,
then: (schema) => schema.required()
})
})
)
});

The problem is that the parent context is not available in .when, so one way to get around this was to "lift" the .when condition so that it is declared at a level where isAgeRequired is a sibling like so:
// Declare the default person schema separately because I'll want to reuse it.
const person = {
name: string().required(),
age: number().nullable()
}
const validator = object({
people: array(
object(personSchema)
).when('isAgeRequired', (isAgeRequired, schema) => {
if (isAgeRequired) {
return array({
...personSchema,
age: personSchema.age.required() // Override existing age rule and attach .required to it
})
}
return schema;
})
});

Related

Mongoose: Creating a new subdocument (part of a nested schema) with it's default values

I have a simple problem that I am facing, unable to find any solution online. I have a mongoose schema that contains a nested schema.
const Person = new Schema(
{
name: { type: String, required: true, default: 'John Smith' },
age: { type: Number, required: false },
hobbies: { type: [HobbiesSchema], required: false, default: undefined },
}
);
const HobbiesSchema = new Schema(
{
hobby: { type: String, required: true, default: 'Football' }
}
);
Let's say I wanted to create a new Person with it's default values, that's pretty easy.
const person = new Person({});
// results in: { name: 'John Smith' }
But what if I wanted to create just a new hobby object for that person. Now, that can easily be handled by mongoose if you define your default values like this:
// for single nested object
hobbies: { type: [HobbiesSchema], required: false, default: default: () => ({}) },
// for array of objects
hobbies: { type: [HobbiesSchema], required: false, default: default: () => ([]) },
The problem here is that even if you set it to required: false, it still inserts default values when you create a new model instance which is undesired behavior for me.
I am aware that setDefaultsOnInsert exists, however I am working with bulk operations so that is not an option for me.
So what I am hoping to find is some way or a workaround to create something like this:
const hobby = new Person.Hobbies({}); // pseudo-code
// result: { hobby: 'Football' }
I need just that object.
Solution:
Okay, I managed to find a solution:
const hobby = Person.schema.path('hobbies').cast({});
// result: { hobby: 'football' }
https://github.com/Automattic/mongoose/issues/9076#issuecomment-646185905

Joi array Object validation based on root key value

I have a complex scenario that I want to validate using Joi
here sample Joi Object Schema
const itemSchema = Joi.object({
product_id: Joi.string().required(),
quantity: Joi.number().required().min(0)
});
let objSchema = {
items: Joi.array().items(itemSchema).required().min(1),
item_return_flag: Joi.string().optional().valid(true, false)
};
depending opon item_return_flag key value true or false, I want to change the items.quantity min value requirement. When true, quantity will be 0 , otherwise it will be 1.
Is there anyway, to control the definition of validation of the object in an array, based on the root object in Joi
The sample code that will switch the schema based one the parent key item_return_flag. Schema of the array need to switch based using Joi.altertnatives()
let itemArr = Joi.object({
product_id: Joi.string().required(),
quantity: Joi.number().required().min(0)
});
let itemArr2 = Joi.object({
product_id: Joi.string().required(),
quantity: Joi.number().required().min(1)
});
let itemSchema = Joi.alternatives()
.when('item_return_flag', { is: true, then: Joi.array().items(itemArr).required().min(1), otherwise: Joi.array().items(itemArr2).required().min(1)}) ;
let objSchema = {
items: itemSchema,
item_return_flag: Joi.string().optional().valid(true, false)
};
It looks to me like you could, following the API docs, do something like this:
let objSchema = {
items: Joi.array().items(Joi.object({
product_id: Joi.string().required(),
quantity: Joi.alternatives().when('item_return_flag', {
is: true, then: Joi.number().required().min(0),
otherwise: Joi.number().required().min(1)
})
})).required().min(1),
item_return_flag: Joi.string().optional().valid(true, false)
};
I'm not 100% sure that's the exact correct structure, but it's close. The Joi.alternatives() is provided for just such use cases.

Mongoose - How to set default value if new value is not in available enum list

I have the following schema:
const userSchema = new mongoose.Schema({
profile: {
name: {
type: String,
default: "",
enum: ["", "Mike", "John", "Bob"]
}
}
}
I would like to ensure that when a user.save action is triggered and provided name variable is not in the list of available enum values, validation does not fail, but sets the value to default.
For example, in Node:
User
.findById(req.user.id)
.then(user => {
user = Object.assign(user, { name: "Sam" })
return user.save()
})
This will fail validation with Sam is not a valid enum value for path profile.name, but the ask is to have value fallback to an empty string:
{
name: ""
}
I tried tapping into Mongoose pre validate hook, but cannot seem to access provided values and documentation has not been helpful.
maybe you can use a pre save hooks
var schema = new Schema(..);
schema.pre('save', function(next) {
// verify here
next();
});

How can I properly set object values inside a promise?

So I have this object:
let user = {
name: null,
age: null,
sex: null,
created_at: null
}
and I want to set the values based on what the promise returns. The data returned has the same property names.
So the promise is this:
promise.get(/*url*/).then(res => {
const data = res.data;
// set values here
})
I have three ways in my mind right now in setting the property values:
// First
user = data
// Second
user = {
name: data.name,
age: data.age,
sex: data.sex,
created_at: data.created_at
}
// Third
Object.assign(user, data);
Which is the best/proper way? And what are the advantages of one over the other?
I'm currently using the Third option. Any help would be much appreciated.
I like Object.assign()
const user = {
name: 'Ronald Mickdonlad',
age: null,
sex: 'no thanks',
created_at: null
}
const res = {
data: {
name: 'Garbo Hamburgler',
age: 1337,
sex: '0%',
created_at: 133713371337
}
}
//fake DB call
console.log(Object.assign({}, user, res.data))
I was trying to show it with spread operator also, but my REPL apparently doesn't have it, so I gave up. See here:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator
This should work also:
return {
...user,
...res.data
}
But, you will need Babel: https://babeljs.io/docs/plugins/transform-object-rest-spread/
I prefer both of those over any other way because it is immutable. You are not reassigning any variables. You are doing read-only once assigned and then creating new objects as you go.
Both Object.assign() and spread operator have right to left precedence, so "newer aka right-most" values will overwrite "older aka left-occurring" values.
Go with this:
const user = {
name: null,
age: null,
sex: null,
created_at: null
}
// if inside an async function
const updatedUserObj = await promise
.get(/*url*/)
.then(res => Object.assign({}, user, res.data))
console.log(updatedUserObj)
Here are a couple more examples to show you Object.assign() further:
const user = {
name: null,
age: null,
sex: null,
created_at: null
}
const someObject = {
coffee: {
good: true,
flavour: 'ultra',
shouldDrink: true,
age: 1337
}
}
// Notice how coffee is the property on the object we just created:
console.log('FIRST EXAMPLE', JSON.stringify(Object.assign({}, someObject), null, 2))
// Notice how someObject is the property on the new object and coffee is its property
console.log('SECOND EXAMPLE', JSON.stringify(Object.assign({}, { someObject }), null, 2))
// Now, watch what happens if we begin overwriting:
console.log('THIRD EXAMPLE', JSON.stringify(Object.assign({}, user, someObject), null, 2))
console.log('FOURTH EXAMPLE', JSON.stringify(Object.assign({}, { user, someObject }), null, 2))
console.log('FIFTH EXAMPLE', JSON.stringify(Object.assign({}, user, { someObject }), null, 2))
console.log('SIXTH EXAMPLE', JSON.stringify(Object.assign({}, { user }, someObject), null, 2))
Option 3 is the better option. In your example with Object.assign(), you are using a factory function to create new instance of your user. The advantage of this is, it doesn't force you to call a constructor of user when you want to create a new user instance. You could also use Object.create()
This is a basic example of Object Orientated Programming.
Read up more here for a better understanding https://www.sitepoint.com/object-oriented-javascript-deep-dive-es6-classes/

GraphQL mutation that accepts an array of dynamic size and common scalar types in one request

I need to be able to create a user and add it's favourite movies (An array of objects with a reference to the Movies collection and his personal rating for each movie) in a single request.
Something that could look like this (pseudocode)
var exSchema = `
type Mutation {
addUser(
name: String!
favMovies: [{ movie: String! #ref to movies coll
personal_rating: Int! # this is different for every movie
}]
) : User
}
...
`
What is the graphql way of doing this in a single request? I know I can achieve the result with multiple mutations/requests but I would like to do it in a single one.
You can pass an array like this
var MovieSchema = `
type Movie {
name: String
}
input MovieInput {
name: String
}
mutation {
addMovies(movies: [MovieInput]): [Movie]
}
`
Then in your mutation, you can pass an array like
mutation {
addMovies(movies: [{name: 'name1'}, {name: 'name2'}]) {
name
}
}
Haven't tested the code but you get the idea
I came up with this simple solution - NO JSON used. Only one input is used. Hope it will help someone else.
I had to add to this type:
type Option {
id: ID!
status: String!
products: [Product!]!
}
We can add to mutation type and add input as follows:
type Mutation {
createOption(data: [createProductInput!]!): Option!
// other mutation definitions
}
input createProductInput {
id: ID!
name: String!
price: Float!
producer: ID!
status: String
}
Then following resolver could be used:
const resolvers = {
Mutation: {
createOption(parent, args, ctx, info) {
const status = args.data[0].status;
// Below code removes 'status' from all array items not to pollute DB.
// if you query for 'status' after adding option 'null' will be shown.
// But 'status': null should not be added to DB. See result of log below.
args.data.forEach((item) => {
delete item.status
});
console.log('args.data - ', args.data);
const option = {
id: uuidv4(),
status: status, // or if using babel status,
products: args.data
}
options.push(option)
return option
},
// other mutation resolvers
}
Now you can use this to add an option (STATUS is taken from first item in the array - it is nullable):
mutation{
createOption(data:
[{
id: "prodB",
name: "componentB",
price: 20,
producer: "e4",
status: "CANCELLED"
},
{
id: "prodD",
name: "componentD",
price: 15,
producer: "e5"
}
]
) {
id
status
products{
name
price
}
}
}
Produces:
{
"data": {
"createOption": {
"id": "d12ef60f-21a8-41f3-825d-5762630acdb4",
"status": "CANCELLED",
"products": [
{
"name": "componentB",
"price": 20,
},
{
"name": "componentD",
"price": 15,
}
]
}
}
}
No need to say that to get above result you need to add:
type Query {
products(query: String): [Product!]!
// others
}
type Product {
id: ID!
name: String!
price: Float!
producer: Company!
status: String
}
I know it is not the best way, but I did not find a way of doing it in documentation.
I ended up manually parsing the correct schema, since JavaScript Arrays and JSON.stringify strings were not accepted as graphQL schema format.
const id = 5;
const title = 'Title test';
let formattedAttachments = '';
attachments.map(attachment => {
formattedAttachments += `{ id: ${attachment.id}, short_id: "${attachment.shortid}" }`;
// { id: 1, short_id: "abcxyz" }{ id: 2, short_id: "bcdqrs" }
});
// Query
const query = `
mutation {
addChallengeReply(
challengeId: ${id},
title: "${title}",
attachments: [${formattedAttachments}]
) {
id
title
description
}
}
`;
What i understand by your requirement is that if you have the following code
const user = {
name:"Rohit",
age:27,
marks: [10,15],
subjects:[
{name:"maths"},
{name:"science"}
]
};
const query = `mutation {
createUser(user:${user}) {
name
}
}`
you must be getting something like
"mutation {
createUser(user:[object Object]) {
name
}
}"
instead of the expected
"mutation {
createUser(user:{
name: "Rohit" ,
age: 27 ,
marks: [10 ,15 ] ,
subjects: [
{name: "maths" } ,
{name: "science" }
]
}) {
name
}
}"
If this is what you wanted to achieve, then gqlast is a nice tag function which you can use to get the expected result
Simply grab the js file from here and use it as:
const user = {
name:"Rohit",
age:27,
marks: [10,15],
subjects:[
{name:"maths"},
{name:"science"}
]
};
const query = gqlast`mutation {
createUser(user:${user}) {
name
}
}`
The result stored in the variable query will be :
"mutation {
createUser(user:{
name: "Rohit" ,
age: 27 ,
marks: [10 ,15 ] ,
subjects: [
{name: "maths" } ,
{name: "science" }
]
}) {
name
}
}"
Pass them as JSON strings. That's what I do.
For those of you who don't need to pass in an array for one request, and are open to the idea of making a request for every mutation. (I am using Vue3, compisition Api, but React and Angular developers still can understand this).
You cannot for loop the mutation like this:
function createProject() {
for (let i = 0; i < state.arrOfItems.length; i++) {
const { mutate: addImplementation } = useMutation(
post_dataToServer,
() => ({
variables: {
implementation_type_id: state.arrOfItems[i],
sow_id: state.newSowId,
},
})
);
addImplementation();
}
}
this will give you an error, because the mutation must be in the setup().
(here is the error you will recieve: https://github.com/vuejs/vue-apollo/issues/888)
Instead create a child component, and map the array in the parent.
in Parent.vue
<div v-for="(card, id) in state.arrOfItems">
<ChildComponent
:id="id"
:card="card"
/>
</div>
in ChildComponent.vue
recieve props and:
const { mutate: addImplementation } = useMutation(
post_dataToServer,
() => ({
variables: {
implementation_id: props.arrOfItems,
id: props.id,
},
})
);

Categories

Resources