Программный интерфейс

Показывать:
/**
 * Метаданные на стороне js: конструкторы, заполнение, кеширование, поиск
 *
 * © Evgeniy Malyarov http://www.oknosoft.ru 2014-2016
 *
 * @module  metadata
 * @submodule meta_meta
 * @requires common
 */


/**
 * ### Хранилище метаданных конфигурации
 * Важнейший объект `metadata.js`. Содержит описание всех классов данных приложения.<br />
 * По данным этого объекта, при старте приложения, формируются менеджеры данных, строятся динамические конструкторы объектов данных,
 * обеспечивается ссылочная типизация, рисуются автоформы объектов и списков.
 *
 * @class Meta
 * @static
 * @menuorder 02
 * @tooltip Описание метаданных
 */
function Meta() {

  var _m;

  _md = this;




  // загружает метаданные из pouchdb
  function meta_from_pouch(meta_db){

    return meta_db.info()
      .then(function () {
        return meta_db.get('meta');

      })
      .then(function (doc) {
        _m = doc;
        doc = null;
        return meta_db.get('meta_patch');

      }).then(function (doc) {
        $p._patch(_m, doc);
        doc = null;
        delete _m._id;
        delete _m._rev;
      });
  }


  /**
   * ### Cоздаёт объекты менеджеров
   * @method create_managers
   * @for Meta
   */
  _md.create_managers = function(){};

  /**
   * ### Инициализирует метаданные
   * загружает описание метаданных из локального или сетевого хранилища или из объекта, переданного в параметре
   *
   * @method create_managers
   * @for Meta
   * @param [meta_db] {Object|String}
   */
  _md.init = function (meta_db) {

    var confirm_count = 0,
      is_local = !meta_db || ($p.wsql.pouch && meta_db == $p.wsql.pouch.local.meta),
      is_remote = meta_db && ($p.wsql.pouch && meta_db == $p.wsql.pouch.local._meta);
    
    function do_init(){

      if(meta_db && !is_local && !is_remote){
        _m = meta_db;
        meta_db = null;

        _md.create_managers();

      }else{

        return meta_from_pouch(meta_db || $p.wsql.pouch.local.meta)
          .then(function () {
            if(is_local){
              _md.create_managers();

            }else{
              return _m;
            }
          })
          .catch($p.record_log);
      }
    }

    function do_reload(){

      dhtmlx.confirm({
        title: $p.msg.file_new_date_title,
        text: $p.msg.file_new_date,
        ok: "Перезагрузка",
        cancel: "Продолжить",
        callback: function(btn) {

          if(btn){

            $p.wsql.pouch.log_out();

            setTimeout(function () {
              $p.eve.redirect = true;
              location.reload(true);
            }, 1000);
            
          }else{

            confirm_count++;
            setTimeout(do_reload, confirm_count * 30000);
            
          }
        }
      });
      
    }

    // этот обработчик нужен только при инициализации, когда в таблицах meta еще нет данных
    $p.on("pouch_change", function (dbid, change) {

      if (dbid != "meta")
        return;

      if(!_m)
        do_init();
        
      else{
        
        // если изменились метаданные, запланировать перезагрузку
        if(performance.now() > 20000 && change.docs.some(function (doc) {
            return doc._id.substr(0,4)!='meta';
          }))
          do_reload();

      }
      
    });

    return do_init();

  };

  /**
   * ### Возвращает описание объекта метаданных
   * @method get
   * @param class_name {String} - например, "doc.calc_order"
   * @param [field_name] {String}
   * @return {Object}
   */
  _md.get = function(class_name, field_name){

    var np = class_name.split(".");

    if(!field_name)
      return _m[np[0]][np[1]];

    var res = {multiline_mode: false, note: "", synonym: "", tooltip: "", type: {is_ref: false,  types: ["string"]}},
      is_doc = "doc,tsk,bp".indexOf(np[0]) != -1,
      is_cat = "cat,cch,cacc,tsk".indexOf(np[0]) != -1;

    if(is_doc && field_name=="number_doc"){
      res.synonym = "Номер";
      res.tooltip = "Номер документа";
      res.type.str_len = 11;

    }else if(is_doc && field_name=="date"){
      res.synonym = "Дата";
      res.tooltip = "Дата документа";
      res.type.date_part = "date_time";
      res.type.types[0] = "date";

    }else if(is_doc && field_name=="posted"){
      res.synonym = "Проведен";
      res.type.types[0] = "boolean";

    }else if(is_cat && field_name=="id"){
      res.synonym = "Код";

    }else if(is_cat && field_name=="name"){
      res.synonym = "Наименование";

    }else if(field_name=="_deleted"){
      res.synonym = "Пометка удаления";
      res.type.types[0] = "boolean";

    }else if(field_name=="is_folder"){
      res.synonym = "Это группа";
      res.type.types[0] = "boolean";

    }else if(field_name=="ref"){
      res.synonym = "Ссылка";
      res.type.is_ref = true;
      res.type.types[0] = class_name;

    }else if(field_name)
      res = _m[np[0]][np[1]].fields[field_name];

    else
      res = _m[np[0]][np[1]];

    return res;
  };

  /**
   * ### Возвращает структуру имён объектов метаданных конфигурации
   *
   * @method get_classes
   */
  _md.get_classes = function () {
    var res = {};
    for(var i in _m){
      res[i] = [];
      for(var j in _m[i])
        res[i].push(j);
    }
    return res;
  };

  /**
   * ### Возвращает тип поля sql для типа данных
   *
   * @method sql_type
   * @param mgr {DataManager}
   * @param f {String}
   * @param mf {Object} - описание метаданных поля
   * @param pg {Boolean} - использовать синтаксис postgreSQL
   * @return {*}
   */
  _md.sql_type = function (mgr, f, mf, pg) {
    var sql;
    if((f == "type" && mgr.table_name == "cch_properties") || (f == "svg" && mgr.table_name == "cat_production_params"))
      sql = " JSON";

    else if(mf.is_ref || mf.types.indexOf("guid") != -1){
      if(!pg)
        sql = " CHAR";

      else if(mf.types.every(function(v){return v.indexOf("enm.") == 0}))
        sql = " character varying(100)";

      else if (!mf.hasOwnProperty("str_len"))
        sql = " uuid";

      else
        sql = " character varying(" + Math.max(36, mf.str_len) + ")";

    }else if(mf.hasOwnProperty("str_len"))
      sql = pg ? (mf.str_len ? " character varying(" + mf.str_len + ")" : " text") : " CHAR";

    else if(mf.date_part)
      if(!pg || mf.date_part == "date")
        sql = " Date";

      else if(mf.date_part == "date_time")
        sql = " timestamp with time zone";

      else
        sql = " time without time zone";

    else if(mf.hasOwnProperty("digits")){
      if(mf.fraction_figits==0)
        sql = pg ? (mf.digits < 7 ? " integer" : " bigint") : " INT";
      else
        sql = pg ? (" numeric(" + mf.digits + "," + mf.fraction_figits + ")") : " FLOAT";

    }else if(mf.types.indexOf("boolean") != -1)
      sql = " BOOLEAN";

    else if(mf.types.indexOf("json") != -1)
      sql = " JSON";

    else
      sql = pg ? " character varying(255)" : " CHAR";

    return sql;
  };

  /**
   * ### Для полей составного типа, добавляет в sql поле описания типа
   * @param mf
   * @param f
   * @param pg
   * @return {string}
   */
  _md.sql_composite = function (mf, f, f0, pg){
    var res = "";
    if(mf[f].type.types.length > 1 && f != "type"){
      if(!f0)
        f0 = f.substr(0, 29) + "_T";
      else{
        f0 = f0.substr(0, 29) + "_T";
      }

      if(pg)
        res = ', "' + f0 + '" character varying(255)';
      else
        res = _md.sql_mask(f0) + " CHAR";
    }
    return res;
  };

  /**
   * ### Заключает имя поля в аппострофы
   * @method sql_mask
   * @param f
   * @param t
   * @return {string}
   * @private
   */
  _md.sql_mask = function(f, t){
    //var mask_names = ["delete", "set", "value", "json", "primary", "content"];
    return ", " + (t ? "_t_." : "") + ("`" + f + "`");
  };

  /**
   * ### Возвращает менеджер объекта по имени класса
   * @method mgr_by_class_name
   * @param class_name {String}
   * @return {DataManager|undefined}
   * @private
   */
  _md.mgr_by_class_name = function(class_name){
    if(class_name){
      var np = class_name.split(".");
      if(np[1] && $p[np[0]])
        return $p[np[0]][np[1]];
    }
  };

  /**
   * ### Возвращает менеджер значения по свойству строки
   * @method value_mgr
   * @param row {Object|TabularSectionRow} - строка табчасти или объект
   * @param f {String} - имя поля
   * @param mf {Object} - описание типа поля mf.type
   * @param array_enabled {Boolean} - возвращать массив для полей составного типа или первый доступный тип
   * @param v {*} - устанавливаемое значение
   * @return {DataManager|Array}
   */
  _md.value_mgr = function(row, f, mf, array_enabled, v){
    var property, oproperty, tnames, rt, mgr;
    if(mf._mgr)
      return mf._mgr;

    function mf_mgr(mgr){
      if(mgr && mf.types.length == 1)
        mf._mgr = mgr;
      return mgr;
    }

    if(mf.types.length == 1){
      tnames = mf.types[0].split(".");
      if(tnames.length > 1 && $p[tnames[0]])
        return mf_mgr($p[tnames[0]][tnames[1]]);

    }else if(v && v.type){
      tnames = v.type.split(".");
      if(tnames.length > 1 && $p[tnames[0]])
        return mf_mgr($p[tnames[0]][tnames[1]]);
    }

    property = row.property || row.param;
    if(f != "value" || !property){

      rt = [];
      mf.types.forEach(function(v){
        tnames = v.split(".");
        if(tnames.length > 1 && $p[tnames[0]][tnames[1]])
          rt.push($p[tnames[0]][tnames[1]]);
      });
      if(rt.length == 1 || row[f] == $p.utils.blank.guid)
        return mf_mgr(rt[0]);

      else if(array_enabled)
        return rt;

      else if((property = row[f]) instanceof DataObj)
        return property._manager;

      else if($p.utils.is_guid(property) && property != $p.utils.blank.guid){
        for(var i in rt){
          mgr = rt[i];
          if(mgr.get(property, false, true))
            return mgr;
        }
      }
    }else{

      // Получаем объект свойства
      if($p.utils.is_data_obj(property))
        oproperty = property;
      else if($p.utils.is_guid(property))
        oproperty = $p.cch.properties.get(property, false);
      else
        return;
      
      if($p.utils.is_data_obj(oproperty)){

        if(oproperty.is_new())
          return $p.cat.property_values;

        // и через его тип выходми на мнеджера значения
        for(rt in oproperty.type.types)
          if(oproperty.type.types[rt].indexOf(".") > -1){
            tnames = oproperty.type.types[rt].split(".");
            break;
          }
        if(tnames && tnames.length > 1 && $p[tnames[0]])
          return mf_mgr($p[tnames[0]][tnames[1]]);
        else
          return oproperty.type;
      }
    }
  };

  /**
   * ### Возвращает имя типа элемента управления для типа поля
   * @method control_by_type
   * @param type
   * @return {*}
   */
  _md.control_by_type = function (type, val) {
    var ft;

    if(typeof val == "boolean" && type.types.indexOf("boolean") != -1){
      ft = "ch";

    } else if(typeof val == "number" && type.digits) {
      if(type.fraction_figits < 5)
        ft = "calck";
      else
        ft = "edn";

    } else if(val instanceof Date && type.date_part){
      ft = "dhxCalendar";
      
    } else if(type.is_ref){
      ft = "ocombo";

    } else if(type.date_part) {
      ft = "dhxCalendar";

    } else if(type.digits) {
      if(type.fraction_figits < 5)
        ft = "calck";
      else
        ft = "edn";

    } else if(type.types[0]=="boolean") {
      ft = "ch";

    } else if(type.hasOwnProperty("str_len") && (type.str_len >= 100 || type.str_len == 0)) {
      ft = "txt";

    } else {
      ft = "ed";

    }
    return ft;
  };

  /**
   * ### Возвращает структуру для инициализации таблицы на форме
   * @method ts_captions
   * @param class_name
   * @param ts_name
   * @param source
   * @return {boolean}
   */
  _md.ts_captions = function (class_name, ts_name, source) {
    if(!source)
      source = {};

    var mts = _md.get(class_name).tabular_sections[ts_name],
      mfrm = _md.get(class_name).form,
      fields = mts.fields, mf;

    // если имеются метаданные формы, используем их
    if(mfrm && mfrm.obj){

      if(!mfrm.obj.tabular_sections[ts_name])
        return;

      source._mixin(mfrm.obj.tabular_sections[ts_name]);

    }else{

      if(ts_name==="contact_information")
        fields = {type: "", kind: "", presentation: ""};

      source.fields = ["row"];
      source.headers = "№";
      source.widths = "40";
      source.min_widths = "";
      source.aligns = "";
      source.sortings = "na";
      source.types = "cntr";

      for(var f in fields){
        mf = mts.fields[f];
        if(!mf.hide){
          source.fields.push(f);
          source.headers += "," + (mf.synonym ? mf.synonym.replace(/,/g, " ") : f);
          source.types += "," + _md.control_by_type(mf.type);
          source.sortings += ",na";
        }
      }
    }

    return true;

  };

  /**
   * ### Возвращает англоязычный синоним строки
   * @method syns_js
   * @param v {String}
   * @return {String}
   */
  _md.syns_js = function (v) {
    var synJS = {
      DeletionMark: '_deleted',
      Description: 'name',
      DataVersion: 'data_version',    // todo: не сохранять это поле в pouchdb
      IsFolder: 'is_folder',
      Number: 'number_doc',
      Date: 'date',
      Дата: 'date',
      Posted: 'posted',
      Code: 'id',
      Parent_Key: 'parent',
      Owner_Key: 'owner',
      Owner:     'owner',
      Ref_Key: 'ref',
      Ссылка: 'ref',
      LineNumber: 'row'
    };
    if(synJS[v])
      return synJS[v];
    return _m.syns_js[_m.syns_1с.indexOf(v)] || v;
  };

  /**
   * ### Возвращает русскоязычный синоним строки
   * @method syns_1с
   * @param v {String}
   * @return {String}
   */
  _md.syns_1с = function (v) {
    var syn1c = {
      _deleted: 'DeletionMark',
      name: 'Description',
      is_folder: 'IsFolder',
      number_doc: 'Number',
      date: 'Date',
      posted: 'Posted',
      id: 'Code',
      ref: 'Ref_Key',
      parent: 'Parent_Key',
      owner: 'Owner_Key',
      row: 'LineNumber'
    };
    if(syn1c[v])
      return syn1c[v];
    return _m.syns_1с[_m.syns_js.indexOf(v)] || v;
  };

  /**
   * ### Возвращает список доступных печатных форм
   * @method printing_plates
   * @return {Object}
   */
  _md.printing_plates = function (pp) {
    if(pp)
      for(var i in pp.doc)
        _m.doc[i].printing_plates = pp.doc[i];

  };

  /**
   * ### Возвращает имя класса по полному имени объекта метаданных 1С
   * @method class_name_from_1c
   * @param name
   */
  _md.class_name_from_1c = function (name) {

    var pn = name.split(".");
    if(pn.length == 1)
      return "enm." + name;
    else if(pn[0] == "Перечисление")
      name = "enm.";
    else if(pn[0] == "Справочник")
      name = "cat.";
    else if(pn[0] == "Документ")
      name = "doc.";
    else if(pn[0] == "РегистрСведений")
      name = "ireg.";
    else if(pn[0] == "РегистрНакопления")
      name = "areg.";
    else if(pn[0] == "РегистрБухгалтерии")
      name = "aссreg.";
    else if(pn[0] == "ПланВидовХарактеристик")
      name = "cch.";
    else if(pn[0] == "ПланСчетов")
      name = "cacc.";
    else if(pn[0] == "Обработка")
      name = "dp.";
    else if(pn[0] == "Отчет")
      name = "rep.";

    return name + _md.syns_js(pn[1]);

  };

  /**
   * ### Возвращает полное именя объекта метаданных 1С по имени класса metadata
   * @method class_name_to_1c
   * @param name
   */
  _md.class_name_to_1c = function (name) {

    var pn = name.split(".");
    if(pn.length == 1)
      return "Перечисление." + name;
    else if(pn[0] == "enm")
      name = "Перечисление.";
    else if(pn[0] == "cat")
      name = "Справочник.";
    else if(pn[0] == "doc")
      name = "Документ.";
    else if(pn[0] == "ireg")
      name = "РегистрСведений.";
    else if(pn[0] == "areg")
      name = "РегистрНакопления.";
    else if(pn[0] == "aссreg")
      name = "РегистрБухгалтерии.";
    else if(pn[0] == "cch")
      name = "ПланВидовХарактеристик.";
    else if(pn[0] == "cacc")
      name = "ПланСчетов.";
    else if(pn[0] == "dp")
      name = "Обработка.";
    else if(pn[0] == "rep")
      name = "Отчет.";

    return name + _md.syns_1с(pn[1]);

  };


  /**
   * ### Создаёт строку SQL с командами создания таблиц для всех объектов метаданных
   * @method create_tables
   */
  _md.create_tables = function(callback, attr){

    var cstep = 0, data_names = [], managers = _md.get_classes(), class_name,
      create = (attr && attr.postgres) ? "" : "USE md; ";

    function on_table_created(){

      cstep--;
      if(cstep==0){
        if(callback)
          callback(create);
        else
          alasql.utils.saveFile("create_tables.sql", create);
      } else
        iteration();
    }

    function iteration(){
      var data = data_names[cstep-1];
      create += data["class"][data.name].get_sql_struct(attr) + "; ";
      on_table_created();
    }

    // TODO переписать на промисах и генераторах и перекинуть в синкер
    "enm,cch,cacc,cat,bp,tsk,doc,ireg,areg".split(",").forEach(function (mgr) {
      for(class_name in managers[mgr])
        data_names.push({"class": $p[mgr], "name": managers[mgr][class_name]});
    });
    cstep = data_names.length;

    iteration();

  };


}