Module: Caprese::Relationships

Extended by:
ActiveSupport::Concern
Included in:
Controller
Defined in:
lib/caprese/controller/concerns/relationships.rb

Instance Method Summary collapse

Instance Method Details

#get_relationship_dataObject

Note:

Adds a links link to this endpoint itself, to be JSON API compliant

Note:

When returning single resource, adds a related endpoint URL that points to the root resource URL

Retrieves the data for a relationship, not just the definition/resource identifier

@note Resource Identifier = { id: '...', type: '....' }
@note Resource = Resource Identifier + { attributes: { ... } }

GET /api/v1/:controller/:id/:relationship(/:relation_primary_key_value)

Examples:

Order<token: ‘asd27h’> with product

links[:self] = 'http://www.example.com/api/v1/orders/asd27h/product'
links[:related] = 'http://www.example.com/api/v1/products/h45sql'

Order<token: ‘asd27h’> with transactions

links[:self] = 'http://www.example.com/api/v1/orders/asd27h/transactions/7ytr4l'
links[:related] = 'http://www.example.com/api/v1/transactions/7ytr4l'


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/caprese/controller/concerns/relationships.rb', line 63

def get_relationship_data
  target =
    if queried_association.reflection.collection?
      scope = relationship_scope(params[:relationship].to_sym, queried_association.reader)

      if params[:relation_primary_key_value].present?
        get_record!(scope, self.class.config.resource_primary_key, params[:relation_primary_key_value])
      else
        apply_sorting_pagination_to_scope(scope)
      end
    else
      queried_association.reader
    end

  links = { self: request.original_url }

  if target.respond_to?(:to_ary)
    serializer_type = :each_serializer
  else
    serializer_type = :serializer

    if url_helpers.respond_to?(related_url = version_name("#{params[:relationship].singularize}_url"))
      links[:related] =
        url_helpers.send(
          related_url,
          target.read_attribute(self.config.resource_primary_key),
          host: caprese_default_url_options_host
        )
    end
  end

  render(
    serializer_type => relationship_serializer(params[:relationship].to_sym),
    json: target,
    fields: query_params[:fields],
    include: query_params[:include],
    links: links
  )
end

#get_relationship_definitionObject

Returns relationship data for a resource

  1. Find resource we are updating relationship for

  2. Check relationship exists *or respond with error*

  3. Add self/related links for relationship

  4. Respond with relationship data

GET /api/v1/:controller/:id/relationships/:relationship

Examples:

to-one relationship

GET /orders/asd27h/relationships/product

{
  "links": {
    "self": "/orders/asd27h/relationships/product",
    "related": "orders/asd27h/product"
  },
  "data": {
    "type": "products",
    "token": "hy7sql"
  }
}

to-many relationship

GET /orders/1/relationships/transactions

{
  "links": {
    "self": "/orders/asd27h/relationships/transactions",
    "related": "orders/asd27h/transactions"
  },
  "data": [
    { "type": "transactions", "token": "hy7sql" },
    { "type": "transactions", "token": "lki26s" },
  ]
}


139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/caprese/controller/concerns/relationships.rb', line 139

def get_relationship_definition
  links = { self: request.original_url }

  # Add related link for this relationship if it exists
  if url_helpers
    .respond_to?(related_path = "relationship_data_#{version_name(unnamespace(params[:controller]).singularize)}_url")

    links[:related] = url_helpers.send(
      related_path,
      params[:id],
      params[:relationship],
      host: caprese_default_url_options_host
    )
  end

  target = queried_association.reader

  if queried_association.reflection.collection?
    target = relationship_scope(params[:relationship], target)
  end

  render(
    json: target,
    fields: {},
    include: query_params[:include],
    links: links
  )
end

#relationship_scope(name, scope) ⇒ Object

Note:

Can be overridden to customize scoping at a per-relationship level

Applies further scopes to a collection association

Examples:

def relationship_scope(name, scope)
  case name
  when :transactions
    scope.by_merchant(...)
  when :orders
    scope.by_user(...)
  end
end

Parameters:

  • name (String)

    the name of the association

  • scope (Relation)

    the scope corresponding to a collection association



23
24
25
# File 'lib/caprese/controller/concerns/relationships.rb', line 23

def relationship_scope(name, scope)
  scope
end

#relationship_serializer(name) ⇒ Serializer, Nil

Note:

Returns nil by default because Caprese::Controller#render will determine the serializer if nil

Allows selection of serializer for any relationship serialized by get_relationship_data

Examples:

def relationship_serializer(name)
  case name
  when :answer
    AnswerSerializer
  else
    super
  end
end

Parameters:

  • name (String)

    the name of the relationship

Returns:

  • (Serializer, Nil)

    the serializer for the relationship or nil if none specified



42
43
44
# File 'lib/caprese/controller/concerns/relationships.rb', line 42

def relationship_serializer(name)
  nil
end

#update_relationship_definitionObject

Updates a relationship for a resource

  1. Find resource we are updating relationship for

  2. Check relationship exists *or respond with error*

  3. Find each potential relationship resource corresponding to the resource identifiers passed in

  4. Modify relationship based on relationship type (one-to-many, one-to-one) and HTTP verb (PATCH, POST, DELETE)

  5. Check if update was successful

* If successful, return 204 No Content
* If unsuccessful, return 403 Forbidden

PATCH/POST/DELETE /api/v1/:controller/:id/relationships/:relationship

Examples:

modify to-one relationship

PATCH /orders/asd27h/relationships/product

{
  "data": { "type": "products", "token": "hy7sql" }
}

clear to-one relationship

PATCH /orders/asd27h/relationships/product

{
  "data": null
}

modify to-many relationship

PATCH /orders/asd27h/relationships/transactions

{
  "data": [
    { "type": "transactions", "token": "hy7sql" },
    { "type": "transactions", "token": "lki26s" },
  ]
}

clear to to-many relationship

PATCH /orders/asd27h/relationships/transactions

{
  "data": []
}

append to to-many relationship

POST /orders/asd27h/relationships/transactions

{
  "data": [
    { "type": "transactions", "token": "hy7sql" },
    { "type": "transactions", "token": "lki26s" },
  ]
}

remove from to-many relationship

DELETE /orders/asd27h/relationships/transactions

{
  "data": [
    { "type": "transactions", "token": "hy7sql" },
    { "type": "transactions", "token": "lki26s" },
  ]
}


230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/caprese/controller/concerns/relationships.rb', line 230

def update_relationship_definition
  successful = false

  if queried_association &&
    flattened_keys_for(permitted_params_for(:update)).include?(params[:relationship].to_sym)
    relationship_name = queried_association.reflection.name

    relationship_resources = []
    begin
      relationship_resources = Array.wrap(records_for_relationship(
        queried_record,
        [],
        relationship_name,
        data_params
      ))
    rescue ActionController::ParameterMissing => e
      # Only PATCH requests are allowed to have no :data (when clearing relationship)
      raise e unless request.patch?
    rescue Caprese::RecordNotFoundError => e
      raise RequestDocumentInvalidError.new(field: :base, code: :not_found, t: e.t.slice(:value))
    end

    # Validate that if we assign queried_record as the inverse of the relationship, the relationship records are
    # still valid
    if !request.delete? && (inverse_reflection = queried_record.class.reflect_on_association(relationship_name).inverse_of)
      relationship_resources.each { |r| r.send("#{inverse_reflection.name}=", queried_record) }
    end

    if relationship_resources.all?(&:valid?)
      successful =
        case queried_association.reflection.macro
        when :has_many
          if request.patch?
            queried_record.send("#{relationship_name}=", relationship_resources)
          elsif request.post?
            queried_record.send(relationship_name).push relationship_resources
          elsif request.delete?
            queried_record.send(relationship_name).delete relationship_resources
          end

          true
        when :has_one
          if request.patch?
            queried_record.send("#{relationship_name}=", relationship_resources[0])
            relationship_resources[0].save if relationship_resources[0].present?
          end
        when :belongs_to
          if request.patch?
            queried_record.send("#{relationship_name}=", relationship_resources[0])
            queried_record.save
          end
        end
    end
  end

  if successful
    head :no_content
  else
    fail ActionForbiddenError.new
  end
end