From f315b1ac5c9369d36fa616c6b4bb5492148564f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Fri, 4 Nov 2022 10:50:33 +0100 Subject: [PATCH] feat(analytics): add analytics entities --- .../migrations/1667555285111-init_database.ts | 16 +++++++ packages/analytics/src/Bootstrap/Container.ts | 10 +++++ .../analytics/src/Bootstrap/DataSource.ts | 5 ++- packages/analytics/src/Bootstrap/Types.ts | 2 + .../src/Domain/Entity/AnalyticsEntity.ts | 14 +++++++ .../AnalyticsEntityRepositoryInterface.ts | 7 ++++ .../MySQLAnalyticsEntityRepository.spec.ts | 42 +++++++++++++++++++ .../MySQL/MySQLAnalyticsEntityRepository.ts | 26 ++++++++++++ 8 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 packages/analytics/migrations/1667555285111-init_database.ts create mode 100644 packages/analytics/src/Domain/Entity/AnalyticsEntity.ts create mode 100644 packages/analytics/src/Domain/Entity/AnalyticsEntityRepositoryInterface.ts create mode 100644 packages/analytics/src/Infra/MySQL/MySQLAnalyticsEntityRepository.spec.ts create mode 100644 packages/analytics/src/Infra/MySQL/MySQLAnalyticsEntityRepository.ts diff --git a/packages/analytics/migrations/1667555285111-init_database.ts b/packages/analytics/migrations/1667555285111-init_database.ts new file mode 100644 index 000000000..189177454 --- /dev/null +++ b/packages/analytics/migrations/1667555285111-init_database.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class initDatabase1667555285111 implements MigrationInterface { + name = 'initDatabase1667555285111' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'CREATE TABLE `analytics_entities` (`id` int NOT NULL AUTO_INCREMENT, `user_uuid` varchar(36) NOT NULL, INDEX `user_uuid` (`user_uuid`), PRIMARY KEY (`id`)) ENGINE=InnoDB', + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP INDEX `user_uuid` ON `analytics_entities`') + await queryRunner.query('DROP TABLE `analytics_entities`') + } +} diff --git a/packages/analytics/src/Bootstrap/Container.ts b/packages/analytics/src/Bootstrap/Container.ts index 0b7827273..9d4d24b23 100644 --- a/packages/analytics/src/Bootstrap/Container.ts +++ b/packages/analytics/src/Bootstrap/Container.ts @@ -28,6 +28,10 @@ import { AnalyticsStoreInterface } from '../Domain/Analytics/AnalyticsStoreInter import { RedisAnalyticsStore } from '../Infra/Redis/RedisAnalyticsStore' import { StatisticsStoreInterface } from '../Domain/Statistics/StatisticsStoreInterface' import { RedisStatisticsStore } from '../Infra/Redis/RedisStatisticsStore' +import { AnalyticsEntityRepositoryInterface } from '../Domain/Entity/AnalyticsEntityRepositoryInterface' +import { MySQLAnalyticsEntityRepository } from '../Infra/MySQL/MySQLAnalyticsEntityRepository' +import { Repository } from 'typeorm' +import { AnalyticsEntity } from '../Domain/Entity/AnalyticsEntity' // eslint-disable-next-line @typescript-eslint/no-var-requires const newrelicFormatter = require('@newrelic/winston-enricher') @@ -97,8 +101,14 @@ export class ContainerConfigLoader { container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true)) // Repositories + container + .bind(TYPES.AnalyticsEntityRepository) + .to(MySQLAnalyticsEntityRepository) // ORM + container + .bind>(TYPES.ORMAnalyticsEntityRepository) + .toConstantValue(AppDataSource.getRepository(AnalyticsEntity)) // Use Case diff --git a/packages/analytics/src/Bootstrap/DataSource.ts b/packages/analytics/src/Bootstrap/DataSource.ts index d397a07b4..daa895367 100644 --- a/packages/analytics/src/Bootstrap/DataSource.ts +++ b/packages/analytics/src/Bootstrap/DataSource.ts @@ -1,4 +1,7 @@ import { DataSource, LoggerOptions } from 'typeorm' + +import { AnalyticsEntity } from '../Domain/Entity/AnalyticsEntity' + import { Env } from './Env' const env: Env = new Env() @@ -33,7 +36,7 @@ export const AppDataSource = new DataSource({ ], removeNodeErrorCount: 10, }, - entities: [], + entities: [AnalyticsEntity], migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'], migrationsRun: true, logging: env.get('DB_DEBUG_LEVEL'), diff --git a/packages/analytics/src/Bootstrap/Types.ts b/packages/analytics/src/Bootstrap/Types.ts index 740cb7b18..6b4f2bf6d 100644 --- a/packages/analytics/src/Bootstrap/Types.ts +++ b/packages/analytics/src/Bootstrap/Types.ts @@ -12,7 +12,9 @@ const TYPES = { REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'), NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'), // Repositories + AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'), // ORM + ORMAnalyticsEntityRepository: Symbol.for('ORMAnalyticsEntityRepository'), // Use Case // Handlers // Services diff --git a/packages/analytics/src/Domain/Entity/AnalyticsEntity.ts b/packages/analytics/src/Domain/Entity/AnalyticsEntity.ts new file mode 100644 index 000000000..c298643e9 --- /dev/null +++ b/packages/analytics/src/Domain/Entity/AnalyticsEntity.ts @@ -0,0 +1,14 @@ +import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm' + +@Entity({ name: 'analytics_entities' }) +export class AnalyticsEntity { + @PrimaryGeneratedColumn() + declare id: number + + @Column({ + name: 'user_uuid', + length: 36, + }) + @Index('user_uuid') + declare userUuid: string +} diff --git a/packages/analytics/src/Domain/Entity/AnalyticsEntityRepositoryInterface.ts b/packages/analytics/src/Domain/Entity/AnalyticsEntityRepositoryInterface.ts new file mode 100644 index 000000000..64e6efdd4 --- /dev/null +++ b/packages/analytics/src/Domain/Entity/AnalyticsEntityRepositoryInterface.ts @@ -0,0 +1,7 @@ +import { Uuid } from '@standardnotes/common' +import { AnalyticsEntity } from './AnalyticsEntity' + +export interface AnalyticsEntityRepositoryInterface { + save(analyticsEntity: AnalyticsEntity): Promise + findOneByUserUuid(userUuid: Uuid): Promise +} diff --git a/packages/analytics/src/Infra/MySQL/MySQLAnalyticsEntityRepository.spec.ts b/packages/analytics/src/Infra/MySQL/MySQLAnalyticsEntityRepository.spec.ts new file mode 100644 index 000000000..8777ee30f --- /dev/null +++ b/packages/analytics/src/Infra/MySQL/MySQLAnalyticsEntityRepository.spec.ts @@ -0,0 +1,42 @@ +import 'reflect-metadata' + +import { Repository, SelectQueryBuilder } from 'typeorm' + +import { AnalyticsEntity } from '../../Domain/Entity/AnalyticsEntity' + +import { MySQLAnalyticsEntityRepository } from './MySQLAnalyticsEntityRepository' + +describe('MySQLAnalyticsEntityRepository', () => { + let ormRepository: Repository + let analyticsEntity: AnalyticsEntity + let queryBuilder: SelectQueryBuilder + + const createRepository = () => new MySQLAnalyticsEntityRepository(ormRepository) + + beforeEach(() => { + analyticsEntity = {} as jest.Mocked + + queryBuilder = {} as jest.Mocked> + + ormRepository = {} as jest.Mocked> + ormRepository.save = jest.fn() + ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder) + }) + + it('should save', async () => { + await createRepository().save(analyticsEntity) + + expect(ormRepository.save).toHaveBeenCalledWith(analyticsEntity) + }) + + it('should find one by user uuid', async () => { + queryBuilder.where = jest.fn().mockReturnThis() + queryBuilder.getOne = jest.fn().mockReturnValue(analyticsEntity) + + const result = await createRepository().findOneByUserUuid('123') + + expect(queryBuilder.where).toHaveBeenCalledWith('analytics_entity.user_uuid = :userUuid', { userUuid: '123' }) + + expect(result).toEqual(analyticsEntity) + }) +}) diff --git a/packages/analytics/src/Infra/MySQL/MySQLAnalyticsEntityRepository.ts b/packages/analytics/src/Infra/MySQL/MySQLAnalyticsEntityRepository.ts new file mode 100644 index 000000000..c36546072 --- /dev/null +++ b/packages/analytics/src/Infra/MySQL/MySQLAnalyticsEntityRepository.ts @@ -0,0 +1,26 @@ +import { Uuid } from '@standardnotes/common' +import { inject, injectable } from 'inversify' +import { Repository } from 'typeorm' + +import TYPES from '../../Bootstrap/Types' +import { AnalyticsEntity } from '../../Domain/Entity/AnalyticsEntity' +import { AnalyticsEntityRepositoryInterface } from '../../Domain/Entity/AnalyticsEntityRepositoryInterface' + +@injectable() +export class MySQLAnalyticsEntityRepository implements AnalyticsEntityRepositoryInterface { + constructor( + @inject(TYPES.ORMAnalyticsEntityRepository) + private ormRepository: Repository, + ) {} + + async findOneByUserUuid(userUuid: Uuid): Promise { + return this.ormRepository + .createQueryBuilder('analytics_entity') + .where('analytics_entity.user_uuid = :userUuid', { userUuid }) + .getOne() + } + + async save(analyticsEntity: AnalyticsEntity): Promise { + return this.ormRepository.save(analyticsEntity) + } +}