Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
400 changes: 293 additions & 107 deletions docs/site/hasOne-relation.md → docs/site/HasOne-relation.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions docs/site/Relation-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ lb4 relation [options]
- `--sourceModel`: Source model.
- `--destinationModel`: Destination model.
- `--foreignKeyName`: Destination/Source model foreign key name for
HasMany/BelongsTo relation, respectively.
HasMany,HasOne/BelongsTo relation, respectively.
- `--relationName`: Relation name.
- `-c`, `--config`: JSON file name or value to configure options.
- `-y`, `--yes`: Skip all confirmation prompts with default or provided value.
Expand All @@ -46,7 +46,7 @@ Defining lb4 relation in one command line interface (cli):
```sh
lb4 relation --sourceModel=<sourceModel>
--destinationModel=<destinationModel> --foreignKeyName=<foreignKeyName>
--relationType=<hasMany|belongsTo> [--relationName=<relationName>] [--format]
--relationType=<hasMany|hasOne|belongsTo> [--relationName=<relationName>] [--format]
```

- `<relationType>` - Type of the relation that will be created between the
Expand Down Expand Up @@ -81,6 +81,7 @@ The tool will prompt you for:
source model and the target model. Supported relation types:

- [HasMany](HasMany-relation.md)
- [HasOne](HasOne-relation.md)
- [BelongsTo](BelongsTo-relation.md)

- **Name of the `source` model.** _(sourceModel)_ Prompts a list of available
Expand Down
2 changes: 1 addition & 1 deletion docs/site/Relations.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ The articles on each type of relation above will show you how to leverage the
new relation engine to define and configure relations in your LoopBack
application.

To generate a `HasMany` or `BelongsTo` relation through the CLI, see
To generate a `HasMany`, `HasOne` or `BelongsTo` relation through the CLI, see
[Relation generator](Relation-generator.md).

## Limitations
Expand Down
199 changes: 199 additions & 0 deletions packages/cli/generators/relation/has-one-relation.generator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// Copyright IBM Corp. 2020. All Rights Reserved.
// Node module: @loopback/cli
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

'use strict';

const path = require('path');
const BaseRelationGenerator = require('./base-relation.generator');
const relationUtils = require('./utils.generator');
const utils = require('../../lib/utils');

const CONTROLLER_TEMPLATE_PATH_HAS_ONE =
'controller-relation-template-has-one.ts.ejs';

module.exports = class HasOneRelationGenerator extends BaseRelationGenerator {
constructor(args, opts) {
super(args, opts);
}

async generateControllers(options) {
this.artifactInfo.sourceModelClassName = options.sourceModel;
this.artifactInfo.targetModelClassName = options.destinationModel;
this.artifactInfo.sourceRepositoryClassName =
this.artifactInfo.sourceModelClassName + 'Repository';
this.artifactInfo.controllerClassName =
this.artifactInfo.sourceModelClassName +
this.artifactInfo.targetModelClassName +
'Controller';
this.artifactInfo.paramSourceRepository = utils.camelCase(
this.artifactInfo.sourceModelClassName + 'Repository',
);

this.artifactInfo.sourceModelName = utils.toFileName(options.sourceModel);
this.artifactInfo.sourceModelPath = utils.pluralize(
this.artifactInfo.sourceModelName,
);
this.artifactInfo.targetModelName = utils.toFileName(
options.destinationModel,
);
this.artifactInfo.targetModelPath = this.artifactInfo.targetModelName;
this.artifactInfo.targetModelRequestBody = utils.camelCase(
this.artifactInfo.targetModelName,
);
this.artifactInfo.relationPropertyName = options.relationName;
this.artifactInfo.sourceModelPrimaryKey = options.sourceModelPrimaryKey;
this.artifactInfo.sourceModelPrimaryKeyType =
options.sourceModelPrimaryKeyType;
this.artifactInfo.targetModelPrimaryKey = options.targetModelPrimaryKey;
this.artifactInfo.foreignKeyName = options.foreignKeyName;

const source = this.templatePath(CONTROLLER_TEMPLATE_PATH_HAS_ONE);

this.artifactInfo.name =
options.sourceModel + '-' + options.destinationModel;
this.artifactInfo.outFile =
utils.toFileName(this.artifactInfo.name) + '.controller.ts';

const dest = this.destinationPath(
path.join(this.artifactInfo.outDir, this.artifactInfo.outFile),
);

this.copyTemplatedFiles(source, dest, this.artifactInfo);
await relationUtils.addExportController(
this,
path.resolve(this.artifactInfo.outDir, 'index.ts'),
this.artifactInfo.controllerClassName,
utils.toFileName(this.artifactInfo.name) + '.controller',
);
}

async generateModels(options) {
// for repo to generate relation name
this.artifactInfo.relationName = options.relationName;
const modelDir = this.artifactInfo.modelDir;
const sourceModel = options.sourceModel;

const targetModel = options.destinationModel;

const relationType = options.relationType;
const relationName = options.relationName;
const fktype = options.sourceModelPrimaryKeyType;
const isForeignKeyExist = options.doesForeignKeyExist;
const foreignKeyName = options.foreignKeyName;

const isDefaultForeignKey =
foreignKeyName === utils.camelCase(options.sourceModel) + 'Id';

let modelProperty;
const project = new relationUtils.AstLoopBackProject();

const sourceFile = relationUtils.addFileToProject(
project,
modelDir,
sourceModel,
);
const sourceClass = relationUtils.getClassObj(sourceFile, sourceModel);
relationUtils.doesRelationExist(sourceClass, relationName);

modelProperty = this.getHasOne(
targetModel,
relationName,
isDefaultForeignKey,
foreignKeyName,
);

relationUtils.addProperty(sourceClass, modelProperty);
const imports = relationUtils.getRequiredImports(targetModel, relationType);

relationUtils.addRequiredImports(sourceFile, imports);
await sourceFile.save();

const targetFile = relationUtils.addFileToProject(
project,
modelDir,
targetModel,
);
const targetClass = relationUtils.getClassObj(targetFile, targetModel);

if (isForeignKeyExist) {
if (
!relationUtils.isValidPropertyType(targetClass, foreignKeyName, fktype)
) {
throw new Error('foreignKey Type Error');
}
} else {
modelProperty = relationUtils.addForeignKey(foreignKeyName, fktype);
relationUtils.addProperty(targetClass, modelProperty);
targetClass.formatText();
await targetFile.save();
}
}

getHasOne(className, relationName, isDefaultForeignKey, foreignKeyName) {
let relationDecorator = [
{
name: 'hasOne',
arguments: [`() => ${className}, {keyTo: '${foreignKeyName}'}`],
},
];
if (isDefaultForeignKey) {
relationDecorator = [
{
name: 'hasOne',
arguments: [`() => ${className}`],
},
];
}

return {
decorators: relationDecorator,
name: relationName,
type: className,
};
}

_getRepositoryRequiredImports(dstModelClassName, dstRepositoryClassName) {
const importsArray = super._getRepositoryRequiredImports(
dstModelClassName,
dstRepositoryClassName,
);
importsArray.push({
name: 'HasOneRepositoryFactory',
module: '@loopback/repository',
});
return importsArray;
}

_getRepositoryRelationPropertyName() {
return this.artifactInfo.relationName;
}

_getRepositoryRelationPropertyType() {
return `HasOneRepositoryFactory<${utils.toClassName(
this.artifactInfo.dstModelClass,
)}, typeof ${utils.toClassName(
this.artifactInfo.srcModelClass,
)}.prototype.${this.artifactInfo.srcModelPrimaryKey}>`;
}

_addCreatorToRepositoryConstructor(classConstructor) {
const relationPropertyName = this._getRepositoryRelationPropertyName();
const statement =
`this.${relationPropertyName} = ` +
`this.createHasOneRepositoryFactoryFor('${relationPropertyName}', ` +
`${utils.camelCase(this.artifactInfo.dstRepositoryClassName)}Getter);`;
classConstructor.insertStatements(1, statement);
}

_registerInclusionResolverForRelation(classConstructor, options) {
const relationPropertyName = this._getRepositoryRelationPropertyName();
if (options.registerInclusionResolver) {
const statement =
`this.registerInclusionResolver(` +
`'${relationPropertyName}', this.${relationPropertyName}.inclusionResolver);`;
classConstructor.insertStatements(2, statement);
}
}
};
9 changes: 9 additions & 0 deletions packages/cli/generators/relation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const relationUtils = require('./utils.generator');

const BelongsToRelationGenerator = require('./belongs-to-relation.generator');
const HasManyRelationGenerator = require('./has-many-relation.generator');
const HasOneRelationGenerator = require('./has-one-relation.generator');

const ERROR_INCORRECT_RELATION_TYPE = 'Incorrect relation type';
const ERROR_MODEL_DOES_NOT_EXIST = 'model does not exist.';
Expand Down Expand Up @@ -141,6 +142,11 @@ module.exports = class RelationGenerator extends ArtifactGenerator {
utils.camelCase(this.artifactInfo.destinationModel),
);
break;
case relationUtils.relationType.hasOne:
defaultRelationName = utils.camelCase(
this.artifactInfo.destinationModel,
);
break;
}

return defaultRelationName;
Expand Down Expand Up @@ -515,6 +521,9 @@ module.exports = class RelationGenerator extends ArtifactGenerator {
case relationUtils.relationType.hasMany:
relationGenerator = new HasManyRelationGenerator(this.args, this.opts);
break;
case relationUtils.relationType.hasOne:
relationGenerator = new HasOneRelationGenerator(this.args, this.opts);
break;
}

try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
Count,
CountSchema,
Filter,
repository,
Where,
} from '@loopback/repository';
import {
del,
get,
getModelSchemaRef,
getWhereSchemaFor,
param,
patch,
post,
requestBody,
} from '@loopback/rest';
import {
<%= sourceModelClassName %>,
<%= targetModelClassName %>,
} from '../models';
import {<%= sourceRepositoryClassName %>} from '../repositories';

export class <%= controllerClassName %> {
constructor(
@repository(<%= sourceRepositoryClassName %>) protected <%= paramSourceRepository %>: <%= sourceRepositoryClassName %>,
) { }

@get('/<%= sourceModelPath %>/{id}/<%= targetModelPath %>', {
responses: {
'200': {
description: '<%= sourceModelClassName %> has one <%= targetModelClassName %>',
content: {
'application/json': {
schema: getModelSchemaRef(<%= targetModelClassName %>),
},
},
},
},
})
async get(
@param.path.<%= sourceModelPrimaryKeyType %>('id') id: <%= sourceModelPrimaryKeyType %>,
@param.query.object('filter') filter?: Filter<<%= targetModelClassName %>>,
): Promise<<%= targetModelClassName %>> {
return this.<%= paramSourceRepository %>.<%= relationPropertyName %>(id).get(filter);
}

@post('/<%= sourceModelPath %>/{id}/<%= targetModelPath %>', {
responses: {
'200': {
description: '<%= sourceModelClassName %> model instance',
content: {'application/json': {schema: getModelSchemaRef(<%= targetModelClassName %>)}},
},
},
})
async create(
@param.path.<%= sourceModelPrimaryKeyType %>('id') id: typeof <%= sourceModelClassName %>.prototype.<%= sourceModelPrimaryKey %>,
@requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(<%= targetModelClassName %>, {
title: 'New<%= targetModelClassName %>In<%= sourceModelClassName %>',
exclude: ['<%= targetModelPrimaryKey %>'],
optional: ['<%= foreignKeyName %>']
}),
},
},
}) <%= targetModelRequestBody %>: Omit<<%= targetModelClassName %>, '<%= targetModelPrimaryKey %>'>,
): Promise<<%= targetModelClassName %>> {
return this.<%= paramSourceRepository %>.<%= relationPropertyName %>(id).create(<%= targetModelRequestBody %>);
}

@patch('/<%= sourceModelPath %>/{id}/<%= targetModelPath %>', {
responses: {
'200': {
description: '<%= sourceModelClassName %>.<%= targetModelClassName %> PATCH success count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async patch(
@param.path.<%= sourceModelPrimaryKeyType %>('id') id: <%= sourceModelPrimaryKeyType %>,
@requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(<%= targetModelClassName %>, {partial: true}),
},
},
})
<%= targetModelRequestBody %>: Partial<<%= targetModelClassName %>>,
@param.query.object('where', getWhereSchemaFor(<%= targetModelClassName %>)) where?: Where<<%= targetModelClassName %>>,
): Promise<Count> {
return this.<%= paramSourceRepository %>.<%= relationPropertyName %>(id).patch(<%= targetModelRequestBody %>, where);
}

@del('/<%= sourceModelPath %>/{id}/<%= targetModelPath %>', {
responses: {
'200': {
description: '<%= sourceModelClassName %>.<%= targetModelClassName %> DELETE success count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async delete(
@param.path.<%= sourceModelPrimaryKeyType %>('id') id: <%= sourceModelPrimaryKeyType %>,
@param.query.object('where', getWhereSchemaFor(<%= targetModelClassName %>)) where?: Where<<%= targetModelClassName %>>,
): Promise<Count> {
return this.<%= paramSourceRepository %>.<%= relationPropertyName %>(id).delete(where);
}
}
1 change: 1 addition & 0 deletions packages/cli/generators/relation/utils.generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const utils = require('../../lib/utils');
exports.relationType = {
belongsTo: 'belongsTo',
hasMany: 'hasMany',
hasOne: 'hasOne',
};

class AstLoopBackProject extends ast.Project {
Expand Down
Loading