From 7a9b455a3238e8d58b8033bbebc131e67bfdcd69 Mon Sep 17 00:00:00 2001 From: warisniz02 Date: Fri, 24 Oct 2025 21:48:38 +0500 Subject: [PATCH 1/2] feat: support relation overwrite by passing --force flag Signed-off-by: warisniz02 --- .../relation/base-relation.generator.js | 2 ++ .../relation/belongs-to-relation.generator.js | 4 +++- .../relation/has-many-relation.generator.js | 18 +++++++++++++- .../has-many-through-relation.generator.js | 4 +++- .../relation/has-one-relation.generator.js | 18 +++++++++++++- packages/cli/generators/relation/index.js | 19 ++++++++++++++- .../references-many-relation.generator.js | 4 +++- .../generators/relation/utils.generator.js | 24 +++++++++++-------- 8 files changed, 77 insertions(+), 16 deletions(-) diff --git a/packages/cli/generators/relation/base-relation.generator.js b/packages/cli/generators/relation/base-relation.generator.js index 3a9ba0a0bf36..12dbacb27431 100644 --- a/packages/cli/generators/relation/base-relation.generator.js +++ b/packages/cli/generators/relation/base-relation.generator.js @@ -99,6 +99,8 @@ module.exports = class BaseRelationGenerator extends ArtifactGenerator { type: this._getRepositoryRelationPropertyType(), }; + if (classDeclaration.getProperty(property.name)) return; + // already checked the existence of property before relationUtils.addProperty(classDeclaration, property); } diff --git a/packages/cli/generators/relation/belongs-to-relation.generator.js b/packages/cli/generators/relation/belongs-to-relation.generator.js index f5a1d46e180d..befb393bb719 100644 --- a/packages/cli/generators/relation/belongs-to-relation.generator.js +++ b/packages/cli/generators/relation/belongs-to-relation.generator.js @@ -95,7 +95,9 @@ module.exports = class BelongsToRelationGenerator extends ( ); const sourceClass = relationUtils.getClassObj(sourceFile, sourceModel); // this checks if the foreign key already exists, so the 2nd param should be foreignKeyName - relationUtils.doesRelationExist(sourceClass, foreignKeyName); + relationUtils.doesRelationExist(sourceClass, foreignKeyName, { + force: this.options.force, + }); const modelProperty = this.getBelongsTo( targetModel, diff --git a/packages/cli/generators/relation/has-many-relation.generator.js b/packages/cli/generators/relation/has-many-relation.generator.js index 4d56c005f0c2..8e71f1111277 100644 --- a/packages/cli/generators/relation/has-many-relation.generator.js +++ b/packages/cli/generators/relation/has-many-relation.generator.js @@ -99,7 +99,9 @@ module.exports = class HasManyRelationGenerator extends BaseRelationGenerator { sourceModel, ); const sourceClass = relationUtils.getClassObj(sourceFile, sourceModel); - relationUtils.doesRelationExist(sourceClass, relationName); + relationUtils.doesRelationExist(sourceClass, relationName, { + force: this.options.force, + }); modelProperty = this.getHasMany( targetModel, @@ -121,6 +123,20 @@ module.exports = class HasManyRelationGenerator extends BaseRelationGenerator { ); const targetClass = relationUtils.getClassObj(targetFile, targetModel); + if (isForeignKeyExist) { + const existingFK = targetClass.getProperty(foreignKeyName); + if ( + existingFK && + !relationUtils.isValidPropertyType(targetClass, foreignKeyName, fktype) + ) { + existingFK.remove(); + const newFK = relationUtils.addForeignKey(foreignKeyName, fktype); + relationUtils.addProperty(targetClass, newFK); + targetClass.formatText(); + await targetFile.save(); + } + } + if (isForeignKeyExist) { if ( !relationUtils.isValidPropertyType(targetClass, foreignKeyName, fktype) diff --git a/packages/cli/generators/relation/has-many-through-relation.generator.js b/packages/cli/generators/relation/has-many-through-relation.generator.js index a7cb949595cd..c7d8d40c56e9 100644 --- a/packages/cli/generators/relation/has-many-through-relation.generator.js +++ b/packages/cli/generators/relation/has-many-through-relation.generator.js @@ -133,7 +133,9 @@ module.exports = class HasManyThroughRelationGenerator extends ( sourceModel, ); const sourceClass = relationUtils.getClassObj(sourceFile, sourceModel); - relationUtils.doesRelationExist(sourceClass, relationName); + relationUtils.doesRelationExist(sourceClass, relationName, { + force: this.options.force, + }); // add the relation to the source model const isDefaultSourceKey = sourceKey === dftSourceKey; const isDefaultTargetKey = targetKey === dftTargetKey; diff --git a/packages/cli/generators/relation/has-one-relation.generator.js b/packages/cli/generators/relation/has-one-relation.generator.js index 2c4585ced1c0..8fd8b0cbc1fc 100644 --- a/packages/cli/generators/relation/has-one-relation.generator.js +++ b/packages/cli/generators/relation/has-one-relation.generator.js @@ -96,7 +96,9 @@ module.exports = class HasOneRelationGenerator extends BaseRelationGenerator { sourceModel, ); const sourceClass = relationUtils.getClassObj(sourceFile, sourceModel); - relationUtils.doesRelationExist(sourceClass, relationName); + relationUtils.doesRelationExist(sourceClass, relationName, { + force: this.options.force, + }); modelProperty = this.getHasOne( targetModel, @@ -122,6 +124,20 @@ module.exports = class HasOneRelationGenerator extends BaseRelationGenerator { ); const targetClass = relationUtils.getClassObj(targetFile, targetModel); + if (isForeignKeyExist) { + const existingFK = targetClass.getProperty(foreignKeyName); + if ( + existingFK && + !relationUtils.isValidPropertyType(targetClass, foreignKeyName, fktype) + ) { + existingFK.remove(); + const newFK = relationUtils.addForeignKey(foreignKeyName, fktype); + relationUtils.addProperty(targetClass, newFK); + targetClass.formatText(); + await targetFile.save(); + } + } + if (isForeignKeyExist) { if ( !relationUtils.isValidPropertyType(targetClass, foreignKeyName, fktype) diff --git a/packages/cli/generators/relation/index.js b/packages/cli/generators/relation/index.js index 0c7cb5703e50..c37c1b72efa1 100644 --- a/packages/cli/generators/relation/index.js +++ b/packages/cli/generators/relation/index.js @@ -603,7 +603,11 @@ module.exports = class RelationGenerator extends ArtifactGenerator { this.artifactInfo.relationType === 'referencesMany') ) { try { - relationUtils.doesRelationExist(cl, this.artifactInfo.foreignKeyName); + relationUtils.doesRelationExist( + cl, + this.artifactInfo.foreignKeyName, + {force: this.options.force}, + ); } catch (err) { /* istanbul ignore next */ this.exit(err); @@ -682,6 +686,19 @@ module.exports = class RelationGenerator extends ArtifactGenerator { ? PROMPT_MESSAGE_RELATION_NAME : PROMPT_MESSAGE_PROPERTY_NAME; + if (!this.options.relationName) + this.artifactInfo.relationName = this.artifactInfo.defaultRelationName; + if ( + (this.artifactInfo.relationType === 'hasMany' || + this.artifactInfo.relationType === 'hasManyThrough') && + this.options.relationName && + !this.options.relationName.endsWith('s') + ) { + this.artifactInfo.relationName = utils.pluralize( + this.artifactInfo.relationName, + ); + } + return this.prompt([ { type: 'string', diff --git a/packages/cli/generators/relation/references-many-relation.generator.js b/packages/cli/generators/relation/references-many-relation.generator.js index dc55fe69abef..8a91ca37084b 100644 --- a/packages/cli/generators/relation/references-many-relation.generator.js +++ b/packages/cli/generators/relation/references-many-relation.generator.js @@ -41,7 +41,9 @@ module.exports = class ReferencesManyRelationGenerator extends ( ); const sourceClass = relationUtils.getClassObj(sourceFile, sourceModel); // this checks if the foreign key already exists, so the 2nd param should be foreignKeyName - relationUtils.doesRelationExist(sourceClass, foreignKeyName); + relationUtils.doesRelationExist(sourceClass, foreignKeyName, { + force: this.options.force, + }); const modelProperty = this.getReferencesMany( targetModel, diff --git a/packages/cli/generators/relation/utils.generator.js b/packages/cli/generators/relation/utils.generator.js index 7fc6cc6efde9..bc35282e487b 100644 --- a/packages/cli/generators/relation/utils.generator.js +++ b/packages/cli/generators/relation/utils.generator.js @@ -114,22 +114,26 @@ exports.doesPropertyExist = function (classObj, propertyName) { .includes(propertyName); }; -exports.doesRelationExist = function (classObj, propertyName) { +exports.doesRelationExist = function (classObj, propertyName, options = {}) { + const force = options.force; if (this.doesPropertyExist(classObj, propertyName)) { // If the property is decorated by `@property()`, // turn it to be a relational property decorated by `@belongsTo()` const decorators = classObj.getProperty(propertyName).getDecorators(); const hasPropertyDecorator = decorators.length > 0 && decorators[0].getName() === 'property'; - // If it's already decorated by a relational decorator, - // throw error - if (!hasPropertyDecorator) { - throw new Error( - 'relational property ' + - propertyName + - ' already exist in the model ' + - classObj.getName(), - ); + if (!force) { + // If it's already decorated by a relational decorator, + // throw error + if (!hasPropertyDecorator) { + throw new Error( + 'relational property ' + + propertyName + + ' already exist in the model ' + + classObj.getName() + + ' Use --force to overwrite it', + ); + } } this.deleteProperty(classObj.getProperty(propertyName)); From d46cc29fa1fec8745b10b7a18b6ff0fc60643e37 Mon Sep 17 00:00:00 2001 From: warisniz02 Date: Sat, 30 May 2026 15:21:42 +0500 Subject: [PATCH 2/2] feat: add relation overwrite tests for --force flag Signed-off-by: warisniz02 --- .../generators/relation.integration.js | 443 ++++++++++++++++++ 1 file changed, 443 insertions(+) diff --git a/packages/cli/test/integration/generators/relation.integration.js b/packages/cli/test/integration/generators/relation.integration.js index 08b6e42c86e6..dbc6c9bc4a56 100644 --- a/packages/cli/test/integration/generators/relation.integration.js +++ b/packages/cli/test/integration/generators/relation.integration.js @@ -306,3 +306,446 @@ describe('lb4 relation', /** @this {Mocha.Suite} */ function () { }); }); }); + +describe('lb4 relation overwrite by passing --force flag', /** @this {Mocha.Suite} */ function () { + this.timeout(30000); + + context('Execute relation when relation already exists', () => { + it('rejects when relation already exists and no --force flag is provided in belongsTo relation', async () => { + await sandbox.reset(); + const projectPath = sandbox.path; + + await testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'belongsTo', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patients', + ]); + process.chdir(projectPath); + + return expect( + testUtils + .executeGenerator(generator) + .withArguments([ + '--relationType', + 'belongsTo', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patients', + ]), + ).to.be.rejectedWith( + /relational property .* already exist in the model .* Use --force to overwrite it/i, + ); + }); + + it('allows overwriting when --force flag is provided in belongsTo relation', async () => { + await sandbox.reset(); + const projectPath = sandbox.path; + console.log('1. projectPath:', projectPath); + + await testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => { + console.log('2. dir inside callback:', dir); + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }); + }) + .withArguments([ + '--relationType', + 'belongsTo', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patients', + ]); + const fs = require('fs'); + console.log('3. files after first run:', fs.readdirSync(projectPath)); + + return expect( + testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'belongsTo', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patients', + '--force', + ]), + ).to.not.be.rejected; + }); + + it('rejects when relation already exists and no --force flag is provided in hasMany relation', async () => { + await sandbox.reset(); + const projectPath = sandbox.path; + + await testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'hasMany', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patients', + ]); + + return expect( + testUtils + .executeGenerator(generator) + .inDir(sandbox.path, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'hasMany', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patients', + ]), + ).to.be.rejectedWith( + /relational property .* already exist in the model .* Use --force to overwrite it/i, + ); + }); + + it('allows overwriting when --force flag is provided in hasMany relation', async () => { + await sandbox.reset(); + const projectPath = sandbox.path; + + await testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'hasMany', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patients', + ]); + + return expect( + testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'hasMany', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patients', + '--force', + ]), + ).to.not.be.rejected; + }); + + it('rejects when relation already exists and no --force flag is provided in hasManyThrough relation', async () => { + await sandbox.reset(); + const projectPath = sandbox.path; + + await testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'hasManyThrough', + '--sourceModel', + 'Customer', + '--destinationModel', + 'Order', + '--throughModel', + 'Address', + '--relationName', + 'orders', + ]); + + return expect( + testUtils + .executeGenerator(generator) + .inDir(projectPath) + .withArguments([ + '--relationType', + 'hasManyThrough', + '--sourceModel', + 'Customer', + '--destinationModel', + 'Order', + '--throughModel', + 'Address', + '--relationName', + 'orders', + ]), + ).to.be.rejectedWith( + /relational property .* already exist in the model .* Use --force to overwrite it/i, + ); + }); + + it('allows overwriting when --force flag is provided in hasManyThrough relation', async () => { + await sandbox.reset(); + const projectPath = sandbox.path; + + await testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'hasManyThrough', + '--sourceModel', + 'Customer', + '--destinationModel', + 'Order', + '--throughModel', + 'Address', + '--relationName', + 'orders', + ]); + + return expect( + testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'hasManyThrough', + '--sourceModel', + 'Customer', + '--destinationModel', + 'Order', + '--throughModel', + 'Address', + '--relationName', + 'orders', + '--force', + ]), + ).to.not.be.rejected; + }); + + it('rejects when relation already exists and no --force flag is provided in hasOne relation', async () => { + await sandbox.reset(); + const projectPath = sandbox.path; + + await testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'hasOne', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patient', + ]); + + return expect( + testUtils + .executeGenerator(generator) + .inDir(projectPath) + .withArguments([ + '--relationType', + 'hasOne', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patient', + ]), + ).to.be.rejectedWith( + /relational property .* already exist in the model .* Use --force to overwrite it/i, + ); + }); + + it('allows overwriting when --force flag is provided in hasOne relation', async () => { + await sandbox.reset(); + const projectPath = sandbox.path; + + await testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'hasOne', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patient', + ]); + + return expect( + testUtils + .executeGenerator(generator) + .inDir(projectPath) + .withArguments([ + '--relationType', + 'hasOne', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patient', + '--force', + ]), + ).to.not.be.rejected; + }); + + it('rejects when relation already exists and no --force flag is provided in referencesMany relation', async () => { + await sandbox.reset(); + const projectPath = sandbox.path; + + await testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'referencesMany', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patientIds', + ]); + + return expect( + testUtils + .executeGenerator(generator) + .inDir(projectPath) + .withArguments([ + '--relationType', + 'referencesMany', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patientIds', + ]), + ).to.be.rejectedWith( + /relational property .* already exist in the model .* Use --force to overwrite it/i, + ); + }); + + it('allows overwriting when --force flag is provided in referencesMany relation', async () => { + await sandbox.reset(); + const projectPath = sandbox.path; + + await testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'referencesMany', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patientIds', + ]); + + return expect( + testUtils + .executeGenerator(generator) + .inDir(projectPath, dir => + testUtils.givenLBProject(dir, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments([ + '--relationType', + 'referencesMany', + '--sourceModel', + 'Doctor', + '--destinationModel', + 'Patient', + '--relationName', + 'patientIds', + '--force', + ]), + ).to.not.be.rejected; + }); + }); +});