@cloudbase/database 1.5.0 → 1.5.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.
@@ -15,7 +15,7 @@ export interface QueryOption extends BaseOption {
15
15
  limit?: number;
16
16
  offset?: number;
17
17
  projection?: Object;
18
- order?: Record<string, any>[];
18
+ order?: Record<string, any>;
19
19
  }
20
20
  export interface UpdateOption extends BaseOption {
21
21
  multiple?: boolean;
@@ -46,6 +46,22 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
46
46
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
47
  }
48
48
  };
49
+ var __read = (this && this.__read) || function (o, n) {
50
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
51
+ if (!m) return o;
52
+ var i = m.call(o), r, ar = [], e;
53
+ try {
54
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
55
+ }
56
+ catch (error) { e = { error: error }; }
57
+ finally {
58
+ try {
59
+ if (r && !r.done && (m = i["return"])) m.call(i);
60
+ }
61
+ finally { if (e) throw e.error; }
62
+ }
63
+ return ar;
64
+ };
49
65
  Object.defineProperty(exports, "__esModule", { value: true });
50
66
  exports.Query = void 0;
51
67
  var constant_1 = require("./constant");
@@ -64,12 +80,14 @@ var Query = (function () {
64
80
  this.watch = function (options) {
65
81
  var ws = (0, utils_1.getWsInstance)(_this._db);
66
82
  var _a = _this._apiOptions, limit = _a.limit, order = _a.order;
67
- return ws.watch(__assign(__assign({}, options), { envId: _this._db.config.env, collectionName: _this._coll, query: JSON.stringify(_this._fieldFilters), limit: limit, orderBy: order
68
- ? order.reduce(function (acc, cur) {
69
- acc[cur.field] = cur.direction;
70
- return acc;
71
- }, {})
72
- : undefined }));
83
+ var orderBy = order
84
+ ? Object.entries(order).reduce(function (acc, _a) {
85
+ var _b = __read(_a, 2), field = _b[0], direction = _b[1];
86
+ acc[field] = direction === -1 ? 'desc' : 'asc';
87
+ return acc;
88
+ }, {})
89
+ : undefined;
90
+ return ws.watch(__assign(__assign({}, options), { envId: _this._db.config.env, collectionName: _this._coll, query: (0, utils_1.toWatchQuery)(_this._fieldFilters), limit: limit, orderBy: orderBy }));
73
91
  };
74
92
  this._db = db;
75
93
  this._coll = coll;
@@ -8,6 +8,7 @@ export declare const getReqOpts: (apiOptions: QueryOption | UpdateOption) => any
8
8
  export declare const filterUndefined: (o: any) => any;
9
9
  export declare const stringifyByEJSON: (params: any) => string;
10
10
  export declare const parseByEJSON: (params: any) => any;
11
+ export declare const toWatchQuery: (ejsonStr: string) => string;
11
12
  export declare class TcbError extends Error {
12
13
  readonly code: string;
13
14
  readonly message: string;
@@ -25,8 +25,19 @@ var __assign = (this && this.__assign) || function () {
25
25
  };
26
26
  return __assign.apply(this, arguments);
27
27
  };
28
+ var __values = (this && this.__values) || function(o) {
29
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
30
+ if (m) return m.call(o);
31
+ if (o && typeof o.length === "number") return {
32
+ next: function () {
33
+ if (o && i >= o.length) o = void 0;
34
+ return { value: o && o[i++], done: !o };
35
+ }
36
+ };
37
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
38
+ };
28
39
  Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.processReturn = exports.E = exports.TcbError = exports.parseByEJSON = exports.stringifyByEJSON = exports.filterUndefined = exports.getReqOpts = exports.getWsInstance = exports.autoCount = exports.withCallback = exports.sleep = void 0;
40
+ exports.processReturn = exports.E = exports.TcbError = exports.toWatchQuery = exports.parseByEJSON = exports.stringifyByEJSON = exports.filterUndefined = exports.getReqOpts = exports.getWsInstance = exports.autoCount = exports.withCallback = exports.sleep = void 0;
30
41
  var __1 = require("../");
31
42
  var bson_1 = require("bson");
32
43
  var type_1 = require("./type");
@@ -106,6 +117,55 @@ var parseByEJSON = function (params) {
106
117
  return bson_1.EJSON.parse(params);
107
118
  };
108
119
  exports.parseByEJSON = parseByEJSON;
120
+ var toWatchLegacyValue = function (val) {
121
+ var e_1, _a;
122
+ if (val instanceof Date) {
123
+ return { $date: +val };
124
+ }
125
+ if (val instanceof RegExp) {
126
+ return { $regex: val.source, $options: val.flags };
127
+ }
128
+ if (val && typeof val === 'object') {
129
+ if (val._bsontype === 'ObjectId') {
130
+ return { $oid: val.toHexString() };
131
+ }
132
+ if (val._bsontype === 'BSONRegExp') {
133
+ return { $regex: val.pattern, $options: val.options };
134
+ }
135
+ if (Array.isArray(val)) {
136
+ return val.map(toWatchLegacyValue);
137
+ }
138
+ var ret = {};
139
+ try {
140
+ for (var _b = __values(Object.keys(val)), _c = _b.next(); !_c.done; _c = _b.next()) {
141
+ var key = _c.value;
142
+ ret[key] = toWatchLegacyValue(val[key]);
143
+ }
144
+ }
145
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
146
+ finally {
147
+ try {
148
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
149
+ }
150
+ finally { if (e_1) throw e_1.error; }
151
+ }
152
+ return ret;
153
+ }
154
+ return val;
155
+ };
156
+ var toWatchQuery = function (ejsonStr) {
157
+ if (!ejsonStr) {
158
+ return '{}';
159
+ }
160
+ try {
161
+ var parsed = bson_1.EJSON.parse(ejsonStr);
162
+ return JSON.stringify(toWatchLegacyValue(parsed));
163
+ }
164
+ catch (e) {
165
+ return ejsonStr;
166
+ }
167
+ };
168
+ exports.toWatchQuery = toWatchQuery;
109
169
  var TcbError = (function (_super) {
110
170
  __extends(TcbError, _super);
111
171
  function TcbError(error) {
@@ -15,7 +15,7 @@ export interface QueryOption extends BaseOption {
15
15
  limit?: number;
16
16
  offset?: number;
17
17
  projection?: Object;
18
- order?: Record<string, any>[];
18
+ order?: Record<string, any>;
19
19
  }
20
20
  export interface UpdateOption extends BaseOption {
21
21
  multiple?: boolean;
package/dist/esm/query.js CHANGED
@@ -45,6 +45,22 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
45
45
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
46
46
  }
47
47
  };
48
+ var __read = (this && this.__read) || function (o, n) {
49
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
50
+ if (!m) return o;
51
+ var i = m.call(o), r, ar = [], e;
52
+ try {
53
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
54
+ }
55
+ catch (error) { e = { error: error }; }
56
+ finally {
57
+ try {
58
+ if (r && !r.done && (m = i["return"])) m.call(i);
59
+ }
60
+ finally { if (e) throw e.error; }
61
+ }
62
+ return ar;
63
+ };
48
64
  import { QueryType } from './constant';
49
65
  import { Db } from './index';
50
66
  import { Validate } from './validate';
@@ -52,7 +68,7 @@ import { Util } from './util';
52
68
  import { QuerySerializer } from './serializer/query';
53
69
  import { UpdateSerializer } from './serializer/update';
54
70
  import { ErrorCode } from './constant';
55
- import { getWsInstance, getReqOpts, stringifyByEJSON, processReturn, withCallback } from './utils/utils';
71
+ import { getWsInstance, getReqOpts, stringifyByEJSON, toWatchQuery, processReturn, withCallback } from './utils/utils';
56
72
  import { ERRORS } from './const/code';
57
73
  import { EJSON } from 'bson';
58
74
  var Query = (function () {
@@ -61,12 +77,14 @@ var Query = (function () {
61
77
  this.watch = function (options) {
62
78
  var ws = getWsInstance(_this._db);
63
79
  var _a = _this._apiOptions, limit = _a.limit, order = _a.order;
64
- return ws.watch(__assign(__assign({}, options), { envId: _this._db.config.env, collectionName: _this._coll, query: JSON.stringify(_this._fieldFilters), limit: limit, orderBy: order
65
- ? order.reduce(function (acc, cur) {
66
- acc[cur.field] = cur.direction;
67
- return acc;
68
- }, {})
69
- : undefined }));
80
+ var orderBy = order
81
+ ? Object.entries(order).reduce(function (acc, _a) {
82
+ var _b = __read(_a, 2), field = _b[0], direction = _b[1];
83
+ acc[field] = direction === -1 ? 'desc' : 'asc';
84
+ return acc;
85
+ }, {})
86
+ : undefined;
87
+ return ws.watch(__assign(__assign({}, options), { envId: _this._db.config.env, collectionName: _this._coll, query: toWatchQuery(_this._fieldFilters), limit: limit, orderBy: orderBy }));
70
88
  };
71
89
  this._db = db;
72
90
  this._coll = coll;
@@ -8,6 +8,7 @@ export declare const getReqOpts: (apiOptions: QueryOption | UpdateOption) => any
8
8
  export declare const filterUndefined: (o: any) => any;
9
9
  export declare const stringifyByEJSON: (params: any) => string;
10
10
  export declare const parseByEJSON: (params: any) => any;
11
+ export declare const toWatchQuery: (ejsonStr: string) => string;
11
12
  export declare class TcbError extends Error {
12
13
  readonly code: string;
13
14
  readonly message: string;
@@ -24,6 +24,17 @@ var __assign = (this && this.__assign) || function () {
24
24
  };
25
25
  return __assign.apply(this, arguments);
26
26
  };
27
+ var __values = (this && this.__values) || function(o) {
28
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
29
+ if (m) return m.call(o);
30
+ if (o && typeof o.length === "number") return {
31
+ next: function () {
32
+ if (o && i >= o.length) o = void 0;
33
+ return { value: o && o[i++], done: !o };
34
+ }
35
+ };
36
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
37
+ };
27
38
  import { Db } from '../';
28
39
  import { EJSON } from 'bson';
29
40
  import { isObject } from './type';
@@ -95,6 +106,54 @@ export var stringifyByEJSON = function (params) {
95
106
  export var parseByEJSON = function (params) {
96
107
  return EJSON.parse(params);
97
108
  };
109
+ var toWatchLegacyValue = function (val) {
110
+ var e_1, _a;
111
+ if (val instanceof Date) {
112
+ return { $date: +val };
113
+ }
114
+ if (val instanceof RegExp) {
115
+ return { $regex: val.source, $options: val.flags };
116
+ }
117
+ if (val && typeof val === 'object') {
118
+ if (val._bsontype === 'ObjectId') {
119
+ return { $oid: val.toHexString() };
120
+ }
121
+ if (val._bsontype === 'BSONRegExp') {
122
+ return { $regex: val.pattern, $options: val.options };
123
+ }
124
+ if (Array.isArray(val)) {
125
+ return val.map(toWatchLegacyValue);
126
+ }
127
+ var ret = {};
128
+ try {
129
+ for (var _b = __values(Object.keys(val)), _c = _b.next(); !_c.done; _c = _b.next()) {
130
+ var key = _c.value;
131
+ ret[key] = toWatchLegacyValue(val[key]);
132
+ }
133
+ }
134
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
135
+ finally {
136
+ try {
137
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
138
+ }
139
+ finally { if (e_1) throw e_1.error; }
140
+ }
141
+ return ret;
142
+ }
143
+ return val;
144
+ };
145
+ export var toWatchQuery = function (ejsonStr) {
146
+ if (!ejsonStr) {
147
+ return '{}';
148
+ }
149
+ try {
150
+ var parsed = EJSON.parse(ejsonStr);
151
+ return JSON.stringify(toWatchLegacyValue(parsed));
152
+ }
153
+ catch (e) {
154
+ return ejsonStr;
155
+ }
156
+ };
98
157
  var TcbError = (function (_super) {
99
158
  __extends(TcbError, _super);
100
159
  function TcbError(error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbase/database",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "database for (node.)js sdk ",
5
5
  "main": "dist/commonjs/index.js",
6
6
  "module": "dist/esm/index.js",
package/src/query.ts CHANGED
@@ -10,7 +10,7 @@ import { UpdateSerializer } from './serializer/update'
10
10
  // import { WSClient } from "./websocket/wsclient"
11
11
  import { IWatchOptions, DBRealtimeListener } from './typings/index'
12
12
  import { ErrorCode } from './constant'
13
- import { getWsInstance, getReqOpts, stringifyByEJSON, processReturn, withCallback } from './utils/utils'
13
+ import { getWsInstance, getReqOpts, stringifyByEJSON, toWatchQuery, processReturn, withCallback } from './utils/utils'
14
14
  import { ERRORS } from './const/code'
15
15
  import { EJSON } from 'bson'
16
16
 
@@ -34,8 +34,8 @@ export interface QueryOption extends BaseOption {
34
34
  offset?: number
35
35
  // 指定显示或者不显示哪些字段
36
36
  projection?: Object
37
- // 结果排序
38
- order?: Record<string, any>[]
37
+ // 结果排序(orderBy() 存为 { field: 1 | -1 })
38
+ order?: Record<string, any>
39
39
  }
40
40
 
41
41
  export interface UpdateOption extends BaseOption {
@@ -598,18 +598,24 @@ export class Query {
598
598
 
599
599
  const { limit, order } = this._apiOptions as QueryOption
600
600
 
601
+ // order 由 orderBy() 存为 { field: 1 | -1 },实时推送协议要求 'asc' | 'desc'
602
+ const orderBy = order
603
+ ? Object.entries(order).reduce<Record<string, string>>((acc, [field, direction]) => {
604
+ acc[field] = direction === -1 ? 'desc' : 'asc'
605
+ return acc
606
+ }, {})
607
+ : undefined
608
+
601
609
  return ws.watch({
602
610
  ...options,
603
611
  envId: this._db.config.env,
604
612
  collectionName: this._coll,
605
- query: JSON.stringify(this._fieldFilters), // 实时推送这里需要换成ejson协议,todo
613
+ // where() 产出 canonical EJSON 字符串,但 watch 协议要求 legacy 内部编码
614
+ // (数字裸值、$date 为 epoch 毫秒数字)——canonical 形态服务端虽接受但会静默匹配不到文档,
615
+ // 且不可再 JSON.stringify(否则双重编码,线上 100003 事故根因)
616
+ query: toWatchQuery(this._fieldFilters),
606
617
  limit,
607
- orderBy: order
608
- ? order.reduce<Record<string, string>>((acc, cur) => {
609
- acc[cur.field] = cur.direction
610
- return acc
611
- }, {})
612
- : undefined
618
+ orderBy
613
619
  })
614
620
  }
615
621
  }
@@ -99,6 +99,65 @@ export const parseByEJSON = params => {
99
99
  return EJSON.parse(params)
100
100
  }
101
101
 
102
+ /**
103
+ * 将 EJSON 值递归转为实时推送 watch 协议要求的 legacy 内部编码(0.12.x 形态)
104
+ * - Date → { $date: <epoch 毫秒数字> }
105
+ * - ObjectId → { $oid }、BSONRegExp/原生 RegExp → { $regex, $options }
106
+ * - 数字/字符串/布尔等基本值保持裸值
107
+ *
108
+ * bson 类型用 _bsontype 字符串判断而非 instanceof:EJSON.parse 产出的实例
109
+ * 来自运行时实际加载的 bson(可能与编译时版本/副本不同),instanceOf 跨副本
110
+ * 会失效,_bsontype 是 bson 各版本稳定的类型标记。
111
+ */
112
+ const toWatchLegacyValue = (val: any): any => {
113
+ if (val instanceof Date) {
114
+ return { $date: +val }
115
+ }
116
+ if (val instanceof RegExp) {
117
+ // 原生 RegExp 兜底(正常链路编码器已转 $regularExpression,raw 模式等可能漏过)
118
+ return { $regex: val.source, $options: val.flags }
119
+ }
120
+ if (val && typeof val === 'object') {
121
+ if (val._bsontype === 'ObjectId') {
122
+ return { $oid: val.toHexString() }
123
+ }
124
+ if (val._bsontype === 'BSONRegExp') {
125
+ return { $regex: val.pattern, $options: val.options }
126
+ }
127
+ if (Array.isArray(val)) {
128
+ return val.map(toWatchLegacyValue)
129
+ }
130
+ const ret: Record<string, any> = {}
131
+ for (const key of Object.keys(val)) {
132
+ ret[key] = toWatchLegacyValue(val[key])
133
+ }
134
+ return ret
135
+ }
136
+ return val
137
+ }
138
+
139
+ /**
140
+ * 实时推送 watch 协议的 query 编码
141
+ *
142
+ * where() 产出的 _fieldFilters 是 canonical EJSON 字符串(relaxed:false),
143
+ * 但服务端 watch 查询引擎只认 0.12.x 的 legacy 内部编码:
144
+ * - 数字必须裸值({$numberInt}/{$numberLong} 会被静默匹配不到文档,实测 0 命中)
145
+ * - 日期必须是 { $date: <epoch 毫秒数字> }(ISO 串与 {$date:{$numberLong}} 均不命中)
146
+ * 此处 parse 后转成 legacy 形态再序列化。
147
+ */
148
+ export const toWatchQuery = (ejsonStr: string): string => {
149
+ if (!ejsonStr) {
150
+ return '{}'
151
+ }
152
+ try {
153
+ const parsed = EJSON.parse(ejsonStr)
154
+ return JSON.stringify(toWatchLegacyValue(parsed))
155
+ } catch (e) {
156
+ // 非 EJSON 文本(EJSON.parse 兼容普通 JSON,走到这说明确实解析不了),原样透传由调用方保证格式
157
+ return ejsonStr
158
+ }
159
+ }
160
+
102
161
  export class TcbError extends Error {
103
162
  readonly code: string
104
163
  readonly message: string
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Query.watch() 回归测试
3
+ *
4
+ * 1.5.0 引入的回归:where() 改用 QuerySerializer.encodeEJSON() 后
5
+ * _fieldFilters 已是 EJSON 字符串,watch() 若再 JSON.stringify 会产生
6
+ * 双重编码(线上 INIT_WATCH 失败、errorCode=100003 的根因)。
7
+ * 伴生问题:orderBy() 改存对象 {field: 1|-1} 后,watch() 仍按 0.12.x
8
+ * 数组形态调用 order.reduce(),带 orderBy 的 watch 会直接抛 TypeError。
9
+ *
10
+ * 第二轮真实环境实测(2026-09-03)追加约束:服务端 watch 查询引擎只认
11
+ * 0.12.x legacy 内部编码——数字裸值、日期 { $date: epoch 毫秒数字 }。
12
+ * canonical EJSON($numberInt/$numberLong/$date ISO)虽不报错但静默匹配
13
+ * 不到文档,因此 watch query 须由 canonical EJSON 转换为 legacy 形态。
14
+ */
15
+ import { Db } from '../src/index'
16
+
17
+ describe('Query.watch() 参数序列化', () => {
18
+ let watchArgs: any[]
19
+
20
+ class MockRequest {
21
+ constructor(_config: any) {}
22
+ async send(_action: string, _data: any) {
23
+ return { data: { ok: 1 }, requestId: 'mock' }
24
+ }
25
+ }
26
+
27
+ class MockWsClient {
28
+ constructor(_opts: any) {}
29
+ watch(args: any) {
30
+ watchArgs.push(args)
31
+ return { close: () => {} }
32
+ }
33
+ }
34
+
35
+ beforeEach(() => {
36
+ Db.reqClass = MockRequest as any
37
+ Db.wsClientClass = MockWsClient as any
38
+ watchArgs = []
39
+ })
40
+
41
+ function makeDb() {
42
+ // getWsInstance 按 env 缓存实例,每个用例用独立 env
43
+ return new Db({ env: `env-${Math.random()}` } as any)
44
+ }
45
+
46
+ it('数字条件应为裸值(legacy 编码),无 $numberInt/$numberLong 标记', () => {
47
+ const db = makeDb()
48
+ db.collection('test-collection')
49
+ .where({ ts: { $gt: 1788421619261 } } as any)
50
+ .watch({ onChange: () => {}, onError: () => {} })
51
+
52
+ expect(watchArgs).toHaveLength(1)
53
+ const query = watchArgs[0].query
54
+ // legacy 形态:数字裸值
55
+ expect(query).toBe('{"ts":{"$gt":1788421619261}}')
56
+ // 双重编码的标志形态:外层以引号开头 —— 必须不存在
57
+ expect(query.startsWith('"')).toBe(false)
58
+ // canonical 标记 —— 必须不存在
59
+ expect(query).not.toContain('$numberInt')
60
+ expect(query).not.toContain('$numberLong')
61
+ })
62
+
63
+ it('where() + command 查询条件同样转为裸数字', () => {
64
+ const db = makeDb()
65
+ db.collection('test-collection')
66
+ .where({ count: db.command.gt(10) } as any)
67
+ .watch({ onChange: () => {}, onError: () => {} })
68
+
69
+ expect(watchArgs[0].query).toBe('{"count":{"$gt":10}}')
70
+ })
71
+
72
+ it('日期条件应转为 { $date: epoch 毫秒数字 }(0.12.x legacy 形态)', () => {
73
+ const db = makeDb()
74
+ const date = new Date('2020-01-01T00:00:00.000Z')
75
+ db.collection('test-collection')
76
+ .where({ createdAt: db.command.gt(date) } as any)
77
+ .watch({ onChange: () => {}, onError: () => {} })
78
+
79
+ expect(watchArgs[0].query).toBe(`{"createdAt":{"$gt":{"$date":${+date}}}}`)
80
+ })
81
+
82
+ it('db.RegExp / 原生 RegExp 应转为 { $regex, $options }($regularExpression 不被服务端识别)', () => {
83
+ const db = makeDb()
84
+ db.collection('test-collection')
85
+ .where({ text: db.RegExp({ regexp: 'probe-\\d+', options: 'i' }) } as any)
86
+ .watch({ onChange: () => {}, onError: () => {} })
87
+ // canonical 编码为 $regularExpression,watch 须转为 0.12.x 的 $regex/$options
88
+ expect(watchArgs[0].query).toBe('{"text":{"$regex":"probe-\\\\d+","$options":"i"}}')
89
+ expect(watchArgs[0].query).not.toContain('$regularExpression')
90
+
91
+ db.collection('test-collection')
92
+ .where({ text: /probe-\d+/i } as any)
93
+ .watch({ onChange: () => {}, onError: () => {} })
94
+ expect(watchArgs[1].query).toBe('{"text":{"$regex":"probe-\\\\d+","$options":"i"}}')
95
+ })
96
+
97
+ it('ObjectId 条件应保持 { $oid } 形态', () => {
98
+ const db = makeDb()
99
+ // 该仓库 ObjectId 构造签名为 { id } 对象形式(bson 风格位置参数会静默得到空 id)
100
+ db.collection('test-collection')
101
+ .where({ _id: db.ObjectId({ id: '507f1f77bcf86cd799439011' }) } as any)
102
+ .watch({ onChange: () => {}, onError: () => {} })
103
+
104
+ expect(watchArgs[0].query).toBe('{"_id":{"$oid":"507f1f77bcf86cd799439011"}}')
105
+ })
106
+
107
+ it('无 where() 时 query 回退为 "{}"', () => {
108
+ const db = makeDb()
109
+ db.collection('test-collection').watch({ onChange: () => {}, onError: () => {} })
110
+
111
+ expect(watchArgs[0].query).toBe('{}')
112
+ })
113
+
114
+ it('字符串等基本类型条件保持原样', () => {
115
+ const db = makeDb()
116
+ db.collection('test-collection')
117
+ .where({ _id: 'xxx', name: 'foo' } as any)
118
+ .watch({ onChange: () => {}, onError: () => {} })
119
+
120
+ expect(watchArgs[0].query).toBe('{"_id":"xxx","name":"foo"}')
121
+ })
122
+
123
+ it('orderBy() 后 watch() 不抛错且方向映射为 asc/desc', () => {
124
+ const db = makeDb()
125
+ expect(() => {
126
+ db.collection('test-collection')
127
+ .where({ ts: { $gt: 1 } } as any)
128
+ .orderBy('ts', 'desc')
129
+ .watch({ onChange: () => {}, onError: () => {} })
130
+ }).not.toThrow()
131
+
132
+ expect(watchArgs[0].orderBy).toEqual({ ts: 'desc' })
133
+ })
134
+
135
+ it('limit 应透传到 watch 参数', () => {
136
+ const db = makeDb()
137
+ db.collection('test-collection')
138
+ .where({ a: 1 } as any)
139
+ .limit(10)
140
+ .watch({ onChange: () => {}, onError: () => {} })
141
+
142
+ expect(watchArgs[0].limit).toBe(10)
143
+ })
144
+ })