Pull to refresh

Comments 40

Обьясните мне, люди, чем отличается этот ваш JSON API от всем известного REST API?
JSON API, в контексте статьи, это протокол, который описывается спекой, доступной по ссылке из статьи. REST скорее архитектурный стиль.
от всем известного REST API?

  1. REST API не бывает.
  2. REST не всем известен. Подавляющее большинство начинает вещать о каких-то там "спецификациях REST" и о том что "POST" это Create а "PUT" это Update, не учитывая такие вещи как "какой метод должен гарантировать идемпотентность операции а какой не очень".
  3. REST это архитектурный стиль WEB-а. В большинстве же API от uniform interface только урлы ресурсов забывая о метаданных ресурсов этих и т.д. Контент тайп хоть делают и то хорошо.
  4. JSON API это намного более конкретная штука. Все реализации между собой похожи, приследуют возможность строить композицию ответов (инклуды) и прочее. Возьмите json api, graphql и т.д. — идеи похожи, реализации просто разные.

я могу продолжать.

Точно такими же мыслями я руководствовался, когда решил написать похожий инструмент в рамках работы над своим новым проектом. В моем случае скрипт рекурсивно обходит JSON-объект с конфигурацией, и исполняет хуки, сгруппированые в классы-драйверы с одинаковыми интерфейсами. Каждый драйвер овечает за генерацию кода определенного типа. На данный момент у меня их 3:
— SQL CREATE,
— JS Pojo Object for server
— JS Pojo Object for browser
В результате, описав в конфиге таблицу так
конфиг
        {
            name: "users",
            singular_name: "user",
            retention: "SoftDeleteRetention",
            audit: "SimpleAudit",
            securify: { sqlWhere: " AND id = ? ", expr: "[ req.user && req.user.getId() || 0 ]" },
            access_delete: { user: false, csr: true, admin: true },
            columns: [
                {
                    name: "id",
                    type: "primaryIdType",
                },
                {
                    name: "username",
                    type: "string",
                    nullable: false,
                    validator: "UsernameFieldValidator",
                    access_user:  true,
                    access_csr:   true,
                    access_admin: true,
                    descr: { en: "Username", ru: "Имя пользователя" }
                },
                {
                    name: "email",
                    type: "emailType",
                    nullable: false,
                    validator: "EmailFieldValidator",
                    access_user:  true,
                    access_csr:   true,
                    access_admin: true,
                    descr: { en: "Email", ru: "Емайл" }
                },
                {
                    name: "password",
                    type: "passwordType",
                    access_user:  { c: true, r: false, u: true  },
                    access_csr:   { c: true, r: false, u: true  },
                    access_admin: { c: true, r: false, u: true  },
                    descr: { en: "Password", ru: "Пароль" }
                },
                {
                    name: "password_reset_token",
                    access_user:  false,
                    access_csr:   false,
                    access_admin: false,
                    type: "string",
                    noGui: true
                },
                {
                    name: "password_reset_expires",
                    access_user:  { c: false, r: false, u: false  },
                    access_csr:   { c: false, r: true,  u: false  },
                    access_admin: { c: false, r: true,  u: false  },
                    type: "datetime",
                    noGui: true
                },
                {
                    name: "addr_line_1",
                    type: "string",
                    access_user:  true,
                    access_csr:   true,
                    access_admin: true,
                    descr: { en: "Address", ru: "Адрес" }
                },
                {
                    name: "addr_line_2",
                    type: "string",
                    access_user:  true,
                    access_csr:   true,
                    access_admin: true,
                    descr: { en: "Address Line 2", ru: "Адрес (продолжение)" }
                },
                {
                    name: "addr_city",
                    type: "string",
                    access_user:  true,
                    access_csr:   true,
                    access_admin: true,
                    descr: { en: "City", ru: "Город" }
                },
                {
                    name: "addr_province",
                    type: "string",
                    access_user:  true,
                    access_csr:   true,
                    access_admin: true,
                    descr: { en: "State/Province", ru: "Регион" }
                },
                {
                    name: "addr_postal_code",
                    type: "string",
                    access_user:  true,
                    access_csr:   true,
                    access_admin: true,
                    descr: { en: "Postal/Zip Code", ru: "Индекс" }
                },
                {
                    name: "addr_country",
                    type: "string",
                    access_user:  true,
                    access_csr:   true,
                    access_admin: true,
                    descr: { en: "Country", ru: "Страна" }
                },
                {
                    name: "is_user",
                    type: "yesNoType",
                    access_user:  { c: false, r: true, u: false  },
                    access_csr:   { c: false, r: true, u: false  },
                    access_admin: { c: true, r: true,  u: true },
                    descr: { en: "Is User", ru: "Пользователь" }
                },
                {
                    name: "is_csr",
                    type: "yesNoType",
                    access_user:  { c: false, r: true, u: false  },
                    access_csr:   { c: false, r: true, u: false  },
                    access_admin: { c: true, r: true,  u: true },
                    descr: { en: "Is CSR", ru: "Менеджер" }
                },
                {
                    name: "is_admin",
                    type: "yesNoType",
                    access_user:  { c: false, r: true, u: false  },
                    access_csr:   { c: false, r: true, u: false  },
                    access_admin: { c: true, r: true,  u: true },
                    descr: { en: "Is Admin", ru: "Админ" }
                },
                {
                    name: "last_active",
                    type: "datetime",
                    access_user:  { c: false, r: true, u: false  },
                    access_csr:   { c: false, r: true, u: false  },
                    access_admin: { c: false, r: true, u: false  },
                    descr: { en: "Last Login", ru: "Последнее подключение" }
                }
            ],
        },


я автоматически получаю
SQL код для создания таблиц
CREATE TABLE IF NOT EXISTS ex_users (
 id int AUTO_INCREMENT NOT NULL,
 PRIMARY KEY (id),
 username varchar(255) NOT NULL,
 email varchar(255) NOT NULL,
 password varchar(255) NULL,
 password_reset_token varchar(255) NULL,
 password_reset_expires BIGINT NULL,
 addr_line_1 varchar(255) NULL,
 addr_line_2 varchar(255) NULL,
 addr_city varchar(255) NULL,
 addr_province varchar(255) NULL,
 addr_postal_code varchar(255) NULL,
 addr_country varchar(255) NULL,
 is_user BOOLEAN NULL,
 is_csr BOOLEAN NULL,
 is_admin BOOLEAN NULL,
 last_active BIGINT NULL,
 create_dt BIGINT NULL,
 created_by int NULL,
 update_dt BIGINT NULL,
 updated_by int NULL,
 delete_dt BIGINT NULL,
 deleted_by int NULL
);



Pojo для сервера
var User = function(req,o){
    App.Model.call(this, req);
    this.$pojonatorDef = App.Pojonator.tables[0];
    this.$data = {};
    this.Copy( o || {$data:{}} );
};
exports.User = User;
User.prototype = Object.create(App.Model.prototype);
User.prototype.constructor = User;
User.prototype.access_delete = {
    user: false,
    csr: true,
    admin: true
};
User.prototype.securify = {
    sqlWhere: " AND id = ? ",
    fn: function(req) { return [ req.user && req.user.getId() || 0 ] }
};
User.prototype.cols = {
    id : {
    camelCaseName: "id",
    order: 0,
    nullable: false,
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    }
},
    username : {
    camelCaseName: "username",
    order: 1,
    nullable: false,
    validator: "UsernameFieldValidator",
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    email : {
    camelCaseName: "email",
    order: 2,
    nullable: false,
    validator: "EmailFieldValidator",
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    password : {
    camelCaseName: "password",
    order: 3,
    nullable: true,
    access: {
        user: {
            c: true,
            r: false,
            u: true
        },
        csr: {
            c: true,
            r: false,
            u: true
        },
        admin: {
            c: true,
            r: false,
            u: true
        }
    },
    mutator: "PasswordTypeDbMutator"
},
    password_reset_token : {
    camelCaseName: "passwordResetToken",
    order: 4,
    nullable: true,
    access: {}
},
    password_reset_expires : {
    camelCaseName: "passwordResetExpires",
    order: 5,
    nullable: true,
    access: {
        user: {
            c: false,
            r: false,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    },
    assignDb2Js: function($source){ return ( $source ? new Date( $source ) : undefined )},
    assignJs2Js: function($source){ return ( $source? new Date( $source )  : undefined )},
    assignJs2Db: function($source){ return ( $source ? $source.getTime()   : undefined )},
    assignSer2Js: function($source){ return ( $source ? new Date( $source  ): undefined )},
    assignJs2Ser: function($source){ return ( $source ? $source.getTime()   : undefined )}
},
    addr_line_1 : {
    camelCaseName: "addrLine_1",
    order: 6,
    nullable: true,
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    addr_line_2 : {
    camelCaseName: "addrLine_2",
    order: 7,
    nullable: true,
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    addr_city : {
    camelCaseName: "addrCity",
    order: 8,
    nullable: true,
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    addr_province : {
    camelCaseName: "addrProvince",
    order: 9,
    nullable: true,
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    addr_postal_code : {
    camelCaseName: "addrPostalCode",
    order: 10,
    nullable: true,
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    addr_country : {
    camelCaseName: "addrCountry",
    order: 11,
    nullable: true,
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    is_user : {
    camelCaseName: "isUser",
    order: 12,
    nullable: true,
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    is_csr : {
    camelCaseName: "isCsr",
    order: 13,
    nullable: true,
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    is_admin : {
    camelCaseName: "isAdmin",
    order: 14,
    nullable: true,
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    last_active : {
    camelCaseName: "lastActive",
    order: 15,
    nullable: true,
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    },
    assignDb2Js: function($source){ return ( $source ? new Date( $source ) : undefined )},
    assignJs2Js: function($source){ return ( $source? new Date( $source )  : undefined )},
    assignJs2Db: function($source){ return ( $source ? $source.getTime()   : undefined )},
    assignSer2Js: function($source){ return ( $source ? new Date( $source  ): undefined )},
    assignJs2Ser: function($source){ return ( $source ? $source.getTime()   : undefined )}
},
    create_dt : {
    camelCaseName: "createDt",
    order: 16,
    nullable: true,
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    },
    assignDb2Js: function($source){ return ( $source ? new Date( $source ) : undefined )},
    assignJs2Js: function($source){ return ( $source? new Date( $source )  : undefined )},
    assignJs2Db: function($source){ return ( $source ? $source.getTime()   : undefined )},
    assignSer2Js: function($source){ return ( $source ? new Date( $source  ): undefined )},
    assignJs2Ser: function($source){ return ( $source ? $source.getTime()   : undefined )}
},
    created_by : {
    camelCaseName: "createdBy",
    order: 17,
    nullable: true,
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    }
},
    update_dt : {
    camelCaseName: "updateDt",
    order: 18,
    nullable: true,
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    },
    assignDb2Js: function($source){ return ( $source ? new Date( $source ) : undefined )},
    assignJs2Js: function($source){ return ( $source? new Date( $source )  : undefined )},
    assignJs2Db: function($source){ return ( $source ? $source.getTime()   : undefined )},
    assignSer2Js: function($source){ return ( $source ? new Date( $source  ): undefined )},
    assignJs2Ser: function($source){ return ( $source ? $source.getTime()   : undefined )}
},
    updated_by : {
    camelCaseName: "updatedBy",
    order: 19,
    nullable: true,
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    }
},
    delete_dt : {
    camelCaseName: "deleteDt",
    order: 20,
    nullable: true,
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    },
    assignDb2Js: function($source){ return ( $source ? new Date( $source ) : undefined )},
    assignJs2Js: function($source){ return ( $source? new Date( $source )  : undefined )},
    assignJs2Db: function($source){ return ( $source ? $source.getTime()   : undefined )},
    assignSer2Js: function($source){ return ( $source ? new Date( $source  ): undefined )},
    assignJs2Ser: function($source){ return ( $source ? $source.getTime()   : undefined )}
},
    deleted_by : {
    camelCaseName: "deletedBy",
    order: 21,
    nullable: true,
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    }
}};
/** @return {number} */
User.prototype.getId = function() { return this.$data.id };
/** @param {number} id */
User.prototype.setId = function(id) {
    this.$data.id !== id  && App.Model.setDirty(this,'id');
    this.$data.id = id;
};/** @return {string} */
User.prototype.getUsername = function() { return this.$data.username };
/** @param {string} username */
User.prototype.setUsername = function(username) {
    this.$data.username !== username  && App.Model.setDirty(this,'username');
    this.$data.username = username;
};/** @return {string} */
User.prototype.getEmail = function() { return this.$data.email };
/** @param {string} email */
User.prototype.setEmail = function(email) {
    this.$data.email !== email  && App.Model.setDirty(this,'email');
    this.$data.email = email;
};/** @return {string} */
User.prototype.getPassword = function() { return this.$data.password };
/** @param {string} password */
User.prototype.setPassword = function(password) {
    this.$data.password !== password  && App.Model.setDirty(this,'password');
    this.$data.password = password;
};/** @return {string} */
User.prototype.getPasswordResetToken = function() { return this.$data.password_reset_token };
/** @param {string} passwordResetToken */
User.prototype.setPasswordResetToken = function(passwordResetToken) {
    this.$data.password_reset_token !== passwordResetToken  && App.Model.setDirty(this,'password_reset_token');
    this.$data.password_reset_token = passwordResetToken;
};User.prototype.getPasswordResetExpires = function() { return this.$data.password_reset_expires };
User.prototype.setPasswordResetExpires = function(passwordResetExpires) {
    this.$data.password_reset_expires !== passwordResetExpires  && App.Model.setDirty(this,'password_reset_expires');
    this.$data.password_reset_expires = passwordResetExpires;
};/** @return {string} */
User.prototype.getAddrLine_1 = function() { return this.$data.addr_line_1 };
/** @param {string} addrLine_1 */
User.prototype.setAddrLine_1 = function(addrLine_1) {
    this.$data.addr_line_1 !== addrLine_1  && App.Model.setDirty(this,'addr_line_1');
    this.$data.addr_line_1 = addrLine_1;
};/** @return {string} */
User.prototype.getAddrLine_2 = function() { return this.$data.addr_line_2 };
/** @param {string} addrLine_2 */
User.prototype.setAddrLine_2 = function(addrLine_2) {
    this.$data.addr_line_2 !== addrLine_2  && App.Model.setDirty(this,'addr_line_2');
    this.$data.addr_line_2 = addrLine_2;
};/** @return {string} */
User.prototype.getAddrCity = function() { return this.$data.addr_city };
/** @param {string} addrCity */
User.prototype.setAddrCity = function(addrCity) {
    this.$data.addr_city !== addrCity  && App.Model.setDirty(this,'addr_city');
    this.$data.addr_city = addrCity;
};/** @return {string} */
User.prototype.getAddrProvince = function() { return this.$data.addr_province };
/** @param {string} addrProvince */
User.prototype.setAddrProvince = function(addrProvince) {
    this.$data.addr_province !== addrProvince  && App.Model.setDirty(this,'addr_province');
    this.$data.addr_province = addrProvince;
};/** @return {string} */
User.prototype.getAddrPostalCode = function() { return this.$data.addr_postal_code };
/** @param {string} addrPostalCode */
User.prototype.setAddrPostalCode = function(addrPostalCode) {
    this.$data.addr_postal_code !== addrPostalCode  && App.Model.setDirty(this,'addr_postal_code');
    this.$data.addr_postal_code = addrPostalCode;
};/** @return {string} */
User.prototype.getAddrCountry = function() { return this.$data.addr_country };
/** @param {string} addrCountry */
User.prototype.setAddrCountry = function(addrCountry) {
    this.$data.addr_country !== addrCountry  && App.Model.setDirty(this,'addr_country');
    this.$data.addr_country = addrCountry;
};/** @return {boolean} */
User.prototype.getIsUser = function() { return this.$data.is_user };
/** @param {boolean} isUser */
User.prototype.setIsUser = function(isUser) {
    this.$data.is_user !== isUser  && App.Model.setDirty(this,'is_user');
    this.$data.is_user = isUser;
};/** @return {boolean} */
User.prototype.getIsCsr = function() { return this.$data.is_csr };
/** @param {boolean} isCsr */
User.prototype.setIsCsr = function(isCsr) {
    this.$data.is_csr !== isCsr  && App.Model.setDirty(this,'is_csr');
    this.$data.is_csr = isCsr;
};/** @return {boolean} */
User.prototype.getIsAdmin = function() { return this.$data.is_admin };
/** @param {boolean} isAdmin */
User.prototype.setIsAdmin = function(isAdmin) {
    this.$data.is_admin !== isAdmin  && App.Model.setDirty(this,'is_admin');
    this.$data.is_admin = isAdmin;
};User.prototype.getLastActive = function() { return this.$data.last_active };
User.prototype.setLastActive = function(lastActive) {
    this.$data.last_active !== lastActive  && App.Model.setDirty(this,'last_active');
    this.$data.last_active = lastActive;
};User.prototype.getCreateDt = function() { return this.$data.create_dt };
User.prototype.setCreateDt = function(createDt) {
    this.$data.create_dt !== createDt  && App.Model.setDirty(this,'create_dt');
    this.$data.create_dt = createDt;
};/** @return {number} */
User.prototype.getCreatedBy = function() { return this.$data.created_by };
/** @param {number} createdBy */
User.prototype.setCreatedBy = function(createdBy) {
    this.$data.created_by !== createdBy  && App.Model.setDirty(this,'created_by');
    this.$data.created_by = createdBy;
};User.prototype.getUpdateDt = function() { return this.$data.update_dt };
User.prototype.setUpdateDt = function(updateDt) {
    this.$data.update_dt !== updateDt  && App.Model.setDirty(this,'update_dt');
    this.$data.update_dt = updateDt;
};/** @return {number} */
User.prototype.getUpdatedBy = function() { return this.$data.updated_by };
/** @param {number} updatedBy */
User.prototype.setUpdatedBy = function(updatedBy) {
    this.$data.updated_by !== updatedBy  && App.Model.setDirty(this,'updated_by');
    this.$data.updated_by = updatedBy;
};User.prototype.getDeleteDt = function() { return this.$data.delete_dt };
User.prototype.setDeleteDt = function(deleteDt) {
    this.$data.delete_dt !== deleteDt  && App.Model.setDirty(this,'delete_dt');
    this.$data.delete_dt = deleteDt;
};/** @return {number} */
User.prototype.getDeletedBy = function() { return this.$data.deleted_by };
/** @param {number} deletedBy */
User.prototype.setDeletedBy = function(deletedBy) {
    this.$data.deleted_by !== deletedBy  && App.Model.setDirty(this,'deleted_by');
    this.$data.deleted_by = deletedBy;
};
User.prototype.DB_TABLE_NAME = 'ex_users';



Pojo для браузера
DB.User = function(req,o){
    this.$data = {};
    this.Copy( o || {$data:{}} );
};
DB.User.prototype = Object.create(Mdl.prototype);
DB.User.prototype.constructor = DB.User;
DB.User.prototype.access_delete = {
    user: false,
    csr: true,
    admin: true
};
DB.User.prototype.cols = {
    id : {
    name: "id",
    camelCaseName: "id",
    order: 0,
    jsGuiType: "FormFieldNumber",
    nullable: false,
    descr: {
        en: "#",
        ru: "№"
    },
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    }
},
    username : {
    name: "username",
    camelCaseName: "username",
    order: 1,
    jsGuiType: "FormFieldText",
    nullable: false,
    validator: "UsernameFieldValidator",
    descr: {
        en: "Username",
        ru: "Имя пользователя"
    },
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    email : {
    name: "email",
    camelCaseName: "email",
    order: 2,
    jsGuiType: "FormFieldText",
    nullable: false,
    validator: "EmailFieldValidator",
    descr: {
        en: "Email",
        ru: "Емайл"
    },
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    password : {
    name: "password",
    camelCaseName: "password",
    order: 3,
    jsGuiType: "FormFieldPassword",
    nullable: true,
    descr: {
        en: "Password",
        ru: "Пароль"
    },
    access: {
        user: {
            c: true,
            r: false,
            u: true
        },
        csr: {
            c: true,
            r: false,
            u: true
        },
        admin: {
            c: true,
            r: false,
            u: true
        }
    }
},
    addr_line_1 : {
    name: "addr_line_1",
    camelCaseName: "addrLine_1",
    order: 4,
    jsGuiType: "FormFieldText",
    nullable: true,
    descr: {
        en: "Address",
        ru: "Адрес"
    },
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    addr_line_2 : {
    name: "addr_line_2",
    camelCaseName: "addrLine_2",
    order: 5,
    jsGuiType: "FormFieldText",
    nullable: true,
    descr: {
        en: "Address Line 2",
        ru: "Адрес (продолжение)"
    },
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    addr_city : {
    name: "addr_city",
    camelCaseName: "addrCity",
    order: 6,
    jsGuiType: "FormFieldText",
    nullable: true,
    descr: {
        en: "City",
        ru: "Город"
    },
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    addr_province : {
    name: "addr_province",
    camelCaseName: "addrProvince",
    order: 7,
    jsGuiType: "FormFieldText",
    nullable: true,
    descr: {
        en: "State/Province",
        ru: "Регион"
    },
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    addr_postal_code : {
    name: "addr_postal_code",
    camelCaseName: "addrPostalCode",
    order: 8,
    jsGuiType: "FormFieldText",
    nullable: true,
    descr: {
        en: "Postal/Zip Code",
        ru: "Индекс"
    },
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    addr_country : {
    name: "addr_country",
    camelCaseName: "addrCountry",
    order: 9,
    jsGuiType: "FormFieldText",
    nullable: true,
    descr: {
        en: "Country",
        ru: "Страна"
    },
    access: {
        user: {
            c: true,
            r: true,
            u: true
        },
        csr: {
            c: true,
            r: true,
            u: true
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    is_user : {
    name: "is_user",
    camelCaseName: "isUser",
    order: 10,
    jsGuiType: "FormFieldText",
    nullable: true,
    descr: {
        en: "Is User",
        ru: "Пользователь"
    },
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    is_csr : {
    name: "is_csr",
    camelCaseName: "isCsr",
    order: 11,
    jsGuiType: "FormFieldText",
    nullable: true,
    descr: {
        en: "Is CSR",
        ru: "Менеджер"
    },
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    is_admin : {
    name: "is_admin",
    camelCaseName: "isAdmin",
    order: 12,
    jsGuiType: "FormFieldText",
    nullable: true,
    descr: {
        en: "Is Admin",
        ru: "Админ"
    },
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: true,
            r: true,
            u: true
        }
    }
},
    last_active : {
    name: "last_active",
    camelCaseName: "lastActive",
    order: 13,
    jsGuiType: "FormFieldDatetime",
    nullable: true,
    descr: {
        en: "Last Login",
        ru: "Последнее подключение"
    },
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    },
    assignJs2Js: function($source){ return ( $source? new Date( $source )  : undefined )},
    assignSer2Js: function($source){ return ( $source ? new Date( $source  ): undefined )},
    assignJs2Ser: function($source){ return ( $source ? $source.getTime()   : undefined )}
},
    create_dt : {
    name: "create_dt",
    camelCaseName: "createDt",
    order: 14,
    jsGuiType: "FormFieldDatetime",
    nullable: true,
    descr: {
        en: "Create Date",
        ru: "Дата создания"
    },
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    },
    assignJs2Js: function($source){ return ( $source? new Date( $source )  : undefined )},
    assignSer2Js: function($source){ return ( $source ? new Date( $source  ): undefined )},
    assignJs2Ser: function($source){ return ( $source ? $source.getTime()   : undefined )}
},
    created_by : {
    name: "created_by",
    camelCaseName: "createdBy",
    order: 15,
    jsGuiType: "FormFieldNumber",
    nullable: true,
    descr: {
        en: "Created By",
        ru: "Создал"
    },
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    }
},
    update_dt : {
    name: "update_dt",
    camelCaseName: "updateDt",
    order: 16,
    jsGuiType: "FormFieldDatetime",
    nullable: true,
    descr: {
        en: "Update Date",
        ru: "Дата изменения"
    },
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    },
    assignJs2Js: function($source){ return ( $source? new Date( $source )  : undefined )},
    assignSer2Js: function($source){ return ( $source ? new Date( $source  ): undefined )},
    assignJs2Ser: function($source){ return ( $source ? $source.getTime()   : undefined )}
},
    updated_by : {
    name: "updated_by",
    camelCaseName: "updatedBy",
    order: 17,
    jsGuiType: "FormFieldNumber",
    nullable: true,
    descr: {
        en: "Updated By",
        ru: "Изменил"
    },
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    }
},
    delete_dt : {
    name: "delete_dt",
    camelCaseName: "deleteDt",
    order: 18,
    jsGuiType: "FormFieldDatetime",
    nullable: true,
    descr: {
        en: "Delete Date",
        ru: "Дата удаления"
    },
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    },
    assignJs2Js: function($source){ return ( $source? new Date( $source )  : undefined )},
    assignSer2Js: function($source){ return ( $source ? new Date( $source  ): undefined )},
    assignJs2Ser: function($source){ return ( $source ? $source.getTime()   : undefined )}
},
    deleted_by : {
    name: "deleted_by",
    camelCaseName: "deletedBy",
    order: 19,
    jsGuiType: "FormFieldNumber",
    nullable: true,
    descr: {
        en: "Deleted By",
        ru: "Удалил"
    },
    access: {
        user: {
            c: false,
            r: true,
            u: false
        },
        csr: {
            c: false,
            r: true,
            u: false
        },
        admin: {
            c: false,
            r: true,
            u: false
        }
    }
}};
/** @return {number} */
DB.User.prototype.getId = function() { return this.$data.id };
/** @param {number} id */
DB.User.prototype.setId = function(id) {
    this.$data.id = id;
};/** @return {string} */
DB.User.prototype.getUsername = function() { return this.$data.username };
/** @param {string} username */
DB.User.prototype.setUsername = function(username) {
    this.$data.username = username;
};/** @return {string} */
DB.User.prototype.getEmail = function() { return this.$data.email };
/** @param {string} email */
DB.User.prototype.setEmail = function(email) {
    this.$data.email = email;
};/** @return {string} */
DB.User.prototype.getPassword = function() { return this.$data.password };
/** @param {string} password */
DB.User.prototype.setPassword = function(password) {
    this.$data.password = password;
};/** @return {string} */
DB.User.prototype.getAddrLine_1 = function() { return this.$data.addr_line_1 };
/** @param {string} addrLine_1 */
DB.User.prototype.setAddrLine_1 = function(addrLine_1) {
    this.$data.addr_line_1 = addrLine_1;
};/** @return {string} */
DB.User.prototype.getAddrLine_2 = function() { return this.$data.addr_line_2 };
/** @param {string} addrLine_2 */
DB.User.prototype.setAddrLine_2 = function(addrLine_2) {
    this.$data.addr_line_2 = addrLine_2;
};/** @return {string} */
DB.User.prototype.getAddrCity = function() { return this.$data.addr_city };
/** @param {string} addrCity */
DB.User.prototype.setAddrCity = function(addrCity) {
    this.$data.addr_city = addrCity;
};/** @return {string} */
DB.User.prototype.getAddrProvince = function() { return this.$data.addr_province };
/** @param {string} addrProvince */
DB.User.prototype.setAddrProvince = function(addrProvince) {
    this.$data.addr_province = addrProvince;
};/** @return {string} */
DB.User.prototype.getAddrPostalCode = function() { return this.$data.addr_postal_code };
/** @param {string} addrPostalCode */
DB.User.prototype.setAddrPostalCode = function(addrPostalCode) {
    this.$data.addr_postal_code = addrPostalCode;
};/** @return {string} */
DB.User.prototype.getAddrCountry = function() { return this.$data.addr_country };
/** @param {string} addrCountry */
DB.User.prototype.setAddrCountry = function(addrCountry) {
    this.$data.addr_country = addrCountry;
};/** @return {boolean} */
DB.User.prototype.getIsUser = function() { return this.$data.is_user };
/** @param {boolean} isUser */
DB.User.prototype.setIsUser = function(isUser) {
    this.$data.is_user = isUser;
};/** @return {boolean} */
DB.User.prototype.getIsCsr = function() { return this.$data.is_csr };
/** @param {boolean} isCsr */
DB.User.prototype.setIsCsr = function(isCsr) {
    this.$data.is_csr = isCsr;
};/** @return {boolean} */
DB.User.prototype.getIsAdmin = function() { return this.$data.is_admin };
/** @param {boolean} isAdmin */
DB.User.prototype.setIsAdmin = function(isAdmin) {
    this.$data.is_admin = isAdmin;
};DB.User.prototype.getLastActive = function() { return this.$data.last_active };
DB.User.prototype.setLastActive = function(lastActive) {
    this.$data.last_active = lastActive;
};DB.User.prototype.getCreateDt = function() { return this.$data.create_dt };
DB.User.prototype.setCreateDt = function(createDt) {
    this.$data.create_dt = createDt;
};/** @return {number} */
DB.User.prototype.getCreatedBy = function() { return this.$data.created_by };
/** @param {number} createdBy */
DB.User.prototype.setCreatedBy = function(createdBy) {
    this.$data.created_by = createdBy;
};DB.User.prototype.getUpdateDt = function() { return this.$data.update_dt };
DB.User.prototype.setUpdateDt = function(updateDt) {
    this.$data.update_dt = updateDt;
};/** @return {number} */
DB.User.prototype.getUpdatedBy = function() { return this.$data.updated_by };
/** @param {number} updatedBy */
DB.User.prototype.setUpdatedBy = function(updatedBy) {
    this.$data.updated_by = updatedBy;
};DB.User.prototype.getDeleteDt = function() { return this.$data.delete_dt };
DB.User.prototype.setDeleteDt = function(deleteDt) {
    this.$data.delete_dt = deleteDt;
};/** @return {number} */
DB.User.prototype.getDeletedBy = function() { return this.$data.deleted_by };
/** @param {number} deletedBy */
DB.User.prototype.setDeletedBy = function(deletedBy) {
    this.$data.deleted_by = deletedBy;
};



— точку в API с контролем доступа

— Default-форму с контролем доступа

Код для операций с БД и генерации HTML-форм умеет пользоваться конфигами таблиц, поэтому если в конфиге указано, что поле «csr_comment» недоступно для чтения/записи с ролью 'user', то это поле автоматически пропадает для этой роли из
— всех HTML-форм
— всех SELECT, INSERT и UPDATE-операций, инициированных с этой ролью через форму или через API

Написание скрипта заняло несколько дней, но зато времени сэкономлено гораздо больше.
А ссылка на код велосипеда где?
На данный момент для велосипеда актуализируется документация и специфичный для нашего проекта код выносится из кода велосипеда в код основного проекта. Следите за новостями в нашем блоге :)
Изначально взяли протобаф и не жалеем до сих пор.
Всё-таки, protobuf про сериализацию данных, а JSON API про их стандартизацию.

Тем не менее, любопытно, на каких платформах вы используете protobuf?
Мы описали весь API с клиентами и вот уже получили наш стандарт! Сервер у нас частично на GO, частично на PHP. Клиенты на Java (андроид агент), на ObjectivC (клиент под iOS), на QT под Windows, OS X и Linux.
Тут тебе с коробки валидация схемы, минимизация трафика и удобные обвертки для работы с протокольными запросами.

Есть OpenAPI, для типичных кэйсов ОК

Заодно можно почти не беспокоится о реализации клиентов на разные языки. Их можно сгенерить swagger-gen-ом.

Насчёт клиентов вы правы, но как быть с сервером? Нам, как бекенду, важно не только договориться с клиентом о том, как будут выглядеть сущности в API, но и уметь генерировать эти сущности из моделей. Насколько я понимаю, для сервера swagger-gen может просто нагенерить пустых экшенов.

Для сервера так же можно сгенерить стаб контроллера с моделями.

Ну, собственно, это я и имел ввиду — генерятся накие DTO без привязки к источникам данных (AR, Doctrine, вот это вот всё), верно?
У автора все делается автоматически. В вашем же случае еще предстоит писать код экшенов и моделей, а также маппингов (когда в базе user_id, а отдать нужно userId) разной степени копипастности, как я понимаю.

ничего не мешает написать такой же staffold-генератор для openapi.


кажется, есть автогенераторы openapi из кода контроллеров.


просто человек тут решил сделать свой велосипед. нормально, в принципе. но не взлетит в рамках сообщества.

Приятно видеть в mature проекте PHP7, современные компоненты и подходы.


  1. Вопрос в одну копилку к предыдущим: рассматривали ли вы GraphQL? Поверхностно ощущение, что цели вашей спеки совпадают с направлением развития GraphQL.
  2. Почему для Doctrine query builder, а не критерии и/или спецификации?
  3. Какой движок Swagger вы используете?
Ну хоть у кого-то взгляд за mature зацепился :)

1. Ну, спека-то, всё же, не наша. Смею предположить, что в момент перехода на JSON API вменяемой имплементации GraphQL на PHP не было. Однако, даже если я не прав, в GraphQL смущает другое: параллельное получение данных из разных источников на PHP хоть и возможно, но не без костылей.
2. Вариант с билдером показался более универсальным.
3. Swagger-UI.
UFO just landed and posted this here
Что вы конкретно имеете ввиду?
UFO just landed and posted this here
Теперь я вас понял.

Получается, что запрос клиента обрабатывается GraphQL-сервером на Node, который отправляет запросы к PHP-бекенду, объединяет ответы и отдаёт результат клиенту обратно. В таком подходе лично мне не нравится наличие промежуточного слоя (Node), и, кроме того, есть ощущение, что со временем бизнес-логика начнёт размазываться по JS и PHP.
UFO just landed and posted this here

На PHP всё равно придётся реализовывать какой-то API.

конечно. Вопрос в удобной склейке и возможности иметь полный контроль за тем как ваша API используется.

UFO just landed and posted this here

Если бизнес-логика на стороне PHP (а иначе зачем он вообще нужен?), то его API тривиальным не будет, он должен обеспечивать все бизнес-операции, все команды если говорить в терминах CQRS. В большинстве сдучаев GraphQL шлюз, он же фронтенд-сервер (не суть на ноде или нет) будет выполнять роль агрегатора (при необходимости) результатов запросов из нескольких источников, диспетчера мутаций и, иногда, координатора распределенных транзакций.


Всё это почти никак не упрощает PHP-бэкенд, разве что до поры до времени можно забыть о размере и, частично, количестве сообщений между бэкендом и шлюзом поскольку, как правило, они не будут выходить за пределы дата-центра, стойки, а то и физической машины, что позволит до поры до времени не внедрять на стороне бэкенда средства управления клиентом формой ответа (список полей, "джойны") и количеством элементов в коллекциях (фильтрация, пагинация), а также средства пакетной обработки.

Я вижу тут два варианта:


  • либо тупой CRUD, но тогда в GraphQL-ный шлюз со временем неминуемо утечёт часть бизнес-логики, что нежелательно,
  • либо то, что описывает уважаемый VolCh, и тут, как мне кажется, перспектива получить "единый API на REST" разобьётся о скалы суровой реальности: разным GraphQL-серверам будет хотеться разных данных и PHP-шный бекенд будет вынужден удовлетворять запросы всех.

При тупом CRUD изначально получается, что бизнес-логика должна быть не на PHP-шном бекенде — либо на клиенте, либо на GraphQL-шлюзе, либо между ними размазана, а бэкенд лишь http-хранилище данных, пускай и знающий что-то об условиях валидности и целостности данных. Domain driven web database.


PHP-шный бекенд всегда должен удовлетворять запросы всех своих клиентов (не в плане безопасности, а в плане функциональности). Вопрос лишь нужно ли пытаться подстроиться под каждого индивидуально, или достаточно одной, почти всегда заведомо избыточной в каждом конкретном случае, версии API. GraphQL-шлюз в этом отношении лишь минимизирует количество клиентов в идеале до одного, но навскиду есть смысл иметь минимум два — публичный и внутренний для бэкофиса.


Хотя, вот прямо сейчас родилась идея и PHP-шный бекенд обернуть в GraphQL, но без всяких внешних связей и со строгой лимитацией глубины запросов для агрегатов, по сути другая реализация JSON API. А если клиенту нужно что-то большее, то GraphQL-шлюз разворачивает во множество запросов. Но в целом это вроде ничего не меняет — PHP-шный бекенд должен удовлетворять всех клиентов (прямо или через шлюз) по определнию.

и PHP-шный бекенд будет вынужден удовлетворять запросы всех.

да но склейкой он не будет заведовать. То есть на стороне сервера будут маленькие шлюзы для данных и операций а уже клиент сам себе будет собирать из этого что ему надо.


У меня есть API для трех платформ и каждой из платформ нужно почти то же самое но не всегда. Это приводит к избыточности и дублированию. Graphql для меня лично является нелпохим решением. Не без проблем но...

параллельное получение данных из разных источников на PHP хоть и возможно, но не без костылей

а зачем параллельное?
Если клиент хочет получить несколько коллекций сущностей за один вопрос, а мы у себя в коде будем собирать эти коллекции последовательно, в чем профит? Клиенту проще параллельно отправить несколько запросов.
Нет, так как для того чтобы сделать запрос тоже уходит время.
Безусловно, но всё слишком сильно зависит от конкретного случая.

Время на «сделать запрос» можно разделить на две составляющие: это время, которое будет потрачено на установление соединение, и время, которое нужно серверному приложению для бутстрапа. Первой составляющей, в общем-то, отчасти можно принебречь, поскольку HTTP/2 поддерживает мультиплексацию, упаковывая несколько запросов в одно соединение.

Так вот, если время, которое приложение потратит на генерацию ответа клиенту, сравнимо с временем, потраченным на сетевое взаимодействие и бутстрап (скажем, API достаёт уже подготовленный ответ из кеша), правда на вашей стороне.
Однако, если получение запрошенных коллекций требует выполнения какой-то сложной бизнес-логики, ситуация перестаёт быть такой однозначной, и тут вполне может выиграть распараллеливание запросов.
Ваш следующий API будет graphql. Инфа 146% :) потому что читая я понимал, что вы именно его и пытались сделать

А почему не использовали fractal, можно же было расширить

Я не работал с fractal, однако, навскидку, этот проект не выглядит хорошим кандидатом для расширения под наши хотелки:
  • fractal, как и многие другие решения, предлагает писать билдеры сущностей (в fractal они именуются трансформерами) — мы же, напротив, хотели уйти от кучи бесполезных классов, внутри которых делается что-нибудь типа
    return ['id' => (int) $item->id];
  • судя по устройству этих самых трансформеров, fractal умеет превращать модели в некие сущности, но не наборот: если клиент прислал нам некую сущность для сохранения, нам придётся руками преобразовывать её в модель (искать в базе, обновлять поля и т. д.)
  • fractal из-за своей архитектуры не умеет и, видимо, не научится делать eager-loading для связей: для lazy-loading они целый синтаксис сделали (довольно приятный, к слову), для eager — делай всё сам.

использую фрактал уже года два, поддерживаю по всем пунктам кроме "обратной трансформации". Вопервых это не является задачей фрактала. Во вторых лично по мне если у вас есть возможность напрямую мэпить json на сущности так как вы описываете я бы подумал вообще о том что бы отказаться от бэкэнда.


Ну и при изменениях требований то что казалось удобным (мэппинг напрямую на сущности) резко становится неудобным (сложно впилить нужную фичу).

Мы преобразовываем json в модели и обратно: таким образом, в коде эндпойнта мы вообще никак не взаимодействуем с сущностями (под сущностями я понимаю те объекты, которые видит клиент нашего API). Кажется, что в данном случае единственное изменение требований, которое сделает такое пребразование неудобным — это отказ JSON API.
Разумеется, есть сложные случаи, когда одна модель должна распасться на несколько сущностей, или, напротив, одна сущность состоит из нескольких моделей — но такие кейсы мы умеем обрабатывать.

Не могли бы вы рассказать в общих чертах, как вы выполняете обратную трансформацию?

Ну и про отказ от бэкенда я вас, честно говоря, не понял :)
Sign up to leave a comment.