Use Apollo-Server to help you quickly develop GraphQL.

Technology stack

The directory structure

.├ ─ config # Config file │ ├─ config.json # Automatically generated, get active on consul, get active on consul. ├─ ├─ exercises - └. Json │ ├─ project.ts # Incorporating with consul in private configuration ├ ─ ─ the db # about db │ ├ ─ ─ index. The ts # about database configuration and log (sequelize) │ ├ ─ ─ migrations / # about database migration script │ └ ─ ─ models / ├─ Lib # ├─ Lib # └ ─ lib # ├── ts # ├─ Error │ ├─ Heavy Metal Flag School - Heavy Metal Flag School - Heavy metal Flag School - Heavy metal Flag School - Heavy metal Flag School - Heavy metal Flag School - Heavy metal Flag School - Heavy metal Flag ├─ exercises # ├─ logs #, Automatically generate │ ├ ─ ─ API. The log # graphql log │ ├ ─ ─ common. Log # general log │ ├ ─ ─ the log # about database ` SQL ` log │ └ ─ ─ redis. Log # about redis │ ├─ Middlewares # KOA Middleware │ ├─ User │ ├─ Context. Ts # requestId ├─ exercises # ├─ exercises # ├─ exercises # ├─ exercises # SRC # series of │ ├─ ├─ SRC # Series of │ ├─ │ ├─ SRC # Series of │ ├── │ ├─ SRC # Series of │ ├── │ ├── Resolvers / # graphql resolvers (Mutation & Query) │ ├ ─ ─ scalars / # graphql scalars │ └ ─ ─ utils. The auxiliary function of ts # graphql ├ ─ ─ Yml # Anti-Flag for docker-composing # Anti-Flag for docker-composing # Anti-Flag for package-lock.json # Anti-Flag for docker-composing # Anti-Flag for package-lock.json # Anti-Flag for package-lock.json # Anti-Flag for package-lock.json # Anti-Flag for package-lock.json # Anti-Flag for package-lock.json - - ├─ ├─ ├─ download.txt # Download.txt # Download.txt # Download.txt # Download.txt # Download.txt # Download.txt # Download.txt # Download.txt # Download.txt # Download.txt # Download.txtCopy the code

Preparation conditions

  • consulYou need it to manage configuration, if not, according totype.tsIn theAppConfigAdd the config. Json
  • sentryYou need it to report an exception. If not, comment out the code…
  • postgresYou need to specify database to generate the table
  • redis

start

Git clone [email protected]: shfshanyue/Apollo - server - the starter. Git CD Apollo - server - the starter NPM run config # pull the configuration on the consul, If not, use config.json instead of NPM run Migrate #Copy the code

Operations about databases

NPM run migrate:new NPM run migrate:undo NPM run migrate:new NPM run migrate:new NPM run migrate:new NPM run migrate:undoCopy the code

Development of guidelines

Single file managementtypeDefresolver

As follows, ObjectType and its corresponding Query in Query and Mutation are defined in a single file. And the typeDef and resolver are centrally managed.

// src/resolvers/Todo.ts const typeDef = gql` type Todo @sql { id: ID! } extend type Query { todos: [Todo!]  } extend type Mutation { createTodo: TODO! } ` const resolver: IResolverObject<any, AppContext> = { Todo: { user () {} }, Query: { todos () {} }, Mutation: { createTodo () {} } }Copy the code

Fetch database fields on demand

You can query on demand using @findOption and inject it into the info.Attributes field in the resolver function

type Query { users: [User!]  @findOption } query USERS { users { id name } }Copy the code
function users ({}, {}, { models }, { attributes }: any) {
  return models.User.findAll({
    attributes
  })
}Copy the code

The database layer solves the N+1 query problem

Use dataloader-Sequelize to solve batch problems of database queries

Select id, name from users where id = 1 select id, name from users where id = 2 select id, Select id, name from users where id in (1, 2, 3) select name from users where id in (1, 2, 3)Copy the code

Solve N+1 query problems with DataLoader

Use ID/Hash instead of Query

TODO requires client cooperation

When Query becomes larger, the volume of HTTP requests becomes larger, which seriously affects application performance. In this case, you can map Query to hash.

When the request volume becomes small, you can use GET requests instead to facilitate caching.

I noticed that the nuggets GraphQL Query has been replaced by ID

useconsulConfiguration management

Project represents the key corresponding to this project in Consul. The project will pull the configuration corresponding to the key and perform the object. assign operation with the local config/project.ts. Dependencies represents the configurations that the project depends on, such as database, cache, and user service configurations. The project pulls the dependencies from Consul.

The final configuration generated by the project is the AppConfig identity.

// config/consul.ts
export const project = 'todo'
export const dependencies = ['redis', 'pg']Copy the code

User authentication

The @auth command indicates that the resource is restricted and requires user login. Roles indicates that only a specific role can access a restricted resource

Directive @auth(# USER, ADMIN can customize roles: [String]) on FIELD_DEFINITION type Query {authInfo: Int @auth}Copy the code

Here’s the code

// src/directives/auth.ts function visitFieldDefinition (field: GraphQLField<any, AppContext>) { const { resolve = defaultFieldResolver } = field const { roles } = this.args // const roles: UserRole[] = ['USER', 'ADMIN'] field.resolve = async (root, args, ctx, info) => { if (! ctx.user) { throw new AuthenticationError('Unauthorized') } if (roles && ! roles.includes(ctx.user.role)) { throw new ForbiddenError('Forbidden') } return resolve.call(this, root, args, ctx, info) } }Copy the code

JWT and whitelist

JWT with Token update

When the user is authenticated successfully, the validity period of his token is checked. If half of the validity period is left, a new token is generated and assigned to the response header.

paging

Add page and pageSize parameters to the list for pagination

type User { id: ID! todos ( page: Int = 1 pageSize: Int = 10 ): [Todo!]  @findOption } query TODOS { todos (page: 1, pageSize: 10) { id name } }Copy the code

User Role Authentication

The log

Add logs and tags for GraphQL, SQL, Redis, and important information such as user

// lib/logger.ts
export const apiLogger = createLogger('api')
export const dbLogger = createLogger('db')
export const redisLogger = createLogger('redis')
export const logger = createLogger('common')Copy the code

Adding requestId (sessionId) to logs

Add requestId to your logs to help track bugs and detect performance issues

// lib/logger.ts
const requestId = format((info) => {
  info.requestId = session.get('requestId')
  return info
})Copy the code

Structured Exception Information

Structured API exception information, where extension.code represents the exception error code, convenient debugging and front-end use. Extensions. exception represents the original exception, stack, and details. Note that extensions. Exception needs to be shielded in production

$ curl 'https://todo.xiange.tech/graphql' -H 'Content-Type: application/json' --data-binary '{"query":"{\n  dbError\n}"}'
{
  "errors": [
    {
      "message": "column User.a does not exist",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "dbError"
      ],
      "extensions": {
        "code": "SequelizeDatabaseError",
        "exception": {
          "name": "SequelizeDatabaseError",
          "original": {
            "name": "error",
            "length": 104,
            "severity": "ERROR",
            "code": "42703",
            "position": "57",
            "file": "parse_relation.c",
            "line": "3293",
            "routine": "errorMissingColumn",
            "sql": "SELECT count(*) AS \"count\" FROM \"users\" AS \"User\" WHERE \"User\".\"a\" = 3;"
          },
          "sql": "SELECT count(*) AS \"count\" FROM \"users\" AS \"User\" WHERE \"User\".\"a\" = 3;",
          "stacktrace": [
            "SequelizeDatabaseError: column User.a does not exist",
            "    at Query.formatError (/code/node_modules/sequelize/lib/dialects/postgres/query.js:354:16)",
          ]
        }
      }
    }
  ],
  "data": {
    "dbError": null
  }
}Copy the code

Mask the exception stack and details in production

Avoid exposing raw exceptions and stack information to production

{
  "errors": [
    {
      "message": "column User.a does not exist",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "dbError"
      ],
      "extensions": {
        "code": "SequelizeDatabaseError"
      }
    }
  ],
  "data": {
    "dbError": null
  }
}Copy the code

Abnormal alarm

Classify exceptions based on the code and report the exceptions to the monitoring system. Here the monitoring system uses Sentry

// lib/error.ts:formatError let code: string = _.get(error, 'extensions.code', 'Error') let info: any let level = Severity.Error if (isAxiosError(originalError)) { code = `Request${originalError.code}` } else if (isJoiValidationError(originalError)) { code = 'JoiValidationError' info = originalError.details } else if (isSequelizeError(originalError)) { code = originalError.name if (isUniqueConstraintError(originalError)) { info = originalError.fields level = Severity.Warning } } else if (isApolloError(originalError)){ level = originalError.level ||  Severity.Warning } else if (isError(originalError)) { code = _.get(originalError, 'code', originalError.name) level = Severity.Fatal } Sentry.withScope(scope => { scope.setTag('code', code) scope.setLevel(level) scope.setExtras(formatError) Sentry.captureException(originalError || error) })Copy the code

Health check

The K8S monitors the application status based on the health check, and responds to and resolves exceptions in a timely manner

$ curl http://todo.xiange.tech/.well-known/apollo/server-health
{"status":"pass"}Copy the code

filebeat & ELK

Log files are sent to the ELK log system through FileBeat for future analysis and debugging

monitoring

Monitor SQL slow queries and time consuming API logs in the logging system, with real-time email notifications (consider nailing)

Parameter calibration

Parameter verification using Joi

function createUser ({}, { name, email, password }, { models, utils }) {
  Joi.assert(email, Joi.string().email())
}

function createTodo ({}, { todo }, { models, utils }) {
  Joi.validate(todo, Joi.object().keys({
    name: Joi.string().min(1),
  }))
}Copy the code

Server side rendering

npm scripts

  • npm start
  • npm test
  • npm run dev

Use CI to enhance code quality