feat(05-02): add SearchProvider backend with model, CRUD, and defaults

- Prisma model SearchProvider with userId/tenantId scoping
- Three default providers (Google/Bing/DuckDuckGo) as constants, always returned without DB seed
- GET/POST/DELETE search-providers endpoints on DashboardController
- Ownership verification on delete (T-05-07), default providers cannot be deleted
- CreateSearchProviderDto with class-validator: urlTemplate must contain {query} (T-05-08)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 11:34:45 +02:00
parent dd0209898b
commit 38dcfdfa6d
4 changed files with 151 additions and 0 deletions
@@ -4,10 +4,46 @@ import {
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { CreateSearchProviderDto } from './dto/create-search-provider.dto';
import { CreateWidgetDto } from './dto/create-widget.dto';
import { SaveLayoutDto } from './dto/save-layout.dto';
import { UpdateWidgetConfigDto } from './dto/update-widget-config.dto';
/**
* Default search providers (D-15).
* Returned as part of getSearchProviders even when no DB rows exist.
* userId null = global defaults — cannot be deleted by users.
*/
const DEFAULT_SEARCH_PROVIDERS = [
{
id: 'google',
userId: null,
tenantId: null,
name: 'Google',
urlTemplate: 'https://www.google.com/search?q={query}',
isDefault: true,
createdAt: new Date('2024-01-01'),
},
{
id: 'bing',
userId: null,
tenantId: null,
name: 'Bing',
urlTemplate: 'https://www.bing.com/search?q={query}',
isDefault: true,
createdAt: new Date('2024-01-01'),
},
{
id: 'ddg',
userId: null,
tenantId: null,
name: 'DuckDuckGo',
urlTemplate: 'https://duckduckgo.com/?q={query}',
isDefault: true,
createdAt: new Date('2024-01-01'),
},
];
/**
* Service managing per-user dashboard layouts and widget instances.
*
@@ -126,4 +162,59 @@ export class DashboardService {
where: { id },
});
}
// --- Search Providers (05-02, D-15) ---
/**
* Returns the three default providers merged with any user-custom providers.
* Defaults are always returned even with an empty DB (no seed migration needed).
*/
async getSearchProviders(userId: string) {
const custom = await this.prisma.searchProvider.findMany({
where: { userId },
orderBy: { createdAt: 'asc' },
});
return [...DEFAULT_SEARCH_PROVIDERS, ...custom];
}
/**
* Creates a user-custom search provider.
*/
async addSearchProvider(
userId: string,
tenantId: string,
dto: CreateSearchProviderDto,
) {
return this.prisma.searchProvider.create({
data: {
userId,
tenantId,
name: dto.name,
urlTemplate: dto.urlTemplate,
isDefault: false,
},
});
}
/**
* Removes a user-custom search provider.
* Verifies ownership — default providers (userId null) cannot be deleted (T-05-07).
*/
async removeSearchProvider(id: string, userId: string) {
// Default providers have hardcoded IDs that won't exist in DB
const provider = await this.prisma.searchProvider.findUnique({
where: { id },
});
if (!provider || provider.userId !== userId) {
throw new NotFoundException(
`Search provider with id '${id}' not found`,
);
}
return this.prisma.searchProvider.delete({
where: { id },
});
}
}