import { ControllerContainer, ControllerContainerInterface, MapperInterface, ServiceIdentifier, } from '@standardnotes/domain-core' import Redis from 'ioredis' import { Container, interfaces } from 'inversify' import { MongoRepository, Repository } from 'typeorm' import * as winston from 'winston' import { SNSClient, SNSClientConfig } from '@aws-sdk/client-sns' import { Revision } from '../Domain/Revision/Revision' import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata' import { RevisionRepositoryInterface } from '../Domain/Revision/RevisionRepositoryInterface' import { SQLLegacyRevisionRepository } from '../Infra/TypeORM/SQL/SQLLegacyRevisionRepository' import { SQLLegacyRevision } from '../Infra/TypeORM/SQL/SQLLegacyRevision' import { AppDataSource } from './DataSource' import { Env } from './Env' import TYPES from './Types' import { TokenDecoderInterface, CrossServiceTokenData, TokenDecoder } from '@standardnotes/security' import { TimerInterface, Timer } from '@standardnotes/time' import { ApiGatewayAuthMiddleware } from '../Infra/InversifyExpress/Middleware/ApiGatewayAuthMiddleware' import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision' import { GetRequiredRoleToViewRevision } from '../Domain/UseCase/GetRequiredRoleToViewRevision/GetRequiredRoleToViewRevision' import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision' import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada' import { RevisionMetadataHttpMapper } from '../Mapping/Http/RevisionMetadataHttpMapper' import { S3Client } from '@aws-sdk/client-s3' import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs' import { DomainEventMessageHandlerInterface, DomainEventHandlerInterface, DomainEventSubscriberFactoryInterface, DomainEventPublisherInterface, } from '@standardnotes/domain-events' import { SQSEventMessageHandler, SQSDomainEventSubscriberFactory, DirectCallEventMessageHandler, DirectCallDomainEventPublisher, SQSOpenTelemetryEventMessageHandler, SNSOpenTelemetryDomainEventPublisher, } from '@standardnotes/domain-events-infra' import { DumpRepositoryInterface } from '../Domain/Dump/DumpRepositoryInterface' import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler' import { ItemDumpedEventHandler } from '../Domain/Handler/ItemDumpedEventHandler' import { RevisionsCopyRequestedEventHandler } from '../Domain/Handler/RevisionsCopyRequestedEventHandler' import { CopyRevisions } from '../Domain/UseCase/CopyRevisions/CopyRevisions' import { FSDumpRepository } from '../Infra/FS/FSDumpRepository' import { S3DumpRepository } from '../Infra/S3/S3ItemDumpRepository' import { RevisionItemStringMapper } from '../Mapping/Backup/RevisionItemStringMapper' import { BaseRevisionsController } from '../Infra/InversifyExpress/Base/BaseRevisionsController' import { Transform } from 'stream' import { MongoDBRevision } from '../Infra/TypeORM/MongoDB/MongoDBRevision' import { MongoDBRevisionRepository } from '../Infra/TypeORM/MongoDB/MongoDBRevisionRepository' import { SQLLegacyRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/SQL/SQLLegacyRevisionMetadataPersistenceMapper' import { SQLLegacyRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLLegacyRevisionPersistenceMapper' import { MongoDBRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/MongoDB/MongoDBRevisionMetadataPersistenceMapper' import { MongoDBRevisionPersistenceMapper } from '../Mapping/Persistence/MongoDB/MongoDBRevisionPersistenceMapper' import { RevisionHttpMapper } from '../Mapping/Http/RevisionHttpMapper' import { RevisionRepositoryResolverInterface } from '../Domain/Revision/RevisionRepositoryResolverInterface' import { TypeORMRevisionRepositoryResolver } from '../Infra/TypeORM/TypeORMRevisionRepositoryResolver' import { RevisionMetadataHttpRepresentation } from '../Mapping/Http/RevisionMetadataHttpRepresentation' import { RevisionHttpRepresentation } from '../Mapping/Http/RevisionHttpRepresentation' import { TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser } from '../Domain/UseCase/Transition/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser/TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser' import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface' import { DomainEventFactory } from '../Domain/Event/DomainEventFactory' import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision' import { SQLRevisionRepository } from '../Infra/TypeORM/SQL/SQLRevisionRepository' import { SQLRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionMetadataPersistenceMapper' import { SQLRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionPersistenceMapper' import { RemoveRevisionsFromSharedVault } from '../Domain/UseCase/RemoveRevisionsFromSharedVault/RemoveRevisionsFromSharedVault' import { ItemRemovedFromSharedVaultEventHandler } from '../Domain/Handler/ItemRemovedFromSharedVaultEventHandler' import { TransitionRequestedEventHandler } from '../Domain/Handler/TransitionRequestedEventHandler' import { SharedVaultRemovedEventHandler } from '../Domain/Handler/SharedVaultRemovedEventHandler' import { TransitionRepositoryInterface } from '../Domain/Transition/TransitionRepositoryInterface' import { RedisTransitionRepository } from '../Infra/Redis/RedisTransitionRepository' import { CreateRevisionFromDump } from '../Domain/UseCase/CreateRevisionFromDump/CreateRevisionFromDump' export class ContainerConfigLoader { constructor(private mode: 'server' | 'worker' = 'server') {} async load(configuration?: { controllerConatiner?: ControllerContainerInterface directCallDomainEventPublisher?: DirectCallDomainEventPublisher logger?: Transform environmentOverrides?: { [name: string]: string } }): Promise { const directCallDomainEventPublisher = configuration?.directCallDomainEventPublisher ?? new DirectCallDomainEventPublisher() const env: Env = new Env(configuration?.environmentOverrides) env.load() const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server' const isConfiguredForSelfHosting = env.get('MODE', true) === 'self-hosted' const isConfiguredForHomeServerOrSelfHosting = isConfiguredForHomeServer || isConfiguredForSelfHosting const isSecondaryDatabaseEnabled = env.get('SECONDARY_DB_ENABLED', true) === 'true' const isConfiguredForInMemoryCache = env.get('CACHE_TYPE', true) === 'memory' const container = new Container({ defaultScope: 'Singleton', }) container .bind(TYPES.Revisions_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING) .toConstantValue(isConfiguredForHomeServerOrSelfHosting) if (!isConfiguredForInMemoryCache) { const redisUrl = env.get('REDIS_URL') const isRedisInClusterMode = redisUrl.indexOf(',') > 0 let redis if (isRedisInClusterMode) { redis = new Redis.Cluster(redisUrl.split(',')) } else { redis = new Redis(redisUrl) } container.bind(TYPES.Revisions_Redis).toConstantValue(redis) container .bind(TYPES.Revisions_TransitionStatusRepository) .toConstantValue(new RedisTransitionRepository(container.get(TYPES.Revisions_Redis))) } let logger: winston.Logger if (configuration?.logger) { logger = configuration.logger as winston.Logger } else { const winstonFormatters = [winston.format.splat(), winston.format.json()] logger = winston.createLogger({ level: env.get('LOG_LEVEL', true) || 'info', format: winston.format.combine(...winstonFormatters), transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })], defaultMeta: { service: 'revisions' }, }) } container.bind(TYPES.Revisions_Logger).toConstantValue(logger) container.bind(TYPES.Revisions_Timer).toDynamicValue(() => new Timer()) const appDataSource = new AppDataSource({ env, runMigrations: this.mode === 'server' }) await appDataSource.initialize() logger.debug('Database initialized') container.bind(TYPES.Revisions_Env).toConstantValue(env) container.bind(TYPES.Revisions_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development') if (!isConfiguredForHomeServer) { // env vars container.bind(TYPES.Revisions_SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN')) container.bind(TYPES.Revisions_SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true)) container.bind(TYPES.Revisions_SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL')) container.bind(TYPES.Revisions_S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true)) container.bind(TYPES.Revisions_S3_BACKUP_BUCKET_NAME).toConstantValue(env.get('S3_BACKUP_BUCKET_NAME', true)) container.bind(TYPES.Revisions_SNS).toDynamicValue((context: interfaces.Context) => { const env: Env = context.container.get(TYPES.Revisions_Env) const snsConfig: SNSClientConfig = { apiVersion: 'latest', region: env.get('SNS_AWS_REGION', true), } if (env.get('SNS_ENDPOINT', true)) { snsConfig.endpoint = env.get('SNS_ENDPOINT', true) } if (env.get('SNS_ACCESS_KEY_ID', true) && env.get('SNS_SECRET_ACCESS_KEY', true)) { snsConfig.credentials = { accessKeyId: env.get('SNS_ACCESS_KEY_ID', true), secretAccessKey: env.get('SNS_SECRET_ACCESS_KEY', true), } } return new SNSClient(snsConfig) }) container .bind(TYPES.Revisions_DomainEventPublisher) .toDynamicValue((context: interfaces.Context) => { return new SNSOpenTelemetryDomainEventPublisher( context.container.get(TYPES.Revisions_SNS), context.container.get(TYPES.Revisions_SNS_TOPIC_ARN), ) }) container.bind(TYPES.Revisions_SQS).toDynamicValue((context: interfaces.Context) => { const env: Env = context.container.get(TYPES.Revisions_Env) const sqsConfig: SQSClientConfig = { region: env.get('SQS_AWS_REGION'), } if (env.get('SQS_ENDPOINT', true)) { sqsConfig.endpoint = env.get('SQS_ENDPOINT', true) } if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) { sqsConfig.credentials = { accessKeyId: env.get('SQS_ACCESS_KEY_ID', true), secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true), } } return new SQSClient(sqsConfig) }) container.bind(TYPES.Revisions_S3).toDynamicValue((context: interfaces.Context) => { const env: Env = context.container.get(TYPES.Revisions_Env) let s3Client = undefined if (env.get('S3_AWS_REGION', true)) { s3Client = new S3Client({ apiVersion: 'latest', region: env.get('S3_AWS_REGION', true), }) } return s3Client }) } else { container .bind(TYPES.Revisions_DomainEventPublisher) .toConstantValue(directCallDomainEventPublisher) } container .bind(TYPES.Revisions_DomainEventFactory) .toConstantValue(new DomainEventFactory(container.get(TYPES.Revisions_Timer))) // Map container .bind>( TYPES.Revisions_SQLLegacyRevisionMetadataPersistenceMapper, ) .toConstantValue(new SQLLegacyRevisionMetadataPersistenceMapper()) container .bind>(TYPES.Revisions_SQLRevisionMetadataPersistenceMapper) .toConstantValue(new SQLRevisionMetadataPersistenceMapper()) container .bind>(TYPES.Revisions_SQLLegacyRevisionPersistenceMapper) .toConstantValue(new SQLLegacyRevisionPersistenceMapper(container.get(TYPES.Revisions_Timer))) container .bind>(TYPES.Revisions_SQLRevisionPersistenceMapper) .toConstantValue(new SQLRevisionPersistenceMapper(container.get(TYPES.Revisions_Timer))) container .bind>( TYPES.Revisions_MongoDBRevisionMetadataPersistenceMapper, ) .toConstantValue(new MongoDBRevisionMetadataPersistenceMapper()) container .bind>(TYPES.Revisions_MongoDBRevisionPersistenceMapper) .toConstantValue(new MongoDBRevisionPersistenceMapper(container.get(TYPES.Revisions_Timer))) // ORM container .bind>(TYPES.Revisions_ORMLegacyRevisionRepository) .toDynamicValue(() => appDataSource.getRepository(SQLLegacyRevision)) container .bind>(TYPES.Revisions_ORMRevisionRepository) .toConstantValue(appDataSource.getRepository(SQLRevision)) // Repositories container .bind(TYPES.Revisions_SQLRevisionRepository) .toConstantValue( isConfiguredForHomeServerOrSelfHosting ? new SQLRevisionRepository( container.get>(TYPES.Revisions_ORMRevisionRepository), container.get>( TYPES.Revisions_SQLRevisionMetadataPersistenceMapper, ), container.get>(TYPES.Revisions_SQLRevisionPersistenceMapper), container.get(TYPES.Revisions_Logger), ) : new SQLLegacyRevisionRepository( container.get>(TYPES.Revisions_ORMLegacyRevisionRepository), container.get>( TYPES.Revisions_SQLLegacyRevisionMetadataPersistenceMapper, ), container.get>( TYPES.Revisions_SQLLegacyRevisionPersistenceMapper, ), container.get(TYPES.Revisions_Logger), ), ) if (isSecondaryDatabaseEnabled) { container .bind>(TYPES.Revisions_ORMMongoRevisionRepository) .toConstantValue(appDataSource.getMongoRepository(MongoDBRevision)) container .bind(TYPES.Revisions_MongoDBRevisionRepository) .toConstantValue( new MongoDBRevisionRepository( container.get>(TYPES.Revisions_ORMMongoRevisionRepository), container.get>( TYPES.Revisions_MongoDBRevisionMetadataPersistenceMapper, ), container.get>(TYPES.Revisions_MongoDBRevisionPersistenceMapper), container.get(TYPES.Revisions_Logger), ), ) } container .bind(TYPES.Revisions_RevisionRepositoryResolver) .toConstantValue( new TypeORMRevisionRepositoryResolver( container.get(TYPES.Revisions_SQLRevisionRepository), isSecondaryDatabaseEnabled ? container.get(TYPES.Revisions_MongoDBRevisionRepository) : null, ), ) container .bind(TYPES.Revisions_GetRequiredRoleToViewRevision) .toDynamicValue((context: interfaces.Context) => { return new GetRequiredRoleToViewRevision(context.container.get(TYPES.Revisions_Timer)) }) // Map container .bind>(TYPES.Revisions_RevisionHttpMapper) .toDynamicValue(() => new RevisionHttpMapper()) container .bind>( TYPES.Revisions_RevisionMetadataHttpMapper, ) .toDynamicValue((context: interfaces.Context) => { return new RevisionMetadataHttpMapper(context.container.get(TYPES.Revisions_GetRequiredRoleToViewRevision)) }) container .bind>(TYPES.Revisions_RevisionItemStringMapper) .toDynamicValue(() => new RevisionItemStringMapper()) container .bind(TYPES.Revisions_DumpRepository) .toConstantValue( env.get('S3_AWS_REGION', true) ? new S3DumpRepository( container.get(TYPES.Revisions_S3_BACKUP_BUCKET_NAME), container.get(TYPES.Revisions_S3), container.get(TYPES.Revisions_RevisionItemStringMapper), ) : new FSDumpRepository(container.get(TYPES.Revisions_RevisionItemStringMapper)), ) // use cases container .bind(TYPES.Revisions_GetRevisionsMetada) .toConstantValue( new GetRevisionsMetada( container.get(TYPES.Revisions_RevisionRepositoryResolver), ), ) container .bind(TYPES.Revisions_GetRevision) .toConstantValue( new GetRevision(container.get(TYPES.Revisions_RevisionRepositoryResolver)), ) container .bind(TYPES.Revisions_DeleteRevision) .toConstantValue( new DeleteRevision( container.get(TYPES.Revisions_RevisionRepositoryResolver), ), ) container .bind(TYPES.Revisions_CopyRevisions) .toConstantValue( new CopyRevisions( container.get(TYPES.Revisions_RevisionRepositoryResolver), ), ) container .bind( TYPES.Revisions_TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser, ) .toConstantValue( new TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser( container.get(TYPES.Revisions_SQLRevisionRepository), isSecondaryDatabaseEnabled ? container.get(TYPES.Revisions_MongoDBRevisionRepository) : null, isConfiguredForInMemoryCache ? null : container.get(TYPES.Revisions_TransitionStatusRepository), container.get(TYPES.Revisions_Timer), container.get(TYPES.Revisions_Logger), env.get('MIGRATION_BATCH_SIZE', true) ? +env.get('MIGRATION_BATCH_SIZE', true) : 100, container.get(TYPES.Revisions_DomainEventPublisher), container.get(TYPES.Revisions_DomainEventFactory), ), ) container .bind(TYPES.Revisions_RemoveRevisionsFromSharedVault) .toConstantValue( new RemoveRevisionsFromSharedVault( isSecondaryDatabaseEnabled ? container.get(TYPES.Revisions_MongoDBRevisionRepository) : container.get(TYPES.Revisions_SQLRevisionRepository), ), ) container .bind(TYPES.Revisions_CreateRevisionFromDump) .toConstantValue( new CreateRevisionFromDump( container.get(TYPES.Revisions_DumpRepository), container.get(TYPES.Revisions_RevisionRepositoryResolver), ), ) // env vars container.bind(TYPES.Revisions_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET')) // Controller container .bind(TYPES.Revisions_ControllerContainer) .toConstantValue(configuration?.controllerConatiner ?? new ControllerContainer()) container .bind>(TYPES.Revisions_CrossServiceTokenDecoder) .toDynamicValue((context: interfaces.Context) => { return new TokenDecoder(context.container.get(TYPES.Revisions_AUTH_JWT_SECRET)) }) container .bind(TYPES.Revisions_ApiGatewayAuthMiddleware) .toDynamicValue((context: interfaces.Context) => { return new ApiGatewayAuthMiddleware( context.container.get(TYPES.Revisions_CrossServiceTokenDecoder), context.container.get(TYPES.Revisions_Logger), ) }) // Handlers container .bind(TYPES.Revisions_ItemDumpedEventHandler) .toConstantValue( new ItemDumpedEventHandler( container.get(TYPES.Revisions_CreateRevisionFromDump), container.get(TYPES.Revisions_Logger), ), ) container .bind(TYPES.Revisions_AccountDeletionRequestedEventHandler) .toConstantValue( new AccountDeletionRequestedEventHandler( container.get(TYPES.Revisions_RevisionRepositoryResolver), container.get(TYPES.Revisions_Logger), ), ) container .bind(TYPES.Revisions_RevisionsCopyRequestedEventHandler) .toConstantValue( new RevisionsCopyRequestedEventHandler( container.get(TYPES.Revisions_CopyRevisions), container.get(TYPES.Revisions_Logger), ), ) container .bind(TYPES.Revisions_ItemRemovedFromSharedVaultEventHandler) .toConstantValue( new ItemRemovedFromSharedVaultEventHandler( container.get(TYPES.Revisions_RemoveRevisionsFromSharedVault), container.get(TYPES.Revisions_Logger), ), ) container .bind(TYPES.Revisions_TransitionRequestedEventHandler) .toConstantValue( new TransitionRequestedEventHandler( false, container.get( TYPES.Revisions_TransitionRevisionsFromPrimaryToSecondaryDatabaseForUser, ), container.get(TYPES.Revisions_Logger), ), ) container .bind(TYPES.Revisions_SharedVaultRemovedEventHandler) .toConstantValue( new SharedVaultRemovedEventHandler( container.get(TYPES.Revisions_RemoveRevisionsFromSharedVault), container.get(TYPES.Revisions_Logger), ), ) const eventHandlers: Map = new Map([ ['ITEM_DUMPED', container.get(TYPES.Revisions_ItemDumpedEventHandler)], ['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Revisions_AccountDeletionRequestedEventHandler)], ['REVISIONS_COPY_REQUESTED', container.get(TYPES.Revisions_RevisionsCopyRequestedEventHandler)], ['ITEM_REMOVED_FROM_SHARED_VAULT', container.get(TYPES.Revisions_ItemRemovedFromSharedVaultEventHandler)], ['TRANSITION_REQUESTED', container.get(TYPES.Revisions_TransitionRequestedEventHandler)], ['SHARED_VAULT_REMOVED', container.get(TYPES.Revisions_SharedVaultRemovedEventHandler)], ]) if (isConfiguredForHomeServer) { const directCallEventMessageHandler = new DirectCallEventMessageHandler( eventHandlers, container.get(TYPES.Revisions_Logger), ) directCallDomainEventPublisher.register(directCallEventMessageHandler) container .bind(TYPES.Revisions_DomainEventMessageHandler) .toConstantValue(directCallEventMessageHandler) } else { container .bind(TYPES.Revisions_DomainEventMessageHandler) .toConstantValue( isConfiguredForHomeServerOrSelfHosting ? new SQSEventMessageHandler(eventHandlers, container.get(TYPES.Revisions_Logger)) : new SQSOpenTelemetryEventMessageHandler( ServiceIdentifier.NAMES.RevisionsWorker, eventHandlers, container.get(TYPES.Revisions_Logger), ), ) container .bind(TYPES.Revisions_DomainEventSubscriberFactory) .toDynamicValue((context: interfaces.Context) => { return new SQSDomainEventSubscriberFactory( context.container.get(TYPES.Revisions_SQS), context.container.get(TYPES.Revisions_SQS_QUEUE_URL), context.container.get(TYPES.Revisions_DomainEventMessageHandler), ) }) } // Inversify Controllers if (isConfiguredForHomeServer) { container .bind(TYPES.Revisions_BaseRevisionsController) .toConstantValue( new BaseRevisionsController( container.get(TYPES.Revisions_GetRevisionsMetada), container.get(TYPES.Revisions_GetRevision), container.get(TYPES.Revisions_DeleteRevision), container.get(TYPES.Revisions_RevisionHttpMapper), container.get(TYPES.Revisions_RevisionMetadataHttpMapper), container.get(TYPES.Revisions_ControllerContainer), ), ) } logger.debug('Configuration complete') return container } }