storages and storage sets

This commit is contained in:
Evert Prants 2023-01-12 22:24:09 +02:00
parent e79e09065c
commit 2b420831ac
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
23 changed files with 937 additions and 2 deletions

View File

@ -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<StorageResponseDto> {
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<StorageResponseDto> {
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<StorageSetResponseDto> {
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<StorageSetResponseDto> {
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<StorageSetResponseDto> {
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<StorageResponseDto> {
return this.service.createStorage(user, room, body);
}
}

View File

@ -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 {}

View File

@ -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))
: [],
};
}
}

View File

@ -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,
) {}

View File

@ -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;
}

View File

@ -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,
) {}

View File

@ -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[];
}

View File

@ -3,6 +3,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AppBuildingModule } from './app-building/app-building.module'; import { AppBuildingModule } from './app-building/app-building.module';
import { AppGroupModule } from './app-group/app-group.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 { AppUser } from './app-user/app-user.module';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
@ -42,6 +43,7 @@ import { SecretsModule } from './shared/secrets/secrets.module';
AppUser, AppUser,
AppGroupModule, AppGroupModule,
AppBuildingModule, AppBuildingModule,
AppStorageModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],

View File

@ -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( async getRoomsOnFloorByBuildingAndUserSub(
buildingId: number, buildingId: number,
floorNumber: number, floorNumber: number,

View File

@ -1,3 +1,4 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { User } from 'src/objects/user/user.entity'; import { User } from 'src/objects/user/user.entity';
import { import {
Column, Column,
@ -12,39 +13,49 @@ import { StoredItem } from './stored-item.entity';
@Entity() @Entity()
export class StoredItemTransaction { export class StoredItemTransaction {
@ApiProperty()
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ApiProperty({ type: String, enum: TransactionType })
@Column({ type: String, default: TransactionType.ACQUIRED }) @Column({ type: String, default: TransactionType.ACQUIRED })
type: TransactionType; type: TransactionType;
@ApiPropertyOptional()
@Column({ nullable: true }) @Column({ nullable: true })
price?: number; price?: number;
@ApiPropertyOptional()
@Column({ nullable: true, default: 'EUR', length: 3 }) @Column({ nullable: true, default: 'EUR', length: 3 })
currency?: string; currency?: string;
@ApiProperty({ type: () => StoredItem })
@ManyToOne(() => StoredItem, { @ManyToOne(() => StoredItem, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
storedItem: StoredItem; storedItem: StoredItem;
@ApiPropertyOptional({ type: () => User })
@ManyToOne(() => User, { @ManyToOne(() => User, {
onDelete: 'SET NULL', onDelete: 'SET NULL',
nullable: true, nullable: true,
}) })
actor?: User; actor?: User;
@ApiPropertyOptional()
@Column({ nullable: true }) @Column({ nullable: true })
notes?: string; notes?: string;
@ApiPropertyOptional()
@Column({ type: 'datetime', nullable: true }) @Column({ type: 'datetime', nullable: true })
actionAt?: Date; actionAt?: Date;
@ApiProperty()
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt: Date;
@ApiProperty()
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; updatedAt: Date;
} }

View File

@ -1,3 +1,4 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { User } from 'src/objects/user/user.entity'; import { User } from 'src/objects/user/user.entity';
import { import {
Column, Column,
@ -13,48 +14,62 @@ import { StoredItem } from './stored-item.entity';
@Entity() @Entity()
export class Item { export class Item {
@ApiProperty()
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ApiProperty()
@Column() @Column()
displayName: string; displayName: string;
@ApiProperty({ type: String, enum: ItemType })
@Column({ type: String, default: ItemType.ITEM }) @Column({ type: String, default: ItemType.ITEM })
type: ItemType; type: ItemType;
@ApiPropertyOptional()
@Column({ nullable: true }) @Column({ nullable: true })
barcode?: string; barcode?: string;
@ApiPropertyOptional()
@Column({ default: false }) @Column({ default: false })
consumable: boolean; consumable: boolean;
@ApiPropertyOptional()
@Column({ nullable: true }) @Column({ nullable: true })
image?: string; image?: string;
@ApiPropertyOptional()
@Column({ nullable: true }) @Column({ nullable: true })
weight?: number; weight?: number;
@ApiPropertyOptional()
@Column({ nullable: true }) @Column({ nullable: true })
url?: string; url?: string;
@ApiPropertyOptional()
@Column({ nullable: true }) @Column({ nullable: true })
notes?: string; notes?: string;
@ApiPropertyOptional({ type: () => StoredItem, isArray: true })
@OneToMany(() => StoredItem, (store) => store.item) @OneToMany(() => StoredItem, (store) => store.item)
instances?: StoredItem[]; instances?: StoredItem[];
@ApiProperty({ type: () => User })
@ManyToOne(() => User, { @ManyToOne(() => User, {
onDelete: 'SET NULL', onDelete: 'SET NULL',
nullable: true, nullable: true,
}) })
addedBy: User; addedBy: User;
@ApiProperty()
@Column({ default: false }) @Column({ default: false })
public: boolean; public: boolean;
@ApiProperty()
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt: Date;
@ApiProperty()
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; updatedAt: Date;
} }

View File

@ -1,3 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { Room } from 'src/objects/building/entities/room.entity'; import { Room } from 'src/objects/building/entities/room.entity';
import { User } from 'src/objects/user/user.entity'; import { User } from 'src/objects/user/user.entity';
import { import {
@ -14,42 +15,53 @@ import { Storage } from './storage.entity';
@Entity() @Entity()
export class StorageSet { export class StorageSet {
@ApiProperty()
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ApiProperty()
@Column() @Column()
displayName: string; displayName: string;
@ApiProperty({ type: () => Room })
@ManyToOne(() => Room, { @ManyToOne(() => Room, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
room: Room; room: Room;
@ApiProperty({ type: String, enum: StorageSetType })
@Column({ type: String }) @Column({ type: String })
type: StorageSetType; type: StorageSetType;
@ApiProperty()
@Column() @Column()
location: string; location: string;
@ApiProperty()
@Column() @Column()
locationDescription: string; locationDescription: string;
@ApiProperty()
@Column() @Column()
color: string; color: string;
@ApiProperty({ type: () => Storage, isArray: true })
@OneToMany(() => Storage, (storage) => storage.set) @OneToMany(() => Storage, (storage) => storage.set)
storages: Storage[]; storages: Storage[];
@ApiProperty({ type: () => User })
@ManyToOne(() => User, { @ManyToOne(() => User, {
onDelete: 'SET NULL', onDelete: 'SET NULL',
nullable: true, nullable: true,
}) })
addedBy: User; addedBy: User;
@ApiProperty()
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt: Date;
@ApiProperty()
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; updatedAt: Date;
} }

View File

@ -1,3 +1,4 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Room } from 'src/objects/building/entities/room.entity'; import { Room } from 'src/objects/building/entities/room.entity';
import { User } from 'src/objects/user/user.entity'; import { User } from 'src/objects/user/user.entity';
import { import {
@ -15,30 +16,38 @@ import { StoredItem } from './stored-item.entity';
@Entity() @Entity()
export class Storage { export class Storage {
@ApiProperty()
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ApiProperty()
@Column() @Column()
displayName: string; displayName: string;
@ApiProperty({ type: () => Room })
@ManyToOne(() => Room, { @ManyToOne(() => Room, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
room: Room; room: Room;
@ApiProperty({ type: String, enum: StorageType })
@Column({ type: String }) @Column({ type: String })
type: StorageType; type: StorageType;
@ApiProperty()
@Column() @Column()
location: string; location: string;
@Column() @ApiPropertyOptional()
locationDescription: string; @Column({ nullable: true })
locationDescription?: string;
@ApiProperty({ type: () => StoredItem, isArray: true })
@OneToMany(() => StoredItem, (item) => item.storage) @OneToMany(() => StoredItem, (item) => item.storage)
items: StoredItem[]; items: StoredItem[];
@ApiProperty({ type: () => StorageSet })
@ManyToOne(() => StorageSet, { @ManyToOne(() => StorageSet, {
nullable: true, nullable: true,
onDelete: 'SET NULL', onDelete: 'SET NULL',
@ -46,18 +55,22 @@ export class Storage {
}) })
set?: StorageSet; set?: StorageSet;
@ApiProperty()
@Column() @Column()
color: string; color: string;
@ApiProperty({ type: () => User })
@ManyToOne(() => User, { @ManyToOne(() => User, {
onDelete: 'SET NULL', onDelete: 'SET NULL',
nullable: true, nullable: true,
}) })
addedBy: User; addedBy: User;
@ApiProperty()
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt: Date;
@ApiProperty()
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; updatedAt: Date;
} }

View File

@ -1,29 +1,36 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { User } from 'src/objects/user/user.entity'; import { User } from 'src/objects/user/user.entity';
import { import {
Column, Column,
CreateDateColumn, CreateDateColumn,
Entity, Entity,
ManyToOne, ManyToOne,
OneToMany,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { StoredItemTransaction } from './item-transaction.entity';
import { Item } from './item.entity'; import { Item } from './item.entity';
import { Storage } from './storage.entity'; import { Storage } from './storage.entity';
@Entity() @Entity()
export class StoredItem { export class StoredItem {
@ApiProperty()
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ApiProperty()
@Column() @Column()
displayName: string; displayName: string;
@ApiProperty({ type: () => Item })
@ManyToOne(() => Item, { @ManyToOne(() => Item, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
item: Item; item: Item;
@ApiPropertyOptional({ type: () => Storage })
@ManyToOne(() => Storage, { @ManyToOne(() => Storage, {
nullable: true, nullable: true,
onDelete: 'SET NULL', onDelete: 'SET NULL',
@ -31,27 +38,41 @@ export class StoredItem {
}) })
storage?: Storage; storage?: Storage;
@ApiPropertyOptional()
@Column({ nullable: true }) @Column({ nullable: true })
notes?: string; notes?: string;
@ApiProperty({ type: () => User })
@ManyToOne(() => User, { @ManyToOne(() => User, {
onDelete: 'SET NULL', onDelete: 'SET NULL',
nullable: true, nullable: true,
}) })
addedBy: User; addedBy: User;
@ApiPropertyOptional({ type: () => StoredItemTransaction, isArray: true })
@OneToMany(
() => StoredItemTransaction,
(transaction) => transaction.storedItem,
)
transactions?: StoredItemTransaction[];
@ApiPropertyOptional()
@Column({ nullable: true, type: 'datetime' }) @Column({ nullable: true, type: 'datetime' })
expiresAt?: Date; expiresAt?: Date;
@ApiPropertyOptional()
@Column({ nullable: true, type: 'datetime' }) @Column({ nullable: true, type: 'datetime' })
acquiredAt?: Date; acquiredAt?: Date;
@ApiPropertyOptional()
@Column({ nullable: true, type: 'datetime' }) @Column({ nullable: true, type: 'datetime' })
consumedAt?: Date; consumedAt?: Date;
@ApiProperty()
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt: Date;
@ApiProperty()
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; updatedAt: Date;
} }

View File

@ -27,4 +27,163 @@ export class StorageService {
private readonly userService: UserService, private readonly userService: UserService,
private readonly buildingService: BuildingService, 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<Storage>) {
const newStorage = new Storage();
Object.assign(newStorage, data);
return this.storageRepository.save(newStorage);
}
async saveItem(data: Partial<Item>) {
const newItem = new Item();
Object.assign(newItem, data);
return this.itemRepository.save(newItem);
}
async saveStoredItem(data: Partial<StoredItem>) {
const newStoredItem = new StoredItem();
Object.assign(newStoredItem, data);
return this.storedItemRepository.save(newStoredItem);
}
async saveStorageSet(data: Partial<StorageSet>) {
const newStorageSet = new StorageSet();
Object.assign(newStorageSet, data);
return this.storageSetRepository.save(newStorageSet);
}
async saveStoredItemTransaction(data: Partial<StoredItemTransaction>) {
const newStoredItemTransaction = new StoredItemTransaction();
Object.assign(newStoredItemTransaction, data);
return this.transactionRepository.save(newStoredItemTransaction);
}
} }

View File

@ -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;
},
);

View File

@ -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;
},
);

View File

@ -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;
},
);

View File

@ -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;
},
);

View File

@ -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<boolean> {
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;
}
}

View File

@ -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<boolean> {
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;
}
}

View File

@ -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<boolean> {
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;
}
}

View File

@ -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<boolean> {
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;
}
}