I am writing a NestJS application. Some of the endpoints support sorting e.g. http://127.0.0.1:3000/api/v1/members?sort=-id&take=100 Which means sort by id descending.
This parameter arrives as a #Query parameter and is passed to my service. This service transforms it into an object which is used by TypeORM:
{
id: 'DESC'
}
I don't want to call this conversion method manually every time I need sorting.
I've tried an intercepter but this one could not easily change the request parameters into the desired object.
A pipe worked but then I still need to add #Query(new SortPipe()) for every endpoint definition.
Another option is in the repository itself. The NestJS documentation is very well written, but misses guidance in where to put what.
Is there someone who had a similar issue with converting Query parameters before they are used in NestJS, and can explain what approach is the best within NestJS?
This question might look like an opinion based question, however I am looking for the way it is supposed to be done with the NestJS philosophy in mind.
Pipes are probably the easiest way to accomplish this. Instead of adding your pipe for every endpoint definition you can add a global pipe that will be called on every endpoint. In your main.ts:
async function bootstrap() {
...
app.useGlobalPipes(new SortPipe());
...
}
You can then create a pipe like this:
import { PipeTransform, Injectable, ArgumentMetadata } from '#nestjs/common';
#Injectable()
export class SortPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
const { type } = metadata;
// Make sure to only run your logic on queries
if (type === 'query') return this.transformQuery(value);
return value;
}
transformQuery(query: any) {
if (typeof query !== 'object' || !value) return query;
const { sort } = query;
if (sort) query.sort = convertForTypeOrm(sort);
return query;
}
}
If you do not want sort value on ALL endpoints to be automatically converted, you can pass custom parameter to #Query(), for example #Query('sort'). And then:
transform(value: any, metadata: ArgumentMetadata) {
const { type, data } = metadata;
// Make sure to only run your logic on queries when 'sort' is supplied
if (type === 'query' && data === 'sort') return this.transformQuery(value);
return value;
}
Related
I have a nestjs project that is mostly in RESTful structure. Everything works fine, but my concern is that some of the routes check for the presence of some query parameters to fetch data.
for instance
#Get('/some-resources')
async getSomeResource(
#Query() query: any
): Promise<HTTPResponseDTO>{
const startDate = query.startDate ? DateTime.fromISO(query.startDate).startOf('day').toISO(): null;
const endDate = query.endDate ? DateTime.fromISO(query.endDate).endOf('day').toISO() : null;
.
.
.
const result = await this.someResourceService.findAll(startDate, endDate,...)
}
Now my question is, is there a cleaner approach to this? Because this can get become a pain to maintain when we have many resources.
As mentioned by Micael Levi, you should be able to do this by creating your own custom pipe. Assuming that what you posted works, you should be able to do something along the lines of:
#Get('/some-resources')
async getSomeResource(
#Query('startDate', ParseDateIsoPipe) startDate?: string,
#Query('endDate', ParseDateIsoPipe) endDate?: string
): Promise<HTTPResponseDTO>{
<code>
}
With your ParseDateIsoPipe as follows (Note that you will still need to import DateTime from the package you are using):
import { PipeTransform, Injectable, ArgumentMetadata } from '#nestjs/common';
#Injectable()
export class ParseDateIsoPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value ? DateTime.fromISO(value).startOf('day').toISO(): null;
}
}
You can use the built-in validation pipe: https://docs.nestjs.com/techniques/validation with the auto validation feature.
I think I might be misunderstanding Nest.js's IoC container, or maybe DI as a whole.
I have a class, JSONDatabase, that I want to instantiate myself based on some config value (can either be JSON or SQL).
My DatabaseService provider:
constructor(common: CommonService, logger: LoggerService) {
// eslint-disable-next-line prettier/prettier
const databaseType: DatabaseType = common.serverConfig.dbType as DatabaseType;
if (databaseType === DatabaseType.JSON) {
this.loadDatabase<JSONDatabase>(new JSONDatabase());
} else if (databaseType === DatabaseType.SQL) {
this.loadDatabase<SQLDatabase>(new SQLDatabase());
} else {
logger.error('Unknown database type.');
}
}
My JSONDatabase class:
export class JSONDatabase implements IDatabase {
dbType = DatabaseType.JSON;
constructor(logger: LoggerService, io: IOService) {
logger.log(`Doing something...`)
}
}
However, the problem with this is that if I want my JSONDatabase to take advantage of injection, ie. it requires both IOService and LoggerService, I need to add the parameters from the DatabaseService constructor rather than inject them through Nest's IoC containers.
Expected 2 arguments, but got 0 [ts(2554)]
json.database.ts(7, 15): An argument for 'logger' was not provided.
Is this the proper way to do this? I feel like manually passing these references through is incorrect, and I should use Nest's custom providers, however, I don't really understand the Nest docs on this subject. I essentially want to be able to new JSONDatabase() without having to pass in references into the constructor and have the Nest.js IoC container inject the existing singletons already (runtime dependency injection?).
I might be completely off base with my thinking here, but I haven't used Nest all that much, so I'm mostly working off of instinct. Any help is appreciated.
The issue you have right now is because you are instantiating JSONDatabase manually when you call new JSONDatabase() not leveraging the DI provided by NestJS. Since the constructor expects 2 arguments (LoggerService, and IOService) and you are providing none, it fails with the message
Expected 2 arguments, but got 0 [ts(2554)]
I think depending on your use case you can try a couple of different options
If you fetch your configuration on startup and set the database once in the application lifetime you can use use a Custom provider with the useFactory syntax.
const providers = [
{
provide: DatabaseService,
useFactory: (logger: LoggerService, io: IOService, config: YourConfigService): IDatabase => {
if (config.databaseType === DatabaseType.JSON) {
return new JSONDatabase(logger, io);
} else if (databaseType === DatabaseType.SQL) {
return new SQLDatabase(logger, io);
} else {
logger.error('Unknown database type.');
}
},
inject: [LoggerService, IOService, YourConfigService]
},
];
#Module({
providers,
exports: providers
})
export class YourModule {}
If you have LoggerService, IOService and YourConfigurationService annotated with #Injectable() NestJS will inject them in the useFactory context. There you can check the databaseType and manually instantiate the correct IDatabase implementation. The drawback with this approach is that you can't easily change the database in runtime. (This might work just fine for your use case)
You can use strategy/factory pattern to get the correct implementation based on a type. Let say you have a method that saves to different databases based on an specific parameter.
#Injectable()
export class SomeService {
constructor(private readonly databaseFactory: DatabaseFactory){}
method(objectToSave: Object, type: DatabaseType) {
databaseFactory.getService(type).save(objectToSave);
}
}
#Injectable()
export class DatabaseFactory {
constructor(private readonly moduleRef: ModuleRef) {}
getService(type: DatabaseType): IDatabase {
this.moduleRef.get(`${type}Database`);
}
}
The idea of the code above is, based on the database type, get the correct singleton from NestJS scope. This way it's easy to add a new database if you want - just add a new type and it's implementation. (and your code can handle multiple databases at the same time!)
I also believe you can simply pass the already injected LoggerService and IOService to the DatabasesService you create manually (You would need to add IOService as a dependency of DatabaseServce
#Injectable()
export class DatabaseService {
constructor(common: CommonService, logger: LoggerService, ioService: IOService) {
// eslint-disable-next-line prettier/prettier
const databaseType: DatabaseType = common.serverConfig.dbType as DatabaseType;
if (databaseType === DatabaseType.JSON) {
this.loadDatabase<JSONDatabase>(new JSONDatabase(logger, ioService));
} else if (databaseType === DatabaseType.SQL) {
this.loadDatabase<SQLDatabase>(new SQLDatabase(logger, ioService));
} else {
logger.error('Unknown database type.');
}
}
}
I'm currently trying to write some code that retrieves a collection from my Firestore instance.
My codebase uses the service repository pattern to keep business logic seperate from the code that retrieves data. For this reason I've made the following code:
import { injectable, inject } from "inversify";
import { IOfficeRepository, TYPES } from "../common/types";
import { Firestore } from "#google-cloud/firestore";
#injectable()
export default class OfficeRepository implements IOfficeRepository {
private fireStoreClient: Firestore;
constructor(#inject(TYPES.FireStoreFactory) firestoreFactory: () => Firestore) {
this.fireStoreClient = firestoreFactory();
};
public async getOffice(officeId: string): Promise<FirebaseFirestore.QueryDocumentSnapshot<FirebaseFirestore.DocumentData>> {
const officeCollection = "offices";
const document = await this.fireStoreClient.collection(officeCollection).get();
return document;
};
}
What I'd like to do is return the value from the get() call to my service, in the service I will be performing checks and executing the business logic that I need.
The get() returns a Promise<FirebaseFirestore.QuerySnapshot<FirebaseFirestore.DocumentData>>, but I am unable to use this as a return type for the function in my repository. I just get the following error:
Type 'QuerySnapshot' is missing the following properties from type 'QueryDocumentSnapshot': createTime, updateTime, data, exists, and 3 more.
I've already looked-up the error, but I wasn't able to find any solution or a post where someone was trying to return the result from the get() function before performing any logic on the result.
So my question is: How would I be able to make this setup work? Or is there something I am doing wrong with this setup? If so, what would be another approach to work this out while using the service repository pattern?
Your declared return type of QueryDocumentSnapshot doesn't match the actual return type of QuerySnapshot.
This line of code:
const document = await this.fireStoreClient.collection(officeCollection).get();
performs a query for all of the documents in the officeCollection collection. As you can see from the API documentation, CollectionReference.get() yields a QuerySnapshot object. The entire set of documents will be available in the returned docs property.
It seems that you expect getOffice to return a single document instead. I'm noticing that you never used the argument officeId to narrow down your query to just the one document you want. Perhaps you meant to do something like this instead to get a single document using its ID?
const document = await this.fireStoreClient
.collection(officeCollection)
.doc(officeId)
.get();
In this case, document will be a DocumentSnapshot object.
I have simple custom decorator:
export const User: () => ParameterDecorator = createParamDecorator(
(data: any, req): UserIdentity => {
const user = getUser(req);
return user;
},
);
And now, I need to validate if we have email in user object.
The problem is that I can't update my current decorator.
Could I extend my current decorator?
Create a new decorator based on the previous one or create a new decorator and combine it?
Yes, you can do "decorator composition" with Nest, but this might not be a perfect solution for your case, depending on what you intend to do when user has no email property.
As per the example from the documentation:
import { applyDecorators } from '#nestjs/common';
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized"' }),
);
}
In this example, Auth is a decorator that can be used to combine all the one passed in applyDecorators.
Thus, I'd recommend extending your decorator using a pipe.
As stated by the documentation:
Nest treats custom param decorators in the same fashion as the built-in ones (#Body(), #Param() and #Query()). This means that pipes are executed for the custom annotated parameters as well (in our examples, the user argument). Moreover, you can apply the pipe directly to the custom decorator:
#Get()
async findOne(#User(new ValidationPipe()) user: UserEntity) {
console.log(user);
}
In this example, User is a custom parameter decorator. And ValidationPipe is passed, but you can imagine passing any pipe.
I'm validating my DTOs with the class-validator package. I enabled the transformation via
app.useGlobalPipes(
new ValidationPipe({
transform: true,
}),
);
in my main.ts file as described in the docs
https://docs.nestjs.com/techniques/validation#transform-payload-objects
I have a optional configuration field in my DTO. This field should be transformed to an empty object if it doesn't exist. The transformation decorator is described here
https://docs.nestjs.com/techniques/serialization#transform
I was hoping to come up with this solution:
export class MyDTO {
#IsObject()
#IsOptional()
#Transform(configuration => configuration || {})
public configuration: object;
}
When I call my API route
#Post()
public create(#Body() myDTO: MyDTO): void {
console.log(myDTO);
}
with an empty body, so without the field configuration my MyDTO instance is
{}
although I would expect it to be
{
configuration: {}
}
What is wrong or what am I missing? I tried to debug the code and it never hits the transformation function. So the #Transform doesn't trigger.
Update
It seems I have to do this
#IsObject()
#IsOptional()
#Transform(configuration => configuration || {}) // will be used if the field exists
public configuration: object = {}; // will be used if the field doesn't exist
The initial value will be used if you pass in an empty body. The transformation only runs if you pass in the field but assign a value like null to it.
Gonna go ahead n put this here too: why not just let typescript manage the default value with setting the value like
export class MyDTO {
#IsObject()
#IsOptional()
public configuration: object = {};
}
That way, if you get a value, great, and if it isn't there, class-transform will put the correct value there.
Looks like there is more discussion about solutions here.