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

nest-restful

v0.7.0

Published

Flexible RESTful CRUD API builder

Downloads

46

Readme

Nest RESTful

Easily build RESTful CRUD APIs

NOTE: This package has been renamed to nest-mikro-orm which replaced TypeORM with MikroORM.

Features

  • Super easy and fast to build RESTful CRUD APIs
  • Fully strongly typed
  • High flexibility and extensibility

Tutorial

Everything in this lib is created using factories which will create a class on its .product property when instantiated based on the options passed to the constructor, so that we can not only custom the product's behavior but also implement strong generic types.

Basic RESTful Resource

@Injectable()
class UsersService extends new RestServiceFactory({
  entityClass: User,
  repoConnection: "auth-db", // default: undefined
  dtoClasses: {
    create: CreateUserDto,
    update: UpdateUserDto,
  },
}).product {}
@Controller()
class UsersController extends new RestControllerFactory<UsersService>({
  restServiceClass: UsersService,
  actions: ["list", "create", "retrieve", "replace", "update", "destroy"],
  lookup: { field: "id" },
  queryDtoClass: new QueryDtoFactory<User>({
    limit: { default: 50, max: 200 },
    offset: { max: 10000 },
    order: {
      in: ["id", "name", "age"],
      default: ["id:desc"],
    },
    expand: { in: ["department", "department.manager"] },
    filter: { in: ["id", "name", "age"] },
  }).product,
}).product {}

| Action | Method | URL | Code | Response | | -------- | ------ | --------- | ----------- | ------------------------------------ | | List | GET | / | 200,400 | { total: number; results: Entity[] } | | Create | POST | / | 201,400 | Entity | | Retrieve | GET | /:userId/ | 200,404 | Entity | | Replace | PUT | /:userId/ | 200,400,404 | Entity | | Update | PATCH | /:userId/ | 200,400,404 | Entity | | Destroy | DELETE | /:userId/ | 204,404 | void |

| Filter Query | Find Operator | | --------------------- | --------------------------- | | name|eq:QCX | Equal("QCX") | | age|gt:16 | MoreThan(16) | | age|gte:16 | MoreThanOrEqual(16) | | name|in:Q,C,X,\,\, | In(["Q", "C", "X", ",,"]) | | age|lt:60 | LessThan(60) | | age|lte:60 | LessThanOrEqual(60) | | name|ne:QCX | Not("QCX") | | name|nin:Q,C,X | Not(In(["Q", "C", "X"])) | | name|like:%C% | Like("%C%") | | name|ilike:%C% | ILike("%C%") | | name|isnull: | IsNull() | | name|notnull: | Not(IsNull()) |

Forcing Query Conditions

class UsersService /*extends ...*/ {
  async finalizeQueryConditions({
    conditions,
    user,
  }: {
    conditions: FindConditions<User>;
    user: User;
  }) {
    return conditions.map((conditions) => ({
      ...conditions,
      isActive: true,
      owner: user,
    }));
  }
}

Forcing Orders

class UsersService /* extends ... */ {
  async parseOrders(args: any) {
    return { ...(await super.parseOrders(args)), age: "ASC" };
  }
}

Custom Request User

By default, the request user will be picked from request.user using a custom decorator, and the metadata type of the user is Object. This behavior can be configured by specifying by requestUser option.

class UsersController extends new RestControllerFactory({
  // ...
  requestUser: { type: User, decorators: [RequestUser()] },
  // ...
}).product {}

Additional Decorators

For example, to apply the auth guard for each action except create:

@Controller()
export class UsersController extends new RestControllerFactory({
  // ...
})
  .applyMethodDecorators("list", UseGuards(JwtAuthGuard))
  .applyMethodDecorators("retrieve", UseGuards(JwtAuthGuard))
  .applyMethodDecorators("replace", UseGuards(JwtAuthGuard))
  .applyMethodDecorators("update", UseGuards(JwtAuthGuard)).product {}

Transforming Entities before Responding

The service's .transform() is called on each entity before sending the response. By default, it takes an entity, call class-transformer's plainToClass() and then return it, which means fields can be excluded from the response using the Exclude() decorator.

@Entity()
class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Exclude()
  password: string;
}

Overriding it to custom transform options or do anything you want.

class UsersService /*extends ...*/ {
  async transform({ entity }: { entity: User }) {
    return plainToClass(User, entity, {
      // ...
    });
  }
}

Overriding Controller's Action Methods

Here is something you should know before overriding the action methods, otherwise something really confusing may happen to you.

  • Nest's controller decorators store metadata in the constructor, and when getting metadata, it will look up the metadata value through the prototype chain, so there is no need to decorate the class again when extending another class.
  • Nest's action method decorators store metadata in action methods directly, when looking metadata, it will look up the value directly from the method, but if we override a method, the method will be different from the old one and all the metadata will be lost, method decorators should be applied again.
  • Nest's param decorators store metadata in the constructor, as said before, there is no need to apply param decorators again.

For example, to custom the list action's response:

class UsersController /*extends ...*/ {
  @Patch(":userId") // method decorators should be applied again
  async list(/* ... */) {
    const data = await super.list(/* ... */);
    return {
      ...data,
      you_could: "put anything here",
    };
  }
}

Access Control

When the action is list or create, the service's .checkPermission() will be called once with { action: "<the-action-name>" } before performing the action.
In other cases it will be called twice, once is with { action: "<the-action-name>" } before loading the target entity and once is with { action: "<the-action-name>", entity: <the-target-entity> } before performing the action.

async checkPermission({
  action,
  entity,
  user,
}: {
  action: ActionName;
  entity?: User;
  user?: User;
}) {
  if (!entity) {
    // forbid authed users to create users
    if (action == 'create' && user) throw new ForbiddenException();
  } else {
    // forbid the user to update anyone except himself
    if (entity.id != user.id) throw new ForbiddenException();
    // forbid the user to update if updated recently
    if (action == 'replace' || action == 'update')
      if (!entity.isUpdatedRecently) throw new ForbiddenException();
  }
}

Handling Relations

For example, you'd like to create a membership for the user when creating a classroom:

class TestService /* extends ... */ {
  @InjectRepository(Membership)
  membershipRepository: Repository<Membership>;

  async create({ data, user }: { data: CreateClassroomDto; user: User }) {
    const membership = await this.membershipRepository.save({
      user,
      role: Role.HeadTeacher,
    });
    const classroom = await this.repository.save({
      ...data,
      members: [membership],
    });
    return classroom;
  }
}

Or you'd like to save nested entities using their primary keys:

class TestService /* extends ... */ {
  @InjectRepository(ChildEntity)
  childRepository!: Repository<ChildEntity>;

  async create({ data }: { data: CreateParentEntityDto }) {
    const childrenEntities = await Promise.all(
      data.children.map((id) => this.childRepository.findOne(id))
    );
    return await this.repository.save({
      ...data,
      children: childrenEntities,
    });
  }
}

Reusability

It is recommended to create your own factory to reuse wonderful overridings by overriding the methods in the factory's protected .createRawClass() method.

class OwnServiceFactory<
  Entity = any,
  CreateDto = Entity,
  UpdateDto = CreateDto,
  LookupField extends LookupableField<Entity> = LookupableField<Entity>
> extends RestServiceFactory<Entity, CreateDto, UpdateDto, LookupField> {
  protected createRawClass() {
    return class RestService extends super.createRawClass() {
      // override methods here
    };
  }
}

DONT use mixins because metadata may be lost.


The service's methods are all designed to be able to be easily overridden to implement better things, you can find more usages by reading the source code.