Refactor schema type handling and enhance utility functions in utils and database modules

This commit introduces several utility functions to improve schema type handling, including `getEmbeddedSchemaType` and `getObjectIdSchemaTypeFromPath`. The `getFilterFieldKind` function is updated to utilize these new utilities for better clarity and functionality. Additionally, the `getArrayParentPaths` function is renamed to `getArrayUnwindPaths` for improved readability, and the logic for fetching unique IDs is enhanced with a new `flattenRefIds` function. These changes streamline the code and enhance the overall management of schema references and array paths.
This commit is contained in:
Tom Butcher 2026-08-22 20:02:21 +01:00
parent b0943f3fa0
commit 0470f62e05
2 changed files with 87 additions and 22 deletions

View File

@ -657,21 +657,26 @@ export const listPropertyValues = async ({ model, property, filter = {}, search
return await model.aggregate(aggregateCommand); return await model.aggregate(aggregateCommand);
}; };
function getEmbeddedSchemaType(path) {
if (!path) return null;
return path.embeddedSchemaType ?? path.$embeddedSchemaType ?? path.caster ?? null;
}
function getObjectRefPathInfo(model, property) { function getObjectRefPathInfo(model, property) {
if (!model?.schema || !property) return null; if (!model?.schema || !property) return null;
const path = model.schema.path(property); const path = model.schema.path(property);
if (!path) return null; if (!path) return null;
const schemaType = path.instance === 'Array' ? path.caster : path; const schemaType = path.instance === 'Array' ? getEmbeddedSchemaType(path) : path;
if (!schemaType) return null; if (!schemaType) return null;
const instance = schemaType.instance; const instance = schemaType.instance;
if (instance !== 'ObjectID' && instance !== 'ObjectId') return null; if (instance !== 'ObjectID' && instance !== 'ObjectId') return null;
return { return {
ref: schemaType.options?.ref, ref: schemaType.options?.ref ?? path.options?.ref,
refPath: schemaType.options?.refPath, 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; return matches.find((name) => name.endsWith('Type') || name === 'type') || matches[0] || null;
} }
function getArrayParentPaths(model, property) { function getArrayUnwindPaths(model, property) {
if (!model?.schema || !property?.includes('.')) return []; if (!model?.schema || !property) return [];
const parents = []; const paths = [];
const parts = property.split('.'); const parts = property.split('.');
for (let i = 1; i < parts.length; i++) { for (let i = 1; i < parts.length; i++) {
const parentPath = parts.slice(0, i).join('.'); const parentPath = parts.slice(0, i).join('.');
const parentSchemaPath = model.schema.path(parentPath); const parentSchemaPath = model.schema.path(parentPath);
if (parentSchemaPath?.instance === 'Array') { 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) { async function fetchBasicObjectsByIds(refName, ids) {
const uniqueIds = [ const uniqueIds = [
...new Map( ...new Map(flattenRefIds(ids).map((id) => [id.toString(), id])).values(),
(ids || [])
.filter((id) => id != null)
.map((id) => [id.toString(), id])
).values(),
]; ];
if (uniqueIds.length === 0) return []; if (uniqueIds.length === 0) return [];
@ -762,10 +791,10 @@ export const getPropertyValues = async ({ model, property, filter = {} }) => {
pipeline.push({ $match: convertedFilter }); pipeline.push({ $match: convertedFilter });
} }
for (const parentPath of getArrayParentPaths(model, property)) { for (const unwindPath of getArrayUnwindPaths(model, property)) {
pipeline.push({ pipeline.push({
$unwind: { $unwind: {
path: `$${parentPath}`, path: `$${unwindPath}`,
preserveNullAndEmptyArrays: false, preserveNullAndEmptyArrays: false,
}, },
}); });
@ -803,6 +832,28 @@ export const getPropertyValues = async ({ model, property, filter = {} }) => {
} }
if (hasFixedRef) { 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); const ids = await model.distinct(property, convertedFilter);
return fetchBasicObjectsByIds(pathInfo.ref, ids); return fetchBasicObjectsByIds(pathInfo.ref, ids);
} }

View File

@ -93,8 +93,19 @@ function getBaseProperty(property) {
return property; return property;
} }
function isObjectIdSchemaType(path) { function getEmbeddedSchemaType(path) {
return path?.instance === 'ObjectId' || path?.instance === 'ObjectID'; 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) { function getSchemaPathFromModels(property, model = null) {
@ -112,12 +123,14 @@ function getFilterFieldKind(property, model = null) {
const inspectPath = (path) => { const inspectPath = (path) => {
if (!path) return null; if (!path) return null;
if (isObjectIdPath(property) || isObjectIdSchemaType(path)) { const objectIdType = getObjectIdSchemaTypeFromPath(path);
if (property.endsWith('._id') && property !== '_id' && path.options?.ref) { if (isObjectIdPath(property) || objectIdType) {
const ref = objectIdType?.options?.ref ?? path.options?.ref;
if (property.endsWith('._id') && property !== '_id' && ref) {
return { kind: 'objectId', property: baseProperty }; return { kind: 'objectId', property: baseProperty };
} }
if (path.options?.ref) { if (ref) {
return { kind: 'objectRef', property: baseProperty, ref: path.options.ref }; return { kind: 'objectRef', property: baseProperty, ref };
} }
return { kind: 'objectId', property: baseProperty }; return { kind: 'objectId', property: baseProperty };
} }
@ -162,7 +175,8 @@ function buildRegexOp(pattern, useOptions = true) {
function getSchemaRefName(property, model = null) { function getSchemaRefName(property, model = null) {
const baseProperty = getBaseProperty(property); const baseProperty = getBaseProperty(property);
const path = model?.schema?.path(baseProperty) ?? getSchemaPathFromModels(baseProperty); 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) { function getRefModelEntryFromSchemaRef(refName) {