wcc-contentful 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|