Skip to content

Commit ca6fdee

Browse files
authored
Includes timestamps macro to the Collection module to handle inserted_at and updated_at attributes (#145)
1 parent 8aa675c commit ca6fdee

File tree

4 files changed

+153
-5
lines changed

4 files changed

+153
-5
lines changed

lib/mongo/collection.ex

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ defmodule Mongo.Collection do
278278
279279
defmodule Board do
280280
281-
use Collection
281+
use Collection
282282
283283
collection "boards" do
284284
@@ -358,6 +358,94 @@ defmodule Mongo.Collection do
358358
"modified" : ISODate("2020-05-19T15:15:14.374Z"),
359359
"title" : "Vega"
360360
}
361+
## Example `timestamps`
362+
363+
defmodule Post do
364+
365+
use Mongo.Collection
366+
367+
collection "posts" do
368+
attribute :title, String.t()
369+
timestamps()
370+
end
371+
372+
def new(title) do
373+
Map.put(new(), :title, title)
374+
end
375+
376+
def store(post) do
377+
MyRepo.insert_or_update(post)
378+
end
379+
end
380+
381+
In this example the macro `timestamps` is used to create two DateTime attributes, `inserted_at` and `updated_at`.
382+
This macro is intented to use with the Repo module, as it will be responsible for updating the value of `updated_at` attribute before execute the action.
383+
384+
iex(1)> post = Post.new("lorem ipsum dolor sit amet")
385+
%Post{
386+
_id: #BSON.ObjectId<6327a7099626f7f61607e179>,
387+
inserted_at: ~U[2022-09-18 23:17:29.087092Z],
388+
title: "lorem ipsum dolor sit amet",
389+
updated_at: ~U[2022-09-18 23:17:29.087070Z]
390+
}
391+
392+
iex(2)> Post.store(post)
393+
{:ok,
394+
%{
395+
_id: #BSON.ObjectId<6327a7099626f7f61607e179>,
396+
inserted_at: ~U[2022-09-18 23:17:29.087092Z],
397+
title: "lorem ipsum dolor sit amet",
398+
updated_at: ~U[2022-09-18 23:19:24.516648Z]
399+
}}
400+
401+
Is possible to change the field names, like Ecto does, and also change the default behaviour:
402+
403+
defmodule Comment do
404+
405+
use Mongo.Collection
406+
407+
collection "comments" do
408+
attribute :text, String.t()
409+
timestamps(inserted_at: :created, updated_at: :modified, default: &__MODULE__.truncated_date_time/0)
410+
end
411+
412+
def new(text) do
413+
Map.put(new(), :text, text)
414+
end
415+
416+
def truncated_date_time() do
417+
utc_now = DateTime.utc_now()
418+
DateTime.truncate(utc_now, :second)
419+
end
420+
end
421+
422+
iex(1)> comment = Comment.new("useful comment")
423+
%Comment{
424+
_id: #BSON.ObjectId<6327aa979626f7f616ae5b4a>,
425+
created: ~U[2022-09-18 23:32:39Z],
426+
modified: ~U[2022-09-18 23:32:39Z],
427+
text: "useful comment"
428+
}
429+
430+
iex(2)> {:ok, comment} = MyRepo.insert(comment)
431+
{:ok,
432+
%Comment{
433+
_id: #BSON.ObjectId<6327aa979626f7f616ae5b4a>,
434+
created: ~U[2022-09-18 23:32:39Z],
435+
modified: ~U[2022-09-18 23:32:42Z],
436+
text: "useful comment"
437+
}}
438+
439+
iex(3)> {:ok, comment} = MyRepo.update(%{comment | text: "not so useful comment"})
440+
{:ok,
441+
%Comment{
442+
_id: #BSON.ObjectId<6327aa979626f7f616ae5b4a>,
443+
created: ~U[2022-09-18 23:32:39Z],
444+
modified: ~U[2022-09-18 23:32:46Z],
445+
text: not so useful comment"
446+
}}
447+
448+
The `timestamps` macro has some limitations as it does not run in batch commands like `insert_all` or `update_all`, nor does it update embedded documents.
361449
362450
"""
363451

@@ -376,6 +464,7 @@ defmodule Mongo.Collection do
376464

377465
Module.register_attribute(__MODULE__, :attributes, accumulate: true)
378466
Module.register_attribute(__MODULE__, :derived, accumulate: true)
467+
Module.register_attribute(__MODULE__, :timestamps, accumulate: true)
379468
Module.register_attribute(__MODULE__, :types, accumulate: true)
380469
Module.register_attribute(__MODULE__, :embed_ones, accumulate: true)
381470
Module.register_attribute(__MODULE__, :embed_manys, accumulate: true)
@@ -440,6 +529,7 @@ defmodule Mongo.Collection do
440529
Collection.__type__(@types)
441530

442531
def __collection__(:attributes), do: unquote(attribute_names)
532+
def __collection__(:timestamps), do: unquote(@timestamps)
443533
def __collection__(:types), do: @types
444534
def __collection__(:collection), do: unquote(@collection)
445535
def __collection__(:id), do: unquote(elem(@id_generator, 0))
@@ -565,12 +655,29 @@ defmodule Mongo.Collection do
565655
end
566656
end
567657

658+
timestamps_function =
659+
quote unquote: false do
660+
def timestamps(nil) do
661+
nil
662+
end
663+
664+
def timestamps(xs) when is_list(xs) do
665+
Enum.map(xs, fn struct -> timestamps(struct) end)
666+
end
667+
668+
def timestamps(struct) do
669+
updated_at = @timestamps[:updated_at]
670+
Collection.timestamps(struct, updated_at, @attributes[updated_at])
671+
end
672+
end
673+
568674
quote do
569675
unquote(prelude)
570676
unquote(postlude)
571677
unquote(new_function)
572678
unquote(load_function)
573679
unquote(dump_function)
680+
unquote(timestamps_function)
574681
end
575682
end
576683

@@ -708,6 +815,34 @@ defmodule Mongo.Collection do
708815
Module.put_attribute(mod, :attributes, {name, opts})
709816
end
710817

818+
@doc """
819+
Defines the `timestamps/1` function.
820+
"""
821+
defmacro timestamps(opts \\ []) do
822+
quote bind_quoted: [opts: opts] do
823+
inserted_at = Keyword.get(opts, :inserted_at, :inserted_at)
824+
updated_at = Keyword.get(opts, :updated_at, :updated_at)
825+
type = Keyword.get(opts, :type, DateTime)
826+
827+
new_opts =
828+
opts
829+
|> Keyword.drop([:inserted_at, :updated_at, :type])
830+
|> Keyword.put_new(:default, &DateTime.utc_now/0)
831+
832+
Module.put_attribute(__MODULE__, :timestamps, {:inserted_at, inserted_at})
833+
Module.put_attribute(__MODULE__, :timestamps, {:updated_at, updated_at})
834+
835+
Collection.__attribute__(__MODULE__, inserted_at, Macro.escape(type), new_opts)
836+
Collection.__attribute__(__MODULE__, updated_at, Macro.escape(type), new_opts)
837+
end
838+
end
839+
840+
def timestamps(struct, nil, _default), do: struct
841+
842+
def timestamps(struct, updated_at, opts) do
843+
Map.put(struct, updated_at, opts[:default].())
844+
end
845+
711846
def dump(%{__struct__: _} = struct) do
712847
map = Map.from_struct(struct)
713848
:maps.map(&dump/2, map) |> filter_nils()

lib/mongo/repo.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ defmodule Mongo.Repo do
6363
unless @read_only do
6464
def insert(%{__struct__: module} = doc, opts \\ []) do
6565
collection = module.__collection__(:collection)
66+
doc = module.timestamps(doc)
6667

6768
case Mongo.insert_one(@topology, collection, module.dump(doc), opts) do
6869
{:error, reason} -> {:error, reason}
@@ -72,6 +73,7 @@ defmodule Mongo.Repo do
7273

7374
def update(%{__struct__: module, _id: id} = doc) do
7475
collection = module.__collection__(:collection)
76+
doc = module.timestamps(doc)
7577

7678
case Mongo.update_one(@topology, collection, %{_id: id}, %{"$set" => module.dump(doc)}, []) do
7779
{:error, reason} -> {:error, reason}
@@ -81,6 +83,7 @@ defmodule Mongo.Repo do
8183

8284
def insert_or_update(%{__struct__: module, _id: id} = doc) do
8385
collection = module.__collection__(:collection)
86+
doc = module.timestamps(doc)
8487

8588
case Mongo.update_one(@topology, collection, %{_id: id}, %{"$set" => module.dump(doc)}, upsert: true) do
8689
{:error, reason} -> {:error, reason}
@@ -101,18 +104,21 @@ defmodule Mongo.Repo do
101104

102105
def insert!(%{__struct__: module} = doc, opts \\ []) do
103106
collection = module.__collection__(:collection)
107+
doc = module.timestamps(doc)
104108
%{inserted_id: id} = Mongo.insert_one!(@topology, collection, module.dump(doc), opts)
105109
%{doc | _id: id}
106110
end
107111

108112
def update!(%{__struct__: module, _id: id} = doc) do
109113
collection = module.__collection__(:collection)
114+
doc = module.timestamps(doc)
110115
Mongo.update_one!(@topology, collection, %{_id: id}, %{"$set" => module.dump(doc)}, [])
111116
doc
112117
end
113118

114119
def insert_or_update!(%{__struct__: module, _id: id} = doc) do
115120
collection = module.__collection__(:collection)
121+
doc = module.timestamps(doc)
116122
update_one_result = Mongo.update_one!(@topology, collection, %{_id: id}, %{"$set" => module.dump(doc)}, upsert: true)
117123

118124
case update_one_result do

test/collections/simple_test.exs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,8 @@ defmodule Collections.SimpleTest do
3737

3838
collection "cards" do
3939
attribute :title, String.t(), default: "new title"
40-
attribute :created, DateString.t(), default: &DateTime.utc_now/0
41-
attribute :modified, DateString.t(), default: &DateTime.utc_now/0
4240
embeds_one(:label, Label, default: &Label.new/0)
41+
timestamps(inserted_at: :created, updated_at: :modified)
4342
end
4443

4544
def insert_one(%Card{} = card, top) do

test/mongo/repo_test.exs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ defmodule Mongo.RepoTest do
1212

1313
collection "posts" do
1414
attribute :title, String.t()
15+
timestamps()
1516
end
1617
end
1718

@@ -343,10 +344,12 @@ defmodule Mongo.RepoTest do
343344
|> Map.put(:title, "test")
344345
|> MyRepo.insert()
345346

346-
{:ok, %Post{title: "updated"}} =
347+
{:ok, %Post{title: "updated"} = updated} =
347348
post
348349
|> Map.put(:title, "updated")
349350
|> MyRepo.update()
351+
352+
assert DateTime.compare(post.updated_at, updated.updated_at) == :lt
350353
end
351354

352355
test "updates a document without changes" do
@@ -366,10 +369,13 @@ defmodule Mongo.RepoTest do
366369
|> Map.put(:title, "test")
367370
|> MyRepo.insert()
368371

369-
%Post{title: "updated"} =
372+
updated =
370373
post
371374
|> Map.put(:title, "updated")
372375
|> MyRepo.update!()
376+
377+
assert updated.title == "updated"
378+
assert DateTime.compare(post.updated_at, updated.updated_at) == :lt
373379
end
374380

375381
test "updates a document without changes" do
@@ -405,6 +411,7 @@ defmodule Mongo.RepoTest do
405411

406412
assert Map.get(post, :_id) == Map.get(updated, :_id)
407413
assert updated.title == "updated"
414+
assert DateTime.compare(post.updated_at, updated.updated_at) == :lt
408415
end
409416

410417
test "updates a document without changes if it does already exist" do
@@ -443,6 +450,7 @@ defmodule Mongo.RepoTest do
443450

444451
assert Map.get(post, :_id) == Map.get(updated, :_id)
445452
assert updated.title == "updated"
453+
assert DateTime.compare(post.updated_at, updated.updated_at) == :lt
446454
end
447455

448456
test "updates a document without changes if it does already exist" do

0 commit comments

Comments
 (0)