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.
Files changed (38) hide show
  1. package/README.md +136 -0
  2. package/bash/publish.sh +3 -0
  3. package/index.mjs +1 -0
  4. package/jsconfig.json +10 -0
  5. package/package.json +28 -0
  6. package/src/collection.mjs +112 -0
  7. package/src/data/errors/operation_fail.mjs +10 -0
  8. package/src/mongo_client.mjs +33 -0
  9. package/src/types.ts +42 -0
  10. package/src/usecases/generate_new_id.mjs +5 -0
  11. package/src/usecases/operation/additional/delete_one_or_throw.mjs +18 -0
  12. package/src/usecases/operation/additional/index.mjs +1 -0
  13. package/src/usecases/operation/count.mjs +17 -0
  14. package/src/usecases/operation/count.test.mjs +49 -0
  15. package/src/usecases/operation/delete_many.mjs +16 -0
  16. package/src/usecases/operation/delete_one.mjs +16 -0
  17. package/src/usecases/operation/delete_one.test.mjs +30 -0
  18. package/src/usecases/operation/find.mjs +34 -0
  19. package/src/usecases/operation/find_one.mjs +26 -0
  20. package/src/usecases/operation/find_one.test.mjs +28 -0
  21. package/src/usecases/operation/index.mjs +9 -0
  22. package/src/usecases/operation/insert_many.mjs +30 -0
  23. package/src/usecases/operation/insert_many.test.mjs +44 -0
  24. package/src/usecases/operation/insert_one.mjs +27 -0
  25. package/src/usecases/operation/insert_one.test.mjs +74 -0
  26. package/src/usecases/operation/update_many.mjs +18 -0
  27. package/src/usecases/operation/update_one.mjs +18 -0
  28. package/src/usecases/operation/update_one.test.mjs +56 -0
  29. package/src/usecases/transformers/id/index.mjs +5 -0
  30. package/src/usecases/transformers/id/rename_find_options.mjs +10 -0
  31. package/src/usecases/transformers/id/rename_to_dev_id.mjs +16 -0
  32. package/src/usecases/transformers/id/rename_to_mongo_id.mjs +16 -0
  33. package/src/usecases/transformers/index.mjs +2 -0
  34. package/src/usecases/transformers/object_id/ids_into_strings.mjs +30 -0
  35. package/src/usecases/transformers/object_id/index.mjs +4 -0
  36. package/src/usecases/transformers/object_id/strings_into_id.mjs +42 -0
  37. package/test/manage_mock_registry.mjs +53 -0
  38. 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
+ ```
@@ -0,0 +1,3 @@
1
+ npm run test || exit -1
2
+
3
+ npm publish
package/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export * from './src/collection.mjs'
package/jsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "checkJs": true,
5
+ "strictNullChecks": true,
6
+ "module": "ES2022",
7
+ "target": "ES2022",
8
+ "moduleResolution": "node"
9
+ }
10
+ }
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,10 @@
1
+ export class OperationFail extends Error {
2
+
3
+ /**
4
+ *
5
+ * @param {string} msg
6
+ */
7
+ constructor(msg) {
8
+ super(msg)
9
+ }
10
+ }
@@ -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,5 @@
1
+ import { ObjectId } from 'mongodb'
2
+
3
+ export function generateNewId() {
4
+ return new ObjectId().toHexString()
5
+ }
@@ -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,5 @@
1
+ export * from './rename_find_options.mjs'
2
+ export * from './rename_to_dev_id.mjs'
3
+ export * from './rename_to_mongo_id.mjs'
4
+
5
+ // Contains use cases related to the id/_id field in an object.
@@ -0,0 +1,10 @@
1
+ import { renameToMongoId } from './index.mjs'
2
+
3
+ /**
4
+ *
5
+ * @param {import('../../../types.js').FindOptions<any ,any>} options
6
+ */
7
+ export function renameFindOptions(options = {}) {
8
+ renameToMongoId(options.projection)
9
+ renameToMongoId(options.sort)
10
+ }
@@ -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,2 @@
1
+ export * from './id/index.mjs'
2
+ export * from './object_id/index.mjs'
@@ -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,4 @@
1
+ export * from './ids_into_strings.mjs'
2
+ export * from './strings_into_id.mjs'
3
+
4
+ // Contains usecases related to the value ObjectId.
@@ -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
+ */