@loopback/repository
Version:
Define and implement a common set of interfaces for interacting with databases
493 lines (464 loc) • 15.2 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved.
// Node module: @loopback/repository
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import {
Application,
Binding,
BindingFromClassOptions,
BindingScope,
Component,
Constructor,
CoreBindings,
createBindingFromClass,
MixinTarget,
} from '@loopback/core';
import debugFactory from 'debug';
import {Class} from '../common-types';
import {SchemaMigrationOptions} from '../datasource';
import {RepositoryBindings, RepositoryTags} from '../keys';
import {Model} from '../model';
import {juggler, Repository} from '../repositories';
const debug = debugFactory('loopback:repository:mixin');
/**
* A mixin class for Application that creates a .repository()
* function to register a repository automatically. Also overrides
* component function to allow it to register repositories automatically.
*
* @example
* ```ts
* class MyApplication extends RepositoryMixin(Application) {}
* ```
*
* Please note: the members in the mixin function are documented in a dummy class
* called <a href="#RepositoryMixinDoc">RepositoryMixinDoc</a>
*
* @param superClass - Application class
* @returns A new class that extends the super class with repository related
* methods
*
* @typeParam T - Type of the application class as the target for the mixin
*
*/
export function RepositoryMixin<T extends MixinTarget<Application>>(
superClass: T,
) {
return class extends superClass {
/**
* Add a repository to this application.
*
* @param repoClass - The repository to add.
* @param nameOrOptions - Name or options for the binding
*
* @example
* ```ts
*
* class NoteRepo {
* model: any;
*
* constructor() {
* const ds: juggler.DataSource = new juggler.DataSource({
* name: 'db',
* connector: 'memory',
* });
*
* this.model = ds.createModel(
* 'note',
* {title: 'string', content: 'string'},
* {}
* );
* }
* };
*
* app.repository(NoteRepo);
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
repository<R extends Repository<any>>(
repoClass: Class<R>,
nameOrOptions?: string | BindingFromClassOptions,
): Binding<R> {
const binding = createBindingFromClass(repoClass, {
namespace: RepositoryBindings.REPOSITORIES,
type: RepositoryTags.REPOSITORY,
defaultScope: BindingScope.TRANSIENT,
...toOptions(nameOrOptions),
}).tag(RepositoryTags.REPOSITORY);
this.add(binding);
return binding;
}
/**
* Retrieve the repository instance from the given Repository class
*
* @param repo - The repository class to retrieve the instance of
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async getRepository<R extends Repository<any>>(repo: Class<R>): Promise<R> {
return this.get(`repositories.${repo.name}`);
}
/**
* Add the dataSource to this application.
*
* @param dataSource - The dataSource to add.
* @param nameOrOptions - The binding name or options of the datasource;
* defaults to dataSource.name
*
* @example
* ```ts
*
* const ds: juggler.DataSource = new juggler.DataSource({
* name: 'db',
* connector: 'memory',
* });
*
* app.dataSource(ds);
*
* // The datasource can be injected with
* constructor(@inject('datasources.db') dataSource: DataSourceType) {
*
* }
* ```
*/
dataSource<D extends juggler.DataSource>(
dataSource: Class<D> | D,
nameOrOptions?: string | BindingFromClassOptions,
): Binding<D> {
const options = toOptions(nameOrOptions);
// We have an instance of
if (dataSource instanceof juggler.DataSource) {
const name = options.name || dataSource.name;
const namespace = options.namespace ?? RepositoryBindings.DATASOURCES;
const key = `${namespace}.${name}`;
return this.bind(key).to(dataSource).tag(RepositoryTags.DATASOURCE);
} else if (typeof dataSource === 'function') {
options.name = options.name || dataSource.dataSourceName;
const binding = createBindingFromClass(dataSource, {
namespace: RepositoryBindings.DATASOURCES,
type: RepositoryTags.DATASOURCE,
defaultScope: BindingScope.SINGLETON,
...options,
});
this.add(binding);
return binding;
} else {
throw new Error('not a valid DataSource.');
}
}
/**
* Register a model class as a binding in the target context
* @param modelClass - Model class
*/
model<M extends Class<unknown>>(modelClass: M) {
const binding = createModelClassBinding(modelClass);
this.add(binding);
return binding;
}
/**
* Add a component to this application. Also mounts
* all the components repositories.
*
* @param component - The component to add.
* @param nameOrOptions - Name or options for the binding.
*
* @example
* ```ts
*
* export class ProductComponent {
* controllers = [ProductController];
* repositories = [ProductRepo, UserRepo];
* providers = {
* [AUTHENTICATION_STRATEGY]: AuthStrategy,
* [AUTHORIZATION_ROLE]: Role,
* };
* };
*
* app.component(ProductComponent);
* ```
*/
// Unfortunately, TypeScript does not allow overriding methods inherited
// from mapped types. https://github.com/microsoft/TypeScript/issues/38496
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
public component<C extends Component = Component>(
componentCtor: Constructor<C>,
nameOrOptions?: string | BindingFromClassOptions,
) {
const binding = super.component(componentCtor, nameOrOptions);
const instance = this.getSync<C & RepositoryComponent>(binding.key);
this.mountComponentRepositories(instance);
this.mountComponentModels(instance);
return binding;
}
/**
* Get an instance of a component and mount all it's
* repositories. This function is intended to be used internally
* by `component()`.
*
* NOTE: Calling `mountComponentRepositories` with a component class
* constructor is deprecated. You should instantiate the component
* yourself and provide the component instance instead.
*
* @param componentInstanceOrClass - The component to mount repositories of
* @internal
*/
mountComponentRepositories(
// accept also component class to preserve backwards compatibility
// TODO(semver-major) Remove support for component class constructor
componentInstanceOrClass: Class<unknown> | RepositoryComponent,
) {
const component = resolveComponentInstance(this);
if (component.repositories) {
for (const repo of component.repositories) {
this.repository(repo);
}
}
// `Readonly<Application>` is a hack to remove protected members
// and thus allow `this` to be passed as a value for `ctx`
function resolveComponentInstance(ctx: Readonly<Application>) {
if (typeof componentInstanceOrClass !== 'function')
return componentInstanceOrClass;
const componentName = componentInstanceOrClass.name;
const componentKey = `${CoreBindings.COMPONENTS}.${componentName}`;
return ctx.getSync<RepositoryComponent>(componentKey);
}
}
/**
* Bind all model classes provided by a component.
* @param component
* @internal
*/
mountComponentModels(component: RepositoryComponent) {
if (!component.models) return;
for (const m of component.models) {
this.model(m);
}
}
/**
* Update or recreate the database schema for all repositories.
*
* **WARNING**: By default, `migrateSchema()` will attempt to preserve data
* while updating the schema in your target database, but this is not
* guaranteed to be safe.
*
* Please check the documentation for your specific connector(s) for
* a detailed breakdown of behaviors for automigrate!
*
* @param options - Migration options, e.g. whether to update tables
* preserving data or rebuild everything from scratch.
*/
async migrateSchema(options: SchemaMigrationOptions = {}): Promise<void> {
const operation =
options.existingSchema === 'drop' ? 'automigrate' : 'autoupdate';
// Instantiate all repositories to ensure models are registered & attached
// to their datasources
const repoBindings: Readonly<Binding<unknown>>[] =
this.findByTag('repository');
await Promise.all(repoBindings.map(b => this.get(b.key)));
// Look up all datasources and update/migrate schemas one by one
const dsBindings: Readonly<Binding<object>>[] = this.findByTag(
RepositoryTags.DATASOURCE,
);
for (const b of dsBindings) {
const ds = await this.get<juggler.DataSource>(b.key);
const disableMigration = ds.settings.disableMigration ?? false;
if (
operation in ds &&
typeof ds[operation] === 'function' &&
!disableMigration
) {
debug('Migrating dataSource %s', b.key);
await ds[operation](options.models);
} else {
debug('Skipping migration of dataSource %s', b.key);
}
}
}
};
}
/**
* This interface describes additional Component properties
* allowing components to contribute Repository-related artifacts.
*/
export interface RepositoryComponent {
/**
* An optional list of Repository classes to bind for dependency injection
* via `app.repository()` API.
*/
repositories?: Class<Repository<Model>>[];
/**
* An optional list of Model classes to bind for dependency injection
* via `app.model()` API.
*/
models?: Class<Model>[];
}
/**
* Normalize name or options to `BindingFromClassOptions`
* @param nameOrOptions - Name or options for binding from class
*/
function toOptions(nameOrOptions?: string | BindingFromClassOptions) {
if (typeof nameOrOptions === 'string') {
return {name: nameOrOptions};
}
return nameOrOptions ?? {};
}
/**
* Interface for an Application mixed in with RepositoryMixin
*/
export interface ApplicationWithRepositories extends Application {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
repository<R extends Repository<any>>(
repo: Class<R>,
name?: string,
): Binding<R>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getRepository<R extends Repository<any>>(repo: Class<R>): Promise<R>;
dataSource<D extends juggler.DataSource>(
dataSource: Class<D> | D,
name?: string,
): Binding<D>;
model<M extends Class<unknown>>(modelClass: M): Binding<M>;
component(component: Class<unknown>, name?: string): Binding;
mountComponentRepositories(component: Class<unknown>): void;
migrateSchema(options?: SchemaMigrationOptions): Promise<void>;
}
/**
* A dummy class created to generate the tsdoc for the members in repository
* mixin. Please don't use it.
*
* The members are implemented in function
* <a href="#RepositoryMixin">RepositoryMixin</a>
*/
export class RepositoryMixinDoc {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(...args: any[]) {
throw new Error(
'This is a dummy class created for apidoc!' + 'Please do not use it!',
);
}
/**
* Add a repository to this application.
*
* @param repo - The repository to add.
*
* @example
* ```ts
*
* class NoteRepo {
* model: any;
*
* constructor() {
* const ds: juggler.DataSource = new juggler.DataSource({
* name: 'db',
* connector: 'memory',
* });
*
* this.model = ds.createModel(
* 'note',
* {title: 'string', content: 'string'},
* {}
* );
* }
* };
*
* app.repository(NoteRepo);
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
repository(repo: Class<Repository<any>>): Binding {
throw new Error();
}
/**
* Retrieve the repository instance from the given Repository class
*
* @param repo - The repository class to retrieve the instance of
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async getRepository<R extends Repository<any>>(repo: Class<R>): Promise<R> {
return new repo() as R;
}
/**
* Add the dataSource to this application.
*
* @param dataSource - The dataSource to add.
* @param name - The binding name of the datasource; defaults to dataSource.name
*
* @example
* ```ts
*
* const ds: juggler.DataSource = new juggler.DataSource({
* name: 'db',
* connector: 'memory',
* });
*
* app.dataSource(ds);
*
* // The datasource can be injected with
* constructor(@inject('datasources.db') dataSource: DataSourceType) {
*
* }
* ```
*/
dataSource(
dataSource: Class<juggler.DataSource> | juggler.DataSource,
name?: string,
): Binding {
throw new Error();
}
/**
* Add a component to this application. Also mounts
* all the components repositories.
*
* @param component - The component to add.
*
* @example
* ```ts
*
* export class ProductComponent {
* controllers = [ProductController];
* repositories = [ProductRepo, UserRepo];
* providers = {
* [AUTHENTICATION_STRATEGY]: AuthStrategy,
* [AUTHORIZATION_ROLE]: Role,
* };
* };
*
* app.component(ProductComponent);
* ```
*/
public component(component: Class<{}>): Binding {
throw new Error();
}
/**
* Get an instance of a component and mount all it's
* repositories. This function is intended to be used internally
* by component()
*
* @param component - The component to mount repositories of
*/
mountComponentRepository(component: Class<{}>) {}
/**
* Update or recreate the database schema for all repositories.
*
* **WARNING**: By default, `migrateSchema()` will attempt to preserve data
* while updating the schema in your target database, but this is not
* guaranteed to be safe.
*
* Please check the documentation for your specific connector(s) for
* a detailed breakdown of behaviors for automigrate!
*
* @param options - Migration options, e.g. whether to update tables
* preserving data or rebuild everything from scratch.
*/
async migrateSchema(options?: SchemaMigrationOptions): Promise<void> {}
}
/**
* Create a binding for the given model class
* @param modelClass - Model class
*/
export function createModelClassBinding<M extends Class<unknown>>(
modelClass: M,
) {
return Binding.bind<M>(`${RepositoryBindings.MODELS}.${modelClass.name}`)
.to(modelClass)
.tag(RepositoryTags.MODEL);
}