fix(api,ci): remove type-only imports for DI and isolate CI ports from dev

- Remove `type` keyword from NestJS injectable class imports across all
  modules to fix runtime DI resolution (330+ handler/listener files)
- Offset CI docker-compose ports (5433/6380/8109/9002) to avoid
  conflicts with running dev containers
- Update .env.test, playwright.config.ts, and e2e workflow to use
  isolated CI ports with configurable overrides
- Fix prisma/seed.ts to use deterministic IDs for Prisma 7 upsert
  compatibility (phoneHash replaced phone as unique index)
- Add dedicated Docker bridge network for CI service containers

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-04-13 01:40:14 +07:00
parent 1617921993
commit 25420720e7
345 changed files with 3266 additions and 924 deletions

View File

@@ -1,9 +1,9 @@
import { InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type CommandBus, type ICommandHandler, type QueryBus } from '@nestjs/cqrs';
import { CommandHandler, CommandBus, ICommandHandler, QueryBus } from '@nestjs/cqrs';
import { createId } from '@paralleldrive/cuid2';
import { type SavedSearch, type Prisma } from '@prisma/client';
import { DomainException, ValidationException, type PrismaService, type LoggerService } from '@modules/shared';
import { CheckQuotaQuery, type QuotaCheckResult, MeterUsageCommand } from '@modules/subscriptions';
import { SavedSearch, Prisma } from '@prisma/client';
import { DomainException, ValidationException, PrismaService, LoggerService } from '@modules/shared';
import { CheckQuotaQuery, QuotaCheckResult, MeterUsageCommand } from '@modules/subscriptions';
import { CreateSavedSearchCommand } from './create-saved-search.command';
export interface CreateSavedSearchResult {

View File

@@ -1,6 +1,6 @@
import { InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { DomainException, ForbiddenException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { DomainException, ForbiddenException, NotFoundException, PrismaService, LoggerService } from '@modules/shared';
import { DeleteSavedSearchCommand } from './delete-saved-search.command';
@CommandHandler(DeleteSavedSearchCommand)

View File

@@ -1,7 +1,7 @@
import { InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService } from '@modules/shared';
import { type ListingIndexerService } from '../../../infrastructure/services/listing-indexer.service';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { DomainException, LoggerService } from '@modules/shared';
import { ListingIndexerService } from '../../../infrastructure/services/listing-indexer.service';
import { ReindexAllCommand } from './reindex-all.command';
export interface ReindexResult {

View File

@@ -1,7 +1,7 @@
import { InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService } from '@modules/shared';
import { type ListingIndexerService } from '../../../infrastructure/services/listing-indexer.service';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { DomainException, LoggerService } from '@modules/shared';
import { ListingIndexerService } from '../../../infrastructure/services/listing-indexer.service';
import { SyncListingCommand } from './sync-listing.command';
@CommandHandler(SyncListingCommand)

View File

@@ -1,7 +1,7 @@
import { InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { type Prisma } from '@prisma/client';
import { DomainException, ForbiddenException, NotFoundException, ValidationException, type PrismaService, type LoggerService } from '@modules/shared';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { Prisma } from '@prisma/client';
import { DomainException, ForbiddenException, NotFoundException, ValidationException, PrismaService, LoggerService } from '@modules/shared';
import { UpdateSavedSearchCommand } from './update-saved-search.command';
export interface UpdateSavedSearchResult {

View File

@@ -1,9 +1,9 @@
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { CacheService, CachePrefix, CacheTTL, DomainException, type LoggerService } from '@modules/shared';
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { CacheService, CachePrefix, CacheTTL, DomainException, LoggerService } from '@modules/shared';
import {
SEARCH_REPOSITORY,
type ISearchRepository,
ISearchRepository,
type SearchResult,
} from '../../../domain/repositories/search.repository';
import { GeoSearchQuery } from './geo-search.query';

View File

@@ -1,6 +1,6 @@
import { InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
import { DomainException, ForbiddenException, NotFoundException, type LoggerService, type PrismaService } from '@modules/shared';
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
import { DomainException, ForbiddenException, NotFoundException, LoggerService, PrismaService } from '@modules/shared';
import { GetSavedSearchQuery } from './get-saved-search.query';
export interface SavedSearchDetail {

View File

@@ -1,6 +1,6 @@
import { InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService, type PrismaService } from '@modules/shared';
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
import { DomainException, LoggerService, PrismaService } from '@modules/shared';
import { GetSavedSearchesQuery } from './get-saved-searches.query';
export interface SavedSearchItem {

View File

@@ -1,9 +1,9 @@
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { CacheService, CachePrefix, CacheTTL, DomainException, type LoggerService } from '@modules/shared';
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { CacheService, CachePrefix, CacheTTL, DomainException, LoggerService } from '@modules/shared';
import {
SEARCH_REPOSITORY,
type ISearchRepository,
ISearchRepository,
type SearchResult,
} from '../../../domain/repositories/search.repository';
import { SearchPropertiesQuery } from './search-properties.query';

View File

@@ -1 +1 @@
export { SEARCH_REPOSITORY, type ISearchRepository, type ListingDocument, type SearchResult, type SearchParams } from './search.repository';
export { SEARCH_REPOSITORY, ISearchRepository, type ListingDocument, type SearchResult, type SearchParams } from './search.repository';

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { Cron, CronExpression } from '@nestjs/schedule';
import { SendNotificationCommand } from '@modules/notifications';
import { type PrismaService, type LoggerService } from '@modules/shared';
import { PrismaService, LoggerService } from '@modules/shared';
/**
* Daily cron job that checks saved searches against new listings published since lastAlertAt.

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { CacheService, CachePrefix, type LoggerService } from '@modules/shared';
import { type ListingIndexerService } from '../services/listing-indexer.service';
import { CacheService, CachePrefix, LoggerService } from '@modules/shared';
import { ListingIndexerService } from '../services/listing-indexer.service';
@Injectable()
export class ListingApprovedEventHandler {

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { type ListingStatusChangedEvent } from '@modules/listings';
import { CacheService, CachePrefix, type LoggerService } from '@modules/shared';
import { type ListingIndexerService } from '../services/listing-indexer.service';
import { ListingStatusChangedEvent } from '@modules/listings';
import { CacheService, CachePrefix, LoggerService } from '@modules/shared';
import { ListingIndexerService } from '../services/listing-indexer.service';
@Injectable()
export class ListingStatusChangedHandler {

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { SendNotificationCommand } from '@modules/notifications';
import { type PrismaService, type LoggerService } from '@modules/shared';
import { PrismaService, LoggerService } from '@modules/shared';
/**
* When a new listing is approved, check all saved searches with alerts enabled

View File

@@ -1,9 +1,9 @@
import { Inject, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { LoggerService, PrismaService } from '@modules/shared';
import {
SEARCH_REPOSITORY,
type ISearchRepository,
ISearchRepository,
type ListingDocument,
} from '../../domain/repositories/search.repository';

View File

@@ -1,14 +1,14 @@
import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { LoggerService, PrismaService } from '@modules/shared';
import {
type ISearchRepository,
ISearchRepository,
type ListingDocument,
type SearchParams,
type SearchResult,
} from '../../domain/repositories/search.repository';
import { buildSearchConditions, buildOrderClause } from './search-query-builder';
import { type RawListingRow, mapRowToListingDocument } from './search-result-mapper';
import { RawListingRow, mapRowToListingDocument } from './search-result-mapper';
/**
* PostgreSQL-backed search repository used as a fallback when Typesense

View File

@@ -1,20 +1,20 @@
import { Injectable } from '@nestjs/common';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import { type Counter } from 'prom-client';
import { Counter } from 'prom-client';
import {
CircuitBreaker,
CircuitOpenError,
type CircuitState,
type LoggerService,
LoggerService,
} from '@modules/shared';
import {
type ISearchRepository,
ISearchRepository,
type ListingDocument,
type SearchParams,
type SearchResult,
} from '../../domain/repositories/search.repository';
import { type PostgresSearchRepository } from './postgres-search.repository';
import { type TypesenseSearchRepository } from './typesense-search.repository';
import { PostgresSearchRepository } from './postgres-search.repository';
import { TypesenseSearchRepository } from './typesense-search.repository';
export const SEARCH_DEGRADATION_TOTAL = 'search_degradation_total';

View File

@@ -1,5 +1,5 @@
import { Prisma } from '@prisma/client';
import { type SearchParams } from '../../domain/repositories/search.repository';
import { SearchParams } from '../../domain/repositories/search.repository';
import { parseFilterBy } from './search-filter-parser';
const FTS_COLUMNS = `coalesce(p."title", '') || ' ' || coalesce(p."description", '') || ' ' || coalesce(p."address", '') || ' ' || coalesce(p."district", '') || ' ' || coalesce(p."city", '')`;

View File

@@ -1,4 +1,4 @@
import { type ListingDocument } from '../../domain/repositories/search.repository';
import { ListingDocument } from '../../domain/repositories/search.repository';
export interface RawListingRow {
listingId: string;

View File

@@ -1,6 +1,6 @@
import { Injectable, type OnModuleInit } from '@nestjs/common';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Client as TypesenseClient } from 'typesense';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
@Injectable()
export class TypesenseClientService implements OnModuleInit {

View File

@@ -1,14 +1,14 @@
import { Injectable } from '@nestjs/common';
import { type Client as TypesenseClient } from 'typesense';
import { type CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
import { type LoggerService } from '@modules/shared';
import { Client as TypesenseClient } from 'typesense';
import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
import { LoggerService } from '@modules/shared';
import {
type ISearchRepository,
ISearchRepository,
type ListingDocument,
type SearchParams,
type SearchResult,
} from '../../domain/repositories/search.repository';
import { type TypesenseClientService } from './typesense-client.service';
import { TypesenseClientService } from './typesense-client.service';
const COLLECTION_NAME = 'listings';

View File

@@ -9,7 +9,7 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import { type CommandBus, type QueryBus } from '@nestjs/cqrs';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import {
ApiTags,
ApiOperation,
@@ -17,17 +17,17 @@ import {
ApiBearerAuth,
ApiParam,
} from '@nestjs/swagger';
import { type JwtPayload, CurrentUser, JwtAuthGuard } from '@modules/auth';
import { JwtPayload, CurrentUser, JwtAuthGuard } from '@modules/auth';
import { CreateSavedSearchCommand } from '../../application/commands/create-saved-search/create-saved-search.command';
import { type CreateSavedSearchResult } from '../../application/commands/create-saved-search/create-saved-search.handler';
import { CreateSavedSearchResult } from '../../application/commands/create-saved-search/create-saved-search.handler';
import { DeleteSavedSearchCommand } from '../../application/commands/delete-saved-search/delete-saved-search.command';
import { UpdateSavedSearchCommand } from '../../application/commands/update-saved-search/update-saved-search.command';
import { type UpdateSavedSearchResult } from '../../application/commands/update-saved-search/update-saved-search.handler';
import { type SavedSearchDetail } from '../../application/queries/get-saved-search/get-saved-search.handler';
import { UpdateSavedSearchResult } from '../../application/commands/update-saved-search/update-saved-search.handler';
import { SavedSearchDetail } from '../../application/queries/get-saved-search/get-saved-search.handler';
import { GetSavedSearchQuery } from '../../application/queries/get-saved-search/get-saved-search.query';
import { type SavedSearchListResult } from '../../application/queries/get-saved-searches/get-saved-searches.handler';
import { SavedSearchListResult } from '../../application/queries/get-saved-searches/get-saved-searches.handler';
import { GetSavedSearchesQuery } from '../../application/queries/get-saved-searches/get-saved-searches.query';
import { type CreateSavedSearchDto, type UpdateSavedSearchDto, type SavedSearchListDto } from '../dto/saved-search.dto';
import { CreateSavedSearchDto, UpdateSavedSearchDto, SavedSearchListDto } from '../dto/saved-search.dto';
@ApiTags('saved-searches')
@ApiBearerAuth('JWT')

View File

@@ -5,7 +5,7 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import { type CommandBus, type QueryBus } from '@nestjs/cqrs';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import {
ApiTags,
ApiOperation,
@@ -15,12 +15,12 @@ import {
import { Roles, JwtAuthGuard, RolesGuard } from '@modules/auth';
import { EndpointRateLimit, EndpointRateLimitGuard } from '@modules/shared';
import { ReindexAllCommand } from '../../application/commands/reindex-all/reindex-all.command';
import { type ReindexResult } from '../../application/commands/reindex-all/reindex-all.handler';
import { ReindexResult } from '../../application/commands/reindex-all/reindex-all.handler';
import { GeoSearchQuery } from '../../application/queries/geo-search/geo-search.query';
import { SearchPropertiesQuery } from '../../application/queries/search-properties/search-properties.query';
import { type SearchResult } from '../../domain/repositories/search.repository';
import { type GeoSearchDto } from '../dto/geo-search.dto';
import { type SearchPropertiesDto } from '../dto/search-properties.dto';
import { SearchResult } from '../../domain/repositories/search.repository';
import { GeoSearchDto } from '../dto/geo-search.dto';
import { SearchPropertiesDto } from '../dto/search-properties.dto';
@ApiTags('search')
@Controller('search')

View File

@@ -1,7 +1,7 @@
import { Module, type OnModuleInit } from '@nestjs/common';
import { Module, OnModuleInit } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { makeCounterProvider } from '@willsoto/nestjs-prometheus';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
import { CreateSavedSearchHandler } from './application/commands/create-saved-search/create-saved-search.handler';
import { DeleteSavedSearchHandler } from './application/commands/delete-saved-search/delete-saved-search.handler';
import { ReindexAllHandler } from './application/commands/reindex-all/reindex-all.handler';