mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-08-25 18:29:38 +00:00
Compare commits
31 Commits
3f2c36d48b
...
dev
Author | SHA1 | Date | |
---|---|---|---|
331c8dffdc | |||
2aa6a40af7 | |||
7ae826eb71 | |||
beed6fcfb7 | |||
015470b5ea | |||
36640b104b | |||
d4af9eaccc | |||
f447cfa065 | |||
d065742d87 | |||
9d952d2fb0 | |||
c0849c2252 | |||
9254db08f9 | |||
b9c4308d1c | |||
db95cc0dab | |||
87c380ab6f | |||
212d0d1974 | |||
a060f92208 | |||
6d529ee0ae | |||
85687e7950 | |||
7e2c3136cf | |||
61348aa351 | |||
dea942f11e | |||
d62e620828 | |||
f0556813ac | |||
6d2252a403 | |||
8d265c9105 | |||
a4095c837b | |||
65d4a56135 | |||
fffa27b6ee | |||
12579fcd6e | |||
2c3b985594 |
64
.github/workflows/pr-description.yml
vendored
Normal file
64
.github/workflows/pr-description.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
name: 🤖 AI PR Description Commenter (100% Safe with jq)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
generate-description:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install GitHub CLI and jq
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gh jq -y
|
||||
|
||||
- name: Fetch PR Commits
|
||||
id: fetch_commits
|
||||
run: |
|
||||
COMMITS=$(gh pr view ${{ github.event.pull_request.number }} --json commits --jq '.commits[].message' | sed 's/^/- /')
|
||||
echo "commits<<EOF" >> $GITHUB_ENV
|
||||
echo "$COMMITS" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Generate PR Description with OpenAI (Safe JSON with jq)
|
||||
run: |
|
||||
REQUEST_BODY=$(jq -n \
|
||||
--arg model "gpt-4o" \
|
||||
--arg content "Given the following commit messages:\n\n${commits}\n\nGenerate a clear and professional pull request description." \
|
||||
'{
|
||||
model: $model,
|
||||
messages: [{ role: "user", content: $content }]
|
||||
}'
|
||||
)
|
||||
|
||||
RESPONSE=$(curl -s https://api.openai.com/v1/chat/completions \
|
||||
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$REQUEST_BODY")
|
||||
|
||||
DESCRIPTION=$(echo "$RESPONSE" | jq -r '.choices[0].message.content')
|
||||
|
||||
echo "---------- OpenAI Raw Response ----------"
|
||||
echo "$RESPONSE"
|
||||
echo "---------- Extracted Description ----------"
|
||||
echo "$DESCRIPTION"
|
||||
|
||||
echo "description<<EOF" >> $GITHUB_ENV
|
||||
echo "$DESCRIPTION" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
commits: ${{ env.commits }}
|
||||
|
||||
- name: Post AI Generated Description as Comment
|
||||
run: |
|
||||
gh pr comment ${{ github.event.pull_request.number }} --body "${{ env.description }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
@ -188,6 +188,11 @@ export class ControllerRoute {
|
||||
static SCENE = class {
|
||||
public static readonly ROUTE = 'scene';
|
||||
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 =
|
||||
'Create a Tap-to-Run Scene';
|
||||
public static readonly CREATE_TAP_TO_RUN_SCENE_DESCRIPTION =
|
||||
@ -223,6 +228,10 @@ export class ControllerRoute {
|
||||
public static readonly CREATE_SPACE_DESCRIPTION =
|
||||
'This endpoint allows you to create a space in a specified community. Optionally, you can specify a parent space to nest the new space under it.';
|
||||
|
||||
public static readonly DUPLICATE_SPACE_SUMMARY = 'Duplicate a space';
|
||||
public static readonly DUPLICATE_SPACE_DESCRIPTION =
|
||||
'This endpoint allows you to create a copy of an existing space in a specified community.';
|
||||
|
||||
public static readonly LIST_SPACE_SUMMARY = 'List spaces in community';
|
||||
public static readonly LIST_SPACE_DESCRIPTION =
|
||||
'List spaces in specified community by community id';
|
||||
@ -768,6 +777,10 @@ export class ControllerRoute {
|
||||
public static readonly ADD_AUTOMATION_DESCRIPTION =
|
||||
'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 =
|
||||
'Get automation details';
|
||||
public static readonly GET_AUTOMATION_DETAILS_DESCRIPTION =
|
||||
|
@ -1,15 +1,13 @@
|
||||
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
|
||||
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||
import {
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { AddDeviceStatusDto } from '../dtos/add.devices-status.dto';
|
||||
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||
import { GetDeviceDetailsFunctionsStatusInterface } from 'src/device/interfaces/get.device.interface';
|
||||
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { firebaseDataBase } from '../../firebase.config';
|
||||
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
|
||||
import {
|
||||
Database,
|
||||
DataSnapshot,
|
||||
@ -17,7 +15,9 @@ import {
|
||||
ref,
|
||||
runTransaction,
|
||||
} from 'firebase/database';
|
||||
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
|
||||
import { GetDeviceDetailsFunctionsStatusInterface } from 'src/device/interfaces/get.device.interface';
|
||||
import { firebaseDataBase } from '../../firebase.config';
|
||||
import { AddDeviceStatusDto } from '../dtos/add.devices-status.dto';
|
||||
@Injectable()
|
||||
export class DeviceStatusFirebaseService {
|
||||
private tuya: TuyaContext;
|
||||
@ -79,35 +79,53 @@ export class DeviceStatusFirebaseService {
|
||||
device: any;
|
||||
}[],
|
||||
): Promise<void> {
|
||||
const allLogs = [];
|
||||
|
||||
console.log(`🔁 Preparing logs from batch of ${batch.length} items...`);
|
||||
|
||||
const allLogs = [];
|
||||
|
||||
for (const item of batch) {
|
||||
const device = item.device;
|
||||
|
||||
if (!device?.uuid) {
|
||||
console.log(`⛔ Skipped unknown device: ${item.deviceTuyaUuid}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const logs = item.log.properties.map((property) =>
|
||||
// Determine properties based on environment
|
||||
const properties =
|
||||
this.isDevEnv && Array.isArray(item.log?.properties)
|
||||
? item.log.properties
|
||||
: Array.isArray(item.status)
|
||||
? item.status
|
||||
: null;
|
||||
|
||||
if (!properties) {
|
||||
console.log(
|
||||
`⛔ Skipped invalid status/properties for device: ${item.deviceTuyaUuid}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const logs = properties.map((property) =>
|
||||
this.deviceStatusLogRepository.create({
|
||||
deviceId: device.uuid,
|
||||
deviceTuyaId: item.deviceTuyaUuid,
|
||||
productId: item.log.productId,
|
||||
productId: device.productDevice?.uuid,
|
||||
log: item.log,
|
||||
code: property.code,
|
||||
value: property.value,
|
||||
eventId: item.log.dataId,
|
||||
eventTime: new Date(property.time).toISOString(),
|
||||
eventId: item.log?.dataId,
|
||||
eventTime: new Date(
|
||||
this.isDevEnv ? property.time : property.t,
|
||||
).toISOString(),
|
||||
}),
|
||||
);
|
||||
|
||||
allLogs.push(...logs);
|
||||
}
|
||||
|
||||
console.log(`📝 Total logs to insert: ${allLogs.length}`);
|
||||
|
||||
const insertLogsPromise = (async () => {
|
||||
const chunkSize = 300;
|
||||
let insertedCount = 0;
|
||||
|
||||
@ -117,9 +135,9 @@ export class DeviceStatusFirebaseService {
|
||||
const result = await this.deviceStatusLogRepository
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into('device-status-log') // or use DeviceStatusLogEntity
|
||||
.into('device-status-log')
|
||||
.values(chunk)
|
||||
.orIgnore() // skip duplicates
|
||||
.orIgnore()
|
||||
.execute();
|
||||
|
||||
insertedCount += result.identifiers.length;
|
||||
@ -131,12 +149,7 @@ export class DeviceStatusFirebaseService {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ Total logs inserted: ${insertedCount} / ${allLogs.length}`,
|
||||
);
|
||||
})();
|
||||
|
||||
await insertLogsPromise;
|
||||
console.log(`✅ Total logs inserted: ${insertedCount} / ${allLogs.length}`);
|
||||
}
|
||||
|
||||
async addDeviceStatusToFirebase(
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import TuyaWebsocket from '../../config/tuya-web-socket-config';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
||||
import { SosHandlerService } from './sos.handler.service';
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as NodeCache from 'node-cache';
|
||||
import TuyaWebsocket from '../../config/tuya-web-socket-config';
|
||||
import { SosHandlerService } from './sos.handler.service';
|
||||
|
||||
@Injectable()
|
||||
export class TuyaWebSocketService implements OnModuleInit {
|
||||
@ -74,7 +74,12 @@ export class TuyaWebSocketService implements OnModuleInit {
|
||||
this.client.message(async (ws: WebSocket, message: any) => {
|
||||
try {
|
||||
const { devId, status, logData } = this.extractMessageData(message);
|
||||
if (!Array.isArray(logData?.properties)) {
|
||||
// console.log(
|
||||
// `📬 Received message for device: ${devId}, status:`,
|
||||
// status,
|
||||
// logData,
|
||||
// );
|
||||
if (!Array.isArray(status)) {
|
||||
this.client.ackMessage(message.messageId);
|
||||
return;
|
||||
}
|
||||
@ -162,6 +167,8 @@ export class TuyaWebSocketService implements OnModuleInit {
|
||||
status: any;
|
||||
logData: any;
|
||||
} {
|
||||
// console.log('Received message:', message);
|
||||
|
||||
const payloadData = message.payload.data;
|
||||
|
||||
if (this.isDevEnv) {
|
||||
|
@ -22,6 +22,11 @@ export class SpaceProductAllocationEntity extends AbstractEntity<SpaceProductAll
|
||||
})
|
||||
public space: SpaceEntity;
|
||||
|
||||
@Column({
|
||||
name: 'space_uuid',
|
||||
})
|
||||
public spaceUuid: string;
|
||||
|
||||
@ManyToOne(() => SpaceModelProductAllocationEntity, {
|
||||
nullable: true,
|
||||
onDelete: 'SET NULL',
|
||||
@ -31,9 +36,19 @@ export class SpaceProductAllocationEntity extends AbstractEntity<SpaceProductAll
|
||||
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
||||
public product: ProductEntity;
|
||||
|
||||
@Column({
|
||||
name: 'product_uuid',
|
||||
})
|
||||
public productUuid: string;
|
||||
|
||||
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' })
|
||||
public tag: NewTagEntity;
|
||||
|
||||
@Column({
|
||||
name: 'tag_uuid',
|
||||
})
|
||||
public tagUuid: string;
|
||||
|
||||
constructor(partial: Partial<SpaceProductAllocationEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||
import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities';
|
||||
import { BookableSpaceEntity } from '../../booking/entities/bookable-space.entity';
|
||||
import { BookingEntity } from '../../booking/entities/booking.entity';
|
||||
import { CommunityEntity } from '../../community/entities';
|
||||
import { DeviceEntity } from '../../device/entities';
|
||||
import { InviteUserSpaceEntity } from '../../Invite-user/entities';
|
||||
@ -20,7 +21,6 @@ import { UserSpaceEntity } from '../../user/entities';
|
||||
import { SpaceDto } from '../dtos';
|
||||
import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
|
||||
import { SubspaceEntity } from './subspace/subspace.entity';
|
||||
import { BookingEntity } from '../../booking/entities/booking.entity';
|
||||
|
||||
@Entity({ name: 'space' })
|
||||
export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||
@ -47,12 +47,26 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||
@JoinColumn({ name: 'community_id' })
|
||||
community: CommunityEntity;
|
||||
|
||||
@ManyToOne(() => SpaceEntity, (space) => space.children, { nullable: true })
|
||||
@Column({
|
||||
name: 'community_id',
|
||||
})
|
||||
communityId: string;
|
||||
|
||||
@ManyToOne(() => SpaceEntity, (space) => space.children, {
|
||||
nullable: true,
|
||||
})
|
||||
parent: SpaceEntity;
|
||||
|
||||
@Column({
|
||||
name: 'parent_uuid',
|
||||
nullable: true,
|
||||
})
|
||||
public parentUuid: string;
|
||||
|
||||
@OneToMany(() => SpaceEntity, (space) => space.parent, {
|
||||
nullable: false,
|
||||
onDelete: 'CASCADE',
|
||||
cascade: true,
|
||||
})
|
||||
children: SpaceEntity[];
|
||||
|
||||
@ -73,16 +87,10 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||
|
||||
@OneToMany(() => SubspaceEntity, (subspace) => subspace.space, {
|
||||
nullable: true,
|
||||
cascade: true,
|
||||
})
|
||||
subspaces?: SubspaceEntity[];
|
||||
|
||||
// Position columns
|
||||
@Column({ type: 'float', nullable: false, default: 0 })
|
||||
public x: number; // X coordinate for position
|
||||
|
||||
@Column({ type: 'float', nullable: false, default: 0 })
|
||||
public y: number; // Y coordinate for position
|
||||
|
||||
@OneToMany(
|
||||
() => DeviceEntity,
|
||||
(devicesSpaceEntity) => devicesSpaceEntity.spaceDevice,
|
||||
|
@ -22,6 +22,11 @@ export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProd
|
||||
})
|
||||
public subspace: SubspaceEntity;
|
||||
|
||||
@Column({
|
||||
name: 'subspace_uuid',
|
||||
})
|
||||
public subspaceUuid: string;
|
||||
|
||||
@ManyToOne(() => SubspaceModelProductAllocationEntity, {
|
||||
nullable: true,
|
||||
onDelete: 'SET NULL',
|
||||
@ -31,9 +36,19 @@ export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProd
|
||||
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
||||
public product: ProductEntity;
|
||||
|
||||
@Column({
|
||||
name: 'product_uuid',
|
||||
})
|
||||
public productUuid: string;
|
||||
|
||||
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' })
|
||||
public tag: NewTagEntity;
|
||||
|
||||
@Column({
|
||||
name: 'tag_uuid',
|
||||
})
|
||||
public tagUuid: string;
|
||||
|
||||
constructor(partial: Partial<SubspaceProductAllocationEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
|
@ -26,6 +26,11 @@ export class SubspaceEntity extends AbstractEntity<SubspaceDto> {
|
||||
@JoinColumn({ name: 'space_uuid' })
|
||||
space: SpaceEntity;
|
||||
|
||||
@Column({
|
||||
name: 'space_uuid',
|
||||
})
|
||||
public spaceUuid: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: false,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios from 'axios';
|
||||
import nodemailer from 'nodemailer';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import Mail from 'nodemailer/lib/mailer';
|
||||
import { BatchEmailData } from './batch-email.interface';
|
||||
import { SingleEmailData } from './single-email.interface';
|
||||
|
31
package-lock.json
generated
31
package-lock.json
generated
@ -48,6 +48,7 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
@ -2365,6 +2366,18 @@
|
||||
"rxjs": "^7.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cqrs/node_modules/uuid": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz",
|
||||
"integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/jwt": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz",
|
||||
@ -12749,18 +12762,6 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/typeorm/node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
@ -12896,9 +12897,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz",
|
||||
"integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==",
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
|
@ -60,6 +60,7 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { AutomationService } from '../services/automation.service';
|
||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||
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 {
|
||||
Body,
|
||||
Controller,
|
||||
@ -9,20 +12,20 @@ import {
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||
import { AutomationParamDto } from '../dtos';
|
||||
import {
|
||||
AddAutomationDto,
|
||||
UpdateAutomationDto,
|
||||
UpdateAutomationStatusDto,
|
||||
} from '../dtos/automation.dto';
|
||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||
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';
|
||||
import { GetAutomationBySpacesDto } from '../dtos/get-automation-by-spaces.dto';
|
||||
import { AutomationService } from '../services/automation.service';
|
||||
|
||||
@ApiTags('Automation Module')
|
||||
@Controller({
|
||||
@ -56,6 +59,28 @@ 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()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('AUTOMATION_VIEW')
|
||||
|
20
src/automation/dtos/get-automation-by-spaces.dto.ts
Normal file
20
src/automation/dtos/get-automation-by-spaces.dto.ts
Normal file
@ -0,0 +1,20 @@
|
||||
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,28 +1,3 @@
|
||||
import {
|
||||
Injectable,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
||||
import {
|
||||
AddAutomationDto,
|
||||
AutomationParamDto,
|
||||
UpdateAutomationDto,
|
||||
UpdateAutomationStatusDto,
|
||||
} from '../dtos';
|
||||
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 {
|
||||
Action,
|
||||
AddAutomationParams,
|
||||
AutomationDetailsResult,
|
||||
AutomationResponseData,
|
||||
Condition,
|
||||
} from '../interface/automation.interface';
|
||||
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
|
||||
import {
|
||||
ActionExecutorEnum,
|
||||
ActionTypeEnum,
|
||||
@ -30,18 +5,45 @@ import {
|
||||
AUTOMATION_TYPE,
|
||||
EntityTypeEnum,
|
||||
} from '@app/common/constants/automation.enum';
|
||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||
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 { 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 { SpaceRepository } from '@app/common/modules/space/repositories';
|
||||
import {
|
||||
BadRequestException,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
|
||||
import { DeviceService } from 'src/device/services';
|
||||
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';
|
||||
import { In } from 'typeorm';
|
||||
import {
|
||||
AddAutomationDto,
|
||||
AutomationParamDto,
|
||||
UpdateAutomationDto,
|
||||
UpdateAutomationStatusDto,
|
||||
} from '../dtos';
|
||||
import { GetAutomationBySpacesDto } from '../dtos/get-automation-by-spaces.dto';
|
||||
import {
|
||||
Action,
|
||||
AddAutomationParams,
|
||||
AutomationDetailsResult,
|
||||
AutomationResponseData,
|
||||
Condition,
|
||||
} from '../interface/automation.interface';
|
||||
|
||||
@Injectable()
|
||||
export class AutomationService {
|
||||
@ -112,128 +114,25 @@ export class AutomationService {
|
||||
);
|
||||
}
|
||||
}
|
||||
async createAutomationExternalService(
|
||||
params: AddAutomationParams,
|
||||
async getAutomationBySpace({ projectUuid, spaceUuid }: GetSpaceParam) {
|
||||
return this.getAutomationBySpaces({ spaces: [spaceUuid] }, projectUuid);
|
||||
}
|
||||
|
||||
async getAutomationBySpaces(
|
||||
{ spaces }: GetAutomationBySpacesDto,
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
await this.validateProject(projectUuid);
|
||||
|
||||
// Fetch automation data from the repository
|
||||
const automationData = await this.automationRepository.find({
|
||||
where: {
|
||||
space: {
|
||||
uuid: param.spaceUuid,
|
||||
uuid: In(spaces ?? []),
|
||||
community: {
|
||||
uuid: param.communityUuid,
|
||||
project: {
|
||||
uuid: param.projectUuid,
|
||||
uuid: projectUuid,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -290,46 +189,277 @@ export class AutomationService {
|
||||
}
|
||||
}
|
||||
|
||||
async findAutomationBySpace(spaceUuid: string, projectUuid: string) {
|
||||
async getAutomationDetails(param: AutomationParamDto) {
|
||||
await this.validateProject(param.projectUuid);
|
||||
|
||||
try {
|
||||
await this.getSpaceByUuid(spaceUuid, projectUuid);
|
||||
const automation = await this.findAutomationByUuid(
|
||||
param.automationUuid,
|
||||
param.projectUuid,
|
||||
);
|
||||
|
||||
const automationData = await this.automationRepository.find({
|
||||
where: {
|
||||
space: { uuid: spaceUuid },
|
||||
disabled: false,
|
||||
},
|
||||
relations: ['space'],
|
||||
});
|
||||
|
||||
const automations = await Promise.all(
|
||||
automationData.map(async (automation) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { actions, ...automationDetails } =
|
||||
await this.getAutomation(automation);
|
||||
const automationDetails = await this.getAutomation(automation);
|
||||
|
||||
return automationDetails;
|
||||
}),
|
||||
);
|
||||
|
||||
return automations;
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`,
|
||||
err.message,
|
||||
`Error fetching automation details for automationUuid ${param.automationUuid}:`,
|
||||
error,
|
||||
);
|
||||
|
||||
if (err instanceof HttpException) {
|
||||
throw err;
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
} else {
|
||||
throw new HttpException(
|
||||
'An error occurred while retrieving scenes',
|
||||
'An error occurred while retrieving automation details',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
async getTapToRunSceneDetailsTuya(
|
||||
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: {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getTapToRunSceneDetailsTuya(
|
||||
sceneUuid: string,
|
||||
): Promise<AutomationDetailsResult> {
|
||||
try {
|
||||
@ -361,35 +491,8 @@ export class AutomationService {
|
||||
}
|
||||
}
|
||||
}
|
||||
async getAutomationDetails(param: AutomationParamDto) {
|
||||
await this.validateProject(param.projectUuid);
|
||||
|
||||
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) {
|
||||
private async getAutomation(automation: AutomationEntity) {
|
||||
try {
|
||||
const response = await this.tuyaService.getSceneRule(
|
||||
automation.automationTuyaUuid,
|
||||
@ -496,13 +599,13 @@ export class AutomationService {
|
||||
}
|
||||
}
|
||||
}
|
||||
async findAutomation(
|
||||
sceneUuid: string,
|
||||
private async findAutomationByUuid(
|
||||
uuid: string,
|
||||
projectUuid: string,
|
||||
): Promise<AutomationEntity> {
|
||||
const automation = await this.automationRepository.findOne({
|
||||
where: {
|
||||
uuid: sceneUuid,
|
||||
uuid: uuid,
|
||||
space: { community: { project: { uuid: projectUuid } } },
|
||||
},
|
||||
relations: ['space'],
|
||||
@ -510,57 +613,14 @@ export class AutomationService {
|
||||
|
||||
if (!automation) {
|
||||
throw new HttpException(
|
||||
`Invalid automation with id ${sceneUuid}`,
|
||||
`Invalid automation with id ${uuid}`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
return automation;
|
||||
}
|
||||
|
||||
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) {
|
||||
private async delete(tuyaAutomationId: string, tuyaSpaceId: string) {
|
||||
try {
|
||||
const response = (await this.tuyaService.deleteSceneRule(
|
||||
tuyaAutomationId,
|
||||
@ -578,7 +638,7 @@ export class AutomationService {
|
||||
}
|
||||
}
|
||||
}
|
||||
async updateAutomationExternalService(
|
||||
private async updateAutomationExternalService(
|
||||
spaceTuyaUuid: string,
|
||||
automationUuid: string,
|
||||
updateAutomationDto: UpdateAutomationDto,
|
||||
@ -626,95 +686,6 @@ 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(
|
||||
actions: Action[],
|
||||
@ -753,7 +724,7 @@ export class AutomationService {
|
||||
action.action_executor === ActionExecutorEnum.RULE_ENABLE
|
||||
) {
|
||||
if (action.action_type === ActionTypeEnum.AUTOMATION) {
|
||||
const automation = await this.findAutomation(
|
||||
const automation = await this.findAutomationByUuid(
|
||||
action.entity_id,
|
||||
projectUuid,
|
||||
);
|
||||
|
@ -3,11 +3,11 @@ import { IsNotEmpty, IsOptional, IsUUID, Matches } from 'class-validator';
|
||||
|
||||
export class BookingRequestDto {
|
||||
@ApiProperty({
|
||||
description: 'Month in MM/YYYY format',
|
||||
example: '07/2025',
|
||||
description: 'Month in MM-YYYY format',
|
||||
example: '07-2025',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@Matches(/^(0[1-9]|1[0-2])\/\d{4}$/, {
|
||||
@Matches(/^(0[1-9]|1[0-2])\-\d{4}$/, {
|
||||
message: 'Date must be in MM/YYYY format',
|
||||
})
|
||||
month: string;
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsDate, IsNotEmpty, IsString, IsUUID, Matches } from 'class-validator';
|
||||
import {
|
||||
IsDate,
|
||||
IsNotEmpty,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Matches,
|
||||
MinDate,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateBookingDto {
|
||||
@ApiProperty({
|
||||
@ -15,6 +22,7 @@ export class CreateBookingDto {
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsDate()
|
||||
@MinDate(new Date())
|
||||
date: Date;
|
||||
|
||||
@ApiProperty({ example: '09:00' })
|
||||
|
@ -54,7 +54,8 @@ export class BookableSpaceService {
|
||||
.createQueryBuilder('space')
|
||||
.leftJoinAndSelect('space.parent', 'parentSpace')
|
||||
.leftJoinAndSelect('space.community', 'community')
|
||||
.where('community.project = :project', { project });
|
||||
.where('space.disabled = :disabled', { disabled: false })
|
||||
.andWhere('community.project = :project', { project });
|
||||
|
||||
if (search) {
|
||||
qb = qb.andWhere(
|
||||
|
@ -51,7 +51,7 @@ export class BookingService {
|
||||
}
|
||||
|
||||
async findAll({ month, space }: BookingRequestDto, project: string) {
|
||||
const [monthNumber, year] = month.split('/').map(Number);
|
||||
const [monthNumber, year] = month.split('-').map(Number);
|
||||
const fromDate = new Date(year, monthNumber - 1, 1);
|
||||
const toDate = new Date(year, monthNumber, 0, 23, 59, 59);
|
||||
return this.bookingEntityRepository.find({
|
||||
|
@ -25,7 +25,7 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { SpaceService } from 'src/space/services';
|
||||
import { QueryRunner, SelectQueryBuilder } from 'typeorm';
|
||||
import { Brackets, QueryRunner, SelectQueryBuilder } from 'typeorm';
|
||||
import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos';
|
||||
import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
|
||||
|
||||
@ -184,18 +184,46 @@ export class CommunityService {
|
||||
|
||||
let qb: undefined | SelectQueryBuilder<CommunityEntity> = undefined;
|
||||
|
||||
const matchingCommunityIdsQb = this.communityRepository
|
||||
.createQueryBuilder('c')
|
||||
.select('c.uuid')
|
||||
.where('c.project = :projectUuid', { projectUuid })
|
||||
.andWhere('c.name != :orphanCommunityName', {
|
||||
orphanCommunityName: `${ORPHAN_COMMUNITY_NAME}-${project.name}`,
|
||||
})
|
||||
.distinct(true);
|
||||
if (includeSpaces) {
|
||||
matchingCommunityIdsQb.leftJoin('c.spaces', 'space');
|
||||
}
|
||||
|
||||
if (search) {
|
||||
matchingCommunityIdsQb
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where('c.name ILIKE :search');
|
||||
if (includeSpaces) qb.orWhere('space.spaceName ILIKE :search');
|
||||
}),
|
||||
)
|
||||
.setParameter('search', `%${search}%`);
|
||||
}
|
||||
|
||||
qb = this.communityRepository
|
||||
.createQueryBuilder('c')
|
||||
.where('c.project = :projectUuid', { projectUuid })
|
||||
.andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`)
|
||||
.distinct(true);
|
||||
|
||||
.andWhere('c.name != :orphanCommunityName', {
|
||||
orphanCommunityName: `${ORPHAN_COMMUNITY_NAME}-${project.name}`,
|
||||
})
|
||||
.andWhere(`c.uuid IN (${matchingCommunityIdsQb.getQuery()})`)
|
||||
.setParameters(matchingCommunityIdsQb.getParameters());
|
||||
if (includeSpaces) {
|
||||
qb.leftJoinAndSelect(
|
||||
'c.spaces',
|
||||
'space',
|
||||
'space.disabled = :disabled AND space.spaceName != :orphanSpaceName',
|
||||
{ disabled: false, orphanSpaceName: ORPHAN_SPACE_NAME },
|
||||
{
|
||||
disabled: false,
|
||||
orphanSpaceName: ORPHAN_SPACE_NAME,
|
||||
},
|
||||
)
|
||||
.leftJoinAndSelect('space.parent', 'parent')
|
||||
.leftJoinAndSelect(
|
||||
@ -204,16 +232,7 @@ export class CommunityService {
|
||||
'children.disabled = :disabled',
|
||||
{ disabled: false },
|
||||
);
|
||||
// .leftJoinAndSelect('space.spaceModel', 'spaceModel')
|
||||
}
|
||||
|
||||
if (search) {
|
||||
qb.andWhere(
|
||||
`c.name ILIKE :search ${includeSpaces ? 'OR space.space_name ILIKE :search' : ''}`,
|
||||
{ search: `%${search}%` },
|
||||
);
|
||||
}
|
||||
|
||||
const customModel = TypeORMCustomModel(this.communityRepository);
|
||||
|
||||
const { baseResponseDto, paginationResponseDto } =
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { SceneService } from '../services/scene.service';
|
||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||
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 {
|
||||
Body,
|
||||
Controller,
|
||||
@ -8,21 +11,21 @@ import {
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||
import { SceneParamDto } from '../dtos';
|
||||
import {
|
||||
AddSceneIconDto,
|
||||
AddSceneTapToRunDto,
|
||||
GetSceneDto,
|
||||
UpdateSceneTapToRunDto,
|
||||
} from '../dtos/scene.dto';
|
||||
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';
|
||||
import { SceneService } from '../services/scene.service';
|
||||
|
||||
@ApiTags('Scene Module')
|
||||
@Controller({
|
||||
@ -52,6 +55,27 @@ 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()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('SCENES_DELETE')
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsNotEmpty,
|
||||
IsString,
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
IsOptional,
|
||||
IsNumber,
|
||||
IsBoolean,
|
||||
} from 'class-validator';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { BooleanValues } from '@app/common/constants/boolean-values.enum';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
class ExecutorProperty {
|
||||
@ApiProperty({
|
||||
@ -187,4 +188,19 @@ export class GetSceneDto {
|
||||
return value.obj.showInHomePage === BooleanValues.TRUE;
|
||||
})
|
||||
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,12 +1,36 @@
|
||||
import {
|
||||
Injectable,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
ActionExecutorEnum,
|
||||
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,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
||||
import { HttpStatusCode } from 'axios';
|
||||
import { DeviceService } from 'src/device/services';
|
||||
import { In } from 'typeorm';
|
||||
import {
|
||||
Action,
|
||||
AddSceneIconDto,
|
||||
@ -15,35 +39,12 @@ import {
|
||||
SceneParamDto,
|
||||
UpdateSceneTapToRunDto,
|
||||
} from '../dtos';
|
||||
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
|
||||
import {
|
||||
AddTapToRunSceneInterface,
|
||||
DeleteTapToRunSceneInterface,
|
||||
SceneDetails,
|
||||
SceneDetailsResult,
|
||||
} 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()
|
||||
export class SceneService {
|
||||
@ -92,158 +93,48 @@ export class SceneService {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async findScenesBySpace(spaceUuid: string, filter: GetSceneDto) {
|
||||
async findScenesBySpace(spaceUuid: string, { showInHomePage }: GetSceneDto) {
|
||||
try {
|
||||
await this.getSpaceByUuid(spaceUuid);
|
||||
const showInHomePage = filter?.showInHomePage;
|
||||
return this.findScenesBySpaces({ showInHomePage, spaces: [spaceUuid] });
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`,
|
||||
error.message,
|
||||
);
|
||||
|
||||
throw error instanceof HttpException
|
||||
? error
|
||||
: new HttpException(
|
||||
'An error occurred while retrieving scenes',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async findScenesBySpaces(
|
||||
{ showInHomePage, spaces }: GetSceneDto,
|
||||
projectUuid?: string,
|
||||
) {
|
||||
try {
|
||||
const scenesData = await this.sceneRepository.find({
|
||||
where: {
|
||||
space: { uuid: spaceUuid },
|
||||
space: {
|
||||
uuid: In(spaces ?? []),
|
||||
community: projectUuid ? { project: { uuid: projectUuid } } : null,
|
||||
},
|
||||
disabled: false,
|
||||
...(showInHomePage ? { showInHomePage } : {}),
|
||||
},
|
||||
relations: ['sceneIcon', 'space', 'space.community'],
|
||||
});
|
||||
|
||||
const safeFetch = async (scene: any) => {
|
||||
const safeFetch = async (scene: SceneEntity) => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { actions, ...sceneDetails } = await this.getScene(
|
||||
scene,
|
||||
spaceUuid,
|
||||
scene.space.uuid,
|
||||
);
|
||||
return sceneDetails;
|
||||
} catch (error) {
|
||||
@ -259,7 +150,7 @@ export class SceneService {
|
||||
return scenes.filter(Boolean); // Remove null values
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`,
|
||||
`Error fetching Tap-to-Run scenes for specified spaces:`,
|
||||
error.message,
|
||||
);
|
||||
|
||||
@ -291,45 +182,6 @@ 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(
|
||||
updateSceneTapToRunDto: UpdateSceneTapToRunDto,
|
||||
sceneUuid: string,
|
||||
@ -386,6 +238,38 @@ 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) {
|
||||
try {
|
||||
const icon = await this.sceneIconRepository.save({
|
||||
@ -454,7 +338,237 @@ export class SceneService {
|
||||
}
|
||||
}
|
||||
|
||||
async getScene(scene: SceneEntity, spaceUuid: string): Promise<SceneDetails> {
|
||||
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 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 {
|
||||
const { actions, name, status } = await this.fetchSceneDetailsFromTuya(
|
||||
scene.sceneTuyaUuid,
|
||||
@ -519,54 +633,7 @@ 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 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) {
|
||||
private async delete(tuyaSceneId: string, tuyaSpaceId: string) {
|
||||
try {
|
||||
const response = (await this.tuyaService.deleteSceneRule(
|
||||
tuyaSceneId,
|
||||
@ -626,45 +693,4 @@ export class SceneService {
|
||||
});
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||
import { AddSpaceDto, CommunitySpaceParam, UpdateSpaceDto } from '../dtos';
|
||||
import { DuplicateSpaceDto } from '../dtos/duplicate-space.dto';
|
||||
import { GetSpaceDto } from '../dtos/get.space.dto';
|
||||
import { GetSpaceParam } from '../dtos/get.space.param';
|
||||
import { OrderSpacesDto } from '../dtos/order.spaces.dto';
|
||||
@ -48,6 +49,26 @@ export class SpaceController {
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('SPACE_ADD')
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.SPACE.ACTIONS.DUPLICATE_SPACE_SUMMARY,
|
||||
description: ControllerRoute.SPACE.ACTIONS.DUPLICATE_SPACE_DESCRIPTION,
|
||||
})
|
||||
@Post(':spaceUuid/duplicate')
|
||||
async duplicateSpace(
|
||||
@Param('spaceUuid', ParseUUIDPipe) spaceUuid: string,
|
||||
@Body() dto: DuplicateSpaceDto,
|
||||
@Param() communitySpaceParam: CommunitySpaceParam,
|
||||
): Promise<BaseResponseDto> {
|
||||
return await this.spaceService.duplicateSpace(
|
||||
spaceUuid,
|
||||
communitySpaceParam,
|
||||
dto,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('SPACE_VIEW')
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
IsArray,
|
||||
IsMongoId,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
@ -48,14 +47,6 @@ export class AddSpaceDto {
|
||||
@IsOptional()
|
||||
public icon?: string;
|
||||
|
||||
@ApiProperty({ description: 'X position on canvas', example: 120 })
|
||||
@IsNumber()
|
||||
x: number;
|
||||
|
||||
@ApiProperty({ description: 'Y position on canvas', example: 200 })
|
||||
@IsNumber()
|
||||
y: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'UUID of the Space Model',
|
||||
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
|
||||
|
18
src/space/dtos/duplicate-space.dto.ts
Normal file
18
src/space/dtos/duplicate-space.dto.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { ORPHAN_SPACE_NAME } from '@app/common/constants/orphan-constant';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, NotEquals } from 'class-validator';
|
||||
|
||||
export class DuplicateSpaceDto {
|
||||
@ApiProperty({
|
||||
description: 'Name of the space (e.g., Floor 1, Unit 101)',
|
||||
example: 'Unit 101',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@NotEquals(ORPHAN_SPACE_NAME, {
|
||||
message() {
|
||||
return `Space name cannot be "${ORPHAN_SPACE_NAME}". Please choose a different name.`;
|
||||
},
|
||||
})
|
||||
spaceName: string;
|
||||
}
|
@ -4,7 +4,6 @@ import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
NotEquals,
|
||||
@ -36,16 +35,6 @@ export class UpdateSpaceDto {
|
||||
@IsOptional()
|
||||
public icon?: string;
|
||||
|
||||
@ApiProperty({ description: 'X position on canvas', example: 120 })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
x?: number;
|
||||
|
||||
@ApiProperty({ description: 'Y position on canvas', example: 200 })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
y?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'List of subspace modifications',
|
||||
type: [UpdateSubspaceDto],
|
||||
|
@ -133,7 +133,7 @@ export class ValidationService {
|
||||
'subspaces.productAllocations',
|
||||
'subspaces.productAllocations.product',
|
||||
'subspaces.devices',
|
||||
'spaceModel',
|
||||
// 'spaceModel',
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -8,6 +8,7 @@ import { generateRandomString } from '@app/common/helper/randomString';
|
||||
import { removeCircularReferences } from '@app/common/helper/removeCircularReferences';
|
||||
import { SpaceProductAllocationEntity } from '@app/common/modules/space/entities/space-product-allocation.entity';
|
||||
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
||||
import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity';
|
||||
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
|
||||
import {
|
||||
InviteSpaceRepository,
|
||||
@ -24,6 +25,7 @@ import { DeviceService } from 'src/device/services';
|
||||
import { SpaceModelService } from 'src/space-model/services';
|
||||
import { TagService } from 'src/tags/services/tags.service';
|
||||
import { DataSource, In, Not, QueryRunner } from 'typeorm';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import { DisableSpaceCommand } from '../commands';
|
||||
import {
|
||||
AddSpaceDto,
|
||||
@ -32,6 +34,7 @@ import {
|
||||
UpdateSpaceDto,
|
||||
} from '../dtos';
|
||||
import { CreateProductAllocationDto } from '../dtos/create-product-allocation.dto';
|
||||
import { DuplicateSpaceDto } from '../dtos/duplicate-space.dto';
|
||||
import { GetSpaceDto } from '../dtos/get.space.dto';
|
||||
import { OrderSpacesDto } from '../dtos/order.spaces.dto';
|
||||
import { SpaceWithParentsDto } from '../dtos/space.parents.dto';
|
||||
@ -93,6 +96,9 @@ export class SpaceService {
|
||||
parentUuid && !isRecursiveCall
|
||||
? await this.validationService.validateSpace(parentUuid)
|
||||
: null;
|
||||
if (parent) {
|
||||
await this.validateNamingConflict(addSpaceDto.spaceName, parent);
|
||||
}
|
||||
|
||||
const spaceModel = spaceModelUuid
|
||||
? await this.validationService.validateSpaceModel(spaceModelUuid)
|
||||
@ -102,8 +108,6 @@ export class SpaceService {
|
||||
// todo: find a better way to handle this instead of naming every key
|
||||
spaceName: addSpaceDto.spaceName,
|
||||
icon: addSpaceDto.icon,
|
||||
x: addSpaceDto.x,
|
||||
y: addSpaceDto.y,
|
||||
spaceModel,
|
||||
parent: isRecursiveCall
|
||||
? recursiveCallParentEntity
|
||||
@ -181,6 +185,154 @@ export class SpaceService {
|
||||
!isRecursiveCall ? await queryRunner.release() : null;
|
||||
}
|
||||
}
|
||||
|
||||
async duplicateSpace(
|
||||
spaceUuid: string,
|
||||
{ communityUuid, projectUuid }: CommunitySpaceParam,
|
||||
dto: DuplicateSpaceDto,
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<BaseResponseDto> {
|
||||
if (!queryRunner) {
|
||||
queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
await this.validationService.validateCommunityAndProject(
|
||||
communityUuid,
|
||||
projectUuid,
|
||||
queryRunner,
|
||||
);
|
||||
await this.handleSpaceDuplication(spaceUuid, dto.spaceName, queryRunner);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
const { data } = await this.getSpacesHierarchyForCommunity(
|
||||
{
|
||||
projectUuid,
|
||||
communityUuid,
|
||||
},
|
||||
{ onlyWithDevices: false },
|
||||
);
|
||||
return new SuccessResponseDto({
|
||||
message: `Space with ID ${spaceUuid} successfully duplicated`,
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
console.log((error as Error).stack);
|
||||
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
private async handleSpaceDuplication(
|
||||
spaceUuid: string,
|
||||
newSpaceName: string | null,
|
||||
queryRunner: QueryRunner,
|
||||
parent?: SpaceEntity,
|
||||
) {
|
||||
const space = await this.spaceRepository.findOne({
|
||||
where: { uuid: spaceUuid },
|
||||
relations: [
|
||||
'children',
|
||||
'productAllocations',
|
||||
'subspaces',
|
||||
'subspaces.productAllocations',
|
||||
],
|
||||
});
|
||||
const clonedSpace = structuredClone(space);
|
||||
const newSpace = queryRunner.manager.create(SpaceEntity, {
|
||||
...clonedSpace,
|
||||
spaceName: newSpaceName || clonedSpace.spaceName,
|
||||
parent,
|
||||
children: undefined,
|
||||
subspaces: undefined,
|
||||
productAllocations: undefined,
|
||||
uuid: uuidV4(),
|
||||
});
|
||||
|
||||
if (clonedSpace.productAllocations?.length) {
|
||||
newSpace.productAllocations = this.copySpaceAllocations(
|
||||
newSpace,
|
||||
clonedSpace.productAllocations,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
if (clonedSpace.subspaces?.length) {
|
||||
newSpace.subspaces = this.copySpaceSubspaces(
|
||||
newSpace,
|
||||
clonedSpace.subspaces,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
const savedSpace = await queryRunner.manager.save(newSpace);
|
||||
|
||||
if (clonedSpace.children?.length) {
|
||||
for (const child of clonedSpace.children) {
|
||||
if (child.disabled) continue;
|
||||
await this.handleSpaceDuplication(
|
||||
child.uuid,
|
||||
child.spaceName,
|
||||
queryRunner,
|
||||
savedSpace,
|
||||
);
|
||||
}
|
||||
}
|
||||
return savedSpace;
|
||||
}
|
||||
private copySpaceSubspaces(
|
||||
newSpace: SpaceEntity,
|
||||
subspaces: SubspaceEntity[],
|
||||
queryRunner: QueryRunner,
|
||||
) {
|
||||
const newSubspaces = [];
|
||||
for (const sub of subspaces) {
|
||||
if (sub.disabled) continue;
|
||||
const clonedSub = structuredClone(sub);
|
||||
delete clonedSub.uuid;
|
||||
const newSubspace = queryRunner.manager.create(SubspaceEntity, {
|
||||
...clonedSub,
|
||||
space: newSpace,
|
||||
productAllocations: [],
|
||||
uuid: uuidV4(),
|
||||
});
|
||||
if (sub.productAllocations?.length) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const { uuid, ...allocation } of sub.productAllocations) {
|
||||
newSubspace.productAllocations.push(
|
||||
queryRunner.manager.create(SubspaceProductAllocationEntity, {
|
||||
...allocation,
|
||||
subspace: newSubspace,
|
||||
uuid: uuidV4(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
newSubspaces.push(newSubspace);
|
||||
}
|
||||
return newSubspaces;
|
||||
}
|
||||
private copySpaceAllocations(
|
||||
newSpace: SpaceEntity,
|
||||
allocations: SpaceProductAllocationEntity[],
|
||||
queryRunner: QueryRunner,
|
||||
) {
|
||||
const newAllocations = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const { uuid, ...allocation } of allocations) {
|
||||
newAllocations.push(
|
||||
queryRunner.manager.create(SpaceProductAllocationEntity, {
|
||||
...allocation,
|
||||
space: newSpace,
|
||||
uuid: uuidV4(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return newAllocations;
|
||||
}
|
||||
|
||||
private checkDuplicateTags(allocations: CreateProductAllocationDto[]) {
|
||||
const tagUuidSet = new Set<string>();
|
||||
const tagNameProductSet = new Set<string>();
|
||||
@ -505,6 +657,8 @@ export class SpaceService {
|
||||
spaceUuid,
|
||||
);
|
||||
|
||||
await this.validateNamingConflict(updateSpaceDto.spaceName, space, true);
|
||||
|
||||
if (space.spaceModel && !updateSpaceDto.spaceModelUuid) {
|
||||
await queryRunner.manager.update(SpaceEntity, space.uuid, {
|
||||
spaceModel: null,
|
||||
@ -655,13 +809,11 @@ export class SpaceService {
|
||||
updateSpaceDto: UpdateSpaceDto,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
const { spaceName, x, y, icon } = updateSpaceDto;
|
||||
const { spaceName, icon } = updateSpaceDto;
|
||||
|
||||
const updateFields: Partial<SpaceEntity> = {};
|
||||
|
||||
if (spaceName) updateFields.spaceName = spaceName;
|
||||
if (x !== undefined) updateFields.x = x;
|
||||
if (y !== undefined) updateFields.y = y;
|
||||
if (icon) updateFields.icon = icon;
|
||||
|
||||
if (Object.keys(updateFields).length > 0) {
|
||||
@ -828,4 +980,34 @@ export class SpaceService {
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
async validateNamingConflict(
|
||||
newSpaceName: string,
|
||||
parent: SpaceEntity,
|
||||
isUpdate: boolean = false,
|
||||
): Promise<void> {
|
||||
if (!isUpdate && parent.spaceName === newSpaceName) {
|
||||
throw new HttpException(
|
||||
`Space can't be created with the same name as its parent space`,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
if (parent.children?.some((child) => child.spaceName === newSpaceName)) {
|
||||
throw new HttpException(
|
||||
`Space name cannot be the same as one of its siblings/children`,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
if (isUpdate) {
|
||||
const sibling = await this.spaceRepository.exists({
|
||||
where: { spaceName: newSpaceName, parent: { uuid: parent.uuid } },
|
||||
});
|
||||
if (sibling) {
|
||||
throw new HttpException(
|
||||
`Space name cannot be the same as one of its siblings/children`,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user