Loopback 4 How to validate requestBody properties - node.js

I'm looking for a way to prevent unwanted properties to be present in the requestBody as described in the associated Model
Here is my model :
import { Model, model, property } from '#loopback/repository';
#model({
name: 'AwsS3',
strict: true,
description: 'AWS S3 Object description',
properties: {
Key: {
type: 'String',
required: 'true',
},
Bucket: {
type: 'String',
requied: 'true',
},
},
})
export class AwsS3 extends Model {
#property({
type: 'string',
description: 'path/to/file',
required: true,
}) Key: string;
#property({
type: 'string',
description: 'AWS-S3-Bucket-Name',
required: true,
})
Bucket: string;
constructor(data: AwsS3) {
super(data);
}
}
I used it like this in the controller
function(#requestBody({
required: true,
description: 'aws object settings',
content: {
'application/json': {},
},
}) body : AwsS3
){
console.log(body);
}
It throws correctly when one of both properties is missing or in the wrong type.
But if i send a json like bellow nothing is thrown and object is processed with the UnwantedProp
{
Key: 'key',
Bucket : 'bucket',
UnwantedProp: 40
}

I found it to be achievable by using the #api decorator and setting the additionalProperties: false from the openapi specs.
use it like :
#api(
basePath: '/',
paths : {
'somepath': {
'post' : {
'x-operation-name': 'myfunction',
'x-controller-name': 'MyController',
// properties for route
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
additionalProperties: false, // <=== here it is
properties: {
Key: { type: 'string'},
Bucket: {type: 'string'},
},
required: ['Bucket', 'Key'],
},
},
},
},
}
}
}
)
export class MyController{
async myfunction(
#requestBody({ settings: { strict: true } }) body
){}
}
when testing it throws the following as expected :
{
"error": {
"statusCode": 422,
"name": "UnprocessableEntityError",
"message": "The request body is invalid. See error object `details` property for more info.",
"code": "VALIDATION_FAILED",
"details": [
{
"path": "",
"code": "additionalProperties",
"message": "should NOT have additional properties",
"info": {
"additionalProperty": "unwantedProp"
}
}
]
}
}

Related

MongoDB Document Validation always fails

I am trying to add in validation on my MongoDB server and everything I give it fails document validation.
The most common error I keep getting:
details: { operatorName: '$and', clausesNotSatisfied: [Array] }
The validation logic I'm using:
{
title: 'Facebook Conversation',
type: 'object',
properties: {
participants: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string'
}
},
required: [
'name'
],
additionalProperties: true
}
},
messages: {
type: 'array',
items: {
type: 'object',
properties: {
sender_name: {
type: 'string'
},
timestamp_ms: {
type: 'number'
},
content: {
type: 'string'
},
type: {
type: 'string'
},
photos: {
type: 'array',
items: {
type: 'object',
properties: {
uri: {
type: 'string'
},
creation_timestamp: {
type: 'number'
}
},
required: [
'uri',
'creation_timestamp'
]
}
},
ip: {
type: 'string'
},
sticker: {
type: 'object',
properties: {
uri: {
type: 'string'
}
}
},
payment_info: {
type: 'object',
properties: {
amount: {
type: 'number'
},
currency: {
type: 'string'
},
creationTime: {
type: 'number'
},
completedTime: {
type: 'number'
},
senderName: {
type: 'string'
},
receiverName: {
type: 'string'
}
}
},
call_duration: {
type: 'number'
},
missed: {
type: 'boolean'
},
gifs: {
type: 'array',
items: {
type: 'object',
properties: {
uri: {
type: 'string'
}
},
required: [
'uri'
]
}
},
share: {
type: 'object',
properties: {
link: {
type: 'string'
},
share_text: {
type: 'string'
}
}
},
videos: {
type: 'array',
items: {
type: 'object',
properties: {
uri: {
type: 'string'
},
creation_timestamp: {
type: 'number'
},
thumbnail: {
type: 'object',
properties: {
uri: {
type: 'string'
}
}
}
},
required: [
'uri',
'creation_timestamp',
'thumbnail'
]
}
},
reactions: {
type: 'array',
items: {
type: 'object',
properties: {
reaction: {
type: 'string'
},
actor: {
type: 'string'
}
},
required: [
'reaction',
'actor'
]
}
},
audio_files: {
type: 'array',
items: {
type: 'object',
properties: {
uri: {
type: 'string'
},
creation_timestamp: {
type: 'number'
}
},
required: [
'uri',
'creation_timestamp'
]
}
},
users: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string'
}
},
required: [
'name'
]
}
},
files: {
type: 'array',
items: {
type: 'object',
properties: {
uri: {
type: 'string'
},
creation_timestamp: {
type: 'number'
}
}
}
}
},
required: [
'sender_name',
'timestamp_ms',
'content'
]
}
},
title: {
type: 'string'
},
is_still_participant: {
type: 'boolean'
},
thread_type: {
type: 'string'
},
thread_path: {
type: 'string'
}
}
}
An example of a JSON file that fails document validation:
{
"participants": [
{
"name": "Person"
},
{
"name": "Me"
}
],
"messages": [
{
"sender_name": "Person",
"timestamp_ms": 1550000006885,
"content": "Message content",
"type": "Generic"
}
],
"title": "Person",
"is_still_participant": true,
"thread_type": "Regular",
"thread_path": "inbox/Person"
}
Typescript code I'm using:
apiRouter.post("/", async (req: any, res: any) => {
res.setHeader("Content-Type", "application/json; charset=utf-8");
try {
let newConversation = JSON.stringify(req.body);
const result = await collections.conversations?.insertOne(newConversation);
result
? res.status(201).send(`Successfully created a new conversation`)
: res.status(500).send("Failed to create a new conversation.");
} catch (error) {
if (error instanceof Error) {
console.error(error);
res.status(400).send(error.message);
} else {
console.log('Unexpected error', error);
}
}
});
If I use JSON.stringify on the req.body, I get this error:
Cannot create property '_id' on string
So I'm really confused as to what I'm doing wrong, I actually allowed JSON to pass without validation and then exported the schema in Compass and used that as validation logic and that doesn't work as well. But maybe that's not meant to be used as validation logic and that's why. But if anybody could help me figure this out, I'd appreciate it.
I figured it out, when I added the JSON as validation in MongoDB, I used Compass and it didn't accept the $schema tag and when I removed it, it allowed me to apply it, so I assumed I entered everything correctly.
However I was looking around MongoDB's website to see examples of JSON schemas and I noticed they enclosed it with $jsonSchema: {}, they were using the shell so I assumed that's why. But I decided to try it and finally documents are successfully validating!

Fastify schema fails to validate type of a property of a nested object

I am new in using Fastify Js. I have this route that fails to validate the type of properties inside the nested object from inside the body schema.
fastify.put('/comment', {
schema: {
body: {
type: 'object',
required: ['from', 'body', 'courseCode'],
properties: {
from: {
type: 'object',
required: ['email', 'username'],
properties: {
// this is the part where validation fails
email: { type: 'string' },
username: { type: 'string' },
}
},
body: { type: 'string' },
courseCode: { type: 'string' },
}
}
}
}, async (req, rep) => {
// logic
return {someResponse};
});
I am using REST Client extension in VSCode. This is my sample request
put http://localhost:3000/comment
Content-Type: application/json
{
"from": {
/* Notice the `email` and `username`, Fastify does not throw an error or some exception
that tells this is suppose to be a string and not an integer */
"email": 3123,
"username": 123123
},
"body": "some comment from enginex",
"courseCode": "d-c"
}
I also try using insomia.io and postman but the result is the same.
Did I miss something important here? Thank You so much.

Fastify calling controller method multiple times when multiple preHandler hook is called

I have a fastify route method with the following schema.
fastify.post('/club', createClubSchema, createClub(fastify));
const createClubSchema = {
schema: {
tags: ['club'],
security: [
{
ApiKeyAuth: [],
},
],
body: {
type: 'object',
required: ['name', 'description'],
properties: {
name: { type: 'string', minLength: 3 },
description: { type: 'string', minLength: 3 },
logoUrl: { type: 'string', minLength: 3 },
},
},
response: {
200: {
type: 'object',
properties: {
status: { type: 'string' },
data: {
type: 'object',
properties: {
name: { type: 'string' },
description: { type: 'string' },
id: { type: 'number' },
color: { type: 'string' },
logoUrl: { type: 'string' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
},
},
},
},
},
preHandler: [grantAccess('create', 'club')],
};
Now this route is in folder where there is autohooks.js is located that has a prehandler hook of it's own which checks whether the request has token in it for authentication purposes.
The problem is after the preHandler hook is called on both the area the createClub controller method is called, so a total of 2 times.
What is the issue? and how can i solve this?
EDIT
This is the autoHooks.js file
const authentication = require('../../middlewares/authentication');
module.exports = async function (fastify, opts, next) {
fastify.addHook('preHandler', authentication.authenticate(fastify));
};

Node.js - Swagger - Unable to render this definition

I am following this toutorial: https://github.com/codeBelt/open-api-documentation/blob/master/src/openApiDocumentation.js
Can I validate my openApiDocumentation.js file somewhow? I get:
Unable to render this definition
The provided definition does not specify a valid version field.
Please indicate a valid Swagger or OpenAPI version field. Supported version fields are swagger: "2.0" and those that match openapi: 3.0.n (for example, openapi: 3.0.0).
I am attaching mi .js file. Maybe you guys will see a typo here. Thanks in advance.
.js file:
const USER_TYPES = {
EXCHANGE: 'xxx',
GIVEAWAY: 'xxx'
}
const openApiDocumentation = {
openapi: '3.0.1',
info: {
version: '1.3.0',
title: 'xxx',
description: 'xxx',
contact: {
name: 'xxx',
email: 'xxx',
}
},
license: {
name: 'Apache 2.0',
url: 'https://www.apache.org/licenses/LICENSE-2.0.html',
},
servers: [
{
url: 'http://localhost:4000/',
description: 'Local server',
},
],
tags: [
{
name: 'Books CRUD operations',
},
],
paths: {
'/findAllBooks': {
get: {
tags: ['CRUD operations'],
description: 'Get all Book offers',
operationId: 'getUsers',
parameters: [
{
name: 'page',
in: 'query',
schema: {
type: 'integer',
default: 1,
},
required: true,
description: 'Page numer used pagination.',
},
],
responses: {
'200': {
description: 'Books were obtained',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Books',
},
},
},
},
'500': {
description: 'Missing parameters',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error',
},
example: {
message: 'page qyery parameter is missing',
internal_code: 'missing_parameters',
},
},
},
},
},
},
},
},
components: {
schemas: {
coverImg: {
type: 'string',
example: 'http:',
},
image: {
type: 'string',
example: 'http',
},
category: {
type: 'string',
example: 'Crafts & Hobbies',
},
linkTypes: {
type: 'object',
properties: {
coverImg: {
$ref: '#/components/schemas/coverImg',
},
images: {
type: 'array',
items: {
$ref: '#/components/schemas/image',
},
}
}
},
offerID: {
type: 'string',
example: '27301927',
},
userID: {
type: 'string',
example: 'efdc5192',
},
title: {
type: 'string',
example: 'Quilting For Dummies',
},
description: {
type: 'string',
example: 'You ',
},
categories: {
type: 'array',
items: {
$ref: '#/components/schemas/category',
},
},
links: {
type: 'object',
items: {
$ref: '#/components/schemas/linkTypes',
},
},
offerType: {
type: 'string',
enum: USER_TYPES,
default: USER_TYPES.EXCHANGE,
},
Book: {
type: 'object',
properties: {
offerID: {
$ref: '#/components/schemas/offerID',
},
userID: {
$ref: '#/components/schemas/userID',
},
title: {
$ref: '#/components/schemas/title',
},
description: {
$ref: '#/components/schemas/description',
},
categories: {
$ref: '#/components/schemas/categories',
},
imageLinks: {
$ref: '#/components/schemas/links',
},
offerType: {
$ref: '#/components/schemas/offerType',
},
},
},
Books: {
type: 'object',
properties: {
users: {
type: 'array',
items: {
$ref: '#/components/schemas/Book',
},
},
},
},
Error: {
type: 'object',
properties: {
message: {
type: 'string',
},
internal_code: {
type: 'string',
},
},
},
},
},
};
There are few mistakes,
license must be inside the info object
info: {
version: '1.3.0',
title: 'xxx',
description: 'xxx',
contact: {
name: 'xxx',
email: 'xxx' // make sure you have used valid email address!
},
license: {
name: 'Apache 2.0',
url: 'https://www.apache.org/licenses/LICENSE-2.0.html'
}
}
enum will allow only array not an object and here you have passed object USER_TYPES, corrected below:
const USER_TYPES = {
EXCHANGE: 'xxx',
GIVEAWAY: 'xxx'
};
const USER_TYPES_ENUM = [
USER_TYPES.EXCHANGE,
USER_TYPES.GIVEAWAY
];
offerType: {
type: 'string',
enum: USER_TYPES_ENUM,
default: USER_TYPES.EXCHANGE,
},
For best practice use https://editor.swagger.io/ (also they have provided a option to convert json to yaml under Edit > Convert to YAML)!

Loopback.js - How do I pass multiple parameters to model remote method

I'm trying to change a remote method for the Idioma model which only had one parameter (id). This is is the actual functioning code:
Model.remoteMethod('idiomaByOferta', {
description: 'Obtener un idioma por oferta',
http: { path: '/oferta/:id', verb: 'get' },
accepts: [
{ arg: 'id', type: 'string', required: true },
{ arg: 'res', type: 'object', http: { source: 'res' } }
],
returns: {
type: 'Object',
root: true,
default: output_structure
}
});
Model.idiomaByOferta = (id, res, cb) => {
parameterValidatorId(id, err => {
if (err) {
res.status(httpStatus.BAD_REQUEST.code).send(err);
}
});
const conn = Model.app.datasources.db.connector;
commons
.getResultSqlString(conn, sqlEstablecimiento.findIdiomas, [id])
.then(stb => {
cb(null, stb);
})
.catch(err => cb(err, null));
};
Model.afterRemote('idiomaByOferta', async (ctx, result, next) => {
delete ctx.res.req.query.limit;
delete ctx.res.req.query.page;
delete query.limit;
delete query.page;
next();
});
Now I want to include another parameter but I haven't found exactly how to do it with required parameters. I have tried the following but it doesn't work:
Model.remoteMethod('idiomaByOferta', {
description: 'Obtener un idioma por oferta',
http: { path: '/oferta', verb: 'get' },
accepts: [
{ arg: 'id', type: 'string', required: true, http: { source: 'query' }},
{ arg: 'nivel', type: 'string', required: true, http: { source: 'query' }},
{ arg: 'res', type: 'object', http: { source: 'res' } }
],
returns: {
type: 'Object',
root: true,
default: output_structure
}
});
Request url: {{url}}/api/idiomas/oferta?id={{oferta}}&nivel=Inicial
Response:
{
"errors": [
{
"code": 938,
"source": "id",
"detail": "is not allowed"
},
{
"code": 963,
"source": "nivel",
"detail": "is not allowed"
}
]
}
I have also tried doing this:
Model.remoteMethod('idiomaByOferta', {
description: 'Obtener un idioma por oferta',
http: { path: '/oferta/:id/nivel/:nivel', verb: 'get' },
accepts: [
{ arg: 'id', type: 'string', required: true},
{ arg: 'nivel', type: 'string', required: true},
{ arg: 'res', type: 'object', http: { source: 'res' } }
],
returns: {
type: 'Object',
root: true,
default: output_structure
}
});
The request times out and is never completed.
The position of your accepts attributes is important.
In your http attribute, the argument path is optionnal and is useful if you want to change the order of your accepts attributes or simply modify the path name.
What I would do is:
Model.remoteMethod('idiomaByOferta', {
description: 'Obtener un idioma por oferta',
http: { path: '/oferta/:id/:nivel', verb: 'get' },
accepts: [
{ arg: 'id', type: 'string', required: true },
{ arg: 'nivel', type: 'string', required: true},
{ arg: 'res', type: 'object', http: { source: 'res' } }
],
returns: {
type: 'Object',
root: true,
default: output_structure
}
});
Model.idiomaByOferta = (id, nivel, res, cb) => { //add nivel here in second position
parameterValidatorId(id, err => {
if (err) {
res.status(httpStatus.BAD_REQUEST.code).send(err);
}
});
parameterValidatorId(nivel, err => {
if (err) {
res.status(httpStatus.BAD_REQUEST.code).send(err);
}
});
const conn = Model.app.datasources.db.connector;
commons
.getResultSqlString(conn, sqlEstablecimiento.findIdiomas, [id, nivel]) //and use it there, maybe, depending on what your code is doing?
.then(stb => {
cb(null, stb);
})
.catch(err => cb(err, null));
};

Resources