Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/context_kit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ defmodule ContextKit do
Blog.query_comments()
Blog.query_comments(status: "published")

# New record
Blog.new_comment(%{status: "published"})

# List records with filtering and pagination
Blog.list_comments(status: "published", paginate: [page: 1, per_page: 20])

Expand Down Expand Up @@ -314,7 +317,7 @@ defmodule ContextKit do
- `repo`: Your Ecto repository module
- `schema`: The Ecto schema module
- `queries`: Module containing custom query functions
- `except`: List of operations to exclude (`:list`, `:get`, `:one`, `:delete`, `:create`, `:update`, `:change`, `:subscribe`, `:broadcast`)
- `except`: List of operations to exclude (`:new`, `:list`, `:get`, `:one`, `:delete`, `:create`, `:update`, `:change`, `:subscribe`, `:broadcast`)
- `plural_resource_name`: Custom plural name for list functions

Additional options for `ContextKit.CRUD.Scoped`:
Expand Down
68 changes: 61 additions & 7 deletions lib/context_kit/crud.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ defmodule ContextKit.CRUD do
* `query_comments/0` - Returns a base query for all comments
* `query_comments/1` - Returns a filtered query based on options (without executing)

### New Operations
* `new_comment/0` - Returns a new comment
* `new_comment/1` - Returns a new comment with params
* `new_comment/2` - Returns a new comment with params and opts

### List Operations
* `list_comments/0` - Returns all comments
* `list_comments/1` - Returns filtered comments based on options
Expand Down Expand Up @@ -86,6 +91,9 @@ defmodule ContextKit.CRUD do
query = MyApp.Blog.query_comments(status: :published)
MyApp.Repo.aggregate(query, :count)

# New comment
MyApp.Blog.new_comment()

# List all comments
MyApp.Blog.list_comments()

Expand Down Expand Up @@ -175,7 +183,7 @@ defmodule ContextKit.CRUD do
unquote(:"query_#{plural_resource_name}")([])
end

@spec unquote(:"query_#{plural_resource_name}")(opts :: Keyword.t()) :: Ecto.Query.t()
@spec unquote(:"query_#{plural_resource_name}")(opts :: keyword()) :: Ecto.Query.t()
def unquote(:"query_#{plural_resource_name}")(opts) when is_list(opts) do
{query, custom_query_options} =
Query.build(Query.new(unquote(schema)), unquote(schema), opts)
Expand All @@ -194,6 +202,52 @@ defmodule ContextKit.CRUD do
]
end

if :new not in unquote(except) do
@doc """
Returns a `%#{unquote(schema_name)}{}`.

Fields can be passed as a map. Optionally, you can pass preloads via the opts
keyword list to preload associations on the returned struct.

## Examples

iex> new_#{unquote(resource_name)}()
%#{unquote(schema_name)}{}

iex> new_#{unquote(resource_name)}(%{foo: "bar"})
%#{unquote(schema_name)}{foo: "bar"}

iex> new_#{unquote(resource_name)}(%{assoc_id: 123}, preload: [:assoc])
%#{unquote(schema_name)}{assoc_id: 123, assoc: %Assoc{}}
"""
@spec unquote(:"new_#{resource_name}")() :: unquote(schema).t()
def unquote(:"new_#{resource_name}")() do
unquote(:"new_#{resource_name}")(%{}, [])
end

@spec unquote(:"new_#{resource_name}")(params :: map()) :: unquote(schema).t()
def unquote(:"new_#{resource_name}")(params) when is_map(params) do
unquote(:"new_#{resource_name}")(params, [])
end

@spec unquote(:"new_#{resource_name}")(params :: map(), opts :: keyword()) :: unquote(schema).t()
def unquote(:"new_#{resource_name}")(params, opts) when is_map(params) and is_list(opts) do
record =
unquote(schema).__struct__()
|> Ecto.Changeset.change()
|> Ecto.Changeset.cast(params, unquote(schema).__schema__(:fields))
|> Ecto.Changeset.apply_changes()

if opts[:preload], do: unquote(repo).preload(record, opts[:preload]), else: record
end

defoverridable [
{unquote(:"new_#{resource_name}"), 0},
{unquote(:"new_#{resource_name}"), 1},
{unquote(:"new_#{resource_name}"), 2}
]
end

if :list not in unquote(except) do
@doc """
Returns the list of `%#{unquote(schema_name)}{}`.
Expand All @@ -213,7 +267,7 @@ defmodule ContextKit.CRUD do
unquote(:"list_#{plural_resource_name}")(%{})
end

@spec unquote(:"list_#{plural_resource_name}")(opts :: Keyword.t() | map()) ::
@spec unquote(:"list_#{plural_resource_name}")(opts :: keyword() | map()) ::
[unquote(schema).t()] | {[unquote(schema).t()], ContextKit.Paginator.t()}
def unquote(:"list_#{plural_resource_name}")(opts) when is_list(opts) or is_non_struct_map(opts) do
{query, custom_query_options} =
Expand Down Expand Up @@ -267,7 +321,7 @@ defmodule ContextKit.CRUD do

@spec unquote(:"get_#{resource_name}")(
id :: term(),
Keyword.t() | map() | Ecto.Query.t()
keyword() | map() | Ecto.Query.t()
) ::
unquote(schema).t() | nil
def unquote(:"get_#{resource_name}")(id, opts)
Expand Down Expand Up @@ -310,7 +364,7 @@ defmodule ContextKit.CRUD do

@spec unquote(:"get_#{resource_name}!")(
id :: term(),
opts :: Keyword.t() | map() | Ecto.Query.t()
opts :: keyword() | map() | Ecto.Query.t()
) :: unquote(schema).t()
def unquote(:"get_#{resource_name}!")(id, opts)
when is_list(opts) or is_map(opts) or is_struct(opts, Ecto.Query) do
Expand Down Expand Up @@ -345,7 +399,7 @@ defmodule ContextKit.CRUD do
iex> one_#{unquote(resource_name)}(opts)
nil
"""
@spec unquote(:"one_#{resource_name}")(opts :: Keyword.t() | map() | Ecto.Query.t()) ::
@spec unquote(:"one_#{resource_name}")(opts :: keyword() | map() | Ecto.Query.t()) ::
unquote(schema).t() | nil
def unquote(:"one_#{resource_name}")(opts) when is_list(opts) or is_map(opts) or is_struct(opts, Ecto.Query) do
{query, custom_query_options} =
Expand All @@ -372,7 +426,7 @@ defmodule ContextKit.CRUD do
iex> one_#{unquote(resource_name)}!(opts)
nil
"""
@spec unquote(:"one_#{resource_name}!")(opts :: Keyword.t() | map() | Ecto.Query.t()) :: unquote(schema).t()
@spec unquote(:"one_#{resource_name}!")(opts :: keyword() | map() | Ecto.Query.t()) :: unquote(schema).t()
def unquote(:"one_#{resource_name}!")(opts) when is_list(opts) or is_map(opts) or is_struct(opts, Ecto.Query) do
{query, custom_query_options} =
Query.build(Query.new(unquote(schema)), unquote(schema), opts)
Expand Down Expand Up @@ -415,7 +469,7 @@ defmodule ContextKit.CRUD do
iex> delete_#{unquote(resource_name)}(id: 1)
{:ok, %#{unquote(schema_name)}{}}
"""
@spec unquote(:"delete_#{resource_name}")(opts :: Keyword.t() | map() | Ecto.Query.t()) ::
@spec unquote(:"delete_#{resource_name}")(opts :: keyword() | map() | Ecto.Query.t()) ::
{:ok, unquote(schema).t()} | {:error, Ecto.Changeset.t()}
def(unquote(:"delete_#{resource_name}")(opts)) do
{query, custom_query_options} =
Expand Down
82 changes: 68 additions & 14 deletions lib/context_kit/crud/scoped.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ defmodule ContextKit.CRUD.Scoped do

## Optional Options

* `:except` - List of operation types to exclude (`:list`, `:get`, `:one`, `:delete`, `:create`, `:update`, `:change`, `:subscribe`, `:broadcast`)
* `:except` - List of operation types to exclude (`:new`, `:list`, `:get`, `:one`, `:delete`, `:create`, `:update`, `:change`, `:subscribe`, `:broadcast`)
* `:plural_resource_name` - Custom plural name for list functions (defaults to singular + "s")

## Generated Functions
Expand All @@ -43,6 +43,11 @@ defmodule ContextKit.CRUD.Scoped do
* `query_comments/1` - Returns a filtered query based on options (without executing)
* `query_comments/2` - Returns a scoped and filtered query if `:scope` is configured

### New Operations
* `new_comment/0` - Returns a new comment
* `new_comment/1` - Returns a new comment with params
* `new_comment/2` - Returns a new comment with params and opts

### List Operations
* `list_comments/0` - Returns all comments
* `list_comments/1` - Returns filtered comments based on options
Expand Down Expand Up @@ -120,6 +125,9 @@ defmodule ContextKit.CRUD.Scoped do
query = MyApp.Blog.query_comments(socket.assigns.current_scope, status: :published)
MyApp.Repo.aggregate(query, :count)

# New comment
MyApp.Blog.new_comment()

# List all comments
MyApp.Blog.list_comments()

Expand Down Expand Up @@ -379,7 +387,7 @@ defmodule ContextKit.CRUD.Scoped do
iex> query_#{unquote(plural_resource_name)}(field: 123) |> Repo.aggregate(:count)
123
"""
@spec unquote(:"query_#{plural_resource_name}")(opts :: Keyword.t()) :: Ecto.Query.t()
@spec unquote(:"query_#{plural_resource_name}")(opts :: keyword()) :: Ecto.Query.t()
def unquote(:"query_#{plural_resource_name}")(opts) when is_list(opts) do
{query, custom_query_options} =
Query.build(Query.new(unquote(schema)), unquote(schema), opts)
Expand All @@ -406,7 +414,7 @@ defmodule ContextKit.CRUD.Scoped do
iex> query_#{unquote(plural_resource_name)}(socket.assigns.current_scope, field: 123) |> Repo.aggregate(:count)
123
"""
@spec unquote(:"query_#{plural_resource_name}")(unquote(scope_module).t(), opts :: Keyword.t()) :: Ecto.Query.t()
@spec unquote(:"query_#{plural_resource_name}")(unquote(scope_module).t(), opts :: keyword()) :: Ecto.Query.t()
def unquote(:"query_#{plural_resource_name}")(%unquote(scope_module){} = scope, opts \\ []) do
opts = Keyword.put(opts, :scope, scope)

Expand All @@ -420,6 +428,52 @@ defmodule ContextKit.CRUD.Scoped do
]
end

if :new not in unquote(except) do
@doc """
Returns a `%#{unquote(schema_name)}{}`.

Fields can be passed as a map. Optionally, you can pass preloads via the opts
keyword list to preload associations on the returned struct.

## Examples

iex> new_#{unquote(resource_name)}()
%#{unquote(schema_name)}{}

iex> new_#{unquote(resource_name)}(%{foo: "bar"})
%#{unquote(schema_name)}{foo: "bar"}

iex> new_#{unquote(resource_name)}(%{assoc_id: 123}, preload: [:assoc])
%#{unquote(schema_name)}{assoc_id: 123, assoc: %Assoc{}}
"""
@spec unquote(:"new_#{resource_name}")() :: unquote(schema).t()
def unquote(:"new_#{resource_name}")() do
unquote(:"new_#{resource_name}")(%{}, [])
end

@spec unquote(:"new_#{resource_name}")(params :: map()) :: unquote(schema).t()
def unquote(:"new_#{resource_name}")(params) when is_map(params) do
unquote(:"new_#{resource_name}")(params, [])
end

@spec unquote(:"new_#{resource_name}")(params :: map(), opts :: keyword()) :: unquote(schema).t()
def unquote(:"new_#{resource_name}")(params, opts) when is_map(params) and is_list(opts) do
record =
unquote(schema).__struct__()
|> Ecto.Changeset.change()
|> Ecto.Changeset.cast(params, unquote(schema).__schema__(:fields))
|> Ecto.Changeset.apply_changes()

if opts[:preload], do: unquote(repo).preload(record, opts[:preload]), else: record
end

defoverridable [
{unquote(:"new_#{resource_name}"), 0},
{unquote(:"new_#{resource_name}"), 1},
{unquote(:"new_#{resource_name}"), 2}
]
end

if :list not in unquote(except) do
@doc """
Returns the list of `%#{unquote(schema_name)}{}`.
Expand Down Expand Up @@ -450,7 +504,7 @@ defmodule ContextKit.CRUD.Scoped do
iex> list_#{unquote(plural_resource_name)}(field: "value")
[%#{unquote(schema_name)}{}, ...]
"""
@spec unquote(:"list_#{plural_resource_name}")(opts :: Keyword.t() | map()) ::
@spec unquote(:"list_#{plural_resource_name}")(opts :: keyword() | map()) ::
[unquote(schema).t()] | {[unquote(schema).t()], ContextKit.Paginator.t()}
def unquote(:"list_#{plural_resource_name}")(opts) when is_list(opts) or is_non_struct_map(opts) do
{query, custom_query_options} =
Expand Down Expand Up @@ -499,7 +553,7 @@ defmodule ContextKit.CRUD.Scoped do
iex> list_#{unquote(plural_resource_name)}(socket.assigns.current_scope, field: "value")
[%#{unquote(schema_name)}{}, ...]
"""
@spec unquote(:"list_#{plural_resource_name}")(unquote(scope_module).t(), opts :: Keyword.t()) ::
@spec unquote(:"list_#{plural_resource_name}")(unquote(scope_module).t(), opts :: keyword()) ::
[unquote(schema).t()] | {[unquote(schema).t()], ContextKit.Paginator.t()}
def unquote(:"list_#{plural_resource_name}")(%unquote(scope_module){} = scope, opts \\ []) do
opts = Keyword.put(opts, :scope, scope)
Expand Down Expand Up @@ -547,7 +601,7 @@ defmodule ContextKit.CRUD.Scoped do
iex> get_#{unquote(resource_name)}(1, field: "test")
nil
"""
@spec unquote(:"get_#{resource_name}")(id :: term(), opts :: Keyword.t() | Ecto.Query.t()) ::
@spec unquote(:"get_#{resource_name}")(id :: term(), opts :: keyword() | Ecto.Query.t()) ::
unquote(schema).t() | nil
def unquote(:"get_#{resource_name}")(id, opts) when is_list(opts) or is_struct(opts, Ecto.Query) do
{query, custom_query_options} =
Expand Down Expand Up @@ -578,7 +632,7 @@ defmodule ContextKit.CRUD.Scoped do
@spec unquote(:"get_#{resource_name}")(
scope :: unquote(scope_module).t(),
id :: term(),
opts :: Keyword.t()
opts :: keyword()
) :: unquote(schema).t() | nil
def unquote(:"get_#{resource_name}")(%unquote(scope_module){} = scope, id, opts \\ []) do
opts = Keyword.put(opts, :scope, scope)
Expand Down Expand Up @@ -626,7 +680,7 @@ defmodule ContextKit.CRUD.Scoped do
"""
@spec unquote(:"get_#{resource_name}!")(
id :: term(),
opts :: Keyword.t() | Ecto.Query.t()
opts :: keyword() | Ecto.Query.t()
) :: unquote(schema).t()
def unquote(:"get_#{resource_name}!")(id, opts) when is_list(opts) or is_struct(opts, Ecto.Query) do
{query, custom_query_options} =
Expand Down Expand Up @@ -657,7 +711,7 @@ defmodule ContextKit.CRUD.Scoped do
@spec unquote(:"get_#{resource_name}!")(
scope :: unquote(scope_module).t(),
id :: term(),
opts :: Keyword.t()
opts :: keyword()
) :: unquote(schema).t()
def unquote(:"get_#{resource_name}!")(%unquote(scope_module){} = scope, id, opts \\ []) do
opts = Keyword.put(opts, :scope, scope)
Expand Down Expand Up @@ -686,7 +740,7 @@ defmodule ContextKit.CRUD.Scoped do
iex> one_#{unquote(resource_name)}(opts)
nil
"""
@spec unquote(:"one_#{resource_name}")(opts :: Keyword.t() | Ecto.Query.t()) ::
@spec unquote(:"one_#{resource_name}")(opts :: keyword() | Ecto.Query.t()) ::
unquote(schema).t() | nil
def unquote(:"one_#{resource_name}")(opts) when is_list(opts) or is_struct(opts, Ecto.Query) do
{query, custom_query_options} =
Expand Down Expand Up @@ -715,7 +769,7 @@ defmodule ContextKit.CRUD.Scoped do
"""
@spec unquote(:"one_#{resource_name}")(
scope :: unquote(scope_module).t(),
opts :: Keyword.t()
opts :: keyword()
) :: unquote(schema).t() | nil
def unquote(:"one_#{resource_name}")(%unquote(scope_module){} = scope, opts \\ []) do
opts = Keyword.put(opts, :scope, scope)
Expand All @@ -741,7 +795,7 @@ defmodule ContextKit.CRUD.Scoped do
iex> one_#{unquote(resource_name)}!(opts)
nil
"""
@spec unquote(:"one_#{resource_name}!")(opts :: Keyword.t() | Ecto.Query.t()) :: unquote(schema).t()
@spec unquote(:"one_#{resource_name}!")(opts :: keyword() | Ecto.Query.t()) :: unquote(schema).t()
def unquote(:"one_#{resource_name}!")(opts) when is_list(opts) or is_struct(opts, Ecto.Query) do
{query, custom_query_options} =
Query.build(Query.new(unquote(schema)), unquote(schema), opts)
Expand Down Expand Up @@ -769,7 +823,7 @@ defmodule ContextKit.CRUD.Scoped do
"""
@spec unquote(:"one_#{resource_name}!")(
scope :: unquote(scope_module).t(),
opts :: Keyword.t()
opts :: keyword()
) :: unquote(schema).t()
def unquote(:"one_#{resource_name}!")(%unquote(scope_module){} = scope, opts \\ []) do
opts = Keyword.put(opts, :scope, scope)
Expand All @@ -794,7 +848,7 @@ defmodule ContextKit.CRUD.Scoped do
iex> delete_#{unquote(resource_name)}(id: 1)
{:ok, %#{unquote(schema_name)}{}}
"""
@spec unquote(:"delete_#{resource_name}")(opts :: Keyword.t() | map() | Ecto.Query.t()) ::
@spec unquote(:"delete_#{resource_name}")(opts :: keyword() | map() | Ecto.Query.t()) ::
{:ok, unquote(schema).t()} | {:error, Ecto.Changeset.t()}
def unquote(:"delete_#{resource_name}")(opts) when is_list(opts) do
{query, custom_query_options} =
Expand Down
12 changes: 12 additions & 0 deletions test/context_kit/crud/scoped_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ defmodule ContextKit.CRUD.ScopedTest do
end
end

describe "new_{:resource}/0-2" do
test "simple new" do
assert %Book{} = Books.new_book()
assert %Book{title: "my book"} = Books.new_book(%{title: "my book"})
end

test "preloads assocs" do
assert {:ok, author} = Repo.insert(%Author{name: "Bob"})
assert %Book{author: %Author{name: "Bob"}} = Books.new_book(%{author_id: author.id}, preload: [:author])
end
end

describe "list_{:resource}/0-1" do
test "simple list" do
assert {:ok, scoped_book} = Repo.insert(%ScopedBook{title: "My Book"})
Expand Down
12 changes: 12 additions & 0 deletions test/context_kit/crud_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ defmodule ContextKit.CRUDTest do
end
end

describe "new_{:resource}/0-2" do
test "simple new" do
assert %Book{} = Books.new_book()
assert %Book{title: "my book"} = Books.new_book(%{title: "my book"})
end

test "preloads assocs" do
assert {:ok, author} = Repo.insert(%Author{name: "Bob"})
assert %Book{author: %Author{name: "Bob"}} = Books.new_book(%{author_id: author.id}, preload: [:author])
end
end

describe "list_{:resource}/0-1" do
test "simple list" do
assert {:ok, book} = Repo.insert(%Book{title: "My Book"})
Expand Down