monsqlize 1.3.0 → 2.0.0
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/CHANGELOG.md +56 -60
- package/LICENSE +201 -21
- package/README.md +537 -1828
- package/changelogs/README.md +160 -0
- package/changelogs/v2.0.0.md +222 -0
- package/dist/cjs/index.cjs +10600 -0
- package/dist/cjs/mongodb/common/transaction-aware.cjs +10 -0
- package/dist/cjs/transaction/CacheLockManager.cjs +100 -0
- package/dist/cjs/transaction/Transaction.cjs +158 -0
- package/dist/cjs/transaction/TransactionManager.cjs +298 -0
- package/dist/esm/index.mjs +10650 -0
- package/dist/types/base.d.ts +81 -0
- package/dist/types/collection.d.ts +1031 -0
- package/dist/types/expression.d.ts +115 -0
- package/dist/types/index.d.ts +23 -0
- package/dist/types/lock.d.ts +74 -0
- package/dist/types/model.d.ts +526 -0
- package/dist/types/mongodb.d.ts +49 -0
- package/dist/types/monsqlize.d.ts +491 -0
- package/dist/types/pool.d.ts +84 -0
- package/dist/types/runtime.d.ts +362 -0
- package/dist/types/saga.d.ts +143 -0
- package/dist/types/slow-query-log.d.ts +126 -0
- package/dist/types/sync.d.ts +103 -0
- package/dist/types/transaction.d.ts +132 -0
- package/package.json +67 -69
- package/index.d.ts +0 -206
- package/index.mjs +0 -52
- package/lib/cache-invalidation.js +0 -279
- package/lib/cache.js +0 -530
- package/lib/common/cursor.js +0 -59
- package/lib/common/docs-urls.js +0 -73
- package/lib/common/index-options.js +0 -223
- package/lib/common/log.js +0 -61
- package/lib/common/namespace.js +0 -22
- package/lib/common/normalize.js +0 -34
- package/lib/common/page-result.js +0 -43
- package/lib/common/runner.js +0 -57
- package/lib/common/server-features.js +0 -232
- package/lib/common/shape-builders.js +0 -27
- package/lib/common/validation.js +0 -113
- package/lib/connect.js +0 -99
- package/lib/constants.js +0 -55
- package/lib/count-queue.js +0 -188
- package/lib/distributed-cache-invalidator.js +0 -260
- package/lib/errors.js +0 -168
- package/lib/expression/cache/ExpressionCache.js +0 -114
- package/lib/expression/compiler/ExpressionCompiler.js +0 -1090
- package/lib/expression/compiler/ExpressionCompilerExtensions.js +0 -531
- package/lib/expression/detector.js +0 -84
- package/lib/expression/factory.js +0 -29
- package/lib/expression/index.js +0 -19
- package/lib/function-cache.js +0 -533
- package/lib/index.js +0 -1251
- package/lib/infrastructure/ConnectionPoolManager.js +0 -464
- package/lib/infrastructure/HealthChecker.js +0 -281
- package/lib/infrastructure/PoolConfig.js +0 -199
- package/lib/infrastructure/PoolSelector.js +0 -225
- package/lib/infrastructure/PoolStats.js +0 -244
- package/lib/infrastructure/ssh-tunnel-ssh2.js +0 -212
- package/lib/infrastructure/ssh-tunnel.js +0 -41
- package/lib/infrastructure/uri-parser.js +0 -36
- package/lib/lock/Lock.js +0 -67
- package/lib/lock/errors.js +0 -28
- package/lib/lock/index.js +0 -13
- package/lib/logger.js +0 -225
- package/lib/model/examples/test.js +0 -311
- package/lib/model/features/defaults.js +0 -161
- package/lib/model/features/populate.js +0 -568
- package/lib/model/features/relations.js +0 -120
- package/lib/model/features/soft-delete.js +0 -349
- package/lib/model/features/version.js +0 -157
- package/lib/model/features/virtuals.js +0 -219
- package/lib/model/index.js +0 -1265
- package/lib/mongodb/common/accessor-helpers.js +0 -59
- package/lib/mongodb/common/agg-pipeline.js +0 -36
- package/lib/mongodb/common/aggregation-validator.js +0 -127
- package/lib/mongodb/common/iid.js +0 -28
- package/lib/mongodb/common/lexicographic-expr.js +0 -53
- package/lib/mongodb/common/shape.js +0 -32
- package/lib/mongodb/common/sort.js +0 -39
- package/lib/mongodb/common/transaction-aware.js +0 -25
- package/lib/mongodb/connect.js +0 -234
- package/lib/mongodb/index.js +0 -639
- package/lib/mongodb/management/admin-ops.js +0 -200
- package/lib/mongodb/management/bookmark-ops.js +0 -167
- package/lib/mongodb/management/cache-ops.js +0 -50
- package/lib/mongodb/management/collection-ops.js +0 -387
- package/lib/mongodb/management/database-ops.js +0 -202
- package/lib/mongodb/management/index-ops.js +0 -475
- package/lib/mongodb/management/index.js +0 -17
- package/lib/mongodb/management/namespace.js +0 -31
- package/lib/mongodb/management/validation-ops.js +0 -268
- package/lib/mongodb/queries/aggregate.js +0 -172
- package/lib/mongodb/queries/chain.js +0 -631
- package/lib/mongodb/queries/count.js +0 -99
- package/lib/mongodb/queries/distinct.js +0 -78
- package/lib/mongodb/queries/find-and-count.js +0 -193
- package/lib/mongodb/queries/find-by-ids.js +0 -236
- package/lib/mongodb/queries/find-one-by-id.js +0 -171
- package/lib/mongodb/queries/find-one.js +0 -71
- package/lib/mongodb/queries/find-page.js +0 -618
- package/lib/mongodb/queries/find.js +0 -171
- package/lib/mongodb/queries/index.js +0 -51
- package/lib/mongodb/queries/watch.js +0 -538
- package/lib/mongodb/writes/common/batch-retry.js +0 -65
- package/lib/mongodb/writes/delete-batch.js +0 -323
- package/lib/mongodb/writes/delete-many.js +0 -181
- package/lib/mongodb/writes/delete-one.js +0 -173
- package/lib/mongodb/writes/find-one-and-delete.js +0 -203
- package/lib/mongodb/writes/find-one-and-replace.js +0 -239
- package/lib/mongodb/writes/find-one-and-update.js +0 -240
- package/lib/mongodb/writes/increment-one.js +0 -259
- package/lib/mongodb/writes/index.js +0 -46
- package/lib/mongodb/writes/insert-batch.js +0 -508
- package/lib/mongodb/writes/insert-many.js +0 -223
- package/lib/mongodb/writes/insert-one.js +0 -169
- package/lib/mongodb/writes/replace-one.js +0 -226
- package/lib/mongodb/writes/result-handler.js +0 -237
- package/lib/mongodb/writes/update-batch.js +0 -416
- package/lib/mongodb/writes/update-many.js +0 -275
- package/lib/mongodb/writes/update-one.js +0 -273
- package/lib/mongodb/writes/upsert-one.js +0 -203
- package/lib/multi-level-cache.js +0 -244
- package/lib/operators.js +0 -330
- package/lib/redis-cache-adapter.js +0 -267
- package/lib/saga/SagaContext.js +0 -67
- package/lib/saga/SagaDefinition.js +0 -32
- package/lib/saga/SagaExecutor.js +0 -201
- package/lib/saga/SagaOrchestrator.js +0 -186
- package/lib/saga/index.js +0 -11
- package/lib/slow-query-log/base-storage.js +0 -70
- package/lib/slow-query-log/batch-queue.js +0 -97
- package/lib/slow-query-log/config-manager.js +0 -196
- package/lib/slow-query-log/index.js +0 -238
- package/lib/slow-query-log/mongodb-storage.js +0 -324
- package/lib/slow-query-log/query-hash.js +0 -39
- package/lib/sync/ChangeStreamSyncManager.js +0 -405
- package/lib/sync/ResumeTokenStore.js +0 -192
- package/lib/sync/SyncConfig.js +0 -127
- package/lib/sync/SyncTarget.js +0 -240
- package/lib/sync/index.js +0 -20
- package/lib/transaction/CacheLockManager.js +0 -162
- package/lib/transaction/DistributedCacheLockManager.js +0 -475
- package/lib/transaction/Transaction.js +0 -315
- package/lib/transaction/TransactionManager.js +0 -267
- package/lib/transaction/index.js +0 -11
- package/lib/utils/objectid-converter.js +0 -632
- package/types/README.md +0 -122
- package/types/base.ts +0 -94
- package/types/batch.ts +0 -187
- package/types/cache.ts +0 -71
- package/types/chain.ts +0 -254
- package/types/collection.ts +0 -357
- package/types/expression.ts +0 -109
- package/types/function-cache.d.ts +0 -135
- package/types/lock.ts +0 -95
- package/types/model/definition.ts +0 -152
- package/types/model/index.ts +0 -10
- package/types/model/instance.ts +0 -121
- package/types/model/relations.ts +0 -121
- package/types/model/virtuals.ts +0 -32
- package/types/monsqlize.ts +0 -245
- package/types/options.ts +0 -192
- package/types/pagination.ts +0 -154
- package/types/pool.ts +0 -125
- package/types/query.ts +0 -71
- package/types/saga.ts +0 -125
- package/types/stream.ts +0 -64
- package/types/sync.ts +0 -79
- package/types/transaction.ts +0 -79
- package/types/write.ts +0 -77
|
@@ -1,568 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PopulateBuilder - populate 构建器
|
|
3
|
-
*
|
|
4
|
-
* 职责:
|
|
5
|
-
* - 管理 populate 路径配置
|
|
6
|
-
* - 执行关联数据填充
|
|
7
|
-
* - 处理批量查询优化
|
|
8
|
-
*
|
|
9
|
-
* @class PopulateBuilder
|
|
10
|
-
*/
|
|
11
|
-
class PopulateBuilder {
|
|
12
|
-
/**
|
|
13
|
-
* 构造函数
|
|
14
|
-
* @param {Model} model - 所属的 Model 实例
|
|
15
|
-
* @param {Collection} collection - monSQLize collection 实例
|
|
16
|
-
*/
|
|
17
|
-
constructor(model, collection) {
|
|
18
|
-
this.model = model;
|
|
19
|
-
this.collection = collection;
|
|
20
|
-
this.populatePaths = []; // 待填充的路径
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 添加 populate 路径
|
|
25
|
-
* @param {string|Array|Object} path - 路径或配置对象
|
|
26
|
-
* @param {Object} [options={}] - populate 选项
|
|
27
|
-
* @returns {PopulateBuilder} 返回自身,支持链式调用
|
|
28
|
-
*/
|
|
29
|
-
populate(path, options = {}) {
|
|
30
|
-
if (Array.isArray(path)) {
|
|
31
|
-
// 数组形式:populate(['profile', 'posts'])
|
|
32
|
-
path.forEach(p => this.populate(p, options));
|
|
33
|
-
return this;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (typeof path === 'object' && path.path) {
|
|
37
|
-
// 对象形式:populate({ path: 'posts', select: '...' })
|
|
38
|
-
this.populatePaths.push(path);
|
|
39
|
-
} else if (typeof path === 'string') {
|
|
40
|
-
// 字符串形式:populate('profile')
|
|
41
|
-
this.populatePaths.push({ path, ...options });
|
|
42
|
-
} else {
|
|
43
|
-
throw new Error('populate 参数必须是字符串、数组或对象');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return this;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 执行 populate(填充关联数据)
|
|
51
|
-
* @param {Array} docs - 查询结果文档
|
|
52
|
-
* @returns {Promise<Array>} 填充后的文档
|
|
53
|
-
*/
|
|
54
|
-
async execute(docs) {
|
|
55
|
-
// 如果没有文档或没有 populate 路径,直接返回
|
|
56
|
-
if (!docs || docs.length === 0) return docs;
|
|
57
|
-
if (this.populatePaths.length === 0) return docs;
|
|
58
|
-
|
|
59
|
-
// 按顺序执行每个 populate 路径
|
|
60
|
-
for (const populateConfig of this.populatePaths) {
|
|
61
|
-
docs = await this._populatePath(docs, populateConfig);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return docs;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 填充单个路径(核心逻辑)
|
|
69
|
-
* @private
|
|
70
|
-
* @param {Array} docs - 文档数组
|
|
71
|
-
* @param {Object} config - populate 配置
|
|
72
|
-
* @returns {Promise<Array>}
|
|
73
|
-
*/
|
|
74
|
-
async _populatePath(docs, config) {
|
|
75
|
-
const { path, select, sort, limit, skip, match, populate: nestedPopulate } = config;
|
|
76
|
-
|
|
77
|
-
// 1. 获取关系定义
|
|
78
|
-
const relation = this.model._relations.get(path);
|
|
79
|
-
if (!relation) {
|
|
80
|
-
throw new Error(`未定义的关系: ${path}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 2. 收集外键值
|
|
84
|
-
const foreignIds = this._collectForeignIds(docs, relation);
|
|
85
|
-
if (foreignIds.length === 0) {
|
|
86
|
-
// 没有外键值,填充 null 或空数组
|
|
87
|
-
this._fillEmptyRelation(docs, path, relation);
|
|
88
|
-
return docs;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 3. 获取关联的集合
|
|
92
|
-
const relatedCollection = this.model.msq.collection(relation.from);
|
|
93
|
-
|
|
94
|
-
// 4. 构建查询条件
|
|
95
|
-
const query = { [relation.foreignField]: { $in: foreignIds } };
|
|
96
|
-
if (match) {
|
|
97
|
-
Object.assign(query, match);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 5. 查询关联文档
|
|
101
|
-
let relatedDocs = await relatedCollection.find(query).toArray();
|
|
102
|
-
|
|
103
|
-
// 6. 🔴 处理 limit=0 的特殊情况:返回空结果
|
|
104
|
-
if (limit === 0) {
|
|
105
|
-
this._fillEmptyRelation(docs, path, relation);
|
|
106
|
-
return docs;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// 7. 🆕 处理嵌套 populate(深度填充)
|
|
110
|
-
if (nestedPopulate && relatedDocs.length > 0) {
|
|
111
|
-
relatedDocs = await this._executeNestedPopulate(
|
|
112
|
-
relatedDocs,
|
|
113
|
-
nestedPopulate,
|
|
114
|
-
relation.from
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 8. 应用选项(修复:select时保留外键字段)
|
|
119
|
-
if (select) {
|
|
120
|
-
relatedDocs = relatedDocs.map(doc => this._selectFields(doc, select, relation.foreignField));
|
|
121
|
-
}
|
|
122
|
-
if (sort) {
|
|
123
|
-
relatedDocs = this._sortDocs(relatedDocs, sort);
|
|
124
|
-
}
|
|
125
|
-
if (skip || limit) {
|
|
126
|
-
const startIndex = skip || 0;
|
|
127
|
-
const endIndex = limit ? startIndex + limit : relatedDocs.length;
|
|
128
|
-
relatedDocs = relatedDocs.slice(startIndex, endIndex);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 9. 构建映射表
|
|
132
|
-
const relatedMap = this._buildRelationMap(relatedDocs, relation);
|
|
133
|
-
|
|
134
|
-
// 10. 填充文档
|
|
135
|
-
this._fillDocuments(docs, path, relation, relatedMap);
|
|
136
|
-
|
|
137
|
-
return docs;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* 收集外键值
|
|
142
|
-
* @private
|
|
143
|
-
* @param {Array} docs - 文档数组
|
|
144
|
-
* @param {Object} relation - 关系定义
|
|
145
|
-
* @returns {Array} 外键值数组(去重)
|
|
146
|
-
*/
|
|
147
|
-
_collectForeignIds(docs, relation) {
|
|
148
|
-
const ids = new Set();
|
|
149
|
-
|
|
150
|
-
for (const doc of docs) {
|
|
151
|
-
const localValue = doc[relation.localField];
|
|
152
|
-
|
|
153
|
-
if (localValue === null || localValue === undefined) {
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (Array.isArray(localValue)) {
|
|
158
|
-
// 外键数组
|
|
159
|
-
localValue.forEach(id => {
|
|
160
|
-
if (id !== null && id !== undefined) {
|
|
161
|
-
ids.add(String(id));
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
} else {
|
|
165
|
-
// 单个外键
|
|
166
|
-
ids.add(String(localValue));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return Array.from(ids);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* 构建关系映射表
|
|
175
|
-
* @private
|
|
176
|
-
* @param {Array} relatedDocs - 关联文档数组
|
|
177
|
-
* @param {Object} relation - 关系定义
|
|
178
|
-
* @returns {Map} 映射表
|
|
179
|
-
*/
|
|
180
|
-
_buildRelationMap(relatedDocs, relation) {
|
|
181
|
-
const map = new Map();
|
|
182
|
-
|
|
183
|
-
for (const doc of relatedDocs) {
|
|
184
|
-
const key = String(doc[relation.foreignField]);
|
|
185
|
-
|
|
186
|
-
if (relation.single) {
|
|
187
|
-
// single: true - 单文档,直接存储
|
|
188
|
-
map.set(key, doc);
|
|
189
|
-
} else {
|
|
190
|
-
// single: false - 数组,追加存储
|
|
191
|
-
if (!map.has(key)) {
|
|
192
|
-
map.set(key, []);
|
|
193
|
-
}
|
|
194
|
-
map.get(key).push(doc);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return map;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* 填充文档
|
|
203
|
-
* @private
|
|
204
|
-
* @param {Array} docs - 文档数组
|
|
205
|
-
* @param {string} path - 填充路径
|
|
206
|
-
* @param {Object} relation - 关系定义
|
|
207
|
-
* @param {Map} relatedMap - 关系映射表
|
|
208
|
-
*/
|
|
209
|
-
_fillDocuments(docs, path, relation, relatedMap) {
|
|
210
|
-
for (const doc of docs) {
|
|
211
|
-
const localValue = doc[relation.localField];
|
|
212
|
-
|
|
213
|
-
if (localValue === null || localValue === undefined) {
|
|
214
|
-
// 外键为空,填充 null 或空数组
|
|
215
|
-
doc[path] = relation.single ? null : [];
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (relation.single) {
|
|
220
|
-
// single: true - 返回单文档
|
|
221
|
-
const key = String(localValue);
|
|
222
|
-
doc[path] = relatedMap.get(key) || null;
|
|
223
|
-
} else {
|
|
224
|
-
// single: false - 返回数组
|
|
225
|
-
const keys = Array.isArray(localValue)
|
|
226
|
-
? localValue.map(String)
|
|
227
|
-
: [String(localValue)];
|
|
228
|
-
|
|
229
|
-
doc[path] = [];
|
|
230
|
-
for (const key of keys) {
|
|
231
|
-
const related = relatedMap.get(key);
|
|
232
|
-
if (related) {
|
|
233
|
-
if (Array.isArray(related)) {
|
|
234
|
-
doc[path].push(...related);
|
|
235
|
-
} else {
|
|
236
|
-
doc[path].push(related);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* 填充空关系
|
|
246
|
-
* @private
|
|
247
|
-
* @param {Array} docs - 文档数组
|
|
248
|
-
* @param {string} path - 填充路径
|
|
249
|
-
* @param {Object} relation - 关系定义
|
|
250
|
-
*/
|
|
251
|
-
_fillEmptyRelation(docs, path, relation) {
|
|
252
|
-
for (const doc of docs) {
|
|
253
|
-
doc[path] = relation.single ? null : [];
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* 选择字段
|
|
259
|
-
* @private
|
|
260
|
-
* @param {Object} doc - 文档对象
|
|
261
|
-
* @param {string} select - 字段选择器(空格分隔)
|
|
262
|
-
* @param {string} [keepField] - 必须保留的字段(如外键字段)
|
|
263
|
-
* @returns {Object} 选择后的文档
|
|
264
|
-
*/
|
|
265
|
-
_selectFields(doc, select, keepField) {
|
|
266
|
-
const fields = select.split(/\s+/).filter(f => f);
|
|
267
|
-
const result = {};
|
|
268
|
-
|
|
269
|
-
// 始终包含 _id
|
|
270
|
-
if (doc._id !== undefined) {
|
|
271
|
-
result._id = doc._id;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
for (const field of fields) {
|
|
275
|
-
if (doc[field] !== undefined) {
|
|
276
|
-
result[field] = doc[field];
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// 🔴 保留外键字段(用于构建关系映射)
|
|
281
|
-
if (keepField && doc[keepField] !== undefined && !fields.includes(keepField)) {
|
|
282
|
-
result[keepField] = doc[keepField];
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return result;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* 排序文档
|
|
290
|
-
* @private
|
|
291
|
-
* @param {Array} docs - 文档数组
|
|
292
|
-
* @param {Object} sort - 排序规则
|
|
293
|
-
* @returns {Array} 排序后的文档数组
|
|
294
|
-
*/
|
|
295
|
-
_sortDocs(docs, sort) {
|
|
296
|
-
return docs.slice().sort((a, b) => {
|
|
297
|
-
for (const [field, order] of Object.entries(sort)) {
|
|
298
|
-
const aVal = a[field];
|
|
299
|
-
const bVal = b[field];
|
|
300
|
-
|
|
301
|
-
if (aVal < bVal) return order === 1 ? -1 : 1;
|
|
302
|
-
if (aVal > bVal) return order === 1 ? 1 : -1;
|
|
303
|
-
}
|
|
304
|
-
return 0;
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* 执行嵌套 populate(深度填充)
|
|
310
|
-
* @private
|
|
311
|
-
* @param {Array} docs - 关联文档数组
|
|
312
|
-
* @param {string|Array|Object} nestedPopulate - 嵌套 populate 配置
|
|
313
|
-
* @param {string} collectionName - 当前集合名称
|
|
314
|
-
* @returns {Promise<Array>} 填充后的文档
|
|
315
|
-
*/
|
|
316
|
-
async _executeNestedPopulate(docs, nestedPopulate, collectionName) {
|
|
317
|
-
// 1. 获取当前集合对应的 Model 定义
|
|
318
|
-
const Model = require('../../model');
|
|
319
|
-
if (!Model.has(collectionName)) {
|
|
320
|
-
// 集合没有定义 Model,跳过嵌套 populate(不报错,也不添加字段)
|
|
321
|
-
return docs;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// 2. 创建临时 ModelInstance 用于嵌套 populate
|
|
325
|
-
const modelDef = Model.get(collectionName);
|
|
326
|
-
const collection = this.model.msq.collection(collectionName);
|
|
327
|
-
const { ModelInstance } = require('../index');
|
|
328
|
-
const tempModel = new ModelInstance(collection, modelDef.definition, this.model.msq);
|
|
329
|
-
|
|
330
|
-
// 3. 验证嵌套 populate 配置类型
|
|
331
|
-
if (
|
|
332
|
-
typeof nestedPopulate !== 'string' &&
|
|
333
|
-
!Array.isArray(nestedPopulate) &&
|
|
334
|
-
!(typeof nestedPopulate === 'object' && nestedPopulate.path)
|
|
335
|
-
) {
|
|
336
|
-
throw new Error('嵌套 populate 参数必须是字符串、数组或对象');
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// 4. 创建新的 PopulateBuilder
|
|
340
|
-
const nestedBuilder = new PopulateBuilder(tempModel, collection);
|
|
341
|
-
|
|
342
|
-
// 5. 添加嵌套 populate 路径(此时会验证关系是否存在)
|
|
343
|
-
if (Array.isArray(nestedPopulate)) {
|
|
344
|
-
// 数组形式:populate: ['comments', 'likes']
|
|
345
|
-
nestedPopulate.forEach(p => nestedBuilder.populate(p));
|
|
346
|
-
} else if (typeof nestedPopulate === 'object' && nestedPopulate.path) {
|
|
347
|
-
// 对象形式:populate: { path: 'comments', select: '...' }
|
|
348
|
-
nestedBuilder.populate(nestedPopulate);
|
|
349
|
-
} else if (typeof nestedPopulate === 'string') {
|
|
350
|
-
// 字符串形式:populate: 'comments'
|
|
351
|
-
nestedBuilder.populate(nestedPopulate);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// 6. 执行嵌套 populate
|
|
355
|
-
const populatedDocs = await nestedBuilder.execute(docs);
|
|
356
|
-
|
|
357
|
-
return populatedDocs;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* PopulateProxy - populate 代理类
|
|
363
|
-
*
|
|
364
|
-
* 职责:
|
|
365
|
-
* - 提供链式 populate 调用接口
|
|
366
|
-
* - 实现 Promise 接口(then/catch)
|
|
367
|
-
* - 处理单文档和数组文档的返回
|
|
368
|
-
*
|
|
369
|
-
* @class PopulateProxy
|
|
370
|
-
*/
|
|
371
|
-
class PopulateProxy {
|
|
372
|
-
/**
|
|
373
|
-
* 构造函数
|
|
374
|
-
* @param {Array|Promise<Array>} docs - 文档数组或返回文档数组的 Promise
|
|
375
|
-
* @param {PopulateBuilder} builder - PopulateBuilder 实例
|
|
376
|
-
* @param {boolean} [singleDoc=false] - 是否返回单文档
|
|
377
|
-
*/
|
|
378
|
-
constructor(docs, builder, singleDoc = false) {
|
|
379
|
-
this._docsOrPromise = docs;
|
|
380
|
-
this._builder = builder;
|
|
381
|
-
this._singleDoc = singleDoc;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* 获取文档数组(如果是 Promise 则先 await)
|
|
386
|
-
* @private
|
|
387
|
-
* @returns {Promise<Array>}
|
|
388
|
-
*/
|
|
389
|
-
async _getDocs() {
|
|
390
|
-
// 如果是 Promise,先 await
|
|
391
|
-
if (this._docsOrPromise && typeof this._docsOrPromise.then === 'function') {
|
|
392
|
-
const result = await this._docsOrPromise;
|
|
393
|
-
// 标准化为数组
|
|
394
|
-
return result === null ? [] : (Array.isArray(result) ? result : [result]);
|
|
395
|
-
}
|
|
396
|
-
// 否则直接返回
|
|
397
|
-
return this._docsOrPromise;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* 添加 populate 路径(链式调用)
|
|
402
|
-
* @param {string|Array|Object} path - 路径或配置对象
|
|
403
|
-
* @param {Object} [options={}] - populate 选项
|
|
404
|
-
* @returns {PopulateProxy} 返回自身,支持链式调用
|
|
405
|
-
*/
|
|
406
|
-
populate(path, options = {}) {
|
|
407
|
-
this._builder.populate(path, options);
|
|
408
|
-
return this; // 返回自己,支持链式调用
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Promise then 接口
|
|
413
|
-
* @param {Function} resolve - 成功回调
|
|
414
|
-
* @param {Function} reject - 失败回调
|
|
415
|
-
* @returns {Promise}
|
|
416
|
-
*/
|
|
417
|
-
async then(resolve, reject) {
|
|
418
|
-
try {
|
|
419
|
-
// 获取文档(如果是 Promise 则先 await)
|
|
420
|
-
const docs = await this._getDocs();
|
|
421
|
-
|
|
422
|
-
// 执行 populate
|
|
423
|
-
const populatedDocs = await this._builder.execute(docs);
|
|
424
|
-
|
|
425
|
-
// 如果是单文档查询(findOne),返回第一个元素或 null
|
|
426
|
-
// 如果是批量查询(find),返回数组
|
|
427
|
-
const result = this._singleDoc ? (populatedDocs[0] || null) : populatedDocs;
|
|
428
|
-
|
|
429
|
-
return resolve(result);
|
|
430
|
-
} catch (error) {
|
|
431
|
-
return reject ? reject(error) : Promise.reject(error);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Promise catch 接口
|
|
437
|
-
* @param {Function} reject - 失败回调
|
|
438
|
-
* @returns {Promise}
|
|
439
|
-
*/
|
|
440
|
-
catch(reject) {
|
|
441
|
-
return this.then(result => result, reject);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Promise finally 接口
|
|
446
|
-
* @param {Function} onFinally - finally 回调
|
|
447
|
-
* @returns {Promise}
|
|
448
|
-
*/
|
|
449
|
-
finally(onFinally) {
|
|
450
|
-
return this.then(
|
|
451
|
-
result => {
|
|
452
|
-
onFinally();
|
|
453
|
-
return result;
|
|
454
|
-
},
|
|
455
|
-
error => {
|
|
456
|
-
onFinally();
|
|
457
|
-
throw error;
|
|
458
|
-
}
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* SpecialPopulateProxy - 用于 findAndCount 和 findPage 的特殊 PopulateProxy
|
|
465
|
-
*
|
|
466
|
-
* 这些方法返回特殊结构:
|
|
467
|
-
* - findAndCount: { data: [...], total: 100 }
|
|
468
|
-
* - findPage: { data: [...], page: 1, pageSize: 10, total: 100, hasNext: true }
|
|
469
|
-
*
|
|
470
|
-
* 需要只对 data 部分进行 populate,保持其他字段不变
|
|
471
|
-
*
|
|
472
|
-
* @class SpecialPopulateProxy
|
|
473
|
-
*/
|
|
474
|
-
class SpecialPopulateProxy {
|
|
475
|
-
/**
|
|
476
|
-
* 构造函数
|
|
477
|
-
* @param {Promise} queryPromise - 返回特殊结构的查询 Promise
|
|
478
|
-
* @param {PopulateBuilder} builder - PopulateBuilder 实例
|
|
479
|
-
* @param {string} method - 方法名(findAndCount 或 findPage)
|
|
480
|
-
*/
|
|
481
|
-
constructor(queryPromise, builder, method) {
|
|
482
|
-
this._queryPromise = queryPromise;
|
|
483
|
-
this._builder = builder;
|
|
484
|
-
this._method = method;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* 添加 populate 路径(链式调用)
|
|
489
|
-
* @param {string|Array|Object} path - 路径或配置对象
|
|
490
|
-
* @param {Object} [options={}] - populate 选项
|
|
491
|
-
* @returns {SpecialPopulateProxy} 返回自身,支持链式调用
|
|
492
|
-
*/
|
|
493
|
-
populate(path, options = {}) {
|
|
494
|
-
this._builder.populate(path, options);
|
|
495
|
-
return this; // 返回自己,支持链式调用
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Promise then 接口
|
|
500
|
-
* @param {Function} resolve - 成功回调
|
|
501
|
-
* @param {Function} reject - 失败回调
|
|
502
|
-
* @returns {Promise}
|
|
503
|
-
*/
|
|
504
|
-
async then(resolve, reject) {
|
|
505
|
-
try {
|
|
506
|
-
// 1. 获取查询结果(特殊结构)
|
|
507
|
-
const result = await this._queryPromise;
|
|
508
|
-
|
|
509
|
-
// 2. 提取数据部分(智能检测字段名)
|
|
510
|
-
// 优先检查实际存在的字段,以支持mock数据和真实数据
|
|
511
|
-
let dataField, data;
|
|
512
|
-
if (result.items !== undefined) {
|
|
513
|
-
// 真实的findPage返回 { items: [...], pageInfo: {...}, totals: {...} }
|
|
514
|
-
dataField = 'items';
|
|
515
|
-
data = result.items || [];
|
|
516
|
-
} else {
|
|
517
|
-
// findAndCount或mock数据返回 { data: [...], total: 100 }
|
|
518
|
-
// 兜底:如果没有items,使用data字段(即使不存在也没关系)
|
|
519
|
-
dataField = 'data';
|
|
520
|
-
data = result.data || [];
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// 3. 对数据部分执行 populate
|
|
524
|
-
const populatedData = await this._builder.execute(data);
|
|
525
|
-
|
|
526
|
-
// 4. 重新组装结果(保持原结构,只替换数据字段)
|
|
527
|
-
const finalResult = {
|
|
528
|
-
...result,
|
|
529
|
-
[dataField]: populatedData
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
return resolve(finalResult);
|
|
533
|
-
} catch (error) {
|
|
534
|
-
return reject ? reject(error) : Promise.reject(error);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Promise catch 接口
|
|
540
|
-
* @param {Function} reject - 失败回调
|
|
541
|
-
* @returns {Promise}
|
|
542
|
-
*/
|
|
543
|
-
catch(reject) {
|
|
544
|
-
return this.then(result => result, reject);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Promise finally 接口
|
|
549
|
-
* @param {Function} onFinally - finally 回调
|
|
550
|
-
* @returns {Promise}
|
|
551
|
-
*/
|
|
552
|
-
finally(onFinally) {
|
|
553
|
-
return this.then(
|
|
554
|
-
result => {
|
|
555
|
-
onFinally();
|
|
556
|
-
return result;
|
|
557
|
-
},
|
|
558
|
-
error => {
|
|
559
|
-
onFinally();
|
|
560
|
-
throw error;
|
|
561
|
-
}
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
module.exports = { PopulateBuilder, PopulateProxy, SpecialPopulateProxy };
|
|
567
|
-
|
|
568
|
-
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RelationManager - 关系定义管理器
|
|
3
|
-
*
|
|
4
|
-
* 职责:
|
|
5
|
-
* - 注册和管理 Model 之间的关系定义
|
|
6
|
-
* - 验证关系配置的正确性
|
|
7
|
-
* - 提供关系查询接口
|
|
8
|
-
*
|
|
9
|
-
* @class RelationManager
|
|
10
|
-
*/
|
|
11
|
-
class RelationManager {
|
|
12
|
-
/**
|
|
13
|
-
* 构造函数
|
|
14
|
-
* @param {Model} model - 所属的 Model 实例
|
|
15
|
-
*/
|
|
16
|
-
constructor(model) {
|
|
17
|
-
this.model = model;
|
|
18
|
-
this.relations = new Map(); // 关系定义缓存
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 注册关系定义
|
|
23
|
-
* @param {string} name - 关系名称
|
|
24
|
-
* @param {Object} config - 关系配置
|
|
25
|
-
* @param {string} config.from - 关联的集合名称(MongoDB 原生)
|
|
26
|
-
* @param {string} config.localField - 本地字段名
|
|
27
|
-
* @param {string} config.foreignField - 外部字段名
|
|
28
|
-
* @param {boolean} [config.single=false] - 是否返回单个文档
|
|
29
|
-
*/
|
|
30
|
-
define(name, config) {
|
|
31
|
-
this.validate(config); // 验证配置
|
|
32
|
-
this.relations.set(name, this.normalize(config));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 标准化配置(设置默认值)
|
|
37
|
-
* @param {Object} config - 原始配置
|
|
38
|
-
* @returns {Object} 标准化后的配置
|
|
39
|
-
*/
|
|
40
|
-
normalize(config) {
|
|
41
|
-
return {
|
|
42
|
-
from: config.from,
|
|
43
|
-
localField: config.localField,
|
|
44
|
-
foreignField: config.foreignField,
|
|
45
|
-
single: config.single !== undefined ? config.single : false // 默认返回数组
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 获取关系定义
|
|
51
|
-
* @param {string} name - 关系名称
|
|
52
|
-
* @returns {Object|null} 关系配置,不存在返回 null
|
|
53
|
-
*/
|
|
54
|
-
get(name) {
|
|
55
|
-
return this.relations.get(name) || null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* 验证关系配置
|
|
60
|
-
* @param {Object} config - 关系配置
|
|
61
|
-
* @throws {Error} 配置不合法时抛出错误
|
|
62
|
-
*/
|
|
63
|
-
validate(config) {
|
|
64
|
-
// 验证必需字段
|
|
65
|
-
const required = ['from', 'localField', 'foreignField'];
|
|
66
|
-
for (const field of required) {
|
|
67
|
-
if (!config[field]) {
|
|
68
|
-
throw new Error(`relations 配置缺少必需字段: ${field}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 验证 from 必须是字符串(集合名)
|
|
73
|
-
if (typeof config.from !== 'string') {
|
|
74
|
-
throw new Error('relations.from 必须是字符串(集合名称)');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// 验证 localField 和 foreignField 必须是字符串
|
|
78
|
-
if (typeof config.localField !== 'string') {
|
|
79
|
-
throw new Error('relations.localField 必须是字符串');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (typeof config.foreignField !== 'string') {
|
|
83
|
-
throw new Error('relations.foreignField 必须是字符串');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 验证 single 必须是布尔值(如果提供)
|
|
87
|
-
if (config.single !== undefined && typeof config.single !== 'boolean') {
|
|
88
|
-
throw new Error('relations.single 必须是布尔值');
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* 获取所有关系定义
|
|
94
|
-
* @returns {Map} 所有关系定义
|
|
95
|
-
*/
|
|
96
|
-
getAll() {
|
|
97
|
-
return this.relations;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* 检查关系是否存在
|
|
102
|
-
* @param {string} name - 关系名称
|
|
103
|
-
* @returns {boolean}
|
|
104
|
-
*/
|
|
105
|
-
has(name) {
|
|
106
|
-
return this.relations.has(name);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 获取所有关系名称
|
|
111
|
-
* @returns {string[]}
|
|
112
|
-
*/
|
|
113
|
-
getNames() {
|
|
114
|
-
return Array.from(this.relations.keys());
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
module.exports = RelationManager;
|
|
119
|
-
|
|
120
|
-
|