assai 0.0.1
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/README.md +136 -0
- package/bash/publish.sh +3 -0
- package/index.mjs +1 -0
- package/jsconfig.json +10 -0
- package/package.json +28 -0
- package/src/collection.mjs +112 -0
- package/src/data/errors/operation_fail.mjs +10 -0
- package/src/mongo_client.mjs +33 -0
- package/src/types.ts +42 -0
- package/src/usecases/generate_new_id.mjs +5 -0
- package/src/usecases/operation/additional/delete_one_or_throw.mjs +18 -0
- package/src/usecases/operation/additional/index.mjs +1 -0
- package/src/usecases/operation/count.mjs +17 -0
- package/src/usecases/operation/count.test.mjs +49 -0
- package/src/usecases/operation/delete_many.mjs +16 -0
- package/src/usecases/operation/delete_one.mjs +16 -0
- package/src/usecases/operation/delete_one.test.mjs +30 -0
- package/src/usecases/operation/find.mjs +34 -0
- package/src/usecases/operation/find_one.mjs +26 -0
- package/src/usecases/operation/find_one.test.mjs +28 -0
- package/src/usecases/operation/index.mjs +9 -0
- package/src/usecases/operation/insert_many.mjs +30 -0
- package/src/usecases/operation/insert_many.test.mjs +44 -0
- package/src/usecases/operation/insert_one.mjs +27 -0
- package/src/usecases/operation/insert_one.test.mjs +74 -0
- package/src/usecases/operation/update_many.mjs +18 -0
- package/src/usecases/operation/update_one.mjs +18 -0
- package/src/usecases/operation/update_one.test.mjs +56 -0
- package/src/usecases/transformers/id/index.mjs +5 -0
- package/src/usecases/transformers/id/rename_find_options.mjs +10 -0
- package/src/usecases/transformers/id/rename_to_dev_id.mjs +16 -0
- package/src/usecases/transformers/id/rename_to_mongo_id.mjs +16 -0
- package/src/usecases/transformers/index.mjs +2 -0
- package/src/usecases/transformers/object_id/ids_into_strings.mjs +30 -0
- package/src/usecases/transformers/object_id/index.mjs +4 -0
- package/src/usecases/transformers/object_id/strings_into_id.mjs +42 -0
- package/test/manage_mock_registry.mjs +53 -0
- package/test/mock_get_collection.mjs +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
This is a small package to improve some things that you, as a developer, have to deal with when working with mongo, like:
|
|
2
|
+
|
|
3
|
+
- [_id](##_id)
|
|
4
|
+
- [ObjectId](##ObjectId)
|
|
5
|
+
- [projection](##projection)
|
|
6
|
+
- [Connection String](##connection-string)
|
|
7
|
+
- [Client Instance](##client-instance)
|
|
8
|
+
|
|
9
|
+
# Example
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import {getCollection} from "assai"
|
|
13
|
+
|
|
14
|
+
const collection = await getCollection()
|
|
15
|
+
const docs = await collection.find({}, {limit: 10})
|
|
16
|
+
/**
|
|
17
|
+
* [{id: "507f1f77bcf86cd799439011", name: "Mario"}, ...]
|
|
18
|
+
*/
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## _id
|
|
22
|
+
|
|
23
|
+
Ever wanted to use just "id" in your collections instead of "_id"?
|
|
24
|
+
|
|
25
|
+
This package does just that!
|
|
26
|
+
|
|
27
|
+
Every data that enters the database can, optionally, have an "id". In this case, before sending the data to the native mongodb driver, the object will rename this property to "_id", which mongodb understands. This operation will be applied to `insertOne` and `insertMany` methods.
|
|
28
|
+
|
|
29
|
+
Also, the methods `updateOne`, `updateMany`, `deleteOne`, `deleteMany`, `findOne` and `find` will also rename the field "id" to "_id".
|
|
30
|
+
|
|
31
|
+
## ObjectId
|
|
32
|
+
|
|
33
|
+
Another thing that is related to "_id" fields are the `ObjectId`s.
|
|
34
|
+
|
|
35
|
+
The issue is that your application can before unnecessarily verbose. To fix that, the package will automatically convert all objectId strings into a ObjectId under the hood and all objectIds that will come from your collection, will be converted to strings.
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
await collection.insertOne({
|
|
39
|
+
name: "Matteo",
|
|
40
|
+
groupId: "507f1f77bcf86cd799439011" // This will be stored as an ObjectId
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Every time you need a new id, you can call the `id` method:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
const myStringId = driver.generateNewId()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
One example this could be useful is if have an API endpoint that accepts structured data. And you use these values to query the database. Like so:
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// Client code
|
|
54
|
+
const response = await axios.post('/posts', {
|
|
55
|
+
userId: "507f1f77bcf86cd799439011"
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This is fine, but you will have to convert a `string` into a `ObjectId` for each field value that is an objectId in your collection. Even though this is easy to do, you could forget somewhere.
|
|
60
|
+
|
|
61
|
+
Instead of carrying this risk, you can use the object as-is and the conversion will be made automatically.
|
|
62
|
+
|
|
63
|
+
## projection
|
|
64
|
+
|
|
65
|
+
The projection from the native mongodb driver is fine as it is. But there is one thing that is annoying: it can cause your program to fail.
|
|
66
|
+
|
|
67
|
+
To be honest, this behavior makes some sense because this usually comes from a mistake the developer made. But this is not always the case and it goes against how mongodb and javascript in general behave: they avoid throwing an exception when possible.
|
|
68
|
+
|
|
69
|
+
For that reason, you won't see this error while using this package:
|
|
70
|
+
```
|
|
71
|
+
Cannot do exclusion on field '...' in inclusion projection
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Making projections like that valid:
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"name": true,
|
|
78
|
+
"createdAt": false
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Connection String
|
|
83
|
+
|
|
84
|
+
A default environment variable is assumed: DATABASE_URL.
|
|
85
|
+
|
|
86
|
+
Which makes it easier to start a connection:
|
|
87
|
+
```js
|
|
88
|
+
const database = await getCollection('myCollection')
|
|
89
|
+
```
|
|
90
|
+
This will read the value from `process.env.DATABASE_URL`.
|
|
91
|
+
|
|
92
|
+
You can still pass a custom connection string:
|
|
93
|
+
```js
|
|
94
|
+
const database = await getCollection('myCollection', {
|
|
95
|
+
cs: 'my connection string',
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Client Instance
|
|
100
|
+
|
|
101
|
+
If you ever worked with serverless, you will notice that you shouldn't open and close a connection everytime your function runs. You need to cache it. The package does this caching for you by default.
|
|
102
|
+
|
|
103
|
+
You could also do this for simplicity, so instead of:
|
|
104
|
+
```js
|
|
105
|
+
// db.js
|
|
106
|
+
let cachedClient = null
|
|
107
|
+
export async function getDb () {
|
|
108
|
+
if (cachedClient == null) {
|
|
109
|
+
const client = await MongoClient('...')
|
|
110
|
+
cachedClient = client
|
|
111
|
+
}
|
|
112
|
+
return cachedClient.db()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// router.js
|
|
116
|
+
import {getDb} from 'db.js'
|
|
117
|
+
|
|
118
|
+
router.post('/', (req, res) => {
|
|
119
|
+
const db = await getDb()
|
|
120
|
+
const col db.collection('myCollection')
|
|
121
|
+
// ...
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
You can simply write:
|
|
126
|
+
```js
|
|
127
|
+
router.post('/', (req, res) => {
|
|
128
|
+
const col = getCollection('myCollection')
|
|
129
|
+
// Here the connection is opened only if it is not opened already.
|
|
130
|
+
// Further calls to this route won't open a connection.
|
|
131
|
+
await driver.insertOne({
|
|
132
|
+
name: req.body.name,
|
|
133
|
+
})
|
|
134
|
+
// ...
|
|
135
|
+
})
|
|
136
|
+
```
|
package/bash/publish.sh
ADDED
package/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './src/collection.mjs'
|
package/jsconfig.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "assai",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"repository": {
|
|
5
|
+
"url": "https://github.com/TimeLord2010/assai",
|
|
6
|
+
"type": "git"
|
|
7
|
+
},
|
|
8
|
+
"description": "A simple mongodb wrapper to make mongo even easier to work with.",
|
|
9
|
+
"main": "index.mjs",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node --env-file=.env index.mjs",
|
|
12
|
+
"test": "node --env-file=.env --test"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mongodb",
|
|
16
|
+
"orm"
|
|
17
|
+
],
|
|
18
|
+
"author": "Vinícius Gabriel",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"mongodb": "^6.5.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@faker-js/faker": "^8.4.1",
|
|
25
|
+
"@types/node": "^20.12.7",
|
|
26
|
+
"typescript": "^5.4.5"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Collection } from 'mongodb'
|
|
2
|
+
import { getMongoClient } from './mongo_client.mjs'
|
|
3
|
+
import { deleteOneOrThrow } from './usecases/operation/additional/index.mjs'
|
|
4
|
+
import {
|
|
5
|
+
count, deleteMany, deleteOne, find, findOne, insertOne, updateMany, updateOne,
|
|
6
|
+
} from './usecases/operation/index.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generates a collection object that automatically manage ObjectId conversion to string.
|
|
10
|
+
*
|
|
11
|
+
* This method will read the string DATABASE_URL to create a connection. If you have it in another
|
|
12
|
+
* location, you will need to pass it at `connectionString` property inside the options parameter.
|
|
13
|
+
*
|
|
14
|
+
* The connection is cached by default. Use `collectionGetter` and `cachedCollectionGetter` to
|
|
15
|
+
* customize this behavior.
|
|
16
|
+
* @template {import('./types.js').MongoDocument} T
|
|
17
|
+
* @param {string} name
|
|
18
|
+
* @param {object} [options]
|
|
19
|
+
* @param {IcollectionGetter<T>} [options.collectionGetter]
|
|
20
|
+
* @param {IcollectionGetter<T>} [options.cachableCollectionGetter]
|
|
21
|
+
* @param {string} [options.connectionString]
|
|
22
|
+
*/
|
|
23
|
+
export async function getCollection(name, options = {}) {
|
|
24
|
+
|
|
25
|
+
/** @type {Collection<T> | null} */
|
|
26
|
+
let _collection = null
|
|
27
|
+
|
|
28
|
+
async function getCollection() {
|
|
29
|
+
const { connectionString, cachableCollectionGetter, collectionGetter } = options
|
|
30
|
+
|
|
31
|
+
// Connection getter from options
|
|
32
|
+
if (collectionGetter != null) return await collectionGetter()
|
|
33
|
+
|
|
34
|
+
// Checking cache
|
|
35
|
+
if (_collection) return _collection
|
|
36
|
+
|
|
37
|
+
// Cache function from options
|
|
38
|
+
if (cachableCollectionGetter != null) {
|
|
39
|
+
_collection = await cachableCollectionGetter()
|
|
40
|
+
return _collection
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Default connection getter
|
|
44
|
+
const client = await getMongoClient({ connectionString })
|
|
45
|
+
let db = client.db()
|
|
46
|
+
/** @type {Collection<T>} */
|
|
47
|
+
_collection = db.collection(name)
|
|
48
|
+
return _collection
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
/**
|
|
53
|
+
* @param {import('mongodb').Filter<T>} query
|
|
54
|
+
*/
|
|
55
|
+
count: async (query = {}) => await count({ query, getCollection }),
|
|
56
|
+
/**
|
|
57
|
+
* @template {import('./types.js').Projection<T>} K
|
|
58
|
+
* @param {import('mongodb').Filter<T>} query
|
|
59
|
+
* @param {import('./types.js').FindOptions<T, K>} options
|
|
60
|
+
*/
|
|
61
|
+
find: async (query, options = {}) => await find({ query, options, getCollection }),
|
|
62
|
+
/**
|
|
63
|
+
* @template {import('./types.js').Projection<T> | undefined} K
|
|
64
|
+
* @param {import('mongodb').Filter<T>} query
|
|
65
|
+
* @param {import('./types.js').FindOptions<T,K>} options
|
|
66
|
+
*/
|
|
67
|
+
findOne: async (query, options = {}) => {
|
|
68
|
+
return await findOne({ query, options, getCollection })
|
|
69
|
+
},
|
|
70
|
+
/**
|
|
71
|
+
* @param {import('./types.js').Optional<T, 'id'>} doc
|
|
72
|
+
*/
|
|
73
|
+
insertOne: async (doc) => await insertOne({ doc, getCollection }),
|
|
74
|
+
/**
|
|
75
|
+
* @param {import('mongodb').Filter<T>} query
|
|
76
|
+
*/
|
|
77
|
+
deleteOne: async (query) => await deleteOne({ query, getCollection }),
|
|
78
|
+
/**
|
|
79
|
+
* Deletes the first document to match the query.
|
|
80
|
+
* This method will throw an error if no documents were deleted.
|
|
81
|
+
* @param {import('mongodb').Filter<T>} query
|
|
82
|
+
* @throw {@link OperationFail} If not document was deleted
|
|
83
|
+
*/
|
|
84
|
+
deleteOneOrThrow: async query => await deleteOneOrThrow({ query, getCollection }),
|
|
85
|
+
/**
|
|
86
|
+
*
|
|
87
|
+
* @param {import('mongodb').Filter<T>} query
|
|
88
|
+
*/
|
|
89
|
+
deleteMany: async (query) => await deleteMany({ query, getCollection }),
|
|
90
|
+
/**
|
|
91
|
+
* @param {import('mongodb').Filter<T>} query
|
|
92
|
+
* @param {import('mongodb').UpdateFilter<T>} update
|
|
93
|
+
*/
|
|
94
|
+
updateOne: async (query, update) => await updateOne({ query, update, getCollection }),
|
|
95
|
+
/**
|
|
96
|
+
* @param {import('mongodb').Filter<T>} query
|
|
97
|
+
* @param {import('mongodb').UpdateFilter<T>} update
|
|
98
|
+
*/
|
|
99
|
+
updateMany: async (query, update) => await updateMany({ query, update, getCollection })
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @template {import('./types.js').MongoDocument} T
|
|
105
|
+
* @callback IcollectionGetter
|
|
106
|
+
* @returns {Promise<Collection<T>>}
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @template {import('./types.js').MongoDocument} T
|
|
111
|
+
* @typedef {Awaited<ReturnType<typeof getCollection<T>>>} ICollection
|
|
112
|
+
*/
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { MongoClient } from 'mongodb'
|
|
2
|
+
import { OperationFail } from './data/errors/operation_fail.mjs'
|
|
3
|
+
|
|
4
|
+
/** @type {MongoClient | null} */
|
|
5
|
+
let client = null
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* @param {object} params
|
|
10
|
+
* @param {string} [params.connectionString]
|
|
11
|
+
*/
|
|
12
|
+
export async function getMongoClient({
|
|
13
|
+
connectionString,
|
|
14
|
+
} = {}) {
|
|
15
|
+
if (!client) {
|
|
16
|
+
function getCS() {
|
|
17
|
+
if (typeof connectionString == 'string') return connectionString
|
|
18
|
+
const DATABASE_URL = process.env.DATABASE_URL
|
|
19
|
+
if (DATABASE_URL == null) {
|
|
20
|
+
throw new OperationFail('DATABASE_URL not configured')
|
|
21
|
+
}
|
|
22
|
+
return DATABASE_URL
|
|
23
|
+
}
|
|
24
|
+
const cs = getCS()
|
|
25
|
+
client = await MongoClient.connect(cs)
|
|
26
|
+
}
|
|
27
|
+
return client
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function closeMongoClient() {
|
|
31
|
+
await client?.close()
|
|
32
|
+
client = null
|
|
33
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { FindOptions as FO } from 'mongodb'
|
|
2
|
+
|
|
3
|
+
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
|
|
4
|
+
|
|
5
|
+
export type MongoDocument = {
|
|
6
|
+
id: string
|
|
7
|
+
[key: string]: any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type NonNullProjection<T extends MongoDocument> = Partial<Record<keyof T, 1 | 0>>
|
|
11
|
+
|
|
12
|
+
export type Projection<T extends MongoDocument> = NonNullProjection<T> | undefined
|
|
13
|
+
|
|
14
|
+
type GivenProjectionReturnType<T extends MongoDocument, P extends NonNullProjection<T>> =
|
|
15
|
+
P extends Partial<Record<keyof T, 0>>
|
|
16
|
+
? {
|
|
17
|
+
[K in keyof T as P[K] extends 0 ? never : K]: T[K]
|
|
18
|
+
}
|
|
19
|
+
: {
|
|
20
|
+
[K in keyof T as P[K] extends 1 ? K : never]: T[K]
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ProjectionReturnType<T extends MongoDocument, P extends Projection<T>> =
|
|
24
|
+
P extends NonNullProjection<T> ? GivenProjectionReturnType<T, P> : T
|
|
25
|
+
|
|
26
|
+
// export type ProjectionReturnType<T extends MongoDocument, P extends Projection<T>> =
|
|
27
|
+
// P extends Partial<Record<keyof T, 0>>
|
|
28
|
+
// ? {
|
|
29
|
+
// [K in keyof T as P[K] extends 0 ? never : K]: T[K]
|
|
30
|
+
// }
|
|
31
|
+
// : (P extends Partial<Record<keyof T, 0 | 1>> ? {
|
|
32
|
+
// [K in keyof T as P[K] extends 1 ? K : (
|
|
33
|
+
// K extends 'id' ? (
|
|
34
|
+
// P[K] extends 0 ? never : K
|
|
35
|
+
// ) :
|
|
36
|
+
// never
|
|
37
|
+
// )]: T[K]
|
|
38
|
+
// } : T)
|
|
39
|
+
|
|
40
|
+
export type FindOptions<T extends MongoDocument, K extends Projection<T>> = Omit<FO<T>, 'projection'> & {
|
|
41
|
+
projection?: K
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Collection } from 'mongodb'
|
|
2
|
+
import { OperationFail } from '../../../data/errors/operation_fail.mjs'
|
|
3
|
+
import { deleteOne } from '../delete_one.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @template {import('../../../types.js').MongoDocument} T
|
|
7
|
+
* @param {object} param
|
|
8
|
+
* @param {import('mongodb').Filter<T>} param.query
|
|
9
|
+
* @param {() => Promise<Collection<T>>} param.getCollection
|
|
10
|
+
*/
|
|
11
|
+
export async function deleteOneOrThrow({
|
|
12
|
+
query, getCollection,
|
|
13
|
+
}) {
|
|
14
|
+
const deleted = await deleteOne({ query, getCollection })
|
|
15
|
+
if (!deleted) {
|
|
16
|
+
throw new OperationFail('Delete one did not delete any documents')
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './delete_one_or_throw.mjs'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Collection } from 'mongodb'
|
|
2
|
+
import { renameToMongoId, stringsIntoId } from '../transformers/index.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @template {import('../../types.js').MongoDocument} T
|
|
6
|
+
* @param {object} params
|
|
7
|
+
* @param {() => Promise<Collection<T>>} params.getCollection
|
|
8
|
+
* @param {import('mongodb').Filter<T>} [params.query]
|
|
9
|
+
*/
|
|
10
|
+
export async function count({ getCollection, query }) {
|
|
11
|
+
renameToMongoId(query)
|
|
12
|
+
stringsIntoId(query)
|
|
13
|
+
|
|
14
|
+
const col = await getCollection()
|
|
15
|
+
const count = await col.countDocuments(query)
|
|
16
|
+
return count
|
|
17
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { fakerPT_BR } from '@faker-js/faker'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { after, before, describe, it } from 'node:test'
|
|
4
|
+
import { mockGetCollection } from '../../../test/mock_get_collection.mjs'
|
|
5
|
+
import { closeMongoClient } from '../../mongo_client.mjs'
|
|
6
|
+
import { generateNewId } from '../generate_new_id.mjs'
|
|
7
|
+
import { count } from './count.mjs'
|
|
8
|
+
import { insertMany } from './insert_many.mjs'
|
|
9
|
+
|
|
10
|
+
describe('count', () => {
|
|
11
|
+
const tag = generateNewId()
|
|
12
|
+
before(async () => {
|
|
13
|
+
/** @type {import('../../../test/mock_get_collection.mjs').ItestCollection[]} */
|
|
14
|
+
const items = []
|
|
15
|
+
for (let i = 0; i < 50; i++) {
|
|
16
|
+
items.push({
|
|
17
|
+
name: fakerPT_BR.person.fullName(),
|
|
18
|
+
tag,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
await insertMany({
|
|
22
|
+
docs: items,
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
getCollection: mockGetCollection,
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
after(async () => {
|
|
29
|
+
await closeMongoClient()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should count all inserted documents', async () => {
|
|
33
|
+
const counter = await count({
|
|
34
|
+
query: { tag },
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
getCollection: mockGetCollection,
|
|
37
|
+
})
|
|
38
|
+
assert.equal(counter, 50)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should return 0 if the query does not match any documents', async () => {
|
|
42
|
+
const counter = await count({
|
|
43
|
+
query: { tag: generateNewId() },
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
getCollection: mockGetCollection,
|
|
46
|
+
})
|
|
47
|
+
assert.equal(counter, 0)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Collection } from 'mongodb'
|
|
2
|
+
import { renameToMongoId, stringsIntoId } from '../transformers/index.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @template {import('../../types.js').MongoDocument} T
|
|
6
|
+
* @param {object} parameter
|
|
7
|
+
* @param {import('mongodb').Filter<T>} parameter.query
|
|
8
|
+
* @param {() => Promise<Collection<T>>} parameter.getCollection
|
|
9
|
+
*/
|
|
10
|
+
export async function deleteMany({ query, getCollection }) {
|
|
11
|
+
renameToMongoId(query)
|
|
12
|
+
stringsIntoId(query)
|
|
13
|
+
const col = await getCollection()
|
|
14
|
+
const r = await col.deleteMany(query)
|
|
15
|
+
return r.deletedCount > 0
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Collection } from 'mongodb'
|
|
2
|
+
import { renameToMongoId, stringsIntoId } from '../transformers/index.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @template {import('../../types.js').MongoDocument} T
|
|
6
|
+
* @param {object} parameter
|
|
7
|
+
* @param {import('mongodb').Filter<T>} parameter.query
|
|
8
|
+
* @param {() => Promise<Collection<T>>} parameter.getCollection
|
|
9
|
+
*/
|
|
10
|
+
export async function deleteOne({ query, getCollection }) {
|
|
11
|
+
renameToMongoId(query)
|
|
12
|
+
stringsIntoId(query)
|
|
13
|
+
const col = await getCollection()
|
|
14
|
+
const r = await col.deleteOne(query)
|
|
15
|
+
return r.deletedCount > 0
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { manageMockRegistry } from '../../../test/manage_mock_registry.mjs'
|
|
4
|
+
import { mockGetCollection } from '../../../test/mock_get_collection.mjs'
|
|
5
|
+
import { count } from './count.mjs'
|
|
6
|
+
import { deleteOne } from './delete_one.mjs'
|
|
7
|
+
|
|
8
|
+
describe('deleteOne', () => {
|
|
9
|
+
const { id } = manageMockRegistry({
|
|
10
|
+
name: 'Made in heaven',
|
|
11
|
+
}, {
|
|
12
|
+
deleteAtEnd: false,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should succeed on deleting by using registered id', async () => {
|
|
16
|
+
const deleted = await deleteOne({
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
getCollection: mockGetCollection,
|
|
19
|
+
query: { id }
|
|
20
|
+
})
|
|
21
|
+
assert(deleted)
|
|
22
|
+
|
|
23
|
+
const docCount = await count({
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
getCollection: mockGetCollection,
|
|
26
|
+
query: { id }
|
|
27
|
+
})
|
|
28
|
+
assert(docCount == 0)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Collection } from 'mongodb'
|
|
2
|
+
import {
|
|
3
|
+
idsIntoString,
|
|
4
|
+
renameFindOptions,
|
|
5
|
+
renameToDevId,
|
|
6
|
+
renameToMongoId,
|
|
7
|
+
stringsIntoId
|
|
8
|
+
} from '../transformers/index.mjs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @template {import('../../types.js').MongoDocument} T
|
|
12
|
+
* @template {import('../../types.js').Projection<T>} K
|
|
13
|
+
* @param {object} parameter
|
|
14
|
+
* @param {() => Promise<Collection<T>>} parameter.getCollection
|
|
15
|
+
* @param {import('mongodb').Filter<T>} parameter.query
|
|
16
|
+
* @param {import('../../types.js').FindOptions<T, K>} [parameter.options]
|
|
17
|
+
* @returns {Promise<T[]>}
|
|
18
|
+
*/
|
|
19
|
+
export async function find({ getCollection, query, options }) {
|
|
20
|
+
renameToMongoId(query)
|
|
21
|
+
renameFindOptions(options)
|
|
22
|
+
|
|
23
|
+
stringsIntoId(query)
|
|
24
|
+
|
|
25
|
+
const col = await getCollection()
|
|
26
|
+
|
|
27
|
+
const docs = await col.find(query, options).toArray()
|
|
28
|
+
const fixedDocs = docs.map((doc) => {
|
|
29
|
+
idsIntoString(doc)
|
|
30
|
+
const transformedDoc = renameToDevId(doc)
|
|
31
|
+
return transformedDoc
|
|
32
|
+
})
|
|
33
|
+
return fixedDocs
|
|
34
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Collection } from 'mongodb'
|
|
2
|
+
import { find } from './find.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @template {import('../../types.js').MongoDocument} T
|
|
6
|
+
* @template {import('../../types.js').Projection<T> | undefined} K
|
|
7
|
+
* @param {object} param
|
|
8
|
+
* @param {import('mongodb').Filter<T>} param.query
|
|
9
|
+
* @param {import('../../types.js').FindOptions<T, K>} [param.options]
|
|
10
|
+
* @param {() => Promise<Collection<T>>} param.getCollection
|
|
11
|
+
*/
|
|
12
|
+
export async function findOne({ query, options, getCollection }) {
|
|
13
|
+
const docs = await find({
|
|
14
|
+
query,
|
|
15
|
+
options: {
|
|
16
|
+
limit: 1,
|
|
17
|
+
...options,
|
|
18
|
+
},
|
|
19
|
+
getCollection,
|
|
20
|
+
})
|
|
21
|
+
const doc = docs[0]
|
|
22
|
+
if (doc == null) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
return doc
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { manageMockRegistry } from '../../../test/manage_mock_registry.mjs'
|
|
4
|
+
import { mockGetCollection } from '../../../test/mock_get_collection.mjs'
|
|
5
|
+
import { findOne } from './find_one.mjs'
|
|
6
|
+
|
|
7
|
+
describe('findOne', () => {
|
|
8
|
+
|
|
9
|
+
const { id, name } = manageMockRegistry({
|
|
10
|
+
name: 'Anna'
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
async function _findOne(query) {
|
|
14
|
+
const r = await findOne({
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
getCollection: mockGetCollection,
|
|
17
|
+
query: query,
|
|
18
|
+
})
|
|
19
|
+
return r
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
it('should succeed at finding a saved document', async () => {
|
|
23
|
+
const savedDoc = await _findOne({ id })
|
|
24
|
+
assert(savedDoc)
|
|
25
|
+
assert.equal(savedDoc.id, id)
|
|
26
|
+
assert.equal(savedDoc.name, name)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './additional/index.mjs'
|
|
2
|
+
export * from './count.mjs'
|
|
3
|
+
export * from './delete_many.mjs'
|
|
4
|
+
export * from './delete_one.mjs'
|
|
5
|
+
export * from './find.mjs'
|
|
6
|
+
export * from './find_one.mjs'
|
|
7
|
+
export * from './insert_one.mjs'
|
|
8
|
+
export * from './update_many.mjs'
|
|
9
|
+
export * from './update_one.mjs'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Collection, ObjectId } from 'mongodb'
|
|
2
|
+
import { renameToMongoId, stringsIntoId } from '../transformers/index.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @template {import('../../types.js').MongoDocument} T
|
|
6
|
+
* @param {object} param
|
|
7
|
+
* @param {import('../../types.js').Optional<T, 'id'>[]} param.docs
|
|
8
|
+
* @param {() => Promise<Collection<T>>} param.getCollection
|
|
9
|
+
*/
|
|
10
|
+
export async function insertMany({ docs, getCollection }) {
|
|
11
|
+
if (docs.length == 0) return []
|
|
12
|
+
for (const doc of docs) {
|
|
13
|
+
renameToMongoId(doc)
|
|
14
|
+
stringsIntoId(doc)
|
|
15
|
+
}
|
|
16
|
+
const col = await getCollection()
|
|
17
|
+
const result = await col.insertMany(
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
docs
|
|
20
|
+
)
|
|
21
|
+
let { insertedIds } = result
|
|
22
|
+
const indexes = Object.keys(insertedIds)
|
|
23
|
+
for (const index of indexes) {
|
|
24
|
+
const id = insertedIds[index]
|
|
25
|
+
if (id instanceof ObjectId) {
|
|
26
|
+
docs[index].id = id.toHexString()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return docs
|
|
30
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { fakerPT_BR } from '@faker-js/faker'
|
|
2
|
+
import { ObjectId } from 'mongodb'
|
|
3
|
+
import assert from 'node:assert'
|
|
4
|
+
import { after, describe, it } from 'node:test'
|
|
5
|
+
import { mockGetCollection } from '../../../test/mock_get_collection.mjs'
|
|
6
|
+
import { closeMongoClient } from '../../mongo_client.mjs'
|
|
7
|
+
import { generateNewId } from '../generate_new_id.mjs'
|
|
8
|
+
import { insertMany } from './insert_many.mjs'
|
|
9
|
+
|
|
10
|
+
describe('insertMany', () => {
|
|
11
|
+
|
|
12
|
+
after(async () => await closeMongoClient())
|
|
13
|
+
|
|
14
|
+
it('should succeed at inserting multiple data', async () => {
|
|
15
|
+
const tag = generateNewId()
|
|
16
|
+
/** @type {import('../../../test/mock_get_collection.mjs').ItestCollection[]} */
|
|
17
|
+
const items = []
|
|
18
|
+
for (let i = 0; i < 50; i++) {
|
|
19
|
+
items.push({
|
|
20
|
+
name: fakerPT_BR.person.fullName(),
|
|
21
|
+
createdAt: fakerPT_BR.date.birthdate(),
|
|
22
|
+
tag: tag,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
const r = await insertMany({
|
|
26
|
+
docs: items,
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
getCollection: mockGetCollection,
|
|
29
|
+
})
|
|
30
|
+
const ids = r.map(x => x.id)
|
|
31
|
+
const allHaveId = ids.every(x => x && ObjectId.isValid(x))
|
|
32
|
+
assert(allHaveId)
|
|
33
|
+
|
|
34
|
+
const col = await mockGetCollection()
|
|
35
|
+
|
|
36
|
+
const docs = await col.find({
|
|
37
|
+
tag: ObjectId.createFromHexString(tag),
|
|
38
|
+
}).toArray()
|
|
39
|
+
assert(docs.length == items.length)
|
|
40
|
+
|
|
41
|
+
const tagFound = docs[0].tag
|
|
42
|
+
assert(tagFound instanceof ObjectId)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Collection, ObjectId } from 'mongodb'
|
|
2
|
+
import { renameToMongoId, stringsIntoId } from '../transformers/index.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @template {import('../../types.js').MongoDocument} T
|
|
6
|
+
* @param {object} param
|
|
7
|
+
* @param {import('../../types.js').Optional<T, 'id'>} param.doc
|
|
8
|
+
* @param {() => Promise<Collection<T>>} param.getCollection
|
|
9
|
+
*/
|
|
10
|
+
export async function insertOne({ doc, getCollection }) {
|
|
11
|
+
renameToMongoId(doc)
|
|
12
|
+
stringsIntoId(doc)
|
|
13
|
+
const col = await getCollection()
|
|
14
|
+
const result = await col.insertOne(
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
doc
|
|
17
|
+
)
|
|
18
|
+
let id = result.insertedId
|
|
19
|
+
if (id instanceof ObjectId) {
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
id = id.toHexString()
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
id,
|
|
25
|
+
...doc,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ObjectId } from 'mongodb'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { after, describe, it } from 'node:test'
|
|
4
|
+
import { mockGetCollection } from '../../../test/mock_get_collection.mjs'
|
|
5
|
+
import { closeMongoClient } from '../../mongo_client.mjs'
|
|
6
|
+
import { generateNewId } from '../generate_new_id.mjs'
|
|
7
|
+
import { insertOne } from './insert_one.mjs'
|
|
8
|
+
|
|
9
|
+
describe('insertOne', () => {
|
|
10
|
+
|
|
11
|
+
after(async () => await closeMongoClient())
|
|
12
|
+
|
|
13
|
+
async function insert(doc) {
|
|
14
|
+
await insertOne({
|
|
15
|
+
doc,
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
getCollection: mockGetCollection,
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
it('should accept a string ids', async () => {
|
|
22
|
+
const id = generateNewId()
|
|
23
|
+
const userId = generateNewId()
|
|
24
|
+
const postId = generateNewId()
|
|
25
|
+
const addressId = generateNewId()
|
|
26
|
+
const name = 'Joseph'
|
|
27
|
+
const doc = {
|
|
28
|
+
id,
|
|
29
|
+
name,
|
|
30
|
+
userId,
|
|
31
|
+
posts: [{ postId }],
|
|
32
|
+
address: { addressId },
|
|
33
|
+
}
|
|
34
|
+
await insert(doc)
|
|
35
|
+
const col = await mockGetCollection()
|
|
36
|
+
const savedDoc = await col.findOne({ _id: new ObjectId(id) })
|
|
37
|
+
assert(savedDoc)
|
|
38
|
+
|
|
39
|
+
// Checking if normal properties are correctly set
|
|
40
|
+
assert(savedDoc.name == name)
|
|
41
|
+
|
|
42
|
+
// Checking _id is indeed an ObjectId instance
|
|
43
|
+
const _id = savedDoc._id
|
|
44
|
+
assert(_id instanceof ObjectId)
|
|
45
|
+
assert(_id.toHexString() == id)
|
|
46
|
+
|
|
47
|
+
// Checking if ids inside arrays are correctly mapped
|
|
48
|
+
const savedPostId = savedDoc.posts?.[0].postId
|
|
49
|
+
assert(savedPostId instanceof ObjectId)
|
|
50
|
+
assert(savedPostId.toHexString() == postId)
|
|
51
|
+
|
|
52
|
+
// Checking if ids inside objects are correctly mapped
|
|
53
|
+
const savedAddressId = savedDoc.address.addressId
|
|
54
|
+
assert(savedAddressId instanceof ObjectId)
|
|
55
|
+
assert(savedAddressId.toHexString() == addressId)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should accept ObjectId as usual', async () => {
|
|
59
|
+
const id = new ObjectId()
|
|
60
|
+
const name = 'Jotoro'
|
|
61
|
+
const doc = { id, name }
|
|
62
|
+
await insert(doc)
|
|
63
|
+
|
|
64
|
+
const col = await mockGetCollection()
|
|
65
|
+
const savedDoc = await col.findOne({ _id: id })
|
|
66
|
+
assert(savedDoc)
|
|
67
|
+
|
|
68
|
+
assert(savedDoc.name == name)
|
|
69
|
+
|
|
70
|
+
const _id = savedDoc._id
|
|
71
|
+
assert(_id instanceof ObjectId)
|
|
72
|
+
assert(_id.toHexString() == id.toHexString())
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Collection } from 'mongodb'
|
|
2
|
+
import { renameToMongoId, stringsIntoId } from '../transformers/index.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @template {import('../../types.js').MongoDocument} T
|
|
6
|
+
* @param {object} param
|
|
7
|
+
* @param {import('mongodb').Filter<T>} param.query
|
|
8
|
+
* @param {import('mongodb').UpdateFilter<T>} param.update
|
|
9
|
+
* @param {() => Promise<Collection<T>>} param.getCollection
|
|
10
|
+
*/
|
|
11
|
+
export async function updateMany({ query, update, getCollection }) {
|
|
12
|
+
renameToMongoId(query)
|
|
13
|
+
stringsIntoId(query)
|
|
14
|
+
stringsIntoId(update)
|
|
15
|
+
const col = await getCollection()
|
|
16
|
+
const r = await col.updateMany(query, update)
|
|
17
|
+
return r.matchedCount > 0
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Collection } from 'mongodb'
|
|
2
|
+
import { renameToMongoId, stringsIntoId } from '../transformers/index.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @template {import('../../types.js').MongoDocument} T
|
|
6
|
+
* @param {object} param
|
|
7
|
+
* @param {import('mongodb').Filter<T>} param.query
|
|
8
|
+
* @param {import('mongodb').UpdateFilter<T>} param.update
|
|
9
|
+
* @param {() => Promise<Collection<T>>} param.getCollection
|
|
10
|
+
*/
|
|
11
|
+
export async function updateOne({ query, update, getCollection }) {
|
|
12
|
+
renameToMongoId(query)
|
|
13
|
+
stringsIntoId(query)
|
|
14
|
+
stringsIntoId(update)
|
|
15
|
+
const col = await getCollection()
|
|
16
|
+
const r = await col.updateOne(query, update)
|
|
17
|
+
return r.matchedCount > 0
|
|
18
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ObjectId } from 'mongodb'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { after, describe, it } from 'node:test'
|
|
4
|
+
import { manageMockRegistry } from '../../../test/manage_mock_registry.mjs'
|
|
5
|
+
import { mockGetCollection } from '../../../test/mock_get_collection.mjs'
|
|
6
|
+
import { closeMongoClient } from '../../mongo_client.mjs'
|
|
7
|
+
import { generateNewId } from '../generate_new_id.mjs'
|
|
8
|
+
import { updateOne } from './update_one.mjs'
|
|
9
|
+
|
|
10
|
+
describe('updateOne', () => {
|
|
11
|
+
|
|
12
|
+
const { id } = manageMockRegistry({
|
|
13
|
+
name: 'Jolyne Cujoh',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
after(() => {
|
|
17
|
+
closeMongoClient()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should update document using string id', async () => {
|
|
21
|
+
const updatedName = 'Jolyne'
|
|
22
|
+
const updated = await updateOne({
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
getCollection: mockGetCollection,
|
|
25
|
+
query: {
|
|
26
|
+
id,
|
|
27
|
+
},
|
|
28
|
+
update: {
|
|
29
|
+
$set: { name: updatedName }
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
assert(updated)
|
|
33
|
+
const col = await mockGetCollection()
|
|
34
|
+
const doc = await col.findOne({
|
|
35
|
+
_id: new ObjectId(id)
|
|
36
|
+
})
|
|
37
|
+
assert(doc)
|
|
38
|
+
assert(doc?.name == updatedName)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should not update any document using non registered id', async () => {
|
|
42
|
+
const updated = await updateOne({
|
|
43
|
+
// @ts-ignore
|
|
44
|
+
getCollection: mockGetCollection,
|
|
45
|
+
query: {
|
|
46
|
+
id: generateNewId(),
|
|
47
|
+
},
|
|
48
|
+
update: {
|
|
49
|
+
$set: {
|
|
50
|
+
createdAt: new Date()
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
assert(!updated)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ```
|
|
3
|
+
* "_id" -> "id"
|
|
4
|
+
* ```
|
|
5
|
+
*/
|
|
6
|
+
export function renameToDevId(obj) {
|
|
7
|
+
if (!obj) return obj
|
|
8
|
+
let { _id, ...rest } = obj
|
|
9
|
+
if (!_id) return obj
|
|
10
|
+
delete obj['_id']
|
|
11
|
+
// if (_id instanceof ObjectId) {
|
|
12
|
+
// _id = _id.toHexString()
|
|
13
|
+
// }
|
|
14
|
+
obj.id = _id
|
|
15
|
+
return obj
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ```
|
|
3
|
+
* "id" -> "_id"
|
|
4
|
+
* ```
|
|
5
|
+
*/
|
|
6
|
+
export function renameToMongoId(obj) {
|
|
7
|
+
if (!obj) return obj
|
|
8
|
+
let { id, ...rest } = obj
|
|
9
|
+
if (!id) return obj
|
|
10
|
+
delete obj['id']
|
|
11
|
+
// if (_id instanceof ObjectId) {
|
|
12
|
+
// _id = _id.toHexString()
|
|
13
|
+
// }
|
|
14
|
+
obj._id = id
|
|
15
|
+
return obj
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ObjectId } from 'mongodb'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert the parameter from `ObjectId` into string whenever possible.
|
|
5
|
+
*
|
|
6
|
+
* If an object or array is given, this function will be applied recursively.
|
|
7
|
+
* @param {*} obj
|
|
8
|
+
*/
|
|
9
|
+
export function idsIntoString(obj) {
|
|
10
|
+
if (obj == null) return obj
|
|
11
|
+
if (typeof obj != 'object') return obj
|
|
12
|
+
if (obj instanceof ObjectId) return obj.toHexString()
|
|
13
|
+
if (Array.isArray(obj)) {
|
|
14
|
+
for (let i = 0; i < obj.length; i++) {
|
|
15
|
+
const item = obj[i]
|
|
16
|
+
obj[i] = idsIntoString(item)
|
|
17
|
+
}
|
|
18
|
+
return obj
|
|
19
|
+
}
|
|
20
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
21
|
+
if (value instanceof ObjectId) {
|
|
22
|
+
obj[key] = value.toHexString()
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
if (value != null && typeof value == 'object') {
|
|
26
|
+
obj[key] = idsIntoString(value)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return obj
|
|
30
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ObjectId } from 'mongodb'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts the input value into an `ObjectId`, doing a recursive internal search if possible.
|
|
5
|
+
*
|
|
6
|
+
* This function will do a recursive operation if the parameter is any of the following:
|
|
7
|
+
* - An object;
|
|
8
|
+
* - An array;
|
|
9
|
+
*
|
|
10
|
+
* If the parameter is a string, then it is converted to `ObjectId` if possible.
|
|
11
|
+
* @param {*} obj
|
|
12
|
+
*/
|
|
13
|
+
export function stringsIntoId(obj) {
|
|
14
|
+
if (obj == null) return obj
|
|
15
|
+
if (isObjectIdString(obj)) return new ObjectId(obj)
|
|
16
|
+
if (typeof obj != 'object') return obj
|
|
17
|
+
if (Array.isArray(obj)) {
|
|
18
|
+
for (let i = 0; i < obj.length; i++) {
|
|
19
|
+
const item = obj[i]
|
|
20
|
+
obj[i] = stringsIntoId(item)
|
|
21
|
+
}
|
|
22
|
+
return obj
|
|
23
|
+
}
|
|
24
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
25
|
+
if (isObjectIdString(value)) {
|
|
26
|
+
obj[key] = ObjectId.createFromHexString(value)
|
|
27
|
+
}
|
|
28
|
+
if (value != null && typeof value == 'object') {
|
|
29
|
+
obj[key] = stringsIntoId(value)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return obj
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
*
|
|
37
|
+
* @param {*} value
|
|
38
|
+
* @returns {value is string}
|
|
39
|
+
*/
|
|
40
|
+
function isObjectIdString(value) {
|
|
41
|
+
return typeof value == 'string' && ObjectId.isValid(value)
|
|
42
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { fakerPT_BR } from '@faker-js/faker'
|
|
2
|
+
import { ObjectId } from 'mongodb'
|
|
3
|
+
import { after, before } from 'node:test'
|
|
4
|
+
import { closeMongoClient } from '../src/mongo_client.mjs'
|
|
5
|
+
import { generateNewId } from '../src/usecases/generate_new_id.mjs'
|
|
6
|
+
import { deleteOne } from '../src/usecases/operation/delete_one.mjs'
|
|
7
|
+
import { insertOne } from '../src/usecases/operation/insert_one.mjs'
|
|
8
|
+
import { mockGetCollection } from './mock_get_collection.mjs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
*
|
|
12
|
+
* @param {import('./mock_get_collection.mjs').ItestCollection} param
|
|
13
|
+
* @param {object} options
|
|
14
|
+
* @param {boolean} [options.deleteAtEnd]
|
|
15
|
+
*/
|
|
16
|
+
export function manageMockRegistry({ tag, ...rest } = {}, {
|
|
17
|
+
deleteAtEnd = true
|
|
18
|
+
} = {}) {
|
|
19
|
+
|
|
20
|
+
const id = generateNewId()
|
|
21
|
+
const name = rest.name ?? fakerPT_BR.person.fullName()
|
|
22
|
+
|
|
23
|
+
before(async () => {
|
|
24
|
+
await insertOne({
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
getCollection: mockGetCollection,
|
|
27
|
+
doc: {
|
|
28
|
+
id,
|
|
29
|
+
createdAt: new Date(),
|
|
30
|
+
tag,
|
|
31
|
+
name: name,
|
|
32
|
+
...rest,
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
after(async () => {
|
|
38
|
+
if (deleteAtEnd) {
|
|
39
|
+
await deleteOne({
|
|
40
|
+
query: {
|
|
41
|
+
_id: ObjectId.createFromHexString(id)
|
|
42
|
+
},
|
|
43
|
+
// @ts-ignore
|
|
44
|
+
getCollection: mockGetCollection,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
await closeMongoClient()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id, name
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Collection, ObjectId } from 'mongodb'
|
|
2
|
+
import { getMongoClient } from '../src/mongo_client.mjs'
|
|
3
|
+
|
|
4
|
+
export async function mockGetCollection(collectionName = 'test') {
|
|
5
|
+
const client = await getMongoClient()
|
|
6
|
+
const db = client.db()
|
|
7
|
+
/** @type {Collection<ItestCollection>} */
|
|
8
|
+
const collection = db.collection(collectionName)
|
|
9
|
+
return collection
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} ItestCollection
|
|
14
|
+
* @property {ObjectId} [_id]
|
|
15
|
+
* @property {string} [id]
|
|
16
|
+
* @property {string} [name]
|
|
17
|
+
* @property {string | ObjectId} [tag]
|
|
18
|
+
* @property {Date} [createdAt]
|
|
19
|
+
* @property {object[]} [posts]
|
|
20
|
+
* @property {object} [address]
|
|
21
|
+
*/
|