Skip to content

Commit

Permalink
Merge pull request #330 from platanus/f/graphql
Browse files Browse the repository at this point in the history
Feat/ Beta support for graphql
  • Loading branch information
Andrés Cádiz Vidal authored Jan 19, 2021
2 parents 5ec806a + 31775e0 commit b75a325
Show file tree
Hide file tree
Showing 21 changed files with 395 additions and 11 deletions.
3 changes: 3 additions & 0 deletions lib/potassium/assets/README.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ readme:
power_api:
title: "API Support"
body: "This projects uses [Power API](https://github.com/platanus/power_api). It's a Rails engine that gathers a set of gems and configurations designed to build incredible REST APIs."
graphql:
title: "API Support"
body: "This projects uses [graphql-ruby](https://graphql-ruby.org/) to generate a GraphQL API."
active_admin:
title: "Administration"
body: |
Expand Down
55 changes: 55 additions & 0 deletions lib/potassium/assets/app/graphql/graphql_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
class GraphqlController < ApplicationController
# If accessing from outside this domain, nullify the session
# This allows for outside API access while preventing CSRF attacks,
# but you'll have to authenticate your user separately
# protect_from_forgery with: :null_session

def execute
variables = prepare_variables(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = { current_user: get_current_user }
result = GqlSampleSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
rescue => e
raise e unless Rails.env.development?
handle_error_in_development e
end

private

# Handle variables in form data, JSON body, or a blank value
def prepare_variables(variables_param)
case variables_param
when String
if variables_param.present?
JSON.parse(variables_param) || {}
else
{}
end
when Hash
variables_param
when ActionController::Parameters
variables_param.to_unsafe_hash # GraphQL-Ruby will validate name and type of incoming variables.
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{variables_param}"
end
end

def handle_error_in_development(e)
logger.error e.message
logger.error e.backtrace.join("\n")

render json: { errors: [{ message: e.message, backtrace: e.backtrace }], data: {} }, status: 500
end

def get_current_user
if request.headers['Authorization']
_, token = request.headers['Authorization'].split
decoded_token = JWT.decode token, ENV['HMAC_SECRET'], true, { algorithm: 'HS256' }
User.find(decoded_token.first["id"])
end
end
end
23 changes: 23 additions & 0 deletions lib/potassium/assets/app/graphql/mutations/login_mutation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require 'jwt'

class Mutations::LoginMutation < Mutations::BaseMutation
null true

argument :email, String, required: true
argument :password, String, required: true


field :token, String, null: true

def resolve(email:, password:)
user = User.find_by(email: email)
if user&.valid_password?(password)
payload = { id: user.id, email: user.email, exp: (Time.zone.now + 24.hours).to_i }
token = JWT.encode payload, ENV['HMAC_SECRET'], 'HS256'
return { token: token }
end
GraphQL::ExecutionError.new("User or Password invalid")
rescue ActiveRecord::RecordInvalid => e
GraphQL::ExecutionError.new("Invalid input: #{e.record.errors.full_messages.join(', ')}")
end
end
4 changes: 4 additions & 0 deletions lib/potassium/assets/app/graphql/queries/base_query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Queries
class BaseQuery < GraphQL::Schema::Resolver
end
end
4 changes: 4 additions & 0 deletions lib/potassium/assets/app/graphql/types/base/base_argument.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Types::Base
class BaseArgument < GraphQL::Schema::Argument
end
end
4 changes: 4 additions & 0 deletions lib/potassium/assets/app/graphql/types/base/base_enum.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Types::Base
class BaseEnum < GraphQL::Schema::Enum
end
end
5 changes: 5 additions & 0 deletions lib/potassium/assets/app/graphql/types/base/base_field.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Types::Base
class BaseField < GraphQL::Schema::Field
argument_class Types::Base::BaseArgument
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Types::Base
class BaseInputObject < GraphQL::Schema::InputObject
argument_class Types::Base::BaseArgument
end
end
7 changes: 7 additions & 0 deletions lib/potassium/assets/app/graphql/types/base/base_interface.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module Types::Base
module BaseInterface
include GraphQL::Schema::Interface

field_class Types::Base::BaseField
end
end
5 changes: 5 additions & 0 deletions lib/potassium/assets/app/graphql/types/base/base_object.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Types::Base
class BaseObject < GraphQL::Schema::Object
field_class Types::Base::BaseField
end
end
4 changes: 4 additions & 0 deletions lib/potassium/assets/app/graphql/types/base/base_scalar.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Types::Base
class BaseScalar < GraphQL::Schema::Scalar
end
end
4 changes: 4 additions & 0 deletions lib/potassium/assets/app/graphql/types/base/base_union.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Types::Base
class BaseUnion < GraphQL::Schema::Union
end
end
10 changes: 10 additions & 0 deletions lib/potassium/assets/app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Types
class MutationType < Types::Base::BaseObject
# TODO: remove me
field :test_field, String, null: false,
description: "An example field added by the generator"
def test_field
"Hello World"
end
end
end
13 changes: 13 additions & 0 deletions lib/potassium/assets/app/graphql/types/query_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Types
class QueryType < Types::Base::BaseObject
# Add root-level fields here.
# They will be entry points for queries on your schema.

# TODO: remove me
field :test_field, String, null: false,
description: "An example field added by the generator"
def test_field
"Hello World!"
end
end
end
20 changes: 20 additions & 0 deletions lib/potassium/assets/config/graphql_playground.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# config/initializers/graphql_playground.rb
# All config options have a default that should work out of the box
if Rails.env.development?
GraphqlPlayground::Rails.configure do |config|
# config.headers = {
# 'X-Auth-Header' => ->(view_context) { "123" }
# }
# config.title = "Playground"
# config.csrf = true
# config.playground_version = "latest"
# # Ideally the assets would be added to your projects `vendor/assets` directories
# config.favicon = "/assets/playground.ico"
# config.playground_js_url = "/assets/playground.js"
# config.playground_css_url = "/assets/playground.css"
# # see: https://github.com/prisma-labs/graphql-playground#settings
config.settings = {
"schema.polling.enable": false
}
end
end
7 changes: 3 additions & 4 deletions lib/potassium/cli_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,11 @@ module Potassium::CliOptions # rubocop:disable Metrics/ModuleLength
default_test_value: false
},
{
type: :switch,
type: :flag,
name: "api",
desc: "Whether to apply the API mode or not",
negatable: true,
desc: "Which API interface to use",
default_value: "none",
default_test_value: false
default_test_value: "None"
},
{
type: :flag,
Expand Down
93 changes: 87 additions & 6 deletions lib/potassium/recipes/api.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
class Recipes::Api < Rails::AppBuilder
def ask
api_support = answer(:api) { Ask.confirm("Do you want to enable API support?") }
set(:api_support, api_support)
api_interfaces = {
rest: "REST (with Power API)",
graphql: "GraphQL (beta)",
none: "None, thanks"
}
api_interface = answer(:api) do
api_interfaces.keys[Ask.list("Which API interface are you using?", api_interfaces.values)]
end
set :api, api_interface.to_sym
end

def create
add_api if get(:api_support)
if get(:api) == :graphql
add_graphql
elsif get(:api) == :rest
add_power_api
end
end

def install
add_api
ask
create
end

def installed?
gem_exists?(/power_api/)
gem_exists?(/power_api/) || gem_exists?(/graphql/)
end

private

def add_api
def add_power_api
gather_gem 'power_api'

gather_gems(:development, :test) do
Expand All @@ -31,4 +43,73 @@ def add_api
generate "power_api:install"
end
end

def add_graphql
gather_gem 'graphql'
if get(:authentication)
gather_gem 'jwt'
end
gather_gems(:development, :test) do
gather_gem 'graphql_playground-rails'
end

after(:gem_install) do
generate "graphql:install --skip_graphiql"
playground_route = <<~HEREDOC
\n
if Rails.env.development?
mount GraphqlPlayground::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
end
HEREDOC
inject_into_file(
'config/routes.rb',
playground_route,
after: 'post "/graphql", to: "graphql#execute"'
)
copy_file(
"../assets/config/graphql_playground.rb",
"config/initializers/graphql_playground.rb"
)
remove_dir 'app/graphql/types'
directory '../assets/app/graphql/types', 'app/graphql/types'
gsub_file 'app/graphql/mutations/base_mutation.rb', 'Types::Base', 'Types::Base::Base'
directory '../assets/app/graphql/queries', 'app/graphql/queries'
gsub_file 'app/graphql/mutations/base_mutation.rb', 'RelayClassic', ''
gsub_file(
'app/graphql/mutations/base_mutation.rb',
" input_object_class Types::Base::BaseInputObject\n", ''
)

if get(:authentication)
copy_file(
'../assets/app/graphql/graphql_controller.rb',
'app/controllers/graphql_controller.rb',
force: true
)
gsub_file(
'app/controllers/graphql_controller.rb',
'GqlSampleSchema',
"#{get(:titleized_app_name).delete(' ')}Schema"
)
copy_file(
'../assets/app/graphql/mutations/login_mutation.rb',
'app/graphql/mutations/login_mutation.rb'
)
inject_into_file(
'app/graphql/types/mutation_type.rb',
"\n field :login, mutation: Mutations::LoginMutation",
after: 'class MutationType < Types::Base::BaseObject'
)
append_to_file(".env.development", "HMAC_SECRET=\n")
end

inject_into_file(
'app/controllers/graphql_controller.rb',
"\n\n skip_before_action :verify_authenticity_token",
after: '# protect_from_forgery with: :null_session'
)

add_readme_section :internal_dependencies, :graphql
end
end
end
53 changes: 53 additions & 0 deletions lib/potassium/recipes/front_end.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ def create
if value == :vue
recipe.setup_vue_with_compiler_build
recipe.setup_jest
if get(:api) == :graphql
recipe.setup_apollo
end
end
recipe.add_responsive_meta_tag
recipe.setup_tailwind
Expand Down Expand Up @@ -83,6 +86,27 @@ def setup_jest
copy_file '../assets/app/javascript/app.spec.js', 'app/javascript/app.spec.js'
end

def setup_apollo
run 'bin/yarn add vue-apollo graphql apollo-client apollo-link apollo-link-http apollo-cache-inmemory graphql-tag'

inject_into_file(
'app/javascript/packs/application.js',
apollo_imports,
after: "import App from '../app.vue';"
)

inject_into_file(
'app/javascript/packs/application.js',
apollo_loading,
after: "import VueApollo from 'vue-apollo';"
)
inject_into_file(
'app/javascript/packs/application.js',
"\n apolloProvider,",
after: "components: { App },"
)
end

private

def frameworks(framework)
Expand All @@ -94,6 +118,35 @@ def frameworks(framework)
frameworks[framework]
end

def apollo_imports
<<~JS
\n
import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import VueApollo from 'vue-apollo';
JS
end

def apollo_loading
<<~JS
\n
const httpLink = createHttpLink({
uri: `${window.location.origin}/graphql`,
})
const cache = new InMemoryCache()
const apolloClient = new ApolloClient({
link: httpLink,
cache,
})
Vue.use(VueApollo)
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})
JS
end

def setup_client_css
application_css = 'app/javascript/css/application.css'
create_file application_css, "", force: true
Expand Down
Loading

0 comments on commit b75a325

Please sign in to comment.