core-services-sdk 1.3.24 → 1.3.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/index.js +1 -0
- package/src/core/normalize-array-operators.js +24 -0
- package/src/mongodb/dsl-to-mongo.js +99 -0
- package/src/mongodb/index.js +2 -0
- package/src/mongodb/paginate.js +48 -0
- package/tests/core/normalize-array-operators.integration.test.js +64 -0
- package/tests/core/normalize-array-operators.unit.test.js +98 -0
- package/tests/mongodb/dsl-to-mongo.integration.test.js +88 -0
- package/tests/mongodb/dsl-to-mongo.unit.test.js +106 -0
- package/tests/mongodb/dsl-to-mongo2.unit.test.js +115 -0
- package/tests/mongodb/paginate.helper.integration.test.js +151 -0
- package/tests/resources/docker-mongo-test.js +103 -0
- package/tests/resources/server-to-mongo.js +28 -0
- package/tests/resources/server.js +23 -0
- package/types/core/index.d.ts +1 -0
- package/types/core/normalize-array-operators.d.ts +1 -0
- package/types/mongodb/dsl-to-mongo.d.ts +7 -0
- package/types/mongodb/index.d.ts +1 -0
package/package.json
CHANGED
package/src/core/index.js
CHANGED
|
@@ -5,4 +5,5 @@ export * from './normalize-min-max.js'
|
|
|
5
5
|
export * from './normalize-to-array.js'
|
|
6
6
|
export * from './combine-unique-arrays.js'
|
|
7
7
|
export * from './normalize-phone-number.js'
|
|
8
|
+
export * from './normalize-array-operators.js'
|
|
8
9
|
export * from './normalize-premitives-types-or-default.js'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const normalizeOperators = (obj) => {
|
|
2
|
+
const arrayOps = ['in', 'nin', 'or', 'and']
|
|
3
|
+
|
|
4
|
+
function normalize(value) {
|
|
5
|
+
if (Array.isArray(value)) {
|
|
6
|
+
return value.map(normalize)
|
|
7
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
8
|
+
// Handle objects with numeric keys (e.g. {0: {...}, 1: {...}})
|
|
9
|
+
const keys = Object.keys(value)
|
|
10
|
+
if (keys.every((k) => /^\d+$/.test(k))) {
|
|
11
|
+
return keys.sort().map((k) => normalize(value[k]))
|
|
12
|
+
}
|
|
13
|
+
for (const [k, v] of Object.entries(value)) {
|
|
14
|
+
value[k] = normalize(v)
|
|
15
|
+
if (arrayOps.includes(k)) {
|
|
16
|
+
value[k] = Array.isArray(value[k]) ? value[k] : [value[k]]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return value
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return normalize(obj)
|
|
24
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a normalized query object into a MongoDB query object.
|
|
3
|
+
*
|
|
4
|
+
* Supported operators: eq, in, nin, gt, gte, lt, lte, and, or.
|
|
5
|
+
*
|
|
6
|
+
* Example input:
|
|
7
|
+
* {
|
|
8
|
+
* userId: { in: ['123','456'] },
|
|
9
|
+
* status: { eq: 'active' },
|
|
10
|
+
* age: { gte: 18 },
|
|
11
|
+
* or: [
|
|
12
|
+
* { role: { eq: 'admin' } },
|
|
13
|
+
* { status: { eq: 'active' } }
|
|
14
|
+
* ]
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* Example output:
|
|
18
|
+
* {
|
|
19
|
+
* userId: { $in: ['123','456'] },
|
|
20
|
+
* status: { $eq: 'active' },
|
|
21
|
+
* age: { $gte: 18 },
|
|
22
|
+
* $or: [
|
|
23
|
+
* { role: { $eq: 'admin' } },
|
|
24
|
+
* { status: { $eq: 'active' } }
|
|
25
|
+
* ]
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* @param {Record<string, any>} query - The normalized query object
|
|
29
|
+
* @returns {Record<string, any>} - A MongoDB query object
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Map DSL logical keywords to Mongo operators.
|
|
33
|
+
*/
|
|
34
|
+
const LOGICAL_OPS = new Map([
|
|
35
|
+
['or', '$or'],
|
|
36
|
+
['and', '$and'],
|
|
37
|
+
['nor', '$nor'],
|
|
38
|
+
['not', '$not'],
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert a condition object (e.g. { gt: 18, lt: 65 }) into Mongo condition.
|
|
43
|
+
* Recursively applies to nested objects.
|
|
44
|
+
*/
|
|
45
|
+
function convertCondition(condition) {
|
|
46
|
+
if (
|
|
47
|
+
condition === null ||
|
|
48
|
+
typeof condition !== 'object' ||
|
|
49
|
+
Array.isArray(condition)
|
|
50
|
+
) {
|
|
51
|
+
return condition // primitive or array stays as is
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return Object.fromEntries(
|
|
55
|
+
Object.entries(condition).map(([op, value]) => {
|
|
56
|
+
if (op.startsWith('$')) {
|
|
57
|
+
return [op, value] // already Mongo operator
|
|
58
|
+
}
|
|
59
|
+
const mongoOp = `$${op}`
|
|
60
|
+
return [mongoOp, convertCondition(value)]
|
|
61
|
+
}),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert a normalized query object into a MongoDB query object.
|
|
67
|
+
*
|
|
68
|
+
* @param {Record<string, any>} query - normalized DSL query
|
|
69
|
+
* @returns {Record<string, any>} - MongoDB query object
|
|
70
|
+
*/
|
|
71
|
+
export function toMongo(query = {}) {
|
|
72
|
+
return Object.fromEntries(
|
|
73
|
+
Object.entries(query).map(([field, condition]) => {
|
|
74
|
+
// logical operator
|
|
75
|
+
if (LOGICAL_OPS.has(field)) {
|
|
76
|
+
const mongoOp = LOGICAL_OPS.get(field)
|
|
77
|
+
if (Array.isArray(condition)) {
|
|
78
|
+
return [mongoOp, condition.map(toMongo)]
|
|
79
|
+
}
|
|
80
|
+
if (condition && typeof condition === 'object') {
|
|
81
|
+
return [mongoOp, toMongo(condition)]
|
|
82
|
+
}
|
|
83
|
+
return [mongoOp, condition]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// operator object
|
|
87
|
+
if (
|
|
88
|
+
condition !== null &&
|
|
89
|
+
typeof condition === 'object' &&
|
|
90
|
+
!Array.isArray(condition)
|
|
91
|
+
) {
|
|
92
|
+
return [field, convertCondition(condition)]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// plain value or null → $eq
|
|
96
|
+
return [field, { $eq: condition }]
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
}
|
package/src/mongodb/index.js
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ObjectId } from 'mongodb'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pagination with SQL-like ascending/descending
|
|
5
|
+
*
|
|
6
|
+
* @param {import('mongodb').Collection} collection
|
|
7
|
+
* @param {Object} options
|
|
8
|
+
* @param {Object} [options.filter={}]
|
|
9
|
+
* @param {string} [options.cursorField='_id']
|
|
10
|
+
* @param {string|Date|ObjectId} [options.cursor]
|
|
11
|
+
* @param {'asc'|'desc'} [options.order='asc']
|
|
12
|
+
* @param {number} [options.limit=10]
|
|
13
|
+
*/
|
|
14
|
+
export async function paginate(
|
|
15
|
+
collection,
|
|
16
|
+
{
|
|
17
|
+
limit = 10,
|
|
18
|
+
projection,
|
|
19
|
+
filter = {},
|
|
20
|
+
cursor = null,
|
|
21
|
+
order = 'desc',
|
|
22
|
+
cursorField = '_id',
|
|
23
|
+
} = {},
|
|
24
|
+
) {
|
|
25
|
+
const query = { ...filter }
|
|
26
|
+
const sort = { [cursorField]: order === 'asc' ? 1 : -1 }
|
|
27
|
+
|
|
28
|
+
if (cursor) {
|
|
29
|
+
if (cursorField === '_id') {
|
|
30
|
+
cursor = new ObjectId(cursor)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
query[cursorField] = order === 'asc' ? { $gt: cursor } : { $lt: cursor }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const results = await collection
|
|
37
|
+
.find(query, { projection })
|
|
38
|
+
.sort(sort)
|
|
39
|
+
.limit(limit)
|
|
40
|
+
.toArray()
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
order,
|
|
44
|
+
list: results,
|
|
45
|
+
previous: results.length ? results[0][cursorField] : null,
|
|
46
|
+
next: results.length ? results[results.length - 1][cursorField] : null,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { buildServer } from '../resources/server.js'
|
|
4
|
+
|
|
5
|
+
describe('Integration: Fastify + normalizeOperators', () => {
|
|
6
|
+
let app
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
app = buildServer()
|
|
10
|
+
await app.listen({ port: 0 })
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterAll(async () => {
|
|
14
|
+
await app.close()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('GET with userId[in]=123,456 should normalize to array', async () => {
|
|
18
|
+
const res = await app.inject('/users?userId[in]=123,456&status[eq]=active')
|
|
19
|
+
const body = res.json()
|
|
20
|
+
|
|
21
|
+
expect(body.dsl).toEqual({
|
|
22
|
+
userId: { in: ['123', '456'] },
|
|
23
|
+
status: { eq: 'active' },
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('GET with userId[in]=123 should wrap in array', async () => {
|
|
28
|
+
const res = await app.inject('/users?userId[in]=123')
|
|
29
|
+
const body = res.json()
|
|
30
|
+
|
|
31
|
+
expect(body.dsl).toEqual({ userId: { in: ['123'] } })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('GET with OR query should normalize to array', async () => {
|
|
35
|
+
const res = await app.inject(
|
|
36
|
+
'/users?or[0][status][eq]=active&or[1][role][eq]=admin',
|
|
37
|
+
)
|
|
38
|
+
const body = res.json()
|
|
39
|
+
|
|
40
|
+
expect(body.dsl).toEqual({
|
|
41
|
+
or: [{ status: { eq: 'active' } }, { role: { eq: 'admin' } }],
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('POST with where body should normalize', async () => {
|
|
46
|
+
const res = await app.inject({
|
|
47
|
+
method: 'POST',
|
|
48
|
+
url: '/users/search',
|
|
49
|
+
payload: {
|
|
50
|
+
where: {
|
|
51
|
+
userId: { in: '123' },
|
|
52
|
+
age: { gte: 18 },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const body = res.json()
|
|
58
|
+
|
|
59
|
+
expect(body.dsl).toEqual({
|
|
60
|
+
userId: { in: ['123'] },
|
|
61
|
+
age: { gte: 18 },
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { normalizeOperators } from '../../src/core/normalize-array-operators.js'
|
|
4
|
+
|
|
5
|
+
describe('normalizeOperators', () => {
|
|
6
|
+
it('should wrap single value in `in` into an array', () => {
|
|
7
|
+
const input = { userId: { in: '123' } }
|
|
8
|
+
const output = normalizeOperators(input)
|
|
9
|
+
expect(output).toEqual({ userId: { in: ['123'] } })
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('should keep array value in `in` as is', () => {
|
|
13
|
+
const input = { userId: { in: ['123', '456'] } }
|
|
14
|
+
const output = normalizeOperators(input)
|
|
15
|
+
expect(output).toEqual({ userId: { in: ['123', '456'] } })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should wrap single value in `nin` into an array', () => {
|
|
19
|
+
const input = { userId: { nin: '789' } }
|
|
20
|
+
const output = normalizeOperators(input)
|
|
21
|
+
expect(output).toEqual({ userId: { nin: ['789'] } })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should normalize object with numeric keys into array (or)', () => {
|
|
25
|
+
const input = {
|
|
26
|
+
or: { 0: { status: { eq: 'active' } }, 1: { role: { eq: 'admin' } } },
|
|
27
|
+
}
|
|
28
|
+
const output = normalizeOperators(input)
|
|
29
|
+
expect(output).toEqual({
|
|
30
|
+
or: [{ status: { eq: 'active' } }, { role: { eq: 'admin' } }],
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should wrap single object in `or` into array', () => {
|
|
35
|
+
const input = { or: { status: { eq: 'active' } } }
|
|
36
|
+
const output = normalizeOperators(input)
|
|
37
|
+
expect(output).toEqual({
|
|
38
|
+
or: [{ status: { eq: 'active' } }],
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should handle nested in + or together', () => {
|
|
43
|
+
const input = {
|
|
44
|
+
or: {
|
|
45
|
+
0: { userId: { in: '123' } },
|
|
46
|
+
1: { userId: { in: ['456', '789'] } },
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
const output = normalizeOperators(input)
|
|
50
|
+
expect(output).toEqual({
|
|
51
|
+
or: [{ userId: { in: ['123'] } }, { userId: { in: ['456', '789'] } }],
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should handle `and` with mixed single and multiple values', () => {
|
|
56
|
+
const input = {
|
|
57
|
+
and: {
|
|
58
|
+
0: { status: { eq: 'active' } },
|
|
59
|
+
1: { userId: { in: '123' } },
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
const output = normalizeOperators(input)
|
|
63
|
+
expect(output).toEqual({
|
|
64
|
+
and: [{ status: { eq: 'active' } }, { userId: { in: ['123'] } }],
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should do nothing for fields without array operators', () => {
|
|
69
|
+
const input = { age: { gte: 18 } }
|
|
70
|
+
const output = normalizeOperators(input)
|
|
71
|
+
expect(output).toEqual({ age: { gte: 18 } })
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should work with deeply nested structures', () => {
|
|
75
|
+
const input = {
|
|
76
|
+
or: {
|
|
77
|
+
0: {
|
|
78
|
+
and: {
|
|
79
|
+
0: { userId: { in: '123' } },
|
|
80
|
+
1: { status: { eq: 'active' } },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
1: { role: { nin: 'guest' } },
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const output = normalizeOperators(input)
|
|
88
|
+
|
|
89
|
+
expect(output).toEqual({
|
|
90
|
+
or: [
|
|
91
|
+
{
|
|
92
|
+
and: [{ userId: { in: ['123'] } }, { status: { eq: 'active' } }],
|
|
93
|
+
},
|
|
94
|
+
{ role: { nin: ['guest'] } },
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { buildServer } from '../resources/server-to-mongo.js'
|
|
4
|
+
|
|
5
|
+
describe('Integration E2E: Fastify + normalizeOperators + toMongo', () => {
|
|
6
|
+
let app
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
app = buildServer()
|
|
10
|
+
await app.listen({ port: 0 })
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterAll(async () => {
|
|
14
|
+
await app.close()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('GET with simple comparison and AND logic', async () => {
|
|
18
|
+
const res = await app.inject(
|
|
19
|
+
'/search?and[0][age][gte]=18&and[1][age][lt]=65&status[eq]=active',
|
|
20
|
+
)
|
|
21
|
+
const body = res.json()
|
|
22
|
+
|
|
23
|
+
expect(body.dsl).toEqual({
|
|
24
|
+
and: [{ age: { gte: '18' } }, { age: { lt: '65' } }],
|
|
25
|
+
status: { eq: 'active' },
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
expect(body.mongo).toEqual({
|
|
29
|
+
$and: [{ age: { $gte: '18' } }, { age: { $lt: '65' } }],
|
|
30
|
+
status: { $eq: 'active' },
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('GET with OR and IN', async () => {
|
|
35
|
+
const res = await app.inject(
|
|
36
|
+
'/search?or[0][role][eq]=admin&or[1][role][eq]=user&userId[in]=123,456',
|
|
37
|
+
)
|
|
38
|
+
const body = res.json()
|
|
39
|
+
|
|
40
|
+
expect(body.dsl).toEqual({
|
|
41
|
+
or: [{ role: { eq: 'admin' } }, { role: { eq: 'user' } }],
|
|
42
|
+
userId: { in: ['123', '456'] },
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(body.mongo).toEqual({
|
|
46
|
+
$or: [{ role: { $eq: 'admin' } }, { role: { $eq: 'user' } }],
|
|
47
|
+
userId: { $in: ['123', '456'] },
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('POST with nested AND/OR, gt/lt, exists', async () => {
|
|
52
|
+
const res = await app.inject({
|
|
53
|
+
method: 'POST',
|
|
54
|
+
url: '/search',
|
|
55
|
+
payload: {
|
|
56
|
+
where: {
|
|
57
|
+
and: [
|
|
58
|
+
{ age: { gt: 18, lt: 65 } },
|
|
59
|
+
{
|
|
60
|
+
or: [{ status: { eq: 'active' } }, { status: { eq: 'pending' } }],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
deletedAt: { exists: false },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const body = res.json()
|
|
69
|
+
|
|
70
|
+
expect(body.dsl).toEqual({
|
|
71
|
+
and: [
|
|
72
|
+
{ age: { gt: 18, lt: 65 } },
|
|
73
|
+
{ or: [{ status: { eq: 'active' } }, { status: { eq: 'pending' } }] },
|
|
74
|
+
],
|
|
75
|
+
deletedAt: { exists: false },
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(body.mongo).toEqual({
|
|
79
|
+
$and: [
|
|
80
|
+
{ age: { $gt: 18, $lt: 65 } },
|
|
81
|
+
{
|
|
82
|
+
$or: [{ status: { $eq: 'active' } }, { status: { $eq: 'pending' } }],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
deletedAt: { $exists: false },
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { toMongo } from '../../src/mongodb/dsl-to-mongo.js'
|
|
4
|
+
|
|
5
|
+
describe('toMongo', () => {
|
|
6
|
+
it('should convert plain values to $eq', () => {
|
|
7
|
+
const input = { status: 'active', role: 'admin' }
|
|
8
|
+
const output = toMongo(input)
|
|
9
|
+
expect(output).toEqual({
|
|
10
|
+
status: { $eq: 'active' },
|
|
11
|
+
role: { $eq: 'admin' },
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should convert eq operator to $eq', () => {
|
|
16
|
+
const input = { status: { eq: 'active' } }
|
|
17
|
+
const output = toMongo(input)
|
|
18
|
+
expect(output).toEqual({ status: { $eq: 'active' } })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should convert in operator to $in', () => {
|
|
22
|
+
const input = { userId: { in: ['123', '456'] } }
|
|
23
|
+
const output = toMongo(input)
|
|
24
|
+
expect(output).toEqual({ userId: { $in: ['123', '456'] } })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should convert nin operator to $nin', () => {
|
|
28
|
+
const input = { userId: { nin: ['123'] } }
|
|
29
|
+
const output = toMongo(input)
|
|
30
|
+
expect(output).toEqual({ userId: { $nin: ['123'] } })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should convert comparison operators correctly', () => {
|
|
34
|
+
const input = {
|
|
35
|
+
age: { gt: 18, lte: 65 },
|
|
36
|
+
}
|
|
37
|
+
const output = toMongo(input)
|
|
38
|
+
expect(output).toEqual({
|
|
39
|
+
age: { $gt: 18, $lte: 65 },
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should handle $or with multiple conditions', () => {
|
|
44
|
+
const input = {
|
|
45
|
+
or: [{ status: { eq: 'active' } }, { role: { eq: 'admin' } }],
|
|
46
|
+
}
|
|
47
|
+
const output = toMongo(input)
|
|
48
|
+
expect(output).toEqual({
|
|
49
|
+
$or: [{ status: { $eq: 'active' } }, { role: { $eq: 'admin' } }],
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should handle $and with multiple conditions', () => {
|
|
54
|
+
const input = {
|
|
55
|
+
and: [{ status: { eq: 'active' } }, { age: { gte: 18 } }],
|
|
56
|
+
}
|
|
57
|
+
const output = toMongo(input)
|
|
58
|
+
expect(output).toEqual({
|
|
59
|
+
$and: [{ status: { $eq: 'active' } }, { age: { $gte: 18 } }],
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should handle nested logical operators', () => {
|
|
64
|
+
const input = {
|
|
65
|
+
or: [
|
|
66
|
+
{ status: { eq: 'active' } },
|
|
67
|
+
{
|
|
68
|
+
and: [{ age: { gte: 18 } }, { age: { lte: 65 } }],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
const output = toMongo(input)
|
|
73
|
+
expect(output).toEqual({
|
|
74
|
+
$or: [
|
|
75
|
+
{ status: { $eq: 'active' } },
|
|
76
|
+
{
|
|
77
|
+
$and: [{ age: { $gte: 18 } }, { age: { $lte: 65 } }],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should handle multiple fields mixed with operators', () => {
|
|
84
|
+
const input = {
|
|
85
|
+
userId: { in: ['123', '456'] },
|
|
86
|
+
status: 'active',
|
|
87
|
+
or: [{ role: { eq: 'admin' } }, { age: { gt: 30 } }],
|
|
88
|
+
}
|
|
89
|
+
const output = toMongo(input)
|
|
90
|
+
expect(output).toEqual({
|
|
91
|
+
userId: { $in: ['123', '456'] },
|
|
92
|
+
status: { $eq: 'active' },
|
|
93
|
+
$or: [{ role: { $eq: 'admin' } }, { age: { $gt: 30 } }],
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should return empty object if input is empty', () => {
|
|
98
|
+
expect(toMongo({})).toEqual({})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should handle null values as $eq null', () => {
|
|
102
|
+
const input = { deletedAt: null }
|
|
103
|
+
const output = toMongo(input)
|
|
104
|
+
expect(output).toEqual({ deletedAt: { $eq: null } })
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { toMongo } from '../../src/mongodb/dsl-to-mongo.js'
|
|
4
|
+
|
|
5
|
+
describe('toMongo - extended operators', () => {
|
|
6
|
+
it('should support all comparison operators', () => {
|
|
7
|
+
const input = {
|
|
8
|
+
age: { eq: 30, ne: 40, gt: 10, gte: 20, lt: 100, lte: 90 },
|
|
9
|
+
userId: { in: ['u1', 'u2'], nin: ['u3'] },
|
|
10
|
+
}
|
|
11
|
+
const output = toMongo(input)
|
|
12
|
+
|
|
13
|
+
expect(output).toEqual({
|
|
14
|
+
age: { $eq: 30, $ne: 40, $gt: 10, $gte: 20, $lt: 100, $lte: 90 },
|
|
15
|
+
userId: { $in: ['u1', 'u2'], $nin: ['u3'] },
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should support logical operators', () => {
|
|
20
|
+
const input = {
|
|
21
|
+
and: [{ age: { gt: 18 } }, { status: { eq: 'active' } }],
|
|
22
|
+
or: [{ role: { eq: 'admin' } }, { role: { eq: 'user' } }],
|
|
23
|
+
nor: [{ country: { eq: 'US' } }],
|
|
24
|
+
}
|
|
25
|
+
const output = toMongo(input)
|
|
26
|
+
|
|
27
|
+
expect(output).toEqual({
|
|
28
|
+
$and: [{ age: { $gt: 18 } }, { status: { $eq: 'active' } }],
|
|
29
|
+
$or: [{ role: { $eq: 'admin' } }, { role: { $eq: 'user' } }],
|
|
30
|
+
$nor: [{ country: { $eq: 'US' } }],
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should support element operators', () => {
|
|
35
|
+
const input = {
|
|
36
|
+
deletedAt: { exists: false },
|
|
37
|
+
status: { type: 'string' },
|
|
38
|
+
}
|
|
39
|
+
const output = toMongo(input)
|
|
40
|
+
|
|
41
|
+
expect(output).toEqual({
|
|
42
|
+
deletedAt: { $exists: false },
|
|
43
|
+
status: { $type: 'string' },
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should support evaluation operators', () => {
|
|
48
|
+
const input = {
|
|
49
|
+
name: { regex: '^haim', options: 'i' },
|
|
50
|
+
score: { mod: [4, 0] },
|
|
51
|
+
description: { text: 'fast search' },
|
|
52
|
+
}
|
|
53
|
+
const output = toMongo(input)
|
|
54
|
+
|
|
55
|
+
expect(output).toEqual({
|
|
56
|
+
name: { $regex: '^haim', $options: 'i' },
|
|
57
|
+
score: { $mod: [4, 0] },
|
|
58
|
+
description: { $text: 'fast search' },
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should support array operators', () => {
|
|
63
|
+
const input = {
|
|
64
|
+
tags: { all: ['tech', 'ai'], size: 3 },
|
|
65
|
+
scores: { elemMatch: { gt: 90 } },
|
|
66
|
+
}
|
|
67
|
+
const output = toMongo(input)
|
|
68
|
+
|
|
69
|
+
expect(output).toEqual({
|
|
70
|
+
tags: { $all: ['tech', 'ai'], $size: 3 },
|
|
71
|
+
scores: { $elemMatch: { $gt: 90 } },
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should not transform unknown operators (false positive check)', () => {
|
|
76
|
+
const input = {
|
|
77
|
+
age: { weirdOp: 123 },
|
|
78
|
+
}
|
|
79
|
+
const output = toMongo(input)
|
|
80
|
+
|
|
81
|
+
expect(output).toEqual({
|
|
82
|
+
age: { $weirdOp: 123 },
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should not wrap already Mongo operators with $$ (false positive)', () => {
|
|
87
|
+
const input = {
|
|
88
|
+
age: { $gte: 18 },
|
|
89
|
+
}
|
|
90
|
+
const output = toMongo(input)
|
|
91
|
+
|
|
92
|
+
expect(output).toEqual({
|
|
93
|
+
age: { $gte: 18 },
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should handle nested structures with mix of operators', () => {
|
|
98
|
+
const input = {
|
|
99
|
+
or: [
|
|
100
|
+
{ and: [{ age: { gte: 18 } }, { age: { lte: 65 } }] },
|
|
101
|
+
{ status: { ne: 'banned' } },
|
|
102
|
+
],
|
|
103
|
+
tags: { all: ['mongo', 'node'] },
|
|
104
|
+
}
|
|
105
|
+
const output = toMongo(input)
|
|
106
|
+
|
|
107
|
+
expect(output).toEqual({
|
|
108
|
+
$or: [
|
|
109
|
+
{ $and: [{ age: { $gte: 18 } }, { age: { $lte: 65 } }] },
|
|
110
|
+
{ status: { $ne: 'banned' } },
|
|
111
|
+
],
|
|
112
|
+
tags: { $all: ['mongo', 'node'] },
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { paginate } from '../../src/mongodb/paginate.js'
|
|
4
|
+
import { startMongo, stopMongo } from '../resources/docker-mongo-test.js'
|
|
5
|
+
import { initializeMongoDb } from '../../src/mongodb/initialize-mongodb.js'
|
|
6
|
+
|
|
7
|
+
const MONGO_PORT = 29050
|
|
8
|
+
const CONTAINER_NAME = 'mongo-auth-attempts-paginate-test'
|
|
9
|
+
|
|
10
|
+
let db
|
|
11
|
+
let collection
|
|
12
|
+
describe('paginate - Integration', () => {
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
startMongo(MONGO_PORT, CONTAINER_NAME)
|
|
15
|
+
|
|
16
|
+
db = await initializeMongoDb({
|
|
17
|
+
config: {
|
|
18
|
+
uri: `mongodb://0.0.0.0:${MONGO_PORT}`,
|
|
19
|
+
options: { dbName: 'users-management' },
|
|
20
|
+
},
|
|
21
|
+
collectionNames: {
|
|
22
|
+
TestDocs: 'test_docs',
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
collection = db.TestDocs
|
|
27
|
+
})
|
|
28
|
+
afterAll(() => {
|
|
29
|
+
stopMongo(CONTAINER_NAME)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
await collection.deleteMany({})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const seedDocs = async (count = 12) => {
|
|
37
|
+
const docs = Array.from({ length: count }).map((_, i) => ({
|
|
38
|
+
name: `doc_${i}`,
|
|
39
|
+
createdAt: new Date(Date.now() + i * 1000),
|
|
40
|
+
}))
|
|
41
|
+
await collection.insertMany(docs)
|
|
42
|
+
return docs
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
it('returns first page with limit', async () => {
|
|
46
|
+
await seedDocs(12)
|
|
47
|
+
|
|
48
|
+
const result = await paginate(collection, {
|
|
49
|
+
filter: {},
|
|
50
|
+
limit: 5,
|
|
51
|
+
order: 'desc',
|
|
52
|
+
cursorField: 'createdAt',
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
expect(result.list).toHaveLength(5)
|
|
56
|
+
expect(result.next).toBeDefined()
|
|
57
|
+
expect(result.previous).toBeDefined()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('returns empty when no docs match filter', async () => {
|
|
61
|
+
const result = await paginate(collection, {
|
|
62
|
+
filter: { name: 'not-exist' },
|
|
63
|
+
limit: 5,
|
|
64
|
+
order: 'desc',
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
expect(result.list).toHaveLength(0)
|
|
68
|
+
expect(result.next).toBeNull()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('supports ascending order', async () => {
|
|
72
|
+
const docs = await seedDocs(3)
|
|
73
|
+
|
|
74
|
+
const ascResult = await paginate(collection, {
|
|
75
|
+
filter: {},
|
|
76
|
+
limit: 3,
|
|
77
|
+
order: 'asc',
|
|
78
|
+
cursorField: 'createdAt',
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
expect(ascResult.list[0].name).toBe(docs[0].name)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('supports descending order', async () => {
|
|
85
|
+
const docs = await seedDocs(3)
|
|
86
|
+
|
|
87
|
+
const descResult = await paginate(collection, {
|
|
88
|
+
filter: {},
|
|
89
|
+
limit: 3,
|
|
90
|
+
order: 'desc',
|
|
91
|
+
cursorField: 'createdAt',
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expect(descResult.list[0].name).toBe(docs[2].name)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('paginates with cursor (next page)', async () => {
|
|
98
|
+
await seedDocs(7)
|
|
99
|
+
|
|
100
|
+
// first page
|
|
101
|
+
const firstPage = await paginate(collection, {
|
|
102
|
+
filter: {},
|
|
103
|
+
limit: 3,
|
|
104
|
+
order: 'asc',
|
|
105
|
+
cursorField: 'createdAt',
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(firstPage.list).toHaveLength(3)
|
|
109
|
+
expect(firstPage.next).toBeDefined()
|
|
110
|
+
|
|
111
|
+
// second page using cursor
|
|
112
|
+
const secondPage = await paginate(collection, {
|
|
113
|
+
filter: {},
|
|
114
|
+
limit: 3,
|
|
115
|
+
order: 'asc',
|
|
116
|
+
cursorField: 'createdAt',
|
|
117
|
+
cursor: firstPage.next,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
expect(secondPage.list).toHaveLength(3)
|
|
121
|
+
// validate no overlap between first and second page
|
|
122
|
+
expect(secondPage.list[0]._id.toString()).not.toBe(
|
|
123
|
+
firstPage.list[0]._id.toString(),
|
|
124
|
+
)
|
|
125
|
+
expect(secondPage.list.map((d) => d._id.toString())).not.toEqual(
|
|
126
|
+
firstPage.list.map((d) => d._id.toString()),
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('paginates with stringified ObjectId as cursor', async () => {
|
|
131
|
+
const docs = await seedDocs(5)
|
|
132
|
+
|
|
133
|
+
// @ts-ignore
|
|
134
|
+
const stringCursor = docs[2]._id.toString()
|
|
135
|
+
|
|
136
|
+
const page = await paginate(collection, {
|
|
137
|
+
filter: {},
|
|
138
|
+
limit: 2,
|
|
139
|
+
order: 'asc',
|
|
140
|
+
cursorField: '_id',
|
|
141
|
+
cursor: stringCursor,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
expect(page.list).toHaveLength(2)
|
|
145
|
+
|
|
146
|
+
// @ts-ignore
|
|
147
|
+
expect(page.list[0]._id.toString()).toBe(docs[3]._id.toString())
|
|
148
|
+
// @ts-ignore
|
|
149
|
+
expect(page.list[1]._id.toString()).toBe(docs[4]._id.toString())
|
|
150
|
+
})
|
|
151
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Start a MongoDB Docker container for testing (standalone)
|
|
5
|
+
* @param {number} port - Host port for MongoDB
|
|
6
|
+
* @param {string} containerName - Name of the Docker container
|
|
7
|
+
*/
|
|
8
|
+
export function startMongo(port = 27027, containerName = 'mongo-test') {
|
|
9
|
+
console.log(`[MongoTest] Starting MongoDB on port ${port}...`)
|
|
10
|
+
|
|
11
|
+
stopMongo(containerName)
|
|
12
|
+
|
|
13
|
+
// Start MongoDB detached
|
|
14
|
+
execSync(`docker run -d --name ${containerName} -p ${port}:27017 mongo:6.0`, {
|
|
15
|
+
stdio: 'inherit',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
waitForMongo(port)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Start a MongoDB Replica Set Docker container for testing
|
|
23
|
+
* @param {number} port - Host port for MongoDB
|
|
24
|
+
* @param {string} containerName - Name of the Docker container
|
|
25
|
+
* @param {string} replSet - Replica set name
|
|
26
|
+
*/
|
|
27
|
+
export function startMongoReplicaSet(
|
|
28
|
+
port = 27027,
|
|
29
|
+
containerName = 'mongo-rs-test',
|
|
30
|
+
replSet = 'rs0',
|
|
31
|
+
) {
|
|
32
|
+
console.log(
|
|
33
|
+
`[MongoTest] Starting MongoDB Replica Set "${replSet}" on port ${port}...`,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Run mongo with replica set enabled
|
|
38
|
+
execSync(
|
|
39
|
+
`docker run -d --name ${containerName} -p ${port}:27017 mongo:6.0 mongod --replSet ${replSet} --bind_ip_all`,
|
|
40
|
+
{ stdio: 'inherit' },
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
waitForMongo(port)
|
|
44
|
+
|
|
45
|
+
// Initialize replica set
|
|
46
|
+
console.log(`[MongoTest] Initializing replica set "${replSet}"...`)
|
|
47
|
+
execSync(
|
|
48
|
+
`docker exec ${containerName} mongosh --eval "rs.initiate({_id: '${replSet}', members:[{ _id:0, host: 'localhost:${port}' }]})"`,
|
|
49
|
+
{ stdio: 'inherit' },
|
|
50
|
+
)
|
|
51
|
+
} catch {
|
|
52
|
+
console.warn(`[MongoTest] Replica set may already be initiated.`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Stop and remove the MongoDB Docker container
|
|
58
|
+
* @param {string} containerName
|
|
59
|
+
*/
|
|
60
|
+
export function stopMongo(containerName = 'mongo-test') {
|
|
61
|
+
console.log(`[MongoTest] Stopping MongoDB...`)
|
|
62
|
+
try {
|
|
63
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' })
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isConnected(port) {
|
|
68
|
+
try {
|
|
69
|
+
execSync(`mongosh --port ${port} --eval "db.runCommand({ ping: 1 })"`, {
|
|
70
|
+
stdio: 'ignore',
|
|
71
|
+
})
|
|
72
|
+
return true
|
|
73
|
+
} catch {
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Wait until MongoDB is ready to accept connections
|
|
79
|
+
* @param {number} port
|
|
80
|
+
*/
|
|
81
|
+
function waitForMongo(port) {
|
|
82
|
+
console.log(`[MongoTest] Waiting for MongoDB to be ready...`)
|
|
83
|
+
const maxRetries = 20
|
|
84
|
+
let retries = 0
|
|
85
|
+
let connected = false
|
|
86
|
+
|
|
87
|
+
while (!connected && retries < maxRetries) {
|
|
88
|
+
try {
|
|
89
|
+
connected = isConnected(port)
|
|
90
|
+
retries++
|
|
91
|
+
} catch {
|
|
92
|
+
retries++
|
|
93
|
+
execSync(`sleep 1`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!connected) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`[MongoTest] MongoDB failed to start within ${maxRetries} seconds`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
console.log(`[MongoTest] MongoDB is ready.`)
|
|
103
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import qs from 'qs'
|
|
2
|
+
import Fastify from 'fastify'
|
|
3
|
+
|
|
4
|
+
import { toMongo } from '../../src/mongodb/dsl-to-mongo.js'
|
|
5
|
+
import { normalizeOperators } from '../../src/core/normalize-array-operators.js'
|
|
6
|
+
|
|
7
|
+
export function buildServer() {
|
|
8
|
+
const app = Fastify({
|
|
9
|
+
querystringParser: (str) => qs.parse(str, { comma: true, depth: 10 }),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
// GET /search?...
|
|
13
|
+
app.get('/search', async (req) => {
|
|
14
|
+
const dsl = normalizeOperators(req.query)
|
|
15
|
+
const mongo = toMongo(dsl)
|
|
16
|
+
return { dsl, mongo }
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// POST /search with body { where: {...} }
|
|
20
|
+
app.post('/search', async (req) => {
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
const dsl = normalizeOperators(req.body?.where || {})
|
|
23
|
+
const mongo = toMongo(dsl)
|
|
24
|
+
return { dsl, mongo }
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return app
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Fastify from 'fastify'
|
|
2
|
+
import qs from 'qs'
|
|
3
|
+
import { normalizeOperators } from '../../src/core/normalize-array-operators.js'
|
|
4
|
+
|
|
5
|
+
export function buildServer() {
|
|
6
|
+
const app = Fastify({
|
|
7
|
+
querystringParser: (str) => qs.parse(str, { comma: true, depth: 10 }),
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
// GET /users
|
|
11
|
+
app.get('/users', async (req) => {
|
|
12
|
+
const dsl = normalizeOperators(req.query)
|
|
13
|
+
return { dsl }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// POST /users/search
|
|
17
|
+
app.post('/users/search', async (req) => {
|
|
18
|
+
const dsl = normalizeOperators(req.body?.where || {})
|
|
19
|
+
return { dsl }
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
return app
|
|
23
|
+
}
|
package/types/core/index.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ export * from "./normalize-min-max.js";
|
|
|
5
5
|
export * from "./normalize-to-array.js";
|
|
6
6
|
export * from "./combine-unique-arrays.js";
|
|
7
7
|
export * from "./normalize-phone-number.js";
|
|
8
|
+
export * from "./normalize-array-operators.js";
|
|
8
9
|
export * from "./normalize-premitives-types-or-default.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function normalizeOperators(obj: any): any;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a normalized query object into a MongoDB query object.
|
|
3
|
+
*
|
|
4
|
+
* @param {Record<string, any>} query - normalized DSL query
|
|
5
|
+
* @returns {Record<string, any>} - MongoDB query object
|
|
6
|
+
*/
|
|
7
|
+
export function toMongo(query?: Record<string, any>): Record<string, any>;
|
package/types/mongodb/index.d.ts
CHANGED