Xây dựng một Api đồ thị cho Node & MYSQL 2019

Nếu bạn ở đây có lẽ bạn đã biết. Bạn biết rằng Graphql đang FREAKING tuyệt vời, tăng tốc độ phát triển và có lẽ là điều tốt nhất đã xảy ra kể từ khi Tesla phát hành mô hình S.

Đây là một mẫu mới mà tôi sử dụng: https://medium.com/@brianschardt/best-graphql-apollo-sql-and-nestjs-template-458f9478b54e

Tuy nhiên, hầu hết các hướng dẫn tôi đã đọc cho thấy cách xây dựng một ứng dụng graphql nhưng giới thiệu vấn đề yêu cầu n + 1 phổ biến. Kết quả là hiệu suất thường siêu kém.

Thực sự điều này tốt hơn một Tesla?

Mục tiêu của tôi trong bài viết này không phải là để giải thích những điều cơ bản của Graphql, mà là chỉ cho ai đó cách nhanh chóng xây dựng API Graphql không có vấn đề n + 1.

Nếu bạn muốn biết lý do tại sao 90% các ứng dụng mới nên sử dụng api biểu đồ thay vì yên tĩnh bấm vào đây.

Bổ sung video:

Mẫu này CÓ THỂ được sử dụng để sản xuất vì nó chứa các cách dễ dàng để quản lý các biến môi trường và có cấu trúc có tổ chức để mã sẽ không bị mất kiểm soát. Để quản lý vấn đề n + 1, chúng tôi sử dụng tải dữ liệu, điều mà facebook phát hành để giải quyết vấn đề này.

Xác thực: JWT

ORM: Sắp xếp lại

Cơ sở dữ liệu: Mysql hoặc Postgres

Các gói quan trọng khác được sử dụng: express, apollo-server, graphql-sequelize, dataloader-sequelize

Lưu ý: Bản đánh máy được sử dụng cho ứng dụng. Nó rất giống với javascript, nếu bạn chưa bao giờ sử dụng bản thảo, tôi sẽ không lo lắng. Tuy nhiên, nếu có đủ nhu cầu, tôi sẽ viết một phiên bản javascript thông thường. Bình luận nếu bạn muốn điều đó.

Bắt đầu

Sao chép repo và cài đặt các mô-đun nút

Đây là một liên kết đến repo, tôi khuyên bạn nên nhân bản nó để theo dõi tốt nhất.

git clone git@github.com: brianschardt / node_graphql_apollo_template.git
cd node_graphql_apollo_template
cài đặt npm
// cài đặt các gói toàn cầu để chạy ứng dụng
npm i -g nốt

Hãy bắt đầu với .env

Đổi tên example.env thành .env và thay đổi nó thành thông tin đăng nhập chính xác cho môi trường của bạn.

NODE_ENV = phát triển

CẢNG = 3001

DB_HOST = localhost
DB_PORT = 3306
DB_NAME = loại
DB_USER = root
DB_PASSWORD = root
DB_DIALECT = mysql

JWT_ENCRYPTION = RandomEncryptKey
JWT_EXPIRATION = 1y

Chạy mã

Bây giờ nếu cơ sở dữ liệu của bạn đang chạy và bạn đã cập nhật chính xác tệp .env của mình với thông tin phù hợp, chúng tôi sẽ có thể chạy ứng dụng của chúng tôi. Điều này sẽ tạo các bảng với lược đồ được xác định tự động trong cơ sở dữ liệu.

// sử dụng để phát triển vì đồng hồ này thay đổi trong mã.
npm chạy bắt đầu: xem
// sử dụng cho sản xuất
npm chạy bắt đầu

Bây giờ hãy xem trình duyệt của bạn và nhập: http: // localhost: 3001 / graphql

Bây giờ bạn sẽ thấy sân chơi graphql cho phép bạn xem tài liệu về những đột biến và truy vấn đã tồn tại. Nó cũng cho phép bạn thực hiện các truy vấn đối với API. Có một vài trong số chúng đã được thực hiện, nhưng để kiểm tra đầy đủ sức mạnh của API mẫu này, bạn có thể muốn tự tạo thông tin cơ sở dữ liệu.

Cơ sở dữ liệu và lược đồ đồ thị

Như bạn thấy khi nhìn vào lược đồ trên sân chơi graphql, nó có cấu trúc khá đơn giản. Chỉ có 2 bảng, tức là Người dùng và Công ty. Người dùng có thể thuộc về một Công ty và Công ty có thể có nhiều người dùng, nghĩa là một liên kết với nhiều người dùng.

Tạo người dùng

Ví dụ gql để chạy trong sân chơi để tạo người dùng. Điều này cũng sẽ trả về JWT để bạn có thể xác thực cho các yêu cầu trong tương lai.

đột biến {
  createdUser (data: {firstName: "test", email: "test@test.com", mật khẩu: "1"}) {
    Tôi
    tên đầu tiên
    jwt
  }
}

Xác thực:

Bây giờ bạn đã có JWT, hãy cho phép kiểm tra xác thực với sân chơi gql để đảm bảo mọi thứ đều hoạt động chính xác. Ở phía dưới bên trái của trang web sẽ có văn bản cho biết HTTP HeadERS. Nhấn vào nó và nhập vào đây:

Lưu ý: thay thế bằng mã thông báo của bạn.

{
  "Ủy quyền": "Người mang mắtJhbGciOiJ ..."
}

Bây giờ hãy chạy truy vấn này trong sân chơi:

truy vấn{
  getUser {
    Tôi
    tên đầu tiên
  }
}

Nếu mọi thứ hoạt động, tên và id người dùng của bạn sẽ được trả lại.

Bây giờ nếu bạn tự chọn cơ sở dữ liệu của mình, với tên và id công ty và gán id đó cho người dùng của bạn và chạy truy vấn này. Công ty nên được trả lại.

truy vấn{
  getUser {
    Tôi
    tên đầu tiên
    Công ty{
      Tôi
      Tên
    }
  }
}

Ok bây giờ bạn đã biết cách sử dụng và kiểm tra API này, hãy nhập mã!

Mã lặn

Tập tin chính - app.ts

Tải phụ thuộc - tải mô hình db và biến env.

nhập * như thể hiện từ 'express';
nhập * dưới dạng jwt từ 'express-jwt';
nhập {ApolloServer} từ 'apollo-server-express';
nhập {sequelize} từ './models';
nhập {ENV} từ './config';

nhập {trình phân giải dưới dạng trình phân giải, lược đồ, lược đồDirectives} từ './graphql';
nhập {createContext, EXPECTED_OPTIONS_KEY} từ 'dataloader-sequelize';
nhập vào từ 'await-to-js';

const app = express ();

Thiết lập phần mềm trung gian và máy chủ Apollo!

Lưu ý: các phần mềm tạo raContext (sắp xếp lại), đó là những gì thoát khỏi vấn đề n + 1. Tất cả điều này được thực hiện trong nền bằng cách sắp xếp lại ngay bây giờ. MA THUẬT!! Điều này sử dụng gói tải dữ liệu facebook.

const authMiddleware = jwt ({
    bí mật: ENV.JWT_ENCRYPTION,
    thông tin đăng nhập: sai,
});
app.use (authMiddleware);
app.use (hàm (err, req, res, next) {
    const errorObject = {error: true, thông báo: `$ {err.name}:
$ {err.message} `};
    if (err.name === 'UnlegError') {
        trả về res.status (401) .json (errorObject);
    } khác {
        trả lại res.status (400) .json (errorObject);
    }
});
máy chủ const = ApolloServer mới ({
    typeDefs: lược đồ,
    người giải quyết,
    lược đồ
    sân chơi: đúng,
    bối cảnh: ({req}) => {
        trở về {
            [EXPECTED_OPTIONS_KEY]: createdContext (sắp xếp lại),
            người dùng: req.user,
        }
    }
});
server.applyMiddleware ({app});

Lắng nghe yêu cầu

app.listen ({port: ENV.PORT}, async () => {
    console.log (` Máy chủ đã sẵn sàng tại http: // localhost: $ {ENV.PORT} $ {server.graphqlPath}`);
    để sai lầm;
    [err] = đang chờ (sequelize.sync (
        // {lực lượng: đúng},
    ));

    nếu (lỗi) {
        console.error ('Lỗi: Không thể kết nối với cơ sở dữ liệu');
    } khác {
        console.log ('Đã kết nối với cơ sở dữ liệu');
    }
});

Các biến cấu hình - config / env.config.ts

Chúng tôi sử dụng dotenv để tải các biến .env vào ứng dụng của chúng tôi.

nhập * dưới dạng dotEnv từ 'dotenv';
dotEnv.config ();

xuất const ENV = {
    CẢNG: 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 || 'nguồn gốc',
    DB_PASSWORD: process.env.DB_PASSWORD || 'nguồn gốc',
    DB_DIALECT: process.env.DB_DIALECT || 'mysql',

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

Thời gian đồ thị !!!

Hãy cùng xem những người giải quyết!

graphql / index.ts

Ở đây chúng tôi đang sử dụng keo gói lược đồ. Điều này giúp chia lược đồ, truy vấn và đột biến của chúng tôi thành các phần riêng biệt để duy trì mã sạch và có tổ chức. Gói này sẽ tự động tìm kiếm thư mục mà chúng tôi chỉ định cho 2 tệp, ví dụ: giản đồ lược đồ và độ phân giải. Sau đó nó lấy chúng và dán chúng lại với nhau. Do đó keo lược đồ tên.

Chỉ thị: đối với các chỉ thị của chúng tôi, chúng tôi tạo một thư mục cho chúng và bao gồm chúng thông qua tệp index.ts.

nhập * dưới dạng keo từ 'schemaglue';
xuất {giản đồDirectives} từ './directives';
export const {lược đồ, độ phân giải} = keo ('src / graphql', {mode: 'ts'});

Chúng tôi đang tạo các thư mục cho mỗi mô hình chúng tôi có tính nhất quán. Vì vậy, chúng tôi có một thư mục người dùng và công ty.

đồ thị / người dùng

Chúng tôi nhận thấy tệp trình phân giải, ngay cả khi sử dụng keo lược đồ, vẫn có thể trở nên rất lớn. Vì vậy, chúng tôi quyết định chia nhỏ nó dựa trên nếu đó là truy vấn, đột biến hoặc bản đồ cho một loại. Như vậy, chúng tôi có thêm 3 tập tin.

  • user.query.ts
  • user.muting.ts
  • user.map.ts

Lưu ý: Nếu bạn muốn thêm đăng ký gql, bạn sẽ tạo một tệp khác có tên: user.subcrip.ts và đưa nó vào tệp trình phân giải.

graphql / user / decver.ts

Tệp này khá đơn giản và các máy chủ để tổ chức các tệp khác trong thư mục này.

nhập {Truy vấn} từ './user.query';
nhập {Bản đồ người dùng} từ "./user.map";
nhập {Đột biến} từ "./user.muting";

xuất const constver = {
  Truy vấn: Truy vấn,
  Người dùng: Bản đồ người dùng,
  Đột biến: Đột biến
};

đồ thị / người dùng / lược đồ

Tệp này xác định lược đồ graphql và trình phân giải của chúng tôi! Siêu quan trọng!

gõ Người dùng {
  id: Int
  email: Chuỗi
  Tên đầu tiên: Chuỗi
  Họ: Chuỗi
  Công ty Công ty
  jwt: Chuỗi @isAuthUser
}

đầu vào UserInput {
    email: Chuỗi
    mật khẩu: Chuỗi
    Tên đầu tiên: Chuỗi
    Họ: Chuỗi
}

loại Truy vấn {
   getUser: Người dùng @isAuth
   loginUser (email: String!, password: String!): Người dùng
}

loại Đột biến {
   createdUser (data: UserInput): Người dùng
}

đồ thị / người dùng / người dùng.query.ts

Tệp này chứa chức năng cho tất cả các truy vấn và đột biến người dùng của chúng tôi. Sử dụng phép thuật từ graphql-sequelize để xử lý rất nhiều công cụ graphql. Nếu bạn đã sử dụng các gói graphql khác hoặc đã thử tạo api graphql của riêng bạn, bạn sẽ nhận ra mức độ quan trọng và thời gian của gói này. Tuy nhiên, nó vẫn cung cấp cho bạn tất cả các tùy chỉnh mà bạn sẽ cần! Ở đây, một liên kết đến tài liệu về gói đó.

nhập {decver} từ 'graphql-sequelize';
nhập {Người dùng} từ '../../models';
nhập vào từ 'await-to-js';

xuất const Query = {
    getUser: decver (Người dùng, {
        trước: async (findOptions, {}, {user}) => {
            return findOptions.where = {id: user.id};
        },
        sau: (người dùng) => {
            trả lại người dùng;
        }
    }),
    loginUser: decver (Người dùng, {
        trước: async (findOptions, {email}) => {
            findOptions.where = {email};
        },
        sau: async (người dùng, {password}) => {
            để sai lầm;
            [err, user] = đang chờ (user.comparePassword (mật khẩu));
            nếu (lỗi) {
              console.log (err);
              ném lỗi mới (err);
            }

            user.login = true; // để cho chỉ thị biết rằng người dùng này được xác thực mà không có tiêu đề ủy quyền
            trả lại người dùng;
        }
    }),
};

graphql / user / user.muting.ts

Tệp này chứa tất cả các đột biến cho phần người dùng trong ứng dụng của chúng tôi.

nhập {độ phân giải dưới dạng rs} từ 'graphql-sequelize';
nhập {Người dùng} từ '../../models';
nhập vào từ 'await-to-js';

xuất const const Đột biến = {
    createdUser: rs (Người dùng, {
      trước: async (findOptions, {data}) => {
        để lỗi, người dùng;
        [err, user] = await to (User.create (data));
        nếu (lỗi) {
          ném nhầm;
        }
        findOptions.where = {id: user.id};
        trả về findOptions;
      },
      sau: (người dùng) => {
        user.login = true;
        trả lại người dùng;
      }
    }),
};

đồ thị / người dùng / user.map.ts

Đây là một điều mà mọi người luôn bỏ qua, và làm cho việc mã hóa và truy vấn trong graphql trở nên khó khăn và có hiệu suất kém. Tuy nhiên, tất cả các gói chúng tôi đã bao gồm giải quyết vấn đề. Các kiểu ánh xạ cho nhau là những gì mang lại cho sức mạnh và sức mạnh của graphql, nhưng mọi người mã hóa nó theo cách làm cho sức mạnh này biến thành điểm yếu. Tuy nhiên, tất cả các gói chúng tôi đã sử dụng được loại bỏ điều đó một cách dễ dàng.

nhập {decver} từ 'graphql-sequelize';
nhập {Người dùng} từ '../../models';
nhập vào từ 'await-to-js';

xuất const const Bản đồ = {
    công ty: giải quyết (User.associations.company),
    jwt: (người dùng) => user.getJwt (),
};

Vâng, nó rất đơn giản !!!

Lưu ý: các chỉ thị graphql trong lược đồ người dùng là những gì bảo vệ các trường nhất định như trường JWT trên người dùng và truy vấn getUser.

Mô hình - mô hình / index.ts

Chúng tôi sử dụng bản thảo sắp xếp thứ tự để chúng tôi có thể đặt các biến cho loại lớp này. Trong tập tin này, chúng tôi bắt đầu bằng cách tải các gói. Sau đó, chúng tôi khởi tạo tuần tự hóa và kết nối nó với db của chúng tôi. Sau đó, chúng tôi xuất khẩu các mô hình.

nhập {Sequelize} từ 'sequelize-typecript';
nhập {ENV} từ '../config/env.config';

export const sequelize = new Sequelize ({
        cơ sở dữ liệu: ENV.DB_NAME,
        phương ngữ: ENV.DB_DIALECT,
        tên người dùng: ENV.DB_USER,
        mật khẩu: ENV.DB_PASSWORD,
        Toán tử Aliases: false,
        đăng nhập: sai,
        Bộ nhớ lưu trữ:',
        modelPaths: [__dirname + '/*.model.ts'],
        modelMatch: (tên tệp, thành viên) => {
           return filename.sub chuỗi (0, filename.indexOf ('. model')) === Member.toLowerCase ();
        },
});
xuất {Người dùng} từ './user.model';
xuất {Công ty} từ './company.model';

ModelPaths và modelMatch là các tùy chọn bổ sung cho biết kiểu sắp xếp thứ tự trong đó mô hình của chúng ta là gì và quy ước đặt tên của chúng là gì.

Mô hình công ty - mô hình / company.model.ts

Ở đây chúng tôi xác định lược đồ công ty bằng cách sử dụng bản thảo tuần tự.

nhập {Bảng, Cột, Mô hình, HasMany, PrimaryKey, AutoIncrement} từ 'sequelize-typecript';
nhập {Người dùng} từ './user.model'
@Table ({dấu thời gian: đúng})
lớp xuất khẩu Công ty mở rộng Mô hình  {

  @Column ({sơ cấp: đúng, tự động xác nhận: đúng})
  số ID;

  @Cột
  tên: chuỗi;

  @HasMany (() => Người dùng)
  người dùng: Người dùng [];
}

Mô hình người dùng - mô hình / user.model.ts

Ở đây chúng tôi xác định mô hình người dùng. Chúng tôi cũng sẽ thêm một số chức năng tùy chỉnh để xác thực.

nhập {Bảng, Cột, Mô hình, HasMany, PrimaryKey, AutoIncrement, BelongsTo, ForeignKey, BeforeSave} từ 'sequelize-typecript';
nhập {Công ty} từ "./company.model";
nhập * dưới dạng bcrypt từ 'bcrypt';
nhập vào từ 'await-to-js';
nhập * dưới dạng jsonwebtoken từ'jsonwebtoken ';
nhập {ENV} từ '../config';

@Table ({dấu thời gian: đúng})
lớp xuất Người dùng mở rộng Mô hình  {
  @Column ({sơ cấp: đúng, tự động xác nhận: đúng})
  số ID;

  @Cột
  FirstName: chuỗi;

  @Cột
  Họ: chuỗi;

  @Cột
  email: chuỗi;

  @Cột
  mật khẩu: chuỗi;

  @ForeignKey (() => Công ty)
  @Cột
  công tyId: số;

  @BelongsTo (() => Công ty)
  Công ty Công ty;
  jwt: chuỗi;
  đăng nhập: boolean;
  @B BeforeSave
  hashPassword tĩnh (người dùng: Người dùng) {
    để sai lầm;
    if (user.changed ('password')) {
        để muối, băm;
        [err, salt] = đang chờ (bcrypt.genSalt (10));
        nếu (lỗi) {
          ném nhầm;
        }

        [err, hash] = đang chờ (bcrypt.hash (user.password, salt));
        nếu (lỗi) {
          ném nhầm;
        }
        user.password = hàm băm;
    }
  }

  async notifyPassword (pw) {
      để sai lầm, vượt qua;
      if (! this.password) {
        ném lỗi mới ('Không có mật khẩu');
      }

      [err, pass] = đang chờ (bcrypt.compare (pw, this.password));
      nếu (lỗi) {
        ném nhầm;
      }

      nếu (! vượt qua) {
        ném 'Mật khẩu không hợp lệ';
      }

      trả lại cái này;
  };

  getJwt () {
      trả về 'Người mang' + jsonwebtoken.sign ({
          id: this.id,
      }, ENV.JWT_ENCRYPTION, {hết hạn vào: ENV.JWT_EXPIRATION});
  }
}

Đó là rất nhiều mã ngay tại đó, vì vậy hãy bình luận nếu bạn muốn tôi phá vỡ nó.

Nếu bạn có bất kỳ đề xuất cải tiến xin vui lòng cho tôi biết! Nếu bạn muốn tôi tạo một mẫu trong javascript thông thường cũng cho tôi biết! Ngoài ra, nếu bạn có bất kỳ câu hỏi nào, tôi sẽ cố gắng trả lời trong cùng một ngày, vì vậy xin đừng sợ để hỏi!

Cảm ơn,

Brian Schardt