joi validation based on array object property value - javascript

I have a set of data to validate:
{
"createUser": {
"isCustomer": true or false,
"data": {
"product": [ // sometimes array of object with id : 3
{
"id":46,
"sumInsured":"4562000",
},
{
"id":45,
"sumInsured":"8532000",
},
]
}
}
These are the following scenarios we need to validate:
1) validate array of objects
2) isCustomer is mandatory when id is 45
3) isCustomer not allowed when id is 3
First is done:
Joi.object({
data: Joi.array()
.items({
id: Joi.number().valid(3,45,46)
.required(),
sumInsured: Joi.string()
.required()
}),
})
I searched a lot regarding the same, but still not able to find a solution for the same.
Any help would be really appreciated.
Thanks in advance.

This a combined Schema for both points 2 & 3.
You will need to use the method .when - which will define your first
if condition. From there, you will have to include another .when to add your second if condition
Such that
.when("data", {
is: <first if condition>,
then: <first if condition do something>
otherwise: .when("data",{
is: <else if>
then: <else if do something>
})
})
To understand the above logically,
it would result in the following
Joi.any().when("data", {
is: <is id 45?>,
then: <is required>
otherwise: Joi.any().when("data",{
is: <is id 3?>
then: <is forbidden>
})
})
Test cases
const test_id_3_ok = {
createUser: {
data: {
product: [
{
id: 3,
},
],
},
},
};
const test_id_46_ok = {
createUser: {
data: {
product: [
{
id: 46,
},
],
},
},
};
const test_id_46_has_customer_ok = {
createUser: {
isCustomer: true,
data: {
product: [
{
id: 46,
},
],
},
},
};
const test_id_46_no_customer_ok = {
createUser: {
data: {
product: [
{
id: 46,
},
],
},
},
};
const test_id_3_has_customer_should_error = {
isCustomer: true,
createUser: {
data: {
product: [
{
id: 3,
},
],
},
},
};
const test_id_45_ok = {
createUser: {
isCustomer: true,
data: {
product: [
{
id: 45,
},
],
},
},
};
const test_id_45_no_customer_should_error = {
createUser: {
data: {
product: [
{
id: 45,
},
],
},
},
};
Schema
const dataSchema = Joi.object({
product: Joi.array().items(
Joi.object({
id: Joi.number().valid(3, 45, 46),
})
),
});
const mandatory = (value) =>
Joi.object({
product: Joi.array().items(
Joi.object({
id: Joi.number().valid(value),
})
),
});
const schema = Joi.object({
createUser: Joi.object({
isCustomer: Joi.any().when("data", {
is: mandatory(3),
then: Joi.forbidden(),
otherwise: Joi.any().when("data", {
is: mandatory(45),
then: Joi.bool().required(),
}),
}),
data: dataSchema,
}),
});
schema.validate(test_id_3_ok) //?
schema.validate(test_id_3_has_customer_should_error); //?
schema.validate(test_id_45_ok); //?
schema.validate(test_id_45_no_customer_should_error); //?
schema.validate(test_id_46_ok); //?
schema.validate(test_id_46_has_customer_ok); //?
schema.validate(test_id_46_no_customer_ok); //?

Related

Mongo Aggregation pipeline update or push

I have a MongoDB Model which consist of array of members as obejcts.
const guestSchema = new mongoose.Schema({
salutation: {
type: String,
},
members: [membersSchema],
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
},
});
Members Schema:
const membersSchema = new mongoose.Schema({
name: String,
status: {
type: String,
enum: ['regular', 'helper'],
default: 'regular',
},
});
I want to achieve of doing an update in case documet with given ID exist or push to an array in case ID with document in array does not exist. I use aggregation pipeline, however I am not able to achieve pushing new document to array. Why can't I use push after else statement like this.
const subDocumentToUpsert = { 'name': mem.name, 'status': mem.status, '_id': ObjectId(mem.id)}
const subDocumentNoID = { 'name': mem.name, 'status': mem.status}
await Guest.findOneAndUpdate(
{ "_id": req.params.id },
[
{
$set: {
members: {
$cond: {
if: { $in: [subDocumentToUpsert._id, '$members._id'] },
then: {
$map: {
input: '$members',
as: 'sub_document',
in: {
$cond: {
if: { $eq: ['$$sub_document._id', subDocumentToUpsert._id] },
then: subDocumentToUpsert,
else: '$$sub_document',
},
},
},
},
else: {
$push: {
subDocumentNoID
},
},
},
},
},
},
},
]);
What is the best way of doing so? Thank you
You can do as follow:
db.collection.update({
_id: {
$in: [
1,
2
]
}
},
[
{
$set: {
members: {
$cond: {
if: {
$in: [
5,
"$members._id"
]
},
then: {
$map: {
input: "$members",
as: "sub",
in: {
$cond: {
if: {
$eq: [
"$$sub._id",
5
]
},
then: {
_id: 5,
status: "regular_updated",
name: "Negan_updated"
},
else: "$$sub"
},
},
},
},
else: {
$concatArrays: [
"$members",
[
{
_id: 5,
status: "regular_upserted",
name: "Negan_upserted"
}
]
]
}
}
}
}
}
}
],
{
multi: true
})
Explained:
Check if _id:5 exist in the subobject and update via $map/$cond only the object that has the _id:5.
In case there is no _id:5 add the new object to the array with $concatArrays.
Playground

Avoid Code Duplication with GraphQL Cursor based Pagination

I've been looking all over for an answer to this and I've been banging my head on the wall. I wrote a cursor based pagination example that works well with graphql and the thing is I thought I would do the same thing with authors, that I did with books and the only way I can figure out how to do this is to completely duplicate everything. On the root query there is quite a long chunk of code handling the pagination and I would hate to do that all over for the authors endpoint but I can't seem to find a way to do this while reusing the code
Here is the code
const express = require('express')
const { graphqlHTTP } = require('express-graphql')
const {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLList,
GraphQLInt,
GraphQLNonNull
} = require('graphql')
const {
PageType,
convertNodeToCursor,
convertCursorToNodeId
} = require('./pagination')
const app = express()
const authors = [
{ id: 1, name: "Author 1"},
{ id: 2, name: "Author 2"},
{ id: 3, name: "Author 3"}
]
const books = [
{ id: 1, title: "Book 1", authorId: 1 },
{ id: 2, title: "Book 2", authorId: 1 },
{ id: 3, title: "Book 3", authorId: 1 },
{ id: 4, title: "Book 4", authorId: 2 },
{ id: 5, title: "Book 5", authorId: 2 },
{ id: 6, title: "Book 6", authorId: 2 },
{ id: 7, title: "Book 7", authorId: 3 },
{ id: 8, title: "Book 8", authorId: 3 },
{ id: 9, title: "Book 9", authorId: 3 }
]
const Book = new GraphQLObjectType({
name: 'Book',
description: 'this is a book',
fields: () => ({
id: { type: GraphQLNonNull(GraphQLInt) },
title: { type: GraphQLNonNull(GraphQLString) },
authorId: { type: GraphQLNonNull(GraphQLInt) },
author: {
type: Author,
resolve: ({authorId}) => {
return authors.find(author => author.id === authorId)
}
}
})
})
const Author = new GraphQLObjectType({
name: 'Author',
description: 'this represents the author of a book',
fields: () => ({
id: { type: GraphQLNonNull(GraphQLInt) },
name: { type: GraphQLNonNull(GraphQLString) },
books: {
type: GraphQLList(Book),
resolve: ({id}) => {
return books.filter(book => book.authorId === id)
}
}
})
})
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
description: 'this is the root query',
fields: () => ({
book: {
type: Book,
description: 'a single book',
args: {
id: { type: GraphQLInt }
},
resolve: (_, { id }) => {
return books.find(book => book.id === id)
}
},
author: {
type: Author,
description: 'a single author',
args: {
id: { type: GraphQLInt },
},
resolve: (_, { id }) => {
return authors.find(author => author.id === id)
}
},
books: {
type: PageType(Book),
description: 'a list of books',
args: {
first: { type: GraphQLInt },
afterCursor: { type: GraphQLString }
},
resolve: (_, { first, afterCursor }) => {
let afterIndex = 0
if (typeof afterCursor === 'string') {
let nodeId = convertCursorToNodeId(afterCursor)
let nodeIndex = books.findIndex(book => book.id === nodeId)
if (nodeIndex >= 0) {
afterIndex = nodeIndex + 1
}
}
const slicedData = books.slice(afterIndex, afterIndex + first)
console.log('sliced data: ', slicedData)
const edges = slicedData.map(node => ({
node,
cursor: convertNodeToCursor(node)
}))
let startCursor = null
let endCursor = null
if (edges.length > 0) {
startCursor = convertNodeToCursor(edges[0].node)
endCursor = convertNodeToCursor(edges[edges.length - 1].node)
}
let hasNextPage = books.length > afterIndex + first
return {
totalCount: books.length,
edges,
pageInfo: {
startCursor,
endCursor,
hasNextPage
}
}
}
}
})
})
const schema = new GraphQLSchema({
query: RootQuery
})
app.use('/graphql', graphqlHTTP({
schema,
graphiql: true
}))
app.listen(3000, () => console.log('app running at http://localhost:3000/graphql'))
and I handle the pagination in another file here:
const {
GraphQLString,
GraphQLInt,
GraphQLBoolean,
GraphQLObjectType,
GraphQLList,
} = require('graphql')
const Edge = (itemType) => {
return new GraphQLObjectType({
name: 'EdgeType',
fields: () => ({
node: { type: itemType },
cursor: { type: GraphQLString }
})
})
}
const PageInfo = new GraphQLObjectType({
name: 'PageInfoType',
fields: () => ({
startCursor: { type: GraphQLString },
endCursor: { type: GraphQLString },
hasNextPage: { type: GraphQLBoolean }
})
})
const PageType = (itemType) => {
return new GraphQLObjectType({
name: 'PageType',
fields: () => ({
totalCount: { type: GraphQLInt },
edges: { type: new GraphQLList(Edge(itemType)) },
pageInfo: { type: PageInfo }
})
})
}
const convertNodeToCursor = (node) => {
// Encoding the cursor value to Base 64 as suggested in GraphQL documentation
return Buffer.from((node.id).toString()).toString('base64')
}
const convertCursorToNodeId = (cursor) => {
// Decoding the cursor value from Base 64 to integer
return parseInt(Buffer.from(cursor, 'base64').toString('ascii'))
}
module.exports = {
PageType,
convertNodeToCursor,
convertCursorToNodeId
}
Now if I copy and paste the books endpoint and change it to authors, and change the type to PageType(Author) then I get another error:
Schema must contain uniquely named types but contains multiple types named "PageType".
So this clearly isn't a solution either
You cannot have one EdgeType that contains Authors and another EdgeType that contains Books. Instead, you will need one AuthorEdge and one BookEdge type.
The same holds for the PageType - there can't be two different types with different fields but the same name.
The solution is relatively simple though - if you dynamically generated these types in a function, also name them dynamically:
const Edge = (itemType) => {
return new GraphQLObjectType({
name: itemType.name + 'Edge',
// ^^^^^^^^^^^^^^^^^^^^^^
fields: () => ({
node: { type: itemType },
cursor: { type: GraphQLString }
})
})
}
const PageInfo = new GraphQLObjectType({
name: 'PageInfo',
fields: () => ({
startCursor: { type: GraphQLString },
endCursor: { type: GraphQLString },
hasNextPage: { type: GraphQLBoolean }
})
})
const PageType = (itemType) => {
return new GraphQLObjectType({
name: itemType.name + 'sPage',
// ^^^^^^^^^^^^^^^^^^^^^^^
fields: () => ({
totalCount: { type: GraphQLInt },
edges: { type: new GraphQLList(Edge(itemType)) },
pageInfo: { type: PageInfo }
})
})
}

filter through multiple arrays and add objects from them to a single array

Example of an object in the accounts array:
const accounts = [
{
id: "5f446f2ecfaf0310387c9603",
picture: "https://api.adorable.io/avatars/75/esther.tucker#zillacon.me",
age: 25,
name: {
first: "Esther",
last: "Tucker",
},
company: "ZILLACON",
email: "esther.tucker#zillacon.me",
registered: "Thursday, May 28, 2015 2:51 PM",
},
Example of an object in the books array:
const books = [
{
id: "5f447132d487bd81da01e25e",
title: "sit eiusmod occaecat eu magna",
genre: "Science",
authorId: 8,
borrows: [
{
id: "5f446f2e2cfa3e1d234679b9",
returned: false,
},
{
id: "5f446f2ed3609b719568a415",
returned: true,
},
{
id: "5f446f2e1c71888e2233621e",
returned: true,
},
{
id: "5f446f2e6059326d9feb9a68",
returned: true,
},
{
id: "5f446f2ede05a0b1e3394d8b",
returned: true,
},
{
id: "5f446f2e4081699cdc6a2735",
returned: true,
},
{
id: "5f446f2e3900dfec59489477",
returned: true,
},
{
id: "5f446f2e6059326d9feb9a68",
returned: true,
},
{
id: "5f446f2e409f8883af2955dd",
returned: true,
},
{
id: "5f446f2e3900dfec59489477",
returned: true,
},
{
id: "5f446f2eae901a82e0259947",
returned: true,
},
{
id: "5f446f2ef2ab5f5a9f60c4f2",
returned: true,
},
{
id: "5f446f2ea6b68cf6f85f6e28",
returned: true,
},
{
id: "5f446f2eed18105706d6ca19",
returned: true,
},
{
id: "5f446f2eae901a82e0259947",
returned: true,
},
{
id: "5f446f2e91c2af00cb74e82b",
returned: true,
},
{
id: "5f446f2e5aa2bb5545a0f8a6",
returned: true,
},
{
id: "5f446f2ea508b6a99c3e42c6",
returned: true,
},
{
id: "5f446f2e50cc2da9cd80efdb",
returned: true,
},
{
id: "5f446f2e0b3e2ff72fc503e7",
returned: true,
},
{
id: "5f446f2e91c2af00cb74e82b",
returned: true,
},
{
id: "5f446f2ef795e593cd3cd19d",
returned: true,
},
{
id: "5f446f2e2f35653fa80bf490",
returned: true,
},
{
id: "5f446f2e7b9cd304fed3a8bc",
returned: true,
},
{
id: "5f446f2ed9aac23c0340aab2",
returned: true,
},
],
},
Example of objects in the authors array:
const authors = [
{
id: 0,
name: {
first: "Lucia",
last: "Moreno",
},
},
{
id: 1,
name: {
first: "Trisha",
last: "Mathis",
},
},
{
id: 2,
name: {
first: "Arnold",
last: "Marks",
},
},
I need to write the function function getBooksPossessedByAccount(account, books, authors) {} that does the following: It returns an array of books and authors that represents all books currently checked out by the given account. Look carefully at the object below, as it's not just the book object; the author object is embedded inside of it.
Output example:
getBooksPossessedByAccount(account, books, authors);
[
{
id: "5f447132320b4bc16f950076",
title: "est voluptate nisi",
genre: "Classics",
authorId: 12,
author: {
id: 12,
name: {
first: "Chrystal",
last: "Lester",
},
},
borrows: [
{
id: "5f446f2e6059326d9feb9a68",
returned: false,
},
...
],
},
]
Here's what I have so far:
function getBooksPossessedByAccount(account, books, authors) {
const accId = account.id;
const result = [];
for (let idxBks = 0; idxBks < books.length; idxBks++) {
if (
books[idxBks].borrows.id === accId &&
books[idxBks].borrows.returned === false
) {
result.push(books[idxBks]);
}
for (let idxAuth = 0; idxAuth < authors.length; idxAuth++) {
let authorIdx = authors[idxAuth];
if (authorIdx.id === result.authorId) {
return [result, { author: authorIdx }];
}
}
}
return result;
}
You need to search all the borrows, not just borrows[0]. You can use the some() method to check all of them.
Since the author information needs to be added as a property to the book object, you shouldn't be pushing it onto the booksOut array.
function getBooksPossessedByAccount(account, books, authors) {
const accId = account.id;
const booksOut = books.filter(
(book) => book.borrows.some(borrow => !borrow.returned && borrow.id === accId)
);
booksOut.forEach(book => book.author = authors.find(author => book.authorID == author.id))
return booksOut;
}
Using some should do the trick..
function getBooksPossessedByAccount(account, books, authors) {
let borrowedBooks=books.filter(book=>
book.some(borrow=>borrow.id===account.id)
)
return borrowedBooks //array of book objects
//return borrowedBooks.map(book=>book.id) //to show array of book ids
}

Filter array inside array

I have the array as below
test_list = [
{
id: 1,
test_name: 'Test 1',
members: [
{
user_id: 3
},
{
user_id: 4
}
],
},
{
id: 2,
test_name: 'Test 2',
members: [
{
user_id: 4
},
{
user_id: 5
},
],
},
{
id: 3,
test_name: 'Test 2',
members: [
{
user_id: 8
},
{
user_id: 10
},
],
}
]
I want to filter the test for specific user_id, example if user_id = 4 I would like to have this result
{
id: 1,
...
},
{
id: 2,
...
},
I have tried with this but it only return the member
test_list.filter(function(item) {
item.members.filter(function(member) {
if(member.user_id === 4) {
return item;
}
});
})
Would anyone please help me in this case?
Check if .some of the objects in the members array have the user_id you're looking for:
test_list = [{
id: 1,
test_name: 'Test 1',
members: [{
user_id: 3
},
{
user_id: 4
}
],
},
{
id: 2,
test_name: 'Test 2',
members: [{
user_id: 4
},
{
user_id: 5
},
],
},
{
id: 3,
test_name: 'Test 2',
members: [{
user_id: 8
}]
}
];
const filtered = test_list.filter(
({ members }) => members.some(
({ user_id }) => user_id === 4
)
);
console.log(filtered);
You could use .reduce() and .filter() method of array to achieve required result.
Please check below working code snippet:
const arr = [{"id":1,"test_name":"Test 1","members":[{"user_id":3},{"user_id":4}]},{"id":2,"test_name":"Test 2","members":[{"user_id":4},{"user_id":5}]},{"id":3,"test_name":"Test 2","members":[{"user_id":8}]}];
const data = arr.reduce((r,{ members,...rest }) => {
let rec = members.filter(o => o.user_id === 4)
if(rec.length){
rest.members = rec;
r.push(rest);
}
return r;
},[]);
console.log(data);
Hope this works.
var members = item.members;
var filterById =members.filter((item1)=>{
return (item1.user_id===4)
});
return filterById.length > 0;
});
console.log(test_List_by_id)```

Javascript filtering nested arrays

I'm trying to filter a on a nested array inside an array of objects in an Angular app. Here's a snippet of the component code -
var teams = [
{ name: 'Team1', members: [{ name: 'm1' }, { name: 'm2' }, { name: 'm3' }] },
{ name: 'Team2', members: [{ name: 'm4' }, { name: 'm5' }, { name: 'm6' }] },
{ name: 'Team3', members: [{ name: 'm7' }, { name: 'm8' }, { name: 'm9' }] }
];
What I'm trying to achieve is if I search for m5 for example my result should be -
var teams = [
{ name: 'Team1', members: [] },
{ name: 'Team2', members: [{ name: 'm5' }] },
{ name: 'Team3', members: [] }
];
So I've got teams and filteredTeams properties and in my search function I'm doing -
onSearchChange(event: any): void {
let value = event.target.value;
this.filteredTeams = this.teams.map(t => {
t.members = t.members.filter(d => d.name.toLowerCase().includes(value));
return t;
})
}
Now this does work to some extent however because I'm replacing the members it's destroying the array on each call (if that makes sense). I understand why this is happening but my question is what would be the best way to achieve this filter?
you were very close, the only thing that you did wrong was mutating the source objects in teams
basically you can use spread operator to generate a new entry and then return a whole new array with new values.
const teams = [
{ name: 'Team1', members: [{ name: 'm1' }, { name: 'm2' }, { name: 'm3' }] },
{ name: 'Team2', members: [{ name: 'm4' }, { name: 'm5' }, { name: 'm6' }] },
{ name: 'Team3', members: [{ name: 'm7' }, { name: 'm8' }, { name: 'm9' }] }
];
const value = 'm5';
const result = teams.map(t => {
const members = t.members.filter(d => d.name.toLowerCase().includes(value));
return { ...t, members };
})
console.log(result)
Check this. Instead of hard coded m5 pass your value.
const teams = [
{ name: 'Team1', members: [{ name: 'm1' }, { name: 'm2' }, { name: 'm3' }] },
{ name: 'Team2', members: [{ name: 'm4' }, { name: 'm5' }, { name: 'm6' }] },
{ name: 'Team3', members: [{ name: 'm7' }, { name: 'm8' }, { name: 'm9' }] }
];
const filteredTeams = teams.map(team => ({ name: team.name, members: team.members.filter(member => member.name.includes('m5')) }));
console.log(filteredTeams);
You are mutating the original objects, but you could assing new properties to the result object for mapping instead.
var teams = [{ name: 'Team1', members: [{ name: 'm1' }, { name: 'm2' }, { name: 'm3' }] }, { name: 'Team2', members: [{ name: 'm4' }, { name: 'm5' }, { name: 'm6' }] }, { name: 'Team3', members: [{ name: 'm7' }, { name: 'm8' }, { name: 'm9' }] }],
result = teams.map(o => Object.assign(
{},
o,
{ members: o.members.filter(({ name }) => name === 'm5') }
));
console.log(result);
console.log(teams);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Try to seperate your filter function first:
const filterTeamMembers = (teams, filterArr) => {
const useFilter = filterArr.map(x => x.toLowerCase());
return teams.map(team => ({
...team,
members: team.members.filter(member => useFilter.includes(member.name))
}))
};
// =========== And then:
onSearchChange(event: any): void {
let value = event.target.value;
this.filteredTeams = filterTeamMembers(this.teams, [value]);
}

Categories

Resources