@cloudbase/database 0.11.0 → 0.12.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 (65) hide show
  1. package/dist/commonjs/collection.d.ts +1 -1
  2. package/dist/commonjs/document.js +16 -6
  3. package/dist/commonjs/transaction/query.d.ts +39 -1
  4. package/dist/commonjs/transaction/query.js +251 -1
  5. package/dist/esm/collection.d.ts +1 -1
  6. package/dist/esm/document.js +16 -6
  7. package/dist/esm/transaction/query.d.ts +39 -1
  8. package/dist/esm/transaction/query.js +251 -1
  9. package/package.json +1 -1
  10. package/src/collection.ts +4 -3
  11. package/src/document.ts +17 -9
  12. package/src/transaction/query.ts +355 -2
  13. package/tests/transaction-query.test.ts +657 -0
  14. package/coverage/lcov-report/base.css +0 -224
  15. package/coverage/lcov-report/block-navigation.js +0 -87
  16. package/coverage/lcov-report/favicon.png +0 -0
  17. package/coverage/lcov-report/index.html +0 -101
  18. package/coverage/lcov-report/prettify.css +0 -1
  19. package/coverage/lcov-report/prettify.js +0 -2
  20. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  21. package/coverage/lcov-report/sorter.js +0 -210
  22. package/coverage/lcov.info +0 -0
  23. package/dist/esm/aggregate.d.ts.map +0 -1
  24. package/dist/esm/collection.d.ts.map +0 -1
  25. package/dist/esm/command.d.ts.map +0 -1
  26. package/dist/esm/commands/logic.d.ts.map +0 -1
  27. package/dist/esm/commands/query.d.ts.map +0 -1
  28. package/dist/esm/commands/update.d.ts.map +0 -1
  29. package/dist/esm/config/app.config.d.ts.map +0 -1
  30. package/dist/esm/config/error.config.d.ts.map +0 -1
  31. package/dist/esm/const/code.d.ts.map +0 -1
  32. package/dist/esm/constant.d.ts.map +0 -1
  33. package/dist/esm/document.d.ts.map +0 -1
  34. package/dist/esm/geo/index.d.ts.map +0 -1
  35. package/dist/esm/geo/interface.d.ts.map +0 -1
  36. package/dist/esm/geo/lineString.d.ts.map +0 -1
  37. package/dist/esm/geo/multiLineString.d.ts.map +0 -1
  38. package/dist/esm/geo/multiPoint.d.ts.map +0 -1
  39. package/dist/esm/geo/multiPolygon.d.ts.map +0 -1
  40. package/dist/esm/geo/point.d.ts.map +0 -1
  41. package/dist/esm/geo/polygon.d.ts.map +0 -1
  42. package/dist/esm/helper/symbol.d.ts.map +0 -1
  43. package/dist/esm/index.d.ts.map +0 -1
  44. package/dist/esm/lib/util.d.ts.map +0 -1
  45. package/dist/esm/operator-map.d.ts.map +0 -1
  46. package/dist/esm/query.d.ts.map +0 -1
  47. package/dist/esm/regexp/index.d.ts.map +0 -1
  48. package/dist/esm/serializer/common.d.ts.map +0 -1
  49. package/dist/esm/serializer/datatype.d.ts.map +0 -1
  50. package/dist/esm/serializer/query.d.ts.map +0 -1
  51. package/dist/esm/serializer/update.d.ts.map +0 -1
  52. package/dist/esm/serverDate/index.d.ts.map +0 -1
  53. package/dist/esm/transaction/collection.d.ts.map +0 -1
  54. package/dist/esm/transaction/document.d.ts.map +0 -1
  55. package/dist/esm/transaction/index.d.ts.map +0 -1
  56. package/dist/esm/transaction/query.d.ts.map +0 -1
  57. package/dist/esm/typings/index.d.ts.map +0 -1
  58. package/dist/esm/typings/realtime.d.ts.map +0 -1
  59. package/dist/esm/util.d.ts.map +0 -1
  60. package/dist/esm/utils/error.d.ts.map +0 -1
  61. package/dist/esm/utils/msg.d.ts.map +0 -1
  62. package/dist/esm/utils/symbol.d.ts.map +0 -1
  63. package/dist/esm/utils/type.d.ts.map +0 -1
  64. package/dist/esm/utils/utils.d.ts.map +0 -1
  65. package/dist/esm/validate.d.ts.map +0 -1
@@ -0,0 +1,657 @@
1
+ import { EJSON } from 'bson'
2
+ import { Query } from '../src/transaction/query'
3
+ import { Transaction } from '../src/transaction/index'
4
+ import { ErrorCode } from '../src/constant'
5
+
6
+ // ===================== Mock 工具 =====================
7
+
8
+ /** 创建一个模拟的 TransactionAPI(_request) */
9
+ function createMockRequest(sendFn?: jest.Mock) {
10
+ return {
11
+ send: sendFn || jest.fn().mockResolvedValue({
12
+ data: 'null',
13
+ requestId: 'mock-req-id'
14
+ })
15
+ }
16
+ }
17
+
18
+ /** 创建一个模拟的 Transaction 实例 */
19
+ function createMockTransaction(overrides?: {
20
+ transactionId?: string
21
+ sendFn?: jest.Mock
22
+ }) {
23
+ const txId = overrides?.transactionId || 'mock-tx-id-123'
24
+ const request = createMockRequest(overrides?.sendFn)
25
+
26
+ const transaction = {
27
+ getRequestMethod: jest.fn().mockReturnValue(request),
28
+ getTransactionId: jest.fn().mockReturnValue(txId)
29
+ } as unknown as Transaction
30
+
31
+ return { transaction, request, txId }
32
+ }
33
+
34
+ // ===================== 测试用例 =====================
35
+
36
+ describe('Transaction Query', () => {
37
+
38
+ // ---------- 构造函数 ----------
39
+ describe('constructor', () => {
40
+ it('应正确初始化默认参数', () => {
41
+ const { transaction } = createMockTransaction()
42
+ const _query = new Query(transaction, 'test-coll')
43
+
44
+ // 内部属性通过链式调用间接验证
45
+ expect(transaction.getRequestMethod).toHaveBeenCalled()
46
+ expect(transaction.getTransactionId).toHaveBeenCalled()
47
+ expect(_query).toBeTruthy()
48
+ })
49
+
50
+ it('应接受可选的 fieldFilters、fieldOrders 和 queryOptions', () => {
51
+ const { transaction } = createMockTransaction()
52
+ const query = new Query(
53
+ transaction,
54
+ 'test-coll',
55
+ { name: 'test' },
56
+ [{ field: 'age', direction: 'asc' }],
57
+ { limit: 10, offset: 5 }
58
+ )
59
+ // 构造不应抛出
60
+ expect(query).toBeTruthy()
61
+ })
62
+ })
63
+
64
+ // ---------- where ----------
65
+ describe('where()', () => {
66
+ it('传入合法对象应返回新的 Query 实例', () => {
67
+ const { transaction } = createMockTransaction()
68
+ const query = new Query(transaction, 'test-coll')
69
+ const newQuery = query.where({ age: 18 })
70
+
71
+ expect(newQuery).toBeInstanceOf(Query)
72
+ expect(newQuery).not.toBe(query) // 返回新实例
73
+ })
74
+
75
+ it('传入非对象应抛出 QueryParamTypeError', () => {
76
+ const { transaction } = createMockTransaction()
77
+ const query = new Query(transaction, 'test-coll')
78
+
79
+ expect(() => query.where('invalid' as any)).toThrow(ErrorCode.QueryParamTypeError)
80
+ expect(() => query.where(123 as any)).toThrow(ErrorCode.QueryParamTypeError)
81
+ expect(() => query.where(null as any)).toThrow(ErrorCode.QueryParamTypeError)
82
+ })
83
+
84
+ it('所有值均为 undefined 应抛出 QueryParamValueError', () => {
85
+ const { transaction } = createMockTransaction()
86
+ const query = new Query(transaction, 'test-coll')
87
+
88
+ expect(() => query.where({ a: undefined, b: undefined })).toThrow(
89
+ ErrorCode.QueryParamValueError
90
+ )
91
+ })
92
+
93
+ it('空对象不应抛出错误', () => {
94
+ const { transaction } = createMockTransaction()
95
+ const query = new Query(transaction, 'test-coll')
96
+
97
+ // 空对象 keys.length === 0,不会走 checkFlag 分支
98
+ expect(() => query.where({})).not.toThrow()
99
+ })
100
+
101
+ it('部分值为 undefined 但至少有一个非 undefined 时不应抛出', () => {
102
+ const { transaction } = createMockTransaction()
103
+ const query = new Query(transaction, 'test-coll')
104
+
105
+ expect(() => query.where({ a: undefined, b: 1 })).not.toThrow()
106
+ })
107
+ })
108
+
109
+ // ---------- orderBy ----------
110
+ describe('orderBy()', () => {
111
+ it('传入合法字段和方向应返回新的 Query 实例', () => {
112
+ const { transaction } = createMockTransaction()
113
+ const query = new Query(transaction, 'test-coll')
114
+ const newQuery = query.orderBy('name', 'asc')
115
+
116
+ expect(newQuery).toBeInstanceOf(Query)
117
+ expect(newQuery).not.toBe(query)
118
+ })
119
+
120
+ it('可链式调用多次 orderBy', () => {
121
+ const { transaction } = createMockTransaction()
122
+ const query = new Query(transaction, 'test-coll')
123
+ const newQuery = query.orderBy('name', 'asc').orderBy('age', 'desc')
124
+
125
+ expect(newQuery).toBeInstanceOf(Query)
126
+ })
127
+
128
+ it('传入非法排序方向应抛出错误', () => {
129
+ const { transaction } = createMockTransaction()
130
+ const query = new Query(transaction, 'test-coll')
131
+
132
+ expect(() => query.orderBy('name', 'invalid' as any)).toThrow(ErrorCode.DirectionError)
133
+ })
134
+ })
135
+
136
+ // ---------- limit ----------
137
+ describe('limit()', () => {
138
+ it('传入合法整数应返回新的 Query 实例', () => {
139
+ const { transaction } = createMockTransaction()
140
+ const query = new Query(transaction, 'test-coll')
141
+ const newQuery = query.limit(10)
142
+
143
+ expect(newQuery).toBeInstanceOf(Query)
144
+ expect(newQuery).not.toBe(query)
145
+ })
146
+
147
+ it('传入非整数应抛出错误', () => {
148
+ const { transaction } = createMockTransaction()
149
+ const query = new Query(transaction, 'test-coll')
150
+
151
+ expect(() => query.limit(1.5)).toThrow()
152
+ expect(() => query.limit(NaN)).toThrow()
153
+ })
154
+ })
155
+
156
+ // ---------- skip ----------
157
+ describe('skip()', () => {
158
+ it('传入合法整数应返回新的 Query 实例', () => {
159
+ const { transaction } = createMockTransaction()
160
+ const query = new Query(transaction, 'test-coll')
161
+ const newQuery = query.skip(20)
162
+
163
+ expect(newQuery).toBeInstanceOf(Query)
164
+ expect(newQuery).not.toBe(query)
165
+ })
166
+
167
+ it('传入非整数应抛出错误', () => {
168
+ const { transaction } = createMockTransaction()
169
+ const query = new Query(transaction, 'test-coll')
170
+
171
+ expect(() => query.skip(2.3)).toThrow()
172
+ })
173
+ })
174
+
175
+ // ---------- field ----------
176
+ describe('field()', () => {
177
+ it('truthy 的非对象值应被转为 1', () => {
178
+ const { transaction } = createMockTransaction()
179
+ const query = new Query(transaction, 'test-coll')
180
+ const newQuery = query.field({ name: true, age: 'yes', score: 1 })
181
+
182
+ expect(newQuery).toBeInstanceOf(Query)
183
+ expect(newQuery).not.toBe(query)
184
+ })
185
+
186
+ it('falsy 值应被转为 0', () => {
187
+ const { transaction } = createMockTransaction()
188
+ const query = new Query(transaction, 'test-coll')
189
+ const newQuery = query.field({ name: true, secret: false, hidden: 0 })
190
+
191
+ expect(newQuery).toBeInstanceOf(Query)
192
+ })
193
+
194
+ it('对象类型的值应保持不变', () => {
195
+ const { transaction } = createMockTransaction()
196
+ const query = new Query(transaction, 'test-coll')
197
+ const projection = { name: { $slice: 1 } }
198
+ const newQuery = query.field(projection)
199
+
200
+ expect(newQuery).toBeInstanceOf(Query)
201
+ // 对象值不会被转为 1
202
+ expect(projection.name).toEqual({ $slice: 1 })
203
+ })
204
+ })
205
+
206
+ // ---------- get ----------
207
+ describe('get()', () => {
208
+ it('应正确发送请求并返回文档列表', async () => {
209
+ const mockData = [{ _id: 'doc1', name: 'Alice' }, { _id: 'doc2', name: 'Bob' }]
210
+ const sendFn = jest.fn().mockResolvedValue({
211
+ data: EJSON.stringify(mockData, { relaxed: false }),
212
+ requestId: 'req-001',
213
+ total: 2,
214
+ limit: 100,
215
+ offset: 0
216
+ })
217
+ const { transaction, txId } = createMockTransaction({ sendFn })
218
+ const query = new Query(transaction, 'users')
219
+
220
+ const result = await query.get()
221
+
222
+ // 验证请求参数
223
+ expect(sendFn).toHaveBeenCalledWith('database.getInTransaction', expect.objectContaining({
224
+ collectionName: 'users',
225
+ transactionId: txId,
226
+ limit: 100 // 默认 limit
227
+ }))
228
+
229
+ // 验证返回结构
230
+ expect(result.requestId).toBe('req-001')
231
+ expect(result.total).toBe(2)
232
+ expect(result.limit).toBe(100)
233
+ expect(result.offset).toBe(0)
234
+ expect(Array.isArray(result.data)).toBe(true)
235
+ expect(result.data.length).toBe(2)
236
+ })
237
+
238
+ it('data 为 "null" 时应返回空数组', async () => {
239
+ const sendFn = jest.fn().mockResolvedValue({
240
+ data: 'null',
241
+ requestId: 'req-002'
242
+ })
243
+ const { transaction } = createMockTransaction({ sendFn })
244
+ const query = new Query(transaction, 'users')
245
+
246
+ const result = await query.get()
247
+
248
+ expect(result.data).toEqual([])
249
+ expect(result.requestId).toBe('req-002')
250
+ })
251
+
252
+ it('返回单个文档对象时应包装为数组', async () => {
253
+ const singleDoc = { _id: 'doc1', name: 'Alice' }
254
+ const sendFn = jest.fn().mockResolvedValue({
255
+ data: EJSON.stringify(singleDoc, { relaxed: false }),
256
+ requestId: 'req-003'
257
+ })
258
+ const { transaction } = createMockTransaction({ sendFn })
259
+ const query = new Query(transaction, 'users')
260
+
261
+ const result = await query.get()
262
+
263
+ expect(result.data.length).toBe(1)
264
+ })
265
+
266
+ it('limit 超过 1000 时应限制为 1000', async () => {
267
+ const sendFn = jest.fn().mockResolvedValue({
268
+ data: 'null',
269
+ requestId: 'req-004'
270
+ })
271
+ const { transaction } = createMockTransaction({ sendFn })
272
+ const query = new Query(transaction, 'users', undefined, [], { limit: 5000 })
273
+
274
+ await query.get()
275
+
276
+ expect(sendFn).toHaveBeenCalledWith('database.getInTransaction', expect.objectContaining({
277
+ limit: 1000
278
+ }))
279
+ })
280
+
281
+ it('limit 小于 1000 时应使用设定值', async () => {
282
+ const sendFn = jest.fn().mockResolvedValue({
283
+ data: 'null',
284
+ requestId: 'req-005'
285
+ })
286
+ const { transaction } = createMockTransaction({ sendFn })
287
+ const query = new Query(transaction, 'users', undefined, [], { limit: 50 })
288
+
289
+ await query.get()
290
+
291
+ expect(sendFn).toHaveBeenCalledWith('database.getInTransaction', expect.objectContaining({
292
+ limit: 50
293
+ }))
294
+ })
295
+
296
+ it('设置了 offset 应正确传参', async () => {
297
+ const sendFn = jest.fn().mockResolvedValue({
298
+ data: 'null',
299
+ requestId: 'req-006'
300
+ })
301
+ const { transaction } = createMockTransaction({ sendFn })
302
+ const query = new Query(transaction, 'users', undefined, [], { offset: 10 })
303
+
304
+ await query.get()
305
+
306
+ expect(sendFn).toHaveBeenCalledWith('database.getInTransaction', expect.objectContaining({
307
+ offset: 10
308
+ }))
309
+ })
310
+
311
+ it('设置了 orderBy 应正确传参', async () => {
312
+ const sendFn = jest.fn().mockResolvedValue({
313
+ data: 'null',
314
+ requestId: 'req-007'
315
+ })
316
+ const { transaction } = createMockTransaction({ sendFn })
317
+ const orders = [{ field: 'age', direction: 'desc' as const }]
318
+ const query = new Query(transaction, 'users', undefined, orders)
319
+
320
+ await query.get()
321
+
322
+ expect(sendFn).toHaveBeenCalledWith('database.getInTransaction', expect.objectContaining({
323
+ order: [{ field: 'age', direction: 'desc' }]
324
+ }))
325
+ })
326
+
327
+ it('设置了 projection 应正确传参', async () => {
328
+ const sendFn = jest.fn().mockResolvedValue({
329
+ data: 'null',
330
+ requestId: 'req-008'
331
+ })
332
+ const { transaction } = createMockTransaction({ sendFn })
333
+ const query = new Query(transaction, 'users', undefined, [], { projection: { name: 1 } })
334
+
335
+ await query.get()
336
+
337
+ expect(sendFn).toHaveBeenCalledWith('database.getInTransaction', expect.objectContaining({
338
+ projection: { name: 1 }
339
+ }))
340
+ })
341
+
342
+ it('设置了 fieldFilters 应作为 query 参数传递', async () => {
343
+ const sendFn = jest.fn().mockResolvedValue({
344
+ data: 'null',
345
+ requestId: 'req-009'
346
+ })
347
+ const { transaction } = createMockTransaction({ sendFn })
348
+ const filters = { age: { $gt: 18 } }
349
+ const query = new Query(transaction, 'users', filters)
350
+
351
+ await query.get()
352
+
353
+ expect(sendFn).toHaveBeenCalledWith('database.getInTransaction', expect.objectContaining({
354
+ query: filters
355
+ }))
356
+ })
357
+
358
+ it('接口返回错误码应抛出异常', async () => {
359
+ const sendFn = jest.fn().mockResolvedValue({
360
+ code: 'DATABASE_ERROR',
361
+ message: 'something went wrong',
362
+ requestId: 'req-err'
363
+ })
364
+ const { transaction } = createMockTransaction({ sendFn })
365
+ const query = new Query(transaction, 'users')
366
+
367
+ await expect(query.get()).rejects.toEqual(expect.objectContaining({
368
+ code: 'DATABASE_ERROR'
369
+ }))
370
+ })
371
+
372
+ it('返回的 result 不含 total/limit/offset 时结果中不应包含对应字段', async () => {
373
+ const sendFn = jest.fn().mockResolvedValue({
374
+ data: 'null',
375
+ requestId: 'req-010'
376
+ })
377
+ const { transaction } = createMockTransaction({ sendFn })
378
+ const query = new Query(transaction, 'users')
379
+
380
+ const result = await query.get()
381
+
382
+ expect(result).not.toHaveProperty('total')
383
+ expect(result).not.toHaveProperty('limit')
384
+ expect(result).not.toHaveProperty('offset')
385
+ })
386
+ })
387
+
388
+ // ---------- count ----------
389
+ describe('count()', () => {
390
+ it('应返回 total 和 requestId', async () => {
391
+ const mockData = [{ _id: '1' }, { _id: '2' }, { _id: '3' }]
392
+ const sendFn = jest.fn().mockResolvedValue({
393
+ data: EJSON.stringify(mockData, { relaxed: false }),
394
+ requestId: 'req-cnt-001',
395
+ total: 3
396
+ })
397
+ const { transaction } = createMockTransaction({ sendFn })
398
+ const query = new Query(transaction, 'users')
399
+
400
+ const result = await query.count()
401
+
402
+ expect(result.requestId).toBe('req-cnt-001')
403
+ expect(result.total).toBe(3)
404
+ })
405
+
406
+ it('后端返回 total 字段时应优先使用后端值', async () => {
407
+ const mockData = [{ _id: '1' }]
408
+ const sendFn = jest.fn().mockResolvedValue({
409
+ data: EJSON.stringify(mockData, { relaxed: false }),
410
+ requestId: 'req-cnt-002',
411
+ total: 100 // 后端总数与 data 长度不同(分页场景)
412
+ })
413
+ const { transaction } = createMockTransaction({ sendFn })
414
+ const query = new Query(transaction, 'users')
415
+
416
+ const result = await query.count()
417
+
418
+ expect(result.total).toBe(100)
419
+ })
420
+
421
+ it('后端未返回 total 时应根据 data 长度计算', async () => {
422
+ const mockData = [{ _id: '1' }, { _id: '2' }]
423
+ const sendFn = jest.fn().mockResolvedValue({
424
+ data: EJSON.stringify(mockData, { relaxed: false }),
425
+ requestId: 'req-cnt-003'
426
+ // 无 total 字段
427
+ })
428
+ const { transaction } = createMockTransaction({ sendFn })
429
+ const query = new Query(transaction, 'users')
430
+
431
+ const result = await query.count()
432
+
433
+ expect(result.total).toBe(2)
434
+ })
435
+
436
+ it('data 为 "null" 且无 total 时应返回 0', async () => {
437
+ const sendFn = jest.fn().mockResolvedValue({
438
+ data: 'null',
439
+ requestId: 'req-cnt-004'
440
+ })
441
+ const { transaction } = createMockTransaction({ sendFn })
442
+ const query = new Query(transaction, 'users')
443
+
444
+ const result = await query.count()
445
+
446
+ expect(result.total).toBe(0)
447
+ })
448
+
449
+ it('应携带 fieldFilters 作为 query 参数', async () => {
450
+ const sendFn = jest.fn().mockResolvedValue({
451
+ data: 'null',
452
+ requestId: 'req-cnt-005'
453
+ })
454
+ const { transaction } = createMockTransaction({ sendFn })
455
+ const filters = { status: 'active' }
456
+ const query = new Query(transaction, 'users', filters)
457
+
458
+ await query.count()
459
+
460
+ expect(sendFn).toHaveBeenCalledWith('database.getInTransaction', expect.objectContaining({
461
+ query: filters
462
+ }))
463
+ })
464
+
465
+ it('接口返回错误码应抛出异常', async () => {
466
+ const sendFn = jest.fn().mockResolvedValue({
467
+ code: 'PERMISSION_DENIED',
468
+ message: 'no access',
469
+ requestId: 'req-cnt-err'
470
+ })
471
+ const { transaction } = createMockTransaction({ sendFn })
472
+ const query = new Query(transaction, 'users')
473
+
474
+ await expect(query.count()).rejects.toEqual(expect.objectContaining({
475
+ code: 'PERMISSION_DENIED'
476
+ }))
477
+ })
478
+ })
479
+
480
+ // ---------- update ----------
481
+ describe('update()', () => {
482
+ it('应正确发送更新请求', async () => {
483
+ const sendFn = jest.fn().mockResolvedValue({
484
+ requestId: 'req-upd-001',
485
+ updated: EJSON.stringify(1, { relaxed: false })
486
+ })
487
+ const { transaction, txId } = createMockTransaction({ sendFn })
488
+ const filters = { name: 'Alice' }
489
+ const query = new Query(transaction, 'users', filters)
490
+
491
+ const result = await query.update({ age: 25 })
492
+
493
+ expect(sendFn).toHaveBeenCalledWith('database.updateDocInTransaction', expect.objectContaining({
494
+ collectionName: 'users',
495
+ transactionId: txId,
496
+ query: filters
497
+ }))
498
+ expect(result.requestId).toBe('req-upd-001')
499
+ expect(result.updated).toBe(1)
500
+ })
501
+
502
+ it('传入空数据或非对象应抛出错误', async () => {
503
+ const { transaction } = createMockTransaction()
504
+ const query = new Query(transaction, 'users')
505
+
506
+ await expect(query.update(null as any)).rejects.toThrow('参数必需是非空对象')
507
+ await expect(query.update(undefined as any)).rejects.toThrow('参数必需是非空对象')
508
+ await expect(query.update('str' as any)).rejects.toThrow('参数必需是非空对象')
509
+ })
510
+
511
+ it('更新数据包含 _id 应抛出错误', async () => {
512
+ const { transaction } = createMockTransaction()
513
+ const query = new Query(transaction, 'users')
514
+
515
+ await expect(query.update({ _id: 'some-id', name: 'test' })).rejects.toThrow('不能更新_id的值')
516
+ })
517
+
518
+ it('接口返回错误码应抛出异常', async () => {
519
+ const sendFn = jest.fn().mockResolvedValue({
520
+ code: 'DATABASE_ERROR',
521
+ message: 'update failed',
522
+ requestId: 'req-upd-err'
523
+ })
524
+ const { transaction } = createMockTransaction({ sendFn })
525
+ const query = new Query(transaction, 'users', { name: 'test' })
526
+
527
+ await expect(query.update({ age: 20 })).rejects.toEqual(expect.objectContaining({
528
+ code: 'DATABASE_ERROR'
529
+ }))
530
+ })
531
+ })
532
+
533
+ // ---------- remove ----------
534
+ describe('remove()', () => {
535
+ it('应正确发送删除请求', async () => {
536
+ const sendFn = jest.fn().mockResolvedValue({
537
+ requestId: 'req-del-001',
538
+ deleted: EJSON.stringify(3, { relaxed: false })
539
+ })
540
+ const { transaction, txId } = createMockTransaction({ sendFn })
541
+ const filters = { status: 'inactive' }
542
+ const query = new Query(transaction, 'users', filters)
543
+
544
+ const result = await query.remove()
545
+
546
+ expect(sendFn).toHaveBeenCalledWith('database.deleteDocInTransaction', expect.objectContaining({
547
+ collectionName: 'users',
548
+ transactionId: txId,
549
+ query: filters
550
+ }))
551
+ expect(result.requestId).toBe('req-del-001')
552
+ expect(result.deleted).toBe(3)
553
+ })
554
+
555
+ it('有 queryOptions 时应输出警告', async () => {
556
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
557
+ const sendFn = jest.fn().mockResolvedValue({
558
+ requestId: 'req-del-002',
559
+ deleted: EJSON.stringify(0, { relaxed: false })
560
+ })
561
+ const { transaction } = createMockTransaction({ sendFn })
562
+ const query = new Query(transaction, 'users', {}, [], { limit: 10 })
563
+
564
+ await query.remove()
565
+
566
+ expect(warnSpy).toHaveBeenCalledWith(
567
+ '`offset`, `limit` and `projection` are not supported in remove() operation'
568
+ )
569
+ warnSpy.mockRestore()
570
+ })
571
+
572
+ it('有 orderBy 时应输出警告', async () => {
573
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
574
+ const sendFn = jest.fn().mockResolvedValue({
575
+ requestId: 'req-del-003',
576
+ deleted: EJSON.stringify(0, { relaxed: false })
577
+ })
578
+ const { transaction } = createMockTransaction({ sendFn })
579
+ const orders = [{ field: 'name', direction: 'asc' as const }]
580
+ const query = new Query(transaction, 'users', {}, orders)
581
+
582
+ await query.remove()
583
+
584
+ expect(warnSpy).toHaveBeenCalledWith(
585
+ '`orderBy` is not supported in remove() operation'
586
+ )
587
+ warnSpy.mockRestore()
588
+ })
589
+
590
+ it('接口返回错误码应抛出异常', async () => {
591
+ const sendFn = jest.fn().mockResolvedValue({
592
+ code: 'PERMISSION_DENIED',
593
+ message: 'cannot delete',
594
+ requestId: 'req-del-err'
595
+ })
596
+ const { transaction } = createMockTransaction({ sendFn })
597
+ const query = new Query(transaction, 'users', { name: 'test' })
598
+
599
+ await expect(query.remove()).rejects.toEqual(expect.objectContaining({
600
+ code: 'PERMISSION_DENIED'
601
+ }))
602
+ })
603
+ })
604
+
605
+ // ---------- 链式调用 ----------
606
+ describe('链式调用', () => {
607
+ it('where → orderBy → limit → skip → field → get 全链路调用', async () => {
608
+ const mockData = [{ _id: 'doc1', name: 'Alice', age: 20 }]
609
+ const sendFn = jest.fn().mockResolvedValue({
610
+ data: EJSON.stringify(mockData, { relaxed: false }),
611
+ requestId: 'req-chain-001',
612
+ total: 1
613
+ })
614
+ const { transaction } = createMockTransaction({ sendFn })
615
+ const query = new Query(transaction, 'users')
616
+
617
+ const result = await query
618
+ .where({ age: 20 })
619
+ .orderBy('name', 'asc')
620
+ .limit(10)
621
+ .skip(5)
622
+ .field({ name: true, age: true })
623
+ .get()
624
+
625
+ expect(result.data.length).toBe(1)
626
+ expect(result.requestId).toBe('req-chain-001')
627
+
628
+ // 验证请求参数中包含各种设置
629
+ const callArgs = sendFn.mock.calls[0][1]
630
+ expect(callArgs.collectionName).toBe('users')
631
+ expect(callArgs.limit).toBe(10)
632
+ expect(callArgs.offset).toBe(5)
633
+ expect(callArgs.order).toEqual([{ field: 'name', direction: 'asc' }])
634
+ expect(callArgs.projection).toBeDefined()
635
+ expect(callArgs.query).toBeDefined()
636
+ })
637
+
638
+ it('每次链式调用都返回新的 Query 实例,不影响原实例', async () => {
639
+ const sendFn = jest.fn().mockResolvedValue({
640
+ data: 'null',
641
+ requestId: 'req-immutable'
642
+ })
643
+ const { transaction } = createMockTransaction({ sendFn })
644
+ const baseQuery = new Query(transaction, 'users')
645
+
646
+ const q1 = baseQuery.where({ age: 18 })
647
+ const q2 = baseQuery.where({ name: 'Bob' })
648
+ const q3 = q1.limit(5)
649
+
650
+ // 都是不同的实例
651
+ expect(q1).not.toBe(baseQuery)
652
+ expect(q2).not.toBe(baseQuery)
653
+ expect(q3).not.toBe(q1)
654
+ expect(q1).not.toBe(q2)
655
+ })
656
+ })
657
+ })