Skip to content

Commit

Permalink
feat(cli): use a custom repository base class
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gczobel-f5 authored and bajtos committed Feb 4, 2019
1 parent 0e92b88 commit edbbe88
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 6 deletions.
142 changes: 142 additions & 0 deletions packages/cli/generators/repository/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,42 @@ 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';

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';
Expand All @@ -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
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 %>
> {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 %>,
) {
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/test/fixtures/repository/index.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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',
},
),
},
];
Original file line number Diff line number Diff line change
@@ -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);
}
}
43 changes: 41 additions & 2 deletions packages/cli/test/integration/generators/repository.integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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\</,
);
assert.fileContent(
expectedFile,
/typeof Decoratordefined.prototype.thePK/,
);
assert.file(INDEX_FILE);
assert.fileContent(
INDEX_FILE,
/export \* from '.\/decoratordefined.repository';/,
);
});
});

describe('valid generation of kv repositories', () => {
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit edbbe88

Please sign in to comment.