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

Будем реализовывать продвинутый функционал постепенно в следующем порядке:

1. Добавление кастомного API к моделям;

2. Добавление валидации данных к моделям;

3. Добавление хуков при работе с моделями;

4. Добавление прав выполнения операций (ACL).

Добавление кастомного API к моделям

Давайте добавим новый метод для голосования за статью, назовем его vote. Для этого открываем консоль и набираем команду:

lb remote-method

Следуем указаниям утилиты. Выбираем модель Article, вводим название метода vote, он будет у нас методом объекта - ставим No, вводим http-путь к методу - /vote и тип http-запроса - put. Далее добавим один аргумент к методу (up) и делаем его boolean типом. Результат положительного ответа будет объект result. Все, наш новый метод готов. Вывод консольной утилиты можно посмотреть на рисунке:

После добавления метода можно протестировать его работу в explorer. Но при первом же его вызове у нас приходит 500 ошибка. Это происходит из-за того, что метод в описание модели добавился (файл /models/article.json), а вот сам код метода в прототип еще мы не записали. Добавив в файл article.js код, сгенерированный утилитой, мы уберем данную ошибку. Теперь добавим к модели Article новое свойство, которое будет отображать количество голосов за статью. Для этого вновь воспользуемся утилитой для добавления свойства, как было описано в предыдущей, первой статье:

lp property

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

Теперь немного изменим код метода vote в article.js:

'use strict';

module.exports = function (Article) {

  /**
  * Validation rules
  */

  Article.validatesNumericalityOf('votes', {int: true});
  Article.validatesLengthOf('title', {min: 3});
  Article.validatesUniquenessOf('title');

  /**
  * Methods
  */

  Article.prototype.vote = function (up, callback) {
    let me = this,
    result = {
    votes: 0
  };

  me.votes = up ? me.votes + 1 : me.votes - 1;

  if (me.votes < 0) {
     me.votes = 0;
  }

  me.save({validate: true}, (err, article) => {
    result = {
     votes: me.votes
    };
    callback(err, result);
    });
  };
}

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

Добавление валидации данных к моделям

На рисунке выше мы также можем видеть код, который находится вне метода vote. Это правила валидации полей модели Article. Добавляются они достаточно просто. Стандартные валидаторы, поставляемые из коробки, определены методами класса. Также есть возможность определить свой валидатор. Для этого необходимо объявить обычную javascript функцию и методом Article.validate прикрепить данный валидатор к полю:

const myOwnValidator = function(err) {
     ...some validation logic
     if (this.name === 'hello'} err();
}

MyModel.validate('name', myOwnValidator, {message: 'Bad name'});

На данный момент используем три стандартных валидатора:

  • Первый проверяет, что число голосов является целым числом.
  • Второй - значение заголовка статьи уникально.
  • Третий - минимальную длину заголовка.
Article.validatesNumericalityOf('votes', {int: true});
Article.validatesUniquenessOf('title');
Article.validatesLenghtOf('title', {min: 3});

И если вы попробуете сохранить статью, не удовлетворяющую данным требованиям валидации, то в ответ сервер отправит объект с описанием ошибок.

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

Article.validateAsync('property', func, options)

Добавление хуков при работе с моделями

Настало время добавить хуки к нашей модели. Это такие функции, которые выполняются перед операциями с моделями (что-то вроде событий). Фреймворк предоставляет множество всевозможных хуков на всех этапах жизни модели:

  • before save
  • after save
  • before delete
  • after delete
  • access

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

/**
* Hooks
*/

Article.observe('before save', (ctx, next) => {
  if (ctx.instance && ctx.instance.blog_id) {
    return Article.app.models.Blog
      .count({id: ctx.instance.blog_id})
      .then(res => {
        if (res < 1) {
          return Promise.reject('Error adding article to non-existing blog.');
        }
      });
  }

  if (ctx.instance.blog_id === 0) {
     return Promise.reject('Error adding article without blog.');
  }

  return next();
});

Также добавим проверку при удалении блога, что в нем нем статей. Т.е., если в блоге присутствуют статьи, то мы не дадим удалить такой блог:

'use strict';

module.exports = function(Blog) {

  /**
  * Validation rules
  */

  Blog.validatesLengthOf('title', {min: 3});
  Blog.validatesUniquenessOf('title');

  /**
  * Hooks
  */

  Blog.observe('before delete', (ctx) => {
    return Blog.app.models.Article
      .count({blog_id: ctx.where.id})
      .then(res => {
        if (res > 0) {
          return Promise.reject('Error deleting blog with articles.');
        }
     });
  });
};

Добавление прав выполнения операций (ACL)

ACL - access control list. Он необходим для определения прав пользователей на те или иные действия в приложении.

Будем устанавливать права доступа по следующему алгоритму - сначала всем все запретим, а потом будем давать разрешения на определенные действия.

Для начала давайте запретим всем неавторизированным пользователям работать со статьями. Как всегда открываем консоль и пишем команду:

lb acl

Далее следуем командам утилиты и выбираем варианты как показана на рисунке.

После выполнения этих действий в файле article.json у нас появится следующие строки, которые отвечают за то, что все действия пользователям запрещены:

"acls": [
  {
    "accessType": "*",
    "principalType": "ROLE",
    "principalId": "$unauthenticated",
    "permission": "DENY"
  },
  {
    "accessType": "EXECUTE",
    "principalType": "ROLE",
    "principalId": "$unauthenticated",
    "permission": "ALLOW",
    "property": "find"
  },
  {
    "accessType": "EXECUTE",
    "principalType": "ROLE",
    "principalId": "$unauthenticated",
    "permission": "ALLOW",
    "property": "vote"
  }
],

Для проверки, что все изменения работают, откроем explorer и попробуем выполнить операции с моделью Article. В ответ, как и следовало ожидать, мы получим ошибку:

То, что неавторизированные пользователи не могут посмотреть список статей нас, конечно, не устраивает. Тогда давайте разрешим всем пользователям эту операцию. Открываем консоль и пишем lb acl. Выбираем модель Article, далее отмечаем, что будем разрешать только один метод, вводим его название find, выбираем роль все неавторизированные пользователи и устанавливаем всем разрешение. Аналогично можно добавить такое разрешение и на метод vote.

Такие же операции необходимо проделать с моделью Blog. Для этого, в принципе, достаточно просто скопировать описание acl из файла article.json и поместить в соответствующую секцию blog.json.

 "acls": [
  {
    "accessType": "*",
    "principalType": "ROLE",
    "principalId": "$unauthenticated",
    "permission": "DENY"
  },
  {
    "accessType": "EXECUTE",
    "principalType": "ROLE",
    "principalId": "$unauthenticated",
    "permission": "ALLOW",
    "property": "find"
  }
],

Вот теперь наше API готово к работе. Но для более качественного кода не хватает, конечно, тестов. Но это уже тема следующей части.

Скачать исходники можно тут:

git clone https://github.com/dionic-i/lb-blog.git​​​​​​​

После загрузки переходим в папку проекта:

npm i
git checkout advanced-models
npm run dev