From 2b420831ac2e4c67aeaf7d80dabec75403773b26 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Thu, 12 Jan 2023 22:24:09 +0200 Subject: [PATCH] storages and storage sets --- src/app-storage/app-storage.controller.ts | 153 +++++++++++++++++ src/app-storage/app-storage.module.ts | 15 ++ src/app-storage/app-storage.service.ts | 121 +++++++++++++ src/app-storage/dto/storage-request.dto.ts | 38 +++++ src/app-storage/dto/storage-response.dto.ts | 19 +++ .../dto/storage-set-request.dto.ts | 38 +++++ .../dto/storage-set-response.dto.ts | 18 ++ src/app.module.ts | 2 + src/objects/building/building.service.ts | 28 +++ .../entities/item-transaction.entity.ts | 11 ++ src/objects/storage/entities/item.entity.ts | 15 ++ .../storage/entities/storage-set.entity.ts | 12 ++ .../storage/entities/storage.entity.ts | 17 +- .../storage/entities/stored-item.entity.ts | 21 +++ src/objects/storage/storage.service.ts | 159 ++++++++++++++++++ .../decorators/current-building.decorator.ts | 13 ++ .../decorators/current-room.decorator.ts | 13 ++ .../current-storage-set.decorator.ts | 13 ++ .../decorators/current-storage.decorator.ts | 13 ++ src/shared/guards/building.guard.ts | 44 +++++ src/shared/guards/room.guard.ts | 52 ++++++ src/shared/guards/storage-set.guard.ts | 62 +++++++ src/shared/guards/storage.guard.ts | 62 +++++++ 23 files changed, 937 insertions(+), 2 deletions(-) create mode 100644 src/app-storage/app-storage.controller.ts create mode 100644 src/app-storage/app-storage.module.ts create mode 100644 src/app-storage/app-storage.service.ts create mode 100644 src/app-storage/dto/storage-request.dto.ts create mode 100644 src/app-storage/dto/storage-response.dto.ts create mode 100644 src/app-storage/dto/storage-set-request.dto.ts create mode 100644 src/app-storage/dto/storage-set-response.dto.ts create mode 100644 src/shared/decorators/current-building.decorator.ts create mode 100644 src/shared/decorators/current-room.decorator.ts create mode 100644 src/shared/decorators/current-storage-set.decorator.ts create mode 100644 src/shared/decorators/current-storage.decorator.ts create mode 100644 src/shared/guards/building.guard.ts create mode 100644 src/shared/guards/room.guard.ts create mode 100644 src/shared/guards/storage-set.guard.ts create mode 100644 src/shared/guards/storage.guard.ts diff --git a/src/app-storage/app-storage.controller.ts b/src/app-storage/app-storage.controller.ts new file mode 100644 index 0000000..9aa9cf8 --- /dev/null +++ b/src/app-storage/app-storage.controller.ts @@ -0,0 +1,153 @@ +import { + Body, + ClassSerializerInterceptor, + Controller, + Delete, + Get, + Patch, + Post, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { + ApiBody, + ApiOkResponse, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger'; +import { Room } from 'src/objects/building/entities/room.entity'; +import { StorageSet } from 'src/objects/storage/entities/storage-set.entity'; +import { Storage } from 'src/objects/storage/entities/storage.entity'; +import { User } from 'src/objects/user/user.entity'; +import { CurrentRoom } from 'src/shared/decorators/current-room.decorator'; +import { CurrentStorageSet } from 'src/shared/decorators/current-storage-set.decorator'; +import { CurrentStorage } from 'src/shared/decorators/current-storage.decorator'; +import { LoggedInUser } from 'src/shared/decorators/user.decorator'; +import { AuthGuard } from 'src/shared/guards/auth.guard'; +import { BuildingGuard } from 'src/shared/guards/building.guard'; +import { RoomGuard } from 'src/shared/guards/room.guard'; +import { StorageSetGuard } from 'src/shared/guards/storage-set.guard'; +import { StorageGuard } from 'src/shared/guards/storage.guard'; +import { AppStorageService } from './app-storage.service'; +import { + StorageCreateRequestDto, + StorageUpdateRequestDto, +} from './dto/storage-request.dto'; +import { StorageResponseDto } from './dto/storage-response.dto'; +import { + StorageSetCreateRequestDto, + StorageSetUpdateRequestDto, +} from './dto/storage-set-request.dto'; +import { StorageSetResponseDto } from './dto/storage-set-response.dto'; + +@Controller({ + path: 'storage', +}) +@ApiTags('storage') +@ApiSecurity('Bearer token') +@UseInterceptors(ClassSerializerInterceptor) +@UseGuards(AuthGuard, BuildingGuard, RoomGuard, StorageGuard) +export class AppStorageController { + constructor(private readonly service: AppStorageService) {} + + @Get(':storageId') + @ApiOperation({ summary: 'Get storage by ID' }) + @ApiOkResponse({ type: StorageResponseDto }) + async getStorage( + @CurrentStorage() storage: Storage, + ): Promise { + return this.service.formatStorageNoItems(storage); + } + + @Patch(':storageId') + @ApiBody({ type: StorageUpdateRequestDto }) + @ApiOperation({ summary: 'Update storage by ID' }) + @ApiOkResponse({ type: StorageResponseDto }) + async updateStorage( + @CurrentStorage() storage: Storage, + @Body() body: StorageUpdateRequestDto, + ): Promise { + return this.service.updateStorage(storage, body); + } + + @UseGuards(StorageSetGuard) + @Get('set/:storageSetId') + @ApiOperation({ summary: 'Get storage set' }) + @ApiOkResponse({ type: StorageSetResponseDto }) + async getStorageSet(@CurrentStorageSet() set: StorageSet) { + return this.service.formatStorageSetNoItems(set); + } + + @UseGuards(StorageSetGuard) + @Patch('set/:storageSetId') + @ApiBody({ type: StorageSetUpdateRequestDto }) + @ApiOperation({ summary: 'Update storage set by ID' }) + @ApiOkResponse({ type: StorageSetResponseDto }) + async updateStorageSet( + @CurrentStorageSet() set: StorageSet, + @Body() body: StorageSetUpdateRequestDto, + ): Promise { + return this.service.updateStorageSet(set, body); + } + + @UseGuards(StorageSetGuard) + @Post('set/:storageSetId/:storageId') + @ApiOperation({ summary: 'Move storage to storage set' }) + @ApiOkResponse({ type: StorageSetResponseDto }) + async moveStorage( + @CurrentStorageSet() set: StorageSet, + @CurrentStorage() storage: Storage, + ): Promise { + return this.service.moveStorage(set, storage); + } + + @UseGuards(StorageSetGuard) + @Delete('set/:storageSetId/:storageId') + @ApiOperation({ summary: 'Remove storage from storage set' }) + @ApiOkResponse({ type: StorageSetResponseDto }) + async removeStorageFromSet( + @CurrentStorageSet() set: StorageSet, + @CurrentStorage() storage: Storage, + ): Promise { + return this.service.removeFromSet(set, storage); + } + + @Get('room/:roomId') + @ApiOperation({ summary: 'Get storages in room' }) + @ApiOkResponse({ type: StorageResponseDto, isArray: true }) + async getStorages(@CurrentRoom() room: Room) { + return this.service.getStoragesInRoom(room.id); + } + + @Get('set/room/:roomId') + @ApiOperation({ summary: 'Get storage sets in room' }) + @ApiOkResponse({ type: StorageSetResponseDto, isArray: true }) + async getStorageSets(@CurrentRoom() room: Room) { + return this.service.getStorageSetsInRoom(room.id); + } + + @Post('set/room/:roomId') + @ApiBody({ type: StorageCreateRequestDto }) + @ApiOperation({ summary: 'Create storage sets in room' }) + @ApiOkResponse({ type: StorageSetResponseDto, isArray: true }) + async createStorageSet( + @LoggedInUser() user: User, + @Body() body: StorageSetCreateRequestDto, + @CurrentRoom() room: Room, + ) { + return this.service.createStorageSet(user, room, body); + } + + @Post('room/:roomId') + @ApiBody({ type: StorageCreateRequestDto }) + @ApiOperation({ summary: 'Add a new storage to room' }) + @ApiOkResponse({ type: StorageResponseDto }) + async addStorage( + @LoggedInUser() user: User, + @Body() body: StorageCreateRequestDto, + @CurrentRoom() room: Room, + ): Promise { + return this.service.createStorage(user, room, body); + } +} diff --git a/src/app-storage/app-storage.module.ts b/src/app-storage/app-storage.module.ts new file mode 100644 index 0000000..d5676dd --- /dev/null +++ b/src/app-storage/app-storage.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { BuildingModule } from 'src/objects/building/building.module'; +import { GroupModule } from 'src/objects/group/group.module'; +import { StorageModule } from 'src/objects/storage/storage.module'; +import { UserModule } from 'src/objects/user/user.module'; +import { AuthModule } from 'src/shared/auth/auth.module'; +import { AppStorageController } from './app-storage.controller'; +import { AppStorageService } from './app-storage.service'; + +@Module({ + imports: [BuildingModule, AuthModule, UserModule, GroupModule, StorageModule], + providers: [AppStorageService], + controllers: [AppStorageController], +}) +export class AppStorageModule {} diff --git a/src/app-storage/app-storage.service.ts b/src/app-storage/app-storage.service.ts new file mode 100644 index 0000000..0589015 --- /dev/null +++ b/src/app-storage/app-storage.service.ts @@ -0,0 +1,121 @@ +import { Injectable } from '@nestjs/common'; +import omit from 'lodash.omit'; +import pick from 'lodash.pick'; +import { BuildingService } from 'src/objects/building/building.service'; +import { Room } from 'src/objects/building/entities/room.entity'; +import { StorageSet } from 'src/objects/storage/entities/storage-set.entity'; +import { Storage } from 'src/objects/storage/entities/storage.entity'; +import { StorageService } from 'src/objects/storage/storage.service'; +import { User } from 'src/objects/user/user.entity'; +import { + StorageCreateRequestDto, + StorageUpdateRequestDto, +} from './dto/storage-request.dto'; +import { + StorageActorResponse, + StorageResponseDto, +} from './dto/storage-response.dto'; +import { + StorageSetCreateRequestDto, + StorageSetUpdateRequestDto, +} from './dto/storage-set-request.dto'; +import { StorageSetResponseDto } from './dto/storage-set-response.dto'; + +@Injectable() +export class AppStorageService { + constructor( + private readonly buildingService: BuildingService, + private readonly storageService: StorageService, + ) {} + + async getStoragesInRoom(roomId: number) { + const storages = await this.storageService.getStoragesInRoom(roomId); + return storages.map((storage) => this.formatStorageNoItems(storage)); + } + + async getStorageSetsInRoom(roomId: number) { + const sets = await this.storageService.getStorageSetsInRoom(roomId); + return sets.map((set) => this.formatStorageSetNoItems(set)); + } + + async createStorage(user: User, room: Room, body: StorageCreateRequestDto) { + // TODO: validate color and location + const newStorage = await this.storageService.saveStorage({ + ...body, + addedBy: user, + room, + }); + + return this.formatStorageNoItems(newStorage); + } + + async createStorageSet( + user: User, + room: Room, + body: StorageSetCreateRequestDto, + ) { + // TODO: validate color and location + const newStorageSet = await this.storageService.saveStorageSet({ + ...body, + addedBy: user, + room, + }); + + return this.formatStorageSetNoItems(newStorageSet); + } + + async updateStorage(storage: Storage, body: StorageUpdateRequestDto) { + Object.assign(storage, body); + await this.storageService.saveStorage(storage); + return this.formatStorageNoItems(storage); + } + + async updateStorageSet( + storageSet: StorageSet, + body: StorageSetUpdateRequestDto, + ) { + Object.assign(storageSet, body); + await this.storageService.saveStorageSet(storageSet); + return this.formatStorageSetNoItems(storageSet); + } + + async moveStorage(set: StorageSet, storage: Storage) { + storage.set = set; + + await this.storageService.saveStorage(storage); + + set.storages = [...(set.storages || []), storage]; + + return this.formatStorageSetNoItems(set); + } + + async removeFromSet(set: StorageSet, storage: Storage) { + storage.set = null; + await this.storageService.saveStorage(storage); + set.storages = (set.storages || []).filter( + (entry) => entry.id !== storage.id, + ); + return this.formatStorageSetNoItems(set); + } + + formatActor(input: User): StorageActorResponse { + return pick(input, ['name', 'sub', 'color']); + } + + formatStorageNoItems(storage: Storage): StorageResponseDto { + return { + ...omit(storage, ['room', 'set', 'items']), + addedBy: storage.addedBy && this.formatActor(storage.addedBy), + }; + } + + formatStorageSetNoItems(set: StorageSet): StorageSetResponseDto { + return { + ...omit(set, ['room']), + addedBy: set.addedBy && this.formatActor(set.addedBy), + storages: set.storages?.length + ? set.storages.map((storage) => this.formatStorageNoItems(storage)) + : [], + }; + } +} diff --git a/src/app-storage/dto/storage-request.dto.ts b/src/app-storage/dto/storage-request.dto.ts new file mode 100644 index 0000000..59bbdbc --- /dev/null +++ b/src/app-storage/dto/storage-request.dto.ts @@ -0,0 +1,38 @@ +import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger'; +import { + IsEnum, + IsOptional, + IsString, + MaxLength, + MinLength, +} from 'class-validator'; +import { StorageType } from 'src/objects/storage/enums/storage-type.enum'; + +export class StorageCreateRequestDto { + @ApiProperty() + @IsString() + @MinLength(3) + @MaxLength(32) + displayName: string; + + @ApiProperty({ type: String, enum: StorageType }) + @IsEnum(StorageType) + type: StorageType; + + @ApiProperty() + @IsString() + location: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + locationDescription?: string; + + @ApiProperty() + @IsString() + color: string; +} + +export class StorageUpdateRequestDto extends PartialType( + StorageCreateRequestDto, +) {} diff --git a/src/app-storage/dto/storage-response.dto.ts b/src/app-storage/dto/storage-response.dto.ts new file mode 100644 index 0000000..f3b7185 --- /dev/null +++ b/src/app-storage/dto/storage-response.dto.ts @@ -0,0 +1,19 @@ +import { ApiPropertyOptional, OmitType, PickType } from '@nestjs/swagger'; +import { Storage } from 'src/objects/storage/entities/storage.entity'; +import { User } from 'src/objects/user/user.entity'; + +export class StorageActorResponse extends PickType(User, [ + 'sub', + 'name', + 'color', +]) {} + +export class StorageResponseDto extends OmitType(Storage, [ + 'room', + 'items', + 'set', + 'addedBy', +]) { + @ApiPropertyOptional({ type: StorageActorResponse }) + addedBy: StorageActorResponse; +} diff --git a/src/app-storage/dto/storage-set-request.dto.ts b/src/app-storage/dto/storage-set-request.dto.ts new file mode 100644 index 0000000..22d6db6 --- /dev/null +++ b/src/app-storage/dto/storage-set-request.dto.ts @@ -0,0 +1,38 @@ +import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger'; +import { + IsEnum, + IsOptional, + IsString, + MaxLength, + MinLength, +} from 'class-validator'; +import { StorageSetType } from 'src/objects/storage/enums/storage-set-type.enum'; + +export class StorageSetCreateRequestDto { + @ApiProperty() + @IsString() + @MinLength(3) + @MaxLength(32) + displayName: string; + + @ApiProperty({ type: String, enum: StorageSetType }) + @IsEnum(StorageSetType) + type: StorageSetType; + + @ApiProperty() + @IsString() + location: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + locationDescription?: string; + + @ApiProperty() + @IsString() + color: string; +} + +export class StorageSetUpdateRequestDto extends PartialType( + StorageSetCreateRequestDto, +) {} diff --git a/src/app-storage/dto/storage-set-response.dto.ts b/src/app-storage/dto/storage-set-response.dto.ts new file mode 100644 index 0000000..6bb0969 --- /dev/null +++ b/src/app-storage/dto/storage-set-response.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { StorageSet } from 'src/objects/storage/entities/storage-set.entity'; +import { + StorageActorResponse, + StorageResponseDto, +} from './storage-response.dto'; + +export class StorageSetResponseDto extends OmitType(StorageSet, [ + 'room', + 'addedBy', + 'storages', +]) { + @ApiPropertyOptional({ type: StorageActorResponse }) + addedBy: StorageActorResponse; + + @ApiProperty({ type: StorageResponseDto, isArray: true }) + storages: StorageResponseDto[]; +} diff --git a/src/app.module.ts b/src/app.module.ts index 6d2f731..bde587d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,6 +3,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppBuildingModule } from './app-building/app-building.module'; import { AppGroupModule } from './app-group/app-group.module'; +import { AppStorageModule } from './app-storage/app-storage.module'; import { AppUser } from './app-user/app-user.module'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @@ -42,6 +43,7 @@ import { SecretsModule } from './shared/secrets/secrets.module'; AppUser, AppGroupModule, AppBuildingModule, + AppStorageModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/objects/building/building.service.ts b/src/objects/building/building.service.ts index 65ee0a2..a0fc139 100644 --- a/src/objects/building/building.service.ts +++ b/src/objects/building/building.service.ts @@ -89,6 +89,34 @@ export class BuildingService { }); } + async getRoomByIdAndUserSub(roomId: number, sub: string, relations = []) { + return this.roomRepository.findOne({ + where: { + id: roomId, + building: { + groups: { + members: { + sub, + }, + }, + }, + }, + relations, + }); + } + + async getRoomByBuilding(buildingId: number, roomId: number, relations = []) { + return this.roomRepository.findOne({ + where: { + id: roomId, + building: { + id: buildingId, + }, + }, + relations, + }); + } + async getRoomsOnFloorByBuildingAndUserSub( buildingId: number, floorNumber: number, diff --git a/src/objects/storage/entities/item-transaction.entity.ts b/src/objects/storage/entities/item-transaction.entity.ts index 1ecc34b..bf8dfb9 100644 --- a/src/objects/storage/entities/item-transaction.entity.ts +++ b/src/objects/storage/entities/item-transaction.entity.ts @@ -1,3 +1,4 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { User } from 'src/objects/user/user.entity'; import { Column, @@ -12,39 +13,49 @@ import { StoredItem } from './stored-item.entity'; @Entity() export class StoredItemTransaction { + @ApiProperty() @PrimaryGeneratedColumn() id: number; + @ApiProperty({ type: String, enum: TransactionType }) @Column({ type: String, default: TransactionType.ACQUIRED }) type: TransactionType; + @ApiPropertyOptional() @Column({ nullable: true }) price?: number; + @ApiPropertyOptional() @Column({ nullable: true, default: 'EUR', length: 3 }) currency?: string; + @ApiProperty({ type: () => StoredItem }) @ManyToOne(() => StoredItem, { onDelete: 'CASCADE', onUpdate: 'CASCADE', }) storedItem: StoredItem; + @ApiPropertyOptional({ type: () => User }) @ManyToOne(() => User, { onDelete: 'SET NULL', nullable: true, }) actor?: User; + @ApiPropertyOptional() @Column({ nullable: true }) notes?: string; + @ApiPropertyOptional() @Column({ type: 'datetime', nullable: true }) actionAt?: Date; + @ApiProperty() @CreateDateColumn() createdAt: Date; + @ApiProperty() @UpdateDateColumn() updatedAt: Date; } diff --git a/src/objects/storage/entities/item.entity.ts b/src/objects/storage/entities/item.entity.ts index e3ccc80..ebdd9ec 100644 --- a/src/objects/storage/entities/item.entity.ts +++ b/src/objects/storage/entities/item.entity.ts @@ -1,3 +1,4 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { User } from 'src/objects/user/user.entity'; import { Column, @@ -13,48 +14,62 @@ import { StoredItem } from './stored-item.entity'; @Entity() export class Item { + @ApiProperty() @PrimaryGeneratedColumn() id: number; + @ApiProperty() @Column() displayName: string; + @ApiProperty({ type: String, enum: ItemType }) @Column({ type: String, default: ItemType.ITEM }) type: ItemType; + @ApiPropertyOptional() @Column({ nullable: true }) barcode?: string; + @ApiPropertyOptional() @Column({ default: false }) consumable: boolean; + @ApiPropertyOptional() @Column({ nullable: true }) image?: string; + @ApiPropertyOptional() @Column({ nullable: true }) weight?: number; + @ApiPropertyOptional() @Column({ nullable: true }) url?: string; + @ApiPropertyOptional() @Column({ nullable: true }) notes?: string; + @ApiPropertyOptional({ type: () => StoredItem, isArray: true }) @OneToMany(() => StoredItem, (store) => store.item) instances?: StoredItem[]; + @ApiProperty({ type: () => User }) @ManyToOne(() => User, { onDelete: 'SET NULL', nullable: true, }) addedBy: User; + @ApiProperty() @Column({ default: false }) public: boolean; + @ApiProperty() @CreateDateColumn() createdAt: Date; + @ApiProperty() @UpdateDateColumn() updatedAt: Date; } diff --git a/src/objects/storage/entities/storage-set.entity.ts b/src/objects/storage/entities/storage-set.entity.ts index 10d56b7..088d855 100644 --- a/src/objects/storage/entities/storage-set.entity.ts +++ b/src/objects/storage/entities/storage-set.entity.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { Room } from 'src/objects/building/entities/room.entity'; import { User } from 'src/objects/user/user.entity'; import { @@ -14,42 +15,53 @@ import { Storage } from './storage.entity'; @Entity() export class StorageSet { + @ApiProperty() @PrimaryGeneratedColumn() id: number; + @ApiProperty() @Column() displayName: string; + @ApiProperty({ type: () => Room }) @ManyToOne(() => Room, { onDelete: 'CASCADE', onUpdate: 'CASCADE', }) room: Room; + @ApiProperty({ type: String, enum: StorageSetType }) @Column({ type: String }) type: StorageSetType; + @ApiProperty() @Column() location: string; + @ApiProperty() @Column() locationDescription: string; + @ApiProperty() @Column() color: string; + @ApiProperty({ type: () => Storage, isArray: true }) @OneToMany(() => Storage, (storage) => storage.set) storages: Storage[]; + @ApiProperty({ type: () => User }) @ManyToOne(() => User, { onDelete: 'SET NULL', nullable: true, }) addedBy: User; + @ApiProperty() @CreateDateColumn() createdAt: Date; + @ApiProperty() @UpdateDateColumn() updatedAt: Date; } diff --git a/src/objects/storage/entities/storage.entity.ts b/src/objects/storage/entities/storage.entity.ts index 231854e..dce1562 100644 --- a/src/objects/storage/entities/storage.entity.ts +++ b/src/objects/storage/entities/storage.entity.ts @@ -1,3 +1,4 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Room } from 'src/objects/building/entities/room.entity'; import { User } from 'src/objects/user/user.entity'; import { @@ -15,30 +16,38 @@ import { StoredItem } from './stored-item.entity'; @Entity() export class Storage { + @ApiProperty() @PrimaryGeneratedColumn() id: number; + @ApiProperty() @Column() displayName: string; + @ApiProperty({ type: () => Room }) @ManyToOne(() => Room, { onDelete: 'CASCADE', onUpdate: 'CASCADE', }) room: Room; + @ApiProperty({ type: String, enum: StorageType }) @Column({ type: String }) type: StorageType; + @ApiProperty() @Column() location: string; - @Column() - locationDescription: string; + @ApiPropertyOptional() + @Column({ nullable: true }) + locationDescription?: string; + @ApiProperty({ type: () => StoredItem, isArray: true }) @OneToMany(() => StoredItem, (item) => item.storage) items: StoredItem[]; + @ApiProperty({ type: () => StorageSet }) @ManyToOne(() => StorageSet, { nullable: true, onDelete: 'SET NULL', @@ -46,18 +55,22 @@ export class Storage { }) set?: StorageSet; + @ApiProperty() @Column() color: string; + @ApiProperty({ type: () => User }) @ManyToOne(() => User, { onDelete: 'SET NULL', nullable: true, }) addedBy: User; + @ApiProperty() @CreateDateColumn() createdAt: Date; + @ApiProperty() @UpdateDateColumn() updatedAt: Date; } diff --git a/src/objects/storage/entities/stored-item.entity.ts b/src/objects/storage/entities/stored-item.entity.ts index 4e1470a..3a8a75b 100644 --- a/src/objects/storage/entities/stored-item.entity.ts +++ b/src/objects/storage/entities/stored-item.entity.ts @@ -1,29 +1,36 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { User } from 'src/objects/user/user.entity'; import { Column, CreateDateColumn, Entity, ManyToOne, + OneToMany, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; +import { StoredItemTransaction } from './item-transaction.entity'; import { Item } from './item.entity'; import { Storage } from './storage.entity'; @Entity() export class StoredItem { + @ApiProperty() @PrimaryGeneratedColumn() id: number; + @ApiProperty() @Column() displayName: string; + @ApiProperty({ type: () => Item }) @ManyToOne(() => Item, { onDelete: 'CASCADE', onUpdate: 'CASCADE', }) item: Item; + @ApiPropertyOptional({ type: () => Storage }) @ManyToOne(() => Storage, { nullable: true, onDelete: 'SET NULL', @@ -31,27 +38,41 @@ export class StoredItem { }) storage?: Storage; + @ApiPropertyOptional() @Column({ nullable: true }) notes?: string; + @ApiProperty({ type: () => User }) @ManyToOne(() => User, { onDelete: 'SET NULL', nullable: true, }) addedBy: User; + @ApiPropertyOptional({ type: () => StoredItemTransaction, isArray: true }) + @OneToMany( + () => StoredItemTransaction, + (transaction) => transaction.storedItem, + ) + transactions?: StoredItemTransaction[]; + + @ApiPropertyOptional() @Column({ nullable: true, type: 'datetime' }) expiresAt?: Date; + @ApiPropertyOptional() @Column({ nullable: true, type: 'datetime' }) acquiredAt?: Date; + @ApiPropertyOptional() @Column({ nullable: true, type: 'datetime' }) consumedAt?: Date; + @ApiProperty() @CreateDateColumn() createdAt: Date; + @ApiProperty() @UpdateDateColumn() updatedAt: Date; } diff --git a/src/objects/storage/storage.service.ts b/src/objects/storage/storage.service.ts index a3bb150..1603c81 100644 --- a/src/objects/storage/storage.service.ts +++ b/src/objects/storage/storage.service.ts @@ -27,4 +27,163 @@ export class StorageService { private readonly userService: UserService, private readonly buildingService: BuildingService, ) {} + + async getStorageByIdAndSub(id: number, sub: string, relations = []) { + return this.storageRepository.findOne({ + where: { + id, + room: { + building: { + groups: { + members: { + sub, + }, + }, + }, + }, + }, + relations, + }); + } + + async getStorageByIdAndBuilding( + id: number, + buildingId: number, + relations = [], + ) { + return this.storageRepository.findOne({ + where: { + id, + room: { + building: { + id: buildingId, + }, + }, + }, + relations, + }); + } + + async getStorageByIdAndRoom(id: number, roomId: number, relations = []) { + return this.storageRepository.findOne({ + where: { + id, + room: { + id: roomId, + }, + }, + relations, + }); + } + + async getStorageSetByIdAndSub(id: number, sub: string, relations = []) { + return this.storageSetRepository.findOne({ + where: { + id, + room: { + building: { + groups: { + members: { + sub, + }, + }, + }, + }, + }, + relations: ['storages', 'storages.addedBy', ...relations], + }); + } + + async getStorageSetByIdAndBuilding( + id: number, + buildingId: number, + relations = [], + ) { + return this.storageSetRepository.findOne({ + where: { + id, + room: { + building: { + id: buildingId, + }, + }, + }, + relations: ['storages', 'storages.addedBy', ...relations], + }); + } + + async getStorageSetByIdAndRoom(id: number, roomId: number, relations = []) { + return this.storageSetRepository.findOne({ + where: { + id, + room: { + id: roomId, + }, + }, + relations: ['storages', 'storages.addedBy', ...relations], + }); + } + + async getStoragesInRoom(roomId: number, relations = []) { + return this.storageRepository.find({ + where: { + room: { + id: roomId, + }, + }, + relations, + }); + } + + async getStorageSetsInRoom(roomId: number, relations = []) { + return this.storageSetRepository.find({ + where: { + room: { + id: roomId, + }, + }, + relations: ['storages', 'storages.addedBy', ...relations], + }); + } + + async getItemsInStorage(storageId: number, relations = []) { + return this.storedItemRepository.find({ + where: { + storage: { + id: storageId, + }, + }, + relations: ['item', ...relations], + }); + } + + async saveStorage(data: Partial) { + const newStorage = new Storage(); + Object.assign(newStorage, data); + return this.storageRepository.save(newStorage); + } + + async saveItem(data: Partial) { + const newItem = new Item(); + Object.assign(newItem, data); + return this.itemRepository.save(newItem); + } + + async saveStoredItem(data: Partial) { + const newStoredItem = new StoredItem(); + Object.assign(newStoredItem, data); + return this.storedItemRepository.save(newStoredItem); + } + + async saveStorageSet(data: Partial) { + const newStorageSet = new StorageSet(); + Object.assign(newStorageSet, data); + return this.storageSetRepository.save(newStorageSet); + } + + async saveStoredItemTransaction(data: Partial) { + const newStoredItemTransaction = new StoredItemTransaction(); + Object.assign(newStoredItemTransaction, data); + return this.transactionRepository.save(newStoredItemTransaction); + } } diff --git a/src/shared/decorators/current-building.decorator.ts b/src/shared/decorators/current-building.decorator.ts new file mode 100644 index 0000000..b1e5be7 --- /dev/null +++ b/src/shared/decorators/current-building.decorator.ts @@ -0,0 +1,13 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { Response } from 'express'; +import { Building } from 'src/objects/building/entities/building.entity'; + +/** + * Get the building from the response. + */ +export const CurrentBuilding = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const response = ctx.switchToHttp().getResponse() as Response; + return response.locals.building as Building; + }, +); diff --git a/src/shared/decorators/current-room.decorator.ts b/src/shared/decorators/current-room.decorator.ts new file mode 100644 index 0000000..3d61cd8 --- /dev/null +++ b/src/shared/decorators/current-room.decorator.ts @@ -0,0 +1,13 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { Response } from 'express'; +import { Room } from 'src/objects/building/entities/room.entity'; + +/** + * Get the room from the response. + */ +export const CurrentRoom = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const response = ctx.switchToHttp().getResponse() as Response; + return response.locals.room as Room; + }, +); diff --git a/src/shared/decorators/current-storage-set.decorator.ts b/src/shared/decorators/current-storage-set.decorator.ts new file mode 100644 index 0000000..7f63403 --- /dev/null +++ b/src/shared/decorators/current-storage-set.decorator.ts @@ -0,0 +1,13 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { Response } from 'express'; +import { StorageSet } from 'src/objects/storage/entities/storage-set.entity'; + +/** + * Get the storage from the response. + */ +export const CurrentStorageSet = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const response = ctx.switchToHttp().getResponse() as Response; + return response.locals.storageSet as StorageSet; + }, +); diff --git a/src/shared/decorators/current-storage.decorator.ts b/src/shared/decorators/current-storage.decorator.ts new file mode 100644 index 0000000..58c4dbd --- /dev/null +++ b/src/shared/decorators/current-storage.decorator.ts @@ -0,0 +1,13 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { Response } from 'express'; +import { Storage } from 'src/objects/storage/entities/storage.entity'; + +/** + * Get the storage from the response. + */ +export const CurrentStorage = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const response = ctx.switchToHttp().getResponse() as Response; + return response.locals.storage as Storage; + }, +); diff --git a/src/shared/guards/building.guard.ts b/src/shared/guards/building.guard.ts new file mode 100644 index 0000000..f0cd2f0 --- /dev/null +++ b/src/shared/guards/building.guard.ts @@ -0,0 +1,44 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Request } from 'express'; +import { BuildingService } from 'src/objects/building/building.service'; +import { User } from 'src/objects/user/user.entity'; + +@Injectable() +export class BuildingGuard implements CanActivate { + constructor(private readonly buildingService: BuildingService) {} + + async canActivate(context: ExecutionContext): Promise { + const http = context.switchToHttp(); + const request = http.getRequest() as Request; + const response = http.getResponse(); + + const user = response.locals.user as User; + if (!user) return false; + + if ( + request.params.buildingId == null && + request.body?.buildingId == null && + request.query?.buildingId == null + ) { + return true; + } + + const buildingId = parseInt( + request.params.buildingId || + request.body?.buildingId || + request.query?.buildingId, + 10, + ); + + if (!buildingId || isNaN(buildingId)) return false; + + const buildingAccess = await this.buildingService.getBuildingByIdAndUserSub( + buildingId, + user.sub, + ); + if (!buildingAccess) return false; + response.locals.building = buildingAccess; + + return true; + } +} diff --git a/src/shared/guards/room.guard.ts b/src/shared/guards/room.guard.ts new file mode 100644 index 0000000..6679bf5 --- /dev/null +++ b/src/shared/guards/room.guard.ts @@ -0,0 +1,52 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Request } from 'express'; +import { BuildingService } from 'src/objects/building/building.service'; +import { Room } from 'src/objects/building/entities/room.entity'; +import { User } from 'src/objects/user/user.entity'; + +@Injectable() +export class RoomGuard implements CanActivate { + constructor(private readonly buildingService: BuildingService) {} + + async canActivate(context: ExecutionContext): Promise { + const http = context.switchToHttp(); + const request = http.getRequest() as Request; + const response = http.getResponse(); + + const user = response.locals.user as User; + if (!user) return false; + + if ( + request.params.roomId == null && + request.body?.roomId == null && + request.query?.roomId == null + ) { + return true; + } + + const roomId = parseInt( + request.params.roomId || request.body?.roomId || request.query?.roomId, + 10, + ); + + if (!roomId || isNaN(roomId)) return false; + + let roomAccess: Room; + if (response.locals.building) { + roomAccess = await this.buildingService.getRoomByBuilding( + roomId, + response.locals.building.id, + ); + } else { + roomAccess = await this.buildingService.getRoomByIdAndUserSub( + roomId, + user.sub, + ); + } + + if (!roomAccess) return false; + response.locals.room = roomAccess; + + return true; + } +} diff --git a/src/shared/guards/storage-set.guard.ts b/src/shared/guards/storage-set.guard.ts new file mode 100644 index 0000000..ad97b6a --- /dev/null +++ b/src/shared/guards/storage-set.guard.ts @@ -0,0 +1,62 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Request } from 'express'; +import { StorageSet } from 'src/objects/storage/entities/storage-set.entity'; +import { StorageService } from 'src/objects/storage/storage.service'; +import { User } from 'src/objects/user/user.entity'; + +@Injectable() +export class StorageSetGuard implements CanActivate { + constructor(private readonly storageService: StorageService) {} + + async canActivate(context: ExecutionContext): Promise { + const http = context.switchToHttp(); + const request = http.getRequest() as Request; + const response = http.getResponse(); + + const user = response.locals.user as User; + if (!user) return false; + + if ( + request.params.storageSetId == null && + request.body?.storageSetId == null && + request.query?.storageSetId == null + ) { + return true; + } + + const storageSetId = parseInt( + request.params.storageSetId || + request.body?.storageSetId || + request.query?.storageSetId, + 10, + ); + + if (!storageSetId || isNaN(storageSetId)) return false; + + let storageSetAccess: StorageSet; + if (response.locals.room) { + storageSetAccess = await this.storageService.getStorageSetByIdAndRoom( + storageSetId, + response.locals.room.id, + ['addedBy'], + ); + } else if (response.locals.building) { + storageSetAccess = await this.storageService.getStorageSetByIdAndBuilding( + storageSetId, + response.locals.building.id, + ['addedBy'], + ); + } else { + storageSetAccess = await this.storageService.getStorageSetByIdAndSub( + storageSetId, + user.sub, + ['addedBy'], + ); + } + + if (!storageSetAccess) return false; + response.locals.storageSet = storageSetAccess; + + return true; + } +} diff --git a/src/shared/guards/storage.guard.ts b/src/shared/guards/storage.guard.ts new file mode 100644 index 0000000..5e5972b --- /dev/null +++ b/src/shared/guards/storage.guard.ts @@ -0,0 +1,62 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Request } from 'express'; +import { Storage } from 'src/objects/storage/entities/storage.entity'; +import { StorageService } from 'src/objects/storage/storage.service'; +import { User } from 'src/objects/user/user.entity'; + +@Injectable() +export class StorageGuard implements CanActivate { + constructor(private readonly storageService: StorageService) {} + + async canActivate(context: ExecutionContext): Promise { + const http = context.switchToHttp(); + const request = http.getRequest() as Request; + const response = http.getResponse(); + + const user = response.locals.user as User; + if (!user) return false; + + if ( + request.params.storageId == null && + request.body?.storageId == null && + request.query?.storageId == null + ) { + return true; + } + + const storageId = parseInt( + request.params.storageId || + request.body?.storageId || + request.query?.storageId, + 10, + ); + + if (!storageId || isNaN(storageId)) return false; + + let storageAccess: Storage; + if (response.locals.room) { + storageAccess = await this.storageService.getStorageByIdAndRoom( + storageId, + response.locals.room.id, + ['addedBy'], + ); + } else if (response.locals.building) { + storageAccess = await this.storageService.getStorageByIdAndBuilding( + storageId, + response.locals.building.id, + ['addedBy'], + ); + } else { + storageAccess = await this.storageService.getStorageByIdAndSub( + storageId, + user.sub, + ['addedBy'], + ); + } + + if (!storageAccess) return false; + response.locals.storage = storageAccess; + + return true; + } +}