Amplication Release 0.12.7 - Good Code and Public Endpoints

Open SourceNode.jsAPINew Release
Yuval hazaz
Yuval hazaz
May 12, 2022
Amplication Release 0.12.7 - Good Code and Public EndpointsAmplication Release 0.12.7 - Good Code and Public Endpoints

Here at Amplication, we believe in good code. While we work night and day to develop new features that will add value for our users, we don’t forget to look under the hood and make sure that Amplication is generating code that meets our exacting standards. When we say good code, we mean that the code is easily understood, can be easily maintained (even by less experienced developers), does what it is intended to do, and does it well.

Good, readable code enables Amplication to fulfill one of its main goals, to enable developers to focus on the code that matters, rather than writing repetitive and boilerplate code. Moreover, we believe developers should maintain full control over the generated code and have the freedom to change it based on their requirements without being constrained by the limitations that typify every black-box solution.

Amplication release 0.12.7 is a good example of how we keep our code fine-tuned while introducing awesome new features. We have done code refactoring with significant improvements to the generated code while introducing support for public endpoints - a feature that was requested by many of our enterprise users.

To see how we did it, check out the examples below. They include major refactoring on the generated code of the controllers, used for the REST API endpoints, and resolvers, used for the GraphQL queries and mutations.

New interceptors for access controls

We created two new NestJS Interceptors to enforce Access Control policies:

AclValidateRequestInterceptor - this interceptor is used to validate that users are not updating or creating data they are not allowed to, based on the permissions that were defined for their role.

AclFilterResponseInterceptor - this interceptor is used to filter the response data based on the permissions that were defined for their role.

These interceptors are replacing the boilerplate code that was manually used in each of the controllers’ endpoints, and resolvers’ queries and mutations.

Interceptor refactored code example 1

Before:

When creating a customer record, the request data was checked for any property that is not allowed to be updated by the current user, and an exception is thrown when needed.

The function was not easily readable and included a lot of boilerplate code.

  @nestAccessControl.UseRoles({
    resource: "Customer",
    action: "create",
    possession: "any",
  })
  @common.Post()
  async create(
    @common.Body() data: CustomerCreateInput,
    @nestAccessControl.UserRoles() userRoles: string[]
  ): Promise<Customer> {
    const permission = this.rolesBuilder.permission({
      role: userRoles,
      action: "create",
      possession: "any",
      resource: "Customer",
    });
    const invalidAttributes = abacUtil.getInvalidAttributes(permission, data);
    if (invalidAttributes.length) {
      const properties = invalidAttributes
        .map((attribute: string) => JSON.stringify(attribute))
        .join(", ");
      const roles = userRoles
        .map((role: string) => JSON.stringify(role))
        .join(",");
      throw new errors.ForbiddenException(
        `providing the properties: ${properties} on ${"Customer"} creation is forbidden for roles: ${roles}`
      );
    }
    return await this.service.create({
      data: data,
      select: {
        id: true,
        createdAt: true,
        updatedAt: true,
        name: true,
      },
    });
  }

After:

Now, the boilerplate code has been removed, and the function includes only a single line of code that calls the service.create function.

Instead of the boilerplate code, the AclValidateRequestInterceptor interceptor was added as a decorator to the function.

  @common.UseInterceptors(AclValidateRequestInterceptor)
  @nestAccessControl.UseRoles({
    resource: "Customer",
    action: "create",
    possession: "any",
  })
  @common.Post()
  async create(@common.Body() data: CustomerCreateInput): Promise<Customer> {
    return await this.service.create({
      data: data,
      select: {
        id: true,
        createdAt: true,
        updatedAt: true,
        name: true,
      },
    });
  }

Interceptor refactored code example 2

Before:

Before returning the customer records to the client, the response data was filtered so only allowed properties are returned.

The function was not easily readable and included a lot of boilerplate code.


  @common.UseGuards(
    defaultAuthGuard.DefaultAuthGuard,
    nestAccessControl.ACGuard
  )
  @nestAccessControl.UseRoles({
    resource: "Customer",
    action: "read",
    possession: "any",
  })
  @common.Get()
  async findMany(
    @common.Req() request: Request,
    @nestAccessControl.UserRoles() userRoles: string[]
  ): Promise<Customer[]> {
    const args = plainToClass(CustomerFindManyArgs, request.query);

    const permission = this.rolesBuilder.permission({
      role: userRoles,
      action: "read",
      possession: "any",
      resource: "Customer",
    });
    const results = await this.service.findMany({
      ...args,
      select: {
        id: true,
        createdAt: true,
        updatedAt: true,
        name: true,
      },
    });
    return results.map((result) => permission.filter(result));
  }

After:

Now, the boilerplate code has been removed, and the function includes only two lines of code.

Instead of the boilerplate code, The AclFilterResponseInterceptor interceptor was added as a decorator to the function.

  @common.UseInterceptors(AclFilterResponseInterceptor)
  @nestAccessControl.UseRoles({
    resource: "Customer",
    action: "read",
    possession: "any",
  })
  @common.Get()
  async findMany(@common.Req() request: Request): Promise<Customer[]> {
    const args = plainToClass(CustomerFindManyArgs, request.query);
    return this.service.findMany({
      ...args,
      select: {
        id: true,
        createdAt: true,
        updatedAt: true,
        name: true,
      },
    });
  }

UseGuard decorator moved to the class level

Decorator refactored code example

Before:

We moved the UseGuard decorator to the class level instead of using it individually on each endpoint.

Before, the UseGuard decorator was added to each controller endpoint individually

  @common.UseGuards(
    defaultAuthGuard.DefaultAuthGuard,
    nestAccessControl.ACGuard
  )
  async delete(
    @common.Param() params: CustomerWhereUniqueInput
  ): Promise<Customer | null> {

After:

Now, the UseGuard decorator has been removed from the function level and it is defined once at each controller or resolver.

@common.UseGuards(defaultAuthGuard.DefaultAuthGuard, nestAccessControl.ACGuard)
export class CustomerControllerBase {

Morgan interceptor moved to the global level

We moved the morgan interceptor to the global level instead of using it individually on each endpoint.

Interceptor refactored code example

Before:

Before, the morgan interceptor was added to each controller endpoint individually

  @common.UseInterceptors(nestMorgan.MorganInterceptor("combined"))  
  async findOne(
    @common.Param() params: CustomerWhereUniqueInput,
    @nestAccessControl.UserRoles() userRoles: string[]
  ): Promise<Customer | null> {

After:

Now, the morgan interceptor was removed from the function level and it is defined once at the application global level.

providers: [
    {
      provide: APP_INTERCEPTOR,
      scope: Scope.REQUEST,
      useClass: MorganInterceptor("combined"),
    },
  ],

Public Endpoints

When building APIs, usually you would like to secure the API so it can be accessed by authorized users only. But, in many use cases, you might be required to build a public API, and sometimes, you may even need to build an API where some endpoints are private while other endpoints are public.

The request to support public endpoints is one of the most popular requests on our GitHub repository https://github.com/amplication/amplication/issues/2006.

This requirement was also something we needed when we built the Amplication blog (using Amplication of course 😍, but that’s for another blog post). As with everything else in Amplication, you can always customize the generated code, and this is exactly what we did to support the development of the blog (see this commit, and this one also).

We have now introduced built-in support to define endpoints as public. This option is available per action per entity- meaning you can easily configure the endpoint so that creating, editing, or deleting blog posts will require authentication, but for viewing the blog posts, no authentication will be needed.

Untitled

Endpoint authentication example

//This endpoint requires authentication
@common.UseInterceptors(AclFilterResponseInterceptor)
@nestAccessControl.UseRoles({
resource: "Customer",
action: "read",
possession: "any",
})
@common.Get()
async findMany(@common.Req() request: Request): Promise<Customer[]> {

//This endpoint is accessible by authenticated and non-authenticated users
//We use the @Public decorator to flag public endpoints
@Public()
@common.Get()
async findMany(@common.Req() request: Request): Promise<Customer[]> {

The Amplication revolution continues

Amplication 0.12.7 is just one step forward in our mission to revolutionize the developer experience.

To see what new features are coming up next, take a look at our roadmap.

If you have an idea for a feature that isn’t scheduled, please go to GitHub and open a Feature Request. If you have questions, need support or just want to share your thoughts- join us on Discord. We’d love to hear from you.