diff --git a/src/registry/common/entities/collection/collection.ts b/src/registry/common/entities/collection/collection.ts index 5821f6ca..ab77f199 100644 --- a/src/registry/common/entities/collection/collection.ts +++ b/src/registry/common/entities/collection/collection.ts @@ -88,6 +88,10 @@ export const Collection: CollectionConstructor = class Collection implements Col this.permissions = permissions; } + async deletePermissions(): Promise { + this.permissions = undefined; + } + async fetchAllPermissions() { this.permissions = this.getAllPermissions(); return Promise.resolve(); diff --git a/src/registry/common/entities/structure-item/types.ts b/src/registry/common/entities/structure-item/types.ts index ce5909ca..845cb39b 100644 --- a/src/registry/common/entities/structure-item/types.ts +++ b/src/registry/common/entities/structure-item/types.ts @@ -25,6 +25,8 @@ export interface StructureItemInstance { setPermissions(permissions: CollectionPermissions | WorkbookPermissions): void; + deletePermissions(args: {parentIds: string[]; skipCheckPermissions?: boolean}): Promise; + enableAllPermissions(): void; fetchAllPermissions(args: {parentIds: string[]}): Promise; diff --git a/src/registry/common/entities/workbook/workbook.ts b/src/registry/common/entities/workbook/workbook.ts index 7022138e..2fb066a1 100644 --- a/src/registry/common/entities/workbook/workbook.ts +++ b/src/registry/common/entities/workbook/workbook.ts @@ -80,6 +80,10 @@ export const Workbook: WorkbookConstructor = class Workbook this.permissions = permissions; } + async deletePermissions(): Promise { + this.permissions = undefined; + } + enableAllPermissions() { this.permissions = { listAccessBindings: true, diff --git a/src/services/new/collection/delete-collections.ts b/src/services/new/collection/delete-collections.ts index c47cd338..88b3ceb2 100644 --- a/src/services/new/collection/delete-collections.ts +++ b/src/services/new/collection/delete-collections.ts @@ -1,15 +1,19 @@ import {ServiceArgs} from '../types'; -import {getPrimary} from '../utils'; +import {getPrimary, getReplica} from '../utils'; import {deleteWorkbooks} from '../workbook'; import {makeSchemaValidator} from '../../../components/validation-schema-compiler'; -import {CURRENT_TIMESTAMP, US_ERRORS} from '../../../const'; -import {raw, transaction} from 'objection'; +import {US_ERRORS} from '../../../const'; +import {transaction} from 'objection'; import {CollectionModel, CollectionModelColumn} from '../../../db/models/new/collection'; import {WorkbookModel, WorkbookModelColumn} from '../../../db/models/new/workbook'; import Utils from '../../../utils'; import {CollectionPermission} from '../../../entities/collection'; import {AppError} from '@gravity-ui/nodekit'; import {getCollectionsListByIds} from './get-collections-list-by-ids'; +import {markCollectionsAsDeleted} from './utils/mark-collections-as-deleted'; +import {getParents, getParentsIdsFromMap} from './utils'; +import {registry} from '../../../registry'; +import {CollectionInstance} from '../../../registry/common/entities/collection/types'; const validateArgs = makeSchemaValidator({ type: 'object', @@ -32,11 +36,7 @@ export const deleteCollections = async ( ) => { const {collectionIds} = args; - const { - tenantId, - projectId, - user: {userId}, - } = ctx.get('info'); + const {tenantId, projectId} = ctx.get('info'); ctx.log('DELETE_COLLECTIONS_START', { collectionIds: await Utils.macrotasksMap(collectionIds, (id) => Utils.encodeId(id)), @@ -49,65 +49,91 @@ export const deleteCollections = async ( const targetTrx = getPrimary(trx); await getCollectionsListByIds( - {ctx, trx: targetTrx, skipValidation, skipCheckPermissions}, + {ctx, trx: getReplica(trx), skipValidation, skipCheckPermissions}, {collectionIds, permission: CollectionPermission.Delete}, ); - const result = await transaction(targetTrx, async (transactionTrx) => { - const recursiveName = 'collectionChildren'; - - const collectionsForDelete = await CollectionModel.query(transactionTrx) - .withRecursive(recursiveName, (qb1) => { - qb1.select() - .from(CollectionModel.tableName) - .where({ - [CollectionModelColumn.TenantId]: tenantId, - [CollectionModelColumn.ProjectId]: projectId, - [CollectionModelColumn.DeletedAt]: null, - }) - .whereIn([CollectionModelColumn.CollectionId], collectionIds) - .union((qb2) => { - qb2.select(`${CollectionModel.tableName}.*`) - .from(CollectionModel.tableName) - .where({ - [`${CollectionModel.tableName}.${CollectionModelColumn.TenantId}`]: - tenantId, - [`${CollectionModel.tableName}.${CollectionModelColumn.ProjectId}`]: - projectId, - [`${CollectionModel.tableName}.${CollectionModelColumn.DeletedAt}`]: - null, - }) - .join( - recursiveName, - `${recursiveName}.${CollectionModelColumn.CollectionId}`, - `${CollectionModel.tableName}.${CollectionModelColumn.ParentId}`, - ); - }); - }) - .select() - .from(recursiveName) - .timeout(CollectionModel.DEFAULT_QUERY_TIMEOUT); - - const collectionsForDeleteIds = collectionsForDelete.map((item) => item.collectionId); - - const workbooksForDelete = await WorkbookModel.query(transactionTrx) - .select() - .where(WorkbookModelColumn.CollectionId, 'in', collectionsForDeleteIds) - .where({ - [WorkbookModelColumn.DeletedAt]: null, - }) - .timeout(WorkbookModel.DEFAULT_QUERY_TIMEOUT); - - for (const workbook of workbooksForDelete) { - if (workbook.isTemplate) { - throw new AppError("Collection with workbook template can't be deleted", { - code: US_ERRORS.COLLECTION_WITH_WORKBOOK_TEMPLATE_CANT_BE_DELETED, + const recursiveName = 'collectionChildren'; + + const collectionsForDelete = await CollectionModel.query(getReplica(trx)) + .withRecursive(recursiveName, (qb1) => { + qb1.select() + .from(CollectionModel.tableName) + .where({ + [CollectionModelColumn.TenantId]: tenantId, + [CollectionModelColumn.ProjectId]: projectId, + [CollectionModelColumn.DeletedAt]: null, + }) + .whereIn([CollectionModelColumn.CollectionId], collectionIds) + .union((qb2) => { + qb2.select(`${CollectionModel.tableName}.*`) + .from(CollectionModel.tableName) + .where({ + [`${CollectionModel.tableName}.${CollectionModelColumn.TenantId}`]: + tenantId, + [`${CollectionModel.tableName}.${CollectionModelColumn.ProjectId}`]: + projectId, + [`${CollectionModel.tableName}.${CollectionModelColumn.DeletedAt}`]: + null, + }) + .join( + recursiveName, + `${recursiveName}.${CollectionModelColumn.CollectionId}`, + `${CollectionModel.tableName}.${CollectionModelColumn.ParentId}`, + ); }); - } + }) + .select() + .from(recursiveName) + .timeout(CollectionModel.DEFAULT_QUERY_TIMEOUT); + + const collectionsForDeleteIds = collectionsForDelete.map((item) => item.collectionId); + + const collectionsMap = new Map(); + + const parents = await getParents({ + ctx, + trx: getReplica(trx), + collectionIds: collectionsForDeleteIds, + }); + + const parentsMap = new Map>(); + + parents.forEach((parent: CollectionModel) => { + parentsMap.set(parent.collectionId, parent.parentId); + }); + + const {Collection} = registry.common.classes.get(); + + collectionsForDelete.forEach((model) => { + const parentId = model.parentId; + + const parentsForCollection = getParentsIdsFromMap(parentId, parentsMap); + + const collection = new Collection({ctx, model}); + + collectionsMap.set(collection, parentsForCollection); + }); + + const workbooksForDelete = await WorkbookModel.query(getReplica(trx)) + .select() + .where(WorkbookModelColumn.CollectionId, 'in', collectionsForDeleteIds) + .where({ + [WorkbookModelColumn.DeletedAt]: null, + }) + .timeout(WorkbookModel.DEFAULT_QUERY_TIMEOUT); + + for (const workbook of workbooksForDelete) { + if (workbook.isTemplate) { + throw new AppError("Collection with workbook template can't be deleted", { + code: US_ERRORS.COLLECTION_WITH_WORKBOOK_TEMPLATE_CANT_BE_DELETED, + }); } + } - const workbookIds = workbooksForDelete.map((workbook) => workbook.workbookId); + const workbookIds = workbooksForDelete.map((workbook) => workbook.workbookId); + const result = await transaction(targetTrx, async (transactionTrx) => { if (workbookIds.length) { await deleteWorkbooks( {ctx, trx: transactionTrx, skipCheckPermissions: true}, @@ -117,17 +143,10 @@ export const deleteCollections = async ( ); } - const deletedCollections = await CollectionModel.query(transactionTrx) - .patch({ - [CollectionModelColumn.DeletedBy]: userId, - [CollectionModelColumn.DeletedAt]: raw(CURRENT_TIMESTAMP), - }) - .where(CollectionModelColumn.CollectionId, 'in', collectionsForDeleteIds) - .andWhere({ - [CollectionModelColumn.DeletedAt]: null, - }) - .returning('*') - .timeout(CollectionModel.DEFAULT_QUERY_TIMEOUT); + const deletedCollections = await markCollectionsAsDeleted( + {ctx, trx, skipCheckPermissions: true}, + {collectionsMap}, + ); return deletedCollections; }); diff --git a/src/services/new/collection/utils/get-parents.ts b/src/services/new/collection/utils/get-parents.ts index 17aa3d9a..446652d8 100644 --- a/src/services/new/collection/utils/get-parents.ts +++ b/src/services/new/collection/utils/get-parents.ts @@ -75,3 +75,21 @@ export const getCollectionsParentIds = async ({ const parents = await getParents({ctx, trx, collectionIds}); return parents.map((item) => item.collectionId); }; + +export const getParentsIdsFromMap = ( + collectionId: string | null, + parentsMap: Map>, +): string[] => { + let id: Nullable = collectionId; + const arr: string[] = id ? [id] : []; + + while (id !== null) { + const curr: Nullable = parentsMap.get(id) || null; + + if (curr) arr.push(curr); + + id = curr; + } + + return arr; +}; diff --git a/src/services/new/collection/utils/mark-collections-as-deleted.ts b/src/services/new/collection/utils/mark-collections-as-deleted.ts new file mode 100644 index 00000000..844decd5 --- /dev/null +++ b/src/services/new/collection/utils/mark-collections-as-deleted.ts @@ -0,0 +1,48 @@ +import {raw} from 'objection'; +import {CURRENT_TIMESTAMP} from '../../../../const'; +import {ServiceArgs} from '../../types'; +import {getPrimary} from '../../utils'; +import {CollectionModel, CollectionModelColumn} from '../../../../db/models/new/collection'; +import {CollectionInstance} from '../../../../registry/common/entities/collection/types'; + +export const markCollectionsAsDeleted = async ( + {ctx, trx, skipCheckPermissions}: ServiceArgs, + {collectionsMap}: {collectionsMap: Map}, +) => { + const collectionIds: string[] = []; + + collectionsMap.forEach((parentIds, collectionInstance) => { + collectionIds.push(collectionInstance.model.collectionId); + }); + + const { + user: {userId}, + } = ctx.get('info'); + + const deletedCollections = await CollectionModel.query(getPrimary(trx)) + .patch({ + [CollectionModelColumn.DeletedBy]: userId, + [CollectionModelColumn.DeletedAt]: raw(CURRENT_TIMESTAMP), + }) + .where(CollectionModelColumn.CollectionId, 'in', collectionIds) + .andWhere({ + [CollectionModelColumn.DeletedAt]: null, + }) + .returning('*') + .timeout(CollectionModel.DEFAULT_QUERY_TIMEOUT); + + const deletePermissionsPromises: Promise[] = []; + + collectionsMap.forEach((parentIds, collectionInstance) => { + deletePermissionsPromises.push( + collectionInstance.deletePermissions({ + parentIds, + skipCheckPermissions, + }), + ); + }); + + await Promise.all(deletePermissionsPromises); + + return deletedCollections; +}; diff --git a/src/services/new/workbook/delete-workbooks.ts b/src/services/new/workbook/delete-workbooks.ts index 0ac1602f..ffec8af6 100644 --- a/src/services/new/workbook/delete-workbooks.ts +++ b/src/services/new/workbook/delete-workbooks.ts @@ -3,15 +3,16 @@ import {getParentIds} from '../collection/utils/get-parents'; import {ServiceArgs} from '../types'; import {getPrimary} from '../utils'; import {makeSchemaValidator} from '../../../components/validation-schema-compiler'; -import {US_ERRORS, CURRENT_TIMESTAMP} from '../../../const'; -import {raw, transaction} from 'objection'; -import {WorkbookModel, WorkbookModelColumn} from '../../../db/models/new/workbook'; +import {US_ERRORS} from '../../../const'; +import {transaction} from 'objection'; import Lock from '../../../db/models/lock'; import {Entry, EntryColumn} from '../../../db/models/new/entry'; import Utils, {makeUserId} from '../../../utils'; import {WorkbookPermission} from '../../../entities/workbook'; import {markEntryAsDeleted} from '../../entry/crud'; import {getWorkbooksListByIds} from './get-workbooks-list-by-ids'; +import {markWorkbooksAsDeleted} from './utils'; +import {WorkbookInstance} from '../../../registry/common/entities/workbook/types'; const validateArgs = makeSchemaValidator({ type: 'object', @@ -57,6 +58,10 @@ export const deleteWorkbooks = async ( {workbookIds}, ); + const workbooksMap: Map = new Map(); + + let parentIds: string[] = []; + const checkDeletePermissionPromises = workbooks.map(async (workbook) => { if (workbook.model.isTemplate) { throw new AppError("Workbook template can't be deleted", { @@ -64,9 +69,7 @@ export const deleteWorkbooks = async ( }); } - if (accessServiceEnabled && !skipCheckPermissions) { - let parentIds: string[] = []; - + if (accessServiceEnabled) { if (workbook.model.collectionId !== null) { parentIds = await getParentIds({ ctx, @@ -75,24 +78,24 @@ export const deleteWorkbooks = async ( }); } - await workbook.checkPermission({ - parentIds, - permission: WorkbookPermission.Delete, - }); + workbooksMap.set(workbook, parentIds); + + if (!skipCheckPermissions) { + await workbook.checkPermission({ + parentIds, + permission: WorkbookPermission.Delete, + }); + } } }); await Promise.all(checkDeletePermissionPromises); const result = await transaction(targetTrx, async (transactionTrx) => { - const deletedWorkbooks = await WorkbookModel.query(transactionTrx) - .patch({ - [WorkbookModelColumn.DeletedBy]: userId, - [WorkbookModelColumn.DeletedAt]: raw(CURRENT_TIMESTAMP), - }) - .whereIn([WorkbookModelColumn.WorkbookId], workbookIds) - .returning('*') - .timeout(WorkbookModel.DEFAULT_QUERY_TIMEOUT); + const deletedWorkbooks = await markWorkbooksAsDeleted( + {ctx, trx, skipCheckPermissions: true}, + {workbooksMap}, + ); const entries = await Entry.query(transactionTrx) .select() diff --git a/src/services/new/workbook/get-workbooks-list-by-ids.ts b/src/services/new/workbook/get-workbooks-list-by-ids.ts index 04b79c4f..7317d85b 100644 --- a/src/services/new/workbook/get-workbooks-list-by-ids.ts +++ b/src/services/new/workbook/get-workbooks-list-by-ids.ts @@ -5,7 +5,7 @@ import {getReplica} from '../utils'; import Utils from '../../../utils'; import {registry} from '../../../registry'; -import {getParents} from '../collection/utils'; +import {getParents, getParentsIdsFromMap} from '../collection/utils'; import {WorkbookPermission} from '../../../entities/workbook'; @@ -32,24 +32,6 @@ type GetWorkbooksListAndAllParentsArgs = { includePermissionsInfo?: boolean; }; -export const getParentsIdsFromMap = ( - collectionId: string | null, - parentsMap: Map>, -): string[] => { - let id: Nullable = collectionId; - const arr: string[] = id ? [id] : []; - - while (id !== null) { - const curr: Nullable = parentsMap.get(id) || null; - - if (curr) arr.push(curr); - - id = curr; - } - - return arr; -}; - export const getWorkbooksListByIds = async ( {ctx, trx, skipValidation = false, skipCheckPermissions = false}: ServiceArgs, args: GetWorkbooksListAndAllParentsArgs, diff --git a/src/services/new/workbook/utils/index.ts b/src/services/new/workbook/utils/index.ts index 7b0ac3fb..87b60d6f 100644 --- a/src/services/new/workbook/utils/index.ts +++ b/src/services/new/workbook/utils/index.ts @@ -1,2 +1,3 @@ export * from './get-entry-permissions-by-workbook'; export * from './check-workbook-permission'; +export * from './mark-workbooks-as-deleted'; diff --git a/src/services/new/workbook/utils/mark-workbooks-as-deleted.ts b/src/services/new/workbook/utils/mark-workbooks-as-deleted.ts new file mode 100644 index 00000000..f31a29b3 --- /dev/null +++ b/src/services/new/workbook/utils/mark-workbooks-as-deleted.ts @@ -0,0 +1,45 @@ +import {raw} from 'objection'; +import {CURRENT_TIMESTAMP} from '../../../../const'; +import {ServiceArgs} from '../../types'; +import {getPrimary} from '../../utils'; +import {WorkbookInstance} from '../../../../registry/common/entities/workbook/types'; +import {WorkbookModel, WorkbookModelColumn} from '../../../../db/models/new/workbook'; + +export const markWorkbooksAsDeleted = async ( + {ctx, trx, skipCheckPermissions}: ServiceArgs, + {workbooksMap}: {workbooksMap: Map}, +) => { + const workbookIds: string[] = []; + + workbooksMap.forEach((parentIds, workbookInstance) => { + workbookIds.push(workbookInstance.model.workbookId); + }); + + const { + user: {userId}, + } = ctx.get('info'); + + const workbooks = await WorkbookModel.query(getPrimary(trx)) + .patch({ + [WorkbookModelColumn.DeletedBy]: userId, + [WorkbookModelColumn.DeletedAt]: raw(CURRENT_TIMESTAMP), + }) + .whereIn([WorkbookModelColumn.WorkbookId], workbookIds) + .returning('*') + .timeout(WorkbookModel.DEFAULT_QUERY_TIMEOUT); + + const deletePermissionsPromises: Promise[] = []; + + workbooksMap.forEach((parentIds, workbookInstance) => { + deletePermissionsPromises.push( + workbookInstance.deletePermissions({ + parentIds, + skipCheckPermissions, + }), + ); + }); + + await Promise.all(deletePermissionsPromises); + + return workbooks; +};