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
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be possibly simplified by using utils to map the artifact name to a file name instead of inferring the artifact name from the file names.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean using AST? Actually, all the CLIs use the same logic... take the first word of the filename as the artifact name...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. I meant to say to use a function to infer the file name from the artifact name and do the match.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


/**
* 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