Skip to content

Latest commit

 

History

History
238 lines (189 loc) · 4.18 KB

README.md

File metadata and controls

238 lines (189 loc) · 4.18 KB

APIQL

Implementation of the API language similar to GraphQL for Ruby on Rails.

It compiles requests into Hashes for faster rendering.

It automatically detects nested entities and eager-loads them for faster DB access!

In controller or Grape API endpoint, handler of POST /apiql method (or any other, see below APIQL.endpoint):

def apiql
  schema = APIQL.cache(params)
  APIQL.new(self, :session, :current_user, :params).render(schema)
end

variables session, current_user and params (you can list any you need in your presenters/responders) will be stored into context you can use in presenters and handlers

Define presenters for your models:

class User < ApplicationRecord
  after_commit { APIQL.drop_cache(self) }

  class Entity < ::APIQL::Entity
    attributes :full_name, :email, :token, :role, :roles # any attributes, methods or associations

    def token # if defined, method will be called instead of attribute
      object.token if object == current_user # directly access to current_user from context
    end

    def roles
      APIQL.cacheable([object, roles], :roles, expires_in: 3600) do
        roles.pluck(:name).join(', ')
      end
    end

    class << self
      def me
        authorize! :show, ::User

        current_user
      end

      def authenticate(email, password)
        user = ::User.find_by(email)
        user.authenticate(password)

        user
      end

      def logout
        current_user&.logout

        :ok
      end
    end
  end

  has_many :roles

  ...
end

class Role < ApplicationRecord
  after_commit { APIQL.drop_cache(self) }

  class Entity < ::APIQL::Entity
    attributes :id, :name
  end
end

JS:

assets/javascripts/application.js:

//= require apiql
APIQL.endpoint = "/apiql"
// function apiql(schema, params = {}, form = null) -- schema is cached, so entire request is passed only for first time, later - short hashes only

authenticate(email, password) {
  apiql(`
    User.logout()

    User.authenticate(email, password) {
      token
    }

    User.me {
      email full_name role token

      roles {
        id title
      }
   }
  `, {
    email: email, // these vars will be passed into methods on the server side
    password: password
  })
  .then(response => { // response contains results of called methods
    let user = response.me
  })
}

logout() {
  apiql(`
    User.logout
  `)
  .then(response => {
  })
}

you can call methods on entities:

  apiql(`
    User.authenticate(email, password) {
      token
    }

    user: User.me.reload {
      email full_name role token

      roles(filter) {
        id title
      }

      contacts.primary {
        full_name
        email
      }
   }
  `, {
    email: email,
    filter: 'all',
    password: password
  })
  .then(response => {
    let user = response.user
  })
}

You can use initializer like this for authorization using cancancan gem:

config/initializers/apiql.rb:

class APIQL
  class Context
    def authorize!(*args)
      ability.authorize!(*args)
    end

    private

    def ability
      @ability ||= ::Ability::Factory.build_ability_for(current_user)
    end
  end
end

CRUD methods available for all models:

  apiql(`
    User.create(user)
  `, {
    user: {
      email: this.email,
      full_name: this.full_name
    }
  })
  .then(response => {
    ...
  })

  apiql(`
    User.find(id) {
      id email full_name
    }
  `, {
    id: this.id
  })
  .then(response => {
    ...
  })

  apiql(`
    User.all {
      id email full_name
    }
  `)
  .then(response => {
    ...
  })

  apiql(`
    User.update(id, user)
  `, {
    id: this.id,
    user: {
      email: this.email,
      full_name: this.full_name
    }
  })
  .then(response => {
    ...
  })

  apiql(`
    User.destroy(id)
  `, {
    id: this.id
  })
  .then(response => {
    ...
  })

or mount methods from external modules:

class APIQL < ::APIQL
  mount ::Services::User # all methods could be called with "user" prefix like "user.logout()"
  mount ::Services::Common, as: '' # all methods could be called without prefixes
  mount ::Services::Employer, as: 'dashboard" # all methods could be called with specified prefix
end