Bouw een Graphql Api voor Node & MYSQL 2019— JWT

Als je hier bent, weet je het waarschijnlijk al. Je weet dat Graphql geweldig FREAKING is, de ontwikkeling versnelt en waarschijnlijk het beste is wat er is gebeurd sinds Tesla het model S heeft uitgebracht.

Hier is een nieuwe sjabloon die ik gebruik: https://medium.com/@brianschardt/best-graphql-apollo-sql-and-nestjs-template-458f9478b54e

De meeste tutorials die ik heb gelezen, laten echter zien hoe een graphql-app te bouwen, maar introduceren het veel voorkomende n + 1-verzoekenprobleem. Als gevolg hiervan zijn de prestaties meestal super slecht.

Is dit echt beter dan een Tesla?

Mijn doel in dit artikel is niet om de basisprincipes van Graphql uit te leggen, maar om iemand te laten zien hoe snel een Graphql API te bouwen die niet het probleem n + 1 heeft.

Als je wilt weten waarom 90% van de nieuwe applicaties graphql api's zouden moeten gebruiken in plaats van rustgevend, klik dan hier.

Videosupplement:

Dit sjabloon IS BEDOELD voor productie omdat het eenvoudige manieren bevat om omgevingsvariabelen te beheren en het heeft een georganiseerde structuur zodat de code niet uit de hand loopt. Om het n + 1-probleem te beheren, gebruiken we het laden van gegevens, het ding dat Facebook heeft uitgebracht om dit probleem op te lossen.

Verificatie: JWT

ORM: Volgorde

Database: Mysql of Postgres

Andere belangrijke pakketten die worden gebruikt: express, apollo-server, graphql-sequelize, dataloader-sequelize

Opmerking: Typescript wordt gebruikt voor de app. Het is zo vergelijkbaar met javascript, als je nog nooit typoscript hebt gebruikt, zou ik me geen zorgen maken. Als er echter voldoende vraag is, zal ik een reguliere javascript-versie schrijven. Geef commentaar als je dat wilt.

Ermee beginnen

Kloon de repo en installeer knooppuntmodules

Hier is een link naar de repo, ik raad aan deze te klonen zodat deze het beste kan worden gevolgd.

git kloon git@github.com: brianschardt / node_graphql_apollo_template.git
cd node_graphql_apollo_template
npm installeren
// installeer globale pakketten om de applicatie uit te voeren
npm i -g nodemon

Laten we beginnen met .env

Wijzig de naam van example.env in .env en wijzig deze in de juiste referenties voor uw omgeving.

NODE_ENV = ontwikkeling

PORT = 3001

DB_HOST = localhost
DB_PORT = 3306
DB_NAME = Type
DB_USER = root
DB_PASSWORD = root
DB_DIALECT = mysql

JWT_ENCRYPTION = randomEncryptionKey
JWT_EXPIRATION = 1y

Voer de code uit

Als uw database actief is en u uw .env-bestand correct hebt bijgewerkt met de juiste informatie, moeten we onze app kunnen uitvoeren. Hiermee worden de tabellen met het gedefinieerde schema automatisch in de database gemaakt.

// gebruik voor ontwikkeling terwijl dit veranderingen in de code in de gaten houdt.
npm run start: kijken
// gebruik voor productie
npm run start

Ga nu naar uw browser en voer in: http: // localhost: 3001 / graphql

Je zou nu graphql playground moeten zien, waarmee je documentatie kunt bekijken over welke mutaties en queries er al zijn. Hiermee kunt u ook vragen stellen met de API. Er zijn er al een paar gemaakt, maar om de kracht van deze sjabloon-API volledig te testen, kunt u de database handmatig met informatie zaaien.

Database- en Graphql-schema

Zoals je kunt zien aan het schema op graphql speeltuin heeft het een vrij eenvoudige structuur. Er zijn slechts 2 tabellen, d.w.z. Gebruiker en Bedrijf. Een gebruiker kan tot één bedrijf behoren en een bedrijf kan veel gebruikers hebben, d.w.z. een een op veel associatie.

Maak een gebruiker aan

Voorbeeld gql om in de speeltuin te rennen om een ​​gebruiker te maken. Dit retourneert ook een JWT zodat u zich kunt verifiëren voor toekomstige verzoeken.

mutatie{
  createUser (gegevens: {firstName: "test", e-mail: "test@test.com", wachtwoord: "1"}) {
    ID kaart
    Voornaam
    JWT
  }
}

authenticeren:

Nu je de JWT hebt, laten we de authenticatie testen met gql playground om te controleren of alles correct werkt. Links onderaan de webpagina staat tekst met HTTP-HEADERS. Klik erop en voer dit in:

Opmerking: vervang door uw token.

{
  "Authorization": "Bearer eyJhbGciOiJ ..."
}

Voer deze zoekopdracht nu uit in de speeltuin:

vraag {
  getUser {
    ID kaart
    Voornaam
  }
}

Als alles goed werkt, moeten uw naam en gebruikers-ID worden teruggestuurd.

Als u nu handmatig uw database met een bedrijfsnaam en id zaait en die id aan uw gebruiker toewijst en deze query uitvoert. Het bedrijf moet worden teruggestuurd.

vraag {
  getUser {
    ID kaart
    Voornaam
    bedrijf{
      ID kaart
      naam
    }
  }
}

Ok nu je weet hoe je deze API kunt gebruiken en testen, kun je de code ingaan!

Code duik

Hoofdbestand - app.ts

Afhankelijkheden laden - laadt DB-modellen en env-variabelen.

import * als express van 'express';
import * als jwt uit 'express-jwt';
import {ApolloServer} uit 'apollo-server-express';
import {sequelize} uit './models';
import {ENV} uit './config';

importeer {resolver als resolvers, schema, schemaDirectives} uit './graphql';
import {createContext, EXPECTED_OPTIONS_KEY} uit 'dataloader-sequelize';
importeren naar van 'await-to-js';

const app = express ();

Middleware en Apollo Server instellen!

Opmerking: de "createContext (vervolg)" is het probleem van n + 1 kwijt. Dit wordt allemaal op de achtergrond gedaan door nu een vervolg te maken. MAGIE!! Dit maakt gebruik van het Facebook-dataloader-pakket.

const authMiddleware = jwt ({
    geheim: ENV.JWT_ENCRYPTION,
    referenties Vereist: false,
});
app.use (authMiddleware);
app.use (functie (err, req, res, next) {
    const errorObject = {error: true, bericht: `$ {err.name}:
$ {Err.message} `};
    if (err.name === 'UnauthorizedError') {
        return res.status (401) .json (errorObject);
    } anders {
        return res.status (400) .json (errorObject);
    }
});
const server = nieuwe ApolloServer ({
    typeDefs: schema,
    resolvers,
    schemaDirectives,
    speelplaats: waar,
    context: ({req}) => {
        terug {
            [EXPECTED_OPTIONS_KEY]: createContext (vervolg),
            gebruiker: req.user,
        }
    }
});
server.applyMiddleware ({app});

Luister naar verzoeken

app.listen ({port: ENV.PORT}, async () => {
    console.log (` Server gereed op http: // localhost: $ {ENV.PORT} $ {server.graphqlPath}`);
    laten vergissen;
    [err] = wachten op (sequelize.sync (
        // {force: true},
    ));

    if (err) {
        console.error ('Fout: kan geen verbinding maken met database');
    } anders {
        console.log ('Verbonden met database');
    }
});

Configuratievariabelen - config / env.config.ts

We gebruiken de dotenv om onze .env-variabelen in onze app te laden.

import * als dotEnv uit 'dotenv';
dotEnv.config ();

export const ENV = {
    POORT: 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 || 'wortel',
    DB_PASSWORD: process.env.DB_PASSWORD || 'wortel',
    DB_DIALECT: process.env.DB_DIALECT || 'Mysql',

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

Graphql-tijd !!!

Laten we die resolvers eens bekijken!

graphql / index.ts

Hier gebruiken we het pakket schemalijm. Dit helpt ons schema, query's en mutaties op te splitsen in afzonderlijke delen om schone en georganiseerde code te behouden. Dit pakket doorzoekt automatisch de map die we opgeven voor 2 bestanden, d.w.z. schema.graphql en resolver.ts. Het grijpt ze dan en plakt ze aan elkaar. Vandaar de naam schema lijm.

Richtlijnen: voor onze richtlijnen maken we een map voor hen en nemen ze op via een index.ts-bestand.

import * als lijm uit 'schemaglue';
exporteer {schemaDirectives} vanuit './directives';
export const {schema, resolver} = glue ('src / graphql', {mode: 'ts'});

We maken mappen voor elk model dat we hebben voor consistentie. We hebben dus een gebruikers- en bedrijfsmap.

graphql / user

We hebben gemerkt dat het resolverbestand, zelfs bij gebruik van schemalijm, nog steeds erg groot kan worden. Dus hebben we besloten om het verder op te splitsen op basis van of het een query, mutatie of kaart voor een type is. We hebben dus nog 3 bestanden.

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

Opmerking: als u gql-abonnementen wilt toevoegen, maakt u een ander bestand met de naam: user.subscription.ts en neemt u dit op in het resolverbestand.

graphql / gebruiker / resolver.ts

Dit bestand is vrij eenvoudig en servers om de andere bestanden in deze map te organiseren.

import {Query} uit './user.query';
import {UserMap} uit "./user.map";
import {Mutation} uit "./user.mutation";

export const resolver = {
  Zoekopdracht: zoekopdracht,
  Gebruiker: UserMap,
  Mutatie: mutatie
};

graphql / gebruiker / schema.graphql

Dit bestand definieert ons graphql-schema en resolvers! Super belangrijk!

type Gebruiker {
  id: Int
  email: String
  firstName: String
  lastName: String
  bedrijf: bedrijf
  jwt: String @isAuthUser
}

invoer UserInput {
    email: String
    wachtwoord: String
    firstName: String
    lastName: String
}

type zoekopdracht {
   getUser: Gebruiker @isAuth
   loginUser (email: String !, wachtwoord: String!): Gebruiker
}

type mutatie {
   createUser (data: UserInput): Gebruiker
}

graphql / gebruiker / user.query.ts

Dit bestand bevat de functionaliteit voor al onze gebruikersquery's en mutaties. Gebruikt de magie van graphql-sequelize om veel van de graphql-dingen te verwerken. Als je andere graphql-pakketten hebt gebruikt of hebt geprobeerd je eigen graphql-api te maken, zul je herkennen hoe belangrijk en tijdbesparend dit pakket is. Toch biedt het u nog steeds alle aanpassingen die u ooit nodig zult hebben! Hier is een link naar documentatie over dat pakket.

import {resolver} uit 'graphql-sequelize';
import {User} uit '../../models';
importeren naar van 'await-to-js';

export const Query = {
    getUser: resolver (Gebruiker, {
        voorheen: async (findOptions, {}, {user}) => {
            return findOptions.where = {id: user.id};
        },
        after: (user) => {
            terugkerende gebruiker;
        }
    }),
    loginUser: resolver (Gebruiker, {
        voorheen: async (findOptions, {email}) => {
            findOptions.where = {email};
        },
        after: async (gebruiker, {wachtwoord}) => {
            laten vergissen;
            [err, user] = wachten op (user.comparePassword (wachtwoord));
            if (err) {
              console.log (err);
              gooi nieuwe fout (err);
            }

            user.login = true; // om de richtlijn te laten weten dat deze gebruiker is geverifieerd zonder een koptekst voor autorisatie
            terugkerende gebruiker;
        }
    }),
};

graphql / gebruiker / user.mutation.ts

Dit bestand bevat alle mutaties voor het gebruikersgedeelte van onze app.

import {resolver as rs} uit 'graphql-sequelize';
import {User} uit '../../models';
importeren naar van 'await-to-js';

export const Mutation = {
    createUser: rs (Gebruiker, {
      voorheen: async (findOptions, {data}) => {
        laat vergissen, gebruiker;
        [err, user] = wachten op (User.create (data));
        if (err) {
          gooien fout;
        }
        findOptions.where = {id: user.id};
        terugkeer findOptions;
      },
      after: (user) => {
        user.login = true;
        terugkerende gebruiker;
      }
    }),
};

graphql / gebruiker / user.map.ts

Dit is degene die mensen altijd over het hoofd zien, en maakt codering en query's in graphql zo moeilijk en hebben slechte prestaties. Alle pakketten die we hebben opgenomen, lossen het probleem echter op. Het aan elkaar koppelen van typen geeft Graphql zijn kracht en sterkte, maar mensen coderen het op zo'n manier dat deze kracht in een zwakte verandert. Alle pakketten die we hebben gebruikt, verwijderen dat echter op een eenvoudige manier.

import {resolver} uit 'graphql-sequelize';
import {User} uit '../../models';
importeren naar van 'await-to-js';

export const UserMap = {
    bedrijf: resolver (User.associations.company),
    jwt: (user) => user.getJwt (),
};

Ja dat is zo simpel !!!

Opmerking: de graphql-richtlijnen in het gebruikersschema beschermen bepaalde velden zoals het JWT-veld voor de gebruiker en de getUser-query.

Modellen - modellen / index.ts

We gebruiken het opeenvolgende typoscript zodat we variabelen kunnen instellen op dit klassentype. In dit bestand beginnen we met het laden van de pakketten. Vervolgens maken we onmiddellijk sequelize en verbinden we deze met onze DB. Daarna exporteren we de modellen.

import {Sequelize} uit 'sequelize-typescript';
import {ENV} uit '../config/env.config';

export const sequelize = nieuw Sequelize ({
        database: ENV.DB_NAME,
        dialect: ENV.DB_DIALECT,
        gebruikersnaam: ENV.DB_USER,
        wachtwoord: ENV.DB_PASSWORD,
        operatorsAliases: false,
        logging: onwaar,
        opslag: ': geheugen:',
        modelPaths: [__dirname + '/*.model.ts'],
        modelMatch: (bestandsnaam, lid) => {
           return filename.substring (0, filename.indexOf ('. model')) === member.toLowerCase ();
        },
});
exporteer {Gebruiker} van './user.model';
exporteer {Bedrijf} van './company.model';

De modelPaden en modelMatch zijn extra opties die sequelize-typescript vertellen waar onze modellen zijn en wat hun naamgevingsconventies zijn.

Bedrijfsmodel - modellen / company.model.ts

Hier definiëren we het bedrijfsschema met behulp van sequelize typescript.

import {Table, Column, Model, HasMany, PrimaryKey, AutoIncrement} uit 'sequelize-typescript';
import {User} uit './user.model'
@Table ({timestamps: true})
exportklasse Bedrijf breidt Model  uit

  @Column ({primaryKey: true, autoIncrement: true})
  ID nummer;

  @Kolom
  naam: string;

  @HasMany (() => Gebruiker)
  gebruikers: Gebruiker [];
}

Gebruikersmodel - modellen / user.model.ts

Hier definiëren we het gebruikersmodel. We zullen ook enkele aangepaste functionaliteit toevoegen voor authenticatie.

import {Table, Column, Model, HasMany, PrimaryKey, AutoIncrement, BelongsTo, ForeignKey, BeforeSave} uit 'sequelize-typescript';
importeer {Bedrijf} uit "./company.model";
import * als bcrypt uit 'bcrypt';
importeren naar van 'await-to-js';
import * als jsonwebtoken van'jsonwebtoken ';
import {ENV} uit '../config';

@Table ({timestamps: true})
exportklasse Gebruiker breidt Model  {uit
  @Column ({primaryKey: true, autoIncrement: true})
  ID nummer;

  @Kolom
  firstName: string;

  @Kolom
  lastName: string;

  @Kolom
  email: string;

  @Kolom
  wachtwoord: string;

  @ForeignKey (() => Bedrijf)
  @Kolom
  companyId: nummer;

  @BelongsTo (() => Bedrijf)
  bedrijf: bedrijf;
  jwt: string;
  login: boolean;
  @BeforeSave
  static async hashPassword (gebruiker: Gebruiker) {
    laten vergissen;
    if (user.changed ('wachtwoord')) {
        laat zout, hasj;
        [err, salt] = wachten op (bcrypt.genSalt (10));
        if (err) {
          gooien fout;
        }

        [err, hash] = wachten op (bcrypt.hash (user.password, salt));
        if (err) {
          gooien fout;
        }
        user.password = hash;
    }
  }

  async ComparePassword (pw) {
      laten dwalen, passeren;
      if (! this.password) {
        throw new Error ('Heeft geen wachtwoord');
      }

      [err, pass] = wachten op (bcrypt.compare (pw, this.password));
      if (err) {
        gooien fout;
      }

      if (! pass) {
        gooi 'Ongeldig wachtwoord';
      }

      geef dit terug;
  };

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

Dat is veel code daar, dus geef commentaar als je wilt dat ik het opsplits.

Laat het me weten als u suggesties voor verbeteringen heeft! Als je wilt dat ik een sjabloon maak in gewoon JavaScript, laat het me dan ook weten! Ook als ik vragen heb, zal ik proberen dezelfde dag te reageren, dus wees niet bang om te vragen!

Bedankt,

Brian Schardt