Skip to content

Commit

Permalink
Add new handlers to get workbooks and collections list by ids (#210)
Browse files Browse the repository at this point in the history
* Add new handler getWorkbooksListByIds

* Add route getCollectionsListByIds

* Add int tests

* Change test

* Refactor

* Refactor getCollectionsListByIds service

* Add additional tests

* Change route name

* Add features
  • Loading branch information
Sergey-weber authored Nov 25, 2024
1 parent fed7145 commit 030b6aa
Show file tree
Hide file tree
Showing 9 changed files with 339 additions and 25 deletions.
16 changes: 16 additions & 0 deletions src/controllers/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
OrderDirection,
Mode,
deleteCollections,
getCollectionsListByIds,
} from '../services/new/collection';
import {
formatCollectionModel,
Expand Down Expand Up @@ -65,6 +66,21 @@ export default {
res.status(code).send(response);
},

getCollectionsListByIds: async (req: Request, res: Response) => {
const {body} = req;

const result = await getCollectionsListByIds(
{ctx: req.ctx},
{
collectionIds: body.collectionIds,
},
);

const formattedResponse = result.map((instance) => formatCollectionModel(instance.model));
const {code, response} = await prepareResponseAsync({data: formattedResponse});
res.status(code).send(response);
},

/**
* @deprecated for structureItemsController.getStructureItems,
* @todo remove, after successful deploy with UI.
Expand Down
16 changes: 16 additions & 0 deletions src/controllers/workbooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
formatRestoreWorkbook,
formatGetWorkbookContent,
} from '../services/new/workbook/formatters';
import {getWorkbooksListByIds} from '../services/new/workbook/get-workbooks-list-by-ids';

export default {
create: async (req: Request, res: Response) => {
Expand Down Expand Up @@ -112,6 +113,21 @@ export default {
res.status(code).send(response);
},

getWorkbooksListByIds: async (req: Request, res: Response) => {
const {body} = req;

const result = await getWorkbooksListByIds(
{ctx: req.ctx},
{
workbookIds: body.workbookIds,
},
);

const formattedResponse = result.map((instance) => formatWorkbookModel(instance.model));
const {code, response} = await prepareResponseAsync({data: formattedResponse});
res.status(code).send(response);
},

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

Expand Down
10 changes: 10 additions & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,11 @@ export function getRoutes(_nodekit: NodeKit, options: GetRoutesOptions) {
handler: workbooksController.getList,
features: [Feature.CollectionsEnabled],
}),
getWorkbooksListByIds: makeRoute({
route: 'POST /v2/workbooks-get-list-by-ids',
handler: workbooksController.getWorkbooksListByIds,
features: [Feature.CollectionsEnabled],
}),
updateWorkbook: makeRoute({
route: 'POST /v2/workbooks/:workbookId/update',
handler: workbooksController.update,
Expand Down Expand Up @@ -404,6 +409,11 @@ export function getRoutes(_nodekit: NodeKit, options: GetRoutesOptions) {
private: true,
features: [Feature.CollectionsEnabled],
}),
getCollectionsListByIds: makeRoute({
route: 'POST /v1/collections-get-list-by-ids',
handler: collectionsController.getCollectionsListByIds,
features: [Feature.CollectionsEnabled],
}),
getCollectionContent: makeRoute({
route: 'GET /v1/collection-content',
handler: collectionsController.getContent,
Expand Down
24 changes: 20 additions & 4 deletions src/services/new/collection/delete-collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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 {makeCollectionsWithParentsMap} from './utils';
import {makeCollectionsWithParentsMap, checkAndSetCollectionPermission} from './utils';

const validateArgs = makeSchemaValidator({
type: 'object',
Expand Down Expand Up @@ -46,9 +46,25 @@ export const deleteCollections = async (

const targetTrx = getPrimary(trx);

await getCollectionsListByIds(
{ctx, trx: getReplica(trx), skipValidation, skipCheckPermissions},
{collectionIds, permission: CollectionPermission.Delete},
const collectionsInstances = await getCollectionsListByIds(
{ctx, trx: getReplica(trx), skipValidation, skipCheckPermissions: true},
{collectionIds},
);

await Promise.all(
collectionsInstances.map(async (collectionInstance) => {
const collection = await checkAndSetCollectionPermission(
{ctx, trx},
{
collectionInstance,
skipCheckPermissions,
includePermissionsInfo: false,
permission: CollectionPermission.Delete,
},
);

return collection;
}),
);

const recursiveName = 'collectionChildren';
Expand Down
81 changes: 62 additions & 19 deletions src/services/new/collection/get-collections-list-by-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ import {ServiceArgs} from '../types';
import {getReplica} from '../utils';
import {makeSchemaValidator} from '../../../components/validation-schema-compiler';
import {CollectionModel, CollectionModelColumn} from '../../../db/models/new/collection';
import {CollectionPermission} from '../../../entities/collection';
import Utils from '../../../utils';
import {checkAndSetCollectionPermission} from './utils';
import {makeCollectionsWithParentsMap} from './utils';
import {Feature, isEnabledFeature} from '../../../components/features';
import {CollectionPermission} from '../../../entities/collection';
import {CollectionInstance} from '../../../registry/common/entities/collection/types';

const validateArgs = makeSchemaValidator({
type: 'object',
required: ['collectionIds'],
properties: {
collectionIds: {
type: 'array',
minItems: 1,
maxItems: 1000,
items: {type: 'string'},
},
includePermissionsInfo: {
Expand All @@ -26,14 +30,13 @@ const validateArgs = makeSchemaValidator({
export interface GetCollectionsListByIdsArgs {
collectionIds: string[];
includePermissionsInfo?: boolean;
permission?: CollectionPermission;
}

export const getCollectionsListByIds = async (
{ctx, trx, skipValidation = false, skipCheckPermissions = false}: ServiceArgs,
args: GetCollectionsListByIdsArgs,
) => {
const {collectionIds, includePermissionsInfo = false, permission} = args;
const {collectionIds, includePermissionsInfo = false} = args;

ctx.log('GET_COLLECTIONS_LIST_BY_IDS_START', {
collectionIds: await Utils.macrotasksMap(collectionIds, (id) => Utils.encodeId(id)),
Expand All @@ -56,30 +59,70 @@ export const getCollectionsListByIds = async (
.whereIn(CollectionModelColumn.CollectionId, collectionIds)
.timeout(CollectionModel.DEFAULT_QUERY_TIMEOUT);

const modelsWithPermissions = await Promise.all(
models.map(async (model) => {
const {Collection} = registry.common.classes.get();
const {accessServiceEnabled} = ctx.config;

const {Collection} = registry.common.classes.get();

const collectionInstance = new Collection({
ctx,
model,
if (!accessServiceEnabled || skipCheckPermissions) {
if (includePermissionsInfo) {
return models.map((model) => {
const collection = new Collection({ctx, model});
collection.enableAllPermissions();
return collection;
});
}

return models.map((model) => new Collection({ctx, model}));
}

const collectionsMap = await makeCollectionsWithParentsMap({ctx, trx}, {models});
const acceptedCollectionsMap = new Map<CollectionModel, string[]>();

const checkPermissionPromises: Promise<CollectionInstance | void>[] = [];

collectionsMap.forEach((parentIds, collection) => {
const promise = collection
.checkPermission({
parentIds,
permission: isEnabledFeature(ctx, Feature.UseLimitedView)
? CollectionPermission.LimitedView
: CollectionPermission.View,
})
.then(() => {
acceptedCollectionsMap.set(collection.model, parentIds);

return collection;
})
.catch(() => {});

const collection = await checkAndSetCollectionPermission(
{ctx, trx},
{collectionInstance, skipCheckPermissions, includePermissionsInfo, permission},
);
checkPermissionPromises.push(promise);
});

let collections = await Promise.all(checkPermissionPromises);

if (includePermissionsInfo) {
const {bulkFetchCollectionsAllPermissions} = registry.common.functions.get();

const mappedCollections: {model: CollectionModel; parentIds: string[]}[] = [];

acceptedCollectionsMap.forEach((parentIds, collectionModel) => {
mappedCollections.push({
model: collectionModel,
parentIds,
});
});

collections = await bulkFetchCollectionsAllPermissions(ctx, mappedCollections);
}

return collection;
}),
);
const result = collections.filter((item) => Boolean(item)) as CollectionInstance[];

ctx.log('GET_COLLECTIONS_LIST_BY_IDS_FINISH', {
collectionIds: await Utils.macrotasksMap(
models.map((item) => item.collectionId),
result.map((item) => item.model.collectionId),
(id) => Utils.encodeId(id),
),
});

return modelsWithPermissions;
return result;
};
7 changes: 5 additions & 2 deletions src/services/new/workbook/get-workbooks-list-by-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ const validateArgs = makeSchemaValidator({
type: 'object',
required: ['workbookIds'],
properties: {
workbookId: {
type: ['array', 'string'],
workbookIds: {
type: 'array',
minItems: 1,
maxItems: 1000,
items: {type: 'string'},
},
includePermissionsInfo: {
type: 'boolean',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import request from 'supertest';
import {app, auth, getCollectionBinding, US_ERRORS} from '../../auth';
import {createMockCollection} from '../../helpers';
import {routes} from '../../../../routes';
import {COLLECTIONS_DEFAULT_FIELDS} from '../../../../models';

const rootCollection = {
collectionId: '',
title: 'Empty root collection',
};

const rootCollection2 = {
collectionId: '',
title: 'Empty root collection 2',
};

describe('Setup', () => {
test('Create collections', async () => {
const collection = await createMockCollection({
title: rootCollection.title,
parentId: null,
});
rootCollection.collectionId = collection.collectionId;

const collection2 = await createMockCollection({
title: rootCollection2.title,
parentId: null,
});
rootCollection2.collectionId = collection2.collectionId;
});
});

describe('Get collections by ids', () => {
test('Auth error', async () => {
await request(app)
.post(routes.getCollectionsListByIds)
.send({
collectionIds: [rootCollection.collectionId, rootCollection2.collectionId],
})
.expect(401);
});

test('Get list without permissions, should return empty list', async () => {
const response = await auth(request(app).post(routes.getCollectionsListByIds))
.send({
collectionIds: [rootCollection.collectionId, rootCollection2.collectionId],
})
.expect(200);

expect(response.body).toStrictEqual([]);
});

test('Get list with permission only 1 collection, should return 1 collection', async () => {
const response = await auth(request(app).post(routes.getCollectionsListByIds), {
accessBindings: [getCollectionBinding(rootCollection.collectionId, 'limitedView')],
})
.send({
collectionIds: [rootCollection.collectionId, rootCollection.collectionId],
})
.expect(200);

expect(response.body).toStrictEqual([
{
...COLLECTIONS_DEFAULT_FIELDS,
collectionId: rootCollection.collectionId,
},
]);
});

test('Get list without ids, should be a validation error', async () => {
const response = await auth(request(app).post(routes.getCollectionsListByIds), {
accessBindings: [
getCollectionBinding(rootCollection.collectionId, 'limitedView'),
getCollectionBinding(rootCollection2.collectionId, 'limitedView'),
],
}).expect(400);

expect(response.body.code).toBe(US_ERRORS.VALIDATION_ERROR);
});

test('Successfully get list by ids', async () => {
const response = await auth(request(app).post(routes.getCollectionsListByIds), {
accessBindings: [
getCollectionBinding(rootCollection.collectionId, 'limitedView'),
getCollectionBinding(rootCollection2.collectionId, 'limitedView'),
],
})
.send({
collectionIds: [rootCollection.collectionId, rootCollection2.collectionId],
})
.expect(200);

expect(response.body).toStrictEqual([
{
...COLLECTIONS_DEFAULT_FIELDS,
collectionId: rootCollection.collectionId,
},
{
...COLLECTIONS_DEFAULT_FIELDS,
collectionId: rootCollection2.collectionId,
},
]);
});
});
Loading

0 comments on commit 030b6aa

Please sign in to comment.