feat(backend): Suche und Filter (Modul 14)
SearchModule mit GET /search?q= über Bienenstände, Völker, Königinnen, Aufgaben und Honigchargen. Kombinierbare Filter (Status, Standort, Bienenstand) in den bestehenden List-Endpunkten.
This commit is contained in:
1 parent
55b9bc2587
commit
146b56d683
15 files changed
+337
-27
No files matched your search
@@ -1,6 +1,14 @@
|
|||||||
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { Role } from '../../generated/prisma';
|
import { ApiaryStatus, Role } from '../../generated/prisma';
|
||||||
import { Roles } from '../auth/decorators/roles.decorator';
|
import { Roles } from '../auth/decorators/roles.decorator';
|
||||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
||||||
@@ -15,8 +23,11 @@ export class ApiariesController {
|
|||||||
constructor(private readonly apiariesService: ApiariesService) {}
|
constructor(private readonly apiariesService: ApiariesService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
findAll() {
|
findAll(
|
||||||
return this.apiariesService.findAll();
|
@Query('status') status?: ApiaryStatus,
|
||||||
|
@Query('location') location?: string,
|
||||||
|
) {
|
||||||
|
return this.apiariesService.findAll({ status, location });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { AuditLogService } from '../audit/audit-log.service';
|
import { AuditLogService } from '../audit/audit-log.service';
|
||||||
import { Apiary } from '../../generated/prisma';
|
import { Apiary, ApiaryStatus } from '../../generated/prisma';
|
||||||
import { CreateApiaryDto } from './dto/create-apiary.dto';
|
import { CreateApiaryDto } from './dto/create-apiary.dto';
|
||||||
import { UpdateApiaryDto } from './dto/update-apiary.dto';
|
import { UpdateApiaryDto } from './dto/update-apiary.dto';
|
||||||
|
|
||||||
|
export interface ApiaryFilter {
|
||||||
|
status?: ApiaryStatus;
|
||||||
|
location?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApiariesService {
|
export class ApiariesService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -12,8 +17,16 @@ export class ApiariesService {
|
|||||||
private readonly auditLog: AuditLogService,
|
private readonly auditLog: AuditLogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findAll(): Promise<Apiary[]> {
|
async findAll(filter: ApiaryFilter = {}): Promise<Apiary[]> {
|
||||||
return this.prisma.apiary.findMany({ orderBy: { name: 'asc' } });
|
return this.prisma.apiary.findMany({
|
||||||
|
where: {
|
||||||
|
status: filter.status,
|
||||||
|
location: filter.location
|
||||||
|
? { contains: filter.location, mode: 'insensitive' }
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(id: string): Promise<Apiary> {
|
async findById(id: string): Promise<Apiary> {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { ExtractionsModule } from './extractions/extractions.module';
|
|||||||
import { HoneyBatchesModule } from './honey-batches/honey-batches.module';
|
import { HoneyBatchesModule } from './honey-batches/honey-batches.module';
|
||||||
import { StorageModule } from './storage/storage.module';
|
import { StorageModule } from './storage/storage.module';
|
||||||
import { DashboardModule } from './dashboard/dashboard.module';
|
import { DashboardModule } from './dashboard/dashboard.module';
|
||||||
|
import { SearchModule } from './search/search.module';
|
||||||
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
|
||||||
import { RolesGuard } from './auth/guards/roles.guard';
|
import { RolesGuard } from './auth/guards/roles.guard';
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ import { RolesGuard } from './auth/guards/roles.guard';
|
|||||||
HoneyBatchesModule,
|
HoneyBatchesModule,
|
||||||
StorageModule,
|
StorageModule,
|
||||||
DashboardModule,
|
DashboardModule,
|
||||||
|
SearchModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { Role } from '../../generated/prisma';
|
import { ColonyStatus, Role } from '../../generated/prisma';
|
||||||
import { Roles } from '../auth/decorators/roles.decorator';
|
import { Roles } from '../auth/decorators/roles.decorator';
|
||||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
||||||
@@ -18,8 +26,11 @@ export class ColoniesController {
|
|||||||
constructor(private readonly coloniesService: ColoniesService) {}
|
constructor(private readonly coloniesService: ColoniesService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
findAll() {
|
findAll(
|
||||||
return this.coloniesService.findAll();
|
@Query('status') status?: ColonyStatus,
|
||||||
|
@Query('apiaryId') apiaryId?: string,
|
||||||
|
) {
|
||||||
|
return this.coloniesService.findAll({ status, apiaryId });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
|||||||
@@ -5,13 +5,18 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { AuditLogService } from '../audit/audit-log.service';
|
import { AuditLogService } from '../audit/audit-log.service';
|
||||||
import { Colony } from '../../generated/prisma';
|
import { Colony, ColonyStatus } from '../../generated/prisma';
|
||||||
import { CreateColonyDto } from './dto/create-colony.dto';
|
import { CreateColonyDto } from './dto/create-colony.dto';
|
||||||
import { UpdateColonyDto } from './dto/update-colony.dto';
|
import { UpdateColonyDto } from './dto/update-colony.dto';
|
||||||
import { MoveColonyDto } from './dto/move-colony.dto';
|
import { MoveColonyDto } from './dto/move-colony.dto';
|
||||||
import { SplitColonyDto } from './dto/split-colony.dto';
|
import { SplitColonyDto } from './dto/split-colony.dto';
|
||||||
import { MergeColonyDto } from './dto/merge-colony.dto';
|
import { MergeColonyDto } from './dto/merge-colony.dto';
|
||||||
|
|
||||||
|
export interface ColonyFilter {
|
||||||
|
status?: ColonyStatus;
|
||||||
|
apiaryId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ColoniesService {
|
export class ColoniesService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -19,8 +24,11 @@ export class ColoniesService {
|
|||||||
private readonly auditLog: AuditLogService,
|
private readonly auditLog: AuditLogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findAll(): Promise<Colony[]> {
|
async findAll(filter: ColonyFilter = {}): Promise<Colony[]> {
|
||||||
return this.prisma.colony.findMany({ orderBy: { code: 'asc' } });
|
return this.prisma.colony.findMany({
|
||||||
|
where: { status: filter.status, apiaryId: filter.apiaryId },
|
||||||
|
orderBy: { code: 'asc' },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(id: string): Promise<Colony> {
|
async findById(id: string): Promise<Colony> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { Role } from '../../generated/prisma';
|
import { HoneyBatchStatus, Role } from '../../generated/prisma';
|
||||||
import { Roles } from '../auth/decorators/roles.decorator';
|
import { Roles } from '../auth/decorators/roles.decorator';
|
||||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
||||||
@@ -15,8 +15,8 @@ export class HoneyBatchesController {
|
|||||||
constructor(private readonly honeyBatchesService: HoneyBatchesService) {}
|
constructor(private readonly honeyBatchesService: HoneyBatchesService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
findAll() {
|
findAll(@Query('status') status?: HoneyBatchStatus) {
|
||||||
return this.honeyBatchesService.findAll();
|
return this.honeyBatchesService.findAll({ status });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { AuditLogService } from '../audit/audit-log.service';
|
import { AuditLogService } from '../audit/audit-log.service';
|
||||||
|
import { HoneyBatchStatus } from '../../generated/prisma';
|
||||||
import { CreateHoneyBatchDto } from './dto/create-honey-batch.dto';
|
import { CreateHoneyBatchDto } from './dto/create-honey-batch.dto';
|
||||||
import { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto';
|
import { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto';
|
||||||
|
|
||||||
@@ -17,6 +18,10 @@ const honeyBatchInclude = {
|
|||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export interface HoneyBatchFilter {
|
||||||
|
status?: HoneyBatchStatus;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HoneyBatchesService {
|
export class HoneyBatchesService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -24,8 +29,9 @@ export class HoneyBatchesService {
|
|||||||
private readonly auditLog: AuditLogService,
|
private readonly auditLog: AuditLogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findAll() {
|
async findAll(filter: HoneyBatchFilter = {}) {
|
||||||
return this.prisma.honeyBatch.findMany({
|
return this.prisma.honeyBatch.findMany({
|
||||||
|
where: { status: filter.status },
|
||||||
include: honeyBatchInclude,
|
include: honeyBatchInclude,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { Role } from '../../generated/prisma';
|
import { QueenStatus, Role } from '../../generated/prisma';
|
||||||
import { Roles } from '../auth/decorators/roles.decorator';
|
import { Roles } from '../auth/decorators/roles.decorator';
|
||||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
||||||
@@ -16,8 +24,8 @@ export class QueensController {
|
|||||||
constructor(private readonly queensService: QueensService) {}
|
constructor(private readonly queensService: QueensService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
findAll() {
|
findAll(@Query('status') status?: QueenStatus) {
|
||||||
return this.queensService.findAll();
|
return this.queensService.findAll({ status });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { AuditLogService } from '../audit/audit-log.service';
|
import { AuditLogService } from '../audit/audit-log.service';
|
||||||
import { Queen } from '../../generated/prisma';
|
import { Queen, QueenStatus } from '../../generated/prisma';
|
||||||
import { CreateQueenDto } from './dto/create-queen.dto';
|
import { CreateQueenDto } from './dto/create-queen.dto';
|
||||||
import { UpdateQueenDto } from './dto/update-queen.dto';
|
import { UpdateQueenDto } from './dto/update-queen.dto';
|
||||||
import { AssignColonyDto } from './dto/assign-colony.dto';
|
import { AssignColonyDto } from './dto/assign-colony.dto';
|
||||||
|
|
||||||
|
export interface QueenFilter {
|
||||||
|
status?: QueenStatus;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QueensService {
|
export class QueensService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -13,8 +17,11 @@ export class QueensService {
|
|||||||
private readonly auditLog: AuditLogService,
|
private readonly auditLog: AuditLogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findAll(): Promise<Queen[]> {
|
async findAll(filter: QueenFilter = {}): Promise<Queen[]> {
|
||||||
return this.prisma.queen.findMany({ orderBy: { code: 'asc' } });
|
return this.prisma.queen.findMany({
|
||||||
|
where: { status: filter.status },
|
||||||
|
orderBy: { code: 'asc' },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(id: string): Promise<Queen> {
|
async findById(id: string): Promise<Queen> {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { SearchService } from './search.service';
|
||||||
|
|
||||||
|
@ApiTags('search')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller('search')
|
||||||
|
export class SearchController {
|
||||||
|
constructor(private readonly searchService: SearchService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
search(@Query('q') q?: string) {
|
||||||
|
return this.searchService.search(q ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SearchController } from './search.controller';
|
||||||
|
import { SearchService } from './search.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [SearchController],
|
||||||
|
providers: [SearchService],
|
||||||
|
})
|
||||||
|
export class SearchModule {}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { SearchService } from './search.service';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
|
describe('SearchService', () => {
|
||||||
|
let service: SearchService;
|
||||||
|
let prisma: {
|
||||||
|
apiary: { findMany: jest.Mock };
|
||||||
|
colony: { findMany: jest.Mock };
|
||||||
|
queen: { findMany: jest.Mock };
|
||||||
|
task: { findMany: jest.Mock };
|
||||||
|
honeyBatch: { findMany: jest.Mock };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
prisma = {
|
||||||
|
apiary: { findMany: jest.fn().mockResolvedValue([]) },
|
||||||
|
colony: { findMany: jest.fn().mockResolvedValue([]) },
|
||||||
|
queen: { findMany: jest.fn().mockResolvedValue([]) },
|
||||||
|
task: { findMany: jest.fn().mockResolvedValue([]) },
|
||||||
|
honeyBatch: { findMany: jest.fn().mockResolvedValue([]) },
|
||||||
|
};
|
||||||
|
service = new SearchService(prisma as unknown as PrismaService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty array for a blank query', async () => {
|
||||||
|
const results = await service.search(' ');
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
expect(prisma.apiary.findMany).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('combines matches from every entity type', async () => {
|
||||||
|
prisma.colony.findMany.mockResolvedValue([
|
||||||
|
{ id: 'colony-1', code: 'V-001' },
|
||||||
|
]);
|
||||||
|
prisma.apiary.findMany.mockResolvedValue([
|
||||||
|
{ id: 'apiary-1', name: 'Heimstand' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const results = await service.search('V-001');
|
||||||
|
|
||||||
|
expect(results).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{ type: 'Colony', id: 'colony-1', label: 'V-001' },
|
||||||
|
{ type: 'Apiary', id: 'apiary-1', label: 'Heimstand' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
type: 'Apiary' | 'Colony' | 'Queen' | 'Task' | 'HoneyBatch';
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RESULT_LIMIT = 10;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SearchService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async search(q: string): Promise<SearchResult[]> {
|
||||||
|
const term = q.trim();
|
||||||
|
if (term.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [apiaries, colonies, queens, tasks, honeyBatches] = await Promise.all(
|
||||||
|
[
|
||||||
|
this.prisma.apiary.findMany({
|
||||||
|
where: { name: { contains: term, mode: 'insensitive' } },
|
||||||
|
take: RESULT_LIMIT,
|
||||||
|
}),
|
||||||
|
this.prisma.colony.findMany({
|
||||||
|
where: { code: { contains: term, mode: 'insensitive' } },
|
||||||
|
take: RESULT_LIMIT,
|
||||||
|
}),
|
||||||
|
this.prisma.queen.findMany({
|
||||||
|
where: { code: { contains: term, mode: 'insensitive' } },
|
||||||
|
take: RESULT_LIMIT,
|
||||||
|
}),
|
||||||
|
this.prisma.task.findMany({
|
||||||
|
where: { title: { contains: term, mode: 'insensitive' } },
|
||||||
|
take: RESULT_LIMIT,
|
||||||
|
}),
|
||||||
|
this.prisma.honeyBatch.findMany({
|
||||||
|
where: { batchNumber: { contains: term, mode: 'insensitive' } },
|
||||||
|
take: RESULT_LIMIT,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
...apiaries.map((apiary) => ({
|
||||||
|
type: 'Apiary' as const,
|
||||||
|
id: apiary.id,
|
||||||
|
label: apiary.name,
|
||||||
|
})),
|
||||||
|
...colonies.map((colony) => ({
|
||||||
|
type: 'Colony' as const,
|
||||||
|
id: colony.id,
|
||||||
|
label: colony.code,
|
||||||
|
})),
|
||||||
|
...queens.map((queen) => ({
|
||||||
|
type: 'Queen' as const,
|
||||||
|
id: queen.id,
|
||||||
|
label: queen.code,
|
||||||
|
})),
|
||||||
|
...tasks.map((task) => ({
|
||||||
|
type: 'Task' as const,
|
||||||
|
id: task.id,
|
||||||
|
label: task.title,
|
||||||
|
})),
|
||||||
|
...honeyBatches.map((honeyBatch) => ({
|
||||||
|
type: 'HoneyBatch' as const,
|
||||||
|
id: honeyBatch.id,
|
||||||
|
label: honeyBatch.batchNumber,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as bcrypt from 'bcryptjs';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { App } from 'supertest/types';
|
||||||
|
import { PrismaService } from '../src/prisma/prisma.service';
|
||||||
|
import { createTestApp } from './utils/create-test-app';
|
||||||
|
import { login } from './utils/api';
|
||||||
|
|
||||||
|
interface SearchResultBody {
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColonyBody {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Search and filters (e2e)', () => {
|
||||||
|
let app: INestApplication<App>;
|
||||||
|
let prisma: PrismaService;
|
||||||
|
const readonlyEmail = `search-e2e-readonly-${Date.now()}@example.com`;
|
||||||
|
const password = 'SuperSecret123!';
|
||||||
|
let readonlyId: string;
|
||||||
|
let readonlyToken: string;
|
||||||
|
let apiaryId: string;
|
||||||
|
let colonyId: string;
|
||||||
|
let dissolvedColonyId: string;
|
||||||
|
const uniqueCode = `SRCH-${Date.now()}`;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await createTestApp();
|
||||||
|
prisma = app.get(PrismaService);
|
||||||
|
|
||||||
|
const passwordHash = await bcrypt.hash(password, 12);
|
||||||
|
const readonly = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: readonlyEmail,
|
||||||
|
passwordHash,
|
||||||
|
name: 'Readonly',
|
||||||
|
role: 'READONLY',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
readonlyId = readonly.id;
|
||||||
|
readonlyToken = (await login(app, readonlyEmail, password)).accessToken;
|
||||||
|
|
||||||
|
const apiary = await prisma.apiary.create({
|
||||||
|
data: { name: `Stand ${Date.now()}` },
|
||||||
|
});
|
||||||
|
apiaryId = apiary.id;
|
||||||
|
const colony = await prisma.colony.create({
|
||||||
|
data: { code: uniqueCode, apiaryId, colonyType: 'PRODUCTION_COLONY' },
|
||||||
|
});
|
||||||
|
colonyId = colony.id;
|
||||||
|
const dissolvedColony = await prisma.colony.create({
|
||||||
|
data: {
|
||||||
|
code: `${uniqueCode}-DISSOLVED`,
|
||||||
|
apiaryId,
|
||||||
|
colonyType: 'PRODUCTION_COLONY',
|
||||||
|
status: 'DISSOLVED',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dissolvedColonyId = dissolvedColony.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await prisma.colony.deleteMany({
|
||||||
|
where: { id: { in: [colonyId, dissolvedColonyId] } },
|
||||||
|
});
|
||||||
|
await prisma.apiary.deleteMany({ where: { id: apiaryId } });
|
||||||
|
await prisma.refreshToken.deleteMany({ where: { userId: readonlyId } });
|
||||||
|
await prisma.auditLog.deleteMany({ where: { userId: readonlyId } });
|
||||||
|
await prisma.user.deleteMany({ where: { id: readonlyId } });
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds a colony by its code across entity types', async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v1/search?q=${uniqueCode}`)
|
||||||
|
.set('Authorization', `Bearer ${readonlyToken}`)
|
||||||
|
.expect(200);
|
||||||
|
const results = response.body as SearchResultBody[];
|
||||||
|
expect(
|
||||||
|
results.some((entry) => entry.type === 'Colony' && entry.id === colonyId),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters colonies by status', async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v1/colonies?status=DISSOLVED&apiaryId=${apiaryId}`)
|
||||||
|
.set('Authorization', `Bearer ${readonlyToken}`)
|
||||||
|
.expect(200);
|
||||||
|
const colonies = response.body as ColonyBody[];
|
||||||
|
expect(colonies.map((entry) => entry.id)).toEqual([dissolvedColonyId]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -121,7 +121,7 @@ Abgeleitet aus [`docs/lastenheft.md`](docs/lastenheft.md) Abschnitt 26. Reihenfo
|
|||||||
|
|
||||||
- Backend: `SearchModule` (`GET /api/v1/search?q=`), Filter-Query-Parameter in bestehenden List-Endpunkten (Standort, Status, Zeitraum etc., kombinierbar)
|
- Backend: `SearchModule` (`GET /api/v1/search?q=`), Filter-Query-Parameter in bestehenden List-Endpunkten (Standort, Status, Zeitraum etc., kombinierbar)
|
||||||
- Frontend: `/search`, Filterleisten in allen Listen-Views
|
- Frontend: `/search`, Filterleisten in allen Listen-Views
|
||||||
- [ ] Backend-Modul fertig, getestet, grün, committet
|
- [x] Backend-Modul fertig, getestet, grün, committet
|
||||||
- [ ] Frontend-Views fertig, getestet, grün, committet
|
- [ ] Frontend-Views fertig, getestet, grün, committet
|
||||||
|
|
||||||
### 15. Responsive Smartphone-Oberfläche
|
### 15. Responsive Smartphone-Oberfläche
|
||||||
|
|||||||
Reference in new issue
Block a user