Создаём REST API сервер на Hapi часть 2: ORM sequelize, инициализация базы данных, конфиги

09.12.2018 09:00

Для работы с базами данных будем использовать ОРМ sequelize. Если Вы читаете это, то Вам даже не стоит задумываться о том, чтобы использовать прямые запросы в бд.

Создаём папку models в src, в которой создаём пару простеньких моделек:

// ./src/models/users.js
'use strict';

const Bcrypt = require('bcryptjs');
const bcryptRounds = 10;

module.exports = (sequelize, DataTypes) => {
  const users = sequelize.define('users', {
    id: {
      allowNull: false,
      autoIncrement: true,
      primaryKey: true,
      type: DataTypes.INTEGER
    },
    name: {
      type: DataTypes.STRING
    },
    email: {
      type: DataTypes.STRING
    },
    password: {
      type: DataTypes.STRING,
      allowNull: false,
      set (val) {
        this.setDataValue('password', Bcrypt.hashSync(val, bcryptRounds));
      }
    }
  });

  users.prototype.verifyPassword = function (unencrypted) {
    return Bcrypt.compareSync(unencrypted, this.get('password'));
  };

  users.dummyData = [
    {
      id: 1,
      name: 'pupkin',
      email: 'pupkin@gmail.com',
      password: Bcrypt.hashSync('12345', bcryptRounds)
    }
  ];

  return users;
};
// ./src/models/messages.js

'use strict';

module.exports = (sequelize, DataTypes) => {
  const messages = sequelize.define('messages', {
    id: {
      allowNull: false,
      autoIncrement: true,
      primaryKey: true,
      type: DataTypes.INTEGER
    },
    user_id: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    message: {
      type: DataTypes.STRING,
      allowNull: false,
    }
  });

  messages.dummyData = [
    {
      id: 1,
      user_id: 1,
      message: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat'
    },
    {
      id: 2,
      user_id: 1,
      message: 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo'
    }
  ];

  return messages;
};

Прежде чем мы сможем работать с бд, нам нужно её инициализировать, создаём scripts/dbInit.js

console.log('DB init');

const fs = require('fs');
const Sequelize = require('sequelize');
const filepaths = require('filepaths');

const config = require('../config');

async function main() {
  if( !fs.existsSync(__dirname + '/../db') ) {
    fs.mkdirSync(__dirname + '/../db');
  }

  let sequelize = new Sequelize(config.db);

  // Загружаем все наши модельки
  for(let modelFile of filepaths.getSync(__dirname + '/../src/models')) 
    require(modelFile)(sequelize, Sequelize.DataTypes);

  // Синхронизируем модели с реальной базой данных, ели она не создана, в этот момент она создастся автоматически
  await sequelize.sync();

  // Пробегаемся по всем табличкам и проверяем сколько в них есть строк
  for(let tableName in sequelize.models) {
    let rowsCount = await sequelize.models[ tableName ].count();

    // если строк 0, то добавляем тестовые данные
    if( rowsCount == 0 ) {
      console.log('>', tableName, 'rows count:', rowsCount, 'add dymmy data to the table...');
      sequelize.models[ tableName ].bulkCreate(sequelize.models[ tableName ].dummyData);
    } else {
      // если > 0 то ничего не делаем
      console.log('>', tableName, 'rows count:', rowsCount, 'table data is already initialized');
    }
  }

  console.log('DB init DONE');
}

main();

В package.json, добаляем команду в секцию scripts:

...
  "scripts": {
    "dbinit": "node ./scripts/dbInit.js"
  },
...

Теперь нам доступна команда: npm run dbinit которая автоматически создаст базу данных с тестовыми записями. Далее есть 2 стула способа использовать модельки в запросах. Сперва расскажу про менее удобный:

Создаём загрузчик моделек:

// ./src/libs/db.js

console.log('DB init');

const fs = require('fs');
const Sequelize = require('sequelize');
const filepaths = require('filepaths');

const config = require('../../config');

function main() {
  if( !fs.existsSync(__dirname + '/../../db') ) {
    fs.mkdirSync(__dirname + '/../../db');
  }

  let sequelize = new Sequelize(config.db);

  // Загружаем все наши модельки
  for(let modelFile of filepaths.getSync(__dirname + '/../models')) 
    require(modelFile)(sequelize, Sequelize.DataTypes);

  return sequelize;
}

module.exports = main();

И тестовый экшен:

// ./src/routes/messages_wrong/get.js

const db = require('../../libs/db');

async function response() {
  let data = await db.models.messages.findAll();
  let count = await db.models.messages.count();

  return {
    meta: {
      total: count
    },
    data: data
  };
}

module.exports = {
  method: 'GET',
  path: '/messages_wrong',
  options: {
    handler: response
  }
};

Запрос:

curl http://127.0.0.1:3030/messages_wrong
{"meta":{"total":2},"data":[{"id":1,"user_id":1,"message":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat","createdAt":"2018-12-09T08:53:21.948Z","updatedAt":"2018-12-09T08:53:21.948Z"},{"id":2,"user_id":1,"message":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo","createdAt":"2018-12-09T08:53:21.948Z","updatedAt":"2018-12-09T08:53:21.948Z"}]}

Теперь более правильный метод подключения sequelize в hapi. Нам понадобится пакет hapi-sequelizejs и немного подправить наш server.js

// server.js
#!/usr/bin/env node
'use strict';

const Hapi = require('hapi');
const filepaths = require('filepaths');
const Sequelize = require('sequelize');
const hapiBoomDecorators = require('hapi-boom-decorators');

const config = require('./config');

async function createServer() {
  // Инициализируем сервер
  const server = await new Hapi.Server(config.server);

  await server.register([
    hapiBoomDecorators,
    // Добавляем это
    {
      plugin: require('hapi-sequelizejs'),
      options: [
        {
          name: config.db.database, 
          models: [__dirname + '/src/models/*.js'], // Путь к моделькам
          //ignoredModels: [__dirname + '/server/models/**/*.js'], // Можем некоторые модельки заигнорить
          sequelize: new Sequelize(config.db), // Инициализируем обычный секьюлайз и передаём его параметром
          sync: true, // Синхронизировать/нет модели с реальной бд
          forceSync: false, // Если тру, то таблицы будут дропнуты перед синхронизацией, остарожно
        },
      ],
    }
    // Конец
  ]);

  // Загружаем все руты из папки ./src/routes/
  let routes = filepaths.getSync(__dirname + '/src/routes/');
  for(let route of routes)
    server.route( require(route) );
  
  // Запускаем сервер
  try {
    await server.start();
    console.log(`Server running at: ${server.info.uri}`);
  } catch(err) { // если не смогли стартовать, выводим ошибку
    console.log(JSON.stringify(err));
  }

  // Функция должна возвращать созданый сервер, зачем оно нужно, расскажу далее
  return server;
}

createServer();

Теперь, при обработке каждого реквеста, внутри объекта request, нам будет доступен метод getModel, с помощью которого уже можем выбрать нужно базу данных и конкретную модель, а потом уже посылать запросики.

// src/routes/messages/get.js

const config = require('../../../config');

async function response(request) {

  // Вот тут выбираем модель 
  const messages = request.getModel(config.db.database, 'messages');

  // И дальше используем её как обычно
  let data = await messages.findAll();
  let count = await messages.count();

  return {
    meta: {
      total: count
    },
    data: data
  };
}

module.exports = {
  method: 'GET',
  path: '/messages',
  options: {
    handler: response
  }
};

В нашем реквесте осталась ещё одна противная строчка, которая будет копипаститься из файла в файл:

...
const config = require('../../../config');
...

Мы и её тоже можем засунуть в объект request, для этого нам нужно подписаться на события "onRequest" и уже в нём инжектить конфиг:

// server.js
...
  server.ext({
    type: 'onRequest',
    method: async function (request, h) {
      request.server.config = Object.assign({}, config);
      return h.continue;
    }
  });
...

И внутри обработчика реквеста, теперь не нужно рекваирить конфиг, а можно использовать request.server.config

// src/routes/messages/get.js

async function response(request) {

  const messages = request.getModel(request.server.config.db.database, 'messages');

  let data = await messages.findAll();
  let count = await messages.count();

  return {
    meta: {
      total: count
    },
    data: data
  };
}

module.exports = {
  method: 'GET',
  path: '/messages',
  options: {
    handler: response
  }
};

Код сервера на гитхабе: https://github.com/hololoev/api_hapi_example_2



Создаём REST API сервер на Hapi часть 1: Создаём базовую версию

Я уже писал как создать API сервер на вебсокетах и express.js. В этот раз мы будем делать классически REST сервер. В качестве основы будем использовать Hapi.

Бухгалтерская/Аудиторская компания в Грузии

Нашел аудиторскую компанию в Грузии, в Батуми, как обычно по сарафанному радио. Сперва походил по меткам на гуглокарте, в 3 местах уже не было компаний. Забрёл в офис местных айтишников и уже они вывели меня на местных аудиторов.


(0) Комментариев