Improve how notifications are created and dispatched (#377)

This is some groundwork in preparation for webhooks.

Related to #373

Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
This commit is contained in:
Sergio C. Arteaga 2020-05-14 09:44:26 +02:00 committed by GitHub
parent c3f0961152
commit 9f24b87d77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1387 additions and 511 deletions

View File

@ -71,7 +71,7 @@ func TestAdd(t *testing.T) {
subscriptionJSON := `
{
"package_id": "00000000-0000-0000-0000-000000000001",
"notification_kind": 0
"event_kind": 0
}
`
testCases := []struct {
@ -158,7 +158,7 @@ func TestDelete(t *testing.T) {
subscriptionJSON := `
{
"package_id": "00000000-0000-0000-0000-000000000001",
"notification_kind": 0
"event_kind": 0
}
`
testCases := []struct {

View File

@ -12,6 +12,7 @@ import (
"github.com/artifacthub/hub/cmd/hub/handlers"
"github.com/artifacthub/hub/internal/chartrepo"
"github.com/artifacthub/hub/internal/email"
"github.com/artifacthub/hub/internal/event"
"github.com/artifacthub/hub/internal/hub"
"github.com/artifacthub/hub/internal/img/pg"
"github.com/artifacthub/hub/internal/notification"
@ -45,7 +46,7 @@ func main() {
es = s
}
// Setup and launch server
// Setup and launch http server
hSvc := &handlers.Services{
OrganizationManager: org.NewManager(db, es),
UserManager: user.NewManager(db, es),
@ -78,6 +79,19 @@ func main() {
}
}()
// Setup and launch events dispatcher
ctx, stop := context.WithCancel(context.Background())
var wg sync.WaitGroup
eSvc := &event.Services{
DB: db,
EventManager: event.NewManager(),
NotificationManager: notification.NewManager(),
SubscriptionManager: subscription.NewManager(db),
}
eventsDispatcher := event.NewDispatcher(eSvc)
wg.Add(1)
go eventsDispatcher.Run(ctx, &wg)
// Setup and launch notifications dispatcher
nSvc := &notification.Services{
DB: db,
@ -87,8 +101,6 @@ func main() {
PackageManager: pkg.NewManager(db),
}
notificationsDispatcher := notification.NewDispatcher(cfg, nSvc)
ctx, stopNotificationsDispatcher := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go notificationsDispatcher.Run(ctx, &wg)
@ -97,7 +109,7 @@ func main() {
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
<-shutdown
log.Info().Msg("Hub server shutting down..")
stopNotificationsDispatcher()
stop()
wg.Wait()
ctx, cancel := context.WithTimeout(context.Background(), cfg.GetDuration("server.shutdownTimeout"))
defer cancel()

View File

@ -46,7 +46,10 @@
{{ template "subscriptions/get_subscriptors.sql" }}
{{ template "subscriptions/get_user_subscriptions.sql" }}
{{ template "events/get_pending_event.sql" }}
{{ template "notifications/get_pending_notification.sql" }}
{{ template "notifications/update_notification_status.sql" }}
---- create above / drop below ----

View File

@ -0,0 +1,35 @@
-- get_pending_event returns a pending event if available, updating its
-- processed state if the event is processed successfully. This function should
-- be called from a transaction that should be rolled back if something goes
-- wrong processing the event.
create or replace function get_pending_event()
returns setof json as $$
declare
v_event_id uuid;
v_event json;
begin
-- Get pending event if available
select event_id, json_build_object(
'event_id', e.event_id,
'event_kind', e.event_kind_id,
'package_version', e.package_version,
'package_id', e.package_id
) into v_event_id, v_event
from event e
where e.processed = false
for update of e skip locked
limit 1;
if not found then
return;
end if;
-- Update event processed state
-- (this will be committed once the event is processed successfully)
update event set
processed = true,
processed_at = current_timestamp
where event_id = v_event_id;
return query select v_event;
end
$$ language plpgsql;

View File

@ -1,35 +1,22 @@
-- get_pending_notification returns a pending notification if available,
-- updating its processed state if the notification is delivered successfully.
-- This function should be called from a transaction that should be rolled back
-- if the notification is not delivered successfully.
-- get_pending_notification returns a pending notification if available.
create or replace function get_pending_notification()
returns setof json as $$
declare
v_notification_id uuid;
v_notification json;
begin
-- Get pending notification if available
select notification_id, json_build_object(
select json_build_object(
'notification_id', n.notification_id,
'package_version', n.package_version,
'package_id', n.package_id,
'notification_kind', n.notification_kind_id
) into v_notification_id, v_notification
'event', json_build_object(
'event_id', e.event_id,
'event_kind', e.event_kind_id,
'package_id', e.package_id,
'package_version', e.package_version
),
'user', json_build_object(
'email', u.email
)
)
from notification n
join event e using (event_id)
join "user" u using (user_id)
where n.processed = false
for update of n skip locked
limit 1;
if not found then
return;
end if;
-- Update notification processed state
-- (this will be committed once the notification is delivered successfully)
update notification set
processed = true,
processed_at = current_timestamp
where notification_id = v_notification_id;
return query select v_notification;
end
$$ language plpgsql;
$$ language sql;

View File

@ -0,0 +1,12 @@
-- update_notification_status updates the status of the provided notification.
create or replace function update_notification_status(
p_notification_id uuid,
p_processed boolean,
p_error text
) returns void as $$
update notification set
processed = p_processed,
processed_at = current_timestamp,
error = nullif(p_error, '')
where notification_id = p_notification_id;
$$ language sql;

View File

@ -141,16 +141,11 @@ begin
deprecated = excluded.deprecated,
updated_at = current_timestamp;
-- Register new release notification if package's latest version has been
-- updated and there are subscriptors for this package and notification kind
-- Register new release event if package's latest version has been updated
if semver_gt(p_pkg->>'version', v_previous_latest_version) then
perform * from subscription
where notification_kind_id = 0 -- New package release
and package_id = v_package_id;
if found then
insert into notification (package_id, package_version, notification_kind_id)
values (v_package_id, p_pkg->>'version', 0);
end if;
insert into event (package_id, package_version, event_kind_id)
values (v_package_id, p_pkg->>'version', 0)
on conflict do nothing;
end if;
end
$$ language plpgsql;

View File

@ -4,10 +4,10 @@ returns void as $$
insert into subscription (
user_id,
package_id,
notification_kind_id
event_kind_id
) values (
(p_subscription->>'user_id')::uuid,
(p_subscription->>'package_id')::uuid,
(p_subscription->>'notification_kind')::int
(p_subscription->>'event_kind')::int
);
$$ language sql;

View File

@ -4,5 +4,5 @@ returns void as $$
delete from subscription
where user_id = (p_subscription->>'user_id')::uuid
and package_id = (p_subscription->>'package_id')::uuid
and notification_kind_id = (p_subscription->>'notification_kind')::int;
and event_kind_id = (p_subscription->>'event_kind')::int;
$$ language sql;

View File

@ -3,13 +3,13 @@
create or replace function get_package_subscriptions(p_user_id uuid, p_package_id uuid)
returns setof json as $$
select coalesce(json_agg(json_build_object(
'notification_kind', notification_kind_id
'event_kind', event_kind_id
)), '[]')
from (
select *
from subscription
where user_id = p_user_id
and package_id = p_package_id
order by notification_kind_id asc
order by event_kind_id asc
) s;
$$ language sql;

View File

@ -1,12 +1,12 @@
-- get_subscriptors returns the users subscribed to the package provided for
-- the given notification kind.
create or replace function get_subscriptors(p_package_id uuid, p_notification_kind int)
-- the given event kind.
create or replace function get_subscriptors(p_package_id uuid, p_event_kind int)
returns setof json as $$
select coalesce(json_agg(json_build_object(
'email', u.email
'user_id', u.user_id
)), '[]')
from subscription s
join "user" u using (user_id)
where s.package_id = p_package_id
and s.notification_kind_id = p_notification_kind;
and s.event_kind_id = p_event_kind;
$$ language sql;

View File

@ -18,8 +18,8 @@ returns setof json as $$
),
'{"name": null, "display_name": null}'::jsonb
)),
'notification_kinds', (
select json_agg(distinct(notification_kind_id))
'event_kinds', (
select json_agg(distinct(event_kind_id))
from subscription
where package_id = sp.package_id
and user_id = p_user_id

View File

@ -147,33 +147,47 @@ create table if not exists user_starred_package (
primary key (user_id, package_id)
);
create table if not exists notification_kind (
notification_kind_id integer primary key,
create table if not exists event_kind (
event_kind_id integer primary key,
name text not null check (name <> '')
);
insert into notification_kind values (0, 'New package release');
insert into notification_kind values (1, 'Security alert');
insert into event_kind values (0, 'New package release');
insert into event_kind values (1, 'Security alert');
create table event (
event_id uuid primary key default gen_random_uuid(),
created_at timestamptz default current_timestamp not null,
processed boolean not null default false,
processed_at timestamptz,
package_version text not null check (package_version <> ''),
package_id uuid not null references package on delete cascade,
event_kind_id integer not null references event_kind on delete restrict,
unique (package_id, package_version, event_kind_id)
);
create index event_not_processed_idx on event (event_id) where processed = 'false';
create table if not exists subscription (
user_id uuid not null references "user" on delete cascade,
package_id uuid not null references package on delete cascade,
event_kind_id integer not null references event_kind on delete restrict,
primary key (user_id, package_id, event_kind_id)
);
create table notification (
notification_id uuid primary key default gen_random_uuid(),
created_at timestamptz default current_timestamp not null,
processed boolean not null default false,
processed_at timestamptz,
package_version text not null check (package_version <> ''),
package_id uuid not null references package on delete cascade,
notification_kind_id integer not null references notification_kind on delete restrict
error text check (error <> ''),
event_id uuid not null references event on delete restrict,
user_id uuid not null references "user" on delete cascade,
unique (event_id, user_id)
);
create index notification_not_processed_idx on notification (notification_id) where processed = 'false';
create table if not exists subscription (
user_id uuid not null references "user" on delete cascade,
package_id uuid not null references package on delete cascade,
notification_kind_id integer not null references notification_kind on delete restrict,
primary key (user_id, package_id, notification_kind_id)
);
{{ if eq .loadSampleData "true" }}
{{ template "data/sample.sql" }}
{{ end }}

View File

@ -0,0 +1,66 @@
-- Start transaction and plan tests
begin;
select plan(4);
-- Declare some variables
\set event1ID '00000000-0000-0000-0000-000000000001'
\set package1ID '00000000-0000-0000-0000-000000000001'
-- No pending events available yet
select is_empty(
$$ select get_pending_event()::jsonb $$,
'Should not return an event'
);
-- Seed some data
insert into package (
package_id,
name,
latest_version,
package_kind_id
) values (
:'package1ID',
'Package 1',
'1.0.0',
1
);
insert into event (event_id, package_version, package_id, event_kind_id)
values (:'event1ID', '1.0.0', :'package1ID', 0);
savepoint before_getting_event;
-- Run some tests
select is(
get_pending_event()::jsonb,
'{
"event_id": "00000000-0000-0000-0000-000000000001",
"event_kind": 0,
"package_version": "1.0.0",
"package_id": "00000000-0000-0000-0000-000000000001"
}'::jsonb,
'An event should be returned'
);
select results_eq(
$$
select processed from event
where event_id = '00000000-0000-0000-0000-000000000001'
$$,
$$
values (true)
$$,
'Event should be marked as processed'
);
rollback to before_getting_event;
select results_eq(
$$
select processed from event
where event_id = '00000000-0000-0000-0000-000000000001'
$$,
$$
values (false)
$$,
'Event should not be marked as processed as transaction was rolled back'
);
-- Finish tests and rollback transaction
select * from finish();
rollback;

View File

@ -1,18 +1,21 @@
-- Start transaction and plan tests
begin;
select plan(4);
select plan(2);
-- Declare some variables
\set notification1ID '00000000-0000-0000-0000-000000000001'
\set user1ID '00000000-0000-0000-0000-000000000001'
\set package1ID '00000000-0000-0000-0000-000000000001'
\set event1ID '00000000-0000-0000-0000-000000000001'
\set notification1ID '00000000-0000-0000-0000-000000000001'
-- No pending notifications available yet
-- No pending events available yet
select is_empty(
$$ select get_pending_notification()::jsonb $$,
'Should not return a notification'
);
-- Seed some data
insert into "user" (user_id, alias, email) values (:'user1ID', 'user1', 'user1@email.com');
insert into package (
package_id,
name,
@ -24,41 +27,27 @@ insert into package (
'1.0.0',
1
);
insert into notification (notification_id, package_version, package_id, notification_kind_id)
values (:'notification1ID', '1.0.0', :'package1ID', 0);
savepoint before_getting_notification;
insert into event (event_id, package_version, package_id, event_kind_id)
values (:'event1ID', '1.0.0', :'package1ID', 0);
insert into notification (notification_id, event_id, user_id)
values (:'notification1ID', :'event1ID', :'user1ID');
-- Run some tests
select is(
get_pending_notification()::jsonb,
'{
"notification_id": "00000000-0000-0000-0000-000000000001",
"package_version": "1.0.0",
"package_id": "00000000-0000-0000-0000-000000000001",
"notification_kind": 0
}'::jsonb,
'Notification should be returned'
);
select results_eq(
$$
select processed from notification
where notification_id = '00000000-0000-0000-0000-000000000001'
$$,
$$
values (true)
$$,
'Notification should be marked as processed'
);
rollback to before_getting_notification;
select results_eq(
$$
select processed from notification
where notification_id = '00000000-0000-0000-0000-000000000001'
$$,
$$
values (false)
$$,
'Notification should not be marked as processed as transaction was rolled back'
"event": {
"event_id": "00000000-0000-0000-0000-000000000001",
"event_kind": 0,
"package_id": "00000000-0000-0000-0000-000000000001",
"package_version": "1.0.0"
},
"user": {
"email": "user1@email.com"
}
}'::jsonb,
'A notification should be returned'
);
-- Finish tests and rollback transaction

View File

@ -0,0 +1,58 @@
-- Start transaction and plan tests
begin;
select plan(2);
-- Declare some variables
\set user1ID '00000000-0000-0000-0000-000000000001'
\set package1ID '00000000-0000-0000-0000-000000000001'
\set event1ID '00000000-0000-0000-0000-000000000001'
\set notification1ID '00000000-0000-0000-0000-000000000001'
-- Seed some data
insert into "user" (user_id, alias, email) values (:'user1ID', 'user1', 'user1@email.com');
insert into package (
package_id,
name,
latest_version,
package_kind_id
) values (
:'package1ID',
'Package 1',
'1.0.0',
1
);
insert into event (event_id, package_version, package_id, event_kind_id)
values (:'event1ID', '1.0.0', :'package1ID', 0);
insert into notification (notification_id, event_id, user_id)
values (:'notification1ID', :'event1ID', :'user1ID');
-- Run some tests
select results_eq(
$$
select processed, processed_at, error from notification
where notification_id = '00000000-0000-0000-0000-000000000001'
$$,
$$
values (false, null::timestamptz, null::text)
$$,
'Notification has not been processed yet'
);
-- Update notification status
select update_notification_status(:'notification1ID', true, 'fake error');
-- Run some tests
select results_eq(
$$
select processed, error from notification
where notification_id = '00000000-0000-0000-0000-000000000001'
$$,
$$
values (true, 'fake error')
$$,
'Notification has been processed'
);
-- Finish tests and rollback transaction
select * from finish();
rollback;

View File

@ -1,6 +1,6 @@
-- Start transaction and plan tests
begin;
select plan(16);
select plan(15);
-- Declare some variables
\set org1ID '00000000-0000-0000-0000-000000000001'
@ -141,18 +141,13 @@ select results_eq(
select is_empty(
$$
select *
from notification n
from event e
join package p using (package_id)
where p.name = 'package1'
$$,
'No new release notifications should exist for first version of package1'
'No new release event should exist for first version of package1'
);
-- Subscribe user1 to package1 new releases notifications
insert into subscription (user_id, package_id, notification_kind_id)
select :'user1ID', package_id, 0
from package where name = 'package1';
-- Register a new version of the package previously registered
select register_package('
{
@ -252,12 +247,12 @@ select is_empty(
select isnt_empty(
$$
select *
from notification n
from event e
join package p using (package_id)
where p.name = 'package1'
and n.package_version = '2.0.0'
and e.package_version = '2.0.0'
$$,
'New release notification should exist for package1 version 2.0.0'
'New release event should exist for package1 version 2.0.0'
);
-- Register an old version of the package previously registered
@ -353,12 +348,12 @@ select results_eq(
select is_empty(
$$
select *
from notification n
from event e
join package p using (package_id)
where p.name = 'package1'
and n.package_version = '0.0.9'
and e.package_version = '0.0.9'
$$,
'No new release notifications should exist for package1 version 0.0.9'
'No new release event should exist for package1 version 0.0.9'
);
-- Register package that belongs to an organization and check it succeeded
@ -392,39 +387,17 @@ select results_eq(
null::uuid
)
$$,
'Package that belongs to organization should exist'
'Package3 that belongs to organization should exist'
);
select is_empty(
$$
select *
from notification n
from event e
join package p using (package_id)
where p.name = 'package3'
and n.package_version = '1.0.0'
and e.package_version = '1.0.0'
$$,
'No new release notifications should exist for first version of package3'
);
-- Register a new version of the package previously registered
select register_package('
{
"kind": 1,
"name": "package3",
"display_name": "Package 3",
"description": "description",
"version": "2.0.0",
"organization_id": "00000000-0000-0000-0000-000000000001"
}
');
select is_empty(
$$
select *
from notification n
join package p using (package_id)
where p.name = 'package3'
and n.package_version = '2.0.0'
$$,
'No new release notifications should exist for new version of package3 (no subscriptors)'
'No new release event should exist for first version of package3'
);
-- Finish tests and rollback transaction

View File

@ -26,7 +26,7 @@ select add_subscription('
{
"user_id": "00000000-0000-0000-0000-000000000001",
"package_id": "00000000-0000-0000-0000-000000000001",
"notification_kind": 0
"event_kind": 0
}
'::jsonb);
@ -36,7 +36,7 @@ select results_eq(
select
user_id,
package_id,
notification_kind_id
event_kind_id
from subscription
$$,
$$

View File

@ -20,7 +20,7 @@ insert into package (
'1.0.0',
1
);
insert into subscription (user_id, package_id, notification_kind_id)
insert into subscription (user_id, package_id, event_kind_id)
values (:'user1ID', :'package1ID', 0);
-- Delete subscription
@ -28,7 +28,7 @@ select delete_subscription('
{
"user_id": "00000000-0000-0000-0000-000000000001",
"package_id": "00000000-0000-0000-0000-000000000001",
"notification_kind": 0
"event_kind": 0
}
'::jsonb);
@ -39,7 +39,7 @@ select is_empty(
from subscription
where user_id = '00000000-0000-0000-0000-000000000001'
and package_id = '00000000-0000-0000-0000-000000000001'
and notification_kind_id = 0
and event_kind_id = 0
$$,
'Subscription should not exist'
);

View File

@ -24,16 +24,16 @@ insert into package (
'1.0.0',
1
);
insert into subscription (user_id, package_id, notification_kind_id)
insert into subscription (user_id, package_id, event_kind_id)
values (:'user1ID', :'package1ID', 0);
-- Run some tests
select is(
get_package_subscriptions(:'user1ID', :'package1ID')::jsonb,
'[{
"notification_kind": 0
"event_kind": 0
}]'::jsonb,
'A subscription with notification kind 0 should be returned'
'A subscription with event kind 0 should be returned'
);
select is(
get_package_subscriptions(:'user2ID', :'package1ID')::jsonb,

View File

@ -27,11 +27,11 @@ insert into package (
'1.0.0',
1
);
insert into subscription (user_id, package_id, notification_kind_id)
insert into subscription (user_id, package_id, event_kind_id)
values (:'user1ID', :'package1ID', 0);
insert into subscription (user_id, package_id, notification_kind_id)
insert into subscription (user_id, package_id, event_kind_id)
values (:'user2ID', :'package1ID', 0);
insert into subscription (user_id, package_id, notification_kind_id)
insert into subscription (user_id, package_id, event_kind_id)
values (:'user3ID', :'package1ID', 1);
-- Run some tests
@ -39,10 +39,10 @@ select is(
get_subscriptors(:'package1ID', 0)::jsonb,
'[
{
"email": "user1@email.com"
"user_id": "00000000-0000-0000-0000-000000000001"
},
{
"email": "user2@email.com"
"user_id": "00000000-0000-0000-0000-000000000002"
}
]'::jsonb,
'Two subscriptors expected for package1 and kind new releases'

View File

@ -51,11 +51,11 @@ insert into package (
1,
:'org1ID'
);
insert into subscription (user_id, package_id, notification_kind_id)
insert into subscription (user_id, package_id, event_kind_id)
values (:'user1ID', :'package1ID', 0);
insert into subscription (user_id, package_id, notification_kind_id)
insert into subscription (user_id, package_id, event_kind_id)
values (:'user1ID', :'package1ID', 1);
insert into subscription (user_id, package_id, notification_kind_id)
insert into subscription (user_id, package_id, event_kind_id)
values (:'user1ID', :'package2ID', 0);
-- Run some tests
@ -74,7 +74,7 @@ select is(
"name": "repo1",
"display_name": "Repo 1"
},
"notification_kinds": [0, 1]
"event_kinds": [0, 1]
}, {
"package_id": "00000000-0000-0000-0000-000000000002",
"kind": 1,
@ -85,7 +85,7 @@ select is(
"organization_name": "org1",
"organization_display_name": "Organization 1",
"chart_repository": null,
"notification_kinds": [0]
"event_kinds": [0]
}]'::jsonb,
'Two subscriptions should be returned'
);

View File

@ -1,6 +1,6 @@
-- Start transaction and plan tests
begin;
select plan(83);
select plan(87);
-- Check default_text_search_config is correct
select results_eq(
@ -16,11 +16,12 @@ select has_extension('pgcrypto');
select tables_are(array[
'chart_repository',
'email_verification_code',
'event',
'event_kind',
'image',
'image_version',
'maintainer',
'notification',
'notification_kind',
'organization',
'package',
'package__maintainer',
@ -51,6 +52,19 @@ select columns_are('email_verification_code', array[
'user_id',
'created_at'
]);
select columns_are('event', array[
'event_id',
'created_at',
'processed',
'processed_at',
'package_version',
'package_id',
'event_kind_id'
]);
select columns_are('event_kind', array[
'event_kind_id',
'name'
]);
select columns_are('image', array[
'image_id',
'original_hash'
@ -70,13 +84,9 @@ select columns_are('notification', array[
'created_at',
'processed',
'processed_at',
'package_version',
'package_id',
'notification_kind_id'
]);
select columns_are('notification_kind', array[
'notification_kind_id',
'name'
'error',
'event_id',
'user_id'
]);
select columns_are('organization', array[
'organization_id',
@ -137,7 +147,7 @@ select columns_are('snapshot', array[
select columns_are('subscription', array[
'user_id',
'package_id',
'notification_kind_id'
'event_kind_id'
]);
select columns_are('user', array[
'user_id',
@ -175,6 +185,11 @@ select indexes_are('email_verification_code', array[
'email_verification_code_pkey',
'email_verification_code_user_id_key'
]);
select indexes_are('event', array[
'event_pkey',
'event_not_processed_idx',
'event_package_id_package_version_event_kind_id_key'
]);
select indexes_are('image', array[
'image_pkey',
'image_original_hash_key'
@ -188,7 +203,8 @@ select indexes_are('maintainer', array[
]);
select indexes_are('notification', array[
'notification_pkey',
'notification_not_processed_idx'
'notification_not_processed_idx',
'notification_event_id_user_id_key'
]);
select indexes_are('organization', array[
'organization_pkey',
@ -283,7 +299,10 @@ select has_function('get_package_subscriptions');
select has_function('get_subscriptors');
select has_function('get_user_subscriptions');
select has_function('get_pending_event');
select has_function('get_pending_notification');
select has_function('update_notification_status');
-- Check package kinds exist
select results_eq(
@ -296,14 +315,14 @@ select results_eq(
'Package kinds should exist'
);
-- Check notification kinds exist
-- Check event kinds exist
select results_eq(
'select * from notification_kind',
'select * from event_kind',
$$ values
(0, 'New package release'),
(1, 'Security alert')
$$,
'Package kinds should exist'
'Event kinds should exist'
);
-- Finish tests and rollback transaction

View File

@ -0,0 +1,70 @@
package event
import (
"context"
"sync"
"github.com/artifacthub/hub/internal/hub"
)
const (
defaultNumWorkers = 2
)
// Services is a wrapper around several internal services used to handle
// events processing.
type Services struct {
DB hub.DB
EventManager hub.EventManager
SubscriptionManager hub.SubscriptionManager
NotificationManager hub.NotificationManager
}
// Dispatcher handles a group of workers in charge of processing events that
// happen in the Hub.
type Dispatcher struct {
numWorkers int
workers []*Worker
}
// NewDispatcher creates a new Dispatcher instance.
func NewDispatcher(svc *Services, opts ...func(d *Dispatcher)) *Dispatcher {
d := &Dispatcher{
numWorkers: defaultNumWorkers,
}
for _, o := range opts {
o(d)
}
d.workers = make([]*Worker, 0, d.numWorkers)
for i := 0; i < d.numWorkers; i++ {
d.workers = append(d.workers, NewWorker(svc))
}
return d
}
// WithNumWorkers allows providing a specific number of workers for a
// Dispatcher instance.
func WithNumWorkers(n int) func(d *Dispatcher) {
return func(d *Dispatcher) {
d.numWorkers = n
}
}
// Run starts the workers and lets them run until the dispatcher is asked to
// stop via the context provided.
func (d *Dispatcher) Run(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
// Start workers
wwg := &sync.WaitGroup{}
wctx, stopWorkers := context.WithCancel(context.Background())
for _, w := range d.workers {
wwg.Add(1)
go w.Run(wctx, wwg)
}
// Stop workers when dispatcher is asked to stop
<-ctx.Done()
stopWorkers()
wwg.Wait()
}

View File

@ -0,0 +1,28 @@
package event
import (
"context"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestDispatcher(t *testing.T) {
// Setup dispatcher
d := NewDispatcher(&Services{}, WithNumWorkers(0))
// Run it
ctx, stopDispatcher := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go d.Run(ctx, &wg)
// Check it stops as expected when asked to do so
stopDispatcher()
assert.Eventually(t, func() bool {
wg.Wait()
return true
}, 2*time.Second, 100*time.Millisecond)
}

31
internal/event/manager.go Normal file
View File

@ -0,0 +1,31 @@
package event
import (
"context"
"encoding/json"
"github.com/artifacthub/hub/internal/hub"
"github.com/jackc/pgx/v4"
)
// Manager provides an API to manage events.
type Manager struct{}
// NewManager creates a new Manager instance.
func NewManager() *Manager {
return &Manager{}
}
// GetPending returns a pending event to be processed if available.
func (m *Manager) GetPending(ctx context.Context, tx pgx.Tx) (*hub.Event, error) {
query := "select get_pending_event()"
var dataJSON []byte
if err := tx.QueryRow(ctx, query).Scan(&dataJSON); err != nil {
return nil, err
}
var e *hub.Event
if err := json.Unmarshal(dataJSON, &e); err != nil {
return nil, err
}
return e, nil
}

View File

@ -0,0 +1,62 @@
package event
import (
"context"
"errors"
"os"
"testing"
"github.com/artifacthub/hub/internal/hub"
"github.com/artifacthub/hub/internal/tests"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var errFake = errors.New("fake error for tests")
func TestMain(m *testing.M) {
zerolog.SetGlobalLevel(zerolog.Disabled)
os.Exit(m.Run())
}
func TestGetPending(t *testing.T) {
dbQuery := "select get_pending_event()"
ctx := context.Background()
t.Run("database error", func(t *testing.T) {
tx := &tests.TXMock{}
tx.On("QueryRow", dbQuery).Return(nil, tests.ErrFakeDatabaseFailure)
m := NewManager()
dataJSON, err := m.GetPending(ctx, tx)
assert.Equal(t, tests.ErrFakeDatabaseFailure, err)
assert.Nil(t, dataJSON)
tx.AssertExpectations(t)
})
t.Run("database query succeeded", func(t *testing.T) {
expectedEvent := &hub.Event{
EventID: "00000000-0000-0000-0000-000000000001",
PackageVersion: "1.0.0",
PackageID: "00000000-0000-0000-0000-000000000001",
EventKind: hub.NewRelease,
}
tx := &tests.TXMock{}
tx.On("QueryRow", dbQuery).Return([]byte(`
{
"event_id": "00000000-0000-0000-0000-000000000001",
"package_version": "1.0.0",
"package_id": "00000000-0000-0000-0000-000000000001",
"event_kind": 0
}
`), nil)
m := NewManager()
e, err := m.GetPending(context.Background(), tx)
require.NoError(t, err)
assert.Equal(t, expectedEvent, e)
tx.AssertExpectations(t)
})
}

21
internal/event/mock.go Normal file
View File

@ -0,0 +1,21 @@
package event
import (
"context"
"github.com/artifacthub/hub/internal/hub"
"github.com/jackc/pgx/v4"
"github.com/stretchr/testify/mock"
)
// ManagerMock is a mock implementation of the EventManager interface.
type ManagerMock struct {
mock.Mock
}
// GetPending implements the EventManager interface.
func (m *ManagerMock) GetPending(ctx context.Context, tx pgx.Tx) (*hub.Event, error) {
args := m.Called(ctx, tx)
data, _ := args.Get(0).(*hub.Event)
return data, args.Error(1)
}

88
internal/event/worker.go Normal file
View File

@ -0,0 +1,88 @@
package event
import (
"context"
"errors"
"sync"
"time"
"github.com/artifacthub/hub/internal/util"
"github.com/jackc/pgx/v4"
"github.com/rs/zerolog/log"
)
const (
pauseOnEmptyQueue = 30 * time.Second
pauseOnError = 10 * time.Second
)
// Worker is in charge of handling events that happen in the Hub.
type Worker struct {
svc *Services
}
// NewWorker creates a new Worker instance.
func NewWorker(svc *Services) *Worker {
return &Worker{
svc: svc,
}
}
// Run is the main loop of the worker. It calls processEvent periodically until
// it's asked to stop via the context provided.
func (w *Worker) Run(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
for {
err := w.processEvent(ctx)
switch err {
case nil:
select {
case <-ctx.Done():
return
default:
}
case pgx.ErrNoRows:
select {
case <-time.After(pauseOnEmptyQueue):
case <-ctx.Done():
return
}
default:
select {
case <-time.After(pauseOnError):
case <-ctx.Done():
return
}
}
}
}
// processEvent gets a pending event from the database and processes it.
func (w *Worker) processEvent(ctx context.Context) error {
return util.DBTransact(ctx, w.svc.DB, func(tx pgx.Tx) error {
// Get pending event to process
e, err := w.svc.EventManager.GetPending(ctx, tx)
if err != nil {
if !errors.Is(err, pgx.ErrNoRows) {
log.Error().Err(err).Msg("error getting pending event")
}
return err
}
// Register event notifications
users, err := w.svc.SubscriptionManager.GetSubscriptors(ctx, e.PackageID, e.EventKind)
if err != nil {
log.Error().Err(err).Msg("error getting subscriptors")
return err
}
for _, u := range users {
if err := w.svc.NotificationManager.Add(ctx, tx, e.EventID, u.UserID); err != nil {
log.Error().Err(err).Msg("error adding notification")
return err
}
}
return nil
})
}

View File

@ -0,0 +1,173 @@
package event
import (
"context"
"sync"
"testing"
"time"
"github.com/artifacthub/hub/internal/hub"
"github.com/artifacthub/hub/internal/notification"
"github.com/artifacthub/hub/internal/subscription"
"github.com/artifacthub/hub/internal/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestWorker(t *testing.T) {
t.Run("error getting pending event", func(t *testing.T) {
sw := newServicesWrapper()
sw.db.On("Begin", mock.Anything).Return(sw.tx, nil)
sw.em.On("GetPending", mock.Anything, mock.Anything).Return(nil, errFake)
sw.tx.On("Rollback", mock.Anything).Return(nil)
w := NewWorker(sw.svc)
go w.Run(sw.ctx, sw.wg)
sw.assertExpectations(t)
})
t.Run("error getting subscriptors", func(t *testing.T) {
sw := newServicesWrapper()
sw.db.On("Begin", mock.Anything).Return(sw.tx, nil)
sw.em.On("GetPending", mock.Anything, mock.Anything).Return(&hub.Event{
EventKind: hub.NewRelease,
PackageID: "packageID",
}, nil)
sw.sm.On("GetSubscriptors", mock.Anything, "packageID", hub.NewRelease).Return(nil, errFake)
sw.tx.On("Rollback", mock.Anything).Return(nil)
w := NewWorker(sw.svc)
go w.Run(sw.ctx, sw.wg)
sw.assertExpectations(t)
})
t.Run("no subscriptors found", func(t *testing.T) {
sw := newServicesWrapper()
sw.db.On("Begin", mock.Anything).Return(sw.tx, nil)
sw.em.On("GetPending", mock.Anything, mock.Anything).Return(&hub.Event{
EventKind: hub.NewRelease,
PackageID: "packageID",
}, nil)
sw.sm.On("GetSubscriptors", mock.Anything, "packageID", hub.NewRelease).Return([]*hub.User{}, nil)
sw.tx.On("Commit", mock.Anything).Return(nil)
w := NewWorker(sw.svc)
go w.Run(sw.ctx, sw.wg)
sw.assertExpectations(t)
})
t.Run("error adding notification", func(t *testing.T) {
sw := newServicesWrapper()
sw.db.On("Begin", mock.Anything).Return(sw.tx, nil)
sw.em.On("GetPending", mock.Anything, mock.Anything).Return(&hub.Event{
EventID: "eventID",
EventKind: hub.NewRelease,
PackageID: "packageID",
}, nil)
sw.sm.On("GetSubscriptors", mock.Anything, "packageID", hub.NewRelease).Return([]*hub.User{
{UserID: "userID"},
}, nil)
sw.nm.On("Add", mock.Anything, mock.Anything, "eventID", "userID").Return(errFake)
sw.tx.On("Rollback", mock.Anything).Return(nil)
w := NewWorker(sw.svc)
go w.Run(sw.ctx, sw.wg)
sw.assertExpectations(t)
})
t.Run("adding one notification succeeded", func(t *testing.T) {
sw := newServicesWrapper()
sw.db.On("Begin", mock.Anything).Return(sw.tx, nil)
sw.em.On("GetPending", mock.Anything, mock.Anything).Return(&hub.Event{
EventID: "eventID",
EventKind: hub.NewRelease,
PackageID: "packageID",
}, nil)
sw.sm.On("GetSubscriptors", mock.Anything, "packageID", hub.NewRelease).Return([]*hub.User{
{UserID: "userID"},
}, nil)
sw.nm.On("Add", mock.Anything, mock.Anything, "eventID", "userID").Return(nil)
sw.tx.On("Commit", mock.Anything).Return(nil)
w := NewWorker(sw.svc)
go w.Run(sw.ctx, sw.wg)
sw.assertExpectations(t)
})
t.Run("adding two notifications succeeded", func(t *testing.T) {
sw := newServicesWrapper()
sw.db.On("Begin", mock.Anything).Return(sw.tx, nil)
sw.em.On("GetPending", mock.Anything, mock.Anything).Return(&hub.Event{
EventID: "eventID",
EventKind: hub.NewRelease,
PackageID: "packageID",
}, nil)
sw.sm.On("GetSubscriptors", mock.Anything, "packageID", hub.NewRelease).Return([]*hub.User{
{UserID: "user1ID"},
{UserID: "user2ID"},
}, nil)
sw.nm.On("Add", mock.Anything, mock.Anything, "eventID", "user1ID").Return(nil)
sw.nm.On("Add", mock.Anything, mock.Anything, "eventID", "user2ID").Return(nil)
sw.tx.On("Commit", mock.Anything).Return(nil)
w := NewWorker(sw.svc)
go w.Run(sw.ctx, sw.wg)
sw.assertExpectations(t)
})
}
type servicesWrapper struct {
ctx context.Context
stopWorker context.CancelFunc
wg *sync.WaitGroup
db *tests.DBMock
tx *tests.TXMock
em *ManagerMock
sm *subscription.ManagerMock
nm *notification.ManagerMock
svc *Services
}
func newServicesWrapper() *servicesWrapper {
// Context and wait group used for Worker.Run()
ctx, stopWorker := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
db := &tests.DBMock{}
tx := &tests.TXMock{}
em := &ManagerMock{}
sm := &subscription.ManagerMock{}
nm := &notification.ManagerMock{}
return &servicesWrapper{
ctx: ctx,
stopWorker: stopWorker,
wg: &wg,
db: db,
tx: tx,
em: em,
sm: sm,
nm: nm,
svc: &Services{
DB: db,
EventManager: em,
SubscriptionManager: sm,
NotificationManager: nm,
},
}
}
func (sw *servicesWrapper) assertExpectations(t *testing.T) {
sw.stopWorker()
assert.Eventually(t, func() bool {
sw.wg.Wait()
return true
}, 2*time.Second, 100*time.Millisecond)
sw.db.AssertExpectations(t)
sw.tx.AssertExpectations(t)
sw.em.AssertExpectations(t)
sw.sm.AssertExpectations(t)
sw.nm.AssertExpectations(t)
}

32
internal/hub/event.go Normal file
View File

@ -0,0 +1,32 @@
package hub
import (
"context"
"github.com/jackc/pgx/v4"
)
// Event represents the details of an event related to a package.
type Event struct {
EventID string `json:"event_id"`
EventKind EventKind `json:"event_kind"`
PackageID string `json:"package_id"`
PackageVersion string `json:"package_version"`
}
// EventKind represents the kind of an event.
type EventKind int64
const (
// NewRelease represents an event for a new package release.
NewRelease EventKind = 0
// SecurityAlert represents an event for a security alert.
SecurityAlert EventKind = 1
)
// EventManager describes the methods an EventManager implementation must
// provide.
type EventManager interface {
GetPending(ctx context.Context, tx pgx.Tx) (*Event, error)
}

View File

@ -3,31 +3,33 @@ package hub
import (
"context"
"github.com/artifacthub/hub/internal/email"
"github.com/jackc/pgx/v4"
)
// Notification represents the details of a notification that will be sent to
// a set of subscribers interested on it.
type Notification struct {
NotificationID string `json:"notification_id"`
PackageVersion string `json:"package_version"`
PackageID string `json:"package_id"`
NotificationKind NotificationKind `json:"notification_kind"`
// NotificationEmailDataCache describes the methods a NotificationEmailDataCache
// implementation must provide.
type NotificationEmailDataCache interface {
Get(ctx context.Context, e *Event) (email.Data, error)
}
// NotificationKind represents the kind of a notification.
type NotificationKind int64
// Notification represents the details of a notification pending to be delivered.
type Notification struct {
NotificationID string `json:"notification_id"`
Event *Event `json:"event"`
User *User `json:"user"`
}
const (
// NewRelease represents a notification for a new package release.
NewRelease NotificationKind = 0
// SecurityAlert represents a notification for a security alert.
SecurityAlert NotificationKind = 1
)
// NotificationManager describes the methods a NotificationManager
// NotificationManager describes the methods an NotificationManager
// implementation must provide.
type NotificationManager interface {
Add(ctx context.Context, tx pgx.Tx, eventID, userID string) error
GetPending(ctx context.Context, tx pgx.Tx) (*Notification, error)
UpdateStatus(
ctx context.Context,
tx pgx.Tx,
notificationID string,
delivered bool,
deliveryErr error,
) error
}

View File

@ -3,11 +3,11 @@ package hub
import "context"
// Subscription represents a user's subscription to receive notifications about
// a given package.
// a given package and event kind.
type Subscription struct {
UserID string `json:"user_id"`
PackageID string `json:"package_id"`
NotificationKind NotificationKind `json:"notification_kind"`
UserID string `json:"user_id"`
PackageID string `json:"package_id"`
EventKind EventKind `json:"event_kind"`
}
// SubscriptionManager describes the methods a SubscriptionManager
@ -17,5 +17,5 @@ type SubscriptionManager interface {
Delete(ctx context.Context, s *Subscription) error
GetByPackageJSON(ctx context.Context, packageID string) ([]byte, error)
GetByUserJSON(ctx context.Context) ([]byte, error)
GetSubscriptors(ctx context.Context, packageID string, notificationKind NotificationKind) ([]*User, error)
GetSubscriptors(ctx context.Context, packageID string, eventKind EventKind) ([]*User, error)
}

View File

@ -36,10 +36,10 @@ func NewDispatcher(cfg *viper.Viper, svc *Services, opts ...func(d *Dispatcher))
for _, o := range opts {
o(d)
}
baseURL := cfg.GetString("server.baseURL")
emailDataCache := NewEmailDataCache(svc.PackageManager, cfg.GetString("server.baseURL"))
d.workers = make([]*Worker, 0, d.numWorkers)
for i := 0; i < d.numWorkers; i++ {
d.workers = append(d.workers, NewWorker(svc, baseURL))
d.workers = append(d.workers, NewWorker(svc, emailDataCache))
}
return d
}

View File

@ -14,7 +14,7 @@ func TestDispatcher(t *testing.T) {
// Setup dispatcher
cfg := viper.New()
cfg.Set("server.baseURL", "http://localhost:8000")
d := NewDispatcher(cfg, nil, WithNumWorkers(0))
d := NewDispatcher(cfg, &Services{}, WithNumWorkers(0))
// Run it
ctx, stopDispatcher := context.WithCancel(context.Background())

View File

@ -0,0 +1,109 @@
package notification
import (
"bytes"
"context"
"fmt"
"sync"
"github.com/artifacthub/hub/internal/email"
"github.com/artifacthub/hub/internal/hub"
)
// EmailDataCache is a cache to store the email data used when sending email
// notifications.
type EmailDataCache struct {
packageManager hub.PackageManager
baseURL string
mu sync.RWMutex
data map[string]email.Data
}
// NewEmailDataCache creates a new EmailDataCache instance.
func NewEmailDataCache(packageManager hub.PackageManager, baseURL string) *EmailDataCache {
return &EmailDataCache{
packageManager: packageManager,
baseURL: baseURL,
data: make(map[string]email.Data),
}
}
// Get returns the email data corresponding to the event provided. If the data
// is available in the cache, we just return it. Otherwise it is built, cached
// and returned.
func (c *EmailDataCache) Get(ctx context.Context, e *hub.Event) (email.Data, error) {
// Email data is already cached for the event provided
c.mu.RLock()
emailData, ok := c.data[e.EventID]
if ok {
c.mu.RUnlock()
return emailData, nil
}
c.mu.RUnlock()
// No email data cached for event provided. Build, cache and return it.
emailData, err := c.buildEmailData(ctx, e)
if err != nil {
return email.Data{}, err
}
c.mu.Lock()
c.data[e.EventID] = emailData
c.mu.Unlock()
return emailData, nil
}
// buildEmailData prepares the email data corresponding to the event provided.
func (c *EmailDataCache) buildEmailData(ctx context.Context, e *hub.Event) (email.Data, error) {
var subject string
var emailBody bytes.Buffer
switch e.EventKind {
case hub.NewRelease:
p, err := c.packageManager.Get(ctx, &hub.GetPackageInput{
PackageID: e.PackageID,
Version: e.PackageVersion,
})
if err != nil {
return email.Data{}, err
}
subject = fmt.Sprintf("%s version %s released", p.Name, e.PackageVersion)
publisher := p.OrganizationName
if publisher == "" {
publisher = p.UserAlias
}
if p.ChartRepository != nil {
publisher += "/" + p.ChartRepository.Name
}
var packagePath string
switch p.Kind {
case hub.Chart:
packagePath = fmt.Sprintf("/package/chart/%s/%s/%s",
p.ChartRepository.Name,
p.NormalizedName,
e.PackageVersion,
)
case hub.Falco:
packagePath = fmt.Sprintf("/package/falco/%s/%s", p.NormalizedName, e.PackageVersion)
case hub.OPA:
packagePath = fmt.Sprintf("/package/opa/%s/%s", p.NormalizedName, e.PackageVersion)
}
data := map[string]interface{}{
"publisher": publisher,
"kind": p.Kind,
"name": p.Name,
"version": e.PackageVersion,
"baseURL": c.baseURL,
"logoImageID": p.LogoImageID,
"packagePath": packagePath,
}
if err := newReleaseEmailTmpl.Execute(&emailBody, data); err != nil {
return email.Data{}, err
}
}
return email.Data{
Subject: subject,
Body: emailBody.Bytes(),
}, nil
}

View File

@ -3,9 +3,17 @@ package notification
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/artifacthub/hub/internal/hub"
"github.com/jackc/pgx/v4"
"github.com/satori/uuid"
)
var (
// ErrInvalidInput indicates that the input provided is not valid.
ErrInvalidInput = errors.New("invalid input")
)
// Manager provides an API to manage notifications.
@ -16,6 +24,19 @@ func NewManager() *Manager {
return &Manager{}
}
// Add adds the provided notification to the database.
func (m *Manager) Add(ctx context.Context, tx pgx.Tx, eventID, userID string) error {
if _, err := uuid.FromString(eventID); err != nil {
return fmt.Errorf("%w: %s", ErrInvalidInput, "invalid event id")
}
if _, err := uuid.FromString(userID); err != nil {
return fmt.Errorf("%w: %s", ErrInvalidInput, "invalid user id")
}
query := `insert into notification (event_id, user_id) values ($1, $2)`
_, err := tx.Exec(ctx, query, eventID, userID)
return err
}
// GetPending returns a pending notification to be delivered if available.
func (m *Manager) GetPending(ctx context.Context, tx pgx.Tx) (*hub.Notification, error) {
query := "select get_pending_notification()"
@ -29,3 +50,23 @@ func (m *Manager) GetPending(ctx context.Context, tx pgx.Tx) (*hub.Notification,
}
return n, nil
}
// UpdateStatus the provided notification status in the database.
func (m *Manager) UpdateStatus(
ctx context.Context,
tx pgx.Tx,
notificationID string,
processed bool,
processedErr error,
) error {
if _, err := uuid.FromString(notificationID); err != nil {
return fmt.Errorf("%w: %s", ErrInvalidInput, "invalid notification id")
}
query := "select update_notification_status($1::uuid, $2::boolean, $3::text)"
var processedErrStr string
if processedErr != nil {
processedErrStr = processedErr.Error()
}
_, err := tx.Exec(ctx, query, notificationID, processed, processedErrStr)
return err
}

View File

@ -2,13 +2,78 @@ package notification
import (
"context"
"errors"
"os"
"testing"
"github.com/artifacthub/hub/internal/hub"
"github.com/artifacthub/hub/internal/tests"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const validUUID = "00000000-0000-0000-0000-000000000001"
var errFake = errors.New("fake error for tests")
func TestMain(m *testing.M) {
zerolog.SetGlobalLevel(zerolog.Disabled)
os.Exit(m.Run())
}
func TestAdd(t *testing.T) {
dbQuery := `insert into notification (event_id, user_id) values ($1, $2)`
t.Run("invalid input", func(t *testing.T) {
testCases := []struct {
errMsg string
eventID string
userID string
}{
{
"invalid event id",
"invalid",
validUUID,
},
{
"invalid user id",
validUUID,
"invalid",
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
m := NewManager()
err := m.Add(context.Background(), nil, tc.eventID, tc.userID)
assert.True(t, errors.Is(err, ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
})
}
})
t.Run("database error", func(t *testing.T) {
tx := &tests.TXMock{}
tx.On("Exec", dbQuery, validUUID, validUUID).Return(tests.ErrFakeDatabaseFailure)
m := NewManager()
err := m.Add(context.Background(), tx, validUUID, validUUID)
assert.Equal(t, tests.ErrFakeDatabaseFailure, err)
tx.AssertExpectations(t)
})
t.Run("database query succeeded", func(t *testing.T) {
tx := &tests.TXMock{}
tx.On("Exec", dbQuery, validUUID, validUUID).Return(nil)
m := NewManager()
err := m.Add(context.Background(), tx, validUUID, validUUID)
assert.NoError(t, err)
tx.AssertExpectations(t)
})
}
func TestGetPending(t *testing.T) {
dbQuery := "select get_pending_notification()"
ctx := context.Background()
@ -26,26 +91,83 @@ func TestGetPending(t *testing.T) {
t.Run("database query succeeded", func(t *testing.T) {
expectedNotification := &hub.Notification{
NotificationID: "00000000-0000-0000-0000-000000000001",
PackageVersion: "1.0.0",
PackageID: "00000000-0000-0000-0000-000000000001",
NotificationKind: hub.NewRelease,
NotificationID: "notificationID",
Event: &hub.Event{
EventKind: hub.NewRelease,
PackageID: "packageID",
PackageVersion: "1.0.0",
},
User: &hub.User{
Email: "user1@email.com",
},
}
tx := &tests.TXMock{}
tx.On("QueryRow", dbQuery).Return([]byte(`
{
"notification_id": "00000000-0000-0000-0000-000000000001",
"package_version": "1.0.0",
"package_id": "00000000-0000-0000-0000-000000000001",
"notification_kind": 0
"notification_id": "notificationID",
"event": {
"event_kind": 0,
"package_id": "packageID",
"package_version": "1.0.0"
},
"user": {
"email": "user1@email.com"
}
}
`), nil)
m := NewManager()
n, err := m.GetPending(context.Background(), tx)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, expectedNotification, n)
tx.AssertExpectations(t)
})
}
func TestUpdateStatus(t *testing.T) {
dbQuery := "select update_notification_status($1::uuid, $2::boolean, $3::text)"
ctx := context.Background()
notificationID := "00000000-0000-0000-0000-000000000001"
t.Run("invalid input", func(t *testing.T) {
testCases := []struct {
errMsg string
notificationID string
}{
{
"invalid notification id",
"invalid",
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
m := NewManager()
err := m.UpdateStatus(ctx, nil, "invalidNotificationID", false, nil)
assert.True(t, errors.Is(err, ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
})
}
})
t.Run("database error", func(t *testing.T) {
tx := &tests.TXMock{}
tx.On("Exec", dbQuery, notificationID, true, "").Return(tests.ErrFakeDatabaseFailure)
m := NewManager()
err := m.UpdateStatus(ctx, tx, notificationID, true, nil)
assert.Equal(t, tests.ErrFakeDatabaseFailure, err)
tx.AssertExpectations(t)
})
t.Run("database query succeeded", func(t *testing.T) {
tx := &tests.TXMock{}
tx.On("Exec", dbQuery, notificationID, true, "").Return(nil)
m := NewManager()
err := m.UpdateStatus(ctx, tx, notificationID, true, nil)
assert.NoError(t, err)
tx.AssertExpectations(t)
})
}

View File

@ -3,6 +3,7 @@ package notification
import (
"context"
"github.com/artifacthub/hub/internal/email"
"github.com/artifacthub/hub/internal/hub"
"github.com/jackc/pgx/v4"
"github.com/stretchr/testify/mock"
@ -13,9 +14,40 @@ type ManagerMock struct {
mock.Mock
}
// Add implements the NotificationManager interface.
func (m *ManagerMock) Add(ctx context.Context, tx pgx.Tx, eventID, userID string) error {
args := m.Called(ctx, tx, eventID, userID)
return args.Error(0)
}
// GetPending implements the NotificationManager interface.
func (m *ManagerMock) GetPending(ctx context.Context, tx pgx.Tx) (*hub.Notification, error) {
args := m.Called(ctx, tx)
data, _ := args.Get(0).(*hub.Notification)
return data, args.Error(1)
}
// UpdateStatus implements the NotificationManager interface.
func (m *ManagerMock) UpdateStatus(
ctx context.Context,
tx pgx.Tx,
notificationID string,
processed bool,
processedErr error,
) error {
args := m.Called(ctx, tx, notificationID, processed, processedErr)
return args.Error(0)
}
// EmailDataCacheMock is a mock implementation of the NotificationEmailDataCache
// interface.
type EmailDataCacheMock struct {
mock.Mock
}
// Get implements the NotificationEmailDataCache interface.
func (m *EmailDataCacheMock) Get(ctx context.Context, e *hub.Event) (email.Data, error) {
args := m.Called(ctx, e)
data, _ := args.Get(0).(email.Data)
return data, args.Error(1)
}

View File

@ -1,14 +1,11 @@
package notification
import (
"bytes"
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/artifacthub/hub/internal/email"
"github.com/artifacthub/hub/internal/hub"
"github.com/artifacthub/hub/internal/util"
"github.com/jackc/pgx/v4"
@ -16,35 +13,34 @@ import (
)
const (
pauseOnEmptyQueue = 1 * time.Minute
pauseOnError = 1 * time.Second
pauseOnEmptyQueue = 30 * time.Second
pauseOnError = 10 * time.Second
)
// Worker is in charge of delivering pending notifications to their intended
// recipients.
// Worker is in charge of delivering notifications to their intended recipients.
type Worker struct {
svc *Services
baseURL string
svc *Services
emailDataCache hub.NotificationEmailDataCache
}
// NewWorker creates a new Worker instance.
func NewWorker(
svc *Services,
baseURL string,
emailDataCache hub.NotificationEmailDataCache,
) *Worker {
return &Worker{
svc: svc,
baseURL: baseURL,
svc: svc,
emailDataCache: emailDataCache,
}
}
// Run is the main loop of the worker. It calls deliverNotification periodically
// Run is the main loop of the worker. It calls processNotification periodically
// until it's asked to stop via the context provided.
func (w *Worker) Run(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
for {
err := w.deliverNotification(ctx)
err := w.processNotification(ctx)
switch err {
case nil:
select {
@ -68,10 +64,11 @@ func (w *Worker) Run(ctx context.Context, wg *sync.WaitGroup) {
}
}
// deliverNotification gets a pending notification from the database and
// processNotification gets a pending notification from the database and
// delivers it.
func (w *Worker) deliverNotification(ctx context.Context) error {
func (w *Worker) processNotification(ctx context.Context) error {
return util.DBTransact(ctx, w.svc.DB, func(tx pgx.Tx) error {
// Get pending notification to process
n, err := w.svc.NotificationManager.GetPending(ctx, tx)
if err != nil {
if !errors.Is(err, pgx.ErrNoRows) {
@ -79,79 +76,21 @@ func (w *Worker) deliverNotification(ctx context.Context) error {
}
return err
}
rcpts, err := w.svc.SubscriptionManager.GetSubscriptors(ctx, n.PackageID, n.NotificationKind)
// Process notification
emailData, err := w.emailDataCache.Get(ctx, n.Event)
if err != nil {
log.Error().Err(err).Msg("error getting notification subscriptors")
log.Error().Err(err).Msg("error getting email data")
return err
}
if len(rcpts) == 0 {
return nil
}
emailData, err := w.prepareEmailData(ctx, n)
emailData.To = n.User.Email
processedErr := w.svc.ES.SendEmail(&emailData)
// Update notification status
err = w.svc.NotificationManager.UpdateStatus(ctx, tx, n.NotificationID, true, processedErr)
if err != nil {
log.Error().Err(err).Msg("error preparing email data")
return err
}
for _, u := range rcpts {
emailData.To = u.Email
if err := w.svc.ES.SendEmail(emailData); err != nil {
log.Error().
Err(err).
Str("notificationID", n.NotificationID).
Str("email", u.Email).
Msg("error sending notification email")
}
log.Error().Err(err).Msg("error updating notification status")
}
return nil
})
}
// prepareEmailData prepares the content of the notification email.
func (w *Worker) prepareEmailData(ctx context.Context, n *hub.Notification) (*email.Data, error) {
var subject string
var emailBody bytes.Buffer
switch n.NotificationKind {
case hub.NewRelease:
p, err := w.svc.PackageManager.Get(ctx, &hub.GetPackageInput{PackageID: n.PackageID})
if err != nil {
return nil, err
}
subject = fmt.Sprintf("%s version %s released", p.Name, n.PackageVersion)
publisher := p.OrganizationName
if publisher == "" {
publisher = p.UserAlias
}
if p.ChartRepository != nil {
publisher += "/" + p.ChartRepository.Name
}
var packagePath string
switch p.Kind {
case hub.Chart:
packagePath = fmt.Sprintf("/package/chart/%s/%s", p.ChartRepository.Name, p.NormalizedName)
case hub.Falco:
packagePath = fmt.Sprintf("/package/falco/%s", p.NormalizedName)
case hub.OPA:
packagePath = fmt.Sprintf("/package/opa/%s", p.NormalizedName)
}
data := map[string]interface{}{
"publisher": publisher,
"kind": p.Kind,
"name": p.Name,
"version": n.PackageVersion,
"baseURL": w.baseURL,
"logoImageID": p.LogoImageID,
"packagePath": packagePath,
}
if err := newReleaseEmailTmpl.Execute(&emailBody, data); err != nil {
return nil, err
}
}
return &email.Data{
Subject: subject,
Body: emailBody.Bytes(),
}, nil
}
// - Publisher (/ Chart repo)

View File

@ -2,8 +2,6 @@ package notification
import (
"context"
"errors"
"os"
"sync"
"testing"
"time"
@ -13,76 +11,35 @@ import (
"github.com/artifacthub/hub/internal/pkg"
"github.com/artifacthub/hub/internal/subscription"
"github.com/artifacthub/hub/internal/tests"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
var errFake = errors.New("fake error for tests")
func TestMain(m *testing.M) {
zerolog.SetGlobalLevel(zerolog.Disabled)
os.Exit(m.Run())
}
func TestWorker(t *testing.T) {
t.Run("error getting pending notification", func(t *testing.T) {
sw := newServicesWrapper()
sw.db.On("Begin", mock.Anything).Return(sw.tx, nil)
sw.tx.On("Rollback", mock.Anything).Return(nil)
sw.nm.On("GetPending", mock.Anything, mock.Anything).Return(nil, errFake)
w := NewWorker(sw.svc, "baseURL")
go w.Run(sw.ctx, sw.wg)
sw.assertExpectations(t)
})
t.Run("error getting notification subscriptors", func(t *testing.T) {
sw := newServicesWrapper()
sw.db.On("Begin", mock.Anything).Return(sw.tx, nil)
sw.tx.On("Rollback", mock.Anything).Return(nil)
sw.nm.On("GetPending", mock.Anything, mock.Anything).Return(&hub.Notification{
PackageID: "packageID",
NotificationKind: hub.NewRelease,
}, nil)
sw.sm.On("GetSubscriptors", mock.Anything, "packageID", hub.NewRelease).Return(nil, errFake)
w := NewWorker(sw.svc, "baseURL")
w := NewWorker(sw.svc, sw.cm)
go w.Run(sw.ctx, sw.wg)
sw.assertExpectations(t)
})
t.Run("no subscriptors found", func(t *testing.T) {
t.Run("error getting email data", func(t *testing.T) {
sw := newServicesWrapper()
sw.db.On("Begin", mock.Anything).Return(sw.tx, nil)
sw.tx.On("Commit", mock.Anything).Return(nil)
sw.nm.On("GetPending", mock.Anything, mock.Anything).Return(&hub.Notification{
PackageID: "packageID",
NotificationKind: hub.NewRelease,
}, nil)
sw.sm.On("GetSubscriptors", mock.Anything, "packageID", hub.NewRelease).Return([]*hub.User{}, nil)
w := NewWorker(sw.svc, "baseURL")
go w.Run(sw.ctx, sw.wg)
sw.assertExpectations(t)
})
t.Run("error preparing email data", func(t *testing.T) {
sw := newServicesWrapper()
sw.db.On("Begin", mock.Anything).Return(sw.tx, nil)
sw.tx.On("Rollback", mock.Anything).Return(nil)
sw.nm.On("GetPending", mock.Anything, mock.Anything).Return(&hub.Notification{
PackageID: "packageID",
NotificationKind: hub.NewRelease,
}, nil)
sw.sm.On("GetSubscriptors", mock.Anything, "packageID", hub.NewRelease).Return([]*hub.User{
{
Email: "user1@email.com",
Event: &hub.Event{
EventKind: hub.NewRelease,
PackageID: "packageID",
},
}, nil)
sw.pm.On("Get", mock.Anything, mock.Anything).Return(nil, errFake)
sw.cm.On("Get", mock.Anything, mock.Anything).Return(nil, errFake)
sw.tx.On("Rollback", mock.Anything).Return(nil)
w := NewWorker(sw.svc, "baseURL")
w := NewWorker(sw.svc, sw.cm)
go w.Run(sw.ctx, sw.wg)
sw.assertExpectations(t)
})
@ -90,20 +47,22 @@ func TestWorker(t *testing.T) {
t.Run("error sending email", func(t *testing.T) {
sw := newServicesWrapper()
sw.db.On("Begin", mock.Anything).Return(sw.tx, nil)
sw.tx.On("Rollback", mock.Anything).Return(nil)
sw.nm.On("GetPending", mock.Anything, mock.Anything).Return(&hub.Notification{
PackageID: "packageID",
NotificationKind: hub.NewRelease,
}, nil)
sw.sm.On("GetSubscriptors", mock.Anything, "packageID", hub.NewRelease).Return([]*hub.User{
{
NotificationID: "notificationID",
Event: &hub.Event{
EventKind: hub.NewRelease,
PackageID: "packageID",
},
User: &hub.User{
Email: "user1@email.com",
},
}, nil)
sw.pm.On("Get", mock.Anything, mock.Anything).Return(nil, errFake)
sw.cm.On("Get", mock.Anything, mock.Anything).Return(&email.Data{}, nil)
sw.es.On("SendEmail", mock.Anything).Return(errFake)
sw.nm.On("UpdateStatus", mock.Anything, mock.Anything, "notificationID", true, errFake).Return(nil)
sw.tx.On("Commit", mock.Anything).Return(nil)
w := NewWorker(sw.svc, "baseURL")
w := NewWorker(sw.svc, sw.cm)
go w.Run(sw.ctx, sw.wg)
sw.assertExpectations(t)
})
@ -111,20 +70,22 @@ func TestWorker(t *testing.T) {
t.Run("notification delivered successfully", func(t *testing.T) {
sw := newServicesWrapper()
sw.db.On("Begin", mock.Anything).Return(sw.tx, nil)
sw.tx.On("Rollback", mock.Anything).Return(nil)
sw.nm.On("GetPending", mock.Anything, mock.Anything).Return(&hub.Notification{
PackageID: "packageID",
NotificationKind: hub.NewRelease,
}, nil)
sw.sm.On("GetSubscriptors", mock.Anything, "packageID", hub.NewRelease).Return([]*hub.User{
{
NotificationID: "notificationID",
Event: &hub.Event{
EventKind: hub.NewRelease,
PackageID: "packageID",
},
User: &hub.User{
Email: "user1@email.com",
},
}, nil)
sw.pm.On("Get", mock.Anything, mock.Anything).Return(nil, errFake)
sw.cm.On("Get", mock.Anything, mock.Anything).Return(&email.Data{}, nil)
sw.es.On("SendEmail", mock.Anything).Return(nil)
sw.nm.On("UpdateStatus", mock.Anything, mock.Anything, "notificationID", true, nil).Return(nil)
sw.tx.On("Commit", mock.Anything).Return(nil)
w := NewWorker(sw.svc, "baseURL")
w := NewWorker(sw.svc, sw.cm)
go w.Run(sw.ctx, sw.wg)
sw.assertExpectations(t)
})
@ -140,6 +101,7 @@ type servicesWrapper struct {
nm *ManagerMock
sm *subscription.ManagerMock
pm *pkg.ManagerMock
cm *EmailDataCacheMock
svc *Services
}
@ -155,6 +117,7 @@ func newServicesWrapper() *servicesWrapper {
nm := &ManagerMock{}
sm := &subscription.ManagerMock{}
pm := &pkg.ManagerMock{}
cm := &EmailDataCacheMock{}
return &servicesWrapper{
ctx: ctx,
@ -166,6 +129,7 @@ func newServicesWrapper() *servicesWrapper {
nm: nm,
sm: sm,
pm: pm,
cm: cm,
svc: &Services{
DB: db,
ES: es,
@ -183,6 +147,9 @@ func (sw *servicesWrapper) assertExpectations(t *testing.T) {
return true
}, 2*time.Second, 100*time.Millisecond)
sw.db.AssertExpectations(t)
sw.tx.AssertExpectations(t)
sw.es.AssertExpectations(t)
sw.nm.AssertExpectations(t)
sw.sm.AssertExpectations(t)
sw.pm.AssertExpectations(t)

View File

@ -80,23 +80,23 @@ func (m *Manager) GetByUserJSON(ctx context.Context) ([]byte, error) {
return dataJSON, nil
}
// GetSubscriptors returns the users subscribed to a package to receive certain
// kind of notifications.
// GetSubscriptors returns the users subscribed to a package to receive
// notifications for certain kind of events.
func (m *Manager) GetSubscriptors(
ctx context.Context,
packageID string,
notificationKind hub.NotificationKind,
eventKind hub.EventKind,
) ([]*hub.User, error) {
if _, err := uuid.FromString(packageID); err != nil {
return nil, fmt.Errorf("%w: %s", ErrInvalidInput, "invalid package id")
}
if notificationKind != hub.NewRelease {
return nil, fmt.Errorf("%w: %s", ErrInvalidInput, "invalid notification kind")
if eventKind != hub.NewRelease {
return nil, fmt.Errorf("%w: %s", ErrInvalidInput, "invalid event kind")
}
query := "select get_subscriptors($1::uuid, $2::integer)"
var dataJSON []byte
err := m.db.QueryRow(ctx, query, packageID, notificationKind).Scan(&dataJSON)
err := m.db.QueryRow(ctx, query, packageID, eventKind).Scan(&dataJSON)
if err != nil {
return nil, err
}
@ -113,8 +113,8 @@ func validateSubscription(s *hub.Subscription) error {
if _, err := uuid.FromString(s.PackageID); err != nil {
return fmt.Errorf("%w: %s", ErrInvalidInput, "invalid package id")
}
if s.NotificationKind != hub.NewRelease {
return fmt.Errorf("%w: %s", ErrInvalidInput, "invalid notification kind")
if s.EventKind != hub.NewRelease {
return fmt.Errorf("%w: %s", ErrInvalidInput, "invalid event kind")
}
return nil
}

View File

@ -39,10 +39,10 @@ func TestAdd(t *testing.T) {
},
},
{
"invalid notification kind",
"invalid event kind",
&hub.Subscription{
PackageID: packageID,
NotificationKind: hub.NotificationKind(5),
PackageID: packageID,
EventKind: hub.EventKind(5),
},
},
}
@ -58,8 +58,8 @@ func TestAdd(t *testing.T) {
})
s := &hub.Subscription{
PackageID: packageID,
NotificationKind: hub.NewRelease,
PackageID: packageID,
EventKind: hub.NewRelease,
}
t.Run("database error", func(t *testing.T) {
@ -106,10 +106,10 @@ func TestDelete(t *testing.T) {
},
},
{
"invalid notification kind",
"invalid event kind",
&hub.Subscription{
PackageID: packageID,
NotificationKind: hub.NotificationKind(5),
PackageID: packageID,
EventKind: hub.EventKind(5),
},
},
}
@ -125,8 +125,8 @@ func TestDelete(t *testing.T) {
})
s := &hub.Subscription{
PackageID: packageID,
NotificationKind: hub.NewRelease,
PackageID: packageID,
EventKind: hub.NewRelease,
}
t.Run("database error", func(t *testing.T) {
@ -230,9 +230,9 @@ func TestGetSubscriptors(t *testing.T) {
t.Run("invalid input", func(t *testing.T) {
testCases := []struct {
errMsg string
packageID string
notificationKind hub.NotificationKind
errMsg string
packageID string
eventKind hub.EventKind
}{
{
"invalid package id",
@ -240,16 +240,16 @@ func TestGetSubscriptors(t *testing.T) {
0,
},
{
"invalid notification kind",
"invalid event kind",
packageID,
hub.NotificationKind(5),
hub.EventKind(5),
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
m := NewManager(nil)
dataJSON, err := m.GetSubscriptors(context.Background(), tc.packageID, tc.notificationKind)
dataJSON, err := m.GetSubscriptors(context.Background(), tc.packageID, tc.eventKind)
assert.True(t, errors.Is(err, ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
assert.Nil(t, dataJSON)
@ -259,7 +259,7 @@ func TestGetSubscriptors(t *testing.T) {
t.Run("database error", func(t *testing.T) {
db := &tests.DBMock{}
db.On("QueryRow", dbQuery, packageID, hub.NotificationKind(0)).Return(nil, tests.ErrFakeDatabaseFailure)
db.On("QueryRow", dbQuery, packageID, hub.EventKind(0)).Return(nil, tests.ErrFakeDatabaseFailure)
m := NewManager(db)
subscriptors, err := m.GetSubscriptors(context.Background(), packageID, hub.NewRelease)
@ -271,21 +271,21 @@ func TestGetSubscriptors(t *testing.T) {
t.Run("database query succeeded", func(t *testing.T) {
expectedSubscriptors := []*hub.User{
{
Email: "user1@email.com",
UserID: "00000000-0000-0000-0000-000000000001",
},
{
Email: "user2@email.com",
UserID: "00000000-0000-0000-0000-000000000002",
},
}
db := &tests.DBMock{}
db.On("QueryRow", dbQuery, packageID, hub.NotificationKind(0)).Return([]byte(`
db.On("QueryRow", dbQuery, packageID, hub.EventKind(0)).Return([]byte(`
[
{
"email": "user1@email.com"
"user_id": "00000000-0000-0000-0000-000000000001"
},
{
"email": "user2@email.com"
"user_id": "00000000-0000-0000-0000-000000000002"
}
]
`), nil)

View File

@ -42,9 +42,9 @@ func (m *ManagerMock) GetByUserJSON(ctx context.Context) ([]byte, error) {
func (m *ManagerMock) GetSubscriptors(
ctx context.Context,
packageID string,
notificationKind hub.NotificationKind,
eventKind hub.EventKind,
) ([]*hub.User, error) {
args := m.Called(ctx, packageID, notificationKind)
args := m.Called(ctx, packageID, eventKind)
data, _ := args.Get(0).([]*hub.User)
return data, args.Error(1)
}

View File

@ -81,8 +81,8 @@ func (m *TXMock) CopyFrom(
// Exec implements the pgx.Tx interface.
func (m *TXMock) Exec(ctx context.Context, query string, params ...interface{}) (pgconn.CommandTag, error) {
// NOTE: not used
return nil, nil
args := m.Called(append([]interface{}{query}, params...)...)
return nil, args.Error(0)
}
// LargeObjects implements the pgx.Tx interface.

View File

@ -6,8 +6,8 @@ import isUndefined from 'lodash/isUndefined';
import {
ChartRepository,
CheckAvailabilityProps,
EventKind,
LogoImage,
NotificationKind,
Organization,
Package,
PackageStars,
@ -341,7 +341,7 @@ export const API = {
return apiFetch(`${API_BASE_URL}/subscriptions/${packageId}`);
},
addSubscription: (packageId: string, notificationKind: NotificationKind): Promise<string | null> => {
addSubscription: (packageId: string, eventKind: EventKind): Promise<string | null> => {
return apiFetch(`${API_BASE_URL}/subscriptions`, {
method: 'POST',
headers: {
@ -349,12 +349,12 @@ export const API = {
},
body: JSON.stringify({
package_id: packageId,
notification_kind: notificationKind,
event_kind: eventKind,
}),
});
},
deleteSubscription: (packageId: string, notificationKind: NotificationKind): Promise<string | null> => {
deleteSubscription: (packageId: string, eventKind: EventKind): Promise<string | null> => {
return apiFetch(`${API_BASE_URL}/subscriptions`, {
method: 'DELETE',
headers: {
@ -362,7 +362,7 @@ export const API = {
},
body: JSON.stringify({
package_id: packageId,
notification_kind: notificationKind,
event_kind: eventKind,
}),
});
},

View File

@ -4,7 +4,7 @@ import React from 'react';
import { FaCheck } from 'react-icons/fa';
import { Link, useHistory } from 'react-router-dom';
import { NotificationKind, Package, PackageKind } from '../../../../../types';
import { EventKind, Package, PackageKind } from '../../../../../types';
import buildPackageURL from '../../../../../utils/buildPackageURL';
import { SubscriptionItem, SUBSCRIPTIONS_LIST } from '../../../../../utils/data';
import prepareQueryString from '../../../../../utils/prepareQueryString';
@ -15,7 +15,7 @@ import styles from './PackageCard.module.css';
interface Props {
package: Package;
changeSubscription: (packageId: string, kind: NotificationKind, isActive: boolean, packageName: string) => void;
changeSubscription: (packageId: string, kind: EventKind, isActive: boolean, packageName: string) => void;
}
const PackageCard = (props: Props) => {
@ -130,8 +130,7 @@ const PackageCard = (props: Props) => {
</div>
{SUBSCRIPTIONS_LIST.map((subs: SubscriptionItem) => {
const isActive =
!isUndefined(props.package.notificationKinds) && props.package.notificationKinds.includes(subs.kind);
const isActive = !isUndefined(props.package.eventKinds) && props.package.eventKinds.includes(subs.kind);
return (
<button

View File

@ -11,5 +11,5 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
}

View File

@ -11,5 +11,5 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
}

View File

@ -11,5 +11,5 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
}

View File

@ -11,5 +11,5 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
}

View File

@ -12,5 +12,5 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
}

View File

@ -12,5 +12,5 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
}

View File

@ -11,5 +11,5 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
}

View File

@ -11,5 +11,5 @@
"name": "ibm-charts",
"displayName": "IBM charts"
},
"notificationKinds": [0]
"eventKinds": [0]
}

View File

@ -8,5 +8,5 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
}

View File

@ -11,5 +11,5 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
}

View File

@ -12,7 +12,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "11d84cdc-e414-43c7-9be0-1f6b11912eb2",
@ -27,7 +27,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "eedf0ee0-841e-4434-bf0b-4c2a175cb8c8",
@ -42,7 +42,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "cfa55ea2-eb69-4703-8be7-73937c6efc4b",
@ -57,7 +57,7 @@
"name": "ibm-charts",
"displayName": "IBM charts"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "ccd1553a-e9bd-45b5-93a3-9ee127dddcb0",
@ -69,7 +69,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "e63b8968-a688-4525-889d-cbbf3df82f9f",
@ -81,7 +81,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "2052b061-2459-4d9f-8cd3-a5e8dbb4b804",
@ -93,7 +93,7 @@
"organizationName": "opa",
"organizationDisplayName": "OPA",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "d3945a69-2aae-4ff9-8fb3-ce3b720d0384",
@ -108,6 +108,6 @@
"name": "incubator",
"displayName": "Incubator"
},
"notificationKinds": [0]
"eventKinds": [0]
}
]

View File

@ -12,7 +12,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "11d84cdc-e414-43c7-9be0-1f6b11912eb2",
@ -27,7 +27,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "eedf0ee0-841e-4434-bf0b-4c2a175cb8c8",
@ -42,7 +42,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "cfa55ea2-eb69-4703-8be7-73937c6efc4b",
@ -57,7 +57,7 @@
"name": "ibm-charts",
"displayName": "IBM charts"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "ccd1553a-e9bd-45b5-93a3-9ee127dddcb0",
@ -69,7 +69,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "e63b8968-a688-4525-889d-cbbf3df82f9f",
@ -81,7 +81,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "2052b061-2459-4d9f-8cd3-a5e8dbb4b804",
@ -93,7 +93,7 @@
"organizationName": "opa",
"organizationDisplayName": "OPA",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "d3945a69-2aae-4ff9-8fb3-ce3b720d0384",
@ -108,6 +108,6 @@
"name": "incubator",
"displayName": "Incubator"
},
"notificationKinds": [0]
"eventKinds": [0]
}
]

View File

@ -12,7 +12,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "11d84cdc-e414-43c7-9be0-1f6b11912eb2",
@ -27,7 +27,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "eedf0ee0-841e-4434-bf0b-4c2a175cb8c8",
@ -42,7 +42,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "cfa55ea2-eb69-4703-8be7-73937c6efc4b",
@ -57,7 +57,7 @@
"name": "ibm-charts",
"displayName": "IBM charts"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "ccd1553a-e9bd-45b5-93a3-9ee127dddcb0",
@ -69,7 +69,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "e63b8968-a688-4525-889d-cbbf3df82f9f",
@ -81,7 +81,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "2052b061-2459-4d9f-8cd3-a5e8dbb4b804",
@ -93,7 +93,7 @@
"organizationName": "opa",
"organizationDisplayName": "OPA",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "d3945a69-2aae-4ff9-8fb3-ce3b720d0384",
@ -108,6 +108,6 @@
"name": "incubator",
"displayName": "Incubator"
},
"notificationKinds": [0]
"eventKinds": [0]
}
]

View File

@ -12,7 +12,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "11d84cdc-e414-43c7-9be0-1f6b11912eb2",
@ -27,7 +27,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "eedf0ee0-841e-4434-bf0b-4c2a175cb8c8",
@ -42,7 +42,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "cfa55ea2-eb69-4703-8be7-73937c6efc4b",
@ -57,7 +57,7 @@
"name": "ibm-charts",
"displayName": "IBM charts"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "ccd1553a-e9bd-45b5-93a3-9ee127dddcb0",
@ -69,7 +69,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "e63b8968-a688-4525-889d-cbbf3df82f9f",
@ -81,7 +81,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "2052b061-2459-4d9f-8cd3-a5e8dbb4b804",
@ -93,7 +93,7 @@
"organizationName": "opa",
"organizationDisplayName": "OPA",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "d3945a69-2aae-4ff9-8fb3-ce3b720d0384",
@ -108,6 +108,6 @@
"name": "incubator",
"displayName": "Incubator"
},
"notificationKinds": [0]
"eventKinds": [0]
}
]

View File

@ -12,7 +12,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "11d84cdc-e414-43c7-9be0-1f6b11912eb2",
@ -27,7 +27,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "eedf0ee0-841e-4434-bf0b-4c2a175cb8c8",
@ -42,7 +42,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "cfa55ea2-eb69-4703-8be7-73937c6efc4b",
@ -57,7 +57,7 @@
"name": "ibm-charts",
"displayName": "IBM charts"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "ccd1553a-e9bd-45b5-93a3-9ee127dddcb0",
@ -69,7 +69,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "e63b8968-a688-4525-889d-cbbf3df82f9f",
@ -81,7 +81,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "2052b061-2459-4d9f-8cd3-a5e8dbb4b804",
@ -93,7 +93,7 @@
"organizationName": "opa",
"organizationDisplayName": "OPA",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "d3945a69-2aae-4ff9-8fb3-ce3b720d0384",
@ -108,6 +108,6 @@
"name": "incubator",
"displayName": "Incubator"
},
"notificationKinds": [0]
"eventKinds": [0]
}
]

View File

@ -12,7 +12,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "11d84cdc-e414-43c7-9be0-1f6b11912eb2",
@ -27,7 +27,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "eedf0ee0-841e-4434-bf0b-4c2a175cb8c8",
@ -42,7 +42,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "cfa55ea2-eb69-4703-8be7-73937c6efc4b",
@ -57,7 +57,7 @@
"name": "ibm-charts",
"displayName": "IBM charts"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "ccd1553a-e9bd-45b5-93a3-9ee127dddcb0",
@ -69,7 +69,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "e63b8968-a688-4525-889d-cbbf3df82f9f",
@ -81,7 +81,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "2052b061-2459-4d9f-8cd3-a5e8dbb4b804",
@ -93,7 +93,7 @@
"organizationName": "opa",
"organizationDisplayName": "OPA",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "d3945a69-2aae-4ff9-8fb3-ce3b720d0384",
@ -108,6 +108,6 @@
"name": "incubator",
"displayName": "Incubator"
},
"notificationKinds": [0]
"eventKinds": [0]
}
]

View File

@ -12,7 +12,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "11d84cdc-e414-43c7-9be0-1f6b11912eb2",
@ -27,7 +27,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "eedf0ee0-841e-4434-bf0b-4c2a175cb8c8",
@ -42,7 +42,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "cfa55ea2-eb69-4703-8be7-73937c6efc4b",
@ -57,7 +57,7 @@
"name": "ibm-charts",
"displayName": "IBM charts"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "ccd1553a-e9bd-45b5-93a3-9ee127dddcb0",
@ -69,7 +69,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "e63b8968-a688-4525-889d-cbbf3df82f9f",
@ -81,7 +81,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "2052b061-2459-4d9f-8cd3-a5e8dbb4b804",
@ -93,7 +93,7 @@
"organizationName": "opa",
"organizationDisplayName": "OPA",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "d3945a69-2aae-4ff9-8fb3-ce3b720d0384",
@ -108,6 +108,6 @@
"name": "incubator",
"displayName": "Incubator"
},
"notificationKinds": [0]
"eventKinds": [0]
}
]

View File

@ -12,7 +12,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "11d84cdc-e414-43c7-9be0-1f6b11912eb2",
@ -27,7 +27,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "eedf0ee0-841e-4434-bf0b-4c2a175cb8c8",
@ -42,7 +42,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "cfa55ea2-eb69-4703-8be7-73937c6efc4b",
@ -57,7 +57,7 @@
"name": "ibm-charts",
"displayName": "IBM charts"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "ccd1553a-e9bd-45b5-93a3-9ee127dddcb0",
@ -69,7 +69,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "e63b8968-a688-4525-889d-cbbf3df82f9f",
@ -81,7 +81,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "2052b061-2459-4d9f-8cd3-a5e8dbb4b804",
@ -93,7 +93,7 @@
"organizationName": "opa",
"organizationDisplayName": "OPA",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "d3945a69-2aae-4ff9-8fb3-ce3b720d0384",
@ -108,6 +108,6 @@
"name": "incubator",
"displayName": "Incubator"
},
"notificationKinds": [0]
"eventKinds": [0]
}
]

View File

@ -12,7 +12,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "11d84cdc-e414-43c7-9be0-1f6b11912eb2",
@ -27,7 +27,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "eedf0ee0-841e-4434-bf0b-4c2a175cb8c8",
@ -42,7 +42,7 @@
"name": "stable",
"displayName": "Stable"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "cfa55ea2-eb69-4703-8be7-73937c6efc4b",
@ -57,7 +57,7 @@
"name": "ibm-charts",
"displayName": "IBM charts"
},
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "ccd1553a-e9bd-45b5-93a3-9ee127dddcb0",
@ -69,7 +69,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "e63b8968-a688-4525-889d-cbbf3df82f9f",
@ -81,7 +81,7 @@
"organizationName": "falco",
"organizationDisplayName": "Falco",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "2052b061-2459-4d9f-8cd3-a5e8dbb4b804",
@ -93,7 +93,7 @@
"organizationName": "opa",
"organizationDisplayName": "OPA",
"chartRepository": null,
"notificationKinds": [0]
"eventKinds": [0]
},
{
"packageId": "d3945a69-2aae-4ff9-8fb3-ce3b720d0384",
@ -108,6 +108,6 @@
"name": "incubator",
"displayName": "Incubator"
},
"notificationKinds": [0]
"eventKinds": [0]
}
]

View File

@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { API } from '../../../../../api';
import { NotificationKind, Package } from '../../../../../types';
import { EventKind, Package } from '../../../../../types';
import alertDispatcher from '../../../../../utils/alertDispatcher';
import buildPackageURL from '../../../../../utils/buildPackageURL';
import { SubscriptionItem, SUBSCRIPTIONS_LIST } from '../../../../../utils/data';
@ -25,7 +25,7 @@ const SubscriptionsSection = (props: Props) => {
const [packages, setPackages] = useState<Package[] | undefined>(undefined);
const [apiError, setApiError] = useState<string | JSX.Element | null>(null);
const getNotificationTitle = (kind: NotificationKind): string => {
const getNotificationTitle = (kind: EventKind): string => {
let title = '';
const notif = SUBSCRIPTIONS_LIST.find((subs: SubscriptionItem) => subs.kind === kind);
if (!isUndefined(notif)) {
@ -34,18 +34,16 @@ const SubscriptionsSection = (props: Props) => {
return title;
};
const updateOptimisticallyPackages = (kind: NotificationKind, isActive: boolean, packageId: string) => {
const updateOptimisticallyPackages = (kind: EventKind, isActive: boolean, packageId: string) => {
const packageToUpdate = !isUndefined(packages)
? packages.find((item: Package) => item.packageId === packageId)
: undefined;
if (!isUndefined(packageToUpdate) && !isUndefined(packageToUpdate.notificationKinds)) {
if (!isUndefined(packageToUpdate) && !isUndefined(packageToUpdate.eventKinds)) {
const newPackages = packages!.filter((item: Package) => item.packageId !== packageId);
if (isActive) {
packageToUpdate.notificationKinds = packageToUpdate.notificationKinds.filter(
(notifKind: number) => notifKind !== kind
);
packageToUpdate.eventKinds = packageToUpdate.eventKinds.filter((notifKind: number) => notifKind !== kind);
} else {
packageToUpdate.notificationKinds.push(kind);
packageToUpdate.eventKinds.push(kind);
}
setPackages(newPackages);
}
@ -68,7 +66,7 @@ const SubscriptionsSection = (props: Props) => {
}
}
async function changeSubscription(packageId: string, kind: NotificationKind, isActive: boolean, packageName: string) {
async function changeSubscription(packageId: string, kind: EventKind, isActive: boolean, packageName: string) {
updateOptimisticallyPackages(kind, isActive, packageId);
try {
if (isActive) {
@ -238,8 +236,7 @@ const SubscriptionsSection = (props: Props) => {
)}
</td>
{SUBSCRIPTIONS_LIST.map((subs: SubscriptionItem) => {
const isActive =
!isUndefined(item.notificationKinds) && item.notificationKinds.includes(subs.kind);
const isActive = !isUndefined(item.eventKinds) && item.eventKinds.includes(subs.kind);
const id = `subs_${item.packageId}_${subs.kind}`;
return (

View File

@ -43,7 +43,7 @@ describe('SubscriptionsButton', () => {
});
it('creates snapshot', async () => {
mocked(API).getPackageSubscriptions.mockResolvedValue([{ notificationKind: 0 }]);
mocked(API).getPackageSubscriptions.mockResolvedValue([{ eventKind: 0 }]);
const result = render(
<AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}>
@ -59,7 +59,7 @@ describe('SubscriptionsButton', () => {
describe('Render', () => {
describe('when user is signed in', () => {
it('renders component with active New releases notification', async () => {
mocked(API).getPackageSubscriptions.mockResolvedValue([{ notificationKind: 0 }]);
mocked(API).getPackageSubscriptions.mockResolvedValue([{ eventKind: 0 }]);
mocked(API).deleteSubscription.mockResolvedValue('');
const { getByText, getByTestId, queryByRole } = render(
@ -227,7 +227,7 @@ describe('SubscriptionsButton', () => {
});
it('to inactivate New release notification', async () => {
mocked(API).getPackageSubscriptions.mockResolvedValue([{ notificationKind: 0 }]);
mocked(API).getPackageSubscriptions.mockResolvedValue([{ eventKind: 0 }]);
mocked(API).deleteSubscription.mockRejectedValue({ statusText: 'error' });
const { getByText, getByTestId } = render(

View File

@ -8,7 +8,7 @@ import { MdNotificationsActive, MdNotificationsOff } from 'react-icons/md';
import { API } from '../../api';
import { AppCtx } from '../../context/AppCtx';
import useOutsideClick from '../../hooks/useOutsideClick';
import { NotificationKind, Subscription } from '../../types';
import { EventKind, Subscription } from '../../types';
import alertDispatcher from '../../utils/alertDispatcher';
import { SubscriptionItem, SUBSCRIPTIONS_LIST } from '../../utils/data';
import styles from './SubscriptionsButton.module.css';
@ -26,7 +26,7 @@ const SubscriptionsButton = (props: Props) => {
const ref = useRef(null);
useOutsideClick([ref], openStatus, () => setOpenStatus(false));
const getNotificationTitle = (kind: NotificationKind): string => {
const getNotificationTitle = (kind: EventKind): string => {
let title = '';
const notif = SUBSCRIPTIONS_LIST.find((subs: SubscriptionItem) => subs.kind === kind);
if (!isUndefined(notif)) {
@ -35,21 +35,21 @@ const SubscriptionsButton = (props: Props) => {
return title;
};
const updateOptimisticallyActiveSubscriptions = (kind: NotificationKind, isActive: boolean) => {
const updateOptimisticallyActiveSubscriptions = (kind: EventKind, isActive: boolean) => {
if (isActive) {
setActiveSubscriptions(activeSubscriptions!.filter((subs) => subs.notificationKind !== kind));
setActiveSubscriptions(activeSubscriptions!.filter((subs) => subs.eventKind !== kind));
} else {
const newSubs = activeSubscriptions ? [...activeSubscriptions] : [];
newSubs.push({ notificationKind: kind });
newSubs.push({ eventKind: kind });
setActiveSubscriptions(newSubs);
}
};
const isActiveNotification = (kind: NotificationKind): boolean => {
const isActiveNotification = (kind: EventKind): boolean => {
let isActive = false;
if (activeSubscriptions) {
for (const activeSubs of activeSubscriptions) {
if (activeSubs.notificationKind === kind) {
if (activeSubs.eventKind === kind) {
isActive = true;
break;
}
@ -96,7 +96,7 @@ const SubscriptionsButton = (props: Props) => {
}
}, [ctx.user]); /* eslint-disable-line react-hooks/exhaustive-deps */
async function changeSubscription(kind: NotificationKind, isActive: boolean) {
async function changeSubscription(kind: EventKind, isActive: boolean) {
updateOptimisticallyActiveSubscriptions(kind, isActive);
try {
if (isActive) {

View File

@ -45,7 +45,7 @@ export interface Package {
links?: PackageLink[];
stars?: number | null;
userAlias: string | null;
notificationKinds?: NotificationKind[];
eventKinds?: EventKind[];
}
export interface PackageData {
@ -199,11 +199,11 @@ export interface PackageStars {
stars: number | null;
}
export enum NotificationKind {
export enum EventKind {
NewPackageRelease = 0,
SecurityAlert,
}
export interface Subscription {
notificationKind: NotificationKind;
eventKind: EventKind;
}

View File

@ -1,10 +1,10 @@
import React from 'react';
import { MdNewReleases } from 'react-icons/md';
import { NotificationKind, PackageKind } from '../types';
import { EventKind, PackageKind } from '../types';
export interface SubscriptionItem {
kind: NotificationKind;
kind: EventKind;
icon: JSX.Element;
name: string;
title: string;
@ -14,7 +14,7 @@ export interface SubscriptionItem {
export const SUBSCRIPTIONS_LIST: SubscriptionItem[] = [
{
kind: NotificationKind.NewPackageRelease,
kind: EventKind.NewPackageRelease,
icon: <MdNewReleases />,
name: 'newRelease',
title: 'New releases',