inaka / Dayron

A repository `similar` to Ecto.Repo that maps to an underlying http client, sending requests to an external rest api instead of a database
http://inaka.net/blog/2016/05/24/introducing-dayron/
Apache License 2.0
159 stars 20 forks source link

Add nested resources support #42

Open flaviogranero opened 8 years ago

flaviogranero commented 8 years ago

Dayron needs to support requests to nested resources, such as /posts/:id/comments

tiagoengel commented 8 years ago

So, I'm thinking in adding support for has_many, has_one and belongs_to. Here's a draft of the api.

The Dayron.Model will now support the has_many, has_one and belongs_to keys. The value of this keys would be keyword lists

defmodule MyApp.User do
  # api requests to http://api.example.com/users
  use Dayron.Model, resource: "users", has_many: comments: Comment

  # struct defining model attributes
  defstruct name: "", age: 0
end

defmodule MyApp.Comment do
  use Dayron.Model, resource: "comments", belongs_to: user: MyApp.User

  defstruct text: ""
end

Support for the ecto schema will still be the same

defmodule MyApp.User do
  use Ecto.Schema
  use Dayron.Model

  schema "users" do
    field :name, :string
    field :age, :integer, default: 0
    has_many :comments, MyApp.Comment
  end
end

defmodule MyApp.Comment do
  use Ecto.Schema
  use Dayron.Model

  schema "comments" do
    field :text, :string
    belongs_to: :user, MyApp.User
  end
end

And the api will be used like this:

RestRepo.all(post, :comments) # => get users/:id/comments
RestRepo.get(post, :comments, comment_id) #=> get users/:id/comments/:comment_id
RestRepo.update!(post, :comments, comment_id, comment_params) # => put users/:id/comments
RestRepo.insert!(post, :comments, comments_params) #=> post users/:id/comments
RestRepo.delete!(post, :comments, comment_id) #=> delete users/:id/comments/:id

For now I'm not planing to suport nested preloads like RestRepo.get(post, [:comments, :attachments], comment_id) but this can be added in the future.

Also, if the json response already have the relationships, it will be parsed normally.

GET users/1

{id: 1, name: "john doe", comments: {id: 1, text: "that's a comment"}}

The Post returned by this get will have the comment property already loaded. In this case I'm thinking in adding the RestRepo.assoc_loaded?(post, :comments) so the user can check this before sending another request.

What do you think?

tiagoengel commented 8 years ago

Thinking a little bit better, I think #43 and #44 should be done before this because for this to work we will need to support data mapping for ecto relationships.

flaviogranero commented 8 years ago

@tiagoengel I like your proposal, and also agree with other tasks dependencies. It's a good plan to check Ecto 2 changes before going with this.

tiagoengel commented 8 years ago

My initial idea for supporting relationships was to mimic the ecto relationships. This way it will be easy to maintain the support for ecto models.

The idea is to have the same functions ecto have to check the relationships.

__schema__(:associations) returns all association names. __schema__(:association, name) returns the association reflection.

I actually implemented those functions (without the reflections), you can take a look here.

An association reflection in ecto looks like this:

# BelongsTo
%Ecto.Association.BelongsTo{cardinality: :one, defaults: [], field: :company,
   on_cast: nil, on_replace: :raise, owner: App.Activity,
   owner_key: :company_id, queryable: App.Company,
   related: App.Company, related_key: :id, relationship: :parent}

# HasOne
%Ecto.Association.Has{cardinality: :one, defaults: [], field: :department,
 on_cast: nil, on_delete: :nothing, on_replace: :raise,
 owner: App.Activity, owner_key: :id,
 queryable: App.Department, related: App.Department,
 related_key: :activity_id, relationship: :child}

# HasMany
%Ecto.Association.Has{cardinality: :many, defaults: [],
 field: :product_activities, on_cast: nil, on_delete: :nothing,
 on_replace: :raise, owner: App.Activity, owner_key: :id,
 queryable: App.ProductActivity, related: App.ProductActivity,
 related_key: :activity_id, relationship: :child}

We don't need to add all these fields only the ones dayron needs

Ecto also have the Ecto.Association.NotLoaded module to indicate an association is not loaded.

Having all this information about the associations I think we can easily parse the relationships. Maybe adding a method build in every Model that will first build the associations if they're present and only after that call the struct method.

If we add support for all this I think adding the api to fetch associations will be straightforward.