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

@scandinavia/nestjs-libs

v2.0.4

Published

NestJS Utilities

Downloads

198

Readme

nestjs-sc-libs

What does this package do?

This package is a collection of many utilities that help building REST applications rapidly.

What does this package contain?

  • MongoDb
    • Transaction Helper
    • Exception Filter
    • Search Extension
    • Softdelete Plugin
  • JWT Exception Filter
  • Coded Exception
  • CASL Foribdden Exception Filter
  • MongoId Parse Pipes
  • File Uploader
  • Global Chaching Interceptor

Transaction Helper

This plugin helper in managing transactions using cls-hooked and using decorators for transactional methods.

For usage, first intialize before anything else in your main.ts file:

import { initi alizeTransactionalContext } from "@scandinavia/nestjs-libs";
initializeTransactionalContext();

Then when intializing MongooseModule, remember to use attach these plugins as global plugins, and to set a connectionName

MongooseModule.forRootAsync({
    inject: [ConfigService],
    connectionName: 'default',
    useFactory: async (
        configService: ConfigService,
    ): Promise<MongooseModuleOptions> => ({
        uri: configService.get('MONGO_URI'),
        replicaSet: configService.get('REPLICA_SET'),
        connectionFactory: (connection: Connection) => {
          connection.plugin(mongooseTrxPlugin);
          connection.plugin(accessibleRecordsPlugin);
          return connection;
        },
    }),
}),

Remember then to setup the connection hooks for that TransactionHelper, the quickest way is to do it in the module constructor:

import { TransactionConnectionManager } from "@scandinavia/nestjs-libs";

export class AppModule {
  constructor(@InjectConnection("default") private appConnection: Connection) {
    appConnection.name = "default";
    TransactionConnectionManager.setConnection(
      this.appConnection,
      this.appConnection.name
    );
  }
}

In order to use transactions, do the following in a service method:

import { Transactional, Propagation } from "@scandinavia/nestjs-libs";

export class Service {
  @Transactional({ propagation: Propagation.Mandatory })
  save() {
    // code that uses any mongoose model
  }
}

There are many Propagation types for configuring transactions, these are:

MANDATORY: Support a current transaction, throw an exception if none exists.

NEVER: Execute non-transactionally, throw an exception if a transaction exists.

NOT_SUPPORTED: Execute non-transactionally, suspend the current transaction if one exists.

REQUIRED: Support a current transaction, create a new one if none exists.

SUPPORTS: Support a current transaction, execute non-transactionally if none exists.

Coded Exceptions

Provide functions to quickly generate Exception classes for usage within the app.

export class AccountDisabledException extends GenCodedException(
  HttpStatus.BAD_REQUEST,
  "ACCOUNT_DISABLED"
) {}

export class AdminNotFoundException extends ResourceNotFoundException(
  "ADMIN"
) {}

Exception Filters

Use all filters as global filters.

The CaslForbiddenExceptionFilter catches any forbidden error from CASL and sends an 403 response.

The CodedExceptionFilter catches CodedExceptions and formats the response accordingly.

The MongoExceptionFilter catches MongoErrors thrown from the MongoDb driver and formats the response accordingly.

Validation and Serialization

First things first, initialize the app to use the validation pipeline, and the serialization interceptor:

app.useGlobalPipes(
  new ValidationPipe({
    transform: true,
    whitelist: true,
  })
);
app.useGlobalInterceptors(
  new CustomClassSerializerInterceptor(app.get(Reflector))
);

ValidationPipe is used to validate any DTO classes annotated with class-validator.

CustomClassSerializerInterceptor is used to serialize any response DTOs returned from a controller method using class-transformer to convert results correctly.

Validation Example:

export class CreateCompanyDto {
  @IsString()
  name: string;

  @ValidateNested()
  @Type(() => CreateUserDtoWithoutCompany)
  mainUser: CreateUserDtoWithoutCompany;
}

Response DTO example:

import { BaseResponseDto } from "@scandinavia/nestjs-libs";
import { Exclude, Expose, Type } from "class-transformer";

@Exclude()
export class UserDto extends BaseResponseDto {
  @Expose()
  firstName: string;
  @Expose()
  lastName: string;
  @Expose()
  email: string;
  @Expose()
  phone: string;
  @Expose()
  activatedAt: Date;
  @Expose()
  type: UserType;
  @Expose()
  @Type(() => CompanyDto)
  company: CompanyDto;
}

Don't forget to decorate the class with @Exclude and decorate all properties with @Expose, also response DTOs should extend BaseResponseDto class.

Remember to transform a plain object to the appropriate DTO before returning it in a controller.

@Get()
async getItem() {
    const item = await this.service.getItem();
    return plainToInstance(ItemResponseDto, item.toObject());
    // always use toObject() on mongoose documents
}

If you want a controller to return a plain object (non DTO), annotate the controller method like so:

@CustomInterceptorIgnore()
returnPlainObj() {
    return {x: 4};
}

Search Plugin

This plugin has many utilties that help make its magic work, it works with casl, class-transformer, class-validator, and mongoose.

First things first, initialize the app to use the validation pipeline, and the serialization interceptor just like in the above section.

Model Definition

Starting with model definition, make your class model either extend StampedCollectionProperties or BaseCollectionProperties, Stamped one includes createdAt and updatedAt fields.

Then decorate properties with SearchKey decorator:

export class Device extends StampedCollectionProperties {
  @SearchKey({ sortable: true, filterable: true, includeInMinified: true })
  @Prop({ required: true })
  name: string;

  @SearchKey({ sortable: true, filterable: true })
  @Prop({ required: true })
  imei: string;

  @Prop({
    ref: SchemaNames.COMPANY,
    type: SchemaTypes.ObjectId,
    required: true,
  })
  @SearchKey({
    sortable: true,
    filterable: true,
    isId: true,
    pathClass: () => Company,
  })
  company: Types.ObjectId | CompanyDocument;
}

filterable: allows filtering on that field.

sortable: allows filtering on that field.

isId: set it to true when dealing with fields that store ObjectId

pathClass: a function that returns the class of that object once it is populated

includeInMinified: return this proprty when requesting a minified query, more on that later.

Controller

Use the SearchDto and the SearchPipe within controllers:

@Get()
async getByCriteria(@Query(new SearchPipe(Device)) searchdto: SearchDto) {
    let abilities;
    // code to assign abilities a value, these are an array of casl rules.
    return this.deviceService.getByCriteria(searchDto.transformedResult, abilities);
}

Use the DocAggregator class to execute the query within the service:

async getByCriteria(searchDto: TransformedSearchDto, abilities) {
    // Pass path description to DocAggregator
    // $ROOT$ is a special path which here points to the Device base document
    // LookupFlags are used when accessing refs, when SINGLE is used, the value of company is a single document, else it's an array.
    // When REQUIRED is used, it works just like an inner-join, else the query works just like a left-join.
    return new DocAggregator(this.deviceModel, DeviceDto).aggregateAndCount(searchDto, {
        $ROOT$: {
            accessibility: this.deviceModel.accessibleBy(abilities).getQuery()
        },
        company: {
            flags: LookupFlags.SINGLE | LookupFlags.REQUIRED
        }
    });
}

The query pipeline is then composed automatically and executed with Count query to fill up pagination info.

Filtering and Sorting

filter within the SearchDto should be passed as a json string which works as a mongo filter and supports some operators.

example: {"id":"618cca7a88d510dc802d3a27"}

sort should be multiple fields seperated with ; use hyphens before a property for descending sorting.

example: company;-createdAt

the above example sorts by company ascending, then by createdAt descending.

passing minfied=true within the query parameters returns only fields annotated with includeInMinfied. This is useful to populate Dropdown Menus with basic information only (id,name) without creating a seperate endpoint to execute such logic.

Uploader

This module uses @nestjs/serve-static, multer, and sharp.

It is used to upload files to be stored either locally on the server, or on AWS S3 cloud storage.

It also provided many validation tools and ways to define transformations on uploaded files like generating thumbnails for uploaded images.

First, initialize in the AppModule within imports:

UploadModule.forRoot({
  storageType: UploadModuleStorageType.LOCAL,
  localStorageOptions: {
    storageDir: "files",
    publicServePath: "assets",
  },
});

According to the above code snippet, any uploaded files will be stored under ./files/public or ./files/private directories. Public files are statically served, while private files are not. Public files will be accessible directly using /assets

As for controller code, where is an example:

@Post()
@UseInterceptors(
    UploadInterceptor({
        logo: {
          destination: 'logos',
          maxNumFiles: 1,
          minNumFiles: 1,
          isPrivate: false,
          pipeline: new ImagePipeline()
            .persist()
            .validate({
              aspectRatio: {
                min: 1,
                max: 1,
              },
            })
            .thumb('small', { width: 100, height: 100 }),
        },
      },
      {
        allowedMimeTypes: [MimeTypes.image],
      },
    ),
)
async upload(@UploadedSingleFileByField('logo') logo: UploadedFile) {
    const smallThumbnail: UploadedFile = logo.processFiles['small'];
    return logo;
}

Caching

Define the CustomCacheInterceptor globally in the app.module.ts file

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: CustomCacheInterceptor,
    },
  ],
})
export class AppModule {}

In order to cache a certain endpoint, use @CacheThisEndpoint decorator in GET controllers.