Skip to content
Closed
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,
defineEntityCrudRepositoryClass,
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>
Copy link
Contributor

Choose a reason for hiding this comment

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

So TypeScript cannot infer the generic types from the parameters in this case?

Copy link
Contributor

Choose a reason for hiding this comment

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

To make inference happy, I have to do the following:

function castDummyRepository<M extends Model>() {
  return class extends DummyCrudRepository<M> {};
}

const AddressRepository = defineCrudRepositoryClass(
  Address,
  castDummyRepository<Address>(),
);

Copy link
Contributor

Choose a reason for hiding this comment

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

TypeScript does not allow typeof DummyCrudRepository<M>.

Copy link
Member Author

Choose a reason for hiding this comment

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

So TypeScript cannot infer the generic types from the parameters in this case?

Unfortunately not. I think we should report a bug to TypeScript (if it wasn't already reported).

To make inference happy, I have to do the following (...)

IMO, castDummyRepository does not solve the problem of people forgetting to specify the repository type and relying on the compiler to infer it. Once they realize some extra effort is needed, then I think it's best to provide generic type parameters and there is no need to introduce cast*Repository functions.

>(Address, DummyCrudRepository);
// `CrudRepository.prototype.find` is inherited
expect(AddressRepository.prototype.find).to.be.a.Function();
// `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,
);
});
});

describe('defineEntityCrudRepositoryClass', () => {
it('should generate entity CRUD repository class', async () => {
const ProductRepository = defineEntityCrudRepositoryClass(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 NamedRepositoryClass<
Copy link
Member Author

Choose a reason for hiding this comment

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

There is no need to have specialized types for KeyValue and CRUD repositories, we can use a generic Repository type as the starting point.

I am not comfortable using RepositoryClass name, because it's too ambiguous and possibly confusing, since it uses different constructor argument than PersistedModelClass and friends.

I was looking for a name that would describe the fact that the repository class is already bound to a given model. ModelRepositoryClass did not feel right either, so I settled on NamedRepositoryClass. Feel free to propose a better name.

Copy link
Contributor

Choose a reason for hiding this comment

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

If it's NamedRepositoryClass, should we add name: string explicitly?

What about GeneratedRepositoryClass?

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe we should take another angle. My intention is to distinguish between a generic repository class that works for any Model/Entity and a repository class that's already specialized for (bound to) a given Model/Entity.

In that light, perhaps BoundRepositoryClass or SpecializedRepositoryClass would be better names?

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 `defineEntityCrudRepositoryClass` 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,
Copy link
Member Author

Choose a reason for hiding this comment

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

Similarly here, we need M to represent the class constructor, not the class instance.

R extends Repository<PrototypeOf<M>>
>(
modelClass: M,
baseRepositoryClass: BaseRepositoryClass<M, R>,
): NamedRepositoryClass<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 = defineEntityCrudRepositoryClass<
* 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 defineEntityCrudRepositoryClass<
E extends Entity,
IdType,
Relations extends object
>(
entityClass: typeof Entity & {prototype: E},
): NamedRepositoryClass<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},
): NamedRepositoryClass<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 @@ -14,6 +14,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';
20 changes: 12 additions & 8 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
`defineEntityCrudRepositoryClass` from `@loopback/repository`). These functions
will help you create controllers and repositories using code.

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 @@ -100,24 +100,28 @@ endpoints of an existing model with a respository.

### Creating a CRUD repository

Use the `defineCrudRepositoryClass` method to create named repositories (based
on the Model) for your app.
Use the `defineEntityCrudRepositoryClass` method to create named repositories
(based on the Model) for your app.

Usage example:

```ts
const ProductRepository = defineCrudRepositoryClass(Product);
import {defineEntityCrudRepositoryClass} from '@loopback/repository';

const ProductRepository = defineEntityCrudRepositoryClass(Product);
this.repository(ProductRepository);
inject('datasources.db')(ProductRepository, undefined, 0);
```

### Integrated example

Here is an example of an app which uses `defineCrudRepositoryClass` and
Here is an example of an app which uses `defineEntityCrudRepositoryClass` and
`defineCrudRestController` to fulfill its repository and controller
requirements.

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

export class TryApplication extends BootMixin(
ServiceMixin(RepositoryMixin(RestApplication)),
) {
Expand All @@ -128,7 +132,7 @@ export class TryApplication extends BootMixin(
async boot(): Promise<void> {
await super.boot();

const ProductRepository = defineCrudRepositoryClass(Product);
const ProductRepository = defineEntityCrudRepositoryClass(Product);
const repoBinding = this.repository(ProductRepository);

inject('datasources.db')(ProductRepository, undefined, 0);
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 {
defineEntityCrudRepositoryClass,
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 Expand Up @@ -287,7 +288,7 @@ describe('CrudRestController for a simple Product model', () => {
async function setupTestScenario() {
const db = new juggler.DataSource({connector: 'memory'});

const ProductRepository = defineCrudRepositoryClass(Product);
const ProductRepository = defineEntityCrudRepositoryClass(Product);

repo = new ProductRepository(db);

Expand Down
Loading