Merge pull request #3140 from specklesystems/fabians/core-ioc-8
chore(server): core IoC 8 - getProjectTopLevelModelsTreeFactory
This commit is contained in:
@@ -100,3 +100,20 @@ export type GetProjectTopLevelModelsTree = (
|
||||
}
|
||||
>
|
||||
>
|
||||
|
||||
export type GetModelTreeItems = (
|
||||
projectId: string,
|
||||
args: Omit<ProjectModelsTreeArgs, 'filter'>,
|
||||
options?: Partial<{
|
||||
filterOutEmptyMain: boolean
|
||||
parentModelName: string
|
||||
}>
|
||||
) => Promise<ModelsTreeItemGraphQLReturn[]>
|
||||
|
||||
export type GetModelTreeItemsTotalCount = (
|
||||
projectId: string,
|
||||
options?: Partial<{
|
||||
filterOutEmptyMain: boolean
|
||||
parentModelName: string
|
||||
}>
|
||||
) => Promise<number>
|
||||
|
||||
@@ -24,9 +24,10 @@ import {
|
||||
} from '@/modules/shared/utils/subscriptions'
|
||||
import {
|
||||
getBranchLatestCommitsFactory,
|
||||
getModelTreeItems,
|
||||
getModelTreeItemsFactory,
|
||||
getModelTreeItemsFilteredFactory,
|
||||
getModelTreeItemsFilteredTotalCountFactory,
|
||||
getModelTreeItemsTotalCountFactory,
|
||||
getPaginatedProjectModelsItemsFactory,
|
||||
getPaginatedProjectModelsTotalCountFactory,
|
||||
getStreamBranchesByNameFactory
|
||||
@@ -54,11 +55,14 @@ const getPaginatedProjectModels = getPaginatedProjectModelsFactory({
|
||||
db
|
||||
})
|
||||
})
|
||||
const getModelTreeItems = getModelTreeItemsFactory({ db })
|
||||
const getProjectTopLevelModelsTree = getProjectTopLevelModelsTreeFactory({
|
||||
getModelTreeItemsFiltered: getModelTreeItemsFilteredFactory({ db }),
|
||||
getModelTreeItemsFilteredTotalCount: getModelTreeItemsFilteredTotalCountFactory({
|
||||
db
|
||||
})
|
||||
}),
|
||||
getModelTreeItems,
|
||||
getModelTreeItemsTotalCount: getModelTreeItemsTotalCountFactory({ db })
|
||||
})
|
||||
|
||||
export = {
|
||||
|
||||
@@ -20,8 +20,10 @@ import {
|
||||
GetBranchById,
|
||||
GetBranchesByIds,
|
||||
GetBranchLatestCommits,
|
||||
GetModelTreeItems,
|
||||
GetModelTreeItemsFiltered,
|
||||
GetModelTreeItemsFilteredTotalCount,
|
||||
GetModelTreeItemsTotalCount,
|
||||
GetPaginatedProjectModelsItems,
|
||||
GetPaginatedProjectModelsTotalCount,
|
||||
GetStreamBranchByName,
|
||||
@@ -482,125 +484,136 @@ export const getModelTreeItemsFilteredTotalCountFactory =
|
||||
return parseInt(row.count || '0')
|
||||
}
|
||||
|
||||
function getModelTreeItemsBaseQuery(
|
||||
projectId: string,
|
||||
options?: Partial<{ filterOutEmptyMain: boolean; parentModelName: string }>
|
||||
) {
|
||||
const cleanInput = (
|
||||
input: string | null | undefined,
|
||||
options?: Partial<{ escapeRegexp: boolean }>
|
||||
const getModelTreeItemsBaseQueryFactory =
|
||||
(deps: { db: Knex }) =>
|
||||
(
|
||||
projectId: string,
|
||||
options?: Partial<{ filterOutEmptyMain: boolean; parentModelName: string }>
|
||||
) => {
|
||||
let clean = (input || '').toLowerCase()
|
||||
clean = trim(trim(clean), '/')
|
||||
clean = options?.escapeRegexp ? clean.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : clean
|
||||
return clean
|
||||
}
|
||||
const cleanInput = (
|
||||
input: string | null | undefined,
|
||||
options?: Partial<{ escapeRegexp: boolean }>
|
||||
) => {
|
||||
let clean = (input || '').toLowerCase()
|
||||
clean = trim(trim(clean), '/')
|
||||
clean = options?.escapeRegexp
|
||||
? clean.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
: clean
|
||||
return clean
|
||||
}
|
||||
|
||||
const { filterOutEmptyMain = true, parentModelName } = options || {}
|
||||
const cleanModelName = cleanInput(parentModelName, { escapeRegexp: true })
|
||||
const branchPartPattern = `[^/]+` // regexp for each branch part between slashes
|
||||
const { filterOutEmptyMain = true, parentModelName } = options || {}
|
||||
const cleanModelName = cleanInput(parentModelName, { escapeRegexp: true })
|
||||
const branchPartPattern = `[^/]+` // regexp for each branch part between slashes
|
||||
|
||||
const regExp = cleanModelName.length
|
||||
? // only direct children of parentModelName
|
||||
`^${cleanModelName}\\/(${branchPartPattern})`
|
||||
: // only first branch part (top level item)
|
||||
`^${branchPartPattern}`
|
||||
const regExp = cleanModelName.length
|
||||
? // only direct children of parentModelName
|
||||
`^${cleanModelName}\\/(${branchPartPattern})`
|
||||
: // only first branch part (top level item)
|
||||
`^${branchPartPattern}`
|
||||
|
||||
const branchPartMatcher = `(regexp_match("branches"."name", '${regExp}', 'i'))[1]`
|
||||
const branchPartMatcher = `(regexp_match("branches"."name", '${regExp}', 'i'))[1]`
|
||||
|
||||
const baseQuery = Branches.knex()
|
||||
.select<{ updatedAt: Date; branchPart: string; longestFullName: string }[]>([
|
||||
// for sorting by updatedAt (of any of the children)
|
||||
knex.raw('MAX(??) as "updatedAt"', [Branches.col.updatedAt]),
|
||||
// for allowing filtering by parent branch name
|
||||
knex.raw(`${branchPartMatcher} as "branchPart"`),
|
||||
// for checking for children + checking if returned row represents 'main' if filterOutEmptyMain
|
||||
knex.raw(`MAX(??) as "longestFullName"`, [Branches.col.name])
|
||||
])
|
||||
.where(Branches.col.streamId, projectId)
|
||||
.andWhere(knex.raw(`${branchPartMatcher} IS NOT NULL`))
|
||||
.groupBy('branchPart')
|
||||
.orderBy('updatedAt', 'desc')
|
||||
const baseQuery = tables
|
||||
.branches(deps.db)
|
||||
.select<{ updatedAt: Date; branchPart: string; longestFullName: string }[]>([
|
||||
// for sorting by updatedAt (of any of the children)
|
||||
knex.raw('MAX(??) as "updatedAt"', [Branches.col.updatedAt]),
|
||||
// for allowing filtering by parent branch name
|
||||
knex.raw(`${branchPartMatcher} as "branchPart"`),
|
||||
// for checking for children + checking if returned row represents 'main' if filterOutEmptyMain
|
||||
knex.raw(`MAX(??) as "longestFullName"`, [Branches.col.name])
|
||||
])
|
||||
.where(Branches.col.streamId, projectId)
|
||||
.andWhere(knex.raw(`${branchPartMatcher} IS NOT NULL`))
|
||||
.groupBy('branchPart')
|
||||
.orderBy('updatedAt', 'desc')
|
||||
|
||||
let finalQuery = baseQuery
|
||||
if (filterOutEmptyMain) {
|
||||
// Select branch id to check for attached commits
|
||||
// And count to ensure that there's only "main" and no children
|
||||
baseQuery.select([
|
||||
knex.raw(`(ARRAY_AGG(??))[1] as "lastId"`, [Branches.col.id]),
|
||||
knex.raw(`COUNT(??) as "branchCount"`, [Branches.col.id])
|
||||
])
|
||||
let finalQuery = baseQuery
|
||||
if (filterOutEmptyMain) {
|
||||
// Select branch id to check for attached commits
|
||||
// And count to ensure that there's only "main" and no children
|
||||
baseQuery.select([
|
||||
knex.raw(`(ARRAY_AGG(??))[1] as "lastId"`, [Branches.col.id]),
|
||||
knex.raw(`COUNT(??) as "branchCount"`, [Branches.col.id])
|
||||
])
|
||||
|
||||
// Wrap base query and filter out empty main
|
||||
finalQuery = knex
|
||||
.select<{ updatedAt: Date; branchPart: string; longestFullName: string }[]>('*')
|
||||
.from(baseQuery.as('sq1'))
|
||||
.where((w1) => {
|
||||
// Either there are children branches, or the branch name isn't 'main'
|
||||
// Or the branch has more than 0 commits
|
||||
w1.where((w2) => {
|
||||
w2.whereNot('branchCount', 1)
|
||||
.orWhereNot('branchPart', knex.raw('"longestFullName"'))
|
||||
.orWhereNot('longestFullName', 'main')
|
||||
}).orWhere(
|
||||
knex.raw(
|
||||
`(SELECT COUNT(*) FROM "branch_commits" WHERE "branch_commits"."branchId" = "lastId")`
|
||||
),
|
||||
'>',
|
||||
0
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
query: finalQuery,
|
||||
parentModelName: cleanInput(parentModelName)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getModelTreeItems(
|
||||
projectId: string,
|
||||
args: Omit<ProjectModelsTreeArgs, 'filter'>,
|
||||
options?: Partial<{ filterOutEmptyMain: boolean; parentModelName: string }>
|
||||
) {
|
||||
const limit = clamp(args.limit || 25, 0, 100)
|
||||
const { query, parentModelName } = getModelTreeItemsBaseQuery(projectId, options)
|
||||
|
||||
const finalQuery = knex.from(query.as('sq1'))
|
||||
finalQuery.limit(limit)
|
||||
|
||||
if (args.cursor) {
|
||||
finalQuery.andWhere('updatedAt', '<', args.cursor)
|
||||
}
|
||||
|
||||
const res = await finalQuery
|
||||
const items = res.map((i): ModelsTreeItemGraphQLReturn => {
|
||||
const fullName = parentModelName
|
||||
? `${parentModelName}/${i.branchPart}`
|
||||
: i.branchPart
|
||||
// Wrap base query and filter out empty main
|
||||
finalQuery = (deps.db().from(baseQuery.as('sq1')) as typeof baseQuery)
|
||||
.select<{ updatedAt: Date; branchPart: string; longestFullName: string }[]>('*')
|
||||
.where((w1) => {
|
||||
// Either there are children branches, or the branch name isn't 'main'
|
||||
// Or the branch has more than 0 commits
|
||||
w1.where((w2) => {
|
||||
w2.whereNot('branchCount', 1)
|
||||
.orWhereNot('branchPart', knex.raw('"longestFullName"'))
|
||||
.orWhereNot('longestFullName', 'main')
|
||||
}).orWhere(
|
||||
knex.raw(
|
||||
`(SELECT COUNT(*) FROM "branch_commits" WHERE "branch_commits"."branchId" = "lastId")`
|
||||
),
|
||||
'>',
|
||||
0
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${projectId}-${fullName}`,
|
||||
projectId,
|
||||
name: i.branchPart,
|
||||
fullName,
|
||||
updatedAt: i.updatedAt,
|
||||
hasChildren: fullName.length < i.longestFullName.length
|
||||
query: finalQuery,
|
||||
parentModelName: cleanInput(parentModelName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
export const getModelTreeItemsFactory =
|
||||
(deps: { db: Knex }): GetModelTreeItems =>
|
||||
async (
|
||||
projectId: string,
|
||||
args: Omit<ProjectModelsTreeArgs, 'filter'>,
|
||||
options?: Partial<{ filterOutEmptyMain: boolean; parentModelName: string }>
|
||||
) => {
|
||||
const limit = clamp(args.limit || 25, 0, 100)
|
||||
const { query, parentModelName } = getModelTreeItemsBaseQueryFactory(deps)(
|
||||
projectId,
|
||||
options
|
||||
)
|
||||
|
||||
export async function getModelTreeItemsTotalCount(
|
||||
projectId: string,
|
||||
options?: Partial<{ filterOutEmptyMain: boolean; parentModelName: string }>
|
||||
) {
|
||||
const { query } = getModelTreeItemsBaseQuery(projectId, options)
|
||||
const q = knex.count<{ count: string }[]>().from(query.as('sq1'))
|
||||
const [row] = await q
|
||||
return parseInt(row.count || '0')
|
||||
}
|
||||
const finalQuery = knex.from(query.as('sq1'))
|
||||
finalQuery.limit(limit)
|
||||
|
||||
if (args.cursor) {
|
||||
finalQuery.andWhere('updatedAt', '<', args.cursor)
|
||||
}
|
||||
|
||||
const res = await finalQuery
|
||||
const items = res.map((i): ModelsTreeItemGraphQLReturn => {
|
||||
const fullName = parentModelName
|
||||
? `${parentModelName}/${i.branchPart}`
|
||||
: i.branchPart
|
||||
|
||||
return {
|
||||
id: `${projectId}-${fullName}`,
|
||||
projectId,
|
||||
name: i.branchPart,
|
||||
fullName,
|
||||
updatedAt: i.updatedAt,
|
||||
hasChildren: fullName.length < i.longestFullName.length
|
||||
}
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
export const getModelTreeItemsTotalCountFactory =
|
||||
(deps: { db: Knex }): GetModelTreeItemsTotalCount =>
|
||||
async (
|
||||
projectId: string,
|
||||
options?: Partial<{ filterOutEmptyMain: boolean; parentModelName: string }>
|
||||
) => {
|
||||
const { query } = getModelTreeItemsBaseQueryFactory(deps)(projectId, options)
|
||||
const q = deps.db().count<{ count: string }[]>().from(query.as('sq1'))
|
||||
const [row] = await q
|
||||
return parseInt(row.count || '0')
|
||||
}
|
||||
|
||||
export const validateBranchName = (name: string) => {
|
||||
name = (name || '').trim()
|
||||
|
||||
@@ -5,18 +5,16 @@ import {
|
||||
StreamBranchesArgs
|
||||
} from '@/modules/core/graph/generated/graphql'
|
||||
import { getBranchesByStreamId } from '@/modules/core/services/branches'
|
||||
import {
|
||||
getModelTreeItems,
|
||||
getModelTreeItemsTotalCount
|
||||
} from '@/modules/core/repositories/branches'
|
||||
import { last } from 'lodash'
|
||||
import { Merge } from 'type-fest'
|
||||
import { ModelsTreeItemGraphQLReturn } from '@/modules/core/helpers/graphTypes'
|
||||
import { getMaximumProjectModelsPerPage } from '@/modules/shared/helpers/envHelper'
|
||||
import { BadRequestError } from '@/modules/shared/errors'
|
||||
import {
|
||||
GetModelTreeItems,
|
||||
GetModelTreeItemsFiltered,
|
||||
GetModelTreeItemsFilteredTotalCount,
|
||||
GetModelTreeItemsTotalCount,
|
||||
GetPaginatedProjectModelsItems,
|
||||
GetPaginatedProjectModelsTotalCount,
|
||||
GetProjectTopLevelModelsTree
|
||||
@@ -61,6 +59,8 @@ export const getProjectTopLevelModelsTreeFactory =
|
||||
(deps: {
|
||||
getModelTreeItemsFiltered: GetModelTreeItemsFiltered
|
||||
getModelTreeItemsFilteredTotalCount: GetModelTreeItemsFilteredTotalCount
|
||||
getModelTreeItems: GetModelTreeItems
|
||||
getModelTreeItemsTotalCount: GetModelTreeItemsTotalCount
|
||||
}): GetProjectTopLevelModelsTree =>
|
||||
async (
|
||||
projectId: string,
|
||||
@@ -86,8 +86,8 @@ export const getProjectTopLevelModelsTreeFactory =
|
||||
totalCount = filteredTotalCount
|
||||
} else {
|
||||
const [unfilteredItems, unfilteredTotalCount] = await Promise.all([
|
||||
getModelTreeItems(projectId, args, options),
|
||||
getModelTreeItemsTotalCount(projectId, options)
|
||||
deps.getModelTreeItems(projectId, args, options),
|
||||
deps.getModelTreeItemsTotalCount(projectId, options)
|
||||
])
|
||||
|
||||
items = unfilteredItems
|
||||
|
||||
Reference in New Issue
Block a user