core-services-sdk 1.3.40 → 1.3.42

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-services-sdk",
3
- "version": "1.3.40",
3
+ "version": "1.3.42",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",
@@ -11,7 +11,7 @@ import { ObjectId } from 'mongodb'
11
11
  * @param {'asc'|'desc'} [options.order='asc']
12
12
  * @param {number} [options.limit=10]
13
13
  */
14
- export async function paginate(
14
+ export async function paginateCursor(
15
15
  collection,
16
16
  {
17
17
  limit = 10,
@@ -52,7 +52,7 @@ export async function paginate(
52
52
  collection,
53
53
  cursorField,
54
54
  })
55
- : { hasNext: false, hasPrevious: false }
55
+ : { hasNext: null, hasPrevious: null }
56
56
 
57
57
  const { hasNext, hasPrevious } = paginationEdges
58
58
  return {
@@ -96,3 +96,62 @@ export async function getPaginationEdges({
96
96
  hasPrevious,
97
97
  }
98
98
  }
99
+
100
+ /**
101
+ * Classic page/limit pagination with total count
102
+ *
103
+ * @param {import('mongodb').Collection} collection
104
+ * @param {Object} options
105
+ * @param {Object} [options.filter={}] - MongoDB filter
106
+ * @param {string} [options.cursorField='_id'] - Field to sort by
107
+ * @param {'asc'|'desc'} [options.order='desc'] - Sort order
108
+ * @param {number} [options.limit=10] - Items per page
109
+ * @param {number} [options.page=1] - Page number (1-based)
110
+ * @param {Object} [options.projection] - Projection fields
111
+ */
112
+ export async function paginate(
113
+ collection,
114
+ {
115
+ sortBy = '_id',
116
+ page = 1,
117
+ limit = 10,
118
+ filter = {},
119
+ projection,
120
+ order = 'desc',
121
+ } = {},
122
+ ) {
123
+ // Validation
124
+ if (page < 1) {
125
+ page = 1
126
+ }
127
+ if (limit < 1) {
128
+ limit = 10
129
+ }
130
+
131
+ const sort = { [sortBy]: order === 'asc' ? 1 : -1 }
132
+ const skip = (page - 1) * limit
133
+
134
+ const [results, totalCount] = await Promise.all([
135
+ collection
136
+ .find(filter, { projection })
137
+ .sort(sort)
138
+ .skip(skip)
139
+ .limit(limit)
140
+ .toArray(),
141
+ collection.countDocuments(filter),
142
+ ])
143
+
144
+ const totalPages = Math.ceil(totalCount / limit)
145
+ const hasNext = page < totalPages
146
+ const hasPrevious = page > 1
147
+
148
+ return {
149
+ order,
150
+ hasNext,
151
+ totalCount,
152
+ totalPages,
153
+ hasPrevious,
154
+ list: results,
155
+ currentPage: page,
156
+ }
157
+ }
@@ -0,0 +1,351 @@
1
+ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
2
+
3
+ import { paginateCursor } 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-cursor-test'
9
+
10
+ let db
11
+ let collection
12
+ describe('paginateCursor - 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 paginateCursor(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).toBeNull()
58
+ })
59
+
60
+ it('returns empty when no docs match filter', async () => {
61
+ const result = await paginateCursor(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 paginateCursor(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 paginateCursor(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('paginateCursors with cursor (next page)', async () => {
98
+ await seedDocs(7)
99
+
100
+ // first page
101
+ const firstPage = await paginateCursor(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 paginateCursor(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('paginateCursors 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 paginateCursor(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
+
152
+ // ---------- PAGINATION EDGES (ASC ORDER) ----------
153
+
154
+ it('ASC - first page: has next, no previous', async () => {
155
+ await seedDocs(7)
156
+
157
+ const page = await paginateCursor(collection, {
158
+ limit: 3,
159
+ order: 'asc',
160
+ cursorField: 'createdAt',
161
+ })
162
+
163
+ expect(page.list).toHaveLength(3)
164
+ expect(page.previous).toBeNull()
165
+ expect(page.next).not.toBeNull()
166
+ })
167
+
168
+ it('ASC - middle page: has both next and previous', async () => {
169
+ await seedDocs(9)
170
+
171
+ const firstPage = await paginateCursor(collection, {
172
+ limit: 3,
173
+ order: 'asc',
174
+ cursorField: 'createdAt',
175
+ })
176
+
177
+ const middlePage = await paginateCursor(collection, {
178
+ limit: 3,
179
+ order: 'asc',
180
+ cursorField: 'createdAt',
181
+ cursor: firstPage.next,
182
+ })
183
+
184
+ expect(middlePage.list).toHaveLength(3)
185
+ expect(middlePage.previous).not.toBeNull()
186
+ expect(middlePage.next).not.toBeNull()
187
+ })
188
+
189
+ it('ASC - last page: has previous, no next', async () => {
190
+ await seedDocs(7)
191
+
192
+ const firstPage = await paginateCursor(collection, {
193
+ limit: 3,
194
+ order: 'asc',
195
+ cursorField: 'createdAt',
196
+ })
197
+ const secondPage = await paginateCursor(collection, {
198
+ limit: 3,
199
+ order: 'asc',
200
+ cursorField: 'createdAt',
201
+ cursor: firstPage.next,
202
+ })
203
+ const lastPage = await paginateCursor(collection, {
204
+ limit: 3,
205
+ order: 'asc',
206
+ cursorField: 'createdAt',
207
+ cursor: secondPage.next,
208
+ })
209
+
210
+ expect(lastPage.list.length).toBeGreaterThan(0)
211
+ expect(lastPage.previous).not.toBeNull()
212
+ expect(lastPage.next).toBeNull()
213
+ })
214
+
215
+ it('ASC - single page: no next, no previous', async () => {
216
+ await seedDocs(2)
217
+
218
+ const singlePage = await paginateCursor(collection, {
219
+ limit: 5,
220
+ order: 'asc',
221
+ cursorField: 'createdAt',
222
+ })
223
+
224
+ expect(singlePage.list).toHaveLength(2)
225
+ expect(singlePage.previous).toBeNull()
226
+ expect(singlePage.next).toBeNull()
227
+ })
228
+
229
+ // ---------- PAGINATION EDGES (DESC ORDER) ----------
230
+
231
+ it('DESC - first page: has next, no previous', async () => {
232
+ await seedDocs(7)
233
+
234
+ const page = await paginateCursor(collection, {
235
+ limit: 3,
236
+ order: 'desc',
237
+ cursorField: 'createdAt',
238
+ })
239
+
240
+ expect(page.list).toHaveLength(3)
241
+ expect(page.previous).toBeNull()
242
+ expect(page.next).not.toBeNull()
243
+ })
244
+
245
+ it('DESC - middle page: has both next and previous', async () => {
246
+ await seedDocs(9)
247
+
248
+ const firstPage = await paginateCursor(collection, {
249
+ limit: 3,
250
+ order: 'desc',
251
+ cursorField: 'createdAt',
252
+ })
253
+
254
+ const middlePage = await paginateCursor(collection, {
255
+ limit: 3,
256
+ order: 'desc',
257
+ cursorField: 'createdAt',
258
+ cursor: firstPage.next,
259
+ })
260
+
261
+ expect(middlePage.list).toHaveLength(3)
262
+ expect(middlePage.previous).not.toBeNull()
263
+ expect(middlePage.next).not.toBeNull()
264
+ })
265
+
266
+ it('DESC - last page: has previous, no next', async () => {
267
+ await seedDocs(7)
268
+
269
+ const firstPage = await paginateCursor(collection, {
270
+ limit: 3,
271
+ order: 'desc',
272
+ cursorField: 'createdAt',
273
+ })
274
+ const secondPage = await paginateCursor(collection, {
275
+ limit: 3,
276
+ order: 'desc',
277
+ cursorField: 'createdAt',
278
+ cursor: firstPage.next,
279
+ })
280
+ const lastPage = await paginateCursor(collection, {
281
+ limit: 3,
282
+ order: 'desc',
283
+ cursorField: 'createdAt',
284
+ cursor: secondPage.next,
285
+ })
286
+
287
+ expect(lastPage.list.length).toBeGreaterThan(0)
288
+ expect(lastPage.previous).not.toBeNull()
289
+ expect(lastPage.next).toBeNull()
290
+ })
291
+
292
+ it('DESC - single page: no next, no previous', async () => {
293
+ await seedDocs(2)
294
+
295
+ const singlePage = await paginateCursor(collection, {
296
+ limit: 5,
297
+ order: 'desc',
298
+ cursorField: 'createdAt',
299
+ })
300
+
301
+ expect(singlePage.list).toHaveLength(2)
302
+ expect(singlePage.previous).toBeNull()
303
+ expect(singlePage.next).toBeNull()
304
+ })
305
+
306
+ // ---------- STABILITY CHECKS ----------
307
+
308
+ it('returns consistent next/previous cursors across calls', async () => {
309
+ await seedDocs(6)
310
+
311
+ const firstPage = await paginateCursor(collection, {
312
+ limit: 3,
313
+ order: 'asc',
314
+ cursorField: 'createdAt',
315
+ })
316
+
317
+ const secondPage = await paginateCursor(collection, {
318
+ limit: 3,
319
+ order: 'asc',
320
+ cursorField: 'createdAt',
321
+ cursor: firstPage.next,
322
+ })
323
+ console.log(secondPage)
324
+ const firstPageLast = firstPage.list[firstPage.list.length - 1].createdAt
325
+ const secondPageFirst = secondPage.list[0].createdAt
326
+
327
+ expect(secondPageFirst.getTime()).toBeGreaterThan(firstPageLast.getTime())
328
+ expect(secondPage.previous).toEqual(secondPage.list[0].createdAt)
329
+ expect(secondPage.next).toBeNull()
330
+ })
331
+
332
+ it('totalCount remains constant across pages', async () => {
333
+ await seedDocs(8)
334
+
335
+ const firstPage = await paginateCursor(collection, {
336
+ limit: 3,
337
+ order: 'asc',
338
+ cursorField: 'createdAt',
339
+ })
340
+
341
+ const secondPage = await paginateCursor(collection, {
342
+ limit: 3,
343
+ order: 'asc',
344
+ cursorField: 'createdAt',
345
+ cursor: firstPage.next,
346
+ })
347
+
348
+ expect(firstPage.totalCount).toBe(8)
349
+ expect(secondPage.totalCount).toBe(8)
350
+ })
351
+ })
@@ -1,17 +1,17 @@
1
1
  import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
2
-
3
2
  import { paginate } from '../../src/mongodb/paginate.js'
4
3
  import { startMongo, stopMongo } from '../resources/docker-mongo-test.js'
5
4
  import { initializeMongoDb } from '../../src/mongodb/initialize-mongodb.js'
6
5
 
7
- const MONGO_PORT = 29050
6
+ const MONGO_PORT = 29051
8
7
  const CONTAINER_NAME = 'mongo-auth-attempts-paginate-test'
9
8
 
10
9
  let db
11
10
  let collection
11
+
12
12
  describe('paginate - Integration', () => {
13
13
  beforeAll(async () => {
14
- startMongo(MONGO_PORT, CONTAINER_NAME)
14
+ await startMongo(MONGO_PORT, CONTAINER_NAME)
15
15
 
16
16
  db = await initializeMongoDb({
17
17
  config: {
@@ -19,21 +19,22 @@ describe('paginate - Integration', () => {
19
19
  options: { dbName: 'users-management' },
20
20
  },
21
21
  collectionNames: {
22
- TestDocs: 'test_docs',
22
+ TestDocs: 'test_docs_page',
23
23
  },
24
24
  })
25
25
 
26
26
  collection = db.TestDocs
27
27
  })
28
- afterAll(() => {
29
- stopMongo(CONTAINER_NAME)
28
+
29
+ afterAll(async () => {
30
+ await stopMongo(CONTAINER_NAME)
30
31
  })
31
32
 
32
33
  beforeEach(async () => {
33
34
  await collection.deleteMany({})
34
35
  })
35
36
 
36
- const seedDocs = async (count = 12) => {
37
+ const seedDocs = async (count = 25) => {
37
38
  const docs = Array.from({ length: count }).map((_, i) => ({
38
39
  name: `doc_${i}`,
39
40
  createdAt: new Date(Date.now() + i * 1000),
@@ -42,110 +43,152 @@ describe('paginate - Integration', () => {
42
43
  return docs
43
44
  }
44
45
 
45
- it('returns first page with limit', async () => {
46
- await seedDocs(12)
46
+ it('returns first page with correct size and metadata', async () => {
47
+ await seedDocs(15)
47
48
 
48
49
  const result = await paginate(collection, {
49
- filter: {},
50
50
  limit: 5,
51
- order: 'desc',
51
+ page: 1,
52
+ order: 'asc',
52
53
  cursorField: 'createdAt',
53
54
  })
54
55
 
55
56
  expect(result.list).toHaveLength(5)
56
- expect(result.next).toBeDefined()
57
- expect(result.previous).toBeNull()
57
+ expect(result.currentPage).toBe(1)
58
+ expect(result.totalPages).toBe(3)
59
+ expect(result.hasNext).toBe(true)
60
+ expect(result.hasPrevious).toBe(false)
58
61
  })
59
62
 
60
- it('returns empty when no docs match filter', async () => {
63
+ it('returns middle page correctly', async () => {
64
+ await seedDocs(15)
65
+
61
66
  const result = await paginate(collection, {
62
- filter: { name: 'not-exist' },
63
67
  limit: 5,
64
- order: 'desc',
68
+ page: 2,
69
+ order: 'asc',
70
+ cursorField: 'createdAt',
65
71
  })
66
72
 
67
- expect(result.list).toHaveLength(0)
68
- expect(result.next).toBeNull()
73
+ expect(result.list).toHaveLength(5)
74
+ expect(result.currentPage).toBe(2)
75
+ expect(result.hasNext).toBe(true)
76
+ expect(result.hasPrevious).toBe(true)
69
77
  })
70
78
 
71
- it('supports ascending order', async () => {
72
- const docs = await seedDocs(3)
79
+ it('returns last page correctly', async () => {
80
+ await seedDocs(15)
73
81
 
74
- const ascResult = await paginate(collection, {
75
- filter: {},
76
- limit: 3,
82
+ const result = await paginate(collection, {
83
+ limit: 5,
84
+ page: 3,
85
+ order: 'asc',
86
+ cursorField: 'createdAt',
87
+ })
88
+
89
+ expect(result.list).toHaveLength(5)
90
+ expect(result.hasNext).toBe(false)
91
+ expect(result.hasPrevious).toBe(true)
92
+ })
93
+
94
+ it('returns empty list for page out of range', async () => {
95
+ await seedDocs(10)
96
+
97
+ const result = await paginate(collection, {
98
+ limit: 5,
99
+ page: 5,
77
100
  order: 'asc',
78
101
  cursorField: 'createdAt',
79
102
  })
80
103
 
81
- expect(ascResult.list[0].name).toBe(docs[0].name)
104
+ expect(result.list).toHaveLength(0)
105
+ expect(result.hasNext).toBe(false)
106
+ expect(result.hasPrevious).toBe(true)
107
+ })
108
+
109
+ it('returns totalCount consistent across pages', async () => {
110
+ await seedDocs(9)
111
+
112
+ const page1 = await paginate(collection, {
113
+ limit: 3,
114
+ page: 1,
115
+ })
116
+
117
+ const page2 = await paginate(collection, {
118
+ limit: 3,
119
+ page: 2,
120
+ })
121
+
122
+ expect(page1.totalCount).toBe(9)
123
+ expect(page2.totalCount).toBe(9)
82
124
  })
83
125
 
84
126
  it('supports descending order', async () => {
85
- const docs = await seedDocs(3)
127
+ const docs = await seedDocs(6)
86
128
 
87
- const descResult = await paginate(collection, {
88
- filter: {},
129
+ const result = await paginate(collection, {
89
130
  limit: 3,
131
+ page: 1,
90
132
  order: 'desc',
91
133
  cursorField: 'createdAt',
92
134
  })
93
135
 
94
- expect(descResult.list[0].name).toBe(docs[2].name)
136
+ expect(result.list[0].name).toBe(docs[5].name)
137
+ expect(result.list[2].name).toBe(docs[3].name)
95
138
  })
96
139
 
97
- it('paginates with cursor (next page)', async () => {
98
- await seedDocs(7)
140
+ it('supports ascending order', async () => {
141
+ const docs = await seedDocs(6)
99
142
 
100
- // first page
101
- const firstPage = await paginate(collection, {
102
- filter: {},
143
+ const result = await paginate(collection, {
103
144
  limit: 3,
145
+ page: 1,
104
146
  order: 'asc',
105
147
  cursorField: 'createdAt',
106
148
  })
107
149
 
108
- expect(firstPage.list).toHaveLength(3)
109
- expect(firstPage.next).toBeDefined()
150
+ expect(result.list[0].name).toBe(docs[0].name)
151
+ expect(result.list[2].name).toBe(docs[2].name)
152
+ })
153
+
154
+ it('returns no documents when filter excludes all', async () => {
155
+ await seedDocs(5)
110
156
 
111
- // second page using cursor
112
- const secondPage = await paginate(collection, {
113
- filter: {},
157
+ const result = await paginate(collection, {
158
+ filter: { name: 'not_existing' },
114
159
  limit: 3,
115
- order: 'asc',
116
- cursorField: 'createdAt',
117
- cursor: firstPage.next,
160
+ page: 1,
118
161
  })
119
162
 
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
- )
163
+ expect(result.list).toHaveLength(0)
164
+ expect(result.totalCount).toBe(0)
165
+ expect(result.totalPages).toBe(0)
128
166
  })
129
167
 
130
- it('paginates with stringified ObjectId as cursor', async () => {
131
- const docs = await seedDocs(5)
168
+ it('paginates deterministically across pages', async () => {
169
+ const docs = await seedDocs(9)
132
170
 
133
- // @ts-ignore
134
- const stringCursor = docs[2]._id.toString()
171
+ const page1 = await paginate(collection, {
172
+ limit: 3,
173
+ page: 1,
174
+ order: 'asc',
175
+ cursorField: 'createdAt',
176
+ })
135
177
 
136
- const page = await paginate(collection, {
137
- filter: {},
138
- limit: 2,
178
+ const page2 = await paginate(collection, {
179
+ limit: 3,
180
+ page: 2,
139
181
  order: 'asc',
140
- cursorField: '_id',
141
- cursor: stringCursor,
182
+ cursorField: 'createdAt',
142
183
  })
143
184
 
144
- expect(page.list).toHaveLength(2)
185
+ // ensure continuity: last of page1 == item before first of page2
186
+ const lastOfPage1 = page1.list[page1.list.length - 1].name
187
+ const firstOfPage2 = page2.list[0].name
145
188
 
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())
189
+ const indexDiff =
190
+ docs.findIndex((d) => d.name === firstOfPage2) -
191
+ docs.findIndex((d) => d.name === lastOfPage1)
192
+ expect(indexDiff).toBe(1)
150
193
  })
151
194
  })
@@ -9,7 +9,7 @@
9
9
  * @param {'asc'|'desc'} [options.order='asc']
10
10
  * @param {number} [options.limit=10]
11
11
  */
12
- export function paginate(
12
+ export function paginateCursor(
13
13
  collection: import('mongodb').Collection,
14
14
  {
15
15
  limit,
@@ -48,4 +48,42 @@ export function getPaginationEdges({
48
48
  hasNext: boolean
49
49
  hasPrevious: boolean
50
50
  }>
51
+ /**
52
+ * Classic page/limit pagination with total count
53
+ *
54
+ * @param {import('mongodb').Collection} collection
55
+ * @param {Object} options
56
+ * @param {Object} [options.filter={}] - MongoDB filter
57
+ * @param {string} [options.cursorField='_id'] - Field to sort by
58
+ * @param {'asc'|'desc'} [options.order='desc'] - Sort order
59
+ * @param {number} [options.limit=10] - Items per page
60
+ * @param {number} [options.page=1] - Page number (1-based)
61
+ * @param {Object} [options.projection] - Projection fields
62
+ */
63
+ export function paginate(
64
+ collection: import('mongodb').Collection,
65
+ {
66
+ sortBy,
67
+ page,
68
+ limit,
69
+ filter,
70
+ projection,
71
+ order,
72
+ }?: {
73
+ filter?: any
74
+ cursorField?: string
75
+ order?: 'asc' | 'desc'
76
+ limit?: number
77
+ page?: number
78
+ projection?: any
79
+ },
80
+ ): Promise<{
81
+ order: 'asc' | 'desc'
82
+ hasNext: boolean
83
+ totalCount: number
84
+ totalPages: number
85
+ hasPrevious: boolean
86
+ list: import('mongodb').WithId<import('bson').Document>[]
87
+ currentPage: number
88
+ }>
51
89
  import { ObjectId } from 'mongodb'