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

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

"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

// 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

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 )

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
- https://strongloop.com/strongblog/loopback-index-support-cloudant-model/
- Defining index for a model in @model decorator #2753
Tests


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

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

Authentication
npm i @loopback/authentication
Authorization
npm i @loopback/authorization