inmemory to postgres
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ACHIEVEMENT_REPOSITORY_TOKEN } from './AchievementPersistenceTokens';
|
||||
import type { IAchievementRepository } from '@core/identity/domain/repositories/IAchievementRepository';
|
||||
|
||||
describe('AchievementPersistenceModule', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset environment before each test
|
||||
delete process.env.GRIDPILOT_API_PERSISTENCE;
|
||||
// Clear module cache to ensure fresh imports
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
it('uses inmemory providers when GRIDPILOT_API_PERSISTENCE=inmemory', async () => {
|
||||
process.env.GRIDPILOT_API_PERSISTENCE = 'inmemory';
|
||||
|
||||
const { AchievementPersistenceModule } = await import('./AchievementPersistenceModule');
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AchievementPersistenceModule],
|
||||
}).compile();
|
||||
|
||||
const repository = module.get<IAchievementRepository>(ACHIEVEMENT_REPOSITORY_TOKEN);
|
||||
// The adapter should provide the domain interface methods
|
||||
expect(repository).toBeDefined();
|
||||
expect(typeof repository.createAchievement).toBe('function');
|
||||
expect(typeof repository.findAchievementById).toBe('function');
|
||||
expect(typeof repository.findAllAchievements).toBe('function');
|
||||
});
|
||||
|
||||
it('uses postgres providers when GRIDPILOT_API_PERSISTENCE=postgres', async () => {
|
||||
const shouldRun = Boolean(process.env.DATABASE_URL);
|
||||
if (!shouldRun) {
|
||||
console.log('Skipping postgres test - DATABASE_URL not set');
|
||||
return;
|
||||
}
|
||||
|
||||
process.env.GRIDPILOT_API_PERSISTENCE = 'postgres';
|
||||
|
||||
const { AchievementPersistenceModule } = await import('./AchievementPersistenceModule');
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AchievementPersistenceModule],
|
||||
}).compile();
|
||||
|
||||
const repository = module.get<IAchievementRepository>(ACHIEVEMENT_REPOSITORY_TOKEN);
|
||||
// The adapter should provide the domain interface methods
|
||||
expect(repository).toBeDefined();
|
||||
expect(typeof repository.createAchievement).toBe('function');
|
||||
expect(typeof repository.findAchievementById).toBe('function');
|
||||
expect(typeof repository.findAllAchievements).toBe('function');
|
||||
});
|
||||
|
||||
it('defaults to inmemory when GRIDPILOT_API_PERSISTENCE is not set', async () => {
|
||||
// Ensure env var is not set
|
||||
delete process.env.GRIDPILOT_API_PERSISTENCE;
|
||||
|
||||
const { AchievementPersistenceModule } = await import('./AchievementPersistenceModule');
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AchievementPersistenceModule],
|
||||
}).compile();
|
||||
|
||||
const repository = module.get<IAchievementRepository>(ACHIEVEMENT_REPOSITORY_TOKEN);
|
||||
// The adapter should provide the domain interface methods
|
||||
expect(repository).toBeDefined();
|
||||
expect(typeof repository.createAchievement).toBe('function');
|
||||
expect(typeof repository.findAchievementById).toBe('function');
|
||||
expect(typeof repository.findAllAchievements).toBe('function');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { getApiPersistence } from '../../env';
|
||||
import { InMemoryAchievementPersistenceModule } from '../inmemory/InMemoryAchievementPersistenceModule';
|
||||
import { PostgresAchievementPersistenceModule } from '../postgres/PostgresAchievementPersistenceModule';
|
||||
|
||||
const selectedPersistenceModule =
|
||||
getApiPersistence() === 'postgres' ? PostgresAchievementPersistenceModule : InMemoryAchievementPersistenceModule;
|
||||
|
||||
@Module({
|
||||
imports: [selectedPersistenceModule],
|
||||
exports: [selectedPersistenceModule],
|
||||
})
|
||||
export class AchievementPersistenceModule {}
|
||||
@@ -0,0 +1,2 @@
|
||||
export const ACHIEVEMENT_REPOSITORY_TOKEN = 'IAchievementRepository';
|
||||
export const USER_ACHIEVEMENT_REPOSITORY_TOKEN = 'IUserAchievementRepository';
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { LoggingModule } from '../../domain/logging/LoggingModule';
|
||||
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
|
||||
import { InMemoryAchievementRepository } from '@adapters/identity/persistence/inmemory/InMemoryAchievementRepository';
|
||||
|
||||
import { ACHIEVEMENT_REPOSITORY_TOKEN } from '../achievement/AchievementPersistenceTokens';
|
||||
import { Achievement, AchievementCategory } from '@core/identity/domain/entities/Achievement';
|
||||
import { UserAchievement } from '@core/identity/domain/entities/UserAchievement';
|
||||
|
||||
// Adapter to convert between domain repository interface and application use case interface
|
||||
class InMemoryAchievementRepositoryAdapter {
|
||||
constructor(private readonly inMemoryRepo: InMemoryAchievementRepository) {}
|
||||
|
||||
// Application use case interface methods
|
||||
async save(achievement: Achievement): Promise<void> {
|
||||
await this.inMemoryRepo.createAchievement(achievement);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Achievement | null> {
|
||||
return await this.inMemoryRepo.findAchievementById(id);
|
||||
}
|
||||
|
||||
// Delegate all other methods to the underlying in-memory repository
|
||||
async findAchievementById(id: string): Promise<Achievement | null> {
|
||||
return await this.inMemoryRepo.findAchievementById(id);
|
||||
}
|
||||
|
||||
async findAllAchievements(): Promise<Achievement[]> {
|
||||
return await this.inMemoryRepo.findAllAchievements();
|
||||
}
|
||||
|
||||
async findAchievementsByCategory(category: AchievementCategory): Promise<Achievement[]> {
|
||||
return await this.inMemoryRepo.findAchievementsByCategory(category);
|
||||
}
|
||||
|
||||
async createAchievement(achievement: Achievement): Promise<Achievement> {
|
||||
return await this.inMemoryRepo.createAchievement(achievement);
|
||||
}
|
||||
|
||||
async findUserAchievementById(id: string): Promise<UserAchievement | null> {
|
||||
return await this.inMemoryRepo.findUserAchievementById(id);
|
||||
}
|
||||
|
||||
async findUserAchievementsByUserId(userId: string): Promise<UserAchievement[]> {
|
||||
return await this.inMemoryRepo.findUserAchievementsByUserId(userId);
|
||||
}
|
||||
|
||||
async findUserAchievementByUserAndAchievement(userId: string, achievementId: string): Promise<UserAchievement | null> {
|
||||
return await this.inMemoryRepo.findUserAchievementByUserAndAchievement(userId, achievementId);
|
||||
}
|
||||
|
||||
async hasUserEarnedAchievement(userId: string, achievementId: string): Promise<boolean> {
|
||||
return await this.inMemoryRepo.hasUserEarnedAchievement(userId, achievementId);
|
||||
}
|
||||
|
||||
async createUserAchievement(userAchievement: UserAchievement): Promise<UserAchievement> {
|
||||
return await this.inMemoryRepo.createUserAchievement(userAchievement);
|
||||
}
|
||||
|
||||
async updateUserAchievement(userAchievement: UserAchievement): Promise<UserAchievement> {
|
||||
return await this.inMemoryRepo.updateUserAchievement(userAchievement);
|
||||
}
|
||||
|
||||
async getAchievementLeaderboard(limit: number): Promise<{ userId: string; points: number; count: number }[]> {
|
||||
return await this.inMemoryRepo.getAchievementLeaderboard(limit);
|
||||
}
|
||||
|
||||
async getUserAchievementStats(userId: string): Promise<{
|
||||
total: number;
|
||||
points: number;
|
||||
byCategory: Record<AchievementCategory, number>;
|
||||
}> {
|
||||
return await this.inMemoryRepo.getUserAchievementStats(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports: [LoggingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: ACHIEVEMENT_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) =>
|
||||
new InMemoryAchievementRepositoryAdapter(new InMemoryAchievementRepository(logger)),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
],
|
||||
exports: [ACHIEVEMENT_REPOSITORY_TOKEN],
|
||||
})
|
||||
export class InMemoryAchievementPersistenceModule {}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { LoggingModule } from '../../domain/logging/LoggingModule';
|
||||
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
|
||||
import type { IAvatarGenerationRepository } from '@core/media/domain/repositories/IAvatarGenerationRepository';
|
||||
import type { IMediaRepository } from '@core/media/domain/repositories/IMediaRepository';
|
||||
import type { IAvatarRepository } from '@core/media/domain/repositories/IAvatarRepository';
|
||||
|
||||
import { InMemoryAvatarGenerationRepository } from '@adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository';
|
||||
|
||||
import { AVATAR_GENERATION_REPOSITORY_TOKEN, MEDIA_REPOSITORY_TOKEN, AVATAR_REPOSITORY_TOKEN } from '../media/MediaPersistenceTokens';
|
||||
|
||||
// Mock implementations for Media and Avatar repositories (inmemory only has AvatarGeneration)
|
||||
class MockMediaRepository implements IMediaRepository {
|
||||
async save(): Promise<void> {}
|
||||
async findById(): Promise<null> { return null; }
|
||||
async findByUploadedBy(): Promise<[]> { return []; }
|
||||
async delete(): Promise<void> {}
|
||||
}
|
||||
|
||||
class MockAvatarRepository implements IAvatarRepository {
|
||||
async save(): Promise<void> {}
|
||||
async findById(): Promise<null> { return null; }
|
||||
async findActiveByDriverId(): Promise<null> { return null; }
|
||||
async findByDriverId(): Promise<[]> { return []; }
|
||||
async delete(): Promise<void> {}
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports: [LoggingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger): IAvatarGenerationRepository =>
|
||||
new InMemoryAvatarGenerationRepository(logger),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: MEDIA_REPOSITORY_TOKEN,
|
||||
useClass: MockMediaRepository,
|
||||
},
|
||||
{
|
||||
provide: AVATAR_REPOSITORY_TOKEN,
|
||||
useClass: MockAvatarRepository,
|
||||
},
|
||||
],
|
||||
exports: [AVATAR_GENERATION_REPOSITORY_TOKEN, MEDIA_REPOSITORY_TOKEN, AVATAR_REPOSITORY_TOKEN],
|
||||
})
|
||||
export class InMemoryMediaPersistenceModule {}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { LoggingModule } from '../../domain/logging/LoggingModule';
|
||||
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
|
||||
import type { INotificationRepository } from '@core/notifications/domain/repositories/INotificationRepository';
|
||||
import type { INotificationPreferenceRepository } from '@core/notifications/domain/repositories/INotificationPreferenceRepository';
|
||||
import type { NotificationService } from '@core/notifications/application/ports/NotificationService';
|
||||
import type { NotificationGatewayRegistry } from '@core/notifications/application/ports/NotificationGateway';
|
||||
|
||||
import { InMemoryNotificationRepository } from '@adapters/notifications/persistence/inmemory/InMemoryNotificationRepository';
|
||||
import { InMemoryNotificationPreferenceRepository } from '@adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository';
|
||||
import { NotificationServiceAdapter } from '@adapters/notifications/ports/NotificationServiceAdapter';
|
||||
import { InMemoryNotificationGatewayRegistry } from '@adapters/notifications/ports/InMemoryNotificationGatewayRegistry';
|
||||
|
||||
import { NOTIFICATION_REPOSITORY_TOKEN, NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN } from '../notifications/NotificationsPersistenceTokens';
|
||||
|
||||
export const NOTIFICATION_SERVICE_TOKEN = 'INotificationService';
|
||||
export const NOTIFICATION_GATEWAY_REGISTRY_TOKEN = 'INotificationGatewayRegistry';
|
||||
|
||||
@Module({
|
||||
imports: [LoggingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: NOTIFICATION_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger): INotificationRepository =>
|
||||
new InMemoryNotificationRepository(logger),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger): INotificationPreferenceRepository =>
|
||||
new InMemoryNotificationPreferenceRepository(logger),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: NOTIFICATION_GATEWAY_REGISTRY_TOKEN,
|
||||
useFactory: (): NotificationGatewayRegistry =>
|
||||
new InMemoryNotificationGatewayRegistry(),
|
||||
inject: [],
|
||||
},
|
||||
{
|
||||
provide: NOTIFICATION_SERVICE_TOKEN,
|
||||
useFactory: (
|
||||
notificationRepo: INotificationRepository,
|
||||
preferenceRepo: INotificationPreferenceRepository,
|
||||
gatewayRegistry: NotificationGatewayRegistry,
|
||||
logger: Logger,
|
||||
): NotificationService =>
|
||||
new NotificationServiceAdapter(notificationRepo, preferenceRepo, gatewayRegistry, logger),
|
||||
inject: [
|
||||
NOTIFICATION_REPOSITORY_TOKEN,
|
||||
NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN,
|
||||
NOTIFICATION_GATEWAY_REGISTRY_TOKEN,
|
||||
'Logger',
|
||||
],
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
NOTIFICATION_REPOSITORY_TOKEN,
|
||||
NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN,
|
||||
NOTIFICATION_SERVICE_TOKEN,
|
||||
NOTIFICATION_GATEWAY_REGISTRY_TOKEN,
|
||||
],
|
||||
})
|
||||
export class InMemoryNotificationsPersistenceModule {}
|
||||
@@ -0,0 +1,68 @@
|
||||
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 { AVATAR_GENERATION_REPOSITORY_TOKEN, MEDIA_REPOSITORY_TOKEN, AVATAR_REPOSITORY_TOKEN } from './MediaPersistenceTokens';
|
||||
|
||||
describe('MediaPersistenceModule', () => {
|
||||
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 { MediaPersistenceModule } = await import('./MediaPersistenceModule');
|
||||
const { InMemoryAvatarGenerationRepository } = await import('@adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository');
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [MediaPersistenceModule],
|
||||
}).compile();
|
||||
|
||||
const avatarGenRepo = module.get(AVATAR_GENERATION_REPOSITORY_TOKEN);
|
||||
expect(avatarGenRepo).toBeInstanceOf(InMemoryAvatarGenerationRepository);
|
||||
|
||||
// Media and Avatar repos are mocks in inmemory mode
|
||||
const mediaRepo = module.get(MEDIA_REPOSITORY_TOKEN);
|
||||
const avatarRepo = module.get(AVATAR_REPOSITORY_TOKEN);
|
||||
expect(mediaRepo).toBeDefined();
|
||||
expect(avatarRepo).toBeDefined();
|
||||
|
||||
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 { MediaPersistenceModule } = await import('./MediaPersistenceModule');
|
||||
const { PostgresMediaPersistenceModule } = await import('../postgres/PostgresMediaPersistenceModule');
|
||||
|
||||
const imports = Reflect.getMetadata(MODULE_METADATA.IMPORTS, MediaPersistenceModule) as unknown[];
|
||||
expect(imports).toContain(PostgresMediaPersistenceModule);
|
||||
});
|
||||
|
||||
it('defaults to inmemory when GRIDPILOT_API_PERSISTENCE is not set', async () => {
|
||||
vi.resetModules();
|
||||
|
||||
delete process.env.GRIDPILOT_API_PERSISTENCE;
|
||||
delete process.env.DATABASE_URL;
|
||||
|
||||
const { MediaPersistenceModule } = await import('./MediaPersistenceModule');
|
||||
const { InMemoryMediaPersistenceModule } = await import('../inmemory/InMemoryMediaPersistenceModule');
|
||||
|
||||
const imports = Reflect.getMetadata(MODULE_METADATA.IMPORTS, MediaPersistenceModule) as unknown[];
|
||||
expect(imports).toContain(InMemoryMediaPersistenceModule);
|
||||
});
|
||||
});
|
||||
14
apps/api/src/persistence/media/MediaPersistenceModule.ts
Normal file
14
apps/api/src/persistence/media/MediaPersistenceModule.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { getApiPersistence } from '../../env';
|
||||
import { InMemoryMediaPersistenceModule } from '../inmemory/InMemoryMediaPersistenceModule';
|
||||
import { PostgresMediaPersistenceModule } from '../postgres/PostgresMediaPersistenceModule';
|
||||
|
||||
const selectedPersistenceModule =
|
||||
getApiPersistence() === 'postgres' ? PostgresMediaPersistenceModule : InMemoryMediaPersistenceModule;
|
||||
|
||||
@Module({
|
||||
imports: [selectedPersistenceModule],
|
||||
exports: [selectedPersistenceModule],
|
||||
})
|
||||
export class MediaPersistenceModule {}
|
||||
3
apps/api/src/persistence/media/MediaPersistenceTokens.ts
Normal file
3
apps/api/src/persistence/media/MediaPersistenceTokens.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository';
|
||||
export const MEDIA_REPOSITORY_TOKEN = 'IMediaRepository';
|
||||
export const AVATAR_REPOSITORY_TOKEN = 'IAvatarRepository';
|
||||
@@ -0,0 +1,73 @@
|
||||
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 { NOTIFICATION_REPOSITORY_TOKEN, NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN, NOTIFICATION_SERVICE_TOKEN, NOTIFICATION_GATEWAY_REGISTRY_TOKEN } from './NotificationsPersistenceTokens';
|
||||
|
||||
describe('NotificationsPersistenceModule', () => {
|
||||
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 { NotificationsPersistenceModule } = await import('./NotificationsPersistenceModule');
|
||||
const { InMemoryNotificationRepository } = await import('@adapters/notifications/persistence/inmemory/InMemoryNotificationRepository');
|
||||
const { InMemoryNotificationPreferenceRepository } = await import('@adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository');
|
||||
const { NotificationServiceAdapter } = await import('@adapters/notifications/ports/NotificationServiceAdapter');
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [NotificationsPersistenceModule],
|
||||
}).compile();
|
||||
|
||||
const notificationRepo = module.get(NOTIFICATION_REPOSITORY_TOKEN);
|
||||
expect(notificationRepo).toBeInstanceOf(InMemoryNotificationRepository);
|
||||
|
||||
const preferenceRepo = module.get(NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN);
|
||||
expect(preferenceRepo).toBeInstanceOf(InMemoryNotificationPreferenceRepository);
|
||||
|
||||
const notificationService = module.get(NOTIFICATION_SERVICE_TOKEN);
|
||||
expect(notificationService).toBeInstanceOf(NotificationServiceAdapter);
|
||||
|
||||
const gatewayRegistry = module.get(NOTIFICATION_GATEWAY_REGISTRY_TOKEN);
|
||||
expect(gatewayRegistry).toBeDefined();
|
||||
|
||||
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 { NotificationsPersistenceModule } = await import('./NotificationsPersistenceModule');
|
||||
const { PostgresNotificationsPersistenceModule } = await import('../postgres/PostgresNotificationsPersistenceModule');
|
||||
|
||||
const imports = Reflect.getMetadata(MODULE_METADATA.IMPORTS, NotificationsPersistenceModule) as unknown[];
|
||||
expect(imports).toContain(PostgresNotificationsPersistenceModule);
|
||||
});
|
||||
|
||||
it('defaults to inmemory when GRIDPILOT_API_PERSISTENCE is not set', async () => {
|
||||
vi.resetModules();
|
||||
|
||||
delete process.env.GRIDPILOT_API_PERSISTENCE;
|
||||
delete process.env.DATABASE_URL;
|
||||
|
||||
const { NotificationsPersistenceModule } = await import('./NotificationsPersistenceModule');
|
||||
const { InMemoryNotificationsPersistenceModule } = await import('../inmemory/InMemoryNotificationsPersistenceModule');
|
||||
|
||||
const imports = Reflect.getMetadata(MODULE_METADATA.IMPORTS, NotificationsPersistenceModule) as unknown[];
|
||||
expect(imports).toContain(InMemoryNotificationsPersistenceModule);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { getApiPersistence } from '../../env';
|
||||
import { InMemoryNotificationsPersistenceModule } from '../inmemory/InMemoryNotificationsPersistenceModule';
|
||||
import { PostgresNotificationsPersistenceModule } from '../postgres/PostgresNotificationsPersistenceModule';
|
||||
|
||||
const selectedPersistenceModule =
|
||||
getApiPersistence() === 'postgres' ? PostgresNotificationsPersistenceModule : InMemoryNotificationsPersistenceModule;
|
||||
|
||||
@Module({
|
||||
imports: [selectedPersistenceModule],
|
||||
exports: [selectedPersistenceModule],
|
||||
})
|
||||
export class NotificationsPersistenceModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const NOTIFICATION_REPOSITORY_TOKEN = 'INotificationRepository';
|
||||
export const NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreferenceRepository';
|
||||
export const NOTIFICATION_SERVICE_TOKEN = 'INotificationService';
|
||||
export const NOTIFICATION_GATEWAY_REGISTRY_TOKEN = 'INotificationGatewayRegistry';
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule, getDataSourceToken } from '@nestjs/typeorm';
|
||||
import type { DataSource } from 'typeorm';
|
||||
|
||||
import { AchievementOrmEntity } from '@adapters/achievement/persistence/typeorm/entities/AchievementOrmEntity';
|
||||
import { UserAchievementOrmEntity } from '@adapters/achievement/persistence/typeorm/entities/UserAchievementOrmEntity';
|
||||
import { TypeOrmAchievementRepository } from '@adapters/achievement/persistence/typeorm/repositories/TypeOrmAchievementRepository';
|
||||
import { AchievementOrmMapper } from '@adapters/achievement/persistence/typeorm/mappers/AchievementOrmMapper';
|
||||
|
||||
import { ACHIEVEMENT_REPOSITORY_TOKEN } from '../achievement/AchievementPersistenceTokens';
|
||||
import { Achievement, AchievementCategory } from '@core/identity/domain/entities/Achievement';
|
||||
import { UserAchievement } from '@core/identity/domain/entities/UserAchievement';
|
||||
|
||||
// Adapter to convert between domain repository interface and application use case interface
|
||||
class AchievementRepositoryAdapter {
|
||||
constructor(private readonly typeOrmRepo: TypeOrmAchievementRepository) {}
|
||||
|
||||
// Application use case interface methods
|
||||
async save(achievement: Achievement): Promise<void> {
|
||||
await this.typeOrmRepo.createAchievement(achievement);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Achievement | null> {
|
||||
return await this.typeOrmRepo.findAchievementById(id);
|
||||
}
|
||||
|
||||
// Delegate all other methods to the underlying TypeOrm repository
|
||||
async findAchievementById(id: string): Promise<Achievement | null> {
|
||||
return await this.typeOrmRepo.findAchievementById(id);
|
||||
}
|
||||
|
||||
async findAllAchievements(): Promise<Achievement[]> {
|
||||
return await this.typeOrmRepo.findAllAchievements();
|
||||
}
|
||||
|
||||
async findAchievementsByCategory(category: AchievementCategory): Promise<Achievement[]> {
|
||||
return await this.typeOrmRepo.findAchievementsByCategory(category);
|
||||
}
|
||||
|
||||
async createAchievement(achievement: Achievement): Promise<Achievement> {
|
||||
return await this.typeOrmRepo.createAchievement(achievement);
|
||||
}
|
||||
|
||||
async findUserAchievementById(id: string): Promise<UserAchievement | null> {
|
||||
return await this.typeOrmRepo.findUserAchievementById(id);
|
||||
}
|
||||
|
||||
async findUserAchievementsByUserId(userId: string): Promise<UserAchievement[]> {
|
||||
return await this.typeOrmRepo.findUserAchievementsByUserId(userId);
|
||||
}
|
||||
|
||||
async findUserAchievementByUserAndAchievement(userId: string, achievementId: string): Promise<UserAchievement | null> {
|
||||
return await this.typeOrmRepo.findUserAchievementByUserAndAchievement(userId, achievementId);
|
||||
}
|
||||
|
||||
async hasUserEarnedAchievement(userId: string, achievementId: string): Promise<boolean> {
|
||||
return await this.typeOrmRepo.hasUserEarnedAchievement(userId, achievementId);
|
||||
}
|
||||
|
||||
async createUserAchievement(userAchievement: UserAchievement): Promise<UserAchievement> {
|
||||
return await this.typeOrmRepo.createUserAchievement(userAchievement);
|
||||
}
|
||||
|
||||
async updateUserAchievement(userAchievement: UserAchievement): Promise<UserAchievement> {
|
||||
return await this.typeOrmRepo.updateUserAchievement(userAchievement);
|
||||
}
|
||||
|
||||
async getAchievementLeaderboard(limit: number): Promise<{ userId: string; points: number; count: number }[]> {
|
||||
return await this.typeOrmRepo.getAchievementLeaderboard(limit);
|
||||
}
|
||||
|
||||
async getUserAchievementStats(userId: string): Promise<{
|
||||
total: number;
|
||||
points: number;
|
||||
byCategory: Record<AchievementCategory, number>;
|
||||
}> {
|
||||
return await this.typeOrmRepo.getUserAchievementStats(userId);
|
||||
}
|
||||
}
|
||||
|
||||
const typeOrmFeatureImports = [TypeOrmModule.forFeature([AchievementOrmEntity, UserAchievementOrmEntity])];
|
||||
|
||||
@Module({
|
||||
imports: [...typeOrmFeatureImports],
|
||||
providers: [
|
||||
{ provide: AchievementOrmMapper, useFactory: () => new AchievementOrmMapper() },
|
||||
{
|
||||
provide: ACHIEVEMENT_REPOSITORY_TOKEN,
|
||||
useFactory: (dataSource: DataSource, mapper: AchievementOrmMapper) =>
|
||||
new AchievementRepositoryAdapter(new TypeOrmAchievementRepository(dataSource, mapper)),
|
||||
inject: [getDataSourceToken(), AchievementOrmMapper],
|
||||
},
|
||||
],
|
||||
exports: [ACHIEVEMENT_REPOSITORY_TOKEN],
|
||||
})
|
||||
export class PostgresAchievementPersistenceModule {}
|
||||
@@ -3,8 +3,8 @@ 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 { TypeOrmAuthRepository } from '@adapters/identity/persistence/typeorm/repositories/TypeOrmAuthRepository';
|
||||
import { TypeOrmUserRepository } from '@adapters/identity/persistence/typeorm/repositories/TypeOrmUserRepository';
|
||||
import { UserOrmMapper } from '@adapters/identity/persistence/typeorm/mappers/UserOrmMapper';
|
||||
import { InMemoryPasswordHashingService } from '@adapters/identity/services/InMemoryPasswordHashingService';
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule, getDataSourceToken } from '@nestjs/typeorm';
|
||||
import type { DataSource } from 'typeorm';
|
||||
|
||||
import { MediaOrmEntity } from '@adapters/media/persistence/typeorm/entities/MediaOrmEntity';
|
||||
import { AvatarOrmEntity } from '@adapters/media/persistence/typeorm/entities/AvatarOrmEntity';
|
||||
import { AvatarGenerationRequestOrmEntity } from '@adapters/media/persistence/typeorm/entities/AvatarGenerationRequestOrmEntity';
|
||||
|
||||
import { TypeOrmMediaRepository } from '@adapters/media/persistence/typeorm/repositories/TypeOrmMediaRepository';
|
||||
import { TypeOrmAvatarRepository } from '@adapters/media/persistence/typeorm/repositories/TypeOrmAvatarRepository';
|
||||
import { TypeOrmAvatarGenerationRepository } from '@adapters/media/persistence/typeorm/repositories/TypeOrmAvatarGenerationRepository';
|
||||
|
||||
import { MediaOrmMapper } from '@adapters/media/persistence/typeorm/mappers/MediaOrmMapper';
|
||||
import { AvatarOrmMapper } from '@adapters/media/persistence/typeorm/mappers/AvatarOrmMapper';
|
||||
import { AvatarGenerationRequestOrmMapper } from '@adapters/media/persistence/typeorm/mappers/AvatarGenerationRequestOrmMapper';
|
||||
|
||||
import {
|
||||
AVATAR_GENERATION_REPOSITORY_TOKEN,
|
||||
MEDIA_REPOSITORY_TOKEN,
|
||||
AVATAR_REPOSITORY_TOKEN,
|
||||
} from '../media/MediaPersistenceTokens';
|
||||
|
||||
const typeOrmFeatureImports = [
|
||||
TypeOrmModule.forFeature([
|
||||
MediaOrmEntity,
|
||||
AvatarOrmEntity,
|
||||
AvatarGenerationRequestOrmEntity,
|
||||
]),
|
||||
];
|
||||
|
||||
@Module({
|
||||
imports: [...typeOrmFeatureImports],
|
||||
providers: [
|
||||
{ provide: MediaOrmMapper, useFactory: () => new MediaOrmMapper() },
|
||||
{ provide: AvatarOrmMapper, useFactory: () => new AvatarOrmMapper() },
|
||||
{ provide: AvatarGenerationRequestOrmMapper, useFactory: () => new AvatarGenerationRequestOrmMapper() },
|
||||
{
|
||||
provide: MEDIA_REPOSITORY_TOKEN,
|
||||
useFactory: (dataSource: DataSource, mapper: MediaOrmMapper) => new TypeOrmMediaRepository(dataSource, mapper),
|
||||
inject: [getDataSourceToken(), MediaOrmMapper],
|
||||
},
|
||||
{
|
||||
provide: AVATAR_REPOSITORY_TOKEN,
|
||||
useFactory: (dataSource: DataSource, mapper: AvatarOrmMapper) => new TypeOrmAvatarRepository(dataSource, mapper),
|
||||
inject: [getDataSourceToken(), AvatarOrmMapper],
|
||||
},
|
||||
{
|
||||
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
|
||||
useFactory: (dataSource: DataSource, mapper: AvatarGenerationRequestOrmMapper) =>
|
||||
new TypeOrmAvatarGenerationRepository(dataSource, mapper),
|
||||
inject: [getDataSourceToken(), AvatarGenerationRequestOrmMapper],
|
||||
},
|
||||
],
|
||||
exports: [AVATAR_GENERATION_REPOSITORY_TOKEN, MEDIA_REPOSITORY_TOKEN, AVATAR_REPOSITORY_TOKEN],
|
||||
})
|
||||
export class PostgresMediaPersistenceModule {}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule, getDataSourceToken } from '@nestjs/typeorm';
|
||||
import type { DataSource } from 'typeorm';
|
||||
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
import type { NotificationService } from '@core/notifications/application/ports/NotificationService';
|
||||
import type { NotificationGatewayRegistry } from '@core/notifications/application/ports/NotificationGateway';
|
||||
import type { INotificationRepository } from '@core/notifications/domain/repositories/INotificationRepository';
|
||||
import type { INotificationPreferenceRepository } from '@core/notifications/domain/repositories/INotificationPreferenceRepository';
|
||||
|
||||
import { NotificationOrmEntity } from '@adapters/notifications/persistence/typeorm/entities/NotificationOrmEntity';
|
||||
import { NotificationPreferenceOrmEntity } from '@adapters/notifications/persistence/typeorm/entities/NotificationPreferenceOrmEntity';
|
||||
|
||||
import { TypeOrmNotificationRepository } from '@adapters/notifications/persistence/typeorm/repositories/TypeOrmNotificationRepository';
|
||||
import { TypeOrmNotificationPreferenceRepository } from '@adapters/notifications/persistence/typeorm/repositories/TypeOrmNotificationPreferenceRepository';
|
||||
|
||||
import { NotificationOrmMapper } from '@adapters/notifications/persistence/typeorm/mappers/NotificationOrmMapper';
|
||||
import { NotificationPreferenceOrmMapper } from '@adapters/notifications/persistence/typeorm/mappers/NotificationPreferenceOrmMapper';
|
||||
|
||||
import { NotificationServiceAdapter } from '@adapters/notifications/ports/NotificationServiceAdapter';
|
||||
import { InMemoryNotificationGatewayRegistry } from '@adapters/notifications/ports/InMemoryNotificationGatewayRegistry';
|
||||
|
||||
import { NOTIFICATION_REPOSITORY_TOKEN, NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN } from '../notifications/NotificationsPersistenceTokens';
|
||||
|
||||
export const NOTIFICATION_SERVICE_TOKEN = 'INotificationService';
|
||||
export const NOTIFICATION_GATEWAY_REGISTRY_TOKEN = 'INotificationGatewayRegistry';
|
||||
|
||||
const typeOrmFeatureImports = [
|
||||
TypeOrmModule.forFeature([
|
||||
NotificationOrmEntity,
|
||||
NotificationPreferenceOrmEntity,
|
||||
]),
|
||||
];
|
||||
|
||||
@Module({
|
||||
imports: [...typeOrmFeatureImports],
|
||||
providers: [
|
||||
{ provide: NotificationOrmMapper, useFactory: () => new NotificationOrmMapper() },
|
||||
{ provide: NotificationPreferenceOrmMapper, useFactory: () => new NotificationPreferenceOrmMapper() },
|
||||
{
|
||||
provide: NOTIFICATION_REPOSITORY_TOKEN,
|
||||
useFactory: (dataSource: DataSource, mapper: NotificationOrmMapper) =>
|
||||
new TypeOrmNotificationRepository(dataSource, mapper),
|
||||
inject: [getDataSourceToken(), NotificationOrmMapper],
|
||||
},
|
||||
{
|
||||
provide: NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN,
|
||||
useFactory: (dataSource: DataSource, mapper: NotificationPreferenceOrmMapper) =>
|
||||
new TypeOrmNotificationPreferenceRepository(dataSource, mapper),
|
||||
inject: [getDataSourceToken(), NotificationPreferenceOrmMapper],
|
||||
},
|
||||
{
|
||||
provide: NOTIFICATION_GATEWAY_REGISTRY_TOKEN,
|
||||
useFactory: (): NotificationGatewayRegistry =>
|
||||
new InMemoryNotificationGatewayRegistry(),
|
||||
inject: [],
|
||||
},
|
||||
{
|
||||
provide: NOTIFICATION_SERVICE_TOKEN,
|
||||
useFactory: (
|
||||
notificationRepo: INotificationRepository,
|
||||
preferenceRepo: INotificationPreferenceRepository,
|
||||
gatewayRegistry: NotificationGatewayRegistry,
|
||||
logger: Logger,
|
||||
): NotificationService =>
|
||||
new NotificationServiceAdapter(notificationRepo, preferenceRepo, gatewayRegistry, logger),
|
||||
inject: [
|
||||
NOTIFICATION_REPOSITORY_TOKEN,
|
||||
NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN,
|
||||
NOTIFICATION_GATEWAY_REGISTRY_TOKEN,
|
||||
'Logger',
|
||||
],
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
NOTIFICATION_REPOSITORY_TOKEN,
|
||||
NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN,
|
||||
NOTIFICATION_SERVICE_TOKEN,
|
||||
NOTIFICATION_GATEWAY_REGISTRY_TOKEN,
|
||||
],
|
||||
})
|
||||
export class PostgresNotificationsPersistenceModule {}
|
||||
@@ -9,8 +9,8 @@ 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 { TypeOrmAuthRepository } from '@adapters/identity/persistence/typeorm/repositories/TypeOrmAuthRepository';
|
||||
import { TypeOrmUserRepository } from '@adapters/identity/persistence/typeorm/repositories/TypeOrmUserRepository';
|
||||
import { UserOrmMapper } from '@adapters/identity/persistence/typeorm/mappers/UserOrmMapper';
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
|
||||
Reference in New Issue
Block a user