What does it do?

graphql-model-directives

pipeline status coverage report

What does it do?

  • Automatically create GraphQL servers from schema files.
  • Waste less time writing and maintaining your backend code.
  • Simplifies writing queries, mutations and seamlessly defines your resolver functions.
  • Generates useable Model/ORM for any backend adapter.
  • Built-in user authentication with user role-based access.
  • Easy pagination featuring field filtering and sorting.
  • Model validations on save, (nested) relations, and automatic orphan deletion.
  • User resources model with permissions system.
  • GraphQL Subscription over WebSocket connection.
  • Harmonious file uploading, with cloud or local processes.
  • Built-in email invitations using nodemailer, or custom methods.
  • Supports Memory, Redis, MongoDB, and several types of SQL database integrations!

At a glance

This example will create a server that has a users collection, and a resources collection that belongs to users.

It works dynamically, and doesn't generate code because it is meant to work at runtime. You can still hook in resolvers for a custom API.

The idea is you provide a graphql.schema with graphql-model-directives and it will create and ORM, and API that works perfectly with GraphQL Apollo.

type User @model(name: "users") @user @api {
  id:                ID!      @id            @permission
  created:           DateTime @created       @index(dir: 1)
  updated:           DateTime @updated       @index(dir: 1)
  birth:             Date     @birthdate     @validation(params: { minAge: 13 })
  age:               String   @virtual       @permission(condition: "showAge")
  showAge:           Boolean                 @default(value: false)
  limitInvites:      Int      @maxInvites    @default(value: 10)
  emailConfirmation: String   @emailConfirm
  emailAddr:         String   @email @login  @validation(params: { template: "email" })
  pass:              String   @password      @validation(params: { min: 4, max: 64 })
  passReset:         String   @passwordReset

  access:            [String] @roles
  tokenList:         [String] @tokens
  tok:               String   @token

  name:              String   @permission
}

type Resource
@model(name: "resource")
@resource(belongsTo: [ "author" ])
@permission(condition: "isPublic")
@api
{
  id:              ID!                  @id @permission
  created:         DateTime             @created
  updated:         DateTime             @updated
                                        @index(dir: -1, compounds: [ "author", "isDone" ])
  authorName:      String               @virtual(params: { method: "alias", value: "author.name" })
  author:          User                 @relation(preserve: true)
                                        @default(context: "user.id")
  description:     String               @default(value: "something something something")
  finished:        DateTime             @index
  json:            JSON
  unique:          String               @unique
  date:            Date                 @index
  float:           Float                @index @default(value: 1.0)
  integer:         Int                  @index @default(value: 0)
  string:          String               @index @default(value: "")
  isDone:          Boolean              @index @default(value: false)
  isPublic:        Boolean              @default(value: true)
}

Documentation Links

Installation

Installation

Install Seaturtle GraphQL Model Directives

> npm install @seaturtle/graphql-model-directives

Install the Apollo Server flavor of your choice with subscription transports.

> npm install apollo-server-express express

Install the DB libraries you plan to use:

> npm install redis redis-mock mongooose sequelize sqlite3

Getting Started

Server Setup

Create a file called server.js:

const { ApolloApiServer } = require('@seaturtle/graphql-model-directives');

const { ApolloServer } = require('apollo-server-express')
const express = require('express');

const config = require('./config');

const rawSchema = require('fs').readFileSync(
  require('path').join(__dirname, 'schema.graphql')
);

const resolvers = {
  // (OPTIONAL) defined automatically
};

const store = new MemoryStore(null, {
  resolvers,
});

const server = new ApolloApiServer({
  store,
  rawSchema,
  config,
  resolvers
}, ApolloServer, express);

Create a file called schema.graphql:

type User @model(name: "users") @user @api {
  id:                ID!      @id            @permission
  created:           DateTime @created       @index(dir: 1)
  updated:           DateTime @updated       @index(dir: 1)
  birthdate:         Date     @birthdate     @validation(params: { minAge: 13 })
  age:               String   @virtual       @permission(condition: "showAge")
  showAge:           Boolean                 @default(value: false)
  maxInvites:        Int      @maxInvites    @default(value: 10)
  email:             String   @email @login  @validation(params: { template: "email" })
  emailConfirm:      String   @emailConfirm
  password:          String   @password      @validation(params: { min: 8, max: 32 })
  passwordReset:     String   @passwordReset
  name:              String   @index
  roles:             [String] @roles
  tokens:            [String] @tokens
  token:             String   @token

  resources(input: SortedPageInput): JSONSortedPage @virtual @resolve(model: {
    name: "Resource",
    method: "find",
    params: [ { author: "root.id" } ]
  })

  finishedResources(page: SortedPageInput): JSONSortedPage @virtual @resolve(model: {
    name: "Resource",
    method: "find",
    params: [ { author: "root.id", isDone: true } ]
  })

  pendingResources(page: SortedPageInput): JSONSortedPage @virtual @resolve(model: {
    name: "Resource",
    method: "find",
    params: [ { author: "root.id", isDone: false } ]
  })
}

type UserInvite @model(name: "userinvites") @resource(belongsTo: "user") @invite(from: "user") @api {
  id:    ID!    @id       @permission
  user:  User   @relation @default(context: "user.id")
  email: String @index    @email @validation(params: { template: "email" })
}

type Resource @model(name: "resource") @permission(condition: "isPublic") @api {
  id:               ID!        @id @permission
  created:          DateTime   @created
  updated:          DateTime   @updated
                               @index(dir: -1, compounds: ["author", "isDone"])
  user:             User       @relation(id: 0)
                               @default(context: "user.id")
  authorName:       String     @virtual(params: { method: "alias", value: "author.name" })
  description:      String     @default(value: "something something something")
  file:             File       @file @permission
  fileUrl:          String     @permission
  finished:         DateTime
  json:             JSON
  boolean:          Boolean
  float:            Float
  integer:          Int
  date:             Date
  string:           String
  nestedResources:  [Resource] @nested
  relatedResources: [Resource] @relation
  isPublic:         Boolean    @default(value: true)
  isDone:           Boolean    @default(value: false) @index
}

type Subscription {
  resourceCreated: Resource
  resourceUpdated: Resource
  resourceRemoved: Resource
}

type Query {
  finishedResources(page: SortedPageInput): JSONSortedPage @resolve(model: {
    name: "Resource",
    method: "find",
    params: [ { user: "context.userId", isDone: true } ]
  }) @auth

  pendingResources(page: SortedPageInput): JSONSortedPage @resolve(model: {
    name: "Resource",
    method: "find",
    params: [ { user: "context.userId", isDone: false } ]
  }) @auth
}

type Mutation {
  upsertResourceFile(id: ID, file: Upload): Resource @resolve(model: {
    method: "upsert",
    params: [ [ ".mixin.args", { user: "context.userId" } ] ]
  }) @auth
}

Create a config.js file:

const path = require('path');
exports.adminToken = 'admin';
exports.adminEmail = 'admin@local';
exports.superLogin = 'admin@local';
exports.superRole = 'super';
exports.inviteOnly = false;
exports.defaultMaxInvites = 10;
exports.jwtAudience = '';
exports.jwtIssuer = '';
exports.jwtHmacSecret = 'secret';
exports.awsPath = null;
exports.uploader = require('./uploader'); // see example/uploader.js
exports.uploaderS3Bucket = 'uploads';
exports.uploaderS3PublicURL = '';
exports.uploaderLocalPath = path.join(__dirname, 'uploads');
exports.uploaderPublicPath = '/uploads';
exports.mailer = require('./mailer'); // see example/mailer.js
exports.mailerType = 'smtp'; // or aws-ses
exports.mailerSMTP = {};
exports.mailerAWS = null;
exports.mailerLog = true;
exports.noreply = 'noreply@local';
const linkHost = 'http://localhost:3000';
exports.linkConfirmEmail = `${linkHost}/confirmEmail`;
exports.linkForgetPassword = `${linkHost}/forgetPassword`;
exports.linkInviteEmail = `${linkHost}/inviteEmail`;

GraphQL Directives

This framework uses a set of custom GraphQL Directives to provide additional database schema annotations.

GraphQL Directives
Static Members
@api(name, plural, options)
@auth(allow = [])
@created()
@default(value?, context?)
@encrypt(store)
@file(store)
@id(store)
@model(store)
@modelType(store)
@nested(store)
@page()
@permission(store)
@relation(store)
@resolve(store)
@resource(store)
@scanned()
@sorted()
@index(store)
@unique(store)
@updated(store)
@user(store)
@email(store)
@emailConfirm(store)
@birthdate(store)
@invite(store)
@login(store)
@maxInvites(store)
@password(store)
@passwordReset(store)
@roles(store)
@token(store)
@tokens(store)
@validation(store)
@virtual(store)

Generic Adapter

Provides core functionality of uses this as a library for defining other database adapters. The GraphQL definitions rely of the ModelDefinitions class in order to describe an internal version of the schema for database integration.

Generic Adapter
Static Members
new ModelDefinitions(name, store)
new Collection()
new StoreAdapter(resolvers, _PubSub, _withFilter, insecureCrypto)
helpers

Data (JSON)

Data (JSON)
Static Members
new DataCollection()
new DataStore(options, crypto)