From 584e4acb174280fe947ac39ce099b7f72c08ca11 Mon Sep 17 00:00:00 2001 From: Muhammad Aaqil Date: Wed, 8 Feb 2023 19:44:14 +0500 Subject: [PATCH] fix: allow belongsTo relation with same table Signed-off-by: Muhammad Aaqil --- .../relation/base-relation.generator.js | 18 ++- .../relation/belongs-to-relation.generator.js | 7 +- ...roller-relation-template-belongs-to.ts.ejs | 4 +- .../generators/relation/utils.generator.js | 15 +- ...lation.belongs-to.integration.snapshots.js | 73 +++++++++ .../controllers/employee.controller.ts | 1 + packages/cli/test/fixtures/relation/index.js | 19 +++ .../relation/models/employee.model.ts | 30 ++++ .../repositories/employee.repository.ts | 13 ++ .../relation.belongs-to.integration.js | 145 ++++++++++++++++++ 10 files changed, 311 insertions(+), 14 deletions(-) create mode 100644 packages/cli/test/fixtures/relation/controllers/employee.controller.ts create mode 100644 packages/cli/test/fixtures/relation/models/employee.model.ts create mode 100644 packages/cli/test/fixtures/relation/repositories/employee.repository.ts diff --git a/packages/cli/generators/relation/base-relation.generator.js b/packages/cli/generators/relation/base-relation.generator.js index 4914f34b4ca4..3a9ba0a0bf36 100644 --- a/packages/cli/generators/relation/base-relation.generator.js +++ b/packages/cli/generators/relation/base-relation.generator.js @@ -77,6 +77,7 @@ module.exports = class BaseRelationGenerator extends ArtifactGenerator { const imports = this._getRepositoryRequiredImports( options.destinationModel, this.artifactInfo.dstRepositoryClassName, + options.sourceModel, ); relationUtils.addRequiredImports( @@ -187,8 +188,12 @@ module.exports = class BaseRelationGenerator extends ArtifactGenerator { this.artifactInfo.relationName = options.relationName; } - _getRepositoryRequiredImports(dstModelClassName, dstRepositoryClassName) { - return [ + _getRepositoryRequiredImports( + dstModelClassName, + dstRepositoryClassName, + srcModelClass, + ) { + const imports = [ { name: dstModelClassName, module: '../models', @@ -201,11 +206,14 @@ module.exports = class BaseRelationGenerator extends ArtifactGenerator { name: 'Getter', module: '@loopback/core', }, - { + ]; + if (dstModelClassName !== srcModelClass) { + imports.push({ name: dstRepositoryClassName, module: `./${utils.toFileName(dstModelClassName)}.repository`, - }, - ]; + }); + } + return imports; } _getRepositoryRelationPropertyName() { diff --git a/packages/cli/generators/relation/belongs-to-relation.generator.js b/packages/cli/generators/relation/belongs-to-relation.generator.js index 5cc0b42d6f80..f5a1d46e180d 100644 --- a/packages/cli/generators/relation/belongs-to-relation.generator.js +++ b/packages/cli/generators/relation/belongs-to-relation.generator.js @@ -106,7 +106,11 @@ module.exports = class BelongsToRelationGenerator extends ( ); relationUtils.addProperty(sourceClass, modelProperty); - const imports = relationUtils.getRequiredImports(targetModel, relationType); + const imports = relationUtils.getRequiredImports( + targetModel, + relationType, + sourceModel, + ); relationUtils.addRequiredImports(sourceFile, imports); sourceClass.formatText(); @@ -147,6 +151,7 @@ module.exports = class BelongsToRelationGenerator extends ( const importsArray = super._getRepositoryRequiredImports( dstModelClassName, dstRepositoryClassName, + this.artifactInfo.srcModelClass, ); importsArray.push({ name: 'BelongsToAccessor', diff --git a/packages/cli/generators/relation/templates/controller-relation-template-belongs-to.ts.ejs b/packages/cli/generators/relation/templates/controller-relation-template-belongs-to.ts.ejs index 55ae1230601f..1451cb6cfb7b 100644 --- a/packages/cli/generators/relation/templates/controller-relation-template-belongs-to.ts.ejs +++ b/packages/cli/generators/relation/templates/controller-relation-template-belongs-to.ts.ejs @@ -6,8 +6,8 @@ import { get, getModelSchemaRef, } from '@loopback/rest'; -import { - <%= sourceModelClassName %>, +import {<%if (sourceModelClassName != targetModelClassName) { %> + <%= sourceModelClassName %>,<% } %> <%= targetModelClassName %>, } from '../models'; import {<%= sourceRepositoryClassName %>} from '../repositories'; diff --git a/packages/cli/generators/relation/utils.generator.js b/packages/cli/generators/relation/utils.generator.js index 50dcdd5f216f..7fc6cc6efde9 100644 --- a/packages/cli/generators/relation/utils.generator.js +++ b/packages/cli/generators/relation/utils.generator.js @@ -209,17 +209,20 @@ exports.addRequiredImports = function (sourceFile, imports) { } }; -exports.getRequiredImports = function (targetModel, relationType) { - return [ - { - name: targetModel, - module: './' + utils.toFileName(targetModel) + '.model', - }, +exports.getRequiredImports = function (targetModel, relationType, sourceModel) { + const requiredImports = [ { name: relationType, module: '@loopback/repository', }, ]; + if (sourceModel !== targetModel) { + requiredImports.push({ + name: targetModel, + module: './' + utils.toFileName(targetModel) + '.model', + }); + } + return requiredImports; }; exports.addCurrentImport = function (sourceFile, currentImport) { diff --git a/packages/cli/snapshots/integration/generators/relation.belongs-to.integration.snapshots.js b/packages/cli/snapshots/integration/generators/relation.belongs-to.integration.snapshots.js index 082ebc452f8c..604bb9058936 100644 --- a/packages/cli/snapshots/integration/generators/relation.belongs-to.integration.snapshots.js +++ b/packages/cli/snapshots/integration/generators/relation.belongs-to.integration.snapshots.js @@ -62,6 +62,29 @@ export class OrderRepository extends DefaultCrudRepository< `; +exports[`lb4 relation checks generated source class repository for same table relation answers {"relationType":"belongsTo","sourceModel":"Employee","destinationModel":"Employee"} generates Employee repository file with different inputs 1`] = ` +import {inject, Getter} from '@loopback/core'; +import {DefaultCrudRepository, repository, BelongsToAccessor} from '@loopback/repository'; +import {DbDataSource} from '../datasources'; +import {Employee} from '../models'; + +export class EmployeeRepository extends DefaultCrudRepository< + Employee, + typeof Employee.prototype.id +> { + + public readonly employee: BelongsToAccessor; + + constructor(@inject('datasources.db') dataSource: DbDataSource, @repository.getter('EmployeeRepository') protected employeeRepositoryGetter: Getter,) { + super(Employee, dataSource); + this.employee = this.createBelongsToAccessorFor('employee', employeeRepositoryGetter,); + this.registerInclusionResolver('employee', this.employee.inclusionResolver); + } +} + +`; + + exports[`lb4 relation checks if the controller file created answers {"relationType":"belongsTo","sourceModel":"Order","destinationModel":"Customer","relationName":"my_customer"} checks controller content with belongsTo relation 1`] = ` import { repository, @@ -160,6 +183,18 @@ export * from './order-customer.controller'; `; +exports[`lb4 relation checks if the controller file created for same table relation answers {"relationType":"belongsTo","sourceModel":"Employee","destinationModel":"Employee"} checks controller content with belongsTo relation with same table 1`] = ` +export class EmployeeController {} + +`; + + +exports[`lb4 relation checks if the controller file created for same table relation answers {"relationType":"belongsTo","sourceModel":"Employee","destinationModel":"Employee"} the new controller file added to index.ts file 1`] = ` +export * from './employee-employee.controller'; + +`; + + exports[`lb4 relation generates model relation for existing property name verifies that a preexisting property will be overwritten 1`] = ` import {Entity, model, property, belongsTo} from '@loopback/repository'; import {Customer} from './customer.model'; @@ -245,3 +280,41 @@ export class Order extends Entity { } `; + + +exports[`lb4 relation generates model relation with same table with default foreignKeyName verifies that a preexisting property will be overwritten 1`] = ` +import {Entity, model, property, belongsTo} from '@loopback/repository'; + +@model() +export class Employee extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + firstName?: string; + + @property({ + type: 'string', + }) + lastName?: string; + + @property({ + type: 'number', + }) + reportsTo?: string; + + @belongsTo(() => Employee, {name: 'reportsToEemployee'}) + employeeId: number; + + constructor(data?: Partial) { + super(data); + } +} + +`; diff --git a/packages/cli/test/fixtures/relation/controllers/employee.controller.ts b/packages/cli/test/fixtures/relation/controllers/employee.controller.ts new file mode 100644 index 000000000000..eae2f38a842e --- /dev/null +++ b/packages/cli/test/fixtures/relation/controllers/employee.controller.ts @@ -0,0 +1 @@ +export class EmployeeController {} diff --git a/packages/cli/test/fixtures/relation/index.js b/packages/cli/test/fixtures/relation/index.js index 1bc7736f3f47..07feeb9fc482 100644 --- a/packages/cli/test/fixtures/relation/index.js +++ b/packages/cli/test/fixtures/relation/index.js @@ -147,6 +147,21 @@ const SourceEntries = { file: 'doctor-patient.controller.ts', content: readSourceFile('./controllers/doctor-patient.controller.ts'), }, + EmployeeModel: { + path: MODEL_APP_PATH, + file: 'employee.model.ts', + content: readSourceFile('./models/employee.model.ts'), + }, + EmployeeRepository: { + path: REPOSITORY_APP_PATH, + file: 'employee.repository.ts', + content: readSourceFile('./repositories/employee.repository.ts'), + }, + EmployeeController: { + path: CONTROLLER_PATH, + file: 'employee.controller.ts', + content: readSourceFile('./controllers/employee.controller.ts'), + }, }; exports.SourceEntries = SourceEntries; @@ -198,6 +213,7 @@ exports.SANDBOX_FILES = [ SourceEntries.DoctorRepository, SourceEntries.PatientRepository, SourceEntries.AppointmentRepository, + SourceEntries.EmployeeRepository, SourceEntries.AccountModel, SourceEntries.CustomerModel, @@ -208,6 +224,8 @@ exports.SANDBOX_FILES = [ SourceEntries.PatientModel, SourceEntries.AppointmentModel, SourceEntries.DoctorPatientController, + SourceEntries.EmployeeModel, + SourceEntries.EmployeeController, ]; exports.SANDBOX_FILES2 = [ @@ -219,6 +237,7 @@ exports.SANDBOX_FILES2 = [ SourceEntries.DoctorRepository, SourceEntries.PatientRepository, SourceEntries.AppointmentRepository, + SourceEntries.EmployeeRepository, SourceEntries.CustomerModel, SourceEntries.CustomerModelWithInheritance, diff --git a/packages/cli/test/fixtures/relation/models/employee.model.ts b/packages/cli/test/fixtures/relation/models/employee.model.ts new file mode 100644 index 000000000000..6d12dc52d25c --- /dev/null +++ b/packages/cli/test/fixtures/relation/models/employee.model.ts @@ -0,0 +1,30 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Employee extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + firstName?: string; + + @property({ + type: 'string', + }) + lastName?: string; + + @property({ + type: 'number', + }) + reportsTo?: string; + + constructor(data?: Partial) { + super(data); + } +} diff --git a/packages/cli/test/fixtures/relation/repositories/employee.repository.ts b/packages/cli/test/fixtures/relation/repositories/employee.repository.ts new file mode 100644 index 000000000000..3eebf7af64c1 --- /dev/null +++ b/packages/cli/test/fixtures/relation/repositories/employee.repository.ts @@ -0,0 +1,13 @@ +import {inject} from '@loopback/core'; +import {DefaultCrudRepository} from '@loopback/repository'; +import {DbDataSource} from '../datasources'; +import {Employee} from '../models'; + +export class EmployeeRepository extends DefaultCrudRepository< + Employee, + typeof Employee.prototype.id +> { + constructor(@inject('datasources.db') dataSource: DbDataSource) { + super(Employee, dataSource); + } +} diff --git a/packages/cli/test/integration/generators/relation.belongs-to.integration.js b/packages/cli/test/integration/generators/relation.belongs-to.integration.js index e8186bbce702..56ad138ced3b 100644 --- a/packages/cli/test/integration/generators/relation.belongs-to.integration.js +++ b/packages/cli/test/integration/generators/relation.belongs-to.integration.js @@ -22,7 +22,9 @@ const sandbox = new TestSandbox(path.resolve(__dirname, '../.sandbox')); const sourceFileName = 'order.model.ts'; const controllerFileName = 'order-customer.controller.ts'; +const controllerFileNameForSameTableRelation = 'employee.controller.ts'; const repositoryFileName = 'order.repository.ts'; +const repositoryFileNameForSameTableRelation = 'employee.repository.ts'; // speed up tests by avoiding reading docs const options = { sourceModelPrimaryKey: 'id', @@ -180,6 +182,149 @@ describe('lb4 relation', /** @this {Mocha.Suite} */ function () { } }); + context( + 'generates model relation with same table with default foreignKeyName', + () => { + const promptList = [ + { + relationType: 'belongsTo', + sourceModel: 'Employee', + destinationModel: 'Employee', + relationName: 'reportsToEemployee', + }, + ]; + + it('verifies that a preexisting property will be overwritten', async () => { + await sandbox.reset(); + + await testUtils + .executeGenerator(generator) + .inDir(sandbox.path, () => + testUtils.givenLBProject(sandbox.path, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withOptions(options) + .withPrompts(promptList[0]); + + const sourceFilePath = path.join( + sandbox.path, + MODEL_APP_PATH, + 'employee.model.ts', + ); + + assert.file(sourceFilePath); + expectFileToMatchSnapshot(sourceFilePath); + }); + }, + ); + + context( + 'checks if the controller file created for same table relation', + () => { + const promptArray = [ + { + relationType: 'belongsTo', + sourceModel: 'Employee', + destinationModel: 'Employee', + }, + ]; + + promptArray.forEach(function (multiItemPrompt) { + describe('answers ' + JSON.stringify(multiItemPrompt), () => { + suite(multiItemPrompt); + }); + }); + + function suite(multiItemPrompt) { + before(async function runGeneratorWithAnswers() { + await sandbox.reset(); + await testUtils + .executeGenerator(generator) + .inDir(sandbox.path, () => + testUtils.givenLBProject(sandbox.path, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withOptions(options) + .withPrompts(multiItemPrompt); + }); + + it('checks controller content with belongsTo relation with same table', async () => { + const filePath = path.join( + sandbox.path, + CONTROLLER_PATH, + controllerFileNameForSameTableRelation, + ); + assert.file(filePath); + expectFileToMatchSnapshot(filePath); + }); + + it('the new controller file added to index.ts file', async () => { + const indexFilePath = path.join( + sandbox.path, + CONTROLLER_PATH, + 'index.ts', + ); + + expectFileToMatchSnapshot(indexFilePath); + }); + } + }, + ); + + context( + 'checks generated source class repository for same table relation', + () => { + const promptArray = [ + { + relationType: 'belongsTo', + sourceModel: 'Employee', + destinationModel: 'Employee', + }, + ]; + + const sourceClassnames = ['Employee']; + + 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, + }), + ) + .withOptions(options) + .withPrompts(multiItemPrompt); + }); + + it( + 'generates ' + + sourceClassnames[i] + + ' repository file with different inputs', + async () => { + const sourceFilePath = path.join( + sandbox.path, + REPOSITORY_APP_PATH, + repositoryFileNameForSameTableRelation, + ); + + assert.file(sourceFilePath); + expectFileToMatchSnapshot(sourceFilePath); + }, + ); + } + }, + ); + context('checks if the controller file created ', () => { const promptArray = [ {