Рубрики
Без рубрики

Леса для лесов Nodejs GraphQL API Server

Недавно я работаю над новым проектом по полным стекам (https://github.com/tomlagier/crypto-dca), и я хотел принять возможность попробовать мою руку при создании API GraphQL. Я не нашел много …

Автор оригинала: Tom Lagier.

Недавно я работал над Новый полный стек проекта И я хотел принять возможность попробовать руку при создании API GraphQL. Я не нашел много ресурсов, которые проходили по всей стеке на задней части от DB до ORM до дизайна API, включая тесты, поэтому я подумал, что могу помочь некоторым другим людям, выкладывая то, что я оказался.

TL; DR:

  1. Докер и докер-состав
  2. Узел сервер
  3. Postgres база данных
  4. Экспресс и Apollo-Server-Express обслуживать запросы
  5. Sequelize Омма
  6. graphql Язык схемы
  7. GraphQL-Sequelize как разъем
  8. Уинстон для ведения журнала
  9. Паспорт с Экспресс-сессия и Connect-Session-Sequelize Для аутентификации и авторизации
  10. моча для тестирования

Что такое график?

Мощность графаql, от https://www.graphql.com/

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

Почему graphql?

Я выбрал GraphQL, потому что, услышав много гудок и играя с некоторыми открытыми Конечные точки Graphiql Я был зацеплен идеей того, чтобы быть в состоянии объявить схему для моих данных и позволить клиенту обращаться с точной формой запроса. Мне понравилось идею передачи минимального количества на провод, дозируя запросы вместе для оптимальных полезных полезных нагрузок и широко кэшируйте как на клиенте, так и на сервере.

Что еще тебе нужно?

Это вопрос, который мне давно подумал. Есть много замечательных примеров для начала работы с сервером GraphQL, но все, казалось, уходили после того, как указываете, что решить Функция должна выполнять работу приема данных для выполнения определенного поля. Итак, от земли вверх, это кусочки, которые я собрал вместе:

  1. Контейнеры для разработки и развертывания
  2. Сервер
  3. База данных
  4. Сервер запроса
  5. ОРМ
  6. ORM для адаптера GraphQL
  7. Схема GraphQL
  8. логирование
  9. Аутентификация и авторизация
  10. Тесты

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

Контейнеры

Мое первое решение было использование контейнеров для инкапсуляции моего API-сервера и базы данных. Моя основная мотивация здесь было упрощение развертывания – для меня гораздо легче загрузить набор изображений на Докер Хаб и сделать простой git pull && docker-compose up -d Чем нужно написать несколько сценариев, чтобы настроить мою среду каждый раз. Docker с Docker-Compose также позволяет легко управлять несколькими средами для разработки, тестирования и производства. Он также упрощает запуск и разрыв местно, где я бегу несколько отдельных сервисов для приложения.

Итак, мне нужно было создать Dockerfile и а Docker-Compose.yml :

DockerFile:

FROM mhart/alpine-node:9RUN mkdir www/WORKDIR www/ADD . .RUN npm install && npm run buildCMD npm run start

docker-compose.yml:

version: "3"services: api: build: ./api image: crypto-dca-api:latest container_name: crypto-dca-api env_file: config/.env environment: - NODE_ENV=production ports: - 8088:8088
db: build: ./db image: crypto-dca-db:latest container_name: crypto-dca-db env_file: config/.env volumes: - crypto-dca-db:/var/lib/postgresql/data ports: - 5432:5432
volumes: crypto-dca-db: driver: local

С этим я могу запустить Docker-Compose Up -D Из корня моего проекта и разверните сервер и базу данных, используя постоянный том. Я также заканчивал создание отдельных контейнеров для разработки, а также другая среда для тестирования. Я оставлю теми, как упражнение для читателя, это просто основной пример того, как выглядит настройка производства.

Чтобы узнать больше о Docker и Docker-Compose, проверьте Официальный учебник

Сервер

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

Я выбрал Узел Как мой язык сервера, потому что я приезжаю с фона на переднем углу, и именно имеет смысл использовать свой опыт домена при создании сервера. Узел является хорошим выбором, потому что он имеет чрезвычайно надежную поддержку сообщества вокруг GraphQL и очень портативно и легко работает в контейнере.

С новым Async/await Поддержка нашей версии 8, модель синхронности намного проще управлять, что является огромным благополумом при построении высокосинхронного API.

База данных

Я пошел с Postgres В качестве моей базы данных на пару причин: она имеет обширную поддержку сообщества с ORMS, это открытый источник, и он легкий. MySQL будет работать так же хорошо Хотя PostgreSQL имеет репутацию на масштабирование лучше. В конечном итоге, БД хорошо рассуждается за ORM, поэтому он относительно просто, если это необходимо, если это необходимо.

Вот (мертвый простой) Dockerfile Для моей базы данных:

FROM postgres:10COPY ./setup-db.sh /docker-entrypoint-initdb.d/setup-db.sh

И мой файл init:

#!/bin/bash
set -e
POSTGRES="psql --username ${POSTGRES_USER}"DATABASES=($POSTGRES_DEV_DB $POSTGRES_TEST_DB $POSTGRES_PROD_DB)
for i in ${DATABASES[@]}; do echo "Creating database: ${i}" psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = '${i}'" | grep -q 1 || psql -U postgres -c "CREATE DATABASE \"${i}\""done

Файл init просто создает базы данных, которые не существуют на запуск контейнера.

Сервер запроса

С основными основаниями, мы можем начать выкладывать кусочки вместе, чтобы на самом деле служить запросу нашим API. Я выбрал Экспресс В качестве базового WebServer Framework из-за его повсеместливости в экосистеме узла, поддержки плагинов и простой API.

Express позволяет нам прослушивать порт и отвечать на HTTP-запросы, но нам нужен другой слой, чтобы позволить нам переваривать и отвечать на запросы GraphQL. Для этого я использую Apollo-Server-Express Отказ Он имеет чрезвычайно простую API и имеет некоторое отображение, чтобы позволить нам определить нашу схему в языке схемы Graphql узела. Вот что он выглядит в действии:

const bodyParser = require('body-parser');const { graphqlExpress, graphiqlExpress } = require('apollo-server-express');const logger = require('../helpers/logger');const { NODE_ENV } = process.env;
module.exports = function (app) { const schema = require('../schema');
app.use('/graphql', bodyParser.json(), (req, res, next) => graphqlExpress({ schema, context: { user: req.user } })(req, res, next) );
if (NODE_ENV === 'development') { app.get('/graphiql', graphiqlExpress({ endpointURL: '/graphql' })); }
logger.info(`Running a GraphQL API server at /graphql`);}

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

ОРМ

Для того чтобы отобразить информацию между языком запроса вашей базы данных (SQL) и родным языком вашего сервера (JavaScript), вы обычно используете ORM. Есть несколько Популярные ORMS в JavaScript , но я решил пойти с Sequelize Поскольку оно самая сильно поддерживаемая, поставляется с инструментом CLI и имеет много активной поддержки сообщества.

Чтобы подключить Sequelize в вашей базе данных, вам нужно сделать несколько вещей. К сожалению, между существующей версией разрыв между существующей версией Sequelize-Cli и последняя версия Sequelize (4). Вы все еще можете использовать SEXELIZE-CLI, чтобы подкрепить приложение Sequelize, но вам может потребоваться сделать некоторые модификации

Чтобы начать, вы можете установить Sequelize-CLI и запустить Sequelize init из вашего каталога проекта. По умолчанию это создаст новую структуру каталогов с конфигурацией, моделями, миграциями и сеянами, а также index.js Файл в каталоге моделей, который создает новый экземпляр ORM с заданной конфигурацией и связывает все модели с этим экземпляром.

Я оказался расщеплением этого в 2 файла для более легкого тестирования:

build-db.js :

var env = process.env.NODE_ENV || 'development';var config = require(__dirname + '/../config/config.js')[env];var Sequelize = require('sequelize');
module.exports = function () {
return config.use_env_variable ? new Sequelize(process.env[config.use_env_variable]) : new Sequelize(config.database, config.username, config.password, config);}

Decorate-db.js :

const fs = require('fs');const path = require('path');const Sequelize = require('sequelize');const modelPath = path.join(__dirname, '../models');
module.exports = function (sequelize) {
const db = {};
fs .readdirSync(modelPath) .filter(file => { return file.indexOf('.') === -1 }) .forEach(folder => { const model = sequelize['import']( path.join(modelPath, folder, 'index.js') ); db[model.name] = model; });
Object.keys(db).forEach(modelName => { if (db[modelName].associate) { db[modelName].associate(db); } });
db.sequelize = sequelize; db.Sequelize = Sequelize;
return db;}

Оттуда вы можете определить свои модели вручную или использовать инструмент CLI, чтобы помочь построить их.

Модели/кошелек/index.js :

const { v4 } = require('uuid');
module.exports = (sequelize, DataTypes) => { const Wallet = sequelize.define('Wallet', { id: { primaryKey: true, type: DataTypes.STRING, defaultValue: () => v4() }, name: { type: DataTypes.STRING, allowNull: false }, address: { type: DataTypes.STRING, allowNull: false }, local: { type: DataTypes.BOOLEAN, allowNull: false } });
Wallet.associate = function ({ User, Wallet }) { Wallet.belongsTo(User); }
return Wallet;};

Как только вы закончите, у вас должен быть объект в JS, который вы можете импортировать в другие файлы, которые дают вам полный доступ к вашей базе данных.

Один изложенный, который я обнаружил, что легко генерируя обе миграции, так и в моей первоначальной форме базы данных было фактически выбросить состояние моего DB в файл и использовать это как необработанный импорт SQL. Это сохранило меня много времени по написанию синтаксиса миграции (который не совсем такой же, как синтаксис определения модели), а также писать сеялки – у меня просто есть несколько файлов SQL, которые я могу загрузить в качестве тестовых состояний или начальное состояние семян для разработки Отказ

Миграция/1-начальное состояние .js :

const { readFile } = require('fs');
module.exports = { up(migration) { return new Promise((res, rej) => { readFile( 'migrations/initial-tables.sql', 'utf8', (err, sql) => { if (err) return rej(err); migration.sequelize.query(sql, { raw: true }) .then(res) .catch(rej); }); }); }, down: (migration) => { return migration.dropAllTables(); }}

ORM для адаптера GraphQL

Один критический пакет, который нам нужно добавить, – это код, который позволяет нам легко отображать моды Sequelize для типов, запросов и мутаций GraphQL. Удобно называется GraphQL-Sequelize Пакет делает это довольно хорошо, предоставляя два отличных абстракция, которые я буду обсуждать ниже – A Resolver Для отображения запросов GraphQL для секвенирования операций и Attributefields Сопоставление, позволяющее нам повторно использовать определения наших моделей в виде списков полей типа GraphQL.

Схема GraphQL

Whaw! Все это работает, и мы даже не написали, что GraphQl пока может понять. Не волнуйтесь, мы там добираемся. Теперь, когда у нас есть представление нашей базы данных JavaScript, нам нужно сопоставить схему GraphQL.

Есть два куска схемы GraphQL, которые нам нужно создать. Первый – это набор типов, который позволяет нам правильно указывать форму наших данных. Второй – список запросов и мутаций, которые мы можем использовать для поиска и манипулирования нашими данными.

Типы

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

Модели/кошелек/Type.js :

const { GraphQLObjectType, GraphQLNonNull, GraphQLBoolean, GraphQLString} = require('graphql');
module.exports = new GraphQLObjectType({ name: 'Wallet', description: 'A wallet address', fields: () => ({ id: { type: new GraphQLNonNull(GraphQLString), description: 'The id of the wallet', }, name: { type: new GraphQLNonNull(GraphQLString), description: 'The name of the wallet', }, address: { type: new GraphQLNonNull(GraphQLString), description: 'The address of the wallet', }, local: { type: new GraphQLNonNull(GraphQLBoolean), description: 'Whether the wallet is local or on an exchange' } })});

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

Недостатком является определенно влагоподъемность – это много работы в основном повторно определить все ваши модели в виде типов GraphQL. К счастью, GraphQL-Sequelize Пакет дает нам ярлык через Attributefields :

const { GraphQLObjectType, GraphQLNonNull, GraphQLBoolean, GraphQLString} = require('graphql');const { attributeFields } = require('graphql-sequelize');const { Wallet } = require('../');
module.exports = new GraphQLObjectType({ name: 'Wallet', description: 'A wallet address', fields: attributeFields(Wallet);});

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

Запросы и мутации

Типы представляют собой кусочки данных в нашей схеме, в то время как запросы и мутации представляют собой способы взаимодействия с этими деталями данных. Я решил создать несколько основных запросов для каждой из моих моделей – некоторые включают поиск, а также некоторые благополучие модификации. Resolver Предусмотрено Sequelize Graphql-Sequelize, делает создание этих абсолютного ветра и начинает показывать некоторую мощность за графиком сочетания с хорошим ORM.

Модели/кошелек/Queries.js :

const { GraphQLNonNull, GraphQLString, GraphQLList} = require('graphql');const { Op: {iLike} } = require('sequelize');const { resolver } = require('graphql-sequelize');const walletType = require('./type');const sort = require('../../helpers/sort');
module.exports = Wallet => ({ wallet: { type: walletType, args: { id: { description: 'ID of wallet', type: new GraphQLNonNull(GraphQLString) } }, resolve: resolver(Wallet, { after: result => result.length ? result[0] : result }) }, wallets: { type: new GraphQLList(walletType), resolve: resolver(Wallet) }, walletSearch: { type: new GraphQLList(walletType), args: { query: { description: 'Fuzzy-matched name of wallet', type: new GraphQLNonNull(GraphQLString) } }, resolve: resolver(Wallet, { dataLoader: false, before: (findOptions, args) => ({ where: { name: { [iLike]: `%${args.query}%` }, }, order: [['name', 'ASC']], ...findOptions }), after: sort }) }})

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

Мутации вполне похожи, хотя вам нужно сделать нагрузку на обновление модели самостоятельно:

Модель/пользователя/мутации .JS :

const { GraphQLNonNull, GraphQLString} = require('graphql');const userType = require('./type');const { resolver } = require('graphql-sequelize');
module.exports = User => ({ createUser: { type: userType, args: { name: { description: 'Unique username', type: new GraphQLNonNull(GraphQLString) }, password: { description: 'Password', type: new GraphQLNonNull(GraphQLString) } }, resolve: async function(root, {name, password}, context, info){ const user = await User.create({ name, password }); return await resolver(User)(root, {id: user.id}, context, info); } }});

С нашими типами, запросами и мутациями созданы, нам просто нужно прошить все вместе в одну схему и подключить его в Apollo-Server-Express:

schema.js :

const { GraphQLObjectType, GraphQLSchema,} = require('graphql');const { queries, mutations } = require('./models/fields');
module.exports = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQuery', fields: () => queries }), mutation: new GraphQLObjectType({ name: 'RootMutation', fields: () => mutations })});

И вуаля! Теперь мы можем начать ударить наш сервер на /graphql и /gaphiql И взаимодействуя с схемой на нашей базе данных.

Нам нужно еще пару кусков, чтобы иметь надежное решение API. Просто быть в состоянии сыграть с нашим API, не означает, что это проверено, поддерживаемым или защищенным. Я кратко поговорим о том, как проверять эти произведения.

логирование

Регистрация – это жизненная часть любого проекта. Это позволяет нам легко идентифицировать именно то, что происходит с нашим приложением и отслеживать ошибки, как они происходят. После игры с ручными бревнами, я решил наубиться с известным пакетом под названием Уинстон Отказ Это позволяет мне установить глобальные уровни журнала и войти в крепкий , Стержер , файл или удаленный API, если я хочу.

помощники/logger.js :

const { NODE_ENV } = process.env;const winston = require('winston');
let level, transports;
switch (NODE_ENV) { case 'development': level = 'verbose'; transports = [new winston.transports.Console()]; break;
case 'production': level = 'verbose'; transports = [new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log', level: 'verbose' })] break;}
module.exports = winston.createLogger({ level, transports});

Это позволяет мне точно зернистому контролю над именно то, что войти в систему. В коде я могу указать уровень сообщения, как так: logger.verbose (сообщение);

Аутентификация и авторизация

Любой API, особенно тот, который позволяет модифицировать или извлекать конфиденциальные данные, потребуется аутентификация и авторизация. Это лучшая статья На этой теме я обнаружил, и он приведет меня к реализации аутентификации отдельно от My GraphQL API.

Чтобы собрать его вместе, я использовал стек Паспорт , Экспресс-сессия и Connect-Session-Sequelize Отказ Это позволяет мне использовать поставщик паспорта для аутентификации пользователя, а затем сохранить токен аутентификации в сеансе базы данных и хранить данные в файле cookie. По запросу я могу разобрать файл cookie и использовать его для идентификации пользователя, делающего запрос. Вот что похоже:

Маршруты/auth.js :

const bodyParser = require('body-parser');const passport = require('passport');const expressSession = require('express-session');const Store = require('connect-session-sequelize')(expressSession.Store);const flash = require('express-flash');const LocalStrategy = require('passport-local').Strategy;const logger = require('../helpers/logger');const { SESSION_KEY} = process.env;
module.exports = function (app) { const db = require('../helpers/db').up();
passport.use('local', new LocalStrategy( async (username, password, done) => { const { validLogin, user } = await db.User.checkPassword(username, password) return validLogin ? done(null, user) : done(null, false, { message: 'Invalid username or password' }); } ));
passport.serializeUser(function(user, done) { done(null, user.id); });
passport.deserializeUser(async function(id, done) { const user = await db.User.findById(id); done(null, user); });
app.use(expressSession({ secret: SESSION_KEY, store: new Store({ db: db.sequelize }), resave: false, saveUninitialized: false }))
app.use(passport.initialize()); app.use(passport.session()); app.use(flash());
app.post( '/auth/local', bodyParser.urlencoded({ extended: true }), passport.authenticate('local'), (req, res) => res.send(req.user.id) );
app.post( '/logout', async (req, res) => { req.logout(); req.session.destroy(function (err) { err && logger.error(err); res.clearCookie('connect.sid'); res.sendStatus(200); }) } )}

Это позволяет нам делать разрешение, потому что он помещает Пользователь на каждом объекте запроса. Итак, если мы оглянемся на наш маршрут GraphQL, мы видим:

app.use('/graphql', bodyParser.json(), (req, res, next) => graphqlExpress({ schema, context: { user: req.user }})(req, res, next));

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

Тесты

Ах, тесты. Одна вещь, которую мы любим либо одержимы, либо забывать о цели. Нахождение хорошего способа проверить эту API было более чем немного сложным – в частности, секвелизовать, кажется, боротся с сбросом до хорошего состояния и закрытия соединений во время тестирования. Вы заметите на протяжении всего кода, что есть много звонков на помощники/дБ Вокруг – это позволяет нам лениво создавать DB при необходимости, а не предполагая, что соединение будет создано на уровне приложений.

Некоторые основные правила для тестирования этого приложения:

  1. Большинство тестов должны быть интеграционными тестами. Docker-Compose позволяет легко раскрутить среду песочницы для тестов и отвечать на данный порт, давайте воспользуемся этим и писать наши тесты с точки зрения клиента, взаимодействующего с нашим API, а не разработчиком API
  2. У нас есть миграции, семена и возможность начать и остановить базу данных. Мы должны использовать это, чтобы тестировать как можно возможное для каждого теста. Давайте не будем носить государство между тестами.
  3. Мы должны сможете посмотреть наши тесты, когда мы развиваемся, чтобы помочь в написании тестов рядом с кодом

Итак, с этим в виду, вот как я создал мою тестовую структуру. Я начал с создания нового сервиса Pocker-Compose для моих тестов:

api-test: build: context: ./api dockerfile: Dockerfile-dev image: crypto-dca-api:latest container_name: crypto-dca-api-test env_file: config/.env environment: - NODE_ENV=test entrypoint: npm run watch-tests volumes: - ./api:/www

Это позволяет мне установить Node-Env и запустите пользовательскую команду для просмотра тестов. Что команда часов-тестов определяется здесь, в моем Package.json :

"watch-tests": "NODE_ENV=test mocha --exit --watch ./test/{unit,integration}/index.js"

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

Вот как выглядит моя интеграция бегуна:

Тест/интеграция/index.js :

const { describe, before, after} = require('mocha');const { up } = require('../../helpers/db');const { start, stop } = require('../../helpers/server');const testDir = require('../helpers/test-dir');const runMigration = require('../helpers/migration');
let db, migrate;
describe('integration tests', () => { before(async () => { db = up(); migrate = runMigration(db); await migrate.down(); await migrate.up(); await start({ db }); });
['db', 'auth', 'graphql', 'rpc'].forEach(dir => testDir(`integration/${dir}`) )
after(async () => { await migrate.down(); await stop(); });})
module.exports = () => db;

Это гарантирует, что мы начинаем с чистого DB и Server State, а мы убираем после себя.

Вот тест в образце интеграции:

const { expect } = require('chai');const { describe, it } = require('mocha');const fetch = require('node-fetch');const { name } = require('../../helpers/sort');
describe('wallet query', () => { it('should be able to query all wallets', async () => { const query = encodeURIComponent(` { wallets { name, address, local } } `);
const resp = await fetch(`http://localhost:8088/graphql?query=${query}`) const { data: { wallets } } = await resp.json();
expect( wallets.sort(name) ).to.deep.equal([{ name: "local BTC", address: "abacadsf", local: true }, { name: "remote BTC", address: "asdfdcvzdsfasd", local: false }, { name: "remote USDT", address: "vczvsadf", local: false }]) });})

Создание этих испытаний легко, потому что мы можем просто загрузить тестовые данные с помощью сеялки, используйте /gaphiql Конечная точка для запуска нашего запроса, проверьте вывод, а затем скопируйте его на тест. Есть немного сложности, потому что я использую сгенерированные UUIDs, поэтому я должен иногда запрашивать идентификатор, прежде чем делать поиск, но по большей части это механический процесс. Я хотел бы исследовать использование Jest Потому что я знаю, что это хорошо, чтобы сделать этот тип государственной дифференцировки.

Это оно! Теперь у нас есть функциональные, протестированные, легко развернутые API GraphQL с аутентификацией и авторизацией. Не тривиально поставить все эти части вместе, но как только вы выясните ядро (Graphql, Sequelize, Graphql-Sequelize), становится довольно простым, чтобы создать что-то чрезвычайно мощное и расширяемое.

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

Ваше здоровье!