Düğüm ve MYSQL 2019 için bir Graphql Api oluşturun - JWT

Eğer buradaysanız, muhtemelen zaten biliyorsunuzdur. Graphql'ın FREAKING'in harika olduğunu, gelişimi hızlandırdığını ve Tesla'nın S modelini piyasaya sürmesinden bu yana gerçekleşen muhtemelen en iyi şey olduğunu biliyorsunuz.

İşte kullandığım yeni bir şablon: https://medium.com/@brianschardt/best-graphql-apollo-sql-and-nestjs-template-458f9478b54e

Ancak, okuduğum çoğu öğretici bir graphql uygulaması nasıl oluşturulacağını, ancak ortak n + 1 istek problemini ortaya çıkardığını gösteriyor. Sonuç olarak, performans genellikle süper zayıftır.

Gerçekten bu bir Tesla'dan daha mı iyi?

Bu makaledeki amacım Graphql'in temellerini açıklamak değil, birisine n + 1 sorunu olmayan bir Graphql API'sini nasıl hızlı bir şekilde oluşturacağını göstermek.

Yeni uygulamaların% 90'ının neden dinlendirici yerine graphql api'leri kullanması gerektiğini bilmek istiyorsanız burayı tıklayın.

Video Eki:

Bu şablon, ortam değişkenlerini yönetmek için kolay yollar içerdiğinden üretim için kullanılmaya uygundur ve kodun elden çıkmaması için organize bir yapıya sahiptir. N + 1 sayısını yönetmek için, facebook'un bu sorunu çözmek için yayınladığı şey olan veri yüklemesini kullanıyoruz.

Kimlik doğrulama: JWT

ORM: Sequelize

Veritabanı: MySQL veya Postgres

Kullanılan diğer önemli paketler: express, apollo-server, graphql-sequelize, dataloader-sequelize

Not: Typescript uygulaması için kullanılır. Javascript'e çok benzer, eğer daha önce typcript kullanmadıysanız endişelenmenize gerek yok. Ancak, yeterli talep varsa, düzenli bir javascript sürümü yazacağım. İsterseniz yorum yapın.

Başlamak

Depoyu klonlayın ve düğüm modüllerini kurun

İşte depoya bir link, en iyisi takip etmek için klonlamanızı öneririm.

git clone git@github.com: brianschardt / node_graphql_apollo_template.git
cd node_graphql_apollo_template
npm kurulum
// uygulamayı çalıştırmak için global paketler kurun
npm i -g nodemon

.Env ile başlayalım

Example.env dosyasını .env olarak yeniden adlandırın ve ortamınız için doğru kimlik bilgileriyle değiştirin.

NODE_ENV = gelişme

= 3001 PORT

DB_HOST = localhost
DB_PORT = 3306
DB_NAME = türü
DB_USER = kök
DB_PASSWORD = kök
DB_DIALECT = mysql

JWT_ENCRYPTION = randomEncryptionKey
JWT_EXPIRATION = 1y

Kodu çalıştırın

Şimdi eğer veritabanı çalışıyorsa ve .env dosyanızı doğru bilgilerle doğru bir şekilde güncellediyseniz, uygulamamızı çalıştırabilmemiz gerekir. Bu, veritabanında otomatik olarak tanımlanan şema ile tablolar yaratacaktır.

// bu, koddaki değişiklikleri izlediğinden, geliştirme için kullanın.
npm run start: izle
// üretim için kullan
npm çalışma başlangıcı

Şimdi tarayıcınıza gidin ve şunu girin: http: // localhost: 3001 / graphql

Şimdi, hangi mutasyonların ve sorguların zaten mevcut olduğuna dair belgeleri görüntülemenizi sağlayan graphql oyun alanını görmelisiniz. Ayrıca, API'ye karşı sorgulamalar yapmanızı sağlar. Hali hazırda bunlardan birkaçı var, ancak bu şablon API'sinin gücünü tamamen test etmek için veritabanını bilgiyle manuel olarak tohumlamak isteyebilirsiniz.

Veritabanı ve Graphql Şeması

Şema’nın graphql oyun alanında göründüğü gibi oldukça basit bir yapıya sahiptir. Yalnızca 2 tablo var, yani Kullanıcı ve Şirket. Bir Kullanıcı bir Şirkete ait olabilir ve bir Şirketin birçok kullanıcısı olabilir, yani bir ila birçok kuruluş olabilir.

Bir kullanıcı oluştur

Bir kullanıcı oluşturmak için oyun alanında çalıştırmak için örnek gql. Bu ayrıca bir JWT döndürür, böylece gelecekteki istekleriniz için kimlik doğrulaması yapabilirsiniz.

mutasyon {
  createUser (veri: {firstName: "test", e-posta: "test@test.com", şifre: "1"}) {
    İD
    İsim
    jwt
  }
}

kimlik doğrulaması:

Artık JWT'ye sahip olduğunuzda, her şeyin doğru çalıştığından emin olmak için gql oyun alanı ile kimlik doğrulamasını test edin. Web sayfasının sol alt tarafında HTTP BAŞLIKLARI yazan bir metin olacaktır. Üzerine tıklayın ve şunu girin:

Not: kartınızla değiştirin.

{
  "Yetkilendirme": "Bearer eyJhbGciOiJ ..."
}

Şimdi bu sorguyu oyun alanında çalıştırın:

sorgu{
  GetUser {
    İD
    İsim
  }
}

Her şey işe yaradıysa, adınız ve kullanıcı kimliğiniz iade edilmelidir.

Şimdi eğer veritabanınızı manuel olarak bir şirket adı ve kimliği ile elinizle eklerseniz ve bu kimliği kullanıcıya atayın ve bu sorguyu çalıştırın. Şirket iade edilmelidir.

sorgu{
  GetUser {
    İD
    İsim
    şirket{
      İD
      isim
    }
  }
}

Tamam, şimdi bu API'nin nasıl kullanılacağını ve test edileceğini biliyorsunuz, koda girmenizi sağlar!

Kod Dalışı

Ana dosya - app.ts

Yükleme Bağımlılıkları - db modellerini ve env değişkenlerini yükler.

* 'express' ifadesinden ifade edildiği gibi import *;
* express-jwt'den jwt olarak içe aktarın;
{apollo-server-express 'ifadesinden {ApolloServer} alın;
import {sequelize} './models';
{ENV} './config' dosyasından içe aktarın;

import {resolver olarak resolver, schema, schemaDirectives} './graphql';
import {createContext, EXPECTED_OPTIONS_KEY} 'dataloader-sequelize';
'a-a-js'den' ithal;

const uygulaması = express ();

Middleware ve Apollo Server Kur!

Not: “createContext (sequelize)”, n + 1 sorunundan kurtulan şeydir. Bunların hepsi şimdi sırayla arka planda yapılır. MAGIC !! Bu facebook dataloader paketini kullanır.

const authMiddleware = jwt ({
    sır: ENV.JWT_ENCRYPTION,
    credentialsRequired: false,
});
app.use (authMiddleware);
app.use (işlev (err, req, res, next) {
    const errorObject = {error: true, mesaj: `$ {err.name}:
$ {Err.message} '};
    if (err.name === 'YetkilendirilmemişError') {
        res.status döndür (401) .json (errorObject);
    } Başka {
        res.status döndürün (400) .json (errorObject);
    }
});
const server = yeni ApolloServer ({
    typeDefs: şema,
    rezolverler,
    schemaDirectives,
    oyun alanı: gerçek,
    bağlamı: ({req}) => {
        dönüş {
            [EXPECTED_OPTIONS_KEY]: createContext (sırayla),
            kullanıcı: req.user,
        }
    }
});
server.applyMiddleware ({app});

İstekleri dinle

app.listen ({port: ENV.PORT}, async () => {
    console.log (` Sunucu http: // localhost: $ {ENV.PORT} $ {server.graphqlPath}` adresinde hazır);
    err olsun;
    [err] = bekliyor (sequelize.sync (
        // {force: true},
    ));

    {(ERR) halinde
        console.error ('Hata: Veritabanına bağlanılamıyor');
    } Başka {
        console.log ('Veritabanına bağlı');
    }
});

Konfigürasyon değişkenleri - config / env.config.ts

.Env değişkenlerimize uygulamamıza yüklemek için dotenv kullanıyoruz.

* 'dotenv' den dotEnv olarak içe aktarma;
dotEnv.config ();

ihracat const ENV = {
    PORT: 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 || 'kök, köken',
    DB_PASSWORD: process.env.DB_PASSWORD || 'kök, köken',
    DB_DIALECT: process.env.DB_DIALECT || 'Mysql',

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

Graphql zamanı !!!

Bu çözümleyicilere bir göz atalım!

graphql / index.ts

Burada paket şema yapıştırıcısını kullanıyoruz. Bu, temiz ve düzenli bir kod sağlamak için şemamızı, sorguları ve mutasyonları ayrı parçalara ayırmaya yardımcı olur. Bu paket 2 dosya için belirlediğimiz dizini otomatik olarak arar, yani schema.graphql ve resolver.ts. Sonra onları kapar ve birbirine yapıştırır. Bu nedenle adı şema tutkalı.

Yönergeler: Yönergelerimiz için onlar için bir dizin oluşturur ve onları bir index.ts dosyası aracılığıyla ekleriz.

'schemaglue' dan yapıştırıcı olarak ithal *;
export {schemaDirectives} './directives' adresinden;
const dışa aktar {şema, çözümleyici} = yapıştırıcı ('src / graphql', {mode: 'ts'});

Tutarlılık için sahip olduğumuz her model için dizinler hazırlıyoruz. Böylece bir kullanıcı ve firma rehberimiz var.

graphql / kullanıcı

Çözümleyici dosyasının şema tutkalı kullanırken bile çok büyük olabileceğini fark ettik. Bu nedenle, bir tür için bir sorgu, mutasyon veya harita olup olmadığına göre onu daha da parçalamaya karar verdik. Böylece 3 tane daha dosyamız var.

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

Not: gql abonelikleri eklemek istiyorsanız, isimli bir başka dosya yaratmalısınız: user.subscription.ts ve bunu çözücü dosyasına dahil edin.

graphql / kullanıcı / resolver.ts

Bu dosya oldukça basit ve sunucular bu dizindeki diğer dosyaları düzenlemek için.

{. Query} './user.query' içinden import;
{UserMap} öğesini "./user.map" adresinden içe aktarın;
"./user.mutation" adresinden {Mutation} alma;

ihracat const resolver = {
  Sorgu: Sorgu
  Kullanıcı: UserMap,
  Mutasyon: Mutasyon
};

graphql / kullanıcı / schema.graphql

Bu dosya graphql şemamızı ve çözümleyicileri tanımlar! Süper önemli!

Kullanıcı türü {
  id: Int
  e-posta: Dize
  firstName: Dize
  lastName: Dize
  şirket: şirket
  jwt: String @isAuthUser
}

UserInput girişi {
    e-posta: Dize
    şifre: Dize
    firstName: Dize
    lastName: Dize
}

Sorgu yazın
   getUser: User @isAuth
   loginUser (e-posta: String !, password: String!): Kullanıcı
}

Mutasyon türü {
   createUser (data: UserInput): Kullanıcı
}

graphql / kullanıcı / user.query.ts

Bu dosya tüm kullanıcı sorgularımız ve mutasyonlarımızın işlevselliğini içerir. Bir çok graphql maddesini işlemek için graphql-sequelize'dan gelen sihri kullanır. Diğer graphql paketlerini kullandıysanız veya kendi graphql api'nizi oluşturmayı denediyseniz, bu paketin ne kadar önemli ve zaman kazandırdığını anlayacaksınız. Yine de, ihtiyacınız olacak tüm kişiselleştirmeyi sağlar! İşte o paket üzerindeki belgelere bir link.

import {resolver} 'graphql-sequelize';
{User} '../../models' adresinden içe aktarın;
'a-a-js'den' ithal;

ihracat const Sorgu = {
    getUser: resolver (Kullanıcı, {
        önce: async (findOptions, {}, {user}) => {
            return findOptions.where = {id: user.id};
        },
        sonra: (kullanıcı) => {
            geri dönüş kullanıcısı;
        }
    }),
    loginUser: resolver (Kullanıcı, {
        önce: async (findOptions, {email}) => {
            findOptions.where = {email};
        },
        sonra: async (kullanıcı, {şifre}) => {
            err olsun;
            [err, user] = bekliyor (user.comparePassword (password));
            if (err) {
              console.log (ERR);
              yeni Hata atmak (err);
            }

            user.login = true; // direktifin bu kullanıcının bir yetkilendirme başlığı olmadan onaylandığını bilmesini sağlamak için
            geri dönüş kullanıcısı;
        }
    }),
};

graphql / kullanıcı / user.mutation.ts

Bu dosya, uygulamamızın kullanıcı bölümü için tüm mutasyonları içerir.

import {resolver as rs} 'graphql-sequelize';
{User} '../../models' adresinden içe aktarın;
'a-a-js'den' ithal;

ihracat const Mutasyon = {
    createUser: rs (Kullanıcı, {
      önce: async (findOptions, {data}) => {
        err, kullanıcı edelim;
        [err, user] = bekliyor (User.create (data));
        if (err) {
          hata atmak;
        }
        findOptions.where = {id: user.id};
        findOptions işlevini döndürür;
      },
      sonra: (kullanıcı) => {
        user.login = true;
        geri dönüş kullanıcısı;
      }
    }),
};

graphql / kullanıcı / user.map.ts

Bu, insanların her zaman göz ardı ettiği ve graphql'de kodlama ve sorgulamayı bu kadar zorlaştıran ve düşük performansa sahip olandır. Ancak, eklediğimiz tüm paketler sorunu çözdü. Türleri birbirine eşlemek, graphql'e gücünü ve gücünü veren şeydir, ancak insanlar bunu bu kuvveti zayıflığa çevirecek şekilde kodlarlar. Bununla birlikte, kullandığımız tüm paketler bundan kolayca kurtulur.

import {resolver} 'graphql-sequelize';
{User} '../../models' adresinden içe aktarın;
'a-a-js'den' ithal;

dışa aktarma const UserMap = {
    şirket: resolver (User.associations.company),
    jwt: (kullanıcı) => user.getJwt (),
};

Evet bu kadar basit !!!

Not: Kullanıcı şemasındaki graphql yönergeleri, kullanıcıdaki JWT alanı ve getUser sorgusu gibi belirli alanları koruyan şeydir.

Modeller - modeller / index.ts

Sequelize typescript'i kullanıyoruz, böylece değişkenleri bu sınıf tipine ayarlayabiliriz. Bu dosyada paketleri yükleyerek başlıyoruz. Sonra sıraya koyulur ve onu db'ye bağlarız. Sonra modelleri ihraç ediyoruz.

import {Sequelize} 'sequelize-typescript';
{ENV} '../config/env.config' içinden içe aktarın;

ihracat const sequelize = yeni Sequelize ({
        veritabanı: ENV.DB_NAME,
        lehçesi: ENV.DB_DIALECT,
        kullanıcı adı: ENV.DB_USER,
        şifre: ENV.DB_PASSWORD,
        işleçleriAliazlar: false,
        günlüğe kaydetme: yanlış,
        depolama hafızası:',
        modelPaths: [__dirname + '/*.model.ts'],
        modelMatch: (dosyaadı, üye) => {
           filename.substring (0, filename.indexOf ('. model')) dönüş = == member.toLowerCase ();
        },
});
export {Kullanıcı} './user.model';
'{//}} ihracat' ./company.model ';

ModelPaths ve modelMatch, modellerimizin nerede olduğunu ve adlandırma kurallarının ne olduğunu sıralı yazı tipini söyleyen ek seçeneklerdir.

Şirket Modeli - modeller / company.model.ts

Burada sequelize typescript'i kullanarak şirket şemasını tanımladık.

import {Table, Column, Model, HasMany, PrimaryKey, AutoIncrement} 'sequelize-typescript';
{Kullanıcı} '.
@Table ({timestamps: true})
ihracat sınıfı Şirket, Model <Şirket> {'i genişletti

  @ Sütun ({birincilKey: true, autoIncrement: true})
  kimlik Numarası;

  @Kolon, sütun
  name: string;

  @HasMany (() => Kullanıcı)
  kullanıcılar: Kullanıcı [];
}

Kullanıcı Modeli - modeller / user.model.ts

Burada kullanıcı modelini tanımlarız. Ayrıca kimlik doğrulama için bazı özel işlevler ekleyeceğiz.

import {Table, Column, Model, HasMany, PrimaryKey, AutoIncrement, Ait Olduğum, ForeignKey, BeforeSave} 'sequelize-typescript';
{Company} 'yi "./company.model" adresinden içe aktarın;
'bcrypt' adresinden bcrypt olarak içe aktar *;
'a-a-js'den' ithal;
'jsonwebtoken' den jsonwebtoken olarak içe aktarma;
{ENV} '../config' dosyasından içe aktarın;

@Table ({timestamps: true})
ihracat sınıfı Kullanıcı Model  {'ı genişletiyor
  @ Sütun ({birincilKey: true, autoIncrement: true})
  kimlik Numarası;

  @Kolon, sütun
  firstName: string;

  @Kolon, sütun
  soyadı: string;

  @Kolon, sütun
  e-posta: string;

  @Kolon, sütun
  şifre: dize;

  @ForeignKey (() => Şirket)
  @Kolon, sütun
  companyId: number;

  @BelongsTo (() => Şirket)
  şirket: Şirket;
  jwt: string;
  giriş: boolean;
  @BeforeSave
  statik async hashPassword (kullanıcı: Kullanıcı) {
    err olsun;
    if (user.changed ('password')) {
        tuz, karma olsun;
        [err, tuz] = bekliyor (bcrypt.genSalt (10));
        if (err) {
          hata atmak;
        }

        [err, hash] = (bcrypt.hash (user.password, salt));
        if (err) {
          hata atmak;
        }
        user.password = karma;
    }
  }

  async comparePassword (pw) {
      err, geçmesine izin ver;
      if (! this.password) {
        yeni hata atmak ('şifresi yok');
      }

      [err, pass] = bekliyor (bcrypt.compare (pw, this.password));
      if (err) {
        hata atmak;
      }

      if (! pass) {
        'Geçersiz şifre' at;
      }

      bunu geri ver;
  };

  getJwt () {
      'Bearer' + jsonwebtoken.sign ({
          id: this.id,
      }, ENV.JWT_ENCRYPTION, {expiresIn: ENV.JWT_EXPIRATION});
  }
}

Buradaki kod çok, bu yüzden yorum yapmamı istiyorsanız yorum yapın.

Gelişmeler için herhangi bir öneriniz varsa, lütfen bana bildirin! Düzenli bir javascript şablonunu hazırlamamı istiyorsanız bana da bildirin! Ayrıca, herhangi bir sorunuz varsa, aynı gün yanıt vermeye çalışacağım, lütfen sormaya korkmayın!

Teşekkürler,

Brian Schardt