From e98d3e3313ab1983c7f9ad3375dff4dc24af9888 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Tue, 10 Jun 2025 14:53:52 +0300 Subject: [PATCH 01/13] Refactor getEntry endpoint --- api/registry/types.ts | 1 + src/controllers/entries/get-entry/index.ts | 66 +++++ .../entries/get-entry/response-model.ts | 106 ++++++++ src/db/models/new/entry/index.ts | 12 +- src/db/models/new/favorite/index.ts | 8 + src/registry/common/functions-map.ts | 2 + src/registry/common/register.ts | 2 + src/registry/common/utils/entry/types.ts | 9 +- src/registry/common/utils/entry/utils.ts | 6 +- src/routes.ts | 11 + .../formatters/format-get-entry-response.ts | 2 +- .../new/entry/get-entry-next/constants.ts | 48 ++++ .../new/entry/get-entry-next/index.ts | 249 ++++++++++++++++++ .../new/entry/get-entry-next/utils.ts | 82 ++++++ src/services/new/entry/get-entry.ts | 195 +++++++------- src/services/new/entry/index.ts | 1 + 16 files changed, 692 insertions(+), 108 deletions(-) create mode 100644 src/controllers/entries/get-entry/index.ts create mode 100644 src/controllers/entries/get-entry/response-model.ts create mode 100644 src/services/new/entry/get-entry-next/constants.ts create mode 100644 src/services/new/entry/get-entry-next/index.ts create mode 100644 src/services/new/entry/get-entry-next/utils.ts diff --git a/api/registry/types.ts b/api/registry/types.ts index fed008a9..494bea43 100644 --- a/api/registry/types.ts +++ b/api/registry/types.ts @@ -52,3 +52,4 @@ export type { export {LogEventType} from '../../src/registry/common/utils/log-event/types'; export type {GatewayApi} from '../../src/registry'; export type {CheckTenant, GetServicePlan} from '../../src/registry/common/utils/tenant/types'; +export type {GetEntryResolveUserLogin} from '../../src/registry/common/utils/entry/types'; diff --git a/src/controllers/entries/get-entry/index.ts b/src/controllers/entries/get-entry/index.ts new file mode 100644 index 00000000..6c60a577 --- /dev/null +++ b/src/controllers/entries/get-entry/index.ts @@ -0,0 +1,66 @@ +import {Request, Response} from '@gravity-ui/expresskit'; + +import {ApiTag} from '../../../components/api-docs'; +import {makeReqParser, z, zc} from '../../../components/zod'; +import {CONTENT_TYPE_JSON} from '../../../const'; +import {getEntryNext} from '../../../services/new/entry'; + +import {getEntryResult} from './response-model'; + +const requestSchema = { + params: z.object({ + entryId: zc.encodedId(), + }), + query: z.object({ + branch: z.enum(['saved', 'published']).optional(), + revId: zc.encodedId().optional(), + includePermissionsInfo: zc.stringBoolean().optional(), + includeLinks: zc.stringBoolean().optional(), + includeServicePlan: zc.stringBoolean().optional(), + includeTenantFeatures: zc.stringBoolean().optional(), + includeFavorite: zc.stringBoolean().optional(), + }), +}; + +const parseReq = makeReqParser(requestSchema); + +export const getEntryController = async (req: Request, res: Response) => { + const {query, params} = await parseReq(req); + + const result = await getEntryNext( + {ctx: req.ctx}, + { + entryId: params.entryId, + branch: query.branch, + revId: query.revId, + includePermissionsInfo: query.includePermissionsInfo, + includeLinks: query.includeLinks, + includeServicePlan: query.includeServicePlan, + includeTenantFeatures: query.includeTenantFeatures, + includeFavorite: query.includeFavorite, + }, + ); + + res.status(200).send(getEntryResult.format(req.ctx, result)); +}; + +getEntryController.api = { + summary: 'Get entry', + tags: [ApiTag.Entries], + request: { + params: requestSchema.params, + query: requestSchema.query, + }, + responses: { + 200: { + description: getEntryResult.schema.description ?? '', + content: { + [CONTENT_TYPE_JSON]: { + schema: getEntryResult.schema, + }, + }, + }, + }, +}; + +getEntryController.manualDecodeId = true; diff --git a/src/controllers/entries/get-entry/response-model.ts b/src/controllers/entries/get-entry/response-model.ts new file mode 100644 index 00000000..3deef7ef --- /dev/null +++ b/src/controllers/entries/get-entry/response-model.ts @@ -0,0 +1,106 @@ +import {AppContext} from '@gravity-ui/nodekit'; + +import {z} from '../../../components/zod'; +import {EntryScope} from '../../../db/models/new/entry/types'; +import {GetEntryNextResult} from '../../../services/new/entry'; +import Utils from '../../../utils'; + +const schema = z + .object({ + entryId: z.string(), + scope: z.nativeEnum(EntryScope), + type: z.string(), + key: z.string().nullable(), + unversionedData: z.record(z.string(), z.unknown()).optional(), + createdBy: z.string(), + createdAt: z.string(), + updatedBy: z.string(), + updatedAt: z.string(), + savedId: z.string().nullable(), + publishedId: z.string().nullable(), + revId: z.string(), + tenantId: z.string().nullable(), + data: z.record(z.string(), z.unknown()).nullable(), + meta: z.record(z.string(), z.unknown()).nullable(), + hidden: z.boolean(), + public: z.boolean(), + workbookId: z.string().nullable(), + links: z.record(z.string(), z.unknown()).optional().nullable(), + isFavorite: z.boolean().optional(), + permissions: z + .object({ + execute: z.boolean().optional(), + read: z.boolean().optional(), + edit: z.boolean().optional(), + admin: z.boolean().optional(), + }) + .optional(), + servicePlan: z.string().optional(), + tenantFeatures: z.record(z.string(), z.unknown()).optional(), + }) + .describe('Get entry result'); + +const format = ( + ctx: AppContext, + { + entry, + revision, + includePermissionsInfo, + permissions, + includeLinks, + includeServicePlan, + servicePlan, + includeTenantFeatures, + tenantFeatures, + includeFavorite, + }: GetEntryNextResult, +): z.infer => { + const {privatePermissions, onlyPublic} = ctx.get('info'); + + let isHiddenUnversionedData = false; + if (!privatePermissions.ownedScopes.includes(entry.scope)) { + isHiddenUnversionedData = true; + } + + let isFavorite: boolean | undefined; + + if (includeFavorite && !onlyPublic) { + isFavorite = Boolean(entry.favorite); + } + + const registry = ctx.get('registry'); + const {getEntryAddFormattedFieldsHook} = registry.common.functions.get(); + const additionalFields = getEntryAddFormattedFieldsHook({ctx}); + + return { + entryId: Utils.encodeId(entry.entryId), + scope: entry.scope, + type: entry.type, + key: entry.displayKey, + unversionedData: isHiddenUnversionedData ? undefined : entry.unversionedData, + createdBy: entry.createdBy, + createdAt: entry.createdAt, + updatedBy: entry.updatedBy, + updatedAt: entry.updatedAt, + savedId: entry.savedId ? Utils.encodeId(entry.savedId) : null, + publishedId: entry.publishedId ? Utils.encodeId(entry.publishedId) : null, + revId: Utils.encodeId(revision.revId), + tenantId: entry.tenantId, + data: revision.data, + meta: revision.meta, + hidden: entry.hidden, + public: entry.public, + workbookId: entry.workbookId ? Utils.encodeId(entry.workbookId) : null, + links: includeLinks ? revision.links : undefined, + isFavorite, + permissions: includePermissionsInfo ? permissions : undefined, + servicePlan: includeServicePlan ? servicePlan : undefined, + tenantFeatures: includeTenantFeatures ? tenantFeatures : undefined, + ...additionalFields, + }; +}; + +export const getEntryResult = { + schema, + format, +}; diff --git a/src/db/models/new/entry/index.ts b/src/db/models/new/entry/index.ts index 7f3930cc..4f9e0582 100644 --- a/src/db/models/new/entry/index.ts +++ b/src/db/models/new/entry/index.ts @@ -2,6 +2,7 @@ import {Model} from '../../..'; import {EntryPermissions} from '../../../../services/new/entry/types'; import {Favorite} from '../favorite'; import {RevisionModel} from '../revision'; +import {Tenant, TenantColumn} from '../tenant'; import {WorkbookModel} from '../workbook'; import {EntryScope} from './types'; @@ -101,12 +102,20 @@ export class Entry extends Model { }, favorite: { relation: Model.HasOneRelation, - modelClass: RevisionModel, + modelClass: Favorite, join: { from: `${Entry.tableName}.${EntryColumn.EntryId}`, to: `${Favorite.tableName}.entryId`, }, }, + tenant: { + relation: Model.BelongsToOneRelation, + modelClass: Tenant, + join: { + from: `${Entry.tableName}.${EntryColumn.TenantId}`, + to: `${Tenant.tableName}.${TenantColumn.TenantId}`, + }, + }, }; } @@ -138,6 +147,7 @@ export class Entry extends Model { publishedRevision?: RevisionModel; workbook?: WorkbookModel; favorite?: Favorite; + tenant?: Tenant; permissions?: EntryPermissions; isLocked?: boolean; diff --git a/src/db/models/new/favorite/index.ts b/src/db/models/new/favorite/index.ts index 0ca27fa2..cfc7926c 100644 --- a/src/db/models/new/favorite/index.ts +++ b/src/db/models/new/favorite/index.ts @@ -1,6 +1,14 @@ import {Model} from '../../..'; import {Entry} from '../entry'; +export const FavoriteColumn = { + EntryId: 'entryId', + TenantId: 'tenantId', + Login: 'login', + Alias: 'alias', + CreatedAt: 'createdAt', +} as const; + export class Favorite extends Model { static get tableName() { return 'favorites'; diff --git a/src/registry/common/functions-map.ts b/src/registry/common/functions-map.ts index fcec5a86..be994030 100644 --- a/src/registry/common/functions-map.ts +++ b/src/registry/common/functions-map.ts @@ -9,6 +9,7 @@ import type {CheckEmbedding} from './utils/embedding/types'; import type { GetEntryAddFormattedFieldsHook, GetEntryBeforeDbRequestHook, + GetEntryResolveUserLogin, IsNeedBypassEntryByKey, } from './utils/entry/types'; import type {LogEvent} from './utils/log-event/types'; @@ -23,6 +24,7 @@ export const commonFunctionsMap = { getZitadelUserRole: makeFunctionTemplate(), getEntryBeforeDbRequestHook: makeFunctionTemplate(), getEntryAddFormattedFieldsHook: makeFunctionTemplate(), + getEntryResolveUserLogin: makeFunctionTemplate(), checkEmbedding: makeFunctionTemplate(), logEvent: makeFunctionTemplate(), checkTenant: makeFunctionTemplate(), diff --git a/src/registry/common/register.ts b/src/registry/common/register.ts index fab6af69..dc76c4a9 100644 --- a/src/registry/common/register.ts +++ b/src/registry/common/register.ts @@ -12,6 +12,7 @@ import {checkEmbedding} from './utils/embedding/utils'; import { getEntryAddFormattedFieldsHook, getEntryBeforeDbRequestHook, + getEntryResolveUserLogin, isNeedBypassEntryByKey, } from './utils/entry/utils'; import {logEvent} from './utils/log-event/utils'; @@ -33,6 +34,7 @@ export const registerCommonPlugins = () => { getZitadelUserRole, getEntryBeforeDbRequestHook, getEntryAddFormattedFieldsHook, + getEntryResolveUserLogin, checkEmbedding, logEvent, checkTenant, diff --git a/src/registry/common/utils/entry/types.ts b/src/registry/common/utils/entry/types.ts index befbfc16..65800bdd 100644 --- a/src/registry/common/utils/entry/types.ts +++ b/src/registry/common/utils/entry/types.ts @@ -1,7 +1,5 @@ import type {AppContext} from '@gravity-ui/nodekit'; -import type {GetEntryResult} from '../../../../services/new/entry/get-entry'; - export type IsNeedBypassEntryByKey = (ctx: AppContext, key?: string) => boolean; export type GetEntryBeforeDbRequestHook = (args: { @@ -9,7 +7,6 @@ export type GetEntryBeforeDbRequestHook = (args: { entryId: string; }) => Promise; -export type GetEntryAddFormattedFieldsHook = (args: { - ctx: AppContext; - result: GetEntryResult; -}) => Promise>; +export type GetEntryAddFormattedFieldsHook = (args: {ctx: AppContext}) => Record; + +export type GetEntryResolveUserLogin = (args: {ctx: AppContext}) => Promise; diff --git a/src/registry/common/utils/entry/utils.ts b/src/registry/common/utils/entry/utils.ts index 7ad397f9..7b4c8e60 100644 --- a/src/registry/common/utils/entry/utils.ts +++ b/src/registry/common/utils/entry/utils.ts @@ -1,6 +1,7 @@ import type { GetEntryAddFormattedFieldsHook, GetEntryBeforeDbRequestHook, + GetEntryResolveUserLogin, IsNeedBypassEntryByKey, } from './types'; @@ -8,5 +9,6 @@ export const isNeedBypassEntryByKey: IsNeedBypassEntryByKey = () => false; export const getEntryBeforeDbRequestHook: GetEntryBeforeDbRequestHook = () => Promise.resolve(); -export const getEntryAddFormattedFieldsHook: GetEntryAddFormattedFieldsHook = () => - Promise.resolve({}); +export const getEntryAddFormattedFieldsHook: GetEntryAddFormattedFieldsHook = () => ({}); + +export const getEntryResolveUserLogin: GetEntryResolveUserLogin = () => Promise.resolve(undefined); diff --git a/src/routes.ts b/src/routes.ts index dc7ac4ad..b882c724 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -6,6 +6,7 @@ import {Feature} from './components/features'; import collections from './controllers/collections'; import colorPalettes from './controllers/color-palettes'; import entries from './controllers/entries'; +import {getEntryController} from './controllers/entries/get-entry'; import favorites from './controllers/favorites'; import helpers from './controllers/helpers'; import homeController from './controllers/home'; @@ -70,6 +71,16 @@ export function getRoutes(_nodekit: NodeKit, options: GetRoutesOptions) { authPolicy: AuthPolicy.disabled, private: true, }), + getEntryV2: makeRoute({ + route: 'GET /v2/entries/:entryId', + handler: getEntryController, + }), + privateGetEntryV2: makeRoute({ + route: 'GET /v2/private/entries/:entryId', + handler: getEntryController, + authPolicy: AuthPolicy.disabled, + private: true, + }), getEntryMeta: makeRoute({ route: 'GET /v1/entries/:entryId/meta', diff --git a/src/services/new/entry/formatters/format-get-entry-response.ts b/src/services/new/entry/formatters/format-get-entry-response.ts index d90e3467..21984a5b 100644 --- a/src/services/new/entry/formatters/format-get-entry-response.ts +++ b/src/services/new/entry/formatters/format-get-entry-response.ts @@ -28,7 +28,7 @@ export const formatGetEntryResponse = async (ctx: CTX, result: GetEntryResult) = const {getEntryAddFormattedFieldsHook} = registry.common.functions.get(); - const additionalFields = await getEntryAddFormattedFieldsHook({ctx, result}); + const additionalFields = getEntryAddFormattedFieldsHook({ctx}); return { entryId: joinedEntryRevisionFavoriteTenant.entryId, diff --git a/src/services/new/entry/get-entry-next/constants.ts b/src/services/new/entry/get-entry-next/constants.ts new file mode 100644 index 00000000..da8bd6d3 --- /dev/null +++ b/src/services/new/entry/get-entry-next/constants.ts @@ -0,0 +1,48 @@ +import {Entry, EntryColumn} from '../../../../db/models/new/entry'; +import {Favorite, FavoriteColumn} from '../../../../db/models/new/favorite'; +import {RevisionModel, RevisionModelColumn} from '../../../../db/models/new/revision'; +import {Tenant, TenantColumn} from '../../../../db/models/new/tenant'; + +export const selectedEntryColumns = [ + `${Entry.tableName}.${EntryColumn.Scope}`, + `${Entry.tableName}.${EntryColumn.Type}`, + `${Entry.tableName}.${EntryColumn.Key}`, + `${Entry.tableName}.${EntryColumn.InnerMeta}`, + `${Entry.tableName}.${EntryColumn.CreatedBy}`, + `${Entry.tableName}.${EntryColumn.CreatedAt}`, + `${Entry.tableName}.${EntryColumn.IsDeleted}`, + `${Entry.tableName}.${EntryColumn.DeletedAt}`, + `${Entry.tableName}.${EntryColumn.Hidden}`, + `${Entry.tableName}.${EntryColumn.DisplayKey}`, + `${Entry.tableName}.${EntryColumn.EntryId}`, + `${Entry.tableName}.${EntryColumn.SavedId}`, + `${Entry.tableName}.${EntryColumn.PublishedId}`, + `${Entry.tableName}.${EntryColumn.TenantId}`, + `${Entry.tableName}.${EntryColumn.Name}`, + `${Entry.tableName}.${EntryColumn.SortName}`, + `${Entry.tableName}.${EntryColumn.Public}`, + `${Entry.tableName}.${EntryColumn.UnversionedData}`, + `${Entry.tableName}.${EntryColumn.WorkbookId}`, + `${Entry.tableName}.${EntryColumn.Mirrored}`, +] as const; + +export const selectedRevisionColumns = [ + `${RevisionModel.tableName}.${RevisionModelColumn.Data}`, + `${RevisionModel.tableName}.${RevisionModelColumn.Meta}`, + `${RevisionModel.tableName}.${RevisionModelColumn.UpdatedBy}`, + `${RevisionModel.tableName}.${RevisionModelColumn.UpdatedAt}`, + `${RevisionModel.tableName}.${RevisionModelColumn.RevId}`, + `${RevisionModel.tableName}.${RevisionModelColumn.Links}`, + `${RevisionModel.tableName}.${RevisionModelColumn.EntryId}`, +] as const; + +export const selectedTenantColumns = [ + `${Tenant.tableName}.${TenantColumn.BillingStartedAt}`, + `${Tenant.tableName}.${TenantColumn.BillingEndedAt}`, + `${Tenant.tableName}.${TenantColumn.Features}`, +] as const; + +export const selectedFavoriteColumns = [ + `${Favorite.tableName}.${FavoriteColumn.EntryId}`, + `${Favorite.tableName}.${FavoriteColumn.Login}`, +] as const; diff --git a/src/services/new/entry/get-entry-next/index.ts b/src/services/new/entry/get-entry-next/index.ts new file mode 100644 index 00000000..fb79e71b --- /dev/null +++ b/src/services/new/entry/get-entry-next/index.ts @@ -0,0 +1,249 @@ +import {AppError} from '@gravity-ui/nodekit'; + +import {Feature, isEnabledFeature} from '../../../../components/features'; +import {US_ERRORS} from '../../../../const'; +import OldEntry from '../../../../db/models/entry'; +import {Entry} from '../../../../db/models/new/entry'; +import {RevisionModel} from '../../../../db/models/new/revision'; +import {TenantColumn} from '../../../../db/models/new/tenant'; +import {DlsActions} from '../../../../types/models'; +import Utils from '../../../../utils'; +import {ServiceArgs} from '../../types'; +import {getReplica} from '../../utils'; +import {EntryPermissions} from '../types'; +import {checkFetchedEntry, checkWorkbookIsolation} from '../utils'; + +import { + selectedEntryColumns, + selectedFavoriteColumns, + selectedRevisionColumns, + selectedTenantColumns, +} from './constants'; +import {checkWorkbookEntry} from './utils'; + +interface GetEntryNextArgs { + entryId: string; + revId?: string; + branch?: 'saved' | 'published'; + includePermissionsInfo?: boolean; + includeLinks?: boolean; + includeServicePlan?: boolean; + includeTenantFeatures?: boolean; + includeFavorite?: boolean; +} + +export type GetEntryNextResult = { + entry: Entry; + revision: RevisionModel; + includePermissionsInfo?: boolean; + permissions: EntryPermissions; + includeLinks?: boolean; + includeServicePlan?: boolean; + servicePlan?: string; + tenantFeatures?: Record; + includeTenantFeatures?: boolean; + includeFavorite?: boolean; +}; + +// eslint-disable-next-line complexity +export const getEntryNext = async ( + {ctx, trx}: ServiceArgs, + args: GetEntryNextArgs, +): Promise => { + const { + entryId, + revId, + branch = 'saved', + includePermissionsInfo, + includeLinks, + includeServicePlan, + includeTenantFeatures, + includeFavorite, + } = args; + + ctx.log('GET_ENTRY_REQUEST', { + entryId: Utils.encodeId(entryId), + revId: Utils.encodeId(revId), + branch, + includePermissionsInfo, + includeLinks, + includeServicePlan, + includeTenantFeatures, + includeFavorite, + }); + + const registry = ctx.get('registry'); + const {DLS} = registry.common.classes.get(); + + const {isPrivateRoute, user, onlyPublic, onlyMirrored} = ctx.get('info'); + + const {getEntryBeforeDbRequestHook, checkEmbedding, getEntryResolveUserLogin} = + registry.common.functions.get(); + + let userLoginPromise; + if (includeFavorite) { + userLoginPromise = user.login ? user.login : getEntryResolveUserLogin({ctx}); + } + + const [userLogin] = await Promise.all([ + userLoginPromise, + getEntryBeforeDbRequestHook({ctx, entryId}), + ]); + + const isEmbedding = checkEmbedding({ctx}); + + const graphRelations = ['workbook', 'tenant(tenantModifier)']; + + if (revId) { + graphRelations.push('revisions(revisionsModifier)'); + } else if (branch === 'saved') { + graphRelations.push('savedRevision(revisionModifier)'); + } else { + graphRelations.push('publishedRevision(revisionModifier)'); + } + + if (includeFavorite && userLogin) { + graphRelations.push('favorite(favoriteModifier)'); + } + + const entry = await Entry.query(getReplica(trx)) + .select(selectedEntryColumns) + .where((builder) => { + builder.where({ + [`${Entry.tableName}.entryId`]: entryId, + [`${Entry.tableName}.isDeleted`]: false, + }); + + if (onlyPublic) { + builder.andWhere({public: true}); + } + + if (onlyMirrored) { + builder.andWhere({mirrored: true}); + } + }) + .withGraphJoined(`[${graphRelations.join(', ')}]`) + .modifiers({ + tenantModifier(builder) { + builder.select(selectedTenantColumns); + }, + + revisionsModifier(builder) { + builder.select(selectedRevisionColumns).where({revId}); + }, + + revisionModifier(builder) { + builder.select(selectedRevisionColumns); + }, + + favoriteModifier(builder) { + builder.select(selectedFavoriteColumns).where({login: userLogin}); + }, + }) + .first() + .timeout(Entry.DEFAULT_QUERY_TIMEOUT); + + const revision = entry?.publishedRevision ?? entry?.savedRevision ?? entry?.revisions?.[0]; + + if (!entry || !revision) { + throw new AppError(US_ERRORS.NOT_EXIST_ENTRY, { + code: US_ERRORS.NOT_EXIST_ENTRY, + }); + } + + if (!entry.tenant) { + throw new AppError(US_ERRORS.NOT_EXIST_TENANT, { + code: US_ERRORS.NOT_EXIST_TENANT, + }); + } + + const {isNeedBypassEntryByKey, getServicePlan} = registry.common.functions.get(); + + const dlsBypassByKeyEnabled = isNeedBypassEntryByKey(ctx, entry.key as string); + + let dlsPermissions: any; // TODO: Update the type after refactoring DLS.checkPermission(...) + let iamPermissions: Optional; + + if (entry.workbook) { + const checkWorkbookEnabled = + !isPrivateRoute && !onlyPublic && !onlyMirrored && !isEmbedding; + + if (checkWorkbookEnabled) { + iamPermissions = await checkWorkbookEntry({ + ctx, + trx, + entry, + workbook: entry.workbook, + includePermissionsInfo, + }); + } + } else { + const checkPermissionEnabled = + !dlsBypassByKeyEnabled && + !isPrivateRoute && + ctx.config.dlsEnabled && + !onlyPublic && + !onlyMirrored; + + if (checkPermissionEnabled) { + dlsPermissions = await DLS.checkPermission( + {ctx, trx}, + { + entryId, + action: DlsActions.Execute, + includePermissionsInfo, + }, + ); + } + + const checkEntryEnabled = !isPrivateRoute && !onlyPublic && !onlyMirrored && !isEmbedding; + + if (checkEntryEnabled) { + if (isEnabledFeature(ctx, Feature.WorkbookIsolationEnabled)) { + checkWorkbookIsolation({ + ctx, + workbookId: null, + }); + } + + await checkFetchedEntry(ctx, entry, getReplica(trx)); + } + } + + let servicePlan: string | undefined; + if (includeServicePlan) { + servicePlan = getServicePlan(entry.tenant); + } + + let tenantFeatures: Record | undefined; + + if (includeTenantFeatures) { + tenantFeatures = entry.tenant[TenantColumn.Features] || undefined; + } + + let permissions: EntryPermissions = {}; + if (includePermissionsInfo) { + permissions = OldEntry.originatePermissions({ + isPrivateRoute, + shared: onlyPublic || isEmbedding, + permissions: dlsPermissions, + iamPermissions, + ctx, + }); + } + + ctx.log('GET_ENTRY_SUCCESS'); + + return { + entry, + revision, + includePermissionsInfo, + permissions, + includeLinks, + includeServicePlan, + servicePlan, + includeTenantFeatures, + tenantFeatures, + includeFavorite, + }; +}; diff --git a/src/services/new/entry/get-entry-next/utils.ts b/src/services/new/entry/get-entry-next/utils.ts new file mode 100644 index 00000000..3f868dd4 --- /dev/null +++ b/src/services/new/entry/get-entry-next/utils.ts @@ -0,0 +1,82 @@ +import {AppContext, AppError} from '@gravity-ui/nodekit'; +import {TransactionOrKnex} from 'objection'; + +import {Feature, isEnabledFeature} from '../../../../components/features'; +import {US_ERRORS} from '../../../../const'; +import {Entry} from '../../../../db/models/new/entry'; +import {WorkbookModel} from '../../../../db/models/new/workbook'; +import {WorkbookPermission} from '../../../../entities/workbook'; +import {getParentIds} from '../../collection/utils'; +import {getReplica} from '../../utils'; +import {getEntryPermissionsByWorkbook} from '../../workbook/utils'; +import {checkWorkbookIsolation} from '../utils'; + +export const checkWorkbookEntry = async ({ + ctx, + trx, + entry, + workbook, + includePermissionsInfo, +}: { + ctx: AppContext; + trx?: TransactionOrKnex; + entry: Entry; + workbook: WorkbookModel; + includePermissionsInfo?: boolean; +}) => { + if (isEnabledFeature(ctx, Feature.WorkbookIsolationEnabled)) { + checkWorkbookIsolation({ + ctx, + workbookId: workbook.workbookId, + }); + } + + let parentIds: string[] = []; + + if (workbook.collectionId !== null) { + parentIds = await getParentIds({ + ctx, + trx: getReplica(trx), + collectionId: workbook.collectionId, + }); + } + + const registry = ctx.get('registry'); + + const {Workbook} = registry.common.classes.get(); + + const workbookInstance = new Workbook({ + ctx, + model: workbook, + }); + + const {accessServiceEnabled} = ctx.config; + + if (accessServiceEnabled) { + if (includePermissionsInfo) { + await workbookInstance.fetchAllPermissions({parentIds}); + + if (!workbookInstance.permissions?.[WorkbookPermission.LimitedView]) { + throw new AppError(US_ERRORS.ACCESS_SERVICE_PERMISSION_DENIED, { + code: US_ERRORS.ACCESS_SERVICE_PERMISSION_DENIED, + }); + } + } else { + await workbookInstance.checkPermission({ + parentIds, + permission: WorkbookPermission.LimitedView, + }); + } + } else { + workbookInstance.enableAllPermissions(); + } + + if (includePermissionsInfo) { + return getEntryPermissionsByWorkbook({ + workbook: workbookInstance, + scope: entry.scope, + }); + } + + return undefined; +}; diff --git a/src/services/new/entry/get-entry.ts b/src/services/new/entry/get-entry.ts index fda48dcb..e7071fbc 100644 --- a/src/services/new/entry/get-entry.ts +++ b/src/services/new/entry/get-entry.ts @@ -134,115 +134,114 @@ export const getEntry = async ( trx: getReplica(trx), }); - if (joinedEntryRevisionFavoriteTenant) { - const {isNeedBypassEntryByKey, getServicePlan} = registry.common.functions.get(); + if (!joinedEntryRevisionFavoriteTenant) { + throw new AppError(US_ERRORS.NOT_EXIST_ENTRY, { + code: US_ERRORS.NOT_EXIST_ENTRY, + }); + } - const dlsBypassByKeyEnabled = isNeedBypassEntryByKey( - ctx, - joinedEntryRevisionFavoriteTenant.key as string, - ); - - let dlsPermissions: any; // TODO: Update the type after refactoring DLS.checkPermission(...) - let iamPermissions: Optional; - - if (joinedEntryRevisionFavoriteTenant.workbookId) { - const checkWorkbookEnabled = - !isPrivateRoute && !onlyPublic && !onlyMirrored && !isEmbedding; - - if (checkWorkbookEnabled) { - if (isEnabledFeature(ctx, Feature.WorkbookIsolationEnabled)) { - checkWorkbookIsolation({ - ctx, - workbookId: joinedEntryRevisionFavoriteTenant.workbookId, - }); - } - - const workbook = await getWorkbook( - {ctx, trx}, - { - workbookId: joinedEntryRevisionFavoriteTenant.workbookId, - includePermissionsInfo, - }, - ); - - if (includePermissionsInfo) { - iamPermissions = getEntryPermissionsByWorkbook({ - workbook, - scope: joinedEntryRevisionFavoriteTenant[EntryColumn.Scope], - }); - } - } - } else { - const checkPermissionEnabled = - !dlsBypassByKeyEnabled && - !isPrivateRoute && - ctx.config.dlsEnabled && - !onlyPublic && - !onlyMirrored; - - const checkEntryEnabled = - !isPrivateRoute && !onlyPublic && !onlyMirrored && !isEmbedding; - - if (checkPermissionEnabled) { - dlsPermissions = await DLS.checkPermission( - {ctx, trx}, - { - entryId, - action: DlsActions.Execute, - includePermissionsInfo, - }, - ); - } + const {isNeedBypassEntryByKey, getServicePlan} = registry.common.functions.get(); + + const dlsBypassByKeyEnabled = isNeedBypassEntryByKey( + ctx, + joinedEntryRevisionFavoriteTenant.key as string, + ); - if (checkEntryEnabled) { - if (isEnabledFeature(ctx, Feature.WorkbookIsolationEnabled)) { - checkWorkbookIsolation({ - ctx, - workbookId: null, - }); - } + let dlsPermissions: any; // TODO: Update the type after refactoring DLS.checkPermission(...) + let iamPermissions: Optional; - await checkFetchedEntry(ctx, joinedEntryRevisionFavoriteTenant, getReplica(trx)); + if (joinedEntryRevisionFavoriteTenant.workbookId) { + const checkWorkbookEnabled = + !isPrivateRoute && !onlyPublic && !onlyMirrored && !isEmbedding; + + if (checkWorkbookEnabled) { + if (isEnabledFeature(ctx, Feature.WorkbookIsolationEnabled)) { + checkWorkbookIsolation({ + ctx, + workbookId: joinedEntryRevisionFavoriteTenant.workbookId, + }); } - } - let servicePlan: string | undefined; - if (includeServicePlan) { - servicePlan = getServicePlan(joinedEntryRevisionFavoriteTenant); + const workbook = await getWorkbook( + {ctx, trx}, + { + workbookId: joinedEntryRevisionFavoriteTenant.workbookId, + includePermissionsInfo, + }, + ); + + if (includePermissionsInfo) { + iamPermissions = getEntryPermissionsByWorkbook({ + workbook, + scope: joinedEntryRevisionFavoriteTenant[EntryColumn.Scope], + }); + } + } + } else { + const checkPermissionEnabled = + !dlsBypassByKeyEnabled && + !isPrivateRoute && + ctx.config.dlsEnabled && + !onlyPublic && + !onlyMirrored; + + const checkEntryEnabled = !isPrivateRoute && !onlyPublic && !onlyMirrored && !isEmbedding; + + if (checkPermissionEnabled) { + dlsPermissions = await DLS.checkPermission( + {ctx, trx}, + { + entryId, + action: DlsActions.Execute, + includePermissionsInfo, + }, + ); } - let tenantFeatures: Record | undefined; + if (checkEntryEnabled) { + if (isEnabledFeature(ctx, Feature.WorkbookIsolationEnabled)) { + checkWorkbookIsolation({ + ctx, + workbookId: null, + }); + } - if (includeTenantFeatures) { - tenantFeatures = joinedEntryRevisionFavoriteTenant[TenantColumn.Features] || undefined; + await checkFetchedEntry(ctx, joinedEntryRevisionFavoriteTenant, getReplica(trx)); } + } - let permissions: EntryPermissions = {}; - if (includePermissionsInfo) { - permissions = OldEntry.originatePermissions({ - isPrivateRoute, - shared: onlyPublic || isEmbedding, - permissions: dlsPermissions, - iamPermissions, - ctx, - }); - } + let servicePlan: string | undefined; + if (includeServicePlan) { + servicePlan = getServicePlan(joinedEntryRevisionFavoriteTenant); + } - ctx.log('GET_ENTRY_SUCCESS'); - - return { - joinedEntryRevisionFavoriteTenant, - permissions, - includePermissionsInfo, - includeLinks, - servicePlan, - includeServicePlan, - includeTenantFeatures, - tenantFeatures, - }; - } else { - throw new AppError(US_ERRORS.NOT_EXIST_ENTRY, { - code: US_ERRORS.NOT_EXIST_ENTRY, + let tenantFeatures: Record | undefined; + + if (includeTenantFeatures) { + tenantFeatures = joinedEntryRevisionFavoriteTenant[TenantColumn.Features] || undefined; + } + + let permissions: EntryPermissions = {}; + if (includePermissionsInfo) { + permissions = OldEntry.originatePermissions({ + isPrivateRoute, + shared: onlyPublic || isEmbedding, + permissions: dlsPermissions, + iamPermissions, + ctx, }); } + + ctx.log('GET_ENTRY_SUCCESS'); + + return { + joinedEntryRevisionFavoriteTenant, + permissions, + includePermissionsInfo, + includeLinks, + servicePlan, + includeServicePlan, + includeTenantFeatures, + tenantFeatures, + }; }; diff --git a/src/services/new/entry/index.ts b/src/services/new/entry/index.ts index a88da372..d1648a06 100644 --- a/src/services/new/entry/index.ts +++ b/src/services/new/entry/index.ts @@ -5,3 +5,4 @@ export * from './get-entry-meta-private'; export * from './get-entry-meta'; export * from './get-entry'; export * from './get-joined-entries-revisions-by-ids'; +export * from './get-entry-next'; From 09cb85c5e344d7cf8257cb43975a148d221d3ebe Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Tue, 10 Jun 2025 17:37:21 +0300 Subject: [PATCH 02/13] Fixes --- src/controllers/entries/get-entry/response-model.ts | 4 ++-- src/controllers/entries/index.ts | 2 ++ src/routes.ts | 5 ++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/controllers/entries/get-entry/response-model.ts b/src/controllers/entries/get-entry/response-model.ts index 3deef7ef..06cd4748 100644 --- a/src/controllers/entries/get-entry/response-model.ts +++ b/src/controllers/entries/get-entry/response-model.ts @@ -80,8 +80,8 @@ const format = ( unversionedData: isHiddenUnversionedData ? undefined : entry.unversionedData, createdBy: entry.createdBy, createdAt: entry.createdAt, - updatedBy: entry.updatedBy, - updatedAt: entry.updatedAt, + updatedBy: revision.updatedBy, + updatedAt: revision.updatedAt, savedId: entry.savedId ? Utils.encodeId(entry.savedId) : null, publishedId: entry.publishedId ? Utils.encodeId(entry.publishedId) : null, revId: Utils.encodeId(revision.revId), diff --git a/src/controllers/entries/index.ts b/src/controllers/entries/index.ts index 3efff651..2261d9b4 100644 --- a/src/controllers/entries/index.ts +++ b/src/controllers/entries/index.ts @@ -31,6 +31,7 @@ import {createEntryAltController} from './create-entry-alt'; import {deleteEntryController} from './delete-entry'; import {getEntriesController} from './get-entries'; import {getEntriesDataController} from './get-entries-data'; +import {getEntryController as getEntryV2Controller} from './get-entry'; import {renameEntryController} from './rename-entry'; import {updateEntryController} from './update-entry'; @@ -44,6 +45,7 @@ export default { createEntryController, createEntryAltController, getEntriesController, + getEntryV2Controller, getEntry: async (req: Request, res: Response) => { const {query, params} = req; diff --git a/src/routes.ts b/src/routes.ts index b882c724..1306db6a 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -6,7 +6,6 @@ import {Feature} from './components/features'; import collections from './controllers/collections'; import colorPalettes from './controllers/color-palettes'; import entries from './controllers/entries'; -import {getEntryController} from './controllers/entries/get-entry'; import favorites from './controllers/favorites'; import helpers from './controllers/helpers'; import homeController from './controllers/home'; @@ -73,11 +72,11 @@ export function getRoutes(_nodekit: NodeKit, options: GetRoutesOptions) { }), getEntryV2: makeRoute({ route: 'GET /v2/entries/:entryId', - handler: getEntryController, + handler: entries.getEntryV2Controller, }), privateGetEntryV2: makeRoute({ route: 'GET /v2/private/entries/:entryId', - handler: getEntryController, + handler: entries.getEntryV2Controller, authPolicy: AuthPolicy.disabled, private: true, }), From 6e3f066f4091e7f43cad527138eab860e1d59fb3 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Tue, 24 Jun 2025 12:22:42 +0300 Subject: [PATCH 03/13] Fix naming --- src/controllers/entries/get-entry/index.ts | 4 ++-- .../new/entry/{get-entry-next => get-entry-v2}/constants.ts | 0 .../new/entry/{get-entry-next => get-entry-v2}/index.ts | 2 +- .../new/entry/{get-entry-next => get-entry-v2}/utils.ts | 0 src/services/new/entry/index.ts | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename src/services/new/entry/{get-entry-next => get-entry-v2}/constants.ts (100%) rename src/services/new/entry/{get-entry-next => get-entry-v2}/index.ts (99%) rename src/services/new/entry/{get-entry-next => get-entry-v2}/utils.ts (100%) diff --git a/src/controllers/entries/get-entry/index.ts b/src/controllers/entries/get-entry/index.ts index 6c60a577..605fd5ab 100644 --- a/src/controllers/entries/get-entry/index.ts +++ b/src/controllers/entries/get-entry/index.ts @@ -3,7 +3,7 @@ import {Request, Response} from '@gravity-ui/expresskit'; import {ApiTag} from '../../../components/api-docs'; import {makeReqParser, z, zc} from '../../../components/zod'; import {CONTENT_TYPE_JSON} from '../../../const'; -import {getEntryNext} from '../../../services/new/entry'; +import {getEntryV2} from '../../../services/new/entry'; import {getEntryResult} from './response-model'; @@ -27,7 +27,7 @@ const parseReq = makeReqParser(requestSchema); export const getEntryController = async (req: Request, res: Response) => { const {query, params} = await parseReq(req); - const result = await getEntryNext( + const result = await getEntryV2( {ctx: req.ctx}, { entryId: params.entryId, diff --git a/src/services/new/entry/get-entry-next/constants.ts b/src/services/new/entry/get-entry-v2/constants.ts similarity index 100% rename from src/services/new/entry/get-entry-next/constants.ts rename to src/services/new/entry/get-entry-v2/constants.ts diff --git a/src/services/new/entry/get-entry-next/index.ts b/src/services/new/entry/get-entry-v2/index.ts similarity index 99% rename from src/services/new/entry/get-entry-next/index.ts rename to src/services/new/entry/get-entry-v2/index.ts index fb79e71b..18fafb04 100644 --- a/src/services/new/entry/get-entry-next/index.ts +++ b/src/services/new/entry/get-entry-v2/index.ts @@ -46,7 +46,7 @@ export type GetEntryNextResult = { }; // eslint-disable-next-line complexity -export const getEntryNext = async ( +export const getEntryV2 = async ( {ctx, trx}: ServiceArgs, args: GetEntryNextArgs, ): Promise => { diff --git a/src/services/new/entry/get-entry-next/utils.ts b/src/services/new/entry/get-entry-v2/utils.ts similarity index 100% rename from src/services/new/entry/get-entry-next/utils.ts rename to src/services/new/entry/get-entry-v2/utils.ts diff --git a/src/services/new/entry/index.ts b/src/services/new/entry/index.ts index d1648a06..5543c328 100644 --- a/src/services/new/entry/index.ts +++ b/src/services/new/entry/index.ts @@ -5,4 +5,4 @@ export * from './get-entry-meta-private'; export * from './get-entry-meta'; export * from './get-entry'; export * from './get-joined-entries-revisions-by-ids'; -export * from './get-entry-next'; +export * from './get-entry-v2'; From 9c68e9503648468350f7ba672b902ca5512346c6 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Tue, 1 Jul 2025 11:15:25 +0300 Subject: [PATCH 04/13] Up gateway version --- package-lock.json | 26 ++++++++++++++------------ package.json | 2 +- src/registry/index.ts | 3 ++- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae84951b..2c46acac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.0", "@gravity-ui/expresskit": "^2.4.0", - "@gravity-ui/gateway": "^2.6.2", + "@gravity-ui/gateway": "^4.7.2", "@gravity-ui/nodekit": "^2.4.1", "@gravity-ui/postgreskit": "^2.0.0", "ajv": "^6.12.4", @@ -834,16 +834,16 @@ } }, "node_modules/@gravity-ui/gateway": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@gravity-ui/gateway/-/gateway-2.6.2.tgz", - "integrity": "sha512-HHNAJikLt74CEH1nOTT8A6hAL4NbigB3U7wy+b/ryjYstBTHfHZNUmGHCRqHp80OhljB2IbHmIsWiUFwjBuCRA==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/@gravity-ui/gateway/-/gateway-4.7.2.tgz", + "integrity": "sha512-gu6mwTGe0I5EO2c41AMepl9FmJxjKA4ItxGmgAfGA62b6Ai6DMHQSLQXCTIV8KE6ddW8luSqHTFVfHLxj4qYRQ==", "license": "MIT", "dependencies": { "@grpc/grpc-js": "^1.9.9", "@grpc/proto-loader": "^0.7.8", "ajv": "^8.12.0", - "axios": "^1.3.5", - "axios-retry": "^3.4.0", + "axios": "^1.8.3", + "axios-retry": "^3.9.1", "lodash": "^4.17.21", "object-sizeof": "^2.6.5", "protobufjs": "^7.2.5", @@ -3417,9 +3417,10 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -3427,9 +3428,10 @@ } }, "node_modules/axios-retry": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.7.0.tgz", - "integrity": "sha512-ZTnCkJbRtfScvwiRnoVskFAfvU0UG3xNcsjwTR0mawSbIJoothxn67gKsMaNAFHRXJ1RmuLhmZBzvyXi3+9WyQ==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.9.1.tgz", + "integrity": "sha512-8PJDLJv7qTTMMwdnbMvrLYuvB47M81wRtxQmEdV5w4rgbTXTt+vtPkXwajOfOdSyv/wZICJOC+/UhXH4aQ/R+w==", + "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.15.4", "is-retry-allowed": "^2.2.0" diff --git a/package.json b/package.json index 8d073f8d..2aef25b7 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.0", "@gravity-ui/expresskit": "^2.4.0", - "@gravity-ui/gateway": "^2.6.2", + "@gravity-ui/gateway": "^4.7.2", "@gravity-ui/nodekit": "^2.4.1", "@gravity-ui/postgreskit": "^2.0.0", "ajv": "^6.12.4", diff --git a/src/registry/index.ts b/src/registry/index.ts index c6c6654e..bcc89f82 100644 --- a/src/registry/index.ts +++ b/src/registry/index.ts @@ -1,8 +1,9 @@ import type {ExpressKit, Request, Response} from '@gravity-ui/expresskit'; -import getGatewayControllers, { +import { ApiWithRoot, GatewayConfig, SchemasByScope, + getGatewayControllers, } from '@gravity-ui/gateway'; import type {AppContext} from '@gravity-ui/nodekit'; From f58bed8d59b061844a467b972ac762b511d7bbf8 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Tue, 1 Jul 2025 12:40:01 +0300 Subject: [PATCH 05/13] Add DLS.checkpermission timeout --- src/components/error-response-presenter.ts | 10 ++++++++ src/const/errors.ts | 1 + src/routes.ts | 8 +++--- .../new/collection/utils/get-parents.ts | 25 ++++++++++++++++--- src/services/new/entry/get-entry-v2/index.ts | 23 ++++++++++------- src/services/new/entry/get-entry-v2/utils.ts | 3 +++ src/utils/index.ts | 1 + src/utils/promise/index.ts | 1 + src/utils/promise/with-timeout.ts | 20 +++++++++++++++ 9 files changed, 75 insertions(+), 17 deletions(-) create mode 100644 src/utils/promise/index.ts create mode 100644 src/utils/promise/with-timeout.ts diff --git a/src/components/error-response-presenter.ts b/src/components/error-response-presenter.ts index fb045861..1485ddc4 100644 --- a/src/components/error-response-presenter.ts +++ b/src/components/error-response-presenter.ts @@ -400,6 +400,16 @@ export default (error: AppError | DBError) => { }; } + case US_ERRORS.OPERATION_TIMEOUT: { + return { + code: 504, + response: { + code, + message: 'Operation timed out', + }, + }; + } + default: return { code: 500, diff --git a/src/const/errors.ts b/src/const/errors.ts index acb47d37..43b6be61 100644 --- a/src/const/errors.ts +++ b/src/const/errors.ts @@ -50,4 +50,5 @@ export const US_ERRORS = { COLLECTION_WITH_WORKBOOK_TEMPLATE_CANT_BE_DELETED: 'COLLECTION_WITH_WORKBOOK_TEMPLATE_CANT_BE_DELETED', TENANT_ID_MISSING_IN_CONTEXT: 'TENANT_ID_MISSING_IN_CONTEXT', + OPERATION_TIMEOUT: 'OPERATION_TIMEOUT', }; diff --git a/src/routes.ts b/src/routes.ts index 1306db6a..6c529ae7 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -64,16 +64,16 @@ export function getRoutes(_nodekit: NodeKit, options: GetRoutesOptions) { route: 'GET /v1/entries/:entryId', handler: entries.getEntry, }), + getEntryV2: makeRoute({ + route: 'GET /v2/entries/:entryId', + handler: entries.getEntryV2Controller, + }), privateGetEntry: makeRoute({ route: 'GET /private/entries/:entryId', handler: entries.getEntry, authPolicy: AuthPolicy.disabled, private: true, }), - getEntryV2: makeRoute({ - route: 'GET /v2/entries/:entryId', - handler: entries.getEntryV2Controller, - }), privateGetEntryV2: makeRoute({ route: 'GET /v2/private/entries/:entryId', handler: entries.getEntryV2Controller, diff --git a/src/services/new/collection/utils/get-parents.ts b/src/services/new/collection/utils/get-parents.ts index c41482a9..d9421322 100644 --- a/src/services/new/collection/utils/get-parents.ts +++ b/src/services/new/collection/utils/get-parents.ts @@ -15,13 +15,20 @@ interface Ctx { interface GetCollectionsParentIds extends Ctx { collectionIds: Nullable[]; + queryTimeout?: number; } interface GetCollectionParentIds extends Ctx { collectionId: string; + getParentsQueryTimeout?: number; } -export const getParents = async ({ctx, trx, collectionIds}: GetCollectionsParentIds) => { +export const getParents = async ({ + ctx, + trx, + collectionIds, + queryTimeout = CollectionModel.DEFAULT_QUERY_TIMEOUT, +}: GetCollectionsParentIds) => { const {tenantId, onlyMirrored} = ctx.get('info'); const targetTrx = getReplica(trx); @@ -59,13 +66,23 @@ export const getParents = async ({ctx, trx, collectionIds}: GetCollectionsParent }) .select() .from(recursiveName) - .timeout(CollectionModel.DEFAULT_QUERY_TIMEOUT); + .timeout(queryTimeout); return result; }; -export const getParentIds = async ({ctx, trx, collectionId}: GetCollectionParentIds) => { - const parents = await getParents({ctx, trx, collectionIds: [collectionId]}); +export const getParentIds = async ({ + ctx, + trx, + collectionId, + getParentsQueryTimeout, +}: GetCollectionParentIds) => { + const parents = await getParents({ + ctx, + trx, + collectionIds: [collectionId], + queryTimeout: getParentsQueryTimeout, + }); return parents.map((item) => item.collectionId); }; diff --git a/src/services/new/entry/get-entry-v2/index.ts b/src/services/new/entry/get-entry-v2/index.ts index 18fafb04..72c375c3 100644 --- a/src/services/new/entry/get-entry-v2/index.ts +++ b/src/services/new/entry/get-entry-v2/index.ts @@ -7,7 +7,7 @@ import {Entry} from '../../../../db/models/new/entry'; import {RevisionModel} from '../../../../db/models/new/revision'; import {TenantColumn} from '../../../../db/models/new/tenant'; import {DlsActions} from '../../../../types/models'; -import Utils from '../../../../utils'; +import Utils, {withTimeout} from '../../../../utils'; import {ServiceArgs} from '../../types'; import {getReplica} from '../../utils'; import {EntryPermissions} from '../types'; @@ -21,6 +21,8 @@ import { } from './constants'; import {checkWorkbookEntry} from './utils'; +const ENTRY_QUERY_TIMEOUT = 3000; + interface GetEntryNextArgs { entryId: string; revId?: string; @@ -141,7 +143,7 @@ export const getEntryV2 = async ( }, }) .first() - .timeout(Entry.DEFAULT_QUERY_TIMEOUT); + .timeout(ENTRY_QUERY_TIMEOUT); const revision = entry?.publishedRevision ?? entry?.savedRevision ?? entry?.revisions?.[0]; @@ -186,13 +188,16 @@ export const getEntryV2 = async ( !onlyMirrored; if (checkPermissionEnabled) { - dlsPermissions = await DLS.checkPermission( - {ctx, trx}, - { - entryId, - action: DlsActions.Execute, - includePermissionsInfo, - }, + dlsPermissions = await withTimeout( + DLS.checkPermission( + {ctx, trx}, + { + entryId, + action: DlsActions.Execute, + includePermissionsInfo, + }, + ), + {timeoutMs: 3000, errorMessage: 'DLS.checkPermission timeout'}, ); } diff --git a/src/services/new/entry/get-entry-v2/utils.ts b/src/services/new/entry/get-entry-v2/utils.ts index 3f868dd4..42fa5f87 100644 --- a/src/services/new/entry/get-entry-v2/utils.ts +++ b/src/services/new/entry/get-entry-v2/utils.ts @@ -11,6 +11,8 @@ import {getReplica} from '../../utils'; import {getEntryPermissionsByWorkbook} from '../../workbook/utils'; import {checkWorkbookIsolation} from '../utils'; +const GET_PARENTS_QUERY_TIMEOUT = 3000; + export const checkWorkbookEntry = async ({ ctx, trx, @@ -38,6 +40,7 @@ export const checkWorkbookEntry = async ({ ctx, trx: getReplica(trx), collectionId: workbook.collectionId, + getParentsQueryTimeout: GET_PARENTS_QUERY_TIMEOUT, }); } diff --git a/src/utils/index.ts b/src/utils/index.ts index 84bd28df..383235e1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -4,3 +4,4 @@ export default Utils; export * from './user'; export * from './validation'; export * from './tenant'; +export * from './promise'; diff --git a/src/utils/promise/index.ts b/src/utils/promise/index.ts new file mode 100644 index 00000000..13f7b1ba --- /dev/null +++ b/src/utils/promise/index.ts @@ -0,0 +1 @@ +export * from './with-timeout'; diff --git a/src/utils/promise/with-timeout.ts b/src/utils/promise/with-timeout.ts new file mode 100644 index 00000000..66fd43ed --- /dev/null +++ b/src/utils/promise/with-timeout.ts @@ -0,0 +1,20 @@ +import {AppError} from '@gravity-ui/nodekit'; + +import {US_ERRORS} from '../../const'; + +export const withTimeout = async ( + promise: Promise, + {timeoutMs, errorMessage}: {timeoutMs: number; errorMessage: string}, +): Promise => { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject( + new AppError(errorMessage, { + code: US_ERRORS.OPERATION_TIMEOUT, + }), + ); + }, timeoutMs); + }); + + return Promise.race([promise, timeoutPromise]); +}; From dea1eb0d4230dbbe18cfc4fb807651e71241a11e Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Wed, 2 Jul 2025 10:54:03 +0300 Subject: [PATCH 06/13] Add test routes --- src/tests/int/routes.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tests/int/routes.ts b/src/tests/int/routes.ts index ba0f492d..425ba646 100644 --- a/src/tests/int/routes.ts +++ b/src/tests/int/routes.ts @@ -1,5 +1,7 @@ export const routes = { entries: '/v1/entries', + entriesGetEntryV2: (entryId: string) => `/v2/entries/${entryId}`, + entriesPrivateGetEntryV2: (entryId: string) => `/v2/private/entries/${entryId}`, privateEntries: '/private/entries', privateCreateEntry: '/private/createEntry', favorites: '/v1/favorites', From ac582a59af3664036b0f4195a27d2a2df4a6c7ce Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Wed, 2 Jul 2025 14:18:16 +0300 Subject: [PATCH 07/13] Fix log --- src/services/new/entry/get-entry-v2/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/new/entry/get-entry-v2/index.ts b/src/services/new/entry/get-entry-v2/index.ts index 72c375c3..a5734cee 100644 --- a/src/services/new/entry/get-entry-v2/index.ts +++ b/src/services/new/entry/get-entry-v2/index.ts @@ -63,7 +63,7 @@ export const getEntryV2 = async ( includeFavorite, } = args; - ctx.log('GET_ENTRY_REQUEST', { + ctx.log('GET_ENTRY_V2_REQUEST', { entryId: Utils.encodeId(entryId), revId: Utils.encodeId(revId), branch, @@ -237,7 +237,7 @@ export const getEntryV2 = async ( }); } - ctx.log('GET_ENTRY_SUCCESS'); + ctx.log('GET_ENTRY_V2_SUCCESS'); return { entry, From fe8506b80a1744a12b566d4a1b136142a1502fc7 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Thu, 3 Jul 2025 12:05:55 +0300 Subject: [PATCH 08/13] Fix types --- .../entries/get-entry/response-model.ts | 4 +- .../new/entry/get-entry-v2/constants.ts | 85 ++++++++++--------- src/services/new/entry/get-entry-v2/index.ts | 16 ++-- src/services/new/entry/get-entry-v2/types.ts | 22 +++++ src/services/new/entry/get-entry-v2/utils.ts | 5 +- 5 files changed, 82 insertions(+), 50 deletions(-) create mode 100644 src/services/new/entry/get-entry-v2/types.ts diff --git a/src/controllers/entries/get-entry/response-model.ts b/src/controllers/entries/get-entry/response-model.ts index 06cd4748..789ab15b 100644 --- a/src/controllers/entries/get-entry/response-model.ts +++ b/src/controllers/entries/get-entry/response-model.ts @@ -2,7 +2,7 @@ import {AppContext} from '@gravity-ui/nodekit'; import {z} from '../../../components/zod'; import {EntryScope} from '../../../db/models/new/entry/types'; -import {GetEntryNextResult} from '../../../services/new/entry'; +import {GetEntryV2Result} from '../../../services/new/entry'; import Utils from '../../../utils'; const schema = z @@ -53,7 +53,7 @@ const format = ( includeTenantFeatures, tenantFeatures, includeFavorite, - }: GetEntryNextResult, + }: GetEntryV2Result, ): z.infer => { const {privatePermissions, onlyPublic} = ctx.get('info'); diff --git a/src/services/new/entry/get-entry-v2/constants.ts b/src/services/new/entry/get-entry-v2/constants.ts index da8bd6d3..1aafbef1 100644 --- a/src/services/new/entry/get-entry-v2/constants.ts +++ b/src/services/new/entry/get-entry-v2/constants.ts @@ -3,46 +3,55 @@ import {Favorite, FavoriteColumn} from '../../../../db/models/new/favorite'; import {RevisionModel, RevisionModelColumn} from '../../../../db/models/new/revision'; import {Tenant, TenantColumn} from '../../../../db/models/new/tenant'; -export const selectedEntryColumns = [ - `${Entry.tableName}.${EntryColumn.Scope}`, - `${Entry.tableName}.${EntryColumn.Type}`, - `${Entry.tableName}.${EntryColumn.Key}`, - `${Entry.tableName}.${EntryColumn.InnerMeta}`, - `${Entry.tableName}.${EntryColumn.CreatedBy}`, - `${Entry.tableName}.${EntryColumn.CreatedAt}`, - `${Entry.tableName}.${EntryColumn.IsDeleted}`, - `${Entry.tableName}.${EntryColumn.DeletedAt}`, - `${Entry.tableName}.${EntryColumn.Hidden}`, - `${Entry.tableName}.${EntryColumn.DisplayKey}`, - `${Entry.tableName}.${EntryColumn.EntryId}`, - `${Entry.tableName}.${EntryColumn.SavedId}`, - `${Entry.tableName}.${EntryColumn.PublishedId}`, - `${Entry.tableName}.${EntryColumn.TenantId}`, - `${Entry.tableName}.${EntryColumn.Name}`, - `${Entry.tableName}.${EntryColumn.SortName}`, - `${Entry.tableName}.${EntryColumn.Public}`, - `${Entry.tableName}.${EntryColumn.UnversionedData}`, - `${Entry.tableName}.${EntryColumn.WorkbookId}`, - `${Entry.tableName}.${EntryColumn.Mirrored}`, -] as const; +export const entryColumns = [ + EntryColumn.Scope, + EntryColumn.Type, + EntryColumn.Key, + EntryColumn.InnerMeta, + EntryColumn.CreatedBy, + EntryColumn.CreatedAt, + EntryColumn.IsDeleted, + EntryColumn.DeletedAt, + EntryColumn.Hidden, + EntryColumn.DisplayKey, + EntryColumn.EntryId, + EntryColumn.SavedId, + EntryColumn.PublishedId, + EntryColumn.TenantId, + EntryColumn.Name, + EntryColumn.SortName, + EntryColumn.Public, + EntryColumn.UnversionedData, + EntryColumn.WorkbookId, + EntryColumn.Mirrored, +]; -export const selectedRevisionColumns = [ - `${RevisionModel.tableName}.${RevisionModelColumn.Data}`, - `${RevisionModel.tableName}.${RevisionModelColumn.Meta}`, - `${RevisionModel.tableName}.${RevisionModelColumn.UpdatedBy}`, - `${RevisionModel.tableName}.${RevisionModelColumn.UpdatedAt}`, - `${RevisionModel.tableName}.${RevisionModelColumn.RevId}`, - `${RevisionModel.tableName}.${RevisionModelColumn.Links}`, - `${RevisionModel.tableName}.${RevisionModelColumn.EntryId}`, -] as const; +export const selectedEntryColumns = entryColumns.map((column) => `${Entry.tableName}.${column}`); -export const selectedTenantColumns = [ - `${Tenant.tableName}.${TenantColumn.BillingStartedAt}`, - `${Tenant.tableName}.${TenantColumn.BillingEndedAt}`, - `${Tenant.tableName}.${TenantColumn.Features}`, +export const revisionColumns = [ + RevisionModelColumn.Data, + RevisionModelColumn.Meta, + RevisionModelColumn.UpdatedBy, + RevisionModelColumn.UpdatedAt, + RevisionModelColumn.RevId, + RevisionModelColumn.Links, + RevisionModelColumn.EntryId, ] as const; -export const selectedFavoriteColumns = [ - `${Favorite.tableName}.${FavoriteColumn.EntryId}`, - `${Favorite.tableName}.${FavoriteColumn.Login}`, +export const selectedRevisionColumns = revisionColumns.map( + (column) => `${RevisionModel.tableName}.${column}`, +); + +export const tenantColumns = [ + TenantColumn.BillingStartedAt, + TenantColumn.BillingEndedAt, + TenantColumn.Features, ] as const; + +export const selectedTenantColumns = tenantColumns.map((column) => `${Tenant.tableName}.${column}`); + +export const favoriteColumns = [FavoriteColumn.EntryId, FavoriteColumn.Login] as const; + +export const selectedFavoriteColumns = tenantColumns.map( + (column) => `${Favorite.tableName}.${column}`, +); diff --git a/src/services/new/entry/get-entry-v2/index.ts b/src/services/new/entry/get-entry-v2/index.ts index a5734cee..bfc4017b 100644 --- a/src/services/new/entry/get-entry-v2/index.ts +++ b/src/services/new/entry/get-entry-v2/index.ts @@ -4,7 +4,6 @@ import {Feature, isEnabledFeature} from '../../../../components/features'; import {US_ERRORS} from '../../../../const'; import OldEntry from '../../../../db/models/entry'; import {Entry} from '../../../../db/models/new/entry'; -import {RevisionModel} from '../../../../db/models/new/revision'; import {TenantColumn} from '../../../../db/models/new/tenant'; import {DlsActions} from '../../../../types/models'; import Utils, {withTimeout} from '../../../../utils'; @@ -19,11 +18,12 @@ import { selectedRevisionColumns, selectedTenantColumns, } from './constants'; +import type {SelectedEntry, SelectedRevision} from './types'; import {checkWorkbookEntry} from './utils'; const ENTRY_QUERY_TIMEOUT = 3000; -interface GetEntryNextArgs { +interface GetEntryV2Args { entryId: string; revId?: string; branch?: 'saved' | 'published'; @@ -34,9 +34,9 @@ interface GetEntryNextArgs { includeFavorite?: boolean; } -export type GetEntryNextResult = { - entry: Entry; - revision: RevisionModel; +export type GetEntryV2Result = { + entry: SelectedEntry; + revision: SelectedRevision; includePermissionsInfo?: boolean; permissions: EntryPermissions; includeLinks?: boolean; @@ -50,8 +50,8 @@ export type GetEntryNextResult = { // eslint-disable-next-line complexity export const getEntryV2 = async ( {ctx, trx}: ServiceArgs, - args: GetEntryNextArgs, -): Promise => { + args: GetEntryV2Args, +): Promise => { const { entryId, revId, @@ -108,7 +108,7 @@ export const getEntryV2 = async ( graphRelations.push('favorite(favoriteModifier)'); } - const entry = await Entry.query(getReplica(trx)) + const entry: SelectedEntry | undefined = await Entry.query(getReplica(trx)) .select(selectedEntryColumns) .where((builder) => { builder.where({ diff --git a/src/services/new/entry/get-entry-v2/types.ts b/src/services/new/entry/get-entry-v2/types.ts new file mode 100644 index 00000000..7881286c --- /dev/null +++ b/src/services/new/entry/get-entry-v2/types.ts @@ -0,0 +1,22 @@ +import {Entry} from '../../../../db/models/new/entry'; +import {Favorite} from '../../../../db/models/new/favorite'; +import {RevisionModel} from '../../../../db/models/new/revision'; +import {Tenant} from '../../../../db/models/new/tenant'; +import {WorkbookModel} from '../../../../db/models/new/workbook'; + +import {entryColumns, favoriteColumns, revisionColumns, tenantColumns} from './constants'; + +type SelectedFavorite = Pick>; + +export type SelectedRevision = Pick>; + +type SelectedTenant = Pick>; + +export type SelectedEntry = Pick> & { + revisions?: SelectedRevision[]; + savedRevision?: SelectedRevision; + publishedRevision?: SelectedRevision; + workbook?: WorkbookModel; + favorite?: SelectedFavorite; + tenant?: SelectedTenant; +}; diff --git a/src/services/new/entry/get-entry-v2/utils.ts b/src/services/new/entry/get-entry-v2/utils.ts index 42fa5f87..3e75e10d 100644 --- a/src/services/new/entry/get-entry-v2/utils.ts +++ b/src/services/new/entry/get-entry-v2/utils.ts @@ -3,7 +3,6 @@ import {TransactionOrKnex} from 'objection'; import {Feature, isEnabledFeature} from '../../../../components/features'; import {US_ERRORS} from '../../../../const'; -import {Entry} from '../../../../db/models/new/entry'; import {WorkbookModel} from '../../../../db/models/new/workbook'; import {WorkbookPermission} from '../../../../entities/workbook'; import {getParentIds} from '../../collection/utils'; @@ -11,6 +10,8 @@ import {getReplica} from '../../utils'; import {getEntryPermissionsByWorkbook} from '../../workbook/utils'; import {checkWorkbookIsolation} from '../utils'; +import {SelectedEntry} from './types'; + const GET_PARENTS_QUERY_TIMEOUT = 3000; export const checkWorkbookEntry = async ({ @@ -22,7 +23,7 @@ export const checkWorkbookEntry = async ({ }: { ctx: AppContext; trx?: TransactionOrKnex; - entry: Entry; + entry: SelectedEntry; workbook: WorkbookModel; includePermissionsInfo?: boolean; }) => { From ef85bbf417a1648bca070800a3e55e0194025dc4 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Thu, 3 Jul 2025 12:44:33 +0300 Subject: [PATCH 09/13] Fix --- src/services/new/entry/get-entry-v2/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/new/entry/get-entry-v2/constants.ts b/src/services/new/entry/get-entry-v2/constants.ts index 1aafbef1..b8b7e182 100644 --- a/src/services/new/entry/get-entry-v2/constants.ts +++ b/src/services/new/entry/get-entry-v2/constants.ts @@ -52,6 +52,6 @@ export const selectedTenantColumns = tenantColumns.map((column) => `${Tenant.tab export const favoriteColumns = [FavoriteColumn.EntryId, FavoriteColumn.Login] as const; -export const selectedFavoriteColumns = tenantColumns.map( +export const selectedFavoriteColumns = favoriteColumns.map( (column) => `${Favorite.tableName}.${column}`, ); From 0a688c1cb5b282606a4e3f807bcd3e34141a2530 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Wed, 13 Aug 2025 11:12:25 +0300 Subject: [PATCH 10/13] Review fixes --- src/components/error-response-presenter.ts | 4 +- src/components/features/types.ts | 1 + src/const/errors.ts | 2 +- src/controllers/entries/index.ts | 44 +++++++++++--------- src/routes.ts | 11 +---- src/services/new/entry/get-entry-v2/index.ts | 21 +++++----- src/utils/promise/with-timeout.ts | 2 +- 7 files changed, 41 insertions(+), 44 deletions(-) diff --git a/src/components/error-response-presenter.ts b/src/components/error-response-presenter.ts index 1485ddc4..9ba61186 100644 --- a/src/components/error-response-presenter.ts +++ b/src/components/error-response-presenter.ts @@ -400,12 +400,12 @@ export default (error: AppError | DBError) => { }; } - case US_ERRORS.OPERATION_TIMEOUT: { + case US_ERRORS.ACTION_TIMEOUT: { return { code: 504, response: { code, - message: 'Operation timed out', + message: 'Action timed out', }, }; } diff --git a/src/components/features/types.ts b/src/components/features/types.ts index a1425481..59e6c211 100644 --- a/src/components/features/types.ts +++ b/src/components/features/types.ts @@ -4,6 +4,7 @@ export enum Feature { ColorPalettesEnabled = 'ColorPalettesEnabled', UseIpV6 = 'UseIpV6', WorkbookIsolationEnabled = 'WorkbookIsolationEnabled', + GetEntryV2Enabled = 'GetEntryV2Enabled', } export type FeaturesConfig = { diff --git a/src/const/errors.ts b/src/const/errors.ts index 43b6be61..42e2c801 100644 --- a/src/const/errors.ts +++ b/src/const/errors.ts @@ -50,5 +50,5 @@ export const US_ERRORS = { COLLECTION_WITH_WORKBOOK_TEMPLATE_CANT_BE_DELETED: 'COLLECTION_WITH_WORKBOOK_TEMPLATE_CANT_BE_DELETED', TENANT_ID_MISSING_IN_CONTEXT: 'TENANT_ID_MISSING_IN_CONTEXT', - OPERATION_TIMEOUT: 'OPERATION_TIMEOUT', + ACTION_TIMEOUT: 'ACTION_TIMEOUT', }; diff --git a/src/controllers/entries/index.ts b/src/controllers/entries/index.ts index 2261d9b4..28cb6275 100644 --- a/src/controllers/entries/index.ts +++ b/src/controllers/entries/index.ts @@ -1,5 +1,6 @@ import {Request, Response} from '@gravity-ui/expresskit'; +import {Feature, isEnabledFeature} from '../../components/features'; import {prepareResponseAsync} from '../../components/response-presenter'; import {US_MASTER_TOKEN_HEADER} from '../../const'; import {EntryScope} from '../../db/models/new/entry/types'; @@ -45,28 +46,31 @@ export default { createEntryController, createEntryAltController, getEntriesController, - getEntryV2Controller, getEntry: async (req: Request, res: Response) => { - const {query, params} = req; - - const result = await getEntry( - {ctx: req.ctx}, - { - entryId: params.entryId, - branch: query.branch as GetEntryArgs['branch'], - revId: query.revId as GetEntryArgs['revId'], - includePermissionsInfo: isTrueArg(query.includePermissionsInfo), - includeLinks: isTrueArg(query.includeLinks), - includeServicePlan: isTrueArg(query.includeServicePlan), - includeTenantFeatures: isTrueArg(query.includeTenantFeatures), - }, - ); - const formattedResponse = await formatGetEntryResponse(req.ctx, result); - - const {code, response} = await prepareResponseAsync({data: formattedResponse}); - - res.status(code).send(response); + if (isEnabledFeature(req.ctx, Feature.GetEntryV2Enabled)) { + await getEntryV2Controller(req, res); + } else { + const {query, params} = req; + + const result = await getEntry( + {ctx: req.ctx}, + { + entryId: params.entryId, + branch: query.branch as GetEntryArgs['branch'], + revId: query.revId as GetEntryArgs['revId'], + includePermissionsInfo: isTrueArg(query.includePermissionsInfo), + includeLinks: isTrueArg(query.includeLinks), + includeServicePlan: isTrueArg(query.includeServicePlan), + includeTenantFeatures: isTrueArg(query.includeTenantFeatures), + }, + ); + const formattedResponse = await formatGetEntryResponse(req.ctx, result); + + const {code, response} = await prepareResponseAsync({data: formattedResponse}); + + res.status(code).send(response); + } }, getEntryMeta: async (req: Request, res: Response) => { diff --git a/src/routes.ts b/src/routes.ts index 6c529ae7..686a2651 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -64,21 +64,12 @@ export function getRoutes(_nodekit: NodeKit, options: GetRoutesOptions) { route: 'GET /v1/entries/:entryId', handler: entries.getEntry, }), - getEntryV2: makeRoute({ - route: 'GET /v2/entries/:entryId', - handler: entries.getEntryV2Controller, - }), privateGetEntry: makeRoute({ route: 'GET /private/entries/:entryId', handler: entries.getEntry, authPolicy: AuthPolicy.disabled, private: true, - }), - privateGetEntryV2: makeRoute({ - route: 'GET /v2/private/entries/:entryId', - handler: entries.getEntryV2Controller, - authPolicy: AuthPolicy.disabled, - private: true, + requireCtxTenantId: true, }), getEntryMeta: makeRoute({ diff --git a/src/services/new/entry/get-entry-v2/index.ts b/src/services/new/entry/get-entry-v2/index.ts index bfc4017b..5cfc5415 100644 --- a/src/services/new/entry/get-entry-v2/index.ts +++ b/src/services/new/entry/get-entry-v2/index.ts @@ -3,14 +3,14 @@ import {AppError} from '@gravity-ui/nodekit'; import {Feature, isEnabledFeature} from '../../../../components/features'; import {US_ERRORS} from '../../../../const'; import OldEntry from '../../../../db/models/entry'; -import {Entry} from '../../../../db/models/new/entry'; +import {Entry, EntryColumn} from '../../../../db/models/new/entry'; import {TenantColumn} from '../../../../db/models/new/tenant'; import {DlsActions} from '../../../../types/models'; import Utils, {withTimeout} from '../../../../utils'; import {ServiceArgs} from '../../types'; import {getReplica} from '../../utils'; import {EntryPermissions} from '../types'; -import {checkFetchedEntry, checkWorkbookIsolation} from '../utils'; +import {checkWorkbookIsolation} from '../utils'; import { selectedEntryColumns, @@ -77,14 +77,17 @@ export const getEntryV2 = async ( const registry = ctx.get('registry'); const {DLS} = registry.common.classes.get(); - const {isPrivateRoute, user, onlyPublic, onlyMirrored} = ctx.get('info'); + const {isPrivateRoute, user, onlyPublic, onlyMirrored, tenantId} = ctx.get('info'); const {getEntryBeforeDbRequestHook, checkEmbedding, getEntryResolveUserLogin} = registry.common.functions.get(); - let userLoginPromise; + let userLoginPromise: Promise = Promise.resolve(undefined); + if (includeFavorite) { - userLoginPromise = user.login ? user.login : getEntryResolveUserLogin({ctx}); + userLoginPromise = user.login + ? Promise.resolve(user.login) + : getEntryResolveUserLogin({ctx}); } const [userLogin] = await Promise.all([ @@ -112,8 +115,9 @@ export const getEntryV2 = async ( .select(selectedEntryColumns) .where((builder) => { builder.where({ - [`${Entry.tableName}.entryId`]: entryId, - [`${Entry.tableName}.isDeleted`]: false, + [`${Entry.tableName}.${EntryColumn.EntryId}`]: entryId, + [`${Entry.tableName}.${EntryColumn.TenantId}`]: tenantId, + [`${Entry.tableName}.${EntryColumn.IsDeleted}`]: false, }); if (onlyPublic) { @@ -210,8 +214,6 @@ export const getEntryV2 = async ( workbookId: null, }); } - - await checkFetchedEntry(ctx, entry, getReplica(trx)); } } @@ -221,7 +223,6 @@ export const getEntryV2 = async ( } let tenantFeatures: Record | undefined; - if (includeTenantFeatures) { tenantFeatures = entry.tenant[TenantColumn.Features] || undefined; } diff --git a/src/utils/promise/with-timeout.ts b/src/utils/promise/with-timeout.ts index 66fd43ed..e2c0a105 100644 --- a/src/utils/promise/with-timeout.ts +++ b/src/utils/promise/with-timeout.ts @@ -10,7 +10,7 @@ export const withTimeout = async ( setTimeout(() => { reject( new AppError(errorMessage, { - code: US_ERRORS.OPERATION_TIMEOUT, + code: US_ERRORS.ACTION_TIMEOUT, }), ); }, timeoutMs); From 3987903f62a86f554305ba372b06e293ff549ae3 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Wed, 13 Aug 2025 13:39:30 +0300 Subject: [PATCH 11/13] Disable manualDecodeId --- src/controllers/entries/get-entry/index.ts | 6 ++---- src/tests/int/routes.ts | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/controllers/entries/get-entry/index.ts b/src/controllers/entries/get-entry/index.ts index 605fd5ab..54491a10 100644 --- a/src/controllers/entries/get-entry/index.ts +++ b/src/controllers/entries/get-entry/index.ts @@ -9,11 +9,11 @@ import {getEntryResult} from './response-model'; const requestSchema = { params: z.object({ - entryId: zc.encodedId(), + entryId: z.string(), }), query: z.object({ branch: z.enum(['saved', 'published']).optional(), - revId: zc.encodedId().optional(), + revId: z.string().optional(), includePermissionsInfo: zc.stringBoolean().optional(), includeLinks: zc.stringBoolean().optional(), includeServicePlan: zc.stringBoolean().optional(), @@ -62,5 +62,3 @@ getEntryController.api = { }, }, }; - -getEntryController.manualDecodeId = true; diff --git a/src/tests/int/routes.ts b/src/tests/int/routes.ts index abc36ad7..ff8c624d 100644 --- a/src/tests/int/routes.ts +++ b/src/tests/int/routes.ts @@ -1,7 +1,5 @@ export const routes = { entries: '/v1/entries', - entriesGetEntryV2: (entryId: string) => `/v2/entries/${entryId}`, - entriesPrivateGetEntryV2: (entryId: string) => `/v2/private/entries/${entryId}`, privateEntries: '/private/entries', privateCreateEntry: '/private/createEntry', favorites: '/v1/favorites', From 4cc5251d763c7450c316dbceab828cb7e873c940 Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Wed, 13 Aug 2025 14:50:21 +0300 Subject: [PATCH 12/13] Fix tenant check --- src/routes.ts | 1 - src/services/new/entry/get-entry-v2/index.ts | 33 +++++++++++++------- src/services/new/entry/get-entry-v2/utils.ts | 9 ------ 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/routes.ts b/src/routes.ts index 04cdbf4f..e050fb02 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -70,7 +70,6 @@ export function getRoutes(_nodekit: NodeKit, options: GetRoutesOptions) { handler: entries.getEntry, authPolicy: AuthPolicy.disabled, private: true, - requireCtxTenantId: true, }), getEntryMeta: makeRoute({ diff --git a/src/services/new/entry/get-entry-v2/index.ts b/src/services/new/entry/get-entry-v2/index.ts index 5cfc5415..921ffa60 100644 --- a/src/services/new/entry/get-entry-v2/index.ts +++ b/src/services/new/entry/get-entry-v2/index.ts @@ -111,13 +111,19 @@ export const getEntryV2 = async ( graphRelations.push('favorite(favoriteModifier)'); } + const checkEntryTenantEnabled = !isPrivateRoute && !onlyPublic && !onlyMirrored && !isEmbedding; + const entry: SelectedEntry | undefined = await Entry.query(getReplica(trx)) .select(selectedEntryColumns) .where((builder) => { builder.where({ [`${Entry.tableName}.${EntryColumn.EntryId}`]: entryId, - [`${Entry.tableName}.${EntryColumn.TenantId}`]: tenantId, [`${Entry.tableName}.${EntryColumn.IsDeleted}`]: false, + ...(checkEntryTenantEnabled + ? { + [`${Entry.tableName}.${EntryColumn.TenantId}`]: tenantId, + } + : {}), }); if (onlyPublic) { @@ -163,6 +169,20 @@ export const getEntryV2 = async ( }); } + const checkWorkbookIsolationEnabled = + !isPrivateRoute && + !onlyPublic && + !onlyMirrored && + !isEmbedding && + isEnabledFeature(ctx, Feature.WorkbookIsolationEnabled); + + if (checkWorkbookIsolationEnabled) { + checkWorkbookIsolation({ + ctx, + workbookId: entry.workbookId, + }); + } + const {isNeedBypassEntryByKey, getServicePlan} = registry.common.functions.get(); const dlsBypassByKeyEnabled = isNeedBypassEntryByKey(ctx, entry.key as string); @@ -204,17 +224,6 @@ export const getEntryV2 = async ( {timeoutMs: 3000, errorMessage: 'DLS.checkPermission timeout'}, ); } - - const checkEntryEnabled = !isPrivateRoute && !onlyPublic && !onlyMirrored && !isEmbedding; - - if (checkEntryEnabled) { - if (isEnabledFeature(ctx, Feature.WorkbookIsolationEnabled)) { - checkWorkbookIsolation({ - ctx, - workbookId: null, - }); - } - } } let servicePlan: string | undefined; diff --git a/src/services/new/entry/get-entry-v2/utils.ts b/src/services/new/entry/get-entry-v2/utils.ts index 3e75e10d..8f9b23d9 100644 --- a/src/services/new/entry/get-entry-v2/utils.ts +++ b/src/services/new/entry/get-entry-v2/utils.ts @@ -1,14 +1,12 @@ import {AppContext, AppError} from '@gravity-ui/nodekit'; import {TransactionOrKnex} from 'objection'; -import {Feature, isEnabledFeature} from '../../../../components/features'; import {US_ERRORS} from '../../../../const'; import {WorkbookModel} from '../../../../db/models/new/workbook'; import {WorkbookPermission} from '../../../../entities/workbook'; import {getParentIds} from '../../collection/utils'; import {getReplica} from '../../utils'; import {getEntryPermissionsByWorkbook} from '../../workbook/utils'; -import {checkWorkbookIsolation} from '../utils'; import {SelectedEntry} from './types'; @@ -27,13 +25,6 @@ export const checkWorkbookEntry = async ({ workbook: WorkbookModel; includePermissionsInfo?: boolean; }) => { - if (isEnabledFeature(ctx, Feature.WorkbookIsolationEnabled)) { - checkWorkbookIsolation({ - ctx, - workbookId: workbook.workbookId, - }); - } - let parentIds: string[] = []; if (workbook.collectionId !== null) { From 3f117fe3b1a0893f2a282b16f41c7643035c85fe Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Thu, 14 Aug 2025 13:55:58 +0300 Subject: [PATCH 13/13] Review fixes --- src/components/error-response-presenter.ts | 1 + src/const/errors.ts | 1 + src/services/new/entry/get-entry-v2/index.ts | 14 +++++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/error-response-presenter.ts b/src/components/error-response-presenter.ts index 9ba61186..11500ff2 100644 --- a/src/components/error-response-presenter.ts +++ b/src/components/error-response-presenter.ts @@ -317,6 +317,7 @@ export default (error: AppError | DBError) => { }; } + case US_ERRORS.ENTRY_AND_WORKBOOK_TENANT_MISMATCH: case US_ERRORS.TOO_MANY_COLOR_PALETTES: { return { code: 500, diff --git a/src/const/errors.ts b/src/const/errors.ts index 42e2c801..1d3ffec7 100644 --- a/src/const/errors.ts +++ b/src/const/errors.ts @@ -51,4 +51,5 @@ export const US_ERRORS = { 'COLLECTION_WITH_WORKBOOK_TEMPLATE_CANT_BE_DELETED', TENANT_ID_MISSING_IN_CONTEXT: 'TENANT_ID_MISSING_IN_CONTEXT', ACTION_TIMEOUT: 'ACTION_TIMEOUT', + ENTRY_AND_WORKBOOK_TENANT_MISMATCH: 'ENTRY_AND_WORKBOOK_TENANT_MISMATCH', }; diff --git a/src/services/new/entry/get-entry-v2/index.ts b/src/services/new/entry/get-entry-v2/index.ts index 921ffa60..647ecc3e 100644 --- a/src/services/new/entry/get-entry-v2/index.ts +++ b/src/services/new/entry/get-entry-v2/index.ts @@ -190,7 +190,19 @@ export const getEntryV2 = async ( let dlsPermissions: any; // TODO: Update the type after refactoring DLS.checkPermission(...) let iamPermissions: Optional; - if (entry.workbook) { + if (entry.workbookId) { + if (!entry.workbook) { + throw new AppError(US_ERRORS.WORKBOOK_NOT_EXISTS, { + code: US_ERRORS.WORKBOOK_NOT_EXISTS, + }); + } + + if (entry.tenantId !== entry.workbook.tenantId) { + throw new AppError(US_ERRORS.ENTRY_AND_WORKBOOK_TENANT_MISMATCH, { + code: US_ERRORS.ENTRY_AND_WORKBOOK_TENANT_MISMATCH, + }); + } + const checkWorkbookEnabled = !isPrivateRoute && !onlyPublic && !onlyMirrored && !isEmbedding;