Skip to content
Merged
18 changes: 18 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug NestJS (start:debug)",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["run", "start:debug"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"autoAttachChildProcesses": true,
"envFile": "${workspaceFolder}/.env",
"skipFiles": ["<node_internals>/**"]
}
]
}
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { join } from 'path';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
import { DevicesModule } from './devices/devices.module';
import { RulesModule } from './rules/rules.module';

@Module({
imports: [
Expand All @@ -37,6 +38,7 @@ import { DevicesModule } from './devices/devices.module';
},
]),
DevicesModule,
RulesModule,
],
controllers: [AppController],
providers: [AppService, { provide: APP_GUARD, useClass: ThrottlerGuard }],
Expand Down
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ async function bootstrap() {
customSwaggerUiPath: join(process.cwd(), 'static', 'docs'),
customCssUrl: '/cw-swagger.css',
customfavIcon: '/favicon.svg',
swaggerOptions: {
persistAuthorization: true,
},
});
app.use(
helmet({
Expand Down
42 changes: 42 additions & 0 deletions src/rules/dto/create-rule.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ApiProperty } from '@nestjs/swagger';
import { Database } from '../../../database.types';

type RuleInsert = Database['public']['Tables']['cw_rules']['Insert'];

export class CreateRuleDto implements RuleInsert {
@ApiProperty()
action_recipient: string;

@ApiProperty()
name: string;

@ApiProperty()
notifier_type: number;

@ApiProperty()
ruleGroupId: string;

@ApiProperty({ required: false })
created_at?: string;

@ApiProperty({ required: false, nullable: true })
dev_eui?: string | null;

@ApiProperty({ required: false })
id?: number;

@ApiProperty({ required: false })
is_triggered?: boolean;

@ApiProperty({ required: false, nullable: true })
last_triggered?: string | null;

@ApiProperty({ required: false })
profile_id?: string;

@ApiProperty({ required: false, nullable: true })
send_using?: string | null;

@ApiProperty({ required: false })
trigger_count?: number;
}
42 changes: 42 additions & 0 deletions src/rules/dto/rule.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ApiProperty } from '@nestjs/swagger';
import { Database } from '../../../database.types';

type RuleRow = Database['public']['Tables']['cw_rules']['Row'];

export class RuleDto implements RuleRow {
@ApiProperty()
id: number;

@ApiProperty()
name: string;

@ApiProperty()
action_recipient: string;

@ApiProperty()
notifier_type: number;

@ApiProperty()
ruleGroupId: string;

@ApiProperty()
profile_id: string;

@ApiProperty({ nullable: true, required: false })
dev_eui: string | null;

@ApiProperty({ nullable: true, required: false })
send_using: string | null;

@ApiProperty()
is_triggered: boolean;

@ApiProperty()
trigger_count: number;

@ApiProperty({ format: 'date-time' })
created_at: string;

@ApiProperty({ nullable: true, required: false, format: 'date-time' })
last_triggered: string | null;
}
9 changes: 9 additions & 0 deletions src/rules/dto/update-rule.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PartialType } from '@nestjs/swagger';
import { Database } from '../../../database.types';
import { CreateRuleDto } from './create-rule.dto';

type RuleUpdate = Database['public']['Tables']['cw_rules']['Update'];

export class UpdateRuleDto
extends PartialType(CreateRuleDto)
implements RuleUpdate {}
1 change: 1 addition & 0 deletions src/rules/entities/rule.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class Rule {}
25 changes: 25 additions & 0 deletions src/rules/rules.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RulesController } from './rules.controller';
import { RulesService } from './rules.service';

describe('RulesController', () => {
let controller: RulesController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [RulesController],
providers: [
{
provide: RulesService,
useValue: {},
},
],
}).compile();

controller = module.get<RulesController>(RulesController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
82 changes: 82 additions & 0 deletions src/rules/rules.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Req, UseGuards } from '@nestjs/common';
import { RulesService } from './rules.service';
import { CreateRuleDto } from './dto/create-rule.dto';
import { UpdateRuleDto } from './dto/update-rule.dto';
import { ApiBearerAuth, ApiOkResponse, ApiSecurity } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt.auth.guard';
import { RuleDto } from './dto/rule.dto';

@ApiBearerAuth('bearerAuth')
@ApiSecurity('apiKey')
@Controller('rules')
export class RulesController {
constructor(private readonly rulesService: RulesService) { }

@UseGuards(JwtAuthGuard)
@ApiOkResponse({
description:
"Create a new rule configuration. Only the fields included in the request body will be used.",
type: RuleDto,
isArray: false,
})
Comment on lines +15 to +21
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API documentation is missing several standard error response decorators that are consistently used throughout the codebase for protected endpoints. Based on the pattern in other controllers (devices, auth, air, etc.), this endpoint should include: @ApiUnauthorizedResponse, @ApiBadRequestResponse, and @ApiInternalServerErrorResponse decorators with appropriate error examples matching the ErrorResponseDto type.

Copilot uses AI. Check for mistakes.
@Post()
create(@Body() createRuleDto: CreateRuleDto, @Req() req) {
const authHeader = req.headers?.authorization;
if (!authHeader) {
throw new Error('Authorization header is required');
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using generic 'throw new Error' is inconsistent with error handling in the rest of the controller layer. All other controllers in the codebase use NestJS exception classes (BadRequestException, UnauthorizedException, etc.). Since the JwtAuthGuard should already handle authentication, this check may be redundant. However, if it's necessary, it should throw 'new UnauthorizedException('Authorization header is required')' to match the error handling pattern used elsewhere.

Copilot uses AI. Check for mistakes.
}
return this.rulesService.create(createRuleDto, req.user, authHeader);
}

@UseGuards(JwtAuthGuard)
@ApiOkResponse({
description:
"Current all of the user's rules configurations.",
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the API documentation description. 'Current all of' should be 'Returns all of' or 'Gets all of'.

Suggested change
"Current all of the user's rules configurations.",
"Returns all of the user's rules configurations.",

Copilot uses AI. Check for mistakes.
type: RuleDto,
isArray: true,
})
Comment on lines +31 to +37
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing API error response decorators. Following the pattern established in other controllers, this endpoint should include @ApiUnauthorizedResponse and @ApiInternalServerErrorResponse decorators.

Copilot uses AI. Check for mistakes.
@Get()
findAll(@Req() req) {
const authHeader = req.headers?.authorization ?? '';
return this.rulesService.findAll(req.user, authHeader);
}

@UseGuards(JwtAuthGuard)
@ApiOkResponse({
description:
"Gets a user's rule configuration by ID.",
type: RuleDto,
isArray: false,
})
Comment on lines +44 to +50
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing API error response decorators. Following the pattern established in other controllers, this endpoint should include @ApiUnauthorizedResponse, @ApiInternalServerErrorResponse, and @ApiNotFoundResponse decorators (since the service can throw NotFoundException).

Copilot uses AI. Check for mistakes.
@Get(':id')
findOne(@Param('id') id: number, @Req() req) {
const authHeader = req.headers?.authorization ?? '';
return this.rulesService.findOne(id, req.user, authHeader);
}

@UseGuards(JwtAuthGuard)
@ApiOkResponse({
description:
"Update a single rule configuration by ID. Only the fields included in the request body will be updated.",
type: RuleDto,
isArray: false,
})
Comment on lines +57 to +63
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing API error response decorators. Following the pattern established in other controllers, this endpoint should include @ApiUnauthorizedResponse, @ApiBadRequestResponse (since dev_eui validation can fail), and @ApiInternalServerErrorResponse decorators.

Copilot uses AI. Check for mistakes.
@Patch(':id')
update(@Param('id') id: number, @Body() updateRuleDto: UpdateRuleDto, @Req() req) {
const authHeader = req.headers?.authorization ?? '';
return this.rulesService.update(id, updateRuleDto, req.user, authHeader);
}

@UseGuards(JwtAuthGuard)
@ApiOkResponse({
description:
"Delete a user's rule configuration by ID.",
type: Number,
isArray: false,
})
Comment on lines +70 to +76
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing API error response decorators. Following the pattern established in other controllers, this endpoint should include @ApiUnauthorizedResponse and @ApiInternalServerErrorResponse decorators.

Copilot uses AI. Check for mistakes.
@Delete(':id')
remove(@Param('id') id: number, @Req() req) {
const authHeader = req.headers?.authorization ?? '';
return this.rulesService.remove(id, req.user, authHeader);
}
}
11 changes: 11 additions & 0 deletions src/rules/rules.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { RulesService } from './rules.service';
import { RulesController } from './rules.controller';
import { SupabaseModule } from '../supabase/supabase.module';

@Module({
imports: [SupabaseModule],
controllers: [RulesController],
providers: [RulesService],
})
export class RulesModule { }
18 changes: 18 additions & 0 deletions src/rules/rules.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RulesService } from './rules.service';

describe('RulesService', () => {
let service: RulesService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [RulesService],
Comment on lines +3 to +9
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test setup is incomplete and will fail. It doesn't mock the SupabaseService dependency that RulesService requires. Following the pattern in src/devices/devices.service.spec.ts, you should add a SupabaseService mock in the providers array with useValue providing stubs for getClient and getAdminClient methods.

Suggested change
describe('RulesService', () => {
let service: RulesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [RulesService],
import { SupabaseService } from '../../supabase/supabase.service';
describe('RulesService', () => {
let service: RulesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
RulesService,
{
provide: SupabaseService,
useValue: {
getClient: jest.fn(),
getAdminClient: jest.fn(),
},
},
],

Copilot uses AI. Check for mistakes.
}).compile();

service = module.get<RulesService>(RulesService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
Loading