Skip to content

Commit edbbe88

Browse files
gczobel-f5bajtos
authored andcommitted
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
1 parent 0e92b88 commit edbbe88

File tree

6 files changed

+221
-6
lines changed

6 files changed

+221
-6
lines changed

packages/cli/generators/repository/index.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,42 @@ const chalk = require('chalk');
1414
const utils = require('../../lib/utils');
1515
const connectors = require('../datasource/connectors.json');
1616
const tsquery = require('../../lib/ast-helper');
17+
const pascalCase = require('change-case').pascalCase;
1718

1819
const VALID_CONNECTORS_FOR_REPOSITORY = ['KeyValueModel', 'PersistedModel'];
1920
const KEY_VALUE_CONNECTOR = ['KeyValueModel'];
2021

2122
const DEFAULT_CRUD_REPOSITORY = 'DefaultCrudRepository';
2223
const KEY_VALUE_REPOSITORY = 'DefaultKeyValueRepository';
24+
const BASE_REPOSITORIES = [DEFAULT_CRUD_REPOSITORY, KEY_VALUE_REPOSITORY];
25+
const CLI_BASE_CRUD_REPOSITORIES = [
26+
{
27+
name: `${DEFAULT_CRUD_REPOSITORY} ${chalk.gray('(Legacy juggler bridge)')}`,
28+
value: DEFAULT_CRUD_REPOSITORY,
29+
},
30+
];
31+
const CLI_BASE_KEY_VALUE_REPOSITORIES = [
32+
{
33+
name: `${KEY_VALUE_REPOSITORY} ${chalk.gray(
34+
'(For access to a key-value store)',
35+
)}`,
36+
value: KEY_VALUE_REPOSITORY,
37+
},
38+
];
39+
const CLI_BASE_SEPARATOR = [
40+
{
41+
type: 'separator',
42+
line: '----- Custom Repositories -----',
43+
},
44+
];
2345

2446
const REPOSITORY_KV_TEMPLATE = 'repository-kv-template.ts.ejs';
2547
const REPOSITORY_CRUD_TEMPLATE = 'repository-crud-default-template.ts.ejs';
2648

2749
const PROMPT_MESSAGE_MODEL =
2850
'Select the model(s) you want to generate a repository';
2951
const PROMPT_MESSAGE_DATA_SOURCE = 'Please select the datasource';
52+
const PROMPT_BASE_REPOSITORY_CLASS = 'Please select the repository base class';
3053
const ERROR_READING_FILE = 'Error reading file';
3154
const ERROR_NO_DATA_SOURCES_FOUND = 'No datasources found at';
3255
const ERROR_NO_MODELS_FOUND = 'No models found at';
@@ -38,6 +61,35 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator {
3861
super(args, opts);
3962
}
4063

64+
/**
65+
* Find all the base artifacts in the given path whose type matches the
66+
* provided artifactType.
67+
* For example, a artifactType of "repository" will search the target path for
68+
* matches to "*.repository.base.ts"
69+
* @param {string} dir The target directory from which to load artifacts.
70+
* @param {string} artifactType The artifact type (ex. "model", "repository")
71+
*/
72+
async _findBaseClasses(dir, artifactType) {
73+
const paths = await utils.findArtifactPaths(dir, artifactType + '.base');
74+
debug(`repository artifact paths: ${paths}`);
75+
76+
// get base class and path
77+
const baseRepositoryList = [];
78+
for (const p of paths) {
79+
//get name removing anything from .artifactType.base
80+
const artifactFile = path.parse(_.last(_.split(p, path.sep))).name;
81+
const firstWord = _.first(_.split(artifactFile, '.'));
82+
const artifactName =
83+
utils.toClassName(firstWord) + utils.toClassName(artifactType);
84+
85+
const baseRepository = {name: artifactName, file: artifactFile};
86+
baseRepositoryList.push(baseRepository);
87+
}
88+
89+
debug(`repository base classes: ${inspect(baseRepositoryList)}`);
90+
return baseRepositoryList;
91+
}
92+
4193
/**
4294
* get the property name for the id field
4395
* @param {string} modelName
@@ -131,6 +183,13 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator {
131183
description: 'A valid datasource name',
132184
});
133185

186+
this.option('repositoryBaseClass', {
187+
type: String,
188+
required: false,
189+
description: 'A valid repository base class',
190+
default: 'DefaultCrudRepository',
191+
});
192+
134193
return super._setupGenerator();
135194
}
136195

@@ -313,6 +372,65 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator {
313372
});
314373
}
315374

375+
async promptBaseClass() {
376+
debug('Prompting for repository base');
377+
if (this.shouldExit()) return;
378+
379+
const availableRepositoryList = [];
380+
381+
debug(`repositoryTypeClass ${this.artifactInfo.repositoryTypeClass}`);
382+
// Add base repositories based on datasource type
383+
if (this.artifactInfo.repositoryTypeClass === KEY_VALUE_REPOSITORY)
384+
availableRepositoryList.push(...CLI_BASE_KEY_VALUE_REPOSITORIES);
385+
else availableRepositoryList.push(...CLI_BASE_CRUD_REPOSITORIES);
386+
availableRepositoryList.push(...CLI_BASE_SEPARATOR);
387+
388+
try {
389+
this.artifactInfo.baseRepositoryList = await this._findBaseClasses(
390+
this.artifactInfo.outDir,
391+
'repository',
392+
);
393+
if (
394+
this.artifactInfo.baseRepositoryList &&
395+
this.artifactInfo.baseRepositoryList.length > 0
396+
) {
397+
availableRepositoryList.push(...this.artifactInfo.baseRepositoryList);
398+
debug(`availableRepositoryList ${availableRepositoryList}`);
399+
}
400+
} catch (err) {
401+
return this.exit(err);
402+
}
403+
404+
if (this.options.repositoryBaseClass) {
405+
debug(
406+
`Base repository received from command line: ${
407+
this.options.repositoryBaseClass
408+
}`,
409+
);
410+
this.artifactInfo.repositoryBaseClass = this.options.repositoryBaseClass;
411+
}
412+
413+
return this.prompt([
414+
{
415+
type: 'list',
416+
name: 'repositoryBaseClass',
417+
message: PROMPT_BASE_REPOSITORY_CLASS,
418+
when: this.artifactInfo.repositoryBaseClass === undefined,
419+
choices: availableRepositoryList,
420+
default: availableRepositoryList[0],
421+
},
422+
])
423+
.then(props => {
424+
debug(`props after custom repository prompt: ${inspect(props)}`);
425+
Object.assign(this.artifactInfo, props);
426+
return props;
427+
})
428+
.catch(err => {
429+
debug(`Error during repository base class prompt: ${err.stack}`);
430+
return this.exit(err);
431+
});
432+
}
433+
316434
async promptModelId() {
317435
if (this.shouldExit()) return false;
318436
let idProperty;
@@ -362,6 +480,22 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator {
362480
async _scaffold() {
363481
if (this.shouldExit()) return false;
364482

483+
this.artifactInfo.isRepositoryBaseBuiltin = BASE_REPOSITORIES.includes(
484+
this.artifactInfo.repositoryBaseClass,
485+
);
486+
debug(
487+
`isRepositoryBaseBuiltin : ${this.artifactInfo.isRepositoryBaseBuiltin}`,
488+
);
489+
if (!this.artifactInfo.isRepositoryBaseBuiltin) {
490+
const baseIndex = _.findIndex(this.artifactInfo.baseRepositoryList, [
491+
'name',
492+
this.artifactInfo.repositoryBaseClass,
493+
]);
494+
this.artifactInfo.repositoryBaseFile = this.artifactInfo.baseRepositoryList[
495+
baseIndex
496+
].file;
497+
}
498+
365499
if (this.options.name) {
366500
this.artifactInfo.className = utils.toClassName(this.options.name);
367501
this.artifactInfo.outFile = utils.getRepositoryFileName(
@@ -401,6 +535,7 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator {
401535
debug(`artifactInfo: ${inspect(this.artifactInfo)}`);
402536
debug(`Copying artifact to: ${dest}`);
403537
}
538+
404539
this.copyTemplatedFiles(source, dest, this.artifactInfo);
405540
return;
406541
}
@@ -412,6 +547,13 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator {
412547
? 'Repositories'
413548
: 'Repository';
414549

550+
this.artifactInfo.modelNameList = _.map(
551+
this.artifactInfo.modelNameList,
552+
repositoryName => {
553+
return repositoryName + 'Repository';
554+
},
555+
);
556+
415557
this.artifactInfo.name = this.artifactInfo.modelNameList
416558
? this.artifactInfo.modelNameList.join()
417559
: this.artifactInfo.modelName;

packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
<%if (isRepositoryBaseBuiltin) { -%>
12
import {<%= repositoryTypeClass %>} from '@loopback/repository';
3+
<% } -%>
24
import {<%= modelName %>} from '../models';
35
import {<%= dataSourceClassName %>} from '../datasources';
46
import {inject} from '@loopback/core';
7+
<%if ( !isRepositoryBaseBuiltin ) { -%>
8+
import {<%=repositoryBaseClass %>} from './<%=repositoryBaseFile %>';
9+
<% } -%>
510

6-
export class <%= className %>Repository extends <%= repositoryTypeClass %><
11+
export class <%= className %>Repository extends <%= repositoryBaseClass %><
712
<%= modelName %>,
813
typeof <%= modelName %>.prototype.<%= idProperty %>
914
> {

packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import {<%= repositoryTypeClass %>} from '@loopback/repository';
1+
<%if (isRepositoryBaseBuiltin) { -%>
2+
import {<%= repositoryTypeClass %>, juggler} from '@loopback/repository';
3+
<% } -%>
24
import {<%= modelName %>} from '../models';
35
import {<%= dataSourceClassName %>} from '../datasources';
46
import {inject} from '@loopback/core';
7+
<%if ( !isRepositoryBaseBuiltin ) { -%>
8+
import {<%=repositoryBaseClass %>} from './<%=repositoryBaseFile %>';
9+
<% } -%>
510

6-
export class <%= className %>Repository extends <%= repositoryTypeClass %><
11+
export class <%= className %>Repository extends <%= repositoryBaseClass %><
712
<%= modelName %>
8-
> {
13+
> {
914
constructor(
1015
@inject('datasources.<%= dataSourceName %>') dataSource: <%= dataSourceClassName %>,
1116
) {

packages/cli/test/fixtures/repository/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const DATASOURCE_APP_PATH = 'src/datasources';
22
const MODEL_APP_PATH = 'src/models';
3+
const REPOSITORY_APP_PATH = 'src/repositories';
34
const CONFIG_PATH = '.';
45
const DUMMY_CONTENT = '--DUMMY VALUE--';
56
const fs = require('fs');
@@ -107,4 +108,14 @@ exports.SANDBOX_FILES = [
107108
encoding: 'utf-8',
108109
}),
109110
},
111+
{
112+
path: REPOSITORY_APP_PATH,
113+
file: 'defaultmodel.repository.base.ts',
114+
content: fs.readFileSync(
115+
require.resolve('./repositories/defaultmodel.repository.base.ts'),
116+
{
117+
encoding: 'utf-8',
118+
},
119+
),
120+
},
110121
];
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {DefaultCrudRepository} from '@loopback/repository';
2+
import {Defaultmodel} from '../models';
3+
import {DbmemDataSource} from '../datasources';
4+
import {inject} from '@loopback/core';
5+
6+
export class DefaultmodelRepository extends DefaultCrudRepository<
7+
Defaultmodel,
8+
typeof Defaultmodel.prototype.id
9+
> {
10+
constructor(@inject('datasources.dbmem') dataSource: DbmemDataSource) {
11+
super(Defaultmodel, dataSource);
12+
}
13+
}

packages/cli/test/integration/generators/repository.integration.js

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,41 @@ describe('lb4 repository', function() {
384384
/export \* from '.\/decoratordefined.repository';/,
385385
);
386386
});
387+
it('generates a crud repository from custom base class', async () => {
388+
await testUtils
389+
.executeGenerator(generator)
390+
.inDir(SANDBOX_PATH, () =>
391+
testUtils.givenLBProject(SANDBOX_PATH, {
392+
additionalFiles: SANDBOX_FILES,
393+
}),
394+
)
395+
.withArguments(
396+
'--datasource dbmem --model decoratordefined --repositoryBaseClass DefaultmodelRepository',
397+
);
398+
const expectedFile = path.join(
399+
SANDBOX_PATH,
400+
REPOSITORY_APP_PATH,
401+
'decoratordefined.repository.ts',
402+
);
403+
assert.file(expectedFile);
404+
assert.fileContent(
405+
expectedFile,
406+
/import {DefaultmodelRepository} from '.\/defaultmodel.repository.base';/,
407+
);
408+
assert.fileContent(
409+
expectedFile,
410+
/export class DecoratordefinedRepository extends DefaultmodelRepository\</,
411+
);
412+
assert.fileContent(
413+
expectedFile,
414+
/typeof Decoratordefined.prototype.thePK/,
415+
);
416+
assert.file(INDEX_FILE);
417+
assert.fileContent(
418+
INDEX_FILE,
419+
/export \* from '.\/decoratordefined.repository';/,
420+
);
421+
});
387422
});
388423

389424
describe('valid generation of kv repositories', () => {
@@ -395,7 +430,9 @@ describe('lb4 repository', function() {
395430
additionalFiles: SANDBOX_FILES,
396431
}),
397432
)
398-
.withArguments('--datasource dbkv --model Defaultmodel');
433+
.withArguments(
434+
'--datasource dbkv --model Defaultmodel --repositoryBaseClass DefaultKeyValueRepository',
435+
);
399436
const expectedFile = path.join(
400437
SANDBOX_PATH,
401438
REPOSITORY_APP_PATH,
@@ -425,7 +462,9 @@ describe('lb4 repository', function() {
425462
}),
426463
)
427464
.withPrompts(basicPrompt)
428-
.withArguments('--model decoratordefined');
465+
.withArguments(
466+
'--model decoratordefined --repositoryBaseClass DefaultKeyValueRepository',
467+
);
429468
const expectedFile = path.join(
430469
SANDBOX_PATH,
431470
REPOSITORY_APP_PATH,

0 commit comments

Comments
 (0)