@hicoder/angular-cli
Version:
Angular UI componenets and service generator. It works with the mean-rest-express package to generate the end to end web application. The input to this generator is the Mongoose schema defined for the express application. mean-rest-express exposes the Res
1,821 lines (1,652 loc) • 95.3 kB
JavaScript
#!/usr/bin/env node --trace-warnings
/*
* Script to create angular UI and service code based on Mongoose schema. This is the command line
* interface for mean-rest-angular package
*
*/
const FIELD_NUMBER_FOR_SELECT_VIEW = 4;
const ejs = require('ejs');
const mongoose = require('mongoose');
const fs = require('fs');
const path = require('path');
const relative = require('relative');
const program = require('commander');
const glob = require('glob');
const minimatch = require('minimatch');
const mkdirp = require('mkdirp');
const readline = require('readline');
const sortedObject = require('sorted-object');
const util = require('util');
const prettier = require('prettier');
const MODE_0666 = parseInt('0666', 8);
const MODE_0755 = parseInt('0755', 8);
const TEMPLATE_DIR = path.join(__dirname, '..', 'templates');
const VERSION = require('../package').version;
const { Selectors } = require('./selectors');
const { getNewFeatures, listViewWidgetToFeatures } = require('./features');
const Util = require('./util');
const {
defaultListWidgets,
defaultListWidgetTypes,
defaultDetailWidgetTypes,
setListViewProperties,
} = require('./default-widgets');
const {
checkCalendarOpts,
calendarOpts,
defaultCalendarOptions,
} = require('./calendar');
const {
slideFieldOpts,
alertFieldOpts,
promotionFieldOpts,
messageFieldOpts,
galleryFieldOpts,
} = require('./featureFields');
const logger = require('./log');
const ROOTDIR = __dirname.replace(/bin$/, 'templates');
const _exit = process.exit;
// Re-assign process.exit because of commander
process.exit = exit;
const views_collection = {}; //views in [schema, briefView, detailView, CreateView, EditView, SearchView, IndexView, associationView] format
const model_collection = {};
const basedirFile = function (relativePath) {
return path.join(__dirname, relativePath);
};
const generatedFile = function (outputDir, prefix, outputFile) {
if (!prefix) prefix = '';
if (prefix !== '' && !outputFile.startsWith('.')) prefix += '-';
let file = prefix + outputFile;
if (outputFile.toLowerCase().startsWith('mdds')) {
// no change if name starts with 'mdds'
file = outputFile;
}
return path.join(outputDir, file);
};
const templates = {
//key:[template_file, output_file_suffix, description, write_options]
//write_options: W: write, A: append
angular: [
'../templates/ui/angular/mdds.angular.json',
'mdds.angular.json',
'angular configuration file',
'W',
],
listStyle: [
'../templates/css/mdds.list-style.css',
'mdds.list-style.css',
'css for list style file',
'W',
],
conf: ['../templates/conf.ts', '.conf.ts', 'module conf file', 'A'],
tokensValue: [
'../templates/tokens.value.ts',
'.tokens.value.ts',
'module token value file',
'A',
],
mainModule: [
'../templates/main.module.ts',
'.module.ts',
'main module file',
'W',
],
mainCoreModule: [
'../templates/main.core.module.ts',
'.core.module.ts',
'main module file',
'W',
],
mainCustModule: [
'../templates/main.cust.module.ts',
'.cust.module.ts',
'main cust module file',
'A',
],
mainExtModule: [
'../templates/main.ext.module.ts',
'.ext.module.ts',
'main external module file',
'A',
],
mainComponent: [
'../templates/main.component.ts',
'.component.ts',
'main component file',
'W',
],
mainComponentHtml: [
'../templates/main.component.html',
'.component.html',
'main component html file',
'W',
],
mainComponentCss: [
'../templates/main.component.css',
'.component.css',
'main component css file',
'W',
],
mainDirective: [
'../templates/main.directive.ts',
'.directive.ts',
'main directive file',
'W',
],
tokens: ['../templates/tokens.ts', '.tokens.ts', 'module token file', 'W'],
routingModule: [
'../templates/routing.module.ts',
'routing.module.ts',
'routing module file',
'W',
],
routingCoreModule: [
'../templates/routing.core.module.ts',
'routing.core.module.ts',
'routing core module file',
'W',
],
routingCorePath: [
'../templates/routing.core.path.ts',
'routing.core.path.ts',
'routing core path file',
'W',
],
routingCustPath: [
'../templates/routing.cust.path.ts',
'routing.cust.path.ts',
'routing cust path file',
'A',
],
schemaBaseService: [
'../templates/schema.base.service.ts',
'.base.service.ts',
'base service file',
'W',
],
schemaService: [
'../templates/schema.service.ts',
'.service.ts',
'service file',
'W',
],
schemaComponent: [
'../templates/schema.component.ts',
'.component.ts',
'component file',
'W',
],
schemaListComponent: [
'../templates/schema-list.component.ts',
'list.component.ts',
'list component file',
'W',
],
schemaListCustComponent: [
'../templates/schema-list.cust.component.ts',
'list.cust.component.ts',
'list customization component file',
'A',
],
schemaListComponentHtml: [
'../templates/schema-list.component.html',
'list.component.html',
'list component html file',
'W',
],
schemaListComponentCss: [
'../templates/schema-list.component.css',
'list.component.css',
'list component css file',
'W',
],
schemaListViewComponent: [
'../templates/schema-list-view.component.ts',
'list-view.component.ts',
'list view component file',
'W',
],
schemaDetail: [
[
'../templates/schema-detail.component.ts',
'detail.component.ts',
'detail component file',
'W',
],
[
'../templates/schema-detail.component.html',
'detail.component.html',
'detail component html file',
'W',
],
[
'../templates/schema-detail.component.css',
'detail.component.css',
'detail component css file',
'W',
],
[
'../templates/schema-detail.cust.component.ts',
'detail.cust.component.ts',
'detail customization component file',
'A',
],
],
schemaDetailShowFieldCompoment: [
'../templates/schema-detail-show-field.component.ts',
'detail-field.component.ts',
'detail show field component file',
'W',
],
schemaDetailShowFieldCompomentHtml: [
'../templates/schema-detail-show-field.component.html',
'detail-field.component.html',
'detail show field component html file',
'W',
],
schemaEditComponent: [
'../templates/schema-edit.component.ts',
'edit.component.ts',
'edit component file',
'W',
],
schemaEditCustComponent: [
'../templates/schema-edit.cust.component.ts',
'edit.cust.component.ts',
'edit customization component file',
'A',
],
schemaEditComponentHtml: [
'../templates/schema-edit.component.html',
'edit.component.html',
'edit component html file',
'W',
],
schemaEditComponentCss: [
'../templates/schema-edit.component.css',
'edit.component.css',
'edit component css file',
'W',
],
mraCss: [
'../templates/mean-express-angular.css',
'mean-express-angular.css',
'mean-rest-angular css file',
'W',
],
};
const PredefinedPatchFields = {
muser_id: { type: String, index: true },
mmodule_name: { type: String, index: true },
permissionTags: { type: [{ type: String }], required: false },
};
const stripDisplayNames = function (viewStr) {
const displayNames = {};
const re = /([^\s\|]+)\[([^\]]*)\]/g; // handle 'field[field displayName]'
const s = viewStr;
let m;
do {
m = re.exec(s);
if (m) {
displayNames[m[1]] = m[2];
}
} while (m);
const viewStrDisplayNameHandled = viewStr.replace(/\[[^\]]*\]/g, '');
return [displayNames, viewStrDisplayNameHandled];
};
const stripFieldHidden = function (viewStr) {
const fieldHidden = {};
const re = /\(([^\)]*)\)/g; // handle 'field<fieldMeta>'
const s = viewStr;
let m;
do {
m = re.exec(s);
if (m) {
fieldHidden[m[1]] = true;
}
} while (m);
const viewStrHiddenHandled = viewStr.replace(/[\(\)]/g, '');
return [fieldHidden, viewStrHiddenHandled];
};
const stripFieldMeta = function (viewStr) {
const fieldMeta = {};
const re = /([^\s\|]+)<([^>]*)>/g; // handle 'field<fieldMeta>'
const s = viewStr;
let m;
do {
m = re.exec(s);
if (m) {
fieldMeta[m[1]] = m[2];
}
} while (m);
const viewStrMetaHandled = viewStr.replace(/<[^\>]*>/g, '');
return [fieldMeta, viewStrMetaHandled];
};
const analizeSearch = function (
briefView,
listCategoryFields,
listCategoryFieldsNotShown,
listSearchFieldsBlackList,
ownSearchStringFields,
listSearchIDLookup,
api,
listActionButtons
) {
let showSearchBox = false;
let stringBoxFields = [];
let ownSearchFields = [];
let noMoreSearchArea = true;
let hasArchive = false;
let hasArray = false;
let hasDate = false;
let IDLookup = listSearchIDLookup;
for (let field of briefView) {
if (field.hidden) continue;
if (field.exclusiveRequired || field.exclusiveRequiredWith) continue;
if (listCategoryFields.includes(field.fieldName)) continue;
if (field.picture || field.file) continue;
if (listSearchFieldsBlackList.includes(field.fieldName)) {
continue;
}
if (field.type === 'AngularSelector') {
continue;
}
if (field.fieldName === '_id') {
IDLookup = true;
continue; // cannot text search based on "_id", but enable IDLookup
}
if (field.type === 'SchemaDate') {
hasDate = true;
}
if (field.type === 'SchemaArray') {
if (
field.elementType === 'SchemaString' &&
!field.elementMultiSelect && // will provide select for field select
!field.hint && // will provide hint for field select
!ownSearchStringFields.includes(field.fieldName)
) {
showSearchBox = true;
stringBoxFields.push(field);
} else {
hasArray = true;
noMoreSearchArea = false;
ownSearchFields.push(field);
}
} else if (
field.type == 'SchemaString' &&
!ownSearchStringFields.includes(field.fieldName)
) {
showSearchBox = true;
stringBoxFields.push(field);
} else {
noMoreSearchArea = false;
ownSearchFields.push(field);
}
}
if (api.includes('A') && listActionButtons[3]) {
noMoreSearchArea = false;
hasArchive = true;
}
return {
showSearchBox,
stringBoxFields,
noMoreSearchArea,
ownSearchFields,
IDLookup,
hasArchive,
hasArray,
hasDate,
};
};
const getWidgetCustomTemplate = function (
widgetCustomTemplates,
widgetCategory,
widgetname
) {
let customTemplates =
!widgetCustomTemplates ||
!widgetCustomTemplates[widgetCategory] ||
widgetCustomTemplates[widgetCategory][widgetname];
if (typeof customTemplates === 'boolean') {
customTemplates = {};
} else {
console.log(
`Using custome templates for ${widgetCategory} ${widgetname} widget.`
);
}
return customTemplates;
};
const getComponentClassAndFileName = function (
widgetCategory,
widgetname,
componentType
) {
let component_file_name = `${widgetCategory}-widget-${widgetname}`;
if (
widgetCategory === 'list' &&
componentType &&
['general', 'select', 'sub', 'association'].includes(componentType)
) {
component_file_name = `${widgetCategory}-${componentType}`;
}
if (
widgetCategory === 'detail' &&
componentType &&
['general', 'select', 'sub', 'association', 'pop'].includes(componentType)
) {
component_file_name = `${widgetCategory}-${componentType}`;
}
let ComponentClassName = component_file_name
.split('-')
.map((x) => Util.capitalizeFirst(x))
.join('');
return [ComponentClassName, component_file_name];
};
const generateSourceFile = function (keyname, template, renderObj, outputDir) {
let renderOptions = { root: ROOTDIR };
let templateFile = basedirFile(template[0]);
let output = generatedFile(outputDir, keyname, template[1]);
let description = template[2];
let options = template[3];
// console.info('Generating %s for "%s"...', description, keyname);
ejs.renderFile(templateFile, renderObj, renderOptions, (err, str) => {
if (err) {
logger.error(
`ERROR! Error happens when generating ${description} for ${keyname}: ${err}`
);
return;
}
let beautified_str = str;
let beautify_option = {
indent_size: 2,
space_in_empty_paren: true,
preserve_newlines: false,
};
const extension = output.split('.').pop();
switch (extension) {
case 'js':
beautified_str = prettier.format(str, { parser: 'babel' });
break;
case 'ts':
beautified_str = prettier.format(str, { parser: 'typescript' });
break;
case 'html':
beautified_str = prettier.format(str, { parser: 'html' });
break;
case 'css':
beautified_str = prettier.format(str, { parser: 'css' });
break;
default:
beautified_str = str;
}
if (options == 'W') {
write(output, beautified_str);
} else if (options == 'A') {
append(output, beautified_str);
}
});
};
const generateWidgetSource = function (
widgetCategory,
widgetname,
widgets,
schemaName,
schemaObj,
dirname,
componentType
) {
let widgetDef;
if (widgets) {
widgetDef = widgets[widgetname];
if (!widgetDef) {
logger.error(
`ERROR! widget definition for ${componentType} ${widgetname} is not given in wigets definition for ${widgetCategory}.`
);
_exit(1);
}
}
// Per widget settings. Remember to reset these values after done.
schemaObj.widgetDef = widgetDef;
schemaObj.customTemplates = getWidgetCustomTemplate(
schemaObj.widgetCustomTemplates,
widgetCategory,
widgetname
);
let [ComponentClassName, component_file_name] = getComponentClassAndFileName(
widgetCategory,
widgetname,
componentType
);
schemaObj.ComponentClassName = ComponentClassName;
schemaObj.component_file_name = component_file_name;
const tsTemplate = `../templates/widgets/${widgetCategory}/${widgetname}/${widgetname}.component.ts`;
const htmlTemplate = `../templates/widgets/${widgetCategory}/${widgetname}/${widgetname}.component.html`;
const cssTemplate = `../templates/widgets/${widgetCategory}/${widgetname}/${widgetname}.component.css`;
if (
!fs.existsSync(basedirFile(tsTemplate)) ||
!fs.existsSync(basedirFile(htmlTemplate)) ||
!fs.existsSync(basedirFile(cssTemplate))
) {
console.log(
`Error! template files for ${widgetCategory} widget '${widgetname}' don't exist! Ignore...`
);
console.log(` Expecting:${tsTemplate} and ${htmlTemplate}`);
console.log(` -- ${tsTemplate}`);
console.log(` -- ${htmlTemplate}`);
console.log(` -- ${cssTemplate}`);
_exit(1);
}
const tsFile = [
tsTemplate,
`${component_file_name}.component.ts`,
`${component_file_name} component file`,
'W',
];
const htmlFile = [
htmlTemplate,
`${component_file_name}.component.html`,
`${component_file_name} component html file`,
'W',
];
const cssFile = [
cssTemplate,
`${component_file_name}.component.css`,
`${component_file_name} component css file`,
'W',
];
generateSourceFile(schemaName, tsFile, schemaObj, dirname);
generateSourceFile(schemaName, htmlFile, schemaObj, dirname);
generateSourceFile(schemaName, cssFile, schemaObj, dirname);
schemaObj.customTemplates = {};
schemaObj.widgetDef = undefined;
return [component_file_name, ComponentClassName];
};
const formatValidatorValue = function (s) {
let str = s;
let textsToReplace = [
[/\.$/, ''],
[' for path `{PATH}`', ''],
['Path `{PATH}` (`{VALUE}`)', 'Value'],
['`{VALUE}`', 'Value'],
];
for (let r of textsToReplace) {
str = str.replace(r[0], r[1]);
}
if (str.includes('`{') || str.includes('}`')) {
logger.error(
`ERROR! Validator error message "${s}" is processed to "${str}" but needs to be processed further`
);
}
// console.log('==', s)
// console.log('==', str)
return str;
};
const formatValidatorRegex = function (s) {
if (s instanceof RegExp) {
return s.source;
}
return str;
};
const getPrimitiveField = function (fieldSchema) {
let primitiveField = {
type: fieldSchema.constructor.name,
defaultValue: fieldSchema.options.default,
jstype: undefined,
numberMin: undefined,
numberMinMsg: undefined,
numberMax: undefined,
numberMaxMsg: undefined,
maxlength: undefined,
maxlengthMsg: undefined,
minlength: undefined,
minlengthMsg: undefined,
enumValues: undefined,
enumMsg: undefined,
match: undefined,
matchMsg: undefined,
ref: undefined,
Ref: undefined,
RefCamel: undefined,
editor: false,
mraType: '',
mraDate: ['date'], // show date only. use 'date time' to show both date and time.
urlDisplay: '',
textarea: false,
mraEmailRecipient: false,
flagDate: false,
flagRef: false,
flagPicture: false,
aspectRatio: 0,
flagFile: false,
flagSharable: false,
calendarTitle: false,
calendarGroup: false,
calendarStartTime: false,
calendarEndTime: false,
calendarRepeat: false,
slideTitle: false,
slideSubTitle: false,
slideDescription: false,
slidePicture: false,
slideLinkURL: false,
slideLinkDisplay: false,
alertDescription: false,
alertLinkDisplay: false,
alertLinkURL: false,
alertLinkShow: false,
alertLinkColor: false,
promotionTitle: false,
promotionDetails: false,
exclusiveRequired: false,
exclusiveRequiredWith: '',
};
// console.log('fieldSchema.validators', fieldSchema.validators)
switch (primitiveField.type) {
case 'SchemaString':
primitiveField.jstype = 'string';
if (fieldSchema.validators)
fieldSchema.validators.forEach((val) => {
if (val.type == 'maxlength' && typeof val.maxlength === 'number') {
primitiveField.maxlength = val.maxlength;
primitiveField.maxlengthMsg = formatValidatorValue(val.message);
}
if (val.type == 'minlength' && typeof val.minlength === 'number') {
primitiveField.minlength = val.minlength;
primitiveField.minlengthMsg = formatValidatorValue(val.message);
}
if (
val.type == 'enum' &&
Array.isArray(val.enumValues) &&
val.enumValues.length > 0
) {
primitiveField.enumValues = val.enumValues;
primitiveField.enumMsg = formatValidatorValue(val.message);
}
if (val.type == 'regexp') {
primitiveField.match = formatValidatorRegex(val.regexp);
primitiveField.matchMsg = formatValidatorValue(val.message);
}
});
if (fieldSchema.options.editor == true) {
primitiveField.editor = true;
} else if (fieldSchema.options.textarea == true) {
primitiveField.textarea = true;
} else if (fieldSchema.options.mraEmailRecipient == true) {
primitiveField.mraEmailRecipient = true;
} else if (fieldSchema.options.mraType) {
primitiveField.mraType = fieldSchema.options.mraType.toLowerCase();
switch (primitiveField.mraType) {
case 'picture':
primitiveField.flagPicture = true;
primitiveField.flagSharable = !!fieldSchema.options.mraSharable;
primitiveField.aspectRatio = fieldSchema.options.aspectRatio || 0;
break;
case 'file':
primitiveField.flagFile = true;
primitiveField.flagSharable = !!fieldSchema.options.mraSharable;
break;
case 'httpurl':
if (fieldSchema.options.urlDisplay) {
primitiveField.urlDisplay = fieldSchema.options.urlDisplay;
}
break;
case 'email':
break;
default:
logger.warning(
`Unrecoganized mraType for SchemaString: ${fieldSchema.options.mraType}. Ignore...`
);
}
}
break;
case 'SchemaBoolean':
primitiveField.jstype = 'boolean';
break;
case 'SchemaNumber':
primitiveField.jstype = 'number';
if (fieldSchema.options.mraType) {
primitiveField.mraType = fieldSchema.options.mraType.toLowerCase();
switch (primitiveField.mraType) {
case 'currency':
break;
default:
logger.warning(
`Unrecoganized mraType for SchemaNumber: ${fieldSchema.options.mraType}. Ignore...`
);
}
}
if (fieldSchema.validators)
fieldSchema.validators.forEach((val) => {
if (val.type == 'min' && typeof val.min === 'number') {
primitiveField.numberMin = val.min;
primitiveField.numberMinMsg = formatValidatorValue(val.message);
}
if (val.type == 'max' && typeof val.max === 'number') {
primitiveField.numberMax = val.max;
primitiveField.numberMaxMsg = formatValidatorValue(val.message);
}
});
break;
case 'ObjectId':
primitiveField.jstype = 'string';
if (fieldSchema.options.ref) {
primitiveField.RefCamel = Util.capitalizeFirst(fieldSchema.options.ref);
primitiveField.ref = fieldSchema.options.ref.toLowerCase();
primitiveField.Ref = Util.capitalizeFirst(primitiveField.ref);
primitiveField.flagRef = true;
}
break;
case 'SchemaDate':
primitiveField.jstype = 'string';
primitiveField.flagDate = true;
if (fieldSchema.options.mraType) {
primitiveField.mraType = fieldSchema.options.mraType; //https://angular.io/api/common/DatePipe
} else {
primitiveField.mraType = 'medium'; // 'medium': equivalent to 'MMM d, y, h:mm:ss a' (Jun 15, 2015, 9:03:01 AM).
}
if (fieldSchema.options.mraDate) {
primitiveField.mraDate = fieldSchema.options.mraDate
.trim()
.split(/\s+/)
.map((x) => x.toLowerCase());
}
break;
default:
logger.warning(`Field type ${primitiveField.type} is not recoganized...`);
}
return primitiveField;
};
const generateViewPicture = function (
API,
schemaName,
viewStr,
schema,
validators,
indexViewNames,
selectors,
fieldMeta,
features
) {
const [field2Meta, viewStrMetaHandled] = stripFieldMeta(viewStr);
const [displayNames, viewStrDisplayNameHandled] =
stripDisplayNames(viewStrMetaHandled);
const [fieldHidden, viewStrPure] = stripFieldHidden(
viewStrDisplayNameHandled
);
//process | in viewStr
let fieldGroups = [];
if (viewStrPure.indexOf('|') > -1) {
let strGroups = viewStrPure.split('|');
for (let str of strGroups) {
let arr = str.match(/\S+/g);
if (arr) {
arr = arr.filter((x) => !fieldHidden[x]);
if (arr.length > 0) {
fieldGroups.push(arr);
}
}
}
}
let viewDef = viewStrPure.replace(/\|/g, ' ').match(/\S+/g) || [];
if (fieldGroups.length == 0) {
//no grouping
if (API === 'L') {
fieldGroups.push(viewDef); // all elements as a group
} else {
for (let e of viewDef) {
if (!fieldHidden[e]) {
fieldGroups.push([e]); //each element as a group
}
}
}
}
let view = [];
let viewMap = {};
let schFeatures = {
hasDate: false,
hasRef: false,
hasEditor: false,
hasRequiredMultiSelection: false,
hasRequiredArray: false,
hasRequiredMap: false,
hasFileUpload: false,
hasEmailing: false,
hasMultiSelect: false,
hasCalendar: false,
};
for (let item of viewDef) {
let showDisplayName = true;
let hidden = !!fieldHidden[item];
let usedMeta = field2Meta[item];
let validatorArray;
if (validators && Array.isArray(validators[item])) {
validatorArray = validators[item];
}
let isIndexField = false;
if (indexViewNames.includes(item)) isIndexField = true;
let requiredField = false;
let fieldDescription = '';
let keyDescription = '';
let valueDescription = '';
let importantInfo = false;
//for array
let elementMultiSelect = false;
let parentType;
let mapKey;
let sortable = false;
let sortField = '';
let selector;
let meta = {};
let primitiveField = {
mraType: '',
editor: false,
textarea: false,
mraEmailRecipient: false,
flagDate: false,
flagRef: false,
flagPicture: false,
flagFile: false,
flagSharable: false,
};
let fieldSchema;
if (item !== '_id' && item in schema.paths) {
if (usedMeta && fieldMeta && fieldMeta[usedMeta]) {
meta = fieldMeta[usedMeta];
if (typeof meta['showDisplayName'] === 'boolean') {
showDisplayName = meta.showDisplayName;
}
for (let sel of [meta.pipe, meta.directive, meta.selector]) {
if (sel) {
if (selectors.hasSelector(sel)) {
selector = selectors.getSelector(sel);
selector.usedCandidate(API);
} else {
logger.warning(
`Selector ${sel} for Field ${item} is not defined. Skipped...`
);
continue;
}
}
}
} else if (usedMeta) {
logger.warning(
`Field meta ${usedMeta} is not defined for field ${item}...`
);
}
fieldSchema = schema.paths[item];
parentType = fieldSchema.constructor.name;
requiredField = fieldSchema.originalRequiredValue === true ? true : false;
//TODO: required could be a function
defaultValue = fieldSchema.defaultValue || fieldSchema.options.default;
if (fieldSchema.options.description) {
//scope of map key defined
fieldDescription = fieldSchema.options.description;
}
if (fieldSchema.options.keyDescription) {
//scope of map key defined
keyDescription = fieldSchema.options.keyDescription;
}
if (fieldSchema.options.valueDescription) {
//scope of map key defined
valueDescription = fieldSchema.options.valueDescription;
}
if (fieldSchema.options.important) {
//scope of map key defined
importantInfo = fieldSchema.options.important;
}
function setFeatures(schFeatures, primitiveField) {
if (primitiveField.flagDate) schFeatures.hasDate = true;
if (primitiveField.flagRef) schFeatures.hasRef = true;
if (primitiveField.editor) schFeatures.hasEditor = true;
if (primitiveField.flagPicture || primitiveField.flagFile)
schFeatures.hasFileUpload = true;
if (primitiveField.mraEmailRecipient) schFeatures.hasEmailing = true;
}
switch (parentType) {
case 'SchemaString':
case 'SchemaBoolean':
case 'SchemaNumber':
case 'ObjectId':
case 'SchemaDate':
primitiveField = getPrimitiveField(fieldSchema);
setFeatures(schFeatures, primitiveField);
sortable = true;
sortField = item;
if (
primitiveField.editor ||
primitiveField.flagPicture ||
primitiveField.flagFile
) {
sortable = false;
}
break;
case 'SchemaArray':
primitiveField = getPrimitiveField(fieldSchema.caster);
setFeatures(schFeatures, primitiveField);
//rewrite the default value for array
let defaultInput = fieldSchema.options.default;
if (Array.isArray(defaultInput)) {
defaultValue = defaultInput;
} else {
defaultValue = undefined;
}
if (fieldSchema.options.elementunique && primitiveField.enumValues) {
elementMultiSelect = true;
schFeatures.hasMultiSelect = true;
if (requiredField) {
schFeatures.hasRequiredMultiSelection = true;
}
} else {
if (requiredField) {
schFeatures.hasRequiredArray = true;
}
}
break;
case 'SchemaMap':
case 'Map':
primitiveField = getPrimitiveField(fieldSchema['$__schemaType']);
setFeatures(schFeatures, primitiveField);
//rewrite the default value for array
let defaultMap = fieldSchema.options.default;
if (typeof defaultMap == 'object') {
defaultValue = defaultMap;
} else {
defaultValue = undefined;
}
if (requiredField) {
schFeatures.hasRequiredMap = true;
}
if (fieldSchema.options.key) {
//scope of map key defined
mapKey = fieldSchema.options.key;
}
break;
default:
logger.warning(
`Field type ${primitiveField.type} is not recoganized for field ${item}...`
);
}
if (parentType == 'SchemaMap') {
parentType = 'Map';
}
} else if (item === '_id') {
parentType = 'SchemaString';
primitiveField.jstype = 'string';
} else if (item in schema.virtuals) {
//Handle as a string
parentType = 'SchemaString';
primitiveField.jstype = 'string';
} else if (usedMeta && selectors && selectors.hasSelector(usedMeta)) {
// selector type:
parentType = 'AngularSelector';
selector = selectors.getSelector(usedMeta);
selector.usedCandidate(API);
showDisplayName = selector.showDisplayName;
if (selector.sortField) {
sortable = true;
sortField = selector.sortField;
}
} else if (usedMeta) {
logger.warning(
`Selector ${usedMeta} for Field ${item} is not defined. Skipped...`
);
continue;
} else {
logger.warning(
`Field ${item} is not defined in schema ${schemaName}. Skipped...`
);
continue;
}
const DN = displayNames[item] || Util.camelToDisplay(item);
const dn = DN.toLowerCase();
let fieldPicture = {
fieldName: item,
FieldName: Util.capitalizeFirst(item),
displayName: DN,
displayname: dn,
showDisplayName,
hidden,
type: parentType,
jstype: primitiveField.jstype,
numberMin: primitiveField.numberMin,
numberMinMsg: primitiveField.numberMinMsg,
numberMax: primitiveField.numberMax,
numberMaxMsg: primitiveField.numberMaxMsg,
maxlength: primitiveField.maxlength,
maxlengthMsg: primitiveField.maxlengthMsg,
minlength: primitiveField.minlength,
minlengthMsg: primitiveField.minlengthMsg,
match: primitiveField.match,
matchMsg: primitiveField.matchMsg,
enumValues: primitiveField.enumValues,
enumMsg: primitiveField.enumMsg,
ref: primitiveField.ref,
Ref: primitiveField.Ref,
RefCamel: primitiveField.RefCamel,
editor: primitiveField.editor, //rich format text
mraType: primitiveField.mraType,
mraDate: primitiveField.mraDate, // array of 'time', 'date'
textarea: primitiveField.textarea, // big text input
mraEmailRecipient: primitiveField.mraEmailRecipient, // an email field an can receive email
picture: primitiveField.flagPicture, // a picture field
aspectRatio: primitiveField.aspectRatio || 0,
file: primitiveField.flagFile, // a file field
sharable: primitiveField.flagSharable, // picture or file is sharable
urlDisplay: primitiveField.urlDisplay, // display text for httpUrl type
//TODO: required could be a function
required: requiredField,
defaultValue: defaultValue,
description: fieldDescription,
keyDescription,
valueDescription,
important: importantInfo,
validators: validatorArray,
isIndexField: isIndexField,
sortable,
sortField,
//for array and map
elementType: primitiveField.type,
elementMultiSelect,
//for map
mapKey,
//selector
selector,
meta,
};
// Handle feature options.
if (fieldSchema) {
// Use the field level definition for these options
for (let opts of [
calendarOpts,
alertFieldOpts,
slideFieldOpts,
messageFieldOpts,
promotionFieldOpts,
galleryFieldOpts,
['exclusiveRequired', 'exclusiveRequiredWith'],
]) {
for (let opt of opts) {
fieldPicture[opt] = fieldSchema.options[opt];
}
}
}
view.push(fieldPicture);
viewMap[item] = fieldPicture;
}
// Check calendar options.
if (features.getUsedFeatures()['hasCalendar' + API]) {
checkCalendarOpts(API, schemaName, view);
}
for (let f of view) {
//handle Map Key
if (f.mapKey) {
//example: this.<anotherfield>.<subfield>
fInfo = f.mapKey.split('.');
if (fInfo.length <= 1) {
logger.warning(
' -- mapKey for',
f.fieldName,
'is not in correct format.'
);
continue;
}
if (fInfo[0] != 'this') {
logger.warning(
' -- mapKey for',
f.fieldName,
"doesn't refer to same schema field."
);
continue;
}
let refField = fInfo[1];
if (refField in viewMap && viewMap[refField].type == 'ObjectId') {
if (fInfo.length <= 2) {
logger.warning(
' -- mapKey for',
f.fieldName,
'refers to a reference but no sub field given.'
);
continue;
}
f.mapKeyInfo = {
type: 'ObjectId',
refSchema: viewMap[refField].ref,
refName: refField,
refService: viewMap[refField].Ref + 'Service',
refSubField: fInfo[2],
};
continue;
}
if (refField in viewMap && viewMap[refField].type == 'SchemaArray') {
f.mapKeyInfo = { type: 'SchemaArray', name: refField };
continue;
}
//console.log(' -- mapKey for', f.fieldName, '. No idea how to get the key from: ', refField);
}
}
// generated form groups (fake fields) replacing real fields.
let formGroups = {
// <selector name>: {fields: [field1, field2...]}
};
for (let f of view) {
if (f.exclusiveRequired) {
formGroups[f.fieldName] = {
fields: [f],
};
}
}
for (let f of view) {
if (f.exclusiveRequiredWith && formGroups[f.exclusiveRequiredWith]) {
formGroups[f.exclusiveRequiredWith]['fields'].push(f);
} else if (f.exclusiveRequiredWith) {
logger.warning(`${schemaName} ${f.fieldName} in ${API} has exclusiveRequiredWith but the exclusiveRequired field is not found.`);
f.exclusiveRequiredWith = '';
}
}
for (let f of view) {
if (f.exclusiveRequired) {
f.formGroup = formGroups[f.fieldName].fields;
}
}
let viewGroups = [];
for (let grp of fieldGroups) {
let arr = grp
.filter((x) => x in viewMap)
.map((x) => viewMap[x])
.filter(
// Remove exclusiveRequiredWith fields
(x) =>
!x.exclusiveRequiredWith || !(x.exclusiveRequiredWith in formGroups)
);
if (arr.length > 0) viewGroups.push(arr);
}
for (let feature in schFeatures) {
if (schFeatures[feature]) {
// if true
features.usedCandidate(feature, API);
}
}
return [viewGroups, view];
};
const setFieldProperty = function (view, fieldArr, include, property, value) {
if (!fieldArr) return;
let arr = fieldArr.slice(0);
for (let f of view) {
if (arr.includes(f.fieldName) === include) {
f[property] = value;
}
}
};
const getAppendFields = function (viewArr, idx) {
let fields = [];
for (let i = idx + 1; i < viewArr.length; i++) {
let field = viewArr[i];
if (field.meta.listShow === 'append') {
fields.append(field);
} else {
break;
}
}
return fields;
};
const getLoginUserPermission = function (permission) {
let othersPermisson = permission['others'];
if (typeof othersPermission !== 'string') {
othersPermission = ''; //not permitted
}
let ownPermisson = permission['own'];
if (typeof ownPermisson !== 'string') {
ownPermisson = ''; //not permitted
}
return { others: othersPermission, own: ownPermisson };
};
const getPermission = function (authz, identity, schemaName) {
let schemaAuthz;
if (schemaName in authz) {
//use the permission definition for the schema
schemaAuthz = authz[schemaName];
}
let moduleAuthz;
if ('module-authz' in authz) {
//use the permission definition for the module
moduleAuthz = authz['module-authz'];
}
let identityPermission;
if (schemaAuthz && identity in schemaAuthz) {
identityPermission = schemaAuthz[identity];
} else if (moduleAuthz && identity in moduleAuthz) {
identityPermission = moduleAuthz[identity];
}
if (identity == 'Anyone') {
if (
typeof identityPermission === 'string' ||
typeof identityPermission === 'undefined'
) {
return identityPermission;
} else {
return ''; //not permitted
}
} else if (identity == 'LoginUser') {
if (
typeof identityPermission === 'string' ||
typeof identityPermission === 'undefined'
) {
return { others: identityPermission, own: identityPermission };
} else if (typeof identityPermission === 'object') {
return getLoginUserPermission(identityPermission);
} else {
return { others: '', own: '' }; //not permitted
}
}
return identityPermission;
};
const getSchemaPermission = function (schemaName, authz) {
let anyonePermission = getPermission(authz, 'Anyone', schemaName);
let permission = '';
if (typeof anyonePermission == 'undefined') {
permission = ''; //not permitted
} else {
permission = anyonePermission;
}
return permission;
};
const setListViewObj = function (schemaObj) {
let clickItemAction = '';
if (schemaObj.api.includes('R') && schemaObj.listToDetail === 'click') {
clickItemAction = 'detail';
}
let cardHasLink =
schemaObj.api.includes('R') && schemaObj.listToDetail === 'link';
let canUpdate = schemaObj.api.includes('U');
let canDelete = schemaObj.api.includes('D');
let canArchive = schemaObj.api.includes('A');
let canCheck = schemaObj.api.includes('D') || schemaObj.api.includes('A');
let includeSubDetail =
(schemaObj.detailSubView.length != 0 && schemaObj.api.includes('R')) &&
schemaObj.listIncludeSubDetail;
let listViewObj = {
clickItemAction,
cardHasLink,
cardHasSelect: false,
includeSubDetail,
canUpdate,
canDelete,
canArchive,
canCheck,
itemMultiSelect: true,
majorUi: false,
};
schemaObj.listViewObj = listViewObj;
return schemaObj;
};
// CLI
around(program, 'optionMissingArgument', function (fn, args) {
program.outputHelp();
fn.apply(this, args);
return { args: [], unknown: [] };
});
before(program, 'outputHelp', function () {
// track if help was shown for unknown option
this._helpShown = true;
});
before(program, 'unknownOption', function () {
// allow unknown options if help was shown, to prevent trailing error
this._allowUnknownOption = this._helpShown;
// show help if not yet shown
if (!this._helpShown) {
program.outputHelp();
}
});
let givenProgramName = process.argv[1];
let programName = path.basename(process.argv[1]);
if (programName === 'hg-angular-cli') {
// called inside hg cli
programName = 'hg angular-gen';
}
program
.name(`${programName}`)
.description('generate Angular UI components with given input schema')
.version(VERSION, ' --version')
.usage('[options] inputfile')
.option(
'-m, --module <module_name>',
'module name generated for the given schemas. Default is schema file name.'
)
.option(
'-a, --api <api_base>',
'api base that will be used for rest calls. Default is "/api/<module_name>".'
)
.option('-o, --output <output_dir>', 'output directory of generated files')
.option(
'-v, --view <view name>',
'admin, or public. Define the views to generate.'
)
.option(
'-f, --framework <ui framework>',
'Angular, React. Default is Angular.'
)
.option(
'-d, --design <ui design>',
'For Angular - Bootstrap, AngularMeterial, ngBootstrap. Default is Bootstrap'
)
.option(
'-c, --conf',
'Configuration for the given framework. -f (--framework) must be provided.'
)
.parse(process.argv);
if (!exit.exited) {
main();
}
/**
* Install an around function; AOP.
*/
function around(obj, method, fn) {
var old = obj[method];
obj[method] = function () {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) args[i] = arguments[i];
return fn.call(this, old, args);
};
}
/**
* Install a before function; AOP.
*/
function before(obj, method, fn) {
var old = obj[method];
obj[method] = function () {
fn.call(this);
old.apply(this, arguments);
};
}
/**
* Prompt for confirmation on STDOUT/STDIN
*/
function confirm(msg, callback) {
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question(msg, function (input) {
rl.close();
callback(/^y|yes|ok|true$/i.test(input));
});
}
/**
* Copy file from template directory.
*/
function copyTemplate(from, to) {
write(to, fs.readFileSync(path.join(TEMPLATE_DIR, from), 'utf-8'));
}
/**
* Copy multiple files from template directory.
*/
function copyTemplateMulti(fromDir, toDir, nameGlob) {
fs.readdirSync(path.join(TEMPLATE_DIR, fromDir))
.filter(minimatch.filter(nameGlob, { matchBase: true }))
.forEach(function (name) {
copyTemplate(path.join(fromDir, name), path.join(toDir, name));
});
}
/**
* Check if the given directory `dir` is empty.
*
* @param {String} dir
* @param {Function} fn
*/
function emptyDirectory(dir, fn) {
fs.readdir(dir, function (err, files) {
if (err && err.code !== 'ENOENT') throw err;
fn(!files || !files.length);
});
}
/**
* Graceful exit for async STDIO
*/
function exit(code) {
// flush output for Node.js Windows pipe bug
// https://github.com/joyent/node/issues/6247 is just one bug example
// https://github.com/visionmedia/mocha/issues/333 has a good discussion
function done() {
if (!draining--) _exit(code);
}
var draining = 0;
var streams = [process.stdout, process.stderr];
exit.exited = true;
streams.forEach(function (stream) {
// submit empty write request and wait for completion
draining += 1;
stream.write('', done);
});
done();
}
/**
* Determine if launched from cmd.exe
*/
function launchedFromCmd() {
return process.platform === 'win32' && process.env._ === undefined;
}
/**
* Load template file.
*/
function loadTemplate(name) {
var contents = fs.readFileSync(
path.join(__dirname, '..', 'templates', name + '.ejs'),
'utf-8'
);
var locals = Object.create(null);
function render() {
return ejs.render(contents, locals, {
escape: util.inspect,
});
}
return {
locals: locals,
render: render,
};
}
function getUiArch() {
let uiFramework = program.opts().framework || 'angular';
uiFramework = uiFramework.toLowerCase();
let uiDesign;
switch (uiFramework) {
case 'angular':
uiDesign = program.opts().design || 'bootstrap';
break;
case 'react':
uiDesign = program.opts().design || 'bootstrap';
break;
default:
}
uiDesign = uiDesign.toLowerCase();
return [uiFramework, uiDesign];
}
function getConfiguration(uiFramework, files) {
let conf = { styles: [], scripts: [] };
for (let file of files) {
file = path.resolve(file);
let json = require(file);
conf.styles = conf.styles.concat(json.styles);
conf.scripts = conf.scripts.concat(json.scripts);
}
conf.styles = conf.styles.filter((x, i) => {
return conf.styles.indexOf(x) === i;
});
conf.scripts = conf.scripts.filter((x, i) => {
return conf.scripts.indexOf(x) === i;
});
console.log('');
console.log(
'*** Please include the following configuration to your project angular.json file:'
);
console.log(' -- architect.build.options.styles');
console.log(JSON.stringify(conf.styles, null, 4));
console.log(
' !!! Please also include one mdds.list-style.css file from your schema dir.\n\n'
);
console.log(' -- architect.build.options.scripts');
console.log(JSON.stringify(conf.scripts, null, 4));
}
/**
* Generate sample configuration for given framework
*/
function configurationGen() {
let [uiFramework, uiDesign] = getUiArch();
let configFile;
switch (uiFramework) {
case 'angular':
configFile = 'mdds.angular.json';
break;
default:
console.error(
`Configuration for framework ${uiFramework} is not supported.`
);
_exit(1);
}
// output directory
let outputDir;
if (!program.opts().output) {
if (fs.existsSync('src/app')) {
outputDir = 'src/app';
console.info(
'NOTE: Output directory is not provided. Use "src/app" directory to check configuration...'
);
} else {
outputDir = './';
console.info(
'NOTE: Output directory is not provided. Use the current to check configuration...'
);
}
} else {
outputDir = program.opts().output;
if (!fs.existsSync(outputDir)) {
console.info(
`Target project output directory ${outputDir} does not exist.`
);
}
}
console.log(`Checking mdds.angular.json under ${outputDir}...`);
glob(outputDir + `/**/${configFile}`, {}, (err, files) => {
if (err) {
console.log(`Error when checking configuration: ${err.stack}`);
_exit(1);
}
console.log(`-- The following configuration files are found: `, files);
getConfiguration(uiFramework, files);
});
}
/**
* Main program.
*/
function main() {
if (program.opts().conf) {
return configurationGen();
}
let inputFile = program.args.shift();
if (!inputFile) {
console.error('Argument error.');
program.outputHelp();
_exit(1);
}
if (!fs.existsSync(inputFile)) {
console.error('Error: cannot find input file: ' + inputFile);
_exit(1);
}
if (!inputFile.endsWith('.js')) {
console.error(
'Error: input file must be a .js file with Mongoose schema defined.'
);
_exit(1);
}
// ui framework
let [uiFramework, uiDesign] = getUiArch();
const moduleFeatures = getNewFeatures(uiFramework, uiDesign);
let uiTemplateDir = path.join(ROOTDIR, 'ui', uiFramework, uiDesign);
if (!fs.existsSync(uiTemplateDir)) {
console.error(
`Combination of UI Framework "${
program.opts().framework
}" and UI Design "${program.opts().design}" is not supported.`
);
_exit(1);
}
const funcs = {
capitalizeFirst: Util.capitalizeFirst,
getAppendFields,
};
let moduleName;
if (!program.opts().module) {
let startPosition = inputFile.lastIndexOf(path.sep) + 1;
moduleName = inputFile.substring(startPosition, inputFile.length - 3);
console.info(
'NOTE: Generated module name is not provided. Use "%s" from input file as module name...',
moduleName
);
} else {
moduleName = program.opts().module;
console.info('Using "%s" as generated module name...', moduleName);
}
let ModuleName = Util.capitalizeFirst(moduleName);
let moduleNameCust = `${moduleName}-cust`;
let apiBase;
if (!program.opts().api) {
apiBase = 'api/' + moduleName;
console.info(
'NOTE: REST API base is not provided. Use "%s" as api base...',
apiBase
);
} else {
apiBase = program.opts().api;
console.info('Using "%s" as api base to call Rest APIs...', apiBase);
}
let generateView;
if (!program.opts().view) {
generateView = 'admin';
} else {
generateView = program.opts().view;
if (generateView !== 'public') generateView = 'admin';
}
console.log('NOTE: generateView for ', generateView);
// output directory
let outputDir;
if (!program.opts().output) {
outputDir = './';
console.info(
'NOTE: Output directory is not provided. Use the current directory for the output...'
);
} else {
outputDir = program.opts().output;
if (!fs.existsSync(outputDir)) {
//console.info('Creating output directory '%s'...', outputDir);
mkdir('.', outputDir);
}
}
let parentOutputDir = outputDir;
outputDir = path.join(parentOutputDir, moduleName);
outputDirCust = path.join(parentOutputDir, moduleNameCust);
let subDirCust = path.join(parentOutputDir, moduleNameCust, 'cust');
if (!fs.existsSync(subDirCust)) {
//console.info('Creating component directory '%s'...', componentDir);
mkdir('.', subDirCust);
}
let subDirExt = path.join(parentOutputDir, moduleNameCust, 'ext');
if (!fs.existsSync(subDirExt)) {
//console.info('Creating component directory '%s'...', componentDir);
mkdir('.', subDirExt);
}
let relativePath = relative(__dirname, inputFile);
let inputFileModule = relativePath.substring(0, relativePath.length - 3);
let sysDef = require(inputFileModule);
let schemas = sysDef.schemas;
let config = sysDef.config;
let authz = sysDef.authz;
let patch = []; //extra fields patching to the schema
if (sysDef.config && sysDef.config.patch) {
patch = sysDef.config.patch;
}
let schemaMap = {};
let validatorFields = [];
let referenceSchemas = []; ////schemas that are referred
let referenceMap = [];
let defaultSchema;
let LOCALE = 'en-US';
if (config && config.LOCALE) LOCA