mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-08-26 10:09:39 +00:00
Compare commits
1 Commits
331c8dffdc
...
task/add-a
Author | SHA1 | Date | |
---|---|---|---|
dcdb10a1fb |
@ -188,11 +188,6 @@ export class ControllerRoute {
|
|||||||
static SCENE = class {
|
static SCENE = class {
|
||||||
public static readonly ROUTE = 'scene';
|
public static readonly ROUTE = 'scene';
|
||||||
static ACTIONS = class {
|
static ACTIONS = class {
|
||||||
public static readonly GET_TAP_TO_RUN_SCENES_SUMMARY =
|
|
||||||
'Get Tap-to-Run Scenes by spaces';
|
|
||||||
public static readonly GET_TAP_TO_RUN_SCENES_DESCRIPTION =
|
|
||||||
'Gets Tap-to-Run scenes by spaces';
|
|
||||||
|
|
||||||
public static readonly CREATE_TAP_TO_RUN_SCENE_SUMMARY =
|
public static readonly CREATE_TAP_TO_RUN_SCENE_SUMMARY =
|
||||||
'Create a Tap-to-Run Scene';
|
'Create a Tap-to-Run Scene';
|
||||||
public static readonly CREATE_TAP_TO_RUN_SCENE_DESCRIPTION =
|
public static readonly CREATE_TAP_TO_RUN_SCENE_DESCRIPTION =
|
||||||
@ -777,10 +772,6 @@ export class ControllerRoute {
|
|||||||
public static readonly ADD_AUTOMATION_DESCRIPTION =
|
public static readonly ADD_AUTOMATION_DESCRIPTION =
|
||||||
'This endpoint creates a new automation based on the provided details.';
|
'This endpoint creates a new automation based on the provided details.';
|
||||||
|
|
||||||
public static readonly GET_AUTOMATION_SUMMARY = 'Get all automations';
|
|
||||||
public static readonly GET_AUTOMATION_DESCRIPTION =
|
|
||||||
'This endpoint retrieves automations data';
|
|
||||||
|
|
||||||
public static readonly GET_AUTOMATION_DETAILS_SUMMARY =
|
public static readonly GET_AUTOMATION_DETAILS_SUMMARY =
|
||||||
'Get automation details';
|
'Get automation details';
|
||||||
public static readonly GET_AUTOMATION_DETAILS_DESCRIPTION =
|
public static readonly GET_AUTOMATION_DETAILS_DESCRIPTION =
|
||||||
|
3
libs/common/src/constants/timer-job-type.enum.ts
Normal file
3
libs/common/src/constants/timer-job-type.enum.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export enum TimerJobTypeEnum {
|
||||||
|
INVITE_USER_EMAIL = 'INVITE_USER_EMAIL',
|
||||||
|
}
|
@ -54,6 +54,7 @@ import { SpaceEntity } from '../modules/space/entities/space.entity';
|
|||||||
import { SubspaceProductAllocationEntity } from '../modules/space/entities/subspace/subspace-product-allocation.entity';
|
import { SubspaceProductAllocationEntity } from '../modules/space/entities/subspace/subspace-product-allocation.entity';
|
||||||
import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity';
|
import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity';
|
||||||
import { NewTagEntity } from '../modules/tag/entities/tag.entity';
|
import { NewTagEntity } from '../modules/tag/entities/tag.entity';
|
||||||
|
import { TimerEntity } from '../modules/timer/entities/timer.entity';
|
||||||
import { TimeZoneEntity } from '../modules/timezone/entities';
|
import { TimeZoneEntity } from '../modules/timezone/entities';
|
||||||
import {
|
import {
|
||||||
UserNotificationEntity,
|
UserNotificationEntity,
|
||||||
@ -121,6 +122,7 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
|
|||||||
SpaceDailyOccupancyDurationEntity,
|
SpaceDailyOccupancyDurationEntity,
|
||||||
BookableSpaceEntity,
|
BookableSpaceEntity,
|
||||||
BookingEntity,
|
BookingEntity,
|
||||||
|
TimerEntity,
|
||||||
],
|
],
|
||||||
namingStrategy: new SnakeNamingStrategy(),
|
namingStrategy: new SnakeNamingStrategy(),
|
||||||
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
||||||
|
@ -15,17 +15,16 @@ import { ProjectEntity } from '../../project/entities';
|
|||||||
import { RoleTypeEntity } from '../../role-type/entities';
|
import { RoleTypeEntity } from '../../role-type/entities';
|
||||||
import { SpaceEntity } from '../../space/entities/space.entity';
|
import { SpaceEntity } from '../../space/entities/space.entity';
|
||||||
import { UserEntity } from '../../user/entities';
|
import { UserEntity } from '../../user/entities';
|
||||||
import { InviteUserDto, InviteUserSpaceDto } from '../dtos';
|
|
||||||
|
|
||||||
@Entity({ name: 'invite-user' })
|
@Entity({ name: 'invite-user' })
|
||||||
@Unique(['email', 'project'])
|
@Unique(['email', 'project'])
|
||||||
export class InviteUserEntity extends AbstractEntity<InviteUserDto> {
|
export class InviteUserEntity extends AbstractEntity {
|
||||||
@Column({
|
@Column({
|
||||||
type: 'uuid',
|
type: 'uuid',
|
||||||
default: () => 'gen_random_uuid()',
|
default: () => 'gen_random_uuid()',
|
||||||
nullable: false,
|
nullable: false,
|
||||||
})
|
})
|
||||||
public uuid: string;
|
uuid: string;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
nullable: false,
|
nullable: false,
|
||||||
@ -49,50 +48,67 @@ export class InviteUserEntity extends AbstractEntity<InviteUserDto> {
|
|||||||
status: string;
|
status: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
public firstName: string;
|
firstName: string;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
nullable: false,
|
nullable: false,
|
||||||
})
|
})
|
||||||
public lastName: string;
|
lastName: string;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
public phoneNumber: string;
|
phoneNumber: string;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
nullable: false,
|
nullable: false,
|
||||||
default: true,
|
default: true,
|
||||||
})
|
})
|
||||||
public isActive: boolean;
|
isActive: boolean;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
nullable: false,
|
nullable: false,
|
||||||
default: true,
|
default: true,
|
||||||
})
|
})
|
||||||
public isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
nullable: false,
|
nullable: false,
|
||||||
unique: true,
|
unique: true,
|
||||||
})
|
})
|
||||||
public invitationCode: string;
|
invitationCode: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
default: new Date(),
|
||||||
|
type: 'date',
|
||||||
|
})
|
||||||
|
accessStartDate: Date;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'date',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
accessEndDate?: Date;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
nullable: false,
|
nullable: false,
|
||||||
enum: Object.values(RoleType),
|
enum: Object.values(RoleType),
|
||||||
})
|
})
|
||||||
public invitedBy: string;
|
invitedBy: string;
|
||||||
|
|
||||||
@ManyToOne(() => RoleTypeEntity, (roleType) => roleType.invitedUsers, {
|
@ManyToOne(() => RoleTypeEntity, (roleType) => roleType.invitedUsers, {
|
||||||
nullable: false,
|
nullable: false,
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
public roleType: RoleTypeEntity;
|
roleType: RoleTypeEntity;
|
||||||
|
|
||||||
@OneToOne(() => UserEntity, (user) => user.inviteUser, {
|
@OneToOne(() => UserEntity, (user) => user.inviteUser, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
@JoinColumn({ name: 'user_uuid' })
|
@JoinColumn({ name: 'user_uuid' })
|
||||||
user: UserEntity;
|
user: UserEntity;
|
||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => InviteUserSpaceEntity,
|
() => InviteUserSpaceEntity,
|
||||||
(inviteUserSpace) => inviteUserSpace.inviteUser,
|
(inviteUserSpace) => inviteUserSpace.inviteUser,
|
||||||
@ -103,32 +119,34 @@ export class InviteUserEntity extends AbstractEntity<InviteUserDto> {
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
@JoinColumn({ name: 'project_uuid' })
|
@JoinColumn({ name: 'project_uuid' })
|
||||||
public project: ProjectEntity;
|
project: ProjectEntity;
|
||||||
|
|
||||||
constructor(partial: Partial<InviteUserEntity>) {
|
constructor(partial: Partial<InviteUserEntity>) {
|
||||||
super();
|
super();
|
||||||
Object.assign(this, partial);
|
Object.assign(this, partial);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity({ name: 'invite-user-space' })
|
@Entity({ name: 'invite-user-space' })
|
||||||
@Unique(['inviteUser', 'space'])
|
@Unique(['inviteUser', 'space'])
|
||||||
export class InviteUserSpaceEntity extends AbstractEntity<InviteUserSpaceDto> {
|
export class InviteUserSpaceEntity extends AbstractEntity {
|
||||||
@Column({
|
@Column({
|
||||||
type: 'uuid',
|
type: 'uuid',
|
||||||
default: () => 'gen_random_uuid()',
|
default: () => 'gen_random_uuid()',
|
||||||
nullable: false,
|
nullable: false,
|
||||||
})
|
})
|
||||||
public uuid: string;
|
uuid: string;
|
||||||
|
|
||||||
@ManyToOne(() => InviteUserEntity, (inviteUser) => inviteUser.spaces, {
|
@ManyToOne(() => InviteUserEntity, (inviteUser) => inviteUser.spaces, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
@JoinColumn({ name: 'invite_user_uuid' })
|
@JoinColumn({ name: 'invite_user_uuid' })
|
||||||
public inviteUser: InviteUserEntity;
|
inviteUser: InviteUserEntity;
|
||||||
|
|
||||||
@ManyToOne(() => SpaceEntity, (space) => space.invitedUsers)
|
@ManyToOne(() => SpaceEntity, (space) => space.invitedUsers)
|
||||||
@JoinColumn({ name: 'space_uuid' })
|
@JoinColumn({ name: 'space_uuid' })
|
||||||
public space: SpaceEntity;
|
space: SpaceEntity;
|
||||||
|
|
||||||
constructor(partial: Partial<InviteUserSpaceEntity>) {
|
constructor(partial: Partial<InviteUserSpaceEntity>) {
|
||||||
super();
|
super();
|
||||||
Object.assign(this, partial);
|
Object.assign(this, partial);
|
||||||
|
37
libs/common/src/modules/timer/entities/timer.entity.ts
Normal file
37
libs/common/src/modules/timer/entities/timer.entity.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { TimerJobTypeEnum } from '@app/common/constants/timer-job-type.enum';
|
||||||
|
import { Column, Entity } from 'typeorm';
|
||||||
|
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'timer' })
|
||||||
|
export class TimerEntity extends AbstractEntity {
|
||||||
|
@Column({
|
||||||
|
type: 'uuid',
|
||||||
|
default: () => 'gen_random_uuid()',
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
uuid: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
nullable: false,
|
||||||
|
enum: Object.values(TimerJobTypeEnum),
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
type: TimerJobTypeEnum;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
nullable: false,
|
||||||
|
type: 'date',
|
||||||
|
})
|
||||||
|
triggerDate: Date;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'jsonb',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
|
||||||
|
constructor(partial: Partial<TimerEntity>) {
|
||||||
|
super();
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { DataSource, Repository } from 'typeorm';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { TimerEntity } from '../entities/timer.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TimerRepository extends Repository<TimerEntity> {
|
||||||
|
constructor(private dataSource: DataSource) {
|
||||||
|
super(TimerEntity, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
}
|
12
libs/common/src/modules/timer/timer.repository.module.ts
Normal file
12
libs/common/src/modules/timer/timer.repository.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { TimerEntity } from './entities/timer.entity';
|
||||||
|
import { TimerRepository } from './repositories/timer.repository';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([TimerEntity])],
|
||||||
|
providers: [TimerRepository],
|
||||||
|
exports: [TimerRepository],
|
||||||
|
})
|
||||||
|
export class TimerRepositoryModule {}
|
@ -11,6 +11,7 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { OtpType } from '../../../../src/constants/otp-type.enum';
|
import { OtpType } from '../../../../src/constants/otp-type.enum';
|
||||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||||
|
import { BookingEntity } from '../../booking/entities/booking.entity';
|
||||||
import { ClientEntity } from '../../client/entities';
|
import { ClientEntity } from '../../client/entities';
|
||||||
import {
|
import {
|
||||||
DeviceNotificationEntity,
|
DeviceNotificationEntity,
|
||||||
@ -29,7 +30,6 @@ import {
|
|||||||
UserOtpDto,
|
UserOtpDto,
|
||||||
UserSpaceDto,
|
UserSpaceDto,
|
||||||
} from '../dtos';
|
} from '../dtos';
|
||||||
import { BookingEntity } from '../../booking/entities/booking.entity';
|
|
||||||
|
|
||||||
@Entity({ name: 'user' })
|
@Entity({ name: 'user' })
|
||||||
export class UserEntity extends AbstractEntity<UserDto> {
|
export class UserEntity extends AbstractEntity<UserDto> {
|
||||||
@ -101,6 +101,9 @@ export class UserEntity extends AbstractEntity<UserDto> {
|
|||||||
@Column({ type: 'timestamp', nullable: true })
|
@Column({ type: 'timestamp', nullable: true })
|
||||||
appAgreementAcceptedAt: Date;
|
appAgreementAcceptedAt: Date;
|
||||||
|
|
||||||
|
@Column({ type: Boolean, default: false })
|
||||||
|
bookingEnabled: boolean;
|
||||||
|
|
||||||
@OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user, {
|
@OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
|
@ -35,16 +35,18 @@ import { UserNotificationModule } from './user-notification/user-notification.mo
|
|||||||
import { UserModule } from './users/user.module';
|
import { UserModule } from './users/user.module';
|
||||||
import { VisitorPasswordModule } from './vistor-password/visitor-password.module';
|
import { VisitorPasswordModule } from './vistor-password/visitor-password.module';
|
||||||
|
|
||||||
|
import { TimerRepositoryModule } from '@app/common/modules/timer/timer.repository.module';
|
||||||
|
import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule';
|
||||||
import { ThrottlerGuard } from '@nestjs/throttler';
|
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||||
import { ThrottlerModule } from '@nestjs/throttler/dist/throttler.module';
|
import { ThrottlerModule } from '@nestjs/throttler/dist/throttler.module';
|
||||||
import { isArray } from 'class-validator';
|
import { isArray } from 'class-validator';
|
||||||
import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger';
|
import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger';
|
||||||
import { AqiModule } from './aqi/aqi.module';
|
import { AqiModule } from './aqi/aqi.module';
|
||||||
import { OccupancyModule } from './occupancy/occupancy.module';
|
|
||||||
import { WeatherModule } from './weather/weather.module';
|
|
||||||
import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule';
|
|
||||||
import { SchedulerModule } from './scheduler/scheduler.module';
|
|
||||||
import { BookingModule } from './booking';
|
import { BookingModule } from './booking';
|
||||||
|
import { OccupancyModule } from './occupancy/occupancy.module';
|
||||||
|
import { SchedulerModule } from './scheduler/scheduler.module';
|
||||||
|
import { TimerModule } from './timer/timer.module';
|
||||||
|
import { WeatherModule } from './weather/weather.module';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
@ -63,6 +65,8 @@ import { BookingModule } from './booking';
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
WinstonModule.forRoot(winstonLoggerOptions),
|
WinstonModule.forRoot(winstonLoggerOptions),
|
||||||
|
TimerModule,
|
||||||
|
TimerRepositoryModule,
|
||||||
ClientModule,
|
ClientModule,
|
||||||
AuthenticationModule,
|
AuthenticationModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
import { AutomationService } from '../services/automation.service';
|
||||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
|
||||||
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
|
||||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@ -12,20 +9,20 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Query,
|
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
|
||||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
|
||||||
import { AutomationParamDto } from '../dtos';
|
|
||||||
import {
|
import {
|
||||||
AddAutomationDto,
|
AddAutomationDto,
|
||||||
UpdateAutomationDto,
|
UpdateAutomationDto,
|
||||||
UpdateAutomationStatusDto,
|
UpdateAutomationStatusDto,
|
||||||
} from '../dtos/automation.dto';
|
} from '../dtos/automation.dto';
|
||||||
import { GetAutomationBySpacesDto } from '../dtos/get-automation-by-spaces.dto';
|
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||||
import { AutomationService } from '../services/automation.service';
|
import { AutomationParamDto } from '../dtos';
|
||||||
|
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||||
|
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||||
|
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||||
|
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||||
|
|
||||||
@ApiTags('Automation Module')
|
@ApiTags('Automation Module')
|
||||||
@Controller({
|
@Controller({
|
||||||
@ -59,28 +56,6 @@ export class AutomationController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@UseGuards(PermissionsGuard)
|
|
||||||
@Permissions('AUTOMATION_VIEW')
|
|
||||||
@Get('')
|
|
||||||
@ApiOperation({
|
|
||||||
summary: ControllerRoute.AUTOMATION.ACTIONS.GET_AUTOMATION_SUMMARY,
|
|
||||||
description: ControllerRoute.AUTOMATION.ACTIONS.GET_AUTOMATION_DESCRIPTION,
|
|
||||||
})
|
|
||||||
async getAutomationBySpaces(
|
|
||||||
@Param() param: ProjectParam,
|
|
||||||
@Query() spaces: GetAutomationBySpacesDto,
|
|
||||||
) {
|
|
||||||
const automation = await this.automationService.getAutomationBySpaces(
|
|
||||||
spaces,
|
|
||||||
param.projectUuid,
|
|
||||||
);
|
|
||||||
return new SuccessResponseDto({
|
|
||||||
message: 'Automation retrieved Successfully',
|
|
||||||
data: automation,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(PermissionsGuard)
|
@UseGuards(PermissionsGuard)
|
||||||
@Permissions('AUTOMATION_VIEW')
|
@Permissions('AUTOMATION_VIEW')
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { Transform } from 'class-transformer';
|
|
||||||
import { IsOptional, IsUUID } from 'class-validator';
|
|
||||||
|
|
||||||
export class GetAutomationBySpacesDto {
|
|
||||||
@ApiProperty({
|
|
||||||
description: 'List of Space IDs to filter automation',
|
|
||||||
required: false,
|
|
||||||
example: ['60d21b4667d0d8992e610c85', '60d21b4967d0d8992e610c86'],
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@Transform(({ value }) => {
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
return [value];
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
@IsUUID('4', { each: true })
|
|
||||||
public spaces?: string[];
|
|
||||||
}
|
|
@ -1,42 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
ActionExecutorEnum,
|
Injectable,
|
||||||
ActionTypeEnum,
|
|
||||||
AUTO_PREFIX,
|
|
||||||
AUTOMATION_TYPE,
|
|
||||||
EntityTypeEnum,
|
|
||||||
} from '@app/common/constants/automation.enum';
|
|
||||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
|
||||||
import { GetSpaceParam } from '@app/common/dto/get.space.param';
|
|
||||||
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
|
||||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
|
||||||
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
|
|
||||||
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
|
|
||||||
import { ConvertedAction } from '@app/common/integrations/tuya/interfaces';
|
|
||||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
|
||||||
import { AutomationEntity } from '@app/common/modules/automation/entities';
|
|
||||||
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
|
||||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
|
||||||
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
|
||||||
import { SceneRepository } from '@app/common/modules/scene/repositories';
|
|
||||||
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
|
||||||
import {
|
|
||||||
BadRequestException,
|
|
||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Injectable,
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
||||||
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
|
|
||||||
import { DeviceService } from 'src/device/services';
|
|
||||||
import { DeleteTapToRunSceneInterface } from 'src/scene/interface/scene.interface';
|
|
||||||
import { In } from 'typeorm';
|
|
||||||
import {
|
import {
|
||||||
AddAutomationDto,
|
AddAutomationDto,
|
||||||
AutomationParamDto,
|
AutomationParamDto,
|
||||||
UpdateAutomationDto,
|
UpdateAutomationDto,
|
||||||
UpdateAutomationStatusDto,
|
UpdateAutomationStatusDto,
|
||||||
} from '../dtos';
|
} from '../dtos';
|
||||||
import { GetAutomationBySpacesDto } from '../dtos/get-automation-by-spaces.dto';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
|
||||||
|
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
|
||||||
|
import { DeviceService } from 'src/device/services';
|
||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
AddAutomationParams,
|
AddAutomationParams,
|
||||||
@ -44,6 +22,26 @@ import {
|
|||||||
AutomationResponseData,
|
AutomationResponseData,
|
||||||
Condition,
|
Condition,
|
||||||
} from '../interface/automation.interface';
|
} from '../interface/automation.interface';
|
||||||
|
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
|
||||||
|
import {
|
||||||
|
ActionExecutorEnum,
|
||||||
|
ActionTypeEnum,
|
||||||
|
AUTO_PREFIX,
|
||||||
|
AUTOMATION_TYPE,
|
||||||
|
EntityTypeEnum,
|
||||||
|
} from '@app/common/constants/automation.enum';
|
||||||
|
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||||
|
import { ConvertedAction } from '@app/common/integrations/tuya/interfaces';
|
||||||
|
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
||||||
|
import { SceneRepository } from '@app/common/modules/scene/repositories';
|
||||||
|
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||||
|
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||||
|
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||||
|
import { AutomationEntity } from '@app/common/modules/automation/entities';
|
||||||
|
import { DeleteTapToRunSceneInterface } from 'src/scene/interface/scene.interface';
|
||||||
|
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||||
|
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||||
|
import { GetSpaceParam } from '@app/common/dto/get.space.param';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AutomationService {
|
export class AutomationService {
|
||||||
@ -114,25 +112,128 @@ export class AutomationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async getAutomationBySpace({ projectUuid, spaceUuid }: GetSpaceParam) {
|
async createAutomationExternalService(
|
||||||
return this.getAutomationBySpaces({ spaces: [spaceUuid] }, projectUuid);
|
params: AddAutomationParams,
|
||||||
}
|
|
||||||
|
|
||||||
async getAutomationBySpaces(
|
|
||||||
{ spaces }: GetAutomationBySpacesDto,
|
|
||||||
projectUuid: string,
|
projectUuid: string,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await this.validateProject(projectUuid);
|
const formattedActions = await this.prepareActions(
|
||||||
|
params.actions,
|
||||||
|
projectUuid,
|
||||||
|
);
|
||||||
|
const formattedCondition = await this.prepareConditions(
|
||||||
|
params.conditions,
|
||||||
|
projectUuid,
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await this.tuyaService.createAutomation(
|
||||||
|
params.spaceTuyaId,
|
||||||
|
params.automationName,
|
||||||
|
params.effectiveTime,
|
||||||
|
params.decisionExpr,
|
||||||
|
formattedCondition,
|
||||||
|
formattedActions,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.result?.id) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Failed to create automation in Tuya',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof HttpException) {
|
||||||
|
throw err;
|
||||||
|
} else if (err.message?.includes('tuya')) {
|
||||||
|
throw new HttpException(
|
||||||
|
'API error: Failed to create automation',
|
||||||
|
HttpStatus.BAD_GATEWAY,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
`An Internal error has been occured ${err}`,
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async add(params: AddAutomationParams, projectUuid: string) {
|
||||||
|
try {
|
||||||
|
const response = await this.createAutomationExternalService(
|
||||||
|
params,
|
||||||
|
projectUuid,
|
||||||
|
);
|
||||||
|
|
||||||
|
const automation = await this.automationRepository.save({
|
||||||
|
automationTuyaUuid: response.result.id,
|
||||||
|
space: { uuid: params.spaceUuid },
|
||||||
|
});
|
||||||
|
|
||||||
|
return automation;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof HttpException) {
|
||||||
|
throw err;
|
||||||
|
} else if (err.message?.includes('tuya')) {
|
||||||
|
throw new HttpException(
|
||||||
|
'API error: Failed to create automation',
|
||||||
|
HttpStatus.BAD_GATEWAY,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
'Database error: Failed to save automation',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSpaceByUuid(spaceUuid: string, projectUuid: string) {
|
||||||
|
try {
|
||||||
|
const space = await this.spaceRepository.findOne({
|
||||||
|
where: {
|
||||||
|
uuid: spaceUuid,
|
||||||
|
community: {
|
||||||
|
project: {
|
||||||
|
uuid: projectUuid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relations: ['community'],
|
||||||
|
});
|
||||||
|
if (!space) {
|
||||||
|
throw new BadRequestException(`Invalid space UUID ${spaceUuid}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
uuid: space.uuid,
|
||||||
|
createdAt: space.createdAt,
|
||||||
|
updatedAt: space.updatedAt,
|
||||||
|
name: space.spaceName,
|
||||||
|
spaceTuyaUuid: space.community.externalId,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof BadRequestException) {
|
||||||
|
throw err; // Re-throw BadRequestException
|
||||||
|
} else {
|
||||||
|
throw new HttpException('Space not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAutomationBySpace(param: GetSpaceParam) {
|
||||||
|
try {
|
||||||
|
await this.validateProject(param.projectUuid);
|
||||||
|
|
||||||
// Fetch automation data from the repository
|
// Fetch automation data from the repository
|
||||||
const automationData = await this.automationRepository.find({
|
const automationData = await this.automationRepository.find({
|
||||||
where: {
|
where: {
|
||||||
space: {
|
space: {
|
||||||
uuid: In(spaces ?? []),
|
uuid: param.spaceUuid,
|
||||||
community: {
|
community: {
|
||||||
|
uuid: param.communityUuid,
|
||||||
project: {
|
project: {
|
||||||
uuid: projectUuid,
|
uuid: param.projectUuid,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -189,277 +290,46 @@ export class AutomationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAutomationDetails(param: AutomationParamDto) {
|
async findAutomationBySpace(spaceUuid: string, projectUuid: string) {
|
||||||
await this.validateProject(param.projectUuid);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const automation = await this.findAutomationByUuid(
|
await this.getSpaceByUuid(spaceUuid, projectUuid);
|
||||||
param.automationUuid,
|
|
||||||
param.projectUuid,
|
|
||||||
);
|
|
||||||
|
|
||||||
const automationDetails = await this.getAutomation(automation);
|
const automationData = await this.automationRepository.find({
|
||||||
|
|
||||||
return automationDetails;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`Error fetching automation details for automationUuid ${param.automationUuid}:`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error instanceof HttpException) {
|
|
||||||
throw error;
|
|
||||||
} else {
|
|
||||||
throw new HttpException(
|
|
||||||
'An error occurred while retrieving automation details',
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async updateAutomation(
|
|
||||||
updateAutomationDto: UpdateAutomationDto,
|
|
||||||
param: AutomationParamDto,
|
|
||||||
) {
|
|
||||||
await this.validateProject(param.projectUuid);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const automation = await this.findAutomationByUuid(
|
|
||||||
param.automationUuid,
|
|
||||||
param.projectUuid,
|
|
||||||
);
|
|
||||||
const space = await this.getSpaceByUuid(
|
|
||||||
automation.space.uuid,
|
|
||||||
param.projectUuid,
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateTuyaAutomationResponse =
|
|
||||||
await this.updateAutomationExternalService(
|
|
||||||
space.spaceTuyaUuid,
|
|
||||||
automation.automationTuyaUuid,
|
|
||||||
updateAutomationDto,
|
|
||||||
param.projectUuid,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!updateTuyaAutomationResponse.success) {
|
|
||||||
throw new HttpException(
|
|
||||||
`Failed to update a external automation`,
|
|
||||||
HttpStatus.BAD_GATEWAY,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const updatedScene = await this.automationRepository.update(
|
|
||||||
{ uuid: param.automationUuid },
|
|
||||||
{
|
|
||||||
space: { uuid: automation.space.uuid },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return new SuccessResponseDto({
|
|
||||||
data: updatedScene,
|
|
||||||
message: `Automation with ID ${param.automationUuid} updated successfully`,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof BadRequestException) {
|
|
||||||
throw err; // Re-throw BadRequestException
|
|
||||||
} else {
|
|
||||||
throw new HttpException(
|
|
||||||
err.message || 'Automation not found',
|
|
||||||
err.status || HttpStatus.NOT_FOUND,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async updateAutomationStatus(
|
|
||||||
updateAutomationStatusDto: UpdateAutomationStatusDto,
|
|
||||||
param: AutomationParamDto,
|
|
||||||
) {
|
|
||||||
const { isEnable, spaceUuid } = updateAutomationStatusDto;
|
|
||||||
await this.validateProject(param.projectUuid);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const automation = await this.findAutomationByUuid(
|
|
||||||
param.automationUuid,
|
|
||||||
param.projectUuid,
|
|
||||||
);
|
|
||||||
const space = await this.getSpaceByUuid(spaceUuid, param.projectUuid);
|
|
||||||
if (!space.spaceTuyaUuid) {
|
|
||||||
throw new HttpException(
|
|
||||||
`Invalid space UUID ${spaceUuid}`,
|
|
||||||
HttpStatus.NOT_FOUND,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.tuyaService.updateAutomationState(
|
|
||||||
space.spaceTuyaUuid,
|
|
||||||
automation.automationTuyaUuid,
|
|
||||||
isEnable,
|
|
||||||
);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof BadRequestException) {
|
|
||||||
throw err; // Re-throw BadRequestException
|
|
||||||
} else {
|
|
||||||
throw new HttpException(
|
|
||||||
err.message || 'Automation not found',
|
|
||||||
err.status || HttpStatus.NOT_FOUND,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async deleteAutomation(param: AutomationParamDto) {
|
|
||||||
const { automationUuid } = param;
|
|
||||||
await this.validateProject(param.projectUuid);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const automationData = await this.findAutomationByUuid(
|
|
||||||
automationUuid,
|
|
||||||
param.projectUuid,
|
|
||||||
);
|
|
||||||
const space = await this.getSpaceByUuid(
|
|
||||||
automationData.space.uuid,
|
|
||||||
param.projectUuid,
|
|
||||||
);
|
|
||||||
await this.delete(automationData.automationTuyaUuid, space.spaceTuyaUuid);
|
|
||||||
const existingSceneDevice = await this.sceneDeviceRepository.findOne({
|
|
||||||
where: { automationTuyaUuid: automationData.automationTuyaUuid },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingSceneDevice) {
|
|
||||||
await this.sceneDeviceRepository.delete({
|
|
||||||
automationTuyaUuid: automationData.automationTuyaUuid,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await this.automationRepository.update(
|
|
||||||
{
|
|
||||||
uuid: automationUuid,
|
|
||||||
},
|
|
||||||
{ disabled: true },
|
|
||||||
);
|
|
||||||
return new SuccessResponseDto({
|
|
||||||
message: `Automation with ID ${automationUuid} deleted successfully`,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof HttpException) {
|
|
||||||
throw err;
|
|
||||||
} else {
|
|
||||||
throw new HttpException(
|
|
||||||
err.message || `Automation not found for id ${param.automationUuid}`,
|
|
||||||
err.status || HttpStatus.NOT_FOUND,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createAutomationExternalService(
|
|
||||||
params: AddAutomationParams,
|
|
||||||
projectUuid: string,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const formattedActions = await this.prepareActions(
|
|
||||||
params.actions,
|
|
||||||
projectUuid,
|
|
||||||
);
|
|
||||||
const formattedCondition = await this.prepareConditions(
|
|
||||||
params.conditions,
|
|
||||||
projectUuid,
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await this.tuyaService.createAutomation(
|
|
||||||
params.spaceTuyaId,
|
|
||||||
params.automationName,
|
|
||||||
params.effectiveTime,
|
|
||||||
params.decisionExpr,
|
|
||||||
formattedCondition,
|
|
||||||
formattedActions,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.result?.id) {
|
|
||||||
throw new HttpException(
|
|
||||||
'Failed to create automation in Tuya',
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof HttpException) {
|
|
||||||
throw err;
|
|
||||||
} else if (err.message?.includes('tuya')) {
|
|
||||||
throw new HttpException(
|
|
||||||
'API error: Failed to create automation',
|
|
||||||
HttpStatus.BAD_GATEWAY,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new HttpException(
|
|
||||||
`An Internal error has been occured ${err}`,
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private async add(params: AddAutomationParams, projectUuid: string) {
|
|
||||||
try {
|
|
||||||
const response = await this.createAutomationExternalService(
|
|
||||||
params,
|
|
||||||
projectUuid,
|
|
||||||
);
|
|
||||||
|
|
||||||
const automation = await this.automationRepository.save({
|
|
||||||
automationTuyaUuid: response.result.id,
|
|
||||||
space: { uuid: params.spaceUuid },
|
|
||||||
});
|
|
||||||
|
|
||||||
return automation;
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof HttpException) {
|
|
||||||
throw err;
|
|
||||||
} else if (err.message?.includes('tuya')) {
|
|
||||||
throw new HttpException(
|
|
||||||
'API error: Failed to create automation',
|
|
||||||
HttpStatus.BAD_GATEWAY,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new HttpException(
|
|
||||||
'Database error: Failed to save automation',
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getSpaceByUuid(spaceUuid: string, projectUuid: string) {
|
|
||||||
try {
|
|
||||||
const space = await this.spaceRepository.findOne({
|
|
||||||
where: {
|
where: {
|
||||||
uuid: spaceUuid,
|
space: { uuid: spaceUuid },
|
||||||
community: {
|
disabled: false,
|
||||||
project: {
|
|
||||||
uuid: projectUuid,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
relations: ['community'],
|
relations: ['space'],
|
||||||
});
|
});
|
||||||
if (!space) {
|
|
||||||
throw new BadRequestException(`Invalid space UUID ${spaceUuid}`);
|
const automations = await Promise.all(
|
||||||
}
|
automationData.map(async (automation) => {
|
||||||
return {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
uuid: space.uuid,
|
const { actions, ...automationDetails } =
|
||||||
createdAt: space.createdAt,
|
await this.getAutomation(automation);
|
||||||
updatedAt: space.updatedAt,
|
|
||||||
name: space.spaceName,
|
return automationDetails;
|
||||||
spaceTuyaUuid: space.community.externalId,
|
}),
|
||||||
};
|
);
|
||||||
|
|
||||||
|
return automations;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof BadRequestException) {
|
console.error(
|
||||||
throw err; // Re-throw BadRequestException
|
`Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`,
|
||||||
|
err.message,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (err instanceof HttpException) {
|
||||||
|
throw err;
|
||||||
} else {
|
} else {
|
||||||
throw new HttpException('Space not found', HttpStatus.NOT_FOUND);
|
throw new HttpException(
|
||||||
|
'An error occurred while retrieving scenes',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async getTapToRunSceneDetailsTuya(
|
||||||
private async getTapToRunSceneDetailsTuya(
|
|
||||||
sceneUuid: string,
|
sceneUuid: string,
|
||||||
): Promise<AutomationDetailsResult> {
|
): Promise<AutomationDetailsResult> {
|
||||||
try {
|
try {
|
||||||
@ -491,8 +361,35 @@ export class AutomationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async getAutomationDetails(param: AutomationParamDto) {
|
||||||
|
await this.validateProject(param.projectUuid);
|
||||||
|
|
||||||
private async getAutomation(automation: AutomationEntity) {
|
try {
|
||||||
|
const automation = await this.findAutomation(
|
||||||
|
param.automationUuid,
|
||||||
|
param.projectUuid,
|
||||||
|
);
|
||||||
|
|
||||||
|
const automationDetails = await this.getAutomation(automation);
|
||||||
|
|
||||||
|
return automationDetails;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error fetching automation details for automationUuid ${param.automationUuid}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error instanceof HttpException) {
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
'An error occurred while retrieving automation details',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getAutomation(automation: AutomationEntity) {
|
||||||
try {
|
try {
|
||||||
const response = await this.tuyaService.getSceneRule(
|
const response = await this.tuyaService.getSceneRule(
|
||||||
automation.automationTuyaUuid,
|
automation.automationTuyaUuid,
|
||||||
@ -599,13 +496,13 @@ export class AutomationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private async findAutomationByUuid(
|
async findAutomation(
|
||||||
uuid: string,
|
sceneUuid: string,
|
||||||
projectUuid: string,
|
projectUuid: string,
|
||||||
): Promise<AutomationEntity> {
|
): Promise<AutomationEntity> {
|
||||||
const automation = await this.automationRepository.findOne({
|
const automation = await this.automationRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
uuid: uuid,
|
uuid: sceneUuid,
|
||||||
space: { community: { project: { uuid: projectUuid } } },
|
space: { community: { project: { uuid: projectUuid } } },
|
||||||
},
|
},
|
||||||
relations: ['space'],
|
relations: ['space'],
|
||||||
@ -613,14 +510,57 @@ export class AutomationService {
|
|||||||
|
|
||||||
if (!automation) {
|
if (!automation) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
`Invalid automation with id ${uuid}`,
|
`Invalid automation with id ${sceneUuid}`,
|
||||||
HttpStatus.NOT_FOUND,
|
HttpStatus.NOT_FOUND,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return automation;
|
return automation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async delete(tuyaAutomationId: string, tuyaSpaceId: string) {
|
async deleteAutomation(param: AutomationParamDto) {
|
||||||
|
const { automationUuid } = param;
|
||||||
|
await this.validateProject(param.projectUuid);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const automationData = await this.findAutomation(
|
||||||
|
automationUuid,
|
||||||
|
param.projectUuid,
|
||||||
|
);
|
||||||
|
const space = await this.getSpaceByUuid(
|
||||||
|
automationData.space.uuid,
|
||||||
|
param.projectUuid,
|
||||||
|
);
|
||||||
|
await this.delete(automationData.automationTuyaUuid, space.spaceTuyaUuid);
|
||||||
|
const existingSceneDevice = await this.sceneDeviceRepository.findOne({
|
||||||
|
where: { automationTuyaUuid: automationData.automationTuyaUuid },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingSceneDevice) {
|
||||||
|
await this.sceneDeviceRepository.delete({
|
||||||
|
automationTuyaUuid: automationData.automationTuyaUuid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await this.automationRepository.update(
|
||||||
|
{
|
||||||
|
uuid: automationUuid,
|
||||||
|
},
|
||||||
|
{ disabled: true },
|
||||||
|
);
|
||||||
|
return new SuccessResponseDto({
|
||||||
|
message: `Automation with ID ${automationUuid} deleted successfully`,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof HttpException) {
|
||||||
|
throw err;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
err.message || `Automation not found for id ${param.automationUuid}`,
|
||||||
|
err.status || HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async delete(tuyaAutomationId: string, tuyaSpaceId: string) {
|
||||||
try {
|
try {
|
||||||
const response = (await this.tuyaService.deleteSceneRule(
|
const response = (await this.tuyaService.deleteSceneRule(
|
||||||
tuyaAutomationId,
|
tuyaAutomationId,
|
||||||
@ -638,7 +578,7 @@ export class AutomationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private async updateAutomationExternalService(
|
async updateAutomationExternalService(
|
||||||
spaceTuyaUuid: string,
|
spaceTuyaUuid: string,
|
||||||
automationUuid: string,
|
automationUuid: string,
|
||||||
updateAutomationDto: UpdateAutomationDto,
|
updateAutomationDto: UpdateAutomationDto,
|
||||||
@ -686,6 +626,95 @@ export class AutomationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async updateAutomation(
|
||||||
|
updateAutomationDto: UpdateAutomationDto,
|
||||||
|
param: AutomationParamDto,
|
||||||
|
) {
|
||||||
|
await this.validateProject(param.projectUuid);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const automation = await this.findAutomation(
|
||||||
|
param.automationUuid,
|
||||||
|
param.projectUuid,
|
||||||
|
);
|
||||||
|
const space = await this.getSpaceByUuid(
|
||||||
|
automation.space.uuid,
|
||||||
|
param.projectUuid,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateTuyaAutomationResponse =
|
||||||
|
await this.updateAutomationExternalService(
|
||||||
|
space.spaceTuyaUuid,
|
||||||
|
automation.automationTuyaUuid,
|
||||||
|
updateAutomationDto,
|
||||||
|
param.projectUuid,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updateTuyaAutomationResponse.success) {
|
||||||
|
throw new HttpException(
|
||||||
|
`Failed to update a external automation`,
|
||||||
|
HttpStatus.BAD_GATEWAY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const updatedScene = await this.automationRepository.update(
|
||||||
|
{ uuid: param.automationUuid },
|
||||||
|
{
|
||||||
|
space: { uuid: automation.space.uuid },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return new SuccessResponseDto({
|
||||||
|
data: updatedScene,
|
||||||
|
message: `Automation with ID ${param.automationUuid} updated successfully`,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof BadRequestException) {
|
||||||
|
throw err; // Re-throw BadRequestException
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
err.message || 'Automation not found',
|
||||||
|
err.status || HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async updateAutomationStatus(
|
||||||
|
updateAutomationStatusDto: UpdateAutomationStatusDto,
|
||||||
|
param: AutomationParamDto,
|
||||||
|
) {
|
||||||
|
const { isEnable, spaceUuid } = updateAutomationStatusDto;
|
||||||
|
await this.validateProject(param.projectUuid);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const automation = await this.findAutomation(
|
||||||
|
param.automationUuid,
|
||||||
|
param.projectUuid,
|
||||||
|
);
|
||||||
|
const space = await this.getSpaceByUuid(spaceUuid, param.projectUuid);
|
||||||
|
if (!space.spaceTuyaUuid) {
|
||||||
|
throw new HttpException(
|
||||||
|
`Invalid space UUID ${spaceUuid}`,
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.tuyaService.updateAutomationState(
|
||||||
|
space.spaceTuyaUuid,
|
||||||
|
automation.automationTuyaUuid,
|
||||||
|
isEnable,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof BadRequestException) {
|
||||||
|
throw err; // Re-throw BadRequestException
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
err.message || 'Automation not found',
|
||||||
|
err.status || HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async prepareActions(
|
private async prepareActions(
|
||||||
actions: Action[],
|
actions: Action[],
|
||||||
@ -724,7 +753,7 @@ export class AutomationService {
|
|||||||
action.action_executor === ActionExecutorEnum.RULE_ENABLE
|
action.action_executor === ActionExecutorEnum.RULE_ENABLE
|
||||||
) {
|
) {
|
||||||
if (action.action_type === ActionTypeEnum.AUTOMATION) {
|
if (action.action_type === ActionTypeEnum.AUTOMATION) {
|
||||||
const automation = await this.findAutomationByUuid(
|
const automation = await this.findAutomation(
|
||||||
action.entity_id,
|
action.entity_id,
|
||||||
projectUuid,
|
projectUuid,
|
||||||
);
|
);
|
||||||
|
@ -2,10 +2,12 @@ import { ApiProperty } from '@nestjs/swagger';
|
|||||||
import {
|
import {
|
||||||
ArrayMinSize,
|
ArrayMinSize,
|
||||||
IsArray,
|
IsArray,
|
||||||
|
IsDate,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
IsUUID,
|
IsUUID,
|
||||||
|
MinDate,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
export class AddUserInvitationDto {
|
export class AddUserInvitationDto {
|
||||||
@ -16,7 +18,7 @@ export class AddUserInvitationDto {
|
|||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
public firstName: string;
|
firstName: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'The last name of the user',
|
description: 'The last name of the user',
|
||||||
@ -25,7 +27,7 @@ export class AddUserInvitationDto {
|
|||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
public lastName: string;
|
lastName: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'The email of the user',
|
description: 'The email of the user',
|
||||||
@ -34,7 +36,7 @@ export class AddUserInvitationDto {
|
|||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
public email: string;
|
email: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'The job title of the user',
|
description: 'The job title of the user',
|
||||||
@ -43,7 +45,7 @@ export class AddUserInvitationDto {
|
|||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
public jobTitle?: string;
|
jobTitle?: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'The company name of the user',
|
description: 'The company name of the user',
|
||||||
@ -52,7 +54,27 @@ export class AddUserInvitationDto {
|
|||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
public companyName?: string;
|
companyName?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Access start date',
|
||||||
|
example: new Date(),
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsDate()
|
||||||
|
@MinDate(new Date())
|
||||||
|
@IsOptional()
|
||||||
|
accessStartDate?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Access start date',
|
||||||
|
example: new Date(),
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsDate()
|
||||||
|
@MinDate(new Date())
|
||||||
|
@IsOptional()
|
||||||
|
accessEndDate?: Date;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'The phone number of the user',
|
description: 'The phone number of the user',
|
||||||
@ -61,7 +83,7 @@ export class AddUserInvitationDto {
|
|||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
public phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'The role uuid of the user',
|
description: 'The role uuid of the user',
|
||||||
@ -70,7 +92,7 @@ export class AddUserInvitationDto {
|
|||||||
})
|
})
|
||||||
@IsUUID('4')
|
@IsUUID('4')
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
public roleUuid: string;
|
roleUuid: string;
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'The project uuid of the user',
|
description: 'The project uuid of the user',
|
||||||
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
|
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
|
||||||
@ -78,7 +100,7 @@ export class AddUserInvitationDto {
|
|||||||
})
|
})
|
||||||
@IsUUID('4')
|
@IsUUID('4')
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
public projectUuid: string;
|
projectUuid: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'The array of space UUIDs (at least one required)',
|
description: 'The array of space UUIDs (at least one required)',
|
||||||
@ -88,7 +110,7 @@ export class AddUserInvitationDto {
|
|||||||
@IsArray()
|
@IsArray()
|
||||||
@IsUUID('4', { each: true })
|
@IsUUID('4', { each: true })
|
||||||
@ArrayMinSize(1)
|
@ArrayMinSize(1)
|
||||||
public spaceUuids: string[];
|
spaceUuids: string[];
|
||||||
constructor(dto: Partial<AddUserInvitationDto>) {
|
constructor(dto: Partial<AddUserInvitationDto>) {
|
||||||
Object.assign(this, dto);
|
Object.assign(this, dto);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { RoleType } from '@app/common/constants/role.type.enum';
|
import { RoleType } from '@app/common/constants/role.type.enum';
|
||||||
|
import { TimerJobTypeEnum } from '@app/common/constants/timer-job-type.enum';
|
||||||
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
|
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
|
||||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||||
@ -23,6 +24,7 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { SpaceUserService } from 'src/space/services';
|
import { SpaceUserService } from 'src/space/services';
|
||||||
|
import { TimerService } from 'src/timer/timer.service';
|
||||||
import { UserSpaceService } from 'src/users/services';
|
import { UserSpaceService } from 'src/users/services';
|
||||||
import {
|
import {
|
||||||
DataSource,
|
DataSource,
|
||||||
@ -52,6 +54,7 @@ export class InviteUserService {
|
|||||||
private readonly spaceRepository: SpaceRepository,
|
private readonly spaceRepository: SpaceRepository,
|
||||||
private readonly roleTypeRepository: RoleTypeRepository,
|
private readonly roleTypeRepository: RoleTypeRepository,
|
||||||
private readonly dataSource: DataSource,
|
private readonly dataSource: DataSource,
|
||||||
|
private readonly timerService: TimerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createUserInvitation(
|
async createUserInvitation(
|
||||||
@ -68,8 +71,14 @@ export class InviteUserService {
|
|||||||
roleUuid,
|
roleUuid,
|
||||||
spaceUuids,
|
spaceUuids,
|
||||||
projectUuid,
|
projectUuid,
|
||||||
|
accessStartDate,
|
||||||
|
accessEndDate,
|
||||||
} = dto;
|
} = dto;
|
||||||
|
if (accessStartDate && accessEndDate && accessEndDate <= accessStartDate) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'accessEndDate must be after accessStartDate',
|
||||||
|
);
|
||||||
|
}
|
||||||
const invitationCode = generateRandomString(6);
|
const invitationCode = generateRandomString(6);
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
|
||||||
@ -114,6 +123,8 @@ export class InviteUserService {
|
|||||||
invitationCode,
|
invitationCode,
|
||||||
invitedBy: roleType,
|
invitedBy: roleType,
|
||||||
project: { uuid: projectUuid },
|
project: { uuid: projectUuid },
|
||||||
|
accessEndDate,
|
||||||
|
accessStartDate,
|
||||||
});
|
});
|
||||||
|
|
||||||
const invitedUser = await queryRunner.manager.save(inviteUser);
|
const invitedUser = await queryRunner.manager.save(inviteUser);
|
||||||
@ -132,12 +143,26 @@ export class InviteUserService {
|
|||||||
|
|
||||||
// Send invitation email
|
// Send invitation email
|
||||||
const spaceNames = validSpaces.map((space) => space.spaceName).join(', ');
|
const spaceNames = validSpaces.map((space) => space.spaceName).join(', ');
|
||||||
await this.emailService.sendEmailWithInvitationTemplate(email, {
|
if (accessStartDate) {
|
||||||
name: firstName,
|
await this.timerService.createJob({
|
||||||
invitationCode,
|
type: TimerJobTypeEnum.INVITE_USER_EMAIL,
|
||||||
role: invitedRoleType.replace(/_/g, ' '),
|
triggerDate: accessStartDate,
|
||||||
spacesList: spaceNames,
|
metadata: {
|
||||||
});
|
email,
|
||||||
|
name: firstName,
|
||||||
|
invitationCode,
|
||||||
|
role: invitedRoleType.replace(/_/g, ' '),
|
||||||
|
spacesList: spaceNames,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this.emailService.sendEmailWithInvitationTemplate(email, {
|
||||||
|
name: firstName,
|
||||||
|
invitationCode,
|
||||||
|
role: invitedRoleType.replace(/_/g, ' '),
|
||||||
|
spacesList: spaceNames,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
import { SceneService } from '../services/scene.service';
|
||||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
|
||||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
|
||||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@ -11,21 +8,21 @@ import {
|
|||||||
Param,
|
Param,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Query,
|
|
||||||
Req,
|
Req,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
|
||||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
|
||||||
import { SceneParamDto } from '../dtos';
|
|
||||||
import {
|
import {
|
||||||
AddSceneIconDto,
|
AddSceneIconDto,
|
||||||
AddSceneTapToRunDto,
|
AddSceneTapToRunDto,
|
||||||
GetSceneDto,
|
|
||||||
UpdateSceneTapToRunDto,
|
UpdateSceneTapToRunDto,
|
||||||
} from '../dtos/scene.dto';
|
} from '../dtos/scene.dto';
|
||||||
import { SceneService } from '../services/scene.service';
|
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||||
|
import { SceneParamDto } from '../dtos';
|
||||||
|
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||||
|
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||||
|
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||||
|
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||||
|
|
||||||
@ApiTags('Scene Module')
|
@ApiTags('Scene Module')
|
||||||
@Controller({
|
@Controller({
|
||||||
@ -55,27 +52,6 @@ export class SceneController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@UseGuards(PermissionsGuard)
|
|
||||||
@Permissions('SCENES_VIEW')
|
|
||||||
@Get('tap-to-run')
|
|
||||||
@ApiOperation({
|
|
||||||
summary: ControllerRoute.SCENE.ACTIONS.GET_TAP_TO_RUN_SCENES_SUMMARY,
|
|
||||||
description:
|
|
||||||
ControllerRoute.SCENE.ACTIONS.GET_TAP_TO_RUN_SCENES_DESCRIPTION,
|
|
||||||
})
|
|
||||||
async getTapToRunSceneBySpaces(
|
|
||||||
@Query() dto: GetSceneDto,
|
|
||||||
@Req() req: any,
|
|
||||||
): Promise<BaseResponseDto> {
|
|
||||||
const projectUuid = req.user.project.uuid;
|
|
||||||
const data = await this.sceneService.findScenesBySpaces(dto, projectUuid);
|
|
||||||
return new SuccessResponseDto({
|
|
||||||
message: 'Scenes Retrieved Successfully',
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(PermissionsGuard)
|
@UseGuards(PermissionsGuard)
|
||||||
@Permissions('SCENES_DELETE')
|
@Permissions('SCENES_DELETE')
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import { BooleanValues } from '@app/common/constants/boolean-values.enum';
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform, Type } from 'class-transformer';
|
|
||||||
import {
|
import {
|
||||||
IsArray,
|
|
||||||
IsBoolean,
|
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsNumber,
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
IsString,
|
||||||
IsUUID,
|
IsArray,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
|
IsOptional,
|
||||||
|
IsNumber,
|
||||||
|
IsBoolean,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
import { Transform, Type } from 'class-transformer';
|
||||||
|
import { BooleanValues } from '@app/common/constants/boolean-values.enum';
|
||||||
|
|
||||||
class ExecutorProperty {
|
class ExecutorProperty {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
@ -188,19 +187,4 @@ export class GetSceneDto {
|
|||||||
return value.obj.showInHomePage === BooleanValues.TRUE;
|
return value.obj.showInHomePage === BooleanValues.TRUE;
|
||||||
})
|
})
|
||||||
public showInHomePage: boolean = false;
|
public showInHomePage: boolean = false;
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
description: 'List of Space IDs to filter automation',
|
|
||||||
required: false,
|
|
||||||
example: ['60d21b4667d0d8992e610c85', '60d21b4967d0d8992e610c86'],
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@Transform(({ value }) => {
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
return [value];
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
@IsUUID('4', { each: true })
|
|
||||||
public spaces?: string[];
|
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
ActionExecutorEnum,
|
Injectable,
|
||||||
ActionTypeEnum,
|
|
||||||
} from '@app/common/constants/automation.enum';
|
|
||||||
import { SceneIconType } from '@app/common/constants/secne-icon-type.enum';
|
|
||||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
|
||||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
|
||||||
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
|
|
||||||
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
|
|
||||||
import { ConvertedAction } from '@app/common/integrations/tuya/interfaces';
|
|
||||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
|
||||||
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
|
||||||
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
|
||||||
import {
|
|
||||||
SceneEntity,
|
|
||||||
SceneIconEntity,
|
|
||||||
} from '@app/common/modules/scene/entities';
|
|
||||||
import {
|
|
||||||
SceneIconRepository,
|
|
||||||
SceneRepository,
|
|
||||||
} from '@app/common/modules/scene/repositories';
|
|
||||||
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
|
||||||
import {
|
|
||||||
BadRequestException,
|
|
||||||
forwardRef,
|
|
||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
BadRequestException,
|
||||||
|
forwardRef,
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { HttpStatusCode } from 'axios';
|
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
||||||
import { DeviceService } from 'src/device/services';
|
|
||||||
import { In } from 'typeorm';
|
|
||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
AddSceneIconDto,
|
AddSceneIconDto,
|
||||||
@ -39,12 +15,35 @@ import {
|
|||||||
SceneParamDto,
|
SceneParamDto,
|
||||||
UpdateSceneTapToRunDto,
|
UpdateSceneTapToRunDto,
|
||||||
} from '../dtos';
|
} from '../dtos';
|
||||||
|
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
|
||||||
import {
|
import {
|
||||||
AddTapToRunSceneInterface,
|
AddTapToRunSceneInterface,
|
||||||
DeleteTapToRunSceneInterface,
|
DeleteTapToRunSceneInterface,
|
||||||
SceneDetails,
|
SceneDetails,
|
||||||
SceneDetailsResult,
|
SceneDetailsResult,
|
||||||
} from '../interface/scene.interface';
|
} from '../interface/scene.interface';
|
||||||
|
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
|
||||||
|
import {
|
||||||
|
ActionExecutorEnum,
|
||||||
|
ActionTypeEnum,
|
||||||
|
} from '@app/common/constants/automation.enum';
|
||||||
|
import {
|
||||||
|
SceneIconRepository,
|
||||||
|
SceneRepository,
|
||||||
|
} from '@app/common/modules/scene/repositories';
|
||||||
|
import { SceneIconType } from '@app/common/constants/secne-icon-type.enum';
|
||||||
|
import {
|
||||||
|
SceneEntity,
|
||||||
|
SceneIconEntity,
|
||||||
|
} from '@app/common/modules/scene/entities';
|
||||||
|
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||||
|
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||||
|
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||||
|
import { HttpStatusCode } from 'axios';
|
||||||
|
import { ConvertedAction } from '@app/common/integrations/tuya/interfaces';
|
||||||
|
import { DeviceService } from 'src/device/services';
|
||||||
|
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
||||||
|
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SceneService {
|
export class SceneService {
|
||||||
@ -93,48 +92,158 @@ export class SceneService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findScenesBySpace(spaceUuid: string, { showInHomePage }: GetSceneDto) {
|
async create(
|
||||||
|
spaceTuyaUuid: string,
|
||||||
|
addSceneTapToRunDto: AddSceneTapToRunDto,
|
||||||
|
projectUuid: string,
|
||||||
|
): Promise<SceneEntity> {
|
||||||
|
const { iconUuid, showInHomePage, spaceUuid } = addSceneTapToRunDto;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.getSpaceByUuid(spaceUuid);
|
const [defaultSceneIcon] = await Promise.all([
|
||||||
return this.findScenesBySpaces({ showInHomePage, spaces: [spaceUuid] });
|
this.getDefaultSceneIcon(),
|
||||||
} catch (error) {
|
]);
|
||||||
console.error(
|
if (!defaultSceneIcon) {
|
||||||
`Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`,
|
throw new HttpException(
|
||||||
error.message,
|
'Default scene icon not found',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.createSceneExternalService(
|
||||||
|
spaceTuyaUuid,
|
||||||
|
addSceneTapToRunDto,
|
||||||
|
projectUuid,
|
||||||
);
|
);
|
||||||
|
|
||||||
throw error instanceof HttpException
|
const scene = await this.sceneRepository.save({
|
||||||
? error
|
sceneTuyaUuid: response.result.id,
|
||||||
: new HttpException(
|
sceneIcon: { uuid: iconUuid || defaultSceneIcon.uuid },
|
||||||
'An error occurred while retrieving scenes',
|
showInHomePage,
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
space: { uuid: spaceUuid },
|
||||||
);
|
});
|
||||||
|
|
||||||
|
return scene;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof HttpException) {
|
||||||
|
throw err;
|
||||||
|
} else if (err.message?.includes('tuya')) {
|
||||||
|
throw new HttpException(
|
||||||
|
'API error: Failed to create scene',
|
||||||
|
HttpStatus.BAD_GATEWAY,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
'Database error: Failed to save scene',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async updateSceneExternalService(
|
||||||
|
spaceTuyaUuid: string,
|
||||||
|
sceneTuyaUuid: string,
|
||||||
|
updateSceneTapToRunDto: UpdateSceneTapToRunDto,
|
||||||
|
projectUuid: string,
|
||||||
|
) {
|
||||||
|
const { sceneName, decisionExpr, actions } = updateSceneTapToRunDto;
|
||||||
|
try {
|
||||||
|
const formattedActions = await this.prepareActions(actions, projectUuid);
|
||||||
|
|
||||||
|
const response = (await this.tuyaService.updateTapToRunScene(
|
||||||
|
sceneTuyaUuid,
|
||||||
|
spaceTuyaUuid,
|
||||||
|
sceneName,
|
||||||
|
formattedActions,
|
||||||
|
decisionExpr,
|
||||||
|
)) as AddTapToRunSceneInterface;
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Failed to update scene in Tuya',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof HttpException) {
|
||||||
|
throw err;
|
||||||
|
} else if (err.message?.includes('tuya')) {
|
||||||
|
throw new HttpException(
|
||||||
|
'API error: Failed to update scene',
|
||||||
|
HttpStatus.BAD_GATEWAY,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
`An Internal error has been occured ${err}`,
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findScenesBySpaces(
|
async createSceneExternalService(
|
||||||
{ showInHomePage, spaces }: GetSceneDto,
|
spaceTuyaUuid: string,
|
||||||
projectUuid?: string,
|
addSceneTapToRunDto: AddSceneTapToRunDto,
|
||||||
|
projectUuid: string,
|
||||||
) {
|
) {
|
||||||
|
const { sceneName, decisionExpr, actions } = addSceneTapToRunDto;
|
||||||
try {
|
try {
|
||||||
|
const formattedActions = await this.prepareActions(actions, projectUuid);
|
||||||
|
|
||||||
|
const response = (await this.tuyaService.addTapToRunScene(
|
||||||
|
spaceTuyaUuid,
|
||||||
|
sceneName,
|
||||||
|
formattedActions,
|
||||||
|
decisionExpr,
|
||||||
|
)) as AddTapToRunSceneInterface;
|
||||||
|
|
||||||
|
if (!response.result?.id) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Failed to create scene in Tuya',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof HttpException) {
|
||||||
|
throw err;
|
||||||
|
} else if (err.message?.includes('tuya')) {
|
||||||
|
throw new HttpException(
|
||||||
|
'API error: Failed to create scene',
|
||||||
|
HttpStatus.BAD_GATEWAY,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
`An Internal error has been occured ${err}`,
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findScenesBySpace(spaceUuid: string, filter: GetSceneDto) {
|
||||||
|
try {
|
||||||
|
await this.getSpaceByUuid(spaceUuid);
|
||||||
|
const showInHomePage = filter?.showInHomePage;
|
||||||
|
|
||||||
const scenesData = await this.sceneRepository.find({
|
const scenesData = await this.sceneRepository.find({
|
||||||
where: {
|
where: {
|
||||||
space: {
|
space: { uuid: spaceUuid },
|
||||||
uuid: In(spaces ?? []),
|
|
||||||
community: projectUuid ? { project: { uuid: projectUuid } } : null,
|
|
||||||
},
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
...(showInHomePage ? { showInHomePage } : {}),
|
...(showInHomePage ? { showInHomePage } : {}),
|
||||||
},
|
},
|
||||||
relations: ['sceneIcon', 'space', 'space.community'],
|
relations: ['sceneIcon', 'space', 'space.community'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const safeFetch = async (scene: SceneEntity) => {
|
const safeFetch = async (scene: any) => {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { actions, ...sceneDetails } = await this.getScene(
|
const { actions, ...sceneDetails } = await this.getScene(
|
||||||
scene,
|
scene,
|
||||||
scene.space.uuid,
|
spaceUuid,
|
||||||
);
|
);
|
||||||
return sceneDetails;
|
return sceneDetails;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -150,7 +259,7 @@ export class SceneService {
|
|||||||
return scenes.filter(Boolean); // Remove null values
|
return scenes.filter(Boolean); // Remove null values
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`Error fetching Tap-to-Run scenes for specified spaces:`,
|
`Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`,
|
||||||
error.message,
|
error.message,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -182,6 +291,45 @@ export class SceneService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchSceneDetailsFromTuya(
|
||||||
|
sceneId: string,
|
||||||
|
): Promise<SceneDetailsResult> {
|
||||||
|
try {
|
||||||
|
const response = await this.tuyaService.getSceneRule(sceneId);
|
||||||
|
const camelCaseResponse = convertKeysToCamelCase(response);
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
actions: tuyaActions = [],
|
||||||
|
} = camelCaseResponse.result;
|
||||||
|
|
||||||
|
const actions = tuyaActions.map((action) => ({ ...action }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
actions,
|
||||||
|
} as SceneDetailsResult;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`Error fetching scene details for scene ID ${sceneId}:`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
if (err instanceof HttpException) {
|
||||||
|
throw err;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
'An error occurred while fetching scene details from Tuya',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async updateTapToRunScene(
|
async updateTapToRunScene(
|
||||||
updateSceneTapToRunDto: UpdateSceneTapToRunDto,
|
updateSceneTapToRunDto: UpdateSceneTapToRunDto,
|
||||||
sceneUuid: string,
|
sceneUuid: string,
|
||||||
@ -238,38 +386,6 @@ export class SceneService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteScene(params: SceneParamDto): Promise<BaseResponseDto> {
|
|
||||||
const { sceneUuid } = params;
|
|
||||||
try {
|
|
||||||
const scene = await this.findScene(sceneUuid);
|
|
||||||
const space = await this.getSpaceByUuid(scene.space.uuid);
|
|
||||||
|
|
||||||
await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid);
|
|
||||||
await this.sceneDeviceRepository.update(
|
|
||||||
{ uuid: sceneUuid },
|
|
||||||
{ disabled: true },
|
|
||||||
);
|
|
||||||
await this.sceneRepository.update(
|
|
||||||
{
|
|
||||||
uuid: sceneUuid,
|
|
||||||
},
|
|
||||||
{ disabled: true },
|
|
||||||
);
|
|
||||||
return new SuccessResponseDto({
|
|
||||||
message: `Scene with ID ${sceneUuid} deleted successfully`,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof HttpException) {
|
|
||||||
throw err;
|
|
||||||
} else {
|
|
||||||
throw new HttpException(
|
|
||||||
err.message || `Scene not found for id ${params.sceneUuid}`,
|
|
||||||
err.status || HttpStatus.NOT_FOUND,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async addSceneIcon(addSceneIconDto: AddSceneIconDto) {
|
async addSceneIcon(addSceneIconDto: AddSceneIconDto) {
|
||||||
try {
|
try {
|
||||||
const icon = await this.sceneIconRepository.save({
|
const icon = await this.sceneIconRepository.save({
|
||||||
@ -338,237 +454,7 @@ export class SceneService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findScene(sceneUuid: string): Promise<SceneEntity> {
|
async getScene(scene: SceneEntity, spaceUuid: string): Promise<SceneDetails> {
|
||||||
const scene = await this.sceneRepository.findOne({
|
|
||||||
where: { uuid: sceneUuid },
|
|
||||||
relations: ['sceneIcon', 'space', 'space.community'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!scene) {
|
|
||||||
throw new HttpException(
|
|
||||||
`Invalid scene with id ${sceneUuid}`,
|
|
||||||
HttpStatus.NOT_FOUND,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return scene;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSpaceByUuid(spaceUuid: string) {
|
|
||||||
try {
|
|
||||||
const space = await this.spaceRepository.findOne({
|
|
||||||
where: {
|
|
||||||
uuid: spaceUuid,
|
|
||||||
},
|
|
||||||
relations: ['community'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!space) {
|
|
||||||
throw new HttpException(
|
|
||||||
`Invalid space UUID ${spaceUuid}`,
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!space.community.externalId) {
|
|
||||||
throw new HttpException(
|
|
||||||
`Space doesn't have any association with tuya${spaceUuid}`,
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
uuid: space.uuid,
|
|
||||||
createdAt: space.createdAt,
|
|
||||||
updatedAt: space.updatedAt,
|
|
||||||
name: space.spaceName,
|
|
||||||
spaceTuyaUuid: space.community.externalId,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof BadRequestException) {
|
|
||||||
throw err;
|
|
||||||
} else {
|
|
||||||
throw new HttpException(
|
|
||||||
`Space with id ${spaceUuid} not found`,
|
|
||||||
HttpStatus.NOT_FOUND,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async create(
|
|
||||||
spaceTuyaUuid: string,
|
|
||||||
addSceneTapToRunDto: AddSceneTapToRunDto,
|
|
||||||
projectUuid: string,
|
|
||||||
): Promise<SceneEntity> {
|
|
||||||
const { iconUuid, showInHomePage, spaceUuid } = addSceneTapToRunDto;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [defaultSceneIcon] = await Promise.all([
|
|
||||||
this.getDefaultSceneIcon(),
|
|
||||||
]);
|
|
||||||
if (!defaultSceneIcon) {
|
|
||||||
throw new HttpException(
|
|
||||||
'Default scene icon not found',
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.createSceneExternalService(
|
|
||||||
spaceTuyaUuid,
|
|
||||||
addSceneTapToRunDto,
|
|
||||||
projectUuid,
|
|
||||||
);
|
|
||||||
|
|
||||||
const scene = await this.sceneRepository.save({
|
|
||||||
sceneTuyaUuid: response.result.id,
|
|
||||||
sceneIcon: { uuid: iconUuid || defaultSceneIcon.uuid },
|
|
||||||
showInHomePage,
|
|
||||||
space: { uuid: spaceUuid },
|
|
||||||
});
|
|
||||||
|
|
||||||
return scene;
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof HttpException) {
|
|
||||||
throw err;
|
|
||||||
} else if (err.message?.includes('tuya')) {
|
|
||||||
throw new HttpException(
|
|
||||||
'API error: Failed to create scene',
|
|
||||||
HttpStatus.BAD_GATEWAY,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new HttpException(
|
|
||||||
'Database error: Failed to save scene',
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private async updateSceneExternalService(
|
|
||||||
spaceTuyaUuid: string,
|
|
||||||
sceneTuyaUuid: string,
|
|
||||||
updateSceneTapToRunDto: UpdateSceneTapToRunDto,
|
|
||||||
projectUuid: string,
|
|
||||||
) {
|
|
||||||
const { sceneName, decisionExpr, actions } = updateSceneTapToRunDto;
|
|
||||||
try {
|
|
||||||
const formattedActions = await this.prepareActions(actions, projectUuid);
|
|
||||||
|
|
||||||
const response = (await this.tuyaService.updateTapToRunScene(
|
|
||||||
sceneTuyaUuid,
|
|
||||||
spaceTuyaUuid,
|
|
||||||
sceneName,
|
|
||||||
formattedActions,
|
|
||||||
decisionExpr,
|
|
||||||
)) as AddTapToRunSceneInterface;
|
|
||||||
|
|
||||||
if (!response.success) {
|
|
||||||
throw new HttpException(
|
|
||||||
'Failed to update scene in Tuya',
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof HttpException) {
|
|
||||||
throw err;
|
|
||||||
} else if (err.message?.includes('tuya')) {
|
|
||||||
throw new HttpException(
|
|
||||||
'API error: Failed to update scene',
|
|
||||||
HttpStatus.BAD_GATEWAY,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new HttpException(
|
|
||||||
`An Internal error has been occured ${err}`,
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createSceneExternalService(
|
|
||||||
spaceTuyaUuid: string,
|
|
||||||
addSceneTapToRunDto: AddSceneTapToRunDto,
|
|
||||||
projectUuid: string,
|
|
||||||
) {
|
|
||||||
const { sceneName, decisionExpr, actions } = addSceneTapToRunDto;
|
|
||||||
try {
|
|
||||||
const formattedActions = await this.prepareActions(actions, projectUuid);
|
|
||||||
|
|
||||||
const response = (await this.tuyaService.addTapToRunScene(
|
|
||||||
spaceTuyaUuid,
|
|
||||||
sceneName,
|
|
||||||
formattedActions,
|
|
||||||
decisionExpr,
|
|
||||||
)) as AddTapToRunSceneInterface;
|
|
||||||
|
|
||||||
if (!response.result?.id) {
|
|
||||||
throw new HttpException(
|
|
||||||
'Failed to create scene in Tuya',
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof HttpException) {
|
|
||||||
throw err;
|
|
||||||
} else if (err.message?.includes('tuya')) {
|
|
||||||
throw new HttpException(
|
|
||||||
'API error: Failed to create scene',
|
|
||||||
HttpStatus.BAD_GATEWAY,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new HttpException(
|
|
||||||
`An Internal error has been occured ${err}`,
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchSceneDetailsFromTuya(
|
|
||||||
sceneId: string,
|
|
||||||
): Promise<SceneDetailsResult> {
|
|
||||||
try {
|
|
||||||
const response = await this.tuyaService.getSceneRule(sceneId);
|
|
||||||
const camelCaseResponse = convertKeysToCamelCase(response);
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
status,
|
|
||||||
actions: tuyaActions = [],
|
|
||||||
} = camelCaseResponse.result;
|
|
||||||
|
|
||||||
const actions = tuyaActions.map((action) => ({ ...action }));
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
status,
|
|
||||||
actions,
|
|
||||||
} as SceneDetailsResult;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(
|
|
||||||
`Error fetching scene details for scene ID ${sceneId}:`,
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
if (err instanceof HttpException) {
|
|
||||||
throw err;
|
|
||||||
} else {
|
|
||||||
throw new HttpException(
|
|
||||||
'An error occurred while fetching scene details from Tuya',
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getScene(
|
|
||||||
scene: SceneEntity,
|
|
||||||
spaceUuid: string,
|
|
||||||
): Promise<SceneDetails> {
|
|
||||||
try {
|
try {
|
||||||
const { actions, name, status } = await this.fetchSceneDetailsFromTuya(
|
const { actions, name, status } = await this.fetchSceneDetailsFromTuya(
|
||||||
scene.sceneTuyaUuid,
|
scene.sceneTuyaUuid,
|
||||||
@ -633,7 +519,54 @@ export class SceneService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async delete(tuyaSceneId: string, tuyaSpaceId: string) {
|
async deleteScene(params: SceneParamDto): Promise<BaseResponseDto> {
|
||||||
|
const { sceneUuid } = params;
|
||||||
|
try {
|
||||||
|
const scene = await this.findScene(sceneUuid);
|
||||||
|
const space = await this.getSpaceByUuid(scene.space.uuid);
|
||||||
|
|
||||||
|
await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid);
|
||||||
|
await this.sceneDeviceRepository.update(
|
||||||
|
{ uuid: sceneUuid },
|
||||||
|
{ disabled: true },
|
||||||
|
);
|
||||||
|
await this.sceneRepository.update(
|
||||||
|
{
|
||||||
|
uuid: sceneUuid,
|
||||||
|
},
|
||||||
|
{ disabled: true },
|
||||||
|
);
|
||||||
|
return new SuccessResponseDto({
|
||||||
|
message: `Scene with ID ${sceneUuid} deleted successfully`,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof HttpException) {
|
||||||
|
throw err;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
err.message || `Scene not found for id ${params.sceneUuid}`,
|
||||||
|
err.status || HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findScene(sceneUuid: string): Promise<SceneEntity> {
|
||||||
|
const scene = await this.sceneRepository.findOne({
|
||||||
|
where: { uuid: sceneUuid },
|
||||||
|
relations: ['sceneIcon', 'space', 'space.community'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!scene) {
|
||||||
|
throw new HttpException(
|
||||||
|
`Invalid scene with id ${sceneUuid}`,
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(tuyaSceneId: string, tuyaSpaceId: string) {
|
||||||
try {
|
try {
|
||||||
const response = (await this.tuyaService.deleteSceneRule(
|
const response = (await this.tuyaService.deleteSceneRule(
|
||||||
tuyaSceneId,
|
tuyaSceneId,
|
||||||
@ -693,4 +626,45 @@ export class SceneService {
|
|||||||
});
|
});
|
||||||
return defaultIcon;
|
return defaultIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSpaceByUuid(spaceUuid: string) {
|
||||||
|
try {
|
||||||
|
const space = await this.spaceRepository.findOne({
|
||||||
|
where: {
|
||||||
|
uuid: spaceUuid,
|
||||||
|
},
|
||||||
|
relations: ['community'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!space) {
|
||||||
|
throw new HttpException(
|
||||||
|
`Invalid space UUID ${spaceUuid}`,
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!space.community.externalId) {
|
||||||
|
throw new HttpException(
|
||||||
|
`Space doesn't have any association with tuya${spaceUuid}`,
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
uuid: space.uuid,
|
||||||
|
createdAt: space.createdAt,
|
||||||
|
updatedAt: space.updatedAt,
|
||||||
|
name: space.spaceName,
|
||||||
|
spaceTuyaUuid: space.community.externalId,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof BadRequestException) {
|
||||||
|
throw err;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
`Space with id ${spaceUuid} not found`,
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
20
src/timer/create-job.dto.ts
Normal file
20
src/timer/create-job.dto.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { TimerJobTypeEnum } from '@app/common/constants/timer-job-type.enum';
|
||||||
|
|
||||||
|
export class BaseCreateJobDto {
|
||||||
|
type: TimerJobTypeEnum;
|
||||||
|
triggerDate: Date;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
}
|
||||||
|
// validate based on jobType
|
||||||
|
export class CreateUserInvitationJobDto extends BaseCreateJobDto {
|
||||||
|
type: TimerJobTypeEnum.INVITE_USER_EMAIL;
|
||||||
|
metadata: {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
invitationCode: string;
|
||||||
|
role: string;
|
||||||
|
spacesList: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateJobDto = CreateUserInvitationJobDto;
|
10
src/timer/timer.module.ts
Normal file
10
src/timer/timer.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { EmailService } from '@app/common/util/email/email.service';
|
||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { TimerService } from './timer.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [TimerService, EmailService],
|
||||||
|
exports: [TimerService],
|
||||||
|
})
|
||||||
|
export class TimerModule {}
|
53
src/timer/timer.service.ts
Normal file
53
src/timer/timer.service.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { TimerJobTypeEnum } from '@app/common/constants/timer-job-type.enum';
|
||||||
|
import { TimerEntity } from '@app/common/modules/timer/entities/timer.entity';
|
||||||
|
import { TimerRepository } from '@app/common/modules/timer/repositories/timer.repository';
|
||||||
|
import { EmailService } from '@app/common/util/email/email.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
|
import { In, LessThanOrEqual } from 'typeorm';
|
||||||
|
import { CreateJobDto } from './create-job.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TimerService {
|
||||||
|
constructor(
|
||||||
|
private readonly timerRepository: TimerRepository,
|
||||||
|
private readonly emailService: EmailService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
createJob(job: CreateJobDto): Promise<TimerEntity> {
|
||||||
|
const timerEntity = this.timerRepository.create(job);
|
||||||
|
return this.timerRepository.save(timerEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
||||||
|
async handleCron() {
|
||||||
|
const jobsToRun = await this.timerRepository.find({
|
||||||
|
where: { triggerDate: LessThanOrEqual(new Date()) },
|
||||||
|
});
|
||||||
|
const successfulJobs = [];
|
||||||
|
for (const job of jobsToRun) {
|
||||||
|
try {
|
||||||
|
await this.handleJob(job);
|
||||||
|
|
||||||
|
successfulJobs.push(job.uuid);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Job ${job.uuid} failed:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.timerRepository.delete({ uuid: In(successfulJobs) });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleJob(job: TimerEntity) {
|
||||||
|
switch (job.type) {
|
||||||
|
case TimerJobTypeEnum.INVITE_USER_EMAIL:
|
||||||
|
return this.emailService.sendEmailWithInvitationTemplate(
|
||||||
|
job.metadata.email,
|
||||||
|
job.metadata,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
// Handle other job types as needed
|
||||||
|
default:
|
||||||
|
console.warn(`Unhandled job type: ${job.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -30,7 +30,7 @@ export class UserService {
|
|||||||
where: {
|
where: {
|
||||||
uuid: userUuid,
|
uuid: userUuid,
|
||||||
},
|
},
|
||||||
relations: ['region', 'timezone', 'roleType', 'project'],
|
relations: ['region', 'timezone', 'roleType', 'project', 'inviteUser'],
|
||||||
});
|
});
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new BadRequestException('Invalid room UUID');
|
throw new BadRequestException('Invalid room UUID');
|
||||||
@ -53,6 +53,10 @@ export class UserService {
|
|||||||
appAgreementAcceptedAt: user?.appAgreementAcceptedAt,
|
appAgreementAcceptedAt: user?.appAgreementAcceptedAt,
|
||||||
role: user?.roleType,
|
role: user?.roleType,
|
||||||
project: user?.project,
|
project: user?.project,
|
||||||
|
bookingPoints: user?.bookingPoints ?? 0,
|
||||||
|
accessStartDate: user?.inviteUser.accessStartDate,
|
||||||
|
accessEndDate: user?.inviteUser.accessEndDate,
|
||||||
|
bookingEnabled: user?.bookingEnabled,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof BadRequestException) {
|
if (err instanceof BadRequestException) {
|
||||||
|
Reference in New Issue
Block a user