good forst commit
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
|
||||
import { RulesService } from './rules.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/roles.guard';
|
||||
import { Roles } from '../auth/roles.decorator';
|
||||
import { CurrentTenantContext } from '../auth/current-tenant.decorator';
|
||||
import { TenantContext } from '../../common/tenant-context';
|
||||
import { CreateRuleRequest, UpdateRuleRequest } from '@tower/types';
|
||||
|
||||
@Controller('admin/rules')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('OWNER', 'ADMIN')
|
||||
export class RulesController {
|
||||
constructor(private readonly rulesService: RulesService) {}
|
||||
|
||||
@Get()
|
||||
list(@CurrentTenantContext() ctx: TenantContext) {
|
||||
return this.rulesService.list(ctx.tenantId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(
|
||||
@CurrentTenantContext() ctx: TenantContext,
|
||||
@Body() body: CreateRuleRequest,
|
||||
) {
|
||||
return this.rulesService.create(ctx.tenantId, body);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(
|
||||
@CurrentTenantContext() ctx: TenantContext,
|
||||
@Param('id') id: string,
|
||||
@Body() body: UpdateRuleRequest,
|
||||
) {
|
||||
return this.rulesService.update(ctx.tenantId, id, body);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(
|
||||
@CurrentTenantContext() ctx: TenantContext,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return this.rulesService.remove(ctx.tenantId, id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RulesController } from './rules.controller';
|
||||
import { RulesService } from './rules.service';
|
||||
|
||||
@Module({
|
||||
controllers: [RulesController],
|
||||
providers: [RulesService],
|
||||
exports: [RulesService],
|
||||
})
|
||||
export class RulesModule {}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConflictException, NotFoundException } from '@nestjs/common';
|
||||
import { RulesService } from './rules.service';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
describe('RulesService', () => {
|
||||
let service: RulesService;
|
||||
const mockPrisma: any = {
|
||||
tenantRule: { findMany: jest.fn(), findUnique: jest.fn(), findFirst: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn() },
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
RulesService,
|
||||
{ provide: PrismaService, useValue: mockPrisma },
|
||||
],
|
||||
}).compile();
|
||||
service = module.get<RulesService>(RulesService);
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
it('returns rules ordered by priority', async () => {
|
||||
mockPrisma.tenantRule.findMany.mockResolvedValue([
|
||||
{ id: 'r1', tenantId: 'tnt-1', matchType: 'HASHTAG', matchValue: '#important', action: 'FLAG', priority: 0, isActive: true, createdAt: new Date('2026-01-01'), updatedAt: new Date('2026-01-01') },
|
||||
{ id: 'r2', tenantId: 'tnt-1', matchType: 'PREFIX', matchValue: '/tower', action: 'FLAG', priority: 1, isActive: true, createdAt: new Date('2026-01-02'), updatedAt: new Date('2026-01-02') },
|
||||
]);
|
||||
const res = await service.list('tnt-1');
|
||||
expect(res).toHaveLength(2);
|
||||
expect(res[0].matchValue).toBe('#important');
|
||||
expect(mockPrisma.tenantRule.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: { tenantId: 'tnt-1' } }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('creates and returns a rule', async () => {
|
||||
mockPrisma.tenantRule.findUnique.mockResolvedValue(null);
|
||||
mockPrisma.tenantRule.create.mockResolvedValue({
|
||||
id: 'r1', tenantId: 'tnt-1', matchType: 'HASHTAG', matchValue: '#event', action: 'FLAG', priority: 0, isActive: true, createdAt: new Date('2026-01-01'), updatedAt: new Date('2026-01-01'),
|
||||
});
|
||||
const res = await service.create('tnt-1', { matchType: 'HASHTAG', matchValue: '#event', action: 'FLAG' });
|
||||
expect(res.matchValue).toBe('#event');
|
||||
});
|
||||
|
||||
it('rejects duplicate rule', async () => {
|
||||
mockPrisma.tenantRule.findUnique.mockResolvedValue({ id: 'existing' });
|
||||
await expect(service.create('tnt-1', { matchType: 'HASHTAG', matchValue: '#event', action: 'FLAG' })).rejects.toThrow(ConflictException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('updates and returns a rule', async () => {
|
||||
mockPrisma.tenantRule.findFirst.mockResolvedValue({ id: 'r1', tenantId: 'tnt-1' });
|
||||
mockPrisma.tenantRule.update.mockResolvedValue({
|
||||
id: 'r1', tenantId: 'tnt-1', matchType: 'HASHTAG', matchValue: '#event', action: 'AUTO_APPROVE', priority: 0, isActive: true, createdAt: new Date('2026-01-01'), updatedAt: new Date('2026-01-02'),
|
||||
});
|
||||
const res = await service.update('tnt-1', 'r1', { action: 'AUTO_APPROVE' });
|
||||
expect(res.action).toBe('AUTO_APPROVE');
|
||||
});
|
||||
|
||||
it('throws on non-existent rule', async () => {
|
||||
mockPrisma.tenantRule.findFirst.mockResolvedValue(null);
|
||||
await expect(service.update('tnt-1', 'missing', { action: 'AUTO_APPROVE' })).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('deletes a rule', async () => {
|
||||
mockPrisma.tenantRule.findFirst.mockResolvedValue({ id: 'r1', tenantId: 'tnt-1' });
|
||||
mockPrisma.tenantRule.delete.mockResolvedValue({});
|
||||
await expect(service.remove('tnt-1', 'r1')).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws on non-existent rule', async () => {
|
||||
mockPrisma.tenantRule.findFirst.mockResolvedValue(null);
|
||||
await expect(service.remove('tnt-1', 'missing')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateRuleRequest, TenantRuleData, UpdateRuleRequest } from '@tower/types';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class RulesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(tenantId: string): Promise<TenantRuleData[]> {
|
||||
const rows = await this.prisma.tenantRule.findMany({
|
||||
where: { tenantId },
|
||||
orderBy: { priority: 'asc' },
|
||||
});
|
||||
return rows.map((r: any) => ({
|
||||
id: r.id,
|
||||
tenantId: r.tenantId,
|
||||
matchType: r.matchType,
|
||||
matchValue: r.matchValue,
|
||||
action: r.action,
|
||||
priority: r.priority,
|
||||
isActive: r.isActive,
|
||||
createdAt: r.createdAt.toISOString(),
|
||||
updatedAt: r.updatedAt.toISOString(),
|
||||
}));
|
||||
}
|
||||
|
||||
async create(tenantId: string, req: CreateRuleRequest): Promise<TenantRuleData> {
|
||||
const existing = await this.prisma.tenantRule.findUnique({
|
||||
where: { tenantId_matchType_matchValue: { tenantId, matchType: req.matchType, matchValue: req.matchValue } },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException('A rule with this matchType + matchValue already exists');
|
||||
}
|
||||
|
||||
const row = await this.prisma.tenantRule.create({
|
||||
data: {
|
||||
tenantId,
|
||||
matchType: req.matchType,
|
||||
matchValue: req.matchValue,
|
||||
action: req.action,
|
||||
priority: req.priority ?? 0,
|
||||
isActive: req.isActive ?? true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
tenantId: row.tenantId,
|
||||
matchType: row.matchType as any,
|
||||
matchValue: row.matchValue,
|
||||
action: row.action as any,
|
||||
priority: row.priority,
|
||||
isActive: row.isActive,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async update(tenantId: string, id: string, req: UpdateRuleRequest): Promise<TenantRuleData> {
|
||||
const rule = await this.prisma.tenantRule.findFirst({ where: { id, tenantId } });
|
||||
if (!rule) throw new NotFoundException('Rule not found');
|
||||
|
||||
const data: any = {};
|
||||
if (req.matchType !== undefined) data.matchType = req.matchType;
|
||||
if (req.matchValue !== undefined) data.matchValue = req.matchValue;
|
||||
if (req.action !== undefined) data.action = req.action;
|
||||
if (req.priority !== undefined) data.priority = req.priority;
|
||||
if (req.isActive !== undefined) data.isActive = req.isActive;
|
||||
|
||||
const row = await this.prisma.tenantRule.update({ where: { id }, data });
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
tenantId: row.tenantId,
|
||||
matchType: row.matchType as any,
|
||||
matchValue: row.matchValue,
|
||||
action: row.action as any,
|
||||
priority: row.priority,
|
||||
isActive: row.isActive,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async remove(tenantId: string, id: string): Promise<void> {
|
||||
const rule = await this.prisma.tenantRule.findFirst({ where: { id, tenantId } });
|
||||
if (!rule) throw new NotFoundException('Rule not found');
|
||||
await this.prisma.tenantRule.delete({ where: { id } });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user