UNPKG

@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
#!/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