Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/ Beta support for graphql #330

Merged
merged 19 commits into from
Jan 19, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment on lines +14 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

creo que esto se parece a una de las cosas que teníamos en potassium antes y que terminamos sacando porque generaba problemas de replicabilidad development/prod y hacía el debugging más oscuro, no sé si es exacto el caso, pero en general tiendo a preferir que estas condiciones sean solo pa casos muy particulares. Porque si los errores pasan de forma distinta en prod que en dev, se vuelve cacho. Aquí está lo que decía.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eso es parte del boiler plate de la gema.
Entiendo que es su forma de solucionar que solo cuando se caiga en development el endpoint de graphql te responda con el backtrace y el error.
Me hace sentido lo que dices de todas maneras, voy a revisar como se comporta sin eso


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
24 changes: 24 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,24 @@
require 'jwt'

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

# argument :user_id, ID, required: true, loads: Types::UserType
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no caché esto

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

se me pasó jeje

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
18 changes: 18 additions & 0 deletions lib/potassium/assets/config/graphql_playground.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# config/initializers/graphql_playground.rb
# All config options have a default that should work out of the box
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
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
92 changes: 86 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pregunta inocente: tendrá sentido tenerlo como recetas aparte? Digo, existe la posibilidad de tener un proyecto usando rest para algunas cosas y graphql para otras?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

si es cierto no lo había pensado.
Es posible que pase.
igual me tinca que eligiendo uno y depués haciendo potassium install <el_otro> debería andar bien por mientras

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,72 @@ 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"'
)
ironcadiz marked this conversation as resolved.
Show resolved Hide resolved
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'
)
ironcadiz marked this conversation as resolved.
Show resolved Hide resolved
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
ironcadiz marked this conversation as resolved.
Show resolved Hide resolved
ironcadiz marked this conversation as resolved.
Show resolved Hide resolved
run 'bin/yarn add vue-apollo graphql apollo-client apollo-link apollo-link-http apollo-cache-inmemory graphql-tag'
ironcadiz marked this conversation as resolved.
Show resolved Hide resolved

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