Skip to main content

Nested Arrays

MongoNestedService manages the elements of an array field embedded inside a parent document. It is the right choice when your data looks like this:

{
"_id": "customer-1",
"name": "Alice",
"addresses": [
{ "_id": "addr-1", "street": "123 Main St", "city": "Springfield", "type": "billing" },
{ "_id": "addr-2", "street": "456 Oak Ave", "city": "Shelbyville", "type": "shipping" }
]
}

Setting up

Pass the parent data type and the array field name to the constructor:

import { MongoNestedService } from '@opra/mongodb';
import { Customer } from '../models/customer.js';
import { Address } from '../models/address.js';

export class CustomerAddressesService extends MongoNestedService<Address> {
constructor() {
super(Customer, 'addresses');
}
}

By default, elements are identified by their _id field. If your array elements use a different key field, set nestedKey:

super(Customer, 'addresses', { nestedKey: 'addressId' });

CRUD operations

Every method takes documentId (parent _id) as its first argument. Methods that target a specific element also take nestedId.

const svc = this.addresses.for(ctx);

// Add an element
const address = await svc.create(customerId, {
street: '123 Main St',
city: 'Springfield',
type: 'billing',
});

// Fetch one by id
const address = await svc.get(customerId, addressId);
const maybeAddress = await svc.findById(customerId, addressId); // returns undefined if not found

// Fetch all (filtered, sorted, paginated)
const addresses = await svc.findMany(customerId, {
filter: { type: 'shipping' },
sort: ['-createdAt'],
limit: 10,
});

// Paginated with total count
const { items, count } = await svc.findManyWithCount(customerId, { limit, skip });

// Update one element
const updated = await svc.update(customerId, addressId, { city: 'Shelbyville' });
await svc.updateOnly(customerId, addressId, { city: 'Shelbyville' }); // no fetch-back

// Update all matching elements
const affected = await svc.updateMany(customerId, { verified: false }, {
filter: { type: 'shipping' },
});

// Delete
await svc.delete(customerId, addressId);
await svc.deleteMany(customerId, { filter: { type: 'billing' } });

// Existence check
const exists = await svc.exists(customerId, addressId);
await svc.assert(customerId, addressId); // throws if not found

nestedFilter — scoped service instances

nestedFilter works like documentFilter but filters on the array elements instead of the parent document. Use it to scope a service to a specific subset of elements.

A common pattern is to create subclasses (or factory methods) for each element type:

export class CustomerAddressesService extends MongoNestedService<Address> {
constructor() {
super(Customer, 'addresses');
}

billingOnly() {
return this.for(undefined, { nestedFilter: { type: 'billing' } });
}

shippingOnly() {
return this.for(undefined, { nestedFilter: { type: 'shipping' } });
}
}

// Usage
const billingAddr = await svc.billingOnly().for(ctx).findOne(customerId);

nestedFilter can also be a function computed per request:

nestedFilter: (command, _this) => ({ tenantId: _this.context.tenantId })

documentFilter — filtering the parent

Operations can also apply an additional filter to the parent document via the documentFilter option. This ensures the operation only affects nested elements inside parent documents that match the constraint:

// Only modify addresses inside active customers
await svc.update(customerId, addressId, { city: 'Springfield' }, {
documentFilter: { status: 'active' },
});

Lifecycle hooks

Override _beforeCreate, _afterDelete, etc. the same way as with MongoCollectionService. See Lifecycle Hooks.


Full API reference

MongoNestedService