Skip to content

Commit 051676e

Browse files
authored
feat(projecthub): create soft delete for projects (#209)
1 parent 2825cda commit 051676e

File tree

8 files changed

+80
-15
lines changed

8 files changed

+80
-15
lines changed

front/lib/front/layout/cache_invalidator.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ defmodule Front.Layout.CacheInvalidator do
3636
{"organization_exchange", "suspension_removed", :suspension_removed},
3737
{"project_exchange", "created", :created_project},
3838
{"project_exchange", "updated", :updated_project},
39-
{"project_exchange", "deleted", :deleted_project},
39+
{"project_exchange", "soft_deleted", :deleted_project},
4040
{"user_exchange", "favorite_created", :starred},
4141
{"user_exchange", "favorite_deleted", :unstarred}
4242
]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class AddDeletedAtForProjects < ActiveRecord::Migration[5.1]
2+
def change
3+
add_column :projects, :deleted_at, :datetime, null: true, default: nil
4+
add_column :projects, :deleted_by, :uuid, null: true, default: nil
5+
end
6+
end

projecthub/lib/projecthub/api/grpc_server.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ defmodule Projecthub.Api.GrpcServer do
338338
if user do
339339
case find_project(req) do
340340
{:ok, project} ->
341-
{:ok, _} = Project.destroy(project, user)
341+
{:ok, _} = Project.soft_destroy(project, user)
342342
DestroyResponse.new(metadata: status_ok(req))
343343

344344
{:error, :not_found} ->

projecthub/lib/projecthub/events/project_deleted.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
defmodule Projecthub.Events.ProjectDeleted do
2-
def publish(project) do
2+
@soft_deleted_routing_key "soft_deleted"
3+
@deleted_routing_key "deleted"
4+
5+
def publish(project, opts \\ []) do
36
timestamp = DateTime.utc_now() |> DateTime.to_unix()
47

58
event =
@@ -10,11 +13,12 @@ defmodule Projecthub.Events.ProjectDeleted do
1013
)
1114

1215
message = InternalApi.Projecthub.ProjectDeleted.encode(event)
16+
routing_key = if opts[:soft_delete], do: @soft_deleted_routing_key, else: @deleted_routing_key
1317

1418
options = %{
1519
url: Application.fetch_env!(:projecthub, :amqp_url),
1620
exchange: "project_exchange",
17-
routing_key: "deleted"
21+
routing_key: routing_key
1822
}
1923

2024
Tackle.publish(message, options)

projecthub/lib/projecthub/models/project.ex

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ defmodule Projecthub.Models.Project do
6666

6767
field(:repository, :map, virtual: true)
6868
# embeds_one(:repository, Repository)
69+
70+
field(:deleted_at, :utc_datetime, default: nil)
71+
field(:deleted_by, :binary_id, default: nil)
6972
end
7073

7174
def create(request_id, user, org, project_spec, repo_details, integration_type, skip_onboarding \\ false) do
@@ -240,7 +243,9 @@ defmodule Projecthub.Models.Project do
240243
:attach_non_default_branch,
241244
:attach_pr,
242245
:attach_forked_pr,
243-
:attach_tag
246+
:attach_tag,
247+
:deleted_at,
248+
:deleted_by
244249
])
245250
|> validate_required([:name, :organization_id, :creator_id])
246251
|> validate_format(:name, ~r/\A[\w\-\.]+\z/,
@@ -251,7 +256,7 @@ defmodule Projecthub.Models.Project do
251256

252257
def find(id) do
253258
if id_is_uuid?(id) do
254-
case Repo.get_by(Project, id: id) do
259+
case from(Project) |> where([p], p.id == ^id) |> where_undeleted() |> Repo.one() do
255260
nil -> {:error, :not_found}
256261
project -> {:ok, project}
257262
end
@@ -263,7 +268,10 @@ defmodule Projecthub.Models.Project do
263268

264269
def find_in_org(org_id, id) do
265270
if id_is_uuid?(id) and id_is_uuid?(org_id) do
266-
case Repo.get_by(__MODULE__, id: id, organization_id: org_id) do
271+
case from(Project)
272+
|> where_undeleted()
273+
|> where([p], p.id == ^id and p.organization_id == ^org_id)
274+
|> Repo.one() do
267275
nil -> {:error, :not_found}
268276
project -> {:ok, project}
269277
end
@@ -281,14 +289,24 @@ defmodule Projecthub.Models.Project do
281289
end
282290

283291
def find_by_name(name, org_id) do
284-
case Repo.get_by(Project, name: name, organization_id: org_id) do
292+
case from(Project)
293+
|> where_undeleted()
294+
|> where([p], p.name == ^name and p.organization_id == ^org_id)
295+
|> Repo.one() do
285296
nil -> {:error, :not_found}
286297
project -> {:ok, project}
287298
end
288299
|> unwrap(&preload_repository/1)
289300
end
290301

291-
def destroy(project, user) do
302+
def soft_destroy(project, user) do
303+
{:ok, _} = update_record(project, %{deleted_at: DateTime.utc_now(), deleted_by: user.id})
304+
{:ok, _} = Events.ProjectDeleted.publish(project, soft_delete: true)
305+
306+
{:ok, nil}
307+
end
308+
309+
def hard_destroy(project, user) do
292310
{:ok, repository} = Repository.find_for_project(project.id)
293311
{:ok, _} = Repository.destroy(repository)
294312

@@ -306,13 +324,15 @@ defmodule Projecthub.Models.Project do
306324
Project
307325
|> where([p], p.organization_id == ^org_id)
308326
|> where([p], p.id in ^ids)
327+
|> where_undeleted()
309328
|> Repo.all()
310329
|> preload_repositories()
311330
end
312331

313332
def count_in_org(org_id) do
314333
Project
315334
|> where([p], p.organization_id == ^org_id)
335+
|> where_undeleted()
316336
|> Repo.aggregate(:count, :id)
317337
end
318338

@@ -334,6 +354,7 @@ defmodule Projecthub.Models.Project do
334354

335355
Project
336356
|> filter_by(options)
357+
|> where_undeleted()
337358
|> Repo.paginate(page: page, page_size: page_size)
338359
|> case do
339360
%{entries: entries} = paged_result ->
@@ -365,6 +386,7 @@ defmodule Projecthub.Models.Project do
365386

366387
Project
367388
|> filter_by(options)
389+
|> where_undeleted()
368390
|> order_by([p], asc: p.organization_id, asc: p.name)
369391
|> Repo.cursor_paginate(
370392
cursor_fields: [:organization_id, :name],
@@ -486,4 +508,9 @@ defmodule Projecthub.Models.Project do
486508
Projecthub.Workers.ProjectInit.lock_and_process(project_id)
487509
end)
488510
end
511+
512+
defp where_undeleted(query) do
513+
query
514+
|> where([project], is_nil(project.deleted_at))
515+
end
489516
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
defmodule Projecthub.Repo.Migrations.AddDeletedAtForProjects do
2+
use Ecto.Migration
3+
4+
def change do
5+
alter table("projects") do
6+
add :deleted_at, :utc_datetime, null: true, default: nil
7+
add :deleted_by, :binary_id, null: true, default: nil
8+
end
9+
end
10+
end

projecthub/test/projecthub/api/grpc_server_test.exs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2598,10 +2598,10 @@ defmodule Projecthub.Api.GrpcServerTest do
25982598
name: ""
25992599
)
26002600

2601-
with_mock Project, [:passthrough], destroy: fn _p, _u -> {:ok, nil} end do
2601+
with_mock Project, [:passthrough], soft_destroy: fn _p, _u -> {:ok, nil} end do
26022602
{:ok, response} = Stub.destroy(channel, request)
26032603

2604-
assert_called(Project.destroy(:_, :_))
2604+
assert_called(Project.soft_destroy(:_, :_))
26052605

26062606
assert response.metadata.status.code ==
26072607
:OK
@@ -2644,10 +2644,10 @@ defmodule Projecthub.Api.GrpcServerTest do
26442644
name: project.name
26452645
)
26462646

2647-
with_mock Project, [:passthrough], destroy: fn _p, _u -> {:ok, nil} end do
2647+
with_mock Project, [:passthrough], soft_destroy: fn _p, _u -> {:ok, nil} end do
26482648
{:ok, response} = Stub.destroy(channel, request)
26492649

2650-
assert_called(Project.destroy(:_, :_))
2650+
assert_called(Project.soft_destroy(:_, :_))
26512651

26522652
assert response.metadata.status.code ==
26532653
:OK

projecthub/test/projecthub/models/project_test.exs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ defmodule Projecthub.Models.ProjectTest do
512512
end
513513
end
514514

515-
describe ".destroy" do
515+
describe ".hard_destroy" do
516516
test "destroys repo, deploy key, schedulers and project records, removes key and hook from github, destroys artifact" do
517517
{:ok, project} = Support.Factories.Project.create_with_repo()
518518

@@ -524,7 +524,7 @@ defmodule Projecthub.Models.ProjectTest do
524524
{Schedulers, [], [delete_all: fn _p, _r -> {:ok, nil} end]},
525525
{Projecthub.Artifact, [], [destroy: fn _, _ -> nil end]}
526526
]) do
527-
{:ok, _} = Project.destroy(project, user)
527+
{:ok, _} = Project.hard_destroy(project, user)
528528
assert_called(Schedulers.delete_all(project, user.id))
529529
assert_called(Events.ProjectDeleted.publish(project))
530530
assert_called(Projecthub.Artifact.destroy(project.artifact_store_id, project.id))
@@ -536,6 +536,24 @@ defmodule Projecthub.Models.ProjectTest do
536536
end
537537
end
538538

539+
describe ".soft_destroy" do
540+
test "soft deletes the project updating deleted_at and deleted_by" do
541+
{:ok, project} = Support.Factories.Project.create_with_repo()
542+
543+
user = %User{github_token: "token"}
544+
545+
{:ok, _} = Project.soft_destroy(project, user)
546+
547+
# Assert project is not found by default find function
548+
assert {:error, :not_found} = Project.find(project.id)
549+
550+
# Assert soft deleted project
551+
soft_deleted_project = Project |> Repo.get(project.id)
552+
assert soft_deleted_project.deleted_at != nil
553+
assert soft_deleted_project.deleted_by == user.id
554+
end
555+
end
556+
539557
describe ".find" do
540558
test "when the project exists => returns the project" do
541559
{:ok, project} = Support.Factories.Project.create()

0 commit comments

Comments
 (0)