Compare commits

..

597 Commits

Author SHA1 Message Date
d29c13f1a2 chore: trigger deploy 2025-07-21 00:06:36 -06:00
321c51af85 Implement code changes to enhance functionality and improve performance 2025-07-20 15:32:15 -06:00
e468818a26 chore: remove unused package-lock.json file 2025-07-20 14:30:55 -06:00
fd5fbf57d8 Merge branch 'main' 2025-07-20 14:30:27 -06:00
c0b95a8b4b Update configuration and documentation for syncrow.ae domain 2025-07-17 20:56:35 -06:00
efa53da8c5 Trigger deployment after env change 2025-07-16 23:41:01 -06:00
85687e7950 fix: refactor device status logging and improve property handling in batch processing 2025-07-16 20:05:58 -06:00
7e2c3136cf fix: update workflow to use OpenAI for PR description generation 2025-07-16 00:28:20 -06:00
61348aa351 fix: update workflow to use HuggingFace Falcon model for PR description generation 2025-07-16 00:08:08 -06:00
dea942f11e fix: update workflow to use HuggingFace for PR description generation 2025-07-16 00:05:33 -06:00
d62e620828 fix: update workflow name and enhance OpenAI request handling in AI PR Description 2025-07-15 23:47:45 -06:00
f0556813ac fix: update workflow name and enhance commit message handling in AI PR Description 2025-07-15 23:45:16 -06:00
6d2252a403 fix: add debug information to AI PR Description workflow 2025-07-15 23:43:00 -06:00
8d265c9105 fix: update workflow name and change GitHub token secret 2025-07-15 23:38:57 -06:00
a4095c837b fix: rename workflow and update AI description generation method 2025-07-15 23:19:22 -06:00
65d4a56135 fix: update GitHub CLI installation method and refine AI PR description prompt 2025-07-15 19:00:23 -06:00
fffa27b6ee Add AI PR Description Action 2025-07-15 18:48:43 -06:00
12579fcd6e fix nodemailer import (#473) 2025-07-15 16:46:50 +03:00
036c8bea17 task: add space filter to bookings (#471) 2025-07-15 13:31:56 +03:00
e5970c02c1 SP-1757, SP-1758, SP-1809, SP-1810: Feat/implement booking (#469)
* fix: commission device API

* task: add create booking API

* add get All api for dashboard & mobile

* add Find APIs for bookings

* implement sending email updates on update bookable space

* move email interfaces to separate files
2025-07-15 10:11:36 +03:00
57cf110229 Increase desired count for ECS service from 1 to 2 2025-07-14 01:41:00 -06:00
2e959a6ef3 Trigger GitHub Action 2025-07-13 23:28:02 -06:00
a6151220e2 Add .env injection from ENV_FILE secret 2025-07-13 23:17:42 -06:00
f4ba9c9bda Trigger GitHub Action 2025-07-13 22:43:00 -06:00
e91966c8f8 Add production deployment GitHub Action 2025-07-13 22:37:27 -06:00
a9cb1b6704 SP-1830: add company name to invite user entity (#468) 2025-07-10 15:41:46 +03:00
a17a271213 add validation checks (#466) 2025-07-10 14:41:50 +03:00
712b7443ac fix: prevent conditions overlapping by adding parenthesis to search condition (#464) 2025-07-10 11:34:50 +03:00
945328c0ce fix: commission device API (#465) 2025-07-10 11:33:55 +03:00
009deaf403 SP-1812: fix: update bookable space (#467) 2025-07-10 11:11:30 +03:00
3cfed2b452 fix: check if subspaces not exists in update space (#463) 2025-07-10 10:56:27 +03:00
09322c5b80 add booking points to user table (#461) 2025-07-09 14:25:42 +03:00
74d3620d0e Chore/space link tag cleanup (#462)
* chore: remove unused imports and dead code for space link

* chore: remove unused imports and dead code

* chore: remove unused imports and dead code of tag service
2025-07-09 14:25:26 +03:00
ab59a310d9 Refactor stack.ts: reorganize imports, enhance security group definitions, and improve Fargate service configuration 2025-07-09 04:46:53 -06:00
30166810ca Fix import order and standardize database name in app.ts 2025-07-09 04:46:42 -06:00
805c5dd180 Update infra:build script to use bash for improved compatibility 2025-07-09 04:46:28 -06:00
e4ba7d46bb Refactor build.sh to improve readability and maintainability by defining variables for configuration and adding descriptive echo statements. 2025-07-09 04:46:18 -06:00
83be80d9f6 add order space API (#459) 2025-07-09 11:44:14 +03:00
2589e391ed fix: add unique validation on subspaces in update space dto (#460) 2025-07-09 11:33:04 +03:00
5cf45c30f4 fix: check if device not found (#458) 2025-07-08 16:55:56 +03:00
0bb178ed10 make point nullable (#457) 2025-07-08 14:50:00 +03:00
ef21b589c0 rds 2025-07-08 13:46:04 +03:00
44f83ea54e Merge branch 'cdk-aq1' of https://github.com/SyncrowIOT/backend into cdk-aq1 2025-07-08 04:26:51 -06:00
e4694db79c add build.sh command 2025-07-08 04:26:19 -06:00
13064296a7 import db 2025-07-08 13:25:23 +03:00
a269f833bc Updates ECR repository handling to import existing repo 2025-07-08 12:44:10 +03:00
9971fb953d SP-1812: Task/booking-system/update-api (#456)
* add update bookable spaces API

* add search to get bookable spaces API
2025-07-08 11:52:22 +03:00
7a07f39f16 add communities filter to devices by project API (#455) 2025-07-08 11:25:15 +03:00
18b21d697c SP-1753 Feat/booking system (#454)
* task: add get all bookable spaces API (#453)

* task: add non bookable space API
2025-07-08 09:02:24 +03:00
66391bafd8 SP-1814: Update out-of-date device-virtual-id (#451)
* task: add a function to update device tuya id

* Add Tuya const uuid to device
2025-07-07 10:38:20 +03:00
fbf62fcd66 Enhances CDK deployment process and documentation
Improves the deployment script to use the UAE  region and adds context for the CDK stack.
2025-07-07 09:37:10 +03:00
25599b9fe2 add '%' to search (#452) 2025-07-06 14:14:23 +03:00
807c5b7dd3 add tag uuid to add device API (#450) 2025-07-03 16:23:42 +03:00
60bc03cf79 fix deployed issue 2025-07-03 01:12:23 -06:00
a9eaf44d31 Merge branch 'main' into dev 2025-07-03 00:10:00 -06:00
d232c06ebe Merge pull request #449 from SyncrowIOT/fix-get-schedule-api
fix: adjust category handling for CUR_2 device type in schedule retrieval
2025-07-03 00:03:38 -06:00
5c916ed445 add pr template 2025-07-03 00:01:37 -06:00
b2fb378e52 Merge pull request #410 from SyncrowIOT/SP-1754-be-implement-configure-space
SP-1754-be-implement-configure-space
2025-07-02 02:26:29 -06:00
c5f8f96977 Merge branch 'dev' into SP-1754-be-implement-configure-space 2025-07-02 02:25:43 -06:00
8f9b15f49f fix: adjust category handling for CUR_2 device type in schedule retrieval 2025-06-30 07:12:43 -06:00
0b9eef276e ensure Timer is the category value for CUR2 type (#448) 2025-06-30 15:52:01 +03:00
b3f8b92826 ensure Timer is the category value for CUR2 type (#446) 2025-06-30 15:35:23 +03:00
b9da00aaa6 Merge pull request #447 from SyncrowIOT/fix/integrate-cur2-with-schedule
add cur2 checks to schedule
2025-06-30 06:27:27 -06:00
5bf44a18e1 add cur2 checks to schedule 2025-06-30 14:09:32 +03:00
2b2772e4ca Merge pull request #445 from SyncrowIOT/fix-update-aqi-data-on-staging
fix: filter daily averages by space_id and event_date in update procedure
2025-06-30 04:11:03 -06:00
13c0f87fc6 fix: filter daily averages by space_id and event_date in update procedure 2025-06-30 04:09:40 -06:00
374fb69804 fix the super user seeded to accept terms and add certificate arn 2025-06-30 03:58:47 -04:00
c9d794d988 fix: update role type formatting in user invitation email 2025-06-30 01:25:09 -06:00
5d4e5ca87e Merge pull request #444 from SyncrowIOT/SP-1736-fe-on-user-management-page-when-i-invited-a-user-as-a-space-member-his-role-appeared-as-admin-in-the-email
SP-1736-fe-on-user-management-page-when-i-invited-a-user-as-a-space-member-his-role-appeared-as-admin-in-the-email
2025-06-30 01:23:55 -06:00
f4e748d735 fix: update role type formatting in user invitation email 2025-06-30 00:58:30 -06:00
f4f7999ae0 add device to firebase & stop moving it from the OEM space (#443) 2025-06-30 09:48:16 +03:00
82c82d521c add deviceName to handle password API (#442) 2025-06-30 08:57:43 +03:00
d4d1ec817d a functioning backend stack bypassing firebase and using an existing domain 2025-06-29 20:45:38 -04:00
c7a4ff1194 fix: schedule device types (#441) 2025-06-29 15:27:55 +03:00
90ab291d83 add curtain module device (#440) 2025-06-29 10:10:19 +03:00
8a4633b158 Merge pull request #439 from SyncrowIOT/add-check-log-to-trace-the-map-issue
feat: enhance device status handling with caching and batch processin…
2025-06-25 18:59:37 -06:00
f80d097ff8 refactor: optimize log insertion and clean up device cache handling in TuyaWebSocketService 2025-06-25 18:57:56 -06:00
04bd156df1 Merge branch 'dev' into add-check-log-to-trace-the-map-issue 2025-06-25 18:42:43 -06:00
731819aeaa feat: enhance device status handling with caching and batch processing improvements 2025-06-25 18:37:46 -06:00
68d2d3b53d fix: improve device retrieval logic in addDeviceStatusToFirebase method 2025-06-25 08:13:02 -06:00
3fcfe2d92f Merge pull request #438 from SyncrowIOT/temp-fix-to-check
fix: enhance device status handling by integrating device cache for improved performance
2025-06-25 08:06:29 -06:00
c0a069b460 fix: enhance device status handling by integrating device cache for improved performance 2025-06-25 08:03:23 -06:00
5381a949bc task: delete used & its relations (#437) 2025-06-25 15:32:46 +03:00
30724d7d37 Merge pull request #436 from SyncrowIOT/add-check-log-to-trace-the-map-issue
fix: add validation for missing properties in device status logs
2025-06-25 05:32:50 -06:00
324661e1ee fix: add missing check for device UUID in batch processing logs 2025-06-25 05:30:15 -06:00
a83424f45b fix: remove unnecessary validation for missing properties in device status logs 2025-06-25 05:29:28 -06:00
71f6ccb4db fix: add validation for missing properties in device status logs 2025-06-25 05:20:26 -06:00
68692b7c8b increase rate limit to 100 per minute for each IP (#435) 2025-06-25 13:50:38 +03:00
4d60c1ed54 Merge pull request #434 from SyncrowIOT/fix-time-out-connections-db
Fix-time-out-connections-db
2025-06-25 04:47:59 -06:00
27dbe04299 fix: remove unnecessary comment from ScheduleModule import in scheduler module 2025-06-25 04:47:38 -06:00
9bebcb2f3e feat: implement scheduler for periodic data updates and optimize database procedures
- Added SchedulerModule and SchedulerService to handle hourly data updates for AQI, occupancy, and energy consumption.
- Refactored existing services to remove unused device repository dependencies and streamline procedure execution.
- Updated SQL procedures to use correct parameter indexing.
- Enhanced error handling and logging for scheduled tasks.
- Integrated new repositories for presence sensor and AQI pollutant stats across multiple modules.
- Added NestJS schedule package for task scheduling capabilities.
2025-06-25 03:20:25 -06:00
43ab0030f0 refactor: clean up unused services and optimize batch processing in DeviceStatusFirebaseService 2025-06-25 03:20:12 -06:00
c48adb73b5 Merge pull request #433 from SyncrowIOT/DATA-adjust-remaining-procedures
DATA-adjust-remaining-procedures
2025-06-25 01:55:12 -06:00
d255e6811e update procedures 2025-06-25 10:47:37 +03:00
e58d2d4831 Test/prevent server block on rate limit (#432) 2025-06-24 14:56:02 +03:00
147cf0b582 Merge pull request #431 from SyncrowIOT/DATA-adjust-procedures
DATA-adjust-procedures
2025-06-24 04:58:09 -06:00
4e6b6f6ac5 adjusted procedures 2025-06-24 13:04:21 +03:00
932a3efd1c Sp 1780 be configure the curtain module device (#424)
* task: add Cur new device configuration
2025-06-24 12:18:46 +03:00
0a1ccad120 add check if not space not found (#430) 2025-06-24 12:18:15 +03:00
f337e6c681 Test/prevent server block on rate limit (#421) 2025-06-24 10:55:38 +03:00
f5bf857071 Merge pull request #429 from SyncrowIOT/add-queue-event-handler
Add queue event handler
2025-06-23 08:13:36 -06:00
d1d4d529a8 Add methods to handle SOS events and device status updates in Firebase and our DB 2025-06-23 08:10:33 -06:00
37b582f521 Merge pull request #428 from SyncrowIOT/add-queue-event-handler
Implement message queue for TuyaWebSocketService and batch processing
2025-06-23 07:35:22 -06:00
cf19f08dca turn on all the updates data points 2025-06-23 07:33:01 -06:00
ff370b2baa Implement message queue for TuyaWebSocketService and batch processing 2025-06-23 07:31:58 -06:00
04f64407e1 turn off some update data points 2025-06-23 07:10:47 -06:00
d7eef5d03e Merge pull request #427 from SyncrowIOT/revert-426-SP-1778-be-fix-time-out-connections-in-the-db
Revert "SP-1778-be-fix-time-out-connections-in-the-db"
2025-06-23 07:09:20 -06:00
c8d691b380 tern off data procedure 2025-06-23 07:02:23 -06:00
75d03366c2 Revert "SP-1778-be-fix-time-out-connections-in-the-db" 2025-06-23 06:58:57 -06:00
52cb69cc84 Merge pull request #426 from SyncrowIOT/SP-1778-be-fix-time-out-connections-in-the-db
SP-1778-be-fix-time-out-connections-in-the-db
2025-06-23 06:38:58 -06:00
a6053b3971 refactor: implement query runners for database operations in multiple services 2025-06-23 06:34:53 -06:00
60d2c8330b fix: increase DB max pool size (#425) 2025-06-23 15:23:53 +03:00
fddd06e06d fix: add space condition to the join operator instead of general query (#423) 2025-06-23 12:44:19 +03:00
3160773c2a fix: spaces structure in communities (#420) 2025-06-23 10:21:55 +03:00
d3d84da5e3 fix: correct property name from bookableConfigs to bookableConfig in BookableSpaceEntity and SpaceEntity 2025-06-23 00:39:29 -06:00
110ed4157a task: add spaces filter to get devices by project (#422) 2025-06-23 09:34:59 +03:00
aa9e90bf08 Test/prevent server block on rate limit (#419)
* increase DB max connection to 50
2025-06-19 14:34:23 +03:00
c5dd5e28fd Test/prevent server block on rate limit (#418) 2025-06-19 13:54:22 +03:00
603e74af09 Test/prevent server block on rate limit (#417)
* task: add trust proxy header

* add logging

* task: test rate limits on sever

* task: increase rate limit timeout

* fix: merge conflicts
2025-06-19 12:54:59 +03:00
6973e8b195 task: sort communities by creation date (#416) 2025-06-19 11:13:24 +03:00
0e36f32ed6 Test/prevent server block on rate limit (#415)
* task: increase rate limit timeout
2025-06-19 10:15:29 +03:00
705ceeba29 Test/prevent server block on rate limit (#414)
* task: test rate limits on sever
2025-06-19 09:45:09 +03:00
92d102d08f Merge pull request #413 from SyncrowIOT/fix-staging-insirt-logs-data
Fix-staging-insirt-logs-data
2025-06-18 07:35:30 -06:00
7dc28d0cb3 fix: enable AQI sensor historical data update in device status processing 2025-06-18 07:32:39 -06:00
d9ad431a23 fix: correct procedure names in energy consumption updates 2025-06-18 05:33:49 -06:00
4bf43dab2b feat: enhance device status DTO and service with optional properties and environment checks 2025-06-18 05:33:43 -06:00
a37d5bb299 task: add trust proxy header (#411)
* task: add trust proxy header

* add logging
2025-06-18 12:05:53 +03:00
49cc762962 fix duplication from conflict merge 2025-06-18 01:56:51 -06:00
a94d4610ed Merge branch 'dev' into SP-1754-be-implement-configure-space 2025-06-18 01:55:48 -06:00
274cdf741f refactor: streamline Booking module and service by removing unused imports and consolidating space validation logic 2025-06-18 01:49:00 -06:00
df59e9a4a3 refactor: update BookableSpaceEntity relationship to OneToOne with SpaceEntity 2025-06-18 01:48:46 -06:00
8c34c68ba6 refactor: remove BookableSpaceDto and its index export 2025-06-18 01:48:35 -06:00
689a38ee0c Revamp/space management (#409)
* task: add getCommunitiesV2

* task: update getOneSpace API to match revamp structure

* refactor: implement modifications to pace management APIs

* refactor: remove space link
2025-06-18 10:34:29 +03:00
332b2f5851 feat: implement Booking module with BookableSpace entity, controller, service, and DTOs for managing bookable spaces 2025-06-17 22:02:13 -06:00
8d44b66dd3 feat: add BookableSpace entity, DTO, repository, and integrate with Space entity 2025-06-17 22:02:08 -06:00
7520b8d9c7 fix: power clamp historical API (#408) 2025-06-17 15:17:49 +03:00
72753b6dfb merge dev to main 2025-06-14 15:18:20 -06:00
568eef8119 Merge branch 'dev' 2025-06-14 15:04:48 -06:00
a91d0f22a4 fix: send correct enable status to email sender function (#407) 2025-06-13 09:46:41 +03:00
0db060ae3f Merge pull request #406 from SyncrowIOT/add-space-daily-occupancy-duration-entity
feat: add SpaceDailyOccupancyDuration entity, DTO, and repository for occupancy tracking
2025-06-12 04:17:18 -06:00
f2ed04f206 feat: add SpaceDailyOccupancyDuration entity, DTO, and repository for occupancy tracking 2025-06-12 02:49:59 -06:00
ea9a65178d fix: add space filter to "join" operation instead of "and" operation (#405) 2025-06-11 16:28:33 +03:00
8503ee728d Refactor/space management (#404)
* refactor: reducing used queries on get communities (#385)

* refactor: fix create space logic (#394)

* Remove unique constraint on subspace and product in SubspaceProductAllocationEntity; update product relation to nullable in NewTagEntity

* refactor: fix create space logic

* device model updated to include the fixes and final columns

* updated space models to include suggested fixes, update final logic and column names

* task: removing old references of the old tag-product relation

* task: remove old use of tags

* task: remove old tag & tag model usage

* refactor: delete space

* task: remove unused functions

* fix lint rule
2025-06-11 13:15:21 +03:00
4f5e1b23f6 Merge pull request #403 from SyncrowIOT/SP-1629-AND-SP-1630-AQI-APIs
Sp 1629 and sp 1630 aqi ap is
2025-06-11 00:28:14 -06:00
2cb77504ca Add PollutantType enum and update AQI-related entities and services to use it 2025-06-11 00:28:00 -06:00
c86be27576 Add AQI module and related services, controllers, and DTOs
- Introduced AqiModule with AqiService and AqiController for handling AQI data.
- Added DTOs for AQI requests: GetAqiDailyBySpaceDto and GetAqiPollutantBySpaceDto.
- Implemented AqiDataService for managing AQI sensor historical data.
- Updated existing modules to include AqiDataService where necessary.
- Defined new routes for AQI data retrieval in ControllerRoute.
2025-06-10 18:19:34 -06:00
3a08f9f258 Merge branch 'dev' into aqi-test 2025-06-10 17:08:06 -06:00
5c96a3b117 Merge pull request #402 from SyncrowIOT/add-code-and-value-as-uniqe-key
Refactor DeviceStatusLogEntity: expand unique constraint to include c…
2025-06-10 01:43:09 -06:00
97e14e70f7 Refactor DeviceStatusLogEntity: expand unique constraint to include code and value 2025-06-10 01:41:41 -06:00
03d44cb14f Merge pull request #401 from SyncrowIOT/fix-log-insert-error
Refactor DeviceStatusLogEntity: update unique constraint to include d…
2025-06-10 01:27:15 -06:00
0793441e06 Refactor DeviceStatusLogEntity: correct unique constraint name for event time and device ID 2025-06-10 01:24:33 -06:00
b6321c2530 Refactor DeviceStatusLogEntity: update unique constraint to include deviceId 2025-06-10 01:18:58 -06:00
b8d34b0d9f Merge pull request #400 from SyncrowIOT/revert-398-fix-log-duplication-issue
Revert "Refactor DeviceStatusLogEntity: update unique constraint and primary …"
2025-06-10 01:04:29 -06:00
c1065126aa Revert "Refactor DeviceStatusLogEntity: update unique constraint and primary …" 2025-06-10 01:03:45 -06:00
1742454984 Merge pull request #398 from SyncrowIOT/fix-log-duplication-issue
Refactor DeviceStatusLogEntity: update unique constraint and primary …
2025-06-10 00:26:43 -06:00
7eb13088ac Refactor DeviceStatusLogEntity: update unique constraint and primary key definition 2025-06-09 04:50:58 -06:00
7b97e50d2e Merge pull request #391 from SyncrowIOT/DATA-space-model-aqi-update-logic
AQI space model updated with new hourly to daily logic for calculatio…
2025-06-04 17:40:39 -04:00
4fb26fc131 Merge pull request #397 from SyncrowIOT/DATA-daily-procedure-aqi
Procedures insert-all, update, and select for daily space air quality…
2025-06-04 17:39:34 -04:00
ee0261d102 Fix typos procedure select and update 2025-06-04 17:32:50 -04:00
0d6de2df43 Refactor AqiSpaceDailyPollutantStatsEntity: update unique constraint and rename event fields for clarity 2025-06-04 15:24:13 -06:00
80e89dd035 fix name of snapshot table 2025-06-04 17:20:25 -04:00
466863e71f Procedures insert-all, update, and select for daily space air quality stats 2025-06-04 16:33:55 -04:00
30aafdede6 Merge pull request #390 from SyncrowIOT/DATA-device-model-aqi-update-logic
device model for aqi updated with hourly to daily logic getting max, …
2025-06-04 12:04:10 +03:00
01ce4d4b29 Merge pull request #396 from SyncrowIOT/fix-some-issue-in-get-commuinty-and-weather-apis
Fix some issue in get commuinty and weather apis
2025-06-04 01:32:21 -06:00
43dfaaa90d fix: utilize WEATHER_API_URL in WeatherService for dynamic API endpoint 2025-06-04 01:32:01 -06:00
ea021ad228 fix: update error message for invalid latitude and longitude in fetchWeatherDetails method 2025-06-04 01:09:49 -06:00
cd3e9016f2 fix: improve error handling in fetchWeatherDetails method 2025-06-04 01:08:33 -06:00
ef2245eae1 Add AQI space daily pollutant stats module and related entities, DTOs, and repositories 2025-06-03 23:37:52 -06:00
3ad81864d1 updated space models to include suggested fixes, update final logic and column names 2025-06-03 21:05:34 -04:00
ab3efedc35 device model updated to include the fixes and final columns 2025-06-03 20:52:27 -04:00
4a984ae5dd Merge pull request #395 from SyncrowIOT/SP-1678-be-implement-weather-apis-by-location
Add Weather module with controller, service, and DTO for fetching weather details
2025-06-03 03:05:01 -06:00
c39129f75b Add Weather module with controller, service, and DTO for fetching weather details 2025-06-03 02:38:44 -06:00
35ce13a67f fix: return proper error on login API (#386) 2025-06-03 09:47:24 +03:00
12a9272b8b Merge pull request #393 from SyncrowIOT/SP-1675-be-return-space-uuid-in-get-devices-api
SP-1675-be-return-space-uuid-in-get-devices-api
2025-06-02 01:29:04 -06:00
0fe6c80731 Add utility function to associate space UUID with devices in community and device services 2025-06-01 21:56:08 -06:00
81e017430e Merge pull request #392 from SyncrowIOT/temp-product-relation-fixes
Remove unique constraint on subspace and product in SubspaceProductAllocationEntity; update product relation to nullable in NewTagEntity
2025-06-02 06:20:03 +03:00
191d0dfaf6 Remove unique constraint on subspace and product in SubspaceProductAllocationEntity; update product relation to nullable in NewTagEntity 2025-06-01 21:16:51 -06:00
5b0135ba80 AQI space model updated with new hourly to daily logic for calculations and categorization of aqi brackets 2025-06-01 16:09:17 -04:00
2fee8c055e device model for aqi updated with hourly to daily logic getting max, min, avergae and percentage of categorical values for each aqi bracket 2025-06-01 16:03:07 -04:00
59161d4049 Merge pull request #389 from SyncrowIOT/DATA-daily-space-aqi-model
Air quality and AQI for space model
2025-06-01 14:31:13 -04:00
b989338790 Merge pull request #388 from SyncrowIOT/DATA-device-aqi-fix-update
fixed pm25 code + date format. Added average AQI level from device
2025-06-01 14:31:00 -04:00
f5ed9d4fce Merge pull request #387 from SyncrowIOT/DATA-occupancy-duration-fix
DATA-occupancy-duration-fix
2025-05-30 11:59:31 +03:00
3ac48183bd procedure fix test 2025-05-30 11:27:12 +03:00
684205053d testing fix 2025-05-30 10:51:26 +03:00
bfd92fdd87 fix 2025-05-29 15:05:06 +03:00
dd54af5f46 fix 2025-05-29 13:38:50 +03:00
90fc44ab53 Air quality and AQI for space model 2025-05-28 21:14:01 -04:00
efdf918159 fixed pm25 code + date format. Added average AQI level from device 2025-05-28 20:42:56 -04:00
25967d02f9 model adjustments 2025-05-28 13:55:56 +03:00
f44dc793a6 Merge pull request #383 from SyncrowIOT/daily-aqi-score
AQI score calculations and air quality model
2025-05-22 14:57:12 +03:00
a7c4bf1c3d AQI score calculations and air quality model 2025-05-22 07:28:50 -04:00
a8bb161ee2 Merge pull request #382 from SyncrowIOT/DATA-monthly-yearly-procedure-occupancy
DATA-occupancy_count_per_month
2025-05-22 14:18:18 +03:00
fe891030aa input year, output month 2025-05-22 13:10:05 +03:00
a40560a0b1 Merge pull request #380 from SyncrowIOT/revert-378-daily-aqi-sensor
Revert "SQL model for aqi score and processing air data"
2025-05-21 20:04:43 -04:00
7d6f1bb944 Revert "SQL model for aqi score and processing air data" 2025-05-21 20:01:05 -04:00
434316fe51 Merge pull request #378 from SyncrowIOT/daily-aqi-sensor
SQL model for aqi score and processing air data
2025-05-21 16:54:19 -04:00
287bb4c5e4 SQL model for aqi score and processing air data 2025-05-21 16:49:44 -04:00
f2e515b180 Merge pull request #377 from SyncrowIOT/SP-1561-be-get-all-projects-api-is-returning-internal-server-error
Sp 1561 be get all projects api is returning internal server error
2025-05-21 16:32:18 +03:00
f7fd96afa1 fix: remove unused params from get all projects api 2025-05-20 14:40:48 +03:00
5292271721 Merge pull request #376 from SyncrowIOT/SP-1555-implement-occupancy-api-for-analytics-dashboard
feat: add occupancy duration data retrieval and update procedures
2025-05-18 17:42:00 +03:00
180d16eeb1 feat: add occupancy duration data retrieval and update procedures 2025-05-18 17:40:41 +03:00
dca3db0c59 Merge pull request #373 from SyncrowIOT/DATA-space-occupancy-duration-procedure
DATA-space-occupancy-duration
2025-05-15 12:36:09 +03:00
56e78683b3 adjusted select 2025-05-15 12:28:38 +03:00
e575e51c4c Merge pull request #375 from SyncrowIOT/fix-duplication-community
feat: optimize getAllDevicesByCommunity to prevent duplicate space processing
2025-05-15 10:09:59 +03:00
8750da7e62 feat: optimize getAllDevicesByCommunity to prevent duplicate space processing 2025-05-15 10:09:29 +03:00
e3e9fe82fc Merge pull request #374 from SyncrowIOT/fix-duplication-devices-by-community
refactor: optimize device retrieval by avoiding duplicate space visits in getAllDevicesByCommunity
2025-05-15 10:08:31 +03:00
e253d1ca03 refactor: optimize device retrieval by avoiding duplicate space visits in getAllDevicesByCommunity 2025-05-15 10:08:02 +03:00
b50d7682f3 updates 2025-05-14 15:50:28 +03:00
1bb3803229 wording 2025-05-14 15:03:52 +03:00
92ee6ee951 bug fix 2025-05-14 14:11:36 +03:00
c06be4736c occupancy duration procedures 2025-05-14 13:34:29 +03:00
5cb4295f8a Merge pull request #372 from SyncrowIOT/fix-get-devices-by-product-type
feat: update DEVICE_SPACE_COMMUNITY route and add validation for spaceUuid and communityUuid in DTO
2025-05-14 13:00:46 +03:00
67331aa92a feat: update DEVICE_SPACE_COMMUNITY route and add validation for spaceUuid and communityUuid in DTO 2025-05-14 13:00:09 +03:00
7ec41f8311 Merge pull request #371 from SyncrowIOT/SP-1554-be-implement-get-all-devices-in-spaces-and-include-the-childs-for-analytics-dashboard
SP-1554-be-implement-get-all-devices-in-spaces-and-include-the-childs-for-analytics-dashboard
2025-05-13 03:20:08 +03:00
4aa3d04478 feat: update device and space services to use productType instead of deviceType and add query support for device retrieval by product type 2025-05-13 03:19:21 +03:00
799fcb6fb9 feat: add DEVICE_SPACE_COMMUNITY route and controller for device retrieval by space or community 2025-05-13 03:06:43 +03:00
921770ea79 Merge pull request #370 from SyncrowIOT/SP-1556-be-implement-occupancy-heat-map-api-for-analytics-dashboard
feat: add occupancy module with controller, service, and related DTOs for heat map data retrieval
2025-05-12 02:16:54 +03:00
7ec4171e1a feat: add occupancy module with controller, service, and related DTOs for heat map data retrieval 2025-05-12 02:15:57 +03:00
c0a6e9ab63 Merge pull request #368 from SyncrowIOT/DATA-space-occupancy-procedures
DATA-daily-space-occupancy-procedures
2025-05-12 01:02:38 +03:00
208281386d Merge pull request #369 from SyncrowIOT/SP-1552-be-implement-energy-consumption-include-all-device-api-for-analytics-dashboard
feat: add groupByDevice option to GetPowerClampBySpaceDto and update service logic
2025-05-11 02:00:51 +03:00
fb5084ba3a feat: add groupByDevice option to GetPowerClampBySpaceDto and update service logic 2025-05-11 01:59:26 +03:00
9c3abdd08a added on conflict 2025-05-09 16:25:30 +03:00
3535c1d8c5 month param 2025-05-09 15:47:05 +03:00
2ba5700fdd space occupancy API 2025-05-09 15:24:39 +03:00
1479c74423 Merge pull request #367 from SyncrowIOT/add-space-presence-sensor-detection-entity
refactor: rename presence sensor entities and update related references
2025-05-09 13:11:56 +03:00
8030644fee refactor: rename presence sensor entities and update related references 2025-05-09 13:11:14 +03:00
d43e860867 Merge pull request #366 from SyncrowIOT/DATA-param-removal
removed param from first CTE
2025-05-08 13:32:32 +03:00
f8269df3fb removed param from first CTE 2025-05-08 13:31:43 +03:00
85602fa952 check deployment 2025-05-08 13:15:31 +03:00
c085514d27 Merge pull request #365 from SyncrowIOT/DATA-date-param-move
DATA-param-moved
2025-05-08 13:12:21 +03:00
fa3cb578df param moved 2025-05-08 13:11:29 +03:00
25a4d3e91b Merge pull request #364 from SyncrowIOT/revert-363-DATA-date-param-filtering
Revert "DATA-date-param-moved"
2025-05-08 13:08:58 +03:00
d3a560d18f Revert "DATA-date-param-moved" 2025-05-08 13:08:41 +03:00
ab99bb5afc Merge pull request #363 from SyncrowIOT/DATA-date-param-filtering
DATA-date-param-moved
2025-05-08 13:07:51 +03:00
67911d5ff1 moved param 2025-05-08 13:06:39 +03:00
b4572beec2 Merge pull request #358 from SyncrowIOT/DATA-daily-occupancy-procedure
DATA-daily occupancy procedure
2025-05-08 12:55:42 +03:00
303493ad45 bug fix of the date parameter 2025-05-08 12:37:11 +03:00
dc06cfff37 removed month to event_date 2025-05-08 10:59:47 +03:00
b3e86ec56f Merge pull request #362 from SyncrowIOT/fix-power-clamp-historical-data
Add endpoints and logic for fetching power clamp data by community or…
2025-05-07 23:09:58 +03:00
45b8cdcaae Add endpoints and logic for fetching power clamp data by community or space
- Introduced new API endpoints to retrieve power clamp historical data based on community or space UUID.
- Updated PowerClampController to handle requests with optional parameters for community and space.
- Enhanced PowerClampService to validate input and fetch devices accordingly.
- Created ResourceParamsDto to manage request parameters.
- Updated ControllerRoute with new action summaries and descriptions.
2025-05-07 23:09:01 +03:00
10005c7897 bug fix 2025-05-07 14:51:06 +03:00
59ffa233ee removed array 2025-05-07 14:48:31 +03:00
078de317e4 procedures 2025-05-07 14:30:56 +03:00
ab89a13f95 procedure 2025-05-07 13:16:36 +03:00
adf1c5e2e0 edit procedure 2025-05-07 12:52:22 +03:00
5ed59e4fcc Merge pull request #361 from SyncrowIOT/Implement-Total-Enargy-by-space-api
Implement total enargy by space api
2025-05-07 12:19:06 +03:00
91abfb41ab Implement month-based date formatting and filtering in PowerClamp service 2025-05-07 12:17:17 +03:00
d40fb7a762 Merge branch 'dev' into SP-1551-be-implement-total-energy-consumption-api-for-analytics-dashboard 2025-05-07 10:59:42 +03:00
71f795babe Merge pull request #360 from SyncrowIOT/DATA-energy_consumption_procedure_edits
DATA-daily_energy_consummed
2025-05-06 11:33:53 +03:00
0d48505eac month name 2025-05-06 11:30:23 +03:00
e538f2b829 grain change 2025-05-06 11:28:43 +03:00
23af8e9de3 Merge pull request #359 from SyncrowIOT/create-presence-sensor-daily-detection-entity
feat: add presence sensor module with entity, DTO, and repository
2025-05-04 22:36:52 +03:00
d197bf2bb4 feat: implement date formatting function and enhance PowerClampService with space-based data retrieval 2025-05-04 22:28:38 +03:00
2a1f1f52f6 feat: add presence sensor module with entity, DTO, and repository 2025-05-04 19:11:14 +03:00
606de2e0e3 commented out test 2025-05-02 14:55:51 +03:00
76806f8df4 latest one 2025-05-02 14:52:13 +03:00
22436af903 daily occupancy procedure 2025-05-02 12:37:29 +03:00
e932d8a4a4 Merge pull request #357 from SyncrowIOT/add-node-local-env-in-winston
refactor: update winston logger configuration for environment-based logging
2025-05-02 02:28:40 +03:00
0fcd974af2 refactor: update winston logger configuration for environment-based logging 2025-05-02 02:28:01 +03:00
e64025b420 Merge pull request #356 from SyncrowIOT/DATA-energy-consummed-space=procedure
DATA-space_energy_consumed_procedure
2025-05-02 01:59:03 +03:00
aee6b8df69 refactor: update procedure names in PowerClampService for clarity 2025-05-02 01:57:12 +03:00
a12046eee2 grouping 2025-05-01 15:19:24 +03:00
46325def06 fixed device_id 2025-05-01 15:16:11 +03:00
796f5c6c08 remove space_id 2025-05-01 15:02:35 +03:00
e365cb0b50 params fixes 2025-05-01 14:54:27 +03:00
cc755e5640 adjusted date param 2025-05-01 14:25:09 +03:00
ccba81ef1f space_energy_consumed_procedure 2025-05-01 12:21:40 +03:00
13e3f3e213 Merge branch 'dev' 2025-04-29 09:58:05 +03:00
25403d5e19 Merge pull request #355 from SyncrowIOT/DATA-device-presence-sensor-detection
Data device presence sensor detection
2025-04-28 13:51:44 +03:00
62f2a6c3c9 created presence detection at space level 2025-04-28 13:44:03 +03:00
dfee2d6524 Merge pull request #354 from SyncrowIOT/fixbug-powerclamp-dto
refactor: enhance PowerClampController with additional ApiParam for powerClampUuid
2025-04-28 10:18:53 +03:00
8040594dc6 refactor: enhance PowerClampController with additional ApiParam for powerClampUuid 2025-04-28 10:18:07 +03:00
3eb6e1040c Merge pull request #353 from SyncrowIOT/fixbug-powerclamp-data
refactor: update PowerClampController and service to use PowerClampParamsDto for parameter handling
2025-04-28 01:38:34 +03:00
588d4cb98e refactor: update PowerClampController and service to use PowerClampParamsDto for parameter handling 2025-04-28 01:37:04 +03:00
94881f0555 Merge pull request #352 from SyncrowIOT/bugfix/commission
fixed commission device
2025-04-27 22:43:59 +04:00
6b8aa6836d fixed commission device 2025-04-27 22:42:03 +04:00
ff731ceea0 added tag check 2025-04-27 11:21:30 +04:00
6de81333ce Merge pull request #351 from SyncrowIOT/bugfix/fixed-delete
removed tags from validate space
2025-04-25 10:59:51 +04:00
488943338b removed tags from validate space 2025-04-25 10:58:35 +04:00
387862a6bb Merge pull request #350 from SyncrowIOT/SP-1355-powr-clap-historical-data
SP-1355-powr-clap-historical-data
2025-04-24 15:09:11 +03:00
72e65882b1 feat: update PowerClamp routes to support historical data retrieval 2025-04-24 15:08:54 +03:00
1bdb13846f Merge branch 'dev' into SP-1355-powr-clap-historical-data 2025-04-24 15:04:11 +03:00
0ee26698ff feat: enhance PowerClamp service and controller to support filtering by date, month, and year 2025-04-24 12:46:24 +03:00
96b911bfa1 increase limit 2025-04-24 13:32:07 +04:00
f9e1219f55 throw appropriate error 2025-04-24 12:55:49 +04:00
e98633749d Merge pull request #349 from SyncrowIOT/SP-1315-BE-API-rate-limit
added health check
2025-04-24 12:26:19 +04:00
9844e13f42 added health check 2025-04-24 12:22:03 +04:00
2594c1229b Merge branch 'dev' of https://github.com/SyncrowIOT/backend into SP-1315-BE-API-rate-limit 2025-04-24 12:21:49 +04:00
04c2c42523 Merge pull request #348 from SyncrowIOT/fix-add-device-issue
Fix-add-device-issue
2025-04-24 11:10:16 +03:00
45eec4bb30 Merge branch 'dev' into fix-add-device-issue 2025-04-24 11:08:38 +03:00
51bf4c0170 fix: update connection timeout value in database module 2025-04-24 11:06:47 +03:00
fa3f49af18 fix: handle potential undefined deviceDetails in getDeviceByDeviceUuid 2025-04-24 11:06:41 +03:00
28ec0377bc Merge pull request #343 from SyncrowIOT/SP-1311
typeorm logger
2025-04-24 11:53:30 +04:00
881618a4ee feat: enhance PowerClamp service with energy consumption procedures and enums 2025-04-24 03:14:03 +03:00
f3fd6646a1 feat: add SqlLoaderService to multiple modules for improved SQL data handling 2025-04-24 02:31:35 +03:00
3c5b97f2f9 feat: add SQL procedures for daily, hourly, and monthly energy consumption data 2025-04-24 02:22:13 +03:00
7249a0badb Merge branch 'dev' into SP-1355-powr-clap-historical-data 2025-04-24 02:16:22 +03:00
92f908c0fb feat: implement PowerClampService for managing hourly energy consumption data 2025-04-24 02:15:45 +03:00
7f43ef5de5 feat: integrate PowerClamp service and repositories across multiple modules 2025-04-24 02:14:58 +03:00
61a7e1c742 remove unused client and space DTOs and repository module 2025-04-23 16:58:42 +03:00
5490b5781d fix deployment error 2025-04-23 16:55:57 +03:00
531f93d42c fix: enhance log output in device status to include UUID and timestamp 2025-04-23 16:52:54 +03:00
0749fab633 health check 2025-04-23 17:31:16 +04:00
9da8214041 Merge pull request #346 from SyncrowIOT/add-product-name-for-all-devices
fix: include productName in device details response
2025-04-23 16:29:08 +03:00
b8f283f5ce Merge pull request #347 from SyncrowIOT/DATA-inserting-historical-data
DATA-inserting historical data
2025-04-23 16:23:41 +03:00
ea1201307c insert historical data 2025-04-23 16:18:14 +03:00
fde34dcf9c fix: update log output in energy status check for better debugging 2025-04-23 15:13:53 +03:00
8228ccc293 feat: add power clamp entities, DTOs, and repository for energy consumption tracking 2025-04-23 15:08:51 +03:00
38e9ce5a47 Merge pull request #345 from SyncrowIOT/DATA-add-parameter
DATA-added-parameter-to-energy-consumed
2025-04-23 15:04:00 +03:00
59d526103c rebase from dev 2025-04-23 14:58:40 +03:00
516a885251 removed all params 2025-04-23 14:54:09 +03:00
6bc068c5e6 add SQL query for hourly energy consumption analysis 2025-04-23 13:06:42 +03:00
d1bc723834 fix: include productName in device details response 2025-04-23 12:51:24 +03:00
cb2056d1b3 added health check 2025-04-23 10:56:03 +04:00
cff947a84a database config 2025-04-22 21:44:46 +04:00
e019774f58 type fixing 2025-04-22 16:55:07 +03:00
6330f7823b adjusted param 2025-04-22 16:48:30 +03:00
fea5d4d828 added more steps 2025-04-22 16:38:02 +03:00
8072812b4b added params 2025-04-22 16:30:18 +03:00
ce7fc0114e Merge pull request #344 from SyncrowIOT/SP-1456-be-internal-server-error-when-we-try-to-save-routine-creation
refactor: clean up unused variable and update destructuring in device service
2025-04-22 13:55:54 +03:00
dabf234349 refactor: clean up unused variable and update destructuring in device service 2025-04-22 13:54:12 +03:00
055518d672 Merge pull request #339 from SyncrowIOT/feat/commission-device
Feat/commission device
2025-04-22 11:04:15 +04:00
aa9a11217b Merge pull request #342 from SyncrowIOT/bugfix/batch-status
fixed the response of batch get status
2025-04-22 10:29:39 +04:00
2470c8e09a fixed the response of batch get status 2025-04-22 10:25:44 +04:00
fcb27155d8 typeorm logger 2025-04-22 10:20:48 +04:00
506f3dae29 Merge pull request #340 from SyncrowIOT/SP-1310
- add winston logger
2025-04-22 09:48:13 +04:00
acbfa450f6 Merge pull request #341 from SyncrowIOT/SP-1357-be-add-our-product-name-and-device-name-related-to-the-syncrow-regardless-the-supplier
SP-1357-be-add-our-product-name-and-device-name-related-to-the-syncrow-regardless-the-supplier
2025-04-22 09:47:21 +04:00
1b3eb08fcb fix: set name column to non-nullable in DeviceEntity 2025-04-22 01:58:31 +03:00
069cdf6f75 feat: add device name to actions and conditions in Automation and Scene services 2025-04-22 01:53:10 +03:00
2f12189eef feat: add device name field to DeviceEntity and update related DTOs and services 2025-04-22 01:53:03 +03:00
69f87b0999 Merge pull request #338 from SyncrowIOT/SP-610-be-otp-email-template-for-change-password-and-sign-up
add OTP email sending functionality and integrate with user authentication flow
2025-04-21 15:22:54 +03:00
be3c9f730d - add winston logger
- updated HTTP logging interceptor
2025-04-21 11:38:11 +04:00
ed3c526efd fix commissioning 2025-04-21 10:58:14 +04:00
2b449e61ea add OTP email sending functionality and integrate with user authentication flow 2025-04-20 22:19:08 +03:00
1e6503c072 Merge pull request #337 from SyncrowIOT/SP-1346-be-sos-button-real-time-sync-issue 2025-04-18 09:21:24 +03:00
d341c47286 add SosHandlerService and integrate SOS handling in TuyaWebSocketService 2025-04-17 16:58:34 +03:00
c34011660f Merge pull request #335 from SyncrowIOT/DATA-parametrized-energy-consumption
DATA-parametrized energy consumption
2025-04-17 10:05:47 +03:00
c677be400c added community info and tag 2025-04-17 10:41:39 +04:00
25650ca91b adjusted params 2025-04-16 12:14:38 +03:00
eba3513e14 added params 2025-04-16 11:22:12 +03:00
f2aad9c6be Merge pull request #336 from SyncrowIOT/SP-1421
filter for not deleted space
2025-04-16 10:46:47 +04:00
2010e631f2 filter for not deleted space 2025-04-16 10:42:20 +04:00
4810cc6f35 added controller to export 2025-04-16 10:28:57 +04:00
d24e7be4ea Merge branch 'dev' of https://github.com/SyncrowIOT/backend into feat/commission-device 2025-04-15 20:01:45 +04:00
5d379e0c36 export 2025-04-15 20:01:42 +04:00
78cb65d712 Added parameter setup 2025-04-15 14:40:52 +03:00
dbabd28624 Merge pull request #334 from SyncrowIOT/SP-1390-be-implement-add-new-product-device-api
Implement product creation functionality with DTO and permissions
2025-04-15 14:12:04 +03:00
32112930c8 Implement product creation functionality with DTO and permissions 2025-04-15 13:26:56 +03:00
30dd8318e3 Add SQL queries for hourly energy consumption tracking and refactor daily energy consumed query 2025-04-15 12:20:37 +03:00
9f3dce535d Merge pull request #333 from SyncrowIOT/SP-1430-be-api-request-returns-400-bad-request-instead-of-empty-array-when-community-has-no-devices-or-spaces
refactor: improve error handling for missing space in device listing
2025-04-14 14:26:37 +03:00
846b7e6463 Merge pull request #331 from SyncrowIOT/fix-some-issues-wehn-add-new-device
feat: add DEVICE_ADD permission and update device handling logic
2025-04-14 14:26:26 +03:00
f723b63a91 Merge branch 'dev' into fix-some-issues-wehn-add-new-device 2025-04-14 14:26:13 +03:00
7ba8c78868 Merge pull request #332 from SyncrowIOT/SP-1431-be-integration-between-data-point-and-backend
Add PowerClamp module with controller and service for energy data retrieval
2025-04-14 14:24:35 +03:00
7be4e4eeef Refactor SQL queries for device presence detection: rename missing_date to date, enhance motion detection logic, and update aggregation for total presence counts. 2025-04-14 14:17:48 +03:00
f71279ea30 Fix SQL query in getPowerClampData method to use 'fact_daily_energy_consumed' 2025-04-14 13:47:40 +03:00
303ea696cb Add SQL queries for device logs, energy consumption, presence detection, and product category definitions 2025-04-14 13:47:32 +03:00
777105fc28 refactor: improve error handling for missing space in device listing 2025-04-14 13:24:40 +03:00
3745962827 Add PowerClamp module with controller and service for energy data retrieval 2025-04-14 12:17:44 +03:00
a3001c8bb8 feat: add DEVICE_ADD permission and update device handling logic 2025-04-13 20:19:21 +03:00
5bc69f869d Merge pull request #330 from SyncrowIOT/bugfix/re-order-batch
Bugfix/re-order-batch
2025-04-11 11:57:57 +04:00
f9eee8e266 reordering controller 2025-04-11 11:50:57 +04:00
de09624db8 added commissioning endpoint 2025-04-08 10:42:48 +04:00
d11c6a88f1 Merge pull request #327 from SyncrowIOT/add-root-user-when-when-create-project
feat: add user UUID to project creation and validate user existence
2025-04-07 09:49:32 +03:00
bac978e2b6 Merge pull request #329 from SyncrowIOT/bugfix/SP-1362
added space community
2025-04-07 09:24:47 +03:00
9c676a5231 added space community 2025-04-07 10:22:20 +04:00
473f028b45 Merge pull request #328 from SyncrowIOT:bugfix/search-space-name
updated the search for commuities
2025-04-06 21:22:03 +04:00
e04f68a7ac updated the search for commuities 2025-04-06 21:14:16 +04:00
22d98fa177 feat: add user UUID to project creation and validate user existence 2025-04-03 12:27:36 +03:00
5e2e95b098 Merge pull request #325 from SyncrowIOT/SP-1306-be-implement-client-credential-flow-that-accept-client-id-and-client-secret-from-3rd-party-applications-and-issue-o-auth-jwt-tokens
SP-1306-be-implement-client-credential-flow-that-accept-client-id-and-client-secret-from-3rd-party-applications-and-issue-o-auth-jwt-tokens
2025-03-28 12:16:16 +03:00
327d678656 Enhance TuyaWebSocketService to handle environment-specific message extraction 2025-03-28 03:40:09 +03:00
36394ae214 Merge pull request #326 from SyncrowIOT/SP-1295-be-fix-tuya-messages-environment-issue
refactor: enhance TuyaWebSocketService to handle environment-specific message extraction
2025-03-28 03:36:16 +03:00
c7fc4e3a5d refactor: enhance TuyaWebSocketService to handle environment-specific message extraction 2025-03-28 03:34:30 +03:00
cf8cf08bdc add client relation btw user and client 2025-03-27 01:55:26 +03:00
cbd3f5028b finished client endpoints 2025-03-27 01:28:40 +03:00
5f45876516 Merge pull request #319 from SyncrowIOT/SP-1322
added search for spaces
2025-03-25 21:55:56 +04:00
1a39d1c1bb Merge pull request #324 from SyncrowIOT/enhance-device-module
Enhance-device-module
2025-03-25 10:26:17 +03:00
cd05c9906d convert the api from post to put 2025-03-25 10:25:13 +03:00
ef9cf6ad28 convert the route to devices 2025-03-25 10:24:52 +03:00
c5dac6c564 add register client api 2025-03-25 10:10:08 +03:00
a4b743eb58 add client entity 2025-03-25 02:29:00 +03:00
ee28d04c76 Merge pull request #323 from SyncrowIOT/SP-1241-be-refactor-device-module
Sp 1241 be refactor device module
2025-03-24 11:20:58 +04:00
6785fb2b1e finished refactor vistitor password device 2025-03-24 03:28:26 +03:00
8efd8cfba2 finished refactor devices module 2025-03-24 02:32:44 +03:00
2712ce673a Merge pull request #322 from SyncrowIOT/edit-visitor-passwords-route
finished edit visitor passwords route
2025-03-21 09:57:24 +03:00
794e12d5d3 finished edit visitor passwords route 2025-03-21 09:56:13 +03:00
fb2e0c6cba Merge pull request #321 from SyncrowIOT/add-community-in-get-scenes-and-automation
finished add community data in scene and automation
2025-03-21 09:46:02 +03:00
338b73ac7a Merge pull request #320 from SyncrowIOT/SP-1309-be-create-custom-nestjs-guard-for-project-validation-and-SP-1242
Sp 1309 be create custom nestjs guard for project validation and sp 1242
2025-03-21 09:45:35 +03:00
d09bdd1f6e finished add community data in scene and automation 2025-03-20 11:12:37 +03:00
dd1229db9c add password as optional 2025-03-20 10:50:35 +03:00
9bc763bad3 refactor visitor passwords apis 2025-03-20 09:42:54 +03:00
b5469cb316 add project in the token 2025-03-20 09:42:28 +03:00
670c026cfd Merge pull request #317 from SyncrowIOT/SP-1331-be-return-space-name-in-get-scene-and-get-automation-by-space-apis
SP-1331-be-return-space-name-in-get-scene-and-get-automation-by-space-apis
2025-03-19 11:05:53 +03:00
611cee79b3 Merge pull request #318 from SyncrowIOT/SP-1332-be-return-the-last-three-parent-for-spaces-that-have-devices
finished return the last three parents
2025-03-19 11:04:43 +03:00
b0a403b0cf added search for spaces 2025-03-19 10:51:38 +04:00
1a313c103d finished return the last three parents 2025-03-19 00:50:24 +03:00
30badcde67 add where for community name 2025-03-18 21:20:27 +04:00
770d08ce08 add space name to get scenes endpoint 2025-03-18 13:35:49 +03:00
770ece21b3 add space name to get automations endpoint 2025-03-18 13:35:09 +03:00
ec851eda9d Merge pull request #316 from SyncrowIOT/SP-1321-be-return-the-spaces-in-get-communities-by-project-api
finished return the spaces in get community
2025-03-18 10:18:37 +03:00
39ac2e6860 add includeSpaces in the query param 2025-03-18 10:17:29 +03:00
9902b02700 finished return the spaces in get community 2025-03-18 01:52:01 +03:00
990bcffc2f Merge pull request #315 from SyncrowIOT/bugfix/make-endpoint-restful-for-get-endpoint
making get one automation endpoint RESTful
2025-03-14 12:29:15 +04:00
a9045a458c updated the endpoint for checking automation status 2025-03-14 12:26:56 +04:00
9f30f0e2c2 making get one automation endpoint RESTful 2025-03-14 11:57:11 +04:00
70047dd337 making endpoint plural 2025-03-14 11:49:00 +04:00
89ff05b6ef Merge pull request #314 from SyncrowIOT/feat/move-automation-by-space-to-space-module
Feat/move-automation-by-space-to-space-module
2025-03-14 11:47:57 +04:00
a495563c42 making it plural 2025-03-14 11:47:32 +04:00
1f25e09388 community 2025-03-14 11:39:56 +04:00
532ded3624 Enforced service accessing space and automation within the project 2025-03-14 11:39:23 +04:00
3d3dcf17dd moved automation by space to different root 2025-03-14 11:38:49 +04:00
378ff1751b Merge pull request #313 from SyncrowIOT/SP-1243
updated api route and added dtos
2025-03-13 14:35:28 +04:00
76fd4b640f updated api route and added dtos 2025-03-13 14:23:40 +04:00
f8f4bf525d Merge pull request #312 from SyncrowIOT/SP-1262
added disabled check
2025-03-13 14:11:41 +04:00
d24b1d88d7 added disabled check 2025-03-13 14:03:59 +04:00
dd5447fc5f Merge pull request #311 from SyncrowIOT/dev 2025-03-13 13:56:50 +04:00
8b3232b2f4 length checking 2025-03-13 13:39:26 +04:00
7df5b9ab08 Merge branch 'main' of https://github.com/SyncrowIOT/backend 2025-03-13 11:06:06 +03:00
06b4407b85 Merge branch 'dev' 2025-03-13 11:05:11 +03:00
1aea072692 Merge pull request #310 from SyncrowIOT/bugfix/unlik-tags-on-update-space-model 2025-03-13 11:05:13 +04:00
c6f314e5c4 fixed unliking tags on tags created via space models 2025-03-13 11:04:23 +04:00
1d6f3b8e65 Merge pull request #309 from SyncrowIOT:dev
propagating of space model to space
2025-03-13 00:27:23 +04:00
ef39e9cfa7 Merge pull request #308 from SyncrowIOT:bugfix/update-space-allocation
Bugfix/update-space-allocation
2025-03-12 23:09:27 +04:00
8cbbb3a8ba updated space product model allocation 2025-03-12 23:08:18 +04:00
537f20f8de propagate subspace allocations 2025-03-12 21:57:08 +04:00
80659f7a48 Merge branch 'dev' 2025-03-12 02:22:33 +03:00
8456eea5dc Merge pull request #307 from SyncrowIOT:bugifx/fix-delete-propagation
Bugifx/fix-delete-propagation
2025-03-12 01:36:28 +04:00
1ce2f3c3cc fix issues 2025-03-12 01:36:10 +04:00
481ed5e7f9 Merge pull request #306 from SyncrowIOT/bugifx/fix-delete-propagation
Bugifx/fix-delete-propagation
2025-03-11 22:31:33 +04:00
67a78dbdfd error 2025-03-11 22:31:14 +04:00
5c47a3195f fixed delete propagation 2025-03-11 22:29:01 +04:00
5b1c14300e Merge pull request #305 from SyncrowIOT:bugfix/added-validation-for-space-model
added validation for space model
2025-03-11 22:12:39 +04:00
1403a670e3 added validation for space model 2025-03-11 22:11:25 +04:00
4a5f2f3b9f Merge branch 'dev' 2025-03-11 20:27:22 +03:00
b6fa37b1a3 Merge pull request #304 from SyncrowIOT/fix-assign-device-to-room
fix tag relation in device entity
2025-03-11 20:04:40 +03:00
5204cd5504 fix tag relation in device entity 2025-03-11 20:03:30 +03:00
a57f4e1c65 Merge branch 'dev' 2025-03-11 15:33:52 +03:00
fa009d6f6e Merge pull request #303 from SyncrowIOT/fix-relation-tags-issue
fix relation tags issue
2025-03-11 15:15:40 +03:00
9f83b4286c fix relation tags issue 2025-03-11 15:14:59 +03:00
5434b8324d Merge pull request #302 from SyncrowIOT:bugfix/unlik-space-model
fix the issue in unliking
2025-03-11 15:47:05 +04:00
1ead195d52 Merge branch 'dev' of https://github.com/SyncrowIOT/backend into bugfix/unlik-space-model 2025-03-11 15:46:21 +04:00
7fae1bf407 fix the issue in unliking 2025-03-11 15:45:21 +04:00
4440a909f3 Merge pull request #301 from SyncrowIOT:bugfix/fix-duplication
Bugfix/fix-duplication
2025-03-11 13:49:03 +04:00
7cc5fd2b54 fixed empty validation 2025-03-11 13:48:22 +04:00
f9f9592603 fix duplication 2025-03-11 13:43:23 +04:00
c62c952544 Merge pull request #300 from SyncrowIOT/fix-get-spaces-hierarchy-issue
remove duplicate issue
2025-03-11 12:20:52 +03:00
f9878ff272 remove duplicate issue 2025-03-11 12:19:26 +03:00
64f83cbda2 fix typo 2025-03-11 12:47:50 +04:00
45a3020b39 Merge pull request #299 from SyncrowIOT:bugix/delete-subspace
Bugix/delete-subspace
2025-03-11 12:44:56 +04:00
6e329d096d unused import 2025-03-11 12:35:54 +04:00
eb42948d8e removed log 2025-03-11 12:34:53 +04:00
3367ca8fab Merge branch 'dev' of https://github.com/SyncrowIOT/backend into bugix/delete-subspace 2025-03-11 01:25:31 +04:00
9122137922 fixed delete subspace model propagation 2025-03-11 01:25:27 +04:00
440be3d423 Merge pull request #298 from SyncrowIOT:bugfix/add-validation-for-update-subspace-name
added validation for subspaces
2025-03-10 21:13:20 +04:00
cacaa105ed added validation for subspaces 2025-03-10 21:12:47 +04:00
7dc2a8d0c9 Merge pull request #297 from SyncrowIOT/fix-get-spaces-issue
return tags name
2025-03-10 15:41:43 +03:00
7e513f0d31 return tags name 2025-03-10 15:40:58 +03:00
a8fbf2b510 fixed issue in create space with space model 2025-03-10 15:52:11 +04:00
04d64a6f38 Merge pull request #296 from SyncrowIOT:bugfix/fix-validaiton
added validation for product allocations
2025-03-10 15:20:49 +04:00
b9f0987bb0 added validation for product allocations 2025-03-10 15:20:15 +04:00
aa609266c7 propagate 2025-03-10 15:13:04 +04:00
f6d47d0e39 Merge branch 'dev' of https://github.com/SyncrowIOT/backend into dev 2025-03-10 11:24:23 +03:00
ea09f633ee remove tags relation from device table 2025-03-10 11:23:51 +03:00
404c0457a7 Merge branch 'dev' of https://github.com/SyncrowIOT/backend into dev 2025-03-10 12:09:09 +04:00
5e9de023ac update add handler 2025-03-10 12:07:57 +04:00
c459cadb41 Merge pull request #293 from SyncrowIOT/remove-old-tag-flow-from-update-space
remove old tag flow
2025-03-10 11:55:28 +04:00
c8d72a90b6 remove old tag flow 2025-03-10 10:42:00 +03:00
283e9406b6 Merge pull request #292 from SyncrowIOT:feat/remove-tag
removed old tags
2025-03-10 10:58:10 +04:00
d1a976d152 removed old tags 2025-03-10 10:57:27 +04:00
3bbc39734b Merge branch 'dev' of https://github.com/SyncrowIOT/backend into feat/fix-propagation 2025-03-10 05:58:50 +04:00
c6793a46a5 Merge pull request #291 from SyncrowIOT/bugfix/create-space-with-space-model 2025-03-09 20:12:58 +04:00
054df46936 fix create space 2025-03-09 20:12:19 +04:00
08db0bd153 Merge pull request #290 from SyncrowIOT:fix/issue-in-listing-spaces
fixed get spaces
2025-03-09 14:04:17 +04:00
63f1f7a7c0 fixed get spaces 2025-03-09 14:03:59 +04:00
36612a40b9 fix propagation 2025-03-09 13:56:06 +04:00
1220ee395d fixing propagation 2025-03-07 23:01:38 +04:00
226781e53f Merge pull request #289 from SyncrowIOT:bugfix/fix-unlinking-space-model
fixed unlinking space model on delete
2025-03-07 12:02:43 +04:00
76d29bab16 fixed unlinking space model on delete 2025-03-07 12:02:13 +04:00
cd781024fe Merge pull request #288 from SyncrowIOT/SP-1296-be-api-request-failure-500-internal-server-error-when-deleting-a-space
fix delete space and sub space
2025-03-07 11:17:50 +04:00
dd56300b96 fix delete space and sub space 2025-03-07 03:16:24 +03:00
c400e6f9d6 Merge pull request #287 from SyncrowIOT/bugfix/fix-overwrite-of-space-linking 2025-03-07 00:54:44 +04:00
d64e78f03e fixed overwriting 2025-03-07 00:54:13 +04:00
9fce11b8c1 Merge pull request #285 from SyncrowIOT:bugfix/create-space
fix the issue in create space
2025-03-07 00:29:42 +04:00
31e84b4a25 fix the issue in create space 2025-03-07 00:28:18 +04:00
107ec72e81 Merge pull request #284 from SyncrowIOT/bugfix/fix-issue-in-deleting-subspace
fix issue on deleting subspace
2025-03-06 22:55:16 +04:00
61fc699cf8 fix issue on deleting subspace 2025-03-06 22:51:19 +04:00
a68f4c1847 fixed allocating tag from subspace to space 2025-03-06 20:54:51 +04:00
1f18f50923 adding tag in space model will add tag to spaces 2025-03-06 15:49:14 +04:00
772459a106 Merge pull request #283 from SyncrowIOT/fix-get-spaces-by-community-response
finished edit the response
2025-03-06 14:33:13 +04:00
f7848a7394 finished edit the response 2025-03-06 13:03:56 +03:00
5e6c7bef9e Merge pull request #280 from SyncrowIOT/SP-1078-be-implement-api-to-assign-a-space-model-or-reassign-it-to-a-space
add space linking when create or update space
2025-03-06 12:33:31 +03:00
372d2c680d Merge pull request #282 from SyncrowIOT/bugfix/space-allocation 2025-03-06 13:10:08 +04:00
d0d1dd615a fixed issue in subspace allocation 2025-03-06 13:09:04 +04:00
61692c70d1 add overwrite when update spaces with spacesModel 2025-03-06 11:13:14 +03:00
07828eafd7 fix issues in deleting from space model product allocation and space product allocation 2025-03-06 11:01:08 +04:00
ed406addb8 Merge pull request #281 from SyncrowIOT/bugfix/update-space-model-on-tag-reuse 2025-03-06 09:36:53 +04:00
6805ff226c added proper validations 2025-03-06 09:35:25 +04:00
6864e000b8 add space linking when create or update space 2025-03-06 02:33:48 +03:00
823cb6cf7a no need to do anything if delete dto is empty 2025-03-05 23:57:38 +04:00
547e37dadb fix delete dto 2025-03-05 23:54:41 +04:00
81a0bc8bbc added validation in process tags 2025-03-05 23:37:13 +04:00
0d4afc40f2 Merge pull request #274 from SyncrowIOT/SP-1080-be-implement-the-validation-for-the-overwrite
finished spaces validation endpoint
2025-03-04 13:05:10 +04:00
9c279d9655 Merge pull request #277 from SyncrowIOT/SP-1228-be-update-the-delete-space-endpoint-with-the-changes-in-tags
Sp 1228 be update the delete space endpoint with the changes in tags
2025-03-04 12:50:07 +04:00
6a4361515d Merge pull request #279 from SyncrowIOT/bugfix/link-space-model 2025-03-04 12:48:54 +04:00
1caecfe6d9 removed the validation for space model subspaces and allocations 2025-03-04 12:48:21 +04:00
19c02da8ad Merge pull request #275 from SyncrowIOT/SP-1226-be-update-the-endpoint-to-list-space-with-tags-from-project
using new tags relation with space
2025-03-04 12:41:45 +04:00
0d3ec0bea1 Merge pull request #278 from SyncrowIOT/bugfix/fix-issues-in-create-space-model
Bugfix/fix-issues-in-create-space-model
2025-03-04 12:39:12 +04:00
553cdb28bf prettify 2025-03-04 00:45:53 +04:00
34f4b61ae0 changed description of controller 2025-03-04 00:45:42 +04:00
d059171b72 fixed issues in create space model 2025-03-04 00:44:22 +04:00
4e43954175 clear subspace tags allocation 2025-03-03 21:06:22 +03:00
2b6f6be149 clear space tags allocation 2025-03-03 21:05:54 +03:00
b4c3ee486e Merge branch 'dev' of https://github.com/SyncrowIOT/backend into dev 2025-03-03 17:58:22 +04:00
5d5984da80 Merge pull request #276 from SyncrowIOT/tags-dto-error
fix ProcessTagDto dto issues
2025-03-03 10:08:38 +03:00
95ecbab165 fix ProcessTagDto dto issues 2025-03-03 10:05:11 +03:00
221a2af32b using new tags relation with space 2025-03-03 00:23:55 +03:00
8a5709b1c9 finished spaces validation endpoint 2025-03-02 22:48:02 +03:00
35e658a3f6 fixed tag controller 2025-03-02 15:45:26 +04:00
e17a3fdda8 Merge pull request #273 from SyncrowIOT/feat/project-tag
New tag entitiy
2025-03-02 00:25:55 +04:00
810ca87301 revertback 2025-03-02 00:23:59 +04:00
e9cda53636 Merge branch 'dev' into feat/project-tag 2025-03-02 00:22:28 +04:00
416329771b Merge pull request #272 from SyncrowIOT/SP-1079-BE-Implement-API-for-linking-space-model-to-space 2025-03-02 00:17:50 +04:00
e90f9d44fc commenting out 2025-03-02 00:13:33 +04:00
9250b08b17 Merge branch 'feat/project-tag' of https://github.com/SyncrowIOT/backend into SP-1079-BE-Implement-API-for-linking-space-model-to-space 2025-03-02 00:08:32 +04:00
1d71be4b06 added linking 2025-03-02 00:08:28 +04:00
d85b374f05 Merge pull request #271 from SyncrowIOT/SP-1182-update-create-and-edit-space-to-use-new-tag-entity
Sp 1182 update create and edit space to use new tag entity
2025-02-28 11:40:38 +04:00
a9e5454b61 Add space product 2025-02-27 13:36:28 +03:00
902ddbab50 update space service 2025-02-27 13:36:08 +03:00
b6c305a6f7 added endpoint to link 2025-02-26 20:11:43 +04:00
2582b88c8c fix summary 2025-02-26 20:06:25 +04:00
7ddc39a7eb Merge branch 'feat/project-tag' of https://github.com/SyncrowIOT/backend into feat/project-tag 2025-02-26 17:51:55 +04:00
419a10e055 Merge pull request #270 from SyncrowIOT/SP-1229-be-return-community-id-with-each-space-in-the-user-management 2025-02-26 16:25:49 +03:00
e3e2981673 Include community details in user spaces response 2025-02-26 16:19:55 +03:00
cb6d359810 Merge pull request #269 from SyncrowIOT/feat/fix-delete-propagation 2025-02-26 16:03:44 +04:00
2a5b35a297 fixed query commit 2025-02-26 16:02:58 +04:00
60a4fb8ea2 Merge pull request #268 from SyncrowIOT:feat/fix-delete-propagation
Feat/fix-delete-propagation
2025-02-26 15:44:02 +04:00
f98c3cd4ad fixed delete propagation 2025-02-26 15:43:27 +04:00
5d962b3c6e Merge branch 'dev' of https://github.com/SyncrowIOT/backend into dev 2025-02-26 15:39:31 +04:00
25f5c62ecf linking dto 2025-02-26 15:39:23 +04:00
9297f1efb3 Merge pull request #267 from SyncrowIOT/SP-1227-BE-Update-the-endpoint-to-list-space-model-with-tags-from-project
added service to delete space model
2025-02-26 12:02:44 +04:00
6b20428719 added service to delete space model 2025-02-26 00:36:37 +04:00
8ecd368b16 fixed query to get and list space model 2025-02-26 00:36:13 +04:00
8739edb6e9 fixed dto description 2025-02-26 00:35:55 +04:00
aed9719178 removed tag service 2025-02-26 00:35:36 +04:00
ed1c7ff167 fixed swagger doc of get space model 2025-02-26 00:35:17 +04:00
edf1cd4157 added formatting for list space model 2025-02-25 23:58:33 +04:00
4326ac1900 Merge pull request #266 from SyncrowIOT:bugfix/module-provider-fix
added services to module
2025-02-25 23:16:25 +04:00
cd8d368e21 added services 2025-02-25 23:16:00 +04:00
2bdc406cf7 Merge pull request #265 from SyncrowIOT/fix-firebase-real-time-issue
change set to transaction
2025-02-25 15:37:02 +03:00
630b3ee2b9 change set to transaction 2025-02-25 15:33:04 +03:00
bcd976e1b2 Merge pull request #264 from SyncrowIOT/SP-1181-BE-Update-create-space-model-and-edit-space-model-to-use-new-tag-entity 2025-02-25 01:38:36 +04:00
67ce73754b fixed update space model 2025-02-25 01:37:23 +04:00
acc8f3ef4d updated delete action 2025-02-24 20:04:36 +04:00
4391339e5e updated subspace model covering all edge cases 2025-02-24 18:41:32 +04:00
d13da50fd7 fixed create space model 2025-02-21 21:22:48 +04:00
394dd0dd6d changed tag endpoint 2025-02-20 17:15:44 +04:00
b2d52c7622 Merge branch 'dev' 2025-02-20 03:46:08 -06:00
1140709e80 Merge pull request #263 from SyncrowIOT/bugfix/fetch-device-space-heirarchy-issue
fixed the heirarchy device fetching issue
2025-02-20 13:33:09 +04:00
a0790f0b48 fixed the heirarchy device fetching issue 2025-02-20 13:29:19 +04:00
c9cbb2e085 Merge pull request #262 from SyncrowIOT/dev
change subspace tag movement
2025-02-19 13:11:46 +04:00
c52d0981db Merge branch 'dev' of https://github.com/SyncrowIOT/backend into dev 2025-02-19 13:10:34 +04:00
6550ed68f6 added to gitignore 2025-02-19 13:10:28 +04:00
c2a6012063 Merge pull request #261 from SyncrowIOT/bugfix/set-space-null-for-tag
on moving a tag from subspace, or subspace to subspace, set space is null
2025-02-19 13:08:23 +04:00
4adb1e683d on moving a space from subspace, or subspace to subspace, set space is null 2025-02-19 13:06:00 +04:00
8aa3de5fdc config 2025-02-18 16:59:38 +04:00
bc1ee9a53b test deploy 2 2025-02-18 05:39:55 -06:00
19356c3833 test deploy 2025-02-18 05:35:06 -06:00
8737ee992b Update GitHub Actions workflow for Node.js app deployment to Azure 2025-02-18 05:08:51 -06:00
e98a99be73 Update GitHub Actions workflow for containerized deployment to Azure Web App 2025-02-18 05:03:05 -06:00
93efa15f3c Empty commit 2025-02-18 04:50:54 -06:00
c305e39ff2 Add or update the Azure App Service build and deployment workflow config 2025-02-18 04:34:31 -06:00
61e4d220dc test deploy 2025-02-18 04:15:24 -06:00
cd4bbe1788 Empty commit 2025-02-18 00:10:22 -06:00
6301da0665 add logs 2025-02-18 09:20:21 +04:00
d770a0c732 Remove robots.txt request handling middleware 2025-02-17 18:51:16 -06:00
030e6ae902 Add middleware to ignore requests for robots*.txt files 2025-02-17 18:43:43 -06:00
9d8287b82b Remove trailing whitespace in GitHub workflow file 2025-02-17 18:05:20 -06:00
d741a6c1f3 Empty commit 2025-02-17 17:50:51 -06:00
6d55704dd4 Merge branch 'dev' 2025-02-17 17:35:45 -06:00
c7ffba7290 fix dependency issue 2025-02-17 23:15:27 +04:00
0059562319 Merge branch 'SP-1181-BE-Update-create-space-model-and-edit-space-model-to-use-new-tag-entity' of https://github.com/SyncrowIOT/backend into SP-1181-BE-Update-create-space-model-and-edit-space-model-to-use-new-tag-entity 2025-02-17 23:07:22 +04:00
7ddd75041e added project validation 2025-02-17 23:07:15 +04:00
07d31bb8bd comment 2025-02-17 22:27:35 +04:00
61a85789d1 tag 2025-02-17 22:19:52 +04:00
565b0f72b7 update 2025-02-17 17:11:56 +04:00
19ef10aa87 Merge pull request #259 from SyncrowIOT/add-space-name-to-door-lock-devices
Add space device relation and space name to device details
2025-02-17 02:18:18 -06:00
733ccee88d Add space device relation and space name to device details 2025-02-17 02:13:28 -06:00
afec306681 Merge pull request #258 from SyncrowIOT/fix-get-device-by-project
Add project UUID filter to device query in DeviceService
2025-02-17 01:16:36 -06:00
6fbeef97df Add project UUID filter to device query in DeviceService 2025-02-17 01:15:48 -06:00
29d9909c8e Merge pull request #256 from SyncrowIOT/feat/move-device-and-visitor-password-to-project
Feat/move device and visitor password to project
2025-02-16 23:31:31 +04:00
2dcce3cce3 roll back 2025-02-16 23:30:14 +04:00
427a57501f Moved get visitor password and get device for visitor password 2025-02-16 23:26:20 +04:00
b59230d5d8 Merge branch 'dev' of https://github.com/SyncrowIOT/backend into dev 2025-02-15 11:33:23 +04:00
e276d4651d new bulk save tags 2025-02-15 11:33:18 +04:00
9eb9b92be3 update space-model service 2025-02-15 11:33:10 +04:00
b2b8d02400 Merge pull request #255 from SyncrowIOT/return-project-uuid-in-user-details
Include project relation in user retrieval
2025-02-13 05:59:32 -06:00
b793597711 Include project relation in user retrieval 2025-02-13 05:53:48 -06:00
d8ad9e55ea Merge pull request #253 from SyncrowIOT/dev
Dev
2025-02-06 09:26:54 +04:00
371 changed files with 22926 additions and 8685 deletions

View File

@ -21,6 +21,7 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
"@typescript-eslint/no-unused-vars": 'warn',
},
settings: {
'import/resolver': {

17
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,17 @@
<!--
Thanks for contributing!
Provide a description of your changes below and a general summary in the title.
-->
## Jira Ticket
[SP-0000](https://syncrow.atlassian.net/browse/SP-0000)
## Description
<!--- Describe your changes in detail -->
## How to Test
<!--- Describe the created APIs / Logic -->

View File

@ -14,24 +14,24 @@ jobs:
runs-on: 'ubuntu-latest'
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to registry
uses: docker/login-action@v2
with:
registry: https://syncrow.azurecr.io/
username: ${{ secrets.AzureAppService_ContainerUsername_47395803300340b49931ea82f6d80be3 }}
password: ${{ secrets.AzureAppService_ContainerPassword_e7b0ff54f54d44cba04a970a22384848 }}
- name: Log in to registry
uses: docker/login-action@v2
with:
registry: https://syncrow.azurecr.io/
username: ${{ secrets.AzureAppService_ContainerUsername_47395803300340b49931ea82f6d80be3 }}
password: ${{ secrets.AzureAppService_ContainerPassword_e7b0ff54f54d44cba04a970a22384848 }}
- name: Build and push container image to registry
uses: docker/build-push-action@v3
with:
push: true
tags: syncrow.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_47395803300340b49931ea82f6d80be3 }}/syncrow/backend:${{ github.sha }}
file: ./Dockerfile
- name: Build and push container image to registry
uses: docker/build-push-action@v3
with:
push: true
tags: syncrow.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_47395803300340b49931ea82f6d80be3 }}/syncrow/backend:${{ github.sha }}
file: ./Dockerfile
deploy:
runs-on: ubuntu-latest
@ -41,11 +41,11 @@ jobs:
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
steps:
- name: Deploy to Azure Web App
id: deploy-to-webapp
uses: azure/webapps-deploy@v2
with:
app-name: 'syncrow'
slot-name: 'staging'
publish-profile: ${{ secrets.AzureAppService_PublishProfile_44f7766441ec4796b74789e9761ef589 }}
images: 'syncrow.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_47395803300340b49931ea82f6d80be3 }}/syncrow/backend:${{ github.sha }}'
- name: Deploy to Azure Web App
id: deploy-to-webapp
uses: azure/webapps-deploy@v2
with:
app-name: 'syncrow'
slot-name: 'staging'
publish-profile: ${{ secrets.AzureAppService_PublishProfile_44f7766441ec4796b74789e9761ef589 }}
images: 'syncrow.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_47395803300340b49931ea82f6d80be3 }}/syncrow/backend:${{ github.sha }}'

73
.github/workflows/main_syncrow(stg).yml vendored Normal file
View File

@ -0,0 +1,73 @@
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions
name: Build and deploy Node.js app to Azure Web App - syncrow
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read #This is required for actions/checkout
steps:
- uses: actions/checkout@v4
- name: Set up Node.js version
uses: actions/setup-node@v3
with:
node-version: '20.x'
- name: npm install, build, and test
run: |
npm install
npm run build --if-present
npm run test --if-present
- name: Zip artifact for deployment
run: zip release.zip ./* -r
- name: Upload artifact for deployment job
uses: actions/upload-artifact@v4
with:
name: node-app
path: release.zip
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'stg'
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
permissions:
id-token: write #This is required for requesting the JWT
contents: read #This is required for actions/checkout
steps:
- name: Download artifact from build job
uses: actions/download-artifact@v4
with:
name: node-app
- name: Unzip artifact for deployment
run: unzip release.zip
- name: Login to Azure
uses: azure/login@v2
with:
client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_515C8E782CFF431AB20448C85CA0FE58 }}
tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_2AEFE5534424490387C08FAE41573CC2 }}
subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_00623C33023749FEA5F6BC36884F9C8A }}
- name: 'Deploy to Azure Web App'
id: deploy-to-webapp
uses: azure/webapps-deploy@v3
with:
app-name: 'syncrow'
slot-name: 'stg'
package: .

64
.github/workflows/pr-description.yml vendored Normal file
View 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 }}

40
.github/workflows/production.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: 🚀 Production Deployment
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout Code
uses: actions/checkout@v4
- name: 🐢 Set up Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: '20'
- name: 🐳 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🔐 Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: me-central-1
- name: 🗂️ Write .env file from ENV_FILE Secret
run: echo "${{ secrets.ENV_FILE }}" > .env
- name: 📦 Install Dependencies
run: npm install
- name: 🛠️ Run Production Build & Deploy Script
run: |
chmod +x ./build.sh
./build.sh

10
.gitignore vendored
View File

@ -3,6 +3,9 @@
/node_modules
/build
#github
/.github/workflows
# Logs
logs
*.log
@ -55,4 +58,9 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
config.dev
config.dev
cdk.out
backend-cdk-new.out
web-cdk.out
backend-cdk.out
backend-cdk-final.out

View File

@ -1,16 +1,28 @@
FROM node:20-alpine
FROM --platform=linux/amd64 node:20-alpine
# curl for health checks
RUN apk add --no-cache curl
WORKDIR /app
COPY package*.json ./
RUN npm install
RUN npm install -g @nestjs/cli
RUN npm install --production --ignore-scripts
COPY . .
RUN npm run build
EXPOSE 4000
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nestjs -u 1001
CMD ["npm", "run", "start"]
RUN chown -R nestjs:nodejs /app
USER nestjs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["npm", "run", "start:prod"]

129
GITHUB_SETUP.md Normal file
View File

@ -0,0 +1,129 @@
# GitHub Actions Setup Guide
## Required GitHub Secrets
Add these secrets to your GitHub repository (Settings > Secrets and variables > Actions):
### AWS Credentials
```
AWS_ACCESS_KEY_ID=your-aws-access-key
AWS_SECRET_ACCESS_KEY=your-aws-secret-key
```
### JWT Configuration (CRITICAL - Generate secure random strings)
```
JWT_SECRET=your-super-secure-jwt-secret-key-here
JWT_SECRET_REFRESH=your-super-secure-refresh-secret-key-here
SECRET_KEY=your-general-encryption-secret-key-here
```
### Admin Configuration
```
SUPER_ADMIN_EMAIL=admin@syncrow.ae
SUPER_ADMIN_PASSWORD=YourSecureAdminPassword123!
```
### Tuya IoT Configuration
```
TUYA_ACCESS_ID=your-tuya-access-id
TUYA_ACCESS_KEY=your-tuya-access-key
TRUN_ON_TUYA_SOCKET=true-or-false
```
### Firebase Configuration
```
FIREBASE_API_KEY=your-firebase-api-key
FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
FIREBASE_PROJECT_ID=your-project-id
FIREBASE_STORAGE_BUCKET=your-project.appspot.com
FIREBASE_MESSAGING_SENDER_ID=your-sender-id
FIREBASE_APP_ID=your-app-id
FIREBASE_MEASUREMENT_ID=your-measurement-id
FIREBASE_DATABASE_URL=https://your-project.firebaseio.com
```
### Google OAuth
```
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
```
### OneSignal Push Notifications
```
ONESIGNAL_APP_ID=your-onesignal-app-id
ONESIGNAL_API_KEY=your-onesignal-api-key
```
### Email Configuration (SMTP)
```
SMTP_HOST=your-smtp-host
SMTP_USER=your-smtp-username
SMTP_PASSWORD=your-smtp-password
```
### Mailtrap Configuration
```
MAILTRAP_API_TOKEN=your-mailtrap-api-token
MAILTRAP_ENABLE_TEMPLATE_UUID=template-uuid
MAILTRAP_DISABLE_TEMPLATE_UUID=template-uuid
MAILTRAP_INVITATION_TEMPLATE_UUID=template-uuid
MAILTRAP_DELETE_USER_TEMPLATE_UUID=template-uuid
MAILTRAP_EDIT_USER_TEMPLATE_UUID=template-uuid
```
### Optional Services (leave empty if not used)
```
AZURE_REDIS_CONNECTIONSTRING=your-redis-connection-string
DOPPLER_PROJECT=your-doppler-project
DOPPLER_CONFIG=your-doppler-config
DOPPLER_ENVIRONMENT=your-doppler-environment
ACCESS_KEY=your-access-key
DOCKER_REGISTRY_SERVER_URL=your-registry-url
DOCKER_REGISTRY_SERVER_USERNAME=your-registry-username
DOCKER_REGISTRY_SERVER_PASSWORD=your-registry-password
```
## Setup Steps
1. **Add AWS Credentials**
- Create IAM user with ECR, ECS, CloudFormation permissions
- Add AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to GitHub Secrets
2. **Generate JWT Secrets**
- Use a secure random string generator
- Make JWT_SECRET and JWT_SECRET_REFRESH different values
- Keep these values secure and never share them
3. **Configure Services**
- Add secrets for each service you're using
- Leave unused services empty (they'll default to empty strings)
4. **Test Deployment**
- Push to master/main branch
- Check GitHub Actions tab for deployment status
- Verify API is accessible at https://api.syncos.syncrow.ae
## Security Notes
- Never commit secrets to the repository
- Use GitHub Secrets for all sensitive values
- Rotate secrets regularly
- Monitor GitHub Actions logs for any exposed values
- Database password is automatically managed by AWS Secrets Manager
## Troubleshooting
- Check GitHub Actions logs for deployment errors
- Verify all required secrets are set
- Ensure AWS credentials have sufficient permissions
- Check ECS service logs in CloudWatch for runtime errors

View File

@ -1,17 +1,19 @@
# Backend
## Overview
This is the backend for an IoT application built using NestJS. It interfaces with the Tuya IoT cloud platform to manage homes, rooms, devices, ...etc.
This is the backend APIs project, developed with NestJS for Syncrow IOT Project.
This is the backend APIs project, developed with NestJS for Syncrow IOT Project.
## Database Model
The database uses PostgreSQL and TypeORM. Below is an entity relationship diagram:
The main entities are:
User - Stores user account information
Home - Represents a home/space
Room - Represents a room/sub-space
Home - Represents a home/space
Room - Represents a room/sub-space
Device - Represents a connected device
Product - Stores metadata about device products
Other Entities - sessions, OTPs, etc.
@ -19,10 +21,11 @@ Other Entities - sessions, OTPs, etc.
The entities have a one-to-many relationship - a user has multiple homes, a home has multiple rooms, and a room has multiple devices.
## Architecture
The application is deployed on Azure App Service using Docker containers. There are separate deployment slots for development, staging, and production environments.
## Installation
First, ensure that you have Node.js `v20.11` or newer (LTS ONLY) installed on your system.
To install the project dependencies, run the following command in the project root directory:
@ -61,8 +64,8 @@ $ npm run test:cov
![Syncrow ERD Digram](https://github.com/SyncrowIOT/backend/assets/83524184/94273a2b-625c-4a34-9cd4-fb14415ce884)
## Architecture
+----------------------------------+
| |
| Applications |
@ -107,3 +110,29 @@ $ npm run test:cov
| | Standby Node | | |
| +------------------+----------------+ |
+-----------------------------------------------------------------+
## CDK Deployment
• Bootstrap CDK (first time only): npx cdk bootstrap aws://482311766496/me-central-1
• List available stacks: npx cdk list
• Deploy infrastructure: npx cdk deploy --require-approval never
• View changes before deploy: npx cdk diff
• Generate CloudFormation template: npx cdk synth
• Destroy infrastructure: npx cdk destroy
• Environment variables are configured in infrastructure/stack.ts
• After code changes: build Docker image, push to ECR, force ECS deployment
• Database seeding happens automatically on first deployment with DB_SYNC=true
• Admin credentials: admin@syncrow.ae / YourSecureAdminPassword123!
• Production API: https://api.syncos.syncrow.ae
• Health check: https://api.syncos.syncrow.ae/health
## GitHub Actions Deployment
• Automatic deployment on push to master/main branch
• Configure GitHub Secrets (see GITHUB_SETUP.md for complete list)
• Required secrets: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, JWT_SECRET, JWT_SECRET_REFRESH
• Workflow builds Docker image, pushes to ECR, and deploys CDK stack
• Environment variables are passed securely via GitHub Secrets
• Manual deployment: Go to Actions tab and run "Deploy Backend to AWS" workflow
• Check deployment status in GitHub Actions tab
• Logs available in CloudWatch under /ecs/syncrow-backend log group

46
build.sh Normal file
View File

@ -0,0 +1,46 @@
#!/bin/bash
set -e
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=${AWS_DEFAULT_REGION:-me-central-1}
REPO_NAME=syncrow-backend
IMAGE_TAG=latest
CLUSTER_NAME=syncrow-backend-cluster
STACK_NAME=SyncrowBackendStack
CERTIFICATE_ARN="arn:aws:acm:$REGION:$ACCOUNT_ID:certificate/bea1e2ae-84a1-414e-8dbf-4599397e7ed0"
echo "🔐 Logging into ECR..."
aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com"
echo "🐳 Building Docker image..."
docker build --platform=linux/amd64 -t $REPO_NAME .
echo "🏷️ Tagging image..."
docker tag $REPO_NAME:$IMAGE_TAG "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$REPO_NAME:$IMAGE_TAG"
echo "📤 Pushing image to ECR..."
docker push "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$REPO_NAME:$IMAGE_TAG"
echo "🔍 Checking if ECS service exists..."
SERVICE_ARN=$(aws ecs list-services \
--cluster $CLUSTER_NAME \
--query 'serviceArns[0]' \
--output text \
--region $REGION 2>/dev/null || echo "")
echo "📦 Deploying CDK Stack..."
npx cdk deploy $STACK_NAME \
--context certificateArn=$CERTIFICATE_ARN \
--require-approval never
if [[ "$SERVICE_ARN" != "" && "$SERVICE_ARN" != "None" ]]; then
SERVICE_NAME=$(basename "$SERVICE_ARN")
echo "🚀 Redeploying ECS Service: $SERVICE_NAME"
aws ecs update-service \
--cluster $CLUSTER_NAME \
--service $SERVICE_NAME \
--force-new-deployment \
--region $REGION
fi
echo "✅ All done."

29
cdk.context.json Normal file
View File

@ -0,0 +1,29 @@
{
"availability-zones:account=426265406140:region=us-east-2": [
"us-east-2a",
"us-east-2b",
"us-east-2c"
],
"availability-zones:account=482311766496:region=us-east-2": [
"us-east-2a",
"us-east-2b",
"us-east-2c"
],
"hosted-zone:account=482311766496:domainName=syncrow.me:region=us-east-2": {
"Id": "/hostedzone/Z02085662NLJECF4DGJV3",
"Name": "syncrow.me."
},
"availability-zones:account=482311766496:region=me-central-1": [
"me-central-1a",
"me-central-1b",
"me-central-1c"
],
"hosted-zone:account=482311766496:domainName=syncrow.me:region=me-central-1": {
"Id": "/hostedzone/Z02085662NLJECF4DGJV3",
"Name": "syncrow.me."
},
"hosted-zone:account=482311766496:domainName=syncrow.ae:region=me-central-1": {
"Id": "/hostedzone/Z01153152LRHQTA1370P4",
"Name": "syncrow.ae."
}
}

58
cdk.json Normal file
View File

@ -0,0 +1,58 @@
{
"app": "npx ts-node --prefer-ts-exts infrastructure/app.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableLogging": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForSourceAction": true
}
}

22
deploy.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
set -e
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=${AWS_DEFAULT_REGION:-me-central-1}
npx cdk deploy SyncrowBackendStack --context certificateArn=arn:aws:acm:me-central-1:482311766496:certificate/bea1e2ae-84a1-414e-8dbf-4599397e7ed0 --require-approval never
aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com
docker build --platform=linux/amd64 -t syncrow-backend .
docker tag syncrow-backend:latest $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/syncrow-backend:latest
docker push $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/syncrow-backend:latest
SERVICE_ARN=$(aws ecs list-services --cluster syncrow-backend-cluster --query 'serviceArns[0]' --output text --region $REGION 2>/dev/null || echo "")
if [ "$SERVICE_ARN" != "" ] && [ "$SERVICE_ARN" != "None" ]; then
SERVICE_NAME=$(echo $SERVICE_ARN | cut -d'/' -f3)
aws ecs update-service --cluster syncrow-backend-cluster --service $SERVICE_NAME --force-new-deployment --region $REGION
else
npx cdk deploy SyncrowBackendStack --context certificateArn=arn:aws:acm:me-central-1:482311766496:certificate/bea1e2ae-84a1-414e-8dbf-4599397e7ed0 --require-approval never
fi

16
infrastructure/app.ts Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import 'source-map-support/register';
import { BackendStack } from './stack';
const app = new cdk.App();
new BackendStack(app, 'SyncrowBackendStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: 'me-central-1',
},
databaseName: 'postgres',
certificateArn:
'arn:aws:acm:me-central-1:482311766496:certificate/423b343e-402b-4978-89bd-cda25f7a8873',
});

393
infrastructure/stack.ts Normal file
View File

@ -0,0 +1,393 @@
import * as cdk from 'aws-cdk-lib';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as route53 from 'aws-cdk-lib/aws-route53';
import { Construct } from 'constructs';
import * as dotenv from 'dotenv';
export interface BackendStackProps extends cdk.StackProps {
vpcId?: string;
databaseName?: string;
certificateArn?: string;
}
export class BackendStack extends cdk.Stack {
public readonly apiUrl: string;
public readonly databaseEndpoint: string;
public readonly vpc: ec2.IVpc;
constructor(scope: Construct, id: string, props?: BackendStackProps) {
super(scope, id, props);
// Load environment variables from .env file
dotenv.config({ path: '.env' });
// VPC - either use existing or create new
this.vpc = props?.vpcId
? ec2.Vpc.fromLookup(this, 'ExistingVpc', { vpcId: props.vpcId })
: new ec2.Vpc(this, 'SyncrowVpc', {
maxAzs: 2,
natGateways: 1,
subnetConfiguration: [
{
cidrMask: 24,
name: 'public',
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
],
});
// Security Groups
const dbSecurityGroup = new ec2.SecurityGroup(
this,
'DatabaseSecurityGroup',
{
vpc: this.vpc,
description: 'Security group for RDS PostgreSQL',
allowAllOutbound: false,
},
);
const ecsSecurityGroup = new ec2.SecurityGroup(this, 'EcsSecurityGroup', {
vpc: this.vpc,
description: 'Security group for ECS Fargate service',
allowAllOutbound: true,
});
const albSecurityGroup = new ec2.SecurityGroup(this, 'AlbSecurityGroup', {
vpc: this.vpc,
description: 'Security group for Application Load Balancer',
allowAllOutbound: true,
});
// Allow ALB to connect to ECS
ecsSecurityGroup.addIngressRule(
albSecurityGroup,
ec2.Port.tcp(3000),
'Allow ALB to connect to ECS service',
);
// Allow ECS to connect to RDS
dbSecurityGroup.addIngressRule(
ecsSecurityGroup,
ec2.Port.tcp(5432),
'Allow ECS to connect to PostgreSQL',
);
// Temporary access for admin IP
dbSecurityGroup.addIngressRule(
ec2.Peer.ipv4('216.126.231.231/32'),
ec2.Port.tcp(5432),
'Temporary access from admin IP',
);
// Allow HTTP/HTTPS traffic to ALB
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(80),
'Allow HTTP traffic',
);
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'Allow HTTPS traffic',
);
const dbCluster = rds.DatabaseCluster.fromDatabaseClusterAttributes(
this,
'SyncrowDatabase',
{
clusterIdentifier: 'syncrow-backend',
instanceIdentifiers: ['syncrowdatabase-instance-1'],
engine: rds.DatabaseClusterEngine.auroraPostgres({
version: rds.AuroraPostgresEngineVersion.VER_16_6,
}),
port: 5432,
securityGroups: [
ec2.SecurityGroup.fromSecurityGroupId(
this,
'ImportedDbSecurityGroup',
'sg-07e163f588b2bac25',
),
],
clusterEndpointAddress:
'syncrow-backend.cluster-criskv1sdkq4.me-central-1.rds.amazonaws.com',
},
);
// Import the existing database secret separately
const dbSecret = rds.DatabaseSecret.fromSecretCompleteArn(
this,
'ImportedDbSecret',
'arn:aws:secretsmanager:me-central-1:482311766496:secret:rds!cluster-43ec14cd-9301-43e2-aa79-d330a429a126-v0JDQN',
);
// ECR Repository for Docker images - import existing repository
const ecrRepository = ecr.Repository.fromRepositoryName(
this,
'SyncrowBackendRepo',
'syncrow-backend',
);
// Output the correct ECR URI for this region
new cdk.CfnOutput(this, 'EcrRepositoryUriRegional', {
value: ecrRepository.repositoryUri,
description: `ECR Repository URI in region ${this.region}`,
exportName: `${this.stackName}-EcrRepositoryUriRegional`,
});
// ECS Cluster
const cluster = new ecs.Cluster(this, 'SyncrowCluster', {
vpc: this.vpc,
clusterName: 'syncrow-backend-cluster',
});
// CloudWatch Log Group
const logGroup = new logs.LogGroup(this, 'SyncrowBackendLogs', {
logGroupName: '/ecs/syncrow-backend',
retention: logs.RetentionDays.ONE_WEEK,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// Use existing wildcard certificate or create new one
const apiCertificate = props?.certificateArn
? acm.Certificate.fromCertificateArn(
this,
'ApiCertificate',
props.certificateArn,
)
: new acm.Certificate(this, 'ApiCertificate', {
domainName: 'api.syncos.syncrow.ae',
validation: acm.CertificateValidation.fromDns(),
});
// ECS Fargate Service with Application Load Balancer
const fargateService =
new ecsPatterns.ApplicationLoadBalancedFargateService(
this,
'SyncrowBackendService',
{
cluster,
memoryLimitMiB: 1024,
cpu: 512,
desiredCount: 2,
domainName: 'api.syncos.syncrow.ae',
domainZone: route53.HostedZone.fromLookup(this, 'SyncrowZone', {
domainName: 'syncrow.ae',
}),
certificate: apiCertificate,
protocol: elbv2.ApplicationProtocol.HTTPS,
redirectHTTP: true,
taskImageOptions: {
image: ecs.ContainerImage.fromEcrRepository(
ecrRepository,
'latest',
),
containerPort: 3000,
enableLogging: true,
environment: {
// App settings
NODE_ENV: process.env.NODE_ENV || 'production',
PORT: process.env.PORT || '3000',
BASE_URL: process.env.BASE_URL || '',
// Database connection (CDK provides these automatically)
AZURE_POSTGRESQL_HOST: dbCluster.clusterEndpoint.hostname,
AZURE_POSTGRESQL_PORT: '5432',
AZURE_POSTGRESQL_DATABASE: props?.databaseName || 'postgres',
AZURE_POSTGRESQL_USER: 'postgres',
AZURE_POSTGRESQL_SSL: process.env.AZURE_POSTGRESQL_SSL || 'false',
AZURE_POSTGRESQL_SYNC:
process.env.AZURE_POSTGRESQL_SYNC || 'false',
// JWT Configuration - CRITICAL: These must be set
JWT_SECRET:
process.env.JWT_SECRET ||
'syncrow-jwt-secret-key-2025-production-environment-very-secure-random-string',
JWT_SECRET_REFRESH:
process.env.JWT_SECRET_REFRESH ||
'syncrow-refresh-secret-key-2025-production-environment-different-secure-string',
JWT_EXPIRE_TIME: process.env.JWT_EXPIRE_TIME || '1h',
JWT_EXPIRE_TIME_REFRESH:
process.env.JWT_EXPIRE_TIME_REFRESH || '7d',
// Firebase Configuration
FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || '',
FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN || '',
FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID || '',
FIREBASE_STORAGE_BUCKET:
process.env.FIREBASE_STORAGE_BUCKET || '',
FIREBASE_MESSAGING_SENDER_ID:
process.env.FIREBASE_MESSAGING_SENDER_ID || '',
FIREBASE_APP_ID: process.env.FIREBASE_APP_ID || '',
FIREBASE_MEASUREMENT_ID:
process.env.FIREBASE_MEASUREMENT_ID || '',
FIREBASE_DATABASE_URL: process.env.FIREBASE_DATABASE_URL || '',
// Tuya IoT Configuration
TUYA_EU_URL:
process.env.TUYA_EU_URL || 'https://openapi.tuyaeu.com',
TUYA_ACCESS_ID: process.env.TUYA_ACCESS_ID || '',
TUYA_ACCESS_KEY: process.env.TUYA_ACCESS_KEY || '',
TRUN_ON_TUYA_SOCKET: process.env.TRUN_ON_TUYA_SOCKET || '',
// Email Configuration
SMTP_HOST: process.env.SMTP_HOST || '',
SMTP_PORT: process.env.SMTP_PORT || '587',
SMTP_SECURE: process.env.SMTP_SECURE || 'true',
SMTP_USER: process.env.SMTP_USER || '',
SMTP_PASSWORD: process.env.SMTP_PASSWORD || '',
// Mailtrap Configuration
MAILTRAP_API_TOKEN: process.env.MAILTRAP_API_TOKEN || '',
MAILTRAP_INVITATION_TEMPLATE_UUID:
process.env.MAILTRAP_INVITATION_TEMPLATE_UUID || '',
MAILTRAP_EDIT_USER_TEMPLATE_UUID:
process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID || '',
MAILTRAP_DISABLE_TEMPLATE_UUID:
process.env.MAILTRAP_DISABLE_TEMPLATE_UUID || '',
MAILTRAP_ENABLE_TEMPLATE_UUID:
process.env.MAILTRAP_ENABLE_TEMPLATE_UUID || '',
MAILTRAP_DELETE_USER_TEMPLATE_UUID:
process.env.MAILTRAP_DELETE_USER_TEMPLATE_UUID || '',
// OneSignal Push Notifications
ONESIGNAL_APP_ID: process.env.ONESIGNAL_APP_ID || '',
ONESIGNAL_API_KEY: process.env.ONESIGNAL_API_KEY || '',
// Admin Configuration
SUPER_ADMIN_EMAIL:
process.env.SUPER_ADMIN_EMAIL || 'admin@yourdomain.com',
SUPER_ADMIN_PASSWORD:
process.env.SUPER_ADMIN_PASSWORD ||
'YourSecureAdminPassword123!',
// Google OAuth
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID || '',
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET || '',
// Other Configuration
OTP_LIMITER: process.env.OTP_LIMITER || '5',
SECRET_KEY:
process.env.SECRET_KEY ||
'another-random-secret-key-for-general-encryption',
ACCESS_KEY: process.env.ACCESS_KEY || '',
DB_SYNC: process.env.DB_SYNC || 'txsrue',
// Redis (used?)
AZURE_REDIS_CONNECTIONSTRING:
process.env.AZURE_REDIS_CONNECTIONSTRING || '',
// Docker Registry (for deployment)
DOCKER_REGISTRY_SERVER_URL:
process.env.DOCKER_REGISTRY_SERVER_URL || '',
DOCKER_REGISTRY_SERVER_USERNAME:
process.env.DOCKER_REGISTRY_SERVER_USERNAME || '',
DOCKER_REGISTRY_SERVER_PASSWORD:
process.env.DOCKER_REGISTRY_SERVER_PASSWORD || '',
// Doppler (if used for secrets management)
DOPPLER_PROJECT: process.env.DOPPLER_PROJECT || '',
DOPPLER_CONFIG: process.env.DOPPLER_CONFIG || '',
DOPPLER_ENVIRONMENT: process.env.DOPPLER_ENVIRONMENT || '',
// Azure specific
WEBSITES_ENABLE_APP_SERVICE_STORAGE:
process.env.WEBSITES_ENABLE_APP_SERVICE_STORAGE || 'false',
},
secrets: {
AZURE_POSTGRESQL_PASSWORD: ecs.Secret.fromSecretsManager(
dbSecret,
'password',
),
},
logDriver: ecs.LogDrivers.awsLogs({
streamPrefix: 'syncrow-backend',
logGroup,
}),
},
publicLoadBalancer: true,
securityGroups: [ecsSecurityGroup],
},
);
// Add security group to load balancer after creation
fargateService.loadBalancer.addSecurityGroup(albSecurityGroup);
// Configure health check
fargateService.targetGroup.configureHealthCheck({
path: '/health',
healthyHttpCodes: '200',
interval: cdk.Duration.seconds(30),
timeout: cdk.Duration.seconds(5),
healthyThresholdCount: 2,
unhealthyThresholdCount: 3,
});
// Auto Scaling
const scalableTarget = fargateService.service.autoScaleTaskCount({
minCapacity: 1,
maxCapacity: 10,
});
scalableTarget.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
scaleInCooldown: cdk.Duration.minutes(5),
scaleOutCooldown: cdk.Duration.minutes(2),
});
scalableTarget.scaleOnMemoryUtilization('MemoryScaling', {
targetUtilizationPercent: 80,
scaleInCooldown: cdk.Duration.minutes(5),
scaleOutCooldown: cdk.Duration.minutes(2),
});
// Grant ECS task access to RDS credentials
dbSecret.grantRead(fargateService.taskDefinition.taskRole);
this.apiUrl = 'https://api.syncos.syncrow.ae';
this.databaseEndpoint = dbCluster.clusterEndpoint.hostname;
// Outputs
new cdk.CfnOutput(this, 'ApiUrl', {
value: this.apiUrl,
description: 'Application Load Balancer URL',
exportName: `${this.stackName}-ApiUrl`,
});
new cdk.CfnOutput(this, 'DatabaseEndpoint', {
value: this.databaseEndpoint,
description: 'RDS Cluster Endpoint',
exportName: `${this.stackName}-DatabaseEndpoint`,
});
new cdk.CfnOutput(this, 'EcrRepositoryUri', {
value: ecrRepository.repositoryUri,
description: 'ECR Repository URI',
exportName: `${this.stackName}-EcrRepositoryUri`,
});
new cdk.CfnOutput(this, 'ClusterName', {
value: cluster.clusterName,
description: 'ECS Cluster Name',
exportName: `${this.stackName}-ClusterName`,
});
new cdk.CfnOutput(this, 'ServiceName', {
value: fargateService.service.serviceName,
description: 'ECS Service Name',
exportName: `${this.stackName}-ServiceName`,
});
}
}

View File

@ -5,4 +5,5 @@ export class AuthInterface {
sessionId: string;
id: number;
role?: object;
project?: object;
}

View File

@ -1,18 +1,19 @@
import { PlatformType } from '@app/common/constants/platform-type.enum';
import { RoleType } from '@app/common/constants/role.type.enum';
import { UserEntity } from '@app/common/modules/user/entities';
import {
BadRequestException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as argon2 from 'argon2';
import { HelperHashService } from '../../helper/services';
import { UserRepository } from '../../../../common/src/modules/user/repositories';
import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository';
import { UserSessionEntity } from '../../../../common/src/modules/session/entities';
import { ConfigService } from '@nestjs/config';
import { OAuth2Client } from 'google-auth-library';
import { PlatformType } from '@app/common/constants/platform-type.enum';
import { RoleType } from '@app/common/constants/role.type.enum';
import { UserSessionEntity } from '../../../../common/src/modules/session/entities';
import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository';
import { UserRepository } from '../../../../common/src/modules/user/repositories';
import { HelperHashService } from '../../helper/services';
@Injectable()
export class AuthService {
@ -32,24 +33,25 @@ export class AuthService {
pass: string,
regionUuid?: string,
platform?: PlatformType,
): Promise<any> {
): Promise<Omit<UserEntity, 'password'>> {
const user = await this.userRepository.findOne({
where: {
email,
region: regionUuid ? { uuid: regionUuid } : undefined,
},
relations: ['roleType'],
relations: ['roleType', 'project'],
});
if (
platform === PlatformType.WEB &&
(user.roleType.type === RoleType.SPACE_OWNER ||
user.roleType.type === RoleType.SPACE_MEMBER)
) {
throw new UnauthorizedException('Access denied for web platform');
}
if (!user) {
throw new BadRequestException('Invalid credentials');
}
if (
platform === PlatformType.WEB &&
[RoleType.SPACE_OWNER, RoleType.SPACE_MEMBER].includes(
user.roleType.type as RoleType,
)
) {
throw new UnauthorizedException('Access denied for web platform');
}
if (!user.isUserVerified) {
throw new BadRequestException('User is not verified');
@ -69,29 +71,37 @@ export class AuthService {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password, ...result } = user;
return result;
// const { password, ...result } = user;
delete user.password;
return user;
}
async createSession(data): Promise<UserSessionEntity> {
return await this.sessionRepository.save(data);
}
async getTokens(payload) {
async getTokens(
payload,
isRefreshToken = true,
accessTokenExpiry = '24h',
refreshTokenExpiry = '30d',
) {
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload, {
secret: this.configService.get<string>('JWT_SECRET'),
expiresIn: '24h',
}),
this.jwtService.signAsync(payload, {
secret: this.configService.get<string>('JWT_SECRET'),
expiresIn: '7d',
expiresIn: accessTokenExpiry,
}),
isRefreshToken
? this.jwtService.signAsync(payload, {
secret: this.configService.get<string>('JWT_SECRET'),
expiresIn: refreshTokenExpiry,
})
: null,
]);
return {
accessToken,
refreshToken,
...(isRefreshToken ? { refreshToken } : {}),
};
}
@ -105,6 +115,8 @@ export class AuthService {
googleCode: user.googleCode,
hasAcceptedWebAgreement: user.hasAcceptedWebAgreement,
hasAcceptedAppAgreement: user.hasAcceptedAppAgreement,
project: user?.project,
bookingPoints: user?.bookingPoints,
};
if (payload.googleCode) {
const profile = await this.getProfile(payload.googleCode);

View File

@ -32,6 +32,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
uuid: payload.uuid,
sessionId: payload.sessionId,
role: payload?.role,
project: payload?.project,
};
} else {
throw new BadRequestException('Unauthorized');

View File

@ -35,6 +35,7 @@ export class RefreshTokenStrategy extends PassportStrategy(
uuid: payload.uuid,
sessionId: payload.sessionId,
role: payload?.role,
project: payload?.project,
};
} else {
throw new BadRequestException('Unauthorized');

View File

@ -1,12 +1,11 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ErrorMessageService } from 'src/error-message/error-message.service';
import { AuthModule } from './auth/auth.module';
import { CommonService } from './common.service';
import config from './config';
import { DatabaseModule } from './database/database.module';
import { HelperModule } from './helper/helper.module';
import { AuthModule } from './auth/auth.module';
import { ConfigModule } from '@nestjs/config';
import config from './config';
import { EmailService } from './util/email.service';
import { ErrorMessageService } from 'src/error-message/error-message.service';
import { TuyaService } from './integrations/tuya/services/tuya.service';
import { SceneDeviceRepository } from './modules/scene-device/repositories';
import { SpaceRepository } from './modules/space';
@ -15,6 +14,7 @@ import {
SubspaceModelRepository,
} from './modules/space-model';
import { SubspaceRepository } from './modules/space/repositories/subspace.repository';
import { EmailService } from './util/email/email.service';
@Module({
providers: [
CommonService,

View File

@ -10,6 +10,8 @@ export default registerAs(
SMTP_USER: process.env.SMTP_USER,
SMTP_SENDER: process.env.SMTP_SENDER,
SMTP_PASSWORD: process.env.SMTP_PASSWORD,
BATCH_EMAIL_API_URL: process.env.BATCH_EMAIL_API_URL,
SEND_EMAIL_API_URL: process.env.SEND_EMAIL_API_URL,
MAILTRAP_API_TOKEN: process.env.MAILTRAP_API_TOKEN,
MAILTRAP_INVITATION_TEMPLATE_UUID:
process.env.MAILTRAP_INVITATION_TEMPLATE_UUID,
@ -19,5 +21,11 @@ export default registerAs(
process.env.MAILTRAP_DELETE_USER_TEMPLATE_UUID,
MAILTRAP_EDIT_USER_TEMPLATE_UUID:
process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID,
MAILTRAP_SEND_OTP_TEMPLATE_UUID:
process.env.MAILTRAP_SEND_OTP_TEMPLATE_UUID,
MAILTRAP_SEND_BOOKING_AVAILABILITY_UPDATE_TEMPLATE_UUID:
process.env.MAILTRAP_SEND_BOOKING_AVAILABILITY_UPDATE_TEMPLATE_UUID,
MAILTRAP_SEND_BOOKING_TIMING_UPDATE_TEMPLATE_UUID:
process.env.MAILTRAP_SEND_BOOKING_TIMING_UPDATE_TEMPLATE_UUID,
}),
);

View File

@ -0,0 +1,4 @@
export enum BatchDeviceTypeEnum {
RESET = 'RESET',
COMMAND = 'COMMAND',
}

View File

@ -1,4 +1,17 @@
export class ControllerRoute {
static CLIENT = class {
public static readonly ROUTE = 'client';
static ACTIONS = class {
public static readonly REGISTER_NEW_CLIENT_SUMMARY =
'Register a new client';
public static readonly REGISTER_NEW_CLIENT_DESCRIPTION =
'This endpoint registers a new client in the system.';
public static readonly LOGIN_CLIENT_SUMMARY = 'Login a client';
public static readonly LOGIN_CLIENT_DESCRIPTION =
'This endpoint allows a client to log in to the system.';
};
};
static PROJECT = class {
public static readonly ROUTE = 'projects';
static ACTIONS = class {
@ -30,6 +43,11 @@ export class ControllerRoute {
'Get user by uuid in project';
public static readonly GET_USER_BY_UUID_IN_PROJECT_DESCRIPTION =
'This endpoint retrieves a user by their unique identifier (UUID) associated with a specific project.';
public static readonly EXPORT_STRUCTURE_CSV_SUMMARY =
'Export project with their full structure to a CSV file';
public static readonly EXPORT_STRUCTURE_CSV_DESCRIPTION =
'This endpoint exports project along with their associated communities, spaces, and nested space hierarchy into a downloadable CSV file. Useful for backups, reports, or audits';
};
};
static PROJECT_USER = class {
@ -51,7 +69,47 @@ export class ControllerRoute {
'Retrieve the list of all regions registered in Syncrow.';
};
};
static BOOKABLE_SPACES = class {
public static readonly ROUTE = 'bookable-spaces';
static ACTIONS = class {
public static readonly ADD_BOOKABLE_SPACES_SUMMARY =
'Add new bookable spaces';
public static readonly ADD_BOOKABLE_SPACES_DESCRIPTION =
'This endpoint allows you to add new bookable spaces by providing the required details.';
public static readonly GET_ALL_BOOKABLE_SPACES_SUMMARY =
'Get all bookable spaces';
public static readonly GET_ALL_BOOKABLE_SPACES_DESCRIPTION =
'This endpoint retrieves all bookable spaces.';
public static readonly UPDATE_BOOKABLE_SPACES_SUMMARY =
'Update existing bookable spaces';
public static readonly UPDATE_BOOKABLE_SPACES_DESCRIPTION =
'This endpoint allows you to update existing bookable spaces by providing the required details.';
};
};
static BOOKING = class {
public static readonly ROUTE = 'bookings';
static ACTIONS = class {
public static readonly ADD_BOOKING_SUMMARY = 'Add new booking';
public static readonly ADD_BOOKING_DESCRIPTION =
'This endpoint allows you to add new booking by providing the required details.';
public static readonly GET_ALL_BOOKINGS_SUMMARY = 'Get all bookings';
public static readonly GET_ALL_BOOKINGS_DESCRIPTION =
'This endpoint retrieves all bookings.';
public static readonly GET_MY_BOOKINGS_SUMMARY = 'Get my bookings';
public static readonly GET_MY_BOOKINGS_DESCRIPTION =
'This endpoint retrieves all bookings for the authenticated user.';
};
};
static COMMUNITY = class {
public static readonly ROUTE = '/projects/:projectUuid/communities';
static ACTIONS = class {
@ -181,6 +239,11 @@ export class ControllerRoute {
public static readonly UPDATE_SPACE_DESCRIPTION =
'Updates a space by its UUID and community ID. You can update the name, parent space, and other properties. If a parent space is provided and not already a parent, its `isParent` flag will be set to true.';
public static readonly UPDATE_CHILDREN_SPACES_ORDER_OF_A_SPACE_SUMMARY =
'Update the order of child spaces under a specific parent space';
public static readonly UPDATE_CHILDREN_SPACES_ORDER_OF_A_SPACE_DESCRIPTION =
'Updates the order of child spaces under a specific parent space. You can provide a new order for the child spaces.';
public static readonly GET_HEIRARCHY_SUMMARY = 'Get space hierarchy';
public static readonly GET_HEIRARCHY_DESCRIPTION =
'This endpoint retrieves the hierarchical structure of spaces under a given space ID. It returns all the child spaces nested within the specified space, organized by their parent-child relationships. ';
@ -197,7 +260,16 @@ export class ControllerRoute {
'retrieves all the spaces associated with a given community, organized into a hierarchical structure.';
};
};
static SPACE_VALIDATION = class {
public static readonly ROUTE = '/projects/:projectUuid/spaces';
static ACTIONS = class {
public static readonly VALIDATE_SPACE_WITH_DEVICES_OR_SUBSPACES_SUMMARY =
'Check if a space has devices or sub-spaces';
public static readonly VALIDATE_SPACE_WITH_DEVICES_OR_SUBSPACES_DESCRIPTION =
'Checks if a space has any devices or sub-spaces associated with it.';
};
};
static SPACE_SCENE = class {
public static readonly ROUTE =
'/projects/:projectUuid/communities/:communityUuid/spaces/:spaceUuid/scenes';
@ -292,7 +364,7 @@ export class ControllerRoute {
public static readonly CREATE_SPACE_MODEL_DESCRIPTION =
'This endpoint allows you to create a new space model within a specified project. A space model defines the structure of spaces, including subspaces, products, and product items, and is uniquely identifiable within the project.';
public static readonly GET_SPACE_MODEL_SUMMARY = 'Get a New Space Model';
public static readonly GET_SPACE_MODEL_SUMMARY = 'Get a Space Model';
public static readonly GET_SPACE_MODEL_DESCRIPTION =
'Fetch a space model details';
@ -307,12 +379,20 @@ export class ControllerRoute {
public static readonly DELETE_SPACE_MODEL_SUMMARY = 'Delete Space Model';
public static readonly DELETE_SPACE_MODEL_DESCRIPTION =
'This endpoint allows you to delete a specified Space Model within a project. Deleting a Space Model disables the model and all its associated subspaces and tags, ensuring they are no longer active but remain in the system for auditing.';
public static readonly LINK_SPACE_MODEL_SUMMARY =
'Link a Space Model to spaces';
public static readonly LINK_SPACE_MODEL_DESCRIPTION =
'This endpoint allows you to link a specified Space Model within a project to multiple spaces. Linking a Space Model applies the model and all its associated subspaces and tags, to the spaces.';
};
};
static PRODUCT = class {
public static readonly ROUTE = 'products';
static ACTIONS = class {
public static readonly CREATE_PRODUCT_SUMMARY = 'Create a new product';
public static readonly CREATE_PRODUCT_DESCRIPTION =
'This endpoint allows you to create a new product in the system.';
public static readonly LIST_PRODUCT_SUMMARY = 'Retrieve all products';
public static readonly LIST_PRODUCT_DESCRIPTION =
'Fetches a list of all products along with their associated device details';
@ -320,7 +400,7 @@ export class ControllerRoute {
};
static TAG = class {
public static readonly ROUTE = 'tags';
public static readonly ROUTE = '/projects/:projectUuid/tags';
static ACTIONS = class {
public static readonly CREATE_TAG_SUMMARY = 'Create a new tag';
public static readonly CREATE_TAG_DESCRIPTION =
@ -362,6 +442,11 @@ export class ControllerRoute {
public static readonly DELETE_USER_SUMMARY = 'Delete user by UUID';
public static readonly DELETE_USER_DESCRIPTION =
'This endpoint deletes a user identified by their UUID. Accessible only by users with the Super Admin role.';
public static readonly DELETE_USER_PROFILE_SUMMARY =
'Delete user profile by UUID';
public static readonly DELETE_USER_PROFILE_DESCRIPTION =
'This endpoint deletes a user profile identified by their UUID. Accessible only by users with the Super Admin role.';
public static readonly UPDATE_USER_WEB_AGREEMENT_SUMMARY =
'Update user web agreement by user UUID';
public static readonly UPDATE_USER_WEB_AGREEMENT_DESCRIPTION =
@ -430,7 +515,16 @@ export class ControllerRoute {
'This endpoint retrieves the terms and conditions for the application.';
};
};
static WEATHER = class {
public static readonly ROUTE = 'weather';
static ACTIONS = class {
public static readonly FETCH_WEATHER_DETAILS_SUMMARY =
'Fetch Weather Details';
public static readonly FETCH_WEATHER_DETAILS_DESCRIPTION =
'This endpoint retrieves the current weather details for a specified location like temperature, humidity, etc.';
};
};
static PRIVACY_POLICY = class {
public static readonly ROUTE = 'policy';
@ -455,18 +549,51 @@ export class ControllerRoute {
'This endpoint retrieves all devices in a specified group within a space, based on the group name and space UUID.';
};
};
static DEVICE = class {
public static readonly ROUTE = 'device';
static PowerClamp = class {
public static readonly ROUTE = 'power-clamp';
static ACTIONS = class {
public static readonly GET_ENERGY_SUMMARY =
'Get power clamp historical data';
public static readonly GET_ENERGY_DESCRIPTION =
'This endpoint retrieves the historical data of a power clamp device based on the provided parameters.';
public static readonly GET_ENERGY_BY_COMMUNITY_OR_SPACE_SUMMARY =
'Get power clamp historical data by community or space';
public static readonly GET_ENERGY_BY_COMMUNITY_OR_SPACE_DESCRIPTION =
'This endpoint retrieves the historical data of power clamp devices based on the provided community or space UUID.';
};
};
static Occupancy = class {
public static readonly ROUTE = 'occupancy';
static ACTIONS = class {
public static readonly ADD_DEVICE_TO_USER_SUMMARY = 'Add device to user';
public static readonly ADD_DEVICE_TO_USER_DESCRIPTION =
'This endpoint adds a device to a user in the system.';
public static readonly GET_OCCUPANCY_HEAT_MAP_SUMMARY =
'Get occupancy heat map data';
public static readonly GET_OCCUPANCY_HEAT_MAP_DESCRIPTION =
'This endpoint retrieves the occupancy heat map data based on the provided parameters.';
};
};
static AQI = class {
public static readonly ROUTE = 'aqi';
public static readonly GET_DEVICES_BY_USER_SUMMARY =
'Get devices by user UUID';
public static readonly GET_DEVICES_BY_USER_DESCRIPTION =
'This endpoint retrieves all devices associated with a specific user.';
static ACTIONS = class {
public static readonly GET_AQI_RANGE_DATA_SUMMARY = 'Get AQI range data';
public static readonly GET_AQI_RANGE_DATA_DESCRIPTION =
'This endpoint retrieves the AQI (Air Quality Index) range data based on the provided parameters.';
public static readonly GET_AQI_DISTRIBUTION_DATA_SUMMARY =
'Get AQI distribution data';
public static readonly GET_AQI_DISTRIBUTION_DATA_DESCRIPTION =
'This endpoint retrieves the AQI (Air Quality Index) distribution data based on the provided parameters.';
};
};
static DEVICE = class {
public static readonly ROUTE = 'devices';
static ACTIONS = class {
public static readonly ADD_DEVICE_SUMMARY = 'Add new device';
public static readonly ADD_DEVICE_DESCRIPTION =
'This endpoint adds a new device in the system.';
public static readonly GET_DEVICES_BY_SPACE_UUID_SUMMARY =
'Get devices by space UUID';
@ -538,18 +665,51 @@ export class ControllerRoute {
'This endpoint retrieves the status of a specific power clamp device.';
public static readonly ADD_SCENE_TO_DEVICE_SUMMARY =
'Add scene to device (4 Scene and 6 Scene devices only)';
'Add scene to device';
public static readonly ADD_SCENE_TO_DEVICE_DESCRIPTION =
'This endpoint adds a scene to a specific switch device.';
public static readonly GET_SCENES_BY_DEVICE_SUMMARY =
'Get scenes by device (4 Scene and 6 Scene devices only)';
'Get scenes by device';
public static readonly GET_SCENES_BY_DEVICE_DESCRIPTION =
'This endpoint retrieves all scenes associated with a specific switch device.';
public static readonly DELETE_SCENES_BY_SWITCH_NAME_SUMMARY =
'Delete scenes by device uuid and switch name (4 Scene and 6 Scene devices only)';
'Delete scenes by device uuid and switch name';
public static readonly DELETE_SCENES_BY_SWITCH_NAME_DESCRIPTION =
'This endpoint deletes all scenes associated with a specific switch device.';
public static readonly POPULATE_TUYA_CONST_UUID_SUMMARY =
'Populate Tuya const UUID';
public static readonly POPULATE_TUYA_CONST_UUID_DESCRIPTION =
'This endpoint populates the Tuya const UUID for all devices.';
};
};
static DEVICE_COMMISSION = class {
public static readonly ROUTE = '/projects/:projectUuid/devices/commission';
static ACTIONS = class {
public static readonly ADD_ALL_DEVICES_SUMMARY = 'Add all devices';
public static readonly ADD_ALL_DEVICES_DESCRIPTION =
'This endpoint add all devices in the system from tuya.';
};
};
static DEVICE_PROJECT = class {
public static readonly ROUTE = '/projects/:projectUuid/devices';
static ACTIONS = class {
public static readonly GET_ALL_DEVICES_SUMMARY = 'Get all devices';
public static readonly GET_ALL_DEVICES_DESCRIPTION =
'This endpoint retrieves all devices in the system.';
};
};
static DEVICE_SPACE_COMMUNITY = class {
public static readonly ROUTE = 'devices-space-community';
static ACTIONS = class {
public static readonly GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_SUMMARY =
'Get all devices by space or community with recursive child';
public static readonly GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_DESCRIPTION =
'This endpoint retrieves all devices in the system by space or community with recursive child.';
};
};
@ -601,18 +761,13 @@ export class ControllerRoute {
};
static AUTOMATION = class {
public static readonly ROUTE = 'automation';
public static readonly ROUTE = '/projects/:projectUuid/automations';
static ACTIONS = class {
public static readonly ADD_AUTOMATION_SUMMARY = 'Add automation';
public static readonly ADD_AUTOMATION_DESCRIPTION =
'This endpoint creates a new automation based on the provided details.';
public static readonly GET_AUTOMATION_BY_SPACE_SUMMARY =
'Get automation by space';
public static readonly GET_AUTOMATION_BY_SPACE_DESCRIPTION =
'This endpoint retrieves the automations associated with a particular space.';
public static readonly GET_AUTOMATION_DETAILS_SUMMARY =
'Get automation details';
public static readonly GET_AUTOMATION_DETAILS_DESCRIPTION =
@ -633,6 +788,17 @@ export class ControllerRoute {
};
};
static AUTOMATION_SPACE = class {
public static readonly ROUTE =
'/projects/:projectUuid/communities/:communityUuid/spaces/:spaceUuid/automations';
static ACTIONS = class {
public static readonly GET_AUTOMATION_BY_SPACE_SUMMARY =
'Get automation by space';
public static readonly GET_AUTOMATION_BY_SPACE_DESCRIPTION =
'This endpoint retrieves the automations associated with a particular space.';
};
};
static DOOR_LOCK = class {
public static readonly ROUTE = 'door-lock';
@ -692,29 +858,15 @@ export class ControllerRoute {
};
};
static VISITOR_PASSWORD = class {
public static readonly ROUTE = 'visitor-password';
public static readonly ROUTE = 'visitor-passwords';
public static readonly PROJECT_ROUTE =
'/projects/:projectUuid/visitor-password';
static ACTIONS = class {
public static readonly ADD_ONLINE_TEMP_PASSWORD_MULTIPLE_TIME_SUMMARY =
'Add online temporary passwords (multiple-time)';
public static readonly ADD_ONLINE_TEMP_PASSWORD_MULTIPLE_TIME_DESCRIPTION =
'This endpoint adds multiple online temporary passwords for door locks.';
public static readonly ADD_ONLINE_TEMP_PASSWORD_ONE_TIME_SUMMARY =
'Add online temporary password (one-time)';
public static readonly ADD_ONLINE_TEMP_PASSWORD_ONE_TIME_DESCRIPTION =
'This endpoint adds a one-time online temporary password for a door lock.';
public static readonly ADD_OFFLINE_TEMP_PASSWORD_ONE_TIME_SUMMARY =
'Add offline temporary password (one-time)';
public static readonly ADD_OFFLINE_TEMP_PASSWORD_ONE_TIME_DESCRIPTION =
'This endpoint adds a one-time offline temporary password for a door lock.';
public static readonly ADD_OFFLINE_TEMP_PASSWORD_MULTIPLE_TIME_SUMMARY =
'Add offline temporary passwords (multiple-time)';
public static readonly ADD_OFFLINE_TEMP_PASSWORD_MULTIPLE_TIME_DESCRIPTION =
'This endpoint adds multiple offline temporary passwords for door locks.';
public static readonly ADD_VISITOR_PASSWORD_SUMMARY =
'Add visitor password';
public static readonly ADD_VISITOR_PASSWORD_DESCRIPTION =
'This endpoint allows you to add a visitor password based on the operation type.';
public static readonly GET_VISITOR_PASSWORD_SUMMARY =
'Get visitor passwords';
public static readonly GET_VISITOR_PASSWORD_DESCRIPTION =

View File

@ -0,0 +1,3 @@
export enum DeviceTypeEnum {
DOOR_LOCK = 'DOOR_LOCK',
}

View File

@ -1,3 +0,0 @@
export const SEND_EMAIL_API_URL_PROD = 'https://send.api.mailtrap.io/api/send/';
export const SEND_EMAIL_API_URL_DEV =
'https://sandbox.api.mailtrap.io/api/send/2634012';

View File

@ -0,0 +1,8 @@
export enum PollutantType {
AQI = 'aqi',
PM25 = 'pm25',
PM10 = 'pm10',
VOC = 'voc',
CO2 = 'co2',
CH2O = 'ch2o',
}

View File

@ -0,0 +1,6 @@
export enum PowerClampEnergyEnum {
ENERGY_CONSUMED = 'EnergyConsumed',
ENERGY_CONSUMED_A = 'EnergyConsumedA',
ENERGY_CONSUMED_B = 'EnergyConsumedB',
ENERGY_CONSUMED_C = 'EnergyConsumedC',
}

View File

@ -0,0 +1,4 @@
export enum PresenceSensorEnum {
PRESENCE_STATE = 'presence_state',
SENSITIVITY = 'sensitivity',
}

View File

@ -15,8 +15,10 @@ export enum ProductType {
WL = 'WL',
GD = 'GD',
CUR = 'CUR',
CUR_2 = 'CUR_2',
PC = 'PC',
FOUR_S = '4S',
SIX_S = '6S',
SOS = 'SOS',
AQI = 'AQI',
}

View File

@ -3,12 +3,14 @@ import { RoleType } from './role.type.enum';
export const RolePermissions = {
[RoleType.SUPER_ADMIN]: [
'DEVICE_SINGLE_CONTROL',
'COMMISSION_DEVICE',
'DEVICE_VIEW',
'DEVICE_DELETE',
'DEVICE_UPDATE',
'DEVICE_BATCH_CONTROL',
'DEVICE_LOCATION_VIEW',
'DEVICE_LOCATION_UPDATE',
'DEVICE_ADD',
'COMMUNITY_VIEW',
'COMMUNITY_ADD',
'COMMUNITY_UPDATE',
@ -23,6 +25,7 @@ export const RolePermissions = {
'SPACE_MODEL_VIEW',
'SPACE_MODEL_UPDATE',
'SPACE_MODEL_DELETE',
'SPACE_MODEL_LINK',
'SPACE_ASSIGN_USER_TO_SPACE',
'SPACE_DELETE_USER_FROM_SPACE',
'SUBSPACE_VIEW',
@ -52,12 +55,16 @@ export const RolePermissions = {
'VISITOR_PASSWORD_DELETE',
'USER_ADD',
'SPACE_MEMBER_ADD',
'COMMISSION_DEVICE',
'PRODUCT_ADD',
],
[RoleType.ADMIN]: [
'COMMISSION_DEVICE',
'DEVICE_SINGLE_CONTROL',
'DEVICE_VIEW',
'DEVICE_DELETE',
'DEVICE_UPDATE',
'DEVICE_ADD',
'DEVICE_BATCH_CONTROL',
'DEVICE_LOCATION_VIEW',
'DEVICE_LOCATION_UPDATE',
@ -75,6 +82,7 @@ export const RolePermissions = {
'SPACE_MODEL_VIEW',
'SPACE_MODEL_UPDATE',
'SPACE_MODEL_DELETE',
'SPACE_MODEL_LINK',
'SPACE_ASSIGN_USER_TO_SPACE',
'SPACE_DELETE_USER_FROM_SPACE',
'SUBSPACE_VIEW',
@ -104,6 +112,8 @@ export const RolePermissions = {
'VISITOR_PASSWORD_DELETE',
'USER_ADD',
'SPACE_MEMBER_ADD',
'COMMISSION_DEVICE',
'PRODUCT_ADD',
],
[RoleType.SPACE_MEMBER]: [
'DEVICE_SINGLE_CONTROL',
@ -119,6 +129,7 @@ export const RolePermissions = {
'SCENES_CONTROL',
],
[RoleType.SPACE_OWNER]: [
'COMMISSION_DEVICE',
'DEVICE_SINGLE_CONTROL',
'DEVICE_VIEW',
'DEVICE_DELETE',
@ -161,5 +172,6 @@ export const RolePermissions = {
'VISITOR_PASSWORD_DELETE',
'USER_ADD',
'SPACE_MEMBER_ADD',
'COMMISSION_DEVICE',
],
};

View File

@ -0,0 +1,2 @@
export const SQL_QUERIES_PATH = 'libs/common/src/sql/queries';
export const SQL_PROCEDURES_PATH = 'libs/common/src/sql/procedures';

View File

@ -0,0 +1,6 @@
export enum VisitorPasswordEnum {
ONLINE_ONE_TIME = 'ONLINE_ONE_TIME',
ONLINE_MULTIPLE_TIME = 'ONLINE_MULTIPLE_TIME',
OFFLINE_ONE_TIME = 'OFFLINE_ONE_TIME',
OFFLINE_MULTIPLE_TIME = 'OFFLINE_MULTIPLE_TIME',
}

View File

@ -0,0 +1,8 @@
import { AsyncLocalStorage } from 'async_hooks';
export interface RequestContextStore {
requestId?: string;
userId?: string;
}
export const requestContext = new AsyncLocalStorage<RequestContextStore>();

View File

@ -1,113 +1,145 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SnakeNamingStrategy } from './strategies';
import { UserEntity } from '../modules/user/entities/user.entity';
import { UserSessionEntity } from '../modules/session/entities/session.entity';
import { UserOtpEntity } from '../modules/user/entities';
import { ProductEntity } from '../modules/product/entities';
import { DeviceEntity } from '../modules/device/entities';
import { PermissionTypeEntity } from '../modules/permission/entities';
import { ProductEntity } from '../modules/product/entities';
import { UserSessionEntity } from '../modules/session/entities/session.entity';
import { UserOtpEntity } from '../modules/user/entities';
import { UserEntity } from '../modules/user/entities/user.entity';
import { SnakeNamingStrategy } from './strategies';
import { UserSpaceEntity } from '../modules/user/entities';
import { DeviceUserPermissionEntity } from '../modules/device/entities';
import { RoleTypeEntity } from '../modules/role-type/entities';
import { UserNotificationEntity } from '../modules/user/entities';
import { DeviceNotificationEntity } from '../modules/device/entities';
import { RegionEntity } from '../modules/region/entities';
import { TimeZoneEntity } from '../modules/timezone/entities';
import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
import { TypeOrmWinstonLogger } from '@app/common/logger/services/typeorm.logger';
import { createLogger } from 'winston';
import { winstonLoggerOptions } from '../logger/services/winston.logger';
import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities';
import { AutomationEntity } from '../modules/automation/entities';
import { BookableSpaceEntity } from '../modules/booking/entities/bookable-space.entity';
import { BookingEntity } from '../modules/booking/entities/booking.entity';
import { ClientEntity } from '../modules/client/entities';
import { CommunityEntity } from '../modules/community/entities';
import { DeviceStatusLogEntity } from '../modules/device-status-log/entities';
import { SceneEntity, SceneIconEntity } from '../modules/scene/entities';
import { SceneDeviceEntity } from '../modules/scene-device/entities';
import { ProjectEntity } from '../modules/project/entities';
import {
SpaceModelEntity,
SubspaceModelEntity,
TagModel,
SpaceModelProductAllocationEntity,
SubspaceModelProductAllocationEntity,
} from '../modules/space-model/entities';
DeviceNotificationEntity,
DeviceUserPermissionEntity,
} from '../modules/device/entities';
import {
InviteUserEntity,
InviteUserSpaceEntity,
} from '../modules/Invite-user/entities';
import { SpaceDailyOccupancyDurationEntity } from '../modules/occupancy/entities';
import {
PowerClampDailyEntity,
PowerClampHourlyEntity,
PowerClampMonthlyEntity,
} from '../modules/power-clamp/entities/power-clamp.entity';
import {
PresenceSensorDailyDeviceEntity,
PresenceSensorDailySpaceEntity,
} from '../modules/presence-sensor/entities';
import { ProjectEntity } from '../modules/project/entities';
import { RegionEntity } from '../modules/region/entities';
import { RoleTypeEntity } from '../modules/role-type/entities';
import { SceneDeviceEntity } from '../modules/scene-device/entities';
import { SceneEntity, SceneIconEntity } from '../modules/scene/entities';
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SubspaceModelEntity,
SubspaceModelProductAllocationEntity,
} from '../modules/space-model/entities';
import { InviteSpaceEntity } from '../modules/space/entities/invite-space.entity';
import { AutomationEntity } from '../modules/automation/entities';
import { SpaceProductAllocationEntity } from '../modules/space/entities/space-product-allocation.entity';
import { NewTagEntity } from '../modules/tag/entities/tag.entity';
import { SpaceEntity } from '../modules/space/entities/space.entity';
import { SpaceLinkEntity } from '../modules/space/entities/space-link.entity';
import { SubspaceProductAllocationEntity } from '../modules/space/entities/subspace/subspace-product-allocation.entity';
import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity';
import { TagEntity } from '../modules/space/entities/tag.entity';
import { NewTagEntity } from '../modules/tag/entities/tag.entity';
import { TimeZoneEntity } from '../modules/timezone/entities';
import {
UserNotificationEntity,
UserSpaceEntity,
} from '../modules/user/entities';
import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
name: 'default',
type: 'postgres',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USER'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
entities: [
NewTagEntity,
ProjectEntity,
UserEntity,
UserSessionEntity,
UserOtpEntity,
ProductEntity,
DeviceUserPermissionEntity,
DeviceEntity,
PermissionTypeEntity,
CommunityEntity,
SpaceEntity,
SpaceLinkEntity,
SubspaceEntity,
TagEntity,
UserSpaceEntity,
DeviceUserPermissionEntity,
RoleTypeEntity,
UserNotificationEntity,
DeviceNotificationEntity,
RegionEntity,
TimeZoneEntity,
VisitorPasswordEntity,
DeviceStatusLogEntity,
SceneEntity,
SceneIconEntity,
SceneDeviceEntity,
SpaceModelEntity,
SubspaceModelEntity,
TagModel,
InviteUserEntity,
InviteUserSpaceEntity,
InviteSpaceEntity,
AutomationEntity,
SpaceModelProductAllocationEntity,
SubspaceModelProductAllocationEntity,
SpaceProductAllocationEntity,
SubspaceProductAllocationEntity,
],
namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
logging: false,
extra: {
charset: 'utf8mb4',
max: 20, // set pool max size
idleTimeoutMillis: 5000, // close idle clients after 5 second
connectionTimeoutMillis: 11_000, // return an error after 11 second if connection could not be established
maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion)
},
continuationLocalStorage: true,
ssl: Boolean(JSON.parse(configService.get('DB_SSL'))),
}),
useFactory: (configService: ConfigService) => {
const winstonLogger = createLogger(winstonLoggerOptions);
const typeOrmLogger = new TypeOrmWinstonLogger(winstonLogger);
return {
name: 'default',
type: 'postgres',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USER'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
entities: [
NewTagEntity,
ProjectEntity,
UserEntity,
UserSessionEntity,
UserOtpEntity,
ProductEntity,
DeviceUserPermissionEntity,
DeviceEntity,
PermissionTypeEntity,
CommunityEntity,
SpaceEntity,
SubspaceEntity,
UserSpaceEntity,
DeviceUserPermissionEntity,
RoleTypeEntity,
UserNotificationEntity,
DeviceNotificationEntity,
RegionEntity,
TimeZoneEntity,
VisitorPasswordEntity,
DeviceStatusLogEntity,
SceneEntity,
SceneIconEntity,
SceneDeviceEntity,
SpaceModelEntity,
SubspaceModelEntity,
InviteUserEntity,
InviteUserSpaceEntity,
InviteSpaceEntity,
AutomationEntity,
SpaceModelProductAllocationEntity,
SubspaceModelProductAllocationEntity,
SpaceProductAllocationEntity,
SubspaceProductAllocationEntity,
ClientEntity,
PowerClampHourlyEntity,
PowerClampDailyEntity,
PowerClampMonthlyEntity,
PresenceSensorDailyDeviceEntity,
PresenceSensorDailySpaceEntity,
AqiSpaceDailyPollutantStatsEntity,
SpaceDailyOccupancyDurationEntity,
BookableSpaceEntity,
BookingEntity,
],
namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
logging: ['query', 'error', 'warn', 'schema', 'migration'],
logger: typeOrmLogger,
extra: {
charset: 'utf8mb4',
max: 100, // set pool max size
idleTimeoutMillis: 3000, // close idle clients after 5 second
connectionTimeoutMillis: 12_000, // return an error after 11 second if connection could not be established
maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion)
},
continuationLocalStorage: true,
ssl: Boolean(JSON.parse(configService.get('DB_SSL'))),
};
},
}),
],
providers: [TypeOrmWinstonLogger],
})
export class DatabaseModule {}

View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { ProjectParam } from './project-param.dto';
import { IsUUID } from 'class-validator';
export class CommunityParam extends ProjectParam {
@ApiProperty({
description: 'UUID of the community this space belongs to',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
communityUuid: string;
}

View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
import { CommunityParam } from './community-space.param';
export class GetSpaceParam extends CommunityParam {
@ApiProperty({
description: 'UUID of the Space',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
spaceUuid: string;
}

View File

@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional } from 'class-validator';
import { PaginationRequestGetListDto } from './pagination.request.dto';
export class PaginationRequestWithSearchGetListDto extends PaginationRequestGetListDto {
@IsOptional()
@ApiProperty({
name: 'search',
required: false,
description: 'Search for community or space name',
})
search?: string;
}

View File

@ -1,11 +1,24 @@
import { IsDate, IsOptional } from 'class-validator';
import { IsPageRequestParam } from '../validators/is-page-request-param.validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsSizeRequestParam } from '../validators/is-size-request-param.validator';
import { Transform } from 'class-transformer';
import { parseToDate } from '../util/parseToDate';
import { IsBoolean, IsOptional } from 'class-validator';
import { BooleanValues } from '../constants/boolean-values.enum';
import { IsPageRequestParam } from '../validators/is-page-request-param.validator';
import { IsSizeRequestParam } from '../validators/is-size-request-param.validator';
export class PaginationRequestGetListDto {
@ApiProperty({
example: true,
description: 'include spaces',
required: false,
default: false,
})
@IsOptional()
@IsBoolean()
@Transform((value) => {
return value.obj.includeSpaces === BooleanValues.TRUE;
})
public includeSpaces?: boolean = false;
@IsOptional()
@IsPageRequestParam({
message: 'Page must be bigger than 0',
@ -27,40 +40,4 @@ export class PaginationRequestGetListDto {
description: 'Size request',
})
size?: number;
@IsOptional()
@ApiProperty({
name: 'name',
required: false,
description: 'Name to be filtered',
})
name?: string;
@ApiProperty({
name: 'from',
required: false,
type: Number,
description: `Start time in UNIX timestamp format to filter`,
example: 1674172800000,
})
@IsOptional()
@Transform(({ value }) => parseToDate(value))
@IsDate({
message: `From must be in UNIX timestamp format in order to parse to Date instance`,
})
from?: Date;
@ApiProperty({
name: 'to',
required: false,
type: Number,
description: `End time in UNIX timestamp format to filter`,
example: 1674259200000,
})
@IsOptional()
@Transform(({ value }) => parseToDate(value))
@IsDate({
message: `To must be in UNIX timestamp format in order to parse to Date instance`,
})
to?: Date;
}

View File

@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
export class GetTagsParam {
export class ProjectParam {
@ApiProperty({
description: 'UUID of the Project',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',

View File

@ -13,6 +13,7 @@ class StatusDto {
@IsNotEmpty()
value: any;
t?: string | number | Date;
}
export class AddDeviceStatusDto {

View File

@ -1,21 +1,29 @@
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 { TuyaContext } from '@tuya/tuya-connector-nodejs';
import {
Database,
DataSnapshot,
get,
ref,
runTransaction,
} from 'firebase/database';
import { GetDeviceDetailsFunctionsStatusInterface } from 'src/device/interfaces/get.device.interface';
import { firebaseDataBase } from '../../firebase.config';
import { Database, DataSnapshot, get, ref, set } from 'firebase/database';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import { AddDeviceStatusDto } from '../dtos/add.devices-status.dto';
@Injectable()
export class DeviceStatusFirebaseService {
private tuya: TuyaContext;
private firebaseDb: Database;
private readonly isDevEnv: boolean;
constructor(
private readonly configService: ConfigService,
private readonly deviceRepository: DeviceRepository,
@ -32,6 +40,8 @@ export class DeviceStatusFirebaseService {
// Initialize firebaseDb using firebaseDataBase function
this.firebaseDb = firebaseDataBase(this.configService);
this.isDevEnv =
this.configService.get<string>('NODE_ENV') === 'development';
}
async addDeviceStatusByDeviceUuid(
deviceTuyaUuid: string,
@ -46,7 +56,7 @@ export class DeviceStatusFirebaseService {
const deviceStatusSaved = await this.createDeviceStatusFirebase({
deviceUuid: device.uuid,
deviceTuyaUuid: deviceTuyaUuid,
status: deviceStatus.status,
status: deviceStatus?.status,
productUuid: deviceStatus.productUuid,
productType: deviceStatus.productType,
});
@ -61,24 +71,107 @@ export class DeviceStatusFirebaseService {
);
}
}
async addDeviceStatusToFirebase(
addDeviceStatusDto: AddDeviceStatusDto,
): Promise<AddDeviceStatusDto | null> {
try {
const device = await this.getDeviceByDeviceTuyaUuid(
addDeviceStatusDto.deviceTuyaUuid,
async addBatchDeviceStatusToOurDb(
batch: {
deviceTuyaUuid: string;
status: any;
log: any;
device: any;
}[],
): Promise<void> {
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;
}
// 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: device.productDevice?.uuid,
log: item.log,
code: property.code,
value: property.value,
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 chunkSize = 300;
let insertedCount = 0;
for (let i = 0; i < allLogs.length; i += chunkSize) {
const chunk = allLogs.slice(i, i + chunkSize);
try {
const result = await this.deviceStatusLogRepository
.createQueryBuilder()
.insert()
.into('device-status-log')
.values(chunk)
.orIgnore()
.execute();
insertedCount += result.identifiers.length;
console.log(
`✅ Inserted ${result.identifiers.length} / ${chunk.length} logs (chunk)`,
);
} catch (error) {
console.error('❌ Insert error (skipped chunk):', error.message);
}
}
console.log(`✅ Total logs inserted: ${insertedCount} / ${allLogs.length}`);
}
async addDeviceStatusToFirebase(
addDeviceStatusDto: AddDeviceStatusDto & { device?: any },
): Promise<AddDeviceStatusDto | null> {
try {
let device = addDeviceStatusDto.device;
if (!device) {
device = await this.getDeviceByDeviceTuyaUuid(
addDeviceStatusDto.deviceTuyaUuid,
);
}
if (device?.uuid) {
return await this.createDeviceStatusFirebase({
deviceUuid: device.uuid,
...addDeviceStatusDto,
productType: device.productDevice?.prodType,
});
}
// Return null if device not found or no UUID
return null;
} catch (error) {
// Handle the error silently, perhaps log it internally or ignore it
return null;
}
}
@ -92,6 +185,15 @@ export class DeviceStatusFirebaseService {
relations: ['productDevice'],
});
}
async getAllDevices() {
return await this.deviceRepository.find({
where: {
isActive: true,
},
relations: ['productDevice'],
});
}
async getDevicesInstructionStatus(deviceUuid: string) {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid);
@ -106,7 +208,7 @@ export class DeviceStatusFirebaseService {
return {
productUuid: deviceDetails.productDevice.uuid,
productType: deviceDetails.productDevice.prodType,
status: deviceStatus.result[0].status,
status: deviceStatus.result[0]?.status,
};
} catch (error) {
throw new HttpException(
@ -154,56 +256,49 @@ export class DeviceStatusFirebaseService {
this.firebaseDb,
`device-status/${addDeviceStatusDto.deviceUuid}`,
);
const snapshot: DataSnapshot = await get(dataRef);
const existingData = snapshot.val() || {};
// Assign default values if fields are not present
if (!existingData.deviceTuyaUuid) {
existingData.deviceTuyaUuid = addDeviceStatusDto.deviceTuyaUuid;
}
if (!existingData.productUuid) {
existingData.productUuid = addDeviceStatusDto.productUuid;
}
if (!existingData.productType) {
existingData.productType = addDeviceStatusDto.productType;
}
if (!existingData.status) {
existingData.status = [];
}
// Use a transaction to handle concurrent updates
await runTransaction(dataRef, (existingData) => {
if (!existingData) {
existingData = {};
}
// Create a map to track existing status codes
const statusMap = new Map(
existingData.status.map((item) => [item.code, item.value]),
);
// Assign default values if fields are not present
if (!existingData.deviceTuyaUuid) {
existingData.deviceTuyaUuid = addDeviceStatusDto.deviceTuyaUuid;
}
if (!existingData.productUuid) {
existingData.productUuid = addDeviceStatusDto.productUuid;
}
if (!existingData.productType) {
existingData.productType = addDeviceStatusDto.productType;
}
if (!existingData?.status) {
existingData.status = [];
}
// Update or add status codes
// Create a map to track existing status codes
const statusMap = new Map(
existingData?.status.map((item) => [item.code, item.value]),
);
for (const statusItem of addDeviceStatusDto.status) {
statusMap.set(statusItem.code, statusItem.value);
}
// Update or add status codes
// Convert the map back to an array format
existingData.status = Array.from(statusMap, ([code, value]) => ({
code,
value,
}));
const newLogs = addDeviceStatusDto.log.properties.map((property) => {
return this.deviceStatusLogRepository.create({
deviceId: addDeviceStatusDto.deviceUuid,
deviceTuyaId: addDeviceStatusDto.deviceTuyaUuid,
productId: addDeviceStatusDto.log.productId,
log: addDeviceStatusDto.log,
code: property.code,
value: property.value,
eventId: addDeviceStatusDto.log.dataId,
eventTime: new Date(property.time).toISOString(),
});
for (const statusItem of addDeviceStatusDto?.status) {
statusMap.set(statusItem.code, statusItem.value);
}
// Convert the map back to an array format
existingData.status = Array.from(statusMap, ([code, value]) => ({
code,
value,
}));
return existingData;
});
await this.deviceStatusLogRepository.save(newLogs);
// Save the updated data to Firebase
await set(dataRef, existingData);
// Return the updated data
return existingData;
const snapshot: DataSnapshot = await get(dataRef);
return snapshot.val();
}
}

View File

@ -3,21 +3,32 @@ import { getDatabase } from 'firebase/database';
import { ConfigService } from '@nestjs/config';
export const initializeFirebaseApp = (configService: ConfigService) => {
const firebaseConfig = {
apiKey: configService.get<string>('FIREBASE_API_KEY'),
authDomain: configService.get<string>('FIREBASE_AUTH_DOMAIN'),
projectId: configService.get<string>('FIREBASE_PROJECT_ID'),
storageBucket: configService.get<string>('FIREBASE_STORAGE_BUCKET'),
messagingSenderId: configService.get<string>(
'FIREBASE_MESSAGING_SENDER_ID',
),
appId: configService.get<string>('FIREBASE_APP_ID'),
measurementId: configService.get<string>('FIREBASE_MEASUREMENT_ID'),
databaseURL: configService.get<string>('FIREBASE_DATABASE_URL'),
};
try {
const firebaseConfig = {
apiKey: configService.get<string>('FIREBASE_API_KEY'),
authDomain: configService.get<string>('FIREBASE_AUTH_DOMAIN'),
projectId: configService.get<string>('FIREBASE_PROJECT_ID'),
storageBucket: configService.get<string>('FIREBASE_STORAGE_BUCKET'),
messagingSenderId: configService.get<string>(
'FIREBASE_MESSAGING_SENDER_ID',
),
appId: configService.get<string>('FIREBASE_APP_ID'),
measurementId: configService.get<string>('FIREBASE_MEASUREMENT_ID'),
databaseURL: configService.get<string>('FIREBASE_DATABASE_URL'),
};
const app = initializeApp(firebaseConfig);
return getDatabase(app);
// Check if required Firebase config is available
if (!firebaseConfig.projectId || firebaseConfig.projectId === 'placeholder-project') {
console.warn('Firebase configuration not available, Firebase features will be disabled');
return null;
}
const app = initializeApp(firebaseConfig);
return getDatabase(app);
} catch (error) {
console.warn('Firebase initialization failed, Firebase features will be disabled:', error.message);
return null;
}
};
export const firebaseDataBase = (configService: ConfigService) =>

View File

@ -0,0 +1,38 @@
export function toDDMMYYYY(dateString?: string | null): string | null {
if (!dateString) return null;
// Ensure dateString is valid format YYYY-MM-DD
const regex = /^\d{4}-\d{2}-\d{2}$/;
if (!regex.test(dateString)) {
throw new Error(
`Invalid date format: ${dateString}. Expected format is YYYY-MM-DD`,
);
}
const [year, month, day] = dateString.split('-');
return `${day}-${month}-${year}`;
}
export function toMMYYYY(dateString?: string | null): string | null {
if (!dateString) return null;
// Ensure dateString is valid format YYYY-MM
const regex = /^\d{4}-\d{2}$/;
if (!regex.test(dateString)) {
throw new Error(
`Invalid date format: ${dateString}. Expected format is YYYY-MM`,
);
}
const [year, month] = dateString.split('-');
return `${month}-${year}`;
}
export function filterByMonth(data: any[], monthDate: string) {
const [year, month] = monthDate.split('-').map(Number);
return data.filter((item) => {
const itemDate = new Date(item.date);
return (
itemDate.getUTCFullYear() === year && itemDate.getUTCMonth() + 1 === month
);
});
}

View File

@ -8,10 +8,14 @@ import { TuyaWebSocketService } from './services/tuya.web.socket.service';
import { OneSignalService } from './services/onesignal.service';
import { DeviceMessagesService } from './services/device.messages.service';
import { DeviceRepositoryModule } from '../modules/device/device.repository.module';
import { DeviceNotificationRepository } from '../modules/device/repositories';
import {
DeviceNotificationRepository,
DeviceRepository,
} from '../modules/device/repositories';
import { DeviceStatusFirebaseModule } from '../firebase/devices-status/devices-status.module';
import { CommunityPermissionService } from './services/community.permission.service';
import { CommunityRepository } from '../modules/community/repositories';
import { SosHandlerService } from './services/sos.handler.service';
@Global()
@Module({
@ -25,6 +29,8 @@ import { CommunityRepository } from '../modules/community/repositories';
DeviceMessagesService,
DeviceNotificationRepository,
CommunityRepository,
SosHandlerService,
DeviceRepository,
],
exports: [
HelperHashService,

View File

@ -0,0 +1,66 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
import { SqlLoaderService } from './sql-loader.service';
@Injectable()
export class AqiDataService {
constructor(
private readonly sqlLoader: SqlLoaderService,
private readonly dataSource: DataSource,
) {}
async updateAQISensorHistoricalData(): Promise<void> {
try {
const { dateStr } = this.getFormattedDates();
// Execute all procedures in parallel
await Promise.all([
this.executeProcedureWithRetry(
'proceduce_update_daily_space_aqi',
[dateStr],
'fact_daily_space_aqi',
),
]);
} catch (err) {
console.error('Failed to update AQI sensor historical data:', err);
throw err;
}
}
private getFormattedDates(): { dateStr: string } {
const now = new Date();
return {
dateStr: now.toLocaleDateString('en-CA'), // YYYY-MM-DD
};
}
private async executeProcedureWithRetry(
procedureFileName: string,
params: (string | number | null)[],
folderName: string,
retries = 3,
): Promise<void> {
try {
const query = this.loadQuery(folderName, procedureFileName);
await this.dataSource.query(query, params);
console.log(`Procedure ${procedureFileName} executed successfully.`);
} catch (err) {
if (retries > 0) {
const delayMs = 1000 * (4 - retries); // Exponential backoff
console.warn(`Retrying ${procedureFileName} (${retries} retries left)`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
return this.executeProcedureWithRetry(
procedureFileName,
params,
folderName,
retries - 1,
);
}
console.error(`Failed to execute ${procedureFileName}:`, err);
throw err;
}
}
private loadQuery(folderName: string, fileName: string): string {
return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH);
}
}

View File

@ -0,0 +1,71 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
import { SqlLoaderService } from './sql-loader.service';
@Injectable()
export class OccupancyService {
constructor(
private readonly sqlLoader: SqlLoaderService,
private readonly dataSource: DataSource,
) {}
async updateOccupancyDataProcedures(): Promise<void> {
try {
const { dateStr } = this.getFormattedDates();
// Execute all procedures in parallel
await Promise.all([
this.executeProcedureWithRetry(
'procedure_update_fact_space_occupancy',
[dateStr],
'fact_space_occupancy_count',
),
this.executeProcedureWithRetry(
'procedure_update_daily_space_occupancy_duration',
[dateStr],
'fact_daily_space_occupancy_duration',
),
]);
} catch (err) {
console.error('Failed to update occupancy data:', err);
throw err;
}
}
private getFormattedDates(): { dateStr: string } {
const now = new Date();
return {
dateStr: now.toLocaleDateString('en-CA'), // YYYY-MM-DD
};
}
private async executeProcedureWithRetry(
procedureFileName: string,
params: (string | number | null)[],
folderName: string,
retries = 3,
): Promise<void> {
try {
const query = this.loadQuery(folderName, procedureFileName);
await this.dataSource.query(query, params);
console.log(`Procedure ${procedureFileName} executed successfully.`);
} catch (err) {
if (retries > 0) {
const delayMs = 1000 * (4 - retries); // Exponential backoff
console.warn(`Retrying ${procedureFileName} (${retries} retries left)`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
return this.executeProcedureWithRetry(
procedureFileName,
params,
folderName,
retries - 1,
);
}
console.error(`Failed to execute ${procedureFileName}:`, err);
throw err;
}
}
private loadQuery(folderName: string, fileName: string): string {
return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH);
}
}

View File

@ -0,0 +1,84 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
import { SqlLoaderService } from './sql-loader.service';
@Injectable()
export class PowerClampService {
constructor(
private readonly sqlLoader: SqlLoaderService,
private readonly dataSource: DataSource,
) {}
async updateEnergyConsumedHistoricalData(): Promise<void> {
try {
const { dateStr, monthYear } = this.getFormattedDates();
// Execute all procedures in parallel
await Promise.all([
this.executeProcedureWithRetry(
'fact_hourly_device_energy_consumed_procedure',
[dateStr],
'fact_device_energy_consumed',
),
this.executeProcedureWithRetry(
'fact_daily_device_energy_consumed_procedure',
[dateStr],
'fact_device_energy_consumed',
),
this.executeProcedureWithRetry(
'fact_monthly_device_energy_consumed_procedure',
[monthYear],
'fact_device_energy_consumed',
),
]);
} catch (err) {
console.error('Failed to update energy consumption data:', err);
throw err;
}
}
private getFormattedDates(): { dateStr: string; monthYear: string } {
const now = new Date();
return {
dateStr: now.toLocaleDateString('en-CA'), // YYYY-MM-DD
monthYear: now
.toLocaleDateString('en-US', {
month: '2-digit',
year: 'numeric',
})
.replace('/', '-'), // MM-YYYY
};
}
private async executeProcedureWithRetry(
procedureFileName: string,
params: (string | number | null)[],
folderName: string,
retries = 3,
): Promise<void> {
try {
const query = this.loadQuery(folderName, procedureFileName);
await this.dataSource.query(query, params);
console.log(`Procedure ${procedureFileName} executed successfully.`);
} catch (err) {
if (retries > 0) {
const delayMs = 1000 * (4 - retries); // Exponential backoff
console.warn(`Retrying ${procedureFileName} (${retries} retries left)`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
return this.executeProcedureWithRetry(
procedureFileName,
params,
folderName,
retries - 1,
);
}
console.error(`Failed to execute ${procedureFileName}:`, err);
throw err;
}
}
private loadQuery(folderName: string, fileName: string): string {
return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH);
}
}

View File

@ -0,0 +1,67 @@
import { Injectable, Logger } from '@nestjs/common';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
@Injectable()
export class SosHandlerService {
private readonly logger = new Logger(SosHandlerService.name);
constructor(
private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService,
) {}
isSosTriggered(status: any): boolean {
return (
Array.isArray(status) &&
status.some((item) => item.code === 'sos' && item.value === 'sos')
);
}
async handleSosEventFirebase(device: any, logData: any): Promise<void> {
const sosTrueStatus = [{ code: 'sos', value: true }];
const sosFalseStatus = [{ code: 'sos', value: false }];
try {
// ✅ Send true status
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({
deviceTuyaUuid: device.deviceTuyaUuid,
status: sosTrueStatus,
log: logData,
device,
});
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb([
{
deviceTuyaUuid: device.deviceTuyaUuid,
status: sosTrueStatus,
log: logData,
device,
},
]);
// ✅ Schedule false status
setTimeout(async () => {
try {
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({
deviceTuyaUuid: device.deviceTuyaUuid,
status: sosFalseStatus,
log: logData,
device,
});
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb([
{
deviceTuyaUuid: device.deviceTuyaUuid,
status: sosFalseStatus,
log: logData,
device,
},
]);
} catch (err) {
this.logger.error('Failed to send SOS false value', err);
}
}, 2000);
} catch (err) {
this.logger.error('Failed to send SOS true value', err);
}
}
}

View File

@ -0,0 +1,22 @@
import { Injectable, Logger } from '@nestjs/common';
import { readFileSync } from 'fs';
import { join } from 'path';
@Injectable()
export class SqlLoaderService {
private readonly logger = new Logger(SqlLoaderService.name);
private readonly sqlRootPath = join(__dirname, '../sql/queries');
loadQuery(module: string, queryName: string, path: string): string {
const filePath = join(process.cwd(), path, module, `${queryName}.sql`);
try {
return readFileSync(filePath, 'utf8');
} catch (error) {
this.logger.error(
`Failed to load SQL query: ${module}/${queryName}`,
error.stack,
);
throw new Error(`SQL query not found: ${module}/${queryName}`);
}
}
}

View File

@ -1,17 +1,33 @@
import { Injectable } 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 { 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 {
private client: any; // Adjust type according to your TuyaWebsocket client
export class TuyaWebSocketService implements OnModuleInit {
private client: any;
private readonly isDevEnv: boolean;
private readonly deviceCache = new NodeCache({ stdTTL: 7200 }); // TTL = 2 hour
private messageQueue: {
devId: string;
status: any;
logData: any;
device: any;
}[] = [];
private isProcessing = false;
constructor(
private readonly configService: ConfigService,
private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService,
private readonly sosHandlerService: SosHandlerService,
) {
// Initialize the TuyaWebsocket client
this.isDevEnv =
this.configService.get<string>('NODE_ENV') === 'development';
this.client = new TuyaWebsocket({
accessId: this.configService.get<string>('tuya-config.TUYA_ACCESS_ID'),
accessKey: this.configService.get<string>('tuya-config.TUYA_ACCESS_KEY'),
@ -21,34 +37,80 @@ export class TuyaWebSocketService {
});
if (this.configService.get<string>('tuya-config.TRUN_ON_TUYA_SOCKET')) {
// Set up event handlers
this.setupEventHandlers();
// Start receiving messages
this.client.start();
}
// Run the queue processor every 15 seconds
setInterval(() => this.processQueue(), 15000);
// Refresh the cache every 1 hour
setInterval(() => this.initializeDeviceCache(), 30 * 60 * 1000); // 30 minutes
}
async onModuleInit() {
await this.initializeDeviceCache();
}
private async initializeDeviceCache() {
try {
const allDevices = await this.deviceStatusFirebaseService.getAllDevices();
allDevices.forEach((device) => {
if (device.deviceTuyaUuid) {
this.deviceCache.set(device.deviceTuyaUuid, device);
}
});
console.log(`✅ Refreshed cache with ${allDevices.length} devices.`);
} catch (error) {
console.error('❌ Failed to initialize device cache:', error);
}
}
private setupEventHandlers() {
// Event handlers
this.client.open(() => {
console.log('open');
});
this.client.message(async (ws: WebSocket, message: any) => {
try {
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({
deviceTuyaUuid: message.payload.data.bizData.devId,
status: message.payload.data.bizData.properties,
log: message.payload.data.bizData,
});
const { devId, status, logData } = this.extractMessageData(message);
// console.log(
// `📬 Received message for device: ${devId}, status:`,
// status,
// logData,
// );
if (!Array.isArray(status)) {
this.client.ackMessage(message.messageId);
return;
}
const device = this.deviceCache.get(devId);
if (!device) {
// console.log(⛔ Unknown device: ${devId}, message ignored.);
this.client.ackMessage(message.messageId);
return;
}
if (this.sosHandlerService.isSosTriggered(status)) {
await this.sosHandlerService.handleSosEventFirebase(devId, logData);
} else {
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({
deviceTuyaUuid: devId,
status,
log: logData,
device,
});
}
// Push to internal queue
this.messageQueue.push({ devId, status, logData, device });
// Acknowledge the message
this.client.ackMessage(message.messageId);
} catch (error) {
console.error('Error processing message:', error);
console.error('Error receiving message:', error);
}
});
this.client.reconnect(() => {
console.log('reconnect');
});
@ -69,4 +131,64 @@ export class TuyaWebSocketService {
console.error('WebSocket error:', error);
});
}
private async processQueue() {
if (this.isProcessing) {
console.log('⏳ Skipping: still processing previous batch');
return;
}
if (this.messageQueue.length === 0) return;
this.isProcessing = true;
const batch = [...this.messageQueue];
this.messageQueue = [];
console.log(`🔁 Processing batch of size: ${batch.length}`);
try {
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb(
batch.map((item) => ({
deviceTuyaUuid: item.devId,
status: item.status,
log: item.logData,
device: item.device,
})),
);
} catch (error) {
console.error('❌ Error processing batch:', error);
this.messageQueue.unshift(...batch); // retry
} finally {
this.isProcessing = false;
}
}
private extractMessageData(message: any): {
devId: string;
status: any;
logData: any;
} {
// console.log('Received message:', message);
const payloadData = message.payload.data;
if (this.isDevEnv) {
return {
devId: payloadData.bizData?.devId,
status: payloadData.bizData?.properties,
logData: payloadData.bizData,
};
} else {
return {
devId: payloadData.devId,
status: payloadData.status,
logData: payloadData,
};
}
}
// private logDeviceData(devId: string, status: any, logData: any): void {
// console.log('Device ID:', devId);
// console.log('Status:', status);
// console.log('Full Data:', logData);
// }
}

View File

@ -0,0 +1,5 @@
// Convert time string (HH:mm) to minutes
export function timeToMinutes(time: string): number {
const [hours, minutes] = time.split(':').map(Number);
return hours * 60 + minutes;
}

View File

@ -49,12 +49,12 @@ export class TuyaService {
path,
});
if (!response.success) {
throw new HttpException(
`Error fetching device details: ${response.msg}`,
HttpStatus.BAD_REQUEST,
);
}
// if (!response.success) {
// throw new HttpException(
// `Error fetching device details: ${response.msg}`,
// HttpStatus.BAD_REQUEST,
// );
// }
return response.result;
}

View File

@ -0,0 +1,12 @@
// src/common/logger/logger.module.ts
import { Module } from '@nestjs/common';
import { WinstonModule } from 'nest-winston';
import { winstonLoggerOptions } from './services/winston.logger';
import { TypeOrmWinstonLogger } from './services/typeorm.logger';
@Module({
imports: [WinstonModule.forRoot(winstonLoggerOptions)],
providers: [TypeOrmWinstonLogger],
exports: [TypeOrmWinstonLogger],
})
export class LoggerModule {}

View File

@ -0,0 +1,72 @@
import { Logger as WinstonLogger } from 'winston';
import { Logger as TypeOrmLogger } from 'typeorm';
import { requestContext } from '@app/common/context/request-context';
const ERROR_THRESHOLD = 2000;
export class TypeOrmWinstonLogger implements TypeOrmLogger {
constructor(private readonly logger: WinstonLogger) {}
private getContext() {
const context = requestContext.getStore();
return {
requestId: context?.requestId ?? 'N/A',
userId: context?.userId ?? null,
};
}
private extractTable(query: string): string {
const match =
query.match(/from\s+["`]?(\w+)["`]?/i) ||
query.match(/into\s+["`]?(\w+)["`]?/i);
return match?.[1] ?? 'unknown';
}
logQuery(query: string, parameters?: any[]) {
const context = this.getContext();
this.logger.debug(`[DB][QUERY] ${query}`, {
...context,
table: this.extractTable(query),
parameters,
});
}
logQueryError(error: string | Error, query: string, parameters?: any[]) {
const context = this.getContext();
this.logger.error(`[DB][ERROR] ${query}`, {
...context,
table: this.extractTable(query),
parameters,
error,
});
}
logQuerySlow(time: number, query: string, parameters?: any[]) {
const context = this.getContext();
const severity = time > ERROR_THRESHOLD ? 'error' : 'warn';
const label = severity === 'error' ? 'VERY SLOW' : 'SLOW';
this.logger[severity](`[DB][${label} > ${time}ms] ${query}`, {
...context,
table: this.extractTable(query),
parameters,
duration: `${time}ms`,
severity,
});
}
logSchemaBuild(message: string) {
this.logger.info(`[DB][SCHEMA] ${message}`);
}
logMigration(message: string) {
this.logger.info(`[DB][MIGRATION] ${message}`);
}
log(level: 'log' | 'info' | 'warn', message: any) {
this.logger.log({
level,
message: `[DB] ${message}`,
});
}
}

View File

@ -0,0 +1,26 @@
import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
import * as winston from 'winston';
export const winstonLoggerOptions: winston.LoggerOptions = {
level:
process.env.AZURE_POSTGRESQL_DATABASE === 'development' ? 'debug' : 'error',
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
nestWinstonModuleUtilities.format.nestLike('MyApp', {
prettyPrint: true,
}),
),
}),
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
format: winston.format.json(),
}),
new winston.transports.File({
filename: 'logs/combined.log',
format: winston.format.json(),
}),
],
};

View File

@ -0,0 +1,14 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { requestContext } from '../context/request-context';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class RequestContextMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
const context = {
requestId: req.headers['x-request-id'] || uuidv4(),
};
requestContext.run(context, () => next());
}
}

View File

@ -16,7 +16,14 @@ export interface TypeORMCustomModelFindAllQuery {
where?: { [key: string]: unknown };
select?: string[];
includeDisable?: boolean | string;
includeSpaces?: boolean;
}
export interface ExtendedTypeORMCustomModelFindAllQuery
extends TypeORMCustomModelFindAllQuery {
search?: string;
}
interface CustomFindAllQuery {
page?: number;
size?: number;

View File

@ -1,6 +1,6 @@
import { RoleType } from '@app/common/constants/role.type.enum';
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class InviteUserDto {
@IsString()
@ -12,8 +12,12 @@ export class InviteUserDto {
public email: string;
@IsString()
@IsNotEmpty()
public jobTitle: string;
@IsOptional()
public jobTitle?: string;
@IsString()
@IsOptional()
public companyName?: string;
@IsEnum(UserStatusEnum)
@IsNotEmpty()

View File

@ -8,14 +8,14 @@ import {
Unique,
} from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { RoleTypeEntity } from '../../role-type/entities';
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
import { UserEntity } from '../../user/entities';
import { RoleType } from '@app/common/constants/role.type.enum';
import { InviteUserDto, InviteUserSpaceDto } from '../dtos';
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { ProjectEntity } from '../../project/entities';
import { RoleTypeEntity } from '../../role-type/entities';
import { SpaceEntity } from '../../space/entities/space.entity';
import { UserEntity } from '../../user/entities';
import { InviteUserDto, InviteUserSpaceDto } from '../dtos';
@Entity({ name: 'invite-user' })
@Unique(['email', 'project'])
@ -37,6 +37,11 @@ export class InviteUserEntity extends AbstractEntity<InviteUserDto> {
})
jobTitle: string;
@Column({
nullable: true,
})
companyName: string;
@Column({
nullable: false,
enum: Object.values(UserStatusEnum),
@ -82,7 +87,10 @@ export class InviteUserEntity extends AbstractEntity<InviteUserDto> {
onDelete: 'CASCADE',
})
public roleType: RoleTypeEntity;
@OneToOne(() => UserEntity, (user) => user.inviteUser, { nullable: true })
@OneToOne(() => UserEntity, (user) => user.inviteUser, {
nullable: true,
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'user_uuid' })
user: UserEntity;
@OneToMany(
@ -112,7 +120,9 @@ export class InviteUserSpaceEntity extends AbstractEntity<InviteUserSpaceDto> {
})
public uuid: string;
@ManyToOne(() => InviteUserEntity, (inviteUser) => inviteUser.spaces)
@ManyToOne(() => InviteUserEntity, (inviteUser) => inviteUser.spaces, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'invite_user_uuid' })
public inviteUser: InviteUserEntity;

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AqiSpaceDailyPollutantStatsEntity } from './entities/aqi.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([AqiSpaceDailyPollutantStatsEntity])],
})
export class AqiRepositoryModule {}

View File

@ -0,0 +1,82 @@
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class AqiSpaceDailyPollutantStatsDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsNotEmpty()
@IsString()
spaceUuid: string;
@IsNotEmpty()
@IsString()
eventDay: string;
@IsNotEmpty()
@IsNumber()
eventHour: number;
@IsNumber()
pm1Min: number;
@IsNumber()
pm1Avg: number;
@IsNumber()
pm1Max: number;
@IsNumber()
pm10Min: number;
@IsNumber()
pm10Avg: number;
@IsNumber()
pm10Max: number;
@IsNumber()
pm25Min: number;
@IsNumber()
pm25Avg: number;
@IsNumber()
pm25Max: number;
@IsNumber()
ch2oMin: number;
@IsNumber()
ch2oAvg: number;
@IsNumber()
ch2oMax: number;
@IsNumber()
vocMin: number;
@IsNumber()
vocAvg: number;
@IsNumber()
vocMax: number;
@IsNumber()
co2Min: number;
@IsNumber()
co2Avg: number;
@IsNumber()
co2Max: number;
@IsNumber()
aqiMin: number;
@IsNumber()
aqiAvg: number;
@IsNumber()
aqiMax: number;
}

View File

@ -0,0 +1 @@
export * from './aqi.dto';

View File

@ -0,0 +1,184 @@
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from '../../space/entities/space.entity';
import { AqiSpaceDailyPollutantStatsDto } from '../dtos';
@Entity({ name: 'space-daily-pollutant-stats' })
@Unique(['spaceUuid', 'eventDate'])
export class AqiSpaceDailyPollutantStatsEntity extends AbstractEntity<AqiSpaceDailyPollutantStatsDto> {
@Column({ nullable: false })
public spaceUuid: string;
@ManyToOne(() => SpaceEntity, (space) => space.aqiSensorDaily)
space: SpaceEntity;
@Column({ type: 'date', nullable: false })
public eventDate: Date;
@Column('float', { nullable: true })
public goodAqiPercentage?: number;
@Column('float', { nullable: true })
public moderateAqiPercentage?: number;
@Column('float', { nullable: true })
public unhealthySensitiveAqiPercentage?: number;
@Column('float', { nullable: true })
public unhealthyAqiPercentage?: number;
@Column('float', { nullable: true })
public veryUnhealthyAqiPercentage?: number;
@Column('float', { nullable: true })
public hazardousAqiPercentage?: number;
@Column('float', { nullable: true })
public dailyAvgAqi?: number;
@Column('float', { nullable: true })
public dailyMaxAqi?: number;
@Column('float', { nullable: true })
public dailyMinAqi?: number;
@Column('float', { nullable: true })
public goodPm25Percentage?: number;
@Column('float', { nullable: true })
public moderatePm25Percentage?: number;
@Column('float', { nullable: true })
public unhealthySensitivePm25Percentage?: number;
@Column('float', { nullable: true })
public unhealthyPm25Percentage?: number;
@Column('float', { nullable: true })
public veryUnhealthyPm25Percentage?: number;
@Column('float', { nullable: true })
public hazardousPm25Percentage?: number;
@Column('float', { nullable: true })
public dailyAvgPm25?: number;
@Column('float', { nullable: true })
public dailyMaxPm25?: number;
@Column('float', { nullable: true })
public dailyMinPm25?: number;
@Column('float', { nullable: true })
public goodPm10Percentage?: number;
@Column('float', { nullable: true })
public moderatePm10Percentage?: number;
@Column('float', { nullable: true })
public unhealthySensitivePm10Percentage?: number;
@Column('float', { nullable: true })
public unhealthyPm10Percentage?: number;
@Column('float', { nullable: true })
public veryUnhealthyPm10Percentage?: number;
@Column('float', { nullable: true })
public hazardousPm10Percentage?: number;
@Column('float', { nullable: true })
public dailyAvgPm10?: number;
@Column('float', { nullable: true })
public dailyMaxPm10?: number;
@Column('float', { nullable: true })
public dailyMinPm10?: number;
@Column('float', { nullable: true })
public goodVocPercentage?: number;
@Column('float', { nullable: true })
public moderateVocPercentage?: number;
@Column('float', { nullable: true })
public unhealthySensitiveVocPercentage?: number;
@Column('float', { nullable: true })
public unhealthyVocPercentage?: number;
@Column('float', { nullable: true })
public veryUnhealthyVocPercentage?: number;
@Column('float', { nullable: true })
public hazardousVocPercentage?: number;
@Column('float', { nullable: true })
public dailyAvgVoc?: number;
@Column('float', { nullable: true })
public dailyMaxVoc?: number;
@Column('float', { nullable: true })
public dailyMinVoc?: number;
@Column('float', { nullable: true })
public goodCo2Percentage?: number;
@Column('float', { nullable: true })
public moderateCo2Percentage?: number;
@Column('float', { nullable: true })
public unhealthySensitiveCo2Percentage?: number;
@Column('float', { nullable: true })
public unhealthyCo2Percentage?: number;
@Column('float', { nullable: true })
public veryUnhealthyCo2Percentage?: number;
@Column('float', { nullable: true })
public hazardousCo2Percentage?: number;
@Column('float', { nullable: true })
public dailyAvgCo2?: number;
@Column('float', { nullable: true })
public dailyMaxCo2?: number;
@Column('float', { nullable: true })
public dailyMinCo2?: number;
@Column('float', { nullable: true })
public goodCh2oPercentage?: number;
@Column('float', { nullable: true })
public moderateCh2oPercentage?: number;
@Column('float', { nullable: true })
public unhealthySensitiveCh2oPercentage?: number;
@Column('float', { nullable: true })
public unhealthyCh2oPercentage?: number;
@Column('float', { nullable: true })
public veryUnhealthyCh2oPercentage?: number;
@Column('float', { nullable: true })
public hazardousCh2oPercentage?: number;
@Column('float', { nullable: true })
public dailyAvgCh2o?: number;
@Column('float', { nullable: true })
public dailyMaxCh2o?: number;
@Column('float', { nullable: true })
public dailyMinCh2o?: number;
constructor(partial: Partial<AqiSpaceDailyPollutantStatsEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1 @@
export * from './aqi.entity';

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { AqiSpaceDailyPollutantStatsEntity } from '../entities';
@Injectable()
export class AqiSpaceDailyPollutantStatsRepository extends Repository<AqiSpaceDailyPollutantStatsEntity> {
constructor(private dataSource: DataSource) {
super(AqiSpaceDailyPollutantStatsEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1 @@
export * from './aqi.repository';

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BookableSpaceEntity } from './entities/bookable-space.entity';
import { BookingEntity } from './entities/booking.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([BookableSpaceEntity, BookingEntity])],
})
export class BookingRepositoryModule {}

View File

@ -0,0 +1,51 @@
import { DaysEnum } from '@app/common/constants/days.enum';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
OneToOne,
UpdateDateColumn,
} from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from '../../space/entities/space.entity';
@Entity('bookable-space')
export class BookableSpaceEntity extends AbstractEntity {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@OneToOne(() => SpaceEntity, (space) => space.bookableConfig)
@JoinColumn({ name: 'space_uuid' })
space: SpaceEntity;
@Column({
type: 'enum',
enum: DaysEnum,
array: true,
nullable: false,
})
daysAvailable: DaysEnum[];
@Column({ type: 'time' })
startTime: string;
@Column({ type: 'time' })
endTime: string;
@Column({ type: Boolean, default: true })
active: boolean;
@Column({ type: 'int', default: null })
points?: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,44 @@
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
UpdateDateColumn,
} from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from '../../space/entities/space.entity';
import { UserEntity } from '../../user/entities';
@Entity('booking')
export class BookingEntity extends AbstractEntity {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@ManyToOne(() => SpaceEntity, (space) => space.bookableConfig)
space: SpaceEntity;
@ManyToOne(() => UserEntity, (user) => user.bookings)
user: UserEntity;
@Column({ type: Date, nullable: false })
date: Date;
@Column({ type: 'time' })
startTime: string;
@Column({ type: 'time' })
endTime: string;
@Column({ type: 'int', default: null })
cost?: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { BookableSpaceEntity } from '../entities/bookable-space.entity';
@Injectable()
export class BookableSpaceEntityRepository extends Repository<BookableSpaceEntity> {
constructor(private dataSource: DataSource) {
super(BookableSpaceEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { BookingEntity } from '../entities/booking.entity';
@Injectable()
export class BookingEntityRepository extends Repository<BookingEntity> {
constructor(private dataSource: DataSource) {
super(BookingEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ClientEntity } from './entities';
@Module({
imports: [TypeOrmModule.forFeature([ClientEntity])],
exports: [TypeOrmModule],
})
export class ClientRepositoryModule {}

View File

@ -0,0 +1,20 @@
import { IsArray, IsNotEmpty, IsString } from 'class-validator';
export class ClientDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public clientId: string;
@IsString()
@IsNotEmpty()
public clientSecret: string;
@IsString()
@IsNotEmpty()
public redirectUri: string;
@IsArray()
@IsNotEmpty()
public scopes: string[];
}

View File

@ -0,0 +1 @@
export * from './client.dto';

View File

@ -0,0 +1,46 @@
import { Entity, Column, Unique, OneToMany } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { ClientDto } from '../dtos';
import { UserEntity } from '../../user/entities';
@Entity({ name: 'clients' })
@Unique(['clientId'])
export class ClientEntity extends AbstractEntity<ClientDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@Column({
length: 255,
nullable: false,
})
name: string;
@Column({
length: 255,
nullable: false,
unique: true,
})
clientId: string;
@Column({
length: 255,
nullable: false,
})
clientSecret: string;
@Column({
length: 255,
nullable: false,
})
redirectUri: string;
@Column('simple-array')
scopes: string[];
@OneToMany(() => UserEntity, (user) => user.client)
users: UserEntity[];
}

View File

@ -0,0 +1 @@
export * from './client.entity';

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { ClientEntity } from '../entities';
@Injectable()
export class ClientRepository extends Repository<ClientEntity> {
constructor(private dataSource: DataSource) {
super(ClientEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1 @@
export * from './client.repository';

View File

@ -2,15 +2,15 @@ import { SourceType } from '@app/common/constants/source-type.enum';
import { Entity, Column, PrimaryColumn, Unique } from 'typeorm';
@Entity('device-status-log')
@Unique('event_time_idx', ['eventTime'])
@Unique('event_time_idx', ['eventTime', 'deviceId', 'code', 'value'])
export class DeviceStatusLogEntity {
@Column({ type: 'int', generated: true, unsigned: true })
@PrimaryColumn({ type: 'int', generated: true, unsigned: true })
id: number;
@Column({ type: 'text' })
eventId: string;
@PrimaryColumn({ type: 'timestamptz' })
@Column({ type: 'timestamptz' })
eventTime: Date;
@Column({

View File

@ -1,25 +1,24 @@
import {
Column,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
Unique,
Index,
JoinColumn,
OneToOne,
JoinTable,
} from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { DeviceDto, DeviceUserPermissionDto } from '../dtos/device.dto';
import { ProductEntity } from '../../product/entities';
import { UserEntity } from '../../user/entities';
import { DeviceNotificationDto } from '../dtos';
import { PermissionTypeEntity } from '../../permission/entities';
import { PowerClampHourlyEntity } from '../../power-clamp/entities/power-clamp.entity';
import { PresenceSensorDailyDeviceEntity } from '../../presence-sensor/entities';
import { ProductEntity } from '../../product/entities';
import { SceneDeviceEntity } from '../../scene-device/entities';
import { SpaceEntity } from '../../space/entities/space.entity';
import { SubspaceEntity } from '../../space/entities/subspace/subspace.entity';
import { TagEntity } from '../../space/entities/tag.entity';
import { NewTagEntity } from '../../tag';
import { UserEntity } from '../../user/entities';
import { DeviceNotificationDto } from '../dtos';
import { DeviceDto, DeviceUserPermissionDto } from '../dtos/device.dto';
@Entity({ name: 'device' })
@Unique(['deviceTuyaUuid'])
@ -29,16 +28,21 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
})
deviceTuyaUuid: string;
@Column({
nullable: true,
})
deviceTuyaConstUuid: string;
@Column({
nullable: true,
default: true,
type: 'boolean',
})
isActive: boolean;
@ManyToOne(() => UserEntity, (user) => user.userSpaces, { nullable: false })
user: UserEntity;
@Column({
nullable: false,
})
name: string;
@OneToMany(
() => DeviceUserPermissionEntity,
(permission) => permission.device,
@ -79,15 +83,13 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
@OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.device, {})
sceneDevices: SceneDeviceEntity[];
@OneToOne(() => TagEntity, (tag) => tag.device, {
nullable: true,
})
tag: TagEntity;
@OneToMany(() => NewTagEntity, (tag) => tag.devices)
@JoinTable({ name: 'device_tags' })
public tags: NewTagEntity[];
@ManyToOne(() => NewTagEntity, (tag) => tag.devices)
@JoinColumn({ name: 'tag_uuid' })
public tag: NewTagEntity;
@OneToMany(() => PowerClampHourlyEntity, (powerClamp) => powerClamp.device)
powerClampHourly: PowerClampHourlyEntity[];
@OneToMany(() => PresenceSensorDailyDeviceEntity, (sensor) => sensor.device)
presenceSensorDaily: PresenceSensorDailyDeviceEntity[];
constructor(partial: Partial<DeviceEntity>) {
super();
Object.assign(this, partial);
@ -114,6 +116,7 @@ export class DeviceNotificationEntity extends AbstractEntity<DeviceNotificationD
@ManyToOne(() => UserEntity, (user) => user.userPermission, {
nullable: false,
onDelete: 'CASCADE',
})
user: UserEntity;
@ -152,6 +155,7 @@ export class DeviceUserPermissionEntity extends AbstractEntity<DeviceUserPermiss
@ManyToOne(() => UserEntity, (user) => user.userPermission, {
nullable: false,
onDelete: 'CASCADE',
})
user: UserEntity;
constructor(partial: Partial<DeviceUserPermissionEntity>) {

View File

@ -0,0 +1 @@
export * from './occupancy.dto';

View File

@ -0,0 +1,23 @@
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class SpaceDailyOccupancyDurationDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public spaceUuid: string;
@IsString()
@IsNotEmpty()
public eventDate: string;
@IsNumber()
@IsNotEmpty()
public occupancyPercentage: number;
@IsNumber()
@IsNotEmpty()
public occupiedSeconds: number;
}

View File

@ -0,0 +1 @@
export * from './occupancy.entity';

View File

@ -0,0 +1,32 @@
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from '../../space/entities/space.entity';
import { SpaceDailyOccupancyDurationDto } from '../dtos';
@Entity({ name: 'space-daily-occupancy-duration' })
@Unique(['spaceUuid', 'eventDate'])
export class SpaceDailyOccupancyDurationEntity extends AbstractEntity<SpaceDailyOccupancyDurationDto> {
@Column({ nullable: false })
public spaceUuid: string;
@Column({ nullable: false, type: 'date' })
public eventDate: string;
public CountTotalPresenceDetected: number;
@ManyToOne(() => SpaceEntity, (space) => space.presenceSensorDaily)
space: SpaceEntity;
@Column({ type: 'int' })
occupancyPercentage: number;
@Column({ type: 'int', nullable: true })
occupiedSeconds?: number;
@Column({ type: 'int', nullable: true })
deviceCount?: number;
constructor(partial: Partial<SpaceDailyOccupancyDurationEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SpaceDailyOccupancyDurationEntity } from './entities/occupancy.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([SpaceDailyOccupancyDurationEntity])],
})
export class SpaceDailyOccupancyDurationRepositoryModule {}

View File

@ -0,0 +1 @@
export * from './occupancy.repository';

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { SpaceDailyOccupancyDurationEntity } from '../entities/occupancy.entity';
@Injectable()
export class SpaceDailyOccupancyDurationEntityRepository extends Repository<SpaceDailyOccupancyDurationEntity> {
constructor(private dataSource: DataSource) {
super(SpaceDailyOccupancyDurationEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1 @@
export * from './power-clamp.dto';

View File

@ -0,0 +1,43 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class PowerClampDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public deviceUuid: string;
@IsString()
@IsOptional()
public hour?: string;
@IsString()
@IsOptional()
public day?: string;
@IsString()
@IsOptional()
public month?: string;
@IsString()
@IsNotEmpty()
public energyConsumedKw: string;
@IsString()
@IsNotEmpty()
public energyConsumedA: string;
@IsString()
@IsNotEmpty()
public energyConsumedB: string;
@IsString()
@IsNotEmpty()
public energyConsumedC: string;
@IsString()
@IsNotEmpty()
public prodType: string;
}

View File

@ -0,0 +1 @@
export * from './power-clamp.entity';

View File

@ -0,0 +1,95 @@
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { PowerClampDto } from '../dtos';
import { DeviceEntity } from '../../device/entities/device.entity';
@Entity({ name: 'power-clamp-energy-consumed-hourly' })
@Unique(['deviceUuid', 'date', 'hour'])
export class PowerClampHourlyEntity extends AbstractEntity<PowerClampDto> {
@Column({ nullable: false })
public deviceUuid: string;
@Column({ nullable: false })
public hour: string;
@Column({ nullable: false, type: 'date' })
public date: string;
@Column({ nullable: true })
public energyConsumedKw: string;
@Column({ nullable: false })
public energyConsumedA: string;
@Column({ nullable: false })
public energyConsumedB: string;
@Column({ nullable: false })
public energyConsumedC: string;
@ManyToOne(() => DeviceEntity, (device) => device.powerClampHourly)
device: DeviceEntity;
constructor(partial: Partial<PowerClampHourlyEntity>) {
super();
Object.assign(this, partial);
}
}
@Entity({ name: 'power-clamp-energy-consumed-daily' })
@Unique(['deviceUuid', 'date'])
export class PowerClampDailyEntity extends AbstractEntity<PowerClampDto> {
@Column({ nullable: false })
public deviceUuid: string;
@Column({ nullable: false, type: 'date' })
public date: string;
@Column({ nullable: true })
public energyConsumedKw: string;
@Column({ nullable: false })
public energyConsumedA: string;
@Column({ nullable: false })
public energyConsumedB: string;
@Column({ nullable: false })
public energyConsumedC: string;
@ManyToOne(() => DeviceEntity, (device) => device.powerClampHourly)
device: DeviceEntity;
constructor(partial: Partial<PowerClampHourlyEntity>) {
super();
Object.assign(this, partial);
}
}
@Entity({ name: 'power-clamp-energy-consumed-monthly' })
@Unique(['deviceUuid', 'month'])
export class PowerClampMonthlyEntity extends AbstractEntity<PowerClampDto> {
@Column({ nullable: false })
public deviceUuid: string;
@Column({ nullable: false })
public month: string;
@Column({ nullable: true })
public energyConsumedKw: string;
@Column({ nullable: false })
public energyConsumedA: string;
@Column({ nullable: false })
public energyConsumedB: string;
@Column({ nullable: false })
public energyConsumedC: string;
@ManyToOne(() => DeviceEntity, (device) => device.powerClampHourly)
device: DeviceEntity;
constructor(partial: Partial<PowerClampHourlyEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PowerClampHourlyEntity } from './entities/power-clamp.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([PowerClampHourlyEntity])],
})
export class PowerClampRepositoryModule {}

View File

@ -0,0 +1 @@
export * from './power-clamp.repository';

View File

@ -0,0 +1,28 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import {
PowerClampDailyEntity,
PowerClampHourlyEntity,
PowerClampMonthlyEntity,
} from '../entities/power-clamp.entity';
@Injectable()
export class PowerClampHourlyRepository extends Repository<PowerClampHourlyEntity> {
constructor(private dataSource: DataSource) {
super(PowerClampHourlyEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class PowerClampDailyRepository extends Repository<PowerClampDailyEntity> {
constructor(private dataSource: DataSource) {
super(PowerClampDailyEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class PowerClampMonthlyRepository extends Repository<PowerClampMonthlyEntity> {
constructor(private dataSource: DataSource) {
super(PowerClampMonthlyEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1 @@
export * from './presence-sensor.dto';

View File

@ -0,0 +1,27 @@
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class PresenceSensorDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public deviceUuid: string;
@IsString()
@IsNotEmpty()
public eventDate: string;
@IsNumber()
@IsNotEmpty()
public CountMotionDetected: number;
@IsNumber()
@IsNotEmpty()
public CountPresenceDetected: number;
@IsNumber()
@IsNotEmpty()
public CountTotalPresenceDetected: number;
}

View File

@ -0,0 +1 @@
export * from './presence-sensor.entity';

View File

@ -0,0 +1,58 @@
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { PresenceSensorDto } from '../dtos';
import { DeviceEntity } from '../../device/entities/device.entity';
import { SpaceEntity } from '../../space/entities/space.entity';
@Entity({ name: 'presence-sensor-daily-device-detection' })
@Unique(['deviceUuid', 'eventDate'])
export class PresenceSensorDailyDeviceEntity extends AbstractEntity<PresenceSensorDto> {
@Column({ nullable: false })
public deviceUuid: string;
@Column({ nullable: false, type: 'date' })
public eventDate: string;
@Column({ nullable: false })
public CountMotionDetected: number;
@Column({ nullable: false })
public CountPresenceDetected: number;
@Column({ nullable: false })
public CountTotalPresenceDetected: number;
@ManyToOne(() => DeviceEntity, (device) => device.presenceSensorDaily)
device: DeviceEntity;
constructor(partial: Partial<PresenceSensorDailyDeviceEntity>) {
super();
Object.assign(this, partial);
}
}
@Entity({ name: 'presence-sensor-daily-space-detection' })
@Unique(['spaceUuid', 'eventDate'])
export class PresenceSensorDailySpaceEntity extends AbstractEntity<PresenceSensorDto> {
@Column({ nullable: false })
public spaceUuid: string;
@Column({ nullable: false, type: 'date' })
public eventDate: string;
@Column({ nullable: false })
public CountMotionDetected: number;
@Column({ nullable: false })
public CountPresenceDetected: number;
@Column({ nullable: false })
public CountTotalPresenceDetected: number;
@ManyToOne(() => SpaceEntity, (space) => space.presenceSensorDaily)
space: SpaceEntity;
constructor(partial: Partial<PresenceSensorDailySpaceEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
PresenceSensorDailyDeviceEntity,
PresenceSensorDailySpaceEntity,
} from './entities/presence-sensor.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [
TypeOrmModule.forFeature([
PresenceSensorDailyDeviceEntity,
PresenceSensorDailySpaceEntity,
]),
],
})
export class PresenceSensorRepositoryModule {}

Some files were not shown because too many files have changed in this diff Show More