mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-11-26 12:44:54 +00:00
space cleanup
This commit is contained in:
@ -17,11 +17,7 @@ import { ProductRepository } from '@app/common/modules/product/repositories';
|
||||
import { PropogateSubspaceHandler } from './handlers';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import { SpaceRepository } from '@app/common/modules/space';
|
||||
import {
|
||||
SubspaceProductItemRepository,
|
||||
SubspaceProductRepository,
|
||||
SubspaceRepository,
|
||||
} from '@app/common/modules/space/repositories/subspace.repository';
|
||||
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
|
||||
|
||||
const CommandHandlers = [PropogateSubspaceHandler];
|
||||
|
||||
@ -38,8 +34,6 @@ const CommandHandlers = [PropogateSubspaceHandler];
|
||||
SubspaceModelRepository,
|
||||
ProductRepository,
|
||||
SubspaceRepository,
|
||||
SubspaceProductRepository,
|
||||
SubspaceProductItemRepository,
|
||||
TagModelService,
|
||||
TagModelRepository,
|
||||
],
|
||||
|
||||
@ -12,35 +12,6 @@ import {
|
||||
} from 'class-validator';
|
||||
import { AddSubspaceDto } from './subspace';
|
||||
|
||||
export class CreateSpaceProductItemDto {
|
||||
@ApiProperty({
|
||||
description: 'Specific name for the product item',
|
||||
example: 'Light 1',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export class ProductAssignmentDto {
|
||||
@ApiProperty({
|
||||
description: 'UUID of the product to be assigned',
|
||||
example: 'prod-uuid-1234',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
productId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Specific names for each product item',
|
||||
type: [CreateSpaceProductItemDto],
|
||||
example: [{ tag: 'Light 1' }, { tag: 'Light 2' }, { tag: 'Light 3' }],
|
||||
})
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateSpaceProductItemDto)
|
||||
items: CreateSpaceProductItemDto[];
|
||||
}
|
||||
|
||||
export class AddSpaceDto {
|
||||
@ApiProperty({
|
||||
description: 'Name of the space (e.g., Floor 1, Unit 101)',
|
||||
@ -97,17 +68,6 @@ export class AddSpaceDto {
|
||||
@IsOptional()
|
||||
direction?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'List of products assigned to this space',
|
||||
type: [ProductAssignmentDto],
|
||||
required: false,
|
||||
})
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@IsOptional()
|
||||
@Type(() => ProductAssignmentDto)
|
||||
products?: ProductAssignmentDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'List of subspaces included in the model',
|
||||
type: [AddSubspaceDto],
|
||||
|
||||
@ -1,13 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { ProductAssignmentDto } from '../add.space.dto';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class AddSubspaceDto {
|
||||
@ApiProperty({
|
||||
@ -17,15 +9,4 @@ export class AddSubspaceDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
subspaceName: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'List of products assigned to this space',
|
||||
type: [ProductAssignmentDto],
|
||||
required: false,
|
||||
})
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@IsOptional()
|
||||
@Type(() => ProductAssignmentDto)
|
||||
products?: ProductAssignmentDto[];
|
||||
}
|
||||
|
||||
@ -4,6 +4,4 @@ export * from './space-device.service';
|
||||
export * from './subspace';
|
||||
export * from './space-link';
|
||||
export * from './space-scene.service';
|
||||
export * from './space-products';
|
||||
export * from './space-product-items';
|
||||
export * from './space-validation.service';
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from './space-product-items.service';
|
||||
@ -1,56 +0,0 @@
|
||||
import {
|
||||
SpaceEntity,
|
||||
SpaceProductEntity,
|
||||
SpaceProductItemRepository,
|
||||
} from '@app/common/modules/space';
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { CreateSpaceProductItemDto } from '../../dtos';
|
||||
import { QueryRunner } from 'typeorm';
|
||||
import { BaseProductItemService } from '../../common';
|
||||
|
||||
@Injectable()
|
||||
export class SpaceProductItemService extends BaseProductItemService {
|
||||
constructor(
|
||||
private readonly spaceProductItemRepository: SpaceProductItemRepository,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async createProductItem(
|
||||
itemModelDtos: CreateSpaceProductItemDto[],
|
||||
spaceProduct: SpaceProductEntity,
|
||||
space: SpaceEntity,
|
||||
queryRunner: QueryRunner,
|
||||
) {
|
||||
if (!itemModelDtos?.length) return;
|
||||
|
||||
const incomingTags = itemModelDtos.map((item) => item.tag);
|
||||
|
||||
await this.validateTags(incomingTags, queryRunner, space.uuid);
|
||||
|
||||
try {
|
||||
const productItems = itemModelDtos.map((dto) =>
|
||||
queryRunner.manager.create(this.spaceProductItemRepository.target, {
|
||||
tag: dto.tag,
|
||||
spaceProduct,
|
||||
}),
|
||||
);
|
||||
|
||||
await this.saveProductItems(
|
||||
productItems,
|
||||
this.spaceProductItemRepository.target,
|
||||
queryRunner,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
error.message ||
|
||||
'An unexpected error occurred while creating product items.',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from './space-products.service';
|
||||
@ -1,176 +0,0 @@
|
||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { ProductRepository } from '@app/common/modules/product/repositories';
|
||||
import { SpaceEntity } from '@app/common/modules/space/entities';
|
||||
import { SpaceProductEntity } from '@app/common/modules/space/entities/space-product.entity';
|
||||
import { In, QueryRunner } from 'typeorm';
|
||||
import { ProductAssignmentDto } from '../../dtos';
|
||||
import { SpaceProductItemService } from '../space-product-items';
|
||||
import { ProductEntity } from '@app/common/modules/product/entities';
|
||||
import { ProductService } from 'src/product/services';
|
||||
|
||||
@Injectable()
|
||||
export class SpaceProductService {
|
||||
constructor(
|
||||
private readonly productRepository: ProductRepository,
|
||||
private readonly spaceProductItemService: SpaceProductItemService,
|
||||
private readonly productService: ProductService,
|
||||
) {}
|
||||
|
||||
async assignProductsToSpace(
|
||||
space: SpaceEntity,
|
||||
products: ProductAssignmentDto[],
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<SpaceProductEntity[]> {
|
||||
let updatedProducts: SpaceProductEntity[] = [];
|
||||
|
||||
try {
|
||||
const uniqueProducts = this.validateUniqueProducts(products);
|
||||
const productEntities = await this.getProductEntities(uniqueProducts);
|
||||
const existingSpaceProducts = await this.getExistingSpaceProducts(
|
||||
space,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
if (existingSpaceProducts) {
|
||||
updatedProducts = await this.updateExistingProducts(
|
||||
existingSpaceProducts,
|
||||
uniqueProducts,
|
||||
productEntities,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
const newProducts = await this.createNewProducts(
|
||||
uniqueProducts,
|
||||
productEntities,
|
||||
space,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
return [...updatedProducts, ...newProducts];
|
||||
} catch (error) {
|
||||
if (!(error instanceof HttpException)) {
|
||||
throw new HttpException(
|
||||
`An error occurred while assigning products to the space ${error}`,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private validateUniqueProducts(
|
||||
products: ProductAssignmentDto[],
|
||||
): ProductAssignmentDto[] {
|
||||
const productIds = new Set();
|
||||
const uniqueProducts = [];
|
||||
|
||||
for (const product of products) {
|
||||
if (productIds.has(product.productId)) {
|
||||
throw new HttpException(
|
||||
`Duplicate product ID found: ${product.productId}`,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
productIds.add(product.productId);
|
||||
uniqueProducts.push(product);
|
||||
}
|
||||
|
||||
return uniqueProducts;
|
||||
}
|
||||
|
||||
private async getProductEntities(
|
||||
products: ProductAssignmentDto[],
|
||||
): Promise<Map<string, any>> {
|
||||
try {
|
||||
const productIds = products.map((p) => p.productId);
|
||||
const productEntities = await this.productRepository.find({
|
||||
where: { uuid: In(productIds) },
|
||||
});
|
||||
return new Map(productEntities.map((p) => [p.uuid, p]));
|
||||
} catch (error) {
|
||||
console.error('Error fetching product entities:', error);
|
||||
throw new HttpException(
|
||||
'Failed to fetch product entities',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async getExistingSpaceProducts(
|
||||
space: SpaceEntity,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<SpaceProductEntity[]> {
|
||||
return queryRunner.manager.find(SpaceProductEntity, {
|
||||
where: { space: { uuid: space.uuid } },
|
||||
relations: ['product'],
|
||||
});
|
||||
}
|
||||
|
||||
private async updateExistingProducts(
|
||||
existingSpaceProducts: SpaceProductEntity[],
|
||||
uniqueProducts: ProductAssignmentDto[],
|
||||
productEntities: Map<string, any>,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<SpaceProductEntity[]> {
|
||||
const updatedProducts = [];
|
||||
|
||||
for (const { productId } of uniqueProducts) {
|
||||
productEntities.get(productId);
|
||||
const existingProduct = existingSpaceProducts.find(
|
||||
(spaceProduct) => spaceProduct.product.uuid === productId,
|
||||
);
|
||||
|
||||
updatedProducts.push(existingProduct);
|
||||
}
|
||||
|
||||
if (updatedProducts.length > 0) {
|
||||
await queryRunner.manager.save(SpaceProductEntity, updatedProducts);
|
||||
}
|
||||
|
||||
return updatedProducts;
|
||||
}
|
||||
|
||||
private async createNewProducts(
|
||||
uniqueSpaceProducts: ProductAssignmentDto[],
|
||||
productEntities: Map<string, any>,
|
||||
space: SpaceEntity,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<SpaceProductEntity[]> {
|
||||
const newProducts = [];
|
||||
|
||||
for (const uniqueSpaceProduct of uniqueSpaceProducts) {
|
||||
const product = productEntities.get(uniqueSpaceProduct.productId);
|
||||
await this.getProduct(uniqueSpaceProduct.productId);
|
||||
|
||||
newProducts.push(
|
||||
queryRunner.manager.create(SpaceProductEntity, {
|
||||
space,
|
||||
product,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (newProducts.length > 0) {
|
||||
await queryRunner.manager.save(SpaceProductEntity, newProducts);
|
||||
|
||||
await Promise.all(
|
||||
uniqueSpaceProducts.map((dto, index) => {
|
||||
const spaceProduct = newProducts[index];
|
||||
return this.spaceProductItemService.createProductItem(
|
||||
dto.items,
|
||||
spaceProduct,
|
||||
space,
|
||||
queryRunner,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return newProducts;
|
||||
}
|
||||
|
||||
async getProduct(productId: string): Promise<ProductEntity> {
|
||||
const product = await this.productService.findOne(productId);
|
||||
return product.data;
|
||||
}
|
||||
}
|
||||
@ -17,7 +17,6 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import { SpaceEntity } from '@app/common/modules/space/entities';
|
||||
import { generateRandomString } from '@app/common/helper/randomString';
|
||||
import { SpaceLinkService } from './space-link';
|
||||
import { SpaceProductService } from './space-products';
|
||||
import { CreateSubspaceModelDto } from 'src/space-model/dtos';
|
||||
import { SubSpaceService } from './subspace';
|
||||
import { DataSource, Not } from 'typeorm';
|
||||
@ -30,7 +29,6 @@ export class SpaceService {
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly spaceRepository: SpaceRepository,
|
||||
private readonly spaceLinkService: SpaceLinkService,
|
||||
private readonly spaceProductService: SpaceProductService,
|
||||
private readonly subSpaceService: SubSpaceService,
|
||||
private readonly validationService: ValidationService,
|
||||
) {}
|
||||
@ -102,13 +100,6 @@ export class SpaceService {
|
||||
);
|
||||
}
|
||||
|
||||
if (products && products.length > 0) {
|
||||
await this.spaceProductService.assignProductsToSpace(
|
||||
newSpace,
|
||||
products,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return new SuccessResponseDto({
|
||||
@ -264,14 +255,9 @@ export class SpaceService {
|
||||
Object.assign(space, updateSpaceDto, { parent });
|
||||
|
||||
// Save the updated space
|
||||
const updatedSpace = await queryRunner.manager.save(space);
|
||||
await queryRunner.manager.save(space);
|
||||
|
||||
if (products && products.length > 0) {
|
||||
await this.spaceProductService.assignProductsToSpace(
|
||||
updatedSpace,
|
||||
products,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
|
||||
@ -1,4 +1,2 @@
|
||||
export * from './subspace.service';
|
||||
export * from './subspace-device.service';
|
||||
export * from './subspace-product-item.service';
|
||||
export * from './subspace-product.service';
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import {
|
||||
SpaceEntity,
|
||||
SubspaceProductEntity,
|
||||
SubspaceProductItemEntity,
|
||||
} from '@app/common/modules/space';
|
||||
import { SubspaceProductItemRepository } from '@app/common/modules/space/repositories/subspace.repository';
|
||||
import { CreateSpaceProductItemDto } from '../../dtos';
|
||||
import { BaseProductItemService } from '../../common';
|
||||
|
||||
@Injectable()
|
||||
export class SubspaceProductItemService extends BaseProductItemService {
|
||||
constructor(
|
||||
private readonly productItemRepository: SubspaceProductItemRepository,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async createItemFromDtos(
|
||||
product: SubspaceProductEntity,
|
||||
itemDto: CreateSpaceProductItemDto[],
|
||||
queryRunner: QueryRunner,
|
||||
space: SpaceEntity,
|
||||
) {
|
||||
if (!itemDto?.length) return;
|
||||
const incomingTags = itemDto.map((item) => item.tag);
|
||||
await this.validateTags(incomingTags, queryRunner, space.uuid);
|
||||
|
||||
try {
|
||||
const productItems = itemDto.map((dto) =>
|
||||
queryRunner.manager.create(SubspaceProductItemEntity, {
|
||||
tag: dto.tag,
|
||||
subspaceProduct: product,
|
||||
}),
|
||||
);
|
||||
|
||||
await queryRunner.manager.save(
|
||||
this.productItemRepository.target,
|
||||
productItems,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message || 'An error occurred while creating product items.',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import {
|
||||
SpaceEntity,
|
||||
SubspaceEntity,
|
||||
SubspaceProductEntity,
|
||||
} from '@app/common/modules/space';
|
||||
import { SubspaceProductItemService } from './subspace-product-item.service';
|
||||
import { ProductAssignmentDto } from 'src/space/dtos';
|
||||
import { ProductService } from 'src/product/services';
|
||||
import { ProductEntity } from '@app/common/modules/product/entities';
|
||||
|
||||
@Injectable()
|
||||
export class SubspaceProductService {
|
||||
constructor(
|
||||
private readonly subspaceProductItemService: SubspaceProductItemService,
|
||||
private readonly productService: ProductService,
|
||||
) {}
|
||||
|
||||
async createFromDto(
|
||||
productDtos: ProductAssignmentDto[],
|
||||
subspace: SubspaceEntity,
|
||||
queryRunner: QueryRunner,
|
||||
space: SpaceEntity,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const newSpaceProducts = await Promise.all(
|
||||
productDtos.map(async (dto) => {
|
||||
const product = await this.getProduct(dto.productId);
|
||||
return queryRunner.manager.create(SubspaceProductEntity, {
|
||||
subspace,
|
||||
product,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const subspaceProducts = await queryRunner.manager.save(
|
||||
SubspaceProductEntity,
|
||||
newSpaceProducts,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
productDtos.map((dto, index) =>
|
||||
this.subspaceProductItemService.createItemFromDtos(
|
||||
subspaceProducts[index],
|
||||
dto.items,
|
||||
queryRunner,
|
||||
space,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
`Failed to create subspace products from DTOs. Error: ${error.message}`,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getProduct(productId: string): Promise<ProductEntity> {
|
||||
const product = await this.productService.findOne(productId);
|
||||
return product.data;
|
||||
}
|
||||
}
|
||||
@ -19,14 +19,12 @@ import {
|
||||
} from '@app/common/modules/space-model';
|
||||
import { ValidationService } from '../space-validation.service';
|
||||
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
|
||||
import { SubspaceProductService } from './subspace-product.service';
|
||||
|
||||
@Injectable()
|
||||
export class SubSpaceService {
|
||||
constructor(
|
||||
private readonly subspaceRepository: SubspaceRepository,
|
||||
private readonly validationService: ValidationService,
|
||||
private readonly productService: SubspaceProductService,
|
||||
) {}
|
||||
|
||||
async createSubspaces(
|
||||
@ -82,17 +80,6 @@ export class SubSpaceService {
|
||||
|
||||
const subspaces = await this.createSubspaces(subspaceData, queryRunner);
|
||||
|
||||
await Promise.all(
|
||||
addSubspaceDtos.map((dto, index) =>
|
||||
this.productService.createFromDto(
|
||||
dto.products,
|
||||
subspaces[index],
|
||||
queryRunner,
|
||||
space,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return subspaces;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
|
||||
@ -12,20 +12,15 @@ import {
|
||||
import {
|
||||
SpaceDeviceService,
|
||||
SpaceLinkService,
|
||||
SpaceProductItemService,
|
||||
SpaceProductService,
|
||||
SpaceSceneService,
|
||||
SpaceService,
|
||||
SpaceUserService,
|
||||
SubspaceDeviceService,
|
||||
SubspaceProductItemService,
|
||||
SubSpaceService,
|
||||
} from './services';
|
||||
import {
|
||||
SpaceProductRepository,
|
||||
SpaceRepository,
|
||||
SpaceLinkRepository,
|
||||
SpaceProductItemRepository,
|
||||
} from '@app/common/modules/space/repositories';
|
||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||
import {
|
||||
@ -48,12 +43,7 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||
import { SpaceModelRepository } from '@app/common/modules/space-model';
|
||||
import { CommunityModule } from 'src/community/community.module';
|
||||
import { ValidationService } from './services';
|
||||
import {
|
||||
SubspaceProductItemRepository,
|
||||
SubspaceProductRepository,
|
||||
SubspaceRepository,
|
||||
} from '@app/common/modules/space/repositories/subspace.repository';
|
||||
import { SubspaceProductService } from './services';
|
||||
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, SpaceRepositoryModule, CommunityModule],
|
||||
@ -75,9 +65,9 @@ import { SubspaceProductService } from './services';
|
||||
SpaceLinkService,
|
||||
SubspaceDeviceService,
|
||||
SpaceRepository,
|
||||
SubspaceRepository,
|
||||
DeviceRepository,
|
||||
CommunityRepository,
|
||||
SubspaceRepository,
|
||||
SpaceLinkRepository,
|
||||
UserSpaceRepository,
|
||||
UserRepository,
|
||||
@ -88,19 +78,11 @@ import { SubspaceProductService } from './services';
|
||||
SceneRepository,
|
||||
DeviceService,
|
||||
DeviceStatusFirebaseService,
|
||||
SubspaceProductItemRepository,
|
||||
DeviceStatusLogRepository,
|
||||
SceneDeviceRepository,
|
||||
SpaceProductService,
|
||||
SpaceProductRepository,
|
||||
|
||||
ProjectRepository,
|
||||
SpaceModelRepository,
|
||||
SubspaceRepository,
|
||||
SpaceProductItemService,
|
||||
SpaceProductItemRepository,
|
||||
SubspaceProductService,
|
||||
SubspaceProductItemService,
|
||||
SubspaceProductRepository,
|
||||
],
|
||||
exports: [SpaceService],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user