diff --git a/packages/auth/database.sqlite b/packages/auth/database.sqlite new file mode 100644 index 000000000..3761bb747 Binary files /dev/null and b/packages/auth/database.sqlite differ diff --git a/packages/auth/migrations/mysql/1688540448427-add-notifications.ts b/packages/auth/migrations/mysql/1688540448427-add-notifications.ts new file mode 100644 index 000000000..f79e707d8 --- /dev/null +++ b/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 { + 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 { + await queryRunner.query('DROP INDEX `index_notifications_on_user_uuid` ON `notifications`') + await queryRunner.query('DROP TABLE `notifications`') + } +} diff --git a/packages/auth/migrations/sqlite/1688540623272-add-notifications.ts b/packages/auth/migrations/sqlite/1688540623272-add-notifications.ts new file mode 100644 index 000000000..5fa10601f --- /dev/null +++ b/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 { + 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 { + await queryRunner.query('DROP INDEX "index_notifications_on_user_uuid"') + await queryRunner.query('DROP TABLE "notifications"') + } +} diff --git a/packages/auth/src/Bootstrap/DataSource.ts b/packages/auth/src/Bootstrap/DataSource.ts index 4ce9d5104..5cbb1dbb7 100644 --- a/packages/auth/src/Bootstrap/DataSource.ts +++ b/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(target: EntityTarget): Repository { - 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 { + 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 } } diff --git a/packages/auth/src/Bootstrap/MigrationsDataSource.ts b/packages/auth/src/Bootstrap/MigrationsDataSource.ts new file mode 100644 index 000000000..b69bd4497 --- /dev/null +++ b/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 diff --git a/packages/auth/src/Domain/Notifications/Notification.spec.ts b/packages/auth/src/Domain/Notifications/Notification.spec.ts new file mode 100644 index 000000000..d13419425 --- /dev/null +++ b/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() + }) +}) diff --git a/packages/auth/src/Domain/Notifications/Notification.ts b/packages/auth/src/Domain/Notifications/Notification.ts new file mode 100644 index 000000000..30ddd85ee --- /dev/null +++ b/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 { + get id(): UniqueEntityId { + return this._id + } + + private constructor(props: NotificationProps, id?: UniqueEntityId) { + super(props, id) + } + + static create(props: NotificationProps, id?: UniqueEntityId): Result { + return Result.ok(new Notification(props, id)) + } +} diff --git a/packages/auth/src/Domain/Notifications/NotificationProps.ts b/packages/auth/src/Domain/Notifications/NotificationProps.ts new file mode 100644 index 000000000..a30408418 --- /dev/null +++ b/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 +} diff --git a/packages/auth/src/Domain/Notifications/NotificationType.spec.ts b/packages/auth/src/Domain/Notifications/NotificationType.spec.ts new file mode 100644 index 000000000..c8b9e4c09 --- /dev/null +++ b/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() + }) +}) diff --git a/packages/auth/src/Domain/Notifications/NotificationType.ts b/packages/auth/src/Domain/Notifications/NotificationType.ts new file mode 100644 index 000000000..6533b0817 --- /dev/null +++ b/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 { + 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 { + const isValidPermission = Object.values(this.TYPES).includes(notificationType) + if (!isValidPermission) { + return Result.fail(`Invalid shared vault user permission ${notificationType}`) + } else { + return Result.ok(new NotificationType({ value: notificationType })) + } + } +} diff --git a/packages/auth/src/Domain/Notifications/NotificationTypeProps.ts b/packages/auth/src/Domain/Notifications/NotificationTypeProps.ts new file mode 100644 index 000000000..38c16ebc1 --- /dev/null +++ b/packages/auth/src/Domain/Notifications/NotificationTypeProps.ts @@ -0,0 +1,3 @@ +export interface NotificationTypeProps { + value: string +} diff --git a/packages/auth/src/Infra/TypeORM/TypeORMNotification.ts b/packages/auth/src/Infra/TypeORM/TypeORMNotification.ts new file mode 100644 index 000000000..48c9a4d34 --- /dev/null +++ b/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 +}