Byg en Graphql Api til Node & MYSQL 2019— JWT

Hvis du er her, ved du sandsynligvis allerede. Du ved, at Graphql FREAKING awesome, fremskynder udviklingen og er sandsynligvis den bedste ting, der er sket siden Tesla frigav modellen S.

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

De fleste tutorials, jeg har læst, viser imidlertid, hvordan man bygger en grafql-app, men introducerer det fælles problem med n + 1-anmodninger. Som et resultat er ydelsen normalt super dårlig.

Er det virkelig bedre end en Tesla?

Mit mål i denne artikel er ikke at forklare det grundlæggende i Graphql, men at vise nogen, hvordan man hurtigt opbygger et Graphql API, der ikke har n + 1-problemet.

Hvis du vil vide, hvorfor 90% af de nye applikationer skal bruge graphql api i stedet for afslappende klik her.

Video Supplement:

Denne skabelon MÅ bruges til produktion, da den indeholder lette måder at administrere miljøvariabler og har en organiseret struktur, så kode ikke kommer ud af hånden. For at styre problemet med n + 1 bruger vi datalæsning, det, som facebook er frigivet for at løse dette problem.

Godkendelse: JWT

ORM: Sequelize

Database: Mysql eller Postgres

Andre vigtige anvendte pakker: express, apollo-server, grafql-sequelize, dataloader-sequelize

Bemærk: Typescript bruges til appen. Det ligner javascript, hvis du aldrig har brugt typeskript ville jeg ikke bekymre mig. Men hvis der er nok efterspørgsel, skriver jeg en regelmæssig javascript-version. Kommenter, hvis du gerne vil have det.

Kom godt i gang

Klon repo og installer node moduler

Her er et link til repoen, jeg anbefaler at klone det for bedst at følge med.

git klon git@github.com: brianschardt / node_graphql_apollo_template.git
cd node_graphql_apollo_template
npm installation
// installere globale pakker for at køre applikationen
npm i -g nodemon

Lad os starte med .env

Omdøb eksempel.env til .env og ændre det til de korrekte legitimationsoplysninger for dit miljø.

NODE_ENV = udvikling

PORT = 3001

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

JWT_ENCRYPTION = randomEncryptionKey
JWT_EXPIRATION = 1y

Kør koden

Hvis din database nu kører, og du har opdateret din .env-fil korrekt med de korrekte oplysninger, skal vi være i stand til at køre vores app. Dette skaber tabellerne med det definerede skema automatisk i databasen.

// bruges til udvikling, da dette ser ændringer i koden.
npm løb start: se
// brug til produktion
npm kørestart

Gå nu til din browser, og indtast: http: // localhost: 3001 / graphql

Du skal nu se grafql legeplads, som giver dig mulighed for at se dokumentation om, hvilke mutationer og forespørgsler der allerede findes. Det giver dig også mulighed for at stille spørgsmål til API'et. Der er allerede et par af dem, men for fuldt ud at teste kraften i denne skabelon-API kan du muligvis manuelt frø databasen med information.

Database og Graphql-skema

Som du ser, når man ser på skemaet på grafik legeplads, har det en temmelig enkel struktur. Der er kun 2 tabeller, dvs. bruger og firma. En bruger kan tilhøre et selskab, og et firma kan have mange brugere, dvs. en en til mange forening.

Opret en bruger

Eksempel gql til at køre i legepladsen for at oprette en bruger. Dette returnerer også en JWT, så du kan autentificere for fremtidige anmodninger.

mutation {
  createUser (data: {firstName: "test", e-mail: "test@test.com", adgangskode: "1"}) {
    id
    fornavn
    JWT
  }
}

Godkend:

Nu hvor du har JWT, lader vi testgodkendelse med gql-legeplads for at sikre, at alt fungerer korrekt. På venstre side af websiden vil der være tekst, der siger HTTP-HEADERS. Klik på det og indtast dette:

Bemærk: udskift med dit token.

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

Kør nu denne forespørgsel på legepladsen:

forespørgsel{
  getUser {
    id
    fornavn
  }
}

Hvis alt fungerede, skal dit navn og bruger-id returneres.

Hvis du nu sætter din database manuelt med et firmanavn og id og tildeler denne id til din bruger og kører denne forespørgsel. Virksomheden skal returneres.

forespørgsel{
  getUser {
    id
    fornavn
    Selskab{
      id
      navn
    }
  }
}

Ok nu, hvor du ved, hvordan du bruger og tester dette API, kan vi komme ind i koden!

Kodedykke

Hovedfil - app.ts

Indlæs afhængigheder - indlæser db-modeller og env-variabler.

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

import {resolver som resolver, skema, schemaDirectives} fra './graphql';
import {createContext, EXPECTED_OPTIONS_KEY} fra 'dataloader-sequelize';
import til fra 'afvente til js';

const app = express ();

Opsæt middleware og Apollo Server!

Bemærk: “createContext (sequelize)” er det, der slipper for n + 1-problemet. Dette gøres alt sammen i baggrunden ved at gøre dette nu. MAGI!! Dette bruger facebook dataloader-pakken.

const autorMiddleware = jwt ({
    hemmelighed: ENV.JWT_ENCRYPTION,
    legitimationsoplysninger Krævet: falsk,
});
app.use (authMiddleware);
app.use (funktion (fejle, req, res, næste) {
    const errorObject = {error: true, meddelelse: `$ {err.name}:
$ {Err.message} `};
    if (err.name === 'UnauthorizedError') {
        return res.status (401) .json (errorObject);
    } andet {
        return res.status (400) .json (errorObject);
    }
});
const server = ny ApolloServer ({
    typeDefs: schema,
    resolvere,
    schemaDirectives,
    legeplads: sandt,
    kontekst: ({req}) => {
        Vend tilbage {
            [EXPECTED_OPTIONS_KEY]: createContext (sequelize),
            bruger: req.user,
        }
    }
});
server.applyMiddleware ({app});

Lyt efter anmodninger

app.listen ({port: ENV.PORT}, async () => {
    console.log (` Server klar på http: // localhost: $ {ENV.PORT} $ {server.graphqlPath}`);
    lad fejle;
    [err] = vente på (sequelize.sync (
        // {kraft: sand},
    ));

    hvis (err) {
        console.error ('Fejl: Kan ikke oprette forbindelse til databasen');
    } andet {
        console.log ('Tilsluttet til database');
    }
});

Konfigurationsvariabler - config / env.config.ts

Vi bruger dotenv til at indlæse vores .env-variabler til vores app.

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

eksport 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 || 'rod',
    DB_PASSWORD: process.env.DB_PASSWORD || 'rod',
    DB_DIALECT: process.env.DB_DIALECT || 'Mysql',

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

Graphql tid !!!

Lad os se på disse beslutningstagere!

graphql / index.ts

Her bruger vi pakningsskema lim. Dette hjælper med at opdele vores skemaer, forespørgsler og mutationer i separate dele for at opretholde ren og organiseret kode. Denne pakke søger automatisk i det bibliotek, vi specificerer for 2 filer, dvs. schema.graphql og resolver.ts. Det griber dem derefter og limer dem sammen. Derfor navnet skema lim.

Direktiver: til vores direktiver opretter vi en mappe til 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 opretter mapper til hver model, vi har for konsistens. Således har vi en bruger- og virksomhedsmappe.

graphql / bruger

Vi har bemærket, at resolver-filen, selv når du bruger skemalim, stadig kan blive meget stor. Så vi besluttede at opdele det yderligere baseret på, om det er en forespørgsel, mutation eller kort for en type. Vi har således 3 flere filer.

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

Bemærk: Hvis du vil tilføje gql-abonnementer, opretter du en anden fil, der hedder: user.subscription.ts og inkluderer den i resolver-filen.

graphql / bruger / resolver.ts

Denne fil er temmelig enkel og servere til at organisere de andre filer i dette bibliotek.

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

eksport const resolver = {
  Forespørgsel: Forespørgsel,
  Bruger: UserMap,
  Mutation: Mutation
};

graphql / bruger / schema.graphql

Denne fil definerer vores grafql-skema og opløsere! Super vigtigt!

type bruger {
  id: Int
  e-mail: streng
  firstName: String
  sidste navn: String
  virksomhed: Virksomhed
  jwt: String @isAuthUser
}

input UserInput {
    e-mail: streng
    adgangskode: String
    firstName: String
    sidste navn: String
}

type forespørgsel {
   getUser: Bruger @isAuth
   loginUser (e-mail: String !, password: String!): Bruger
}

type mutation {
   createUser (data: UserInput): Bruger
}

graphql / bruger / user.query.ts

Denne fil indeholder funktionaliteten til alle vores brugerforespørgsler og mutationer. Bruger magien fra grafql-sequelize til at håndtere en masse af grafikl-tingene. Hvis du har brugt andre graphql-pakker eller prøvet at oprette din egen graphql api, vil du genkende, hvor vigtig og tidsbesparende denne pakke er. Alligevel giver det dig all den tilpasning, du nogensinde har brug for! Her er et link til dokumentation om denne pakke.

import {resolver} fra 'graphql-sequelize';
import {Bruger} fra '../../modeller';
import til fra 'afvente til js';

eksport const Query = {
    getUser: resolver (Bruger, {
        før: async (findOptions, {}, {user}) => {
            return findOptions.where = {id: user.id};
        },
        efter: (bruger) => {
            returnerende bruger;
        }
    }),
    loginUser: resolver (Bruger, {
        før: async (findOptions, {email}) => {
            findOptions.where = {email};
        },
        efter: async (bruger, {password}) => {
            lad fejle;
            [fejle, bruger] = vente på (user.comparePassword (adgangskode));
            hvis (fejle) {
              console.log (err);
              kaste ny fejl (fejl);
            }

            user.login = true; // for at lade direktivet vide, at denne bruger er godkendt uden en autorisationshoved
            returnerende bruger;
        }
    }),
};

graphql / bruger / user.mutation.ts

Denne fil indeholder al mutationen for brugersektionen af ​​vores app.

import {resolver som rs} fra 'graphql-sequelize';
import {Bruger} fra '../../modeller';
import til fra 'afvente til js';

eksport konst mutation = {
    createUser: rs (Bruger, {
      før: async (findOptions, {data}) => {
        lad fejl, bruger;
        [fejle, bruger] = vente på (User.create (data));
        hvis (fejle) {
          kaster fejl;
        }
        findOptions.where = {id: user.id};
        return findOptions;
      },
      efter: (bruger) => {
        user.login = sandt;
        returnerende bruger;
      }
    }),
};

graphql / bruger / user.map.ts

Dette er den, folk altid overser, og som gør kodning og forespørgsel i grafql så vanskelig og har dårlige resultater. Alle de pakker, vi har inkluderet, løser imidlertid problemet. At kortlægge typer til hinanden er det, der giver graphql sin styrke og styrke, men folk koder det på en sådan måde, at denne styrke bliver til en svaghed. Imidlertid slipper alle de pakker, vi har brugt, det på en nem måde.

import {resolver} fra 'graphql-sequelize';
import {Bruger} fra '../../modeller';
import til fra 'afvente til js';

eksport konst UserMap = {
    firma: resolver (User.associations.company),
    jwt: (bruger) => user.getJwt (),
};

Ja, det er det så enkelt !!!

Bemærk: Graphql-direktiverne i brugersskemaet er det, der beskytter bestemte felter som JWT-feltet på brugeren og getUser-forespørgslen.

Modeller - modeller / indeks.ts

Vi bruger sequelize-typen, så vi kan indstille variabler til denne klassetype. I denne fil starter vi med at indlæse pakkerne. Derefter initialiserer vi sequelize og forbinder det til vores db. Derefter eksporterer vi modellerne.

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

export const sequelize = new Sequelize ({
        database: ENV.DB_NAME,
        dialekt: ENV.DB_DIALECT,
        brugernavn: ENV.DB_USER,
        adgangskode: ENV.DB_PASSWORD,
        operatorer Aliaser: falsk,
        logning: falsk,
        opbevaring: ': hukommelse:',
        modelPaths: [__dirname + '/*.model.ts'],
        modelMatch: (filnavn, medlem) => {
           return filename.substring (0, filename.indexOf ('. model')) === member.toLowerCase ();
        },
});
eksport {Bruger} fra './user.model';
eksport {Company} fra './company.model';

ModelPaths og modelMatch er ekstra muligheder, der fortæller sequelize-typescript, hvor vores modeller er, og hvad deres navnekonventioner er.

Virksomhedsmodel - modeller / company.model.ts

Her definerer vi virksomhedsskemaet ved hjælp af sequelize-typeskript.

import {Tabel, kolonne, model, HasMany, PrimaryKey, AutoIncrement} fra 'sequelize-typescript';
import {Bruger} fra './user.model'
@Table ({tidsstempler: sandt})
eksportklasse Virksomhed udvider Model  {

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

  @Kolonne
  navn: streng;

  @HasMany (() => Bruger)
  brugere: Bruger [];
}

Brugermodel - modeller / user.model.ts

Her definerer vi brugermodellen. Vi vil også tilføje nogle brugerdefinerede funktioner til godkendelse.

import {Tabel, kolonne, model, HasMany, PrimaryKey, AutoIncrement, BelongsTo, ForeignKey, BeforeSave} fra 'sequelize-typescript';
import {Company} fra "./company.model";
import * som bcrypt fra 'bcrypt';
import til fra 'afvente til js';
import * som jsonwebtoken fra 'jsonwebtoken';
import {ENV} fra '../config';

@Table ({tidsstempler: sandt})
Eksportklasse Bruger udvider Model  {
  @Column ({primaryKey: true, autoIncrement: true})
  ID-nummer;

  @Kolonne
  firstName: string;

  @Kolonne
  efternavn: streng;

  @Kolonne
  e-mail: streng;

  @Kolonne
  adgangskode: streng;

  @ForeignKey (() => Virksomhed)
  @Kolonne
  firmaId: antal;

  @BelongsTo (() => Virksomhed)
  virksomhed: Virksomhed;
  jwt: streng;
  login: boolsk;
  @BeforeSave
  statisk async hashPassword (bruger: Bruger) {
    lad fejle;
    if (user.chang ('adgangskode')) {
        lad salt, hash;
        [fejlagtigt, salt] = venter på (bcrypt.genSalt (10));
        hvis (fejle) {
          kaster fejl;
        }

        [err, hash] = venter på (bcrypt.hash (user.password, salt));
        hvis (fejle) {
          kaster fejl;
        }
        user.password = hash;
    }
  }

  async sammenligne Passord (pw) {
      lad fejle, passere;
      hvis (! dette.passord) {
        kaste ny fejl ('Har ikke adgangskode');
      }

      [fejle, bestå] = vente på (bcrypt.compare (pw, this.password));
      hvis (fejle) {
        kaster fejl;
      }

      hvis (! bestå) {
        smid 'Ugyldigt kodeord';
      }

      returner dette;
  };

  getJwt () {
      returner 'Bearer' + jsonwebtoken.sign ({
          id: this.id,
      }, ENV.JWT_ENCRYPTION, {udløber i: ENV.JWT_EXPIRATION});
  }
}

Det er en masse kode lige der, så kommenter hvis du vil have mig til at nedbryde den.

Hvis du har nogle forslag til forbedringer, så fortæl mig det! Hvis du vil have mig til at lave en skabelon i javascript, fortæl mig det også! Hvis du har spørgsmål, prøver jeg også at svare samme dag, så vær ikke bange for at stille!

Tak,

Brian Schardt