From df5caea56f18da6e6f7b698933b562aadfff74c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 28 Sep 2016 12:45:34 +0200 Subject: [PATCH 001/147] Installed needed gems for GraphQL --- Gemfile | 3 +++ Gemfile.lock | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 83a3bddbd..cf753556a 100644 --- a/Gemfile +++ b/Gemfile @@ -63,6 +63,9 @@ gem 'browser' gem 'turnout' gem 'redcarpet' +gem 'graphql' +gem 'graphiql-rails' + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug' diff --git a/Gemfile.lock b/Gemfile.lock index bf37a7091..71a7ce2eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -176,6 +176,9 @@ GEM geocoder (1.3.7) globalid (0.3.7) activesupport (>= 4.1.0) + graphiql-rails (1.3.0) + rails + graphql (0.18.11) groupdate (3.0.1) activesupport (>= 3) gyoku (1.3.1) @@ -466,6 +469,8 @@ DEPENDENCIES foundation-rails foundation_rails_helper fuubar + graphiql-rails + graphql groupdate i18n-tasks initialjs-rails (= 0.2.0.4) @@ -506,4 +511,4 @@ DEPENDENCIES whenever BUNDLED WITH - 1.12.5 + 1.13.1 From ab333d6627ae780990ceb750bb99136994487e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 28 Sep 2016 12:47:07 +0200 Subject: [PATCH 002/147] Created controller and route to answer GraphQL queries --- app/controllers/graphql_controller.rb | 11 +++++++++++ config/routes.rb | 5 +++++ 2 files changed, 16 insertions(+) create mode 100644 app/controllers/graphql_controller.rb diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb new file mode 100644 index 000000000..11accf52a --- /dev/null +++ b/app/controllers/graphql_controller.rb @@ -0,0 +1,11 @@ +class GraphqlController < ApplicationController + + def query + #puts "I'm the GraphqlController inside the #query action!!" + + render json: ConsulSchema.execute( + params[:query], + variables: params[:variables] || {} + ) + end +end diff --git a/config/routes.rb b/config/routes.rb index df1a7c6fb..b86cb0c30 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -262,6 +262,11 @@ Rails.application.routes.draw do end end + # GraphQL + mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/queries" + post '/queries', to: 'graphql#query' + + if Rails.env.development? mount LetterOpenerWeb::Engine, at: "/letter_opener" end From 022340b1cf076dd969c4969d6fcd463dd3b72bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 28 Sep 2016 12:48:24 +0200 Subject: [PATCH 003/147] Created GraphQL Schema and query root --- app/graph/consul_schema.rb | 3 +++ app/graph/types/query_root.rb | 5 +++++ config/application.rb | 2 ++ 3 files changed, 10 insertions(+) create mode 100644 app/graph/consul_schema.rb create mode 100644 app/graph/types/query_root.rb diff --git a/app/graph/consul_schema.rb b/app/graph/consul_schema.rb new file mode 100644 index 000000000..dc7db1354 --- /dev/null +++ b/app/graph/consul_schema.rb @@ -0,0 +1,3 @@ +ConsulSchema = GraphQL::Schema.define do + query QueryRoot +end diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb new file mode 100644 index 000000000..121b5f389 --- /dev/null +++ b/app/graph/types/query_root.rb @@ -0,0 +1,5 @@ +QueryRoot = GraphQL::ObjectType.define do + name "Query" + description "The query root for this schema" + +end diff --git a/config/application.rb b/config/application.rb index 3f2a0861a..3182e319c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -45,6 +45,8 @@ module Consul config.autoload_paths << "#{Rails.root}/app/models/custom" config.paths['app/views'].unshift(Rails.root.join('app', 'views', 'custom')) + # Add GraphQL directories to the autoload path + config.autoload_paths << Rails.root.join('app', 'graph', 'types') end end From 530da6b893021cc64d891163dfe4982430519384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 28 Sep 2016 12:49:38 +0200 Subject: [PATCH 004/147] Implemented simple test query to proposals --- app/controllers/graphql_controller.rb | 1 + app/graph/queries/getProposal.graphql | 7 +++++++ app/graph/types/proposal_type.rb | 8 ++++++++ app/graph/types/query_root.rb | 8 ++++++++ 4 files changed, 24 insertions(+) create mode 100644 app/graph/queries/getProposal.graphql create mode 100644 app/graph/types/proposal_type.rb diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 11accf52a..47deceac0 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -1,4 +1,5 @@ class GraphqlController < ApplicationController + authorize_resource :proposal def query #puts "I'm the GraphqlController inside the #query action!!" diff --git a/app/graph/queries/getProposal.graphql b/app/graph/queries/getProposal.graphql new file mode 100644 index 000000000..875dc0831 --- /dev/null +++ b/app/graph/queries/getProposal.graphql @@ -0,0 +1,7 @@ +query getProposal { + proposal(id: 22) { + id, + title, + description + } +} diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb new file mode 100644 index 000000000..223906f90 --- /dev/null +++ b/app/graph/types/proposal_type.rb @@ -0,0 +1,8 @@ +ProposalType = GraphQL::ObjectType.define do + name "Proposal" + description "A single proposal entry returns a proposal with author, total votes and comments" + # Expose fields associated with Proposal model + field :id, types.ID, "The id of this proposal" + field :title, types.String, "The title of this proposal" + field :description, types.String, "The description of this proposal" +end diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb index 121b5f389..28d1685be 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/types/query_root.rb @@ -2,4 +2,12 @@ QueryRoot = GraphQL::ObjectType.define do name "Query" description "The query root for this schema" + field :proposal do + type ProposalType + description "Find a Proposal by id" + argument :id, !types.ID + resolve -> (object, arguments, context) { + Proposal.find(arguments["id"]) + } + end end From b90a6664b0733f1c124a79e8cab0f8280c003408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 11:21:25 +0200 Subject: [PATCH 005/147] Deleted useless file --- app/graph/queries/getProposal.graphql | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 app/graph/queries/getProposal.graphql diff --git a/app/graph/queries/getProposal.graphql b/app/graph/queries/getProposal.graphql deleted file mode 100644 index 875dc0831..000000000 --- a/app/graph/queries/getProposal.graphql +++ /dev/null @@ -1,7 +0,0 @@ -query getProposal { - proposal(id: 22) { - id, - title, - description - } -} From 5c7f03bdf10b9b9ea9ec085e1120a09f79af9190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 11:22:18 +0200 Subject: [PATCH 006/147] Added more fields to graphql proposal_type --- app/graph/types/proposal_type.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 223906f90..292c0aa15 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -5,4 +5,11 @@ ProposalType = GraphQL::ObjectType.define do field :id, types.ID, "The id of this proposal" field :title, types.String, "The title of this proposal" field :description, types.String, "The description of this proposal" + field :question, types.String, "The question of this proposal" + field :external_url, types.String, "External url related to this proposal" + field :flags_count, types.Int, "Number of flags of this proposal" + field :cached_votes_up, types.Int, "Number of upvotes of this proposal" + field :comments_count, types.Int, "Number of comments on this proposal" + field :summary, types.String, "The summary of this proposal" + field :video_url, types.String, "External video url related to this proposal" end From 96d688e2869bb13df6c9f9d522982aca28cc16d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 11:31:08 +0200 Subject: [PATCH 007/147] Created graphql comment_type --- app/graph/types/comment_type.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/graph/types/comment_type.rb diff --git a/app/graph/types/comment_type.rb b/app/graph/types/comment_type.rb new file mode 100644 index 000000000..2c74bee40 --- /dev/null +++ b/app/graph/types/comment_type.rb @@ -0,0 +1,17 @@ +CommentType = GraphQL::ObjectType.define do + name "Comment" + description "A reply to a proposal, spending proposal, debate or comment" + + field :id, !types.ID, "The unique ID of this comment" + field :commentable_id, !types.Int, "ID of the resource where this comment was placed on" + field :commentable_type, !types.String, "Type of resource where this comment was placed on" + field :body, !types.String, "The body of this comment" + field :subject, !types.String, "The subject of this comment" + field :user_id, !types.Int, "The ID of the user who made this comment" + field :created_at, !types.String, "The date this comment was posted" + field :updated_at, !types.String, "The date this comment was edited" + field :flags_count, !types.Int, "The number of flags of this comment" + field :cached_votes_total, !types.Int, "The total number of votes of this comment" + field :cached_votes_up, !types.Int, "The total number of upvotes of this comment" + field :cached_votes_down, !types.Int, "The total number of downvotes of this comment" +end From dc0c36d11718a4d5544a6188f4b685f8d5c2bc31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 11:41:02 +0200 Subject: [PATCH 008/147] Added more fields to graphql proposal_type --- app/graph/types/proposal_type.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 292c0aa15..67e06d70d 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -7,9 +7,14 @@ ProposalType = GraphQL::ObjectType.define do field :description, types.String, "The description of this proposal" field :question, types.String, "The question of this proposal" field :external_url, types.String, "External url related to this proposal" + field :author_id, types.Int, "ID of the author of this proposal" field :flags_count, types.Int, "Number of flags of this proposal" field :cached_votes_up, types.Int, "Number of upvotes of this proposal" field :comments_count, types.Int, "Number of comments on this proposal" field :summary, types.String, "The summary of this proposal" field :video_url, types.String, "External video url related to this proposal" + field :geozone_id, types.Int, "ID of the geozone affected by this proposal" + field :retired_at, types.String, "Date when this proposal was retired" + field :retired_reason, types.String, "Reason why this proposal was retired" + field :retired_explanation, types.String, "Explanation why this proposal was retired" end From d201f6a48ad9034a7bbeb006aad68a792596a2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 11:42:13 +0200 Subject: [PATCH 009/147] Ability to retrieve proposal comments --- app/graph/types/proposal_type.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 67e06d70d..2bc1e37b0 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -17,4 +17,7 @@ ProposalType = GraphQL::ObjectType.define do field :retired_at, types.String, "Date when this proposal was retired" field :retired_reason, types.String, "Reason why this proposal was retired" field :retired_explanation, types.String, "Explanation why this proposal was retired" + + # Linked resources + field :comments, !types[!CommentType], "Comments in this proposal" end From de913c5536125d956cf28598ed57d4a4d38cabbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 13:10:05 +0200 Subject: [PATCH 010/147] Fixed grapqhl comment_type fields --- app/graph/types/comment_type.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/graph/types/comment_type.rb b/app/graph/types/comment_type.rb index 2c74bee40..8701ee04f 100644 --- a/app/graph/types/comment_type.rb +++ b/app/graph/types/comment_type.rb @@ -3,15 +3,15 @@ CommentType = GraphQL::ObjectType.define do description "A reply to a proposal, spending proposal, debate or comment" field :id, !types.ID, "The unique ID of this comment" - field :commentable_id, !types.Int, "ID of the resource where this comment was placed on" - field :commentable_type, !types.String, "Type of resource where this comment was placed on" - field :body, !types.String, "The body of this comment" - field :subject, !types.String, "The subject of this comment" + field :commentable_id, types.Int, "ID of the resource where this comment was placed on" + field :commentable_type, types.String, "Type of resource where this comment was placed on" + field :body, types.String, "The body of this comment" + field :subject, types.String, "The subject of this comment" field :user_id, !types.Int, "The ID of the user who made this comment" - field :created_at, !types.String, "The date this comment was posted" - field :updated_at, !types.String, "The date this comment was edited" - field :flags_count, !types.Int, "The number of flags of this comment" - field :cached_votes_total, !types.Int, "The total number of votes of this comment" - field :cached_votes_up, !types.Int, "The total number of upvotes of this comment" - field :cached_votes_down, !types.Int, "The total number of downvotes of this comment" + field :created_at, types.String, "The date this comment was posted" + field :updated_at, types.String, "The date this comment was edited" + field :flags_count, types.Int, "The number of flags of this comment" + field :cached_votes_total, types.Int, "The total number of votes of this comment" + field :cached_votes_up, types.Int, "The total number of upvotes of this comment" + field :cached_votes_down, types.Int, "The total number of downvotes of this comment" end From 6a3f7d725cecb8ed649839b25571abfbe39f5a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 13:11:34 +0200 Subject: [PATCH 011/147] QueryRoot now returns proposals, comment(:id) and comments --- app/graph/types/query_root.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb index 28d1685be..5b8664dc6 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/types/query_root.rb @@ -10,4 +10,29 @@ QueryRoot = GraphQL::ObjectType.define do Proposal.find(arguments["id"]) } end + + field :proposals do + type types[ProposalType] + description "Find all Proposals" + resolve -> (object, arguments, context) { + Proposal.all + } + end + + field :comment do + type CommentType + description "Find a Comment by id" + argument :id, !types.ID + resolve -> (object, arguments, context) { + Comment.find(arguments["id"]) + } + end + + field :comments do + type types[CommentType] + description "Find all Comments" + resolve -> (object, arguments, context) { + Comment.all + } + end end From a6149018d9fb754f7d781feb235ac7878f80a3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 13:26:18 +0200 Subject: [PATCH 012/147] Created graphql user_type --- app/graph/types/user_type.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/graph/types/user_type.rb diff --git a/app/graph/types/user_type.rb b/app/graph/types/user_type.rb new file mode 100644 index 000000000..c88e5acde --- /dev/null +++ b/app/graph/types/user_type.rb @@ -0,0 +1,11 @@ +UserType = GraphQL::ObjectType.define do + name "User" + description "An user entry, returns basic user information" + # Expose fields associated with User model + field :id, types.ID, "The id of this user" + field :created_at, types.String, "Date when this user was created" + field :username, types.String, "The username of this user" + field :geozone_id, types.Int, "The ID of the geozone where this user is active" + field :gender, types.String, "The gender of this user" + field :date_of_birth, types.String, "The birthdate of this user" +end From 657f57f9a2161b0baef6be78474e99e49d73173e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Sep 2016 13:26:54 +0200 Subject: [PATCH 013/147] Ability to link authors/users from comments and proposals --- app/graph/types/comment_type.rb | 3 +++ app/graph/types/proposal_type.rb | 1 + 2 files changed, 4 insertions(+) diff --git a/app/graph/types/comment_type.rb b/app/graph/types/comment_type.rb index 8701ee04f..9078257af 100644 --- a/app/graph/types/comment_type.rb +++ b/app/graph/types/comment_type.rb @@ -14,4 +14,7 @@ CommentType = GraphQL::ObjectType.define do field :cached_votes_total, types.Int, "The total number of votes of this comment" field :cached_votes_up, types.Int, "The total number of upvotes of this comment" field :cached_votes_down, types.Int, "The total number of downvotes of this comment" + + # Linked resources + field :user, !UserType, "User who made this comment" end diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 2bc1e37b0..1e3514997 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -19,5 +19,6 @@ ProposalType = GraphQL::ObjectType.define do field :retired_explanation, types.String, "Explanation why this proposal was retired" # Linked resources + field :author, UserType, "Author of this proposal" field :comments, !types[!CommentType], "Comments in this proposal" end From 31f0b9b98d6f4b5e4d25a209d5fb03ae5804cc77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 11:28:16 +0200 Subject: [PATCH 014/147] Created graphql geozone_type --- app/graph/types/geozone_type.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 app/graph/types/geozone_type.rb diff --git a/app/graph/types/geozone_type.rb b/app/graph/types/geozone_type.rb new file mode 100644 index 000000000..fc4bb2343 --- /dev/null +++ b/app/graph/types/geozone_type.rb @@ -0,0 +1,10 @@ +GeozoneType = GraphQL::ObjectType.define do + name "Geozone" + description "A geozone entry, returns basic geozone information" + # Expose fields associated with Geozone model + field :id, types.ID, "The id of this geozone" + field :name, types.String, "The name of this geozone" + field :html_map_coordinates, types.String, "HTML map coordinates of this geozone" + field :external_code, types.String, "The external code of this geozone" + field :census_code, types.String, "The census code of this geozone" +end From 57b01e09216a42221c64008dfe21755131f5386c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 11:33:52 +0200 Subject: [PATCH 015/147] Ability to retrieve geozone from proposals and users --- app/graph/types/proposal_type.rb | 1 + app/graph/types/user_type.rb | 3 +++ 2 files changed, 4 insertions(+) diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 1e3514997..58ece72a6 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -21,4 +21,5 @@ ProposalType = GraphQL::ObjectType.define do # Linked resources field :author, UserType, "Author of this proposal" field :comments, !types[!CommentType], "Comments in this proposal" + field :geozone, GeozoneType, "Geozone affected by this proposal" end diff --git a/app/graph/types/user_type.rb b/app/graph/types/user_type.rb index c88e5acde..224ae2702 100644 --- a/app/graph/types/user_type.rb +++ b/app/graph/types/user_type.rb @@ -8,4 +8,7 @@ UserType = GraphQL::ObjectType.define do field :geozone_id, types.Int, "The ID of the geozone where this user is active" field :gender, types.String, "The gender of this user" field :date_of_birth, types.String, "The birthdate of this user" + + # Linked resources + field :geozone, GeozoneType, "Geozone where this user is registered" end From eae18d9e376ad71d3e2f5f6ac6cf2a8d6d94b2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 11:34:12 +0200 Subject: [PATCH 016/147] Deleted useless debugging comment --- app/controllers/graphql_controller.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 47deceac0..bf9858a7d 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -2,8 +2,6 @@ class GraphqlController < ApplicationController authorize_resource :proposal def query - #puts "I'm the GraphqlController inside the #query action!!" - render json: ConsulSchema.execute( params[:query], variables: params[:variables] || {} From 78423edb2a85196dec7d20347f0614a1c8aa7b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 11:49:05 +0200 Subject: [PATCH 017/147] Created graphql debate_type --- app/graph/types/debate_type.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/graph/types/debate_type.rb diff --git a/app/graph/types/debate_type.rb b/app/graph/types/debate_type.rb new file mode 100644 index 000000000..095d50688 --- /dev/null +++ b/app/graph/types/debate_type.rb @@ -0,0 +1,22 @@ +DebateType = GraphQL::ObjectType.define do + name "Debate" + description "A single debate entry with associated info" + # Expose fields associated with Debate model + field :id, !types.ID, "The id of this debate" + field :title, types.String, "The title of this debate" + field :description, types.String, "The description of this debate" + field :author_id, types.Int, "ID of the author of this proposal" + field :created_at, types.String, "Date when this debate was created" + field :updated_at, types.String, "Date when this debate was edited" + field :flags_count, types.Int, "Number of flags of this debate" + field :cached_votes_total, types.Int, "The total number of votes of this debate" + field :cached_votes_up, types.Int, "The total number of upvotes of this debate" + field :cached_votes_down, types.Int, "The total number of downvotes of this debate" + field :comments_count, types.Int, "Number of comments on this debate" + field :geozone_id, types.Int, "ID of the geozone affected by this debate" + + # Linked resources + field :author, UserType, "Author of this debate" + field :comments, !types[!CommentType], "Comments in this debate" + field :geozone, GeozoneType, "Geozone affected by this debate" +end From afb2d19ab32051a5888d055d525edea347cf757c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 11:49:34 +0200 Subject: [PATCH 018/147] Revised which fields are allowed to be null --- app/graph/types/geozone_type.rb | 2 +- app/graph/types/proposal_type.rb | 2 +- app/graph/types/query_root.rb | 4 ++-- app/graph/types/user_type.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/graph/types/geozone_type.rb b/app/graph/types/geozone_type.rb index fc4bb2343..bcea6dc93 100644 --- a/app/graph/types/geozone_type.rb +++ b/app/graph/types/geozone_type.rb @@ -2,7 +2,7 @@ GeozoneType = GraphQL::ObjectType.define do name "Geozone" description "A geozone entry, returns basic geozone information" # Expose fields associated with Geozone model - field :id, types.ID, "The id of this geozone" + field :id, !types.ID, "The id of this geozone" field :name, types.String, "The name of this geozone" field :html_map_coordinates, types.String, "HTML map coordinates of this geozone" field :external_code, types.String, "The external code of this geozone" diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 58ece72a6..8456f0a4f 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -2,7 +2,7 @@ ProposalType = GraphQL::ObjectType.define do name "Proposal" description "A single proposal entry returns a proposal with author, total votes and comments" # Expose fields associated with Proposal model - field :id, types.ID, "The id of this proposal" + field :id, !types.ID, "The id of this proposal" field :title, types.String, "The title of this proposal" field :description, types.String, "The description of this proposal" field :question, types.String, "The question of this proposal" diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb index 5b8664dc6..1193f8b52 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/types/query_root.rb @@ -12,7 +12,7 @@ QueryRoot = GraphQL::ObjectType.define do end field :proposals do - type types[ProposalType] + type !types[!ProposalType] description "Find all Proposals" resolve -> (object, arguments, context) { Proposal.all @@ -29,7 +29,7 @@ QueryRoot = GraphQL::ObjectType.define do end field :comments do - type types[CommentType] + type !types[!CommentType] description "Find all Comments" resolve -> (object, arguments, context) { Comment.all diff --git a/app/graph/types/user_type.rb b/app/graph/types/user_type.rb index 224ae2702..bb64b818f 100644 --- a/app/graph/types/user_type.rb +++ b/app/graph/types/user_type.rb @@ -2,7 +2,7 @@ UserType = GraphQL::ObjectType.define do name "User" description "An user entry, returns basic user information" # Expose fields associated with User model - field :id, types.ID, "The id of this user" + field :id, !types.ID, "The id of this user" field :created_at, types.String, "Date when this user was created" field :username, types.String, "The username of this user" field :geozone_id, types.Int, "The ID of the geozone where this user is active" From c4c6b1b9f7a35e1339936e06566e4f67491d8ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 11:49:59 +0200 Subject: [PATCH 019/147] Ability to query for debate(:id) and debates at the QueryRoot --- app/graph/types/query_root.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb index 1193f8b52..38f292bb9 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/types/query_root.rb @@ -19,6 +19,23 @@ QueryRoot = GraphQL::ObjectType.define do } end + field :debate do + type DebateType + description "Find a Debate by id" + argument :id, !types.ID + resolve -> (object, arguments, context) { + Debate.find(arguments["id"]) + } + end + + field :debates do + type !types[!DebateType] + description "Find all Debates" + resolve -> (object, arguments, context) { + Debate.all + } + end + field :comment do type CommentType description "Find a Comment by id" From afd0e98ceabb3eaccff7ce70de7d45e9f1a268c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 12:03:37 +0200 Subject: [PATCH 020/147] Created CommentableInterface --- app/graph/types/comment_type.rb | 1 + app/graph/types/commentable_interface.rb | 21 +++++++++++++++++++++ app/graph/types/debate_type.rb | 3 +++ app/graph/types/proposal_type.rb | 3 +++ 4 files changed, 28 insertions(+) create mode 100644 app/graph/types/commentable_interface.rb diff --git a/app/graph/types/comment_type.rb b/app/graph/types/comment_type.rb index 9078257af..36a9f709e 100644 --- a/app/graph/types/comment_type.rb +++ b/app/graph/types/comment_type.rb @@ -17,4 +17,5 @@ CommentType = GraphQL::ObjectType.define do # Linked resources field :user, !UserType, "User who made this comment" + field :commentable, CommentableInterface, "Element which was commented" end diff --git a/app/graph/types/commentable_interface.rb b/app/graph/types/commentable_interface.rb new file mode 100644 index 000000000..3bbe96c4f --- /dev/null +++ b/app/graph/types/commentable_interface.rb @@ -0,0 +1,21 @@ +CommentableInterface = GraphQL::InterfaceType.define do + name "Commentable" + + field :id, !types.ID, "ID of the commentable" + field :title, types.String, "The title of this commentable" + field :description, types.String, "The description of this commentable" + field :author_id, types.Int, "ID of the author of this commentable" + field :comments_count, types.Int, "Number of comments on this commentable" + + # Linked resources + field :author, UserType, "Author of this commentable" + field :comments, !types[!CommentType], "Comments in this commentable" + field :geozone, GeozoneType, "Geozone affected by this commentable" +end + + +# Then, object types may include it: +CoffeeType = GraphQL::ObjectType.define do + # ... + interfaces([BeverageInterface]) +end diff --git a/app/graph/types/debate_type.rb b/app/graph/types/debate_type.rb index 095d50688..1cae3a2a4 100644 --- a/app/graph/types/debate_type.rb +++ b/app/graph/types/debate_type.rb @@ -1,6 +1,9 @@ DebateType = GraphQL::ObjectType.define do name "Debate" description "A single debate entry with associated info" + + interfaces([CommentableInterface]) + # Expose fields associated with Debate model field :id, !types.ID, "The id of this debate" field :title, types.String, "The title of this debate" diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 8456f0a4f..007b5824c 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -1,6 +1,9 @@ ProposalType = GraphQL::ObjectType.define do name "Proposal" description "A single proposal entry returns a proposal with author, total votes and comments" + + interfaces([CommentableInterface]) + # Expose fields associated with Proposal model field :id, !types.ID, "The id of this proposal" field :title, types.String, "The title of this proposal" From 5f407d505955a5d0f9be189cc1f99ca8c5c18308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 12:04:03 +0200 Subject: [PATCH 021/147] Added schema interface resolver --- app/graph/consul_schema.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/graph/consul_schema.rb b/app/graph/consul_schema.rb index dc7db1354..bf05f48b9 100644 --- a/app/graph/consul_schema.rb +++ b/app/graph/consul_schema.rb @@ -1,3 +1,9 @@ ConsulSchema = GraphQL::Schema.define do query QueryRoot + + resolve_type -> (object, ctx) { + # look up types by class name + type_name = object.class.name + ConsulSchema.types[type_name] + } end From fab190b16cc09a513a1609234aafe0cdd3474b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 30 Sep 2016 12:06:10 +0200 Subject: [PATCH 022/147] Deleted accidentally commited code --- app/graph/types/commentable_interface.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/graph/types/commentable_interface.rb b/app/graph/types/commentable_interface.rb index 3bbe96c4f..1f62b0d09 100644 --- a/app/graph/types/commentable_interface.rb +++ b/app/graph/types/commentable_interface.rb @@ -12,10 +12,3 @@ CommentableInterface = GraphQL::InterfaceType.define do field :comments, !types[!CommentType], "Comments in this commentable" field :geozone, GeozoneType, "Geozone affected by this commentable" end - - -# Then, object types may include it: -CoffeeType = GraphQL::ObjectType.define do - # ... - interfaces([BeverageInterface]) -end From fe2546dbbbb7c8c5cfcf1a6f0457240788a73b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 1 Oct 2016 08:50:16 +0200 Subject: [PATCH 023/147] Added clarifying comments related to security --- app/controllers/graphql_controller.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index bf9858a7d..86ea423da 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -1,4 +1,7 @@ class GraphqlController < ApplicationController + + # (!!) Está autorizando todos los resources, no sólo Proposal ¿por qué? + # (!!) Nos da acceso a recursos a los que se supone que no tenemos acceso, cómo 'Geozones', ¿por qué? authorize_resource :proposal def query From 4c121cb9f5d8480bb94fab298e577a82fcc7903f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 6 Oct 2016 11:05:47 +0200 Subject: [PATCH 024/147] Reject deeply-nested queries --- app/graph/consul_schema.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/graph/consul_schema.rb b/app/graph/consul_schema.rb index bf05f48b9..47ef8a112 100644 --- a/app/graph/consul_schema.rb +++ b/app/graph/consul_schema.rb @@ -1,6 +1,9 @@ ConsulSchema = GraphQL::Schema.define do query QueryRoot + # Reject deeply-nested queries + max_depth 7 + resolve_type -> (object, ctx) { # look up types by class name type_name = object.class.name From 56c4dbc2f47446addc1cd1748d8c995b17b07fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 12 Oct 2016 17:27:04 +0200 Subject: [PATCH 025/147] Add support for GraphQL pagination --- app/graph/types/commentable_interface.rb | 9 ++++++++- app/graph/types/debate_type.rb | 8 +++++++- app/graph/types/proposal_type.rb | 10 ++++++++-- app/graph/types/query_root.rb | 9 +++------ 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/app/graph/types/commentable_interface.rb b/app/graph/types/commentable_interface.rb index 1f62b0d09..ad51a55eb 100644 --- a/app/graph/types/commentable_interface.rb +++ b/app/graph/types/commentable_interface.rb @@ -1,6 +1,8 @@ CommentableInterface = GraphQL::InterfaceType.define do name "Commentable" + # Expose fields associated with Commentable models + field :id, !types.ID, "ID of the commentable" field :title, types.String, "The title of this commentable" field :description, types.String, "The description of this commentable" @@ -8,7 +10,12 @@ CommentableInterface = GraphQL::InterfaceType.define do field :comments_count, types.Int, "Number of comments on this commentable" # Linked resources + field :author, UserType, "Author of this commentable" - field :comments, !types[!CommentType], "Comments in this commentable" + + connection :comments, CommentType.connection_type do + description "Comments in this commentable" + end + field :geozone, GeozoneType, "Geozone affected by this commentable" end diff --git a/app/graph/types/debate_type.rb b/app/graph/types/debate_type.rb index 1cae3a2a4..1c36f8b40 100644 --- a/app/graph/types/debate_type.rb +++ b/app/graph/types/debate_type.rb @@ -5,6 +5,7 @@ DebateType = GraphQL::ObjectType.define do interfaces([CommentableInterface]) # Expose fields associated with Debate model + field :id, !types.ID, "The id of this debate" field :title, types.String, "The title of this debate" field :description, types.String, "The description of this debate" @@ -19,7 +20,12 @@ DebateType = GraphQL::ObjectType.define do field :geozone_id, types.Int, "ID of the geozone affected by this debate" # Linked resources + field :author, UserType, "Author of this debate" - field :comments, !types[!CommentType], "Comments in this debate" + + connection :comments, CommentType.connection_type do + description "Comments in this debate" + end + field :geozone, GeozoneType, "Geozone affected by this debate" end diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb index 007b5824c..c9d599b8d 100644 --- a/app/graph/types/proposal_type.rb +++ b/app/graph/types/proposal_type.rb @@ -3,8 +3,9 @@ ProposalType = GraphQL::ObjectType.define do description "A single proposal entry returns a proposal with author, total votes and comments" interfaces([CommentableInterface]) - + # Expose fields associated with Proposal model + field :id, !types.ID, "The id of this proposal" field :title, types.String, "The title of this proposal" field :description, types.String, "The description of this proposal" @@ -22,7 +23,12 @@ ProposalType = GraphQL::ObjectType.define do field :retired_explanation, types.String, "Explanation why this proposal was retired" # Linked resources + field :author, UserType, "Author of this proposal" - field :comments, !types[!CommentType], "Comments in this proposal" + + connection :comments, CommentType.connection_type do + description "Comments in this proposal" + end + field :geozone, GeozoneType, "Geozone affected by this proposal" end diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb index 38f292bb9..007271932 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/types/query_root.rb @@ -11,8 +11,7 @@ QueryRoot = GraphQL::ObjectType.define do } end - field :proposals do - type !types[!ProposalType] + connection :proposals, ProposalType.connection_type do description "Find all Proposals" resolve -> (object, arguments, context) { Proposal.all @@ -28,8 +27,7 @@ QueryRoot = GraphQL::ObjectType.define do } end - field :debates do - type !types[!DebateType] + connection :debates, DebateType.connection_type do description "Find all Debates" resolve -> (object, arguments, context) { Debate.all @@ -45,8 +43,7 @@ QueryRoot = GraphQL::ObjectType.define do } end - field :comments do - type !types[!CommentType] + connection :comments, CommentType.connection_type do description "Find all Comments" resolve -> (object, arguments, context) { Comment.all From dc62f6c914971263ef72bfd442bbcb2152b36564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 12 Oct 2016 17:28:01 +0200 Subject: [PATCH 026/147] Skip authorization check in GraphQL controller --- app/controllers/graphql_controller.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 86ea423da..665e6b93d 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -1,8 +1,6 @@ class GraphqlController < ApplicationController - # (!!) Está autorizando todos los resources, no sólo Proposal ¿por qué? - # (!!) Nos da acceso a recursos a los que se supone que no tenemos acceso, cómo 'Geozones', ¿por qué? - authorize_resource :proposal + skip_authorization_check def query render json: ConsulSchema.execute( From c96d1c460a2b2e9f2da555dfe4c9bb05389d6350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 12 Oct 2016 17:56:24 +0200 Subject: [PATCH 027/147] Ability to query for a specific User --- app/graph/types/query_root.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/graph/types/query_root.rb b/app/graph/types/query_root.rb index 007271932..0c3ed66b9 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/types/query_root.rb @@ -49,4 +49,14 @@ QueryRoot = GraphQL::ObjectType.define do Comment.all } end + + field :user do + type UserType + description "Find a User by id" + argument :id, !types.ID + resolve -> (object, arguments, context) { + User.find(arguments["id"]) + } + end + end From 757e68969df8fd5443b5c6800ffa263e7efa2f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 12 Oct 2016 17:57:56 +0200 Subject: [PATCH 028/147] Ability to access proposals, debates and comments authored by a specific user --- app/graph/types/user_type.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/graph/types/user_type.rb b/app/graph/types/user_type.rb index bb64b818f..922f51258 100644 --- a/app/graph/types/user_type.rb +++ b/app/graph/types/user_type.rb @@ -1,7 +1,9 @@ UserType = GraphQL::ObjectType.define do name "User" description "An user entry, returns basic user information" + # Expose fields associated with User model + field :id, !types.ID, "The id of this user" field :created_at, types.String, "Date when this user was created" field :username, types.String, "The username of this user" @@ -10,5 +12,18 @@ UserType = GraphQL::ObjectType.define do field :date_of_birth, types.String, "The birthdate of this user" # Linked resources + field :geozone, GeozoneType, "Geozone where this user is registered" + + connection :proposals, ProposalType.connection_type do + description "Proposals authored by this user" + end + + connection :debates, DebateType.connection_type do + description "Debates authored by this user" + end + + connection :comments, CommentType.connection_type do + description "Comments authored by this user" + end end From e7de5c3a7f0c6e7bcc7b4caea1ada23e2bef02cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 8 Nov 2016 16:38:51 +0100 Subject: [PATCH 029/147] Deleted old GraphQL type files --- app/graph/types/comment_type.rb | 21 --------------- app/graph/types/commentable_interface.rb | 21 --------------- app/graph/types/debate_type.rb | 31 --------------------- app/graph/types/geozone_type.rb | 10 ------- app/graph/types/proposal_type.rb | 34 ------------------------ app/graph/types/user_type.rb | 29 -------------------- 6 files changed, 146 deletions(-) delete mode 100644 app/graph/types/comment_type.rb delete mode 100644 app/graph/types/commentable_interface.rb delete mode 100644 app/graph/types/debate_type.rb delete mode 100644 app/graph/types/geozone_type.rb delete mode 100644 app/graph/types/proposal_type.rb delete mode 100644 app/graph/types/user_type.rb diff --git a/app/graph/types/comment_type.rb b/app/graph/types/comment_type.rb deleted file mode 100644 index 36a9f709e..000000000 --- a/app/graph/types/comment_type.rb +++ /dev/null @@ -1,21 +0,0 @@ -CommentType = GraphQL::ObjectType.define do - name "Comment" - description "A reply to a proposal, spending proposal, debate or comment" - - field :id, !types.ID, "The unique ID of this comment" - field :commentable_id, types.Int, "ID of the resource where this comment was placed on" - field :commentable_type, types.String, "Type of resource where this comment was placed on" - field :body, types.String, "The body of this comment" - field :subject, types.String, "The subject of this comment" - field :user_id, !types.Int, "The ID of the user who made this comment" - field :created_at, types.String, "The date this comment was posted" - field :updated_at, types.String, "The date this comment was edited" - field :flags_count, types.Int, "The number of flags of this comment" - field :cached_votes_total, types.Int, "The total number of votes of this comment" - field :cached_votes_up, types.Int, "The total number of upvotes of this comment" - field :cached_votes_down, types.Int, "The total number of downvotes of this comment" - - # Linked resources - field :user, !UserType, "User who made this comment" - field :commentable, CommentableInterface, "Element which was commented" -end diff --git a/app/graph/types/commentable_interface.rb b/app/graph/types/commentable_interface.rb deleted file mode 100644 index ad51a55eb..000000000 --- a/app/graph/types/commentable_interface.rb +++ /dev/null @@ -1,21 +0,0 @@ -CommentableInterface = GraphQL::InterfaceType.define do - name "Commentable" - - # Expose fields associated with Commentable models - - field :id, !types.ID, "ID of the commentable" - field :title, types.String, "The title of this commentable" - field :description, types.String, "The description of this commentable" - field :author_id, types.Int, "ID of the author of this commentable" - field :comments_count, types.Int, "Number of comments on this commentable" - - # Linked resources - - field :author, UserType, "Author of this commentable" - - connection :comments, CommentType.connection_type do - description "Comments in this commentable" - end - - field :geozone, GeozoneType, "Geozone affected by this commentable" -end diff --git a/app/graph/types/debate_type.rb b/app/graph/types/debate_type.rb deleted file mode 100644 index 1c36f8b40..000000000 --- a/app/graph/types/debate_type.rb +++ /dev/null @@ -1,31 +0,0 @@ -DebateType = GraphQL::ObjectType.define do - name "Debate" - description "A single debate entry with associated info" - - interfaces([CommentableInterface]) - - # Expose fields associated with Debate model - - field :id, !types.ID, "The id of this debate" - field :title, types.String, "The title of this debate" - field :description, types.String, "The description of this debate" - field :author_id, types.Int, "ID of the author of this proposal" - field :created_at, types.String, "Date when this debate was created" - field :updated_at, types.String, "Date when this debate was edited" - field :flags_count, types.Int, "Number of flags of this debate" - field :cached_votes_total, types.Int, "The total number of votes of this debate" - field :cached_votes_up, types.Int, "The total number of upvotes of this debate" - field :cached_votes_down, types.Int, "The total number of downvotes of this debate" - field :comments_count, types.Int, "Number of comments on this debate" - field :geozone_id, types.Int, "ID of the geozone affected by this debate" - - # Linked resources - - field :author, UserType, "Author of this debate" - - connection :comments, CommentType.connection_type do - description "Comments in this debate" - end - - field :geozone, GeozoneType, "Geozone affected by this debate" -end diff --git a/app/graph/types/geozone_type.rb b/app/graph/types/geozone_type.rb deleted file mode 100644 index bcea6dc93..000000000 --- a/app/graph/types/geozone_type.rb +++ /dev/null @@ -1,10 +0,0 @@ -GeozoneType = GraphQL::ObjectType.define do - name "Geozone" - description "A geozone entry, returns basic geozone information" - # Expose fields associated with Geozone model - field :id, !types.ID, "The id of this geozone" - field :name, types.String, "The name of this geozone" - field :html_map_coordinates, types.String, "HTML map coordinates of this geozone" - field :external_code, types.String, "The external code of this geozone" - field :census_code, types.String, "The census code of this geozone" -end diff --git a/app/graph/types/proposal_type.rb b/app/graph/types/proposal_type.rb deleted file mode 100644 index c9d599b8d..000000000 --- a/app/graph/types/proposal_type.rb +++ /dev/null @@ -1,34 +0,0 @@ -ProposalType = GraphQL::ObjectType.define do - name "Proposal" - description "A single proposal entry returns a proposal with author, total votes and comments" - - interfaces([CommentableInterface]) - - # Expose fields associated with Proposal model - - field :id, !types.ID, "The id of this proposal" - field :title, types.String, "The title of this proposal" - field :description, types.String, "The description of this proposal" - field :question, types.String, "The question of this proposal" - field :external_url, types.String, "External url related to this proposal" - field :author_id, types.Int, "ID of the author of this proposal" - field :flags_count, types.Int, "Number of flags of this proposal" - field :cached_votes_up, types.Int, "Number of upvotes of this proposal" - field :comments_count, types.Int, "Number of comments on this proposal" - field :summary, types.String, "The summary of this proposal" - field :video_url, types.String, "External video url related to this proposal" - field :geozone_id, types.Int, "ID of the geozone affected by this proposal" - field :retired_at, types.String, "Date when this proposal was retired" - field :retired_reason, types.String, "Reason why this proposal was retired" - field :retired_explanation, types.String, "Explanation why this proposal was retired" - - # Linked resources - - field :author, UserType, "Author of this proposal" - - connection :comments, CommentType.connection_type do - description "Comments in this proposal" - end - - field :geozone, GeozoneType, "Geozone affected by this proposal" -end diff --git a/app/graph/types/user_type.rb b/app/graph/types/user_type.rb deleted file mode 100644 index 922f51258..000000000 --- a/app/graph/types/user_type.rb +++ /dev/null @@ -1,29 +0,0 @@ -UserType = GraphQL::ObjectType.define do - name "User" - description "An user entry, returns basic user information" - - # Expose fields associated with User model - - field :id, !types.ID, "The id of this user" - field :created_at, types.String, "Date when this user was created" - field :username, types.String, "The username of this user" - field :geozone_id, types.Int, "The ID of the geozone where this user is active" - field :gender, types.String, "The gender of this user" - field :date_of_birth, types.String, "The birthdate of this user" - - # Linked resources - - field :geozone, GeozoneType, "Geozone where this user is registered" - - connection :proposals, ProposalType.connection_type do - description "Proposals authored by this user" - end - - connection :debates, DebateType.connection_type do - description "Debates authored by this user" - end - - connection :comments, CommentType.connection_type do - description "Comments authored by this user" - end -end From 277c784234ba16da286f4dc87970c392237a3f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 8 Nov 2016 17:40:13 +0100 Subject: [PATCH 030/147] First simple API version with autogenerated GraphQL types --- app/graph/exposable_field.rb | 26 +++++++++++++ app/graph/exposable_model.rb | 31 ++++++++++++++++ app/graph/{types => }/query_root.rb | 17 +++++---- app/graph/type_builder.rb | 57 +++++++++++++++++++++++++++++ config/application.rb | 2 +- 5 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 app/graph/exposable_field.rb create mode 100644 app/graph/exposable_model.rb rename app/graph/{types => }/query_root.rb (77%) create mode 100644 app/graph/type_builder.rb diff --git a/app/graph/exposable_field.rb b/app/graph/exposable_field.rb new file mode 100644 index 000000000..2b54083a7 --- /dev/null +++ b/app/graph/exposable_field.rb @@ -0,0 +1,26 @@ +class ExposableField + attr_reader :name, :graphql_type + + def initialize(column, options = {}) + @name = column.name + @graphql_type = ExposableField.convert_type(column.type) + end + + private + + # Return a GraphQL type for 'database_type' + def self.convert_type(database_type) + case database_type + when :integer + GraphQL::INT_TYPE + when :boolean + GraphQL::BOOLEAN_TYPE + when :float + GraphQL::FLOAT_TYPE + when :double + GraphQL::FLOAT_TYPE + else + GraphQL::STRING_TYPE + end + end +end diff --git a/app/graph/exposable_model.rb b/app/graph/exposable_model.rb new file mode 100644 index 000000000..9e4b228ad --- /dev/null +++ b/app/graph/exposable_model.rb @@ -0,0 +1,31 @@ +class ExposableModel + attr_reader :exposed_fields, :name, :description + + def initialize(model_class, options = {}) + @model_class = model_class + @filter_list = options[:filter_list] || [] + @name = model_class.name + @description = model_class.model_name.human + @filter_strategy = options[:filter_strategy] + set_exposed_fields + end + + private + + # determine which model fields are exposed to the API + def set_exposed_fields + case @filter_strategy + when :whitelist + @exposed_fields = @model_class.columns.select do |column| + @filter_list.include? column.name + end + when :blacklist + @exposed_fields = @model_class.columns.select do |column| + !(@filter_list.include? column.name) + end + else + @exposed_fields = [] + end + end + +end diff --git a/app/graph/types/query_root.rb b/app/graph/query_root.rb similarity index 77% rename from app/graph/types/query_root.rb rename to app/graph/query_root.rb index 0c3ed66b9..928dceb6a 100644 --- a/app/graph/types/query_root.rb +++ b/app/graph/query_root.rb @@ -3,7 +3,7 @@ QueryRoot = GraphQL::ObjectType.define do description "The query root for this schema" field :proposal do - type ProposalType + type TYPE_BUILDER.types[Proposal] description "Find a Proposal by id" argument :id, !types.ID resolve -> (object, arguments, context) { @@ -11,7 +11,8 @@ QueryRoot = GraphQL::ObjectType.define do } end - connection :proposals, ProposalType.connection_type do + field :proposals do + type types[TYPE_BUILDER.types[Proposal]] description "Find all Proposals" resolve -> (object, arguments, context) { Proposal.all @@ -19,7 +20,7 @@ QueryRoot = GraphQL::ObjectType.define do end field :debate do - type DebateType + type TYPE_BUILDER.types[Debate] description "Find a Debate by id" argument :id, !types.ID resolve -> (object, arguments, context) { @@ -27,7 +28,8 @@ QueryRoot = GraphQL::ObjectType.define do } end - connection :debates, DebateType.connection_type do + field :debates do + type types[TYPE_BUILDER.types[Debate]] description "Find all Debates" resolve -> (object, arguments, context) { Debate.all @@ -35,7 +37,7 @@ QueryRoot = GraphQL::ObjectType.define do end field :comment do - type CommentType + type TYPE_BUILDER.types[Comment] description "Find a Comment by id" argument :id, !types.ID resolve -> (object, arguments, context) { @@ -43,7 +45,8 @@ QueryRoot = GraphQL::ObjectType.define do } end - connection :comments, CommentType.connection_type do + field :comments do + type types[TYPE_BUILDER.types[Comment]] description "Find all Comments" resolve -> (object, arguments, context) { Comment.all @@ -51,7 +54,7 @@ QueryRoot = GraphQL::ObjectType.define do end field :user do - type UserType + type TYPE_BUILDER.types[User] description "Find a User by id" argument :id, !types.ID resolve -> (object, arguments, context) { diff --git a/app/graph/type_builder.rb b/app/graph/type_builder.rb new file mode 100644 index 000000000..7de5bfd23 --- /dev/null +++ b/app/graph/type_builder.rb @@ -0,0 +1,57 @@ +class TypeBuilder + attr_reader :filter_strategy, :graphql_models + + def initialize(graphql_models, options = {}) + @graphql_models = graphql_models + @graphql_types = {} + + # determine filter strategy for this field + if (options[:filter_strategy] == :blacklist) + @filter_strategy = :blacklist + else + @filter_strategy = :whitelist + end + + create_all_types + end + + def types + @graphql_types + end + +private + + def create_all_types + @graphql_models.keys.each do |model_class| + @graphql_types[model_class] = create_type(model_class) + end + end + + def create_type(model_class) + type_builder = self + + graphql_type = GraphQL::ObjectType.define do + em = ExposableModel.new(model_class, filter_strategy: type_builder.filter_strategy, filter_list: type_builder.graphql_models[model_class]) + + name(em.name) + description(em.description) + + em.exposed_fields.each do |column| + ef = ExposableField.new(column) + field(ef.name, ef.graphql_type) + end + end + + return graphql_type + end +end + +graphql_models = {} + +graphql_models.store(User, ['id', 'username']) +graphql_models.store(Proposal, ['id', 'title', 'description', 'author_id', 'created_at']) +graphql_models.store(Debate, ['id', 'title', 'description', 'author_id', 'created_at']) +graphql_models.store(Comment, ['id', 'commentable_id', 'commentable_type', 'body']) +graphql_models.store(Geozone, ['id', 'name', 'html_map_coordinates']) + +TYPE_BUILDER = TypeBuilder.new(graphql_models, filter_strategy: :whitelist) diff --git a/config/application.rb b/config/application.rb index 3182e319c..623afc4f1 100644 --- a/config/application.rb +++ b/config/application.rb @@ -46,7 +46,7 @@ module Consul config.paths['app/views'].unshift(Rails.root.join('app', 'views', 'custom')) # Add GraphQL directories to the autoload path - config.autoload_paths << Rails.root.join('app', 'graph', 'types') + config.autoload_paths << Rails.root.join('app', 'graph') end end From 49317e2c110736db802d5a5c513c3438d723f789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 8 Nov 2016 22:19:16 +0100 Subject: [PATCH 031/147] Added support for associations (WITHOUT PAGINATION) --- app/graph/exposable_association.rb | 8 ++++++ app/graph/exposable_model.rb | 27 +++++++++++------- app/graph/type_builder.rb | 46 +++++++++++++++++++++++++----- 3 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 app/graph/exposable_association.rb diff --git a/app/graph/exposable_association.rb b/app/graph/exposable_association.rb new file mode 100644 index 000000000..78e650b0a --- /dev/null +++ b/app/graph/exposable_association.rb @@ -0,0 +1,8 @@ +class ExposableAssociation + attr_reader :name, :type + + def initialize(association) + @name = association.name + @type = association.macro # :has_one, :belongs_to or :has_many + end +end diff --git a/app/graph/exposable_model.rb b/app/graph/exposable_model.rb index 9e4b228ad..22976c63d 100644 --- a/app/graph/exposable_model.rb +++ b/app/graph/exposable_model.rb @@ -1,30 +1,35 @@ class ExposableModel - attr_reader :exposed_fields, :name, :description + attr_reader :name, :description, :exposed_fields, :exposed_associations def initialize(model_class, options = {}) - @model_class = model_class - @filter_list = options[:filter_list] || [] @name = model_class.name @description = model_class.model_name.human + @field_filter_list = options[:field_filter_list] || [] + @assoc_filter_list = options[:assoc_filter_list] || [] @filter_strategy = options[:filter_strategy] - set_exposed_fields + set_exposed_items(model_class) end private - # determine which model fields are exposed to the API - def set_exposed_fields + # determine which model fields and associations are exposed to the API + def set_exposed_items(model_class) + @exposed_fields = check_against_safety_list(model_class.columns, @field_filter_list) + @exposed_associations = check_against_safety_list(model_class.reflect_on_all_associations, @assoc_filter_list) + end + + def check_against_safety_list(all_items, safety_list) case @filter_strategy when :whitelist - @exposed_fields = @model_class.columns.select do |column| - @filter_list.include? column.name + exposed_items = all_items.select do |column| + safety_list.include? column.name.to_s # works for both symbols and strings end when :blacklist - @exposed_fields = @model_class.columns.select do |column| - !(@filter_list.include? column.name) + exposed_items = all_items.select do |column| + !(safety_list.include? column.name.to_s) end else - @exposed_fields = [] + exposed_items = [] end end diff --git a/app/graph/type_builder.rb b/app/graph/type_builder.rb index 7de5bfd23..1277f8746 100644 --- a/app/graph/type_builder.rb +++ b/app/graph/type_builder.rb @@ -1,5 +1,6 @@ class TypeBuilder attr_reader :filter_strategy, :graphql_models + attr_accessor :graphql_types # contains all generated GraphQL types def initialize(graphql_models, options = {}) @graphql_models = graphql_models @@ -31,14 +32,30 @@ private type_builder = self graphql_type = GraphQL::ObjectType.define do - em = ExposableModel.new(model_class, filter_strategy: type_builder.filter_strategy, filter_list: type_builder.graphql_models[model_class]) + em = ExposableModel.new( + model_class, + filter_strategy: type_builder.filter_strategy, + field_filter_list: type_builder.graphql_models[model_class][:fields], + assoc_filter_list: type_builder.graphql_models[model_class][:associations] + ) name(em.name) description(em.description) em.exposed_fields.each do |column| ef = ExposableField.new(column) - field(ef.name, ef.graphql_type) + field(ef.name, ef.graphql_type) # returns a GraphQL::Field + end + + em.exposed_associations.each do |association| + ea = ExposableAssociation.new(association) + if ea.type.in? [:has_one, :belongs_to] + field(ea.name, -> { type_builder.graphql_types[association.klass] }) + elsif ea.type.in? [:has_many] + field(ea.name, -> { + types[type_builder.graphql_types[association.klass]] + }) + end end end @@ -48,10 +65,25 @@ end graphql_models = {} -graphql_models.store(User, ['id', 'username']) -graphql_models.store(Proposal, ['id', 'title', 'description', 'author_id', 'created_at']) -graphql_models.store(Debate, ['id', 'title', 'description', 'author_id', 'created_at']) -graphql_models.store(Comment, ['id', 'commentable_id', 'commentable_type', 'body']) -graphql_models.store(Geozone, ['id', 'name', 'html_map_coordinates']) +graphql_models.store(User, { + fields: ['id', 'username'], + associations: ['proposals', 'debates'] +}) +graphql_models.store(Proposal, { + fields: ['id', 'title', 'description', 'author_id', 'created_at'], + associations: ['author'] +}) +graphql_models.store(Debate, { + fields: ['id', 'title', 'description', 'author_id', 'created_at'], + associations: ['author'] +}) +graphql_models.store(Comment, { + fields: ['id', 'commentable_id', 'commentable_type', 'body'], + associations: ['author'] +}) +graphql_models.store(Geozone, { + fields: ['id', 'name', 'html_map_coordinates'], + associations: [] +}) TYPE_BUILDER = TypeBuilder.new(graphql_models, filter_strategy: :whitelist) From 59a355df1b17df6a8ca05776a961b2bd14514884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 10 Nov 2016 20:53:15 +0100 Subject: [PATCH 032/147] First working version of new GraphQL::TypeCreator after major refactoring --- app/graph/consul_schema.rb | 12 ---- app/graph/exposable_association.rb | 8 --- app/graph/exposable_field.rb | 26 --------- app/graph/exposable_model.rb | 36 ------------ app/graph/query_root.rb | 65 ---------------------- app/graph/type_builder.rb | 89 ------------------------------ config/application.rb | 3 - config/initializers/graphql.rb | 46 +++++++++++++++ lib/graph_ql/type_creator.rb | 42 ++++++++++++++ 9 files changed, 88 insertions(+), 239 deletions(-) delete mode 100644 app/graph/consul_schema.rb delete mode 100644 app/graph/exposable_association.rb delete mode 100644 app/graph/exposable_field.rb delete mode 100644 app/graph/exposable_model.rb delete mode 100644 app/graph/query_root.rb delete mode 100644 app/graph/type_builder.rb create mode 100644 config/initializers/graphql.rb create mode 100644 lib/graph_ql/type_creator.rb diff --git a/app/graph/consul_schema.rb b/app/graph/consul_schema.rb deleted file mode 100644 index 47ef8a112..000000000 --- a/app/graph/consul_schema.rb +++ /dev/null @@ -1,12 +0,0 @@ -ConsulSchema = GraphQL::Schema.define do - query QueryRoot - - # Reject deeply-nested queries - max_depth 7 - - resolve_type -> (object, ctx) { - # look up types by class name - type_name = object.class.name - ConsulSchema.types[type_name] - } -end diff --git a/app/graph/exposable_association.rb b/app/graph/exposable_association.rb deleted file mode 100644 index 78e650b0a..000000000 --- a/app/graph/exposable_association.rb +++ /dev/null @@ -1,8 +0,0 @@ -class ExposableAssociation - attr_reader :name, :type - - def initialize(association) - @name = association.name - @type = association.macro # :has_one, :belongs_to or :has_many - end -end diff --git a/app/graph/exposable_field.rb b/app/graph/exposable_field.rb deleted file mode 100644 index 2b54083a7..000000000 --- a/app/graph/exposable_field.rb +++ /dev/null @@ -1,26 +0,0 @@ -class ExposableField - attr_reader :name, :graphql_type - - def initialize(column, options = {}) - @name = column.name - @graphql_type = ExposableField.convert_type(column.type) - end - - private - - # Return a GraphQL type for 'database_type' - def self.convert_type(database_type) - case database_type - when :integer - GraphQL::INT_TYPE - when :boolean - GraphQL::BOOLEAN_TYPE - when :float - GraphQL::FLOAT_TYPE - when :double - GraphQL::FLOAT_TYPE - else - GraphQL::STRING_TYPE - end - end -end diff --git a/app/graph/exposable_model.rb b/app/graph/exposable_model.rb deleted file mode 100644 index 22976c63d..000000000 --- a/app/graph/exposable_model.rb +++ /dev/null @@ -1,36 +0,0 @@ -class ExposableModel - attr_reader :name, :description, :exposed_fields, :exposed_associations - - def initialize(model_class, options = {}) - @name = model_class.name - @description = model_class.model_name.human - @field_filter_list = options[:field_filter_list] || [] - @assoc_filter_list = options[:assoc_filter_list] || [] - @filter_strategy = options[:filter_strategy] - set_exposed_items(model_class) - end - - private - - # determine which model fields and associations are exposed to the API - def set_exposed_items(model_class) - @exposed_fields = check_against_safety_list(model_class.columns, @field_filter_list) - @exposed_associations = check_against_safety_list(model_class.reflect_on_all_associations, @assoc_filter_list) - end - - def check_against_safety_list(all_items, safety_list) - case @filter_strategy - when :whitelist - exposed_items = all_items.select do |column| - safety_list.include? column.name.to_s # works for both symbols and strings - end - when :blacklist - exposed_items = all_items.select do |column| - !(safety_list.include? column.name.to_s) - end - else - exposed_items = [] - end - end - -end diff --git a/app/graph/query_root.rb b/app/graph/query_root.rb deleted file mode 100644 index 928dceb6a..000000000 --- a/app/graph/query_root.rb +++ /dev/null @@ -1,65 +0,0 @@ -QueryRoot = GraphQL::ObjectType.define do - name "Query" - description "The query root for this schema" - - field :proposal do - type TYPE_BUILDER.types[Proposal] - description "Find a Proposal by id" - argument :id, !types.ID - resolve -> (object, arguments, context) { - Proposal.find(arguments["id"]) - } - end - - field :proposals do - type types[TYPE_BUILDER.types[Proposal]] - description "Find all Proposals" - resolve -> (object, arguments, context) { - Proposal.all - } - end - - field :debate do - type TYPE_BUILDER.types[Debate] - description "Find a Debate by id" - argument :id, !types.ID - resolve -> (object, arguments, context) { - Debate.find(arguments["id"]) - } - end - - field :debates do - type types[TYPE_BUILDER.types[Debate]] - description "Find all Debates" - resolve -> (object, arguments, context) { - Debate.all - } - end - - field :comment do - type TYPE_BUILDER.types[Comment] - description "Find a Comment by id" - argument :id, !types.ID - resolve -> (object, arguments, context) { - Comment.find(arguments["id"]) - } - end - - field :comments do - type types[TYPE_BUILDER.types[Comment]] - description "Find all Comments" - resolve -> (object, arguments, context) { - Comment.all - } - end - - field :user do - type TYPE_BUILDER.types[User] - description "Find a User by id" - argument :id, !types.ID - resolve -> (object, arguments, context) { - User.find(arguments["id"]) - } - end - -end diff --git a/app/graph/type_builder.rb b/app/graph/type_builder.rb deleted file mode 100644 index 1277f8746..000000000 --- a/app/graph/type_builder.rb +++ /dev/null @@ -1,89 +0,0 @@ -class TypeBuilder - attr_reader :filter_strategy, :graphql_models - attr_accessor :graphql_types # contains all generated GraphQL types - - def initialize(graphql_models, options = {}) - @graphql_models = graphql_models - @graphql_types = {} - - # determine filter strategy for this field - if (options[:filter_strategy] == :blacklist) - @filter_strategy = :blacklist - else - @filter_strategy = :whitelist - end - - create_all_types - end - - def types - @graphql_types - end - -private - - def create_all_types - @graphql_models.keys.each do |model_class| - @graphql_types[model_class] = create_type(model_class) - end - end - - def create_type(model_class) - type_builder = self - - graphql_type = GraphQL::ObjectType.define do - em = ExposableModel.new( - model_class, - filter_strategy: type_builder.filter_strategy, - field_filter_list: type_builder.graphql_models[model_class][:fields], - assoc_filter_list: type_builder.graphql_models[model_class][:associations] - ) - - name(em.name) - description(em.description) - - em.exposed_fields.each do |column| - ef = ExposableField.new(column) - field(ef.name, ef.graphql_type) # returns a GraphQL::Field - end - - em.exposed_associations.each do |association| - ea = ExposableAssociation.new(association) - if ea.type.in? [:has_one, :belongs_to] - field(ea.name, -> { type_builder.graphql_types[association.klass] }) - elsif ea.type.in? [:has_many] - field(ea.name, -> { - types[type_builder.graphql_types[association.klass]] - }) - end - end - end - - return graphql_type - end -end - -graphql_models = {} - -graphql_models.store(User, { - fields: ['id', 'username'], - associations: ['proposals', 'debates'] -}) -graphql_models.store(Proposal, { - fields: ['id', 'title', 'description', 'author_id', 'created_at'], - associations: ['author'] -}) -graphql_models.store(Debate, { - fields: ['id', 'title', 'description', 'author_id', 'created_at'], - associations: ['author'] -}) -graphql_models.store(Comment, { - fields: ['id', 'commentable_id', 'commentable_type', 'body'], - associations: ['author'] -}) -graphql_models.store(Geozone, { - fields: ['id', 'name', 'html_map_coordinates'], - associations: [] -}) - -TYPE_BUILDER = TypeBuilder.new(graphql_models, filter_strategy: :whitelist) diff --git a/config/application.rb b/config/application.rb index 623afc4f1..a3d048fef 100644 --- a/config/application.rb +++ b/config/application.rb @@ -44,9 +44,6 @@ module Consul config.autoload_paths << "#{Rails.root}/app/controllers/custom" config.autoload_paths << "#{Rails.root}/app/models/custom" config.paths['app/views'].unshift(Rails.root.join('app', 'views', 'custom')) - - # Add GraphQL directories to the autoload path - config.autoload_paths << Rails.root.join('app', 'graph') end end diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb new file mode 100644 index 000000000..7dec74d12 --- /dev/null +++ b/config/initializers/graphql.rb @@ -0,0 +1,46 @@ +API_TYPE_DEFINITIONS = { + User => %I[ id username ], + Proposal => %I[ id title description author_id author created_at ] +} + +api_types = {} + +API_TYPE_DEFINITIONS.each do |model, fields| + api_types[model] = GraphQL::TypeCreator.create(model, fields, api_types) +end + +ConsulSchema = GraphQL::Schema.define do + query QueryRoot + + # Reject deeply-nested queries + max_depth 7 + + resolve_type -> (object, ctx) { + # look up types by class name + type_name = object.class.name + ConsulSchema.types[type_name] + } +end + +QueryRoot = GraphQL::ObjectType.define do + name "Query" + description "The query root for this schema" + + field :proposal do + type api_types[Proposal] + description "Find a Proposal by id" + argument :id, !types.ID + resolve -> (object, arguments, context) { + Proposal.find(arguments["id"]) + } + end + + field :proposals do + type types[api_types[Proposal]] + description "Find all Proposals" + resolve -> (object, arguments, context) { + Proposal.all + } + end + +end diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb new file mode 100644 index 000000000..44a542ed2 --- /dev/null +++ b/lib/graph_ql/type_creator.rb @@ -0,0 +1,42 @@ +module GraphQL + class TypeCreator + + # Return a GraphQL type for a 'database_type' + TYPES_CONVERSION = Hash.new(GraphQL::STRING_TYPE).merge( + integer: GraphQL::INT_TYPE, + boolean: GraphQL::BOOLEAN_TYPE, + float: GraphQL::FLOAT_TYPE, + double: GraphQL::FLOAT_TYPE + ) + + def self.create(model, field_names, api_types) + + new_graphql_type = GraphQL::ObjectType.define do + + name(model.name) + description("Generated programmatically from model: #{model.name}") + + # Make a field for each column + field_names.each do |field_name| + if model.column_names.include?(field_name.to_s) + field(field_name.to_s, TYPES_CONVERSION[field_name]) + else + association = model.reflect_on_all_associations.find { |a| a.name == field_name } + case association.macro + when :has_one + field(association.name, -> { api_types[association.klass] }) + when :belongs_to + field(association.name, -> { api_types[association.klass] }) + when :has_many + connection(association.name, api_types[association.klass].connection_type { + description "Description for a field with pagination" + }) + end + end + end + end + + return new_graphql_type + end + end +end From dd4b29898cb9e557fb568701833f95343be9524f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 10 Nov 2016 22:38:22 +0100 Subject: [PATCH 033/147] Ran forgotten migration added by my last pull from consul/master --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index a93942873..48e591bf7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -464,7 +464,7 @@ ActiveRecord::Schema.define(version: 20161102133838) do t.boolean "email_digest", default: true t.boolean "email_on_direct_message", default: true t.boolean "official_position_badge", default: false - t.datetime "password_changed_at", default: '2016-11-02 13:51:14', null: false + t.datetime "password_changed_at", default: '2016-11-10 21:34:46', null: false end add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree From 0a23533a02186078aa88923c0c274e941c0f74f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 11 Nov 2016 12:51:45 +0100 Subject: [PATCH 034/147] Refactor creation of the QueryRoot Also incremented the max_depth for nested queries since the use of pagination increases the needed depth to retrieve useful data (edges, nodes, etc.) --- config/initializers/graphql.rb | 39 +++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 7dec74d12..188d515dd 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,5 +1,6 @@ API_TYPE_DEFINITIONS = { - User => %I[ id username ], + User => %I[ id username proposals ], + Debate => %I[ id title description author_id author created_at ], Proposal => %I[ id title description author_id author created_at ] } @@ -13,7 +14,7 @@ ConsulSchema = GraphQL::Schema.define do query QueryRoot # Reject deeply-nested queries - max_depth 7 + max_depth 10 resolve_type -> (object, ctx) { # look up types by class name @@ -26,21 +27,25 @@ QueryRoot = GraphQL::ObjectType.define do name "Query" description "The query root for this schema" - field :proposal do - type api_types[Proposal] - description "Find a Proposal by id" - argument :id, !types.ID - resolve -> (object, arguments, context) { - Proposal.find(arguments["id"]) - } - end + API_TYPE_DEFINITIONS.each_key do |model| - field :proposals do - type types[api_types[Proposal]] - description "Find all Proposals" - resolve -> (object, arguments, context) { - Proposal.all - } - end + # create an entry field to retrive a single object + field model.name.underscore.to_sym do + type api_types[model] + description "Find one #{model.model_name.human} by ID" + argument :id, !types.ID + resolve -> (object, arguments, context) { + model.find(arguments["id"]) + } + end + # create an entry filed to retrive a paginated collection + connection model.name.underscore.pluralize.to_sym, api_types[model].connection_type do + description "Find all #{model.model_name.human.pluralize}" + resolve -> (object, arguments, context) { + model.all + } + end + + end end From fc2d4fda5b682bc58019d78f8bb927702a06aba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 11 Nov 2016 12:52:54 +0100 Subject: [PATCH 035/147] Fixed bug in TypeCreator and improved field descriptions --- lib/graph_ql/type_creator.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 44a542ed2..ae5c65aee 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -14,7 +14,7 @@ module GraphQL new_graphql_type = GraphQL::ObjectType.define do name(model.name) - description("Generated programmatically from model: #{model.name}") + description("#{model.model_name.human}") # Make a field for each column field_names.each do |field_name| @@ -29,7 +29,10 @@ module GraphQL field(association.name, -> { api_types[association.klass] }) when :has_many connection(association.name, api_types[association.klass].connection_type { - description "Description for a field with pagination" + description "#{association.klass.model_name.human.pluralize}" + resolve -> (object, arguments, context) { + association.klass.all + } }) end end From f6cd7a345651388b54912e993d067a36301f507f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 13 Nov 2016 20:07:09 +0100 Subject: [PATCH 036/147] Disable CSRF protection for GraphQL controller Related documentation at: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtec tion/ClassMethods.html --- app/controllers/graphql_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 665e6b93d..f1f4795ae 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -1,5 +1,6 @@ class GraphqlController < ApplicationController + skip_before_action :verify_authenticity_token skip_authorization_check def query From 195d1c5f690525b59c77a27ce10eb222ed788fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 14 Nov 2016 15:11:41 +0100 Subject: [PATCH 037/147] Fixed bug in GraphQL::TypeCreator --- lib/graph_ql/type_creator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index ae5c65aee..c2c03046e 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -19,7 +19,7 @@ module GraphQL # Make a field for each column field_names.each do |field_name| if model.column_names.include?(field_name.to_s) - field(field_name.to_s, TYPES_CONVERSION[field_name]) + field(field_name.to_s, TYPES_CONVERSION[model.columns_hash[field_name.to_s].type]) else association = model.reflect_on_all_associations.find { |a| a.name == field_name } case association.macro From b10b7013193ed409e0d60632dcee63b938d8c5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 14 Nov 2016 16:08:56 +0100 Subject: [PATCH 038/147] Started GraphQL::TypeCreator testing --- config/initializers/graphql.rb | 3 +- spec/lib/graphql_spec.rb | 68 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 spec/lib/graphql_spec.rb diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 188d515dd..843fe2c00 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,7 +1,8 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals ], Debate => %I[ id title description author_id author created_at ], - Proposal => %I[ id title description author_id author created_at ] + Proposal => %I[ id title description author_id author created_at ], + Comment => %I[ id body author_id author commentable_id commentable] } api_types = {} diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb new file mode 100644 index 000000000..2369df348 --- /dev/null +++ b/spec/lib/graphql_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' + +describe GraphQL::TypeCreator do + let!(:api_types) { {} } + let!(:user_type) { GraphQL::TypeCreator.create(User, %I[ id ], api_types) } + let!(:comment_type) { GraphQL::TypeCreator.create(Comment, %I[ id ], api_types) } + let!(:debate_type) { GraphQL::TypeCreator.create(Debate, %I[ id title author ], api_types) } + # TODO: no puedo añadir los comentarios a la field_list de Debate porque como + # las conexiones se crean de forma lazy creo que provoca que falle la creación + # del resto de tipos y provoca que fallen todos los tests. + # let!(:debate_type) { GraphQL::TypeCreator.create(Debate, %I[ id title author comments ], api_types) } + + describe "::create" do + describe "creates fields" do + it "for int attributes" do + created_field = debate_type.fields['id'] + + expect(created_field).to be_a(GraphQL::Field) + expect(created_field.type).to be_a(GraphQL::ScalarType) + expect(created_field.type.name).to eq('Int') + end + + it "for string attributes" do + created_field = debate_type.fields['title'] + + expect(created_field).to be_a(GraphQL::Field) + expect(created_field.type).to be_a(GraphQL::ScalarType) + expect(created_field.type.name).to eq('String') + end + end + + describe "creates connections for" do + it ":belongs_to associations" do + connection = debate_type.fields['author'] + + # TODO: because connection types are created and added lazily to the + # api_types hash (with that proc thing ->) I don't really know how to + # test this. + # connection.class shows GraphQL::Field + # connection.inspect shows some weird info + + expect(connection).to be_a(GraphQL::Field) + # expect(connection.type).to be_a(api_types[User]) + expect(connection.name).to eq('author') + end + + it ":has_one associations" do + skip "need to find association example that uses :has_one" + end + + it ":has_many associations" do + skip "still don't know how to handle relay connections inside RSpec" + + connection = debate_type.fields['comments'] + + # TODO: because connection types are created and added lazily to the + # api_types hash (with that proc thing ->) I don't really know how to + # test this. + # connection.class shows GraphQL::Field + # connection.inspect shows some weird info + + expect(connection).to be_a(GraphQL::Field) + # expect(created_connection.type).to be_a(api_types[Comment]) + expect(connection.name).to eq('comments') + end + end + end +end From 36bdad8851b38e8c0770939b3a599aa9814ce68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 17 Nov 2016 17:23:27 +0100 Subject: [PATCH 039/147] Fixed bug in GraphQL::TypeCreator --- lib/graph_ql/type_creator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index c2c03046e..848d92704 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -28,12 +28,12 @@ module GraphQL when :belongs_to field(association.name, -> { api_types[association.klass] }) when :has_many - connection(association.name, api_types[association.klass].connection_type { + connection(association.name, -> { api_types[association.klass].connection_type { description "#{association.klass.model_name.human.pluralize}" resolve -> (object, arguments, context) { association.klass.all } - }) + }}) end end end From 6fca1f02feb48f148e3ba3156ad43a90b085d5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 17 Nov 2016 17:23:57 +0100 Subject: [PATCH 040/147] Added clarifying comments related to GraphQL return types --- app/controllers/graphql_controller.rb | 2 ++ lib/graph_ql/type_creator.rb | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index f1f4795ae..e3bedceea 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -4,6 +4,8 @@ class GraphqlController < ApplicationController skip_authorization_check def query + # ConsulSchema.execute returns the query result in the shape of a Hash, which + # is sent back to the client rendered in JSON render json: ConsulSchema.execute( params[:query], variables: params[:variables] || {} diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 848d92704..5a831fd7d 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -38,8 +38,7 @@ module GraphQL end end end - - return new_graphql_type + return new_graphql_type # GraphQL::ObjectType end end end From fc61d63affe3dc4a7095fc84976347899eed500e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 17 Nov 2016 17:25:27 +0100 Subject: [PATCH 041/147] Fixed bug for the Comment element inside the API_TYPE_DEFINITIONS --- config/initializers/graphql.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 843fe2c00..06b39aaab 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -2,7 +2,7 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals ], Debate => %I[ id title description author_id author created_at ], Proposal => %I[ id title description author_id author created_at ], - Comment => %I[ id body author_id author commentable_id commentable] + Comment => %I[ id body user_id user commentable_id ] } api_types = {} From 90d5034174f320343db986ab11a0e991d25c5f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 17 Nov 2016 17:47:09 +0100 Subject: [PATCH 042/147] Half-made tests for GraphqlController --- spec/controllers/graphql_controller_spec.rb | 62 +++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 spec/controllers/graphql_controller_spec.rb diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb new file mode 100644 index 000000000..8afc8e60b --- /dev/null +++ b/spec/controllers/graphql_controller_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +describe GraphqlController do + let!(:uri) { URI::HTTP.build(host: 'localhost', path: '/queries', port: 3000) } + + describe "GET request" do + it "is accepted when valid" do + # Like POST requests but the query string goes in the URL + # More info at: http://graphql.org/learn/serving-over-http/#get-request + skip + end + + it "is rejected when not valid" do + skip + end + end + + describe "POST request" do + let(:post_request) { + req = Net::HTTP::Post.new(uri) + req['Content-Type'] = 'application/json' + req + } + + it "succeeds when valid" do + body = { query: "{ proposals(first: 2) { edges { node { id } } } }" }.to_json + response = Net::HTTP.start(uri.host, uri.port) do |http| + post_request.body = body + http.request(post_request) + end + + # Is it enough to check the status code or should I also check the body? + expect(response.code.to_i).to eq(200) + end + + it "succeeds and returns an error when disclosed attributes are requested" do + body = { query: "{ user(id: 1) { encrypted_password } }" }.to_json + response = Net::HTTP.start(uri.host, uri.port) do |http| + post_request.body = body + http.request(post_request) + end + + body_hash = JSON.parse(response.body) + expect(body_hash['errors']).to be_present + end + + it "fails when no query string is provided" do + body = {}.to_json + response = Net::HTTP.start(uri.host, uri.port) do |http| + post_request.body = body + http.request(post_request) + end + + # TODO: I must find a way to handle this better. Right now it shows a 500 + # Internal Server Error, I think I should always return a valid (but empty) + # JSON document like '{}' + expect(response.code.to_i).not_to eq(200) + end + + end + +end From 96bfbbf9af55bca7ed909c5be7ade11dc137cad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 17 Nov 2016 17:48:03 +0100 Subject: [PATCH 043/147] Half-made tests for GraphQL::TypeCreator --- spec/lib/{graphql_spec.rb => type_creator_spec.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/lib/{graphql_spec.rb => type_creator_spec.rb} (100%) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/type_creator_spec.rb similarity index 100% rename from spec/lib/graphql_spec.rb rename to spec/lib/type_creator_spec.rb From 4e2a003931a48bdbae907115f0d76e851bd2dc44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 17 Nov 2016 17:54:02 +0100 Subject: [PATCH 044/147] Half-made tests for ConsulSchema --- spec/lib/graphql_spec.rb | 56 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 spec/lib/graphql_spec.rb diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb new file mode 100644 index 000000000..5f7ab34bb --- /dev/null +++ b/spec/lib/graphql_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' + +describe ConsulSchema do + let(:context) { {} } # should be overriden for specific queries + let(:variables) { {} } + let(:result) { ConsulSchema.execute(query_string, context: context, variables: variables) } + + describe "answers simple queries" do + let(:query_string) { "{ proposals { edges { node { title } } } }" } + let(:context) { + { proposal: create(:proposal, title: "A new proposal") } + } + + it "can return proposal titles" do + proposals = result["data"]["proposals"]["edges"].collect { |edge| edge['node'] } + expect(proposals.size).to eq(1) + expect(proposals.first['title']).to eq("A new proposal") + end + end + + describe "queries with nested associations" do + let(:query_string) { "{ proposals { edges { node { id, title, author { username } } } } }" } + + let(:mrajoy) { create(:user, username: 'mrajoy') } + let(:dtrump) { create(:user, username: 'dtrump') } + let(:context) { + { proposal_1: create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } + { proposal_2: create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } + { proposal_3: create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } + } + + it "returns the appropiate fields" do + proposals = result["data"]["proposals"]["edges"].collect { |edge| edge['node'] } + + expected_result = [ + { + 'id' => 1, + 'title' => 'Bajar el IVA', + 'author' => { 'username' => 'mrajoy' } + }, + { + 'id' => 2, + 'title' => 'Censurar los memes', + 'author' => { 'username' => 'mrajoy' } + }, + { + 'id' => 3, + 'title' => 'Construir un muro', + 'author' => { 'username' => 'dtrump' } + } + ] + + expect(proposals).to match_array(expected_result) + end + end +end From 700fcaab168255853277936fa2d96f0c70610bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 18 Nov 2016 16:24:41 +0100 Subject: [PATCH 045/147] Expose proposal/debate comments in the API so I can test :has_many associations --- config/initializers/graphql.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 06b39aaab..1f0c8968d 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,7 +1,7 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals ], - Debate => %I[ id title description author_id author created_at ], - Proposal => %I[ id title description author_id author created_at ], + Debate => %I[ id title description author_id author created_at comments ], + Proposal => %I[ id title description author_id author created_at comments ], Comment => %I[ id body user_id user commentable_id ] } From 860704d908c54cf711581b4c33ec2386a78822d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 18 Nov 2016 16:27:15 +0100 Subject: [PATCH 046/147] Rewrote tests to tackle more specific stuff --- spec/lib/graphql_spec.rb | 64 ++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 5f7ab34bb..f14242dd6 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -5,16 +5,62 @@ describe ConsulSchema do let(:variables) { {} } let(:result) { ConsulSchema.execute(query_string, context: context, variables: variables) } - describe "answers simple queries" do - let(:query_string) { "{ proposals { edges { node { title } } } }" } - let(:context) { - { proposal: create(:proposal, title: "A new proposal") } - } + describe "queries to single elements" do + let(:proposal) { create(:proposal, title: 'Proposal Title') } + let(:query_string) { "{ proposal(id: #{proposal.id}) { id, title } }" } - it "can return proposal titles" do - proposals = result["data"]["proposals"]["edges"].collect { |edge| edge['node'] } - expect(proposals.size).to eq(1) - expect(proposals.first['title']).to eq("A new proposal") + it "returns fields of numeric type" do + returned_proposal = result['data']['proposal'] + expect(returned_proposal['id']).to eq(proposal.id) + end + + it "returns fields of String type" do + returned_proposal = result['data']['proposal'] + expect(returned_proposal['title']).to eq('Proposal Title') + end + + describe "supports nested queries" do + it "with :has_one associations" do + skip "I think this test isn't needed" + # TODO: the only has_one associations inside the project are in the User + # model (administrator, valuator, etc.). But since I think this data + # shouldn't be exposed to the API, there's no point in testing this. + # + # user = create(:user) + # admin = create(:administrator) + # user.administrator = admin + # query_string = "{ user(id: #{user.id}) { administrator { id } } }" + # + # result = ConsulSchema.execute(query_string, context: {}, variables: {}) + # returned_admin = result['data']['user']['administrator'] + # + # expect(returned_admin.id).to eq(admin.id) + end + + it "with :belongs_to associations" do + user = create(:user) + proposal = create(:proposal, author: user) + query_string = "{ proposal(id: #{proposal.id}) { author { username } } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + returned_proposal = result['data']['proposal'] + + expect(returned_proposal['author']['username']).to eq(user.username) + end + + it "with :has_many associations" do + user = create(:user) + proposal = create(:proposal) + create(:comment, body: "Blah Blah", author: user, commentable: proposal) + create(:comment, body: "I told ya", author: user, commentable: proposal) + query_string = "{ proposal(id: #{proposal.id}) { comments { edges { node { body } } } } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + comments = result['data']['proposal']['comments']['edges'].collect { |edge| edge['node'] } + comment_bodies = comments.collect { |comment| comment['body'] } + + expect(comment_bodies).to match_array(["Blah Blah", "I told ya"]) + end end end From 7bb17781248f848e455b4b75f7eefa4869b961db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 18 Nov 2016 19:57:07 +0100 Subject: [PATCH 047/147] Finished tests for GraphQL queries for single resources --- spec/lib/graphql_spec.rb | 71 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index f14242dd6..c0d2fbdfc 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -20,6 +20,8 @@ describe ConsulSchema do end describe "supports nested queries" do + let(:user) { create(:user) } + it "with :has_one associations" do skip "I think this test isn't needed" # TODO: the only has_one associations inside the project are in the User @@ -38,7 +40,6 @@ describe ConsulSchema do end it "with :belongs_to associations" do - user = create(:user) proposal = create(:proposal, author: user) query_string = "{ proposal(id: #{proposal.id}) { author { username } } }" @@ -49,7 +50,6 @@ describe ConsulSchema do end it "with :has_many associations" do - user = create(:user) proposal = create(:proposal) create(:comment, body: "Blah Blah", author: user, commentable: proposal) create(:comment, body: "I told ya", author: user, commentable: proposal) @@ -62,9 +62,72 @@ describe ConsulSchema do expect(comment_bodies).to match_array(["Blah Blah", "I told ya"]) end end + + describe "does not expose confidential" do + let(:user) { create(:user) } + + it "Int fields" do + query_string = "{ user(id: #{user.id}) { failed_census_calls_count } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + + expect(result['data']).to be_nil + expect(result['errors'].first['message']).to eq("Field 'failed_census_calls_count' doesn't exist on type 'User'") + end + + it "String fields" do + query_string = "{ user(id: #{user.id}) { encrypted_password } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + + expect(result['data']).to be_nil + expect(result['errors'].first['message']).to eq("Field 'encrypted_password' doesn't exist on type 'User'") + end + + it ":has_one associations" do + user.administrator = create(:administrator) + query_string = "{ user(id: #{user.id}) { administrator { id } } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + + expect(result['data']).to be_nil + expect(result['errors'].first['message']).to eq("Field 'administrator' doesn't exist on type 'User'") + end + + it ":belongs_to associations" do + create(:failed_census_call, user: user) + + query_string = "{ user(id: #{user.id}) { failed_census_calls { id } } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + + expect(result['data']).to be_nil + expect(result['errors'].first['message']).to eq("Field 'failed_census_calls' doesn't exist on type 'User'") + end + + it ":has_many associations" do + create(:direct_message, sender: user) + query_string = "{ user(id: #{user.id}) { direct_messages_sent { id } } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + + expect(result['data']).to be_nil + expect(result['errors'].first['message']).to eq("Field 'direct_messages_sent' doesn't exist on type 'User'") + end + + it "fields inside nested queries" do + proposal = create(:proposal, author: user) + query_string = "{ proposal(id: #{proposal.id}) { author { reset_password_sent_at } } }" + + result = ConsulSchema.execute(query_string, context: {}, variables: {}) + + expect(result['data']).to be_nil + expect(result['errors'].first['message']).to eq("Field 'reset_password_sent_at' doesn't exist on type 'User'") + end + end end - describe "queries with nested associations" do + describe "queries to collections" do let(:query_string) { "{ proposals { edges { node { id, title, author { username } } } } }" } let(:mrajoy) { create(:user, username: 'mrajoy') } @@ -75,7 +138,7 @@ describe ConsulSchema do { proposal_3: create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } } - it "returns the appropiate fields" do + it "return the appropiate fields" do proposals = result["data"]["proposals"]["edges"].collect { |edge| edge['node'] } expected_result = [ From 2271f6d634050ef9c5e9e8767cf0f40272bb7751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 22 Nov 2016 15:22:27 +0100 Subject: [PATCH 048/147] Renamed GraphQL route, added support for GET requests --- config/routes.rb | 6 +++--- spec/controllers/graphql_controller_spec.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index b86cb0c30..63be98384 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -263,9 +263,9 @@ Rails.application.routes.draw do end # GraphQL - mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/queries" - post '/queries', to: 'graphql#query' - + mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql' + get '/graphql', to: 'graphql#query' + post '/graphql', to: 'graphql#query' if Rails.env.development? mount LetterOpenerWeb::Engine, at: "/letter_opener" diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index 8afc8e60b..bf4bb935e 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' describe GraphqlController do - let!(:uri) { URI::HTTP.build(host: 'localhost', path: '/queries', port: 3000) } + let!(:uri) { URI::HTTP.build(host: 'localhost', path: '/graphql', port: 3000) } describe "GET request" do it "is accepted when valid" do From 13273f4bc2dfe992ead940e33f979b37c26b835e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 22 Nov 2016 23:49:05 +0100 Subject: [PATCH 049/147] DRYed specs --- spec/lib/graphql_spec.rb | 172 +++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 98 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index c0d2fbdfc..00505ac1c 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -7,122 +7,98 @@ describe ConsulSchema do describe "queries to single elements" do let(:proposal) { create(:proposal, title: 'Proposal Title') } - let(:query_string) { "{ proposal(id: #{proposal.id}) { id, title } }" } - it "returns fields of numeric type" do - returned_proposal = result['data']['proposal'] - expect(returned_proposal['id']).to eq(proposal.id) + describe "can returns fields of" do + let(:query_string) { "{ proposal(id: #{proposal.id}) { id, title } }" } + subject(:returned_proposal) { result['data']['proposal'] } + + it "numeric type" do + expect(returned_proposal['id']).to eq(proposal.id) + end + + it "String type" do + expect(returned_proposal['title']).to eq(proposal.title) + end end - it "returns fields of String type" do - returned_proposal = result['data']['proposal'] - expect(returned_proposal['title']).to eq('Proposal Title') - end + describe "support nested" do + let(:proposal_author) { create(:user) } + let(:comments_author) { create(:user) } + let(:proposal) { create(:proposal, author: proposal_author) } + let!(:comment_1) { create(:comment, author: comments_author, commentable: proposal) } + let!(:comment_2) { create(:comment, author: comments_author, commentable: proposal) } + let(:query_string) { "{ proposal(id: #{proposal.id}) { author { username }, comments { edges { node { body } } } } }" } + subject(:returned_proposal) { result['data']['proposal'] } - describe "supports nested queries" do - let(:user) { create(:user) } - - it "with :has_one associations" do + it ":has_one associations" do skip "I think this test isn't needed" # TODO: the only has_one associations inside the project are in the User # model (administrator, valuator, etc.). But since I think this data # shouldn't be exposed to the API, there's no point in testing this. - # - # user = create(:user) - # admin = create(:administrator) - # user.administrator = admin - # query_string = "{ user(id: #{user.id}) { administrator { id } } }" - # - # result = ConsulSchema.execute(query_string, context: {}, variables: {}) - # returned_admin = result['data']['user']['administrator'] - # - # expect(returned_admin.id).to eq(admin.id) - end - - it "with :belongs_to associations" do - proposal = create(:proposal, author: user) - query_string = "{ proposal(id: #{proposal.id}) { author { username } } }" - - result = ConsulSchema.execute(query_string, context: {}, variables: {}) - returned_proposal = result['data']['proposal'] - - expect(returned_proposal['author']['username']).to eq(user.username) - end - - it "with :has_many associations" do - proposal = create(:proposal) - create(:comment, body: "Blah Blah", author: user, commentable: proposal) - create(:comment, body: "I told ya", author: user, commentable: proposal) - query_string = "{ proposal(id: #{proposal.id}) { comments { edges { node { body } } } } }" - - result = ConsulSchema.execute(query_string, context: {}, variables: {}) - comments = result['data']['proposal']['comments']['edges'].collect { |edge| edge['node'] } - comment_bodies = comments.collect { |comment| comment['body'] } - - expect(comment_bodies).to match_array(["Blah Blah", "I told ya"]) - end - end - - describe "does not expose confidential" do - let(:user) { create(:user) } - - it "Int fields" do - query_string = "{ user(id: #{user.id}) { failed_census_calls_count } }" - - result = ConsulSchema.execute(query_string, context: {}, variables: {}) - - expect(result['data']).to be_nil - expect(result['errors'].first['message']).to eq("Field 'failed_census_calls_count' doesn't exist on type 'User'") - end - - it "String fields" do - query_string = "{ user(id: #{user.id}) { encrypted_password } }" - - result = ConsulSchema.execute(query_string, context: {}, variables: {}) - - expect(result['data']).to be_nil - expect(result['errors'].first['message']).to eq("Field 'encrypted_password' doesn't exist on type 'User'") - end - - it ":has_one associations" do - user.administrator = create(:administrator) - query_string = "{ user(id: #{user.id}) { administrator { id } } }" - - result = ConsulSchema.execute(query_string, context: {}, variables: {}) - - expect(result['data']).to be_nil - expect(result['errors'].first['message']).to eq("Field 'administrator' doesn't exist on type 'User'") end it ":belongs_to associations" do - create(:failed_census_call, user: user) - - query_string = "{ user(id: #{user.id}) { failed_census_calls { id } } }" - - result = ConsulSchema.execute(query_string, context: {}, variables: {}) - - expect(result['data']).to be_nil - expect(result['errors'].first['message']).to eq("Field 'failed_census_calls' doesn't exist on type 'User'") + expect(returned_proposal['author']['username']).to eq(proposal_author.username) end it ":has_many associations" do - create(:direct_message, sender: user) - query_string = "{ user(id: #{user.id}) { direct_messages_sent { id } } }" + comments = returned_proposal['comments']['edges'].collect { |edge| edge['node'] } + comment_bodies = comments.collect { |comment| comment['body'] } - result = ConsulSchema.execute(query_string, context: {}, variables: {}) + expect(comment_bodies).to match_array([comment_1.body, comment_2.body]) + end + end - expect(result['data']).to be_nil - expect(result['errors'].first['message']).to eq("Field 'direct_messages_sent' doesn't exist on type 'User'") + describe "do not expose confidential" do + let(:user) { create(:user) } + subject(:data) { result['data'] } + subject(:errors) { result['errors'] } + subject(:error_msg) { errors.first['message'] } + + describe "fields of Int type" do + let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls_count } }" } + + specify { expect(data).to be_nil } + specify { expect(error_msg).to eq("Field 'failed_census_calls_count' doesn't exist on type 'User'") } end - it "fields inside nested queries" do - proposal = create(:proposal, author: user) - query_string = "{ proposal(id: #{proposal.id}) { author { reset_password_sent_at } } }" + describe "fields of String type" do + let(:query_string) { "{ user(id: #{user.id}) { encrypted_password } }" } - result = ConsulSchema.execute(query_string, context: {}, variables: {}) + specify { expect(data).to be_nil } + specify { expect(error_msg).to eq("Field 'encrypted_password' doesn't exist on type 'User'") } + end - expect(result['data']).to be_nil - expect(result['errors'].first['message']).to eq("Field 'reset_password_sent_at' doesn't exist on type 'User'") + describe "fields inside nested queries" do + let(:proposal) { create(:proposal, author: user) } + let(:query_string) { "{ proposal(id: #{proposal.id}) { author { reset_password_sent_at } } }" } + + specify { expect(data).to be_nil } + specify { expect(error_msg).to eq("Field 'reset_password_sent_at' doesn't exist on type 'User'") } + end + + describe ":has_one associations" do + let(:administrator) { create(:administrator) } + let(:query_string) { "{ user(id: #{user.id}) { administrator { id } } }" } + + specify { expect(data).to be_nil } + specify { expect(error_msg).to eq("Field 'administrator' doesn't exist on type 'User'") } + end + + describe ":belongs_to associations" do + let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls { id } } }" } + let(:census_call) { create(:failed_census_call, user: user) } + + specify { expect(data).to be_nil } + specify { expect(error_msg).to eq("Field 'failed_census_calls' doesn't exist on type 'User'") } + end + + describe ":has_many associations" do + let(:message) { create(:direct_message, sender: user) } + let(:query_string) { "{ user(id: #{user.id}) { direct_messages_sent { id } } }" } + + specify { expect(data).to be_nil } + specify { expect(error_msg).to eq("Field 'direct_messages_sent' doesn't exist on type 'User'") } end end end @@ -132,11 +108,11 @@ describe ConsulSchema do let(:mrajoy) { create(:user, username: 'mrajoy') } let(:dtrump) { create(:user, username: 'dtrump') } - let(:context) { + let(:context) do { proposal_1: create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } { proposal_2: create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } { proposal_3: create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } - } + end it "return the appropiate fields" do proposals = result["data"]["proposals"]["edges"].collect { |edge| edge['node'] } From 027bdf3f1601fb3b531a9043d34b545bf0621ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 23 Nov 2016 00:16:01 +0100 Subject: [PATCH 050/147] DRYed specs --- spec/lib/graphql_spec.rb | 67 +++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 00505ac1c..8e2679f8f 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -6,19 +6,17 @@ describe ConsulSchema do let(:result) { ConsulSchema.execute(query_string, context: context, variables: variables) } describe "queries to single elements" do - let(:proposal) { create(:proposal, title: 'Proposal Title') } + let(:proposal) { create(:proposal) } + subject(:returned_proposal) { result['data']['proposal'] } - describe "can returns fields of" do - let(:query_string) { "{ proposal(id: #{proposal.id}) { id, title } }" } - subject(:returned_proposal) { result['data']['proposal'] } + describe "return fields of Int type" do + let(:query_string) { "{ proposal(id: #{proposal.id}) { id } }" } + specify { expect(returned_proposal['id']).to eq(proposal.id) } + end - it "numeric type" do - expect(returned_proposal['id']).to eq(proposal.id) - end - - it "String type" do - expect(returned_proposal['title']).to eq(proposal.title) - end + describe "return fields of String type" do + let(:query_string) { "{ proposal(id: #{proposal.id}) { title } }" } + specify { expect(returned_proposal['title']).to eq(proposal.title) } end describe "support nested" do @@ -28,7 +26,6 @@ describe ConsulSchema do let!(:comment_1) { create(:comment, author: comments_author, commentable: proposal) } let!(:comment_2) { create(:comment, author: comments_author, commentable: proposal) } let(:query_string) { "{ proposal(id: #{proposal.id}) { author { username }, comments { edges { node { body } } } } }" } - subject(:returned_proposal) { result['data']['proposal'] } it ":has_one associations" do skip "I think this test isn't needed" @@ -104,38 +101,32 @@ describe ConsulSchema do end describe "queries to collections" do - let(:query_string) { "{ proposals { edges { node { id, title, author { username } } } } }" } - let(:mrajoy) { create(:user, username: 'mrajoy') } let(:dtrump) { create(:user, username: 'dtrump') } - let(:context) do - { proposal_1: create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } - { proposal_2: create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } - { proposal_3: create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } + let!(:proposal_1) { create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } + let!(:proposal_2) { create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } + let!(:proposal_3) { create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } + subject(:returned_proposals) { result['data']['proposals']["edges"].collect { |edge| edge['node'] } } + + describe "return fields of Int type" do + let(:query_string) { "{ proposals { edges { node { id } } } }" } + let(:ids) { returned_proposals.collect { |proposal| proposal['id'] } } + + specify { expect(ids).to match_array([3, 1, 2]) } end - it "return the appropiate fields" do - proposals = result["data"]["proposals"]["edges"].collect { |edge| edge['node'] } + describe "return fields of String type" do + let(:query_string) { "{ proposals { edges { node { title } } } }" } + let(:titles) { returned_proposals.collect { |proposal| proposal['title'] } } - expected_result = [ - { - 'id' => 1, - 'title' => 'Bajar el IVA', - 'author' => { 'username' => 'mrajoy' } - }, - { - 'id' => 2, - 'title' => 'Censurar los memes', - 'author' => { 'username' => 'mrajoy' } - }, - { - 'id' => 3, - 'title' => 'Construir un muro', - 'author' => { 'username' => 'dtrump' } - } - ] + specify { expect(titles).to match_array(['Construir un muro', 'Censurar los memes', 'Bajar el IVA']) } + end - expect(proposals).to match_array(expected_result) + describe "return nested fields" do + let(:query_string) { "{ proposals { edges { node { author { username } } } } }" } + let(:authors) { returned_proposals.collect { |proposal| proposal['author']['username'] } } + + specify { expect(authors).to match_array(['mrajoy', 'dtrump', 'mrajoy']) } end end end From 6230712b7433092cc33696e8c5dc78ea85e5b087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 25 Nov 2016 00:56:26 +0100 Subject: [PATCH 051/147] Fixed bug in my schema.rb --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 48e591bf7..a93942873 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -464,7 +464,7 @@ ActiveRecord::Schema.define(version: 20161102133838) do t.boolean "email_digest", default: true t.boolean "email_on_direct_message", default: true t.boolean "official_position_badge", default: false - t.datetime "password_changed_at", default: '2016-11-10 21:34:46', null: false + t.datetime "password_changed_at", default: '2016-11-02 13:51:14', null: false end add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree From 9ed51cf477514d9641cf447c2e48b5cfee188a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 25 Nov 2016 01:02:05 +0100 Subject: [PATCH 052/147] Useless commit --- spec/controllers/graphql_controller_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index bf4bb935e..3504de501 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -12,6 +12,7 @@ describe GraphqlController do it "is rejected when not valid" do skip + # Just doing this to trigger Travis CI build end end From 0eae2fada628f37399468d3aa91e1cd39230f5b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 27 Nov 2016 13:50:01 +0100 Subject: [PATCH 053/147] Refactor GraphQL controller specs --- Gemfile | 1 + Gemfile.lock | 15 +++ config/routes.rb | 1 - spec/controllers/graphql_controller_spec.rb | 100 +++++++++++--------- 4 files changed, 71 insertions(+), 46 deletions(-) diff --git a/Gemfile b/Gemfile index 6d7f660f3..9180e62fb 100644 --- a/Gemfile +++ b/Gemfile @@ -96,6 +96,7 @@ group :test do gem 'poltergeist' gem 'coveralls', require: false gem 'email_spec' + gem 'http' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 9638fae74..eaef01916 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -143,6 +143,8 @@ GEM railties (>= 3.2.6, < 5.0) diff-lcs (1.2.5) docile (1.1.5) + domain_name (0.5.20161021) + unf (>= 0.0.5, < 1.0.0) easy_translate (0.5.0) json thread @@ -189,6 +191,15 @@ GEM hashie (3.4.3) highline (1.7.8) htmlentities (4.3.4) + http (2.1.0) + addressable (~> 2.3) + http-cookie (~> 1.0) + http-form_data (~> 1.0.1) + http_parser.rb (~> 0.6.0) + http-cookie (1.0.3) + domain_name (~> 0.5) + http-form_data (1.0.1) + http_parser.rb (0.6.0) httpi (2.4.1) rack i18n (0.7.0) @@ -423,6 +434,9 @@ GEM thread_safe (~> 0.1) uglifier (3.0.3) execjs (>= 0.3.0, < 3) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.2) unicode-display_width (1.1.1) unicorn (5.1.0) kgio (~> 2.6) @@ -484,6 +498,7 @@ DEPENDENCIES graphiql-rails graphql groupdate (~> 3.1.0) + http i18n-tasks initialjs-rails (= 0.2.0.4) invisible_captcha (~> 0.9.1) diff --git a/config/routes.rb b/config/routes.rb index 63be98384..b530df52c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -264,7 +264,6 @@ Rails.application.routes.draw do # GraphQL mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql' - get '/graphql', to: 'graphql#query' post '/graphql', to: 'graphql#query' if Rails.env.development? diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index 3504de501..2b53cd0fb 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -1,63 +1,73 @@ require 'rails_helper' +require 'http' describe GraphqlController do - let!(:uri) { URI::HTTP.build(host: 'localhost', path: '/graphql', port: 3000) } + let(:uri) { URI::HTTP.build(host: 'localhost', path: '/graphql', port: 3000) } + let(:query_string) { "" } + let(:body) { {query: query_string}.to_json } - describe "GET request" do - it "is accepted when valid" do - # Like POST requests but the query string goes in the URL - # More info at: http://graphql.org/learn/serving-over-http/#get-request - skip - end + describe "POST requests" do + let(:author) { create(:user) } + let(:proposal) { create(:proposal, author: author) } + let(:response) { HTTP.headers('Content-Type' => 'application/json').post(uri, body: body) } + let(:response_body) { JSON.parse(response.body) } - it "is rejected when not valid" do - skip - # Just doing this to trigger Travis CI build - end - end + context "when query string is valid" do + let(:query_string) { "{ proposal(id: #{proposal.id}) { title, author { username } } }" } + let(:returned_proposal) { response_body['data']['proposal'] } - describe "POST request" do - let(:post_request) { - req = Net::HTTP::Post.new(uri) - req['Content-Type'] = 'application/json' - req - } - - it "succeeds when valid" do - body = { query: "{ proposals(first: 2) { edges { node { id } } } }" }.to_json - response = Net::HTTP.start(uri.host, uri.port) do |http| - post_request.body = body - http.request(post_request) + it "returns HTTP 200 OK" do + expect(response.code).to eq(200) end - # Is it enough to check the status code or should I also check the body? - expect(response.code.to_i).to eq(200) - end - - it "succeeds and returns an error when disclosed attributes are requested" do - body = { query: "{ user(id: 1) { encrypted_password } }" }.to_json - response = Net::HTTP.start(uri.host, uri.port) do |http| - post_request.body = body - http.request(post_request) + it "returns first-level fields" do + expect(returned_proposal['title']).to eq(proposal.title) end - body_hash = JSON.parse(response.body) - expect(body_hash['errors']).to be_present + it "returns nested fields" do + expect(returned_proposal['author']['username']).to eq(author.username) + end end - it "fails when no query string is provided" do - body = {}.to_json - response = Net::HTTP.start(uri.host, uri.port) do |http| - post_request.body = body - http.request(post_request) + context "when query string asks for invalid fields" do + let(:query_string) { "{ proposal(id: #{proposal.id}) { missing_field } }" } + + it "returns HTTP 200 OK" do + expect(response.code).to eq(200) end - # TODO: I must find a way to handle this better. Right now it shows a 500 - # Internal Server Error, I think I should always return a valid (but empty) - # JSON document like '{}' - expect(response.code.to_i).not_to eq(200) + it "doesn't return any data" do + expect(response_body['data']).to be_nil + end + + it "returns error inside body" do + expect(response_body['errors']).to be_present + end + end + + context "when query string is not valid" do + let(:query_string) { "invalid" } + + it "returns HTTP 400 Bad Request" do + expect(response.code).to eq(400) + end + end + + context "when query string is missing" do + let(:query_string) { nil } + + it "returns HTTP 400 Bad Request" do + expect(response.code).to eq(400) + end + end + + context "when body is missing" do + let(:body) { nil } + + it "returns HTTP 400 Bad Request" do + expect(response.code).to eq(400) + end end end - end From 76dd11631aab8f477c46c151db05861f04a3c32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 30 Nov 2016 13:38:13 +0100 Subject: [PATCH 054/147] Fixes error initializing database --- app/models/concerns/measurable.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/measurable.rb b/app/models/concerns/measurable.rb index 44b7ddabb..5ac2f2a14 100644 --- a/app/models/concerns/measurable.rb +++ b/app/models/concerns/measurable.rb @@ -4,11 +4,11 @@ module Measurable class_methods do def title_max_length - @@title_max_length ||= self.columns.find { |c| c.name == 'title' }.limit || 80 + @@title_max_length ||= (self.columns.find { |c| c.name == 'title' }.limit rescue nil) || 80 end def responsible_name_max_length - @@responsible_name_max_length ||= self.columns.find { |c| c.name == 'responsible_name' }.limit || 60 + @@responsible_name_max_length ||= (self.columns.find { |c| c.name == 'responsible_name' }.limit rescue nil) || 60 end def question_max_length @@ -21,4 +21,4 @@ module Measurable end -end \ No newline at end of file +end From d1e1ee9d4159d43c00cd8a4e5a5383d95427230a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 30 Nov 2016 13:43:10 +0100 Subject: [PATCH 055/147] Moved api_types into TypeCreator --- config/initializers/graphql.rb | 10 +++++----- lib/graph_ql/type_creator.rb | 31 +++++++++++++++++++------------ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 1f0c8968d..3918d794a 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -5,10 +5,10 @@ API_TYPE_DEFINITIONS = { Comment => %I[ id body user_id user commentable_id ] } -api_types = {} +type_creator = GraphQL::TypeCreator.new API_TYPE_DEFINITIONS.each do |model, fields| - api_types[model] = GraphQL::TypeCreator.create(model, fields, api_types) + type_creator.create(model, fields) end ConsulSchema = GraphQL::Schema.define do @@ -28,11 +28,11 @@ QueryRoot = GraphQL::ObjectType.define do name "Query" description "The query root for this schema" - API_TYPE_DEFINITIONS.each_key do |model| + type_creator.created_types.each do |model, created_type| # create an entry field to retrive a single object field model.name.underscore.to_sym do - type api_types[model] + type created_type description "Find one #{model.model_name.human} by ID" argument :id, !types.ID resolve -> (object, arguments, context) { @@ -41,7 +41,7 @@ QueryRoot = GraphQL::ObjectType.define do end # create an entry filed to retrive a paginated collection - connection model.name.underscore.pluralize.to_sym, api_types[model].connection_type do + connection model.name.underscore.pluralize.to_sym, created_type.connection_type do description "Find all #{model.model_name.human.pluralize}" resolve -> (object, arguments, context) { model.all diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 5a831fd7d..b47624df9 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -1,6 +1,5 @@ module GraphQL class TypeCreator - # Return a GraphQL type for a 'database_type' TYPES_CONVERSION = Hash.new(GraphQL::STRING_TYPE).merge( integer: GraphQL::INT_TYPE, @@ -9,9 +8,16 @@ module GraphQL double: GraphQL::FLOAT_TYPE ) - def self.create(model, field_names, api_types) + attr_accessor :created_types - new_graphql_type = GraphQL::ObjectType.define do + def initialize + @created_types = {} + end + + def create(model, field_names) + type_creator = self + + created_type = GraphQL::ObjectType.define do name(model.name) description("#{model.model_name.human}") @@ -24,21 +30,22 @@ module GraphQL association = model.reflect_on_all_associations.find { |a| a.name == field_name } case association.macro when :has_one - field(association.name, -> { api_types[association.klass] }) + field(association.name, -> { type_creator.created_types[association.klass] }) when :belongs_to - field(association.name, -> { api_types[association.klass] }) + field(association.name, -> { type_creator.created_types[association.klass] }) when :has_many - connection(association.name, -> { api_types[association.klass].connection_type { - description "#{association.klass.model_name.human.pluralize}" - resolve -> (object, arguments, context) { - association.klass.all - } - }}) + connection association.name, -> do + type_creator.created_types[association.klass].connection_type do + description "#{association.klass.model_name.human.pluralize}" + resolve -> (object, arguments, context) { association.klass.all } + end + end end end end end - return new_graphql_type # GraphQL::ObjectType + created_types[model] = created_type + return created_type # GraphQL::ObjectType end end end From a51626272415e396023c29142f8e71a72a0389d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 30 Nov 2016 13:43:50 +0100 Subject: [PATCH 056/147] Example graphql_controller test --- spec/controllers/graphql_controller_spec.rb | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index 2b53cd0fb..e0244ee68 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -1,6 +1,22 @@ require 'rails_helper' -require 'http' +# Hacerlo como los test de controlador de rails + +describe GraphqlController, type: :request do + let(:proposal) { create(:proposal) } + + it "answers simple json queries" do + headers = { "CONTENT_TYPE" => "application/json" } + #post "/widgets", '{ "widget": { "name":"My Widget" } }', headers + post '/graphql', { query: "{ proposal(id: #{proposal.id}) { title } }" }.to_json, headers + expect(response).to have_http_status(200) + expect(JSON.parse(response.body)['data']['proposal']['title']).to eq(proposal.title) + end + + +end + +=begin describe GraphqlController do let(:uri) { URI::HTTP.build(host: 'localhost', path: '/graphql', port: 3000) } let(:query_string) { "" } @@ -71,3 +87,4 @@ describe GraphqlController do end end +=end From aef20aadf73ef438d93d7c8446af2d364be3adfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 30 Nov 2016 13:44:31 +0100 Subject: [PATCH 057/147] Refactor specs for TypeCreator and ConsulSchema --- config/initializers/graphql.rb | 5 +- spec/lib/graphql_spec.rb | 283 ++++++++++++++++++--------------- spec/lib/type_creator_spec.rb | 24 ++- 3 files changed, 174 insertions(+), 138 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 3918d794a..306c01890 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,8 +1,9 @@ API_TYPE_DEFINITIONS = { - User => %I[ id username proposals ], + User => %I[ id username proposals organization ], Debate => %I[ id title description author_id author created_at comments ], Proposal => %I[ id title description author_id author created_at comments ], - Comment => %I[ id body user_id user commentable_id ] + Comment => %I[ id body user_id user commentable_id ], + Organization => %I[ id name ] } type_creator = GraphQL::TypeCreator.new diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 8e2679f8f..5fac92e28 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -1,132 +1,157 @@ require 'rails_helper' -describe ConsulSchema do - let(:context) { {} } # should be overriden for specific queries - let(:variables) { {} } - let(:result) { ConsulSchema.execute(query_string, context: context, variables: variables) } - - describe "queries to single elements" do - let(:proposal) { create(:proposal) } - subject(:returned_proposal) { result['data']['proposal'] } - - describe "return fields of Int type" do - let(:query_string) { "{ proposal(id: #{proposal.id}) { id } }" } - specify { expect(returned_proposal['id']).to eq(proposal.id) } - end - - describe "return fields of String type" do - let(:query_string) { "{ proposal(id: #{proposal.id}) { title } }" } - specify { expect(returned_proposal['title']).to eq(proposal.title) } - end - - describe "support nested" do - let(:proposal_author) { create(:user) } - let(:comments_author) { create(:user) } - let(:proposal) { create(:proposal, author: proposal_author) } - let!(:comment_1) { create(:comment, author: comments_author, commentable: proposal) } - let!(:comment_2) { create(:comment, author: comments_author, commentable: proposal) } - let(:query_string) { "{ proposal(id: #{proposal.id}) { author { username }, comments { edges { node { body } } } } }" } - - it ":has_one associations" do - skip "I think this test isn't needed" - # TODO: the only has_one associations inside the project are in the User - # model (administrator, valuator, etc.). But since I think this data - # shouldn't be exposed to the API, there's no point in testing this. - end - - it ":belongs_to associations" do - expect(returned_proposal['author']['username']).to eq(proposal_author.username) - end - - it ":has_many associations" do - comments = returned_proposal['comments']['edges'].collect { |edge| edge['node'] } - comment_bodies = comments.collect { |comment| comment['body'] } - - expect(comment_bodies).to match_array([comment_1.body, comment_2.body]) - end - end - - describe "do not expose confidential" do - let(:user) { create(:user) } - subject(:data) { result['data'] } - subject(:errors) { result['errors'] } - subject(:error_msg) { errors.first['message'] } - - describe "fields of Int type" do - let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls_count } }" } - - specify { expect(data).to be_nil } - specify { expect(error_msg).to eq("Field 'failed_census_calls_count' doesn't exist on type 'User'") } - end - - describe "fields of String type" do - let(:query_string) { "{ user(id: #{user.id}) { encrypted_password } }" } - - specify { expect(data).to be_nil } - specify { expect(error_msg).to eq("Field 'encrypted_password' doesn't exist on type 'User'") } - end - - describe "fields inside nested queries" do - let(:proposal) { create(:proposal, author: user) } - let(:query_string) { "{ proposal(id: #{proposal.id}) { author { reset_password_sent_at } } }" } - - specify { expect(data).to be_nil } - specify { expect(error_msg).to eq("Field 'reset_password_sent_at' doesn't exist on type 'User'") } - end - - describe ":has_one associations" do - let(:administrator) { create(:administrator) } - let(:query_string) { "{ user(id: #{user.id}) { administrator { id } } }" } - - specify { expect(data).to be_nil } - specify { expect(error_msg).to eq("Field 'administrator' doesn't exist on type 'User'") } - end - - describe ":belongs_to associations" do - let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls { id } } }" } - let(:census_call) { create(:failed_census_call, user: user) } - - specify { expect(data).to be_nil } - specify { expect(error_msg).to eq("Field 'failed_census_calls' doesn't exist on type 'User'") } - end - - describe ":has_many associations" do - let(:message) { create(:direct_message, sender: user) } - let(:query_string) { "{ user(id: #{user.id}) { direct_messages_sent { id } } }" } - - specify { expect(data).to be_nil } - specify { expect(error_msg).to eq("Field 'direct_messages_sent' doesn't exist on type 'User'") } - end - end - end - - describe "queries to collections" do - let(:mrajoy) { create(:user, username: 'mrajoy') } - let(:dtrump) { create(:user, username: 'dtrump') } - let!(:proposal_1) { create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } - let!(:proposal_2) { create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } - let!(:proposal_3) { create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } - subject(:returned_proposals) { result['data']['proposals']["edges"].collect { |edge| edge['node'] } } - - describe "return fields of Int type" do - let(:query_string) { "{ proposals { edges { node { id } } } }" } - let(:ids) { returned_proposals.collect { |proposal| proposal['id'] } } - - specify { expect(ids).to match_array([3, 1, 2]) } - end - - describe "return fields of String type" do - let(:query_string) { "{ proposals { edges { node { title } } } }" } - let(:titles) { returned_proposals.collect { |proposal| proposal['title'] } } - - specify { expect(titles).to match_array(['Construir un muro', 'Censurar los memes', 'Bajar el IVA']) } - end - - describe "return nested fields" do - let(:query_string) { "{ proposals { edges { node { author { username } } } } }" } - let(:authors) { returned_proposals.collect { |proposal| proposal['author']['username'] } } - - specify { expect(authors).to match_array(['mrajoy', 'dtrump', 'mrajoy']) } - end - end +# TODO: uno por cada tipo escalar, uno por cada asociacion, uno con query anidada * 2 (uno para asegurarse de que se muestra y otro para cuando se oculta) +def execute(query_string, context = {}, variables = {}) + ConsulSchema.execute(query_string, context: context, variables: variables) +end + +def dig(response, path) + response.dig(*path.split('.')) +end + +describe ConsulSchema do + let(:proposal_author) { create(:user) } + let(:proposal) { create(:proposal, author: proposal_author) } + + it "returns fields of Int type" do + response = execute("{ proposal(id: #{proposal.id}) { id } }") + expect(dig(response, 'data.proposal.id')).to eq(proposal.id) + end + + it "returns fields of String type" do + response = execute("{ proposal(id: #{proposal.id}) { title } }") + expect(dig(response, 'data.proposal.title')).to eq(proposal.title) + end + + it "returns has_one associations" do + organization = create(:organization) + response = execute("{ user(id: #{organization.user_id}) { organization { name } } }") + expect(dig(response, 'data.user.organization.name')).to eq(organization.name) + end + + it "returns belongs_to associations" do + response = execute("{ proposal(id: #{proposal.id}) { author { username } } }") + expect(dig(response, 'data.proposal.author.username')).to eq(proposal.author.username) + end + + it "returns has_many associations" do + comments_author = create(:user) + comment_1 = create(:comment, author: comments_author, commentable: proposal) + comment_2 = create(:comment, author: comments_author, commentable: proposal) + + response = execute("{ proposal(id: #{proposal.id}) { comments { edges { node { body } } } } }") + comments = dig(response, 'data.proposal.comments.edges').collect { |edge| edge['node'] } + comment_bodies = comments.collect { |comment| comment['body'] } + + expect(comment_bodies).to match_array([comment_1.body, comment_2.body]) + end + + # it "hides confidential fields of Int type" do + # response = execute("{ user(id: #{user.id}) { failed_census_calls_count } }") + # expect(response['data']).to be_nil + # expect(response[]) + # end + # + # it "hides confidential fields of String type" do + # skip + # response = execute("{ user(id: #{user.id}) { encrypted_password } }") + # end + # + # it "hides confidential has_one associations" do + # skip + # response = execute("{ user(id: #{user.id}) { administrator { id } } }") + # end + # + # it "hides confidential belongs_to associations" do + # skip + # response = execute("{ user(id: #{user.id}) { failed_census_calls { id } } }") + # end + # + # it "hides confidential has_many associations" do + # skip + # response = execute("{ user(id: #{user.id}) { direct_messages_sent { id } } }") + # end + # + # describe "do not expose confidential" do + # let(:user) { create(:user) } + # subject(:data) { response['data'] } + # subject(:errors) { response['errors'] } + # subject(:error_msg) { errors.first['message'] } + # + # describe "fields of Int type" do + # let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls_count } }" } + # + # specify { expect(data).to be_nil } + # specify { expect(error_msg).to eq("Field 'failed_census_calls_count' doesn't exist on type 'User'") } + # end + # + # describe "fields of String type" do + # let(:query_string) { "{ user(id: #{user.id}) { encrypted_password } }" } + # + # specify { expect(data).to be_nil } + # specify { expect(error_msg).to eq("Field 'encrypted_password' doesn't exist on type 'User'") } + # end + # + # describe "fields inside nested queries" do + # let(:proposal) { create(:proposal, author: user) } + # let(:query_string) { "{ proposal(id: #{proposal.id}) { author { reset_password_sent_at } } }" } + # + # specify { expect(data).to be_nil } + # specify { expect(error_msg).to eq("Field 'reset_password_sent_at' doesn't exist on type 'User'") } + # end + # + # describe ":has_one associations" do + # let(:administrator) { create(:administrator) } + # let(:query_string) { "{ user(id: #{user.id}) { administrator { id } } }" } + # + # specify { expect(data).to be_nil } + # specify { expect(error_msg).to eq("Field 'administrator' doesn't exist on type 'User'") } + # end + # + # describe ":belongs_to associations" do + # let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls { id } } }" } + # let(:census_call) { create(:failed_census_call, user: user) } + # + # specify { expect(data).to be_nil } + # specify { expect(error_msg).to eq("Field 'failed_census_calls' doesn't exist on type 'User'") } + # end + # + # describe ":has_many associations" do + # let(:message) { create(:direct_message, sender: user) } + # let(:query_string) { "{ user(id: #{user.id}) { direct_messages_sent { id } } }" } + # + # specify { expect(data).to be_nil } + # specify { expect(error_msg).to eq("Field 'direct_messages_sent' doesn't exist on type 'User'") } + # end + # end + # + # describe "queries to collections" do + # let(:mrajoy) { create(:user, username: 'mrajoy') } + # let(:dtrump) { create(:user, username: 'dtrump') } + # let!(:proposal_1) { create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } + # let!(:proposal_2) { create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } + # let!(:proposal_3) { create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } + # subject(:returned_proposals) { response['data']['proposals']["edges"].collect { |edge| edge['node'] } } + # + # describe "return fields of Int type" do + # let(:query_string) { "{ proposals { edges { node { id } } } }" } + # let(:ids) { returned_proposals.collect { |proposal| proposal['id'] } } + # + # specify { expect(ids).to match_array([3, 1, 2]) } + # end + # + # describe "return fields of String type" do + # let(:query_string) { "{ proposals { edges { node { title } } } }" } + # let(:titles) { returned_proposals.collect { |proposal| proposal['title'] } } + # + # specify { expect(titles).to match_array(['Construir un muro', 'Censurar los memes', 'Bajar el IVA']) } + # end + # + # describe "return nested fields" do + # let(:query_string) { "{ proposals { edges { node { author { username } } } } }" } + # let(:authors) { returned_proposals.collect { |proposal| proposal['author']['username'] } } + # + # specify { expect(authors).to match_array(['mrajoy', 'dtrump', 'mrajoy']) } + # end + # end end diff --git a/spec/lib/type_creator_spec.rb b/spec/lib/type_creator_spec.rb index 2369df348..4ad667812 100644 --- a/spec/lib/type_creator_spec.rb +++ b/spec/lib/type_creator_spec.rb @@ -1,10 +1,12 @@ require 'rails_helper' describe GraphQL::TypeCreator do - let!(:api_types) { {} } - let!(:user_type) { GraphQL::TypeCreator.create(User, %I[ id ], api_types) } - let!(:comment_type) { GraphQL::TypeCreator.create(Comment, %I[ id ], api_types) } - let!(:debate_type) { GraphQL::TypeCreator.create(Debate, %I[ id title author ], api_types) } + let(:type_creator) { GraphQL::TypeCreator.new } + + #let(:api_types) { {} } + #let!(:user_type) { GraphQL::TypeCreator.create(User, %I[ id ], api_types) } + #let!(:comment_type) { GraphQL::TypeCreator.create(Comment, %I[ id ], api_types) } + #let!(:debate_type) { GraphQL::TypeCreator.create(Debate, %I[ id title author ], api_types) } # TODO: no puedo añadir los comentarios a la field_list de Debate porque como # las conexiones se crean de forma lazy creo que provoca que falle la creación # del resto de tipos y provoca que fallen todos los tests. @@ -13,6 +15,7 @@ describe GraphQL::TypeCreator do describe "::create" do describe "creates fields" do it "for int attributes" do + debate_type = type_creator.create(Debate, %I[ id ]) created_field = debate_type.fields['id'] expect(created_field).to be_a(GraphQL::Field) @@ -21,6 +24,7 @@ describe GraphQL::TypeCreator do end it "for string attributes" do + skip created_field = debate_type.fields['title'] expect(created_field).to be_a(GraphQL::Field) @@ -31,6 +35,9 @@ describe GraphQL::TypeCreator do describe "creates connections for" do it ":belongs_to associations" do + user_type = type_creator.create(User, %I[ id ]) + debate_type = type_creator.create(Debate, %I[ author ]) + connection = debate_type.fields['author'] # TODO: because connection types are created and added lazily to the @@ -40,7 +47,8 @@ describe GraphQL::TypeCreator do # connection.inspect shows some weird info expect(connection).to be_a(GraphQL::Field) - # expect(connection.type).to be_a(api_types[User]) + #debugger + expect(connection.type).to eq(user_type) expect(connection.name).to eq('author') end @@ -49,7 +57,9 @@ describe GraphQL::TypeCreator do end it ":has_many associations" do - skip "still don't know how to handle relay connections inside RSpec" + #skip "still don't know how to handle relay connections inside RSpec" + comment_type = type_creator.create(Comment, %I[ id ]) + debate_type = type_creator.create(Debate, %I[ author ]) connection = debate_type.fields['comments'] @@ -60,7 +70,7 @@ describe GraphQL::TypeCreator do # connection.inspect shows some weird info expect(connection).to be_a(GraphQL::Field) - # expect(created_connection.type).to be_a(api_types[Comment]) + expect(connection.type).to be_a(api_types[Comment]) expect(connection.name).to eq('comments') end end From c973195267593e5cd447117a9f9f9981d74c3d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 2 Dec 2016 21:30:13 +0100 Subject: [PATCH 058/147] Refactor ConsulSchema specs --- spec/lib/graphql_spec.rb | 160 ++++++++++++--------------------------- 1 file changed, 49 insertions(+), 111 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 5fac92e28..50e115a7f 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -1,6 +1,5 @@ require 'rails_helper' -# TODO: uno por cada tipo escalar, uno por cada asociacion, uno con query anidada * 2 (uno para asegurarse de que se muestra y otro para cuando se oculta) def execute(query_string, context = {}, variables = {}) ConsulSchema.execute(query_string, context: context, variables: variables) end @@ -9,9 +8,15 @@ def dig(response, path) response.dig(*path.split('.')) end +def hidden_field?(response, field_name) + data_is_empty = response['data'].nil? + error_is_present = ((response['errors'].first['message'] =~ /Field '#{field_name}' doesn't exist on type '[[:alnum:]]*'/) == 0) + data_is_empty && error_is_present +end + describe ConsulSchema do - let(:proposal_author) { create(:user) } - let(:proposal) { create(:proposal, author: proposal_author) } + let(:user) { create(:user) } + let(:proposal) { create(:proposal, author: user) } it "returns fields of Int type" do response = execute("{ proposal(id: #{proposal.id}) { id } }") @@ -46,112 +51,45 @@ describe ConsulSchema do expect(comment_bodies).to match_array([comment_1.body, comment_2.body]) end - # it "hides confidential fields of Int type" do - # response = execute("{ user(id: #{user.id}) { failed_census_calls_count } }") - # expect(response['data']).to be_nil - # expect(response[]) - # end - # - # it "hides confidential fields of String type" do - # skip - # response = execute("{ user(id: #{user.id}) { encrypted_password } }") - # end - # - # it "hides confidential has_one associations" do - # skip - # response = execute("{ user(id: #{user.id}) { administrator { id } } }") - # end - # - # it "hides confidential belongs_to associations" do - # skip - # response = execute("{ user(id: #{user.id}) { failed_census_calls { id } } }") - # end - # - # it "hides confidential has_many associations" do - # skip - # response = execute("{ user(id: #{user.id}) { direct_messages_sent { id } } }") - # end - # - # describe "do not expose confidential" do - # let(:user) { create(:user) } - # subject(:data) { response['data'] } - # subject(:errors) { response['errors'] } - # subject(:error_msg) { errors.first['message'] } - # - # describe "fields of Int type" do - # let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls_count } }" } - # - # specify { expect(data).to be_nil } - # specify { expect(error_msg).to eq("Field 'failed_census_calls_count' doesn't exist on type 'User'") } - # end - # - # describe "fields of String type" do - # let(:query_string) { "{ user(id: #{user.id}) { encrypted_password } }" } - # - # specify { expect(data).to be_nil } - # specify { expect(error_msg).to eq("Field 'encrypted_password' doesn't exist on type 'User'") } - # end - # - # describe "fields inside nested queries" do - # let(:proposal) { create(:proposal, author: user) } - # let(:query_string) { "{ proposal(id: #{proposal.id}) { author { reset_password_sent_at } } }" } - # - # specify { expect(data).to be_nil } - # specify { expect(error_msg).to eq("Field 'reset_password_sent_at' doesn't exist on type 'User'") } - # end - # - # describe ":has_one associations" do - # let(:administrator) { create(:administrator) } - # let(:query_string) { "{ user(id: #{user.id}) { administrator { id } } }" } - # - # specify { expect(data).to be_nil } - # specify { expect(error_msg).to eq("Field 'administrator' doesn't exist on type 'User'") } - # end - # - # describe ":belongs_to associations" do - # let(:query_string) { "{ user(id: #{user.id}) { failed_census_calls { id } } }" } - # let(:census_call) { create(:failed_census_call, user: user) } - # - # specify { expect(data).to be_nil } - # specify { expect(error_msg).to eq("Field 'failed_census_calls' doesn't exist on type 'User'") } - # end - # - # describe ":has_many associations" do - # let(:message) { create(:direct_message, sender: user) } - # let(:query_string) { "{ user(id: #{user.id}) { direct_messages_sent { id } } }" } - # - # specify { expect(data).to be_nil } - # specify { expect(error_msg).to eq("Field 'direct_messages_sent' doesn't exist on type 'User'") } - # end - # end - # - # describe "queries to collections" do - # let(:mrajoy) { create(:user, username: 'mrajoy') } - # let(:dtrump) { create(:user, username: 'dtrump') } - # let!(:proposal_1) { create(:proposal, id: 1, title: "Bajar el IVA", author: mrajoy) } - # let!(:proposal_2) { create(:proposal, id: 2, title: "Censurar los memes", author: mrajoy) } - # let!(:proposal_3) { create(:proposal, id: 3, title: "Construir un muro", author: dtrump) } - # subject(:returned_proposals) { response['data']['proposals']["edges"].collect { |edge| edge['node'] } } - # - # describe "return fields of Int type" do - # let(:query_string) { "{ proposals { edges { node { id } } } }" } - # let(:ids) { returned_proposals.collect { |proposal| proposal['id'] } } - # - # specify { expect(ids).to match_array([3, 1, 2]) } - # end - # - # describe "return fields of String type" do - # let(:query_string) { "{ proposals { edges { node { title } } } }" } - # let(:titles) { returned_proposals.collect { |proposal| proposal['title'] } } - # - # specify { expect(titles).to match_array(['Construir un muro', 'Censurar los memes', 'Bajar el IVA']) } - # end - # - # describe "return nested fields" do - # let(:query_string) { "{ proposals { edges { node { author { username } } } } }" } - # let(:authors) { returned_proposals.collect { |proposal| proposal['author']['username'] } } - # - # specify { expect(authors).to match_array(['mrajoy', 'dtrump', 'mrajoy']) } - # end - # end + it "executes deeply nested queries" do + org_user = create(:user) + organization = create(:organization, user: org_user) + org_proposal = create(:proposal, author: org_user) + response = execute("{ proposal(id: #{org_proposal.id}) { author { organization { name } } } }") + + expect(dig(response, 'data.proposal.author.organization.name')).to eq(organization.name) + end + + it "hides confidential fields of Int type" do + response = execute("{ user(id: #{user.id}) { failed_census_calls_count } }") + expect(hidden_field?(response, 'failed_census_calls_count')).to be_truthy + end + + it "hides confidential fields of String type" do + response = execute("{ user(id: #{user.id}) { encrypted_password } }") + expect(hidden_field?(response, 'encrypted_password')).to be_truthy + end + + it "hides confidential has_one associations" do + user.administrator = create(:administrator) + response = execute("{ user(id: #{user.id}) { administrator { id } } }") + expect(hidden_field?(response, 'administrator')).to be_truthy + end + + it "hides confidential belongs_to associations" do + create(:failed_census_call, user: user) + response = execute("{ user(id: #{user.id}) { failed_census_calls { id } } }") + expect(hidden_field?(response, 'failed_census_calls')).to be_truthy + end + + it "hides confidential has_many associations" do + create(:direct_message, sender: user) + response = execute("{ user(id: #{user.id}) { direct_messages_sent { id } } }") + expect(hidden_field?(response, 'direct_messages_sent')).to be_truthy + end + + it "hides confidential fields inside deeply nested queries" do + response = execute("{ proposals(first: 1) { edges { node { author { encrypted_password } } } } }") + expect(hidden_field?(response, 'encrypted_password')).to be_truthy + end end From a0f1976c1adba3de67c6204dec2df1324d5fa34a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 2 Dec 2016 21:53:36 +0100 Subject: [PATCH 059/147] Refactor GraphQL::TypeCreator specs --- spec/lib/type_creator_spec.rb | 96 ++++++++++++++--------------------- 1 file changed, 38 insertions(+), 58 deletions(-) diff --git a/spec/lib/type_creator_spec.rb b/spec/lib/type_creator_spec.rb index 4ad667812..2fc1bf78d 100644 --- a/spec/lib/type_creator_spec.rb +++ b/spec/lib/type_creator_spec.rb @@ -3,76 +3,56 @@ require 'rails_helper' describe GraphQL::TypeCreator do let(:type_creator) { GraphQL::TypeCreator.new } - #let(:api_types) { {} } - #let!(:user_type) { GraphQL::TypeCreator.create(User, %I[ id ], api_types) } - #let!(:comment_type) { GraphQL::TypeCreator.create(Comment, %I[ id ], api_types) } - #let!(:debate_type) { GraphQL::TypeCreator.create(Debate, %I[ id title author ], api_types) } - # TODO: no puedo añadir los comentarios a la field_list de Debate porque como - # las conexiones se crean de forma lazy creo que provoca que falle la creación - # del resto de tipos y provoca que fallen todos los tests. - # let!(:debate_type) { GraphQL::TypeCreator.create(Debate, %I[ id title author comments ], api_types) } - describe "::create" do - describe "creates fields" do - it "for int attributes" do - debate_type = type_creator.create(Debate, %I[ id ]) - created_field = debate_type.fields['id'] + it "creates fields for Int attributes" do + debate_type = type_creator.create(Debate, %I[ id ]) + created_field = debate_type.fields['id'] - expect(created_field).to be_a(GraphQL::Field) - expect(created_field.type).to be_a(GraphQL::ScalarType) - expect(created_field.type.name).to eq('Int') - end - - it "for string attributes" do - skip - created_field = debate_type.fields['title'] - - expect(created_field).to be_a(GraphQL::Field) - expect(created_field.type).to be_a(GraphQL::ScalarType) - expect(created_field.type.name).to eq('String') - end + expect(created_field).to be_a(GraphQL::Field) + expect(created_field.type).to be_a(GraphQL::ScalarType) + expect(created_field.type.name).to eq('Int') end - describe "creates connections for" do - it ":belongs_to associations" do - user_type = type_creator.create(User, %I[ id ]) - debate_type = type_creator.create(Debate, %I[ author ]) + it "creates fields for String attributes" do + debate_type = type_creator.create(Debate, %I[ title ]) + created_field = debate_type.fields['title'] - connection = debate_type.fields['author'] + expect(created_field).to be_a(GraphQL::Field) + expect(created_field.type).to be_a(GraphQL::ScalarType) + expect(created_field.type.name).to eq('String') + end - # TODO: because connection types are created and added lazily to the - # api_types hash (with that proc thing ->) I don't really know how to - # test this. - # connection.class shows GraphQL::Field - # connection.inspect shows some weird info + it "creates connections for :belongs_to associations" do + user_type = type_creator.create(User, %I[ id ]) + debate_type = type_creator.create(Debate, %I[ author ]) - expect(connection).to be_a(GraphQL::Field) - #debugger - expect(connection.type).to eq(user_type) - expect(connection.name).to eq('author') - end + connection = debate_type.fields['author'] - it ":has_one associations" do - skip "need to find association example that uses :has_one" - end + expect(connection).to be_a(GraphQL::Field) + expect(connection.type).to eq(user_type) + expect(connection.name).to eq('author') + end - it ":has_many associations" do - #skip "still don't know how to handle relay connections inside RSpec" - comment_type = type_creator.create(Comment, %I[ id ]) - debate_type = type_creator.create(Debate, %I[ author ]) + it "creates connections for :has_one associations" do + user_type = type_creator.create(User, %I[ organization ]) + organization_type = type_creator.create(Organization, %I[ id ]) - connection = debate_type.fields['comments'] + connection = user_type.fields['organization'] - # TODO: because connection types are created and added lazily to the - # api_types hash (with that proc thing ->) I don't really know how to - # test this. - # connection.class shows GraphQL::Field - # connection.inspect shows some weird info + expect(connection).to be_a(GraphQL::Field) + expect(connection.type).to eq(organization_type) + expect(connection.name).to eq('organization') + end - expect(connection).to be_a(GraphQL::Field) - expect(connection.type).to be_a(api_types[Comment]) - expect(connection.name).to eq('comments') - end + it "creates connections for :has_many associations" do + comment_type = type_creator.create(Comment, %I[ id ]) + debate_type = type_creator.create(Debate, %I[ comments ]) + + connection = debate_type.fields['comments'] + + expect(connection).to be_a(GraphQL::Field) + expect(connection.type).to eq(comment_type.connection_type) + expect(connection.name).to eq('comments') end end end From 6013f8494577491576a104183330f63f73221e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 3 Dec 2016 13:36:14 +0100 Subject: [PATCH 060/147] Handle multiple kind of requests in GraphQL controller * Added support for GET requests * Added support for application/graphql content-type for POST requests * Handling query string parsing errors in controller * Wrote specs for all this --- app/controllers/graphql_controller.rb | 26 +++-- config/routes.rb | 1 + spec/controllers/graphql_controller_spec.rb | 110 +++++++------------- 3 files changed, 59 insertions(+), 78 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index e3bedceea..be4f51f03 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -4,11 +4,25 @@ class GraphqlController < ApplicationController skip_authorization_check def query - # ConsulSchema.execute returns the query result in the shape of a Hash, which - # is sent back to the client rendered in JSON - render json: ConsulSchema.execute( - params[:query], - variables: params[:variables] || {} - ) + + if request.headers["CONTENT_TYPE"] == 'application/graphql' + query_string = request.body.string # request.body.class => StringIO + else + query_string = params[:query] + end + + if query_string.nil? + render json: { message: 'Query string not present' }, status: :bad_request + else + begin + response = ConsulSchema.execute( + query_string, + variables: params[:variables] || {} + ) + render json: response, status: :ok + rescue GraphQL::ParseError + render json: { message: 'Query string is not valid JSON' }, status: :bad_request + end + end end end diff --git a/config/routes.rb b/config/routes.rb index b530df52c..63be98384 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -264,6 +264,7 @@ Rails.application.routes.draw do # GraphQL mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql' + get '/graphql', to: 'graphql#query' post '/graphql', to: 'graphql#query' if Rails.env.development? diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index e0244ee68..e95bdd3c9 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -1,90 +1,56 @@ require 'rails_helper' -# Hacerlo como los test de controlador de rails +# Useful resource: http://graphql.org/learn/serving-over-http/ describe GraphqlController, type: :request do let(:proposal) { create(:proposal) } - it "answers simple json queries" do - headers = { "CONTENT_TYPE" => "application/json" } - #post "/widgets", '{ "widget": { "name":"My Widget" } }', headers - post '/graphql', { query: "{ proposal(id: #{proposal.id}) { title } }" }.to_json, headers - expect(response).to have_http_status(200) - expect(JSON.parse(response.body)['data']['proposal']['title']).to eq(proposal.title) + describe "handles GET request" do + specify "with query string inside query params" do + get '/graphql', query: "{ proposal(id: #{proposal.id}) { title } }" + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data']['proposal']['title']).to eq(proposal.title) + end + + specify "with malformed query string" do + get '/graphql', query: 'Malformed query string' + expect(response).to have_http_status(:bad_request) + expect(JSON.parse(response.body)['message']).to eq('Query string is not valid JSON') + end + + specify "without query string" do + get '/graphql' + expect(response).to have_http_status(:bad_request) + expect(JSON.parse(response.body)['message']).to eq('Query string not present') + end end - -end + describe "handles POST request" do + let(:json_headers) { { "CONTENT_TYPE" => "application/json" } } -=begin -describe GraphqlController do - let(:uri) { URI::HTTP.build(host: 'localhost', path: '/graphql', port: 3000) } - let(:query_string) { "" } - let(:body) { {query: query_string}.to_json } - - describe "POST requests" do - let(:author) { create(:user) } - let(:proposal) { create(:proposal, author: author) } - let(:response) { HTTP.headers('Content-Type' => 'application/json').post(uri, body: body) } - let(:response_body) { JSON.parse(response.body) } - - context "when query string is valid" do - let(:query_string) { "{ proposal(id: #{proposal.id}) { title, author { username } } }" } - let(:returned_proposal) { response_body['data']['proposal'] } - - it "returns HTTP 200 OK" do - expect(response.code).to eq(200) - end - - it "returns first-level fields" do - expect(returned_proposal['title']).to eq(proposal.title) - end - - it "returns nested fields" do - expect(returned_proposal['author']['username']).to eq(author.username) - end + specify "with json-encoded query string inside body" do + post '/graphql', { query: "{ proposal(id: #{proposal.id}) { title } }" }.to_json, json_headers + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data']['proposal']['title']).to eq(proposal.title) end - context "when query string asks for invalid fields" do - let(:query_string) { "{ proposal(id: #{proposal.id}) { missing_field } }" } - - it "returns HTTP 200 OK" do - expect(response.code).to eq(200) - end - - it "doesn't return any data" do - expect(response_body['data']).to be_nil - end - - it "returns error inside body" do - expect(response_body['errors']).to be_present - end + specify "with raw query string inside body" do + graphql_headers = { "CONTENT_TYPE" => "application/graphql" } + post '/graphql', "{ proposal(id: #{proposal.id}) { title } }", graphql_headers + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data']['proposal']['title']).to eq(proposal.title) end - context "when query string is not valid" do - let(:query_string) { "invalid" } - - it "returns HTTP 400 Bad Request" do - expect(response.code).to eq(400) - end + specify "with malformed query string" do + post '/graphql', { query: "Malformed query string" }.to_json, json_headers + expect(response).to have_http_status(:bad_request) + expect(JSON.parse(response.body)['message']).to eq('Query string is not valid JSON') end - context "when query string is missing" do - let(:query_string) { nil } - - it "returns HTTP 400 Bad Request" do - expect(response.code).to eq(400) - end + it "without query string" do + post '/graphql', json_headers + expect(response).to have_http_status(:bad_request) + expect(JSON.parse(response.body)['message']).to eq('Query string not present') end - - context "when body is missing" do - let(:body) { nil } - - it "returns HTTP 400 Bad Request" do - expect(response.code).to eq(400) - end - end - end end -=end From 42355e770c77c58e52043b912243cb0d1441d256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 5 Dec 2016 18:42:17 +0100 Subject: [PATCH 061/147] Fixed bug related to wrong use of GraphQL::Relay connection helper --- lib/graph_ql/type_creator.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index b47624df9..a508c88d6 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -34,11 +34,11 @@ module GraphQL when :belongs_to field(association.name, -> { type_creator.created_types[association.klass] }) when :has_many - connection association.name, -> do - type_creator.created_types[association.klass].connection_type do - description "#{association.klass.model_name.human.pluralize}" - resolve -> (object, arguments, context) { association.klass.all } - end + connection( + association.name, + Proc.new { type_creator.created_types[association.klass].connection_type } + ) do + resolve -> (object, arguments, context) { association.klass.all } end end end From b2730b6239de3b8a088acc202858702da8ffb40a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 11 Dec 2016 15:20:17 +0100 Subject: [PATCH 062/147] DRY GraphQL::TypeCreator --- lib/graph_ql/type_creator.rb | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index a508c88d6..d0dfde580 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -27,19 +27,11 @@ module GraphQL if model.column_names.include?(field_name.to_s) field(field_name.to_s, TYPES_CONVERSION[model.columns_hash[field_name.to_s].type]) else - association = model.reflect_on_all_associations.find { |a| a.name == field_name } - case association.macro - when :has_one - field(association.name, -> { type_creator.created_types[association.klass] }) - when :belongs_to - field(association.name, -> { type_creator.created_types[association.klass] }) - when :has_many - connection( - association.name, - Proc.new { type_creator.created_types[association.klass].connection_type } - ) do - resolve -> (object, arguments, context) { association.klass.all } - end + association = type_creator.class.association?(model, field_name) + if type_creator.class.needs_pagination?(association) + connection association.name, -> { type_creator.created_types[association.klass].connection_type } + else + field association.name, -> { type_creator.created_types[association.klass] } end end end @@ -47,5 +39,13 @@ module GraphQL created_types[model] = created_type return created_type # GraphQL::ObjectType end + + def self.association?(model, field_name) + model.reflect_on_all_associations.find { |a| a.name == field_name } + end + + def self.needs_pagination?(association) + association.macro == :has_many + end end end From 82954c922a33519ac8da7c37647fb97609fdf97b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 11 Dec 2016 15:42:17 +0100 Subject: [PATCH 063/147] Expose public_author for proposals --- app/models/concerns/has_public_author.rb | 9 +++++++++ app/models/concerns/has_public_author_types.rb | 9 +++++++++ app/models/proposal.rb | 2 ++ config/initializers/graphql.rb | 8 ++++---- lib/graph_ql/type_creator.rb | 7 ++++--- 5 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 app/models/concerns/has_public_author.rb create mode 100644 app/models/concerns/has_public_author_types.rb diff --git a/app/models/concerns/has_public_author.rb b/app/models/concerns/has_public_author.rb new file mode 100644 index 000000000..17cd5edd4 --- /dev/null +++ b/app/models/concerns/has_public_author.rb @@ -0,0 +1,9 @@ +module HasPublicAuthor + def public_author_id + public_author.try(:id) + end + + def public_author + self.author.public_activity? ? self.author : nil + end +end diff --git a/app/models/concerns/has_public_author_types.rb b/app/models/concerns/has_public_author_types.rb new file mode 100644 index 000000000..6555b8b4e --- /dev/null +++ b/app/models/concerns/has_public_author_types.rb @@ -0,0 +1,9 @@ +module HasPublicAuthorTypes + def public_author_id_type + GraphQL::INT_TYPE + end + + def public_author_type + TypeCreator.created_types[User] + end +end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 7bd4b7a92..8e67f23dd 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -6,6 +6,8 @@ class Proposal < ActiveRecord::Base include Sanitizable include Searchable include Filterable + include HasPublicAuthor + extend HasPublicAuthorTypes acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 306c01890..7213b64d9 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,15 +1,15 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals organization ], Debate => %I[ id title description author_id author created_at comments ], - Proposal => %I[ id title description author_id author created_at comments ], + Proposal => %I[ id title description author_id author public_author_id public_author created_at comments ], Comment => %I[ id body user_id user commentable_id ], Organization => %I[ id name ] } -type_creator = GraphQL::TypeCreator.new +TypeCreator = GraphQL::TypeCreator.new API_TYPE_DEFINITIONS.each do |model, fields| - type_creator.create(model, fields) + TypeCreator.create(model, fields) end ConsulSchema = GraphQL::Schema.define do @@ -29,7 +29,7 @@ QueryRoot = GraphQL::ObjectType.define do name "Query" description "The query root for this schema" - type_creator.created_types.each do |model, created_type| + TypeCreator.created_types.each do |model, created_type| # create an entry field to retrive a single object field model.name.underscore.to_sym do diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index d0dfde580..127599f95 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -22,17 +22,18 @@ module GraphQL name(model.name) description("#{model.model_name.human}") - # Make a field for each column + # Make a field for each column, association or method field_names.each do |field_name| if model.column_names.include?(field_name.to_s) field(field_name.to_s, TYPES_CONVERSION[model.columns_hash[field_name.to_s].type]) - else - association = type_creator.class.association?(model, field_name) + elsif association = type_creator.class.association?(model, field_name) if type_creator.class.needs_pagination?(association) connection association.name, -> { type_creator.created_types[association.klass].connection_type } else field association.name, -> { type_creator.created_types[association.klass] } end + else + field field_name.to_s, model.send("#{field_name}_type".to_sym) end end end From 402b07d959538895c724c60d61140fb6246b9557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 13 Dec 2016 14:04:53 +0100 Subject: [PATCH 064/147] Expose Proposal public_author using scopes instead of modules --- app/models/concerns/has_public_author.rb | 9 --------- app/models/concerns/has_public_author_types.rb | 9 --------- app/models/proposal.rb | 3 +-- app/models/user.rb | 17 +++++++++-------- config/initializers/graphql.rb | 8 ++++---- lib/graph_ql/type_creator.rb | 5 ++--- 6 files changed, 16 insertions(+), 35 deletions(-) delete mode 100644 app/models/concerns/has_public_author.rb delete mode 100644 app/models/concerns/has_public_author_types.rb diff --git a/app/models/concerns/has_public_author.rb b/app/models/concerns/has_public_author.rb deleted file mode 100644 index 17cd5edd4..000000000 --- a/app/models/concerns/has_public_author.rb +++ /dev/null @@ -1,9 +0,0 @@ -module HasPublicAuthor - def public_author_id - public_author.try(:id) - end - - def public_author - self.author.public_activity? ? self.author : nil - end -end diff --git a/app/models/concerns/has_public_author_types.rb b/app/models/concerns/has_public_author_types.rb deleted file mode 100644 index 6555b8b4e..000000000 --- a/app/models/concerns/has_public_author_types.rb +++ /dev/null @@ -1,9 +0,0 @@ -module HasPublicAuthorTypes - def public_author_id_type - GraphQL::INT_TYPE - end - - def public_author_type - TypeCreator.created_types[User] - end -end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 8e67f23dd..1f9ede642 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -6,8 +6,6 @@ class Proposal < ActiveRecord::Base include Sanitizable include Searchable include Filterable - include HasPublicAuthor - extend HasPublicAuthorTypes acts_as_votable acts_as_paranoid column: :hidden_at @@ -16,6 +14,7 @@ class Proposal < ActiveRecord::Base RETIRE_OPTIONS = %w(duplicated started unfeasible done other) belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :public_author, -> { public_activity }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable has_many :proposal_notifications diff --git a/app/models/user.rb b/app/models/user.rb index 3ca79cedf..a54be0a16 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -46,14 +46,15 @@ class User < ActiveRecord::Base attr_accessor :skip_password_validation attr_accessor :use_redeemable_code - scope :administrators, -> { joins(:administrators) } - scope :moderators, -> { joins(:moderator) } - scope :organizations, -> { joins(:organization) } - scope :officials, -> { where("official_level > 0") } - scope :for_render, -> { includes(:organization) } - scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } - scope :email_digest, -> { where(email_digest: true) } - scope :active, -> { where(erased_at: nil) } + scope :administrators, -> { joins(:administrators) } + scope :moderators, -> { joins(:moderator) } + scope :organizations, -> { joins(:organization) } + scope :officials, -> { where("official_level > 0") } + scope :for_render, -> { includes(:organization) } + scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } + scope :email_digest, -> { where(email_digest: true) } + scope :active, -> { where(erased_at: nil) } + scope :public_activity, -> { where(public_activity: true) } before_validation :clean_document_number diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 7213b64d9..1cfd238aa 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,15 +1,15 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals organization ], Debate => %I[ id title description author_id author created_at comments ], - Proposal => %I[ id title description author_id author public_author_id public_author created_at comments ], + Proposal => %I[ id title description public_author created_at comments ], Comment => %I[ id body user_id user commentable_id ], Organization => %I[ id name ] } -TypeCreator = GraphQL::TypeCreator.new +type_creator = GraphQL::TypeCreator.new API_TYPE_DEFINITIONS.each do |model, fields| - TypeCreator.create(model, fields) + type_creator.create(model, fields) end ConsulSchema = GraphQL::Schema.define do @@ -29,7 +29,7 @@ QueryRoot = GraphQL::ObjectType.define do name "Query" description "The query root for this schema" - TypeCreator.created_types.each do |model, created_type| + type_creator.created_types.each do |model, created_type| # create an entry field to retrive a single object field model.name.underscore.to_sym do diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 127599f95..3f35c2c1e 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -26,14 +26,13 @@ module GraphQL field_names.each do |field_name| if model.column_names.include?(field_name.to_s) field(field_name.to_s, TYPES_CONVERSION[model.columns_hash[field_name.to_s].type]) - elsif association = type_creator.class.association?(model, field_name) + else + association = type_creator.class.association?(model, field_name) if type_creator.class.needs_pagination?(association) connection association.name, -> { type_creator.created_types[association.klass].connection_type } else field association.name, -> { type_creator.created_types[association.klass] } end - else - field field_name.to_s, model.send("#{field_name}_type".to_sym) end end end From c155da5e10b3ec00eb792b2052900e1775ea78a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 13 Dec 2016 14:20:43 +0100 Subject: [PATCH 065/147] Change seeds to provide meaningful data for querying public_author from API --- db/dev_seeds.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 87dc94a2e..b0a2347ee 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -41,7 +41,15 @@ puts "Creating Users" def create_user(email, username = Faker::Name.name) pwd = '12345678' puts " #{username}" - User.create!(username: username, email: email, password: pwd, password_confirmation: pwd, confirmed_at: Time.current, terms_of_service: "1") + User.create!( + username: username, + email: email, + password: pwd, + password_confirmation: pwd, + confirmed_at: Time.current, + terms_of_service: "1", + public_activity: (rand(1..100) > 30) + ) end admin = create_user('admin@consul.dev', 'admin') From 10eedebcb2154a6f2743bce32135f6ade94d424c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 13 Dec 2016 14:26:00 +0100 Subject: [PATCH 066/147] Refactor GraphQL specs to use public_author instead of author --- spec/lib/graphql_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 50e115a7f..eed7c6d34 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -35,8 +35,8 @@ describe ConsulSchema do end it "returns belongs_to associations" do - response = execute("{ proposal(id: #{proposal.id}) { author { username } } }") - expect(dig(response, 'data.proposal.author.username')).to eq(proposal.author.username) + response = execute("{ proposal(id: #{proposal.id}) { public_author { username } } }") + expect(dig(response, 'data.proposal.public_author.username')).to eq(proposal.public_author.username) end it "returns has_many associations" do @@ -55,9 +55,9 @@ describe ConsulSchema do org_user = create(:user) organization = create(:organization, user: org_user) org_proposal = create(:proposal, author: org_user) - response = execute("{ proposal(id: #{org_proposal.id}) { author { organization { name } } } }") + response = execute("{ proposal(id: #{org_proposal.id}) { public_author { organization { name } } } }") - expect(dig(response, 'data.proposal.author.organization.name')).to eq(organization.name) + expect(dig(response, 'data.proposal.public_author.organization.name')).to eq(organization.name) end it "hides confidential fields of Int type" do @@ -89,7 +89,7 @@ describe ConsulSchema do end it "hides confidential fields inside deeply nested queries" do - response = execute("{ proposals(first: 1) { edges { node { author { encrypted_password } } } } }") + response = execute("{ proposals(first: 1) { edges { node { public_author { encrypted_password } } } } }") expect(hidden_field?(response, 'encrypted_password')).to be_truthy end end From efade39412e77f5691144796d99b508bbdba5b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 13 Dec 2016 14:40:22 +0100 Subject: [PATCH 067/147] Added specs for User 'public_activity' scope --- spec/factories.rb | 3 ++- spec/models/user_spec.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/spec/factories.rb b/spec/factories.rb index 7b446f3a0..63d870fd9 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -6,8 +6,9 @@ FactoryGirl.define do sequence(:email) { |n| "manuela#{n}@consul.dev" } password 'judgmentday' - terms_of_service '1' + terms_of_service '1' confirmed_at { Time.current } + public_activity true trait :incomplete_verification do after :create do |user| diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d7235dc86..a76765cc5 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -351,6 +351,18 @@ describe User do end end + + describe "public activity" do + it "returns users whose activity feed is public" do + user1 = create(:user) + user2 = create(:user, public_activity: false) + user3 = create(:user) + + expect(User.public_activity).to include(user1) + expect(User.public_activity).not_to include(user2) + expect(User.public_activity).to include(user3) + end + end end describe "self.search" do From 64cf3cde34fc971efd41d781effaec669229d7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 26 Dec 2016 12:52:23 +0100 Subject: [PATCH 068/147] Modified seeds to provide more meaningful data --- db/dev_seeds.rb | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index b0a2347ee..5d1d19588 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -39,16 +39,17 @@ puts "Creating Geozones" puts "Creating Users" def create_user(email, username = Faker::Name.name) - pwd = '12345678' puts " #{username}" User.create!( - username: username, - email: email, - password: pwd, - password_confirmation: pwd, - confirmed_at: Time.current, - terms_of_service: "1", - public_activity: (rand(1..100) > 30) + username: username, + email: email, + password: '12345678', + password_confirmation: '12345678', + confirmed_at: Time.current, + terms_of_service: "1", + gender: ['Male', 'Female'].sample, + date_of_birth: rand((Time.current - 80.years) .. (Time.current - 16.years)), + public_activity: (rand(1..100) > 30) ) end @@ -86,11 +87,11 @@ end official.update(official_level: i, official_position: "Official position #{i}") end -(1..40).each do |i| +(1..100).each do |i| user = create_user("user#{i}@consul.dev") level = [1, 2, 3].sample if level >= 2 - user.update(residence_verified_at: Time.current, confirmed_phone: Faker::PhoneNumber.phone_number, document_number: Faker::Number.number(10), document_type: "1" ) + user.update(residence_verified_at: Time.current, confirmed_phone: Faker::PhoneNumber.phone_number, document_number: Faker::Number.number(10), document_type: "1", geozone: Geozone.reorder("RANDOM()").first) end if level == 3 user.update(verified_at: Time.current, document_number: Faker::Number.number(10) ) @@ -252,7 +253,7 @@ end puts "Voting Debates, Proposals & Comments" (1..100).each do - voter = not_org_users.reorder("RANDOM()").first + voter = not_org_users.level_two_or_three_verified.reorder("RANDOM()").first vote = [true, false].sample debate = Debate.reorder("RANDOM()").first debate.vote_by(voter: voter, vote: vote) @@ -266,7 +267,7 @@ end end (1..100).each do - voter = User.level_two_or_three_verified.reorder("RANDOM()").first + voter = not_org_users.level_two_or_three_verified.reorder("RANDOM()").first proposal = Proposal.reorder("RANDOM()").first proposal.vote_by(voter: voter, vote: true) end From 5daf59e81396decd1cedc98736c748d225cc67a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 26 Dec 2016 13:42:45 +0100 Subject: [PATCH 069/147] Update Proposal fields exposed in API --- config/initializers/graphql.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 1cfd238aa..f0b27dac6 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,7 +1,7 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals organization ], Debate => %I[ id title description author_id author created_at comments ], - Proposal => %I[ id title description public_author created_at comments ], + Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments public_author ], Comment => %I[ id body user_id user commentable_id ], Organization => %I[ id name ] } From 2311cd4b1d3c8719cbafbba3d28b9f39e0b2edcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 26 Dec 2016 18:42:12 +0100 Subject: [PATCH 070/147] Renamed User::public_activity scope --- app/models/debate.rb | 1 + app/models/proposal.rb | 2 +- app/models/user.rb | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/debate.rb b/app/models/debate.rb index 020259d80..13cff5b3f 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -13,6 +13,7 @@ class Debate < ActiveRecord::Base include ActsAsParanoidAliases belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :public_author, -> { with_public_activity }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 1f9ede642..64cadbb24 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -14,7 +14,7 @@ class Proposal < ActiveRecord::Base RETIRE_OPTIONS = %w(duplicated started unfeasible done other) belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' - belongs_to :public_author, -> { public_activity }, class_name: 'User', foreign_key: 'author_id' + belongs_to :public_author, -> { with_public_activity }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable has_many :proposal_notifications diff --git a/app/models/user.rb b/app/models/user.rb index c82ae3435..4952c286c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -54,7 +54,7 @@ class User < ActiveRecord::Base scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } scope :email_digest, -> { where(email_digest: true) } scope :active, -> { where(erased_at: nil) } - scope :public_activity, -> { where(public_activity: true) } + scope :with_public_activity, -> { where(public_activity: true) } before_validation :clean_document_number From 2741dcf03ca384ce91f9c41a09bdd10bb5b9b17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 26 Dec 2016 20:45:46 +0100 Subject: [PATCH 071/147] Temporary remove organizations from API --- config/initializers/graphql.rb | 2 -- spec/lib/graphql_spec.rb | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index f0b27dac6..420d53dcf 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,9 +1,7 @@ API_TYPE_DEFINITIONS = { - User => %I[ id username proposals organization ], Debate => %I[ id title description author_id author created_at comments ], Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments public_author ], Comment => %I[ id body user_id user commentable_id ], - Organization => %I[ id name ] } type_creator = GraphQL::TypeCreator.new diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index eed7c6d34..3dbf393b7 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -29,6 +29,7 @@ describe ConsulSchema do end it "returns has_one associations" do + pending "Organizations are not being exposed yet" organization = create(:organization) response = execute("{ user(id: #{organization.user_id}) { organization { name } } }") expect(dig(response, 'data.user.organization.name')).to eq(organization.name) @@ -52,6 +53,7 @@ describe ConsulSchema do end it "executes deeply nested queries" do + pending "Organizations are not being exposed yet" org_user = create(:user) organization = create(:organization, user: org_user) org_proposal = create(:proposal, author: org_user) From 506696df0a31ee1602af432f5c0448ff3cb354fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 27 Dec 2016 18:17:02 +0100 Subject: [PATCH 072/147] Call ::public_for_api if available --- config/initializers/graphql.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 420d53dcf..02c8c750f 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -34,17 +34,25 @@ QueryRoot = GraphQL::ObjectType.define do type created_type description "Find one #{model.model_name.human} by ID" argument :id, !types.ID - resolve -> (object, arguments, context) { - model.find(arguments["id"]) - } + resolve -> (object, arguments, context) do + if model.respond_to?(:public_for_api) + model.public_for_api.find(arguments["id"]) + else + model.find(arguments["id"]) + end + end end # create an entry filed to retrive a paginated collection connection model.name.underscore.pluralize.to_sym, created_type.connection_type do description "Find all #{model.model_name.human.pluralize}" - resolve -> (object, arguments, context) { - model.all - } + resolve -> (object, arguments, context) do + if model.respond_to?(:public_for_api) + model.public_for_api + else + model.all + end + end end end From 0e501e2edd6f984ac522876a7158d263edf80f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 27 Dec 2016 18:18:00 +0100 Subject: [PATCH 073/147] Update API fields for Debate, User and Geozone --- config/initializers/graphql.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 02c8c750f..bf5d8d8f9 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,7 +1,9 @@ API_TYPE_DEFINITIONS = { - Debate => %I[ id title description author_id author created_at comments ], + User => %I[ id username proposals ], + Debate => %I[ id title description created_at cached_votes_total cached_votes_up cached_votes_down comments_count hot_score confidence_score geozone_id geozone comments public_author ], Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments public_author ], Comment => %I[ id body user_id user commentable_id ], + Geozone => %I[ id name ] } type_creator = GraphQL::TypeCreator.new From 90ff84b6efb4db78d18b74b10f84fbd5c6971089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 27 Dec 2016 18:18:22 +0100 Subject: [PATCH 074/147] Update API fields for Comment --- app/models/comment.rb | 7 ++++++ config/initializers/graphql.rb | 2 +- spec/models/comment_spec.rb | 41 ++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index 0bfb6a320..a1d89a33f 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -16,6 +16,7 @@ class Comment < ActiveRecord::Base belongs_to :commentable, -> { with_hidden }, polymorphic: true, counter_cache: true belongs_to :user, -> { with_hidden } + belongs_to :public_author, -> { with_public_activity }, class_name: 'User', foreign_key: 'user_id' before_save :calculate_confidence_score @@ -24,6 +25,12 @@ class Comment < ActiveRecord::Base scope :not_as_admin_or_moderator, -> { where("administrator_id IS NULL").where("moderator_id IS NULL")} scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } + scope :public_for_api, -> do + joins("FULL OUTER JOIN debates ON commentable_type = 'Debate' AND commentable_id = debates.id"). + joins("FULL OUTER JOIN proposals ON commentable_type = 'Proposal' AND commentable_id = proposals.id"). + where("commentable_type = 'Proposal' AND proposals.hidden_at IS NULL OR commentable_type = 'Debate' AND debates.hidden_at IS NULL") + end + scope :sort_by_most_voted, -> { order(confidence_score: :desc, created_at: :desc) } scope :sort_descendants_by_most_voted, -> { order(confidence_score: :desc, created_at: :asc) } diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index bf5d8d8f9..b6d98562b 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -2,7 +2,7 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals ], Debate => %I[ id title description created_at cached_votes_total cached_votes_up cached_votes_down comments_count hot_score confidence_score geozone_id geozone comments public_author ], Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments public_author ], - Comment => %I[ id body user_id user commentable_id ], + Comment => %I[ id commentable_id commentable_type body created_at cached_votes_total cached_votes_up cached_votes_down ancestry confidence_score public_author ], Geozone => %I[ id name ] } diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index b899b4e89..ecd167237 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -132,4 +132,45 @@ describe Comment do end end + describe "public_for_api" do + it "returns comments" do + comment = create(:comment) + + expect(Comment.public_for_api).to include(comment) + end + + it "does not return hidden comments" do + hidden_comment = create(:comment, :hidden) + + expect(Comment.public_for_api).not_to include(hidden_comment) + end + + it "returns comments on debates" do + debate = create(:debate) + comment = create(:comment, commentable: debate) + + expect(Comment.public_for_api).to include(comment) + end + + it "does not return comments on hidden debates" do + hidden_debate = create(:debate, :hidden) + comment = create(:comment, commentable: hidden_debate) + + expect(Comment.public_for_api).not_to include(comment) + end + + it "returns comments on proposals" do + proposal = create(:proposal) + comment = create(:comment, commentable: proposal) + + expect(Comment.public_for_api).to include(comment) + end + + it "does not return comments on hidden proposals" do + hidden_proposal = create(:proposal, :hidden) + comment = create(:comment, commentable: hidden_proposal) + + expect(Comment.public_for_api).not_to include(comment) + end + end end From 989680407085a5ea44b8d9f451ed97f120cb1d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 27 Dec 2016 18:30:35 +0100 Subject: [PATCH 075/147] Fixes typo in spec --- spec/models/user_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e04ea9291..239815c10 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -358,9 +358,9 @@ describe User do user2 = create(:user, public_activity: false) user3 = create(:user) - expect(User.public_activity).to include(user1) - expect(User.public_activity).not_to include(user2) - expect(User.public_activity).to include(user3) + expect(User.with_public_activity).to include(user1) + expect(User.with_public_activity).not_to include(user2) + expect(User.with_public_activity).to include(user3) end end end From 074a8182f61126d6ce477e04126d41f26bd65e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Dec 2016 10:58:40 +0100 Subject: [PATCH 076/147] Add ProposalNotification to api --- app/models/proposal_notification.rb | 2 ++ config/initializers/graphql.rb | 3 ++- spec/models/proposal_notification_spec.rb | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/models/proposal_notification.rb b/app/models/proposal_notification.rb index 60912d887..686f3ce90 100644 --- a/app/models/proposal_notification.rb +++ b/app/models/proposal_notification.rb @@ -7,6 +7,8 @@ class ProposalNotification < ActiveRecord::Base validates :proposal, presence: true validate :minimum_interval + scope :public_for_api, -> { joins(:proposal).where("proposals.hidden_at IS NULL") } + def minimum_interval return true if proposal.try(:notifications).blank? if proposal.notifications.last.created_at > (Time.current - Setting[:proposal_notification_minimum_interval_in_days].to_i.days).to_datetime diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index b6d98562b..b899476f7 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,9 +1,10 @@ API_TYPE_DEFINITIONS = { User => %I[ id username proposals ], Debate => %I[ id title description created_at cached_votes_total cached_votes_up cached_votes_down comments_count hot_score confidence_score geozone_id geozone comments public_author ], - Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments public_author ], + Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments proposal_notifications public_author ], Comment => %I[ id commentable_id commentable_type body created_at cached_votes_total cached_votes_up cached_votes_down ancestry confidence_score public_author ], Geozone => %I[ id name ] + ProposalNotification => %I[ title body proposal_id created_at proposal ], } type_creator = GraphQL::TypeCreator.new diff --git a/spec/models/proposal_notification_spec.rb b/spec/models/proposal_notification_spec.rb index f66b254b6..cd8bf346a 100644 --- a/spec/models/proposal_notification_spec.rb +++ b/spec/models/proposal_notification_spec.rb @@ -22,6 +22,22 @@ describe ProposalNotification do expect(notification).to_not be_valid end + describe "public_for_api scope" do + it "returns proposal notifications" do + proposal = create(:proposal) + notification = create(:proposal_notification, proposal: proposal) + + expect(ProposalNotification.public_for_api).to include(notification) + end + + it "blocks notifications whose proposal is hidden" do + proposal = create(:proposal, :hidden) + notification = create(:proposal_notification, proposal: proposal) + + expect(ProposalNotification.public_for_api).not_to include(notification) + end + end + describe "minimum interval between notifications" do before(:each) do From 80c3108a18eee72e4da5c2f932ccbeda162f6d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Dec 2016 11:13:57 +0100 Subject: [PATCH 077/147] Add Tag to api --- app/models/tag.rb | 3 +++ config/initializers/graphql.rb | 1 + spec/models/tag_spec.rb | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 app/models/tag.rb create mode 100644 spec/models/tag_spec.rb diff --git a/app/models/tag.rb b/app/models/tag.rb new file mode 100644 index 000000000..8f34a8696 --- /dev/null +++ b/app/models/tag.rb @@ -0,0 +1,3 @@ +class Tag < ActsAsTaggableOn::Tag + scope :public_for_api, -> { where("kind IS NULL OR kind = 'category'") } +end diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index b899476f7..a7e1bfa85 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -5,6 +5,7 @@ API_TYPE_DEFINITIONS = { Comment => %I[ id commentable_id commentable_type body created_at cached_votes_total cached_votes_up cached_votes_down ancestry confidence_score public_author ], Geozone => %I[ id name ] ProposalNotification => %I[ title body proposal_id created_at proposal ], + Tag => %I[ id name taggings_count kind ], } type_creator = GraphQL::TypeCreator.new diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb new file mode 100644 index 000000000..fa26b7622 --- /dev/null +++ b/spec/models/tag_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +describe 'Tag' do + + describe "public_for_api scope" do + it "returns tags whose kind is NULL" do + tag = create(:tag, kind: nil) + + expect(Tag.public_for_api).to include(tag) + end + + it "returns tags whose kind is 'category'" do + tag = create(:tag, kind: 'category') + + expect(Tag.public_for_api).to include(tag) + end + + it "blocks other kinds of tags" do + tag = create(:tag, kind: 'foo') + + expect(Tag.public_for_api).not_to include(tag) + end + end +end From 90130f20e796f42d0a54dd3d72accbc4ff9a21b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Dec 2016 12:03:00 +0100 Subject: [PATCH 078/147] Add Votes info to api --- app/models/user.rb | 18 ++++++------ app/models/vote.rb | 10 ++++++- config/initializers/graphql.rb | 5 ++-- spec/models/vote_spec.rb | 51 ++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 11 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 4952c286c..f1e48c572 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -46,15 +46,17 @@ class User < ActiveRecord::Base attr_accessor :skip_password_validation attr_accessor :use_redeemable_code - scope :administrators, -> { joins(:administrators) } - scope :moderators, -> { joins(:moderator) } - scope :organizations, -> { joins(:organization) } - scope :officials, -> { where("official_level > 0") } - scope :for_render, -> { includes(:organization) } - scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } - scope :email_digest, -> { where(email_digest: true) } - scope :active, -> { where(erased_at: nil) } + scope :administrators, -> { joins(:administrators) } + scope :moderators, -> { joins(:moderator) } + scope :organizations, -> { joins(:organization) } + scope :officials, -> { where("official_level > 0") } + scope :for_render, -> { includes(:organization) } + scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } + scope :email_digest, -> { where(email_digest: true) } + scope :active, -> { where(erased_at: nil) } scope :with_public_activity, -> { where(public_activity: true) } + scope :public_for_api, -> { select("id, username") } + scope :for_votes, -> { select("users.gender, users.geozone_id") } before_validation :clean_document_number diff --git a/app/models/vote.rb b/app/models/vote.rb index 47dd3f007..a6b0691c6 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,2 +1,10 @@ class Vote < ActsAsVotable::Vote -end \ No newline at end of file + belongs_to :public_voter, -> { for_votes }, class_name: 'User', foreign_key: :voter_id + + scope :public_for_api, -> do + joins("FULL OUTER JOIN debates ON votable_type = 'Debate' AND votable_id = debates.id"). + joins("FULL OUTER JOIN proposals ON votable_type = 'Proposal' AND votable_id = proposals.id"). + joins("FULL OUTER JOIN comments ON votable_type = 'Comment' AND votable_id = comments.id"). + where("votable_type = 'Proposal' AND proposals.hidden_at IS NULL OR votable_type = 'Debate' AND debates.hidden_at IS NULL OR votable_type = 'Comment' AND comments.hidden_at IS NULL") + end +end diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index a7e1bfa85..e837f294b 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,11 +1,12 @@ API_TYPE_DEFINITIONS = { - User => %I[ id username proposals ], + User => %I[ id username gender geozone_id geozone ], Debate => %I[ id title description created_at cached_votes_total cached_votes_up cached_votes_down comments_count hot_score confidence_score geozone_id geozone comments public_author ], Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments proposal_notifications public_author ], Comment => %I[ id commentable_id commentable_type body created_at cached_votes_total cached_votes_up cached_votes_down ancestry confidence_score public_author ], - Geozone => %I[ id name ] + Geozone => %I[ id name ], ProposalNotification => %I[ title body proposal_id created_at proposal ], Tag => %I[ id name taggings_count kind ], + Vote => %I[ votable_id votable_type created_at public_voter ] } type_creator = GraphQL::TypeCreator.new diff --git a/spec/models/vote_spec.rb b/spec/models/vote_spec.rb index 1a44e7606..14929ef2c 100644 --- a/spec/models/vote_spec.rb +++ b/spec/models/vote_spec.rb @@ -40,4 +40,55 @@ describe 'Vote' do expect(vote.value).to eq(false) end end + + describe 'public_for_api scope' do + it 'returns votes on debates' do + debate = create(:debate) + vote = create(:vote, votable: debate) + + expect(Vote.public_for_api).to include(vote) + end + + it 'blocks votes on hidden debates' do + debate = create(:debate, :hidden) + vote = create(:vote, votable: debate) + + expect(Vote.public_for_api).not_to include(vote) + end + + it 'returns votes on proposals' do + proposal = create(:proposal) + vote = create(:vote, votable: proposal) + + expect(Vote.public_for_api).to include(vote) + end + + it 'blocks votes on hidden proposals' do + proposal = create(:proposal, :hidden) + vote = create(:vote, votable: proposal) + + expect(Vote.public_for_api).not_to include(vote) + end + + it 'returns votes on comments' do + comment = create(:comment) + vote = create(:vote, votable: comment) + + expect(Vote.public_for_api).to include(vote) + end + + it 'blocks votes on hidden comments' do + comment = create(:comment, :hidden) + vote = create(:vote, votable: comment) + + expect(Vote.public_for_api).not_to include(vote) + end + + it 'blocks any other kind of votes' do + spending_proposal = create(:spending_proposal) + vote = create(:vote, votable: spending_proposal) + + expect(Vote.public_for_api).not_to include(vote) + end + end end From 0aed5338f64d6dc9fb2825d85e818c8d35ad143d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Dec 2016 14:17:44 +0100 Subject: [PATCH 079/147] Customize resolvers to only permit allowed records --- lib/graph_ql/type_creator.rb | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 3f35c2c1e..74c9b6fbe 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -28,10 +28,22 @@ module GraphQL field(field_name.to_s, TYPES_CONVERSION[model.columns_hash[field_name.to_s].type]) else association = type_creator.class.association?(model, field_name) + target_model = association.klass + public_elements = target_model.respond_to?(:public_for_api) ? target_model.public_for_api : target_model.all + if type_creator.class.needs_pagination?(association) - connection association.name, -> { type_creator.created_types[association.klass].connection_type } + connection(association.name, -> { type_creator.created_types[target_model].connection_type }) do + resolve -> (object, arguments, context) do + object.send(association.name).all & public_elements.all + end + end else - field association.name, -> { type_creator.created_types[association.klass] } + field(association.name, -> { type_creator.created_types[target_model] }) do + resolve -> (object, arguments, context) do + linked_element = object.send(field_name) + public_elements.include?(linked_element) ? linked_element : nil + end + end end end end From 0f663603d0af0876e481d5c5b8a071335b346009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 29 Dec 2016 14:19:11 +0100 Subject: [PATCH 080/147] Only create field to retrieve single objects in QueryRoot if ID was exposed --- config/initializers/graphql.rb | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index e837f294b..d2353e308 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -35,15 +35,17 @@ QueryRoot = GraphQL::ObjectType.define do type_creator.created_types.each do |model, created_type| # create an entry field to retrive a single object - field model.name.underscore.to_sym do - type created_type - description "Find one #{model.model_name.human} by ID" - argument :id, !types.ID - resolve -> (object, arguments, context) do - if model.respond_to?(:public_for_api) - model.public_for_api.find(arguments["id"]) - else - model.find(arguments["id"]) + if API_TYPE_DEFINITIONS[model].include?(:id) + field model.name.underscore.to_sym do + type created_type + description "Find one #{model.model_name.human} by ID" + argument :id, !types.ID + resolve -> (object, arguments, context) do + if model.respond_to?(:public_for_api) + model.public_for_api.find(arguments["id"]) + else + model.find(arguments["id"]) + end end end end From 7dc4d80e7bbab49a45d818ccc321a2abfe291be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 1 Jan 2017 19:36:07 +0100 Subject: [PATCH 081/147] Read GraphQL field types from file instead of from database --- config/initializers/graphql.rb | 90 ++++++++++++++++++++++++++++++---- lib/graph_ql/type_creator.rb | 59 +++++++++++----------- spec/lib/type_creator_spec.rb | 16 +++--- 3 files changed, 120 insertions(+), 45 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index d2353e308..e9e64bac4 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,12 +1,84 @@ API_TYPE_DEFINITIONS = { - User => %I[ id username gender geozone_id geozone ], - Debate => %I[ id title description created_at cached_votes_total cached_votes_up cached_votes_down comments_count hot_score confidence_score geozone_id geozone comments public_author ], - Proposal => %I[ id title description external_url cached_votes_up comments_count hot_score confidence_score created_at summary video_url geozone_id retired_at retired_reason retired_explanation geozone comments proposal_notifications public_author ], - Comment => %I[ id commentable_id commentable_type body created_at cached_votes_total cached_votes_up cached_votes_down ancestry confidence_score public_author ], - Geozone => %I[ id name ], - ProposalNotification => %I[ title body proposal_id created_at proposal ], - Tag => %I[ id name taggings_count kind ], - Vote => %I[ votable_id votable_type created_at public_voter ] + User => { + id: :integer, + username: :string, + gender: :string, + geozone_id: :integer, + geozone: Geozone + }, + Debate => { + id: :integer, + title: :string, + description: :string, + created_at: :string, + cached_votes_total: :integer, + cached_votes_up: :integer, + cached_votes_down: :integer, + comments_count: :integer, + hot_score: :integer, + confidence_score: :integer, + geozone_id: :integer, + geozone: Geozone, + comments: [Comment], + public_author: User + }, + Proposal => { + id: :integer, + title: :string, + description: :sting, + external_url: :string, + cached_votes_up: :integer, + comments_count: :integer, + hot_score: :integer, + confidence_score: :integer, + created_at: :string, + summary: :string, + video_url: :string, + geozone_id: :integer, + retired_at: :string, + retired_reason: :string, + retired_explanation: :string, + geozone: Geozone, + comments: [Comment], + proposal_notifications: [ProposalNotification], + public_author: User + }, + Comment => { + id: :integer, + commentable_id: :integer, + commentable_type: :string, + body: :string, + created_at: :string, + cached_votes_total: :integer, + cached_votes_up: :integer, + cached_votes_down: :integer, + ancestry: :string, + confidence_score: :integer, + public_author: User + }, + Geozone => { + id: :integer, + name: :string + }, + ProposalNotification => { + title: :string, + body: :string, + proposal_id: :integer, + created_at: :string, + proposal: Proposal + }, + Tag => { + id: :integer, + name: :string, + taggings_count: :integer, + kind: :string + }, + Vote => { + votable_id: :integer, + votable_type: :string, + created_at: :string, + public_voter: User + } } type_creator = GraphQL::TypeCreator.new @@ -35,7 +107,7 @@ QueryRoot = GraphQL::ObjectType.define do type_creator.created_types.each do |model, created_type| # create an entry field to retrive a single object - if API_TYPE_DEFINITIONS[model].include?(:id) + if API_TYPE_DEFINITIONS[model][:id] field model.name.underscore.to_sym do type created_type description "Find one #{model.model_name.human} by ID" diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 74c9b6fbe..468577cff 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -1,12 +1,13 @@ module GraphQL class TypeCreator # Return a GraphQL type for a 'database_type' - TYPES_CONVERSION = Hash.new(GraphQL::STRING_TYPE).merge( + SCALAR_TYPES = { integer: GraphQL::INT_TYPE, boolean: GraphQL::BOOLEAN_TYPE, float: GraphQL::FLOAT_TYPE, - double: GraphQL::FLOAT_TYPE - ) + double: GraphQL::FLOAT_TYPE, + string: GraphQL::STRING_TYPE + } attr_accessor :created_types @@ -14,7 +15,7 @@ module GraphQL @created_types = {} end - def create(model, field_names) + def create(model, fields) type_creator = self created_type = GraphQL::ObjectType.define do @@ -23,41 +24,43 @@ module GraphQL description("#{model.model_name.human}") # Make a field for each column, association or method - field_names.each do |field_name| - if model.column_names.include?(field_name.to_s) - field(field_name.to_s, TYPES_CONVERSION[model.columns_hash[field_name.to_s].type]) - else - association = type_creator.class.association?(model, field_name) - target_model = association.klass - public_elements = target_model.respond_to?(:public_for_api) ? target_model.public_for_api : target_model.all - - if type_creator.class.needs_pagination?(association) - connection(association.name, -> { type_creator.created_types[target_model].connection_type }) do - resolve -> (object, arguments, context) do - object.send(association.name).all & public_elements.all - end + fields.each do |name, type| + case GraphQL::TypeCreator.type_kind(type) + when :scalar + field name, SCALAR_TYPES[type] + when :simple_association + field(name, -> { type_creator.created_types[type] }) do + resolve -> (object, arguments, context) do + target_public_elements = type.respond_to?(:public_for_api) ? type.public_for_api : type.all + wanted_element = object.send(name) + target_public_elements.include?(wanted_element) ? wanted_element : nil end - else - field(association.name, -> { type_creator.created_types[target_model] }) do - resolve -> (object, arguments, context) do - linked_element = object.send(field_name) - public_elements.include?(linked_element) ? linked_element : nil - end + end + when :paginated_association + type = type.first + connection(name, -> { type_creator.created_types[type].connection_type }) do + resolve -> (object, arguments, context) do + target_public_elements = type.respond_to?(:public_for_api) ? type.public_for_api : type.all + object.send(name).all & target_public_elements.all end end end end + end created_types[model] = created_type return created_type # GraphQL::ObjectType end - def self.association?(model, field_name) - model.reflect_on_all_associations.find { |a| a.name == field_name } + def self.type_kind(type) + if SCALAR_TYPES[type] + :scalar + elsif type.class == Class + :simple_association + elsif type.class == Array + :paginated_association + end end - def self.needs_pagination?(association) - association.macro == :has_many - end end end diff --git a/spec/lib/type_creator_spec.rb b/spec/lib/type_creator_spec.rb index 2fc1bf78d..1e25b91fd 100644 --- a/spec/lib/type_creator_spec.rb +++ b/spec/lib/type_creator_spec.rb @@ -5,7 +5,7 @@ describe GraphQL::TypeCreator do describe "::create" do it "creates fields for Int attributes" do - debate_type = type_creator.create(Debate, %I[ id ]) + debate_type = type_creator.create(Debate, { id: :integer }) created_field = debate_type.fields['id'] expect(created_field).to be_a(GraphQL::Field) @@ -14,7 +14,7 @@ describe GraphQL::TypeCreator do end it "creates fields for String attributes" do - debate_type = type_creator.create(Debate, %I[ title ]) + debate_type = type_creator.create(Debate, { title: :string }) created_field = debate_type.fields['title'] expect(created_field).to be_a(GraphQL::Field) @@ -23,8 +23,8 @@ describe GraphQL::TypeCreator do end it "creates connections for :belongs_to associations" do - user_type = type_creator.create(User, %I[ id ]) - debate_type = type_creator.create(Debate, %I[ author ]) + user_type = type_creator.create(User, { id: :integer }) + debate_type = type_creator.create(Debate, { author: User }) connection = debate_type.fields['author'] @@ -34,8 +34,8 @@ describe GraphQL::TypeCreator do end it "creates connections for :has_one associations" do - user_type = type_creator.create(User, %I[ organization ]) - organization_type = type_creator.create(Organization, %I[ id ]) + user_type = type_creator.create(User, { organization: Organization }) + organization_type = type_creator.create(Organization, { id: :integer }) connection = user_type.fields['organization'] @@ -45,8 +45,8 @@ describe GraphQL::TypeCreator do end it "creates connections for :has_many associations" do - comment_type = type_creator.create(Comment, %I[ id ]) - debate_type = type_creator.create(Debate, %I[ comments ]) + comment_type = type_creator.create(Comment, { id: :integer }) + debate_type = type_creator.create(Debate, { comments: [Comment] }) connection = debate_type.fields['comments'] From 9c6001e9874e8587dfd71e6a498aca0fb74ce5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 1 Jan 2017 20:46:08 +0100 Subject: [PATCH 082/147] Move public_author into module --- app/models/comment.rb | 2 +- app/models/concerns/has_public_author.rb | 5 +++++ app/models/debate.rb | 2 +- app/models/proposal.rb | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 app/models/concerns/has_public_author.rb diff --git a/app/models/comment.rb b/app/models/comment.rb index a1d89a33f..1d5c9aa30 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,5 +1,6 @@ class Comment < ActiveRecord::Base include Flaggable + include HasPublicAuthor acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases @@ -16,7 +17,6 @@ class Comment < ActiveRecord::Base belongs_to :commentable, -> { with_hidden }, polymorphic: true, counter_cache: true belongs_to :user, -> { with_hidden } - belongs_to :public_author, -> { with_public_activity }, class_name: 'User', foreign_key: 'user_id' before_save :calculate_confidence_score diff --git a/app/models/concerns/has_public_author.rb b/app/models/concerns/has_public_author.rb new file mode 100644 index 000000000..a1af255c5 --- /dev/null +++ b/app/models/concerns/has_public_author.rb @@ -0,0 +1,5 @@ +module HasPublicAuthor + def public_author + self.author.public_activity? ? self.author : nil + end +end diff --git a/app/models/debate.rb b/app/models/debate.rb index 13cff5b3f..5e7491b83 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -7,13 +7,13 @@ class Debate < ActiveRecord::Base include Sanitizable include Searchable include Filterable + include HasPublicAuthor acts_as_votable acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' - belongs_to :public_author, -> { with_public_activity }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 64cadbb24..ad51436b0 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -6,6 +6,7 @@ class Proposal < ActiveRecord::Base include Sanitizable include Searchable include Filterable + include HasPublicAuthor acts_as_votable acts_as_paranoid column: :hidden_at @@ -14,7 +15,6 @@ class Proposal < ActiveRecord::Base RETIRE_OPTIONS = %w(duplicated started unfeasible done other) belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' - belongs_to :public_author, -> { with_public_activity }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable has_many :proposal_notifications From 4783f14eb409573119a961464901ea20c9a630f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 00:13:16 +0100 Subject: [PATCH 083/147] Show voter info in api --- app/models/user.rb | 2 -- app/models/vote.rb | 5 ++++- app/models/voter.rb | 1 + config/initializers/graphql.rb | 21 ++++++++++++--------- lib/graph_ql/type_creator.rb | 11 ++++++++++- 5 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 app/models/voter.rb diff --git a/app/models/user.rb b/app/models/user.rb index f1e48c572..893ddd56d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -55,8 +55,6 @@ class User < ActiveRecord::Base scope :email_digest, -> { where(email_digest: true) } scope :active, -> { where(erased_at: nil) } scope :with_public_activity, -> { where(public_activity: true) } - scope :public_for_api, -> { select("id, username") } - scope :for_votes, -> { select("users.gender, users.geozone_id") } before_validation :clean_document_number diff --git a/app/models/vote.rb b/app/models/vote.rb index a6b0691c6..f51e67ef2 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,5 +1,4 @@ class Vote < ActsAsVotable::Vote - belongs_to :public_voter, -> { for_votes }, class_name: 'User', foreign_key: :voter_id scope :public_for_api, -> do joins("FULL OUTER JOIN debates ON votable_type = 'Debate' AND votable_id = debates.id"). @@ -7,4 +6,8 @@ class Vote < ActsAsVotable::Vote joins("FULL OUTER JOIN comments ON votable_type = 'Comment' AND votable_id = comments.id"). where("votable_type = 'Proposal' AND proposals.hidden_at IS NULL OR votable_type = 'Debate' AND debates.hidden_at IS NULL OR votable_type = 'Comment' AND comments.hidden_at IS NULL") end + + def public_voter + User.select("gender, geozone_id, date_of_birth").where(id: self.voter_id).first + end end diff --git a/app/models/voter.rb b/app/models/voter.rb new file mode 100644 index 000000000..021d1c768 --- /dev/null +++ b/app/models/voter.rb @@ -0,0 +1 @@ +Voter = User diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index e9e64bac4..e6e8be2f4 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,12 +1,15 @@ API_TYPE_DEFINITIONS = { - User => { + User => { id: :integer, - username: :string, - gender: :string, - geozone_id: :integer, - geozone: Geozone + username: :string }, - Debate => { + Voter => { + gender: :string, + date_of_birth: :string, + geozone_id: :integer, + geozone: Geozone + }, + Debate => { id: :integer, title: :string, description: :string, @@ -43,7 +46,7 @@ API_TYPE_DEFINITIONS = { proposal_notifications: [ProposalNotification], public_author: User }, - Comment => { + Comment => { id: :integer, commentable_id: :integer, commentable_type: :string, @@ -56,7 +59,7 @@ API_TYPE_DEFINITIONS = { confidence_score: :integer, public_author: User }, - Geozone => { + Geozone => { id: :integer, name: :string }, @@ -77,7 +80,7 @@ API_TYPE_DEFINITIONS = { votable_id: :integer, votable_type: :string, created_at: :string, - public_voter: User + public_voter: Voter } } diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 468577cff..aa303ec58 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -33,7 +33,11 @@ module GraphQL resolve -> (object, arguments, context) do target_public_elements = type.respond_to?(:public_for_api) ? type.public_for_api : type.all wanted_element = object.send(name) - target_public_elements.include?(wanted_element) ? wanted_element : nil + if target_public_elements.include?(wanted_element) || GraphQL::TypeCreator.matching_exceptions.include?(name) + wanted_element + else + nil + end end end when :paginated_association @@ -62,5 +66,10 @@ module GraphQL end end + # TODO: esto es una ñapa, hay que buscar una solución mejor + def self.matching_exceptions + [:public_voter] + end + end end From 8fe38f889bd9c639011e718f62582231ceeac1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 10:52:27 +0100 Subject: [PATCH 084/147] Fix typo --- config/initializers/graphql.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index e6e8be2f4..cc242e722 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -28,7 +28,7 @@ API_TYPE_DEFINITIONS = { Proposal => { id: :integer, title: :string, - description: :sting, + description: :string, external_url: :string, cached_votes_up: :integer, comments_count: :integer, From ac6802572fe76b4fac8ed367a0ec7086ef6977e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 11:06:24 +0100 Subject: [PATCH 085/147] Fix collision problems related to User and Voter classes --- app/models/voter.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/voter.rb b/app/models/voter.rb index 021d1c768..7d4f52676 100644 --- a/app/models/voter.rb +++ b/app/models/voter.rb @@ -1 +1,2 @@ -Voter = User +class Voter < User +end From eadceb106c98fc7caf341b4c0c910fa954a10756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 11:27:56 +0100 Subject: [PATCH 086/147] Only mount graphiql in development environment --- Gemfile | 2 +- config/routes.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 16da51749..b9799f3d6 100644 --- a/Gemfile +++ b/Gemfile @@ -66,7 +66,6 @@ gem 'turnout', '~> 2.4.0' gem 'redcarpet' gem 'graphql' -gem 'graphiql-rails' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console @@ -104,6 +103,7 @@ end group :development do # Access an IRB console on exception pages or by using <%= console %> in views gem 'web-console', '3.3.0' + gem 'graphiql-rails' end eval_gemfile './Gemfile_custom' diff --git a/config/routes.rb b/config/routes.rb index d8f0d4476..5a8d8c2b2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -265,12 +265,12 @@ Rails.application.routes.draw do end # GraphQL - mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql' get '/graphql', to: 'graphql#query' post '/graphql', to: 'graphql#query' if Rails.env.development? mount LetterOpenerWeb::Engine, at: "/letter_opener" + mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql' end mount Tolk::Engine => '/translate', :as => 'tolk' From a2befe6bc1ceb3cb71c7a310e96a129e989f9f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 11:28:05 +0100 Subject: [PATCH 087/147] Remove unused gem --- Gemfile | 1 - Gemfile.lock | 15 --------------- 2 files changed, 16 deletions(-) diff --git a/Gemfile b/Gemfile index b9799f3d6..94d66a281 100644 --- a/Gemfile +++ b/Gemfile @@ -97,7 +97,6 @@ group :test do gem 'poltergeist' gem 'coveralls', require: false gem 'email_spec' - gem 'http' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 1832910e5..ed7d74082 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -140,8 +140,6 @@ GEM railties (>= 3.2.6, < 5.0) diff-lcs (1.2.5) docile (1.1.5) - domain_name (0.5.20161021) - unf (>= 0.0.5, < 1.0.0) easy_translate (0.5.0) json thread @@ -188,15 +186,6 @@ GEM hashie (3.4.6) highline (1.7.8) htmlentities (4.3.4) - http (2.1.0) - addressable (~> 2.3) - http-cookie (~> 1.0) - http-form_data (~> 1.0.1) - http_parser.rb (~> 0.6.0) - http-cookie (1.0.3) - domain_name (~> 0.5) - http-form_data (1.0.1) - http_parser.rb (0.6.0) httpi (2.4.1) rack i18n (0.7.0) @@ -437,9 +426,6 @@ GEM thread_safe (~> 0.1) uglifier (3.0.4) execjs (>= 0.3.0, < 3) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.2) unicode-display_width (1.1.1) unicorn (5.1.0) kgio (~> 2.6) @@ -500,7 +486,6 @@ DEPENDENCIES graphiql-rails graphql groupdate (~> 3.1.0) - http i18n-tasks initialjs-rails (= 0.2.0.4) invisible_captcha (~> 0.9.1) From 508360cfa5f3e6644b25ccda229b4abbf83c9ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 11:35:19 +0100 Subject: [PATCH 088/147] Remove unused User scope --- app/models/user.rb | 1 - spec/models/user_spec.rb | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 893ddd56d..03f2db27b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -54,7 +54,6 @@ class User < ActiveRecord::Base scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } scope :email_digest, -> { where(email_digest: true) } scope :active, -> { where(erased_at: nil) } - scope :with_public_activity, -> { where(public_activity: true) } before_validation :clean_document_number diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 239815c10..ed0bac164 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -351,18 +351,6 @@ describe User do end end - - describe "public activity" do - it "returns users whose activity feed is public" do - user1 = create(:user) - user2 = create(:user, public_activity: false) - user3 = create(:user) - - expect(User.with_public_activity).to include(user1) - expect(User.with_public_activity).not_to include(user2) - expect(User.with_public_activity).to include(user3) - end - end end describe "self.search" do From f4e0ef7eea81577487bc864b3ee57de61dee44ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 12:00:58 +0100 Subject: [PATCH 089/147] Truncate votes api timestamp to hour --- app/models/vote.rb | 4 ++++ config/initializers/graphql.rb | 8 ++++---- spec/models/vote_spec.rb | 7 +++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/models/vote.rb b/app/models/vote.rb index f51e67ef2..3f60688d7 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -10,4 +10,8 @@ class Vote < ActsAsVotable::Vote def public_voter User.select("gender, geozone_id, date_of_birth").where(id: self.voter_id).first end + + def public_timestamp + self.created_at.change(min: 0) + end end diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index cc242e722..fdb5975e3 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -77,10 +77,10 @@ API_TYPE_DEFINITIONS = { kind: :string }, Vote => { - votable_id: :integer, - votable_type: :string, - created_at: :string, - public_voter: Voter + votable_id: :integer, + votable_type: :string, + public_timestamp: :string, + public_voter: Voter } } diff --git a/spec/models/vote_spec.rb b/spec/models/vote_spec.rb index 14929ef2c..8e5db0ce7 100644 --- a/spec/models/vote_spec.rb +++ b/spec/models/vote_spec.rb @@ -91,4 +91,11 @@ describe 'Vote' do expect(Vote.public_for_api).not_to include(vote) end end + + describe '#public_timestamp' do + it "truncates created_at timestamp up to minutes" do + vote = create(:vote, created_at: Time.zone.parse('2016-02-10 15:30:45')) + expect(vote.public_timestamp).to eq(Time.zone.parse('2016-02-10 15:00:00')) + end + end end From 0b4004042f95e28e8695286abd5e5aaca346db91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 2 Jan 2017 14:12:37 +0100 Subject: [PATCH 090/147] Expose age_range instead of date_of_birth for Voter/User class --- app/models/user.rb | 4 ++++ config/initializers/graphql.rb | 8 ++++---- lib/age_range_calculator.rb | 15 +++++++++++++++ spec/lib/age_range_calculator_spec.rb | 14 ++++++++++++++ spec/models/user_spec.rb | 6 ++++++ 5 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 lib/age_range_calculator.rb create mode 100644 spec/lib/age_range_calculator_spec.rb diff --git a/app/models/user.rb b/app/models/user.rb index 03f2db27b..04ca708a9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -244,6 +244,10 @@ class User < ActiveRecord::Base end delegate :can?, :cannot?, to: :ability + def age_range + AgeRangeCalculator::range_from_birthday(self.date_of_birth).to_s + end + private def clean_document_number diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index fdb5975e3..6fb30ca45 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -4,10 +4,10 @@ API_TYPE_DEFINITIONS = { username: :string }, Voter => { - gender: :string, - date_of_birth: :string, - geozone_id: :integer, - geozone: Geozone + gender: :string, + age_range: :string, + geozone_id: :integer, + geozone: Geozone }, Debate => { id: :integer, diff --git a/lib/age_range_calculator.rb b/lib/age_range_calculator.rb new file mode 100644 index 000000000..acb12e19e --- /dev/null +++ b/lib/age_range_calculator.rb @@ -0,0 +1,15 @@ +class AgeRangeCalculator + + MIN_AGE = 16 + MAX_AGE = 1.0/0.0 # Infinity + RANGES = [ (MIN_AGE..25), (26..40), (41..60), (61..MAX_AGE) ] + + def self.range_from_birthday(dob) + # Inspired by: http://stackoverflow.com/questions/819263/get-persons-age-in-ruby/2357790#2357790 + now = Time.current.to_date + age = now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1) + + index = RANGES.find_index { |range| range.include?(age) } + index ? RANGES[index] : nil + end +end diff --git a/spec/lib/age_range_calculator_spec.rb b/spec/lib/age_range_calculator_spec.rb new file mode 100644 index 000000000..a6fd183ed --- /dev/null +++ b/spec/lib/age_range_calculator_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +describe AgeRangeCalculator do + subject { AgeRangeCalculator } + + describe '::range_from_birthday' do + it 'returns the age range' do + expect(subject::range_from_birthday(Time.current - 1.year)).to eq(nil) + expect(subject::range_from_birthday(Time.current - 26.year)).to eq(26..40) + expect(subject::range_from_birthday(Time.current - 60.year)).to eq(41..60) + expect(subject::range_from_birthday(Time.current - 200.year)).to eq(61..subject::MAX_AGE) + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ed0bac164..ca59cb288 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -469,4 +469,10 @@ describe User do end + describe "#age_range" do + it 'returns string representation of age range' do + user = create(:user, date_of_birth: Time.current - 41.years) + expect(user.age_range).to eq('41..60') + end + end end From 2c5925f9474a7e515d1d97a332dfd0b105c042a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 3 Jan 2017 13:24:00 +0100 Subject: [PATCH 091/147] Refactor GraphqlController --- app/controllers/graphql_controller.rb | 52 +++++++++++++++++---------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index be4f51f03..de7ae7de7 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -1,28 +1,44 @@ class GraphqlController < ApplicationController + attr_accessor :query_variables, :query_string skip_before_action :verify_authenticity_token skip_authorization_check + class QueryStringError < StandardError; end + def query - - if request.headers["CONTENT_TYPE"] == 'application/graphql' - query_string = request.body.string # request.body.class => StringIO - else - query_string = params[:query] - end - - if query_string.nil? + begin + set_query_environment + response = ConsulSchema.execute query_string, variables: query_variables + render json: response, status: :ok + rescue GraphqlController::QueryStringError render json: { message: 'Query string not present' }, status: :bad_request - else - begin - response = ConsulSchema.execute( - query_string, - variables: params[:variables] || {} - ) - render json: response, status: :ok - rescue GraphQL::ParseError - render json: { message: 'Query string is not valid JSON' }, status: :bad_request - end + rescue GraphQL::ParseError + render json: { message: 'Query string is not valid JSON' }, status: :bad_request end end + + private + + def set_query_environment + set_query_string + set_query_variables + end + + def set_query_string + if request.headers["CONTENT_TYPE"] == 'application/graphql' + @query_string = request.body.string # request.body.class => StringIO + else + @query_string = params[:query] + end + if query_string.nil? then raise GraphqlController::QueryStringError end + end + + def set_query_variables + if params[:variables].nil? + @query_variables = {} + else + @query_variables = params[:variables] + end + end end From 4e12e9055cf42574a577248542866caaf2a399d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 3 Jan 2017 16:27:11 +0100 Subject: [PATCH 092/147] Refactor GraphQL::TypeCreator --- lib/graph_ql/type_creator.rb | 49 ++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index aa303ec58..2a4a4ad45 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -24,29 +24,18 @@ module GraphQL description("#{model.model_name.human}") # Make a field for each column, association or method - fields.each do |name, type| - case GraphQL::TypeCreator.type_kind(type) + fields.each do |field_name, field_type| + case TypeCreator.type_kind(field_type) when :scalar - field name, SCALAR_TYPES[type] + field(field_name, SCALAR_TYPES[field_type]) when :simple_association - field(name, -> { type_creator.created_types[type] }) do - resolve -> (object, arguments, context) do - target_public_elements = type.respond_to?(:public_for_api) ? type.public_for_api : type.all - wanted_element = object.send(name) - if target_public_elements.include?(wanted_element) || GraphQL::TypeCreator.matching_exceptions.include?(name) - wanted_element - else - nil - end - end + field(field_name, -> { type_creator.created_types[field_type] }) do + resolve TypeCreator.create_association_resolver(field_name, field_type) end when :paginated_association - type = type.first - connection(name, -> { type_creator.created_types[type].connection_type }) do - resolve -> (object, arguments, context) do - target_public_elements = type.respond_to?(:public_for_api) ? type.public_for_api : type.all - object.send(name).all & target_public_elements.all - end + field_type = field_type.first + connection(field_name, -> { type_creator.created_types[field_type].connection_type }) do + resolve TypeCreator.create_association_resolver(field_name, field_type) end end end @@ -56,6 +45,28 @@ module GraphQL return created_type # GraphQL::ObjectType end + def self.create_association_resolver(field_name, field_type) + -> (object, arguments, context) do + allowed_elements = target_public_elements(field_type) + requested_elements = object.send(field_name) + filter_forbidden_elements(requested_elements, allowed_elements, field_name) + end + end + + def self.target_public_elements(field_type) + field_type.respond_to?(:public_for_api) ? field_type.public_for_api : field_type.all + end + + def self.filter_forbidden_elements(requested, allowed_elements, field_name=nil) + if field_name && matching_exceptions.include?(field_name) + requested + elsif requested.respond_to?(:each) + requested.all & allowed_elements.all + else + allowed_elements.include?(requested) ? requested : nil + end + end + def self.type_kind(type) if SCALAR_TYPES[type] :scalar From cdc3b4637036dc1b7502147521a3b1719affa4ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 3 Jan 2017 16:52:13 +0100 Subject: [PATCH 093/147] Extract TypeCreator methods into AssociationResolver class --- lib/graph_ql/association_resolver.rb | 35 ++++++++++++++++++++++++++++ lib/graph_ql/type_creator.rb | 32 ++----------------------- 2 files changed, 37 insertions(+), 30 deletions(-) create mode 100644 lib/graph_ql/association_resolver.rb diff --git a/lib/graph_ql/association_resolver.rb b/lib/graph_ql/association_resolver.rb new file mode 100644 index 000000000..a896a0644 --- /dev/null +++ b/lib/graph_ql/association_resolver.rb @@ -0,0 +1,35 @@ +module GraphQL + class AssociationResolver + attr_reader :field_name, :target_model, :allowed_elements + + def initialize(field_name, target_model) + @field_name = field_name + @target_model = target_model + @allowed_elements = target_public_elements + end + + def call(object, arguments, context) + requested_elements = object.send(field_name) + filter_forbidden_elements(requested_elements) + end + + def target_public_elements + target_model.respond_to?(:public_for_api) ? target_model.public_for_api : target_model.all + end + + def filter_forbidden_elements(requested_elements) + if AssociationResolver.matching_exceptions.include?(field_name) + requested_elements + elsif requested_elements.respond_to?(:each) + requested_elements.all & allowed_elements.all + else + allowed_elements.include?(requested_elements) ? requested_elements : nil + end + end + + def self.matching_exceptions + [:public_voter] + end + + end +end diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 2a4a4ad45..b77360c72 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -1,6 +1,5 @@ module GraphQL class TypeCreator - # Return a GraphQL type for a 'database_type' SCALAR_TYPES = { integer: GraphQL::INT_TYPE, boolean: GraphQL::BOOLEAN_TYPE, @@ -30,12 +29,12 @@ module GraphQL field(field_name, SCALAR_TYPES[field_type]) when :simple_association field(field_name, -> { type_creator.created_types[field_type] }) do - resolve TypeCreator.create_association_resolver(field_name, field_type) + resolve GraphQL::AssociationResolver.new(field_name, field_type) end when :paginated_association field_type = field_type.first connection(field_name, -> { type_creator.created_types[field_type].connection_type }) do - resolve TypeCreator.create_association_resolver(field_name, field_type) + resolve GraphQL::AssociationResolver.new(field_name, field_type) end end end @@ -45,28 +44,6 @@ module GraphQL return created_type # GraphQL::ObjectType end - def self.create_association_resolver(field_name, field_type) - -> (object, arguments, context) do - allowed_elements = target_public_elements(field_type) - requested_elements = object.send(field_name) - filter_forbidden_elements(requested_elements, allowed_elements, field_name) - end - end - - def self.target_public_elements(field_type) - field_type.respond_to?(:public_for_api) ? field_type.public_for_api : field_type.all - end - - def self.filter_forbidden_elements(requested, allowed_elements, field_name=nil) - if field_name && matching_exceptions.include?(field_name) - requested - elsif requested.respond_to?(:each) - requested.all & allowed_elements.all - else - allowed_elements.include?(requested) ? requested : nil - end - end - def self.type_kind(type) if SCALAR_TYPES[type] :scalar @@ -77,10 +54,5 @@ module GraphQL end end - # TODO: esto es una ñapa, hay que buscar una solución mejor - def self.matching_exceptions - [:public_voter] - end - end end From 9cfb3eb52e246f0b62cb147ab89d2d78cea400b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 3 Jan 2017 18:30:53 +0100 Subject: [PATCH 094/147] Write specs for GraphQL::AssociationResolver --- lib/graph_ql/association_resolver.rb | 29 +++++++------ spec/lib/association_resolver_spec.rb | 62 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 spec/lib/association_resolver_spec.rb diff --git a/lib/graph_ql/association_resolver.rb b/lib/graph_ql/association_resolver.rb index a896a0644..142b24313 100644 --- a/lib/graph_ql/association_resolver.rb +++ b/lib/graph_ql/association_resolver.rb @@ -13,23 +13,24 @@ module GraphQL filter_forbidden_elements(requested_elements) end - def target_public_elements - target_model.respond_to?(:public_for_api) ? target_model.public_for_api : target_model.all - end - - def filter_forbidden_elements(requested_elements) - if AssociationResolver.matching_exceptions.include?(field_name) - requested_elements - elsif requested_elements.respond_to?(:each) - requested_elements.all & allowed_elements.all - else - allowed_elements.include?(requested_elements) ? requested_elements : nil - end - end - def self.matching_exceptions [:public_voter] end + private + + def target_public_elements + target_model.respond_to?(:public_for_api) ? target_model.public_for_api : target_model.all + end + + def filter_forbidden_elements(requested_elements) + if AssociationResolver.matching_exceptions.include?(field_name) + requested_elements + elsif requested_elements.respond_to?(:each) + requested_elements.all & allowed_elements.all + else + allowed_elements.include?(requested_elements) ? requested_elements : nil + end + end end end diff --git a/spec/lib/association_resolver_spec.rb b/spec/lib/association_resolver_spec.rb new file mode 100644 index 000000000..adb8a8b34 --- /dev/null +++ b/spec/lib/association_resolver_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +describe GraphQL::AssociationResolver do + let(:comments_resolver) { GraphQL::AssociationResolver.new(:comments, Comment) } + let(:geozone_resolver) { GraphQL::AssociationResolver.new(:geozone, Geozone) } + let(:geozones_resolver) { GraphQL::AssociationResolver.new(:geozones, Geozone) } + + describe '#initialize' do + it 'sets allowed elements for unscoped models' do + geozone_1 = create(:geozone) + geozone_2 = create(:geozone) + + expect(geozones_resolver.allowed_elements).to match_array([geozone_1, geozone_2]) + end + + it 'sets allowed elements for scoped models' do + public_comment = create(:comment, commentable: create(:proposal)) + restricted_comment = create(:comment, commentable: create(:proposal, :hidden)) + + expect(comments_resolver.allowed_elements).to match_array([public_comment]) + end + end + + describe '#call' do + it 'resolves simple associations' do + geozone = create(:geozone) + proposal = create(:proposal, geozone: geozone) + + result = geozone_resolver.call(proposal, nil, nil) + + expect(result).to eq(geozone) + end + + it 'blocks forbidden elements when resolving simple associations' do + skip 'None of the current models allows this spec to be executed' + end + + it 'resolves paginated associations' do + proposal = create(:proposal) + comment_1 = create(:comment, commentable: proposal) + comment_2 = create(:comment, commentable: proposal) + comment_3 = create(:comment, commentable: create(:proposal)) + + result = comments_resolver.call(proposal, nil, nil) + + expect(result).to match_array([comment_1, comment_2]) + end + + it 'blocks forbidden elements when resolving paginated associations' do + proposal = create(:proposal, :hidden) + comment = create(:comment, commentable: proposal) + + result = comments_resolver.call(proposal, nil, nil) + + expect(result).to be_empty + end + + it 'permits all elements for exceptions' do + skip 'Current implementation is temporary' + end + end +end From b36deb03829d8e42932c821a05c8e41a3daf6311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 4 Jan 2017 14:26:30 +0100 Subject: [PATCH 095/147] Read API types and fields from config/api.yml --- config/api.yml | 84 ++++++++++++++++++++++++ config/initializers/graphql.rb | 116 ++++++++------------------------- 2 files changed, 112 insertions(+), 88 deletions(-) create mode 100644 config/api.yml diff --git a/config/api.yml b/config/api.yml new file mode 100644 index 000000000..bdf1b8175 --- /dev/null +++ b/config/api.yml @@ -0,0 +1,84 @@ +User: + fields: + id: integer + username: string +Voter: + options: [disable_filtering] + fields: + gender: string + age_range: string + geozone_id: integer + geozone: Geozone +Debate: + fields: + id: integer + title: string + description: string + created_at: string + cached_votes_total: integer + cached_votes_up: integer + cached_votes_down: integer + comments_count: integer + hot_score: integer + confidence_score: integer + geozone_id: integer + geozone: Geozone + comments: [Comment] + public_author: User +Proposal: + fields: + id: integer + title: string + description: string + external_url: string + cached_votes_up: integer + comments_count: integer + hot_score: integer + confidence_score: integer + created_at: string + summary: string + video_url: string + geozone_id: integer + retired_at: string + retired_reason: string + retired_explanation: string + geozone: Geozone + comments: [Comment] + proposal_notifications: [ProposalNotification] + public_author: User +Comment: + fields: + id: integer + commentable_id: integer + commentable_type: string + body: string + created_at: string + cached_votes_total: integer + cached_votes_up: integer + cached_votes_down: integer + ancestry: string + confidence_score: integer + public_author: User +Geozone: + fields: + id: integer + name: string +ProposalNotification: + fields: + title: string + body: string + proposal_id: integer + created_at: string + proposal: Proposal +Tag: + fields: + id: integer + name: string + taggings_count: integer + kind: string +Vote: + fields: + votable_id: integer + votable_type: string + public_timestamp: string + public_voter: Voter diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 6fb30ca45..2d257285d 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,93 +1,33 @@ -API_TYPE_DEFINITIONS = { - User => { - id: :integer, - username: :string - }, - Voter => { - gender: :string, - age_range: :string, - geozone_id: :integer, - geozone: Geozone - }, - Debate => { - id: :integer, - title: :string, - description: :string, - created_at: :string, - cached_votes_total: :integer, - cached_votes_up: :integer, - cached_votes_down: :integer, - comments_count: :integer, - hot_score: :integer, - confidence_score: :integer, - geozone_id: :integer, - geozone: Geozone, - comments: [Comment], - public_author: User - }, - Proposal => { - id: :integer, - title: :string, - description: :string, - external_url: :string, - cached_votes_up: :integer, - comments_count: :integer, - hot_score: :integer, - confidence_score: :integer, - created_at: :string, - summary: :string, - video_url: :string, - geozone_id: :integer, - retired_at: :string, - retired_reason: :string, - retired_explanation: :string, - geozone: Geozone, - comments: [Comment], - proposal_notifications: [ProposalNotification], - public_author: User - }, - Comment => { - id: :integer, - commentable_id: :integer, - commentable_type: :string, - body: :string, - created_at: :string, - cached_votes_total: :integer, - cached_votes_up: :integer, - cached_votes_down: :integer, - ancestry: :string, - confidence_score: :integer, - public_author: User - }, - Geozone => { - id: :integer, - name: :string - }, - ProposalNotification => { - title: :string, - body: :string, - proposal_id: :integer, - created_at: :string, - proposal: Proposal - }, - Tag => { - id: :integer, - name: :string, - taggings_count: :integer, - kind: :string - }, - Vote => { - votable_id: :integer, - votable_type: :string, - public_timestamp: :string, - public_voter: Voter - } -} +api_config = YAML.load_file('./config/api.yml') + +API_TYPE_DEFINITIONS = {} + +# Parse API configuration file + +api_config.each do |api_type_model, api_type_info| + model = api_type_model.constantize + options = api_type_info['options'] + fields = {} + + api_type_info['fields'].each do |field_name, field_type| + if field_type.is_a?(Array) # paginated association + fields[field_name.to_sym] = [field_type.first.constantize] + elsif GraphQL::TypeCreator::SCALAR_TYPES[field_type.to_sym] + fields[field_name.to_sym] = field_type.to_sym + else # simple association + fields[field_name.to_sym] = field_type.constantize + end + end + + API_TYPE_DEFINITIONS[model] = { options: options, fields: fields } +end + +# Create all GraphQL types type_creator = GraphQL::TypeCreator.new -API_TYPE_DEFINITIONS.each do |model, fields| - type_creator.create(model, fields) +API_TYPE_DEFINITIONS.each do |model, info| + type_creator.create(model, info[:fields]) end ConsulSchema = GraphQL::Schema.define do @@ -110,7 +50,7 @@ QueryRoot = GraphQL::ObjectType.define do type_creator.created_types.each do |model, created_type| # create an entry field to retrive a single object - if API_TYPE_DEFINITIONS[model][:id] + if API_TYPE_DEFINITIONS[model][:fields][:id] field model.name.underscore.to_sym do type created_type description "Find one #{model.model_name.human} by ID" From 861a723724ca037fe23c946388a04d0550e6c89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 17:22:18 +0100 Subject: [PATCH 096/147] Only show public_voter if votable.total_votes are above threshold --- app/models/comment.rb | 1 + app/models/concerns/public_voters_stats.rb | 12 ++++++ app/models/debate.rb | 1 + app/models/proposal.rb | 1 + app/models/vote.rb | 2 +- db/dev_seeds.rb | 3 ++ spec/models/comment_spec.rb | 2 + .../concerns/public_voters_stats_spec.rb | 39 +++++++++++++++++++ spec/models/debate_spec.rb | 2 + spec/models/proposal_spec.rb | 2 + spec/models/vote_spec.rb | 19 +++++++++ spec/spec_helper.rb | 3 +- 12 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 app/models/concerns/public_voters_stats.rb create mode 100644 spec/models/concerns/public_voters_stats_spec.rb diff --git a/app/models/comment.rb b/app/models/comment.rb index 1d5c9aa30..c1e99fcef 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,6 +1,7 @@ class Comment < ActiveRecord::Base include Flaggable include HasPublicAuthor + include PublicVotersStats acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases diff --git a/app/models/concerns/public_voters_stats.rb b/app/models/concerns/public_voters_stats.rb new file mode 100644 index 000000000..88d695f20 --- /dev/null +++ b/app/models/concerns/public_voters_stats.rb @@ -0,0 +1,12 @@ +module PublicVotersStats + + def votes_above_threshold? + threshold = Setting["#{self.class.name.downcase}_api_votes_threshold"] + threshold = (threshold ? threshold.to_i : default_threshold) + (total_votes >= threshold) + end + + def default_threshold + 200 + end +end diff --git a/app/models/debate.rb b/app/models/debate.rb index 5e7491b83..c2a4eb7f6 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -8,6 +8,7 @@ class Debate < ActiveRecord::Base include Searchable include Filterable include HasPublicAuthor + include PublicVotersStats acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 6ddcf7085..f915f173a 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -7,6 +7,7 @@ class Proposal < ActiveRecord::Base include Searchable include Filterable include HasPublicAuthor + include PublicVotersStats acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/vote.rb b/app/models/vote.rb index 3f60688d7..af607ffa8 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -8,7 +8,7 @@ class Vote < ActsAsVotable::Vote end def public_voter - User.select("gender, geozone_id, date_of_birth").where(id: self.voter_id).first + votable.votes_above_threshold? ? voter : nil end def public_timestamp diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 0af5564e1..c73552d18 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -15,6 +15,9 @@ Setting.create(key: 'proposal_code_prefix', value: 'MAD') Setting.create(key: 'votes_for_proposal_success', value: '100') Setting.create(key: 'months_to_archive_proposals', value: '12') Setting.create(key: 'comments_body_max_length', value: '1000') +Settings.create(key: 'debate_api_votes_threshold', value: '150') +Settings.create(key: 'proposal_api_votes_threshold', value: '150') +Settings.create(key: 'comment_api_votes_threshold', value: '30') Setting.create(key: 'twitter_handle', value: '@consul_dev') Setting.create(key: 'twitter_hashtag', value: '#consul_dev') diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index ecd167237..e1f39ea0d 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -4,6 +4,8 @@ describe Comment do let(:comment) { build(:comment) } + it_behaves_like "public_voters_stats" + it "is valid" do expect(comment).to be_valid end diff --git a/spec/models/concerns/public_voters_stats_spec.rb b/spec/models/concerns/public_voters_stats_spec.rb new file mode 100644 index 000000000..eaa923599 --- /dev/null +++ b/spec/models/concerns/public_voters_stats_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +shared_examples_for 'public_voters_stats' do + let(:model) { described_class } # the class that includes the concern + + describe 'votes_above_threshold?' do + let(:votable) { create(model.to_s.underscore.to_sym) } + + context 'with default threshold value' do + it 'is true when votes are above threshold' do + 200.times { create(:vote, votable: votable) } + + expect(votable.votes_above_threshold?).to be_truthy + end + + it 'is false when votes are under threshold' do + 199.times { create(:vote, votable: votable) } + + expect(votable.votes_above_threshold?).to be_falsey + end + end + + context 'with custom threshold value' do + it 'is true when votes are above threshold' do + create(:setting, key: "#{model.to_s.underscore}_api_votes_threshold", value: '2') + 2.times { create(:vote, votable: votable) } + + expect(votable.votes_above_threshold?).to be_truthy + end + + it 'is false when votes are under threshold' do + create(:setting, key: "#{model.to_s.underscore}_api_votes_threshold", value: '2') + create(:vote, votable: votable) + + expect(votable.votes_above_threshold?).to be_falsey + end + end + end +end diff --git a/spec/models/debate_spec.rb b/spec/models/debate_spec.rb index 1b94393d1..7c40e63fe 100644 --- a/spec/models/debate_spec.rb +++ b/spec/models/debate_spec.rb @@ -4,6 +4,8 @@ require 'rails_helper' describe Debate do let(:debate) { build(:debate) } + it_behaves_like "public_voters_stats" + it "should be valid" do expect(debate).to be_valid end diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb index 9a9e9e9bc..77eb3e358 100644 --- a/spec/models/proposal_spec.rb +++ b/spec/models/proposal_spec.rb @@ -4,6 +4,8 @@ require 'rails_helper' describe Proposal do let(:proposal) { build(:proposal) } + it_behaves_like "public_voters_stats" + it "should be valid" do expect(proposal).to be_valid end diff --git a/spec/models/vote_spec.rb b/spec/models/vote_spec.rb index 8e5db0ce7..9dc30d95a 100644 --- a/spec/models/vote_spec.rb +++ b/spec/models/vote_spec.rb @@ -92,6 +92,25 @@ describe 'Vote' do end end + describe 'public_voter' do + it 'only returns voter if votable has enough votes' do + create(:setting, key: 'proposal_api_votes_threshold', value: '2') + + proposal_1 = create(:proposal) + proposal_2 = create(:proposal) + + voter_1 = create(:user) + voter_2 = create(:user) + + vote_1 = create(:vote, votable: proposal_1, voter: voter_1) + vote_2 = create(:vote, votable: proposal_2, voter: voter_1) + vote_3 = create(:vote, votable: proposal_2, voter: voter_2) + + expect(vote_1.public_voter).to be_nil + expect(vote_2.public_voter).to eq(voter_1) + end + end + describe '#public_timestamp' do it "truncates created_at timestamp up to minutes" do vote = create(:vote, created_at: Time.zone.parse('2016-02-10 15:30:45')) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a40934d92..e9e055bca 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,6 +3,7 @@ require 'database_cleaner' require 'email_spec' require 'devise' require 'knapsack' +Dir["./spec/models/concerns/*.rb"].each { |f| require f } Dir["./spec/support/**/*.rb"].sort.each { |f| require f } RSpec.configure do |config| @@ -105,4 +106,4 @@ RSpec.configure do |config| end # Parallel build helper configuration for travis -Knapsack::Adapters::RSpecAdapter.bind \ No newline at end of file +Knapsack::Adapters::RSpecAdapter.bind From f467bb64c76bdb3c12925425320b051bd2036011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 18:00:14 +0100 Subject: [PATCH 097/147] Wrote specs for HasPublicAuthor module --- spec/models/comment_spec.rb | 3 ++- .../models/concerns/has_public_author_spec.rb | 21 +++++++++++++++++++ spec/models/debate_spec.rb | 3 ++- spec/models/proposal_spec.rb | 1 + 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 spec/models/concerns/has_public_author_spec.rb diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index e1f39ea0d..09e6a9900 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -5,7 +5,8 @@ describe Comment do let(:comment) { build(:comment) } it_behaves_like "public_voters_stats" - + it_behaves_like "has_public_author" + it "is valid" do expect(comment).to be_valid end diff --git a/spec/models/concerns/has_public_author_spec.rb b/spec/models/concerns/has_public_author_spec.rb new file mode 100644 index 000000000..a5a9c2811 --- /dev/null +++ b/spec/models/concerns/has_public_author_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +shared_examples_for 'has_public_author' do + let(:model) { described_class } + + describe 'public_author' do + it "returns author if author's activity is public" do + author = create(:user, public_activity: true) + authored_element = create(model.to_s.underscore.to_sym, author: author) + + expect(authored_element.public_author).to eq(author) + end + + it "returns nil if author's activity is private" do + author = create(:user, public_activity: false) + authored_element = create(model.to_s.underscore.to_sym, author: author) + + expect(authored_element.public_author).to be_nil + end + end +end diff --git a/spec/models/debate_spec.rb b/spec/models/debate_spec.rb index 7c40e63fe..4db0e5f0a 100644 --- a/spec/models/debate_spec.rb +++ b/spec/models/debate_spec.rb @@ -5,7 +5,8 @@ describe Debate do let(:debate) { build(:debate) } it_behaves_like "public_voters_stats" - + it_behaves_like "has_public_author" + it "should be valid" do expect(debate).to be_valid end diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb index 77eb3e358..08ed6b384 100644 --- a/spec/models/proposal_spec.rb +++ b/spec/models/proposal_spec.rb @@ -5,6 +5,7 @@ describe Proposal do let(:proposal) { build(:proposal) } it_behaves_like "public_voters_stats" + it_behaves_like "has_public_author" it "should be valid" do expect(proposal).to be_valid From 1e95cdfce37db6f200e0487e1bbfe0696e10abe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 21:58:29 +0100 Subject: [PATCH 098/147] Fix dev_seeds --- db/dev_seeds.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index c73552d18..0a4738987 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -15,9 +15,9 @@ Setting.create(key: 'proposal_code_prefix', value: 'MAD') Setting.create(key: 'votes_for_proposal_success', value: '100') Setting.create(key: 'months_to_archive_proposals', value: '12') Setting.create(key: 'comments_body_max_length', value: '1000') -Settings.create(key: 'debate_api_votes_threshold', value: '150') -Settings.create(key: 'proposal_api_votes_threshold', value: '150') -Settings.create(key: 'comment_api_votes_threshold', value: '30') +Setting.create(key: 'debate_api_votes_threshold', value: '2') +Setting.create(key: 'proposal_api_votes_threshold', value: '2') +Setting.create(key: 'comment_api_votes_threshold', value: '2') Setting.create(key: 'twitter_handle', value: '@consul_dev') Setting.create(key: 'twitter_hashtag', value: '#consul_dev') From a31f6be3bde8d53c25610306288a5b36baa5fda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 22:16:17 +0100 Subject: [PATCH 099/147] Add proposal notifications to seeds --- db/dev_seeds.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 0a4738987..347994a00 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -376,3 +376,12 @@ Proposal.last(3).each do |proposal| created_at: rand((Time.current - 1.week) .. Time.current)) puts " #{banner.title}" end + +puts "Creating proposal notifications" + +100.times do |i| + ProposalNotification.create!(title: "Proposal notification title #{i}", + body: "Proposal notification body #{i}", + author: User.reorder("RANDOM()").first, + proposal: Proposal.reorder("RANDOM()").first) +end From 3c73f354167c92d8030b2c285957569ec3aa9d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 22:20:28 +0100 Subject: [PATCH 100/147] Removed useless code --- config/api.yml | 1 - lib/graph_ql/association_resolver.rb | 8 +------- spec/lib/association_resolver_spec.rb | 7 ++----- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/config/api.yml b/config/api.yml index bdf1b8175..452fa22f7 100644 --- a/config/api.yml +++ b/config/api.yml @@ -3,7 +3,6 @@ User: id: integer username: string Voter: - options: [disable_filtering] fields: gender: string age_range: string diff --git a/lib/graph_ql/association_resolver.rb b/lib/graph_ql/association_resolver.rb index 142b24313..22dd210f3 100644 --- a/lib/graph_ql/association_resolver.rb +++ b/lib/graph_ql/association_resolver.rb @@ -13,10 +13,6 @@ module GraphQL filter_forbidden_elements(requested_elements) end - def self.matching_exceptions - [:public_voter] - end - private def target_public_elements @@ -24,9 +20,7 @@ module GraphQL end def filter_forbidden_elements(requested_elements) - if AssociationResolver.matching_exceptions.include?(field_name) - requested_elements - elsif requested_elements.respond_to?(:each) + if requested_elements.respond_to?(:each) requested_elements.all & allowed_elements.all else allowed_elements.include?(requested_elements) ? requested_elements : nil diff --git a/spec/lib/association_resolver_spec.rb b/spec/lib/association_resolver_spec.rb index adb8a8b34..3e8bdf897 100644 --- a/spec/lib/association_resolver_spec.rb +++ b/spec/lib/association_resolver_spec.rb @@ -25,7 +25,7 @@ describe GraphQL::AssociationResolver do it 'resolves simple associations' do geozone = create(:geozone) proposal = create(:proposal, geozone: geozone) - + result = geozone_resolver.call(proposal, nil, nil) expect(result).to eq(geozone) @@ -54,9 +54,6 @@ describe GraphQL::AssociationResolver do expect(result).to be_empty end - - it 'permits all elements for exceptions' do - skip 'Current implementation is temporary' - end end + end From d76e4518e666816eb3264433553493f1513bc68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 22:43:42 +0100 Subject: [PATCH 101/147] Extract QueryRoot resolve functions into its own classes --- config/initializers/graphql.rb | 16 ++-------------- lib/graph_ql/root_collection_resolver.rb | 17 +++++++++++++++++ lib/graph_ql/root_element_resolver.rb | 17 +++++++++++++++++ 3 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 lib/graph_ql/root_collection_resolver.rb create mode 100644 lib/graph_ql/root_element_resolver.rb diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 2d257285d..b385f1483 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -55,26 +55,14 @@ QueryRoot = GraphQL::ObjectType.define do type created_type description "Find one #{model.model_name.human} by ID" argument :id, !types.ID - resolve -> (object, arguments, context) do - if model.respond_to?(:public_for_api) - model.public_for_api.find(arguments["id"]) - else - model.find(arguments["id"]) - end - end + resolve GraphQL::RootElementResolver.new(model) end end # create an entry filed to retrive a paginated collection connection model.name.underscore.pluralize.to_sym, created_type.connection_type do description "Find all #{model.model_name.human.pluralize}" - resolve -> (object, arguments, context) do - if model.respond_to?(:public_for_api) - model.public_for_api - else - model.all - end - end + resolve GraphQL::RootCollectionResolver.new(model) end end diff --git a/lib/graph_ql/root_collection_resolver.rb b/lib/graph_ql/root_collection_resolver.rb new file mode 100644 index 000000000..fa0e3c9ec --- /dev/null +++ b/lib/graph_ql/root_collection_resolver.rb @@ -0,0 +1,17 @@ +module GraphQL + class RootCollectionResolver + attr_reader :target_model + + def initialize(target_model) + @target_model = target_model + end + + def call(object, arguments, context) + if target_model.respond_to?(:public_for_api) + target_model.public_for_api + else + target_model.all + end + end + end +end diff --git a/lib/graph_ql/root_element_resolver.rb b/lib/graph_ql/root_element_resolver.rb new file mode 100644 index 000000000..05d0b9bfb --- /dev/null +++ b/lib/graph_ql/root_element_resolver.rb @@ -0,0 +1,17 @@ +module GraphQL + class RootElementResolver + attr_reader :target_model + + def initialize(target_model) + @target_model = target_model + end + + def call(object, arguments, context) + if target_model.respond_to?(:public_for_api) + target_model.public_for_api.find(arguments["id"]) + else + target_model.find(arguments["id"]) + end + end + end +end From 0b302c2afc06490c46db913ff8c84ad9394cb0e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sat, 7 Jan 2017 23:04:30 +0100 Subject: [PATCH 102/147] Move code from GraphQL initializer into GraphQL::TypeCreator --- config/initializers/graphql.rb | 38 ++++-------------------------- lib/graph_ql/type_creator.rb | 42 +++++++++++++++++++++++++++++++--- spec/lib/type_creator_spec.rb | 20 ++++++++-------- 3 files changed, 54 insertions(+), 46 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index b385f1483..478cf10e3 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,6 +1,6 @@ api_config = YAML.load_file('./config/api.yml') -API_TYPE_DEFINITIONS = {} +api_type_definitions = {} # Parse API configuration file @@ -19,16 +19,13 @@ api_config.each do |api_type_model, api_type_info| end end - API_TYPE_DEFINITIONS[model] = { options: options, fields: fields } + api_type_definitions[model] = { options: options, fields: fields } end # Create all GraphQL types - -type_creator = GraphQL::TypeCreator.new - -API_TYPE_DEFINITIONS.each do |model, info| - type_creator.create(model, info[:fields]) -end +type_creator = GraphQL::TypeCreator.new(api_type_definitions) +type_creator.create_api_types +QueryRoot = type_creator.create_query_root ConsulSchema = GraphQL::Schema.define do query QueryRoot @@ -42,28 +39,3 @@ ConsulSchema = GraphQL::Schema.define do ConsulSchema.types[type_name] } end - -QueryRoot = GraphQL::ObjectType.define do - name "Query" - description "The query root for this schema" - - type_creator.created_types.each do |model, created_type| - - # create an entry field to retrive a single object - if API_TYPE_DEFINITIONS[model][:fields][:id] - field model.name.underscore.to_sym do - type created_type - description "Find one #{model.model_name.human} by ID" - argument :id, !types.ID - resolve GraphQL::RootElementResolver.new(model) - end - end - - # create an entry filed to retrive a paginated collection - connection model.name.underscore.pluralize.to_sym, created_type.connection_type do - description "Find all #{model.model_name.human.pluralize}" - resolve GraphQL::RootCollectionResolver.new(model) - end - - end -end diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index b77360c72..15803285e 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -8,13 +8,20 @@ module GraphQL string: GraphQL::STRING_TYPE } - attr_accessor :created_types + attr_accessor :created_types, :api_type_definitions - def initialize + def initialize(api_type_definitions) + @api_type_definitions = api_type_definitions @created_types = {} end - def create(model, fields) + def create_api_types + api_type_definitions.each do |model, info| + self.create_type(model, info[:fields]) + end + end + + def create_type(model, fields) type_creator = self created_type = GraphQL::ObjectType.define do @@ -44,6 +51,35 @@ module GraphQL return created_type # GraphQL::ObjectType end + def create_query_root + type_creator = self + + GraphQL::ObjectType.define do + name 'QueryRoot' + description 'The query root for this schema' + + type_creator.created_types.each do |model, created_type| + + # create an entry field to retrive a single object + if type_creator.api_type_definitions[model][:fields][:id] + field model.name.underscore.to_sym do + type created_type + description "Find one #{model.model_name.human} by ID" + argument :id, !types.ID + resolve GraphQL::RootElementResolver.new(model) + end + end + + # create an entry filed to retrive a paginated collection + connection model.name.underscore.pluralize.to_sym, created_type.connection_type do + description "Find all #{model.model_name.human.pluralize}" + resolve GraphQL::RootCollectionResolver.new(model) + end + + end + end + end + def self.type_kind(type) if SCALAR_TYPES[type] :scalar diff --git a/spec/lib/type_creator_spec.rb b/spec/lib/type_creator_spec.rb index 1e25b91fd..9948d77d0 100644 --- a/spec/lib/type_creator_spec.rb +++ b/spec/lib/type_creator_spec.rb @@ -1,11 +1,11 @@ require 'rails_helper' describe GraphQL::TypeCreator do - let(:type_creator) { GraphQL::TypeCreator.new } + let(:type_creator) { GraphQL::TypeCreator.new({}) } - describe "::create" do + describe "::create_type" do it "creates fields for Int attributes" do - debate_type = type_creator.create(Debate, { id: :integer }) + debate_type = type_creator.create_type(Debate, { id: :integer }) created_field = debate_type.fields['id'] expect(created_field).to be_a(GraphQL::Field) @@ -14,7 +14,7 @@ describe GraphQL::TypeCreator do end it "creates fields for String attributes" do - debate_type = type_creator.create(Debate, { title: :string }) + debate_type = type_creator.create_type(Debate, { title: :string }) created_field = debate_type.fields['title'] expect(created_field).to be_a(GraphQL::Field) @@ -23,8 +23,8 @@ describe GraphQL::TypeCreator do end it "creates connections for :belongs_to associations" do - user_type = type_creator.create(User, { id: :integer }) - debate_type = type_creator.create(Debate, { author: User }) + user_type = type_creator.create_type(User, { id: :integer }) + debate_type = type_creator.create_type(Debate, { author: User }) connection = debate_type.fields['author'] @@ -34,8 +34,8 @@ describe GraphQL::TypeCreator do end it "creates connections for :has_one associations" do - user_type = type_creator.create(User, { organization: Organization }) - organization_type = type_creator.create(Organization, { id: :integer }) + user_type = type_creator.create_type(User, { organization: Organization }) + organization_type = type_creator.create_type(Organization, { id: :integer }) connection = user_type.fields['organization'] @@ -45,8 +45,8 @@ describe GraphQL::TypeCreator do end it "creates connections for :has_many associations" do - comment_type = type_creator.create(Comment, { id: :integer }) - debate_type = type_creator.create(Debate, { comments: [Comment] }) + comment_type = type_creator.create_type(Comment, { id: :integer }) + debate_type = type_creator.create_type(Debate, { comments: [Comment] }) connection = debate_type.fields['comments'] From ed6ba384dd2350b0b3dde7ebfaf81195e46bdf21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 8 Jan 2017 11:37:15 +0100 Subject: [PATCH 103/147] Wrote specs for GraphQL root resolvers --- lib/graph_ql/root_element_resolver.rb | 4 +-- .../graph_ql/root_collection_resolver_spec.rb | 27 ++++++++++++++ .../graph_ql/root_element_resolver_spec.rb | 35 +++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 spec/lib/graph_ql/root_collection_resolver_spec.rb create mode 100644 spec/lib/graph_ql/root_element_resolver_spec.rb diff --git a/lib/graph_ql/root_element_resolver.rb b/lib/graph_ql/root_element_resolver.rb index 05d0b9bfb..4191f8eee 100644 --- a/lib/graph_ql/root_element_resolver.rb +++ b/lib/graph_ql/root_element_resolver.rb @@ -8,9 +8,9 @@ module GraphQL def call(object, arguments, context) if target_model.respond_to?(:public_for_api) - target_model.public_for_api.find(arguments["id"]) + target_model.public_for_api.find_by(id: arguments["id"]) else - target_model.find(arguments["id"]) + target_model.find_by(id: arguments["id"]) end end end diff --git a/spec/lib/graph_ql/root_collection_resolver_spec.rb b/spec/lib/graph_ql/root_collection_resolver_spec.rb new file mode 100644 index 000000000..87f0319ff --- /dev/null +++ b/spec/lib/graph_ql/root_collection_resolver_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +describe GraphQL::RootCollectionResolver do + let(:comments_resolver) { GraphQL::RootCollectionResolver.new(Comment) } + + describe '#call' do + it 'resolves collections' do + comment_1 = create(:comment) + comment_2 = create(:comment) + + result = comments_resolver.call(nil, nil, nil) + + expect(result).to match_array([comment_1, comment_2]) + end + + it 'blocks collection forbidden elements' do + proposal = create(:proposal, :hidden) + comment_1 = create(:comment) + comment_2 = create(:comment, commentable: proposal) + + result = comments_resolver.call(nil, nil, nil) + + expect(result).to match_array([comment_1]) + end + end + +end diff --git a/spec/lib/graph_ql/root_element_resolver_spec.rb b/spec/lib/graph_ql/root_element_resolver_spec.rb new file mode 100644 index 000000000..7ca64a7e8 --- /dev/null +++ b/spec/lib/graph_ql/root_element_resolver_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +describe GraphQL::RootElementResolver do + let(:comment_resolver) { GraphQL::RootElementResolver.new(Comment) } + + describe '#call' do + it 'resolves simple elements' do + comment = create(:comment) + arguments = { 'id' => comment.id } + + result = comment_resolver.call(nil, arguments, nil) + + expect(result).to eq(comment) + end + + it 'returns nil when requested element is forbidden' do + proposal = create(:proposal, :hidden) + comment = create(:comment, commentable: proposal) + arguments = { 'id' => comment.id } + + result = comment_resolver.call(nil, arguments, nil) + + expect(result).to be_nil + end + + it 'returns nil when requested element does not exist' do + arguments = { 'id' => 1 } + + result = comment_resolver.call(nil, arguments, nil) + + expect(result).to be_nil + end + end + +end From 7027164867f2f9831c8e0459eee016fe26ef7102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 8 Jan 2017 11:37:56 +0100 Subject: [PATCH 104/147] Clean code --- config/initializers/graphql.rb | 13 ++++--------- lib/graph_ql/type_creator.rb | 6 +++++- .../lib/{ => graph_ql}/association_resolver_spec.rb | 0 spec/lib/{ => graph_ql}/type_creator_spec.rb | 0 4 files changed, 9 insertions(+), 10 deletions(-) rename spec/lib/{ => graph_ql}/association_resolver_spec.rb (100%) rename spec/lib/{ => graph_ql}/type_creator_spec.rb (100%) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 478cf10e3..01d54ab19 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -22,20 +22,15 @@ api_config.each do |api_type_model, api_type_info| api_type_definitions[model] = { options: options, fields: fields } end -# Create all GraphQL types type_creator = GraphQL::TypeCreator.new(api_type_definitions) -type_creator.create_api_types -QueryRoot = type_creator.create_query_root +QueryRoot = type_creator.query_root ConsulSchema = GraphQL::Schema.define do query QueryRoot - - # Reject deeply-nested queries max_depth 10 - resolve_type -> (object, ctx) { - # look up types by class name - type_name = object.class.name + resolve_type -> (object, ctx) do + type_name = object.class.name # look up types by class name ConsulSchema.types[type_name] - } + end end diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 15803285e..9042d7f3f 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -1,3 +1,5 @@ +require 'graphql' + module GraphQL class TypeCreator SCALAR_TYPES = { @@ -8,11 +10,13 @@ module GraphQL string: GraphQL::STRING_TYPE } - attr_accessor :created_types, :api_type_definitions + attr_accessor :created_types, :api_type_definitions, :query_root def initialize(api_type_definitions) @api_type_definitions = api_type_definitions @created_types = {} + create_api_types + @query_root = create_query_root end def create_api_types diff --git a/spec/lib/association_resolver_spec.rb b/spec/lib/graph_ql/association_resolver_spec.rb similarity index 100% rename from spec/lib/association_resolver_spec.rb rename to spec/lib/graph_ql/association_resolver_spec.rb diff --git a/spec/lib/type_creator_spec.rb b/spec/lib/graph_ql/type_creator_spec.rb similarity index 100% rename from spec/lib/type_creator_spec.rb rename to spec/lib/graph_ql/type_creator_spec.rb From b9d2bc280140bfe79037020a3353c8aa1de2dfc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 8 Jan 2017 12:45:48 +0100 Subject: [PATCH 105/147] Wrote specs for QueryType creation --- config/initializers/graphql.rb | 4 +- lib/graph_ql/type_creator.rb | 70 +++++++++++++------------- spec/lib/graph_ql/type_creator_spec.rb | 35 ++++++++++++- 3 files changed, 72 insertions(+), 37 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 01d54ab19..971a565e4 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -23,10 +23,10 @@ api_config.each do |api_type_model, api_type_info| end type_creator = GraphQL::TypeCreator.new(api_type_definitions) -QueryRoot = type_creator.query_root +QueryType = type_creator.query_type ConsulSchema = GraphQL::Schema.define do - query QueryRoot + query QueryType max_depth 10 resolve_type -> (object, ctx) do diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb index 9042d7f3f..b705839c3 100644 --- a/lib/graph_ql/type_creator.rb +++ b/lib/graph_ql/type_creator.rb @@ -10,21 +10,26 @@ module GraphQL string: GraphQL::STRING_TYPE } - attr_accessor :created_types, :api_type_definitions, :query_root + attr_accessor :created_types, :api_type_definitions, :query_type def initialize(api_type_definitions) @api_type_definitions = api_type_definitions @created_types = {} create_api_types - @query_root = create_query_root + create_query_type end - def create_api_types - api_type_definitions.each do |model, info| - self.create_type(model, info[:fields]) + def self.type_kind(type) + if SCALAR_TYPES[type] + :scalar + elsif type.class == Class + :simple_association + elsif type.class == Array + :paginated_association end end + # TODO: this method shouldn't be public just for testing purposes, ¿smell? def create_type(model, fields) type_creator = self @@ -55,44 +60,41 @@ module GraphQL return created_type # GraphQL::ObjectType end - def create_query_root - type_creator = self + private - GraphQL::ObjectType.define do - name 'QueryRoot' - description 'The query root for this schema' + def create_api_types + api_type_definitions.each do |model, info| + self.create_type(model, info[:fields]) + end + end - type_creator.created_types.each do |model, created_type| + def create_query_type + type_creator = self - # create an entry field to retrive a single object - if type_creator.api_type_definitions[model][:fields][:id] - field model.name.underscore.to_sym do - type created_type - description "Find one #{model.model_name.human} by ID" - argument :id, !types.ID - resolve GraphQL::RootElementResolver.new(model) + @query_type = GraphQL::ObjectType.define do + name 'QueryType' + description 'The root query for this schema' + + type_creator.created_types.each do |model, created_type| + # create field to retrive a single object + if type_creator.api_type_definitions[model][:fields][:id] + field model.name.underscore.to_sym do + type created_type + description "Find one #{model.model_name.human} by ID" + argument :id, !types.ID + resolve GraphQL::RootElementResolver.new(model) + end end - end - # create an entry filed to retrive a paginated collection - connection model.name.underscore.pluralize.to_sym, created_type.connection_type do - description "Find all #{model.model_name.human.pluralize}" - resolve GraphQL::RootCollectionResolver.new(model) + # create connection to retrive a collection + connection model.name.underscore.pluralize.to_sym, created_type.connection_type do + description "Find all #{model.model_name.human.pluralize}" + resolve GraphQL::RootCollectionResolver.new(model) + end end end end - end - - def self.type_kind(type) - if SCALAR_TYPES[type] - :scalar - elsif type.class == Class - :simple_association - elsif type.class == Array - :paginated_association - end - end end end diff --git a/spec/lib/graph_ql/type_creator_spec.rb b/spec/lib/graph_ql/type_creator_spec.rb index 9948d77d0..d5291c961 100644 --- a/spec/lib/graph_ql/type_creator_spec.rb +++ b/spec/lib/graph_ql/type_creator_spec.rb @@ -1,7 +1,40 @@ require 'rails_helper' describe GraphQL::TypeCreator do - let(:type_creator) { GraphQL::TypeCreator.new({}) } + let(:api_type_definitions) { {} } + let(:type_creator) { GraphQL::TypeCreator.new(api_type_definitions) } + + describe "::query_type" do + let(:api_type_definitions) do + { + ProposalNotification => { fields: { title: 'string' } }, + Proposal => { fields: { id: 'integer', title: 'string' } } + } + end + let(:query_type) { type_creator.query_type } + + it 'has fields to retrieve single objects whose model fields included an ID' do + field = query_type.fields['proposal'] + proposal_type = type_creator.created_types[Proposal] + + expect(field).to be_a(GraphQL::Field) + expect(field.type).to eq(proposal_type) + expect(field.name).to eq('proposal') + end + + it 'does not have fields to retrieve single objects whose model fields did not include an ID' do + expect(query_type.fields['proposal_notification']).to be_nil + end + + it "has connections to retrieve collections of objects" do + connection = query_type.fields['proposals'] + proposal_type = type_creator.created_types[Proposal] + + expect(connection).to be_a(GraphQL::Field) + expect(connection.type).to eq(proposal_type.connection_type) + expect(connection.name).to eq('proposals') + end + end describe "::create_type" do it "creates fields for Int attributes" do From 398bc8c211dfee451da74ac985cc5fbab76e7958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 8 Jan 2017 13:39:50 +0100 Subject: [PATCH 106/147] Split api types creation and query type creation between two different classes --- config/initializers/graphql.rb | 9 +- lib/graph_ql/api_types_creator.rb | 68 +++++++++++++ lib/graph_ql/query_type_creator.rb | 40 ++++++++ lib/graph_ql/type_creator.rb | 100 ------------------- spec/lib/graph_ql/api_types_creator_spec.rb | 58 +++++++++++ spec/lib/graph_ql/query_type_creator_spec.rb | 39 ++++++++ spec/lib/graph_ql/type_creator_spec.rb | 91 ----------------- 7 files changed, 211 insertions(+), 194 deletions(-) create mode 100644 lib/graph_ql/api_types_creator.rb create mode 100644 lib/graph_ql/query_type_creator.rb delete mode 100644 lib/graph_ql/type_creator.rb create mode 100644 spec/lib/graph_ql/api_types_creator_spec.rb create mode 100644 spec/lib/graph_ql/query_type_creator_spec.rb delete mode 100644 spec/lib/graph_ql/type_creator_spec.rb diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 971a565e4..55c34ec1f 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -12,7 +12,7 @@ api_config.each do |api_type_model, api_type_info| api_type_info['fields'].each do |field_name, field_type| if field_type.is_a?(Array) # paginated association fields[field_name.to_sym] = [field_type.first.constantize] - elsif GraphQL::TypeCreator::SCALAR_TYPES[field_type.to_sym] + elsif GraphQL::ApiTypesCreator::SCALAR_TYPES[field_type.to_sym] fields[field_name.to_sym] = field_type.to_sym else # simple association fields[field_name.to_sym] = field_type.constantize @@ -22,8 +22,11 @@ api_config.each do |api_type_model, api_type_info| api_type_definitions[model] = { options: options, fields: fields } end -type_creator = GraphQL::TypeCreator.new(api_type_definitions) -QueryType = type_creator.query_type +api_types_creator = GraphQL::ApiTypesCreator.new(api_type_definitions) +created_api_types = api_types_creator.create + +query_type_creator = GraphQL::QueryTypeCreator.new(created_api_types) +QueryType = query_type_creator.create ConsulSchema = GraphQL::Schema.define do query QueryType diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb new file mode 100644 index 000000000..51a7af0e8 --- /dev/null +++ b/lib/graph_ql/api_types_creator.rb @@ -0,0 +1,68 @@ +require 'graphql' + +module GraphQL + class ApiTypesCreator + SCALAR_TYPES = { + integer: GraphQL::INT_TYPE, + boolean: GraphQL::BOOLEAN_TYPE, + float: GraphQL::FLOAT_TYPE, + double: GraphQL::FLOAT_TYPE, + string: GraphQL::STRING_TYPE + } + + attr_accessor :created_types + + def initialize(api_type_definitions) + @api_type_definitions = api_type_definitions + @created_types = {} + end + + def create + @api_type_definitions.each do |model, info| + self.create_type(model, info[:fields]) + end + created_types + end + + def self.type_kind(type) + if SCALAR_TYPES[type] + :scalar + elsif type.class == Class + :simple_association + elsif type.class == Array + :paginated_association + end + end + + def create_type(model, fields) + api_types_creator = self + + created_type = GraphQL::ObjectType.define do + + name(model.name) + description("#{model.model_name.human}") + + # Make a field for each column, association or method + fields.each do |field_name, field_type| + case ApiTypesCreator.type_kind(field_type) + when :scalar + field(field_name, SCALAR_TYPES[field_type]) + when :simple_association + field(field_name, -> { api_types_creator.created_types[field_type] }) do + resolve GraphQL::AssociationResolver.new(field_name, field_type) + end + when :paginated_association + field_type = field_type.first + connection(field_name, -> { api_types_creator.created_types[field_type].connection_type }) do + resolve GraphQL::AssociationResolver.new(field_name, field_type) + end + end + end + + end + created_types[model] = created_type + return created_type # GraphQL::ObjectType + end + + end +end diff --git a/lib/graph_ql/query_type_creator.rb b/lib/graph_ql/query_type_creator.rb new file mode 100644 index 000000000..b7bde9f4e --- /dev/null +++ b/lib/graph_ql/query_type_creator.rb @@ -0,0 +1,40 @@ +require 'graphql' + +module GraphQL + class QueryTypeCreator + + attr_accessor :created_api_types + + def initialize(created_api_types) + @created_api_types = created_api_types + end + + def create + query_type_creator = self + + GraphQL::ObjectType.define do + name 'QueryType' + description 'The root query for the schema' + + query_type_creator.created_api_types.each do |model, created_type| + # debugger + if created_type.fields['id'] + field model.name.underscore.to_sym do + type created_type + description "Find one #{model.model_name.human} by ID" + argument :id, !types.ID + resolve GraphQL::RootElementResolver.new(model) + end + end + + connection model.name.underscore.pluralize.to_sym, created_type.connection_type do + description "Find all #{model.model_name.human.pluralize}" + resolve GraphQL::RootCollectionResolver.new(model) + end + + end + end + end + + end +end diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb deleted file mode 100644 index b705839c3..000000000 --- a/lib/graph_ql/type_creator.rb +++ /dev/null @@ -1,100 +0,0 @@ -require 'graphql' - -module GraphQL - class TypeCreator - SCALAR_TYPES = { - integer: GraphQL::INT_TYPE, - boolean: GraphQL::BOOLEAN_TYPE, - float: GraphQL::FLOAT_TYPE, - double: GraphQL::FLOAT_TYPE, - string: GraphQL::STRING_TYPE - } - - attr_accessor :created_types, :api_type_definitions, :query_type - - def initialize(api_type_definitions) - @api_type_definitions = api_type_definitions - @created_types = {} - create_api_types - create_query_type - end - - def self.type_kind(type) - if SCALAR_TYPES[type] - :scalar - elsif type.class == Class - :simple_association - elsif type.class == Array - :paginated_association - end - end - - # TODO: this method shouldn't be public just for testing purposes, ¿smell? - def create_type(model, fields) - type_creator = self - - created_type = GraphQL::ObjectType.define do - - name(model.name) - description("#{model.model_name.human}") - - # Make a field for each column, association or method - fields.each do |field_name, field_type| - case TypeCreator.type_kind(field_type) - when :scalar - field(field_name, SCALAR_TYPES[field_type]) - when :simple_association - field(field_name, -> { type_creator.created_types[field_type] }) do - resolve GraphQL::AssociationResolver.new(field_name, field_type) - end - when :paginated_association - field_type = field_type.first - connection(field_name, -> { type_creator.created_types[field_type].connection_type }) do - resolve GraphQL::AssociationResolver.new(field_name, field_type) - end - end - end - - end - created_types[model] = created_type - return created_type # GraphQL::ObjectType - end - - private - - def create_api_types - api_type_definitions.each do |model, info| - self.create_type(model, info[:fields]) - end - end - - def create_query_type - type_creator = self - - @query_type = GraphQL::ObjectType.define do - name 'QueryType' - description 'The root query for this schema' - - type_creator.created_types.each do |model, created_type| - # create field to retrive a single object - if type_creator.api_type_definitions[model][:fields][:id] - field model.name.underscore.to_sym do - type created_type - description "Find one #{model.model_name.human} by ID" - argument :id, !types.ID - resolve GraphQL::RootElementResolver.new(model) - end - end - - # create connection to retrive a collection - connection model.name.underscore.pluralize.to_sym, created_type.connection_type do - description "Find all #{model.model_name.human.pluralize}" - resolve GraphQL::RootCollectionResolver.new(model) - end - end - - end - end - - end -end diff --git a/spec/lib/graph_ql/api_types_creator_spec.rb b/spec/lib/graph_ql/api_types_creator_spec.rb new file mode 100644 index 000000000..10b43ba28 --- /dev/null +++ b/spec/lib/graph_ql/api_types_creator_spec.rb @@ -0,0 +1,58 @@ +require 'rails_helper' + +describe GraphQL::ApiTypesCreator do + let(:api_types_creator) { GraphQL::ApiTypesCreator.new( {} ) } + + describe "::create_type" do + it "creates fields for Int attributes" do + debate_type = api_types_creator.create_type(Debate, { id: :integer }) + created_field = debate_type.fields['id'] + + expect(created_field).to be_a(GraphQL::Field) + expect(created_field.type).to be_a(GraphQL::ScalarType) + expect(created_field.type.name).to eq('Int') + end + + it "creates fields for String attributes" do + debate_type = api_types_creator.create_type(Debate, { title: :string }) + created_field = debate_type.fields['title'] + + expect(created_field).to be_a(GraphQL::Field) + expect(created_field.type).to be_a(GraphQL::ScalarType) + expect(created_field.type.name).to eq('String') + end + + it "creates connections for :belongs_to associations" do + user_type = api_types_creator.create_type(User, { id: :integer }) + debate_type = api_types_creator.create_type(Debate, { author: User }) + + connection = debate_type.fields['author'] + + expect(connection).to be_a(GraphQL::Field) + expect(connection.type).to eq(user_type) + expect(connection.name).to eq('author') + end + + it "creates connections for :has_one associations" do + user_type = api_types_creator.create_type(User, { organization: Organization }) + organization_type = api_types_creator.create_type(Organization, { id: :integer }) + + connection = user_type.fields['organization'] + + expect(connection).to be_a(GraphQL::Field) + expect(connection.type).to eq(organization_type) + expect(connection.name).to eq('organization') + end + + it "creates connections for :has_many associations" do + comment_type = api_types_creator.create_type(Comment, { id: :integer }) + debate_type = api_types_creator.create_type(Debate, { comments: [Comment] }) + + connection = debate_type.fields['comments'] + + expect(connection).to be_a(GraphQL::Field) + expect(connection.type).to eq(comment_type.connection_type) + expect(connection.name).to eq('comments') + end + end +end diff --git a/spec/lib/graph_ql/query_type_creator_spec.rb b/spec/lib/graph_ql/query_type_creator_spec.rb new file mode 100644 index 000000000..980c4f1a6 --- /dev/null +++ b/spec/lib/graph_ql/query_type_creator_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +describe GraphQL::QueryTypeCreator do + let(:api_type_definitions) do + { + ProposalNotification => { fields: { title: 'string' } }, + Proposal => { fields: { id: 'integer', title: 'string' } } + } + end + let(:api_types_creator) { GraphQL::ApiTypesCreator.new(api_type_definitions) } + let(:created_api_types) { api_types_creator.create } + let(:query_type_creator) { GraphQL::QueryTypeCreator.new(created_api_types) } + + describe "::create" do + let(:query_type) { query_type_creator.create } + + it 'creates a QueryType with fields to retrieve single objects whose model fields included an ID' do + field = query_type.fields['proposal'] + proposal_type = query_type_creator.created_api_types[Proposal] + + expect(field).to be_a(GraphQL::Field) + expect(field.type).to eq(proposal_type) + expect(field.name).to eq('proposal') + end + + it 'creates a QueryType without fields to retrieve single objects whose model fields did not include an ID' do + expect(query_type.fields['proposal_notification']).to be_nil + end + + it "creates a QueryType with connections to retrieve collections of objects" do + connection = query_type.fields['proposals'] + proposal_type = query_type_creator.created_api_types[Proposal] + + expect(connection).to be_a(GraphQL::Field) + expect(connection.type).to eq(proposal_type.connection_type) + expect(connection.name).to eq('proposals') + end + end +end diff --git a/spec/lib/graph_ql/type_creator_spec.rb b/spec/lib/graph_ql/type_creator_spec.rb deleted file mode 100644 index d5291c961..000000000 --- a/spec/lib/graph_ql/type_creator_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'rails_helper' - -describe GraphQL::TypeCreator do - let(:api_type_definitions) { {} } - let(:type_creator) { GraphQL::TypeCreator.new(api_type_definitions) } - - describe "::query_type" do - let(:api_type_definitions) do - { - ProposalNotification => { fields: { title: 'string' } }, - Proposal => { fields: { id: 'integer', title: 'string' } } - } - end - let(:query_type) { type_creator.query_type } - - it 'has fields to retrieve single objects whose model fields included an ID' do - field = query_type.fields['proposal'] - proposal_type = type_creator.created_types[Proposal] - - expect(field).to be_a(GraphQL::Field) - expect(field.type).to eq(proposal_type) - expect(field.name).to eq('proposal') - end - - it 'does not have fields to retrieve single objects whose model fields did not include an ID' do - expect(query_type.fields['proposal_notification']).to be_nil - end - - it "has connections to retrieve collections of objects" do - connection = query_type.fields['proposals'] - proposal_type = type_creator.created_types[Proposal] - - expect(connection).to be_a(GraphQL::Field) - expect(connection.type).to eq(proposal_type.connection_type) - expect(connection.name).to eq('proposals') - end - end - - describe "::create_type" do - it "creates fields for Int attributes" do - debate_type = type_creator.create_type(Debate, { id: :integer }) - created_field = debate_type.fields['id'] - - expect(created_field).to be_a(GraphQL::Field) - expect(created_field.type).to be_a(GraphQL::ScalarType) - expect(created_field.type.name).to eq('Int') - end - - it "creates fields for String attributes" do - debate_type = type_creator.create_type(Debate, { title: :string }) - created_field = debate_type.fields['title'] - - expect(created_field).to be_a(GraphQL::Field) - expect(created_field.type).to be_a(GraphQL::ScalarType) - expect(created_field.type.name).to eq('String') - end - - it "creates connections for :belongs_to associations" do - user_type = type_creator.create_type(User, { id: :integer }) - debate_type = type_creator.create_type(Debate, { author: User }) - - connection = debate_type.fields['author'] - - expect(connection).to be_a(GraphQL::Field) - expect(connection.type).to eq(user_type) - expect(connection.name).to eq('author') - end - - it "creates connections for :has_one associations" do - user_type = type_creator.create_type(User, { organization: Organization }) - organization_type = type_creator.create_type(Organization, { id: :integer }) - - connection = user_type.fields['organization'] - - expect(connection).to be_a(GraphQL::Field) - expect(connection.type).to eq(organization_type) - expect(connection.name).to eq('organization') - end - - it "creates connections for :has_many associations" do - comment_type = type_creator.create_type(Comment, { id: :integer }) - debate_type = type_creator.create_type(Debate, { comments: [Comment] }) - - connection = debate_type.fields['comments'] - - expect(connection).to be_a(GraphQL::Field) - expect(connection.type).to eq(comment_type.connection_type) - expect(connection.name).to eq('comments') - end - end -end From e1b3d468e50923e8c9abcafb2928d99c7e50477b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Sun, 8 Jan 2017 14:27:20 +0100 Subject: [PATCH 107/147] Fix failing spec --- lib/graph_ql/query_type_creator.rb | 1 - spec/lib/graph_ql/query_type_creator_spec.rb | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/graph_ql/query_type_creator.rb b/lib/graph_ql/query_type_creator.rb index b7bde9f4e..9918b0638 100644 --- a/lib/graph_ql/query_type_creator.rb +++ b/lib/graph_ql/query_type_creator.rb @@ -17,7 +17,6 @@ module GraphQL description 'The root query for the schema' query_type_creator.created_api_types.each do |model, created_type| - # debugger if created_type.fields['id'] field model.name.underscore.to_sym do type created_type diff --git a/spec/lib/graph_ql/query_type_creator_spec.rb b/spec/lib/graph_ql/query_type_creator_spec.rb index 980c4f1a6..9669de0c1 100644 --- a/spec/lib/graph_ql/query_type_creator_spec.rb +++ b/spec/lib/graph_ql/query_type_creator_spec.rb @@ -3,8 +3,8 @@ require 'rails_helper' describe GraphQL::QueryTypeCreator do let(:api_type_definitions) do { - ProposalNotification => { fields: { title: 'string' } }, - Proposal => { fields: { id: 'integer', title: 'string' } } + ProposalNotification => { fields: { title: :string } }, + Proposal => { fields: { id: :integer, title: :string } } } end let(:api_types_creator) { GraphQL::ApiTypesCreator.new(api_type_definitions) } From 60786f17c2a11d520304a0403e61d016496aa050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 9 Jan 2017 10:14:31 +0100 Subject: [PATCH 108/147] Improved code coverage for GraphQL resolvers specs --- lib/graph_ql/root_element_resolver.rb | 17 ++++++++++---- .../graph_ql/root_collection_resolver_spec.rb | 13 ++++++----- .../graph_ql/root_element_resolver_spec.rb | 23 +++++++++++++------ 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/lib/graph_ql/root_element_resolver.rb b/lib/graph_ql/root_element_resolver.rb index 4191f8eee..cb963ac1f 100644 --- a/lib/graph_ql/root_element_resolver.rb +++ b/lib/graph_ql/root_element_resolver.rb @@ -7,11 +7,18 @@ module GraphQL end def call(object, arguments, context) - if target_model.respond_to?(:public_for_api) - target_model.public_for_api.find_by(id: arguments["id"]) - else - target_model.find_by(id: arguments["id"]) - end + public_elements.find_by(id: arguments['id']) end + + private + + def public_elements + if target_model.respond_to?(:public_for_api) + target_model.public_for_api + else + target_model + end + end + end end diff --git a/spec/lib/graph_ql/root_collection_resolver_spec.rb b/spec/lib/graph_ql/root_collection_resolver_spec.rb index 87f0319ff..edfc6d393 100644 --- a/spec/lib/graph_ql/root_collection_resolver_spec.rb +++ b/spec/lib/graph_ql/root_collection_resolver_spec.rb @@ -1,19 +1,20 @@ require 'rails_helper' describe GraphQL::RootCollectionResolver do + let(:geozones_resolver) { GraphQL::RootCollectionResolver.new(Geozone) } let(:comments_resolver) { GraphQL::RootCollectionResolver.new(Comment) } describe '#call' do - it 'resolves collections' do - comment_1 = create(:comment) - comment_2 = create(:comment) + it 'returns the whole colleciton for unscoped models' do + geozone_1 = create(:geozone) + geozone_2 = create(:geozone) - result = comments_resolver.call(nil, nil, nil) + result = geozones_resolver.call(nil, nil, nil) - expect(result).to match_array([comment_1, comment_2]) + expect(result).to match_array([geozone_1, geozone_2]) end - it 'blocks collection forbidden elements' do + it 'blocks forbidden elements for scoped models' do proposal = create(:proposal, :hidden) comment_1 = create(:comment) comment_2 = create(:comment, commentable: proposal) diff --git a/spec/lib/graph_ql/root_element_resolver_spec.rb b/spec/lib/graph_ql/root_element_resolver_spec.rb index 7ca64a7e8..b86fab36e 100644 --- a/spec/lib/graph_ql/root_element_resolver_spec.rb +++ b/spec/lib/graph_ql/root_element_resolver_spec.rb @@ -2,13 +2,14 @@ require 'rails_helper' describe GraphQL::RootElementResolver do let(:comment_resolver) { GraphQL::RootElementResolver.new(Comment) } + let(:geozone_resolver) { GraphQL::RootElementResolver.new(Geozone) } describe '#call' do + it 'resolves simple elements' do comment = create(:comment) - arguments = { 'id' => comment.id } - result = comment_resolver.call(nil, arguments, nil) + result = comment_resolver.call(nil, {'id' => comment.id}, nil) expect(result).to eq(comment) end @@ -16,20 +17,28 @@ describe GraphQL::RootElementResolver do it 'returns nil when requested element is forbidden' do proposal = create(:proposal, :hidden) comment = create(:comment, commentable: proposal) - arguments = { 'id' => comment.id } - result = comment_resolver.call(nil, arguments, nil) + result = comment_resolver.call(nil, {'id' => comment.id}, nil) expect(result).to be_nil end it 'returns nil when requested element does not exist' do - arguments = { 'id' => 1 } - - result = comment_resolver.call(nil, arguments, nil) + result = comment_resolver.call(nil, {'id' => 1}, nil) expect(result).to be_nil end + + it 'uses the public_for_api scope when available' do + geozone = create(:geozone) + comment = create(:comment, commentable: create(:proposal, :hidden)) + + geozone_result = geozone_resolver.call(nil, {'id' => geozone.id}, nil) + comment_result = comment_resolver.call(nil, {'id' => comment.id}, nil) + + expect(geozone_result).to eq(geozone) + expect(comment_result).to be_nil + end end end From b0bd30e3d1d928d4817e095a4459df710071700c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 9 Jan 2017 13:36:53 +0100 Subject: [PATCH 109/147] Move api config file parsing method into the ApiTypesCreator class --- config/initializers/graphql.rb | 23 +---------------------- lib/graph_ql/api_types_creator.rb | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 55c34ec1f..f10b42b3b 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,26 +1,5 @@ api_config = YAML.load_file('./config/api.yml') - -api_type_definitions = {} - -# Parse API configuration file - -api_config.each do |api_type_model, api_type_info| - model = api_type_model.constantize - options = api_type_info['options'] - fields = {} - - api_type_info['fields'].each do |field_name, field_type| - if field_type.is_a?(Array) # paginated association - fields[field_name.to_sym] = [field_type.first.constantize] - elsif GraphQL::ApiTypesCreator::SCALAR_TYPES[field_type.to_sym] - fields[field_name.to_sym] = field_type.to_sym - else # simple association - fields[field_name.to_sym] = field_type.constantize - end - end - - api_type_definitions[model] = { options: options, fields: fields } -end +api_type_definitions = GraphQL::ApiTypesCreator::parse_api_config_file(api_config) api_types_creator = GraphQL::ApiTypesCreator.new(api_type_definitions) created_api_types = api_types_creator.create diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 51a7af0e8..374af269d 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -64,5 +64,28 @@ module GraphQL return created_type # GraphQL::ObjectType end + def self.parse_api_config_file(file) + api_type_definitions = {} + + file.each do |api_type_model, api_type_info| + model = api_type_model.constantize + options = api_type_info['options'] + fields = {} + + api_type_info['fields'].each do |field_name, field_type| + if field_type.is_a?(Array) # paginated association + fields[field_name.to_sym] = [field_type.first.constantize] + elsif SCALAR_TYPES[field_type.to_sym] + fields[field_name.to_sym] = field_type.to_sym + else # simple association + fields[field_name.to_sym] = field_type.constantize + end + end + + api_type_definitions[model] = { options: options, fields: fields } + end + + api_type_definitions + end end end From 6940b90260a95c836dc7cb2858132df54c1db153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 9 Jan 2017 14:11:51 +0100 Subject: [PATCH 110/147] Add more fields to api.yml --- app/models/comment.rb | 2 ++ app/models/debate.rb | 1 + app/models/proposal.rb | 1 + app/models/vote.rb | 4 ++++ config/api.yml | 10 ++++++++-- config/initializers/graphql.rb | 2 +- 6 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index c1e99fcef..eed5fb4ff 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -19,6 +19,8 @@ class Comment < ActiveRecord::Base belongs_to :commentable, -> { with_hidden }, polymorphic: true, counter_cache: true belongs_to :user, -> { with_hidden } + has_many :votes, -> { for_comments }, foreign_key: 'votable_id' + before_save :calculate_confidence_score scope :for_render, -> { with_hidden.includes(user: :organization) } diff --git a/app/models/debate.rb b/app/models/debate.rb index c2a4eb7f6..34fcf548e 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -17,6 +17,7 @@ class Debate < ActiveRecord::Base belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable + has_many :votes, -> { for_debates }, foreign_key: 'votable_id' validates :title, presence: true validates :description, presence: true diff --git a/app/models/proposal.rb b/app/models/proposal.rb index f915f173a..527ed7968 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -19,6 +19,7 @@ class Proposal < ActiveRecord::Base belongs_to :geozone has_many :comments, as: :commentable has_many :proposal_notifications + has_many :votes, -> { for_proposals }, foreign_key: 'votable_id' validates :title, presence: true validates :question, presence: true diff --git a/app/models/vote.rb b/app/models/vote.rb index af607ffa8..841a5784e 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,5 +1,9 @@ class Vote < ActsAsVotable::Vote + scope :for_debates, -> { where(votable_type: 'Debate') } + scope :for_proposals, -> { where(votable_type: 'Proposal') } + scope :for_comments, -> { where(votable_type: 'Comment') } + scope :public_for_api, -> do joins("FULL OUTER JOIN debates ON votable_type = 'Debate' AND votable_id = debates.id"). joins("FULL OUTER JOIN proposals ON votable_type = 'Proposal' AND votable_id = proposals.id"). diff --git a/config/api.yml b/config/api.yml index 452fa22f7..a8eb1efbe 100644 --- a/config/api.yml +++ b/config/api.yml @@ -1,7 +1,10 @@ User: fields: - id: integer - username: string + id: integer + username: string + debates: [Debate] + proposals: [Proposal] + comments: [Comment] Voter: fields: gender: string @@ -24,6 +27,7 @@ Debate: geozone: Geozone comments: [Comment] public_author: User + votes: [Vote] Proposal: fields: id: integer @@ -45,6 +49,7 @@ Proposal: comments: [Comment] proposal_notifications: [ProposalNotification] public_author: User + votes: [Vote] Comment: fields: id: integer @@ -58,6 +63,7 @@ Comment: ancestry: string confidence_score: integer public_author: User + votes: [Vote] Geozone: fields: id: integer diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index f10b42b3b..d477361fc 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -9,7 +9,7 @@ QueryType = query_type_creator.create ConsulSchema = GraphQL::Schema.define do query QueryType - max_depth 10 + max_depth 12 resolve_type -> (object, ctx) do type_name = object.class.name # look up types by class name From d1f14ffb93ce5c5463d5038db70988cb0d293f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 12 Jan 2017 19:51:58 +0100 Subject: [PATCH 111/147] Simplify the way GraphQL 'resolve' is used --- app/models/comment.rb | 2 +- app/models/proposal_notification.rb | 4 +- app/models/tag.rb | 6 +- app/models/vote.rb | 2 +- .../initializers/active_record_extensions.rb | 1 + lib/active_record_extensions.rb | 12 ++++ lib/graph_ql/api_types_creator.rb | 4 +- lib/graph_ql/association_resolver.rb | 30 ---------- lib/graph_ql/query_type_creator.rb | 4 +- lib/graph_ql/root_collection_resolver.rb | 17 ------ lib/graph_ql/root_element_resolver.rb | 24 -------- .../lib/graph_ql/association_resolver_spec.rb | 59 ------------------- .../graph_ql/root_collection_resolver_spec.rb | 28 --------- .../graph_ql/root_element_resolver_spec.rb | 44 -------------- 14 files changed, 27 insertions(+), 210 deletions(-) create mode 100644 config/initializers/active_record_extensions.rb create mode 100644 lib/active_record_extensions.rb delete mode 100644 lib/graph_ql/association_resolver.rb delete mode 100644 lib/graph_ql/root_collection_resolver.rb delete mode 100644 lib/graph_ql/root_element_resolver.rb delete mode 100644 spec/lib/graph_ql/association_resolver_spec.rb delete mode 100644 spec/lib/graph_ql/root_collection_resolver_spec.rb delete mode 100644 spec/lib/graph_ql/root_element_resolver_spec.rb diff --git a/app/models/comment.rb b/app/models/comment.rb index eed5fb4ff..5671fa1dc 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -28,7 +28,7 @@ class Comment < ActiveRecord::Base scope :not_as_admin_or_moderator, -> { where("administrator_id IS NULL").where("moderator_id IS NULL")} scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } - scope :public_for_api, -> do + def self.public_for_api joins("FULL OUTER JOIN debates ON commentable_type = 'Debate' AND commentable_id = debates.id"). joins("FULL OUTER JOIN proposals ON commentable_type = 'Proposal' AND commentable_id = proposals.id"). where("commentable_type = 'Proposal' AND proposals.hidden_at IS NULL OR commentable_type = 'Debate' AND debates.hidden_at IS NULL") diff --git a/app/models/proposal_notification.rb b/app/models/proposal_notification.rb index 686f3ce90..3483806c5 100644 --- a/app/models/proposal_notification.rb +++ b/app/models/proposal_notification.rb @@ -7,7 +7,9 @@ class ProposalNotification < ActiveRecord::Base validates :proposal, presence: true validate :minimum_interval - scope :public_for_api, -> { joins(:proposal).where("proposals.hidden_at IS NULL") } + def self.public_for_api + joins(:proposal).where("proposals.hidden_at IS NULL") + end def minimum_interval return true if proposal.try(:notifications).blank? diff --git a/app/models/tag.rb b/app/models/tag.rb index 8f34a8696..2bfc89942 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -1,3 +1,7 @@ class Tag < ActsAsTaggableOn::Tag - scope :public_for_api, -> { where("kind IS NULL OR kind = 'category'") } + + def self.public_for_api + where("kind IS NULL OR kind = 'category'") + end + end diff --git a/app/models/vote.rb b/app/models/vote.rb index 841a5784e..019191dca 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -4,7 +4,7 @@ class Vote < ActsAsVotable::Vote scope :for_proposals, -> { where(votable_type: 'Proposal') } scope :for_comments, -> { where(votable_type: 'Comment') } - scope :public_for_api, -> do + def self.public_for_api joins("FULL OUTER JOIN debates ON votable_type = 'Debate' AND votable_id = debates.id"). joins("FULL OUTER JOIN proposals ON votable_type = 'Proposal' AND votable_id = proposals.id"). joins("FULL OUTER JOIN comments ON votable_type = 'Comment' AND votable_id = comments.id"). diff --git a/config/initializers/active_record_extensions.rb b/config/initializers/active_record_extensions.rb new file mode 100644 index 000000000..57659f192 --- /dev/null +++ b/config/initializers/active_record_extensions.rb @@ -0,0 +1 @@ +require 'active_record_extensions' diff --git a/lib/active_record_extensions.rb b/lib/active_record_extensions.rb new file mode 100644 index 000000000..1dd9add87 --- /dev/null +++ b/lib/active_record_extensions.rb @@ -0,0 +1,12 @@ +module PublicForApi + + extend ActiveSupport::Concern + + class_methods do + def public_for_api + all + end + end +end + +ActiveRecord::Base.send(:include, PublicForApi) diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 374af269d..604683de1 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -49,12 +49,12 @@ module GraphQL field(field_name, SCALAR_TYPES[field_type]) when :simple_association field(field_name, -> { api_types_creator.created_types[field_type] }) do - resolve GraphQL::AssociationResolver.new(field_name, field_type) + resolve -> (object, arguments, context) { field_type.public_for_api.find(object) } end when :paginated_association field_type = field_type.first connection(field_name, -> { api_types_creator.created_types[field_type].connection_type }) do - resolve GraphQL::AssociationResolver.new(field_name, field_type) + resolve -> (object, arguments, context) { field_type.public_for_api } end end end diff --git a/lib/graph_ql/association_resolver.rb b/lib/graph_ql/association_resolver.rb deleted file mode 100644 index 22dd210f3..000000000 --- a/lib/graph_ql/association_resolver.rb +++ /dev/null @@ -1,30 +0,0 @@ -module GraphQL - class AssociationResolver - attr_reader :field_name, :target_model, :allowed_elements - - def initialize(field_name, target_model) - @field_name = field_name - @target_model = target_model - @allowed_elements = target_public_elements - end - - def call(object, arguments, context) - requested_elements = object.send(field_name) - filter_forbidden_elements(requested_elements) - end - - private - - def target_public_elements - target_model.respond_to?(:public_for_api) ? target_model.public_for_api : target_model.all - end - - def filter_forbidden_elements(requested_elements) - if requested_elements.respond_to?(:each) - requested_elements.all & allowed_elements.all - else - allowed_elements.include?(requested_elements) ? requested_elements : nil - end - end - end -end diff --git a/lib/graph_ql/query_type_creator.rb b/lib/graph_ql/query_type_creator.rb index 9918b0638..6dfde67c0 100644 --- a/lib/graph_ql/query_type_creator.rb +++ b/lib/graph_ql/query_type_creator.rb @@ -22,13 +22,13 @@ module GraphQL type created_type description "Find one #{model.model_name.human} by ID" argument :id, !types.ID - resolve GraphQL::RootElementResolver.new(model) + resolve -> (object, arguments, context) { model.public_for_api.find_by(id: arguments['id'])} end end connection model.name.underscore.pluralize.to_sym, created_type.connection_type do description "Find all #{model.model_name.human.pluralize}" - resolve GraphQL::RootCollectionResolver.new(model) + resolve -> (object, arguments, context) { model.public_for_api } end end diff --git a/lib/graph_ql/root_collection_resolver.rb b/lib/graph_ql/root_collection_resolver.rb deleted file mode 100644 index fa0e3c9ec..000000000 --- a/lib/graph_ql/root_collection_resolver.rb +++ /dev/null @@ -1,17 +0,0 @@ -module GraphQL - class RootCollectionResolver - attr_reader :target_model - - def initialize(target_model) - @target_model = target_model - end - - def call(object, arguments, context) - if target_model.respond_to?(:public_for_api) - target_model.public_for_api - else - target_model.all - end - end - end -end diff --git a/lib/graph_ql/root_element_resolver.rb b/lib/graph_ql/root_element_resolver.rb deleted file mode 100644 index cb963ac1f..000000000 --- a/lib/graph_ql/root_element_resolver.rb +++ /dev/null @@ -1,24 +0,0 @@ -module GraphQL - class RootElementResolver - attr_reader :target_model - - def initialize(target_model) - @target_model = target_model - end - - def call(object, arguments, context) - public_elements.find_by(id: arguments['id']) - end - - private - - def public_elements - if target_model.respond_to?(:public_for_api) - target_model.public_for_api - else - target_model - end - end - - end -end diff --git a/spec/lib/graph_ql/association_resolver_spec.rb b/spec/lib/graph_ql/association_resolver_spec.rb deleted file mode 100644 index 3e8bdf897..000000000 --- a/spec/lib/graph_ql/association_resolver_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'rails_helper' - -describe GraphQL::AssociationResolver do - let(:comments_resolver) { GraphQL::AssociationResolver.new(:comments, Comment) } - let(:geozone_resolver) { GraphQL::AssociationResolver.new(:geozone, Geozone) } - let(:geozones_resolver) { GraphQL::AssociationResolver.new(:geozones, Geozone) } - - describe '#initialize' do - it 'sets allowed elements for unscoped models' do - geozone_1 = create(:geozone) - geozone_2 = create(:geozone) - - expect(geozones_resolver.allowed_elements).to match_array([geozone_1, geozone_2]) - end - - it 'sets allowed elements for scoped models' do - public_comment = create(:comment, commentable: create(:proposal)) - restricted_comment = create(:comment, commentable: create(:proposal, :hidden)) - - expect(comments_resolver.allowed_elements).to match_array([public_comment]) - end - end - - describe '#call' do - it 'resolves simple associations' do - geozone = create(:geozone) - proposal = create(:proposal, geozone: geozone) - - result = geozone_resolver.call(proposal, nil, nil) - - expect(result).to eq(geozone) - end - - it 'blocks forbidden elements when resolving simple associations' do - skip 'None of the current models allows this spec to be executed' - end - - it 'resolves paginated associations' do - proposal = create(:proposal) - comment_1 = create(:comment, commentable: proposal) - comment_2 = create(:comment, commentable: proposal) - comment_3 = create(:comment, commentable: create(:proposal)) - - result = comments_resolver.call(proposal, nil, nil) - - expect(result).to match_array([comment_1, comment_2]) - end - - it 'blocks forbidden elements when resolving paginated associations' do - proposal = create(:proposal, :hidden) - comment = create(:comment, commentable: proposal) - - result = comments_resolver.call(proposal, nil, nil) - - expect(result).to be_empty - end - end - -end diff --git a/spec/lib/graph_ql/root_collection_resolver_spec.rb b/spec/lib/graph_ql/root_collection_resolver_spec.rb deleted file mode 100644 index edfc6d393..000000000 --- a/spec/lib/graph_ql/root_collection_resolver_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'rails_helper' - -describe GraphQL::RootCollectionResolver do - let(:geozones_resolver) { GraphQL::RootCollectionResolver.new(Geozone) } - let(:comments_resolver) { GraphQL::RootCollectionResolver.new(Comment) } - - describe '#call' do - it 'returns the whole colleciton for unscoped models' do - geozone_1 = create(:geozone) - geozone_2 = create(:geozone) - - result = geozones_resolver.call(nil, nil, nil) - - expect(result).to match_array([geozone_1, geozone_2]) - end - - it 'blocks forbidden elements for scoped models' do - proposal = create(:proposal, :hidden) - comment_1 = create(:comment) - comment_2 = create(:comment, commentable: proposal) - - result = comments_resolver.call(nil, nil, nil) - - expect(result).to match_array([comment_1]) - end - end - -end diff --git a/spec/lib/graph_ql/root_element_resolver_spec.rb b/spec/lib/graph_ql/root_element_resolver_spec.rb deleted file mode 100644 index b86fab36e..000000000 --- a/spec/lib/graph_ql/root_element_resolver_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'rails_helper' - -describe GraphQL::RootElementResolver do - let(:comment_resolver) { GraphQL::RootElementResolver.new(Comment) } - let(:geozone_resolver) { GraphQL::RootElementResolver.new(Geozone) } - - describe '#call' do - - it 'resolves simple elements' do - comment = create(:comment) - - result = comment_resolver.call(nil, {'id' => comment.id}, nil) - - expect(result).to eq(comment) - end - - it 'returns nil when requested element is forbidden' do - proposal = create(:proposal, :hidden) - comment = create(:comment, commentable: proposal) - - result = comment_resolver.call(nil, {'id' => comment.id}, nil) - - expect(result).to be_nil - end - - it 'returns nil when requested element does not exist' do - result = comment_resolver.call(nil, {'id' => 1}, nil) - - expect(result).to be_nil - end - - it 'uses the public_for_api scope when available' do - geozone = create(:geozone) - comment = create(:comment, commentable: create(:proposal, :hidden)) - - geozone_result = geozone_resolver.call(nil, {'id' => geozone.id}, nil) - comment_result = comment_resolver.call(nil, {'id' => comment.id}, nil) - - expect(geozone_result).to eq(geozone) - expect(comment_result).to be_nil - end - end - -end From 7a9373942a24d88bdd147dde0387e4ad0cc23980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 12 Jan 2017 20:11:47 +0100 Subject: [PATCH 112/147] Testing GraphQL 1.3.0 in Travis --- Gemfile | 2 +- Gemfile.lock | 4 ++-- app/controllers/graphql_controller.rb | 19 ++++++++++++++++++- config/initializers/graphql.rb | 18 +----------------- spec/lib/graphql_spec.rb | 20 +++++++++++++++++++- 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/Gemfile b/Gemfile index 94d66a281..c195fc8e8 100644 --- a/Gemfile +++ b/Gemfile @@ -65,7 +65,7 @@ gem 'browser' gem 'turnout', '~> 2.4.0' gem 'redcarpet' -gem 'graphql' +gem 'graphql', '~> 1.3.0' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console diff --git a/Gemfile.lock b/Gemfile.lock index ed7d74082..4ee71e106 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -178,7 +178,7 @@ GEM activesupport (>= 4.1.0) graphiql-rails (1.3.0) rails - graphql (0.18.11) + graphql (1.3.0) groupdate (3.1.1) activesupport (>= 3) gyoku (1.3.1) @@ -484,7 +484,7 @@ DEPENDENCIES foundation_rails_helper (~> 2.0.0) fuubar graphiql-rails - graphql + graphql (~> 1.3.0) groupdate (~> 3.1.0) i18n-tasks initialjs-rails (= 0.2.0.4) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index de7ae7de7..f62e2b3dd 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -8,8 +8,25 @@ class GraphqlController < ApplicationController def query begin + # ------------------------------------------------------------------------ + api_types_creator = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS) + created_api_types = api_types_creator.create + + query_type_creator = GraphQL::QueryTypeCreator.new(created_api_types) + query_type = query_type_creator.create + + consul_schema = GraphQL::Schema.define do + query query_type + max_depth 12 + + resolve_type -> (object, ctx) do + type_name = object.class.name # look up types by class name + ConsulSchema.types[type_name] + end + end + # ------------------------------------------------------------------------ set_query_environment - response = ConsulSchema.execute query_string, variables: query_variables + response = consul_schema.execute query_string, variables: query_variables render json: response, status: :ok rescue GraphqlController::QueryStringError render json: { message: 'Query string not present' }, status: :bad_request diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index d477361fc..05cd2333c 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,18 +1,2 @@ api_config = YAML.load_file('./config/api.yml') -api_type_definitions = GraphQL::ApiTypesCreator::parse_api_config_file(api_config) - -api_types_creator = GraphQL::ApiTypesCreator.new(api_type_definitions) -created_api_types = api_types_creator.create - -query_type_creator = GraphQL::QueryTypeCreator.new(created_api_types) -QueryType = query_type_creator.create - -ConsulSchema = GraphQL::Schema.define do - query QueryType - max_depth 12 - - resolve_type -> (object, ctx) do - type_name = object.class.name # look up types by class name - ConsulSchema.types[type_name] - end -end +API_TYPE_DEFINITIONS = GraphQL::ApiTypesCreator::parse_api_config_file(api_config) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 3dbf393b7..caa2bd085 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -1,5 +1,23 @@ require 'rails_helper' +# ------------------------------------------------------------------------------ +api_types_creator = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS) +created_api_types = api_types_creator.create + +query_type_creator = GraphQL::QueryTypeCreator.new(created_api_types) +QueryType = query_type_creator.create + +ConsulSchema = GraphQL::Schema.define do + query QueryType + max_depth 12 + + resolve_type -> (object, ctx) do + type_name = object.class.name # look up types by class name + ConsulSchema.types[type_name] + end +end +# ------------------------------------------------------------------------------ + def execute(query_string, context = {}, variables = {}) ConsulSchema.execute(query_string, context: context, variables: variables) end @@ -14,7 +32,7 @@ def hidden_field?(response, field_name) data_is_empty && error_is_present end -describe ConsulSchema do +describe 'ConsulSchema' do let(:user) { create(:user) } let(:proposal) { create(:proposal, author: user) } From f2eb8724d30f95c32b34b9f1ec48e33ebca73395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 13 Jan 2017 00:54:52 +0100 Subject: [PATCH 113/147] Remove duplicated scopes --- app/models/vote.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/models/vote.rb b/app/models/vote.rb index 019191dca..15d486ea4 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,9 +1,5 @@ class Vote < ActsAsVotable::Vote - scope :for_debates, -> { where(votable_type: 'Debate') } - scope :for_proposals, -> { where(votable_type: 'Proposal') } - scope :for_comments, -> { where(votable_type: 'Comment') } - def self.public_for_api joins("FULL OUTER JOIN debates ON votable_type = 'Debate' AND votable_id = debates.id"). joins("FULL OUTER JOIN proposals ON votable_type = 'Proposal' AND votable_id = proposals.id"). From d2a8d509b684b4cc3e8927365fbe883ad5cdf366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 13 Jan 2017 01:21:59 +0100 Subject: [PATCH 114/147] Cleaning --- lib/graph_ql/api_types_creator.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 604683de1..7e3b263fb 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -28,9 +28,9 @@ module GraphQL if SCALAR_TYPES[type] :scalar elsif type.class == Class - :simple_association + :singular_association elsif type.class == Array - :paginated_association + :multiple_association end end @@ -47,11 +47,11 @@ module GraphQL case ApiTypesCreator.type_kind(field_type) when :scalar field(field_name, SCALAR_TYPES[field_type]) - when :simple_association + when :singular_association field(field_name, -> { api_types_creator.created_types[field_type] }) do - resolve -> (object, arguments, context) { field_type.public_for_api.find(object) } + resolve -> (object, arguments, context) { field_type.public_for_api.find_by(id: object.id) } end - when :paginated_association + when :multiple_association field_type = field_type.first connection(field_name, -> { api_types_creator.created_types[field_type].connection_type }) do resolve -> (object, arguments, context) { field_type.public_for_api } From cb0a477d88ada2f9687c76b78f4b125e7634af27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 16 Jan 2017 11:23:48 +0100 Subject: [PATCH 115/147] Fix weird bug caused by pending specs --- spec/lib/graphql_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index caa2bd085..8c5e40910 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -47,7 +47,7 @@ describe 'ConsulSchema' do end it "returns has_one associations" do - pending "Organizations are not being exposed yet" + skip "Organizations are not being exposed yet" organization = create(:organization) response = execute("{ user(id: #{organization.user_id}) { organization { name } } }") expect(dig(response, 'data.user.organization.name')).to eq(organization.name) @@ -71,7 +71,7 @@ describe 'ConsulSchema' do end it "executes deeply nested queries" do - pending "Organizations are not being exposed yet" + skip "Organizations are not being exposed yet" org_user = create(:user) organization = create(:organization, user: org_user) org_proposal = create(:proposal, author: org_user) From 0107ddce110918a9585c10ba5f457382e6cdcf6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 16 Jan 2017 11:24:02 +0100 Subject: [PATCH 116/147] Fix bug when resolving multiple associations --- lib/graph_ql/api_types_creator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 7e3b263fb..cf266d13f 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -54,7 +54,7 @@ module GraphQL when :multiple_association field_type = field_type.first connection(field_name, -> { api_types_creator.created_types[field_type].connection_type }) do - resolve -> (object, arguments, context) { field_type.public_for_api } + resolve -> (object, arguments, context) { field_type.public_for_api & object.send(field_name) } end end end From 232f5aa621573646d1ebd07b338fedd7e29223b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 16 Jan 2017 11:25:12 +0100 Subject: [PATCH 117/147] Parse GraphQL query variables to JSON This is required after updating the gem to 1.3.0 --- app/controllers/graphql_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index f62e2b3dd..5c628fc58 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -52,10 +52,10 @@ class GraphqlController < ApplicationController end def set_query_variables - if params[:variables].nil? + if params[:variables].blank? @query_variables = {} else - @query_variables = params[:variables] + @query_variables = JSON.parse(params[:variables]) end end end From 18fb1485ebc97473111adc8f93f3a1c55a74ff32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Mon, 16 Jan 2017 11:28:28 +0100 Subject: [PATCH 118/147] No need to specify the resolve type for GraphQL::Schema This was needed back in the 0.18.11 version but not anymore after the update to 1.3.0 --- app/controllers/graphql_controller.rb | 5 ----- spec/lib/graphql_spec.rb | 5 ----- 2 files changed, 10 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 5c628fc58..eb694b981 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -18,11 +18,6 @@ class GraphqlController < ApplicationController consul_schema = GraphQL::Schema.define do query query_type max_depth 12 - - resolve_type -> (object, ctx) do - type_name = object.class.name # look up types by class name - ConsulSchema.types[type_name] - end end # ------------------------------------------------------------------------ set_query_environment diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 8c5e40910..99597c690 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -10,11 +10,6 @@ QueryType = query_type_creator.create ConsulSchema = GraphQL::Schema.define do query QueryType max_depth 12 - - resolve_type -> (object, ctx) do - type_name = object.class.name # look up types by class name - ConsulSchema.types[type_name] - end end # ------------------------------------------------------------------------------ From ef6d089022d7f1b99d3e589d51fef1db0964f785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 25 Jan 2017 11:17:08 +0100 Subject: [PATCH 119/147] Refactor GraphqlController --- app/controllers/graphql_controller.rb | 41 +++++++++------------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index eb694b981..98ceb3111 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -1,5 +1,4 @@ class GraphqlController < ApplicationController - attr_accessor :query_variables, :query_string skip_before_action :verify_authenticity_token skip_authorization_check @@ -8,19 +7,7 @@ class GraphqlController < ApplicationController def query begin - # ------------------------------------------------------------------------ - api_types_creator = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS) - created_api_types = api_types_creator.create - - query_type_creator = GraphQL::QueryTypeCreator.new(created_api_types) - query_type = query_type_creator.create - - consul_schema = GraphQL::Schema.define do - query query_type - max_depth 12 - end - # ------------------------------------------------------------------------ - set_query_environment + if query_string.nil? then raise GraphqlController::QueryStringError end response = consul_schema.execute query_string, variables: query_variables render json: response, status: :ok rescue GraphqlController::QueryStringError @@ -32,25 +19,25 @@ class GraphqlController < ApplicationController private - def set_query_environment - set_query_string - set_query_variables + def consul_schema + api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create + query_type = GraphQL::QueryTypeCreator.new(api_types).create + + GraphQL::Schema.define do + query query_type + max_depth 12 + end end - def set_query_string + def query_string if request.headers["CONTENT_TYPE"] == 'application/graphql' - @query_string = request.body.string # request.body.class => StringIO + request.body.string # request.body.class => StringIO else - @query_string = params[:query] + params[:query] end - if query_string.nil? then raise GraphqlController::QueryStringError end end - def set_query_variables - if params[:variables].blank? - @query_variables = {} - else - @query_variables = JSON.parse(params[:variables]) - end + def query_variables + params[:variables].blank? ? {} : JSON.parse(params[:variables]) end end From e3fca5c49f0f8008c88978f0e1f9f0548453ea38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 25 Jan 2017 13:37:29 +0100 Subject: [PATCH 120/147] Remove functionality related to votes demographic info Since this was giving me too much pain and nobody actually requested this functionality, I decided to remove it. --- app/models/comment.rb | 1 - app/models/concerns/public_voters_stats.rb | 12 ------------ app/models/debate.rb | 1 - app/models/proposal.rb | 1 - app/models/vote.rb | 4 ---- app/models/voter.rb | 2 -- config/api.yml | 7 ------- db/dev_seeds.rb | 3 --- lib/age_range_calculator.rb | 15 --------------- lib/graph_ql/api_types_creator.rb | 3 +-- spec/lib/age_range_calculator_spec.rb | 14 -------------- 11 files changed, 1 insertion(+), 62 deletions(-) delete mode 100644 app/models/concerns/public_voters_stats.rb delete mode 100644 app/models/voter.rb delete mode 100644 lib/age_range_calculator.rb delete mode 100644 spec/lib/age_range_calculator_spec.rb diff --git a/app/models/comment.rb b/app/models/comment.rb index 5671fa1dc..118e56b28 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,7 +1,6 @@ class Comment < ActiveRecord::Base include Flaggable include HasPublicAuthor - include PublicVotersStats acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases diff --git a/app/models/concerns/public_voters_stats.rb b/app/models/concerns/public_voters_stats.rb deleted file mode 100644 index 88d695f20..000000000 --- a/app/models/concerns/public_voters_stats.rb +++ /dev/null @@ -1,12 +0,0 @@ -module PublicVotersStats - - def votes_above_threshold? - threshold = Setting["#{self.class.name.downcase}_api_votes_threshold"] - threshold = (threshold ? threshold.to_i : default_threshold) - (total_votes >= threshold) - end - - def default_threshold - 200 - end -end diff --git a/app/models/debate.rb b/app/models/debate.rb index 34fcf548e..ce5b63fd7 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -8,7 +8,6 @@ class Debate < ActiveRecord::Base include Searchable include Filterable include HasPublicAuthor - include PublicVotersStats acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 527ed7968..0cfc9bcc9 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -7,7 +7,6 @@ class Proposal < ActiveRecord::Base include Searchable include Filterable include HasPublicAuthor - include PublicVotersStats acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/vote.rb b/app/models/vote.rb index 15d486ea4..93bcc54be 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -7,10 +7,6 @@ class Vote < ActsAsVotable::Vote where("votable_type = 'Proposal' AND proposals.hidden_at IS NULL OR votable_type = 'Debate' AND debates.hidden_at IS NULL OR votable_type = 'Comment' AND comments.hidden_at IS NULL") end - def public_voter - votable.votes_above_threshold? ? voter : nil - end - def public_timestamp self.created_at.change(min: 0) end diff --git a/app/models/voter.rb b/app/models/voter.rb deleted file mode 100644 index 7d4f52676..000000000 --- a/app/models/voter.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Voter < User -end diff --git a/config/api.yml b/config/api.yml index a8eb1efbe..7e8559ced 100644 --- a/config/api.yml +++ b/config/api.yml @@ -5,12 +5,6 @@ User: debates: [Debate] proposals: [Proposal] comments: [Comment] -Voter: - fields: - gender: string - age_range: string - geozone_id: integer - geozone: Geozone Debate: fields: id: integer @@ -86,4 +80,3 @@ Vote: votable_id: integer votable_type: string public_timestamp: string - public_voter: Voter diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 347994a00..4a716a160 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -15,9 +15,6 @@ Setting.create(key: 'proposal_code_prefix', value: 'MAD') Setting.create(key: 'votes_for_proposal_success', value: '100') Setting.create(key: 'months_to_archive_proposals', value: '12') Setting.create(key: 'comments_body_max_length', value: '1000') -Setting.create(key: 'debate_api_votes_threshold', value: '2') -Setting.create(key: 'proposal_api_votes_threshold', value: '2') -Setting.create(key: 'comment_api_votes_threshold', value: '2') Setting.create(key: 'twitter_handle', value: '@consul_dev') Setting.create(key: 'twitter_hashtag', value: '#consul_dev') diff --git a/lib/age_range_calculator.rb b/lib/age_range_calculator.rb deleted file mode 100644 index acb12e19e..000000000 --- a/lib/age_range_calculator.rb +++ /dev/null @@ -1,15 +0,0 @@ -class AgeRangeCalculator - - MIN_AGE = 16 - MAX_AGE = 1.0/0.0 # Infinity - RANGES = [ (MIN_AGE..25), (26..40), (41..60), (61..MAX_AGE) ] - - def self.range_from_birthday(dob) - # Inspired by: http://stackoverflow.com/questions/819263/get-persons-age-in-ruby/2357790#2357790 - now = Time.current.to_date - age = now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1) - - index = RANGES.find_index { |range| range.include?(age) } - index ? RANGES[index] : nil - end -end diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index cf266d13f..03ddd5d3a 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -69,7 +69,6 @@ module GraphQL file.each do |api_type_model, api_type_info| model = api_type_model.constantize - options = api_type_info['options'] fields = {} api_type_info['fields'].each do |field_name, field_type| @@ -82,7 +81,7 @@ module GraphQL end end - api_type_definitions[model] = { options: options, fields: fields } + api_type_definitions[model] = { fields: fields } end api_type_definitions diff --git a/spec/lib/age_range_calculator_spec.rb b/spec/lib/age_range_calculator_spec.rb deleted file mode 100644 index a6fd183ed..000000000 --- a/spec/lib/age_range_calculator_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'rails_helper' - -describe AgeRangeCalculator do - subject { AgeRangeCalculator } - - describe '::range_from_birthday' do - it 'returns the age range' do - expect(subject::range_from_birthday(Time.current - 1.year)).to eq(nil) - expect(subject::range_from_birthday(Time.current - 26.year)).to eq(26..40) - expect(subject::range_from_birthday(Time.current - 60.year)).to eq(41..60) - expect(subject::range_from_birthday(Time.current - 200.year)).to eq(61..subject::MAX_AGE) - end - end -end From e7f55b10e2d180c14c76a90705c8c37eaf3c4fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Wed, 25 Jan 2017 13:58:44 +0100 Subject: [PATCH 121/147] Remove obsolete specs --- app/models/user.rb | 4 -- spec/lib/graphql_spec.rb | 11 ++---- spec/models/comment_spec.rb | 1 - .../concerns/public_voters_stats_spec.rb | 39 ------------------- spec/models/debate_spec.rb | 1 - spec/models/proposal_spec.rb | 1 - spec/models/user_spec.rb | 6 --- spec/models/vote_spec.rb | 19 --------- 8 files changed, 3 insertions(+), 79 deletions(-) delete mode 100644 spec/models/concerns/public_voters_stats_spec.rb diff --git a/app/models/user.rb b/app/models/user.rb index 04ca708a9..03f2db27b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -244,10 +244,6 @@ class User < ActiveRecord::Base end delegate :can?, :cannot?, to: :ability - def age_range - AgeRangeCalculator::range_from_birthday(self.date_of_birth).to_s - end - private def clean_document_number diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 99597c690..b2a9ed477 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -1,17 +1,12 @@ require 'rails_helper' -# ------------------------------------------------------------------------------ -api_types_creator = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS) -created_api_types = api_types_creator.create - -query_type_creator = GraphQL::QueryTypeCreator.new(created_api_types) -QueryType = query_type_creator.create +api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create +query_type = GraphQL::QueryTypeCreator.new(api_types).create ConsulSchema = GraphQL::Schema.define do - query QueryType + query query_type max_depth 12 end -# ------------------------------------------------------------------------------ def execute(query_string, context = {}, variables = {}) ConsulSchema.execute(query_string, context: context, variables: variables) diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index 09e6a9900..db68a9e4e 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -4,7 +4,6 @@ describe Comment do let(:comment) { build(:comment) } - it_behaves_like "public_voters_stats" it_behaves_like "has_public_author" it "is valid" do diff --git a/spec/models/concerns/public_voters_stats_spec.rb b/spec/models/concerns/public_voters_stats_spec.rb deleted file mode 100644 index eaa923599..000000000 --- a/spec/models/concerns/public_voters_stats_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'spec_helper' - -shared_examples_for 'public_voters_stats' do - let(:model) { described_class } # the class that includes the concern - - describe 'votes_above_threshold?' do - let(:votable) { create(model.to_s.underscore.to_sym) } - - context 'with default threshold value' do - it 'is true when votes are above threshold' do - 200.times { create(:vote, votable: votable) } - - expect(votable.votes_above_threshold?).to be_truthy - end - - it 'is false when votes are under threshold' do - 199.times { create(:vote, votable: votable) } - - expect(votable.votes_above_threshold?).to be_falsey - end - end - - context 'with custom threshold value' do - it 'is true when votes are above threshold' do - create(:setting, key: "#{model.to_s.underscore}_api_votes_threshold", value: '2') - 2.times { create(:vote, votable: votable) } - - expect(votable.votes_above_threshold?).to be_truthy - end - - it 'is false when votes are under threshold' do - create(:setting, key: "#{model.to_s.underscore}_api_votes_threshold", value: '2') - create(:vote, votable: votable) - - expect(votable.votes_above_threshold?).to be_falsey - end - end - end -end diff --git a/spec/models/debate_spec.rb b/spec/models/debate_spec.rb index 4db0e5f0a..5272b6e7a 100644 --- a/spec/models/debate_spec.rb +++ b/spec/models/debate_spec.rb @@ -4,7 +4,6 @@ require 'rails_helper' describe Debate do let(:debate) { build(:debate) } - it_behaves_like "public_voters_stats" it_behaves_like "has_public_author" it "should be valid" do diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb index 08ed6b384..50df57963 100644 --- a/spec/models/proposal_spec.rb +++ b/spec/models/proposal_spec.rb @@ -4,7 +4,6 @@ require 'rails_helper' describe Proposal do let(:proposal) { build(:proposal) } - it_behaves_like "public_voters_stats" it_behaves_like "has_public_author" it "should be valid" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ca59cb288..ed0bac164 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -469,10 +469,4 @@ describe User do end - describe "#age_range" do - it 'returns string representation of age range' do - user = create(:user, date_of_birth: Time.current - 41.years) - expect(user.age_range).to eq('41..60') - end - end end diff --git a/spec/models/vote_spec.rb b/spec/models/vote_spec.rb index 9dc30d95a..8e5db0ce7 100644 --- a/spec/models/vote_spec.rb +++ b/spec/models/vote_spec.rb @@ -92,25 +92,6 @@ describe 'Vote' do end end - describe 'public_voter' do - it 'only returns voter if votable has enough votes' do - create(:setting, key: 'proposal_api_votes_threshold', value: '2') - - proposal_1 = create(:proposal) - proposal_2 = create(:proposal) - - voter_1 = create(:user) - voter_2 = create(:user) - - vote_1 = create(:vote, votable: proposal_1, voter: voter_1) - vote_2 = create(:vote, votable: proposal_2, voter: voter_1) - vote_3 = create(:vote, votable: proposal_2, voter: voter_2) - - expect(vote_1.public_voter).to be_nil - expect(vote_2.public_voter).to eq(voter_1) - end - end - describe '#public_timestamp' do it "truncates created_at timestamp up to minutes" do vote = create(:vote, created_at: Time.zone.parse('2016-02-10 15:30:45')) From 83267330a772c15bf9d9a89de7e6915b5447c67b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 10:47:12 +0100 Subject: [PATCH 122/147] Group methods related to text generation for GraphQL documentation into a module --- app/models/comment.rb | 1 + app/models/concerns/graphqlable.rb | 32 +++++++++++++++++++++++++++++ app/models/debate.rb | 1 + app/models/geozone.rb | 3 +++ app/models/proposal.rb | 1 + app/models/proposal_notification.rb | 3 +++ app/models/user.rb | 2 ++ app/models/vote.rb | 2 ++ lib/graph_ql/api_types_creator.rb | 4 ++-- lib/graph_ql/query_type_creator.rb | 8 ++++---- 10 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 app/models/concerns/graphqlable.rb diff --git a/app/models/comment.rb b/app/models/comment.rb index 118e56b28..3c7e2c8db 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,6 +1,7 @@ class Comment < ActiveRecord::Base include Flaggable include HasPublicAuthor + include Graphqlable acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases diff --git a/app/models/concerns/graphqlable.rb b/app/models/concerns/graphqlable.rb new file mode 100644 index 000000000..0e8fdb2de --- /dev/null +++ b/app/models/concerns/graphqlable.rb @@ -0,0 +1,32 @@ +module Graphqlable + extend ActiveSupport::Concern + + class_methods do + + def graphql_field_name + self.name.gsub('::', '_').underscore.to_sym + end + + def graphql_pluralized_field_name + self.name.gsub('::', '_').underscore.pluralize.to_sym + end + + def graphql_field_description + "Find one #{self.model_name.human} by ID" + end + + def graphql_pluralized_field_description + "Find all #{self.model_name.human.pluralize}" + end + + def graphql_type_name + self.name.gsub('::', '_') + end + + def graphql_type_description + "#{self.model_name.human}" + end + + end + +end diff --git a/app/models/debate.rb b/app/models/debate.rb index ce5b63fd7..cd4b147cb 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -8,6 +8,7 @@ class Debate < ActiveRecord::Base include Searchable include Filterable include HasPublicAuthor + include Graphqlable acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/geozone.rb b/app/models/geozone.rb index 7e38ce97d..55121d0ee 100644 --- a/app/models/geozone.rb +++ b/app/models/geozone.rb @@ -1,4 +1,7 @@ class Geozone < ActiveRecord::Base + + include Graphqlable + has_many :proposals has_many :spending_proposals has_many :debates diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 0cfc9bcc9..6e286104e 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -7,6 +7,7 @@ class Proposal < ActiveRecord::Base include Searchable include Filterable include HasPublicAuthor + include Graphqlable acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/proposal_notification.rb b/app/models/proposal_notification.rb index 3483806c5..7faa0fec1 100644 --- a/app/models/proposal_notification.rb +++ b/app/models/proposal_notification.rb @@ -1,4 +1,7 @@ class ProposalNotification < ActiveRecord::Base + + include Graphqlable + belongs_to :author, class_name: 'User', foreign_key: 'author_id' belongs_to :proposal diff --git a/app/models/user.rb b/app/models/user.rb index 03f2db27b..e2916306c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,8 @@ class User < ActiveRecord::Base acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + include Graphqlable + has_one :administrator has_one :moderator has_one :valuator diff --git a/app/models/vote.rb b/app/models/vote.rb index 93bcc54be..eae53e9c3 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,5 +1,7 @@ class Vote < ActsAsVotable::Vote + include Graphqlable + def self.public_for_api joins("FULL OUTER JOIN debates ON votable_type = 'Debate' AND votable_id = debates.id"). joins("FULL OUTER JOIN proposals ON votable_type = 'Proposal' AND votable_id = proposals.id"). diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 03ddd5d3a..4cb3b18c4 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -39,8 +39,8 @@ module GraphQL created_type = GraphQL::ObjectType.define do - name(model.name) - description("#{model.model_name.human}") + name model.graphql_type_name + description model.graphql_type_description # Make a field for each column, association or method fields.each do |field_name, field_type| diff --git a/lib/graph_ql/query_type_creator.rb b/lib/graph_ql/query_type_creator.rb index 6dfde67c0..79020950e 100644 --- a/lib/graph_ql/query_type_creator.rb +++ b/lib/graph_ql/query_type_creator.rb @@ -18,16 +18,16 @@ module GraphQL query_type_creator.created_api_types.each do |model, created_type| if created_type.fields['id'] - field model.name.underscore.to_sym do + field model.graphql_field_name do type created_type - description "Find one #{model.model_name.human} by ID" + description model.graphql_field_description argument :id, !types.ID resolve -> (object, arguments, context) { model.public_for_api.find_by(id: arguments['id'])} end end - connection model.name.underscore.pluralize.to_sym, created_type.connection_type do - description "Find all #{model.model_name.human.pluralize}" + connection model.graphql_pluralized_field_name, created_type.connection_type do + description model.graphql_pluralized_field_description resolve -> (object, arguments, context) { model.public_for_api } end From 69fc161b8338fbec6f1b65703d3d2b00fd276f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 10:48:05 +0100 Subject: [PATCH 123/147] Show taggings in API --- app/models/tag.rb | 7 ------- config/api.yml | 4 +++- config/initializers/acts_as_taggable_on.rb | 8 +++++++- 3 files changed, 10 insertions(+), 9 deletions(-) delete mode 100644 app/models/tag.rb diff --git a/app/models/tag.rb b/app/models/tag.rb deleted file mode 100644 index 2bfc89942..000000000 --- a/app/models/tag.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Tag < ActsAsTaggableOn::Tag - - def self.public_for_api - where("kind IS NULL OR kind = 'category'") - end - -end diff --git a/config/api.yml b/config/api.yml index 7e8559ced..4fda3393c 100644 --- a/config/api.yml +++ b/config/api.yml @@ -22,6 +22,7 @@ Debate: comments: [Comment] public_author: User votes: [Vote] + tags: ["ActsAsTaggableOn::Tag"] Proposal: fields: id: integer @@ -44,6 +45,7 @@ Proposal: proposal_notifications: [ProposalNotification] public_author: User votes: [Vote] + tags: ["ActsAsTaggableOn::Tag"] Comment: fields: id: integer @@ -69,7 +71,7 @@ ProposalNotification: proposal_id: integer created_at: string proposal: Proposal -Tag: +ActsAsTaggableOn::Tag: fields: id: integer name: string diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index 847a8c993..45b6e706c 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -20,6 +20,8 @@ module ActsAsTaggableOn Tag.class_eval do + include Graphqlable + def increment_custom_counter_for(taggable_type) Tag.increment_counter(custom_counter_field_name_for(taggable_type), id) end @@ -42,10 +44,14 @@ module ActsAsTaggableOn ActsAsTaggableOn::Tag.where('taggings.taggable_type' => 'SpendingProposal').includes(:taggings).order(:name).uniq end + def self.public_for_api + where("kind IS NULL OR kind = 'category'") + end + private def custom_counter_field_name_for(taggable_type) "#{taggable_type.underscore.pluralize}_count" end end -end \ No newline at end of file +end From e8fc387574b6f3c44bb74324f59ae14ada37e825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 10:57:02 +0100 Subject: [PATCH 124/147] Add Organization to API --- app/models/organization.rb | 3 +++ config/api.yml | 16 +++++++++++----- spec/lib/graphql_spec.rb | 2 -- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/models/organization.rb b/app/models/organization.rb index 74fd16111..94ef986a1 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -1,4 +1,7 @@ class Organization < ActiveRecord::Base + + include Graphqlable + belongs_to :user, touch: true validates :name, presence: true diff --git a/config/api.yml b/config/api.yml index 4fda3393c..d11a94bd3 100644 --- a/config/api.yml +++ b/config/api.yml @@ -1,10 +1,11 @@ User: fields: - id: integer - username: string - debates: [Debate] - proposals: [Proposal] - comments: [Comment] + id: integer + username: string + debates: [Debate] + proposals: [Proposal] + comments: [Comment] + organization: Organization Debate: fields: id: integer @@ -82,3 +83,8 @@ Vote: votable_id: integer votable_type: string public_timestamp: string +Organization: + fields: + id: integer + user_id: integer + name: string diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index b2a9ed477..bc491f6ba 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -37,7 +37,6 @@ describe 'ConsulSchema' do end it "returns has_one associations" do - skip "Organizations are not being exposed yet" organization = create(:organization) response = execute("{ user(id: #{organization.user_id}) { organization { name } } }") expect(dig(response, 'data.user.organization.name')).to eq(organization.name) @@ -61,7 +60,6 @@ describe 'ConsulSchema' do end it "executes deeply nested queries" do - skip "Organizations are not being exposed yet" org_user = create(:user) organization = create(:organization, user: org_user) org_proposal = create(:proposal, author: org_user) From 0c00b5376565f629323b12b0b62b553198ae9b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 10:59:46 +0100 Subject: [PATCH 125/147] Add Vote vote_flag field to API --- config/api.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/api.yml b/config/api.yml index d11a94bd3..669602611 100644 --- a/config/api.yml +++ b/config/api.yml @@ -83,6 +83,7 @@ Vote: votable_id: integer votable_type: string public_timestamp: string + vote_flag: boolean Organization: fields: id: integer From 14798deab0686f3948e0c19089bee2405b21f19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 12:00:01 +0100 Subject: [PATCH 126/147] Fix bug in singular_association resolver --- lib/graph_ql/api_types_creator.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 4cb3b18c4..2142a9b1e 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -49,7 +49,14 @@ module GraphQL field(field_name, SCALAR_TYPES[field_type]) when :singular_association field(field_name, -> { api_types_creator.created_types[field_type] }) do - resolve -> (object, arguments, context) { field_type.public_for_api.find_by(id: object.id) } + resolve -> (object, arguments, context) do + association_target = object.send(field_name) + if association_target.nil? + nil + else + field_type.public_for_api.find_by(id: association_target.id) + end + end end when :multiple_association field_type = field_type.first From 7cbd8da9941e75c50f2cddce52911fc235a1d8ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 12:26:42 +0100 Subject: [PATCH 127/147] Move outdated Tag specs to ActsAsTaggableOn::Tag specs file --- spec/lib/acts_as_taggable_on_spec.rb | 20 ++++++++++++++++++++ spec/models/tag_spec.rb | 24 ------------------------ 2 files changed, 20 insertions(+), 24 deletions(-) delete mode 100644 spec/models/tag_spec.rb diff --git a/spec/lib/acts_as_taggable_on_spec.rb b/spec/lib/acts_as_taggable_on_spec.rb index dbf76d52b..59a469f88 100644 --- a/spec/lib/acts_as_taggable_on_spec.rb +++ b/spec/lib/acts_as_taggable_on_spec.rb @@ -64,6 +64,26 @@ describe 'ActsAsTaggableOn' do expect(tag.proposals_count).to eq(1) end end + + describe "public_for_api scope" do + it "returns tags whose kind is NULL" do + tag = create(:tag, kind: nil) + + expect(ActsAsTaggableOn::Tag.public_for_api).to include(tag) + end + + it "returns tags whose kind is 'category'" do + tag = create(:tag, kind: 'category') + + expect(ActsAsTaggableOn::Tag.public_for_api).to include(tag) + end + + it "blocks other kinds of tags" do + tag = create(:tag, kind: 'foo') + + expect(ActsAsTaggableOn::Tag.public_for_api).not_to include(tag) + end + end end end diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb deleted file mode 100644 index fa26b7622..000000000 --- a/spec/models/tag_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'rails_helper' - -describe 'Tag' do - - describe "public_for_api scope" do - it "returns tags whose kind is NULL" do - tag = create(:tag, kind: nil) - - expect(Tag.public_for_api).to include(tag) - end - - it "returns tags whose kind is 'category'" do - tag = create(:tag, kind: 'category') - - expect(Tag.public_for_api).to include(tag) - end - - it "blocks other kinds of tags" do - tag = create(:tag, kind: 'foo') - - expect(Tag.public_for_api).not_to include(tag) - end - end -end From 4b7cebf68647f3d53f7e96616b7fb28af51da001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 26 Jan 2017 18:29:59 +0100 Subject: [PATCH 128/147] Improved public_for_api scopes and corresponding specs --- config/initializers/acts_as_taggable_on.rb | 17 ++++++- spec/lib/acts_as_taggable_on_spec.rb | 53 +++++++++++++++++++++- spec/models/comment_spec.rb | 9 +++- spec/models/debate_spec.rb | 12 +++++ spec/models/proposal_spec.rb | 12 +++++ 5 files changed, 98 insertions(+), 5 deletions(-) diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index 45b6e706c..719f1271f 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -21,7 +21,7 @@ module ActsAsTaggableOn Tag.class_eval do include Graphqlable - + def increment_custom_counter_for(taggable_type) Tag.increment_counter(custom_counter_field_name_for(taggable_type), id) end @@ -45,7 +45,20 @@ module ActsAsTaggableOn end def self.public_for_api - where("kind IS NULL OR kind = 'category'") + find_by_sql(%| + SELECT * + FROM tags + WHERE (tags.kind IS NULL OR tags.kind = 'category') AND tags.id IN ( + SELECT tag_id + FROM ( + SELECT COUNT(taggings.id) AS taggings_count, tag_id + FROM ((taggings FULL OUTER JOIN proposals ON taggable_type = 'Proposal' AND taggable_id = proposals.id) FULL OUTER JOIN debates ON taggable_type = 'Debate' AND taggable_id = debates.id) + WHERE (taggable_type = 'Proposal' AND proposals.hidden_at IS NULL) OR (taggable_type = 'Debate' AND debates.hidden_at IS NULL) + GROUP BY tag_id + ) AS tag_taggings_count_relation + WHERE taggings_count > 0 + ) + |) end private diff --git a/spec/lib/acts_as_taggable_on_spec.rb b/spec/lib/acts_as_taggable_on_spec.rb index 59a469f88..d261bbad2 100644 --- a/spec/lib/acts_as_taggable_on_spec.rb +++ b/spec/lib/acts_as_taggable_on_spec.rb @@ -66,23 +66,72 @@ describe 'ActsAsTaggableOn' do end describe "public_for_api scope" do - it "returns tags whose kind is NULL" do + + it "returns tags whose kind is NULL and have at least one tagging whose taggable is not hidden" do tag = create(:tag, kind: nil) + proposal = create(:proposal) + proposal.tag_list.add(tag) + proposal.save expect(ActsAsTaggableOn::Tag.public_for_api).to include(tag) end - it "returns tags whose kind is 'category'" do + it "returns tags whose kind is 'category' and have at least one tagging whose taggable is not hidden" do tag = create(:tag, kind: 'category') + proposal = create(:proposal) + proposal.tag_list.add(tag) + proposal.save expect(ActsAsTaggableOn::Tag.public_for_api).to include(tag) end it "blocks other kinds of tags" do tag = create(:tag, kind: 'foo') + proposal = create(:proposal) + proposal.tag_list.add(tag) + proposal.save expect(ActsAsTaggableOn::Tag.public_for_api).not_to include(tag) end + + it "blocks tags that don't have at least one tagged element" do + tag = create(:tag) + + expect(ActsAsTaggableOn::Tag.public_for_api).to_not include(tag) + end + + it 'only permits tags on proposals or debates' do + tag_1 = create(:tag) + tag_2 = create(:tag) + tag_3 = create(:tag) + + proposal = create(:proposal) + spending_proposal = create(:spending_proposal) + debate = create(:debate) + + proposal.tag_list.add(tag_1) + spending_proposal.tag_list.add(tag_2) + debate.tag_list.add(tag_3) + + proposal.save + spending_proposal.save + debate.save + + expect(ActsAsTaggableOn::Tag.public_for_api).to match_array([tag_1, tag_3]) + end + + it 'blocks tags after its taggings became hidden' do + tag = create(:tag) + proposal = create(:proposal) + proposal.tag_list.add(tag) + proposal.save + + expect(ActsAsTaggableOn::Tag.public_for_api).to include(tag) + + proposal.delete + + expect(ActsAsTaggableOn::Tag.public_for_api).to be_empty + end end end diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index db68a9e4e..993710c13 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -134,7 +134,7 @@ describe Comment do end end - describe "public_for_api" do + describe "public_for_api scope" do it "returns comments" do comment = create(:comment) @@ -174,5 +174,12 @@ describe Comment do expect(Comment.public_for_api).not_to include(comment) end + + it 'does not return comments on budget investments' do + budget_investment = create(:budget_investment) + comment = create(:comment, commentable: budget_investment) + + expect(Comment.public_for_api).not_to include(comment) + end end end diff --git a/spec/models/debate_spec.rb b/spec/models/debate_spec.rb index 5272b6e7a..bae8a5f16 100644 --- a/spec/models/debate_spec.rb +++ b/spec/models/debate_spec.rb @@ -700,4 +700,16 @@ describe Debate do end end + describe 'public_for_api scope' do + it 'returns debates' do + debate = create(:debate) + expect(Debate.public_for_api).to include(debate) + end + + it 'does not return hidden debates' do + debate = create(:debate, :hidden) + expect(Debate.public_for_api).to_not include(debate) + end + end + end diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb index 7c8d3fb41..b2bb63140 100644 --- a/spec/models/proposal_spec.rb +++ b/spec/models/proposal_spec.rb @@ -842,4 +842,16 @@ describe Proposal do end end + describe 'public_for_api scope' do + it 'returns proposals' do + proposal = create(:proposal) + expect(Proposal.public_for_api).to include(proposal) + end + + it 'does not return hidden proposals' do + proposal = create(:proposal, :hidden) + expect(Proposal.public_for_api).to_not include(proposal) + end + end + end From 6101848f7affd5e43c597ae5f85fa8f1b29d2209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 11:54:02 +0100 Subject: [PATCH 129/147] Run pending migration --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index fb563c8db..b332f05d7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -578,7 +578,7 @@ ActiveRecord::Schema.define(version: 20170114154421) do t.boolean "email_digest", default: true t.boolean "email_on_direct_message", default: true t.boolean "official_position_badge", default: false - t.datetime "password_changed_at", default: '2016-11-02 13:51:14', null: false + t.datetime "password_changed_at", default: '2016-12-21 17:55:08', null: false t.boolean "created_from_signature", default: false end From 073ce38a8d190e08f9ac08885a447541a6476cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 12:39:44 +0100 Subject: [PATCH 130/147] Improved API docs autogeneration --- app/models/concerns/graphqlable.rb | 8 ++++---- config/initializers/acts_as_taggable_on.rb | 12 ++++++++++++ lib/graph_ql/api_types_creator.rb | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/models/concerns/graphqlable.rb b/app/models/concerns/graphqlable.rb index 0e8fdb2de..b62d57808 100644 --- a/app/models/concerns/graphqlable.rb +++ b/app/models/concerns/graphqlable.rb @@ -7,14 +7,14 @@ module Graphqlable self.name.gsub('::', '_').underscore.to_sym end - def graphql_pluralized_field_name - self.name.gsub('::', '_').underscore.pluralize.to_sym - end - def graphql_field_description "Find one #{self.model_name.human} by ID" end + def graphql_pluralized_field_name + self.name.gsub('::', '_').underscore.pluralize.to_sym + end + def graphql_pluralized_field_description "Find all #{self.model_name.human.pluralize}" end diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index 719f1271f..68dad364e 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -61,6 +61,18 @@ module ActsAsTaggableOn |) end + def self.graphql_field_name + :tag + end + + def self.graphql_pluralized_field_name + :tags + end + + def self.graphql_type_name + 'Tag' + end + private def custom_counter_field_name_for(taggable_type) "#{taggable_type.underscore.pluralize}_count" diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 2142a9b1e..ddad7ca04 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -46,7 +46,7 @@ module GraphQL fields.each do |field_name, field_type| case ApiTypesCreator.type_kind(field_type) when :scalar - field(field_name, SCALAR_TYPES[field_type]) + field(field_name, SCALAR_TYPES[field_type], model.human_attribute_name(field_name)) when :singular_association field(field_name, -> { api_types_creator.created_types[field_type] }) do resolve -> (object, arguments, context) do From 620c83fb69143878d562843b457cb69f265caaa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 12:54:33 +0100 Subject: [PATCH 131/147] Simplify the way QueryTypeCreator is used --- app/controllers/graphql_controller.rb | 4 ++-- lib/graph_ql/query_type_creator.rb | 12 ++---------- spec/lib/graph_ql/query_type_creator_spec.rb | 12 ++++-------- spec/lib/graphql_spec.rb | 5 ++--- 4 files changed, 10 insertions(+), 23 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 98ceb3111..ddc143e62 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -20,8 +20,8 @@ class GraphqlController < ApplicationController private def consul_schema - api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create - query_type = GraphQL::QueryTypeCreator.new(api_types).create + api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create + query_type = GraphQL::QueryTypeCreator.create(api_types) GraphQL::Schema.define do query query_type diff --git a/lib/graph_ql/query_type_creator.rb b/lib/graph_ql/query_type_creator.rb index 79020950e..36e6626b9 100644 --- a/lib/graph_ql/query_type_creator.rb +++ b/lib/graph_ql/query_type_creator.rb @@ -3,20 +3,12 @@ require 'graphql' module GraphQL class QueryTypeCreator - attr_accessor :created_api_types - - def initialize(created_api_types) - @created_api_types = created_api_types - end - - def create - query_type_creator = self - + def self.create(api_types) GraphQL::ObjectType.define do name 'QueryType' description 'The root query for the schema' - query_type_creator.created_api_types.each do |model, created_type| + api_types.each do |model, created_type| if created_type.fields['id'] field model.graphql_field_name do type created_type diff --git a/spec/lib/graph_ql/query_type_creator_spec.rb b/spec/lib/graph_ql/query_type_creator_spec.rb index 9669de0c1..7412ae0da 100644 --- a/spec/lib/graph_ql/query_type_creator_spec.rb +++ b/spec/lib/graph_ql/query_type_creator_spec.rb @@ -7,19 +7,16 @@ describe GraphQL::QueryTypeCreator do Proposal => { fields: { id: :integer, title: :string } } } end - let(:api_types_creator) { GraphQL::ApiTypesCreator.new(api_type_definitions) } - let(:created_api_types) { api_types_creator.create } - let(:query_type_creator) { GraphQL::QueryTypeCreator.new(created_api_types) } + let(:api_types) { GraphQL::ApiTypesCreator.new(api_type_definitions).create } describe "::create" do - let(:query_type) { query_type_creator.create } + let(:query_type) { GraphQL::QueryTypeCreator.create(api_types) } it 'creates a QueryType with fields to retrieve single objects whose model fields included an ID' do field = query_type.fields['proposal'] - proposal_type = query_type_creator.created_api_types[Proposal] expect(field).to be_a(GraphQL::Field) - expect(field.type).to eq(proposal_type) + expect(field.type).to eq(api_types[Proposal]) expect(field.name).to eq('proposal') end @@ -29,10 +26,9 @@ describe GraphQL::QueryTypeCreator do it "creates a QueryType with connections to retrieve collections of objects" do connection = query_type.fields['proposals'] - proposal_type = query_type_creator.created_api_types[Proposal] expect(connection).to be_a(GraphQL::Field) - expect(connection.type).to eq(proposal_type.connection_type) + expect(connection.type).to eq(api_types[Proposal].connection_type) expect(connection.name).to eq('proposals') end end diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index bc491f6ba..2d976a608 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -1,8 +1,7 @@ require 'rails_helper' -api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create -query_type = GraphQL::QueryTypeCreator.new(api_types).create - +api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create +query_type = GraphQL::QueryTypeCreator.create(api_types) ConsulSchema = GraphQL::Schema.define do query query_type max_depth 12 From c52602e04b4b28237cd469087478a3c8dad8f832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 13:13:19 +0100 Subject: [PATCH 132/147] Simplify the way ApiTypesCreator is used --- app/controllers/graphql_controller.rb | 2 +- lib/graph_ql/api_types_creator.rb | 25 +++++++------------- spec/lib/graph_ql/api_types_creator_spec.rb | 18 +++++++------- spec/lib/graph_ql/query_type_creator_spec.rb | 2 +- spec/lib/graphql_spec.rb | 2 +- 5 files changed, 20 insertions(+), 29 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index ddc143e62..ea1decdb5 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -20,7 +20,7 @@ class GraphqlController < ApplicationController private def consul_schema - api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create + api_types = GraphQL::ApiTypesCreator.create(API_TYPE_DEFINITIONS) query_type = GraphQL::QueryTypeCreator.create(api_types) GraphQL::Schema.define do diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index ddad7ca04..c82a5e25e 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -10,16 +10,10 @@ module GraphQL string: GraphQL::STRING_TYPE } - attr_accessor :created_types - - def initialize(api_type_definitions) - @api_type_definitions = api_type_definitions - @created_types = {} - end - - def create - @api_type_definitions.each do |model, info| - self.create_type(model, info[:fields]) + def self.create(api_types_definitions) + created_types = {} + api_types_definitions.each do |model, info| + create_type(model, info[:fields], created_types) end created_types end @@ -34,10 +28,9 @@ module GraphQL end end - def create_type(model, fields) - api_types_creator = self + def self.create_type(model, fields, created_types) - created_type = GraphQL::ObjectType.define do + created_types[model] = GraphQL::ObjectType.define do name model.graphql_type_name description model.graphql_type_description @@ -48,7 +41,7 @@ module GraphQL when :scalar field(field_name, SCALAR_TYPES[field_type], model.human_attribute_name(field_name)) when :singular_association - field(field_name, -> { api_types_creator.created_types[field_type] }) do + field(field_name, -> { created_types[field_type] }) do resolve -> (object, arguments, context) do association_target = object.send(field_name) if association_target.nil? @@ -60,15 +53,13 @@ module GraphQL end when :multiple_association field_type = field_type.first - connection(field_name, -> { api_types_creator.created_types[field_type].connection_type }) do + connection(field_name, -> { created_types[field_type].connection_type }) do resolve -> (object, arguments, context) { field_type.public_for_api & object.send(field_name) } end end end end - created_types[model] = created_type - return created_type # GraphQL::ObjectType end def self.parse_api_config_file(file) diff --git a/spec/lib/graph_ql/api_types_creator_spec.rb b/spec/lib/graph_ql/api_types_creator_spec.rb index 10b43ba28..637c7f1fe 100644 --- a/spec/lib/graph_ql/api_types_creator_spec.rb +++ b/spec/lib/graph_ql/api_types_creator_spec.rb @@ -1,11 +1,11 @@ require 'rails_helper' describe GraphQL::ApiTypesCreator do - let(:api_types_creator) { GraphQL::ApiTypesCreator.new( {} ) } + let(:created_types) { {} } describe "::create_type" do it "creates fields for Int attributes" do - debate_type = api_types_creator.create_type(Debate, { id: :integer }) + debate_type = GraphQL::ApiTypesCreator.create_type(Debate, { id: :integer }, created_types) created_field = debate_type.fields['id'] expect(created_field).to be_a(GraphQL::Field) @@ -14,7 +14,7 @@ describe GraphQL::ApiTypesCreator do end it "creates fields for String attributes" do - debate_type = api_types_creator.create_type(Debate, { title: :string }) + debate_type = GraphQL::ApiTypesCreator.create_type(Debate, { title: :string }, created_types) created_field = debate_type.fields['title'] expect(created_field).to be_a(GraphQL::Field) @@ -23,8 +23,8 @@ describe GraphQL::ApiTypesCreator do end it "creates connections for :belongs_to associations" do - user_type = api_types_creator.create_type(User, { id: :integer }) - debate_type = api_types_creator.create_type(Debate, { author: User }) + user_type = GraphQL::ApiTypesCreator.create_type(User, { id: :integer }, created_types) + debate_type = GraphQL::ApiTypesCreator.create_type(Debate, { author: User }, created_types) connection = debate_type.fields['author'] @@ -34,8 +34,8 @@ describe GraphQL::ApiTypesCreator do end it "creates connections for :has_one associations" do - user_type = api_types_creator.create_type(User, { organization: Organization }) - organization_type = api_types_creator.create_type(Organization, { id: :integer }) + user_type = GraphQL::ApiTypesCreator.create_type(User, { organization: Organization }, created_types) + organization_type = GraphQL::ApiTypesCreator.create_type(Organization, { id: :integer }, created_types) connection = user_type.fields['organization'] @@ -45,8 +45,8 @@ describe GraphQL::ApiTypesCreator do end it "creates connections for :has_many associations" do - comment_type = api_types_creator.create_type(Comment, { id: :integer }) - debate_type = api_types_creator.create_type(Debate, { comments: [Comment] }) + comment_type = GraphQL::ApiTypesCreator.create_type(Comment, { id: :integer }, created_types) + debate_type = GraphQL::ApiTypesCreator.create_type(Debate, { comments: [Comment] }, created_types) connection = debate_type.fields['comments'] diff --git a/spec/lib/graph_ql/query_type_creator_spec.rb b/spec/lib/graph_ql/query_type_creator_spec.rb index 7412ae0da..6d3fe7a2a 100644 --- a/spec/lib/graph_ql/query_type_creator_spec.rb +++ b/spec/lib/graph_ql/query_type_creator_spec.rb @@ -7,7 +7,7 @@ describe GraphQL::QueryTypeCreator do Proposal => { fields: { id: :integer, title: :string } } } end - let(:api_types) { GraphQL::ApiTypesCreator.new(api_type_definitions).create } + let(:api_types) { GraphQL::ApiTypesCreator.create(api_type_definitions) } describe "::create" do let(:query_type) { GraphQL::QueryTypeCreator.create(api_types) } diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 2d976a608..06f89a356 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -api_types = GraphQL::ApiTypesCreator.new(API_TYPE_DEFINITIONS).create +api_types = GraphQL::ApiTypesCreator.create(API_TYPE_DEFINITIONS) query_type = GraphQL::QueryTypeCreator.create(api_types) ConsulSchema = GraphQL::Schema.define do query query_type From cb06075c682686bd1ef7a8e76c2db9ddb8cfcc81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 13:47:09 +0100 Subject: [PATCH 133/147] Clarify spec message --- spec/models/comment_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index 993710c13..a86566734 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -175,7 +175,7 @@ describe Comment do expect(Comment.public_for_api).not_to include(comment) end - it 'does not return comments on budget investments' do + it 'does not return comments on elements which are not debates or proposals' do budget_investment = create(:budget_investment) comment = create(:comment, commentable: budget_investment) From 6b47ce065f18fc3579db17d00d2722e1b6e0a029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 14:27:28 +0100 Subject: [PATCH 134/147] Fix buggy associations --- app/models/comment.rb | 2 -- app/models/debate.rb | 1 - app/models/proposal.rb | 1 - config/api.yml | 6 +++--- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index 269dac25d..16bb207d3 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -19,8 +19,6 @@ class Comment < ActiveRecord::Base belongs_to :commentable, -> { with_hidden }, polymorphic: true, counter_cache: true belongs_to :user, -> { with_hidden } - has_many :votes, -> { for_comments }, foreign_key: 'votable_id' - before_save :calculate_confidence_score scope :for_render, -> { with_hidden.includes(user: :organization) } diff --git a/app/models/debate.rb b/app/models/debate.rb index 56adb4f80..8254bdc10 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -17,7 +17,6 @@ class Debate < ActiveRecord::Base belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable - has_many :votes, -> { for_debates }, foreign_key: 'votable_id' validates :title, presence: true validates :description, presence: true diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 09741f5f9..c69fe4194 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -19,7 +19,6 @@ class Proposal < ActiveRecord::Base belongs_to :geozone has_many :comments, as: :commentable has_many :proposal_notifications - has_many :votes, -> { for_proposals }, foreign_key: 'votable_id' validates :title, presence: true validates :question, presence: true diff --git a/config/api.yml b/config/api.yml index 669602611..d007202cb 100644 --- a/config/api.yml +++ b/config/api.yml @@ -22,7 +22,7 @@ Debate: geozone: Geozone comments: [Comment] public_author: User - votes: [Vote] + votes_for: [Vote] tags: ["ActsAsTaggableOn::Tag"] Proposal: fields: @@ -45,7 +45,7 @@ Proposal: comments: [Comment] proposal_notifications: [ProposalNotification] public_author: User - votes: [Vote] + votes_for: [Vote] tags: ["ActsAsTaggableOn::Tag"] Comment: fields: @@ -60,7 +60,7 @@ Comment: ancestry: string confidence_score: integer public_author: User - votes: [Vote] + votes_for: [Vote] Geozone: fields: id: integer From fd240b2fc7531a059e3a315400ffd2df76a5db85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Fri, 27 Jan 2017 14:46:26 +0100 Subject: [PATCH 135/147] Rescue all exceptions in production --- app/controllers/graphql_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index ea1decdb5..ba8edc2b4 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -14,6 +14,8 @@ class GraphqlController < ApplicationController render json: { message: 'Query string not present' }, status: :bad_request rescue GraphQL::ParseError render json: { message: 'Query string is not valid JSON' }, status: :bad_request + rescue + unless Rails.env.production? then raise end end end From 70a1dbde9401e4862efaf0948e802e79c3adc85c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Tue, 31 Jan 2017 13:52:25 +0100 Subject: [PATCH 136/147] Fix bug when parsing query variables sent by the GraphiQL desktop client --- app/controllers/graphql_controller.rb | 8 +++++- spec/controllers/graphql_controller_spec.rb | 29 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index ba8edc2b4..ce8f093db 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -12,6 +12,8 @@ class GraphqlController < ApplicationController render json: response, status: :ok rescue GraphqlController::QueryStringError render json: { message: 'Query string not present' }, status: :bad_request + rescue JSON::ParserError + render json: { message: 'Error parsing JSON' }, status: :bad_request rescue GraphQL::ParseError render json: { message: 'Query string is not valid JSON' }, status: :bad_request rescue @@ -40,6 +42,10 @@ class GraphqlController < ApplicationController end def query_variables - params[:variables].blank? ? {} : JSON.parse(params[:variables]) + if params[:variables].blank? || params[:variables] == 'null' + {} + else + JSON.parse(params[:variables]) + end end end diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index e95bdd3c9..f96392b73 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -8,18 +8,21 @@ describe GraphqlController, type: :request do describe "handles GET request" do specify "with query string inside query params" do get '/graphql', query: "{ proposal(id: #{proposal.id}) { title } }" + expect(response).to have_http_status(:ok) expect(JSON.parse(response.body)['data']['proposal']['title']).to eq(proposal.title) end specify "with malformed query string" do get '/graphql', query: 'Malformed query string' + expect(response).to have_http_status(:bad_request) expect(JSON.parse(response.body)['message']).to eq('Query string is not valid JSON') end specify "without query string" do get '/graphql' + expect(response).to have_http_status(:bad_request) expect(JSON.parse(response.body)['message']).to eq('Query string not present') end @@ -30,6 +33,7 @@ describe GraphqlController, type: :request do specify "with json-encoded query string inside body" do post '/graphql', { query: "{ proposal(id: #{proposal.id}) { title } }" }.to_json, json_headers + expect(response).to have_http_status(:ok) expect(JSON.parse(response.body)['data']['proposal']['title']).to eq(proposal.title) end @@ -37,20 +41,45 @@ describe GraphqlController, type: :request do specify "with raw query string inside body" do graphql_headers = { "CONTENT_TYPE" => "application/graphql" } post '/graphql', "{ proposal(id: #{proposal.id}) { title } }", graphql_headers + expect(response).to have_http_status(:ok) expect(JSON.parse(response.body)['data']['proposal']['title']).to eq(proposal.title) end specify "with malformed query string" do post '/graphql', { query: "Malformed query string" }.to_json, json_headers + expect(response).to have_http_status(:bad_request) expect(JSON.parse(response.body)['message']).to eq('Query string is not valid JSON') end it "without query string" do post '/graphql', json_headers + expect(response).to have_http_status(:bad_request) expect(JSON.parse(response.body)['message']).to eq('Query string not present') end end + + describe "correctly parses query variables" do + let(:query_string) { "{ proposal(id: #{proposal.id}) { title } }" } + + specify "when absent" do + get '/graphql', { query: query_string } + + expect(response).to have_http_status(:ok) + end + + specify "when specified as the 'null' string" do + get '/graphql', { query: query_string, variables: 'null' } + + expect(response).to have_http_status(:ok) + end + + specify "when specified as an empty string" do + get '/graphql', { query: query_string, variables: '' } + + expect(response).to have_http_status(:ok) + end + end end From cad66ea85ac668d95c9a8f16b6e426b870b1d0e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garce=CC=81s?= Date: Thu, 11 May 2017 22:29:21 +0200 Subject: [PATCH 137/147] Update GraphQL and GraphiQL gem --- Gemfile | 4 ++-- Gemfile.lock | 8 ++++---- spec/controllers/graphql_controller_spec.rb | 14 ++++++++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index d7f212e86..3cd21e0eb 100644 --- a/Gemfile +++ b/Gemfile @@ -67,7 +67,7 @@ gem 'redcarpet', '~> 3.4.0' gem 'paperclip' -gem 'graphql', '~> 1.3.0' +gem 'graphql', '~> 1.5.12' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console @@ -104,7 +104,7 @@ end group :development do # Access an IRB console on exception pages or by using <%= console %> in views gem 'web-console', '3.3.0' - gem 'graphiql-rails' + gem 'graphiql-rails', '~> 1.4.1' end eval_gemfile './Gemfile_custom' diff --git a/Gemfile.lock b/Gemfile.lock index 585295501..b0d2ddc22 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -174,9 +174,9 @@ GEM geocoder (1.4.3) globalid (0.3.7) activesupport (>= 4.1.0) - graphiql-rails (1.3.0) + graphiql-rails (1.4.1) rails - graphql (1.3.0) + graphql (1.5.12) groupdate (3.2.0) gyoku (1.3.1) builder (>= 2.1.2) @@ -499,8 +499,8 @@ DEPENDENCIES foundation-rails (~> 6.2.4.0) foundation_rails_helper (~> 2.0.0) fuubar - graphiql-rails - graphql (~> 1.3.0) + graphiql-rails (~> 1.4.1) + graphql (~> 1.5.12) groupdate (~> 3.2.0) i18n-tasks (~> 0.9.15) initialjs-rails (= 0.2.0.4) diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index f96392b73..1b8c64793 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -2,6 +2,12 @@ require 'rails_helper' # Useful resource: http://graphql.org/learn/serving-over-http/ +def parser_error_raised?(response) + data_is_empty = response['data'].nil? + error_is_present = (JSON.parse(response.body)['errors'].first['message'] =~ /^Parse error on/) + data_is_empty && error_is_present +end + describe GraphqlController, type: :request do let(:proposal) { create(:proposal) } @@ -16,8 +22,8 @@ describe GraphqlController, type: :request do specify "with malformed query string" do get '/graphql', query: 'Malformed query string' - expect(response).to have_http_status(:bad_request) - expect(JSON.parse(response.body)['message']).to eq('Query string is not valid JSON') + expect(response).to have_http_status(:ok) + expect(parser_error_raised?(response)).to be_truthy end specify "without query string" do @@ -49,8 +55,8 @@ describe GraphqlController, type: :request do specify "with malformed query string" do post '/graphql', { query: "Malformed query string" }.to_json, json_headers - expect(response).to have_http_status(:bad_request) - expect(JSON.parse(response.body)['message']).to eq('Query string is not valid JSON') + expect(response).to have_http_status(:ok) + expect(parser_error_raised?(response)).to be_truthy end it "without query string" do From 3c7f60d62547d36ccdb50574bda4da48336ae032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garce=CC=81s?= Date: Thu, 11 May 2017 22:42:43 +0200 Subject: [PATCH 138/147] Fix initializer --- config/initializers/graphql.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb index 05cd2333c..2731e1887 100644 --- a/config/initializers/graphql.rb +++ b/config/initializers/graphql.rb @@ -1,2 +1,4 @@ -api_config = YAML.load_file('./config/api.yml') -API_TYPE_DEFINITIONS = GraphQL::ApiTypesCreator::parse_api_config_file(api_config) +if ActiveRecord::Base.connection.tables.any? + api_config = YAML.load_file('./config/api.yml') + API_TYPE_DEFINITIONS = GraphQL::ApiTypesCreator::parse_api_config_file(api_config) +end From ad8aba07399feae2afc1a969109c5a28782edf02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garce=CC=81s?= Date: Mon, 15 May 2017 20:13:20 +0200 Subject: [PATCH 139/147] Revised public fields, wrote more exhaustive specs --- app/models/concerns/graphqlable.rb | 4 + app/models/user.rb | 12 + app/models/vote.rb | 16 +- config/api.yml | 44 +- lib/graph_ql/api_types_creator.rb | 6 +- spec/lib/graphql_spec.rb | 596 +++++++++++++++++++++- spec/models/comment_spec.rb | 6 + spec/models/proposal_notification_spec.rb | 8 +- spec/models/vote_spec.rb | 25 +- 9 files changed, 665 insertions(+), 52 deletions(-) diff --git a/app/models/concerns/graphqlable.rb b/app/models/concerns/graphqlable.rb index b62d57808..651a285b9 100644 --- a/app/models/concerns/graphqlable.rb +++ b/app/models/concerns/graphqlable.rb @@ -29,4 +29,8 @@ module Graphqlable end + def public_created_at + self.created_at.change(min: 0) + end + end diff --git a/app/models/user.rb b/app/models/user.rb index 1c6d3d2eb..f5a6586fd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -286,6 +286,18 @@ class User < ActiveRecord::Base end delegate :can?, :cannot?, to: :ability + def public_proposals + public_activity? ? proposals : [] + end + + def public_debates + public_activity? ? debates : [] + end + + def public_comments + public_activity? ? comments : [] + end + private def clean_document_number diff --git a/app/models/vote.rb b/app/models/vote.rb index eae53e9c3..49cc4cd86 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,15 +1,19 @@ class Vote < ActsAsVotable::Vote include Graphqlable - + def self.public_for_api joins("FULL OUTER JOIN debates ON votable_type = 'Debate' AND votable_id = debates.id"). joins("FULL OUTER JOIN proposals ON votable_type = 'Proposal' AND votable_id = proposals.id"). joins("FULL OUTER JOIN comments ON votable_type = 'Comment' AND votable_id = comments.id"). - where("votable_type = 'Proposal' AND proposals.hidden_at IS NULL OR votable_type = 'Debate' AND debates.hidden_at IS NULL OR votable_type = 'Comment' AND comments.hidden_at IS NULL") - end - - def public_timestamp - self.created_at.change(min: 0) + where("(votable_type = 'Proposal' AND proposals.hidden_at IS NULL) OR \ + (votable_type = 'Debate' AND debates.hidden_at IS NULL) OR \ + ( \ + (votable_type = 'Comment' AND comments.hidden_at IS NULL) AND \ + ( \ + (comments.commentable_type = 'Proposal' AND (comments.commentable_id IN (SELECT id FROM proposals WHERE hidden_at IS NULL GROUP BY id))) OR \ + (comments.commentable_type = 'Debate' AND (comments.commentable_id IN (SELECT id FROM debates WHERE hidden_at IS NULL GROUP BY id))) \ + ) \ + )") end end diff --git a/config/api.yml b/config/api.yml index d007202cb..0c668eac2 100644 --- a/config/api.yml +++ b/config/api.yml @@ -2,24 +2,22 @@ User: fields: id: integer username: string - debates: [Debate] - proposals: [Proposal] - comments: [Comment] - organization: Organization + public_debates: [Debate] + public_proposals: [Proposal] + public_comments: [Comment] +# organization: Organization Debate: fields: id: integer title: string description: string - created_at: string + public_created_at: string cached_votes_total: integer cached_votes_up: integer cached_votes_down: integer comments_count: integer hot_score: integer confidence_score: integer - geozone_id: integer - geozone: Geozone comments: [Comment] public_author: User votes_for: [Vote] @@ -34,7 +32,7 @@ Proposal: comments_count: integer hot_score: integer confidence_score: integer - created_at: string + public_created_at: string summary: string video_url: string geozone_id: integer @@ -53,7 +51,7 @@ Comment: commentable_id: integer commentable_type: string body: string - created_at: string + public_created_at: string cached_votes_total: integer cached_votes_up: integer cached_votes_down: integer @@ -67,11 +65,11 @@ Geozone: name: string ProposalNotification: fields: - title: string - body: string - proposal_id: integer - created_at: string - proposal: Proposal + title: string + body: string + proposal_id: integer + public_created_at: string + proposal: Proposal ActsAsTaggableOn::Tag: fields: id: integer @@ -80,12 +78,12 @@ ActsAsTaggableOn::Tag: kind: string Vote: fields: - votable_id: integer - votable_type: string - public_timestamp: string - vote_flag: boolean -Organization: - fields: - id: integer - user_id: integer - name: string + votable_id: integer + votable_type: string + public_created_at: string + vote_flag: boolean +# Organization: +# fields: +# id: integer +# user_id: integer +# name: string diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index c82a5e25e..acf3e2d20 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -44,11 +44,7 @@ module GraphQL field(field_name, -> { created_types[field_type] }) do resolve -> (object, arguments, context) do association_target = object.send(field_name) - if association_target.nil? - nil - else - field_type.public_for_api.find_by(id: association_target.id) - end + association_target.present? ? field_type.public_for_api.find_by(id: association_target.id) : nil end end when :multiple_association diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 06f89a356..8bf12245f 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -21,32 +21,46 @@ def hidden_field?(response, field_name) data_is_empty && error_is_present end +def extract_fields(response, collection_name, field_chain) + fields = field_chain.split('.') + dig(response, "data.#{collection_name}.edges").collect do |node| + begin + if fields.size > 1 + node['node'][fields.first][fields.second] + else + node['node'][fields.first] + end + rescue NoMethodError + end + end.compact +end + describe 'ConsulSchema' do let(:user) { create(:user) } let(:proposal) { create(:proposal, author: user) } - it "returns fields of Int type" do + it 'returns fields of Int type' do response = execute("{ proposal(id: #{proposal.id}) { id } }") expect(dig(response, 'data.proposal.id')).to eq(proposal.id) end - it "returns fields of String type" do + it 'returns fields of String type' do response = execute("{ proposal(id: #{proposal.id}) { title } }") expect(dig(response, 'data.proposal.title')).to eq(proposal.title) end - it "returns has_one associations" do + xit 'returns has_one associations' do organization = create(:organization) response = execute("{ user(id: #{organization.user_id}) { organization { name } } }") expect(dig(response, 'data.user.organization.name')).to eq(organization.name) end - it "returns belongs_to associations" do + it 'returns belongs_to associations' do response = execute("{ proposal(id: #{proposal.id}) { public_author { username } } }") expect(dig(response, 'data.proposal.public_author.username')).to eq(proposal.public_author.username) end - it "returns has_many associations" do + it 'returns has_many associations' do comments_author = create(:user) comment_1 = create(:comment, author: comments_author, commentable: proposal) comment_2 = create(:comment, author: comments_author, commentable: proposal) @@ -58,7 +72,7 @@ describe 'ConsulSchema' do expect(comment_bodies).to match_array([comment_1.body, comment_2.body]) end - it "executes deeply nested queries" do + xit 'executes deeply nested queries' do org_user = create(:user) organization = create(:organization, user: org_user) org_proposal = create(:proposal, author: org_user) @@ -67,36 +81,594 @@ describe 'ConsulSchema' do expect(dig(response, 'data.proposal.public_author.organization.name')).to eq(organization.name) end - it "hides confidential fields of Int type" do + it 'hides confidential fields of Int type' do response = execute("{ user(id: #{user.id}) { failed_census_calls_count } }") expect(hidden_field?(response, 'failed_census_calls_count')).to be_truthy end - it "hides confidential fields of String type" do + it 'hides confidential fields of String type' do response = execute("{ user(id: #{user.id}) { encrypted_password } }") expect(hidden_field?(response, 'encrypted_password')).to be_truthy end - it "hides confidential has_one associations" do + xit 'hides confidential has_one associations' do user.administrator = create(:administrator) response = execute("{ user(id: #{user.id}) { administrator { id } } }") expect(hidden_field?(response, 'administrator')).to be_truthy end - it "hides confidential belongs_to associations" do + it 'hides confidential belongs_to associations' do create(:failed_census_call, user: user) response = execute("{ user(id: #{user.id}) { failed_census_calls { id } } }") expect(hidden_field?(response, 'failed_census_calls')).to be_truthy end - it "hides confidential has_many associations" do + it 'hides confidential has_many associations' do create(:direct_message, sender: user) response = execute("{ user(id: #{user.id}) { direct_messages_sent { id } } }") expect(hidden_field?(response, 'direct_messages_sent')).to be_truthy end - it "hides confidential fields inside deeply nested queries" do + it 'hides confidential fields inside deeply nested queries' do response = execute("{ proposals(first: 1) { edges { node { public_author { encrypted_password } } } } }") expect(hidden_field?(response, 'encrypted_password')).to be_truthy end + + describe 'Users' do + let(:user) { create(:user, public_activity: false) } + + it 'does not link debates if activity is not public' do + create(:debate, author: user) + + response = execute("{ user(id: #{user.id}) { public_debates { edges { node { title } } } } }") + received_debates = dig(response, 'data.user.public_debates.edges') + + expect(received_debates).to eq [] + end + + it 'does not link proposals if activity is not public' do + create(:proposal, author: user) + + response = execute("{ user(id: #{user.id}) { public_proposals { edges { node { title } } } } }") + received_proposals = dig(response, 'data.user.public_proposals.edges') + + expect(received_proposals).to eq [] + end + + it 'does not link comments if activity is not public' do + create(:comment, author: user) + + response = execute("{ user(id: #{user.id}) { public_comments { edges { node { body } } } } }") + received_comments = dig(response, 'data.user.public_comments.edges') + + expect(received_comments).to eq [] + end + + end + + describe 'Proposals' do + it 'does not include hidden proposals' do + visible_proposal = create(:proposal) + hidden_proposal = create(:proposal, :hidden) + + response = execute('{ proposals { edges { node { title } } } }') + received_titles = extract_fields(response, 'proposals', 'title') + + expect(received_titles).to match_array [visible_proposal.title] + end + + xit 'only returns proposals of the Human Rights proceeding' do + proposal = create(:proposal) + human_rights_proposal = create(:proposal, proceeding: 'Derechos Humanos', sub_proceeding: 'Right to have a job') + other_proceeding_proposal = create(:proposal) + other_proceeding_proposal.update_attribute(:proceeding, 'Another proceeding') + + response = execute('{ proposals { edges { node { title } } } }') + received_titles = extract_fields(response, 'proposals', 'title') + + expect(received_titles).to match_array [proposal.title, human_rights_proposal.title] + end + + it 'includes proposals of authors even if public activity is set to false' do + visible_author = create(:user, public_activity: true) + hidden_author = create(:user, public_activity: false) + + visible_proposal = create(:proposal, author: visible_author) + hidden_proposal = create(:proposal, author: hidden_author) + + response = execute('{ proposals { edges { node { title } } } }') + received_titles = extract_fields(response, 'proposals', 'title') + + expect(received_titles).to match_array [visible_proposal.title, hidden_proposal.title] + end + + it 'does not link author if public activity is set to false' do + visible_author = create(:user, public_activity: true) + hidden_author = create(:user, public_activity: false) + + visible_proposal = create(:proposal, author: visible_author) + hidden_proposal = create(:proposal, author: hidden_author) + + response = execute('{ proposals { edges { node { public_author { username } } } } }') + received_authors = extract_fields(response, 'proposals', 'public_author.username') + + expect(received_authors).to match_array [visible_author.username] + end + + it 'only returns date and hour for created_at' do + created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone) + create(:proposal, created_at: created_at) + + response = execute('{ proposals { edges { node { public_created_at } } } }') + received_timestamps = extract_fields(response, 'proposals', 'public_created_at') + + expect(received_timestamps.first).to include('09:00:00') + end + + it 'only retruns tags with kind nil or category' do + tag = create(:tag, name: 'Parks') + category_tag = create(:tag, name: 'Health', kind: 'category') + admin_tag = create(:tag, name: 'Admin tag', kind: 'admin') + + proposal = create(:proposal, tag_list: 'Parks, Health, Admin tag') + + response = execute("{ proposal(id: #{proposal.id}) { tags { edges { node { name } } } } }") + received_tags = dig(response, 'data.proposal.tags.edges').map { |node| node['node']['name'] } + + expect(received_tags).to match_array ['Parks', 'Health'] + end + + end + + describe 'Debates' do + it 'does not include hidden debates' do + visible_debate = create(:debate) + hidden_debate = create(:debate, :hidden) + + response = execute('{ debates { edges { node { title } } } }') + received_titles = extract_fields(response, 'debates', 'title') + + expect(received_titles).to match_array [visible_debate.title] + end + + it 'includes debates of authors even if public activity is set to false' do + visible_author = create(:user, public_activity: true) + hidden_author = create(:user, public_activity: false) + + visible_debate = create(:debate, author: visible_author) + hidden_debate = create(:debate, author: hidden_author) + + response = execute('{ debates { edges { node { title } } } }') + received_titles = extract_fields(response, 'debates', 'title') + + expect(received_titles).to match_array [visible_debate.title, hidden_debate.title] + end + + it 'does not link author if public activity is set to false' do + visible_author = create(:user, public_activity: true) + hidden_author = create(:user, public_activity: false) + + visible_debate = create(:debate, author: visible_author) + hidden_debate = create(:debate, author: hidden_author) + + response = execute('{ debates { edges { node { public_author { username } } } } }') + received_authors = extract_fields(response, 'debates', 'public_author.username') + + expect(received_authors).to match_array [visible_author.username] + end + + it 'only returns date and hour for created_at' do + created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone) + create(:debate, created_at: created_at) + + response = execute('{ debates { edges { node { public_created_at } } } }') + received_timestamps = extract_fields(response, 'debates', 'public_created_at') + + expect(received_timestamps.first).to include('09:00:00') + end + + it 'only retruns tags with kind nil or category' do + tag = create(:tag, name: 'Parks') + category_tag = create(:tag, name: 'Health', kind: 'category') + admin_tag = create(:tag, name: 'Admin tag', kind: 'admin') + + debate = create(:debate, tag_list: 'Parks, Health, Admin tag') + + response = execute("{ debate(id: #{debate.id}) { tags { edges { node { name } } } } }") + received_tags = dig(response, 'data.debate.tags.edges').map { |node| node['node']['name'] } + + expect(received_tags).to match_array ['Parks', 'Health'] + end + end + + describe 'Comments' do + it 'only returns comments from proposals and debates' do + proposal_comment = create(:comment, commentable: create(:proposal)) + debate_comment = create(:comment, commentable: create(:debate)) + spending_proposal_comment = build(:comment, commentable: create(:spending_proposal)).save(skip_validation: true) + + response = execute('{ comments { edges { node { commentable_type } } } }') + received_commentables = extract_fields(response, 'comments', 'commentable_type') + + expect(received_commentables).to match_array ['Proposal', 'Debate'] + end + + it 'displays comments of authors even if public activity is set to false' do + visible_author = create(:user, public_activity: true) + hidden_author = create(:user, public_activity: false) + + visible_comment = create(:comment, user: visible_author) + hidden_comment = create(:comment, user: hidden_author) + + response = execute('{ comments { edges { node { body } } } }') + received_comments = extract_fields(response, 'comments', 'body') + + expect(received_comments).to match_array [visible_comment.body, hidden_comment.body] + end + + it 'does not link author if public activity is set to false' do + visible_author = create(:user, public_activity: true) + hidden_author = create(:user, public_activity: false) + + visible_comment = create(:comment, author: visible_author) + hidden_comment = create(:comment, author: hidden_author) + + response = execute('{ comments { edges { node { public_author { username } } } } }') + received_authors = extract_fields(response, 'comments', 'public_author.username') + + expect(received_authors).to match_array [visible_author.username] + end + + it 'does not include hidden comments' do + visible_comment = create(:comment) + hidden_comment = create(:comment, hidden_at: Time.now) + + response = execute('{ comments { edges { node { body } } } }') + received_comments = extract_fields(response, 'comments', 'body') + + expect(received_comments).to match_array [visible_comment.body] + end + + it 'does not include comments from hidden proposals' do + visible_proposal = create(:proposal) + hidden_proposal = create(:proposal, hidden_at: Time.now) + + visible_proposal_comment = create(:comment, commentable: visible_proposal) + hidden_proposal_comment = create(:comment, commentable: hidden_proposal) + + response = execute('{ comments { edges { node { body } } } }') + received_comments = extract_fields(response, 'comments', 'body') + + expect(received_comments).to match_array [visible_proposal_comment.body] + end + + it 'does not include comments from hidden debates' do + visible_debate = create(:debate) + hidden_debate = create(:debate, hidden_at: Time.now) + + visible_debate_comment = create(:comment, commentable: visible_debate) + hidden_debate_comment = create(:comment, commentable: hidden_debate) + + response = execute('{ comments { edges { node { body } } } }') + received_comments = extract_fields(response, 'comments', 'body') + + expect(received_comments).to match_array [visible_debate_comment.body] + end + + it 'does not include comments of debates that are not public' do + not_public_debate = create(:debate, :hidden) + not_public_debate_comment = create(:comment, commentable: not_public_debate) + allow(Comment).to receive(:public_for_api).and_return([]) + + response = execute('{ comments { edges { node { body } } } }') + received_comments = extract_fields(response, 'comments', 'body') + + expect(received_comments).to_not include(not_public_debate_comment.body) + end + + it 'does not include comments of proposals that are not public' do + not_public_proposal = create(:proposal) + not_public_proposal_comment = create(:comment, commentable: not_public_proposal) + allow(Comment).to receive(:public_for_api).and_return([]) + + response = execute('{ comments { edges { node { body } } } }') + received_comments = extract_fields(response, 'comments', 'body') + + expect(received_comments).to_not include(not_public_proposal_comment.body) + end + + it 'only returns date and hour for created_at' do + created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone) + create(:comment, created_at: created_at) + + response = execute('{ comments { edges { node { public_created_at } } } }') + received_timestamps = extract_fields(response, 'comments', 'public_created_at') + + expect(received_timestamps.first).to include('09:00:00') + end + end + + describe 'Geozones' do + it 'returns geozones' do + geozone_names = [ create(:geozone), create(:geozone) ].map { |geozone| geozone.name } + + response = execute('{ geozones { edges { node { name } } } }') + received_names = extract_fields(response, 'geozones', 'name') + + expect(received_names).to match_array geozone_names + end + end + + describe 'Proposal notifications' do + + it 'does not include proposal notifications for hidden proposals' do + visible_proposal = create(:proposal) + hidden_proposal = create(:proposal, :hidden) + + visible_proposal_notification = create(:proposal_notification, proposal: visible_proposal) + hidden_proposal_notification = create(:proposal_notification, proposal: hidden_proposal) + + response = execute('{ proposal_notifications { edges { node { title } } } }') + received_notifications = extract_fields(response, 'proposal_notifications', 'title') + + expect(received_notifications).to match_array [visible_proposal_notification.title] + end + + it 'does not include proposal notifications for proposals that are not public' do + not_public_proposal = create(:proposal) + not_public_proposal_notification = create(:proposal_notification, proposal: not_public_proposal) + allow(ProposalNotification).to receive(:public_for_api).and_return([]) + + response = execute('{ proposal_notifications { edges { node { title } } } }') + received_notifications = extract_fields(response, 'proposal_notifications', 'title') + + expect(received_notifications).to_not include(not_public_proposal_notification.title) + end + + it 'only returns date and hour for created_at' do + created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone) + create(:proposal_notification, created_at: created_at) + + response = execute('{ proposal_notifications { edges { node { public_created_at } } } }') + received_timestamps = extract_fields(response, 'proposal_notifications', 'public_created_at') + + expect(received_timestamps.first).to include('09:00:00') + end + + it 'only links proposal if public' do + visible_proposal = create(:proposal) + hidden_proposal = create(:proposal, :hidden) + + visible_proposal_notification = create(:proposal_notification, proposal: visible_proposal) + hidden_proposal_notification = create(:proposal_notification, proposal: hidden_proposal) + + response = execute('{ proposal_notifications { edges { node { proposal { title } } } } }') + received_proposals = extract_fields(response, 'proposal_notifications', 'proposal.title') + + expect(received_proposals).to match_array [visible_proposal.title] + end + + end + + describe 'Tags' do + it 'only display tags with kind nil or category' do + tag = create(:tag, name: 'Parks') + category_tag = create(:tag, name: 'Health', kind: 'category') + admin_tag = create(:tag, name: 'Admin tag', kind: 'admin') + + proposal = create(:proposal, tag_list: 'Parks') + proposal = create(:proposal, tag_list: 'Health') + proposal = create(:proposal, tag_list: 'Admin tag') + + response = execute('{ tags { edges { node { name } } } }') + received_tags = extract_fields(response, 'tags', 'name') + + expect(received_tags).to match_array ['Parks', 'Health'] + end + + xit 'uppercase and lowercase tags work ok together for proposals' do + create(:tag, name: 'Health') + create(:tag, name: 'health') + create(:proposal, tag_list: 'health') + create(:proposal, tag_list: 'Health') + + response = execute('{ tags { edges { node { name } } } }') + received_tags = extract_fields(response, 'tags', 'name') + + expect(received_tags).to match_array ['Health', 'health'] + end + + xit 'uppercase and lowercase tags work ok together for debates' do + create(:tag, name: 'Health') + create(:tag, name: 'health') + create(:debate, tag_list: 'Health') + create(:debate, tag_list: 'health') + + response = execute('{ tags { edges { node { name } } } }') + received_tags = extract_fields(response, 'tags', 'name') + + expect(received_tags).to match_array ['Health', 'health'] + end + + it 'does not display tags for hidden proposals' do + proposal = create(:proposal, tag_list: 'Health') + hidden_proposal = create(:proposal, :hidden, tag_list: 'SPAM') + + response = execute('{ tags { edges { node { name } } } }') + received_tags = extract_fields(response, 'tags', 'name') + + expect(received_tags).to match_array ['Health'] + end + + it 'does not display tags for hidden debates' do + debate = create(:debate, tag_list: 'Health, Transportation') + hidden_debate = create(:debate, :hidden, tag_list: 'SPAM') + + response = execute('{ tags { edges { node { name } } } }') + received_tags = extract_fields(response, 'tags', 'name') + + expect(received_tags).to match_array ['Health', 'Transportation'] + end + + xit "does not display tags for proceeding's proposals" do + valid_proceeding_proposal = create(:proposal, proceeding: 'Derechos Humanos', sub_proceeding: 'Right to a Home', tag_list: 'Health') + invalid_proceeding_proposal = create(:proposal, tag_list: 'Animals') + invalid_proceeding_proposal.update_attribute('proceeding', 'Random') + + response = execute('{ tags { edges { node { name } } } }') + received_tags = extract_fields(response, 'tags', 'name') + + expect(received_tags).to match_array ['Health'] + end + + it 'does not display tags for taggings that are not public' do + proposal = create(:proposal, tag_list: 'Health') + allow(ActsAsTaggableOn::Tag).to receive(:public_for_api).and_return([]) + + response = execute('{ tags { edges { node { name } } } }') + received_tags = extract_fields(response, 'tags', 'name') + + expect(received_tags).to_not include('Health') + end + + end + + describe 'Votes' do + + it 'only returns votes from proposals, debates and comments' do + proposal = create(:proposal) + debate = create(:debate) + comment = create(:comment) + spending_proposal = create(:spending_proposal) + + proposal_vote = create(:vote, votable: proposal) + debate_vote = create(:vote, votable: debate) + comment_vote = create(:vote, votable: comment) + spending_proposal_vote = create(:vote, votable: spending_proposal) + + response = execute('{ votes { edges { node { votable_type } } } }') + received_votables = extract_fields(response, 'votes', 'votable_type') + + expect(received_votables).to match_array ['Proposal', 'Debate', 'Comment'] + end + + it 'does not include votes from hidden debates' do + visible_debate = create(:debate) + hidden_debate = create(:debate, :hidden) + + visible_debate_vote = create(:vote, votable: visible_debate) + hidden_debate_vote = create(:vote, votable: hidden_debate) + + response = execute('{ votes { edges { node { votable_id } } } }') + received_debates = extract_fields(response, 'votes', 'votable_id') + + expect(received_debates).to match_array [visible_debate.id] + end + + it 'does not include votes of hidden proposals' do + visible_proposal = create(:proposal) + hidden_proposal = create(:proposal, hidden_at: Time.now) + + visible_proposal_vote = create(:vote, votable: visible_proposal) + hidden_proposal_vote = create(:vote, votable: hidden_proposal) + + response = execute('{ votes { edges { node { votable_id } } } }') + received_proposals = extract_fields(response, 'votes', 'votable_id') + + expect(received_proposals).to match_array [visible_proposal.id] + end + + it 'does not include votes of hidden comments' do + visible_comment = create(:comment) + hidden_comment = create(:comment, hidden_at: Time.now) + + visible_comment_vote = create(:vote, votable: visible_comment) + hidden_comment_vote = create(:vote, votable: hidden_comment) + + response = execute('{ votes { edges { node { votable_id } } } }') + received_comments = extract_fields(response, 'votes', 'votable_id') + + expect(received_comments).to match_array [visible_comment.id] + end + + it 'does not include votes of comments from a hidden proposal' do + visible_proposal = create(:proposal) + hidden_proposal = create(:proposal, :hidden) + + visible_proposal_comment = create(:comment, commentable: visible_proposal) + hidden_proposal_comment = create(:comment, commentable: hidden_proposal) + + visible_proposal_comment_vote = create(:vote, votable: visible_proposal_comment) + hidden_proposal_comment_vote = create(:vote, votable: hidden_proposal_comment) + + response = execute('{ votes { edges { node { votable_id } } } }') + received_votables = extract_fields(response, 'votes', 'votable_id') + + expect(received_votables).to match_array [visible_proposal_comment.id] + end + + it 'does not include votes of comments from a hidden debate' do + visible_debate = create(:debate) + hidden_debate = create(:debate, :hidden) + + visible_debate_comment = create(:comment, commentable: visible_debate) + hidden_debate_comment = create(:comment, commentable: hidden_debate) + + visible_debate_comment_vote = create(:vote, votable: visible_debate_comment) + hidden_debate_comment_vote = create(:vote, votable: hidden_debate_comment) + + response = execute('{ votes { edges { node { votable_id } } } }') + received_votables = extract_fields(response, 'votes', 'votable_id') + + expect(received_votables).to match_array [visible_debate_comment.id] + end + + it 'does not include votes of debates that are not public' do + not_public_debate = create(:debate) + allow(Vote).to receive(:public_for_api).and_return([]) + + not_public_debate_vote = create(:vote, votable: not_public_debate) + + response = execute('{ votes { edges { node { votable_id } } } }') + received_votables = extract_fields(response, 'votes', 'votable_id') + + expect(received_votables).to_not include(not_public_debate.id) + end + + it 'does not include votes of a hidden proposals' do + not_public_proposal = create(:proposal) + allow(Vote).to receive(:public_for_api).and_return([]) + + not_public_proposal_vote = create(:vote, votable: not_public_proposal) + + response = execute('{ votes { edges { node { votable_id } } } }') + received_votables = extract_fields(response, 'votes', 'votable_id') + + expect(received_votables).to_not include(not_public_proposal.id) + end + + it 'does not include votes of a hidden comments' do + not_public_comment = create(:comment) + allow(Vote).to receive(:public_for_api).and_return([]) + + not_public_comment_vote = create(:vote, votable: not_public_comment) + + response = execute('{ votes { edges { node { votable_id } } } }') + received_votables = extract_fields(response, 'votes', 'votable_id') + + expect(received_votables).to_not include(not_public_comment.id) + end + + it 'only returns date and hour for created_at' do + created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone) + create(:vote, created_at: created_at) + + response = execute('{ votes { edges { node { public_created_at } } } }') + received_timestamps = extract_fields(response, 'votes', 'public_created_at') + + expect(received_timestamps.first).to include('09:00:00') + end + + end + end diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index a86566734..a060ef59c 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -181,5 +181,11 @@ describe Comment do expect(Comment.public_for_api).not_to include(comment) end + + it 'does not return comments with no commentable' do + comment = build(:comment, commentable: nil).save!(validate: false) + + expect(Comment.public_for_api).to_not include(comment) + end end end diff --git a/spec/models/proposal_notification_spec.rb b/spec/models/proposal_notification_spec.rb index cd8bf346a..88a98ebee 100644 --- a/spec/models/proposal_notification_spec.rb +++ b/spec/models/proposal_notification_spec.rb @@ -30,12 +30,18 @@ describe ProposalNotification do expect(ProposalNotification.public_for_api).to include(notification) end - it "blocks notifications whose proposal is hidden" do + it "blocks proposal notifications whose proposal is hidden" do proposal = create(:proposal, :hidden) notification = create(:proposal_notification, proposal: proposal) expect(ProposalNotification.public_for_api).not_to include(notification) end + + it "blocks proposal notifications without proposal" do + proposal = build(:proposal_notification, proposal: nil).save!(validate: false) + + expect(ProposalNotification.public_for_api).not_to include(notification) + end end describe "minimum interval between notifications" do diff --git a/spec/models/vote_spec.rb b/spec/models/vote_spec.rb index 8e5db0ce7..25fd069f2 100644 --- a/spec/models/vote_spec.rb +++ b/spec/models/vote_spec.rb @@ -84,18 +84,33 @@ describe 'Vote' do expect(Vote.public_for_api).not_to include(vote) end + it 'blocks votes on comments on hidden proposals' do + hidden_proposal = create(:proposal, :hidden) + comment_on_hidden_proposal = create(:comment, commentable: hidden_proposal) + vote = create(:vote, votable: comment_on_hidden_proposal) + + expect(Vote.public_for_api).to_not include(vote) + end + + it 'blocks votes on comments on hidden debates' do + hidden_debate = create(:debate, :hidden) + comment_on_hidden_debate = create(:comment, commentable: hidden_debate) + vote = create(:vote, votable: comment_on_hidden_debate) + + expect(Vote.public_for_api).to_not include(vote) + end + it 'blocks any other kind of votes' do spending_proposal = create(:spending_proposal) vote = create(:vote, votable: spending_proposal) expect(Vote.public_for_api).not_to include(vote) end - end - describe '#public_timestamp' do - it "truncates created_at timestamp up to minutes" do - vote = create(:vote, created_at: Time.zone.parse('2016-02-10 15:30:45')) - expect(vote.public_timestamp).to eq(Time.zone.parse('2016-02-10 15:00:00')) + it 'blocks votes without votable' do + vote = build(:vote, votable: nil).save!(validate: false) + + expect(Vote.public_for_api).not_to include(vote) end end end From d4752491f82af39ea9a62392c6bc8c32a092093b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garce=CC=81s?= Date: Mon, 15 May 2017 20:39:59 +0200 Subject: [PATCH 140/147] Fix timezones --- spec/lib/graphql_spec.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index 8bf12245f..725cc109a 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -196,13 +196,13 @@ describe 'ConsulSchema' do end it 'only returns date and hour for created_at' do - created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone) + created_at = Time.zone.parse("2017-12-31 9:30:15") create(:proposal, created_at: created_at) response = execute('{ proposals { edges { node { public_created_at } } } }') received_timestamps = extract_fields(response, 'proposals', 'public_created_at') - expect(received_timestamps.first).to include('09:00:00') + expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") end it 'only retruns tags with kind nil or category' do @@ -258,13 +258,13 @@ describe 'ConsulSchema' do end it 'only returns date and hour for created_at' do - created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone) + created_at = Time.zone.parse("2017-12-31 9:30:15") create(:debate, created_at: created_at) response = execute('{ debates { edges { node { public_created_at } } } }') received_timestamps = extract_fields(response, 'debates', 'public_created_at') - expect(received_timestamps.first).to include('09:00:00') + expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") end it 'only retruns tags with kind nil or category' do @@ -378,13 +378,13 @@ describe 'ConsulSchema' do end it 'only returns date and hour for created_at' do - created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone) + created_at = Time.zone.parse("2017-12-31 9:30:15") create(:comment, created_at: created_at) response = execute('{ comments { edges { node { public_created_at } } } }') received_timestamps = extract_fields(response, 'comments', 'public_created_at') - expect(received_timestamps.first).to include('09:00:00') + expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") end end @@ -426,13 +426,13 @@ describe 'ConsulSchema' do end it 'only returns date and hour for created_at' do - created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone) + created_at = Time.zone.parse("2017-12-31 9:30:15") create(:proposal_notification, created_at: created_at) response = execute('{ proposal_notifications { edges { node { public_created_at } } } }') received_timestamps = extract_fields(response, 'proposal_notifications', 'public_created_at') - expect(received_timestamps.first).to include('09:00:00') + expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") end it 'only links proposal if public' do @@ -660,13 +660,13 @@ describe 'ConsulSchema' do end it 'only returns date and hour for created_at' do - created_at = Time.new(2017, 12, 31, 9, 30, 15).in_time_zone(Time.zone) + created_at = Time.zone.parse("2017-12-31 9:30:15") create(:vote, created_at: created_at) response = execute('{ votes { edges { node { public_created_at } } } }') received_timestamps = extract_fields(response, 'votes', 'public_created_at') - expect(received_timestamps.first).to include('09:00:00') + expect(Time.zone.parse(received_timestamps.first)).to eq Time.zone.parse("2017-12-31 9:00:00") end end From 18db68a4305cd07b68476a7ca4f0346a70673c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garce=CC=81s?= Date: Mon, 29 May 2017 09:47:39 +0200 Subject: [PATCH 141/147] Force pagination, limit query depth and complexity --- app/controllers/graphql_controller.rb | 3 ++- lib/graph_ql/api_types_creator.rb | 2 +- lib/graph_ql/query_type_creator.rb | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index ce8f093db..762239dc8 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -29,7 +29,8 @@ class GraphqlController < ApplicationController GraphQL::Schema.define do query query_type - max_depth 12 + max_depth 8 + max_complexity 2500 end end diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index acf3e2d20..4e548748c 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -49,7 +49,7 @@ module GraphQL end when :multiple_association field_type = field_type.first - connection(field_name, -> { created_types[field_type].connection_type }) do + connection(field_name, -> { created_types[field_type].connection_type }, max_page_size: 50, complexity: 1000) do resolve -> (object, arguments, context) { field_type.public_for_api & object.send(field_name) } end end diff --git a/lib/graph_ql/query_type_creator.rb b/lib/graph_ql/query_type_creator.rb index 36e6626b9..71beb8033 100644 --- a/lib/graph_ql/query_type_creator.rb +++ b/lib/graph_ql/query_type_creator.rb @@ -18,7 +18,7 @@ module GraphQL end end - connection model.graphql_pluralized_field_name, created_type.connection_type do + connection(model.graphql_pluralized_field_name, created_type.connection_type, max_page_size: 50, complexity: 1000) do description model.graphql_pluralized_field_description resolve -> (object, arguments, context) { model.public_for_api } end From 9ec8b166d7758947f4be0cb9854162640c25ea1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garce=CC=81s?= Date: Thu, 1 Jun 2017 20:04:51 +0200 Subject: [PATCH 142/147] Use scopes for better query performance --- app/models/comment.rb | 3 +-- app/models/debate.rb | 1 + app/models/geozone.rb | 2 ++ app/models/proposal.rb | 1 + app/models/proposal_notification.rb | 4 +--- app/models/user.rb | 7 ++++--- app/models/vote.rb | 3 ++- config/initializers/active_record_extensions.rb | 1 - config/initializers/acts_as_taggable_on.rb | 2 +- lib/active_record_extensions.rb | 12 ------------ lib/graph_ql/api_types_creator.rb | 2 +- 11 files changed, 14 insertions(+), 24 deletions(-) delete mode 100644 config/initializers/active_record_extensions.rb delete mode 100644 lib/active_record_extensions.rb diff --git a/app/models/comment.rb b/app/models/comment.rb index ec90553aa..1bb72af7e 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -26,8 +26,7 @@ class Comment < ActiveRecord::Base scope :with_visible_author, -> { joins(:user).where("users.hidden_at IS NULL") } scope :not_as_admin_or_moderator, -> { where("administrator_id IS NULL").where("moderator_id IS NULL")} scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } - - def self.public_for_api + scope :public_for_api, -> do joins("FULL OUTER JOIN debates ON commentable_type = 'Debate' AND commentable_id = debates.id"). joins("FULL OUTER JOIN proposals ON commentable_type = 'Proposal' AND commentable_id = proposals.id"). where("commentable_type = 'Proposal' AND proposals.hidden_at IS NULL OR commentable_type = 'Debate' AND debates.hidden_at IS NULL") diff --git a/app/models/debate.rb b/app/models/debate.rb index 8254bdc10..95940b3c3 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -39,6 +39,7 @@ class Debate < ActiveRecord::Base scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } scope :last_week, -> { where("created_at >= ?", 7.days.ago)} scope :featured, -> { where("featured_at is not null")} + scope :public_for_api, -> { all } # Ahoy setup visitable # Ahoy will automatically assign visit_id on create diff --git a/app/models/geozone.rb b/app/models/geozone.rb index 9ef4065ef..ad0fe9cd5 100644 --- a/app/models/geozone.rb +++ b/app/models/geozone.rb @@ -8,6 +8,8 @@ class Geozone < ActiveRecord::Base has_many :users validates :name, presence: true + scope :public_for_api, -> { all } + def self.names Geozone.pluck(:name) end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 9ed84f9d9..f37dff3d5 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -53,6 +53,7 @@ class Proposal < ActiveRecord::Base scope :retired, -> { where.not(retired_at: nil) } scope :not_retired, -> { where(retired_at: nil) } scope :successful, -> { where("cached_votes_up >= ?", Proposal.votes_needed_for_success) } + scope :public_for_api, -> { all } def to_param "#{id}-#{title}".parameterize diff --git a/app/models/proposal_notification.rb b/app/models/proposal_notification.rb index 7faa0fec1..25ae40883 100644 --- a/app/models/proposal_notification.rb +++ b/app/models/proposal_notification.rb @@ -10,9 +10,7 @@ class ProposalNotification < ActiveRecord::Base validates :proposal, presence: true validate :minimum_interval - def self.public_for_api - joins(:proposal).where("proposals.hidden_at IS NULL") - end + scope :public_for_api, -> { joins(:proposal).where("proposals.hidden_at IS NULL") } def minimum_interval return true if proposal.try(:notifications).blank? diff --git a/app/models/user.rb b/app/models/user.rb index f5a6586fd..fc34490a7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -59,6 +59,7 @@ class User < ActiveRecord::Base scope :email_digest, -> { where(email_digest: true) } scope :active, -> { where(erased_at: nil) } scope :erased, -> { where.not(erased_at: nil) } + scope :public_for_api, -> { all } before_validation :clean_document_number @@ -287,15 +288,15 @@ class User < ActiveRecord::Base delegate :can?, :cannot?, to: :ability def public_proposals - public_activity? ? proposals : [] + public_activity? ? proposals : User.none end def public_debates - public_activity? ? debates : [] + public_activity? ? debates : User.none end def public_comments - public_activity? ? comments : [] + public_activity? ? comments : User.none end private diff --git a/app/models/vote.rb b/app/models/vote.rb index 49cc4cd86..b390a733a 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -2,7 +2,7 @@ class Vote < ActsAsVotable::Vote include Graphqlable - def self.public_for_api + scope :public_for_api, -> do joins("FULL OUTER JOIN debates ON votable_type = 'Debate' AND votable_id = debates.id"). joins("FULL OUTER JOIN proposals ON votable_type = 'Proposal' AND votable_id = proposals.id"). joins("FULL OUTER JOIN comments ON votable_type = 'Comment' AND votable_id = comments.id"). @@ -16,4 +16,5 @@ class Vote < ActsAsVotable::Vote ) \ )") end + end diff --git a/config/initializers/active_record_extensions.rb b/config/initializers/active_record_extensions.rb deleted file mode 100644 index 57659f192..000000000 --- a/config/initializers/active_record_extensions.rb +++ /dev/null @@ -1 +0,0 @@ -require 'active_record_extensions' diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index 68dad364e..98cef9a0f 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -44,7 +44,7 @@ module ActsAsTaggableOn ActsAsTaggableOn::Tag.where('taggings.taggable_type' => 'SpendingProposal').includes(:taggings).order(:name).uniq end - def self.public_for_api + scope :public_for_api, -> do find_by_sql(%| SELECT * FROM tags diff --git a/lib/active_record_extensions.rb b/lib/active_record_extensions.rb deleted file mode 100644 index 1dd9add87..000000000 --- a/lib/active_record_extensions.rb +++ /dev/null @@ -1,12 +0,0 @@ -module PublicForApi - - extend ActiveSupport::Concern - - class_methods do - def public_for_api - all - end - end -end - -ActiveRecord::Base.send(:include, PublicForApi) diff --git a/lib/graph_ql/api_types_creator.rb b/lib/graph_ql/api_types_creator.rb index 4e548748c..f8f78355e 100644 --- a/lib/graph_ql/api_types_creator.rb +++ b/lib/graph_ql/api_types_creator.rb @@ -50,7 +50,7 @@ module GraphQL when :multiple_association field_type = field_type.first connection(field_name, -> { created_types[field_type].connection_type }, max_page_size: 50, complexity: 1000) do - resolve -> (object, arguments, context) { field_type.public_for_api & object.send(field_name) } + resolve -> (object, arguments, context) { object.send(field_name).public_for_api } end end end From 0ad1ff5845e1f9913968c57ad215b3f29509f37e Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 13 Jun 2017 12:11:20 +0200 Subject: [PATCH 143/147] changes the way public_for_api is calculated in comment --- app/models/comment.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index c285ab0d8..ffa02d09c 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -27,9 +27,10 @@ class Comment < ActiveRecord::Base scope :not_as_admin_or_moderator, -> { where("administrator_id IS NULL").where("moderator_id IS NULL")} scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } scope :public_for_api, -> do - joins("FULL OUTER JOIN debates ON commentable_type = 'Debate' AND commentable_id = debates.id"). - joins("FULL OUTER JOIN proposals ON commentable_type = 'Proposal' AND commentable_id = proposals.id"). - where("commentable_type = 'Proposal' AND proposals.hidden_at IS NULL OR commentable_type = 'Debate' AND debates.hidden_at IS NULL") + where(%{(comments.commentable_type = 'Debate' and comments.commentable_id in (?)) or + (comments.commentable_type = 'Proposal' and comments.commentable_id in (?))}, + Debate.public_for_api.pluck(:id), + Proposal.public_for_api.pluck(:id)) end scope :sort_by_most_voted, -> { order(confidence_score: :desc, created_at: :desc) } From 92633195d978db174537c93c9de785dcfceea412 Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 13 Jun 2017 12:19:02 +0200 Subject: [PATCH 144/147] refactors proposal_notification.public_for_api --- app/models/proposal_notification.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/proposal_notification.rb b/app/models/proposal_notification.rb index 25ae40883..9f36679a4 100644 --- a/app/models/proposal_notification.rb +++ b/app/models/proposal_notification.rb @@ -1,7 +1,7 @@ class ProposalNotification < ActiveRecord::Base include Graphqlable - + belongs_to :author, class_name: 'User', foreign_key: 'author_id' belongs_to :proposal @@ -10,7 +10,7 @@ class ProposalNotification < ActiveRecord::Base validates :proposal, presence: true validate :minimum_interval - scope :public_for_api, -> { joins(:proposal).where("proposals.hidden_at IS NULL") } + scope :public_for_api, -> { where(proposal_id: Proposal.public_for_api.pluck(:id)) } def minimum_interval return true if proposal.try(:notifications).blank? From a580e607869fc914cd794fa8f9ee894fbc5b7895 Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 13 Jun 2017 16:14:24 +0200 Subject: [PATCH 145/147] refactor public_for_api acts_as_taggable scopes --- config/initializers/acts_as_taggable_on.rb | 32 ++++++++++------------ 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb index 98cef9a0f..e570e08de 100644 --- a/config/initializers/acts_as_taggable_on.rb +++ b/config/initializers/acts_as_taggable_on.rb @@ -5,6 +5,15 @@ module ActsAsTaggableOn after_create :increment_tag_custom_counter after_destroy :touch_taggable, :decrement_tag_custom_counter + scope :public_for_api, -> do + where(%{taggings.tag_id in (?) and + (taggings.taggable_type = 'Debate' and taggings.taggable_id in (?)) or + (taggings.taggable_type = 'Proposal' and taggings.taggable_id in (?))}, + Tag.where('kind IS NULL or kind = ?', 'category').pluck(:id), + Debate.public_for_api.pluck(:id), + Proposal.public_for_api.pluck(:id)) + end + def touch_taggable taggable.touch if taggable.present? end @@ -22,6 +31,12 @@ module ActsAsTaggableOn include Graphqlable + scope :public_for_api, -> do + where('(tags.kind IS NULL or tags.kind = ?) and tags.id in (?)', + 'category', + Tagging.public_for_api.pluck('DISTINCT taggings.tag_id')) + end + def increment_custom_counter_for(taggable_type) Tag.increment_counter(custom_counter_field_name_for(taggable_type), id) end @@ -44,23 +59,6 @@ module ActsAsTaggableOn ActsAsTaggableOn::Tag.where('taggings.taggable_type' => 'SpendingProposal').includes(:taggings).order(:name).uniq end - scope :public_for_api, -> do - find_by_sql(%| - SELECT * - FROM tags - WHERE (tags.kind IS NULL OR tags.kind = 'category') AND tags.id IN ( - SELECT tag_id - FROM ( - SELECT COUNT(taggings.id) AS taggings_count, tag_id - FROM ((taggings FULL OUTER JOIN proposals ON taggable_type = 'Proposal' AND taggable_id = proposals.id) FULL OUTER JOIN debates ON taggable_type = 'Debate' AND taggable_id = debates.id) - WHERE (taggable_type = 'Proposal' AND proposals.hidden_at IS NULL) OR (taggable_type = 'Debate' AND debates.hidden_at IS NULL) - GROUP BY tag_id - ) AS tag_taggings_count_relation - WHERE taggings_count > 0 - ) - |) - end - def self.graphql_field_name :tag end From 430e020a6af4918606f5d197123530e6810dd874 Mon Sep 17 00:00:00 2001 From: kikito Date: Tue, 13 Jun 2017 16:27:09 +0200 Subject: [PATCH 146/147] refactors public_for_api in vote --- app/models/vote.rb | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/app/models/vote.rb b/app/models/vote.rb index b390a733a..14b11a68d 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -3,18 +3,12 @@ class Vote < ActsAsVotable::Vote include Graphqlable scope :public_for_api, -> do - joins("FULL OUTER JOIN debates ON votable_type = 'Debate' AND votable_id = debates.id"). - joins("FULL OUTER JOIN proposals ON votable_type = 'Proposal' AND votable_id = proposals.id"). - joins("FULL OUTER JOIN comments ON votable_type = 'Comment' AND votable_id = comments.id"). - where("(votable_type = 'Proposal' AND proposals.hidden_at IS NULL) OR \ - (votable_type = 'Debate' AND debates.hidden_at IS NULL) OR \ - ( \ - (votable_type = 'Comment' AND comments.hidden_at IS NULL) AND \ - ( \ - (comments.commentable_type = 'Proposal' AND (comments.commentable_id IN (SELECT id FROM proposals WHERE hidden_at IS NULL GROUP BY id))) OR \ - (comments.commentable_type = 'Debate' AND (comments.commentable_id IN (SELECT id FROM debates WHERE hidden_at IS NULL GROUP BY id))) \ - ) \ - )") + where(%{(votes.votable_type = 'Debate' and votes.votable_id in (?)) or + (votes.votable_type = 'Proposal' and votes.votable_id in (?)) or + (votes.votable_type = 'Comment' and votes.votable_id in (?))}, + Debate.public_for_api.pluck(:id), + Proposal.public_for_api.pluck(:id), + Comment.public_for_api.pluck(:id)) end - + end From aa29905e3191ad229adb2d623882b8a31c703b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baza=CC=81n?= Date: Wed, 14 Jun 2017 10:44:33 +0200 Subject: [PATCH 147/147] updates graphql --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index f3355313c..b737e0080 100644 --- a/Gemfile +++ b/Gemfile @@ -71,7 +71,7 @@ gem 'rails-assets-markdown-it', source: 'https://rails-assets.org' gem 'cocoon' -gem 'graphql', '~> 1.5.12' +gem 'graphql', '~> 1.6.3' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console diff --git a/Gemfile.lock b/Gemfile.lock index fdfe79413..9a37210bc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -178,7 +178,7 @@ GEM activesupport (>= 4.1.0) graphiql-rails (1.4.1) rails - graphql (1.5.12) + graphql (1.6.3) groupdate (3.2.0) gyoku (1.3.1) builder (>= 2.1.2) @@ -507,7 +507,7 @@ DEPENDENCIES foundation_rails_helper (~> 2.0.0) fuubar graphiql-rails (~> 1.4.1) - graphql (~> 1.5.12) + graphql (~> 1.6.3) groupdate (~> 3.2.0) i18n-tasks (~> 0.9.15) initialjs-rails (= 0.2.0.4)