A Repository represents a specialized Service interface that providesstrong-typed data access (for example, CRUD) operations of a domain modelagainst the underlying database or service.

Repository diagram

Note:Repositories are adding behavior to Models. Models describe the shape of data, Repositories provide behavior like CRUD operations. This is different from LoopBack 3.x where models implement behavior too.

Tip: A single model can be used with multiple different Repositories.

A Repository can be defined and implemented by application developers.LoopBack ships a few predefined Repository interfaces for typical CRUD and KVoperations. These Repository implementations leverage Model definition andDataSource configuration to fulfill the logic for data access.

  1. interface Repository<T extends Model> {}
  2. interface CustomerRepository extends Repository<Customer> {
  3. find(filter?: Filter<Customer>, options?: Options): Promise<Customer[]>;
  4. findByEmail(email: string): Promise<Customer>;
  5. // ...
  6. }

See more examples at:

Installation

Legacy juggler support has been enabled in loopback-next and can be importedfrom the @loopback/repository package. In order to do this, save@loopback/repository as a dependency in your application.

You can then install your favorite connector by saving it as part of yourapplication dependencies.

Repository Mixin

@loopback/repository provides a mixin for your Application that enablesconvenience methods that automatically bind repository classes for you.Repositories declared by components are also bound automatically.

Repositories are bound to repositories.${ClassName}. See example below forusage.

  1. import {Application} from '@loopback/core';
  2. import {RepositoryMixin} from '@loopback/repository';
  3. import {AccountRepository, CategoryRepository} from './repositories';
  4. // Using the Mixin
  5. class MyApplication extends RepositoryMixin(Application) {}
  6. const app = new MyApplication();
  7. // AccountRepository will be bound to key `repositories.AccountRepository`
  8. app.repository(AccountRepository);
  9. // CategoryRepository will be bound to key `repositories.CategoryRepository`
  10. app.repository(CategoryRepository);

Configure datasources

DataSource is a named configuration of a connector. The configurationproperties vary by connectors. For example, a datasource for MySQL needs toset the connector property to loopback-connector-mysql with settings asfollows:

  1. {
  2. "host": "localhost",
  3. "port": 3306,
  4. "user": "my-user",
  5. "password": "my-password",
  6. "database": "demo"
  7. }

Connector is a provider that implements data access or api calls with aspecific backend system, such as a database, a REST service, a SOAP Web Service,or a gRPC micro-service. It abstracts such interactions as a list of operationsin the form of Node.js methods.

Typically, a connector translates LoopBack query and mutation requests intonative api calls supported by the underlying Node.js driver for the givenbackend. For example, a connector for MySQL will map create method to SQLINSERT statement, which can be executed through MySQL driver for Node.js.

When a DataSource is instantiated, the configuration properties will be usedto initialize the connector to connect to the backend system. You can define aDataSource using legacy Juggler in your LoopBack 4 app as follows:

src/datsources/db.datasource.ts

  1. import {juggler} from '@loopback/repository';
  2. // this is just an example, 'test' database doesn't actually exist
  3. export const db = new juggler.DataSource({
  4. connector: 'mysql',
  5. host: 'localhost',
  6. port: 3306,
  7. database: 'test',
  8. password: 'pass',
  9. user: 'root',
  10. });

Define models

Models are defined as regular JavaScript classes. If you want your model to bepersisted in a database, your model must have an id property and inherit fromEntity base class.

TypeScript version:

  1. import {Entity, model, property} from '@loopback/repository';
  2. @model()
  3. export class Account extends Entity {
  4. @property({id: true})
  5. id: number;
  6. @property({required: true})
  7. name: string;
  8. }

JavaScript version:

  1. import {Entity, ModelDefinition} from '@loopback/repository';
  2. export class Account extends Entity {}
  3. Account.definition = new ModelDefinition({
  4. name: 'Account',
  5. properties: {
  6. id: {type: 'number', id: true},
  7. name: {type: 'string', required: true},
  8. },
  9. });

Define repositories

Use DefaultCrudRepository class to create a repository leveraging the legacyjuggler bridge and binding your Entity-based class with a datasource you haveconfigured earlier. It’s recommended that you useDependency Injection to retrieve your datasource.

TypeScript version:

  1. import {DefaultCrudRepository, juggler} from '@loopback/repository';
  2. import {Account, AccountRelations} from '../models';
  3. import {DbDataSource} from '../datasources';
  4. import {inject} from '@loopback/context';
  5. export class AccountRepository extends DefaultCrudRepository<
  6. Account,
  7. typeof Account.prototype.id,
  8. AccountRelations
  9. > {
  10. constructor(@inject('datasources.db') dataSource: DbDataSource) {
  11. super(Account, dataSource);
  12. }
  13. }

JavaScript version:

  1. import {DefaultCrudRepository} from '@loopback/repository';
  2. import {Account} from '../models/account.model';
  3. import {db} from '../datasources/db.datasource';
  4. export class AccountRepository extends DefaultCrudRepository {
  5. constructor() {
  6. super(Account, db);
  7. }
  8. }

Controller Configuration

Once your DataSource is defined for your repository, all the CRUD methods youcall in your repository will use the Juggler and your connector’s methods unlessyou overwrite them. In your controller, you will need to define a repositoryproperty and create a new instance of the repository you configured yourDataSource for in the constructor of your controller class as follows:

  1. export class AccountController {
  2. constructor(
  3. @repository(AccountRepository) public repository: AccountRepository,
  4. ) {}
  5. }

Defining CRUD methods for your application

When you want to define new CRUD methods for your application, you will need tomodify the API Definitions and their corresponding methods in your controller.Here are examples of some basic CRUD methods:

  • Create API Definition:
  1. {
  2. "/accounts/create": {
  3. "post": {
  4. "x-operation-name": "createAccount",
  5. "requestBody": {
  6. "description": "The account instance to create.",
  7. "required": true,
  8. "content": {
  9. "application/json": {
  10. "schema": {
  11. "type": "object"
  12. }
  13. }
  14. }
  15. },
  16. "responses": {
  17. "200": {
  18. "description": "Account instance created",
  19. "content": {
  20. "application/json": {
  21. "schema": {
  22. "$ref": "#/components/schemas/Account"
  23. }
  24. }
  25. }
  26. }
  27. }
  28. }
  29. }
  30. }

Create Controller method:

  1. async createAccount(accountInstance: Account) {
  2. return this.repository.create(accountInstance);
  3. }
  • Find API Definition:
  1. {
  2. "/accounts": {
  3. "get": {
  4. "x-operation-name": "getAccount",
  5. "responses": {
  6. "200": {
  7. "description": "List of accounts",
  8. "content": {
  9. "application/json": {
  10. "schema": {
  11. "type": "array",
  12. "items": {
  13. "$ref": "#/components/schemas/Account"
  14. }
  15. }
  16. }
  17. }
  18. }
  19. }
  20. }
  21. }
  22. }

Find Controller method:

  1. async getAccount() {
  2. return this.repository.find();
  3. }

Don’t forget to register the complete version of your OpenAPI spec throughapp.api().

Please See Testing Your Application section inorder to set up and write unit, acceptance, and integration tests for yourapplication.

Access KeyValue Stores

We can now access key-value stores such as Redis using theKeyValueRepository.

Define a KeyValue Datasource

We first need to define a datasource to configure the key-value store. Forbetter flexibility, we split the datasource definition into two files. The jsonfile captures the configuration properties and it can be possibly overridden bydependency injection.

  • redis.datasource.json
  1. {
  2. "name": "redis",
  3. "connector": "kv-redis",
  4. "host": "127.0.0.1",
  5. "port": 6379,
  6. "password": "",
  7. "db": 0
  8. }
  • redis.datasource.tsThe class uses a configuration object to set up a datasource for the Redisinstance. By default, the configuration is loaded from redis.datasource.json.We can override it by binding a new object to datasources.config.redis for acontext.
  1. import {inject} from '@loopback/core';
  2. import {juggler, AnyObject} from '@loopback/repository';
  3. import * as config from './redis.datasource.json';
  4. export class RedisDataSource extends juggler.DataSource {
  5. static dataSourceName = 'redis';
  6. constructor(
  7. @inject('datasources.config.redis', {optional: true})
  8. dsConfig: AnyObject = config,
  9. ) {
  10. super(dsConfig);
  11. }
  12. }

To generate the datasource automatically, use lb4 datasource command andselect Redis key-value connector (supported by StrongLoop).

Define a KeyValueRepository

The KeyValueRepository binds a model such as ShoppingCart to theRedisDataSource. The base DefaultKeyValueRepository class provides animplementation based on loopback-datasource-juggler.

  1. import {DefaultKeyValueRepository} from '@loopback/repository';
  2. import {ShoppingCart} from '../models/shopping-cart.model';
  3. import {RedisDataSource} from '../datasources/redis.datasource';
  4. import {inject} from '@loopback/context';
  5. export class ShoppingCartRepository extends DefaultKeyValueRepository<
  6. ShoppingCart
  7. > {
  8. constructor(@inject('datasources.redis') ds: RedisDataSource) {
  9. super(ShoppingCart, ds);
  10. }
  11. }

Perform Key Value Operations

The KeyValueRepository provides a set of key based operations, such as set,get, delete, expire, ttl, and keys. SeeKeyValueRepositoryfor a complete list.

  1. // Please note the ShoppingCartRepository can be instantiated using Dependency
  2. // Injection
  3. const repo: ShoppingCartRepository =
  4. new ShoppingCartRepository(new RedisDataSource());
  5. const cart1: ShoppingCart = givenShoppingCart1();
  6. const cart2: ShoppingCart = givenShoppingCart2();
  7. async function testKV() {
  8. // Store carts using userId as the key
  9. await repo.set(cart1.userId, cart1);
  10. await repo.set(cart2.userId, cart2);
  11. // Retrieve a cart by its key
  12. const result = await repo.get(cart1.userId);
  13. console.log(result);
  14. });
  15. testKV();

Persist Data without Juggler [Using MySQL database]

Important: This section has not been updated and codeexamples may not work out of the box.

LoopBack 4 gives you the flexibility to create your own custom Datasources whichutilize your own custom connector for your favorite back end database. You canthen fine tune your CRUD methods to your liking.

Example Application

You can look atthe account-without-juggler application as an example.

  • Implement the CrudConnector interface from @loopback/repository package.Here is one way to do it

  • Implement the DataSource interface from @loopback/repository. Toimplement the DataSource interface, you must give it a name, supply yourcustom connector class created in the previous step, and instantiate it:

  1. export class MySQLDs implements DataSource {
  2. name: 'mysqlDs';
  3. connector: MySqlConn;
  4. settings: Object;
  5. constructor() {
  6. this.settings = require('./mysql.json'); // connection configuration
  7. this.connector = new MySqlConn(this.settings);
  8. }
  9. }
  • Extend CrudRepositoryImpl class from @loopback/repository and supplyyour custom DataSource and model to it:
  1. import {CrudRepositoryImpl} from '@loopback/repository';
  2. import {MySQLDs} from './datasources/mysqlds.datasource';
  3. import {Account} from './models/account.model';
  4. export class NewRepository extends CrudRepositoryImpl<Account, string> {
  5. constructor() {
  6. const ds = new MySQLDs();
  7. super(ds, Account);
  8. }
  9. }

You can override the functions it provides, which ultimately call on yourconnector’s implementation of them, or write new ones.

Configure Controller

The next step is to wire your new DataSource to your controller. This step isessentially the same as above, but can also be done as follows using DependencyInjection:

  • Bind instance of your repository to a certain key in your application class
  1. class AccountMicroservice extends Application {
  2. private _startTime: Date;
  3. constructor() {
  4. super();
  5. const app = this;
  6. app.controller(AccountController);
  7. app.bind('repositories.NewRepository').toClass(NewRepository);
  8. }
  • Inject the bound instance into the repository property of your controller.inject can be imported from @loopback/context.
  1. export class AccountController {
  2. @repository(NewRepository)
  3. private repository: NewRepository;
  4. }

Example custom connector CRUD methods

Here is an example of a find function which uses the node-js mysql driver toretrieve all the rows that match a particular filter for a model instance.

  1. public find(
  2. modelClass: Class<Account>,
  3. filter: Filter<Account>,
  4. options: Options
  5. ): Promise<Account[]> {
  6. let self = this;
  7. let sqlStmt = "SELECT * FROM " + modelClass.name;
  8. if (filter.where) {
  9. let sql = "?? = ?";
  10. let formattedSql = "";
  11. for (var key in filter.where) {
  12. formattedSql = mysql.format(sql, [key, filter.where[key]]);
  13. }
  14. sqlStmt += " WHERE " + formattedSql;
  15. }
  16. debug("Find ", sqlStmt);
  17. return new Promise<Account[]>(function(resolve, reject) {
  18. self.connection.query(sqlStmt, function(err: any, results: Account[]) {
  19. if (err !== null) return reject(err);
  20. resolve(results);
  21. });
  22. });
  23. }

Example Application

You can look atthe account application as an example.