|
1 | 1 | defmodule EctoResource do
|
2 | 2 | @moduledoc """
|
3 |
| - This module provides a DSL to easily generate the basic functions for a schema. |
4 |
| - This allows the context to focus on interesting, atypical implementations rather |
5 |
| - than the redundent, drifting CRUD functions. |
| 3 | + EctoResource |
| 4 | + ============ |
| 5 | + Eliminate boilerplate involved in defining basic CRUD functions in a Phoenix context or Elixir module. |
| 6 | +
|
| 7 | + When using [Context modules](https://hexdocs.pm/phoenix/contexts.html) in a [Phoenix](https://phoenixframework.org/) application, |
| 8 | + there's a general need to define the standard CRUD functions for a given `Ecto.Schema`. Phoenix context generators will even do this automatically. |
| 9 | + Soon you will notice that there's quite a lot of code involved in CRUD access within your contexts. |
| 10 | +
|
| 11 | + This can become problematic for a few reasons: |
| 12 | +
|
| 13 | + * Boilerplate functions for CRUD access, for every `Ecto.Schema` referenced in that context, introduce more noise than signal. This can obscure the more interesting details of the context. |
| 14 | + * These functions may tend to accumulate drift from the standard API by inviting edits for new use-cases, reducing the usefulness of naming conventions. |
| 15 | + * The burden of locally testing wrapper functions, yields low value for the writing and maintainence investment. |
| 16 | +
|
| 17 | + In short, at best this code is redundant and at worst is a deviant entanglement of modified conventions. All of which amounts to a more-painful development experience. `EctoResource` was created to ease this pain. |
| 18 | +
|
| 19 | + Usage |
| 20 | + ----- |
| 21 | +
|
| 22 | + ### Basic usage - generate all `EctoResource` functions |
| 23 | +
|
| 24 | + ```elixir |
| 25 | + defmodule MyApp.MyContext do |
| 26 | + alias MyApp.Repo |
| 27 | + alias MyApp.Schema |
| 28 | + use EctoResource |
| 29 | +
|
| 30 | + using_repo(Repo) do |
| 31 | + resource(Schema) |
| 32 | + end |
| 33 | + end |
| 34 | + ``` |
| 35 | +
|
| 36 | + This generates all the functions `EctoResource` has to offer: |
| 37 | +
|
| 38 | + * `MyContext.all_schemas/1` |
| 39 | + * `MyContext.change_schema/1` |
| 40 | + * `MyContext.create_schema/1` |
| 41 | + * `MyContext.create_schema!/1` |
| 42 | + * `MyContext.delete_schema/1` |
| 43 | + * `MyContext.delete_schema!/1` |
| 44 | + * `MyContext.get_schema/2` |
| 45 | + * `MyContext.get_schema!/2` |
| 46 | + * `MyContext.get_schema_by/2` |
| 47 | + * `MyContext.get_schema_by!/2` |
| 48 | + * `MyContext.update_schema/2` |
| 49 | + * `MyContext.update_schema!/2` |
| 50 | +
|
| 51 | + ### Explicit usage - generate only given functions |
| 52 | +
|
| 53 | + ```elixir |
| 54 | + defmodule MyApp.MyContext do |
| 55 | + alias MyApp.Repo |
| 56 | + alias MyApp.Schema |
| 57 | + use EctoResource |
| 58 | +
|
| 59 | + using_repo(Repo) do |
| 60 | + resource(Schema, only: [:create, :delete!]) |
| 61 | + end |
| 62 | + end |
| 63 | + ``` |
| 64 | +
|
| 65 | + This generates only the given functions: |
| 66 | +
|
| 67 | + * `MyContext.create_schema/1` |
| 68 | + * `MyContext.delete_schema!/1` |
| 69 | +
|
| 70 | + ### Exclusive usage - generate all but the given functions |
| 71 | +
|
| 72 | + ```elixir |
| 73 | + defmodule MyApp.MyContext do |
| 74 | + alias MyApp.Repo |
| 75 | + alias MyApp.Schema |
| 76 | + use EctoResource |
| 77 | +
|
| 78 | + using_repo(Repo) do |
| 79 | + resource(Schema, except: [:create, :delete!]) |
| 80 | + end |
| 81 | + end |
| 82 | + ``` |
| 83 | +
|
| 84 | + This generates all the functions excluding the given functions: |
| 85 | +
|
| 86 | + * `MyContext.all_schemas/1` |
| 87 | + * `MyContext.change_schema/1` |
| 88 | + * `MyContext.create_schema!/1` |
| 89 | + * `MyContext.delete_schema/1` |
| 90 | + * `MyContext.get_schema/2` |
| 91 | + * `MyContext.get_schema_by/2` |
| 92 | + * `MyContext.get_schema_by!/2` |
| 93 | + * `MyContext.get_schema!/2` |
| 94 | + * `MyContext.update_schema/2` |
| 95 | + * `MyContext.update_schema!/2` |
| 96 | +
|
| 97 | + ### Alias `:read` - generate data access functions |
| 98 | +
|
| 99 | + ```elixir |
| 100 | + defmodule MyApp.MyContext do |
| 101 | + alias MyApp.Repo |
| 102 | + alias MyApp.Schema |
| 103 | + use EctoResource |
| 104 | +
|
| 105 | + using_repo(Repo) do |
| 106 | + resource(Schema, :read) |
| 107 | + end |
| 108 | + end |
| 109 | + ``` |
| 110 | +
|
| 111 | + This generates all the functions necessary for reading data: |
| 112 | +
|
| 113 | + * `MyContext.all_schemas/1` |
| 114 | + * `MyContext.get_schema/2` |
| 115 | + * `MyContext.get_schema!/2` |
| 116 | +
|
| 117 | + ### Alias `:read_write` - generate data access and manipulation functions, excluding delete |
| 118 | +
|
| 119 | + ```elixir |
| 120 | + defmodule MyApp.MyContext do |
| 121 | + alias MyApp.Repo |
| 122 | + alias MyApp.Schema |
| 123 | + use EctoResource |
| 124 | +
|
| 125 | + using_repo(Repo) do |
| 126 | + resource(Schema, :read_write) |
| 127 | + end |
| 128 | + end |
| 129 | + ``` |
| 130 | +
|
| 131 | + This generates all the functions except `delete_schema/1` and `delete_schema!/1`: |
| 132 | +
|
| 133 | + * `MyContext.all_schemas/1` |
| 134 | + * `MyContext.change_schema/1` |
| 135 | + * `MyContext.create_schema/1` |
| 136 | + * `MyContext.create_schema!/1` |
| 137 | + * `MyContext.get_schema/2` |
| 138 | + * `MyContext.get_schema!/2` |
| 139 | + * `MyContext.update_schema/2` |
| 140 | + * `MyContext.update_schema!/2` |
| 141 | +
|
| 142 | + ### Resource functions |
| 143 | +
|
| 144 | + The general idea of the generated resource functions is to abstract away the `Ecto.Repo` and `Ecto.Schema` parts of data access with `Ecto` and provide an API to the context that feels natural and clear to the caller. |
| 145 | +
|
| 146 | + The following examples will all assume a repo named `Repo` and a schema named `Person`. |
| 147 | +
|
| 148 | + #### all_people |
| 149 | +
|
| 150 | + Fetches a list of all %Person{} entries from the data store. _Note: `EctoResource` will pluralize this function name using `Inflex`_ |
| 151 | +
|
| 152 | + ```elixir |
| 153 | + iex> all_people() |
| 154 | + [%Person{id: 1}] |
| 155 | +
|
| 156 | + iex> all_people(preloads: [:address]) |
| 157 | + [%Person{id: 1, address: %Address{}}] |
| 158 | +
|
| 159 | + iex> all_people(order_by: [desc: :id]) |
| 160 | + [%Person{id: 2}, %Person{id: 1}] |
| 161 | +
|
| 162 | + iex> all_people(preloads: [:address], order_by: [desc: :id])) |
| 163 | + [ |
| 164 | + %Person{ |
| 165 | + id: 2, |
| 166 | + address: %Address{} |
| 167 | + }, |
| 168 | + %Person{ |
| 169 | + id: 1, |
| 170 | + address: %Address{} |
| 171 | + } |
| 172 | + ] |
| 173 | +
|
| 174 | + iex> all_people(where: [id: 2]) |
| 175 | + [%Person{id: 2, address: %Address{}}] |
| 176 | + ``` |
| 177 | +
|
| 178 | + #### change_person |
| 179 | +
|
| 180 | + Creates a `%Person{}` changeset. |
| 181 | +
|
| 182 | + ```elixir |
| 183 | + iex> change_person(%{name: "Example Person"}) |
| 184 | + #Ecto.Changeset< |
| 185 | + action: nil, |
| 186 | + changes: %{name: "Example Person"}, |
| 187 | + errors: [], |
| 188 | + data: #Person<>, |
| 189 | + valid?: true |
| 190 | + > |
| 191 | + ``` |
| 192 | +
|
| 193 | + #### create_person |
| 194 | +
|
| 195 | + Inserts a `%Person{}` with the given attributes in the data store, returning an `:ok`/`:error` tuple. |
| 196 | +
|
| 197 | + ```elixir |
| 198 | + iex> create_person(%{name: "Example Person"}) |
| 199 | + {:ok, %Person{id: 123, name: "Example Person"}} |
| 200 | +
|
| 201 | + iex> create_person(%{invalid: "invalid"}) |
| 202 | + {:error, %Ecto.Changeset} |
| 203 | + ``` |
| 204 | +
|
| 205 | + #### create_person! |
| 206 | +
|
| 207 | + Inserts a `%Person{}` with the given attributes in the data store, returning a `%Person{}` or raises `Ecto.InvalidChangesetError`. |
| 208 | +
|
| 209 | + ```elixir |
| 210 | + iex> create_person!(%{name: "Example Person"}) |
| 211 | + %Person{id: 123, name: "Example Person"} |
| 212 | +
|
| 213 | + iex> create_person!(%{invalid: "invalid"}) |
| 214 | + ** (Ecto.InvalidChangesetError) |
| 215 | + ``` |
| 216 | +
|
| 217 | + #### delete_person |
| 218 | +
|
| 219 | + Deletes a given `%Person{}` from the data store, returning an `:ok`/`:error` tuple. |
| 220 | +
|
| 221 | + ```elixir |
| 222 | + iex> delete_person(%Person{id: 1}) |
| 223 | + {:ok, %Person{id: 1}} |
| 224 | +
|
| 225 | + iex> delete_person(%Person{id: 999}) |
| 226 | + {:error, %Ecto.Changeset} |
| 227 | + ``` |
| 228 | +
|
| 229 | + #### delete_person! |
| 230 | +
|
| 231 | + Deletes a given `%Person{}` from the data store, returning the deleted `%Person{}`, or raises `Ecto.StaleEntryError`. |
| 232 | +
|
| 233 | + ```elixir |
| 234 | + iex> delete_person!(%Person{id: 1}) |
| 235 | + %Person{id: 1} |
| 236 | +
|
| 237 | + iex> delete_person!(%Person{id: 999}) |
| 238 | + ** (Ecto.StaleEntryError) |
| 239 | + ``` |
| 240 | +
|
| 241 | + #### get_person |
| 242 | +
|
| 243 | + Fetches a single `%Person{}` from the data store where the primary key matches the given id, returns a `%Person{}` or `nil`. |
| 244 | +
|
| 245 | + ```elixir |
| 246 | + iex> get_person(1) |
| 247 | + %Person{id: 1} |
| 248 | +
|
| 249 | + iex> get_person(999) |
| 250 | + nil |
| 251 | +
|
| 252 | + iex> get_person(1, preloads: [:address]) |
| 253 | + %Person{ |
| 254 | + id: 1, |
| 255 | + address: %Address{} |
| 256 | + } |
| 257 | + ``` |
| 258 | +
|
| 259 | + #### get_person! |
| 260 | +
|
| 261 | + Fetches a single `%Person{}` from the data store where the primary key matches the given id, returns a `%Person{}` or raises `Ecto.NoResultsError`. |
| 262 | +
|
| 263 | + ```elixir |
| 264 | + iex> get_person!(1) |
| 265 | + %Person{id: 1} |
| 266 | +
|
| 267 | + iex> get_person!(999) |
| 268 | + ** (Ecto.NoResultsError) |
| 269 | +
|
| 270 | + iex> get_person!(1, preloads: [:address]) |
| 271 | + %Person{ |
| 272 | + id: 1, |
| 273 | + address: %Address{} |
| 274 | + } |
| 275 | + ``` |
| 276 | +
|
| 277 | + #### get_person_by |
| 278 | +
|
| 279 | + Fetches a single `%Person{}` from the data store where the attributes match the |
| 280 | + given values. |
| 281 | +
|
| 282 | + ```elixir |
| 283 | + iex> get_person_by(%{name: "Chuck Norris"}) |
| 284 | + %Person{name: "Chuck Norris"} |
| 285 | +
|
| 286 | + iex> get_person_by(%{name: "Doesn't Exist"}) |
| 287 | + nil |
| 288 | + ``` |
| 289 | +
|
| 290 | + #### get_person_by! |
| 291 | +
|
| 292 | + Fetches a single `%Person{}` from the data store where the attributes match the |
| 293 | + given values. Raises an `Ecto.NoResultsError` if the record does not exist |
| 294 | +
|
| 295 | + ```elixir |
| 296 | + iex> get_person_by!(%{name: "Chuck Norris"}) |
| 297 | + %Person{name: "Chuck Norris"} |
| 298 | +
|
| 299 | + iex> get_person_by!(%{name: "Doesn't Exist"}) |
| 300 | + ** (Ecto.NoResultsError) |
| 301 | + ``` |
| 302 | +
|
| 303 | + #### update_person |
| 304 | +
|
| 305 | + Updates a given %Person{} with the given attributes, returns an `:ok`/`:error` tuple. |
| 306 | +
|
| 307 | + ```elixir |
| 308 | + iex> update_person(%Person{id: 1}, %{name: "New Person"}) |
| 309 | + {:ok, %Person{id: 1, name: "New Person"}} |
| 310 | +
|
| 311 | + iex> update_person(%Person{id: 1}, %{invalid: "invalid"}) |
| 312 | + {:error, %Ecto.Changeset} |
| 313 | + ``` |
| 314 | +
|
| 315 | + #### update_person! |
| 316 | +
|
| 317 | + Updates a given %Person{} with the given attributes, returns a %Person{} or raises `Ecto.InvalidChangesetError`. |
| 318 | +
|
| 319 | + ```elixir |
| 320 | + iex> update_person!(%Person{id: 1}, %{name: "New Person"}) |
| 321 | + %Person{id: 1, name: "New Person"} |
| 322 | +
|
| 323 | + iex> update_person!(%Person{id: 1}, %{invalid: "invalid"}) |
| 324 | + ** (Ecto.InvalidChangesetError) |
| 325 | + ``` |
| 326 | +
|
| 327 | + Caveats |
| 328 | + ------- |
| 329 | + This is not meant to be used as a wrapper for all the Repo functions within a context. Not all callbacks defined in Ecto.Repo are generated. `EctoResource` should be used to help reduce boilerplate code and tests for general CRUD operations. |
| 330 | +
|
| 331 | + It may be the case that `EctoResource` needs to evolve and provide slightly more functionality/flexibility in the future. However, the general focus is reducing boilerplate code. |
6 | 332 | """
|
7 | 333 |
|
8 | 334 | alias __MODULE__
|
|
0 commit comments