npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@macfja/nestjs-mongoose

v1.0.0

Published

A simple solution to create a CRUD controller for a MongoDB collection in NestJS

Downloads

9

Readme

Nest.js + Mongoose

A simple solution to create a CRUD controller for a MongoDB collection.

GitHub Repo stars Download per week License NPM version JavaScript Style Guide: Biomejs

Features

  • CRUD operation
    • Create a document
    • Read: One or a list of document
    • Update a document
    • Delete a document
  • Complete OpenApi documentation
  • Convert a DTO (public object) to/from Mongo Entity (internal object)
  • Fine control on what is available (CRUD, fields, sorting)
  • Multiple input and output format (built-in: Json:api, HAL, JSON+LD and JSON)
    • Input (Create and Update): Json:api and JSON
    • Output (Read): Json:api, HAL, JSON+LD and JSON
  • Standard error output format (RFC-9457)
  • Full-customizable
    • Override controller routes
    • Add your custom input and output format
    • Custom DTO converter
  • And much more...

(Json:api and HAL format are enabled by default)

Installation

NPM

npm install @macfja/nestjs-mongoose

Usage

Nest Module

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 {}

Mongoose Schema

Declare your mongoose schema as usual.

Controller

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-versa
  • dtoConstructor 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).

DTO

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.

Converter

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 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 Entity as is come from the database

With this you should have a functional CRUD for your MongoDB collection.

Configuration

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: [])

Custom data representation (input and output)

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 of resourceType (or the value of modelInjectionName)
    • 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 of resourceType (or the value of modelInjectionName)
    • 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 the Creator resource from the representation
    • input is the JSON receive by the controller
    • type is the value of resourceType (or the value of modelInjectionName)
    • 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 the Updater resource from the representation
    • input is the JSON receive by the controller
    • type is the value of resourceType (or the value of modelInjectionName)
    • 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, 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 DTO
  • resourceType 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 definition
  • schema The linked OpenApi schema

Override a route

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, 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.

  • use @CreateOneDecorator, for the creation route (controller method createOne)
  • use @DeleteOneDecorator, for the removing route (controller method deleteOne)
  • use @UpdateOneDecorator, for the modification route (controller method updateOne)
  • use @GetListDecorator, for the listing route (controller method getList)
  • use @GetOneDecorator, for the reading route (controller method getOne)

Filter operator

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

OpenApi version

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" });