0.7.0 • Published 3 years ago

nest-restful v0.7.0

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
3 years ago

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 {}
ActionMethodURLCodeResponse
ListGET/200,400{ total: number; results: Entity[] }
CreatePOST/201,400Entity
RetrieveGET/:userId/200,404Entity
ReplacePUT/:userId/200,400,404Entity
UpdatePATCH/:userId/200,400,404Entity
DestroyDELETE/:userId/204,404void
Filter QueryFind Operator
name|eq:QCXEqual("QCX")
age|gt:16MoreThan(16)
age|gte:16MoreThanOrEqual(16)
name|in:Q,C,X,\,\,In(["Q", "C", "X", ",,"])
age|lt:60LessThan(60)
age|lte:60LessThanOrEqual(60)
name|ne:QCXNot("QCX")
name|nin:Q,C,XNot(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.

0.5.5

3 years ago

0.7.0

3 years ago

0.6.0

3 years ago

0.5.4

3 years ago

0.5.3

3 years ago

0.5.2

3 years ago

0.5.1

3 years ago

0.5.0

3 years ago

0.4.1

3 years ago

0.4.0

3 years ago

0.3.4

3 years ago

0.3.3

3 years ago

0.3.2

3 years ago

0.3.1

3 years ago

0.3.0

3 years ago

0.2.2

3 years ago

0.2.1

3 years ago

0.2.0

3 years ago

0.1.0

3 years ago