Skip to content

Commit 47ec90a

Browse files
committed
fix(cli): support keyTo option in belongsTo relation generator
Adds keyTo support to the belongsTo relation CLI generator: - Passes keyTo to getBelongsTo() decorator so the FK can resolve to a non-PK field on the target model - Resolves fktype from the target model's keyTo property type - Reflects keyTo in the controller OpenAPI description - Propagates keyTo from --config CLI option to artifactInfo in the relation generator scaffold step - Adds integration tests and snapshots for keyTo with and without a custom relation name Fixes #11594 Signed-off-by: Abubakar Awan <abkawan6@gmail.com>
1 parent d1838ad commit 47ec90a

5 files changed

Lines changed: 244 additions & 4 deletions

File tree

packages/cli/generators/relation/belongs-to-relation.generator.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ module.exports = class BelongsToRelationGenerator extends (
4949
);
5050

5151
this.artifactInfo.relationPropertyName = options.relationName;
52+
this.artifactInfo.keyTo = options.keyTo;
5253
this.artifactInfo.targetModelPrimaryKey =
5354
options.destinationModelPrimaryKey;
5455
this.artifactInfo.targetModelPrimaryKeyType =
@@ -85,7 +86,11 @@ module.exports = class BelongsToRelationGenerator extends (
8586
const relationName = options.relationName;
8687
const defaultRelationName = options.defaultRelationName;
8788
const foreignKeyName = options.foreignKeyName;
88-
const fktype = options.destinationModelPrimaryKeyType;
89+
const keyTo = options.keyTo;
90+
const fktype = keyTo
91+
? relationUtils.getModelPropertyType(modelDir, targetModel, keyTo) ||
92+
options.destinationModelPrimaryKeyType
93+
: options.destinationModelPrimaryKeyType;
8994

9095
const project = new relationUtils.AstLoopBackProject();
9196
const sourceFile = relationUtils.addFileToProject(
@@ -103,6 +108,7 @@ module.exports = class BelongsToRelationGenerator extends (
103108
defaultRelationName,
104109
foreignKeyName,
105110
fktype,
111+
keyTo,
106112
);
107113

108114
relationUtils.addProperty(sourceClass, modelProperty);
@@ -123,20 +129,29 @@ module.exports = class BelongsToRelationGenerator extends (
123129
defaultRelationName,
124130
foreignKeyName,
125131
fktype,
132+
keyTo,
126133
) {
134+
const keyToOption = keyTo ? `, keyTo: '${keyTo}'` : '';
135+
127136
// checks if relation name is customized
128137
let relationDecorator = [
129138
{
130139
name: 'belongsTo',
131-
arguments: [`() => ${className}`],
140+
arguments: [
141+
keyTo
142+
? `() => ${className}, {keyTo: '${keyTo}'}`
143+
: `() => ${className}`,
144+
],
132145
},
133146
];
134147
// already checked if the relation name is the same as the source key before
135148
if (defaultRelationName !== relationName) {
136149
relationDecorator = [
137150
{
138151
name: 'belongsTo',
139-
arguments: [`() => ${className}, {name: '${relationName}'}`],
152+
arguments: [
153+
`() => ${className}, {name: '${relationName}'${keyToOption}}`,
154+
],
140155
},
141156
];
142157
}

packages/cli/generators/relation/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,10 @@ module.exports = class RelationGenerator extends ArtifactGenerator {
761761

762762
debug('Invoke generator...');
763763

764+
if (this.options.keyTo) {
765+
this.artifactInfo.keyTo = this.options.keyTo;
766+
}
767+
764768
let relationGenerator;
765769

766770
this.artifactInfo.name = this.artifactInfo.relationType;

packages/cli/generators/relation/templates/controller-relation-template-belongs-to.ts.ejs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class <%= controllerClassName %> {
2121
@get('/<%= sourceModelPath %>/{id}/<%= targetModelName %>', {
2222
responses: {
2323
'200': {
24-
description: '<%= targetModelClassName %> belonging to <%= sourceModelClassName %>',
24+
description: '<%= targetModelClassName %> belonging to <%= sourceModelClassName %><%= keyTo ? " (resolved via " + targetModelClassName + "." + keyTo + ")" : "" %>',
2525
content: {
2626
'application/json': {
2727
schema: getModelSchemaRef(<%= targetModelClassName %>),

packages/cli/snapshots/integration/generators/relation.belongs-to.integration.snapshots.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,147 @@ export class Employee extends Entity {
373373
}
374374
375375
`;
376+
377+
378+
exports[`lb4 relation generates belongsTo relation with keyTo option generated model includes keyTo in belongsTo decorator 1`] = `
379+
import {Entity, model, property, belongsTo} from '@loopback/repository';
380+
import {Customer} from './customer.model';
381+
382+
@model()
383+
export class Order extends Entity {
384+
@property({
385+
type: 'number',
386+
id: true,
387+
default: 0,
388+
})
389+
id?: number;
390+
391+
@property({
392+
type: 'string',
393+
})
394+
name?: string;
395+
396+
@belongsTo(() => Customer, {keyTo: 'name'})
397+
customerId: string;
398+
399+
constructor(data?: Partial<Order>) {
400+
super(data);
401+
}
402+
}
403+
404+
`;
405+
406+
407+
exports[`lb4 relation generates belongsTo relation with keyTo option generated controller description includes resolved via reference 1`] = `
408+
import {
409+
repository,
410+
} from '@loopback/repository';
411+
import {
412+
param,
413+
get,
414+
getModelSchemaRef,
415+
} from '@loopback/rest';
416+
import {
417+
Order,
418+
Customer,
419+
} from '../models';
420+
import {OrderRepository} from '../repositories';
421+
422+
export class OrderCustomerController {
423+
constructor(
424+
@repository(OrderRepository)
425+
public orderRepository: OrderRepository,
426+
) { }
427+
428+
@get('/orders/{id}/customer', {
429+
responses: {
430+
'200': {
431+
description: 'Customer belonging to Order (resolved via Customer.name)',
432+
content: {
433+
'application/json': {
434+
schema: getModelSchemaRef(Customer),
435+
},
436+
},
437+
},
438+
},
439+
})
440+
async getCustomer(
441+
@param.path.number('id') id: typeof Order.prototype.id,
442+
): Promise<Customer> {
443+
return this.orderRepository.customer(id);
444+
}
445+
}
446+
447+
`;
448+
449+
450+
exports[`lb4 relation generates belongsTo relation with keyTo and custom relation name generated model includes both name and keyTo in belongsTo decorator 1`] = `
451+
import {Entity, model, property, belongsTo} from '@loopback/repository';
452+
import {Customer} from './customer.model';
453+
454+
@model()
455+
export class Order extends Entity {
456+
@property({
457+
type: 'number',
458+
id: true,
459+
default: 0,
460+
})
461+
id?: number;
462+
463+
@property({
464+
type: 'string',
465+
})
466+
name?: string;
467+
468+
@belongsTo(() => Customer, {name: 'myCustomer', keyTo: 'name'})
469+
customerId: string;
470+
471+
constructor(data?: Partial<Order>) {
472+
super(data);
473+
}
474+
}
475+
476+
`;
477+
478+
479+
exports[`lb4 relation generates belongsTo relation with keyTo and custom relation name generated controller description includes resolved via reference with custom relation name 1`] = `
480+
import {
481+
repository,
482+
} from '@loopback/repository';
483+
import {
484+
param,
485+
get,
486+
getModelSchemaRef,
487+
} from '@loopback/rest';
488+
import {
489+
Order,
490+
Customer,
491+
} from '../models';
492+
import {OrderRepository} from '../repositories';
493+
494+
export class OrderCustomerController {
495+
constructor(
496+
@repository(OrderRepository)
497+
public orderRepository: OrderRepository,
498+
) { }
499+
500+
@get('/orders/{id}/customer', {
501+
responses: {
502+
'200': {
503+
description: 'Customer belonging to Order (resolved via Customer.name)',
504+
content: {
505+
'application/json': {
506+
schema: getModelSchemaRef(Customer),
507+
},
508+
},
509+
},
510+
},
511+
})
512+
async getCustomer(
513+
@param.path.number('id') id: typeof Order.prototype.id,
514+
): Promise<Customer> {
515+
return this.orderRepository.myCustomer(id);
516+
}
517+
}
518+
519+
`;

packages/cli/test/integration/generators/relation.belongs-to.integration.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,4 +497,81 @@ describe('lb4 relation', /** @this {Mocha.Suite} */ function () {
497497
});
498498
},
499499
);
500+
501+
context('generates belongsTo relation with keyTo option', () => {
502+
before(async function runGeneratorWithAnswers() {
503+
await sandbox.reset();
504+
await testUtils
505+
.executeGenerator(generator)
506+
.inDir(sandbox.path, () =>
507+
testUtils.givenLBProject(sandbox.path, {
508+
additionalFiles: SANDBOX_FILES,
509+
}),
510+
)
511+
.withArguments([
512+
'--config',
513+
`{"relationType":"belongsTo","sourceModel":"Order","destinationModel":"Customer","foreignKeyName":"customerId","keyTo":"name"}`,
514+
]);
515+
});
516+
517+
it('generated model includes keyTo in belongsTo decorator', async () => {
518+
const sourceFilePath = path.join(
519+
sandbox.path,
520+
MODEL_APP_PATH,
521+
sourceFileName,
522+
);
523+
assert.file(sourceFilePath);
524+
expectFileToMatchSnapshot(sourceFilePath);
525+
});
526+
527+
it('generated controller description includes resolved via reference', async () => {
528+
const filePath = path.join(
529+
sandbox.path,
530+
CONTROLLER_PATH,
531+
controllerFileName,
532+
);
533+
assert.file(filePath);
534+
expectFileToMatchSnapshot(filePath);
535+
});
536+
});
537+
538+
context(
539+
'generates belongsTo relation with keyTo and custom relation name',
540+
() => {
541+
before(async function runGeneratorWithAnswers() {
542+
await sandbox.reset();
543+
await testUtils
544+
.executeGenerator(generator)
545+
.inDir(sandbox.path, () =>
546+
testUtils.givenLBProject(sandbox.path, {
547+
additionalFiles: SANDBOX_FILES,
548+
}),
549+
)
550+
.withArguments([
551+
'--config',
552+
`{"relationType":"belongsTo","sourceModel":"Order","destinationModel":"Customer","foreignKeyName":"customerId","relationName":"myCustomer","keyTo":"name"}`,
553+
]);
554+
});
555+
556+
it('generated model includes both name and keyTo in belongsTo decorator', async () => {
557+
const sourceFilePath = path.join(
558+
sandbox.path,
559+
MODEL_APP_PATH,
560+
sourceFileName,
561+
);
562+
assert.file(sourceFilePath);
563+
expectFileToMatchSnapshot(sourceFilePath);
564+
});
565+
566+
it('generated controller description includes resolved via reference with custom relation name', async () => {
567+
const filePath = path.join(
568+
sandbox.path,
569+
CONTROLLER_PATH,
570+
controllerFileName,
571+
);
572+
assert.file(filePath);
573+
expectFileToMatchSnapshot(filePath);
574+
});
575+
},
576+
);
500577
});

0 commit comments

Comments
 (0)