Bygg en Graphql Api for Node & MYSQL 2019 - JWT

Hvis du er her vet du sannsynligvis allerede. Du vet at Graphql er FREAKING awesome, fremskynder utviklingen, og er sannsynligvis det beste som har skjedd siden Tesla ga ut modellen S.

Her er en ny mal som jeg bruker: https://medium.com/@brianschardt/best-graphql-apollo-sql-and-nestjs-template-458f9478b54e

Imidlertid viser de fleste opplæringsprogrammer jeg har lest hvordan du bygger en grafql-app, men introduserer det vanlige n + 1-forespørselsproblemet. Som et resultat er ytelsen vanligvis super dårlig.

Er dette virkelig bedre enn en Tesla?

Målet mitt med denne artikkelen er ikke å forklare det grunnleggende i Graphql, men å vise noen hvordan du raskt kan bygge et Graphql API som ikke har n + 1-problemet.

Hvis du vil vite hvorfor 90% av de nye applikasjonene bør bruke grafql-api i stedet for å slappe av, klikk her.

Videotillegg:

Denne malen MÅ brukes for produksjon, da den inneholder enkle måter å administrere miljøvariabler på, og har en organisert struktur slik at koden ikke kommer ut av hånden. For å håndtere problemet med n + 1 bruker vi datainnlasting, det som Facebook har utgitt for å løse dette problemet.

Autentisering: JWT

ORM: Sequelize

Database: Mysql eller Postgres

Andre viktige pakker som brukes: express, apollo-server, graphql-sequelize, dataloader-sequelize

Merk: Typescript brukes for appen. Det ligner så mye på javascript, hvis du aldri har brukt typeskript ville jeg ikke være bekymret. Imidlertid, hvis det er nok etterspørsel, vil jeg skrive en vanlig javascript-versjon. Kommenter hvis du vil ha det.

Starter

Klone repoen og installer nodemoduler

Her er en lenke til repoen, jeg anbefaler å klone den for best å følge med.

git klone git@github.com: brianschardt / node_graphql_apollo_template.git
cd node_graphql_apollo_template
npm installasjon
// installere globale pakker for å kjøre applikasjon
npm i -g nodemon

La oss starte med .env

Gi nytt navn til eksempel.env til .env og endre det til riktig legitimasjon for miljøet.

NODE_ENV = utvikling

PORT = 3001

DB_HOST = localhost
DB_PORT = 3306
DB_NAME = type
Db = rot
Db = rot
DB_DIALECT = mysql

JWT_ENCRYPTION = randomEncryptionKey
JWT_EXPIRATION = 1y

Kjør koden

Hvis databasen din kjører og du har oppdatert .env-filen riktig med riktig informasjon, skal vi kunne kjøre appen vår. Dette vil opprette tabellene med det definerte skjemaet automatisk i databasen.

// bruk for utvikling da dette ser på endringer i koden.
npm løpe start: se på
// bruk for produksjon
npm kjørestart

Gå nå til nettleseren din og skriv inn: http: // localhost: 3001 / graphql

Du bør nå se grafql lekeplass som lar deg se dokumentasjon på hvilke mutasjoner og spørsmål som allerede finnes. Det lar deg også lage spørsmål mot API. Det er et par av dem allerede laget, men for å teste ut kraften til denne malen API kan det være lurt å manuelt frø databasen med informasjon.

Database og Graphql-skjema

Som du ser når du ser på skjemaene på grafikklekplassen har den en ganske enkel struktur. Det er bare to tabeller, dvs. bruker og selskap. En bruker kan tilhøre et selskap og et selskap kan ha mange brukere, dvs. en for mange forening.

Opprett en bruker

Eksempel gql for å løpe på lekeplassen for å opprette en bruker. Dette returnerer også en JWT slik at du kan autentisere for fremtidige forespørsler.

mutasjon {
  createUser (data: {firstName: "test", e-post: "test@test.com", passord: "1"}) {
    id
    fornavn
    JWT
  }
}

Verifisere:

Nå som du har JWT, kan du teste autentisering med gql lekeplass for å sikre at alt fungerer som det skal. På venstre bunn av nettsiden vil det være tekst som sier HTTP HEADERS. Klikk på den og skriv inn dette:

Merk: erstatt med symbolet ditt.

{
  "Autorisasjon": "Bærer eyJhbGciOiJ ..."
}

Kjør nå denne spørringen på lekeplassen:

spørsmål{
  getUser {
    id
    fornavn
  }
}

Hvis alt fungerte, skulle ditt navn og bruker-ID returneres.

Nå hvis du manuelt frø databasen din, med et firmanavn og id, og tildeler denne IDen til brukeren din, og kjører denne spørringen. Selskapet skal returneres.

spørsmål{
  getUser {
    id
    fornavn
    selskap{
      id
      Navn
    }
  }
}

OK, nå som du vet hvordan du bruker og tester dette APIet, kan du få inn koden!

Kodedykke

Hovedfil - app.ts

Lastavhengigheter - laster db-modeller og env-variabler.

import * som uttrykk fra 'ekspress';
import * som jwt fra 'express-jwt';
import {ApolloServer} fra 'apollo-server-express';
import {sequelize} fra './models';
import {ENV} fra './config';

import {resolver as resolvers, schema, schemaDirectives} fra './graphql';
importer {createContext, EXPECTED_OPTIONS_KEY} fra 'dataloader-sequelize';
importere til fra 'vente til jer';

const app = express ();

Sett opp mellomvare og Apollo Server!

Merk: “createContext (sequelize)” er det som blir kvitt n + 1-problemet. Dette gjøres i bakgrunnen av oppfølger nå. MAGIC !! Denne bruker facebook dataloader-pakken.

const authMiddleware = jwt ({
    hemmelighet: ENV.JWT_ENCRYPTION,
    legitimasjonKrevd: falsk,
});
app.use (authMiddleware);
app.use (funksjon (feil, spørsmål, res, neste) {
    const errorObject = {error: true, melding: `$ {err.name}:
$ {} Err.message `};
    if (err.name === 'UnauthorizedError') {
        return res.status (401) .json (errorObject);
    } annet {
        return res.status (400) .json (errorObject);
    }
});
const server = new ApolloServer ({
    typeDefs: schema,
    omgjørere,
    schemaDirectives,
    lekeplass: sant,
    kontekst: ({req}) => {
        komme tilbake {
            [EXPECTED_OPTIONS_KEY]: createContext (oppfølger),
            bruker: req.user,
        }
    }
});
server.applyMiddleware ({app});

Lytt etter forespørsler

app.listen ({port: ENV.PORT}, async () => {
    console.log (` Server klar på http: // localhost: $ {ENV.PORT} $ {server.graphqlPath}`);
    la feil;
    [err] = venter på (sequelize.sync (
        // {force: true},
    ));

    if (err) {
        console.error ('Feil: Kan ikke koble til database');
    } annet {
        console.log ('Tilkoblet database');
    }
});

Konfigurasjonsvariabler - config / env.config.ts

Vi bruker dotenv for å laste inn .env-variablene til appen vår.

import * som dotEnv fra 'dotenv';
dotEnv.config ();

eksport const ENV = {
    PORT: prosess.env.PORT || '3000',

    DB_HOST: prosess.env.DB_HOST || '127.0.0.1',
    DB_PORT: process.env.DB_PORT || '3306',
    DB_NAME: prosess.env.DB_NAME || 'Dbnavn',
    DB_USER: prosess.env.DB_USER || 'rot',
    DB_PASSWORD: process.env.DB_PASSWORD || 'rot',
    DB_DIALECT: prosess.env.DB_DIALECT || 'Mysql',

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

Grafittid !!!

La oss se på disse resolusjonene!

graphql / index.ts

Her bruker vi pakkeskjemaet lim. Dette hjelper med å dele opp skjemaene, spørsmålene og mutasjonene våre i separate deler for å opprettholde ren og organisert kode. Denne pakken søker automatisk i katalogen vi spesifiserer for to filer, det vil si schema.graphql og resolver.ts. Den griper dem deretter og limer dem sammen. Derav navnet skjema lim.

Direktiver: for våre direktiver lager vi en katalog for dem og inkluderer dem via en index.ts-fil.

import * som lim fra 'schemaglue';
eksporter {schemaDirectives} fra './directives';
eksport const {schema, resolver} = lim ('src / graphql', {mode: 'ts'});

Vi lager kataloger for hver modell vi har for konsistens. Dermed har vi bruker- og firmakatalogen.

graphql / bruker

Vi la merke til at resolver-filen, selv når du bruker skjema lim, fortsatt kan bli veldig stor. Så vi bestemte oss for å bryte den videre ut basert på om det er et spørsmål, mutasjon eller kart for en type. Dermed har vi 3 filer til.

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

Merk: Hvis du vil legge til gql-abonnementer, vil du opprette en annen fil som heter: user.subscription.ts og inkludere den i resolver-filen.

graphql / bruker / resolver.ts

Denne filen er ganske enkel og servere for å organisere de andre filene i denne katalogen.

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

eksport const resolver = {
  Spørsmål: Spørsmål,
  Bruker: UserMap,
  Mutasjon: Mutasjon
};

graphql / bruker / schema.graphql

Denne filen definerer vårt grafql-skjema og oppløsere! Super viktig!

skriv bruker {
  id: Int
  e-post: String
  fornavn: Streng
  etternavn: String
  selskap: Selskap
  jwt: String @isAuthUser
}

input UserInput {
    e-post: String
    passord: String
    fornavn: Streng
    etternavn: String
}

type spørring {
   getUser: User @isAuth
   loginUser (e-post: String !, passord: String!): Bruker
}

type mutasjon {
   createUser (data: UserInput): Bruker
}

graphql / bruker / user.query.ts

Denne filen inneholder funksjonaliteten for alle brukerens spørsmål og mutasjoner. Bruker magien fra graphql-sequelize for å håndtere mye av graphql-tingene. Hvis du har brukt andre graphql-pakker eller prøvd å lage din egen graphql api, vil du gjenkjenne hvor viktig og tidsbesparende denne pakken er. Likevel gir det deg all den tilpasningen du noen gang vil trenge! Her er en lenke til dokumentasjon om den pakken.

importer {resolver} fra 'graphql-sequelize';
import {Bruker} fra '../../modeller';
importere til fra 'vente til jer';

eksport const Query = {
    getUser: resolver (Bruker, {
        før: async (findOptions, {}, {user}) => {
            return findOptions.where = {id: user.id};
        }
        etter: (bruker) => {
            retur bruker;
        }
    }),
    loginUser: resolver (Bruker, {
        før: async (findOptions, {email}) => {
            findOptions.where = {email};
        }
        etter: async (bruker, {passord}) => {
            la feil;
            [feil, bruker] = venter på (user.comparePassword (passord));
            hvis (feil) {
              console.log (err);
              kaste ny feil (feil);
            }

            user.login = true; // for å gi beskjed om at denne brukeren er autentisert uten en autorisasjonshode
            retur bruker;
        }
    }),
};

graphql / bruker / user.mutation.ts

Denne filen inneholder all mutasjonen for brukerdelen av appen vår.

import {resolver as rs} fra 'graphql-sequelize';
import {Bruker} fra '../../modeller';
importere til fra 'vente til jer';

export const Mutation = {
    createUser: rs (Bruker, {
      før: async (findOptions, {data}) => {
        la feil, bruker;
        [feil, bruker] = avvente (User.create (data));
        hvis (feil) {
          kaste feil;
        }
        findOptions.where = {id: user.id};
        return findOptions;
      }
      etter: (bruker) => {
        user.login = sant;
        retur bruker;
      }
    }),
};

graphql / bruker / user.map.ts

Dette er den som folk alltid overser, og som gjør koding og spørring i graphql så vanskelig og har dårlig ytelse. Alle pakkene vi har inkludert løser imidlertid problemet. Å kartlegge typer til hverandre er det som gir graphql sin kraft og styrke, men folk koder det på en slik måte at får denne styrken til å bli en svakhet. Alle pakkene vi har brukt blir imidlertid kvitt det på en enkel måte.

importer {resolver} fra 'graphql-sequelize';
import {Bruker} fra '../../modeller';
importere til fra 'vente til jer';

eksport konst UserMap = {
    selskap: resolver (User.associations.company),
    jwt: (bruker) => bruker.getJwt (),
};

Ja det er det så enkelt !!!

Merk: grafikkdirektivene i brukerskjemaet er det som beskytter visse felt som JWT-feltet på brukeren og getUser-spørringen.

Modeller - modeller / indeks.ts

Vi bruker sequelize-typeskriptet slik at vi kan stille inn variabler til denne klassetypen. I denne filen starter vi med å laste inn pakkene. Så oppgir vi oppfølger og kobler den til db-en vår. Så eksporterer vi modellene.

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

export const sequelize = new Sequelize ({
        database: ENV.DB_NAME,
        dialekt: ENV.DB_DIALECT,
        brukernavn: ENV.DB_USER,
        passord: ENV.DB_PASSWORD,
        operatørerAliaser: falsk,
        logging: falsk,
        lagring: ': minne:',
        modelPaths: [__dirname + '/*.model.ts'],
        modelMatch: (filnavn, medlem) => {
           return filename.substring (0, filename.indexOf ('. model')) === member.toLowerCase ();
        }
});
eksporter {Bruker} fra './bruker.modell';
eksporter {Company} fra './company.model';

ModelPaths og modelMatch er ekstra alternativer som forteller sequelize-typescript hvor modellene våre er og hva deres navnekonvensjoner er.

Bedriftsmodell - modeller / company.model.ts

Her definerer vi firmaskjemaet ved å bruke sequelize-typeskript.

import {Tabell, kolonne, modell, HasMany, PrimaryKey, AutoIncrement} fra 'sequelize-typescript';
importer {User} fra './user.model'
@Table ({timestamps: true})
eksportklasse Company utvider Model  {

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

  @Kolonne
  navn: streng;

  @HasMany (() => Bruker)
  brukere: Bruker [];
}

Brukermodell - modeller / user.model.ts

Her definerer vi brukermodellen. Vi vil også legge til noe tilpasset funksjonalitet for autentisering.

import {Tabell, kolonne, modell, HasMany, PrimaryKey, AutoIncrement, BelongsTo, ForeignKey, BeforeSave} fra 'sequelize-typescript';
import {Company} fra "./company.model";
import * som bcrypt fra 'bcrypt';
importere til fra 'vente til jer';
import * som jsonwebtoken fra 'jsonwebtoken';
import {ENV} fra '../config';

@Table ({timestamps: true})
Eksportklasse Bruker utvider Model  {
  @Column ({primaryKey: true, autoIncrement: true})
  ID-nummer;

  @Kolonne
  fornavn: streng;

  @Kolonne
  etternavn: streng;

  @Kolonne
  e-post: streng;

  @Kolonne
  passord: streng;

  @ForeignKey (() => Selskap)
  @Kolonne
  firmaId: nummer;

  @BelongsTo (() => Firma)
  selskap: selskap;
  jwt: streng;
  innlogging: boolean;
  @BeforeSave
  statisk async hash-passord (bruker: bruker) {
    la feil;
    if (bruker.byttet ('passord')) {
        la salt, hasj;
        [feil, salt] = vent på (bcrypt.genSalt (10));
        hvis (feil) {
          kaste feil;
        }

        [err, hash] = vent på (bcrypt.hash (bruker.passord, salt));
        hvis (feil) {
          kaste feil;
        }
        user.password = hash;
    }
  }

  async sammenligne Passord (pw) {
      la feil, passere;
      hvis (! dette.passordet) {
        kaste ny feil ('Har ikke passord');
      }

      [feil, pass] = venter på (bcrypt.compare (pw, this.password));
      hvis (feil) {
        kaste feil;
      }

      hvis (! pass) {
        kast 'Ugyldig passord';
      }

      returner dette;
  };

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

Det er mye kode akkurat der, så kommenter hvis du vil at jeg skal dele den ned.

Hvis du har noen forslag til forbedringer, gi meg beskjed! Hvis du vil at jeg skal lage en mal i vanlig javascript, gi meg beskjed! Hvis du har spørsmål, prøver jeg å svare samme dag, så vær ikke redd for å spørre!

Takk,

Brian Schardt