diff --git a/README.md b/README.md index 5bcf48ae8f..2149dc9704 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,6 @@ You can see a live demo of CloudBeaver here: https://demo.cloudbeaver.io ## Contribution As a community-driven open-source project, we warmly welcome contributions through GitHub pull requests. -[We are happy to reward](https://dbeaver.com/help-beaver/) our most active contributors every major sprint. +[We are happy to reward](https://dbeaver.com/help-dbeaver/) our most active contributors every major sprint. The most significant contribution to our code for the major release 24.2.0 was made by: 1. [matthieukhl](https://github.com/matthieukhl) diff --git a/webapp/packages/core-authentication/src/UsersResource.ts b/webapp/packages/core-authentication/src/UsersResource.ts index 6e790e75a3..ea81f6d1b0 100644 --- a/webapp/packages/core-authentication/src/UsersResource.ts +++ b/webapp/packages/core-authentication/src/UsersResource.ts @@ -9,12 +9,11 @@ import { runInAction } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { - CACHED_RESOURCE_DEFAULT_PAGE_LIMIT, - CACHED_RESOURCE_DEFAULT_PAGE_OFFSET, CachedMapAllKey, CachedMapResource, CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey, + getOffsetPageKeyInfo, isResourceAlias, type ResourceKey, resourceKeyList, @@ -260,19 +259,11 @@ export class UsersResource extends CachedMapResource user.userId), users.length === limit, ]); diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts index 17ef52750d..90f8d9c0d1 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts @@ -15,6 +15,7 @@ import { CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey, CachedResourceOffsetPageTargetKey, + getOffsetPageKeyInfo, isResourceAlias, type ResourceKey, resourceKeyList, @@ -96,16 +97,8 @@ export class DBObjectResource extends CachedMapResource { } protected async loader(originalKey: ResourceKey): Promise> { - let limit = this.navTreeResource.childrenLimit; - let offset = CACHED_RESOURCE_DEFAULT_PAGE_OFFSET; const parentKey = this.aliases.isAlias(originalKey, DBObjectParentKey); - const pageKey = - this.aliases.isAlias(originalKey, CachedResourceOffsetPageKey) || this.aliases.isAlias(originalKey, CachedResourceOffsetPageListKey); - - if (pageKey) { - limit = pageKey.options.limit; - offset = pageKey.options.offset; - } + const { isPageListKey, offset, limit } = getOffsetPageKeyInfo(this, originalKey, undefined, this.navTreeResource.childrenLimit); if (parentKey) { const nodeId = parentKey.options.parentId; @@ -116,11 +109,11 @@ export class DBObjectResource extends CachedMapResource { this.set(resourceKeyList(keys), dbObjects); this.offsetPagination.setPage( - CachedResourceOffsetPageKey(offset, limit).setParent(CachedResourceOffsetPageTargetKey(originalKey)), + isPageListKey + ? CachedResourceOffsetPageListKey(offset, limit).setParent(parentKey || CachedResourceOffsetPageTargetKey(nodeId)) + : CachedResourceOffsetPageKey(offset, limit).setParent(parentKey || CachedResourceOffsetPageTargetKey(nodeId)), keys, - this.navTreeResource.offsetPagination.hasNextPage( - CachedResourceOffsetPageKey(offset, limit).setParent(CachedResourceOffsetPageTargetKey(nodeId)), - ), + keys.length === limit, ); }); diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts index 4f52088d9a..5f996e2976 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts @@ -12,12 +12,12 @@ import { injectable } from '@cloudbeaver/core-di'; import { Executor, ExecutorInterrupter, IExecutionContext, IExecutor } from '@cloudbeaver/core-executor'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; import { - CACHED_RESOURCE_DEFAULT_PAGE_OFFSET, CachedMapAllKey, CachedMapResource, CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey, CachedResourceOffsetPageTargetKey, + getOffsetPageKeyInfo, type ICachedResourceMetadata, isResourceAlias, isResourceKeyList, @@ -275,9 +275,10 @@ export class NavTreeResource extends CachedMapResource): Promise> { - const pageKey = - this.aliases.isAlias(originalKey, CachedResourceOffsetPageKey) || this.aliases.isAlias(originalKey, CachedResourceOffsetPageListKey); + const { isPageListKey, pageTargetKey, offset, limit } = getOffsetPageKeyInfo(this, originalKey, undefined, this.childrenLimit); const allKey = this.aliases.isAlias(originalKey, CachedMapAllKey); - const pageTarget = this.aliases.isAlias(originalKey, CachedResourceOffsetPageTargetKey); if (allKey) { throw new Error('Loading all nodes is prohibited'); } - const offset = pageKey?.options.offset ?? CACHED_RESOURCE_DEFAULT_PAGE_OFFSET; - const limit = pageKey?.options.limit ?? this.childrenLimit; const values: NavNodeChildrenQuery[] = []; const pages: Parameters[] = []; await ResourceKeyUtils.forEachAsync(originalKey, async key => { - const nodeId = pageTarget?.options?.target ?? key; + const nodeId = pageTargetKey ?? key; const navNodeChildren = await this.loadNodeChildren(nodeId, offset, limit); values.push(navNodeChildren); pages.push([ - CachedResourceOffsetPageKey(offset, navNodeChildren.navNodeChildren.length).setParent(CachedResourceOffsetPageTargetKey(nodeId)), + isPageListKey + ? CachedResourceOffsetPageListKey(offset, navNodeChildren.navNodeChildren.length).setParent(CachedResourceOffsetPageTargetKey(nodeId)) + : CachedResourceOffsetPageKey(offset, navNodeChildren.navNodeChildren.length).setParent(CachedResourceOffsetPageTargetKey(nodeId)), navNodeChildren.navNodeChildren.map(node => node.id), navNodeChildren.navNodeChildren.length === limit, ]); diff --git a/webapp/packages/core-resource/src/Resource/CachedResource.ts b/webapp/packages/core-resource/src/Resource/CachedResource.ts index 50f8a914c9..e37a73e0ed 100644 --- a/webapp/packages/core-resource/src/Resource/CachedResource.ts +++ b/webapp/packages/core-resource/src/Resource/CachedResource.ts @@ -34,7 +34,7 @@ import { isResourceAlias } from './ResourceAlias'; import { ResourceError } from './ResourceError'; import type { ResourceKey, ResourceKeyFlat } from './ResourceKey'; import { resourceKeyAlias } from './ResourceKeyAlias'; -import { resourceKeyList } from './ResourceKeyList'; +import { isResourceKeyList, resourceKeyList } from './ResourceKeyList'; import { resourceKeyListAlias } from './ResourceKeyListAlias'; import { ResourceOffsetPagination } from './ResourceOffsetPagination'; @@ -91,41 +91,58 @@ export abstract class CachedResource< this.aliases.add(CachedResourceParamKey, () => defaultKey); this.aliases.add(CachedResourceListEmptyKey, () => resourceKeyList([])); this.aliases.add(CachedResourceOffsetPageTargetKey, key => key.options.target); - this.aliases.add(CachedResourceOffsetPageKey, key => { - const keys = []; - const pageInfo = this.offsetPagination.getPageInfo(key); + this.aliases.add( + CachedResourceOffsetPageListKey, + key => key.parent! as any, + (param, key) => { + if (!isResourceKeyList(key)) { + return key as any; + } - if (pageInfo) { - const from = key.options.offset; - const to = key.options.offset + key.options.limit; + const keys = new Set(); + const pageInfo = this.offsetPagination.getPageInfo(param); - for (const page of pageInfo.pages) { - if (page.isHasCommonSegment(from, to)) { - keys.push(...page.get(from, to)); + if (pageInfo) { + const from = param.options.offset; + const to = param.options.offset + param.options.limit; + + for (const page of pageInfo.pages) { + if (page.isHasCommonSegment(from, to)) { + for (const pageKey of page.get(from, to)) { + keys.add(pageKey); + } + } } } - } + return resourceKeyList(key.filter(value => keys.has(value))); + }, + ); + this.aliases.add( + CachedResourceOffsetPageKey, + key => key.parent! as any, + (param, key) => { + if (!isResourceKeyList(key)) { + return key as any; + } - // todo: return single element? - return resourceKeyList([...new Set(keys)]); - }); - this.aliases.add(CachedResourceOffsetPageListKey, key => { - const keys = []; - const pageInfo = this.offsetPagination.getPageInfo(key); + const keys = new Set(); + const pageInfo = this.offsetPagination.getPageInfo(param); - if (pageInfo) { - const from = key.options.offset; - const to = key.options.offset + key.options.limit; + if (pageInfo) { + const from = param.options.offset; + const to = param.options.offset + param.options.limit; - for (const page of pageInfo.pages) { - if (page.isHasCommonSegment(from, to)) { - keys.push(...page.get(from, to)); + for (const page of pageInfo.pages) { + if (page.isHasCommonSegment(from, to)) { + for (const pageKey of page.get(from, to)) { + keys.add(pageKey); + } + } } } - } - - return resourceKeyList([...new Set(keys)]); - }); + return resourceKeyList(key.filter(value => keys.has(value))); + }, + ); // this.logger.spy(this.beforeLoad, 'beforeLoad'); // this.logger.spy(this.onDataOutdated, 'onDataOutdated'); @@ -329,6 +346,7 @@ export abstract class CachedResource< } const pageKey = this.aliases.isAlias(param, CachedResourceOffsetPageKey) || this.aliases.isAlias(param, CachedResourceOffsetPageListKey); + if (pageKey) { const pageInfo = this.offsetPagination.getPageInfo(pageKey); diff --git a/webapp/packages/core-resource/src/Resource/ResourceAlias.ts b/webapp/packages/core-resource/src/Resource/ResourceAlias.ts index ed23a86b95..ca8f17372f 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceAlias.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceAlias.ts @@ -42,7 +42,7 @@ export abstract class ResourceAlias return undefined; } - setParent(parent: ResourceAlias): this { + setParent(parent: ResourceAlias | undefined): this { parent = this.parent ? this.parent.setParent(parent) : parent; const copy = new (this.constructor as any)(this.id, this.options, parent) as this; return copy; diff --git a/webapp/packages/core-resource/src/Resource/ResourceAliases.ts b/webapp/packages/core-resource/src/Resource/ResourceAliases.ts index fa608783c9..c58f340918 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceAliases.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceAliases.ts @@ -8,7 +8,7 @@ import { toJS } from 'mobx'; import { isResourceAlias, type ResourceAlias, ResourceAliasFactory, type ResourceAliasOptions } from './ResourceAlias'; -import type { ResourceKey } from './ResourceKey'; +import type { ResourceKey, ResourceKeySimple } from './ResourceKey'; import type { ResourceKeyAlias } from './ResourceKeyAlias'; import { isResourceKeyList, ResourceKeyList } from './ResourceKeyList'; import type { ResourceKeyListAlias } from './ResourceKeyListAlias'; @@ -16,14 +16,25 @@ import type { ResourceLogger } from './ResourceLogger'; export type IParamAlias = { id: string; - getAlias: (param: ResourceAlias) => ResourceKey; + getAlias: ResourceAliasResolver; + transformKey?: ResourceAliasKeyTransformer; }; +export type ResourceAliasResolver = (param: ResourceAlias) => ResourceKey; + +export type ResourceAliasKeyTransformer = >( + param: ResourceAlias, + key: T, +) => T; + export class ResourceAliases { protected paramAliases: Array>; private captureAliasGetterExecution: boolean; - constructor(private readonly logger: ResourceLogger, private readonly validateKey: (key: TKey) => boolean) { + constructor( + private readonly logger: ResourceLogger, + private readonly validateKey: (key: TKey) => boolean, + ) { this.paramAliases = []; this.captureAliasGetterExecution = false; @@ -52,21 +63,23 @@ export class ResourceAliases { add( param: ResourceAlias | ResourceAliasFactory, - getAlias: (param: ResourceAlias) => ResourceKey, + getAlias: ResourceAliasResolver, + transformKey?: ResourceAliasKeyTransformer, ): void { - this.paramAliases.push({ id: param.id, getAlias }); + this.paramAliases.push({ id: param.id, getAlias, transformKey }); } replace( param: ResourceAlias | ResourceAliasFactory, - getAlias: (param: ResourceAlias) => ResourceKey, + getAlias: ResourceAliasResolver, + transformKey?: ResourceAliasKeyTransformer, ): void { const indexOf = this.paramAliases.findIndex(aliasInfo => aliasInfo.id === param.id); if (indexOf === -1) { - this.add(param, getAlias); + this.add(param, getAlias, transformKey); } else { - this.paramAliases.splice(indexOf, 1, { id: param.id, getAlias }); + this.paramAliases.splice(indexOf, 1, { id: param.id, getAlias, transformKey }); } } @@ -111,6 +124,7 @@ export class ResourceAliases { transformToKey(param: ResourceKey): TKey | ResourceKeyList { let deep = 0; + const transforms: Array<{ key: ResourceKeyAlias | ResourceKeyListAlias; alias: IParamAlias }> = []; while (deep < 10) { if (!this.validateResourceKey(param)) { let paramString = JSON.stringify(toJS(param)); @@ -124,7 +138,15 @@ export class ResourceAliases { if (isResourceAlias(param)) { for (const alias of this.paramAliases) { if (alias.id === param.id) { - param = this.captureGetAlias(alias, param); + transforms.push({ key: param, alias }); + const nextParam = this.captureGetAlias(alias, param); + + if (isResourceAlias(nextParam)) { + param = nextParam.setParent(param); + } else { + param = nextParam; + } + deep++; break; } @@ -141,6 +163,11 @@ export class ResourceAliases { if (isResourceAlias(param)) { throw new Error(`Alias ${param.toString()} is not registered in ${this.logger.getName()}`); } + + for (const { key, alias } of transforms) { + param = alias.transformKey ? alias.transformKey(key, param) : param; + } + return param; } diff --git a/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts b/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts index b5c9c67039..74179e1d87 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts @@ -11,7 +11,7 @@ import { DefaultValueGetter, isPrimitive, MetadataMap } from '@cloudbeaver/core- import { CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey } from './CachedResourceOffsetPageKeys'; import type { ICachedResourceMetadata } from './ICachedResourceMetadata'; -import { isResourceAlias } from './ResourceAlias'; +import { isResourceAlias, ResourceAlias } from './ResourceAlias'; import type { ResourceAliases } from './ResourceAliases'; import type { ResourceKey, ResourceKeyFlat } from './ResourceKey'; import { isResourceKeyList, ResourceKeyList } from './ResourceKeyList'; @@ -111,6 +111,8 @@ export class ResourceMetadata { if (this.some(param, predicate)) { result = true; } + } else if (predicate(this.get(param))) { + result = true; } } @@ -194,11 +196,11 @@ export class ResourceMetadata { if (isResourceAlias(key)) { key = this.aliases.transformToAlias(key); - if (this.aliases.isAlias(key, CachedResourceOffsetPageKey) || this.aliases.isAlias(key, CachedResourceOffsetPageListKey)) { + if (isResourceAlias(key, CachedResourceOffsetPageKey) || isResourceAlias(key, CachedResourceOffsetPageListKey)) { return this.getMetadataKeyRef(key.parent as any); } - return key.toString() as TKey; + return (key as ResourceAlias).toString() as TKey; } if (isPrimitive(key)) { diff --git a/webapp/packages/core-resource/src/Resource/getOffsetPageKeyInfo.ts b/webapp/packages/core-resource/src/Resource/getOffsetPageKeyInfo.ts new file mode 100644 index 0000000000..c51ceae077 --- /dev/null +++ b/webapp/packages/core-resource/src/Resource/getOffsetPageKeyInfo.ts @@ -0,0 +1,46 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { CachedResource } from './CachedResource'; +import { + CACHED_RESOURCE_DEFAULT_PAGE_LIMIT, + CACHED_RESOURCE_DEFAULT_PAGE_OFFSET, + CachedResourceOffsetPageKey, + CachedResourceOffsetPageListKey, + CachedResourceOffsetPageTargetKey, +} from './CachedResourceOffsetPageKeys'; +import { CachedResourceKey } from './IResource'; + +interface IOffsetPageKeyInfo { + limit: number; + offset: number; + isPageListKey: boolean; + pageTargetKey?: any; +} + +export function getOffsetPageKeyInfo( + resource: CachedResource, + originalKey: CachedResourceKey, + offset = CACHED_RESOURCE_DEFAULT_PAGE_OFFSET, + limit = CACHED_RESOURCE_DEFAULT_PAGE_LIMIT, +): IOffsetPageKeyInfo { + const pageListKey = resource.aliases.isAlias(originalKey, CachedResourceOffsetPageListKey); + const pageKey = resource.aliases.isAlias(originalKey, CachedResourceOffsetPageKey) || pageListKey; + const pageTargetKey = resource.aliases.isAlias(originalKey, CachedResourceOffsetPageTargetKey); + + if (pageKey) { + limit = pageKey.options.limit; + offset = pageKey.options.offset; + } + + return { + limit, + offset, + isPageListKey: !!pageListKey, + pageTargetKey: pageTargetKey?.options.target, + }; +} diff --git a/webapp/packages/core-resource/src/index.ts b/webapp/packages/core-resource/src/index.ts index 6ff9ce3edd..ca7ebd42d6 100644 --- a/webapp/packages/core-resource/src/index.ts +++ b/webapp/packages/core-resource/src/index.ts @@ -18,6 +18,7 @@ export { getNextPageOffset, type ICachedResourceOffsetPageOptions, } from './Resource/CachedResourceOffsetPageKeys'; +export * from './Resource/getOffsetPageKeyInfo'; export * from './Resource/CachedTreeResource/CachedTreeResource'; export * from './Resource/CachedTreeResource/ICachedTreeMoveData'; export * from './Resource/ICachedResourceMetadata'; diff --git a/webapp/packages/product-default/package.json b/webapp/packages/product-default/package.json index 95ad13e66a..4bd8b88606 100644 --- a/webapp/packages/product-default/package.json +++ b/webapp/packages/product-default/package.json @@ -5,7 +5,7 @@ "src/**/*.scss", "public/**/*" ], - "version": "24.2.1", + "version": "24.2.2", "description": "CloudBeaver Community", "license": "Apache-2.0", "main": "dist/index.js",