feat(05-01): dashboard backend — Prisma models, CRUD API, module wiring
- Add DashboardLayout and WidgetInstance Prisma models with userId/tenantId scoping - Create DashboardController with 6 endpoints (layout CRUD + widget CRUD) - Create DashboardService with ownership verification on all widget mutations (T-05-01) - Add SaveLayoutDto, CreateWidgetDto, UpdateWidgetConfigDto with class-validator - Register DashboardModule in app.module.ts imports Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateWidgetDto } from './dto/create-widget.dto';
|
||||
import { SaveLayoutDto } from './dto/save-layout.dto';
|
||||
import { UpdateWidgetConfigDto } from './dto/update-widget-config.dto';
|
||||
|
||||
/**
|
||||
* Service managing per-user dashboard layouts and widget instances.
|
||||
*
|
||||
* Layout (position/size) and widget config are stored in separate models
|
||||
* to avoid unnecessary saves when only one changes (RESEARCH anti-pattern).
|
||||
*
|
||||
* All operations are scoped by userId for security (T-05-01, T-05-02).
|
||||
*/
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Returns the user's saved layout, or a default empty layout
|
||||
* with all breakpoint arrays initialized.
|
||||
*/
|
||||
async getLayout(userId: string) {
|
||||
const record = await this.prisma.dashboardLayout.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
return { lg: [], md: [], sm: [], xs: [], xxs: [] };
|
||||
}
|
||||
|
||||
return record.layouts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upserts the user's dashboard layout.
|
||||
* Creates a new record if none exists, updates if it does.
|
||||
*/
|
||||
async saveLayout(userId: string, tenantId: string, dto: SaveLayoutDto) {
|
||||
return this.prisma.dashboardLayout.upsert({
|
||||
where: { userId },
|
||||
update: { layouts: dto.layouts as unknown as Prisma.InputJsonValue },
|
||||
create: {
|
||||
userId,
|
||||
tenantId,
|
||||
layouts: dto.layouts as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all widget instances for a given user.
|
||||
*/
|
||||
async getWidgets(userId: string) {
|
||||
return this.prisma.widgetInstance.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new widget instance for the user.
|
||||
*/
|
||||
async addWidget(userId: string, tenantId: string, dto: CreateWidgetDto) {
|
||||
return this.prisma.widgetInstance.create({
|
||||
data: {
|
||||
userId,
|
||||
tenantId,
|
||||
widgetType: dto.widgetType,
|
||||
config: (dto.config ?? {}) as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the config of a widget instance.
|
||||
* Verifies ownership by userId before updating (T-05-01).
|
||||
*/
|
||||
async updateWidgetConfig(
|
||||
id: string,
|
||||
userId: string,
|
||||
dto: UpdateWidgetConfigDto,
|
||||
) {
|
||||
const widget = await this.prisma.widgetInstance.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!widget || widget.userId !== userId) {
|
||||
throw new NotFoundException(
|
||||
`Widget with id '${id}' not found`,
|
||||
);
|
||||
}
|
||||
|
||||
// Merge existing config with new config
|
||||
const mergedConfig = {
|
||||
...(widget.config as Record<string, unknown>),
|
||||
...dto.config,
|
||||
};
|
||||
|
||||
return this.prisma.widgetInstance.update({
|
||||
where: { id },
|
||||
data: { config: mergedConfig as unknown as Prisma.InputJsonValue },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a widget instance.
|
||||
* Verifies ownership by userId before deleting (T-05-01).
|
||||
*/
|
||||
async removeWidget(id: string, userId: string) {
|
||||
const widget = await this.prisma.widgetInstance.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!widget || widget.userId !== userId) {
|
||||
throw new NotFoundException(
|
||||
`Widget with id '${id}' not found`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.prisma.widgetInstance.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user