From 344611c3d2e55c2696dc514198bd15e05a98fd8d Mon Sep 17 00:00:00 2001 From: Sergei Samokhvalov Date: Tue, 23 Jan 2024 15:46:47 +0300 Subject: [PATCH] Add join favorites for getting workbook entries (#47) * Add join favorites for getting workbook entries * Add isFavorite to response object in testts * Add isFavorite to response object in testts * Add where with login and tenant for query * Add new method findWithPagination * Add new class JoinedEntryFavorite * Add updatedAt field for joined entry * Add updatedBy field for joined entry * Change fields for select * Fix tests * Fix private tests * Add findPage to JoinedEntryRevisionFavorite, add formatter - formatGetWorkbookContent * Fix tests * Change format response * Refactor * Fix after review * Rm projectId in where --- src/controllers/workbooks.ts | 4 +- src/db/presentations/constants.ts | 21 +++++ src/db/presentations/index.ts | 1 + .../joined-entry-favorite/index.ts | 89 +++++++++++++++++++ .../joined-entry-revision-favorite/index.ts | 41 +++++++-- .../joined-entry-revision/index.ts | 22 +---- src/db/presentations/utils.ts | 11 +++ .../formatters/format-get-workbook-content.ts | 43 +++++++++ src/services/new/workbook/formatters/index.ts | 1 + .../new/workbook/get-workbook-content.ts | 66 +++++++------- .../int/common/workbooks.private.test.ts | 2 + src/tests/int/common/workbooks.test.ts | 4 + 12 files changed, 246 insertions(+), 59 deletions(-) create mode 100644 src/db/presentations/constants.ts create mode 100644 src/db/presentations/joined-entry-favorite/index.ts create mode 100644 src/db/presentations/utils.ts create mode 100644 src/services/new/workbook/formatters/format-get-workbook-content.ts diff --git a/src/controllers/workbooks.ts b/src/controllers/workbooks.ts index 8815fe87..dffeaa65 100644 --- a/src/controllers/workbooks.ts +++ b/src/controllers/workbooks.ts @@ -27,6 +27,7 @@ import { formatWorkbooksList, formatSetWorkbookIsTemplate, formatRestoreWorkbook, + formatGetWorkbookContent, } from '../services/new/workbook/formatters'; export default { @@ -84,7 +85,8 @@ export default { }, ); - const {code, response} = prepareResponse({data: result}); + const formattedResponse = formatGetWorkbookContent(result); + const {code, response} = prepareResponse({data: formattedResponse}); res.status(code).send(response); }, diff --git a/src/db/presentations/constants.ts b/src/db/presentations/constants.ts new file mode 100644 index 00000000..35b0d361 --- /dev/null +++ b/src/db/presentations/constants.ts @@ -0,0 +1,21 @@ +export const selectedEntryColumns = [ + 'scope', + 'type', + 'key', + 'innerMeta', + 'createdBy', + 'createdAt', + 'isDeleted', + 'deletedAt', + 'hidden', + 'displayKey', + 'entryId', + 'savedId', + 'publishedId', + 'tenantId', + 'name', + 'sortName', + 'public', + 'unversionedData', + 'workbookId', +] as const; diff --git a/src/db/presentations/index.ts b/src/db/presentations/index.ts index e4182344..d5c1de4a 100644 --- a/src/db/presentations/index.ts +++ b/src/db/presentations/index.ts @@ -3,3 +3,4 @@ export * from './joined-entry-revision-favorite'; export * from './joined-migration-tenant'; export * from './joined-entry-migration-tenant'; export * from './joined-embed-embedding-secret'; +export * from './joined-entry-favorite'; diff --git a/src/db/presentations/joined-entry-favorite/index.ts b/src/db/presentations/joined-entry-favorite/index.ts new file mode 100644 index 00000000..4a1639b7 --- /dev/null +++ b/src/db/presentations/joined-entry-favorite/index.ts @@ -0,0 +1,89 @@ +import type {Knex} from 'knex'; +import {TransactionOrKnex, raw, Modifier, Page} from 'objection'; +import {Model} from '../..'; +import {Entry} from '../../models/new/entry'; +import {Favorite} from '../../models/new/favorite'; + +import {leftJoinFavorite} from '../utils'; + +import {selectedEntryColumns} from '../constants'; + +const selectedColumns = [ + ...selectedEntryColumns.map((col) => `${Entry.tableName}.${col}`), + raw(`CASE WHEN ${Favorite.tableName}.entry_id IS NULL THEN FALSE ELSE TRUE END AS is_favorite`), +]; + +export type JoinedEntryFavoriteColumns = Pick> & { + isFavorite: boolean; +}; + +export class JoinedEntryFavorite extends Model { + static get tableName() { + return Entry.tableName; + } + + static get idColumn() { + return Entry.idColumn; + } + + static find({ + where, + userLogin, + trx, + }: { + where: Record | ((builder: Knex.QueryBuilder) => void); + userLogin: string; + trx: TransactionOrKnex; + }) { + return JoinedEntryFavorite.query(trx) + .select(selectedColumns) + .leftJoin(Favorite.tableName, leftJoinFavorite(userLogin)) + .where(where) + .timeout(JoinedEntryFavorite.DEFAULT_QUERY_TIMEOUT) as unknown as Promise< + JoinedEntryFavoriteColumns[] + >; + } + + static findOne({ + where, + userLogin, + trx, + }: { + where: Record | ((builder: Knex.QueryBuilder) => void); + userLogin: string; + trx: TransactionOrKnex; + }) { + return JoinedEntryFavorite.query(trx) + .select(selectedColumns) + .leftJoin(Favorite.tableName, leftJoinFavorite(userLogin)) + .where(where) + .first() + .timeout(JoinedEntryFavorite.DEFAULT_QUERY_TIMEOUT) as unknown as Promise< + JoinedEntryFavoriteColumns | undefined + >; + } + + static findPage({ + where, + userLogin, + modify, + page, + pageSize, + trx, + }: { + where: Record | ((builder: Knex.QueryBuilder) => void); + trx: TransactionOrKnex; + modify: Modifier; + userLogin: string; + page: number; + pageSize: number; + }) { + return JoinedEntryFavorite.query(trx) + .select(selectedColumns) + .leftJoin(Favorite.tableName, leftJoinFavorite(userLogin)) + .where(where) + .modify(modify) + .page(page, pageSize) + .timeout(JoinedEntryFavorite.DEFAULT_QUERY_TIMEOUT) as unknown as Promise>; + } +} diff --git a/src/db/presentations/joined-entry-revision-favorite/index.ts b/src/db/presentations/joined-entry-revision-favorite/index.ts index f4fd184e..3684adcf 100644 --- a/src/db/presentations/joined-entry-revision-favorite/index.ts +++ b/src/db/presentations/joined-entry-revision-favorite/index.ts @@ -1,5 +1,5 @@ import type {Knex} from 'knex'; -import {TransactionOrKnex, raw} from 'objection'; +import {TransactionOrKnex, raw, Modifier, Page} from 'objection'; import { selectedColumns as joinedEntryRevisionColumns, joinRevision, @@ -7,21 +7,19 @@ import { JoinedEntryRevisionColumns, JoinRevisionArgs, } from '../joined-entry-revision'; + import {Entry} from '../../models/new/entry'; + import {RevisionModel} from '../../models/new/revision'; import {Favorite} from '../../models/new/favorite'; +import {leftJoinFavorite} from '../utils'; + const selectedColumns = [ ...joinedEntryRevisionColumns, raw(`CASE WHEN ${Favorite.tableName}.entry_id IS NULL THEN FALSE ELSE TRUE END AS is_favorite`), ]; -export const leftJoinFavorite = (userLogin: string) => (builder: Knex.JoinClause) => { - builder - .on(`${Favorite.tableName}.entryId`, `${Entry.tableName}.entryId`) - .andOnIn(`${Favorite.tableName}.login`, [userLogin]); -}; - export type JoinedEntryRevisionFavoriteColumns = JoinedEntryRevisionColumns & { isFavorite: boolean; }; @@ -69,4 +67,33 @@ export class JoinedEntryRevisionFavorite extends JoinedEntryRevision { JoinedEntryRevisionFavoriteColumns | undefined >; } + + static findPage({ + where, + modify, + joinRevisionArgs = {}, + userLogin, + page, + pageSize, + trx, + }: { + where: Record | ((builder: Knex.QueryBuilder) => void); + modify: Modifier; + joinRevisionArgs?: JoinRevisionArgs; + userLogin: string; + page: number; + pageSize: number; + trx: TransactionOrKnex; + }) { + return JoinedEntryRevisionFavorite.query(trx) + .select(selectedColumns) + .join(RevisionModel.tableName, joinRevision(joinRevisionArgs)) + .leftJoin(Favorite.tableName, leftJoinFavorite(userLogin)) + .where(where) + .modify(modify) + .page(page, pageSize) + .timeout(JoinedEntryRevisionFavorite.DEFAULT_QUERY_TIMEOUT) as unknown as Promise< + Page + >; + } } diff --git a/src/db/presentations/joined-entry-revision/index.ts b/src/db/presentations/joined-entry-revision/index.ts index 4349a2ef..d2ff4c35 100644 --- a/src/db/presentations/joined-entry-revision/index.ts +++ b/src/db/presentations/joined-entry-revision/index.ts @@ -4,27 +4,7 @@ import {Model} from '../..'; import {Entry} from '../../models/new/entry'; import {RevisionModel} from '../../models/new/revision'; -const selectedEntryColumns = [ - 'scope', - 'type', - 'key', - 'innerMeta', - 'createdBy', - 'createdAt', - 'isDeleted', - 'deletedAt', - 'hidden', - 'displayKey', - 'entryId', - 'savedId', - 'publishedId', - 'tenantId', - 'name', - 'sortName', - 'public', - 'unversionedData', - 'workbookId', -] as const; +import {selectedEntryColumns} from '../constants'; const selectedRevisionColumns = [ 'data', diff --git a/src/db/presentations/utils.ts b/src/db/presentations/utils.ts new file mode 100644 index 00000000..fdf05146 --- /dev/null +++ b/src/db/presentations/utils.ts @@ -0,0 +1,11 @@ +import type {Knex} from 'knex'; + +import {Entry} from '../models/new/entry'; + +import {Favorite} from '../models/new/favorite'; + +export const leftJoinFavorite = (userLogin: string) => (builder: Knex.JoinClause) => { + builder + .on(`${Favorite.tableName}.entryId`, `${Entry.tableName}.entryId`) + .andOnIn(`${Favorite.tableName}.login`, [userLogin]); +}; diff --git a/src/services/new/workbook/formatters/format-get-workbook-content.ts b/src/services/new/workbook/formatters/format-get-workbook-content.ts new file mode 100644 index 00000000..28488f05 --- /dev/null +++ b/src/services/new/workbook/formatters/format-get-workbook-content.ts @@ -0,0 +1,43 @@ +import {EntryPermissions} from '../../entry/types'; +import {JoinedEntryRevisionFavoriteColumns} from '../../../../db/presentations/joined-entry-revision-favorite'; + +export type GetContentResult = { + permissions?: EntryPermissions; + isLocked: boolean; +} & JoinedEntryRevisionFavoriteColumns; + +export const formatGetJoinedEntryRevisionFavorite = ( + joinedEntryRevisionFavorite: GetContentResult, +) => { + return { + entryId: joinedEntryRevisionFavorite.entryId, + scope: joinedEntryRevisionFavorite.scope, + type: joinedEntryRevisionFavorite.type, + key: joinedEntryRevisionFavorite.displayKey, + createdBy: joinedEntryRevisionFavorite.createdBy, + createdAt: joinedEntryRevisionFavorite.createdAt, + updatedBy: joinedEntryRevisionFavorite.updatedBy, + updatedAt: joinedEntryRevisionFavorite.updatedAt, + savedId: joinedEntryRevisionFavorite.savedId, + publishedId: joinedEntryRevisionFavorite.publishedId, + meta: joinedEntryRevisionFavorite.meta, + hidden: joinedEntryRevisionFavorite.hidden, + workbookId: joinedEntryRevisionFavorite.workbookId, + isFavorite: joinedEntryRevisionFavorite.isFavorite, + isLocked: joinedEntryRevisionFavorite.isLocked, + permissions: joinedEntryRevisionFavorite.permissions, + }; +}; + +export const formatGetWorkbookContent = ({ + entries, + nextPageToken, +}: { + entries: GetContentResult[]; + nextPageToken?: string; +}) => { + return { + entries: entries.map((entry) => formatGetJoinedEntryRevisionFavorite(entry)), + nextPageToken, + }; +}; diff --git a/src/services/new/workbook/formatters/index.ts b/src/services/new/workbook/formatters/index.ts index 3a0ffea0..2d73f9d7 100644 --- a/src/services/new/workbook/formatters/index.ts +++ b/src/services/new/workbook/formatters/index.ts @@ -6,3 +6,4 @@ export * from './format-workbook-model-with-operation'; export * from './format-workbook-models-list'; export * from './format-set-workbook-is-template'; export * from './format-restore-workbook'; +export * from './format-get-workbook-content'; diff --git a/src/services/new/workbook/get-workbook-content.ts b/src/services/new/workbook/get-workbook-content.ts index 5ad6e564..0ca9d69b 100644 --- a/src/services/new/workbook/get-workbook-content.ts +++ b/src/services/new/workbook/get-workbook-content.ts @@ -1,6 +1,5 @@ import {makeSchemaValidator} from '../../../components/validation-schema-compiler'; -import {RETURN_NAVIGATION_COLUMNS, DEFAULT_PAGE, DEFAULT_PAGE_SIZE} from '../../../const'; -import Navigation from '../../../db/models/navigation'; +import {DEFAULT_PAGE, DEFAULT_PAGE_SIZE} from '../../../const'; import {EntryScope} from '../../../db/models/new/entry/types'; import Utils, {logInfo} from '../../../utils'; import {UsPermission} from '../../../types/models'; @@ -10,6 +9,8 @@ import {getWorkbook} from './get-workbook'; import {getEntryPermissionsByWorkbook} from './utils'; import {Feature, isEnabledFeature} from '../../../components/features'; +import {JoinedEntryRevisionFavorite} from '../../../db/presentations'; + const validateArgs = makeSchemaValidator({ type: 'object', required: ['workbookId'], @@ -108,6 +109,8 @@ export const getWorkbookContent = async ( const targetTrx = getReplica(trx); + const {user, tenantId} = ctx.get('info'); + const workbook = await getWorkbook( {ctx, trx, skipValidation: true, skipCheckPermissions}, { @@ -116,15 +119,14 @@ export const getWorkbookContent = async ( }, ); - // TODO: Get rid of Navigation - const entriesPage = await Navigation.query(targetTrx) - .select(RETURN_NAVIGATION_COLUMNS) - .join('revisions', 'entries.savedId', 'revisions.revId') - .where({ - workbookId: workbookId, - isDeleted: false, - }) - .where((builder) => { + const entriesPage = await JoinedEntryRevisionFavorite.findPage({ + where: (builder) => { + builder.where({ + 'entries.tenantId': tenantId, + workbookId: workbookId, + isDeleted: false, + }); + if (isEnabledFeature(ctx, Feature.UseLimitedView) && !workbook.permissions?.view) { builder.whereNotIn('scope', ['dataset', 'connection']); } @@ -141,8 +143,8 @@ export const getWorkbookContent = async ( if (scope) { builder.whereIn('scope', Array.isArray(scope) ? scope : [scope]); } - }) - .modify((builder) => { + }, + modify: (builder) => { if (orderBy) { switch (orderBy.field) { case 'updatedAt': @@ -156,28 +158,32 @@ export const getWorkbookContent = async ( break; } } - }) - .page(page, pageSize) - .timeout(Navigation.DEFAULT_QUERY_TIMEOUT); + }, + trx: targetTrx, + userLogin: user.login, + page, + pageSize, + }); const nextPageToken = Utils.getNextPageToken(page, pageSize, entriesPage.total); - const entries: Array = - entriesPage.results.map((entry) => { - let permissions: Optional; + const entries = entriesPage.results.map((entry) => { + let permissions: Optional; - if (includePermissionsInfo) { - permissions = getEntryPermissionsByWorkbook({ - ctx, - workbook, - scope: entry.scope, - }); - } + if (includePermissionsInfo) { + permissions = getEntryPermissionsByWorkbook({ + ctx, + workbook, + scope: entry.scope, + }); + } - entry.permissions = permissions; - entry.isLocked = false; - return entry; - }); + return { + ...entry, + permissions, + isLocked: false, + }; + }); ctx.log('GET_WORKBOOK_CONTENT_FINISH'); diff --git a/src/tests/int/common/workbooks.private.test.ts b/src/tests/int/common/workbooks.private.test.ts index 6c976c22..83391263 100644 --- a/src/tests/int/common/workbooks.private.test.ts +++ b/src/tests/int/common/workbooks.private.test.ts @@ -124,6 +124,7 @@ describe('Private Entries in workboooks managment', () => { entryId: expect.any(String), hidden: false, isLocked: false, + isFavorite: false, key: expect.any(String), meta: {}, publishedId: null, @@ -207,6 +208,7 @@ describe('Private for one workboook managment', () => { isLocked: false, key: expect.any(String), meta: {}, + isFavorite: false, publishedId: null, savedId: expect.any(String), scope: 'dataset', diff --git a/src/tests/int/common/workbooks.test.ts b/src/tests/int/common/workbooks.test.ts index 81dc4949..69c1a0cf 100644 --- a/src/tests/int/common/workbooks.test.ts +++ b/src/tests/int/common/workbooks.test.ts @@ -373,6 +373,7 @@ describe('Entries in workboooks managment', () => { entryId: expect.any(String), hidden: false, isLocked: false, + isFavorite: false, key: expect.any(String), meta: {}, publishedId: null, @@ -389,6 +390,7 @@ describe('Entries in workboooks managment', () => { entryId: expect.any(String), hidden: false, isLocked: false, + isFavorite: false, key: expect.any(String), meta: {}, publishedId: null, @@ -448,6 +450,7 @@ describe('Entries in workboooks managment', () => { entryId: expect.any(String), hidden: false, isLocked: false, + isFavorite: false, key: expect.any(String), meta: {}, publishedId: null, @@ -464,6 +467,7 @@ describe('Entries in workboooks managment', () => { entryId: expect.any(String), hidden: false, isLocked: false, + isFavorite: false, key: expect.any(String), meta: {}, publishedId: null,