From ebc13cefb0dde6ea170fc5d2b5abb53b7d68c5bc Mon Sep 17 00:00:00 2001 From: Sergei Samokhvalov Date: Tue, 1 Oct 2024 15:58:06 +0300 Subject: [PATCH] Add check parent folder existence before restore entry (#185) * Add check parent folder existence before restore entry * Add method to utils getFullParentFolderKeys, add unit tests * Rm logs * Fix name test * Fix after review * Fix after review * Refactor * fix --- src/services/entry/actions/update-entry.ts | 60 ++++++++++++++++++++-- src/tests/unit/utils/utils.test.ts | 56 +++++++++++++++++--- src/utils/utils.ts | 26 ++++++++++ 3 files changed, 131 insertions(+), 11 deletions(-) diff --git a/src/services/entry/actions/update-entry.ts b/src/services/entry/actions/update-entry.ts index a0d658ae..c2436a08 100644 --- a/src/services/entry/actions/update-entry.ts +++ b/src/services/entry/actions/update-entry.ts @@ -27,6 +27,8 @@ import {WorkbookPermission} from '../../../entities/workbook'; import {Optional} from 'utility-types'; import {checkEntry} from './check-entry'; import {registry} from '../../../registry'; +import {EntryScope} from '../../../db/models/new/entry/types'; +import {EntryColumn} from '../../../db/models/new/entry'; type Mode = 'save' | 'publish' | 'recover'; const ModeValues: Mode[] = ['save', 'publish', 'recover']; @@ -405,9 +407,61 @@ export async function updateEntry(ctx: CTX, updateData: UpdateEntryData) { const entryObj: EntryColumns | undefined = entry ? entry.toJSON() : undefined; if (entryObj) { + const entryTenantId = entryObj.tenantId; + + const entryObjInnerMeta = entryObj.innerMeta as NonNullable< + EntryColumns['innerMeta'] + >; + const newKey = entryObjInnerMeta.oldKey; + + if (!entryObj.workbookId) { + const keyLowerCase = newKey.toLowerCase(); + const isFolder = Utils.isFolder({scope: entryObj.scope}); + const keyFormatted = Utils.formatKey(keyLowerCase, isFolder); + + const parentFolderKeys = Utils.getFullParentFolderKeys( + keyFormatted, + ).filter((key) => !Utils.isRoot(key)); + + if (parentFolderKeys.length) { + const parentFolders = await Entry.query(trx) + .where({ + [EntryColumn.TenantId]: entryTenantId, + [EntryColumn.Scope]: EntryScope.Folder, + [EntryColumn.IsDeleted]: false, + }) + .whereIn(EntryColumn.Key, parentFolderKeys) + .timeout(Entry.DEFAULT_QUERY_TIMEOUT); + + const existingKeysParentFolders = new Set(); + + const notFoundParentFolders: string[] = []; + + parentFolders.forEach((folder) => { + existingKeysParentFolders.add(folder.key); + }); + + parentFolderKeys.forEach((key) => { + if (!existingKeysParentFolders.has(key)) { + notFoundParentFolders.push(key); + } + }); + + if (notFoundParentFolders.length) { + throw new AppError( + `Couldn't found these parent folders - '${notFoundParentFolders.join( + ', ', + )}'`, + { + code: US_ERRORS.PARENT_FOLDER_NOT_EXIST, + }, + ); + } + } + } + if (entryObj.scope === 'folder') { const entryObjKey = entryObj.key; - const entryTenantId = entryObj.tenantId; const children = await Entry.query(trx) .select() @@ -445,10 +499,6 @@ export async function updateEntry(ctx: CTX, updateData: UpdateEntryData) { }), ); } else { - const entryObjInnerMeta = entryObj.innerMeta as NonNullable< - EntryColumns['innerMeta'] - >; - const newKey = entryObjInnerMeta.oldKey; const newDisplayKey = entryObjInnerMeta.oldDisplayKey; const newInnerMeta: Optional = entryObjInnerMeta; diff --git a/src/tests/unit/utils/utils.test.ts b/src/tests/unit/utils/utils.test.ts index 8c1b2b2a..d0f49aa6 100644 --- a/src/tests/unit/utils/utils.test.ts +++ b/src/tests/unit/utils/utils.test.ts @@ -2,14 +2,14 @@ import Utils from '../../../utils'; describe('Utils', () => { describe('Utils.getCopyNumber', () => { - test('Shoud return 0 if name does not have prefix COPY', () => { + test('Should return 0 if name does not have prefix COPY', () => { const name = 'DASH'; const copyNameNumber = Utils.getCopyNumber(name); expect(copyNameNumber).toBe(0); }); - test('Shoud return number if name has prefix COPY', () => { + test('Should return number if name has prefix COPY', () => { const name = 'DASH (COPY 7)'; const copyNameNumber = Utils.getCopyNumber(name); @@ -18,14 +18,14 @@ describe('Utils', () => { }); describe('Utils.setCopyNumber', () => { - test('Shoud return name without prefix COPY, if count === 0', () => { + test('Should return name without prefix COPY, if count === 0', () => { const name = 'DASH'; const replacedCopyNumber = Utils.setCopyNumber(name, 0); expect(replacedCopyNumber).toBe('DASH'); }); - test('Shoud increment counter if name has prefix COPY', () => { + test('Should increment counter if name has prefix COPY', () => { const name = 'DASH'; const replacedCopyNumber = Utils.setCopyNumber(name, 1); @@ -34,16 +34,60 @@ describe('Utils', () => { }); describe('Utils.getNameWithoutCopyNumber', () => { - test('Shoud return name without prefix, if name has prefix', () => { + test('Should return name without prefix, if name has prefix', () => { const nameWithoutCopy = Utils.getNameWithoutCopyNumber('DASH (COPY 7)'); expect(nameWithoutCopy).toBe('DASH'); }); - test('Shoud return name without prefix, if name has not prefix', () => { + test('Should return name without prefix, if name has not prefix', () => { const nameWithoutCopy = Utils.getNameWithoutCopyNumber('DASH'); expect(nameWithoutCopy).toBe('DASH'); }); }); + + describe('Utils.getFullParentFolderKeys', () => { + test('Should return all full parent folder keys', () => { + const parentFolderKeys = Utils.getFullParentFolderKeys( + 'foldername/nestedfolder/nestedfolder2/nestedfolder3', + ); + + expect(parentFolderKeys).toEqual([ + 'foldername/nestedfolder/nestedfolder2/', + 'foldername/nestedfolder/', + 'foldername/', + ]); + }); + + test('Should return parent folder, if the entity does not have / at the end ', () => { + const parentFolderKeys = Utils.getFullParentFolderKeys('entries-basic-tests/dataset'); + + expect(parentFolderKeys).toEqual(['entries-basic-tests/']); + }); + + test('Should return root folder, if no parent folder', () => { + const parentFolderKeys = Utils.getFullParentFolderKeys('foldername'); + + expect(parentFolderKeys).toEqual(['/']); + }); + + test('Should return root folder, when only / in input', () => { + const parentFolderKeys = Utils.getFullParentFolderKeys('/'); + + expect(parentFolderKeys).toEqual(['/']); + }); + + test('Should return root folder, when only 1 entity in string', () => { + const parentFolderKeys = Utils.getFullParentFolderKeys('foldername/'); + + expect(parentFolderKeys).toEqual(['/']); + }); + + test('Should return empty array, when input empty string', () => { + const parentFolderKeys = Utils.getFullParentFolderKeys(''); + + expect(parentFolderKeys).toEqual([]); + }); + }); }); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index f8c7cbeb..3d1fdc54 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -72,6 +72,32 @@ export class Utils { return parentFolderKey ? parentFolderKey : '/'; } + static getFullParentFolderKeys(keyFormatted = '') { + let parentFolderKey = keyFormatted; + + const parentFolderKeys = []; + + while (parentFolderKey.length) { + const newParentFolderKey = Utils.getParentFolderKey({ + keyFormatted: parentFolderKey, + }); + + if (newParentFolderKey) { + parentFolderKey = newParentFolderKey; + + parentFolderKeys.push(newParentFolderKey); + + if (newParentFolderKey.split('/').length <= 2) { + break; + } + } else { + break; + } + } + + return parentFolderKeys; + } + static isRoot(key: string | undefined) { return key === '/'; }