fix: merge mysql and mysql-legacy together (#883)

This commit is contained in:
Karol Sójko 2023-10-20 09:42:10 +02:00 committed by GitHub
parent d570146378
commit e19f7a7b7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 512 additions and 1584 deletions

View file

@ -1,41 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class init1669113322388 implements MigrationInterface {
name = 'init1669113322388'
public async up(queryRunner: QueryRunner): Promise<void> {
await this.syncSchemaBetweenLegacyRevisions(queryRunner)
await queryRunner.query(
'CREATE TABLE IF NOT EXISTS `revisions` (`uuid` varchar(36) NOT NULL, `item_uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `content` mediumtext NULL, `content_type` varchar(255) NULL, `items_key_id` varchar(255) NULL, `enc_item_key` text NULL, `auth_hash` varchar(255) NULL, `creation_date` date NULL, `created_at` datetime(6) NULL, `updated_at` datetime(6) NULL, INDEX `item_uuid` (`item_uuid`), INDEX `user_uuid` (`user_uuid`), INDEX `creation_date` (`creation_date`), INDEX `created_at` (`created_at`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `created_at` ON `revisions`')
await queryRunner.query('DROP INDEX `creation_date` ON `revisions`')
await queryRunner.query('DROP INDEX `user_uuid` ON `revisions`')
await queryRunner.query('DROP INDEX `item_uuid` ON `revisions`')
await queryRunner.query('DROP TABLE `revisions`')
}
private async syncSchemaBetweenLegacyRevisions(queryRunner: QueryRunner): Promise<void> {
const revisionsTableExistsQueryResult = await queryRunner.manager.query(
'SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = "revisions"',
)
const revisionsTableExists = revisionsTableExistsQueryResult[0].count === 1
if (!revisionsTableExists) {
return
}
const revisionsTableHasUserUuidColumnQueryResult = await queryRunner.manager.query(
'SELECT COUNT(*) as count FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = "revisions" AND column_name = "user_uuid"',
)
const revisionsTableHasUserUuidColumn = revisionsTableHasUserUuidColumnQueryResult[0].count === 1
if (revisionsTableHasUserUuidColumn) {
return
}
await queryRunner.query('ALTER TABLE `revisions` ADD COLUMN `user_uuid` varchar(36) NULL')
}
}

View file

@ -1,28 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class removeDateIndexes1669636497932 implements MigrationInterface {
name = 'removeDateIndexes1669636497932'
public async up(queryRunner: QueryRunner): Promise<void> {
const indexRevisionsOnCreatedAt = await queryRunner.manager.query(
'SHOW INDEX FROM `revisions` where `key_name` = "created_at"',
)
const indexRevisionsOnCreatedAtExist = indexRevisionsOnCreatedAt && indexRevisionsOnCreatedAt.length > 0
if (indexRevisionsOnCreatedAtExist) {
await queryRunner.query('DROP INDEX `created_at` ON `revisions`')
}
const indexRevisionsOnCreationDate = await queryRunner.manager.query(
'SHOW INDEX FROM `revisions` where `key_name` = "creation_date"',
)
const indexRevisionsOnCreationDateAtExist = indexRevisionsOnCreationDate && indexRevisionsOnCreationDate.length > 0
if (indexRevisionsOnCreationDateAtExist) {
await queryRunner.query('DROP INDEX `creation_date` ON `revisions`')
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('CREATE INDEX `creation_date` ON `revisions` (`creation_date`)')
await queryRunner.query('CREATE INDEX `created_at` ON `revisions` (`created_at`)')
}
}

View file

@ -1,13 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class makeUserUuidNullable1669735585016 implements MigrationInterface {
name = 'makeUserUuidNullable1669735585016'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `revisions` CHANGE `user_uuid` `user_uuid` varchar(36) NULL')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `revisions` CHANGE `user_uuid` `user_uuid` varchar(36) NOT NULL')
}
}

View file

@ -34,7 +34,7 @@ export class removeDateIndexes1669636497932 implements MigrationInterface {
)
const revisionsTableExists = revisionsTableExistsQueryResult[0].count === 1
if (revisionsTableExists) {
await queryRunner.query('RENAME TABLE `revisions` TO `revisions_revisions`')
await queryRunner.query('ALTER TABLE `revisions` RENAME TO `revisions_revisions`, ALGORITHM=INSTANT')
}
}
}

View file

@ -19,7 +19,7 @@ export class makeUserUuidNullable1669735585016 implements MigrationInterface {
)
const revisionsTableExists = revisionsTableExistsQueryResult[0].count === 1
if (revisionsTableExists) {
await queryRunner.query('RENAME TABLE `revisions` TO `revisions_revisions`')
await queryRunner.query('ALTER TABLE `revisions` RENAME TO `revisions_revisions`, ALGORITHM=INSTANT')
}
}
}

View file

@ -4,9 +4,13 @@ export class AddSharedVaultInformation1693915383950 implements MigrationInterfac
public async up(queryRunner: QueryRunner): Promise<void> {
await this.renameRevisionsTable(queryRunner)
await queryRunner.query('ALTER TABLE `revisions_revisions` ADD `edited_by` varchar(36) NULL')
await queryRunner.query('ALTER TABLE `revisions_revisions` ADD `shared_vault_uuid` varchar(36) NULL')
await queryRunner.query('ALTER TABLE `revisions_revisions` ADD `key_system_identifier` varchar(36) NULL')
await queryRunner.query('ALTER TABLE `revisions_revisions` ADD `edited_by` varchar(36) NULL, ALGORITHM = INSTANT')
await queryRunner.query(
'ALTER TABLE `revisions_revisions` ADD `shared_vault_uuid` varchar(36) NULL, ALGORITHM = INSTANT',
)
await queryRunner.query(
'ALTER TABLE `revisions_revisions` ADD `key_system_identifier` varchar(36) NULL, ALGORITHM = INSTANT',
)
await queryRunner.query(
'CREATE INDEX `index_revisions_on_shared_vault_uuid` ON `revisions_revisions` (`shared_vault_uuid`)',
)
@ -25,7 +29,7 @@ export class AddSharedVaultInformation1693915383950 implements MigrationInterfac
)
const revisionsTableExists = revisionsTableExistsQueryResult[0].count === 1
if (revisionsTableExists) {
await queryRunner.query('RENAME TABLE `revisions` TO `revisions_revisions`')
await queryRunner.query('ALTER TABLE `revisions` RENAME TO `revisions_revisions`, ALGORITHM=INSTANT')
}
}
}

View file

@ -9,7 +9,7 @@ export class RenameRevisionsTable1694425333972 implements MigrationInterface {
)
const revisionsTableExists = revisionsTableExistsQueryResult[0].count === 1
if (revisionsTableExists) {
await queryRunner.query('RENAME TABLE `revisions` TO `revisions_revisions`')
await queryRunner.query('ALTER TABLE `revisions` RENAME TO `revisions_revisions`, ALGORITHM=INSTANT')
}
}

View file

@ -11,8 +11,6 @@ import * as winston from 'winston'
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'
@ -48,8 +46,6 @@ import { S3DumpRepository } from '../Infra/S3/S3ItemDumpRepository'
import { RevisionItemStringMapper } from '../Mapping/Backup/RevisionItemStringMapper'
import { BaseRevisionsController } from '../Infra/InversifyExpress/Base/BaseRevisionsController'
import { Transform } from 'stream'
import { SQLLegacyRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/SQL/SQLLegacyRevisionMetadataPersistenceMapper'
import { SQLLegacyRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLLegacyRevisionPersistenceMapper'
import { RevisionHttpMapper } from '../Mapping/Http/RevisionHttpMapper'
import { RevisionMetadataHttpRepresentation } from '../Mapping/Http/RevisionMetadataHttpRepresentation'
import { RevisionHttpRepresentation } from '../Mapping/Http/RevisionHttpRepresentation'
@ -160,25 +156,14 @@ export class ContainerConfigLoader {
}
// Map
container
.bind<MapperInterface<RevisionMetadata, SQLLegacyRevision>>(
TYPES.Revisions_SQLLegacyRevisionMetadataPersistenceMapper,
)
.toConstantValue(new SQLLegacyRevisionMetadataPersistenceMapper())
container
.bind<MapperInterface<RevisionMetadata, SQLRevision>>(TYPES.Revisions_SQLRevisionMetadataPersistenceMapper)
.toConstantValue(new SQLRevisionMetadataPersistenceMapper())
container
.bind<MapperInterface<Revision, SQLLegacyRevision>>(TYPES.Revisions_SQLLegacyRevisionPersistenceMapper)
.toConstantValue(new SQLLegacyRevisionPersistenceMapper(container.get<TimerInterface>(TYPES.Revisions_Timer)))
container
.bind<MapperInterface<Revision, SQLRevision>>(TYPES.Revisions_SQLRevisionPersistenceMapper)
.toConstantValue(new SQLRevisionPersistenceMapper(container.get<TimerInterface>(TYPES.Revisions_Timer)))
// ORM
container
.bind<Repository<SQLLegacyRevision>>(TYPES.Revisions_ORMLegacyRevisionRepository)
.toDynamicValue(() => appDataSource.getRepository(SQLLegacyRevision))
container
.bind<Repository<SQLRevision>>(TYPES.Revisions_ORMRevisionRepository)
.toConstantValue(appDataSource.getRepository(SQLRevision))
@ -187,25 +172,14 @@ export class ContainerConfigLoader {
container
.bind<RevisionRepositoryInterface>(TYPES.Revisions_SQLRevisionRepository)
.toConstantValue(
isConfiguredForHomeServerOrSelfHosting
? new SQLRevisionRepository(
container.get<Repository<SQLRevision>>(TYPES.Revisions_ORMRevisionRepository),
container.get<MapperInterface<RevisionMetadata, SQLRevision>>(
TYPES.Revisions_SQLRevisionMetadataPersistenceMapper,
),
container.get<MapperInterface<Revision, SQLRevision>>(TYPES.Revisions_SQLRevisionPersistenceMapper),
container.get<winston.Logger>(TYPES.Revisions_Logger),
)
: new SQLLegacyRevisionRepository(
container.get<Repository<SQLLegacyRevision>>(TYPES.Revisions_ORMLegacyRevisionRepository),
container.get<MapperInterface<RevisionMetadata, SQLLegacyRevision>>(
TYPES.Revisions_SQLLegacyRevisionMetadataPersistenceMapper,
),
container.get<MapperInterface<Revision, SQLLegacyRevision>>(
TYPES.Revisions_SQLLegacyRevisionPersistenceMapper,
),
container.get<winston.Logger>(TYPES.Revisions_Logger),
),
new SQLRevisionRepository(
container.get<Repository<SQLRevision>>(TYPES.Revisions_ORMRevisionRepository),
container.get<MapperInterface<RevisionMetadata, SQLRevision>>(
TYPES.Revisions_SQLRevisionMetadataPersistenceMapper,
),
container.get<MapperInterface<Revision, SQLRevision>>(TYPES.Revisions_SQLRevisionPersistenceMapper),
container.get<winston.Logger>(TYPES.Revisions_Logger),
),
)
container

View file

@ -1,8 +1,6 @@
import { DataSource, EntityTarget, LoggerOptions, ObjectLiteral, Repository } from 'typeorm'
import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'
import { SQLLegacyRevision } from '../Infra/TypeORM/SQL/SQLLegacyRevision'
import { Env } from './Env'
import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision'
@ -34,23 +32,15 @@ export class AppDataSource {
const isConfiguredForMySQL = this.configuration.env.get('DB_TYPE') === 'mysql'
const isConfiguredForHomeServerOrSelfHosting =
this.configuration.env.get('MODE', true) === 'home-server' ||
this.configuration.env.get('MODE', true) === 'self-hosted'
const maxQueryExecutionTime = this.configuration.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +this.configuration.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
const migrationsSourceDirectoryName = isConfiguredForMySQL
? isConfiguredForHomeServerOrSelfHosting
? 'mysql'
: 'mysql-legacy'
: 'sqlite'
const migrationsSourceDirectoryName = isConfiguredForMySQL ? 'mysql' : 'sqlite'
const commonDataSourceOptions = {
maxQueryExecutionTime,
entities: [isConfiguredForHomeServerOrSelfHosting ? SQLRevision : SQLLegacyRevision],
entities: [SQLRevision],
migrations: [`${__dirname}/../../migrations/${migrationsSourceDirectoryName}/*.js`],
migrationsRun: this.configuration.runMigrations,
logging: <LoggerOptions>this.configuration.env.get('DB_DEBUG_LEVEL', true) ?? 'info',

View file

@ -7,18 +7,13 @@ const TYPES = {
Revisions_S3: Symbol.for('Revisions_S3'),
Revisions_Env: Symbol.for('Revisions_Env'),
// Map
Revisions_SQLLegacyRevisionMetadataPersistenceMapper: Symbol.for(
'Revisions_SQLLegacyRevisionMetadataPersistenceMapper',
),
Revisions_SQLRevisionMetadataPersistenceMapper: Symbol.for('Revisions_SQLRevisionMetadataPersistenceMapper'),
Revisions_SQLLegacyRevisionPersistenceMapper: Symbol.for('Revisions_SQLLegacyRevisionPersistenceMapper'),
Revisions_SQLRevisionPersistenceMapper: Symbol.for('Revisions_SQLRevisionPersistenceMapper'),
Revisions_RevisionItemStringMapper: Symbol.for('Revisions_RevisionItemStringMapper'),
Revisions_RevisionHttpMapper: Symbol.for('Revisions_RevisionHttpMapper'),
Revisions_RevisionMetadataHttpMapper: Symbol.for('Revisions_RevisionMetadataHttpMapper'),
// ORM
Revisions_ORMRevisionRepository: Symbol.for('Revisions_ORMRevisionRepository'),
Revisions_ORMLegacyRevisionRepository: Symbol.for('Revisions_ORMLegacyRevisionRepository'),
// Repositories
Revisions_SQLRevisionRepository: Symbol.for('Revisions_SQLRevisionRepository'),
Revisions_DumpRepository: Symbol.for('Revisions_DumpRepository'),

View file

@ -1,83 +0,0 @@
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'revisions' })
export class SQLLegacyRevision {
@PrimaryGeneratedColumn('uuid')
declare uuid: string
@Column({
name: 'item_uuid',
length: 36,
})
@Index('item_uuid')
declare itemUuid: string
@Column({
name: 'user_uuid',
length: 36,
type: 'varchar',
nullable: true,
})
@Index('user_uuid')
declare userUuid: string | null
@Column({
type: 'text',
nullable: true,
})
declare content: string | null
@Column({
name: 'content_type',
type: 'varchar',
length: 255,
nullable: true,
})
declare contentType: string | null
@Column({
type: 'varchar',
name: 'items_key_id',
length: 255,
nullable: true,
})
declare itemsKeyId: string | null
@Column({
name: 'enc_item_key',
type: 'text',
nullable: true,
})
declare encItemKey: string | null
@Column({
name: 'auth_hash',
type: 'varchar',
length: 255,
nullable: true,
})
declare authHash: string | null
@Column({
name: 'creation_date',
type: 'date',
nullable: true,
})
declare creationDate: Date
@Column({
name: 'created_at',
type: 'datetime',
precision: 6,
nullable: true,
})
declare createdAt: Date
@Column({
name: 'updated_at',
type: 'datetime',
precision: 6,
nullable: true,
})
declare updatedAt: Date
}

View file

@ -1,161 +0,0 @@
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { Repository } from 'typeorm'
import { Logger } from 'winston'
import { Revision } from '../../../Domain/Revision/Revision'
import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
import { RevisionRepositoryInterface } from '../../../Domain/Revision/RevisionRepositoryInterface'
import { SQLLegacyRevision } from './SQLLegacyRevision'
export class SQLLegacyRevisionRepository implements RevisionRepositoryInterface {
constructor(
protected ormRepository: Repository<SQLLegacyRevision>,
protected revisionMetadataMapper: MapperInterface<RevisionMetadata, SQLLegacyRevision>,
protected revisionMapper: MapperInterface<Revision, SQLLegacyRevision>,
protected logger: Logger,
) {}
async clearSharedVaultAndKeySystemAssociations(_dto: { itemUuid?: Uuid; sharedVaultUuid: Uuid }): Promise<void> {
this.logger.error('Method clearSharedVaultAndKeySystemAssociations not implemented.')
}
async countByUserUuid(userUuid: Uuid): Promise<number> {
return this.ormRepository
.createQueryBuilder()
.where('user_uuid = :userUuid', { userUuid: userUuid.value })
.getCount()
}
async findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Revision[]> {
const queryBuilder = this.ormRepository
.createQueryBuilder('revision')
.where('revision.user_uuid = :userUuid', { userUuid: dto.userUuid.value })
.orderBy('revision.created_at', 'ASC')
if (dto.offset !== undefined) {
queryBuilder.skip(dto.offset)
}
if (dto.limit !== undefined) {
queryBuilder.take(dto.limit)
}
const sqlRevisions = await queryBuilder.getMany()
const revisions = []
for (const sqlRevision of sqlRevisions) {
revisions.push(this.revisionMapper.toDomain(sqlRevision))
}
return revisions
}
async updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.update()
.set({
userUuid: userUuid.value,
})
.where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
.execute()
}
async findByItemUuid(itemUuid: Uuid): Promise<Revision[]> {
const SQLLegacyRevisions = await this.ormRepository
.createQueryBuilder()
.where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
.getMany()
const revisions = []
for (const revision of SQLLegacyRevisions) {
revisions.push(this.revisionMapper.toDomain(revision))
}
return revisions
}
async removeByUserUuid(userUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.delete()
.from('revisions')
.where('user_uuid = :userUuid', { userUuid: userUuid.value })
.execute()
}
async removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.delete()
.from('revisions')
.where('uuid = :revisionUuid AND user_uuid = :userUuid', {
userUuid: userUuid.value,
revisionUuid: revisionUuid.value,
})
.execute()
}
async findOneByUuid(revisionUuid: Uuid, userUuid: Uuid, _sharedVaultUuids: Uuid[]): Promise<Revision | null> {
const SQLLegacyRevision = await this.ormRepository
.createQueryBuilder()
.where('uuid = :revisionUuid', { revisionUuid: revisionUuid.value })
.andWhere('user_uuid = :userUuid', { userUuid: userUuid.value })
.getOne()
if (SQLLegacyRevision === null) {
return null
}
return this.revisionMapper.toDomain(SQLLegacyRevision)
}
async insert(revision: Revision): Promise<boolean> {
const projection = this.revisionMapper.toProjection(revision)
await this.ormRepository.insert(projection)
return true
}
async update(revision: Revision): Promise<boolean> {
const projection = this.revisionMapper.toProjection(revision)
const { uuid, ...rest } = projection
await this.ormRepository.update({ uuid: uuid }, rest)
return true
}
async findMetadataByItemId(
itemUuid: Uuid,
userUuid: Uuid,
_sharedVaultUuids: Uuid[],
): Promise<Array<RevisionMetadata>> {
const queryBuilder = this.ormRepository
.createQueryBuilder()
.select('uuid', 'uuid')
.addSelect('content_type', 'contentType')
.addSelect('created_at', 'createdAt')
.addSelect('updated_at', 'updatedAt')
.addSelect('item_uuid', 'itemUuid')
.where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
.andWhere('user_uuid = :userUuid', { userUuid: userUuid.value })
.orderBy('created_at', 'DESC')
const simplifiedRevisions = await queryBuilder.getRawMany()
this.logger.debug(
`Found ${simplifiedRevisions.length} revisions entries for item ${itemUuid.value}`,
simplifiedRevisions,
)
const metadata = []
for (const simplifiedRevision of simplifiedRevisions) {
metadata.push(this.revisionMetadataMapper.toDomain(simplifiedRevision))
}
return metadata
}
}

View file

@ -1,9 +1,86 @@
import { Column, Entity, Index } from 'typeorm'
import { SQLLegacyRevision } from './SQLLegacyRevision'
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'revisions_revisions' })
export class SQLRevision extends SQLLegacyRevision {
export class SQLRevision {
@PrimaryGeneratedColumn('uuid')
declare uuid: string
@Column({
name: 'item_uuid',
length: 36,
})
@Index('item_uuid')
declare itemUuid: string
@Column({
name: 'user_uuid',
length: 36,
type: 'varchar',
nullable: true,
})
@Index('user_uuid')
declare userUuid: string | null
@Column({
type: 'text',
nullable: true,
})
declare content: string | null
@Column({
name: 'content_type',
type: 'varchar',
length: 255,
nullable: true,
})
declare contentType: string | null
@Column({
type: 'varchar',
name: 'items_key_id',
length: 255,
nullable: true,
})
declare itemsKeyId: string | null
@Column({
name: 'enc_item_key',
type: 'text',
nullable: true,
})
declare encItemKey: string | null
@Column({
name: 'auth_hash',
type: 'varchar',
length: 255,
nullable: true,
})
declare authHash: string | null
@Column({
name: 'creation_date',
type: 'date',
nullable: true,
})
declare creationDate: Date
@Column({
name: 'created_at',
type: 'datetime',
precision: 6,
nullable: true,
})
declare createdAt: Date
@Column({
name: 'updated_at',
type: 'datetime',
precision: 6,
nullable: true,
})
declare updatedAt: Date
@Column({
type: 'varchar',
name: 'edited_by',

View file

@ -4,20 +4,18 @@ import { Logger } from 'winston'
import { Revision } from '../../../Domain/Revision/Revision'
import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
import { SQLLegacyRevisionRepository } from './SQLLegacyRevisionRepository'
import { RevisionRepositoryInterface } from '../../../Domain/Revision/RevisionRepositoryInterface'
import { SQLRevision } from './SQLRevision'
export class SQLRevisionRepository extends SQLLegacyRevisionRepository {
export class SQLRevisionRepository implements RevisionRepositoryInterface {
constructor(
protected override ormRepository: Repository<SQLRevision>,
protected override revisionMetadataMapper: MapperInterface<RevisionMetadata, SQLRevision>,
protected override revisionMapper: MapperInterface<Revision, SQLRevision>,
protected override logger: Logger,
) {
super(ormRepository, revisionMetadataMapper, revisionMapper, logger)
}
protected ormRepository: Repository<SQLRevision>,
protected revisionMetadataMapper: MapperInterface<RevisionMetadata, SQLRevision>,
protected revisionMapper: MapperInterface<Revision, SQLRevision>,
protected logger: Logger,
) {}
override async removeByUserUuid(userUuid: Uuid): Promise<void> {
async removeByUserUuid(userUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.delete()
@ -26,7 +24,7 @@ export class SQLRevisionRepository extends SQLLegacyRevisionRepository {
.execute()
}
override async removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void> {
async removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.delete()
@ -38,7 +36,7 @@ export class SQLRevisionRepository extends SQLLegacyRevisionRepository {
.execute()
}
override async findOneByUuid(revisionUuid: Uuid, userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<Revision | null> {
async findOneByUuid(revisionUuid: Uuid, userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<Revision | null> {
const queryBuilder = this.ormRepository.createQueryBuilder()
if (sharedVaultUuids.length > 0) {
@ -66,10 +64,7 @@ export class SQLRevisionRepository extends SQLLegacyRevisionRepository {
return this.revisionMapper.toDomain(sqlRevision)
}
override async clearSharedVaultAndKeySystemAssociations(dto: {
itemUuid?: Uuid
sharedVaultUuid: Uuid
}): Promise<void> {
async clearSharedVaultAndKeySystemAssociations(dto: { itemUuid?: Uuid; sharedVaultUuid: Uuid }): Promise<void> {
const queryBuilder = this.ormRepository.createQueryBuilder().update().set({
sharedVaultUuid: null,
keySystemIdentifier: null,
@ -89,7 +84,7 @@ export class SQLRevisionRepository extends SQLLegacyRevisionRepository {
await queryBuilder.execute()
}
override async findMetadataByItemId(
async findMetadataByItemId(
itemUuid: Uuid,
userUuid: Uuid,
sharedVaultUuids: Uuid[],
@ -134,4 +129,78 @@ export class SQLRevisionRepository extends SQLLegacyRevisionRepository {
return metadata
}
async countByUserUuid(userUuid: Uuid): Promise<number> {
return this.ormRepository
.createQueryBuilder()
.where('user_uuid = :userUuid', { userUuid: userUuid.value })
.getCount()
}
async findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Revision[]> {
const queryBuilder = this.ormRepository
.createQueryBuilder('revision')
.where('revision.user_uuid = :userUuid', { userUuid: dto.userUuid.value })
.orderBy('revision.created_at', 'ASC')
if (dto.offset !== undefined) {
queryBuilder.skip(dto.offset)
}
if (dto.limit !== undefined) {
queryBuilder.take(dto.limit)
}
const sqlRevisions = await queryBuilder.getMany()
const revisions = []
for (const sqlRevision of sqlRevisions) {
revisions.push(this.revisionMapper.toDomain(sqlRevision))
}
return revisions
}
async updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.update()
.set({
userUuid: userUuid.value,
})
.where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
.execute()
}
async findByItemUuid(itemUuid: Uuid): Promise<Revision[]> {
const SQLRevisions = await this.ormRepository
.createQueryBuilder()
.where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
.getMany()
const revisions = []
for (const revision of SQLRevisions) {
revisions.push(this.revisionMapper.toDomain(revision))
}
return revisions
}
async insert(revision: Revision): Promise<boolean> {
const projection = this.revisionMapper.toProjection(revision)
await this.ormRepository.insert(projection)
return true
}
async update(revision: Revision): Promise<boolean> {
const projection = this.revisionMapper.toProjection(revision)
const { uuid, ...rest } = projection
await this.ormRepository.update({ uuid: uuid }, rest)
return true
}
}

View file

@ -1,51 +0,0 @@
import { MapperInterface, Dates, UniqueEntityId, ContentType, Uuid } from '@standardnotes/domain-core'
import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
import { SQLLegacyRevision } from '../../../Infra/TypeORM/SQL/SQLLegacyRevision'
export class SQLLegacyRevisionMetadataPersistenceMapper
implements MapperInterface<RevisionMetadata, SQLLegacyRevision>
{
toDomain(projection: SQLLegacyRevision): RevisionMetadata {
const contentTypeOrError = ContentType.create(projection.contentType)
if (contentTypeOrError.isFailed()) {
throw new Error(`Could not create content type: ${contentTypeOrError.getError()}`)
}
const contentType = contentTypeOrError.getValue()
const createdAt = projection.createdAt instanceof Date ? projection.createdAt : new Date(projection.createdAt)
const updatedAt = projection.updatedAt instanceof Date ? projection.updatedAt : new Date(projection.updatedAt)
const datesOrError = Dates.create(createdAt, updatedAt)
if (datesOrError.isFailed()) {
throw new Error(`Could not create dates: ${datesOrError.getError()}`)
}
const dates = datesOrError.getValue()
const itemUuidOrError = Uuid.create(projection.itemUuid)
if (itemUuidOrError.isFailed()) {
throw new Error(`Could not create item uuid: ${itemUuidOrError.getError()}`)
}
const itemUuid = itemUuidOrError.getValue()
const revisionMetadataOrError = RevisionMetadata.create(
{
contentType,
dates,
sharedVaultUuid: null,
itemUuid,
},
new UniqueEntityId(projection.uuid),
)
if (revisionMetadataOrError.isFailed()) {
throw new Error(`Could not create revision metdata: ${revisionMetadataOrError.getError()}`)
}
return revisionMetadataOrError.getValue()
}
toProjection(_domain: RevisionMetadata): SQLLegacyRevision {
throw new Error('Method not implemented.')
}
}

View file

@ -1,78 +0,0 @@
import { MapperInterface, Dates, UniqueEntityId, Uuid, ContentType } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { Revision } from '../../../Domain/Revision/Revision'
import { SQLLegacyRevision } from '../../../Infra/TypeORM/SQL/SQLLegacyRevision'
export class SQLLegacyRevisionPersistenceMapper implements MapperInterface<Revision, SQLLegacyRevision> {
constructor(private timer: TimerInterface) {}
toDomain(projection: SQLLegacyRevision): Revision {
const contentTypeOrError = ContentType.create(projection.contentType)
if (contentTypeOrError.isFailed()) {
throw new Error(`Could not map typeorm revision to domain revision: ${contentTypeOrError.getError()}`)
}
const contentType = contentTypeOrError.getValue()
const datesOrError = Dates.create(projection.createdAt, projection.updatedAt)
if (datesOrError.isFailed()) {
throw new Error(`Could not map typeorm revision to domain revision: ${datesOrError.getError()}`)
}
const dates = datesOrError.getValue()
const itemUuidOrError = Uuid.create(projection.itemUuid)
if (itemUuidOrError.isFailed()) {
throw new Error(`Could not map typeorm revision to domain revision: ${itemUuidOrError.getError()}`)
}
const itemUuid = itemUuidOrError.getValue()
let userUuid = null
if (projection.userUuid !== null) {
const userUuidOrError = Uuid.create(projection.userUuid)
if (userUuidOrError.isFailed()) {
throw new Error(`Could not map typeorm revision to domain revision: ${userUuidOrError.getError()}`)
}
userUuid = userUuidOrError.getValue()
}
const revisionOrError = Revision.create(
{
authHash: projection.authHash,
content: projection.content,
contentType,
creationDate: new Date(this.timer.convertDateToFormattedString(projection.creationDate, 'YYYY-MM-DD')),
encItemKey: projection.encItemKey,
itemsKeyId: projection.itemsKeyId,
itemUuid,
userUuid,
dates,
},
new UniqueEntityId(projection.uuid),
)
if (revisionOrError.isFailed()) {
throw new Error(`Could not map typeorm revision to domain revision: ${revisionOrError.getError()}`)
}
return revisionOrError.getValue()
}
toProjection(domain: Revision): SQLLegacyRevision {
const sqlRevision = new SQLLegacyRevision()
sqlRevision.authHash = domain.props.authHash
sqlRevision.content = domain.props.content
sqlRevision.contentType = domain.props.contentType.value
sqlRevision.createdAt = domain.props.dates.createdAt
sqlRevision.updatedAt = domain.props.dates.updatedAt
sqlRevision.creationDate = new Date(
this.timer.convertDateToFormattedString(domain.props.creationDate, 'YYYY-MM-DD'),
)
sqlRevision.encItemKey = domain.props.encItemKey
sqlRevision.itemUuid = domain.props.itemUuid.value
sqlRevision.itemsKeyId = domain.props.itemsKeyId
sqlRevision.userUuid = domain.props.userUuid ? domain.props.userUuid.value : null
sqlRevision.uuid = domain.id.toString()
return sqlRevision
}
}

View file

@ -1,50 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class initDatabase1606470249552 implements MigrationInterface {
name = 'initDatabase1606470249552'
public async up(queryRunner: QueryRunner): Promise<void> {
await this.fixUpdatedAtTimestampsFromLegacyMigration(queryRunner)
await queryRunner.query(
'CREATE TABLE IF NOT EXISTS `items` (`uuid` varchar(36) NOT NULL, `duplicate_of` varchar(36) NULL, `items_key_id` varchar(255) NULL, `content` mediumtext NULL, `content_type` varchar(255) NULL, `enc_item_key` text NULL, `auth_hash` varchar(255) NULL, `user_uuid` varchar(36) NULL, `deleted` tinyint(1) NULL DEFAULT 0, `last_user_agent` text NULL, `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL, `created_at_timestamp` BIGINT NOT NULL, `updated_at_timestamp` BIGINT NOT NULL, INDEX `index_items_on_content_type` (`content_type`), INDEX `index_items_on_user_uuid` (`user_uuid`), INDEX `index_items_on_deleted` (`deleted`), INDEX `updated_at_timestamp` (`updated_at_timestamp`), INDEX `index_items_on_updated_at` (`updated_at`), INDEX `user_uuid_and_updated_at_timestamp_and_created_at_timestamp` (`user_uuid`, `updated_at_timestamp`, `created_at_timestamp`), INDEX `index_items_on_user_uuid_and_updated_at_and_created_at` (`user_uuid`, `updated_at`, `created_at`), INDEX `index_items_on_user_uuid_and_content_type` (`user_uuid`, `content_type`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
await queryRunner.query(
'CREATE TABLE IF NOT EXISTS `revisions` (`uuid` varchar(36) NOT NULL, `item_uuid` varchar(36) NULL, `content` mediumtext NULL, `content_type` varchar(255) NULL, `items_key_id` varchar(255) NULL, `enc_item_key` text NULL, `auth_hash` varchar(255) NULL, `creation_date` date NULL, `created_at` datetime(6) NULL, `updated_at` datetime(6) NULL, INDEX `index_revisions_on_item_uuid` (`item_uuid`), INDEX `index_revisions_on_creation_date` (`creation_date`), INDEX `index_revisions_on_created_at` (`created_at`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
await queryRunner.query(
'CREATE TABLE IF NOT EXISTS `item_revisions` (`uuid` varchar(36) NOT NULL, `item_uuid` varchar(36) NOT NULL, `revision_uuid` varchar(36) NOT NULL, INDEX `index_item_revisions_on_item_uuid` (`item_uuid`), INDEX `index_item_revisions_on_revision_uuid` (`revision_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(_queryRunner: QueryRunner): Promise<void> {
return
}
private async fixUpdatedAtTimestampsFromLegacyMigration(queryRunner: QueryRunner): Promise<void> {
const itemsTableExistsQueryResult = await queryRunner.manager.query(
'SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = "items"',
)
const itemsTableExists = itemsTableExistsQueryResult[0].count === 1
if (!itemsTableExists) {
return
}
const updatedAtTimestampColumnExistsQueryResult = await queryRunner.manager.query(
'SELECT COUNT(*) as count FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = "items" AND column_name = "updated_at_timestamp"',
)
const updatedAtTimestampColumnExists = updatedAtTimestampColumnExistsQueryResult[0].count === 1
if (updatedAtTimestampColumnExists) {
return
}
await queryRunner.query('ALTER TABLE `items` ADD COLUMN `updated_at_timestamp` BIGINT NOT NULL')
await queryRunner.query('ALTER TABLE `items` ADD COLUMN `created_at_timestamp` BIGINT NOT NULL')
await queryRunner.query(
'ALTER TABLE `items` ADD INDEX `user_uuid_and_updated_at_timestamp_and_created_at_timestamp` (`user_uuid`, `updated_at_timestamp`, `created_at_timestamp`)',
)
await queryRunner.query('ALTER TABLE `items` ADD INDEX `updated_at_timestamp` (`updated_at_timestamp`)')
await queryRunner.query('UPDATE `items` SET `created_at_timestamp` = UNIX_TIMESTAMP(`created_at`) * 1000000')
await queryRunner.query('UPDATE `items` SET `updated_at_timestamp` = UNIX_TIMESTAMP(`updated_at`) * 1000000')
}
}

View file

@ -1,15 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addExtensionSettings1617615657558 implements MigrationInterface {
name = 'addExtensionSettings1617615657558'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE IF NOT EXISTS `extension_settings` (`uuid` varchar(36) NOT NULL, `extension_id` varchar(255) NULL, `mute_emails` tinyint(1) NULL DEFAULT 0, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX `index_extension_settings_on_extension_id` (`extension_id`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(_queryRunner: QueryRunner): Promise<void> {
return
}
}

View file

@ -1,27 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class dropUnusedIndexes1629964808297 implements MigrationInterface {
name = 'dropUnusedIndexes1629964808297'
public async up(queryRunner: QueryRunner): Promise<void> {
const indexItemsOnUserAndTimestamp = await queryRunner.manager.query(
'SHOW INDEX FROM `items` where `key_name` = "index_items_on_user_uuid_and_updated_at_and_created_at"',
)
const indexItemsOnUserAndTimestampExists = indexItemsOnUserAndTimestamp && indexItemsOnUserAndTimestamp.length > 0
if (indexItemsOnUserAndTimestampExists) {
await queryRunner.query('ALTER TABLE `items` DROP INDEX index_items_on_user_uuid_and_updated_at_and_created_at')
}
const indexItemsOnUpdatedAt = await queryRunner.manager.query(
'SHOW INDEX FROM `items` where `key_name` = "index_items_on_updated_at"',
)
const indexItemsOnUpdatedAtExists = indexItemsOnUpdatedAt && indexItemsOnUpdatedAt.length > 0
if (indexItemsOnUpdatedAtExists) {
await queryRunner.query('ALTER TABLE `items` DROP INDEX index_items_on_updated_at')
}
}
public async down(): Promise<void> {
return
}
}

View file

@ -1,11 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class refactorCalculatingIntegrityHash1630318893601 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `items` ADD INDEX `user_uuid_and_deleted` (`user_uuid`, `deleted`)')
}
public async down(): Promise<void> {
return
}
}

View file

@ -1,12 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class restrictContentType1630417724617 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('UPDATE `items` SET content_type = "Unknown" WHERE `content_type` IS NULL')
await queryRunner.query('ALTER TABLE `items` CHANGE `content_type` `content_type` varchar(255) NOT NULL')
}
public async down(): Promise<void> {
return
}
}

View file

@ -1,26 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
import { v4 } from 'uuid'
export class addRevisionForDuplicatedItems1631529502150 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const itemRevisions = await queryRunner.manager.query(
'SELECT r.uuid as originalRevisionUuid, ir.item_uuid as properItemUuid, ir.uuid as relationUuid FROM revisions r INNER JOIN item_revisions ir ON ir.revision_uuid = r.uuid AND ir.item_uuid <> r.item_uuid',
)
for (const itemRevision of itemRevisions) {
const revisionUuid = v4()
await queryRunner.manager.query(
`INSERT INTO revisions (uuid, item_uuid, content, content_type, items_key_id, enc_item_key, auth_hash, creation_date, created_at, updated_at) SELECT "${revisionUuid}", "${itemRevision['properItemUuid']}", content, content_type, items_key_id, enc_item_key, auth_hash, creation_date, created_at, updated_at FROM revisions WHERE uuid = "${itemRevision['originalRevisionUuid']}"`,
)
await queryRunner.manager.query(
`UPDATE item_revisions SET revision_uuid = "${revisionUuid}" WHERE uuid = "${itemRevision['relationUuid']}"`,
)
}
}
public async down(): Promise<void> {
return
}
}

View file

@ -1,13 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class dropItemRevisionsJoiningTable1631530260504 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP TABLE `item_revisions`')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `item_revisions` (`uuid` varchar(36) NOT NULL, `item_uuid` varchar(36) NOT NULL, `revision_uuid` varchar(36) NOT NULL, INDEX `index_item_revisions_on_item_uuid` (`item_uuid`), INDEX `index_item_revisions_on_revision_uuid` (`revision_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
}

View file

@ -1,36 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class cleanupOrphanItemsAndRevisions1632219307742 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const usersTableExistsQueryResult = await queryRunner.manager.query(
'SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = "users"',
)
const usersTableExists = usersTableExistsQueryResult[0].count === 1
if (usersTableExists) {
const orphanedItems = await queryRunner.manager.query(
'SELECT i.uuid as uuid FROM items i LEFT JOIN users u ON i.user_uuid = u.uuid WHERE u.uuid IS NULL',
)
for (const orphanedItem of orphanedItems) {
await queryRunner.manager.query(`DELETE FROM revisions WHERE item_uuid = "${orphanedItem['uuid']}"`)
await queryRunner.manager.query(`DELETE FROM items WHERE uuid = "${orphanedItem['uuid']}"`)
}
}
await queryRunner.manager.query('DELETE FROM items WHERE user_uuid IS NULL')
const orphanedRevisions = await queryRunner.manager.query(
'SELECT r.uuid as uuid FROM revisions r LEFT JOIN items i ON r.item_uuid = i.uuid WHERE i.uuid IS NULL',
)
for (const orphanedRevision of orphanedRevisions) {
await queryRunner.manager.query(`DELETE FROM revisions WHERE uuid = "${orphanedRevision['uuid']}"`)
}
await queryRunner.manager.query('DELETE FROM revisions WHERE item_uuid IS NULL')
}
public async down(): Promise<void> {
return
}
}

View file

@ -1,28 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addRevisionsItemsRelation1632221263106 implements MigrationInterface {
name = 'addRevisionsItemsRelation1632221263106'
public async up(queryRunner: QueryRunner): Promise<void> {
const indexRevisionsOnItemUuid = await queryRunner.manager.query(
'SHOW INDEX FROM `revisions` where `key_name` = "index_revisions_on_item_uuid"',
)
const indexRevisionsOnItemUuidExists = indexRevisionsOnItemUuid && indexRevisionsOnItemUuid.length > 0
if (indexRevisionsOnItemUuidExists) {
await queryRunner.query('DROP INDEX `index_revisions_on_item_uuid` ON `revisions`')
}
await queryRunner.query('ALTER TABLE `revisions` CHANGE `item_uuid` `item_uuid` varchar(36) NOT NULL')
await queryRunner.query('ALTER TABLE `items` CHANGE `user_uuid` `user_uuid` varchar(36) NOT NULL')
await queryRunner.query(
'ALTER TABLE `revisions` ADD CONSTRAINT `FK_ab3b92e54701fe3010022a31d90` FOREIGN KEY (`item_uuid`) REFERENCES `items`(`uuid`) ON DELETE CASCADE ON UPDATE NO ACTION',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `revisions` DROP FOREIGN KEY `FK_ab3b92e54701fe3010022a31d90`')
await queryRunner.query('ALTER TABLE `items` CHANGE `user_uuid` `user_uuid` varchar(36) NULL')
await queryRunner.query('ALTER TABLE `revisions` CHANGE `item_uuid` `item_uuid` varchar(36) NULL')
await queryRunner.query('CREATE INDEX `index_revisions_on_item_uuid` ON `revisions` (`item_uuid`)')
}
}

View file

@ -1,13 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addItemContentSize1637738491169 implements MigrationInterface {
name = 'addItemContentSize1637738491169'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `items` ADD `content_size` INT UNSIGNED NULL')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `items` DROP COLUMN `content_size`')
}
}

View file

@ -1,11 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class removeExtensionSettings1639134926025 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP TABLE `extension_settings`')
}
public async down(): Promise<void> {
return
}
}

View file

@ -1,11 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class removeSfExtensionItems1642073387521 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.manager.query('DELETE FROM items WHERE content_type = "SF|Extension"')
}
public async down(): Promise<void> {
return
}
}

View file

@ -1,11 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class removeUserAgent1647501696205 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `items` DROP COLUMN `last_user_agent`')
}
public async down(): Promise<void> {
return
}
}

View file

@ -1,13 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addUpdatedWithSession1654518291191 implements MigrationInterface {
name = 'addUpdatedWithSession1654518291191'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `items` ADD `updated_with_session` varchar(36) NULL')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `items` DROP COLUMN `updated_with_session`')
}
}

View file

@ -1,16 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddNotifications1689671563304 implements MigrationInterface {
name = 'AddNotifications1689671563304'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE IF NOT EXISTS `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`')
}
}

View file

@ -1,25 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddSharedVaultAndKeySystemAssociations1689671563305 implements MigrationInterface {
name = 'AddSharedVaultAndKeySystemAssociations1689671563305'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `shared_vault_associations` (`uuid` varchar(36) NOT NULL, `shared_vault_uuid` varchar(36) NOT NULL, `item_uuid` varchar(36) NOT NULL, `last_edited_by` varchar(36) NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `shared_vault_uuid_on_shared_vault_associations` (`shared_vault_uuid`), INDEX `item_uuid_on_shared_vault_associations` (`item_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
await queryRunner.query(
'CREATE TABLE `key_system_associations` (`uuid` varchar(36) NOT NULL, `key_system_uuid` varchar(36) NOT NULL, `item_uuid` varchar(36) NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `key_system_uuid_on_key_system_associations` (`key_system_uuid`), INDEX `item_uuid_on_key_system_associations` (`item_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `item_uuid_on_key_system_associations` ON `key_system_associations`')
await queryRunner.query('DROP INDEX `key_system_uuid_on_key_system_associations` ON `key_system_associations`')
await queryRunner.query('DROP TABLE `key_system_associations`')
await queryRunner.query('DROP INDEX `item_uuid_on_shared_vault_associations` ON `shared_vault_associations`')
await queryRunner.query(
'DROP INDEX `shared_vault_uuid_on_shared_vault_associations` ON `shared_vault_associations`',
)
await queryRunner.query('DROP TABLE `shared_vault_associations`')
}
}

View file

@ -1,29 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddSharedVaultsWithUsersAndInvites1689677728282 implements MigrationInterface {
name = 'AddSharedVaultsWithUsersAndInvites1689677728282'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `shared_vaults` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `file_upload_bytes_used` int NOT NULL, `file_upload_bytes_limit` int NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `user_uuid_on_shared_vaults` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
await queryRunner.query(
'CREATE TABLE `shared_vault_users` (`uuid` varchar(36) NOT NULL, `shared_vault_uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `permission` varchar(24) NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `shared_vault_uuid_on_shared_vault_users` (`shared_vault_uuid`), INDEX `user_uuid_on_shared_vault_users` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
await queryRunner.query(
'CREATE TABLE `shared_vault_invites` (`uuid` varchar(36) NOT NULL, `shared_vault_uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `sender_uuid` varchar(36) NOT NULL, `encrypted_message` text NOT NULL, `permission` varchar(24) NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `shared_vault_uuid_on_shared_vault_invites` (`shared_vault_uuid`), INDEX `user_uuid_on_shared_vault_invites` (`user_uuid`), INDEX `sender_uuid_on_shared_vault_invites` (`sender_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `sender_uuid_on_shared_vault_invites` ON `shared_vault_invites`')
await queryRunner.query('DROP INDEX `user_uuid_on_shared_vault_invites` ON `shared_vault_invites`')
await queryRunner.query('DROP INDEX `shared_vault_uuid_on_shared_vault_invites` ON `shared_vault_invites`')
await queryRunner.query('DROP TABLE `shared_vault_invites`')
await queryRunner.query('DROP INDEX `user_uuid_on_shared_vault_users` ON `shared_vault_users`')
await queryRunner.query('DROP INDEX `shared_vault_uuid_on_shared_vault_users` ON `shared_vault_users`')
await queryRunner.query('DROP TABLE `shared_vault_users`')
await queryRunner.query('DROP INDEX `user_uuid_on_shared_vaults` ON `shared_vaults`')
await queryRunner.query('DROP TABLE `shared_vaults`')
}
}

View file

@ -1,17 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddMessages1689745128577 implements MigrationInterface {
name = 'AddMessages1689745128577'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `messages` (`uuid` varchar(36) NOT NULL, `recipient_uuid` varchar(36) NOT NULL, `sender_uuid` varchar(36) NOT NULL, `encrypted_message` text NOT NULL, `replaceability_identifier` varchar(255) NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `recipient_uuid_on_messages` (`recipient_uuid`), INDEX `sender_uuid_on_messages` (`sender_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `sender_uuid_on_messages` ON `messages`')
await queryRunner.query('DROP INDEX `recipient_uuid_on_messages` ON `messages`')
await queryRunner.query('DROP TABLE `messages`')
}
}

View file

@ -1,27 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RenameKeyMessageIdentifier1689746180559 implements MigrationInterface {
name = 'RenameKeyMessageIdentifier1689746180559'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `key_system_uuid_on_key_system_associations` ON `key_system_associations`')
await queryRunner.query(
'ALTER TABLE `key_system_associations` CHANGE `key_system_uuid` `key_system_identifier` varchar(36) NOT NULL',
)
await queryRunner.query(
'CREATE INDEX `key_system_identifier_on_key_system_associations` ON `key_system_associations` (`key_system_identifier`)',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'DROP INDEX `key_system_identifier_on_key_system_associations` ON `key_system_associations`',
)
await queryRunner.query(
'ALTER TABLE `key_system_associations` CHANGE `key_system_identifier` `key_system_uuid` varchar(36) NOT NULL',
)
await queryRunner.query(
'CREATE INDEX `key_system_uuid_on_key_system_associations` ON `key_system_associations` (`key_system_uuid`)',
)
}
}

View file

@ -1,23 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class DeletePrivileges1690900526061 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const itemsWithPrivilegesContentTypeQueryResult = await queryRunner.manager.query(
'SELECT COUNT(*) as count FROM items i WHERE i.content_type = "SN|Privileges"',
)
const itemsWithPrivilegesContentTypeCount = +itemsWithPrivilegesContentTypeQueryResult[0].count
const batchSize = 1_000
const batchCount = Math.ceil(itemsWithPrivilegesContentTypeCount / batchSize)
for (let batchIndex = 0; batchIndex < batchCount; batchIndex++) {
await queryRunner.startTransaction()
await queryRunner.manager.query(`DELETE FROM items WHERE content_type = "SN|Privileges" LIMIT ${batchSize}`)
await queryRunner.commitTransaction()
}
}
public async down(): Promise<void> {
return
}
}

View file

@ -1,11 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class UpdateUnknownContent1690975361562 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.manager.query('UPDATE items SET content_type = "Note" WHERE content_type = "Unknown"')
}
public async down(): Promise<void> {
return
}
}

View file

@ -1,22 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RemoveRevisionsForeignKey1692176803410 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const revisionsTableExistsQueryResult = await queryRunner.manager.query(
'SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = "revisions"',
)
const revisionsTableExists = revisionsTableExistsQueryResult[0].count === 1
if (revisionsTableExists) {
try {
await queryRunner.query('ALTER TABLE `revisions` DROP FOREIGN KEY `FK_ab3b92e54701fe3010022a31d90`')
} catch (error) {
// eslint-disable-next-line no-console
console.log('Error dropping foreign key: ', (error as Error).message)
}
}
}
public async down(): Promise<void> {
return
}
}

View file

@ -1,21 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RemoveAssociations1692264556858 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'DROP INDEX `key_system_identifier_on_key_system_associations` ON `key_system_associations`',
)
await queryRunner.query('DROP INDEX `item_uuid_on_key_system_associations` ON `key_system_associations`')
await queryRunner.query('DROP TABLE `key_system_associations`')
await queryRunner.query('DROP INDEX `item_uuid_on_shared_vault_associations` ON `shared_vault_associations`')
await queryRunner.query(
'DROP INDEX `shared_vault_uuid_on_shared_vault_associations` ON `shared_vault_associations`',
)
await queryRunner.query('DROP TABLE `shared_vault_associations`')
}
public async down(): Promise<void> {
return
}
}

View file

@ -1,13 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RemoveSharedVaultLimit1692619430384 implements MigrationInterface {
name = 'RemoveSharedVaultLimit1692619430384'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `shared_vaults` DROP COLUMN `file_upload_bytes_limit`')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `shared_vaults` ADD `file_upload_bytes_limit` int NOT NULL')
}
}

View file

@ -1,13 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddDesignatedSurvivor1695284084365 implements MigrationInterface {
name = 'AddDesignatedSurvivor1695284084365'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `shared_vault_users` ADD `is_designated_survivor` tinyint NOT NULL DEFAULT 0')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `shared_vault_users` DROP COLUMN `is_designated_survivor`')
}
}

View file

@ -1,11 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RemoveNotifications1695643525793 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DELETE FROM `notifications`')
}
public async down(): Promise<void> {
return
}
}

View file

@ -4,16 +4,16 @@ export class AddSharedVaultInformation1693219736168 implements MigrationInterfac
name = 'AddSharedVaultInformation1693219736168'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `items` ADD `last_edited_by` varchar(36) NULL')
await queryRunner.query('ALTER TABLE `items` ADD `shared_vault_uuid` varchar(36) NULL')
await queryRunner.query('ALTER TABLE `items` ADD `key_system_identifier` varchar(36) NULL')
await queryRunner.query('ALTER TABLE `items` ADD `last_edited_by` varchar(36) NULL, ALGORITHM = INSTANT')
await queryRunner.query('ALTER TABLE `items` ADD `shared_vault_uuid` varchar(36) NULL, ALGORITHM = INSTANT')
await queryRunner.query('ALTER TABLE `items` ADD `key_system_identifier` varchar(36) NULL, ALGORITHM = INSTANT')
await queryRunner.query('CREATE INDEX `index_items_on_shared_vault_uuid` ON `items` (`shared_vault_uuid`)')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `index_items_on_shared_vault_uuid` ON `items`')
await queryRunner.query('ALTER TABLE `items` DROP COLUMN `key_system_identifier`')
await queryRunner.query('ALTER TABLE `items` DROP COLUMN `shared_vault_uuid`')
await queryRunner.query('ALTER TABLE `items` DROP COLUMN `last_edited_by`')
await queryRunner.query('ALTER TABLE `items` DROP COLUMN `key_system_identifier`, ALGORITHM = INSTANT')
await queryRunner.query('ALTER TABLE `items` DROP COLUMN `shared_vault_uuid`, ALGORITHM = INSTANT')
await queryRunner.query('ALTER TABLE `items` DROP COLUMN `last_edited_by`, ALGORITHM = INSTANT')
}
}

View file

@ -6,7 +6,6 @@ import TYPES from './Types'
import { AppDataSource } from './DataSource'
import { SNSClient, SNSClientConfig } from '@aws-sdk/client-sns'
import { ItemRepositoryInterface } from '../Domain/Item/ItemRepositoryInterface'
import { SQLLegacyItemRepository } from '../Infra/TypeORM/SQLLegacyItemRepository'
import { Repository } from 'typeorm'
import { Item } from '../Domain/Item/Item'
import {
@ -64,8 +63,6 @@ import {
} from '@standardnotes/domain-core'
import { BaseItemsController } from '../Infra/InversifyExpressUtils/Base/BaseItemsController'
import { Transform } from 'stream'
import { SQLLegacyItem } from '../Infra/TypeORM/SQLLegacyItem'
import { SQLLegacyItemPersistenceMapper } from '../Mapping/Persistence/SQLLegacyItemPersistenceMapper'
import { ItemHttpRepresentation } from '../Mapping/Http/ItemHttpRepresentation'
import { ItemHttpMapper } from '../Mapping/Http/ItemHttpMapper'
import { SavedItemHttpRepresentation } from '../Mapping/Http/SavedItemHttpRepresentation'
@ -303,9 +300,6 @@ export class ContainerConfigLoader {
)
// Mapping
container
.bind<MapperInterface<Item, SQLLegacyItem>>(TYPES.Sync_SQLLegacyItemPersistenceMapper)
.toConstantValue(new SQLLegacyItemPersistenceMapper())
container
.bind<MapperInterface<Item, SQLItem>>(TYPES.Sync_SQLItemPersistenceMapper)
.toConstantValue(new SQLItemPersistenceMapper())
@ -363,9 +357,6 @@ export class ContainerConfigLoader {
.toConstantValue(new NotificationHttpMapper())
// ORM
container
.bind<Repository<SQLLegacyItem>>(TYPES.Sync_ORMLegacyItemRepository)
.toDynamicValue(() => appDataSource.getRepository(SQLLegacyItem))
container
.bind<Repository<SQLItem>>(TYPES.Sync_ORMItemRepository)
.toConstantValue(appDataSource.getRepository(SQLItem))
@ -389,17 +380,11 @@ export class ContainerConfigLoader {
container
.bind<ItemRepositoryInterface>(TYPES.Sync_SQLItemRepository)
.toConstantValue(
isConfiguredForHomeServerOrSelfHosting
? new SQLItemRepository(
container.get<Repository<SQLItem>>(TYPES.Sync_ORMItemRepository),
container.get<MapperInterface<Item, SQLItem>>(TYPES.Sync_SQLItemPersistenceMapper),
container.get<Logger>(TYPES.Sync_Logger),
)
: new SQLLegacyItemRepository(
container.get<Repository<SQLLegacyItem>>(TYPES.Sync_ORMLegacyItemRepository),
container.get<MapperInterface<Item, SQLLegacyItem>>(TYPES.Sync_SQLLegacyItemPersistenceMapper),
container.get<Logger>(TYPES.Sync_Logger),
),
new SQLItemRepository(
container.get<Repository<SQLItem>>(TYPES.Sync_ORMItemRepository),
container.get<MapperInterface<Item, SQLItem>>(TYPES.Sync_SQLItemPersistenceMapper),
container.get<Logger>(TYPES.Sync_Logger),
),
)
container
.bind<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository)

View file

@ -2,7 +2,6 @@ import { DataSource, EntityTarget, LoggerOptions, ObjectLiteral, Repository } fr
import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'
import { Env } from './Env'
import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
import { SQLLegacyItem } from '../Infra/TypeORM/SQLLegacyItem'
import { TypeORMNotification } from '../Infra/TypeORM/TypeORMNotification'
import { TypeORMSharedVault } from '../Infra/TypeORM/TypeORMSharedVault'
import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
@ -36,24 +35,17 @@ export class AppDataSource {
this.configuration.env.load()
const isConfiguredForMySQL = this.configuration.env.get('DB_TYPE') === 'mysql'
const isConfiguredForHomeServerOrSelfHosting =
this.configuration.env.get('MODE', true) === 'home-server' ||
this.configuration.env.get('MODE', true) === 'self-hosted'
const maxQueryExecutionTime = this.configuration.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +this.configuration.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
const migrationsSourceDirectoryName = isConfiguredForMySQL
? isConfiguredForHomeServerOrSelfHosting
? 'mysql'
: 'mysql-legacy'
: 'sqlite'
const migrationsSourceDirectoryName = isConfiguredForMySQL ? 'mysql' : 'sqlite'
const commonDataSourceOptions = {
maxQueryExecutionTime,
entities: [
isConfiguredForHomeServerOrSelfHosting ? SQLItem : SQLLegacyItem,
SQLItem,
TypeORMNotification,
TypeORMSharedVault,
TypeORMSharedVaultUser,

View file

@ -15,7 +15,6 @@ const TYPES = {
Sync_MessageRepository: Symbol.for('Sync_MessageRepository'),
// ORM
Sync_ORMItemRepository: Symbol.for('Sync_ORMItemRepository'),
Sync_ORMLegacyItemRepository: Symbol.for('Sync_ORMLegacyItemRepository'),
Sync_ORMSharedVaultRepository: Symbol.for('Sync_ORMSharedVaultRepository'),
Sync_ORMSharedVaultInviteRepository: Symbol.for('Sync_ORMSharedVaultInviteRepository'),
Sync_ORMSharedVaultUserRepository: Symbol.for('Sync_ORMSharedVaultUserRepository'),
@ -128,7 +127,6 @@ const TYPES = {
Sync_MessagePersistenceMapper: Symbol.for('Sync_MessagePersistenceMapper'),
Sync_MessageHttpMapper: Symbol.for('Sync_MessageHttpMapper'),
Sync_NotificationHttpMapper: Symbol.for('Sync_NotificationHttpMapper'),
Sync_SQLLegacyItemPersistenceMapper: Symbol.for('Sync_SQLLegacyItemPersistenceMapper'),
Sync_SQLItemPersistenceMapper: Symbol.for('Sync_SQLItemPersistenceMapper'),
Sync_ItemHttpMapper: Symbol.for('Sync_ItemHttpMapper'),
Sync_ItemHashHttpMapper: Symbol.for('Sync_ItemHashHttpMapper'),

View file

@ -1,9 +1,121 @@
import { Column, Entity, Index } from 'typeorm'
import { SQLLegacyItem } from './SQLLegacyItem'
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'items' })
export class SQLItem extends SQLLegacyItem {
@Index('index_items_on_user_uuid_and_content_type', ['userUuid', 'contentType'])
@Index('user_uuid_and_updated_at_timestamp_and_created_at_timestamp', [
'userUuid',
'updatedAtTimestamp',
'createdAtTimestamp',
])
@Index('user_uuid_and_deleted', ['userUuid', 'deleted'])
export class SQLItem {
@PrimaryGeneratedColumn('uuid')
declare uuid: string
@Column({
type: 'varchar',
name: 'duplicate_of',
length: 36,
nullable: true,
})
declare duplicateOf: string | null
@Column({
type: 'varchar',
name: 'items_key_id',
length: 255,
nullable: true,
})
declare itemsKeyId: string | null
@Column({
type: 'text',
nullable: true,
})
declare content: string | null
@Column({
name: 'content_type',
type: 'varchar',
length: 255,
nullable: true,
})
@Index('index_items_on_content_type')
declare contentType: string | null
@Column({
name: 'content_size',
type: 'int',
nullable: true,
})
declare contentSize: number | null
@Column({
name: 'enc_item_key',
type: 'text',
nullable: true,
})
declare encItemKey: string | null
@Column({
name: 'auth_hash',
type: 'varchar',
length: 255,
nullable: true,
})
declare authHash: string | null
@Column({
name: 'user_uuid',
length: 36,
})
@Index('index_items_on_user_uuid')
declare userUuid: string
@Column({
type: 'tinyint',
precision: 1,
nullable: true,
default: 0,
})
@Index('index_items_on_deleted')
declare deleted: boolean
@Column({
name: 'created_at',
type: 'datetime',
precision: 6,
})
declare createdAt: Date
@Column({
name: 'updated_at',
type: 'datetime',
precision: 6,
})
declare updatedAt: Date
@Column({
name: 'created_at_timestamp',
type: 'bigint',
})
declare createdAtTimestamp: number
@Column({
name: 'updated_at_timestamp',
type: 'bigint',
})
@Index('updated_at_timestamp')
declare updatedAtTimestamp: number
@Column({
name: 'updated_with_session',
type: 'varchar',
length: 36,
nullable: true,
})
declare updatedWithSession: string | null
@Column({
type: 'varchar',
name: 'last_edited_by',

View file

@ -3,20 +3,20 @@ import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { Item } from '../../Domain/Item/Item'
import { SQLItem } from './SQLItem'
import { SQLLegacyItemRepository } from './SQLLegacyItemRepository'
import { ItemQuery } from '../../Domain/Item/ItemQuery'
import { ItemRepositoryInterface } from '../../Domain/Item/ItemRepositoryInterface'
import { ExtendedIntegrityPayload } from '../../Domain/Item/ExtendedIntegrityPayload'
import { ItemContentSizeDescriptor } from '../../Domain/Item/ItemContentSizeDescriptor'
import { SQLItem } from './SQLItem'
export class SQLItemRepository extends SQLLegacyItemRepository {
export class SQLItemRepository implements ItemRepositoryInterface {
constructor(
protected override ormRepository: Repository<SQLItem>,
protected override mapper: MapperInterface<Item, SQLItem>,
protected override logger: Logger,
) {
super(ormRepository, mapper, logger)
}
protected ormRepository: Repository<SQLItem>,
protected mapper: MapperInterface<Item, SQLItem>,
protected logger: Logger,
) {}
override async deleteByUserUuidInSharedVaults(userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<void> {
async deleteByUserUuidInSharedVaults(userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<void> {
await this.ormRepository
.createQueryBuilder('item')
.delete()
@ -28,7 +28,7 @@ export class SQLItemRepository extends SQLLegacyItemRepository {
.execute()
}
override async deleteByUserUuidAndNotInSharedVault(userUuid: Uuid): Promise<void> {
async deleteByUserUuidAndNotInSharedVault(userUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder('item')
.delete()
@ -38,25 +38,21 @@ export class SQLItemRepository extends SQLLegacyItemRepository {
.execute()
}
override async updateSharedVaultOwner(dto: {
sharedVaultUuid: Uuid
fromOwnerUuid: Uuid
toOwnerUuid: Uuid
}): Promise<void> {
async updateSharedVaultOwner(dto: { sharedVaultUuid: Uuid; fromOwnerUuid: Uuid; toOwnerUuid: Uuid }): Promise<void> {
await this.ormRepository
.createQueryBuilder('item')
.update()
.set({
userUuid: dto.toOwnerUuid.value,
})
.where('shared_vault_uuid = :sharedVaultUuid AND user_uuid = :fromOwnerUuid', {
sharedVaultUuid: dto.sharedVaultUuid.value,
.where('user_uuid = :fromOwnerUuid AND shared_vault_uuid = :sharedVaultUuid', {
fromOwnerUuid: dto.fromOwnerUuid.value,
sharedVaultUuid: dto.sharedVaultUuid.value,
})
.execute()
}
override async unassignFromSharedVault(sharedVaultUuid: Uuid): Promise<void> {
async unassignFromSharedVault(sharedVaultUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder('item')
.update()
@ -69,7 +65,186 @@ export class SQLItemRepository extends SQLLegacyItemRepository {
.execute()
}
protected override createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder<SQLItem> {
async removeByUuid(uuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder('item')
.delete()
.from('items')
.where('uuid = :uuid', { uuid: uuid.value })
.execute()
}
async insert(item: Item): Promise<void> {
const projection = this.mapper.toProjection(item)
await this.ormRepository.insert(projection)
}
async update(item: Item): Promise<void> {
const projection = this.mapper.toProjection(item)
const { uuid, ...updateValues } = projection
await this.ormRepository.update({ uuid: uuid }, updateValues)
}
async remove(item: Item): Promise<void> {
await this.ormRepository.remove(this.mapper.toProjection(item))
}
async updateContentSize(itemUuid: string, contentSize: number): Promise<void> {
await this.ormRepository
.createQueryBuilder('item')
.update()
.set({
contentSize,
})
.where('uuid = :itemUuid', {
itemUuid,
})
.execute()
}
async findContentSizeForComputingTransferLimit(query: ItemQuery): Promise<ItemContentSizeDescriptor[]> {
const queryBuilder = this.createFindAllQueryBuilder(query)
queryBuilder.select('item.uuid', 'uuid')
queryBuilder.addSelect('item.content_size', 'contentSize')
const items = await queryBuilder.getRawMany()
const itemContentSizeDescriptors: ItemContentSizeDescriptor[] = []
for (const item of items) {
const ItemContentSizeDescriptorOrError = ItemContentSizeDescriptor.create(item.uuid, item.contentSize)
if (ItemContentSizeDescriptorOrError.isFailed()) {
this.logger.error(
`Failed to create ItemContentSizeDescriptor for item ${
item.uuid
}: ${ItemContentSizeDescriptorOrError.getError()}`,
)
continue
}
itemContentSizeDescriptors.push(ItemContentSizeDescriptorOrError.getValue())
}
return itemContentSizeDescriptors
}
async findByUuid(uuid: Uuid): Promise<Item | null> {
const persistence = await this.ormRepository
.createQueryBuilder('item')
.where('item.uuid = :uuid', {
uuid: uuid.value,
})
.getOne()
if (persistence === null) {
return null
}
try {
const item = this.mapper.toDomain(persistence)
return item
} catch (error) {
this.logger.error(
`Failed to map item ${uuid.value} for user ${persistence.userUuid} by uuid: ${(error as Error).message}`,
)
return null
}
}
async findDatesForComputingIntegrityHash(userUuid: string): Promise<Array<{ updated_at_timestamp: number }>> {
const queryBuilder = this.ormRepository.createQueryBuilder('item')
queryBuilder.select('item.updated_at_timestamp')
queryBuilder.where('item.user_uuid = :userUuid', { userUuid: userUuid })
queryBuilder.andWhere('item.deleted = :deleted', { deleted: false })
const items = await queryBuilder.getRawMany()
return items.sort((itemA, itemB) => itemB.updated_at_timestamp - itemA.updated_at_timestamp)
}
async findItemsForComputingIntegrityPayloads(userUuid: string): Promise<ExtendedIntegrityPayload[]> {
const queryBuilder = this.ormRepository.createQueryBuilder('item')
queryBuilder.select('item.uuid', 'uuid')
queryBuilder.addSelect('item.updated_at_timestamp', 'updated_at_timestamp')
queryBuilder.addSelect('item.content_type', 'content_type')
queryBuilder.where('item.user_uuid = :userUuid', { userUuid: userUuid })
queryBuilder.andWhere('item.deleted = :deleted', { deleted: false })
const items = await queryBuilder.getRawMany()
return items.sort((itemA, itemB) => itemB.updated_at_timestamp - itemA.updated_at_timestamp)
}
async findByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Item | null> {
const persistence = await this.ormRepository
.createQueryBuilder('item')
.where('item.uuid = :uuid AND item.user_uuid = :userUuid', {
uuid,
userUuid,
})
.getOne()
if (persistence === null) {
return null
}
try {
const item = this.mapper.toDomain(persistence)
return item
} catch (error) {
this.logger.error(
`Failed to map item ${uuid} for user ${persistence.userUuid} by uuid and userUuid: ${(error as Error).message}`,
)
return null
}
}
async findAll(query: ItemQuery): Promise<Item[]> {
const persistence = await this.createFindAllQueryBuilder(query).getMany()
const domainItems: Item[] = []
for (const persistencItem of persistence) {
try {
domainItems.push(this.mapper.toDomain(persistencItem))
} catch (error) {
this.logger.error(
`Failed to map item ${persistencItem.uuid} for user ${persistencItem.userUuid} to domain: ${
(error as Error).message
}`,
)
}
}
return domainItems
}
async countAll(query: ItemQuery): Promise<number> {
return this.createFindAllQueryBuilder(query).getCount()
}
async markItemsAsDeleted(itemUuids: Array<string>, updatedAtTimestamp: number): Promise<void> {
await this.ormRepository
.createQueryBuilder('item')
.update()
.set({
deleted: true,
content: null,
encItemKey: null,
authHash: null,
updatedAtTimestamp,
})
.where('uuid IN (:...uuids)', {
uuids: itemUuids,
})
.execute()
}
protected createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder<SQLItem> {
const queryBuilder = this.ormRepository.createQueryBuilder('item')
if (query.sortBy !== undefined && query.sortOrder !== undefined) {

View file

@ -1,118 +0,0 @@
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'items' })
@Index('index_items_on_user_uuid_and_content_type', ['userUuid', 'contentType'])
@Index('user_uuid_and_updated_at_timestamp_and_created_at_timestamp', [
'userUuid',
'updatedAtTimestamp',
'createdAtTimestamp',
])
@Index('user_uuid_and_deleted', ['userUuid', 'deleted'])
export class SQLLegacyItem {
@PrimaryGeneratedColumn('uuid')
declare uuid: string
@Column({
type: 'varchar',
name: 'duplicate_of',
length: 36,
nullable: true,
})
declare duplicateOf: string | null
@Column({
type: 'varchar',
name: 'items_key_id',
length: 255,
nullable: true,
})
declare itemsKeyId: string | null
@Column({
type: 'text',
nullable: true,
})
declare content: string | null
@Column({
name: 'content_type',
type: 'varchar',
length: 255,
nullable: true,
})
@Index('index_items_on_content_type')
declare contentType: string | null
@Column({
name: 'content_size',
type: 'int',
nullable: true,
})
declare contentSize: number | null
@Column({
name: 'enc_item_key',
type: 'text',
nullable: true,
})
declare encItemKey: string | null
@Column({
name: 'auth_hash',
type: 'varchar',
length: 255,
nullable: true,
})
declare authHash: string | null
@Column({
name: 'user_uuid',
length: 36,
})
@Index('index_items_on_user_uuid')
declare userUuid: string
@Column({
type: 'tinyint',
precision: 1,
nullable: true,
default: 0,
})
@Index('index_items_on_deleted')
declare deleted: boolean
@Column({
name: 'created_at',
type: 'datetime',
precision: 6,
})
declare createdAt: Date
@Column({
name: 'updated_at',
type: 'datetime',
precision: 6,
})
declare updatedAt: Date
@Column({
name: 'created_at_timestamp',
type: 'bigint',
})
declare createdAtTimestamp: number
@Column({
name: 'updated_at_timestamp',
type: 'bigint',
})
@Index('updated_at_timestamp')
declare updatedAtTimestamp: number
@Column({
name: 'updated_with_session',
type: 'varchar',
length: 36,
nullable: true,
})
declare updatedWithSession: string | null
}

View file

@ -1,263 +0,0 @@
import { Repository, SelectQueryBuilder } from 'typeorm'
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { Item } from '../../Domain/Item/Item'
import { ItemQuery } from '../../Domain/Item/ItemQuery'
import { ItemRepositoryInterface } from '../../Domain/Item/ItemRepositoryInterface'
import { ExtendedIntegrityPayload } from '../../Domain/Item/ExtendedIntegrityPayload'
import { SQLLegacyItem } from './SQLLegacyItem'
import { ItemContentSizeDescriptor } from '../../Domain/Item/ItemContentSizeDescriptor'
export class SQLLegacyItemRepository implements ItemRepositoryInterface {
constructor(
protected ormRepository: Repository<SQLLegacyItem>,
protected mapper: MapperInterface<Item, SQLLegacyItem>,
protected logger: Logger,
) {}
async deleteByUserUuidInSharedVaults(_userUuid: Uuid, _sharedVaultUuids: Uuid[]): Promise<void> {
this.logger.error('Method deleteByUserUuidInSharedVaults not supported.')
}
async updateSharedVaultOwner(_dto: { sharedVaultUuid: Uuid; fromOwnerUuid: Uuid; toOwnerUuid: Uuid }): Promise<void> {
this.logger.error('Method updateSharedVaultOwner not supported.')
}
async unassignFromSharedVault(_sharedVaultUuid: Uuid): Promise<void> {
this.logger.error('Method unassignFromSharedVault not supported.')
}
async removeByUuid(uuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder('item')
.delete()
.from('items')
.where('uuid = :uuid', { uuid: uuid.value })
.execute()
}
async insert(item: Item): Promise<void> {
const projection = this.mapper.toProjection(item)
await this.ormRepository.insert(projection)
}
async update(item: Item): Promise<void> {
const projection = this.mapper.toProjection(item)
const { uuid, ...updateValues } = projection
await this.ormRepository.update({ uuid: uuid }, updateValues)
}
async remove(item: Item): Promise<void> {
await this.ormRepository.remove(this.mapper.toProjection(item))
}
async updateContentSize(itemUuid: string, contentSize: number): Promise<void> {
await this.ormRepository
.createQueryBuilder('item')
.update()
.set({
contentSize,
})
.where('uuid = :itemUuid', {
itemUuid,
})
.execute()
}
async findContentSizeForComputingTransferLimit(query: ItemQuery): Promise<ItemContentSizeDescriptor[]> {
const queryBuilder = this.createFindAllQueryBuilder(query)
queryBuilder.select('item.uuid', 'uuid')
queryBuilder.addSelect('item.content_size', 'contentSize')
const items = await queryBuilder.getRawMany()
const itemContentSizeDescriptors: ItemContentSizeDescriptor[] = []
for (const item of items) {
const ItemContentSizeDescriptorOrError = ItemContentSizeDescriptor.create(item.uuid, item.contentSize)
if (ItemContentSizeDescriptorOrError.isFailed()) {
this.logger.error(
`Failed to create ItemContentSizeDescriptor for item ${
item.uuid
}: ${ItemContentSizeDescriptorOrError.getError()}`,
)
continue
}
itemContentSizeDescriptors.push(ItemContentSizeDescriptorOrError.getValue())
}
return itemContentSizeDescriptors
}
async deleteByUserUuidAndNotInSharedVault(userUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder('item')
.delete()
.from('items')
.where('user_uuid = :userUuid', { userUuid: userUuid.value })
.execute()
}
async findByUuid(uuid: Uuid): Promise<Item | null> {
const persistence = await this.ormRepository
.createQueryBuilder('item')
.where('item.uuid = :uuid', {
uuid: uuid.value,
})
.getOne()
if (persistence === null) {
return null
}
try {
const item = this.mapper.toDomain(persistence)
return item
} catch (error) {
this.logger.error(
`Failed to map item ${uuid.value} for user ${persistence.userUuid} by uuid: ${(error as Error).message}`,
)
return null
}
}
async findDatesForComputingIntegrityHash(userUuid: string): Promise<Array<{ updated_at_timestamp: number }>> {
const queryBuilder = this.ormRepository.createQueryBuilder('item')
queryBuilder.select('item.updated_at_timestamp')
queryBuilder.where('item.user_uuid = :userUuid', { userUuid: userUuid })
queryBuilder.andWhere('item.deleted = :deleted', { deleted: false })
const items = await queryBuilder.getRawMany()
return items.sort((itemA, itemB) => itemB.updated_at_timestamp - itemA.updated_at_timestamp)
}
async findItemsForComputingIntegrityPayloads(userUuid: string): Promise<ExtendedIntegrityPayload[]> {
const queryBuilder = this.ormRepository.createQueryBuilder('item')
queryBuilder.select('item.uuid', 'uuid')
queryBuilder.addSelect('item.updated_at_timestamp', 'updated_at_timestamp')
queryBuilder.addSelect('item.content_type', 'content_type')
queryBuilder.where('item.user_uuid = :userUuid', { userUuid: userUuid })
queryBuilder.andWhere('item.deleted = :deleted', { deleted: false })
const items = await queryBuilder.getRawMany()
return items.sort((itemA, itemB) => itemB.updated_at_timestamp - itemA.updated_at_timestamp)
}
async findByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Item | null> {
const persistence = await this.ormRepository
.createQueryBuilder('item')
.where('item.uuid = :uuid AND item.user_uuid = :userUuid', {
uuid,
userUuid,
})
.getOne()
if (persistence === null) {
return null
}
try {
const item = this.mapper.toDomain(persistence)
return item
} catch (error) {
this.logger.error(
`Failed to map item ${uuid} for user ${persistence.userUuid} by uuid and userUuid: ${(error as Error).message}`,
)
return null
}
}
async findAll(query: ItemQuery): Promise<Item[]> {
const persistence = await this.createFindAllQueryBuilder(query).getMany()
const domainItems: Item[] = []
for (const persistencItem of persistence) {
try {
domainItems.push(this.mapper.toDomain(persistencItem))
} catch (error) {
this.logger.error(
`Failed to map item ${persistencItem.uuid} for user ${persistencItem.userUuid} to domain: ${
(error as Error).message
}`,
)
}
}
return domainItems
}
async countAll(query: ItemQuery): Promise<number> {
return this.createFindAllQueryBuilder(query).getCount()
}
async markItemsAsDeleted(itemUuids: Array<string>, updatedAtTimestamp: number): Promise<void> {
await this.ormRepository
.createQueryBuilder('item')
.update()
.set({
deleted: true,
content: null,
encItemKey: null,
authHash: null,
updatedAtTimestamp,
})
.where('uuid IN (:...uuids)', {
uuids: itemUuids,
})
.execute()
}
protected createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder<SQLLegacyItem> {
const queryBuilder = this.ormRepository.createQueryBuilder('item')
if (query.sortBy !== undefined && query.sortOrder !== undefined) {
queryBuilder.orderBy(`item.${query.sortBy}`, query.sortOrder)
}
if (query.userUuid !== undefined) {
queryBuilder.where('item.user_uuid = :userUuid', { userUuid: query.userUuid })
}
if (query.uuids && query.uuids.length > 0) {
queryBuilder.andWhere('item.uuid IN (:...uuids)', { uuids: query.uuids })
}
if (query.deleted !== undefined) {
queryBuilder.andWhere('item.deleted = :deleted', { deleted: query.deleted })
}
if (query.contentType) {
if (Array.isArray(query.contentType)) {
queryBuilder.andWhere('item.content_type IN (:...contentTypes)', { contentTypes: query.contentType })
} else {
queryBuilder.andWhere('item.content_type = :contentType', { contentType: query.contentType })
}
}
if (query.lastSyncTime && query.syncTimeComparison) {
queryBuilder.andWhere(`item.updated_at_timestamp ${query.syncTimeComparison} :lastSyncTime`, {
lastSyncTime: query.lastSyncTime,
})
}
if (query.createdBetween !== undefined) {
queryBuilder.andWhere('item.created_at >= :createdAfter AND item.created_at <= :createdBefore', {
createdAfter: query.createdBetween[0].toISOString(),
createdBefore: query.createdBetween[1].toISOString(),
})
}
if (query.offset !== undefined) {
queryBuilder.skip(query.offset)
}
if (query.limit !== undefined) {
queryBuilder.take(query.limit)
}
return queryBuilder
}
}

View file

@ -1,102 +0,0 @@
import { Timestamps, MapperInterface, UniqueEntityId, Uuid, ContentType, Dates } from '@standardnotes/domain-core'
import { Item } from '../../Domain/Item/Item'
import { SQLLegacyItem } from '../../Infra/TypeORM/SQLLegacyItem'
export class SQLLegacyItemPersistenceMapper implements MapperInterface<Item, SQLLegacyItem> {
toDomain(projection: SQLLegacyItem): Item {
const uuidOrError = Uuid.create(projection.uuid)
if (uuidOrError.isFailed()) {
throw new Error(`Failed to create item from projection: ${uuidOrError.getError()}`)
}
const uuid = uuidOrError.getValue()
let duplicateOf = null
if (projection.duplicateOf) {
const duplicateOfOrError = Uuid.create(projection.duplicateOf)
if (duplicateOfOrError.isFailed()) {
throw new Error(`Failed to create item from projection: ${duplicateOfOrError.getError()}`)
}
duplicateOf = duplicateOfOrError.getValue()
}
const contentTypeOrError = ContentType.create(projection.contentType)
if (contentTypeOrError.isFailed()) {
throw new Error(`Failed to create item from projection: ${contentTypeOrError.getError()}`)
}
const contentType = contentTypeOrError.getValue()
const userUuidOrError = Uuid.create(projection.userUuid)
if (userUuidOrError.isFailed()) {
throw new Error(`Failed to create item from projection: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const datesOrError = Dates.create(projection.createdAt, projection.updatedAt)
if (datesOrError.isFailed()) {
throw new Error(`Failed to create item from projection: ${datesOrError.getError()}`)
}
const dates = datesOrError.getValue()
const timestampsOrError = Timestamps.create(projection.createdAtTimestamp, projection.updatedAtTimestamp)
if (timestampsOrError.isFailed()) {
throw new Error(`Failed to create item from projection: ${timestampsOrError.getError()}`)
}
const timestamps = timestampsOrError.getValue()
let updatedWithSession = null
if (projection.updatedWithSession) {
const updatedWithSessionOrError = Uuid.create(projection.updatedWithSession)
if (updatedWithSessionOrError.isFailed()) {
throw new Error(`Failed to create item from projection: ${updatedWithSessionOrError.getError()}`)
}
updatedWithSession = updatedWithSessionOrError.getValue()
}
const itemOrError = Item.create(
{
duplicateOf,
itemsKeyId: projection.itemsKeyId,
content: projection.content,
contentType,
contentSize: projection.contentSize ?? undefined,
encItemKey: projection.encItemKey,
authHash: projection.authHash,
userUuid,
deleted: !!projection.deleted,
dates,
timestamps,
updatedWithSession,
},
new UniqueEntityId(uuid.value),
)
if (itemOrError.isFailed()) {
throw new Error(`Failed to create item from projection: ${itemOrError.getError()}`)
}
return itemOrError.getValue()
}
toProjection(domain: Item): SQLLegacyItem {
const typeorm = new SQLLegacyItem()
typeorm.uuid = domain.id.toString()
typeorm.duplicateOf = domain.props.duplicateOf ? domain.props.duplicateOf.value : null
typeorm.itemsKeyId = domain.props.itemsKeyId
typeorm.content = domain.props.content
typeorm.contentType = domain.props.contentType.value
typeorm.contentSize = domain.props.contentSize ?? null
typeorm.encItemKey = domain.props.encItemKey
typeorm.authHash = domain.props.authHash
typeorm.userUuid = domain.props.userUuid.value
typeorm.deleted = !!domain.props.deleted
typeorm.createdAt = domain.props.dates.createdAt
typeorm.updatedAt = domain.props.dates.updatedAt
typeorm.createdAtTimestamp = domain.props.timestamps.createdAt
typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt
typeorm.updatedWithSession = domain.props.updatedWithSession ? domain.props.updatedWithSession.value : null
return typeorm
}
}