lib/wcc/contentful/store/base.rb in wcc-contentful-0.2.2 vs lib/wcc/contentful/store/base.rb in wcc-contentful-0.3.0.pre.rc
- old
+ new
@@ -1,22 +1,39 @@
-
# frozen_string_literal: true
+# @api Store
module WCC::Contentful::Store
+ # This is the base class for stores which implement #index, and therefore
+ # must be kept up-to-date via the Sync API.
+ # @abstract At a minimum subclasses should override {#find}, {#find_all}, {#set},
+ # and #{delete}. As an alternative to overriding set and delete, the subclass
+ # can override {#index}. Index is called when a webhook triggers a sync, to
+ # update the store.
class Base
+ # Finds an entry by it's ID. The returned entry is a JSON hash
+ # @abstract Subclasses should implement this at a minimum to provide data
+ # to the WCC::Contentful::Model API.
def find(_id)
raise NotImplementedError, "#{self.class} does not implement #find"
end
+ # Sets the value of the entry with the given ID in the store.
+ # @abstract
def set(_id, _value)
raise NotImplementedError, "#{self.class} does not implement #set"
end
+ # Removes the entry by ID from the store.
+ # @abstract
def delete(_id)
raise NotImplementedError, "#{self.class} does not implement #delete"
end
+ # Processes a data point received via the Sync API. This can be a published
+ # entry or asset, or a 'DeletedEntry' or 'DeletedAsset'. The default
+ # implementation calls into #set and #delete to perform the appropriate
+ # operations in the store.
def index(json)
# Subclasses can override to do this in a more performant thread-safe way.
# Example: postgres_store could do this in a stored procedure for speed
mutex.with_write_lock do
prev =
@@ -41,49 +58,152 @@
json
end
end
end
- def find_by(content_type:, filter: nil)
+ # Finds the first entry matching the given filter. A content type is required.
+ #
+ # @param [String] content_type The ID of the content type to search for.
+ # @param [Hash] filter A set of key-value pairs defining filter operations.
+ # See WCC::Contentful::Store::Base::Query
+ # @param [Hash] options An optional set of additional parameters to the query
+ # defining for example include depth. Not all store implementations respect all options.
+ def find_by(content_type:, filter: nil, options: nil)
# default implementation - can be overridden
- q = find_all(content_type: content_type)
+ q = find_all(content_type: content_type, options: { limit: 1 }.merge!(options || {}))
q = q.apply(filter) if filter
q.first
end
+ # Finds all entries of the given content type. A content type is required.
+ #
+ # @abstract Subclasses should implement this at a minimum to provide data
+ # to the {WCC::Contentful::Model} API.
+ # @param [String] content_type The ID of the content type to search for.
+ # @param [Hash] options An optional set of additional parameters to the query
+ # defining for example include depth. Not all store implementations respect all options.
+ # @return [Query] A query object that exposes methods to apply filters
# rubocop:disable Lint/UnusedMethodArgument
- def find_all(content_type:)
+ def find_all(content_type:, options: nil)
raise NotImplementedError, "#{self.class} does not implement find_all"
end
# rubocop:enable Lint/UnusedMethodArgument
def initialize
@mutex = Concurrent::ReentrantReadWriteLock.new
end
+ def ensure_hash(val)
+ raise ArgumentError, 'Value must be a Hash' unless val.is_a?(Hash)
+ end
+
protected
attr_reader :mutex
+ # The base class for query objects returned by find_all. Subclasses should
+ # override the #result method to return an array-like containing the query
+ # results.
class Query
delegate :first, to: :result
delegate :map, to: :result
delegate :count, to: :result
+ OPERATORS = %i[
+ eq
+ ne
+ all
+ in
+ nin
+ exists
+ lt
+ lte
+ gt
+ gte
+ query
+ match
+ ].freeze
+
+ # @abstract Subclasses should provide this in order to fetch the results
+ # of the query.
def result
raise NotImplementedError
end
+ def initialize(store)
+ @store = store
+ end
+
+ # @abstract Subclasses can either override this method to properly respond
+ # to find_by query objects, or they can define a method for each supported
+ # operator. Ex. `#eq`, `#ne`, `#gt`.
+ def apply_operator(operator, field, expected, context = nil)
+ respond_to?(operator) ||
+ raise(ArgumentError, "Operator not implemented: #{operator}")
+
+ public_send(operator, field, expected, context)
+ end
+
+ # Called with a filter object by {Base#find_by} in order to apply the filter.
def apply(filter, context = nil)
filter.reduce(self) do |query, (field, value)|
if value.is_a?(Hash)
- k = value.keys.first
- raise ArgumentError, "Filter not implemented: #{value}" unless query.respond_to?(k)
- query.public_send(k, field, value[k], context)
+ if op?(k = value.keys.first)
+ query.apply_operator(k.to_sym, field.to_s, value[k], context)
+ else
+ query.nested_conditions(field, value, context)
+ end
else
- query.eq(field.to_s, value)
+ query.apply_operator(:eq, field.to_s, value)
end
end
+ end
+
+ protected
+
+ # naive implementation recursively descends the graph to turns links into
+ # the actual entry data. This calls {Base#find} for each link and so it is
+ # very inefficient.
+ #
+ # @abstract Override this to provide a more efficient implementation for
+ # a given store.
+ def resolve_includes(entry, depth)
+ return entry unless entry && depth && depth > 0 && fields = entry['fields']
+
+ fields.each do |(_name, locales)|
+ # TODO: handle non-* locale
+ locales.each do |(locale, val)|
+ locales[locale] =
+ if val.is_a? Array
+ val.map { |e| resolve_link(e, depth) }
+ else
+ resolve_link(val, depth)
+ end
+ end
+ end
+
+ entry
+ end
+
+ def resolve_link(val, depth)
+ return val unless val.is_a?(Hash) && val.dig('sys', 'type') == 'Link'
+ return val unless included = @store.find(val.dig('sys', 'id'))
+
+ resolve_includes(included, depth - 1)
+ end
+
+ private
+
+ def op?(key)
+ OPERATORS.include?(key.to_sym)
+ end
+
+ def sys?(field)
+ field.to_s =~ /sys\./
+ end
+
+ def id?(field)
+ field.to_sym == :id
end
end
end
end