feat: groups
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
AlbumGroupCreateAllDto,
|
||||
AlbumGroupDeleteAllDto,
|
||||
AlbumGroupResponseDto,
|
||||
AlbumGroupUpdateDto,
|
||||
} from 'src/dtos/album-group.dto';
|
||||
import {
|
||||
AddUsersDto,
|
||||
AlbumInfoDto,
|
||||
@@ -15,7 +21,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { AlbumService } from 'src/services/album.service';
|
||||
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
|
||||
import { GroupIdAndIdParamDto, ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Albums')
|
||||
@Controller('albums')
|
||||
@@ -86,6 +92,43 @@ export class AlbumController {
|
||||
return this.service.removeAssets(auth, id, dto);
|
||||
}
|
||||
|
||||
@Get(':id/groups')
|
||||
@Authenticated({ permission: Permission.AlbumGroupRead })
|
||||
getGroupsForAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AlbumGroupResponseDto[]> {
|
||||
return this.service.getGroups(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id/groups')
|
||||
@Authenticated({ permission: Permission.AlbumGroupCreate })
|
||||
addGroupsToAlbum(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: AlbumGroupCreateAllDto,
|
||||
): Promise<AlbumGroupResponseDto[]> {
|
||||
return this.service.upsertGroups(auth, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id/groups')
|
||||
@Authenticated({ permission: Permission.AlbumGroupDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
removeGroupsFromAlbum(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: AlbumGroupDeleteAllDto,
|
||||
): Promise<void> {
|
||||
return this.service.removeGroups(auth, id, dto);
|
||||
}
|
||||
|
||||
@Put(':id/groups/:groupId')
|
||||
@Authenticated({ permission: Permission.AlbumGroupUpdate })
|
||||
updateAlbumGroup(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id, groupId }: GroupIdAndIdParamDto,
|
||||
@Body() dto: AlbumGroupUpdateDto,
|
||||
): Promise<AlbumGroupResponseDto> {
|
||||
return this.service.updateGroup(auth, id, groupId, dto);
|
||||
}
|
||||
|
||||
@Put(':id/users')
|
||||
@Authenticated({ permission: Permission.AlbumUserCreate })
|
||||
addUsersToAlbum(
|
||||
|
||||
85
server/src/controllers/group-admin.controller.ts
Normal file
85
server/src/controllers/group-admin.controller.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { GroupUserCreateAllDto, GroupUserDeleteAllDto, GroupUserResponseDto } from 'src/dtos/group-user.dto';
|
||||
import {
|
||||
GroupAdminCreateDto,
|
||||
GroupAdminResponseDto,
|
||||
GroupAdminSearchDto,
|
||||
GroupAdminUpdateDto,
|
||||
} from 'src/dtos/group.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { GroupAdminService } from 'src/services/group-admin.service';
|
||||
import { UserIdAndIdParamDto, UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Groups (admin)')
|
||||
@Controller('admin/groups')
|
||||
export class GroupAdminController {
|
||||
constructor(private service: GroupAdminService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated({ permission: Permission.AdminGroupRead, admin: true })
|
||||
searchGroupsAdmin(@Auth() auth: AuthDto, @Query() dto: GroupAdminSearchDto): Promise<GroupAdminResponseDto[]> {
|
||||
return this.service.search(auth, dto);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authenticated({ permission: Permission.AdminGroupCreate, admin: true })
|
||||
createGroupAdmin(@Auth() auth: AuthDto, @Body() dto: GroupAdminCreateDto): Promise<GroupAdminResponseDto> {
|
||||
return this.service.create(auth, dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.AdminGroupRead, admin: true })
|
||||
getGroupAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<GroupAdminResponseDto> {
|
||||
return this.service.get(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated({ permission: Permission.AdminGroupUpdate, admin: true })
|
||||
updateGroupAdmin(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: GroupAdminUpdateDto,
|
||||
): Promise<GroupAdminResponseDto> {
|
||||
return this.service.update(auth, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated({ permission: Permission.AdminGroupDelete, admin: true })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteGroupAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
||||
@Get(':id/users')
|
||||
@Authenticated({ permission: Permission.AdminGroupUserRead, admin: true })
|
||||
getUsersForGroupAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<GroupUserResponseDto[]> {
|
||||
return this.service.getUsers(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id/users')
|
||||
@Authenticated({ permission: Permission.AdminGroupUserCreate, admin: true })
|
||||
addUsersToGroupAdmin(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: GroupUserCreateAllDto,
|
||||
): Promise<GroupUserResponseDto[]> {
|
||||
return this.service.addUsers(auth, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id/user')
|
||||
@Authenticated({ permission: Permission.AdminGroupUserDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
removeUsersFromGroupAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: GroupUserDeleteAllDto) {
|
||||
return this.service.removeUsers(auth, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id/user/:userId')
|
||||
@Authenticated({ permission: Permission.AdminGroupUserDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
removeUserFromGroupAdmin(@Auth() auth: AuthDto, @Param() { id, userId }: UserIdAndIdParamDto) {
|
||||
return this.service.removeUser(auth, id, userId);
|
||||
}
|
||||
}
|
||||
40
server/src/controllers/group.controller.ts
Normal file
40
server/src/controllers/group.controller.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { GroupUserResponseDto } from 'src/dtos/group-user.dto';
|
||||
import { GroupResponseDto } from 'src/dtos/group.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { GroupService } from 'src/services/group.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Groups')
|
||||
@Controller('groups')
|
||||
export class GroupController {
|
||||
constructor(private service: GroupService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated({ permission: Permission.GroupRead })
|
||||
searchMyGroups(@Auth() auth: AuthDto): Promise<GroupResponseDto[]> {
|
||||
return this.service.search(auth);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.GroupRead })
|
||||
getMyGroup(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<GroupResponseDto> {
|
||||
return this.service.get(auth, id);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated({ permission: Permission.GroupDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
leaveMyGroup(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
||||
@Get(':id/users')
|
||||
@Authenticated({ permission: Permission.GroupRead, admin: true })
|
||||
getMyGroupUsers(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<GroupUserResponseDto[]> {
|
||||
return this.service.getUsers(auth, id);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import { AuthController } from 'src/controllers/auth.controller';
|
||||
import { DownloadController } from 'src/controllers/download.controller';
|
||||
import { DuplicateController } from 'src/controllers/duplicate.controller';
|
||||
import { FaceController } from 'src/controllers/face.controller';
|
||||
import { GroupAdminController } from 'src/controllers/group-admin.controller';
|
||||
import { GroupController } from 'src/controllers/group.controller';
|
||||
import { JobController } from 'src/controllers/job.controller';
|
||||
import { LibraryController } from 'src/controllers/library.controller';
|
||||
import { MapController } from 'src/controllers/map.controller';
|
||||
@@ -43,6 +45,8 @@ export const controllers = [
|
||||
DownloadController,
|
||||
DuplicateController,
|
||||
FaceController,
|
||||
GroupController,
|
||||
GroupAdminController,
|
||||
JobController,
|
||||
LibraryController,
|
||||
MapController,
|
||||
|
||||
@@ -144,6 +144,17 @@ export type UserAdmin = User & {
|
||||
metadata: UserMetadataItem[];
|
||||
};
|
||||
|
||||
export type Group = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export type GroupAdmin = Group & {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export type StorageAsset = {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
@@ -319,6 +330,7 @@ export const columns = {
|
||||
'shared_link.allowDownload',
|
||||
'shared_link.password',
|
||||
],
|
||||
groupAdmin: ['group.id', 'group.name', 'group.description', 'group.createdAt', 'group.updatedAt'],
|
||||
user: userColumns,
|
||||
userWithPrefix: userWithPrefixColumns,
|
||||
userAdmin: [
|
||||
|
||||
56
server/src/dtos/album-group.dto.ts
Normal file
56
server/src/dtos/album-group.dto.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsArray, ValidateNested } from 'class-validator';
|
||||
import { Group } from 'src/database';
|
||||
import { GroupResponseDto, mapGroup } from 'src/dtos/group.dto';
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
import { ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class AlbumGroupCreateAllDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AlbumGroupDto)
|
||||
groups!: AlbumGroupDto[];
|
||||
}
|
||||
|
||||
export class AlbumGroupDeleteAllDto {
|
||||
@ValidateUUID({ each: true })
|
||||
groupIds!: string[];
|
||||
}
|
||||
|
||||
export class AlbumGroupDto {
|
||||
@ValidateUUID()
|
||||
groupId!: string;
|
||||
|
||||
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', optional: true })
|
||||
role?: AlbumUserRole;
|
||||
}
|
||||
|
||||
export class AlbumGroupUpdateDto {
|
||||
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
|
||||
role!: AlbumUserRole;
|
||||
}
|
||||
|
||||
export class AlbumGroupResponseDto extends GroupResponseDto {
|
||||
metadata!: AlbumGroupMetadata;
|
||||
}
|
||||
|
||||
export class AlbumGroupMetadata {
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
}
|
||||
|
||||
type AlbumGroup = {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
group: Group;
|
||||
};
|
||||
|
||||
export const mapAlbumGroup = (albumGroup: AlbumGroup): AlbumGroupResponseDto => {
|
||||
return {
|
||||
...mapGroup(albumGroup.group),
|
||||
metadata: {
|
||||
createdAt: albumGroup.createdAt,
|
||||
updatedAt: albumGroup.updatedAt,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -3,9 +3,10 @@ import { Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator';
|
||||
import _ from 'lodash';
|
||||
import { AlbumUser, AuthSharedLink, User } from 'src/database';
|
||||
import { AlbumGroupDto } from 'src/dtos/album-group.dto';
|
||||
import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
import { mapUser, UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { AlbumUserRole, AssetOrder } from 'src/enum';
|
||||
import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
@@ -50,6 +51,12 @@ export class CreateAlbumDto {
|
||||
@Type(() => AlbumUserCreateDto)
|
||||
albumUsers?: AlbumUserCreateDto[];
|
||||
|
||||
@Optional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AlbumGroupDto)
|
||||
groups?: AlbumGroupDto[];
|
||||
|
||||
@ValidateUUID({ optional: true, each: true })
|
||||
assetIds?: string[];
|
||||
}
|
||||
|
||||
46
server/src/dtos/group-user.dto.ts
Normal file
46
server/src/dtos/group-user.dto.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ArrayNotEmpty } from 'class-validator';
|
||||
import { User } from 'src/database';
|
||||
import { mapUser, UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { ValidateUUID } from 'src/validation';
|
||||
|
||||
export class GroupUserCreateAllDto {
|
||||
@ArrayNotEmpty()
|
||||
users!: GroupUserDto[];
|
||||
}
|
||||
|
||||
export class GroupUserDeleteAllDto {
|
||||
@ValidateUUID({ each: true })
|
||||
userIds!: string[];
|
||||
}
|
||||
|
||||
export class GroupUserDto {
|
||||
@ValidateUUID()
|
||||
userId!: string;
|
||||
|
||||
// TODO potentially add a role UserGroupRole field here
|
||||
}
|
||||
|
||||
export class GroupUserResponseDto extends UserResponseDto {
|
||||
metadata!: GroupUserMetadata;
|
||||
}
|
||||
|
||||
export class GroupUserMetadata {
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
}
|
||||
|
||||
type GroupUser = {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const mapGroupUser = (groupUser: GroupUser): GroupUserResponseDto => {
|
||||
return {
|
||||
...mapUser(groupUser.user),
|
||||
metadata: {
|
||||
createdAt: groupUser.createdAt,
|
||||
updatedAt: groupUser.updatedAt,
|
||||
},
|
||||
};
|
||||
};
|
||||
69
server/src/dtos/group.dto.ts
Normal file
69
server/src/dtos/group.dto.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
|
||||
import { Group, GroupAdmin } from 'src/database';
|
||||
import { GroupUserDto } from 'src/dtos/group-user.dto';
|
||||
import { Optional, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class GroupAdminSearchDto {
|
||||
@ValidateUUID({ optional: true })
|
||||
id?: string;
|
||||
|
||||
@ValidateUUID({ optional: true })
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export class GroupAdminCreateDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name!: string;
|
||||
|
||||
@Optional({ nullable: true, emptyToNull: true })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
description?: string | null;
|
||||
|
||||
@Optional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => GroupUserDto)
|
||||
@IsArray()
|
||||
users?: GroupUserDto[];
|
||||
}
|
||||
|
||||
export class GroupAdminUpdateDto {
|
||||
@Optional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name?: string;
|
||||
|
||||
@Optional({ nullable: true, emptyToNull: true })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export class GroupResponseDto {
|
||||
id!: string;
|
||||
name!: string;
|
||||
description!: string | null;
|
||||
}
|
||||
|
||||
export class GroupAdminResponseDto extends GroupResponseDto {
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
}
|
||||
|
||||
export const mapGroup = (group: Group | GroupAdmin) => {
|
||||
return {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
description: group.description,
|
||||
};
|
||||
};
|
||||
|
||||
export const mapGroupAdmin = (group: GroupAdmin) => {
|
||||
return {
|
||||
...mapGroup(group),
|
||||
createdAt: group.createdAt,
|
||||
updatedAt: group.updatedAt,
|
||||
};
|
||||
};
|
||||
@@ -111,6 +111,11 @@ export enum Permission {
|
||||
AlbumUserUpdate = 'albumUser.update',
|
||||
AlbumUserDelete = 'albumUser.delete',
|
||||
|
||||
AlbumGroupCreate = 'albumGroup.create',
|
||||
AlbumGroupRead = 'albumGroup.read',
|
||||
AlbumGroupUpdate = 'albumGroup.update',
|
||||
AlbumGroupDelete = 'albumGroup.delete',
|
||||
|
||||
AuthChangePassword = 'auth.changePassword',
|
||||
|
||||
AuthDeviceDelete = 'authDevice.delete',
|
||||
@@ -125,6 +130,9 @@ export enum Permission {
|
||||
FaceUpdate = 'face.update',
|
||||
FaceDelete = 'face.delete',
|
||||
|
||||
GroupRead = 'group.read',
|
||||
GroupDelete = 'group.delete',
|
||||
|
||||
JobCreate = 'job.create',
|
||||
JobRead = 'job.read',
|
||||
|
||||
@@ -230,6 +238,16 @@ export enum Permission {
|
||||
UserProfileImageUpdate = 'userProfileImage.update',
|
||||
UserProfileImageDelete = 'userProfileImage.delete',
|
||||
|
||||
AdminGroupCreate = 'adminGroup.create',
|
||||
AdminGroupRead = 'adminGroup.read',
|
||||
AdminGroupUpdate = 'adminGroup.update',
|
||||
AdminGroupDelete = 'adminGroup.delete',
|
||||
|
||||
AdminGroupUserCreate = 'adminGroupUser.create',
|
||||
AdminGroupUserRead = 'adminGroupUser.read',
|
||||
AdminGroupUserUpdate = 'adminGroupUser.update',
|
||||
AdminGroupUserDelete = 'adminGroupUser.delete',
|
||||
|
||||
AdminUserCreate = 'adminUser.create',
|
||||
AdminUserRead = 'adminUser.read',
|
||||
AdminUserUpdate = 'adminUser.update',
|
||||
|
||||
87
server/src/queries/album.group.repository.sql
Normal file
87
server/src/queries/album.group.repository.sql
Normal file
@@ -0,0 +1,87 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- AlbumGroupRepository.getAll
|
||||
select
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"group"."id",
|
||||
"group"."name",
|
||||
"group"."description"
|
||||
from
|
||||
"group"
|
||||
where
|
||||
"group"."id" = "album_group"."groupId"
|
||||
) as obj
|
||||
) as "group",
|
||||
"album_group"."createdAt",
|
||||
"album_group"."updatedAt"
|
||||
from
|
||||
"album_group"
|
||||
inner join "group" on "album_group"."groupId" = "group"."id"
|
||||
where
|
||||
"albumId" = $1
|
||||
order by
|
||||
"group"."name"
|
||||
|
||||
-- AlbumGroupRepository.createAll
|
||||
insert into
|
||||
"album_group" ("albumId", "groupId", "role")
|
||||
values
|
||||
($1, $2, $3)
|
||||
on conflict ("albumId", "groupId") do update
|
||||
set
|
||||
"role" = "excluded"."role"
|
||||
returning
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"description"
|
||||
from
|
||||
"group"
|
||||
where
|
||||
"album_group"."groupId" = "group"."id"
|
||||
) as obj
|
||||
) as "group"
|
||||
|
||||
-- AlbumGroupRepository.deleteAll
|
||||
delete from "album_group"
|
||||
where
|
||||
"albumId" = $1
|
||||
and "groupId" in ($2)
|
||||
|
||||
-- AlbumGroupRepository.update
|
||||
update "album_group"
|
||||
set
|
||||
"role" = $1
|
||||
where
|
||||
"albumId" = $2
|
||||
and "groupId" = $3
|
||||
returning
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"description"
|
||||
from
|
||||
"group"
|
||||
where
|
||||
"album_group"."groupId" = "group"."id"
|
||||
) as obj
|
||||
) as "group"
|
||||
11
server/src/queries/group.repository.sql
Normal file
11
server/src/queries/group.repository.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- GroupRepository.search
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
from
|
||||
"group"
|
||||
65
server/src/queries/group.user.repository.sql
Normal file
65
server/src/queries/group.user.repository.sql
Normal file
@@ -0,0 +1,65 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- GroupUserRepository.getAll
|
||||
select
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"user2"."id",
|
||||
"user2"."name",
|
||||
"user2"."email",
|
||||
"user2"."avatarColor",
|
||||
"user2"."profileImagePath",
|
||||
"user2"."profileChangedAt"
|
||||
from
|
||||
"user" as "user2"
|
||||
where
|
||||
"user2"."id" = "group_user"."userId"
|
||||
) as obj
|
||||
) as "user",
|
||||
"group_user"."createdAt",
|
||||
"group_user"."updatedAt"
|
||||
from
|
||||
"group_user"
|
||||
inner join "user" on "group_user"."userId" = "user"."id"
|
||||
where
|
||||
"groupId" = $1
|
||||
order by
|
||||
"user"."name"
|
||||
|
||||
-- GroupUserRepository.createAll
|
||||
insert into
|
||||
"group_user" ("userId", "groupId")
|
||||
values
|
||||
($1, $2)
|
||||
on conflict do nothing
|
||||
returning
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"user2"."id",
|
||||
"user2"."name",
|
||||
"user2"."email",
|
||||
"user2"."avatarColor",
|
||||
"user2"."profileImagePath",
|
||||
"user2"."profileChangedAt"
|
||||
from
|
||||
"user" as "user2"
|
||||
where
|
||||
"group_user"."userId" = "user2"."id"
|
||||
) as obj
|
||||
) as "user"
|
||||
|
||||
-- GroupUserRepository.deleteAll
|
||||
delete from "group_user"
|
||||
where
|
||||
"groupId" = $1
|
||||
and "userId" in ($2)
|
||||
98
server/src/repositories/album-group.repository.ts
Normal file
98
server/src/repositories/album-group.repository.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely, NotNull, Updateable } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { Chunked, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AlbumGroupDto } from 'src/dtos/album-group.dto';
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { AlbumGroupTable } from 'src/schema/tables/album-group.table';
|
||||
|
||||
type AlbumGroup = { albumId: string; groupId: string };
|
||||
|
||||
@Injectable()
|
||||
export class AlbumGroupRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAll(albumId: string) {
|
||||
return this.db
|
||||
.selectFrom('album_group')
|
||||
.where('albumId', '=', albumId)
|
||||
.innerJoin('group', 'album_group.groupId', 'group.id')
|
||||
.orderBy('group.name')
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('group')
|
||||
.select(['group.id', 'group.name', 'group.description'])
|
||||
.whereRef('group.id', '=', 'album_group.groupId'),
|
||||
).as('group'),
|
||||
)
|
||||
.$narrowType<{ group: NotNull }>()
|
||||
.select(['album_group.createdAt', 'album_group.updatedAt'])
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, [{ groupId: DummyValue.UUID, role: DummyValue.STRING }]] })
|
||||
@Chunked({ paramIndex: 1 })
|
||||
createAll(albumId: string, groups: AlbumGroupDto[]) {
|
||||
return this.db
|
||||
.insertInto('album_group')
|
||||
.values(groups.map(({ groupId, role }) => ({ albumId, groupId, role: role ?? AlbumUserRole.Editor })))
|
||||
.onConflict((oc) =>
|
||||
oc.columns(['albumId', 'groupId']).doUpdateSet((eb) => ({
|
||||
role: eb.ref('excluded.role'),
|
||||
})),
|
||||
)
|
||||
.returning(['createdAt', 'updatedAt'])
|
||||
.returning((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb.selectFrom('group').whereRef('album_group.groupId', '=', 'group.id').select(['id', 'name', 'description']),
|
||||
).as('group'),
|
||||
)
|
||||
.$narrowType<{ group: NotNull }>()
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
@Chunked({ paramIndex: 1 })
|
||||
deleteAll(albumId: string, groupIds: string[]) {
|
||||
if (groupIds.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.db.deleteFrom('album_group').where('albumId', '=', albumId).where('groupId', 'in', groupIds).execute();
|
||||
}
|
||||
|
||||
async exists({ albumId, groupId }: AlbumGroup) {
|
||||
const albumGroup = await this.db
|
||||
.selectFrom('album_group')
|
||||
.select(['albumId'])
|
||||
.where('albumId', '=', albumId)
|
||||
.where('groupId', '=', groupId)
|
||||
.execute();
|
||||
|
||||
return !!albumGroup;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ albumId: DummyValue.UUID, groupId: DummyValue.UUID }, { role: DummyValue.STRING }] })
|
||||
update({ albumId, groupId }: AlbumGroup, dto: Updateable<AlbumGroupTable>) {
|
||||
return this.db
|
||||
.updateTable('album_group')
|
||||
.set(dto)
|
||||
.where('albumId', '=', albumId)
|
||||
.where('groupId', '=', groupId)
|
||||
.returning(['createdAt', 'updatedAt'])
|
||||
.returning((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb.selectFrom('group').whereRef('album_group.groupId', '=', 'group.id').select(['id', 'name', 'description']),
|
||||
).as('group'),
|
||||
)
|
||||
.$narrowType<{ group: NotNull }>()
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
delete({ albumId, groupId }: AlbumGroup) {
|
||||
return this.db.deleteFrom('album_group').where('albumId', '=', albumId).where('groupId', '=', groupId).execute();
|
||||
}
|
||||
}
|
||||
71
server/src/repositories/group-user.repository.ts
Normal file
71
server/src/repositories/group-user.repository.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely, NotNull } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { Chunked, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { DB } from 'src/schema';
|
||||
|
||||
type GroupUser = { groupId: string; userId: string };
|
||||
|
||||
@Injectable()
|
||||
export class GroupUserRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAll(groupId: string) {
|
||||
return this.db
|
||||
.selectFrom('group_user')
|
||||
.where('groupId', '=', groupId)
|
||||
.innerJoin('user', 'group_user.userId', 'user.id')
|
||||
.orderBy('user.name', 'asc')
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb.selectFrom('user as user2').select(columns.userWithPrefix).whereRef('user2.id', '=', 'group_user.userId'),
|
||||
).as('user'),
|
||||
)
|
||||
.$narrowType<{ user: NotNull }>()
|
||||
.select(['group_user.createdAt', 'group_user.updatedAt'])
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
@Chunked({ paramIndex: 1 })
|
||||
createAll(groupId: string, userIds: string[]) {
|
||||
return this.db
|
||||
.insertInto('group_user')
|
||||
.values(userIds.map((userId) => ({ userId, groupId })))
|
||||
.onConflict((oc) => oc.doNothing())
|
||||
.returning(['createdAt', 'updatedAt'])
|
||||
.returning((eb) =>
|
||||
jsonObjectFrom(
|
||||
eb.selectFrom('user as user2').whereRef('group_user.userId', '=', 'user2.id').select(columns.userWithPrefix),
|
||||
).as('user'),
|
||||
)
|
||||
.$narrowType<{ user: NotNull }>()
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
@Chunked({ paramIndex: 1 })
|
||||
deleteAll(groupId: string, usersIds: string[]) {
|
||||
if (usersIds.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.db.deleteFrom('group_user').where('groupId', '=', groupId).where('userId', 'in', usersIds).execute();
|
||||
}
|
||||
|
||||
get({ groupId, userId }: GroupUser) {
|
||||
return this.db
|
||||
.selectFrom('group_user')
|
||||
.innerJoin('group', 'group.id', 'group_user.groupId')
|
||||
.select(['group.id', 'group.name', 'group.description'])
|
||||
.where('group_user.groupId', '=', groupId)
|
||||
.where('group_user.userId', '=', userId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
delete({ userId, groupId }: GroupUser) {
|
||||
return this.db.deleteFrom('group_user').where('userId', '=', userId).where('groupId', '=', groupId).execute();
|
||||
}
|
||||
}
|
||||
65
server/src/repositories/group.repository.ts
Normal file
65
server/src/repositories/group.repository.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { GenerateSql } from 'src/decorators';
|
||||
import { GroupUserDto } from 'src/dtos/group-user.dto';
|
||||
import { DB } from 'src/schema';
|
||||
import { GroupTable } from 'src/schema/tables/group.table';
|
||||
|
||||
@Injectable()
|
||||
export class GroupRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql()
|
||||
search(options: { id?: string; userId?: string } = {}) {
|
||||
const { id, userId } = options;
|
||||
return this.db
|
||||
.selectFrom('group')
|
||||
.select(['group.id', 'group.name', 'group.description', 'group.createdAt', 'group.updatedAt'])
|
||||
.$if(!!id, (eb) => eb.where('group.id', '=', id!))
|
||||
.$if(!!userId, (eb) =>
|
||||
eb.innerJoin('group_user', 'group_user.groupId', 'group.id').where('group_user.userId', '=', userId!),
|
||||
)
|
||||
.orderBy('group.name', 'asc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
create(group: Insertable<GroupTable>, users?: GroupUserDto[]) {
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
const newGroup = await tx
|
||||
.insertInto('group')
|
||||
.values(group)
|
||||
.returning(columns.groupAdmin)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const groupId = newGroup.id;
|
||||
|
||||
if (users && users.length > 0) {
|
||||
await tx
|
||||
.insertInto('group_user')
|
||||
.values(users.map(({ userId }) => ({ groupId, userId })))
|
||||
.execute();
|
||||
}
|
||||
|
||||
return newGroup;
|
||||
});
|
||||
}
|
||||
|
||||
get(id: string) {
|
||||
return this.db.selectFrom('group').select(columns.groupAdmin).where('id', '=', id).executeTakeFirst();
|
||||
}
|
||||
|
||||
update(id: string, group: Updateable<GroupTable>) {
|
||||
return this.db
|
||||
.updateTable('group')
|
||||
.set(group)
|
||||
.where('id', '=', id)
|
||||
.returning(columns.groupAdmin)
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
delete(id: string) {
|
||||
return this.db.deleteFrom('group').where('id', '=', id).executeTakeFirstOrThrow();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -14,6 +15,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';
|
||||
@@ -48,6 +51,7 @@ export const repositories = [
|
||||
AccessRepository,
|
||||
ActivityRepository,
|
||||
AlbumRepository,
|
||||
AlbumGroupRepository,
|
||||
AlbumUserRepository,
|
||||
AuditRepository,
|
||||
ApiKeyRepository,
|
||||
@@ -61,6 +65,8 @@ export const repositories = [
|
||||
DuplicateRepository,
|
||||
EmailRepository,
|
||||
EventRepository,
|
||||
GroupRepository,
|
||||
GroupUserRepository,
|
||||
JobRepository,
|
||||
LibraryRepository,
|
||||
LoggingRepository,
|
||||
|
||||
@@ -165,6 +165,46 @@ export const album_user_delete_audit = registerFunction({
|
||||
END`,
|
||||
});
|
||||
|
||||
export const album_group_delete_audit = registerFunction({
|
||||
name: 'album_group_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
language: 'PLPGSQL',
|
||||
body: `
|
||||
BEGIN
|
||||
INSERT INTO "album_audit" ("albumId", "userId")
|
||||
SELECT OLD."albumId", "group_user"."userId"
|
||||
FROM OLD INNER JOIN "group_user" ON "group_user"."groupId" = OLD."groupId";
|
||||
|
||||
IF pg_trigger_depth() = 1 THEN
|
||||
INSERT INTO album_group_audit ("albumId", "groupId")
|
||||
SELECT "albumId", "groupId"
|
||||
FROM OLD;
|
||||
END IF;
|
||||
|
||||
RETURN NULL;
|
||||
END`,
|
||||
});
|
||||
|
||||
export const group_user_delete_audit = registerFunction({
|
||||
name: 'group_user_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
language: 'PLPGSQL',
|
||||
body: `
|
||||
BEGIN
|
||||
INSERT INTO group_audit ("groupId", "userId")
|
||||
SELECT "groupId", "userId"
|
||||
FROM OLD;
|
||||
|
||||
IF pg_trigger_depth() = 1 THEN
|
||||
INSERT INTO group_user_audit ("groupId", "userId")
|
||||
SELECT "groupId", "userId"
|
||||
FROM OLD;
|
||||
END IF;
|
||||
|
||||
RETURN NULL;
|
||||
END`,
|
||||
});
|
||||
|
||||
export const memory_delete_audit = registerFunction({
|
||||
name: 'memory_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
|
||||
@@ -22,6 +22,8 @@ import { ActivityTable } from 'src/schema/tables/activity.table';
|
||||
import { AlbumAssetAuditTable } from 'src/schema/tables/album-asset-audit.table';
|
||||
import { AlbumAssetTable } from 'src/schema/tables/album-asset.table';
|
||||
import { AlbumAuditTable } from 'src/schema/tables/album-audit.table';
|
||||
import { AlbumGroupAuditTable } from 'src/schema/tables/album-group-audit.table';
|
||||
import { AlbumGroupTable } from 'src/schema/tables/album-group.table';
|
||||
import { AlbumUserAuditTable } from 'src/schema/tables/album-user-audit.table';
|
||||
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
@@ -36,6 +38,10 @@ import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { AuditTable } from 'src/schema/tables/audit.table';
|
||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
||||
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
||||
import { GroupAuditTable } from 'src/schema/tables/group-audit.table';
|
||||
import { GroupUserAuditTable } from 'src/schema/tables/group-user-audit.table';
|
||||
import { GroupUserTable } from 'src/schema/tables/group-user.table';
|
||||
import { GroupTable } from 'src/schema/tables/group.table';
|
||||
import { LibraryTable } from 'src/schema/tables/library.table';
|
||||
import { MemoryAssetAuditTable } from 'src/schema/tables/memory-asset-audit.table';
|
||||
import { MemoryAssetTable } from 'src/schema/tables/memory-asset.table';
|
||||
@@ -71,12 +77,14 @@ import { Database, Extensions, Generated, Int8 } from 'src/sql-tools';
|
||||
export class ImmichDatabase {
|
||||
tables = [
|
||||
ActivityTable,
|
||||
AlbumAssetTable,
|
||||
AlbumAssetAuditTable,
|
||||
AlbumAuditTable,
|
||||
AlbumTable,
|
||||
AlbumGroupTable,
|
||||
AlbumGroupAuditTable,
|
||||
AlbumUserAuditTable,
|
||||
AlbumUserTable,
|
||||
AlbumTable,
|
||||
AlbumAssetTable,
|
||||
AlbumAssetAuditTable,
|
||||
ApiKeyTable,
|
||||
AssetAuditTable,
|
||||
AssetFaceTable,
|
||||
@@ -88,6 +96,10 @@ export class ImmichDatabase {
|
||||
AssetExifTable,
|
||||
FaceSearchTable,
|
||||
GeodataPlacesTable,
|
||||
GroupTable,
|
||||
GroupAuditTable,
|
||||
GroupUserTable,
|
||||
GroupUserAuditTable,
|
||||
LibraryTable,
|
||||
MemoryTable,
|
||||
MemoryAuditTable,
|
||||
@@ -154,6 +166,8 @@ export interface DB {
|
||||
album_audit: AlbumAuditTable;
|
||||
album_asset: AlbumAssetTable;
|
||||
album_asset_audit: AlbumAssetAuditTable;
|
||||
album_group: AlbumGroupTable;
|
||||
album_group_audit: AlbumGroupAuditTable;
|
||||
album_user: AlbumUserTable;
|
||||
album_user_audit: AlbumUserAuditTable;
|
||||
|
||||
@@ -173,6 +187,11 @@ export interface DB {
|
||||
|
||||
geodata_places: GeodataPlacesTable;
|
||||
|
||||
group: GroupTable;
|
||||
group_audit: GroupAuditTable;
|
||||
group_user: GroupUserTable;
|
||||
group_user_audit: GroupUserAuditTable;
|
||||
|
||||
library: LibraryTable;
|
||||
|
||||
memory: MemoryTable;
|
||||
|
||||
118
server/src/schema/migrations/1753908954798-AddUserGroups.ts
Normal file
118
server/src/schema/migrations/1753908954798-AddUserGroups.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE OR REPLACE FUNCTION group_delete_audit()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO group_audit ("groupId")
|
||||
SELECT "id"
|
||||
FROM OLD;
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`CREATE OR REPLACE FUNCTION group_user_delete_audit()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO group_audit ("groupId", "userId")
|
||||
SELECT "groupId", "userId"
|
||||
FROM OLD;
|
||||
|
||||
IF pg_trigger_depth() = 1 THEN
|
||||
INSERT INTO group_user_audit ("groupId", "userId")
|
||||
SELECT "groupId", "userId"
|
||||
FROM OLD;
|
||||
END IF;
|
||||
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`CREATE TABLE "group_audit" (
|
||||
"id" uuid NOT NULL DEFAULT immich_uuid_v7(),
|
||||
"groupId" uuid NOT NULL,
|
||||
"deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(),
|
||||
CONSTRAINT "group_audit_pkey" PRIMARY KEY ("id")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "group_audit_deletedAt_idx" ON "group_audit" ("deletedAt");`.execute(db);
|
||||
await sql`CREATE TABLE "group_user_audit" (
|
||||
"id" uuid NOT NULL DEFAULT immich_uuid_v7(),
|
||||
"groupId" uuid NOT NULL,
|
||||
"userId" uuid NOT NULL,
|
||||
"deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(),
|
||||
CONSTRAINT "group_user_audit_pkey" PRIMARY KEY ("id")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "group_user_audit_groupId_idx" ON "group_user_audit" ("groupId");`.execute(db);
|
||||
await sql`CREATE INDEX "group_user_audit_userId_idx" ON "group_user_audit" ("userId");`.execute(db);
|
||||
await sql`CREATE INDEX "group_user_audit_deletedAt_idx" ON "group_user_audit" ("deletedAt");`.execute(db);
|
||||
await sql`CREATE TABLE "group_user" (
|
||||
"groupId" uuid NOT NULL,
|
||||
"userId" uuid NOT NULL,
|
||||
"createId" uuid NOT NULL DEFAULT immich_uuid_v7(),
|
||||
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"updateId" uuid NOT NULL DEFAULT immich_uuid_v7(),
|
||||
"updatedAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "group_user_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "album" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "group_user_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "group_user_pkey" PRIMARY KEY ("groupId", "userId")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "group_user_groupId_idx" ON "group_user" ("groupId");`.execute(db);
|
||||
await sql`CREATE INDEX "group_user_userId_idx" ON "group_user" ("userId");`.execute(db);
|
||||
await sql`CREATE INDEX "group_user_createId_idx" ON "group_user" ("createId");`.execute(db);
|
||||
await sql`CREATE INDEX "group_user_updateId_idx" ON "group_user" ("updateId");`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "group_user_delete_audit"
|
||||
AFTER DELETE ON "group_user"
|
||||
REFERENCING OLD TABLE AS "old"
|
||||
FOR EACH STATEMENT
|
||||
WHEN (pg_trigger_depth() <= 1)
|
||||
EXECUTE FUNCTION group_user_delete_audit();`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "group_user_updatedAt"
|
||||
BEFORE UPDATE ON "group_user"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||
await sql`CREATE TABLE "group" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"name" character varying NOT NULL,
|
||||
"description" character varying,
|
||||
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"updateId" uuid NOT NULL DEFAULT immich_uuid_v7(),
|
||||
CONSTRAINT "group_name_uq" UNIQUE ("name"),
|
||||
CONSTRAINT "group_pkey" PRIMARY KEY ("id")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "group_updatedAt_id_idx" ON "group" ("updatedAt", "id");`.execute(db);
|
||||
await sql`CREATE INDEX "group_updateId_idx" ON "group" ("updateId");`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "group_delete_audit"
|
||||
AFTER DELETE ON "group"
|
||||
REFERENCING OLD TABLE AS "old"
|
||||
FOR EACH STATEMENT
|
||||
WHEN (pg_trigger_depth() = 0)
|
||||
EXECUTE FUNCTION group_delete_audit();`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "group_updatedAt"
|
||||
BEFORE UPDATE ON "group"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_group_delete_audit', '{"type":"function","name":"group_delete_audit","sql":"CREATE OR REPLACE FUNCTION group_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO group_audit (\\"groupId\\")\\n SELECT \\"id\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_group_user_delete_audit', '{"type":"function","name":"group_user_delete_audit","sql":"CREATE OR REPLACE FUNCTION group_user_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO group_audit (\\"groupId\\", \\"userId\\")\\n SELECT \\"groupId\\", \\"userId\\"\\n FROM OLD;\\n\\n IF pg_trigger_depth() = 1 THEN\\n INSERT INTO group_user_audit (\\"groupId\\", \\"userId\\")\\n SELECT \\"groupId\\", \\"userId\\"\\n FROM OLD;\\n END IF;\\n\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_group_user_delete_audit', '{"type":"trigger","name":"group_user_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"group_user_delete_audit\\"\\n AFTER DELETE ON \\"group_user\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() <= 1)\\n EXECUTE FUNCTION group_user_delete_audit();"}'::jsonb);`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_group_user_updatedAt', '{"type":"trigger","name":"group_user_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"group_user_updatedAt\\"\\n BEFORE UPDATE ON \\"group_user\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_group_delete_audit', '{"type":"trigger","name":"group_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"group_delete_audit\\"\\n AFTER DELETE ON \\"group\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION group_delete_audit();"}'::jsonb);`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_group_updatedAt', '{"type":"trigger","name":"group_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"group_updatedAt\\"\\n BEFORE UPDATE ON \\"group\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TABLE "group_audit";`.execute(db);
|
||||
await sql`DROP TABLE "group_user_audit";`.execute(db);
|
||||
await sql`DROP TABLE "group_user";`.execute(db);
|
||||
await sql`DROP TABLE "group";`.execute(db);
|
||||
await sql`DROP FUNCTION group_delete_audit;`.execute(db);
|
||||
await sql`DROP FUNCTION group_user_delete_audit;`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_group_delete_audit';`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_group_user_delete_audit';`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_group_user_delete_audit';`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_group_user_updatedAt';`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_group_delete_audit';`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_group_updatedAt';`.execute(db);
|
||||
}
|
||||
11
server/src/schema/migrations/1753973644248-FixStuff.ts
Normal file
11
server/src/schema/migrations/1753973644248-FixStuff.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "group_user" DROP CONSTRAINT "group_user_groupId_fkey";`.execute(db);
|
||||
await sql`ALTER TABLE "group_user" ADD CONSTRAINT "group_user_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "group" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "group_user" DROP CONSTRAINT "group_user_groupId_fkey";`.execute(db);
|
||||
await sql`ALTER TABLE "group_user" ADD CONSTRAINT "group_user_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "album" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||
}
|
||||
33
server/src/schema/migrations/1753976066354-FixStuff2.ts
Normal file
33
server/src/schema/migrations/1753976066354-FixStuff2.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TRIGGER "group_delete_audit" ON "group";`.execute(db);
|
||||
await sql`ALTER TABLE "group_audit" ADD "userId" uuid NOT NULL;`.execute(db);
|
||||
await sql`DROP FUNCTION group_delete_audit;`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_group_delete_audit';`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_group_delete_audit';`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE OR REPLACE FUNCTION public.group_delete_audit()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
INSERT INTO group_audit ("groupId")
|
||||
SELECT "id"
|
||||
FROM OLD;
|
||||
RETURN NULL;
|
||||
END
|
||||
$function$
|
||||
`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "group_delete_audit"
|
||||
AFTER DELETE ON "group"
|
||||
REFERENCING OLD TABLE AS "old"
|
||||
FOR EACH STATEMENT
|
||||
WHEN ((pg_trigger_depth() = 0))
|
||||
EXECUTE FUNCTION group_delete_audit();`.execute(db);
|
||||
await sql`ALTER TABLE "group_audit" DROP COLUMN "userId";`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_group_delete_audit', '{"sql":"CREATE OR REPLACE FUNCTION group_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO group_audit (\\"groupId\\")\\n SELECT \\"id\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;","name":"group_delete_audit","type":"function"}'::jsonb);`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_group_delete_audit', '{"sql":"CREATE OR REPLACE TRIGGER \\"group_delete_audit\\"\\n AFTER DELETE ON \\"group\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION group_delete_audit();","name":"group_delete_audit","type":"trigger"}'::jsonb);`.execute(db);
|
||||
}
|
||||
70
server/src/schema/migrations/1753995223470-FixStuff.ts
Normal file
70
server/src/schema/migrations/1753995223470-FixStuff.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE OR REPLACE FUNCTION album_group_delete_audit()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO "album_audit" ("albumId", "userId")
|
||||
SELECT OLD."albumId", "group_user"."userId"
|
||||
FROM OLD INNER JOIN "group_user" ON "group_user"."groupId" = OLD."groupId";
|
||||
|
||||
IF pg_trigger_depth() = 1 THEN
|
||||
INSERT INTO album_group_audit ("albumId", "groupId")
|
||||
SELECT "albumId", "groupId"
|
||||
FROM OLD;
|
||||
END IF;
|
||||
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`CREATE TABLE "album_group_audit" (
|
||||
"id" uuid NOT NULL DEFAULT immich_uuid_v7(),
|
||||
"albumId" uuid NOT NULL,
|
||||
"groupId" uuid NOT NULL,
|
||||
"deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(),
|
||||
CONSTRAINT "album_group_audit_pkey" PRIMARY KEY ("id")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "album_group_audit_albumId_idx" ON "album_group_audit" ("albumId");`.execute(db);
|
||||
await sql`CREATE INDEX "album_group_audit_groupId_idx" ON "album_group_audit" ("groupId");`.execute(db);
|
||||
await sql`CREATE INDEX "album_group_audit_deletedAt_idx" ON "album_group_audit" ("deletedAt");`.execute(db);
|
||||
await sql`CREATE TABLE "album_group" (
|
||||
"albumId" uuid NOT NULL,
|
||||
"groupId" uuid NOT NULL,
|
||||
"role" character varying NOT NULL DEFAULT 'editor',
|
||||
"createId" uuid NOT NULL DEFAULT immich_uuid_v7(),
|
||||
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"updateId" uuid NOT NULL DEFAULT immich_uuid_v7(),
|
||||
"updatedAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "album_group_albumId_fkey" FOREIGN KEY ("albumId") REFERENCES "album" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "album_group_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "group" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "album_group_pkey" PRIMARY KEY ("albumId", "groupId")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "album_group_albumId_idx" ON "album_group" ("albumId");`.execute(db);
|
||||
await sql`CREATE INDEX "album_group_groupId_idx" ON "album_group" ("groupId");`.execute(db);
|
||||
await sql`CREATE INDEX "album_group_createId_idx" ON "album_group" ("createId");`.execute(db);
|
||||
await sql`CREATE INDEX "album_group_updateId_idx" ON "album_group" ("updateId");`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "album_group_delete_audit"
|
||||
AFTER DELETE ON "album_group"
|
||||
REFERENCING OLD TABLE AS "old"
|
||||
FOR EACH STATEMENT
|
||||
WHEN (pg_trigger_depth() <= 1)
|
||||
EXECUTE FUNCTION album_group_delete_audit();`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "album_group_updatedAt"
|
||||
BEFORE UPDATE ON "album_group"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_album_group_delete_audit', '{"type":"function","name":"album_group_delete_audit","sql":"CREATE OR REPLACE FUNCTION album_group_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO \\"album_audit\\" (\\"albumId\\", \\"userId\\")\\n SELECT OLD.\\"albumId\\", \\"group_user\\".\\"userId\\"\\n FROM OLD INNER JOIN \\"group_user\\" ON \\"group_user\\".\\"groupId\\" = OLD.\\"groupId\\";\\n\\n IF pg_trigger_depth() = 1 THEN\\n INSERT INTO album_group_audit (\\"albumId\\", \\"groupId\\")\\n SELECT \\"albumId\\", \\"groupId\\"\\n FROM OLD;\\n END IF;\\n\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_group_delete_audit', '{"type":"trigger","name":"album_group_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"album_group_delete_audit\\"\\n AFTER DELETE ON \\"album_group\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() <= 1)\\n EXECUTE FUNCTION album_group_delete_audit();"}'::jsonb);`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_group_updatedAt', '{"type":"trigger","name":"album_group_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"album_group_updatedAt\\"\\n BEFORE UPDATE ON \\"album_group\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TABLE "album_group_audit";`.execute(db);
|
||||
await sql`DROP TABLE "album_group";`.execute(db);
|
||||
await sql`DROP FUNCTION album_group_delete_audit;`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_album_group_delete_audit';`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_group_delete_audit';`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_group_updatedAt';`.execute(db);
|
||||
}
|
||||
17
server/src/schema/tables/album-group-audit.table.ts
Normal file
17
server/src/schema/tables/album-group-audit.table.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
|
||||
|
||||
@Table('album_group_audit')
|
||||
export class AlbumGroupAuditTable {
|
||||
@PrimaryGeneratedUuidV7Column()
|
||||
id!: Generated<string>;
|
||||
|
||||
@Column({ type: 'uuid', index: true })
|
||||
albumId!: string;
|
||||
|
||||
@Column({ type: 'uuid', index: true })
|
||||
groupId!: string;
|
||||
|
||||
@CreateDateColumn({ default: () => 'clock_timestamp()', index: true })
|
||||
deletedAt!: Generated<Timestamp>;
|
||||
}
|
||||
56
server/src/schema/tables/album-group.table.ts
Normal file
56
server/src/schema/tables/album-group.table.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
import { album_group_delete_audit } from 'src/schema/functions';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { GroupTable } from 'src/schema/tables/group.table';
|
||||
import {
|
||||
AfterDeleteTrigger,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ForeignKeyColumn,
|
||||
Generated,
|
||||
Table,
|
||||
Timestamp,
|
||||
UpdateDateColumn,
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table({ name: 'album_group' })
|
||||
@UpdatedAtTrigger('album_group_updatedAt')
|
||||
@AfterDeleteTrigger({
|
||||
scope: 'statement',
|
||||
function: album_group_delete_audit,
|
||||
referencingOldTableAs: 'old',
|
||||
when: 'pg_trigger_depth() <= 1',
|
||||
})
|
||||
export class AlbumGroupTable {
|
||||
@ForeignKeyColumn(() => AlbumTable, {
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
nullable: false,
|
||||
primary: true,
|
||||
})
|
||||
albumId!: string;
|
||||
|
||||
@ForeignKeyColumn(() => GroupTable, {
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
nullable: false,
|
||||
primary: true,
|
||||
})
|
||||
groupId!: string;
|
||||
|
||||
@Column({ type: 'character varying', default: AlbumUserRole.Editor })
|
||||
role!: Generated<AlbumUserRole>;
|
||||
|
||||
@CreateIdColumn({ index: true })
|
||||
createId!: Generated<string>;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Generated<Timestamp>;
|
||||
|
||||
@UpdateIdColumn({ index: true })
|
||||
updateId!: Generated<string>;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Generated<Timestamp>;
|
||||
}
|
||||
17
server/src/schema/tables/group-audit.table.ts
Normal file
17
server/src/schema/tables/group-audit.table.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
|
||||
|
||||
@Table('group_audit')
|
||||
export class GroupAuditTable {
|
||||
@PrimaryGeneratedUuidV7Column()
|
||||
id!: Generated<string>;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
groupId!: string;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
userId!: string;
|
||||
|
||||
@CreateDateColumn({ default: () => 'clock_timestamp()', index: true })
|
||||
deletedAt!: Generated<Timestamp>;
|
||||
}
|
||||
17
server/src/schema/tables/group-user-audit.table.ts
Normal file
17
server/src/schema/tables/group-user-audit.table.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
|
||||
|
||||
@Table('group_user_audit')
|
||||
export class GroupUserAuditTable {
|
||||
@PrimaryGeneratedUuidV7Column()
|
||||
id!: Generated<string>;
|
||||
|
||||
@Column({ type: 'uuid', index: true })
|
||||
groupId!: string;
|
||||
|
||||
@Column({ type: 'uuid', index: true })
|
||||
userId!: string;
|
||||
|
||||
@CreateDateColumn({ default: () => 'clock_timestamp()', index: true })
|
||||
deletedAt!: Generated<Timestamp>;
|
||||
}
|
||||
51
server/src/schema/tables/group-user.table.ts
Normal file
51
server/src/schema/tables/group-user.table.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { group_user_delete_audit } from 'src/schema/functions';
|
||||
import { GroupTable } from 'src/schema/tables/group.table';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import {
|
||||
AfterDeleteTrigger,
|
||||
CreateDateColumn,
|
||||
ForeignKeyColumn,
|
||||
Generated,
|
||||
Table,
|
||||
Timestamp,
|
||||
UpdateDateColumn,
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table({ name: 'group_user' })
|
||||
@UpdatedAtTrigger('group_user_updatedAt')
|
||||
@AfterDeleteTrigger({
|
||||
scope: 'statement',
|
||||
function: group_user_delete_audit,
|
||||
referencingOldTableAs: 'old',
|
||||
when: 'pg_trigger_depth() <= 1',
|
||||
})
|
||||
export class GroupUserTable {
|
||||
@ForeignKeyColumn(() => GroupTable, {
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
nullable: false,
|
||||
primary: true,
|
||||
})
|
||||
groupId!: string;
|
||||
|
||||
@ForeignKeyColumn(() => UserTable, {
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
nullable: false,
|
||||
primary: true,
|
||||
})
|
||||
userId!: string;
|
||||
|
||||
@CreateIdColumn({ index: true })
|
||||
createId!: Generated<string>;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Generated<Timestamp>;
|
||||
|
||||
@UpdateIdColumn({ index: true })
|
||||
updateId!: Generated<string>;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Generated<Timestamp>;
|
||||
}
|
||||
34
server/src/schema/tables/group.table.ts
Normal file
34
server/src/schema/tables/group.table.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Generated,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
Table,
|
||||
Timestamp,
|
||||
UpdateDateColumn,
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table('group')
|
||||
@UpdatedAtTrigger('group_updatedAt')
|
||||
@Index({ columns: ['updatedAt', 'id'] })
|
||||
export class GroupTable {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: Generated<string>;
|
||||
|
||||
@Column({ unique: true })
|
||||
name!: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
description!: string | null;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Generated<Timestamp>;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Generated<Timestamp>;
|
||||
|
||||
@UpdateIdColumn({ index: true })
|
||||
updateId!: Generated<string>;
|
||||
}
|
||||
@@ -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] });
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
95
server/src/services/group-admin.service.ts
Normal file
95
server/src/services/group-admin.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
38
server/src/services/group.service.ts
Normal file
38
server/src/services/group.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -80,12 +80,20 @@ export const ValidateUUID = (options?: UUIDOptions & ApiPropertyOptions) => {
|
||||
};
|
||||
|
||||
export class UUIDParamDto {
|
||||
@IsNotEmpty()
|
||||
@IsUUID('4')
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@ValidateUUID()
|
||||
id!: string;
|
||||
}
|
||||
|
||||
export class UserIdAndIdParamDto extends UUIDParamDto {
|
||||
@ValidateUUID()
|
||||
userId!: string;
|
||||
}
|
||||
|
||||
export class GroupIdAndIdParamDto extends UUIDParamDto {
|
||||
@ValidateUUID()
|
||||
groupId!: string;
|
||||
}
|
||||
|
||||
export class UUIDAssetIDParamDto {
|
||||
@ValidateUUID()
|
||||
id!: string;
|
||||
|
||||
Reference in New Issue
Block a user