Skip to content

Commit

Permalink
Add private route for restore workbook (#31)
Browse files Browse the repository at this point in the history
* add new route, controller for restore workbook

* Added where DeletedAt depends on workbook deletedAt

* Add int tests

* Add praparing for test

* Fix test

* Fix test

* Add deleting workbook test

* Fix test deleting

* Add test for getting entries workbook after restore

* Add new error - WORKBOOK_IS_ALREADY_RESTORED

* Fix after review
  • Loading branch information
Sergey-weber authored Dec 7, 2023
1 parent 8c83579 commit ce1cb0e
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/components/error-response-presenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,14 @@ export default (error: AppError | DBError) => {
},
};
}
case US_ERRORS.WORKBOOK_IS_ALREADY_RESTORED: {
return {
code: 400,
response: {
message: 'The workbook is alredy restored',
},
};
}
case US_ERRORS.WORKBOOK_ENTITY_ERROR: {
return {
code: 500,
Expand Down
1 change: 1 addition & 0 deletions src/const/us-error-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const US_ERRORS = {
COLLECTION_NOT_EXISTS: 'COLLECTION_NOT_EXISTS',
COLLECTION_CIRCULAR_REFERENCE_ERROR: 'COLLECTION_CIRCULAR_REFERENCE_ERROR',
WORKBOOK_NOT_EXISTS: 'WORKBOOK_NOT_EXISTS',
WORKBOOK_IS_ALREADY_RESTORED: 'WORKBOOK_IS_ALREADY_RESTORED',
WORKBOOK_ENTITY_ERROR: 'WORKBOOK_ENTITY_ERROR',
WORKBOOK_COPY_FILE_CONNECTION_ERROR: 'WORKBOOK_COPY_FILE_CONNECTION_ERROR',
WORKBOOK_OPERATION_FORBIDDEN: 'WORKBOOK_OPERATION_FORBIDDEN',
Expand Down
19 changes: 19 additions & 0 deletions src/controllers/workbooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getAllWorkbooks,
OrderField,
OrderDirection,
restoreWorkbook,
} from '../services/new/workbook';
import {
formatWorkbookModel,
Expand All @@ -25,6 +26,7 @@ import {
formatWorkbookModelWithOperation,
formatWorkbooksList,
formatSetWorkbookIsTemplate,
formatRestoreWorkbook,
} from '../services/new/workbook/formatters';

export default {
Expand Down Expand Up @@ -260,4 +262,21 @@ export default {
const {code, response} = prepareResponse({data: formattedResponse});
res.status(code).send(response);
},

restore: async (req: Request, res: Response) => {
const {params} = req;

const result = await restoreWorkbook(
{
ctx: req.ctx,
},
{
workbookId: params.workbookId,
},
);

const formattedResponse = formatRestoreWorkbook(result);
const {code, response} = prepareResponse({data: formattedResponse});
res.status(code).send(response);
},
};
8 changes: 8 additions & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,14 @@ export function getRoutes(nodekit: NodeKit, options: GetRoutesOptions) {
write: true,
}),

privateRestoreWorkbook: makeRoute({
route: 'POST /private/v2/workbooks/:workbookId/restore',
handler: workbooksController.restore,
authPolicy: AuthPolicy.disabled,
private: true,
write: true,
}),

privateGetAllWorkbooks: makeRoute({
route: 'GET /private/all-workbooks',
handler: workbooksController.getAll,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {WorkbookModel} from '../../../../db/models/new/workbook';

export const formatRestoreWorkbook = (workbookModel: WorkbookModel) => {
return {
workbookId: workbookModel.workbookId,
};
};
1 change: 1 addition & 0 deletions src/services/new/workbook/formatters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './format-workbook-model';
export * from './format-workbook-model-with-operation';
export * from './format-workbook-models-list';
export * from './format-set-workbook-is-template';
export * from './format-restore-workbook';
1 change: 1 addition & 0 deletions src/services/new/workbook/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './set-workbook-is-template';
export * from './copy-workbook';
export * from './copy-workbook-template';
export * from './get-all-workbooks';
export * from './restore-workbook';
112 changes: 112 additions & 0 deletions src/services/new/workbook/restore-workbook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {transaction, raw} from 'objection';
import {AppError} from '@gravity-ui/nodekit';
import {ServiceArgs} from '../types';
import {getReplica, getPrimary} from '../utils';
import {makeSchemaValidator} from '../../../components/validation-schema-compiler';
import {WorkbookModel, WorkbookModelColumn} from '../../../db/models/new/workbook';
import Utils, {logInfo} from '../../../utils';
import {Entry, EntryColumn} from '../../../db/models/new/entry';

import {US_ERRORS, CURRENT_TIMESTAMP, DEFAULT_QUERY_TIMEOUT, TRASH_FOLDER} from '../../../const';

const validateArgs = makeSchemaValidator({
type: 'object',
required: ['workbookId'],
properties: {
workbookId: {
type: 'string',
},
},
});

export interface RestoreWorkbookArgs {
workbookId: string;
}

export const restoreWorkbook = async (
{ctx, trx, skipValidation = false}: ServiceArgs,
args: RestoreWorkbookArgs,
) => {
const {workbookId} = args;

logInfo(ctx, 'RESTORE_WORKBOOK_START', {
workbookId: Utils.encodeId(workbookId),
});

if (!skipValidation) {
validateArgs(args);
}

const {tenantId} = ctx.get('info');

const targetTrx = getReplica(trx);

const model = await WorkbookModel.query(targetTrx)
.select()
.where({
[WorkbookModelColumn.TenantId]: tenantId,
[WorkbookModelColumn.WorkbookId]: workbookId,
})
.first()
.timeout(WorkbookModel.DEFAULT_QUERY_TIMEOUT);

if (!model) {
throw new AppError(US_ERRORS.WORKBOOK_NOT_EXISTS, {
code: US_ERRORS.WORKBOOK_NOT_EXISTS,
});
}

if (model.deletedAt === null) {
throw new AppError(US_ERRORS.WORKBOOK_IS_ALREADY_RESTORED, {
code: US_ERRORS.WORKBOOK_IS_ALREADY_RESTORED,
});
}

const primaryTrx = getPrimary(trx);

const result = await transaction(primaryTrx, async (transactionTrx) => {
const restoredWorkbook = await WorkbookModel.query(transactionTrx)
.patch({
[WorkbookModelColumn.DeletedBy]: null,
[WorkbookModelColumn.DeletedAt]: null,
[WorkbookModelColumn.UpdatedAt]: raw(CURRENT_TIMESTAMP),
})
.where({
[WorkbookModelColumn.WorkbookId]: model.workbookId,
[WorkbookModelColumn.TenantId]: tenantId,
})
.returning('*')
.first()
.timeout(WorkbookModel.DEFAULT_QUERY_TIMEOUT);

await Entry.query(transactionTrx)
.patch({
key: raw(`regexp_replace(key, '${TRASH_FOLDER}/', '')`),
displayKey: raw(`regexp_replace(display_key, '${TRASH_FOLDER}/', '')`),
innerMeta: raw(`inner_meta - 'oldKey' - 'oldDisplayKey'`),
isDeleted: false,
deletedAt: null,
updatedAt: raw(CURRENT_TIMESTAMP),
})
.where({
[EntryColumn.WorkbookId]: workbookId,
[EntryColumn.TenantId]: tenantId,
})
.andWhere(EntryColumn.DeletedAt, '>=', model.deletedAt)
.timeout(DEFAULT_QUERY_TIMEOUT);

return restoredWorkbook;
});

if (!result) {
throw new AppError(US_ERRORS.WORKBOOK_NOT_EXISTS, {
code: US_ERRORS.WORKBOOK_NOT_EXISTS,
});
}

ctx.log('RESTORE_WORKBOOK_FINISH', {
workbookId: Utils.encodeId(workbookId),
});

return result;
};
81 changes: 81 additions & 0 deletions src/tests/int/common/workbooks.private.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,84 @@ describe('Private Entries in workboooks managment', () => {
await withScopeHeaders(request(app).delete(`/v2/workbooks/${testWorkbookId}`)).expect(200);
});
});

describe('Private for one workboook managment', () => {
test('Create workbook with entries – [POST /private/v2/workbooks]', async () => {
const testTitle = 'Test private workbook with entries title';
const testDescription = 'Test private workbook with entries description';

const entry1Name = 'Entry in test workbook 1';

const responseWorkbook = await withScopeHeaders(request(app).post('/private/v2/workbooks'))
.set({[US_MASTER_TOKEN_HEADER]: usApp.config.masterToken})
.send({
title: testTitle,
description: testDescription,
})
.expect(200);

const {body: bodyWorkbook} = responseWorkbook;

testWorkbookId = bodyWorkbook.workbookId;

await withScopeHeaders(request(app).post('/v1/entries'))
.send({
scope: 'dataset',
type: 'graph',
meta: {},
data: {},
name: entry1Name,
workbookId: testWorkbookId,
})
.expect(200);
});

test('Restore workbook with entries – [POST /private/v2/workbooks/:workbookId/restore]', async () => {
await withScopeHeaders(request(app).delete(`/v2/workbooks/${testWorkbookId}`)).expect(200);

await request(app).post(`/private/v2/workbooks/${testWorkbookId}/restore`).expect(403);

const response = await withScopeHeaders(
request(app)
.post(`/private/v2/workbooks/${testWorkbookId}/restore`)
.set({[US_MASTER_TOKEN_HEADER]: usApp.config.masterToken}),
).expect(200);

const {body} = response;

expect(body).toStrictEqual({
workbookId: testWorkbookId,
});

const responseEntries = await withScopeHeaders(
request(app)
.get(`/private/v2/workbooks/${testWorkbookId}/entries`)
.set({[US_MASTER_TOKEN_HEADER]: usApp.config.masterToken}),
).expect(200);

const {body: bodyEntries} = responseEntries;

expect(bodyEntries).toStrictEqual({
entries: expect.arrayContaining([
{
createdAt: expect.any(String),
createdBy: expect.any(String),
entryId: expect.any(String),
hidden: false,
isLocked: false,
key: expect.any(String),
meta: {},
publishedId: null,
savedId: expect.any(String),
scope: 'dataset',
type: 'graph',
updatedAt: expect.any(String),
updatedBy: expect.any(String),
workbookId: testWorkbookId,
},
]),
});

await withScopeHeaders(request(app).delete(`/v2/workbooks/${testWorkbookId}`)).expect(200);
});
});

0 comments on commit ce1cb0e

Please sign in to comment.