From 8f4a1444e29f3cf3cb991a364e5d23fc3e99b917 Mon Sep 17 00:00:00 2001 From: Lokesh Mohanty Date: Thu, 30 Jan 2020 10:41:42 +0530 Subject: [PATCH 1/2] feat(cli): add hasOne relation type to `lb4 relation` --- .../relation/has-one-relation.generator.js | 199 +++++ packages/cli/generators/relation/index.js | 9 + ...ontroller-relation-template-has-one.ts.ejs | 110 +++ .../generators/relation/utils.generator.js | 1 + .../relation.has-one.integration.snapshots.js | 760 ++++++++++++++++++ .../address-customer.controller.ts | 1 + packages/cli/test/fixtures/relation/index.js | 54 ++ .../models/address-class-type.model.ts | 19 + .../relation/models/address-class.model.ts | 19 + .../relation/models/address-with-fk.model.ts | 25 + .../fixtures/relation/models/address.model.ts | 20 + .../relation/models/customer6.model.ts | 22 + .../address-class-type.repository.ts | 13 + .../repositories/address-class.repository.ts | 13 + .../repositories/address.repository.ts | 17 + .../relation.has-one.integration.js | 416 ++++++++++ 16 files changed, 1698 insertions(+) create mode 100644 packages/cli/generators/relation/has-one-relation.generator.js create mode 100644 packages/cli/generators/relation/templates/controller-relation-template-has-one.ts.ejs create mode 100644 packages/cli/snapshots/integration/generators/relation.has-one.integration.snapshots.js create mode 100644 packages/cli/test/fixtures/relation/controllers/address-customer.controller.ts create mode 100644 packages/cli/test/fixtures/relation/models/address-class-type.model.ts create mode 100644 packages/cli/test/fixtures/relation/models/address-class.model.ts create mode 100644 packages/cli/test/fixtures/relation/models/address-with-fk.model.ts create mode 100644 packages/cli/test/fixtures/relation/models/address.model.ts create mode 100644 packages/cli/test/fixtures/relation/models/customer6.model.ts create mode 100644 packages/cli/test/fixtures/relation/repositories/address-class-type.repository.ts create mode 100644 packages/cli/test/fixtures/relation/repositories/address-class.repository.ts create mode 100644 packages/cli/test/fixtures/relation/repositories/address.repository.ts create mode 100644 packages/cli/test/integration/generators/relation.has-one.integration.js diff --git a/packages/cli/generators/relation/has-one-relation.generator.js b/packages/cli/generators/relation/has-one-relation.generator.js new file mode 100644 index 000000000000..10799256bc7e --- /dev/null +++ b/packages/cli/generators/relation/has-one-relation.generator.js @@ -0,0 +1,199 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/cli +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +const path = require('path'); +const BaseRelationGenerator = require('./base-relation.generator'); +const relationUtils = require('./utils.generator'); +const utils = require('../../lib/utils'); + +const CONTROLLER_TEMPLATE_PATH_HAS_ONE = + 'controller-relation-template-has-one.ts.ejs'; + +module.exports = class HasOneRelationGenerator extends BaseRelationGenerator { + constructor(args, opts) { + super(args, opts); + } + + async generateControllers(options) { + this.artifactInfo.sourceModelClassName = options.sourceModel; + this.artifactInfo.targetModelClassName = options.destinationModel; + this.artifactInfo.sourceRepositoryClassName = + this.artifactInfo.sourceModelClassName + 'Repository'; + this.artifactInfo.controllerClassName = + this.artifactInfo.sourceModelClassName + + this.artifactInfo.targetModelClassName + + 'Controller'; + this.artifactInfo.paramSourceRepository = utils.camelCase( + this.artifactInfo.sourceModelClassName + 'Repository', + ); + + this.artifactInfo.sourceModelName = utils.toFileName(options.sourceModel); + this.artifactInfo.sourceModelPath = utils.pluralize( + this.artifactInfo.sourceModelName, + ); + this.artifactInfo.targetModelName = utils.toFileName( + options.destinationModel, + ); + this.artifactInfo.targetModelPath = this.artifactInfo.targetModelName; + this.artifactInfo.targetModelRequestBody = utils.camelCase( + this.artifactInfo.targetModelName, + ); + this.artifactInfo.relationPropertyName = options.relationName; + this.artifactInfo.sourceModelPrimaryKey = options.sourceModelPrimaryKey; + this.artifactInfo.sourceModelPrimaryKeyType = + options.sourceModelPrimaryKeyType; + this.artifactInfo.targetModelPrimaryKey = options.targetModelPrimaryKey; + this.artifactInfo.foreignKeyName = options.foreignKeyName; + + const source = this.templatePath(CONTROLLER_TEMPLATE_PATH_HAS_ONE); + + this.artifactInfo.name = + options.sourceModel + '-' + options.destinationModel; + this.artifactInfo.outFile = + utils.toFileName(this.artifactInfo.name) + '.controller.ts'; + + const dest = this.destinationPath( + path.join(this.artifactInfo.outDir, this.artifactInfo.outFile), + ); + + this.copyTemplatedFiles(source, dest, this.artifactInfo); + await relationUtils.addExportController( + this, + path.resolve(this.artifactInfo.outDir, 'index.ts'), + this.artifactInfo.controllerClassName, + utils.toFileName(this.artifactInfo.name) + '.controller', + ); + } + + async generateModels(options) { + // for repo to generate relation name + this.artifactInfo.relationName = options.relationName; + const modelDir = this.artifactInfo.modelDir; + const sourceModel = options.sourceModel; + + const targetModel = options.destinationModel; + + const relationType = options.relationType; + const relationName = options.relationName; + const fktype = options.sourceModelPrimaryKeyType; + const isForeignKeyExist = options.doesForeignKeyExist; + const foreignKeyName = options.foreignKeyName; + + const isDefaultForeignKey = + foreignKeyName === utils.camelCase(options.sourceModel) + 'Id'; + + let modelProperty; + const project = new relationUtils.AstLoopBackProject(); + + const sourceFile = relationUtils.addFileToProject( + project, + modelDir, + sourceModel, + ); + const sourceClass = relationUtils.getClassObj(sourceFile, sourceModel); + relationUtils.doesRelationExist(sourceClass, relationName); + + modelProperty = this.getHasOne( + targetModel, + relationName, + isDefaultForeignKey, + foreignKeyName, + ); + + relationUtils.addProperty(sourceClass, modelProperty); + const imports = relationUtils.getRequiredImports(targetModel, relationType); + + relationUtils.addRequiredImports(sourceFile, imports); + await sourceFile.save(); + + const targetFile = relationUtils.addFileToProject( + project, + modelDir, + targetModel, + ); + const targetClass = relationUtils.getClassObj(targetFile, targetModel); + + if (isForeignKeyExist) { + if ( + !relationUtils.isValidPropertyType(targetClass, foreignKeyName, fktype) + ) { + throw new Error('foreignKey Type Error'); + } + } else { + modelProperty = relationUtils.addForeignKey(foreignKeyName, fktype); + relationUtils.addProperty(targetClass, modelProperty); + targetClass.formatText(); + await targetFile.save(); + } + } + + getHasOne(className, relationName, isDefaultForeignKey, foreignKeyName) { + let relationDecorator = [ + { + name: 'hasOne', + arguments: [`() => ${className}, {keyTo: '${foreignKeyName}'}`], + }, + ]; + if (isDefaultForeignKey) { + relationDecorator = [ + { + name: 'hasOne', + arguments: [`() => ${className}`], + }, + ]; + } + + return { + decorators: relationDecorator, + name: relationName, + type: className, + }; + } + + _getRepositoryRequiredImports(dstModelClassName, dstRepositoryClassName) { + const importsArray = super._getRepositoryRequiredImports( + dstModelClassName, + dstRepositoryClassName, + ); + importsArray.push({ + name: 'HasOneRepositoryFactory', + module: '@loopback/repository', + }); + return importsArray; + } + + _getRepositoryRelationPropertyName() { + return this.artifactInfo.relationName; + } + + _getRepositoryRelationPropertyType() { + return `HasOneRepositoryFactory<${utils.toClassName( + this.artifactInfo.dstModelClass, + )}, typeof ${utils.toClassName( + this.artifactInfo.srcModelClass, + )}.prototype.${this.artifactInfo.srcModelPrimaryKey}>`; + } + + _addCreatorToRepositoryConstructor(classConstructor) { + const relationPropertyName = this._getRepositoryRelationPropertyName(); + const statement = + `this.${relationPropertyName} = ` + + `this.createHasOneRepositoryFactoryFor('${relationPropertyName}', ` + + `${utils.camelCase(this.artifactInfo.dstRepositoryClassName)}Getter);`; + classConstructor.insertStatements(1, statement); + } + + _registerInclusionResolverForRelation(classConstructor, options) { + const relationPropertyName = this._getRepositoryRelationPropertyName(); + if (options.registerInclusionResolver) { + const statement = + `this.registerInclusionResolver(` + + `'${relationPropertyName}', this.${relationPropertyName}.inclusionResolver);`; + classConstructor.insertStatements(2, statement); + } + } +}; diff --git a/packages/cli/generators/relation/index.js b/packages/cli/generators/relation/index.js index d1612e7fb7d2..285a09aa1b47 100644 --- a/packages/cli/generators/relation/index.js +++ b/packages/cli/generators/relation/index.js @@ -15,6 +15,7 @@ const relationUtils = require('./utils.generator'); const BelongsToRelationGenerator = require('./belongs-to-relation.generator'); const HasManyRelationGenerator = require('./has-many-relation.generator'); +const HasOneRelationGenerator = require('./has-one-relation.generator'); const ERROR_INCORRECT_RELATION_TYPE = 'Incorrect relation type'; const ERROR_MODEL_DOES_NOT_EXIST = 'model does not exist.'; @@ -141,6 +142,11 @@ module.exports = class RelationGenerator extends ArtifactGenerator { utils.camelCase(this.artifactInfo.destinationModel), ); break; + case relationUtils.relationType.hasOne: + defaultRelationName = utils.camelCase( + this.artifactInfo.destinationModel, + ); + break; } return defaultRelationName; @@ -515,6 +521,9 @@ module.exports = class RelationGenerator extends ArtifactGenerator { case relationUtils.relationType.hasMany: relationGenerator = new HasManyRelationGenerator(this.args, this.opts); break; + case relationUtils.relationType.hasOne: + relationGenerator = new HasOneRelationGenerator(this.args, this.opts); + break; } try { diff --git a/packages/cli/generators/relation/templates/controller-relation-template-has-one.ts.ejs b/packages/cli/generators/relation/templates/controller-relation-template-has-one.ts.ejs new file mode 100644 index 000000000000..9749579b38be --- /dev/null +++ b/packages/cli/generators/relation/templates/controller-relation-template-has-one.ts.ejs @@ -0,0 +1,110 @@ +import { + Count, + CountSchema, + Filter, + repository, + Where, +} from '@loopback/repository'; +import { + del, + get, + getModelSchemaRef, + getWhereSchemaFor, + param, + patch, + post, + requestBody, +} from '@loopback/rest'; +import { + <%= sourceModelClassName %>, + <%= targetModelClassName %>, +} from '../models'; +import {<%= sourceRepositoryClassName %>} from '../repositories'; + +export class <%= controllerClassName %> { + constructor( + @repository(<%= sourceRepositoryClassName %>) protected <%= paramSourceRepository %>: <%= sourceRepositoryClassName %>, + ) { } + + @get('/<%= sourceModelPath %>/{id}/<%= targetModelPath %>', { + responses: { + '200': { + description: '<%= sourceModelClassName %> has one <%= targetModelClassName %>', + content: { + 'application/json': { + schema: getModelSchemaRef(<%= targetModelClassName %>), + }, + }, + }, + }, + }) + async get( + @param.path.<%= sourceModelPrimaryKeyType %>('id') id: <%= sourceModelPrimaryKeyType %>, + @param.query.object('filter') filter?: Filter<<%= targetModelClassName %>>, + ): Promise<<%= targetModelClassName %>> { + return this.<%= paramSourceRepository %>.<%= relationPropertyName %>(id).get(filter); + } + + @post('/<%= sourceModelPath %>/{id}/<%= targetModelPath %>', { + responses: { + '200': { + description: '<%= sourceModelClassName %> model instance', + content: {'application/json': {schema: getModelSchemaRef(<%= targetModelClassName %>)}}, + }, + }, + }) + async create( + @param.path.<%= sourceModelPrimaryKeyType %>('id') id: typeof <%= sourceModelClassName %>.prototype.<%= sourceModelPrimaryKey %>, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(<%= targetModelClassName %>, { + title: 'New<%= targetModelClassName %>In<%= sourceModelClassName %>', + exclude: ['<%= targetModelPrimaryKey %>'], + optional: ['<%= foreignKeyName %>'] + }), + }, + }, + }) <%= targetModelRequestBody %>: Omit<<%= targetModelClassName %>, '<%= targetModelPrimaryKey %>'>, + ): Promise<<%= targetModelClassName %>> { + return this.<%= paramSourceRepository %>.<%= relationPropertyName %>(id).create(<%= targetModelRequestBody %>); + } + + @patch('/<%= sourceModelPath %>/{id}/<%= targetModelPath %>', { + responses: { + '200': { + description: '<%= sourceModelClassName %>.<%= targetModelClassName %> PATCH success count', + content: {'application/json': {schema: CountSchema}}, + }, + }, + }) + async patch( + @param.path.<%= sourceModelPrimaryKeyType %>('id') id: <%= sourceModelPrimaryKeyType %>, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(<%= targetModelClassName %>, {partial: true}), + }, + }, + }) + <%= targetModelRequestBody %>: Partial<<%= targetModelClassName %>>, + @param.query.object('where', getWhereSchemaFor(<%= targetModelClassName %>)) where?: Where<<%= targetModelClassName %>>, + ): Promise { + return this.<%= paramSourceRepository %>.<%= relationPropertyName %>(id).patch(<%= targetModelRequestBody %>, where); + } + + @del('/<%= sourceModelPath %>/{id}/<%= targetModelPath %>', { + responses: { + '200': { + description: '<%= sourceModelClassName %>.<%= targetModelClassName %> DELETE success count', + content: {'application/json': {schema: CountSchema}}, + }, + }, + }) + async delete( + @param.path.<%= sourceModelPrimaryKeyType %>('id') id: <%= sourceModelPrimaryKeyType %>, + @param.query.object('where', getWhereSchemaFor(<%= targetModelClassName %>)) where?: Where<<%= targetModelClassName %>>, + ): Promise { + return this.<%= paramSourceRepository %>.<%= relationPropertyName %>(id).delete(where); + } +} diff --git a/packages/cli/generators/relation/utils.generator.js b/packages/cli/generators/relation/utils.generator.js index bb897c3bd730..465254f663f2 100644 --- a/packages/cli/generators/relation/utils.generator.js +++ b/packages/cli/generators/relation/utils.generator.js @@ -13,6 +13,7 @@ const utils = require('../../lib/utils'); exports.relationType = { belongsTo: 'belongsTo', hasMany: 'hasMany', + hasOne: 'hasOne', }; class AstLoopBackProject extends ast.Project { diff --git a/packages/cli/snapshots/integration/generators/relation.has-one.integration.snapshots.js b/packages/cli/snapshots/integration/generators/relation.has-one.integration.snapshots.js new file mode 100644 index 000000000000..a1b98f56bc2c --- /dev/null +++ b/packages/cli/snapshots/integration/generators/relation.has-one.integration.snapshots.js @@ -0,0 +1,760 @@ +// IMPORTANT +// This snapshot file is auto-generated, but designed for humans. +// It should be checked into source control and tracked carefully. +// Re-generate by setting UPDATE_SNAPSHOTS=1 and running tests. +// Make sure to inspect the changes in the snapshots below. +// Do not ignore changes! + +'use strict'; + +exports[ + `lb4 relation HasOne checks generated source class repository answers {"relationType":"hasOne","sourceModel":"Customer","destinationModel":"Address"} generates Customer repository file with different inputs 1` +] = ` +import {DefaultCrudRepository, repository, HasOneRepositoryFactory} from '@loopback/repository'; +import {Customer, Address} from '../models'; +import {DbDataSource} from '../datasources'; +import {inject, Getter} from '@loopback/core'; +import {AddressRepository} from './address.repository'; + +export class CustomerRepository extends DefaultCrudRepository< + Customer, + typeof Customer.prototype.id +> { + + public readonly address: HasOneRepositoryFactory; + + constructor(@inject('datasources.db') dataSource: DbDataSource, @repository.getter('AddressRepository') protected addressRepositoryGetter: Getter,) { + super(Customer, dataSource); + this.address = this.createHasOneRepositoryFactoryFor('address', addressRepositoryGetter); + this.registerInclusionResolver('address', this.address.inclusionResolver); + } +} + +`; + +exports[ + `lb4 relation HasOne checks generated source class repository answers {"relationType":"hasOne","sourceModel":"CustomerClass","destinationModel":"AddressClass","registerInclusionResolver":true} generates CustomerClass repository file with different inputs 1` +] = ` +import {DefaultCrudRepository, repository, HasOneRepositoryFactory} from '@loopback/repository'; +import {CustomerClass, AddressClass} from '../models'; +import {MyDBDataSource} from '../datasources'; +import {inject, Getter} from '@loopback/core'; +import {AddressClassRepository} from './address-class.repository'; + +export class CustomerClassRepository extends DefaultCrudRepository< + CustomerClass, + typeof CustomerClass.prototype.custNumber +> { + + public readonly addressClass: HasOneRepositoryFactory; + + constructor(@inject('datasources.myDB') dataSource: MyDBDataSource, @repository.getter('AddressClassRepository') protected addressClassRepositoryGetter: Getter,) { + super(CustomerClass, dataSource); + this.addressClass = this.createHasOneRepositoryFactoryFor('addressClass', addressClassRepositoryGetter); + this.registerInclusionResolver('addressClass', this.addressClass.inclusionResolver); + } +} + +`; + +exports[ + `lb4 relation HasOne checks generated source class repository answers {"relationType":"hasOne","sourceModel":"CustomerClassType","destinationModel":"AddressClassType","registerInclusionResolver":false} generates CustomerClassType repository file with different inputs 1` +] = ` +import {DefaultCrudRepository, repository, HasOneRepositoryFactory} from '@loopback/repository'; +import {CustomerClassType, AddressClassType} from '../models'; +import {MyDBDataSource} from '../datasources'; +import {inject, Getter} from '@loopback/core'; +import {AddressClassTypeRepository} from './address-class-type.repository'; + +export class CustomerClassTypeRepository extends DefaultCrudRepository< + CustomerClassType, + typeof CustomerClassType.prototype.custNumber +> { + + public readonly addressClassType: HasOneRepositoryFactory; + + constructor(@inject('datasources.myDB') dataSource: MyDBDataSource, @repository.getter('AddressClassTypeRepository') protected addressClassTypeRepositoryGetter: Getter,) { + super(CustomerClassType, dataSource); + this.addressClassType = this.createHasOneRepositoryFactoryFor('addressClassType', addressClassTypeRepositoryGetter); + } +} + +`; + +exports[ + `lb4 relation HasOne checks if the controller file created answers {"relationType":"hasOne","sourceModel":"Customer","destinationModel":"Address"} checks controller content with hasOne relation 1` +] = ` +import { + Count, + CountSchema, + Filter, + repository, + Where, +} from '@loopback/repository'; +import { + del, + get, + getModelSchemaRef, + getWhereSchemaFor, + param, + patch, + post, + requestBody, +} from '@loopback/rest'; +import { + Customer, + Address, +} from '../models'; +import {CustomerRepository} from '../repositories'; + +export class CustomerAddressController { + constructor( + @repository(CustomerRepository) protected customerRepository: CustomerRepository, + ) { } + + @get('/customers/{id}/address', { + responses: { + '200': { + description: 'Customer has one Address', + content: { + 'application/json': { + schema: getModelSchemaRef(Address), + }, + }, + }, + }, + }) + async get( + @param.path.number('id') id: number, + @param.query.object('filter') filter?: Filter
, + ): Promise
{ + return this.customerRepository.address(id).get(filter); + } + + @post('/customers/{id}/address', { + responses: { + '200': { + description: 'Customer model instance', + content: {'application/json': {schema: getModelSchemaRef(Address)}}, + }, + }, + }) + async create( + @param.path.number('id') id: typeof Customer.prototype.id, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(Address, { + title: 'NewAddressInCustomer', + exclude: ['id'], + optional: ['customerId'] + }), + }, + }, + }) address: Omit, + ): Promise
{ + return this.customerRepository.address(id).create(address); + } + + @patch('/customers/{id}/address', { + responses: { + '200': { + description: 'Customer.Address PATCH success count', + content: {'application/json': {schema: CountSchema}}, + }, + }, + }) + async patch( + @param.path.number('id') id: number, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(Address, {partial: true}), + }, + }, + }) + address: Partial
, + @param.query.object('where', getWhereSchemaFor(Address)) where?: Where
, + ): Promise { + return this.customerRepository.address(id).patch(address, where); + } + + @del('/customers/{id}/address', { + responses: { + '200': { + description: 'Customer.Address DELETE success count', + content: {'application/json': {schema: CountSchema}}, + }, + }, + }) + async delete( + @param.path.number('id') id: number, + @param.query.object('where', getWhereSchemaFor(Address)) where?: Where
, + ): Promise { + return this.customerRepository.address(id).delete(where); + } +} + +`; + +exports[ + `lb4 relation HasOne checks if the controller file created answers {"relationType":"hasOne","sourceModel":"CustomerClass","destinationModel":"AddressClass","relationName":"myAddress"} checks controller content with hasOne relation 1` +] = ` +import { + Count, + CountSchema, + Filter, + repository, + Where, +} from '@loopback/repository'; +import { + del, + get, + getModelSchemaRef, + getWhereSchemaFor, + param, + patch, + post, + requestBody, +} from '@loopback/rest'; +import { + CustomerClass, + AddressClass, +} from '../models'; +import {CustomerClassRepository} from '../repositories'; + +export class CustomerClassAddressClassController { + constructor( + @repository(CustomerClassRepository) protected customerClassRepository: CustomerClassRepository, + ) { } + + @get('/customer-classes/{id}/address-class', { + responses: { + '200': { + description: 'CustomerClass has one AddressClass', + content: { + 'application/json': { + schema: getModelSchemaRef(AddressClass), + }, + }, + }, + }, + }) + async get( + @param.path.number('id') id: number, + @param.query.object('filter') filter?: Filter, + ): Promise { + return this.customerClassRepository.myAddress(id).get(filter); + } + + @post('/customer-classes/{id}/address-class', { + responses: { + '200': { + description: 'CustomerClass model instance', + content: {'application/json': {schema: getModelSchemaRef(AddressClass)}}, + }, + }, + }) + async create( + @param.path.number('id') id: typeof CustomerClass.prototype.custNumber, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(AddressClass, { + title: 'NewAddressClassInCustomerClass', + exclude: ['addressNumber'], + optional: ['customerClassId'] + }), + }, + }, + }) addressClass: Omit, + ): Promise { + return this.customerClassRepository.myAddress(id).create(addressClass); + } + + @patch('/customer-classes/{id}/address-class', { + responses: { + '200': { + description: 'CustomerClass.AddressClass PATCH success count', + content: {'application/json': {schema: CountSchema}}, + }, + }, + }) + async patch( + @param.path.number('id') id: number, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(AddressClass, {partial: true}), + }, + }, + }) + addressClass: Partial, + @param.query.object('where', getWhereSchemaFor(AddressClass)) where?: Where, + ): Promise { + return this.customerClassRepository.myAddress(id).patch(addressClass, where); + } + + @del('/customer-classes/{id}/address-class', { + responses: { + '200': { + description: 'CustomerClass.AddressClass DELETE success count', + content: {'application/json': {schema: CountSchema}}, + }, + }, + }) + async delete( + @param.path.number('id') id: number, + @param.query.object('where', getWhereSchemaFor(AddressClass)) where?: Where, + ): Promise { + return this.customerClassRepository.myAddress(id).delete(where); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with custom foreignKey answers {"relationType":"hasOne","sourceModel":"Customer","destinationModel":"Address","foreignKeyName":"mykey"} add the keyTo to the source model 1` +] = ` +import {Entity, model, property, hasOne} from '@loopback/repository'; +import {Address} from './address.model'; + +@model() +export class Customer extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + name?: string; + + @hasOne(() => Address, {keyTo: 'mykey'}) + address: Address; + + constructor(data?: Partial) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with custom foreignKey answers {"relationType":"hasOne","sourceModel":"Customer","destinationModel":"Address","foreignKeyName":"mykey"} add the keyTo to the source model 2` +] = ` +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Address extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + name?: string; + + @property({ + type: 'number', + }) + mykey?: number; + + constructor(data?: Partial
) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with custom foreignKey answers {"relationType":"hasOne","sourceModel":"CustomerClass","destinationModel":"AddressClass","foreignKeyName":"mykey"} add the keyTo to the source model 1` +] = ` +import {Entity, model, property, hasOne} from '@loopback/repository'; +import {AddressClass} from './address-class.model'; + +@model() +export class CustomerClass extends Entity { + @property({ + type: 'number', + id: true, + }) + custNumber?: number; + + @property({ + type: 'string', + }) + name?: string; + + @hasOne(() => AddressClass, {keyTo: 'mykey'}) + addressClass: AddressClass; + + constructor(data?: Partial) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with custom foreignKey answers {"relationType":"hasOne","sourceModel":"CustomerClass","destinationModel":"AddressClass","foreignKeyName":"mykey"} add the keyTo to the source model 2` +] = ` +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class AddressClass extends Entity { + @property({ + type: 'number', + id: true, + }) + addressNumber?: number; + + @property({ + type: 'string', + }) + name?: string; + + @property({ + type: 'number', + }) + mykey?: number; + + constructor(data?: Partial) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with custom foreignKey answers {"relationType":"hasOne","sourceModel":"CustomerClassType","destinationModel":"AddressClassType","foreignKeyName":"mykey"} add the keyTo to the source model 1` +] = ` +import {Entity, model, property, hasOne} from '@loopback/repository'; +import {AddressClassType} from './address-class-type.model'; + +@model() +export class CustomerClassType extends Entity { + @property({ + type: 'number', + id: true, + }) + custNumber: number; + + @property({ + type: 'string', + }) + name?: string; + + @hasOne(() => AddressClassType, {keyTo: 'mykey'}) + addressClassType: AddressClassType; + + constructor(data?: Partial) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with custom foreignKey answers {"relationType":"hasOne","sourceModel":"CustomerClassType","destinationModel":"AddressClassType","foreignKeyName":"mykey"} add the keyTo to the source model 2` +] = ` +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class AddressClassType extends Entity { + @property({ + type: 'string', + id: true, + }) + addressString: string; + + @property({ + type: 'string', + }) + name?: string; + + @property({ + type: 'number', + }) + mykey?: number; + + constructor(data?: Partial) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with custom relation name answers {"relationType":"hasOne","sourceModel":"Customer","destinationModel":"Address","relationName":"myAddress"} relation name should be myAddress 1` +] = ` +import {Entity, model, property, hasOne} from '@loopback/repository'; +import {Address} from './address.model'; + +@model() +export class Customer extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + name?: string; + + @hasOne(() => Address) + myAddress: Address; + + constructor(data?: Partial) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with custom relation name answers {"relationType":"hasOne","sourceModel":"Customer","destinationModel":"Address","relationName":"myAddress"} relation name should be myAddress 2` +] = ` +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Address extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + name?: string; + + @property({ + type: 'number', + }) + customerId?: number; + + constructor(data?: Partial
) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with custom relation name answers {"relationType":"hasOne","sourceModel":"CustomerClass","destinationModel":"AddressClass","relationName":"myAddress"} relation name should be myAddress 1` +] = ` +import {Entity, model, property, hasOne} from '@loopback/repository'; +import {AddressClass} from './address-class.model'; + +@model() +export class CustomerClass extends Entity { + @property({ + type: 'number', + id: true, + }) + custNumber?: number; + + @property({ + type: 'string', + }) + name?: string; + + @hasOne(() => AddressClass) + myAddress: AddressClass; + + constructor(data?: Partial) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with custom relation name answers {"relationType":"hasOne","sourceModel":"CustomerClass","destinationModel":"AddressClass","relationName":"myAddress"} relation name should be myAddress 2` +] = ` +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class AddressClass extends Entity { + @property({ + type: 'number', + id: true, + }) + addressNumber?: number; + + @property({ + type: 'string', + }) + name?: string; + + @property({ + type: 'number', + }) + customerClassId?: number; + + constructor(data?: Partial) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with custom relation name answers {"relationType":"hasOne","sourceModel":"CustomerClassType","destinationModel":"AddressClassType","relationName":"myAddress"} relation name should be myAddress 1` +] = ` +import {Entity, model, property, hasOne} from '@loopback/repository'; +import {AddressClassType} from './address-class-type.model'; + +@model() +export class CustomerClassType extends Entity { + @property({ + type: 'number', + id: true, + }) + custNumber: number; + + @property({ + type: 'string', + }) + name?: string; + + @hasOne(() => AddressClassType) + myAddress: AddressClassType; + + constructor(data?: Partial) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with custom relation name answers {"relationType":"hasOne","sourceModel":"CustomerClassType","destinationModel":"AddressClassType","relationName":"myAddress"} relation name should be myAddress 2` +] = ` +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class AddressClassType extends Entity { + @property({ + type: 'string', + id: true, + }) + addressString: string; + + @property({ + type: 'string', + }) + name?: string; + + @property({ + type: 'number', + }) + customerClassTypeId?: number; + + constructor(data?: Partial) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with default values answers {"relationType":"hasOne","sourceModel":"Customer","destinationModel":"Address"} has correct default imports 1` +] = ` +import {Entity, model, property, hasOne} from '@loopback/repository'; +import {Address} from './address.model'; + +@model() +export class Customer extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + name?: string; + + @hasOne(() => Address) + address: Address; + + constructor(data?: Partial) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with default values answers {"relationType":"hasOne","sourceModel":"CustomerClass","destinationModel":"AddressClass"} has correct default imports 1` +] = ` +import {Entity, model, property, hasOne} from '@loopback/repository'; +import {AddressClass} from './address-class.model'; + +@model() +export class CustomerClass extends Entity { + @property({ + type: 'number', + id: true, + }) + custNumber?: number; + + @property({ + type: 'string', + }) + name?: string; + + @hasOne(() => AddressClass) + addressClass: AddressClass; + + constructor(data?: Partial) { + super(data); + } +} + +`; + +exports[ + `lb4 relation HasOne generates model relation with default values answers {"relationType":"hasOne","sourceModel":"CustomerClassType","destinationModel":"AddressClassType"} has correct default imports 1` +] = ` +import {Entity, model, property, hasOne} from '@loopback/repository'; +import {AddressClassType} from './address-class-type.model'; + +@model() +export class CustomerClassType extends Entity { + @property({ + type: 'number', + id: true, + }) + custNumber: number; + + @property({ + type: 'string', + }) + name?: string; + + @hasOne(() => AddressClassType) + addressClassType: AddressClassType; + + constructor(data?: Partial) { + super(data); + } +} + +`; diff --git a/packages/cli/test/fixtures/relation/controllers/address-customer.controller.ts b/packages/cli/test/fixtures/relation/controllers/address-customer.controller.ts new file mode 100644 index 000000000000..4a943ecf8154 --- /dev/null +++ b/packages/cli/test/fixtures/relation/controllers/address-customer.controller.ts @@ -0,0 +1 @@ +export class AddressCustomerController {} diff --git a/packages/cli/test/fixtures/relation/index.js b/packages/cli/test/fixtures/relation/index.js index 7d2ceecce3dc..28f448b7211a 100644 --- a/packages/cli/test/fixtures/relation/index.js +++ b/packages/cli/test/fixtures/relation/index.js @@ -17,6 +17,11 @@ const SourceEntries = { file: 'customer.model.ts', content: readSourceFile('./models/customer5.model.ts'), }, + CustomerModelWithAddressProperty: { + path: MODEL_APP_PATH, + file: 'customer.model.ts', + content: readSourceFile('./models/customer6.model.ts'), + }, CustomerRepository: { path: REPOSITORY_APP_PATH, file: 'customer.repository.ts', @@ -83,6 +88,44 @@ const SourceEntries = { content: readSourceFile('./repositories/order-class-type.repository.ts'), }, + AddressModel: { + path: MODEL_APP_PATH, + file: 'address.model.ts', + content: readSourceFile('./models/address.model.ts'), + }, + AddressModelWithCustomerIdProperty: { + path: MODEL_APP_PATH, + file: 'address.model.ts', + content: readSourceFile('./models/address-with-fk.model.ts'), + }, + AddressRepository: { + path: REPOSITORY_APP_PATH, + file: 'address.repository.ts', + content: readSourceFile('./repositories/address.repository.ts'), + }, + + AddressClassModel: { + path: MODEL_APP_PATH, + file: 'address-class.model.ts', + content: readSourceFile('./models/address-class.model.ts'), + }, + AddressClassRepository: { + path: REPOSITORY_APP_PATH, + file: 'address-class.repository.ts', + content: readSourceFile('./repositories/address-class.repository.ts'), + }, + + AddressClassTypeModel: { + path: MODEL_APP_PATH, + file: 'address-class-type.model.ts', + content: readSourceFile('./models/address-class-type.model.ts'), + }, + AddressClassTypeRepository: { + path: REPOSITORY_APP_PATH, + file: 'address-class-type.repository.ts', + content: readSourceFile('./repositories/address-class-type.repository.ts'), + }, + NoKeyModel: { path: MODEL_APP_PATH, file: 'no-key.model.ts', @@ -175,10 +218,13 @@ exports.SANDBOX_FILES = [ }, SourceEntries.CustomerRepository, SourceEntries.OrderRepository, + SourceEntries.AddressRepository, SourceEntries.CustomerClassRepository, SourceEntries.OrderClassRepository, + SourceEntries.AddressClassRepository, SourceEntries.CustomerClassTypeRepository, SourceEntries.OrderClassTypeRepository, + SourceEntries.AddressClassTypeRepository, SourceEntries.NoKeyRepository, { path: DATASOURCE_APP_PATH, @@ -187,21 +233,27 @@ exports.SANDBOX_FILES = [ }, SourceEntries.CustomerModel, SourceEntries.OrderModel, + SourceEntries.AddressModel, SourceEntries.NoKeyModel, SourceEntries.NoRepoModel, SourceEntries.CustomerClassModel, SourceEntries.OrderClassModel, + SourceEntries.AddressClassModel, SourceEntries.CustomerClassTypeModel, SourceEntries.OrderClassTypeModel, + SourceEntries.AddressClassTypeModel, ]; exports.SANDBOX_FILES2 = [ SourceEntries.CustomerRepository, SourceEntries.OrderRepository, + SourceEntries.AddressRepository, SourceEntries.CustomerClassRepository, SourceEntries.OrderClassRepository, + SourceEntries.AddressClassRepository, SourceEntries.CustomerClassTypeRepository, SourceEntries.OrderClassTypeRepository, + SourceEntries.AddressClassTypeRepository, SourceEntries.NoKeyRepository, { @@ -212,10 +264,12 @@ exports.SANDBOX_FILES2 = [ SourceEntries.CustomerModel, SourceEntries.OrderModel, + SourceEntries.AddressModel, SourceEntries.NoKeyModel, SourceEntries.NoRepoModel, SourceEntries.CustomerClassModel, SourceEntries.OrderClassModel, + SourceEntries.AddressClassModel, SourceEntries.CustomerClassTypeModel, SourceEntries.IndexOfControllers, diff --git a/packages/cli/test/fixtures/relation/models/address-class-type.model.ts b/packages/cli/test/fixtures/relation/models/address-class-type.model.ts new file mode 100644 index 000000000000..34dc743e8606 --- /dev/null +++ b/packages/cli/test/fixtures/relation/models/address-class-type.model.ts @@ -0,0 +1,19 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class AddressClassType extends Entity { + @property({ + type: 'string', + id: true, + }) + addressString: string; + + @property({ + type: 'string', + }) + name?: string; + + constructor(data?: Partial) { + super(data); + } +} diff --git a/packages/cli/test/fixtures/relation/models/address-class.model.ts b/packages/cli/test/fixtures/relation/models/address-class.model.ts new file mode 100644 index 000000000000..9a5646ff4645 --- /dev/null +++ b/packages/cli/test/fixtures/relation/models/address-class.model.ts @@ -0,0 +1,19 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class AddressClass extends Entity { + @property({ + type: 'number', + id: true, + }) + addressNumber?: number; + + @property({ + type: 'string', + }) + name?: string; + + constructor(data?: Partial) { + super(data); + } +} diff --git a/packages/cli/test/fixtures/relation/models/address-with-fk.model.ts b/packages/cli/test/fixtures/relation/models/address-with-fk.model.ts new file mode 100644 index 000000000000..223b07fb6601 --- /dev/null +++ b/packages/cli/test/fixtures/relation/models/address-with-fk.model.ts @@ -0,0 +1,25 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Address extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + name?: string; + + @property({ + type: 'string', + }) + customerId: string; + + constructor(data?: Partial
) { + super(data); + } +} diff --git a/packages/cli/test/fixtures/relation/models/address.model.ts b/packages/cli/test/fixtures/relation/models/address.model.ts new file mode 100644 index 000000000000..3e905de3f7d0 --- /dev/null +++ b/packages/cli/test/fixtures/relation/models/address.model.ts @@ -0,0 +1,20 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Address extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + name?: string; + + constructor(data?: Partial
) { + super(data); + } +} diff --git a/packages/cli/test/fixtures/relation/models/customer6.model.ts b/packages/cli/test/fixtures/relation/models/customer6.model.ts new file mode 100644 index 000000000000..e92659e2df09 --- /dev/null +++ b/packages/cli/test/fixtures/relation/models/customer6.model.ts @@ -0,0 +1,22 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Customer extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + name?: string; + + address: string; + + constructor(data?: Partial) { + super(data); + } +} diff --git a/packages/cli/test/fixtures/relation/repositories/address-class-type.repository.ts b/packages/cli/test/fixtures/relation/repositories/address-class-type.repository.ts new file mode 100644 index 000000000000..86dd63f73cd2 --- /dev/null +++ b/packages/cli/test/fixtures/relation/repositories/address-class-type.repository.ts @@ -0,0 +1,13 @@ +import {DefaultCrudRepository} from '@loopback/repository'; +import {AddressClassType} from '../models'; +import {MyDBDataSource} from '../datasources'; +import {inject} from '@loopback/core'; + +export class AddressClassTypeRepository extends DefaultCrudRepository< + AddressClassType, + typeof AddressClassType.prototype.orderString +> { + constructor(@inject('datasources.myDB') dataSource: MyDBDataSource) { + super(AddressClassType, dataSource); + } +} diff --git a/packages/cli/test/fixtures/relation/repositories/address-class.repository.ts b/packages/cli/test/fixtures/relation/repositories/address-class.repository.ts new file mode 100644 index 000000000000..212c732f3ba4 --- /dev/null +++ b/packages/cli/test/fixtures/relation/repositories/address-class.repository.ts @@ -0,0 +1,13 @@ +import {DefaultCrudRepository} from '@loopback/repository'; +import {AddressClass} from '../models'; +import {MyDBDataSource} from '../datasources'; +import {inject} from '@loopback/core'; + +export class AddressClassRepository extends DefaultCrudRepository< + AddressClass, + typeof AddressClass.prototype.orderNumber +> { + constructor(@inject('datasources.myDB') dataSource: MyDBDataSource) { + super(AddressClass, dataSource); + } +} diff --git a/packages/cli/test/fixtures/relation/repositories/address.repository.ts b/packages/cli/test/fixtures/relation/repositories/address.repository.ts new file mode 100644 index 000000000000..2a21dd8261e0 --- /dev/null +++ b/packages/cli/test/fixtures/relation/repositories/address.repository.ts @@ -0,0 +1,17 @@ +import {DefaultCrudRepository, BelongsToAccessor} from '@loopback/repository'; +import {Address, Customer} from '../models'; +import {DbDataSource} from '../datasources'; +import {inject} from '@loopback/core'; + +export class AddressRepository extends DefaultCrudRepository< + Address, + typeof Address.prototype.id +> { + public readonly myCustomer: BelongsToAccessor< + Customer, + typeof Address.prototype.id + >; + constructor(@inject('datasources.db') dataSource: DbDataSource) { + super(Address, dataSource); + } +} diff --git a/packages/cli/test/integration/generators/relation.has-one.integration.js b/packages/cli/test/integration/generators/relation.has-one.integration.js new file mode 100644 index 000000000000..fe62b8dd0b3d --- /dev/null +++ b/packages/cli/test/integration/generators/relation.has-one.integration.js @@ -0,0 +1,416 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/cli +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +const path = require('path'); +const assert = require('yeoman-assert'); +const {expect, TestSandbox} = require('@loopback/testlab'); +const {expectFileToMatchSnapshot} = require('../../snapshots'); + +const generator = path.join(__dirname, '../../../generators/relation'); +const {SANDBOX_FILES, SourceEntries} = require('../../fixtures/relation'); +const testUtils = require('../../test-utils'); + +// Test Sandbox +const SANDBOX_PATH = path.resolve(__dirname, '..', '.sandbox'); +const MODEL_APP_PATH = 'src/models'; +const CONTROLLER_PATH = 'src/controllers'; +const REPOSITORY_APP_PATH = 'src/repositories'; + +const sandbox = new TestSandbox(SANDBOX_PATH); + +const sourceFileName = [ + 'customer.model.ts', + 'customer-class.model.ts', + 'customer-class-type.model.ts', +]; +const targetFileName = [ + 'address.model.ts', + 'address-class.model.ts', + 'address-class-type.model.ts', +]; +const controllerFileName = [ + 'customer-address.controller.ts', + 'customer-class-address-class.controller.ts', + 'customer-class-type-address-class-type.controller.ts', +]; +const repositoryFileName = [ + 'customer.repository.ts', + 'customer-class.repository.ts', + 'customer-class-type.repository.ts', +]; + +describe('lb4 relation HasOne', function() { + // eslint-disable-next-line no-invalid-this + this.timeout(30000); + + it('rejects relation when the corresponding repository does not exist', async () => { + await sandbox.reset(); + + const prompt = { + relationType: 'hasOne', + sourceModel: 'NoRepo', + destinationModel: 'Customer', + }; + + return expect( + testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withPrompts(prompt), + ).to.be.rejectedWith( + /NoRepoRepository class does not exist\. Please create repository first with \"lb4 repository\" command\./, + ); + }); + + it('rejects relation when source key already exist in the model', async () => { + await sandbox.reset(); + + const prompt = { + relationType: 'hasOne', + sourceModel: 'Customer', + destinationModel: 'Address', + }; + + return expect( + testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: [ + SourceEntries.CustomerModelWithAddressProperty, + SourceEntries.AddressModel, + SourceEntries.CustomerRepository, + SourceEntries.AddressRepository, + ], + }), + ) + .withPrompts(prompt), + ).to.be.rejectedWith( + /relational property address already exist in the model Customer/, + ); + }); + + context('Execute relation with existing relation name', () => { + it('rejects if the relation name already exists in the repository', async () => { + await sandbox.reset(); + + const prompt = { + relationType: 'hasOne', + sourceModel: 'Address', + destinationModel: 'Customer', + relationName: 'myCustomer', + }; + + return expect( + testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: [ + SourceEntries.CustomerModel, + SourceEntries.AddressModel, + SourceEntries.CustomerRepository, + SourceEntries.AddressRepository, + ], + }), + ) + .withPrompts(prompt), + ).to.be.rejectedWith( + `relation myCustomer already exists in the repository AddressRepository.`, + ); + }); + }); + + // special cases regardless of the repository type + context('generates model relation with default values', () => { + const promptArray = [ + { + relationType: 'hasOne', + sourceModel: 'Customer', + destinationModel: 'Address', + }, + { + relationType: 'hasOne', + sourceModel: 'CustomerClass', + destinationModel: 'AddressClass', + }, + { + relationType: 'hasOne', + sourceModel: 'CustomerClassType', + destinationModel: 'AddressClassType', + }, + ]; + + promptArray.forEach(function(multiItemPrompt, i) { + describe('answers ' + JSON.stringify(multiItemPrompt), () => { + suite(multiItemPrompt, i); + }); + }); + + function suite(multiItemPrompt, i) { + before(async function runGeneratorWithAnswers() { + await sandbox.reset(); + await testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withPrompts(multiItemPrompt); + }); + + it('has correct default imports', async () => { + const sourceFilePath = path.join( + SANDBOX_PATH, + MODEL_APP_PATH, + sourceFileName[i], + ); + assert.file(sourceFilePath); + expectFileToMatchSnapshot(sourceFilePath); + }); + } + }); + + context('generates model relation with custom relation name', () => { + const promptArray = [ + { + relationType: 'hasOne', + sourceModel: 'Customer', + destinationModel: 'Address', + relationName: 'myAddress', + }, + { + relationType: 'hasOne', + sourceModel: 'CustomerClass', + destinationModel: 'AddressClass', + relationName: 'myAddress', + }, + { + relationType: 'hasOne', + sourceModel: 'CustomerClassType', + destinationModel: 'AddressClassType', + relationName: 'myAddress', + }, + ]; + + promptArray.forEach(function(multiItemPrompt, i) { + describe('answers ' + JSON.stringify(multiItemPrompt), () => { + suite(multiItemPrompt, i); + }); + }); + + function suite(multiItemPrompt, i) { + before(async function runGeneratorWithAnswers() { + await sandbox.reset(); + await testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withPrompts(multiItemPrompt); + }); + + it('relation name should be myAddress', async () => { + const sourceFilePath = path.join( + SANDBOX_PATH, + MODEL_APP_PATH, + sourceFileName[i], + ); + const targetFilePath = path.join( + SANDBOX_PATH, + MODEL_APP_PATH, + targetFileName[i], + ); + + assert.file(sourceFilePath); + assert.file(targetFilePath); + expectFileToMatchSnapshot(sourceFilePath); + expectFileToMatchSnapshot(targetFilePath); + }); + } + }); + + context('generates model relation with custom foreignKey', () => { + const promptArray = [ + { + relationType: 'hasOne', + sourceModel: 'Customer', + destinationModel: 'Address', + foreignKeyName: 'mykey', + }, + { + relationType: 'hasOne', + sourceModel: 'CustomerClass', + destinationModel: 'AddressClass', + foreignKeyName: 'mykey', + }, + { + relationType: 'hasOne', + sourceModel: 'CustomerClassType', + destinationModel: 'AddressClassType', + foreignKeyName: 'mykey', + }, + ]; + + promptArray.forEach(function(multiItemPrompt, i) { + describe('answers ' + JSON.stringify(multiItemPrompt), () => { + suite(multiItemPrompt, i); + }); + }); + + function suite(multiItemPrompt, i) { + before(async function runGeneratorWithAnswers() { + await sandbox.reset(); + await testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withPrompts(multiItemPrompt); + }); + + it('add the keyTo to the source model', async () => { + const sourceFilePath = path.join( + SANDBOX_PATH, + MODEL_APP_PATH, + sourceFileName[i], + ); + const targetFilePath = path.join( + SANDBOX_PATH, + MODEL_APP_PATH, + targetFileName[i], + ); + + assert.file(sourceFilePath); + assert.file(targetFilePath); + expectFileToMatchSnapshot(sourceFilePath); + expectFileToMatchSnapshot(targetFilePath); + }); + } + }); + + context('checks if the controller file created ', () => { + const promptArray = [ + { + relationType: 'hasOne', + sourceModel: 'Customer', + destinationModel: 'Address', + }, + { + relationType: 'hasOne', + sourceModel: 'CustomerClass', + destinationModel: 'AddressClass', + relationName: 'myAddress', + }, + ]; + + promptArray.forEach(function(multiItemPrompt, i) { + describe('answers ' + JSON.stringify(multiItemPrompt), () => { + suite(multiItemPrompt, i); + }); + }); + + function suite(multiItemPrompt, i) { + before(async function runGeneratorWithAnswers() { + await sandbox.reset(); + await testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withPrompts(multiItemPrompt); + }); + + it('new controller file has been created', async () => { + const filePath = path.join( + SANDBOX_PATH, + CONTROLLER_PATH, + controllerFileName[i], + ); + assert.file(filePath); + }); + + it('checks controller content with hasOne relation', async () => { + const filePath = path.join( + SANDBOX_PATH, + CONTROLLER_PATH, + controllerFileName[i], + ); + expectFileToMatchSnapshot(filePath); + }); + } + }); + + context('checks generated source class repository', () => { + const promptArray = [ + { + relationType: 'hasOne', + sourceModel: 'Customer', + destinationModel: 'Address', + }, + { + relationType: 'hasOne', + sourceModel: 'CustomerClass', + destinationModel: 'AddressClass', + registerInclusionResolver: true, + }, + { + relationType: 'hasOne', + sourceModel: 'CustomerClassType', + destinationModel: 'AddressClassType', + registerInclusionResolver: false, + }, + ]; + + const sourceClassnames = ['Customer', 'CustomerClass', 'CustomerClassType']; + promptArray.forEach(function(multiItemPrompt, i) { + describe('answers ' + JSON.stringify(multiItemPrompt), () => { + suite(multiItemPrompt, i); + }); + }); + + function suite(multiItemPrompt, i) { + before(async function runGeneratorWithAnswers() { + await sandbox.reset(); + await testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withPrompts(multiItemPrompt); + }); + + it( + 'generates ' + + sourceClassnames[i] + + ' repository file with different inputs', + async () => { + const sourceFilePath = path.join( + SANDBOX_PATH, + REPOSITORY_APP_PATH, + repositoryFileName[i], + ); + + expectFileToMatchSnapshot(sourceFilePath); + }, + ); + } + }); +}); From bd15c3af1ad15c542e54f524bcf39b17350cb513 Mon Sep 17 00:00:00 2001 From: Lokesh Mohanty Date: Thu, 30 Jan 2020 22:28:43 +0530 Subject: [PATCH 2/2] docs(cli): add hasOne relation type docs --- ...{hasOne-relation.md => HasOne-relation.md} | 400 +++++++++++++----- docs/site/Relation-generator.md | 5 +- docs/site/Relations.md | 2 +- 3 files changed, 297 insertions(+), 110 deletions(-) rename docs/site/{hasOne-relation.md => HasOne-relation.md} (60%) diff --git a/docs/site/hasOne-relation.md b/docs/site/HasOne-relation.md similarity index 60% rename from docs/site/hasOne-relation.md rename to docs/site/HasOne-relation.md index 6fce77a56603..291b683104bb 100644 --- a/docs/site/hasOne-relation.md +++ b/docs/site/HasOne-relation.md @@ -3,11 +3,9 @@ lang: en title: 'hasOne Relation' keywords: LoopBack 4.0, LoopBack 4 sidebar: lb4_sidebar -permalink: /doc/en/lb4/hasOne-relation.html +permalink: /doc/en/lb4/HasOne-relation.html --- -{% include note.html content="There are some limitations to `Inclusion Resolver`. See [Limitations](Relations.md#limitations)." %} - ## Overview {% include note.html content=" @@ -17,49 +15,46 @@ Using this relation with NoSQL databases will result in unexpected behavior, such as the ability to create a relation with a model that does not exist. We are [working on a solution](https://github.com/strongloop/loopback-next/issues/2341) to better handle this. It is fine to use this relation with NoSQL databases for purposes such as navigating related models, where the referential integrity is not critical. " %} +{% include note.html content="There are some limitations to `Inclusion Resolver`. See [Limitations](Relations.md#limitations)." %} + A `hasOne` relation denotes a one-to-one connection of a model to another model through referential integrity. The referential integrity is enforced by a -foreign key constraint on the target model which usually references a primary -key on the source model and a unique constraint on the same column/key to ensure -one-to-one mapping. This relation indicates that each instance of the declaring -or source model has exactly one instance of the target model. Let's take an -example where an application has models `Supplier` and `Account` and a -`Supplier` can only have one `Account` on the system as illustrated in the -diagram below. +foreign key constraint on both the source model and the target model which +usually references a primary key on the source model for the target model and +primary key on the target model for the source model. This relation indicates +that each instance of the declaring or source model belongs to exactly one +instance of the target model. For example, in an application with suppliers and +accounts, a supplier can have only one account as illustrated in the diagram +below. ![hasOne relation illustration](./imgs/hasOne-relation-example.png) The diagram shows target model **Account** has property **supplierId** as the foreign key to reference the declaring model **Supplier's** primary key **id**. -**supplierId** needs to also be used in a unique index to ensure each -**Supplier** has only one related **Account** instance. To add a `hasOne` relation to your LoopBack application and expose its related routes, you need to perform the following steps: -1. Decorate properties on the source and target models with `@hasOne` and - `@belongsTo` to let LoopBack gather the necessary metadata. -2. Modify the source model repository class to provide access to a constrained +1. Add a property to your model to access related model instance. +2. Add a foreign key property in the target model referring to the source + model's id. +3. Modify the source model repository class to provide access to a constrained target model repository. -3. Call the constrained target model repository CRUD APIs in your controller +4. Call the constrained target model repository CRUD APIs in your controller methods. -Right now, LoopBack collects the necessary metadata and exposes the relation -APIs for the `hasOne` relation, but does not guarantee referential integrity. -This has to be set up by the user or DBA in the underlying database and an -example is shown below on how to do it with MySQL. - ## Defining a hasOne Relation This section describes how to define a `hasOne` relation at the model level using the `@hasOne` decorator. The relation constrains the target repository by -the foreign key property on its associated model. The `hasOne` relation is -defined on a source model `Supplier` in the example below: +the foreign key property on its associated model. The following example shows +how to define a `hasOne` relation on a source model `Supplier` and a target +model `Account`. {% include code-caption.html content="/src/models/supplier.model.ts" %} ```ts -import {Account, AccountWithRelations} from './account.model'; +import {Account} from './account.model'; import {Entity, property, hasOne} from '@loopback/repository'; export class Supplier extends Entity { @@ -82,37 +77,101 @@ export class Supplier extends Entity { super(data); } } +``` + +The definition of the `hasOne` relation is inferred by using the `@hasOne` +decorator. The decorator takes in a function resolving the target model class +constructor and optionally a custom foreign key to store the relation metadata. +The decorator logic also designates the relation type and tries to infer the +foreign key on the target model (`keyTo` in the relation metadata) to a default +value (source model name appended with `Id` in camel case, same as LoopBack 3). -export interface SupplierRelations { - account?: AccountWithRelations; +The decorated property name is used as the relation name and stored as part of +the source model definition's relation metadata. The property type metadata is +also preserved as a type of `Account` as part of the decoration. (Check +[Relation Metadata](HasOne-relation.md#relation-metadata) section below for more +details) + +A usage of the decorator with a custom foreign key name for the above example is +as follows: + +```ts +// import statements +class Supplier extends Entity { + // constructor, properties, etc. + @hasOne(() => Account, {keyTo: 'supplierId'}) + account?: Account; } +``` + +Add the source model's id as the foreign key property (`supplierId`) in the +target model. -export type SupplierWithRelations = Supplier & SupplierRelations; +{% include code-caption.html content="/src/models/account.model.ts" %} + +```ts +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Account extends Entity { + @property({ + type: 'number', + id: true, + required: true, + }) + id: number; + + @property({ + type: 'string', + required: true, + }) + name: string; + + @property({ + type: 'number', + }) + supplierId?: number; + + constructor(data?: Partial) { + super(data); + } +} + +export interface AccountRelations { + // describe navigational properties here +} + +export type AccountWithRelations = Account & AccountRelations; ``` -On the other side of the relation, we'd need to declare a `belongsTo` relation -since every `Account` has to belong to exactly one `Supplier`: +The foreign key property (`supplierId`) in the target model can be added via a +corresponding [belongsTo](BelongsTo-relation.md) relation, too. + +{% include code-caption.html content="/src/models/account.model.ts" %} ```ts +import {Entity, model, property, belongsTo} from '@loopback/repository'; import {Supplier, SupplierWithRelations} from './supplier.model'; -import {Entity, property, belongsTo} from '@loopback/repository'; +@model() export class Account extends Entity { @property({ type: 'number', id: true, + required: true, }) id: number; @property({ type: 'string', + required: true, }) - accountManager: string; + name: string; @belongsTo(() => Supplier) supplierId: number; - constructor(data: Partial) { + constructor(data?: Partial) { super(data); } } @@ -124,14 +183,12 @@ export interface AccountRelations { export type AccountWithRelations = Account & AccountRelations; ``` -### Relation Metadata +LB4 also provides an CLI tool `lb4 relation` to generate `hasOne` relation for +you. Before you check out the +[`Relation Generator`](https://loopback.io/doc/en/lb4/Relation-generator.html) +page, read on to learn how you can define relations to meet your requirements. -The definition of the `hasOne` relation is inferred by using the `@hasOne` -decorator. The decorator takes in a function resolving the target model class -constructor and optionally a has one relation definition object which can e.g. -contain a custom foreign key to be stored as the relation metadata. The -decorator logic also designates the relation type and tries to infer the foreign -key. +### Relation Metadata LB4 uses three `keyFrom`, `keyTo` and `name` fields in the `hasOne` relation metadata to configure relations. The relation metadata has its own default @@ -169,8 +226,8 @@ values for these three fields: -We recommend to use default values. If you'd like to customize foreign key name, -you'll need to specify some fields through the relation decorators. +We recommend to use default values. If you'd like to customize the foreign key +name, you'll need to specify some fields through the relation decorator. For customizing the foreign key name, `keyTo` field needs to be specified via `@hasOne` decorator. The following example shows how to customize the foreign @@ -181,14 +238,28 @@ key name as `suppId` instead of `supplierId`: @model() export class Supplier extends Entity { // constructor, properties, etc. + @hasOne(() => Account, {keyTo: 'suppId'}) - account?: Account; + account: Account; +} +``` + +```ts +// import statements +@model() +export class Account extends Entity { + // constructor, properties, etc. + + @property({ + type: 'number', + }) + suppId: number; // customized foreign key name } ``` Notice that if you decorate the corresponding customized foreign key of the -target model with `@belongsTo`, you also need to specify the `belongTo` relation -name in the `name` field of its relation metadata. See +target model with `@belongsTo`, you also need to specify the `belongsTo` +relation name in the `name` field of its relation metadata. See [BelongsTo](BelongsTo-relation.md) for more details. ```ts @@ -198,7 +269,7 @@ export class Account extends Entity { // constructor, properties, etc. // specify the belongsTo relation name if a customized name is used here - @belongsTo(() => Supplier, {name: 'supplier'}) // specify the belongsTo relation name + @belongsTo(() => Supplier, {name: 'supplier'}) // the name of this belongsTo relation suppId: number; // customized foreign key name } ``` @@ -215,17 +286,17 @@ export class Supplier extends Entity { id: number; // if you'd like to use this property as the source id - // of a certain relation that relates to a model `Manufacturer` + // of a certain relation that relates to a model `Review` @property({ type: 'number', }) - supplier_id: number; // not primary key + authorId: number; // not primary key - @hasOne(() => Account) - account?: Account; + @hasOne(() => Review, {keyFrom: 'authorId'}) + review: Review; - @hasOne(() => Manufacturer, {keyFrom: 'supplier_id'}) - manufacturer ?: Manufacturer; + @hasOne(() => Account) + account: Account; // ..constructor } @@ -240,11 +311,11 @@ details. ```ts // import statements @model() -export class Manufacturer extends Entity { +export class Review extends Entity { // constructor, properties, etc. // specify the keyTo if the source key is not the id property - @belongsTo(() => Supplier, {keyTo: 'supplier_id'}) + @belongsTo(() => Supplier, {keyTo: 'authorId'}) supplierId: number; // default foreign key name } ``` @@ -252,7 +323,7 @@ export class Manufacturer extends Entity { {% include important.html content="It is user's responsibility to make sure the non-id source key doesn't have duplicate value. Besides, LB4 doesn't support composite keys for now. e.g joining two tables with more than one source key. Related GitHub issue: [Composite primary/foreign keys](https://github.com/strongloop/loopback-next/issues/1830)" %} If you need to use _different names for models and database columns_, to use -`suppAccount` as db column name instead of `account` for example, the following +`my_account` as db column name other than `account` for example, the following setting would allow you to do so: ```ts @@ -260,41 +331,14 @@ setting would allow you to do so: @model() export class Supplier extends Entity { // constructor, properties, etc. - @hasOne(() => Supplier, {keyFrom: 'account'}, {name: 'suppAccount'}) - account: number; + @hasOne(() => Account, {keyFrom: 'account'}, {name: 'my_account'}) + account: Account; } ``` _Notice: the `name` field in the third parameter is not part of the relation metadata. It's part of property definition._ -## Setting up your database for hasOne relation - MySQL - -At the moment, LoopBack does not provide the means to enforce referential -integrity for the `hasOne` relation. It is up to users to set this up at the -database layer so constraints are not violated. Let's take MySQL as the backing -database for our application. Given the `Supplier` has one `Account` scenario -above, we need to run two SQL statements on the `Account` table for the database -to enforce referential integrity and align with LoopBack's `hasOne` relation. - -1. Make `supplierId` property or column a foreign key which references the `id` - from Supplier model's `id` property: - -```sql -ALTER TABLE .Account ADD FOREIGN KEY (supplierId) REFERENCES .Supplier(id); -``` - -2. Create a unique index for the same property `supplierId`, so that for each - `Supplier` instance, there is only one associated `Account` instance. - -```sql - ALTER TABLE .Account ADD UNIQUE INDEX supplierIndex (supplierId); -``` - -Before making the following changes, please follow the steps outlined in -[Database Migrations](Database-migrations.md) to create the database schemas -defined by the models in your application. - ## Configuring a hasOne relation The configuration and resolution of a `hasOne` relation takes place at the @@ -304,17 +348,16 @@ repository, the following are required: - In the constructor of your source repository class, use [Dependency Injection](Dependency-injection.md) to receive a getter function - for obtaining an instance of the target repository. \_Note: We need a getter + for obtaining an instance of the target repository. _Note: We need a getter function, accepting a string repository name instead of a repository constructor, or a repository instance, in `Account` to break a cyclic dependency between a repository with a hasOne relation and a repository with - the matching belongsTo relation. + the matching belongsTo relation._ - Declare a property with the factory function type `HasOneRepositoryFactory` on the source repository class. - -- Call the `createHasOneRepositoryFactoryFor` function in the constructor of the +- call the `createHasOneRepositoryFactoryFor` function in the constructor of the source repository class with the relation name (decorated relation property on the source model) and target repository instance and assign it the property mentioned above. @@ -359,13 +402,27 @@ export class SupplierRepository extends DefaultCrudRepository< ``` The following CRUD APIs are now available in the constrained target repository -factory `Account` for instances of `supplierRepository`: +factory `account` for instances of `SupplierRepository`: -- `create` for creating an `Account` model instance belonging to `Supplier` - model instance +- `create` for creating a target model instance belonging to `Supplier` model + instance ([API Docs](https://loopback.io/doc/en/lb4/apidocs.repository.hasonerepository.create.html)) -- `get` finding the target model instance belonging to `Supplier` model instance - ([API Docs](https://loopback.io/doc/en/lb4/apidocs.repository.hasonerepository.get.html)) +- `find` finding target model instance belonging to Supplier model instance + ([API Docs](https://loopback.io/doc/en/lb4/apidocs.repository.hasonerepository.find.html)) +- `delete` for deleting target model instance belonging to Supplier model + instance + ([API Docs](https://loopback.io/doc/en/lb4/apidocs.repository.hasonerepository.delete.html)) +- `patch` for patching target model instance belonging to Supplier model + instance + ([API Docs](https://loopback.io/doc/en/lb4/apidocs.repository.hasonerepository.patch.html)) + +For **updating** (full replace of all properties on a `PUT` endpoint for +instance) a target model you have to directly use this model repository. In this +case, the caller must provide both the foreignKey value and the primary key +(id). Since the caller already has access to the primary key of the target +model, there is no need to go through the relation repository and the operation +can be performed directly on `DefaultCrudRepository` for the target model +(`AccountRepository` in our example). ## Using hasOne constrained repository in a controller @@ -409,8 +466,8 @@ with the name following the pattern `__{methodName}__{relationName}__` (e.g. relation in LoopBack 4. First, it keeps controller classes smaller. Second, it creates a logical separation of ordinary repositories and relational repositories and thus the controllers which use them. Therefore, as shown above, -don't add `Account`-related methods to `SupplierController`, but instead create -a new `SupplierAccountController` class for them. +don't add account-related methods to `SupplierController`, but instead create a +new `SupplierAccountController` class for them. {% include note.html content=" The type of `accountData` above will possibly change to `Partial` to exclude @@ -432,15 +489,15 @@ to show the idea: A `hasOne` relation has an `inclusionResolver` function as a property. It fetches target models for the given list of source model instances. -Using the relation between `Supplier` and `Account` we have shown above, a -`Supplier` has one `Account`. +Use the relation between `Supplier` and `Account` we use above, a `Supplier` has +one `Account`. After setting up the relation in the repository class, the inclusion resolver -allows users to retrieve all suppliers along with their related account -instances through the following code at the repository level: +allows users to retrieve all suppliers along with their related accounts through +the following code at the repository level: ```ts -supplierRepository.find({include: [{relation: 'account'}]}); +supplierRepo.find({include: [{relation: 'account'}]}); ``` or use APIs with controllers: @@ -461,21 +518,24 @@ GET http://localhost:3000/suppliers?filter[include][][relation]=account allowed to be traversed. Users can decide to which resolvers can be added.) The following code snippet shows how to register the inclusion resolver for the -hasOne relation 'account': +has-one relation 'account': ```ts export class SupplierRepository extends DefaultCrudRepository { account: HasOneRepositoryFactory; + constructor( dataSource: juggler.DataSource, accountRepositoryGetter: Getter, ) { super(Supplier, dataSource); + // we already have this line to create a HasOneRepository factory this.account = this.createHasOneRepositoryFactoryFor( 'account', accountRepositoryGetter, ); + // add this line to register inclusion resolver this.registerInclusionResolver('account', this.account.inclusionResolver); } @@ -483,8 +543,8 @@ export class SupplierRepository extends DefaultCrudRepository { ``` - We can simply include the relation in queries via `find()`, `findOne()`, and - `findById()` methods. For example, these queries return all suppliers with - their `Account`: + `findById()` methods. For example, these queries return all Suppliers with + their `Account`s: if you process data at the repository level: @@ -508,14 +568,15 @@ export class SupplierRepository extends DefaultCrudRepository { account: {accountManager: 'Odin', supplierId: 1}, }, { - id: 5, + id: 2, name: 'Loki', account: {accountManager: 'Frigga', supplierId: 5}, }, ]; ``` -{% include note.html content="The query syntax is a slightly different from LB3. We are also thinking about simplifying the query syntax. Check our GitHub issue for more information: [Simpler Syntax for Inclusion](https://github.com/strongloop/loopback-next/issues/3205)" %} +{% include note.html content="The query syntax is a slightly different from LB3. We are also thinking about simplifying the query syntax. Check our GitHub issue for more information: +[Simpler Syntax for Inclusion](https://github.com/strongloop/loopback-next/issues/3205)" %} Here is a diagram to make this more intuitive: @@ -528,6 +589,131 @@ Here is a diagram to make this more intuitive: ### Query multiple relations It is possible to query several relations or nested include relations with -custom scope once you have the inclusion resolver of each relation set up. -Check[HasMany - Query multiple relations](HasMany-relation.md#query-multiple-relations) -for the usage and examples. +custom scope. Once you have the inclusion resolver of each relation set up, the +following queries would allow you traverse data differently: + +In our example, we have relations: + +- `Customer` _hasOne_ an `Address` - denoted as `address`. +- `Customer` _hasMany_ `Order`s - denoted as `orders`. +- `Order` _hasMany_ `Manufacturer` - denoted as `manufacturers`. + +To query **multiple relations**, for example, return all Suppliers including +their orders and address, in Node API: + +```ts +customerRepo.find({include: [{relation: 'orders'}, {relation: 'address'}]}); +``` + +Equivalently, with url, you can do: + +``` +GET http://localhost:3000/customers?filter[include][0][relation]=orders&filter[include][1][relation]=address +``` + +This gives + +```ts +[ + { + id: 1, + name: 'Thor', + addressId: 3 + orders: [ + {name: 'Mjolnir', customerId: 1}, + {name: 'Rocket Raccoon', customerId: 1}, + ], + address:{ + id: 3 + city: 'Thrudheim', + province: 'Asgard', + zipcode: '8200', + } + }, + { + id: 2, + name: 'Captain', + orders: [{name: 'Shield', customerId: 2}], // doesn't have a related address + }, +] +``` + +To query **nested relations**, for example, return all Suppliers including their +orders and include orders' manufacturers , this can be done with filter: + +```ts +customerRepo.find({ + include: [ + { + relation: 'orders', + scope: { + include: [{relation: 'manufacturers'}], + }, + }, + ], +}); +``` + +( You might use `encodeURIComponent(JSON.stringify(filter))` to convert the +filter object to a query string.) + + + +which gives + +```ts +{ + id: 1, + name: 'Thor', + addressId: 3 + orders: [ + { + name: 'Mjolnir', + customerId: 1 + }, + { + name: 'Rocket Raccoon', + customerId: 1, + manufacturers:[ // nested related models of orders + { + name: 'ToysRUs', + orderId: 1 + }, + { + name: 'ToysRThem', + orderId: 1 + } + ] + }, + ], +} +``` + +You can also have other query clauses in the scope such as `where`, `limit`, +etc. + +```ts +customerRepo.find({ + include: [ + { + relation: 'orders', + scope: { + where: {name: 'ToysRUs'}, + include: [{relation: 'manufacturers'}], + }, + }, + ], +}); +``` + +The `Where` clause above filters the result of `orders`. + +{% include tip.html content="Make sure that you have all inclusion resolvers that you need REGISTERED, and +all relation names should be UNIQUE."%} diff --git a/docs/site/Relation-generator.md b/docs/site/Relation-generator.md index 29b6be75dffa..93ea2c1578e2 100644 --- a/docs/site/Relation-generator.md +++ b/docs/site/Relation-generator.md @@ -33,7 +33,7 @@ lb4 relation [options] - `--sourceModel`: Source model. - `--destinationModel`: Destination model. - `--foreignKeyName`: Destination/Source model foreign key name for - HasMany/BelongsTo relation, respectively. + HasMany,HasOne/BelongsTo relation, respectively. - `--relationName`: Relation name. - `-c`, `--config`: JSON file name or value to configure options. - `-y`, `--yes`: Skip all confirmation prompts with default or provided value. @@ -46,7 +46,7 @@ Defining lb4 relation in one command line interface (cli): ```sh lb4 relation --sourceModel= --destinationModel= --foreignKeyName= ---relationType= [--relationName=] [--format] +--relationType= [--relationName=] [--format] ``` - `` - Type of the relation that will be created between the @@ -81,6 +81,7 @@ The tool will prompt you for: source model and the target model. Supported relation types: - [HasMany](HasMany-relation.md) + - [HasOne](HasOne-relation.md) - [BelongsTo](BelongsTo-relation.md) - **Name of the `source` model.** _(sourceModel)_ Prompts a list of available diff --git a/docs/site/Relations.md b/docs/site/Relations.md index 4ddb0c63bbfb..581427054ec9 100644 --- a/docs/site/Relations.md +++ b/docs/site/Relations.md @@ -57,7 +57,7 @@ The articles on each type of relation above will show you how to leverage the new relation engine to define and configure relations in your LoopBack application. -To generate a `HasMany` or `BelongsTo` relation through the CLI, see +To generate a `HasMany`, `HasOne` or `BelongsTo` relation through the CLI, see [Relation generator](Relation-generator.md). ## Limitations