بناء Graphql Api للعقدة و MYSQL 2019 - JWT

إذا كنت هنا ربما تعرف بالفعل. أنت تعلم أن Graphql رائعة ، وتسرع عملية التطوير ، وربما يكون أفضل شيء حدث منذ أن أصدرت Tesla طراز S.

إليك نموذج جديد أستخدمه: https://medium.com/@brianschardt/best-graphql-apollo-sql-and-nestjs-template-458f9478b54e

ومع ذلك ، فإن معظم البرامج التعليمية التي قرأتها توضح كيفية إنشاء تطبيق graphql مع تقديم مشكلة طلبات n + 1 الشائعة. نتيجة لذلك ، يكون الأداء في العادة ضعيفًا.

هل هذا أفضل من تسلا؟

هدفي في هذه المقالة ليس شرح أساسيات Graphql ، ولكن لإظهار شخص ما كيفية إنشاء واجهة برمجة تطبيقات Graphql بسرعة لا تتضمن مشكلة n + 1.

إذا كنت تريد معرفة سبب استخدام 90٪ من التطبيقات الجديدة لـ graphql api بدلاً من الراحة ، انقر هنا.

ملحق الفيديو:

هذا القالب ضروري لاستخدامه في الإنتاج لأنه يحتوي على طرق سهلة لإدارة متغيرات البيئة ، وله هيكل منظم بحيث لا ينفد الكود. لإدارة مشكلة n + 1 ، نستخدم تحميل البيانات ، الشيء الذي أصدره facebook لحل هذه المشكلة.

المصادقة: JWT

ORM: تسلسل

قاعدة البيانات: الخلية ، أو بوستجرس

الحزم المهمة الأخرى المستخدمة: صريح ، خادم أبولو ، graphql-sequelize ، dataloader-sequelize

ملاحظة: يتم استخدام Typescript للتطبيق. إنه مشابه لجافا سكريبت ، إذا لم تكن تستخدم الطباع أبداً فلن أقلق. ومع ذلك ، إذا كان هناك ما يكفي من الطلب سأكتب نسخة جافا سكريبت العادية. التعليق إذا كنت ترغب في ذلك.

ابدء

استنساخ الريبو وتثبيت وحدات العقدة

هنا رابط إلى الريبو ، أوصي باستنساخه لمتابعة أفضل على طول.

git clone git@github.com: brianschardt / node_graphql_apollo_template.git
مؤتمر نزع السلاح node_graphql_apollo_template
تثبيت npm
// تثبيت الحزم العالمية لتشغيل التطبيق
npm i -g nodemon

لنبدأ مع .env

إعادة تسمية example.env إلى .env وتغييره إلى بيانات الاعتماد الصحيحة للبيئة الخاصة بك.

NODE_ENV = التنمية

PORT = 3001

DB_HOST = المضيف المحلي
DB_PORT = 3306
DB_NAME = نوع
DB_USER = الجذر
DB_PASSWORD = الجذر
DB_DIALECT = ك

JWT_ENCRYPTION = randomEncryptionKey
JWT_EXPIRATION = 1Y

قم بتشغيل الكود

الآن إذا كانت قاعدة البيانات قيد التشغيل وقمت بتحديث ملف .env بشكل صحيح بالمعلومات المناسبة ، فيجب أن نتمكن من تشغيل تطبيقنا. سيؤدي ذلك إلى إنشاء الجداول ذات المخطط المحدد تلقائيًا في قاعدة البيانات.

// استخدم للتطوير حيث أن هذه الساعات تتغير في الكود.
تشغيل npm start: watch
// استخدام للإنتاج
بدء تشغيل npm

اذهب الآن إلى متصفحك وأدخل: http: // localhost: 3001 / graphql

يجب أن تشاهد الآن ملعب graphql الذي يسمح لك بمشاهدة الوثائق المتعلقة بالطفرات والاستعلامات الموجودة بالفعل. كما يسمح لك بإجراء استعلامات مقابل واجهة برمجة التطبيقات. يوجد اثنان منها بالفعل ، ولكن لاختبار قوة API في هذا القالب بالكامل ، قد ترغب في إدخال قاعدة البيانات يدويًا مع المعلومات.

قاعدة البيانات ومخطط Graphql

كما ترون من النظر إلى المخطط في ملعب graphql ، فإن له هيكل بسيط للغاية. لا يوجد سوى جدولين ، أي المستخدم والشركة. يمكن أن ينتمي المستخدم إلى شركة واحدة ويمكن للشركة أن تضم العديد من المستخدمين ، أي واحدة لكثير من الجمعيات.

إنشاء مستخدم

مثال gql لتشغيل في الملعب لإنشاء مستخدم. سيعود هذا أيضًا بـ JWT حتى تتمكن من المصادقة على الطلبات المستقبلية.

طفره{
  createUser (البيانات: {firstName: "test" ، البريد الإلكتروني: "test@test.com" ، كلمة المرور: "1"}) {
    هوية شخصية
    الاسم الاول
    JWT
  }
}

المصادقة:

الآن لديك JWT ، يتيح اختبار المصادقة مع ملعب gql للتأكد من أن كل شيء يعمل بشكل صحيح. في الجزء السفلي الأيسر من صفحة الويب ، سيكون هناك نص يقول رؤوس HTTP. انقر عليها وأدخل هذا:

ملاحظة: استبدل برمزك المميز.

{
  "التخويل": "Bearer eyJhbGciOiJ ..."
}

الآن قم بتشغيل هذا الاستعلام في الملعب:

الاستعلام {
  getUser {
    هوية شخصية
    الاسم الاول
  }
}

إذا كان كل شيء يعمل يجب إعادة اسمك ومعرف المستخدم.

الآن إذا وضعت قاعدة بياناتك يدويًا ، مع اسم الشركة ومعرفها ، وقمت بتعيين هذا المعرّف للمستخدم الخاص بك ، وقمت بتشغيل هذا الاستعلام. يجب أن تعاد الشركة.

الاستعلام {
  getUser {
    هوية شخصية
    الاسم الاول
    شركة{
      هوية شخصية
      اسم
    }
  }
}

حسنًا ، بعد أن عرفت كيفية استخدام واجهة برمجة التطبيقات هذه واختبارها ، يمكنك الدخول إلى الكود

كود الغوص

الملف الرئيسي - app.ts

تحميل التبعيات - تحميل نماذج ديسيبل ، والمتغيرات env.

استيراد * كما هو صريح من 'صريح' ؛
استيراد * كـ jwt من 'express-jwt' ؛
استيراد {ApolloServer} من 'apollo-server-express' ؛
استيراد {sequelize} من './models' ؛
استيراد {ENV} من './config' ؛

استيراد {حلالاً كمحللات أو مخطط أو مخطط} من './graphql' ؛
استيراد {createContext، EXPECTED_OPTIONS_KEY} من 'dataloader-sequelize' ؛
استيراد إلى من "انتظار إلى js" ؛

التطبيق const = express () ؛

الإعداد الوسيطة وخادم أبولو!

ملاحظة: "createContext (sequelize)" هو ما يتخلص من مشكلة n + 1. يتم كل ذلك في الخلفية عن طريق تسلسل الآن. سحر!! هذا يستخدم حزمة الفيسبوك dataloader.

const authMiddleware = jwt ({
    سر: ENV.JWT_ENCRYPTION ،
    بيانات الاعتماد المطلوبة: false ،
})؛
app.use (authMiddleware)؛
app.use (function (err، req، res، next) {
    const errorObject = {error: true ، message: `$ {err.name}:
$ {err.message} `}؛
    if (err.name === 'UnauthorizedError') {
        return res.status (401) .json (errorObject)؛
    } آخر {
        return res.status (400) .json (errorObject)؛
    }
})؛
خادم const = جديد ApolloServer ({
    typeDefs: المخطط ،
    حل له،
    schemaDirectives،
    ملعب: صحيح ،
    السياق: ({req}) => {
        إرجاع {
            [EXPECTED_OPTIONS_KEY]: createContext (sequelize) ،
            المستخدم: req.user ،
        }
    }
})؛
server.applyMiddleware ({app}) ؛

الاستماع للطلبات

app.listen ({port: ENV.PORT}، async () => {
    console.log (`` خادم جاهز على http: // localhost: $ {ENV.PORT} $ {server.graphqlPath} `) ؛
    دعنا نخطئ
    [err] = في انتظار (sequelize.sync (
        // {force: true} ،
    ))؛

    إذا (يخطئ) {
        console.error ('خطأ: لا يمكن الاتصال بقاعدة البيانات') ؛
    } آخر {
        console.log ('متصل بقاعدة البيانات') ؛
    }
})؛

متغيرات التكوين - config / env.config.ts

نحن نستخدم dotenv لتحميل في المتغيرات .env لدينا إلى التطبيق لدينا.

استيراد * كـ dotEnv من 'dotenv' ؛
dotEnv.config ()؛

const التصدير ENV = {
    بورت: process.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 || "الخلية"،

    JWT_ENCRYPTION: process.env.JWT_ENCRYPTION || "secureKey،
    JWT_EXPIRATION: process.env.JWT_EXPIRATION || "1Y،
}؛

Graphql الوقت!

دعنا نلقي نظرة على هؤلاء المحللين!

graphql / index.ts

نحن هنا نستخدم حزمة مخطط الغراء. يساعد هذا في تقسيم المخطط والاستعلامات والطفرات الخاصة بنا إلى أجزاء منفصلة للحفاظ على تعليمات برمجية نظيفة ومنظمة. تبحث هذه الحزمة تلقائيًا في الدليل الذي نحدده لملفين ، على سبيل المثال ، schema.graphql و solutionver.ts. ثم يمسك بها ويصقها معًا. ومن هنا اسم مخطط الغراء.

التوجيهات: بالنسبة للتوجيهات الخاصة بنا ، نقوم بإنشاء دليل لهم وإدراجها عبر ملف index.ts.

استيراد * كغراء من "مخطط" ؛
تصدير {schemaDirectives} من './directives' ؛
export const {schema، determver} = glue ('src / graphql'، {mode: 'ts'})؛

نحن نصنع أدلة لكل نموذج لدينا من أجل التناسق. وبالتالي ، لدينا دليل المستخدم والشركة.

graphql / المستخدم

لاحظنا أن ملف محلل البيانات ، حتى عند استخدام الغراء المخطط ، لا يزال كبيرًا جدًا. لذلك قررنا تقسيمها بشكل أكبر بناءً على ما إذا كان استعلامًا أو طفرة أو خريطة للنوع. وبالتالي ، لدينا 3 ملفات أخرى.

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

ملاحظة: إذا كنت ترغب في إضافة اشتراكات gql ، فستقوم بإنشاء ملف آخر يسمى: user.subscription.ts وإدراجه في ملف محلل البيانات.

graphql / المستخدم / resolver.ts

هذا الملف بسيط للغاية وخوادم لتنظيم الملفات الأخرى في هذا الدليل.

استيراد {Query} من './user.query' ؛
استيراد {UserMap} من "./user.map" ؛
استيراد {Mutation} من "./user.mutation" ؛

محلل تصدير التصدير = {
  الاستعلام: الاستعلام ،
  مستخدم: خريطة المستخدم ،
  طفرة: طفرة
}؛

graphql / المستخدم / schema.graphql

يحدد هذا الملف مخطط graphql الخاص بنا ، والمحللات! مهم جدا!

اكتب المستخدم
  المعرف: كثافة العمليات
  البريد الإلكتروني: سلسلة
  الاسم الأول: سلسلة
  اسم العائلة: سلسلة
  شركة شركة
  jwt: StringisAuthUser
}

مدخلات المستخدم
    البريد الإلكتروني: سلسلة
    كلمة المرور: سلسلة
    الاسم الأول: سلسلة
    اسم العائلة: سلسلة
}

اكتب استعلام {
   getUser: مستخدمisAuth
   loginUser (البريد الإلكتروني: سلسلة! ، كلمة المرور: سلسلة!): مستخدم
}

اكتب طفرة
   createUser (البيانات: UserInput): المستخدم
}

graphql / المستخدم / user.query.ts

يحتوي هذا الملف على وظائف جميع استفسارات المستخدم والطفرات. يستخدم السحر من graphql-sequelize للتعامل مع الكثير من الأشياء graphql. إذا كنت قد استخدمت حزم graphql الأخرى أو حاولت إنشاء واجهة graphql api الخاصة بك ، فسوف تتعرف على مدى أهمية هذه الحزمة وتوفير الوقت لها. ومع ذلك لا يزال يوفر لك كل التخصيص الذي ستحتاجه من أي وقت مضى! فيما يلي رابط للوثائق في تلك الحزمة.

استيراد {حلالاً من 'graphql-sequelize' ؛
استيراد {User} من '../../models' ؛
استيراد إلى من "انتظار إلى js" ؛

استعلام const التصدير = {
    getUser: حلالا (مستخدم ، {
        قبل: async (findOptions ، {} ، {user}) => {
            إرجاع findOptions.where = {id: user.id}؛
        }،
        بعد: (المستخدم) => {
            عودة المستخدم
        }
    })،
    loginUser: حلالا (مستخدم ، {
        قبل: async (findOptions، {email}) => {
            findOptions.where = {email}؛
        }،
        بعد: غير متزامن (مستخدم ، {password}) => {
            دعنا نخطئ
            [err، user] = في انتظار (user.comparePassword (password))؛
            إذا (يخطئ) {
              console.log (يخطئ)؛
              رمي خطأ جديد (يخطئ) ؛
            }

            user.login = true ؛ // لإعلام التوجيه بأنه تتم مصادقة هذا المستخدم بدون رأس ترخيص
            عودة المستخدم
        }
    })،
}؛

graphql / المستخدم / user.mutation.ts

يحتوي هذا الملف على كل طفرة قسم المستخدم في تطبيقنا.

قم باستيراد {حلال الحزم كـ rs} من 'graphql-sequelize' ؛
استيراد {User} من '../../models' ؛
استيراد إلى من "انتظار إلى js" ؛

طفرة كون التصدير = {
    createUser: rs (مستخدم ، {
      قبل: async (findOptions، {data}) => {
        واسمحوا يخطئ ، المستخدم.
        [err، user] = في انتظار (User.create (data))؛
        إذا (يخطئ) {
          رمي يخطئ
        }
        findOptions.where = {id: user.id}؛
        إرجاع findOptions ؛
      }،
      بعد: (المستخدم) => {
        user.login = صحيح ؛
        عودة المستخدم
      }
    })،
}؛

graphql / المستخدم / user.map.ts

هذا هو الذي يغفله الناس دائمًا ، ويجعل عملية الترميز والاستعلام في graphql صعبة للغاية ولديهم أداء ضعيف. ومع ذلك ، فإن جميع الحزم التي قمنا بإدراجها تحل المشكلة. تعيين أنواع لبعضها البعض هو ما يعطي graphql قوته وقوته ، ولكن الناس يرمزون له بطريقة تجعل هذه القوة تتحول إلى نقطة ضعف. ومع ذلك ، فإن جميع الحزم التي استخدمناها تتخلص من ذلك بطريقة سهلة.

استيراد {حلالاً من 'graphql-sequelize' ؛
استيراد {User} من '../../models' ؛
استيراد إلى من "انتظار إلى js" ؛

تصدير const UserMap = {
    الشركة: محلل (User.associations.company) ،
    jwt: (user) => user.getJwt () ،
}؛

نعم هذا بسيط جدا !!!

ملاحظة: توجيهات graphql في مخطط المستخدم هي التي تحمي بعض الحقول مثل حقل JWT على المستخدم واستعلام getUser.

النماذج - النماذج / index.ts

نحن نستخدم typelize typescript حتى نتمكن من ضبط المتغيرات على هذا النوع من الفصل. نبدأ في هذا الملف عن طريق تحميل الحزم. ثم نحن إنشاء تسلسل وتوصيله إلى ديسيبل لدينا. ثم نحن تصدير النماذج.

استيراد {Sequelize} من 'sequelize-typescript' ؛
استيراد {ENV} من '../config/env.config' ؛

تصدير const sequelize = جديد Sequelize ({
        قاعدة البيانات: ENV.DB_NAME ،
        لهجة: ENV.DB_DIALECT ،
        اسم المستخدم: ENV.DB_USER ،
        كلمة المرور: ENV.DB_PASSWORD ،
        عوامل التشغيل:
        تسجيل الدخول: خطأ ،
        ذاكرة التخزين:'،
        modelPaths: [__dirname + '/*.model.ts'] ،
        modelMatch: (اسم الملف ، عضو) => {
           return filename.substring (0، filename.indexOf ('. model')) === member.toLowerCase ()؛
        }،
})؛
تصدير {User} من './user.model' ؛
تصدير {Company} من './company.model' ؛

يعد modelPaths و modelMatch من الخيارات الإضافية التي تخبر sequelize-typescript أين توجد موديلاتنا وما هي اصطلاحات التسمية الخاصة بها.

نموذج الشركة - النماذج / company.model.ts

نحن هنا نحدد مخطط الشركة باستخدام typelize typescript.

import {Table، Column، Model، HasMany، PrimaryKey، AutoIncrement} من 'sequelize-typescript'؛
استيراد {User} من './user.model'
Table ({timestamps: true})
شركة تصدير الفئة تمدد النموذج <الشركة>

  @ Column ({primaryKey: true ، autoIncrement: true})
  المعرف: رقم

  @عمود
  اسم: سلسلة؛

  HasMany (() => مستخدم)
  المستخدمين: المستخدم [] ؛
}

نموذج المستخدم - نماذج / user.model.ts

هنا نحدد نموذج المستخدم. سنضيف أيضًا بعض الوظائف المخصصة للمصادقة.

import {Table، Column، Model، HasMany، PrimaryKey، AutoIncrement، BelongsTo، ForeignKey، BeforeSave} من 'sequelize-typescript'؛
استيراد {Company} من "./company.model"؛
استيراد * كـ bcrypt من "bcrypt" ؛
استيراد إلى من "انتظار إلى js" ؛
import * as jsonwebtoken from'jsonwebtoken '؛
استيراد {ENV} من '../config' ؛

Table ({timestamps: true})
فئة التصدير المستخدم يمتد النموذج <المستخدم>
  @ Column ({primaryKey: true ، autoIncrement: true})
  المعرف: رقم

  @عمود
  الاسم الأول: سلسلة ؛

  @عمود
  اسم العائلة: سلسلة ؛

  @عمود
  البريد الإلكتروني: سلسلة.

  @عمود
  كلمة المرور: سلسلة ؛

  ForeignKey (() => الشركة)
  @عمود
  companyId: رقم

  BelongsTo (() => الشركة)
  شركة شركة؛
  jwt: سلسلة ؛
  تسجيل الدخول: منطقية.
  BeforeSave
  static async hashPassword (مستخدم: مستخدم) {
    دعنا نخطئ
    if (user.changed ('password')) {
        دع الملح ، التجزئة.
        [err، salt] = في انتظار (bcrypt.genSalt (10)) ؛
        إذا (يخطئ) {
          رمي يخطئ
        }

        [err، hash] = في انتظار (bcrypt.hash (user.password، salt))؛
        إذا (يخطئ) {
          رمي يخطئ
        }
        user.password = hash؛
    }
  }

  async comparPassword (pw) {
      دعنا نخطئ
      إذا (! this.password) {
        رمي خطأ جديد ('ليس لديه كلمة مرور') ؛
      }

      [err، pass] = في انتظار (bcrypt.compare (pw، this.password))؛
      إذا (يخطئ) {
        رمي يخطئ
      }

      إذا (! pass) {
        رمي "كلمة مرور غير صالحة" ؛
      }

      ارجع هذا
  }؛

  getJwt () {
      إرجاع "Bearer" + jsonwebtoken.sign ({
          المعرف: هذا.
      }، ENV.JWT_ENCRYPTION، {expiresIn: ENV.JWT_EXPIRATION})؛
  }
}

هذا كثير من التعليمات البرمجية هناك ، لذا يرجى التعليق إذا كنت تريد مني أن أقسمها.

إذا كان لديك أي اقتراحات للتحسينات ، فيرجى إخبارنا! إذا كنت تريد مني إعداد قالب في جافا سكريبت العادي ، فأعلمني أيضًا! أيضًا ، إذا كان لديك أي أسئلة فسأحاول الرد في نفس اليوم ، لذا يرجى عدم الخوف من طرحها!

شكر،

براين شاردت