wcc-contentful-graphql 1.0.0.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b2546f3684d711b927716615568c5a1d90994bd3242a91f056be643b0e9a8976
4
+ data.tar.gz: a396b86a4fe13e61b031b73c3f55889dfeaf0cc0406bcf7413d00159daf7cd8d
5
+ SHA512:
6
+ metadata.gz: 8415a779ccb2d1997531d741b49e2b57a1d37f27f4283102047b8152ed7a5f2b234ef54661b2642d47ee1ae4f2b2c054698d71e225ae237815879cb55aa17c45
7
+ data.tar.gz: fe9ad8a75a6be28f8b2eb996601741238dc5a9df0f314555c069a330bf79139214a70c21d041ae069dc4a2411a69cd496fae21520aba07f8fde3894a3b1f659e
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
4
+ --order rand
@@ -0,0 +1,101 @@
1
+ # A guardfile for making Danger Plugins
2
+ # For more info see https://github.com/guard/guard#readme
3
+
4
+ # To run, use `bundle exec guard`.
5
+
6
+ def watch_async(regexp)
7
+ raise ArgumentError, "No block given" unless block_given?
8
+ match_queue = Queue.new
9
+
10
+ watch(regexp) do |match|
11
+ # Producer - add matches to the match queue
12
+ match_queue << match
13
+ nil
14
+ end
15
+
16
+ # Consumer - process matches as a batch
17
+ Thread.new do
18
+ loop do
19
+ matches = []
20
+ matches << match_queue.pop
21
+
22
+ loop do
23
+ begin
24
+ matches << match_queue.pop(true)
25
+ rescue ThreadError
26
+ break
27
+ end
28
+ end
29
+
30
+ begin
31
+ yield matches if matches.length > 0
32
+ rescue StandardError => ex
33
+ STDERR.puts "Error! #{ex}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ group :red_green_refactor, halt_on_fail: true do
40
+ guard :rspec, cmd: 'bundle exec rspec' do
41
+ require 'guard/rspec/dsl'
42
+ dsl = Guard::RSpec::Dsl.new(self)
43
+
44
+ # RSpec files
45
+ rspec = dsl.rspec
46
+ watch(rspec.spec_helper) { rspec.spec_dir }
47
+ # watch(rspec.spec_support) { rspec.spec_dir }
48
+ watch(rspec.spec_files)
49
+
50
+ # Ruby files
51
+ ruby = dsl.ruby
52
+ watch(%r{lib/wcc/(.+)\.rb$}) { |m| rspec.spec.call("wcc/#{m[1]}") }
53
+ watch(%r{lib/generators/(.+)\.rb$}) { |m| rspec.spec.call("generators/#{m[1]}") }
54
+
55
+ # Rails files
56
+ rails = dsl.rails(view_extensions: %w[erb haml slim])
57
+ dsl.watch_spec_files_for(rails.app_files)
58
+ dsl.watch_spec_files_for(rails.views)
59
+
60
+ watch(rails.controllers) do |m|
61
+ [
62
+ rspec.spec.call("routing/#{m[1]}_routing"),
63
+ rspec.spec.call("controllers/#{m[1]}_controller"),
64
+ rspec.spec.call("acceptance/#{m[1]}")
65
+ ]
66
+ end
67
+
68
+ # Rails config changes
69
+ watch(rails.spec_helper) { rspec.spec_dir }
70
+ watch(rails.routes) { "#{rspec.spec_dir}/routing" }
71
+ watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
72
+
73
+ # Capybara features specs
74
+ watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
75
+ watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
76
+ end
77
+
78
+ guard :rubocop, cli: ['--display-cop-names'] do
79
+ watch(%r{.+\.rb$})
80
+ watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
81
+ end
82
+
83
+ guard :shell, all_on_start: false do
84
+ watch_async(%r{app/views/(.+\.html.*\.erb)}) { |matches|
85
+
86
+ matches = matches.map { |m| File.absolute_path(m[0]) }
87
+ Dir.chdir('..') {
88
+ system("bundle exec erblint #{matches.join(' ')}")
89
+ }
90
+ }
91
+ end
92
+ end
93
+
94
+ group :autofix do
95
+ guard :rubocop, all_on_start: false, cli: ['--auto-correct', '--display-cop-names'] do
96
+ watch(%r{.+\.rb$})
97
+ watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
98
+ end
99
+ end
100
+
101
+ scope group: :red_green_refactor
@@ -0,0 +1,128 @@
1
+ [![Gem Version](https://badge.fury.io/rb/wcc-contentful-graphql.svg)](https://rubygems.org/gems/wcc-contentful-graphql)
2
+ [![Build Status](https://travis-ci.org/watermarkchurch/wcc-contentful.svg?branch=master)](https://travis-ci.org/watermarkchurch/wcc-contentful)
3
+ [![Coverage Status](https://coveralls.io/repos/github/watermarkchurch/wcc-contentful/badge.svg?branch=master)](https://coveralls.io/github/watermarkchurch/wcc-contentful?branch=master)
4
+
5
+ # WCC::Contentful::Graphql
6
+
7
+ This gem creates a GraphQL schema over your configured [data store](https://www.rubydoc.info/gems/wcc-contentful#Store_API).
8
+ You can execute queries against this GraphQL schema to get all your contentful
9
+ data. Under the hood, queries are executed against your backing store to
10
+ resolve all the requested data.
11
+
12
+ ### Important note!
13
+ The GraphQL schema currently does not utilize the "include" parameter, so it is
14
+ a very good idea to configure your store to either `:direct`
15
+ or `:lazy_sync`. If you don't do this, you will see a lot of requests to
16
+ Contentful for specific entries by ID as the GraphQL resolver walks all your links!
17
+
18
+ [More info on configuration can be found here](https://www.rubydoc.info/gems/wcc-contentful/WCC%2FContentful%2FConfiguration:store=)
19
+
20
+ ## Usage
21
+
22
+ Querying directly within your app
23
+ ```rb
24
+ schema = WCC::Contentful::Services.instance.graphql_schema
25
+ => #<GraphQL::Schema ...>
26
+
27
+ result = schema.execute(<<~QUERY)
28
+ {
29
+ allConference(filter: { code: { eq: "CLC2020" } }) {
30
+ title
31
+ startDate
32
+ code
33
+ }
34
+ }
35
+ QUERY
36
+ GET https://cdn.contentful.com/spaces/xxxxx/entries?content_type=conference&fields.code.en-US=CLC2020&locale=%2A
37
+ Status 200
38
+ => #<GraphQL::Query::Result @query=... @to_h={"data"=>{"allConference"=>[{"title"=>"Church Leaders Conference", "startDate"=>"2020-04-28", "code"=>"CLC2020"}]}}>
39
+ result.to_h
40
+ => {"data"=>
41
+ {"allConference"=>
42
+ [{"title"=>"Church Leaders Conference",
43
+ "startDate"=>"2020-04-28",
44
+ "code"=>"CLC2020"}]}}
45
+ ```
46
+
47
+ Setting up a controller to respond to GraphQL queries
48
+
49
+ ```rb
50
+ class Api::GraphqlController < Api::BaseController
51
+ include WCC::Contentful::ServiceAccessors
52
+
53
+ skip_before_action :authenticate_user!, only: :query
54
+
55
+ def query
56
+ result = graphql_schema.execute(
57
+ params[:query],
58
+ variables: params[:variables]
59
+ )
60
+ render json: result
61
+ end
62
+ end
63
+ ```
64
+
65
+ ## Advanced Configuration
66
+
67
+ ### Including your Contentful schema inside another GraphQL schema
68
+
69
+ ```rb
70
+ QueryType = GraphQL::ObjectType.define do
71
+ # extend this to get 'schema_stitch'
72
+ extend WCC::Contentful::Graphql::Federation
73
+
74
+ name 'RootQuery'
75
+
76
+ field 'a', types.String
77
+
78
+ schema_stitch(WCC::Contentful::Services.instance.graphql_schema,
79
+ namespace: 'contentful')
80
+ end
81
+
82
+ Schema = GraphQL::Schema.define do
83
+ query QueryType
84
+
85
+ resolve_type ->(type, obj, ctx) {
86
+ raise StandardError, "Cannot resolve type #{type} #{obj.inspect} #{ctx.inspect}"
87
+ }
88
+ end
89
+
90
+ File.write('schema.gql', GraphQL::Schema::Printer.print_schema(Schema))
91
+ ```
92
+ results in...
93
+ ```gql
94
+ schema {
95
+ query: RootQuery
96
+ }
97
+
98
+ type Contentful {
99
+ """
100
+ Find a Asset
101
+ """
102
+ Asset(_content_type: Contentful_StringQueryOperatorInput, description: Contentful_StringQueryOperatorInput, id: ID, title: Contentful_StringQueryOperatorInput): Contentful_Asset
103
+
104
+ """
105
+ Find a CallToAction
106
+ """
107
+ CallToAction(_content_type: Contentful_StringQueryOperatorInput, id: ID, internalTitle: Contentful_StringQueryOperatorInput, style: Contentful_StringQueryOperatorInput, text: Contentful_StringQueryOperatorInput, title: Contentful_StringQueryOperatorInput): Contentful_CallToAction
108
+ ...
109
+ ```
110
+
111
+ ### Limiting the schema to only a few fields
112
+
113
+ ```rb
114
+ store = WCC::Contentful::Services.instance.store
115
+ builder =
116
+ WCC::Contentful::Graphql::Builder.new(
117
+ WCC::Contentful.types,
118
+ store,
119
+ ).configure do
120
+ root_types.slice!('conference')
121
+
122
+ schema_types['conference'].define do
123
+ # change the types of some fields, undefine fields, etc...
124
+ end
125
+ end
126
+
127
+ @schema = builder.build_schema
128
+ ```
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::Contentful
4
+ class Services
5
+ # A GraphQL schema that will query Contentful using your configured store.
6
+ #
7
+ # @api Store
8
+ def graphql_schema
9
+ @graphql_schema ||=
10
+ ensure_configured do |_config|
11
+ WCC::Contentful::Graphql::Builder.new(
12
+ WCC::Contentful.types,
13
+ store
14
+ ).build_schema
15
+ end
16
+ end
17
+ end
18
+
19
+ module ServiceAccessors
20
+ def graphql_schema
21
+ Services.instance.graphql_schema
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem 'graphql', '~> 1.7'
4
+ require 'graphql'
5
+
6
+ module WCC::Contentful
7
+ # This module builds a GraphQL schema out of our IndexedRepresentation.
8
+ # It is currently unused and not hooked up in the WCC::Contentful.init! method.
9
+ # TODO: https://github.com/watermarkchurch/wcc-contentful/issues/14 hook it up
10
+ module Graphql
11
+ end
12
+ end
13
+
14
+ require_relative 'graphql/builder'
15
+ require_relative 'graphql/federation'
16
+ require_relative 'ext/services'
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql'
4
+
5
+ require_relative 'types'
6
+ require_relative 'field_helper'
7
+
8
+ GraphQL::Define::DefinedObjectProxy.__send__(:include, WCC::Contentful::Graphql::FieldHelper)
9
+
10
+ module WCC::Contentful::Graphql
11
+ class Builder
12
+ attr_reader :schema_types
13
+ attr_reader :root_types
14
+
15
+ def initialize(types, store)
16
+ @types = types if types.is_a? WCC::Contentful::IndexedRepresentation
17
+ @types ||=
18
+ if types.is_a?(String) && File.exist?(types)
19
+ WCC::Contentful::ContentTypeIndexer.load(types).types
20
+ end
21
+
22
+ unless @types
23
+ raise ArgumentError, 'Cannot parse types - not an IndexedRepresentation ' \
24
+ "nor a schema file on disk: #{types}"
25
+ end
26
+
27
+ @store = store
28
+
29
+ @schema_types = build_schema_types
30
+ @root_types = @schema_types.dup
31
+ end
32
+
33
+ def configure(&block)
34
+ instance_exec(&block)
35
+ self
36
+ end
37
+
38
+ def build_schema
39
+ root_query_type = build_root_query(root_types)
40
+
41
+ builder = self
42
+ GraphQL::Schema.define do
43
+ query root_query_type
44
+
45
+ resolve_type ->(_type, obj, _ctx) {
46
+ content_type = WCC::Contentful::Helpers.content_type_from_raw(obj)
47
+ builder.schema_types[content_type]
48
+ }
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def build_root_query(schema_types)
55
+ store = @store
56
+
57
+ GraphQL::ObjectType.define do
58
+ name 'Query'
59
+ description 'The query root of this schema'
60
+
61
+ schema_types.each do |content_type, schema_type|
62
+ field schema_type.name.to_sym do
63
+ type schema_type
64
+ argument :id, types.ID
65
+ description "Find a #{schema_type.name}"
66
+
67
+ schema_type.fields.each do |(name, field)|
68
+ next unless input_type = Types::QueryOperatorInput.call(field.type)
69
+
70
+ argument name, input_type
71
+ end
72
+
73
+ resolve ->(_obj, args, _ctx) {
74
+ if args['id'].nil?
75
+ store.find_by(content_type: content_type, filter: args.to_h)
76
+ else
77
+ store.find(args['id'])
78
+ end
79
+ }
80
+ end
81
+
82
+ field "all#{schema_type.name}".to_sym do
83
+ type schema_type.to_list_type
84
+ argument :filter, Types::FilterInputType.call(schema_type)
85
+
86
+ resolve ->(_obj, args, ctx) {
87
+ relation = store.find_all(content_type: content_type)
88
+ relation = relation.apply(args[:filter].to_h, ctx) if args[:filter]
89
+ relation.to_enum
90
+ }
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ def build_schema_types
97
+ @types.each_with_object({}) do |(k, v), h|
98
+ h[k] = build_schema_type(v)
99
+ end
100
+ end
101
+
102
+ def build_schema_type(typedef)
103
+ store = @store
104
+ builder = self
105
+ content_type = typedef.content_type
106
+
107
+ GraphQL::ObjectType.define do
108
+ name(typedef.name)
109
+
110
+ description("Generated from content type #{content_type}")
111
+
112
+ field :id, !types.ID do
113
+ resolve ->(obj, _args, _ctx) {
114
+ obj.dig('sys', 'id')
115
+ }
116
+ end
117
+
118
+ field :_content_type, !types.String do
119
+ resolve ->(_, _, _) {
120
+ content_type
121
+ }
122
+ end
123
+
124
+ # Make a field for each column:
125
+ typedef.fields.each_value do |f|
126
+ case f.type
127
+ when :Asset
128
+ field(f.name.to_sym, -> {
129
+ type = builder.schema_types['Asset']
130
+ type = type.to_list_type if f.array
131
+ type
132
+ }) do
133
+ resolve contentful_link_resolver(f.name, store: store)
134
+ end
135
+ when :Link
136
+ field(f.name.to_sym, -> {
137
+ type =
138
+ if f.link_types.nil? || f.link_types.empty?
139
+ builder.schema_types['AnyContentful'] ||=
140
+ Types::BuildUnionType.call(builder.schema_types, 'AnyContentful')
141
+ elsif f.link_types.length == 1
142
+ builder.schema_types[f.link_types.first]
143
+ else
144
+ from_types = builder.schema_types.select { |key| f.link_types.include?(key) }
145
+ name = "#{typedef.name}_#{f.name}"
146
+ builder.schema_types[name] ||= Types::BuildUnionType.call(from_types, name)
147
+ end
148
+ type = type.to_list_type if f.array
149
+ type
150
+ }) do
151
+ resolve contentful_link_resolver(f.name, store: store)
152
+ end
153
+ else
154
+ contentful_field(f.name, f.type, array: f.array)
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extend this module inside a root query definition to do schema federation.
4
+ # https://blog.apollographql.com/apollo-federation-f260cf525d21
5
+ #
6
+ # This handles only queries, not mutations or subscriptions.
7
+ module WCC::Contentful::Graphql::Federation
8
+ extend self
9
+
10
+ # Accepts an externally defined schema with a root query, and "stitches" it's
11
+ # query root into the current GraphQL::ObjectType definition.
12
+ # All fields on the external query object like `resource()`, `allResource()`
13
+ # will be inserted into the current object. The `resolve` method for those
14
+ # fields will execute a query on the external schema, returning the results.
15
+ def schema_stitch(schema, namespace: nil)
16
+ ns_titleized = namespace&.titleize
17
+ ns = NamespacesTypes.new(namespace: ns_titleized)
18
+
19
+ def_fields =
20
+ proc {
21
+ schema.query.fields.each do |(key, field_def)|
22
+ field key, ns.namespaced(field_def.type) do
23
+ description field_def.description
24
+
25
+ field_def.arguments.each do |(arg_name, arg)|
26
+ argument arg_name, ns.namespaced(arg.type)
27
+ end
28
+
29
+ resolve delegate_to_schema(schema)
30
+ end
31
+ end
32
+ }
33
+
34
+ if namespace
35
+ stub_class = Struct.new(:name)
36
+ namespaced_type =
37
+ GraphQL::ObjectType.define do
38
+ name ns_titleized
39
+
40
+ instance_exec(&def_fields)
41
+ end
42
+
43
+ field namespace, namespaced_type do
44
+ resolve ->(_obj, _arguments, _context) { stub_class.new(namespace) }
45
+ end
46
+ else
47
+ def_fields.call
48
+ end
49
+ end
50
+
51
+ def delegate_to_schema(schema, field_name: nil, arguments: nil)
52
+ ->(obj, inner_args, context) {
53
+ field_name ||= context.ast_node.name
54
+
55
+ arguments = arguments.call(obj, inner_args, context) if arguments&.respond_to?(:call)
56
+ arguments = BuildsArguments.call(arguments) if arguments
57
+ arguments ||= context.ast_node.arguments
58
+
59
+ field_node = GraphQL::Language::Nodes::Field.new(
60
+ name: field_name,
61
+ arguments: arguments,
62
+ selections: context.ast_node.selections,
63
+ directives: context.ast_node.directives
64
+ )
65
+
66
+ query_node = GraphQL::Language::Nodes::OperationDefinition.new(
67
+ name: context.query.operation_name,
68
+ operation_type: 'query',
69
+ variables: context.query.selected_operation.variables,
70
+ selections: [
71
+ field_node
72
+ ]
73
+ )
74
+
75
+ # the ast_node.to_query_string prints the relevant section of the query to
76
+ # a string. We build a query out of that which we execute on the external
77
+ # schema.
78
+ query = query_node.to_query_string
79
+
80
+ result = schema.execute(query,
81
+ variables: context.query.variables)
82
+
83
+ if result['errors'].present?
84
+ raise GraphQL::ExecutionError.new(
85
+ result.dig('errors', 0, 'message'),
86
+ ast_node: context.ast_node
87
+ )
88
+ end
89
+
90
+ result.dig('data', field_name)
91
+ }
92
+ end
93
+ end
94
+
95
+ GraphQL::Define::DefinedObjectProxy.__send__(:include, WCC::Contentful::Graphql::Federation)
96
+
97
+ require_relative './federation/namespaces_types'
98
+ require_relative './federation/builds_arguments'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::Contentful::Graphql::Federation
4
+ BuildsArguments =
5
+ Struct.new(:argument) do
6
+ def self.call(arguments)
7
+ arguments.map { |arg| new(arg).call }
8
+ end
9
+
10
+ def call
11
+ return argument if argument.is_a? GraphQL::Language::Nodes::Argument
12
+
13
+ GraphQL::Language::Nodes::Argument.new(name: key, value: value)
14
+ end
15
+
16
+ private
17
+
18
+ def key
19
+ argument[0]
20
+ end
21
+
22
+ def value
23
+ if argument[1].is_a? Hash
24
+ return GraphQL::Language::Nodes::InputObject.new(arguments: BuildsArguments.call(argument[1]))
25
+ end
26
+
27
+ argument[1]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This GraphQL type definition wraps a type definition from an external schema
4
+ # and redefines it in our top-level schema, so that names do not clash.
5
+ # ex. "Campus" in the events schema becomes "Event_Campus"
6
+ class WCC::Contentful::Graphql::Federation::NamespacesTypes
7
+ class << self
8
+ def registry
9
+ @registry ||= {}
10
+ end
11
+ end
12
+
13
+ attr_reader :namespace
14
+
15
+ def initialize(namespace:)
16
+ @namespace = namespace
17
+ end
18
+
19
+ # Gets the graphql type definition for the externally resolved field
20
+ def namespaced(type)
21
+ return type if type.default_scalar?
22
+ return namespaced(type.of_type).to_list_type if type.is_a?(GraphQL::ListType)
23
+ return namespaced(type.of_type).to_non_null_type if type.is_a?(GraphQL::NonNullType)
24
+
25
+ me = self
26
+ ns = namespace
27
+ typename = [namespace, type.to_s].compact.join('_')
28
+ self.class.registry[typename] ||=
29
+ if type.is_a?(GraphQL::UnionType)
30
+ possible_types =
31
+ type.possible_types.map { |t| me.namespaced(t) }
32
+ GraphQL::UnionType.define do
33
+ name typename
34
+ possible_types possible_types
35
+ end
36
+ elsif type.is_a?(GraphQL::InputObjectType)
37
+ GraphQL::InputObjectType.define do
38
+ name typename
39
+ type.arguments.each do |(name, arg)|
40
+ argument name, me.namespaced(arg.type)
41
+ end
42
+ end
43
+ elsif type.is_a?(GraphQL::ScalarType)
44
+ GraphQL::ScalarType.define do
45
+ name typename
46
+
47
+ coerce_input type.method(:coerce_input)
48
+ coerce_result type.method(:coerce_result)
49
+ end
50
+ elsif type.is_a?(GraphQL::ObjectType)
51
+ GraphQL::ObjectType.define do
52
+ name typename
53
+ description "#{type.name} from remote#{ns ? ' ' + ns : ''}"
54
+
55
+ type.fields.each do |(name, field_def)|
56
+ field name, me.namespaced(field_def.type) do
57
+ field_def.arguments.each do |(arg_name, arg)|
58
+ argument arg_name, me.namespaced(arg.type)
59
+ end
60
+
61
+ resolve ->(obj, _args, _ctx) do
62
+ # The object is a JSON response that came back from the
63
+ # external schema. Resolve the value by using the hash key.
64
+ obj[name]
65
+ end
66
+ end
67
+ end
68
+ end
69
+ else
70
+ raise ArgumentError, "Cannot namespace type #{type} (#{type.class})"
71
+ end
72
+ end
73
+ # rubocop:enable
74
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::Contentful::Graphql::FieldHelper
4
+ extend self
5
+
6
+ def contentful_field_resolver(field_name)
7
+ field_name = field_name.to_s
8
+
9
+ ->(obj, _args, ctx) {
10
+ if obj.is_a? Array
11
+ obj.map { |o| o.dig('fields', field_name, ctx[:locale] || 'en-US') }
12
+ else
13
+ obj.dig('fields', field_name, ctx[:locale] || 'en-US')
14
+ end
15
+ }
16
+ end
17
+
18
+ def contentful_field(field_name, type, array: false, &block)
19
+ field_name = field_name.to_s
20
+
21
+ type =
22
+ case type
23
+ when :DateTime
24
+ types.String
25
+ when :Coordinates
26
+ WCC::Contentful::Graphql::Types::CoordinatesType
27
+ when :Json
28
+ WCC::Contentful::Graphql::Types::HashType
29
+ else
30
+ if type.is_a?(Symbol) || type.is_a?(String)
31
+ types.public_send(type)
32
+ elsif type.is_a?(GraphQL::BaseType)
33
+ type
34
+ else
35
+ raise ArgumentError, "Unknown type arg '#{type}' for field #{field_name}"
36
+ end
37
+ end
38
+ type = type.to_list_type if array
39
+ field(field_name.to_sym, type) do
40
+ resolve contentful_field_resolver(field_name)
41
+
42
+ instance_exec(&block) if block_given?
43
+ end
44
+ end
45
+
46
+ def contentful_link_resolver(field_name, store:)
47
+ ->(obj, _args, ctx) {
48
+ links = obj.dig('fields', field_name, ctx[:locale] || 'en-US')
49
+ return if links.nil?
50
+
51
+ if links.is_a? Array
52
+ links.reject(&:nil?).map { |l| store.find(l.dig('sys', 'id')) }
53
+ else
54
+ store.find(links.dig('sys', 'id'))
55
+ end
56
+ }
57
+ end
58
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::Contentful::Graphql::Types
4
+ DateTimeType =
5
+ GraphQL::ScalarType.define do
6
+ name 'DateTime'
7
+
8
+ coerce_result ->(value, _ctx) { Time.zone.parse(value) }
9
+ end
10
+
11
+ HashType =
12
+ GraphQL::ScalarType.define do
13
+ name 'Hash'
14
+
15
+ coerce_result ->(value, _ctx) {
16
+ return value if value.is_a? Array
17
+ return value.to_h if value.respond_to?(:to_h)
18
+ return JSON.parse(value) if value.is_a? String
19
+
20
+ raise ArgumentError, "Cannot coerce value '#{value}' to a hash"
21
+ }
22
+ end
23
+
24
+ CoordinatesType =
25
+ GraphQL::ObjectType.define do
26
+ name 'Coordinates'
27
+
28
+ field :lat, !types.Float, hash_key: 'lat'
29
+ field :lon, !types.Float, hash_key: 'lon'
30
+ end
31
+
32
+ StringQueryOperatorInput =
33
+ GraphQL::InputObjectType.define do
34
+ name 'StringQueryOperatorInput'
35
+
36
+ argument :eq, types.String
37
+ end
38
+
39
+ QueryOperatorInput =
40
+ ->(type) do
41
+ map = {
42
+ 'String' => StringQueryOperatorInput
43
+ # 'Int' =>
44
+ # 'Boolean' =>
45
+ }
46
+
47
+ map[type.unwrap.name]
48
+ end
49
+
50
+ FilterInputType =
51
+ ->(schema_type) do
52
+ GraphQL::InputObjectType.define do
53
+ name "#{schema_type.name}FilterInput"
54
+
55
+ schema_type.fields.each do |(name, field)|
56
+ next unless input_type = QueryOperatorInput.call(field.type)
57
+
58
+ argument name, input_type
59
+ end
60
+ end
61
+ end
62
+
63
+ BuildUnionType =
64
+ ->(from_types, union_type_name) do
65
+ possible_types = from_types.values.reject { |t| t.is_a? GraphQL::UnionType }
66
+
67
+ GraphQL::UnionType.define do
68
+ name union_type_name
69
+ possible_types possible_types
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC
4
+ module Contentful
5
+ module Graphql
6
+ VERSION = '1.0.0-rc1'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'wcc/contentful/graphql/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'wcc-contentful-graphql'
9
+ spec.version = WCC::Contentful::Graphql::VERSION
10
+ spec.authors = ['Watermark Dev']
11
+ spec.email = ['dev@watermark.org']
12
+
13
+ spec.summary = File.readlines(File.expand_path('README.md', __dir__)).join
14
+ spec.description = 'GraphQL interface over WCC::Contentful store'
15
+ spec.homepage = 'https://github.com/watermarkchurch/wcc-contentful/wcc-contentful-graphql'
16
+ spec.license = 'MIT'
17
+
18
+ spec.required_ruby_version = '>= 2.3'
19
+
20
+ spec.files =
21
+ `git ls-files -z`.split("\x0").reject do |f|
22
+ f.match(%r{^(test|spec|features)/})
23
+ end
24
+
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.add_development_dependency 'coveralls'
28
+ spec.add_development_dependency 'dotenv', '~> 2.2'
29
+ spec.add_development_dependency 'httplog', '~> 1.0'
30
+ spec.add_development_dependency 'rake', '~> 10.0'
31
+ spec.add_development_dependency 'rspec', '~> 3.0'
32
+ spec.add_development_dependency 'rspec_junit_formatter', '~> 0.3.0'
33
+ spec.add_development_dependency 'rubocop', '0.68'
34
+ spec.add_development_dependency 'simplecov', '~> 0.16.1'
35
+ spec.add_development_dependency 'webmock', '~> 3.0'
36
+
37
+ # Makes testing easy via `bundle exec guard`
38
+ spec.add_development_dependency 'guard', '~> 2.14'
39
+ spec.add_development_dependency 'guard-rspec', '~> 4.7'
40
+ spec.add_development_dependency 'guard-rubocop', '~> 1.3.0'
41
+ spec.add_development_dependency 'guard-shell', '~> 0.7.1'
42
+
43
+ # for generators
44
+ spec.add_development_dependency 'generator_spec', '~> 0.9.4'
45
+ # spec.add_development_dependency 'rails', '~> 5.1'
46
+ # spec.add_development_dependency 'rspec-rails', '~> 3.7'
47
+ spec.add_development_dependency 'sqlite3', '~> 1.3.6'
48
+ spec.add_development_dependency 'timecop', '~> 0.9.1'
49
+
50
+ spec.add_dependency 'graphql', '~> 1.7'
51
+ spec.add_dependency 'wcc-contentful', "~> #{WCC::Contentful::Graphql::VERSION}"
52
+ end
metadata ADDED
@@ -0,0 +1,347 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wcc-contentful-graphql
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.pre.rc1
5
+ platform: ruby
6
+ authors:
7
+ - Watermark Dev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-01-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: coveralls
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dotenv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: httplog
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec_junit_formatter
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.3.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.3.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '='
102
+ - !ruby/object:Gem::Version
103
+ version: '0.68'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '='
109
+ - !ruby/object:Gem::Version
110
+ version: '0.68'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.16.1
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.16.1
125
+ - !ruby/object:Gem::Dependency
126
+ name: webmock
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: guard
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '2.14'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '2.14'
153
+ - !ruby/object:Gem::Dependency
154
+ name: guard-rspec
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '4.7'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '4.7'
167
+ - !ruby/object:Gem::Dependency
168
+ name: guard-rubocop
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 1.3.0
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 1.3.0
181
+ - !ruby/object:Gem::Dependency
182
+ name: guard-shell
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: 0.7.1
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: 0.7.1
195
+ - !ruby/object:Gem::Dependency
196
+ name: generator_spec
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: 0.9.4
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: 0.9.4
209
+ - !ruby/object:Gem::Dependency
210
+ name: sqlite3
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - "~>"
214
+ - !ruby/object:Gem::Version
215
+ version: 1.3.6
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - "~>"
221
+ - !ruby/object:Gem::Version
222
+ version: 1.3.6
223
+ - !ruby/object:Gem::Dependency
224
+ name: timecop
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - "~>"
228
+ - !ruby/object:Gem::Version
229
+ version: 0.9.1
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - "~>"
235
+ - !ruby/object:Gem::Version
236
+ version: 0.9.1
237
+ - !ruby/object:Gem::Dependency
238
+ name: graphql
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - "~>"
242
+ - !ruby/object:Gem::Version
243
+ version: '1.7'
244
+ type: :runtime
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - "~>"
249
+ - !ruby/object:Gem::Version
250
+ version: '1.7'
251
+ - !ruby/object:Gem::Dependency
252
+ name: wcc-contentful
253
+ requirement: !ruby/object:Gem::Requirement
254
+ requirements:
255
+ - - "~>"
256
+ - !ruby/object:Gem::Version
257
+ version: 1.0.0.pre.rc1
258
+ type: :runtime
259
+ prerelease: false
260
+ version_requirements: !ruby/object:Gem::Requirement
261
+ requirements:
262
+ - - "~>"
263
+ - !ruby/object:Gem::Version
264
+ version: 1.0.0.pre.rc1
265
+ description: GraphQL interface over WCC::Contentful store
266
+ email:
267
+ - dev@watermark.org
268
+ executables: []
269
+ extensions: []
270
+ extra_rdoc_files: []
271
+ files:
272
+ - ".rspec"
273
+ - Guardfile
274
+ - README.md
275
+ - lib/wcc/contentful/ext/services.rb
276
+ - lib/wcc/contentful/graphql.rb
277
+ - lib/wcc/contentful/graphql/builder.rb
278
+ - lib/wcc/contentful/graphql/federation.rb
279
+ - lib/wcc/contentful/graphql/federation/builds_arguments.rb
280
+ - lib/wcc/contentful/graphql/federation/namespaces_types.rb
281
+ - lib/wcc/contentful/graphql/field_helper.rb
282
+ - lib/wcc/contentful/graphql/types.rb
283
+ - lib/wcc/contentful/graphql/version.rb
284
+ - wcc-contentful-graphql.gemspec
285
+ homepage: https://github.com/watermarkchurch/wcc-contentful/wcc-contentful-graphql
286
+ licenses:
287
+ - MIT
288
+ metadata: {}
289
+ post_install_message:
290
+ rdoc_options: []
291
+ require_paths:
292
+ - lib
293
+ required_ruby_version: !ruby/object:Gem::Requirement
294
+ requirements:
295
+ - - ">="
296
+ - !ruby/object:Gem::Version
297
+ version: '2.3'
298
+ required_rubygems_version: !ruby/object:Gem::Requirement
299
+ requirements:
300
+ - - ">"
301
+ - !ruby/object:Gem::Version
302
+ version: 1.3.1
303
+ requirements: []
304
+ rubyforge_project:
305
+ rubygems_version: 2.7.6.2
306
+ signing_key:
307
+ specification_version: 4
308
+ summary: '[![Gem Version](https://badge.fury.io/rb/wcc-contentful-graphql.svg)](https://rubygems.org/gems/wcc-contentful-graphql)
309
+ [![Build Status](https://travis-ci.org/watermarkchurch/wcc-contentful.svg?branch=master)](https://travis-ci.org/watermarkchurch/wcc-contentful)
310
+ [![Coverage Status](https://coveralls.io/repos/github/watermarkchurch/wcc-contentful/badge.svg?branch=master)](https://coveralls.io/github/watermarkchurch/wcc-contentful?branch=master) #
311
+ WCC::Contentful::Graphql This gem creates a GraphQL schema over your configured
312
+ [data store](https://www.rubydoc.info/gems/wcc-contentful#Store_API). You can execute
313
+ queries against this GraphQL schema to get all your contentful data. Under the
314
+ hood, queries are executed against your backing store to resolve all the requested
315
+ data. ### Important note! The GraphQL schema currently does not utilize the "include"
316
+ parameter, so it is a very good idea to configure your store to either `:direct`
317
+ or `:lazy_sync`. If you don''t do this, you will see a lot of requests to Contentful
318
+ for specific entries by ID as the GraphQL resolver walks all your links! [More
319
+ info on configuration can be found here](https://www.rubydoc.info/gems/wcc-contentful/WCC%2FContentful%2FConfiguration:store=) ##
320
+ Usage Querying directly within your app ```rb schema = WCC::Contentful::Services.instance.graphql_schema
321
+ => #<GraphQL::Schema ...> result = schema.execute(<<~QUERY) { allConference(filter:
322
+ { code: { eq: "CLC2020" } }) { title startDate code } } QUERY GET https://cdn.contentful.com/spaces/xxxxx/entries?content_type=conference&fields.code.en-US=CLC2020&locale=%2A
323
+ Status 200 => #<GraphQL::Query::Result @query=... @to_h={"data"=>{"allConference"=>[{"title"=>"Church
324
+ Leaders Conference", "startDate"=>"2020-04-28", "code"=>"CLC2020"}]}}> result.to_h
325
+ => {"data"=> {"allConference"=> [{"title"=>"Church Leaders Conference", "startDate"=>"2020-04-28",
326
+ "code"=>"CLC2020"}]}} ``` Setting up a controller to respond to GraphQL queries ```rb
327
+ class Api::GraphqlController < Api::BaseController include WCC::Contentful::ServiceAccessors skip_before_action
328
+ :authenticate_user!, only: :query def query result = graphql_schema.execute( params[:query],
329
+ variables: params[:variables] ) render json: result end end ``` ## Advanced Configuration ###
330
+ Including your Contentful schema inside another GraphQL schema ```rb QueryType
331
+ = GraphQL::ObjectType.define do # extend this to get ''schema_stitch'' extend WCC::Contentful::Graphql::Federation name
332
+ ''RootQuery'' field ''a'', types.String schema_stitch(WCC::Contentful::Services.instance.graphql_schema,
333
+ namespace: ''contentful'') end Schema = GraphQL::Schema.define do query QueryType resolve_type
334
+ ->(type, obj, ctx) { raise StandardError, "Cannot resolve type #{type} #{obj.inspect}
335
+ #{ctx.inspect}" } end File.write(''schema.gql'', GraphQL::Schema::Printer.print_schema(Schema))
336
+ ``` results in... ```gql schema { query: RootQuery } type Contentful { """ Find
337
+ a Asset """ Asset(_content_type: Contentful_StringQueryOperatorInput, description:
338
+ Contentful_StringQueryOperatorInput, id: ID, title: Contentful_StringQueryOperatorInput):
339
+ Contentful_Asset """ Find a CallToAction """ CallToAction(_content_type: Contentful_StringQueryOperatorInput,
340
+ id: ID, internalTitle: Contentful_StringQueryOperatorInput, style: Contentful_StringQueryOperatorInput,
341
+ text: Contentful_StringQueryOperatorInput, title: Contentful_StringQueryOperatorInput):
342
+ Contentful_CallToAction ... ``` ### Limiting the schema to only a few fields ```rb
343
+ store = WCC::Contentful::Services.instance.store builder = WCC::Contentful::Graphql::Builder.new(
344
+ WCC::Contentful.types, store, ).configure do root_types.slice!(''conference'') schema_types[''conference''].define
345
+ do # change the types of some fields, undefine fields, etc... end end @schema =
346
+ builder.build_schema ```'
347
+ test_files: []