From e8fe9af1ae7fb8a12f1ed1cb76dcb307fe4bbcab Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Mon, 11 Nov 2024 23:26:06 +0300 Subject: [PATCH 01/13] refactor: make object parameter in getItem --- .../backend/src/services/base/base-agg.repository.ts | 12 ++++++------ .../src/services/base/base-local.repository.ts | 10 +++++----- libs/backend/src/services/base/base.repository.ts | 8 ++++---- .../src/services/base/repository.interface.ts | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/libs/backend/src/services/base/base-agg.repository.ts b/libs/backend/src/services/base/base-agg.repository.ts index d964e49b..f5d65710 100644 --- a/libs/backend/src/services/base/base-agg.repository.ts +++ b/libs/backend/src/services/base/base-agg.repository.ts @@ -9,16 +9,16 @@ export class BaseAggRepository implements IRepository { private local: IRepository ) {} - async getItem(id: EntityId, source?: EntitySourceType): Promise { + async getItem({ id, source }: { id: EntityId; source?: EntitySourceType }): Promise { if (source === EntitySourceType.Local) { - return this.local.getItem(id) + return this.local.getItem({ id }) } else if (source === EntitySourceType.Origin) { - return this.remote.getItem(id) + return this.remote.getItem({ id }) } else { // ToDo: why local is preferred? - const localItem = await this.local.getItem(id) + const localItem = await this.local.getItem({ id }) if (localItem) return localItem - return this.remote.getItem(id) + return this.remote.getItem({ id }) } } @@ -89,7 +89,7 @@ export class BaseAggRepository implements IRepository { } private async _deleteLocalItemIfExist(id: EntityId) { - if (await this.local.getItem(id)) { + if (await this.local.getItem({ id })) { await this.local.deleteItem(id) } } diff --git a/libs/backend/src/services/base/base-local.repository.ts b/libs/backend/src/services/base/base-local.repository.ts index 7d214285..6f44d5ea 100644 --- a/libs/backend/src/services/base/base-local.repository.ts +++ b/libs/backend/src/services/base/base-local.repository.ts @@ -17,7 +17,7 @@ export class BaseLocalRepository implements IRepository { this._entityKey = getEntity(EntityType).name } - async getItem(id: EntityId): Promise { + async getItem({ id }: { id: EntityId }): Promise { const parsedId = BaseLocalRepository._parseGlobalId(id) if (!parsedId) return null @@ -48,7 +48,7 @@ export class BaseLocalRepository implements IRepository { return true }) - const items = await Promise.all(filteredKeys.map((id) => this.getItem(id))) + const items = await Promise.all(filteredKeys.map((id) => this.getItem({ id }))) return items.filter((x) => x !== null) } @@ -62,12 +62,12 @@ export class BaseLocalRepository implements IRepository { } return true }) - + return filteredItems } async createItem(item: T): Promise { - if (await this.getItem(item.id)) { + if (await this.getItem({ id: item.id })) { throw new Error('Item with that ID already exists') } @@ -79,7 +79,7 @@ export class BaseLocalRepository implements IRepository { } async editItem(item: T): Promise { - if (!(await this.getItem(item.id))) { + if (!(await this.getItem({ id: item.id }))) { throw new Error('Item with that ID does not exist') } diff --git a/libs/backend/src/services/base/base.repository.ts b/libs/backend/src/services/base/base.repository.ts index 3db5d374..bf27f1f3 100644 --- a/libs/backend/src/services/base/base.repository.ts +++ b/libs/backend/src/services/base/base.repository.ts @@ -32,7 +32,7 @@ export class BaseRepository implements IRepository { this._entityKey = getEntity(EntityType).name } - async getItem(id: EntityId): Promise { + async getItem({ id }: { id: EntityId }): Promise { const { authorId, localId } = this._parseGlobalId(id) if (authorId === WildcardKey || localId === WildcardKey) { @@ -125,7 +125,7 @@ export class BaseRepository implements IRepository { return [authorId, this._entityKey, localId].join(KeyDelimiter) }) - const documents = await Promise.all(documentIds.map((id) => this.getItem(id))).then( + const documents = await Promise.all(documentIds.map((id) => this.getItem({ id }))).then( (documents) => documents.filter((x) => x !== null) ) @@ -133,7 +133,7 @@ export class BaseRepository implements IRepository { } async createItem(item: T, tx?: Transaction): Promise { - if (await this.getItem(item.id)) { + if (await this.getItem({ id: item.id })) { throw new Error('Item with that ID already exists') } @@ -141,7 +141,7 @@ export class BaseRepository implements IRepository { } async editItem(item: T, tx?: Transaction): Promise { - if (!(await this.getItem(item.id))) { + if (!(await this.getItem({ id: item.id }))) { throw new Error('Item with that ID does not exist') } diff --git a/libs/backend/src/services/base/repository.interface.ts b/libs/backend/src/services/base/repository.interface.ts index f1020094..2619fe4e 100644 --- a/libs/backend/src/services/base/repository.interface.ts +++ b/libs/backend/src/services/base/repository.interface.ts @@ -3,7 +3,7 @@ import { Transaction } from '../unit-of-work/transaction' import { Base, EntityId, EntitySourceType } from './base.entity' export interface IRepository { - getItem(id: EntityId, source?: EntitySourceType): Promise + getItem(options: { id: EntityId; source?: EntitySourceType }): Promise getItems(options?: { authorId?: string; localId?: string }): Promise getItemsByIndex(entity: Partial): Promise createItem(item: T, tx?: Transaction): Promise From 320f1778664b60a0551c4e0d646bb2527a28b0c1 Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Tue, 12 Nov 2024 16:55:39 +0300 Subject: [PATCH 02/13] refactor: make object parameter in getItem --- .../src/services/application/application.service.ts | 2 +- libs/backend/src/services/document/document.service.ts | 7 +++++-- libs/backend/src/services/link-db/link-db.service.ts | 2 +- libs/backend/src/services/mutation/mutation.service.ts | 10 +++++----- .../src/services/notification/notification.service.ts | 6 +++--- .../services/parser-config/parser-config.service.ts | 2 +- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/libs/backend/src/services/application/application.service.ts b/libs/backend/src/services/application/application.service.ts index c671e980..5fbec878 100644 --- a/libs/backend/src/services/application/application.service.ts +++ b/libs/backend/src/services/application/application.service.ts @@ -29,7 +29,7 @@ export class ApplicationService { } public async getApplication(appId: AppId): Promise { - const app = await this.applicationRepository.getItem(appId) + const app = await this.applicationRepository.getItem({ id: appId }) return app?.toDto() ?? null } diff --git a/libs/backend/src/services/document/document.service.ts b/libs/backend/src/services/document/document.service.ts index fd3486cc..fd032426 100644 --- a/libs/backend/src/services/document/document.service.ts +++ b/libs/backend/src/services/document/document.service.ts @@ -25,7 +25,7 @@ export class DocumentSerivce { globalDocumentId: DocumentId, source?: EntitySourceType ): Promise { - const document = await this.documentRepository.getItem(globalDocumentId, source) + const document = await this.documentRepository.getItem({ id: globalDocumentId, source }) return document?.toDto() ?? null } @@ -288,7 +288,10 @@ export class DocumentSerivce { name: DocumentSerivce._incrementPostfix(documentFork.metadata.name!), }, }) - done = !(await this.documentRepository.getItem(documentFork.id, EntitySourceType.Origin)) + done = !(await this.documentRepository.getItem({ + id: documentFork.id, + source: EntitySourceType.Origin, + })) } return documentFork diff --git a/libs/backend/src/services/link-db/link-db.service.ts b/libs/backend/src/services/link-db/link-db.service.ts index bd8fb0a8..66102d0a 100644 --- a/libs/backend/src/services/link-db/link-db.service.ts +++ b/libs/backend/src/services/link-db/link-db.service.ts @@ -77,7 +77,7 @@ export class LinkDbService { // ToDo: too much data will be retrieved here, becuase it created by users const ctxLinksNullPossible = await Promise.all( - ctxLinkIds.map((id) => this._linkDbRepository.getItem(id)) + ctxLinkIds.map((id) => this._linkDbRepository.getItem({ id })) ) ctxLinks = ctxLinksNullPossible.filter((x) => x !== null) } diff --git a/libs/backend/src/services/mutation/mutation.service.ts b/libs/backend/src/services/mutation/mutation.service.ts index 08e0e448..031be2c9 100644 --- a/libs/backend/src/services/mutation/mutation.service.ts +++ b/libs/backend/src/services/mutation/mutation.service.ts @@ -31,7 +31,7 @@ export class MutationService { ) {} async getMutation(mutationId: string): Promise { - const mutation = await this.mutationRepository.getItem(mutationId) + const mutation = await this.mutationRepository.getItem({ id: mutationId }) return mutation?.toDto() ?? null } @@ -137,7 +137,7 @@ export class MutationService { const mutation = await this._fixMutationErrors(Mutation.create(dto)) // ToDo: move to provider? - if (!(await this.mutationRepository.getItem(mutation.id))) { + if (!(await this.mutationRepository.getItem({ id: mutation.id }))) { throw new Error('Mutation with that ID does not exist') } @@ -218,7 +218,7 @@ export class MutationService { const { sourceMutationId, targetMutationId } = notification.payload as PullRequestPayload - const sourceMutation = await this.mutationRepository.getItem(sourceMutationId) + const sourceMutation = await this.mutationRepository.getItem({ id: sourceMutationId }) if (!sourceMutation) { throw new Error('Source mutation not found') @@ -270,7 +270,7 @@ export class MutationService { throw new Error('The mutation is not a fork and does not have an origin to apply changes to') } - const originalMutation = await this.mutationRepository.getItem(originalMutationId) + const originalMutation = await this.mutationRepository.getItem({ id: originalMutationId }) if (!originalMutation) { throw new Error('The origin mutation does not exist') @@ -293,7 +293,7 @@ export class MutationService { throw new Error('The mutation is not a fork and does not have an origin to apply changes to') } - const originalMutation = await this.mutationRepository.getItem(originalMutationId) + const originalMutation = await this.mutationRepository.getItem({ id: originalMutationId }) if (!originalMutation) { throw new Error('The origin mutation does not exist') diff --git a/libs/backend/src/services/notification/notification.service.ts b/libs/backend/src/services/notification/notification.service.ts index 2c28dd22..fcd1b2b1 100644 --- a/libs/backend/src/services/notification/notification.service.ts +++ b/libs/backend/src/services/notification/notification.service.ts @@ -21,7 +21,7 @@ export class NotificationService { // ToDo: return dto async getNotification(notificationId: string): Promise { - return this.notificationRepository.getItem(notificationId) + return this.notificationRepository.getItem({ id: notificationId }) } async createNotification(dto: NotificationCreateDto, tx?: Transaction): Promise { @@ -164,7 +164,7 @@ export class NotificationService { callback: (resolution: Resolution, notification: Notification) => void, tx?: Transaction ) { - const notification = await this.notificationRepository.getItem(notificationId) + const notification = await this.notificationRepository.getItem({ id: notificationId }) if (!notification) { throw new Error('Notification not found') @@ -210,7 +210,7 @@ export class NotificationService { const hash = UserLinkService._hashString(notificationId) const resolutionId = `${accountId}/resolution/${hash}` - const resolution = await this.resolutionRepository.getItem(resolutionId) + const resolution = await this.resolutionRepository.getItem({ id: resolutionId }) if (resolution) return resolution diff --git a/libs/backend/src/services/parser-config/parser-config.service.ts b/libs/backend/src/services/parser-config/parser-config.service.ts index cb2ba2c1..bd1fc183 100644 --- a/libs/backend/src/services/parser-config/parser-config.service.ts +++ b/libs/backend/src/services/parser-config/parser-config.service.ts @@ -10,7 +10,7 @@ export class ParserConfigService { if (parserId === 'mweb') return null if (parserId === 'engine') return null - return this.parserConfigRepository.getItem(parserId) + return this.parserConfigRepository.getItem({ id: parserId }) } public async getAllParserConfigs(): Promise { From 38dd5a55ce5eeb4f4b56eeb0264f297ce7592d99 Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Wed, 13 Nov 2024 14:43:16 +0300 Subject: [PATCH 03/13] feat: getting versioning data --- .../src/services/base/base-agg.repository.ts | 62 ++++++- .../services/base/base-local.repository.ts | 14 +- libs/backend/src/services/base/base.entity.ts | 1 + .../src/services/base/base.repository.ts | 157 +++++++++++++++--- .../src/services/base/decorators/column.ts | 1 + .../src/services/base/decorators/entity.ts | 2 +- .../src/services/base/repository.interface.ts | 9 +- .../src/services/mutation/mutation.entity.ts | 10 +- .../services/user-link/user-link.entity.ts | 2 +- 9 files changed, 223 insertions(+), 35 deletions(-) diff --git a/libs/backend/src/services/base/base-agg.repository.ts b/libs/backend/src/services/base/base-agg.repository.ts index f5d65710..21b4314b 100644 --- a/libs/backend/src/services/base/base-agg.repository.ts +++ b/libs/backend/src/services/base/base-agg.repository.ts @@ -9,16 +9,24 @@ export class BaseAggRepository implements IRepository { private local: IRepository ) {} - async getItem({ id, source }: { id: EntityId; source?: EntitySourceType }): Promise { + async getItem({ + id, + source, + version, + }: { + id: EntityId + source?: EntitySourceType + version?: string + }): Promise { if (source === EntitySourceType.Local) { - return this.local.getItem({ id }) + return this.local.getItem({ id, version }) } else if (source === EntitySourceType.Origin) { - return this.remote.getItem({ id }) + return this.remote.getItem({ id, version }) } else { // ToDo: why local is preferred? - const localItem = await this.local.getItem({ id }) + const localItem = await this.local.getItem({ id, version }) if (localItem) return localItem - return this.remote.getItem({ id }) + return this.remote.getItem({ id, version }) } } @@ -88,6 +96,50 @@ export class BaseAggRepository implements IRepository { } } + async getTagValue({ + id, + source, + tag, + }: { + id: EntityId + source?: EntitySourceType + tag: string + }): Promise { + if (source === EntitySourceType.Local) { + return this.local.getTagValue({ id, tag }) + } else if (source === EntitySourceType.Origin) { + return this.remote.getTagValue({ id, tag }) + } else { + throw new Error('Invalid source') + } + } + + async getTags({ id, source }: { id: EntityId; source?: EntitySourceType }): Promise { + if (source === EntitySourceType.Local) { + return this.local.getTags({ id }) + } else if (source === EntitySourceType.Origin) { + return this.remote.getTags({ id }) + } else { + throw new Error('Invalid source') + } + } + + async getVersions({ + id, + source, + }: { + id: EntityId + source?: EntitySourceType + }): Promise { + if (source === EntitySourceType.Local) { + return this.local.getVersions({ id }) + } else if (source === EntitySourceType.Origin) { + return this.remote.getVersions({ id }) + } else { + throw new Error('Invalid source') + } + } + private async _deleteLocalItemIfExist(id: EntityId) { if (await this.local.getItem({ id })) { await this.local.deleteItem(id) diff --git a/libs/backend/src/services/base/base-local.repository.ts b/libs/backend/src/services/base/base-local.repository.ts index 6f44d5ea..82d88194 100644 --- a/libs/backend/src/services/base/base-local.repository.ts +++ b/libs/backend/src/services/base/base-local.repository.ts @@ -17,7 +17,7 @@ export class BaseLocalRepository implements IRepository { this._entityKey = getEntity(EntityType).name } - async getItem({ id }: { id: EntityId }): Promise { + async getItem({ id, version }: { id: EntityId; version?: string }): Promise { const parsedId = BaseLocalRepository._parseGlobalId(id) if (!parsedId) return null @@ -129,6 +129,18 @@ export class BaseLocalRepository implements IRepository { return entity } + async getVersions(options: { id: EntityId }): Promise { + throw new Error('Method not implemented.') + } + + async getTagValue(options: { id: EntityId; tag: string }): Promise { + throw new Error('Method not implemented.') + } + + async getTags(options: { id: EntityId }): Promise { + throw new Error('Method not implemented.') + } + private static _parseGlobalId(globalId: EntityId): { authorId: string type: string diff --git a/libs/backend/src/services/base/base.entity.ts b/libs/backend/src/services/base/base.entity.ts index 6b6d8303..f5caa6c8 100644 --- a/libs/backend/src/services/base/base.entity.ts +++ b/libs/backend/src/services/base/base.entity.ts @@ -15,6 +15,7 @@ export class Base { source: EntitySourceType = EntitySourceType.Local blockNumber: number = 0 // ToDo: fake block number timestamp: number = 0 // ToDo: fake timestamp + version: string = '0' // ToDo: fake version? get authorId(): string | null { return this.id.split(KeyDelimiter)[0] ?? null diff --git a/libs/backend/src/services/base/base.repository.ts b/libs/backend/src/services/base/base.repository.ts index bf27f1f3..7e808107 100644 --- a/libs/backend/src/services/base/base.repository.ts +++ b/libs/backend/src/services/base/base.repository.ts @@ -18,38 +18,87 @@ const KeyDelimiter = '/' const EmptyValue = '' const SelfKey = '' const BlockNumberKey = ':block' +const TagsKey = 'tags' +const VersionsKey = 'versions' +const LatestTagName = 'latest' // ToDo: type EntityId = string export class BaseRepository implements IRepository { private _entityKey: string + private _isVersionedEntity: boolean constructor( private EntityType: { new (): T }, public socialDb: SocialDbService ) { - this._entityKey = getEntity(EntityType).name + const { name, versioned } = getEntity(EntityType) + this._entityKey = name + this._isVersionedEntity = versioned ?? false // ToDo: move to the decorator } - async getItem({ id }: { id: EntityId }): Promise { + async getItem({ id, version }: { id: EntityId; version?: string }): Promise { const { authorId, localId } = this._parseGlobalId(id) if (authorId === WildcardKey || localId === WildcardKey) { throw new Error('Wildcard is not supported') } - const keys = [authorId, SettingsKey, ProjectIdKey, this._entityKey, localId] + if (this._isVersionedEntity) { + version = version ?? (await this.getTagValue({ id, tag: LatestTagName })) ?? undefined + + if (!version) return null + } + + const baseKeys = [authorId, SettingsKey, ProjectIdKey, this._entityKey, localId] + + const allKeysForFetching: string[][] = [] + + const columnNames = Object.getOwnPropertyNames(new this.EntityType()) + + for (const columnName of columnNames) { + // ToDo: why prototype? + const column = getColumn(this.EntityType.prototype, columnName) + + if (!column) continue + + const columnKeys = baseKeys.concat( + column.versioned ? [VersionsKey, version!, columnName] : [columnName] + ) + + if (column.type === ColumnType.Set) { + allKeysForFetching.push(columnKeys.concat(RecursiveWildcardKey)) + } else if (column.type === ColumnType.Json) { + allKeysForFetching.push(columnKeys) + } else if (column.type === ColumnType.AsIs) { + // ToDo: introduce new ColumnType? + allKeysForFetching.push(columnKeys) + allKeysForFetching.push(columnKeys.concat(RecursiveWildcardKey)) + } + } + const queryResult = await this.socialDb.get( - [[...keys, RecursiveWildcardKey].join(KeyDelimiter)], + allKeysForFetching.map((keys) => keys.join(KeyDelimiter)), { withBlockHeight: true } ) - const itemWithMeta = SocialDbService.getValueByKey(keys, queryResult) + const nonVersionedData = SocialDbService.getValueByKey(baseKeys, queryResult) + const versionedData = SocialDbService.getValueByKey( + baseKeys.concat(VersionsKey, version!), + queryResult + ) - if (!itemWithMeta) return null + if (!nonVersionedData && !versionedData) return null + + const item = this._makeItemFromSocialDb(id, { + ...nonVersionedData, + [VersionsKey]: undefined, // remove key from nonVersionedData + ...versionedData, + version, + }) - return this._makeItemFromSocialDb(id, itemWithMeta) + return item } async getItems(options?: { authorId?: EntityId; localId?: EntityId }): Promise { @@ -59,20 +108,16 @@ export class BaseRepository implements IRepository { const keys = [authorId, SettingsKey, ProjectIdKey, this._entityKey, localId] // ToDo: out of gas - const queryResult = await this.socialDb.get( - [[...keys, RecursiveWildcardKey].join(KeyDelimiter)], - { withBlockHeight: true } - ) + const fetchedKeys = await this.socialDb.keys([keys.join(KeyDelimiter)]) - const mutationsByKey = SocialDbService.splitObjectByDepth(queryResult, keys.length) - - const items = Object.entries(mutationsByKey).map(([key, itemWithMeta]: [string, any]) => { - const [accountId, , , , localMutationId] = key.split(KeyDelimiter) - const itemId = [accountId, this._entityKey, localMutationId].join(KeyDelimiter) - return this._makeItemFromSocialDb(itemId, itemWithMeta) + const itemKeys = fetchedKeys.map((key) => { + const [authorId, , , , localId] = key.split(KeyDelimiter) + return [authorId, this._entityKey, localId].join(KeyDelimiter) }) - return items + const items = await Promise.all(itemKeys.map((id) => this.getItem({ id }))) + + return items.filter((x) => x !== null) } async getItemsByIndex(entity: Partial): Promise { @@ -88,7 +133,11 @@ export class BaseRepository implements IRepository { throw new Error('Column not found') } - const { name, type, transformer } = column + const { name, type, transformer, versioned } = column + + if (versioned) { + throw new Error('Versioned columns are not supported for indexing') + } if (type !== ColumnType.Set) { throw new Error('Only Set columns can be indexed') @@ -120,16 +169,16 @@ export class BaseRepository implements IRepository { const foundKeys = await this.socialDb.keys([keys.join(KeyDelimiter)]) - const documentIds = foundKeys.map((key: string) => { + const itemIds = foundKeys.map((key: string) => { const [authorId, , , , localId] = key.split(KeyDelimiter) return [authorId, this._entityKey, localId].join(KeyDelimiter) }) - const documents = await Promise.all(documentIds.map((id) => this.getItem({ id }))).then( - (documents) => documents.filter((x) => x !== null) + const items = await Promise.all(itemIds.map((id) => this.getItem({ id }))).then((items) => + items.filter((x) => x !== null) ) - return documents + return items } async createItem(item: T, tx?: Transaction): Promise { @@ -219,6 +268,67 @@ export class BaseRepository implements IRepository { return entity } + async getVersions({ id }: { id: EntityId }): Promise { + const { authorId, localId } = this._parseGlobalId(id) + + if (authorId === WildcardKey || localId === WildcardKey) { + throw new Error('Wildcard is not supported') + } + + const keys = [ + authorId, + SettingsKey, + ProjectIdKey, + this._entityKey, + localId, + VersionsKey, + WildcardKey, + ] + + const foundKeys = await this.socialDb.keys([keys.join(KeyDelimiter)]) + + return foundKeys + } + + async getTagValue({ id, tag }: { id: EntityId; tag: string }): Promise { + const { authorId, localId } = this._parseGlobalId(id) + + if (authorId === WildcardKey || localId === WildcardKey) { + throw new Error('Wildcard is not supported') + } + + const keys = [authorId, SettingsKey, ProjectIdKey, this._entityKey, localId, TagsKey, tag] + const queryResult = await this.socialDb.get([[...keys].join(KeyDelimiter)]) + + const itemWithMeta = SocialDbService.getValueByKey(keys, queryResult) + + if (!itemWithMeta) return null + + return itemWithMeta + } + + async getTags({ id }: { id: EntityId }): Promise { + const { authorId, localId } = this._parseGlobalId(id) + + if (authorId === WildcardKey || localId === WildcardKey) { + throw new Error('Wildcard is not supported') + } + + const keys = [ + authorId, + SettingsKey, + ProjectIdKey, + this._entityKey, + localId, + TagsKey, + WildcardKey, + ] + + const foundKeys = await this.socialDb.keys([keys.join(KeyDelimiter)]) + + return foundKeys + } + private async _commitOrQueue(dataToSave: Value, tx?: Transaction) { if (tx) { tx.queue(dataToSave) @@ -234,6 +344,7 @@ export class BaseRepository implements IRepository { entity.id = id entity.blockNumber = rawWithMeta[BlockNumberKey] entity.source = EntitySourceType.Origin + entity.version = rawWithMeta.version ?? '0' // ToDo: fake version // ToDo: calculate it like localId and authorId? entity.timestamp = this.socialDb.getTimestampByBlockHeight(entity.blockNumber) diff --git a/libs/backend/src/services/base/decorators/column.ts b/libs/backend/src/services/base/decorators/column.ts index 0e6174ce..c6f1d409 100644 --- a/libs/backend/src/services/base/decorators/column.ts +++ b/libs/backend/src/services/base/decorators/column.ts @@ -11,6 +11,7 @@ export enum ColumnType { export type TargetMetadata = { type?: ColumnType name?: string + versioned?: boolean transformer?: { from?: (item: any) => any to?: (item: any) => any diff --git a/libs/backend/src/services/base/decorators/entity.ts b/libs/backend/src/services/base/decorators/entity.ts index a3e9678c..c1feccc4 100644 --- a/libs/backend/src/services/base/decorators/entity.ts +++ b/libs/backend/src/services/base/decorators/entity.ts @@ -2,7 +2,7 @@ import 'reflect-metadata/lite' const Key = Symbol('entity') -export type EntityMetadata = { name: string } +export type EntityMetadata = { name: string; versioned?: boolean } export function Entity(options: EntityMetadata) { return Reflect.metadata(Key, options) diff --git a/libs/backend/src/services/base/repository.interface.ts b/libs/backend/src/services/base/repository.interface.ts index 2619fe4e..26b58f7f 100644 --- a/libs/backend/src/services/base/repository.interface.ts +++ b/libs/backend/src/services/base/repository.interface.ts @@ -3,7 +3,7 @@ import { Transaction } from '../unit-of-work/transaction' import { Base, EntityId, EntitySourceType } from './base.entity' export interface IRepository { - getItem(options: { id: EntityId; source?: EntitySourceType }): Promise + getItem(options: { id: EntityId; source?: EntitySourceType; version?: string }): Promise getItems(options?: { authorId?: string; localId?: string }): Promise getItemsByIndex(entity: Partial): Promise createItem(item: T, tx?: Transaction): Promise @@ -11,4 +11,11 @@ export interface IRepository { saveItem(item: T, tx?: Transaction): Promise deleteItem(id: EntityId, tx?: Transaction): Promise constructItem(item: Omit & { metadata: EntityMetadata }): Promise + getTagValue(options: { + id: EntityId + source?: EntitySourceType + tag: string + }): Promise + getTags(options: { id: EntityId; source?: EntitySourceType }): Promise + getVersions(options: { id: EntityId; source?: EntitySourceType }): Promise } diff --git a/libs/backend/src/services/mutation/mutation.entity.ts b/libs/backend/src/services/mutation/mutation.entity.ts index 6e57720a..70ddf5c4 100644 --- a/libs/backend/src/services/mutation/mutation.entity.ts +++ b/libs/backend/src/services/mutation/mutation.entity.ts @@ -14,15 +14,19 @@ export type AppInMutation = { documentId: DocumentId | null } -@Entity({ name: 'mutation' }) +@Entity({ name: 'mutation', versioned: true }) export class Mutation extends Base { @Column() metadata: EntityMetadata = {} - @Column({ type: ColumnType.Json, transformer: { from: normalizeApps, to: denormalizeApps } }) + @Column({ + type: ColumnType.Json, + versioned: true, + transformer: { from: normalizeApps, to: denormalizeApps }, + }) apps: AppInMutation[] = [] - @Column({ type: ColumnType.Json }) + @Column({ type: ColumnType.Json, versioned: true }) targets: Target[] = [] toDto(): MutationDto { diff --git a/libs/backend/src/services/user-link/user-link.entity.ts b/libs/backend/src/services/user-link/user-link.entity.ts index 9133ee2c..f857d7b3 100644 --- a/libs/backend/src/services/user-link/user-link.entity.ts +++ b/libs/backend/src/services/user-link/user-link.entity.ts @@ -11,7 +11,7 @@ export type LinkIndex = string @Entity({ name: 'link' }) export class IndexedLink extends Base { - @Column({ type: ColumnType.Set }) + @Column({ type: ColumnType.Set, versioned: false }) indexes: string[] = [] } From 99292eb82e7e3d526b3f2568d242ae2a122a205a Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Wed, 13 Nov 2024 15:12:58 +0300 Subject: [PATCH 04/13] feat: provide backward compatibility --- .../src/services/base/base.repository.ts | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/libs/backend/src/services/base/base.repository.ts b/libs/backend/src/services/base/base.repository.ts index 7e808107..6a462b38 100644 --- a/libs/backend/src/services/base/base.repository.ts +++ b/libs/backend/src/services/base/base.repository.ts @@ -47,8 +47,6 @@ export class BaseRepository implements IRepository { if (this._isVersionedEntity) { version = version ?? (await this.getTagValue({ id, tag: LatestTagName })) ?? undefined - - if (!version) return null } const baseKeys = [authorId, SettingsKey, ProjectIdKey, this._entityKey, localId] @@ -63,18 +61,26 @@ export class BaseRepository implements IRepository { if (!column) continue - const columnKeys = baseKeys.concat( - column.versioned ? [VersionsKey, version!, columnName] : [columnName] - ) + const { type, versioned } = column + + // Scalar types should be queried without wildcard + if (type === ColumnType.Json || type === ColumnType.AsIs) { + if (versioned) { + allKeysForFetching.push(baseKeys.concat([VersionsKey, version!, columnName])) + allKeysForFetching.push(baseKeys.concat([columnName])) // backward compatibility + } else { + allKeysForFetching.push(baseKeys.concat([columnName])) + } + } - if (column.type === ColumnType.Set) { - allKeysForFetching.push(columnKeys.concat(RecursiveWildcardKey)) - } else if (column.type === ColumnType.Json) { - allKeysForFetching.push(columnKeys) - } else if (column.type === ColumnType.AsIs) { - // ToDo: introduce new ColumnType? - allKeysForFetching.push(columnKeys) - allKeysForFetching.push(columnKeys.concat(RecursiveWildcardKey)) + // Non-scalar types should be queried with wildcard + if (type === ColumnType.Set || type === ColumnType.AsIs) { + if (versioned) { + allKeysForFetching.push(baseKeys.concat([VersionsKey, version!, columnName, WildcardKey])) + allKeysForFetching.push(baseKeys.concat([columnName])) // backward compatibility + } else { + allKeysForFetching.push(baseKeys.concat([columnName, WildcardKey])) + } } } @@ -94,7 +100,7 @@ export class BaseRepository implements IRepository { const item = this._makeItemFromSocialDb(id, { ...nonVersionedData, [VersionsKey]: undefined, // remove key from nonVersionedData - ...versionedData, + ...versionedData, // it overrides backward compatible props version, }) From 89ecc2b79f9181e8eaa6a2f81edb5fedd598ac9c Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Wed, 13 Nov 2024 15:23:29 +0300 Subject: [PATCH 05/13] fix: empty icons --- .../src/services/base/base.repository.ts | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/libs/backend/src/services/base/base.repository.ts b/libs/backend/src/services/base/base.repository.ts index 6a462b38..1080b2bc 100644 --- a/libs/backend/src/services/base/base.repository.ts +++ b/libs/backend/src/services/base/base.repository.ts @@ -65,7 +65,7 @@ export class BaseRepository implements IRepository { // Scalar types should be queried without wildcard if (type === ColumnType.Json || type === ColumnType.AsIs) { - if (versioned) { + if (versioned && version) { allKeysForFetching.push(baseKeys.concat([VersionsKey, version!, columnName])) allKeysForFetching.push(baseKeys.concat([columnName])) // backward compatibility } else { @@ -75,11 +75,13 @@ export class BaseRepository implements IRepository { // Non-scalar types should be queried with wildcard if (type === ColumnType.Set || type === ColumnType.AsIs) { - if (versioned) { - allKeysForFetching.push(baseKeys.concat([VersionsKey, version!, columnName, WildcardKey])) - allKeysForFetching.push(baseKeys.concat([columnName])) // backward compatibility + if (versioned && version) { + allKeysForFetching.push( + baseKeys.concat([VersionsKey, version!, columnName, RecursiveWildcardKey]) + ) + allKeysForFetching.push(baseKeys.concat([columnName, RecursiveWildcardKey])) // backward compatibility } else { - allKeysForFetching.push(baseKeys.concat([columnName, WildcardKey])) + allKeysForFetching.push(baseKeys.concat([columnName, RecursiveWildcardKey])) } } } @@ -104,6 +106,17 @@ export class BaseRepository implements IRepository { version, }) + console.log( + item, + { + nonVersionedData, + versionedData, + }, + { + allKeysForFetching, + } + ) + return item } From 3739150bc752f4f353c3e27037690b52b2de4b80 Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Wed, 13 Nov 2024 15:33:30 +0300 Subject: [PATCH 06/13] feat: name mapping (open_with) --- .../src/services/base/base.repository.ts | 26 ++++++------------- .../src/services/document/document.entity.ts | 2 +- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/libs/backend/src/services/base/base.repository.ts b/libs/backend/src/services/base/base.repository.ts index 1080b2bc..7dc90dfa 100644 --- a/libs/backend/src/services/base/base.repository.ts +++ b/libs/backend/src/services/base/base.repository.ts @@ -61,15 +61,16 @@ export class BaseRepository implements IRepository { if (!column) continue - const { type, versioned } = column + const { type, versioned, name } = column + const rawColumnName = name ?? columnName // Scalar types should be queried without wildcard if (type === ColumnType.Json || type === ColumnType.AsIs) { if (versioned && version) { - allKeysForFetching.push(baseKeys.concat([VersionsKey, version!, columnName])) - allKeysForFetching.push(baseKeys.concat([columnName])) // backward compatibility + allKeysForFetching.push(baseKeys.concat([VersionsKey, version!, rawColumnName])) + allKeysForFetching.push(baseKeys.concat([rawColumnName])) // backward compatibility } else { - allKeysForFetching.push(baseKeys.concat([columnName])) + allKeysForFetching.push(baseKeys.concat([rawColumnName])) } } @@ -77,11 +78,11 @@ export class BaseRepository implements IRepository { if (type === ColumnType.Set || type === ColumnType.AsIs) { if (versioned && version) { allKeysForFetching.push( - baseKeys.concat([VersionsKey, version!, columnName, RecursiveWildcardKey]) + baseKeys.concat([VersionsKey, version!, rawColumnName, RecursiveWildcardKey]) ) - allKeysForFetching.push(baseKeys.concat([columnName, RecursiveWildcardKey])) // backward compatibility + allKeysForFetching.push(baseKeys.concat([rawColumnName, RecursiveWildcardKey])) // backward compatibility } else { - allKeysForFetching.push(baseKeys.concat([columnName, RecursiveWildcardKey])) + allKeysForFetching.push(baseKeys.concat([rawColumnName, RecursiveWildcardKey])) } } } @@ -106,17 +107,6 @@ export class BaseRepository implements IRepository { version, }) - console.log( - item, - { - nonVersionedData, - versionedData, - }, - { - allKeysForFetching, - } - ) - return item } diff --git a/libs/backend/src/services/document/document.entity.ts b/libs/backend/src/services/document/document.entity.ts index b973786c..523764fb 100644 --- a/libs/backend/src/services/document/document.entity.ts +++ b/libs/backend/src/services/document/document.entity.ts @@ -17,7 +17,7 @@ export class Document extends Base { @Column({ name: 'open_with', type: ColumnType.Set }) openWith: AppId[] = [] - @Column({ name: 'content', type: ColumnType.Json }) + @Column({ type: ColumnType.Json }) content: any = null toDto(): DocumentDto { From 5bb05dfe10083430c62180b5639f38f4bba4e22d Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Thu, 14 Nov 2024 01:13:28 +0300 Subject: [PATCH 07/13] feat: saving versioned items --- .../src/services/base/base.repository.ts | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/libs/backend/src/services/base/base.repository.ts b/libs/backend/src/services/base/base.repository.ts index 7dc90dfa..c5c02300 100644 --- a/libs/backend/src/services/base/base.repository.ts +++ b/libs/backend/src/services/base/base.repository.ts @@ -211,10 +211,13 @@ export class BaseRepository implements IRepository { const keys = [authorId, SettingsKey, ProjectIdKey, this._entityKey, localId] - const convertedItem = this._makeItemToSocialDb(item) + if (this._isVersionedEntity) { + // increment version + item.version = item.version ? (parseInt(item.version) + 1).toString() : '1' + } + const convertedItem = this._makeItemToSocialDb(item) const dataToSave = SocialDbService.buildNestedData(keys, convertedItem) - await this._commitOrQueue(dataToSave, tx) // ToDo: add timestamp and blockNumber @@ -391,7 +394,12 @@ export class BaseRepository implements IRepository { } private _makeItemToSocialDb(entity: T): Value { - const raw: Value = {} + const raw: Value = this._isVersionedEntity + ? { + [VersionsKey]: { [entity.version]: {} }, + [TagsKey]: { [LatestTagName]: entity.version }, + } + : {} for (const entityKey in entity) { // ToDo: why prototype? @@ -399,7 +407,7 @@ export class BaseRepository implements IRepository { if (!column) continue - const { type, transformer, name } = column + const { type, transformer, name, versioned } = column const rawKey = name ?? entityKey @@ -407,12 +415,20 @@ export class BaseRepository implements IRepository { ? transformer.to(entity[entityKey]) : entity[entityKey] + let rawValue: any = null + if (type === ColumnType.AsIs) { - raw[rawKey] = transformedValue + rawValue = transformedValue } else if (type === ColumnType.Json) { - raw[rawKey] = serializeToDeterministicJson(transformedValue) + rawValue = serializeToDeterministicJson(transformedValue) } else if (type === ColumnType.Set) { - raw[rawKey] = BaseRepository._makeSetToSocialDb(transformedValue) + rawValue = BaseRepository._makeSetToSocialDb(transformedValue) + } + + if (versioned) { + raw[VersionsKey][entity.version][rawKey] = rawValue + } else { + raw[rawKey] = rawValue } } @@ -420,6 +436,10 @@ export class BaseRepository implements IRepository { } private _parseGlobalId(globalId: EntityId): { authorId: string; localId: string } { + if (!globalId) { + throw new Error(`Invalid entity ID: ${globalId}`) + } + const [authorId, entityType, localId] = globalId.split(KeyDelimiter) if (entityType !== this._entityKey) { From f36dc6de31386c7c083c4851084f4f7b2fc6f175 Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Thu, 14 Nov 2024 01:17:41 +0300 Subject: [PATCH 08/13] feat: show version number --- .../multitable-panel/components/mutation-editor-modal.tsx | 5 ++++- libs/backend/src/services/base/base.dto.ts | 1 + libs/backend/src/services/base/base.entity.ts | 1 + .../src/services/notification/notification.service.ts | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx b/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx index c7e16480..e5499d65 100644 --- a/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx +++ b/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx @@ -197,6 +197,7 @@ const EMPTY_MUTATION_ID = '/mutation/NewMutation' const createEmptyMutation = (): MutationDto => ({ authorId: null, blockNumber: 0, + version: '0', id: EMPTY_MUTATION_ID, localId: 'NewMutation', timestamp: 0, @@ -378,7 +379,9 @@ export const MutationEditorModal: FC = ({ apps, baseMutation, localMutati /> -

{baseMutation.metadata.name}

+

+ {baseMutation.metadata.name} (v{baseMutation.version}) +

by{' '} {!baseMutation.authorId && !loggedInAccountId diff --git a/libs/backend/src/services/base/base.dto.ts b/libs/backend/src/services/base/base.dto.ts index 8d9bfe0a..d5463d0d 100644 --- a/libs/backend/src/services/base/base.dto.ts +++ b/libs/backend/src/services/base/base.dto.ts @@ -7,4 +7,5 @@ export type BaseDto = { authorId: string | null blockNumber: number timestamp: number + version: string } diff --git a/libs/backend/src/services/base/base.entity.ts b/libs/backend/src/services/base/base.entity.ts index f5caa6c8..bc0eefd5 100644 --- a/libs/backend/src/services/base/base.entity.ts +++ b/libs/backend/src/services/base/base.entity.ts @@ -60,6 +60,7 @@ export class Base { source: this.source, blockNumber: this.blockNumber, timestamp: this.timestamp, + version: this.version, } } } diff --git a/libs/backend/src/services/notification/notification.service.ts b/libs/backend/src/services/notification/notification.service.ts index fcd1b2b1..9a4db0d6 100644 --- a/libs/backend/src/services/notification/notification.service.ts +++ b/libs/backend/src/services/notification/notification.service.ts @@ -247,6 +247,7 @@ export class NotificationService { recipients: notification.recipients, result: resolution.result, status: resolution.status, + version: notification.version, } } } From 2403f68c355498e4f65352a55ea6ff722e851158 Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Fri, 15 Nov 2024 15:44:49 +0300 Subject: [PATCH 09/13] feat: mutation version switcher ui --- .../multitable-panel/components/dropdown.tsx | 2 +- .../components/mutation-editor-modal.tsx | 5 +- .../components/mutation-version-dropdown.tsx | 42 ++++++++++++ .../src/services/base/base.repository.ts | 2 +- .../src/services/mutation/mutation.service.ts | 34 +++++++++- .../src/services/settings/settings.service.ts | 11 +++ .../app/contexts/mutable-web-context/index.ts | 1 + .../mutable-web-context.tsx | 4 ++ .../mutable-web-provider.tsx | 68 +++++++++++++++---- .../use-mutation-versions.tsx | 22 ++++++ .../use-selected-mutation.tsx | 28 ++++++++ libs/engine/src/index.ts | 1 + 12 files changed, 202 insertions(+), 18 deletions(-) create mode 100644 apps/extension/src/contentscript/multitable-panel/components/mutation-version-dropdown.tsx create mode 100644 libs/engine/src/app/contexts/mutable-web-context/use-mutation-versions.tsx create mode 100644 libs/engine/src/app/contexts/mutable-web-context/use-selected-mutation.tsx diff --git a/apps/extension/src/contentscript/multitable-panel/components/dropdown.tsx b/apps/extension/src/contentscript/multitable-panel/components/dropdown.tsx index d4f42157..7002c51c 100644 --- a/apps/extension/src/contentscript/multitable-panel/components/dropdown.tsx +++ b/apps/extension/src/contentscript/multitable-panel/components/dropdown.tsx @@ -187,7 +187,7 @@ export const Dropdown: FC = ({ ) : selectedMutation.source === EntitySourceType.Local ? ( ) : null} - {selectedMutation.metadata.name} + {selectedMutation.metadata.name} (v{selectedMutation.version}) {selectedMutation.authorId ? ( by {selectedMutation.authorId} diff --git a/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx b/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx index e5499d65..b5d1f2ad 100644 --- a/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx +++ b/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx @@ -13,6 +13,8 @@ import { AppInMutation } from '@mweb/backend' import { Image } from './image' import { useSaveMutation, useMutableWeb } from '@mweb/engine' import { ButtonsGroup } from './buttons-group' +import { useMutationVersions } from '@mweb/engine' +import { MutationVersionDropdown } from './mutation-version-dropdown' const SelectedMutationEditorWrapper = styled.div` display: flex; @@ -380,7 +382,8 @@ export const MutationEditorModal: FC = ({ apps, baseMutation, localMutati

- {baseMutation.metadata.name} (v{baseMutation.version}) + {baseMutation.metadata.name}{' '} +

by{' '} diff --git a/apps/extension/src/contentscript/multitable-panel/components/mutation-version-dropdown.tsx b/apps/extension/src/contentscript/multitable-panel/components/mutation-version-dropdown.tsx new file mode 100644 index 00000000..90cd3236 --- /dev/null +++ b/apps/extension/src/contentscript/multitable-panel/components/mutation-version-dropdown.tsx @@ -0,0 +1,42 @@ +import { useMutableWeb, useMutationVersions } from '@mweb/engine' +import React from 'react' +import { FC } from 'react' + +const LatestKey = 'latest' + +export const MutationVersionDropdown: FC<{ mutationId: string | null }> = ({ mutationId }) => { + const { + switchMutationVersion, + selectedMutation, + mutationVersions: currentMutationVersions, + } = useMutableWeb() + const { mutationVersions, areMutationVersionsLoading } = useMutationVersions(mutationId) + + if (!mutationId) { + return null + } + + if (!selectedMutation || areMutationVersionsLoading) { + return
Loading...
+ } + + const handleChange = (e: React.ChangeEvent) => { + if (mutationId) { + switchMutationVersion( + mutationId, + e.target.value === LatestKey ? null : e.target.value?.toString() + ) + } + } + + return ( + + ) +} diff --git a/libs/backend/src/services/base/base.repository.ts b/libs/backend/src/services/base/base.repository.ts index c5c02300..b920ec9e 100644 --- a/libs/backend/src/services/base/base.repository.ts +++ b/libs/backend/src/services/base/base.repository.ts @@ -299,7 +299,7 @@ export class BaseRepository implements IRepository { const foundKeys = await this.socialDb.keys([keys.join(KeyDelimiter)]) - return foundKeys + return foundKeys.map((key: string) => key.split(KeyDelimiter).pop()!) } async getTagValue({ id, tag }: { id: EntityId; tag: string }): Promise { diff --git a/libs/backend/src/services/mutation/mutation.service.ts b/libs/backend/src/services/mutation/mutation.service.ts index 031be2c9..e291d5e9 100644 --- a/libs/backend/src/services/mutation/mutation.service.ts +++ b/libs/backend/src/services/mutation/mutation.service.ts @@ -30,11 +30,23 @@ export class MutationService { private nearSigner: NearSigner ) {} - async getMutation(mutationId: string): Promise { - const mutation = await this.mutationRepository.getItem({ id: mutationId }) + async getMutation( + mutationId: string, + source?: EntitySourceType, + version?: string + ): Promise { + const mutation = await this.mutationRepository.getItem({ id: mutationId, source, version }) return mutation?.toDto() ?? null } + async getMutationVersions(mutationId: string): Promise<{ version: string }[]> { + const versions = await this.mutationRepository.getVersions({ + id: mutationId, + source: EntitySourceType.Origin, + }) + return versions.map((v) => ({ version: v })) + } + async getMutationsForContext(context: IContextNode): Promise { const mutations = await this.mutationRepository.getItems() return mutations @@ -92,6 +104,15 @@ export class MutationService { return value ?? null } + async getMutationVersion(mutationId: string): Promise { + const value = await this.settingsService.getMutationVersion(mutationId) + return value ?? null + } + + async setMutationVersion(mutationId: string, version: string | null = null): Promise { + return this.settingsService.setMutationVersion(mutationId, version) + } + async setPreferredSource(mutationId: string, source: EntitySourceType | null): Promise { return this.settingsService.setPreferredSource(mutationId, source) } @@ -253,6 +274,15 @@ export class MutationService { return currentDate } + async getMutationWithSettings( + mutationId: string, + source?: EntitySourceType, + version?: string + ): Promise { + const mutation = await this.getMutation(mutationId, source, version) + return mutation ? this.populateMutationWithSettings(mutation) : null + } + public async populateMutationWithSettings(mutation: MutationDto): Promise { const lastUsage = await this.settingsService.getMutationLastUsage( mutation.id, diff --git a/libs/backend/src/services/settings/settings.service.ts b/libs/backend/src/services/settings/settings.service.ts index 7762ea98..35b8bf84 100644 --- a/libs/backend/src/services/settings/settings.service.ts +++ b/libs/backend/src/services/settings/settings.service.ts @@ -7,6 +7,7 @@ const FAVORITE_MUTATION = 'favorite-mutation' const PREFERRED_SOURCE = 'preferred-source' const MUTATION_LAST_USAGE = 'mutation-last-usage' const STOPPED_APPS = 'stopped-apps' +const MUTATION_VERSION = 'mutation-version' export class SettingsSerivce { constructor(private localDb: LocalDbService) {} @@ -58,4 +59,14 @@ export class SettingsSerivce { const key = LocalDbService.makeKey(STOPPED_APPS, mutationId, appInstanceId) return this.localDb.setItem(key, isEnabled) } + + async getMutationVersion(mutationId: string): Promise { + const key = LocalDbService.makeKey(MUTATION_VERSION, mutationId) + return (await this.localDb.getItem(key)) ?? null + } + + async setMutationVersion(mutationId: string, version: string | null = null): Promise { + const key = LocalDbService.makeKey(MUTATION_VERSION, mutationId) + return this.localDb.setItem(key, version) + } } diff --git a/libs/engine/src/app/contexts/mutable-web-context/index.ts b/libs/engine/src/app/contexts/mutable-web-context/index.ts index 71ec1ea4..e4ee0e90 100644 --- a/libs/engine/src/app/contexts/mutable-web-context/index.ts +++ b/libs/engine/src/app/contexts/mutable-web-context/index.ts @@ -8,3 +8,4 @@ export { useEditMutation } from './use-edit-mutation' export { useMutations } from './use-mutations' export { useMutation } from './use-mutation' export { useDeleteLocalMutation } from './use-delete-local-mutation' +export { useMutationVersions } from './use-mutation-versions' diff --git a/libs/engine/src/app/contexts/mutable-web-context/mutable-web-context.tsx b/libs/engine/src/app/contexts/mutable-web-context/mutable-web-context.tsx index d84dda6c..635802f8 100644 --- a/libs/engine/src/app/contexts/mutable-web-context/mutable-web-context.tsx +++ b/libs/engine/src/app/contexts/mutable-web-context/mutable-web-context.tsx @@ -24,6 +24,8 @@ export type MutableWebContextState = { removeMutationFromRecents: (mutationId: string) => void setMutations: React.Dispatch> setMutationApps: React.Dispatch> + switchMutationVersion: (mutationId: string, version?: string | null) => void + mutationVersions: { [key: string]: string | null } } export const contextDefaultValues: MutableWebContextState = { @@ -44,6 +46,8 @@ export const contextDefaultValues: MutableWebContextState = { removeMutationFromRecents: () => undefined, setMutations: () => undefined, setMutationApps: () => undefined, + switchMutationVersion: () => undefined, + mutationVersions: {}, } export const MutableWebContext = createContext(contextDefaultValues) diff --git a/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx b/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx index 81132b22..1f177153 100644 --- a/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx +++ b/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx @@ -12,6 +12,7 @@ import { getNearConfig } from '@mweb/backend' import { ModalContextState } from '../modal-context/modal-context' import { MutationDto } from '@mweb/backend' import { ParserType, ParserConfig } from '@mweb/core' +import { useSelectedMutation } from './use-selected-mutation' type Props = { config: EngineConfig @@ -53,6 +54,9 @@ const MutableWebProvider: FC = ({ config, defaultMutationId, modalApi, ch const [preferredSources, setPreferredSources] = useState<{ [key: string]: EntitySourceType | null }>({}) + const [mutationVersions, setMutationVersions] = useState<{ + [key: string]: string | null + }>({}) const [favoriteMutationId, setFavoriteMutationId] = useState(null) useEffect(() => { @@ -73,6 +77,17 @@ const MutableWebProvider: FC = ({ config, defaultMutationId, modalApi, ch fn() }, [engine, mutations]) + useEffect(() => { + const fn = async () => { + const newMutationVersions: { [key: string]: string | null } = {} + for (const mut of mutations) { + newMutationVersions[mut.id] = await engine.mutationService.getMutationVersion(mut.id) + } + setMutationVersions(newMutationVersions) + } + fn() + }, [engine, mutations]) + const getMutationToBeLoaded = useCallback(async () => { const favoriteMutation = await engine.mutationService.getFavoriteMutation() const lastUsedMutation = tree ? await engine.mutationService.getLastUsedMutation(tree) : null @@ -80,21 +95,28 @@ const MutableWebProvider: FC = ({ config, defaultMutationId, modalApi, ch return lastUsedMutation ?? favoriteMutation }, [engine, tree]) - const selectedMutation = useMemo(() => { - if (!selectedMutationId) return null + // const selectedMutation = useMemo(() => { + // if (!selectedMutationId) return null - const [localMut, remoteMut] = mutations - .filter((m) => m.id === selectedMutationId) - .sort((a) => (a.source === EntitySourceType.Local ? -1 : 1)) + // const [localMut, remoteMut] = mutations + // .filter((m) => m.id === selectedMutationId) + // .sort((a) => (a.source === EntitySourceType.Local ? -1 : 1)) - if (preferredSources[selectedMutationId] === EntitySourceType.Local) { - return localMut ?? remoteMut ?? null - } else if (preferredSources[selectedMutationId] === EntitySourceType.Origin) { - return remoteMut ?? localMut ?? null - } else { - return localMut ?? remoteMut ?? null - } - }, [mutations, selectedMutationId, preferredSources]) + // if (preferredSources[selectedMutationId] === EntitySourceType.Local) { + // return localMut ?? remoteMut ?? null + // } else if (preferredSources[selectedMutationId] === EntitySourceType.Origin) { + // return remoteMut ?? localMut ?? null + // } else { + // return localMut ?? remoteMut ?? null + // } + // }, [mutations, selectedMutationId, preferredSources, mutationVersions]) + + const { selectedMutation } = useSelectedMutation( + engine, + selectedMutationId, + selectedMutationId ? preferredSources[selectedMutationId] : undefined, + selectedMutationId ? mutationVersions[selectedMutationId] : undefined + ) useEffect(() => { getMutationToBeLoaded().then((favoriteMutationId) => { @@ -244,6 +266,24 @@ const MutableWebProvider: FC = ({ config, defaultMutationId, modalApi, ch [engine, mutations] ) + // ToDo: move to separate hook + const switchMutationVersion = useCallback( + async (mutationId: string, version?: string | null) => { + try { + const mut = mutations.find((m) => m.id === mutationId) + if (!mut) return + setMutationVersions((oldMutationVersions) => ({ + ...oldMutationVersions, + [mutationId]: version ?? null, + })) + await engine.mutationService.setMutationVersion(mutationId, version) + } catch (err) { + console.error(err) + } + }, + [engine, mutations] + ) + const getPreferredSource = (mutationId: string): EntitySourceType | null => preferredSources[mutationId] @@ -286,6 +326,8 @@ const MutableWebProvider: FC = ({ config, defaultMutationId, modalApi, ch favoriteMutationId, setMutations, setMutationApps, + switchMutationVersion, + mutationVersions, } return ( diff --git a/libs/engine/src/app/contexts/mutable-web-context/use-mutation-versions.tsx b/libs/engine/src/app/contexts/mutable-web-context/use-mutation-versions.tsx new file mode 100644 index 00000000..85b42347 --- /dev/null +++ b/libs/engine/src/app/contexts/mutable-web-context/use-mutation-versions.tsx @@ -0,0 +1,22 @@ +import { useMutableWeb } from './use-mutable-web' +import { useQuery } from '../../hooks/use-query' + +export const useMutationVersions = (mutationId: string | null) => { + const { engine } = useMutableWeb() + + const { + data: mutationVersions, + isLoading: areMutationVersionsLoading, + // error, + } = useQuery<{ version: string }[]>({ + query: () => + mutationId ? engine.mutationService.getMutationVersions(mutationId) : Promise.resolve([]), + deps: [engine, mutationId], + initialData: [], + }) + + return { + mutationVersions, + areMutationVersionsLoading, + } +} diff --git a/libs/engine/src/app/contexts/mutable-web-context/use-selected-mutation.tsx b/libs/engine/src/app/contexts/mutable-web-context/use-selected-mutation.tsx new file mode 100644 index 00000000..f4c5fb4d --- /dev/null +++ b/libs/engine/src/app/contexts/mutable-web-context/use-selected-mutation.tsx @@ -0,0 +1,28 @@ +import { useQuery } from '../../hooks/use-query' +import { Engine, EntitySourceType, MutationWithSettings } from '@mweb/backend' + +export const useSelectedMutation = ( + engine: Engine, + mutationId: string | null, + source?: EntitySourceType | null, + version?: string | null +) => { + const { + data: selectedMutation, + isLoading: isSelectedMutationLoading, + error: selectedMutationError, + } = useQuery({ + query: () => + mutationId + ? engine.mutationService.getMutationWithSettings( + mutationId, + source ?? undefined, + version ?? undefined + ) + : Promise.resolve(null), + deps: [mutationId, source, version], + initialData: null, + }) + + return { selectedMutation, isSelectedMutationLoading, selectedMutationError } +} diff --git a/libs/engine/src/index.ts b/libs/engine/src/index.ts index 8101fb89..f87f5492 100644 --- a/libs/engine/src/index.ts +++ b/libs/engine/src/index.ts @@ -10,6 +10,7 @@ export { useEditMutation, useMutationApp, useDeleteLocalMutation, + useMutationVersions, } from './app/contexts/mutable-web-context' export { ShadowDomWrapper } from './app/components/shadow-dom-wrapper' export { App as MutableWebProvider } from './app/app' From 379a0f7a36edc685c59e6c22b9de920fe1ba49a3 Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Sun, 17 Nov 2024 04:34:56 +0300 Subject: [PATCH 10/13] fix: selected mutation version are not loaded --- .../components/mutation-version-dropdown.tsx | 2 +- .../mutable-web-provider.tsx | 36 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/apps/extension/src/contentscript/multitable-panel/components/mutation-version-dropdown.tsx b/apps/extension/src/contentscript/multitable-panel/components/mutation-version-dropdown.tsx index 90cd3236..9e691c5b 100644 --- a/apps/extension/src/contentscript/multitable-panel/components/mutation-version-dropdown.tsx +++ b/apps/extension/src/contentscript/multitable-panel/components/mutation-version-dropdown.tsx @@ -17,7 +17,7 @@ export const MutationVersionDropdown: FC<{ mutationId: string | null }> = ({ mut } if (!selectedMutation || areMutationVersionsLoading) { - return
Loading...
+ return Loading... } const handleChange = (e: React.ChangeEvent) => { diff --git a/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx b/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx index 1f177153..a315bfbc 100644 --- a/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx +++ b/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx @@ -69,20 +69,22 @@ const MutableWebProvider: FC = ({ config, defaultMutationId, modalApi, ch const fn = async () => { const localMutations = mutations.filter((mut) => mut.source === EntitySourceType.Local) const newPreferredSources: { [key: string]: EntitySourceType | null } = {} - for (const mut of localMutations) { - newPreferredSources[mut.id] = await engine.mutationService.getPreferredSource(mut.id) - } - setPreferredSources(newPreferredSources) - } - fn() - }, [engine, mutations]) - - useEffect(() => { - const fn = async () => { const newMutationVersions: { [key: string]: string | null } = {} - for (const mut of mutations) { - newMutationVersions[mut.id] = await engine.mutationService.getMutationVersion(mut.id) - } + + await Promise.all([ + ...localMutations.map((mut) => + engine.mutationService + .getPreferredSource(mut.id) + .then((source) => (newPreferredSources[mut.id] = source)) + ), + ...mutations.map((mut) => + engine.mutationService + .getMutationVersion(mut.id) + .then((version) => (newMutationVersions[mut.id] = version)) + ), + ]) + + setPreferredSources(newPreferredSources) setMutationVersions(newMutationVersions) } fn() @@ -111,7 +113,7 @@ const MutableWebProvider: FC = ({ config, defaultMutationId, modalApi, ch // } // }, [mutations, selectedMutationId, preferredSources, mutationVersions]) - const { selectedMutation } = useSelectedMutation( + const { selectedMutation, isSelectedMutationLoading } = useSelectedMutation( engine, selectedMutationId, selectedMutationId ? preferredSources[selectedMutationId] : undefined, @@ -306,7 +308,11 @@ const MutableWebProvider: FC = ({ config, defaultMutationId, modalApi, ch ) const isLoading = - isMutationsLoading || isAppsLoading || isMutationAppsLoading || isMutationParsersLoading + isMutationsLoading || + isAppsLoading || + isMutationAppsLoading || + isMutationParsersLoading || + isSelectedMutationLoading const state: MutableWebContextState = { config: nearConfig, From c4eb734886d96e6f9cf01bce5f743b6a8a886f39 Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Sun, 17 Nov 2024 07:04:18 +0300 Subject: [PATCH 11/13] feat: update mutation when version was switched --- .../multitable-panel/components/mutation-editor-modal.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx b/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx index b5d1f2ad..9747491c 100644 --- a/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx +++ b/apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx @@ -257,7 +257,7 @@ const alerts: { [name: string]: IAlert } = { } export const MutationEditorModal: FC = ({ apps, baseMutation, localMutations, onClose }) => { - const { switchMutation, switchPreferredSource } = useMutableWeb() + const { switchMutation, switchPreferredSource, isLoading } = useMutableWeb() const loggedInAccountId = useAccountId() const [isModified, setIsModified] = useState(true) const [appIdToOpenDocsModal, setAppIdToOpenDocsModal] = useState(null) @@ -277,9 +277,13 @@ export const MutationEditorModal: FC = ({ apps, baseMutation, localMutati const [editingMutation, setEditingMutation] = useState(chooseEditingMutation()) const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false) - const [alert, setAlert] = useState(null) + // Reload the base mutation if it changed (e.g. if a mutation version was updated) + useEffect(() => { + setEditingMutation(chooseEditingMutation()) + }, [isLoading]) + useEffect(() => { const doChecksForAlerts = (): IAlert | null => { if (!loggedInAccountId) return alerts.noWallet From e615aea04f4482234e0a1a2a88f00d0d51905358 Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Tue, 19 Nov 2024 12:43:19 +0300 Subject: [PATCH 12/13] fix: mutation is not refreshed after editing --- .../src/services/mutation/mutation.service.ts | 12 ++++++++---- .../mutable-web-context/mutable-web-provider.tsx | 5 +++-- .../mutable-web-context/use-edit-mutation.ts | 4 +++- .../mutable-web-context/use-selected-mutation.tsx | 3 ++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/libs/backend/src/services/mutation/mutation.service.ts b/libs/backend/src/services/mutation/mutation.service.ts index e291d5e9..e5073e70 100644 --- a/libs/backend/src/services/mutation/mutation.service.ts +++ b/libs/backend/src/services/mutation/mutation.service.ts @@ -162,6 +162,8 @@ export class MutationService { throw new Error('Mutation with that ID does not exist') } + let editedMutation: Mutation + if (mutation.source === EntitySourceType.Origin) { const performTx = (tx: Transaction) => Promise.all([ @@ -172,17 +174,19 @@ export class MutationService { // reuse transaction if (tx) { - await performTx(tx) + const result = await performTx(tx) + editedMutation = result[0] } else { - await this.unitOfWorkService.runInTransaction(performTx) + const result = await this.unitOfWorkService.runInTransaction(performTx) + editedMutation = result[0] } } else if (mutation.source === EntitySourceType.Local) { - await this.mutationRepository.editItem(mutation, tx) + editedMutation = await this.mutationRepository.editItem(mutation, tx) } else { throw new Error('Invalid entity source') } - return this.populateMutationWithSettings(mutation.toDto()) + return this.populateMutationWithSettings(editedMutation.toDto()) } async saveMutation( diff --git a/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx b/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx index a315bfbc..70be4ead 100644 --- a/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx +++ b/libs/engine/src/app/contexts/mutable-web-context/mutable-web-provider.tsx @@ -113,7 +113,7 @@ const MutableWebProvider: FC = ({ config, defaultMutationId, modalApi, ch // } // }, [mutations, selectedMutationId, preferredSources, mutationVersions]) - const { selectedMutation, isSelectedMutationLoading } = useSelectedMutation( + const { selectedMutation, isSelectedMutationLoading, setSelectedMutation } = useSelectedMutation( engine, selectedMutationId, selectedMutationId ? preferredSources[selectedMutationId] : undefined, @@ -232,8 +232,9 @@ const MutableWebProvider: FC = ({ config, defaultMutationId, modalApi, ch if (mutation.id === selectedMutationId) { switchPreferredSource(mutation.id, mutation.source) + setSelectedMutation(mutationWithSettings) } - }, []) + }, [selectedMutationId]) // ToDo: move to separate hook const setFavoriteMutation = useCallback( diff --git a/libs/engine/src/app/contexts/mutable-web-context/use-edit-mutation.ts b/libs/engine/src/app/contexts/mutable-web-context/use-edit-mutation.ts index 4aac8a94..2b6ea9ce 100644 --- a/libs/engine/src/app/contexts/mutable-web-context/use-edit-mutation.ts +++ b/libs/engine/src/app/contexts/mutable-web-context/use-edit-mutation.ts @@ -4,7 +4,7 @@ import { MutableWebContext } from './mutable-web-context' import { SaveMutationOptions } from '@mweb/backend' export function useEditMutation() { - const { engine, setMutations } = useContext(MutableWebContext) + const { engine, setMutations, refreshMutation } = useContext(MutableWebContext) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) @@ -22,6 +22,8 @@ export function useEditMutation() { : mut ) ) + + refreshMutation(editedMutation) } catch (err) { console.error(err) if (err instanceof Error) { diff --git a/libs/engine/src/app/contexts/mutable-web-context/use-selected-mutation.tsx b/libs/engine/src/app/contexts/mutable-web-context/use-selected-mutation.tsx index f4c5fb4d..c51991d7 100644 --- a/libs/engine/src/app/contexts/mutable-web-context/use-selected-mutation.tsx +++ b/libs/engine/src/app/contexts/mutable-web-context/use-selected-mutation.tsx @@ -11,6 +11,7 @@ export const useSelectedMutation = ( data: selectedMutation, isLoading: isSelectedMutationLoading, error: selectedMutationError, + setData: setSelectedMutation, } = useQuery({ query: () => mutationId @@ -24,5 +25,5 @@ export const useSelectedMutation = ( initialData: null, }) - return { selectedMutation, isSelectedMutationLoading, selectedMutationError } + return { selectedMutation, isSelectedMutationLoading, selectedMutationError, setSelectedMutation } } From ca68da39c32b72470b3642031de12ebc62b77d97 Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Tue, 19 Nov 2024 13:16:05 +0300 Subject: [PATCH 13/13] fix: editing of previous versions overrides next one --- libs/backend/src/services/base/base.repository.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/backend/src/services/base/base.repository.ts b/libs/backend/src/services/base/base.repository.ts index b920ec9e..fb22929d 100644 --- a/libs/backend/src/services/base/base.repository.ts +++ b/libs/backend/src/services/base/base.repository.ts @@ -213,7 +213,8 @@ export class BaseRepository implements IRepository { if (this._isVersionedEntity) { // increment version - item.version = item.version ? (parseInt(item.version) + 1).toString() : '1' + const lastVersion = await this.getTagValue({ id: item.id, tag: LatestTagName }) + item.version = lastVersion ? (parseInt(lastVersion) + 1).toString() : '1' } const convertedItem = this._makeItemToSocialDb(item)