feat: add notification preferences database & audit support (#14100)

This commit is contained in:
Danny Kopping
2024-08-05 16:18:45 +02:00
committed by GitHub
parent 49a2880abc
commit e164b1e71c
49 changed files with 3229 additions and 368 deletions
+38
View File
@@ -1474,6 +1474,23 @@ func (q *querier) GetNotificationMessagesByStatus(ctx context.Context, arg datab
return q.db.GetNotificationMessagesByStatus(ctx, arg)
}
func (q *querier) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (database.NotificationTemplate, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationTemplate); err != nil {
return database.NotificationTemplate{}, err
}
return q.db.GetNotificationTemplateByID(ctx, id)
}
func (q *querier) GetNotificationTemplatesByKind(ctx context.Context, kind database.NotificationTemplateKind) ([]database.NotificationTemplate, error) {
// TODO: restrict 'system' kind to admins only?
// All notification templates share the same rbac.Object, so there is no need
// to authorize them individually. If this passes, all notification templates can be read.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationTemplate); err != nil {
return nil, err
}
return q.db.GetNotificationTemplatesByKind(ctx, kind)
}
func (q *querier) GetNotificationsSettings(ctx context.Context) (string, error) {
// No authz checks
return q.db.GetNotificationsSettings(ctx)
@@ -2085,6 +2102,13 @@ func (q *querier) GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([
return q.db.GetUserLinksByUserID(ctx, userID)
}
func (q *querier) GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]database.NotificationPreference, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationPreference.WithOwner(userID.String())); err != nil {
return nil, err
}
return q.db.GetUserNotificationPreferences(ctx, userID)
}
func (q *querier) GetUserWorkspaceBuildParameters(ctx context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
u, err := q.db.GetUserByID(ctx, params.OwnerID)
if err != nil {
@@ -3011,6 +3035,13 @@ func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemb
return q.db.UpdateMemberRoles(ctx, arg)
}
func (q *querier) UpdateNotificationTemplateMethodByID(ctx context.Context, arg database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationTemplate); err != nil {
return database.NotificationTemplate{}, err
}
return q.db.UpdateNotificationTemplateMethodByID(ctx, arg)
}
func (q *querier) UpdateOAuth2ProviderAppByID(ctx context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceOauth2App); err != nil {
return database.OAuth2ProviderApp{}, err
@@ -3326,6 +3357,13 @@ func (q *querier) UpdateUserLoginType(ctx context.Context, arg database.UpdateUs
return q.db.UpdateUserLoginType(ctx, arg)
}
func (q *querier) UpdateUserNotificationPreferences(ctx context.Context, arg database.UpdateUserNotificationPreferencesParams) (int64, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationPreference.WithOwner(arg.UserID.String())); err != nil {
return -1, err
}
return q.db.UpdateUserNotificationPreferences(ctx, arg)
}
func (q *querier) UpdateUserProfile(ctx context.Context, arg database.UpdateUserProfileParams) (database.User, error) {
u, err := q.db.GetUserByID(ctx, arg.ID)
if err != nil {
+39
View File
@@ -16,6 +16,7 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/codersdk"
@@ -2561,6 +2562,10 @@ func (s *MethodTestSuite) TestSystemFunctions() {
AgentID: uuid.New(),
}).Asserts(tpl, policy.ActionCreate)
}))
}
func (s *MethodTestSuite) TestNotifications() {
// System functions
s.Run("AcquireNotificationMessages", s.Subtest(func(db database.Store, check *expects) {
// TODO: update this test once we have a specific role for notifications
check.Args(database.AcquireNotificationMessagesParams{}).Asserts(rbac.ResourceSystem, policy.ActionUpdate)
@@ -2596,6 +2601,40 @@ func (s *MethodTestSuite) TestSystemFunctions() {
Limit: 10,
}).Asserts(rbac.ResourceSystem, policy.ActionRead)
}))
// Notification templates
s.Run("GetNotificationTemplateByID", s.Subtest(func(db database.Store, check *expects) {
user := dbgen.User(s.T(), db, database.User{})
check.Args(user.ID).Asserts(rbac.ResourceNotificationTemplate, policy.ActionRead).
Errors(dbmem.ErrUnimplemented)
}))
s.Run("GetNotificationTemplatesByKind", s.Subtest(func(db database.Store, check *expects) {
check.Args(database.NotificationTemplateKindSystem).
Asserts(rbac.ResourceNotificationTemplate, policy.ActionRead).
Errors(dbmem.ErrUnimplemented)
}))
s.Run("UpdateNotificationTemplateMethodByID", s.Subtest(func(db database.Store, check *expects) {
check.Args(database.UpdateNotificationTemplateMethodByIDParams{
Method: database.NullNotificationMethod{NotificationMethod: database.NotificationMethodWebhook, Valid: true},
ID: notifications.TemplateWorkspaceDormant,
}).Asserts(rbac.ResourceNotificationTemplate, policy.ActionUpdate).
Errors(dbmem.ErrUnimplemented)
}))
// Notification preferences
s.Run("GetUserNotificationPreferences", s.Subtest(func(db database.Store, check *expects) {
user := dbgen.User(s.T(), db, database.User{})
check.Args(user.ID).
Asserts(rbac.ResourceNotificationPreference.WithOwner(user.ID.String()), policy.ActionRead)
}))
s.Run("UpdateUserNotificationPreferences", s.Subtest(func(db database.Store, check *expects) {
user := dbgen.User(s.T(), db, database.User{})
check.Args(database.UpdateUserNotificationPreferencesParams{
UserID: user.ID,
NotificationTemplateIds: []uuid.UUID{notifications.TemplateWorkspaceAutoUpdated, notifications.TemplateWorkspaceDeleted},
Disableds: []bool{true, false},
}).Asserts(rbac.ResourceNotificationPreference.WithOwner(user.ID.String()), policy.ActionUpdate)
}))
}
func (s *MethodTestSuite) TestOAuth2ProviderApps() {
+87
View File
@@ -65,6 +65,7 @@ func New() database.Store {
files: make([]database.File, 0),
gitSSHKey: make([]database.GitSSHKey, 0),
notificationMessages: make([]database.NotificationMessage, 0),
notificationPreferences: make([]database.NotificationPreference, 0),
parameterSchemas: make([]database.ParameterSchema, 0),
provisionerDaemons: make([]database.ProvisionerDaemon, 0),
workspaceAgents: make([]database.WorkspaceAgent, 0),
@@ -160,6 +161,7 @@ type data struct {
jfrogXRayScans []database.JfrogXrayScan
licenses []database.License
notificationMessages []database.NotificationMessage
notificationPreferences []database.NotificationPreference
oauth2ProviderApps []database.OAuth2ProviderApp
oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret
oauth2ProviderAppCodes []database.OAuth2ProviderAppCode
@@ -2708,6 +2710,18 @@ func (q *FakeQuerier) GetNotificationMessagesByStatus(_ context.Context, arg dat
return out, nil
}
func (*FakeQuerier) GetNotificationTemplateByID(_ context.Context, _ uuid.UUID) (database.NotificationTemplate, error) {
// Not implementing this function because it relies on state in the database which is created with migrations.
// We could consider using code-generation to align the database state and dbmem, but it's not worth it right now.
return database.NotificationTemplate{}, ErrUnimplemented
}
func (*FakeQuerier) GetNotificationTemplatesByKind(_ context.Context, _ database.NotificationTemplateKind) ([]database.NotificationTemplate, error) {
// Not implementing this function because it relies on state in the database which is created with migrations.
// We could consider using code-generation to align the database state and dbmem, but it's not worth it right now.
return nil, ErrUnimplemented
}
func (q *FakeQuerier) GetNotificationsSettings(_ context.Context) (string, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@@ -4853,6 +4867,22 @@ func (q *FakeQuerier) GetUserLinksByUserID(_ context.Context, userID uuid.UUID)
return uls, nil
}
func (q *FakeQuerier) GetUserNotificationPreferences(_ context.Context, userID uuid.UUID) ([]database.NotificationPreference, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
out := make([]database.NotificationPreference, 0, len(q.notificationPreferences))
for _, np := range q.notificationPreferences {
if np.UserID != userID {
continue
}
out = append(out, np)
}
return out, nil
}
func (q *FakeQuerier) GetUserWorkspaceBuildParameters(_ context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@@ -7520,6 +7550,12 @@ func (q *FakeQuerier) UpdateMemberRoles(_ context.Context, arg database.UpdateMe
return database.OrganizationMember{}, sql.ErrNoRows
}
func (*FakeQuerier) UpdateNotificationTemplateMethodByID(_ context.Context, _ database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) {
// Not implementing this function because it relies on state in the database which is created with migrations.
// We could consider using code-generation to align the database state and dbmem, but it's not worth it right now.
return database.NotificationTemplate{}, ErrUnimplemented
}
func (q *FakeQuerier) UpdateOAuth2ProviderAppByID(_ context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
err := validateDatabaseType(arg)
if err != nil {
@@ -8114,6 +8150,57 @@ func (q *FakeQuerier) UpdateUserLoginType(_ context.Context, arg database.Update
return database.User{}, sql.ErrNoRows
}
func (q *FakeQuerier) UpdateUserNotificationPreferences(_ context.Context, arg database.UpdateUserNotificationPreferencesParams) (int64, error) {
err := validateDatabaseType(arg)
if err != nil {
return 0, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
var upserted int64
for i := range arg.NotificationTemplateIds {
var (
found bool
templateID = arg.NotificationTemplateIds[i]
disabled = arg.Disableds[i]
)
for j, np := range q.notificationPreferences {
if np.UserID != arg.UserID {
continue
}
if np.NotificationTemplateID != templateID {
continue
}
np.Disabled = disabled
np.UpdatedAt = dbtime.Now()
q.notificationPreferences[j] = np
upserted++
found = true
break
}
if !found {
np := database.NotificationPreference{
Disabled: disabled,
UserID: arg.UserID,
NotificationTemplateID: templateID,
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
}
q.notificationPreferences = append(q.notificationPreferences, np)
upserted++
}
}
return upserted, nil
}
func (q *FakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUserProfileParams) (database.User, error) {
if err := validateDatabaseType(arg); err != nil {
return database.User{}, err
+35
View File
@@ -746,6 +746,20 @@ func (m metricsStore) GetNotificationMessagesByStatus(ctx context.Context, arg d
return r0, r1
}
func (m metricsStore) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (database.NotificationTemplate, error) {
start := time.Now()
r0, r1 := m.s.GetNotificationTemplateByID(ctx, id)
m.queryLatencies.WithLabelValues("GetNotificationTemplateByID").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) GetNotificationTemplatesByKind(ctx context.Context, kind database.NotificationTemplateKind) ([]database.NotificationTemplate, error) {
start := time.Now()
r0, r1 := m.s.GetNotificationTemplatesByKind(ctx, kind)
m.queryLatencies.WithLabelValues("GetNotificationTemplatesByKind").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) GetNotificationsSettings(ctx context.Context) (string, error) {
start := time.Now()
r0, r1 := m.s.GetNotificationsSettings(ctx)
@@ -1222,6 +1236,13 @@ func (m metricsStore) GetUserLinksByUserID(ctx context.Context, userID uuid.UUID
return r0, r1
}
func (m metricsStore) GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]database.NotificationPreference, error) {
start := time.Now()
r0, r1 := m.s.GetUserNotificationPreferences(ctx, userID)
m.queryLatencies.WithLabelValues("GetUserNotificationPreferences").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) GetUserWorkspaceBuildParameters(ctx context.Context, ownerID database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
start := time.Now()
r0, r1 := m.s.GetUserWorkspaceBuildParameters(ctx, ownerID)
@@ -1957,6 +1978,13 @@ func (m metricsStore) UpdateMemberRoles(ctx context.Context, arg database.Update
return member, err
}
func (m metricsStore) UpdateNotificationTemplateMethodByID(ctx context.Context, arg database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) {
start := time.Now()
r0, r1 := m.s.UpdateNotificationTemplateMethodByID(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateNotificationTemplateMethodByID").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) UpdateOAuth2ProviderAppByID(ctx context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
start := time.Now()
r0, r1 := m.s.UpdateOAuth2ProviderAppByID(ctx, arg)
@@ -2139,6 +2167,13 @@ func (m metricsStore) UpdateUserLoginType(ctx context.Context, arg database.Upda
return r0, r1
}
func (m metricsStore) UpdateUserNotificationPreferences(ctx context.Context, arg database.UpdateUserNotificationPreferencesParams) (int64, error) {
start := time.Now()
r0, r1 := m.s.UpdateUserNotificationPreferences(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateUserNotificationPreferences").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) UpdateUserProfile(ctx context.Context, arg database.UpdateUserProfileParams) (database.User, error) {
start := time.Now()
user, err := m.s.UpdateUserProfile(ctx, arg)
+75
View File
@@ -1495,6 +1495,36 @@ func (mr *MockStoreMockRecorder) GetNotificationMessagesByStatus(arg0, arg1 any)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationMessagesByStatus", reflect.TypeOf((*MockStore)(nil).GetNotificationMessagesByStatus), arg0, arg1)
}
// GetNotificationTemplateByID mocks base method.
func (m *MockStore) GetNotificationTemplateByID(arg0 context.Context, arg1 uuid.UUID) (database.NotificationTemplate, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetNotificationTemplateByID", arg0, arg1)
ret0, _ := ret[0].(database.NotificationTemplate)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetNotificationTemplateByID indicates an expected call of GetNotificationTemplateByID.
func (mr *MockStoreMockRecorder) GetNotificationTemplateByID(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationTemplateByID", reflect.TypeOf((*MockStore)(nil).GetNotificationTemplateByID), arg0, arg1)
}
// GetNotificationTemplatesByKind mocks base method.
func (m *MockStore) GetNotificationTemplatesByKind(arg0 context.Context, arg1 database.NotificationTemplateKind) ([]database.NotificationTemplate, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetNotificationTemplatesByKind", arg0, arg1)
ret0, _ := ret[0].([]database.NotificationTemplate)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetNotificationTemplatesByKind indicates an expected call of GetNotificationTemplatesByKind.
func (mr *MockStoreMockRecorder) GetNotificationTemplatesByKind(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationTemplatesByKind", reflect.TypeOf((*MockStore)(nil).GetNotificationTemplatesByKind), arg0, arg1)
}
// GetNotificationsSettings mocks base method.
func (m *MockStore) GetNotificationsSettings(arg0 context.Context) (string, error) {
m.ctrl.T.Helper()
@@ -2545,6 +2575,21 @@ func (mr *MockStoreMockRecorder) GetUserLinksByUserID(arg0, arg1 any) *gomock.Ca
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserLinksByUserID", reflect.TypeOf((*MockStore)(nil).GetUserLinksByUserID), arg0, arg1)
}
// GetUserNotificationPreferences mocks base method.
func (m *MockStore) GetUserNotificationPreferences(arg0 context.Context, arg1 uuid.UUID) ([]database.NotificationPreference, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserNotificationPreferences", arg0, arg1)
ret0, _ := ret[0].([]database.NotificationPreference)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserNotificationPreferences indicates an expected call of GetUserNotificationPreferences.
func (mr *MockStoreMockRecorder) GetUserNotificationPreferences(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserNotificationPreferences", reflect.TypeOf((*MockStore)(nil).GetUserNotificationPreferences), arg0, arg1)
}
// GetUserWorkspaceBuildParameters mocks base method.
func (m *MockStore) GetUserWorkspaceBuildParameters(arg0 context.Context, arg1 database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
m.ctrl.T.Helper()
@@ -4131,6 +4176,21 @@ func (mr *MockStoreMockRecorder) UpdateMemberRoles(arg0, arg1 any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMemberRoles", reflect.TypeOf((*MockStore)(nil).UpdateMemberRoles), arg0, arg1)
}
// UpdateNotificationTemplateMethodByID mocks base method.
func (m *MockStore) UpdateNotificationTemplateMethodByID(arg0 context.Context, arg1 database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateNotificationTemplateMethodByID", arg0, arg1)
ret0, _ := ret[0].(database.NotificationTemplate)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateNotificationTemplateMethodByID indicates an expected call of UpdateNotificationTemplateMethodByID.
func (mr *MockStoreMockRecorder) UpdateNotificationTemplateMethodByID(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateNotificationTemplateMethodByID", reflect.TypeOf((*MockStore)(nil).UpdateNotificationTemplateMethodByID), arg0, arg1)
}
// UpdateOAuth2ProviderAppByID mocks base method.
func (m *MockStore) UpdateOAuth2ProviderAppByID(arg0 context.Context, arg1 database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
m.ctrl.T.Helper()
@@ -4504,6 +4564,21 @@ func (mr *MockStoreMockRecorder) UpdateUserLoginType(arg0, arg1 any) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserLoginType", reflect.TypeOf((*MockStore)(nil).UpdateUserLoginType), arg0, arg1)
}
// UpdateUserNotificationPreferences mocks base method.
func (m *MockStore) UpdateUserNotificationPreferences(arg0 context.Context, arg1 database.UpdateUserNotificationPreferencesParams) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUserNotificationPreferences", arg0, arg1)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateUserNotificationPreferences indicates an expected call of UpdateUserNotificationPreferences.
func (mr *MockStoreMockRecorder) UpdateUserNotificationPreferences(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserNotificationPreferences", reflect.TypeOf((*MockStore)(nil).UpdateUserNotificationPreferences), arg0, arg1)
}
// UpdateUserProfile mocks base method.
func (m *MockStore) UpdateUserProfile(arg0 context.Context, arg1 database.UpdateUserProfileParams) (database.User, error) {
m.ctrl.T.Helper()
+49 -3
View File
@@ -84,7 +84,8 @@ CREATE TYPE notification_message_status AS ENUM (
'sent',
'permanent_failure',
'temporary_failure',
'unknown'
'unknown',
'inhibited'
);
CREATE TYPE notification_method AS ENUM (
@@ -92,6 +93,10 @@ CREATE TYPE notification_method AS ENUM (
'webhook'
);
CREATE TYPE notification_template_kind AS ENUM (
'system'
);
CREATE TYPE parameter_destination_scheme AS ENUM (
'none',
'environment_variable',
@@ -164,7 +169,8 @@ CREATE TYPE resource_type AS ENUM (
'oauth2_provider_app_secret',
'custom_role',
'organization_member',
'notifications_settings'
'notifications_settings',
'notification_template'
);
CREATE TYPE startup_script_behavior AS ENUM (
@@ -249,6 +255,23 @@ BEGIN
END;
$$;
CREATE FUNCTION inhibit_enqueue_if_disabled() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
-- Fail the insertion if the user has disabled this notification.
IF EXISTS (SELECT 1
FROM notification_preferences
WHERE disabled = TRUE
AND user_id = NEW.user_id
AND notification_template_id = NEW.notification_template_id) THEN
RAISE EXCEPTION 'cannot enqueue message: user has disabled this notification';
END IF;
RETURN NEW;
END;
$$;
CREATE FUNCTION insert_apikey_fail_if_user_deleted() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -567,17 +590,29 @@ CREATE TABLE notification_messages (
queued_seconds double precision
);
CREATE TABLE notification_preferences (
user_id uuid NOT NULL,
notification_template_id uuid NOT NULL,
disabled boolean DEFAULT false NOT NULL,
created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
);
CREATE TABLE notification_templates (
id uuid NOT NULL,
name text NOT NULL,
title_template text NOT NULL,
body_template text NOT NULL,
actions jsonb,
"group" text
"group" text,
method notification_method,
kind notification_template_kind DEFAULT 'system'::notification_template_kind NOT NULL
);
COMMENT ON TABLE notification_templates IS 'Templates from which to create notification messages.';
COMMENT ON COLUMN notification_templates.method IS 'NULL defers to the deployment-level method';
CREATE TABLE oauth2_provider_app_codes (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -1536,6 +1571,9 @@ ALTER TABLE ONLY licenses
ALTER TABLE ONLY notification_messages
ADD CONSTRAINT notification_messages_pkey PRIMARY KEY (id);
ALTER TABLE ONLY notification_preferences
ADD CONSTRAINT notification_preferences_pkey PRIMARY KEY (user_id, notification_template_id);
ALTER TABLE ONLY notification_templates
ADD CONSTRAINT notification_templates_name_key UNIQUE (name);
@@ -1798,6 +1836,8 @@ CREATE INDEX workspace_resources_job_id_idx ON workspace_resources USING btree (
CREATE UNIQUE INDEX workspaces_owner_id_lower_idx ON workspaces USING btree (owner_id, lower((name)::text)) WHERE (deleted = false);
CREATE TRIGGER inhibit_enqueue_if_disabled BEFORE INSERT ON notification_messages FOR EACH ROW EXECUTE FUNCTION inhibit_enqueue_if_disabled();
CREATE TRIGGER tailnet_notify_agent_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_agents FOR EACH ROW EXECUTE FUNCTION tailnet_notify_agent_change();
CREATE TRIGGER tailnet_notify_client_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_clients FOR EACH ROW EXECUTE FUNCTION tailnet_notify_client_change();
@@ -1851,6 +1891,12 @@ ALTER TABLE ONLY notification_messages
ALTER TABLE ONLY notification_messages
ADD CONSTRAINT notification_messages_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY notification_preferences
ADD CONSTRAINT notification_preferences_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE;
ALTER TABLE ONLY notification_preferences
ADD CONSTRAINT notification_preferences_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY oauth2_provider_app_codes
ADD CONSTRAINT oauth2_provider_app_codes_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE;
@@ -17,6 +17,8 @@ const (
ForeignKeyJfrogXrayScansWorkspaceID ForeignKeyConstraint = "jfrog_xray_scans_workspace_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE;
ForeignKeyNotificationMessagesNotificationTemplateID ForeignKeyConstraint = "notification_messages_notification_template_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE;
ForeignKeyNotificationMessagesUserID ForeignKeyConstraint = "notification_messages_user_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyNotificationPreferencesNotificationTemplateID ForeignKeyConstraint = "notification_preferences_notification_template_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE;
ForeignKeyNotificationPreferencesUserID ForeignKeyConstraint = "notification_preferences_user_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyOauth2ProviderAppCodesAppID ForeignKeyConstraint = "oauth2_provider_app_codes_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE;
ForeignKeyOauth2ProviderAppCodesUserID ForeignKeyConstraint = "oauth2_provider_app_codes_user_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyOauth2ProviderAppSecretsAppID ForeignKeyConstraint = "oauth2_provider_app_secrets_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE;
@@ -0,0 +1,9 @@
ALTER TABLE notification_templates
DROP COLUMN IF EXISTS method,
DROP COLUMN IF EXISTS kind;
DROP TABLE IF EXISTS notification_preferences;
DROP TYPE IF EXISTS notification_template_kind;
DROP TRIGGER IF EXISTS inhibit_enqueue_if_disabled ON notification_messages;
DROP FUNCTION IF EXISTS inhibit_enqueue_if_disabled;
@@ -0,0 +1,52 @@
CREATE TABLE notification_preferences
(
user_id uuid REFERENCES users ON DELETE CASCADE NOT NULL,
notification_template_id uuid REFERENCES notification_templates ON DELETE CASCADE NOT NULL,
disabled bool NOT NULL DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, notification_template_id)
);
-- Add a new type (to be expanded upon later) which specifies the kind of notification template.
CREATE TYPE notification_template_kind AS ENUM (
'system'
);
ALTER TABLE notification_templates
-- Allow per-template notification method (enterprise only).
ADD COLUMN method notification_method,
-- Update all existing notification templates to be system templates.
ADD COLUMN kind notification_template_kind DEFAULT 'system'::notification_template_kind NOT NULL;
COMMENT ON COLUMN notification_templates.method IS 'NULL defers to the deployment-level method';
-- No equivalent in down migration because ENUM values cannot be deleted.
ALTER TYPE notification_message_status ADD VALUE IF NOT EXISTS 'inhibited';
-- Function to prevent enqueuing notifications unnecessarily.
CREATE OR REPLACE FUNCTION inhibit_enqueue_if_disabled()
RETURNS TRIGGER AS
$$
BEGIN
-- Fail the insertion if the user has disabled this notification.
IF EXISTS (SELECT 1
FROM notification_preferences
WHERE disabled = TRUE
AND user_id = NEW.user_id
AND notification_template_id = NEW.notification_template_id) THEN
RAISE EXCEPTION 'cannot enqueue message: user has disabled this notification';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger to execute above function on insertion.
CREATE TRIGGER inhibit_enqueue_if_disabled
BEFORE INSERT
ON notification_messages
FOR EACH ROW
EXECUTE FUNCTION inhibit_enqueue_if_disabled();
-- Allow modifications to notification templates to be audited.
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'notification_template';
@@ -0,0 +1,5 @@
INSERT INTO notification_templates (id, name, title_template, body_template, "group")
VALUES ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'A', 'title', 'body', 'Group 1') ON CONFLICT DO NOTHING;
INSERT INTO notification_preferences (user_id, notification_template_id, disabled, created_at, updated_at)
VALUES ('a0061a8e-7db7-4585-838c-3116a003dd21', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', FALSE, '2024-07-15 10:30:00+00', '2024-07-15 10:30:00+00');
+74 -2
View File
@@ -669,6 +669,7 @@ const (
NotificationMessageStatusPermanentFailure NotificationMessageStatus = "permanent_failure"
NotificationMessageStatusTemporaryFailure NotificationMessageStatus = "temporary_failure"
NotificationMessageStatusUnknown NotificationMessageStatus = "unknown"
NotificationMessageStatusInhibited NotificationMessageStatus = "inhibited"
)
func (e *NotificationMessageStatus) Scan(src interface{}) error {
@@ -713,7 +714,8 @@ func (e NotificationMessageStatus) Valid() bool {
NotificationMessageStatusSent,
NotificationMessageStatusPermanentFailure,
NotificationMessageStatusTemporaryFailure,
NotificationMessageStatusUnknown:
NotificationMessageStatusUnknown,
NotificationMessageStatusInhibited:
return true
}
return false
@@ -727,6 +729,7 @@ func AllNotificationMessageStatusValues() []NotificationMessageStatus {
NotificationMessageStatusPermanentFailure,
NotificationMessageStatusTemporaryFailure,
NotificationMessageStatusUnknown,
NotificationMessageStatusInhibited,
}
}
@@ -788,6 +791,61 @@ func AllNotificationMethodValues() []NotificationMethod {
}
}
type NotificationTemplateKind string
const (
NotificationTemplateKindSystem NotificationTemplateKind = "system"
)
func (e *NotificationTemplateKind) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = NotificationTemplateKind(s)
case string:
*e = NotificationTemplateKind(s)
default:
return fmt.Errorf("unsupported scan type for NotificationTemplateKind: %T", src)
}
return nil
}
type NullNotificationTemplateKind struct {
NotificationTemplateKind NotificationTemplateKind `json:"notification_template_kind"`
Valid bool `json:"valid"` // Valid is true if NotificationTemplateKind is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullNotificationTemplateKind) Scan(value interface{}) error {
if value == nil {
ns.NotificationTemplateKind, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.NotificationTemplateKind.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullNotificationTemplateKind) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.NotificationTemplateKind), nil
}
func (e NotificationTemplateKind) Valid() bool {
switch e {
case NotificationTemplateKindSystem:
return true
}
return false
}
func AllNotificationTemplateKindValues() []NotificationTemplateKind {
return []NotificationTemplateKind{
NotificationTemplateKindSystem,
}
}
type ParameterDestinationScheme string
const (
@@ -1353,6 +1411,7 @@ const (
ResourceTypeCustomRole ResourceType = "custom_role"
ResourceTypeOrganizationMember ResourceType = "organization_member"
ResourceTypeNotificationsSettings ResourceType = "notifications_settings"
ResourceTypeNotificationTemplate ResourceType = "notification_template"
)
func (e *ResourceType) Scan(src interface{}) error {
@@ -1409,7 +1468,8 @@ func (e ResourceType) Valid() bool {
ResourceTypeOauth2ProviderAppSecret,
ResourceTypeCustomRole,
ResourceTypeOrganizationMember,
ResourceTypeNotificationsSettings:
ResourceTypeNotificationsSettings,
ResourceTypeNotificationTemplate:
return true
}
return false
@@ -1435,6 +1495,7 @@ func AllResourceTypeValues() []ResourceType {
ResourceTypeCustomRole,
ResourceTypeOrganizationMember,
ResourceTypeNotificationsSettings,
ResourceTypeNotificationTemplate,
}
}
@@ -2034,6 +2095,14 @@ type NotificationMessage struct {
QueuedSeconds sql.NullFloat64 `db:"queued_seconds" json:"queued_seconds"`
}
type NotificationPreference struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"`
Disabled bool `db:"disabled" json:"disabled"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// Templates from which to create notification messages.
type NotificationTemplate struct {
ID uuid.UUID `db:"id" json:"id"`
@@ -2042,6 +2111,9 @@ type NotificationTemplate struct {
BodyTemplate string `db:"body_template" json:"body_template"`
Actions []byte `db:"actions" json:"actions"`
Group sql.NullString `db:"group" json:"group"`
// NULL defers to the deployment-level method
Method NullNotificationMethod `db:"method" json:"method"`
Kind NotificationTemplateKind `db:"kind" json:"kind"`
}
// A table used to configure apps that can use Coder as an OAuth2 provider, the reverse of what we are calling external authentication.
+5
View File
@@ -162,6 +162,8 @@ type sqlcQuerier interface {
GetLicenses(ctx context.Context) ([]License, error)
GetLogoURL(ctx context.Context) (string, error)
GetNotificationMessagesByStatus(ctx context.Context, arg GetNotificationMessagesByStatusParams) ([]NotificationMessage, error)
GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error)
GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error)
GetNotificationsSettings(ctx context.Context) (string, error)
GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error)
GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error)
@@ -265,6 +267,7 @@ type sqlcQuerier interface {
GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error)
GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error)
GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([]UserLink, error)
GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]NotificationPreference, error)
GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error)
// This will never return deleted users.
GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error)
@@ -401,6 +404,7 @@ type sqlcQuerier interface {
UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error)
UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error)
UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error)
UpdateNotificationTemplateMethodByID(ctx context.Context, arg UpdateNotificationTemplateMethodByIDParams) (NotificationTemplate, error)
UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error)
UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error)
UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error)
@@ -427,6 +431,7 @@ type sqlcQuerier interface {
UpdateUserLink(ctx context.Context, arg UpdateUserLinkParams) (UserLink, error)
UpdateUserLinkedID(ctx context.Context, arg UpdateUserLinkedIDParams) (UserLink, error)
UpdateUserLoginType(ctx context.Context, arg UpdateUserLoginTypeParams) (User, error)
UpdateUserNotificationPreferences(ctx context.Context, arg UpdateUserNotificationPreferencesParams) (int64, error)
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error)
UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error)
UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error)
+172 -12
View File
@@ -3335,14 +3335,18 @@ SELECT
nm.id,
nm.payload,
nm.method,
nm.attempt_count::int AS attempt_count,
nm.queued_seconds::float AS queued_seconds,
nm.attempt_count::int AS attempt_count,
nm.queued_seconds::float AS queued_seconds,
-- template
nt.id AS template_id,
nt.id AS template_id,
nt.title_template,
nt.body_template
nt.body_template,
-- preferences
(CASE WHEN np.disabled IS NULL THEN false ELSE np.disabled END)::bool AS disabled
FROM acquired nm
JOIN notification_templates nt ON nm.notification_template_id = nt.id
LEFT JOIN notification_preferences AS np
ON (np.user_id = nm.user_id AND np.notification_template_id = nm.notification_template_id)
`
type AcquireNotificationMessagesParams struct {
@@ -3361,6 +3365,7 @@ type AcquireNotificationMessagesRow struct {
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
TitleTemplate string `db:"title_template" json:"title_template"`
BodyTemplate string `db:"body_template" json:"body_template"`
Disabled bool `db:"disabled" json:"disabled"`
}
// Acquires the lease for a given count of notification messages, to enable concurrent dequeuing and subsequent sending.
@@ -3396,6 +3401,7 @@ func (q *sqlQuerier) AcquireNotificationMessages(ctx context.Context, arg Acquir
&i.TemplateID,
&i.TitleTemplate,
&i.BodyTemplate,
&i.Disabled,
); err != nil {
return nil, err
}
@@ -3534,10 +3540,11 @@ func (q *sqlQuerier) EnqueueNotificationMessage(ctx context.Context, arg Enqueue
const fetchNewMessageMetadata = `-- name: FetchNewMessageMetadata :one
SELECT nt.name AS notification_name,
nt.actions AS actions,
nt.method AS custom_method,
u.id AS user_id,
u.email AS user_email,
COALESCE(NULLIF(u.name, ''), NULLIF(u.username, ''))::text AS user_name,
COALESCE(u.username, '') AS user_username
u.username AS user_username
FROM notification_templates nt,
users u
WHERE nt.id = $1
@@ -3550,12 +3557,13 @@ type FetchNewMessageMetadataParams struct {
}
type FetchNewMessageMetadataRow struct {
NotificationName string `db:"notification_name" json:"notification_name"`
Actions []byte `db:"actions" json:"actions"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
UserEmail string `db:"user_email" json:"user_email"`
UserName string `db:"user_name" json:"user_name"`
UserUsername string `db:"user_username" json:"user_username"`
NotificationName string `db:"notification_name" json:"notification_name"`
Actions []byte `db:"actions" json:"actions"`
CustomMethod NullNotificationMethod `db:"custom_method" json:"custom_method"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
UserEmail string `db:"user_email" json:"user_email"`
UserName string `db:"user_name" json:"user_name"`
UserUsername string `db:"user_username" json:"user_username"`
}
// This is used to build up the notification_message's JSON payload.
@@ -3565,6 +3573,7 @@ func (q *sqlQuerier) FetchNewMessageMetadata(ctx context.Context, arg FetchNewMe
err := row.Scan(
&i.NotificationName,
&i.Actions,
&i.CustomMethod,
&i.UserID,
&i.UserEmail,
&i.UserName,
@@ -3574,7 +3583,10 @@ func (q *sqlQuerier) FetchNewMessageMetadata(ctx context.Context, arg FetchNewMe
}
const getNotificationMessagesByStatus = `-- name: GetNotificationMessagesByStatus :many
SELECT id, notification_template_id, user_id, method, status, status_reason, created_by, payload, attempt_count, targets, created_at, updated_at, leased_until, next_retry_after, queued_seconds FROM notification_messages WHERE status = $1 LIMIT $2::int
SELECT id, notification_template_id, user_id, method, status, status_reason, created_by, payload, attempt_count, targets, created_at, updated_at, leased_until, next_retry_after, queued_seconds
FROM notification_messages
WHERE status = $1
LIMIT $2::int
`
type GetNotificationMessagesByStatusParams struct {
@@ -3621,6 +3633,154 @@ func (q *sqlQuerier) GetNotificationMessagesByStatus(ctx context.Context, arg Ge
return items, nil
}
const getNotificationTemplateByID = `-- name: GetNotificationTemplateByID :one
SELECT id, name, title_template, body_template, actions, "group", method, kind
FROM notification_templates
WHERE id = $1::uuid
`
func (q *sqlQuerier) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error) {
row := q.db.QueryRowContext(ctx, getNotificationTemplateByID, id)
var i NotificationTemplate
err := row.Scan(
&i.ID,
&i.Name,
&i.TitleTemplate,
&i.BodyTemplate,
&i.Actions,
&i.Group,
&i.Method,
&i.Kind,
)
return i, err
}
const getNotificationTemplatesByKind = `-- name: GetNotificationTemplatesByKind :many
SELECT id, name, title_template, body_template, actions, "group", method, kind
FROM notification_templates
WHERE kind = $1::notification_template_kind
`
func (q *sqlQuerier) GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error) {
rows, err := q.db.QueryContext(ctx, getNotificationTemplatesByKind, kind)
if err != nil {
return nil, err
}
defer rows.Close()
var items []NotificationTemplate
for rows.Next() {
var i NotificationTemplate
if err := rows.Scan(
&i.ID,
&i.Name,
&i.TitleTemplate,
&i.BodyTemplate,
&i.Actions,
&i.Group,
&i.Method,
&i.Kind,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserNotificationPreferences = `-- name: GetUserNotificationPreferences :many
SELECT user_id, notification_template_id, disabled, created_at, updated_at
FROM notification_preferences
WHERE user_id = $1::uuid
`
func (q *sqlQuerier) GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]NotificationPreference, error) {
rows, err := q.db.QueryContext(ctx, getUserNotificationPreferences, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []NotificationPreference
for rows.Next() {
var i NotificationPreference
if err := rows.Scan(
&i.UserID,
&i.NotificationTemplateID,
&i.Disabled,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateNotificationTemplateMethodByID = `-- name: UpdateNotificationTemplateMethodByID :one
UPDATE notification_templates
SET method = $1::notification_method
WHERE id = $2::uuid
RETURNING id, name, title_template, body_template, actions, "group", method, kind
`
type UpdateNotificationTemplateMethodByIDParams struct {
Method NullNotificationMethod `db:"method" json:"method"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateNotificationTemplateMethodByID(ctx context.Context, arg UpdateNotificationTemplateMethodByIDParams) (NotificationTemplate, error) {
row := q.db.QueryRowContext(ctx, updateNotificationTemplateMethodByID, arg.Method, arg.ID)
var i NotificationTemplate
err := row.Scan(
&i.ID,
&i.Name,
&i.TitleTemplate,
&i.BodyTemplate,
&i.Actions,
&i.Group,
&i.Method,
&i.Kind,
)
return i, err
}
const updateUserNotificationPreferences = `-- name: UpdateUserNotificationPreferences :execrows
INSERT
INTO notification_preferences (user_id, notification_template_id, disabled)
SELECT $1::uuid, new_values.notification_template_id, new_values.disabled
FROM (SELECT UNNEST($2::uuid[]) AS notification_template_id,
UNNEST($3::bool[]) AS disabled) AS new_values
ON CONFLICT (user_id, notification_template_id) DO UPDATE
SET disabled = EXCLUDED.disabled,
updated_at = CURRENT_TIMESTAMP
`
type UpdateUserNotificationPreferencesParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
NotificationTemplateIds []uuid.UUID `db:"notification_template_ids" json:"notification_template_ids"`
Disableds []bool `db:"disableds" json:"disableds"`
}
func (q *sqlQuerier) UpdateUserNotificationPreferences(ctx context.Context, arg UpdateUserNotificationPreferencesParams) (int64, error) {
result, err := q.db.ExecContext(ctx, updateUserNotificationPreferences, arg.UserID, pq.Array(arg.NotificationTemplateIds), pq.Array(arg.Disableds))
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const deleteOAuth2ProviderAppByID = `-- name: DeleteOAuth2ProviderAppByID :exec
DELETE FROM oauth2_provider_apps WHERE id = $1
`
+46 -7
View File
@@ -2,10 +2,11 @@
-- This is used to build up the notification_message's JSON payload.
SELECT nt.name AS notification_name,
nt.actions AS actions,
nt.method AS custom_method,
u.id AS user_id,
u.email AS user_email,
COALESCE(NULLIF(u.name, ''), NULLIF(u.username, ''))::text AS user_name,
COALESCE(u.username, '') AS user_username
u.username AS user_username
FROM notification_templates nt,
users u
WHERE nt.id = @notification_template_id
@@ -79,14 +80,18 @@ SELECT
nm.id,
nm.payload,
nm.method,
nm.attempt_count::int AS attempt_count,
nm.queued_seconds::float AS queued_seconds,
nm.attempt_count::int AS attempt_count,
nm.queued_seconds::float AS queued_seconds,
-- template
nt.id AS template_id,
nt.id AS template_id,
nt.title_template,
nt.body_template
nt.body_template,
-- preferences
(CASE WHEN np.disabled IS NULL THEN false ELSE np.disabled END)::bool AS disabled
FROM acquired nm
JOIN notification_templates nt ON nm.notification_template_id = nt.id;
JOIN notification_templates nt ON nm.notification_template_id = nt.id
LEFT JOIN notification_preferences AS np
ON (np.user_id = nm.user_id AND np.notification_template_id = nm.notification_template_id);
-- name: BulkMarkNotificationMessagesFailed :execrows
UPDATE notification_messages
@@ -131,4 +136,38 @@ WHERE id IN
WHERE nested.updated_at < NOW() - INTERVAL '7 days');
-- name: GetNotificationMessagesByStatus :many
SELECT * FROM notification_messages WHERE status = @status LIMIT sqlc.arg('limit')::int;
SELECT *
FROM notification_messages
WHERE status = @status
LIMIT sqlc.arg('limit')::int;
-- name: GetUserNotificationPreferences :many
SELECT *
FROM notification_preferences
WHERE user_id = @user_id::uuid;
-- name: UpdateUserNotificationPreferences :execrows
INSERT
INTO notification_preferences (user_id, notification_template_id, disabled)
SELECT @user_id::uuid, new_values.notification_template_id, new_values.disabled
FROM (SELECT UNNEST(@notification_template_ids::uuid[]) AS notification_template_id,
UNNEST(@disableds::bool[]) AS disabled) AS new_values
ON CONFLICT (user_id, notification_template_id) DO UPDATE
SET disabled = EXCLUDED.disabled,
updated_at = CURRENT_TIMESTAMP;
-- name: UpdateNotificationTemplateMethodByID :one
UPDATE notification_templates
SET method = sqlc.narg('method')::notification_method
WHERE id = @id::uuid
RETURNING *;
-- name: GetNotificationTemplateByID :one
SELECT *
FROM notification_templates
WHERE id = @id::uuid;
-- name: GetNotificationTemplatesByKind :many
SELECT *
FROM notification_templates
WHERE kind = @kind::notification_template_kind;
+1
View File
@@ -24,6 +24,7 @@ const (
UniqueLicensesJWTKey UniqueConstraint = "licenses_jwt_key" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt);
UniqueLicensesPkey UniqueConstraint = "licenses_pkey" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_pkey PRIMARY KEY (id);
UniqueNotificationMessagesPkey UniqueConstraint = "notification_messages_pkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_pkey PRIMARY KEY (id);
UniqueNotificationPreferencesPkey UniqueConstraint = "notification_preferences_pkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_pkey PRIMARY KEY (user_id, notification_template_id);
UniqueNotificationTemplatesNameKey UniqueConstraint = "notification_templates_name_key" // ALTER TABLE ONLY notification_templates ADD CONSTRAINT notification_templates_name_key UNIQUE (name);
UniqueNotificationTemplatesPkey UniqueConstraint = "notification_templates_pkey" // ALTER TABLE ONLY notification_templates ADD CONSTRAINT notification_templates_pkey PRIMARY KEY (id);
UniqueOauth2ProviderAppCodesPkey UniqueConstraint = "oauth2_provider_app_codes_pkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_pkey PRIMARY KEY (id);