inmemory to postgres

This commit is contained in:
2025-12-29 18:34:12 +01:00
parent 9e17d0752a
commit f5639a367f
176 changed files with 10175 additions and 468 deletions

View File

@@ -2,7 +2,6 @@ import { Test, TestingModule } from '@nestjs/testing';
import { AnalyticsModule } from './AnalyticsModule';
import { AnalyticsController } from './AnalyticsController';
import { AnalyticsService } from './AnalyticsService';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
describe('AnalyticsModule', () => {
let module: TestingModule;
@@ -10,10 +9,7 @@ describe('AnalyticsModule', () => {
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [AnalyticsModule],
})
.overrideProvider('Logger_TOKEN')
.useClass(ConsoleLogger)
.compile();
}).compile();
});
it('should compile the module', () => {

View File

@@ -1,10 +1,12 @@
import { Module } from '@nestjs/common';
import { AnalyticsPersistenceModule } from '../../persistence/analytics/AnalyticsPersistenceModule';
import { AnalyticsController } from './AnalyticsController';
import { AnalyticsService } from './AnalyticsService';
import { AnalyticsProviders } from './AnalyticsProviders';
@Module({
imports: [],
imports: [AnalyticsPersistenceModule],
controllers: [AnalyticsController],
providers: AnalyticsProviders,
exports: [AnalyticsService],

View File

@@ -6,18 +6,18 @@ import type { IPageViewRepository } from '@core/analytics/application/repositori
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import { Provider } from '@nestjs/common';
const Logger_TOKEN = 'Logger_TOKEN';
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
import {
ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN,
ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN,
} from '../../persistence/analytics/AnalyticsPersistenceTokens';
const LOGGER_TOKEN = 'Logger';
const RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN = 'RecordPageViewOutputPort_TOKEN';
const RECORD_ENGAGEMENT_OUTPUT_PORT_TOKEN = 'RecordEngagementOutputPort_TOKEN';
const GET_DASHBOARD_DATA_OUTPUT_PORT_TOKEN = 'GetDashboardDataOutputPort_TOKEN';
const GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN = 'GetAnalyticsMetricsOutputPort_TOKEN';
import { InMemoryEngagementRepository } from '@adapters/analytics/persistence/inmemory/InMemoryEngagementRepository';
import { InMemoryPageViewRepository } from '@adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { GetAnalyticsMetricsUseCase } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
import { GetDashboardDataOutput, GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
import { RecordEngagementUseCase } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
@@ -34,20 +34,6 @@ export const AnalyticsProviders: Provider[] = [
RecordEngagementPresenter,
GetDashboardDataPresenter,
GetAnalyticsMetricsPresenter,
{
provide: Logger_TOKEN,
useClass: ConsoleLogger,
},
{
provide: IPAGE_VIEW_REPO_TOKEN,
useFactory: (logger: Logger) => new InMemoryPageViewRepository(logger),
inject: [Logger_TOKEN],
},
{
provide: IENGAGEMENT_REPO_TOKEN,
useFactory: (logger: Logger) => new InMemoryEngagementRepository(logger),
inject: [Logger_TOKEN],
},
{
provide: RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN,
useExisting: RecordPageViewPresenter,
@@ -68,24 +54,24 @@ export const AnalyticsProviders: Provider[] = [
provide: RecordPageViewUseCase,
useFactory: (repo: IPageViewRepository, logger: Logger, output: UseCaseOutputPort<RecordPageViewOutput>) =>
new RecordPageViewUseCase(repo, logger, output),
inject: [IPAGE_VIEW_REPO_TOKEN, Logger_TOKEN, RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN],
inject: [ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN, LOGGER_TOKEN, RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN],
},
{
provide: RecordEngagementUseCase,
useFactory: (repo: IEngagementRepository, logger: Logger, output: UseCaseOutputPort<RecordEngagementOutput>) =>
new RecordEngagementUseCase(repo, logger, output),
inject: [IENGAGEMENT_REPO_TOKEN, Logger_TOKEN, RECORD_ENGAGEMENT_OUTPUT_PORT_TOKEN],
inject: [ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN, LOGGER_TOKEN, RECORD_ENGAGEMENT_OUTPUT_PORT_TOKEN],
},
{
provide: GetDashboardDataUseCase,
useFactory: (logger: Logger, output: UseCaseOutputPort<GetDashboardDataOutput>) =>
new GetDashboardDataUseCase(logger, output),
inject: [Logger_TOKEN, GET_DASHBOARD_DATA_OUTPUT_PORT_TOKEN],
inject: [LOGGER_TOKEN, GET_DASHBOARD_DATA_OUTPUT_PORT_TOKEN],
},
{
provide: GetAnalyticsMetricsUseCase,
useFactory: (logger: Logger, output: UseCaseOutputPort<GetAnalyticsMetricsOutput>, repo: IPageViewRepository) =>
new GetAnalyticsMetricsUseCase(logger, output, repo),
inject: [Logger_TOKEN, GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN, IPAGE_VIEW_REPO_TOKEN],
inject: [LOGGER_TOKEN, GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN, ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN],
},
];

View File

@@ -1,4 +1,5 @@
import { Module } from '@nestjs/common';
import { IdentityPersistenceModule } from '../../persistence/identity/IdentityPersistenceModule';
import { AuthService } from './AuthService';
import { AuthController } from './AuthController';
import { AuthProviders } from './AuthProviders';
@@ -7,14 +8,9 @@ import { AuthorizationGuard } from './AuthorizationGuard';
import { AuthorizationService } from './AuthorizationService';
@Module({
imports: [IdentityPersistenceModule],
controllers: [AuthController],
providers: [
AuthService,
...AuthProviders,
AuthenticationGuard,
AuthorizationService,
AuthorizationGuard,
],
providers: [AuthService, ...AuthProviders, AuthenticationGuard, AuthorizationService, AuthorizationGuard],
exports: [AuthService, AuthenticationGuard, AuthorizationService, AuthorizationGuard],
})
export class AuthModule {}

View File

@@ -1,30 +1,28 @@
import { Provider } from '@nestjs/common';
// Import interfaces and concrete implementations
import { StoredUser } from '@core/identity/domain/repositories/IUserRepository';
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
import { InMemoryAuthRepository } from '@adapters/identity/persistence/inmemory/InMemoryAuthRepository';
import { InMemoryUserRepository } from '@adapters/identity/persistence/inmemory/InMemoryUserRepository';
import { InMemoryPasswordHashingService } from '@adapters/identity/services/InMemoryPasswordHashingService';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { CookieIdentitySessionAdapter } from '@adapters/identity/session/CookieIdentitySessionAdapter';
import { LoginUseCase } from '@core/identity/application/use-cases/LoginUseCase';
import { LogoutUseCase } from '@core/identity/application/use-cases/LogoutUseCase';
import { SignupUseCase } from '@core/identity/application/use-cases/SignupUseCase';
import type { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort';
import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository';
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
import type { LoginResult } from '@core/identity/application/use-cases/LoginUseCase';
import type { LogoutResult } from '@core/identity/application/use-cases/LogoutUseCase';
import type { SignupResult } from '@core/identity/application/use-cases/SignupUseCase';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import {
AUTH_REPOSITORY_TOKEN,
PASSWORD_HASHING_SERVICE_TOKEN,
USER_REPOSITORY_TOKEN,
} from '../../persistence/identity/IdentityPersistenceTokens';
import { AuthSessionPresenter } from './presenters/AuthSessionPresenter';
import { CommandResultPresenter } from './presenters/CommandResultPresenter';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import type { LoginResult } from '@core/identity/application/use-cases/LoginUseCase';
import type { SignupResult } from '@core/identity/application/use-cases/SignupUseCase';
import type { LogoutResult } from '@core/identity/application/use-cases/LogoutUseCase';
import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository';
import type { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort';
// Define the tokens for dependency injection
export const AUTH_REPOSITORY_TOKEN = 'IAuthRepository';
export const USER_REPOSITORY_TOKEN = 'IUserRepository';
export const PASSWORD_HASHING_SERVICE_TOKEN = 'IPasswordHashingService';
export { AUTH_REPOSITORY_TOKEN, USER_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN };
export const LOGGER_TOKEN = 'Logger';
export const IDENTITY_SESSION_PORT_TOKEN = 'IdentitySessionPort';
export const LOGIN_USE_CASE_TOKEN = 'LoginUseCase';
@@ -35,38 +33,6 @@ export const AUTH_SESSION_OUTPUT_PORT_TOKEN = 'AuthSessionOutputPort';
export const COMMAND_RESULT_OUTPUT_PORT_TOKEN = 'CommandResultOutputPort';
export const AuthProviders: Provider[] = [
{
provide: AUTH_REPOSITORY_TOKEN,
useFactory: (userRepository: InMemoryUserRepository, passwordHashingService: IPasswordHashingService, logger: Logger) =>
new InMemoryAuthRepository(userRepository, passwordHashingService, logger),
inject: [USER_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
},
{
provide: USER_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => {
const initialUsers: StoredUser[] = [
{
// Match seeded racing driver id so dashboard works in inmemory mode.
id: 'driver-1',
email: 'admin@gridpilot.local',
passwordHash: 'demo_salt_321nimda', // InMemoryPasswordHashingService: "admin123" reversed.
displayName: 'Admin',
salt: '',
createdAt: new Date(),
},
];
return new InMemoryUserRepository(logger, initialUsers);
},
inject: [LOGGER_TOKEN],
},
{
provide: PASSWORD_HASHING_SERVICE_TOKEN,
useClass: InMemoryPasswordHashingService,
},
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
{
provide: IDENTITY_SESSION_PORT_TOKEN,
useFactory: (logger: Logger) => new CookieIdentitySessionAdapter(logger),

View File

@@ -4,11 +4,11 @@ import { SeedRacingData, type RacingSeedDependencies } from '../../../../../adap
import { Inject, Module, OnModuleInit } from '@nestjs/common';
import { getApiPersistence, getEnableBootstrap } from '../../env';
import { RacingPersistenceModule } from '../../persistence/racing/RacingPersistenceModule';
import { InMemorySocialPersistenceModule } from '../../persistence/inmemory/InMemorySocialPersistenceModule';
import { SocialPersistenceModule } from '../../persistence/social/SocialPersistenceModule';
import { BootstrapProviders, ENSURE_INITIAL_DATA_TOKEN } from './BootstrapProviders';
@Module({
imports: [RacingPersistenceModule, InMemorySocialPersistenceModule],
imports: [RacingPersistenceModule, SocialPersistenceModule],
providers: BootstrapProviders,
})
export class BootstrapModule implements OnModuleInit {

View File

@@ -1,4 +1,5 @@
import { Provider } from '@nestjs/common';
import { SOCIAL_FEED_REPOSITORY_TOKEN, SOCIAL_GRAPH_REPOSITORY_TOKEN } from '../../persistence/social/SocialPersistenceTokens';
import { EnsureInitialData } from '../../../../../adapters/bootstrap/EnsureInitialData';
import type { RacingSeedDependencies } from '../../../../../adapters/bootstrap/SeedRacingData';
import { SignupWithEmailUseCase, type SignupWithEmailResult } from '@core/identity/application/use-cases/SignupWithEmailUseCase';
@@ -105,8 +106,8 @@ export const BootstrapProviders: Provider[] = [
'ITeamRepository',
'ITeamMembershipRepository',
'ISponsorRepository',
'IFeedRepository',
'ISocialGraphRepository',
SOCIAL_FEED_REPOSITORY_TOKEN,
SOCIAL_GRAPH_REPOSITORY_TOKEN,
],
},
{

View File

@@ -1,12 +1,12 @@
import { Module } from '@nestjs/common';
import { RacingPersistenceModule } from '../../persistence/racing/RacingPersistenceModule';
import { InMemorySocialPersistenceModule } from '../../persistence/inmemory/InMemorySocialPersistenceModule';
import { SocialPersistenceModule } from '../../persistence/social/SocialPersistenceModule';
import { DashboardService } from './DashboardService';
import { DashboardController } from './DashboardController';
import { DashboardProviders } from './DashboardProviders';
@Module({
imports: [RacingPersistenceModule, InMemorySocialPersistenceModule],
imports: [RacingPersistenceModule, SocialPersistenceModule],
controllers: [DashboardController],
providers: [DashboardService, ...DashboardProviders],
exports: [DashboardService],

View File

@@ -11,6 +11,8 @@ import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/IL
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import { SOCIAL_FEED_REPOSITORY_TOKEN, SOCIAL_GRAPH_REPOSITORY_TOKEN } from '../../persistence/social/SocialPersistenceTokens';
import { ImageServicePort } from '@core/media/application/ports/ImageServicePort';
import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
@@ -28,8 +30,6 @@ export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
export const STANDING_REPOSITORY_TOKEN = 'IStandingRepository';
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
export const FEED_REPOSITORY_TOKEN = 'IFeedRepository';
export const SOCIAL_GRAPH_REPOSITORY_TOKEN = 'ISocialGraphRepository';
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
export const DASHBOARD_OVERVIEW_USE_CASE_TOKEN = 'DashboardOverviewUseCase';
export const DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN = 'DashboardOverviewOutputPort';
@@ -86,7 +86,7 @@ export const DashboardProviders: Provider[] = [
STANDING_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
RACE_REGISTRATION_REPOSITORY_TOKEN,
FEED_REPOSITORY_TOKEN,
SOCIAL_FEED_REPOSITORY_TOKEN,
SOCIAL_GRAPH_REPOSITORY_TOKEN,
IMAGE_SERVICE_TOKEN,
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,

View File

@@ -1,12 +1,12 @@
import { Module } from '@nestjs/common';
import { RacingPersistenceModule } from '../../persistence/racing/RacingPersistenceModule';
import { InMemorySocialPersistenceModule } from '../../persistence/inmemory/InMemorySocialPersistenceModule';
import { SocialPersistenceModule } from '../../persistence/social/SocialPersistenceModule';
import { DriverService } from './DriverService';
import { DriverController } from './DriverController';
import { DriverProviders } from './DriverProviders';
@Module({
imports: [RacingPersistenceModule, InMemorySocialPersistenceModule],
imports: [RacingPersistenceModule, SocialPersistenceModule],
controllers: [DriverController],
providers: [DriverService, ...DriverProviders],
exports: [DriverService],

View File

@@ -7,8 +7,10 @@ export const IMAGE_SERVICE_PORT_TOKEN = 'IImageServicePort';
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
export const NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreferenceRepository';
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
import { SOCIAL_GRAPH_REPOSITORY_TOKEN } from '../../persistence/social/SocialPersistenceTokens';
export const TEAM_MEMBERSHIP_REPOSITORY_TOKEN = 'ITeamMembershipRepository';
export const SOCIAL_GRAPH_REPOSITORY_TOKEN = 'ISocialGraphRepository';
export { SOCIAL_GRAPH_REPOSITORY_TOKEN };
export const LOGGER_TOKEN = 'Logger';
export const GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN = 'GetDriversLeaderboardUseCase';

View File

@@ -1,9 +1,13 @@
import { Module } from '@nestjs/common';
import { PaymentsPersistenceModule } from '../../persistence/payments/PaymentsPersistenceModule';
import { PaymentsService } from './PaymentsService';
import { PaymentsController } from './PaymentsController';
import { PaymentsProviders } from './PaymentsProviders';
@Module({
imports: [PaymentsPersistenceModule],
controllers: [PaymentsController],
providers: [PaymentsService, ...PaymentsProviders],
exports: [PaymentsService],

View File

@@ -5,7 +5,7 @@ import type { IPaymentRepository } from '@core/payments/domain/repositories/IPay
import type { IMembershipFeeRepository, IMemberPaymentRepository } from '@core/payments/domain/repositories/IMembershipFeeRepository';
import type { IPrizeRepository } from '@core/payments/domain/repositories/IPrizeRepository';
import type { IWalletRepository, ITransactionRepository } from '@core/payments/domain/repositories/IWalletRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application';
// Import use cases
import { GetPaymentsUseCase } from '@core/payments/application/use-cases/GetPaymentsUseCase';
@@ -22,10 +22,6 @@ import { GetWalletUseCase } from '@core/payments/application/use-cases/GetWallet
import { ProcessWalletTransactionUseCase } from '@core/payments/application/use-cases/ProcessWalletTransactionUseCase';
// Import concrete in-memory implementations
import { InMemoryPaymentRepository } from '@adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
import { InMemoryMembershipFeeRepository, InMemoryMemberPaymentRepository } from '@adapters/payments/persistence/inmemory/InMemoryMembershipFeeRepository';
import { InMemoryPrizeRepository } from '@adapters/payments/persistence/inmemory/InMemoryPrizeRepository';
import { InMemoryWalletRepository, InMemoryTransactionRepository } from '@adapters/payments/persistence/inmemory/InMemoryWalletRepository';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Presenters
@@ -150,37 +146,7 @@ export const PaymentsProviders: Provider[] = [
useClass: ConsoleLogger,
},
// Repositories (repositories are injected into use cases, NOT into services)
{
provide: PAYMENT_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryPaymentRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: MEMBERSHIP_FEE_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryMembershipFeeRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: MEMBER_PAYMENT_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryMemberPaymentRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: PRIZE_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryPrizeRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: WALLET_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryWalletRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: TRANSACTION_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryTransactionRepository(logger),
inject: [LOGGER_TOKEN],
},
// Repositories are provided by PaymentsPersistenceModule behind tokens.
// Use cases (use cases receive repositories, services receive use cases)
{

View File

@@ -1,9 +1,19 @@
export const PAYMENT_REPOSITORY_TOKEN = 'IPaymentRepository';
export const MEMBERSHIP_FEE_REPOSITORY_TOKEN = 'IMembershipFeeRepository';
export const MEMBER_PAYMENT_REPOSITORY_TOKEN = 'IMemberPaymentRepository';
export const PRIZE_REPOSITORY_TOKEN = 'IPrizeRepository';
export const WALLET_REPOSITORY_TOKEN = 'IWalletRepository';
export const TRANSACTION_REPOSITORY_TOKEN = 'ITransactionRepository';
import {
PAYMENTS_MEMBER_PAYMENT_REPOSITORY_TOKEN,
PAYMENTS_MEMBERSHIP_FEE_REPOSITORY_TOKEN,
PAYMENTS_PAYMENT_REPOSITORY_TOKEN,
PAYMENTS_PRIZE_REPOSITORY_TOKEN,
PAYMENTS_TRANSACTION_REPOSITORY_TOKEN,
PAYMENTS_WALLET_REPOSITORY_TOKEN,
} from '../../persistence/payments/PaymentsPersistenceTokens';
export const PAYMENT_REPOSITORY_TOKEN = PAYMENTS_PAYMENT_REPOSITORY_TOKEN;
export const MEMBERSHIP_FEE_REPOSITORY_TOKEN = PAYMENTS_MEMBERSHIP_FEE_REPOSITORY_TOKEN;
export const MEMBER_PAYMENT_REPOSITORY_TOKEN = PAYMENTS_MEMBER_PAYMENT_REPOSITORY_TOKEN;
export const PRIZE_REPOSITORY_TOKEN = PAYMENTS_PRIZE_REPOSITORY_TOKEN;
export const WALLET_REPOSITORY_TOKEN = PAYMENTS_WALLET_REPOSITORY_TOKEN;
export const TRANSACTION_REPOSITORY_TOKEN = PAYMENTS_TRANSACTION_REPOSITORY_TOKEN;
export const LOGGER_TOKEN = 'Logger';
export const GET_PAYMENTS_USE_CASE_TOKEN = 'GetPaymentsUseCase';

View File

@@ -36,6 +36,11 @@ export function getApiPersistence(): ApiPersistence {
return requireOneOf('GRIDPILOT_API_PERSISTENCE', configured, ['postgres', 'inmemory'] as const);
}
// Tests should default to in-memory even when DATABASE_URL exists, unless explicitly overridden.
if (process.env.NODE_ENV === 'test') {
return 'inmemory';
}
return process.env.DATABASE_URL ? 'postgres' : 'inmemory';
}

View File

@@ -0,0 +1,93 @@
import 'reflect-metadata';
import { Test } from '@nestjs/testing';
import type { TestingModule } from '@nestjs/testing';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN,
ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN,
ANALYTICS_SNAPSHOT_REPOSITORY_TOKEN,
} from './AnalyticsPersistenceTokens';
describe('AnalyticsPersistenceModule', () => {
const originalEnv = { ...process.env };
afterEach(() => {
process.env = originalEnv;
delete (globalThis as { __GP_TEST_DATA_SOURCE__?: unknown }).__GP_TEST_DATA_SOURCE__;
vi.restoreAllMocks();
vi.resetModules();
vi.unmock('@nestjs/typeorm');
});
it('uses inmemory providers when GRIDPILOT_API_PERSISTENCE=inmemory', async () => {
vi.resetModules();
process.env.GRIDPILOT_API_PERSISTENCE = 'inmemory';
delete process.env.DATABASE_URL;
const { AnalyticsPersistenceModule } = await import('./AnalyticsPersistenceModule');
const { InMemoryPageViewRepository } = await import('@adapters/analytics/persistence/inmemory/InMemoryPageViewRepository');
const { InMemoryEngagementRepository } = await import('@adapters/analytics/persistence/inmemory/InMemoryEngagementRepository');
const { InMemoryAnalyticsSnapshotRepository } = await import('@adapters/analytics/persistence/inmemory/InMemoryAnalyticsSnapshotRepository');
const module: TestingModule = await Test.createTestingModule({
imports: [AnalyticsPersistenceModule],
}).compile();
expect(module.get(ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN)).toBeInstanceOf(InMemoryPageViewRepository);
expect(module.get(ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN)).toBeInstanceOf(InMemoryEngagementRepository);
expect(module.get(ANALYTICS_SNAPSHOT_REPOSITORY_TOKEN)).toBeInstanceOf(InMemoryAnalyticsSnapshotRepository);
await module.close();
});
it('uses postgres providers when GRIDPILOT_API_PERSISTENCE=postgres', async () => {
vi.resetModules();
process.env.GRIDPILOT_API_PERSISTENCE = 'postgres';
delete process.env.DATABASE_URL;
const DATA_SOURCE_TOKEN = 'TEST_DATA_SOURCE_TOKEN';
const fakeDataSource = {
getRepository: () => ({}),
};
(globalThis as { __GP_TEST_DATA_SOURCE__?: unknown }).__GP_TEST_DATA_SOURCE__ = fakeDataSource;
vi.doMock('@nestjs/typeorm', async () => {
return {
TypeOrmModule: {
forFeature: () => ({
module: class TypeOrmFeatureStubModule {},
providers: [
{
provide: DATA_SOURCE_TOKEN,
useValue: (globalThis as { __GP_TEST_DATA_SOURCE__?: unknown }).__GP_TEST_DATA_SOURCE__,
},
],
exports: [DATA_SOURCE_TOKEN],
}),
},
getDataSourceToken: () => DATA_SOURCE_TOKEN,
};
});
const { AnalyticsPersistenceModule } = await import('./AnalyticsPersistenceModule');
const { TypeOrmPageViewRepository } = await import('@adapters/analytics/persistence/typeorm/repositories/TypeOrmPageViewRepository');
const { TypeOrmEngagementRepository } = await import('@adapters/analytics/persistence/typeorm/repositories/TypeOrmEngagementRepository');
const { TypeOrmAnalyticsSnapshotRepository } = await import('@adapters/analytics/persistence/typeorm/repositories/TypeOrmAnalyticsSnapshotRepository');
const module: TestingModule = await Test.createTestingModule({
imports: [AnalyticsPersistenceModule],
}).compile();
expect(module.get(ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN)).toBeInstanceOf(TypeOrmPageViewRepository);
expect(module.get(ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN)).toBeInstanceOf(TypeOrmEngagementRepository);
expect(module.get(ANALYTICS_SNAPSHOT_REPOSITORY_TOKEN)).toBeInstanceOf(TypeOrmAnalyticsSnapshotRepository);
await module.close();
});
});

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { getApiPersistence } from '../../env';
import { InMemoryAnalyticsPersistenceModule } from '../inmemory/InMemoryAnalyticsPersistenceModule';
import { PostgresAnalyticsPersistenceModule } from '../postgres/PostgresAnalyticsPersistenceModule';
const selectedPersistenceModule =
getApiPersistence() === 'postgres' ? PostgresAnalyticsPersistenceModule : InMemoryAnalyticsPersistenceModule;
@Module({
imports: [selectedPersistenceModule],
exports: [selectedPersistenceModule],
})
export class AnalyticsPersistenceModule {}

View File

@@ -0,0 +1,3 @@
export const ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN = 'ANALYTICS_IPageViewRepository';
export const ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN = 'ANALYTICS_IEngagementRepository';
export const ANALYTICS_SNAPSHOT_REPOSITORY_TOKEN = 'ANALYTICS_IAnalyticsSnapshotRepository';

View File

@@ -0,0 +1,49 @@
import 'reflect-metadata';
import { MODULE_METADATA } from '@nestjs/common/constants';
import { Test } from '@nestjs/testing';
import type { TestingModule } from '@nestjs/testing';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { USER_REPOSITORY_TOKEN } from './IdentityPersistenceTokens';
describe('IdentityPersistenceModule', () => {
const originalEnv = { ...process.env };
afterEach(() => {
process.env = originalEnv;
vi.restoreAllMocks();
});
it('uses inmemory providers when GRIDPILOT_API_PERSISTENCE=inmemory', async () => {
vi.resetModules();
process.env.GRIDPILOT_API_PERSISTENCE = 'inmemory';
delete process.env.DATABASE_URL;
const { IdentityPersistenceModule } = await import('./IdentityPersistenceModule');
const { InMemoryUserRepository } = await import('@adapters/identity/persistence/inmemory/InMemoryUserRepository');
const module: TestingModule = await Test.createTestingModule({
imports: [IdentityPersistenceModule],
}).compile();
const userRepo = module.get(USER_REPOSITORY_TOKEN);
expect(userRepo).toBeInstanceOf(InMemoryUserRepository);
await module.close();
});
it('uses postgres module when GRIDPILOT_API_PERSISTENCE=postgres', async () => {
vi.resetModules();
process.env.GRIDPILOT_API_PERSISTENCE = 'postgres';
delete process.env.DATABASE_URL;
const { IdentityPersistenceModule } = await import('./IdentityPersistenceModule');
const { PostgresIdentityPersistenceModule } = await import('../postgres/PostgresIdentityPersistenceModule');
const imports = Reflect.getMetadata(MODULE_METADATA.IMPORTS, IdentityPersistenceModule) as unknown[];
expect(imports).toContain(PostgresIdentityPersistenceModule);
});
});

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { getApiPersistence } from '../../env';
import { InMemoryIdentityPersistenceModule } from '../inmemory/InMemoryIdentityPersistenceModule';
import { PostgresIdentityPersistenceModule } from '../postgres/PostgresIdentityPersistenceModule';
const selectedPersistenceModule =
getApiPersistence() === 'postgres' ? PostgresIdentityPersistenceModule : InMemoryIdentityPersistenceModule;
@Module({
imports: [selectedPersistenceModule],
exports: [selectedPersistenceModule],
})
export class IdentityPersistenceModule {}

View File

@@ -0,0 +1,3 @@
export const AUTH_REPOSITORY_TOKEN = 'IAuthRepository';
export const USER_REPOSITORY_TOKEN = 'IUserRepository';
export const PASSWORD_HASHING_SERVICE_TOKEN = 'IPasswordHashingService';

View File

@@ -0,0 +1,46 @@
import { Module } from '@nestjs/common';
import { LoggingModule } from '../../domain/logging/LoggingModule';
import type { Logger } from '@core/shared/application/Logger';
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
import type { IAnalyticsSnapshotRepository } from '@core/analytics/domain/repositories/IAnalyticsSnapshotRepository';
import type { IPageViewRepository } from '@core/analytics/application/repositories/IPageViewRepository';
import { InMemoryAnalyticsSnapshotRepository } from '@adapters/analytics/persistence/inmemory/InMemoryAnalyticsSnapshotRepository';
import { InMemoryEngagementRepository } from '@adapters/analytics/persistence/inmemory/InMemoryEngagementRepository';
import { InMemoryPageViewRepository } from '@adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
import {
ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN,
ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN,
ANALYTICS_SNAPSHOT_REPOSITORY_TOKEN,
} from '../analytics/AnalyticsPersistenceTokens';
@Module({
imports: [LoggingModule],
providers: [
{
provide: ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN,
useFactory: (logger: Logger): IPageViewRepository => new InMemoryPageViewRepository(logger),
inject: ['Logger'],
},
{
provide: ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN,
useFactory: (logger: Logger): IEngagementRepository => new InMemoryEngagementRepository(logger),
inject: ['Logger'],
},
{
provide: ANALYTICS_SNAPSHOT_REPOSITORY_TOKEN,
useFactory: (logger: Logger): IAnalyticsSnapshotRepository => new InMemoryAnalyticsSnapshotRepository(logger),
inject: ['Logger'],
},
],
exports: [
ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN,
ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN,
ANALYTICS_SNAPSHOT_REPOSITORY_TOKEN,
],
})
export class InMemoryAnalyticsPersistenceModule {}

View File

@@ -0,0 +1,49 @@
import { Module } from '@nestjs/common';
import { LoggingModule } from '../../domain/logging/LoggingModule';
import type { Logger } from '@core/shared/application/Logger';
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
import type { StoredUser } from '@core/identity/domain/repositories/IUserRepository';
import { InMemoryAuthRepository } from '@adapters/identity/persistence/inmemory/InMemoryAuthRepository';
import { InMemoryUserRepository } from '@adapters/identity/persistence/inmemory/InMemoryUserRepository';
import { InMemoryPasswordHashingService } from '@adapters/identity/services/InMemoryPasswordHashingService';
import { AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, USER_REPOSITORY_TOKEN } from '../identity/IdentityPersistenceTokens';
@Module({
imports: [LoggingModule],
providers: [
{
provide: USER_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => {
const initialUsers: StoredUser[] = [
{
// Match seeded racing driver id so dashboard works in inmemory mode.
id: 'driver-1',
email: 'admin@gridpilot.local',
passwordHash: 'demo_salt_321nimda', // InMemoryPasswordHashingService: "admin123" reversed.
displayName: 'Admin',
salt: '',
createdAt: new Date(),
},
];
return new InMemoryUserRepository(logger, initialUsers);
},
inject: ['Logger'],
},
{
provide: AUTH_REPOSITORY_TOKEN,
useFactory: (userRepository: InMemoryUserRepository, passwordHashingService: IPasswordHashingService, logger: Logger) =>
new InMemoryAuthRepository(userRepository, passwordHashingService, logger),
inject: [USER_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, 'Logger'],
},
{
provide: PASSWORD_HASHING_SERVICE_TOKEN,
useClass: InMemoryPasswordHashingService,
},
],
exports: [USER_REPOSITORY_TOKEN, AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN],
})
export class InMemoryIdentityPersistenceModule {}

View File

@@ -0,0 +1,72 @@
import { Module } from '@nestjs/common';
import { LoggingModule } from '../../domain/logging/LoggingModule';
import type { Logger } from '@core/shared/application/Logger';
import type { IPaymentRepository } from '@core/payments/domain/repositories/IPaymentRepository';
import type {
IMemberPaymentRepository,
IMembershipFeeRepository,
} from '@core/payments/domain/repositories/IMembershipFeeRepository';
import type { IPrizeRepository } from '@core/payments/domain/repositories/IPrizeRepository';
import type { ITransactionRepository, IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
import { InMemoryMembershipFeeRepository, InMemoryMemberPaymentRepository } from '@adapters/payments/persistence/inmemory/InMemoryMembershipFeeRepository';
import { InMemoryPaymentRepository } from '@adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
import { InMemoryPrizeRepository } from '@adapters/payments/persistence/inmemory/InMemoryPrizeRepository';
import { InMemoryTransactionRepository, InMemoryWalletRepository } from '@adapters/payments/persistence/inmemory/InMemoryWalletRepository';
import {
PAYMENTS_MEMBER_PAYMENT_REPOSITORY_TOKEN,
PAYMENTS_MEMBERSHIP_FEE_REPOSITORY_TOKEN,
PAYMENTS_PAYMENT_REPOSITORY_TOKEN,
PAYMENTS_PRIZE_REPOSITORY_TOKEN,
PAYMENTS_TRANSACTION_REPOSITORY_TOKEN,
PAYMENTS_WALLET_REPOSITORY_TOKEN,
} from '../payments/PaymentsPersistenceTokens';
@Module({
imports: [LoggingModule],
providers: [
{
provide: PAYMENTS_PAYMENT_REPOSITORY_TOKEN,
useFactory: (logger: Logger): IPaymentRepository => new InMemoryPaymentRepository(logger),
inject: ['Logger'],
},
{
provide: PAYMENTS_MEMBERSHIP_FEE_REPOSITORY_TOKEN,
useFactory: (logger: Logger): IMembershipFeeRepository => new InMemoryMembershipFeeRepository(logger),
inject: ['Logger'],
},
{
provide: PAYMENTS_MEMBER_PAYMENT_REPOSITORY_TOKEN,
useFactory: (logger: Logger): IMemberPaymentRepository => new InMemoryMemberPaymentRepository(logger),
inject: ['Logger'],
},
{
provide: PAYMENTS_PRIZE_REPOSITORY_TOKEN,
useFactory: (logger: Logger): IPrizeRepository => new InMemoryPrizeRepository(logger),
inject: ['Logger'],
},
{
provide: PAYMENTS_WALLET_REPOSITORY_TOKEN,
useFactory: (logger: Logger): IWalletRepository => new InMemoryWalletRepository(logger),
inject: ['Logger'],
},
{
provide: PAYMENTS_TRANSACTION_REPOSITORY_TOKEN,
useFactory: (logger: Logger): ITransactionRepository => new InMemoryTransactionRepository(logger),
inject: ['Logger'],
},
],
exports: [
PAYMENTS_PAYMENT_REPOSITORY_TOKEN,
PAYMENTS_MEMBERSHIP_FEE_REPOSITORY_TOKEN,
PAYMENTS_MEMBER_PAYMENT_REPOSITORY_TOKEN,
PAYMENTS_PRIZE_REPOSITORY_TOKEN,
PAYMENTS_WALLET_REPOSITORY_TOKEN,
PAYMENTS_TRANSACTION_REPOSITORY_TOKEN,
],
})
export class InMemoryPaymentsPersistenceModule {}

View File

@@ -12,14 +12,13 @@ import {
InMemorySocialGraphRepository,
} from '@adapters/social/persistence/inmemory/InMemorySocialAndFeed';
export const FEED_REPOSITORY_TOKEN = 'IFeedRepository';
export const SOCIAL_GRAPH_REPOSITORY_TOKEN = 'ISocialGraphRepository';
import { SOCIAL_FEED_REPOSITORY_TOKEN, SOCIAL_GRAPH_REPOSITORY_TOKEN } from '../social/SocialPersistenceTokens';
@Module({
imports: [LoggingModule],
providers: [
{
provide: FEED_REPOSITORY_TOKEN,
provide: SOCIAL_FEED_REPOSITORY_TOKEN,
useFactory: (logger: Logger): IFeedRepository =>
new InMemoryFeedRepository(logger, { drivers: [], friendships: [], feedEvents: [] }),
inject: ['Logger'],
@@ -31,6 +30,6 @@ export const SOCIAL_GRAPH_REPOSITORY_TOKEN = 'ISocialGraphRepository';
inject: ['Logger'],
},
],
exports: [FEED_REPOSITORY_TOKEN, SOCIAL_GRAPH_REPOSITORY_TOKEN],
exports: [SOCIAL_FEED_REPOSITORY_TOKEN, SOCIAL_GRAPH_REPOSITORY_TOKEN],
})
export class InMemorySocialPersistenceModule {}

View File

@@ -0,0 +1,49 @@
import 'reflect-metadata';
import { MODULE_METADATA } from '@nestjs/common/constants';
import { Test } from '@nestjs/testing';
import type { TestingModule } from '@nestjs/testing';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { PAYMENTS_WALLET_REPOSITORY_TOKEN } from './PaymentsPersistenceTokens';
describe('PaymentsPersistenceModule', () => {
const originalEnv = { ...process.env };
afterEach(() => {
process.env = originalEnv;
vi.restoreAllMocks();
});
it('uses inmemory providers when GRIDPILOT_API_PERSISTENCE=inmemory', async () => {
vi.resetModules();
process.env.GRIDPILOT_API_PERSISTENCE = 'inmemory';
delete process.env.DATABASE_URL;
const { PaymentsPersistenceModule } = await import('./PaymentsPersistenceModule');
const { InMemoryWalletRepository } = await import('@adapters/payments/persistence/inmemory/InMemoryWalletRepository');
const module: TestingModule = await Test.createTestingModule({
imports: [PaymentsPersistenceModule],
}).compile();
const walletRepo = module.get(PAYMENTS_WALLET_REPOSITORY_TOKEN);
expect(walletRepo).toBeInstanceOf(InMemoryWalletRepository);
await module.close();
});
it('uses postgres module when GRIDPILOT_API_PERSISTENCE=postgres', async () => {
vi.resetModules();
process.env.GRIDPILOT_API_PERSISTENCE = 'postgres';
delete process.env.DATABASE_URL;
const { PaymentsPersistenceModule } = await import('./PaymentsPersistenceModule');
const { PostgresPaymentsPersistenceModule } = await import('../postgres/PostgresPaymentsPersistenceModule');
const imports = Reflect.getMetadata(MODULE_METADATA.IMPORTS, PaymentsPersistenceModule) as unknown[];
expect(imports).toContain(PostgresPaymentsPersistenceModule);
});
});

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { getApiPersistence } from '../../env';
import { InMemoryPaymentsPersistenceModule } from '../inmemory/InMemoryPaymentsPersistenceModule';
import { PostgresPaymentsPersistenceModule } from '../postgres/PostgresPaymentsPersistenceModule';
const selectedPersistenceModule =
getApiPersistence() === 'postgres' ? PostgresPaymentsPersistenceModule : InMemoryPaymentsPersistenceModule;
@Module({
imports: [selectedPersistenceModule],
exports: [selectedPersistenceModule],
})
export class PaymentsPersistenceModule {}

View File

@@ -0,0 +1,6 @@
export const PAYMENTS_PAYMENT_REPOSITORY_TOKEN = 'PAYMENTS_IPaymentRepository';
export const PAYMENTS_MEMBERSHIP_FEE_REPOSITORY_TOKEN = 'PAYMENTS_IMembershipFeeRepository';
export const PAYMENTS_MEMBER_PAYMENT_REPOSITORY_TOKEN = 'PAYMENTS_IMemberPaymentRepository';
export const PAYMENTS_PRIZE_REPOSITORY_TOKEN = 'PAYMENTS_IPrizeRepository';
export const PAYMENTS_WALLET_REPOSITORY_TOKEN = 'PAYMENTS_IWalletRepository';
export const PAYMENTS_TRANSACTION_REPOSITORY_TOKEN = 'PAYMENTS_ITransactionRepository';

View File

@@ -0,0 +1,56 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule, getDataSourceToken } from '@nestjs/typeorm';
import type { DataSource } from 'typeorm';
import { LoggingModule } from '../../domain/logging/LoggingModule';
import {
ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN,
ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN,
ANALYTICS_SNAPSHOT_REPOSITORY_TOKEN,
} from '../analytics/AnalyticsPersistenceTokens';
import { AnalyticsSnapshotOrmEntity } from '@adapters/analytics/persistence/typeorm/entities/AnalyticsSnapshotOrmEntity';
import { EngagementEventOrmEntity } from '@adapters/analytics/persistence/typeorm/entities/EngagementEventOrmEntity';
import { PageViewOrmEntity } from '@adapters/analytics/persistence/typeorm/entities/PageViewOrmEntity';
import { AnalyticsSnapshotOrmMapper } from '@adapters/analytics/persistence/typeorm/mappers/AnalyticsSnapshotOrmMapper';
import { EngagementEventOrmMapper } from '@adapters/analytics/persistence/typeorm/mappers/EngagementEventOrmMapper';
import { PageViewOrmMapper } from '@adapters/analytics/persistence/typeorm/mappers/PageViewOrmMapper';
import { TypeOrmAnalyticsSnapshotRepository } from '@adapters/analytics/persistence/typeorm/repositories/TypeOrmAnalyticsSnapshotRepository';
import { TypeOrmEngagementRepository } from '@adapters/analytics/persistence/typeorm/repositories/TypeOrmEngagementRepository';
import { TypeOrmPageViewRepository } from '@adapters/analytics/persistence/typeorm/repositories/TypeOrmPageViewRepository';
const typeOrmFeatureImports = [
TypeOrmModule.forFeature([PageViewOrmEntity, EngagementEventOrmEntity, AnalyticsSnapshotOrmEntity]),
];
@Module({
imports: [LoggingModule, ...typeOrmFeatureImports],
providers: [
{ provide: PageViewOrmMapper, useFactory: () => new PageViewOrmMapper() },
{ provide: EngagementEventOrmMapper, useFactory: () => new EngagementEventOrmMapper() },
{ provide: AnalyticsSnapshotOrmMapper, useFactory: () => new AnalyticsSnapshotOrmMapper() },
{
provide: ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource, mapper: PageViewOrmMapper) =>
new TypeOrmPageViewRepository(dataSource.getRepository(PageViewOrmEntity), mapper),
inject: [getDataSourceToken(), PageViewOrmMapper],
},
{
provide: ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource, mapper: EngagementEventOrmMapper) =>
new TypeOrmEngagementRepository(dataSource.getRepository(EngagementEventOrmEntity), mapper),
inject: [getDataSourceToken(), EngagementEventOrmMapper],
},
{
provide: ANALYTICS_SNAPSHOT_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource, mapper: AnalyticsSnapshotOrmMapper) =>
new TypeOrmAnalyticsSnapshotRepository(dataSource.getRepository(AnalyticsSnapshotOrmEntity), mapper),
inject: [getDataSourceToken(), AnalyticsSnapshotOrmMapper],
},
],
exports: [ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN, ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN, ANALYTICS_SNAPSHOT_REPOSITORY_TOKEN],
})
export class PostgresAnalyticsPersistenceModule {}

View File

@@ -0,0 +1,40 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule, getDataSourceToken } from '@nestjs/typeorm';
import type { DataSource } from 'typeorm';
import { UserOrmEntity } from '@adapters/identity/persistence/typeorm/entities/UserOrmEntity';
import { TypeOrmAuthRepository } from '@adapters/identity/persistence/typeorm/TypeOrmAuthRepository';
import { TypeOrmUserRepository } from '@adapters/identity/persistence/typeorm/TypeOrmUserRepository';
import { UserOrmMapper } from '@adapters/identity/persistence/typeorm/mappers/UserOrmMapper';
import { InMemoryPasswordHashingService } from '@adapters/identity/services/InMemoryPasswordHashingService';
import {
AUTH_REPOSITORY_TOKEN,
PASSWORD_HASHING_SERVICE_TOKEN,
USER_REPOSITORY_TOKEN,
} from '../identity/IdentityPersistenceTokens';
const typeOrmFeatureImports = [TypeOrmModule.forFeature([UserOrmEntity])];
@Module({
imports: [...typeOrmFeatureImports],
providers: [
{ provide: UserOrmMapper, useFactory: () => new UserOrmMapper() },
{
provide: USER_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource, mapper: UserOrmMapper) => new TypeOrmUserRepository(dataSource, mapper),
inject: [getDataSourceToken(), UserOrmMapper],
},
{
provide: AUTH_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource, mapper: UserOrmMapper) => new TypeOrmAuthRepository(dataSource, mapper),
inject: [getDataSourceToken(), UserOrmMapper],
},
{
provide: PASSWORD_HASHING_SERVICE_TOKEN,
useClass: InMemoryPasswordHashingService,
},
],
exports: [USER_REPOSITORY_TOKEN, AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN],
})
export class PostgresIdentityPersistenceModule {}

View File

@@ -0,0 +1,105 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule, getDataSourceToken } from '@nestjs/typeorm';
import type { DataSource } from 'typeorm';
import { LoggingModule } from '../../domain/logging/LoggingModule';
import type { IPaymentRepository } from '@core/payments/domain/repositories/IPaymentRepository';
import type { IMemberPaymentRepository, IMembershipFeeRepository } from '@core/payments/domain/repositories/IMembershipFeeRepository';
import type { IPrizeRepository } from '@core/payments/domain/repositories/IPrizeRepository';
import type { ITransactionRepository, IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
import { PaymentsMemberPaymentOrmEntity } from '@adapters/payments/persistence/typeorm/entities/PaymentsMemberPaymentOrmEntity';
import { PaymentsMembershipFeeOrmEntity } from '@adapters/payments/persistence/typeorm/entities/PaymentsMembershipFeeOrmEntity';
import { PaymentsPaymentOrmEntity } from '@adapters/payments/persistence/typeorm/entities/PaymentsPaymentOrmEntity';
import { PaymentsPrizeOrmEntity } from '@adapters/payments/persistence/typeorm/entities/PaymentsPrizeOrmEntity';
import { PaymentsTransactionOrmEntity } from '@adapters/payments/persistence/typeorm/entities/PaymentsTransactionOrmEntity';
import { PaymentsWalletOrmEntity } from '@adapters/payments/persistence/typeorm/entities/PaymentsWalletOrmEntity';
import { PaymentsMemberPaymentOrmMapper } from '@adapters/payments/persistence/typeorm/mappers/PaymentsMemberPaymentOrmMapper';
import { PaymentsMembershipFeeOrmMapper } from '@adapters/payments/persistence/typeorm/mappers/PaymentsMembershipFeeOrmMapper';
import { PaymentsPaymentOrmMapper } from '@adapters/payments/persistence/typeorm/mappers/PaymentsPaymentOrmMapper';
import { PaymentsPrizeOrmMapper } from '@adapters/payments/persistence/typeorm/mappers/PaymentsPrizeOrmMapper';
import { PaymentsWalletOrmMapper } from '@adapters/payments/persistence/typeorm/mappers/PaymentsWalletOrmMapper';
import { TypeOrmMemberPaymentRepository, TypeOrmMembershipFeeRepository } from '@adapters/payments/persistence/typeorm/repositories/TypeOrmMembershipFeeRepository';
import { TypeOrmPaymentRepository } from '@adapters/payments/persistence/typeorm/repositories/TypeOrmPaymentRepository';
import { TypeOrmPrizeRepository } from '@adapters/payments/persistence/typeorm/repositories/TypeOrmPrizeRepository';
import { TypeOrmTransactionRepository, TypeOrmWalletRepository } from '@adapters/payments/persistence/typeorm/repositories/TypeOrmWalletRepository';
import {
PAYMENTS_MEMBER_PAYMENT_REPOSITORY_TOKEN,
PAYMENTS_MEMBERSHIP_FEE_REPOSITORY_TOKEN,
PAYMENTS_PAYMENT_REPOSITORY_TOKEN,
PAYMENTS_PRIZE_REPOSITORY_TOKEN,
PAYMENTS_TRANSACTION_REPOSITORY_TOKEN,
PAYMENTS_WALLET_REPOSITORY_TOKEN,
} from '../payments/PaymentsPersistenceTokens';
const typeOrmFeatureImports = [
TypeOrmModule.forFeature([
PaymentsWalletOrmEntity,
PaymentsTransactionOrmEntity,
PaymentsPaymentOrmEntity,
PaymentsPrizeOrmEntity,
PaymentsMembershipFeeOrmEntity,
PaymentsMemberPaymentOrmEntity,
]),
];
@Module({
imports: [LoggingModule, ...typeOrmFeatureImports],
providers: [
{ provide: PaymentsWalletOrmMapper, useFactory: () => new PaymentsWalletOrmMapper() },
{ provide: PaymentsPaymentOrmMapper, useFactory: () => new PaymentsPaymentOrmMapper() },
{ provide: PaymentsPrizeOrmMapper, useFactory: () => new PaymentsPrizeOrmMapper() },
{ provide: PaymentsMembershipFeeOrmMapper, useFactory: () => new PaymentsMembershipFeeOrmMapper() },
{ provide: PaymentsMemberPaymentOrmMapper, useFactory: () => new PaymentsMemberPaymentOrmMapper() },
{
provide: PAYMENTS_WALLET_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource, mapper: PaymentsWalletOrmMapper): IWalletRepository =>
new TypeOrmWalletRepository(dataSource, mapper),
inject: [getDataSourceToken(), PaymentsWalletOrmMapper],
},
{
provide: PAYMENTS_TRANSACTION_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource, mapper: PaymentsWalletOrmMapper): ITransactionRepository =>
new TypeOrmTransactionRepository(dataSource, mapper),
inject: [getDataSourceToken(), PaymentsWalletOrmMapper],
},
{
provide: PAYMENTS_PAYMENT_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource, mapper: PaymentsPaymentOrmMapper): IPaymentRepository =>
new TypeOrmPaymentRepository(dataSource, mapper),
inject: [getDataSourceToken(), PaymentsPaymentOrmMapper],
},
{
provide: PAYMENTS_PRIZE_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource, mapper: PaymentsPrizeOrmMapper): IPrizeRepository =>
new TypeOrmPrizeRepository(dataSource, mapper),
inject: [getDataSourceToken(), PaymentsPrizeOrmMapper],
},
{
provide: PAYMENTS_MEMBERSHIP_FEE_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource, mapper: PaymentsMembershipFeeOrmMapper): IMembershipFeeRepository =>
new TypeOrmMembershipFeeRepository(dataSource, mapper),
inject: [getDataSourceToken(), PaymentsMembershipFeeOrmMapper],
},
{
provide: PAYMENTS_MEMBER_PAYMENT_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource, mapper: PaymentsMemberPaymentOrmMapper): IMemberPaymentRepository =>
new TypeOrmMemberPaymentRepository(dataSource, mapper),
inject: [getDataSourceToken(), PaymentsMemberPaymentOrmMapper],
},
],
exports: [
PAYMENTS_WALLET_REPOSITORY_TOKEN,
PAYMENTS_TRANSACTION_REPOSITORY_TOKEN,
PAYMENTS_PAYMENT_REPOSITORY_TOKEN,
PAYMENTS_PRIZE_REPOSITORY_TOKEN,
PAYMENTS_MEMBERSHIP_FEE_REPOSITORY_TOKEN,
PAYMENTS_MEMBER_PAYMENT_REPOSITORY_TOKEN,
],
})
export class PostgresPaymentsPersistenceModule {}

View File

@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule, getDataSourceToken } from '@nestjs/typeorm';
import type { DataSource } from 'typeorm';
import { TypeOrmModule, getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
import type { DataSource, Repository } from 'typeorm';
import { LoggingModule } from '../../domain/logging/LoggingModule';
@@ -27,112 +27,240 @@ import {
TRANSACTION_REPOSITORY_TOKEN,
} from '../inmemory/InMemoryRacingPersistenceModule';
import { DriverOrmEntity } from '@adapters/racing/persistence/typeorm/entities/DriverOrmEntity';
import { LeagueMembershipOrmEntity } from '@adapters/racing/persistence/typeorm/entities/LeagueMembershipOrmEntity';
import { LeagueOrmEntity } from '@adapters/racing/persistence/typeorm/entities/LeagueOrmEntity';
import { LeagueScoringConfigOrmEntity } from '@adapters/racing/persistence/typeorm/entities/LeagueScoringConfigOrmEntity';
import { RaceOrmEntity } from '@adapters/racing/persistence/typeorm/entities/RaceOrmEntity';
import { RaceRegistrationOrmEntity } from '@adapters/racing/persistence/typeorm/entities/RaceRegistrationOrmEntity';
import { SeasonOrmEntity } from '@adapters/racing/persistence/typeorm/entities/SeasonOrmEntity';
import { ResultOrmEntity } from '@adapters/racing/persistence/typeorm/entities/ResultOrmEntity';
import { StandingOrmEntity } from '@adapters/racing/persistence/typeorm/entities/StandingOrmEntity';
import {
GameOrmEntity,
LeagueWalletOrmEntity,
PenaltyOrmEntity,
ProtestOrmEntity,
SeasonSponsorshipOrmEntity,
SponsorOrmEntity,
SponsorshipPricingOrmEntity,
SponsorshipRequestOrmEntity,
TransactionOrmEntity,
} from '@adapters/racing/persistence/typeorm/entities/MissingRacingOrmEntities';
import {
TeamJoinRequestOrmEntity,
TeamMembershipOrmEntity,
TeamOrmEntity,
} from '@adapters/racing/persistence/typeorm/entities/TeamOrmEntities';
import { TypeOrmDriverRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmDriverRepository';
import { TypeOrmLeagueMembershipRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueMembershipRepository';
import { TypeOrmLeagueRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueRepository';
import { TypeOrmLeagueScoringConfigRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueScoringConfigRepository';
import { TypeOrmRaceRegistrationRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmRaceRegistrationRepository';
import { TypeOrmRaceRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmRaceRepository';
import { TypeOrmSeasonRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmSeasonRepository';
import { TypeOrmResultRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmResultRepository';
import { TypeOrmStandingRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmStandingRepository';
import {
TypeOrmGameRepository,
TypeOrmLeagueWalletRepository,
TypeOrmSeasonSponsorshipRepository,
TypeOrmSponsorRepository,
TypeOrmSponsorshipPricingRepository,
TypeOrmSponsorshipRequestRepository,
TypeOrmTransactionRepository,
} from '@adapters/racing/persistence/typeorm/repositories/CommerceTypeOrmRepositories';
import { TypeOrmPenaltyRepository, TypeOrmProtestRepository } from '@adapters/racing/persistence/typeorm/repositories/StewardingTypeOrmRepositories';
import { TypeOrmTeamMembershipRepository, TypeOrmTeamRepository } from '@adapters/racing/persistence/typeorm/repositories/TeamTypeOrmRepositories';
import { DriverOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/DriverOrmMapper';
import { LeagueMembershipOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueMembershipOrmMapper';
import { LeagueOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper';
import { RaceRegistrationOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/RaceRegistrationOrmMapper';
import { RaceOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/RaceOrmMapper';
import { SeasonOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/SeasonOrmMapper';
import { ResultOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/ResultOrmMapper';
import { StandingOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/StandingOrmMapper';
import { PointsTableJsonMapper } from '@adapters/racing/persistence/typeorm/mappers/PointsTableJsonMapper';
import { ChampionshipConfigJsonMapper } from '@adapters/racing/persistence/typeorm/mappers/ChampionshipConfigJsonMapper';
import { LeagueScoringConfigOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueScoringConfigOrmMapper';
import {
GameOrmMapper,
LeagueWalletOrmMapper,
SeasonSponsorshipOrmMapper,
SponsorOrmMapper,
SponsorshipPricingOrmMapper,
SponsorshipRequestOrmMapper,
TransactionOrmMapper,
} from '@adapters/racing/persistence/typeorm/mappers/CommerceOrmMappers';
import { MoneyOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/MoneyOrmMapper';
import { PenaltyOrmMapper, ProtestOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/StewardingOrmMappers';
import { TeamMembershipOrmMapper, TeamOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/TeamOrmMappers';
function makePlaceholder(token: string): unknown {
return Object.freeze({
__token: token,
__kind: 'postgres-placeholder',
__notImplemented(): never {
throw new Error(`[PostgresRacingPersistenceModule] Placeholder provider "${token}" is not implemented yet`);
},
});
}
import { getPointsSystems } from '@adapters/bootstrap/PointsSystems';
const RACING_POINTS_SYSTEMS_TOKEN = 'RACING_POINTS_SYSTEMS_TOKEN';
const typeOrmFeatureImports = [
TypeOrmModule.forFeature([LeagueOrmEntity, SeasonOrmEntity, RaceOrmEntity, LeagueScoringConfigOrmEntity]),
TypeOrmModule.forFeature([
DriverOrmEntity,
LeagueMembershipOrmEntity,
LeagueOrmEntity,
SeasonOrmEntity,
RaceOrmEntity,
RaceRegistrationOrmEntity,
LeagueScoringConfigOrmEntity,
ResultOrmEntity,
StandingOrmEntity,
TeamOrmEntity,
TeamMembershipOrmEntity,
TeamJoinRequestOrmEntity,
PenaltyOrmEntity,
ProtestOrmEntity,
GameOrmEntity,
LeagueWalletOrmEntity,
TransactionOrmEntity,
SponsorOrmEntity,
SponsorshipPricingOrmEntity,
SponsorshipRequestOrmEntity,
SeasonSponsorshipOrmEntity,
]),
];
@Module({
imports: [LoggingModule, ...typeOrmFeatureImports],
providers: [
{ provide: DriverOrmMapper, useFactory: () => new DriverOrmMapper() },
{ provide: LeagueMembershipOrmMapper, useFactory: () => new LeagueMembershipOrmMapper() },
{ provide: RaceRegistrationOrmMapper, useFactory: () => new RaceRegistrationOrmMapper() },
{ provide: ResultOrmMapper, useFactory: () => new ResultOrmMapper() },
{ provide: StandingOrmMapper, useFactory: () => new StandingOrmMapper() },
{ provide: LeagueOrmMapper, useFactory: () => new LeagueOrmMapper() },
{ provide: RaceOrmMapper, useFactory: () => new RaceOrmMapper() },
{ provide: SeasonOrmMapper, useFactory: () => new SeasonOrmMapper() },
{ provide: TeamOrmMapper, useFactory: () => new TeamOrmMapper() },
{ provide: TeamMembershipOrmMapper, useFactory: () => new TeamMembershipOrmMapper() },
{ provide: PenaltyOrmMapper, useFactory: () => new PenaltyOrmMapper() },
{ provide: ProtestOrmMapper, useFactory: () => new ProtestOrmMapper() },
{ provide: MoneyOrmMapper, useFactory: () => new MoneyOrmMapper() },
{ provide: GameOrmMapper, useFactory: () => new GameOrmMapper() },
{ provide: SponsorOrmMapper, useFactory: () => new SponsorOrmMapper() },
{
provide: LeagueWalletOrmMapper,
useFactory: (moneyMapper: MoneyOrmMapper) => new LeagueWalletOrmMapper(moneyMapper),
inject: [MoneyOrmMapper],
},
{
provide: TransactionOrmMapper,
useFactory: (moneyMapper: MoneyOrmMapper) => new TransactionOrmMapper(moneyMapper),
inject: [MoneyOrmMapper],
},
{
provide: SponsorshipPricingOrmMapper,
useFactory: (moneyMapper: MoneyOrmMapper) => new SponsorshipPricingOrmMapper(moneyMapper),
inject: [MoneyOrmMapper],
},
{
provide: SponsorshipRequestOrmMapper,
useFactory: (moneyMapper: MoneyOrmMapper) => new SponsorshipRequestOrmMapper(moneyMapper),
inject: [MoneyOrmMapper],
},
{
provide: SeasonSponsorshipOrmMapper,
useFactory: (moneyMapper: MoneyOrmMapper) => new SeasonSponsorshipOrmMapper(moneyMapper),
inject: [MoneyOrmMapper],
},
{ provide: RACING_POINTS_SYSTEMS_TOKEN, useFactory: () => getPointsSystems() },
{
provide: DRIVER_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(DRIVER_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (dataSource: DataSource, mapper: DriverOrmMapper) => new TypeOrmDriverRepository(dataSource, mapper),
inject: [getDataSourceToken(), DriverOrmMapper],
},
{
provide: LEAGUE_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource) => {
const leagueMapper = new LeagueOrmMapper();
return new TypeOrmLeagueRepository(dataSource, leagueMapper);
},
inject: [getDataSourceToken()],
useFactory: (dataSource: DataSource, mapper: LeagueOrmMapper) => new TypeOrmLeagueRepository(dataSource, mapper),
inject: [getDataSourceToken(), LeagueOrmMapper],
},
{
provide: RACE_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource) => {
const raceMapper = new RaceOrmMapper();
return new TypeOrmRaceRepository(dataSource, raceMapper);
},
inject: [getDataSourceToken()],
useFactory: (dataSource: DataSource, mapper: RaceOrmMapper) => new TypeOrmRaceRepository(dataSource, mapper),
inject: [getDataSourceToken(), RaceOrmMapper],
},
{
provide: RESULT_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(RESULT_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (dataSource: DataSource, mapper: ResultOrmMapper) => new TypeOrmResultRepository(dataSource, mapper),
inject: [getDataSourceToken(), ResultOrmMapper],
},
{
provide: STANDING_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(STANDING_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (
dataSource: DataSource,
standingMapper: StandingOrmMapper,
resultMapper: ResultOrmMapper,
leagueMapper: LeagueOrmMapper,
pointsSystems: Record<string, Record<number, number>>,
) => new TypeOrmStandingRepository(dataSource, standingMapper, resultMapper, leagueMapper, pointsSystems),
inject: [getDataSourceToken(), StandingOrmMapper, ResultOrmMapper, LeagueOrmMapper, RACING_POINTS_SYSTEMS_TOKEN],
},
{
provide: LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (dataSource: DataSource, mapper: LeagueMembershipOrmMapper) =>
new TypeOrmLeagueMembershipRepository(dataSource, mapper),
inject: [getDataSourceToken(), LeagueMembershipOrmMapper],
},
{
provide: RACE_REGISTRATION_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(RACE_REGISTRATION_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (dataSource: DataSource, mapper: RaceRegistrationOrmMapper) =>
new TypeOrmRaceRegistrationRepository(dataSource, mapper),
inject: [getDataSourceToken(), RaceRegistrationOrmMapper],
},
{
provide: TEAM_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(TEAM_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (repo: Repository<TeamOrmEntity>, mapper: TeamOrmMapper) => new TypeOrmTeamRepository(repo, mapper),
inject: [getRepositoryToken(TeamOrmEntity), TeamOrmMapper],
},
{
provide: TEAM_MEMBERSHIP_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(TEAM_MEMBERSHIP_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (
membershipRepo: Repository<TeamMembershipOrmEntity>,
joinRequestRepo: Repository<TeamJoinRequestOrmEntity>,
mapper: TeamMembershipOrmMapper,
) => new TypeOrmTeamMembershipRepository(membershipRepo, joinRequestRepo, mapper),
inject: [
getRepositoryToken(TeamMembershipOrmEntity),
getRepositoryToken(TeamJoinRequestOrmEntity),
TeamMembershipOrmMapper,
],
},
{
provide: PENALTY_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(PENALTY_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (repo: Repository<PenaltyOrmEntity>, mapper: PenaltyOrmMapper) => new TypeOrmPenaltyRepository(repo, mapper),
inject: [getRepositoryToken(PenaltyOrmEntity), PenaltyOrmMapper],
},
{
provide: PROTEST_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(PROTEST_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (repo: Repository<ProtestOrmEntity>, mapper: ProtestOrmMapper) => new TypeOrmProtestRepository(repo, mapper),
inject: [getRepositoryToken(ProtestOrmEntity), ProtestOrmMapper],
},
{
provide: SEASON_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource) => {
const seasonMapper = new SeasonOrmMapper();
return new TypeOrmSeasonRepository(dataSource, seasonMapper);
},
inject: [getDataSourceToken()],
useFactory: (dataSource: DataSource, mapper: SeasonOrmMapper) => new TypeOrmSeasonRepository(dataSource, mapper),
inject: [getDataSourceToken(), SeasonOrmMapper],
},
{
provide: SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(SEASON_SPONSORSHIP_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (repo: Repository<SeasonSponsorshipOrmEntity>, mapper: SeasonSponsorshipOrmMapper) =>
new TypeOrmSeasonSponsorshipRepository(repo, mapper),
inject: [getRepositoryToken(SeasonSponsorshipOrmEntity), SeasonSponsorshipOrmMapper],
},
{
provide: LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
@@ -146,33 +274,36 @@ const typeOrmFeatureImports = [
},
{
provide: GAME_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(GAME_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (repo: Repository<GameOrmEntity>, mapper: GameOrmMapper) => new TypeOrmGameRepository(repo, mapper),
inject: [getRepositoryToken(GameOrmEntity), GameOrmMapper],
},
{
provide: LEAGUE_WALLET_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(LEAGUE_WALLET_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (repo: Repository<LeagueWalletOrmEntity>, mapper: LeagueWalletOrmMapper) =>
new TypeOrmLeagueWalletRepository(repo, mapper),
inject: [getRepositoryToken(LeagueWalletOrmEntity), LeagueWalletOrmMapper],
},
{
provide: TRANSACTION_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(TRANSACTION_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (repo: Repository<TransactionOrmEntity>, mapper: TransactionOrmMapper) => new TypeOrmTransactionRepository(repo, mapper),
inject: [getRepositoryToken(TransactionOrmEntity), TransactionOrmMapper],
},
{
provide: SPONSOR_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(SPONSOR_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (repo: Repository<SponsorOrmEntity>, mapper: SponsorOrmMapper) => new TypeOrmSponsorRepository(repo, mapper),
inject: [getRepositoryToken(SponsorOrmEntity), SponsorOrmMapper],
},
{
provide: SPONSORSHIP_PRICING_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(SPONSORSHIP_PRICING_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (repo: Repository<SponsorshipPricingOrmEntity>, mapper: SponsorshipPricingOrmMapper) =>
new TypeOrmSponsorshipPricingRepository(repo, mapper),
inject: [getRepositoryToken(SponsorshipPricingOrmEntity), SponsorshipPricingOrmMapper],
},
{
provide: SPONSORSHIP_REQUEST_REPOSITORY_TOKEN,
useFactory: () => makePlaceholder(SPONSORSHIP_REQUEST_REPOSITORY_TOKEN),
inject: ['Logger'],
useFactory: (repo: Repository<SponsorshipRequestOrmEntity>, mapper: SponsorshipRequestOrmMapper) =>
new TypeOrmSponsorshipRequestRepository(repo, mapper),
inject: [getRepositoryToken(SponsorshipRequestOrmEntity), SponsorshipRequestOrmMapper],
},
],
exports: [

View File

@@ -0,0 +1,50 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule, getDataSourceToken } from '@nestjs/typeorm';
import type { DataSource } from 'typeorm';
import { LoggingModule } from '../../domain/logging/LoggingModule';
import { SOCIAL_FEED_REPOSITORY_TOKEN, SOCIAL_GRAPH_REPOSITORY_TOKEN } from '../social/SocialPersistenceTokens';
import { DriverOrmEntity } from '@adapters/racing/persistence/typeorm/entities/DriverOrmEntity';
import { DriverOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/DriverOrmMapper';
import { FeedItemOrmEntity } from '@adapters/social/persistence/typeorm/entities/FeedItemOrmEntity';
import { FriendshipOrmEntity } from '@adapters/social/persistence/typeorm/entities/FriendshipOrmEntity';
import { FeedItemOrmMapper } from '@adapters/social/persistence/typeorm/mappers/FeedItemOrmMapper';
import { TypeOrmFeedRepository } from '@adapters/social/persistence/typeorm/repositories/TypeOrmFeedRepository';
import { TypeOrmSocialGraphRepository } from '@adapters/social/persistence/typeorm/repositories/TypeOrmSocialGraphRepository';
const typeOrmFeatureImports = [
TypeOrmModule.forFeature([FeedItemOrmEntity, FriendshipOrmEntity, DriverOrmEntity]),
];
@Module({
imports: [LoggingModule, ...typeOrmFeatureImports],
providers: [
{ provide: FeedItemOrmMapper, useFactory: () => new FeedItemOrmMapper() },
{ provide: DriverOrmMapper, useFactory: () => new DriverOrmMapper() },
{
provide: SOCIAL_FEED_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource, mapper: FeedItemOrmMapper) =>
new TypeOrmFeedRepository(
dataSource.getRepository(FeedItemOrmEntity),
dataSource.getRepository(FriendshipOrmEntity),
mapper,
),
inject: [getDataSourceToken(), FeedItemOrmMapper],
},
{
provide: SOCIAL_GRAPH_REPOSITORY_TOKEN,
useFactory: (dataSource: DataSource, driverMapper: DriverOrmMapper) =>
new TypeOrmSocialGraphRepository(
dataSource.getRepository(FriendshipOrmEntity),
dataSource.getRepository(DriverOrmEntity),
driverMapper,
),
inject: [getDataSourceToken(), DriverOrmMapper],
},
],
exports: [SOCIAL_FEED_REPOSITORY_TOKEN, SOCIAL_GRAPH_REPOSITORY_TOKEN],
})
export class PostgresSocialPersistenceModule {}

View File

@@ -0,0 +1,84 @@
import 'reflect-metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { DataSource } from 'typeorm';
import { PageView } from '@core/analytics/domain/entities/PageView';
import { PageViewOrmEntity } from '@adapters/analytics/persistence/typeorm/entities/PageViewOrmEntity';
import { EngagementEventOrmEntity } from '@adapters/analytics/persistence/typeorm/entities/EngagementEventOrmEntity';
import { AnalyticsSnapshotOrmEntity } from '@adapters/analytics/persistence/typeorm/entities/AnalyticsSnapshotOrmEntity';
import { PageViewOrmMapper } from '@adapters/analytics/persistence/typeorm/mappers/PageViewOrmMapper';
import { EngagementEventOrmMapper } from '@adapters/analytics/persistence/typeorm/mappers/EngagementEventOrmMapper';
import { AnalyticsSnapshotOrmMapper } from '@adapters/analytics/persistence/typeorm/mappers/AnalyticsSnapshotOrmMapper';
import { TypeOrmPageViewRepository } from '@adapters/analytics/persistence/typeorm/repositories/TypeOrmPageViewRepository';
import { TypeOrmEngagementRepository } from '@adapters/analytics/persistence/typeorm/repositories/TypeOrmEngagementRepository';
import { TypeOrmAnalyticsSnapshotRepository } from '@adapters/analytics/persistence/typeorm/repositories/TypeOrmAnalyticsSnapshotRepository';
const databaseUrl = process.env.DATABASE_URL;
const describeIfDatabase = databaseUrl ? describe : describe.skip;
describeIfDatabase('TypeORM Analytics repositories (postgres slice)', () => {
let dataSource: DataSource;
beforeAll(async () => {
if (!databaseUrl) {
throw new Error('DATABASE_URL is required to run postgres integration tests');
}
dataSource = new DataSource({
type: 'postgres',
url: databaseUrl,
entities: [PageViewOrmEntity, EngagementEventOrmEntity, AnalyticsSnapshotOrmEntity],
synchronize: true,
});
await dataSource.initialize();
});
afterAll(async () => {
if (dataSource?.isInitialized) {
await dataSource.destroy();
}
});
it('supports: persist page view + read it back + count unique visitors', async () => {
const pageViewOrmRepo = dataSource.getRepository(PageViewOrmEntity);
const engagementOrmRepo = dataSource.getRepository(EngagementEventOrmEntity);
const snapshotOrmRepo = dataSource.getRepository(AnalyticsSnapshotOrmEntity);
await snapshotOrmRepo.clear();
await engagementOrmRepo.clear();
await pageViewOrmRepo.clear();
const pageViewRepo = new TypeOrmPageViewRepository(pageViewOrmRepo, new PageViewOrmMapper());
const engagementRepo = new TypeOrmEngagementRepository(engagementOrmRepo, new EngagementEventOrmMapper());
const snapshotRepo = new TypeOrmAnalyticsSnapshotRepository(snapshotOrmRepo, new AnalyticsSnapshotOrmMapper());
// Ensure all repositories at least construct and basic ops work
expect(pageViewRepo).toBeDefined();
expect(engagementRepo).toBeDefined();
expect(snapshotRepo).toBeDefined();
const pv1 = PageView.create({
id: `pv-it-${Date.now()}`,
entityType: 'league',
entityId: 'league-it-1',
visitorType: 'anonymous',
sessionId: 'sess-it-1',
visitorId: 'visitor-it-1',
timestamp: new Date(),
durationMs: 8000,
});
await pageViewRepo.save(pv1);
const loaded = await pageViewRepo.findById(pv1.id);
expect(loaded?.id).toBe(pv1.id);
const unique = await pageViewRepo.countUniqueVisitors('league', 'league-it-1');
expect(unique).toBe(1);
});
});

View File

@@ -0,0 +1,83 @@
import 'reflect-metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { DataSource } from 'typeorm';
import { User } from '@core/identity/domain/entities/User';
import { EmailAddress } from '@core/identity/domain/value-objects/EmailAddress';
import { PasswordHash } from '@core/identity/domain/value-objects/PasswordHash';
import { UserId } from '@core/identity/domain/value-objects/UserId';
import { UserOrmEntity } from '@adapters/identity/persistence/typeorm/entities/UserOrmEntity';
import { TypeOrmAuthRepository } from '@adapters/identity/persistence/typeorm/TypeOrmAuthRepository';
import { TypeOrmUserRepository } from '@adapters/identity/persistence/typeorm/TypeOrmUserRepository';
import { UserOrmMapper } from '@adapters/identity/persistence/typeorm/mappers/UserOrmMapper';
const databaseUrl = process.env.DATABASE_URL;
const describeIfDatabase = databaseUrl ? describe : describe.skip;
describeIfDatabase('TypeORM Identity repositories (postgres slice)', () => {
let dataSource: DataSource;
beforeAll(async () => {
if (!databaseUrl) {
throw new Error('DATABASE_URL is required to run postgres integration tests');
}
dataSource = new DataSource({
type: 'postgres',
url: databaseUrl,
entities: [UserOrmEntity],
synchronize: true,
});
await dataSource.initialize();
});
afterAll(async () => {
if (dataSource?.isInitialized) {
await dataSource.destroy();
}
});
it('supports: persist user + find by email + update displayName', async () => {
const mapper = new UserOrmMapper();
const userRepo = new TypeOrmUserRepository(dataSource, mapper);
const authRepo = new TypeOrmAuthRepository(dataSource, mapper);
const email = EmailAddress.create(`it-${Date.now()}@example.com`);
const passwordHash = PasswordHash.fromHash('bcrypt-hash-for-it');
const userId = UserId.create();
const user = User.create({
id: userId,
email: email.value,
displayName: 'Integration User',
passwordHash,
});
await authRepo.save(user);
const loaded = await authRepo.findByEmail(email);
expect(loaded).not.toBeNull();
expect(loaded!.getId().value).toBe(user.getId().value);
expect(loaded!.getEmail()).toBe(email.value.toLowerCase());
expect(loaded!.getDisplayName()).toBe('Integration User');
const stored = await userRepo.findByEmail(email.value.toUpperCase());
expect(stored).not.toBeNull();
expect(stored!.email).toBe(email.value.toLowerCase());
const updated = User.create({
id: userId,
email: email.value,
displayName: 'Integration User Updated',
passwordHash,
});
await authRepo.save(updated);
const loadedUpdated = await authRepo.findByEmail(email);
expect(loadedUpdated!.getDisplayName()).toBe('Integration User Updated');
});
});

View File

@@ -0,0 +1,179 @@
import 'reflect-metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { DataSource } from 'typeorm';
import { MemberPaymentStatus } from '@core/payments/domain/entities/MemberPayment';
import { MembershipFeeType } from '@core/payments/domain/entities/MembershipFee';
import { PaymentStatus, PaymentType, PayerType } from '@core/payments/domain/entities/Payment';
import { PrizeType } from '@core/payments/domain/entities/Prize';
import { TransactionType } from '@core/payments/domain/entities/Wallet';
import { PaymentsMemberPaymentOrmEntity } from '@adapters/payments/persistence/typeorm/entities/PaymentsMemberPaymentOrmEntity';
import { PaymentsMembershipFeeOrmEntity } from '@adapters/payments/persistence/typeorm/entities/PaymentsMembershipFeeOrmEntity';
import { PaymentsPaymentOrmEntity } from '@adapters/payments/persistence/typeorm/entities/PaymentsPaymentOrmEntity';
import { PaymentsPrizeOrmEntity } from '@adapters/payments/persistence/typeorm/entities/PaymentsPrizeOrmEntity';
import { PaymentsTransactionOrmEntity } from '@adapters/payments/persistence/typeorm/entities/PaymentsTransactionOrmEntity';
import { PaymentsWalletOrmEntity } from '@adapters/payments/persistence/typeorm/entities/PaymentsWalletOrmEntity';
import { PaymentsMemberPaymentOrmMapper } from '@adapters/payments/persistence/typeorm/mappers/PaymentsMemberPaymentOrmMapper';
import { PaymentsMembershipFeeOrmMapper } from '@adapters/payments/persistence/typeorm/mappers/PaymentsMembershipFeeOrmMapper';
import { PaymentsPaymentOrmMapper } from '@adapters/payments/persistence/typeorm/mappers/PaymentsPaymentOrmMapper';
import { PaymentsPrizeOrmMapper } from '@adapters/payments/persistence/typeorm/mappers/PaymentsPrizeOrmMapper';
import { PaymentsWalletOrmMapper } from '@adapters/payments/persistence/typeorm/mappers/PaymentsWalletOrmMapper';
import { TypeOrmMemberPaymentRepository, TypeOrmMembershipFeeRepository } from '@adapters/payments/persistence/typeorm/repositories/TypeOrmMembershipFeeRepository';
import { TypeOrmPaymentRepository } from '@adapters/payments/persistence/typeorm/repositories/TypeOrmPaymentRepository';
import { TypeOrmPrizeRepository } from '@adapters/payments/persistence/typeorm/repositories/TypeOrmPrizeRepository';
import { TypeOrmTransactionRepository, TypeOrmWalletRepository } from '@adapters/payments/persistence/typeorm/repositories/TypeOrmWalletRepository';
const databaseUrl = process.env.DATABASE_URL;
const describeIfDatabase = databaseUrl ? describe : describe.skip;
describeIfDatabase('TypeORM Payments repositories (postgres slice)', () => {
let dataSource: DataSource;
beforeAll(async () => {
if (!databaseUrl) {
throw new Error('DATABASE_URL is required to run postgres integration tests');
}
dataSource = new DataSource({
type: 'postgres',
url: databaseUrl,
entities: [
PaymentsWalletOrmEntity,
PaymentsTransactionOrmEntity,
PaymentsPaymentOrmEntity,
PaymentsPrizeOrmEntity,
PaymentsMembershipFeeOrmEntity,
PaymentsMemberPaymentOrmEntity,
],
synchronize: true,
});
await dataSource.initialize();
});
afterAll(async () => {
if (dataSource?.isInitialized) {
await dataSource.destroy();
}
});
it('supports: wallet+tx+payment+prize+membership fee+member payment', async () => {
const walletMapper = new PaymentsWalletOrmMapper();
const walletRepo = new TypeOrmWalletRepository(dataSource, walletMapper);
const txRepo = new TypeOrmTransactionRepository(dataSource, walletMapper);
const paymentRepo = new TypeOrmPaymentRepository(dataSource, new PaymentsPaymentOrmMapper());
const prizeRepo = new TypeOrmPrizeRepository(dataSource, new PaymentsPrizeOrmMapper());
const feeRepo = new TypeOrmMembershipFeeRepository(dataSource, new PaymentsMembershipFeeOrmMapper());
const memberPaymentRepo = new TypeOrmMemberPaymentRepository(dataSource, new PaymentsMemberPaymentOrmMapper());
const now = Date.now();
const leagueId = `league-it-${now}`;
const wallet = {
id: `wallet-it-${now}`,
leagueId,
balance: 0,
totalRevenue: 0,
totalPlatformFees: 0,
totalWithdrawn: 0,
currency: 'USD',
createdAt: new Date(),
};
await walletRepo.create(wallet);
const tx = {
id: `txn-it-${now}`,
walletId: wallet.id,
type: TransactionType.DEPOSIT,
amount: 25,
description: 'Integration deposit',
createdAt: new Date(),
};
await txRepo.create(tx);
const loadedWallet = await walletRepo.findByLeagueId(wallet.leagueId);
expect(loadedWallet).not.toBeNull();
expect(loadedWallet!.leagueId).toBe(wallet.leagueId);
const loadedTxs = await txRepo.findByWalletId(wallet.id);
expect(loadedTxs.map(t => t.id)).toContain(tx.id);
const payment = {
id: `payment-it-${now}`,
type: PaymentType.MEMBERSHIP_FEE,
amount: 100,
platformFee: 5,
netAmount: 95,
payerId: `driver-it-${now}`,
payerType: PayerType.DRIVER,
leagueId,
status: PaymentStatus.PENDING,
createdAt: new Date(),
};
await paymentRepo.create(payment);
const loadedPayments = await paymentRepo.findByLeagueId(leagueId);
expect(loadedPayments.map(p => p.id)).toContain(payment.id);
const prize = {
id: `prize-it-${now}`,
leagueId,
seasonId: `season-it-${now}`,
position: 1,
name: 'Winner',
amount: 250,
type: PrizeType.CASH,
awarded: false,
createdAt: new Date(),
};
await prizeRepo.create(prize);
const loadedPrizes = await prizeRepo.findByLeagueIdAndSeasonId(leagueId, prize.seasonId);
expect(loadedPrizes.map(p => p.id)).toContain(prize.id);
const fee = {
id: `fee-it-${now}`,
leagueId,
type: MembershipFeeType.SEASON,
amount: 100,
enabled: true,
createdAt: new Date(),
updatedAt: new Date(),
};
await feeRepo.create(fee);
const loadedFee = await feeRepo.findByLeagueId(leagueId);
expect(loadedFee?.id).toBe(fee.id);
const memberPayment = {
id: `member-payment-it-${now}`,
feeId: fee.id,
driverId: payment.payerId,
amount: 100,
platformFee: 5,
netAmount: 95,
status: MemberPaymentStatus.PAID,
dueDate: new Date(),
paidAt: new Date(),
};
await memberPaymentRepo.create(memberPayment);
const loadedMemberPayments = await memberPaymentRepo.findByLeagueIdAndDriverId(
leagueId,
memberPayment.driverId,
feeRepo,
);
expect(loadedMemberPayments.map(mp => mp.id)).toContain(memberPayment.id);
});
});

View File

@@ -0,0 +1,193 @@
import 'reflect-metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { DataSource } from 'typeorm';
import { Game } from '@core/racing/domain/entities/Game';
import { LeagueWallet } from '@core/racing/domain/entities/league-wallet/LeagueWallet';
import { Transaction } from '@core/racing/domain/entities/league-wallet/Transaction';
import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
import { SeasonSponsorship } from '@core/racing/domain/entities/season/SeasonSponsorship';
import { SponsorshipRequest } from '@core/racing/domain/entities/SponsorshipRequest';
import { Money } from '@core/racing/domain/value-objects/Money';
import { SponsorshipPricing } from '@core/racing/domain/value-objects/SponsorshipPricing';
import {
GameOrmEntity,
LeagueWalletOrmEntity,
SeasonSponsorshipOrmEntity,
SponsorOrmEntity,
SponsorshipPricingOrmEntity,
SponsorshipRequestOrmEntity,
TransactionOrmEntity,
} from '../../../../../../adapters/racing/persistence/typeorm/entities/MissingRacingOrmEntities';
import { MoneyOrmMapper } from '../../../../../../adapters/racing/persistence/typeorm/mappers/MoneyOrmMapper';
import {
GameOrmMapper,
LeagueWalletOrmMapper,
SeasonSponsorshipOrmMapper,
SponsorOrmMapper,
SponsorshipPricingOrmMapper,
SponsorshipRequestOrmMapper,
TransactionOrmMapper,
} from '../../../../../../adapters/racing/persistence/typeorm/mappers/CommerceOrmMappers';
import {
TypeOrmGameRepository,
TypeOrmLeagueWalletRepository,
TypeOrmSeasonSponsorshipRepository,
TypeOrmSponsorRepository,
TypeOrmSponsorshipPricingRepository,
TypeOrmSponsorshipRequestRepository,
TypeOrmTransactionRepository,
} from '../../../../../../adapters/racing/persistence/typeorm/repositories/CommerceTypeOrmRepositories';
const databaseUrl = process.env.DATABASE_URL;
const describeIfDatabase = databaseUrl ? describe : describe.skip;
describeIfDatabase('TypeORM Racing repositories (commerce slice)', () => {
let dataSource: DataSource;
beforeAll(async () => {
if (!databaseUrl) {
throw new Error('DATABASE_URL is required to run postgres integration tests');
}
dataSource = new DataSource({
type: 'postgres',
url: databaseUrl,
entities: [
GameOrmEntity,
SponsorOrmEntity,
LeagueWalletOrmEntity,
TransactionOrmEntity,
SponsorshipPricingOrmEntity,
SponsorshipRequestOrmEntity,
SeasonSponsorshipOrmEntity,
],
synchronize: true,
});
await dataSource.initialize();
});
afterAll(async () => {
if (dataSource?.isInitialized) {
await dataSource.destroy();
}
});
it('supports: game + sponsor + wallet + transaction + sponsorship objects', async () => {
const moneyMapper = new MoneyOrmMapper();
const gameRepo = new TypeOrmGameRepository(
dataSource.getRepository(GameOrmEntity),
new GameOrmMapper(),
);
const sponsorRepo = new TypeOrmSponsorRepository(
dataSource.getRepository(SponsorOrmEntity),
new SponsorOrmMapper(),
);
const walletRepo = new TypeOrmLeagueWalletRepository(
dataSource.getRepository(LeagueWalletOrmEntity),
new LeagueWalletOrmMapper(moneyMapper),
);
const txRepo = new TypeOrmTransactionRepository(
dataSource.getRepository(TransactionOrmEntity),
new TransactionOrmMapper(moneyMapper),
);
const pricingRepo = new TypeOrmSponsorshipPricingRepository(
dataSource.getRepository(SponsorshipPricingOrmEntity),
new SponsorshipPricingOrmMapper(moneyMapper),
);
const requestRepo = new TypeOrmSponsorshipRequestRepository(
dataSource.getRepository(SponsorshipRequestOrmEntity),
new SponsorshipRequestOrmMapper(moneyMapper),
);
const seasonSponsorshipRepo = new TypeOrmSeasonSponsorshipRepository(
dataSource.getRepository(SeasonSponsorshipOrmEntity),
new SeasonSponsorshipOrmMapper(moneyMapper),
);
const game = Game.create({ id: 'iracing', name: 'iRacing' });
await dataSource.getRepository(GameOrmEntity).save(new GameOrmMapper().toOrmEntity(game));
const persistedGame = await gameRepo.findById('iracing');
expect(persistedGame?.name.toString()).toBe('iRacing');
const sponsor = Sponsor.create({
id: '00000000-0000-4000-8000-000000000010',
name: 'Sponsor Inc',
contactEmail: 'sponsor@example.com',
createdAt: new Date('2025-01-01T00:00:00.000Z'),
});
await sponsorRepo.create(sponsor);
const leagueId = '00000000-0000-4000-8000-000000000020';
const wallet = LeagueWallet.create({
id: '00000000-0000-4000-8000-000000000030',
leagueId,
balance: Money.create(0, 'USD'),
createdAt: new Date('2025-01-01T00:00:00.000Z'),
transactionIds: [],
});
await walletRepo.create(wallet);
const tx = Transaction.rehydrate({
id: '00000000-0000-4000-8000-000000000040',
walletId: wallet.id.toString(),
type: 'refund',
amount: Money.create(10, 'USD'),
platformFee: Money.create(1, 'USD'),
netAmount: Money.create(9, 'USD'),
status: 'completed',
createdAt: new Date('2025-01-02T00:00:00.000Z'),
});
await txRepo.create(tx);
const walletTxs = await txRepo.findByWalletId(wallet.id.toString());
expect(walletTxs.map((t) => t.id.toString())).toContain(tx.id.toString());
const pricing = SponsorshipPricing.create({
acceptingApplications: true,
mainSlot: {
tier: 'main',
price: Money.create(100, 'USD'),
benefits: ['Logo'],
available: true,
maxSlots: 1,
},
});
await pricingRepo.save('team', 'team-1', pricing);
const persistedPricing = await pricingRepo.findByEntity('team', 'team-1');
expect(persistedPricing?.acceptingApplications).toBe(true);
const request = SponsorshipRequest.create({
id: '00000000-0000-4000-8000-000000000050',
sponsorId: sponsor.id.toString(),
entityType: 'team',
entityId: 'team-1',
tier: 'main',
offeredAmount: Money.create(100, 'USD'),
message: 'We would like to sponsor you',
});
await requestRepo.create(request);
const pending = await requestRepo.findPendingByEntity('team', 'team-1');
expect(pending.map((r) => r.id)).toContain(request.id);
const seasonSponsorship = SeasonSponsorship.create({
id: '00000000-0000-4000-8000-000000000060',
seasonId: '00000000-0000-4000-8000-000000000070',
sponsorId: sponsor.id.toString(),
tier: 'main',
pricing: Money.create(100, 'USD'),
});
await seasonSponsorshipRepo.create(seasonSponsorship);
const seasonSponsorships = await seasonSponsorshipRepo.findBySponsorId(sponsor.id.toString());
expect(seasonSponsorships.map((s) => s.id)).toContain(seasonSponsorship.id);
});
});

View File

@@ -0,0 +1,164 @@
import 'reflect-metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { DataSource } from 'typeorm';
import { Team } from '@core/racing/domain/entities/Team';
import { Penalty } from '@core/racing/domain/entities/penalty/Penalty';
import { Protest } from '@core/racing/domain/entities/Protest';
import {
PenaltyOrmEntity,
ProtestOrmEntity,
} from '../../../../../../adapters/racing/persistence/typeorm/entities/MissingRacingOrmEntities';
import {
TeamJoinRequestOrmEntity,
TeamMembershipOrmEntity,
TeamOrmEntity,
} from '../../../../../../adapters/racing/persistence/typeorm/entities/TeamOrmEntities';
import { TeamMembershipOrmMapper, TeamOrmMapper } from '../../../../../../adapters/racing/persistence/typeorm/mappers/TeamOrmMappers';
import { PenaltyOrmMapper, ProtestOrmMapper } from '../../../../../../adapters/racing/persistence/typeorm/mappers/StewardingOrmMappers';
import {
TypeOrmTeamMembershipRepository,
TypeOrmTeamRepository,
} from '../../../../../../adapters/racing/persistence/typeorm/repositories/TeamTypeOrmRepositories';
import {
TypeOrmPenaltyRepository,
TypeOrmProtestRepository,
} from '../../../../../../adapters/racing/persistence/typeorm/repositories/StewardingTypeOrmRepositories';
const databaseUrl = process.env.DATABASE_URL;
const describeIfDatabase = databaseUrl ? describe : describe.skip;
describeIfDatabase('TypeORM Racing repositories (teams + stewarding slice)', () => {
let dataSource: DataSource;
beforeAll(async () => {
if (!databaseUrl) {
throw new Error('DATABASE_URL is required to run postgres integration tests');
}
dataSource = new DataSource({
type: 'postgres',
url: databaseUrl,
entities: [
TeamOrmEntity,
TeamMembershipOrmEntity,
TeamJoinRequestOrmEntity,
PenaltyOrmEntity,
ProtestOrmEntity,
],
synchronize: true,
});
await dataSource.initialize();
});
afterAll(async () => {
if (dataSource?.isInitialized) {
await dataSource.destroy();
}
});
it('supports: create team + membership + join request + stewarding records', async () => {
const teamRepo = new TypeOrmTeamRepository(
dataSource.getRepository(TeamOrmEntity),
new TeamOrmMapper(),
);
const teamMembershipRepo = new TypeOrmTeamMembershipRepository(
dataSource.getRepository(TeamMembershipOrmEntity),
dataSource.getRepository(TeamJoinRequestOrmEntity),
new TeamMembershipOrmMapper(),
);
const penaltyRepo = new TypeOrmPenaltyRepository(
dataSource.getRepository(PenaltyOrmEntity),
new PenaltyOrmMapper(),
);
const protestRepo = new TypeOrmProtestRepository(
dataSource.getRepository(ProtestOrmEntity),
new ProtestOrmMapper(),
);
const leagueId = '00000000-0000-4000-8000-000000000010';
const team = Team.create({
id: '00000000-0000-4000-8000-000000000001',
name: 'Integration Team',
tag: 'INT',
description: 'Team for integration tests',
ownerId: '00000000-0000-4000-8000-000000000002',
leagues: [leagueId],
createdAt: new Date('2025-01-01T00:00:00.000Z'),
});
await teamRepo.create(team);
const membership = {
teamId: team.id,
driverId: '00000000-0000-4000-8000-000000000003',
role: 'driver' as const,
status: 'active' as const,
joinedAt: new Date('2025-01-01T00:00:00.000Z'),
};
await teamMembershipRepo.saveMembership(membership);
const joinRequest = {
id: 'join-req-1',
teamId: team.id,
driverId: '00000000-0000-4000-8000-000000000004',
requestedAt: new Date('2025-01-02T00:00:00.000Z'),
message: 'Please let me in',
};
await teamMembershipRepo.saveJoinRequest(joinRequest);
const byLeague = await teamRepo.findByLeagueId(leagueId);
expect(byLeague.map((t) => t.id)).toContain(team.id);
const members = await teamMembershipRepo.getTeamMembers(team.id);
expect(members).toHaveLength(1);
expect(members[0]?.driverId).toBe(membership.driverId);
const requests = await teamMembershipRepo.getJoinRequests(team.id);
expect(requests.map((r) => r.id)).toContain('join-req-1');
const raceId = '00000000-0000-4000-8000-000000000020';
const protest = Protest.create({
id: '00000000-0000-4000-8000-000000000030',
raceId,
protestingDriverId: membership.driverId,
accusedDriverId: '00000000-0000-4000-8000-000000000005',
incident: { lap: 1, description: 'Contact' },
status: 'pending',
filedAt: new Date('2025-01-03T00:00:00.000Z'),
});
await protestRepo.create(protest);
const penalty = Penalty.rehydrate({
id: '00000000-0000-4000-8000-000000000040',
leagueId,
raceId,
driverId: membership.driverId,
type: 'warning',
reason: 'Unsafe rejoin',
issuedBy: '00000000-0000-4000-8000-000000000006',
status: 'pending',
issuedAt: new Date('2025-01-04T00:00:00.000Z'),
protestId: protest.id,
});
await penaltyRepo.create(penalty);
const penaltiesForRace = await penaltyRepo.findByRaceId(raceId);
expect(penaltiesForRace.map((p) => p.id)).toContain(penalty.id);
const protestsForRace = await protestRepo.findByRaceId(raceId);
expect(protestsForRace.map((p) => p.id)).toContain(protest.id);
});
});

View File

@@ -0,0 +1,97 @@
import 'reflect-metadata';
import { getDataSourceToken } from '@nestjs/typeorm';
import { Test } from '@nestjs/testing';
import type { TestingModule } from '@nestjs/testing';
import { afterEach, describe, expect, it } from 'vitest';
import type { DataSource } from 'typeorm';
import { DatabaseModule } from '../../domain/database/DatabaseModule';
import { PostgresSocialPersistenceModule } from '../postgres/PostgresSocialPersistenceModule';
import { SOCIAL_FEED_REPOSITORY_TOKEN, SOCIAL_GRAPH_REPOSITORY_TOKEN } from './SocialPersistenceTokens';
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import { FeedItemOrmEntity } from '@adapters/social/persistence/typeorm/entities/FeedItemOrmEntity';
import { FriendshipOrmEntity } from '@adapters/social/persistence/typeorm/entities/FriendshipOrmEntity';
import { DriverOrmEntity } from '@adapters/racing/persistence/typeorm/entities/DriverOrmEntity';
describe('PostgresSocialPersistenceModule (integration)', () => {
const shouldRun = Boolean(process.env.DATABASE_URL);
afterEach(() => {
delete process.env.GRIDPILOT_API_PERSISTENCE;
});
(shouldRun ? it : it.skip)('reads driver feed against a real database', async () => {
process.env.GRIDPILOT_API_PERSISTENCE = 'postgres';
const module: TestingModule = await Test.createTestingModule({
imports: [DatabaseModule, PostgresSocialPersistenceModule],
}).compile();
const dataSource = module.get<DataSource>(getDataSourceToken());
const driverRepo = dataSource.getRepository(DriverOrmEntity);
const friendshipRepo = dataSource.getRepository(FriendshipOrmEntity);
const feedOrmRepo = dataSource.getRepository(FeedItemOrmEntity);
await feedOrmRepo.clear();
await friendshipRepo.clear();
await driverRepo.clear();
const driverA = new DriverOrmEntity();
driverA.id = '00000000-0000-0000-0000-000000000001';
driverA.iracingId = '1';
driverA.name = 'A';
driverA.country = 'DE';
driverA.bio = null;
driverA.joinedAt = new Date('2025-01-01T00:00:00.000Z');
const driverB = new DriverOrmEntity();
driverB.id = '00000000-0000-0000-0000-000000000002';
driverB.iracingId = '2';
driverB.name = 'B';
driverB.country = 'DE';
driverB.bio = null;
driverB.joinedAt = new Date('2025-01-01T00:00:00.000Z');
await driverRepo.save([driverA, driverB]);
const friendship = new FriendshipOrmEntity();
friendship.driverId = driverA.id;
friendship.friendId = driverB.id;
friendship.createdAt = new Date('2025-01-01T00:00:00.000Z');
await friendshipRepo.save(friendship);
const item = new FeedItemOrmEntity();
item.id = '00000000-0000-0000-0000-0000000000aa';
item.timestamp = new Date('2025-02-01T00:00:00.000Z');
item.type = 'new-result-posted';
item.actorFriendId = null;
item.actorDriverId = driverB.id;
item.leagueId = null;
item.raceId = null;
item.teamId = null;
item.position = null;
item.headline = 'Hello';
item.body = null;
item.ctaLabel = null;
item.ctaHref = null;
await feedOrmRepo.save(item);
const feedRepo = module.get<IFeedRepository>(SOCIAL_FEED_REPOSITORY_TOKEN);
const socialGraphRepo = module.get<ISocialGraphRepository>(SOCIAL_GRAPH_REPOSITORY_TOKEN);
const friendIds = await socialGraphRepo.getFriendIds(driverA.id);
expect(friendIds).toEqual([driverB.id]);
const feed = await feedRepo.getFeedForDriver(driverA.id);
expect(feed).toHaveLength(1);
expect(feed[0]?.id).toBe(item.id);
await module.close();
});
});

View File

@@ -0,0 +1,53 @@
import 'reflect-metadata';
import { MODULE_METADATA } from '@nestjs/common/constants';
import { Test } from '@nestjs/testing';
import type { TestingModule } from '@nestjs/testing';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { SOCIAL_FEED_REPOSITORY_TOKEN, SOCIAL_GRAPH_REPOSITORY_TOKEN } from './SocialPersistenceTokens';
describe('SocialPersistenceModule', () => {
const originalEnv = { ...process.env };
afterEach(() => {
process.env = originalEnv;
vi.restoreAllMocks();
});
it('uses inmemory providers when GRIDPILOT_API_PERSISTENCE=inmemory', async () => {
vi.resetModules();
process.env.GRIDPILOT_API_PERSISTENCE = 'inmemory';
delete process.env.DATABASE_URL;
const { SocialPersistenceModule } = await import('./SocialPersistenceModule');
const { InMemoryFeedRepository } = await import('@adapters/social/persistence/inmemory/InMemorySocialAndFeed');
const { InMemorySocialGraphRepository } = await import('@adapters/social/persistence/inmemory/InMemorySocialAndFeed');
const module: TestingModule = await Test.createTestingModule({
imports: [SocialPersistenceModule],
}).compile();
const feedRepo = module.get(SOCIAL_FEED_REPOSITORY_TOKEN);
const socialRepo = module.get(SOCIAL_GRAPH_REPOSITORY_TOKEN);
expect(feedRepo).toBeInstanceOf(InMemoryFeedRepository);
expect(socialRepo).toBeInstanceOf(InMemorySocialGraphRepository);
await module.close();
});
it('uses postgres module when GRIDPILOT_API_PERSISTENCE=postgres', async () => {
vi.resetModules();
process.env.GRIDPILOT_API_PERSISTENCE = 'postgres';
delete process.env.DATABASE_URL;
const { SocialPersistenceModule } = await import('./SocialPersistenceModule');
const { PostgresSocialPersistenceModule } = await import('../postgres/PostgresSocialPersistenceModule');
const imports = Reflect.getMetadata(MODULE_METADATA.IMPORTS, SocialPersistenceModule) as unknown[];
expect(imports).toContain(PostgresSocialPersistenceModule);
});
});

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { getApiPersistence } from '../../env';
import { InMemorySocialPersistenceModule } from '../inmemory/InMemorySocialPersistenceModule';
import { PostgresSocialPersistenceModule } from '../postgres/PostgresSocialPersistenceModule';
const selectedPersistenceModule =
getApiPersistence() === 'postgres' ? PostgresSocialPersistenceModule : InMemorySocialPersistenceModule;
@Module({
imports: [selectedPersistenceModule],
exports: [selectedPersistenceModule],
})
export class SocialPersistenceModule {}

View File

@@ -0,0 +1,2 @@
export const SOCIAL_FEED_REPOSITORY_TOKEN = 'IFeedRepository';
export const SOCIAL_GRAPH_REPOSITORY_TOKEN = 'ISocialGraphRepository';