diff --git a/src/database/database.js b/src/database/database.js index 12cf402..747c4ca 100644 --- a/src/database/database.js +++ b/src/database/database.js @@ -657,21 +657,26 @@ export const listPropertyValues = async ({ model, property, filter = {}, search return await model.aggregate(aggregateCommand); }; +function getEmbeddedSchemaType(path) { + if (!path) return null; + return path.embeddedSchemaType ?? path.$embeddedSchemaType ?? path.caster ?? null; +} + function getObjectRefPathInfo(model, property) { if (!model?.schema || !property) return null; const path = model.schema.path(property); if (!path) return null; - const schemaType = path.instance === 'Array' ? path.caster : path; + const schemaType = path.instance === 'Array' ? getEmbeddedSchemaType(path) : path; if (!schemaType) return null; const instance = schemaType.instance; if (instance !== 'ObjectID' && instance !== 'ObjectId') return null; return { - ref: schemaType.options?.ref, - refPath: schemaType.options?.refPath, + ref: schemaType.options?.ref ?? path.options?.ref, + refPath: schemaType.options?.refPath ?? path.options?.refPath, }; } @@ -690,28 +695,52 @@ function inferTypeFieldFromRefFunction(refFn) { return matches.find((name) => name.endsWith('Type') || name === 'type') || matches[0] || null; } -function getArrayParentPaths(model, property) { - if (!model?.schema || !property?.includes('.')) return []; +function getArrayUnwindPaths(model, property) { + if (!model?.schema || !property) return []; - const parents = []; + const paths = []; const parts = property.split('.'); for (let i = 1; i < parts.length; i++) { const parentPath = parts.slice(0, i).join('.'); const parentSchemaPath = model.schema.path(parentPath); if (parentSchemaPath?.instance === 'Array') { - parents.push(parentPath); + paths.push(parentPath); } } - return parents; + + const path = model.schema.path(property); + if (path?.instance === 'Array') { + paths.push(property); + } + return paths; +} + +function flattenRefIds(ids) { + const flattened = []; + const visit = (value) => { + if (value == null) return; + if (Array.isArray(value)) { + value.forEach(visit); + return; + } + if ( + typeof value === 'object' && + !(value instanceof mongoose.Types.ObjectId) && + !(value instanceof Date) && + value._id != null + ) { + visit(value._id); + return; + } + flattened.push(value); + }; + visit(ids); + return flattened; } async function fetchBasicObjectsByIds(refName, ids) { const uniqueIds = [ - ...new Map( - (ids || []) - .filter((id) => id != null) - .map((id) => [id.toString(), id]) - ).values(), + ...new Map(flattenRefIds(ids).map((id) => [id.toString(), id])).values(), ]; if (uniqueIds.length === 0) return []; @@ -762,10 +791,10 @@ export const getPropertyValues = async ({ model, property, filter = {} }) => { pipeline.push({ $match: convertedFilter }); } - for (const parentPath of getArrayParentPaths(model, property)) { + for (const unwindPath of getArrayUnwindPaths(model, property)) { pipeline.push({ $unwind: { - path: `$${parentPath}`, + path: `$${unwindPath}`, preserveNullAndEmptyArrays: false, }, }); @@ -803,6 +832,28 @@ export const getPropertyValues = async ({ model, property, filter = {} }) => { } if (hasFixedRef) { + const unwindPaths = getArrayUnwindPaths(model, property); + if (unwindPaths.length > 0) { + const pipeline = []; + if (Object.keys(convertedFilter).length > 0) { + pipeline.push({ $match: convertedFilter }); + } + for (const unwindPath of unwindPaths) { + pipeline.push({ + $unwind: { + path: `$${unwindPath}`, + preserveNullAndEmptyArrays: false, + }, + }); + } + pipeline.push( + { $match: { [property]: { $ne: null } } }, + { $group: { _id: `$${property}` } } + ); + const ids = (await model.aggregate(pipeline)).map((row) => row._id); + return fetchBasicObjectsByIds(pathInfo.ref, ids); + } + const ids = await model.distinct(property, convertedFilter); return fetchBasicObjectsByIds(pathInfo.ref, ids); } diff --git a/src/utils.js b/src/utils.js index f835e0a..b20ff60 100644 --- a/src/utils.js +++ b/src/utils.js @@ -93,8 +93,19 @@ function getBaseProperty(property) { return property; } -function isObjectIdSchemaType(path) { - return path?.instance === 'ObjectId' || path?.instance === 'ObjectID'; +function getEmbeddedSchemaType(path) { + if (!path) return null; + return path.embeddedSchemaType ?? path.$embeddedSchemaType ?? path.caster ?? null; +} + +function getObjectIdSchemaTypeFromPath(path) { + if (!path) return null; + const schemaType = path.instance === 'Array' ? getEmbeddedSchemaType(path) : path; + if (!schemaType) return null; + if (schemaType.instance === 'ObjectId' || schemaType.instance === 'ObjectID') { + return schemaType; + } + return null; } function getSchemaPathFromModels(property, model = null) { @@ -112,12 +123,14 @@ function getFilterFieldKind(property, model = null) { const inspectPath = (path) => { if (!path) return null; - if (isObjectIdPath(property) || isObjectIdSchemaType(path)) { - if (property.endsWith('._id') && property !== '_id' && path.options?.ref) { + const objectIdType = getObjectIdSchemaTypeFromPath(path); + if (isObjectIdPath(property) || objectIdType) { + const ref = objectIdType?.options?.ref ?? path.options?.ref; + if (property.endsWith('._id') && property !== '_id' && ref) { return { kind: 'objectId', property: baseProperty }; } - if (path.options?.ref) { - return { kind: 'objectRef', property: baseProperty, ref: path.options.ref }; + if (ref) { + return { kind: 'objectRef', property: baseProperty, ref }; } return { kind: 'objectId', property: baseProperty }; } @@ -162,7 +175,8 @@ function buildRegexOp(pattern, useOptions = true) { function getSchemaRefName(property, model = null) { const baseProperty = getBaseProperty(property); const path = model?.schema?.path(baseProperty) ?? getSchemaPathFromModels(baseProperty); - return path?.options?.ref ?? path?.caster?.options?.ref ?? null; + const embeddedType = getEmbeddedSchemaType(path); + return path?.options?.ref ?? embeddedType?.options?.ref ?? null; } function getRefModelEntryFromSchemaRef(refName) {