generator-loopback
Version:
Yeoman generator for LoopBack
338 lines (308 loc) • 10 kB
JavaScript
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
// Node module: generator-loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var g = require('../lib/globalize');
var yeoman = require('yeoman-generator');
var chalk = require('chalk');
var ActionsMixin = require('../lib/actions');
var helpers = require('../lib/helpers');
var helpText = require('../lib/help');
var validateRequiredName = helpers.validateRequiredName;
var checkPropertyName = helpers.checkPropertyName;
var typeChoices = helpers.getTypeChoices();
var debug = require('debug')('loopback:generator:property');
module.exports = class PropertyGenerator extends ActionsMixin(yeoman) {
// NOTE(bajtos)
// This generator does not track file changes via yeoman,
// as loopback-workspace is editing (modifying) files when
// saving project changes.
constructor(args, opts) {
super(args, opts);
this.modelEmitter = opts && opts.modelEmitter;
this.isInvokedByModelGenerator = opts && opts.isInvokedByModelGenerator;
// yeoman generator doesn't have a function to exit in the middle
// so this flag is introduced to skip executing the followed functions
// whenever we decide to exit.
// See its usage in function `askForPropertyName()`
this.skipExecution = false;
}
help() {
return helpText.customHelp(this, 'loopback_property_usage.txt');
}
loadProject() {
debug('loading project...');
this.loadProjectForGenerator();
debug('loaded project.');
}
loadModels() {
debug('loading models...');
this.loadModelsForGenerator();
debug('loaded models.');
}
askForModel() {
if (this.options.modelName) {
this.modelName = this.options.modelName;
return;
}
var prompts = [
{
name: 'model',
message: g.f('Select the model:'),
type: 'list',
choices: this.editableModelNames,
},
];
return this.prompt(prompts).then(function(answers) {
this.modelName = answers.model;
}.bind(this));
}
findModelDefinition() {
this.modelDefinition = this.projectModels.filter(function(m) {
return m.name === this.modelName;
}.bind(this))[0];
if (!this.modelDefinition) {
var msg = g.f('Model not found: %s', this.modelName);
this.log(chalk.red(msg));
this.async()(new Error(msg));
}
}
// Seperate the property name prompt from others so that the
// generator could exit when:
// - it is invoked by the model generator
// - the property name is empty
askForPropertyName() {
const self = this;
this.name = this.options.propertyName;
var prompts = [
{
name: 'name',
message: g.f('Enter the property name:'),
validate: checkModelPropertyName,
default: this.propDefinition && this.propDefinition.name,
when: function() {
return !this.name && this.name !== 0;
}.bind(this),
},
];
return this.prompt(prompts).then(function(answers) {
debug('answers: %j', answers);
if (self.isInvokedByModelGenerator && !answers.name) {
self.modelEmitter.emit('exitModelGenerator');
// Set the flag to `true` to perform 'exit' for the generator
self.skipExecution = true;
}
this.name = answers.name || this.name;
}.bind(this));
function checkModelPropertyName(name) {
if (self.isInvokedByModelGenerator && !name) return true;
return checkPropertyName(name);
}
}
askForParameters() {
if (this.skipExecution) return;
if (this.modelDefinition.base === 'KeyValueModel') {
var msg = g.f('KeyValueModel does not support model definition ' +
'properties');
this.log(chalk.red(msg));
return this.async(new Error(msg));
}
var prompts = [
{
name: 'type',
message: g.f('Property type:'),
type: 'list',
default: this.propDefinition &&
(Array.isArray(this.propDefinition.type) ?
'array' : this.propDefinition.type),
choices: typeChoices,
},
{
name: 'customType',
message: g.f('Enter the type:'),
required: true,
validate: validateRequiredName,
when: function(answers) {
return answers.type === null;
},
},
{
name: 'itemType',
message: g.f('The type of array items:'),
type: 'list',
default: this.propDefinition &&
this.propDefinition.type &&
Array.isArray(this.propDefinition.type) &&
this.propDefinition.type[0],
choices: typeChoices.filter(function(t) { return t !== 'array'; }),
when: function(answers) {
return answers.type === 'array';
},
},
{
name: 'customItemType',
message: g.f('Enter the item type:'),
validate: validateRequiredName,
when: function(answers) {
return answers.type === 'array' && answers.itemType === null;
},
},
{
name: 'required',
message: g.f('Required?'),
type: 'confirm',
default: false,
},
{
name: 'defaultValue',
message: g.f('Default value[leave blank for none]:'),
default: null,
when: function(answers) {
return answers.type !== null &&
answers.type !== 'buffer' &&
answers.type !== 'any' &&
typeChoices.indexOf(answers.type) !== -1;
},
},
];
return this.prompt(prompts).then(function(answers) {
debug('answers: %j', answers);
if (answers.type === 'array') {
var itemType = answers.customItemType || answers.itemType;
this.type = itemType ? [itemType] : 'array';
} else {
this.type = answers.customType || answers.type;
}
this.propDefinition = {
name: this.name,
type: this.type,
};
if (answers.required) {
this.propDefinition.required = Boolean(answers.required);
}
if (answers.defaultValue === '') {
answers.defaultValue = null;
return;
}
try {
debug('property definition input: %j', this.propDefinition);
coerceDefaultValue(this.propDefinition, answers.defaultValue);
debug('property coercion output: %j', this.propDefinition);
} catch (err) {
debug('Failed to coerce property default value: ', err);
this.log(g.f('Warning: please enter the %s property again. The ' +
'default value provided "%s" is not valid for the selected type: %s',
this.name, answers.defaultValue, this.type));
return this.askForParameters();
}
}.bind(this));
}
property() {
if (this.skipExecution) return;
var done = this.async();
this.modelDefinition.properties.create(this.propDefinition, function(err) {
helpers.reportValidationError(err, this.log);
return done(err);
}.bind(this));
}
saveProject() {
if (this.skipExecution) return;
debug('saving project...');
this.saveProjectForGenerator();
debug('saved project.');
}
emitEventEnd() {
if (this.skipExecution) return;
debug("property generator emits event 'finished'");
if (this.modelEmitter) this.modelEmitter.emit('finished');
}
};
function coerceDefaultValue(propDef, value) {
var itemType;
if (Array.isArray(propDef.type)) {
itemType = propDef.type[0];
propDef.type = 'array';
}
switch (propDef.type) {
case 'string':
if (value === 'uuid' || value === 'guid') {
propDef.defaultFn = value;
} else {
propDef.default = value;
}
break;
case 'number':
propDef.default = castToNumber(value);
break;
case 'boolean':
propDef.default = castToBoolean(value);
break;
case 'date':
if (value.toLowerCase() === 'now') {
propDef.defaultFn = 'now';
} else {
propDef.default = castToDate(value);
}
break;
case 'array':
propDef.type = [itemType];
if (itemType === 'string') {
propDef.default = value.replace(/[\s,]+/g, ',').split(',');
} else if (itemType === 'number') {
propDef.default = value.replace(/[\s,]+/g, ',').split(',')
.map(castToNumber);
} else if (itemType === 'boolean') {
propDef.default = value.replace(/[\s,]+/g, ',').split(',')
.map(castToBoolean);
} else if (itemType === 'date') {
propDef.default = value.replace(/[\s,]+/g, ',').split(',')
.map(castToDate);
} else {
propDef.default = value;
}
break;
case 'geopoint':
if (value.indexOf('lat') !== -1 && value.indexOf('lng') !== -1) {
propDef.default = JSON.parse(value);
} else {
var geo = value.replace(/[\s,]+/g, ',').split(',');
propDef.default = {};
propDef.default.lat = castToNumber(geo[0]);
propDef.default.lng = castToNumber(geo[1]);
}
break;
case 'object':
propDef.default = JSON.parse(value);
break;
default:
propDef.default = value;
}
}
function castToDate(value) {
var dateValue;
var isNumber = /^[0-9]+$/.test(value);
if (isNumber) {
dateValue = new Date(castToNumber(value));
} else {
dateValue = new Date(value);
}
if (isNaN(dateValue.getTime())) {
throw Error(g.f('Invalid default Date value: %s', value));
}
return dateValue;
}
function castToNumber(value) {
var numberValue = Number(value);
if (isNaN(numberValue)) {
throw Error('Invalid default number value: ' + value);
}
return numberValue;
}
function castToBoolean(value) {
if (['true', '1', 't', 'false', '0', 'f'].indexOf(value) === -1) {
throw Error('Invalid default boolean value "' + value +
'". Expected default values: true|false, 1|0, t|f');
}
return (['true', '1', 't'].indexOf(value) !== -1) ? true : false;
}