feat: groups

This commit is contained in:
Jason Rasmussen
2025-07-30 18:18:38 -04:00
parent 641a3baadd
commit 4a881022c3
76 changed files with 6515 additions and 124 deletions

View File

@@ -1,4 +1,11 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import {
AlbumGroupCreateAllDto,
AlbumGroupDeleteAllDto,
AlbumGroupResponseDto,
AlbumGroupUpdateDto,
mapAlbumGroup,
} from 'src/dtos/album-group.dto';
import {
AddUsersDto,
AlbumInfoDto,
@@ -204,6 +211,38 @@ export class AlbumService extends BaseService {
return results;
}
async getGroups(auth: AuthDto, id: string): Promise<AlbumGroupResponseDto[]> {
await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [id] });
const albumGroups = await this.albumGroupRepository.getAll(id);
return albumGroups.map((albumGroup) => mapAlbumGroup(albumGroup));
}
async upsertGroups(auth: AuthDto, id: string, { groups }: AlbumGroupCreateAllDto): Promise<AlbumGroupResponseDto[]> {
await this.requireAccess({ auth, permission: Permission.AlbumUpdate, ids: [id] });
const albumGroups = await this.albumGroupRepository.createAll(id, groups);
return albumGroups.map((albumGroup) => mapAlbumGroup(albumGroup));
}
async removeGroups(auth: AuthDto, id: string, dto: AlbumGroupDeleteAllDto): Promise<void> {
await this.requireAccess({ auth, permission: Permission.AlbumUpdate, ids: [id] });
await this.albumGroupRepository.deleteAll(id, dto.groupIds);
}
async updateGroup(
auth: AuthDto,
id: string,
groupId: string,
dto: AlbumGroupUpdateDto,
): Promise<AlbumGroupResponseDto> {
await this.requireAccess({ auth, permission: Permission.AlbumUpdate, ids: [id] });
const exists = await this.albumGroupRepository.exists({ albumId: id, groupId });
if (!exists) {
throw new BadRequestException('Album group not found');
}
const albumGroup = await this.albumGroupRepository.update({ albumId: id, groupId }, { role: dto.role });
return mapAlbumGroup(albumGroup);
}
async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise<AlbumResponseDto> {
await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] });

View File

@@ -7,6 +7,7 @@ import { StorageCore } from 'src/cores/storage.core';
import { UserAdmin } from 'src/database';
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumGroupRepository } from 'src/repositories/album-group.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
import { AlbumRepository } from 'src/repositories/album.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
@@ -21,6 +22,8 @@ import { DownloadRepository } from 'src/repositories/download.repository';
import { DuplicateRepository } from 'src/repositories/duplicate.repository';
import { EmailRepository } from 'src/repositories/email.repository';
import { EventRepository } from 'src/repositories/event.repository';
import { GroupUserRepository } from 'src/repositories/group-user.repository';
import { GroupRepository } from 'src/repositories/group.repository';
import { JobRepository } from 'src/repositories/job.repository';
import { LibraryRepository } from 'src/repositories/library.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
@@ -111,6 +114,7 @@ export class BaseService {
protected accessRepository: AccessRepository,
protected activityRepository: ActivityRepository,
protected albumRepository: AlbumRepository,
protected albumGroupRepository: AlbumGroupRepository,
protected albumUserRepository: AlbumUserRepository,
protected apiKeyRepository: ApiKeyRepository,
protected assetRepository: AssetRepository,
@@ -124,6 +128,8 @@ export class BaseService {
protected duplicateRepository: DuplicateRepository,
protected emailRepository: EmailRepository,
protected eventRepository: EventRepository,
protected groupRepository: GroupRepository,
protected groupUserRepository: GroupUserRepository,
protected jobRepository: JobRepository,
protected libraryRepository: LibraryRepository,
protected machineLearningRepository: MachineLearningRepository,

View File

@@ -0,0 +1,95 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { PostgresError } from 'postgres';
import { AuthDto } from 'src/dtos/auth.dto';
import {
GroupUserCreateAllDto,
GroupUserDeleteAllDto,
GroupUserResponseDto,
mapGroupUser,
} from 'src/dtos/group-user.dto';
import {
GroupAdminCreateDto,
GroupAdminResponseDto,
GroupAdminSearchDto,
GroupAdminUpdateDto,
mapGroupAdmin,
} from 'src/dtos/group.dto';
import { BaseService } from 'src/services/base.service';
@Injectable()
export class GroupAdminService extends BaseService {
async search(auth: AuthDto, dto: GroupAdminSearchDto): Promise<GroupAdminResponseDto[]> {
const groups = await this.groupRepository.search(dto);
return groups.map((group) => mapGroupAdmin(group));
}
async create(auth: AuthDto, dto: GroupAdminCreateDto): Promise<GroupAdminResponseDto> {
try {
const { users, ...groupDto } = dto;
const group = await this.groupRepository.create(groupDto, users);
return mapGroupAdmin(group);
} catch (error) {
this.handleError(error);
}
}
async get(auth: AuthDto, id: string): Promise<GroupAdminResponseDto> {
const group = await this.findOrFail(id);
return mapGroupAdmin(group);
}
async update(auth: AuthDto, id: string, dto: GroupAdminUpdateDto): Promise<GroupAdminResponseDto> {
await this.findOrFail(id);
const updated = await this.groupRepository.update(id, { ...dto, updatedAt: new Date() });
return mapGroupAdmin(updated);
}
async delete(auth: AuthDto, id: string): Promise<void> {
await this.findOrFail(id);
await this.groupRepository.delete(id);
}
async getUsers(auth: AuthDto, id: string): Promise<GroupUserResponseDto[]> {
await this.findOrFail(id);
const users = await this.groupUserRepository.getAll(id);
return users.map((user) => mapGroupUser(user));
}
async addUsers(auth: AuthDto, id: string, { users }: GroupUserCreateAllDto): Promise<GroupUserResponseDto[]> {
await this.findOrFail(id);
const userIds = users.map(({ userId }) => userId);
const groupUsers = await this.groupUserRepository.createAll(id, userIds);
return groupUsers.map((groupUser) => mapGroupUser(groupUser));
}
async removeUsers(auth: AuthDto, id: string, dto: GroupUserDeleteAllDto): Promise<void> {
await this.findOrFail(id);
await this.groupUserRepository.deleteAll(id, dto.userIds);
}
async removeUser(auth: AuthDto, id: string, userId: string): Promise<void> {
await this.findOrFail(id);
const exists = await this.groupUserRepository.get({ groupId: id, userId });
if (!exists) {
throw new BadRequestException('Group does not include this user');
}
await this.groupUserRepository.delete({ groupId: id, userId });
}
private handleError(error: unknown): never {
if ((error as PostgresError).constraint_name === 'group_name_uq') {
throw new BadRequestException('Group with this name already exists');
}
throw error;
}
private async findOrFail(id: string) {
const group = await this.groupRepository.get(id);
if (!group) {
throw new BadRequestException('Group not found');
}
return group;
}
}

View File

@@ -0,0 +1,38 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { AuthDto } from 'src/dtos/auth.dto';
import { GroupUserResponseDto, mapGroupUser } from 'src/dtos/group-user.dto';
import { GroupResponseDto, mapGroup } from 'src/dtos/group.dto';
import { BaseService } from 'src/services/base.service';
@Injectable()
export class GroupService extends BaseService {
async search(auth: AuthDto): Promise<GroupResponseDto[]> {
const groups = await this.groupRepository.search({ userId: auth.user.id });
return groups.map((group) => mapGroup(group));
}
async get(auth: AuthDto, id: string): Promise<GroupResponseDto> {
const group = await this.findOrFail({ userId: auth.user.id, groupId: id });
return mapGroup(group);
}
async delete(auth: AuthDto, id: string): Promise<void> {
await this.findOrFail({ userId: auth.user.id, groupId: id });
await this.groupUserRepository.delete({ userId: auth.user.id, groupId: id });
}
async getUsers(auth: AuthDto, id: string): Promise<GroupUserResponseDto[]> {
await this.findOrFail({ userId: auth.user.id, groupId: id });
const users = await this.groupUserRepository.getAll(id);
return users.map((user) => mapGroupUser(user));
}
async findOrFail({ userId, groupId }: { userId: string; groupId: string }): Promise<GroupResponseDto> {
const [group] = await this.groupUserRepository.get({ userId, groupId });
if (!group) {
throw new BadRequestException('Group not found');
}
return group;
}
}

View File

@@ -11,6 +11,8 @@ import { CliService } from 'src/services/cli.service';
import { DatabaseService } from 'src/services/database.service';
import { DownloadService } from 'src/services/download.service';
import { DuplicateService } from 'src/services/duplicate.service';
import { GroupAdminService } from 'src/services/group-admin.service';
import { GroupService } from 'src/services/group.service';
import { JobService } from 'src/services/job.service';
import { LibraryService } from 'src/services/library.service';
import { MapService } from 'src/services/map.service';
@@ -54,6 +56,8 @@ export const services = [
DatabaseService,
DownloadService,
DuplicateService,
GroupAdminService,
GroupService,
JobService,
LibraryService,
MapService,