Override ref value in smallrye microprofile using custom OASFilter in quarkus application - openapi

I'm trying to override ref value in schema for microprofile health check API in quarkus application. I followed the below link Smallrye open api interceptor and created a custom filter which overrides OASFilter. But, the ref value is not picking the new ref value from the filter.
#Override
public APIResponse filterAPIResponse(APIResponse apiResponse) {
if (apiResponse.getRef()=="#/components/schemas/HealthCheckResponse")
{
String ref = "#components/schemas/microProfile";
apiResponse.setRef(ref);
}
return apiResponse;
}
Basically, I need to add description to each property inside a schema.
Existing schema:
HealthCheckResponse:
type: object
properties:
data:
type: object
nullable: true
name:
type: string
status:
$ref: '#/components/schemas/HealthCheckStatus'
HealthCheckStatus:
enum:
- DOWN
- UP
type: string
Expected schema change:
microProfile:
description: microprofile response
type: object
properties:
data:
description: "Information of the service. If the service is down, this holds\
\ the information of why it is failed."
type: object
name:
description: 'Service Name '
type: string
status:
description: 'Service Status '
type: string
I added the property mp.openapi.filter= custom filter name in application.properties file. Any help is greatly appreciated.

There are 2 ways that I am aware off:
You can programmatically create your own custom schema and reference it.
In this case, the Schema is created programmatically, and does not have to exist anywhere in your openapi.yaml file.
void updateHealthStatusRefWithProgrammaticSchema(OpenAPI openAPI) {
openAPI.getPaths().getPathItems().forEach((String pathName, PathItem pathItem) -> {
if (pathName.equalsIgnoreCase("/health-check")) {
Schema dynamicSchema = OASFactory.createSchema().title("Programmatic-MicroProfile").description("dynamic-schema-description").type(Schema.SchemaType.OBJECT).properties(Map.of("custom-field-data", OASFactory.createSchema().description("Information of the service. If the service is down, this holds the information of why it is failed.").type(Schema.SchemaType.OBJECT)));
openAPI.getComponents().addSchema("Dynamic-MicroProfile", dynamicSchema);
pathItem.getGET().getResponses().getAPIResponse("200").getContent().getMediaType("application/json").setSchema(dynamicSchema);
}
});
}
You can have a defined Static Schema in your openapi.yaml which you can reference programmatically.
In this case, the schema must exist in your openapi.yaml file, as you can see we are searching for it by doing get()**
void updateHealthStatusRefByUsingStaticSchema(OpenAPI openAPI) {
openAPI.getPaths().getPathItems().forEach((String pathName, PathItem pathItem) -> {
if (pathName.equalsIgnoreCase("/health-check")) {
Schema staticMicroProfileSchema = openAPI.getComponents().getSchemas().get("Static-MicroProfile");
pathItem.getGET().getResponses().getAPIResponse("200").getContent().getMediaType(MediaType.APPLICATION_JSON).setSchema(staticMicroProfileSchema);
}
});
}
You update the openapi.yaml only if you want to have a Static Schema already defined.
openapi.yaml
components:
schemas:
HealthCheckResponse:
type: object
properties:
data:
type: object
nullable: true
name:
type: string
status:
$ref: '#/components/schemas/HealthCheckStatus'
HealthCheckStatus:
enum:
- DOWN
- UP
type: string
Static-MicroProfile:
description: microprofile response
type: object
properties:
data:
description: "Information of the service. If the service is down, this holds\
\ the information of why it is failed."
type: object
name:
description: 'Service Name '
type: string
status:
description: 'Service Status '
type: string
Filter:
package org.acme;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.microprofile.openapi.OASFactory;
import org.eclipse.microprofile.openapi.OASFilter;
import org.eclipse.microprofile.openapi.models.Components;
import org.eclipse.microprofile.openapi.models.OpenAPI;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.eclipse.microprofile.openapi.models.PathItem;
import org.eclipse.microprofile.openapi.models.examples.Example;
import io.quarkus.logging.Log;
import org.eclipse.microprofile.openapi.models.media.Schema;
import javax.ws.rs.core.MediaType;
public class CustomOASFilter implements OASFilter {
ObjectMapper objectMapper = new ObjectMapper();
#Override
public void filterOpenAPI(OpenAPI openAPI) {
//openApi.getComponents() will result in NULL as we don't have any openapi.yaml file.
Components defaultComponents = OASFactory.createComponents();
if (openAPI.getComponents() == null) {
openAPI.setComponents(defaultComponents);
}
generateExamples().forEach(openAPI.getComponents()::addExample);
updateHealthStatusRefWithProgrammaticSchema(openAPI);
}
Map<String, Example> generateExamples() {
Map<String, Example> examples = new LinkedHashMap<>();
try {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
String userJSON = new String(loader.getResourceAsStream("user.json").readAllBytes(), StandardCharsets.UTF_8);
String customerJson = new String(loader.getResourceAsStream("customer.json").readAllBytes(), StandardCharsets.UTF_8);
Example userExample = OASFactory.createExample().description("User JSON Example Description").value(objectMapper.readValue(userJSON, ObjectNode.class));
Example customerExample = OASFactory.createExample().description("Customer JSON Example Description").value(objectMapper.readValue(customerJson, ObjectNode.class));
examples.put("userExample", userExample);
examples.put("customerExample", customerExample);
} catch (IOException ioException) {
Log.error(ioException);
}
return examples;
}
void updateHealthStatusRefWithProgrammaticSchema(OpenAPI openAPI) {
openAPI.getPaths().getPathItems().forEach((String pathName, PathItem pathItem) -> {
if (pathName.equalsIgnoreCase("/health-check")) {
Schema dynamicSchema = OASFactory.createSchema().title("Programmatic-MicroProfile").description("dynamic-schema-description").type(Schema.SchemaType.OBJECT).properties(Map.of("custom-field-data", OASFactory.createSchema().description("Information of the service. If the service is down, this holds the information of why it is failed.").type(Schema.SchemaType.OBJECT)));
openAPI.getComponents().addSchema("Dynamic-MicroProfile", dynamicSchema);
pathItem.getGET().getResponses().getAPIResponse("200").getContent().getMediaType("application/json").setSchema(dynamicSchema);
}
});
}
void updateHealthStatusRefByUsingStaticSchema(OpenAPI openAPI) {
openAPI.getPaths().getPathItems().forEach((String pathName, PathItem pathItem) -> {
if (pathName.equalsIgnoreCase("/health-check")) {
Schema staticMicroProfileSchema = openAPI.getComponents().getSchemas().get("Static-MicroProfile");
pathItem.getGET().getResponses().getAPIResponse("200").getContent().getMediaType(MediaType.APPLICATION_JSON).setSchema(staticMicroProfileSchema);
}
});
}
}
application.properties:
mp.openapi.filter=org.acme.CustomOASFilter
Full-Example:
https://github.com/smustafa/quarkuks-openapi-exampleobject-loading-external-files

Related

Is there any way to get the list of validators used in moongoose schema?

I want to get the list of validators that is used in a moongoose schema? Something like
const userModel = {
firstName: {
type:String,
required: true
}
}
// is there any method to get validations like
console.log(userModel.getValidators())
Output:
{
firstName: {required: true ....etc},
}
Once you setup your model using the SchemaFactory.createForClass method from a class with a #Schema decorator as shown in the docs, you can export the schema. If you then, import the schema and access its obj property, you can extract information about the field.
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import { Document } from 'mongoose';
export type CatDocument = Cat & Document;
#Schema()
export class Cat {
#Prop({ required: true })
name: string;
#Prop()
age: number;
#Prop()
breed: string;
}
export const CatSchema = SchemaFactory.createForClass(Cat);
import { CatSchema } from './cat.schema';
console.log(CatSchema.obj.name.required); // true
console.log(CatSchema.obj.age.type.name); // 'Number'

typeOrm does not create entries in schema

I am using nestjs with typeOrm with Postgresql.
I am looking to save all data from the app in a different schema to 'public' as this is well known.
I have schema specified in an options file as below:
import { TypeOrmModuleOptions } from '#nestjs/typeorm';
export const typeOrmConfig: TypeOrmModuleOptions = {
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'xxxx',
password: 'xxx',
database: 'taskmanagement',
autoLoadEntities: true,
synchronize: true,
schema: 'sh',
};
I create the task as below:
async createTask(createTaskDto: CreateTaskDto): Promise<Task> {
// create directly using the entity
const { title, description } = createTaskDto;
const task = new Task();
task.title = title;
task.description = description;
task.status = TaskStatus.OPEN;
await task.save();
return task;
}
The Task entity is defined as:
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { TaskStatus } from './task-status.enum';
#Entity()
export class Task extends BaseEntity {
// #PrimaryGeneratedColumn('uuid')
// id: string;
#PrimaryGeneratedColumn()
id: number;
#Column()
title: string;
#Column()
description: string;
#Column()
status: TaskStatus;
}
The record is saved to the schema 'public', not my custom schema 'sh'.
I would expect typeOrm to use the schema from the config given that it writes to DB specified in the config options file.
Is this a bug with nestjs/typeOrm or am I missing a setting?
Run build script - this seems to get the revised config file to be used.

NestJS with mongoose schema, interface and dto approach question

I am new into nestJS and mongoDB and its not clear for me why do we need to declare DTO, schema and interface for each collection we want to save in our mongoDB. IE. I have a collection (unfortunately I've named it collection but it does not matter) and this is my DTO:
export class CollectionDto {
readonly description: string;
readonly name: string;
readonly expiration: Date;
}
interface:
import { Document } from 'mongoose';
export interface Collection extends Document {
readonly description: string;
readonly name: string;
readonly expiration: Date;
}
and schema:
import * as mongoose from 'mongoose';
export const CollectionSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
description: {
type: String,
required: false,
},
expiration: {
type: String,
required: true,
}
});
My doubt is that do we really need as many as three objects with almost the same contents? It looks strange at first sight.
I was working with mongoose a lot in plain nodejs basis and as well I'm starting to work with NestJS. Mongoose defines two things so that you can use mongodb to create, query, update and delete documents: Schema and Model. You already have your schema, and for model in plain mongoose should be as:
import * as mongoose from 'mongoose';
export const CollectionSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
description: {
type: String,
required: false,
},
expiration: {
type: String,
required: true,
}
});
const Collection = mongoose.model('collections', CollectionSchema);
Collection here will be mongoose model. So far so good.
In NestJs, and if you are going to follow API best practices, you will use a DTO (Data Transfer Object). NestJs in doc mention that is preferable to use classes than interfaces, so you don't need interfaces here. When you define Mongoose schema, you can also define Model/Schema:
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import { Document } from 'mongoose';
export type CollectionDocument = Collection & Document;
#Schema()
export class Collection {
#Prop()
name: string;
#Prop()
description: number;
#Prop()
expiration: string;
}
export const CollectionSchema = SchemaFactory.createForClass(Collection);
And for your services and controllers you use both (model and DTO):
import { Model } from 'mongoose';
import { Injectable } from '#nestjs/common';
import { InjectModel } from '#nestjs/mongoose';
import { Collection, CollectionDocument } from './schemas/collection.schema';
import { CollectionDto } from './dto/collection.dto';
#Injectable()
export class CollectionService {
constructor(#InjectModel(Collection.name) private collectionModel: Model<CollectionDocument>) {}
async create(createColDto: CollectionDto): Promise<Collection> {
const createdCollection = new this.collectionModel(createColDto);
return createdCollection.save();
}
async findAll(): Promise<Collection[]> {
return this.collectionModel.find().exec();
}
}
After this, you can user Swagger to automatic doc of your APIs.
NestJS Mongo Techniques

Why DTOs are not throwing Validation error in nestjs?

I am using DTO in my code, and I am getting the response as expected but in code DTOs are not throwing error for example
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}
In this name, age, the breed is a required field and each has their data type but while running on the postman when I am not passing all the required field or only one field into postman body I am not getting any errors like age is required if I have passed other two fields or I have given value of the parameter not according to data type like:- age : twenty five then also it should throw error but I am not getting.
So, This is class created for
import { ApiProperty } from '#nestjs/swagger';
export class Cat {
#ApiProperty({ example: 'Kitty', description: 'The name of the Cat' })
name: string;
#ApiProperty({ example: 1, description: 'The age of the Cat' })
age: number;
#ApiProperty({
example: 'Maine Coon',
description: 'The breed of the Cat',
})
breed: string;
}
This is controller in which I am importing class and Dto.
import { Body, Controller, Get, Param, Post } from '#nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
ApiResponse,
ApiTags,
} from '#nestjs/swagger';
import { CatsService } from './cats.service';
import { Cat } from './classes/cat.class';
import { CreateCatDto } from './dto/create-cat.dto';
#ApiBearerAuth()
#ApiTags('cats')
#Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
#Post()
#ApiOperation({ summary: 'Create cat' })
#ApiResponse({ status: 403, description: 'Forbidden.' })
async create(#Body() createCatDto: CreateCatDto): Promise<Cat> {
return this.catsService.create(createCatDto);
}
}
I don't know why you selected nestjs-swagger tag, DTO by itself will not validate inputs, maybe you need to use a ValidationPipe with the class-validator package as suggested on docs https://docs.nestjs.com/techniques/validation#validation
It's as simple as putting a decorator on your code now:
import { IsEmail, IsNotEmpty } from 'class-validator';
export class CreateCatDto {
#IsNotEmpty()
#IsString()
readonly name: string;
#IsNotEmpty()
#IsInt()
readonly age: number;
#IsNotEmpty()
readonly breed: string;
You can see all the items here: https://github.com/typestack/class-validator#validation-decorators
And if you want to sanitize the request body, you should use a serializer to help:
https://docs.nestjs.com/techniques/serialization#serialization
This will show or hide your DTO properties based on decorators of each field. You need to install class-transformer package.
import { Exclude } from 'class-transformer';
export class UserEntity {
id: number;
firstName: string;
lastName: string;
#Exclude()
password: string;
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial);
}
}
It's important to remember that interceptors will run on your request and response.

angular2-mdl table component with server side data

I experiment with Angular 2 - Material Design Lite especially with the table component but I can not figure out how would I pass data from server on ajax request. Here is the example provided for table initialisation.
How would I pass data from restAPI to table component?
Here I have a kind of working example. I placed the initial data on my Component Init method where I call the DataService which populates the table. I'm not sure if is the right workaround but at this point I have data in table.
import { Component, ViewChild, ViewContainerRef, OnInit, Pipe, PipeTransform } from '#angular/core';
import { MdDialog, MdDialogConfig, MdIcon } from "#angular/material";
import { AuthenticationService, DialogsService, DataService } from '../../../services/';
import { RouterModule, Routes, Router } from '#angular/router';
import {
IMdlTableModelItem,
MdlDefaultTableModel
} from 'angular2-mdl';
export interface ITableItem extends IMdlTableModelItem {
username: string;
email: string;
role: string;
unitPrice: number;
}
#Component({
selector: 'employees',
templateUrl: 'app/layouts/secure/employees/employees.html',
providers: [DialogsService, MdIcon]
})
export class EmployeesComponent implements OnInit {
public message: string;
public employees: any[];
public result: any;
public showSearchBar: false;
public tableData:[ITableItem];
public selected;
public tableModel = new MdlDefaultTableModel([
{key:'username', name:'Username', sortable:true},
{key:'email', name:'Email', sortable:true},
{key:'role', name:'Role', sortable:true},
{key:'status', name:'Status', sortable:true},
{key:'unitPrice', name:'Test', numeric:true}
]);
constructor(
private dialogsService: DialogsService,
public viewContainerRef: ViewContainerRef,
private _dataService : DataService,
private router: Router
) {
}
openDialog() {
this.dialogsService
.confirm('User Form', 'Are you sure you want to do this?', this.viewContainerRef)
.subscribe(res => this.result = res);
}
toggleSearch() {
console.log(this)
}
ngOnInit() {
var self = this;
this._dataService
.GetAll('employees')
.subscribe( data => {
data = Object.keys(data).map((key)=>{ return data[key]})
this.employees = data;
this.tableData = data;
this.tableModel.addAll(this.tableData);
}, error => console.log(error),
() => function ( data ) {
this.tableData = this.employees;
this.tableModel.addAll(this.tableData);
this.selected = this.tableData.filter( data => data.selected);
},
);
}
generateArray(obj){
return Object.keys(obj).map((key)=>{ return obj[key]});
}
selectionChanged($event){
this.selected = $event.value;
}
}
#fefe made it a little more difficult than it had to be, at least with the current version. The magic of the as keyword can do the heavy lifting.
For example my class setup looks like:
import...
export interface IUnreadMessage extends IMdlTableModelItem {
messageId: number;
subject: string;
from: string;
}
#Component ...
export class ...
private unreadMessagesTable = new MdlDefaultTableModel([
{key: 'messageId', name: 'Message ID'},
{key: 'subject', name: 'Subject'},
{key: 'from', name: 'From'}
]);
Then in my ajax call I have:
...ajax call here).subscribe(value => {
const messages = value as Array<IUnreadMessage>;
this.unreadMessagesTable.addAll(messages);
},
error => {
...error handler here...
});
Make sure your interface is EXACTLY (including case) the same as your returned ajax data and it should hook right up!