Δημιουργήστε ένα Graphql Api για κόμβο & MYSQL 2019-JWT

Εάν είστε εδώ, ίσως ήδη γνωρίζετε. Γνωρίζετε ότι το Graphql FREAKING είναι φοβερό, επιταχύνει την ανάπτυξη και είναι ίσως το καλύτερο που έχει συμβεί από τότε που ο Tesla κυκλοφόρησε το μοντέλο S.

Εδώ είναι ένα νέο πρότυπο που χρησιμοποιώ: https://medium.com/@brianschardt/best-graphql-apollo-sql-and-nestjs-template-458f9478b54e

Ωστόσο, τα περισσότερα μαθήματα που έχω διαβάσει δείχνουν πώς να δημιουργήσετε μια εφαρμογή graphql, αλλά να εισαγάγετε το κοινό πρόβλημα αιτήσεων n + 1. Ως αποτέλεσμα, η απόδοση είναι συνήθως εξαιρετικά κακή.

Πραγματικά είναι αυτό καλύτερο από ένα Tesla;

Ο στόχος μου σε αυτό το άρθρο δεν είναι να εξηγήσω τα βασικά στοιχεία του Graphql, αλλά να δείξω σε κάποιον πώς να χτίσει γρήγορα ένα API Graphql που δεν έχει το πρόβλημα n + 1.

Εάν θέλετε να μάθετε γιατί το 90% των νέων εφαρμογών θα πρέπει να χρησιμοποιήσετε το graphql api αντί για ξεκούραστο πατήστε εδώ.

Συμπλήρωμα βίντεο:

Αυτό το πρότυπο ΜΠΟΡΕΙ να χρησιμοποιηθεί για την παραγωγή καθώς περιέχει εύκολους τρόπους διαχείρισης των μεταβλητών περιβάλλοντος και έχει οργανωμένη δομή, ώστε ο κώδικας να μην ξεφορτωθεί. Για να διαχειριστούμε το πρόβλημα n + 1, χρησιμοποιούμε τη φόρτωση των δεδομένων, το facebook που απελευθερώνεται για να λύσει αυτό το ζήτημα.

Έλεγχος ταυτότητας: JWT

ORM: Συνεχίστε

Βάση δεδομένων: Mysql ή Postgres

Άλλα σημαντικά πακέτα που χρησιμοποιήθηκαν: express, apollo-server, graphql-sequelize, dataloader-sequelize

Σημείωση: Το typescript χρησιμοποιείται για την εφαρμογή. Είναι τόσο παρόμοιο με το javascript, αν δεν έχετε χρησιμοποιήσει ποτέ τη γραφή, δεν θα ανησυχούσα. Ωστόσο, αν υπάρχει αρκετή ζήτηση, θα γράψω μια κανονική έκδοση javascript. Σχολιάστε αν θέλετε.

Ξεκινώντας

Κλωνοποιήστε το repo και εγκαταστήστε τις μονάδες κόμβων

Εδώ είναι μια σύνδεση με το repo, προτείνω την κλωνοποίηση για να ακολουθήσει καλύτερα.

git clone git@github.com: brianschardt / node_graphql_apollo_template.git
cd node_graphql_apollo_template
npm install
// εγκαταστήστε τα πακέτα για την εκτέλεση της εφαρμογής
npm i -godemon

Ας αρχίσουμε με

Μετονομάστε example.env σε .env και αλλάξτε τα με τα σωστά διαπιστευτήρια για το περιβάλλον σας.

NODE_ENV = ανάπτυξη

PORT = 3001

DB_HOST = localhost
DB_PORT = 3306
DB_NAME = type
DB_USER = ρίζα
DB_PASSWORD = ρίζα
DB_DIALECT = mysql

JWT_ENCRYPTION = randomEncryptionKey
JWT_EXPIRATION = 1γ

Εκτελέστε τον κώδικα

Τώρα, εάν τρέχετε βάση δεδομένων και έχετε ενημερώσει σωστά το αρχείο .env με τις σωστές πληροφορίες, θα πρέπει να μπορέσουμε να εκτελέσουμε την εφαρμογή μας. Αυτό θα δημιουργήσει αυτόματα τους πίνακες με το καθορισμένο σχήμα στη βάση δεδομένων.

// χρήση για την ανάπτυξη, καθώς αυτό το ρολόι αλλάζει στον κώδικα.
npm run start: ρολόι
// χρήση για παραγωγή
npm έναρξη εκκίνησης

Τώρα πήρε το πρόγραμμα περιήγησής σας και πληκτρολογήστε: http: // localhost: 3001 / graphql

Θα πρέπει να δείτε τώρα την παιδική χαρά graphql που σας επιτρέπει να δείτε τεκμηρίωση σχετικά με τις μεταλλάξεις και τα ερωτήματα που υπάρχουν ήδη. Σας επιτρέπει επίσης να κάνετε ερωτήματα ενάντια στο API. Υπάρχουν μερικές από αυτές που έχουν ήδη γίνει, αλλά για να δοκιμάσετε πλήρως τη δύναμη αυτού του προτύπου API, ίσως θελήσετε να τοποθετήσετε με μη αυτόματο τρόπο τη βάση δεδομένων με πληροφορίες.

Βάση δεδομένων και σχήμα γραφήματος

Όπως βλέπετε από την εξέταση της γραφικής παράστασης στην παιδική χαρά graphql έχει μια αρκετά απλή δομή. Υπάρχουν μόνο 2 πίνακες, δηλ. Χρήστης και Εταιρεία. Ένας χρήστης μπορεί να ανήκει σε μία Εταιρεία και μια Εταιρεία μπορεί να έχει πολλούς χρήστες, δηλ. Έναν συσχετισμό μεταξύ πολλών.

Δημιουργία χρήστη

Παράδειγμα gql για να τρέξει στην παιδική χαρά για να δημιουργήσει ένα χρήστη. Αυτό θα επιστρέψει επίσης ένα JWT ώστε να μπορείτε να πιστοποιήσετε την ταυτότητά σας για μελλοντικά αιτήματα.

μετάλλαξη{
  createUser (δεδομένα: {firstName: "test", email: "test@test.com", κωδικός: "1"}) {
    ταυτότητα
    όνομα
    jwt
  }}
}}

Πιστοποιώ την αυθεντικότητα:

Τώρα που έχετε το JWT, μπορείτε να ελέγξετε τον έλεγχο ταυτότητας με την παιδική χαρά gql για να βεβαιωθείτε ότι όλα λειτουργούν σωστά. Στο αριστερό κάτω μέρος της ιστοσελίδας θα υπάρχει κείμενο που λέει HEADERS HTTP. Κάντε κλικ σε αυτό και πληκτρολογήστε το εξής:

Σημείωση: αντικαταστήστε με το διακριτικό σας.

{
  "Εξουσιοδότηση": "Φορέας eyJhbGciOiJ ..."
}}

Τώρα εκτελέστε αυτό το ερώτημα στην παιδική χαρά:

ερώτηση{
  getUser {
    ταυτότητα
    όνομα
  }}
}}

Αν όλα λειτουργούν το όνομά σας και το αναγνωριστικό χρήστη θα πρέπει να επιστραφούν.

Τώρα, αν διοχετεύσετε με μη αυτόματο τρόπο τη βάση δεδομένων σας, με όνομα εταιρείας και αναγνωριστικό και αντιστοιχίσετε αυτό το αναγνωριστικό στον χρήστη και εκτελέστε αυτό το ερώτημα. Η εταιρεία θα πρέπει να επιστραφεί.

ερώτηση{
  getUser {
    ταυτότητα
    όνομα
    Εταιρία{
      ταυτότητα
      όνομα
    }}
  }}
}}

Εντάξει τώρα που ξέρετε πώς να χρησιμοποιήσετε και να δοκιμάσετε αυτό το API σας επιτρέπει να πάρετε τον κώδικα!

Κωδικοποίηση κατάδυσης

Κύριο αρχείο - apps.ts

Υποστήριξη φορτίων - φορτώνει μοντέλα db και μεταβλητές env.

εισαγωγή * όπως εκφράζεται από το 'express';
εισαγωγή * ως jwt από το 'express-jwt';
εισαγωγή {ApolloServer} από το "apollo-server-express".
εισαγωγή {sequelize} από './models'.
εισαγωγή {ENV} από το "./config".

εισαγωγή {resolver ως resolvers, schema, schemaDirectives} από './graphql'.
εισαγάγετε {createContext, EXPECTED_OPTIONS_KEY} από το "dataaloader-sequelize".
εισαγάγετε από "περιμένετε-σε-js"?

const app = express ();

Εγκαταστήστε το μεσαίο λογισμικό και το διακομιστή Apollo!

Σημείωση: Το "createContext (συνέχιση)" είναι αυτό που ξεφορτώνεται το πρόβλημα n + 1. Αυτό γίνεται όλα στο παρασκήνιο συνεχίζοντας τώρα. ΜΑΓΕΙΑ!! Αυτό χρησιμοποιεί το πακέτο dataloader του facebook.

const authMiddleware = jwt ({
    μυστικό: ENV.JWT_ENCRYPTION,
    Πιστοποιητικά απαιτούμενα: ψευδή,
});
app.use (authMiddleware);
app.use (λειτουργία (err, req, res, επόμενο) {
    const errorObject = {σφάλμα: true, μήνυμα: `$ {err.name}:
$ {err.message}}}.
    αν (err.name === 'Μη εξουσιοδοτημένοError') {
        επιστροφή res.status (401) .json (errorObject);
    } else {
        επιστροφή res.status (400) .json (errorObject);
    }}
});
const server = νέο ApolloServer ({
    typeDefs: σχήμα,
    resolvers,
    schemaDirectives,
    παιδική χαρά: αληθινή,
    πλαίσιο: ({req}) => {
        ΕΠΙΣΤΡΟΦΗ {
            [EXPECTED_OPTIONS_KEY]: createContext (συνέχιση),
            χρήστης: req.user,
        }}
    }}
});
server.applyMiddleware ({app});

Ακούστε για αιτήματα

app.listen ({port: ENV.PORT}, async () => {
    console.log (` Server έτοιμος στο http: // localhost: $ {ENV.PORT} $ {server.graphqlPath} ');
    ας σκεφτούμε.
    [err] = περιμένετε να (sequelize.sync (
        // {force: true},
    )).

    αν (err) {
        console.error ('Σφάλμα: Δεν μπορώ να συνδεθώ στη βάση δεδομένων').
    } else {
        console.log ('Συνδεδεμένη στη βάση δεδομένων').
    }}
});

Μεταβλητές διαμόρφωσης - config / env.config.ts

Χρησιμοποιούμε το dotenv για να φορτώσουμε τις μεταβλητές μας .env στην εφαρμογή μας.

εισαγωγή * ως dotEnv από 'dotenv';
dotEnv.config ();

εξαγωγή const ENV = {
    ΛΙΜΑΝΙ: 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 || 'ρίζα',
    DB_PASSWORD: process.env.DB_PASSWORD || 'ρίζα',
    DB_DIALECT: process.env.DB_DIALECT || 'mysql',

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

Graphql χρόνο !!!

Ας ρίξουμε μια ματιά σε αυτούς τους διαχωριστές!

graphql / index.ts

Εδώ χρησιμοποιούμε την κόλλα σχήματος συσκευασίας. Αυτό βοηθά στη διάλυση του σχήματος, των ερωτημάτων και των μεταλλάξεων σε ξεχωριστά τμήματα για τη διατήρηση καθαρού και οργανωμένου κώδικα. Αυτό το πακέτο αναζητά αυτόματα τον κατάλογο που καθορίζουμε για 2 αρχεία, δηλ. Schema.graphql και resolver.ts. Στη συνέχεια τα αρπάζει και τα κολλάει μαζί. Εξ ου και η κόλλα σχήματος ονομάτων.

Οδηγίες: Για τις οδηγίες μας δημιουργούμε έναν κατάλογο για αυτούς και τους συμπεριλαμβάνουμε μέσω ενός αρχείου index.ts.

εισαγωγή * ως κόλλα από το «schemaglue».
εξαγωγή {schemaDirectives} από './directives'.
εξαγωγή const {σχήμα, resolver} = κόλλα ('src / graphql', {mode: 'ts'});

Κάνουμε καταλόγους για κάθε μοντέλο που έχουμε για συνέπεια. Έτσι, έχουμε τον κατάλογο χρηστών και εταιρειών.

graphql / χρήστη

Παρατηρήσαμε ότι το αρχείο ανάλυσης, ακόμη και όταν χρησιμοποιείτε κόλλα σχήματος, μπορεί ακόμα να πάρει πολύ μεγάλο μέγεθος. Έτσι, αποφασίσαμε να το διαλύσουμε περαιτέρω αν βασίζεται σε ερώτημα, μετάλλαξη ή χάρτη για έναν τύπο. Έτσι, έχουμε άλλα 3 αρχεία.

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

Σημείωση: Αν θέλετε να προσθέσετε συνδρομές gql, θα δημιουργήσετε ένα άλλο αρχείο που ονομάζεται user.subscription.ts και θα το συμπεριλάβετε στο αρχείο resolver.

graphql / user / resolver.ts

Αυτό το αρχείο είναι αρκετά απλό και οι διακομιστές για να οργανώσετε τα άλλα αρχεία σε αυτόν τον κατάλογο.

εισαγωγή {Query} από το αρχείο ./user.query ';
εισαγωγή {UserMap} από "./user.map".
εισαγωγή {Μεταλλαγή} από "./user.mutation".

εξαγωγή const resolver = {
  Ερώτημα: Ερώτημα,
  Χρήστης: UserMap,
  Μεταλλάξεις: Μεταλλάξεις
},

graphql / user / schema.graphql

Αυτό το αρχείο ορίζει το γράφημα graphql μας, και το resolvers! Εξαιρετικά σημαντικό!

πληκτρολογήστε Χρήστη {
  id: Int
  διεύθυνση ηλεκτρονικού ταχυδρομείου: συμβολοσειρά
  firstName: String
  lastName: String
  εταιρεία: Εταιρεία
  jwt: Σειρά @isAuthUser
}}

inputInput εισόδου {
    διεύθυνση ηλεκτρονικού ταχυδρομείου: συμβολοσειρά
    κωδικός: String
    firstName: String
    lastName: String
}}

type Query {
   getUser: Χρήστης @ isAuth
   loginUser (διεύθυνση ηλεκτρονικού ταχυδρομείου: String !, κωδικός: String!): Χρήστης
}}

τύπου Mutation {
   createUser (δεδομένα: UserInput): Χρήστης
}}

graphql / user / user.query.ts

Αυτό το αρχείο περιέχει τη λειτουργικότητα για όλα τα ερωτήματα και τις μεταλλάξεις των χρηστών μας. Χρησιμοποιεί τη μαγεία από την αλληλεπίδραση με το graphql για να χειριστεί πολλά πράγματα στο graphql. Εάν έχετε χρησιμοποιήσει άλλα πακέτα graphql ή έχετε δοκιμάσει να δημιουργήσετε το δικό σας graphql api, θα αναγνωρίσετε πόσο σημαντικό είναι και το χρόνο εξοικονόμησης αυτού του πακέτου. Παρόλα αυτά, σας παρέχει όλη την προσαρμογή που θα χρειαστείτε ποτέ! Ακολουθεί ένας σύνδεσμος με την τεκμηρίωση σχετικά με το πακέτο.

εισαγωγή {resolver} από το 'graphql-sequelize'.
εισαγωγή {Χρήστης από "../../models";
εισαγάγετε από "περιμένετε-σε-js"?

export const Query = {
    getUser: resolver (χρήστης, {
        πριν: async (findOptions, {}, {user}) => {
            επιστροφή findOptions.where = {id: user.id};
        },
        μετά από: (χρήστης) => {
            Επιστροφή χρηστών?
        }}
    }),
    loginUser: resolver (Χρήστης, {
        πριν: async (findOptions, {email}) => {
            findOptions.where = {email};
        },
        μετά: async (χρήστης, {password}) => {
            ας σκεφτούμε.
            [err, user] = περιμένει (user.comparePassword (κωδικός πρόσβασης));
            αν (err) {
              console.log (λάθος);
              ρίξτε νέο σφάλμα (err);
            }}

            user.login = true; // για να ενημερώσετε την οδηγία ότι ο χρήστης αυτός έχει πιστοποιηθεί χωρίς μια επικεφαλίδα εξουσιοδότησης
            Επιστροφή χρηστών?
        }}
    }),
},

graphql / user / user.mutation.ts

Αυτό το αρχείο περιέχει όλη τη μετάλλαξη για το τμήμα χρήστη της εφαρμογής μας.

εισαγωγή {resolver as rs} από το 'graphql-sequelize'.
εισαγωγή {Χρήστης από "../../models";
εισαγάγετε από "περιμένετε-σε-js"?

export const Mutation = {
    createUser: rs (Χρήστης, {
      πριν: async (findOptions, {data}) => {
        ας σφάλμα, χρήστης?
        [err, user] = περιμένει (User.create (δεδομένα))?
        αν (err) {
          ρίξτε λάθος?
        }}
        findOptions.where = {id: user.id};
        επιστροφή findOptions;
      },
      μετά από: (χρήστης) => {
        user.login = true;
        Επιστροφή χρηστών?
      }}
    }),
},

graphql / user / user.map.ts

Αυτό είναι αυτό που οι άνθρωποι πάντα παραβλέπουν και κάνουν την κωδικοποίηση και την ερώτηση στο graphql τόσο δύσκολη και έχει κακή απόδοση. Ωστόσο, όλα τα πακέτα που έχουμε συμπεριλάβει επιλύει το ζήτημα. Η αντιστοίχιση των τύπων μεταξύ τους είναι αυτό που δίνει τη δύναμη και τη δύναμή του, αλλά οι άνθρωποι το κωδικοποιούν με τέτοιο τρόπο ώστε να μετατρέπεται αυτή η δύναμη σε αδυναμία. Ωστόσο, όλα τα πακέτα που έχουμε χρησιμοποιήσει απαλλαγούν από αυτό με έναν εύκολο τρόπο.

εισαγωγή {resolver} από το 'graphql-sequelize'.
εισαγωγή {Χρήστης από "../../models";
εισαγάγετε από "περιμένετε-σε-js"?

export const UserMap = {
    εταιρεία: resolver (User.associations.company),
    jwt: (χρήστης) => user.getJwt (),
},

Ναι, αυτό είναι τόσο απλό!

Σημείωση: οι οδηγίες graphql στο σχήμα χρήστη είναι αυτό που προστατεύει ορισμένα πεδία όπως το πεδίο JWT στον χρήστη και το ερώτημα getUser.

Μοντέλα - μοντέλα / index.ts

Χρησιμοποιούμε τη συνέχιση της πληκτρολόγησης ώστε να μπορέσουμε να ορίσουμε μεταβλητές σε αυτόν τον τύπο κλάσης. Σε αυτό το αρχείο ξεκινάμε φορτώντας τα πακέτα. Στη συνέχεια κάνουμε instantiate sequelize και συνδέστε το με το db μας. Στη συνέχεια εξάγουμε τα μοντέλα.

εισαγωγή {Sequelize} από το "sequelize-typcript".
εισαγωγή {ENV} από το αρχείο "../config/env.config".

εξαγωγή const sequelize = new Sequelize ({
        βάση δεδομένων: ENV.DB_NAME,
        διάλεκτο: ENV.DB_DIALECT,
        username: ENV.DB_USER,
        κωδικός πρόσβασης: ENV.DB_PASSWORD,
        operatorsAliases: false,
        καταγραφή: ψευδής,
        αποθήκευση: ': μνήμη:',
        μοντέλοΔιαδρομές: [__dirname + '/*.model.ts'],
        modelMatch: (όνομα αρχείου, μέλος) => {
           επιστροφή αρχείουnamename.substring (0, filename.indexOf ('. model')) === member.toLowerCase ();
        },
});
εξαγωγή {χρήστης} από "./user.model";
εξαγωγή {Εταιρεία} από το "./company.model";

Τα μοντέλαPaths και modelMatch είναι πρόσθετες επιλογές που λένε ότι ακολουθούν-τύπος όπου τα μοντέλα μας είναι και ποιες είναι οι ονομασίες conventions τους.

Εταιρικό μοντέλο - μοντέλα / company.model.ts

Εδώ ορίζουμε το σχήμα της εταιρείας χρησιμοποιώντας τη συνέχιση της γραφής.

Εισαγωγή {Πίνακας, Στήλη, Μοντέλο, HasMany, PrimaryKey, AutoIncrement} από το "sequelize-typcript".
εισαγωγή {User} από το "./user.model"
@Table ({timestamps: true})
η εταιρεία κλάσης εξαγωγής επεκτείνει το μοντέλο  {

  @Column ({primaryKey: true, autoIncrement: true})
  αριθμό ταυτότητας;

  @Στήλη
  όνομα: συμβολοσειρά;

  @HasMany (() => Χρήστης)
  χρήστες: χρήστης [];
}}

Μοντέλο χρήστη - μοντέλα / user.model.ts

Εδώ καθορίζουμε το μοντέλο χρήστη. Θα προσθέσουμε επίσης κάποια προσαρμοσμένη λειτουργικότητα για έλεγχο ταυτότητας.

Εισαγωγή {Πίνακας, Στήλη, Μοντέλο, HasMany, PrimaryKey, AutoIncrement, BelongsTo, ForeignKey, BeforeSave} από το "sequelize-typcript".
εισαγωγή {Εταιρεία} από "./company.model";
εισαγωγή * ως κρυπτογράφηση από το 'bcrypt'.
εισαγάγετε από "περιμένετε-σε-js"?
εισαγωγή * ως jsonwebtoken από'jsonwebtoken ';
εισαγωγή {ENV} από το "../config".

@Table ({timestamps: true})
ο χρήστης κλάσης εξαγωγής επεκτείνει το μοντέλο  {
  @Column ({primaryKey: true, autoIncrement: true})
  αριθμό ταυτότητας;

  @Στήλη
  firstName: string;

  @Στήλη
  lastName: string;

  @Στήλη
  email: string;

  @Στήλη
  κωδικός πρόσβασης: συμβολοσειρά;

  @ForeignKey (() => Εταιρεία)
  @Στήλη
  companyId: αριθμός;

  @BelongsTo (() => Εταιρεία)
  εταιρεία: Εταιρεία;
  jwt: string;
  login: boolean;
  @BeforeSave
  static async hashPassword (χρήστης: χρήστης) {
    ας σκεφτούμε.
    αν (user.changed ('password')) {
        αφήστε το αλάτι, το hash;
        [err, αλάτι] = περιμένετε στο (bcrypt.genSalt (10));
        αν (err) {
          ρίξτε λάθος?
        }}

        [err, hash] = περιμένει στο (bcrypt.hash (user.password, αλάτι));
        αν (err) {
          ρίξτε λάθος?
        }}
        user.password = hash;
    }}
  }}

  async comparePassword (pw) {
      ας πάει λάθος, περάσει?
      αν (! this.password) {
        ρίξτε νέο Σφάλμα ('Δεν έχει κωδικό');
      }}

      [err, pass] = περιμένει στο (bcrypt.compare (pw, this.password)).
      αν (err) {
        ρίξτε λάθος?
      }}

      αν (! pass) {
        ρίξτε "Μη έγκυρος κωδικός πρόσβασης".
      }}

      επιστρέψτε αυτό?
  },

  getJwt () {
      επιστροφή 'Bearer' + jsonwebtoken.sign ({
          id: this.id,
      }, ENV.JWT_ENCRYPTION, {expiresIn: ENV.JWT_EXPIRATION}).
  }}
}}

Αυτός είναι πολύς κώδικας ακριβώς εκεί, οπότε σχολιάστε εάν θέλετε να το σπάσω.

Εάν έχετε οποιεσδήποτε προτάσεις για βελτιώσεις παρακαλώ ενημερώστε μας! Αν θέλετε να κάνω ένα πρότυπο σε τακτικές javascript επίσης ενημερώστε με! Επίσης, Αν έχετε οποιεσδήποτε ερωτήσεις, θα προσπαθήσω να απαντήσω την ίδια μέρα, γι 'αυτό παρακαλώ μην φοβάστε να ρωτήσετε!

Ευχαριστώ,

Brian Schardt