Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { OutboxModule } from './outbox/outbox.module';
import { VerificationModule } from './verification/verification.module';
import { TelegramBotModule } from './telegram-bot/telegram-bot.module';
import { IdempotencyInterceptor } from './common/interceptors/idempotency.interceptor';
import { SearchModule } from './search/search.module';
import { ExportModule } from './export/export.module';

@Module({
Expand Down Expand Up @@ -114,6 +115,7 @@ import { ExportModule } from './export/export.module';
ExportModule,
TelegramBotModule,
ModerationModule,
SearchModule,
FeatureFlagsModule,
],
controllers: [AppController, TestController, TestExceptionController],
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async function bootstrap() {
.addTag('news', 'Crypto news aggregation and sentiment analysis')
.addTag('portfolio', 'Portfolio tracking and performance metrics')
.addTag('stellar', 'Stellar blockchain integration')
.addTag('search', 'Search and discovery endpoints')
.addServer('http://localhost:3000', 'Development')
.addServer('https://api.lumenpulse.io', 'Production')
.build();
Expand Down
45 changes: 45 additions & 0 deletions apps/backend/src/search/dto/asset-search.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsIn, IsInt, IsOptional, Min } from 'class-validator';
import { AssetDiscoveryQueryDto } from '../../stellar/dto/asset-discovery.dto';

export class AssetSearchQueryDto extends AssetDiscoveryQueryDto {
@ApiProperty({
description: 'Filter by minimum number of accounts holding the asset',
required: false,
example: 100,
})
@IsOptional()
@IsInt()
@Min(0)
minAccounts?: number;

@ApiProperty({
description: 'Filter by maximum number of accounts holding the asset',
required: false,
example: 100000,
})
@IsOptional()
@IsInt()
@Min(0)
maxAccounts?: number;

@ApiProperty({
description: 'Filter assets requiring authorization (auth_required flag)',
required: false,
example: false,
})
@IsOptional()
@IsBoolean()
authRequired?: boolean;

@ApiProperty({
description: 'Sort results by relevance or by number of accounts',
required: false,
enum: ['relevance', 'accounts'],
default: 'relevance',
example: 'relevance',
})
@IsOptional()
@IsIn(['relevance', 'accounts'])
sort?: 'relevance' | 'accounts';
}
67 changes: 67 additions & 0 deletions apps/backend/src/search/dto/ecosystem-search.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsIn, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';

export type EcosystemEntityKind = 'tag' | 'category';

export class EcosystemSearchQueryDto {
@ApiProperty({
description: 'Search query (matches tag/category value)',
required: false,
example: 'stellar',
})
@IsOptional()
@IsString()
q?: string;

@ApiProperty({
description: 'Entity kinds to include',
required: false,
enum: ['tag', 'category'],
default: 'tag',
example: 'tag',
})
@IsOptional()
@IsIn(['tag', 'category'])
kind?: EcosystemEntityKind;

@ApiProperty({
description: 'Pagination limit (top N by usage)',
required: false,
default: 25,
minimum: 1,
maximum: 200,
example: 25,
})
@IsOptional()
@IsInt()
@Min(1)
@Max(200)
limit?: number;

@ApiProperty({
description: 'Whether to include entity usage counts',
required: false,
default: true,
example: true,
})
@IsOptional()
@IsBoolean()
includeCounts?: boolean;
}

export class EcosystemEntityDto {
@ApiProperty({ enum: ['tag', 'category'], example: 'tag' })
kind: EcosystemEntityKind;

@ApiProperty({ example: 'stellar' })
value: string;

@ApiProperty({ example: 123, required: false })
count?: number;
}

export class EcosystemSearchResponseDto {
@ApiProperty({ type: [EcosystemEntityDto] })
items: EcosystemEntityDto[];
}

121 changes: 121 additions & 0 deletions apps/backend/src/search/dto/project-search.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsEnum,
IsInt,
IsOptional,
IsString,
Max,
Min,
} from 'class-validator';
import { VerificationStatus } from '../../verification/dto/verification.dto';

export class ProjectSearchQueryDto {
@ApiProperty({
description: 'Search query (matches project name or numeric id)',
required: false,
example: 'Lumen',
})
@IsOptional()
@IsString()
q?: string;

@ApiProperty({
description: 'Filter by verification status',
required: false,
enum: VerificationStatus,
example: VerificationStatus.Pending,
})
@IsOptional()
@IsEnum(VerificationStatus)
status?: VerificationStatus;

@ApiProperty({
description: 'Filter by owner Stellar public key',
required: false,
example: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN',
})
@IsOptional()
@IsString()
ownerPublicKey?: string;

@ApiProperty({
description: 'Pagination limit',
required: false,
default: 20,
minimum: 1,
maximum: 100,
example: 20,
})
@IsOptional()
@IsInt()
@Min(1)
@Max(100)
limit?: number;

@ApiProperty({
description: 'Pagination offset',
required: false,
default: 0,
minimum: 0,
example: 0,
})
@IsOptional()
@IsInt()
@Min(0)
offset?: number;
}

export class ProjectSearchItemDto {
@ApiProperty({ example: 1 })
projectId: number;

@ApiProperty({ example: 'LumenPulse' })
name: string;

@ApiProperty({
example: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN',
})
ownerPublicKey: string;

@ApiProperty({ enum: VerificationStatus, example: VerificationStatus.Pending })
status: VerificationStatus;

@ApiProperty({ example: 0 })
votesFor: number;

@ApiProperty({ example: 0 })
votesAgainst: number;

@ApiProperty({ example: 1712345678 })
registeredAt: number;

@ApiProperty({ example: 0 })
resolvedAt: number;

@ApiProperty({
description: 'Percentage of quorum reached (0–100)',
example: 0,
})
quorumProgress: number;

@ApiProperty({
description: 'Relevance score used for ordering',
example: 100,
})
score: number;
}

export class ProjectSearchResponseDto {
@ApiProperty({ type: [ProjectSearchItemDto] })
items: ProjectSearchItemDto[];

@ApiProperty({ example: 42 })
total: number;

@ApiProperty({ example: 20 })
limit: number;

@ApiProperty({ example: 0 })
offset: number;
}

72 changes: 72 additions & 0 deletions apps/backend/src/search/search.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Controller, Get, HttpCode, HttpStatus, Query } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { SearchService } from './search.service';
import {
ProjectSearchQueryDto,
ProjectSearchResponseDto,
} from './dto/project-search.dto';
import { AssetSearchQueryDto } from './dto/asset-search.dto';
import { AssetDiscoveryResponseDto } from '../stellar/dto/asset-discovery.dto';
import {
EcosystemSearchQueryDto,
EcosystemSearchResponseDto,
} from './dto/ecosystem-search.dto';

@ApiTags('search')
@Controller('search')
export class SearchController {
constructor(private readonly searchService: SearchService) {}

@Get('projects')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Search projects',
description:
'Search registered projects with basic relevance ranking and optional status/owner filters.',
})
@ApiResponse({
status: 200,
description: 'Project search results',
type: ProjectSearchResponseDto,
})
searchProjects(@Query() query: ProjectSearchQueryDto): ProjectSearchResponseDto {
return this.searchService.searchProjects(query);
}

@Get('assets')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Search Stellar assets',
description:
'Wraps Stellar asset discovery with basic ranking and extra filters (accounts/auth flags).',
})
@ApiResponse({
status: 200,
description: 'Asset search results',
type: AssetDiscoveryResponseDto,
})
async searchAssets(
@Query() query: AssetSearchQueryDto,
): Promise<AssetDiscoveryResponseDto> {
return this.searchService.searchAssets(query);
}

@Get('ecosystem')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Search ecosystem entities',
description:
'Search top tags or categories derived from stored news articles.',
})
@ApiResponse({
status: 200,
description: 'Ecosystem entity results',
type: EcosystemSearchResponseDto,
})
async searchEcosystem(
@Query() query: EcosystemSearchQueryDto,
): Promise<EcosystemSearchResponseDto> {
return this.searchService.searchEcosystemEntities(query);
}
}

15 changes: 15 additions & 0 deletions apps/backend/src/search/search.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { News } from '../news/news.entity';
import { StellarModule } from '../stellar/stellar.module';
import { VerificationModule } from '../verification/verification.module';
import { SearchController } from './search.controller';
import { SearchService } from './search.service';

@Module({
imports: [StellarModule, VerificationModule, TypeOrmModule.forFeature([News])],
controllers: [SearchController],
providers: [SearchService],
})
export class SearchModule {}

Loading
Loading