diff --git a/packages/server/modules/core/domain/objects/operations.ts b/packages/server/modules/core/domain/objects/operations.ts index 3a118546b..50543219c 100644 --- a/packages/server/modules/core/domain/objects/operations.ts +++ b/packages/server/modules/core/domain/objects/operations.ts @@ -6,7 +6,7 @@ import { SpeckleObjectClosureEntry } from '@/modules/core/domain/objects/types' import { BatchedSelectOptions } from '@/modules/shared/helpers/dbHelper' -import { Nullable, Optional } from '@speckle/shared' +import { MaybeNullOrUndefined, Nullable, Optional } from '@speckle/shared' import { Knex } from 'knex' import type stream from 'node:stream' @@ -54,6 +54,18 @@ export type GetObjectChildrenStream = (params: { objectId: string }) => Promise> +export type GetObjectChildren = (params: { + streamId: string + objectId: string + limit?: MaybeNullOrUndefined + depth?: MaybeNullOrUndefined + select?: MaybeNullOrUndefined + cursor?: MaybeNullOrUndefined +}) => Promise<{ + objects: Omit[] + cursor: string | null +}> + export type CreateObject = (params: { streamId: string object: RawSpeckleObject diff --git a/packages/server/modules/core/graph/resolvers/objects.ts b/packages/server/modules/core/graph/resolvers/objects.ts index 311e75530..9809a73a7 100644 --- a/packages/server/modules/core/graph/resolvers/objects.ts +++ b/packages/server/modules/core/graph/resolvers/objects.ts @@ -1,13 +1,11 @@ import { authorizeResolver } from '@/modules/shared' -import { - getObjectChildren, - getObjectChildrenQuery -} from '@/modules/core/services/objects' +import { getObjectChildrenQuery } from '@/modules/core/services/objects' import { isNonNullable, Roles } from '@speckle/shared' import { Resolvers } from '@/modules/core/graph/generated/graphql' import { + getObjectChildrenFactory, getObjectFactory, storeClosuresIfNotFoundFactory, storeObjectsIfNotFoundFactory @@ -20,6 +18,7 @@ const createObjects = createObjectsFactory({ storeObjectsIfNotFoundFactory: storeObjectsIfNotFoundFactory({ db }), storeClosuresIfNotFound: storeClosuresIfNotFoundFactory({ db }) }) +const getObjectChildren = getObjectChildrenFactory({ db }) const getStreamObject: NonNullable['object'] = async function object(parent, args) { @@ -42,14 +41,22 @@ export = { objectId: parent.id, limit: args.limit, depth: args.depth, - select: args.select, + select: args.select?.filter(isNonNullable), cursor: args.cursor }) - result.objects.forEach((x) => (x.streamId = parent.streamId)) + + // Hacky typing here, but I want to avoid filling up memory with a new array of new objects w/ .map() + const objects = result.objects as Array< + (typeof result)['objects'][number] & { + streamId: string + } + > + objects.forEach((x) => (x.streamId = parent.streamId)) + return { totalCount: parent.totalChildrenCount || 0, cursor: result.cursor, - objects: result.objects + objects } } diff --git a/packages/server/modules/core/helpers/graphTypes.ts b/packages/server/modules/core/helpers/graphTypes.ts index 6e01d68fe..b120552d4 100644 --- a/packages/server/modules/core/helpers/graphTypes.ts +++ b/packages/server/modules/core/helpers/graphTypes.ts @@ -62,7 +62,10 @@ export type ModelsTreeItemGraphQLReturn = Omit /** * Return type for top-level mutations groupings like `projectMutations`, `activeUserMutations` etc. diff --git a/packages/server/modules/core/repositories/objects.ts b/packages/server/modules/core/repositories/objects.ts index 09b2b6611..cd2077875 100644 --- a/packages/server/modules/core/repositories/objects.ts +++ b/packages/server/modules/core/repositories/objects.ts @@ -10,6 +10,7 @@ import { GetBatchedStreamObjects, GetFormattedObject, GetObject, + GetObjectChildren, GetObjectChildrenStream, GetStreamObjects, StoreClosuresIfNotFound, @@ -19,6 +20,7 @@ import { } from '@/modules/core/domain/objects/operations' import { SpeckleObject } from '@/modules/core/domain/objects/types' import { SetOptional } from 'type-fest' +import { set, toNumber } from 'lodash' const ObjectChildrenClosure = buildTableHelper('object_children_closure', [ 'parent', @@ -163,3 +165,87 @@ export const getObjectChildrenStreamFactory = .orderBy('objects.id') return q.stream({ highWaterMark: 500 }) } + +export const getObjectChildrenFactory = + (deps: { db: Knex }): GetObjectChildren => + async ({ streamId, objectId, limit, depth, select, cursor }) => { + limit = toNumber(limit || 0) || 50 + depth = toNumber(depth || 0) || 1000 + + let fullObjectSelect = false + + const q = deps.db.with( + 'object_children_closure', + knex.raw( + `SELECT objects.id as parent, d.key as child, d.value as mindepth, ? as "streamId" + FROM objects + JOIN jsonb_each_text(objects.data->'__closure') d ON true + where objects.id = ?`, + [streamId, objectId] + ) + ) + + if (Array.isArray(select)) { + select.forEach((field, index) => { + q.select( + knex.raw('jsonb_path_query(data, :path) as :name:', { + path: '$.' + field, + name: '' + index + }) + ) + }) + } else { + fullObjectSelect = true + q.select('data') + } + + q.select('id') + q.select('createdAt') + q.select('speckleType') + q.select('totalChildrenCount') + + q.from('object_children_closure') + + q.rightJoin('objects', function () { + this.on('objects.streamId', '=', 'object_children_closure.streamId').andOn( + 'objects.id', + '=', + 'object_children_closure.child' + ) + }) + .where( + knex.raw('object_children_closure."streamId" = ? AND parent = ?', [ + streamId, + objectId + ]) + ) + .andWhere(knex.raw('object_children_closure.mindepth < ?', [depth])) + .andWhere(knex.raw('id > ?', [cursor ? cursor : '0'])) + .orderBy('objects.id') + .limit(limit) + + const rows = await q + + if (rows.length === 0) { + return { objects: rows, cursor: null } + } + + if (!fullObjectSelect) + rows.forEach((o, i, arr) => { + const no = { + id: o.id, + createdAt: o.createdAt, + speckleType: o.speckleType, + totalChildrenCount: o.totalChildrenCount, + data: {} + } + let k = 0 + for (const field of select || []) { + set(no.data, field, o[k++]) + } + arr[i] = no + }) + + const lastId = rows[rows.length - 1].id + return { objects: rows, cursor: lastId } + } diff --git a/packages/server/modules/core/services/objects.js b/packages/server/modules/core/services/objects.js index 0cdabf4c4..b4d068f44 100644 --- a/packages/server/modules/core/services/objects.js +++ b/packages/server/modules/core/services/objects.js @@ -5,91 +5,6 @@ const knex = require(`@/db/knex`) const Objects = () => knex('objects') module.exports = { - /** - * @returns {Promise<{objects: import('@/modules/core/helpers/types').ObjectRecord[], cursor: string | null}>} - */ - async getObjectChildren({ streamId, objectId, limit, depth, select, cursor }) { - limit = parseInt(limit) || 50 - depth = parseInt(depth) || 1000 - - let fullObjectSelect = false - - const q = knex.with( - 'object_children_closure', - knex.raw( - `SELECT objects.id as parent, d.key as child, d.value as mindepth, ? as "streamId" - FROM objects - JOIN jsonb_each_text(objects.data->'__closure') d ON true - where objects.id = ?`, - [streamId, objectId] - ) - ) - - if (Array.isArray(select)) { - select.forEach((field, index) => { - q.select( - knex.raw('jsonb_path_query(data, :path) as :name:', { - path: '$.' + field, - name: '' + index - }) - ) - }) - } else { - fullObjectSelect = true - q.select('data') - } - - q.select('id') - q.select('createdAt') - q.select('speckleType') - q.select('totalChildrenCount') - - q.from('object_children_closure') - - q.rightJoin('objects', function () { - this.on('objects.streamId', '=', 'object_children_closure.streamId').andOn( - 'objects.id', - '=', - 'object_children_closure.child' - ) - }) - .where( - knex.raw('object_children_closure."streamId" = ? AND parent = ?', [ - streamId, - objectId - ]) - ) - .andWhere(knex.raw('object_children_closure.mindepth < ?', [depth])) - .andWhere(knex.raw('id > ?', [cursor ? cursor : '0'])) - .orderBy('objects.id') - .limit(limit) - - const rows = await q - - if (rows.length === 0) { - return { objects: rows, cursor: null } - } - - if (!fullObjectSelect) - rows.forEach((o, i, arr) => { - const no = { - id: o.id, - createdAt: o.createdAt, - speckleType: o.speckleType, - totalChildrenCount: o.totalChildrenCount, - data: {} - } - let k = 0 - for (const field of select) { - set(no.data, field, o[k++]) - } - arr[i] = no - }) - - const lastId = rows[rows.length - 1].id - return { objects: rows, cursor: lastId } - }, - /** * * This query is inefficient on larger sets (n * 10k objects) as we need to return the total count on an arbitrarily (user) defined selection of objects. diff --git a/packages/server/modules/core/tests/objects.spec.js b/packages/server/modules/core/tests/objects.spec.js index 5480b679e..f61ca3ac4 100644 --- a/packages/server/modules/core/tests/objects.spec.js +++ b/packages/server/modules/core/tests/objects.spec.js @@ -7,11 +7,7 @@ const { cloneDeep, times, random, padStart } = require('lodash') const { beforeEachContext } = require('@/test/hooks') const { getAnIdForThisOnePlease } = require('@/test/helpers') -const { - getObjects, - getObjectChildren, - getObjectChildrenQuery -} = require('../services/objects') +const { getObjects, getObjectChildrenQuery } = require('../services/objects') const { getStreamFactory, createStreamFactory @@ -86,7 +82,8 @@ const { storeClosuresIfNotFoundFactory, storeObjectsIfNotFoundFactory, getFormattedObjectFactory, - getObjectChildrenStreamFactory + getObjectChildrenStreamFactory, + getObjectChildrenFactory } = require('@/modules/core/repositories/objects') const sampleCommit = JSON.parse(`{ @@ -190,6 +187,7 @@ const createObjects = createObjectsFactory({ }) const getObject = getFormattedObjectFactory({ db }) const getObjectChildrenStream = getObjectChildrenStreamFactory({ db }) +const getObjectChildren = getObjectChildrenFactory({ db }) describe('Objects @core-objects', () => { const userOne = {