refactor: remove legacy spaces and migrate lists to standalone resources with direct sharing
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
This commit is contained in:
1 parent
177b69234c
commit
0d6c603e9c
9 files changed
+616
-950
No files matched your search
+233
-417
@@ -137,28 +137,6 @@ func (s *Store) migrate() error {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_lists_user ON task_lists(user_id, updated_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spaces (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
icon TEXT NOT NULL DEFAULT '🏠',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
invite_code TEXT UNIQUE NOT NULL,
|
||||
is_personal INTEGER NOT NULL DEFAULT 0,
|
||||
owner_id TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(owner_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS space_members (
|
||||
space_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'editor',
|
||||
joined_at TEXT NOT NULL,
|
||||
PRIMARY KEY (space_id, user_id),
|
||||
FOREIGN KEY(space_id) REFERENCES spaces(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS organizations (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
@@ -259,16 +237,32 @@ func (s *Store) migrate() error {
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lists (
|
||||
id TEXT PRIMARY KEY,
|
||||
space_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
icon TEXT NOT NULL DEFAULT '🛒',
|
||||
list_type TEXT NOT NULL DEFAULT 'shopping',
|
||||
updated_at TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(space_id) REFERENCES spaces(id) ON DELETE CASCADE
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_lists_space ON lists(space_id, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_lists_user ON lists(user_id, updated_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS list_shares (
|
||||
id TEXT PRIMARY KEY,
|
||||
list_id TEXT NOT NULL,
|
||||
target_type TEXT NOT NULL CHECK(target_type IN ('team', 'user')),
|
||||
target_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'editor' CHECK(role IN ('editor', 'viewer')),
|
||||
created_by TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE(list_id, target_type, target_id),
|
||||
FOREIGN KEY(list_id) REFERENCES lists(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_list_shares_target ON list_shares(target_type, target_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_list_shares_list ON list_shares(list_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS list_items (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -342,14 +336,20 @@ func (s *Store) migrate() error {
|
||||
"CREATE INDEX IF NOT EXISTS idx_doc_team_shares_team ON doc_team_shares(team_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_doc_team_shares_doc ON doc_team_shares(doc_id);",
|
||||
|
||||
"CREATE TABLE IF NOT EXISTS space_team_shares (id TEXT PRIMARY KEY, space_id TEXT NOT NULL, team_id TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'editor', created_at TEXT NOT NULL, created_by TEXT NOT NULL, UNIQUE(space_id, team_id));",
|
||||
"CREATE INDEX IF NOT EXISTS idx_space_team_shares_team ON space_team_shares(team_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_space_team_shares_space ON space_team_shares(space_id);",
|
||||
"ALTER TABLE lists ADD COLUMN user_id TEXT NOT NULL DEFAULT '';",
|
||||
"CREATE TABLE IF NOT EXISTS list_shares (id TEXT PRIMARY KEY, list_id TEXT NOT NULL, target_type TEXT NOT NULL CHECK(target_type IN ('team', 'user')), target_id TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'editor' CHECK(role IN ('editor', 'viewer')), created_at TEXT NOT NULL, created_by TEXT NOT NULL, UNIQUE(list_id, target_type, target_id));",
|
||||
"CREATE INDEX IF NOT EXISTS idx_list_shares_target ON list_shares(target_type, target_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_list_shares_list ON list_shares(list_id);",
|
||||
}
|
||||
for _, q := range columnMigrations {
|
||||
_, _ = s.db.Exec(q)
|
||||
}
|
||||
|
||||
// Clean up legacy space_id from documents and task_lists if empty or orphaned
|
||||
_, _ = s.db.Exec(`DROP TABLE IF EXISTS spaces`)
|
||||
_, _ = s.db.Exec(`DROP TABLE IF EXISTS space_members`)
|
||||
_, _ = s.db.Exec(`DROP TABLE IF EXISTS space_team_shares`)
|
||||
|
||||
// Backfill username for existing accounts where username is empty
|
||||
_, _ = s.db.Exec(`UPDATE users SET username = LOWER(SUBSTR(email, 1, INSTR(email, '@') - 1)) WHERE (username IS NULL OR username = '') AND INSTR(email, '@') > 0`)
|
||||
|
||||
@@ -442,13 +442,10 @@ func (s *Store) SeedStarterContent(user *domain.User) error {
|
||||
_, _ = s.UpdateThoughtTriage(user.ID, keptTask.ID, "keep", "task", "", now[:10], "")
|
||||
}
|
||||
|
||||
familySpace, err := s.CreateSpace(user.ID, "Familie & Zuhause", "🏡", "Geteilter Bereich für Einkäufe, Haushalt & Urlaubsplanung", false)
|
||||
if err == nil && familySpace != nil {
|
||||
shopList, err := s.CreateList(familySpace.ID, "Wocheneinkauf Supermarkt", "🛒", "shopping")
|
||||
if err == nil && shopList != nil {
|
||||
_, _ = s.CreateListItem(shopList.ID, "Hafermilch (Barista Edition)", "Kühlregal", "3x", 0)
|
||||
_, _ = s.CreateListItem(shopList.ID, "Espressobohnen (100% Arabica)", "Kaffee & Tee", "1 kg", 1)
|
||||
}
|
||||
shopList, err := s.CreateList(user.ID, "Wocheneinkauf Supermarkt", "🛒", "shopping")
|
||||
if err == nil && shopList != nil {
|
||||
_, _ = s.CreateListItem(shopList.ID, "Hafermilch (Barista Edition)", "Kühlregal", "3x", 0)
|
||||
_, _ = s.CreateListItem(shopList.ID, "Espressobohnen (100% Arabica)", "Kaffee & Tee", "1 kg", 1)
|
||||
}
|
||||
|
||||
starterBlocks := []domain.DocumentBlock{
|
||||
@@ -692,12 +689,8 @@ func (s *Store) GetDiscoverableUsers(viewerID string) ([]domain.User, error) {
|
||||
SELECT tm2.user_id FROM team_members tm1
|
||||
JOIN team_members tm2 ON tm1.team_id = tm2.team_id
|
||||
WHERE tm1.user_id = ?
|
||||
UNION
|
||||
SELECT sm2.user_id FROM space_members sm1
|
||||
JOIN space_members sm2 ON sm1.space_id = sm2.space_id
|
||||
WHERE sm1.user_id = ?
|
||||
)`,
|
||||
viewerID, viewerID, viewerID,
|
||||
viewerID, viewerID,
|
||||
)
|
||||
if err != nil {
|
||||
return []domain.User{*viewer}, nil
|
||||
@@ -879,15 +872,14 @@ func (s *Store) accessibleTaskListIDsQuery() string {
|
||||
SELECT task_list_id FROM task_list_shares
|
||||
WHERE (target_type = 'user' AND target_id = ?)
|
||||
OR (target_type = 'team' AND target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
||||
)
|
||||
OR (space_id != '' AND space_id IN (SELECT space_id FROM space_members WHERE user_id = ?))`
|
||||
)`
|
||||
}
|
||||
|
||||
func (s *Store) GetThoughtByID(userID, thoughtID string) (*domain.Thought, error) {
|
||||
row := s.db.QueryRow(
|
||||
`SELECT id, user_id, content, tags, priority, source, status, is_completed, converted_type, converted_ref_id, due_date, task_list_id, kanban_status, notes, alarm_sent, created_at, triaged_at, subtasks
|
||||
FROM thoughts WHERE id = ? AND (user_id = ? OR task_list_id IN (`+s.accessibleTaskListIDsQuery()+`))`,
|
||||
thoughtID, userID, userID, userID, userID, userID, userID,
|
||||
thoughtID, userID, userID, userID, userID, userID,
|
||||
)
|
||||
return scanThought(row)
|
||||
}
|
||||
@@ -940,7 +932,7 @@ func (s *Store) GetTasks(userID string, taskListID string) ([]domain.Thought, er
|
||||
AND task_list_id = ?
|
||||
AND (status = 'kept' OR (status = 'converted' AND converted_type = 'task'))
|
||||
ORDER BY is_completed ASC, CASE WHEN due_date = '' THEN 1 ELSE 0 END, due_date ASC, created_at DESC`,
|
||||
userID, userID, userID, userID, userID, userID, taskListID,
|
||||
userID, userID, userID, userID, userID, taskListID,
|
||||
)
|
||||
} else {
|
||||
rows, err = s.db.Query(
|
||||
@@ -949,7 +941,7 @@ func (s *Store) GetTasks(userID string, taskListID string) ([]domain.Thought, er
|
||||
WHERE (user_id = ? OR task_list_id IN (`+s.accessibleTaskListIDsQuery()+`))
|
||||
AND (status = 'kept' OR (status = 'converted' AND converted_type = 'task'))
|
||||
ORDER BY is_completed ASC, CASE WHEN due_date = '' THEN 1 ELSE 0 END, due_date ASC, created_at DESC`,
|
||||
userID, userID, userID, userID, userID, userID,
|
||||
userID, userID, userID, userID, userID,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -2190,339 +2182,9 @@ func (s *Store) GetCollaborators(currentUserID string) ([]domain.User, error) {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// --- SPACES & REALTIME LISTS ---
|
||||
// --- STANDALONE REALTIME LISTS & SHARING ---
|
||||
|
||||
func (s *Store) CreateSpace(ownerID, name, icon, description string, isPersonal bool) (*domain.Space, error) {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return nil, errors.New("space name required")
|
||||
}
|
||||
if icon == "" {
|
||||
icon = "🏠"
|
||||
}
|
||||
now := domain.NowISO()
|
||||
inviteCode := strings.ToUpper(domain.NewID("inv")[4:12])
|
||||
personalInt := 0
|
||||
if isPersonal {
|
||||
personalInt = 1
|
||||
}
|
||||
|
||||
sp := &domain.Space{
|
||||
ID: domain.NewID("spc"),
|
||||
Name: strings.TrimSpace(name),
|
||||
Icon: icon,
|
||||
Description: strings.TrimSpace(description),
|
||||
InviteCode: inviteCode,
|
||||
IsPersonal: isPersonal,
|
||||
OwnerID: ownerID,
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO spaces (id, name, icon, description, invite_code, is_personal, owner_id, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
sp.ID, sp.Name, sp.Icon, sp.Description, sp.InviteCode, personalInt, sp.OwnerID, sp.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, _ = s.db.Exec(
|
||||
`INSERT INTO space_members (space_id, user_id, role, joined_at) VALUES (?, ?, 'owner', ?)`,
|
||||
sp.ID, ownerID, now,
|
||||
)
|
||||
return s.GetSpaceByID(sp.ID)
|
||||
}
|
||||
|
||||
func (s *Store) JoinSpaceByInviteCode(userID, inviteCode string) (*domain.Space, error) {
|
||||
inviteCode = strings.ToUpper(strings.TrimSpace(inviteCode))
|
||||
var spaceID string
|
||||
err := s.db.QueryRow(`SELECT id FROM spaces WHERE invite_code = ?`, inviteCode).Scan(&spaceID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Ungültiger Einladungscode")
|
||||
}
|
||||
|
||||
_, err = s.db.Exec(
|
||||
`INSERT OR IGNORE INTO space_members (space_id, user_id, role, joined_at) VALUES (?, ?, 'editor', ?)`,
|
||||
spaceID, userID, domain.NowISO(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.GetSpaceByID(spaceID)
|
||||
}
|
||||
|
||||
func (s *Store) GetSpaceByID(spaceID string) (*domain.Space, error) {
|
||||
sp := &domain.Space{}
|
||||
var personalInt int
|
||||
err := s.db.QueryRow(
|
||||
`SELECT id, name, icon, description, invite_code, is_personal, owner_id, created_at FROM spaces WHERE id = ?`,
|
||||
spaceID,
|
||||
).Scan(&sp.ID, &sp.Name, &sp.Icon, &sp.Description, &sp.InviteCode, &personalInt, &sp.OwnerID, &sp.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sp.IsPersonal = personalInt != 0
|
||||
sp.Members, _ = s.GetSpaceMembers(spaceID)
|
||||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM lists WHERE space_id = ?`, spaceID).Scan(&sp.ListsCount)
|
||||
return sp, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSpaceMembers(spaceID string) ([]domain.SpaceMember, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT sm.space_id, sm.user_id, u.display_name, u.email, sm.role, sm.joined_at
|
||||
FROM space_members sm
|
||||
JOIN users u ON u.id = sm.user_id
|
||||
WHERE sm.space_id = ?
|
||||
ORDER BY sm.joined_at ASC`,
|
||||
spaceID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
members := make([]domain.SpaceMember, 0)
|
||||
for rows.Next() {
|
||||
var m domain.SpaceMember
|
||||
if err := rows.Scan(&m.SpaceID, &m.UserID, &m.DisplayName, &m.Email, &m.Role, &m.JoinedAt); err == nil {
|
||||
members = append(members, m)
|
||||
}
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (s *Store) AddSpaceMember(currentUserID, spaceID, targetUserIdentifier, role string) (*domain.SpaceMember, error) {
|
||||
// 1. Check if current user is member of space
|
||||
var myRole string
|
||||
err := s.db.QueryRow(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, currentUserID).Scan(&myRole)
|
||||
if err != nil {
|
||||
return nil, errors.New("keine Berechtigung für diesen Bereich")
|
||||
}
|
||||
|
||||
// 2. Resolve target user by ID, username or email
|
||||
cleanTarget := strings.TrimSpace(targetUserIdentifier)
|
||||
var targetUser *domain.User
|
||||
if strings.HasPrefix(cleanTarget, "usr_") {
|
||||
targetUser, _ = s.GetUserByID(cleanTarget)
|
||||
}
|
||||
if targetUser == nil {
|
||||
targetUser, _ = s.GetUserByLogin(cleanTarget)
|
||||
}
|
||||
if targetUser == nil {
|
||||
targetUser, err = s.GetUserByID(cleanTarget)
|
||||
}
|
||||
if err != nil || targetUser == nil {
|
||||
return nil, errors.New("benutzer nicht gefunden")
|
||||
}
|
||||
|
||||
// Protect existing owner from role downgrade!
|
||||
var existingRole string
|
||||
_ = s.db.QueryRow(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, targetUser.ID).Scan(&existingRole)
|
||||
if existingRole == "owner" {
|
||||
role = "owner"
|
||||
} else if role != "owner" && role != "editor" && role != "viewer" {
|
||||
role = "editor"
|
||||
}
|
||||
|
||||
now := domain.NowISO()
|
||||
_, err = s.db.Exec(
|
||||
`INSERT INTO space_members (space_id, user_id, role, joined_at) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(space_id, user_id) DO UPDATE SET role = excluded.role`,
|
||||
spaceID, targetUser.ID, role, now,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &domain.SpaceMember{
|
||||
SpaceID: spaceID,
|
||||
UserID: targetUser.ID,
|
||||
DisplayName: targetUser.DisplayName,
|
||||
Email: targetUser.Email,
|
||||
Role: role,
|
||||
JoinedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateSpaceTeamShare(currentUserID, spaceID, teamID, role string) (*domain.SpaceShare, error) {
|
||||
var myRole string
|
||||
err := s.db.QueryRow(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, currentUserID).Scan(&myRole)
|
||||
if err != nil || (myRole != "owner" && myRole != "editor") {
|
||||
return nil, errors.New("keine Berechtigung für diesen Bereich")
|
||||
}
|
||||
|
||||
cleanTeamID := strings.TrimSpace(teamID)
|
||||
var teamName string
|
||||
err = s.db.QueryRow(`SELECT name FROM teams WHERE id = ?`, cleanTeamID).Scan(&teamName)
|
||||
if err != nil {
|
||||
return nil, errors.New("team nicht gefunden")
|
||||
}
|
||||
|
||||
if role != "owner" && role != "editor" && role != "viewer" {
|
||||
role = "editor"
|
||||
}
|
||||
|
||||
now := domain.NowISO()
|
||||
shareID := domain.NewID("spshare")
|
||||
_, err = s.db.Exec(
|
||||
`INSERT INTO space_team_shares (id, space_id, team_id, role, created_at, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(space_id, team_id) DO UPDATE SET role = excluded.role`,
|
||||
shareID, spaceID, cleanTeamID, role, now, currentUserID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &domain.SpaceShare{
|
||||
ID: shareID,
|
||||
SpaceID: spaceID,
|
||||
TargetType: "team",
|
||||
TargetID: cleanTeamID,
|
||||
TargetName: teamName,
|
||||
Role: role,
|
||||
CreatedBy: currentUserID,
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSpaceShares(spaceID string) ([]domain.SpaceShare, error) {
|
||||
shares := make([]domain.SpaceShare, 0)
|
||||
|
||||
// 1. Team shares
|
||||
tRows, err := s.db.Query(
|
||||
`SELECT s.id, s.space_id, s.team_id, t.name, s.role, s.created_by, s.created_at
|
||||
FROM space_team_shares s
|
||||
JOIN teams t ON s.team_id = t.id
|
||||
WHERE s.space_id = ?
|
||||
ORDER BY s.created_at ASC`,
|
||||
spaceID,
|
||||
)
|
||||
if err == nil {
|
||||
for tRows.Next() {
|
||||
var sh domain.SpaceShare
|
||||
if err := tRows.Scan(&sh.ID, &sh.SpaceID, &sh.TargetID, &sh.TargetName, &sh.Role, &sh.CreatedBy, &sh.CreatedAt); err == nil {
|
||||
sh.TargetType = "team"
|
||||
shares = append(shares, sh)
|
||||
}
|
||||
}
|
||||
_ = tRows.Close()
|
||||
}
|
||||
|
||||
// 2. Individual invited members (non-owner members)
|
||||
var ownerID string
|
||||
_ = s.db.QueryRow(`SELECT owner_id FROM spaces WHERE id = ?`, spaceID).Scan(&ownerID)
|
||||
|
||||
mRows, err := s.db.Query(
|
||||
`SELECT sm.user_id, u.display_name, u.email, sm.role, sm.joined_at
|
||||
FROM space_members sm
|
||||
JOIN users u ON u.id = sm.user_id
|
||||
WHERE sm.space_id = ? AND sm.user_id != ?
|
||||
ORDER BY sm.joined_at ASC`,
|
||||
spaceID, ownerID,
|
||||
)
|
||||
if err == nil {
|
||||
for mRows.Next() {
|
||||
var uid, dName, email, role, joinedAt string
|
||||
if err := mRows.Scan(&uid, &dName, &email, &role, &joinedAt); err == nil {
|
||||
name := dName
|
||||
if name == "" {
|
||||
name = email
|
||||
}
|
||||
shares = append(shares, domain.SpaceShare{
|
||||
ID: "sm_" + uid,
|
||||
SpaceID: spaceID,
|
||||
TargetType: "user",
|
||||
TargetID: uid,
|
||||
TargetName: name,
|
||||
Role: role,
|
||||
CreatedAt: joinedAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
_ = mRows.Close()
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func (s *Store) RemoveSpaceTeamShare(currentUserID, spaceID, targetID string) error {
|
||||
var myRole string
|
||||
err := s.db.QueryRow(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, currentUserID).Scan(&myRole)
|
||||
if err != nil || (myRole != "owner" && myRole != "editor") {
|
||||
return errors.New("keine Berechtigung zum Entfernen der Freigabe")
|
||||
}
|
||||
|
||||
cleanTarget := strings.TrimSpace(targetID)
|
||||
_, err = s.db.Exec(`DELETE FROM space_team_shares WHERE space_id = ? AND (id = ? OR team_id = ?)`, spaceID, cleanTarget, cleanTarget)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) RemoveSpaceMember(currentUserID, spaceID, targetUserID string) error {
|
||||
var myRole string
|
||||
err := s.db.QueryRow(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, currentUserID).Scan(&myRole)
|
||||
if err != nil {
|
||||
return errors.New("keine Berechtigung für diesen Bereich")
|
||||
}
|
||||
|
||||
cleanTarget := strings.TrimSpace(targetUserID)
|
||||
if strings.HasPrefix(cleanTarget, "team_") || strings.HasPrefix(cleanTarget, "spshare_") {
|
||||
return s.RemoveSpaceTeamShare(currentUserID, spaceID, cleanTarget)
|
||||
}
|
||||
|
||||
if currentUserID != cleanTarget && myRole != "owner" {
|
||||
return errors.New("nur der Eigentümer kann andere Mitglieder entfernen")
|
||||
}
|
||||
|
||||
var targetRole string
|
||||
_ = s.db.QueryRow(`SELECT role FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, cleanTarget).Scan(&targetRole)
|
||||
if targetRole == "owner" {
|
||||
var ownerCount int
|
||||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM space_members WHERE space_id = ? AND role = 'owner'`, spaceID).Scan(&ownerCount)
|
||||
if ownerCount <= 1 {
|
||||
return errors.New("der letzte Eigentümer kann den Bereich nicht verlassen")
|
||||
}
|
||||
}
|
||||
|
||||
_, err = s.db.Exec(`DELETE FROM space_members WHERE space_id = ? AND user_id = ?`, spaceID, cleanTarget)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) GetSpacesForUser(userID string) ([]domain.Space, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT s.id, s.name, s.icon, s.description, s.invite_code, s.is_personal, s.owner_id, s.created_at
|
||||
FROM spaces s
|
||||
LEFT JOIN space_members sm ON sm.space_id = s.id AND sm.user_id = ?
|
||||
LEFT JOIN space_team_shares sts ON sts.space_id = s.id AND sts.team_id IN (SELECT team_id FROM team_members WHERE user_id = ?)
|
||||
WHERE sm.user_id = ? OR sts.team_id IS NOT NULL
|
||||
GROUP BY s.id
|
||||
ORDER BY s.created_at ASC`,
|
||||
userID, userID, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spaces := make([]domain.Space, 0)
|
||||
for rows.Next() {
|
||||
var sp domain.Space
|
||||
var personalInt int
|
||||
if err := rows.Scan(&sp.ID, &sp.Name, &sp.Icon, &sp.Description, &sp.InviteCode, &personalInt, &sp.OwnerID, &sp.CreatedAt); err == nil {
|
||||
sp.IsPersonal = personalInt != 0
|
||||
spaces = append(spaces, sp)
|
||||
}
|
||||
}
|
||||
_ = rows.Close()
|
||||
|
||||
for i := range spaces {
|
||||
spaces[i].Members, _ = s.GetSpaceMembers(spaces[i].ID)
|
||||
spaces[i].Shares, _ = s.GetSpaceShares(spaces[i].ID)
|
||||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM lists WHERE space_id = ?`, spaces[i].ID).Scan(&spaces[i].ListsCount)
|
||||
}
|
||||
return spaces, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateList(spaceID, title, icon, listType string) (*domain.SharedList, error) {
|
||||
func (s *Store) CreateList(userID, title, icon, listType string) (*domain.SharedList, error) {
|
||||
if strings.TrimSpace(title) == "" {
|
||||
return nil, errors.New("list title required")
|
||||
}
|
||||
@@ -2535,59 +2197,167 @@ func (s *Store) CreateList(spaceID, title, icon, listType string) (*domain.Share
|
||||
now := domain.NowISO()
|
||||
lst := &domain.SharedList{
|
||||
ID: domain.NewID("lst"),
|
||||
SpaceID: spaceID,
|
||||
UserID: userID,
|
||||
IsOwner: true,
|
||||
Role: "editor",
|
||||
Title: strings.TrimSpace(title),
|
||||
Icon: icon,
|
||||
ListType: listType,
|
||||
Items: []domain.ListItem{},
|
||||
Shares: []domain.ListShare{},
|
||||
UpdatedAt: now,
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO lists (id, space_id, title, icon, list_type, updated_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
lst.ID, lst.SpaceID, lst.Title, lst.Icon, lst.ListType, lst.UpdatedAt, lst.CreatedAt,
|
||||
`INSERT INTO lists (id, user_id, title, icon, list_type, updated_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
lst.ID, lst.UserID, lst.Title, lst.Icon, lst.ListType, lst.UpdatedAt, lst.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = s.db.QueryRow(`SELECT display_name FROM users WHERE id = ?`, userID).Scan(&lst.OwnerName)
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetListsForSpace(spaceID string) ([]domain.SharedList, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, space_id, title, icon, list_type, updated_at, created_at
|
||||
FROM lists WHERE space_id = ? ORDER BY created_at ASC`,
|
||||
spaceID,
|
||||
func (s *Store) AddListShare(currentUserID, listID, targetType, targetID, role string) (*domain.ListShare, error) {
|
||||
if targetType != "team" && targetType != "user" {
|
||||
return nil, errors.New("ungültiger Freigabetyp")
|
||||
}
|
||||
targetID = strings.TrimSpace(targetID)
|
||||
if targetID == "" {
|
||||
return nil, errors.New("Ziel-ID erforderlich")
|
||||
}
|
||||
if role != "viewer" && role != "editor" {
|
||||
role = "editor"
|
||||
}
|
||||
|
||||
if targetType == "user" {
|
||||
var targetUser *domain.User
|
||||
if strings.HasPrefix(targetID, "usr_") {
|
||||
targetUser, _ = s.GetUserByID(targetID)
|
||||
}
|
||||
if targetUser == nil {
|
||||
targetUser, _ = s.GetUserByLogin(targetID)
|
||||
}
|
||||
if targetUser == nil {
|
||||
targetUser, _ = s.GetUserByEmail(targetID)
|
||||
}
|
||||
if targetUser == nil {
|
||||
return nil, fmt.Errorf("benutzer nicht gefunden: %s", targetID)
|
||||
}
|
||||
targetID = targetUser.ID
|
||||
}
|
||||
|
||||
// Verify current user is owner or editor of the list
|
||||
var ownerID string
|
||||
err := s.db.QueryRow(`SELECT user_id FROM lists WHERE id = ?`, listID).Scan(&ownerID)
|
||||
if err != nil {
|
||||
return nil, errors.New("liste nicht gefunden")
|
||||
}
|
||||
if ownerID != currentUserID {
|
||||
var myRole string
|
||||
err = s.db.QueryRow(`SELECT role FROM list_shares WHERE list_id = ? AND target_type = 'user' AND target_id = ?`, listID, currentUserID).Scan(&myRole)
|
||||
if err != nil || myRole != "editor" {
|
||||
return nil, errors.New("keine Berechtigung zum Teilen dieser Liste")
|
||||
}
|
||||
}
|
||||
|
||||
now := domain.NowISO()
|
||||
share := &domain.ListShare{
|
||||
ID: domain.NewID("lsh"),
|
||||
ListID: listID,
|
||||
TargetType: targetType,
|
||||
TargetID: targetID,
|
||||
Role: role,
|
||||
CreatedBy: currentUserID,
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
_, err = s.db.Exec(
|
||||
`INSERT INTO list_shares (id, list_id, target_type, target_id, role, created_by, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(list_id, target_type, target_id) DO UPDATE SET role = excluded.role`,
|
||||
share.ID, share.ListID, share.TargetType, share.TargetID, share.Role, share.CreatedBy, share.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lists := make([]domain.SharedList, 0)
|
||||
_ = s.db.QueryRow(`SELECT id, created_at FROM list_shares WHERE list_id = ? AND target_type = ? AND target_id = ?`,
|
||||
listID, targetType, targetID).Scan(&share.ID, &share.CreatedAt)
|
||||
|
||||
if targetType == "team" {
|
||||
_ = s.db.QueryRow(`SELECT name FROM teams WHERE id = ?`, targetID).Scan(&share.TargetName)
|
||||
} else {
|
||||
_ = s.db.QueryRow(`SELECT display_name, email FROM users WHERE id = ?`, targetID).Scan(&share.TargetName, &share.Email)
|
||||
}
|
||||
return share, nil
|
||||
}
|
||||
|
||||
func (s *Store) RemoveListShare(currentUserID, listID, shareIDOrTargetID string) error {
|
||||
var ownerID string
|
||||
err := s.db.QueryRow(`SELECT user_id FROM lists WHERE id = ?`, listID).Scan(&ownerID)
|
||||
if err != nil {
|
||||
return errors.New("liste nicht gefunden")
|
||||
}
|
||||
if ownerID != currentUserID {
|
||||
return errors.New("nur der Eigentümer kann Freigaben entfernen")
|
||||
}
|
||||
|
||||
cleanTarget := strings.TrimSpace(shareIDOrTargetID)
|
||||
_, err = s.db.Exec(
|
||||
`DELETE FROM list_shares WHERE list_id = ? AND (id = ? OR target_id = ?)`,
|
||||
listID, cleanTarget, cleanTarget,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) GetListShares(listID string) ([]domain.ListShare, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, list_id, target_type, target_id, role, created_by, created_at
|
||||
FROM list_shares WHERE list_id = ? ORDER BY created_at ASC`,
|
||||
listID,
|
||||
)
|
||||
if err != nil {
|
||||
return []domain.ListShare{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
shares := make([]domain.ListShare, 0)
|
||||
for rows.Next() {
|
||||
var l domain.SharedList
|
||||
if err := rows.Scan(&l.ID, &l.SpaceID, &l.Title, &l.Icon, &l.ListType, &l.UpdatedAt, &l.CreatedAt); err == nil {
|
||||
lists = append(lists, l)
|
||||
var sh domain.ListShare
|
||||
if err := rows.Scan(&sh.ID, &sh.ListID, &sh.TargetType, &sh.TargetID, &sh.Role, &sh.CreatedBy, &sh.CreatedAt); err == nil {
|
||||
if sh.TargetType == "team" {
|
||||
_ = s.db.QueryRow(`SELECT name FROM teams WHERE id = ?`, sh.TargetID).Scan(&sh.TargetName)
|
||||
} else {
|
||||
_ = s.db.QueryRow(`SELECT display_name, email FROM users WHERE id = ?`, sh.TargetID).Scan(&sh.TargetName, &sh.Email)
|
||||
}
|
||||
shares = append(shares, sh)
|
||||
}
|
||||
}
|
||||
_ = rows.Close()
|
||||
|
||||
for i := range lists {
|
||||
lists[i].Items, _ = s.GetListItems(lists[i].ID)
|
||||
}
|
||||
return lists, nil
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetAllListsForUser(userID string) ([]domain.SharedList, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT DISTINCT l.id, l.space_id, l.title, l.icon, l.list_type, l.updated_at, l.created_at
|
||||
`SELECT DISTINCT l.id, l.user_id, l.title, l.icon, l.list_type, l.updated_at, l.created_at,
|
||||
u.display_name,
|
||||
CASE
|
||||
WHEN l.user_id = ? THEN 'owner'
|
||||
WHEN ls.role IS NOT NULL THEN ls.role
|
||||
ELSE 'editor'
|
||||
END as effective_role
|
||||
FROM lists l
|
||||
LEFT JOIN space_members sm ON sm.space_id = l.space_id AND sm.user_id = ?
|
||||
LEFT JOIN space_team_shares sts ON sts.space_id = l.space_id AND sts.team_id IN (SELECT team_id FROM team_members WHERE user_id = ?)
|
||||
WHERE sm.user_id = ? OR sts.team_id IS NOT NULL
|
||||
LEFT JOIN users u ON u.id = l.user_id
|
||||
LEFT JOIN list_shares ls ON ls.list_id = l.id AND (
|
||||
(ls.target_type = 'user' AND ls.target_id = ?)
|
||||
OR (ls.target_type = 'team' AND ls.target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
||||
)
|
||||
WHERE l.user_id = ?
|
||||
OR ls.id IS NOT NULL
|
||||
ORDER BY l.updated_at DESC`,
|
||||
userID, userID, userID,
|
||||
userID, userID, userID, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -2596,7 +2366,12 @@ func (s *Store) GetAllListsForUser(userID string) ([]domain.SharedList, error) {
|
||||
lists := make([]domain.SharedList, 0)
|
||||
for rows.Next() {
|
||||
var l domain.SharedList
|
||||
if err := rows.Scan(&l.ID, &l.SpaceID, &l.Title, &l.Icon, &l.ListType, &l.UpdatedAt, &l.CreatedAt); err == nil {
|
||||
var ownerName sql.NullString
|
||||
var effectiveRole string
|
||||
if err := rows.Scan(&l.ID, &l.UserID, &l.Title, &l.Icon, &l.ListType, &l.UpdatedAt, &l.CreatedAt, &ownerName, &effectiveRole); err == nil {
|
||||
l.OwnerName = ownerName.String
|
||||
l.IsOwner = (l.UserID == userID)
|
||||
l.Role = effectiveRole
|
||||
lists = append(lists, l)
|
||||
}
|
||||
}
|
||||
@@ -2604,25 +2379,68 @@ func (s *Store) GetAllListsForUser(userID string) ([]domain.SharedList, error) {
|
||||
|
||||
for i := range lists {
|
||||
lists[i].Items, _ = s.GetListItems(lists[i].ID)
|
||||
lists[i].Shares, _ = s.GetListShares(lists[i].ID)
|
||||
}
|
||||
return lists, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetListByID(listID string) (*domain.SharedList, error) {
|
||||
func (s *Store) GetListByID(userID, listID string) (*domain.SharedList, error) {
|
||||
var l domain.SharedList
|
||||
err := s.db.QueryRow(
|
||||
`SELECT id, space_id, title, icon, list_type, updated_at, created_at FROM lists WHERE id = ?`,
|
||||
listID,
|
||||
).Scan(&l.ID, &l.SpaceID, &l.Title, &l.Icon, &l.ListType, &l.UpdatedAt, &l.CreatedAt)
|
||||
var ownerName sql.NullString
|
||||
var effectiveRole string
|
||||
|
||||
var query string
|
||||
var args []interface{}
|
||||
if userID == "" {
|
||||
query = `SELECT l.id, l.user_id, l.title, l.icon, l.list_type, l.updated_at, l.created_at,
|
||||
u.display_name, 'owner' as effective_role
|
||||
FROM lists l
|
||||
LEFT JOIN users u ON u.id = l.user_id
|
||||
WHERE l.id = ?`
|
||||
args = []interface{}{listID}
|
||||
} else {
|
||||
query = `SELECT l.id, l.user_id, l.title, l.icon, l.list_type, l.updated_at, l.created_at,
|
||||
u.display_name,
|
||||
CASE
|
||||
WHEN l.user_id = ? THEN 'owner'
|
||||
WHEN ls.role IS NOT NULL THEN ls.role
|
||||
ELSE 'editor'
|
||||
END as effective_role
|
||||
FROM lists l
|
||||
LEFT JOIN users u ON u.id = l.user_id
|
||||
LEFT JOIN list_shares ls ON ls.list_id = l.id AND (
|
||||
(ls.target_type = 'user' AND ls.target_id = ?)
|
||||
OR (ls.target_type = 'team' AND ls.target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
||||
)
|
||||
WHERE l.id = ? AND (
|
||||
l.user_id = ?
|
||||
OR ls.id IS NOT NULL
|
||||
)`
|
||||
args = []interface{}{userID, userID, userID, listID, userID}
|
||||
}
|
||||
|
||||
err := s.db.QueryRow(query, args...).Scan(&l.ID, &l.UserID, &l.Title, &l.Icon, &l.ListType, &l.UpdatedAt, &l.CreatedAt, &ownerName, &effectiveRole)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.OwnerName = ownerName.String
|
||||
l.IsOwner = (userID == "" || l.UserID == userID)
|
||||
l.Role = effectiveRole
|
||||
l.Items, _ = s.GetListItems(l.ID)
|
||||
l.Shares, _ = s.GetListShares(l.ID)
|
||||
return &l, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteList(listID string) error {
|
||||
_, err := s.db.Exec(`DELETE FROM lists WHERE id = ?`, listID)
|
||||
func (s *Store) DeleteList(userID, listID string) error {
|
||||
var ownerID string
|
||||
err := s.db.QueryRow(`SELECT user_id FROM lists WHERE id = ?`, listID).Scan(&ownerID)
|
||||
if err != nil {
|
||||
return errors.New("liste nicht gefunden")
|
||||
}
|
||||
if ownerID != userID {
|
||||
return errors.New("nur der Eigentümer kann die Liste löschen")
|
||||
}
|
||||
_, err = s.db.Exec(`DELETE FROM lists WHERE id = ?`, listID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2829,9 +2647,11 @@ func (s *Store) GetDashboardStats(userID string) (*domain.DashboardStats, error)
|
||||
_ = s.db.QueryRow(`
|
||||
SELECT COUNT(DISTINCT l.id)
|
||||
FROM lists l
|
||||
LEFT JOIN space_members sm ON sm.space_id = l.space_id AND sm.user_id = ?
|
||||
LEFT JOIN space_team_shares sts ON sts.space_id = l.space_id AND sts.team_id IN (SELECT team_id FROM team_members WHERE user_id = ?)
|
||||
WHERE sm.user_id = ? OR sts.team_id IS NOT NULL`,
|
||||
LEFT JOIN list_shares ls ON ls.list_id = l.id AND (
|
||||
(ls.target_type = 'user' AND ls.target_id = ?)
|
||||
OR (ls.target_type = 'team' AND ls.target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
||||
)
|
||||
WHERE l.user_id = ? OR ls.id IS NOT NULL`,
|
||||
userID, userID, userID).Scan(&st.ListsCount)
|
||||
|
||||
// Count open (uncompleted) To-Dos across accessible workspace documents (owned or shared)
|
||||
@@ -2908,7 +2728,6 @@ func (s *Store) ExportUserData(userID string) (*domain.FullExport, error) {
|
||||
}
|
||||
thoughts, _ := s.GetThoughts(userID, "all", 5000)
|
||||
docs, _ := s.GetDocuments(userID)
|
||||
spaces, _ := s.GetSpacesForUser(userID)
|
||||
lists, _ := s.GetAllListsForUser(userID)
|
||||
taskLists, _ := s.GetTaskLists(userID)
|
||||
|
||||
@@ -2918,7 +2737,6 @@ func (s *Store) ExportUserData(userID string) (*domain.FullExport, error) {
|
||||
User: *user,
|
||||
Thoughts: thoughts,
|
||||
Documents: docs,
|
||||
Spaces: spaces,
|
||||
Lists: lists,
|
||||
TaskLists: taskLists,
|
||||
}, nil
|
||||
@@ -3127,9 +2945,8 @@ func (s *Store) GetTaskLists(userID string) ([]domain.TaskList, error) {
|
||||
WHERE (target_type = 'user' AND target_id = ?)
|
||||
OR (target_type = 'team' AND target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
||||
)
|
||||
OR (space_id != '' AND space_id IN (SELECT space_id FROM space_members WHERE user_id = ?))
|
||||
ORDER BY updated_at DESC`,
|
||||
userID, userID, userID, userID, userID,
|
||||
userID, userID, userID, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return []domain.TaskList{}, err
|
||||
@@ -3192,9 +3009,8 @@ func (s *Store) GetTaskListByID(userID, listID string) (*domain.TaskList, error)
|
||||
WHERE (target_type = 'user' AND target_id = ?)
|
||||
OR (target_type = 'team' AND target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
||||
)
|
||||
OR (space_id != '' AND space_id IN (SELECT space_id FROM space_members WHERE user_id = ?))
|
||||
)`,
|
||||
listID, userID, userID, userID, userID, userID,
|
||||
listID, userID, userID, userID, userID,
|
||||
).Scan(&tl.ID, &tl.UserID, &tl.SpaceID, &tl.TeamID, &tl.Title, &tl.Icon, &tl.Color, &colsJSON, &tl.CreatedAt, &tl.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -48,12 +48,8 @@ func TestStoreLifecycle(t *testing.T) {
|
||||
t.Fatalf("expected converted document, got status=%s type=%s ref=%s", triaged.Status, triaged.ConvertedType, triaged.ConvertedRefID)
|
||||
}
|
||||
|
||||
// 3. Test Shared Space & Shopping List
|
||||
sp, err := store.CreateSpace(user.ID, "Test WG", "🍕", "Test Space", false)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSpace failed: %v", err)
|
||||
}
|
||||
lst, err := store.CreateList(sp.ID, "REWE Einkauf", "🛒", "shopping")
|
||||
// 3. Test Standalone Shopping List & Sharing
|
||||
lst, err := store.CreateList(user.ID, "REWE Einkauf", "🛒", "shopping")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateList failed: %v", err)
|
||||
}
|
||||
@@ -66,26 +62,26 @@ func TestStoreLifecycle(t *testing.T) {
|
||||
t.Fatalf("ToggleListItem failed: %v", err)
|
||||
}
|
||||
|
||||
// Test AddSpaceMember & RemoveSpaceMember
|
||||
// Test AddListShare & RemoveListShare
|
||||
u2, err := store.CreateUser("collab_space@example.com", "hash", "Collab User", "member")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateUser u2 failed: %v", err)
|
||||
}
|
||||
member, err := store.AddSpaceMember(user.ID, sp.ID, u2.ID, "editor")
|
||||
if err != nil || member.UserID != u2.ID {
|
||||
t.Fatalf("AddSpaceMember failed: %v", err)
|
||||
share, err := store.AddListShare(user.ID, lst.ID, "user", u2.ID, "editor")
|
||||
if err != nil || share.TargetID != u2.ID {
|
||||
t.Fatalf("AddListShare failed: %v", err)
|
||||
}
|
||||
members, err := store.GetSpaceMembers(sp.ID)
|
||||
if err != nil || len(members) != 2 {
|
||||
t.Fatalf("expected 2 members, got %d", len(members))
|
||||
shares, err := store.GetListShares(lst.ID)
|
||||
if err != nil || len(shares) != 1 {
|
||||
t.Fatalf("expected 1 share, got %d", len(shares))
|
||||
}
|
||||
err = store.RemoveSpaceMember(user.ID, sp.ID, u2.ID)
|
||||
err = store.RemoveListShare(user.ID, lst.ID, share.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("RemoveSpaceMember failed: %v", err)
|
||||
t.Fatalf("RemoveListShare failed: %v", err)
|
||||
}
|
||||
membersAfter, _ := store.GetSpaceMembers(sp.ID)
|
||||
if len(membersAfter) != 1 {
|
||||
t.Fatalf("expected 1 member after remove, got %d", len(membersAfter))
|
||||
sharesAfter, _ := store.GetListShares(lst.ID)
|
||||
if len(sharesAfter) != 0 {
|
||||
t.Fatalf("expected 0 shares after remove, got %d", len(sharesAfter))
|
||||
}
|
||||
|
||||
// 4. Test Webhook API Key generation & validation
|
||||
@@ -99,7 +95,7 @@ func TestStoreLifecycle(t *testing.T) {
|
||||
}
|
||||
|
||||
// 5. Test TaskLists CRUD
|
||||
taskList, err := store.CreateTaskList(user.ID, sp.ID, "", "Haus & Garten", "🏡", "#10b981")
|
||||
taskList, err := store.CreateTaskList(user.ID, "", "", "Haus & Garten", "🏡", "#10b981")
|
||||
if err != nil || taskList.ID == "" {
|
||||
t.Fatalf("CreateTaskList failed: %v", err)
|
||||
}
|
||||
@@ -160,8 +156,8 @@ func TestStoreLifecycle(t *testing.T) {
|
||||
}
|
||||
|
||||
// Share with user2 as "read"
|
||||
share, err := store.CreateDocShare(user.ID, newDoc.ID, user2.ID, "read")
|
||||
if err != nil || share.Permission != "read" {
|
||||
docShare, err := store.CreateDocShare(user.ID, newDoc.ID, user2.ID, "read")
|
||||
if err != nil || docShare.Permission != "read" {
|
||||
t.Fatalf("CreateDocShare failed: %v", err)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user