UNPKG

@loopback/repository

Version:

Define and implement a common set of interfaces for interacting with databases

493 lines (464 loc) 15.2 kB
// 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); }