core-services-sdk 1.3.25 → 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
CHANGED
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,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
|
+
}
|