This package provide a simple solution to create a CRUD controller for a MongoDB collection.
Support output format in Json:api, HAL, JSON+LD and JSON. (Json:api, HAL are enabled by default)
npm install @macfja/nestjs-mongoose
In your main module (i.e. src/app.module.ts
) configure your MongooseModule.
Something similar to:
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { CatController, DogController } from "./app.controller";
import { CatController, DogController } from "./cat/cat.controller";
import { Cat, CatSchema } from "./cat/cat.schema";
import { AppService } from "./app.service";
@Module({
imports: [
MongooseModule.forRoot("mongodb://root:root@localhost:27017/example?authSource=admin"),
MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }]),
// ^^^^^^^^
// This value is important, it's what will be used to
// link the controller to the Mongoose Schema
],
controllers: [AppController, CatController],
providers: [AppService],
})
export class AppModule {}
Declare your mongoose schema as usual.
In your controller (i.e. src/cat/cat.controller.ts
)
import { MongooseControllerFactory } from "@macfja/nestjs-mongoose";
import { Controller } from "@nestjs/common";
import { ApiTags } from "@nestjs/swagger";
import { CatConverter } from "./cat.converter";
import { CatDto } from "./cat.dto";
@Controller("cats")
@ApiTags("Cat Api")
export class CatController extends MongooseControllerFactory(Cat.name, new CatConverter(), CatDto, CatDto, CatDto) {}
// ^^^^^^^^
// Same as the value of the MongooseModule.forFeature
To get the automatic CRUD controller, we need to extend the function MongooseControllerFactory
, which take 6 parameters (the last one is optional):
declare function MongooseControllerFactory<Resource, Dto extends JsonObject, Searchable extends SimpleType, Creator extends JsonObject, Updater extends JsonObject>(
modelInjectionName: string,
converter: EntityConverter<Resource, Searchable, Dto, Creator, Updater>,
dtoConstructor?: Type<Dto>,
creatorConstructor?: Type<Creator>,
updaterConstructor?: Type<Updater>,
options?: MongooseControllerOptions<Dto, Creator, Updater>
): Type<MongooseController<Dto, Searchable, Creator, Updater>>;
-
modelInjectionName
is the name linked to the schema (same as declared inMongooseModule.forFeature
) -
converter
is the instance responsible to convert your DTO into Mongo Entity and vice-versa -
dtoConstructor
is the class representation of one element of your collection that you want to return (if missing orundefined
, it's the same asoptions.disable.read: true
) -
creatorConstructor
is the class representation of a new element to add to your collection that you want to receive (if missing orundefined
, it's the same asoptions.disable.create: true
) -
updaterConstructor
is the class representation of one element to update in your collection that you want to receive (if missing orundefined
, it's the same asoptions.disable.update: true
) -
options
is a set of configuration to change what the controller can do (more information later in this document).
Let's see how the DTO (i.e. src/cat/cat.dto.ts
) are:
import { BaseDto } from "@macfja/nestjs-mongoose";
import { ApiProperty } from "@nestjs/swagger";
export class CatDto extends BaseDto implements CatDtoType {
constructor(name: string, breed: string, age: number) {
super();
this.name = name;
this.breed = breed;
this.age = age;
}
@ApiProperty()
name: string;
@ApiProperty()
breed: string;
@ApiProperty()
age: number;
}
export type CatDtoType = {
name: string;
breed: string;
age: number;
};
export type CatSearchableDto = {
name?: string;
breed?: string;
};
BaseDto
is a helper class to ease the typing, and it's completely optional.
Let's take a look on the converter (i.e. src/cat/cat.converter.ts
):
import {
type EntityConverter,
type PartialWithId,
type SearchField,
toMongoFilterQuery,
toMongoSort,
} from "@macfja/nestjs-mongoose";
import { type FilterQuery, type HydratedDocument, type SortOrder, Types } from "mongoose";
import { CatDto, type CatDtoType, type CatSearchableDto } from "./cat.dto";
import type { Cat } from "./cat.schema";
export class CatConverter implements EntityConverter<Cat, CatSearchableDto, CatDto> {
fromDtoFields(fields?: Array<keyof CatDtoType>): Array<keyof Cat> {
return fields
?.filter((field) => ["name", "age", "breed"].includes(field))
.map((field) => {
switch (field) {
case "name":
return "name";
case "age":
return "age";
case "breed":
return "breed";
}
return false;
})
.filter(Boolean) as Array<keyof Cat>;
}
fromDtoSort(sort?: Array<string>): Record<string, SortOrder> {
return toMongoSort(sort ?? []);
}
toDto(input: HydratedDocument<Cat>): CatDto {
return new CatDto(input.name, input.breed, input.age);
}
fromSearchable(input?: SearchField<CatSearchableDto>): FilterQuery<Cat> {
return toMongoFilterQuery(input);
}
fromCreator(input: Partial<CatDtoType>): Partial<Cat> {
return {
...input,
};
}
fromUpdater(id: string, input: Partial<CatDtoType>): PartialWithId<Cat> {
return {
...input,
_id: new Types.ObjectId(id),
};
}
}
The converter do 6 transformation:
-
fromDtoFields()
allow you to define the list of field to select (projection
) in the MongoDB query from the name of field of the DTO -
fromDtoSort()
allow to set how the document are sorted. The parameter is a list of field name of the DTO (can be prefixed by-
to reverse the order). -
fromSearchable()
allow to change the filtering provided (The operators are not exactly the same as MongoDB, set later in this document) -
fromCreator()
allow you to transform a creation DTO into a Mongoose entity data -
fromUpdater()
allow you to transform a modification DTO into a Mongoose entity data -
toDto()
allow you to transform a document from MongoDB into the DTO you want to display
@macfja/nestjs-mongoose
come with all sort of helper to ease the creation of a converter:
-
toMongoFilterQuery()
: Transform a received filter into a MongoDB query filter -
toMongoSort()
: Transform a list of field name and negate field name into a MongoDB sort parameter -
class OneToOneConverter
: A preconfigured converter that output a MongoDB as is come from the database
With this you should have a functional CRUD for your MongoDB collection.
As mentioned earlier, MongooseControllerFactory
has a 6th parameter to control how it's works:
type MongooseControllerOptions<Dto extends JsonObject, Creator extends JsonObject, Updater extends JsonObject> = Partial<{
disable: Partial<{
list: boolean;
get: boolean;
update: boolean;
create: boolean;
delete: boolean;
read: boolean;
write: boolean;
}>;
pageSize: Partial<{
default: number;
max: number;
}>;
urlResolver: ProblemDetailTypeUrlResolver;
logger: LoggerService;
resourceType: string;
representations: Array<Representation<Dto, Creator, Updater>>;
operators: typeof Operators;
operatorValidator: Partial<{
escapeInvalidLogicalOperator: boolean;
throwOnInvalidOperator: boolean;
}>;
}>;
-
disable
, It allow to remove some part of teh controller:-
list
: iftrue
, the listing of document is removed from the controller (default:false
) -
get
: iftrue
, getting one document from its id is removed from the controller (default:false
) -
update
: iftrue
, updating one document is removed from the controller (default:false
) -
create
: iftrue
, creating a document is removed from the controller (default:false
) -
delete
: iftrue
, deleting one document from its id is removed from the controller (default:false
) -
read
: iftrue
, the listing and getting one document are removed from the controller, the output of the creation and modification of a document is disabled (default:false
) -
writing
: iftrue
, updating, creating and deleting a document are removed from the controller (default:false
)
-
-
pageSize
, Allow to change the pagination size:-
default
, the default page size is none is provided (default:10
) -
max
, the maximum page size allowed. If the value requested by the user is superior, it is set to this value (default:200
)
-
-
urlResolver
, TheProblemDetail
error resolver (default:undefined
=> Resolve URL tohttps://httpstatuses.com/
) -
logger
, The logger to use. Used byProblemDetail
(default:undefined
=> no log) -
resourceType
, The name of the resource to display in the outputs. Used for Json:Api, HAL (default:undefined
=> same as themodelInjectionName
provided to the factory) -
outputFormats
, The list of document representation standard to use (default:[instance of JsonApi, instance of Hal]
) -
operators
, List of operators to display in the swagger (default:[ "$eq", "$neq", "$gt", "$gte", "$lt", "$lte", "$start", "$end", "$regex", "$null", "$def", "$in", "$nin", "$or", "$and" ]
) -
operatorValidator
, defined how the operators parser/validator react on invalid value:-
escapeInvalidLogicalOperator
, iftrue
and$and
or$or
are defined but not allowed, escape the operator (add\
before the operator), otherwise use the same behavior defined bythrowOnInvalidOperator
(default:false
) -
throwOnInvalidOperator
, iftrue
and a not allowed operator is present, throw an 400 error, otherwise, remove the operator from the filter (default:true
)
-
The library come with 4 built-in representation:
-
JsonApi
, which implement the{json:api}
spec and support:- Get a list of document (Response)
- Get one document (Response)
- Update one document (Request)
- Create one document (Request)
-
HAL
, which implement the HAL spec and support:- Get a list of document (Response)
- Get one document (Response)
-
JsonLdFactory(context: string)
, which implement the JSON-LD spec with Hydra spec for collection and support:- Get a list of document (Response)
- Get one document (Response)
-
SimpleJson
, which have a minimal encapsulation and support:- Get a list of document (Response)
- Get one document (Response)
- Update one document (Request)
- Create one document (Request)
To create a new output format, you need an instance of Representation
type Representation<Dto extends JsonObject = any, Creator extends JsonObject = any, Updater extends JsonObject = any> = {
readonly renderOne?: RenderOne<Dto>;
readonly renderPage?: RenderPage<Dto>;
readonly getOneResponseSwaggerExtension?: OneResponseSwaggerExtension<Dto>;
readonly getCollectionResponseSwaggerExtension?: CollectionResponseSwaggerExtension<Dto>;
readonly getCreateRequestSwaggerExtension?: CreateRequestSwaggerExtension<Creator>;
readonly getUpdateRequestSwaggerExtension?: UpdateRequestSwaggerExtension<Updater>;
readonly parseCreateRequest?: ParseCreate<Creator>;
readonly parseUpdateRequest?: ParseUpdate<Updater>;
readonly contentType: string;
};
The property contentType
indicate the output MIME type of your representation, it is also use (with the Accept
header, or Content-type
header) to determine which representation to use.
-
RenderOne<Resource extends JsonObject> = (id: string, type: string, self: string, resource: Resource) => JsonObject
, this function is call to render a document-
id
is the MongoDB id of the document -
type
is the value ofresourceType
(or the value ofmodelInjectionName
) -
self
is the URL pathname of the controller -
resource
is the DTO version of the MongoDB document
-
-
RenderPage<Resource extends JsonObject> = (type: string, self: string, count: number, pageData: { size: number; current: number; }, resources: Map<string, Resource>) => JsonObject
, this function is call to render a paginated list of documents-
type
is the value ofresourceType
(or the value ofmodelInjectionName
) -
self
is the URL pathname of the controller -
pageData
is the current information about the page (the size of a page, and the current page number) -
resources
is list of item of the page. The key of the map is the MongoDB id of the document, the value the DTO version
-
-
ParseCreate<Creator extends JsonObject> = (input: JsonObject, type: string) => Creator | never
, this function extract theCreator
resource from the representation-
input
is the JSON receive by the controller -
type
is the value ofresourceType
(or the value ofmodelInjectionName
) - If an error occurs wil validating/parsing the input, a
ProblemDetailException
(or any exception) can be thrown
-
-
ParseUpdate<Updater extends JsonObject> = (input: JsonObject, type: string, id: string) => Updater | never
, this function extract theUpdater
resource from the representation-
input
is the JSON receive by the controller -
type
is the value ofresourceType
(or the value ofmodelInjectionName
) -
id
is the identifier of the document to update - If an error occurs wil validating/parsing the input, a
ProblemDetailException
(or any exception) can be thrown
-
OneResponseSwaggerExtension
, CollectionResponseSwaggerExtension
, CreateRequestSwaggerExtension
, UpdateRequestSwaggerExtension
all extends the interface
type SwaggerExtensionMaker<Input extends JsonObject = JsonObject> = (
attribute: Type<Input>,
resourceType: string,
) => SwaggerSchemaExtension
-
attribute
is the class of a DTO -
resourceType
is the value ofresourceType
(of the configuration), or the value ofmodelInjectionName
The functions return a SwaggerSchemaExtension
to helper generate an accurate OpenApi document
interface SwaggerSchemaExtension {
extraModels: Parameters<typeof ApiExtraModels>;
schema: SchemaObject;
}
-
extraModels
List of class to inject in the OpenApi definition -
schema
The linked OpenApi schema
You can change the behavior of a simple route by overriding it like a normal OOP class.
import { GetOneDecorator, Hal, JsonApi, type JsonObject, MongooseControllerFactory } from "@macfja/nestjs-mongoose";
import { Controller } from "@nestjs/common";
import { ApiTags } from "@nestjs/swagger";
import type e from "express";
import { CatConverter } from "./cat.converter";
import { CatDto } from "./cat.dto";
import { Cat } from "./cat.schema";
@Controller("cats")
@ApiTags("Cat Api")
export class CatController extends MongooseControllerFactory(Cat.name, new CatConverter(), CatDto, CatDto, CatDto) {
@GetOneDecorator(CatDto, Cat.name, [JsonApi, Hal])
override async getOne(response: e.Response, request: e.Request, id: string, fields?: unknown): Promise<JsonObject> {
// Do something cool with the input params
const result = await super.getOne(response, request, id, fields);
// Do something cool with the output
return result;
}
}
The decorator @GetOneDecorator
add all the annotation needed for the OpenApi documentation and the route declaration.
- use
@CreateOneDecorator
, for the creation route (controller methodcreateOne
) - use
@DeleteOneDecorator
, for the removing route (controller methoddeleteOne
) - use
@UpdateOneDecorator
, for the modification route (controller methodupdateOne
) - use
@GetListDecorator
, for the listing route (controller methodgetList
) - use
@GetOneDecorator
, for the reading route (controller methodgetOne
)
This library is a slightly different list of operator
@macfja/nestjs-mongoose |
MongoDB |
---|---|
$eq |
$eq |
$neq |
$ne |
$gt |
$gt |
$gte |
$gte |
$lt |
$lt |
$lte |
$lte |
$start |
$regex with a altered value |
$end |
$regex with a altered value |
$regex |
$regex |
$null |
$eq with the value to null
|
$def |
$ne with the value to null
|
$in |
$in |
$nin |
$nin |
$or |
$or |
$and |
$and |
The library handle the case where several $regex
, $eq
, $ne
would appear in the MongoDB request if the function toMongoFilterQuery
is used