Skip to content

Commit

Permalink
Add a feature to copy entries to workbooks (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
bt4R9 authored Nov 1, 2023
1 parent 40d472a commit d7f4d09
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 7 deletions.
10 changes: 8 additions & 2 deletions src/components/middlewares/decode-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ export const decodeId = (req: Request, _res: Response, next: NextFunction) => {
}

if (req.body && req.body[idVariable]) {
const encodedId = req.body[idVariable];
req.body[idVariable] = Utils.decodeId(encodedId);
const entity = req.body[idVariable] as string | string[];

if (Array.isArray(entity)) {
req.body[idVariable] = entity.map((encodedId) => Utils.decodeId(encodedId));
} else {
const encodedId = req.body[idVariable];
req.body[idVariable] = Utils.decodeId(encodedId);
}
}
}
} catch {
Expand Down
1 change: 1 addition & 0 deletions src/const/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export const ALLOWED_SCOPE_VALUES = [
export const ID_VARIABLES = [
'ids',
'entryId',
'entryIds',
'oldEntryId',
'revId',
'draftId',
Expand Down
16 changes: 16 additions & 0 deletions src/controllers/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getEntryMetaPrivate,
GetEntryMetaPrivateArgs,
copyEntryToWorkbook,
copyEntriesToWorkbook,
} from '../services/new/entry';
import {
formatGetEntryResponse,
Expand Down Expand Up @@ -241,6 +242,21 @@ export default {
res.status(code).send(response);
},

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

const result = await copyEntriesToWorkbook(
{ctx: req.ctx},
{
entryIds: body.entryIds,
workbookId: body.workbookId,
},
);

const {code, response} = prepareResponse({data: result});
res.status(code).send(response);
},

getEntries: async (req: Request, res: Response) => {
const query = req.query as unknown as ST.GetEntries;

Expand Down
6 changes: 6 additions & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,12 @@ export function getRoutes(nodekit: NodeKit, options: GetRoutesOptions) {
handler: entriesController.copyEntryToWorkbook,
write: true,
}),

copyEntriesToWorkbook: makeRoute({
route: 'POST /v2/copy-entries',
handler: entriesController.copyEntriesToWorkbook,
write: true,
}),
};
}

Expand Down
53 changes: 49 additions & 4 deletions src/services/entry/actions/copy-to-workbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {TransactionOrKnex} from 'objection';
import {AppError} from '@gravity-ui/nodekit';

import {getId} from '../../../db';
import {Entry} from '../../../db/models/new/entry';
import {Entry, EntryColumn, EntryScope, EntryType} from '../../../db/models/new/entry';
import {JoinedEntryRevision} from '../../../db/presentations/joined-entry-revision';
import {WorkbookModel} from '../../../db/models/new/workbook';
import {CTX} from '../../../types/models';
Expand All @@ -15,6 +15,7 @@ import {getParentIds} from '../../new/collection/utils/get-parents';
import Link from '../../../db/models/links';

import {makeSchemaValidator} from '../../../components/validation-schema-compiler';
import {resolveEntriesNameCollisions} from '../../new/entry/utils/resolveNameCollisions';

interface Params {
entryIds: Entry['entryId'][];
Expand All @@ -23,6 +24,7 @@ interface Params {
trxOverride?: TransactionOrKnex;
skipLinkSync?: boolean;
skipWorkbookPermissionsCheck?: boolean;
resolveNameCollisions?: boolean;
}

export const validateParams = makeSchemaValidator({
Expand All @@ -45,9 +47,14 @@ export const validateParams = makeSchemaValidator({
skipWorkbookPermissionsCheck: {
type: 'boolean',
},
resolveNameCollisions: {
type: 'boolean',
},
},
});

const fileConnectionTypes: string[] = [EntryType.File, EntryType.GsheetsV2];

export const copyToWorkbook = async (ctx: CTX, params: Params) => {
const {
entryIds,
Expand All @@ -56,6 +63,7 @@ export const copyToWorkbook = async (ctx: CTX, params: Params) => {
trxOverride,
skipLinkSync,
skipWorkbookPermissionsCheck = false,
resolveNameCollisions,
} = params;

logInfo(ctx, 'COPY_ENTRY_TO_WORKBOOK_CALL', {
Expand Down Expand Up @@ -118,6 +126,21 @@ export const copyToWorkbook = async (ctx: CTX, params: Params) => {
);
}

const isFileConnection =
joinedEntryRevision.scope === EntryScope.Connection &&
fileConnectionTypes.includes(joinedEntryRevision.type);

if (isFileConnection) {
throw new AppError(
`Entry ${Utils.encodeId(
joinedEntryRevision.entryId,
)} is a file connection and cannot be copied to a workbook.`,
{
code: US_ERRORS.WORKBOOK_COPY_FILE_CONNECTION_ERROR,
},
);
}

if (workbookId === undefined) {
workbookId = joinedEntryRevision.workbookId;
} else if (joinedEntryRevision.workbookId !== workbookId) {
Expand All @@ -138,6 +161,25 @@ export const copyToWorkbook = async (ctx: CTX, params: Params) => {
});
}

let entryNamesOverride = new Map<string, string>();

if (resolveNameCollisions) {
const targetWorkbookEntries = await Entry.query(Entry.replica)
.select()
.where(EntryColumn.WorkbookId, destinationWorkbookId)
.andWhere({
[EntryColumn.TenantId]: tenantId,
[EntryColumn.IsDeleted]: false,
})
.orderBy(EntryColumn.SortName, 'asc')
.timeout(Entry.DEFAULT_QUERY_TIMEOUT);

entryNamesOverride = resolveEntriesNameCollisions({
existingEntries: targetWorkbookEntries,
addingEntries: originJoinedEntryRevisions,
});
}

const mapEntryIdsWithOldIds = new Map<string, string>();

const newEntries = await Promise.all(
Expand All @@ -146,9 +188,12 @@ export const copyToWorkbook = async (ctx: CTX, params: Params) => {

mapEntryIdsWithOldIds.set(newEntryId, originJoinedEntryRevision.entryId);

const displayKey = `${newEntryId}/${Utils.getNameByKey({
key: originJoinedEntryRevision.displayKey,
})}`;
const name =
entryNamesOverride?.get(originJoinedEntryRevision.entryId) ??
Utils.getNameByKey({
key: originJoinedEntryRevision.displayKey,
});
const displayKey = `${newEntryId}/${name}`;
const key = displayKey.toLowerCase();

const links = originJoinedEntryRevision.links as Nullable<Record<string, string>>;
Expand Down
78 changes: 78 additions & 0 deletions src/services/new/entry/copy-entries-to-workbook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {ServiceArgs} from '../types';
import {makeSchemaValidator} from '../../../components/validation-schema-compiler';
import Utils, {logInfo, makeUserId} from '../../../utils';
import {crossSyncCopiedJoinedEntryRevisions} from '../workbook';
import {transaction} from 'objection';
import {getPrimary} from '../utils';
import {copyToWorkbook} from '../../entry/actions';
import {JoinedEntryRevisionColumns} from '../../../db/presentations';

export type CopyEntriesToWorkbookParams = {
entryIds: string[];
workbookId: string;
};

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

export const copyEntriesToWorkbook = async (
{ctx, trx, skipValidation = false, skipCheckPermissions = false}: ServiceArgs,
args: CopyEntriesToWorkbookParams,
) => {
if (!skipValidation) {
validateArgs(args);
}

const {entryIds, workbookId: targetWorkbookId} = args;
const {user} = ctx.get('info');
const updatedBy = makeUserId(user.userId);

logInfo(ctx, 'COPY_ENTRIES_TO_WORKBOOK_START', {
entryIds: entryIds.map((entryId) => Utils.encodeId(entryId)),
workbookId: Utils.encodeId(targetWorkbookId),
copiedBy: updatedBy,
});

await transaction(getPrimary(trx), async (transactionTrx) => {
const copiedJoinedEntryRevisions = await copyToWorkbook(ctx, {
entryIds,
destinationWorkbookId: targetWorkbookId,
trxOverride: transactionTrx,
skipLinkSync: true,
skipWorkbookPermissionsCheck: skipCheckPermissions,
resolveNameCollisions: true,
});

const filteredCopiedJoinedEntryRevisions = copiedJoinedEntryRevisions.filter(
(item) => item.newJoinedEntryRevision !== undefined,
) as {
newJoinedEntryRevision: JoinedEntryRevisionColumns;
oldEntryId: string;
}[];

await crossSyncCopiedJoinedEntryRevisions({
copiedJoinedEntryRevisions: filteredCopiedJoinedEntryRevisions,
ctx,
trx: transactionTrx,
});
});

logInfo(ctx, 'COPY_ENTRIES_TO_WORKBOOK_FINISH');

return {
workbookId: targetWorkbookId,
};
};
1 change: 1 addition & 0 deletions src/services/new/entry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './get-entry-meta';
export * from './get-entry-meta-private';
export * from './get-entry-by-key';
export * from './copy-entry-to-workbook';
export * from './copy-entries-to-workbook';
59 changes: 59 additions & 0 deletions src/services/new/entry/utils/resolveNameCollisions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Utils from '../../../../utils';

export const resolveEntriesNameCollisions = ({
existingEntries,
addingEntries,
}: {
existingEntries: Array<{entryId: string; displayKey: Nullable<string>}>;
addingEntries: Array<{entryId: string; displayKey: Nullable<string>}>;
}) => {
const mapCollisions = new Map<string, number>();

existingEntries.forEach(({displayKey}) => {
if (displayKey) {
const entryName = Utils.getNameByKey({key: displayKey});
const entryNameWithoutCopy = Utils.getNameWithoutCopyNumber(entryName);
const preparedEntryName = entryNameWithoutCopy.toLowerCase();
const copyNameNumber = Utils.getCopyNumber(entryName);

if (copyNameNumber > 0) {
mapCollisions.set(
preparedEntryName,
Math.max(mapCollisions.get(preparedEntryName) ?? 0, copyNameNumber) + 1,
);
} else {
mapCollisions.set(
preparedEntryName,
(mapCollisions.get(preparedEntryName) ?? 0) + 1,
);
}
}
});

const addingNames = new Map<string, string>();

addingEntries.forEach((entry) => {
let collisionCount: number;

const {entryId, displayKey} = entry;

const entryName = Utils.getNameByKey({key: displayKey});
const copyNameNumber = Utils.getCopyNumber(entryName);
const entryNameWithoutCopy = Utils.getNameWithoutCopyNumber(entryName);
const preparedEntryName = entryNameWithoutCopy.toLowerCase();

if (copyNameNumber > 0) {
collisionCount = Math.max(mapCollisions.get(preparedEntryName) ?? 0, copyNameNumber);
} else {
collisionCount = mapCollisions.get(preparedEntryName) ?? 0;
}

const newEntryName = Utils.setCopyNumber(entryNameWithoutCopy, collisionCount);

mapCollisions.set(preparedEntryName, collisionCount + 1);

addingNames.set(entryId, newEntryName);
});

return addingNames;
};
2 changes: 1 addition & 1 deletion src/services/new/workbook/copy-workbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ async function getUniqWorkbookTitle({
return uniqTitle;
}

async function crossSyncCopiedJoinedEntryRevisions({
export async function crossSyncCopiedJoinedEntryRevisions({
copiedJoinedEntryRevisions,
ctx,
trx,
Expand Down
Loading

0 comments on commit d7f4d09

Please sign in to comment.