Compare commits

...

10 Commits

Author SHA1 Message Date
standardci
e115523acd chore(release): publish new version
- @standardnotes/analytics@1.42.0
 - @standardnotes/api-gateway@1.36.4
 - @standardnotes/auth-server@1.49.7
 - @standardnotes/syncing-server@1.10.15
2022-11-04 10:20:35 +00:00
Karol Sójko
35611fbc07 fix(analytics): imports 2022-11-04 11:18:03 +01:00
Karol Sójko
034aa38153 feat(analytics): add user registered handler 2022-11-04 11:10:03 +01:00
standardci
795728ab31 chore(release): publish new version
- @standardnotes/analytics@1.41.0
 - @standardnotes/api-gateway@1.36.3
 - @standardnotes/auth-server@1.49.6
 - @standardnotes/syncing-server@1.10.14
2022-11-04 09:58:38 +00:00
Karol Sójko
262d295121 fix(analytics): linter setup with migrations 2022-11-04 10:56:22 +01:00
Karol Sójko
4e5ac0a47b feat(analytics): add retrieving user analytics id 2022-11-04 10:55:43 +01:00
standardci
51b8cbdab2 chore(release): publish new version
- @standardnotes/analytics@1.40.0
 - @standardnotes/api-gateway@1.36.2
 - @standardnotes/auth-server@1.49.5
 - @standardnotes/syncing-server@1.10.13
2022-11-04 09:52:57 +00:00
Karol Sójko
f315b1ac5c feat(analytics): add analytics entities 2022-11-04 10:50:38 +01:00
standardci
2feaa8d956 chore(release): publish new version
- @standardnotes/analytics@1.39.1
 - @standardnotes/api-gateway@1.36.1
 - @standardnotes/auth-server@1.49.4
 - @standardnotes/syncing-server@1.10.12
2022-11-04 09:16:15 +00:00
Karol Sójko
5329f2a2fb fix(analytics): linter setup 2022-11-04 10:14:18 +01:00
24 changed files with 366 additions and 7 deletions

View File

@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.42.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.41.0...@standardnotes/analytics@1.42.0) (2022-11-04)
### Bug Fixes
* **analytics:** imports ([35611fb](https://github.com/standardnotes/server/commit/35611fbc07e50efbba954d7c96db9917e0dddd7d))
### Features
* **analytics:** add user registered handler ([034aa38](https://github.com/standardnotes/server/commit/034aa381539cf4b46769cfd1646dec2051d836c3))
# [1.41.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.40.0...@standardnotes/analytics@1.41.0) (2022-11-04)
### Bug Fixes
* **analytics:** linter setup with migrations ([262d295](https://github.com/standardnotes/server/commit/262d2951218753f6f14613c5d8ae20ade0e4ef06))
### Features
* **analytics:** add retrieving user analytics id ([4e5ac0a](https://github.com/standardnotes/server/commit/4e5ac0a47b469e5fa681f0131d04c9823cebedf3))
# [1.40.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.39.1...@standardnotes/analytics@1.40.0) (2022-11-04)
### Features
* **analytics:** add analytics entities ([f315b1a](https://github.com/standardnotes/server/commit/f315b1ac5c9369d36fa616c6b4bb5492148564f8))
## [1.39.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.39.0...@standardnotes/analytics@1.39.1) (2022-11-04)
### Bug Fixes
* **analytics:** linter setup ([5329f2a](https://github.com/standardnotes/server/commit/5329f2a2fb815f8691e37279fcadcf01beb716ad))
# [1.39.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.38.0...@standardnotes/analytics@1.39.0) (2022-11-04)
### Features

View File

@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class initDatabase1667555285111 implements MigrationInterface {
name = 'initDatabase1667555285111'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query('DROP INDEX `user_uuid` ON `analytics_entities`')
await queryRunner.query('DROP TABLE `analytics_entities`')
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "1.39.0",
"version": "1.42.0",
"engines": {
"node": ">=14.0.0 <17.0.0"
},

View File

@@ -28,6 +28,12 @@ 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'
import { GetUserAnalyticsId } from '../Domain/UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { UserRegisteredEventHandler } from '../Domain/Handler/UserRegisteredEventHandler'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -97,12 +103,20 @@ export class ContainerConfigLoader {
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
// Repositories
container
.bind<AnalyticsEntityRepositoryInterface>(TYPES.AnalyticsEntityRepository)
.to(MySQLAnalyticsEntityRepository)
// ORM
container
.bind<Repository<AnalyticsEntity>>(TYPES.ORMAnalyticsEntityRepository)
.toConstantValue(AppDataSource.getRepository(AnalyticsEntity))
// Use Case
container.bind<GetUserAnalyticsId>(TYPES.GetUserAnalyticsId).to(GetUserAnalyticsId)
// Hanlders
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
// Services
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
@@ -127,7 +141,9 @@ export class ContainerConfigLoader {
)
}
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([])
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
])
if (env.get('SQS_QUEUE_URL', true)) {
container

View File

@@ -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: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),

View File

@@ -12,9 +12,13 @@ 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
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
// Handlers
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
// Services
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),

View File

@@ -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
}

View File

@@ -0,0 +1,7 @@
import { Uuid } from '@standardnotes/common'
import { AnalyticsEntity } from './AnalyticsEntity'
export interface AnalyticsEntityRepositoryInterface {
save(analyticsEntity: AnalyticsEntity): Promise<AnalyticsEntity>
findOneByUserUuid(userUuid: Uuid): Promise<AnalyticsEntity | null>
}

View File

@@ -0,0 +1,47 @@
import 'reflect-metadata'
import { UserRegisteredEvent } from '@standardnotes/domain-events'
import { ProtocolVersion } from '@standardnotes/common'
import { UserRegisteredEventHandler } from './UserRegisteredEventHandler'
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { Period } from '../Time/Period'
describe('UserRegisteredEventHandler', () => {
let analyticsEntityRepository: AnalyticsEntityRepositoryInterface
let event: UserRegisteredEvent
let analyticsStore: AnalyticsStoreInterface
const createHandler = () => new UserRegisteredEventHandler(analyticsEntityRepository, analyticsStore)
beforeEach(() => {
event = {} as jest.Mocked<UserRegisteredEvent>
event.createdAt = new Date(1)
event.payload = {
userUuid: '1-2-3',
email: 'test@test.te',
protocolVersion: ProtocolVersion.V004,
}
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
analyticsEntityRepository.save = jest.fn().mockImplementation((entity) => ({
...entity,
id: 1,
}))
})
it('should save analytics entity upon user registration', async () => {
await createHandler().handle(event)
expect(analyticsEntityRepository.save).toHaveBeenCalled()
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['register'], 1, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
})
})

View File

@@ -0,0 +1,29 @@
import { DomainEventHandlerInterface, UserRegisteredEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { AnalyticsEntity } from '../Entity/AnalyticsEntity'
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
import { Period } from '../Time/Period'
@injectable()
export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
) {}
async handle(event: UserRegisteredEvent): Promise<void> {
let analyticsEntity = new AnalyticsEntity()
analyticsEntity.userUuid = event.payload.userUuid
analyticsEntity = await this.analyticsEntityRepository.save(analyticsEntity)
await this.analyticsStore.markActivity([AnalyticsActivity.Register], analyticsEntity.id, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
}
}

View File

@@ -0,0 +1,37 @@
import 'reflect-metadata'
import { AnalyticsEntity } from '../../Entity/AnalyticsEntity'
import { AnalyticsEntityRepositoryInterface } from '../../Entity/AnalyticsEntityRepositoryInterface'
import { GetUserAnalyticsId } from './GetUserAnalyticsId'
describe('GetUserAnalyticsId', () => {
let analyticsEntityRepository: AnalyticsEntityRepositoryInterface
let analyticsEntity: AnalyticsEntity
const createUseCase = () => new GetUserAnalyticsId(analyticsEntityRepository)
beforeEach(() => {
analyticsEntity = { id: 123 } as jest.Mocked<AnalyticsEntity>
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue(analyticsEntity)
})
it('should return analytics id for a user', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({ analyticsId: 123 })
})
it('should throw error if user is missing analytics entity', async () => {
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
let error = null
try {
await createUseCase().execute({ userUuid: '1-2-3' })
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
})

View File

@@ -0,0 +1,25 @@
import { inject, injectable } from 'inversify'
import TYPES from '../../../Bootstrap/Types'
import { AnalyticsEntityRepositoryInterface } from '../../Entity/AnalyticsEntityRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { GetUserAnalyticsIdDTO } from './GetUserAnalyticsIdDTO'
import { GetUserAnalyticsIdResponse } from './GetUserAnalyticsIdResponse'
@injectable()
export class GetUserAnalyticsId implements UseCaseInterface {
constructor(
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
) {}
async execute(dto: GetUserAnalyticsIdDTO): Promise<GetUserAnalyticsIdResponse> {
const analyticsEntity = await this.analyticsEntityRepository.findOneByUserUuid(dto.userUuid)
if (analyticsEntity === null) {
throw new Error(`Could not find analytics entity for user ${dto.userUuid}`)
}
return {
analyticsId: analyticsEntity.id,
}
}
}

View File

@@ -0,0 +1,5 @@
import { Uuid } from '@standardnotes/common'
export type GetUserAnalyticsIdDTO = {
userUuid: Uuid
}

View File

@@ -0,0 +1,3 @@
export type GetUserAnalyticsIdResponse = {
analyticsId: number
}

View File

@@ -0,0 +1,3 @@
export interface UseCaseInterface {
execute(...args: any[]): Promise<Record<string, unknown>>
}

View File

@@ -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<AnalyticsEntity>
let analyticsEntity: AnalyticsEntity
let queryBuilder: SelectQueryBuilder<AnalyticsEntity>
const createRepository = () => new MySQLAnalyticsEntityRepository(ormRepository)
beforeEach(() => {
analyticsEntity = {} as jest.Mocked<AnalyticsEntity>
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<AnalyticsEntity>>
ormRepository = {} as jest.Mocked<Repository<AnalyticsEntity>>
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)
})
})

View File

@@ -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<AnalyticsEntity>,
) {}
async findOneByUserUuid(userUuid: Uuid): Promise<AnalyticsEntity | null> {
return this.ormRepository
.createQueryBuilder('analytics_entity')
.where('analytics_entity.user_uuid = :userUuid', { userUuid })
.getOne()
}
async save(analyticsEntity: AnalyticsEntity): Promise<AnalyticsEntity> {
return this.ormRepository.save(analyticsEntity)
}
}

View File

@@ -5,7 +5,9 @@
"outDir": "./dist",
},
"include": [
"src/**/*"
"src/**/*",
"bin/**/*",
"migrations/**/*"
],
"references": []
}

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.36.3...@standardnotes/api-gateway@1.36.4) (2022-11-04)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.36.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.36.2...@standardnotes/api-gateway@1.36.3) (2022-11-04)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.36.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.36.1...@standardnotes/api-gateway@1.36.2) (2022-11-04)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.36.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.36.0...@standardnotes/api-gateway@1.36.1) (2022-11-04)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.36.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.35.1...@standardnotes/api-gateway@1.36.0) (2022-11-04)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.36.0",
"version": "1.36.4",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.49.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.49.6...@standardnotes/auth-server@1.49.7) (2022-11-04)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.49.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.49.5...@standardnotes/auth-server@1.49.6) (2022-11-04)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.49.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.49.4...@standardnotes/auth-server@1.49.5) (2022-11-04)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.49.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.49.3...@standardnotes/auth-server@1.49.4) (2022-11-04)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.49.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.49.2...@standardnotes/auth-server@1.49.3) (2022-11-04)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.49.3",
"version": "1.49.7",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.15](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.10.14...@standardnotes/syncing-server@1.10.15) (2022-11-04)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.10.14](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.10.13...@standardnotes/syncing-server@1.10.14) (2022-11-04)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.10.13](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.10.12...@standardnotes/syncing-server@1.10.13) (2022-11-04)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.10.12](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.10.11...@standardnotes/syncing-server@1.10.12) (2022-11-04)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.10.11](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.10.10...@standardnotes/syncing-server@1.10.11) (2022-11-04)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.10.11",
"version": "1.10.15",
"engines": {
"node": ">=16.0.0 <17.0.0"
},