mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 11:48:42 +02:00
Compare commits
1 Commits
fix/ws-all
...
agent/lamb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42633a8d6c |
@@ -75,6 +75,8 @@ import type {
|
||||
ListAutopilotsResponse,
|
||||
GetAutopilotResponse,
|
||||
ListAutopilotRunsResponse,
|
||||
NotificationPreferenceResponse,
|
||||
NotificationPreferences,
|
||||
} from "../types";
|
||||
import type { OnboardingCompletionPath } from "../onboarding/types";
|
||||
import { type Logger, noopLogger } from "../logger";
|
||||
@@ -783,6 +785,18 @@ export class ApiClient {
|
||||
return this.fetch("/api/inbox/archive-completed", { method: "POST" });
|
||||
}
|
||||
|
||||
// Notification preferences
|
||||
async getNotificationPreferences(): Promise<NotificationPreferenceResponse> {
|
||||
return this.fetch("/api/notification-preferences");
|
||||
}
|
||||
|
||||
async updateNotificationPreferences(preferences: NotificationPreferences): Promise<NotificationPreferenceResponse> {
|
||||
return this.fetch("/api/notification-preferences", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ preferences }),
|
||||
});
|
||||
}
|
||||
|
||||
// App Config
|
||||
async getConfig(): Promise<{
|
||||
cdn_domain: string;
|
||||
|
||||
2
packages/core/notification-preferences/index.ts
Normal file
2
packages/core/notification-preferences/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./mutations";
|
||||
34
packages/core/notification-preferences/mutations.ts
Normal file
34
packages/core/notification-preferences/mutations.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { api } from "../api";
|
||||
import { useWorkspaceId } from "../hooks";
|
||||
import { notificationPreferenceKeys } from "./queries";
|
||||
import type { NotificationPreferences, NotificationPreferenceResponse } from "../types";
|
||||
|
||||
export function useUpdateNotificationPreferences() {
|
||||
const qc = useQueryClient();
|
||||
const wsId = useWorkspaceId();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (preferences: NotificationPreferences) =>
|
||||
api.updateNotificationPreferences(preferences),
|
||||
onMutate: async (preferences) => {
|
||||
await qc.cancelQueries({ queryKey: notificationPreferenceKeys.all(wsId) });
|
||||
const prev = qc.getQueryData<NotificationPreferenceResponse>(
|
||||
notificationPreferenceKeys.all(wsId),
|
||||
);
|
||||
qc.setQueryData<NotificationPreferenceResponse>(
|
||||
notificationPreferenceKeys.all(wsId),
|
||||
(old) => old ? { ...old, preferences } : { workspace_id: wsId, preferences },
|
||||
);
|
||||
return { prev };
|
||||
},
|
||||
onError: (_err, _vars, ctx) => {
|
||||
if (ctx?.prev) {
|
||||
qc.setQueryData(notificationPreferenceKeys.all(wsId), ctx.prev);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
qc.invalidateQueries({ queryKey: notificationPreferenceKeys.all(wsId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
13
packages/core/notification-preferences/queries.ts
Normal file
13
packages/core/notification-preferences/queries.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { api } from "../api";
|
||||
|
||||
export const notificationPreferenceKeys = {
|
||||
all: (wsId: string) => ["notification-preferences", wsId] as const,
|
||||
};
|
||||
|
||||
export function notificationPreferenceOptions(wsId: string) {
|
||||
return queryOptions({
|
||||
queryKey: notificationPreferenceKeys.all(wsId),
|
||||
queryFn: () => api.getNotificationPreferences(),
|
||||
});
|
||||
}
|
||||
@@ -35,6 +35,9 @@
|
||||
"./inbox/queries": "./inbox/queries.ts",
|
||||
"./inbox/mutations": "./inbox/mutations.ts",
|
||||
"./inbox/ws-updaters": "./inbox/ws-updaters.ts",
|
||||
"./notification-preferences": "./notification-preferences/index.ts",
|
||||
"./notification-preferences/queries": "./notification-preferences/queries.ts",
|
||||
"./notification-preferences/mutations": "./notification-preferences/mutations.ts",
|
||||
"./chat": "./chat/index.ts",
|
||||
"./chat/queries": "./chat/queries.ts",
|
||||
"./chat/mutations": "./chat/mutations.ts",
|
||||
|
||||
@@ -38,6 +38,7 @@ export type {
|
||||
} from "./agent";
|
||||
export type { Workspace, WorkspaceRepo, Member, MemberRole, User, MemberWithUser, Invitation } from "./workspace";
|
||||
export type { InboxItem, InboxSeverity, InboxItemType } from "./inbox";
|
||||
export type { NotificationGroupKey, NotificationGroupValue, NotificationPreferences, NotificationPreferenceResponse } from "./notification-preference";
|
||||
export type { Comment, CommentType, CommentAuthorType, Reaction } from "./comment";
|
||||
export type { Label, CreateLabelRequest, UpdateLabelRequest, ListLabelsResponse, IssueLabelsResponse } from "./label";
|
||||
export type { TimelineEntry, AssigneeFrequencyEntry } from "./activity";
|
||||
|
||||
15
packages/core/types/notification-preference.ts
Normal file
15
packages/core/types/notification-preference.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type NotificationGroupKey =
|
||||
| "assignments"
|
||||
| "status_changes"
|
||||
| "comments"
|
||||
| "updates"
|
||||
| "agent_activity";
|
||||
|
||||
export type NotificationGroupValue = "all" | "muted";
|
||||
|
||||
export type NotificationPreferences = Partial<Record<NotificationGroupKey, NotificationGroupValue>>;
|
||||
|
||||
export interface NotificationPreferenceResponse {
|
||||
workspace_id: string;
|
||||
preferences: NotificationPreferences;
|
||||
}
|
||||
106
packages/views/settings/components/notifications-tab.tsx
Normal file
106
packages/views/settings/components/notifications-tab.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useWorkspaceId } from "@multica/core/hooks";
|
||||
import { notificationPreferenceOptions } from "@multica/core/notification-preferences/queries";
|
||||
import { useUpdateNotificationPreferences } from "@multica/core/notification-preferences/mutations";
|
||||
import type { NotificationGroupKey, NotificationPreferences } from "@multica/core/types";
|
||||
import { Card, CardContent } from "@multica/ui/components/ui/card";
|
||||
import { Switch } from "@multica/ui/components/ui/switch";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const notificationGroups: {
|
||||
key: NotificationGroupKey;
|
||||
label: string;
|
||||
description: string;
|
||||
}[] = [
|
||||
{
|
||||
key: "assignments",
|
||||
label: "Assignments",
|
||||
description: "When you are assigned or unassigned from an issue",
|
||||
},
|
||||
{
|
||||
key: "status_changes",
|
||||
label: "Status changes",
|
||||
description: "When an issue you follow changes status (e.g. todo, in progress, done)",
|
||||
},
|
||||
{
|
||||
key: "comments",
|
||||
label: "Comments & Mentions",
|
||||
description: "New comments on issues you follow, or when someone @mentions you",
|
||||
},
|
||||
{
|
||||
key: "updates",
|
||||
label: "Priority & Due date",
|
||||
description: "When priority or due date changes on issues you follow",
|
||||
},
|
||||
{
|
||||
key: "agent_activity",
|
||||
label: "Agent activity",
|
||||
description: "When an agent task completes or fails",
|
||||
},
|
||||
];
|
||||
|
||||
export function NotificationsTab() {
|
||||
const wsId = useWorkspaceId();
|
||||
const { data } = useQuery(notificationPreferenceOptions(wsId));
|
||||
const mutation = useUpdateNotificationPreferences();
|
||||
|
||||
const preferences = data?.preferences ?? {};
|
||||
|
||||
const handleToggle = (key: NotificationGroupKey, enabled: boolean) => {
|
||||
const updated: NotificationPreferences = {
|
||||
...preferences,
|
||||
[key]: enabled ? "all" : "muted",
|
||||
};
|
||||
// Remove keys set to "all" (default) to keep the object clean
|
||||
if (enabled) {
|
||||
delete updated[key];
|
||||
}
|
||||
mutation.mutate(updated, {
|
||||
onError: () => toast.error("Failed to update notification settings"),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<section className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold">Inbox Notifications</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Control which events generate inbox notifications. Muted event types
|
||||
are silently filtered — you can still see them by visiting the issue
|
||||
directly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="divide-y">
|
||||
{notificationGroups.map((group) => {
|
||||
const enabled = preferences[group.key] !== "muted";
|
||||
return (
|
||||
<div
|
||||
key={group.key}
|
||||
className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
|
||||
>
|
||||
<div className="space-y-0.5 pr-4">
|
||||
<p className="text-sm font-medium">{group.label}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{group.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
handleToggle(group.key, checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { User, Palette, Key, Settings, Users, FolderGit2, FlaskConical } from "lucide-react";
|
||||
import { User, Palette, Key, Settings, Users, FolderGit2, FlaskConical, Bell } from "lucide-react";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@multica/ui/components/ui/tabs";
|
||||
import { useCurrentWorkspace } from "@multica/core/paths";
|
||||
import { AccountTab } from "./account-tab";
|
||||
@@ -11,10 +11,12 @@ import { WorkspaceTab } from "./workspace-tab";
|
||||
import { MembersTab } from "./members-tab";
|
||||
import { RepositoriesTab } from "./repositories-tab";
|
||||
import { LabsTab } from "./labs-tab";
|
||||
import { NotificationsTab } from "./notifications-tab";
|
||||
|
||||
const accountTabs = [
|
||||
{ value: "profile", label: "Profile", icon: User },
|
||||
{ value: "appearance", label: "Appearance", icon: Palette },
|
||||
{ value: "notifications", label: "Notifications", icon: Bell },
|
||||
{ value: "tokens", label: "API Tokens", icon: Key },
|
||||
];
|
||||
|
||||
@@ -81,6 +83,7 @@ export function SettingsPage({ extraAccountTabs }: SettingsPageProps = {}) {
|
||||
<div className="w-full max-w-3xl mx-auto p-6">
|
||||
<TabsContent value="profile"><AccountTab /></TabsContent>
|
||||
<TabsContent value="appearance"><AppearanceTab /></TabsContent>
|
||||
<TabsContent value="notifications"><NotificationsTab /></TabsContent>
|
||||
<TabsContent value="tokens"><TokensTab /></TabsContent>
|
||||
<TabsContent value="workspace"><WorkspaceTab /></TabsContent>
|
||||
<TabsContent value="repositories"><RepositoriesTab /></TabsContent>
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/multica-ai/multica/server/internal/events"
|
||||
"github.com/multica-ai/multica/server/internal/handler"
|
||||
"github.com/multica-ai/multica/server/internal/util"
|
||||
@@ -74,6 +75,70 @@ var parentBubbleNotifTypes = map[string]bool{
|
||||
"status_changed": true,
|
||||
}
|
||||
|
||||
// notifTypeToGroup maps each InboxItemType to a user-configurable preference
|
||||
// group. Types not in this map are always delivered (not configurable).
|
||||
var notifTypeToGroup = map[string]string{
|
||||
"issue_assigned": "assignments",
|
||||
"unassigned": "assignments",
|
||||
"assignee_changed": "assignments",
|
||||
"status_changed": "status_changes",
|
||||
"new_comment": "comments",
|
||||
"mentioned": "comments",
|
||||
"priority_changed": "updates",
|
||||
"due_date_changed": "updates",
|
||||
"task_completed": "agent_activity",
|
||||
"task_failed": "agent_activity",
|
||||
"agent_blocked": "agent_activity",
|
||||
"agent_completed": "agent_activity",
|
||||
}
|
||||
|
||||
// isNotifMuted returns true if the given notification type is muted for a user
|
||||
// based on their parsed preferences map.
|
||||
func isNotifMuted(prefs map[string]string, notifType string) bool {
|
||||
group, ok := notifTypeToGroup[notifType]
|
||||
if !ok {
|
||||
return false // unconfigurable types are always delivered
|
||||
}
|
||||
return prefs[group] == "muted"
|
||||
}
|
||||
|
||||
// loadUserPrefs loads notification preferences for a set of user IDs in a
|
||||
// workspace. Returns a map from user_id string to parsed preferences.
|
||||
func loadUserPrefs(
|
||||
ctx context.Context,
|
||||
queries *db.Queries,
|
||||
workspaceID string,
|
||||
userIDs []string,
|
||||
) map[string]map[string]string {
|
||||
if len(userIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
uuids := make([]pgtype.UUID, len(userIDs))
|
||||
for i, id := range userIDs {
|
||||
uuids[i] = parseUUID(id)
|
||||
}
|
||||
|
||||
rows, err := queries.ListNotificationPreferencesByUsers(ctx, db.ListNotificationPreferencesByUsersParams{
|
||||
WorkspaceID: parseUUID(workspaceID),
|
||||
UserIds: uuids,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("failed to load notification preferences", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(map[string]map[string]string, len(rows))
|
||||
for _, row := range rows {
|
||||
var prefs map[string]string
|
||||
if err := json.Unmarshal(row.Preferences, &prefs); err != nil {
|
||||
continue
|
||||
}
|
||||
result[util.UUIDToString(row.UserID)] = prefs
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// notifySubscribers queries the subscriber table for an issue, excludes the
|
||||
// actor and any extra IDs, and creates inbox items for each remaining member
|
||||
// subscriber. Publishes an inbox:new event for each notification.
|
||||
@@ -162,6 +227,15 @@ func notifyIssueSubscribers(
|
||||
return notified
|
||||
}
|
||||
|
||||
// Batch-load notification preferences for all member subscribers.
|
||||
var memberIDs []string
|
||||
for _, sub := range subs {
|
||||
if sub.UserType == "member" {
|
||||
memberIDs = append(memberIDs, util.UUIDToString(sub.UserID))
|
||||
}
|
||||
}
|
||||
userPrefs := loadUserPrefs(ctx, queries, workspaceID, memberIDs)
|
||||
|
||||
for _, sub := range subs {
|
||||
// Only notify member-type subscribers (not agents)
|
||||
if sub.UserType != "member" {
|
||||
@@ -180,6 +254,11 @@ func notifyIssueSubscribers(
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if this notification type is muted by the user
|
||||
if prefs, ok := userPrefs[subID]; ok && isNotifMuted(prefs, notifType) {
|
||||
continue
|
||||
}
|
||||
|
||||
item, err := queries.CreateInboxItem(ctx, db.CreateInboxItemParams{
|
||||
WorkspaceID: parseUUID(workspaceID),
|
||||
RecipientType: "member",
|
||||
@@ -237,6 +316,14 @@ func notifyDirect(
|
||||
return
|
||||
}
|
||||
|
||||
// Check notification preferences for member recipients.
|
||||
if recipientType == "member" {
|
||||
prefs := loadUserPrefs(ctx, queries, workspaceID, []string{recipientID})
|
||||
if p, ok := prefs[recipientID]; ok && isNotifMuted(p, notifType) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
item, err := queries.CreateInboxItem(ctx, db.CreateInboxItemParams{
|
||||
WorkspaceID: parseUUID(workspaceID),
|
||||
RecipientType: recipientType,
|
||||
@@ -308,10 +395,23 @@ func notifyMentionedMembers(
|
||||
}
|
||||
}
|
||||
|
||||
// Batch-load notification preferences for all mention recipients.
|
||||
var mentionUserIDs []string
|
||||
for id := range recipientIDs {
|
||||
if id != e.ActorID && !skip[id] {
|
||||
mentionUserIDs = append(mentionUserIDs, id)
|
||||
}
|
||||
}
|
||||
mentionPrefs := loadUserPrefs(context.Background(), queries, e.WorkspaceID, mentionUserIDs)
|
||||
|
||||
for id := range recipientIDs {
|
||||
if id == e.ActorID || skip[id] {
|
||||
continue
|
||||
}
|
||||
// Skip if mentions/comments are muted by this user
|
||||
if p, ok := mentionPrefs[id]; ok && isNotifMuted(p, "mentioned") {
|
||||
continue
|
||||
}
|
||||
item, err := queries.CreateInboxItem(context.Background(), db.CreateInboxItemParams{
|
||||
WorkspaceID: parseUUID(e.WorkspaceID),
|
||||
RecipientType: "member",
|
||||
|
||||
@@ -487,6 +487,12 @@ func NewRouterWithOptions(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus
|
||||
r.Post("/{id}/read", h.MarkInboxRead)
|
||||
r.Post("/{id}/archive", h.ArchiveInboxItem)
|
||||
})
|
||||
|
||||
// Notification preferences
|
||||
r.Route("/api/notification-preferences", func(r chi.Router) {
|
||||
r.Get("/", h.GetNotificationPreferences)
|
||||
r.Put("/", h.UpdateNotificationPreferences)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
124
server/internal/handler/notification_preference.go
Normal file
124
server/internal/handler/notification_preference.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/multica-ai/multica/server/internal/logger"
|
||||
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
||||
)
|
||||
|
||||
// validNotifGroups is the set of notification preference group keys that the
|
||||
// API accepts. Keys not in this set are rejected.
|
||||
var validNotifGroups = map[string]bool{
|
||||
"assignments": true,
|
||||
"status_changes": true,
|
||||
"comments": true,
|
||||
"updates": true,
|
||||
"agent_activity": true,
|
||||
}
|
||||
|
||||
// validNotifValues is the set of allowed preference values per group.
|
||||
var validNotifValues = map[string]bool{
|
||||
"all": true,
|
||||
"muted": true,
|
||||
}
|
||||
|
||||
func (h *Handler) GetNotificationPreferences(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := requireUserID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
workspaceID := ctxWorkspaceID(r.Context())
|
||||
|
||||
pref, err := h.Queries.GetNotificationPreference(r.Context(), db.GetNotificationPreferenceParams{
|
||||
WorkspaceID: parseUUID(workspaceID),
|
||||
UserID: parseUUID(userID),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"workspace_id": workspaceID,
|
||||
"preferences": map[string]any{},
|
||||
})
|
||||
return
|
||||
}
|
||||
slog.Warn("GetNotificationPreference failed", append(logger.RequestAttrs(r), "error", err)...)
|
||||
writeError(w, http.StatusInternalServerError, "failed to get notification preferences")
|
||||
return
|
||||
}
|
||||
|
||||
var prefs map[string]string
|
||||
if err := json.Unmarshal(pref.Preferences, &prefs); err != nil {
|
||||
prefs = map[string]string{}
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"workspace_id": workspaceID,
|
||||
"preferences": prefs,
|
||||
})
|
||||
}
|
||||
|
||||
type updateNotifPrefRequest struct {
|
||||
Preferences map[string]string `json:"preferences"`
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateNotificationPreferences(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := requireUserID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
workspaceID := ctxWorkspaceID(r.Context())
|
||||
|
||||
var req updateNotifPrefRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Preferences == nil {
|
||||
writeError(w, http.StatusBadRequest, "preferences field is required")
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range req.Preferences {
|
||||
if !validNotifGroups[k] {
|
||||
writeError(w, http.StatusBadRequest, "invalid preference group: "+k)
|
||||
return
|
||||
}
|
||||
if !validNotifValues[v] {
|
||||
writeError(w, http.StatusBadRequest, "invalid preference value: "+v)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
prefsJSON, err := json.Marshal(req.Preferences)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to marshal preferences")
|
||||
return
|
||||
}
|
||||
|
||||
pref, err := h.Queries.UpsertNotificationPreference(r.Context(), db.UpsertNotificationPreferenceParams{
|
||||
WorkspaceID: parseUUID(workspaceID),
|
||||
UserID: parseUUID(userID),
|
||||
Preferences: prefsJSON,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("UpsertNotificationPreference failed", append(logger.RequestAttrs(r), "error", err)...)
|
||||
writeError(w, http.StatusInternalServerError, "failed to update notification preferences")
|
||||
return
|
||||
}
|
||||
|
||||
var prefs map[string]string
|
||||
if err := json.Unmarshal(pref.Preferences, &prefs); err != nil {
|
||||
prefs = map[string]string{}
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"workspace_id": workspaceID,
|
||||
"preferences": prefs,
|
||||
})
|
||||
}
|
||||
1
server/migrations/064_notification_preference.down.sql
Normal file
1
server/migrations/064_notification_preference.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS notification_preference;
|
||||
8
server/migrations/064_notification_preference.up.sql
Normal file
8
server/migrations/064_notification_preference.up.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE notification_preference (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
preferences JSONB NOT NULL DEFAULT '{}',
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(workspace_id, user_id)
|
||||
);
|
||||
@@ -53,6 +53,11 @@ type DeleteDaemonTokensByWorkspaceAndDaemonParams struct {
|
||||
DaemonID string `json:"daemon_id"`
|
||||
}
|
||||
|
||||
// Callers MUST also invalidate auth.DaemonTokenCache for each affected
|
||||
// token_hash so the deletion takes effect before the cache TTL expires.
|
||||
// Today this query has no caller; when a deregister / rotate flow lands,
|
||||
// change this to :many RETURNING token_hash and call
|
||||
// DaemonTokenCache.Invalidate(hash) for each row.
|
||||
func (q *Queries) DeleteDaemonTokensByWorkspaceAndDaemon(ctx context.Context, arg DeleteDaemonTokensByWorkspaceAndDaemonParams) error {
|
||||
_, err := q.db.Exec(ctx, deleteDaemonTokensByWorkspaceAndDaemon, arg.WorkspaceID, arg.DaemonID)
|
||||
return err
|
||||
|
||||
@@ -321,6 +321,14 @@ type Member struct {
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type NotificationPreference struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Preferences []byte `json:"preferences"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type PersonalAccessToken struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
|
||||
98
server/pkg/db/generated/notification_preference.sql.go
Normal file
98
server/pkg/db/generated/notification_preference.sql.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: notification_preference.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const getNotificationPreference = `-- name: GetNotificationPreference :one
|
||||
SELECT id, workspace_id, user_id, preferences, updated_at FROM notification_preference
|
||||
WHERE workspace_id = $1 AND user_id = $2
|
||||
`
|
||||
|
||||
type GetNotificationPreferenceParams struct {
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetNotificationPreference(ctx context.Context, arg GetNotificationPreferenceParams) (NotificationPreference, error) {
|
||||
row := q.db.QueryRow(ctx, getNotificationPreference, arg.WorkspaceID, arg.UserID)
|
||||
var i NotificationPreference
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.WorkspaceID,
|
||||
&i.UserID,
|
||||
&i.Preferences,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listNotificationPreferencesByUsers = `-- name: ListNotificationPreferencesByUsers :many
|
||||
SELECT id, workspace_id, user_id, preferences, updated_at FROM notification_preference
|
||||
WHERE workspace_id = $1 AND user_id = ANY($2::uuid[])
|
||||
`
|
||||
|
||||
type ListNotificationPreferencesByUsersParams struct {
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
UserIds []pgtype.UUID `json:"user_ids"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListNotificationPreferencesByUsers(ctx context.Context, arg ListNotificationPreferencesByUsersParams) ([]NotificationPreference, error) {
|
||||
rows, err := q.db.Query(ctx, listNotificationPreferencesByUsers, arg.WorkspaceID, arg.UserIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []NotificationPreference{}
|
||||
for rows.Next() {
|
||||
var i NotificationPreference
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.WorkspaceID,
|
||||
&i.UserID,
|
||||
&i.Preferences,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const upsertNotificationPreference = `-- name: UpsertNotificationPreference :one
|
||||
INSERT INTO notification_preference (workspace_id, user_id, preferences)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (workspace_id, user_id)
|
||||
DO UPDATE SET preferences = $3, updated_at = now()
|
||||
RETURNING id, workspace_id, user_id, preferences, updated_at
|
||||
`
|
||||
|
||||
type UpsertNotificationPreferenceParams struct {
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Preferences []byte `json:"preferences"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertNotificationPreference(ctx context.Context, arg UpsertNotificationPreferenceParams) (NotificationPreference, error) {
|
||||
row := q.db.QueryRow(ctx, upsertNotificationPreference, arg.WorkspaceID, arg.UserID, arg.Preferences)
|
||||
var i NotificationPreference
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.WorkspaceID,
|
||||
&i.UserID,
|
||||
&i.Preferences,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
14
server/pkg/db/queries/notification_preference.sql
Normal file
14
server/pkg/db/queries/notification_preference.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- name: GetNotificationPreference :one
|
||||
SELECT * FROM notification_preference
|
||||
WHERE workspace_id = $1 AND user_id = $2;
|
||||
|
||||
-- name: UpsertNotificationPreference :one
|
||||
INSERT INTO notification_preference (workspace_id, user_id, preferences)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (workspace_id, user_id)
|
||||
DO UPDATE SET preferences = $3, updated_at = now()
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListNotificationPreferencesByUsers :many
|
||||
SELECT * FROM notification_preference
|
||||
WHERE workspace_id = $1 AND user_id = ANY(sqlc.arg('user_ids')::uuid[]);
|
||||
Reference in New Issue
Block a user