# Decorators for Endpoints

# For Controller

# Required Decorator

# ApiTags

@ApiTags("Public Collect Code")

# ApiOperation

@ApiOperation({
  summary: "Request a collect code with X uon",
  description: `
  \nUser/Group requests a collect code with X uon real time via the Public UON API.
  \nA \`distribution\` is created from the BE with the following fields:

  \n  - \`id\`: distribution id.
  \n  - \`maxTotalUons\`: maximum number of uons allowed for this distribution.
  \n  - \`salesType (RETAIL | WHOLESALE)\`: indicate UONs distributed from this distribution is RETAIL or WHOLESALE (for reporting purpose).
  \n  - \`userId\`: the user/group that is authorized for this distribution. Must be the same user that owns the "api-key" sent together in request header. Uon Card will have "sponsor by" value of this user/group's name.`,
})

# ApiSecurity

@ApiSecurity("api-key")

# ApiProperty

@ApiProperty({
  type: Number,
  required: true,
  maximum: 2000,
  minimum: 1,
  example: 1,
  description:
    "The number of uons that the collect code to be generated would contain",
})

# Depends on API design (Stoplight), we need to add corresponding decorator

# ApiParam

@ApiParam({
  name: "distributionId",
  example: 1550858321152427,
  description: "The `id` of the distribution",
  type: "integer",
  required: true,
  format: "int64",
})

# ApiQuery

@ApiQuery({
  name: "offset",
  example: 1,
  description: "pagination",
  type: "number",
  required: true,
})

# ApiOkResponse

class PublicCollectCodeCreateResponseData {
  @ApiProperty({
    example: "WT0005-AMSHWYQB",
    required: true,
    type: String,
  })
  collectCode!: string;

  @ApiProperty({
    example: "https://earthtoday.com/collect/john?code=WT0005-AMSHWYQB",
    required: true,
    type: String,
  })
  collectUrl!: string;
}

class PublicCollectCodeCreateResponseDto {
  @ApiProperty({
    required: true,
    type: PublicCollectCodeCreateResponseData,
    title: "",
  })
  data!: PublicCollectCodeCreateResponseData;
}

@ApiOkResponse({
  description: "The collect code has been successfully created.",
  type: PublicCollectCodeCreateResponseDto,
})

Notice that title property from PublicCollectCodeCreateResponseDto is empty string, this make Redoc render cleaner as image below

# ApiNotFoundResponse

export abstract class NotFoundErrorApiProperties {
  @ApiProperty({
    example: "ERROR",
  })
  status!: string;

  @ApiProperty({
    example: "NotFoundError",
  })
  type!: string;

  @ApiProperty({
    example: "The resource cannot be found in the system",
  })
  detail!: string;

  @ApiProperty({
    example: "53bffb246fb16f60613abf31a95102a3",
  })
  correlationId!: string;

  @ApiProperty({
    example: "<ResourceType>",
    type: "string",
    enum: NotFoundErrorResourceType,
  })
  resourceType!: NotFoundErrorResourceType;
}

@ApiNotFoundResponse({
  description:
    "The distribution with the provided id cannot be found in the system",
  type: NotFoundErrorApiProperties,
})

# ApiBadRequestResponse

@ApiBadRequestResponse({
    description: "You have an error in your request",
    type: BadRequestErrorApiProperties,
  })

# ApiForbiddenResponse

@ApiForbiddenResponse({
  description:
    "The maximum number of UONs authorized for this distribution has been exceeded",
  type: MaximumNumberOfUONsExceededErrorApiProperties,
})

# ApiUnauthorizedResponse

@ApiUnauthorizedResponse({
  description: "The provided api-key is invalid",
  type: AuthorizationErrorApiProperties,
})

# ApiInternalServerErrorResponse

@ApiInternalServerErrorResponse({
  description:
    "The server encountered an unexpected condition that prevented it from fulfilling the request",
  type: InternalServerErrorApiProperties,
})

# For API request body, response

Every request body, response MUST be defined as a class, and the name convention is XxxPayloadDto & XxxResponseDto

class PublicCollectCodeCreateDataPayload {
  @ApiProperty({
    type: Number,
    required: true,
    maximum: 2000,
    minimum: 1,
    example: 1,
    description:
      "The number of uons that the collect code to be generated would contain",
  })
  @IsNumber()
  @Max(2000, {
    message: "Maximum uon amount is 2000",
  })
  @Min(1, {
    message: "Minimum uon amount is 1",
  })
  uonAmount!: number;
}
export class PublicCollectCodeCreatePayloadDto {
  @ValidateNested()
  @ApiProperty({ type: PublicCollectCodeCreateDataPayload, title: "" })
  @Type(() => PublicCollectCodeCreateDataPayload)
  @IsNotEmpty()
  data!: PublicCollectCodeCreateDataPayload;
}
export class PublicCollectCodeCreateResponseDto {
  ...
}

Most of the cases, ApiProperty decorator is enough.

Notice that the request body and response ALWAYS wrapped with data property

# For API error response

We can define error response schema as below

@ApiUnauthorizedResponse({
    description: "The provided api-key is invalid",
    schema: {
      type: "object",
      properties: {
        status: {
          type: "string",
          example: "ERROR",
        },
        type: {
          type: "string",
          example: "AuthorizationError",
        },
        detail: {
          type: "string",
          example: "The provided api-key is invalid",
        },
        correlationId: {
          type: "string",
          example: "53bffb246fb16f60613abf31a95102a3",
        },
      },
    },
  })

But we SHOULD define an abstract class for reusable, this class usually exported from the same custom error file

AuthorizationError.ts
export abstract class AuthorizationErrorApiProperties {
 ...
}

publicCollectCode.controller.ts
@ApiUnauthorizedResponse({
    description: "The provided api-key is invalid",
    type: AuthorizationErrorApiProperties,
 })

# Utility Decorator

# PersonalRequestDecorator

Endpoints with format /v*/users/me/... we can apply PersonalRequestDecorator as shortcut to apply multiple decorators at a same time

@Get("/users/me/cards/starred")
@PersonalRequestDecorator()

Body structure of PersonalRequestDecorator looks like:

export function PersonalRequestDecorator() {
  return applyDecorators(
    ApiBearerAuth(),
    ApiUnauthorizedResponse({
      description: "The bearer token is invalid",
      type: AuthenticationFailedErrorApiProperties,
    }),
  );
}

# PublicApiRequestDecorator

Endpoints with format /public/v*/users/me/... we can apply PublicApiRequestDecorator as shortcut to apply multiple decorators at a same time

@Post("public/v1/distributions/:distributionId/collect-codes")
@PublicApiRequestDecorator()

Body structure of PublicApiRequestDecorator looks like:

export function PublicApiRequestDecorator() {
  return applyDecorators(
    ApiSecurity("api-key"),
    ApiUnauthorizedResponse({
      description: "The provided api-key is invalid",
      type: AuthorizationErrorApiProperties,
    }),
    ApiInternalServerErrorResponse({
      description:
        "The server encountered an unexpected condition that prevented it from fulfilling the request",
      type: InternalServerErrorApiProperties,
    }),
  );
}