diff --git a/packages/repository/src/relations/has-one/has-one-repository.factory.ts b/packages/repository/src/relations/has-one/has-one-repository.factory.ts new file mode 100644 index 000000000000..3573760aeae9 --- /dev/null +++ b/packages/repository/src/relations/has-one/has-one-repository.factory.ts @@ -0,0 +1,110 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-todo +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as debugFactory from 'debug'; +import {camelCase} from 'lodash'; +import {DataObject} from '../../common-types'; +import {InvalidRelationError} from '../../errors'; +import {Entity} from '../../model'; +import {EntityCrudRepository} from '../../repositories/repository'; +import {isTypeResolver} from '../../type-resolver'; +import {Getter, HasOneDefinition} from '../relation.types'; +import {DefaultHasOneRepository, HasOneRepository} from './has-one.repository'; + +const debug = debugFactory('loopback:repository:has-many-repository-factory'); + +export type HasOneRepositoryFactory = ( + fkValue: ForeignKeyType, +) => HasOneRepository; + +/** + * Enforces a constraint on a repository based on a relationship contract + * between models. For example, if a Customer model is related to an Address model + * via a HasOne relation, then, the relational repository returned by the + * factory function would be constrained by a Customer model instance's id(s). + * + * @param relationMeta The relation metadata used to describe the + * relationship and determine how to apply the constraint. + * @param targetRepo The repository which represents the target model of a + * relation attached to a datasource. + * @returns The factory function which accepts a foreign key value to constrain + * the given target repository + */ +export function createHasOneRepositoryFactory< + Target extends Entity, + TargetID, + ForeignKeyType +>( + relationMetadata: HasOneDefinition, + targetRepositoryGetter: Getter>, +): HasOneRepositoryFactory { + const meta = resolveHasOneMetadata(relationMetadata); + debug('Resolved HasOne relation metadata: %o', meta); + return function(fkValue: ForeignKeyType) { + // tslint:disable-next-line:no-any + const constraint: any = {[meta.keyTo]: fkValue}; + return new DefaultHasOneRepository< + Target, + TargetID, + EntityCrudRepository + >(targetRepositoryGetter, constraint as DataObject); + }; +} + +type HasOneResolvedDefinition = HasOneDefinition & {keyTo: string}; + +/** + * Resolves given hasMany metadata if target is specified to be a resolver. + * Mainly used to infer what the `keyTo` property should be from the target's + * belongsTo metadata + * @param relationMeta hasMany metadata to resolve + */ +function resolveHasOneMetadata( + relationMeta: HasOneDefinition, +): HasOneResolvedDefinition { + if (!isTypeResolver(relationMeta.target)) { + const reason = 'target must be a type resolver'; + throw new InvalidRelationError(reason, relationMeta); + } + + if (relationMeta.keyTo) { + // The explict cast is needed because of a limitation of type inference + return relationMeta as HasOneResolvedDefinition; + } + + const sourceModel = relationMeta.source; + if (!sourceModel || !sourceModel.modelName) { + const reason = 'source model must be defined'; + throw new InvalidRelationError(reason, relationMeta); + } + + const targetModel = relationMeta.target(); + debug( + 'Resolved model %s from given metadata: %o', + targetModel.modelName, + targetModel, + ); + const defaultFkName = camelCase(sourceModel.modelName + '_id'); + const hasDefaultFkProperty = + targetModel.definition && + targetModel.definition.properties && + targetModel.definition.properties[defaultFkName]; + + if (!hasDefaultFkProperty) { + const reason = `target model ${ + targetModel.name + } is missing definition of foreign key ${defaultFkName}`; + throw new InvalidRelationError(reason, relationMeta); + } + + if ( + !targetModel.definition.properties[defaultFkName].id === true && + !targetModel.definition.properties[defaultFkName].generated === false + ) { + // throw InvalidRelationError('property must be a generated id field') + } + + return Object.assign(relationMeta, {keyTo: defaultFkName}); +} diff --git a/packages/repository/src/relations/has-one/has-one.decorator.ts b/packages/repository/src/relations/has-one/has-one.decorator.ts index a450e3423678..dcf783991ce5 100644 --- a/packages/repository/src/relations/has-one/has-one.decorator.ts +++ b/packages/repository/src/relations/has-one/has-one.decorator.ts @@ -3,15 +3,37 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {Entity, EntityResolver} from '../../model'; import {relation} from '../relation.decorator'; -import {RelationType} from '../relation.types'; +import {HasOneDefinition, RelationType} from '../relation.types'; -/** +/* * Decorator for hasOne - * @param definition + * infers foreign key name from target model name unless explicitly specified + * @param targetResolver Target model for hasOne relation + * @param definition Optional metadata for setting up hasOne relation * @returns {(target:any, key:string)} */ -export function hasOne(definition?: Object) { - const rel = Object.assign({type: RelationType.hasOne}, definition); - return relation(rel); +export function hasOne( + targetResolver: EntityResolver, + definition?: Partial, +) { + return function(decoratedTarget: Object, key: string) { + // property.array(targetResolver)(decoratedTarget, key); + + const meta: HasOneDefinition = Object.assign( + // default values, can be customized by the caller + {}, + // properties provided by the caller + definition, + // properties enforced by the decorator + { + type: RelationType.hasOne, + name: key, + source: decoratedTarget.constructor, + target: targetResolver, + }, + ); + relation(meta)(decoratedTarget, key); + }; } diff --git a/packages/repository/src/relations/has-one/has-one.repository.ts b/packages/repository/src/relations/has-one/has-one.repository.ts new file mode 100644 index 000000000000..7c9fe8dbcd4b --- /dev/null +++ b/packages/repository/src/relations/has-one/has-one.repository.ts @@ -0,0 +1,93 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-todo +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Getter} from '@loopback/context'; +import {DataObject, Options} from '../../common-types'; +import {Entity} from '../../model'; +import {Filter, Where} from '../../query'; +import { + constrainDataObject, + constrainFilter, +} from '../../repositories/constraint-utils'; +import {EntityCrudRepository} from '../../repositories/repository'; + +/** + * CRUD operations for a target repository of a HasMany relation + */ +export interface HasOneRepository { + /** + * Create a target model instance + * @param targetModelData The target model data + * @param options Options for the operation + * @returns A promise which resolves to the newly created target model instance + */ + create( + targetModelData: DataObject, + options?: Options, + ): Promise; + + /** + * Find the only target model instance that belongs to the declaring model. + * @param filter Query filter without a Where condition + * @param options Options for the operations + * @returns A promise of the target object or null if not found. + */ + get( + filter?: Exclude, Where>, + options?: Options, + ): Promise; +} + +export class DefaultHasOneRepository< + TargetEntity extends Entity, + TargetID, + TargetRepository extends EntityCrudRepository +> implements HasOneRepository { + /** + * Constructor of DefaultHasManyEntityCrudRepository + * @param getTargetRepository the getter of the related target model repository instance + * @param constraint the key value pair representing foreign key name to constrain + * the target repository instance + */ + constructor( + public getTargetRepository: Getter, + public constraint: DataObject, + ) {} + + async create( + targetModelData: DataObject, + options?: Options, + ): Promise { + const targetRepository = await this.getTargetRepository(); + // should we have an in memory LUT instead of a db query to increase + // performance here? + const found = await targetRepository.find( + constrainFilter({}, this.constraint), + ); + if (found.length > 0) { + throw new Error( + 'HasOne relation does not allow creation of more than one target model instance', + ); + } else { + return await targetRepository.create( + constrainDataObject(targetModelData, this.constraint), + options, + ); + } + } + + async get( + filter?: Exclude, Where>, + options?: Options, + ): Promise { + const targetRepository = await this.getTargetRepository(); + const found = await targetRepository.find( + Object.assign({limit: 1}, constrainFilter(filter, this.constraint)), + options, + ); + // TODO: throw EntityNotFound error when target model instance not found + return found[0]; + } +} diff --git a/packages/repository/src/relations/has-one/index.ts b/packages/repository/src/relations/has-one/index.ts index aac0595f913f..509e8f77d413 100644 --- a/packages/repository/src/relations/has-one/index.ts +++ b/packages/repository/src/relations/has-one/index.ts @@ -4,3 +4,5 @@ // License text available at https://opensource.org/licenses/MIT export * from './has-one.decorator'; +export * from './has-one.decorator'; +export * from './has-one-repository.factory'; diff --git a/packages/repository/src/relations/relation.types.ts b/packages/repository/src/relations/relation.types.ts index 02cf9e3ad249..fb15b5be4f88 100644 --- a/packages/repository/src/relations/relation.types.ts +++ b/packages/repository/src/relations/relation.types.ts @@ -70,12 +70,26 @@ export interface BelongsToDefinition extends RelationDefinitionBase { keyTo?: string; } +export interface HasOneDefinition extends RelationDefinitionBase { + type: RelationType.hasOne; + + /** + * The foreign key used by the target model. + * + * E.g. when a Customer has one Address instance, then keyTo is "customerId". + * Note that "customerId" is the default FK assumed by the framework, users + * can provide a custom FK name by setting "keyTo". + */ + keyTo?: string; +} + /** * A union type describing all possible Relation metadata objects. */ export type RelationMetadata = | HasManyDefinition | BelongsToDefinition + | HasOneDefinition // TODO(bajtos) add other relation types and remove RelationDefinitionBase once // all relation types are covered. | RelationDefinitionBase; diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index 73a095bac6dc..9dee24db2e15 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -25,6 +25,9 @@ import { createHasManyRepositoryFactory, BelongsToAccessor, createBelongsToAccessor, + createHasOneRepositoryFactory, + HasOneDefinition, + HasOneRepositoryFactory, } from '../relations'; import {resolveType} from '../type-resolver'; import {EntityCrudRepository} from './repository'; @@ -196,6 +199,21 @@ export class DefaultCrudRepository ); } + protected _createHasOneRepositoryFactoryFor< + Target extends Entity, + TargetID, + ForeignKeyType + >( + relationName: string, + targetRepoGetter: Getter>, + ): HasOneRepositoryFactory { + const meta = this.entityClass.definition.relations[relationName]; + return createHasOneRepositoryFactory( + meta as HasOneDefinition, + targetRepoGetter, + ); + } + async create(entity: DataObject, options?: Options): Promise { const model = await ensurePromise(this.modelClass.create(entity, options)); return this.toEntity(model); diff --git a/packages/repository/test/acceptance/has-one.relation.acceptance.ts b/packages/repository/test/acceptance/has-one.relation.acceptance.ts new file mode 100644 index 000000000000..2c40137eb342 --- /dev/null +++ b/packages/repository/test/acceptance/has-one.relation.acceptance.ts @@ -0,0 +1,171 @@ +// Copyright IBM Corp. 2017,2018. 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} from '@loopback/core'; +import {expect} from '@loopback/testlab'; +import * as _ from 'lodash'; +import { + ApplicationWithRepositories, + juggler, + repository, + RepositoryMixin, + Filter, +} from '../..'; +import {Address} from '../fixtures/models'; +import {CustomerRepository, AddressRepository} from '../fixtures/repositories'; +import {Where} from '../..'; + +describe('hasOne relation', () => { + // Given a Customer and Address models - see definitions at the bottom + + let app: ApplicationWithRepositories; + let controller: CustomerController; + let customerRepo: CustomerRepository; + let addressRepo: AddressRepository; + let existingCustomerId: number; + + before(givenApplicationWithMemoryDB); + before(givenBoundCrudRepositoriesForCustomerAndAddress); + before(givenCustomerController); + + beforeEach(async () => { + await addressRepo.deleteAll(); + }); + + beforeEach(async () => { + existingCustomerId = (await givenPersistedCustomerInstance()).id; + }); + + it('can create an instance of the related model', async () => { + const address = await controller.createCustomerAddress(existingCustomerId, { + street: '123 test avenue', + }); + expect(address.toObject()).to.containDeep({ + customerId: existingCustomerId, + street: '123 test avenue', + }); + + const persisted = await addressRepo.findById(address.zipcode); + expect(persisted.toObject()).to.deepEqual(address.toObject()); + }); + + it("doesn't allow to create related model instance twice", async () => { + const address = await controller.createCustomerAddress(existingCustomerId, { + street: '123 test avenue', + }); + expect( + controller.createCustomerAddress(existingCustomerId, { + street: '456 test street', + zipcode: '44012', + }), + ).to.be.rejectedWith( + /does not allow creation of more than one target model instance/, + ); + expect(address.toObject()).to.containDeep({ + customerId: existingCustomerId, + street: '123 test avenue', + }); + + const persisted = await addressRepo.findById(address.zipcode); + expect(persisted.toObject()).to.deepEqual(address.toObject()); + expect(addressRepo.findById('44012')).to.be.rejectedWith( + /Entity not found/, + ); + }); + + it('can find instance of the related model', async () => { + const address = await controller.createCustomerAddress(existingCustomerId, { + street: '123 test avenue', + }); + const notMyAddress = await controller.createCustomerAddress( + existingCustomerId + 1, + { + street: '456 test road', + }, + ); + const foundAddress = await controller.findCustomerAddress( + existingCustomerId, + ); + expect(foundAddress).to.containEql(address); + expect(foundAddress).to.not.containEql(notMyAddress); + + const persisted = await addressRepo.find({ + where: {customerId: existingCustomerId}, + }); + expect(persisted[0]).to.deepEqual(foundAddress); + }); + + it('does not allow where filter to find related model instance', async () => { + const address = await controller.createCustomerAddress(existingCustomerId, { + street: '123 test avenue', + }); + + const foundAddress = await controller.findCustomerAddressWithFilter( + existingCustomerId, + {where: {street: '123 test avenue'}}, + ); + // TODO: make sure this test fails when where condition is supplied + // compiler should have errored out (?) + expect(foundAddress).to.containEql(address); + + const persisted = await addressRepo.find({ + where: {customerId: existingCustomerId}, + }); + expect(persisted[0]).to.deepEqual(foundAddress); + }); + + /*---------------- HELPERS -----------------*/ + + class CustomerController { + constructor( + @repository(CustomerRepository) + protected customerRepository: CustomerRepository, + ) {} + + async createCustomerAddress( + customerId: number, + addressData: Partial
, + ): Promise
{ + return await this.customerRepository + .address(customerId) + .create(addressData); + } + + async findCustomerAddress(customerId: number) { + return await this.customerRepository.address(customerId).get(); + } + + async findCustomerAddressWithFilter( + customerId: number, + filter: Filter
, + ) { + return await this.customerRepository.address(customerId).get(filter); + } + } + + function givenApplicationWithMemoryDB() { + class TestApp extends RepositoryMixin(Application) {} + app = new TestApp(); + app.dataSource(new juggler.DataSource({name: 'db', connector: 'memory'})); + } + + async function givenBoundCrudRepositoriesForCustomerAndAddress() { + app.repository(CustomerRepository); + app.repository(AddressRepository); + customerRepo = await app.getRepository(CustomerRepository); + addressRepo = await app.getRepository(AddressRepository); + } + + async function givenCustomerController() { + app.controller(CustomerController); + controller = await app.get( + 'controllers.CustomerController', + ); + } + + async function givenPersistedCustomerInstance() { + return customerRepo.create({name: 'a customer'}); + } +}); diff --git a/packages/repository/test/fixtures/models/address.model.ts b/packages/repository/test/fixtures/models/address.model.ts new file mode 100644 index 000000000000..5b14b1216dbd --- /dev/null +++ b/packages/repository/test/fixtures/models/address.model.ts @@ -0,0 +1,34 @@ +// Copyright IBM Corp. 2018. 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 {Entity, model, property, belongsTo} from '../../..'; +import {Customer} from './customer.model'; + +@model() +export class Address extends Entity { + @property({ + type: 'string', + }) + street: String; + @property({ + type: 'string', + }) + zipcode: String; + @property({ + type: 'string', + }) + city: String; + @property({ + type: 'string', + }) + province: String; + + @belongsTo(() => Customer) + @property({ + id: true, + generated: false, + }) + customerId: number; +} diff --git a/packages/repository/test/fixtures/models/customer.model.ts b/packages/repository/test/fixtures/models/customer.model.ts index 3631ce9ec7a4..e05d5e23ea22 100644 --- a/packages/repository/test/fixtures/models/customer.model.ts +++ b/packages/repository/test/fixtures/models/customer.model.ts @@ -3,8 +3,9 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Entity, hasMany, model, property} from '../../..'; +import {Entity, hasMany, model, property, hasOne} from '../../..'; import {Order} from './order.model'; +import {Address} from './address.model'; @model() export class Customer extends Entity { @@ -21,4 +22,7 @@ export class Customer extends Entity { @hasMany(() => Order) orders: Order[]; + + @hasOne(() => Address) + address: Address; } diff --git a/packages/repository/test/fixtures/models/index.ts b/packages/repository/test/fixtures/models/index.ts index 3f10d310a1b1..0907e67c9dfe 100644 --- a/packages/repository/test/fixtures/models/index.ts +++ b/packages/repository/test/fixtures/models/index.ts @@ -6,3 +6,4 @@ export * from './customer.model'; export * from './order.model'; export * from './product.model'; +export * from './address.model'; diff --git a/packages/repository/test/fixtures/repositories/address.repository.ts b/packages/repository/test/fixtures/repositories/address.repository.ts new file mode 100644 index 000000000000..1b57a67eaf0b --- /dev/null +++ b/packages/repository/test/fixtures/repositories/address.repository.ts @@ -0,0 +1,36 @@ +// Copyright IBM Corp. 2018. 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 {Getter, inject} from '@loopback/context'; +import { + BelongsToAccessor, + DefaultCrudRepository, + juggler, + repository, +} from '../../..'; +import {Customer, Address} from '../models'; +import {CustomerRepository} from '../repositories'; + +export class AddressRepository extends DefaultCrudRepository< + Address, + typeof Address.prototype.zipcode +> { + public readonly customer: BelongsToAccessor< + Customer, + typeof Address.prototype.zipcode + >; + + constructor( + @inject('datasources.db') protected db: juggler.DataSource, + @repository.getter('CustomerRepository') + customerRepositoryGetter: Getter, + ) { + super(Address, db); + this.customer = this._createBelongsToAccessorFor( + 'customerId', + customerRepositoryGetter, + ); + } +} diff --git a/packages/repository/test/fixtures/repositories/customer.repository.ts b/packages/repository/test/fixtures/repositories/customer.repository.ts index 6fb384e1d90b..35ce49901f82 100644 --- a/packages/repository/test/fixtures/repositories/customer.repository.ts +++ b/packages/repository/test/fixtures/repositories/customer.repository.ts @@ -10,8 +10,10 @@ import { juggler, repository, } from '../../..'; -import {Customer, Order} from '../models'; +import {Customer, Order, Address} from '../models'; import {OrderRepository} from './order.repository'; +import {HasOneRepositoryFactory} from '../../../src'; +import {AddressRepository} from './address.repository'; export class CustomerRepository extends DefaultCrudRepository< Customer, @@ -21,15 +23,25 @@ export class CustomerRepository extends DefaultCrudRepository< Order, typeof Customer.prototype.id >; + public readonly address: HasOneRepositoryFactory< + Address, + typeof Customer.prototype.id + >; constructor( @inject('datasources.db') protected db: juggler.DataSource, @repository.getter('OrderRepository') orderRepositoryGetter: Getter, + @repository.getter('AddressRepository') + addressRepositoryGetter: Getter, ) { super(Customer, db); this.orders = this._createHasManyRepositoryFactoryFor( 'orders', orderRepositoryGetter, ); + this.address = this._createHasOneRepositoryFactoryFor( + 'address', + addressRepositoryGetter, + ); } } diff --git a/packages/repository/test/fixtures/repositories/index.ts b/packages/repository/test/fixtures/repositories/index.ts index 7da82c261ecf..c983615b3d7d 100644 --- a/packages/repository/test/fixtures/repositories/index.ts +++ b/packages/repository/test/fixtures/repositories/index.ts @@ -6,3 +6,4 @@ export * from './customer.repository'; export * from './order.repository'; export * from './product.repository'; +export * from './address.repository'; diff --git a/packages/repository/test/unit/decorator/model-and-relation.decorator.unit.ts b/packages/repository/test/unit/decorator/model-and-relation.decorator.unit.ts index bf096d15557a..31dfe10f8ba7 100644 --- a/packages/repository/test/unit/decorator/model-and-relation.decorator.unit.ts +++ b/packages/repository/test/unit/decorator/model-and-relation.decorator.unit.ts @@ -125,7 +125,7 @@ describe('model decorator', () => { @hasMany(() => Order) orders?: Order[]; - @hasOne() + @hasOne(() => Order) lastOrder?: Order; @relation({type: RelationType.hasMany}) @@ -292,8 +292,11 @@ describe('model decorator', () => { RELATIONS_KEY, Customer.prototype, ) || /* istanbul ignore next */ {}; - expect(meta.lastOrder).to.eql({ + expect(meta.lastOrder).to.containEql({ type: RelationType.hasOne, + name: 'lastOrder', + target: () => Order, + source: Customer, }); }); diff --git a/packages/repository/test/unit/repositories/has-one-repository-factory.unit.ts b/packages/repository/test/unit/repositories/has-one-repository-factory.unit.ts new file mode 100644 index 000000000000..759d8e9533a4 --- /dev/null +++ b/packages/repository/test/unit/repositories/has-one-repository-factory.unit.ts @@ -0,0 +1,136 @@ +// Copyright IBM Corp. 2017,2018. 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 {Getter} from '@loopback/context'; +import {createStubInstance, expect} from '@loopback/testlab'; +import { + createHasOneRepositoryFactory, + DefaultCrudRepository, + Entity, + HasOneDefinition, + juggler, + ModelDefinition, + RelationType, +} from '../../..'; + +describe('createHasOneRepositoryFactory', () => { + let customerRepo: CustomerRepository; + + beforeEach(givenStubbedCustomerRepo); + + it('rejects relations with missing source', () => { + const relationMeta = givenHasOneDefinition({ + source: undefined, + }); + + expect(() => + createHasOneRepositoryFactory( + relationMeta, + Getter.fromValue(customerRepo), + ), + ).to.throw(/source model must be defined/); + }); + + it('rejects relations with missing target', () => { + const relationMeta = givenHasOneDefinition({ + target: undefined, + }); + + expect(() => + createHasOneRepositoryFactory( + relationMeta, + Getter.fromValue(customerRepo), + ), + ).to.throw(/target must be a type resolver/); + }); + + it('rejects relations with a target that is not a type resolver', () => { + const relationMeta = givenHasOneDefinition({ + // tslint:disable-next-line:no-any + target: Address as any, + // the cast to any above is necessary to disable compile check + // we want to verify runtime assertion + }); + + expect(() => + createHasOneRepositoryFactory( + relationMeta, + Getter.fromValue(customerRepo), + ), + ).to.throw(/target must be a type resolver/); + }); + + it('rejects relations with keyTo pointing to an unknown property', () => { + const relationMeta = givenHasOneDefinition({ + target: () => Address, + // Let the relation use the default keyTo value "customerId" + // which does not exist on the Customer model! + keyTo: undefined, + }); + + expect(() => + createHasOneRepositoryFactory( + relationMeta, + Getter.fromValue(customerRepo), + ), + ).to.throw(/target model Address is missing.*foreign key customerId/); + }); + + /*------------- HELPERS ---------------*/ + + class Address extends Entity { + static definition = new ModelDefinition('Address') + .addProperty('street', { + type: 'string', + }) + .addProperty('zipcode', { + type: 'string', + }) + .addProperty('city', { + type: 'string', + }) + .addProperty('province', { + type: 'string', + }); + street: String; + zipcode: String; + city: String; + province: String; + } + + class Customer extends Entity { + static definition = new ModelDefinition('Customer').addProperty('id', { + type: Number, + id: true, + }); + id: number; + } + + class CustomerRepository extends DefaultCrudRepository< + Customer, + typeof Customer.prototype.id + > { + constructor(dataSource: juggler.DataSource) { + super(Customer, dataSource); + } + } + + function givenStubbedCustomerRepo() { + customerRepo = createStubInstance(CustomerRepository); + } + + function givenHasOneDefinition( + props?: Partial, + ): HasOneDefinition { + const defaults: HasOneDefinition = { + type: RelationType.hasOne, + name: 'address', + target: () => Address, + source: Customer, + }; + + return Object.assign(defaults, props); + } +});