From b36f8e8c21e56e30f5c3a43ee84378b6bd94a078 Mon Sep 17 00:00:00 2001 From: Muhammad Aaqil Date: Mon, 7 Apr 2025 08:39:17 +0500 Subject: [PATCH] feat: generate relations while discovering models Signed-off-by: Muhammad Aaqil --- packages/cli/generators/discover/index.js | 80 ++++- packages/cli/generators/repository/index.js | 32 ++ .../discover.integration.snapshots.js | 294 ++++++++++++++++-- packages/cli/test/fixtures/discover/index.js | 10 + .../fixtures/discover/mem.datasource.ts.txt | 22 ++ .../generators/discover.integration.js | 50 ++- 6 files changed, 440 insertions(+), 48 deletions(-) create mode 100644 packages/cli/test/fixtures/discover/mem.datasource.ts.txt diff --git a/packages/cli/generators/discover/index.js b/packages/cli/generators/discover/index.js index 2f0b19492af1..998a0622b91d 100644 --- a/packages/cli/generators/discover/index.js +++ b/packages/cli/generators/discover/index.js @@ -363,6 +363,12 @@ module.exports = class DiscoveryGenerator extends ArtifactGenerator { this.artifactInfo.indexesToBeUpdated = this.artifactInfo.indexesToBeUpdated || []; + const relations = []; + const repositoryConfigs = { + datasource: '', + repositories: new Set(), + repositoryBaseClass: 'DefaultCrudRepository', + }; // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < this.artifactInfo.modelDefinitions.length; i++) { const modelDefinition = this.artifactInfo.modelDefinitions[i]; @@ -391,23 +397,18 @@ module.exports = class DiscoveryGenerator extends ArtifactGenerator { ); // If targetModel is not in discovered models, skip creating relation if (targetModel) { - Object.assign(templateData.properties[relation.foreignKey], { - relation, - }); - if (!relationImports.includes(relation.type)) { - relationImports.push(relation.type); - } - relationDestinationImports.push(relation.model); - - foreignKeys[relationName] = {}; - Object.assign(foreignKeys[relationName], { - name: relationName, - entity: relation.model, - entityKey: Object.entries(targetModel.properties).find( - x => x?.[1].id === 1, - )?.[0], - foreignKey: relation.foreignKey, - }); + const configs = {}; + configs['sourceModel'] = templateData.name; + configs['destinationModel'] = targetModel.name; + configs['foreignKeyName'] = relation.foreignKey; + configs['relationType'] = relation.type; + configs['registerInclusionResolver'] = true; + configs['yes'] = true; + relations.push(configs); + repositoryConfigs['datasource'] = + this.options.datasource || this.options.dataSource; + repositoryConfigs.repositories.add(templateData.name); + repositoryConfigs.repositories.add(targetModel.name); } } // remove model import if the model relation is with itself @@ -462,6 +463,9 @@ module.exports = class DiscoveryGenerator extends ArtifactGenerator { // This part at the end is just for the ArtifactGenerator // end message to output something nice, before it was "Discover undefined was created in src/models/" this.artifactInfo.type = 'Models'; + this.artifactInfo.relationConfigs = relations; + repositoryConfigs['relations'] = JSON.stringify(relations); + this.artifactInfo.repositoryConfigs = repositoryConfigs; this.artifactInfo.name = this.artifactInfo.modelDefinitions .map(d => d.name) .join(','); @@ -469,5 +473,47 @@ module.exports = class DiscoveryGenerator extends ArtifactGenerator { async end() { await super.end(); + await this._generateRepositories(); + } + + async _generateRepositories() { + if ( + !this.artifactInfo.repositoryConfigs || + !this.artifactInfo.repositoryConfigs.repositories || + this.artifactInfo.repositoryConfigs.repositories.size === 0 + ) { + debug( + 'No repository configurations found, skipping repository generation', + ); + return; + } + const {repositories, datasource, repositoryBaseClass, relations} = + this.artifactInfo.repositoryConfigs; + // Convert Set to Array and iterate + const modelList = Array.from(repositories); + for (let index = 0; index < modelList.length; index++) { + const model = modelList[index]; + const repoGenOptions = { + name: model, + model, + datasource, + repositoryBaseClass, + yes: true, + skipInstall: true, + skipCache: true, + }; + if (index === modelList.length - 1) { + repoGenOptions.relations = relations; + } + // Use composeWith to invoke the repository generator + const repoGen = require('../repository'); + this.composeWith( + { + Generator: repoGen, + path: require.resolve('../repository'), + }, + repoGenOptions, + ); + } } }; diff --git a/packages/cli/generators/repository/index.js b/packages/cli/generators/repository/index.js index 6a0619f59021..76eb0554b95b 100644 --- a/packages/cli/generators/repository/index.js +++ b/packages/cli/generators/repository/index.js @@ -571,5 +571,37 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { ); this.artifactInfo.name = `${this.artifactInfo.className}Repository`; await super.end(); + await this._generateRelations(); + } + + async _generateRelations() { + if (!this.artifactInfo.relations) { + debug('No relation configurations found, skipping relation generation'); + return; + } + this.artifactInfo.relations = JSON.parse(this.artifactInfo.relations); + if (!this.artifactInfo.relations.length) { + debug('No relation configurations found, skipping relation generation'); + return; + } + this.artifactInfo.relations.forEach(relation => { + const repoGen = require('../relation'); + this.composeWith( + { + Generator: repoGen, + path: require.resolve('../relation'), + }, + { + sourceModel: relation.sourceModel, + destinationModel: relation.destinationModel, + foreignKeyName: relation.foreignKeyName, + relationType: relation.relationType, + registerInclusionResolver: relation.registerInclusionResolver, + yes: true, + skipInstall: true, + skipCache: true, + }, + ); + }); } }; diff --git a/packages/cli/snapshots/integration/generators/discover.integration.snapshots.js b/packages/cli/snapshots/integration/generators/discover.integration.snapshots.js index c368ee5c4a5a..9460214305f0 100644 --- a/packages/cli/snapshots/integration/generators/discover.integration.snapshots.js +++ b/packages/cli/snapshots/integration/generators/discover.integration.snapshots.js @@ -58,15 +58,137 @@ export type ViewWithRelations = View & ViewRelations; exports[`lb4 discover integration model discovery discovers models with --relations 1`] = ` -import {Entity, model, property, belongsTo} from '@loopback/repository'; +import { + repository, +} from '@loopback/repository'; +import { + param, + get, + getModelSchemaRef, +} from '@loopback/rest'; +import { + Appointment, + Doctor, +} from '../models'; +import {AppointmentRepository} from '../repositories'; + +export class AppointmentDoctorController { + constructor( + @repository(AppointmentRepository) + public appointmentRepository: AppointmentRepository, + ) { } + + @get('/appointments/{id}/doctor', { + responses: { + '200': { + description: 'Doctor belonging to Appointment', + content: { + 'application/json': { + schema: getModelSchemaRef(Doctor), + }, + }, + }, + }, + }) + async getDoctor( + @param.path.number('id') id: typeof Appointment.prototype.id, + ): Promise { + return this.appointmentRepository.doctor(id); + } +} + +`; + + +exports[`lb4 discover integration model discovery discovers models with --relations 2`] = ` +import { + repository, +} from '@loopback/repository'; +import { + param, + get, + getModelSchemaRef, +} from '@loopback/rest'; +import { + Appointment, + Patient, +} from '../models'; +import {AppointmentRepository} from '../repositories'; + +export class AppointmentPatientController { + constructor( + @repository(AppointmentRepository) + public appointmentRepository: AppointmentRepository, + ) { } + + @get('/appointments/{id}/patient', { + responses: { + '200': { + description: 'Patient belonging to Appointment', + content: { + 'application/json': { + schema: getModelSchemaRef(Patient), + }, + }, + }, + }, + }) + async getPatient( + @param.path.number('id') id: typeof Appointment.prototype.id, + ): Promise { + return this.appointmentRepository.patient(id); + } +} + +`; -@model({ - settings: { - foreignKeys: { - doctorRel: {name: 'doctorRel', entity: 'Doctor', entityKey: 'id', foreignKey: 'reportsTo'} - } + +exports[`lb4 discover integration model discovery discovers models with --relations 3`] = ` +import { + repository, +} from '@loopback/repository'; +import { + param, + get, + getModelSchemaRef, +} from '@loopback/rest'; +import { + Doctor, +} from '../models'; +import {DoctorRepository} from '../repositories'; + +export class DoctorDoctorController { + constructor( + @repository(DoctorRepository) + public doctorRepository: DoctorRepository, + ) { } + + @get('/doctors/{id}/doctor', { + responses: { + '200': { + description: 'Doctor belonging to Doctor', + content: { + 'application/json': { + schema: getModelSchemaRef(Doctor), + }, + }, + }, + }, + }) + async getDoctor( + @param.path.number('id') id: typeof Doctor.prototype.id, + ): Promise { + return this.doctorRepository.reportsTo(id); } -}) +} + +`; + + +exports[`lb4 discover integration model discovery discovers models with --relations 4`] = ` +import {Entity, model, property, belongsTo} from '@loopback/repository'; + +@model() export class Doctor extends Entity { @property({ type: 'number', @@ -82,8 +204,7 @@ export class Doctor extends Entity { name?: string; @belongsTo(() => Doctor) - reportsTo?: number; - + reportsTo: number; // Define well-known properties here // Indexer property to allow additional data @@ -162,22 +283,139 @@ export type TestWithRelations = Test & TestRelations; exports[`lb4 discover integration model discovery generate relations with --relations 1`] = ` -import {Entity, model, property, belongsTo} from '@loopback/repository'; -import {Doctor,Patient} from '.'; - -@model({ - settings: { - foreignKeys: { - doctorIdRel: {name: 'doctorIdRel', entity: 'Doctor', entityKey: 'id', foreignKey: 'doctorId'}, - patientIdRel: { - name: 'patientIdRel', - entity: 'Patient', - entityKey: 'pid', - foreignKey: 'patientId' - } - } +import { + repository, +} from '@loopback/repository'; +import { + param, + get, + getModelSchemaRef, +} from '@loopback/rest'; +import { + Appointment, + Doctor, +} from '../models'; +import {AppointmentRepository} from '../repositories'; + +export class AppointmentDoctorController { + constructor( + @repository(AppointmentRepository) + public appointmentRepository: AppointmentRepository, + ) { } + + @get('/appointments/{id}/doctor', { + responses: { + '200': { + description: 'Doctor belonging to Appointment', + content: { + 'application/json': { + schema: getModelSchemaRef(Doctor), + }, + }, + }, + }, + }) + async getDoctor( + @param.path.number('id') id: typeof Appointment.prototype.id, + ): Promise { + return this.appointmentRepository.doctor(id); } -}) +} + +`; + + +exports[`lb4 discover integration model discovery generate relations with --relations 2`] = ` +import { + repository, +} from '@loopback/repository'; +import { + param, + get, + getModelSchemaRef, +} from '@loopback/rest'; +import { + Appointment, + Patient, +} from '../models'; +import {AppointmentRepository} from '../repositories'; + +export class AppointmentPatientController { + constructor( + @repository(AppointmentRepository) + public appointmentRepository: AppointmentRepository, + ) { } + + @get('/appointments/{id}/patient', { + responses: { + '200': { + description: 'Patient belonging to Appointment', + content: { + 'application/json': { + schema: getModelSchemaRef(Patient), + }, + }, + }, + }, + }) + async getPatient( + @param.path.number('id') id: typeof Appointment.prototype.id, + ): Promise { + return this.appointmentRepository.patient(id); + } +} + +`; + + +exports[`lb4 discover integration model discovery generate relations with --relations 3`] = ` +import { + repository, +} from '@loopback/repository'; +import { + param, + get, + getModelSchemaRef, +} from '@loopback/rest'; +import { + Doctor, +} from '../models'; +import {DoctorRepository} from '../repositories'; + +export class DoctorDoctorController { + constructor( + @repository(DoctorRepository) + public doctorRepository: DoctorRepository, + ) { } + + @get('/doctors/{id}/doctor', { + responses: { + '200': { + description: 'Doctor belonging to Doctor', + content: { + 'application/json': { + schema: getModelSchemaRef(Doctor), + }, + }, + }, + }, + }) + async getDoctor( + @param.path.number('id') id: typeof Doctor.prototype.id, + ): Promise { + return this.doctorRepository.reportsTo(id); + } +} + +`; + + +exports[`lb4 discover integration model discovery generate relations with --relations 4`] = ` +import {Entity, model, property, belongsTo} from '@loopback/repository'; +import {Doctor} from './doctor.model'; +import {Patient} from './patient.model'; + +@model() export class Appointment extends Entity { @property({ type: 'number', @@ -188,13 +426,11 @@ export class Appointment extends Entity { mysql: {columnName: 'id', dataType: 'int', dataLength: null, dataPrecision: 10, dataScale: 0, nullable: 'N', generated: 1}, }) id?: number; - - @belongsTo(() => Patient) - patientId?: number; - @belongsTo(() => Doctor) - doctorId?: number; + doctorId: number; + @belongsTo(() => Patient) + patientId: number; // Define well-known properties here // Indexer property to allow additional data diff --git a/packages/cli/test/fixtures/discover/index.js b/packages/cli/test/fixtures/discover/index.js index 9135b633bff7..9faf5c09c5a6 100644 --- a/packages/cli/test/fixtures/discover/index.js +++ b/packages/cli/test/fixtures/discover/index.js @@ -6,4 +6,14 @@ exports.SANDBOX_FILES = [ file: 'mem.datasource.js', content: fs.readFileSync(require.resolve('./mem.datasource.js.txt')), }, + { + path: 'src/datasources', + file: 'mem.datasource.ts', + content: fs.readFileSync(require.resolve('./mem.datasource.ts.txt')), + }, + { + path: 'src/datasources', + file: 'index.ts', + content: `export * from './mem.datasource';\n`, + }, ]; diff --git a/packages/cli/test/fixtures/discover/mem.datasource.ts.txt b/packages/cli/test/fixtures/discover/mem.datasource.ts.txt new file mode 100644 index 000000000000..fd0ea3bee7b3 --- /dev/null +++ b/packages/cli/test/fixtures/discover/mem.datasource.ts.txt @@ -0,0 +1,22 @@ +import {lifeCycleObserver, LifeCycleObserver} from '@loopback/core'; +import {juggler} from '@loopback/repository'; + +const config = { + name: 'mem', + connector: 'memory', +}; + +@lifeCycleObserver('datasource') +export class MemDataSource extends juggler.DataSource + implements LifeCycleObserver { + static dataSourceName = 'mem'; + static readonly defaultConfig = config; + + constructor( + dsConfig: object = config, + ) { + super(dsConfig); + } +} + +export default MemDataSource; \ No newline at end of file diff --git a/packages/cli/test/integration/generators/discover.integration.js b/packages/cli/test/integration/generators/discover.integration.js index 3fd9cf9c5cbf..0c4817ad6fd3 100644 --- a/packages/cli/test/integration/generators/discover.integration.js +++ b/packages/cli/test/integration/generators/discover.integration.js @@ -97,6 +97,31 @@ const appointmentModel = path.join( 'src/models/appointment.model.ts', ); const doctorModel = path.join(sandbox.path, 'src/models/doctor.model.ts'); +const doctorRepository = path.join( + sandbox.path, + 'src/repositories/doctor.repository.ts', +); +const appointmentRepository = path.join( + sandbox.path, + 'src/repositories/appointment.repository.ts', +); +const patientRepository = path.join( + sandbox.path, + 'src/repositories/patient.repository.ts', +); + +const doctorDoctorController = path.join( + sandbox.path, + 'src/controllers/doctor-doctor.controller.ts', +); +const appointmentDoctorController = path.join( + sandbox.path, + 'src/controllers/appointment-doctor.controller.ts', +); +const appointmentPatientController = path.join( + sandbox.path, + 'src/controllers/appointment-patient.controller.ts', +); const defaultExpectedIndexFile = path.join(sandbox.path, 'src/models/index.ts'); const movedExpectedTestModel = path.join(sandbox.path, 'src/test.model.ts'); @@ -111,6 +136,7 @@ describe('lb4 discover integration', () => { beforeEach('reset sandbox', async () => { await sandbox.reset(); await sandbox.mkdir('dist/datasources'); + await sandbox.mkdir('src/datasources'); }); it('generates all models without prompts using --all --dataSource', /** @this {Mocha.Context} */ async function () { @@ -212,7 +238,8 @@ describe('lb4 discover integration', () => { assert.file(defaultExpectedTestModel); expectFileToMatchSnapshot(defaultExpectedTestModel); }); - it('generate relations with --relations', async () => { + it('generate relations with --relations', /** @this {Mocha.Context} */ async function () { + this.timeout(20000); await testUtils .executeGenerator(generator) .inDir(sandbox.path, () => @@ -221,10 +248,20 @@ describe('lb4 discover integration', () => { }), ) .withOptions(relationsSetTrue); + assert.file(appointmentRepository); + assert.file(patientRepository); + assert.file(doctorRepository); + assert.file(appointmentDoctorController); + assert.file(appointmentPatientController); + assert.file(doctorDoctorController); + expectFileToMatchSnapshot(appointmentDoctorController); + expectFileToMatchSnapshot(appointmentPatientController); + expectFileToMatchSnapshot(doctorDoctorController); assert.file(appointmentModel); expectFileToMatchSnapshot(appointmentModel); }); - it('discovers models with --relations', async () => { + it('discovers models with --relations', /** @this {Mocha.Context} */ async function () { + this.timeout(20000); await testUtils .executeGenerator(generator) .inDir(sandbox.path, () => @@ -233,6 +270,15 @@ describe('lb4 discover integration', () => { }), ) .withOptions(relationsSetTrue); + assert.file(appointmentRepository); + assert.file(patientRepository); + assert.file(doctorRepository); + assert.file(appointmentDoctorController); + assert.file(appointmentPatientController); + assert.file(doctorDoctorController); + expectFileToMatchSnapshot(appointmentDoctorController); + expectFileToMatchSnapshot(appointmentPatientController); + expectFileToMatchSnapshot(doctorDoctorController); assert.file(doctorModel); expectFileToMatchSnapshot(doctorModel); });