Loopback 4

  • [x] Database Connectors
  • [x] Model
  • [x] Relations
  • [x] Filter
  • [x] Indexes
  • [x] Tests
  • [x] Authorization

Database Connectors

PostgreSQL Connector

npm install loopback-connector-postgresql --save

Create datasource

lb4 datasource
// ./src/datasources/english.datasource.ts
import { inject, lifeCycleObserver, LifeCycleObserver } from "@loopback/core";
import { juggler } from "@loopback/repository";

const config = {
  name: "english",
  connector: "postgresql",
  url: "postgres://postgres_dev:123456@localhost:5432/english_api",
};

// Observe application's life cycle to disconnect the datasource when
// application is stopped. This allows the application to be shut down
// gracefully. The `stop()` method is inherited from `juggler.DataSource`.
// Learn more at https://loopback.io/doc/en/lb4/Life-cycle.html
@lifeCycleObserver("datasource")
export class EnglishDataSource
  extends juggler.DataSource
  implements LifeCycleObserver
{
  static dataSourceName = "english";
  static readonly defaultConfig = config;

  constructor(
    @inject("datasources.config.english", { optional: true })
    dsConfig: object = config
  ) {
    super(dsConfig);
  }
}

Debug Database Query

Linux

DEBUG=loopback:connector* npm start

Window

set DEBUG=loopback:connector* && npm start

Model

Definition

A Model describes business domain objects, for example: Task, TaskStatus and Project. It usually defines a list of properties with name, type, and other constraints(format, min, max, ...).

Model can be used for data exchanged between client-server or different systems.

Model definitions can be mapped to many forms: relational database schemas, JSON schemas, OpenAPI Schemas, gRPC message definitions,...

A JSON Object conforming to the Task model definition can be passed in REST/HTTP payload to create a new Task or stored in a document database such as Mongo

There are 2 subtly different types of models for domain objects:

  • Entity : A domain object that has an identity (ID). Its equality is based on the identity.
  • Model: A domain object that does not have an identity (ID). Its equality is based on the structural value
@model()
export class Task extends Entity {
  @property({
    type: "string",
    id: true,
    defaultFn: "uuid",
  })
  id?: string;

  @property({
    type: "string",
    required: true,
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 120,
    },
  })
  name: string;
}

@model()
export class SocialLink {
  @property({
    type: "string",
    required: true,
  })
  linkType: string;

  @property({
    type: "string",
    required: true,
  })
  link: string;
}

ID - How to create auto increment for ID property?

{
  id: true,
  type: 'Number',
  required: false,
  generated: true // enables auto-generation
}

How to use UUID?

use uuid that is generated by your LB application by setting defaultFn: uuid

@property({
    id: true,
    type: 'string'
    defaultFn: 'uuid',
    // generated: true,  -> not needed
  })
id: string;

use PostgreSQL built-in (extension and) uuid functions

@property({
  id: true,
  type: 'String',
  required: false,
  // settings below are needed
  generated: true,
  useDefaultIdType: false,
  postgresql: {
    dataType: 'uuid',
  },
})
id: string;

The setting uses uuid-ossp extension and uuid_generate_v4() function as default.

If you’d like to use other extensions and functions, you can do:

 @property({
  id: true,
  type: 'String',
  required: false,
  // settings below are needed
  generated: true,
  useDefaultIdType: false,
  postgresql: {
    dataType: 'uuid',
    extension: 'myExtension',
    defaultFn: 'myuuid'
  },
})
id: string;

Common data types mapping

image

import { model, property } from "@loopback/repository";
import { BaseEntity } from "./base.entity";

@model({
  settings: {
    idInjection: false,
    postgresql: { schema: "public", table: "sample" },
  },
})
export class Sample extends BaseEntity {
  @property({
    type: "string",
    length: 30,

    postgresql: {
      columnName: "short_string_prop",
      dataType: "character varying",
      dataLength: 30,
      dataPrecision: null,
      dataScale: null,
      nullable: "YES",
    },
  })
  shortStringProp?: string;

  @property({
    type: "string",
    length: 120,

    postgresql: {
      columnName: "long_string_prop",
      dataType: "character varying",
      dataLength: 120,
      dataPrecision: null,
      dataScale: null,
      nullable: "YES",
    },
  })
  longStringProp?: string;

  @property({
    type: "string",

    postgresql: {
      columnName: "text_prop",
      dataType: "text",
      nullable: "YES",
    },
  })
  textProp?: string;

  @property({
    type: "string",
    length: 1,

    postgresql: {
      columnName: "char_prop",
      dataType: "character",
      dataLength: 1,
      dataPrecision: null,
      dataScale: null,
      nullable: "YES",
    },
  })
  charProp?: string;

  @property({
    type: "boolean",

    postgresql: {
      columnName: "bool_prop",
      dataType: "boolean",
      nullable: "YES",
    },
  })
  boolProp?: boolean;

  @property({
    type: "string",
    required: false,
    postgresql: {
      columnName: "buffer_prop",
      dataType: "bytea",
      nullable: "YES",
    },
  })
  bufferProp?: string;

  @property({
    type: "number",
    scale: 0,

    postgresql: {
      columnName: "int_prop",
      dataType: "integer",
      dataLength: null,
      dataPrecision: null,
      dataScale: 0,
      nullable: "YES",
    },
  })
  intProp?: number;

  @property({
    type: "number",
    scale: 0,

    postgresql: {
      columnName: "bigint_prop",
      dataType: "bigint",
      dataLength: null,
      dataPrecision: null,
      dataScale: 0,
      nullable: "YES",
    },
  })
  bigintProp?: number;

  @property({
    type: "number",
    precision: 53,

    postgresql: {
      columnName: "double_prop",
      dataType: "float",
      dataLength: null,
      dataPrecision: 53,
      dataScale: null,
      nullable: "YES",
    },
  })
  doubleProp?: number;

  @property({
    type: "date",

    postgresql: {
      columnName: "date_prop",
      dataType: "date",
      nullable: "YES",
    },
  })
  dateProp?: string;

  @property({
    type: "date",

    postgresql: {
      columnName: "timestamptz_prop",
      dataType: "timestamp with time zone",
      nullable: "YES",
    },
  })
  timestamptzProp?: string;

  @property({
    type: "string",

    postgresql: {
      columnName: "time_prop",
      dataType: "time",
      nullable: "YES",
    },
  })
  timeProp?: string;

  @property({
    type: "string",

    postgresql: {
      columnName: "point_prop",
      dataType: "point",
      nullable: "YES",
    },
  })
  pointProp?: string;

  @property({
    type: "object",

    postgresql: {
      columnName: "json_prop",
      dataType: "json",
      nullable: "YES",
    },
  })
  jsonProp?: object;

  @property({
    type: "array",
    itemType: "string",

    postgresql: {
      columnName: "array_string_prop",
      dataType: "varchar[]",
      nullable: "YES",
    },
  })
  arrayStringProp?: string[];

  // Define well-known properties here

  // Indexer property to allow additional data
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  [prop: string]: any;

  constructor(data?: Partial<Sample>) {
    super(data);
  }
}

export interface Point {
  lat: number;
  lng: number;
}

export interface SampleRelations {
  // describe navigational properties here
}

export type SampleWithRelations = Sample & SampleRelations;

Data validation

import { model, property } from "@loopback/repository";
import { IsISO8601, IsNotEmpty, Max, MaxLength, Min } from "class-validator";
import { IsValidTime } from "./validator";

@model()
export class CreateSampleDto {
  @property({
    required: true,
  })
  @IsNotEmpty()
  @MaxLength(30)
  shortStringProp: string;

  @property({
    required: true,
  })
  @IsNotEmpty()
  @MaxLength(120)
  longStringProp?: string;

  @property()
  textProp?: string;

  @property()
  charProp?: string;

  @property()
  boolProp?: boolean;

  @property()
  bufferProp?: string;

  @property({
    required: true,
  })
  @IsNotEmpty()
  @Min(0)
  @Max(Number.MAX_SAFE_INTEGER)
  intProp?: number;

  @property({
    required: true,
  })
  @IsNotEmpty()
  @Min(0)
  @Max(Number.MAX_VALUE)
  bigintProp?: number;

  @property({
    required: true,
  })
  @IsNotEmpty()
  @Max(Number.MAX_VALUE)
  doubleProp?: number;

  @property()
  @IsISO8601()
  dateProp?: string;

  @property()
  @IsISO8601()
  timestamptzProp?: string;

  @property()
  @IsValidTime()
  timeProp?: string;

  @property()
  pointProp?: string;

  @property()
  jsonProp?: object;

  @property({
    type: "array",
    itemType: "string",
  })
  arrayStringProp?: string[];
}

Auto updated model properties createdAt and updatedAt

  • https://github.com/loopbackio/loopback-next/issues/1857

BaseEntity

// ./src/models/base.entity.ts
import { Entity, property } from "@loopback/repository";

export abstract class BaseEntity extends Entity {
  @property({
    type: "string",
    id: true,
    defaultFn: "uuid",
  })
  id?: string;

  @property({
    type: "Date",
    name: "created_at",
  })
  createdAt?: Date;

  @property({
    type: "Date",
    name: "updated_at",
  })
  updatedAt?: Date;

  constructor(data?: Partial<BaseEntity>) {
    super(data);
  }
}

// ./src/models/todo.model.ts
import { model, property } from "@loopback/repository";
import { BaseEntity } from "./base.entity";

@model()
export class Todo extends BaseEntity {
  @property({
    type: "string",
    required: true,
  })
  title: string;

  @property({
    type: "boolean",
  })
  isComplete?: boolean;

  constructor(data?: Partial<Todo>) {
    super(data);
  }
}

export interface TodoRelations {
  // describe navigational properties here
}

export type TodoWithRelations = Todo & TodoRelations;

TimestampRepositoryMixin

// ./src/mixins/time-stamp.mixin.ts
import { Constructor } from "@loopback/core";
import {
  Count,
  DataObject,
  Entity,
  EntityCrudRepository,
  Options,
  Where,
} from "@loopback/repository";

export function TimeStampRepositoryMixin<
  E extends Entity & { createdAt?: Date; updatedAt?: Date },
  ID,
  R extends Constructor<EntityCrudRepository<E, ID>>
>(repository: R) {
  class MixedRepository extends repository {
    async create(entity: DataObject<E>, options?: Options): Promise<E> {
      entity.createdAt = new Date();
      entity.updatedAt = new Date();
      return super.create(entity, options);
    }

    async updateAll(
      data: DataObject<E>,
      where?: Where<E>,
      options?: Options
    ): Promise<Count> {
      data.updatedAt = new Date();
      return super.updateAll(data, where, options);
    }

    async replaceById(
      id: ID,
      data: DataObject<E>,
      options?: Options
    ): Promise<void> {
      data.updatedAt = new Date();
      return super.replaceById(id, data, options);
    }

    async updateById(
      id: ID,
      data: DataObject<E>,
      options?: Options
    ): Promise<void> {
      data.updatedAt = new Date();
      return super.updateById(id, data, options);
    }
  }
  return MixedRepository;
}

// ./src/repositories/todo.repository.ts

import { TimeStampRepositoryMixin } from "@english/mixins/time-stamp.mixin";
import { Constructor, inject } from "@loopback/core";
import { DefaultCrudRepository } from "@loopback/repository";
import { EnglishDataSource } from "../datasources";
import { Todo, TodoRelations } from "../models";

export class TodoRepository extends TimeStampRepositoryMixin<
  Todo,
  typeof Todo.prototype.id,
  Constructor<
    DefaultCrudRepository<Todo, typeof Todo.prototype.id, TodoRelations>
  >
>(DefaultCrudRepository) {
  constructor(@inject("datasources.english") dataSource: EnglishDataSource) {
    super(Todo, dataSource);
  }
}

Migrate data to create/update schema

image

"migrate:setup": "\"npm run rebuild\" && node ./dist/migrate --rebuild",
"migrate": "\"npm run rebuild\" && node ./dist/migrate node ./dist/migrate",
// ./migrate.ts
import { EnglishApi } from "./application";
import { Task, Todo } from "./models";
import { Sample } from "./models/sample.model";

export async function migrate(args: string[]) {
  const existingSchema = args.includes("--rebuild") ? "drop" : "alter";
  console.log("Migrating schemas (%s existing schema)", existingSchema);

  const app = new EnglishApi();
  await app.boot();
  await app.migrateSchema({
    existingSchema,
    // The order of table creation is important.
    // A referenced table must exist before creating a
    // foreign key constraint.
    // For PostgreSQL connector, it does not create tables in the
    // right order.  Therefore, this change is needed.
    models: [Todo.name, Task.name, Sample.name],
  });

  // Connectors usually keep a pool of opened connections,
  // this keeps the process running even after all work is done.
  // We need to exit explicitly.
  process.exit(0);
}

migrate(process.argv).catch((err) => {
  console.error("Cannot migrate database schema", err);
  process.exit(1);
});

Relations

One To One Relationship

image

// employee.model.ts
/* eslint-disable @typescript-eslint/naming-convention */
import { model, property } from "@loopback/repository";
import { hasOne } from "@loopback/repository/dist/relations";
import { BaseEntity } from "./base.entity";
import { Vehicle } from "./vehicle.model";

@model({
  settings: {
    scope: {
      limit: 100,
    },
    indexes: {
      name_idx: {
        keys: {
          name: 1,
        },
        options: {
          unique: false,
        },
      },
    },
  },
})
export class Employee extends BaseEntity {
  @property({
    type: "string",
    required: true,
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 120,
    },
  })
  name: string;

  @property({
    type: "string",
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 120,
    },
  })
  position?: string;

  @hasOne(() => Vehicle, { keyTo: "employee_id", keyFrom: "id" })
  vehicle: Vehicle;

  constructor(data?: Partial<Employee>) {
    super(data);
  }
}

export interface EmployeeRelations {
  // describe navigational properties here
}

export type EmployeeWithRelations = Employee & EmployeeRelations;

// vehicle.model.ts
import { Employee } from "./employee.model";
/* eslint-disable @typescript-eslint/naming-convention */
import { belongsTo, model, property } from "@loopback/repository";
import { BaseEntity } from "./base.entity";

export enum VehicleType {
  BIKE = 1,
  MOTOR_BIKE = 2,
  CAR = 3,
}

@model({
  settings: {
    foreignKeys: {
      fk_vehicle_employeeId: {
        name: "fk_vehicle_employeeId",
        entity: "Employee",
        entityKey: "id",
        foreignKey: "employee_id",
        onDelete: "CASCADE",
        onUpdate: "RESTRICT",
      },
    },
  },
})
export class Vehicle extends BaseEntity {
  @property({
    type: "string",
    required: true,
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 120,
    },
  })
  name: string;

  @property({
    type: "number",
    name: "vehicle_type",
    postgresql: {
      dataType: "integer",
    },
  })
  vehicleType?: VehicleType;

  @belongsTo(() => Employee, { name: "employee" })
  employee_id: string;

  constructor(data?: Partial<Vehicle>) {
    super(data);
  }
}

export interface VehicleRelations {
  // describe navigational properties here
}

export type VehicleWithRelations = Vehicle & VehicleRelations;

// employee.repository.ts
import { TimeStampRepositoryMixin } from "@english/mixins/time-stamp.mixin";
import { Constructor, Getter, inject } from "@loopback/core";
import {
  DefaultCrudRepository,
  HasOneRepositoryFactory,
  repository,
} from "@loopback/repository";
import { EnglishDataSource } from "../datasources";
import { Employee, EmployeeRelations } from "../models";
import { Vehicle } from "../models/vehicle.model";
import { VehicleRepository } from "./vehicle.repository";

export class EmployeeRepository extends TimeStampRepositoryMixin<
  Employee,
  typeof Employee.prototype.id,
  Constructor<
    DefaultCrudRepository<
      Employee,
      typeof Employee.prototype.id,
      EmployeeRelations
    >
  >
>(DefaultCrudRepository) {
  public readonly vehicle: HasOneRepositoryFactory<
    Vehicle,
    typeof Employee.prototype.id
  >;

  constructor(
    @inject("datasources.english") dataSource: EnglishDataSource,
    @repository.getter("VehicleRepository")
    getVehicleRepository: Getter<VehicleRepository>
  ) {
    super(Employee, dataSource);
    this.vehicle = this.createHasOneRepositoryFactoryFor(
      "vehicle",
      getVehicleRepository
    );
    // add this line to register inclusion resolver
    this.registerInclusionResolver("vehicle", this.vehicle.inclusionResolver);
  }
}
// employee.controller.ts
import {
  Count,
  CountSchema,
  Filter,
  FilterExcludingWhere,
  repository,
  Where,
} from "@loopback/repository";
import {
  del,
  get,
  getModelSchemaRef,
  param,
  patch,
  post,
  put,
  requestBody,
  response,
} from "@loopback/rest";
import { Employee } from "../models";
import { EmployeeRepository } from "../repositories";
import { Vehicle } from "./../models/vehicle.model";

export class EmployeeController {
  @post("/employees/{id}/vehicle")
  @response(200, {
    description: "Employee Vehicle POST success",
  })
  async(
    @param.path.string("id") employeeId: typeof Employee.prototype.id,
    @requestBody({
      content: {
        "application/json": {
          schema: getModelSchemaRef(Vehicle, { partial: true }),
        },
      },
    })
    vehicleData: Vehicle
  ): Promise<Vehicle> {
    return this.employeeRepository.vehicle(employeeId).create(vehicleData);
  }
}

One to Many

image

import { model, property, hasMany } from "@loopback/repository";
import { BaseEntity } from "./base.entity";
import { Order } from "./order.model";

@model()
export class Customer extends BaseEntity {
  @property({
    type: "string",
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 60,
    },
  })
  name?: string;

  @property({
    type: "string",
    name: "phone_number",
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 20,
    },
  })
  phoneNumber?: string;

  @property({
    type: "string",
    name: "email_address",
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 120,
    },
  })
  emailAddress?: string;

  @hasMany(() => Order)
  orders: Order[];

  constructor(data?: Partial<Customer>) {
    super(data);
  }
}

export interface CustomerRelations {
  // describe navigational properties here
}

export type CustomerWithRelations = Customer & CustomerRelations;
import { belongsTo, model, property } from "@loopback/repository";
import { BaseEntity } from "./base.entity";
import { Customer } from "./customer.model";

@model()
export class Order extends BaseEntity {
  @property({
    type: "date",
    name: "delivery_date",
    postgresql: {
      dataType: "timestamp with time zone",
      nullable: "YES",
    },
  })
  deliveryDate?: string;

  @belongsTo(
    () => Customer,
    {},
    {
      name: "customer_id",
      postgresql: {
        dataType: "VARCHAR",
        dataLength: 36,
      },
    }
  )
  customerId: string;

  constructor(data?: Partial<Order>) {
    super(data);
  }
}

export interface OrderRelations {
  // describe navigational properties here
}

export type OrderWithRelations = Order & OrderRelations;

Self-Relationship

/* eslint-disable @typescript-eslint/naming-convention */
import { belongsTo, hasMany, model, property } from "@loopback/repository";
import { hasOne } from "@loopback/repository/dist/relations";
import { BaseEntity } from "./base.entity";
import { Vehicle } from "./vehicle.model";

@model({
  settings: {
    scope: {
      limit: 100,
    },
    indexes: {
      name_idx: {
        keys: {
          name: 1,
        },
        options: {
          unique: false,
        },
      },
    },
  },
})
export class Employee extends BaseEntity {
  @property({
    type: "string",
    required: true,
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 120,
    },
  })
  name: string;

  @property({
    type: "string",
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 120,
    },
  })
  position?: string;

  @hasOne(() => Vehicle, { keyTo: "employeeId", keyFrom: "id" })
  vehicle: Vehicle;

  @hasMany(() => Employee, { keyTo: "managerId" })
  employees: Employee[];

  @belongsTo(
    () => Employee,
    { name: "manager" },
    {
      name: "manager_id",
      postgresql: {
        dataType: "VARCHAR",
        dataLength: 36,
      },
    }
  )
  managerId?: string;

  constructor(data?: Partial<Employee>) {
    super(data);
  }
}

export interface EmployeeRelations {
  // describe navigational properties here
}

export type EmployeeWithRelations = Employee & EmployeeRelations;
import { TimeStampRepositoryMixin } from "@english/mixins/time-stamp.mixin";
import { Constructor, Getter, inject } from "@loopback/core";
import {
  BelongsToAccessor,
  DefaultCrudRepository,
  HasManyRepositoryFactory,
  HasOneRepositoryFactory,
  repository,
} from "@loopback/repository";
import { EnglishDataSource } from "../datasources";
import { Employee, EmployeeRelations } from "../models";
import { Vehicle } from "../models/vehicle.model";
import { VehicleRepository } from "./vehicle.repository";

export class EmployeeRepository extends TimeStampRepositoryMixin<
  Employee,
  typeof Employee.prototype.id,
  Constructor<
    DefaultCrudRepository<
      Employee,
      typeof Employee.prototype.id,
      EmployeeRelations
    >
  >
>(DefaultCrudRepository) {
  public readonly vehicle: HasOneRepositoryFactory<
    Vehicle,
    typeof Employee.prototype.id
  >;

  public readonly employees: HasManyRepositoryFactory<
    Employee,
    typeof Employee.prototype.id
  >;

  public readonly manager: BelongsToAccessor<
    Employee,
    typeof Employee.prototype.id
  >;

  constructor(
    @inject("datasources.english") dataSource: EnglishDataSource,
    @repository.getter("VehicleRepository")
    getVehicleRepository: Getter<VehicleRepository>,
    @repository.getter("EmployeeRepository")
    protected employeeRepositoryGetter: Getter<EmployeeRepository>
  ) {
    super(Employee, dataSource);
    this.manager = this.createBelongsToAccessorFor(
      "manager",
      employeeRepositoryGetter
    );
    this.employees = this.createHasManyRepositoryFactoryFor(
      "employees",
      employeeRepositoryGetter
    );
    this.vehicle = this.createHasOneRepositoryFactoryFor(
      "vehicle",
      getVehicleRepository
    );
    // add this line to register inclusion resolver
    this.registerInclusionResolver("vehicle", this.vehicle.inclusionResolver);
    this.registerInclusionResolver("manager", this.manager.inclusionResolver);
  }
}

Many to Many Relationship ( HasMany + HasManyThrough )

image

Models

import { hasMany, model, property } from "@loopback/repository";
import { BaseEntity } from "./base.entity";
import { BookAuthor } from "./book-author.model";
import { Book } from "./book.model";

@model()
export class Author extends BaseEntity {
  @property({
    type: "string",
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 60,
    },
  })
  name?: string;

  @hasMany(() => Book, { through: { model: () => BookAuthor } })
  books: Book[];

  constructor(data?: Partial<Author>) {
    super(data);
  }
}

export interface AuthorRelations {
  // describe navigational properties here
}

export type AuthorWithRelations = Author & AuthorRelations;

import { hasMany, model, property } from "@loopback/repository";
import { Author } from "./author.model";
import { BaseEntity } from "./base.entity";
import { BookAuthor } from "./book-author.model";

@model()
export class Book extends BaseEntity {
  @property({
    type: "string",
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 60,
    },
  })
  name?: string;

  @hasMany(() => Author, { through: { model: () => BookAuthor } })
  authors: Author[];

  constructor(data?: Partial<Book>) {
    super(data);
  }
}

export interface BookRelations {
  // describe navigational properties here
}

export type BookWithRelations = Book & BookRelations;

import { model, property } from "@loopback/repository";
import { BaseEntity } from "./base.entity";

@model()
export class BookAuthor extends BaseEntity {
  @property({
    type: "string",
    name: "book_id",
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 36,
    },
  })
  bookId?: string;

  @property({
    type: "string",
    name: "author_id",
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 36,
    },
  })
  authorId?: string;

  constructor(data?: Partial<BookAuthor>) {
    super(data);
  }
}

export interface BookAuthorRelations {
  // describe navigational properties here
}

export type BookAuthorWithRelations = BookAuthor & BookAuthorRelations;

DTO

import { Author, Book } from "@english/models";
import { property } from "@loopback/repository";

export class BookDto extends Book {
  @property({
    type: "array",
    itemType: "string",
  })
  authorIds: typeof Author.prototype.id[];
}

Repositories

/* eslint-disable @typescript-eslint/naming-convention */
import { TimeStampRepositoryMixin } from "@english/mixins/time-stamp.mixin";
import { Constructor, Getter, inject } from "@loopback/core";
import {
  DefaultCrudRepository,
  HasManyThroughRepositoryFactory,
  repository,
} from "@loopback/repository";
import { EnglishDataSource } from "../datasources";
import { Author, AuthorRelations, Book, BookAuthor } from "../models";
import { BookAuthorRepository } from "./book-author.repository";
import { BookRepository } from "./book.repository";

export class AuthorRepository extends TimeStampRepositoryMixin<
  Author,
  typeof Author.prototype.id,
  Constructor<
    DefaultCrudRepository<Author, typeof Author.prototype.id, AuthorRelations>
  >
>(DefaultCrudRepository) {
  public readonly books: HasManyThroughRepositoryFactory<
    Book,
    typeof Book.prototype.id,
    BookAuthor,
    typeof Author.prototype.id
  >;

  constructor(
    @inject("datasources.english") dataSource: EnglishDataSource,
    @repository.getter("BookAuthorRepository")
    protected BookAuthorRepositoryGetter: Getter<BookAuthorRepository>,
    @repository.getter("BookRepository")
    protected bookRepositoryGetter: Getter<BookRepository>
  ) {
    super(Author, dataSource);
    this.books = this.createHasManyThroughRepositoryFactoryFor(
      "books",
      bookRepositoryGetter,
      BookAuthorRepositoryGetter
    );
    this.registerInclusionResolver("books", this.books.inclusionResolver);
  }
}

/* eslint-disable @typescript-eslint/naming-convention */
import { TimeStampRepositoryMixin } from "@english/mixins/time-stamp.mixin";
import { Constructor, Getter, inject } from "@loopback/core";
import {
  DefaultCrudRepository,
  HasManyThroughRepositoryFactory,
  repository,
} from "@loopback/repository";
import { EnglishDataSource } from "../datasources";
import { Author, Book, BookAuthor, BookRelations } from "../models";
import { AuthorRepository } from "./author.repository";
import { BookAuthorRepository } from "./book-author.repository";

export class BookRepository extends TimeStampRepositoryMixin<
  Book,
  typeof Book.prototype.id,
  Constructor<
    DefaultCrudRepository<Book, typeof Book.prototype.id, BookRelations>
  >
>(DefaultCrudRepository) {
  public readonly authors: HasManyThroughRepositoryFactory<
    Author,
    typeof Author.prototype.id,
    BookAuthor,
    typeof Book.prototype.id
  >;

  constructor(
    @inject("datasources.english") dataSource: EnglishDataSource,
    @repository.getter("BookAuthorRepository")
    protected BookAuthorRepositoryGetter: Getter<BookAuthorRepository>,
    @repository.getter("AuthorRepository")
    protected authorRepositoryGetter: Getter<AuthorRepository>
  ) {
    super(Book, dataSource);
    this.authors = this.createHasManyThroughRepositoryFactoryFor(
      "authors",
      authorRepositoryGetter,
      BookAuthorRepositoryGetter
    );
    this.registerInclusionResolver("authors", this.authors.inclusionResolver);
  }
}

import { inject } from "@loopback/core";
import { DefaultCrudRepository } from "@loopback/repository";
import { EnglishDataSource } from "../datasources";
import { BookAuthor, BookAuthorRelations } from "../models";

export class BookAuthorRepository extends DefaultCrudRepository<
  BookAuthor,
  typeof BookAuthor.prototype.bookId,
  BookAuthorRelations
> {
  constructor(@inject("datasources.english") dataSource: EnglishDataSource) {
    super(BookAuthor, dataSource);
  }
}

Controllers

import {
  Count,
  CountSchema,
  Filter,
  FilterExcludingWhere,
  repository,
  Where,
} from "@loopback/repository";
import {
  post,
  param,
  get,
  getModelSchemaRef,
  patch,
  put,
  del,
  requestBody,
  response,
} from "@loopback/rest";
import { Author } from "../models";
import { AuthorRepository } from "../repositories";

export class AuthorController {
  constructor(
    @repository(AuthorRepository)
    public authorRepository: AuthorRepository
  ) {}

  @post("/authors")
  @response(200, {
    description: "Author model instance",
    content: { "application/json": { schema: getModelSchemaRef(Author) } },
  })
  async create(
    @requestBody({
      content: {
        "application/json": {
          schema: getModelSchemaRef(Author, {
            title: "NewAuthor",
            exclude: ["id"],
          }),
        },
      },
    })
    author: Omit<Author, "id">
  ): Promise<Author> {
    return this.authorRepository.create(author);
  }

  @get("/authors/count")
  @response(200, {
    description: "Author model count",
    content: { "application/json": { schema: CountSchema } },
  })
  async count(@param.where(Author) where?: Where<Author>): Promise<Count> {
    return this.authorRepository.count(where);
  }

  @get("/authors")
  @response(200, {
    description: "Array of Author model instances",
    content: {
      "application/json": {
        schema: {
          type: "array",
          items: getModelSchemaRef(Author, { includeRelations: true }),
        },
      },
    },
  })
  async find(@param.filter(Author) filter?: Filter<Author>): Promise<Author[]> {
    return this.authorRepository.find(filter);
  }

  @patch("/authors")
  @response(200, {
    description: "Author PATCH success count",
    content: { "application/json": { schema: CountSchema } },
  })
  async updateAll(
    @requestBody({
      content: {
        "application/json": {
          schema: getModelSchemaRef(Author, { partial: true }),
        },
      },
    })
    author: Author,
    @param.where(Author) where?: Where<Author>
  ): Promise<Count> {
    return this.authorRepository.updateAll(author, where);
  }

  @get("/authors/{id}")
  @response(200, {
    description: "Author model instance",
    content: {
      "application/json": {
        schema: getModelSchemaRef(Author, { includeRelations: true }),
      },
    },
  })
  async findById(
    @param.path.string("id") id: string,
    @param.filter(Author, { exclude: "where" })
    filter?: FilterExcludingWhere<Author>
  ): Promise<Author> {
    return this.authorRepository.findById(id, filter);
  }

  @patch("/authors/{id}")
  @response(204, {
    description: "Author PATCH success",
  })
  async updateById(
    @param.path.string("id") id: string,
    @requestBody({
      content: {
        "application/json": {
          schema: getModelSchemaRef(Author, { partial: true }),
        },
      },
    })
    author: Author
  ): Promise<void> {
    await this.authorRepository.updateById(id, author);
  }

  @put("/authors/{id}")
  @response(204, {
    description: "Author PUT success",
  })
  async replaceById(
    @param.path.string("id") id: string,
    @requestBody() author: Author
  ): Promise<void> {
    await this.authorRepository.replaceById(id, author);
  }

  @del("/authors/{id}")
  @response(204, {
    description: "Author DELETE success",
  })
  async deleteById(@param.path.string("id") id: string): Promise<void> {
    await this.authorRepository.deleteById(id);
  }
}

import { toArrayPromise } from "@english/helpers";
import {
  Count,
  CountSchema,
  Filter,
  FilterExcludingWhere,
  repository,
  Where,
} from "@loopback/repository";
import {
  del,
  get,
  getModelSchemaRef,
  param,
  patch,
  post,
  put,
  requestBody,
  response,
} from "@loopback/rest";
import { Book } from "../models";
import { BookRepository } from "../repositories";
import { BookDto } from "./../dtos/book.dto";

export class BookController {
  constructor(
    @repository(BookRepository)
    public bookRepository: BookRepository
  ) {}

  @post("/books")
  @response(200, {
    description: "Book model instance",
    content: { "application/json": { schema: getModelSchemaRef(Book) } },
  })
  async create(
    @requestBody({
      content: {
        "application/json": {
          schema: getModelSchemaRef(BookDto, {
            title: "NewBook",
          }),
        },
      },
    })
    book: Omit<BookDto, "id">
  ): Promise<Book> {
    const { authorIds, ...bookData } = book;
    const b = await this.bookRepository.create(bookData);
    if (b) {
      // link
      await Promise.all(
        toArrayPromise(authorIds, (authorId: string) => {
          return this.bookRepository.authors(b.id).link(authorId);
        })
      );
    }
    return this.bookRepository.findById(b.id, { include: ["authors"] });
  }

  @get("/books/count")
  @response(200, {
    description: "Book model count",
    content: { "application/json": { schema: CountSchema } },
  })
  async count(@param.where(Book) where?: Where<Book>): Promise<Count> {
    return this.bookRepository.count(where);
  }

  @get("/books")
  @response(200, {
    description: "Array of Book model instances",
    content: {
      "application/json": {
        schema: {
          type: "array",
          items: getModelSchemaRef(Book, { includeRelations: true }),
        },
      },
    },
  })
  async find(@param.filter(Book) filter?: Filter<Book>): Promise<Book[]> {
    return this.bookRepository.find(filter);
  }

  @patch("/books")
  @response(200, {
    description: "Book PATCH success count",
    content: { "application/json": { schema: CountSchema } },
  })
  async updateAll(
    @requestBody({
      content: {
        "application/json": {
          schema: getModelSchemaRef(Book, { partial: true }),
        },
      },
    })
    book: Book,
    @param.where(Book) where?: Where<Book>
  ): Promise<Count> {
    return this.bookRepository.updateAll(book, where);
  }

  @get("/books/{id}")
  @response(200, {
    description: "Book model instance",
    content: {
      "application/json": {
        schema: getModelSchemaRef(Book, { includeRelations: true }),
      },
    },
  })
  async findById(
    @param.path.string("id") id: string,
    @param.filter(Book, { exclude: "where" })
    filter?: FilterExcludingWhere<Book>
  ): Promise<Book> {
    return this.bookRepository.findById(id, filter);
  }

  @patch("/books/{id}")
  @response(204, {
    description: "Book PATCH success",
  })
  async updateById(
    @param.path.string("id") id: string,
    @requestBody({
      content: {
        "application/json": {
          schema: getModelSchemaRef(BookDto, { partial: true }),
        },
      },
    })
    book: BookDto
  ): Promise<void> {
    const exitedLinks = await this.bookRepository.authors(id).find();
    if (exitedLinks.length > 0) {
      await this.bookRepository.authors(id).unlinkAll();
    }
    await Promise.all(
      toArrayPromise(book.authorIds, (authorId: string) => {
        return this.bookRepository.authors(id).link(authorId);
      })
    );
    // await this.bookRepository.authors(id).link(book.authorId);
    await this.bookRepository.updateById(id, book);
  }

  @put("/books/{id}")
  @response(204, {
    description: "Book PUT success",
  })
  async replaceById(
    @param.path.string("id") id: string,
    @requestBody() book: Book
  ): Promise<void> {
    await this.bookRepository.replaceById(id, book);
  }

  @del("/books/{id}")
  @response(204, {
    description: "Book DELETE success",
  })
  async deleteById(@param.path.string("id") id: string): Promise<void> {
    await this.bookRepository.deleteById(id);
  }
}

One To Many ( References Many )

import { model, property, referencesMany } from "@loopback/repository";
import { BaseEntity } from "./base.entity";
import { Store } from "./store.model";

@model()
export class Location extends BaseEntity {
  @property({
    type: "string",
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 60,
    },
  })
  name?: string;

  @referencesMany(() => Store)
  storeIds: string[];

  constructor(data?: Partial<Location>) {
    super(data);
  }
}

export interface LocationRelations {
  // describe navigational properties here
}

export type LocationWithRelations = Location & LocationRelations;

import { model, property } from "@loopback/repository";
import { BaseEntity } from "./base.entity";

@model()
export class Store extends BaseEntity {
  @property({
    type: "string",
    postgresql: {
      dataType: "VARCHAR",
      dataLength: 60,
    },
  })
  name?: string;
  constructor(data?: Partial<Store>) {
    super(data);
  }
}

export interface StoreRelations {
  // describe navigational properties here
}

export type StoreWithRelations = Store & StoreRelations;
import { Constructor, inject, Getter } from "@loopback/core";

import {
  DefaultCrudRepository,
  repository,
  ReferencesManyAccessor,
} from "@loopback/repository";
import { EnglishDataSource } from "../datasources";
import { Location, LocationRelations, Store } from "../models";

import { DATASOURCE } from "@english/config/environment";
import { TimeStampRepositoryMixin } from "@english/mixins/time-stamp.mixin";
import { StoreRepository } from "./store.repository";

export class LocationRepository extends TimeStampRepositoryMixin<
  Location,
  typeof Location.prototype.id,
  Constructor<
    DefaultCrudRepository<
      Location,
      typeof Location.prototype.id,
      LocationRelations
    >
  >
>(DefaultCrudRepository) {
  public readonly stores: ReferencesManyAccessor<
    Store,
    typeof Location.prototype.id
  >;

  constructor(
    @inject(`datasources.${DATASOURCE}`) dataSource: EnglishDataSource,
    @repository.getter("StoreRepository")
    protected storeRepositoryGetter: Getter<StoreRepository>
  ) {
    super(Location, dataSource);
    this.stores = this.createReferencesManyAccessorFor(
      "stores",
      storeRepositoryGetter
    );
    this.registerInclusionResolver("stores", this.stores.inclusionResolver);
  }
}

import { Constructor, inject } from "@loopback/core";

import { DefaultCrudRepository } from "@loopback/repository";
import { EnglishDataSource } from "../datasources";
import { Store, StoreRelations } from "../models";

import { DATASOURCE } from "@english/config/environment";
import { TimeStampRepositoryMixin } from "@english/mixins/time-stamp.mixin";

export class StoreRepository extends TimeStampRepositoryMixin<
  Store,
  typeof Store.prototype.id,
  Constructor<
    DefaultCrudRepository<Store, typeof Store.prototype.id, StoreRelations>
  >
>(DefaultCrudRepository) {
  constructor(
    @inject(`datasources.${DATASOURCE}`) dataSource: EnglishDataSource
  ) {
    super(Store, dataSource);
  }
}

Generate relation with CLI

lb4 relation

Filter

curl --location -g --request GET 'http://localhost:1337/employees?filter[include][0][relation]=vehicle&filter[include][0][scope][where][vehicleType]=1&filter[include][1][relation]=manager'

Indexes

Tests

image

image

Tools

Securing Application

Authentication is a process of verifying user/entity to the system, which enables identified/validated access to the protected routes.

image

Authorization is a process of deciding if a user can perform an action on a protected resource.

image

Authentication

npm i @loopback/authentication

Authorization

npm i @loopback/authorization

Casbin

Last Updated:
Contributors: misostack