Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save artello73/e10f0bbc6cd11f5083472a7455543c68 to your computer and use it in GitHub Desktop.
Save artello73/e10f0bbc6cd11f5083472a7455543c68 to your computer and use it in GitHub Desktop.

Revisions

  1. @zmts zmts revised this gist Feb 16, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -239,7 +239,7 @@ router.get('/api/users', function(req, res){
    __Выходит такой Lifecycle >> Запрос проходит дефолтные мидлвари >> Попадает в контроллер >> Попадает в `actionRunner` тот проверяет права доступа текущего юзера к экшену, валидирует запрос и стартует процесс обработки (статическая ф-ция `run`) >> В экшене выполняется бизнес логика и результат возращается в контроллер >> `actionRunner` получает результат и возвращает его клиенту.__

    Что не стоит делать в контроллере:
    - Валидировать параметры(`req.params`). Как показала практика это плохая идея. Проверку параметров лучше стоит делать непосредственно в экшене. Таким образом в дальнейшем будет более наглядно видно какие парметры в запросе доступны экшену.
    - Валидировать параметры(`req.params`). Как показала практика это плохая идея. Проверку параметров лучше делать непосредственно в экшене. Таким образом в дальнейшем будет более наглядно видно какие парметры в запросе доступны экшену.

    ## Actions
    Основным ключевым моментом является использование отдельного класса для каждого эндпоинта. Я определяю понятие `Action` как класс инкапсулирующий всю логику работы эндпоинта. То есть для реализации круда у нас будет 5 файлов (`CreatePostAction`, `GetPostByIdAction`, `UpdatePostAction`, `DeletePostAction`, `ListPostsAction`) по экшену на каждый эндпоинт.
  2. @zmts zmts revised this gist Feb 16, 2020. 1 changed file with 11 additions and 11 deletions.
    22 changes: 11 additions & 11 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -35,7 +35,7 @@ Controller(роутинг, проверка прав по роли) >> Action(п
    - [Controllers](#controllers)
    - [Actions](#actions)
    - [Model](#model)
    - [Clients](#clients)
    - [Agents](#agents)
    - [Providers](#providers)
    - [Helpers](#authhelpers)
    - [DAO](#dao)
    @@ -376,20 +376,20 @@ async function getById (id, options = {}) {
    }
    ```

    ## Clients
    `Clients` - это врапперы над внешними API/клиентами. Предназначение данной абстракции обеспечение единого контракта работы с внешними ресурсами. Допустим нам необходимо использовать в качестве хранилища файлов сервис от Амазон AWS S3. Мы создаем `S3Client` добавляем в него свои методы-обертки для работы с данным хранилищем. В случае критического изменения в клиенте амазона мы соотвественно меняем методы обертки без ущерба и переписывания остальной логики в остальных частях приложения. Единожды создав экземпляр клиент-враппера, можно переиспользовать его в разных местах вместо того что бы плодить инстансы голого клиента.
    ## Agents
    Агенты - это врапперы над внешними API/клиентами. Предназначение данной абстракции обеспечение единого контракта работы с внешними ресурсами. Допустим нам необходимо использовать в качестве хранилища файлов сервис от Амазон AWS S3. Мы создаем `S3Agent` добавляем в него свои методы-обертки. В случае критического изменения в клиенте амазона мы соотвественно меняем методы обертки без ущерба и переписывания остальной логики в остальных частях приложения.

    Перехваченные ошибки внешних API выбрасываем предватительно обернув их в свой кастомный класс ошибки (дабы иметь идиный интервейс работы с ошибками).
    Перехваченные ошибки внешних API выбрасываем предватительно обернув их в свой кастомный класс ошибки (дабы иметь единый интервейс работы с ошибками).

    <details>
    <summary>client example:</summary>
    <summary>agent example:</summary>

    ```js
    const { assert, AppError, errorCodes } = require('supra-core')
    const AWS = require('aws-sdk')
    const $ = Symbol('private scope')

    class S3Client {
    class S3Agent {
    constructor (options) {
    assert.object(options, { required: true, notEmpty: true })
    assert.string(options.access, { required: true, notEmpty: true })
    @@ -445,17 +445,17 @@ module.exports = S3Client
    </details>

    ## Providers
    Как быть в ситуации когда необходимо использовать один и тот же клиент в нескольких экшенах ? Для этого случая предназанчен слой `Providers`. Дабы не плодить для каждого экшена свой клиент создаем необходимое единожды в провайдере, а дальше импортируем его в нужных местах.
    Как быть в ситуации когда необходимо использовать один и тот же агент в нескольких экшенах ? Для этого случая предназанчен слой `Providers`. Дабы не плодить для каждого экшена свой агент создаем необходимое единожды в провайдере, а дальше импортируем его в нужных местах.
    <details>
    <summary>code:</summary>

    ```js
    const S3Client = require('../core/clients/S3Client')
    const S3Agent = require('../core/clients/S3Agent')
    const config = require('../config')

    class RootProvider {
    constructor () {
    this.s3Client = new S3Client({
    this.s3Agent = new S3Agent({
    access: config.s3.access,
    secret: config.s3.secret,
    bucket: config.s3.bucket
    @@ -554,9 +554,9 @@ testControllerHandler (req, res, next) {
    ```js
    // IS GOOD

    testControllerHandler (req) {
    testControllerHandler (ctx) {
    // my logic with throwed error
    await testService.getTest({ id: req.params.id })
    await testService.getTest({ id: ctx.params.id })
    }
    ```
    </details>
  3. @zmts zmts revised this gist Feb 16, 2020. 1 changed file with 5 additions and 3 deletions.
    8 changes: 5 additions & 3 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -16,9 +16,11 @@ code: https://github.com/zmts/supra-api-nodejs

    #### Несколько слов про разделение приложения на слои:

    Обычно в слое контроллеров данные из запроса валидируются и приводятся к виду необходимому для последующего сервисного слоя. В свою очередь сервисный слой полностью изолирует бизнес логику и возвращает результат вычислений в контроллер. Но _иногда_, а может и немного _чаще_ в контроллер просачивается бизнес логика, а в сервисный слой валидация, а то и вообще контекст запроса. Дабы так не происходило данный подход предлагает использовать единый слой для всей логики касаемо конкретного `use case`. Назовем этот слой `Action layer`. В итоге имеем:
    1. Минималистичный контроллер - сугубо маппинг экшенов на роуты
    2. Слой экшенов отвечающий за валидацию, всевозможные проверки доступа(владелец/не владелец/админ/не админ/аноним...) и бизнес логику
    Обычно в слое контроллеров данные из запроса валидируются и приводятся к виду необходимому для последующего сервисного слоя. В свою очередь сервисный слой полностью изолирует бизнес логику и возвращает результат вычислений в контроллер. Но _иногда_, а может и немного _чаще_ в контроллер просачивается бизнес логика, а в сервисный слой валидация, а то и вообще контекст запроса. Дабы так не происходило данный подход предлагает использовать единый слой для всей логики касаемо конкретного `use case`. Назовем этот слой `Action layer`.

    В итоге имеем:
    1. Минималистичный контроллер - маппинг экшенов на роуты
    2. Экшен - определяет правила валидации входящих данных, проверки прав доступа(владелец/не владелец/админ/не админ/аноним...) и бизнес логику
    3. Data layer - прослойка к БД

    ```
  4. @zmts zmts revised this gist Feb 16, 2020. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -346,12 +346,12 @@ class PostModel extends BaseModel {
    }
    }

    Каждое поле модели это инстанс `Rule` класса. `Rule` принимает в себя валидатор и описание онной простым текстом. Выполнение ф-ции валидации обязано вернуть либо булево значение либо строку(`error.message`)

    module.exports = PostModel
    ```
    </details>

    Каждое поле модели это инстанс класса `Rule` состоящий из валидатора и описания. Выполнение ф-ции валидации обязано вернуть либо булево значение либо строку(`error.message`)

    Все последующие проверки полей в других компонентах приложения обязаны импортироваться из схемы модели. Например в cлое `DAO` в неком `getById(id)` вместо того что-бы делать:
    ```js
    async function getById (id, options = {}) {
  5. @zmts zmts revised this gist Feb 16, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -250,7 +250,7 @@ __Выходит такой Lifecycle >> Запрос проходит дефо
    Правила валидации (`validationRules`) импортируются из модели или в случае необходимости используются кастомные.

    ***Роль модели в экшене:***
    Дабы не дублировать кучу подобных моделей(одна модель для создания сущности со всеми `required` полями, другая для обновления, третья для обновления конкретно указанного набора полей итд...) было принято решение не указывать в ф-ции валидации модели `required` требование. Валидаровать поля запроса в экшене помогает небольшой класс-хелпер `RequestRule` в его задачи входит предоставить [базовой ф-ции валидации](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L96) интересующие ее [аргументы](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L112) (ф-цию валидатор и `required` флаг).
    Дабы не дублировать кучу подобных моделей(одна модель для создания сущности со всеми `required` полями, другая для обновления, третья для обновления указанного набора полей итд...) было принято решение не указывать в ф-ции валидации модели `required` требование. Вместо этого `required` флаг имеет место быть в момент валидации как опция класса `RequestRule`.
    ```js
    {
    validationRules: {
  6. @zmts zmts revised this gist Feb 16, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -26,7 +26,7 @@ Controller(роутинг, проверка прав по роли) >> Action(п
    ```

    ## Содержание
    - [Assertion(type checking)](#assertiontype-checking)
    - [Assert](#assert)
    - [Config](#config)
    - [Server](#serverapp-initialization)
    - [Middlewares](#middlewares)
  7. @zmts zmts revised this gist Feb 16, 2020. 1 changed file with 4 additions and 2 deletions.
    6 changes: 4 additions & 2 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -41,8 +41,10 @@ Controller(роутинг, проверка прав по роли) >> Action(п
    - [Logging](#logging)
    - [Policy/Roles/Permissions](#policyrolespermissions)

    ## Assertion(type checking)
    Проверка типов это один из инструментов программиста который намного облегчает жизнь и помогает держать приложение в порядке. В этом мне помогает небольшой [Assertion class](https://github.com/zmts/supra-api-nodejs/tree/master/core/lib/assert). Из существующих библиотек есть [node-assert-plus](https://github.com/joyent/node-assert-plus).
    ## Assert
    > Assert — это специальная конструкция, позволяющая проверять предположения о значениях произвольных данных в произвольном месте программы. Эта конструкция может автоматически сигнализировать при обнаружении некорректных данных, что обычно приводит к выбросу исключения с указанием места обнаружения некорректных данных. https://habr.com/ru/post/141080/
    В этом мне помогает небольшой [Assertion class](https://github.com/zmts/supra-api-nodejs/tree/master/core/lib/assert). Из существующих библиотек есть [node-assert-plus](https://github.com/joyent/node-assert-plus).
    ```js
    async function foo (id, options = {}) {
    assert.integer(id, { required: true })
  8. @zmts zmts revised this gist Jan 30, 2020. 1 changed file with 17 additions and 14 deletions.
    31 changes: 17 additions & 14 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -475,28 +475,31 @@ module.exports = new RootProvider()
    ## Errors
    Работа с ошибками организована через единственнный кастомный класс ошибки и список эррор кодов (хранящийся в виде конфига), это позволяет не создавать на каждый тип ошибки свой класс.
    ```js
    class ErrorWrapper extends Error {
    class AppError extends Error {
    constructor (options) {
    if (!options || !options.message) throw new Error('message param required')
    if (!options || !options.description) throw new Error('description param required')

    super()
    this.message = options.message
    this.description = options.description || undefined // default error description from errorCodes
    this.message = options.message || this.description // message thrown by error
    this.status = options.status || 500
    this.code = options.code || 'SERVER_ERROR'
    this.code = options.code || 'UNEXPECTED_ERROR'
    this.layer = options.layer || undefined
    this.meta = options.meta || undefined
    this.req = options.req || undefined
    this.origin = options.origin || undefined // origin error data
    }
    }

    module.exports = ErrorWrapper
    ```
    ```js
    module.exports = {
    ACCESS: { message: 'Access denied', status: 403, code: 'ACCESS_ERROR' },
    BAD_ROLE: { message: 'Bad role', status: 403, code: 'BAD_ROLE_ERROR' },
    DB: { status: 500, code: 'DB_ERROR' }
    ACCESS: { description: 'Access denied', status: 403, code: 'ACCESS_ERROR' },
    BAD_ROLE: { description: 'Bad role', status: 403, code: 'BAD_ROLE_ERROR' },
    DB: { description: 'Database error occurred', status: 500, code: 'DB_ERROR' }
    }
    ```
    ```js
    new ErrorWrapper({ ...errorCodes.ACCESS, message: 'Access denied dude !!!' })
    new AppError({ ...errorCodes.ACCESS, message: 'Access denied dude !!!' })
    ```
    В последствии на фронте выводим ошибку из поля `message` или то что фронтенд посчтитает нужным в зависимости от поля `code`.

    @@ -616,7 +619,7 @@ return new Promise((resolve, reject) => {
    if (currentUser.role === roles.superadmin) return resolve()
    if (permissions[currentUser.role].includes(accessTagAll)) return resolve()
    if (permissions[currentUser.role].includes(accessTag)) return resolve()
    return reject(new ErrorWrapper({ ...errorCodes.ACCESS, message: 'Access denied, don\'t have permissions.' }))
    return reject(new AppError({ ...errorCodes.ACCESS, message: 'Access denied, don\'t have permissions.' }))
    })
    ```
    </details>
    @@ -632,9 +635,9 @@ return new Promise((resolve, reject) => {
    if (user.id === model.userId) return resolve(model)
    if (!model.private) return resolve(model)
    if (model.private) {
    return reject(new ErrorWrapper({ ...errorCodes.ACCESS, message: `User ${user.id} don't have access to model ${model.id}` }))
    return reject(new AppError({ ...errorCodes.ACCESS, message: `User ${user.id} don't have access to model ${model.id}` }))
    }
    return reject(new ErrorWrapper({ ...errorCodes.ACCESS }))
    return reject(new AppError({ ...errorCodes.ACCESS }))
    })
    ```
    </details>
    @@ -671,7 +674,7 @@ module.exports = GetPostByIdAction
    return new Promise((resolve, reject) => {
    if (user.role === roles.superadmin) return resolve()
    if (user.id === model.userId) return resolve()
    return reject(new ErrorWrapper({ ...errorCodes.ACCESS }))
    return reject(new AppError({ ...errorCodes.ACCESS }))
    })
    ```
    </details>
  9. @zmts zmts revised this gist Jan 30, 2020. 1 changed file with 12 additions and 4 deletions.
    16 changes: 12 additions & 4 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -375,10 +375,13 @@ async function getById (id, options = {}) {
    ## Clients
    `Clients` - это врапперы над внешними API/клиентами. Предназначение данной абстракции обеспечение единого контракта работы с внешними ресурсами. Допустим нам необходимо использовать в качестве хранилища файлов сервис от Амазон AWS S3. Мы создаем `S3Client` добавляем в него свои методы-обертки для работы с данным хранилищем. В случае критического изменения в клиенте амазона мы соотвественно меняем методы обертки без ущерба и переписывания остальной логики в остальных частях приложения. Единожды создав экземпляр клиент-враппера, можно переиспользовать его в разных местах вместо того что бы плодить инстансы голого клиента.

    Перехваченные ошибки внешних API выбрасываем предватительно обернув их в свой кастомный класс ошибки (дабы иметь идиный интервейс работы с ошибками).

    <details>
    <summary>client example:</summary>

    ```js
    const { assert, AppError, errorCodes } = require('supra-core')
    const AWS = require('aws-sdk')
    const $ = Symbol('private scope')

    @@ -388,6 +391,7 @@ class S3Client {
    assert.string(options.access, { required: true, notEmpty: true })
    assert.string(options.secret, { required: true, notEmpty: true })
    assert.string(options.bucket, { required: true, notEmpty: true })
    assert.instanceOf(options.logger, AbstractLogger)

    AWS.config.update({
    accessKeyId: options.access,
    @@ -396,10 +400,11 @@ class S3Client {

    this[$] = {
    client: new AWS.S3(),
    bucket: options.bucket
    bucket: options.bucket,
    logger: options.logger
    }

    __logger.info(`${this.constructor.name} constructed...`)
    this[$].logger.debug(`${this.constructor.name} constructed...`)
    }

    async uploadImage (buffer, fileName) {
    @@ -419,8 +424,11 @@ class S3Client {

    this[$].client.upload(params, (error, data) => {
    if (error) {
    __logger.error(`${this.constructor.name}: unable to upload objects`, error)
    return reject(error)
    return reject(new AppError({
    ...errorCodes.EXTERNAL,
    message: `${this.constructor.name}: unable to upload object. ${error.message}`,
    origin: error
    }))
    }
    resolve(data.Location)
    })
  10. @zmts zmts revised this gist Jan 21, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -16,7 +16,7 @@ code: https://github.com/zmts/supra-api-nodejs

    #### Несколько слов про разделение приложения на слои:

    Обычно в слое Контроллеров данные из запроса валидируются и приводятся к виду необходимому для последующего сервисного слоя. В свою очередь сервисный слой полностью изолирует бизнес логику и возвращает результат вычислений в контроллер. Или иначе ? Как вы считаете ? И так каждый программист делает как считает нужно либо так как привык. Давайте будем откровенны. Приводило ли подобная архитектура к тому ради чего задумывалась(к порядку и разделению логики) ? Зачастую все эти попытки сводятся к `big ball of mud`. Зачастую в контроллер просачивается бизнес логика, а в сервисный слой валидация, а то и вообще контекст запроса. Дабы так не происходило данный подход предлагает использовать единый слой для всей логики касаемо конкретного `use case`. Назовем этот слой `Action layer`. В итоге имеем:
    Обычно в слое контроллеров данные из запроса валидируются и приводятся к виду необходимому для последующего сервисного слоя. В свою очередь сервисный слой полностью изолирует бизнес логику и возвращает результат вычислений в контроллер. Но _иногда_, а может и немного _чаще_ в контроллер просачивается бизнес логика, а в сервисный слой валидация, а то и вообще контекст запроса. Дабы так не происходило данный подход предлагает использовать единый слой для всей логики касаемо конкретного `use case`. Назовем этот слой `Action layer`. В итоге имеем:
    1. Минималистичный контроллер - сугубо маппинг экшенов на роуты
    2. Слой экшенов отвечающий за валидацию, всевозможные проверки доступа(владелец/не владелец/админ/не админ/аноним...) и бизнес логику
    3. Data layer - прослойка к БД
  11. @zmts zmts revised this gist Dec 30, 2019. 1 changed file with 15 additions and 0 deletions.
    15 changes: 15 additions & 0 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -55,6 +55,8 @@ async function foo (id, options = {}) {
    `Assertion` это набор статических методов для базовой проверки типов аргуметров и возможно некоторых дополнительных опций (аля `{ required: true, notEmpty: true, positive: true }`).

    ## Config
    Конфиг это святая-святых настроек приложения посему должен инстансироватся и инициализироватся(через асинхронные методы) перед HTTP сервером.

    Все параметры неободимые для работы приложения должны хранится в едином конфиге. Параметры конфига делятся на два типа: констаннты и переменные. Первые храним как обычные поля, вторые извлекаем из переменных окружения (для примера это: логины/пароли доступа в БД, ключи для шифрования сессий или JWT токенов, ключи доступа к другим внешним ресурсам итд...).
    ```js
    class AppConfig extends BaseConfig {
    @@ -65,6 +67,19 @@ class AppConfig extends BaseConfig {
    this.host = this.set('APP_HOST', this.joi.string().required(), 'localhost')
    this.name = this.set('APP_NAME', this.joi.string().required(), 'SupraAPI')
    this.foo = 'bar'
    this.dbUser = ''
    this.dbPassword = ''
    }

    async getSomeAsyncCredentials () {
    const { data } = await getDBCredentials()
    this.dbUser = data.user
    this.dbPassword = data.password
    }

    async init () {
    await getSomeAsyncCredentials()
    logger.debug(`${this.constructor.name}: Initialization finish...`)
    }
    }
    ```
  12. @zmts zmts revised this gist Nov 25, 2019. 1 changed file with 30 additions and 6 deletions.
    36 changes: 30 additions & 6 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -478,27 +478,36 @@ new ErrorWrapper({ ...errorCodes.ACCESS, message: 'Access denied dude !!!' })
    В последствии на фронте выводим ошибку из поля `message` или то что фронтенд посчтитает нужным в зависимости от поля `code`.

    ### Обработка ошибок
    Опишу частую ошибку встречающуюся во многих проектах на просторах интернета. Все ошибки обязаны быть перехваченны в единственном месте, в глобальном обработчике ошибок(глобальная `error middleware`). Остальные `unexpected` ошибки аля `unhandledRejection` в соотвествующих им хендлерам.
    https://github.com/zmts/supra-api-nodejs/blob/master/core/lib/Server.js#L71
    <details>
    <summary>Опишу частую ошибку встречающуюся во многих проектах на просторах интернета</summary>

    __Это означает никаких try/catch и локальной обработки ошибок в контроллерах__
    ```js
    // IS BAD

    testControllerHandler (req, res, next) {
    try {
    // my logic with throwed error
    await testService.getTest({ id })
    await testService.getTest({ id: req.params.id })
    } catch (error) {
    res.send(error) // это и есть локальная обработка ошибки
    }
    }
    ```
    </details>

    Все ошибки обязаны быть перехваченны в единственном месте, в глобальном обработчике ошибок(глобальная `error middleware`). Остальные `unexpected` ошибки аля `unhandledRejection` в соотвествующих им хендлерам. __Это означает никаких try/catch и локальной обработки ошибок в контроллерах__
    https://github.com/zmts/supra-api-nodejs/blob/master/core/lib/Server.js#L71

    <details>
    <summary>Только в случае если нам как-то необходимо обработать/дополнить ошибку делаем так:</summary>

    __Только в случае если нам как-то необходимо обработать/дополнить ошибку делаем так:__
    ```js
    // IS GOOD

    testControllerHandler (req, res, next) {
    try {
    // my logic with throwed error
    await testService.getTest({ id })
    await testService.getTest({ id: req.params.id })
    } catch (error) {
    if (error.code === 'SPECIFIC_ERROR_CODE') {
    error.message = 'Lets describe our specific error'
    @@ -507,6 +516,21 @@ testControllerHandler (req, res, next) {
    }
    }
    ```
    </details>

    <details>
    <summary>В остальных случаях хватит такого</summary>

    ```js
    // IS GOOD

    testControllerHandler (req) {
    // my logic with throwed error
    await testService.getTest({ id: req.params.id })
    }
    ```
    </details>


    ## Logging
    Вместо `console.log` используем логгеры под каждый тип сообщения (`trace`, `warn`, `error`, `fatal`). Логи пишем в `Sentry` или что-то подобное. Ошибки и security issues логируем в первую очередь, дальше все предупреждения и [трассирующие логи](https://en.wikipedia.org/wiki/Tracing_(software))
  13. @zmts zmts revised this gist Nov 25, 2019. 1 changed file with 31 additions and 0 deletions.
    31 changes: 31 additions & 0 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -477,6 +477,37 @@ new ErrorWrapper({ ...errorCodes.ACCESS, message: 'Access denied dude !!!' })
    ```
    В последствии на фронте выводим ошибку из поля `message` или то что фронтенд посчтитает нужным в зависимости от поля `code`.

    ### Обработка ошибок
    Опишу частую ошибку встречающуюся во многих проектах на просторах интернета. Все ошибки обязаны быть перехваченны в единственном месте, в глобальном обработчике ошибок(глобальная `error middleware`). Остальные `unexpected` ошибки аля `unhandledRejection` в соотвествующих им хендлерам.
    https://github.com/zmts/supra-api-nodejs/blob/master/core/lib/Server.js#L71

    __Это означает никаких try/catch и локальной обработки ошибок в контроллерах__
    ```js
    testControllerHandler (req, res, next) {
    try {
    // my logic with throwed error
    await testService.getTest({ id })
    } catch (error) {
    res.send(error) // это и есть локальная обработка ошибки
    }
    }
    ```

    __Только в случае если нам как-то необходимо обработать/дополнить ошибку делаем так:__
    ```js
    testControllerHandler (req, res, next) {
    try {
    // my logic with throwed error
    await testService.getTest({ id })
    } catch (error) {
    if (error.code === 'SPECIFIC_ERROR_CODE') {
    error.message = 'Lets describe our specific error'
    }
    next(error)
    }
    }
    ```

    ## Logging
    Вместо `console.log` используем логгеры под каждый тип сообщения (`trace`, `warn`, `error`, `fatal`). Логи пишем в `Sentry` или что-то подобное. Ошибки и security issues логируем в первую очередь, дальше все предупреждения и [трассирующие логи](https://en.wikipedia.org/wiki/Tracing_(software))

  14. @zmts zmts revised this gist Nov 12, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -22,7 +22,7 @@ code: https://github.com/zmts/supra-api-nodejs
    3. Data layer - прослойка к БД

    ```
    Controller(роутинг) >> Action(проверка прав, схема валидации запроса, логика юзкейса) >> Data Layer(биндинги к БД)
    Controller(роутинг, проверка прав по роли) >> Action(проверка прав по id, схема валидации запроса, логика юзкейса) >> Data Layer(биндинги к БД)
    ```

    ## Содержание
  15. @zmts zmts revised this gist Nov 10, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -55,7 +55,7 @@ async function foo (id, options = {}) {
    `Assertion` это набор статических методов для базовой проверки типов аргуметров и возможно некоторых дополнительных опций (аля `{ required: true, notEmpty: true, positive: true }`).

    ## Config
    Все параметры неободимые для работы приложения должны хранится в эдином конфиге. Параметры конфига делятся на два типа: констаннты и переменные. Первые храним как обычные поля, вторые извлекаем из переменных окружения (для примера это: логины/пароли доступа в БД, ключи для шифрования сессий или JWT токенов, ключи доступа к другим внешним ресурсам итд...).
    Все параметры неободимые для работы приложения должны хранится в едином конфиге. Параметры конфига делятся на два типа: констаннты и переменные. Первые храним как обычные поля, вторые извлекаем из переменных окружения (для примера это: логины/пароли доступа в БД, ключи для шифрования сессий или JWT токенов, ключи доступа к другим внешним ресурсам итд...).
    ```js
    class AppConfig extends BaseConfig {
    constructor () {
  16. @zmts zmts revised this gist Nov 9, 2019. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -232,9 +232,6 @@ __Выходит такой Lifecycle >> Запрос проходит дефо

    Правила валидации (`validationRules`) импортируются из модели или в случае необходимости используются кастомные.

    p.s.
    А еще разделение логики на отдельные экшены облегчает командную работу над проектом. Меньше конфликтов при слиянии веток :)

    ***Роль модели в экшене:***
    Дабы не дублировать кучу подобных моделей(одна модель для создания сущности со всеми `required` полями, другая для обновления, третья для обновления конкретно указанного набора полей итд...) было принято решение не указывать в ф-ции валидации модели `required` требование. Валидаровать поля запроса в экшене помогает небольшой класс-хелпер `RequestRule` в его задачи входит предоставить [базовой ф-ции валидации](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L96) интересующие ее [аргументы](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L112) (ф-цию валидатор и `required` флаг).
    ```js
    @@ -291,7 +288,10 @@ module.exports = CreateAction
    ```
    </details>

    Все эшнены являются __framework agnostic__ это значит что в экшенах отсуствует код относящийся к веб-фреймворку. Что позволеят нам переиспользовать экшены как в других проектах так и сдругими фреймворками.
    Все эшнены являются __framework agnostic__ это значит что в экшенах отсуствует код относящийся к веб-фреймворку. Что позволеят нам переиспользовать экшены как в других проектах так и с другими фреймворками.

    __p.s.__
    А еще разделение логики на отдельные экшены, облегчает командную работу над проектом. Меньше конфликтов при слиянии веток :)

    ## Model
    В данном подходе модель представляет собой исключительно набор полей и правил валидации без какой либо бизнес логики и доп. ф-ционала.
  17. @zmts zmts revised this gist Nov 9, 2019. 1 changed file with 3 additions and 0 deletions.
    3 changes: 3 additions & 0 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -232,6 +232,9 @@ __Выходит такой Lifecycle >> Запрос проходит дефо

    Правила валидации (`validationRules`) импортируются из модели или в случае необходимости используются кастомные.

    p.s.
    А еще разделение логики на отдельные экшены облегчает командную работу над проектом. Меньше конфликтов при слиянии веток :)

    ***Роль модели в экшене:***
    Дабы не дублировать кучу подобных моделей(одна модель для создания сущности со всеми `required` полями, другая для обновления, третья для обновления конкретно указанного набора полей итд...) было принято решение не указывать в ф-ции валидации модели `required` требование. Валидаровать поля запроса в экшене помогает небольшой класс-хелпер `RequestRule` в его задачи входит предоставить [базовой ф-ции валидации](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L96) интересующие ее [аргументы](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L112) (ф-цию валидатор и `required` флаг).
    ```js
  18. @zmts zmts revised this gist Nov 6, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -355,7 +355,7 @@ async function getById (id, options = {}) {
    ```

    ## Clients
    `Clients` - это врапперы над внешними API/клиентами. Предназначение данной абстракции обеспечение единого контракта работы с внешними ресурсами. Допустим нам необходимо использовать в качестве хранилища файлов сервис от Амазон AWS S3. Мы создаем `S3Client` добавляем в него свои методы-обертки для работы с данным хранилищем. В случае критического изменения в клиенте амазона мы соотвественно меняем методы обертки без ущерба и переписывания остальной логики в остальных частях приложения.
    `Clients` - это врапперы над внешними API/клиентами. Предназначение данной абстракции обеспечение единого контракта работы с внешними ресурсами. Допустим нам необходимо использовать в качестве хранилища файлов сервис от Амазон AWS S3. Мы создаем `S3Client` добавляем в него свои методы-обертки для работы с данным хранилищем. В случае критического изменения в клиенте амазона мы соотвественно меняем методы обертки без ущерба и переписывания остальной логики в остальных частях приложения. Единожды создав экземпляр клиент-враппера, можно переиспользовать его в разных местах вместо того что бы плодить инстансы голого клиента.

    <details>
    <summary>client example:</summary>
  19. @zmts zmts revised this gist Nov 6, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -355,7 +355,7 @@ async function getById (id, options = {}) {
    ```

    ## Clients
    `Clients` - это врапперы над внешними API/клиентами. Предназначение данного слоя обеспечение единого контракта работы с внешними ресурсами. Допустим нам необходимо использовать в качестве хранилища файлов сервис от Амазон AWS S3. Мы создаем `S3Client` добавляем в него свои методы-обертки для работы с данным хранилищем. В случае критического изменения в клиенте амазон мы соотвественно меняем методы обертки без ущерба и переписывания остальной логики в остальных частях приложения.
    `Clients` - это врапперы над внешними API/клиентами. Предназначение данной абстракции обеспечение единого контракта работы с внешними ресурсами. Допустим нам необходимо использовать в качестве хранилища файлов сервис от Амазон AWS S3. Мы создаем `S3Client` добавляем в него свои методы-обертки для работы с данным хранилищем. В случае критического изменения в клиенте амазона мы соотвественно меняем методы обертки без ущерба и переписывания остальной логики в остальных частях приложения.

    <details>
    <summary>client example:</summary>
  20. @zmts zmts revised this gist Nov 6, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -233,7 +233,7 @@ __Выходит такой Lifecycle >> Запрос проходит дефо
    Правила валидации (`validationRules`) импортируются из модели или в случае необходимости используются кастомные.

    ***Роль модели в экшене:***
    Дабы не дублировать кучу подобных моделей(одна модель для создания сущности со всеми `required` полями другая для обновления, третья для обновления конкретно указанного набора полей итд...) было принято решение не указывать в ф-ции валидации модели `required` требование. Валидаровать поля запроса в экшене помогает небольшой класс-хелпер `RequestRule` в его задачи входит предоставить [базовой ф-ции валидации](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L96) интересующие ее [аргументы](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L112) (ф-цию валидатор и `required` флаг).
    Дабы не дублировать кучу подобных моделей(одна модель для создания сущности со всеми `required` полями, другая для обновления, третья для обновления конкретно указанного набора полей итд...) было принято решение не указывать в ф-ции валидации модели `required` требование. Валидаровать поля запроса в экшене помогает небольшой класс-хелпер `RequestRule` в его задачи входит предоставить [базовой ф-ции валидации](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L96) интересующие ее [аргументы](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L112) (ф-цию валидатор и `required` флаг).
    ```js
    {
    validationRules: {
  21. @zmts zmts revised this gist Nov 6, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -217,7 +217,7 @@ router.get('/api/users', function(req, res){
    - Забирает результат вычисления и отправляет(`req.json(data)`) пользователю
    - В случе ошибки: собираются метаданные и передаются дальше для логирования

    __Выходит такой Lifecycle >> Запрос попадает в контроллер >> Проходит дефолтные мидлвари >> Попадает в `actionRunner` тот проверяет права доступа текущего юзера к экшену, валидирует запрос и стартует процесс обработки (статическая ф-ция `run` в каждом экшене) >> В экшене выполняется бизнес логика и результат возращается в контроллер >> `actionRunner` получает результат и возвращает его клиенту.__
    __Выходит такой Lifecycle >> Запрос проходит дефолтные мидлвари >> Попадает в контроллер >> Попадает в `actionRunner` тот проверяет права доступа текущего юзера к экшену, валидирует запрос и стартует процесс обработки (статическая ф-ция `run`) >> В экшене выполняется бизнес логика и результат возращается в контроллер >> `actionRunner` получает результат и возвращает его клиенту.__

    Что не стоит делать в контроллере:
    - Валидировать параметры(`req.params`). Как показала практика это плохая идея. Проверку параметров лучше стоит делать непосредственно в экшене. Таким образом в дальнейшем будет более наглядно видно какие парметры в запросе доступны экшену.
  22. @zmts zmts revised this gist Nov 6, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -17,7 +17,7 @@ code: https://github.com/zmts/supra-api-nodejs
    #### Несколько слов про разделение приложения на слои:

    Обычно в слое Контроллеров данные из запроса валидируются и приводятся к виду необходимому для последующего сервисного слоя. В свою очередь сервисный слой полностью изолирует бизнес логику и возвращает результат вычислений в контроллер. Или иначе ? Как вы считаете ? И так каждый программист делает как считает нужно либо так как привык. Давайте будем откровенны. Приводило ли подобная архитектура к тому ради чего задумывалась(к порядку и разделению логики) ? Зачастую все эти попытки сводятся к `big ball of mud`. Зачастую в контроллер просачивается бизнес логика, а в сервисный слой валидация, а то и вообще контекст запроса. Дабы так не происходило данный подход предлагает использовать единый слой для всей логики касаемо конкретного `use case`. Назовем этот слой `Action layer`. В итоге имеем:
    1. Минималистичный контроллер - сугубо маппинг экшенов на роуты.
    1. Минималистичный контроллер - сугубо маппинг экшенов на роуты
    2. Слой экшенов отвечающий за валидацию, всевозможные проверки доступа(владелец/не владелец/админ/не админ/аноним...) и бизнес логику
    3. Data layer - прослойка к БД

  23. @zmts zmts revised this gist Nov 6, 2019. 1 changed file with 4 additions and 10 deletions.
    14 changes: 4 additions & 10 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -16,16 +16,10 @@ code: https://github.com/zmts/supra-api-nodejs

    #### Несколько слов про разделение приложения на слои:

    Часто в интернетах встречается подобная архитектура. Или еще чего __более многослойное__.

    ```
    Controller >> Service layer >> Business layer >> Data layer
    ```

    В средних и более крупных проектах для организации логики приложения используют два таких слоя как `Business layer` и `
    layer`. В двух словах: `Business layer` полностью изолирует бизнес процессы, а `Service layer` валидирует и подготавливает данные для бизнес логики и вызывает ее для дальнейшей отправки полученного результа в контроллер. Или иначе ? Как вы считаете ? Каждый программист делает как считает нужно либо так как привык. Давайте будем откровенны. Приводило ли подобная архитектура к тому ради чего задумывалась(к порядку и разделению логики) ? Зачастую все эти попытки сводятся к `big ball of mud`. Дабы так не происходило данная архитектура предлагает использовать единый слой для всей логики касаемой конкетного `use case`. Назовем этот слой `Action layer`. Да это логически мешает несколько слоев в один. Но в итоге с таким подходом мы имеем больше порядка в коде чем было описано выше. Что в итоге нам нравится разрабатывать ? Код который просто читается и который мы понимаем или спорить(ломать голову) в каком слое должен находится какой кусок кода ? Такой подход скорее всего будет работать для небольших и средних проектов, ну а энтерпраз всегда жил и будет жить своим аутентичным миром.

    В своих проектах я придерживаюсь трех трехслойного подхода.
    Обычно в слое Контроллеров данные из запроса валидируются и приводятся к виду необходимому для последующего сервисного слоя. В свою очередь сервисный слой полностью изолирует бизнес логику и возвращает результат вычислений в контроллер. Или иначе ? Как вы считаете ? И так каждый программист делает как считает нужно либо так как привык. Давайте будем откровенны. Приводило ли подобная архитектура к тому ради чего задумывалась(к порядку и разделению логики) ? Зачастую все эти попытки сводятся к `big ball of mud`. Зачастую в контроллер просачивается бизнес логика, а в сервисный слой валидация, а то и вообще контекст запроса. Дабы так не происходило данный подход предлагает использовать единый слой для всей логики касаемо конкретного `use case`. Назовем этот слой `Action layer`. В итоге имеем:
    1. Минималистичный контроллер - сугубо маппинг экшенов на роуты.
    2. Слой экшенов отвечающий за валидацию, всевозможные проверки доступа(владелец/не владелец/админ/не админ/аноним...) и бизнес логику
    3. Data layer - прослойка к БД

    ```
    Controller(роутинг) >> Action(проверка прав, схема валидации запроса, логика юзкейса) >> Data Layer(биндинги к БД)
  24. @zmts zmts revised this gist Oct 28, 2019. 1 changed file with 0 additions and 2 deletions.
    2 changes: 0 additions & 2 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -621,5 +621,3 @@ function foo (firstRequired, secondRequired, { someOptionalParam = false } = {})
    Видео Виктора Турского про архитектуру
    - https://www.youtube.com/watch?v=Z08xL-oXMh0 (2017)
    - https://www.youtube.com/watch?v=TjvIEgBCxZo (2019)

    _Если что вопросы/предложения пишите в коментарии/приват https://t.me/sasha_zmts постараюсь ответить._
  25. @zmts zmts revised this gist Oct 24, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -41,7 +41,7 @@ Controller(роутинг) >> Action(проверка прав, схема ва
    - [Model](#model)
    - [Clients](#clients)
    - [Providers](#providers)
    - [Helpers](#helpers)
    - [Helpers](#authhelpers)
    - [DAO](#dao)
    - [Errors](#errors)
    - [Logging](#logging)
  26. @zmts zmts revised this gist Oct 24, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -446,7 +446,7 @@ module.exports = new RootProvider()
    ```
    </details>

    ## Helpers
    ## Auth(helpers)
    Хелперы отвечают за всевозможный процессинговые и утилитарные ф-ции(шифрование, JWT) В своем большинстве реализованные через промис.

    ## DAO
  27. @zmts zmts revised this gist Oct 24, 2019. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -239,12 +239,12 @@ __Выходит такой Lifecycle >> Запрос попадает в кон
    Правила валидации (`validationRules`) импортируются из модели или в случае необходимости используются кастомные.

    ***Роль модели в экшене:***
    Дабы не дублировать кучу подобных моделей(одна модель для создания сущности со всеми `required` полями другая для обновления, третья для обновления конкретно указанного набора полей итд...) было принято решение не указывать в ф-ции валидации модели `required` требование. Валидаровать поля запроса в экшене помогает небольшой класс-хелпер `RequestRule` в его задачи входит предоставить [базовой ф-ции валидации](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L96) интерисующие ее [аргументы](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L112) (ф-цию валидатор и `required` флаг).
    Дабы не дублировать кучу подобных моделей(одна модель для создания сущности со всеми `required` полями другая для обновления, третья для обновления конкретно указанного набора полей итд...) было принято решение не указывать в ф-ции валидации модели `required` требование. Валидаровать поля запроса в экшене помогает небольшой класс-хелпер `RequestRule` в его задачи входит предоставить [базовой ф-ции валидации](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L96) интересующие ее [аргументы](https://github.com/zmts/supra-api-nodejs/blob/master/controllers/BaseController.js#L112) (ф-цию валидатор и `required` флаг).
    ```js
    {
    validationRules: {
    body: {
    id: new RequestRule(PostModel.schema.id, true)
    id: new RequestRule(PostModel.schema.id, { required: true })
    }
    }
    }
  28. @zmts zmts revised this gist Oct 24, 2019. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -174,12 +174,12 @@ actionRunner (action) {
    <summary>Таким образом контроллер представляет собой совсем небольшую прослойку мапинга экшенов на роуты</summary>

    ```js

    const router = require('express').Router()
    const actions = require('../actions/posts')
    const BaseController = require('../core/BaseController')
    const ErrorWrapper = require('../core/ErrorWrapper')
    const { errorCodes } = require('../config')
    const logger = require('../logger')

    class PostsController extends BaseController {
    get router () {
    @@ -195,7 +195,7 @@ class PostsController extends BaseController {
    }

    async init () {
    __logger.info(`${this.constructor.name} initialized...`)
    logger.info(`${this.constructor.name} initialized...`)
    }
    }

  29. @zmts zmts revised this gist Sep 10, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -44,7 +44,7 @@ Controller(роутинг) >> Action(проверка прав, схема ва
    - [Helpers](#helpers)
    - [DAO](#dao)
    - [Errors](#errors)
    - [Logging](#errors)
    - [Logging](#logging)
    - [Policy/Roles/Permissions](#policyrolespermissions)

    ## Assertion(type checking)
  30. @zmts zmts revised this gist Sep 10, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion aboutNodeJsArchitecture.md
    Original file line number Diff line number Diff line change
    @@ -228,7 +228,7 @@ __Выходит такой Lifecycle >> Запрос попадает в кон
    Что не стоит делать в контроллере:
    - Валидировать параметры(`req.params`). Как показала практика это плохая идея. Проверку параметров лучше стоит делать непосредственно в экшене. Таким образом в дальнейшем будет более наглядно видно какие парметры в запросе доступны экшену.

    ## Action
    ## Actions
    Основным ключевым моментом является использование отдельного класса для каждого эндпоинта. Я определяю понятие `Action` как класс инкапсулирующий всю логику работы эндпоинта. То есть для реализации круда у нас будет 5 файлов (`CreatePostAction`, `GetPostByIdAction`, `UpdatePostAction`, `DeletePostAction`, `ListPostsAction`) по экшену на каждый эндпоинт.

    Каждый экшен обязан имплементировать такой контракт: