adonis-odm
Version:
A comprehensive MongoDB ODM for AdonisJS with Lucid-style API, type-safe relationships, embedded documents, and transaction support
216 lines (215 loc) • 8.73 kB
JavaScript
import { DatabaseOperationException, RelationshipException } from '../exceptions/index.js';
/**
* Utility functions for handling nested documents efficiently
*/
export class NestedDocumentHelpers {
/**
* Bulk load referenced documents for multiple models
* This helps avoid N+1 query problems
*/
static async bulkLoadReferences(models, referenceField, ReferenceModel, targetField) {
try {
// Extract all reference IDs
const referenceIds = models
.map((model) => model[referenceField])
.filter((id) => id !== null && id !== undefined);
if (referenceIds.length === 0)
return;
// Load all referenced documents in a single query
let referencedDocs;
try {
referencedDocs = await ReferenceModel
.query()
.where('_id', 'in', referenceIds)
.all();
}
catch (error) {
throw new DatabaseOperationException(`bulk load references for ${ReferenceModel.name}`, error);
}
// Create a map for quick lookup
const referencedMap = new Map();
referencedDocs.forEach((doc) => {
referencedMap.set(doc._id.toString(), doc);
});
// Assign referenced documents to models
const field = targetField || referenceField.replace(/Id$/, '');
models.forEach((model) => {
const refId = model[referenceField];
if (refId) {
const referencedDoc = referencedMap.get(refId.toString());
if (referencedDoc) {
;
model[field] = referencedDoc;
}
}
});
}
catch (error) {
// Re-throw DatabaseOperationException as-is, wrap others
if (error instanceof DatabaseOperationException) {
throw error;
}
throw new RelationshipException(`bulk load ${referenceField}`, models[0]?.constructor.name || 'Unknown', error);
}
}
/**
* Paginate with selective reference loading
* Only load references for the current page, not all data
*/
static async paginateWithReferences(ModelClass, page, perPage, referenceField, ReferenceModel, targetField, conditions) {
// Build query with conditions
let query = ModelClass.query();
if (conditions) {
conditions.forEach((condition) => {
if (condition.operator && condition.value !== undefined) {
query = query.where(condition.field, condition.operator, condition.value);
}
});
}
// Get paginated results
const paginatedResult = await query.paginate(page, perPage);
// Bulk load references for the current page only
await this.bulkLoadReferences(paginatedResult.data, referenceField, ReferenceModel, targetField);
return paginatedResult;
}
/**
* Create document with nested data
* Handles both embedded and referenced approaches
*/
static async createWithNested(ModelClass, data, nestedConfig) {
if (!nestedConfig) {
return await ModelClass.create(data);
}
const { field, isEmbedded, NestedModel, referenceField } = nestedConfig;
const nestedData = data[field];
delete data[field];
if (isEmbedded) {
// For embedded documents, include nested data directly
data[field] = nestedData;
return await ModelClass.create(data);
}
else {
// For referenced documents, create nested document first
if (!NestedModel || !nestedData) {
return await ModelClass.create(data);
}
const nestedDoc = await NestedModel.create(nestedData);
data[referenceField || field + 'Id'] = nestedDoc._id;
const mainDoc = await ModelClass.create(data);
mainDoc[field] = nestedDoc;
return mainDoc;
}
}
/**
* Update document with nested data
*/
static async updateWithNested(model, data, nestedConfig) {
if (!nestedConfig) {
model.merge(data);
await model.save();
return model;
}
const { field, isEmbedded, NestedModel, referenceField } = nestedConfig;
const nestedData = data[field];
delete data[field];
// Update main document data
if (Object.keys(data).length > 0) {
model.merge(data);
}
if (nestedData) {
if (isEmbedded) {
// For embedded documents, update nested data directly
const currentNested = model[field] || {};
model[field] = { ...currentNested, ...nestedData };
}
else {
// For referenced documents, update the referenced document
const refId = model[referenceField || field + 'Id'];
if (refId && NestedModel) {
const nestedDoc = await NestedModel.find(refId);
if (nestedDoc) {
nestedDoc.merge(nestedData);
await nestedDoc.save();
model[field] = nestedDoc;
}
}
}
}
await model.save();
return model;
}
/**
* Query with nested conditions
* Handles different approaches for embedded vs referenced documents
*/
static async queryWithNestedConditions(ModelClass, nestedConditions, mainConditions, options) {
const embeddedConditions = nestedConditions.filter((c) => c.isEmbedded);
const referencedConditions = nestedConditions.filter((c) => !c.isEmbedded);
let query = ModelClass.query();
// Apply main conditions
if (mainConditions) {
mainConditions.forEach((condition) => {
query = query.where(condition.field, condition.operator, condition.value);
});
}
// Apply embedded conditions directly
embeddedConditions.forEach((condition) => {
query = query.where(condition.field, condition.operator, condition.value);
});
// Handle referenced conditions
if (referencedConditions.length > 0) {
const referenceIds = [];
for (const condition of referencedConditions) {
if (condition.NestedModel) {
const nestedDocs = await condition.NestedModel
.query()
.where(condition.field, condition.operator, condition.value)
.all();
const ids = nestedDocs.map((doc) => doc._id);
referenceIds.push(...ids);
}
}
if (referenceIds.length > 0) {
const referenceField = referencedConditions[0].referenceField || 'profileId';
query = query.where(referenceField, 'in', referenceIds);
}
}
// Apply ordering
if (options?.orderBy) {
query = query.orderBy(options.orderBy.field, options.orderBy.direction);
}
// Apply pagination
if (options?.page && options?.perPage) {
return await query.paginate(options.page, options.perPage);
}
return await query.all();
}
/**
* Aggregate nested document statistics
*/
static async aggregateNestedStats(ModelClass, nestedField, isEmbedded, NestedModel) {
if (isEmbedded) {
// For embedded documents, use MongoDB aggregation
// This would require access to the raw MongoDB collection
// Implementation depends on your database manager setup
return {
totalWithNested: 0,
totalWithoutNested: 0,
// Add more statistics as needed
};
}
else {
// For referenced documents, use separate queries
const totalUsers = await ModelClass.query().count();
const usersWithNested = await ModelClass
.query()
.where(nestedField + 'Id', 'exists', true)
.count();
return {
totalWithNested: usersWithNested,
totalWithoutNested: totalUsers - usersWithNested,
totalNested: NestedModel ? await NestedModel.query().count() : 0,
};
}
}
}