Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
@macfja/nestjs-mongoose
Advanced tools
A simple solution to create a CRUD controller for a MongoDB collection in NestJS
A simple solution to create a CRUD controller for a MongoDB collection.
(Json:api and HAL format 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 (Only the first two are mandatory):
declare function MongooseControllerFactory<Resource, Dto extends JsonObject, Creator extends JsonObject, Updater extends JsonObject>(
modelInjectionName: string,
converter: EntityConverter<Resource, Dto, Creator, Updater>,
dtoConstructor?: Type<Dto>,
creatorConstructor?: Type<Creator>,
updaterConstructor?: Type<Updater>,
options?: MongooseControllerOptions<Dto, Creator, Updater>
): Type<MongooseController<Dto, Creator, Updater>>;
modelInjectionName
is the name linked to the schema (same as declared in MongooseModule.forFeature
)converter
is the instance responsible to convert your DTO into Mongo Entity and vice-versadtoConstructor
is the class representation of one element of your collection that you want to return
(if missing or undefined
, it's the same as options.disable.read: true
)creatorConstructor
is the class representation of a new element to add to your collection that you want to receive
(if missing or undefined
, it's the same as options.disable.create: true
)updaterConstructor
is the class representation of one element to update in your collection that you want to receive
(if missing or undefined
, it's the same as options.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<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;
};
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 DotKeys,
type EntityConverter,
type PartialWithId,
type SearchField,
toMongoFilterQuery,
toMongoSort,
} from "@macfja/nestjs-mongoose";
import { type FilterQuery, type HydratedDocument, type SortOrder, Types } from "mongoose";
import { CatDto } from "./cat.dto";
import type { Cat } from "./cat.schema";
export class CatConverter implements EntityConverter<Cat, CatDto, CatDto, CatDto> {
fromDtoFields(fields?: Array<DotKeys<CatDto>>): Array<DotKeys<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<DotKeys<Cat>>;
}
fromDtoSort(sort?: Array<string>): Record<string, SortOrder> {
return toMongoSort(sort ?? []);
}
toDto(input: HydratedDocument<Cat>): Partial<CatDto> {
return new CatDto(input.name, input.breed, input.age);
}
fromSearchable(input?: SearchField<CatDto>): FilterQuery<Cat> {
return toMongoFilterQuery(input);
}
fromCreator(input: Partial<CatDto>): Partial<Cat> {
return {
...input,
};
}
fromUpdater(id: string, input: Partial<CatDto>): 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 DTOfromDtoSort()
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 datafromUpdater()
allow you to transform a modification DTO into a Mongoose entity datatoDto()
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 filtertoMongoSort()
: Transform a list of field name and negate field name into a MongoDB sort parameterclass OneToOneConverter
: A preconfigured converter that output a MongoDB Entity as is come from the databaseWith 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>>;
filter: Partial<{
operators: typeof Operators;
actionOnInvalid: FilterParserAction;
fields: Partial<{
exclude: Array<string>;
add: Array<string>;
}>;
}>;
sort: Partial<{
exclude: Array<string>;
add: Array<string>;
}>;
projection: Partial<{
exclude: Array<string>;
add: Array<string>;
}>;
}>;
disable
, It allow to remove some part of the controller:
list
: if true
, the listing of document is removed from the controller (default: false
)get
: if true
, getting one document from its id is removed from the controller (default: false
)update
: if true
, updating one document is removed from the controller (default: false
)create
: if true
, creating a document is removed from the controller (default: false
)delete
: if true
, deleting one document from its id is removed from the controller (default: false
)read
: if true
, 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
: if true
, 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 if 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
, The ProblemDetail
error resolver (default: undefined
=> Resolve URL to https://httpstatuses.com/
)logger
, The logger to use. Used by ProblemDetail
(default: undefined
=> no log)resourceType
, The name of the resource to display in the outputs. Used for Json:Api, HAL (default: undefined
=> same as the modelInjectionName
provided to the factory)representations
, The list of document representation standard to use (default: [instance of JsonApi, instance of Hal]
)filter
, Configuration of the list filter:
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" ]
)actionOnInvalid
, Define how the filter parser should handle invalid operator or field. (default: FilterParserAction.THROW
)fields
, Filter fields options:
exclude
, List of field to remove (default: []
)add
, List of field to add (default: []
)sort
, Sort fields options:
exclude
, List of field to remove (default: []
)add
, List of field to add (default: []
)projection
, Projection (display) fields options:
exclude
, List of field to remove (default: []
)add
, List of field to add (default: []
)The library come with 4 built-in representation:
JsonApi
, which implement the {json:api}
spec and support:
HAL
, which implement the HAL spec and support:
JsonLdFactory(context: string)
, which implement the JSON-LD spec with Hydra spec for collection and support:
SimpleJson
, which have a minimal encapsulation and support:
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 documenttype
is the value of resourceType
(or the value of modelInjectionName
)self
is the URL pathname of the controllerresource
is the DTO version of the MongoDB documentRenderPage<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 of resourceType
(or the value of modelInjectionName
)self
is the URL pathname of the controllerpageData
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 versionParseCreate<Creator extends JsonObject> = (input: JsonObject, type: string) => Creator | never
, this function extract the Creator
resource from the representation
input
is the JSON receive by the controllertype
is the value of resourceType
(or the value of modelInjectionName
)ProblemDetailException
(or any exception) can be thrownParseUpdate<Updater extends JsonObject> = (input: JsonObject, type: string, id: string) => Updater | never
, this function extract the Updater
resource from the representation
input
is the JSON receive by the controllertype
is the value of resourceType
(or the value of modelInjectionName
)id
is the identifier of the document to updateProblemDetailException
(or any exception) can be thrownOneResponseSwaggerExtension
, CollectionResponseSwaggerExtension
, CreateRequestSwaggerExtension
, UpdateRequestSwaggerExtension
all extends the interface, and are used to describe a OpenApi resource
type SwaggerExtensionMaker<Input extends JsonObject = JsonObject> = (
attribute: Type<Input>,
resourceType: string,
) => SwaggerSchemaExtension
attribute
is the class of a DTOresourceType
is the value of resourceType
(of the configuration), or the value of modelInjectionName
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 definitionschema
The linked OpenApi schemaYou 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, accept?: unknown): Promise<JsonObject> {
// Do something cool with the input params
const result = await super.getOne(response, request, id, fields, accept);
// Do something cool with the output
return result;
}
}
The decorator @GetOneDecorator
add all the annotation needed for the OpenApi documentation and the route declaration.
@CreateOneDecorator
, for the creation route (controller method createOne
)@DeleteOneDecorator
, for the removing route (controller method deleteOne
)@UpdateOneDecorator
, for the modification route (controller method updateOne
)@GetListDecorator
, for the listing route (controller method getList
)@GetOneDecorator
, for the reading route (controller method getOne
)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
The OpenApi documentation generated by the library is in version 3.1.0
.
To enable it in NestJs you need to manually set the version.
In your main.ts
file, you need to change parameter of SwaggerModule.setup
function.
const document = SwaggerModule.createDocument(/* ... */);
SwaggerModule.setup("api", app, { ...document, openapi: "3.1.0" });
FAQs
A simple solution to create a CRUD controller for a MongoDB collection in NestJS
We found that @macfja/nestjs-mongoose demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.