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