From a25a52bbb967b59588c205ad737e32840642f583 Mon Sep 17 00:00:00 2001 From: gczobel-f5 Date: Wed, 9 Jan 2019 14:05:22 +0200 Subject: [PATCH] feat(cli): use a custom repository base class Allow the user to specify a custom Repository class to inherit from. CLI supports custom repository name * via an interactive prompt * via CLI options * via JSON config Two tests modified to use the new parameter to pass Modified tests: * generates a kv repository from default model * generates a kv repository from decorator defined model --- packages/cli/generators/repository/index.js | 142 ++++++++++++++++++ .../repository-crud-default-template.ts.ejs | 7 +- .../repository-kv-template.ts.ejs | 11 +- .../cli/test/fixtures/repository/index.js | 11 ++ .../defaultmodel.repository.base.ts | 13 ++ .../generators/repository.integration.js | 43 +++++- 6 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 packages/cli/test/fixtures/repository/repositories/defaultmodel.repository.base.ts diff --git a/packages/cli/generators/repository/index.js b/packages/cli/generators/repository/index.js index 429cf9c67643..f73e87f66c8f 100644 --- a/packages/cli/generators/repository/index.js +++ b/packages/cli/generators/repository/index.js @@ -14,12 +14,34 @@ const chalk = require('chalk'); const utils = require('../../lib/utils'); const connectors = require('../datasource/connectors.json'); const tsquery = require('../../lib/ast-helper'); +const pascalCase = require('change-case').pascalCase; const VALID_CONNECTORS_FOR_REPOSITORY = ['KeyValueModel', 'PersistedModel']; const KEY_VALUE_CONNECTOR = ['KeyValueModel']; const DEFAULT_CRUD_REPOSITORY = 'DefaultCrudRepository'; const KEY_VALUE_REPOSITORY = 'DefaultKeyValueRepository'; +const BASE_REPOSITORIES = [DEFAULT_CRUD_REPOSITORY, KEY_VALUE_REPOSITORY]; +const CLI_BASE_CRUD_REPOSITORIES = [ + { + name: `${DEFAULT_CRUD_REPOSITORY} ${chalk.gray('(Legacy juggler bridge)')}`, + value: DEFAULT_CRUD_REPOSITORY, + }, +]; +const CLI_BASE_KEY_VALUE_REPOSITORIES = [ + { + name: `${KEY_VALUE_REPOSITORY} ${chalk.gray( + '(For access to a key-value store)', + )}`, + value: KEY_VALUE_REPOSITORY, + }, +]; +const CLI_BASE_SEPARATOR = [ + { + type: 'separator', + line: '----- Custom Repositories -----', + }, +]; const REPOSITORY_KV_TEMPLATE = 'repository-kv-template.ts.ejs'; const REPOSITORY_CRUD_TEMPLATE = 'repository-crud-default-template.ts.ejs'; @@ -27,6 +49,7 @@ const REPOSITORY_CRUD_TEMPLATE = 'repository-crud-default-template.ts.ejs'; const PROMPT_MESSAGE_MODEL = 'Select the model(s) you want to generate a repository'; const PROMPT_MESSAGE_DATA_SOURCE = 'Please select the datasource'; +const PROMPT_BASE_REPOSITORY_CLASS = 'Please select the repository base class'; const ERROR_READING_FILE = 'Error reading file'; const ERROR_NO_DATA_SOURCES_FOUND = 'No datasources found at'; const ERROR_NO_MODELS_FOUND = 'No models found at'; @@ -38,6 +61,35 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { super(args, opts); } + /** + * Find all the base artifacts in the given path whose type matches the + * provided artifactType. + * For example, a artifactType of "repository" will search the target path for + * matches to "*.repository.base.ts" + * @param {string} dir The target directory from which to load artifacts. + * @param {string} artifactType The artifact type (ex. "model", "repository") + */ + async _findBaseClasses(dir, artifactType) { + const paths = await utils.findArtifactPaths(dir, artifactType + '.base'); + debug(`repository artifact paths: ${paths}`); + + // get base class and path + const baseRepositoryList = []; + for (const p of paths) { + //get name removing anything from .artifactType.base + const artifactFile = path.parse(_.last(_.split(p, path.sep))).name; + const firstWord = _.first(_.split(artifactFile, '.')); + const artifactName = + utils.toClassName(firstWord) + utils.toClassName(artifactType); + + const baseRepository = {name: artifactName, file: artifactFile}; + baseRepositoryList.push(baseRepository); + } + + debug(`repository base classes: ${inspect(baseRepositoryList)}`); + return baseRepositoryList; + } + /** * get the property name for the id field * @param {string} modelName @@ -131,6 +183,13 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { description: 'A valid datasource name', }); + this.option('repositoryBaseClass', { + type: String, + required: false, + description: 'A valid repository base class', + default: 'DefaultCrudRepository', + }); + return super._setupGenerator(); } @@ -313,6 +372,65 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { }); } + async promptBaseClass() { + debug('Prompting for repository base'); + if (this.shouldExit()) return; + + const availableRepositoryList = []; + + debug(`repositoryTypeClass ${this.artifactInfo.repositoryTypeClass}`); + // Add base repositories based on datasource type + if (this.artifactInfo.repositoryTypeClass === KEY_VALUE_REPOSITORY) + availableRepositoryList.push(...CLI_BASE_KEY_VALUE_REPOSITORIES); + else availableRepositoryList.push(...CLI_BASE_CRUD_REPOSITORIES); + availableRepositoryList.push(...CLI_BASE_SEPARATOR); + + try { + this.artifactInfo.baseRepositoryList = await this._findBaseClasses( + this.artifactInfo.outDir, + 'repository', + ); + if ( + this.artifactInfo.baseRepositoryList && + this.artifactInfo.baseRepositoryList.length > 0 + ) { + availableRepositoryList.push(...this.artifactInfo.baseRepositoryList); + debug(`availableRepositoryList ${availableRepositoryList}`); + } + } catch (err) { + return this.exit(err); + } + + if (this.options.repositoryBaseClass) { + debug( + `Base repository received from command line: ${ + this.options.repositoryBaseClass + }`, + ); + this.artifactInfo.repositoryBaseClass = this.options.repositoryBaseClass; + } + + return this.prompt([ + { + type: 'list', + name: 'repositoryBaseClass', + message: PROMPT_BASE_REPOSITORY_CLASS, + when: this.artifactInfo.repositoryBaseClass === undefined, + choices: availableRepositoryList, + default: availableRepositoryList[0], + }, + ]) + .then(props => { + debug(`props after custom repository prompt: ${inspect(props)}`); + Object.assign(this.artifactInfo, props); + return props; + }) + .catch(err => { + debug(`Error during repository base class prompt: ${err.stack}`); + return this.exit(err); + }); + } + async promptModelId() { if (this.shouldExit()) return false; let idProperty; @@ -362,6 +480,22 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { async _scaffold() { if (this.shouldExit()) return false; + this.artifactInfo.isRepositoryBaseBuiltin = BASE_REPOSITORIES.includes( + this.artifactInfo.repositoryBaseClass, + ); + debug( + `isRepositoryBaseBuiltin : ${this.artifactInfo.isRepositoryBaseBuiltin}`, + ); + if (!this.artifactInfo.isRepositoryBaseBuiltin) { + const baseIndex = _.findIndex(this.artifactInfo.baseRepositoryList, [ + 'name', + this.artifactInfo.repositoryBaseClass, + ]); + this.artifactInfo.repositoryBaseFile = this.artifactInfo.baseRepositoryList[ + baseIndex + ].file; + } + if (this.options.name) { this.artifactInfo.className = utils.toClassName(this.options.name); this.artifactInfo.outFile = utils.getRepositoryFileName( @@ -401,6 +535,7 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { debug(`artifactInfo: ${inspect(this.artifactInfo)}`); debug(`Copying artifact to: ${dest}`); } + this.copyTemplatedFiles(source, dest, this.artifactInfo); return; } @@ -412,6 +547,13 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { ? 'Repositories' : 'Repository'; + this.artifactInfo.modelNameList = _.map( + this.artifactInfo.modelNameList, + repositoryName => { + return repositoryName + 'Repository'; + }, + ); + this.artifactInfo.name = this.artifactInfo.modelNameList ? this.artifactInfo.modelNameList.join() : this.artifactInfo.modelName; diff --git a/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs b/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs index 4dcc90d2b4b6..10c17ca0151c 100644 --- a/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs +++ b/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs @@ -1,9 +1,14 @@ +<%if (isRepositoryBaseBuiltin) { -%> import {<%= repositoryTypeClass %>} from '@loopback/repository'; +<% } -%> import {<%= modelName %>} from '../models'; import {<%= dataSourceClassName %>} from '../datasources'; import {inject} from '@loopback/core'; +<%if ( !isRepositoryBaseBuiltin ) { -%> +import {<%=repositoryBaseClass %>} from './<%=repositoryBaseFile %>'; +<% } -%> -export class <%= className %>Repository extends <%= repositoryTypeClass %>< +export class <%= className %>Repository extends <%= repositoryBaseClass %>< <%= modelName %>, typeof <%= modelName %>.prototype.<%= idProperty %> > { diff --git a/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs b/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs index d84bc66f309b..2f70bfdda13c 100644 --- a/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs +++ b/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs @@ -1,11 +1,16 @@ -import {<%= repositoryTypeClass %>} from '@loopback/repository'; +<%if (isRepositoryBaseBuiltin) { -%> +import {<%= repositoryTypeClass %>, juggler} from '@loopback/repository'; +<% } -%> import {<%= modelName %>} from '../models'; import {<%= dataSourceClassName %>} from '../datasources'; import {inject} from '@loopback/core'; +<%if ( !isRepositoryBaseBuiltin ) { -%> +import {<%=repositoryBaseClass %>} from './<%=repositoryBaseFile %>'; +<% } -%> -export class <%= className %>Repository extends <%= repositoryTypeClass %>< +export class <%= className %>Repository extends <%= repositoryBaseClass %>< <%= modelName %> - > { +> { constructor( @inject('datasources.<%= dataSourceName %>') dataSource: <%= dataSourceClassName %>, ) { diff --git a/packages/cli/test/fixtures/repository/index.js b/packages/cli/test/fixtures/repository/index.js index 7e0a993a78bd..6e224c0e6db5 100644 --- a/packages/cli/test/fixtures/repository/index.js +++ b/packages/cli/test/fixtures/repository/index.js @@ -1,5 +1,6 @@ const DATASOURCE_APP_PATH = 'src/datasources'; const MODEL_APP_PATH = 'src/models'; +const REPOSITORY_APP_PATH = 'src/repositories'; const CONFIG_PATH = '.'; const DUMMY_CONTENT = '--DUMMY VALUE--'; const fs = require('fs'); @@ -107,4 +108,14 @@ exports.SANDBOX_FILES = [ encoding: 'utf-8', }), }, + { + path: REPOSITORY_APP_PATH, + file: 'defaultmodel.repository.base.ts', + content: fs.readFileSync( + require.resolve('./repositories/defaultmodel.repository.base.ts'), + { + encoding: 'utf-8', + }, + ), + }, ]; diff --git a/packages/cli/test/fixtures/repository/repositories/defaultmodel.repository.base.ts b/packages/cli/test/fixtures/repository/repositories/defaultmodel.repository.base.ts new file mode 100644 index 000000000000..ddf7f1ca71f7 --- /dev/null +++ b/packages/cli/test/fixtures/repository/repositories/defaultmodel.repository.base.ts @@ -0,0 +1,13 @@ +import {DefaultCrudRepository} from '@loopback/repository'; +import {Defaultmodel} from '../models'; +import {DbmemDataSource} from '../datasources'; +import {inject} from '@loopback/core'; + +export class DefaultmodelRepository extends DefaultCrudRepository< + Defaultmodel, + typeof Defaultmodel.prototype.id +> { + constructor(@inject('datasources.dbmem') dataSource: DbmemDataSource) { + super(Defaultmodel, dataSource); + } +} diff --git a/packages/cli/test/integration/generators/repository.integration.js b/packages/cli/test/integration/generators/repository.integration.js index ed393f68d0e5..82fa0ac8b771 100644 --- a/packages/cli/test/integration/generators/repository.integration.js +++ b/packages/cli/test/integration/generators/repository.integration.js @@ -384,6 +384,41 @@ describe('lb4 repository', function() { /export \* from '.\/decoratordefined.repository';/, ); }); + it('generates a crud repository from custom base class', async () => { + await testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withArguments( + '--datasource dbmem --model decoratordefined --repositoryBaseClass DefaultmodelRepository', + ); + const expectedFile = path.join( + SANDBOX_PATH, + REPOSITORY_APP_PATH, + 'decoratordefined.repository.ts', + ); + assert.file(expectedFile); + assert.fileContent( + expectedFile, + /import {DefaultmodelRepository} from '.\/defaultmodel.repository.base';/, + ); + assert.fileContent( + expectedFile, + /export class DecoratordefinedRepository extends DefaultmodelRepository\ { @@ -395,7 +430,9 @@ describe('lb4 repository', function() { additionalFiles: SANDBOX_FILES, }), ) - .withArguments('--datasource dbkv --model Defaultmodel'); + .withArguments( + '--datasource dbkv --model Defaultmodel --repositoryBaseClass DefaultKeyValueRepository', + ); const expectedFile = path.join( SANDBOX_PATH, REPOSITORY_APP_PATH, @@ -425,7 +462,9 @@ describe('lb4 repository', function() { }), ) .withPrompts(basicPrompt) - .withArguments('--model decoratordefined'); + .withArguments( + '--model decoratordefined --repositoryBaseClass DefaultKeyValueRepository', + ); const expectedFile = path.join( SANDBOX_PATH, REPOSITORY_APP_PATH,