Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: move functions to define repository classes from @loopback/rest-crud to @loopback/repository #4792

Merged
merged 2 commits into from
May 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright IBM Corp. 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 {expect} from '@loopback/testlab';
import {
AnyObject,
Count,
CrudRepository,
DataObject,
DefaultCrudRepository,
DefaultKeyValueRepository,
defineCrudRepositoryClass,
defineKeyValueRepositoryClass,
defineRepositoryClass,
Entity,
Filter,
juggler,
model,
Model,
property,
Where,
} from '../../..';

describe('RepositoryClass builder', () => {
describe('defineRepositoryClass', () => {
it('should generate custom repository class', async () => {
const AddressRepository = defineRepositoryClass<
typeof Address,
DummyCrudRepository<Address>
>(Address, DummyCrudRepository);
// `CrudRepository.prototype.find` is inherited
expect(AddressRepository.prototype.find).to.be.a.Function();
raymondfeng marked this conversation as resolved.
Show resolved Hide resolved
// `DummyCrudRepository.prototype.findByTitle` is inherited
expect(AddressRepository.prototype.findByTitle).to.be.a.Function();
expect(AddressRepository.name).to.equal('AddressRepository');
expect(Object.getPrototypeOf(AddressRepository)).to.equal(
DummyCrudRepository,
);
bajtos marked this conversation as resolved.
Show resolved Hide resolved
});
});

describe('defineCrudRepositoryClass', () => {
it('should generate entity CRUD repository class', async () => {
const ProductRepository = defineCrudRepositoryClass(Product);

expect(ProductRepository.name).to.equal('ProductRepository');
expect(ProductRepository.prototype.find).to.be.a.Function();
expect(ProductRepository.prototype.findById).to.be.a.Function();
expect(Object.getPrototypeOf(ProductRepository)).to.equal(
DefaultCrudRepository,
);
});
});

describe('defineKeyValueRepositoryClass', () => {
it('should generate key value repository class', async () => {
const ProductRepository = defineKeyValueRepositoryClass(Product);

expect(ProductRepository.name).to.equal('ProductRepository');
expect(ProductRepository.prototype.get).to.be.a.Function();
expect(Object.getPrototypeOf(ProductRepository)).to.equal(
DefaultKeyValueRepository,
);
});
});

@model()
class Product extends Entity {
@property({id: true})
id: number;

@property()
name: string;
}

@model()
class Address extends Model {
@property()
street: string;

@property()
city: string;

@property()
state: string;
}

class DummyCrudRepository<M extends Model> implements CrudRepository<M> {
constructor(
private modelCtor: typeof Model & {prototype: M},
private dataSource: juggler.DataSource,
) {}
create(
dataObject: DataObject<M>,
options?: AnyObject | undefined,
): Promise<M> {
throw new Error('Method not implemented.');
}
createAll(
dataObjects: DataObject<M>[],
options?: AnyObject | undefined,
): Promise<M[]> {
throw new Error('Method not implemented.');
}
find(
filter?: Filter<M> | undefined,
options?: AnyObject | undefined,
): Promise<(M & {})[]> {
throw new Error('Method not implemented.');
}
updateAll(
dataObject: DataObject<M>,
where?: Where<M> | undefined,
options?: AnyObject | undefined,
): Promise<Count> {
throw new Error('Method not implemented.');
}
deleteAll(
where?: Where<M> | undefined,
options?: AnyObject | undefined,
): Promise<Count> {
throw new Error('Method not implemented.');
}
count(
where?: Where<M> | undefined,
options?: AnyObject | undefined,
): Promise<Count> {
throw new Error('Method not implemented.');
}

// An extra method to verify it's available for the defined repo class
findByTitle(title: string): Promise<M[]> {
throw new Error('Method not implemented.');
}
}
});
170 changes: 170 additions & 0 deletions packages/repository/src/define-repository-class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright IBM Corp. 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 assert from 'assert';
import {PrototypeOf} from './common-types';
import {Entity, Model} from './model';
import {
DefaultCrudRepository,
DefaultKeyValueRepository,
juggler,
Repository,
} from './repositories';

/**
* Signature for a Repository class bound to a given model. The constructor
* accepts only the dataSource to use for persistence.
*
* `define*` functions return a class implementing this interface.
*
* @typeParam M - Model class
* @typeParam R - Repository class/interface
*/
export interface ModelRepositoryClass<
M extends Model,
R extends Repository<M>
> {
/**
* The constructor for the generated repository class
* @param dataSource - DataSource object
*/
new (dataSource: juggler.DataSource): R;
prototype: R;
}

/**
* Signature for repository classes that can be used as the base class for
* `define*` functions. The constructor of a base repository class accepts
* the target model constructor and the datasource to use.
*
* `define*` functions require a class implementing this interface on input.
*
* @typeParam M - Model class constructor, e.g `typeof Model`.
* **❗️IMPORTANT: The type argument `M` is describing the model constructor type
* (e.g. `typeof Model`), not the model instance type (`Model`) as is the case
* in other repository-related types. The constructor type is required
* to support custom repository classes requiring a Model subclass in the
* constructor arguments, e.g. `Entity` or a user-provided model.**
*
* @typeParam R - Repository class/interface
*/
export interface BaseRepositoryClass<
M extends typeof Model,
R extends Repository<PrototypeOf<M>>
> {
/**
* The constructor for the generated repository class
* @param modelClass - Model class
* @param dataSource - DataSource object
*/
new (modelClass: M, dataSource: juggler.DataSource): R;
prototype: R;
}

/**
* Create (define) a repository class for the given model.
*
* See also `defineCrudRepositoryClass` and `defineKeyValueRepositoryClass`
* for convenience wrappers providing repository class factory for the default
* CRUD and KeyValue implementations.
*
* **❗️IMPORTANT: The compiler (TypeScript 3.8) is not able to correctly infer
* generic arguments `M` and `R` from the class constructors provided in
* function arguments. You must always provide both M and R types explicitly.**
*
* @example
*
* ```ts
* const AddressRepository = defineRepositoryClass<
* typeof Address,
* DefaultEntityCrudRepository<
* Address,
* typeof Address.prototype.id,
* AddressRelations
* >,
* >(Address, DefaultCrudRepository);
* ```
*
* @param modelClass - A model class such as `Address`.
* @param baseRepositoryClass - Repository implementation to use as the base,
* e.g. `DefaultCrudRepository`.
*
* @typeParam M - Model class constructor (e.g. `typeof Address`)
* @typeParam R - Repository class (e.g. `DefaultCrudRepository<Address, number>`)
*/
export function defineRepositoryClass<
M extends typeof Model,
R extends Repository<PrototypeOf<M>>
>(
modelClass: M,
baseRepositoryClass: BaseRepositoryClass<M, R>,
): ModelRepositoryClass<PrototypeOf<M>, R> {
const repoName = modelClass.name + 'Repository';
const defineNamedRepo = new Function(
'ModelCtor',
'BaseRepository',
`return class ${repoName} extends BaseRepository {
constructor(dataSource) {
super(ModelCtor, dataSource);
}
};`,
);

const repo = defineNamedRepo(modelClass, baseRepositoryClass);
assert.equal(repo.name, repoName);
return repo;
}

/**
* Create (define) an entity CRUD repository class for the given model.
* This function always uses `DefaultCrudRepository` as the base class,
* use `defineRepositoryClass` if you want to use your own base repository.
*
* @example
*
* ```ts
* const ProductRepository = defineCrudRepositoryClass<
* Product,
* typeof Product.prototype.id,
* ProductRelations
* >(Product);
* ```
*
* @param entityClass - An entity class such as `Product`.
*
* @typeParam E - An entity class
* @typeParam IdType - ID type for the entity
* @typeParam Relations - Relations for the entity
*/
export function defineCrudRepositoryClass<
E extends Entity,
IdType,
Relations extends object
>(
entityClass: typeof Entity & {prototype: E},
): ModelRepositoryClass<E, DefaultCrudRepository<E, IdType, Relations>> {
return defineRepositoryClass(entityClass, DefaultCrudRepository);
}

/**
* Create (define) a KeyValue repository class for the given entity.
* This function always uses `DefaultKeyValueRepository` as the base class,
* use `defineRepositoryClass` if you want to use your own base repository.
*
* @example
*
* ```ts
* const ProductKeyValueRepository = defineKeyValueRepositoryClass(Product);
* ```
*
* @param modelClass - An entity class such as `Product`.
*
* @typeParam M - Model class
*/
export function defineKeyValueRepositoryClass<M extends Model>(
modelClass: typeof Model & {prototype: M},
): ModelRepositoryClass<M, DefaultKeyValueRepository<M>> {
return defineRepositoryClass(modelClass, DefaultKeyValueRepository);
}
1 change: 1 addition & 0 deletions packages/repository/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export * from './model';
export * from './query';
export * from './relations';
export * from './repositories';
export * from './define-repository-class';
export * from './transaction';
export * from './type-resolver';
export * from './types';
10 changes: 7 additions & 3 deletions packages/rest-crud/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ class defined without the need for a repository or controller class file.

If you would like more flexibility, e.g. if you would only like to define a
default `CrudRest` controller or repository, you can use the two helper methods
(`defineCrudRestController` and `defineCrudRepositoryClass`) exposed from
`@loopback/rest-crud`. These functions will help you create controllers and
respositories using code.
(`defineCrudRestController` from `@loopback/rest-crud` and
`defineCrudRepositoryClass` from `@loopback/repository`). These functions will
help you create controllers and repositories using code.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we revert this change since defineCrudRepositoryClass is still exported by @loopback/rest-crud? (Same comment applies to other similar doc changes below.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine either way.


For the examples in the following sections, we are also assuming a model named
`Product`, and a datasource named `db` have already been created.
Expand Down Expand Up @@ -106,6 +106,8 @@ on the Model) for your app.
Usage example:

```ts
import {defineCrudRepositoryClass} from '@loopback/repository';

const ProductRepository = defineCrudRepositoryClass(Product);
this.repository(ProductRepository);
inject('datasources.db')(ProductRepository, undefined, 0);
Expand All @@ -118,6 +120,8 @@ Here is an example of an app which uses `defineCrudRepositoryClass` and
requirements.

```ts
import {defineCrudRepositoryClass} from '@loopback/repository';

export class TryApplication extends BootMixin(
ServiceMixin(RepositoryMixin(RestApplication)),
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// License text available at https://opensource.org/licenses/MIT

import {
defineCrudRepositoryClass,
Entity,
EntityCrudRepository,
juggler,
Expand All @@ -18,7 +19,7 @@ import {
givenHttpServerConfig,
toJSON,
} from '@loopback/testlab';
import {defineCrudRepositoryClass, defineCrudRestController} from '../..';
import {defineCrudRestController} from '../..';

// In this test scenario, we create a product with a required & an optional
// property and use the default model settings (strict mode, forceId).
Expand Down
3 changes: 2 additions & 1 deletion packages/rest-crud/src/crud-rest.api-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ import {
import {
ApplicationWithRepositories,
Class,
defineCrudRepositoryClass,
Entity,
EntityCrudRepository,
} from '@loopback/repository';
import {Model} from '@loopback/rest';
import debugFactory from 'debug';
import {defineCrudRepositoryClass, defineCrudRestController} from '.';
import {defineCrudRestController} from '.';

const debug = debugFactory('loopback:boot:crud-rest');

Expand Down
3 changes: 2 additions & 1 deletion packages/rest-crud/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
* @packageDocumentation
*/

// Re-export `defineCrudRepositoryClass` for backward-compatibility
export {defineCrudRepositoryClass} from '@loopback/repository';
export * from './crud-rest.api-builder';
export * from './crud-rest.component';
export * from './crud-rest.controller';
export * from './repository-builder';
Loading