Skip to content

Commit

Permalink
fix(mongodb): MongoDB Aggregation improvements (#3366)
Browse files Browse the repository at this point in the history
DaddyWarbucks authored May 29, 2024
1 parent 188278e commit f2829b1
Showing 3 changed files with 634 additions and 121 deletions.
19 changes: 9 additions & 10 deletions docs/api/databases/mongodb.md
Original file line number Diff line number Diff line change
@@ -66,8 +66,7 @@ MongoDB adapter specific options are:

The [common API options](./common.md#options) are:

- `id {string}` (_optional_, default: `'_id'`) - The name of the id field property. By design, MongoDB will always add an `_id` property.
- `id {string}` (_optional_) - The name of the id field property (usually set by default to `id` or `_id`).
- `id {string}` (_optional_, default: `'_id'`) - The name of the id field property. By design, MongoDB will always add an `_id` property. But you can choose to use a different property as your primary key.
- `paginate {Object}` (_optional_) - A [pagination object](#pagination) containing a `default` and `max` page size
- `multi {string[]|boolean}` (_optional_, default: `false`) - Allow `create` with arrays and `patch` and `remove` with id `null` to change multiple items. Can be `true` for all methods or an array of allowed methods (e.g. `[ 'remove', 'create' ]`)

@@ -79,31 +78,31 @@ There are additionally several legacy options in the [common API options](./comm

### aggregateRaw(params)

The `find` method has been split into separate utilities for converting params into different types of MongoDB requests. By default, requests are processed by this method and are run through the MongoDB Aggregation Pipeline. This method returns a raw MongoDB Cursor object, which can be used to perform custom pagination or in custom server scripts, if desired.
The `find` method has been split into separate utilities for converting params into different types of MongoDB requests. When using `params.pipeline`, the `aggregateRaw` method is used to convert the Feathers params into a MongoDB aggregation pipeline with the `model.aggregate` method. This method returns a raw MongoDB Cursor object, which can be used to perform custom pagination or in custom server scripts, if desired.

### findRaw(params)

`findRaw(params)` is used when `params.mongodb` is set to retrieve data using `params.mongodb` as the `FindOptions` object. This method returns a raw MongoDB Cursor object, which can be used to perform custom pagination or in custom server scripts, if desired.
`findRaw(params)` This method is used when there is no `params.pipeline` and uses the common `model.find` method. It returns a raw MongoDB Cursor object, which can be used to perform custom pagination or in custom server scripts, if desired.

### makeFeathersPipeline(params)

`makeFeathersPipeline(params)` takes a set of Feathers params and converts them to a pipeline array, ready to pass to `collection.aggregate`. This utility comprises the bulk of the `aggregateRaw` functionality, but does not use `params.pipeline`.
`makeFeathersPipeline(params)` takes a set of Feathers params and converts them to a pipeline array, ready to pass to `model.aggregate`. This utility comprises the bulk of the `aggregateRaw` functionality, but does not use `params.pipeline`.

### Custom Params

The `@feathersjs/mongodb` adapter utilizes two custom params which control adapter-specific features: `params.pipeline` and `params.mongodb`.
The `@feathersjs/mongodb` adapter utilizes three custom params which control adapter-specific features: `params.pipeline`, `params.mongodb`, and `params.adapter`.

#### params.adapter

Allows to dynamically set the [adapter options](#options) (like the `Model` collection) for a service method call.

#### params.pipeline

Used for [aggregation pipelines](#aggregation-pipeline).
Used for [aggregation pipelines](#aggregation-pipeline). Whenever this property is set, the adapter will use the `model.aggregate` method instead of the `model.find` method. The `pipeline` property should be an array of [aggregation stages](https://www.mongodb.com/docs/manual/reference/operator/aggregation-pipeline/).

#### params.mongodb

When making a [service method](/api/services.md) call, `params` can contain an`mongodb` property (for example, `{upsert: true}`) which allows modifying the options used to run the MongoDB query. The adapter will use the `collection.find` method and not the [aggregation pipeline](#aggregation-pipeline) when you use `params.mongodb`.
When making a [service method](/api/services.md) call, `params` can contain an`mongodb` property (for example, `{ upsert: true }`) which allows modifying the options used to run the MongoDB query. This param can be used for both find and aggregation queries.

## Transactions

@@ -198,9 +197,9 @@ See the MongoDB documentation for instructions on performing full-text search us

## Aggregation Pipeline

In Feathers v5 Dove, we added support for the full power of MongoDB's Aggregation Framework and blends it seamlessly with the familiar Feathers Query syntax. All `find` queries now use the Aggregation Framework, by default.
In Feathers v5 Dove, we added support for the full power of MongoDB's Aggregation Framework and blends it seamlessly with the familiar Feathers Query syntax. The `find` method automatically uses the aggregation pipeline when `params.pipeline` is set.

The Aggregation Framework is accessed through the mongoClient's `collection.aggregate` method, which accepts an array of "stages". Each stage contains an operator which describes an operation to apply to the previous step's data. Each stage applies the operation to the results of the previous step. It’s now possible to perform any of the [Aggregation Stages](https://www.mongodb.com/docs/upcoming/reference/operator/aggregation-pipeline/) like `$lookup` and `$unwind`, integration with the normal Feathers queries.
The Aggregation Framework is accessed through the mongoClient's `model.aggregate` method, which accepts an array of "stages". Each stage contains an operator which describes an operation to apply to the previous step's data. Each stage applies the operation to the results of the previous step. It’s now possible to perform any of the [Aggregation Stages](https://www.mongodb.com/docs/upcoming/reference/operator/aggregation-pipeline/) like `$lookup` and `$unwind`, integration with the normal Feathers queries.

Here's how it works with the operators that match the Feathers Query syntax. Let's convert the following Feathers query:

392 changes: 281 additions & 111 deletions packages/mongodb/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -7,13 +7,15 @@ import {
DeleteOptions,
CountDocumentsOptions,
ReplaceOptions,
Document
FindOneAndReplaceOptions,
FindOneAndUpdateOptions,
Document,
FindOneAndDeleteOptions
} from 'mongodb'
import { BadRequest, MethodNotAllowed, NotFound } from '@feathersjs/errors'
import { _ } from '@feathersjs/commons'
import {
AdapterBase,
select,
AdapterParams,
AdapterServiceOptions,
PaginationOptions,
@@ -39,6 +41,8 @@ export interface MongoDBAdapterParams<Q = AdapterQuery>
| DeleteOptions
| CountDocumentsOptions
| ReplaceOptions
| FindOneAndReplaceOptions
| FindOneAndDeleteOptions
}

export type AdapterId = Id | ObjectId
@@ -103,16 +107,16 @@ export class MongoDbAdapter<
async findRaw(params: ServiceParams) {
const { filters, query } = this.filterQuery(null, params)
const model = await this.getModel(params)
const q = model.find(query, { ...params.mongodb })

if (filters.$select !== undefined) {
q.project(this.getSelect(filters.$select))
}
const q = model.find(query, params.mongodb)

if (filters.$sort !== undefined) {
q.sort(filters.$sort)
}

if (filters.$select !== undefined) {
q.project(this.getProjection(filters.$select))
}

if (filters.$skip !== undefined) {
q.skip(filters.$skip)
}
@@ -124,6 +128,7 @@ export class MongoDbAdapter<
return q
}

/* TODO: Remove $out and $merge stages, else it returns an empty cursor. I think its safe to assume this is primarily for querying. */
async aggregateRaw(params: ServiceParams) {
const model = await this.getModel(params)
const pipeline = params.pipeline || []
@@ -132,32 +137,36 @@ export class MongoDbAdapter<
const feathersPipeline = this.makeFeathersPipeline(params)
const after = index >= 0 ? pipeline.slice(index + 1) : pipeline

return model.aggregate([...before, ...feathersPipeline, ...after])
return model.aggregate([...before, ...feathersPipeline, ...after], params.mongodb)
}

makeFeathersPipeline(params: ServiceParams) {
const { filters, query } = this.filterQuery(null, params)
const pipeline: Document[] = [{ $match: query }]

if (filters.$select !== undefined) {
pipeline.push({ $project: this.getSelect(filters.$select) })
}

if (filters.$sort !== undefined) {
pipeline.push({ $sort: filters.$sort })
}

if (filters.$select !== undefined) {
pipeline.push({ $project: this.getProjection(filters.$select) })
}

if (filters.$skip !== undefined) {
pipeline.push({ $skip: filters.$skip })
}

if (filters.$limit !== undefined) {
pipeline.push({ $limit: filters.$limit })
}

return pipeline
}

getSelect(select: string[] | { [key: string]: number }) {
getProjection(select?: string[] | { [key: string]: number }) {
if (!select) {
return undefined
}
if (Array.isArray(select)) {
if (!select.includes(this.id)) {
select = [this.id, ...select]
@@ -181,10 +190,6 @@ export class MongoDbAdapter<
return select
}

async _findOrGet(id: NullableAdapterId, params: ServiceParams) {
return id === null ? await this._find(params) : await this._get(id, params)
}

normalizeId<D>(id: NullableAdapterId, data: D): D {
if (this.id === '_id') {
// Default Mongo IDs cannot be updated. The Mongo library handles
@@ -201,32 +206,77 @@ export class MongoDbAdapter<
return data
}

async countDocuments(params: ServiceParams) {
const { useEstimatedDocumentCount } = this.getOptions(params)
const { query } = this.filterQuery(null, params)

if (params.pipeline) {
const aggregateParams = {
...params,
query: {
...params.query,
$select: [this.id],
$sort: undefined,
$skip: undefined,
$limit: undefined
}
}
const result = await this.aggregateRaw(aggregateParams).then((result) => result.toArray())
return result.length
}

const model = await this.getModel(params)

if (useEstimatedDocumentCount && typeof model.estimatedDocumentCount === 'function') {
return model.estimatedDocumentCount()
}

return model.countDocuments(query, params.mongodb)
}

async _get(id: AdapterId, params: ServiceParams = {} as ServiceParams): Promise<Result> {
const {
query,
filters: { $select }
} = this.filterQuery(id, params)
const projection = $select
? {
projection: {
...this.getSelect($select),
[this.id]: 1
}

if (params.pipeline) {
const aggregateParams = {
...params,
query: {
...params.query,
$limit: 1,
$and: (params.query.$and || []).concat({
[this.id]: this.getObjectId(id)
})
}
: {}
}

return this.aggregateRaw(aggregateParams)
.then((result) => result.toArray())
.then(([result]) => {
if (!result) {
throw new NotFound(`No record found for id '${id}'`)
}

return result
})
.catch(errorHandler)
}

const findOptions: FindOptions = {
...params.mongodb,
...projection
projection: this.getProjection($select),
...params.mongodb
}

return this.getModel(params)
.then((model) => model.findOne(query, findOptions))
.then((data) => {
if (data == null) {
.then((result) => {
if (!result) {
throw new NotFound(`No record found for id '${id}'`)
}

return data
return result
})
.catch(errorHandler)
}
@@ -235,34 +285,40 @@ export class MongoDbAdapter<
async _find(params?: ServiceParams & { paginate: false }): Promise<Result[]>
async _find(params?: ServiceParams): Promise<Paginated<Result> | Result[]>
async _find(params: ServiceParams = {} as ServiceParams): Promise<Paginated<Result> | Result[]> {
const { paginate, useEstimatedDocumentCount } = this.getOptions(params)
const { filters, query } = this.filterQuery(null, params)
const useAggregation = !params.mongodb && filters.$limit !== 0
const countDocuments = async () => {
if (paginate && paginate.default) {
const model = await this.getModel(params)
if (useEstimatedDocumentCount && typeof model.estimatedDocumentCount === 'function') {
return model.estimatedDocumentCount()
} else {
return model.countDocuments(query, { ...params.mongodb })
}
const { paginate } = this.getOptions(params)
const { filters } = this.filterQuery(null, params)
const paginationDisabled = params.paginate === false || !paginate || !paginate.default

const getData = () => {
const result = params.pipeline ? this.aggregateRaw(params) : this.findRaw(params)
return result.then((result) => result.toArray())
}

if (paginationDisabled) {
if (filters.$limit === 0) {
return [] as Result[]
}
const data = await getData()
return data as Result[]
}

if (filters.$limit === 0) {
return {
total: await this.countDocuments(params),
data: [] as Result[],
limit: filters.$limit,
skip: filters.$skip || 0
}
return Promise.resolve(0)
}

const [request, total] = await Promise.all([
useAggregation ? this.aggregateRaw(params) : this.findRaw(params),
countDocuments()
])
const [data, total] = await Promise.all([getData(), this.countDocuments(params)])

const page = {
return {
total,
data: data as Result[],
limit: filters.$limit,
skip: filters.$skip || 0,
data: filters.$limit === 0 ? [] : ((await request.toArray()) as any as Result[])
skip: filters.$skip || 0
}

return paginate && paginate.default ? page : page.data
}

async _create(data: Data, params?: ServiceParams): Promise<Result>
@@ -272,12 +328,14 @@ export class MongoDbAdapter<
data: Data | Data[],
params: ServiceParams = {} as ServiceParams
): Promise<Result | Result[]> {
const writeOptions = params.mongodb
if (Array.isArray(data) && !this.allowsMulti('create', params)) {
throw new MethodNotAllowed('Can not create multiple entries')
}

const model = await this.getModel(params)
const setId = (item: any) => {
const entry = Object.assign({}, item)

// Generate a MongoId if we use a custom id
if (this.id !== '_id' && typeof entry[this.id] === 'undefined') {
return {
[this.id]: new ObjectId().toHexString(),
@@ -288,17 +346,29 @@ export class MongoDbAdapter<
return entry
}

const promise = Array.isArray(data)
? model
.insertMany(data.map(setId), writeOptions)
.then(async (result) =>
model.find({ _id: { $in: Object.values(result.insertedIds) } }, params.mongodb).toArray()
)
: model
.insertOne(setId(data), writeOptions)
.then(async (result) => model.findOne({ _id: result.insertedId }, params.mongodb))
if (Array.isArray(data)) {
const created = await model.insertMany(data.map(setId), params.mongodb).catch(errorHandler)
return this._find({
...params,
paginate: false,
query: {
_id: { $in: Object.values(created.insertedIds) },
$select: params.query?.$select
}
})
}

return promise.then(select(params, this.id)).catch(errorHandler)
const created = await model.insertOne(setId(data), params.mongodb).catch(errorHandler)
const result = await this._find({
...params,
paginate: false,
query: {
_id: created.insertedId,
$select: params.query?.$select,
$limit: 1
}
})
return result[0]
}

async _patch(id: null, data: PatchData | Partial<Result>, params?: ServiceParams): Promise<Result[]>
@@ -321,59 +391,140 @@ export class MongoDbAdapter<
const model = await this.getModel(params)
const {
query,
filters: { $select }
filters: { $sort, $select }
} = this.filterQuery(id, params)
const updateOptions = { ...params.mongodb }
const modifier = Object.keys(data).reduce((current, key) => {
const value = (data as any)[key]

if (key.charAt(0) !== '$') {
current.$set = {
...current.$set,
[key]: value

const replacement = Object.keys(data).reduce(
(current, key) => {
const value = (data as any)[key]

if (key.charAt(0) !== '$') {
current.$set[key] = value
} else if (key === '$set') {
current.$set = {
...current.$set,
...value
}
} else {
current[key] = value
}
} else {
current[key] = value
}

return current
}, {} as any)
const originalIds = await this._findOrGet(id, {
...params,
query: {
...query,
$select: [this.id]
return current
},
paginate: false
})
const items = Array.isArray(originalIds) ? originalIds : [originalIds]
const idList = items.map((item: any) => item[this.id])
const findParams = {
...params,
paginate: false,
query: {
[this.id]: { $in: idList },
$select
{ $set: {} } as any
)

if (id === null) {
const findParams = {
...params,
paginate: false,
query: {
...params.query,
$select: [this.id]
}
}

return this._find(findParams)
.then(async (result) => {
const idList = (result as Result[]).map((item: any) => item[this.id])
await model.updateMany({ [this.id]: { $in: idList } }, replacement, params.mongodb)
return this._find({
...params,
paginate: false,
query: {
[this.id]: { $in: idList },
$sort,
$select
}
})
})
.catch(errorHandler)
}

await model.updateMany(query, modifier, updateOptions)
if (params.pipeline) {
const getParams = {
...params,
query: {
...params.query,
$select: [this.id]
}
}

return this._get(id, getParams)
.then(async () => {
await model.updateOne({ [this.id]: id }, replacement, params.mongodb)
return this._get(id, {
...params,
query: { $select }
})
})
.catch(errorHandler)
}

const updateOptions: FindOneAndUpdateOptions = {
projection: this.getProjection($select),
...(params.mongodb as FindOneAndUpdateOptions),
returnDocument: 'after'
}

return this._findOrGet(id, findParams).catch(errorHandler)
return model
.findOneAndUpdate(query, replacement, updateOptions)
.then((result) => {
if (!result) {
throw new NotFound(`No record found for id '${id}'`)
}
return result as Result
})
.catch(errorHandler)
}

async _update(id: AdapterId, data: Data, params: ServiceParams = {} as ServiceParams): Promise<Result> {
if (id === null || Array.isArray(data)) {
throw new BadRequest("You can not replace multiple instances. Did you mean 'patch'?")
}

const {
query,
filters: { $select }
} = this.filterQuery(id, params)
const model = await this.getModel(params)
const { query } = this.filterQuery(id, params)
const replaceOptions = { ...params.mongodb }
const replacement = this.normalizeId(id, data)

if (params.pipeline) {
const getParams = {
...params,
query: {
...params.query,
$select: [this.id]
}
}

await model.replaceOne(query, this.normalizeId(id, data), replaceOptions)
return this._get(id, getParams)
.then(async () => {
await model.replaceOne({ [this.id]: id }, replacement, params.mongodb)
return this._get(id, {
...params,
query: { $select }
})
})
.catch(errorHandler)
}

const replaceOptions: FindOneAndReplaceOptions = {
projection: this.getProjection($select),
...(params.mongodb as FindOneAndReplaceOptions),
returnDocument: 'after'
}

return this._findOrGet(id, params).catch(errorHandler)
return model
.findOneAndReplace(query, replacement, replaceOptions)
.then((result) => {
if (!result) {
throw new NotFound(`No record found for id '${id}'`)
}
return result as Result
})
.catch(errorHandler)
}

async _remove(id: null, params?: ServiceParams): Promise<Result[]>
@@ -388,24 +539,43 @@ export class MongoDbAdapter<
}

const model = await this.getModel(params)
const {
query,
filters: { $select }
} = this.filterQuery(id, params)
const deleteOptions = { ...params.mongodb }
const { query } = this.filterQuery(id, params)
const findParams = {
...params,
paginate: false,
query: {
...query,
$select
}
paginate: false
}

if (id === null) {
return this._find(findParams)
.then(async (result) => {
const idList = (result as Result[]).map((item: any) => item[this.id])
await model.deleteMany({ [this.id]: { $in: idList } }, params.mongodb)
return result
})
.catch(errorHandler)
}

if (params.pipeline) {
return this._get(id, params)
.then(async (result) => {
await model.deleteOne({ [this.id]: id }, params.mongodb)
return result
})
.catch(errorHandler)
}

return this._findOrGet(id, findParams)
.then(async (items) => {
await model.deleteMany(query, deleteOptions)
return items
const deleteOptions: FindOneAndDeleteOptions = {
...(params.mongodb as FindOneAndDeleteOptions),
projection: this.getProjection(params.query?.$select)
}

return model
.findOneAndDelete(query, deleteOptions)
.then((result) => {
if (!result) {
throw new NotFound(`No record found for id '${id}'`)
}
return result as Result
})
.catch(errorHandler)
}
344 changes: 344 additions & 0 deletions packages/mongodb/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -83,6 +83,11 @@ const testSuite = adapterTests([
'params.adapter + multi'
])

const defaultPaginate = {
default: 10,
max: 50
}

describe('Feathers MongoDB Service', () => {
const personSchema = {
$id: 'Person',
@@ -384,6 +389,96 @@ describe('Feathers MongoDB Service', () => {
assert.strictEqual(patched[0].friends?.length, 2)
})

it('can use $limit in patch', async () => {
const data = { name: 'ddd' }
const query = { $limit: 1 }

const result = await peopleService._patch(null, data, {
query
})

assert.strictEqual(result.length, 1)
assert.strictEqual(result[0].name, 'ddd')

const pipelineResult = await peopleService._patch(null, data, {
pipeline: [],
query
})

assert.strictEqual(pipelineResult.length, 1)
assert.strictEqual(pipelineResult[0].name, 'ddd')
})

it('can use $limit in remove', async () => {
const query = { $limit: 1 }

const result = await peopleService._remove(null, {
query
})

assert.strictEqual(result.length, 1)

const pipelineResult = await peopleService._remove(null, {
pipeline: [],
query
})

assert.strictEqual(pipelineResult.length, 1)
})

it('can use $sort in patch', async () => {
const updated = await peopleService._patch(
null,
{ name: 'ddd' },
{
query: { $limit: 1, $sort: { name: -1 } }
}
)

const result = await peopleService.find({
paginate: false,
query: { $limit: 1, $sort: { name: -1 } }
})

assert.strictEqual(updated.length, 1)
assert.strictEqual(result[0].name, 'ddd')

const pipelineUpdated = await peopleService._patch(
null,
{ name: 'eee' },
{
pipeline: [],
query: { $limit: 1, $sort: { name: -1 } }
}
)

const pipelineResult = await peopleService.find({
paginate: false,
pipeline: [],
query: { $limit: 1, $sort: { name: -1 } }
})

assert.strictEqual(pipelineUpdated.length, 1)
assert.strictEqual(pipelineResult[0].name, 'eee')
})

it('can use $sort in remove', async () => {
const removed = await peopleService._remove(null, {
query: { $limit: 1, $sort: { name: -1 } }
})

assert.strictEqual(removed.length, 1)
assert.strictEqual(removed[0].name, 'ccc')

const pipelineRemoved = await peopleService._remove(null, {
pipeline: [],
query: { $limit: 1, $sort: { name: -1 } }
})

assert.strictEqual(pipelineRemoved.length, 1)
assert.strictEqual(pipelineRemoved[0].name, 'aaa')
})

it('overrides default index selection using hint param if present', async () => {
const indexed = await peopleService.create({
name: 'Indexed',
@@ -474,6 +569,175 @@ describe('Feathers MongoDB Service', () => {
assert.deepEqual(result[0].person, bob)
assert.equal(result.length, 1)
})

it('can count documents with aggregation', async () => {
const service = app.service('people')
const paginateBefore = service.options.paginate
service.options.paginate = defaultPaginate
const query = { age: { $gte: 25 } }
const findResult = await app.service('people').find({ query })
const aggregationResult = await app.service('people').find({ query, pipeline: [] })

assert.deepStrictEqual(findResult.total, aggregationResult.total)

service.options.paginate = paginateBefore
})

it('can use aggregation in _get', async () => {
const dave = await app.service('people').create({ name: 'Dave', age: 25 })
const result = await app.service('people').get(dave._id, {
pipeline: [{ $addFields: { aggregation: true } }]
})

assert.deepStrictEqual(result, { ...dave, aggregation: true })

app.service('people').remove(dave._id)
})

it('can use aggregation in _create', async () => {
const dave = (await app.service('people').create(
{ name: 'Dave' },
{
pipeline: [{ $addFields: { aggregation: true } }]
}
)) as any

assert.deepStrictEqual(dave.aggregation, true)

app.service('people').remove(dave._id)
})

it('can use aggregation in multi _create', async () => {
app.service('people').options.multi = true
const dave = (await app.service('people').create([{ name: 'Dave' }], {
pipeline: [{ $addFields: { aggregation: true } }]
})) as any

assert.deepStrictEqual(dave[0].aggregation, true)

app.service('people').options.multi = false
app.service('people').remove(dave[0]._id)
})

it('can use aggregation in _update', async () => {
const dave = await app.service('people').create({ name: 'Dave' })
const result = await app.service('people').update(
dave._id,
{
name: 'Marshal'
},
{
pipeline: [{ $addFields: { aggregation: true } }]
}
)

assert.deepStrictEqual(result, { ...dave, name: 'Marshal', aggregation: true })

app.service('people').remove(dave._id)
})

it('can use aggregation in _patch', async () => {
const dave = await app.service('people').create({ name: 'Dave' })
const result = await app.service('people').patch(
dave._id,
{
name: 'Marshal'
},
{
pipeline: [{ $addFields: { aggregation: true } }]
}
)

assert.deepStrictEqual(result, { ...dave, name: 'Marshal', aggregation: true })

app.service('people').remove(dave._id)
})

it('can use aggregation in multi _patch', async () => {
app.service('people').options.multi = true
const dave = await app.service('people').create({ name: 'Dave' })
const result = await app.service('people').patch(
null,
{
name: 'Marshal'
},
{
query: { _id: dave._id },
pipeline: [{ $addFields: { aggregation: true } }]
}
)

assert.deepStrictEqual(result[0], { ...dave, name: 'Marshal', aggregation: true })

app.service('people').options.multi = false
app.service('people').remove(dave._id)
})

it('can use aggregation and query in _update', async () => {
const dave = await app.service('people').create({ name: 'Dave' })
const result = await app.service('people').update(
dave._id,
{
name: 'Marshal'
},
{
query: { name: 'Dave' },
pipeline: [{ $addFields: { aggregation: true } }]
}
)

assert.deepStrictEqual(result, { ...dave, name: 'Marshal', aggregation: true })

app.service('people').remove(dave._id)
})

it('can use aggregation and query in _patch', async () => {
const dave = await app.service('people').create({ name: 'Dave' })
const result = await app.service('people').patch(
dave._id,
{
name: 'Marshal'
},
{
query: { name: 'Dave' },
pipeline: [{ $addFields: { aggregation: true } }]
}
)

assert.deepStrictEqual(result, { ...dave, name: 'Marshal', aggregation: true })

app.service('people').remove(dave._id)
})

it('can use aggregation in _remove', async () => {
const dave = await app.service('people').create({ name: 'Dave' })
const result = await app.service('people').remove(dave._id, {
pipeline: [{ $addFields: { aggregation: true } }]
})

assert.deepStrictEqual(result, { ...dave, aggregation: true })

try {
await await app.service('people').get(dave._id)
throw new Error('Should never get here')
} catch (error: any) {
assert.strictEqual(error.name, 'NotFound', 'Got a NotFound Feathers error')
}
})

it('can use aggregation in multi _remove', async () => {
app.service('people').options.multi = true
const dave = await app.service('people').create({ name: 'Dave' })
const result = await app.service('people').remove(null, {
query: { _id: dave._id },
pipeline: [{ $addFields: { aggregation: true } }]
})

assert.deepStrictEqual(result[0], { ...dave, aggregation: true })

app.service('people').options.multi = false
// app.service('people').remove(dave._id)
})
})

describe('query validation', () => {
@@ -492,6 +756,86 @@ describe('Feathers MongoDB Service', () => {
})
})

// TODO: Should this test be part of the adapterTests?
describe('Updates mutated query', () => {
it('Can re-query mutated data', async () => {
const dave = await app.service('people').create({ name: 'Dave' })
const dave2 = await app.service('people').create({ name: 'Dave' })
app.service('people').options.multi = true

const updated = await app
.service('people')
.update(dave._id, { name: 'Marshal' }, { query: { name: 'Dave' } })

assert.deepStrictEqual(updated, {
...dave,
name: 'Marshal'
})

const patched = await app
.service('people')
.patch(dave._id, { name: 'Dave' }, { query: { name: 'Marshal' } })

assert.deepStrictEqual(patched, {
...dave,
name: 'Dave'
})

const updatedPipeline = await app
.service('people')
.update(dave._id, { name: 'Marshal' }, { query: { name: 'Dave' }, pipeline: [] })

assert.deepStrictEqual(updatedPipeline, {
...dave,
name: 'Marshal'
})

const patchedPipeline = await app
.service('people')
.patch(dave._id, { name: 'Dave' }, { query: { name: 'Marshal' }, pipeline: [] })

assert.deepStrictEqual(patchedPipeline, {
...dave,
name: 'Dave'
})

const multiPatch = await app
.service('people')
.patch(null, { name: 'Marshal' }, { query: { name: 'Dave' } })

assert.deepStrictEqual(multiPatch, [
{
...dave,
name: 'Marshal'
},
{
...dave2,
name: 'Marshal'
}
])

const multiPatchPipeline = await app
.service('people')
.patch(null, { name: 'Dave' }, { query: { name: 'Marshal' }, pipeline: [] })

assert.deepStrictEqual(multiPatchPipeline, [
{
...dave,
name: 'Dave'
},
{
...dave2,
name: 'Dave'
}
])

app.service('people').options.multi = false

app.service('people').remove(dave._id)
app.service('people').remove(dave2._id)
})
})

testSuite(app, errors, 'people', '_id')
testSuite(app, errors, 'people-customid', 'customid')
})

0 comments on commit f2829b1

Please sign in to comment.