wcc-contentful 0.4.0.pre.rc → 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.
Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/Guardfile +43 -0
  3. data/README.md +101 -12
  4. data/app/controllers/wcc/contentful/webhook_controller.rb +25 -24
  5. data/app/jobs/wcc/contentful/webhook_enable_job.rb +36 -2
  6. data/config/routes.rb +1 -1
  7. data/doc/wcc-contentful.png +0 -0
  8. data/lib/tasks/download_schema.rake +12 -0
  9. data/lib/wcc/contentful.rb +70 -16
  10. data/lib/wcc/contentful/active_record_shim.rb +72 -0
  11. data/lib/wcc/contentful/configuration.rb +177 -46
  12. data/lib/wcc/contentful/content_type_indexer.rb +14 -0
  13. data/lib/wcc/contentful/downloads_schema.rb +112 -0
  14. data/lib/wcc/contentful/engine.rb +33 -14
  15. data/lib/wcc/contentful/event.rb +171 -0
  16. data/lib/wcc/contentful/events.rb +41 -0
  17. data/lib/wcc/contentful/exceptions.rb +3 -0
  18. data/lib/wcc/contentful/indexed_representation.rb +2 -2
  19. data/lib/wcc/contentful/instrumentation.rb +31 -0
  20. data/lib/wcc/contentful/link.rb +28 -0
  21. data/lib/wcc/contentful/link_visitor.rb +122 -0
  22. data/lib/wcc/contentful/middleware.rb +7 -0
  23. data/lib/wcc/contentful/middleware/store.rb +158 -0
  24. data/lib/wcc/contentful/middleware/store/caching_middleware.rb +114 -0
  25. data/lib/wcc/contentful/model.rb +37 -3
  26. data/lib/wcc/contentful/model_builder.rb +1 -0
  27. data/lib/wcc/contentful/model_methods.rb +40 -15
  28. data/lib/wcc/contentful/model_singleton_methods.rb +47 -30
  29. data/lib/wcc/contentful/rake.rb +3 -0
  30. data/lib/wcc/contentful/rspec.rb +13 -8
  31. data/lib/wcc/contentful/services.rb +61 -27
  32. data/lib/wcc/contentful/simple_client.rb +81 -25
  33. data/lib/wcc/contentful/simple_client/management.rb +43 -10
  34. data/lib/wcc/contentful/simple_client/response.rb +61 -22
  35. data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +17 -17
  36. data/lib/wcc/contentful/store.rb +7 -66
  37. data/lib/wcc/contentful/store/README.md +85 -0
  38. data/lib/wcc/contentful/store/base.rb +34 -119
  39. data/lib/wcc/contentful/store/cdn_adapter.rb +71 -12
  40. data/lib/wcc/contentful/store/factory.rb +186 -0
  41. data/lib/wcc/contentful/store/instrumentation.rb +55 -0
  42. data/lib/wcc/contentful/store/interface.rb +82 -0
  43. data/lib/wcc/contentful/store/memory_store.rb +27 -24
  44. data/lib/wcc/contentful/store/postgres_store.rb +253 -107
  45. data/lib/wcc/contentful/store/postgres_store/schema_1.sql +73 -0
  46. data/lib/wcc/contentful/store/postgres_store/schema_2.sql +21 -0
  47. data/lib/wcc/contentful/store/query.rb +246 -0
  48. data/lib/wcc/contentful/store/query/interface.rb +63 -0
  49. data/lib/wcc/contentful/store/rspec_examples.rb +48 -0
  50. data/lib/wcc/contentful/store/rspec_examples/basic_store.rb +629 -0
  51. data/lib/wcc/contentful/store/rspec_examples/include_param.rb +283 -0
  52. data/lib/wcc/contentful/store/rspec_examples/nested_queries.rb +342 -0
  53. data/lib/wcc/contentful/sync_engine.rb +181 -0
  54. data/lib/wcc/contentful/test/attributes.rb +17 -5
  55. data/lib/wcc/contentful/test/factory.rb +22 -46
  56. data/lib/wcc/contentful/version.rb +1 -1
  57. data/wcc-contentful.gemspec +14 -11
  58. metadata +201 -146
  59. data/Gemfile +0 -6
  60. data/app/jobs/wcc/contentful/delayed_sync_job.rb +0 -63
  61. data/lib/wcc/contentful/client_ext.rb +0 -28
  62. data/lib/wcc/contentful/graphql.rb +0 -14
  63. data/lib/wcc/contentful/graphql/builder.rb +0 -177
  64. data/lib/wcc/contentful/graphql/types.rb +0 -54
  65. data/lib/wcc/contentful/simple_client/http_adapter.rb +0 -24
  66. data/lib/wcc/contentful/store/lazy_cache_store.rb +0 -161
data/Gemfile DELETED
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
-
5
- # Specify your gem's dependencies in wcc-contentful.gemspec
6
- gemspec
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'active_job'
4
-
5
- module WCC::Contentful
6
- # This job uses the Contentful Sync API to update the configured store with
7
- # the latest data from Contentful.
8
- class DelayedSyncJob < ActiveJob::Base
9
- include WCC::Contentful::ServiceAccessors
10
-
11
- self.queue_adapter = :async
12
- queue_as :default
13
-
14
- def self.mutex
15
- @mutex ||= Mutex.new
16
- end
17
-
18
- def perform(event = nil)
19
- up_to_id = nil
20
- up_to_id = event[:up_to_id] || event.dig('sys', 'id') if event
21
- sync!(up_to_id: up_to_id)
22
- end
23
-
24
- # Calls the Contentful Sync API and updates the configured store with the returned
25
- # data.
26
- #
27
- # @param [String] up_to_id
28
- # An ID that we know has changed and should come back from the sync.
29
- # If we don't find this ID in the sync data, then drop a job to try
30
- # the sync again after a few minutes.
31
- #
32
- def sync!(up_to_id: nil)
33
- return unless store.respond_to?(:index)
34
-
35
- self.class.mutex.synchronize do
36
- next_sync_token = store.find('sync:token')&.fetch('token')
37
- sync_resp = client.sync(sync_token: next_sync_token)
38
-
39
- id_found = up_to_id.nil?
40
-
41
- count = 0
42
- sync_resp.items.each do |item|
43
- id = item.dig('sys', 'id')
44
- id_found ||= id == up_to_id
45
- store.index(item)
46
- count += 1
47
- end
48
- store.set('sync:token', token: sync_resp.next_sync_token)
49
-
50
- logger.info "Synced #{count} entries. Next sync token:\n #{sync_resp.next_sync_token}"
51
- sync_later!(up_to_id: up_to_id) unless id_found
52
- sync_resp.next_sync_token
53
- end
54
- end
55
-
56
- # Drops an ActiveJob job to invoke WCC::Contentful.sync! after a given amount
57
- # of time.
58
- def sync_later!(up_to_id: nil, wait: 10.minutes)
59
- WCC::Contentful::DelayedSyncJob.set(wait: wait)
60
- .perform_later(up_to_id: up_to_id)
61
- end
62
- end
63
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Contentful::Client
4
- class << self
5
- alias_method :old_get_http, :get_http
6
- end
7
-
8
- def self.adapter
9
- @adapter ||=
10
- WCC::Contentful::SimpleClient.load_adapter(WCC::Contentful.configuration.http_adapter) ||
11
- ->(url, query, headers, proxy) { old_get_http(url, query, headers, proxy) }
12
- end
13
-
14
- def self.get_http(url, query, headers = {}, proxy = {})
15
- if environment = WCC::Contentful.configuration.environment
16
- url = rewrite_to_environment(url, environment)
17
- end
18
-
19
- adapter.call(url, query, headers, proxy)
20
- end
21
-
22
- REWRITE_REGEXP = /^(https?\:\/\/(?:\w+)\.contentful\.com\/spaces\/[^\/]+\/)(?!environments)(.+)$/
23
- def self.rewrite_to_environment(url, environment)
24
- return url unless m = REWRITE_REGEXP.match(url)
25
-
26
- File.join(m[1], 'environments', environment, m[2])
27
- end
28
- end
@@ -1,14 +0,0 @@
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://zube.io/watermarkchurch/development/c/2234 hook it up
10
- module Graphql
11
- end
12
- end
13
-
14
- require_relative 'graphql/builder'
@@ -1,177 +0,0 @@
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)
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_all(content_type: content_type)
62
- # TODO: improve this POC
63
- if args[:filter]
64
- filter = {}
65
- filter[args[:filter]['field']] = { eq: args[:filter][:eq] }
66
- relation = relation.apply(filter, ctx)
67
- end
68
- relation.result
69
- }
70
- end
71
- end
72
- end
73
- end
74
-
75
- def build_schema_types
76
- @types.each_with_object({}) do |(k, v), h|
77
- h[k] = build_schema_type(v)
78
- end
79
- end
80
-
81
- def build_schema_type(typedef)
82
- store = @store
83
- builder = self
84
- content_type = typedef.content_type
85
-
86
- GraphQL::ObjectType.define do
87
- name(typedef.name)
88
-
89
- description("Generated from content type #{content_type}")
90
-
91
- field :id, !types.ID do
92
- resolve ->(obj, _args, _ctx) {
93
- obj.dig('sys', 'id')
94
- }
95
- end
96
-
97
- field :_content_type, !types.String do
98
- resolve ->(_, _, _) {
99
- content_type
100
- }
101
- end
102
-
103
- # Make a field for each column:
104
- typedef.fields.each_value do |f|
105
- case f.type
106
- when :Asset
107
- field(f.name.to_sym, -> {
108
- type = builder.schema_types['Asset']
109
- type = type.to_list_type if f.array
110
- type
111
- }) do
112
- resolve ->(obj, _args, ctx) {
113
- links = obj.dig('fields', f.name, ctx[:locale] || 'en-US')
114
- return if links.nil?
115
-
116
- if links.is_a? Array
117
- links.reject(&:nil?).map { |l| store.find(l.dig('sys', 'id')) }
118
- else
119
- store.find(links.dig('sys', 'id'))
120
- end
121
- }
122
- end
123
- when :Link
124
- field(f.name.to_sym, -> {
125
- type =
126
- if f.link_types.nil? || f.link_types.empty?
127
- builder.schema_types['AnyContentful'] ||=
128
- Types::BuildUnionType.call(builder.schema_types, 'AnyContentful')
129
- elsif f.link_types.length == 1
130
- builder.schema_types[f.link_types.first]
131
- else
132
- from_types = builder.schema_types.select { |key| f.link_types.include?(key) }
133
- name = "#{typedef.name}_#{f.name}"
134
- builder.schema_types[name] ||= Types::BuildUnionType.call(from_types, name)
135
- end
136
- type = type.to_list_type if f.array
137
- type
138
- }) do
139
- resolve ->(obj, _args, ctx) {
140
- links = obj.dig('fields', f.name, ctx[:locale] || 'en-US')
141
- return if links.nil?
142
-
143
- if links.is_a? Array
144
- links.reject(&:nil?).map { |l| store.find(l.dig('sys', 'id')) }
145
- else
146
- store.find(links.dig('sys', 'id'))
147
- end
148
- }
149
- end
150
- else
151
- type =
152
- case f.type
153
- when :DateTime
154
- Types::DateTimeType
155
- when :Coordinates
156
- Types::CoordinatesType
157
- when :Json
158
- Types::HashType
159
- else
160
- types.public_send(f.type)
161
- end
162
- type = type.to_list_type if f.array
163
- field(f.name.to_sym, type) do
164
- resolve ->(obj, _args, ctx) {
165
- if obj.is_a? Array
166
- obj.map { |o| o.dig('fields', f.name, ctx[:locale] || 'en-US') }
167
- else
168
- obj.dig('fields', f.name, ctx[:locale] || 'en-US')
169
- end
170
- }
171
- end
172
- end
173
- end
174
- end
175
- end
176
- end
177
- end
@@ -1,54 +0,0 @@
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
- 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
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- gem 'http'
4
- require 'http'
5
-
6
- class HttpAdapter
7
- def call(url, query, headers = {}, proxy = {})
8
- if proxy[:host]
9
- HTTP[headers].via(proxy[:host], proxy[:port], proxy[:username], proxy[:password])
10
- .get(url, params: query)
11
- else
12
- HTTP[headers].get(url, params: query)
13
- end
14
- end
15
-
16
- def post(url, body, headers = {}, proxy = {})
17
- if proxy[:host]
18
- HTTP[headers].via(proxy[:host], proxy[:port], proxy[:username], proxy[:password])
19
- .post(url, json: body)
20
- else
21
- HTTP[headers].post(url, json: body)
22
- end
23
- end
24
- end
@@ -1,161 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WCC::Contentful::Store
4
- class LazyCacheStore
5
- def initialize(client, cache: nil)
6
- @cdn = CDNAdapter.new(client)
7
- @cache = cache || ActiveSupport::Cache::MemoryStore.new
8
- @client = client
9
- end
10
-
11
- def find(key, **options)
12
- found =
13
- @cache.fetch(key) do
14
- # if it's not a contentful ID don't hit the API.
15
- # Store a nil object if we can't find the object on the CDN.
16
- (@cdn.find(key, options) || nil_obj(key)) if key =~ /^\w+$/
17
- end
18
-
19
- case found.try(:dig, 'sys', 'type')
20
- when 'Nil', 'DeletedEntry', 'DeletedAsset'
21
- nil
22
- else
23
- found
24
- end
25
- end
26
-
27
- # TODO: https://github.com/watermarkchurch/wcc-contentful/issues/18
28
- # figure out how to cache the results of a find_by query, ex:
29
- # `find_by('slug' => '/about')`
30
- def find_by(content_type:, filter: nil, options: nil)
31
- if filter.keys == ['sys.id']
32
- # Direct ID lookup, like what we do in `WCC::Contentful::ModelMethods.resolve`
33
- # We can return just this item. Stores are not required to implement :include option.
34
- if found = @cache.read(filter['sys.id'])
35
- return found
36
- end
37
- end
38
-
39
- q = find_all(content_type: content_type, options: { limit: 1 }.merge!(options || {}))
40
- q = q.apply(filter) if filter
41
- q.first
42
- end
43
-
44
- def find_all(content_type:, options: nil)
45
- Query.new(
46
- store: self,
47
- client: @client,
48
- relation: { content_type: content_type },
49
- cache: @cache,
50
- options: options
51
- )
52
- end
53
-
54
- # #index is called whenever the sync API comes back with more data.
55
- def index(json)
56
- id = json.dig('sys', 'id')
57
- return unless prev = @cache.read(id)
58
-
59
- if (prev_rev = prev&.dig('sys', 'revision')) && (next_rev = json.dig('sys', 'revision'))
60
- return prev if next_rev < prev_rev
61
- end
62
-
63
- # we also set deletes in the cache - no need to go hit the API when we know
64
- # this is a nil object
65
- ensure_hash json
66
- @cache.write(id, json)
67
-
68
- case json.dig('sys', 'type')
69
- when 'DeletedEntry', 'DeletedAsset'
70
- nil
71
- else
72
- json
73
- end
74
- end
75
-
76
- def set(key, value)
77
- ensure_hash value
78
- old = @cache.read(key)
79
- @cache.write(key, value)
80
- old
81
- end
82
-
83
- def delete(key)
84
- old = @cache.read(key)
85
- @cache.delete(key)
86
- old
87
- end
88
-
89
- def nil_obj(id)
90
- {
91
- 'sys' => {
92
- 'id' => id,
93
- 'type' => 'Nil',
94
- 'revision' => 1
95
- }
96
- }
97
- end
98
-
99
- def ensure_hash(val)
100
- raise ArgumentError, 'Value must be a Hash' unless val.is_a?(Hash)
101
- end
102
-
103
- class Query < CDNAdapter::Query
104
- def initialize(cache:, **extra)
105
- super(cache: cache, **extra)
106
- @cache = cache
107
- end
108
-
109
- private
110
-
111
- def response
112
- # Disabling because the superclass already took `@response`
113
- # rubocop:disable Naming/MemoizedInstanceVariableName
114
- @wrapped_response ||= ResponseWrapper.new(super, @cache)
115
- # rubocop:enable Naming/MemoizedInstanceVariableName
116
- end
117
-
118
- ResponseWrapper =
119
- Struct.new(:response, :cache) do
120
- delegate :count, to: :response
121
-
122
- def items
123
- @items ||=
124
- response.items.map do |item|
125
- id = item.dig('sys', 'id')
126
- prev = cache.read(id)
127
- unless (prev_rev = prev&.dig('sys', 'revision')) &&
128
- (next_rev = item.dig('sys', 'revision')) &&
129
- next_rev < prev_rev
130
-
131
- cache.write(id, item)
132
- end
133
-
134
- item
135
- end
136
- end
137
-
138
- def includes
139
- @includes ||= IncludesWrapper.new(response, cache)
140
- end
141
- end
142
-
143
- IncludesWrapper =
144
- Struct.new(:response, :cache) do
145
- def [](id)
146
- return unless item = response.includes[id]
147
-
148
- prev = cache.read(id)
149
- unless (prev_rev = prev&.dig('sys', 'revision')) &&
150
- (next_rev = item.dig('sys', 'revision')) &&
151
- next_rev < prev_rev
152
-
153
- cache.write(id, item)
154
- end
155
-
156
- item
157
- end
158
- end
159
- end
160
- end
161
- end