Browse Source

feat(auth): add notifications model (#638)

Karol Sójko 1 year ago
parent
commit
fecfd54728

BIN
packages/auth/database.sqlite


+ 16 - 0
packages/auth/migrations/mysql/1688540448427-add-notifications.ts

@@ -0,0 +1,16 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddNotifications1688540448427 implements MigrationInterface {
+  name = 'AddNotifications1688540448427'
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(
+      'CREATE TABLE `notifications` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `type` varchar(36) NOT NULL, `payload` text NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `index_notifications_on_user_uuid` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
+    )
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`')
+    await queryRunner.query('DROP TABLE `notifications`')
+  }
+}

+ 17 - 0
packages/auth/migrations/sqlite/1688540623272-add-notifications.ts

@@ -0,0 +1,17 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddNotifications1688540623272 implements MigrationInterface {
+  name = 'AddNotifications1688540623272'
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(
+      'CREATE TABLE "notifications" ("uuid" varchar PRIMARY KEY NOT NULL, "user_uuid" varchar(36) NOT NULL, "type" varchar(36) NOT NULL, "payload" text NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
+    )
+    await queryRunner.query('CREATE INDEX "index_notifications_on_user_uuid" ON "notifications" ("user_uuid") ')
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX "index_notifications_on_user_uuid"')
+    await queryRunner.query('DROP TABLE "notifications"')
+  }
+}

+ 12 - 6
packages/auth/src/Bootstrap/DataSource.ts

@@ -18,21 +18,26 @@ import { TypeORMEmergencyAccessInvitation } from '../Infra/TypeORM/TypeORMEmerge
 import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
 import { Env } from './Env'
 import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
+import { TypeORMNotification } from '../Infra/TypeORM/TypeORMNotification'
 
 export class AppDataSource {
-  private dataSource: DataSource | undefined
+  private _dataSource: DataSource | undefined
 
   constructor(private env: Env) {}
 
   getRepository<Entity extends ObjectLiteral>(target: EntityTarget<Entity>): Repository<Entity> {
-    if (!this.dataSource) {
+    if (!this._dataSource) {
       throw new Error('DataSource not initialized')
     }
 
-    return this.dataSource.getRepository(target)
+    return this._dataSource.getRepository(target)
   }
 
   async initialize(): Promise<void> {
+    await this.dataSource.initialize()
+  }
+
+  get dataSource(): DataSource {
     this.env.load()
 
     const isConfiguredForMySQL = this.env.get('DB_TYPE') === 'mysql'
@@ -60,6 +65,7 @@ export class AppDataSource {
         TypeORMAuthenticatorChallenge,
         TypeORMEmergencyAccessInvitation,
         TypeORMCacheEntry,
+        TypeORMNotification,
       ],
       migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
       migrationsRun: true,
@@ -104,7 +110,7 @@ export class AppDataSource {
         database: inReplicaMode ? undefined : this.env.get('DB_DATABASE'),
       }
 
-      this.dataSource = new DataSource(mySQLDataSourceOptions)
+      this._dataSource = new DataSource(mySQLDataSourceOptions)
     } else {
       const sqliteDataSourceOptions: SqliteConnectionOptions = {
         ...commonDataSourceOptions,
@@ -112,9 +118,9 @@ export class AppDataSource {
         database: this.env.get('DB_SQLITE_DATABASE_PATH'),
       }
 
-      this.dataSource = new DataSource(sqliteDataSourceOptions)
+      this._dataSource = new DataSource(sqliteDataSourceOptions)
     }
 
-    await this.dataSource.initialize()
+    return this._dataSource
   }
 }

+ 7 - 0
packages/auth/src/Bootstrap/MigrationsDataSource.ts

@@ -0,0 +1,7 @@
+import { AppDataSource } from './DataSource'
+import { Env } from './Env'
+
+const env: Env = new Env()
+env.load()
+
+export const MigrationsDataSource = new AppDataSource(env).dataSource

+ 18 - 0
packages/auth/src/Domain/Notifications/Notification.spec.ts

@@ -0,0 +1,18 @@
+import { Timestamps, Uuid } from '@standardnotes/domain-core'
+
+import { Notification } from './Notification'
+import { NotificationType } from './NotificationType'
+
+describe('Notification', () => {
+  it('should create an entity', () => {
+    const entityOrError = Notification.create({
+      timestamps: Timestamps.create(123456789, 123456789).getValue(),
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      payload: 'payload',
+      type: NotificationType.create(NotificationType.TYPES.SharedVaultItemRemoved).getValue(),
+    })
+
+    expect(entityOrError.isFailed()).toBeFalsy()
+    expect(entityOrError.getValue().id).not.toBeNull()
+  })
+})

+ 17 - 0
packages/auth/src/Domain/Notifications/Notification.ts

@@ -0,0 +1,17 @@
+import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
+
+import { NotificationProps } from './NotificationProps'
+
+export class Notification extends Entity<NotificationProps> {
+  get id(): UniqueEntityId {
+    return this._id
+  }
+
+  private constructor(props: NotificationProps, id?: UniqueEntityId) {
+    super(props, id)
+  }
+
+  static create(props: NotificationProps, id?: UniqueEntityId): Result<Notification> {
+    return Result.ok<Notification>(new Notification(props, id))
+  }
+}

+ 9 - 0
packages/auth/src/Domain/Notifications/NotificationProps.ts

@@ -0,0 +1,9 @@
+import { Timestamps, Uuid } from '@standardnotes/domain-core'
+import { NotificationType } from './NotificationType'
+
+export interface NotificationProps {
+  userUuid: Uuid
+  type: NotificationType
+  payload: string
+  timestamps: Timestamps
+}

+ 16 - 0
packages/auth/src/Domain/Notifications/NotificationType.spec.ts

@@ -0,0 +1,16 @@
+import { NotificationType } from './NotificationType'
+
+describe('NotificationType', () => {
+  it('should create a value object', () => {
+    const valueOrError = NotificationType.create(NotificationType.TYPES.SharedVaultItemRemoved)
+
+    expect(valueOrError.isFailed()).toBeFalsy()
+    expect(valueOrError.getValue().value).toEqual('shared_vault_item_removed')
+  })
+
+  it('should not create an invalid value object', () => {
+    const valueOrError = NotificationType.create('TEST')
+
+    expect(valueOrError.isFailed()).toBeTruthy()
+  })
+})

+ 27 - 0
packages/auth/src/Domain/Notifications/NotificationType.ts

@@ -0,0 +1,27 @@
+import { Result, ValueObject } from '@standardnotes/domain-core'
+
+import { NotificationTypeProps } from './NotificationTypeProps'
+
+export class NotificationType extends ValueObject<NotificationTypeProps> {
+  static readonly TYPES = {
+    SharedVaultItemRemoved: 'shared_vault_item_removed',
+    RemovedFromSharedVault: 'removed_from_shared_vault',
+  }
+
+  get value(): string {
+    return this.props.value
+  }
+
+  private constructor(props: NotificationTypeProps) {
+    super(props)
+  }
+
+  static create(notificationType: string): Result<NotificationType> {
+    const isValidPermission = Object.values(this.TYPES).includes(notificationType)
+    if (!isValidPermission) {
+      return Result.fail<NotificationType>(`Invalid shared vault user permission ${notificationType}`)
+    } else {
+      return Result.ok<NotificationType>(new NotificationType({ value: notificationType }))
+    }
+  }
+}

+ 3 - 0
packages/auth/src/Domain/Notifications/NotificationTypeProps.ts

@@ -0,0 +1,3 @@
+export interface NotificationTypeProps {
+  value: string
+}

+ 38 - 0
packages/auth/src/Infra/TypeORM/TypeORMNotification.ts

@@ -0,0 +1,38 @@
+import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity({ name: 'notifications' })
+export class TypeORMNotification {
+  @PrimaryGeneratedColumn('uuid')
+  declare uuid: string
+
+  @Column({
+    name: 'user_uuid',
+    length: 36,
+  })
+  @Index('index_notifications_on_user_uuid')
+  declare userUuid: string
+
+  @Column({
+    name: 'type',
+    length: 36,
+  })
+  declare type: string
+
+  @Column({
+    name: 'payload',
+    type: 'text',
+  })
+  declare payload: string
+
+  @Column({
+    name: 'created_at_timestamp',
+    type: 'bigint',
+  })
+  declare createdAtTimestamp: number
+
+  @Column({
+    name: 'updated_at_timestamp',
+    type: 'bigint',
+  })
+  declare updatedAtTimestamp: number
+}