graphql-rails-schemaker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dee18b3ddc12dcfeb8a5af7efff3f9bda49765c5
4
+ data.tar.gz: 11d353b219a8bd9578c45e6d00de5e924964f555
5
+ SHA512:
6
+ metadata.gz: 8794d320f62f775543c675587177250f0e00af3c7154cf50ff530c279d26c4c89d437c573c63d3fbd021b5776db6bf1a307c6e7e7d539d43f6dbd29305dbd0d3
7
+ data.tar.gz: 9bedf41247e193fd0ef9ee5f2922c2d39cbfbb5553baca88f37935dd8c5017326b99bc60a5a7953cdef70692ce528e2fce31b5ae97d3fd2b4b38baec09ac478c
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at turner.cole@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in graphql-rails-schemaker.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Cole Turner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # graphql-rails-schemaker
2
+ A rake task to interactive create a GraphQL Schema for Rails Models.
3
+
4
+ ## To generate a base schema:
5
+ - Run "rails graphql:generate"
6
+ - When prompted, enter "c" or "s" for camel case or snake case output respectively.
7
+ - Follow the prompts to generate the schema.
8
+
9
+
10
+ ### Word of Caution
11
+ This tool is designed to facilitate setup of a GraphQL Schema in Rails 5 Application. It has not been tested in any prior verison of Rails. This task will not run it if detects a previous setup @ `./app/graph/schema.rb` It will overwrite any files in `./app/graph/` if no `schema.rb` exists.
12
+
13
+ GraphQL Rails Schemaker is not a one-size-fits all solution. It will create a "base" schema including object types and sub-type dependencies from all models existing in the Rails application. It has been designed to formulate a generic schema to fit a wide variety of applications with support for associations.
14
+
15
+ **Do not run this in production environments.**
16
+
17
+
18
+ # Todo
19
+ - Generate Enum and Union Types
20
+ - Add Input Type templates
21
+ - Add Mutation Type templates
22
+ - Integration with `graphql-rails-resolver` (if installed)
23
+
24
+ ## Needs Help
25
+ The `object_type.rb` template is large and cumbersome. The Todo above is planned for action. If you would like to handle any of the above, please file a pull request and add your name to the credits list.
26
+
27
+
28
+ ## Credits
29
+ Cole Turner (http://cole.codes/)
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "graphql/rails/schemaker"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'graphql/rails/schemaker/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "graphql-rails-schemaker"
8
+ spec.version = Graphql::Rails::Schemaker::VERSION
9
+ spec.date = Date.today.to_s
10
+ spec.authors = ["Cole Turner"]
11
+ spec.email = ["turner.cole@gmail.com"]
12
+
13
+ spec.summary = ""
14
+ spec.homepage = "https://github.com/colepatrickturner/graphql-rails-schemaker"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_runtime_dependency "graphql", ">= 0.19.0"
25
+ spec.add_development_dependency "bundler", "~> 1.13"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.required_ruby_version = '>= 2.2.2'
28
+ end
data/lib/.DS_Store ADDED
Binary file
Binary file
@@ -0,0 +1,12 @@
1
+ require "graphql/rails/schemaker/version"
2
+ require 'graphql/rails/schemaker/railtie' if defined?(Rails)
3
+
4
+ module Graphql
5
+ module Rails
6
+ module Schemaker
7
+ def self.root
8
+ File.dirname __dir__
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,36 @@
1
+ class CamelCaseMiddleware
2
+ def call(parent_type, parent_object, field_definition, field_args, query_context, next_middleware)
3
+ next_middleware.call([parent_type, parent_object, field_definition, transform_arguments(field_args), query_context])
4
+ end
5
+
6
+ def transform_arguments(field_args)
7
+ transformed_args = {}
8
+ types = {}
9
+
10
+ field_args.each_value do |arg_value|
11
+ key = arg_value.key.to_s
12
+ unless key == "clientMutationId"
13
+ key = key.underscore
14
+ end
15
+
16
+ transformed_args[key] = transform_value(arg_value.value)
17
+ types[key] = arg_value.definition
18
+ end
19
+
20
+ GraphQL::Query::Arguments.new(transformed_args, argument_definitions: types)
21
+ end
22
+
23
+ def transform_value(value)
24
+ case value
25
+ when Array
26
+ value.map { |v| transform_value(v) }
27
+ when Hash
28
+ Hash[value.map { |k, v| [underscore_key(k), convert_hash_keys(v)] }]
29
+ when GraphQL::Query::Arguments
30
+ transform_arguments(value)
31
+ else
32
+ value
33
+ end
34
+ end
35
+
36
+ end
@@ -0,0 +1,12 @@
1
+ <%=name%>Enum = GraphQL::EnumType.define do
2
+ name "<%=name%>"
3
+ description "Enumerated values for <%=name%>"
4
+
5
+ <%
6
+ values.each do |key,value|
7
+ -%>
8
+ value(<%=key.inspect%>, <%="Value of \"#{value}\"".inspect%>, value: <%=value.inspect%>)
9
+ <%
10
+ end
11
+ -%>
12
+ end
@@ -0,0 +1,6 @@
1
+ <%=mutation_type_name%> = GraphQL::ObjectType.define do
2
+ name "Mutation"
3
+ description "The mutation root of this schema"
4
+
5
+ #field :sampleMutation, field: SampleMutation.field
6
+ end
@@ -0,0 +1,71 @@
1
+ <%=model.name%>Type = GraphQL::ObjectType.define do
2
+ name "<%=model.name%>"
3
+ description "Type for <%=model.name%> object"
4
+
5
+ interfaces [::GraphQL::Relay::Node.interface]
6
+
7
+ <%
8
+ attributes.each do |name, attribute|
9
+ association = nil
10
+
11
+ if attribute.key? :association
12
+ association = attribute[:association]
13
+ elsif model.respond_to? :reflect_on_all_associations
14
+
15
+ end
16
+
17
+ if model.respond_to? :primary_key and name.to_s == model.primary_key
18
+ -%>
19
+ global_id_field :<%=name%>
20
+ <%
21
+ elsif association.present?
22
+ if association.polymorphic?
23
+ object_model = association.active_record
24
+ else
25
+ object_model = association.klass
26
+ end
27
+
28
+ if association.collection?
29
+ list_or_connection = nil
30
+ while list_or_connection.nil?
31
+ STDOUT.puts "Should many type `\e[32m#{name}\e[0m` on \e[32m#{model.name}\e[0m be a list or connection? (\e[34ml = list \e[39m| \e[32mc = connection \e[39m| \e[93msh = help\e[39m)."
32
+ command = STDIN.gets.chomp.downcase
33
+ case command
34
+ when "c", "g"
35
+ list_or_connection = :connection
36
+ when "l"
37
+ list_or_connection = :list
38
+ when "h"
39
+ STDOUT.puts "Lists return all objects in the association. Use connections if pagination is necessary."
40
+ end
41
+ end
42
+
43
+ if list_or_connection == :connection
44
+ -%>
45
+ connection :<%=association.plural_name%>, <%=object_model.is_a?(model) or object_model == model ? "-> { " : ""%><%=object_model%>Type<%=association.polymorphic? ? "Union" : ""%>.connection_type<%=object_model.is_a?(model) or object_model == model ? " }" : ""%>, "Association to many `<%=association.plural_name%>` on <%=model.name%>"
46
+ <%
47
+ else
48
+ -%>
49
+ field :<%=association.plural_name%>, <%=object_model.is_a?(model) or object_model == model ? "-> { " : ""%><%=object_model%>Type<%=association.polymorphic? ? "Union" : ""%>.to_list_type<%=object_model.is_a?(model) or object_model == model ? " }" : ""%>, "Association to many `<%=association.plural_name%>` on <%=model.name%>"
50
+ <%
51
+ end
52
+ else
53
+ -%>
54
+ field :<%=association.name%>, <%=object_model.is_a?(model) or object_model == model ? "-> { " : ""%><%=object_model%>Type<%=association.polymorphic? ? "Union" : ""%><%=object_model.is_a?(model) or object_model == model ? " }" : ""%>, "Association to one `<%=association.name%>` on <%=model.name%>"
55
+ <%
56
+ end
57
+ else
58
+ type_str =
59
+ if attribute.key? :type and attribute[:type].present?
60
+ attribute[:type]
61
+ else
62
+ "types.String"
63
+ end
64
+
65
+ -%>
66
+ field :<%=name%>, <%=type_str%>, "Property `<%=attribute[:property]%>` for <%=model.name%>"<%=attribute[:property].to_s != name.to_s ? ", property: :#{attribute[:property]}" : ""%>
67
+ <%
68
+ end
69
+ end
70
+ %>
71
+ end
@@ -0,0 +1,13 @@
1
+ <%= query_type_name %> = GraphQL::ObjectType.define do
2
+ name "Query"
3
+ description "The query root of this schema"
4
+
5
+ field :<%=root_type_name.underscore%> do
6
+ type <%=root_type_name%>Type
7
+ resolve -> (obj, args, ctx) {
8
+ ctx[:<%=root_type_name.underscore%>]
9
+ }
10
+ end
11
+
12
+ field :node, ::GraphQL::Relay::Node.field
13
+ end
@@ -0,0 +1,11 @@
1
+ module Graphql
2
+ module Rails
3
+ module Schemaker
4
+ class Railtie < ::Rails::Railtie
5
+ rake_tasks do
6
+ load 'tasks/schemaker.rake'
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ Schema = GraphQL::Schema.define do
2
+ query <%= query_type_name %>
3
+ mutation <%= mutation_type_name %>
4
+
5
+ resolve_type -> (object, ctx) {
6
+ Schema.types[object.class.name]
7
+ }
8
+
9
+ object_from_id -> (id, ctx) {
10
+ type_name, object_id = GraphQL::Schema::UniqueWithinType.decode(id)
11
+
12
+ unless type_name.safe_constantize.present?
13
+ raise ArgumentError, "Type of object (#{type_name}) does not exist."
14
+ end
15
+
16
+ type_name.constantize.find(object_id)
17
+ }
18
+
19
+ id_from_object -> (obj, type_defn, ctx) {
20
+ GraphQL::Schema::UniqueWithinType.encode(type_defn.name, obj.id)
21
+ }
22
+
23
+ <%= middleware %>
24
+ end
@@ -0,0 +1,19 @@
1
+ require 'erb'
2
+
3
+ class TemplateRenderer
4
+ def self.empty_binding
5
+ binding
6
+ end
7
+
8
+ def self.render_string(template_content, locals = {})
9
+ b = empty_binding
10
+ locals.each { |k, v| b.local_variable_set(k, v) }
11
+
12
+ ERB.new(template_content, nil, '-').result(b)
13
+ end
14
+
15
+ def self.render(file, locals = {})
16
+ path = File.join Graphql::Rails::Schemaker.root, file
17
+ render_string(File.read(path), locals)
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ <%=polymorphic.name.to_s.camelize%>TypeUnion = GraphQL::UnionType.define do
2
+ name "<%=polymorphic.name.to_s.camelize%>"
3
+ description "Objects for `<%=polymorphic.active_record.name.to_s%>`"
4
+ possible_types [<%=associations.map(&:name).map { |n| "#{n.to_s.camelize}Type" }.join(", ")%>]
5
+ end
@@ -0,0 +1,7 @@
1
+ module Graphql
2
+ module Rails
3
+ module Schemaker
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,569 @@
1
+ require 'fileutils'
2
+ require 'set'
3
+ require 'graphql/rails/schemaker/template_renderer'
4
+
5
+ namespace :schemaker do
6
+ desc "Tasks for operating a GraphQL Schema Server"
7
+
8
+ task introspect: :environment do
9
+ viewer = SystemViewer.new
10
+
11
+ query_string = GraphQL::Introspection::INTROSPECTION_QUERY
12
+ result_hash = Schema.execute(query_string, context: {viewer: viewer})
13
+
14
+ File.open("./build/schema.json","w+") do |f|
15
+ f.write(result_hash.to_json)
16
+ end
17
+ end
18
+
19
+ task generate: :environment do
20
+ if File.exist? "./app/graph/schema.rb"
21
+ abort "\e[1mAnother schema already exists @ ./app/graph/schema.rb\e[0m"
22
+ end
23
+
24
+ # Ensure directories are available
25
+ #FileUtils::mkdir_p Rails.root.join("app", "graph", "mutations")
26
+ #FileUtils::mkdir_p Rails.root.join("app", "graph", "resolvers")
27
+ FileUtils::mkdir_p Rails.root.join("app", "graph", "types")
28
+
29
+ # Load our application
30
+ Rails.application.eager_load!
31
+ models = ApplicationRecord.descendants.collect { |type| type }
32
+ scalar_types = [:string, :integer, :float, :id, :boolean]
33
+
34
+ # Determine whether to do snake case or camel case
35
+ STDOUT.puts "\e[1mUse camelCase or snake_case? Enter (c/S).\e[0m"
36
+ name_format =
37
+ if STDIN.gets.chomp.downcase == 'c'
38
+ :camel
39
+ else
40
+ :snake
41
+ end
42
+
43
+ # Function to convert object types to string
44
+ object_type_to_string = lambda { |model, attributes|
45
+ TemplateRenderer.render("rails/schemaker/object_type.erb", { model: model, attributes: attributes, all_models: models })
46
+ }
47
+
48
+ # Function to convert enum types to string
49
+ enum_type_to_string = lambda { |name, values|
50
+ TemplateRenderer.render("rails/schemaker/enum_type.erb", { name: name, values: values })
51
+ }
52
+
53
+ # Function to write object types
54
+ put_object_type = lambda { |file, model, attributes|
55
+ File.open(file, 'w+') { |file| file.write(object_type_to_string.call(model, attributes)) }
56
+ }
57
+
58
+ # Function to write enum types
59
+ put_enum_type = lambda { |file, name, value|
60
+ File.open(file, 'w+') { |file| file.write(enum_type_to_string.call(name, value)) }
61
+ }
62
+
63
+ STDOUT.puts "Using #{name_format == :camel ? 'camel case' : 'snake case'}"
64
+
65
+ # Schema vars
66
+ query_type_name = "QueryType"
67
+ while ApplicationRecord.descendants.map(&:name).include? query_type_name
68
+ if query_type_name == "QueryType"
69
+ query_type_name = "QueryRootType"
70
+ elsif query_type_name == "QueryRootType"
71
+ query_type_name = "QueryRootObjectType"
72
+ elsif query_type_name == "QueryRootObjectType"
73
+ STDOUT.puts "Unable to formulate a query root type name - taken: QueryType, QueryRootType, QueryRootObjectType"
74
+ abort "Exiting..."
75
+ end
76
+ end
77
+
78
+ mutation_type_name = "MutationType"
79
+ while ApplicationRecord.descendants.map(&:name).include? mutation_type_name
80
+ if query_type_name == "MutationType"
81
+ query_type_name = "MutationRootType"
82
+ elsif query_type_name == "MutationRootType"
83
+ query_type_name = "MutationRootObjectType"
84
+ elsif query_type_name == "MutationRootObjectType"
85
+ STDOUT.puts "Unable to formulate a mutation root type name - taken: MutationType, MutationRootType, MutationRootObjectType"
86
+ abort "Exiting..."
87
+ end
88
+ end
89
+
90
+ # The Main Schema entry point
91
+ schema_rb_file = "./app/graph/schema.rb"
92
+ unless File.exist? schema_rb_file
93
+ STDOUT.puts "\e[1m\e[32mGenerating Schema Root...\e[0m"
94
+
95
+ middleware = name_format == :camel ? "middleware AuthorizationMiddleware.new" : ""
96
+
97
+ src = TemplateRenderer.render("rails/schemaker/schema.erb", { middleware: middleware, query_type_name: query_type_name, mutation_type_name: mutation_type_name })
98
+
99
+ File.open(schema_rb_file, 'w+') { |file| file.write(src) }
100
+ end
101
+
102
+ # Camel Case Middleware
103
+ if name_format == :camel
104
+ FileUtils::mkdir_p Rails.root.join("app", "graph", "middleware")
105
+
106
+ middleware_rb_file = "./app/graph/middleware/camel_case_middleware.rb"
107
+ unless File.exist? middleware_rb_file
108
+ src = TemplateRenderer.render("rails/schemaker/camel_case_middleware.erb")
109
+
110
+ File.open(middleware_rb_file, 'w+') { |file| file.write(src) }
111
+ end
112
+ end
113
+
114
+ fake_association = Class.new(Object) {
115
+ def initialize(model, plural_name:, macro: :has_many, polymorphic: false)
116
+ @model = model
117
+ @plural_name = plural_name || model.name.pluralize
118
+ @macro = macro
119
+ @polymorphic = polymorphic
120
+ end
121
+
122
+ def klass() @model end
123
+ def macro() @macro end
124
+ def plural_name() @plural_name end
125
+ def polymorphic?() @polymorphic end
126
+ def collection?() [:has_many, :has_and_belongs_to_many].include?(@macro) end
127
+ }
128
+
129
+ scalar_types = { :id => "types.ID", :boolean => "types.Boolean", :integer => "types.Int", :float => "types.Float", :decimal => "types.Float", :string => "types.String"}
130
+
131
+ guess_type = lambda { |model, name|
132
+ return :enum if model.defined_enums.key?(name)
133
+ matches = model.columns.select { |c| c.name == name }
134
+
135
+ return matches.first.type if matches.present?
136
+
137
+ :string
138
+ }
139
+
140
+ graphl_field_type = Proc.new { |type, name|
141
+ graphql_type = nil
142
+ graphql_type = scalar_types[type.to_sym] if scalar_types.key? type.to_sym
143
+
144
+ if type == :enum
145
+ graphql_type = "#{name.to_s.camelize}Enum"
146
+ end
147
+
148
+ graphql_type = scalar_types[:string] if type.nil?
149
+
150
+ graphql_type
151
+ }
152
+
153
+ STDOUT.puts ""
154
+ STDOUT.puts "\e[1m\e[32mGenerating Object Types...\e[0m"
155
+
156
+ active_models = []
157
+ active_enum = {}
158
+ active_models_attributes = {}
159
+ models.each do |model|
160
+ model.connection
161
+ skipped = false
162
+ STDOUT.puts
163
+ STDOUT.puts "----------------------------------"
164
+ STDOUT.puts "\e[1mGenerating type for model: \e[32m#{model}\e[0m"
165
+ attributes = Set.new
166
+ attribute_types = {}
167
+ attribute_graphql_types = {}
168
+ attribute_properties = {}
169
+ attribute_associations = {}
170
+
171
+ # Track all the columns
172
+ model.columns.each do |column|
173
+ name = column.name
174
+
175
+ type_sym =
176
+ if model.defined_enums.key? name
177
+ :enum
178
+ else
179
+ column.type
180
+ end
181
+
182
+ graphql_type = graphl_field_type.call(type_sym, name)
183
+
184
+ new_name = name_format == :camel ? name.camelize(:lower).to_sym : name.underscore.to_sym
185
+ attributes.add(new_name)
186
+ attribute_types[new_name] = type_sym
187
+ attribute_graphql_types[new_name] = graphql_type
188
+ attribute_properties[new_name] = name
189
+ end
190
+
191
+ # Track all associations
192
+ model.reflect_on_all_associations.each do |association|
193
+ name = association.collection? ? association.plural_name.to_s : association.name.to_s
194
+ type = :object
195
+
196
+ new_name = name_format == :camel ? name.camelize(:lower).to_sym : name.underscore.to_sym
197
+ attributes.add(new_name)
198
+ attribute_types[new_name] = type
199
+ attribute_graphql_types[new_name] = graphl_field_type.call(type, name)
200
+ attribute_properties[new_name] = name
201
+ attribute_associations[new_name] = association
202
+ end
203
+
204
+ # Process commands for each model
205
+ last_input = $_
206
+ is_repeating = false
207
+ while last_input != "g"
208
+ unless is_repeating
209
+ STDOUT.puts
210
+ STDOUT.puts "Using attributes: #{attributes.to_a.join(", ")}"
211
+ STDOUT.puts "To continue, enter one of the following commands: (\e[34mg = generate \e[39m| \e[31mr = remove attribute \e[39m| \e[32ma = add attribute \e[39m| \e[93ms = skip model\e[39m)"
212
+ command = STDIN.gets.chomp.downcase
213
+ end
214
+
215
+ is_repeating = false
216
+
217
+ case command
218
+ when "s"
219
+ skipped = true
220
+ break
221
+ when "a"
222
+ STDOUT.puts
223
+ STDOUT.puts "Enter attribute name and type in following format: \e[32m#{name_format == :snake ? "property_name" : "columName"}\e[39m:(\e[96m#{scalar_types.join("\e[39m|\e[96m")}\e[39m)"
224
+ raw_attr = STDIN.gets.chomp.downcase
225
+ new_attr = raw_attr
226
+
227
+ if name_format == :camel
228
+ new_attr = raw_attr.camelize(:lower)
229
+ else
230
+ new_attr = raw_attr.underscore
231
+ end
232
+
233
+ unless new_attr === raw_attr
234
+ STDOUT.puts "Converting to #{name_format == :camel ? 'camelCase' : 'snake_case'} - \"#{new_attr}\""
235
+ end
236
+
237
+ unless new_attr.present?
238
+ is_repeating = false
239
+ next
240
+ end
241
+
242
+ unless new_attr.include? ":"
243
+ last_input = "a"
244
+ is_repeating = true
245
+ next
246
+ end
247
+
248
+ name, type = new_attr.split(":")
249
+ property = name
250
+ tries = 0;
251
+
252
+ if attributes.include? name.to_sym
253
+ STDOUT.puts "Attribute #{name} already exists. Retrying..."
254
+ is_repeating = false
255
+ next
256
+ end
257
+
258
+ until model.column_names.include? property or model.respond_to? property.to_sym
259
+ if tries >= 3
260
+ STDOUT.puts "Tried three times. Retrying..."
261
+ break
262
+ end
263
+
264
+ STDOUT.puts "#{model} does not possess property \"#{property}\", what should \"#{name}\" respond with?"
265
+ input = STDIN.gets.chomp
266
+ property = input if input.present?
267
+
268
+ tries += 1
269
+ end
270
+
271
+ if property.nil?
272
+ is_repeating = true
273
+ next
274
+ end
275
+
276
+ unless name == property
277
+ attribute_properties[name] = property
278
+ end
279
+
280
+ attributes.add(name.to_sym)
281
+ attribute_types[name.to_sym] = type
282
+ attribute_graphql_types[name.to_sym] = graphl_field_type.call(type, name.to_sym)
283
+
284
+ when "r"
285
+ STDOUT.puts "Enter the name of the attribute to remove:"
286
+ raw_attr = STDIN.gets.chomp.downcase
287
+
288
+ if raw_attr.include? ":"
289
+ raw_attr = raw_attr.split(":").first
290
+ end
291
+
292
+ remove_attr = raw_attr
293
+
294
+ if name_format == :camel
295
+ remove_attr = raw_attr.camelize(:lower)
296
+ else
297
+ remove_attr = raw_attr.underscore
298
+ end
299
+
300
+ unless remove_attr === raw_attr
301
+ STDOUT.puts "Converting to #{name_format == :camel ? 'camelCase' : 'snake_case'} - \"#{remove_attr}\""
302
+ end
303
+
304
+ unless attributes.include? remove_attr.to_sym
305
+ STDOUT.puts "Attribute \"#{remove_attr}\" does not exist."
306
+ is_repeating = true
307
+ next
308
+ end
309
+
310
+ STDOUT.puts "Removing attribute \"#{remove_attr}\"."
311
+ attributes.delete(remove_attr.to_sym)
312
+ attribute_types.delete remove_attr.to_sym
313
+ attribute_graphql_types.delete remove_attr.to_sym
314
+ attribute_properties.delete remove_attr.to_sym
315
+
316
+ is_repeating = false
317
+ when "g"
318
+ break
319
+ else
320
+ STDOUT.puts "Unrecognized command #{command}"
321
+ end
322
+
323
+ end
324
+
325
+ if skipped
326
+ STDOUT.puts "\e[93mSkipping #{model.name}...\e[39m"
327
+ next
328
+ end
329
+
330
+ object_type_file = "./app/graph/types/#{model.name.underscore}_type.rb"
331
+ attr_composed = {}
332
+
333
+ attributes.each do |attribute|
334
+ hash = { :type => attribute_types[attribute], :graphql_type => attribute_graphql_types[attribute], :property => attribute_properties[attribute] }
335
+
336
+ if attribute_associations.key? attribute
337
+ hash[:association] = attribute_associations[attribute]
338
+ end
339
+
340
+ if model.defined_enums.key? hash[:property]
341
+ active_enum[attribute.to_s.camelize] = model.defined_enums[hash[:property]]
342
+ end
343
+
344
+ attr_composed[attribute] = hash
345
+ end
346
+
347
+ put_object_type.call(object_type_file, model, attr_composed.sort_by { |k,v| [k == :id ? 0 : 1, k] })
348
+
349
+ # Save this config for later
350
+ active_models.push(model)
351
+ active_models_attributes[model] = attr_composed
352
+
353
+ # Todo
354
+ # 2. Generate input types
355
+ # 4. Generate generic resolvers (if using graphql-rails-resolver)
356
+
357
+ end
358
+
359
+ puts "active_enum = #{active_enum}"
360
+ if active_enum.present?
361
+ STDOUT.puts ""
362
+ STDOUT.puts "----------------------------------"
363
+ STDOUT.puts ""
364
+ STDOUT.puts "\e[1m\e[32mGenerating Enum Types...\e[0m"
365
+
366
+ active_enum.each do |name, values|
367
+ enum_type_file = "./app/graph/types/#{name.underscore}_enum.rb"
368
+
369
+ STDOUT.puts "\e[34m#{name}Enum\e[0m"
370
+ put_enum_type.call(enum_type_file, name, values)
371
+ end
372
+ end
373
+
374
+ STDOUT.puts ""
375
+ STDOUT.puts "----------------------------------"
376
+ STDOUT.puts ""
377
+
378
+ # Generate union types from generated models
379
+ polymorphics = active_models.map { |m| m.reflect_on_all_associations.select(&:polymorphic?) }.flatten
380
+ if polymorphics.present?
381
+ STDOUT.puts "\e[1m\e[32mGenerating Union Types...\e[0m"
382
+ polymorphics.each do |polymorphic|
383
+
384
+ STDOUT.puts "\e[34m#{polymorphic.name.to_s.camelize}Type\e[0m"
385
+
386
+ polymorphic_rb_file = "./app/graph/types/#{polymorphic.name}_union.rb"
387
+ associations = active_models.select { |m| m.reflect_on_all_associations.select{ |j| j.options[:as] == polymorphic.name }.present? }
388
+
389
+ src = TemplateRenderer.render("rails/schemaker/union_type.erb", { polymorphic: polymorphic, associations: associations })
390
+
391
+ File.open(polymorphic_rb_file, 'w+') { |file| file.write(src) }
392
+ end
393
+ end
394
+
395
+ STDOUT.puts ""
396
+ STDOUT.puts "----------------------------------"
397
+ STDOUT.puts ""
398
+
399
+ STDOUT.puts "\e[1m\e[32mGenerating Query Root...\e[0m"
400
+ STDOUT.puts "A query root is the entry point to your Schema."
401
+ STDOUT.puts ""
402
+ STDOUT.puts "If you plan to use Relay v1, your Schema needs a global node to work properly."
403
+ STDOUT.puts "See https://github.com/facebook/relay/issues/112 for more info."
404
+
405
+ STDOUT.puts ""
406
+ STDOUT.puts "----------------------------------"
407
+ STDOUT.puts ""
408
+
409
+ STDOUT.puts "How would you like to generate your query root?"
410
+ STDOUT.puts "1 - Use Global Node (default)"
411
+ STDOUT.puts "2 - Expose all fields on query root"
412
+ STDOUT.puts ""
413
+
414
+ command = STDIN.gets.chomp.downcase
415
+ until ["1", "2", "", "g"].include? command
416
+ STDOUT.puts "\"#{command}\" not recognized."
417
+ command = STDIN.gets.chomp.downcase
418
+ end
419
+
420
+ if ["", "g"].include? command
421
+ command = "1"
422
+ end
423
+
424
+ # Generate global node for query root
425
+ if command == "1"
426
+ root_type_name = nil
427
+
428
+ # Check if developers already made a global node model
429
+ if active_models.map(&:name).include? "Viewer"
430
+ STDOUT.puts "A model by the name 'Viewer' already exists. Should this model be the global node? (Y/n)"
431
+
432
+ command = STDIN.gets.chomp.downcase
433
+ until ["y", "n", "", "g"].include?(command)
434
+ STDOUT.puts "\"#{command}\" not recognized."
435
+ command = STDIN.gets.chomp.downcase
436
+ end
437
+
438
+ if ["", "g"].include? command
439
+ command = "y"
440
+ end
441
+
442
+ root_type_name = "Viewer"
443
+
444
+ # Reconfigure the file
445
+ model = active_models.select { |m| m.name == "Viewer" }.first
446
+ attributes = active_models_attributes[model]
447
+
448
+ root_type_attr = {}
449
+
450
+ active_models.map do |model|
451
+ key = model.name.pluralize
452
+
453
+ if name_format == :camel
454
+ key = key.camelize(:lower)
455
+ else
456
+ key = key.underscore
457
+ end
458
+
459
+ while key.nil? or attributes.key? key or attributes.key? key.to_sym
460
+ STDOUT.puts "Field `\e[31m#{key}\e[0m` already exists on `\e[32m#{root_type_name}\e[0m`. Enter a new name for the field:"
461
+ command = STDIN.gets.chomp
462
+ unless command.present? and command[/[a-zA-Z]+/] == command
463
+ command = nil
464
+ next
465
+ end
466
+
467
+ key = command
468
+ end
469
+
470
+ if name_format == :camel
471
+ key = key.camelize(:lower)
472
+ else
473
+ key = key.underscore
474
+ end
475
+
476
+ association = fake_association.new(model, plural_name: key)
477
+ association.polymorphic?
478
+
479
+ root_type_attr[key] = { :type => :id, :association => association}
480
+ end
481
+
482
+ object_type_file = "./app/graph/types/#{model.name.underscore}_type.rb"
483
+ sorted_fields = Hash[root_type_attr.sort_by{ |k,v| k }]
484
+ put_object_type.call(object_type_file, model, attributes.merge(sorted_fields))
485
+ else
486
+ STDOUT.puts "Define the root type to expose your object types: (letters only) (default = \e[32mViewer\e[0m)"
487
+ name_command = STDIN.gets.chomp.downcase
488
+ while root_type_name.nil?
489
+ if name_command.empty?
490
+ root_type_name = "Viewer"
491
+ else
492
+ unless name_command[/[a-zA-Z]+/] == name_command
493
+ STDOUT.puts "Root type name must contain only letters. No numbers or special characters."
494
+ next
495
+ end
496
+
497
+ if models.map(&:name).include?(name_command)
498
+ STDOUT.puts "A model already exists by that name. Are you sure? (y/N)"
499
+ command = STDIN.gets.chomp.downcase
500
+ until ["y", "n", ""].include?(command)
501
+ STDOUT.puts "\"#{command}\" not recognized."
502
+ command = STDIN.gets.chomp.downcase
503
+ end
504
+
505
+ if command == ""
506
+ command = "n"
507
+ end
508
+
509
+ if command == "n"
510
+ next
511
+ end
512
+ end
513
+
514
+ root_type_name = name_command.classify
515
+ end
516
+ end
517
+
518
+ # Generate global node type with generated models
519
+ Object.const_set(root_type_name, Class.new { def name() root_type_name end })
520
+
521
+ root_type_attr = {}
522
+ active_models.each do |model|
523
+ key = model.name.pluralize
524
+
525
+ if name_format == :camel
526
+ key = key.camelize(:lower)
527
+ else
528
+ key = key.underscore
529
+ end
530
+
531
+ association = fake_association.new(model, plural_name: key)
532
+ root_type_attr[key] = { :type => :id, :association => association}
533
+ end
534
+
535
+ root_object_type_file = "./app/graph/types/#{root_type_name.underscore}_type.rb"
536
+ put_object_type.call(root_object_type_file, root_type_name.constantize, root_type_attr)
537
+
538
+ end
539
+ end
540
+
541
+ # Generate the Query root type (include)
542
+ query_type_rb_file = "./app/graph/types/#{query_type_name.underscore}.rb"
543
+ unless File.exist? query_type_rb_file
544
+ STDOUT.puts "Generating Query Root Type..."
545
+
546
+ src = TemplateRenderer.render("rails/schemaker/query_type.erb", { query_type_name: query_type_name, root_type_name: root_type_name })
547
+
548
+ File.open(query_type_rb_file, 'w+') { |file| file.write(src) }
549
+ end
550
+
551
+
552
+ # Generate the Mutation root type (include)
553
+ # TODO - Add Mutations
554
+ mutation_type_rb_file = "./app/graph/types/#{mutation_type_name.underscore}.rb"
555
+ unless File.exist? mutation_type_rb_file
556
+ STDOUT.puts "Generating Mutation Root Type..."
557
+
558
+ src = TemplateRenderer.render("rails/schemaker/mutation_type.erb", { mutation_type_name: mutation_type_name })
559
+
560
+ File.open(mutation_type_rb_file, 'w+') { |file| file.write(src) }
561
+ end
562
+
563
+
564
+ STDOUT.puts "\e[32mDone...\e[0m"
565
+
566
+
567
+ end
568
+
569
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphql-rails-schemaker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Cole Turner
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-10-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: graphql
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.19.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.19.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.13'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ description:
56
+ email:
57
+ - turner.cole@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - CODE_OF_CONDUCT.md
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - bin/console
69
+ - bin/setup
70
+ - graphql-rails-schemaker.gemspec
71
+ - lib/.DS_Store
72
+ - lib/graphql/rails/.DS_Store
73
+ - lib/graphql/rails/schemaker.rb
74
+ - lib/graphql/rails/schemaker/camel_case_middleware.erb
75
+ - lib/graphql/rails/schemaker/enum_type.erb
76
+ - lib/graphql/rails/schemaker/mutation_type.erb
77
+ - lib/graphql/rails/schemaker/object_type.erb
78
+ - lib/graphql/rails/schemaker/query_type.erb
79
+ - lib/graphql/rails/schemaker/railtie.rb
80
+ - lib/graphql/rails/schemaker/schema.erb
81
+ - lib/graphql/rails/schemaker/template_renderer.rb
82
+ - lib/graphql/rails/schemaker/union_type.erb
83
+ - lib/graphql/rails/schemaker/version.rb
84
+ - lib/tasks/schemaker.rake
85
+ homepage: https://github.com/colepatrickturner/graphql-rails-schemaker
86
+ licenses:
87
+ - MIT
88
+ metadata: {}
89
+ post_install_message:
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: 2.2.2
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubyforge_project:
105
+ rubygems_version: 2.5.1
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: ''
109
+ test_files: []