wcc-contentful-graphql 1.0.0.pre.rc1

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.
@@ -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: []