Skip to content

Commit 8643aff

Browse files
authored
new_record function (#8)
1 parent 275a73a commit 8643aff

5 files changed

Lines changed: 157 additions & 22 deletions

File tree

lib/context_kit.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ defmodule ContextKit do
141141
Blog.query_comments()
142142
Blog.query_comments(status: "published")
143143
144+
# New record
145+
Blog.new_comment(%{status: "published"})
146+
144147
# List records with filtering and pagination
145148
Blog.list_comments(status: "published", paginate: [page: 1, per_page: 20])
146149
@@ -314,7 +317,7 @@ defmodule ContextKit do
314317
- `repo`: Your Ecto repository module
315318
- `schema`: The Ecto schema module
316319
- `queries`: Module containing custom query functions
317-
- `except`: List of operations to exclude (`:list`, `:get`, `:one`, `:delete`, `:create`, `:update`, `:change`, `:subscribe`, `:broadcast`)
320+
- `except`: List of operations to exclude (`:new`, `:list`, `:get`, `:one`, `:delete`, `:create`, `:update`, `:change`, `:subscribe`, `:broadcast`)
318321
- `plural_resource_name`: Custom plural name for list functions
319322
320323
Additional options for `ContextKit.CRUD.Scoped`:

lib/context_kit/crud.ex

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ defmodule ContextKit.CRUD do
3838
* `query_comments/0` - Returns a base query for all comments
3939
* `query_comments/1` - Returns a filtered query based on options (without executing)
4040
41+
### New Operations
42+
* `new_comment/0` - Returns a new comment
43+
* `new_comment/1` - Returns a new comment with params
44+
* `new_comment/2` - Returns a new comment with params and opts
45+
4146
### List Operations
4247
* `list_comments/0` - Returns all comments
4348
* `list_comments/1` - Returns filtered comments based on options
@@ -86,6 +91,9 @@ defmodule ContextKit.CRUD do
8691
query = MyApp.Blog.query_comments(status: :published)
8792
MyApp.Repo.aggregate(query, :count)
8893
94+
# New comment
95+
MyApp.Blog.new_comment()
96+
8997
# List all comments
9098
MyApp.Blog.list_comments()
9199
@@ -175,7 +183,7 @@ defmodule ContextKit.CRUD do
175183
unquote(:"query_#{plural_resource_name}")([])
176184
end
177185

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

205+
if :new not in unquote(except) do
206+
@doc """
207+
Returns a `%#{unquote(schema_name)}{}`.
208+
209+
Fields can be passed as a map. Optionally, you can pass preloads via the opts
210+
keyword list to preload associations on the returned struct.
211+
212+
## Examples
213+
214+
iex> new_#{unquote(resource_name)}()
215+
%#{unquote(schema_name)}{}
216+
217+
iex> new_#{unquote(resource_name)}(%{foo: "bar"})
218+
%#{unquote(schema_name)}{foo: "bar"}
219+
220+
iex> new_#{unquote(resource_name)}(%{assoc_id: 123}, preload: [:assoc])
221+
%#{unquote(schema_name)}{assoc_id: 123, assoc: %Assoc{}}
222+
"""
223+
@spec unquote(:"new_#{resource_name}")() :: unquote(schema).t()
224+
def unquote(:"new_#{resource_name}")() do
225+
unquote(:"new_#{resource_name}")(%{}, [])
226+
end
227+
228+
@spec unquote(:"new_#{resource_name}")(params :: map()) :: unquote(schema).t()
229+
def unquote(:"new_#{resource_name}")(params) when is_map(params) do
230+
unquote(:"new_#{resource_name}")(params, [])
231+
end
232+
233+
@spec unquote(:"new_#{resource_name}")(params :: map(), opts :: keyword()) :: unquote(schema).t()
234+
def unquote(:"new_#{resource_name}")(params, opts) when is_map(params) and is_list(opts) do
235+
record =
236+
unquote(schema).__struct__()
237+
|> Ecto.Changeset.change()
238+
|> Ecto.Changeset.cast(params, unquote(schema).__schema__(:fields))
239+
|> Ecto.Changeset.apply_changes()
240+
241+
if opts[:preload], do: unquote(repo).preload(record, opts[:preload]), else: record
242+
end
243+
244+
defoverridable [
245+
{unquote(:"new_#{resource_name}"), 0},
246+
{unquote(:"new_#{resource_name}"), 1},
247+
{unquote(:"new_#{resource_name}"), 2}
248+
]
249+
end
250+
197251
if :list not in unquote(except) do
198252
@doc """
199253
Returns the list of `%#{unquote(schema_name)}{}`.
@@ -213,7 +267,7 @@ defmodule ContextKit.CRUD do
213267
unquote(:"list_#{plural_resource_name}")(%{})
214268
end
215269

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

268322
@spec unquote(:"get_#{resource_name}")(
269323
id :: term(),
270-
Keyword.t() | map() | Ecto.Query.t()
324+
keyword() | map() | Ecto.Query.t()
271325
) ::
272326
unquote(schema).t() | nil
273327
def unquote(:"get_#{resource_name}")(id, opts)
@@ -310,7 +364,7 @@ defmodule ContextKit.CRUD do
310364

311365
@spec unquote(:"get_#{resource_name}!")(
312366
id :: term(),
313-
opts :: Keyword.t() | map() | Ecto.Query.t()
367+
opts :: keyword() | map() | Ecto.Query.t()
314368
) :: unquote(schema).t()
315369
def unquote(:"get_#{resource_name}!")(id, opts)
316370
when is_list(opts) or is_map(opts) or is_struct(opts, Ecto.Query) do
@@ -345,7 +399,7 @@ defmodule ContextKit.CRUD do
345399
iex> one_#{unquote(resource_name)}(opts)
346400
nil
347401
"""
348-
@spec unquote(:"one_#{resource_name}")(opts :: Keyword.t() | map() | Ecto.Query.t()) ::
402+
@spec unquote(:"one_#{resource_name}")(opts :: keyword() | map() | Ecto.Query.t()) ::
349403
unquote(schema).t() | nil
350404
def unquote(:"one_#{resource_name}")(opts) when is_list(opts) or is_map(opts) or is_struct(opts, Ecto.Query) do
351405
{query, custom_query_options} =
@@ -372,7 +426,7 @@ defmodule ContextKit.CRUD do
372426
iex> one_#{unquote(resource_name)}!(opts)
373427
nil
374428
"""
375-
@spec unquote(:"one_#{resource_name}!")(opts :: Keyword.t() | map() | Ecto.Query.t()) :: unquote(schema).t()
429+
@spec unquote(:"one_#{resource_name}!")(opts :: keyword() | map() | Ecto.Query.t()) :: unquote(schema).t()
376430
def unquote(:"one_#{resource_name}!")(opts) when is_list(opts) or is_map(opts) or is_struct(opts, Ecto.Query) do
377431
{query, custom_query_options} =
378432
Query.build(Query.new(unquote(schema)), unquote(schema), opts)
@@ -415,7 +469,7 @@ defmodule ContextKit.CRUD do
415469
iex> delete_#{unquote(resource_name)}(id: 1)
416470
{:ok, %#{unquote(schema_name)}{}}
417471
"""
418-
@spec unquote(:"delete_#{resource_name}")(opts :: Keyword.t() | map() | Ecto.Query.t()) ::
472+
@spec unquote(:"delete_#{resource_name}")(opts :: keyword() | map() | Ecto.Query.t()) ::
419473
{:ok, unquote(schema).t()} | {:error, Ecto.Changeset.t()}
420474
def(unquote(:"delete_#{resource_name}")(opts)) do
421475
{query, custom_query_options} =

lib/context_kit/crud/scoped.ex

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ defmodule ContextKit.CRUD.Scoped do
3131
3232
## Optional Options
3333
34-
* `:except` - List of operation types to exclude (`:list`, `:get`, `:one`, `:delete`, `:create`, `:update`, `:change`, `:subscribe`, `:broadcast`)
34+
* `:except` - List of operation types to exclude (`:new`, `:list`, `:get`, `:one`, `:delete`, `:create`, `:update`, `:change`, `:subscribe`, `:broadcast`)
3535
* `:plural_resource_name` - Custom plural name for list functions (defaults to singular + "s")
3636
3737
## Generated Functions
@@ -43,6 +43,11 @@ defmodule ContextKit.CRUD.Scoped do
4343
* `query_comments/1` - Returns a filtered query based on options (without executing)
4444
* `query_comments/2` - Returns a scoped and filtered query if `:scope` is configured
4545
46+
### New Operations
47+
* `new_comment/0` - Returns a new comment
48+
* `new_comment/1` - Returns a new comment with params
49+
* `new_comment/2` - Returns a new comment with params and opts
50+
4651
### List Operations
4752
* `list_comments/0` - Returns all comments
4853
* `list_comments/1` - Returns filtered comments based on options
@@ -120,6 +125,9 @@ defmodule ContextKit.CRUD.Scoped do
120125
query = MyApp.Blog.query_comments(socket.assigns.current_scope, status: :published)
121126
MyApp.Repo.aggregate(query, :count)
122127
128+
# New comment
129+
MyApp.Blog.new_comment()
130+
123131
# List all comments
124132
MyApp.Blog.list_comments()
125133
@@ -379,7 +387,7 @@ defmodule ContextKit.CRUD.Scoped do
379387
iex> query_#{unquote(plural_resource_name)}(field: 123) |> Repo.aggregate(:count)
380388
123
381389
"""
382-
@spec unquote(:"query_#{plural_resource_name}")(opts :: Keyword.t()) :: Ecto.Query.t()
390+
@spec unquote(:"query_#{plural_resource_name}")(opts :: keyword()) :: Ecto.Query.t()
383391
def unquote(:"query_#{plural_resource_name}")(opts) when is_list(opts) do
384392
{query, custom_query_options} =
385393
Query.build(Query.new(unquote(schema)), unquote(schema), opts)
@@ -406,7 +414,7 @@ defmodule ContextKit.CRUD.Scoped do
406414
iex> query_#{unquote(plural_resource_name)}(socket.assigns.current_scope, field: 123) |> Repo.aggregate(:count)
407415
123
408416
"""
409-
@spec unquote(:"query_#{plural_resource_name}")(unquote(scope_module).t(), opts :: Keyword.t()) :: Ecto.Query.t()
417+
@spec unquote(:"query_#{plural_resource_name}")(unquote(scope_module).t(), opts :: keyword()) :: Ecto.Query.t()
410418
def unquote(:"query_#{plural_resource_name}")(%unquote(scope_module){} = scope, opts \\ []) do
411419
opts = Keyword.put(opts, :scope, scope)
412420

@@ -420,6 +428,52 @@ defmodule ContextKit.CRUD.Scoped do
420428
]
421429
end
422430

431+
if :new not in unquote(except) do
432+
@doc """
433+
Returns a `%#{unquote(schema_name)}{}`.
434+
435+
Fields can be passed as a map. Optionally, you can pass preloads via the opts
436+
keyword list to preload associations on the returned struct.
437+
438+
## Examples
439+
440+
iex> new_#{unquote(resource_name)}()
441+
%#{unquote(schema_name)}{}
442+
443+
iex> new_#{unquote(resource_name)}(%{foo: "bar"})
444+
%#{unquote(schema_name)}{foo: "bar"}
445+
446+
iex> new_#{unquote(resource_name)}(%{assoc_id: 123}, preload: [:assoc])
447+
%#{unquote(schema_name)}{assoc_id: 123, assoc: %Assoc{}}
448+
"""
449+
@spec unquote(:"new_#{resource_name}")() :: unquote(schema).t()
450+
def unquote(:"new_#{resource_name}")() do
451+
unquote(:"new_#{resource_name}")(%{}, [])
452+
end
453+
454+
@spec unquote(:"new_#{resource_name}")(params :: map()) :: unquote(schema).t()
455+
def unquote(:"new_#{resource_name}")(params) when is_map(params) do
456+
unquote(:"new_#{resource_name}")(params, [])
457+
end
458+
459+
@spec unquote(:"new_#{resource_name}")(params :: map(), opts :: keyword()) :: unquote(schema).t()
460+
def unquote(:"new_#{resource_name}")(params, opts) when is_map(params) and is_list(opts) do
461+
record =
462+
unquote(schema).__struct__()
463+
|> Ecto.Changeset.change()
464+
|> Ecto.Changeset.cast(params, unquote(schema).__schema__(:fields))
465+
|> Ecto.Changeset.apply_changes()
466+
467+
if opts[:preload], do: unquote(repo).preload(record, opts[:preload]), else: record
468+
end
469+
470+
defoverridable [
471+
{unquote(:"new_#{resource_name}"), 0},
472+
{unquote(:"new_#{resource_name}"), 1},
473+
{unquote(:"new_#{resource_name}"), 2}
474+
]
475+
end
476+
423477
if :list not in unquote(except) do
424478
@doc """
425479
Returns the list of `%#{unquote(schema_name)}{}`.
@@ -450,7 +504,7 @@ defmodule ContextKit.CRUD.Scoped do
450504
iex> list_#{unquote(plural_resource_name)}(field: "value")
451505
[%#{unquote(schema_name)}{}, ...]
452506
"""
453-
@spec unquote(:"list_#{plural_resource_name}")(opts :: Keyword.t() | map()) ::
507+
@spec unquote(:"list_#{plural_resource_name}")(opts :: keyword() | map()) ::
454508
[unquote(schema).t()] | {[unquote(schema).t()], ContextKit.Paginator.t()}
455509
def unquote(:"list_#{plural_resource_name}")(opts) when is_list(opts) or is_non_struct_map(opts) do
456510
{query, custom_query_options} =
@@ -499,7 +553,7 @@ defmodule ContextKit.CRUD.Scoped do
499553
iex> list_#{unquote(plural_resource_name)}(socket.assigns.current_scope, field: "value")
500554
[%#{unquote(schema_name)}{}, ...]
501555
"""
502-
@spec unquote(:"list_#{plural_resource_name}")(unquote(scope_module).t(), opts :: Keyword.t()) ::
556+
@spec unquote(:"list_#{plural_resource_name}")(unquote(scope_module).t(), opts :: keyword()) ::
503557
[unquote(schema).t()] | {[unquote(schema).t()], ContextKit.Paginator.t()}
504558
def unquote(:"list_#{plural_resource_name}")(%unquote(scope_module){} = scope, opts \\ []) do
505559
opts = Keyword.put(opts, :scope, scope)
@@ -547,7 +601,7 @@ defmodule ContextKit.CRUD.Scoped do
547601
iex> get_#{unquote(resource_name)}(1, field: "test")
548602
nil
549603
"""
550-
@spec unquote(:"get_#{resource_name}")(id :: term(), opts :: Keyword.t() | Ecto.Query.t()) ::
604+
@spec unquote(:"get_#{resource_name}")(id :: term(), opts :: keyword() | Ecto.Query.t()) ::
551605
unquote(schema).t() | nil
552606
def unquote(:"get_#{resource_name}")(id, opts) when is_list(opts) or is_struct(opts, Ecto.Query) do
553607
{query, custom_query_options} =
@@ -578,7 +632,7 @@ defmodule ContextKit.CRUD.Scoped do
578632
@spec unquote(:"get_#{resource_name}")(
579633
scope :: unquote(scope_module).t(),
580634
id :: term(),
581-
opts :: Keyword.t()
635+
opts :: keyword()
582636
) :: unquote(schema).t() | nil
583637
def unquote(:"get_#{resource_name}")(%unquote(scope_module){} = scope, id, opts \\ []) do
584638
opts = Keyword.put(opts, :scope, scope)
@@ -626,7 +680,7 @@ defmodule ContextKit.CRUD.Scoped do
626680
"""
627681
@spec unquote(:"get_#{resource_name}!")(
628682
id :: term(),
629-
opts :: Keyword.t() | Ecto.Query.t()
683+
opts :: keyword() | Ecto.Query.t()
630684
) :: unquote(schema).t()
631685
def unquote(:"get_#{resource_name}!")(id, opts) when is_list(opts) or is_struct(opts, Ecto.Query) do
632686
{query, custom_query_options} =
@@ -657,7 +711,7 @@ defmodule ContextKit.CRUD.Scoped do
657711
@spec unquote(:"get_#{resource_name}!")(
658712
scope :: unquote(scope_module).t(),
659713
id :: term(),
660-
opts :: Keyword.t()
714+
opts :: keyword()
661715
) :: unquote(schema).t()
662716
def unquote(:"get_#{resource_name}!")(%unquote(scope_module){} = scope, id, opts \\ []) do
663717
opts = Keyword.put(opts, :scope, scope)
@@ -686,7 +740,7 @@ defmodule ContextKit.CRUD.Scoped do
686740
iex> one_#{unquote(resource_name)}(opts)
687741
nil
688742
"""
689-
@spec unquote(:"one_#{resource_name}")(opts :: Keyword.t() | Ecto.Query.t()) ::
743+
@spec unquote(:"one_#{resource_name}")(opts :: keyword() | Ecto.Query.t()) ::
690744
unquote(schema).t() | nil
691745
def unquote(:"one_#{resource_name}")(opts) when is_list(opts) or is_struct(opts, Ecto.Query) do
692746
{query, custom_query_options} =
@@ -715,7 +769,7 @@ defmodule ContextKit.CRUD.Scoped do
715769
"""
716770
@spec unquote(:"one_#{resource_name}")(
717771
scope :: unquote(scope_module).t(),
718-
opts :: Keyword.t()
772+
opts :: keyword()
719773
) :: unquote(schema).t() | nil
720774
def unquote(:"one_#{resource_name}")(%unquote(scope_module){} = scope, opts \\ []) do
721775
opts = Keyword.put(opts, :scope, scope)
@@ -741,7 +795,7 @@ defmodule ContextKit.CRUD.Scoped do
741795
iex> one_#{unquote(resource_name)}!(opts)
742796
nil
743797
"""
744-
@spec unquote(:"one_#{resource_name}!")(opts :: Keyword.t() | Ecto.Query.t()) :: unquote(schema).t()
798+
@spec unquote(:"one_#{resource_name}!")(opts :: keyword() | Ecto.Query.t()) :: unquote(schema).t()
745799
def unquote(:"one_#{resource_name}!")(opts) when is_list(opts) or is_struct(opts, Ecto.Query) do
746800
{query, custom_query_options} =
747801
Query.build(Query.new(unquote(schema)), unquote(schema), opts)
@@ -769,7 +823,7 @@ defmodule ContextKit.CRUD.Scoped do
769823
"""
770824
@spec unquote(:"one_#{resource_name}!")(
771825
scope :: unquote(scope_module).t(),
772-
opts :: Keyword.t()
826+
opts :: keyword()
773827
) :: unquote(schema).t()
774828
def unquote(:"one_#{resource_name}!")(%unquote(scope_module){} = scope, opts \\ []) do
775829
opts = Keyword.put(opts, :scope, scope)
@@ -794,7 +848,7 @@ defmodule ContextKit.CRUD.Scoped do
794848
iex> delete_#{unquote(resource_name)}(id: 1)
795849
{:ok, %#{unquote(schema_name)}{}}
796850
"""
797-
@spec unquote(:"delete_#{resource_name}")(opts :: Keyword.t() | map() | Ecto.Query.t()) ::
851+
@spec unquote(:"delete_#{resource_name}")(opts :: keyword() | map() | Ecto.Query.t()) ::
798852
{:ok, unquote(schema).t()} | {:error, Ecto.Changeset.t()}
799853
def unquote(:"delete_#{resource_name}")(opts) when is_list(opts) do
800854
{query, custom_query_options} =

test/context_kit/crud/scoped_test.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@ defmodule ContextKit.CRUD.ScopedTest do
8989
end
9090
end
9191

92+
describe "new_{:resource}/0-2" do
93+
test "simple new" do
94+
assert %Book{} = Books.new_book()
95+
assert %Book{title: "my book"} = Books.new_book(%{title: "my book"})
96+
end
97+
98+
test "preloads assocs" do
99+
assert {:ok, author} = Repo.insert(%Author{name: "Bob"})
100+
assert %Book{author: %Author{name: "Bob"}} = Books.new_book(%{author_id: author.id}, preload: [:author])
101+
end
102+
end
103+
92104
describe "list_{:resource}/0-1" do
93105
test "simple list" do
94106
assert {:ok, scoped_book} = Repo.insert(%ScopedBook{title: "My Book"})

test/context_kit/crud_test.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ defmodule ContextKit.CRUDTest do
3636
end
3737
end
3838

39+
describe "new_{:resource}/0-2" do
40+
test "simple new" do
41+
assert %Book{} = Books.new_book()
42+
assert %Book{title: "my book"} = Books.new_book(%{title: "my book"})
43+
end
44+
45+
test "preloads assocs" do
46+
assert {:ok, author} = Repo.insert(%Author{name: "Bob"})
47+
assert %Book{author: %Author{name: "Bob"}} = Books.new_book(%{author_id: author.id}, preload: [:author])
48+
end
49+
end
50+
3951
describe "list_{:resource}/0-1" do
4052
test "simple list" do
4153
assert {:ok, book} = Repo.insert(%Book{title: "My Book"})

0 commit comments

Comments
 (0)