///
///
///
const stripBom = require("strip-bom");
import fs = require("fs");
import path = require('path');
import lodash = require('lodash');
import metadata = require('MetadataClasses');
import { ModelLocales } from '../flexberry-core/Locales';
const TAB = " ";
class SortedPair{
index: number;
str: string;
constructor(index: number, str: string) {
this.index=index;
this.str=str;
}
}
export default class ModelBlueprint {
model: string;
serializerAttrs: string;
offlineSerializerAttrs: string;
parentModelName: string;
parentClassName: string;
parentExternal: boolean;
className: string;
namespace: string;
projections: string;
hasCpValidations: boolean;
validations: string;
name: string;
needsAllModels: string;
needsAllEnums: string;
needsAllObjects: string;
lodashVariables: {};
enums: { [key: string]: metadata.Enumeration; };
enumImports: { [key: string]: string; } = {};
constructor(blueprint, options) {
let modelsDir = path.join(options.metadataDir, "models");
if (!options.file) {
options.file = options.entity.name + ".json";
}
let model: metadata.Model = ModelBlueprint.loadModel(modelsDir, options.file);
this.parentModelName = model.parentModelName;
this.parentClassName = model.parentClassName;
if (model.parentModelName) {
let parentModel: metadata.Model = ModelBlueprint.loadModel(modelsDir, model.parentModelName + ".json");
this.parentExternal = parentModel.external;
}
this.hasCpValidations = ModelBlueprint.checkCpValidations(options);
this.enums = ModelBlueprint.loadEnums(options.metadataDir);
this.className = model.className;
this.namespace = model.nameSpace;
this.serializerAttrs = this.getSerializerAttrs(model);
this.offlineSerializerAttrs = this.getOfflineSerializerAttrs(model);
this.projections = this.getJSForProjections(model, modelsDir);
this.model = this.getJSForModel(model);
this.name = options.entity.name;
this.needsAllModels = this.getNeedsAllModels(modelsDir);
this.needsAllEnums = this.getNeedsTransforms(path.join(options.metadataDir, "enums"));
this.needsAllObjects = this.getNeedsTransforms(path.join(options.metadataDir, "objects"));
let localePathTemplate: lodash.TemplateExecutor = this.getLocalePathTemplate(options, blueprint.isDummy, path.join("models", options.entity.name + ".js"));
let modelLocales = new ModelLocales(model, modelsDir, "ru", localePathTemplate);
this.lodashVariables = modelLocales.getLodashVariablesProperties();
if (this.hasCpValidations) {
this.validations = this.getValidations(model);
}
}
static checkCpValidations(blueprint): boolean {
return 'ember-cp-validations' in blueprint.project.dependencies();
}
static loadModel(modelsDir: string, modelFileName: string): metadata.Model {
let modelFile = path.join(modelsDir, modelFileName);
let content = stripBom(fs.readFileSync(modelFile, "utf8"));
let model: metadata.Model = JSON.parse(content);
return model;
}
static loadEnums(metadataDir: string): { [key: string]: metadata.Enumeration; } {
let enums: { [key: string]: metadata.Enumeration; } = {};
let enumsDir: string = path.join(metadataDir, "enums");
let files = fs.readdirSync(enumsDir);
for (let fileName of files) {
let parsedPath: path.ParsedPath = path.parse(fileName);
if (parsedPath.ext === ".json") {
let enumContent = fs.readFileSync(path.join(enumsDir, fileName), "utf8");
enums[parsedPath.name] = JSON.parse(stripBom(enumContent));
}
}
return enums;
}
getNeedsTransforms(dir: string): string {
let list = fs.readdirSync(dir);
let transforms: string[] = [];
for (let e of list) {
let pp: path.ParsedPath = path.parse(e);
if (pp.ext != ".json")
continue;
transforms.push(` 'transform:${pp.name}'`);
}
return transforms.join(",\n");
}
getNeedsAllModels(modelsDir: string): string {
let listModels = fs.readdirSync(modelsDir);
let models: string[] = [];
for (let model of listModels) {
let pp: path.ParsedPath = path.parse(model);
if (pp.ext != ".json")
continue;
models.push(` 'model:${pp.name}'`);
}
return models.join(",\n");
}
getSerializerAttrs(model: metadata.Model): string {
let attrs: string[] = [];
for (let belongsTo of model.belongsTo) {
attrs.push(belongsTo.name + ": { serialize: 'odata-id', deserialize: 'records' }");
}
for (let hasMany of model.hasMany) {
attrs.push(hasMany.name + ": { serialize: false, deserialize: 'records' }");
}
if(attrs.length===0){
return "";
}
return " "+attrs.join(",\n ");
}
getOfflineSerializerAttrs(model: metadata.Model): string {
let attrs: string[] = [];
for (let belongsTo of model.belongsTo) {
attrs.push(belongsTo.name + ": { serialize: 'id', deserialize: 'records' }");
}
for (let hasMany of model.hasMany) {
attrs.push(hasMany.name + ": { serialize: 'ids', deserialize: 'records' }");
}
if (attrs.length === 0) {
return "";
}
return " " + attrs.join(",\n ");
}
getJSForModel(model: metadata.Model): string {
let attrs: string[] = [], validations: string[] = [];
let templateBelongsTo = lodash.template("<%=name%>: DS.belongsTo('<%=relatedTo%>', { inverse: <%if(inverse){%>'<%=inverse%>'<%}else{%>null<%}%>, async: false<%if(polymorphic){%>, polymorphic: true<%}%> })");
let templateHasMany = lodash.template("<%=name%>: DS.hasMany('<%=relatedTo%>', { inverse: <%if(inverse){%>'<%=inverse%>'<%}else{%>null<%}%>, async: false })");
let attr: metadata.DSattr;
for (attr of model.attrs) {
let comment = "";
if (!attr.stored) {
comment =
"/**\n" +
TAB + TAB + "Non-stored property.\n\n" +
TAB + TAB + `@property ${attr.name}\n` +
TAB + "*/\n" + TAB;
}
var options = [];
var optionsStr = "";
if (attr.defaultValue) {
switch (attr.type) {
case 'decimal':
case 'number':
case 'boolean':
options.push(`defaultValue: ${attr.defaultValue}`);
break;
case 'date':
// The UTC handling policy depends solely on backend configuration.
if (attr.defaultValue === 'Now' || attr.defaultValue === 'UtcNow') {
options.push(`defaultValue() { return new Date(); }`);
break;
}
throw new Error(`The default '${attr.defaultValue}' value for '${attr.name}' attribute of 'date' type is not supported.`);
default:
if (this.enums.hasOwnProperty(attr.type)) {
let enumName = `${this.enums[attr.type].className}Enum`;
this.enumImports[enumName] = attr.type;
options.push(`defaultValue: ${enumName}.${attr.defaultValue}`);
} else {
options.push(`defaultValue: '${attr.defaultValue}'`);
}
}
}
if (attr.ordered) {
options.push("ordered: true");
}
if (options.length != 0) {
optionsStr = ", { " + options.join(', ') + ' }';
}
attrs.push(`${comment}${attr.name}: DS.attr('${attr.type}'${optionsStr})`);
if (!this.hasCpValidations && attr.notNull) {
if (attr.type === "date") {
validations.push(attr.name + ": { datetime: true }");
} else {
validations.push(attr.name + ": { presence: true }");
}
}
if (attr.stored)
continue;
let methodToSetNotStoredProperty =
"/**\n" +
TAB + TAB + "Method to set non-stored property.\n" +
TAB + TAB + "Please, use code below in model class (outside of this mixin) otherwise it will be replaced during regeneration of models.\n" +
TAB + TAB + `Please, implement '${attr.name}Compute' method in model class (outside of this mixin) if you want to compute value of '${attr.name}' property.\n\n` +
TAB + TAB + `@method _${attr.name}Compute\n` +
TAB + TAB + "@private\n" +
TAB + TAB + "@example\n" +
TAB + TAB + TAB + "```javascript\n" +
TAB + TAB + TAB + `_${attr.name}Changed: Ember.on('init', Ember.observer('${attr.name}', function() {\n` +
TAB + TAB + TAB + TAB + `Ember.run.once(this, '_${attr.name}Compute');\n` +
TAB + TAB + TAB + "}))\n" +
TAB + TAB + TAB + "```\n" +
TAB + "*/\n" +
TAB + `_${attr.name}Compute: function() {\n` +
TAB + TAB + `let result = (this.${attr.name}Compute && typeof this.${attr.name}Compute === 'function') ?` +
(attr.name.length > 21 ? '\n' + TAB + TAB + TAB : ' ') + `this.${attr.name}Compute() : null;\n` +
TAB + TAB + `this.set('${attr.name}', result);\n` +
TAB + "}";
attrs.push(methodToSetNotStoredProperty);
}
let belongsTo: metadata.DSbelongsTo;
for (belongsTo of model.belongsTo) {
if (!this.hasCpValidations && belongsTo.presence) {
validations.push(belongsTo.name + ": { presence: true }");
}
attrs.push(templateBelongsTo(belongsTo));
}
for (let hasMany of model.hasMany) {
attrs.push(templateHasMany(hasMany));
}
if (!this.hasCpValidations) {
let validationsFunc=TAB + TAB + TAB + validations.join(",\n" + TAB + TAB + TAB) + "\n";
if(validations.length===0){
validationsFunc="";
}
validationsFunc =
"getValidations: function () {\n" +
TAB + TAB + "let parentValidations = this._super();\n" +
TAB + TAB + "let thisValidations = {\n" +
validationsFunc + TAB + TAB + "};\n" +
TAB + TAB + "return Ember.$.extend(true, {}, parentValidations, thisValidations);\n" +
TAB + "}";
let initFunction =
"init: function () {\n" +
TAB + TAB + "this.set('validations', this.getValidations());\n" +
TAB + TAB + "this._super.apply(this, arguments);\n" +
TAB + "}";
attrs.push(validationsFunc, initFunction);
}
return attrs.length ? `\n${TAB + attrs.join(`,\n${TAB}`)}\n` : '';
}
joinProjHasMany(detailHasMany: metadata.ProjHasMany, modelsDir: string, level: number): SortedPair {
let hasManyAttrs: SortedPair[] = [];
let hasManyModel: metadata.Model = ModelBlueprint.loadModel(modelsDir, detailHasMany.relatedTo + ".json");
let hasManyProj = lodash.find(hasManyModel.projections, function(pr: metadata.ProjectionForModel) { return pr.name === detailHasMany.projectionName; });
if (hasManyProj) {
for (let attr of hasManyProj.attrs) {
hasManyAttrs.push(this.declareProjAttr(attr));
}
for (let belongsTo of hasManyProj.belongsTo) {
hasManyAttrs.push(this.joinProjBelongsTo(belongsTo, level + 1));
}
let indent: string[] = [];
for (let i = 0; i < level; i++) {
indent.push(TAB);
}
let indentStr = indent.join("");
indent.pop();
let indentStr2 = indent.join("");
hasManyAttrs=lodash.sortBy(hasManyAttrs,["index"]);
let attrsStr = lodash.map(hasManyAttrs, "str").join(",\n" + indentStr);
if(hasManyAttrs.length===0){
attrsStr = "";
indentStr = "";
}
return new SortedPair(Number.MAX_VALUE,`${detailHasMany.name}: hasMany('${detailHasMany.relatedTo}', '${detailHasMany.caption}', {\n${indentStr}${attrsStr}\n${indentStr2}})`);
}
return new SortedPair(Number.MAX_VALUE,"");
}
joinProjBelongsTo(belongsTo: metadata.ProjBelongsTo, level: number): SortedPair {
let belongsToAttrs: SortedPair[] = [];
let index=Number.MAX_VALUE;
for (let attr of belongsTo.attrs) {
belongsToAttrs.push(this.declareProjAttr(attr));
}
for (let belongsTo2 of belongsTo.belongsTo) {
belongsToAttrs.push(this.joinProjBelongsTo(belongsTo2, level + 1));
}
let hiddenStr = "";
if (belongsTo.hidden || belongsTo.index==-1) {
hiddenStr = `, { index: ${belongsTo.index}, hidden: true }`;
}else{
if (belongsTo.lookupValueField) {
hiddenStr = `, { index: ${belongsTo.index}, displayMemberPath: '${belongsTo.lookupValueField}' }`;
} else {
hiddenStr = `, { index: ${belongsTo.index} }`;
}
}
let indent: string[] = [];
for (let i = 0; i < level; i++) {
indent.push(TAB);
}
let indentStr = indent.join("");
indent.pop();
let indentStr2 = indent.join("");
belongsToAttrs=lodash.sortBy(belongsToAttrs,["index"]);
let attrsStr = lodash.map(belongsToAttrs, "str").join(",\n" + indentStr);
if(belongsToAttrs.length===0){
attrsStr = "";
indentStr = "";
}else{
index=belongsToAttrs[0].index;
if(index==-1)
index=Number.MAX_VALUE;
}
return new SortedPair(index,`${belongsTo.name}: belongsTo('${belongsTo.relatedTo}', '${belongsTo.caption}', {\n${indentStr}${attrsStr}\n${indentStr2}}${hiddenStr})`);
}
declareProjAttr(attr: metadata.ProjAttr): SortedPair {
let hiddenStr = "";
if (attr.hidden) {
hiddenStr = `, { index: ${attr.index}, hidden: true }`;
} else {
hiddenStr = `, { index: ${attr.index} }`;
}
return new SortedPair(attr.index, `${attr.name}: attr('${attr.caption}'${hiddenStr})`);
}
getJSForProjections(model: metadata.Model, modelsDir: string): string {
let projections: string[] = [];
let projName: string;
if(model.projections.length===0){
return null;
}
for (let proj of model.projections) {
let projAttrs: SortedPair[] = [];
for (let attr of proj.attrs) {
projAttrs.push(this.declareProjAttr(attr));
}
for (let belongsTo of proj.belongsTo) {
projAttrs.push(this.joinProjBelongsTo(belongsTo, 3));
}
for (let hasMany of proj.hasMany) {
let hasManyAttrs: SortedPair[] = [];
let detailModel: metadata.Model = ModelBlueprint.loadModel(modelsDir, hasMany.relatedTo + ".json");
projName = hasMany.projectionName;
let detailProj = lodash.find(detailModel.projections, function(pr: metadata.ProjectionForModel) { return pr.name === projName; });
if (detailProj) {
for (let detailAttr of detailProj.attrs) {
hasManyAttrs.push(this.declareProjAttr(detailAttr));
}
for (let detailBelongsTo of detailProj.belongsTo) {
hasManyAttrs.push(this.joinProjBelongsTo(detailBelongsTo, 4));
}
for (let detailHasMany of detailProj.hasMany) {
hasManyAttrs.push(this.joinProjHasMany(detailHasMany, modelsDir, 4));
}
}
hasManyAttrs=lodash.sortBy(hasManyAttrs,["index"]);
let attrsStr = lodash.map(hasManyAttrs, "str").join(",\n ");
projAttrs.push(new SortedPair(Number.MAX_VALUE, `${hasMany.name}: hasMany('${hasMany.relatedTo}', '${hasMany.caption}', {\n ${attrsStr}\n })`));
}
projAttrs=lodash.sortBy(projAttrs,["index"]);
let attrsStr = lodash.map(projAttrs, "str").join(",\n ");
projections.push(` modelClass.defineProjection('${proj.name}', '${proj.modelName}', {\n ${attrsStr}\n });`);
}
return `\n${projections.join("\n\n")}\n`;
}
getValidations(model: metadata.Model): string {
let validators = {};
for (let attr of model.attrs) {
validators[attr.name] = [`validator('ds-error'),`];
switch (attr.type) {
case 'date':
validators[attr.name].push(`validator('date'),`);
if (attr.notNull) {
validators[attr.name].push(`validator('presence', true),`);
}
break;
case 'string':
case 'boolean':
if (attr.notNull) {
validators[attr.name].push(`validator('presence', true),`);
}
break;
case 'number':
case 'decimal':
let options = 'allowString: true';
options += attr.notNull ? '' : ', allowBlank: true';
options += attr.type === 'number' ? ', integer: true' : '';
validators[attr.name].push(`validator('number', { ${options} }),`);
break;
}
}
for (let belongsTo of model.belongsTo) {
validators[belongsTo.name] = [`validator('ds-error'),`];
if (belongsTo.presence) {
validators[belongsTo.name].push(`validator('presence', true),`);
}
}
for (let hasMany of model.hasMany) {
validators[hasMany.name] = [`validator('ds-error'),`, `validator('has-many'),`];
}
let validations = [];
for (let validationKey in validators) {
let descriptionKey = `descriptionKey: 'models.${model.modelName}.validations.${validationKey}.__caption__',`;
let _validators = `validators: [\n${TAB + TAB + TAB + validators[validationKey].join(`\n${TAB + TAB + TAB}`)}\n${TAB + TAB}],`;
validations.push(`${TAB + validationKey}: {\n${TAB + TAB + descriptionKey}\n${TAB + TAB + _validators}\n${TAB}}`);
}
return validations.length ? `\n${validations.join(',\n')},\n` : '';
}
private getLocalePathTemplate(options, isDummy, localePathSuffix: string): lodash.TemplateExecutor {
let targetRoot = "app"
if (options.project.pkg.keywords && options.project.pkg.keywords["0"] === "ember-addon" ) {
targetRoot = isDummy ? path.join("tests/dummy", targetRoot) : "addon";
}
return lodash.template(path.join(targetRoot, "locales", "${ locale }", localePathSuffix));
}
}