Is it necessary to use multiple Joi schema validations with Hapi? - javascript

I looked at an existing codebase and and I noticed that the codebase had two schema validations for what I feel a single schema could validate as the second schema is an offshoot of the first.
See codebase below.
export const StudentSchema = Joi.object().keys({
_id,
name,
dob,
gender,
grade
});
export const StudentUpdateSchema = Joi.object().keys({
name,
dob,
})
Now these schema are being used in the following routes:
//CREATE ROUTE
{
method: 'POST',
path: '/students/{id}',
handler: async (request) => {
const { id } = request.params;
return Student.create(id, request.payload);
},
options: {
...sharedOptions,
description: 'Enrolling a student',
validate: {
failAction,
payload: StudentSchema,
params: {
studentId: Joi.objectId().required()
},
},
response: {
status: {
200: StudentSchema,
400: Joi.any(),
},
},
},
},
// UPDATE ROUTE
{
method: 'PUT',
path: '/students/{id}',
handler: async (request) => {
const { id } = request.params;
return Student.update(Id, { $set: request.payload });
},
options: {
...sharedOptions,
description: 'Update student details',
validate: {
failAction,
payload: StudentUpdateSchema,
params: {
studentId: Joi.objectId().required(),
},
},
response: {
status: {
200: StudentSchema,
400: Joi.any(),
404: Joi.any(),
},
},
},
}
I am new to Hapi but have some experience with Express and Mongoose and I am inclined to re-writing these validations to use ONLY the StudentSchema in both the 'POST' and 'PUT' method since the StudentSchema contains all that is required to handle both the create and update functionality.
Can anyone with experience in HapiJS validate the pros/cons of using a single schema object (in this case StudentSchema) or should I continue the current paradigm of using the two schemas?

I would argue this design pattern is the better practice, On a partial update you should have a fitting schema to represent the incoming object better. However you can avoid the two schemas while preserving the concept behind it by extending the first one using optionalKeys.
const createSchema = Joi.object().keys({
_id: Joi.objectId(),
name: Joi.string().required(),
dob: Joi.string().required(),
gender: Joi.string().required(),
grade: Joi.number().required()
});
const updateSchema = createSchema.optionalKeys("gender", "grade", "_id");
Joi.validate({name: "this fails"}, createSchema); // error: required fields missing
Joi.validate({name: "this works"}, updateSchema);
This way you have a full schema that protects you while also allowing a partial field update.

Related

How to save an array of strings in mongodb

I have found a few similar questions on stack overflow like this one:
How to save array of Strings in Node Mongodb
Mongoose - Save array of strings
but I cant figure out why my method is not working
I am trying to save the string of arrays "jobType".
context: I am creating an app where people can post jobs.
each job can have multiple types.
here is my job model::
const mongoose = require("mongoose");
const postSchema = mongoose.Schema({
content: { type: String, required: true },
imagePath: { type: String, required: true },
state: { type: String, required: true },
substate: { type: String, required: true },
address: { type: String, required: true },
jobType: { type: [String] },
creator: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }
});
module.exports = mongoose.model("Job", postSchema);
this is the API used to save the data on MongoDB:
I am 100% sure that the data is getting to the API correctly.
the parameter "req.body.jobtype" contains all the info as a string.
I am trying to use JSON.parse to change the string into an array but its not working.
when I check MongoDB, an empty array is being stored
const Job = require("../models/job");
exports.createJob = (req, res, next) => {
console.log('create job api hit')
const url = req.protocol + "://" + req.get("host");
const post = new Job({
content: req.body.content,
imagePath: url + "/images/" + req.file.filename,
creator: req.userData.userId,
state: 'new',
substate: 'new',
address: req.body.address,
jobtype: JSON.parse(req.body.jobtype) // fix: not storing correctly
});
post
.save()
.then(createdJob => {
res.status(201).json({
message: "Job added successfully",
post: {
...createdJob,
'pecker':'pecker hecks out',
id: createdJob._id
}
});
})
.catch(error => {
res.status(500).json({
message: JSON.stringify(error)
});
});
};
You have a typo. In your model, you defined jobType property, but when saving the data, you are passing it as jobtype.
So, instead of this:
jobtype: JSON.parse(req.body.jobtype)
Do this:
jobType: JSON.parse(req.body.jobtype)

How I make a list of documents with mongoose? (Schema)

I don't know how to make a list of documents with mongoose (Node JS), someone know's how to make that?
new mongoose.Schema({
first_name: String,
last_name: String,
companies: [
{
name_company:String,
post:String,
time_in_post:String,
}
]
});
I need to insert many documents with companies schema at different times but I don't know how to make that with mongoose.
Can someone help me?
Seems like splitting this into different schemas might be a good idea. Especially if other people will be apart of the same company.
For instance, maybe you could try this:
const CompanySchema = new mongoose.Schema({
name: String,
// ... other company specific attributes
});
const Company = mongoose.model("Company", CompanySchema);
const UserSchema = new mongoose.Schema({
firstName: String,
lastName: String,
companies: [
{
company: { type: mongoose.Schema.Types.ObjectId, ref: 'Company' },
post: String,
timeInPost: String,
}
]
});
const User = mongoose.model("User", UserSchema);
And the interaction with the models could look like this:
(async () => {
try {
const company1 = await Company.create({ name: "Company 1" });
const company2 = await Company.create({ name: "Company 2" });
const user1 = await User.create({
firstName: "first",
lastName: "last",
companies: [
{
company: company1._id,
post: "....",
timeInPost: "....",
},
{
company: company2._id,
post: "....",
timeInPost: "....",
},
],
});
const users = await User.find({}).populate("companies.company"); // in order to populate the company with the document from the company collection
// users with companies info
} catch (error) {
console.log(error);
}
})();
Referenced from the mongoose documentation: https://alexanderzeitler.com/articles/mongoose-referencing-schema-in-properties-and-arrays/
You could use this approach, with this way you avoid data duplication:
const companySchema = new mongoose.Schema({
name_company: String,
post: String,
time_in_post: String
});
new mongoose.Schema({
firstName: String,
lastName: String,
companies: [companySchema] // Array of subdocuments
});
See full documentation here: https://mongoosejs.com/docs/guide.html

Joi validation multiple AND conditions

I'm trying to validate my target object with following conditions:
if (target.company === `GP` AND one of target.documents type equals `full`) {
one of target.documents type must equals `part-should-be-error`
} else {
throw new Error()
}
In this example, validation doesn't return any errors, but it should, because of 'part' !== 'part-should-be-error'
I tried https://stackoverflow.com/a/53647449/10432429 but it doesn't work with Joi v15
Since I can't merge array schema with alternative schema, all that I can to do is use $ to get global context, but seems that it doesn't work too
I have codebase on Joi v15.1.1, so please install same version
npm i #hapi/joi#15.1.1
const Joi = require('#hapi/joi');
(async () => {
const target = {
name: 'Jeff',
company: 'GP',
documents: [
{type: 'full', value: 'apple'},
{type: 'part', value: 'tesla'},
],
};
const documents = Joi.object().keys({
type: Joi.string().valid(['full', 'part', 'empty']),
value: Joi.string().min(1).max(40)
.required(),
}).required();
const schema = Joi.object()
.keys({
name: Joi.string().min(1).max(20)
.required(),
company: Joi.string().valid(['GP', 'QW']),
documents: Joi.array().items(documents).min(1)
.when('$', {
is: Joi.object().keys({
company: Joi.string().valid(['GP']),
documents: Joi.array().has(Joi.object({type: Joi.string().valid('full')}).unknown().required()),
}).unknown().required(),
then: Joi.array().has(Joi.object({type: Joi.string().valid(['part-should-be-error'])}).unknown()).required(),
})
.required(),
});
await Joi.validate(target, schema, {context: target});
})();
If I do sth wierd, please feel free to show another way how to solve this

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.

Modify GraphQLObjectType fields at runtime

Assume I have the following code as graphql schema. A userType including id and name for users, and there's two kind of queries: allUsers: [userType] and user(id: Int!): userType.
let db = [{
id: 1,
name: 'Amir'
}, {
id: 2,
name: 'John'
}];
const userType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLInt },
name: { type: GraphQLString }
}
});
const queryType = new GraphQLObjectType({
name: 'Query',
fields: {
allUsers: {
type: new GraphQLList(userType),
resolve: () => db
},
user: {
type: userType,
args: {
id: { type: new GraphQLNonNull(GraphQLInt) }
},
resolve: (_, { id }) => db.find(user => user.id == id);
}
}
})
let schema = new GraphQLSchema({ query: queryType });
I need to modify this structure at boot time. I mean before actually executing the last line.
To add more kind of queries, I deferred the schema creation (new GraphQLSchema(...)) to the end, after all the modifications are done. So I can add more fields to the query itself or perhaps modify existing ones.
But how can I modify the types that are already defined? Basically, I need to add other fields to userType, like permissions, which itself is a GraphQLObjectType and has its own resolve function.
Something like this:
let queryFields = {};
const userType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLInt },
name: { type: GraphQLString }
}
});
queryFields['allUsers'] = {
type: new GraphQLList(userType),
// ...
}
queryFields['user'] = {
type: userType,
//...
}
/* HERE <---------------------------------------- */
userType.fields.permission = {
type: GraphQLString,
resolve: user => getPermissionsFor(user);
}
const queryType = new GraphQLObjectType({
name: 'Query',
fields: queryFields
})
var schema = new GraphQLSchema({ query: queryType });
Thanks!
What I have done at the end is to add another layer between the logic of my app and GraphQL. So I have created another library that holds the information about the schema and the types, and it has an API to modify the existing types in the schema. Once all the modifications are in place, we can extract a GraphQL schema from the library.
That's the whole idea. For implementation detail, I have wrote a post here: Distributed Schema Creation in GraphQL

Categories