module Caprese::Persistence
Public Instance Methods
Creates a new record of whatever type a given controller manages
@note For this action to succeed, the given controller must define `create_params`
@see #create_params
# File lib/caprese/controller/concerns/persistence.rb, line 39 def create fail_on_type_mismatch(data_params[:type]) record = queried_record_scope.build assign_changes_from_document(record, data_params, permitted_params_for(:create)) execute_after_initialize_callbacks(record) execute_before_create_callbacks(record) execute_before_save_callbacks(record) fail RecordInvalidError.new(record, engaged_field_aliases) if record.errors.any? record.save! persist_collection_relationships(record) execute_after_create_callbacks(record) execute_after_save_callbacks(record) render( json: record, status: :created, fields: query_params[:fields], include: query_params[:include] ) end
Destroys a record of whatever type a given controller manages
-
Execute any before_destroy callbacks, with the record to be destroyed passed in
-
Destroy the record, ensuring that it checks the model for dependencies before doing so
-
Execute any after_destroy callbacks, with the destroyed resource passed in
-
Return 204 No Content if the record was successfully deleted
# File lib/caprese/controller/concerns/persistence.rb, line 99 def destroy execute_before_destroy_callbacks(queried_record) queried_record.destroy! execute_after_destroy_callbacks(queried_record) head :no_content end
Updates a record of whatever type a given controller manages
@note For this action to succeed, the given controller must define `update_params`
@see #update_params
# File lib/caprese/controller/concerns/persistence.rb, line 71 def update fail_on_type_mismatch(data_params[:type]) assign_changes_from_document(queried_record, data_params, permitted_params_for(:update)) execute_before_update_callbacks(queried_record) execute_before_save_callbacks(queried_record) fail RecordInvalidError.new(queried_record, engaged_field_aliases) if queried_record.errors.any? queried_record.save! execute_after_update_callbacks(queried_record) execute_after_save_callbacks(queried_record) render( json: queried_record, fields: query_params[:fields], include: query_params[:include] ) end
Private Instance Methods
Builds permitted attributes and relationships into the queried record
@example
params = { type: 'orders', attributes: { price: '...', other: '...' }, relationships: { product: { data: { token: 'asj38k', type: 'products' } } } } create_params => [:price] assign_changes_from_document(record, params, create_params) => { price: '...', product: Product<@token='asj38k'> }
@example
params = { type: 'orders', attributes: { price: '...', other: '...' }, relationships: { order_items: { data: [{ type: 'order_items', attributes: { title: 'An order item', amount: 5.0, tax: 0.0 } }] } } } create_params => [:price, order_items: [:title, :amount, :tax]] assign_changes_from_document(record, params, create_params) # => { price: '...', order_items: [OrderItem<@token=nil,@title='An order item',@amount=5.0,@tax=0.0>] }
@param [ActiveRecord::Base] record the record to build attribute into @param [Parameters] data the data sent to the server to construct and assign to the record @param [Array] permitted_params the permitted params for the action @option [String] parent_relationship_name the parent relationship assigning these attributes to the record, used to determine
engaged aliases @see concerns/aliasing
# File lib/caprese/controller/concerns/persistence.rb, line 226 def assign_changes_from_document(record, data, permitted_params = [], parent_relationship_name: nil) # TODO: Make safe by enforcing that only a single alias/unalias can be engaged at once aliases_document = if parent_relationship_name engaged_field_aliases[parent_relationship_name] ||= {} else engaged_field_aliases end if data[:attributes] assign_fields_to_record record, extract_attributes_from_document( record, data[:attributes], permitted_params, aliases_document ) end if data[:relationships] collection_relationships, singular_relationships = flattened_keys_for(permitted_params) .select { |k| begin record.association(actual_field(k, record.class)) rescue ActiveRecord::AssociationNotFoundError false end } .partition { |k| record.association(actual_field(k, record.class)).reflection.collection? } .map { |s| s.map { |r| permitted_params.include?(r) ? r : { r => nested_params_for(r, permitted_params) } } } assign_fields_to_record record, extract_relationships_from_document( record, data[:relationships], singular_relationships, aliases_document ) assign_fields_to_record record, extract_relationships_from_document( record, data[:relationships], collection_relationships, aliases_document ) end end
Assigns fields to the record conditionally based on whether or not assign_attributes is available @note Allows non-ActiveRecord models to be handled
@param [ActiveRecord::Base,Struct] record the record to assign fields to @param [Hash] fields the fields to assign to the record
# File lib/caprese/controller/concerns/persistence.rb, line 280 def assign_fields_to_record(record, fields) if record.respond_to?(:assign_attributes) record.assign_attributes(fields) else fields.each { |k, v| record.send("#{k}=", v) } end end
Indicates whether or not :attributes or :relationships keys are in a resource identifier, thus allowing us to construct this data into the final record
@param [Hash] resource_identifier the resource identifier to check for constructable data in @return [Boolean] whether or not the resource identifier contains constructable data
# File lib/caprese/controller/concerns/persistence.rb, line 397 def contains_constructable_data?(resource_identifier) [:attributes, :relationships].any? { |k| resource_identifier.key?(k) } end
Requires the data param standard to JSON API
@return [StrongParameters] the strong params in the `data` object param
# File lib/caprese/controller/concerns/persistence.rb, line 112 def data_params params.require('data') end
Builds an object of attributes to assign to a record, based on a document
@param [ActiveRecord] record the record corresponding to the data document @param [Parameters] data the document to extract attributes from @param [Array<Symbol,Hash>] permitted_params the permitted attributes that can be assigned through this controller @param [Hash] aliases_document the aliases document reflects usage of aliases in the data document @return [Hash] the object of attributes to assign to the record
# File lib/caprese/controller/concerns/persistence.rb, line 295 def extract_attributes_from_document(record, data, permitted_params, aliases_document) data.permit(*permitted_params).each_with_object({}) do |(attribute_name, val), attributes| attribute_name = attribute_name.to_sym actual_attribute_name = actual_field(attribute_name, record.class) if attribute_name != actual_attribute_name aliases_document[attribute_name] = true end attributes[actual_attribute_name] = val end end
Builds an object of relationships to assign to a record, based on a document
@param [ActiveRecord] record the record corresponding to the data document @param [Parameters] data the document to extract relationships from @param [Array<Symbol,Hash>] permitted_relationships the permitted relationships that can be assigned through this controller @param [Hash] aliases_document the aliases document reflects usage of aliases in the data document @return [Hash] the object of relationships to assign to the record
# File lib/caprese/controller/concerns/persistence.rb, line 315 def extract_relationships_from_document(record, data, permitted_relationships, aliases_document) data .slice(*flattened_keys_for(permitted_relationships)) .each_with_object({}) do |(relationship_name, relationship_data), relationships| relationship_name = relationship_name.to_sym actual_relationship_name = actual_field(relationship_name, record.class) if relationship_name != actual_relationship_name aliases_document[relationship_name] = {} end begin raise RequestDocumentInvalidError.new(field: :base) unless relationship_data.has_key?(:data) relationship_result = records_for_relationship( record, nested_params_for(relationship_name, permitted_relationships), relationship_name, relationship_data[:data] ) reflection = record.association(actual_relationship_name).reflection if (reflection.collection? && !relationship_result.is_a?(Array)) || (!reflection.collection? && relationship_result.is_a?(Array)) raise RequestDocumentInvalidError.new(field: :base) end if record.persisted? && reflection.collection? && (inverse_reflection = record.class.reflect_on_association(actual_relationship_name).inverse_of) relationship_result.each { |r| r.send("#{inverse_reflection.name}=", record) } invalid_results = relationship_result.reject(&:valid?) raise RecordInvalidError.new(invalid_results.first) if invalid_results.any? end relationships[actual_relationship_name] = relationship_result rescue Caprese::RecordNotFoundError => e record.errors.add(relationship_name, :not_found, t: e.t.slice(:value)) rescue RecordInvalidError => e propagate_errors_to_parent( record, relationship_name, e.record.errors.to_a ) rescue RequestDocumentInvalidError => e propagate_errors_to_parent( record, relationship_name, [e] ) end end end
Flattens an array of the top level keys for a given set of params
@example
create_params => [:body, user: [:name], post: [:title]] flattened_keys_for(create_params) => [:body, :user, :post]
@param [Array] params the params to flatten keys for @return [Array] the flattened array of keys for the action params
# File lib/caprese/controller/concerns/persistence.rb, line 169 def flattened_keys_for(params) params.map do |p| if p.is_a?(Hash) p.keys else p end end.flatten end
Gets a set of nested params in an action_params definition
@example
create_params => [:body, user: [:name, :email]] nested_params_for(user, create_params) => [:name, :email]
@param [Symbol] key the key of the nested params @param [Array] params the params to search for the key in @return [Array,Nil] the nested params for a given key
# File lib/caprese/controller/concerns/persistence.rb, line 156 def nested_params_for(key, params) key = key.to_sym params.detect { |p| p.is_a?(Hash) && p.has_key?(key) }.try(:[], key) end
An array of symbols stating params that are permitted for a create
action
for a record
@note Abstract function, must be overridden by every controller
@return [Array] a list of params permitted to create a record of whatever type
a given controller manages
# File lib/caprese/controller/concerns/persistence.rb, line 123 def permitted_create_params fail NotImplementedError end
Gets the permitted params for a given action (create, update)
@param [Symbol] action the action to get permitted params for @return [Array] the permitted params for a given action
# File lib/caprese/controller/concerns/persistence.rb, line 142 def permitted_params_for(action) send("permitted_#{action}_params") end
An array of symbols stating params that are permitted for a update
action
for a record
@note Abstract function, must be overridden by every controller
@return [Array] a list of params permitted to update a record of whatever type
a given controller manages
# File lib/caprese/controller/concerns/persistence.rb, line 134 def permitted_update_params fail NotImplementedError end
Called in create, after the record is saved. When creating a new record, and assigning to it existing has_many association relation, the records in the relation will be pushed onto the appropriate target, but the relationship will not be persisted in their attributes until their owner is saved.
This methods persists the collection relation(s) pushed onto the record's association target(s)
# File lib/caprese/controller/concerns/persistence.rb, line 422 def persist_collection_relationships(record) record.class.reflect_on_all_associations .select { |ref| ref.collection? && !ref.through_reflection && record.association(ref.name).any? } .map do |ref| [ ref.has_inverse? ? ref.inverse_of.name : ref.options[:as], record.association(ref.name).target ] end .to_h.each do |name, targets| targets.each { |t| t.update name => record } end end
Propagates errors to parent with nested field name
@param [ActiveRecord] parent the parent to propagate errors to @param [String] relationship_name the name to use when nesting the errors @param [Array<Error>] errors the errors to propagate
# File lib/caprese/controller/concerns/persistence.rb, line 406 def propagate_errors_to_parent(parent, relationship_name, errors) errors.each do |error| parent.errors.add( error.field == :base ? relationship_name : "#{relationship_name}.#{error.field}", error.code, t: error.t.except(:field, :field_title) ) end end
Gets all the records for a relationship given a relationship data definition
@param [ActiveRecord] owner the owner of the relationship @param [Array] permitted_params the permitted params for the @param [String] relationship_name the name of the relationship to get records for @param [Hash,Array<Hash>] relationship_data the resource identifier data to use to find/build records @return [ActiveRecord,Array<ActiveRecord>] the record(s) for the relationship
# File lib/caprese/controller/concerns/persistence.rb, line 377 def records_for_relationship(owner, permitted_params, relationship_name, relationship_data) result = Array.wrap(relationship_data).map do |relationship_data_item| ref = record_for_resource_identifier(relationship_data_item) if ref && contains_constructable_data?(relationship_data_item) assign_changes_from_document(ref, relationship_data_item, permitted_params, parent_relationship_name: relationship_name) propagate_errors_to_parent(owner, relationship_name, ref.errors.to_a) if ref.errors.any? end ref end relationship_data.is_a?(Array) && result || result.first end