wcc-contentful 0.3.0 → 1.0.0.pre.rc2

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 (99) hide show
  1. checksums.yaml +5 -5
  2. data/.rspec +1 -1
  3. data/Guardfile +43 -0
  4. data/README.md +161 -11
  5. data/Rakefile +3 -6
  6. data/app/controllers/wcc/contentful/webhook_controller.rb +25 -24
  7. data/app/jobs/wcc/contentful/webhook_enable_job.rb +36 -2
  8. data/bin/console +4 -3
  9. data/bin/rails +2 -0
  10. data/config/routes.rb +1 -1
  11. data/doc +1 -0
  12. data/lib/tasks/download_schema.rake +12 -0
  13. data/lib/wcc/contentful.rb +69 -45
  14. data/lib/wcc/contentful/active_record_shim.rb +72 -0
  15. data/lib/wcc/contentful/configuration.rb +177 -46
  16. data/lib/wcc/contentful/content_type_indexer.rb +14 -0
  17. data/lib/wcc/contentful/downloads_schema.rb +112 -0
  18. data/lib/wcc/contentful/engine.rb +33 -14
  19. data/lib/wcc/contentful/event.rb +171 -0
  20. data/lib/wcc/contentful/events.rb +41 -0
  21. data/lib/wcc/contentful/exceptions.rb +3 -33
  22. data/lib/wcc/contentful/indexed_representation.rb +2 -2
  23. data/lib/wcc/contentful/instrumentation.rb +31 -0
  24. data/lib/wcc/contentful/link.rb +28 -0
  25. data/lib/wcc/contentful/link_visitor.rb +122 -0
  26. data/lib/wcc/contentful/middleware.rb +7 -0
  27. data/lib/wcc/contentful/middleware/store.rb +158 -0
  28. data/lib/wcc/contentful/middleware/store/caching_middleware.rb +114 -0
  29. data/lib/wcc/contentful/model.rb +37 -4
  30. data/lib/wcc/contentful/model_builder.rb +1 -0
  31. data/lib/wcc/contentful/model_methods.rb +40 -15
  32. data/lib/wcc/contentful/model_singleton_methods.rb +47 -30
  33. data/lib/wcc/contentful/rake.rb +4 -0
  34. data/lib/wcc/contentful/rspec.rb +46 -0
  35. data/lib/wcc/contentful/services.rb +61 -27
  36. data/lib/wcc/contentful/simple_client.rb +81 -25
  37. data/lib/wcc/contentful/simple_client/management.rb +43 -10
  38. data/lib/wcc/contentful/simple_client/response.rb +61 -22
  39. data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +17 -17
  40. data/lib/wcc/contentful/store.rb +7 -66
  41. data/lib/wcc/contentful/store/README.md +85 -0
  42. data/lib/wcc/contentful/store/base.rb +34 -119
  43. data/lib/wcc/contentful/store/cdn_adapter.rb +71 -12
  44. data/lib/wcc/contentful/store/factory.rb +186 -0
  45. data/lib/wcc/contentful/store/instrumentation.rb +55 -0
  46. data/lib/wcc/contentful/store/interface.rb +82 -0
  47. data/lib/wcc/contentful/store/memory_store.rb +27 -24
  48. data/lib/wcc/contentful/store/postgres_store.rb +268 -101
  49. data/lib/wcc/contentful/store/postgres_store/schema_1.sql +73 -0
  50. data/lib/wcc/contentful/store/postgres_store/schema_2.sql +21 -0
  51. data/lib/wcc/contentful/store/query.rb +246 -0
  52. data/lib/wcc/contentful/store/query/interface.rb +63 -0
  53. data/lib/wcc/contentful/store/rspec_examples.rb +48 -0
  54. data/lib/wcc/contentful/store/rspec_examples/basic_store.rb +629 -0
  55. data/lib/wcc/contentful/store/rspec_examples/include_param.rb +283 -0
  56. data/lib/wcc/contentful/store/rspec_examples/nested_queries.rb +342 -0
  57. data/lib/wcc/contentful/sync_engine.rb +181 -0
  58. data/lib/wcc/contentful/test.rb +7 -0
  59. data/lib/wcc/contentful/test/attributes.rb +56 -0
  60. data/lib/wcc/contentful/test/double.rb +76 -0
  61. data/lib/wcc/contentful/test/factory.rb +101 -0
  62. data/lib/wcc/contentful/version.rb +1 -1
  63. data/wcc-contentful.gemspec +28 -14
  64. metadata +248 -152
  65. data/.circleci/config.yml +0 -51
  66. data/.gitignore +0 -26
  67. data/.rubocop.yml +0 -242
  68. data/.rubocop_todo.yml +0 -19
  69. data/.travis.yml +0 -5
  70. data/CHANGELOG.md +0 -180
  71. data/CODE_OF_CONDUCT.md +0 -74
  72. data/Gemfile +0 -8
  73. data/LICENSE.txt +0 -21
  74. data/app/jobs/wcc/contentful/delayed_sync_job.rb +0 -63
  75. data/lib/generators/wcc/USAGE +0 -24
  76. data/lib/generators/wcc/model_generator.rb +0 -90
  77. data/lib/generators/wcc/templates/.keep +0 -0
  78. data/lib/generators/wcc/templates/Procfile +0 -3
  79. data/lib/generators/wcc/templates/contentful_shell_wrapper +0 -385
  80. data/lib/generators/wcc/templates/menu/generated_add_menus.ts +0 -192
  81. data/lib/generators/wcc/templates/menu/models/menu.rb +0 -23
  82. data/lib/generators/wcc/templates/menu/models/menu_button.rb +0 -23
  83. data/lib/generators/wcc/templates/page/generated_add_pages.ts +0 -50
  84. data/lib/generators/wcc/templates/page/models/page.rb +0 -23
  85. data/lib/generators/wcc/templates/release +0 -9
  86. data/lib/generators/wcc/templates/wcc_contentful.rb +0 -17
  87. data/lib/wcc/contentful/client_ext.rb +0 -28
  88. data/lib/wcc/contentful/graphql.rb +0 -14
  89. data/lib/wcc/contentful/graphql/builder.rb +0 -177
  90. data/lib/wcc/contentful/graphql/types.rb +0 -54
  91. data/lib/wcc/contentful/model/dropdown_menu.rb +0 -7
  92. data/lib/wcc/contentful/model/menu.rb +0 -6
  93. data/lib/wcc/contentful/model/menu_button.rb +0 -16
  94. data/lib/wcc/contentful/model/page.rb +0 -8
  95. data/lib/wcc/contentful/model/redirect.rb +0 -19
  96. data/lib/wcc/contentful/model_validators.rb +0 -121
  97. data/lib/wcc/contentful/model_validators/dsl.rb +0 -166
  98. data/lib/wcc/contentful/simple_client/http_adapter.rb +0 -24
  99. data/lib/wcc/contentful/store/lazy_cache_store.rb +0 -161
@@ -29,8 +29,8 @@ module WCC::Contentful
29
29
  ret
30
30
  end
31
31
 
32
- def to_json
33
- @types.to_json
32
+ def to_json(*args)
33
+ @types.to_json(*args)
34
34
  end
35
35
 
36
36
  def deep_dup
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::Contentful
4
+ module Instrumentation
5
+ extend ActiveSupport::Concern
6
+
7
+ def _instrumentation_event_prefix
8
+ @_instrumentation_event_prefix ||=
9
+ # WCC::Contentful => contentful.wcc
10
+ '.' + (is_a?(Class) || is_a?(Module) ? self : self.class)
11
+ .name.parameterize.split('-').reverse.join('.')
12
+ end
13
+
14
+ included do
15
+ protected
16
+
17
+ def _instrument(name, payload = {}, &block)
18
+ name += _instrumentation_event_prefix
19
+ (@_instrumentation ||= WCC::Contentful::Services.instance.instrumentation)
20
+ .instrument(name, payload, &block)
21
+ end
22
+ end
23
+
24
+ class << self
25
+ def instrument(name, payload = {}, &block)
26
+ WCC::Contentful::Services.instance
27
+ .instrumentation.instrument(name, payload, &block)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class WCC::Contentful::Link
4
+ attr_reader :id
5
+ attr_reader :link_type
6
+ attr_reader :raw
7
+
8
+ LINK_TYPES = {
9
+ Asset: 'Asset',
10
+ Link: 'Entry'
11
+ }.freeze
12
+
13
+ def initialize(model, link_type = nil)
14
+ @id = model.try(:id) || model
15
+ @link_type = link_type
16
+ @link_type ||= model.is_a?(WCC::Contentful::Model::Asset) ? :Asset : :Link
17
+ @raw =
18
+ {
19
+ 'sys' => {
20
+ 'type' => 'Link',
21
+ 'linkType' => LINK_TYPES[@link_type] || link_type,
22
+ 'id' => @id
23
+ }
24
+ }
25
+ end
26
+
27
+ alias_method :to_h, :raw
28
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The LinkVisitor is a utility class for walking trees of linked entries.
4
+ # It is used internally by the Store layer to compose the resulting resolved hashes.
5
+ # But you can use it too!
6
+ class WCC::Contentful::LinkVisitor
7
+ attr_reader :entry
8
+ attr_reader :type
9
+ attr_reader :fields
10
+ attr_reader :depth
11
+
12
+ # @param [Hash] entry The entry hash (resolved or unresolved) to walk
13
+ # @param [Array<String, Symbol>] The fields to select from the entry tree.
14
+ # Use `:Link` to select only links, or `'slug'` to select all slugs in the tree.
15
+ # @param [Fixnum] depth (optional) How far to walk down the tree of links. Be careful of
16
+ # recursive trees!
17
+ # @example
18
+ # entry = store.find_by(id: id, include: 3)
19
+ # WCC::Contentful::LinkVisitor.new(entry, 'slug', depth: 3)
20
+ # .map { |slug| 'https://mirror-site' + slug }
21
+ def initialize(entry, *fields, depth: 0)
22
+ unless entry.is_a?(Hash) && entry.dig('sys', 'id')
23
+ raise ArgumentError, "Please provide an entry as a hash value (got #{entry})"
24
+ end
25
+ unless ct = entry.dig('sys', 'contentType', 'sys', 'id')
26
+ raise ArgumentError, 'Entry has no content type!'
27
+ end
28
+
29
+ @type = WCC::Contentful.types[ct]
30
+ raise ArgumentError, "Unknown content type '#{ct}'" unless @type
31
+
32
+ @entry = entry
33
+ @fields = fields
34
+ @depth = depth
35
+ end
36
+
37
+ # Walks an entry and its resolved links, without transforming the entry.
38
+ # @yield [value, field, locale]
39
+ # @yieldparam [Object] value The value of the selected field.
40
+ # @yieldparam [WCC::Contentful::IndexedRepresentation::Field] field The type of the selected field
41
+ # @yieldparam [String] locale The locale of the current field value
42
+ # @returns nil
43
+ def each(&block)
44
+ _each do |val, field, locale, index|
45
+ yield(val, field, locale, index) if should_yield_field?(field)
46
+
47
+ next unless should_walk_link?(field, val)
48
+
49
+ self.class.new(val, *fields, depth: depth - 1).each(&block)
50
+ end
51
+
52
+ nil
53
+ end
54
+
55
+ def map!(&block)
56
+ _each do |val, field, locale, index|
57
+ if should_yield_field?(field)
58
+ val = yield(val, field, locale, index)
59
+ set_field(field, locale, index, val)
60
+ end
61
+
62
+ next unless should_walk_link?(field, val)
63
+
64
+ self.class.new(val, *fields, depth: depth - 1).map!(&block)
65
+ end
66
+
67
+ entry
68
+ end
69
+
70
+ private
71
+
72
+ def _each(&block)
73
+ type.fields.each_value do |f|
74
+ each_field(f, &block)
75
+ end
76
+ end
77
+
78
+ def each_field(field)
79
+ each_locale(field) do |val, locale|
80
+ if field.array
81
+ val&.each_with_index do |v, index|
82
+ yield(v, field, locale, index) unless v.nil?
83
+ end
84
+ else
85
+ yield(val, field, locale) unless val.nil?
86
+ end
87
+ end
88
+ end
89
+
90
+ def each_locale(field)
91
+ raw_value = entry.dig('fields', field.name)
92
+ if locale = entry.dig('sys', 'locale')
93
+ if raw_value.is_a?(Hash) && raw_value[locale]
94
+ # it's a locale=* entry, but they've added sys.locale to those now
95
+ raw_value = raw_value[locale]
96
+ end
97
+ yield(raw_value, locale)
98
+ else
99
+ raw_value&.each_with_object({}) do |(l, val), h|
100
+ h[l] = yield(val, l)
101
+ end
102
+ end
103
+ end
104
+
105
+ def should_yield_field?(field)
106
+ fields.empty? || fields.include?(field.type) || fields.include?(field.name)
107
+ end
108
+
109
+ def should_walk_link?(field, val, dep = depth)
110
+ dep > 0 && field.type == :Link && val.dig('sys', 'type') == 'Entry'
111
+ end
112
+
113
+ def set_field(field, locale, index, val)
114
+ current_field = (entry['fields'][field.name] ||= {})
115
+
116
+ if field.array
117
+ (current_field[locale] ||= [])[index] = val
118
+ else
119
+ current_field[locale] = val
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::Contentful::Middleware
4
+ end
5
+
6
+ require_relative './middleware/store'
7
+ # someday: ./middleware/client & ./middleware/model ?
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../middleware'
4
+
5
+ # A Store middleware wraps the Store interface to perform any desired transformations
6
+ # on the Contentful entries coming back from the store. A Store middleware must
7
+ # implement the Store interface as well as a `store=` attribute writer, which is
8
+ # used to inject the next store or middleware in the chain.
9
+ #
10
+ # The Store interface can be seen on the WCC::Contentful::Store::Base class. It
11
+ # consists of the `#find, #find_by, #find_all, #set, #delete,` and `#index` methods.
12
+ #
13
+ # Including this concern will define those methods to pass through to the next store.
14
+ # Any of those methods can be overridden on the implementing middleware.
15
+ # It will also expose two overridable methods, `#select?` and `#transform`. These
16
+ # methods are applied when reading values out of the store, and can be used to
17
+ # apply a filter or transformation to each entry in the store.
18
+ module WCC::Contentful::Middleware::Store
19
+ extend ActiveSupport::Concern
20
+ include WCC::Contentful::Store::Interface
21
+
22
+ attr_accessor :store
23
+
24
+ delegate :index, :index?, to: :store
25
+
26
+ class_methods do
27
+ def call(store, *content_delivery_params, **_)
28
+ instance = new(*content_delivery_params)
29
+ instance.store = store
30
+ instance
31
+ end
32
+ end
33
+
34
+ def find(id, **options)
35
+ found = store.find(id, **options)
36
+ return transform(found) if found && (!has_select? || select?(found))
37
+ end
38
+
39
+ def find_by(options: nil, **args)
40
+ result = store.find_by(**args.merge(options: options))
41
+ return unless result && (!has_select? || select?(result))
42
+
43
+ result = resolve_includes(result, options[:include]) if options && options[:include]
44
+ transform(result)
45
+ end
46
+
47
+ def find_all(options: nil, **args)
48
+ DelegatingQuery.new(
49
+ store.find_all(**args.merge(options: options)),
50
+ middleware: self,
51
+ options: options
52
+ )
53
+ end
54
+
55
+ def resolve_includes(entry, depth)
56
+ return entry unless entry && depth && depth > 0
57
+
58
+ WCC::Contentful::LinkVisitor.new(entry, :Link, depth: depth).map! do |val|
59
+ resolve_link(val)
60
+ end
61
+ end
62
+
63
+ def resolve_link(val)
64
+ return val unless resolved_link?(val)
65
+
66
+ if !has_select? || select?(val)
67
+ transform(val)
68
+ else
69
+ # Pretend it's an unresolved link -
70
+ # matches the behavior of a store when the link cannot be retrieved
71
+ WCC::Contentful::Link.new(val.dig('sys', 'id'), val.dig('sys', 'type')).to_h
72
+ end
73
+ end
74
+
75
+ def resolved_link?(value)
76
+ value.is_a?(Hash) && value.dig('sys', 'type') == 'Entry'
77
+ end
78
+
79
+ def has_select? # rubocop:disable Naming/PredicateName
80
+ respond_to?(:select?)
81
+ end
82
+
83
+ # The default version of `#transform` just returns the entry.
84
+ # Override this with your own implementation.
85
+ def transform(entry)
86
+ entry
87
+ end
88
+
89
+ class DelegatingQuery
90
+ include WCC::Contentful::Store::Query::Interface
91
+ include Enumerable
92
+
93
+ # by default all enumerable methods delegated to the to_enum method
94
+ delegate(*(Enumerable.instance_methods - Module.instance_methods), to: :to_enum)
95
+
96
+ def count
97
+ if middleware.has_select?
98
+ raise NameError, "Count cannot be determined because the middleware '#{middleware}'" \
99
+ " implements the #select? method. Please use '.to_a.count' to count entries that" \
100
+ ' pass the #select? method.'
101
+ end
102
+
103
+ # The wrapped query may get count from the "Total" field in the response,
104
+ # or apply a "COUNT(*)" to the query.
105
+ wrapped_query.count
106
+ end
107
+
108
+ attr_reader :wrapped_query, :middleware, :options
109
+
110
+ def to_enum
111
+ result = wrapped_query.to_enum
112
+ result = result.select { |x| middleware.select?(x) } if middleware.has_select?
113
+
114
+ if options && options[:include]
115
+ result = result.map { |x| middleware.resolve_includes(x, options[:include]) }
116
+ end
117
+
118
+ result.map { |x| middleware.transform(x) }
119
+ end
120
+
121
+ def apply(filter, context = nil)
122
+ self.class.new(
123
+ wrapped_query.apply(filter, context),
124
+ middleware: middleware,
125
+ options: options,
126
+ **@extra
127
+ )
128
+ end
129
+
130
+ def apply_operator(operator, field, expected, context = nil)
131
+ self.class.new(
132
+ wrapped_query.apply_operator(operator, field, expected, context),
133
+ middleware: middleware,
134
+ options: options,
135
+ **@extra
136
+ )
137
+ end
138
+
139
+ WCC::Contentful::Store::Query::Interface::OPERATORS.each do |op|
140
+ # @see #apply_operator
141
+ define_method(op) do |field, expected, context = nil|
142
+ self.class.new(
143
+ wrapped_query.public_send(op, field, expected, context),
144
+ middleware: middleware,
145
+ options: options,
146
+ **@extra
147
+ )
148
+ end
149
+ end
150
+
151
+ def initialize(wrapped_query, middleware:, options: nil, **extra)
152
+ @wrapped_query = wrapped_query
153
+ @middleware = middleware
154
+ @options = options
155
+ @extra = extra
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::Contentful::Middleware::Store
4
+ class CachingMiddleware
5
+ include WCC::Contentful::Middleware::Store
6
+ # include instrumentation, but not specifically store stack instrumentation
7
+ include WCC::Contentful::Instrumentation
8
+
9
+ attr_accessor :expires_in
10
+
11
+ def initialize(cache = nil)
12
+ @cache = cache || ActiveSupport::Cache::MemoryStore.new
13
+ @expires_in = nil
14
+ end
15
+
16
+ def find(key, **options)
17
+ event = 'fresh'
18
+ found =
19
+ @cache.fetch(key, expires_in: expires_in) do
20
+ event = 'miss'
21
+ # if it's not a contentful ID don't hit the API.
22
+ # Store a nil object if we can't find the object on the CDN.
23
+ (store.find(key, options) || nil_obj(key)) if key =~ /^\w+$/
24
+ end
25
+ _instrument(event, key: key, options: options)
26
+
27
+ case found.try(:dig, 'sys', 'type')
28
+ when 'Nil', 'DeletedEntry', 'DeletedAsset'
29
+ nil
30
+ else
31
+ found
32
+ end
33
+ end
34
+
35
+ # TODO: https://github.com/watermarkchurch/wcc-contentful/issues/18
36
+ # figure out how to cache the results of a find_by query, ex:
37
+ # `find_by('slug' => '/about')`
38
+ def find_by(content_type:, filter: nil, options: nil)
39
+ if filter&.keys == ['sys.id']
40
+ # Direct ID lookup, like what we do in `WCC::Contentful::ModelMethods.resolve`
41
+ # We can return just this item. Stores are not required to implement :include option.
42
+ if found = @cache.read(filter['sys.id'])
43
+ return found
44
+ end
45
+ end
46
+
47
+ store.find_by(content_type: content_type, filter: filter, options: options)
48
+ end
49
+
50
+ delegate :find_all, to: :store
51
+
52
+ # #index is called whenever the sync API comes back with more data.
53
+ def index(json)
54
+ delegated_result = store.index(json) if store.index?
55
+ caching_result = _index(json)
56
+ # _index returns nil if we don't already have it cached - so use the store result.
57
+ # store result is nil if it doesn't index, so use the caching result if we have it.
58
+ # They ought to be the same thing if it's cached and the store also indexes.
59
+ caching_result || delegated_result
60
+ end
61
+
62
+ def index?
63
+ true
64
+ end
65
+
66
+ private
67
+
68
+ LAZILY_CACHEABLE_TYPES = %w[
69
+ Entry
70
+ Asset
71
+ DeletedEntry
72
+ DeletedAsset
73
+ ].freeze
74
+
75
+ def _index(json)
76
+ ensure_hash(json)
77
+ id = json.dig('sys', 'id')
78
+ type = json.dig('sys', 'type')
79
+ prev = @cache.read(id)
80
+ return if prev.nil? && LAZILY_CACHEABLE_TYPES.include?(type)
81
+
82
+ if (prev_rev = prev&.dig('sys', 'revision')) && (next_rev = json.dig('sys', 'revision'))
83
+ return prev if next_rev < prev_rev
84
+ end
85
+
86
+ # we also set DeletedEntry objects in the cache - no need to go hit the API when we know
87
+ # this is a nil object
88
+ @cache.write(id, json, expires_in: expires_in)
89
+
90
+ case type
91
+ when 'DeletedEntry', 'DeletedAsset'
92
+ _instrument 'delete', id: id
93
+ nil
94
+ else
95
+ _instrument 'set', id: id
96
+ json
97
+ end
98
+ end
99
+
100
+ def nil_obj(id)
101
+ {
102
+ 'sys' => {
103
+ 'id' => id,
104
+ 'type' => 'Nil',
105
+ 'revision' => 1
106
+ }
107
+ }
108
+ end
109
+
110
+ def ensure_hash(val)
111
+ raise ArgumentError, 'Value must be a Hash' unless val.is_a?(Hash)
112
+ end
113
+ end
114
+ end