Створіть Graphql Api для Node & MYSQL 2019— JWT

Якщо ви тут, напевно, ви вже знаєте. Ви знаєте, що Graphql НАВЧАЛЬНО дивовижний, прискорює розвиток, і це, мабуть, найкраще, що сталося з моменту випуску Tesla моделі S.

Ось новий шаблон, який я використовую: https://medium.com/@brianschardt/best-graphql-apollo-sql-and-nestjs-template-458f9478b54e

Однак більшість навчальних посібників я прочитав, як створити додаток graphql, але ввести загальну проблему запитів n + 1. Як результат, продуктивність, як правило, дуже низька.

Дійсно це краще, ніж Tesla?

Моя мета в цій статті - не пояснити основи Graphql, а показати комусь, як швидко побудувати API Graphql, у якого немає питання n + 1.

Якщо ви хочете знати, чому 90% нових додатків повинні використовувати graphql api замість спокійного натисніть тут.

Доповнення до відео:

Цей шаблон НЕОБХІДНО використовуватись для виробництва, оскільки він містить прості способи управління змінними середовища і має організовану структуру, тому код не вийде з-під руки. Для управління проблемою n + 1 ми використовуємо завантаження даних, що вирішило проблему facebook для вирішення цієї проблеми.

Аутентифікація: JWT

ОРМ: Послідовно

База даних: Mysql або Postgres

Інші важливі використовувані пакети: express, apollo-сервер, graphql-sequelize, dataloader-sequelize

Примітка. Тип програми використовується для додатка. Він настільки схожий на JavaScript, якщо ви ніколи не використовували машинопис, я б не хвилювався. Однак, якщо є достатній попит, я напишу звичайну версію JavaScript. Прокоментуйте, якщо ви хочете цього.

Починаємо

Клоніруйте репо і встановіть модулі вузлів

Ось посилання на репо, я рекомендую клонувати його, щоб найкраще слідкувати далі.

git clone git@github.com: brianschardt / node_graphql_apollo_template.git
cd node_graphql_apollo_template
npm встановити
// встановити глобальні пакети для запуску програми
npm я -g nodemon

Почнемо з .env

Перейменуйте example.env у .env та змініть його на правильні облікові дані для вашого оточення.

NODE_ENV = розробка

ПОРТ = 3001

DB_HOST = localhost
DB_PORT = 3306
DB_NAME = тип
DB_USER = корінь
DB_PASSWORD = корінь
DB_DIALECT = mysql

JWT_ENCRYPTION = randomEncryptionKey
JWT_EXPIRATION = 1y

Запустіть код

Тепер, якщо у вас працює база даних, і ви правильно оновили .env-файл з належною інформацією, ми повинні мати можливість запустити наш додаток. Це створить таблиці з визначеною схемою автоматично в базі даних.

// використовувати для розробки, оскільки цей час спостерігається за зміною коду.
npm run start: дивитися
// використання для виробництва
npm запуск запуску

Тепер перейдіть до свого браузера та введіть: http: // localhost: 3001 / graphql

Тепер ви повинні побачити майданчик graphql, який дозволяє переглядати документацію про мутації та запити, які вже існують. Це також дозволяє робити запити проти API. Є декілька з них вже зроблені, але щоб повністю перевірити потужність цього шаблону API, можливо, ви захочете вручну набрати базу даних з інформацією.

База даних та схема Graphql

Як ви бачите з перегляду схеми на майданчику graphql, вона має досить просту структуру. Є лише 2 таблиці, тобто користувач та компанія. Користувач може належати до однієї компанії, а компанія може мати багато користувачів, тобто асоціацію, яка належить багатьом.

Створіть користувача

Приклад gql для запуску на майданчику для створення користувача. Це також поверне JWT, щоб ви могли пройти автентифікацію для майбутніх запитів.

мутація {
  createUser (дані: {firstName: "test", email: "test@test.com", пароль: "1"}) {
    ід
    ім'я
    jwt
  }
}

Автентифікація:

Тепер, коли у вас є JWT, давайте перевірити автентифікацію з ігровим майданчиком gql, щоб переконатися, що все працює правильно. У лівій нижній частині веб-сторінки розміститься текст із написанням HTTP HEADERS. Клацніть по ньому і введіть це:

Примітка: замініть маркер.

{
  "Авторизація": "Носій eyJhbGciOiJ ..."
}

Тепер запустіть цей запит на дитячому майданчику:

запит {
  getUser {
    ід
    ім'я
  }
}

Якщо все спрацювало, ваше ім’я та ідентифікатор користувача потрібно повернути.

Тепер, якщо ви вручну нанесіть свою базу даних, з назвою компанії та ідентифікатором, і призначте цей ідентифікатор своєму користувачеві та запустіть цей запит. Компанію слід повернути.

запит {
  getUser {
    ід
    ім'я
    компанія {
      ід
      назва
    }
  }
}

Гаразд, тепер, коли ви знаєте, як користуватися та тестувати цей API, можна потрапити в код!

Код занурення

Основний файл - app.ts

Залежно від завантаження - завантажує db-моделі та змінні env.

імпортувати * як експрес з 'експрес';
імпортувати * як jwt з 'express-jwt';
імпорт {ApolloServer} з 'apollo-server-express';
імпорт {sequelize} з './models';
імпорт {ENV} з './config';

імпортувати {Resolver як резолютори, схему, schemaDirectives} з './graphql';
імпортувати {createContext, EXPECTED_OPTIONS_KEY} з 'dataloader-sequelize';
імпорт з "очікує на-js";

const app = express ();

Налаштуйте програмне забезпечення та сервер Apollo!

Примітка: "createContext (sequelize)" - це те, що позбавляється від проблеми n + 1. Це все робиться на задньому плані, секвелізуючи зараз. МАГІЧНИЙ !! Для цього використовується пакет завантажувачів даних у Facebook.

const authMiddleware = jwt ({
    секрет: ENV.JWT_ENCRYPTION,
    Потрібні дані: false,
});
app.use (authMiddleware);
app.use (функція (помилка, req, res, next) {
    const errorObject = {помилка: вірно, повідомлення: `$ {err.name}:
$ {err.message} `};
    if (err.name === 'UnauthorizedError') {
        повернути res.status (401) .json (errorObject);
    } else {
        повернути res.status (400) .json (errorObject);
    }
});
const сервер = новий ApolloServer ({
    typeDefs: схема,
    рішення,
    схемиДирективи,
    майданчик: справжній,
    контекст: ({req}) => {
        повернути {
            [EXPECTED_OPTIONS_KEY]: createContext (секвелізувати),
            користувач: req.user,
        }
    }
});
server.applyMiddleware ({додаток});

Прослуховуйте запити

app.listen ({порт: ENV.PORT}, async () => {
    console.log (` Сервер готовий за адресою http: // localhost: $ {ENV.PORT} $ {server.graphqlPath}`);
    нехай помиляється;
    [err] = очікуємо на (sequelize.sync (
        // {сила: правда},
    ));

    якщо (помилка) {
        console.error ("Помилка. Неможливо підключитися до бази даних");
    } else {
        console.log ("Підключено до бази даних");
    }
});

Змінні конфігурації - config / env.config.ts

Ми використовуємо dotenv для завантаження змінних .env у наш додаток.

імпортувати * як dotEnv з 'dotenv';
dotEnv.config ();

експорт const ENV = {
    ПОРТ: процес.env.PORT || '3000',

    DB_HOST: process.env.DB_HOST || '127.0.0.1',
    DB_PORT: process.env.DB_PORT || '3306',
    DB_NAME: process.env.DB_NAME || 'dbName',
    DB_USER: process.env.DB_USER || "корінь",
    DB_PASSWORD: process.env.DB_PASSWORD || "корінь",
    DB_DIALECT: process.env.DB_DIALECT || 'mysql',

    JWT_ENCRYPTION: process.env.JWT_ENCRYPTION || 'secureKey',
    JWT_EXPIRATION: process.env.JWT_EXPIRATION || '1y',
};

Час Graphql !!!

Давайте подивимось на ці рішення?

graphql / index.ts

Тут ми використовуємо пакетну схему клею. Це допомагає розбити нашу схему, запити та мутації на окремі частини для підтримки чистого та організованого коду. Цей пакет автоматично здійснює пошук у вказаному нами каталозі для 2-х файлів, тобто schema.graphql та resolver.ts. Потім він захоплює їх і склеює. Звідси і назва схеми клею.

Директиви: для наших директив ми створюємо каталог для них і включаємо їх через файл index.ts.

імпортувати * як клей із 'schemaglue';
експортувати {schemaDirectives} з './directives';
export const {schema, resolutionver} = glue ('src / graphql', {mode: 'ts'});

Ми робимо каталоги для кожної моделі, яку ми маємо для послідовності. Таким чином, ми маємо довідник користувача та компанії.

graphql / користувач

Ми помітили, що файл резолюції навіть при використанні схеми клею все ще може отримати дуже великий розмір. Тож ми вирішили розбити його далі на основі того, чи це запит, мутація чи карта для типу. Таким чином, у нас є ще 3 файли.

  • user.query.ts
  • user.mutation.ts
  • user.map.ts

Примітка: Якщо ви хочете додати підписки на gql, ви створили б інший файл під назвою: user.subscription.ts і включите його у файл резолюції.

graphql / user / resolutionver.ts

Цей файл досить простий і сервери впорядковують інші файли в цьому каталозі.

import {Запит} з './user.query';
імпортувати {UserMap} з "./user.map";
імпорт {Mutation} з "./user.mutation";

export const resolutionver = {
  Запит: Запит,
  Користувач: UserMap,
  Мутація: Мутація
};

graphql / user / schema.graphql

Цей файл визначає нашу схему graphql та роздільну здатність! Супер важливо!

введіть користувача {
  id: Int
  електронна пошта: Рядок
  firstName: Рядок
  lastName: Рядок
  компанія: Компанія
  jwt: String @isAuthUser
}

вхід UserInput {
    електронна пошта: Рядок
    пароль: Рядок
    firstName: Рядок
    lastName: Рядок
}

введіть запит {
   getUser: Користувач @isAuth
   loginUser (електронна пошта: String !, пароль: String!): Користувач
}

Тип мутації {
   createUser (дані: UserInput): Користувач
}

graphql / user / user.query.ts

Цей файл містить функціональність для всіх наших запитів та мутацій користувачів. Використовує магію від graphql-sequelize для обробки багатьох матеріалів graphql. Якщо ви використовували інші пакунки graphql або намагалися створити свій власний графічний api-графік, ви визнаєте, наскільки важливий і економій цей пакет час. Але він все ще надає вам всі налаштування, які вам коли-небудь знадобляться! Ось посилання на документацію на цей пакет.

import {resolutionver} з 'graphql-sequelize';
імпортувати {User} з '../../models';
імпорт з "очікує на-js";

експорт const Query = {
    getUser: resolutionver (Користувач, {
        до: async (findOptions, {}, {user}) => {
            return findOptions.where = {id: user.id};
        },
        після: (користувач) => {
            повернути користувача;
        }
    }),
    loginUser: роздільник (користувач, {
        перед: async (findOptions, {email}) => {
            findOptions.where = {email};
        },
        після: async (користувач, {пароль}) => {
            нехай помиляється;
            [помилка, користувач] = чекаємо на (user.comparePassword (пароль));
            якщо (помилка) {
              console.log (помилка);
              кинути нову помилку (помилка);
            }

            user.login = true; // повідомляти директиві про те, що цей користувач має автентифікацію без заголовка авторизації
            повернути користувача;
        }
    }),
};

graphql / user / user.mutation.ts

Цей файл містить усі мутації для розділу користувача нашої програми.

import {resolutionver as rs} з 'graphql-sequelize';
імпортувати {User} з '../../models';
імпорт з "очікує на-js";

експортування const Mutation = {
    createUser: rs (Користувач, {
      до: async (findOptions, {data}) => {
        нехай помиляється, користувач;
        [помилка, користувач] = очікуємо на (User.create (дані));
        якщо (помилка) {
          помилка кинути;
        }
        findOptions.where = {id: user.id};
        повернення findOptions;
      },
      після: (користувач) => {
        user.login = true;
        повернути користувача;
      }
    }),
};

graphql / user / user.map.ts

Це той, який люди завжди оминають, і робить кодування та запити у graphql настільки складними та мають низьку продуктивність. Однак усі пакунки, які ми включили, вирішують проблему. З'єднання типів один з одним - це те, що надає graphql його силу та силу, але люди кодують це таким чином, що ця сила перетворюється на слабкість. Однак усі пакунки, які ми використовували, позбавляються цього просто.

import {resolutionver} з 'graphql-sequelize';
імпортувати {User} з '../../models';
імпорт з "очікує на-js";

export const UserMap = {
    компанія: резольвер (User.associations.company),
    jwt: (user) => user.getJwt (),
};

Так, це так просто !!!

Примітка: директиви graphql в схемі користувача - це те, що захищає певні поля, такі як поле JWT для користувача та запит getUser.

Моделі - моделі / index.ts

Ми використовуємо sequelize typecript, щоб ми могли встановлювати змінні для цього типу класу. У цьому файлі ми починаємо з завантаження пакетів. Тоді ми інстанціюємо секвелізувати і підключаємо його до нашого db. Потім ми експортуємо моделі.

import {Sequelize} з 'sequelize-typecript';
імпорт {ENV} з '../config/env.config';

export const sequelize = new Sequelize ({
        база даних: ENV.DB_NAME,
        діалект: ENV.DB_DIALECT,
        ім'я користувача: ENV.DB_USER,
        пароль: ENV.DB_PASSWORD,
        ОператориПовноваження: false,
        реєстрація: помилково,
        зберігання: ': пам'ять:',
        modelPaths: [__dirname + '/*.model.ts'],
        modelMatch: (ім'я файла, член) => {
           повернути filename.substring (0, filename.indexOf ('. model')) === member.toLowerCase ();
        },
});
експортувати {User} з './user.model';
експортувати {Company} з './company.model';

ModelPaths та modelMatch - це додаткові параметри, які вказують sequelize-typecript, де знаходяться наші моделі та які їх умови іменування.

Модель компанії - моделі / company.model.ts

Тут ми визначаємо схему компанії за допомогою sequelize typecript.

імпорт {Table, Column, Model, HasMany, PrimaryKey, AutoIncrement} з 'sequelize-typecript';
import {User} з './user.model'
@Table ({часові позначки: правда})
експортний клас Компанія розширює Модель <Компанія> {

  @Column ({PrimaryKey: true, autoIncrement: true})
  номер документа;

  @Column
  назва: рядок;

  @HasMany (() => Користувач)
  користувачі: Користувач [];
}

Модель користувача - моделі / user.model.ts

Тут ми визначаємо модель користувача. Також ми додамо деякі спеціальні функції для аутентифікації.

імпорт {Table, Column, Model, HasMany, PrimaryKey, AutoIncrement, BelongsTo, ForeignKey, BeforeSave} з 'sequelize-typecript';
import {Company} з "./company.model";
імпортувати * як bcrypt з 'bcrypt';
імпорт з "очікує на-js";
імпортувати * як jsonwebtoken з'jsonwebtoken ';
import {ENV} з '../config';

@Table ({часові позначки: правда})
експортний клас Користувач розширює Модель  {
  @Column ({PrimaryKey: true, autoIncrement: true})
  номер документа;

  @Column
  firstName: рядок;

  @Column
  lastName: рядок;

  @Column
  електронна пошта: рядок;

  @Column
  пароль: рядок;

  @ForeignKey (() => Компанія)
  @Column
  companyId: номер;

  @BelongsTo (() => компанія)
  компанія: Компанія;
  jwt: рядок;
  вхід: булевий;
  @BeforeSave
  static async hashPassword (користувач: Користувач) {
    нехай помиляється;
    якщо (user.changed ('пароль')) {
        нехай сіль, хеш;
        [помилка, сіль] = чекати (bcrypt.genSalt (10));
        якщо (помилка) {
          помилка кинути;
        }

        [помилка, хеш] = чекати (bcrypt.hash (user.password, сіль));
        якщо (помилка) {
          помилка кинути;
        }
        user.password = хеш;
    }
  }

  async ComparePassword (pw) {
      нехай помиляється, проходить;
      if (! this.password) {
        кинути нову помилку ("Немає пароля");
      }

      [помилка, пропуск] = очікуємо на (bcrypt.compare (pw, this.password));
      якщо (помилка) {
        помилка кинути;
      }

      якщо (! пройти) {
        кинути "Недійсний пароль";
      }

      повернути це;
  };

  getJwt () {
      повернути "Носій" + jsonwebtoken.sign ({
          id: this.id,
      }, ENV.JWT_ENCRYPTION, {expiresIn: ENV.JWT_EXPIRATION});
  }
}

Там дуже багато коду, тому коментуйте, якщо ви хочете, щоб я його зламав.

Якщо у вас є якісь пропозиції щодо покращень, будь ласка, повідомте мене! Якщо ви хочете, щоб я робив шаблон у звичайному javascript, також дайте мені знати! Крім того, якщо у вас виникнуть запитання, я спробую відповісти в той же день, тому, не бійтеся запитати!

Дякую,

Брайан Шардт