From dcf9a5e5edd60d5f6dc121b0165b9a4127ad22d2 Mon Sep 17 00:00:00 2001 From: Muhammad Aaqil Date: Sun, 20 Sep 2026 09:09:20 +0500 Subject: [PATCH] feat: support non-primary key based hasManyThrough relation Signed-off-by: Muhammad Aaqil --- docs/site/Relation-generator.md | 34 ++- packages/cli/.yo-rc.json | 21 ++ .../has-many-through-relation.generator.js | 42 +++- packages/cli/generators/relation/index.js | 127 +++++++++++ ...-relation-template-has-many-through.ts.ejs | 63 +++++- ....has-many-through.integration.snapshots.js | 207 ++++++++++++++++++ packages/cli/test/fixtures/relation/index.js | 37 ++++ .../relation/models/customer8.model.ts | 26 +++ .../models/order-custom-ref-key.model.ts | 30 +++ .../fixtures/relation/models/product.model.ts | 26 +++ .../repositories/customer8.repository.ts | 13 ++ .../order-custom-ref-key.repository.ts | 13 ++ .../repositories/product.repository.ts | 13 ++ .../relation.has-many-through.integration.js | 55 ++++- packages/cli/test/test-utils.js | 1 - 15 files changed, 692 insertions(+), 16 deletions(-) create mode 100644 packages/cli/test/fixtures/relation/models/customer8.model.ts create mode 100644 packages/cli/test/fixtures/relation/models/order-custom-ref-key.model.ts create mode 100644 packages/cli/test/fixtures/relation/models/product.model.ts create mode 100644 packages/cli/test/fixtures/relation/repositories/customer8.repository.ts create mode 100644 packages/cli/test/fixtures/relation/repositories/order-custom-ref-key.repository.ts create mode 100644 packages/cli/test/fixtures/relation/repositories/product.repository.ts diff --git a/docs/site/Relation-generator.md b/docs/site/Relation-generator.md index 9d0658e7d442..6334eeffb42f 100644 --- a/docs/site/Relation-generator.md +++ b/docs/site/Relation-generator.md @@ -47,6 +47,12 @@ lb4 relation [options] through model. For HasManyThrough relation only. - `--targetKeyOnThrough`: Foreign key that references the target model on the through model. For HasManyThrough relation only. +- `--customReferenceKeys`: Confrimation if there is any custom reference key to + source or target model. For HasManyThrough relation only. +- `--customSourceModelKey`: Custom key referencing in the source model. For + HasManyThrough relation only. +- `--customTargetModelKey`: Custom key referencing in the target model. For + HasManyThrough relation only. - `-c`, `--config`: JSON file name or value to configure options. - `-y`, `--yes`: Skip all confirmation prompts with default or provided value. - `--format`: Format generated code using `npm run lint:fix`. @@ -113,7 +119,11 @@ lb4 relation --sourceModel= [--sourceModelPrimaryKeyType=] [--destinationModelPrimaryKey=] [--destinationModelPrimaryKeyType=] -[--sourceKeyOnThrough=] [--targetKeyOnThrough] +[--sourceKeyOnThrough=] +[--targetKeyOnThrough] +[--customReferenceKeys=] +[--customSourceModelKey=] +[--customTargetModelKey] [--format] ``` @@ -126,6 +136,15 @@ lb4 relation --sourceModel= - `` - Property on the through model that references the primary key property of the target model. +- `` - Confirm if there is any custom reference key to + source or target model. + +- `` - Property to that references the key in the source + model + +- `` - Property to that references the key in the target + model + ### Interactive Prompts The tool will prompt you for: @@ -165,6 +184,19 @@ The tool will prompt you for: relation only. Default value: `` + `Id` in camelCase, e.g `patientId`. +- Confirmation for any custom reference key(s) (`customReferenceKeys`). Prompts + to confirm if there is any custom reference keys to be defined referenceing + source or target model. For HasManyThrough relation only. The default value is + false + +- Name of custom reference key in source model (`customSourceModelKey`). Prompts + a property name that references non-primary key in source model. Only prompts + if `customReferenceKeys` is true.For HasManyThrough relation only. + +- Name of custom reference key in target model (`customTargetModelKey`). Prompts + a property name that references non-primary key in target model. Only prompts + if `customReferenceKeys` is true.For HasManyThrough relation only. + - Name of the relation (`relationName`). Prompts for the Source property name. Note: Leave blank to use the default. Default values: - plural form of `` for `hasMany` and `hasManyThrough` relations, diff --git a/packages/cli/.yo-rc.json b/packages/cli/.yo-rc.json index 6973e6a7ffd9..1b4ce9947115 100644 --- a/packages/cli/.yo-rc.json +++ b/packages/cli/.yo-rc.json @@ -1457,6 +1457,27 @@ "name": "registerInclusionResolver", "hide": false }, + "customReferenceKeys": { + "type": "String", + "required": false, + "description": "Any Custom Reference Kyes", + "name": "customReferenceKeys", + "hide": false + }, + "customSourceModelKey": { + "type": "String", + "required": false, + "description": "Custom Source model key", + "name": "customSourceModelKey", + "hide": false + }, + "customTargetModelKey": { + "type": "String", + "required": false, + "description": "Custom Destination model", + "name": "customTargetModelKey", + "hide": false + }, "config": { "type": "String", "alias": "c", 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..0e4df69ba998 100644 --- a/packages/cli/generators/relation/has-many-through-relation.generator.js +++ b/packages/cli/generators/relation/has-many-through-relation.generator.js @@ -52,7 +52,7 @@ module.exports = class HasManyThroughRelationGenerator extends ( options.destinationModel, ); this.artifactInfo.targetRepositoryClassName = - this.artifactInfo.targetModelName + 'Repository'; + this.artifactInfo.targetModelClassName + 'Repository'; this.artifactInfo.paramTargetRepository = utils.camelCase( this.artifactInfo.targetModelName + 'Repository', ); @@ -80,7 +80,22 @@ module.exports = class HasManyThroughRelationGenerator extends ( const dest = this.destinationPath( path.join(this.artifactInfo.outDir, this.artifactInfo.outFile), ); - + this.artifactInfo.idPath = 'id'; + this.artifactInfo.customSourceModelKey = options.customSourceModelKey; + if (options.customSourceModelKey) { + this.artifactInfo.idPath = utils.camelCase(options.customSourceModelKey); + const customSourceModelKeyType = relationUtils.getModelPropertyType( + this.artifactInfo.modelDir, + options.sourceModel, + options.customSourceModelKey, + ); + if (customSourceModelKeyType) { + this.artifactInfo.sourceModelPrimaryKeyType = customSourceModelKeyType; + } + } + this.artifactInfo.customTargetModelKey = options.customTargetModelKey; + this.artifactInfo.sourceKeyOnThrough = options.sourceKeyOnThrough; + this.artifactInfo.targetKeyOnThrough = options.targetKeyOnThrough; this.copyTemplatedFiles(source, dest, this.artifactInfo); await relationUtils.addExportController( this, @@ -104,8 +119,13 @@ module.exports = class HasManyThroughRelationGenerator extends ( const targetKey = options.targetKeyOnThrough; const dftSourceKey = options.defaultSourceKeyOnThrough; const dftTargetKey = options.defaultTargetKeyOnThrough; - const sourceKeyType = options.sourceModelPrimaryKeyType; - const targetKeyType = options.destinationModelPrimaryKeyType; + const customSourceModelKey = options.customSourceModelKey; + const customTargetModelKey = options.customTargetModelKey; + const sourceKeyType = + options.customSourceModelKeyType || options.sourceModelPrimaryKeyType; + const targetKeyType = + options.customTargetModelKeyType || + options.destinationModelPrimaryKeyType; // checks if both target and source key exist in through model const project = new relationUtils.AstLoopBackProject(); @@ -145,6 +165,8 @@ module.exports = class HasManyThroughRelationGenerator extends ( sourceKey, isDefaultTargetKey, targetKey, + customSourceModelKey, + customTargetModelKey, ); relationUtils.addProperty(sourceClass, modelProperty); let imports; @@ -200,21 +222,31 @@ module.exports = class HasManyThroughRelationGenerator extends ( sourceKey, isDefaultTargetKey, targetKey, + customSourceModelKey, + customTargetModelKey, ) { let keyFrom = ''; let keyTo = ''; + let customReferenceKeyFrom = ''; + let customReferenceKeyTo = ''; if (!isDefaultSourceKey) { keyFrom = `, keyFrom: '${sourceKey}'`; } if (!isDefaultTargetKey) { keyTo = `, keyTo: '${targetKey}'`; } + if (customSourceModelKey) { + customReferenceKeyFrom = `customReferenceKeyFrom: '${customSourceModelKey}', `; + } + if (customTargetModelKey) { + customReferenceKeyTo = `customReferenceKeyTo: '${customTargetModelKey}', `; + } const relationDecorator = [ { name: 'hasMany', arguments: [ - `() => ${targetClass}, {through: {model: () => ${throughModel}${keyFrom}${keyTo}}}`, + `() => ${targetClass}, {${customReferenceKeyFrom}${customReferenceKeyTo}through: {model: () => ${throughModel}${keyFrom}${keyTo}}}`, ], }, ]; diff --git a/packages/cli/generators/relation/index.js b/packages/cli/generators/relation/index.js index 163c49ecb422..05d86f21e2a7 100644 --- a/packages/cli/generators/relation/index.js +++ b/packages/cli/generators/relation/index.js @@ -138,6 +138,7 @@ module.exports = class RelationGenerator extends ArtifactGenerator { required: false, description: g.f('Relation name'), }); + this.option('defaultRelationName', { type: String, required: false, @@ -151,6 +152,25 @@ module.exports = class RelationGenerator extends ArtifactGenerator { 'Allow queries to include data from related ', ), }); + + this.option('customReferenceKeys', { + type: String, + required: false, + description: g.f('Any Custom Reference Kyes'), + }); + + this.option('customSourceModelKey', { + type: String, + required: false, + description: g.f('Custom Source model key'), + }); + + this.option('customTargetModelKey', { + type: String, + required: false, + description: g.f('Custom Destination model'), + }); + this.artifactInfo = { type: 'relation', rootDir: utils.sourceRootDir, @@ -384,6 +404,113 @@ module.exports = class RelationGenerator extends ArtifactGenerator { } } + // Prompt a user for confirmation if they have custom reference keys + async promptCustomReferenceKeys() { + if (this.shouldExit()) return false; + if (this.options.customReferenceKeys) { + this.artifactInfo.customReferenceKeys = this.options.customReferenceKeys; + if (this.options.customSourceModelKey) { + this.artifactInfo.customSourceModelKey = + this.options.customSourceModelKey; + } + if (this.options.customTargetModelKey) { + this.artifactInfo.customTargetModelKey = + this.options.customTargetModelKey; + } + } + + if (this.artifactInfo.relationType === 'hasManyThrough') { + const props = await this.prompt([ + { + type: 'confirm', + name: 'customReferenceKeys', + message: g.f( + 'Do you have custom reference keys for source and target models?', + ), + when: this.artifactInfo.customReferenceKeys === undefined, + default: false, + }, + ]); + Object.assign(this.artifactInfo, props); + if (this.artifactInfo.customReferenceKeys) { + const answerSource = await this.prompt([ + { + type: 'input', + name: 'customSourceModelKey', + message: g.f('What is the name of reference key in source model?'), + when: this.artifactInfo.customSourceModelKey === undefined, + }, + ]); + if (answerSource.customSourceModelKey) { + this.artifactInfo.customSourceModelKey = + answerSource.customSourceModelKey; + } + const answerTarget = await this.prompt([ + { + type: 'input', + name: 'customTargetModelKey', + message: g.f('What is the name of reference key in target model?'), + when: this.artifactInfo.customTargetModelKey === undefined, + }, + ]); + if (answerTarget.customTargetModelKey) { + this.artifactInfo.customTargetModelKey = + answerTarget.customTargetModelKey; + } + const customSourceModelKeyType = relationUtils.getModelPropertyType( + this.artifactInfo.modelDir, + this.artifactInfo.sourceModel, + this.artifactInfo.customSourceModelKey, + ); + + const customTargetModelKeyType = relationUtils.getModelPropertyType( + this.artifactInfo.modelDir, + this.artifactInfo.destinationModel, + this.artifactInfo.customTargetModelKey, + ); + if (customSourceModelKeyType) { + this.artifactInfo.customSourceModelKeyType = customSourceModelKeyType; + } + let answer = await this.prompt([ + { + type: 'list', + name: 'customSourceModelKeyType', + message: g.f( + 'What is the type of the custom reference key of source model?', + ), + choices: ['number', 'string', 'object'], + when: this.artifactInfo.customSourceModelKeyType === undefined, + default: 'number', + }, + ]); + if (answer.customSourceModelKeyType) { + this.artifactInfo.customSourceModelKeyType = + answer.customSourceModelKeyType; + } + + if (customTargetModelKeyType) { + this.artifactInfo.customTargetModelKeyType = customTargetModelKeyType; + } + answer = await this.prompt([ + { + type: 'list', + name: 'customTargetModelKeyType', + message: g.f( + 'What is the type of the custom reference key of source model?', + ), + choices: ['number', 'string', 'object'], + when: this.artifactInfo.customTargetModelKeyType === undefined, + default: 'number', + }, + ]); + if (answer.customTargetModelKeyType) { + this.artifactInfo.customTargetModelKeyType = + answer.customTargetModelKeyType; + } + } + } + } + /** * Prompt foreign key if not exist: * 1. From source model get primary key. If primary key does not exist - diff --git a/packages/cli/generators/relation/templates/controller-relation-template-has-many-through.ts.ejs b/packages/cli/generators/relation/templates/controller-relation-template-has-many-through.ts.ejs index e009f8a30e0b..af2d56f1c6e8 100644 --- a/packages/cli/generators/relation/templates/controller-relation-template-has-many-through.ts.ejs +++ b/packages/cli/generators/relation/templates/controller-relation-template-has-many-through.ts.ejs @@ -20,14 +20,30 @@ import {<%if (sourceModelClassName != targetModelClassName) { %> <%= throughModelClassName %>, <%= targetModelClassName %>, } from '../models'; +<%_ if (customSourceModelKey) { _%> +import { +<%_ if (sourceRepositoryClassName !== targetRepositoryClassName) { _%> +<%= targetRepositoryClassName %>, +<%_ } _%> +<%= throughRepositoryClassName %>, +<%= sourceRepositoryClassName %>, +} from '../repositories'; +<%_ } else { _%> import {<%= sourceRepositoryClassName %>} from '../repositories'; +<%_ } _%> export class <%= controllerClassName %> { constructor( @repository(<%= sourceRepositoryClassName %>) protected <%= paramSourceRepository %>: <%= sourceRepositoryClassName %>, +<%_ if (customSourceModelKey) { _%> + <%_ if (sourceRepositoryClassName !== targetRepositoryClassName) { _%> + @repository(<%= targetRepositoryClassName %>) protected <%= paramTargetRepository %>: <%= targetRepositoryClassName %>, + <%_ } _%> + @repository(<%= throughRepositoryClassName %>) protected <%= paramThroughRepository %>: <%= throughRepositoryClassName %>, +<%_ } _%> ) { } - @get('/<%= sourceModelPath %>/{id}/<%= targetModelPath %>', { + @get('/<%= sourceModelPath %>/{<%= idPath %>}/<%= targetModelPath %>', { responses: { '200': { description: 'Array of <%= sourceModelClassName %> has many <%= targetModelClassName %> through <%= throughModelClassName %>', @@ -40,13 +56,20 @@ export class <%= controllerClassName %> { }, }) async find( - @param.path.<%= sourceModelPrimaryKeyType %>('id') id: <%= sourceModelPrimaryKeyType %>, + @param.path.<%= sourceModelPrimaryKeyType %>('<%= idPath %>') <%= idPath %>: <%= sourceModelPrimaryKeyType %>, @param.query.object('filter') filter?: Filter<<%= targetModelClassName %>>, ): Promise<<%= targetModelClassName %>[]> { + <%_ if (customSourceModelKey) { _%> + const keys: any[] = []; + const throughKeys: <%= throughModelClassName %>[] = await this.<%= paramThroughRepository %>.find({where: {<%= sourceKeyOnThrough %>: <%= idPath %>}}); + throughKeys.forEach(throughKey => { keys.push(throughKey.<%= targetKeyOnThrough %>); }); + return this.<%= paramTargetRepository %>.find({...filter, where: { <%= customTargetModelKey %>: { inq: keys } }}); + <%_ } else { _%> return this.<%= paramSourceRepository %>.<%= relationPropertyName %>(id).find(filter); + <%_ } _%> } - @post('/<%= sourceModelPath %>/{id}/<%= targetModelPath %>', { + @post('/<%= sourceModelPath %>/{<%= idPath %>}/<%= targetModelPath %>', { responses: { '200': { description: 'create a <%= targetModelClassName %> model instance', @@ -55,7 +78,7 @@ export class <%= controllerClassName %> { }, }) async create( - @param.path.<%= sourceModelPrimaryKeyType %>('id') id: typeof <%= sourceModelClassName %>.prototype.<%= sourceModelPrimaryKey %>, + @param.path.<%= sourceModelPrimaryKeyType %>('<%= idPath %>') <%= idPath %>: <%= customSourceModelKey ? sourceModelPrimaryKeyType + ',' : 'typeof ' + sourceModelClassName + '.prototype.' + sourceModelPrimaryKey + ',' %> @requestBody({ content: { 'application/json': { @@ -67,10 +90,17 @@ export class <%= controllerClassName %> { }, }) <%= targetModelRequestBody %>: Omit<<%= targetModelClassName %>, '<%= targetModelPrimaryKey %>'>, ): Promise<<%= targetModelClassName %>> { + <%_ if (customSourceModelKey) { _%> + const object = await this.<%= paramTargetRepository %>.create(<%= targetModelRequestBody %>); + const through = {<%= sourceKeyOnThrough %>: <%= idPath %>, <%= targetKeyOnThrough %>: object.<%= customTargetModelKey %>}; + await this.<%= paramThroughRepository %>.create(through); + return object; + <%_ } else { _%> return this.<%= paramSourceRepository %>.<%= relationPropertyName %>(id).create(<%= targetModelRequestBody %>); + <%_ } _%> } - @patch('/<%= sourceModelPath %>/{id}/<%= targetModelPath %>', { + @patch('/<%= sourceModelPath %>/{<%= idPath %>}/<%= targetModelPath %>', { responses: { '200': { description: '<%= sourceModelClassName %>.<%= targetModelClassName %> PATCH success count', @@ -79,7 +109,7 @@ export class <%= controllerClassName %> { }, }) async patch( - @param.path.<%= sourceModelPrimaryKeyType %>('id') id: <%= sourceModelPrimaryKeyType %>, + @param.path.<%= sourceModelPrimaryKeyType %>('<%= idPath %>') <%= idPath %>: <%= sourceModelPrimaryKeyType %>, @requestBody({ content: { 'application/json': { @@ -90,10 +120,17 @@ export class <%= controllerClassName %> { <%= targetModelRequestBody %>: Partial<<%= targetModelClassName %>>, @param.query.object('where', getWhereSchemaFor(<%= targetModelClassName %>)) where?: Where<<%= targetModelClassName %>>, ): Promise { + <%_ if (customSourceModelKey) { _%> + const keys: any[] = []; + const throughKeys: <%= throughModelClassName %>[] = await this.<%= paramThroughRepository %>.find({where: {<%= sourceKeyOnThrough %>: <%= idPath %>}}); + throughKeys.forEach(throughKey => { keys.push(throughKey.<%= targetKeyOnThrough %>); }); + return this.<%= paramTargetRepository %>.updateAll(<%= targetModelRequestBody %>, {...where, <%= customTargetModelKey %>: { inq: keys }}); + <%_ } else { _%> return this.<%= paramSourceRepository %>.<%= relationPropertyName %>(id).patch(<%= targetModelRequestBody %>, where); + <%_ } _%> } - @del('/<%= sourceModelPath %>/{id}/<%= targetModelPath %>', { + @del('/<%= sourceModelPath %>/{<%= idPath %>}/<%= targetModelPath %>', { responses: { '200': { description: '<%= sourceModelClassName %>.<%= targetModelClassName %> DELETE success count', @@ -102,9 +139,19 @@ export class <%= controllerClassName %> { }, }) async delete( - @param.path.<%= sourceModelPrimaryKeyType %>('id') id: <%= sourceModelPrimaryKeyType %>, + @param.path.<%= sourceModelPrimaryKeyType %>('<%= idPath %>') <%= idPath %>: <%= sourceModelPrimaryKeyType %>, @param.query.object('where', getWhereSchemaFor(<%= targetModelClassName %>)) where?: Where<<%= targetModelClassName %>>, ): Promise { + <%_ if (customSourceModelKey) { _%> + const keys: any[] = []; + const throughKeys: <%= throughModelClassName %>[] = await this.<%= paramThroughRepository %>.find({where: {<%= sourceKeyOnThrough %>: <%= idPath %>}}); + throughKeys.forEach(throughKey => { keys.push(throughKey.<%= targetKeyOnThrough %>); }); + return this.<%= paramTargetRepository %>.deleteAll({ + ...where, + <%= customTargetModelKey %>: { inq: keys } + }); + <%_ } else { _%> return this.<%= paramSourceRepository %>.<%= relationPropertyName %>(id).delete(where); + <%_ } _%> } } diff --git a/packages/cli/snapshots/integration/generators/relation.has-many-through.integration.snapshots.js b/packages/cli/snapshots/integration/generators/relation.has-many-through.integration.snapshots.js index 9510bb8414a4..5acdc28043ab 100644 --- a/packages/cli/snapshots/integration/generators/relation.has-many-through.integration.snapshots.js +++ b/packages/cli/snapshots/integration/generators/relation.has-many-through.integration.snapshots.js @@ -552,6 +552,213 @@ export class Appointment extends Entity { `; +exports[`lb4 relation HasManyThrough generates model relation with custom reference keys with --config has correct default foreign keys 1`] = ` +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class OrderCustomRefKey extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + customerCode?: string; + + @property({ + type: 'string', + }) + productSku?: string; + + @property({ + type: 'number', + }) + quantity?: number; + + constructor(data?: Partial) { + super(data); + } +} + +`; + + +exports[`lb4 relation HasManyThrough generates model relation with custom reference keys with --config has correct imports and relation name products 1`] = ` +import { + Count, + CountSchema, + Filter, + repository, + Where, +} from '@loopback/repository'; + import { + del, + get, + getModelSchemaRef, + getWhereSchemaFor, + param, + patch, + post, + requestBody, +} from '@loopback/rest'; +import { +Customer8, +OrderCustomRefKey, +Product, +} from '../models'; +import { +ProductRepository, +OrderCustomRefKeyRepository, +Customer8Repository, +} from '../repositories'; + +export class Customer8ProductController { + constructor( + @repository(Customer8Repository) protected customer8Repository: Customer8Repository, + @repository(ProductRepository) protected productRepository: ProductRepository, + @repository(OrderCustomRefKeyRepository) protected orderCustomRefKeyRepository: OrderCustomRefKeyRepository, + ) { } + + @get('/customer8s/{customerCode}/products', { + responses: { + '200': { + description: 'Array of Customer8 has many Product through OrderCustomRefKey', + content: { + 'application/json': { + schema: {type: 'array', items: getModelSchemaRef(Product)}, + }, + }, + }, + }, + }) + async find( + @param.path.string('customerCode') customerCode: string, + @param.query.object('filter') filter?: Filter, + ): Promise { + const keys: any[] = []; + const throughKeys: OrderCustomRefKey[] = await this.orderCustomRefKeyRepository.find({where: {customerCode: customerCode}}); + throughKeys.forEach(throughKey => { keys.push(throughKey.productSku); }); + return this.productRepository.find({...filter, where: { sku: { inq: keys } }}); + } + + @post('/customer8s/{customerCode}/products', { + responses: { + '200': { + description: 'create a Product model instance', + content: {'application/json': {schema: getModelSchemaRef(Product)}}, + }, + }, + }) + async create( + @param.path.string('customerCode') customerCode: string, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(Product, { + title: 'NewProductInCustomer8', + exclude: ['id'], + }), + }, + }, + }) product: Omit, + ): Promise { + const object = await this.productRepository.create(product); + const through = {customerCode: customerCode, productSku: object.sku}; + await this.orderCustomRefKeyRepository.create(through); + return object; + } + + @patch('/customer8s/{customerCode}/products', { + responses: { + '200': { + description: 'Customer8.Product PATCH success count', + content: {'application/json': {schema: CountSchema}}, + }, + }, + }) + async patch( + @param.path.string('customerCode') customerCode: string, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(Product, {partial: true}), + }, + }, + }) + product: Partial, + @param.query.object('where', getWhereSchemaFor(Product)) where?: Where, + ): Promise { + const keys: any[] = []; + const throughKeys: OrderCustomRefKey[] = await this.orderCustomRefKeyRepository.find({where: {customerCode: customerCode}}); + throughKeys.forEach(throughKey => { keys.push(throughKey.productSku); }); + return this.productRepository.updateAll(product, {...where, sku: { inq: keys }}); + } + + @del('/customer8s/{customerCode}/products', { + responses: { + '200': { + description: 'Customer8.Product DELETE success count', + content: {'application/json': {schema: CountSchema}}, + }, + }, + }) + async delete( + @param.path.string('customerCode') customerCode: string, + @param.query.object('where', getWhereSchemaFor(Product)) where?: Where, + ): Promise { + const keys: any[] = []; + const throughKeys: OrderCustomRefKey[] = await this.orderCustomRefKeyRepository.find({where: {customerCode: customerCode}}); + throughKeys.forEach(throughKey => { keys.push(throughKey.productSku); }); + return this.productRepository.deleteAll({ + ...where, + sku: { inq: keys } + }); + } +} + +`; + + +exports[`lb4 relation HasManyThrough generates model relation with custom reference keys with --config has correct imports and relation name products 2`] = ` +import {Entity, model, property, hasMany} from '@loopback/repository'; +import {Product} from './product.model'; +import {OrderCustomRefKey} from './order-custom-ref-key.model'; + +@model() +export class Customer8 extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + index: {unique: true}, + }) + customerCode?: string; + + @property({ + type: 'string', + }) + name?: string; + + @hasMany(() => Product, {customReferenceKeyFrom: 'customerCode', customReferenceKeyTo: 'sku', through: {model: () => OrderCustomRefKey, keyFrom: 'customerCode', keyTo: 'productSku'}}) + products: Product[]; + + constructor(data?: Partial) { + super(data); + } +} + +`; + + exports[`lb4 relation HasManyThrough generates model relation with custom relation name answers {"relationType":"hasManyThrough","sourceModel":"Doctor","destinationModel":"Patient","throughModel":"Appointment","relationName":"myPatients"} relation name should be myPatients 1`] = ` import {Entity, model, property, hasMany} from '@loopback/repository'; import {Patient} from './patient.model'; diff --git a/packages/cli/test/fixtures/relation/index.js b/packages/cli/test/fixtures/relation/index.js index 720850b57377..9d9ddaf86fea 100644 --- a/packages/cli/test/fixtures/relation/index.js +++ b/packages/cli/test/fixtures/relation/index.js @@ -250,6 +250,8 @@ exports.SANDBOX_FILES = [ SourceEntries.FriendModel, SourceEntries.UserRepository, SourceEntries.FriendRepository, + SourceEntries.PersonModel, + SourceEntries.PersonRepository, ]; exports.SANDBOX_FILES2 = [ @@ -293,6 +295,41 @@ exports.SANDBOX_FILES4 = [ SourceEntries.OrderRepository, ]; +exports.SANDBOX_FILES5 = [ + { + path: MODEL_APP_PATH, + file: 'customer8.model.ts', + content: readSourceFile('./models/customer8.model.ts'), + }, + { + path: REPOSITORY_APP_PATH, + file: 'customer8.repository.ts', + content: readSourceFile('./repositories/customer8.repository.ts'), + }, + { + path: MODEL_APP_PATH, + file: 'product.model.ts', + content: readSourceFile('./models/product.model.ts'), + }, + { + path: REPOSITORY_APP_PATH, + file: 'product.repository.ts', + content: readSourceFile('./repositories/product.repository.ts'), + }, + { + path: MODEL_APP_PATH, + file: 'order-custom-ref-key.model.ts', + content: readSourceFile('./models/order-custom-ref-key.model.ts'), + }, + { + path: REPOSITORY_APP_PATH, + file: 'order-custom-ref-key.repository.ts', + content: readSourceFile( + './repositories/order-custom-ref-key.repository.ts', + ), + }, +]; + function readSourceFile(relativePath) { return fs.readFileSync(require.resolve(relativePath), {encoding: 'utf-8'}); } diff --git a/packages/cli/test/fixtures/relation/models/customer8.model.ts b/packages/cli/test/fixtures/relation/models/customer8.model.ts new file mode 100644 index 000000000000..d41da48e7e5a --- /dev/null +++ b/packages/cli/test/fixtures/relation/models/customer8.model.ts @@ -0,0 +1,26 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Customer8 extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + index: {unique: true}, + }) + customerCode?: string; + + @property({ + type: 'string', + }) + name?: string; + + constructor(data?: Partial) { + super(data); + } +} diff --git a/packages/cli/test/fixtures/relation/models/order-custom-ref-key.model.ts b/packages/cli/test/fixtures/relation/models/order-custom-ref-key.model.ts new file mode 100644 index 000000000000..8fac3843372f --- /dev/null +++ b/packages/cli/test/fixtures/relation/models/order-custom-ref-key.model.ts @@ -0,0 +1,30 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class OrderCustomRefKey extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + customerCode?: string; + + @property({ + type: 'string', + }) + productSku?: string; + + @property({ + type: 'number', + }) + quantity?: number; + + constructor(data?: Partial) { + super(data); + } +} diff --git a/packages/cli/test/fixtures/relation/models/product.model.ts b/packages/cli/test/fixtures/relation/models/product.model.ts new file mode 100644 index 000000000000..16a1a4b83ec9 --- /dev/null +++ b/packages/cli/test/fixtures/relation/models/product.model.ts @@ -0,0 +1,26 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Product extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + index: {unique: true}, + }) + sku?: string; + + @property({ + type: 'string', + }) + name?: string; + + constructor(data?: Partial) { + super(data); + } +} diff --git a/packages/cli/test/fixtures/relation/repositories/customer8.repository.ts b/packages/cli/test/fixtures/relation/repositories/customer8.repository.ts new file mode 100644 index 000000000000..b1082bbaafe7 --- /dev/null +++ b/packages/cli/test/fixtures/relation/repositories/customer8.repository.ts @@ -0,0 +1,13 @@ +import {inject} from '@loopback/core'; +import {DefaultCrudRepository} from '@loopback/repository'; +import {DbDataSource} from '../datasources'; +import {Customer8} from '../models'; + +export class Customer8Repository extends DefaultCrudRepository< + Customer8, + typeof Customer8.prototype.id +> { + constructor(@inject('datasources.db') dataSource: DbDataSource) { + super(Customer8, dataSource); + } +} diff --git a/packages/cli/test/fixtures/relation/repositories/order-custom-ref-key.repository.ts b/packages/cli/test/fixtures/relation/repositories/order-custom-ref-key.repository.ts new file mode 100644 index 000000000000..450657635353 --- /dev/null +++ b/packages/cli/test/fixtures/relation/repositories/order-custom-ref-key.repository.ts @@ -0,0 +1,13 @@ +import {inject} from '@loopback/core'; +import {DefaultCrudRepository} from '@loopback/repository'; +import {DbDataSource} from '../datasources'; +import {OrderCustomRefKey} from '../models'; + +export class OrderCustomRefKeyRepository extends DefaultCrudRepository< + OrderCustomRefKey, + typeof OrderCustomRefKey.prototype.id +> { + constructor(@inject('datasources.db') dataSource: DbDataSource) { + super(OrderCustomRefKey, dataSource); + } +} diff --git a/packages/cli/test/fixtures/relation/repositories/product.repository.ts b/packages/cli/test/fixtures/relation/repositories/product.repository.ts new file mode 100644 index 000000000000..1a54c8ba0f5e --- /dev/null +++ b/packages/cli/test/fixtures/relation/repositories/product.repository.ts @@ -0,0 +1,13 @@ +import {inject} from '@loopback/core'; +import {DefaultCrudRepository} from '@loopback/repository'; +import {DbDataSource} from '../datasources'; +import {Product} from '../models'; + +export class ProductRepository extends DefaultCrudRepository< + Product, + typeof Product.prototype.id +> { + constructor(@inject('datasources.db') dataSource: DbDataSource) { + super(Product, dataSource); + } +} diff --git a/packages/cli/test/integration/generators/relation.has-many-through.integration.js b/packages/cli/test/integration/generators/relation.has-many-through.integration.js index 64f1a3e9e9d4..f6cd787d4140 100644 --- a/packages/cli/test/integration/generators/relation.has-many-through.integration.js +++ b/packages/cli/test/integration/generators/relation.has-many-through.integration.js @@ -11,7 +11,7 @@ const {TestSandbox} = require('@loopback/testlab'); const {expectFileToMatchSnapshot} = require('../../snapshots'); const generator = path.join(__dirname, '../../../generators/relation'); -const {SANDBOX_FILES} = require('../../fixtures/relation'); +const {SANDBOX_FILES, SANDBOX_FILES5} = require('../../fixtures/relation'); const testUtils = require('../../test-utils'); // Test Sandbox @@ -31,6 +31,10 @@ const sourceFileNameForSameTable = 'user.model.ts'; const repositoryFileNameForSameTable = 'user.repository.ts'; const controllerFileNameForSameTable = 'user-user.controller.ts'; +const customReferenceRelationSourceModel = 'customer8.model.ts'; +const customReferenceRelationThroughModel = 'order-custom-ref-key.model.ts'; +const customReferenceRelationController = 'customer-8-product.controller.ts'; + // speed up tests by avoiding reading docs const options = { sourceModelPrimaryKey: 'id', @@ -432,4 +436,53 @@ describe('lb4 relation HasManyThrough', /** @this {Mocha.Suite} */ function () { }); } }); + + context( + 'generates model relation with custom reference keys with --config', + () => { + before(async function runGeneratorWithAnswers() { + await sandbox.reset(); + await testUtils + .executeGenerator(generator) + .inDir(sandbox.path, () => + testUtils.givenLBProject(sandbox.path, { + additionalFiles: SANDBOX_FILES5, + }), + ) + .withArguments([ + '--config', + '{"relationName": "products", "customReferenceKeys": true, "sourceModel": "Customer8", "customSourceModelKey": "customerCode", "destinationModel": "Product", "customTargetModelKey": "sku", "throughModel": "OrderCustomRefKey", "relationType": "hasManyThrough", "sourceKeyOnThrough": "customerCode", "targetKeyOnThrough": "productSku", "registerInclusionResolver": true}', + ]); + }); + + it('has correct imports and relation name products', async () => { + const controllerFilePath = path.join( + sandbox.path, + CONTROLLER_PATH, + customReferenceRelationController, + ); + const sourceFilePath = path.join( + sandbox.path, + MODEL_APP_PATH, + customReferenceRelationSourceModel, + ); + + assert.file(controllerFilePath); + assert.file(sourceFilePath); + + expectFileToMatchSnapshot(controllerFilePath); + expectFileToMatchSnapshot(sourceFilePath); + }); + + it('has correct default foreign keys', async () => { + const throughFilePath = path.join( + sandbox.path, + MODEL_APP_PATH, + customReferenceRelationThroughModel, + ); + assert.file(throughFilePath); + expectFileToMatchSnapshot(throughFilePath); + }); + }, + ); }); diff --git a/packages/cli/test/test-utils.js b/packages/cli/test/test-utils.js index 177c80de00ce..85c930db6655 100644 --- a/packages/cli/test/test-utils.js +++ b/packages/cli/test/test-utils.js @@ -121,7 +121,6 @@ exports.givenLBProject = function (rootDir, options = {}) { const repoPath = path.join(rootDir, '/src/repositories/bar.repository.ts'); fse.writeFileSync(repoPath, '--DUMMY VALUE--'); } - if (sandBoxFiles.length > 0) { for (const theFile of sandBoxFiles) { const fullPath = path.join(rootDir, theFile.path, theFile.file);