wcc-contentful 0.0.3 → 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 +4 -4
- data/.circleci/config.yml +51 -0
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/.rubocop.yml +240 -0
- data/.rubocop_todo.yml +13 -0
- data/CHANGELOG.md +7 -1
- data/Gemfile +4 -2
- data/Guardfile +36 -0
- data/README.md +1 -1
- data/Rakefile +5 -3
- data/bin/rspec +3 -0
- data/lib/generators/wcc/USAGE +24 -0
- data/lib/generators/wcc/menu_generator.rb +67 -0
- data/lib/generators/wcc/templates/.keep +0 -0
- data/lib/generators/wcc/templates/Procfile +3 -0
- data/lib/generators/wcc/templates/contentful_shell_wrapper +342 -0
- data/lib/generators/wcc/templates/menu/generated_add_menus.ts +85 -0
- data/lib/generators/wcc/templates/menu/menu.rb +25 -0
- data/lib/generators/wcc/templates/menu/menu_button.rb +25 -0
- data/lib/generators/wcc/templates/release +9 -0
- data/lib/generators/wcc/templates/wcc_contentful.rb +18 -0
- data/lib/wcc/contentful.rb +93 -26
- data/lib/wcc/contentful/client_ext.rb +15 -0
- data/lib/wcc/contentful/configuration.rb +93 -0
- data/lib/wcc/contentful/content_type_indexer.rb +153 -0
- data/lib/wcc/contentful/exceptions.rb +34 -0
- data/lib/wcc/contentful/graphql.rb +15 -0
- data/lib/wcc/contentful/graphql/builder.rb +172 -0
- data/lib/wcc/contentful/graphql/types.rb +54 -0
- data/lib/wcc/contentful/helpers.rb +28 -0
- data/lib/wcc/contentful/indexed_representation.rb +111 -0
- data/lib/wcc/contentful/model.rb +24 -0
- data/lib/wcc/contentful/model/menu.rb +7 -0
- data/lib/wcc/contentful/model/menu_button.rb +15 -0
- data/lib/wcc/contentful/model_builder.rb +151 -0
- data/lib/wcc/contentful/model_validators.rb +64 -0
- data/lib/wcc/contentful/model_validators/dsl.rb +165 -0
- data/lib/wcc/contentful/simple_client.rb +127 -0
- data/lib/wcc/contentful/simple_client/response.rb +160 -0
- data/lib/wcc/contentful/store.rb +8 -0
- data/lib/wcc/contentful/store/cdn_adapter.rb +79 -0
- data/lib/wcc/contentful/store/memory_store.rb +75 -0
- data/lib/wcc/contentful/store/postgres_store.rb +132 -0
- data/lib/wcc/contentful/version.rb +3 -1
- data/wcc-contentful.gemspec +49 -24
- metadata +261 -16
- data/lib/wcc/contentful/redirect.rb +0 -33
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WCC::Contentful
|
4
|
+
class ValidationError < StandardError
|
5
|
+
Message =
|
6
|
+
Struct.new(:path, :error) do
|
7
|
+
def to_s
|
8
|
+
"#{path}: #{error}"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :errors
|
13
|
+
|
14
|
+
def initialize(errors)
|
15
|
+
@errors = ValidationError.join_msg_keys(errors)
|
16
|
+
super("Content Type Schema from Contentful failed validation!\n #{@errors.join("\n ")}")
|
17
|
+
end
|
18
|
+
|
19
|
+
# Turns the error messages hash into an array of message structs like:
|
20
|
+
# menu.fields.name.type: must be equal to String
|
21
|
+
def self.join_msg_keys(hash)
|
22
|
+
ret =
|
23
|
+
hash.map do |k, v|
|
24
|
+
if v.is_a?(Hash)
|
25
|
+
msgs = join_msg_keys(v)
|
26
|
+
msgs.map { |msg| Message.new(k.to_s + '.' + msg.path, msg.error) }
|
27
|
+
else
|
28
|
+
v.map { |msg| Message.new(k.to_s, msg) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
ret.flatten(1)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
gem 'graphql', '~> 1.7'
|
5
|
+
require 'graphql'
|
6
|
+
|
7
|
+
module WCC::Contentful
|
8
|
+
# This module builds a GraphQL schema out of our IndexedRepresentation.
|
9
|
+
# It is currently unused and not hooked up in the WCC::Contentful.init! method.
|
10
|
+
# TODO: https://zube.io/watermarkchurch/development/c/2234 hook it up
|
11
|
+
module Graphql
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
require_relative 'graphql/builder'
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql'
|
4
|
+
|
5
|
+
require_relative 'types'
|
6
|
+
|
7
|
+
module WCC::Contentful::Graphql
|
8
|
+
class Builder
|
9
|
+
attr_reader :schema_types
|
10
|
+
|
11
|
+
def initialize(types, store)
|
12
|
+
@types = types
|
13
|
+
@store = store
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_schema
|
17
|
+
@schema_types = build_schema_types
|
18
|
+
|
19
|
+
root_query_type = build_root_query(@schema_types)
|
20
|
+
|
21
|
+
builder = self
|
22
|
+
GraphQL::Schema.define do
|
23
|
+
query root_query_type
|
24
|
+
|
25
|
+
resolve_type ->(_type, obj, _ctx) {
|
26
|
+
content_type = WCC::Contentful::Helpers.content_type_from_raw(obj)
|
27
|
+
builder.schema_types[content_type]
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def build_root_query(schema_types)
|
35
|
+
store = @store
|
36
|
+
|
37
|
+
GraphQL::ObjectType.define do
|
38
|
+
name 'Query'
|
39
|
+
description 'The query root of this schema'
|
40
|
+
|
41
|
+
schema_types.each do |content_type, schema_type|
|
42
|
+
field schema_type.name.to_sym do
|
43
|
+
type schema_type
|
44
|
+
argument :id, types.ID
|
45
|
+
description "Find a #{schema_type.name} by ID"
|
46
|
+
|
47
|
+
resolve ->(_obj, args, _ctx) {
|
48
|
+
if args['id'].nil?
|
49
|
+
store.find_by(content_type: content_type).first
|
50
|
+
else
|
51
|
+
store.find(args['id'])
|
52
|
+
end
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
field "all#{schema_type.name}".to_sym do
|
57
|
+
type schema_type.to_list_type
|
58
|
+
argument :filter, Types::FilterType
|
59
|
+
|
60
|
+
resolve ->(_obj, args, ctx) {
|
61
|
+
relation = store.find_by(content_type: content_type)
|
62
|
+
relation = relation.apply(args[:filter], ctx) if args[:filter]
|
63
|
+
relation.result
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_schema_types
|
71
|
+
@types.each_with_object({}) do |(k, v), h|
|
72
|
+
h[k] = build_schema_type(v)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_schema_type(typedef)
|
77
|
+
store = @store
|
78
|
+
builder = self
|
79
|
+
content_type = typedef.content_type
|
80
|
+
|
81
|
+
GraphQL::ObjectType.define do
|
82
|
+
name(typedef.name)
|
83
|
+
|
84
|
+
description("Generated from content type #{content_type}")
|
85
|
+
|
86
|
+
field :id, !types.ID do
|
87
|
+
resolve ->(obj, _args, _ctx) {
|
88
|
+
obj.dig('sys', 'id')
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
field :_content_type, !types.String do
|
93
|
+
resolve ->(_, _, _) {
|
94
|
+
content_type
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
# Make a field for each column:
|
99
|
+
typedef.fields.each_value do |f|
|
100
|
+
case f.type
|
101
|
+
when :Asset
|
102
|
+
field(f.name.to_sym, -> {
|
103
|
+
type = builder.schema_types['Asset']
|
104
|
+
type = type.to_list_type if f.array
|
105
|
+
type
|
106
|
+
}) do
|
107
|
+
resolve ->(obj, _args, ctx) {
|
108
|
+
links = obj.dig('fields', f.name, ctx[:locale] || 'en-US')
|
109
|
+
return if links.nil?
|
110
|
+
|
111
|
+
if links.is_a? Array
|
112
|
+
links.reject(&:nil?).map { |l| store.find(l.dig('sys', 'id')) }
|
113
|
+
else
|
114
|
+
store.find(links.dig('sys', 'id'))
|
115
|
+
end
|
116
|
+
}
|
117
|
+
end
|
118
|
+
when :Link
|
119
|
+
field(f.name.to_sym, -> {
|
120
|
+
type =
|
121
|
+
if f.link_types.nil? || f.link_types.empty?
|
122
|
+
builder.schema_types['AnyContentful'] ||=
|
123
|
+
Types::BuildUnionType.call(builder.schema_types, 'AnyContentful')
|
124
|
+
elsif f.link_types.length == 1
|
125
|
+
builder.schema_types[f.link_types.first]
|
126
|
+
else
|
127
|
+
from_types = builder.schema_types.select { |key| f.link_types.include?(key) }
|
128
|
+
name = "#{typedef.name}_#{f.name}"
|
129
|
+
builder.schema_types[name] ||= Types::BuildUnionType.call(from_types, name)
|
130
|
+
end
|
131
|
+
type = type.to_list_type if f.array
|
132
|
+
type
|
133
|
+
}) do
|
134
|
+
resolve ->(obj, _args, ctx) {
|
135
|
+
links = obj.dig('fields', f.name, ctx[:locale] || 'en-US')
|
136
|
+
return if links.nil?
|
137
|
+
|
138
|
+
if links.is_a? Array
|
139
|
+
links.reject(&:nil?).map { |l| store.find(l.dig('sys', 'id')) }
|
140
|
+
else
|
141
|
+
store.find(links.dig('sys', 'id'))
|
142
|
+
end
|
143
|
+
}
|
144
|
+
end
|
145
|
+
else
|
146
|
+
type =
|
147
|
+
case f.type
|
148
|
+
when :DateTime
|
149
|
+
Types::DateTimeType
|
150
|
+
when :Coordinates
|
151
|
+
Types::CoordinatesType
|
152
|
+
when :Json
|
153
|
+
Types::HashType
|
154
|
+
else
|
155
|
+
types.public_send(f.type)
|
156
|
+
end
|
157
|
+
type = type.to_list_type if f.array
|
158
|
+
field(f.name.to_sym, type) do
|
159
|
+
resolve ->(obj, _args, ctx) {
|
160
|
+
if obj.is_a? Array
|
161
|
+
obj.map { |o| o.dig('fields', f.name, ctx[:locale] || 'en-US') }
|
162
|
+
else
|
163
|
+
obj.dig('fields', f.name, ctx[:locale] || 'en-US')
|
164
|
+
end
|
165
|
+
}
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module WCC::Contentful::Graphql::Types
|
5
|
+
DateTimeType =
|
6
|
+
GraphQL::ScalarType.define do
|
7
|
+
name 'DateTime'
|
8
|
+
|
9
|
+
coerce_result ->(value, _ctx) { Time.zone.parse(value) }
|
10
|
+
end
|
11
|
+
|
12
|
+
HashType =
|
13
|
+
GraphQL::ScalarType.define do
|
14
|
+
name 'Hash'
|
15
|
+
|
16
|
+
coerce_result ->(value, _ctx) {
|
17
|
+
return value if value.is_a? Array
|
18
|
+
return value.to_h if value.respond_to?(:to_h)
|
19
|
+
return JSON.parse(value) if value.is_a? String
|
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
|
+
AnyScalarInputType =
|
33
|
+
GraphQL::ScalarType.define do
|
34
|
+
name 'Any'
|
35
|
+
end
|
36
|
+
|
37
|
+
FilterType =
|
38
|
+
GraphQL::InputObjectType.define do
|
39
|
+
name 'filter'
|
40
|
+
|
41
|
+
argument :field, !types.String
|
42
|
+
argument :eq, AnyScalarInputType
|
43
|
+
end
|
44
|
+
|
45
|
+
BuildUnionType =
|
46
|
+
->(from_types, union_type_name) do
|
47
|
+
possible_types = from_types.values.reject { |t| t.is_a? GraphQL::UnionType }
|
48
|
+
|
49
|
+
GraphQL::UnionType.define do
|
50
|
+
name union_type_name
|
51
|
+
possible_types possible_types
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module WCC::Contentful::Helpers
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def content_type_from_raw(value)
|
8
|
+
case value.dig('sys', 'type')
|
9
|
+
when 'Entry'
|
10
|
+
value.dig('sys', 'contentType', 'sys', 'id')
|
11
|
+
when 'Asset'
|
12
|
+
'Asset'
|
13
|
+
else
|
14
|
+
raise ArgumentError, "Unknown content type '#{value.dig('sys', 'type') || 'null'}'"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def constant_from_content_type(content_type)
|
19
|
+
content_type.camelize.gsub(/[^_a-zA-Z0-9]/, '_')
|
20
|
+
end
|
21
|
+
|
22
|
+
def shared_prefix(string_array)
|
23
|
+
string_array.reduce do |l, s|
|
24
|
+
l = l.chop while l != s[0...l.length]
|
25
|
+
l
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WCC::Contentful
|
4
|
+
# The result of running the indexer on raw content types to produce
|
5
|
+
# a type definition which can be used to build models or graphql types.
|
6
|
+
class IndexedRepresentation
|
7
|
+
def initialize(types = {})
|
8
|
+
@types = types
|
9
|
+
end
|
10
|
+
|
11
|
+
delegate :keys, to: :@types
|
12
|
+
delegate :[], to: :@types
|
13
|
+
delegate :each_with_object, to: :@types
|
14
|
+
delegate :each_value, to: :@types
|
15
|
+
|
16
|
+
def []=(id, value)
|
17
|
+
raise ArgumentError unless value.is_a?(ContentType)
|
18
|
+
@types[id] = value
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.from_json(hash)
|
22
|
+
hash = JSON.parse(hash) if hash.is_a?(String)
|
23
|
+
|
24
|
+
ret = IndexedRepresentation.new
|
25
|
+
hash.each do |id, content_type_hash|
|
26
|
+
ret[id] = ContentType.new(content_type_hash)
|
27
|
+
end
|
28
|
+
ret
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_json
|
32
|
+
@types.to_json
|
33
|
+
end
|
34
|
+
|
35
|
+
class ContentType
|
36
|
+
ATTRIBUTES = %i[
|
37
|
+
name
|
38
|
+
content_type
|
39
|
+
fields
|
40
|
+
].freeze
|
41
|
+
|
42
|
+
attr_accessor(*ATTRIBUTES)
|
43
|
+
|
44
|
+
def initialize(hash_or_id = nil)
|
45
|
+
@fields = {}
|
46
|
+
return unless hash_or_id
|
47
|
+
if hash_or_id.is_a?(String)
|
48
|
+
@name = hash_or_id
|
49
|
+
return
|
50
|
+
end
|
51
|
+
|
52
|
+
if raw_fields = (hash_or_id.delete('fields') || hash_or_id.delete(:fields))
|
53
|
+
raw_fields.each do |field_name, raw_field|
|
54
|
+
@fields[field_name] = Field.new(raw_field)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
hash_or_id.each { |k, v| public_send("#{k}=", v) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Field
|
63
|
+
ATTRIBUTES = %i[
|
64
|
+
name
|
65
|
+
type
|
66
|
+
array
|
67
|
+
required
|
68
|
+
link_types
|
69
|
+
].freeze
|
70
|
+
|
71
|
+
attr_accessor(*ATTRIBUTES)
|
72
|
+
|
73
|
+
TYPES = %i[
|
74
|
+
String
|
75
|
+
Int
|
76
|
+
Float
|
77
|
+
DateTime
|
78
|
+
Boolean
|
79
|
+
Json
|
80
|
+
Coordinates
|
81
|
+
Link
|
82
|
+
Asset
|
83
|
+
].freeze
|
84
|
+
|
85
|
+
def type=(raw_type)
|
86
|
+
unless TYPES.include?(raw_type)
|
87
|
+
raise ArgumentError, "Unknown type #{raw_type}, expected one of: #{TYPES}"
|
88
|
+
end
|
89
|
+
@type = raw_type
|
90
|
+
end
|
91
|
+
|
92
|
+
def initialize(hash_or_id = nil)
|
93
|
+
return unless hash_or_id
|
94
|
+
if hash_or_id.is_a?(String)
|
95
|
+
@name = hash_or_id
|
96
|
+
return
|
97
|
+
end
|
98
|
+
|
99
|
+
if raw_type = hash_or_id.delete('type')
|
100
|
+
raw_type = raw_type.to_sym
|
101
|
+
unless TYPES.include?(raw_type)
|
102
|
+
raise ArgumentError, "Unknown type #{raw_type}, expected one of: #{TYPES}"
|
103
|
+
end
|
104
|
+
@type = raw_type
|
105
|
+
end
|
106
|
+
|
107
|
+
hash_or_id.each { |k, v| public_send("#{k}=", v) }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class WCC::Contentful::Model
|
5
|
+
extend WCC::Contentful::Helpers
|
6
|
+
extend WCC::Contentful::ModelValidators
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :store
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.all_models
|
13
|
+
WCC::Contentful::Model.constants(false).map { |k| WCC::Contentful::Model.const_get(k) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.find(id, context = nil)
|
17
|
+
return unless raw = store.find(id)
|
18
|
+
|
19
|
+
content_type = content_type_from_raw(raw)
|
20
|
+
|
21
|
+
const = WCC::Contentful::Model.const_get(constant_from_content_type(content_type))
|
22
|
+
const.new(raw, context)
|
23
|
+
end
|
24
|
+
end
|